@essrt/physics 22.0.1 → 23.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -500,6 +500,8 @@ function createDeepOrbitalHistory({
500
500
  };
501
501
  }
502
502
 
503
+ /** The factory's walks, one per step size — see build(). @type {Map<number, { grid: Map<number, any>, frontier: Record<number, { s: number[], t: number }> }>} */
504
+ const walks = new Map();
503
505
  /**
504
506
  * Integrate once over [min(t0,t1,0), max(t0,t1,0)] and return a sampler
505
507
  * on a stepYr grid (linear interpolation; angles interpolated on the
@@ -514,7 +516,18 @@ function createDeepOrbitalHistory({
514
516
  if (Math.max(Math.abs(tMin), Math.abs(tMax)) > 50000 && stepYr % 250 !== 0) {
515
517
  throw new Error(`deep-orbital-history: builds beyond ±50 kyr need stepYr % 250 === 0 (got ${stepYr})`);
516
518
  }
517
- const grid = new Map();
519
+ // ONE WALK PER STEP SIZE, EXTENDED, NEVER RESTARTED (plan 13 A1, measured):
520
+ // every caller's tier routing asked for `build(span, -span, step)` and
521
+ // rebuilt from J2000 whenever a deeper epoch arrived, so the four deep
522
+ // fixture anchors (380…2460 Ma) walked ~49 million RK4 steps — 14 of the
523
+ // chain's 17 minutes — and the API's first read at −498 Myr would walk
524
+ // ~5 million. The walk is a pure function of t from the fixed J2000 state
525
+ // S0, so continuing a stored walk from its last (s, t) yields the SAME
526
+ // step sequence as a fresh walk to the farther end: bit-identical nodes
527
+ // (the sampler-order-purity gate pins this). The grid and the two
528
+ // direction frontiers live on the factory, keyed by stepYr; a returned
529
+ // sampler reads the shared grid, which only ever gains nodes.
530
+ let w = walks.get(stepYr);
518
531
  const S0 = S0A;
519
532
  // D1-revised adaptive stepping: 5-yr RK4 near the era (the certified-
520
533
  // precision zone), 250-yr beyond ±50 kyr — still 103 steps per
@@ -526,8 +539,16 @@ function createDeepOrbitalHistory({
526
539
  // 250-aligned; switching one fine-step later would leave the coarse walk
527
540
  // permanently off-grid — measured: every key beyond ±50 kyr missing).
528
541
  const hStepAt = (/** @type {number} */ t) => (Math.abs(t) < 50000 ? 5 : 250);
542
+ if (!w) {
543
+ w = { grid: new Map(), frontier: { [-1]: { s: S0, t: 0 }, [1]: { s: S0, t: 0 } } };
544
+ walks.set(stepYr, w);
545
+ }
546
+ const grid = w.grid;
529
547
  for (const dir of [-1, +1]) {
530
- let s = S0, t = 0;
548
+ const end = dir < 0 ? tMin : tMax;
549
+ const front = w.frontier[dir];
550
+ if (dir < 0 ? end >= front.t : end <= front.t) continue; // already walked this far
551
+ let s = front.s, t = front.t;
531
552
  // Each stored node also carries the equinox-node RATE (deg/yr) from a
532
553
  // ±2.5-yr central difference on the integrator's own state — the C1
533
554
  // (Hermite) interpolation input. Linear interpolation of the node
@@ -552,8 +573,7 @@ function createDeepOrbitalHistory({
552
573
  smp.periOfDateRateDegPerYr = (((lp.peri - lm.peri + 540) % 360) - 180) / (2 * hR);
553
574
  grid.set(key, smp);
554
575
  };
555
- storeWithRate(0, s, 0);
556
- const end = dir < 0 ? tMin : tMax;
576
+ if (t === 0 && !grid.has(0)) storeWithRate(0, s, 0);
557
577
  while (dir < 0 ? t > end : t < end) {
558
578
  const H_STEP = hStepAt(t);
559
579
  s = rk4(s, t, dir * H_STEP);
@@ -568,6 +588,7 @@ function createDeepOrbitalHistory({
568
588
  // zone boundary is itself 250-aligned, so the walk stays on-grid).
569
589
  if (Math.abs(t - Math.round(t / stepYr) * stepYr) < 1e-6) storeWithRate(Math.round(t / stepYr) * stepYr, s, t);
570
590
  }
591
+ w.frontier[dir] = { s, t };
571
592
  }
572
593
  return {
573
594
  /** @param {number} tYr */
package/src/index.js CHANGED
@@ -167,7 +167,12 @@ export { PLANET_DISPLAY_NAMES } from './planets/display-names.cjs';
167
167
  // Phase 8.4 — the climate/ΔT machinery, extracted layer by layer.
168
168
  export { createDeltaTCycles } from './deltat/cycles.cjs';
169
169
  export { createDeepTimeLod } from './deltat/deep-time.cjs';
170
- export { createMoonRecessionHistory, createSolarChannelBudget } from './deltat/recession-history.cjs';
170
+ // plan 12 (option A): the Earth–Moon distance history is a CITED PHYSICAL
171
+ // INPUT (Farhat et al. 2022, the AstroGeo22 ephemeris) read as a shape on the
172
+ // model's J2000 anchor; the fitted quartic, the rock-fitted regime spline and
173
+ // the solar-channel budget are deleted.
174
+ export { createMoonDistanceHistory } from './deltat/moon-distance-history.cjs';
175
+ export { MOON_DISTANCE_ARTIFACT, MOON_DISTANCE_ARTIFACT_HASH } from './deltat/moon-distance-artifact.cjs';
171
176
  export { deltaTEspenakMeeusCanonSeconds } from './deltat/historical.cjs';
172
177
  export { evalClimateL1OrbitalPermil, laggedL1Terms, createAlphaGiaChannel } from './climate/l1-orbital.cjs';
173
178
  // Phase 8.5 — eclipse geometry (single-copy: the browser had no Node twin).
@@ -24,9 +24,7 @@ import { computeSolarTorqueShare } from '../earth/precession-composed.cjs';
24
24
  * @property {number} solarLuminosityW IAU 2015 nominal (W)
25
25
  * @property {number} solarWindKgPerS Ulysses/ACE/Wind
26
26
  * @property {number} speedOfLightKmPerS km/s (fallback 299792458 m/s if falsy)
27
- * @property {number} alpha1PerMa
28
- * @property {number} alpha3PerMa3
29
- * @property {number} alpha4PerMa4
27
+ * @property {number} alpha1PerMa the LLR recession rate — the cited distance history's J2000 slope
30
28
  * @property {number} meanSiderealYearSeconds
31
29
  * @property {number} lodJ2000Seconds ω₀: the J2000 day length, SI seconds (the SI day since plan 09)
32
30
  * @property {number} sunMassKg M_SUN
@@ -85,8 +83,6 @@ export const deriveEpochParams = (raw) => {
85
83
  return Object.freeze({
86
84
  epochYear: 2000,
87
85
  alpha1PerMa: raw.alpha1PerMa,
88
- alpha3PerMa3: raw.alpha3PerMa3,
89
- alpha4PerMa4: raw.alpha4PerMa4,
90
86
  moonDistanceNowM,
91
87
  moonLockDistanceM,
92
88
  totalAngularMomentumKgM2S,
@@ -32,9 +32,7 @@
32
32
  /**
33
33
  * @typedef {Object} EpochParams
34
34
  * @property {number} epochYear year at t_Ma = 0 (2000, NOT startmodelYear)
35
- * @property {number} alpha1PerMa Moon recession, LLR-anchored
36
- * @property {number} alpha3PerMa3
37
- * @property {number} alpha4PerMa4
35
+ * @property {number} alpha1PerMa Moon recession, LLR-anchored (the J2000 slope of the cited history)
38
36
  * @property {number} moonDistanceNowM a_Moon at J2000, metres
39
37
  * @property {number} moonLockDistanceM tidal-lock asymptote, metres
40
38
  * @property {number} totalAngularMomentumKgM2S L_tot of the Earth-Moon system
@@ -53,17 +51,21 @@
53
51
  /**
54
52
  * @typedef {Object} EpochPrimitives
55
53
  * @property {(year: number) => number} tMa
56
- * @property {(year: number) => number} moonDistanceMetres
54
+ * @property {(year: number) => number|null} moonDistanceMetres - null beyond the cited history's span
57
55
  * @property {(year: number) => number|null} lodSeconds
58
56
  * @property {(year: number) => number} siderealYearSeconds
59
57
  * @property {(year: number) => number} tropicalYearSeconds sidereal less one equinox turn per T_p(t) (the composed precession period)
60
58
  */
61
59
 
62
60
  /**
63
- * @param {{params: EpochParams, alphaAtAgeMa: (tMa: number) => number}} deps
61
+ * @param {{params: EpochParams, alphaAtAgeMa: (tMa: number) => number,
62
+ * moonDistanceMetresAtAge: (tMa: number) => number|null}} deps —
63
+ * moonDistanceMetresAtAge is the engine's cited distance history
64
+ * (deltat/moon-distance-history: Farhat et al. 2022 as a shape on the
65
+ * J2000 anchor; plan 12 option A — the layer0 quartic twin is deleted).
64
66
  * @returns {EpochPrimitives}
65
67
  */
66
- export const createEpochPrimitives = ({ params: p, alphaAtAgeMa }) => {
68
+ export const createEpochPrimitives = ({ params: p, alphaAtAgeMa, moonDistanceMetresAtAge }) => {
67
69
  /**
68
70
  * Years before the epoch, in Ma. The ONE place this conversion lives.
69
71
  * @param {number} year
@@ -72,24 +74,21 @@ export const createEpochPrimitives = ({ params: p, alphaAtAgeMa }) => {
72
74
  const tMa = (year) => (p.epochYear - year) / 1e6;
73
75
 
74
76
  /**
75
- * Moon semi-major axis. Cubic in t_Ma: alpha1 is LLR-anchored, alpha3/alpha4
76
- * are the Farhat 2022 deep-time fit.
77
+ * Moon distance at age t — the cited history (null beyond its span).
77
78
  * @param {number} t age in Ma
78
- * @returns {number} metres
79
+ * @returns {number|null} metres
79
80
  */
80
- const moonDistanceMetresCore = (t) =>
81
- p.moonDistanceNowM
82
- * (1 + p.alpha1PerMa * t + p.alpha3PerMa3 * t * t * t + p.alpha4PerMa4 * t * t * t * t);
81
+ const moonDistanceMetresCore = (t) => moonDistanceMetresAtAge(t);
83
82
 
84
83
  /**
85
84
  * Length of day from angular-momentum conservation: as the Moon recedes it
86
85
  * takes orbital angular momentum and Earth's spin slows to compensate.
87
86
  * @param {number} t age in Ma
88
- * @returns {number|null} seconds; null past the tidal-lock asymptote
87
+ * @returns {number|null} seconds; null past the tidal-lock asymptote or the history's span
89
88
  */
90
89
  const lodSecondsCore = (t) => {
91
90
  const a = moonDistanceMetresCore(t);
92
- if (a <= 0 || a >= p.moonLockDistanceM) return null;
91
+ if (a === null || a <= 0 || a >= p.moonLockDistanceM) return null;
93
92
  const iEarth = alphaAtAgeMa(t) * p.earthMassKg * p.earthRadiusM * p.earthRadiusM;
94
93
  return (2 * Math.PI * iEarth)
95
94
  / (p.totalAngularMomentumKgM2S
package/src/model.js CHANGED
@@ -28,7 +28,8 @@ import { createPlanetSpinChannelFromArtifacts, computeObliquityJ2000Deg } from '
28
28
  import { PLANET_DISPLAY_NAMES } from './planets/display-names.cjs';
29
29
  import { createDeltaTCycles } from './deltat/cycles.cjs';
30
30
  import { createDeepTimeLod } from './deltat/deep-time.cjs';
31
- import { createMoonRecessionHistory, createSolarChannelBudget } from './deltat/recession-history.cjs';
31
+ import { createMoonDistanceHistory } from './deltat/moon-distance-history.cjs';
32
+ import { MOON_DISTANCE_ARTIFACT } from './deltat/moon-distance-artifact.cjs';
32
33
  import { evalClimateL1OrbitalPermil, createAlphaGiaChannel } from './climate/l1-orbital.cjs';
33
34
  import { createDeepEccChannel } from './moon/deep-ecc-channel.cjs';
34
35
  import { DEEP_MODES_ARTIFACT } from './moon/deep-modes-artifact.cjs';
@@ -197,8 +198,6 @@ export function assembleModel(C, F, laws = {}, secularSeriesArtifact = /** @type
197
198
  solarWindKgPerS: C.physicalConstants.solarWindMassLossKgPerS,
198
199
  speedOfLightKmPerS: C.physicalConstants.speedOfLight,
199
200
  alpha1PerMa: C.deepTime.alpha1PerMa,
200
- alpha3PerMa3: C.deepTime.alpha3PerMa3,
201
- alpha4PerMa4: C.deepTime.alpha4PerMa4,
202
201
  meanSiderealYearSeconds,
203
202
  lodJ2000Seconds: meanLengthOfDay, // plan 09: ω₀ = the SI day (was the H/13 kinematic day)
204
203
  sunMassKg: M_SUN,
@@ -219,38 +218,22 @@ export function assembleModel(C, F, laws = {}, secularSeriesArtifact = /** @type
219
218
  axialPrecessionJ2000Years: certifiedAxialPrecessionJ2000Years(),
220
219
  });
221
220
 
222
- // ── Driver 1½: regime-aware recession history + the solar channels ────────
223
- // The quartic stays bit-identical ≤ jointMa (the Wells/Wu-gated era);
224
- // beyond, the fitted staircase spline to the Roche crossing, and the
225
- // ocean-leak/thermal-pump channels make L_EM time-dependent.
226
- const REGIME = C.deepTime.recessionRegime;
227
- const recession = createMoonRecessionHistory({
221
+ // ── The Earth–Moon distance history — a CITED PHYSICAL INPUT (plan 12, option A)
222
+ // Farhat et al. 2022 (the AstroGeo22 ephemeris, embedded VERBATIM by the
223
+ // constants generator) read as the ratio a(t)/a(0) on the model's own J2000
224
+ // anchor, the J2000 slope the LLR observation α₁; null beyond its span. LOD
225
+ // follows by angular-momentum conservation with the J2000 L_EM constant —
226
+ // the former quartic, regime spline and solar-channel budget are deleted.
227
+ const recession = createMoonDistanceHistory({
228
+ table: MOON_DISTANCE_ARTIFACT,
228
229
  aMoonNowMetres: EPOCH_PARAMS.moonDistanceNowM,
229
230
  alpha1PerMa: EPOCH_PARAMS.alpha1PerMa,
230
- alpha3PerMa3: EPOCH_PARAMS.alpha3PerMa3,
231
- alpha4PerMa4: EPOCH_PARAMS.alpha4PerMa4,
232
- regime: {
233
- jointMa: REGIME.jointMa,
234
- knotAgesMa: REGIME.knotAgesMa,
235
- knotDistancesKm: REGIME.knotDistancesKm,
236
- genesisMa: REGIME.genesisMa,
237
- rocheLimitKm: REGIME.rocheLimitKm,
238
- },
239
231
  });
240
232
  const moonDistanceMetresAtAge = recession.distanceMetresAtAge;
241
- const solarBudget = createSolarChannelBudget({
242
- lTotalJ2000KgM2S: EPOCH_PARAMS.totalAngularMomentumKgM2S,
243
- mMoonAloneKg: EPOCH_PARAMS.moonMassKg,
244
- gmEmM3PerS2: EPOCH_PARAMS.gmEarthMoonM3S2,
245
- eFactorMoon: EPOCH_PARAMS.moonEccentricityFactor,
246
- beta0: REGIME.solarOceanLeakBeta0,
247
- pumpStartMa: REGIME.thermalPumpStartMa,
248
- pumpEndMa: REGIME.thermalPumpEndMa,
249
- pumpFactor: REGIME.thermalPumpFactor,
250
- jointMa: REGIME.jointMa,
251
- genesisMa: REGIME.genesisMa,
252
- distanceMetresAtAge: moonDistanceMetresAtAge,
253
- });
233
+ /** The published surfaces' km reading — NaN beyond the cited history's span (the API's
234
+ * validity span lies inside it; the deep charts stop where the input stops).
235
+ * @param {number} year @returns {number} */
236
+ const moonDistanceKmAtYearOrNaN = (year) => { const m = moonDistanceMetresAtAge(yearToTMa(year)); return m === null ? NaN : m / 1000; };
254
237
 
255
238
  const DT = F.DT_STACK;
256
239
  const RES = F.DT_RESONATOR;
@@ -343,8 +326,6 @@ export function assembleModel(C, F, laws = {}, secularSeriesArtifact = /** @type
343
326
  aLockMetres: EPOCH_PARAMS.moonLockDistanceM,
344
327
  aMoonNowMetres: EPOCH_PARAMS.moonDistanceNowM,
345
328
  alpha1PerMa: EPOCH_PARAMS.alpha1PerMa,
346
- alpha3PerMa3: EPOCH_PARAMS.alpha3PerMa3,
347
- alpha4PerMa4: EPOCH_PARAMS.alpha4PerMa4,
348
329
  lodNowH13Seconds: EPOCH_PARAMS.lodNowH13Seconds,
349
330
  meanSiderealYearJ2000Seconds: meanSiderealYearSeconds,
350
331
  solarMassLossFracPerYear: EPOCH_PARAMS.solarMassLossFracPerYear,
@@ -361,7 +342,9 @@ export function assembleModel(C, F, laws = {}, secularSeriesArtifact = /** @type
361
342
  cycleLodSumAt: dtCycleLodCorrectionSum,
362
343
  swingLodAt: (year) => dtCycles.swingLodSecondsAt(year),
363
344
  swingLodRateAt: (year) => dtCycles.swingLodRateAt(year),
364
- lEmAtAgeKgm2S: solarBudget.lEmAtAgeKgm2S,
345
+ // plan 12 option A: L_EM is the J2000 constant (conservation; no channels);
346
+ // the LOD-rate decomposition's tidal term reads the history's analytic slope.
347
+ moonDistanceSlopeMetresPerMaAtAge: recession.slopeMetresPerMaAtAge,
365
348
  });
366
349
 
367
350
  // (Plan 09 step 7a: the integrated ∫dt/H_era phase table and its counter
@@ -588,19 +571,23 @@ export function assembleModel(C, F, laws = {}, secularSeriesArtifact = /** @type
588
571
  // and the year-length rates (±0.5-yr central differences through the
589
572
  // grid) then returned grid-segment averages instead of local rates
590
573
  // (anomalistic of date +2.63 s / +19 s, visit-order dependent).
591
- const samplers = /** @type {Map<number|'deep', any>} */ (new Map());
592
- let deepRangeYr = 0;
574
+ const samplers = /** @type {Map<number, any>} */ (new Map());
593
575
  const gridStep = (/** @type {number} */ n) => (n <= 50000 ? 100 : n <= 2000000 ? 1000 : 5000);
594
- const tierSpan = (/** @type {number} */ n) => (n <= 50000 ? 50000 : n <= 2000000 ? 2000000 : Math.ceil(n * 1.25 / 5000) * 5000);
576
+ const tierSpan = (/** @type {number} */ n) => (n <= 50000 ? 50000 : 2000000);
595
577
  const sampleAt = (/** @type {number} */ year) => {
596
578
  const t = year - 2000, need = Math.max(20000, Math.abs(t) * 1.25);
597
- const span = tierSpan(need);
598
- const key = span > 2000000 ? /** @type {'deep'} */ ('deep') : span;
599
- if (!samplers.has(key) || (key === 'deep' && span > deepRangeYr)) {
600
- if (key === 'deep') deepRangeYr = span;
601
- samplers.set(key, hist.build(span, -span, gridStep(span)));
579
+ if (need > 2000000) {
580
+ // The deep tier (5000-yr grid): walked in the queried DIRECTION only,
581
+ // to two nodes past the query, and EXTENDED by the factory's shared
582
+ // walk when a deeper epoch arrives (plan 13 A1). It was built to
583
+ // ±1.25·|t| in both directions and rebuilt from J2000 on every deeper
584
+ // probe — bit-identical values, 4× the RK4 steps of one walk.
585
+ const span = Math.ceil((Math.abs(t) + 2 * 5000) / 5000) * 5000;
586
+ return hist.build(t < 0 ? -span : span, 0, 5000).at(t);
602
587
  }
603
- return samplers.get(key).at(t);
588
+ const span = tierSpan(need);
589
+ if (!samplers.has(span)) samplers.set(span, hist.build(span, -span, gridStep(span)));
590
+ return samplers.get(span).at(t);
604
591
  };
605
592
  // The anomalistic year rides THIS movement's own apsidal line (the
606
593
  // scene's perihelion) — the factory owns the construction; the planet
@@ -686,8 +673,9 @@ export function assembleModel(C, F, laws = {}, secularSeriesArtifact = /** @type
686
673
  const moonSiderealMonthDaysAt = (year) => {
687
674
  const tMa = yearToTMa(year);
688
675
  const lod = deepLod.lodSecondsAtAge(tMa);
689
- if (lod === null) return NaN;
690
- const aAppKm = moonDistanceMetresAtAge(tMa) / 1000;
676
+ const aAppM = moonDistanceMetresAtAge(tMa);
677
+ if (lod === null || aAppM === null) return NaN;
678
+ const aAppKm = aAppM / 1000;
691
679
  const sidYrDays = deepLod.siderealYearSecondsAtAge(tMa) / lod;
692
680
  const deltaA = aAppKm * (1 / (MASS_RATIO_EARTH_MOON + 1)) * (moonSiderealMonthInput / sidYrDays);
693
681
  const aCorrM = (aAppKm + deltaA) * 1000;
@@ -973,9 +961,6 @@ export function assembleModel(C, F, laws = {}, secularSeriesArtifact = /** @type
973
961
  const moonChain = createMoonMonthChain({
974
962
  constants: {
975
963
  aMoonNowMetres: EPOCH_PARAMS.moonDistanceNowM,
976
- alpha1PerMa: EPOCH_PARAMS.alpha1PerMa,
977
- alpha3PerMa3: EPOCH_PARAMS.alpha3PerMa3,
978
- alpha4PerMa4: EPOCH_PARAMS.alpha4PerMa4,
979
964
  gmEarthMoonM3PerS2: EPOCH_PARAMS.gmEarthMoonM3S2,
980
965
  massRatioEarthMoon: MASS_RATIO_EARTH_MOON,
981
966
  moonSiderealMonthInputDays: moonSiderealMonthInput,
@@ -1516,6 +1501,34 @@ export function assembleModel(C, F, laws = {}, secularSeriesArtifact = /** @type
1516
1501
  jdFromYear,
1517
1502
  siTropicalYearDays,
1518
1503
  }),
1504
+ // The Moon-Kepler mass chain — the J2000 derivation docs 24 / the website's
1505
+ // "mass from the Moon" page narrate, published from the one place that
1506
+ // computes it (physics 22.1.0; the pages used to carry its numbers as
1507
+ // hand-typed literals): Kepler III on the Moon's orbit with the
1508
+ // solar-tidal shift Δa = a·μ·m (μ the Moon's mass fraction, m the lunar
1509
+ // small parameter T_M/T_sidereal-year), the Earth/Moon split on the
1510
+ // DE440 mass ratio, and GM_Sun from Earth's heliocentric orbit minus
1511
+ // GM_Earth. Every value is the J2000 snapshot the rest of the model runs
1512
+ // on; the of-date Moon distance is the recession history (epoch.*).
1513
+ massChain: Object.freeze({
1514
+ moonDistanceKm,
1515
+ moonSiderealMonthDays: moonSiderealMonth,
1516
+ siderealYearDays: meanSiderealYearDays,
1517
+ dayLengthSeconds: meanLengthOfDay,
1518
+ massRatioEarthMoon: MASS_RATIO_EARTH_MOON,
1519
+ lunarSmallParameter: moonSiderealMonth / meanSiderealYearDays,
1520
+ moonMassFraction: 1 / (MASS_RATIO_EARTH_MOON + 1),
1521
+ solarTidalShiftKm: moonOrbitalShift,
1522
+ moonDistanceCorrectedKm: moonDistanceCorrected,
1523
+ gmEarthMoonSystemKm3S2: GM_EARTH_MOON_SYSTEM,
1524
+ gmEarthKm3S2: GM_EARTH_ALONE,
1525
+ gmMoonKm3S2: GM_MOON_ALONE,
1526
+ gmSunPlusEarthKm3S2: GM_SUN_PLUS_EARTH,
1527
+ gmSunKm3S2: GM_SUN,
1528
+ sunMassKg: M_SUN,
1529
+ earthMassKg: GM_EARTH_ALONE / G_CONSTANT,
1530
+ gravitationalConstantKm3KgS2: G_CONSTANT,
1531
+ }),
1519
1532
  // Plan 09: the ONE certified Sun's mean elements of date — the lunar
1520
1533
  // arguments of every runtime build their Sun from these two (the Node
1521
1534
  // scene twin and the browser read them here; the "model timeline" Sun on
@@ -1540,7 +1553,7 @@ export function assembleModel(C, F, laws = {}, secularSeriesArtifact = /** @type
1540
1553
  // + the calibrated ΔT cycle stack. 86,400.001780 s at J2000.
1541
1554
  lodRealSecondsAtYear: /** @param {number} year @returns {number} */ (year) => lodRealSeconds(year),
1542
1555
  alphaAtYear: /** @param {number} year @returns {number} */ (year) => earthMoiFactorAtAge(yearToTMa(year)),
1543
- moonDistanceKmAtYear: /** @param {number} year @returns {number} */ (year) => moonDistanceMetresAtAge(yearToTMa(year)) / 1000,
1556
+ moonDistanceKmAtYear: /** @param {number} year @returns {number} */ (year) => moonDistanceKmAtYearOrNaN(year),
1544
1557
  // D6: OF-DATE — mass-loss law / the banked planetary λ̇ ratio
1545
1558
  // (identical at J2000 where the ratio ≡ 1 by construction)
1546
1559
  siderealYearSecondsAtYear: /** @param {number} year @returns {number} */ (year) => yearLengthsM.siderealYearSecondsAtYear(year),
@@ -1718,7 +1731,7 @@ export function assembleModel(C, F, laws = {}, secularSeriesArtifact = /** @type
1718
1731
  // the single surface the panel, charts, API and website consume.
1719
1732
  yearLengths: yearLengthsM,
1720
1733
  moon: Object.freeze({
1721
- distanceKmAtYear: /** @param {number} year @returns {number} */ (year) => moonDistanceMetresAtAge(yearToTMa(year)) / 1000,
1734
+ distanceKmAtYear: /** @param {number} year @returns {number} */ (year) => moonDistanceKmAtYearOrNaN(year),
1722
1735
  siderealMonthDaysAtYear: moonSiderealMonthDaysAt,
1723
1736
  synodicMonthDays: moonSynodicMonthDays,
1724
1737
  // The apparent-position chain (truncated Meeus Ch. 47 series on
@@ -1752,7 +1765,7 @@ export function assembleModel(C, F, laws = {}, secularSeriesArtifact = /** @type
1752
1765
  // construction sites (tools/verify/eclipse-audit.js, the browser
1753
1766
  // _eclipse twins) spread the SAME assembly into their own
1754
1767
  // createEclipseFinders call instead of triplicating it (the
1755
- // three-runtimes rule — cf. recession-history):
1768
+ // three-runtimes rule — cf. moon-distance-history):
1756
1769
  frameworkSunDeps: Object.freeze({
1757
1770
  sunMeanLongitudeJ2000Deg: sunL0Deg,
1758
1771
  tropicalRateDegPerCy: sunTropicalRateDegPerCy,
@@ -49,9 +49,6 @@
49
49
  * @param {{
50
50
  * constants: {
51
51
  * aMoonNowMetres: number,
52
- * alpha1PerMa: number,
53
- * alpha3PerMa3: number,
54
- * alpha4PerMa4: number,
55
52
  * gmEarthMoonM3PerS2: number,
56
53
  * massRatioEarthMoon: number,
57
54
  * moonSiderealMonthInputDays: number,
@@ -67,7 +64,7 @@
67
64
  * meanSiderealYearSecondsAtAge: (tMa: number) => number,
68
65
  * precessionPeriodYearsAtAge: (tMa: number) => (number | null),
69
66
  * modulation: (tMa: number, s: number) => number,
70
- * distanceMetresAtAge?: (tMa: number) => number,
67
+ * distanceMetresAtAge: (tMa: number) => number|null,
71
68
  * },
72
69
  * }} deps — fns are the ENGINE'S OWN layer-0/1 evaluators and the shared
73
70
  * eccentricity channel's modulation; constants are the engine's derived
@@ -77,32 +74,27 @@
77
74
  * 5c — the tropical month's equinox turn, formerly the counter's 13/H(t),
78
75
  * 0.086 % slow: the chain's J2000 tropical month sat 5.85 ms above the IAU
79
76
  * input, the 1/T_p form is within 0.1 ms of it).
80
- * fns.distanceMetresAtAge, when present, OVERRIDES the internal quartic —
81
- * the Driver-1½ regime-aware history (recession-history.cjs); absent, the
82
- * internal quartic is the pure twin (bit-identical for t ≤ jointMa).
77
+ * fns.distanceMetresAtAge is the engine's CITED distance history
78
+ * (deltat/moon-distance-history; plan 12 option A — the internal quartic
79
+ * twin this chain carried until then is deleted; null beyond the span).
83
80
  */
84
81
  function createMoonMonthChain({ constants, fns }) {
85
82
  const {
86
- aMoonNowMetres, alpha1PerMa, alpha3PerMa3, alpha4PerMa4,
83
+ aMoonNowMetres,
87
84
  gmEarthMoonM3PerS2, massRatioEarthMoon, moonSiderealMonthInputDays,
88
85
  meanSiderealYearJ2000Seconds,
89
86
  moonApsidalJ2000Seconds, moonNodalJ2000Seconds,
90
87
  moonSiderealMonthJ2000Seconds,
91
88
  sPerigee, sNode,
92
89
  } = constants;
93
- const { meanLodSecondsAtAge, meanSiderealYearSecondsAtAge, precessionPeriodYearsAtAge, modulation } = fns;
90
+ const { meanLodSecondsAtAge, meanSiderealYearSecondsAtAge, precessionPeriodYearsAtAge, modulation, distanceMetresAtAge } = fns;
91
+ if (typeof distanceMetresAtAge !== 'function') throw new Error('month-chain: fns.distanceMetresAtAge (the cited distance history) is required');
92
+ void aMoonNowMetres;
94
93
 
95
- /** Farhat α₁/α₃/α₄ polynomial — Earth–Moon distance in metres at age t_Ma.
96
- * @param {number} tMa @returns {number} */
97
- function quarticMetresAtAge(tMa) {
98
- const t = tMa;
99
- return aMoonNowMetres * (1 + alpha1PerMa * t + alpha3PerMa3 * t * t * t + alpha4PerMa4 * t * t * t * t);
100
- }
101
- const distanceMetresAtAge = fns.distanceMetresAtAge ?? quarticMetresAtAge;
102
-
103
- /** @param {number} tMa @returns {number} */
94
+ /** @param {number} tMa @returns {number|null} */
104
95
  function distanceKmAtAge(tMa) {
105
- return distanceMetresAtAge(tMa) / 1000;
96
+ const m = distanceMetresAtAge(tMa);
97
+ return m === null ? null : m / 1000;
106
98
  }
107
99
 
108
100
  /** Solar Δa correction (km) for the Moon's apparent semi-major axis
@@ -120,6 +112,7 @@ function createMoonMonthChain({ constants, fns }) {
120
112
  * @param {number} tMa @returns {number | null} */
121
113
  function distanceCorrectedKmAtAge(tMa) {
122
114
  const aApp = distanceKmAtAge(tMa);
115
+ if (aApp === null) return null;
123
116
  const dA = solarDeltaAKmAtAge(tMa, aApp);
124
117
  return (dA === null) ? null : aApp + dA;
125
118
  }