@essrt/physics 4.4.0 → 4.5.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": "4.4.0",
3
+ "version": "4.5.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": "v13.0",
@@ -83,6 +83,46 @@ function createDeepOrbitalHistory({
83
83
  const s0 = sum(0), R = [anchor[0] - s0[0], anchor[1] - s0[1]];
84
84
  return (/** @type {number} */ t) => { const [x, y] = sum(t); return [x + R[0], y + R[1]]; };
85
85
  };
86
+ // CO-ROTATING anchor (the ϖ̇ fix, measured): the DC residual above is a
87
+ // spurious zero-frequency mode — it preserves the J2000 VALUE but dilutes
88
+ // the local argument rate by |sum(0)|/|anchor| (z: 11.47 → 7.93″/yr, a
89
+ // 92 s-class anomalistic-year bias in the mode tier). Here the residual
90
+ // ROTATES at the raw sum's own local arg-rate g* = Im(ż·z̄)/|z|²
91
+ // (analytic), so at t=0 both the value AND the rate are exact, and at
92
+ // depth the residual is one |R|-amplitude term at a physical frequency
93
+ // instead of a DC offset (deep envelope measured unchanged:
94
+ // [0.0001, 0.0672] → [0.0003, 0.0678] over 10–50 Myr).
95
+ // z ONLY: ζ keeps the DC anchor DELIBERATELY — its anchor is ~0 by
96
+ // definition (Earth's inclination to the J2000 ecliptic at J2000), i.e.
97
+ // the coordinate origin; a rotating residual there would inject a fake
98
+ // wobble into n̂(t).
99
+ const mkAnchoredCoRot = (
100
+ /** @type {ReadonlyArray<{omegaRadPerYr: number, re: number, im: number}>} */ modes,
101
+ /** @type {[number, number]} */ anchor,
102
+ ) => {
103
+ const sum = (/** @type {number} */ t) => {
104
+ let re = 0, im = 0;
105
+ for (const m of modes) {
106
+ const c = Math.cos(m.omegaRadPerYr * t), s = Math.sin(m.omegaRadPerYr * t);
107
+ re += m.re * c - m.im * s;
108
+ im += m.re * s + m.im * c;
109
+ }
110
+ return [re, im];
111
+ };
112
+ let q0 = 0, p0 = 0, dq0 = 0, dp0 = 0;
113
+ for (const m of modes) {
114
+ q0 += m.re; p0 += m.im;
115
+ dq0 += -m.omegaRadPerYr * m.im;
116
+ dp0 += m.omegaRadPerYr * m.re;
117
+ }
118
+ const gStar = (dp0 * q0 - dq0 * p0) / (q0 * q0 + p0 * p0);
119
+ const R = [anchor[0] - q0, anchor[1] - p0];
120
+ return (/** @type {number} */ t) => {
121
+ const [x, y] = sum(t);
122
+ const c = Math.cos(gStar * t), s = Math.sin(gStar * t);
123
+ return [x + R[0] * c - R[1] * s, y + R[0] * s + R[1] * c];
124
+ };
125
+ };
86
126
  const s2h = Math.sin(anchorInclEclipticDeg / 2 * D2R);
87
127
  const zetaAnchor = /** @type {[number, number]} */ (
88
128
  [s2h * Math.cos(anchorAscNodeEclipticDeg * D2R), s2h * Math.sin(anchorAscNodeEclipticDeg * D2R)]);
@@ -91,16 +131,29 @@ function createDeepOrbitalHistory({
91
131
  // engine's own ζ series (anchored at J2000 exactly like the mode sums);
92
132
  // outside it, the anchored mode sum is the tail. Without zetaSeries the
93
133
  // mode sum serves at every t — the pre-C-2 behavior, bit-identical.
94
- let zetaAt = zetaModeSum;
95
- if (zetaSeries) {
96
- const { t0Yr, stepYr, q: sq, p: sp } = zetaSeries;
97
- const nS = sq.length, tEndYr = t0Yr + (nS - 1) * stepYr;
98
- const li = (/** @type {ReadonlyArray<number>} */ arr, /** @type {number} */ tt) => {
134
+ // C1 series interpolation (D4g, measured): the 500-yr LINEAR series lerp
135
+ // put kinks in n̂(t)/z(t) at its nodes, and the equinox RATE inherited
136
+ // them the grid-dependent bump around J2000 in per-year P(t) (the same
137
+ // interpolation-order disease as the spin grid, one level down). Cubic
138
+ // Hermite with central-difference node slopes (one-sided at the ends);
139
+ // exact at nodes, so the J2000 anchors are unchanged.
140
+ const mkSeriesCubic = (/** @type {number} */ t0Yr, /** @type {number} */ stepYr, /** @type {number} */ nS) =>
141
+ (/** @type {ReadonlyArray<number>} */ arr, /** @type {number} */ tt) => {
99
142
  const x = (tt - t0Yr) / stepYr;
100
143
  const i = Math.max(0, Math.min(nS - 2, Math.floor(x)));
101
144
  const f = x - i;
102
- return arr[i] * (1 - f) + arr[i + 1] * f;
145
+ const y0 = arr[i], y1 = arr[i + 1];
146
+ const m0 = i > 0 ? (y1 - arr[i - 1]) / 2 : y1 - y0;
147
+ const m1 = i + 2 < nS ? (arr[i + 2] - y0) / 2 : y1 - y0;
148
+ const f2 = f * f, f3 = f2 * f;
149
+ return (2 * f3 - 3 * f2 + 1) * y0 + (f3 - 2 * f2 + f) * m0
150
+ + (-2 * f3 + 3 * f2) * y1 + (f3 - f2) * m1;
103
151
  };
152
+ let zetaAt = zetaModeSum;
153
+ if (zetaSeries) {
154
+ const { t0Yr, stepYr, q: sq, p: sp } = zetaSeries;
155
+ const nS = sq.length, tEndYr = t0Yr + (nS - 1) * stepYr;
156
+ const li = mkSeriesCubic(t0Yr, stepYr, nS);
104
157
  const R = [zetaAnchor[0] - li(sq, 0), zetaAnchor[1] - li(sp, 0)];
105
158
  zetaAt = (/** @type {number} */ t) => (t >= t0Yr && t <= tEndYr)
106
159
  ? [li(sq, t) + R[0], li(sp, t) + R[1]]
@@ -108,19 +161,14 @@ function createDeepOrbitalHistory({
108
161
  }
109
162
  const zAnchor = /** @type {[number, number]} */ (
110
163
  [anchorE * Math.cos(anchorPeriEclipticDeg * D2R), anchorE * Math.sin(anchorPeriEclipticDeg * D2R)]);
111
- const zModeSum = mkAnchored(zModes, zAnchor);
164
+ const zModeSum = mkAnchoredCoRot(zModes, zAnchor); // co-rotating residual — the ϖ̇ fix (see mkAnchoredCoRot)
112
165
  // C-4a: the z-side one-source path — same construction as zetaSeries
113
166
  // (anchored engine series inside the span, anchored mode sum as the tail).
114
167
  let zAt = zModeSum;
115
168
  if (zSeries) {
116
169
  const { t0Yr, stepYr, q: sq, p: sp } = zSeries;
117
170
  const nS = sq.length, tEndYr = t0Yr + (nS - 1) * stepYr;
118
- const li = (/** @type {ReadonlyArray<number>} */ arr, /** @type {number} */ tt) => {
119
- const x = (tt - t0Yr) / stepYr;
120
- const i = Math.max(0, Math.min(nS - 2, Math.floor(x)));
121
- const f = x - i;
122
- return arr[i] * (1 - f) + arr[i + 1] * f;
123
- };
171
+ const li = mkSeriesCubic(t0Yr, stepYr, nS); // C1 (see mkSeriesCubic)
124
172
  const R = [zAnchor[0] - li(sq, 0), zAnchor[1] - li(sp, 0)];
125
173
  zAt = (/** @type {number} */ t) => (t >= t0Yr && t <= tEndYr)
126
174
  ? [li(sq, t) + R[0], li(sp, t) + R[1]]
@@ -149,7 +197,7 @@ function createDeepOrbitalHistory({
149
197
  // constants: ψ̇(t) comes from the same certified year-length machinery
150
198
  // as the J2000 anchor. Absent the option, α stays constant (the
151
199
  // pre-D1 ±Myr-class behavior, bit-identical).
152
- const alphaAt = axialPrecessionYearsAtYearFn
200
+ const alphaAtGeneral = axialPrecessionYearsAtYearFn
153
201
  ? (/** @type {number} */ t) =>
154
202
  ((2 * Math.PI) / axialPrecessionYearsAtYearFn(2000 + t)) / Math.cos(EPS0)
155
203
  : () => ALPHA;
@@ -157,6 +205,71 @@ function createDeepOrbitalHistory({
157
205
  const cross = (/** @type {number[]} */ a, /** @type {number[]} */ b) =>
158
206
  [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
159
207
  const dot = (/** @type {number[]} */ a, /** @type {number[]} */ b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
208
+
209
+ // THE LUNISOLAR SELF-ANCHOR (D4e — measured, plan 02): the injected
210
+ // anchor axialPrecessionYearsJ2000 = sid/(sid−sol) is the GENERAL
211
+ // precession period, but ŝ must precess at the LUNISOLAR rate — the
212
+ // n̂(t) geometry supplies the planetary part itself, so integrating at
213
+ // the general rate DOUBLE-COUNTS the planetary mean (measured:
214
+ // −0.0966″/yr of equinox rate → +2.4 s of tropical year at J2000, and
215
+ // 0.19% of ε wobble phasing — the dominant term of the old 0.039° rms
216
+ // vs La2004 over −200 kyr; the lunisolar anchor collapses it to
217
+ // 0.0048°). Self-anchored at construction, no pasted numbers: a one-RK4
218
+ // -step probe at the general rate measures the realized equinox rate at
219
+ // J2000; the deficit rescales α so the REALIZED general precession hits
220
+ // the injected anchor exactly (dp/dα = 1 to first order — one
221
+ // fixed-point step lands on target, measured). H(t) shape preserved
222
+ // (constant factor on the caller's evaluator).
223
+
224
+ // THE ANCHORED SPIN AXIS (measured): ε₀ is defined against the ECLIPTIC
225
+ // — Earth's own mean orbit plane — never against the chain artifact's
226
+ // coordinate pole. The anchor elements carry i₀ ≈ 0.37″ of integrator-
227
+ // frame residue, and tilting ŝ(0) from the coordinate pole projected
228
+ // −0.2865″ of it straight into ε(J2000) (the C-1 era gate's offset,
229
+ // masked until the C1 series interpolation removed the lerp kink at the
230
+ // J2000 node). Build ŝ(0) FROM n̂(0): tilt ε₀ about the J2000 equinox
231
+ // direction projected into the orbit plane. Frame-invariant (the
232
+ // artifact's coordinate frame drops out) and ε(J2000) ≡ ε₀ by
233
+ // construction; reduces to [0, sin ε₀, cos ε₀] exactly when n̂(0) = ẑ.
234
+ const S0A = (() => {
235
+ const n0 = orbitNormal(0);
236
+ const px = [1 - n0[0] * n0[0], -n0[0] * n0[1], -n0[0] * n0[2]];
237
+ const pm = Math.hypot(px[0], px[1], px[2]);
238
+ const u = [px[0] / pm, px[1] / pm, px[2] / pm];
239
+ const v = cross(n0, u);
240
+ return [0, 1, 2].map((i) => Math.cos(EPS0) * n0[i] + Math.sin(EPS0) * v[i]);
241
+ })();
242
+
243
+ const K_LUNI = (() => {
244
+ const S0P = S0A;
245
+ const dv = (/** @type {number[]} */ s, /** @type {number} */ t) => {
246
+ const n = orbitNormal(t);
247
+ const k = alphaAtGeneral(t) * dot(s, n);
248
+ const c = cross(s, n);
249
+ return [k * c[0], k * c[1], k * c[2]];
250
+ };
251
+ const step = (/** @type {number[]} */ s, /** @type {number} */ t, /** @type {number} */ h) => {
252
+ const k1 = dv(s, t);
253
+ const k2 = dv([s[0] + h / 2 * k1[0], s[1] + h / 2 * k1[1], s[2] + h / 2 * k1[2]], t + h / 2);
254
+ const k3 = dv([s[0] + h / 2 * k2[0], s[1] + h / 2 * k2[1], s[2] + h / 2 * k2[2]], t + h / 2);
255
+ const k4 = dv([s[0] + h * k3[0], s[1] + h * k3[1], s[2] + h * k3[2]], t + h);
256
+ const o = [0, 1, 2].map((i) => s[i] + h / 6 * (k1[i] + 2 * k2[i] + 2 * k3[i] + k4[i]));
257
+ const r = Math.hypot(o[0], o[1], o[2]);
258
+ return [o[0] / r, o[1] / r, o[2] / r];
259
+ };
260
+ const eqLonDeg = (/** @type {number[]} */ s, /** @type {number} */ t) => {
261
+ const n = orbitNormal(t);
262
+ const g = cross(s, n);
263
+ return Math.atan2(g[1], g[0]) * R2D;
264
+ };
265
+ const lM = eqLonDeg(step(S0P, 0, -0.5), -0.5);
266
+ const lP = eqLonDeg(step(S0P, 0, +0.5), +0.5);
267
+ const pProbeDegYr = ((lM - lP + 540) % 360) - 180; // retrograde → positive
268
+ const pTargetDegYr = 360 / axialPrecessionYearsJ2000;
269
+ const psiDot0 = alphaAtGeneral(0) * Math.cos(EPS0);
270
+ return (psiDot0 + (pTargetDegYr - pProbeDegYr) * D2R) / psiDot0;
271
+ })();
272
+ const alphaAt = (/** @type {number} */ t) => alphaAtGeneral(t) * K_LUNI;
160
273
  const deriv = (/** @type {number[]} */ s, /** @type {number} */ t) => {
161
274
  const n = orbitNormal(t);
162
275
  const k = alphaAt(t) * dot(s, n);
@@ -173,6 +286,14 @@ function createDeepOrbitalHistory({
173
286
  return [o[0] / r, o[1] / r, o[2] / r];
174
287
  };
175
288
 
289
+ /** Equinox-node longitude only (J2000 ecliptic frame) — the light read
290
+ * used for the node-rate finite difference at store time. */
291
+ function eqLonOnlyDeg(/** @type {number[]} */ s, /** @type {number} */ t) {
292
+ const n = orbitNormal(t);
293
+ const g = cross(s, n);
294
+ return Math.atan2(g[1], g[0]) * R2D;
295
+ }
296
+
176
297
  /** One quantity bundle at time t (years from J2000) from spin axis s. */
177
298
  function sampleAt(/** @type {number[]} */ s, /** @type {number} */ t) {
178
299
  const n = orbitNormal(t);
@@ -198,6 +319,26 @@ function createDeepOrbitalHistory({
198
319
  eSinPeri: e * Math.sin(periOfDateDeg * D2R),
199
320
  eCosPeri: e * Math.cos(periOfDateDeg * D2R),
200
321
  inclEclDeg: i * R2D,
322
+ // The equinox node (ŝ×n̂) longitude in the J2000 ecliptic frame —
323
+ // its year-over-year retrograde advance IS the general precession of
324
+ // date, wobble included (the n̂(t) geometry generates the equinox
325
+ // wobble; ψ̇ itself is the secular α(H(t))). Consumers derive the
326
+ // tropical year of date from it: T_trop = T_sid·(1 − p_yr/360°).
327
+ // ⚠ RATE-CONSUMER CONTRACT (measured): this field's rate is correct
328
+ // AS IS — α is self-anchored to the LUNISOLAR rate at construction
329
+ // (K_LUNI, the one-RK4-step probe above), so the realized general
330
+ // precession p_geom(J2000) equals 360/axialPrecessionYearsJ2000 by
331
+ // construction. No correction, no δ subtraction. History (why this
332
+ // contract exists): before the self-anchor, α carried the GENERAL
333
+ // sid/(sid−sol) rate and the n̂(t) geometry re-added the planetary
334
+ // mean — a double-count of +0.097″/yr ≈ +2.4 s of tropical year at
335
+ // J2000 — and consumers had to subtract the runtime anchor
336
+ // δ = p_geom(J2000) − 360/axialPrecessionYearsJ2000.
337
+ // A consumer may still measure δ at runtime as a SELF-CHECK; it
338
+ // must read ≈0 (sub-1e-4 ″/yr class). The LONGITUDE itself is raw
339
+ // geometry and has always needed no correction.
340
+ equinoxLonJ2000Deg: ((Math.atan2(gu[1], gu[0]) * R2D) % 360 + 360) % 360,
341
+ equinoxLonRateDegPerYr: 0, // filled at store time (build's ±2.5-yr central difference)
201
342
  };
202
343
  }
203
344
 
@@ -216,7 +357,7 @@ function createDeepOrbitalHistory({
216
357
  throw new Error(`deep-orbital-history: builds beyond ±50 kyr need stepYr % 250 === 0 (got ${stepYr})`);
217
358
  }
218
359
  const grid = new Map();
219
- const S0 = [0, Math.sin(EPS0), Math.cos(EPS0)];
360
+ const S0 = S0A;
220
361
  // D1-revised adaptive stepping: 5-yr RK4 near the era (the certified-
221
362
  // precision zone), 250-yr beyond ±50 kyr — still 103 steps per
222
363
  // precession cycle and ≥196 samples of the fastest ζ mode (49 kyr), so
@@ -229,7 +370,23 @@ function createDeepOrbitalHistory({
229
370
  const hStepAt = (/** @type {number} */ t) => (Math.abs(t) < 50000 ? 5 : 250);
230
371
  for (const dir of [-1, +1]) {
231
372
  let s = S0, t = 0;
232
- grid.set(0, sampleAt(s, 0));
373
+ // Each stored node also carries the equinox-node RATE (deg/yr) from a
374
+ // ±2.5-yr central difference on the integrator's own state — the C1
375
+ // (Hermite) interpolation input. Linear interpolation of the node
376
+ // longitude made the realized equinox rate PIECEWISE-CONSTANT per
377
+ // grid cell — the report's per-year precession column showed flat
378
+ // centuries with steps at the 1700/1800/1900/2000 cell edges
379
+ // (measured; the levels were the real wobble at 100-yr resolution,
380
+ // the staircase was this interpolation order).
381
+ const storeWithRate = (/** @type {number} */ key, /** @type {number[]} */ sv, /** @type {number} */ tv) => {
382
+ const smp = sampleAt(sv, tv);
383
+ const hR = 2.5;
384
+ const lp = eqLonOnlyDeg(rk4(sv, tv, +hR), tv + hR);
385
+ const lm = eqLonOnlyDeg(rk4(sv, tv, -hR), tv - hR);
386
+ smp.equinoxLonRateDegPerYr = (((lp - lm + 540) % 360) - 180) / (2 * hR);
387
+ grid.set(key, smp);
388
+ };
389
+ storeWithRate(0, s, 0);
233
390
  const end = dir < 0 ? tMin : tMax;
234
391
  while (dir < 0 ? t > end : t < end) {
235
392
  const H_STEP = hStepAt(t);
@@ -243,7 +400,7 @@ function createDeepOrbitalHistory({
243
400
  // 100-yr keys, 50 yr off). Exact alignment is guaranteed by the
244
401
  // guard below (coarse builds use 250-aligned grids; the 50,000-yr
245
402
  // zone boundary is itself 250-aligned, so the walk stays on-grid).
246
- if (Math.abs(t - Math.round(t / stepYr) * stepYr) < 1e-6) grid.set(Math.round(t / stepYr) * stepYr, sampleAt(s, t));
403
+ if (Math.abs(t - Math.round(t / stepYr) * stepYr) < 1e-6) storeWithRate(Math.round(t / stepYr) * stepYr, s, t);
247
404
  }
248
405
  }
249
406
  return {
@@ -262,12 +419,33 @@ function createDeepOrbitalHistory({
262
419
  eSinPeri: lerp(a.eSinPeri, b.eSinPeri),
263
420
  eCosPeri: lerp(a.eCosPeri, b.eCosPeri),
264
421
  inclEclDeg: lerp(a.inclEclDeg, b.inclEclDeg),
422
+ // C1 (cubic Hermite) — linear interpolation here made the
423
+ // realized equinox RATE piecewise-constant per grid cell (the
424
+ // measured per-year precession staircase). Node rates come from
425
+ // the build's own ±2.5-yr central differences.
426
+ equinoxLonJ2000Deg: (() => {
427
+ const l0 = a.equinoxLonJ2000Deg;
428
+ const dl = ((b.equinoxLonJ2000Deg - l0 + 540) % 360) - 180; // unwrapped segment
429
+ const m0 = a.equinoxLonRateDegPerYr * stepYr;
430
+ const m1 = (b.equinoxLonRateDegPerYr ?? a.equinoxLonRateDegPerYr) * stepYr;
431
+ const f2 = f * f, f3 = f2 * f;
432
+ const v = (2 * f3 - 3 * f2 + 1) * l0 + (f3 - 2 * f2 + f) * m0
433
+ + (-2 * f3 + 3 * f2) * (l0 + dl) + (f3 - f2) * m1;
434
+ return ((v % 360) + 360) % 360;
435
+ })(),
436
+ equinoxLonRateDegPerYr: lerp(a.equinoxLonRateDegPerYr, b.equinoxLonRateDegPerYr ?? a.equinoxLonRateDegPerYr),
265
437
  };
266
438
  },
267
439
  };
268
440
  }
269
441
 
270
- return { build, alphaArcsecPerYr: ALPHA * R2D * 3600 };
442
+ return {
443
+ build,
444
+ alphaArcsecPerYr: ALPHA * R2D * 3600, // the injected GENERAL-anchor form (input echo, unchanged semantics)
445
+ // D4e diagnostics: the self-anchored lunisolar rate actually integrated.
446
+ alphaLunisolarArcsecPerYr: ALPHA * K_LUNI * R2D * 3600,
447
+ axialPrecessionYearsLunisolarJ2000: axialPrecessionYearsJ2000 / K_LUNI,
448
+ };
271
449
  }
272
450
 
273
451
  module.exports = { createDeepOrbitalHistory };