@essrt/physics 5.0.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -83,7 +83,7 @@ day lengths), `cardinal` (solstice/equinox JD, RA, year length), `moon`
83
83
  JD(UT) axis — ecliptic longitude, latitude and distance from the shared
84
84
  Meeus series over framework-native arguments), `eclipse` (geocentric
85
85
  solar/lunar eclipse search over a JD window, greatest-eclipse convention),
86
- `climate` (the L1 orbital-forcing formula) and `planets` (the Fibonacci-law
86
+ `climate` (the L1 orbital-forcing formula) and `planets` (the retired integer-law
87
87
  records and orientation at epoch). Validation targets are refused as
88
88
  inputs — a counterfactual cannot move the goalposts it is judged by.
89
89
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@essrt/physics",
3
- "version": "5.0.0",
3
+ "version": "6.0.0",
4
4
  "description": "ESSRT (Expanding Solar System Resonance Theory) physics core — pure, injectable-constants, no I/O, no globals, no DOM. The complete model incl. fitted coefficients; every published version immutably ships one recorded model identity.",
5
5
  "essrt": {
6
6
  "modelVersion": "v14.0",
@@ -68,6 +68,12 @@ const { createComposedPrecession } = require('../earth/precession-composed.cjs')
68
68
  * @property {(year: number) => number} cycleLodSumAt - gated δLOD sum (incl. swing)
69
69
  * @property {(year: number) => number} swingLodAt - gated swing δLOD alone
70
70
  * @property {(year: number) => number} swingLodRateAt - gated analytic swing rate
71
+ * @property {() => number} precessionPeriodJ2000YearsFn - the model's DERIVED
72
+ * J2000 axial precession period: the certified year-length laws' beat at
73
+ * 2000, sid/(sid − trop) ≈ 25,771.4 yr (plan 06 S5 — one J2000 precession
74
+ * reading; the same anchor the hybrid obliquity self-anchors on). Read
75
+ * LAZILY on the first composed-rate use, never at construction: every
76
+ * runtime's year laws read THIS factory's bases.
71
77
  * @property {(tMa: number) => number} [lEmAtAgeKgm2S] - OPTIONAL time-dependent
72
78
  * Earth-Moon angular momentum (the Driver-1½ solar channels,
73
79
  * recession-history.cjs). Absent → the J2000 constant, which the budget
@@ -117,12 +123,15 @@ function createDeepTimeLod(deps) {
117
123
  return K.holisticYearJ2000 * LOD_s / K.lodNowH13Seconds;
118
124
  }
119
125
 
120
- // The unit H(t) ≡ 13 × the composed lunisolar precession period (plan 06
121
- // D6 → Phase 3): ψ̇(t) = [ω(t)/ω₀]·p₀·[f_S + (1 − f_S)(a₀/a_M(t))³], p₀ =
122
- // 1,296,000/(H₀/13). ONE formula home: earth/precession-composed, built
123
- // here on this factory's own LOD and recession history.
126
+ // The composed lunisolar precession rate (plan 06 D6 → Phase 3 → S5):
127
+ // ψ̇(t) = [ω(t)/ω₀]·p₀·[f_S + (1 − f_S)(a₀/a_M(t))³], p₀ the model's
128
+ // DERIVED J2000 rate 1,296,000/axial0 (the certified year-length laws'
129
+ // beat at 2000 ≈ 25,771.4 yr — NOT 1,296,000/(H₀/13) = 50.245 ″/yr, the
130
+ // fit anchor's reading, 0.086 % slow; S5). ONE formula home:
131
+ // earth/precession-composed, built here on this factory's own LOD and
132
+ // recession history; the anchor resolves lazily on the first rate use.
124
133
  const composed = createComposedPrecession({
125
- p0ArcsecPerYr: 1296000 / (K.holisticYearJ2000 / 13),
134
+ p0ArcsecPerYr: () => 1296000 / deps.precessionPeriodJ2000YearsFn(),
126
135
  solarShare: K.precessionSolarShareJ2000,
127
136
  lodSecondsAtAge,
128
137
  lodJ2000Seconds: K.lodNowH13Seconds,
@@ -131,11 +140,13 @@ function createDeepTimeLod(deps) {
131
140
  yearToTMa: /** @param {number} year */ (year) => (2000 - year) / 1e6,
132
141
  });
133
142
 
134
- /** The unit H(t) = 13·T_p,composed(t) — H is 13 lunisolar precession periods
135
- * at every epoch (plan 06: H is a UNIT). Written as H_era / [f_S + (1 − f_S)
136
- * (a₀/a_M)³] — algebraically 13·1,296,000/ψ̇_composed, spelled in the SAME
137
- * operations as layer0's holisticHCore so the layer0 gate holds the twins
138
- * bit-identical (13·T_p differs by one ULP). ≡ H_era wherever (a₀/a_M)³ ≈ 1.
143
+ /** The unit H(t) = H_era / [f_S + (1 − f_S)(a₀/a_M)³] — the internal unit
144
+ * scales WITH the composed lunisolar precession period (H(t)/T_p(t) =
145
+ * H₀/axial0 = 13.011 at every epoch) but is NOT 13 of them: H₀ was fitted
146
+ * on the perihelion-of-date beat, and the "H = 13·T_p" claim is retired
147
+ * (plan 06 S5; docs/retired-record.md). Spelled in the SAME operations as
148
+ * layer0's holisticHCore so the layer0 gate holds the twins bit-identical.
149
+ * ≡ H_era wherever (a₀/a_M)³ ≈ 1. Identifier kept (plan 06 P4).
139
150
  * @param {number} t_Ma @returns {number|null} */
140
151
  function hAtAge(t_Ma) {
141
152
  const hEra = eraClockHAtAge(t_Ma);
@@ -143,12 +154,20 @@ function createDeepTimeLod(deps) {
143
154
  return hEra === null || term === null ? null : hEra / term;
144
155
  }
145
156
 
146
- /** The composed lunisolar precession rate, ″/yr (= 1,296,000·13/H(t)).
157
+ /** The composed lunisolar precession rate, ″/yr — p₀·[ω/ω₀]·[f_S + (1 − f_S)
158
+ * (a₀/a_M)³] on the derived J2000 anchor (S5). ≡ 1,296,000·(H₀/axial0)/H(t).
147
159
  * @param {number} t_Ma @returns {number|null} */
148
160
  function lunisolarPrecessionRateArcsecPerYrAtAge(t_Ma) {
149
161
  return composed.composedRateArcsecPerYrAtAge(t_Ma);
150
162
  }
151
163
 
164
+ /** The mean lunisolar precession period T_p(t), years — 1,296,000/ψ̇(t);
165
+ * 25,771.4 at J2000 (the model's one J2000 precession reading, S5).
166
+ * @param {number} t_Ma @returns {number|null} */
167
+ function lunisolarPrecessionPeriodYearsAtAge(t_Ma) {
168
+ return composed.composedPeriodYearsAtAge(t_Ma);
169
+ }
170
+
152
171
  /** Sidereal year seconds (Kepler under linear mass loss, dT/T = −2 dM/M).
153
172
  * @param {number} t_Ma @returns {number} */
154
173
  function siderealYearSecondsAtAge(t_Ma) {
@@ -165,7 +184,16 @@ function createDeepTimeLod(deps) {
165
184
  return K.meanSiderealYearJ2000Seconds * (1 - mass_loss_fraction) * (1 - mass_loss_fraction);
166
185
  }
167
186
 
168
- /** @param {number} t_Ma @returns {number} tropical = sidereal · (1 − 13/H(t)) */
187
+ /** @param {number} t_Ma @returns {number} tropical = sidereal · (1 − 13/H(t)).
188
+ * The 13/H(t) here is the unit's CALENDAR convention (one turn per H₀/13
189
+ * of the unit — the kinematic day/year identities on H/(H − 13) and the
190
+ * deep JD↔year calendar ride it), NOT a precession claim: the published
191
+ * precession period is the composed T_p (S5), 0.086 % apart. Kept
192
+ * unchanged in S5 so nothing certified moves (calendar, goldens, CSV);
193
+ * plan-06 D2 DECIDED: relabel, not re-base — the 0.086 % is the fit
194
+ * anchor's convention (H₀/T_p = 13.011, constant at every epoch beyond
195
+ * ±2 Myr, measured), and re-basing would move every comb coefficient
196
+ * for a rounding-level gain. This tier stays a named device. */
169
197
  function tropicalYearSecondsAtAge(t_Ma) {
170
198
  const sidSec = siderealYearSecondsAtAge(t_Ma);
171
199
  const Ht = hAtAge(t_Ma);
@@ -331,7 +359,7 @@ function createDeepTimeLod(deps) {
331
359
 
332
360
  return {
333
361
  lodSecondsAtAge, lodSecondsAtAgeWithAlpha, lodHoursAtAge, hAtAge,
334
- lunisolarPrecessionRateArcsecPerYrAtAge,
362
+ lunisolarPrecessionRateArcsecPerYrAtAge, lunisolarPrecessionPeriodYearsAtAge,
335
363
  // the composition's terms (plan 06 Phase 3 S3, the lunisolar surface): (a₀/a_M)³ and f_S + (1 − f_S)(a₀/a_M)³
336
364
  lunarTorqueFactorAtAge: composed.lunarTorqueFactorAtAge,
337
365
  precessionTorqueTermAtAge: composed.torqueTermAtAge,
@@ -12,15 +12,25 @@
12
12
  * derived from the shared constants (two-body point-mass torques with the
13
13
  * (1 − e²)^(−3/2) eccentricity factors and the lunar-inclination factor
14
14
  * 1 − 3/2·sin² i_M; ≈ 0.316, the textbook value). At J2000 every factor is 1.
15
- * Plan 06 Phase 3: the unit H(t) IS 13 of these periods at every epoch
16
- * (deltat/deep-time.cjs hAtAge builds on this factory), so H(t)/13 and the
17
- * composed period are one quantity. The pre-Phase-3 structural clock
18
- * 13·1,296,000/H_era(t) (pure ω-scaling, H_era = H₀·LOD/LOD₀) survives only
19
- * as the FROZEN ERA CLOCK's named phase convention (eraClockHAtAge) and in
20
- * docs/retired-record.md, with its record: it read 70.9 ″/yr against
21
- * Lantink et al. 2022's 108.6 ± 8.5 at 2.46 Ga and 65.2 against Meyers &
22
- * Malinverno 2018's 85.79 ± 2.72 at 1.4 Ga, where the composed rate reads
23
- * 104.5 and 86.5.
15
+ *
16
+ * THE ANCHOR p₀ (plan 06 S5 — one J2000 precession reading): p₀ is the
17
+ * model's DERIVED J2000 rate, 1,296,000/axial0 with axial0 the certified
18
+ * year-length laws' beat at 2000 (sid/(sid − trop) = 25,771.4 yr = 50.2883
19
+ * ″/yr, IAU 50.2879 to 8×10⁻⁶) — the SAME anchor the hybrid obliquity
20
+ * self-anchors on. It is NOT 1,296,000/(H/13): H₀ = 335,317 was fitted on
21
+ * the 1246 AD perihelion–solstice alignment (the perihelion-of-date beat),
22
+ * so H₀/13 = 25,793.6 yr is the fit anchor's reading, 0.086 % slow, and
23
+ * is not a period of anything the model computes (nor a window mean: the
24
+ * published period averages 25,598 yr over ±26 kyr). The unit H(t) =
25
+ * H_era/[torque term] (deltat/deep-time.cjs hAtAge) scales WITH the
26
+ * composed period — H(t)/T_p(t) = H₀/axial0 = 13.011 at every epoch — but
27
+ * is not 13 of them; the 13 is a fit-era label that leaves the physics.
28
+ * The pre-Phase-3 structural clock 13·1,296,000/H_era(t) (pure ω-scaling,
29
+ * H_era = H₀·LOD/LOD₀) survives only as the FROZEN ERA CLOCK's named phase
30
+ * convention (eraClockHAtAge) and in docs/retired-record.md, with its
31
+ * record: it read 70.9 ″/yr against Lantink et al. 2022's 108.6 ± 8.5 at
32
+ * 2.46 Ga and 65.2 against Meyers & Malinverno 2018's 85.79 ± 2.72 at
33
+ * 1.4 Ga, where the composed rate reads 104.6 and 86.6.
24
34
  *
25
35
  * Pure factory: the tidal chain is injected (each runtime's own LOD and
26
36
  * moon-distance evaluators), no artifact reads, no globals.
@@ -47,15 +57,18 @@ function computeSolarTorqueShare(c) {
47
57
 
48
58
  /**
49
59
  * @param {{
50
- * p0ArcsecPerYr: number,
60
+ * p0ArcsecPerYr: number | (() => number),
51
61
  * solarShare: number,
52
62
  * lodSecondsAtAge: (tMa: number) => (number | null),
53
63
  * lodJ2000Seconds: number,
54
64
  * moonDistanceMetresAtAge: (tMa: number) => (number | null),
55
65
  * moonDistanceJ2000Metres: number,
56
66
  * yearToTMa: (year: number) => number,
57
- * }} deps - p0 = 1,296,000/(H/13) (the model's J2000 rate); lodJ2000Seconds
58
- * the SAME day basis lodSecondsAtAge(0) returns.
67
+ * }} deps - p0 = the model's derived J2000 rate, 1,296,000/axial0 (S5; a
68
+ * number or a LAZY getter — the callers' certified year laws read the
69
+ * deep-time factory this is built inside, so the anchor is resolved on
70
+ * the first RATE use, never at construction; the torque term needs no
71
+ * anchor); lodJ2000Seconds the SAME day basis lodSecondsAtAge(0) returns.
59
72
  * @returns {{
60
73
  * composedRateArcsecPerYrAtAge: (tMa: number) => (number | null),
61
74
  * composedPeriodYearsAtAge: (tMa: number) => (number | null),
@@ -69,7 +82,18 @@ function computeSolarTorqueShare(c) {
69
82
  * }}
70
83
  */
71
84
  function createComposedPrecession(deps) {
72
- const { p0ArcsecPerYr: p0, solarShare: fS } = deps;
85
+ const { solarShare: fS } = deps;
86
+ /** @type {number|undefined} */
87
+ let p0Memo;
88
+ /** The J2000 anchor, resolved once on first use (lazy getter allowed). */
89
+ const p0 = () => {
90
+ if (p0Memo === undefined) {
91
+ const v = typeof deps.p0ArcsecPerYr === 'function' ? deps.p0ArcsecPerYr() : deps.p0ArcsecPerYr;
92
+ if (!(v > 0)) throw new Error(`createComposedPrecession: p0ArcsecPerYr must resolve to a positive rate, got ${v}`);
93
+ p0Memo = v;
94
+ }
95
+ return p0Memo;
96
+ };
73
97
  /** @param {number} tMa */
74
98
  const lunarTorqueFactorAtAge = (tMa) => {
75
99
  const a = deps.moonDistanceMetresAtAge(tMa);
@@ -88,7 +112,7 @@ function createComposedPrecession(deps) {
88
112
  const lod = deps.lodSecondsAtAge(tMa);
89
113
  const term = torqueTermAtAge(tMa);
90
114
  if (lod === null || !(lod > 0) || term === null) return null;
91
- return (deps.lodJ2000Seconds / lod) * p0 * term;
115
+ return (deps.lodJ2000Seconds / lod) * p0() * term;
92
116
  };
93
117
  /** @param {number} tMa */
94
118
  const composedPeriodYearsAtAge = (tMa) => {
@@ -96,13 +120,13 @@ function createComposedPrecession(deps) {
96
120
  return r === null ? null : 1296000 / r;
97
121
  };
98
122
  /** ψ̇(t)/ψ̇₀ — the factor a consumer scales ITS OWN certified J2000
99
- * anchor by (the hybrids anchor on the of-date beat 25,771.4 yr, not on
100
- * p₀'s 25,793; the ratio is what D6 changes, the anchor stays theirs).
101
- * ≡ 1 at J2000; null out of the tidal chain's domain.
123
+ * anchor by (since S5 p₀ and the hybrids' anchor are the same derived
124
+ * value, 25,771.4 yr; the ratio is what D6 changes). ≡ 1 at J2000; null
125
+ * out of the tidal chain's domain.
102
126
  * @param {number} tMa */
103
127
  const composedRateRatioAtAge = (tMa) => {
104
128
  const r = composedRateArcsecPerYrAtAge(tMa);
105
- return r === null ? null : r / p0;
129
+ return r === null ? null : r / p0();
106
130
  };
107
131
  return {
108
132
  composedRateArcsecPerYrAtAge,
@@ -113,7 +137,7 @@ function createComposedPrecession(deps) {
113
137
  torqueTermAtAge,
114
138
  lunarTorqueFactorAtAge,
115
139
  solarShare: fS,
116
- p0ArcsecPerYr: p0,
140
+ get p0ArcsecPerYr() { return p0(); },
117
141
  };
118
142
  }
119
143
 
package/src/index.js CHANGED
@@ -227,19 +227,22 @@ export const createModel = (constants = GENERATED, opts = {}) => {
227
227
  hash,
228
228
 
229
229
  /**
230
- * H-lattice periods — the structural identities, in years.
230
+ * The fit-era lattice identities H/13, H/3, H/16, in years — DEVICE tier.
231
231
  *
232
- * PURE ALGEBRA OVER THE CONTEXT. No epoch, no formula, no fit. These are
233
- * the divisor relationships H/13, H/3, H/16 that define the lattice, and
234
- * they exist here in Phase 5 for one reason: a hash-only counterfactual
235
- * test would still pass if `createModel` ignored its argument entirely.
236
- * Something has to READ the context and return a number for injection to be
237
- * demonstrated end to end.
232
+ * PURE ALGEBRA OVER THE CONTEXT. No epoch, no formula, no fit. They exist
233
+ * here for one reason: a hash-only counterfactual test would still pass if
234
+ * `createModel` ignored its argument entirely. Something has to READ the
235
+ * context and return a number for injection to be demonstrated end to end.
238
236
  *
239
- * The motion model is Phase 6. This is not it.
237
+ * These are NOT the published periods (plan 06 S5): the axial precession
238
+ * period is `epoch.axialPrecessionYearsAtYear` (25,771.4 yr at J2000, the
239
+ * of-date year laws' beat); H/13 = 25,793.6 is the fit anchor's reading,
240
+ * 0.086 % slow — H₀ was fitted on the perihelion-of-date beat. Likewise
241
+ * H/3 and H/16 against the chain's apsidal and perihelion-of-date periods.
242
+ * The property names keep their historical form (plan 06 P4).
240
243
  *
241
244
  * `divisor` and `period` are never interchangeable (CLAUDE.md): 13 is the
242
- * divisor, H/13 years is the period. The names say which.
245
+ * divisor, H/13 years is the identity's value. The names say which.
243
246
  *
244
247
  * @returns {{axialPrecessionPeriodYears: number, inclinationPrecessionPeriodYears: number, perihelionPrecessionPeriodYears: number}}
245
248
  */
@@ -149,7 +149,10 @@ export const createEpochPrimitives = ({ params: p, alphaAtAgeMa }) => {
149
149
  };
150
150
 
151
151
  /**
152
- * Tropical year — sidereal less one axial-precession turn, H/13.
152
+ * Tropical year — sidereal less one calendar turn of the unit, 13/H(t): the
153
+ * unit's CALENDAR convention (plan 06 S5 — a device identity, 0.086 % from
154
+ * one turn per the published precession period; D2 decided: it stays a
155
+ * named device, never re-based).
153
156
  * Falls back to the J2000 H past the asymptote, matching the shipped chain.
154
157
  * @param {number} t age in Ma
155
158
  * @returns {number} seconds
package/src/model.js CHANGED
@@ -285,6 +285,9 @@ export function assembleModel(C, F, laws = {}, secularSeriesArtifact = /** @type
285
285
  },
286
286
  moonDistanceMetresAtAge,
287
287
  moiFactorAtAge: earthMoiFactorAtAge,
288
+ // S5: the composed clock's J2000 anchor — the certified year laws' beat
289
+ // at 2000 (ONE home below, shared with the hybrid's self-anchor); lazy.
290
+ precessionPeriodJ2000YearsFn: () => certifiedAxialPrecessionJ2000Years(),
288
291
  siderealYearDaysFourierAt: (year) => evalSiderealYearFourierIAU(year),
289
292
  cycleLodSumAt: dtCycleLodCorrectionSum,
290
293
  swingLodAt: (year) => dtCycles.swingLodSecondsAt(year),
@@ -435,6 +438,24 @@ export function assembleModel(C, F, laws = {}, secularSeriesArtifact = /** @type
435
438
  const days = deepLod.eraClockYearInDaysAtAge(yearToTMa(year));
436
439
  return days === null ? meanSolarYearDays : days;
437
440
  };
441
+ /** THE J2000 precession anchor (plan 06 S5 — one J2000 precession
442
+ * reading): the certified of-date year laws at 2000, sid/(sid − trop) =
443
+ * 25,771.4 yr = 50.2883 ″/yr (IAU 50.2879 to 8×10⁻⁶) — identical to the
444
+ * engine/browser Direct twins. ONE home: the composed clock's p₀, the
445
+ * hybrid's self-anchor and the lunisolar surface all read it. The former
446
+ * H/13 reading (25,793.6) is the fit anchor's, not a period. Memoized;
447
+ * hoisted so the deep-time factory's lazy dep can name it.
448
+ * @returns {number} */
449
+ function certifiedAxialPrecessionJ2000Years() {
450
+ if (certifiedAxial0Memo === undefined) {
451
+ const sidLaw2000 = evalYearFourier(2000, siderealYearDaysBase(2000), F.SIDEREAL_YEAR_HARMONICS);
452
+ const solLaw2000 = evalYearFourier(2000, tropicalYearDaysBase(2000), F.TROPICAL_YEAR_HARMONICS);
453
+ certifiedAxial0Memo = sidLaw2000 / (sidLaw2000 - solLaw2000);
454
+ }
455
+ return certifiedAxial0Memo;
456
+ }
457
+ /** @type {number|undefined} */
458
+ let certifiedAxial0Memo;
438
459
  /** @param {number} year @returns {number} */
439
460
  const anomalisticYearDaysBase = (year) => {
440
461
  const tMa = yearToTMa(year);
@@ -524,9 +545,7 @@ export function assembleModel(C, F, laws = {}, secularSeriesArtifact = /** @type
524
545
  // movement's realized precession 25 yr off the certified value — the
525
546
  // hidden split behind the API's 365.242204 tropical mean (found in the
526
547
  // owner's one-implementation drive).
527
- const sidLaw2000 = evalYearFourier(2000, siderealYearDaysBase(2000), F.SIDEREAL_YEAR_HARMONICS);
528
- const solLaw2000 = evalYearFourier(2000, tropicalYearDaysBase(2000), F.TROPICAL_YEAR_HARMONICS);
529
- const axial0 = sidLaw2000 / (sidLaw2000 - solLaw2000);
548
+ const axial0 = certifiedAxialPrecessionJ2000Years(); // S5: the ONE home
530
549
  // D6 → Phase 3 (plan 06): the deep-time ψ̇(t) the hybrid precesses on is
531
550
  // the COMPOSED lunisolar rate, and since Phase 3 the unit H(t) IS
532
551
  // 13 × that period (deltat/deep-time.cjs hAtAge), so the injection is
@@ -1216,19 +1235,31 @@ export function assembleModel(C, F, laws = {}, secularSeriesArtifact = /** @type
1216
1235
  // ── The assembled surface ─────────────────────────────────────────────────
1217
1236
  // Plan 06 Phase 3 S2 — THE published of-date precession period and mean
1218
1237
  // tropical year: the one-family route (B) inside the banked tiers
1219
- // (|Δyear| ≤ ONE_FAMILY_WINDOW_YEARS), the unit's secular mean beyond
1220
- // (H(t)/13; the tidal-chain year). Shared by `epoch` and `lunisolar`.
1238
+ // (|Δyear| ≤ ONE_FAMILY_WINDOW_YEARS), the composed lunisolar period
1239
+ // beyond (S5: on the derived J2000 anchor, so the two faces agree at
1240
+ // 25,771.4 at J2000 and the deep tail is the same clock scaled; the
1241
+ // tidal-chain year). Shared by `epoch` and `lunisolar`.
1221
1242
  /** @param {number} year @returns {number} */
1222
1243
  const publishedAxialPrecessionYearsAtYear = (year) => {
1223
1244
  if (Math.abs(year - 2000) <= ONE_FAMILY_WINDOW_YEARS) return yearLengthsM.axialPrecessionYearsAtYear(year);
1224
- const h = deepLod.hAtAge(yearToTMa(year));
1225
- return (h === null ? H : h) / 13;
1245
+ const p = deepLod.lunisolarPrecessionPeriodYearsAtAge(yearToTMa(year));
1246
+ return p === null ? certifiedAxialPrecessionJ2000Years() : p;
1226
1247
  };
1227
1248
  /** @param {number} year @returns {number} */
1228
1249
  const publishedTropicalYearSecondsAtYear = (year) => {
1229
1250
  if (Math.abs(year - 2000) <= ONE_FAMILY_WINDOW_YEARS) return yearLengthsM.tropicalYearSecondsAtYear(year);
1230
1251
  return deepLod.tropicalYearSecondsAtAge(yearToTMa(year));
1231
1252
  };
1253
+ // Plan 06 S6 — |s₃|, the dominant nodal mode of Earth's orbit: the
1254
+ // largest-amplitude ζ mode of the banked deep secular modes (the recipe
1255
+ // the registry's `eclPrecYears`/`obliqCycleYears` and the browser's
1256
+ // obliquity-beat helper use). ONE home here; the API reads it.
1257
+ const s3ArcsecPerYr = (() => {
1258
+ const z = DEEP_MODES_ARTIFACT.earthZeta
1259
+ .filter((m) => Math.abs(m.omegaRadPerYr) > 1e-9)
1260
+ .sort((a, b) => Math.hypot(b.re, b.im) - Math.hypot(a.re, a.im))[0];
1261
+ return Math.abs((z.omegaRadPerYr * 180) / Math.PI) * 3600;
1262
+ })();
1232
1263
 
1233
1264
  return Object.freeze({
1234
1265
  time: Object.freeze({
@@ -1251,7 +1282,7 @@ export function assembleModel(C, F, laws = {}, secularSeriesArtifact = /** @type
1251
1282
  // one-family route (B): the hybrid's own equinox regression against
1252
1283
  // the λ̇-corrected sidereal year (~25,771 yr at J2000 ≈ IAU), inside
1253
1284
  // the banked tiers (|Δyear| ≤ ONE_FAMILY_WINDOW_YEARS); beyond, the
1254
- // unit's SECULAR mean H(t)/13 = the composed lunisolar period (the
1285
+ // composed lunisolar period on the same J2000 anchor (S5; the
1255
1286
  // of-date wobble is unresolved there). The comb family's beat
1256
1287
  // sid/(sid − tropicalYearDirectDays) — the frozen era clock's
1257
1288
  // kinematic-day device — is no longer published here; it still
@@ -1263,24 +1294,23 @@ export function assembleModel(C, F, laws = {}, secularSeriesArtifact = /** @type
1263
1294
  // one-family inside the window, the tidal-chain mean beyond.
1264
1295
  tropicalYearSecondsAtYear: publishedTropicalYearSecondsAtYear,
1265
1296
  }),
1266
- // Plan 06 Phase 3 S3 — the lunisolar precession clock as a first-class
1267
- // surface: Earth's spin clock, ONE home for its published faces, spoken
1268
- // in PERIODS and their RATIOS — no unit, no integer. T_p(t) is the
1269
- // composed lunisolar period (calculation map 2.1 step 6; the internal
1270
- // identifier H is 13·T_p by definition and is not a face here); the
1271
- // composition's terms; the of-date beat (the published family, seamed
1272
- // at ±2 Myr); the apsidal period from the engine-D chain's secular
1273
- // tangent and the two ratios the owner found wandering by hand —
1274
- // T_aps/T_p (4.33 at J2000, 0.84 … 9.5 across ±26 kyr) and T_peri/T_p
1275
- // with T_peri = 1/(1/T_p + 1/T_aps) the perihelion-of-date period
1276
- // (0.812 at J2000). That these read close to 13/3 and 13/16 today is
1277
- // the J2000 reading, stated nowhere as structure.
1297
+ // Plan 06 Phase 3 S3 → S5 — the lunisolar precession clock as a
1298
+ // first-class surface: Earth's spin clock, ONE home for its published
1299
+ // faces, spoken in PERIODS and their RATIOS — no unit, no integer.
1300
+ // ONE J2000 reading (S5): every face reads 25,771.4 yr at J2000 — the
1301
+ // of-date period (the published family, seamed at ±2 Myr) and the
1302
+ // composed mean T_p(t) (the deep-time clock, anchored on the same
1303
+ // derived value). The composition's terms; the apsidal period from the
1304
+ // engine-D chain's secular tangent; the two ratios the owner found
1305
+ // wandering by hand — T_aps/T_p (4.33 at J2000, 0.84 … 9.9 across
1306
+ // ±26 kyr) and T_peri/T_p with T_peri = 1/(1/T_p + 1/T_aps) the
1307
+ // perihelion-of-date period (0.812 at J2000) — on the of-date T_p.
1278
1308
  lunisolar: Object.freeze({
1279
- /** The mean lunisolar precession period T_p(t), years — the composed torque rate's period. @param {number} year @returns {number} */
1280
- meanPeriodYearsAtYear: (year) => { const h = deepLod.hAtAge(yearToTMa(year)); return (h === null ? H : h) / 13; },
1281
- /** The composed rate ψ̇(t) = [ω/ω₀]·p₀·[f_S + (1 − f_S)(a₀/a_M)³], ″/yr. @param {number} year @returns {number} */
1282
- meanRateArcsecPerYrAtYear: (year) => { const r = deepLod.lunisolarPrecessionRateArcsecPerYrAtAge(yearToTMa(year)); return r === null ? 1296000 / (H / 13) : r; },
1283
- /** The of-date period — the one-family beat inside ±2 Myr, the mean beyond. @param {number} year @returns {number} */
1309
+ /** The mean lunisolar precession period T_p(t), years — the composed torque rate's period on the derived J2000 anchor (25,771.4 at J2000). @param {number} year @returns {number} */
1310
+ meanPeriodYearsAtYear: (year) => { const p = deepLod.lunisolarPrecessionPeriodYearsAtAge(yearToTMa(year)); return p === null ? certifiedAxialPrecessionJ2000Years() : p; },
1311
+ /** The composed rate ψ̇(t) = [ω/ω₀]·p₀·[f_S + (1 − f_S)(a₀/a_M)³], ″/yr (p₀ = 50.2883, derived). @param {number} year @returns {number} */
1312
+ meanRateArcsecPerYrAtYear: (year) => { const r = deepLod.lunisolarPrecessionRateArcsecPerYrAtAge(yearToTMa(year)); return r === null ? 1296000 / certifiedAxialPrecessionJ2000Years() : r; },
1313
+ /** The of-date period — the one-family beat inside ±2 Myr, the composed mean beyond. @param {number} year @returns {number} */
1284
1314
  ofDatePeriodYearsAtYear: publishedAxialPrecessionYearsAtYear,
1285
1315
  /** f_S, the solar fraction of the J2000 precession torque. */
1286
1316
  solarShareJ2000: EPOCH_PARAMS.precessionSolarShareJ2000,
@@ -1288,28 +1318,35 @@ export function assembleModel(C, F, laws = {}, secularSeriesArtifact = /** @type
1288
1318
  lunarTorqueFactorAtYear: (year) => { const f = deepLod.lunarTorqueFactorAtAge(yearToTMa(year)); return f === null ? 1 : f; },
1289
1319
  /** f_S + (1 − f_S)(a₀/a_M)³ — the torque term the unit divides H_era by. @param {number} year @returns {number} */
1290
1320
  torqueTermAtYear: (year) => { const t = deepLod.precessionTorqueTermAtAge(yearToTMa(year)); return t === null ? 1 : t; },
1291
- /** The hybrid's precession constant α = p₀ / cos ε₀, ″/yr (ε₀ the J2000 obliquity input). */
1292
- torqueConstantJ2000ArcsecPerYr: (1296000 / (H / 13)) / Math.cos((C.earthOrbital.obliquityJ2000_deg * Math.PI) / 180),
1321
+ /** The hybrid's precession constant α = p₀ / cos ε₀, ″/yr (p₀ the derived J2000 rate, ε₀ the J2000 obliquity input; 54.81). */
1322
+ torqueConstantJ2000ArcsecPerYr: (1296000 / certifiedAxialPrecessionJ2000Years()) / Math.cos((C.earthOrbital.obliquityJ2000_deg * Math.PI) / 180),
1323
+ /** |s₃|, the dominant nodal mode of Earth's orbit — the largest-amplitude ζ mode of the banked deep secular modes, ″/yr (18.85; the obliquity beat's partner). ONE home for the registry, the browser and the API. */
1324
+ nodalModeS3ArcsecPerYr: s3ArcsecPerYr,
1325
+ /** The ecliptic (nodal) precession period 1,296,000/|s₃|, years (68,751) — an orbital quantity, fixed at every epoch at the two-body level. */
1326
+ nodalPeriodYears: 1296000 / s3ArcsecPerYr,
1327
+ /** The obliquity beat 2π/(ψ̇(t) − |s₃|), years — the SHIPPED deep-time obliquity period (falsification leg 1; 41,224 at J2000, on the composed rate). @param {number} year @returns {number} */
1328
+ obliquityBeatYearsAtYear: (year) => {
1329
+ const r = deepLod.lunisolarPrecessionRateArcsecPerYrAtAge(yearToTMa(year));
1330
+ const psiDot = r === null ? 1296000 / certifiedAxialPrecessionJ2000Years() : r;
1331
+ return 1296000 / (psiDot - s3ArcsecPerYr);
1332
+ },
1293
1333
  /** The apsidal (perihelion vs the stars) period from the engine-D chain's secular tangent, years — inside the published window only (the tangent is an extrapolation beyond the banked series: it turns negative at −5 Myr); null beyond. @param {number} year @returns {number|null} */
1294
1334
  apsidalPeriodYearsAtYear: (year) => (Math.abs(year - 2000) <= ONE_FAMILY_WINDOW_YEARS ? 360 / computeApsidalSecularDegPerYr(year, kcChainsM.earth, kcChainsM) : null),
1295
- /** T_aps(t) / T_p(t) — the apsidal period in mean precession periods (4.33 at J2000, a fitted reading; 0.84 … 9.5 across ±26 kyr, measured); null beyond the published window. @param {number} year @returns {number|null} */
1335
+ /** T_aps(t) / T_p(t) — the apsidal period in of-date precession periods (4.33 at J2000, a reading; 0.84 … 9.9 across ±26 kyr, measured); null beyond the published window. @param {number} year @returns {number|null} */
1296
1336
  apsidalPerPrecessionAtYear: (year) => {
1297
1337
  if (Math.abs(year - 2000) > ONE_FAMILY_WINDOW_YEARS) return null;
1298
- const h = deepLod.hAtAge(yearToTMa(year));
1299
- return (360 / computeApsidalSecularDegPerYr(year, kcChainsM.earth, kcChainsM)) / ((h === null ? H : h) / 13);
1338
+ return (360 / computeApsidalSecularDegPerYr(year, kcChainsM.earth, kcChainsM)) / publishedAxialPrecessionYearsAtYear(year);
1300
1339
  },
1301
- /** T_peri(t) = 1/(1/T_p + 1/T_aps) — the perihelion-of-date period (equinox precession + inertial perihelion motion, frame arithmetic at every epoch), years; null beyond the published window. @param {number} year @returns {number|null} */
1340
+ /** T_peri(t) = 1/(1/T_p + 1/T_aps) — the perihelion-of-date period (equinox precession + inertial perihelion motion, frame arithmetic at every epoch), years, on the of-date T_p; null beyond the published window. @param {number} year @returns {number|null} */
1302
1341
  periOfDatePeriodYearsAtYear: (year) => {
1303
1342
  if (Math.abs(year - 2000) > ONE_FAMILY_WINDOW_YEARS) return null;
1304
- const h = deepLod.hAtAge(yearToTMa(year));
1305
- const tp = (h === null ? H : h) / 13, taps = 360 / computeApsidalSecularDegPerYr(year, kcChainsM.earth, kcChainsM);
1343
+ const tp = publishedAxialPrecessionYearsAtYear(year), taps = 360 / computeApsidalSecularDegPerYr(year, kcChainsM.earth, kcChainsM);
1306
1344
  return 1 / (1 / tp + 1 / taps);
1307
1345
  },
1308
1346
  /** T_peri(t) / T_p(t) (0.812 at J2000 — the J2000 reading); null beyond the published window. @param {number} year @returns {number|null} */
1309
1347
  periOfDatePerPrecessionAtYear: (year) => {
1310
1348
  if (Math.abs(year - 2000) > ONE_FAMILY_WINDOW_YEARS) return null;
1311
- const h = deepLod.hAtAge(yearToTMa(year));
1312
- const tp = (h === null ? H : h) / 13, taps = 360 / computeApsidalSecularDegPerYr(year, kcChainsM.earth, kcChainsM);
1349
+ const tp = publishedAxialPrecessionYearsAtYear(year), taps = 360 / computeApsidalSecularDegPerYr(year, kcChainsM.earth, kcChainsM);
1313
1350
  return (1 / (1 / tp + 1 / taps)) / tp;
1314
1351
  },
1315
1352
  /** The published of-date window, years from 2000. */
@@ -62,6 +62,15 @@ export type DeepTimeLodDeps = {
62
62
  * - gated analytic swing rate
63
63
  */
64
64
  swingLodRateAt: (year: number) => number;
65
+ /**
66
+ * - the model's DERIVED
67
+ * J2000 axial precession period: the certified year-length laws' beat at
68
+ * 2000, sid/(sid − trop) ≈ 25,771.4 yr (plan 06 S5 — one J2000 precession
69
+ * reading; the same anchor the hybrid obliquity self-anchors on). Read
70
+ * LAZILY on the first composed-rate use, never at construction: every
71
+ * runtime's year laws read THIS factory's bases.
72
+ */
73
+ precessionPeriodJ2000YearsFn: () => number;
65
74
  /**
66
75
  * - OPTIONAL time-dependent
67
76
  * Earth-Moon angular momentum (the Driver-1½ solar channels,
@@ -99,6 +108,12 @@ export type DeepTimeLodDeps = {
99
108
  * @property {(year: number) => number} cycleLodSumAt - gated δLOD sum (incl. swing)
100
109
  * @property {(year: number) => number} swingLodAt - gated swing δLOD alone
101
110
  * @property {(year: number) => number} swingLodRateAt - gated analytic swing rate
111
+ * @property {() => number} precessionPeriodJ2000YearsFn - the model's DERIVED
112
+ * J2000 axial precession period: the certified year-length laws' beat at
113
+ * 2000, sid/(sid − trop) ≈ 25,771.4 yr (plan 06 S5 — one J2000 precession
114
+ * reading; the same anchor the hybrid obliquity self-anchors on). Read
115
+ * LAZILY on the first composed-rate use, never at construction: every
116
+ * runtime's year laws read THIS factory's bases.
102
117
  * @property {(tMa: number) => number} [lEmAtAgeKgm2S] - OPTIONAL time-dependent
103
118
  * Earth-Moon angular momentum (the Driver-1½ solar channels,
104
119
  * recession-history.cjs). Absent → the J2000 constant, which the budget
@@ -111,6 +126,7 @@ export function createDeepTimeLod(deps: DeepTimeLodDeps): {
111
126
  lodHoursAtAge: (t_Ma: number) => number | null;
112
127
  hAtAge: (t_Ma: number) => number | null;
113
128
  lunisolarPrecessionRateArcsecPerYrAtAge: (t_Ma: number) => number | null;
129
+ lunisolarPrecessionPeriodYearsAtAge: (t_Ma: number) => number | null;
114
130
  lunarTorqueFactorAtAge: (tMa: number) => (number | null);
115
131
  precessionTorqueTermAtAge: (tMa: number) => (number | null);
116
132
  siderealYearSecondsAtAge: (t_Ma: number) => number;
@@ -17,15 +17,18 @@ export function computeSolarTorqueShare(c: {
17
17
  }): number;
18
18
  /**
19
19
  * @param {{
20
- * p0ArcsecPerYr: number,
20
+ * p0ArcsecPerYr: number | (() => number),
21
21
  * solarShare: number,
22
22
  * lodSecondsAtAge: (tMa: number) => (number | null),
23
23
  * lodJ2000Seconds: number,
24
24
  * moonDistanceMetresAtAge: (tMa: number) => (number | null),
25
25
  * moonDistanceJ2000Metres: number,
26
26
  * yearToTMa: (year: number) => number,
27
- * }} deps - p0 = 1,296,000/(H/13) (the model's J2000 rate); lodJ2000Seconds
28
- * the SAME day basis lodSecondsAtAge(0) returns.
27
+ * }} deps - p0 = the model's derived J2000 rate, 1,296,000/axial0 (S5; a
28
+ * number or a LAZY getter — the callers' certified year laws read the
29
+ * deep-time factory this is built inside, so the anchor is resolved on
30
+ * the first RATE use, never at construction; the torque term needs no
31
+ * anchor); lodJ2000Seconds the SAME day basis lodSecondsAtAge(0) returns.
29
32
  * @returns {{
30
33
  * composedRateArcsecPerYrAtAge: (tMa: number) => (number | null),
31
34
  * composedPeriodYearsAtAge: (tMa: number) => (number | null),
@@ -39,7 +42,7 @@ export function computeSolarTorqueShare(c: {
39
42
  * }}
40
43
  */
41
44
  export function createComposedPrecession(deps: {
42
- p0ArcsecPerYr: number;
45
+ p0ArcsecPerYr: number | (() => number);
43
46
  solarShare: number;
44
47
  lodSecondsAtAge: (tMa: number) => (number | null);
45
48
  lodJ2000Seconds: number;
package/types/model.d.ts CHANGED
@@ -37,11 +37,11 @@ export function assembleModel(C: Readonly<Record<string, any>>, F: Readonly<Reco
37
37
  tropicalYearSecondsAtYear: (year: number) => number;
38
38
  }>;
39
39
  lunisolar: Readonly<{
40
- /** The mean lunisolar precession period T_p(t), years — the composed torque rate's period. @param {number} year @returns {number} */
40
+ /** The mean lunisolar precession period T_p(t), years — the composed torque rate's period on the derived J2000 anchor (25,771.4 at J2000). @param {number} year @returns {number} */
41
41
  meanPeriodYearsAtYear: (year: number) => number;
42
- /** The composed rate ψ̇(t) = [ω/ω₀]·p₀·[f_S + (1 − f_S)(a₀/a_M)³], ″/yr. @param {number} year @returns {number} */
42
+ /** The composed rate ψ̇(t) = [ω/ω₀]·p₀·[f_S + (1 − f_S)(a₀/a_M)³], ″/yr (p₀ = 50.2883, derived). @param {number} year @returns {number} */
43
43
  meanRateArcsecPerYrAtYear: (year: number) => number;
44
- /** The of-date period — the one-family beat inside ±2 Myr, the mean beyond. @param {number} year @returns {number} */
44
+ /** The of-date period — the one-family beat inside ±2 Myr, the composed mean beyond. @param {number} year @returns {number} */
45
45
  ofDatePeriodYearsAtYear: (year: number) => number;
46
46
  /** f_S, the solar fraction of the J2000 precession torque. */
47
47
  solarShareJ2000: number;
@@ -49,13 +49,19 @@ export function assembleModel(C: Readonly<Record<string, any>>, F: Readonly<Reco
49
49
  lunarTorqueFactorAtYear: (year: number) => number;
50
50
  /** f_S + (1 − f_S)(a₀/a_M)³ — the torque term the unit divides H_era by. @param {number} year @returns {number} */
51
51
  torqueTermAtYear: (year: number) => number;
52
- /** The hybrid's precession constant α = p₀ / cos ε₀, ″/yr (ε₀ the J2000 obliquity input). */
52
+ /** The hybrid's precession constant α = p₀ / cos ε₀, ″/yr (p₀ the derived J2000 rate, ε₀ the J2000 obliquity input; 54.81). */
53
53
  torqueConstantJ2000ArcsecPerYr: number;
54
+ /** |s₃|, the dominant nodal mode of Earth's orbit — the largest-amplitude ζ mode of the banked deep secular modes, ″/yr (18.85; the obliquity beat's partner). ONE home for the registry, the browser and the API. */
55
+ nodalModeS3ArcsecPerYr: number;
56
+ /** The ecliptic (nodal) precession period 1,296,000/|s₃|, years (68,751) — an orbital quantity, fixed at every epoch at the two-body level. */
57
+ nodalPeriodYears: number;
58
+ /** The obliquity beat 2π/(ψ̇(t) − |s₃|), years — the SHIPPED deep-time obliquity period (falsification leg 1; 41,224 at J2000, on the composed rate). @param {number} year @returns {number} */
59
+ obliquityBeatYearsAtYear: (year: number) => number;
54
60
  /** The apsidal (perihelion vs the stars) period from the engine-D chain's secular tangent, years — inside the published window only (the tangent is an extrapolation beyond the banked series: it turns negative at −5 Myr); null beyond. @param {number} year @returns {number|null} */
55
61
  apsidalPeriodYearsAtYear: (year: number) => number | null;
56
- /** T_aps(t) / T_p(t) — the apsidal period in mean precession periods (4.33 at J2000, a fitted reading; 0.84 … 9.5 across ±26 kyr, measured); null beyond the published window. @param {number} year @returns {number|null} */
62
+ /** T_aps(t) / T_p(t) — the apsidal period in of-date precession periods (4.33 at J2000, a reading; 0.84 … 9.9 across ±26 kyr, measured); null beyond the published window. @param {number} year @returns {number|null} */
57
63
  apsidalPerPrecessionAtYear: (year: number) => number | null;
58
- /** T_peri(t) = 1/(1/T_p + 1/T_aps) — the perihelion-of-date period (equinox precession + inertial perihelion motion, frame arithmetic at every epoch), years; null beyond the published window. @param {number} year @returns {number|null} */
64
+ /** T_peri(t) = 1/(1/T_p + 1/T_aps) — the perihelion-of-date period (equinox precession + inertial perihelion motion, frame arithmetic at every epoch), years, on the of-date T_p; null beyond the published window. @param {number} year @returns {number|null} */
59
65
  periOfDatePeriodYearsAtYear: (year: number) => number | null;
60
66
  /** T_peri(t) / T_p(t) (0.812 at J2000 — the J2000 reading); null beyond the published window. @param {number} year @returns {number|null} */
61
67
  periOfDatePerPrecessionAtYear: (year: number) => number | null;