@essrt/physics 24.0.0 → 24.1.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,9 +1,9 @@
1
1
  {
2
2
  "name": "@essrt/physics",
3
- "version": "24.0.0",
3
+ "version": "24.1.0",
4
4
  "description": "ESSRT (Expanding Solar System Resonance Theory) physics core — 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
- "modelVersion": "v26.6",
6
+ "modelVersion": "v26.7",
7
7
  "_note": "The model identity this package version ships (IP-unified-architecture §10: package semver tracks the API axis; the model version + CONSTANTS_HASH/COEFFICIENTS_HASH in src/constants/ carry the scientific identity). The version-pinning gate enforces this pairing against public/input/model-version.json."
8
8
  },
9
9
  "type": "module",
@@ -26,11 +26,18 @@
26
26
  * tableAnchorYear vs driftRefYear pair).
27
27
  *
28
28
  * NUMERICS ARE LOAD-BEARING: fixed 10-yr grid over ±250 kyr, per-cell
29
- * 3-point Simpson at build, LINEAR interpolation on read; adaptive Simpson
30
- * (~1 sample/kyr, n ∈ [32, 1024], even) as the out-of-range fallback. The
31
- * table replaced the per-call Simpson for in-range spans (the L-4
32
- * 92 s/century full-canon regression); an "analytically better" integrator
33
- * here would detach the runtime from the certified numbers.
29
+ * 3-point Simpson at build, LINEAR interpolation on read; beyond the dense
30
+ * grid a COARSE cumulative table (1-kyr cells, the same per-cell Simpson,
31
+ * anchored at the dense edge) grown lazily in 250-kyr chunks in the direction
32
+ * asked for — the owner's playback at 1,000 yr/s past year 250,000 had fallen
33
+ * to 190 ms of JS per frame: the former fallback, an adaptive Simpson of up to
34
+ * 1,024 samples per call cached by exact year pair, re-integrated every one
35
+ * of the four Moon chains from the grid edge on every frame, each sample a
36
+ * deep-time LOD/α evaluation. The adaptive Simpson remains only for a chain
37
+ * undefined somewhere in range (null) or beyond the coarse limit (±600 Myr,
38
+ * past the model's domain). An "analytically better" integrator here would
39
+ * detach the runtime from the certified numbers: the in-range path is
40
+ * byte-identical to before.
34
41
  */
35
42
 
36
43
  'use strict';
@@ -125,6 +132,62 @@ function createChainCycleIntegrator({
125
132
  return tab[i] + (idxF - i) * (tab[i + 1] - tab[i]);
126
133
  }
127
134
 
135
+ // ── The coarse extension beyond the dense grid ─────────────────────────────
136
+ // Per chain and per direction, a cumulative table in COARSE_STEP cells that
137
+ // starts at the dense edge with the edge's own value and grows by COARSE_CHUNK
138
+ // years whenever a year beyond its reach is asked for. The cells are built
139
+ // with the same 3-point Simpson as the dense grid; the read is the same
140
+ // linear interpolation. The period laws vary on Myr scales at deep time, so
141
+ // 1-kyr cells hold the count to far below a cycle — the old fallback sampled
142
+ // ~1 per kyr too, but re-did the whole span on every call.
143
+ const COARSE_STEP = 1000, COARSE_CHUNK = 250000, COARSE_LIMIT_YEARS = 600e6;
144
+ /** @typedef {{ cum: number[], ok: boolean }} CoarseSide */
145
+ /** @type {WeakMap<PeriodSecondsAtAge, { fwd: CoarseSide, bwd: CoarseSide }>} */
146
+ const coarse = new WeakMap();
147
+ /** @param {PeriodSecondsAtAge} periodFnSeconds @param {number} y @returns {number | null} cycles per tick */
148
+ const integrandAt = (periodFnSeconds, y) => {
149
+ const tPeriodS = periodFnSeconds((ageAnchorYear - y) / 1e6);
150
+ return tPeriodS === null ? null : tropicalYearJ2000Seconds / tPeriodS;
151
+ };
152
+ /** Grow one side until it covers `distance` years from the dense edge (or fails on a null chain).
153
+ * @param {PeriodSecondsAtAge} periodFnSeconds @param {CoarseSide} side @param {number} edgeYear
154
+ * @param {1|-1} dir @param {number} distance */
155
+ function growCoarse(periodFnSeconds, side, edgeYear, dir, distance) {
156
+ const need = Math.ceil(distance / COARSE_STEP) + 1;
157
+ let target = side.cum.length;
158
+ while (target < need) target += COARSE_CHUNK / COARSE_STEP;
159
+ for (let i = side.cum.length; i < target && side.ok; i++) {
160
+ const y0 = edgeYear + dir * (i - 1) * COARSE_STEP, y1 = y0 + dir * COARSE_STEP;
161
+ const f0 = integrandAt(periodFnSeconds, y0), fm = integrandAt(periodFnSeconds, (y0 + y1) / 2), f1 = integrandAt(periodFnSeconds, y1);
162
+ if (f0 === null || fm === null || f1 === null) { side.ok = false; break; }
163
+ side.cum.push(side.cum[i - 1] + dir * (f0 + 4 * fm + f1) * (COARSE_STEP / 6));
164
+ }
165
+ }
166
+ /** C(y) beyond the dense grid, or null when the chain is undefined on the way there.
167
+ * @param {PeriodSecondsAtAge} periodFnSeconds @param {Float64Array} tab @param {number} y @returns {number | null} */
168
+ function coarseAt(periodFnSeconds, tab, y) {
169
+ let c = coarse.get(periodFnSeconds);
170
+ if (!c) {
171
+ c = { fwd: { cum: [tab[tab.length - 1]], ok: true }, bwd: { cum: [tab[0]], ok: true } };
172
+ coarse.set(periodFnSeconds, c);
173
+ }
174
+ const dir = y >= gridMaxYear ? 1 : -1;
175
+ const side = dir === 1 ? c.fwd : c.bwd;
176
+ const edgeYear = dir === 1 ? gridMaxYear : gridMinYear;
177
+ const distance = Math.abs(y - edgeYear);
178
+ if (distance > COARSE_LIMIT_YEARS) return null;
179
+ if (side.cum.length < Math.ceil(distance / COARSE_STEP) + 1) growCoarse(periodFnSeconds, side, edgeYear, dir, distance);
180
+ if (!side.ok || side.cum.length < Math.ceil(distance / COARSE_STEP) + 1) return null;
181
+ const idxF = distance / COARSE_STEP;
182
+ const i = Math.floor(idxF);
183
+ return side.cum[i] + (idxF - i) * (side.cum[i + 1] - side.cum[i]);
184
+ }
185
+ /** C(y) anywhere the tables reach: dense inside the grid, coarse beyond.
186
+ * @param {PeriodSecondsAtAge} periodFnSeconds @param {Float64Array} tab @param {number} y @returns {number | null} */
187
+ function cumulativeAt(periodFnSeconds, tab, y) {
188
+ return (y > gridMinYear && y < gridMaxYear) ? tableAt(tab, y) : coarseAt(periodFnSeconds, tab, y);
189
+ }
190
+
128
191
  /** Cycles of the chain between two calendar years. Snapshot mode: linear
129
192
  * at the J2000 rate. Deep time: table lookup in ±range, adaptive Simpson
130
193
  * beyond. null past the tidal-lock asymptote.
@@ -147,10 +210,16 @@ function createChainCycleIntegrator({
147
210
  return dy * tropicalYearJ2000Seconds / tJ2000;
148
211
  }
149
212
 
150
- // Cumulative-table fast path (deterministic, call-order independent)
151
- if (yearA > gridMinYear && yearA < gridMaxYear && yearB > gridMinYear && yearB < gridMaxYear) {
152
- const tab = table(periodFnSeconds);
153
- if (tab !== null) return tableAt(tab, yearB) - tableAt(tab, yearA);
213
+ // Cumulative-table fast path (deterministic, call-order independent): the
214
+ // dense grid inside ±250 kyr — byte-identical to before — and the lazily
215
+ // grown coarse extension beyond it.
216
+ const tab = table(periodFnSeconds);
217
+ if (tab !== null) {
218
+ if (yearA > gridMinYear && yearA < gridMaxYear && yearB > gridMinYear && yearB < gridMaxYear) {
219
+ return tableAt(tab, yearB) - tableAt(tab, yearA);
220
+ }
221
+ const cA = cumulativeAt(periodFnSeconds, tab, yearA), cB = cumulativeAt(periodFnSeconds, tab, yearB);
222
+ if (cA !== null && cB !== null) return cB - cA;
154
223
  }
155
224
 
156
225
  let cache = caches.get(periodFnSeconds);
@@ -162,7 +231,8 @@ function createChainCycleIntegrator({
162
231
  const hit = cache.get(cacheKey);
163
232
  if (hit !== undefined) return hit;
164
233
 
165
- // Adaptive Simpson: ~1 sample per 1 kyr, capped at 1024 to bound per-call cost.
234
+ // Adaptive Simpson (a chain undefined in range, or beyond the coarse limit):
235
+ // ~1 sample per 1 kyr, capped at 1024 to bound per-call cost.
166
236
  let n = Math.max(32, Math.ceil(Math.abs(dy) / 1000));
167
237
  if (n > 1024) n = 1024;
168
238
  if (n % 2 === 1) n++;
@@ -39,7 +39,7 @@
39
39
  export const CONSTANTS_HASH = "da713fc2d846767d";
40
40
 
41
41
  /** Model version label — single source: public/input/model-version.json (§10 two-axis scheme). */
42
- export const MODEL_VERSION = "v26.6";
42
+ export const MODEL_VERSION = "v26.7";
43
43
 
44
44
  /** Canonical preprint DOI — single source: public/input/model-version.json. */
45
45
  export const PREPRINT_DOI = "10.21203/rs.3.rs-8758810/v4";
@@ -145,8 +145,46 @@ function createMoonArguments({ constants, fns }) {
145
145
  }
146
146
  return kPl;
147
147
  }
148
+ // ── Deep-time carrier tables ────────────────────────────────────────────────
149
+ // In the era (|T| ≤ CARRIER_DENSE_CY = 250 kyr) each carrier is the Simpson
150
+ // sum below, untouched — the certified numbers. Beyond, the integral
151
+ // continues from the dense edge on a cumulative table of CARRIER_CELL_CY
152
+ // cells (1 kyr; per-cell 3-point Simpson, a 3-point Simpson over the
153
+ // partial last cell), grown in CARRIER_CHUNK cells in the direction asked
154
+ // for. The former form re-integrated the whole span from J2000 on EVERY
155
+ // call with N ∝ |T| nodes — 77 evaluations of the e channel at 300 kyr,
156
+ // 2,500 at 10 Myr, per lunar argument set and frame — the deep-time
157
+ // playback cost the owner measured (the loop's JS at 190 ms a frame).
158
+ const CARRIER_DENSE_CY = 2500, CARRIER_CELL_CY = 10, CARRIER_CHUNK = 2500;
159
+ /** @param {(t: number) => number} f the SCALED integrand per century
160
+ * @param {(T: number) => number} dense the in-era Simpson (its value at ±CARRIER_DENSE_CY seeds the table)
161
+ * @returns {(T: number) => number} */
162
+ function makeDeepCarrier(f, dense) {
163
+ /** @type {{ fwd: number[] | null, bwd: number[] | null }} */
164
+ const sides = { fwd: null, bwd: null };
165
+ return (T) => {
166
+ if (Math.abs(T) <= CARRIER_DENSE_CY) return dense(T);
167
+ const dir = T > 0 ? 1 : -1;
168
+ let cum = dir === 1 ? sides.fwd : sides.bwd;
169
+ if (!cum) { cum = [dense(dir * CARRIER_DENSE_CY)]; if (dir === 1) sides.fwd = cum; else sides.bwd = cum; }
170
+ const dist = Math.abs(T) - CARRIER_DENSE_CY;
171
+ const k = Math.floor(dist / CARRIER_CELL_CY);
172
+ let target = cum.length;
173
+ while (target < k + 2) target += CARRIER_CHUNK;
174
+ for (let i = cum.length; i < target; i++) {
175
+ const t0 = dir * (CARRIER_DENSE_CY + (i - 1) * CARRIER_CELL_CY), t1 = t0 + dir * CARRIER_CELL_CY;
176
+ cum.push(cum[i - 1] + dir * (f(t0) + 4 * f((t0 + t1) / 2) + f(t1)) * (CARRIER_CELL_CY / 6));
177
+ }
178
+ const ta = dir * (CARRIER_DENSE_CY + k * CARRIER_CELL_CY);
179
+ const hp = T - ta;
180
+ return cum[k] + (f(ta) + 4 * f(ta + hp / 2) + f(T)) * (hp / 6);
181
+ };
182
+ }
183
+
184
+ /** The planetary carrier's SCALED integrand, k_PL · (e² − e0²), per century. @param {number} t */
185
+ const planetaryIntegrand = (t) => { const e = eccAt(t * 100); return kPlValue() * (e * e - eccE0 * eccE0); };
148
186
  /** @param {number} T @returns {number} */
149
- function planetaryCarrier(T) {
187
+ function planetaryCarrierDense(T) {
150
188
  if (T === 0) return 0;
151
189
  const k = kPlValue();
152
190
  // The channel's e0 anchor CONST — not eccAt(0), which under integrated
@@ -160,12 +198,12 @@ function createMoonArguments({ constants, fns }) {
160
198
  for (let i = 1; i < N; i++) sum += f(i * h) * (i % 2 ? 4 : 2);
161
199
  return k * sum * h / 3;
162
200
  }
201
+ const planetaryCarrier = makeDeepCarrier(planetaryIntegrand, planetaryCarrierDense);
163
202
 
164
203
  /** @type {{eps0: number, C: number} | null} */
165
204
  let obl = null;
166
- /** @param {number} T @returns {number} */
167
- function obliquityCarrier(T) {
168
- if (T === 0) return 0;
205
+ /** The obliquity carrier's normalization, once. @returns {{eps0: number, C: number}} */
206
+ function oblInit() {
169
207
  if (obl === null) {
170
208
  // (d′): when the of-date rate completion is live, its ∫(p_dyn − p_kin)
171
209
  // carries the model-native share of the precession ACCELERATION
@@ -179,14 +217,23 @@ function createMoonArguments({ constants, fns }) {
179
217
  const epsDot = computeObliquityEarth(2050) - computeObliquityEarth(1950);
180
218
  obl = { eps0, C: 2 * T2_OBL / epsDot };
181
219
  }
220
+ return obl;
221
+ }
222
+ /** The obliquity carrier's SCALED integrand, C · (ε − ε0), per century. @param {number} t */
223
+ const obliquityIntegrand = (t) => { const O = oblInit(); return O.C * (computeObliquityEarth(2000 + t * 100) - O.eps0); };
224
+ /** @param {number} T @returns {number} */
225
+ function obliquityCarrierDense(T) {
226
+ if (T === 0) return 0;
227
+ const O = oblInit();
182
228
  /** @param {number} t */
183
- const f = (t) => computeObliquityEarth(2000 + t * 100) - /** @type {{eps0: number, C: number}} */ (obl).eps0;
229
+ const f = (t) => computeObliquityEarth(2000 + t * 100) - O.eps0;
184
230
  const N = Math.max(2, 2 * Math.ceil(Math.abs(T) * 100 / 2000));
185
231
  const h = T / N;
186
232
  let sum = f(0) + f(T);
187
233
  for (let i = 1; i < N; i++) sum += f(i * h) * (i % 2 ? 4 : 2);
188
- return /** @type {{eps0: number, C: number}} */ (obl).C * sum * h / 3;
234
+ return O.C * sum * h / 3;
189
235
  }
236
+ const obliquityCarrier = makeDeepCarrier(obliquityIntegrand, obliquityCarrierDense);
190
237
 
191
238
  // ── Secular-ë completion carrier (ṅ campaign, plan §12i item 0 (c)) ──────
192
239
  // The certified snapshot's Lp T³/T⁴ tail is DERIVED physics (v4 D3,
@@ -234,6 +281,10 @@ function createMoonArguments({ constants, fns }) {
234
281
  const env = Math.cos(Math.PI / 2 * q) ** 2;
235
282
  return (0.5 * S.dd2 * t * t + S.d3Sec * t * t * t / 6) * env;
236
283
  };
284
+ // Beyond the era the envelope has long closed (|t| ≥ Tq ≈ 279 cy → 0): the
285
+ // integral is the constant to ±Tq, so integrate exactly that span instead of
286
+ // N ∝ |T| nodes of zeros (in-era calls untouched).
287
+ if (Math.abs(T) > CARRIER_DENSE_CY) T = Math.sign(T) * S.Tq;
237
288
  const N = Math.max(2, 2 * Math.ceil(Math.abs(T) * 100 / 2000));
238
289
  const h = T / N;
239
290
  let sum = f(0) + f(T);
@@ -374,7 +425,18 @@ function createMoonArguments({ constants, fns }) {
374
425
  * (the anchors are measured against exactly this).
375
426
  * @param {number} jd
376
427
  * @returns {{Lp: number, w: number, om: number, Lsun: number, ws: number, dy: number} | null} */
428
+ /** @type {number} */ let _partsJd = NaN;
429
+ /** @type {ReturnType<typeof fwArgsDeepPartsAt>} */ let _partsLast = null;
430
+ /** The parts at a JD — memoized on the exact JD: a frame asks the same instant for
431
+ * the Moon, the umbra disc and the element rows. @param {number} jd */
377
432
  function fwArgsDeepParts(jd) {
433
+ if (jd === _partsJd && _partsLast !== null) return _partsLast;
434
+ const P = fwArgsDeepPartsAt(jd);
435
+ _partsJd = jd; _partsLast = P;
436
+ return P;
437
+ }
438
+ /** @param {number} jd */
439
+ function fwArgsDeepPartsAt(jd) {
378
440
  const A = bundle;
379
441
  if (argsY0 === null) argsY0 = jdToSIyear(j2000JD);
380
442
  const y = jdToSIyear(jd);