@essrt/physics 26.0.0 → 26.0.1

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": "26.0.0",
3
+ "version": "26.0.1",
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
6
  "modelVersion": "v27.1",
@@ -28,7 +28,9 @@
28
28
  * NUMERICS ARE LOAD-BEARING: fixed 10-yr grid over ±250 kyr, per-cell
29
29
  * 3-point Simpson at build, LINEAR interpolation on read; beyond the dense
30
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
31
+ * anchored at the dense edge; read with a 3-point Simpson over the partial
32
+ * cell, so the count's RATE is continuous — plan 23 follow-up, see the tier
33
+ * note below) grown lazily in 250-kyr chunks in the direction
32
34
  * asked for — the owner's playback at 1,000 yr/s past year 250,000 had fallen
33
35
  * to 190 ms of JS per frame: the former fallback, an adaptive Simpson of up to
34
36
  * 1,024 samples per call cached by exact year pair, re-integrated every one
@@ -136,9 +138,19 @@ function createChainCycleIntegrator({
136
138
  // Per chain and per direction, a cumulative table in COARSE_STEP cells that
137
139
  // starts at the dense edge with the edge's own value and grows by COARSE_CHUNK
138
140
  // 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
141
+ // with the same 3-point Simpson as the dense grid; the read is a 3-point
142
+ // Simpson over the partial cell from the cell's start (three integrand
143
+ // evaluations per read), NOT the dense grid's linear interpolation: a
144
+ // linear read makes the count's rate piecewise constant per cell, and the
145
+ // of-date node and perigee are SMALL differences of two large counts
146
+ // (N_drac − N_trop: 13.4 − 13.37 cycles/yr), so each table's per-cell slope
147
+ // step — 1e-6 of its own rate, the month's tidal drift over 1 kyr — landed
148
+ // fully on the difference: a ±0.3-d sawtooth per cell on the node period
149
+ // beyond ±250 kyr (measured on the Node Regression panel, plan 23
150
+ // follow-up). On the dense 10-yr grid the same step is 100× smaller and
151
+ // below every reader, so its linear read — the byte-identical in-window
152
+ // path — stays. The period laws vary on Myr scales at deep time, so 1-kyr
153
+ // cells hold the count to far below a cycle — the old fallback sampled
142
154
  // ~1 per kyr too, but re-did the whole span on every call.
143
155
  const COARSE_STEP = 1000, COARSE_CHUNK = 250000, COARSE_LIMIT_YEARS = 600e6;
144
156
  /** @typedef {{ cum: number[], ok: boolean }} CoarseSide */
@@ -178,9 +190,13 @@ function createChainCycleIntegrator({
178
190
  if (distance > COARSE_LIMIT_YEARS) return null;
179
191
  if (side.cum.length < Math.ceil(distance / COARSE_STEP) + 1) growCoarse(periodFnSeconds, side, edgeYear, dir, distance);
180
192
  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]);
193
+ const i = Math.floor(distance / COARSE_STEP);
194
+ // the partial cell from its start y0 to y: a 3-point Simpson, signed by (y − y0)
195
+ const y0 = edgeYear + dir * i * COARSE_STEP;
196
+ if (y === y0) return side.cum[i];
197
+ const f0 = integrandAt(periodFnSeconds, y0), fm = integrandAt(periodFnSeconds, (y0 + y) / 2), f1 = integrandAt(periodFnSeconds, y);
198
+ if (f0 === null || fm === null || f1 === null) return null;
199
+ return side.cum[i] + (f0 + 4 * fm + f1) * ((y - y0) / 6);
184
200
  }
185
201
  /** C(y) anywhere the tables reach: dense inside the grid, coarse beyond.
186
202
  * @param {PeriodSecondsAtAge} periodFnSeconds @param {Float64Array} tab @param {number} y @returns {number | null} */
@@ -163,59 +163,63 @@ function createMoonArguments({ constants, fns }) {
163
163
  return kPl;
164
164
  }
165
165
  // ── Deep-time carrier tables ────────────────────────────────────────────────
166
- // In the era (|T| ≤ CARRIER_DENSE_CY = 250 kyr) each carrier is the Simpson
167
- // sum below, untouched — the certified numbers. Beyond, the integral
168
- // continues from the dense edge on a cumulative table of CARRIER_CELL_CY
169
- // cells (1 kyr; per-cell 3-point Simpson, a 3-point Simpson over the
170
- // partial last cell), grown in CARRIER_CHUNK cells in the direction asked
171
- // for. The former form re-integrated the whole span from J2000 on EVERY
172
- // call with N ∝ |T| nodes — 77 evaluations of the e channel at 300 kyr,
173
- // 2,500 at 10 Myr, per lunar argument set and frame — the deep-time
174
- // playback cost the owner measured (the loop's JS at 190 ms a frame).
175
- const CARRIER_DENSE_CY = 2500, CARRIER_CELL_CY = 10, CARRIER_CHUNK = 2500;
166
+ // Every carrier is ∫₀ᵀ f dt on ONE cumulative table per direction: fine
167
+ // cells of CARRIER_FINE_CY (100 yr) to ±CARRIER_FINE_RANGE_CY (250 kyr),
168
+ // grown in CARRIER_FINE_CHUNK cells (10 kyr — an in-window call never
169
+ // samples deeper than it must), then CARRIER_CELL_CY cells (1 kyr) grown in
170
+ // CARRIER_CHUNK (the former deep tier, unchanged); per-cell 3-point Simpson
171
+ // and a 3-point Simpson over the partial last cell. Continuous in value AND
172
+ // in rate: the per-cell error is fixed by the cell, so nothing jumps when T
173
+ // grows. (Plan 23 follow-up, measured on the Node Regression panel: the
174
+ // in-era form was one Simpson from J2000 to T whose node count stepped every
175
+ // 2 kyr (obliquity) / 8 kyr (planetary), so the accumulated error jumped
176
+ // there — 0.3–6″ on the obliquity carrier at 20–250 kyr, 10–15″ on the
177
+ // planetary one at ±6/22 kyr — invisible on L′'s rate, a notch of 0.5 d on
178
+ // the node period once the remainder rode Ω. The earlier form that
179
+ // re-integrated the whole span on every call cost the deep-time playback
180
+ // 190 ms a frame — the owner's measurement behind the tables.)
181
+ const CARRIER_FINE_CY = 1, CARRIER_FINE_RANGE_CY = 2500, CARRIER_FINE_CHUNK = 100;
182
+ const CARRIER_CELL_CY = 10, CARRIER_CHUNK = 2500;
176
183
  /** @param {(t: number) => number} f the SCALED integrand per century
177
- * @param {(T: number) => number} dense the in-era Simpson (its value at ±CARRIER_DENSE_CY seeds the table)
178
- * @returns {(T: number) => number} */
179
- function makeDeepCarrier(f, dense) {
180
- /** @type {{ fwd: number[] | null, bwd: number[] | null }} */
184
+ * @returns {(T: number) => number} ∫₀ᵀ f dt, T in Julian centuries (signed) */
185
+ function makeDeepCarrier(f) {
186
+ /** @type {{ fwd: {fine: number[], coarse: number[] | null} | null, bwd: {fine: number[], coarse: number[] | null} | null }} */
181
187
  const sides = { fwd: null, bwd: null };
188
+ const NF = CARRIER_FINE_RANGE_CY / CARRIER_FINE_CY;
189
+ /** The signed cell integral from t0 (nearer J2000) to t1. @param {number} t0 @param {number} t1 */
190
+ const cell = (t0, t1) => (f(t0) + 4 * f((t0 + t1) / 2) + f(t1)) * ((t1 - t0) / 6);
191
+ /** @param {{fine: number[]}} s @param {number} dir @param {number} upTo grow the fine table to this many cells */
192
+ const growFine = (s, dir, upTo) => {
193
+ for (let i = s.fine.length; i <= upTo; i++) s.fine.push(s.fine[i - 1] + cell(dir * (i - 1) * CARRIER_FINE_CY, dir * i * CARRIER_FINE_CY));
194
+ };
182
195
  return (T) => {
183
- if (Math.abs(T) <= CARRIER_DENSE_CY) return dense(T);
184
- const dir = T > 0 ? 1 : -1;
185
- let cum = dir === 1 ? sides.fwd : sides.bwd;
186
- if (!cum) { cum = [dense(dir * CARRIER_DENSE_CY)]; if (dir === 1) sides.fwd = cum; else sides.bwd = cum; }
187
- const dist = Math.abs(T) - CARRIER_DENSE_CY;
196
+ if (T === 0) return 0;
197
+ const dir = T > 0 ? 1 : -1, a = Math.abs(T);
198
+ let s = dir === 1 ? sides.fwd : sides.bwd;
199
+ if (!s) { s = { fine: [0], coarse: null }; if (dir === 1) sides.fwd = s; else sides.bwd = s; }
200
+ if (a <= CARRIER_FINE_RANGE_CY) {
201
+ const k = Math.floor(a / CARRIER_FINE_CY);
202
+ if (s.fine.length < k + 2) growFine(s, dir, Math.min(NF, Math.ceil((k + 1) / CARRIER_FINE_CHUNK) * CARRIER_FINE_CHUNK));
203
+ return s.fine[k] + cell(dir * k * CARRIER_FINE_CY, T);
204
+ }
205
+ if (!s.coarse) { growFine(s, dir, NF); s.coarse = [s.fine[NF]]; }
206
+ const dist = a - CARRIER_FINE_RANGE_CY;
188
207
  const k = Math.floor(dist / CARRIER_CELL_CY);
189
- let target = cum.length;
208
+ let target = s.coarse.length;
190
209
  while (target < k + 2) target += CARRIER_CHUNK;
191
- for (let i = cum.length; i < target; i++) {
192
- const t0 = dir * (CARRIER_DENSE_CY + (i - 1) * CARRIER_CELL_CY), t1 = t0 + dir * CARRIER_CELL_CY;
193
- cum.push(cum[i - 1] + dir * (f(t0) + 4 * f((t0 + t1) / 2) + f(t1)) * (CARRIER_CELL_CY / 6));
210
+ for (let i = s.coarse.length; i < target; i++) {
211
+ const t0 = dir * (CARRIER_FINE_RANGE_CY + (i - 1) * CARRIER_CELL_CY);
212
+ s.coarse.push(s.coarse[i - 1] + cell(t0, t0 + dir * CARRIER_CELL_CY));
194
213
  }
195
- const ta = dir * (CARRIER_DENSE_CY + k * CARRIER_CELL_CY);
196
- const hp = T - ta;
197
- return cum[k] + (f(ta) + 4 * f(ta + hp / 2) + f(T)) * (hp / 6);
214
+ return s.coarse[k] + cell(dir * (CARRIER_FINE_RANGE_CY + k * CARRIER_CELL_CY), T);
198
215
  };
199
216
  }
200
217
 
201
- /** The planetary carrier's SCALED integrand, k_PL · (e² − e0²), per century. @param {number} t */
218
+ /** The planetary carrier's SCALED integrand, k_PL · (e² − e0²), per century
219
+ * (e0 the channel's anchor CONST — not eccAt(0), which under integrated
220
+ * phase carries the R3 drift correction, the 8.2-2 convention). @param {number} t */
202
221
  const planetaryIntegrand = (t) => { const e = eccAt(t * 100); return kPlValue() * (e * e - eccE0 * eccE0); };
203
- /** @param {number} T @returns {number} */
204
- function planetaryCarrierDense(T) {
205
- if (T === 0) return 0;
206
- const k = kPlValue();
207
- // The channel's e0 anchor CONST — not eccAt(0), which under integrated
208
- // phase carries the R3 drift correction (the 8.2-2 convention).
209
- const e0sq = eccE0 * eccE0;
210
- /** @param {number} t */
211
- const f = (t) => { const e = eccAt(t * 100); return e * e - e0sq; };
212
- const N = Math.max(2, 2 * Math.ceil(Math.abs(T) * 100 / 8000));
213
- const h = T / N;
214
- let sum = f(0) + f(T);
215
- for (let i = 1; i < N; i++) sum += f(i * h) * (i % 2 ? 4 : 2);
216
- return k * sum * h / 3;
217
- }
218
- const planetaryCarrier = makeDeepCarrier(planetaryIntegrand, planetaryCarrierDense);
222
+ const planetaryCarrier = makeDeepCarrier(planetaryIntegrand);
219
223
 
220
224
  /** @type {{eps0: number, C: number} | null} */
221
225
  let obl = null;
@@ -238,19 +242,7 @@ function createMoonArguments({ constants, fns }) {
238
242
  }
239
243
  /** The obliquity carrier's SCALED integrand, C · (ε − ε0), per century. @param {number} t */
240
244
  const obliquityIntegrand = (t) => { const O = oblInit(); return O.C * (computeObliquityEarth(2000 + t * 100) - O.eps0); };
241
- /** @param {number} T @returns {number} */
242
- function obliquityCarrierDense(T) {
243
- if (T === 0) return 0;
244
- const O = oblInit();
245
- /** @param {number} t */
246
- const f = (t) => computeObliquityEarth(2000 + t * 100) - O.eps0;
247
- const N = Math.max(2, 2 * Math.ceil(Math.abs(T) * 100 / 2000));
248
- const h = T / N;
249
- let sum = f(0) + f(T);
250
- for (let i = 1; i < N; i++) sum += f(i * h) * (i % 2 ? 4 : 2);
251
- return O.C * sum * h / 3;
252
- }
253
- const obliquityCarrier = makeDeepCarrier(obliquityIntegrand, obliquityCarrierDense);
245
+ const obliquityCarrier = makeDeepCarrier(obliquityIntegrand);
254
246
 
255
247
  /** The frame share's REMAINDER for ϖ and Ω (plan 23). The physical exponents gave
256
248
  * up the frame's T² the Meeus-effective pair carried (T2F, Lieske's); pFix
@@ -307,24 +299,20 @@ function createMoonArguments({ constants, fns }) {
307
299
  const d2Ch = (e2(10) - 2 * e2(0) + e2(-10)) / 100;
308
300
  secComp = { dd2: d2Sec - d2Ch, d3Sec, Tq: SECULAR_COMPLETION_TAPER_YEARS / 100 };
309
301
  }
310
- const S = secComp;
311
- /** @param {number} t */
312
- const f = (t) => {
313
- const q = Math.abs(t) / S.Tq;
314
- if (q >= 1) return 0;
315
- const env = Math.cos(Math.PI / 2 * q) ** 2;
316
- return (0.5 * S.dd2 * t * t + S.d3Sec * t * t * t / 6) * env;
317
- };
318
- // Beyond the era the envelope has long closed (|t| ≥ Tq ≈ 279 cy → 0): the
319
- // integral is the constant to ±Tq, so integrate exactly that span instead of
320
- // N ∝ |T| nodes of zeros (in-era calls untouched).
321
- if (Math.abs(T) > CARRIER_DENSE_CY) T = Math.sign(T) * S.Tq;
322
- const N = Math.max(2, 2 * Math.ceil(Math.abs(T) * 100 / 2000));
323
- const h = T / N;
324
- let sum = f(0) + f(T);
325
- for (let i = 1; i < N; i++) sum += f(i * h) * (i % 2 ? 4 : 2);
326
- return kPlValue() * sum * h / 3;
302
+ // Beyond the taper the envelope has closed (|t| ≥ Tq ≈ 279 cy → 0): the
303
+ // integral is the constant to ±Tq, so the table is read at ±Tq there.
304
+ const Tq = secComp.Tq;
305
+ return secCarrier(Math.abs(T) > Tq ? Math.sign(T) * Tq : T);
327
306
  }
307
+ /** The completion's SCALED integrand, k_PL · the tapered curvature difference, per century. @param {number} t */
308
+ const secIntegrand = (t) => {
309
+ const S = /** @type {{dd2: number, d3Sec: number, Tq: number}} */ (secComp);
310
+ const q = Math.abs(t) / S.Tq;
311
+ if (q >= 1) return 0;
312
+ const env = Math.cos(Math.PI / 2 * q) ** 2;
313
+ return kPlValue() * (0.5 * S.dd2 * t * t + S.d3Sec * t * t * t / 6) * env;
314
+ };
315
+ const secCarrier = makeDeepCarrier(secIntegrand);
328
316
 
329
317
  // ── Deep-time branch: always-chains (Stage B) ────────────────────────────
330
318
  /** @type {number | null} */