@essrt/physics 9.2.0 → 11.0.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/README.md +21 -0
- package/package.json +2 -2
- package/src/climate/l1-orbital.cjs +27 -3
- package/src/constants/generated.d.ts +3 -2
- package/src/constants/generated.js +4 -3
- package/src/earth/deep-orbital-history.cjs +147 -35
- package/src/earth/sidereal-channel-artifact.cjs +4 -3
- package/src/earth/sidereal-year-channel.cjs +14 -4
- package/src/earth/year-lengths.cjs +88 -53
- package/src/model.js +27 -14
- package/src/moon/deep-modes-artifact.cjs +2 -2
- package/src/phase/deep-calendar.cjs +6 -0
- package/types/climate/l1-orbital.d.cts +24 -3
- package/types/constants/generated.d.ts +3 -2
- package/types/earth/deep-orbital-history.d.cts +20 -2
- package/types/earth/sidereal-channel-artifact.d.cts +2 -2
- package/types/earth/year-lengths.d.cts +14 -16
- package/types/model.d.ts +8 -2
- package/types/moon/deep-modes-artifact.d.cts +14 -1
- package/types/phase/deep-calendar.d.cts +5 -0
package/README.md
CHANGED
|
@@ -87,6 +87,27 @@ solar/lunar eclipse search over a JD window, greatest-eclipse convention),
|
|
|
87
87
|
records and orientation at epoch). Validation targets are refused as
|
|
88
88
|
inputs — a counterfactual cannot move the goalposts it is judged by.
|
|
89
89
|
|
|
90
|
+
### The series artifact
|
|
91
|
+
|
|
92
|
+
Earth's published orbit history (eccentricity, perihelion, obliquity) is
|
|
93
|
+
the engine's banked N-body series — a 9-MB governed artifact kept in the
|
|
94
|
+
[public repository](https://github.com/dvansonsbeek/3d)
|
|
95
|
+
(`data/nbody-secular-series.json`), not inside this package. Pass it in to
|
|
96
|
+
get the published values:
|
|
97
|
+
|
|
98
|
+
```js
|
|
99
|
+
const series = JSON.parse(readFileSync('nbody-secular-series.json', 'utf8'));
|
|
100
|
+
const model = createModel(undefined, { secularSeriesArtifact: series });
|
|
101
|
+
model.earth.eccentricity(2000 - 100000); // 0.04008 — the series (La2004: 0.04006)
|
|
102
|
+
createModel().earth.eccentricity(2000 - 100000); // 0.03374 — the mode table
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Without the artifact `createModel()` evaluates the embedded 18-term mode
|
|
106
|
+
table at every epoch: exact at J2000, a compact approximation away from it
|
|
107
|
+
(RMS 3.6·10⁻³ in e against La2010a over the last 500 kyr, where the series
|
|
108
|
+
reads 2.5·10⁻⁵). The hosted API, the MCP server and the simulator always
|
|
109
|
+
pass the artifact.
|
|
110
|
+
|
|
90
111
|
## Underneath: a parts library
|
|
91
112
|
|
|
92
113
|
`createModel()` is assembly over unassembled factories. Each factory
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@essrt/physics",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "11.0.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": "
|
|
6
|
+
"modelVersion": "v16.0",
|
|
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",
|
|
@@ -85,17 +85,35 @@ function laggedL1Terms(l1Terms, relaxationKyr) {
|
|
|
85
85
|
* relaxationKyr: number,
|
|
86
86
|
* alphaGiaRateJ2000PerYr: number,
|
|
87
87
|
* alphaJ2000: number,
|
|
88
|
-
*
|
|
89
|
-
*
|
|
88
|
+
* j2RateJ2000PerYr?: number,
|
|
89
|
+
* j2J2000?: number,
|
|
90
|
+
* }} cfg - relaxationKyr, alphaGiaRateJ2000PerYr and j2RateJ2000PerYr are
|
|
91
|
+
* model-parameters deepTime constants; alphaJ2000 the IERS
|
|
92
|
+
* moment-of-inertia factor; j2J2000 Earth's J₂ (astro-reference)
|
|
90
93
|
* @returns {{
|
|
91
94
|
* alphaAt: (year: number) => number,
|
|
95
|
+
* j2RatioAt: (year: number) => number,
|
|
92
96
|
* laggedL1PermilAt: (year: number) => number,
|
|
93
97
|
* kPerPermille: number,
|
|
94
98
|
* relaxationKyr: number,
|
|
95
99
|
* laggedL1Terms: Array<{period_kyr: number, a: number, b: number}>,
|
|
96
100
|
* }}
|
|
101
|
+
*
|
|
102
|
+
* THE SAME REDISTRIBUTION, ITS SECOND OBSERVABLE — j2RatioAt(year) = J₂(t)/J₂₀.
|
|
103
|
+
* The mass that changes the polar moment changes the oblateness, and the
|
|
104
|
+
* luni-solar precession rate is proportional to it (ψ̇ ∝ (C − A)/C; the
|
|
105
|
+
* consumer is the spin integration, earth/deep-orbital-history). The J₂ side
|
|
106
|
+
* is scaled on its OWN observation, exactly as the α side is on its rate:
|
|
107
|
+
*
|
|
108
|
+
* J₂(t) = J₂₀ − k_J · [ L1'(t) − L1'(2000) ], k_J = −(dJ₂/dt)_obs / (dL1'/dt)₂₀₀₀
|
|
109
|
+
*
|
|
110
|
+
* so dJ₂/dt(J2000) IS the observed satellite rate (Cox & Chao 2002) by
|
|
111
|
+
* construction, and the J₂→α conversion factor never enters. A constant
|
|
112
|
+
* dJ₂/dt would be a rate valid at a point used across a span; this is the
|
|
113
|
+
* history. Bounded: ±5·10⁻⁴ over the glacial cycles, no secular part.
|
|
114
|
+
* Without the two constants the ratio is 1 at every year.
|
|
97
115
|
*/
|
|
98
|
-
function createAlphaGiaChannel({ l1Terms, yStdDenormalization, relaxationKyr, alphaGiaRateJ2000PerYr, alphaJ2000 }) {
|
|
116
|
+
function createAlphaGiaChannel({ l1Terms, yStdDenormalization, relaxationKyr, alphaGiaRateJ2000PerYr, alphaJ2000, j2RateJ2000PerYr, j2J2000 }) {
|
|
99
117
|
const lagged = laggedL1Terms(l1Terms, relaxationKyr);
|
|
100
118
|
const regime = { l1Terms: lagged, yStdDenormalization };
|
|
101
119
|
// dL'/dyear at J2000 in closed form: with t = (2000 − year)/1000,
|
|
@@ -105,8 +123,14 @@ function createAlphaGiaChannel({ l1Terms, yStdDenormalization, relaxationKyr, al
|
|
|
105
123
|
const slopeJ2000PermillePerYr = (-sum / 1000) * yStdDenormalization;
|
|
106
124
|
const kPerPermille = -alphaGiaRateJ2000PerYr / slopeJ2000PermillePerYr; // dα/dt = −k · dL'/dt
|
|
107
125
|
const laggedJ2000 = evalClimateL1OrbitalPermil(2000, regime);
|
|
126
|
+
const hasJ2 = j2RateJ2000PerYr !== undefined && j2J2000 !== undefined;
|
|
127
|
+
// (k_J / J₂₀) per ‰ — the relative oblateness coupling, from the observed dJ₂/dt
|
|
128
|
+
const j2RelPerPermille = hasJ2 ? (-j2RateJ2000PerYr / slopeJ2000PermillePerYr) / j2J2000 : 0;
|
|
108
129
|
return {
|
|
109
130
|
alphaAt: (year) => alphaJ2000 - kPerPermille * (evalClimateL1OrbitalPermil(year, regime) - laggedJ2000),
|
|
131
|
+
j2RatioAt: hasJ2
|
|
132
|
+
? (year) => 1 - j2RelPerPermille * (evalClimateL1OrbitalPermil(year, regime) - laggedJ2000)
|
|
133
|
+
: () => 1,
|
|
110
134
|
laggedL1PermilAt: (year) => evalClimateL1OrbitalPermil(year, regime),
|
|
111
135
|
kPerPermille,
|
|
112
136
|
relaxationKyr,
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
// Gives the TypeScript website full type safety at the boundary (§2g) while
|
|
3
3
|
// packages/physics stays JavaScript.
|
|
4
4
|
|
|
5
|
-
export declare const CONSTANTS_HASH: "
|
|
5
|
+
export declare const CONSTANTS_HASH: "726b10e23d8ace09";
|
|
6
6
|
|
|
7
7
|
export declare const MODEL_VERSION: string;
|
|
8
8
|
|
|
9
9
|
export declare const PREPRINT_DOI: string;
|
|
10
10
|
|
|
11
11
|
export declare const DEFAULT_CONSTANTS: {
|
|
12
|
-
readonly hash: "
|
|
12
|
+
readonly hash: "726b10e23d8ace09";
|
|
13
13
|
readonly additionalBodies: {
|
|
14
14
|
"pluto": {
|
|
15
15
|
"name": string;
|
|
@@ -136,6 +136,7 @@ export declare const DEFAULT_CONSTANTS: {
|
|
|
136
136
|
"alpha4PerMa4": number;
|
|
137
137
|
"alphaGiaRateJ2000PerYr": number;
|
|
138
138
|
"alphaGiaRelaxationKyr": number;
|
|
139
|
+
"j2RateJ2000PerYr": number;
|
|
139
140
|
"dtStackTaperFullHalfwidthYr": number;
|
|
140
141
|
"dtStackTaperTotalHalfwidthYr": number;
|
|
141
142
|
"recessionRegime": {
|
|
@@ -33,17 +33,17 @@
|
|
|
33
33
|
* carry it so a counterfactual is reproducible (§2d).
|
|
34
34
|
* @type {string}
|
|
35
35
|
*/
|
|
36
|
-
export const CONSTANTS_HASH = "
|
|
36
|
+
export const CONSTANTS_HASH = "726b10e23d8ace09";
|
|
37
37
|
|
|
38
38
|
/** Model version label — single source: public/input/model-version.json (§10 two-axis scheme). */
|
|
39
|
-
export const MODEL_VERSION = "
|
|
39
|
+
export const MODEL_VERSION = "v16.0";
|
|
40
40
|
|
|
41
41
|
/** Canonical preprint DOI — single source: public/input/model-version.json. */
|
|
42
42
|
export const PREPRINT_DOI = "10.21203/rs.3.rs-8758810/v4";
|
|
43
43
|
|
|
44
44
|
/** @type {Readonly<Record<string, unknown>>} */
|
|
45
45
|
export const DEFAULT_CONSTANTS = Object.freeze({
|
|
46
|
-
hash: "
|
|
46
|
+
hash: "726b10e23d8ace09",
|
|
47
47
|
additionalBodies: {
|
|
48
48
|
"pluto": {
|
|
49
49
|
"name": "Pluto",
|
|
@@ -182,6 +182,7 @@ export const DEFAULT_CONSTANTS = Object.freeze({
|
|
|
182
182
|
"alpha4PerMa4": 1.3619800519e-16,
|
|
183
183
|
"alphaGiaRateJ2000PerYr": -1.35e-11,
|
|
184
184
|
"alphaGiaRelaxationKyr": 6,
|
|
185
|
+
"j2RateJ2000PerYr": -2.7e-11,
|
|
185
186
|
"dtStackTaperFullHalfwidthYr": 300000,
|
|
186
187
|
"dtStackTaperTotalHalfwidthYr": 400000,
|
|
187
188
|
"recessionRegime": {
|
|
@@ -45,6 +45,8 @@
|
|
|
45
45
|
* axialPrecessionYearsJ2000: number,
|
|
46
46
|
* obliquityJ2000Deg: number,
|
|
47
47
|
* axialPrecessionYearsAtYearFn?: (year: number) => number,
|
|
48
|
+
* solarTorqueShareJ2000?: number,
|
|
49
|
+
* dynamicalEllipticityRatioAtYearFn?: (year: number) => number,
|
|
48
50
|
* }} deps — mode tables from the governed deep artifact (choose the ζ tier
|
|
49
51
|
* per consumer: era = its own 8-term extraction, deep = the 16-term
|
|
50
52
|
* table; NEVER a slice); anchors from the chain artifact's one home; the
|
|
@@ -57,13 +59,17 @@
|
|
|
57
59
|
* vs La2004 over −200 kyr vs the deep tier's 0.069° — the C-1 verdict,
|
|
58
60
|
* plan 02) and zetaModes serves only as the TAIL beyond the span (the
|
|
59
61
|
* seam at the span edge is the extraction residual, far outside every
|
|
60
|
-
* certified window).
|
|
62
|
+
* certified window). solarTorqueShareJ2000 and
|
|
63
|
+
* dynamicalEllipticityRatioAtYearFn: THE PRECESSION OF DATE — the two
|
|
64
|
+
* factors on the precession constant (see alphaAtGeneral below); absent,
|
|
65
|
+
* the constant is the secular law alone, bit-identical.
|
|
61
66
|
*/
|
|
62
67
|
function createDeepOrbitalHistory({
|
|
63
68
|
zModes, zetaModes, zetaSeries, zSeries,
|
|
64
69
|
anchorE, anchorPeriEclipticDeg, anchorInclEclipticDeg, anchorAscNodeEclipticDeg,
|
|
65
70
|
axialPrecessionYearsJ2000, obliquityJ2000Deg,
|
|
66
71
|
axialPrecessionYearsAtYearFn,
|
|
72
|
+
solarTorqueShareJ2000, dynamicalEllipticityRatioAtYearFn,
|
|
67
73
|
}) {
|
|
68
74
|
const D2R = Math.PI / 180, R2D = 180 / Math.PI;
|
|
69
75
|
|
|
@@ -198,10 +204,59 @@ function createDeepOrbitalHistory({
|
|
|
198
204
|
// dynamical). No new constants: ψ̇(t)'s J2000 anchor is the same
|
|
199
205
|
// certified year-length machinery; only its deep-time scaling is injected. Absent the option, α stays constant (the
|
|
200
206
|
// pre-D1 ±Myr-class behavior, bit-identical).
|
|
201
|
-
const
|
|
207
|
+
const alphaSecular = axialPrecessionYearsAtYearFn
|
|
202
208
|
? (/** @type {number} */ t) =>
|
|
203
209
|
((2 * Math.PI) / axialPrecessionYearsAtYearFn(2000 + t)) / Math.cos(EPS0)
|
|
204
210
|
: () => ALPHA;
|
|
211
|
+
// THE PRECESSION OF DATE (plan 06 §9 item 10 — owner: "adopt it in the
|
|
212
|
+
// model"). The secular law above scales the J2000 rate on the tidal history;
|
|
213
|
+
// the torque of date carries two more factors, both exactly 1 at J2000:
|
|
214
|
+
//
|
|
215
|
+
// SOLAR TORQUE — ∝ (1 − e²)^(−3/2): the Sun's share f_S of the torque is
|
|
216
|
+
// the engine's own solar-share formula, which holds e at its J2000 value;
|
|
217
|
+
// here it rides the eccentricity OF DATE, the z this factory already
|
|
218
|
+
// carries. Measured: the frozen form runs the precession 0.032 % slow on
|
|
219
|
+
// the million-year mean (the mean e² sits above today's) — 4.5° of
|
|
220
|
+
// equinox at −1 Myr, the obliquity ~100″ rms off La2004 there; with the
|
|
221
|
+
// factor it holds 12″ rms over the last Myr (La2004 integrates the same
|
|
222
|
+
// eccentricity-dependent torque).
|
|
223
|
+
//
|
|
224
|
+
// DYNAMICAL ELLIPTICITY — ψ̇ ∝ (C − A)/C ∝ J₂: the injected J₂(t)/J₂₀ of
|
|
225
|
+
// the engine's GIA channel (climate/l1-orbital j2RatioAt — the ice-age
|
|
226
|
+
// mass redistribution the length-of-day side already carries, scaled on
|
|
227
|
+
// the observed dJ₂/dt). Bounded ±5·10⁻⁴, no secular part.
|
|
228
|
+
//
|
|
229
|
+
// Together they close the millennial equinox: against Vondrák et al. (2011)
|
|
230
|
+
// the lag was −26.5″ at −2950 and −20.1″ at −2450 (the Sun's whole residual
|
|
231
|
+
// against Horizons there); with them −6.3″ and −3.7″, ≤ 0.3″ inside
|
|
232
|
+
// −450…+2500 (banked by tools/verify/equinox-vs-vondrak.js, which refuses
|
|
233
|
+
// to write if a factor is dropped). Absent both options the constant is
|
|
234
|
+
// the secular law alone — bit-identical to the pre-adoption factory.
|
|
235
|
+
const hasOfDate = solarTorqueShareJ2000 !== undefined || dynamicalEllipticityRatioAtYearFn !== undefined;
|
|
236
|
+
const oneMinusE0Sq = (() => { const [x0, y0] = zAt(0); return 1 - (x0 * x0 + y0 * y0); })();
|
|
237
|
+
const solarTorqueFactor = solarTorqueShareJ2000 !== undefined
|
|
238
|
+
? (/** @type {number} */ t) => {
|
|
239
|
+
const [zx, zy] = zAt(t);
|
|
240
|
+
return 1 + solarTorqueShareJ2000 * (Math.pow((1 - (zx * zx + zy * zy)) / oneMinusE0Sq, -1.5) - 1);
|
|
241
|
+
}
|
|
242
|
+
: () => 1;
|
|
243
|
+
const ellipticityFactor = dynamicalEllipticityRatioAtYearFn
|
|
244
|
+
? (/** @type {number} */ t) => dynamicalEllipticityRatioAtYearFn(2000 + t)
|
|
245
|
+
: () => 1;
|
|
246
|
+
// Two-entry memo on the precession constant of date: RK4 asks k2 and k3 at
|
|
247
|
+
// the same instant and the next step's k1 repeats this step's k4 — half
|
|
248
|
+
// the evaluations of the secular law AND of the two factors (measured: the
|
|
249
|
+
// tier builds cost what they did before the factors), the values
|
|
250
|
+
// bit-identical (a pure function of t).
|
|
251
|
+
let memoT0 = NaN, memoV0 = 0, memoT1 = NaN, memoV1 = 0;
|
|
252
|
+
const alphaOfDate = (/** @type {number} */ t) => {
|
|
253
|
+
if (t === memoT0) return memoV0;
|
|
254
|
+
if (t === memoT1) return memoV1;
|
|
255
|
+
const v = alphaSecular(t) * (solarTorqueFactor(t) * ellipticityFactor(t));
|
|
256
|
+
memoT1 = memoT0; memoV1 = memoV0; memoT0 = t; memoV0 = v;
|
|
257
|
+
return v;
|
|
258
|
+
};
|
|
259
|
+
const alphaAtGeneral = hasOfDate ? alphaOfDate : alphaSecular;
|
|
205
260
|
|
|
206
261
|
const cross = (/** @type {number[]} */ a, /** @type {number[]} */ b) =>
|
|
207
262
|
[a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
|
|
@@ -287,12 +342,46 @@ function createDeepOrbitalHistory({
|
|
|
287
342
|
return [o[0] / r, o[1] / r, o[2] / r];
|
|
288
343
|
};
|
|
289
344
|
|
|
290
|
-
/**
|
|
291
|
-
*
|
|
292
|
-
|
|
345
|
+
/** The equinox's GENERAL-PRECESSION longitude (deg, J2000 ecliptic frame):
|
|
346
|
+
* the broken angle through the ascending node of the ecliptic of date on
|
|
347
|
+
* the J2000 ecliptic — Π (the node's longitude) plus the arc along the
|
|
348
|
+
* ecliptic of date from that node to the equinox. This is the p_A of the
|
|
349
|
+
* precession literature (Lieske 1977; Simon et al. 1994: p_A = Λ_A − Π_A):
|
|
350
|
+
* the quantity the mean longitude OF DATE subtracts from the fixed-frame
|
|
351
|
+
* mean longitude — the N-body λ = Ω + ω + M is itself this broken angle
|
|
352
|
+
* (lattice-long-window-test.mjs), so the tropical year of date must be
|
|
353
|
+
* built on THIS rate. NOT the projected longitude atan2(g_y, g_x)
|
|
354
|
+
* (equinoxLonJ2000Deg, which the frame reconstruction needs): the two
|
|
355
|
+
* agree at J2000, where the ecliptic of date coincides with the J2000
|
|
356
|
+
* ecliptic, and part with the SQUARE of the ecliptic's tilt to its J2000
|
|
357
|
+
* position — (i²/4)·sin 2u class: 0.9″ at 0 AD, 5.9″ at −3000,
|
|
358
|
+
* arcminutes at deep time; as a rate 0.05 s of tropical year at ±4 kyr,
|
|
359
|
+
* 0.1–0.2 s at ±10 kyr, ~1 s beyond 100 kyr (measured 2026-09: the scene's
|
|
360
|
+
* own sidereal angle read exactly this against the projected-rate law at
|
|
361
|
+
* 16 epochs, and the re-booked Sun is flat against DE441 in the inertial
|
|
362
|
+
* frame over −2500…+2000 where the projected booking ramped to +5.6″).
|
|
363
|
+
* Degenerate node as i → 0: the dogleg's two parts compensate each other's
|
|
364
|
+
* error, and below 1e-9 the projected form is the exact limit.
|
|
365
|
+
* @param {number[]} n the ecliptic pole of date @param {number[]} gu the unit equinox */
|
|
366
|
+
function generalPrecessionLonDeg(n, gu) {
|
|
367
|
+
const Nx = -n[1], Ny = n[0]; // ẑ × n̂ — the ascending node of the ecliptic of date
|
|
368
|
+
const sN = Math.hypot(Nx, Ny);
|
|
369
|
+
if (sN < 1e-9) return Math.atan2(gu[1], gu[0]) * R2D;
|
|
370
|
+
const N = [Nx / sN, Ny / sN, 0];
|
|
371
|
+
const Pi = Math.atan2(N[1], N[0]);
|
|
372
|
+
const u = Math.atan2(dot(n, cross(N, gu)), dot(N, gu));
|
|
373
|
+
return (Pi + u) * R2D;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** Equinox-node longitudes only (J2000 ecliptic frame; projected and
|
|
377
|
+
* general-precession forms) — the light read used for the node-rate
|
|
378
|
+
* finite differences at store time. */
|
|
379
|
+
function eqLonsOnlyDeg(/** @type {number[]} */ s, /** @type {number} */ t) {
|
|
293
380
|
const n = orbitNormal(t);
|
|
294
381
|
const g = cross(s, n);
|
|
295
|
-
|
|
382
|
+
const gn = Math.hypot(g[0], g[1], g[2]);
|
|
383
|
+
const gu = [g[0] / gn, g[1] / gn, g[2] / gn];
|
|
384
|
+
return { proj: Math.atan2(gu[1], gu[0]) * R2D, general: generalPrecessionLonDeg(n, gu) };
|
|
296
385
|
}
|
|
297
386
|
|
|
298
387
|
/** One quantity bundle at time t (years from J2000) from spin axis s. */
|
|
@@ -328,26 +417,38 @@ function createDeepOrbitalHistory({
|
|
|
328
417
|
// interpolation on the grid is exact to the ζ series' own resolution.
|
|
329
418
|
orbitNormalX: n[0],
|
|
330
419
|
orbitNormalY: n[1],
|
|
331
|
-
// The equinox node (ŝ×n̂) longitude in the J2000 ecliptic
|
|
332
|
-
//
|
|
333
|
-
//
|
|
334
|
-
//
|
|
335
|
-
// tropical
|
|
336
|
-
//
|
|
420
|
+
// The equinox node (ŝ×n̂) PROJECTED longitude in the J2000 ecliptic
|
|
421
|
+
// frame — the frame reconstruction's input (frame-of-date.cjs rebuilds
|
|
422
|
+
// the unit equinox from it). Its rate is the general precession only
|
|
423
|
+
// to first order in the ecliptic's tilt to its J2000 position: the
|
|
424
|
+
// tropical-year consumers read generalPrecessionLonDeg below (measured
|
|
425
|
+
// 2026-09 — the projected rate booked the Sun's mean longitude of date
|
|
426
|
+
// 5.9″ off at −3000 and the sidereal year of date 1 s off at depth).
|
|
427
|
+
// ⚠ RATE-CONSUMER CONTRACT (measured): these fields' rates are correct
|
|
337
428
|
// AS IS — α is self-anchored to the LUNISOLAR rate at construction
|
|
338
429
|
// (K_LUNI, the one-RK4-step probe above), so the realized general
|
|
339
|
-
// precession p_geom(J2000) equals 360/axialPrecessionYearsJ2000
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
// sid/(sid−sol) rate and
|
|
343
|
-
// mean — a double-count of
|
|
344
|
-
// J2000 — and consumers had to
|
|
430
|
+
// precession p_geom(J2000) equals 360/axialPrecessionYearsJ2000 per
|
|
431
|
+
// JULIAN year (the sampler's time unit) by construction. No
|
|
432
|
+
// correction, no δ subtraction. History (why this contract exists):
|
|
433
|
+
// before the self-anchor, α carried the GENERAL sid/(sid−sol) rate and
|
|
434
|
+
// the n̂(t) geometry re-added the planetary mean — a double-count of
|
|
435
|
+
// +0.097″/yr ≈ +2.4 s of tropical year at J2000 — and consumers had to
|
|
436
|
+
// subtract the runtime anchor
|
|
345
437
|
// δ = p_geom(J2000) − 360/axialPrecessionYearsJ2000.
|
|
346
438
|
// A consumer may still measure δ at runtime as a SELF-CHECK; it
|
|
347
439
|
// must read ≈0 (sub-1e-4 ″/yr class). The LONGITUDE itself is raw
|
|
348
440
|
// geometry and has always needed no correction.
|
|
349
441
|
equinoxLonJ2000Deg: ((Math.atan2(gu[1], gu[0]) * R2D) % 360 + 360) % 360,
|
|
350
442
|
equinoxLonRateDegPerYr: 0, // filled at store time (build's ±2.5-yr central difference)
|
|
443
|
+
// The equinox's GENERAL-PRECESSION longitude (the broken angle through
|
|
444
|
+
// the node of the ecliptic of date — the literature's p_A; see
|
|
445
|
+
// generalPrecessionLonDeg above): its year-over-year retrograde advance
|
|
446
|
+
// IS the general precession of date, wobble included (the n̂(t)
|
|
447
|
+
// geometry generates the equinox wobble; ψ̇ itself is the secular
|
|
448
|
+
// α(H(t)) times the two factors of date). The tropical year of date is
|
|
449
|
+
// built on this rate (earth/year-lengths.cjs).
|
|
450
|
+
generalPrecessionLonDeg: ((generalPrecessionLonDeg(n, gu) % 360) + 360) % 360,
|
|
451
|
+
generalPrecessionLonRateDegPerYr: 0, // filled at store time (build's ±2.5-yr central difference)
|
|
351
452
|
};
|
|
352
453
|
}
|
|
353
454
|
|
|
@@ -390,9 +491,10 @@ function createDeepOrbitalHistory({
|
|
|
390
491
|
const storeWithRate = (/** @type {number} */ key, /** @type {number[]} */ sv, /** @type {number} */ tv) => {
|
|
391
492
|
const smp = sampleAt(sv, tv);
|
|
392
493
|
const hR = 2.5;
|
|
393
|
-
const lp =
|
|
394
|
-
const lm =
|
|
395
|
-
smp.equinoxLonRateDegPerYr = (((lp - lm + 540) % 360) - 180) / (2 * hR);
|
|
494
|
+
const lp = eqLonsOnlyDeg(rk4(sv, tv, +hR), tv + hR);
|
|
495
|
+
const lm = eqLonsOnlyDeg(rk4(sv, tv, -hR), tv - hR);
|
|
496
|
+
smp.equinoxLonRateDegPerYr = (((lp.proj - lm.proj + 540) % 360) - 180) / (2 * hR);
|
|
497
|
+
smp.generalPrecessionLonRateDegPerYr = (((lp.general - lm.general + 540) % 360) - 180) / (2 * hR);
|
|
396
498
|
grid.set(key, smp);
|
|
397
499
|
};
|
|
398
500
|
storeWithRate(0, s, 0);
|
|
@@ -421,6 +523,19 @@ function createDeepOrbitalHistory({
|
|
|
421
523
|
const f = (tYr - k0) / stepYr;
|
|
422
524
|
const lerp = (/** @type {number} */ x, /** @type {number} */ y) => x + (y - x) * f;
|
|
423
525
|
const dAng = (/** @type {number} */ x, /** @type {number} */ y) => x + (((y - x + 540) % 360) - 180) * f;
|
|
526
|
+
// C1 (cubic Hermite) on a wrapped angle with node rates (deg/yr) —
|
|
527
|
+
// linear interpolation here made the realized equinox RATE
|
|
528
|
+
// piecewise-constant per grid cell (the measured per-year
|
|
529
|
+
// precession staircase). Node rates come from the build's own
|
|
530
|
+
// ±2.5-yr central differences.
|
|
531
|
+
const hermiteAngle = (/** @type {number} */ l0, /** @type {number} */ l1, /** @type {number} */ r0, /** @type {number} */ r1) => {
|
|
532
|
+
const dl = ((l1 - l0 + 540) % 360) - 180; // unwrapped segment
|
|
533
|
+
const m0 = r0 * stepYr, m1 = r1 * stepYr;
|
|
534
|
+
const f2 = f * f, f3 = f2 * f;
|
|
535
|
+
const v = (2 * f3 - 3 * f2 + 1) * l0 + (f3 - 2 * f2 + f) * m0
|
|
536
|
+
+ (-2 * f3 + 3 * f2) * (l0 + dl) + (f3 - f2) * m1;
|
|
537
|
+
return ((v % 360) + 360) % 360;
|
|
538
|
+
};
|
|
424
539
|
return {
|
|
425
540
|
epsDeg: lerp(a.epsDeg, b.epsDeg),
|
|
426
541
|
e: lerp(a.e, b.e),
|
|
@@ -430,21 +545,12 @@ function createDeepOrbitalHistory({
|
|
|
430
545
|
inclEclDeg: lerp(a.inclEclDeg, b.inclEclDeg),
|
|
431
546
|
orbitNormalX: lerp(a.orbitNormalX, b.orbitNormalX),
|
|
432
547
|
orbitNormalY: lerp(a.orbitNormalY, b.orbitNormalY),
|
|
433
|
-
|
|
434
|
-
// realized equinox RATE piecewise-constant per grid cell (the
|
|
435
|
-
// measured per-year precession staircase). Node rates come from
|
|
436
|
-
// the build's own ±2.5-yr central differences.
|
|
437
|
-
equinoxLonJ2000Deg: (() => {
|
|
438
|
-
const l0 = a.equinoxLonJ2000Deg;
|
|
439
|
-
const dl = ((b.equinoxLonJ2000Deg - l0 + 540) % 360) - 180; // unwrapped segment
|
|
440
|
-
const m0 = a.equinoxLonRateDegPerYr * stepYr;
|
|
441
|
-
const m1 = (b.equinoxLonRateDegPerYr ?? a.equinoxLonRateDegPerYr) * stepYr;
|
|
442
|
-
const f2 = f * f, f3 = f2 * f;
|
|
443
|
-
const v = (2 * f3 - 3 * f2 + 1) * l0 + (f3 - 2 * f2 + f) * m0
|
|
444
|
-
+ (-2 * f3 + 3 * f2) * (l0 + dl) + (f3 - f2) * m1;
|
|
445
|
-
return ((v % 360) + 360) % 360;
|
|
446
|
-
})(),
|
|
548
|
+
equinoxLonJ2000Deg: hermiteAngle(a.equinoxLonJ2000Deg, b.equinoxLonJ2000Deg, a.equinoxLonRateDegPerYr, b.equinoxLonRateDegPerYr ?? a.equinoxLonRateDegPerYr),
|
|
447
549
|
equinoxLonRateDegPerYr: lerp(a.equinoxLonRateDegPerYr, b.equinoxLonRateDegPerYr ?? a.equinoxLonRateDegPerYr),
|
|
550
|
+
// the same C1 form for the general-precession longitude (its
|
|
551
|
+
// year-over-year difference is the tropical-year law's p).
|
|
552
|
+
generalPrecessionLonDeg: hermiteAngle(a.generalPrecessionLonDeg, b.generalPrecessionLonDeg, a.generalPrecessionLonRateDegPerYr, b.generalPrecessionLonRateDegPerYr ?? a.generalPrecessionLonRateDegPerYr),
|
|
553
|
+
generalPrecessionLonRateDegPerYr: lerp(a.generalPrecessionLonRateDegPerYr, b.generalPrecessionLonRateDegPerYr ?? a.generalPrecessionLonRateDegPerYr),
|
|
448
554
|
};
|
|
449
555
|
},
|
|
450
556
|
};
|
|
@@ -456,6 +562,12 @@ function createDeepOrbitalHistory({
|
|
|
456
562
|
// D4e diagnostics: the self-anchored lunisolar rate actually integrated.
|
|
457
563
|
alphaLunisolarArcsecPerYr: ALPHA * K_LUNI * R2D * 3600,
|
|
458
564
|
axialPrecessionYearsLunisolarJ2000: axialPrecessionYearsJ2000 / K_LUNI,
|
|
565
|
+
/** THE PRECESSION OF DATE — the two factors this integration applies to
|
|
566
|
+
* the secular precession constant at t years from J2000 (both 1 at
|
|
567
|
+
* t = 0; both 1 everywhere when the options are absent). ONE home for
|
|
568
|
+
* the published surface and the docs.
|
|
569
|
+
* @param {number} t @returns {{solarTorque: number, ellipticity: number}} */
|
|
570
|
+
precessionOfDateFactorsAt: (t) => ({ solarTorque: solarTorqueFactor(t), ellipticity: ellipticityFactor(t) }),
|
|
459
571
|
};
|
|
460
572
|
}
|
|
461
573
|
|