@essrt/physics 23.3.2 → 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 +6 -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 +35 -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
@@ -0,0 +1,38 @@
1
+ /**
2
+ * ΔT (TT − UT1) — the raw integral plus the cycle stack, memoized on the exact argument.
3
+ *
4
+ * One slice of `assembleModel` (../model.js): receives the context the earlier slices built, returns the bindings the later ones read.
5
+ */
6
+
7
+ /** @param {import('./moon-epoch.js').Ctx} ctx @returns the bindings the later slices read */
8
+ export function assembleDeltaT(ctx) {
9
+ const { F, deepLod, dtCycles, yearToTMa } = ctx;
10
+ /** @param {number} tMa @returns {number} */
11
+ // Exact-argument memo (the alphaMemo pattern of deltat/deep-time-chain.js): the cardinal Newton
12
+ // solve asks for ΔT at the SAME jd twice per step (jdTTFromUT in the
13
+ // apparent Sun and again under the planetary completion) — measured 8.8 →
14
+ // 5.6 ms per panel year (8 solves) at modern epochs, values bit-identical.
15
+ /** @type {Map<number, number>} */
16
+ const dtMemo = new Map();
17
+ /** @param {number} tMa @returns {number} */
18
+ const meanDeltaTSecondsAtAge = (tMa) => {
19
+ if (tMa === 0) return 0;
20
+ const hit = dtMemo.get(tMa);
21
+ if (hit !== undefined) return hit;
22
+ let result = deepLod.deltaTRawSecondsAtAge(tMa);
23
+ const yearY = 2000 - tMa * 1e6;
24
+ result += dtCycles.cycleDeltaTSecondsAt('bond', yearY);
25
+ result += dtCycles.cycleDeltaTSecondsAt('hallstatt', yearY);
26
+ result += dtCycles.cycleDeltaTSecondsAt('jose5', yearY);
27
+ result += dtCycles.cycleDeltaTSecondsAt('jose4', yearY);
28
+ result += dtCycles.swingDeltaTSecondsAt(yearY);
29
+ if (dtMemo.size >= 8192) dtMemo.clear();
30
+ dtMemo.set(tMa, result);
31
+ return result;
32
+ };
33
+ /** @param {number} year @returns {number} */
34
+ const deltaTSeconds = (year) => F.DT_ANCHOR.deltaTStartSeconds + meanDeltaTSecondsAtAge(yearToTMa(year)); // the joint fit's anchor (plan 14: its own artifact)
35
+
36
+ return { deltaTSeconds, meanDeltaTSecondsAtAge };
37
+ }
38
+ /** @typedef {import('./moon-epoch.js').Ctx & ReturnType<typeof assembleDeltaT>} Ctx */
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Derived constants — the §9-order scalars from the constants context and the J2000 mass chain.
3
+ *
4
+ * One slice of `assembleModel` (../model.js): receives the context the earlier slices built, returns the bindings the later ones read.
5
+ */
6
+ import { computeMassChainJ2000 } from '../earth/mass-chain.cjs';
7
+ import { computeMeanTropicalYearJ2000Days } from '../earth/year-lengths.cjs';
8
+
9
+ /** @param {import('./context.js').ModelInputs} ctx @returns the bindings the later slices read */
10
+ export function assembleDerivedConstants(ctx) {
11
+ const { C } = ctx;
12
+ // The mean solar year in days (the calendar's year axis) is the J2000 mean
13
+ // tropical year DERIVED in one home from the IAU sidereal year and p_A per
14
+ // Julian year (year-lengths.cjs).
15
+ const meanSolarYearDays = computeMeanTropicalYearJ2000Days(C.yearLengthRef.siderealYear, C.earthOrbital.generalPrecessionJ2000_arcsecPerYr);
16
+ // The scene epoch's ONE home — astro-reference timeReference.sceneEpochJD /
17
+ // sceneEpochYear; the SI-year axis (time-axis.js) anchors on the epoch itself.
18
+ const startmodelYear = C.timeReference.sceneEpochYear;
19
+ const startmodelJD = C.timeReference.sceneEpochJD;
20
+
21
+ const meanSiderealYearDays = C.yearLengthRef.siderealYear;
22
+ // (the sidereal year in SI seconds is the mass chain's own, below)
23
+ // The J2000 day length ω₀ is the SI day (an input).
24
+ const meanLengthOfDay = C.yearLengthRef.solarDay;
25
+ const meanTropicalYearJ2000Seconds = meanSolarYearDays * meanLengthOfDay;
26
+ // THE J2000 precession anchor p₀ in days (Julian years) — the IAU 2006
27
+ // general precession; the Moon's star-referenced periods read it.
28
+ const axialPrecessionJ2000Days = (1296000 / C.earthOrbital.generalPrecessionJ2000_arcsecPerYr) * 365.25;
29
+
30
+ const G_CONSTANT = C.physicalConstants.G_CONSTANT;
31
+ const MASS_RATIO_EARTH_MOON = C.physicalConstants.MASS_RATIO_EARTH_MOON;
32
+ const currentAUDistance = C.physicalConstants.currentAUDistance;
33
+ const moonDistanceKm = C.moonReference.moonDistance;
34
+ const moonSiderealMonthInput = C.moonReference.moonSiderealMonthInput;
35
+
36
+ // The Moon's sidereal month is the IAU input itself.
37
+ const moonSiderealMonth = moonSiderealMonthInput;
38
+
39
+ // The mass chain (Moon Kepler → GM_EM → the Earth/Moon split → the Sun, §9
40
+ // order) — ONE home: earth/mass-chain.cjs (the deep-time chain,
41
+ // deep-time-chain.js, computes the same scalars from the same context).
42
+ const mass = computeMassChainJ2000({
43
+ moonDistanceKm, moonSiderealMonthDays: moonSiderealMonth, meanSiderealYearDays,
44
+ meanLengthOfDaySeconds: meanLengthOfDay, massRatioEarthMoon: MASS_RATIO_EARTH_MOON,
45
+ currentAUDistanceKm: currentAUDistance, gravitationalConstantKm3KgS2: G_CONSTANT,
46
+ });
47
+ const moonOrbitalShift = mass.moonOrbitalShiftKm;
48
+ const moonDistanceCorrected = mass.moonDistanceCorrectedKm;
49
+ const GM_EARTH_MOON_SYSTEM = mass.gmEarthMoonSystemKm3S2;
50
+ const GM_EARTH_ALONE = mass.gmEarthAloneKm3S2;
51
+ const GM_MOON_ALONE = mass.gmMoonAloneKm3S2;
52
+ const GM_SUN_PLUS_EARTH = mass.gmSunPlusEarthKm3S2;
53
+ const GM_SUN = mass.gmSunKm3S2;
54
+ const M_SUN = mass.sunMassKg;
55
+
56
+ return { GM_EARTH_ALONE, GM_EARTH_MOON_SYSTEM, GM_MOON_ALONE, GM_SUN, GM_SUN_PLUS_EARTH, G_CONSTANT, MASS_RATIO_EARTH_MOON, M_SUN, axialPrecessionJ2000Days, currentAUDistance, meanLengthOfDay, meanSiderealYearDays, meanSolarYearDays, meanTropicalYearJ2000Seconds, moonDistanceCorrected, moonDistanceKm, moonOrbitalShift, moonSiderealMonth, moonSiderealMonthInput, startmodelJD, startmodelYear };
57
+ }
58
+ /** @typedef {import('./context.js').ModelInputs & ReturnType<typeof assembleDerivedConstants>} Ctx */
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Earth scalars of date — e, ϖ, ε read from the one-source movement; the eclipse Sun's mean elements (the research `laws` hook).
3
+ *
4
+ * One slice of `assembleModel` (../model.js): receives the context the earlier slices built, returns the bindings the later ones read.
5
+ */
6
+
7
+ /** @param {import('./deep-time-chain.js').Ctx} ctx @returns the bindings the later slices read */
8
+ export function assembleEarthScalars(ctx) {
9
+ const { F, laws, late } = ctx;
10
+ /** @param {number} year @returns {number} */
11
+ // Plan 06 layer B — the eclipse Sun onto the series. The PUBLISHED Earth
12
+ // ϖ of date is the one-source series (the secular orbit; its J2000 element
13
+ // ≡ La2004's — the same field the D4c apsidal wheel renders). The SUN's
14
+ // equation of centre needs the MEAN elements of the era: the series plus the
15
+ // derived mean offset of the osculating channel (F.EARTH_OSCULATING_MEAN_
16
+ // OFFSET, tools/verify/earth-osculating-offset.js — the 1890–2110 mean of
17
+ // osculating − secular from the Horizons-seeded nine-body run). Measured:
18
+ // the fast osculating wobble is carried by the completion's direct terms and
19
+ // double-counts if added; the window mean is what the Sun residual holds
20
+ // (all-phase JPL sd 3.23″ → 1.59″).
21
+ const earthPerihelionDeg = (year) => late.oneSourceM.periOfDateDegAt(year);
22
+ const SUN_OFF = F.EARTH_OSCULATING_MEAN_OFFSET;
23
+ /** The eclipse Sun's ϖ of date (mean elements). @param {number} year @returns {number} */
24
+ const sunPerihelionDegAt = laws.perihelionLongitudeDegAt ?? ((year) => late.oneSourceM.periOfDateDegAt(year) + SUN_OFF.dPomArcsec / 3600);
25
+ // The obliquity of date is the one-source hybrid everywhere (the eclipse
26
+ // tier's besselian reads it here).
27
+ /** @param {number} year @returns {number} */
28
+ const obliquityDeg = (year) => late.oneSourceM.epsAt(year);
29
+ // ONE eccentricity for the whole model, two readers. The PUBLISHED Earth
30
+ // eccentricity is the one-source series (|z|, the secular orbit) and the
31
+ // SUN's e adds the derived mean offset (see sunPerihelionDegAt) — the
32
+ // clock-side machinery reads these: the eclipse Sun's equation of centre,
33
+ // the besselian Sun distance, the cardinal braid. The LUNAR chain
34
+ // (E-factor, perigee/node modulation, argument Δe² and the of-date rates)
35
+ // rides the ONE deep e — the engine's own ±10-Myr z-vector, `deepEcc` of
36
+ // the deep-time chain (deep-time-chain.js); the two agree within 4.2e-5
37
+ // in-era. The laws hook deliberately does NOT reach the deep e channel.
38
+ /** @param {number} year @returns {number} */
39
+ const eccentricityAt = (year) => late.oneSourceM.eAt(year);
40
+ /** The eclipse Sun's e (mean elements). @param {number} year @returns {number} */
41
+ const sunEccentricityAt = laws.eccentricityAt ?? ((year) => late.oneSourceM.eAt(year) + SUN_OFF.dE);
42
+ // (Earth's inclination and node of date — `inclinationDeg` /
43
+ // `ascendingNodeDeg` — are the one-source construction in inv-plane-elements.js.)
44
+
45
+ return { earthPerihelionDeg, eccentricityAt, obliquityDeg, sunEccentricityAt, sunPerihelionDegAt };
46
+ }
47
+ /** @typedef {import('./deep-time-chain.js').Ctx & ReturnType<typeof assembleEarthScalars>} Ctx */
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Earth's invariable-plane elements of date — the ONE published i_inv / Ω_inv.
3
+ *
4
+ * One slice of `assembleModel` (../model.js): receives the context the earlier slices built, returns the bindings the later ones read.
5
+ */
6
+ import { CHAIN_ARTIFACT } from '../planets/chain-artifact.js';
7
+ import { convertNodeSFrameToEquatorOriginDeg } from '../planets/inv-plane-frame.cjs';
8
+
9
+ /** @param {import('./planet-elements-of-date.js').Ctx} ctx @returns the bindings the later slices read */
10
+ export function assembleInvPlaneElements(ctx) {
11
+ const { nodeOriginSSDeg, oneSourceM } = ctx;
12
+ // ONE published i_inv / Ω_inv everywhere — the mirror of the browser's
13
+ // inclInvPlaneModel / ascNodeInvPlaneModel (identical ops): the engine's
14
+ // own Earth-orbit normal of date (the one-source sampler — the banked
15
+ // secular series inside ±10 Myr, the mode tail beyond) against the
16
+ // artifact's invariable plane, the node expressed in the Souami & Souchay
17
+ // longitude origin through the same derived conversion the planets' node
18
+ // takes. Doc 05 banks the one-source reading at rms 0.003° vs La2010 over
19
+ // −500 kyr. Argument: the engine year (dynamical time,
20
+ // Julian from J2000 TT — the sampler's own coordinate, as the chain's).
21
+ const invPlaneGeometry = (() => {
22
+ const ip = /** @type {{ inclEclipticDeg: number, ascNodeEclipticDeg: number }} */ (/** @type {any} */ (CHAIN_ARTIFACT).invariablePlane);
23
+ const D2R = Math.PI / 180;
24
+ const si = Math.sin(ip.inclEclipticDeg * D2R), ci = Math.cos(ip.inclEclipticDeg * D2R);
25
+ const nInv = [si * Math.sin(ip.ascNodeEclipticDeg * D2R), -si * Math.cos(ip.ascNodeEclipticDeg * D2R), ci];
26
+ const xIp = [Math.cos(ip.ascNodeEclipticDeg * D2R), Math.sin(ip.ascNodeEclipticDeg * D2R), 0];
27
+ 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]];
28
+ return { ascNodeEclipticDeg: ip.ascNodeEclipticDeg, nInv, xIp, yIp };
29
+ })();
30
+ /** The engine Earth-orbit normal of date, J2000 ecliptic frame. @param {number} year @returns {number[]} */
31
+ const earthOrbitNormalJ2000At = (year) => {
32
+ const s = oneSourceM.sampleAt(year);
33
+ return [s.orbitNormalX, s.orbitNormalY, Math.sqrt(Math.max(0, 1 - s.orbitNormalX * s.orbitNormalX - s.orbitNormalY * s.orbitNormalY))];
34
+ };
35
+ /** Earth's inclination of date to the model's invariable plane, degrees. @param {number} year @returns {number} */
36
+ const inclinationDeg = (year) => {
37
+ const nE = earthOrbitNormalJ2000At(year), nInv = invPlaneGeometry.nInv;
38
+ const d = nE[0] * nInv[0] + nE[1] * nInv[1] + nE[2] * nInv[2];
39
+ return Math.acos(Math.min(1, Math.max(-1, d))) / (Math.PI / 180);
40
+ };
41
+ /** Earth's ascending node of date on the invariable plane, S&S origin, degrees in [0, 360). @param {number} year @returns {number} */
42
+ const ascendingNodeDeg = (year) => {
43
+ const D2R = Math.PI / 180;
44
+ const { ascNodeEclipticDeg, nInv, xIp, yIp } = invPlaneGeometry;
45
+ const nE = earthOrbitNormalJ2000At(year);
46
+ 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]];
47
+ const L = Math.hypot(N[0], N[1], N[2]);
48
+ N = [N[0] / L, N[1] / L, N[2] / L];
49
+ const sFrameDeg = ((Math.atan2(N[0] * yIp[0] + N[1] * yIp[1] + N[2] * yIp[2],
50
+ N[0] * xIp[0] + N[1] * xIp[1] + N[2] * xIp[2]) / D2R + ascNodeEclipticDeg) % 360 + 360) % 360;
51
+ return convertNodeSFrameToEquatorOriginDeg(sFrameDeg, nodeOriginSSDeg());
52
+ };
53
+
54
+ return { ascendingNodeDeg, inclinationDeg };
55
+ }
56
+ /** @typedef {import('./planet-elements-of-date.js').Ctx & ReturnType<typeof assembleInvPlaneElements>} Ctx */
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Lunar theory — the shared-chain assembly: the derived months, the chain-cycle counters, the arguments, UT→TT, the Meeus series.
3
+ *
4
+ * One slice of `assembleModel` (../model.js): receives the context the earlier slices built, returns the bindings the later ones read.
5
+ */
6
+ import { createChainCycleIntegrator } from '../chain-cycles/index.cjs';
7
+ import { J2000_JD, JULIAN_CENTURY_DAYS } from '../constants/units.cjs';
8
+ import { createMoonArguments, jdToDecimalYear } from '../moon/arguments.cjs';
9
+ import { createMoonSeries } from '../moon/series.cjs';
10
+ import { driver2PeriodSecondsAtAge } from '../planets/orbit-chain.cjs';
11
+
12
+ /** @param {import('./time-axis.js').Ctx} ctx @returns the bindings the later slices read */
13
+ export function assembleLunarTheory(ctx) {
14
+ const {
15
+ C, F, late, EPOCH_PARAMS, axialPrecessionJ2000Days, chain, deepEcc, deepLod, meanDeltaTSecondsAtAge,
16
+ meanSiderealYearDays, meanSolarYearDays, meanTropicalYearJ2000Seconds, moonChain, moonDistanceKm,
17
+ moonDistanceMetresAtAge, moonSiderealMonth, oneSourceM, startmodelJD, startmodelYear, sunPerihelionDegAt,
18
+ yearFromJD,
19
+ } = ctx;
20
+ // Wiring mirrors the engine call sites exactly — tools/lib/deep-time.js
21
+ // (ecc channel, month chain, J2000 precession anchors),
22
+ // tools/lib/scene-graph.js (chain-cycles, arguments, series) and
23
+ // tools/verify/eclipse-audit.js (finders). Deep-time and framework-native
24
+ // are hardwired ON here: they are the shipped defaults; the A/B env
25
+ // toggles (SG_DEEP_TIME/MOON_ARGS_PURE_MEEUS) stay an engine concern.
26
+ const j2000JD = J2000_JD; // ONE home: constants/units (plan 14 step 2)
27
+ const julianCenturyDays = JULIAN_CENTURY_DAYS;
28
+
29
+ // J2000 Moon precession anchors (the IAU inputs themselves). The
30
+ // legacy-'ICRF'-named inputs hold the EQUINOX-OF-DATE observables (Meeus/
31
+ // IERS 3231.50 d / 6798.38 d); the star-referenced (inertial) periods
32
+ // follow by removing the equinox's own precession, 1/T_E = 1/T_I ∓ 1/T_p
33
+ // (perigee advances, node regresses), T_p the J2000 precession anchor.
34
+ const { moonApsidalJ2000Seconds } = chain;
35
+ // (The of-date perigee/node are the chain's count compositions —
36
+ // mcApsidalOfDate / mcNodalOfDate below.)
37
+
38
+ // Derived months on the IAU inputs (constants.js §Moon derived months)
39
+ const moonTropicalMonthDays = 1 / (1 / moonSiderealMonth + 1 / axialPrecessionJ2000Days);
40
+ const moonAnomalisticMonthDays = 1 / (1 / moonSiderealMonth - 86400 / moonApsidalJ2000Seconds);
41
+ const moonSynodicMonthDays = moonSiderealMonth * meanSiderealYearDays / (meanSiderealYearDays - moonSiderealMonth);
42
+
43
+ // The Layer-2 month/precession chain (Brouwer-Clemence m² scaling × the
44
+ // e_E-line modulation on the ONE deep e — decision (ii), plan 02 §8) is
45
+ // the deep-time chain's `moonChain` (deep-time-chain.js).
46
+
47
+ // Chain-cycle integrator. S5/S12 conventions: age anchor = startmodelYear
48
+ // (the scene's t_Ma convention), grid anchor C(2000) = 0 — grid anchor ≠
49
+ // age anchor, deliberately. One stable period fn per chain so the shared
50
+ // Float64Array tables key correctly and persist.
51
+ const chainCycles = createChainCycleIntegrator({
52
+ ageAnchorYear: startmodelYear,
53
+ tropicalYearJ2000Seconds: meanTropicalYearJ2000Seconds, // the SI tick (plan 09 step 7c′)
54
+ isDeepTime: () => true,
55
+ });
56
+ /** @param {number} tMa @returns {number|null} */
57
+ const nodalMonthPeriodFn = (tMa) => moonChain.nodalMonthSecondsAtAge(tMa);
58
+ /** @param {number} tMa @returns {number|null} */
59
+ const tropicalMonthPeriodFn = (tMa) => moonChain.tropicalMonthSecondsAtAge(tMa);
60
+ /** @param {number} tMa @returns {number|null} */
61
+ const anomalisticMonthPeriodFn = (tMa) => moonChain.anomalisticMonthSecondsAtAge(tMa);
62
+ /** @param {number} tMa @returns {number|null} */
63
+ const siderealMonthPeriodFn = (tMa) => moonChain.siderealMonthSecondsAtAge(tMa);
64
+ const jupiterT0Seconds = C.planetOrbitalElements.jupiter.solarYearInput * 86400;
65
+ /** @param {number} tMa @returns {number} */
66
+ const jupiterPeriodFn = (tMa) => driver2PeriodSecondsAtAge(tMa, jupiterT0Seconds, EPOCH_PARAMS.solarMassLossFracPerYear);
67
+ /** @param {number} a @param {number} b @returns {number|null} */
68
+ const mcDraconic = (a, b) => chainCycles.cyclesBetween(nodalMonthPeriodFn, a, b);
69
+ /** @param {number} a @param {number} b @returns {number|null} */
70
+ const mcTropical = (a, b) => chainCycles.cyclesBetween(tropicalMonthPeriodFn, a, b);
71
+ /** @param {number} a @param {number} b @returns {number|null} */
72
+ const mcAnomalistic = (a, b) => chainCycles.cyclesBetween(anomalisticMonthPeriodFn, a, b);
73
+ /** @param {number} a @param {number} b @returns {number|null} */
74
+ const mcJupiter = (a, b) => chainCycles.cyclesBetween(jupiterPeriodFn, a, b);
75
+ /** The Moon's sidereal (star-referenced) orbits — the series' A3 argument (plan 09 step 5c). @param {number} a @param {number} b @returns {number|null} */
76
+ const mcSidereal = (a, b) => chainCycles.cyclesBetween(siderealMonthPeriodFn, a, b);
77
+ /** @param {number} a @param {number} b @returns {number|null} */
78
+ const mcApsidalOfDate = (a, b) => {
79
+ const t = mcTropical(a, b), n = mcAnomalistic(a, b);
80
+ return (t === null || n === null) ? null : t - n;
81
+ };
82
+ /** @param {number} a @param {number} b @returns {number|null} */
83
+ const mcNodalOfDate = (a, b) => {
84
+ const dr = mcDraconic(a, b), t = mcTropical(a, b);
85
+ return (dr === null || t === null) ? null : dr - t;
86
+ };
87
+
88
+ // The argument skeleton (the _FW_MOON bundle; Sun secular deviations on
89
+ // the CALENDAR year coordinate — S3)
90
+ const moonArgs = createMoonArguments({
91
+ constants: {
92
+ j2000JD,
93
+ julianCenturyDays,
94
+ eccentricityJ2000: C.earthOrbital.earthEccentricityJ2000,
95
+ eccentricityDotJ2000: C.earthOrbital.earthEccentricityDotJ2000,
96
+ eccentricityDotDotJ2000: C.earthOrbital.earthEccentricityDotDotJ2000,
97
+ elpEarthFigureJ2ArcsecPerCy2: C.moonMeeus.elpW1T2Decomposition_arcsecPerCy2.earthFigureJ2,
98
+ elpGeneralPrecessionPA_T2ArcsecPerCy2: C.moonMeeus.elpW1T2Decomposition_arcsecPerCy2.generalPrecessionPA_T2_Lieske1976,
99
+ // Decision (ii): the lunar chain reads ONE deep e end to end
100
+ eccE0: deepEcc.e0,
101
+ },
102
+ fns: {
103
+ eccAt: /** @param {number} tYr */ (tYr) => deepEcc.eccAt(tYr),
104
+ channelIntegral: /** @param {number} T @param {number} s */ (T, s) => deepEcc.channelIntegral(T, s),
105
+ // The arguments' obliquity carrier reads the PUBLISHED ε (the one-source
106
+ // hybrid) — one ε everywhere. Twins: script.js (_sceneEpsTargetDeg), scene-graph.js.
107
+ computeObliquityEarth: /** @param {number} year @returns {number} */ (year) => oneSourceM.epsAt(year),
108
+ // The arguments' Sun IS the one certified Sun — the mean longitude that
109
+ // integrates the one-family tropical year (the eclipse finder's Sun,
110
+ // sun-and-eclipses.js) and the series' mean perihelion of date
111
+ // (unwrapped), both on the TT Julian-year axis. Twins: scene-graph.js,
112
+ // script.js.
113
+ sunMeanLonDegAtJdTT: /** @param {number} jdTT @returns {number} */ (jdTT) => late.sunMeanLongitudeDegAt(2000 + (jdTT - j2000JD) / 365.25),
114
+ sunPerihelionLonDegAtJdTT: /** @param {number} jdTT @returns {number} */ (jdTT) => sunPerihelionDegAt(2000 + (jdTT - j2000JD) / 365.25),
115
+ jdToSIyear: yearFromJD,
116
+ tropicalOrbitsBetween: mcTropical,
117
+ apsidalOfDateCyclesBetween: mcApsidalOfDate,
118
+ nodalOfDateCyclesBetween: mcNodalOfDate,
119
+ isDeepTime: () => true,
120
+ isFrameworkNative: () => true,
121
+ // (d′) of-date rate completion: the DYNAMICAL axial precession and the
122
+ // KINEMATIC pair's beat the chains embed. MATCHED TRIPLE with the
123
+ // tools-lib and browser wirings. The dynamical rate of date is the
124
+ // published precession period's (the one-family beat inside its window,
125
+ // the composed clock beyond — published-periods.js). Degrees per Julian year.
126
+ pDynDegPerYearAt: /** @param {number} year */ (year) => 360 / late.publishedAxialPrecessionYearsAtYear(year),
127
+ pKinDegPerYearAt: /** @param {number} year */ (year) => {
128
+ // MODEL-START-anchored age, matching the tools-lib/browser wirings
129
+ // bit-exactly — NOT yearToTMa's 2000.0 convention (the recorded
130
+ // 0.4977-yr parity trap, same class as getMoonDistanceKm in the series wiring below).
131
+ const t = (startmodelYear - year) / 1e6;
132
+ const sid = deepLod.siderealYearSecondsAtAge(t);
133
+ const trop = deepLod.tropicalYearSecondsAtAge(t);
134
+ return (sid === null || trop === null) ? 0 : 360 * (sid - trop) / sid;
135
+ },
136
+ },
137
+ });
138
+
139
+ // UT→TT on the CALENDAR decimal-year coordinate (script.js Phase 9.16 —
140
+ // a linear-year approximation here once cost ~5–6 s of ΔT and ~1e-3° of
141
+ // Moon longitude at the Babylonian epochs)
142
+ /** @param {number} jd @returns {number} */
143
+ const jdTTFromUT = (jd) => {
144
+ const tMa = (startmodelYear - jdToDecimalYear(jd)) / 1e6;
145
+ const dT = meanDeltaTSecondsAtAge(tMa);
146
+ return Number.isFinite(dT) ? jd + dT / 86400 : jd;
147
+ };
148
+ // ΔT at a JD on the CALENDAR-year axis (mirrors deep-time frameworkDeltaT;
149
+ // NOT deltaTSeconds of delta-t.js, which adds the deltaTStart anchor)
150
+ /** @param {number} jd @returns {number} */
151
+ const frameworkDeltaTSecondsAtJD = (jd) => {
152
+ const decYear = startmodelYear + (jd - startmodelJD) / meanSolarYearDays;
153
+ const dT = meanDeltaTSecondsAtAge((startmodelYear - decYear) / 1e6);
154
+ return Number.isFinite(dT) ? dT : 0;
155
+ };
156
+
157
+ // Bounded Meeus E-factor from the ONE deep e (decision (ii); the
158
+ // pure-Meeus polynomial A/B branch stays engine-local)
159
+ /** @param {number} dDays @returns {number} */
160
+ // (on the channel's Julian-year axis — the N-body series' time)
161
+ const fwEFactor = (dDays) => deepEcc.eFactorAt(dDays / 365.25);
162
+
163
+ // D2 derived additional-argument rates (deg/cy, from the J2000 month inputs —
164
+ // docs/109 §22 for the derivation)
165
+ const fwA2RateDegPerCy = 2 * ((360 * 36525) / moonTropicalMonthDays)
166
+ - (360 * 36525) / moonAnomalisticMonthDays
167
+ - 2 * ((360 * 36525) / C.planetOrbitalElements.jupiter.solarYearInput);
168
+ const fwA3RateDegPerCy = (360 * 36525) / moonSiderealMonth;
169
+
170
+ // Meeus Ch. 47 truncated series (framework-native arguments + E-factor)
171
+ const moonSeries = createMoonSeries({
172
+ constants: {
173
+ moonL: F.MEEUS_LONGITUDE_TERMS,
174
+ moonB: F.MEEUS_LATITUDE_TERMS,
175
+ moonR: F.MEEUS_DISTANCE_TERMS.terms,
176
+ moonRMeanKm: F.MEEUS_DISTANCE_TERMS.meanKm,
177
+ moonDistanceJ2000Km: C.moonReference.moonDistance,
178
+ j2000JD,
179
+ julianCenturyDays,
180
+ fwA2RateDegPerCy,
181
+ fwA3RateDegPerCy,
182
+ },
183
+ fns: {
184
+ argsAt: /** @param {number} jdTT */ (jdTT) => moonArgs.argsAt(jdTT),
185
+ eFactorForD: fwEFactor,
186
+ eFactorAtJdTT: /** @param {number} jdTT */ (jdTT) => fwEFactor(jdTT - j2000JD),
187
+ // 20.3d(i): the Driver-1 ratio at the EVALUATED epoch (per-jd pure
188
+ // evaluator). MATCHED TRIPLE with the tools-lib and browser getters:
189
+ // identical age arithmetic — the SI-linear year (yearFromJD, never
190
+ // the calendar year: the recorded linear-vs-calendar mirror trap)
191
+ // against the MODEL-START anchor (startmodelYear, the browser's
192
+ // J2000_CALENDAR_YEAR — NOT this model's 2000.0 yearToTMa convention:
193
+ // the 0.5-yr difference is 4.95e-11 of distance and failed the
194
+ // bit-exact parity gate; plan 11 step 5 dropped the −0.83-d correction
195
+ // from the anchor, both twins alike). Falls back to the J2000 constant
196
+ // without a jd (legacy call shape) or past the domain.
197
+ getMoonDistanceKm: /** @param {number} [jdTT] */ (jdTT) => {
198
+ if (jdTT === undefined) return moonDistanceKm;
199
+ const d = moonDistanceMetresAtAge((startmodelYear - yearFromJD(jdTT)) / 1e6);
200
+ return d === null ? moonDistanceKm : d / 1000;
201
+ },
202
+ getEccentricityBase: () => C.moonReference.moonOrbitalEccentricityBase,
203
+ deltaTSeconds: /** @param {number} jd */ (jd) => (jdTTFromUT(jd) - jd) * 86400,
204
+ jdToSIyear: yearFromJD,
205
+ tropicalOrbitsBetween: mcTropical,
206
+ apsidalOfDateCyclesBetween: mcApsidalOfDate,
207
+ siderealOrbitsBetween: mcSidereal,
208
+ jupiterOrbitsBetween: mcJupiter,
209
+ isDeepTime: () => true,
210
+ isFrameworkNative: () => true,
211
+ },
212
+ });
213
+
214
+ return { frameworkDeltaTSecondsAtJD, j2000JD, jdTTFromUT, julianCenturyDays, moonArgs, moonSeries, moonSynodicMonthDays };
215
+ }
216
+ /** @typedef {import('./time-axis.js').Ctx & ReturnType<typeof assembleLunarTheory>} Ctx */
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The Moon at epoch — the solar-Δa-corrected Kepler month.
3
+ *
4
+ * One slice of `assembleModel` (../model.js): receives the context the earlier slices built, returns the bindings the later ones read.
5
+ */
6
+
7
+ /** @param {import('./one-source-movement.js').Ctx} ctx @returns the bindings the later slices read */
8
+ export function assembleMoonEpoch(ctx) {
9
+ const {
10
+ EPOCH_PARAMS, MASS_RATIO_EARTH_MOON, deepLod, moonDistanceMetresAtAge, moonSiderealMonthInput, yearToTMa,
11
+ } = ctx;
12
+ /** Solar-Δa-corrected Kepler month. @param {number} year @returns {number} */
13
+ const moonSiderealMonthDaysAt = (year) => {
14
+ const tMa = yearToTMa(year);
15
+ const lod = deepLod.lodSecondsAtAge(tMa);
16
+ const aAppM = moonDistanceMetresAtAge(tMa);
17
+ if (lod === null || aAppM === null) return NaN;
18
+ const aAppKm = aAppM / 1000;
19
+ const sidYrDays = deepLod.siderealYearSecondsAtAge(tMa) / lod;
20
+ const deltaA = aAppKm * (1 / (MASS_RATIO_EARTH_MOON + 1)) * (moonSiderealMonthInput / sidYrDays);
21
+ const aCorrM = (aAppKm + deltaA) * 1000;
22
+ const monthSeconds = 2 * Math.PI * Math.sqrt(Math.pow(aCorrM, 3) / EPOCH_PARAMS.gmEarthMoonM3S2);
23
+ return monthSeconds / lod;
24
+ };
25
+
26
+ return { moonSiderealMonthDaysAt };
27
+ }
28
+ /** @typedef {import('./one-source-movement.js').Ctx & ReturnType<typeof assembleMoonEpoch>} Ctx */
@@ -0,0 +1,192 @@
1
+ /**
2
+ * The one-source movement — the hybrid's ε(t), e(t), ϖ(t) on the per-tier sampler, the ONE year-length family, and the days of date on them.
3
+ *
4
+ * One slice of `assembleModel` (../model.js): receives the context the earlier slices built, returns the bindings the later ones read.
5
+ */
6
+ import { createDeepOrbitalHistory } from '../earth/deep-orbital-history.cjs';
7
+ import { ONE_FAMILY_WINDOW_YEARS, createYearLengths } from '../earth/year-lengths.cjs';
8
+ import { DEEP_MODES_ARTIFACT } from '../moon/deep-modes-artifact.cjs';
9
+ import { CHAIN_ARTIFACT } from '../planets/chain-artifact.js';
10
+
11
+ /** @param {import('./year-day-lengths.js').Ctx} ctx @returns the bindings the later slices read */
12
+ export function assembleOneSourceMovement(ctx) {
13
+ const {
14
+ C, options, secularSeriesArtifact, EPOCH_PARAMS, alphaGia, certifiedAxialPrecessionJ2000Years, dayLengthSeconds,
15
+ deepLod, dtCycleLodCorrectionSum, meanSolarYearDays, startmodelYear, yearToTMa,
16
+ } = ctx;
17
+ // The cardinal INSTANTS are the crossings of the one Sun the scene renders
18
+ // and the eclipse chain certifies (cardinal-instants.js); the cardinal
19
+ // STRUCTURE here is the EoC layer (year lengths, crossing offsets, the
20
+ // e(t)-proportional spread) on the one-source movement's own e(t)/ϖ(t) —
21
+ // the MODE tier (the package's embedded deep-modes artifact; the series
22
+ // tier when the governed artifact is injected). Absolute dates deliberately
23
+ // absent — see cardinal/one-source-structure.cjs. The mean year is SI
24
+ // SECONDS: the sidereal year of date reduced by the SECULAR equinox
25
+ // precession on the composed T_p(t).
26
+ // THE ONE of-date year-length family — createYearLengths owns tropical
27
+ // (equinox-rate mean, wobble included), sidereal (the λ̇ channel),
28
+ // anomalistic (the cardinal structure on the SAME mean), all λ̇-corrected
29
+ // coherently, SI seconds, and the beats of that pair.
30
+ // S3 tier unification: when the caller injects the governed secular-
31
+ // series artifact (createModel opts.secularSeriesArtifact — the API/Node
32
+ // path; the npm package alone has no 8 MB series), the movement runs the
33
+ // SERIES tier exactly like the browser — killing the mode-vs-series
34
+ // value split (measured: 6 s on the anomalistic year at J2000).
35
+ // The ONE-SOURCE movement: the hybrid's ε(t) (plan 06 Phase 3 S3b — THE
36
+ // published obliquity) and the one-family year lengths, on the same
37
+ // per-tier sampler.
38
+ const oneSourceM = (() => {
39
+ const AEarth = /** @type {any} */ (CHAIN_ARTIFACT).j2000AnchorElements.earth;
40
+ // The ψ̇ anchor: the IAU 2006 general precession p_A (25,771.575 yr — the
41
+ // ONE home certifiedAxialPrecessionJ2000Years, twins in tools-lib and the
42
+ // browser).
43
+ const axial0 = certifiedAxialPrecessionJ2000Years(); // the ONE home
44
+ // The deep-time ψ̇(t) the hybrid precesses on is the COMPOSED lunisolar
45
+ // rate — its period of date T_p(t) read directly; the composition carries
46
+ // the lunar 1/a³ growth (104.5 ″/yr at 2.46 Ga against Lantink 2022's
47
+ // 108.6 ± 8.5).
48
+ const seriesArt = /** @type {any} */ (secularSeriesArtifact);
49
+ const sb = seriesArt && seriesArt.bodies && seriesArt.bodies.earth;
50
+ // Plan 12 §8.5 (owner): the hybrid's own domain is the banked ζ series' span (years; null
51
+ // without a series). Beyond it the ζ-mode tail carries the Quaternary mean with no tidal
52
+ // evolution — the API refuses the obliquity there and serves the cited mean beside it.
53
+ const obliquityHybridDomainYears = sb
54
+ ? /** @type {[number, number]} */ ([2000 + seriesArt.t0Yr, 2000 + seriesArt.t0Yr + (sb.zetaQ.length - 1) * sb.stepYr])
55
+ : null;
56
+ const hist = createDeepOrbitalHistory({
57
+ zModes: DEEP_MODES_ARTIFACT.earthZ,
58
+ zetaModes: DEEP_MODES_ARTIFACT.earthZeta,
59
+ ...(sb ? {
60
+ zetaSeries: { t0Yr: seriesArt.t0Yr, stepYr: sb.stepYr, q: sb.zetaQ, p: sb.zetaP },
61
+ zSeries: { t0Yr: seriesArt.t0Yr, stepYr: sb.stepYr, q: sb.zQ, p: sb.zP },
62
+ } : {}),
63
+ anchorE: AEarth.e,
64
+ anchorPeriEclipticDeg: AEarth.lonPeriEclipticDeg,
65
+ anchorInclEclipticDeg: AEarth.inclEclipticDeg,
66
+ anchorAscNodeEclipticDeg: AEarth.ascNodeEclipticDeg,
67
+ axialPrecessionYearsJ2000: axial0,
68
+ // The hybrid's initial condition is the OBSERVED J2000 mean obliquity —
69
+ // the IAU 2006 input constant (84381.406″), the same anchor the
70
+ // browser's _deepHistSeries and the Node one-source movement use.
71
+ obliquityJ2000Deg: C.earthOrbital.obliquityJ2000_deg,
72
+ // The CONSTANT's scaling (the torque period), not the published rate of date: the
73
+ // integration's own ŝ·n̂ carries the obliquity's cosine, so the cited mean obliquity's
74
+ // factor (plan 12 §8 option (a)) must not enter twice. Bit-identical to the pre-§8 form.
75
+ axialPrecessionYearsAtYearFn: (yr) => deepLod.lunisolarTorquePeriodYearsAtAge((startmodelYear - yr) / 1e6) ?? axial0,
76
+ // THE PRECESSION OF DATE: on top of the secular law above, the solar
77
+ // torque at the eccentricity of date and the oblateness of date from
78
+ // the GIA channel — both exactly 1 at J2000, so the J2000 anchor is
79
+ // untouched. The channel is
80
+ // read PURE (alphaGia, not the lattice-pin-aware earthMoiFactorAtAge):
81
+ // the samplers are cached, their values must be pure in year.
82
+ solarTorqueShareJ2000: EPOCH_PARAMS.precessionSolarShareJ2000,
83
+ // options.giaEllipticityOfDate === false: the orbital forcing alone (the T1 line derivation).
84
+ dynamicalEllipticityRatioAtYearFn: options.giaEllipticityOfDate === false ? undefined : alphaGia.j2RatioAt,
85
+ });
86
+ // PER-TIER ROUTING (the anomalistic-contamination fix): one sampler PER
87
+ // grid tier, built on first entry and KEPT — a query always reads the
88
+ // tier its own span selects. The former single grown sampler replaced
89
+ // the 100-yr grid with the 1000/5000-yr one after any deep-time probe,
90
+ // and the year-length rates (±0.5-yr central differences through the
91
+ // grid) then returned grid-segment averages instead of local rates
92
+ // (anomalistic of date +2.63 s / +19 s, visit-order dependent).
93
+ const samplers = /** @type {Map<number, any>} */ (new Map());
94
+ const gridStep = (/** @type {number} */ n) => (n <= 50000 ? 100 : n <= 2000000 ? 1000 : 5000);
95
+ const tierSpan = (/** @type {number} */ n) => (n <= 50000 ? 50000 : 2000000);
96
+ const sampleAt = (/** @type {number} */ year) => {
97
+ const t = year - 2000, need = Math.max(20000, Math.abs(t) * 1.25);
98
+ if (need > 2000000) {
99
+ // The deep tier (5000-yr grid): walked in the queried DIRECTION only,
100
+ // to two nodes past the query, and EXTENDED by the factory's shared
101
+ // walk when a deeper epoch arrives (plan 13 A1). It was built to
102
+ // ±1.25·|t| in both directions and rebuilt from J2000 on every deeper
103
+ // probe — bit-identical values, 4× the RK4 steps of one walk.
104
+ const span = Math.ceil((Math.abs(t) + 2 * 5000) / 5000) * 5000;
105
+ return hist.build(t < 0 ? -span : span, 0, 5000).at(t);
106
+ }
107
+ const span = tierSpan(need);
108
+ if (!samplers.has(span)) samplers.set(span, hist.build(span, -span, gridStep(span)));
109
+ return samplers.get(span).at(t);
110
+ };
111
+ // The anomalistic year rides THIS movement's own apsidal line (the
112
+ // scene's perihelion) — the factory owns the construction; the planet
113
+ // chain's secular tangent is no longer a member (year-lengths.cjs).
114
+ const yearLengths = createYearLengths({
115
+ sampleAt,
116
+ massLossSiderealSecondsAtYearFn: (year) => deepLod.siderealYearSecondsAtAge(yearToTMa(year)),
117
+ });
118
+ return {
119
+ yearLengths,
120
+ /** 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 */
121
+ sampleAt,
122
+ /** EXPERIMENT accessors: the series' e and equinox-referenced ϖ of date. @param {number} year @returns {number} */
123
+ eAt: (year) => sampleAt(year).e,
124
+ /** @param {number} year @returns {number} */
125
+ periOfDateDegAt: (year) => sampleAt(year).periOfDateDeg,
126
+ /** 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} */
127
+ epsAt: (year) => sampleAt(year).epsDeg,
128
+ /** The hybrid's own domain, [yearFrom, yearTo] — the banked ζ series' span (plan 12 §8.5); null without a series. */
129
+ obliquityHybridDomainYears,
130
+ /** 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}} */
131
+ precessionOfDateFactorsAt: (year) => hist.precessionOfDateFactorsAt(year - 2000),
132
+ /** The spin axis's INERTIAL precession period at J2000, years — the self-anchored
133
+ * lunisolar rate the hybrid integrates ŝ on (deep-orbital-history D4e): the general
134
+ * precession anchor plus the planetary mean the ecliptic's own motion carries. */
135
+ axialPrecessionYearsLunisolarJ2000: hist.axialPrecessionYearsLunisolarJ2000,
136
+ };
137
+ })();
138
+ const yearLengthsM = oneSourceM.yearLengths;
139
+ /** THE one-family tropical year of date in SI seconds: route B inside the
140
+ * one-family window, the tidal-chain year beyond (plan 06 R1 — the Sun's
141
+ * mean longitude, the cardinal instants and the published year lengths
142
+ * all ride this one function). @param {number} year @returns {number} */
143
+ const oneFamilyTropicalYearSeconds = (year) => (Math.abs(year - 2000) <= ONE_FAMILY_WINDOW_YEARS
144
+ ? yearLengthsM.tropicalYearSecondsAtYear(year)
145
+ : (deepLod.tropicalYearSecondsAtAge(yearToTMa(year)) ?? meanSolarYearDays * 86400));
146
+
147
+ /** Tropical year of date in DAYS OF THE EPOCH (the kinematic day,
148
+ * year-day-lengths.js) — the one-family SI year over the epoch's day
149
+ * length, so solarYearSeconds ≡ the one-family year. @param {number} year @returns {number} */
150
+ const tropicalYearDays = (year) => oneFamilyTropicalYearSeconds(year) / dayLengthSeconds(year);
151
+
152
+ /** Layer-4 solar day of date in SI seconds — LOD_real, the shipped
153
+ * observable (the tweakpane Solar Day row, the registry's lodRealPhysical,
154
+ * the Framework Verification panel's day): the kinematic day of date + the
155
+ * ecliptic missing-motion term on the nodal period + the calibrated ΔT
156
+ * cycle stack incl. the core-mantle swing. Twin of tools/lib
157
+ * computeLodRealSecondsAtEpoch and the browser's _vfpSolarDaySecondsOfDate
158
+ * (86,400.001780 s at 2000). @param {number} year @returns {number} */
159
+ const lodRealSeconds = (year) =>
160
+ dayLengthSeconds(year)
161
+ + (deepLod.eclipticLodCorrectionSecondsAtAge(yearToTMa(year)) ?? 0)
162
+ + dtCycleLodCorrectionSum(year);
163
+ /** Mean sidereal day of date in SI seconds — on LOD_real (plan 08 storyline
164
+ * sweep, 2026-10-09: the published day bases are the SI day of exactly
165
+ * 86,400 s, the IAU constants, and LOD_real; this rode the KINEMATIC day
166
+ * before, 1.7 ms below the simulator's readout at J2000). One rotation
167
+ * more against the equinox than there are solar days in a year.
168
+ * @param {number} year @returns {number} */
169
+ const siderealDaySeconds = (year) => {
170
+ const Y = tropicalYearDays(year);
171
+ return (lodRealSeconds(year) * Y) / (Y + 1);
172
+ };
173
+ /** Mean stellar day of date in SI seconds — the sidereal day plus the
174
+ * equinox's daily regression projected onto the equator (on LOD_real, like
175
+ * the sidereal day). @param {number} year @returns {number} */
176
+ const stellarDaySeconds = (year) => {
177
+ const tMa = yearToTMa(year);
178
+ // One equinox turn per T_p(t) — the composed lunisolar period (its
179
+ // certified J2000 anchor past the chain's domain).
180
+ const TpRaw = deepLod.lunisolarPrecessionPeriodYearsAtAge(tMa);
181
+ const Tp = TpRaw === null ? certifiedAxialPrecessionJ2000Years() : TpRaw;
182
+ const syD = tropicalYearDays(year);
183
+ const sidDay = siderealDaySeconds(year);
184
+ // The RA projection reads the published ε (the one-source hybrid) — the
185
+ // browser's _sceneEpsTargetDeg twin; 1e-8 s.
186
+ const raProjection = Math.cos((oneSourceM.epsAt(year) * Math.PI) / 180);
187
+ return (sidDay / Tp / (syD + 1)) * raProjection + sidDay;
188
+ };
189
+
190
+ return { lodRealSeconds, oneFamilyTropicalYearSeconds, oneSourceM, siderealDaySeconds, stellarDaySeconds, tropicalYearDays, yearLengthsM };
191
+ }
192
+ /** @typedef {import('./year-day-lengths.js').Ctx & ReturnType<typeof assembleOneSourceMovement>} Ctx */