@essrt/physics 1.7.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@essrt/physics",
3
- "version": "1.7.0",
3
+ "version": "1.9.0",
4
4
  "description": "ESSRT (Expanding Solar System Resonance Theory) physics core \u2014 pure, injectable-constants, no I/O, no globals, no DOM. The complete model incl. fitted coefficients; every published version immutably ships one recorded model identity.",
5
5
  "essrt": {
6
6
  "modelVersion": "v11.0",
@@ -36,6 +36,7 @@
36
36
  "./chain-cycles": "./src/chain-cycles/index.cjs",
37
37
  "./moon/arguments": "./src/moon/arguments.cjs",
38
38
  "./moon/series": "./src/moon/series.cjs",
39
+ "./moon/series-extension": "./src/moon/series-extension.cjs",
39
40
  "./moon/apparent": "./src/moon/apparent.cjs",
40
41
  "./planets/geometry": "./src/planets/geometry.cjs",
41
42
  "./planets/fibonacci-laws": "./src/planets/fibonacci-laws.cjs",
@@ -52,6 +53,7 @@
52
53
  "./deltat/recession-history": "./src/deltat/recession-history.cjs",
53
54
  "./climate/l1-orbital": "./src/climate/l1-orbital.cjs",
54
55
  "./eclipse/finders": "./src/eclipse/finders.cjs",
56
+ "./eclipse/sun-planetary-completion": "./src/eclipse/sun-planetary-completion.cjs",
55
57
  "./reference/published-curves": "./src/reference/published-curves.cjs",
56
58
  "./sun/longitude-correction": "./src/sun/longitude-correction.cjs"
57
59
  },
@@ -57,6 +57,8 @@
57
57
  * @property {(dDaysTT: number) => {lonDeg: number, latDeg: number, distKm: number}} moonFullAtDaysTT
58
58
  * full-series Moon: ecliptic-of-date longitude/latitude (deg) + distance (km), TT axis (days since J2000)
59
59
  * @property {(jdUT: number) => number} sunLonDegAt - geometric mean sun longitude (deg), JD(UT) axis with the finder's internal ΔT
60
+ * @property {(T: number) => number} sunCompletionDeg - planetary completion (deg) at T centuries TT, SUBTRACTED from the finder sun (see eclipse/sun-planetary-completion.cjs; the finders deliberately stay without it — their fitted anchors and certified canon statistics were produced on the bare form, and elongation-class timing absorbs the omission into the fitted phases)
61
+ * @property {(T: number) => {dLonDeg: number, dLatDeg: number}} moonExtensionAt - derived series-extension tail (deg) at T centuries TT, ADDED to the full-series Moon longitude/latitude (see moon/series-extension.cjs; same finder-scoping rationale as sunCompletionDeg — the finders stay on the bare series)
60
62
  * @property {(jd: number) => number} deltaTSecondsAt - framework ΔT (J2000-zeroed convention)
61
63
  * @property {(year: number) => number} obliquityDegAt - framework obliquity (deg) at calendar year
62
64
  * @property {(year: number) => number} eccentricityAt - framework Earth-orbit eccentricity at calendar year
@@ -122,32 +124,44 @@ function createBesselian(deps) {
122
124
  const jb = jdUT + K.ttBridgeSeconds / 86400; // finder-axis input → its internal ΔT lands on true TT
123
125
  const year = deps.yearFromJD(jb);
124
126
  const eps = deps.obliquityDegAt(year) * D2R;
125
- const sunLon = deps.sunLonDegAt(jb);
126
127
  const dTT = (jb - K.j2000JD) + deps.deltaTSecondsAt(jb) / 86400;
128
+ const sunLon = deps.sunLonDegAt(jb) - deps.sunCompletionDeg(dTT / K.julianCenturyDays);
127
129
  const moon = deps.moonFullAtDaysTT(dTT);
130
+ const ext = deps.moonExtensionAt(dTT / K.julianCenturyDays);
128
131
  return {
129
132
  S: eclToEq(sunLon, 0, sunDistanceKm(year, sunLon), eps),
130
- M: eclToEq(moon.lonDeg, moon.latDeg, moon.distKm, eps),
133
+ M: eclToEq(moon.lonDeg + ext.dLonDeg, moon.latDeg + ext.dLatDeg, moon.distKm, eps),
131
134
  };
132
135
  }
133
136
 
134
137
  /** Umbra-axis ground intersection at JD(UT): geodetic latitude, east
135
138
  * longitude — or null when the axis misses the ellipsoid.
139
+ * EXACT axis ∩ ellipsoid: scaling z by 1/(1−f) turns the WGS84 quadric
140
+ * into a sphere of radius R_E. The earlier R_E-SPHERE intersection put
141
+ * the hit up to R_E·f·sin²φ ≈ 21 km above the true surface, and the
142
+ * offset projects into the shadow-plane metric as ~height·cos(alt) —
143
+ * a latitude/sun-altitude-correlated phantom error (measured: 2021
144
+ * Antarctica 12.8″ → 6.9″, 2008 Arctic 7.3″ → 1.6″, 2026 Iberia
145
+ * low-sun 4.4″ → 1.2″; NASA path tables are ellipsoid-based). The
146
+ * geodetic latitude is then exact for the on-surface point:
147
+ * tan φ = z / ((1−f)²·ρ).
136
148
  * @param {number} jdUT @returns {{latDeg: number, lonDeg: number} | null} */
137
149
  function umbraGroundAt(jdUT) {
138
150
  const { S, M } = bodiesAt(jdUT);
139
151
  const dv = [M[0] - S[0], M[1] - S[1], M[2] - S[2]];
140
152
  const dl = Math.hypot(dv[0], dv[1], dv[2]);
141
153
  const d = [dv[0] / dl, dv[1] / dl, dv[2] / dl];
142
- const MdotD = M[0] * d[0] + M[1] * d[1] + M[2] * d[2];
143
- const MdotM = M[0] * M[0] + M[1] * M[1] + M[2] * M[2];
144
- const disc = MdotD * MdotD - (MdotM - R_E_KM * R_E_KM);
154
+ const w = 1 / (1 - F);
155
+ const Ms = [M[0], M[1], M[2] * w], ds = [d[0], d[1], d[2] * w];
156
+ const A = ds[0] * ds[0] + ds[1] * ds[1] + ds[2] * ds[2];
157
+ const B = Ms[0] * ds[0] + Ms[1] * ds[1] + Ms[2] * ds[2];
158
+ const Cq = Ms[0] * Ms[0] + Ms[1] * Ms[1] + Ms[2] * Ms[2] - R_E_KM * R_E_KM;
159
+ const disc = B * B - A * Cq;
145
160
  if (disc < 0) return null;
146
- const s = -MdotD - Math.sqrt(disc);
161
+ const s = (-B - Math.sqrt(disc)) / A;
147
162
  const hit = [M[0] + s * d[0], M[1] + s * d[1], M[2] + s * d[2]];
148
- const r = Math.hypot(hit[0], hit[1], hit[2]);
149
- const latGc = Math.asin(Math.max(-1, Math.min(1, hit[2] / r)));
150
- const latDeg = Math.atan(Math.tan(latGc) / ((1 - F) * (1 - F))) / D2R;
163
+ const rho = Math.hypot(hit[0], hit[1]);
164
+ const latDeg = Math.atan2(hit[2], (1 - F) * (1 - F) * rho) / D2R;
151
165
  let lonDeg = Math.atan2(hit[1], hit[0]) / D2R - gmstDeg(jdUT);
152
166
  lonDeg = ((lonDeg + 540) % 360) - 180;
153
167
  return { latDeg, lonDeg };
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Sun planetary completion v2 — the DERIVED table (Stage D2, plan §12i
3
+ * item 10; supersedes the v1 fitted 10-term table).
4
+ *
5
+ * DERIVATION, not fit: the table is extracted from the framework's OWN
6
+ * physics — twin epoch-phased 8-body RK4 integrations (Sun, planets, EMB)
7
+ * built entirely from framework constants, planet phases taken from the
8
+ * engine scene graph, differenced full-vs-base3 to isolate the planetary
9
+ * signal, with the EMB secular-perihelion channel projected out (that
10
+ * channel belongs to the framework's own ϖ(t)/e(t) laws). The 68 terms
11
+ * below are the analytic reading of that derived signal: main synodic
12
+ * tones plus eccentricity-modulation sidebands (main ± mean anomaly of
13
+ * the modulating planet, main ± M_E). Table-vs-signal fidelity 0.64″.
14
+ * JPL Horizons enters only as VALIDATION, never as fit target: the
15
+ * derived signal matches JPL residuals at corr −0.997 / slope −0.999
16
+ * (0.60″ combined), and shipping it takes the all-phase Sun residual
17
+ * 3.48″ → 2.12″, the 179-eclipse syzygy fleet 5.51″ → 3.99″, and the
18
+ * NASA centerline reference set 5.30″ → 3.94″ shadow-plane (jointly
19
+ * with the derived Moon series extension, moon/series-extension.cjs —
20
+ * the pre-registered acceptance was all four gates, never a subset).
21
+ * Instruments: tools/explore/d2-derived-sun.mjs (signal),
22
+ * d2-sun-table-extraction.mjs (table), d2-joint-preview.mjs /
23
+ * d2-widened-centerlines.mjs (validation).
24
+ *
25
+ * DECLARED INPUTS (the three non-framework residues):
26
+ * (i) planet/Sun mass ratios — observed IAU constants (shared with the
27
+ * whole framework; nothing here fits them);
28
+ * (ii) argument polynomials — IAU-class instrument literals, phase
29
+ * carriers only; their framework-native derivation is research
30
+ * item 20.3e;
31
+ * (iii) the 2lE term (+1.42″ sin 2lE) — DECLARED FITTED, not derived: a
32
+ * finder-annual artifact of the current SUN_HARMONICS chain, to be
33
+ * absorbed at the next Step-0 refit. Everything else in this file
34
+ * is derived. The Earth-around-EMB wobble, fitted −6.64″ in v1, is
35
+ * now DERIVED: amplitude a_M·μ/AU with μ = 1/(1+M_E/M_M), injected
36
+ * by the factory below from live package constants (6.4399″ at the
37
+ * current constants), sign negative in the subtract convention.
38
+ *
39
+ * THE CONSTANT-ATTRIBUTION RULE (v1's 20.3h-lite trap, still honored):
40
+ * the table carries NO constant term — the constant is attributed to the
41
+ * tier's existing anchors, so the terms ship without the +6″ syzygy-mean
42
+ * penalty an eclipse-sample fit produced.
43
+ *
44
+ * Sign convention: unchanged from v1 — the evaluation models
45
+ * (framework − truth), so consumers SUBTRACT it from the finder Sun
46
+ * longitude. TERMS literals are stored in the extraction's native sign
47
+ * (d2-sun-table.local.json) and negated in the evaluator, exactly as the
48
+ * validated joint-preview instrument composes them.
49
+ *
50
+ * MATCHED PAIR: the amplitudes are the residual of the CURRENT finder
51
+ * Sun chain (framework rate + EoC + SUN_HARMONICS) and must be
52
+ * RE-DERIVED whenever that chain moves — a Step-0 SUN_HARMONICS refit,
53
+ * an eccentricity/perihelion definition change, or 20.3e. Re-derivation
54
+ * is the D2 instrument chain above (regenerate d2-sun-table.local.json,
55
+ * re-embed). ENFORCED: PAIRED_SUN_HARMONICS_SHA256 below is the
56
+ * fingerprint of the SUN_LONGITUDE_HARMONICS this table was derived
57
+ * under; the create-model parity gate (test:model, in `npm run check`)
58
+ * recomputes it from live constants and fails on mismatch. The api
59
+ * centerline gate (≤8″ shadow-plane) backstops gross staleness
60
+ * independently.
61
+ */
62
+
63
+ 'use strict';
64
+
65
+ /** Of-date mean-longitude polynomials, degrees per Julian century TT
66
+ * (IAU-class instrument literals — phase carriers, see header (ii)).
67
+ * Body order everywhere below: Mercury, Venus, Earth (EMB), Mars,
68
+ * Jupiter, Saturn. */
69
+ const ARG_L0 = [252.250906, 181.979801, 100.466457, 355.433000, 34.351519, 50.077444];
70
+ const ARG_L1 = [149472.674, 58517.815676, 36000.769780, 19141.696300, 3036.302389, 1223.511013];
71
+ /** J2000 perihelion longitudes (deg), same body order — mean anomaly
72
+ * M_X = l_X − ϖ_X; slow ϖ drift is absorbed by the sidebands over the
73
+ * valid window. */
74
+ const PERI = [77.456, 131.564, 102.937, 336.060, 14.331, 93.057];
75
+ /** Moon mean elongation (deg/cy TT) — the EMB-wobble carrier. */
76
+ const ARG_D = [297.8501921, 445267.1114034];
77
+
78
+ /**
79
+ * The derived table, extraction-native sign (negated in the evaluator):
80
+ * [[6 mean-longitude multipliers lMe,lV,lE,lM,lJ,lS],
81
+ * [6 mean-anomaly multipliers MMe,MV,ME,MMa,MJ,MS], cos″, sin″].
82
+ * 68 terms ≥ 0.05″ from the D2 extraction (79-term LSQ, fidelity 0.64″).
83
+ * @type {Array<[number[], number[], number, number]>}
84
+ */
85
+ const TERMS = [
86
+ [[0, 0, 1, 0, -1, 0], [0, 0, 0, 0, 0, 0], -0.0841, -11.6309],
87
+ [[0, 0, 2, 0, -2, 0], [0, 0, -1, 0, 0, 0], -0.3235, 8.2645],
88
+ [[0, 0, 1, -1, 0, 0], [0, 0, 0, -1, 0, 0], -2.9917, -6.8503],
89
+ [[0, 0, 2, -2, 0, 0], [0, 0, -1, 0, 0, 0], -6.2197, -2.1203],
90
+ [[0, 2, -2, 0, 0, 0], [0, 0, 0, 0, 0, 0], 0.0199, -6.4326],
91
+ [[0, 0, 1, 0, -2, 0], [0, 0, 1, 0, 0, 0], 1.7254, 4.5991],
92
+ [[0, 1, -1, 0, 0, 0], [0, 0, 0, 0, 0, 0], 0.1362, 4.8325],
93
+ [[0, 0, 1, 0, -2, 0], [0, 0, 0, 0, 1, 0], 1.1469, 4.2788],
94
+ [[0, 0, 1, 0, -2, 0], [0, 0, 0, 0, 0, 0], -3.8890, 1.2230],
95
+ [[0, 0, 2, 0, -2, 0], [0, 0, 0, 0, 0, 0], 3.3414, 1.5744],
96
+ [[0, 0, 1, 0, -1, 0], [0, 0, 0, 0, -1, 0], -2.6985, -1.2967],
97
+ [[0, 3, -4, 0, 0, 0], [0, 0, 0, 0, 0, 0], 2.9374, -0.5408],
98
+ [[0, 0, 1, 0, -1, 0], [0, 0, -1, 0, 0, 0], -2.5406, -0.1812],
99
+ [[0, 0, 2, -2, 0, 0], [0, 0, 0, 0, 0, 0], 1.3755, -2.0301],
100
+ [[0, 0, 2, -3, 0, 0], [0, 0, 0, 1, 0, 0], -2.2367, -0.4398],
101
+ [[0, 0, -1, 2, 0, 0], [0, 0, 0, 0, 0, 0], 1.8705, 0.3499],
102
+ [[0, 3, -3, 0, 0, 0], [0, -1, 0, 0, 0, 0], -1.3492, 1.2893],
103
+ [[0, 0, 2, 0, -3, 0], [0, 0, 0, 0, 1, 0], 1.5453, 0.1289],
104
+ [[0, 2, -3, 0, 0, 0], [0, 0, 0, 0, 0, 0], 1.3344, -0.4621],
105
+ [[0, 3, -3, 0, 0, 0], [0, 0, -1, 0, 0, 0], -0.0755, -1.4049],
106
+ [[0, 0, -2, 4, 0, 0], [0, 0, 0, 0, 0, 0], 0.2780, 1.2111],
107
+ [[0, 0, -1, 2, 0, 0], [0, 0, -1, 0, 0, 0], -0.7161, 0.8440],
108
+ [[0, 3, -4, 0, 0, 0], [0, 0, -1, 0, 0, 0], 0.9894, 0.3587],
109
+ [[0, 0, 2, 0, -3, 0], [0, 0, 0, 0, 0, 0], 0.0063, 1.0463],
110
+ [[0, 2, -3, 0, 0, 0], [0, 0, 1, 0, 0, 0], 0.9834, 0.1318],
111
+ [[0, 0, 2, -3, 0, 0], [0, 0, 0, -1, 0, 0], 0.4834, 0.8318],
112
+ [[0, 1, -1, 0, 0, 0], [0, -1, 0, 0, 0, 0], -0.5335, 0.5905],
113
+ [[0, 0, 2, -2, 0, 0], [0, 0, 0, -1, 0, 0], 0.5371, -0.5685],
114
+ [[0, 0, 2, -3, 0, 0], [0, 0, 0, 0, 0, 0], -0.7373, -0.0880],
115
+ [[0, 2, -2, 0, 0, 0], [0, 0, -1, 0, 0, 0], 0.0163, -0.7372],
116
+ [[0, 0, -1, 2, 0, 0], [0, 0, 0, -1, 0, 0], -0.0119, 0.6500],
117
+ [[0, 0, 2, 0, -2, 0], [0, 0, 0, 0, 1, 0], 0.0023, -0.6287],
118
+ [[0, 0, 1, 0, -1, 0], [0, 0, 1, 0, 0, 0], 0.5714, -0.1358],
119
+ [[0, 0, -2, 4, 0, 0], [0, 0, -1, 0, 0, 0], -0.5046, 0.2858],
120
+ [[0, 0, 1, 0, -1, 0], [0, 0, 0, 0, 1, 0], -0.5567, 0.1036],
121
+ [[0, 2, -3, 0, 0, 0], [0, 0, -1, 0, 0, 0], 0.5469, -0.0167],
122
+ [[0, 2, -3, 0, 0, 0], [0, 1, 0, 0, 0, 0], -0.4309, 0.2809],
123
+ [[0, 0, 2, 0, -2, 0], [0, 0, 0, 0, -1, 0], 0.2206, -0.4616],
124
+ [[0, 3, -4, 0, 0, 0], [0, -1, 0, 0, 0, 0], -0.4549, -0.2097],
125
+ [[0, 0, 2, 0, -3, 0], [0, 0, -1, 0, 0, 0], 0.2809, 0.3489],
126
+ [[0, 0, 1, -1, 0, 0], [0, 0, 0, 0, 0, 0], 0.2863, 0.3116],
127
+ [[0, 0, 2, -3, 0, 0], [0, 0, -1, 0, 0, 0], -0.4141, -0.0359],
128
+ [[0, 0, 1, 0, 0, -1], [0, 0, 0, 0, 0, 0], 0.0863, -0.4015],
129
+ [[0, 0, -1, 2, 0, 0], [0, 0, 0, 1, 0, 0], -0.2200, 0.3372],
130
+ [[0, 1, -1, 0, 0, 0], [0, 0, -1, 0, 0, 0], -0.2017, 0.2910],
131
+ [[0, 2, -2, 0, 0, 0], [0, -1, 0, 0, 0, 0], 0.2499, -0.1777],
132
+ [[0, 0, 1, 0, -2, 0], [0, 0, 0, 0, -1, 0], -0.2254, 0.1676],
133
+ [[0, 3, -3, 0, 0, 0], [0, 0, 0, 0, 0, 0], -0.1859, -0.1961],
134
+ [[0, 0, 1, 0, 0, -1], [0, 0, 0, 0, 0, 1], -0.0710, 0.2499],
135
+ [[0, 0, -2, 4, 0, 0], [0, 0, 0, -1, 0, 0], 0.2125, -0.1175],
136
+ [[0, 0, 1, 0, 0, -1], [0, 0, -1, 0, 0, 0], -0.0460, 0.2379],
137
+ [[0, 3, -4, 0, 0, 0], [0, 1, 0, 0, 0, 0], -0.1410, 0.1566],
138
+ [[0, 0, 1, -1, 0, 0], [0, 0, 0, 1, 0, 0], -0.0476, 0.1897],
139
+ [[0, 2, -2, 0, 0, 0], [0, 0, 1, 0, 0, 0], -0.1559, -0.1155],
140
+ [[0, 0, 2, 0, -3, 0], [0, 0, 1, 0, 0, 0], 0.1631, -0.0164],
141
+ [[0, 1, -1, 0, 0, 0], [0, 1, 0, 0, 0, 0], 0.1307, -0.0678],
142
+ [[0, 0, 2, -3, 0, 0], [0, 0, 1, 0, 0, 0], 0.1331, -0.0291],
143
+ [[0, 0, -2, 4, 0, 0], [0, 0, 1, 0, 0, 0], 0.1227, 0.0482],
144
+ [[0, 0, 2, 0, 0, -2], [0, 0, 0, 0, 0, 0], -0.0388, 0.1044],
145
+ [[0, 0, 1, 0, 0, -1], [0, 0, 0, 0, 0, -1], -0.0309, 0.1027],
146
+ [[0, 0, 1, 0, -2, 0], [0, 0, -1, 0, 0, 0], -0.0741, 0.0308],
147
+ [[0, 0, 2, 0, -3, 0], [0, 0, 0, 0, -1, 0], 0.0102, 0.0783],
148
+ [[0, 1, -1, 0, 0, 0], [0, 0, 1, 0, 0, 0], -0.0026, 0.0765],
149
+ [[0, 0, 2, 0, -2, 0], [0, 0, 1, 0, 0, 0], -0.0084, 0.0686],
150
+ [[0, 3, -4, 0, 0, 0], [0, 0, 1, 0, 0, 0], 0.0273, -0.0576],
151
+ [[0, 0, 1, -1, 0, 0], [0, 0, 1, 0, 0, 0], 0.0570, 0.0117],
152
+ [[0, 0, 2, -2, 0, 0], [0, 0, 0, 1, 0, 0], 0.0426, -0.0336],
153
+ [[0, 0, 1, -1, 0, 0], [0, 0, -1, 0, 0, 0], -0.0335, -0.0395],
154
+ ];
155
+
156
+ /** The declared-fitted semiannual leftover (header (iii)): +1.42″ sin 2lE. */
157
+ const FITTED_2LE_ARCSEC = 1.42;
158
+
159
+ const D2R = Math.PI / 180;
160
+
161
+ /**
162
+ * @param {{ embWobbleArcsec: number }} opts - the DERIVED Earth-around-EMB
163
+ * wobble amplitude a_M·μ/AU in arcsec (μ = 1/(1+M_E/M_M)); computed
164
+ * from live constants by the model wiring so it tracks the constants.
165
+ * @returns {{ sunPlanetaryCompletionDeg: (T: number) => number }}
166
+ */
167
+ function createSunPlanetaryCompletion({ embWobbleArcsec }) {
168
+ if (!Number.isFinite(embWobbleArcsec)) {
169
+ throw new Error('createSunPlanetaryCompletion: embWobbleArcsec must be a finite number (derived a_M·μ/AU in arcsec)');
170
+ }
171
+ /**
172
+ * Planetary-completion correction to the finder Sun longitude.
173
+ * @param {number} T - Julian centuries TT from J2000
174
+ * @returns {number} degrees — SUBTRACT from the finder Sun longitude
175
+ */
176
+ function sunPlanetaryCompletionDeg(T) {
177
+ const l = new Float64Array(6), M = new Float64Array(6);
178
+ for (let i = 0; i < 6; i++) {
179
+ l[i] = (ARG_L0[i] + ARG_L1[i] * T) * D2R;
180
+ M[i] = l[i] - PERI[i] * D2R;
181
+ }
182
+ let table = 0;
183
+ for (const [kl, kM, cA, sA] of TERMS) {
184
+ let th = 0;
185
+ for (let i = 0; i < 6; i++) th += kl[i] * l[i] + kM[i] * M[i];
186
+ table += cA * Math.cos(th) + sA * Math.sin(th);
187
+ }
188
+ const D = (ARG_D[0] + ARG_D[1] * T) * D2R;
189
+ const arcsec = -table - embWobbleArcsec * Math.sin(D)
190
+ + FITTED_2LE_ARCSEC * Math.sin(2 * l[2]);
191
+ return arcsec / 3600;
192
+ }
193
+ return { sunPlanetaryCompletionDeg };
194
+ }
195
+
196
+ /** sha256/16 of JSON.stringify(FITTED_COEFFICIENTS.SUN_LONGITUDE_HARMONICS)
197
+ * at derivation time — the matched-pair fingerprint asserted by test:model.
198
+ * Unchanged from v1: SUN_HARMONICS did not move in the D2 landing. */
199
+ const PAIRED_SUN_HARMONICS_SHA256 = 'e2cf42e9770c9e0a';
200
+
201
+ module.exports = { createSunPlanetaryCompletion, PAIRED_SUN_HARMONICS_SHA256 };
package/src/model.js CHANGED
@@ -31,6 +31,8 @@ import { createMoonMonthChain } from './moon/month-chain.cjs';
31
31
  import { createChainCycleIntegrator } from './chain-cycles/index.cjs';
32
32
  import { createMoonArguments, jdToDecimalYear } from './moon/arguments.cjs';
33
33
  import { createMoonSeries } from './moon/series.cjs';
34
+ import { createSunPlanetaryCompletion } from './eclipse/sun-planetary-completion.cjs';
35
+ import { moonSeriesExtensionDeg } from './moon/series-extension.cjs';
34
36
  import { createEclipseFinders } from './eclipse/finders.cjs';
35
37
  import { createBesselian } from './eclipse/besselian.cjs';
36
38
  import { driver2PeriodSecondsAtAge } from './planets/orbit-chain.cjs';
@@ -855,12 +857,19 @@ export function assembleModel(C, F) {
855
857
  // curve, the same convention every other ΔT consumer reads). Both axis
856
858
  // conventions are measured against the NASA path-table centerlines —
857
859
  // see eclipse/besselian.cjs.
860
+ // Derived Earth-around-EMB wobble for the Sun completion: a_M·μ/AU,
861
+ // μ = 1/(1+M_E/M_M) — 6.4399″ at current constants, tracks them live.
862
+ const embWobbleArcsec = (moonDistanceKm / (MASS_RATIO_EARTH_MOON + 1) / currentAUDistance)
863
+ * (648000 / Math.PI);
864
+ const { sunPlanetaryCompletionDeg } = createSunPlanetaryCompletion({ embWobbleArcsec });
858
865
  const besselian = createBesselian({
859
866
  moonFullAtDaysTT: /** @param {number} dDaysTT */ (dDaysTT) => {
860
867
  const ev = moonSeries.sceneEvalAt(dDaysTT);
861
868
  return { lonDeg: ev.lonDeg, latDeg: ev.latDeg, distKm: ev.distKm };
862
869
  },
863
870
  sunLonDegAt: /** @param {number} jdUT */ (jdUT) => eclipseFinders.sunLonDegAt(jdUT),
871
+ sunCompletionDeg: sunPlanetaryCompletionDeg,
872
+ moonExtensionAt: moonSeriesExtensionDeg,
864
873
  deltaTSecondsAt: /** @param {number} jd */ (jd) => (jdTTFromUT(jd) - jd) * 86400,
865
874
  obliquityDegAt: obliquityDeg,
866
875
  eccentricityAt,
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Moon series extension — the DERIVED Delaunay tail beyond the shipped
3
+ * Meeus-60 head (Stage D2 Phase A, plan §12i item 10).
4
+ *
5
+ * DERIVATION, not fit: the amplitudes come from the Stage-D1 3-body lab
6
+ * (tools/explore/derive-meeus-amplitudes.js — RK4 on framework constants,
7
+ * IC calibration to the free elements), joint-LSQ over [Meeus-60 head +
8
+ * extended Delaunay catalog] on a 120-yr window. Head fidelity
9
+ * 100.00–100.03% (the derivation reproduces the shipped head, so the
10
+ * tail is the same physics one order deeper); dt-halving drift 0.000″.
11
+ * Extraction: tools/explore/d2-extension-extraction.mjs.
12
+ *
13
+ * JPL enters only as OUT-OF-SAMPLE VALIDATION (960 all-phase epochs):
14
+ * shipped-Moon residual λ 3.68″ → 3.12″ (predicted 3.08″) and
15
+ * β 1.04″ → 0.65″ (predicted 0.66″) with the extension ADDED; the
16
+ * sign-flipped control degrades both — the integrator predicted reality.
17
+ * Ships JOINTLY with the derived Sun completion v2
18
+ * (eclipse/sun-planetary-completion.cjs): the Moon-only form degrades
19
+ * the NASA centerline reference set (its tail was cancelling Sun-side
20
+ * leftover tones there), and the pre-registered acceptance was all four
21
+ * gates, never a subset.
22
+ *
23
+ * Arguments are the instrument-grade Delaunay polynomials (IAU/Meeus
24
+ * class, degrees/Julian-century TT) — phase carriers only, same
25
+ * epistemic status as the Sun completion's argument literals (their
26
+ * framework-native derivation is research item 20.3e).
27
+ *
28
+ * Sign convention: sine terms, ADDED to the full-series Moon longitude
29
+ * and latitude (the validated sense; see besselian.cjs bodiesAt).
30
+ *
31
+ * THE A2 PLANETARY TAIL (the second derived section below): the Moon's
32
+ * direct planetary perturbations, derived from epoch-phased twin 8-body
33
+ * integrations (full = planets+J2 vs base = J2-only; planet phases from
34
+ * the engine graph, the Moon at its true epoch Delaunay phases; fitted
35
+ * on the run's own arguments with a main-problem absorber catalog,
36
+ * absorbers discarded — instrument
37
+ * tools/explore/d2-planetary-moon-epoch.mjs). Cross-validated: V−E
38
+ * +0.849″ sin (independent JPL fit: +0.85), E−J −0.681″ (JPL: −0.69);
39
+ * content 0.896″. JPL out-of-sample (960 all-phase epochs): λ scatter
40
+ * 3.12″ → 3.03″ (the extraction predicted 2.98); the sign-flipped
41
+ * control degrades to 3.47″; the syzygy fleet improves 3.99″ → 3.82″
42
+ * in EVERY era. The β rows (0.074″ content) measurably do NOT help and
43
+ * are not shipped. RECORDED TENSION (owner-accepted at shipping): the
44
+ * 13-event NASA centerline scoreboard moves 2.22″ → 2.68″ mean — the
45
+ * A1-class correlated-subsample effect (the tracked events sit −1.6″
46
+ * below the +1.4″ fleet mean and sample the near-annual arguments at
47
+ * correlated phases; the era-split refutes anchor double-counting and
48
+ * the modern era improves most, 3.57 → 3.34). The ≤8″ api gate holds
49
+ * (max 6.2″). Coefficients are stored with the run→real argument
50
+ * mapping already applied (the extraction's heliocentric-planet vs
51
+ * geocentric-sun π-offset is folded into the signs).
52
+ */
53
+
54
+ 'use strict';
55
+
56
+ /** Delaunay argument polynomials [deg at J2000, deg/cy, deg/cy²] TT. */
57
+ const DELAUNAY = {
58
+ D: [297.8501921, 445267.1114034, -0.0018819], // mean elongation
59
+ M: [357.5291092, 35999.0502909, -0.0001536], // Sun mean anomaly
60
+ Mp: [134.9633964, 477198.8675055, 0.0087414], // Moon mean anomaly
61
+ F: [93.2720950, 483202.0175233, -0.0036539], // argument of latitude
62
+ };
63
+
64
+ /** Longitude tail: [kD, kM, kMp, kF, sin″] — 47 derived terms ≥ 0.1″
65
+ * (RMS content 2.02″). */
66
+ const LON_TERMS = [
67
+ [2, 0, 1, 2, -0.9900],
68
+ [2, 0, -4, 0, 0.9465],
69
+ [2, -2, 1, 0, 0.7524],
70
+ [0, 1, -3, 0, -0.6704],
71
+ [4, 1, -1, 0, -0.6323],
72
+ [1, 0, 2, 0, -0.5864],
73
+ [1, 0, 0, -2, -0.5813],
74
+ [2, 0, -2, -2, -0.5631],
75
+ [1, -1, 0, 0, -0.5617],
76
+ [0, 1, 3, 0, -0.5455],
77
+ [2, 0, -2, 2, -0.5253],
78
+ [2, -1, -3, 0, 0.4674],
79
+ [2, 0, 2, -2, -0.4625],
80
+ [2, -1, -1, 2, -0.4271],
81
+ [0, 1, 0, 2, 0.4173],
82
+ [6, 0, -1, 0, 0.3954],
83
+ [2, -1, 1, -2, -0.3876],
84
+ [2, -1, 0, 2, -0.3816],
85
+ [2, 2, -1, -2, -0.3654],
86
+ [2, 2, 1, -2, -0.3615],
87
+ [4, 1, -2, 0, -0.3575],
88
+ [0, 0, 3, 2, -0.3281],
89
+ [4, -2, -1, 0, 0.3106],
90
+ [0, 1, -1, -2, 0.3016],
91
+ [2, -2, -2, 0, 0.2964],
92
+ [4, 0, -1, -2, 0.2963],
93
+ [2, 1, 2, 0, -0.2897],
94
+ [4, 1, 0, 0, -0.2887],
95
+ [4, -1, 1, 0, 0.2833],
96
+ [0, 1, 1, 2, 0.2643],
97
+ [2, 2, -2, 0, -0.2635],
98
+ [1, 1, -2, 0, 0.2569],
99
+ [1, 0, 0, 2, 0.2524],
100
+ [4, 0, 2, 0, 0.2190],
101
+ [4, 0, -1, 2, -0.2007],
102
+ [0, 2, -2, 0, -0.1813],
103
+ [0, 0, 3, -2, -0.1784],
104
+ [2, 2, 0, 0, -0.1684],
105
+ [2, 0, -1, -2, 0.1634],
106
+ [4, -2, 0, 0, 0.1549],
107
+ [2, 2, -3, 0, 0.1545],
108
+ [1, -1, -1, 0, -0.1479],
109
+ [2, 1, -3, 0, 0.1290],
110
+ [6, 0, 0, 0, 0.1268],
111
+ [1, 0, -3, 0, -0.1252],
112
+ [2, 0, 2, 2, -0.1236],
113
+ [1, -1, 1, 0, -0.1225],
114
+ ];
115
+
116
+ /** Latitude tail: [kD, kM, kMp, kF, sin″] — 30 derived terms ≥ 0.1″
117
+ * (RMS content 0.80″). */
118
+ const LAT_TERMS = [
119
+ [4, -1, -1, 1, 0.3390],
120
+ [2, 0, -1, -3, 0.3282],
121
+ [2, -2, -1, 1, 0.3148],
122
+ [0, 1, 2, -1, -0.3050],
123
+ [0, 1, -2, 1, -0.2988],
124
+ [2, 0, 1, -3, -0.2919],
125
+ [2, -2, -1, -1, 0.2685],
126
+ [0, 0, 4, 1, 0.2636],
127
+ [2, 0, -3, 1, 0.2526],
128
+ [2, 0, -1, 3, -0.2447],
129
+ [2, 1, 1, 1, -0.2369],
130
+ [4, 0, 1, 1, 0.2128],
131
+ [2, 2, 0, -1, -0.1749],
132
+ [4, 1, -1, -1, -0.1737],
133
+ [4, -1, 0, 1, 0.1581],
134
+ [2, 0, 3, -1, 0.1465],
135
+ [0, 0, 2, -3, -0.1461],
136
+ [2, 0, 0, 3, -0.1444],
137
+ [1, 0, -1, 1, 0.1387],
138
+ [2, 0, 3, 1, 0.1381],
139
+ [2, 0, -4, -1, 0.1335],
140
+ [2, -1, 2, -1, 0.1283],
141
+ [2, -1, 2, 1, 0.1241],
142
+ [0, 2, -1, -1, -0.1208],
143
+ [0, 0, 2, 3, -0.1178],
144
+ [4, 1, 0, -1, -0.1128],
145
+ [2, 2, -1, -1, -0.1127],
146
+ [1, 0, -2, -1, -0.1093],
147
+ [2, 2, -1, 1, -0.1044],
148
+ [1, 1, 1, 1, 0.1014],
149
+ ];
150
+
151
+ /** Planetary mean-longitude polynomials, degrees per Julian century TT
152
+ * (IAU-class instrument literals — same carriers as the Sun completion).
153
+ * Order: Venus, Earth (EMB), Mars, Jupiter, Saturn. */
154
+ const PLANET_L0 = [181.979801, 100.466457, 355.433000, 34.351519, 50.077444];
155
+ const PLANET_L1 = [58517.815676, 36000.769780, 19141.696300, 3036.302389, 1223.511013];
156
+
157
+ /**
158
+ * The A2 planetary longitude tail:
159
+ * [[kV, kE, kMa, kJ, kSa, kD, kMp], cos″, sin″] — 15 derived terms
160
+ * (content 0.896″), run→real argument mapping folded into the signs.
161
+ * @type {Array<[number[], number, number]>}
162
+ */
163
+ const PLANETARY_LON_TERMS = [
164
+ [[1, -1, 0, 0, 0, 0, 0], -0.0056, -0.8488],
165
+ [[2, -2, 0, 0, 0, 0, 0], -0.0034, 0.3166],
166
+ [[3, -3, 0, 0, 0, 0, 0], -0.0051, -0.0387],
167
+ [[0, 1, 0, -1, 0, 0, 0], 0.0325, 0.6814],
168
+ [[0, 2, 0, -2, 0, 0, 0], -0.0084, -0.1835],
169
+ [[0, 2, -2, 0, 0, 0, 0], 0.0002, 0.2205],
170
+ [[0, 1, 0, 0, -1, 0, 0], 0.0078, 0.0463],
171
+ [[1, -1, 0, 0, 0, 2, 0], -0.0020, -0.0308],
172
+ [[1, -1, 0, 0, 0, -2, 0], 0.0045, -0.1193],
173
+ [[0, 1, 0, -1, 0, 2, 0], 0.0014, 0.0322],
174
+ [[0, 1, 0, -1, 0, -2, 0], 0.0161, 0.2141],
175
+ [[1, -1, 0, 0, 0, 0, 1], 0.0023, -0.1929],
176
+ [[1, -1, 0, 0, 0, 0, -1], -0.0029, -0.2306],
177
+ [[0, 1, 0, -1, 0, 0, 1], 0.0059, 0.1729],
178
+ [[0, 1, 0, -1, 0, 0, -1], -0.0077, 0.2235],
179
+ ];
180
+
181
+ const D2R = Math.PI / 180;
182
+
183
+ /**
184
+ * Derived series-extension correction to the full-series Moon:
185
+ * the Delaunay tail plus the A2 planetary tail (longitude only).
186
+ * @param {number} T - Julian centuries TT from J2000
187
+ * @returns {{dLonDeg: number, dLatDeg: number}} degrees — ADD to the
188
+ * full-series Moon longitude/latitude
189
+ */
190
+ function moonSeriesExtensionDeg(T) {
191
+ const poly = /** @param {number[]} p */ (p) => (p[0] + (p[1] + p[2] * T) * T) * D2R;
192
+ const D = poly(DELAUNAY.D), M = poly(DELAUNAY.M), Mp = poly(DELAUNAY.Mp), F = poly(DELAUNAY.F);
193
+ let lon = 0, lat = 0;
194
+ for (const [kD, kM, kMp, kF, s] of LON_TERMS) lon += s * Math.sin(kD * D + kM * M + kMp * Mp + kF * F);
195
+ for (const [kD, kM, kMp, kF, s] of LAT_TERMS) lat += s * Math.sin(kD * D + kM * M + kMp * Mp + kF * F);
196
+ const l = new Float64Array(5);
197
+ for (let i = 0; i < 5; i++) l[i] = (PLANET_L0[i] + PLANET_L1[i] * T) * D2R;
198
+ for (const [k, cA, sA] of PLANETARY_LON_TERMS) {
199
+ const th = k[0] * l[0] + k[1] * l[1] + k[2] * l[2] + k[3] * l[3] + k[4] * l[4] + k[5] * D + k[6] * Mp;
200
+ lon += cA * Math.cos(th) + sA * Math.sin(th);
201
+ }
202
+ return { dLonDeg: lon / 3600, dLatDeg: lat / 3600 };
203
+ }
204
+
205
+ module.exports = { moonSeriesExtensionDeg };