@essrt/physics 4.4.1 → 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.1",
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",
@@ -131,16 +131,29 @@ function createDeepOrbitalHistory({
131
131
  // engine's own ζ series (anchored at J2000 exactly like the mode sums);
132
132
  // outside it, the anchored mode sum is the tail. Without zetaSeries the
133
133
  // mode sum serves at every t — the pre-C-2 behavior, bit-identical.
134
- let zetaAt = zetaModeSum;
135
- if (zetaSeries) {
136
- const { t0Yr, stepYr, q: sq, p: sp } = zetaSeries;
137
- const nS = sq.length, tEndYr = t0Yr + (nS - 1) * stepYr;
138
- 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) => {
139
142
  const x = (tt - t0Yr) / stepYr;
140
143
  const i = Math.max(0, Math.min(nS - 2, Math.floor(x)));
141
144
  const f = x - i;
142
- 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;
143
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);
144
157
  const R = [zetaAnchor[0] - li(sq, 0), zetaAnchor[1] - li(sp, 0)];
145
158
  zetaAt = (/** @type {number} */ t) => (t >= t0Yr && t <= tEndYr)
146
159
  ? [li(sq, t) + R[0], li(sp, t) + R[1]]
@@ -155,12 +168,7 @@ function createDeepOrbitalHistory({
155
168
  if (zSeries) {
156
169
  const { t0Yr, stepYr, q: sq, p: sp } = zSeries;
157
170
  const nS = sq.length, tEndYr = t0Yr + (nS - 1) * stepYr;
158
- const li = (/** @type {ReadonlyArray<number>} */ arr, /** @type {number} */ tt) => {
159
- const x = (tt - t0Yr) / stepYr;
160
- const i = Math.max(0, Math.min(nS - 2, Math.floor(x)));
161
- const f = x - i;
162
- return arr[i] * (1 - f) + arr[i + 1] * f;
163
- };
171
+ const li = mkSeriesCubic(t0Yr, stepYr, nS); // C1 (see mkSeriesCubic)
164
172
  const R = [zAnchor[0] - li(sq, 0), zAnchor[1] - li(sp, 0)];
165
173
  zAt = (/** @type {number} */ t) => (t >= t0Yr && t <= tEndYr)
166
174
  ? [li(sq, t) + R[0], li(sp, t) + R[1]]
@@ -189,7 +197,7 @@ function createDeepOrbitalHistory({
189
197
  // constants: ψ̇(t) comes from the same certified year-length machinery
190
198
  // as the J2000 anchor. Absent the option, α stays constant (the
191
199
  // pre-D1 ±Myr-class behavior, bit-identical).
192
- const alphaAt = axialPrecessionYearsAtYearFn
200
+ const alphaAtGeneral = axialPrecessionYearsAtYearFn
193
201
  ? (/** @type {number} */ t) =>
194
202
  ((2 * Math.PI) / axialPrecessionYearsAtYearFn(2000 + t)) / Math.cos(EPS0)
195
203
  : () => ALPHA;
@@ -197,6 +205,71 @@ function createDeepOrbitalHistory({
197
205
  const cross = (/** @type {number[]} */ a, /** @type {number[]} */ b) =>
198
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]];
199
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;
200
273
  const deriv = (/** @type {number[]} */ s, /** @type {number} */ t) => {
201
274
  const n = orbitNormal(t);
202
275
  const k = alphaAt(t) * dot(s, n);
@@ -213,6 +286,14 @@ function createDeepOrbitalHistory({
213
286
  return [o[0] / r, o[1] / r, o[2] / r];
214
287
  };
215
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
+
216
297
  /** One quantity bundle at time t (years from J2000) from spin axis s. */
217
298
  function sampleAt(/** @type {number[]} */ s, /** @type {number} */ t) {
218
299
  const n = orbitNormal(t);
@@ -238,6 +319,26 @@ function createDeepOrbitalHistory({
238
319
  eSinPeri: e * Math.sin(periOfDateDeg * D2R),
239
320
  eCosPeri: e * Math.cos(periOfDateDeg * D2R),
240
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)
241
342
  };
242
343
  }
243
344
 
@@ -256,7 +357,7 @@ function createDeepOrbitalHistory({
256
357
  throw new Error(`deep-orbital-history: builds beyond ±50 kyr need stepYr % 250 === 0 (got ${stepYr})`);
257
358
  }
258
359
  const grid = new Map();
259
- const S0 = [0, Math.sin(EPS0), Math.cos(EPS0)];
360
+ const S0 = S0A;
260
361
  // D1-revised adaptive stepping: 5-yr RK4 near the era (the certified-
261
362
  // precision zone), 250-yr beyond ±50 kyr — still 103 steps per
262
363
  // precession cycle and ≥196 samples of the fastest ζ mode (49 kyr), so
@@ -269,7 +370,23 @@ function createDeepOrbitalHistory({
269
370
  const hStepAt = (/** @type {number} */ t) => (Math.abs(t) < 50000 ? 5 : 250);
270
371
  for (const dir of [-1, +1]) {
271
372
  let s = S0, t = 0;
272
- 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);
273
390
  const end = dir < 0 ? tMin : tMax;
274
391
  while (dir < 0 ? t > end : t < end) {
275
392
  const H_STEP = hStepAt(t);
@@ -283,7 +400,7 @@ function createDeepOrbitalHistory({
283
400
  // 100-yr keys, 50 yr off). Exact alignment is guaranteed by the
284
401
  // guard below (coarse builds use 250-aligned grids; the 50,000-yr
285
402
  // zone boundary is itself 250-aligned, so the walk stays on-grid).
286
- 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);
287
404
  }
288
405
  }
289
406
  return {
@@ -302,12 +419,33 @@ function createDeepOrbitalHistory({
302
419
  eSinPeri: lerp(a.eSinPeri, b.eSinPeri),
303
420
  eCosPeri: lerp(a.eCosPeri, b.eCosPeri),
304
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),
305
437
  };
306
438
  },
307
439
  };
308
440
  }
309
441
 
310
- 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
+ };
311
449
  }
312
450
 
313
451
  module.exports = { createDeepOrbitalHistory };