@essrt/physics 9.1.1 → 9.2.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": "9.1.1",
3
+ "version": "9.2.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
6
  "modelVersion": "v14.0",
@@ -32,6 +32,7 @@
32
32
  "exports": {
33
33
  ".": { "types": "./types/index.d.ts", "default": "./src/index.js" },
34
34
  "./phase": { "types": "./types/phase/index.d.cts", "default": "./src/phase/index.cjs" },
35
+ "./phase/deep-calendar": { "types": "./types/phase/deep-calendar.d.cts", "default": "./src/phase/deep-calendar.cjs" },
35
36
  "./cardinal": { "types": "./types/cardinal/index.d.cts", "default": "./src/cardinal/index.cjs" },
36
37
  "./cardinal/one-source-structure": { "types": "./types/cardinal/one-source-structure.d.cts", "default": "./src/cardinal/one-source-structure.cjs" },
37
38
  "./moon/ecc-channel": { "types": "./types/moon/ecc-channel.d.cts", "default": "./src/moon/ecc-channel.cjs" },
package/src/index.js CHANGED
@@ -99,6 +99,10 @@ export { createDerivedViews } from './layer1/index.js';
99
99
  // purpose: tools/lib requires the same files via the exports-map subpaths;
100
100
  // re-exported here so bundled ESM consumers need only the package root).
101
101
  export { createPhaseMachinery } from './phase/index.cjs';
102
+ // The deep-time calendar (year ↔ JD under the drifting year length, the
103
+ // H-balanced event finder) — ONE implementation for both engines since the
104
+ // year/day collapse (formerly the PHASE-B-DUPLICATE family).
105
+ export { createDeepCalendar } from './phase/deep-calendar.cjs';
102
106
  export { createCardinalModel } from './cardinal/index.cjs';
103
107
  // D4b: the one-source cardinal structure — the EoC layer (year lengths,
104
108
  // crossing offsets, the e(t)-proportional spread) on the movement's own
@@ -57,16 +57,18 @@ export const createDerivedViews = ({ primitives: L0 }) => {
57
57
  * browser shows exactly the same one-epoch split, which is how we know the
58
58
  * port is faithful rather than merely close.
59
59
  *
60
- * It exists because `src/script.js` carries BOTH as separate mutable globals
60
+ * It exists because `src/script.js` carried BOTH as separate mutable globals
61
61
  * — `meansiderealyearlengthinDays` and `meansiderealyearlengthinDays_kinematic`
62
- * — and they hold the same value at every epoch once `recomputeEpochAnchors`
63
- * has run. They diverge only at module load, where one is the IAU anchor
64
- * (365.256363004) and the other is lattice-derived: the 118 ms of R16.
62
+ * — holding the same value at every epoch once `recomputeEpochAnchors` had
63
+ * run, diverging only at module load, where one is the IAU anchor
64
+ * (365.256363004) and the other lattice-derived: the 118 ms of R16.
65
65
  *
66
66
  * So the "kinematic" variant was never a different quantity, only a
67
- * different arrival route with a different starting constant. Keeping the
68
- * route explicit here lets B.3 retire one global without asserting the
69
- * equality on faith.
67
+ * different arrival route with a different starting constant. The year/day
68
+ * collapse retired the mutable global: `meansiderealyearlengthinDays_kinematic`
69
+ * is now a J2000 CONSTANT in the browser (this route at f(2000)), and the
70
+ * epoch value is read from `meansiderealyearlengthinDays`. This view stays
71
+ * so the equality is measured, not asserted.
70
72
  *
71
73
  * @param {number} year
72
74
  * @returns {number|null}
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Deep-time calendar — year ↔ JD under the drifting year length, and the
3
+ * H-balanced event finder. ONE implementation for both engines.
4
+ *
5
+ * Until the year/day collapse these five functions lived twice, hand-mirrored:
6
+ * `src/script.js` (the original) and `tools/lib/deep-time.js` (the port,
7
+ * marked PHASE-B-DUPLICATE). The website's runtime twin was a third copy, and
8
+ * both Phase E website bugs (Simpson-vs-trapezoid convention, the missing
9
+ * lattice-α pin) were hand-mirror divergences. This module is extracted
10
+ * VERBATIM from the browser copy — operation for operation — so the numerics
11
+ * are the shipped ones:
12
+ *
13
+ * - the days-per-year integral rides the phase machinery's grid (10-kyr
14
+ * cells, trapezoid rule, linear interpolation between cells) — the same
15
+ * discretization the fitted coefficients were produced against;
16
+ * - the table is built under the LATTICE α (R2): `withLatticeAlpha` is the
17
+ * engine's pin, injected, so a table build can never see the climate-
18
+ * modulated α(t);
19
+ * - `yearToJD` CARRIES A KNOWN 0.6-d ZERO-POINT OFFSET (measured −0.600 d in
20
+ * both engines): the anchor cell is seeded by linear extrapolation but read
21
+ * back by interpolation across a 10-kyr cell. DO NOT "fix" it in
22
+ * isolation — `balancedYearAtCycle` round-trips through `cyclesBetween`,
23
+ * which absorbs the offset (that is why the bracket lands on
24
+ * −302635.004 / 32682.268 exactly); normalising the table without
25
+ * re-checking that round-trip moves the Step 6a window;
26
+ * - `balancedYearAtCycle` round-trips through the JD deliberately, NOT as a
27
+ * shortcut to bisecting `cyclesBetween` in calendar units: calendar-year
28
+ * delta = H_J2000 exactly, but SI-year delta does not — they differ by
29
+ * ~6.7 SI yr per H. Dropping the round-trip returns the SI label instead of
30
+ * the calendar year. The Newton step uses H at J2000 (the browser stepped
31
+ * with its live H; the iteration converges to 1e-12 cycles either way and
32
+ * the two agree at J2000 scene state by construction).
33
+ *
34
+ * CommonJS on purpose, like `./index.cjs`: `tools/lib` is CJS and cannot
35
+ * require an ESM module synchronously; ESM imports CJS natively.
36
+ */
37
+
38
+ 'use strict';
39
+
40
+ /**
41
+ * @typedef {Object} PhaseMachinery
42
+ * @property {() => void} ensureTable
43
+ * @property {(year: number) => (number | null)} cumulAtYear
44
+ * @property {(targetCumul: number) => (number | null)} yearAtCumul
45
+ * @property {() => {yearMin: number, yearMax: number, stepYears: number, j2000Idx: number, length: number}} grid
46
+ */
47
+
48
+ /**
49
+ * @typedef {Object} DeepCalendar
50
+ * @property {(jd: number) => number} jdToSIyear SI-tropical-year label for a
51
+ * JD, anchored at startModelYearWithCorrection — NOT a calendar year (the
52
+ * scene's precession rotations integrate on this axis; a fit on it agrees
53
+ * with the runtime by construction; the round-trip bias Y_SI − Y is −11.0 yr
54
+ * at −302,635 and grows quadratically).
55
+ * @property {(targetCumul: number) => (number | null)} yearAtCumulIntegral
56
+ * inverse of the phase machinery's cumulAtYear — null outside the table.
57
+ * @property {() => void} ensureCumulDaysTable builds the ∫ daysPerYear dt table
58
+ * (idempotent; under the lattice α).
59
+ * @property {(year: number) => (number | null)} yearToJD calendar year → JD,
60
+ * integrating days-per-year from startModelYear; null outside the table
61
+ * domain or past the tidal-lock asymptote; startModelJD exactly at
62
+ * year = startModelYear.
63
+ * @property {(cycleOffset: number) => (number | null)} balancedYearAtCycle
64
+ * calendar year of the k-th H-balanced event, k = 0 being `balancedYear`;
65
+ * negative k = past; null outside the table domain.
66
+ * @property {() => (number | null)} cumulDaysTableLength diagnostic — the
67
+ * table length once built, null before.
68
+ */
69
+
70
+ /**
71
+ * @param {{
72
+ * phase: () => PhaseMachinery,
73
+ * meanYearInDaysAtAgeMa: (tMa: number) => (number | null),
74
+ * withLatticeAlpha: (build: () => void) => void,
75
+ * startModelJD: number,
76
+ * startModelYear: number,
77
+ * startModelYearWithCorrection: number,
78
+ * siTropicalYearDays: number,
79
+ * balancedYear: number,
80
+ * hJ2000: number,
81
+ * cyclesBetween: (yearA: number, yearB: number, divisorN: number) => (number | null),
82
+ * }} deps — `phase` is the engine's (lazily built) phase machinery, whose
83
+ * grid the days table shares; `meanYearInDaysAtAgeMa` the engine's
84
+ * days-per-year source (the LOD channel's `yearInDaysAtAge`);
85
+ * `withLatticeAlpha` the engine's R2 pin; `cyclesBetween` the engine's
86
+ * cycle counter (the browser passes its toggle-aware form, Node the
87
+ * integrated form — the caller's convention, preserved); `startModelYear`
88
+ * is the table zero (2000.5) and `startModelYearWithCorrection` the SI-axis
89
+ * anchor (≈ 2000.4977) — two different constants, do not unify.
90
+ * @returns {DeepCalendar}
91
+ */
92
+ function createDeepCalendar({
93
+ phase,
94
+ meanYearInDaysAtAgeMa,
95
+ withLatticeAlpha,
96
+ startModelJD,
97
+ startModelYear,
98
+ startModelYearWithCorrection,
99
+ siTropicalYearDays,
100
+ balancedYear,
101
+ hJ2000,
102
+ cyclesBetween,
103
+ }) {
104
+ /** @type {(jd: number) => number} */
105
+ const jdToSIyear = (jd) => startModelYearWithCorrection + (jd - startModelJD) / siTropicalYearDays;
106
+
107
+ /** @type {(targetCumul: number) => (number | null)} */
108
+ const yearAtCumulIntegral = (targetCumul) => phase().yearAtCumul(targetCumul);
109
+
110
+ /** ∫ daysPerYear dt from startModelYear, on the phase grid. @type {Float64Array | null} */
111
+ let cumulDaysTable = null;
112
+
113
+ function ensureCumulDaysTable() {
114
+ if (cumulDaysTable !== null) return;
115
+ withLatticeAlpha(buildCumulDaysTable); // R2: lattice tables pin α
116
+ }
117
+
118
+ function buildCumulDaysTable() {
119
+ const P = phase();
120
+ P.ensureTable();
121
+ const { yearMin, stepYears, j2000Idx, length: N } = P.grid();
122
+ const table = new Float64Array(N);
123
+
124
+ /** @type {(year: number) => (number | null)} */
125
+ const daysPerYear = (year) => meanYearInDaysAtAgeMa((startModelYear - year) / 1e6);
126
+
127
+ const gridYearAtJ2000Idx = yearMin + j2000Idx * stepYears;
128
+ const partialYearOffset = startModelYear - gridYearAtJ2000Idx;
129
+ const daysAtJ2000 = meanYearInDaysAtAgeMa(0);
130
+ table[j2000Idx] = -partialYearOffset * /** @type {number} */ (daysAtJ2000);
131
+
132
+ let prev = daysPerYear(gridYearAtJ2000Idx);
133
+ for (let i = j2000Idx + 1; i < N; i++) {
134
+ const curr = daysPerYear(yearMin + i * stepYears);
135
+ table[i] = (prev !== null && curr !== null && !Number.isNaN(table[i - 1]))
136
+ ? table[i - 1] + 0.5 * (prev + curr) * stepYears
137
+ : NaN;
138
+ prev = curr;
139
+ }
140
+
141
+ prev = daysPerYear(gridYearAtJ2000Idx);
142
+ for (let i = j2000Idx - 1; i >= 0; i--) {
143
+ const curr = daysPerYear(yearMin + i * stepYears);
144
+ table[i] = (prev !== null && curr !== null && !Number.isNaN(table[i + 1]))
145
+ ? table[i + 1] - 0.5 * (prev + curr) * stepYears
146
+ : NaN;
147
+ prev = curr;
148
+ }
149
+ cumulDaysTable = table;
150
+ }
151
+
152
+ /** @type {(year: number) => (number | null)} */
153
+ function yearToJD(year) {
154
+ if (!Number.isFinite(year)) return null;
155
+ ensureCumulDaysTable();
156
+ const table = /** @type {Float64Array} */ (cumulDaysTable);
157
+ const { yearMin, yearMax, stepYears } = phase().grid();
158
+ if (year < yearMin || year > yearMax) return null;
159
+ const idx_f = (year - yearMin) / stepYears;
160
+ const idx_lo = Math.floor(idx_f);
161
+ const idx_hi = Math.min(idx_lo + 1, table.length - 1);
162
+ const v_lo = table[idx_lo];
163
+ const v_hi = table[idx_hi];
164
+ if (Number.isNaN(v_lo) || Number.isNaN(v_hi)) return null;
165
+ const daysFromStartModel = v_lo + (idx_f - idx_lo) * (v_hi - v_lo);
166
+ return startModelJD + daysFromStartModel;
167
+ }
168
+
169
+ /** @type {(cycleOffset: number) => (number | null)} */
170
+ function balancedYearAtCycle(cycleOffset) {
171
+ // No cycleOffset === 0 short-circuit: returning balancedYear directly
172
+ // skips the JD round-trip below, misses the exact integer-cycle JD by
173
+ // ~0.5 d and gives ~21″ obliquity drift + ~3e-9 e drift at cycle 0.
174
+ const refCumul = phase().cumulAtYear(balancedYear);
175
+ if (refCumul === null) return null;
176
+ let Y = yearAtCumulIntegral(refCumul + cycleOffset);
177
+ if (Y === null) return null;
178
+ for (let iter = 0; iter < 5; iter++) {
179
+ const jd = yearToJD(Y);
180
+ if (jd === null) return Y;
181
+ const Y_SI = jdToSIyear(jd);
182
+ if (!Number.isFinite(Y_SI)) return Y;
183
+ const corrected = cyclesBetween(balancedYear, Y_SI, 1);
184
+ if (corrected === null) return Y;
185
+ const error = corrected - cycleOffset;
186
+ if (Math.abs(error) < 1e-12) break;
187
+ Y = Y - error * hJ2000;
188
+ }
189
+ return Y;
190
+ }
191
+
192
+ return Object.freeze({
193
+ jdToSIyear,
194
+ yearAtCumulIntegral,
195
+ ensureCumulDaysTable,
196
+ yearToJD,
197
+ balancedYearAtCycle,
198
+ cumulDaysTableLength: () => (cumulDaysTable === null ? null : cumulDaysTable.length),
199
+ });
200
+ }
201
+
202
+ module.exports = { createDeepCalendar };
package/types/index.d.ts CHANGED
@@ -2,6 +2,7 @@ export { createEpochPrimitives } from "./layer0/index.js";
2
2
  export { deriveEpochParams } from "./layer0/derive-params.js";
3
3
  export { createDerivedViews } from "./layer1/index.js";
4
4
  export { createPhaseMachinery } from "./phase/index.cjs";
5
+ export { createDeepCalendar } from "./phase/deep-calendar.cjs";
5
6
  export { createCardinalModel } from "./cardinal/index.cjs";
6
7
  export { createCardinalStructure } from "./cardinal/one-source-structure.cjs";
7
8
  export { createMoonEccChannel } from "./moon/ecc-channel.cjs";
@@ -0,0 +1,110 @@
1
+ export type PhaseMachinery = {
2
+ ensureTable: () => void;
3
+ cumulAtYear: (year: number) => (number | null);
4
+ yearAtCumul: (targetCumul: number) => (number | null);
5
+ grid: () => {
6
+ yearMin: number;
7
+ yearMax: number;
8
+ stepYears: number;
9
+ j2000Idx: number;
10
+ length: number;
11
+ };
12
+ };
13
+ export type DeepCalendar = {
14
+ /**
15
+ * SI-tropical-year label for a
16
+ * JD, anchored at startModelYearWithCorrection — NOT a calendar year (the
17
+ * scene's precession rotations integrate on this axis; a fit on it agrees
18
+ * with the runtime by construction; the round-trip bias Y_SI − Y is −11.0 yr
19
+ * at −302,635 and grows quadratically).
20
+ */
21
+ jdToSIyear: (jd: number) => number;
22
+ /**
23
+ * inverse of the phase machinery's cumulAtYear — null outside the table.
24
+ */
25
+ yearAtCumulIntegral: (targetCumul: number) => (number | null);
26
+ /**
27
+ * builds the ∫ daysPerYear dt table
28
+ * (idempotent; under the lattice α).
29
+ */
30
+ ensureCumulDaysTable: () => void;
31
+ /**
32
+ * calendar year → JD,
33
+ * integrating days-per-year from startModelYear; null outside the table
34
+ * domain or past the tidal-lock asymptote; startModelJD exactly at
35
+ * year = startModelYear.
36
+ */
37
+ yearToJD: (year: number) => (number | null);
38
+ /**
39
+ * calendar year of the k-th H-balanced event, k = 0 being `balancedYear`;
40
+ * negative k = past; null outside the table domain.
41
+ */
42
+ balancedYearAtCycle: (cycleOffset: number) => (number | null);
43
+ /**
44
+ * diagnostic — the
45
+ * table length once built, null before.
46
+ */
47
+ cumulDaysTableLength: () => (number | null);
48
+ };
49
+ /**
50
+ * @typedef {Object} PhaseMachinery
51
+ * @property {() => void} ensureTable
52
+ * @property {(year: number) => (number | null)} cumulAtYear
53
+ * @property {(targetCumul: number) => (number | null)} yearAtCumul
54
+ * @property {() => {yearMin: number, yearMax: number, stepYears: number, j2000Idx: number, length: number}} grid
55
+ */
56
+ /**
57
+ * @typedef {Object} DeepCalendar
58
+ * @property {(jd: number) => number} jdToSIyear SI-tropical-year label for a
59
+ * JD, anchored at startModelYearWithCorrection — NOT a calendar year (the
60
+ * scene's precession rotations integrate on this axis; a fit on it agrees
61
+ * with the runtime by construction; the round-trip bias Y_SI − Y is −11.0 yr
62
+ * at −302,635 and grows quadratically).
63
+ * @property {(targetCumul: number) => (number | null)} yearAtCumulIntegral
64
+ * inverse of the phase machinery's cumulAtYear — null outside the table.
65
+ * @property {() => void} ensureCumulDaysTable builds the ∫ daysPerYear dt table
66
+ * (idempotent; under the lattice α).
67
+ * @property {(year: number) => (number | null)} yearToJD calendar year → JD,
68
+ * integrating days-per-year from startModelYear; null outside the table
69
+ * domain or past the tidal-lock asymptote; startModelJD exactly at
70
+ * year = startModelYear.
71
+ * @property {(cycleOffset: number) => (number | null)} balancedYearAtCycle
72
+ * calendar year of the k-th H-balanced event, k = 0 being `balancedYear`;
73
+ * negative k = past; null outside the table domain.
74
+ * @property {() => (number | null)} cumulDaysTableLength diagnostic — the
75
+ * table length once built, null before.
76
+ */
77
+ /**
78
+ * @param {{
79
+ * phase: () => PhaseMachinery,
80
+ * meanYearInDaysAtAgeMa: (tMa: number) => (number | null),
81
+ * withLatticeAlpha: (build: () => void) => void,
82
+ * startModelJD: number,
83
+ * startModelYear: number,
84
+ * startModelYearWithCorrection: number,
85
+ * siTropicalYearDays: number,
86
+ * balancedYear: number,
87
+ * hJ2000: number,
88
+ * cyclesBetween: (yearA: number, yearB: number, divisorN: number) => (number | null),
89
+ * }} deps — `phase` is the engine's (lazily built) phase machinery, whose
90
+ * grid the days table shares; `meanYearInDaysAtAgeMa` the engine's
91
+ * days-per-year source (the LOD channel's `yearInDaysAtAge`);
92
+ * `withLatticeAlpha` the engine's R2 pin; `cyclesBetween` the engine's
93
+ * cycle counter (the browser passes its toggle-aware form, Node the
94
+ * integrated form — the caller's convention, preserved); `startModelYear`
95
+ * is the table zero (2000.5) and `startModelYearWithCorrection` the SI-axis
96
+ * anchor (≈ 2000.4977) — two different constants, do not unify.
97
+ * @returns {DeepCalendar}
98
+ */
99
+ export function createDeepCalendar({ phase, meanYearInDaysAtAgeMa, withLatticeAlpha, startModelJD, startModelYear, startModelYearWithCorrection, siTropicalYearDays, balancedYear, hJ2000, cyclesBetween, }: {
100
+ phase: () => PhaseMachinery;
101
+ meanYearInDaysAtAgeMa: (tMa: number) => (number | null);
102
+ withLatticeAlpha: (build: () => void) => void;
103
+ startModelJD: number;
104
+ startModelYear: number;
105
+ startModelYearWithCorrection: number;
106
+ siTropicalYearDays: number;
107
+ balancedYear: number;
108
+ hJ2000: number;
109
+ cyclesBetween: (yearA: number, yearB: number, divisorN: number) => (number | null);
110
+ }): DeepCalendar;