@essrt/physics 23.3.3 → 23.6.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.
Files changed (57) hide show
  1. package/README.md +4 -1
  2. package/package.json +2 -2
  3. package/src/constants/coefficients.d.ts +5 -1
  4. package/src/constants/coefficients.js +3 -2
  5. package/src/constants/generated.d.ts +2 -3
  6. package/src/constants/generated.js +3 -4
  7. package/src/constants/units.cjs +4 -1
  8. package/src/deltat/deep-time.cjs +37 -72
  9. package/src/index.js +30 -48
  10. package/src/model/cardinal-instants.js +101 -0
  11. package/src/model/context.js +54 -0
  12. package/src/model/deep-time-chain.js +79 -0
  13. package/src/model/delta-t.js +38 -0
  14. package/src/model/derived-constants.js +58 -0
  15. package/src/model/earth-scalars.js +47 -0
  16. package/src/model/inv-plane-elements.js +56 -0
  17. package/src/model/lunar-theory.js +216 -0
  18. package/src/model/moon-epoch.js +28 -0
  19. package/src/model/one-source-movement.js +192 -0
  20. package/src/model/planet-elements-of-date.js +61 -0
  21. package/src/model/planet-records.js +63 -0
  22. package/src/model/planet-spin-channel.js +41 -0
  23. package/src/model/published-periods.js +38 -0
  24. package/src/model/sun-and-eclipses.js +199 -0
  25. package/src/model/surface.js +330 -0
  26. package/src/model/time-axis.js +24 -0
  27. package/src/model/year-day-lengths.js +42 -0
  28. package/src/model.js +62 -1612
  29. package/src/moon/arguments.cjs +27 -43
  30. package/src/planets/chain-artifact.js +2 -2
  31. package/src/planets/keplerian-chain.cjs +1 -1
  32. package/types/constants/coefficients.d.ts +5 -1
  33. package/types/constants/generated.d.ts +2 -3
  34. package/types/constants/units.d.cts +3 -0
  35. package/types/deltat/deep-time.d.cts +18 -24
  36. package/types/index.d.ts +1 -1
  37. package/types/model/cardinal-instants.d.ts +14 -0
  38. package/types/model/context.d.ts +87 -0
  39. package/types/model/deep-time-chain.d.ts +288 -0
  40. package/types/model/delta-t.d.ts +11 -0
  41. package/types/model/derived-constants.d.ts +25 -0
  42. package/types/model/earth-scalars.d.ts +14 -0
  43. package/types/model/inv-plane-elements.d.ts +6 -0
  44. package/types/model/lunar-theory.d.ts +72 -0
  45. package/types/model/moon-epoch.d.ts +10 -0
  46. package/types/model/one-source-movement.d.ts +74 -0
  47. package/types/model/planet-elements-of-date.d.ts +9 -0
  48. package/types/model/planet-records.d.ts +7 -0
  49. package/types/model/planet-spin-channel.d.ts +29 -0
  50. package/types/model/published-periods.d.ts +7 -0
  51. package/types/model/sun-and-eclipses.d.ts +52 -0
  52. package/types/model/surface.d.ts +279 -0
  53. package/types/model/time-axis.d.ts +12 -0
  54. package/types/model/year-day-lengths.d.ts +8 -0
  55. package/types/model.d.ts +9 -81
  56. package/types/moon/arguments.d.cts +5 -11
  57. package/types/planets/keplerian-chain.d.cts +3 -1
package/src/model.js CHANGED
@@ -8,6 +8,16 @@
8
8
  * eventual tools/lib adapter collapse swaps onto it instead of keeping a
9
9
  * private twin.
10
10
  *
11
+ * The wiring is COMPOSED here from named slices (./model/): each slice file
12
+ * assembles one family of quantities from the context the earlier slices
13
+ * built and returns the bindings the later ones read — a reader finds the
14
+ * obliquity of date in model/one-source-movement.js, the eclipse finders in
15
+ * model/sun-and-eclipses.js, the published object in model/surface.js. The
16
+ * slices run in dependency order on ONE shared context object, so every memo
17
+ * and sampler exists once; the few bindings a slice reads at call time from a
18
+ * later slice are the `late` set (model/context.js), filled below as each
19
+ * owner returns.
20
+ *
11
21
  * Counterfactuals are first-class (§2d): the context validation and the
12
22
  * counterfactual hashing live in index.js's `createModel`, which composes
13
23
  * this assembly with the resolved, frozen context. This module stays pure
@@ -17,40 +27,23 @@
17
27
  * src/lib/essrt.ts) operation-for-operation where FP association matters,
18
28
  * which itself mirrors tools/lib/constants.js §9.
19
29
  */
20
- import { buildPlanetChainsFromArtifactData, computePlanetElementsAtYear } from './planets/keplerian-chain.cjs';
21
- import { createSecularSeriesOverride } from './planets/secular-series.cjs';
22
- import { computeEquatorNodeOriginSFrameDeg, convertNodeSFrameToEquatorOriginDeg } from './planets/inv-plane-frame.cjs';
23
- import { createYearLengths, computeMeanTropicalYearJ2000Days, ONE_FAMILY_WINDOW_YEARS } from './earth/year-lengths.cjs';
24
- import { createDeepOrbitalHistory } from './earth/deep-orbital-history.cjs';
25
- import { CHAIN_ARTIFACT } from './planets/chain-artifact.js';
26
- import { createPlanetSpinChannelFromArtifacts, computeObliquityJ2000Deg } from './planets/spin-channel.cjs';
27
- import { PLANET_DISPLAY_NAMES } from './planets/display-names.cjs';
28
- import { createDeepTimeChain } from './deltat/deep-time-chain.js';
29
- import { computeMassChainJ2000 } from './earth/mass-chain.cjs';
30
- import { MOON_DISTANCE_ARTIFACT } from './deltat/moon-distance-artifact.cjs';
31
- import { evalClimateL1OrbitalPermil } from './climate/l1-orbital.cjs';
32
- import { DEEP_MODES_ARTIFACT } from './moon/deep-modes-artifact.cjs';
33
- import { createChainCycleIntegrator } from './chain-cycles/index.cjs';
34
- import { createMoonArguments, jdToDecimalYear } from './moon/arguments.cjs';
35
- import { createMoonSeries } from './moon/series.cjs';
36
- import { createSunPlanetaryCompletion, computeCarrierRatesDegPerCy } from './eclipse/sun-planetary-completion.cjs';
37
- import { createEclipseFinders } from './eclipse/finders.cjs';
38
- import { createBesselian } from './eclipse/besselian.cjs';
39
- import { driver2PeriodSecondsAtAge } from './planets/orbit-chain.cjs';
40
-
41
- /**
42
- * RA-day-offset Fourier amplitudes (ms). KNOWN EXCEPTION carried over from
43
- * the reference adapters: these are fit results that live as literals in the
44
- * engine too — packaging them into FITTED_COEFFICIENTS is the remaining
45
- * §7a-step-1b move; until then this is their single packaged home.
46
- */
47
- // (Plan 07 R10 moved the RA day offset's two cosine amplitudes from literals
48
- // here into F.RA_DAY_OFFSET_MS; plan 09 step 5b deleted that family — it was
49
- // the equation of time's window term of an instrument, not a motion.)
50
-
51
- // (The Moon-channel eccentricity sensitivities s_ϖ/s_Ω — the [g/g₀]^s exponents of
52
- // the factored deep-time law — have ONE home since plan 13 B2:
53
- // deltat/deep-time-chain.js MOON_ECC_SENSITIVITY_PERIGEE / _NODE.)
30
+ import { assembleDerivedConstants } from './model/derived-constants.js';
31
+ import { assembleDeepTimeChain } from './model/deep-time-chain.js';
32
+ import { assembleEarthScalars } from './model/earth-scalars.js';
33
+ import { assembleYearDayLengths } from './model/year-day-lengths.js';
34
+ import { assembleOneSourceMovement } from './model/one-source-movement.js';
35
+ import { assembleMoonEpoch } from './model/moon-epoch.js';
36
+ import { assembleDeltaT } from './model/delta-t.js';
37
+ import { assemblePlanetRecords } from './model/planet-records.js';
38
+ import { assemblePlanetSpinChannel } from './model/planet-spin-channel.js';
39
+ import { assemblePlanetElementsOfDate } from './model/planet-elements-of-date.js';
40
+ import { assembleInvPlaneElements } from './model/inv-plane-elements.js';
41
+ import { assembleTimeAxis } from './model/time-axis.js';
42
+ import { assembleLunarTheory } from './model/lunar-theory.js';
43
+ import { assembleSunAndEclipses } from './model/sun-and-eclipses.js';
44
+ import { assemblePublishedPeriods } from './model/published-periods.js';
45
+ import { assembleCardinalInstants } from './model/cardinal-instants.js';
46
+ import { assembleSurface } from './model/surface.js';
54
47
 
55
48
  /**
56
49
  * Assemble the model surfaces from a resolved constants context + fitted
@@ -59,1584 +52,41 @@ import { driver2PeriodSecondsAtAge } from './planets/orbit-chain.cjs';
59
52
  *
60
53
  * @param {Readonly<Record<string, any>>} C the frozen constants context
61
54
  * @param {Readonly<Record<string, any>>} F the fitted coefficients
62
- * @param {{ eccentricityAt?: (year: number) => number, eccentricityRateAt?: (year: number) => number, perihelionLongitudeDegAt?: (year: number) => number }} [laws]
63
- * RESEARCH OVERRIDES for Earth's orbit laws (doc 109 §7): an alternative e(t),
64
- * de/dt(t) and ϖ_of-date(t) flow through every consumer (the eclipse Sun, the
65
- * Moon's E-factor, the cardinal points) exactly as the shipped laws do. Default
66
- * {} = the shipped laws, bit-identical to before this parameter existed. Not a
67
- * counterfactual in the §2d sense (no hash change) — callers must say when
68
- * they used it; the generators refuse to --write under an override.
69
- * @param {{ giaEllipticityOfDate?: boolean }} [options]
70
- * `giaEllipticityOfDate: false` builds the one-source history WITHOUT the J₂(t)/J₂₀ factor of date
71
- * (the ice-driven GIA channel, which rides the climate formula's own post-MPT layer): the orbital
72
- * FORCING alone. The T1 exporter derives the climate lines' obliquity rate on it, so the lines
73
- * cannot depend on their own fit (v23.0; measured 3e-5 of the rate, 2–3 yr of period). Default true =
74
- * the shipped physics (v15.0), bit-identical to before this parameter existed.
75
- * @returns the assembled surfaces (epoch, earth, lengths, cardinal, moon) — type inferred so ReturnType stays precise
55
+ * @param {import('./model/context.js').Laws} [laws] research overrides for Earth's orbit laws
56
+ * (model/context.js); default {} = the shipped laws, bit-identical to before this parameter existed
57
+ * @param {any} [secularSeriesArtifact] the governed N-body secular series (the API/Node path; the
58
+ * npm package alone has no 8 MB series)
59
+ * @param {import('./model/context.js').Options} [options] `giaEllipticityOfDate: false` = the orbital
60
+ * forcing alone (model/context.js); default true = the shipped physics, bit-identical to before
61
+ * @returns the assembled surfaces (epoch, earth, lengths, cardinal, moon, …) — type inferred so ReturnType stays precise
76
62
  */
77
63
  export function assembleModel(C, F, laws = {}, secularSeriesArtifact = /** @type {any} */ (null), options = {}) {
78
- // ── Derived constants (constants.js §9 order) ─────────────────────────────
79
- // (plan 09 step 7b: `foundational.holisticyearLength` — the unit H — is no
80
- // longer read here; the unit function left the physics with it)
81
- // Plan 09: the mean solar year in days (the calendar's year axis) is the
82
- // J2000 mean tropical year DERIVED in one home from the IAU sidereal year
83
- // and p_A per Julian year (year-lengths.cjs); the Meeus & Savoie 1992 input
84
- // 365.2421897 left the registry, the device's rounding onto 1/(H/8) before it.
85
- const meanSolarYearDays = computeMeanTropicalYearJ2000Days(C.yearLengthRef.siderealYear, C.earthOrbital.generalPrecessionJ2000_arcsecPerYr);
86
- // Plan 11 step 5: the scene epoch's ONE home — astro-reference
87
- // timeReference.sceneEpochJD / sceneEpochYear (moved verbatim from the
88
- // deleted foundational block); correctionDays is deleted, so the SI-year
89
- // axis below anchors on the epoch itself.
90
- const startmodelYear = C.timeReference.sceneEpochYear;
91
- const startmodelJD = C.timeReference.sceneEpochJD;
92
- // Plan 07 R10: the correction combs measure their phase from J2000. The
93
- // former origin — perihelionalignmentYear − temperatureGraphMostLikely ×
94
- // H/16 = −302,635, the "balanced year" — was rotated out of every fitted
95
- // (sin, cos) pair exactly (tools/fit/reorigin-combs-j2000.mjs; measured
96
- // bit-identical on every surface over ±300 kyr); it is no longer an input.
97
- // Plan 09 step 5b: the J2000 comb origin's last reader (the RA day offset
98
- // comb) left with that device; no comb phase is evaluated here any more.
99
-
100
- const meanSiderealYearDays = C.yearLengthRef.siderealYear;
101
- // (the sidereal year in SI seconds is the mass chain's own, below)
102
- // Plan 09: the J2000 day length ω₀ is the SI day (an input); the device's
103
- // H/13 kinematic-sidereal-days construction (86,399.99968 s) is deleted.
104
- const meanLengthOfDay = C.yearLengthRef.solarDay;
105
- const meanTropicalYearJ2000Seconds = meanSolarYearDays * meanLengthOfDay;
106
- // Plan 09: THE J2000 precession anchor p₀ in days (Julian years) — the IAU
107
- // 2006 general precession; the Moon's star-referenced periods read it.
108
- const axialPrecessionJ2000Days = (1296000 / C.earthOrbital.generalPrecessionJ2000_arcsecPerYr) * 365.25;
109
-
110
- // Plan 07 R5/R6/R9: `earthtiltMean`, `earthInvPlaneInclinationAmplitude`
111
- // and `eccentricityAmplitude` were read here only to invert the K and ψ
112
- // constants from Earth's calibration and to build the H/3 inclination
113
- // device. With both laws retired nothing in this module reads them — they
114
- // remain on C.earth as the K device's J2000 pose inputs (the scene's wheel
115
- // geometry at the chain anchor epoch) and for the registry's record keys.
116
- // (Plan 09: the 16-harmonic solstice-obliquity comb — SOLSTICE_OBLIQUITY_
117
- // MEAN_FITTED / _HARMONICS, the frozen era record — is deleted; the model's
118
- // ONE obliquity of date is the one-source hybrid, `oneSourceM.epsAt`.)
119
-
120
- const G_CONSTANT = C.physicalConstants.G_CONSTANT;
121
- const MASS_RATIO_EARTH_MOON = C.physicalConstants.MASS_RATIO_EARTH_MOON;
122
- const currentAUDistance = C.physicalConstants.currentAUDistance;
123
- const moonDistanceKm = C.moonReference.moonDistance;
124
- const moonSiderealMonthInput = C.moonReference.moonSiderealMonthInput;
125
-
126
- // Plan 09: the Moon's sidereal month is the IAU input itself (the device's
127
- // eighth-integer-per-8H count read it back 7.8 ms long; deleted).
128
- const moonSiderealMonth = moonSiderealMonthInput;
129
-
130
- // The mass chain (Moon Kepler → GM_EM → the Earth/Moon split → the Sun, §9
131
- // order) — ONE home since plan 13 B3: earth/mass-chain.cjs (the deep-time
132
- // chain below computes the same scalars from the same context).
133
- const mass = computeMassChainJ2000({
134
- moonDistanceKm, moonSiderealMonthDays: moonSiderealMonth, meanSiderealYearDays,
135
- meanLengthOfDaySeconds: meanLengthOfDay, massRatioEarthMoon: MASS_RATIO_EARTH_MOON,
136
- currentAUDistanceKm: currentAUDistance, gravitationalConstantKm3KgS2: G_CONSTANT,
137
- });
138
- const moonOrbitalShift = mass.moonOrbitalShiftKm;
139
- const moonDistanceCorrected = mass.moonDistanceCorrectedKm;
140
- const GM_EARTH_MOON_SYSTEM = mass.gmEarthMoonSystemKm3S2;
141
- const GM_EARTH_ALONE = mass.gmEarthAloneKm3S2;
142
- const GM_MOON_ALONE = mass.gmMoonAloneKm3S2;
143
- const GM_SUN_PLUS_EARTH = mass.gmSunPlusEarthKm3S2;
144
- const GM_SUN = mass.gmSunKm3S2;
145
- const M_SUN = mass.sunMassKg;
146
-
147
- // ── THE DEEP-TIME CHAIN — ONE assembly (plan 13 B2) ──────────────────────
148
- // The GIA α channel (the lagged response to the L1 ice history, with the R2
149
- // lattice pin and the exact-argument memo), the Layer-0 bundle, the cited
150
- // Earth–Moon distance history (plan 12 option A), the ΔT cycle stack (all
151
- // channels ON here), |s₃|, the LOD/ΔT core and the lunar month chain are
152
- // assembled once in deltat/deep-time-chain.js from this context's constants
153
- // and fitted coefficients (so a counterfactual context flows through them);
154
- // the Node twin and the browser destructure the same module.
155
- const chain = createDeepTimeChain({
156
- constants: C, fitted: F, deepModes: DEEP_MODES_ARTIFACT, moonDistanceArtifact: MOON_DISTANCE_ARTIFACT,
157
- ageAnchorYear: startmodelYear,
158
- });
159
- const { alphaGia, earthMoiFactorAtAge, EPOCH_PARAMS, recession, dtCycles, dtCycleLodCorrectionSum, s3ArcsecPerYr, deepLod, deepEcc, moonChain } = chain;
160
- const CLIMATE_REGIME = F.CLIMATE_FORMULA_COEFFS.regimes['lr04-post-mpt'];
161
- /** The climate formula's L1 (unlagged) — the orbital δ¹⁸O layer itself. @param {number} year @returns {number} */
162
- const evalClimateL1 = (year) => evalClimateL1OrbitalPermil(year, {
163
- l1Terms: CLIMATE_REGIME.L1,
164
- yStdDenormalization: CLIMATE_REGIME.denormalization.y_std,
165
- });
166
- const moonDistanceMetresAtAge = recession.distanceMetresAtAge;
167
- /** The published surfaces' km reading — NaN beyond the cited history's span (the API's
168
- * validity span lies inside it; the deep charts stop where the input stops).
169
- * @param {number} year @returns {number} */
170
- const moonDistanceKmAtYearOrNaN = (year) => { const m = moonDistanceMetresAtAge(yearToTMa(year)); return m === null ? NaN : m / 1000; };
171
-
172
- /** THE REALIZED POLE RATE OVER A WINDOW, ″/yr — the mean over [yearFrom, yearTo] of the rate the spin
173
- * integration actually turns the pole at: the inertial secular rate of date × the solar-torque factor at
174
- * e(t) × J₂(t)/J₂₀ × cos ε(t)/cos ε₀ (ds/dt = α (ŝ·n̂)(ŝ×n̂): the azimuthal rate about n̂ is α cos ε).
175
- * The secular rate here is the CONSTANT's scaling (lunisolarTorqueRate…), not the published rate of
176
- * date: the hybrid's own ε(t) carries the ONE cosine, so the cited mean obliquity's factor (plan 12 §8)
177
- * does not enter twice — inside the ζ-series span the hybrid IS the model's obliquity of date.
178
- * Measured (2026-10): the dominant obliquity line of the model's own ε(t) over the last 5 Myr sits at
179
- * this mean minus |s₃| (40,938 fitted vs 40,936 predicted), NOT at the J2000 beat (41,098) — the pole
180
- * turned faster in the past (spin +0.06 ″/yr over 5 Myr, lower mean obliquity +0.05, higher mean
181
- * eccentricity +0.015); La2004's own series reads the same (40,917). The climate records' preferred
182
- * "+0.12…+0.20 ″/yr on the obliquity lines" is this window mean (50.515 over the LR04 record).
183
- * Calls the one-source history and the deep-time LOD declared below (evaluated at call time).
184
- * `ellipticity: false` leaves the J₂(t)/J₂₀ factor out: that factor is the ice-driven GIA channel, which
185
- * rides the climate formula's own post-MPT layer — the one piece that would make the climate LINES
186
- * depend on their own fit (measured: its LR04-window mean moved 1 − 3e-5 → 1 across one refit,
187
- * 1.4e-3 ″/yr, 1 yr of period). The line derivation (the T1 exporter) excludes it; the published
188
- * realized period and the hybrid verdict's gate keep the full rate.
189
- * @param {number} yearFrom @param {number} yearTo @param {number} [stepYears=500]
190
- * @param {{ellipticity?: boolean}} [opts] @returns {number} */
191
- const spinAxisInertialRateMeanArcsecPerYr = (yearFrom, yearTo, stepYears = 500, opts = {}) => {
192
- const useEllipticity = opts.ellipticity !== false;
193
- const lo = Math.min(yearFrom, yearTo), hi = Math.max(yearFrom, yearTo);
194
- const cosEps0 = Math.cos(oneSourceM.epsAt(2000) * Math.PI / 180);
195
- const kInertial = certifiedAxialPrecessionJ2000Years() / oneSourceM.axialPrecessionYearsLunisolarJ2000;
196
- let sum = 0, n = 0;
197
- for (let y = lo; y <= hi; y += stepYears) {
198
- const r = deepLod.lunisolarTorqueRateArcsecPerYrAtAge(yearToTMa(y));
199
- const psiDot = (r === null ? 1296000 / certifiedAxialPrecessionJ2000Years() : r) * kInertial;
200
- const f = oneSourceM.precessionOfDateFactorsAt(y);
201
- sum += psiDot * f.solarTorque * (useEllipticity ? f.ellipticity : 1) * Math.cos(oneSourceM.epsAt(y) * Math.PI / 180) / cosEps0;
202
- n++;
203
- }
204
- return sum / n;
205
- };
206
-
207
- // (Plan 09 step 7a: the integrated ∫dt/H_era phase table and its counter
208
- // `cyclesBetween` — the frozen era clock's own convention H_era =
209
- // H₀·LOD/LOD₀, plan 06 D8's second named counter, which every FROZEN comb
210
- // read — are DELETED with the last comb; `epoch.cyclesBetween` left the
211
- // surface with them, no reader remained. Step 5b: `phaseRadians` left with
212
- // the RA day offset comb.)
213
-
214
- /** @param {number} year @returns {number} */
215
- const yearToTMa = (year) => (2000 - year) / 1e6;
216
-
217
- // ── Earth scalars (integrated-phase display semantics) ────────────────────
218
- /** @param {number} year @returns {number} */
219
- // Plan 06 layer B — the eclipse Sun onto the series. The PUBLISHED Earth
220
- // ϖ of date is the one-source series (the secular orbit; its J2000 element
221
- // ≡ La2004's — the same field the D4c apsidal wheel renders). The SUN's
222
- // equation of centre needs the MEAN elements of the era: the series plus the
223
- // derived mean offset of the osculating channel (F.EARTH_OSCULATING_MEAN_
224
- // OFFSET, tools/verify/earth-osculating-offset.js — the 1890–2110 mean of
225
- // osculating − secular from the Horizons-seeded nine-body run). Measured:
226
- // the fast osculating wobble is carried by the completion's direct terms and
227
- // double-counts if added; the window mean is what the Sun residual holds
228
- // (all-phase JPL sd 3.23″ → 1.59″; the former K law's Standish anchor
229
- // 1.58″). The former K perihelion Fourier law (H divisors) is retired here.
230
- const earthPerihelionDeg = (year) => oneSourceM.periOfDateDegAt(year);
231
- const SUN_OFF = F.EARTH_OSCULATING_MEAN_OFFSET;
232
- /** The eclipse Sun's ϖ of date (mean elements). @param {number} year @returns {number} */
233
- const sunPerihelionDegAt = laws.perihelionLongitudeDegAt ?? ((year) => oneSourceM.periOfDateDegAt(year) + SUN_OFF.dPomArcsec / 3600);
234
- // Plan 09: the obliquity of date is the one-source hybrid everywhere — the
235
- // eclipse tier (besselian deps) read the retired 16-harmonic comb here.
236
- /** @param {number} year @returns {number} */
237
- const obliquityDeg = (year) => oneSourceM.epsAt(year);
238
- // ECCENTRICITY UNIFICATION — ONE law for the whole model (plan
239
- // IP-eccentricity-unification, decision D1 = form (e)). Earth's orbital
240
- // eccentricity is the magnitude of a vector sum, e = |v₁ + v₂|: v₁ of
241
- // length base' along the ICRF perihelion (rotating once per H/3), v₂ of
242
- // length base'/2 FIXED in space along the inclination-cycle anchor
243
- // direction. Their relative angle θ rides the System-Reset lattice phase
244
- // (θ(t) = 3(t − balancedYear)/H·360° − 180°, θ(J2000) = 81.178°) — the
245
- // SAME anchor the inclination law and the Moon channel ride (their
246
- // extremes coincide: max −23,200, min −79,100). Then
247
- // e(t) = base' · (1 + cos θ(t) / 2), base' = e(J2000) / (1 + cos θ(J2000) / 2)
248
- // base' is DERIVED from the observed J2000 eccentricity and the anchor
249
- // (0.015520; the Law-5 balance moves 99.8636 → 99.8645%, Earth's weight
250
- // there being 0.05%). No new constants. Why H/3 and not the former H/16
251
- // beat law: eccentricity is frame-invariant and may carry only fixed-frame
252
- // lattice periods; H/16 is the OF-DATE perihelion period (13 + 3 = 16 —
253
- // the H/3 rotation seen from the H/13 equinox) and belongs to ϖ_of-date;
254
- // measured, the H/16 law's present ė (−0.84e-5/cy) is 5× below the
255
- // observed −4.20e-5 while this law reads −4.31e-5 (Phase-0 record: the
256
- // H/3-law candidate instrument, deleted at plan 09 step 7e — docs/109 §22;
257
- // JPL Sun 1.49″, registry 0.80″, syzygy 3.72″). Consumers (engine-switch decision (ii) restatement,
258
- // plan 02 §8): the eclipse Sun (equation of centre), the besselian Sun
259
- // distance and the cardinal-point braid — the CLOCK-side machinery. The
260
- // LUNAR chain (E-factor, perigee/node modulation/T², argument Δe² and
261
- // of-date rates) rides the ONE deep e (deep-ecc-channel.cjs — the
262
- // engine's own ±10-Myr z-vector); this H/3 line is its epoch-local
263
- // tangent (E18), agreeing within 4.2e-5 across the historical era.
264
- // ONE implementation for all three runtimes: moon/ecc-channel.cjs (the
265
- // Node engine's deep-time.js and the browser's script.js instantiate the
266
- // same channel with the same inputs). Its phase counter runs from J2000
267
- // (θ₀ = ϖ_ICRF(J2000) − 21.77° = 81.178°, the System-Reset anchor in
268
- // anchor form — doc 66 §1); base' is derived inside from the observed
269
- // J2000 eccentricity.
270
- // (The H/3 eccentricity-law channel — createMoonEccChannel — left this
271
- // package with plan 06 layer B: the Sun's e is the one-source series plus
272
- // the derived mean offset above; the browser's scene Sun still reads the
273
- // law through its own _moonEcc until the cardinal points move.)
274
- // Engine-switch decision (ii) (plan 02 §8): the ONE deep e — the engine's
275
- // own ±10-Myr mode table, anchored form — feeds the ENTIRE lunar chain
276
- // (modulation, cycle counts, arguments eccAt/channelIntegral, E-factor).
277
- // The Sun/clock machinery (eclipse Sun EoC, besselian Sun distance,
278
- // cardinal braid) reads the one-source series below (plan 06 layer B —
279
- // the two agree within 4.2e-5 in-era). The laws hook deliberately does
280
- // NOT reach this channel.
281
- // (`deepEcc` is the deep-time chain's, destructured above.)
282
- /** @param {number} year @returns {number} */
283
- // Plan 06 layer B: the PUBLISHED Earth eccentricity is the one-source series
284
- // (|z|, the secular orbit); the SUN's e adds the derived mean offset (see
285
- // sunPerihelionDegAt). The former H/3 eccentricity law is retired here.
286
- const eccentricityAt = (year) => oneSourceM.eAt(year);
287
- /** The eclipse Sun's e (mean elements). @param {number} year @returns {number} */
288
- const sunEccentricityAt = laws.eccentricityAt ?? ((year) => oneSourceM.eAt(year) + SUN_OFF.dE);
289
- /** de/dyear of the Sun's e — the cardinal braid's equation-of-centre
290
- * derivative rides it (±0.5-yr central difference; the constant offset
291
- * cancels). @param {number} year @returns {number} */
292
- // (Plan 07 R5/R9: Earth's H/3 inclination cosine and −H/5 linear node — the
293
- // K device — stood here; `inclinationDeg`/`ascendingNodeDeg` are now the
294
- // one-source construction defined with the planets' chain evaluators below.)
295
-
296
- // ── Year/day lengths ──────────────────────────────────────────────────────
297
- // Plan 09: the three year-length combs (TROPICAL/SIDEREAL/ANOMALISTIC_
298
- // YEAR_HARMONICS on the frozen era clock's bases) and their evaluators
299
- // are deleted. The year lengths of date in DAYS are the published SI year
300
- // lengths over the kinematic day of date; the kinematic day of date is the
301
- // Layer-0 tidal mean LOD, exactly ω₀ (the SI day) at J2000 — the former
302
- // "sidereal seconds over comb days" carried the λ̇ ripple of the sidereal
303
- // year as a day-length ripple (≤ 0.1 ms in the era, 0.66 ms at ±26 kyr).
304
- /** THE J2000 precession anchor p₀ (plan 09): the IAU 2006 general
305
- * precession in longitude at J2000 (astro-reference.json
306
- * earthOrbital.generalPrecessionJ2000_arcsecPerYr = 50.28796195 ″/yr),
307
- * stated as the period 1,296,000/p_A = 25,771.575 Julian years. ONE home:
308
- * the composed clock's p₀, the hybrid's self-anchor and the lunisolar
309
- * surface all read it. Before plan 09 it was the retired year combs' beat
310
- * at 2000 (plan 06 S5: 25,771.395 yr, 8 ppm faster); the H/13 reading
311
- * (25,793.6) was the fit anchor's, not a period. The identifier keeps its
312
- * name. @returns {number} */
313
- function certifiedAxialPrecessionJ2000Years() {
314
- return 1296000 / C.earthOrbital.generalPrecessionJ2000_arcsecPerYr;
315
- }
316
- /** Kinematic day of date (Layer 0 tidal mean LOD; ω₀ at J2000). Past the
317
- * tidal-lock asymptote the chain is null — the J2000 day stands in, as
318
- * the retired comb base did. @param {number} year @returns {number} */
319
- const dayLengthSeconds = (year) => deepLod.lodSecondsAtAge(yearToTMa(year)) ?? meanLengthOfDay;
320
- /** Sidereal year of date in days of the epoch — the published SI sidereal
321
- * year (the λ̇ channel inside the one-family window, the mass-loss law
322
- * beyond) over the kinematic day of date. @param {number} year @returns {number} */
323
- const siderealYearDays = (year) => publishedSiderealYearSecondsAtYear(year) / dayLengthSeconds(year);
324
- /** Anomalistic year of date in days of the epoch — the published SI
325
- * anomalistic year (one-family inside its window; NaN beyond, where the
326
- * movement's apsidal line is not resolved). @param {number} year @returns {number} */
327
- const anomalisticYearDays = (year) => {
328
- const s = Math.abs(year - 2000) <= ONE_FAMILY_WINDOW_YEARS ? yearLengthsM.anomalisticYearSecondsAtYear(year) : NaN;
329
- return s / dayLengthSeconds(year);
330
- };
331
- // (Plan 09 step 5b: raDayOffsetMs — the fitted RA_DAY_OFFSET_MS comb, a −14.194 ms
332
- // mean + lines on H/16 and H/8 — and measuredSolarDaySeconds = dayLength + that
333
- // offset are deleted. The quantity was the equation of time's window term of the
334
- // Framework-Verification instrument: 365 solar noons counted from the June
335
- // solstice end 0.2422 d short of the tropical year, so the mean noon-to-noon
336
- // interval differs from the mean solar day by −(0.2422/365)·dEoT/dt at the
337
- // solstice — the e·cos(90° − ϖ) term on the perihelion-of-date cycle (the comb's
338
- // "H/16" line, 5.6 ms vs 5.2 first-order) and the tan²(ε/2) term on the obliquity
339
- // cycle (the "H/8" line, 1.7 vs 1.4). A property of the window, not of the sky:
340
- // the physical day of date is dayLengthSeconds; the equation of time is the
341
- // certified Sun's. docs/11 § "The retired RA day offset".)
342
-
343
- // ── Cardinal points (the fitted model retired, R1)──────────────────────────────────────────────────
344
- // Plan 06 R1: the fitted cardinal-point model (CARDINAL_POINT_* — the §10
345
- // derived form on the frozen era clock, fitted to the retired K scene's
346
- // events; measured 60–140 min from Meeus ch. 27 at 0..−1000 and 6–25 min
347
- // in 1500–2900) left this package. The instants are the CROSSINGS of the
348
- // one Sun the scene renders and the eclipse chain certifies — see
349
- // `cardinalCrossingJdUT` below the finders.
350
- // ── One-source cardinal structure (D4b) ───────────────────────────────────
351
- // The EoC layer (year lengths, crossing offsets, the e(t)-proportional
352
- // spread) on the one-source movement's own e(t)/ϖ(t) — the MODE tier (the
353
- // package's embedded deep-modes artifact; the banked-series tier is the
354
- // repo-data-bound instrument). Absolute dates deliberately absent — see
355
- // cardinal/one-source-structure.cjs. The mean year is SI SECONDS: the
356
- // sidereal year of date reduced by the SECULAR equinox precession
357
- // (axial0·H(t)/H₀ with H(t) the UNIT = 13·T_p,composed — plan 06 D6/Phase
358
- // 3, the movement's leg-1 convention, NOT the H/13 kinematic identity at
359
- // J2000; the two are a recorded 0.09% relation tension there).
360
- // THE ONE of-date year-length family (owner: "move to 1 implementation")
361
- // — createYearLengths owns tropical (equinox-rate mean, wobble included),
362
- // sidereal (the D6 λ̇ channel), anomalistic (the cardinal structure on
363
- // the SAME mean), all λ̇-corrected coherently, SI seconds, and the beats
364
- // of that pair. The internal laws, the kinematic beat constructions
365
- // (axialPrecessionYears*), the scene engine and the frozen era clock
366
- // stay on their own families as before.
367
- // S3 tier unification: when the caller injects the governed secular-
368
- // series artifact (createModel opts.secularSeriesArtifact — the API/Node
369
- // path; the npm package alone has no 8 MB series), the movement runs the
370
- // SERIES tier exactly like the browser — killing the mode-vs-series
371
- // value split (measured: 6 s on the anomalistic year at J2000).
372
- // The ONE-SOURCE movement: the hybrid's ε(t) (plan 06 Phase 3 S3b — THE
373
- // published obliquity) and the one-family year lengths, on the same
374
- // per-tier sampler.
375
- const oneSourceM = (() => {
376
- const AEarth = /** @type {any} */ (CHAIN_ARTIFACT).j2000AnchorElements.earth;
377
- // The ψ̇ anchor: the IAU 2006 general precession p_A (plan 09; 25,771.575
378
- // yr — the ONE home certifiedAxialPrecessionJ2000Years, twins in
379
- // tools-lib and the browser). History: plan 06 S5 anchored it on the
380
- // certified year combs' beat at 2000 (25,771.4); before that the device's
381
- // mean pair (meanSiderealYearDays/meanSolarYearDays → 25,796) anchored
382
- // this movement's realized precession 25 yr off — the hidden split behind
383
- // the API's 365.242204 tropical mean (found in the owner's
384
- // one-implementation drive).
385
- const axial0 = certifiedAxialPrecessionJ2000Years(); // S5: the ONE home
386
- // D6 → Phase 3 (plan 06): the deep-time ψ̇(t) the hybrid precesses on is
387
- // the COMPOSED lunisolar rate — its period of date T_p(t) read directly
388
- // (plan 09 step 5c; it was period₀·H(t)/H₀ on the unit, the same number
389
- // to 2e-16 since the unit scales with T_p). Not the frozen era clock's
390
- // H_era (which misses the lunar 1/a³ growth: 70.9 vs Lantink 2022's
391
- // 108.6 ± 8.5 ″/yr at 2.46 Ga; the composed rate reads 104.5).
392
- const seriesArt = /** @type {any} */ (secularSeriesArtifact);
393
- const sb = seriesArt && seriesArt.bodies && seriesArt.bodies.earth;
394
- // Plan 12 §8.5 (owner): the hybrid's own domain is the banked ζ series' span (years; null
395
- // without a series). Beyond it the ζ-mode tail carries the Quaternary mean with no tidal
396
- // evolution — the API refuses the obliquity there and serves the cited mean beside it.
397
- const obliquityHybridDomainYears = sb
398
- ? /** @type {[number, number]} */ ([2000 + seriesArt.t0Yr, 2000 + seriesArt.t0Yr + (sb.zetaQ.length - 1) * sb.stepYr])
399
- : null;
400
- const hist = createDeepOrbitalHistory({
401
- zModes: DEEP_MODES_ARTIFACT.earthZ,
402
- zetaModes: DEEP_MODES_ARTIFACT.earthZeta,
403
- ...(sb ? {
404
- zetaSeries: { t0Yr: seriesArt.t0Yr, stepYr: sb.stepYr, q: sb.zetaQ, p: sb.zetaP },
405
- zSeries: { t0Yr: seriesArt.t0Yr, stepYr: sb.stepYr, q: sb.zQ, p: sb.zP },
406
- } : {}),
407
- anchorE: AEarth.e,
408
- anchorPeriEclipticDeg: AEarth.lonPeriEclipticDeg,
409
- anchorInclEclipticDeg: AEarth.inclEclipticDeg,
410
- anchorAscNodeEclipticDeg: AEarth.ascNodeEclipticDeg,
411
- axialPrecessionYearsJ2000: axial0,
412
- // Phase 3 S3b: the hybrid's initial condition is the OBSERVED J2000
413
- // mean obliquity — the IAU 2006 input constant (84381.406″), the same
414
- // anchor the browser's _deepHistSeries and the Node one-source movement
415
- // use. It was the K comb's own J2000 value, 0.233″ above IAU: a
416
- // rendering-device residual imported as the initial condition of the
417
- // physics (found when the package began publishing the hybrid).
418
- obliquityJ2000Deg: C.earthOrbital.obliquityJ2000_deg,
419
- // The CONSTANT's scaling (the torque period), not the published rate of date: the
420
- // integration's own ŝ·n̂ carries the obliquity's cosine, so the cited mean obliquity's
421
- // factor (plan 12 §8 option (a)) must not enter twice. Bit-identical to the pre-§8 form.
422
- axialPrecessionYearsAtYearFn: (yr) => deepLod.lunisolarTorquePeriodYearsAtAge((startmodelYear - yr) / 1e6) ?? axial0,
423
- // THE PRECESSION OF DATE (plan 06 §9 item 10): on top of the secular
424
- // law above, the solar torque at the eccentricity of date and the
425
- // oblateness of date from the GIA channel — both exactly 1 at J2000,
426
- // so the unit H(t) and the J2000 anchor are untouched. The channel is
427
- // read PURE (alphaGia, not the lattice-pin-aware earthMoiFactorAtAge):
428
- // the samplers are cached, their values must be pure in year.
429
- solarTorqueShareJ2000: EPOCH_PARAMS.precessionSolarShareJ2000,
430
- // options.giaEllipticityOfDate === false: the orbital forcing alone (the T1 line derivation).
431
- dynamicalEllipticityRatioAtYearFn: options.giaEllipticityOfDate === false ? undefined : alphaGia.j2RatioAt,
432
- });
433
- // PER-TIER ROUTING (the anomalistic-contamination fix): one sampler PER
434
- // grid tier, built on first entry and KEPT — a query always reads the
435
- // tier its own span selects. The former single grown sampler replaced
436
- // the 100-yr grid with the 1000/5000-yr one after any deep-time probe,
437
- // and the year-length rates (±0.5-yr central differences through the
438
- // grid) then returned grid-segment averages instead of local rates
439
- // (anomalistic of date +2.63 s / +19 s, visit-order dependent).
440
- const samplers = /** @type {Map<number, any>} */ (new Map());
441
- const gridStep = (/** @type {number} */ n) => (n <= 50000 ? 100 : n <= 2000000 ? 1000 : 5000);
442
- const tierSpan = (/** @type {number} */ n) => (n <= 50000 ? 50000 : 2000000);
443
- const sampleAt = (/** @type {number} */ year) => {
444
- const t = year - 2000, need = Math.max(20000, Math.abs(t) * 1.25);
445
- if (need > 2000000) {
446
- // The deep tier (5000-yr grid): walked in the queried DIRECTION only,
447
- // to two nodes past the query, and EXTENDED by the factory's shared
448
- // walk when a deeper epoch arrives (plan 13 A1). It was built to
449
- // ±1.25·|t| in both directions and rebuilt from J2000 on every deeper
450
- // probe — bit-identical values, 4× the RK4 steps of one walk.
451
- const span = Math.ceil((Math.abs(t) + 2 * 5000) / 5000) * 5000;
452
- return hist.build(t < 0 ? -span : span, 0, 5000).at(t);
453
- }
454
- const span = tierSpan(need);
455
- if (!samplers.has(span)) samplers.set(span, hist.build(span, -span, gridStep(span)));
456
- return samplers.get(span).at(t);
457
- };
458
- // The anomalistic year rides THIS movement's own apsidal line (the
459
- // scene's perihelion) — the factory owns the construction; the planet
460
- // chain's secular tangent is no longer a member (year-lengths.cjs).
461
- const yearLengths = createYearLengths({
462
- sampleAt,
463
- massLossSiderealSecondsAtYearFn: (year) => deepLod.siderealYearSecondsAtAge(yearToTMa(year)),
464
- });
465
- return {
466
- yearLengths,
467
- /** The one-source sample at a decimal year (the engine year) — ε, e, ϖ of date, the Earth-orbit normal (the fields the frame of date and the invariable-plane elements read). @param {number} year */
468
- sampleAt,
469
- /** EXPERIMENT accessors: the series' e and equinox-referenced ϖ of date. @param {number} year @returns {number} */
470
- eAt: (year) => sampleAt(year).e,
471
- /** @param {number} year @returns {number} */
472
- periOfDateDegAt: (year) => sampleAt(year).periOfDateDeg,
473
- /** The hybrid's obliquity at a decimal year, degrees — the banked series inside its span, the α(t)-coupled ζ-tail integration beyond (the deep sampler grows ~0.1 s/Myr). @param {number} year @returns {number} */
474
- epsAt: (year) => sampleAt(year).epsDeg,
475
- /** The hybrid's own domain, [yearFrom, yearTo] — the banked ζ series' span (plan 12 §8.5); null without a series. */
476
- obliquityHybridDomainYears,
477
- /** The precession of date's two factors at a decimal year (the factory's own — ONE home). @param {number} year @returns {{solarTorque: number, ellipticity: number}} */
478
- precessionOfDateFactorsAt: (year) => hist.precessionOfDateFactorsAt(year - 2000),
479
- /** The spin axis's INERTIAL precession period at J2000, years — the self-anchored
480
- * lunisolar rate the hybrid integrates ŝ on (deep-orbital-history D4e): the general
481
- * precession anchor plus the planetary mean the ecliptic's own motion carries. */
482
- axialPrecessionYearsLunisolarJ2000: hist.axialPrecessionYearsLunisolarJ2000,
483
- };
484
- })();
485
- const yearLengthsM = oneSourceM.yearLengths;
486
- /** THE one-family tropical year of date in SI seconds: route B inside the
487
- * one-family window, the tidal-chain year beyond (plan 06 R1 — the Sun's
488
- * mean longitude, the cardinal instants and the published year lengths
489
- * all ride this one function). @param {number} year @returns {number} */
490
- const oneFamilyTropicalYearSeconds = (year) => (Math.abs(year - 2000) <= ONE_FAMILY_WINDOW_YEARS
491
- ? yearLengthsM.tropicalYearSecondsAtYear(year)
492
- : (deepLod.tropicalYearSecondsAtAge(yearToTMa(year)) ?? meanSolarYearDays * 86400));
493
-
494
- /** Tropical year of date in DAYS OF THE EPOCH (the kinematic day below) —
495
- * the one-family SI year over the epoch's day length, so solarYearSeconds
496
- * ≡ the one-family year. Plan 06 R1: formerly the fitted cardinal model's
497
- * 4-mean. @param {number} year @returns {number} */
498
- const tropicalYearDays = (year) => oneFamilyTropicalYearSeconds(year) / dayLengthSeconds(year);
499
-
500
- /** Layer-4 solar day of date in SI seconds — LOD_real, the shipped
501
- * observable (the tweakpane Solar Day row, the registry's lodRealPhysical,
502
- * the Framework Verification panel's day): the kinematic day of date + the
503
- * ecliptic missing-motion term on the nodal period + the calibrated ΔT
504
- * cycle stack incl. the core-mantle swing. Twin of tools/lib
505
- * computeLodRealSecondsAtEpoch and the browser's _vfpSolarDaySecondsOfDate
506
- * (86,400.001780 s at 2000). @param {number} year @returns {number} */
507
- const lodRealSeconds = (year) =>
508
- dayLengthSeconds(year)
509
- + (deepLod.eclipticLodCorrectionSecondsAtAge(yearToTMa(year)) ?? 0)
510
- + dtCycleLodCorrectionSum(year);
511
- /** Mean sidereal day of date in SI seconds — on LOD_real (plan 08 storyline
512
- * sweep, 2026-10-09: the published day bases are the SI day of exactly
513
- * 86,400 s, the IAU constants, and LOD_real; this rode the KINEMATIC day
514
- * before, 1.7 ms below the simulator's readout at J2000). One rotation
515
- * more against the equinox than there are solar days in a year.
516
- * @param {number} year @returns {number} */
517
- const siderealDaySeconds = (year) => {
518
- const Y = tropicalYearDays(year);
519
- return (lodRealSeconds(year) * Y) / (Y + 1);
520
- };
521
- /** Mean stellar day of date in SI seconds — the sidereal day plus the
522
- * equinox's daily regression projected onto the equator (on LOD_real, like
523
- * the sidereal day). @param {number} year @returns {number} */
524
- const stellarDaySeconds = (year) => {
525
- const tMa = yearToTMa(year);
526
- // S5 (plan 06): one equinox turn per T_p(t) — the composed lunisolar
527
- // period (its certified J2000 anchor past the chain's domain), NOT the
528
- // unit's counter H(t)/13 (0.086 % slow; 7 µs on the 8.37 ms offset).
529
- const TpRaw = deepLod.lunisolarPrecessionPeriodYearsAtAge(tMa);
530
- const Tp = TpRaw === null ? certifiedAxialPrecessionJ2000Years() : TpRaw;
531
- const syD = tropicalYearDays(year);
532
- const sidDay = siderealDaySeconds(year);
533
- // Layer A (plan 06): the RA projection reads the published ε (the one-source
534
- // hybrid), not the K comb — the browser's _sceneEpsTargetDeg twin; 1e-8 s.
535
- const raProjection = Math.cos((oneSourceM.epsAt(year) * Math.PI) / 180);
536
- return (sidDay / Tp / (syD + 1)) * raProjection + sidDay;
537
- };
538
-
539
- // ── Moon at epoch ─────────────────────────────────────────────────────────
540
- /** Solar-Δa-corrected Kepler month. @param {number} year @returns {number} */
541
- const moonSiderealMonthDaysAt = (year) => {
542
- const tMa = yearToTMa(year);
543
- const lod = deepLod.lodSecondsAtAge(tMa);
544
- const aAppM = moonDistanceMetresAtAge(tMa);
545
- if (lod === null || aAppM === null) return NaN;
546
- const aAppKm = aAppM / 1000;
547
- const sidYrDays = deepLod.siderealYearSecondsAtAge(tMa) / lod;
548
- const deltaA = aAppKm * (1 / (MASS_RATIO_EARTH_MOON + 1)) * (moonSiderealMonthInput / sidYrDays);
549
- const aCorrM = (aAppKm + deltaA) * 1000;
550
- const monthSeconds = 2 * Math.PI * Math.sqrt(Math.pow(aCorrM, 3) / EPOCH_PARAMS.gmEarthMoonM3S2);
551
- return monthSeconds / lod;
552
- };
553
-
554
- // ── ΔT (TT − UT1): raw Simpson + sequential stack adds ────────────────────
555
- /** @param {number} tMa @returns {number} */
556
- // Exact-argument memo (the alphaMemo pattern above): the cardinal Newton
557
- // solve asks for ΔT at the SAME jd twice per step (jdTTFromUT in the
558
- // apparent Sun and again under the planetary completion) — measured 8.8 →
559
- // 5.6 ms per panel year (8 solves) at modern epochs, values bit-identical.
560
- /** @type {Map<number, number>} */
561
- const dtMemo = new Map();
562
- /** @param {number} tMa @returns {number} */
563
- const meanDeltaTSecondsAtAge = (tMa) => {
564
- if (tMa === 0) return 0;
565
- const hit = dtMemo.get(tMa);
566
- if (hit !== undefined) return hit;
567
- let result = deepLod.deltaTRawSecondsAtAge(tMa);
568
- const yearY = 2000 - tMa * 1e6;
569
- result += dtCycles.cycleDeltaTSecondsAt('bond', yearY);
570
- result += dtCycles.cycleDeltaTSecondsAt('hallstatt', yearY);
571
- result += dtCycles.cycleDeltaTSecondsAt('jose5', yearY);
572
- result += dtCycles.cycleDeltaTSecondsAt('jose4', yearY);
573
- result += dtCycles.swingDeltaTSecondsAt(yearY);
574
- if (dtMemo.size >= 8192) dtMemo.clear();
575
- dtMemo.set(tMa, result);
576
- return result;
577
- };
578
- /** @param {number} year @returns {number} */
579
- const deltaTSeconds = (year) => C.earthOrbital.deltaTStart + meanDeltaTSecondsAtAge(yearToTMa(year));
580
-
581
- // ── Planets: Fibonacci-law derivation chain + orientation ─────────────────
582
- const PLANET_KEYS = ['mercury', 'venus', 'mars', 'jupiter', 'saturn', 'uranus', 'neptune'];
583
- const massEarthAlone = GM_EARTH_ALONE / G_CONSTANT;
584
- /** @type {Record<string, number>} */
585
- const massFraction = {};
586
- for (const k of PLANET_KEYS) massFraction[k] = 1 / C.physicalConstants.massRatioDE440[k];
587
- massFraction.earth = massEarthAlone / M_SUN;
588
-
589
- // (plan 07 R6: eccentricityAmplitudeK — the K constant inverted from
590
- // Earth's calibration — and the t2000 phase offset it fed went with the
591
- // eccentricity law; plan 07 R5: PSI — ψ = 3·A_earth·√(m_E/m_☉) — and the
592
- // inclination law it fed went the same way. systemResetN is kept where
593
- // other consumers read it.)
594
-
595
- // Plan 07 R1/R8: the internal PLANET_RECORDS — the device record the
596
- // fitters once read (the H·num/den perihelion period from
597
- // perihelionEclipticFraction, fibonacciD, antiPhase, inclinationCycleAnchor,
598
- // the −8H/N node period) — had no reader left after R6/R5 and is gone with
599
- // its JSON inputs. What remains is the PUBLISHED record below.
600
- /** @type {Record<string, Record<string, any>>} */
601
- const PUBLISHED_PLANET_RECORDS = {}; // what model.planets.record() serves (R8)
602
- for (const k of PLANET_KEYS) {
603
- const ar = C.planetOrbitalElements[k];
604
- // Plan 06 Phase 7 commit 2: the K device's integer axial and obliquity
605
- // fractions are retired; the obliquity is the DERIVED J2000 obliquity of
606
- // the spin channel (the IAU pole against the chain's J2000 plane, acute
607
- // form) — from the governed artifacts, no fractions. (The g-mode beat the
608
- // K law's period rode left with the internal record at plan 07 R1/R8.)
609
- const obliquityDerived = computeObliquityJ2000Deg({
610
- spin: C.planetSpinPhysical[k],
611
- anchorInclEclipticDeg: /** @type {any} */ (CHAIN_ARTIFACT).j2000AnchorElements[k].inclEclipticDeg,
612
- anchorAscNodeEclipticDeg: /** @type {any} */ (CHAIN_ARTIFACT).j2000AnchorElements[k].ascNodeEclipticDeg,
613
- obliquityJ2000Deg: C.earthOrbital.obliquityJ2000_deg,
614
- });
615
- const obliquityMean = Math.min(obliquityDerived, 180 - obliquityDerived); // acute form of the derived J2000 obliquity
616
- // R8 (plan 07): what `model.planets.record()` PUBLISHES is the structural
617
- // record, not the device. The internal record that kept the ψ/K law
618
- // outputs for the fitting pipeline went piecewise: the K half at R6, the ψ
619
- // half at R5, the lattice inputs at R1/R8 (nothing read it by then).
620
- // Nothing outside needs the device, and serving it made /v1/bodies
621
- // contradict the model's own chain on the SAME quantities: Mercury's node
622
- // cycle 298,060 yr against the chain's 232,001, Jupiter's perihelion period
623
- // 68,783 against 304,456, Neptune's 670,634 against 2,136,796. The two
624
- // views are separated here: internal keeps the device for the fitters,
625
- // published carries the structural inputs and the chain.
626
- // · kept as-is — the REFERENCE inputs (IAU J2000 ϖ, Souami & Souchay
627
- // node and inclination, DE440 e, the period input, the axial tilt) and
628
- // `obliquityMean`, which is engine-DERIVED (the spin channel against
629
- // the chain's J2000 plane), not a device value.
630
- // · repointed, the quantity surviving as a chain eigenmode — the two
631
- // PERIODS, now 1,296,000/g and 1,296,000/s. Signs kept as before:
632
- // apsidal prograde positive, nodal regression negative.
633
- // · dropped, the quantity BEING the device — ascendingNodeCyclesIn8H,
634
- // fibonacciD, antiPhase, inclinationCycleAnchor, wobblePeriod (lattice
635
- // inputs and the device's beat), and the inclination/eccentricity
636
- // mean+amplitude+phase triples, which presume a single oscillation the
637
- // chain does not have. Their honest replacement is the value AT AN
638
- // EPOCH, which `/v1/bodies` already serves in its `at[]` rows.
639
- const chainModes = /** @type {any} */ (CHAIN_ARTIFACT);
640
- const gArc = chainModes.g[k] && chainModes.g[k].arcsecPerYr;
641
- const sArc = chainModes.s[k] && chainModes.s[k].arcsecPerYr;
642
- PUBLISHED_PLANET_RECORDS[k] = Object.freeze({
643
- name: PLANET_DISPLAY_NAMES[k], // presentation — planets/display-names, the deleted JSON's last leaf (plan 11 step 6)
644
- solarYearInput: ar.solarYearInput,
645
- longitudePerihelion: ar.longitudePerihelion,
646
- ascendingNodeInvPlane: ar.ascendingNodeInvPlaneClosedForm, // the closed-form verified J2000 node — astro-reference since plan 11 step 2
647
- invPlaneInclinationJ2000: ar.invPlaneInclinationJ2000,
648
- orbitalEccentricityJ2000: ar.orbitalEccentricityJ2000,
649
- axialTiltJ2000: ar.axialTiltJ2000,
650
- obliquityMean,
651
- perihelionEclipticYears: gArc ? 1296000 / gArc : undefined,
652
- ascendingNodePeriod: sArc ? 1296000 / sArc : undefined,
653
- });
654
- }
655
-
656
- // ── The planets' spin channel (plan 06 Phase 7): the precession constant
657
- // from each planet's OWN torques (astro-reference planetSpinPhysical) on the
658
- // model's OWN orbit — the chain's J2000 a/e/plane and the deep ζ table —
659
- // integrated as dŝ/dt = α(ŝ·n̂)(ŝ×n̂) from the IAU J2000 pole. ONE home:
660
- // planets/spin-channel.cjs; built lazily per planet, pure in `year`.
661
- /** @type {Map<string, ReturnType<typeof createPlanetSpinChannelFromArtifacts>>} */
662
- const spinChannels = new Map();
663
- /** @param {string} k */
664
- const planetSpin = (k) => {
665
- if (!PLANET_KEYS.includes(k)) throw new Error(`planets.spin: unknown planet '${k}'`);
666
- let ch = spinChannels.get(k);
667
- if (!ch) {
668
- ch = createPlanetSpinChannelFromArtifacts({
669
- key: k,
670
- planetSpinPhysical: C.planetSpinPhysical,
671
- chainAnchorElements: /** @type {any} */ (CHAIN_ARTIFACT).j2000AnchorElements,
672
- planetZeta: /** @type {any} */ (DEEP_MODES_ARTIFACT).planetZeta,
673
- massFractionOfSun: massFraction[k],
674
- gmSunKm3S2: GM_SUN,
675
- obliquityJ2000Deg: C.earthOrbital.obliquityJ2000_deg,
676
- });
677
- spinChannels.set(k, ch);
678
- }
679
- return ch;
680
- };
681
-
682
- // The planets' elements of date ride THE CHAIN — the only planet path the
683
- // simulator renders (K5) — with the D5 series handover beyond each planet's
684
- // measured boundary when the series artifact is supplied: the mirror of the
685
- // browser's _kcElementsOfDate → _kcSeriesSecularEl. Until v16.2 the API
686
- // served the retired device here (the record's linear lattice rates:
687
- // perihelionEclipticYears, ascendingNodePeriod = −8H/N, the balanced-year
688
- // inclination oscillation) — a published surface disagreeing with the
689
- // simulator (owner-found audit, 2026-10). Those forms
690
- // (planets/orientation.cjs) now serve only the structural record and the
691
- // browser's no-chain bodies (Pluto, Halley, Eros). The chain's argument is
692
- // dynamical time; the API's decimal year is taken as that argument (ΔT on a
693
- // secular element is far below its stated accuracy).
694
- let planetChains = /** @type {any} */ (null);
695
- let planetSeriesOverride = /** @type {any} */ (null);
696
- let planetNodeOriginSSDeg = /** @type {number|null} */ (null);
697
- /** @param {string} k @param {number} year */
698
- const planetChainElementsAt = (k, year) => {
699
- if (!planetChains) planetChains = buildPlanetChainsFromArtifactData(/** @type {any} */ (CHAIN_ARTIFACT));
700
- const el = computePlanetElementsAtYear(year, planetChains[k], planetChains);
701
- const seriesArt = /** @type {any} */ (secularSeriesArtifact);
702
- if (!seriesArt) return el;
703
- if (!planetSeriesOverride) {
704
- planetSeriesOverride = createSecularSeriesOverride({
705
- series: seriesArt,
706
- anchorElements: /** @type {any} */ (CHAIN_ARTIFACT).j2000AnchorElements,
707
- invariablePlane: /** @type {any} */ (CHAIN_ARTIFACT).invariablePlane,
708
- planetZModes: /** @type {any} */ (DEEP_MODES_ARTIFACT).planetZ,
709
- planetZetaModes: /** @type {any} */ (DEEP_MODES_ARTIFACT).planetZeta,
710
- });
711
- }
712
- return planetSeriesOverride.applyToElements(k, year, el);
713
- };
714
- /** Ecliptic longitude of perihelion, J2000 ecliptic and equinox (the chain's ϖ). @param {string} k @param {number} year @returns {number} */
715
- const planetPerihelionDeg = (k, year) => planetChainElementsAt(k, year).lonPeriEclipticDeg;
716
- /** The S&S longitude origin as an s-frame longitude (J2000-fixed, derived). @returns {number} */
717
- const nodeOriginSSDeg = () => {
718
- if (planetNodeOriginSSDeg === null) {
719
- planetNodeOriginSSDeg = computeEquatorNodeOriginSFrameDeg(/** @type {any} */ (CHAIN_ARTIFACT).invariablePlane, C.earthOrbital.obliquityJ2000_deg);
720
- }
721
- return planetNodeOriginSSDeg;
722
- };
723
- /** Ascending node on the model's invariable plane, from the plane's ascending node on the ICRF equator (the Souami & Souchay 2012 origin; derived conversion, inv-plane-frame). @param {string} k @param {number} year @returns {number} */
724
- const planetAscNodeDeg = (k, year) => convertNodeSFrameToEquatorOriginDeg(planetChainElementsAt(k, year).ascNodeInvPlaneDeg, nodeOriginSSDeg());
725
- /** Inclination to the model's invariable plane (exact orbit-normal rotation). @param {string} k @param {number} year @returns {number} */
726
- const planetInclinationDeg = (k, year) => planetChainElementsAt(k, year).inclInvPlaneDeg;
727
- /** Orbital eccentricity of date, from the same chain elements (R8): the
728
- * honest replacement for the record's retired base + amplitude + phase
729
- * triple, which presumed a single oscillation the chain's multi-mode
730
- * e-vector does not have. @param {string} k @param {number} year @returns {number} */
731
- const planetEccentricity = (k, year) => planetChainElementsAt(k, year).e;
732
-
733
- // ── Earth's invariable-plane elements of date (plan 07 R5/R9) ─────────────
734
- // ONE published i_inv / Ω_inv everywhere — the mirror of the browser's
735
- // inclInvPlaneModel / ascNodeInvPlaneModel (identical ops): the engine's
736
- // own Earth-orbit normal of date (the one-source sampler — the banked
737
- // secular series inside ±10 Myr, the mode tail beyond) against the
738
- // artifact's invariable plane, the node expressed in the Souami & Souchay
739
- // longitude origin through the same derived conversion the planets' node
740
- // takes. The H/3 cosine on the fixed apsidal carrier and the −H/5 linear
741
- // node that served here until R5 were the K device: 1.578677° against
742
- // 1.578422° at J2000, 1.24° against 1.91° at −100 kyr, and 112° against
743
- // 94° of node there. Doc 05 banks the one-source reading at rms 0.003°
744
- // vs La2010 over −500 kyr. Argument: the engine year (dynamical time,
745
- // Julian from J2000 TT — the sampler's own coordinate, as the chain's).
746
- const invPlaneGeometry = (() => {
747
- const ip = /** @type {{ inclEclipticDeg: number, ascNodeEclipticDeg: number }} */ (/** @type {any} */ (CHAIN_ARTIFACT).invariablePlane);
748
- const D2R = Math.PI / 180;
749
- const si = Math.sin(ip.inclEclipticDeg * D2R), ci = Math.cos(ip.inclEclipticDeg * D2R);
750
- const nInv = [si * Math.sin(ip.ascNodeEclipticDeg * D2R), -si * Math.cos(ip.ascNodeEclipticDeg * D2R), ci];
751
- const xIp = [Math.cos(ip.ascNodeEclipticDeg * D2R), Math.sin(ip.ascNodeEclipticDeg * D2R), 0];
752
- const yIp = [nInv[1] * xIp[2] - nInv[2] * xIp[1], nInv[2] * xIp[0] - nInv[0] * xIp[2], nInv[0] * xIp[1] - nInv[1] * xIp[0]];
753
- return { ascNodeEclipticDeg: ip.ascNodeEclipticDeg, nInv, xIp, yIp };
754
- })();
755
- /** The engine Earth-orbit normal of date, J2000 ecliptic frame. @param {number} year @returns {number[]} */
756
- const earthOrbitNormalJ2000At = (year) => {
757
- const s = oneSourceM.sampleAt(year);
758
- return [s.orbitNormalX, s.orbitNormalY, Math.sqrt(Math.max(0, 1 - s.orbitNormalX * s.orbitNormalX - s.orbitNormalY * s.orbitNormalY))];
759
- };
760
- /** Earth's inclination of date to the model's invariable plane, degrees. @param {number} year @returns {number} */
761
- const inclinationDeg = (year) => {
762
- const nE = earthOrbitNormalJ2000At(year), nInv = invPlaneGeometry.nInv;
763
- const d = nE[0] * nInv[0] + nE[1] * nInv[1] + nE[2] * nInv[2];
764
- return Math.acos(Math.min(1, Math.max(-1, d))) / (Math.PI / 180);
765
- };
766
- /** Earth's ascending node of date on the invariable plane, S&S origin, degrees in [0, 360). @param {number} year @returns {number} */
767
- const ascendingNodeDeg = (year) => {
768
- const D2R = Math.PI / 180;
769
- const { ascNodeEclipticDeg, nInv, xIp, yIp } = invPlaneGeometry;
770
- const nE = earthOrbitNormalJ2000At(year);
771
- let N = [nInv[1] * nE[2] - nInv[2] * nE[1], nInv[2] * nE[0] - nInv[0] * nE[2], nInv[0] * nE[1] - nInv[1] * nE[0]];
772
- const L = Math.hypot(N[0], N[1], N[2]);
773
- N = [N[0] / L, N[1] / L, N[2] / L];
774
- const sFrameDeg = ((Math.atan2(N[0] * yIp[0] + N[1] * yIp[1] + N[2] * yIp[2],
775
- N[0] * xIp[0] + N[1] * xIp[1] + N[2] * xIp[2]) / D2R + ascNodeEclipticDeg) % 360 + 360) % 360;
776
- return convertNodeSFrameToEquatorOriginDeg(sFrameDeg, nodeOriginSSDeg());
777
- };
778
-
779
- // ── Time axis: exact JD ↔ model-year conversion ───────────────────────────
780
- // The model's `year` inputs live on the SI axis (the axis the fits were
781
- // anchored on — tools/lib `_jdToSIyear`): linear in SI 86400-s days from the
782
- // model start, so the JD↔year map is closed-form and exact to double
783
- // precision. Callers holding an exact JD (e.g. 2058768.5385006 TT) convert
784
- // here and NEVER roll their own — a caller-side linear-vs-calendar mix once
785
- // put the eclipse umbra twin 8 km off (§12h; ≤1 m once unified).
786
- const siTropicalYearDays = meanTropicalYearJ2000Seconds / 86400;
787
- /** Model year (SI axis) at a JD(TT). @param {number} jd @returns {number} */
788
- const yearFromJD = (jd) => startmodelYear + (jd - startmodelJD) / siTropicalYearDays;
789
- /** JD(TT) at a model year (SI axis). @param {number} year @returns {number} */
790
- const jdFromYear = (year) => startmodelJD + (year - startmodelYear) * siTropicalYearDays;
791
-
792
- // ── Lunar theory: the shared-chain assembly (§7a slice-2b) ────────────────
793
- // Wiring mirrors the engine call sites exactly — tools/lib/deep-time.js
794
- // (ecc channel, month chain, J2000 precession anchors),
795
- // tools/lib/scene-graph.js (chain-cycles, arguments, series) and
796
- // tools/verify/eclipse-audit.js (finders). Deep-time and framework-native
797
- // are hardwired ON here: they are the shipped defaults; the A/B env
798
- // toggles (SG_DEEP_TIME/MOON_ARGS_PURE_MEEUS) stay an engine concern.
799
- const j2000JD = 2451545.0;
800
- const julianCenturyDays = 36525;
801
-
802
- // J2000 Moon precession anchors (plan 09: the IAU inputs themselves). The
803
- // legacy-'ICRF'-named inputs hold the EQUINOX-OF-DATE observables (Meeus/
804
- // IERS 3231.50 d / 6798.38 d); the star-referenced (inertial) periods
805
- // follow by removing the equinox's own precession, 1/T_E = 1/T_I ∓ 1/T_p
806
- // (perigee advances, node regresses), T_p the J2000 precession anchor.
807
- // The retired era device counted these as eighth-integers per 8H and read
808
- // the periods back as totalDaysInH/N (the ∓13 was H/13 cycles per H).
809
- const { moonApsidalJ2000Seconds } = chain;
810
- // (Plan 09 step 5c: the per-H of-date cycle counts and the month chain's H²
811
- // scaling of the of-date periods are deleted; the of-date perigee/node are
812
- // the chain's count compositions.)
813
-
814
- // Derived months on the IAU inputs (constants.js §Moon derived months)
815
- const moonTropicalMonthDays = 1 / (1 / moonSiderealMonth + 1 / axialPrecessionJ2000Days);
816
- const moonAnomalisticMonthDays = 1 / (1 / moonSiderealMonth - 86400 / moonApsidalJ2000Seconds);
817
- const moonSynodicMonthDays = moonSiderealMonth * meanSiderealYearDays / (meanSiderealYearDays - moonSiderealMonth);
818
-
819
- // The Layer-2 month/precession chain (Brouwer-Clemence m² scaling × the
820
- // e_E-line modulation on the ONE deep e — decision (ii), plan 02 §8) is
821
- // the deep-time chain's `moonChain` above.
822
-
823
- // Chain-cycle integrator. S5/S12 conventions: age anchor = startmodelYear
824
- // (the scene's t_Ma convention), grid anchor C(2000) = 0 — grid anchor ≠
825
- // age anchor, deliberately. One stable period fn per chain so the shared
826
- // Float64Array tables key correctly and persist.
827
- const chainCycles = createChainCycleIntegrator({
828
- ageAnchorYear: startmodelYear,
829
- tropicalYearJ2000Seconds: meanTropicalYearJ2000Seconds, // the SI tick (plan 09 step 7c′)
830
- isDeepTime: () => true,
831
- });
832
- /** @param {number} tMa @returns {number|null} */
833
- const nodalMonthPeriodFn = (tMa) => moonChain.nodalMonthSecondsAtAge(tMa);
834
- /** @param {number} tMa @returns {number|null} */
835
- const tropicalMonthPeriodFn = (tMa) => moonChain.tropicalMonthSecondsAtAge(tMa);
836
- /** @param {number} tMa @returns {number|null} */
837
- const anomalisticMonthPeriodFn = (tMa) => moonChain.anomalisticMonthSecondsAtAge(tMa);
838
- /** @param {number} tMa @returns {number|null} */
839
- const siderealMonthPeriodFn = (tMa) => moonChain.siderealMonthSecondsAtAge(tMa);
840
- const jupiterT0Seconds = C.planetOrbitalElements.jupiter.solarYearInput * 86400;
841
- /** @param {number} tMa @returns {number} */
842
- const jupiterPeriodFn = (tMa) => driver2PeriodSecondsAtAge(tMa, jupiterT0Seconds, EPOCH_PARAMS.solarMassLossFracPerYear);
843
- /** @param {number} a @param {number} b @returns {number|null} */
844
- const mcDraconic = (a, b) => chainCycles.cyclesBetween(nodalMonthPeriodFn, a, b);
845
- /** @param {number} a @param {number} b @returns {number|null} */
846
- const mcTropical = (a, b) => chainCycles.cyclesBetween(tropicalMonthPeriodFn, a, b);
847
- /** @param {number} a @param {number} b @returns {number|null} */
848
- const mcAnomalistic = (a, b) => chainCycles.cyclesBetween(anomalisticMonthPeriodFn, a, b);
849
- /** @param {number} a @param {number} b @returns {number|null} */
850
- const mcJupiter = (a, b) => chainCycles.cyclesBetween(jupiterPeriodFn, a, b);
851
- /** The Moon's sidereal (star-referenced) orbits — the series' A3 argument (plan 09 step 5c). @param {number} a @param {number} b @returns {number|null} */
852
- const mcSidereal = (a, b) => chainCycles.cyclesBetween(siderealMonthPeriodFn, a, b);
853
- /** @param {number} a @param {number} b @returns {number|null} */
854
- const mcApsidalOfDate = (a, b) => {
855
- const t = mcTropical(a, b), n = mcAnomalistic(a, b);
856
- return (t === null || n === null) ? null : t - n;
857
- };
858
- /** @param {number} a @param {number} b @returns {number|null} */
859
- const mcNodalOfDate = (a, b) => {
860
- const dr = mcDraconic(a, b), t = mcTropical(a, b);
861
- return (dr === null || t === null) ? null : dr - t;
862
- };
863
-
864
- // (The K snapshot-phase obliquity the lunar chain was certified against left
865
- // with layer A / item 3c: the arguments' carrier reads the published ε.)
866
-
867
- // The argument skeleton (the _FW_MOON bundle; Sun secular deviations on
868
- // the CALENDAR year coordinate — S3)
869
- const moonArgs = createMoonArguments({
870
- constants: {
871
- j2000JD,
872
- julianCenturyDays,
873
- eccentricityJ2000: C.earthOrbital.earthEccentricityJ2000,
874
- eccentricityDotJ2000: C.earthOrbital.earthEccentricityDotJ2000,
875
- eccentricityDotDotJ2000: C.earthOrbital.earthEccentricityDotDotJ2000,
876
- elpEarthFigureJ2ArcsecPerCy2: C.moonMeeus.elpW1T2Decomposition_arcsecPerCy2.earthFigureJ2,
877
- elpGeneralPrecessionPA_T2ArcsecPerCy2: C.moonMeeus.elpW1T2Decomposition_arcsecPerCy2.generalPrecessionPA_T2_Lieske1976,
878
- // Decision (ii): the lunar chain reads ONE deep e end to end
879
- eccE0: deepEcc.e0,
880
- },
881
- fns: {
882
- eccAt: /** @param {number} tYr */ (tYr) => deepEcc.eccAt(tYr),
883
- channelIntegral: /** @param {number} T @param {number} s */ (T, s) => deepEcc.channelIntegral(T, s),
884
- // Layer A / item 3c (plan 06): the arguments' obliquity carrier reads the
885
- // PUBLISHED ε (the one-source hybrid), not the K snapshot comb — one ε
886
- // everywhere. Twins: script.js (_sceneEpsTargetDeg), scene-graph.js.
887
- computeObliquityEarth: /** @param {number} year @returns {number} */ (year) => oneSourceM.epsAt(year),
888
- // Plan 09: the arguments' Sun IS the one certified Sun — the mean
889
- // longitude that integrates the one-family tropical year (the eclipse
890
- // finder's Sun) and the series' mean perihelion of date (unwrapped),
891
- // both on the TT Julian-year axis. The former "model timeline" Sun
892
- // (360° per mean solar year plus the integral of the retired year
893
- // combs) sat 0.2° behind the IAU mean Sun at −2000 — the combs could
894
- // not carry the tropical year's secular drift. Twins: scene-graph.js,
895
- // script.js.
896
- sunMeanLonDegAtJdTT: /** @param {number} jdTT @returns {number} */ (jdTT) => sunMeanLongitudeDegAt(2000 + (jdTT - j2000JD) / 365.25),
897
- sunPerihelionLonDegAtJdTT: /** @param {number} jdTT @returns {number} */ (jdTT) => sunPerihelionDegAt(2000 + (jdTT - j2000JD) / 365.25),
898
- jdToSIyear: yearFromJD,
899
- tropicalOrbitsBetween: mcTropical,
900
- apsidalOfDateCyclesBetween: mcApsidalOfDate,
901
- nodalOfDateCyclesBetween: mcNodalOfDate,
902
- isDeepTime: () => true,
903
- isFrameworkNative: () => true,
904
- // (d′) of-date rate completion: the DYNAMICAL axial precession (the
905
- // tweakpane day-form identity — real at J2000, epoch-valid) and the
906
- // KINEMATIC pair's beat the chains embed. MATCHED TRIPLE with the
907
- // tools-lib and browser wirings.
908
- // Plan 09: the dynamical rate of date is the published precession
909
- // period's (the one-family beat inside its window, the composed clock
910
- // beyond) — the retired year combs' day-form beat read 60 % of the
911
- // secular drift. Degrees per Julian year.
912
- pDynDegPerYearAt: /** @param {number} year */ (year) => 360 / publishedAxialPrecessionYearsAtYear(year),
913
- pKinDegPerYearAt: /** @param {number} year */ (year) => {
914
- // MODEL-START-anchored age, matching the tools-lib/browser wirings
915
- // bit-exactly — NOT yearToTMa's 2000.0 convention (the recorded
916
- // 0.4977-yr parity trap, same class as getMoonDistanceKm above).
917
- const t = (startmodelYear - year) / 1e6;
918
- const sid = deepLod.siderealYearSecondsAtAge(t);
919
- const trop = deepLod.tropicalYearSecondsAtAge(t);
920
- return (sid === null || trop === null) ? 0 : 360 * (sid - trop) / sid;
921
- },
922
- },
923
- });
924
-
925
- // UT→TT on the CALENDAR decimal-year coordinate (script.js Phase 9.16 —
926
- // a linear-year approximation here once cost ~5–6 s of ΔT and ~1e-3° of
927
- // Moon longitude at the Babylonian epochs)
928
- /** @param {number} jd @returns {number} */
929
- const jdTTFromUT = (jd) => {
930
- const tMa = (startmodelYear - jdToDecimalYear(jd)) / 1e6;
931
- const dT = meanDeltaTSecondsAtAge(tMa);
932
- return Number.isFinite(dT) ? jd + dT / 86400 : jd;
933
- };
934
- // ΔT at a JD on the CALENDAR-year axis (mirrors deep-time frameworkDeltaT;
935
- // NOT deltaTSeconds above, which adds the deltaTStart anchor)
936
- /** @param {number} jd @returns {number} */
937
- const frameworkDeltaTSecondsAtJD = (jd) => {
938
- const decYear = startmodelYear + (jd - startmodelJD) / meanSolarYearDays;
939
- const dT = meanDeltaTSecondsAtAge((startmodelYear - decYear) / 1e6);
940
- return Number.isFinite(dT) ? dT : 0;
941
- };
942
-
943
- // Bounded Meeus E-factor from the ONE deep e (decision (ii); the
944
- // pure-Meeus polynomial A/B branch stays engine-local)
945
- /** @param {number} dDays @returns {number} */
946
- // (plan 09 step 7c: on the channel's Julian-year axis — the N-body series'
947
- // time; the device's rounded tropical-year input read the axis 2e-5 short)
948
- const fwEFactor = (dDays) => deepEcc.eFactorAt(dDays / 365.25);
949
-
950
- // D2 derived additional-argument rates (deg/cy, from the J2000 month inputs;
951
- // the derivation record derive-a1a2a3.js was deleted at plan 09 step 7e — docs/109 §22)
952
- const fwA2RateDegPerCy = 2 * ((360 * 36525) / moonTropicalMonthDays)
953
- - (360 * 36525) / moonAnomalisticMonthDays
954
- - 2 * ((360 * 36525) / C.planetOrbitalElements.jupiter.solarYearInput);
955
- const fwA3RateDegPerCy = (360 * 36525) / moonSiderealMonth;
956
-
957
- // Meeus Ch. 47 truncated series (framework-native arguments + E-factor)
958
- const moonSeries = createMoonSeries({
959
- constants: {
960
- moonL: F.MEEUS_LONGITUDE_TERMS,
961
- moonB: F.MEEUS_LATITUDE_TERMS,
962
- moonR: F.MEEUS_DISTANCE_TERMS.terms,
963
- moonRMeanKm: F.MEEUS_DISTANCE_TERMS.meanKm,
964
- moonDistanceJ2000Km: C.moonReference.moonDistance,
965
- j2000JD,
966
- julianCenturyDays,
967
- fwA2RateDegPerCy,
968
- fwA3RateDegPerCy,
969
- },
970
- fns: {
971
- argsAt: /** @param {number} jdTT */ (jdTT) => moonArgs.argsAt(jdTT),
972
- eFactorForD: fwEFactor,
973
- eFactorAtJdTT: /** @param {number} jdTT */ (jdTT) => fwEFactor(jdTT - j2000JD),
974
- // 20.3d(i): the Driver-1 ratio at the EVALUATED epoch (per-jd pure
975
- // evaluator). MATCHED TRIPLE with the tools-lib and browser getters:
976
- // identical age arithmetic — the SI-linear year (yearFromJD, never
977
- // the calendar year: the recorded linear-vs-calendar mirror trap)
978
- // against the MODEL-START anchor (startmodelYear, the browser's
979
- // J2000_CALENDAR_YEAR — NOT this model's 2000.0 yearToTMa convention:
980
- // the 0.5-yr difference is 4.95e-11 of distance and failed the
981
- // bit-exact parity gate; plan 11 step 5 dropped the −0.83-d correction
982
- // from the anchor, both twins alike). Falls back to the J2000 constant
983
- // without a jd (legacy call shape) or past the domain.
984
- getMoonDistanceKm: /** @param {number} [jdTT] */ (jdTT) => {
985
- if (jdTT === undefined) return moonDistanceKm;
986
- const d = moonDistanceMetresAtAge((startmodelYear - yearFromJD(jdTT)) / 1e6);
987
- return d === null ? moonDistanceKm : d / 1000;
988
- },
989
- getEccentricityBase: () => C.moonReference.moonOrbitalEccentricityBase,
990
- deltaTSeconds: /** @param {number} jd */ (jd) => (jdTTFromUT(jd) - jd) * 86400,
991
- jdToSIyear: yearFromJD,
992
- tropicalOrbitsBetween: mcTropical,
993
- apsidalOfDateCyclesBetween: mcApsidalOfDate,
994
- siderealOrbitsBetween: mcSidereal,
995
- jupiterOrbitsBetween: mcJupiter,
996
- isDeepTime: () => true,
997
- isFrameworkNative: () => true,
998
- },
999
- });
1000
-
1001
- // ── E4: the framework-native Sun (§12i item 11 — the 3b landing) ─────────
1002
- // Assembled from the model's own laws, ZERO fitted sun constants:
1003
- // e(t) = the H/16 eccentricity-channel law + the derived H/3
1004
- // inclination-coupling imprint (amplitude base/2, lattice phase,
1005
- // J2000-anchored difference form) — the osculating decomposition:
1006
- // osculating e = H/16 channel + inclination coupling.
1007
- // L(t) = L0 + the mean tropical rate + the f(Y) drift SHAPE only,
1008
- // ∫(rate_SI(y) − rate_SI(2000)) dy: the rate ANCHOR stays the mean
1009
- // year (eclipse-endorsed); the drift is the Step 6c year-harmonic
1010
- // claim in SI/TT (year-in-days × LOD — the LOD-day part of the
1011
- // raw drift is UT-vs-TT and stays ΔT's job) PLUS the derived
1012
- // TORQUE term (E5): the year harmonics carry only the GEOMETRIC
1013
- // equinox displacement (the tilt nodes — the exact 8:3 amplitude
1014
- // signature); the classical luni-solar torque adds a precession-
1015
- // RATE modulation δp = −p₀·tan ε·δε(t) on the model's own
1016
- // two-component obliquity law (−A cos φ₃ + A cos φ₈). Both
1017
- // lengthen the year at obliquity max, so they ADD; per-divisor
1018
- // drift scale 1 + p₀·tan²ε·H/(2π·div) = 1.306 (H/8) / 1.815
1019
- // (H/3) — the structure the ancient corpus blind-selected before
1020
- // the derivation existed. Zero new constants. Trapezoid table,
1021
- // 10-yr steps over −3000..3000; outside, the drift freezes at the
1022
- // edge (rate reverts to linear — the finder domain is the corpus
1023
- // era).
1024
- // ϖ(t) = the shipped H/16 perihelion law (earthPerihelionDeg).
1025
- // Measured (tools/explore/e3b-native-sun.mjs): beats the Meeus Ch. 25
1026
- // basis on JPL all-phase (0.95″ vs 1.28″ scatter) and on ancient-corpus
1027
- // timing structure (0.37 vs 0.50 min detrended vs Meeus-T²); required-ΔT
1028
- // shift 2–4 min ≈ 0.23σ of Stephenson scatter (lunar bias improves); the
1029
- // D2 completion table is unchanged (residual 2lE re-fit ≈ 0.1″ — noise).
1030
- const sunL0Deg = C.earthOrbital.sunMeanLongitudeJ2000_deg;
1031
- const sunTropicalRateDegPerCy = 360 * julianCenturyDays / meanSolarYearDays;
1032
- // The eclipse Sun's eccentricity IS the model's one eccentricity law
1033
- // (eccentricityAt above) — the former sunEccentricityAt (FQ-7-Sun option
1034
- // C-small: the J2000-anchored H/3 line) was the additive-anchor form of the
1035
- // same movement and is retired by the unification (measured identical on
1036
- // every modern gate).
1037
- const sunMeanLongitudeDegAt = (() => {
1038
- // Plan 06 R1 — the Sun's mean longitude of date is the INTEGRAL of the
1039
- // one-source tropical year: L(t) = L0 + 360·∫_{2000}^{t} dt′/T_trop(t′),
1040
- // T_trop the route-B tropical year of date in SI seconds (the ONE
1041
- // year-length family; the frozen era clock's tropicalYearDaysBase × LOD
1042
- // and its comb ripple + torque term left here). MEASURED before the
1043
- // move (tools/explore r1 probes, plan 06 record): the route-B tropical
1044
- // year equals the IAU/Laskar drift expression to 0.1 s over −1000..+3000
1045
- // while the era-clock year carried NO secular drift (+1.09 s flat), so
1046
- // the former mean longitude wandered +12 min around 500 AD and diverged
1047
- // −3.3 min/cy after 2100 against both Meeus ch. 27 and the one-source
1048
- // integral — outside every eclipse and JPL gate. MODERN-WINDOW COST,
1049
- // stated (plan 06 I1 corrected an earlier misreading of the cache's
1050
- // clock): against the JPL Sun cache (UT instants, verified; the registry
1051
- // instrument's bridge) the one-source Sun read 1.29″ sd in 1970–2049
1052
- // where the era-clock Sun read 0.80″ — both carried ~0.02–0.03″/yr
1053
- // trends against JPL over 1900–2100; accepted because over millennia
1054
- // the era-clock Sun is minutes off. Plan 06 I2 located the trend: the
1055
- // Earth–Mars–Jupiter long inequality (1783 yr, 6.3″) the smooth year
1056
- // cannot carry — derived on the model's own N-body and shipped in the
1057
- // planetary completion, the instrument reads 1.03″ (Horizons ±3000 yr:
1058
- // modern-window mean +8.4 → +0.8″, per-millennium sd roughly halved).
1059
- // NUMERICS (rate vs point value): a cumulative trapezoid table of
1060
- // cycles, yearly inside ±20,000 yr and per century beyond, grown on
1061
- // demand from 2000 in both directions and interpolated inside a cell —
1062
- // never a rate multiplied by a span. Beyond the one-family window the
1063
- // tidal-chain year continues the integrand (the scene overlay ends at
1064
- // 20,000 yr; the finders refuse out-of-domain epochs upstream).
1065
- const JULIAN_YEAR_S = 365.25 * 86400;
1066
- const tropSec = oneFamilyTropicalYearSeconds;
1067
- const FINE_SPAN = 20000, COARSE_STEP = 100;
1068
- /** cycles from 2000 to 2000 ± i·step (fwd/bwd tiers) */
1069
- /** @type {number[]} */ const fwdFine = [0];
1070
- /** @type {number[]} */ const bwdFine = [0];
1071
- /** @type {number[]} */ const fwdCoarse = [];
1072
- /** @type {number[]} */ const bwdCoarse = [];
1073
- /** UNSIGNED trapezoid cycles over the span between two years (Julian
1074
- * years → SI seconds over the one-family year); the caller applies the
1075
- * direction sign. @param {number} ya @param {number} yb @returns {number} */
1076
- const segAbs = (ya, yb) => 0.5 * (Math.abs(yb - ya) * JULIAN_YEAR_S) * (1 / tropSec(ya) + 1 / tropSec(yb));
1077
- /** signed cycles from 2000 to `year` @param {number} year @returns {number} */
1078
- const cyclesTo = (year) => {
1079
- const dy = year - 2000;
1080
- const s = dy >= 0 ? 1 : -1, a = Math.abs(dy);
1081
- // WARM THE SAMPLER TO THE TARGET FIRST (plan 06 R4, measured): the
1082
- // table below grows outward in 1-yr / 100-yr steps, and every step
1083
- // beyond ~1.6 Myr enlarged the one-family sampler's >2-Myr tier just
1084
- // past its span, rebuilding the whole deep integration about every 32
1085
- // steps — ~125 rebuilds and 22 s for the first deep-epoch call (the
1086
- // scene's first jump to −5 Myr), 0.4 ms once built. One read at the
1087
- // clamped target builds the tier at its final span; the grid stores
1088
- // are endpoint-independent, so every value is bit-identical.
1089
- tropSec(2000 + s * Math.min(a, ONE_FAMILY_WINDOW_YEARS));
1090
- const fine = s > 0 ? fwdFine : bwdFine;
1091
- const growFine = (/** @type {number} */ upto) => { while (fine.length <= upto) { const k = fine.length; fine.push(fine[k - 1] + s * segAbs(2000 + s * (k - 1), 2000 + s * k)); } };
1092
- if (a <= FINE_SPAN) {
1093
- const i = Math.floor(a), f = a - i;
1094
- growFine(i + 1);
1095
- return fine[i] + f * (fine[i + 1] - fine[i]);
1096
- }
1097
- growFine(FINE_SPAN);
1098
- const coarse = s > 0 ? fwdCoarse : bwdCoarse;
1099
- if (coarse.length === 0) coarse.push(fine[FINE_SPAN]);
1100
- const c = (a - FINE_SPAN) / COARSE_STEP, ci = Math.floor(c), cf = c - ci;
1101
- while (coarse.length <= ci + 1) { const k = coarse.length; const y0 = 2000 + s * (FINE_SPAN + (k - 1) * COARSE_STEP); coarse.push(coarse[k - 1] + s * segAbs(y0, y0 + s * COARSE_STEP)); }
1102
- return coarse[ci] + cf * (coarse[ci + 1] - coarse[ci]);
1103
- };
1104
- return /** @param {number} year @returns {number} */ (year) => sunL0Deg + 360 * cyclesTo(year);
1105
- })();
1106
- // Plan 09: the lunar arguments' Sun perihelion of date is the eclipse
1107
- // Sun's mean perihelion (`sunPerihelionDegAt`) read DIRECTLY, wrapped.
1108
- // Every consumer wraps the argument it enters (M = Lsun − ϖ − 180) or
1109
- // differences it over ±25 days (the rate anchors, wrapped to ±180°), so
1110
- // no continuous angle is needed. MEASURED: a 100-yr unwrapping ladder
1111
- // (two series reads per rung, 53,400 rungs at −5.34 Myr) cost the cold
1112
- // deep jump 1,062 s — 343× the in-table jump against the perf gate's 8× —
1113
- // for a value the consumers reduced mod 360 anyway.
1114
-
1115
- // Eclipse finders — wired like the engine probe (tools/verify/
1116
- // eclipse-audit.js). The finder axis is JD(UT): the series wrapper applies
1117
- // UT→TT internally. Ground-track/umbra paths deliberately absent: the
1118
- // scene-umbra projection navigates the Tychosium-derived scaffold, which
1119
- // never enters this package (§2h).
1120
- const eclipseFinders = createEclipseFinders({
1121
- moonLonDegAt: /** @param {number} jd */ (jd) => moonSeries.truncatedLonDeg(jd),
1122
- moonBetaDegAt: /** @param {number} jd */ (jd) => moonSeries.truncatedBetaDeg(jd),
1123
- moonDistanceKmAt: /** @param {number} jd */ (jd) => moonSeries.truncatedDistanceKm(jd),
1124
- deltaTSecondsAt: frameworkDeltaTSecondsAtJD,
1125
- getSynodicMonthDays: () => moonSynodicMonthDays,
1126
- getSunDistanceKm: () => currentAUDistance,
1127
- frameworkSun: {
1128
- sunMeanLongitudeJ2000Deg: sunL0Deg,
1129
- tropicalRateDegPerCy: sunTropicalRateDegPerCy,
1130
- eccentricityAt: sunEccentricityAt,
1131
- perihelionLongitudeDegAt: sunPerihelionDegAt,
1132
- meanLongitudeDegAt: sunMeanLongitudeDegAt,
1133
- },
1134
- constants: {
1135
- rEarthMetres: (C.bodyDiametersKm.earth / 2) * 1000,
1136
- moonDiameterKm: C.bodyDiametersKm.moon,
1137
- sunDiameterKm: C.bodyDiametersKm.sun,
1138
- j2000JD,
1139
- julianCenturyDays,
1140
- },
1141
- });
1142
-
1143
- // 20.3g — the solar-eclipse LOCATION tier: shadow geometry on the FULL
1144
- // series (sceneEvalAt — the truncated finder forms omit the fitted Lp
1145
- // anchor and the −2235·sin(Lp) β family, deliberate for finder
1146
- // statistics, required here) at the model's ABSOLUTE TT (deltaTStart +
1147
- // curve, the same convention every other ΔT consumer reads). Both axis
1148
- // conventions are measured against the NASA path-table centerlines —
1149
- // see eclipse/besselian.cjs.
1150
- // Derived Earth-around-EMB wobble for the Sun completion: a_M·μ/AU,
1151
- // μ = 1/(1+M_E/M_M) — 6.4399″ at current constants, tracks them live.
1152
- const embWobbleArcsec = (moonDistanceKm / (MASS_RATIO_EARTH_MOON + 1) / currentAUDistance)
1153
- * (648000 / Math.PI);
1154
- // FQ-5 N3 — the completion's carriers are FRAMEWORK-derived from the
1155
- // model's own planet period records (the Moon-elongation rate from the
1156
- // sidereal month/year identity). Plan 06 I2 — the carriers are SIDEREAL
1157
- // (measured): the records' solarYearInput are OF-DATE periods (their
1158
- // rates sit +1.39..1.62°/cy above the VSOP sidereal mean motions — the
1159
- // framework precession plus input rounding), and a perturbation argument
1160
- // is inertial (D'Alembert: only Σk = 0 arguments are frame-free), so on
1161
- // of-date carriers every Σk ≠ 0 argument drifted by Σk·ψ(t) — 42..52° at
1162
- // −3000 for the table's Σk = −1 terms (their ancient scatter against
1163
- // Horizons 6.4 → 4.4″ on sidereal carriers), and 6 % of frequency for the
1164
- // 1783-yr long inequality (1683 yr on the of-date carriers). Constant-free:
1165
- // the record rate minus the model's own J2000 precession p₀ =
1166
- // 360·36525·(1/T_trop − 1/T_sid) for the planets, the framework sidereal
1167
- // year for Earth; identical to the N3 carriers at J2000 (same phase
1168
- // anchors), ≤ 1.4°·|Σk| apart at the 200-yr extraction window's edges
1169
- // (the composed table reproduces the N3 one there: JPL 1900–2100
1170
- // all-phase sd unchanged to 0.01″). The parity gate's fingerprint mirrors
1171
- // this arithmetic (test/create-model-parity.test.mjs).
1172
- // PLAN 06 I3 — the carriers are the model's own banked J2000 sidereal mean
1173
- // motions (the embedded sun-completion artifact; ONE home,
1174
- // computeCarrierRatesDegPerCy — the former record-based arithmetic sat
1175
- // 4.1″/yr off for Venus, 1.7″/yr for Jupiter: 83° of the Venus–Earth
1176
- // argument at ±9000 yr), Earth the framework sidereal year, the Moon
1177
- // elongation from the sidereal month/year identity. The long-period rows
1178
- // ride the embedded e-vectors of date, and the lunar equation rides the
1179
- // framework's own Moon elongation of date (ṅ included) — the constant-rate
1180
- // carrier ran ~29° off it at ±9000 yr.
1181
- const carrierRatesDegPerCy = computeCarrierRatesDegPerCy({ meanSiderealYearDays, moonSiderealMonthDays: moonSiderealMonthInput });
1182
- const { sunPlanetaryCompletionDeg } = createSunPlanetaryCompletion({
1183
- embWobbleArcsec,
1184
- carrierRatesDegPerCy,
1185
- moonElongationDegAtT: /** @param {number} T */ (T) => moonArgs.argsAt(j2000JD + T * julianCenturyDays).D,
1186
- });
1187
- const besselian = createBesselian({
1188
- moonFullAtDaysTT: /** @param {number} dDaysTT */ (dDaysTT) => {
1189
- const ev = moonSeries.sceneEvalAt(dDaysTT);
1190
- return { lonDeg: ev.lonDeg, latDeg: ev.latDeg, distKm: ev.distKm };
1191
- },
1192
- sunLonDegAt: /** @param {number} jdUT */ (jdUT) => eclipseFinders.sunLonDegAt(jdUT),
1193
- sunCompletionDeg: sunPlanetaryCompletionDeg,
1194
- // plan 06 R3 item 1: the location tier on APPARENT places — the Sun's derived κ
1195
- // (the same helper the cardinal instants use), the Moon's relative light-time
1196
- // inside the besselian; the series-extension tail now lives in sceneEvalAt.
1197
- sunAberrationDegAt: /** @param {number} year */ (year) => sunAberrationArcsecAt(year) / 3600,
1198
- deltaTSecondsAt: /** @param {number} jd */ (jd) => (jdTTFromUT(jd) - jd) * 86400,
1199
- obliquityDegAt: obliquityDeg,
1200
- eccentricityAt: sunEccentricityAt,
1201
- perihelionLongitudeDegAt: sunPerihelionDegAt,
1202
- yearFromJD,
1203
- constants: {
1204
- j2000JD,
1205
- julianCenturyDays,
1206
- earthDiameterKm: C.bodyDiametersKm.earth,
1207
- moonDiameterKm: C.bodyDiametersKm.moon,
1208
- sunDiameterKm: C.bodyDiametersKm.sun,
1209
- sunDistanceKm: currentAUDistance,
1210
- earthFlatteningInverse: C.physicalConstants.earthFlatteningInverseWGS84,
1211
- ttBridgeSeconds: C.earthOrbital.deltaTStart,
1212
- gmstMeanSiderealT0Deg: C.physicalConstants.gmstMeanSiderealT0Deg,
1213
- gmstMeanSiderealRateDegPerDay: C.physicalConstants.gmstMeanSiderealRateDegPerDay,
1214
- gmstMeanSiderealT2Deg: C.physicalConstants.gmstMeanSiderealT2Deg,
1215
- speedOfLightKmS: C.physicalConstants.speedOfLight,
1216
- },
1217
- });
1218
-
1219
- // ── The assembled surface ─────────────────────────────────────────────────
1220
- // Plan 06 Phase 3 S2 — THE published of-date precession period and mean
1221
- // tropical year: the one-family route (B) inside the banked tiers
1222
- // (|Δyear| ≤ ONE_FAMILY_WINDOW_YEARS), the composed lunisolar period
1223
- // beyond (S5: on the derived J2000 anchor, so the two faces agree at
1224
- // 25,771.4 at J2000 and the deep tail is the same clock scaled; the
1225
- // tidal-chain year). Shared by `epoch` and `lunisolar`.
1226
- /** @param {number} year @returns {number} */
1227
- const publishedAxialPrecessionYearsAtYear = (year) => {
1228
- if (Math.abs(year - 2000) <= ONE_FAMILY_WINDOW_YEARS) return yearLengthsM.axialPrecessionYearsAtYear(year);
1229
- const p = deepLod.lunisolarPrecessionPeriodYearsAtAge(yearToTMa(year));
1230
- return p === null ? certifiedAxialPrecessionJ2000Years() : p;
1231
- };
1232
- /** @param {number} year @returns {number} */
1233
- const publishedTropicalYearSecondsAtYear = (year) => {
1234
- if (Math.abs(year - 2000) <= ONE_FAMILY_WINDOW_YEARS) return yearLengthsM.tropicalYearSecondsAtYear(year);
1235
- return deepLod.tropicalYearSecondsAtAge(yearToTMa(year));
1236
- };
1237
- /** The published sidereal year of date, SI seconds — the one-family λ̇
1238
- * channel inside the window, the mass-loss law beyond (plan 09: the
1239
- * `lengths` surface's days ride it). @param {number} year @returns {number} */
1240
- const publishedSiderealYearSecondsAtYear = (year) => {
1241
- if (Math.abs(year - 2000) <= ONE_FAMILY_WINDOW_YEARS) return yearLengthsM.siderealYearSecondsAtYear(year);
1242
- return deepLod.siderealYearSecondsAtAge(yearToTMa(year));
1243
- };
1244
- // ── Cardinal instants (plan 06 R1) ─────────────────────────────────────
1245
- /** The completed certified Sun (finder Sun − the derived planetary
1246
- * completion) at a UT model-JD — the same longitude the scene renders
1247
- * inside the overlay window; MEAN GEOMETRIC, mean equinox of date.
1248
- * @param {number} jdUT @returns {number} */
1249
- const sunLonCompletedDegAtJdUT = (jdUT) => eclipseFinders.sunLonDegAt(jdUT)
1250
- - sunPlanetaryCompletionDeg((jdTTFromUT(jdUT) - j2000JD) / julianCenturyDays);
1251
- /** The APPARENT Sun the cardinal instants are defined on (an equinox is
1252
- * the apparent Sun crossing the equator — the USNO/Meeus instants are
1253
- * apparent): the completed geometric Sun minus the aberration constant,
1254
- * plus the nutation in longitude. Both derived, zero new constants —
1255
- * κ = 2π·a/(c·T_sid·√(1−e²)) from the AU, c, the one-family sidereal year
1256
- * and the Sun's e (20.50″ at J2000); Δψ from the four IAU 1980 leading
1257
- * terms on the model's OWN lunar node, mean Sun and mean Moon (the
1258
- * framework arguments), the same family the registry's JPL Sun
1259
- * instrument bridges with. @param {number} jdUT @returns {number} */
1260
- /** The Sun's derived aberration constant of date, arcsec — κ = 2π·a/(c·T_sid·√(1−e²))
1261
- * from the AU, c, the one-family sidereal year and the Sun's e (20.50″ at
1262
- * J2000); ONE home for the cardinal instants and the eclipse location tier.
1263
- * @param {number} year @returns {number} */
1264
- const sunAberrationArcsecAt = (year) => {
1265
- const e = sunEccentricityAt(year);
1266
- return (2 * Math.PI * currentAUDistance
1267
- / (C.physicalConstants.speedOfLight * yearLengthsM.siderealYearSecondsAtYear(year) * Math.sqrt(1 - e * e)))
1268
- * (648000 / Math.PI);
1269
- };
1270
- /** @param {number} jdUT @returns {number} */
1271
- const sunApparentLonDegAtJdUT = (jdUT) => {
1272
- const jdTT = jdTTFromUT(jdUT);
1273
- const year = 2000 + (jdTT - j2000JD) / 365.25;
1274
- const kappaArcsec = sunAberrationArcsecAt(year);
1275
- const a = moonArgs.argsAt(jdTT);
1276
- const d2r = Math.PI / 180;
1277
- const Om = (a.Lp - a.F) * d2r, Ls = (a.Lp - a.D) * d2r, Lm = a.Lp * d2r;
1278
- const NU = C.physicalConstants.nutationLeadingTermsArcsec;
1279
- const dPsiArcsec = NU.psiOmega * Math.sin(Om) + NU.psi2Ls * Math.sin(2 * Ls)
1280
- + NU.psi2Lm * Math.sin(2 * Lm) + NU.psi2Omega * Math.sin(2 * Om);
1281
- return sunLonCompletedDegAtJdUT(jdUT) + (dPsiArcsec - kappaArcsec) / 3600;
1282
- };
1283
- /** @param {string} type @returns {number} */
1284
- const cardinalTargetDeg = (type) => {
1285
- const t = { VE: 0, SS: 90, AE: 180, WS: 270 }[type];
1286
- if (t === undefined) throw new RangeError(`cardinal type must be VE|SS|AE|WS, got ${type}`);
1287
- return t;
1288
- };
1289
- const CARDINAL_SI_YEAR_D = 365.2422;
1290
- /** The crossing NEAREST a seed instant — the unique root within ±½ tropical
1291
- * year of the seed (the wrapped longitude difference is the Newton residual).
1292
- * Plan 06 R4c: the seed is a JD because the caller's year may live on a
1293
- * different axis than the model year — the simulator's cardinal panel
1294
- * labels its year with the displayed CALENDAR date (the Julian calendar
1295
- * before 1582, 365.25-d years) while `year` below is the model year (the SI
1296
- * axis, 365.2422-d count from J2000); the two part by 0.0078 d/yr, 114 yr at
1297
- * −5.34 Myr, where the panel showed the events of −5341772 for a date in
1298
- * −5341886 (owner). Seeding at the calendar year's midpoint returns that
1299
- * year's own events at every epoch. @param {number} jdSeedUT @param {string} type @returns {number} */
1300
- const cardinalCrossingNearJdUT = (jdSeedUT, type) => {
1301
- const target = cardinalTargetDeg(type);
1302
- const RATE = 360 / CARDINAL_SI_YEAR_D;
1303
- let jd = jdSeedUT;
1304
- for (let i = 0; i < 20; i++) {
1305
- const d = ((((sunApparentLonDegAtJdUT(jd) - target) + 540) % 360 + 360) % 360) - 180;
1306
- jd -= d / RATE;
1307
- if (Math.abs(d) < 1e-10) break;
1308
- }
1309
- return jd - C.earthOrbital.deltaTStart / 86400;
1310
- };
1311
- /** The TRUE-UT JD at which the APPARENT Sun's longitude of date equals
1312
- * the cardinal target in the given year — Newton on the crossing (the
1313
- * Sun's rate 360°/tropical year; a handful of steps from the tropical-year
1314
- * seed). Replaces the fitted cardinal model's instants (R1). Plan 06 R3
1315
- * item 2: the Newton root lives on the FINDER axis (the curve-only ΔT the
1316
- * eclipse finders certify), and the returned instant is that root minus the
1317
- * deltaTStart bridge — true UT = TT − (deltaTStart + curve), the same
1318
- * bridge the besselian and the registry instruments apply. Before this the
1319
- * published instants were 54.55 s (0.9 min) late. Against Horizons over
1320
- * ±3000 yr the remaining offset is the Sun's (plan 06 I2 measured).
1321
- * `year` is the MODEL year (the SI axis) — see cardinalCrossingNearJdUT.
1322
- * @param {number} year @param {string} type @returns {number} */
1323
- const cardinalCrossingJdUT = (year, type) => {
1324
- const target = cardinalTargetDeg(type);
1325
- // seed: Jan 1.5 of the year (J2000 = 2000 Jan 1.5) + the mean date of the
1326
- // March equinox (day 79.3) + the quarter-turns to the target
1327
- const seed = j2000JD + (year - 2000) * CARDINAL_SI_YEAR_D + 79.3 + (target / 360) * CARDINAL_SI_YEAR_D;
1328
- return cardinalCrossingNearJdUT(seed, type);
1329
- };
1330
-
1331
- return Object.freeze({
1332
- time: Object.freeze({
1333
- yearFromJD,
1334
- jdFromYear,
1335
- siTropicalYearDays,
1336
- }),
1337
- // The Moon-Kepler mass chain — the J2000 derivation docs 24 / the website's
1338
- // "mass from the Moon" page narrate, published from the one place that
1339
- // computes it (physics 22.1.0; the pages used to carry its numbers as
1340
- // hand-typed literals): Kepler III on the Moon's orbit with the
1341
- // solar-tidal shift Δa = a·μ·m (μ the Moon's mass fraction, m the lunar
1342
- // small parameter T_M/T_sidereal-year), the Earth/Moon split on the
1343
- // DE440 mass ratio, and GM_Sun from Earth's heliocentric orbit minus
1344
- // GM_Earth. Every value is the J2000 snapshot the rest of the model runs
1345
- // on; the of-date Moon distance is the recession history (epoch.*).
1346
- massChain: Object.freeze({
1347
- moonDistanceKm,
1348
- moonSiderealMonthDays: moonSiderealMonth,
1349
- siderealYearDays: meanSiderealYearDays,
1350
- dayLengthSeconds: meanLengthOfDay,
1351
- massRatioEarthMoon: MASS_RATIO_EARTH_MOON,
1352
- lunarSmallParameter: moonSiderealMonth / meanSiderealYearDays,
1353
- moonMassFraction: 1 / (MASS_RATIO_EARTH_MOON + 1),
1354
- solarTidalShiftKm: moonOrbitalShift,
1355
- moonDistanceCorrectedKm: moonDistanceCorrected,
1356
- gmEarthMoonSystemKm3S2: GM_EARTH_MOON_SYSTEM,
1357
- gmEarthKm3S2: GM_EARTH_ALONE,
1358
- gmMoonKm3S2: GM_MOON_ALONE,
1359
- gmSunPlusEarthKm3S2: GM_SUN_PLUS_EARTH,
1360
- gmSunKm3S2: GM_SUN,
1361
- sunMassKg: M_SUN,
1362
- earthMassKg: GM_EARTH_ALONE / G_CONSTANT,
1363
- gravitationalConstantKm3KgS2: G_CONSTANT,
1364
- }),
1365
- // Plan 09: the ONE certified Sun's mean elements of date — the lunar
1366
- // arguments of every runtime build their Sun from these two (the Node
1367
- // scene twin and the browser read them here; the "model timeline" Sun on
1368
- // the retired year combs is gone).
1369
- sun: Object.freeze({
1370
- /** Mean longitude of date, degrees, unwrapped — L0 + 360·∫dt/T_trop (the one-family tropical year). @param {number} year @returns {number} */
1371
- meanLongitudeDegAt: sunMeanLongitudeDegAt,
1372
- /** The Sun's mean perihelion longitude of date (Earth's ϖ, mean elements), degrees, wrapped to [0, 360). @param {number} year @returns {number} */
1373
- perihelionLongitudeDegAt: sunPerihelionDegAt,
1374
- }),
1375
- epoch: Object.freeze({
1376
- yearToTMa,
1377
- // (plan 09 step 7b: `hAtYear` — the unit H(t) on a published surface, the
1378
- // API's `epoch.h` — is deleted; the deep-time clock is axialPrecessionYearsAtYear)
1379
- // The Layer-2 mean day of date — the tidal + GIA base the deep-time law
1380
- // gives at any age (86,399.9997 s at J2000): the day the paleo anchors
1381
- // and the ΔT integrand ride. NOT the observable solar day of date —
1382
- // that is lodRealSecondsAtYear below (plan 08 storyline sweep).
1383
- lodSecondsAtYear: /** @param {number} year @returns {number|null} */ (year) => deepLod.lodSecondsAtAge(yearToTMa(year)),
1384
- // LOD_real of date — the Layer-4 observable (the simulator's Solar Day,
1385
- // the registry's lodRealPhysical): the kinematic day + the ecliptic term
1386
- // + the calibrated ΔT cycle stack. 86,400.001780 s at J2000.
1387
- lodRealSecondsAtYear: /** @param {number} year @returns {number} */ (year) => lodRealSeconds(year),
1388
- alphaAtYear: /** @param {number} year @returns {number} */ (year) => earthMoiFactorAtAge(yearToTMa(year)),
1389
- moonDistanceKmAtYear: /** @param {number} year @returns {number} */ (year) => moonDistanceKmAtYearOrNaN(year),
1390
- // D6: OF-DATE — mass-loss law / the banked planetary λ̇ ratio
1391
- // (identical at J2000 where the ratio ≡ 1 by construction)
1392
- siderealYearSecondsAtYear: /** @param {number} year @returns {number} */ (year) => yearLengthsM.siderealYearSecondsAtYear(year),
1393
- deltaTSecondsAtYear: deltaTSeconds,
1394
- // Plan 06 Phase 3 S2 — THE published of-date precession period is the
1395
- // one-family route (B): the hybrid's own equinox regression against
1396
- // the λ̇-corrected sidereal year (~25,771 yr at J2000 ≈ IAU), inside
1397
- // the banked tiers (|Δyear| ≤ ONE_FAMILY_WINDOW_YEARS); beyond, the
1398
- // composed lunisolar period on the same J2000 anchor (S5; the
1399
- // of-date wobble is unresolved there). The comb family's beat (the
1400
- // frozen era clock's kinematic-day device) is deleted at plan 09; the
1401
- // lunar chain's (d′) rate completion (pDynDegPerYearAt) rides THIS
1402
- // published period — a MATCHED TRIPLE with the eclipse gates (M0(a)
1403
- // measured a 3–15 min lunar-timing shift when it moved; the records
1404
- // were re-baselined on the plan 09 move).
1405
- axialPrecessionYearsAtYear: publishedAxialPrecessionYearsAtYear,
1406
- // The same publication rule for the mean tropical year of date (SI s):
1407
- // one-family inside the window, the tidal-chain mean beyond.
1408
- tropicalYearSecondsAtYear: publishedTropicalYearSecondsAtYear,
1409
- }),
1410
- // Plan 06 Phase 3 S3 → S5 — the lunisolar precession clock as a
1411
- // first-class surface: Earth's spin clock, ONE home for its published
1412
- // faces, spoken in PERIODS and their RATIOS — no unit, no integer.
1413
- // ONE J2000 reading (S5): every face reads 25,771.4 yr at J2000 — the
1414
- // of-date period (the published family, seamed at ±2 Myr) and the
1415
- // composed mean T_p(t) (the deep-time clock, anchored on the same
1416
- // derived value). The composition's terms; the apsidal period from the
1417
- // engine-D chain's secular tangent; the two ratios the owner found
1418
- // wandering by hand — T_aps/T_p (4.33 at J2000, 0.84 … 9.9 across
1419
- // ±26 kyr) and T_peri/T_p with T_peri = 1/(1/T_p + 1/T_aps) the
1420
- // perihelion-of-date period (0.812 at J2000) — on the of-date T_p.
1421
- lunisolar: Object.freeze({
1422
- /** The mean lunisolar precession period T_p(t), years — 1,296,000/the precession RATE of date (25,771.6 at J2000): THE published deep-time clock. @param {number} year @returns {number} */
1423
- meanPeriodYearsAtYear: (year) => { const p = deepLod.lunisolarPrecessionPeriodYearsAtAge(yearToTMa(year)); return p === null ? certifiedAxialPrecessionJ2000Years() : p; },
1424
- /** THE PRECESSION RATE OF DATE ψ̇(t) = [ω/ω₀]·p₀·[f_S + (1 − f_S)(a₀/a_M)³] × cos ε̄(t)/cos ε̄(0), ″/yr — the constant's scaling on the spin and the lunar torque times the cited mean obliquity's cosine (plan 12 §8 option (a); p₀ = 50.2880, the IAU 2006 input). @param {number} year @returns {number} */
1425
- meanRateArcsecPerYrAtYear: (year) => { const r = deepLod.lunisolarPrecessionRateArcsecPerYrAtAge(yearToTMa(year)); return r === null ? 1296000 / certifiedAxialPrecessionJ2000Years() : r; },
1426
- /** THE PRECESSION CONSTANT OF DATE as a rate on the J2000 obliquity, ″/yr — the spin and torque scaling alone (the hybrid obliquity integration's integrand: its own ŝ·n̂ carries the cosine). @param {number} year @returns {number} */
1427
- torqueRateArcsecPerYrAtYear: (year) => { const r = deepLod.lunisolarTorqueRateArcsecPerYrAtAge(yearToTMa(year)); return r === null ? 1296000 / certifiedAxialPrecessionJ2000Years() : r; },
1428
- /** 1,296,000/the torque rate, years. @param {number} year @returns {number} */
1429
- torquePeriodYearsAtYear: (year) => { const p = deepLod.lunisolarTorquePeriodYearsAtAge(yearToTMa(year)); return p === null ? certifiedAxialPrecessionJ2000Years() : p; },
1430
- /** cos ε̄(t)/cos ε̄(0) — the rate of date's obliquity factor on the constant (1 at J2000; 1.0075 at the Devonian, 1.0245 at 2.46 Ga). @param {number} year @returns {number} */
1431
- precessionObliquityFactorAtYear: (year) => { const f = deepLod.precessionObliquityFactorAtAge(yearToTMa(year)); return f === null ? 1 : f; },
1432
- /** THE CITED MEAN OBLIQUITY OF DATE ε̄(t), degrees — Farhat et al. 2022's own column of the Earth–Moon history (a Myr-scale MEAN: 23.25° at age 0 against the J2000 instant 23.44°; 22.23° Devonian, 19.73° at 2.46 Ga); read as the shape cos ε̄(t)/cos ε̄(0) on the precession rate. NaN beyond the input's span (4,429 Ma); forward of J2000 the J2000 slope is held. @param {number} year @returns {number} */
1433
- meanObliquityCitedDegAtYear: (year) => { const e = chain.meanObliquityCitedDegAtAge(yearToTMa(year)); return e === null ? NaN : e; },
1434
- /** The of-date period — the one-family beat inside ±2 Myr, the composed mean beyond. @param {number} year @returns {number} */
1435
- ofDatePeriodYearsAtYear: publishedAxialPrecessionYearsAtYear,
1436
- /** f_S, the solar fraction of the J2000 precession torque. */
1437
- solarShareJ2000: EPOCH_PARAMS.precessionSolarShareJ2000,
1438
- /** THE PRECESSION OF DATE — the solar-torque factor 1 + f_S·([(1 − e²)/(1 − e₀²)]^(−3/2) − 1) at the eccentricity of date (1 at J2000; +3.2·10⁻⁴ on the million-year mean). The spin integration multiplies it into the secular rate. @param {number} year @returns {number} */
1439
- solarTorqueFactorOfDateAtYear: (year) => oneSourceM.precessionOfDateFactorsAt(year).solarTorque,
1440
- /** THE PRECESSION OF DATE — J₂(t)/J₂₀, the oblateness of date from the GIA channel (ψ̇ ∝ J₂; 1 at J2000, bounded ±5·10⁻⁴; dJ₂/dt at J2000 = the observed satellite rate). @param {number} year @returns {number} */
1441
- ellipticityRatioOfDateAtYear: (year) => oneSourceM.precessionOfDateFactorsAt(year).ellipticity,
1442
- /** (a₀/a_M(t))³ — the lunar torque's growth on the recession history. @param {number} year @returns {number} */
1443
- lunarTorqueFactorAtYear: (year) => { const f = deepLod.lunarTorqueFactorAtAge(yearToTMa(year)); return f === null ? 1 : f; },
1444
- /** f_S + (1 − f_S)(a₀/a_M)³ — the torque term the unit divides H_era by. @param {number} year @returns {number} */
1445
- torqueTermAtYear: (year) => { const t = deepLod.precessionTorqueTermAtAge(yearToTMa(year)); return t === null ? 1 : t; },
1446
- /** The hybrid's precession constant α = p₀ / cos ε₀, ″/yr (p₀ the derived J2000 rate, ε₀ the J2000 obliquity input; 54.81). */
1447
- torqueConstantJ2000ArcsecPerYr: (1296000 / certifiedAxialPrecessionJ2000Years()) / Math.cos((C.earthOrbital.obliquityJ2000_deg * Math.PI) / 180),
1448
- /** |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. */
1449
- nodalModeS3ArcsecPerYr: s3ArcsecPerYr,
1450
- /** The ecliptic (nodal) precession period 1,296,000/|s₃|, years (68,751) — an orbital quantity, fixed at every epoch at the two-body level. */
1451
- nodalPeriodYears: 1296000 / s3ArcsecPerYr,
1452
- /** THE SPIN AXIS'S INERTIAL PRECESSION RATE OF DATE, ″/yr — the composed rate scaled by the
1453
- * hybrid's self-anchor factor (the inertial-to-general ratio at J2000, the multiplicative form
1454
- * the obliquity integration itself applies): 50.385 at J2000 (series-driven). The obliquity beat and the
1455
- * obliquity lines ride THIS rate; the general precession (meanRateArcsecPerYrAtYear) is the
1456
- * equinox along the ecliptic of date. @param {number} year @returns {number} */
1457
- spinAxisInertialRateArcsecPerYrAtYear: (year) => {
1458
- const r = deepLod.lunisolarPrecessionRateArcsecPerYrAtAge(yearToTMa(year));
1459
- const psiDot = r === null ? 1296000 / certifiedAxialPrecessionJ2000Years() : r;
1460
- return psiDot * (certifiedAxialPrecessionJ2000Years() / oneSourceM.axialPrecessionYearsLunisolarJ2000);
1461
- },
1462
- /** The obliquity beat 2π/(ψ̇ᵢₙ(t) − |s₃|), years — the SHIPPED deep-time obliquity period at an INSTANT
1463
- * (falsification leg 1; 41,098 at J2000). ψ̇ᵢₙ is the spin axis's INERTIAL rate (above): the equator
1464
- * against the ecliptic's nodal mode in the inertial frame, the rate the hybrid obliquity integration
1465
- * self-anchors on — not the general precession, which reads the beat 0.3 % long (41,224). v23.0: a
1466
- * dated record sees the beat of the pole rate's mean over its own window, with the pole's of-date
1467
- * factors (obliquityPeriodRealizedYears below: 40,927 over the LR04 record, the climate formula's p+s₀
1468
- * line; La2004's own series reads 40,900 on the same window — compare realized with realized, never
1469
- * this instant with Laskar's long-window precession frequency 50.476). @param {number} year @returns {number} */
1470
- obliquityBeatYearsAtYear: (year) => {
1471
- const r = deepLod.lunisolarPrecessionRateArcsecPerYrAtAge(yearToTMa(year));
1472
- const psiDot = r === null ? 1296000 / certifiedAxialPrecessionJ2000Years() : r;
1473
- const psiDotInertial = psiDot * (certifiedAxialPrecessionJ2000Years() / oneSourceM.axialPrecessionYearsLunisolarJ2000);
1474
- return 1296000 / (psiDotInertial - s3ArcsecPerYr);
1475
- },
1476
- /** THE REALIZED POLE RATE OVER A WINDOW, ″/yr — see the one home above (the mean of the inertial rate of
1477
- * date with its three of-date factors); the rate the obliquity lines of a record spanning the window carry. */
1478
- spinAxisInertialRateMeanArcsecPerYr,
1479
- /** THE REALIZED OBLIQUITY PERIOD OVER A WINDOW, years — 1,296,000/(⟨ψ̇ᵢₙ⟩_window − |s₃|): the period a record
1480
- * spanning [yearFrom, yearTo] sees (40,930 over the LR04 record 0–5.32 Ma, 41,018 over the last Myr); the
1481
- * J2000 beat (obliquityBeatYearsAtYear) is its instantaneous limit.
1482
- * @param {number} yearFrom @param {number} yearTo @returns {number} */
1483
- obliquityPeriodRealizedYears: (yearFrom, yearTo) => 1296000 / (spinAxisInertialRateMeanArcsecPerYr(yearFrom, yearTo) - s3ArcsecPerYr),
1484
- /** The apsidal (perihelion vs the stars) period of date, JULIAN years — the ONE movement's own apsidal rate (the scene's perihelion; anom/(anom − sid) of the one family, ≡ 360°/apsidalRateFixedArcsecPerJulianYr), inside the published window only; null beyond. Before 2026-09 this rode the planet chain's secular tangent, which parted from the scene's perihelion passages by seconds of anomalistic year within a few millennia (year-lengths.cjs). @param {number} year @returns {number|null} */
1485
- apsidalPeriodYearsAtYear: (year) => (Math.abs(year - 2000) <= ONE_FAMILY_WINDOW_YEARS ? yearLengthsM.inclinationPrecessionYearsAtYear(year) : null),
1486
- /** 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} */
1487
- apsidalPerPrecessionAtYear: (year) => {
1488
- if (Math.abs(year - 2000) > ONE_FAMILY_WINDOW_YEARS) return null;
1489
- return yearLengthsM.inclinationPrecessionYearsAtYear(year) / publishedAxialPrecessionYearsAtYear(year);
1490
- },
1491
- /** 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), JULIAN years, on the of-date T_p — ≡ the one family's anom/(anom − trop); null beyond the published window. @param {number} year @returns {number|null} */
1492
- periOfDatePeriodYearsAtYear: (year) => {
1493
- if (Math.abs(year - 2000) > ONE_FAMILY_WINDOW_YEARS) return null;
1494
- const tp = publishedAxialPrecessionYearsAtYear(year), taps = yearLengthsM.inclinationPrecessionYearsAtYear(year);
1495
- return 1 / (1 / tp + 1 / taps);
1496
- },
1497
- /** T_peri(t) / T_p(t) (0.812 at J2000 — the J2000 reading); null beyond the published window. @param {number} year @returns {number|null} */
1498
- periOfDatePerPrecessionAtYear: (year) => {
1499
- if (Math.abs(year - 2000) > ONE_FAMILY_WINDOW_YEARS) return null;
1500
- const tp = publishedAxialPrecessionYearsAtYear(year), taps = yearLengthsM.inclinationPrecessionYearsAtYear(year);
1501
- return (1 / (1 / tp + 1 / taps)) / tp;
1502
- },
1503
- /** The published of-date window, years from 2000. */
1504
- publishedWindowYears: ONE_FAMILY_WINDOW_YEARS,
1505
- /** THE SPIN AXIS'S INERTIAL PRECESSION PERIOD at J2000, years (25,722 in the shipped,
1506
- * series-driven configuration) — the rate the hybrid obliquity integration self-anchors
1507
- * ŝ on (earth/deep-orbital-history, D4e): the general-precession anchor p₀ PLUS the
1508
- * planetary mean the N-body ecliptic's own motion carries (+0.097 ″/yr ≡ IAU's planetary
1509
- * precession ψ_A − p_A; the bare 18-mode table anchors 0.18 — the g₂-gap trap: pass the
1510
- * secular series). Two rates, two uses — the obliquity line frequencies p + s_i (the
1511
- * spin axis against the ecliptic's nodal modes, in the inertial frame) carry THIS
1512
- * rate; the climatic-precession lines p + g_i (the perihelion from the equinox along
1513
- * the ecliptic of date) carry p₀. */
1514
- spinAxisInertialPeriodYearsJ2000: oneSourceM.axialPrecessionYearsLunisolarJ2000,
1515
- /** The same as a rate, ″/yr (50.385 at J2000 with the series; p₀ = 50.288 is the general precession). */
1516
- spinAxisInertialRateArcsecPerYrJ2000: 1296000 / oneSourceM.axialPrecessionYearsLunisolarJ2000,
1517
- }),
1518
- earth: Object.freeze({
1519
- perihelionLongitudeDeg: earthPerihelionDeg,
1520
- // Plan 06 Phase 3 S3b: THE published obliquity is the hybrid — one
1521
- // torque law integrated on the dynamical orbit plane of the N-body
1522
- // chain, zero fitted constants (≡ La2004 to ≤ 4″ over ±50 kyr). The
1523
- // fitted 16-harmonic K comb that sat beside it as `obliquityCombDeg`
1524
- // (a device fitted to the scene's own wheel geometry, 1.27° off
1525
- // La2004 at −20 kyr) is deleted with the frozen era device (plan 09).
1526
- // Beyond the ζ series' span (±10 Myr) the hybrid carries NO tidal evolution
1527
- // of the mean obliquity (nothing in ds/dt = α(ŝ·n̂)(ŝ×n̂) moves ŝ·n̂'s mean):
1528
- // its deep mean is the Quaternary mean on the tail's mean plane (plan 12
1529
- // §8.3). The cited mean obliquity of date is `meanObliquityCitedDeg`.
1530
- obliquityDeg: /** @param {number} year @returns {number} */ (year) => oneSourceM.epsAt(year),
1531
- /** The hybrid's own domain, [yearFrom, yearTo] — the banked ζ series' span, ±10 Myr about J2000 (plan 12 §8.5: the published surfaces serve `obliquityDeg` inside it only; null without a series, i.e. a bare createModel()). */
1532
- obliquityHybridDomainYears: oneSourceM.obliquityHybridDomainYears,
1533
- /** The cited mean obliquity of date ε̄(t) (Farhat et al. 2022's column; plan 12 §8 option (a)), degrees — a Myr-scale mean; NaN beyond 4,429 Ma. @param {number} year @returns {number} */
1534
- meanObliquityCitedDeg: /** @param {number} year @returns {number} */ (year) => { const e = chain.meanObliquityCitedDegAtAge(yearToTMa(year)); return e === null ? NaN : e; },
1535
- eccentricity: eccentricityAt,
1536
- inclinationDeg,
1537
- ascendingNodeDeg,
1538
- }),
1539
- // The DEVICE family (plan 06 Phase 3 S2): the frozen era clock's comb
1540
- // year lengths (A), its cardinal 4-mean (A′) and the kinematic-day stack
1541
- // built on them (the ΔT stack's Layer 3/4 basis, USNO-anchored). NOT the
1542
- // published year lengths — those are `yearLengths` (the one-family
1543
- // route) and the seamed `epoch.*AtYear` surfaces above.
1544
- lengths: Object.freeze({
1545
- tropicalYearDays,
1546
- siderealYearDays,
1547
- anomalisticYearDays,
1548
- dayLengthSeconds,
1549
- lodRealSeconds,
1550
- siderealDaySeconds,
1551
- stellarDaySeconds,
1552
- }),
1553
- // Plan 06 R1: the instants are the crossings of the ONE Sun (the
1554
- // certified completed Sun the scene renders), UT model-JD; the RA at a
1555
- // crossing is the target longitude by construction (λ = 0/90/180/270 ⇒
1556
- // RA = 0/90/180/270, any obliquity); the per-type year length is the
1557
- // interval between successive crossings (SI days).
1558
- cardinal: Object.freeze({
1559
- jd: /** @param {number} year @param {string} type @returns {number} */ (year, type) => cardinalCrossingJdUT(year, type),
1560
- // R4c: the crossing nearest a UT instant (a JD seed — for callers whose
1561
- // year coordinate is not the model year, e.g. a calendar year's midpoint).
1562
- jdNearUT: /** @param {number} jdUT @param {string} type @returns {number} */ (jdUT, type) => cardinalCrossingNearJdUT(jdUT, type),
1563
- raDeg: /** @param {number} year @param {string} type @returns {number} */ (year, type) => { void year; return cardinalTargetDeg(type); },
1564
- yearLengthDays: /** @param {number} year @param {string} type @returns {number} */ (year, type) => cardinalCrossingJdUT(year + 1, type) - cardinalCrossingJdUT(year, type),
1565
- }),
1566
- // D4b: the one-source cardinal STRUCTURE — the EoC layer on the
1567
- // movement's own e(t)/ϖ(t), valid at every epoch (mode tier). Absolute
1568
- // dates deliberately absent; `cardinal` above stays the certified era
1569
- // device for those.
1570
- cardinalStructure: Object.freeze({
1571
- // D6: year LENGTHS gain the λ̇ drift coherently (rate-form
1572
- // correction — beats stay invariant); offsets/spreads keep the raw
1573
- // form (μs-class / second-order there).
1574
- yearLengthSeconds: /** @param {number} year @param {'VE'|'SS'|'AE'|'WS'} type @returns {number} */ (year, type) => yearLengthsM.cardinal.yearLengthSeconds(year, type),
1575
- eocOffsetSeconds: /** @param {number} year @param {'VE'|'SS'|'AE'|'WS'} type @returns {number} */ (year, type) => yearLengthsM.cardinal.eocOffsetSeconds(year, type),
1576
- spreadSeconds: /** @param {number} year */ (year) => yearLengthsM.cardinal.spreadSeconds(year),
1577
- anomalisticYearSeconds: /** @param {number} year @returns {number} */ (year) => yearLengthsM.anomalisticYearSecondsAtYear(year),
1578
- }),
1579
- // THE ONE of-date year-length family + its beats (S2; SI seconds) —
1580
- // the single surface the panel, charts, API and website consume.
1581
- yearLengths: yearLengthsM,
1582
- moon: Object.freeze({
1583
- distanceKmAtYear: /** @param {number} year @returns {number} */ (year) => moonDistanceKmAtYearOrNaN(year),
1584
- siderealMonthDaysAtYear: moonSiderealMonthDaysAt,
1585
- synodicMonthDays: moonSynodicMonthDays,
1586
- // The apparent-position chain (truncated Meeus Ch. 47 series on
1587
- // framework-native arguments). JD(UT) axis — UT→TT applied internally.
1588
- lonDegAtJD: /** @param {number} jd @returns {number} */ (jd) => moonSeries.truncatedLonDeg(jd),
1589
- betaDegAtJD: /** @param {number} jd @returns {number} */ (jd) => moonSeries.truncatedBetaDeg(jd),
1590
- distanceKmAtJD: /** @param {number} jd @returns {number} */ (jd) => moonSeries.truncatedDistanceKm(jd),
1591
- }),
1592
- eclipse: Object.freeze({
1593
- sunLonDegAtJD: /** @param {number} jd @returns {number} */ (jd) => eclipseFinders.sunLonDegAt(jd),
1594
- // The COMPLETED tier Sun (K8b follow-up, owner-approved injection):
1595
- // the bare finder Sun minus the DERIVED planetary-completion table —
1596
- // 70 framework-carrier terms plus the 6.44″ Earth-around-EMB wobble
1597
- // (the "lunar equation"; eclipse/sun-planetary-completion.cjs, zero
1598
- // fitted constants). The finders themselves deliberately stay bare
1599
- // (their certified canon statistics were produced on the bare form;
1600
- // besselian.cjs applies the same subtraction internally). This
1601
- // surface exists for the SCENE tier's E5 wheel-Sun bridge, so the
1602
- // rendered Sun carries the model's own derived apparent-class terms.
1603
- // Clock: the completion argument rides TT centuries from the same
1604
- // framework ΔT the besselian composition uses.
1605
- sunLonCompletedDegAtJD: /** @param {number} jd @returns {number} */ (jd) =>
1606
- eclipseFinders.sunLonDegAt(jd)
1607
- - sunPlanetaryCompletionDeg((jdTTFromUT(jd) - j2000JD) / julianCenturyDays),
1608
- /** The APPARENT Sun (completed geometric − the derived aberration constant + the leading nutation terms on the model's own arguments) — the quantity Horizons' observer ecliptic longitude is; the cardinal instants are its crossings (plan 06 R1/I1). @param {number} jdUT @returns {number} */
1609
- sunApparentLonDegAtJD: /** @param {number} jdUT @returns {number} */ (jdUT) => sunApparentLonDegAtJdUT(jdUT),
1610
- findLunarInRange: /** @param {number} jdStart @param {number} jdEnd */ (jdStart, jdEnd) => eclipseFinders.findLunarEclipsesInRange(jdStart, jdEnd),
1611
- findSolarInRange: /** @param {number} jdStart @param {number} jdEnd */ (jdStart, jdEnd) => eclipseFinders.findSolarEclipsesInRange(jdStart, jdEnd),
1612
- deltaTSecondsAtJD: frameworkDeltaTSecondsAtJD,
1613
- // E4 — the framework-native Sun deps, exported so the OTHER finder
1614
- // construction sites (tools/verify/eclipse-audit.js, the browser
1615
- // _eclipse twins) spread the SAME assembly into their own
1616
- // createEclipseFinders call instead of triplicating it (the
1617
- // three-runtimes rule — cf. moon-distance-history):
1618
- frameworkSunDeps: Object.freeze({
1619
- sunMeanLongitudeJ2000Deg: sunL0Deg,
1620
- tropicalRateDegPerCy: sunTropicalRateDegPerCy,
1621
- eccentricityAt: sunEccentricityAt,
1622
- perihelionLongitudeDegAt: sunPerihelionDegAt,
1623
- meanLongitudeDegAt: sunMeanLongitudeDegAt,
1624
- }),
1625
- // 20.3g location tier (see eclipse/besselian.cjs):
1626
- umbraGroundAtJD: /** @param {number} jd @returns {{latDeg: number, lonDeg: number} | null} */ (jd) => besselian.umbraGroundAt(jd),
1627
- solarLocalCircumstances: /** @param {number} jdGreatest @param {number} latDeg @param {number} lonDeg */ (jdGreatest, latDeg, lonDeg) => besselian.localCircumstances(jdGreatest, latDeg, lonDeg),
1628
- }),
1629
- climate: Object.freeze({
1630
- l1OrbitalPermil: evalClimateL1,
1631
- }),
1632
- planets: Object.freeze({
1633
- keys: Object.freeze([...PLANET_KEYS]),
1634
- record: /** @param {string} k @returns {Record<string, any>|undefined} */ (k) => PUBLISHED_PLANET_RECORDS[k],
1635
- perihelionLongitudeDeg: planetPerihelionDeg,
1636
- ascendingNodeInvPlaneDeg: planetAscNodeDeg,
1637
- invPlaneInclinationDeg: planetInclinationDeg,
1638
- eccentricity: planetEccentricity,
1639
- spin: planetSpin,
1640
- }),
1641
- });
64
+ /** The forward bindings (model/context.js) — each filled the moment its owning slice returns. */
65
+ const late = /** @type {import('./model/context.js').LateBindings} */ ({});
66
+ /** @type {import('./model/context.js').ModelInputs} */
67
+ const inputs = { C, F, laws, secularSeriesArtifact, options, late };
68
+ const c1 = Object.assign(inputs, assembleDerivedConstants(inputs));
69
+ const c2 = Object.assign(c1, assembleDeepTimeChain(c1));
70
+ const c3 = Object.assign(c2, assembleEarthScalars(c2));
71
+ const c4 = Object.assign(c3, assembleYearDayLengths(c3));
72
+ late.certifiedAxialPrecessionJ2000Years = c4.certifiedAxialPrecessionJ2000Years;
73
+ const c5 = Object.assign(c4, assembleOneSourceMovement(c4));
74
+ late.oneSourceM = c5.oneSourceM;
75
+ late.yearLengthsM = c5.yearLengthsM;
76
+ const c6 = Object.assign(c5, assembleMoonEpoch(c5));
77
+ const c7 = Object.assign(c6, assembleDeltaT(c6));
78
+ const c8 = Object.assign(c7, assemblePlanetRecords(c7));
79
+ const c9 = Object.assign(c8, assemblePlanetSpinChannel(c8));
80
+ const c10 = Object.assign(c9, assemblePlanetElementsOfDate(c9));
81
+ const c11 = Object.assign(c10, assembleInvPlaneElements(c10));
82
+ const c12 = Object.assign(c11, assembleTimeAxis(c11));
83
+ const c13 = Object.assign(c12, assembleLunarTheory(c12));
84
+ const c14 = Object.assign(c13, assembleSunAndEclipses(c13));
85
+ late.sunMeanLongitudeDegAt = c14.sunMeanLongitudeDegAt;
86
+ const c15 = Object.assign(c14, assemblePublishedPeriods(c14));
87
+ late.publishedAxialPrecessionYearsAtYear = c15.publishedAxialPrecessionYearsAtYear;
88
+ late.publishedSiderealYearSecondsAtYear = c15.publishedSiderealYearSecondsAtYear;
89
+ const c16 = Object.assign(c15, assembleCardinalInstants(c15));
90
+ late.sunAberrationArcsecAt = c16.sunAberrationArcsecAt;
91
+ return assembleSurface(c16);
1642
92
  }