@essrt/physics 1.1.0 → 1.3.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 +10 -3
- package/package.json +3 -2
- package/src/constants/generated.d.ts +13 -2
- package/src/constants/generated.js +27 -4
- package/src/deltat/deep-time.cjs +8 -3
- package/src/deltat/recession-history.cjs +168 -0
- package/src/index.js +1 -0
- package/src/model.js +428 -5
- package/src/moon/month-chain.cjs +6 -1
package/README.md
CHANGED
|
@@ -75,8 +75,13 @@ with their own formula), `identity` (version + hashes + citation DOI), `epoch`
|
|
|
75
75
|
(H, LOD, α, ΔT, deep-time quantities at any year ±500 Myr), `earth`
|
|
76
76
|
(obliquity, eccentricity, inclination, perihelion), `lengths` (year and
|
|
77
77
|
day lengths), `cardinal` (solstice/equinox JD, RA, year length), `moon`
|
|
78
|
-
(distance
|
|
79
|
-
|
|
78
|
+
(distance and months at epoch, plus the apparent-position chain on the
|
|
79
|
+
JD(UT) axis — ecliptic longitude, latitude and distance from the shared
|
|
80
|
+
Meeus series over framework-native arguments), `eclipse` (geocentric
|
|
81
|
+
solar/lunar eclipse search over a JD window, greatest-eclipse convention),
|
|
82
|
+
`climate` (the L1 orbital-forcing formula) and `planets` (the Fibonacci-law
|
|
83
|
+
records and orientation at epoch). Validation targets are refused as
|
|
84
|
+
inputs — a counterfactual cannot move the goalposts it is judged by.
|
|
80
85
|
|
|
81
86
|
## Underneath: a parts library
|
|
82
87
|
|
|
@@ -88,7 +93,9 @@ browser. For custom wiring (different α channel, subset assembly), use
|
|
|
88
93
|
the factories directly: the reference adapter is `tools/lib/` in the
|
|
89
94
|
[public repository](https://github.com/dvansonsbeek/3d) — the hosted
|
|
90
95
|
simulator and the project's verification gates run on it. A service
|
|
91
|
-
surface
|
|
96
|
+
surface over `createModel()` lives in the same repository
|
|
97
|
+
(`packages/api`, an OpenAPI 3.1 handler with zero runtime dependencies)
|
|
98
|
+
— build-only until the hosting decision.
|
|
92
99
|
|
|
93
100
|
## Provenance
|
|
94
101
|
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@essrt/physics",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.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
|
-
"modelVersion": "
|
|
6
|
+
"modelVersion": "v11.0",
|
|
7
7
|
"_note": "The model identity this package version ships (IP-unified-architecture \u00a710: 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",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"./deltat/cycles": "./src/deltat/cycles.cjs",
|
|
50
50
|
"./deltat/deep-time": "./src/deltat/deep-time.cjs",
|
|
51
51
|
"./deltat/historical": "./src/deltat/historical.cjs",
|
|
52
|
+
"./deltat/recession-history": "./src/deltat/recession-history.cjs",
|
|
52
53
|
"./climate/l1-orbital": "./src/climate/l1-orbital.cjs",
|
|
53
54
|
"./eclipse/finders": "./src/eclipse/finders.cjs",
|
|
54
55
|
"./reference/published-curves": "./src/reference/published-curves.cjs",
|
|
@@ -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: "471a3f440b88cd86";
|
|
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: "471a3f440b88cd86";
|
|
13
13
|
readonly additionalBodies: {
|
|
14
14
|
"pluto": {
|
|
15
15
|
"name": string;
|
|
@@ -137,6 +137,17 @@ export declare const DEFAULT_CONSTANTS: {
|
|
|
137
137
|
"alphaClimateScalePerMille": number;
|
|
138
138
|
"dtStackTaperFullHalfwidthYr": number;
|
|
139
139
|
"dtStackTaperTotalHalfwidthYr": number;
|
|
140
|
+
"recessionRegime": {
|
|
141
|
+
"jointMa": number;
|
|
142
|
+
"knotAgesMa": number[];
|
|
143
|
+
"knotDistancesKm": number[];
|
|
144
|
+
"genesisMa": number;
|
|
145
|
+
"rocheLimitKm": number;
|
|
146
|
+
"solarOceanLeakBeta0": number;
|
|
147
|
+
"thermalPumpStartMa": number;
|
|
148
|
+
"thermalPumpEndMa": number;
|
|
149
|
+
"thermalPumpFactor": number;
|
|
150
|
+
};
|
|
140
151
|
};
|
|
141
152
|
readonly earth: {
|
|
142
153
|
"earthtiltMean": number;
|
|
@@ -32,17 +32,17 @@
|
|
|
32
32
|
* carry it so a counterfactual is reproducible (§2d).
|
|
33
33
|
* @type {string}
|
|
34
34
|
*/
|
|
35
|
-
export const CONSTANTS_HASH = "
|
|
35
|
+
export const CONSTANTS_HASH = "471a3f440b88cd86";
|
|
36
36
|
|
|
37
37
|
/** Model version label — single source: public/input/model-version.json (§10 two-axis scheme). */
|
|
38
|
-
export const MODEL_VERSION = "
|
|
38
|
+
export const MODEL_VERSION = "v11.0";
|
|
39
39
|
|
|
40
40
|
/** Canonical preprint DOI — single source: public/input/model-version.json. */
|
|
41
41
|
export const PREPRINT_DOI = "10.21203/rs.3.rs-8758810/v4";
|
|
42
42
|
|
|
43
43
|
/** @type {Readonly<Record<string, unknown>>} */
|
|
44
44
|
export const DEFAULT_CONSTANTS = Object.freeze({
|
|
45
|
-
hash: "
|
|
45
|
+
hash: "471a3f440b88cd86",
|
|
46
46
|
additionalBodies: {
|
|
47
47
|
"pluto": {
|
|
48
48
|
"name": "Pluto",
|
|
@@ -181,7 +181,30 @@ export const DEFAULT_CONSTANTS = Object.freeze({
|
|
|
181
181
|
"alpha4PerMa4": 1.3619800519e-16,
|
|
182
182
|
"alphaClimateScalePerMille": -3.93e-7,
|
|
183
183
|
"dtStackTaperFullHalfwidthYr": 300000,
|
|
184
|
-
"dtStackTaperTotalHalfwidthYr": 400000
|
|
184
|
+
"dtStackTaperTotalHalfwidthYr": 400000,
|
|
185
|
+
"recessionRegime": {
|
|
186
|
+
"jointMa": 1000,
|
|
187
|
+
"knotAgesMa": [
|
|
188
|
+
1400,
|
|
189
|
+
1900,
|
|
190
|
+
2450,
|
|
191
|
+
3200,
|
|
192
|
+
3900
|
|
193
|
+
],
|
|
194
|
+
"knotDistancesKm": [
|
|
195
|
+
337531.908,
|
|
196
|
+
322949.311,
|
|
197
|
+
322533.193,
|
|
198
|
+
296189.703,
|
|
199
|
+
275119.896
|
|
200
|
+
],
|
|
201
|
+
"genesisMa": 4498,
|
|
202
|
+
"rocheLimitKm": 9439.643,
|
|
203
|
+
"solarOceanLeakBeta0": 0.2,
|
|
204
|
+
"thermalPumpStartMa": 1000,
|
|
205
|
+
"thermalPumpEndMa": 1307,
|
|
206
|
+
"thermalPumpFactor": 0.454
|
|
207
|
+
}
|
|
185
208
|
},
|
|
186
209
|
earth: {
|
|
187
210
|
"earthtiltMean": 23.413527499101747,
|
package/src/deltat/deep-time.cjs
CHANGED
|
@@ -64,19 +64,24 @@
|
|
|
64
64
|
* @property {(year: number) => number} cycleLodSumAt - gated δLOD sum (incl. swing)
|
|
65
65
|
* @property {(year: number) => number} swingLodAt - gated swing δLOD alone
|
|
66
66
|
* @property {(year: number) => number} swingLodRateAt - gated analytic swing rate
|
|
67
|
+
* @property {(tMa: number) => number} [lEmAtAgeKgm2S] - OPTIONAL time-dependent
|
|
68
|
+
* Earth-Moon angular momentum (the Driver-1½ solar channels,
|
|
69
|
+
* recession-history.cjs). Absent → the J2000 constant, which the budget
|
|
70
|
+
* module equals exactly for t ≤ jointMa — the pure-twin convention.
|
|
67
71
|
*/
|
|
68
72
|
|
|
69
73
|
/** @param {DeepTimeLodDeps} deps */
|
|
70
74
|
function createDeepTimeLod(deps) {
|
|
71
75
|
const K = deps.constants;
|
|
76
|
+
const lEmAt = deps.lEmAtAgeKgm2S ?? (() => K.lTotalEmKgm2S);
|
|
72
77
|
|
|
73
|
-
/** Layer 1/2 mean LOD: LOD = 2π·I(t) / (
|
|
78
|
+
/** Layer 1/2 mean LOD: LOD = 2π·I(t) / (L_EM(t) − L_moon(t)).
|
|
74
79
|
* @param {number} t_Ma @returns {number|null} seconds, null past tidal lock */
|
|
75
80
|
function lodSecondsAtAge(t_Ma) {
|
|
76
81
|
const a = deps.moonDistanceMetresAtAge(t_Ma);
|
|
77
82
|
if (a <= 0 || a >= K.aLockMetres) return null;
|
|
78
83
|
return (2 * Math.PI * (deps.moiFactorAtAge(t_Ma) * K.mEarthAloneKg * K.rEarthMetres * K.rEarthMetres)) /
|
|
79
|
-
(
|
|
84
|
+
(lEmAt(t_Ma) - K.mMoonAloneKg * Math.sqrt(K.gmEmM3PerS2 * a) * K.eFactorMoon);
|
|
80
85
|
}
|
|
81
86
|
|
|
82
87
|
/** Same LOD with an EXPLICIT α (the browser's "α at climate mean" curve).
|
|
@@ -86,7 +91,7 @@ function createDeepTimeLod(deps) {
|
|
|
86
91
|
if (a <= 0 || a >= K.aLockMetres) return null;
|
|
87
92
|
const iEarth = alpha * K.mEarthAloneKg * K.rEarthMetres * K.rEarthMetres;
|
|
88
93
|
return (2 * Math.PI * iEarth) /
|
|
89
|
-
(
|
|
94
|
+
(lEmAt(t_Ma) - K.mMoonAloneKg * Math.sqrt(K.gmEmM3PerS2 * a) * K.eFactorMoon);
|
|
90
95
|
}
|
|
91
96
|
|
|
92
97
|
/** @param {number} t_Ma @returns {number|null} */
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regime-aware lunar recession history + the solar angular-momentum
|
|
3
|
+
* channels (Driver 1½) — THE shared implementation.
|
|
4
|
+
*
|
|
5
|
+
* Background (docs/99 §"The ungated mid-Precambrian window", docs/106 §D,
|
|
6
|
+
* and the research record in tools/explore/{farhat-divergence-probe,
|
|
7
|
+
* regime-aware-recession-study,fit-regime-recession}.js): Farhat 2022's
|
|
8
|
+
* central result is that lunar recession followed a resonant STAIRCASE —
|
|
9
|
+
* a single smooth quartic anchored at the modern LLR rate, the gated
|
|
10
|
+
* 0–650 Ma era and the Roche crossing cannot follow it through the
|
|
11
|
+
* 1–3.5 Ga window (−8.9σ Joffre, −11.4σ Moodies). This module carries
|
|
12
|
+
* the regime-aware history:
|
|
13
|
+
*
|
|
14
|
+
* t ≤ jointMa — the calibrated quartic, BIT-IDENTICAL (the
|
|
15
|
+
* Wells/Wu-gated era; its α set is the calibrated
|
|
16
|
+
* effective closed-budget pair and is not touched).
|
|
17
|
+
* jointMa..genesisMa — monotone cubic Hermite (Fritsch–Carlson) through
|
|
18
|
+
* the fitted knots, value- AND slope-continuous at
|
|
19
|
+
* the joint (analytic polynomial derivative), ending
|
|
20
|
+
* at the rigid Roche limit at genesisMa.
|
|
21
|
+
* t ≥ genesisMa — clamped at the Roche value (the Moon does not
|
|
22
|
+
* exist beyond its genesis; boundary value, not an
|
|
23
|
+
* extrapolation).
|
|
24
|
+
*
|
|
25
|
+
* The solar channels (explicit ONLY beyond jointMa — inside the gated era
|
|
26
|
+
* they are absorbed by the α calibration, and applying them there without
|
|
27
|
+
* recalibration measurably breaks Wells; see the study's variant 3):
|
|
28
|
+
* leak τ_s = β₀ (a/a₀)⁶ · τ_moon — ocean solar tide (M²/AU⁶ ratio
|
|
29
|
+
* scaling of the semidiurnal torque)
|
|
30
|
+
* pump τ_t = f_p · (τ_moon + τ_s) inside [pumpStartMa, pumpEndMa] —
|
|
31
|
+
* the insolation-driven atmospheric thermal tide
|
|
32
|
+
* (Zahnle–Walker 1987; Bartlett–Stevenson 2016;
|
|
33
|
+
* Mitchell–Kirscher 2023). The FITTED f_p ≈ 0.45 over a ~300 Myr
|
|
34
|
+
* window (degenerate with 0.15 × 900 Myr — the data constrain
|
|
35
|
+
* the INTEGRATED pump, ~2σ preferred over zero, Zhou 2024's
|
|
36
|
+
* contrary reading honestly noted in the fit record).
|
|
37
|
+
*
|
|
38
|
+
* Fit provenance: tools/explore/fit-regime-recession.js — 11 anchors
|
|
39
|
+
* (Lantink/Joffre 2022 · Weeli Wolli · Zhou 2024 ×3 paired a+LOD ·
|
|
40
|
+
* Meyers–Malinverno 2018 · Nanfen 2023 · Moodies), all ≤1.3σ; genesis
|
|
41
|
+
* pinned at the shipped 4498 Ma Roche crossing (the genesis profile is
|
|
42
|
+
* flat — the data do not constrain it). The knot values ARE the shipped
|
|
43
|
+
* definition (metre-rounded at fit time, not truncated from a more
|
|
44
|
+
* precise source).
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
'use strict';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @typedef {Object} RecessionRegime
|
|
51
|
+
* @property {number} jointMa - regime boundary; quartic below, spline above
|
|
52
|
+
* @property {number[]} knotAgesMa - interior knot ages (ascending)
|
|
53
|
+
* @property {number[]} knotDistancesKm - fitted knot distances
|
|
54
|
+
* @property {number} genesisMa - the Roche-crossing epoch
|
|
55
|
+
* @property {number} rocheLimitKm - the rigid Roche endpoint
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {{
|
|
60
|
+
* aMoonNowMetres: number,
|
|
61
|
+
* alpha1PerMa: number, alpha3PerMa3: number, alpha4PerMa4: number,
|
|
62
|
+
* regime: RecessionRegime,
|
|
63
|
+
* }} deps
|
|
64
|
+
* @returns {{ distanceMetresAtAge: (tMa: number) => number }}
|
|
65
|
+
*/
|
|
66
|
+
function createMoonRecessionHistory(deps) {
|
|
67
|
+
const { aMoonNowMetres: aNow, alpha1PerMa: a1, alpha3PerMa3: a3, alpha4PerMa4: a4 } = deps;
|
|
68
|
+
const R = deps.regime;
|
|
69
|
+
|
|
70
|
+
/** The calibrated quartic (identical expression to every consumer).
|
|
71
|
+
* @param {number} tMa @returns {number} */
|
|
72
|
+
const polyM = (tMa) => aNow * (1 + a1 * tMa + a3 * tMa * tMa * tMa + a4 * tMa * tMa * tMa * tMa);
|
|
73
|
+
/** Analytic slope of the quartic, m/Ma.
|
|
74
|
+
* @param {number} tMa @returns {number} */
|
|
75
|
+
const polySlopeM = (tMa) => aNow * (a1 + 3 * a3 * tMa * tMa + 4 * a4 * tMa * tMa * tMa);
|
|
76
|
+
|
|
77
|
+
// Knot chain: joint (value+slope from the quartic) → fitted knots → Roche.
|
|
78
|
+
const ts = [R.jointMa, ...R.knotAgesMa, R.genesisMa];
|
|
79
|
+
const vs = [polyM(R.jointMa), ...R.knotDistancesKm.map((km) => km * 1000), R.rocheLimitKm * 1000];
|
|
80
|
+
const n = ts.length;
|
|
81
|
+
const d = [];
|
|
82
|
+
for (let i = 0; i < n - 1; i++) d.push((vs[i + 1] - vs[i]) / (ts[i + 1] - ts[i]));
|
|
83
|
+
const m = new Array(n);
|
|
84
|
+
m[0] = polySlopeM(R.jointMa);
|
|
85
|
+
for (let i = 1; i < n - 1; i++) m[i] = (d[i - 1] * d[i] <= 0) ? 0 : (d[i - 1] + d[i]) / 2;
|
|
86
|
+
m[n - 1] = d[n - 2];
|
|
87
|
+
// Fritsch–Carlson monotonicity limiter — the history must be monotone
|
|
88
|
+
// into the past (the Moon only recedes on the mean).
|
|
89
|
+
for (let i = 0; i < n - 1; i++) {
|
|
90
|
+
if (d[i] === 0) { m[i] = 0; m[i + 1] = 0; continue; }
|
|
91
|
+
const a = m[i] / d[i], b = m[i + 1] / d[i];
|
|
92
|
+
const s = a * a + b * b;
|
|
93
|
+
if (s > 9) { const tau = 3 / Math.sqrt(s); m[i] = tau * a * d[i]; m[i + 1] = tau * b * d[i]; }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** @param {number} tMa @returns {number} metres */
|
|
97
|
+
function distanceMetresAtAge(tMa) {
|
|
98
|
+
if (tMa <= R.jointMa) return polyM(tMa);
|
|
99
|
+
if (tMa >= R.genesisMa) return R.rocheLimitKm * 1000;
|
|
100
|
+
let i = 0;
|
|
101
|
+
while (i < n - 2 && tMa > ts[i + 1]) i++;
|
|
102
|
+
const h = ts[i + 1] - ts[i], s = (tMa - ts[i]) / h, s2 = s * s, s3 = s2 * s;
|
|
103
|
+
return (2 * s3 - 3 * s2 + 1) * vs[i] + (s3 - 2 * s2 + s) * h * m[i]
|
|
104
|
+
+ (-2 * s3 + 3 * s2) * vs[i + 1] + (s3 - s2) * h * m[i + 1];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { distanceMetresAtAge };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The solar angular-momentum channels: L_EM(t) integrated backward from
|
|
112
|
+
* J2000 with the ocean leak and the thermal-tide pump, explicit only
|
|
113
|
+
* beyond jointMa. Absent this module, every consumer's L_EM is the J2000
|
|
114
|
+
* constant — which remains the exact behaviour for t ≤ jointMa.
|
|
115
|
+
*
|
|
116
|
+
* @param {{
|
|
117
|
+
* lTotalJ2000KgM2S: number,
|
|
118
|
+
* mMoonAloneKg: number, gmEmM3PerS2: number, eFactorMoon: number,
|
|
119
|
+
* beta0: number,
|
|
120
|
+
* pumpStartMa: number, pumpEndMa: number, pumpFactor: number,
|
|
121
|
+
* jointMa: number, genesisMa: number,
|
|
122
|
+
* distanceMetresAtAge: (tMa: number) => number,
|
|
123
|
+
* stepMa?: number,
|
|
124
|
+
* }} deps
|
|
125
|
+
* @returns {{ lEmAtAgeKgm2S: (tMa: number) => number }}
|
|
126
|
+
*/
|
|
127
|
+
function createSolarChannelBudget(deps) {
|
|
128
|
+
const STEP = deps.stepMa ?? 5;
|
|
129
|
+
/** @param {number} aM @returns {number} */
|
|
130
|
+
const lOrb = (aM) => deps.mMoonAloneKg * Math.sqrt(deps.gmEmM3PerS2 * aM) * deps.eFactorMoon;
|
|
131
|
+
const a0 = deps.distanceMetresAtAge(0);
|
|
132
|
+
|
|
133
|
+
/** @type {Float64Array|null} */
|
|
134
|
+
let table = null;
|
|
135
|
+
const build = () => {
|
|
136
|
+
const nSteps = Math.ceil(deps.genesisMa / STEP) + 1;
|
|
137
|
+
const t = new Float64Array(nSteps);
|
|
138
|
+
let L = deps.lTotalJ2000KgM2S;
|
|
139
|
+
for (let i = 0; i < nSteps; i++) {
|
|
140
|
+
t[i] = L;
|
|
141
|
+
const tMa = i * STEP;
|
|
142
|
+
const dLorb = lOrb(deps.distanceMetresAtAge(tMa)) - lOrb(deps.distanceMetresAtAge(tMa + STEP));
|
|
143
|
+
const mid = tMa + STEP / 2;
|
|
144
|
+
const leakDt = mid > deps.jointMa ? deps.beta0 * Math.pow(deps.distanceMetresAtAge(tMa) / a0, 6) * dLorb : 0;
|
|
145
|
+
const pumpDt = (mid > deps.pumpStartMa && mid < deps.pumpEndMa) ? deps.pumpFactor * (dLorb + leakDt) : 0;
|
|
146
|
+
L = L - (pumpDt - leakDt);
|
|
147
|
+
}
|
|
148
|
+
return t;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/** @param {number} tMa @returns {number} */
|
|
152
|
+
function lEmAtAgeKgm2S(tMa) {
|
|
153
|
+
// The channels are zero below jointMa by construction — return the
|
|
154
|
+
// constant EXACTLY there (no table interpolation), so the gated era
|
|
155
|
+
// stays bit-identical to the closed-budget engine (the 1-ULP fixture
|
|
156
|
+
// gates depend on this).
|
|
157
|
+
if (tMa <= deps.jointMa) return deps.lTotalJ2000KgM2S;
|
|
158
|
+
if (table === null) table = build();
|
|
159
|
+
const x = tMa / STEP;
|
|
160
|
+
const i = Math.min(Math.floor(x), table.length - 2);
|
|
161
|
+
const f = Math.min(x - i, 1);
|
|
162
|
+
return table[i] * (1 - f) + table[i + 1] * f;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return { lEmAtAgeKgm2S };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
module.exports = { createMoonRecessionHistory, createSolarChannelBudget };
|
package/src/index.js
CHANGED
|
@@ -125,6 +125,7 @@ export { createPlanetModel } from './planets/model.cjs';
|
|
|
125
125
|
// Phase 8.4 — the climate/ΔT machinery, extracted layer by layer.
|
|
126
126
|
export { createDeltaTCycles } from './deltat/cycles.cjs';
|
|
127
127
|
export { createDeepTimeLod } from './deltat/deep-time.cjs';
|
|
128
|
+
export { createMoonRecessionHistory, createSolarChannelBudget } from './deltat/recession-history.cjs';
|
|
128
129
|
export { deltaTEspenakMeeusCanonSeconds } from './deltat/historical.cjs';
|
|
129
130
|
export { evalClimateL1OrbitalPermil } from './climate/l1-orbital.cjs';
|
|
130
131
|
// Phase 8.5 — eclipse geometry (single-copy: the browser had no Node twin).
|
package/src/model.js
CHANGED
|
@@ -18,11 +18,21 @@
|
|
|
18
18
|
* which itself mirrors tools/lib/constants.js §9.
|
|
19
19
|
*/
|
|
20
20
|
import { deriveEpochParams } from './layer0/derive-params.js';
|
|
21
|
+
import * as FL from './planets/fibonacci-laws.cjs';
|
|
22
|
+
import * as planetOrientation from './planets/orientation.cjs';
|
|
21
23
|
import { createPhaseMachinery } from './phase/index.cjs';
|
|
22
24
|
import { createCardinalModel } from './cardinal/index.cjs';
|
|
23
25
|
import { createDeltaTCycles } from './deltat/cycles.cjs';
|
|
24
26
|
import { createDeepTimeLod } from './deltat/deep-time.cjs';
|
|
27
|
+
import { createMoonRecessionHistory, createSolarChannelBudget } from './deltat/recession-history.cjs';
|
|
25
28
|
import { evalClimateL1OrbitalPermil } from './climate/l1-orbital.cjs';
|
|
29
|
+
import { createMoonEccChannel } from './moon/ecc-channel.cjs';
|
|
30
|
+
import { createMoonMonthChain } from './moon/month-chain.cjs';
|
|
31
|
+
import { createChainCycleIntegrator } from './chain-cycles/index.cjs';
|
|
32
|
+
import { createMoonArguments, jdToDecimalYear } from './moon/arguments.cjs';
|
|
33
|
+
import { createMoonSeries } from './moon/series.cjs';
|
|
34
|
+
import { createEclipseFinders } from './eclipse/finders.cjs';
|
|
35
|
+
import { driver2PeriodSecondsAtAge } from './planets/orbit-chain.cjs';
|
|
26
36
|
|
|
27
37
|
/**
|
|
28
38
|
* RA-day-offset Fourier amplitudes (ms). KNOWN EXCEPTION carried over from
|
|
@@ -34,6 +44,17 @@ const RA_DAY_OFFSET_MEAN_MS = -14.194;
|
|
|
34
44
|
const RA_DAY_OFFSET_ECC_MS = -5.64;
|
|
35
45
|
const RA_DAY_OFFSET_OBLIQ_MS = -1.684;
|
|
36
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Moon-channel eccentricity sensitivities (perigee/node), the [g/g₀]^s
|
|
49
|
+
* exponents of the factored deep-time law. Same known-exception class as the
|
|
50
|
+
* RA offsets above: single source src/script.js _FW_MOON, mirrored as
|
|
51
|
+
* literals in tools/lib/deep-time.js (_ECOMP_S_W/_ECOMP_S_N). Both are
|
|
52
|
+
* Meeus-effective — S_N moved 1.0 → 1.018 with the v4 frame-attribution
|
|
53
|
+
* batch.
|
|
54
|
+
*/
|
|
55
|
+
const MOON_ECC_SENSITIVITY_PERIGEE = 2.407;
|
|
56
|
+
const MOON_ECC_SENSITIVITY_NODE = 1.018;
|
|
57
|
+
|
|
37
58
|
/**
|
|
38
59
|
* Assemble the model surfaces from a resolved constants context + fitted
|
|
39
60
|
* coefficients. Internal: `createModel` in index.js composes this with the
|
|
@@ -138,11 +159,38 @@ export function assembleModel(C, F) {
|
|
|
138
159
|
gmEarthMoonSystemKm3S2: GM_EARTH_MOON_SYSTEM,
|
|
139
160
|
});
|
|
140
161
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
162
|
+
// ── Driver 1½: regime-aware recession history + the solar channels ────────
|
|
163
|
+
// The quartic stays bit-identical ≤ jointMa (the Wells/Wu-gated era);
|
|
164
|
+
// beyond, the fitted staircase spline to the Roche crossing, and the
|
|
165
|
+
// ocean-leak/thermal-pump channels make L_EM time-dependent.
|
|
166
|
+
const REGIME = C.deepTime.recessionRegime;
|
|
167
|
+
const recession = createMoonRecessionHistory({
|
|
168
|
+
aMoonNowMetres: EPOCH_PARAMS.moonDistanceNowM,
|
|
169
|
+
alpha1PerMa: EPOCH_PARAMS.alpha1PerMa,
|
|
170
|
+
alpha3PerMa3: EPOCH_PARAMS.alpha3PerMa3,
|
|
171
|
+
alpha4PerMa4: EPOCH_PARAMS.alpha4PerMa4,
|
|
172
|
+
regime: {
|
|
173
|
+
jointMa: REGIME.jointMa,
|
|
174
|
+
knotAgesMa: REGIME.knotAgesMa,
|
|
175
|
+
knotDistancesKm: REGIME.knotDistancesKm,
|
|
176
|
+
genesisMa: REGIME.genesisMa,
|
|
177
|
+
rocheLimitKm: REGIME.rocheLimitKm,
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
const moonDistanceMetresAtAge = recession.distanceMetresAtAge;
|
|
181
|
+
const solarBudget = createSolarChannelBudget({
|
|
182
|
+
lTotalJ2000KgM2S: EPOCH_PARAMS.totalAngularMomentumKgM2S,
|
|
183
|
+
mMoonAloneKg: EPOCH_PARAMS.moonMassKg,
|
|
184
|
+
gmEmM3PerS2: EPOCH_PARAMS.gmEarthMoonM3S2,
|
|
185
|
+
eFactorMoon: EPOCH_PARAMS.moonEccentricityFactor,
|
|
186
|
+
beta0: REGIME.solarOceanLeakBeta0,
|
|
187
|
+
pumpStartMa: REGIME.thermalPumpStartMa,
|
|
188
|
+
pumpEndMa: REGIME.thermalPumpEndMa,
|
|
189
|
+
pumpFactor: REGIME.thermalPumpFactor,
|
|
190
|
+
jointMa: REGIME.jointMa,
|
|
191
|
+
genesisMa: REGIME.genesisMa,
|
|
192
|
+
distanceMetresAtAge: moonDistanceMetresAtAge,
|
|
193
|
+
});
|
|
146
194
|
|
|
147
195
|
const DT = F.DT_STACK;
|
|
148
196
|
const RES = F.DT_RESONATOR;
|
|
@@ -202,6 +250,7 @@ export function assembleModel(C, F) {
|
|
|
202
250
|
cycleLodSumAt: dtCycleLodCorrectionSum,
|
|
203
251
|
swingLodAt: (year) => dtCycles.swingLodSecondsAt(year),
|
|
204
252
|
swingLodRateAt: (year) => dtCycles.swingLodRateAt(year),
|
|
253
|
+
lEmAtAgeKgm2S: solarBudget.lEmAtAgeKgm2S,
|
|
205
254
|
});
|
|
206
255
|
|
|
207
256
|
// ── Integrated ∫1/H(t)dt phase (R2 α-pin honoured while building) ─────────
|
|
@@ -399,6 +448,128 @@ export function assembleModel(C, F) {
|
|
|
399
448
|
/** @param {number} year @returns {number} */
|
|
400
449
|
const deltaTSeconds = (year) => C.earthOrbital.deltaTStart + meanDeltaTSecondsAtAge(yearToTMa(year));
|
|
401
450
|
|
|
451
|
+
// ── Planets: Fibonacci-law derivation chain + orientation ─────────────────
|
|
452
|
+
const PLANET_KEYS = ['mercury', 'venus', 'mars', 'jupiter', 'saturn', 'uranus', 'neptune'];
|
|
453
|
+
const massEarthAlone = GM_EARTH_ALONE / G_CONSTANT;
|
|
454
|
+
/** @type {Record<string, number>} */
|
|
455
|
+
const massFraction = {};
|
|
456
|
+
for (const k of PLANET_KEYS) massFraction[k] = 1 / C.physicalConstants.massRatioDE440[k];
|
|
457
|
+
massFraction.earth = massEarthAlone / M_SUN;
|
|
458
|
+
|
|
459
|
+
const PSI = FL.computePsiConstant({
|
|
460
|
+
earthInvPlaneInclinationAmplitude: earthInclAmplitude,
|
|
461
|
+
massEarthAlone,
|
|
462
|
+
massSun: M_SUN,
|
|
463
|
+
});
|
|
464
|
+
const eccentricityAmplitudeK = FL.computeKConstant({
|
|
465
|
+
eccentricityAmplitude,
|
|
466
|
+
massEarthAlone,
|
|
467
|
+
massSun: M_SUN,
|
|
468
|
+
earthTiltMeanDeg: earthtiltMean,
|
|
469
|
+
});
|
|
470
|
+
const systemResetN = C.foundational.systemResetN;
|
|
471
|
+
const t2000 = 2000 - (balancedYear - systemResetN * H);
|
|
472
|
+
const balancedJD = startmodelJD - meanSolarYearDays * (startModelYearWithCorrection - balancedYear);
|
|
473
|
+
|
|
474
|
+
/** @param {[number, number]|null} frac @returns {number|null} */
|
|
475
|
+
const fractionToYears = (frac) => (frac === null ? null : (H * frac[0]) / frac[1]);
|
|
476
|
+
|
|
477
|
+
/** @type {Record<string, Record<string, any>>} */
|
|
478
|
+
const PLANET_RECORDS = {};
|
|
479
|
+
for (const k of PLANET_KEYS) {
|
|
480
|
+
const mp = C.planets[k];
|
|
481
|
+
const ar = C.planetOrbitalElements[k];
|
|
482
|
+
const ecl = /** @type {number} */ (fractionToYears(mp.perihelionEclipticFraction));
|
|
483
|
+
const axial = /** @type {number} */ (fractionToYears(mp.axialPrecessionFraction));
|
|
484
|
+
const obliquityCycle = fractionToYears(mp.obliquityCycleFraction)
|
|
485
|
+
?? Math.abs(1 / (1 / ecl - 1 / (H / 13)));
|
|
486
|
+
const wobble = FL.computeWobblePeriodYears(ecl, axial, H);
|
|
487
|
+
const il = FL.computeInclinationLaw({
|
|
488
|
+
fibonacciD: mp.fibonacciD,
|
|
489
|
+
massFrac: massFraction[k],
|
|
490
|
+
invPlaneInclinationJ2000: ar.invPlaneInclinationJ2000,
|
|
491
|
+
longitudePerihelion: ar.longitudePerihelion,
|
|
492
|
+
inclinationCycleAnchor: mp.inclinationCycleAnchor,
|
|
493
|
+
antiPhase: mp.antiPhase || false,
|
|
494
|
+
}, PSI);
|
|
495
|
+
const obliquityMean = FL.computeObliquityMeanSnapshot({
|
|
496
|
+
axialTiltJ2000: ar.axialTiltJ2000,
|
|
497
|
+
invPlaneInclinationAmplitude: il.amplitude,
|
|
498
|
+
perihelionEclipticYears: ecl,
|
|
499
|
+
}, obliquityCycle, { H, t2000 });
|
|
500
|
+
const el = FL.computeEccentricityLaw({
|
|
501
|
+
fibonacciD: mp.fibonacciD,
|
|
502
|
+
massFrac: massFraction[k],
|
|
503
|
+
solarYearInput: ar.solarYearInput,
|
|
504
|
+
orbitalEccentricityJ2000: ar.orbitalEccentricityJ2000,
|
|
505
|
+
antiPhase: mp.antiPhase || false,
|
|
506
|
+
}, {
|
|
507
|
+
kConstant: eccentricityAmplitudeK,
|
|
508
|
+
obliquityMeanDeg: obliquityMean,
|
|
509
|
+
wobblePeriodYears: wobble,
|
|
510
|
+
t2000,
|
|
511
|
+
meanSolarYearDays,
|
|
512
|
+
});
|
|
513
|
+
PLANET_RECORDS[k] = Object.freeze({
|
|
514
|
+
name: mp.name,
|
|
515
|
+
perihelionEclipticYears: ecl,
|
|
516
|
+
longitudePerihelion: ar.longitudePerihelion,
|
|
517
|
+
ascendingNodeCyclesIn8H: mp.ascendingNodeCyclesIn8H,
|
|
518
|
+
ascendingNodePeriod: -(8 * H) / mp.ascendingNodeCyclesIn8H,
|
|
519
|
+
axialPrecessionYears: axial,
|
|
520
|
+
obliquityCycle,
|
|
521
|
+
wobblePeriod: wobble,
|
|
522
|
+
fibonacciD: mp.fibonacciD,
|
|
523
|
+
antiPhase: mp.antiPhase || false,
|
|
524
|
+
ascendingNodeInvPlane: mp.ascendingNodeInvPlane,
|
|
525
|
+
inclinationCycleAnchor: mp.inclinationCycleAnchor,
|
|
526
|
+
invPlaneInclinationJ2000: ar.invPlaneInclinationJ2000,
|
|
527
|
+
invPlaneInclinationAmplitude: il.amplitude,
|
|
528
|
+
invPlaneInclinationMean: il.mean,
|
|
529
|
+
obliquityMean,
|
|
530
|
+
orbitalEccentricityJ2000: ar.orbitalEccentricityJ2000,
|
|
531
|
+
orbitalEccentricityAmplitude: el.amplitude,
|
|
532
|
+
orbitalEccentricityBase: el.base,
|
|
533
|
+
eccentricityPhaseJ2000: el.phaseJ2000,
|
|
534
|
+
solarYearInput: ar.solarYearInput,
|
|
535
|
+
axialTiltJ2000: ar.axialTiltJ2000,
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** Perihelion longitude (linear lattice rate). @param {string} k @param {number} year @returns {number} */
|
|
540
|
+
const planetPerihelionDeg = (k, year) => {
|
|
541
|
+
const p = PLANET_RECORDS[k];
|
|
542
|
+
return (((p.longitudePerihelion + (360.0 * (year - 2000)) / p.perihelionEclipticYears) % 360) + 360) % 360;
|
|
543
|
+
};
|
|
544
|
+
/** Ascending node on the invariable plane. @param {string} k @param {number} year @returns {number} */
|
|
545
|
+
const planetAscNodeDeg = (k, year) => {
|
|
546
|
+
const p = PLANET_RECORDS[k];
|
|
547
|
+
return planetOrientation.ascendingNodeInvPlaneLinearAt({
|
|
548
|
+
ascendingNodeInvPlane: p.ascendingNodeInvPlane,
|
|
549
|
+
ascendingNodePeriod: p.ascendingNodePeriod,
|
|
550
|
+
perihelionEclipticYears: p.perihelionEclipticYears,
|
|
551
|
+
}, year);
|
|
552
|
+
};
|
|
553
|
+
/** Invariable-plane inclination (signed ICRF rate, scene year→JD axis). @param {string} k @param {number} year @returns {number} */
|
|
554
|
+
const planetInclinationDeg = (k, year) => {
|
|
555
|
+
const p = PLANET_RECORDS[k];
|
|
556
|
+
const jd = startmodelJD + (year - startmodelYear) * meanSolarYearDays;
|
|
557
|
+
const yearsSinceBalanced = (jd - balancedJD) / meanSolarYearDays;
|
|
558
|
+
return planetOrientation.invPlaneInclinationAt({
|
|
559
|
+
isEarth: false,
|
|
560
|
+
invPlaneInclinationJ2000: p.invPlaneInclinationJ2000,
|
|
561
|
+
invPlaneInclinationMean: p.invPlaneInclinationMean,
|
|
562
|
+
invPlaneInclinationAmplitude: p.invPlaneInclinationAmplitude,
|
|
563
|
+
inclinationCycleAnchor: p.inclinationCycleAnchor,
|
|
564
|
+
longitudePerihelion: p.longitudePerihelion,
|
|
565
|
+
perihelionEclipticYears: p.perihelionEclipticYears,
|
|
566
|
+
antiPhase: p.antiPhase,
|
|
567
|
+
}, yearsSinceBalanced, {
|
|
568
|
+
H,
|
|
569
|
+
yearsFromBalancedToJ2000: (startmodelJD - balancedJD) / meanSolarYearDays,
|
|
570
|
+
});
|
|
571
|
+
};
|
|
572
|
+
|
|
402
573
|
// ── Time axis: exact JD ↔ model-year conversion ───────────────────────────
|
|
403
574
|
// The model's `year` inputs live on the SI axis (the axis the fits were
|
|
404
575
|
// anchored on — tools/lib `_jdToSIyear`): linear in SI 86400-s days from the
|
|
@@ -412,6 +583,236 @@ export function assembleModel(C, F) {
|
|
|
412
583
|
/** JD(TT) at a model year (SI axis). @param {number} year @returns {number} */
|
|
413
584
|
const jdFromYear = (year) => startmodelJD + (year - startModelYearWithCorrection) * siTropicalYearDays;
|
|
414
585
|
|
|
586
|
+
// ── Lunar theory: the shared-chain assembly (§7a slice-2b) ────────────────
|
|
587
|
+
// Wiring mirrors the engine call sites exactly — tools/lib/deep-time.js
|
|
588
|
+
// (ecc channel, month chain, J2000 precession anchors),
|
|
589
|
+
// tools/lib/scene-graph.js (chain-cycles, arguments, series) and
|
|
590
|
+
// tools/verify/eclipse-audit.js (finders). Deep-time and framework-native
|
|
591
|
+
// are hardwired ON here: they are the shipped defaults; the A/B env
|
|
592
|
+
// toggles (SG_DEEP_TIME/MOON_ARGS_PURE_MEEUS) stay an engine concern.
|
|
593
|
+
const j2000JD = 2451545.0;
|
|
594
|
+
const julianCenturyDays = 36525;
|
|
595
|
+
|
|
596
|
+
// J2000 Moon precession anchors (Option C+ — of-date observational anchors
|
|
597
|
+
// in the legacy-'ICRF'-named inputs; the E values are star-referenced ∓13)
|
|
598
|
+
const nApsidalIJ2000 = Math.round((8 * totalDaysInH) / C.moonReference.moonApsidalPrecessionDaysInputICRF) / 8;
|
|
599
|
+
const nNodalIJ2000 = Math.round((8 * totalDaysInH) / C.moonReference.moonNodalPrecessionDaysInputICRF) / 8;
|
|
600
|
+
const nApsidalEJ2000 = nApsidalIJ2000 - 13;
|
|
601
|
+
const nNodalEJ2000 = nNodalIJ2000 + 13;
|
|
602
|
+
const moonApsidalJ2000Seconds = (totalDaysInH / nApsidalEJ2000) * meanLengthOfDay;
|
|
603
|
+
const moonNodalJ2000Seconds = (totalDaysInH / nNodalEJ2000) * meanLengthOfDay;
|
|
604
|
+
const moonSiderealMonthJ2000Seconds = moonSiderealMonth * meanLengthOfDay;
|
|
605
|
+
|
|
606
|
+
// 8H-lattice derived months (constants.js §Moon derived months)
|
|
607
|
+
const nSid = Math.round((8 * totalDaysInH) / moonSiderealMonthInput) / 8;
|
|
608
|
+
const moonTropicalMonthDays = totalDaysInH / (nSid + 13);
|
|
609
|
+
const moonAnomalisticMonthDays = totalDaysInH / (nSid - nApsidalEJ2000);
|
|
610
|
+
const moonSynodicMonthDays = totalDaysInH / (nSid + 13 - H);
|
|
611
|
+
|
|
612
|
+
// The framework H/3 eccentricity line (the Moon channel's view of the
|
|
613
|
+
// wobble movement — NOT the H/16 orbital eccentricity above)
|
|
614
|
+
const moonEcc = createMoonEccChannel({
|
|
615
|
+
cyclesBetween,
|
|
616
|
+
eccentricityBase,
|
|
617
|
+
perihelionLongitudeJ2000Deg: C.earthOrbital.earthPerihelionLongitudeJ2000,
|
|
618
|
+
inclinationCycleAnchorDeg: C.earthOrbital.earthInclinationCycleAnchor,
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
// Layer-2 month/precession chain (Brouwer-Clemence m² scaling × the
|
|
622
|
+
// e_E-line modulation)
|
|
623
|
+
const moonChain = createMoonMonthChain({
|
|
624
|
+
constants: {
|
|
625
|
+
aMoonNowMetres: EPOCH_PARAMS.moonDistanceNowM,
|
|
626
|
+
alpha1PerMa: EPOCH_PARAMS.alpha1PerMa,
|
|
627
|
+
alpha3PerMa3: EPOCH_PARAMS.alpha3PerMa3,
|
|
628
|
+
alpha4PerMa4: EPOCH_PARAMS.alpha4PerMa4,
|
|
629
|
+
gmEarthMoonM3PerS2: EPOCH_PARAMS.gmEarthMoonM3S2,
|
|
630
|
+
massRatioEarthMoon: MASS_RATIO_EARTH_MOON,
|
|
631
|
+
moonSiderealMonthInputDays: moonSiderealMonthInput,
|
|
632
|
+
holisticYearJ2000: H,
|
|
633
|
+
meanSiderealYearJ2000Seconds: meanSiderealYearSeconds,
|
|
634
|
+
nApsidalOfDateJ2000: nApsidalIJ2000,
|
|
635
|
+
nNodalOfDateJ2000: nNodalIJ2000,
|
|
636
|
+
moonApsidalJ2000Seconds,
|
|
637
|
+
moonNodalJ2000Seconds,
|
|
638
|
+
moonSiderealMonthJ2000Seconds,
|
|
639
|
+
sPerigee: MOON_ECC_SENSITIVITY_PERIGEE,
|
|
640
|
+
sNode: MOON_ECC_SENSITIVITY_NODE,
|
|
641
|
+
},
|
|
642
|
+
fns: {
|
|
643
|
+
meanLodSecondsAtAge: /** @param {number} tMa */ (tMa) => deepLod.lodSecondsAtAge(tMa),
|
|
644
|
+
meanSiderealYearSecondsAtAge: /** @param {number} tMa */ (tMa) => deepLod.siderealYearSecondsAtAge(tMa),
|
|
645
|
+
meanHAtAge: /** @param {number} tMa */ (tMa) => deepLod.hAtAge(tMa),
|
|
646
|
+
modulation: /** @param {number} tMa @param {number} s */ (tMa, s) => moonEcc.modulation(tMa, s),
|
|
647
|
+
distanceMetresAtAge: moonDistanceMetresAtAge,
|
|
648
|
+
},
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
// Chain-cycle integrator. S5/S12 conventions: age anchor = startmodelYear
|
|
652
|
+
// (the scene's t_Ma convention), grid anchor C(2000) = 0 — grid anchor ≠
|
|
653
|
+
// age anchor, deliberately. One stable period fn per chain so the shared
|
|
654
|
+
// Float64Array tables key correctly and persist.
|
|
655
|
+
const chainCycles = createChainCycleIntegrator({
|
|
656
|
+
ageAnchorYear: startmodelYear,
|
|
657
|
+
tropicalYearSecondsAtAge: /** @param {number} tMa */ (tMa) => deepLod.tropicalYearSecondsAtAge(tMa),
|
|
658
|
+
tropicalYearJ2000Seconds: meanTropicalYearJ2000Seconds,
|
|
659
|
+
isDeepTime: () => true,
|
|
660
|
+
});
|
|
661
|
+
/** @param {number} tMa @returns {number|null} */
|
|
662
|
+
const nodalMonthPeriodFn = (tMa) => moonChain.nodalMonthSecondsAtAge(tMa);
|
|
663
|
+
/** @param {number} tMa @returns {number|null} */
|
|
664
|
+
const tropicalMonthPeriodFn = (tMa) => moonChain.tropicalMonthSecondsAtAge(tMa);
|
|
665
|
+
/** @param {number} tMa @returns {number|null} */
|
|
666
|
+
const anomalisticMonthPeriodFn = (tMa) => moonChain.anomalisticMonthSecondsAtAge(tMa);
|
|
667
|
+
const jupiterT0Seconds = C.planetOrbitalElements.jupiter.solarYearInput * 86400;
|
|
668
|
+
/** @param {number} tMa @returns {number} */
|
|
669
|
+
const jupiterPeriodFn = (tMa) => driver2PeriodSecondsAtAge(tMa, jupiterT0Seconds, EPOCH_PARAMS.solarMassLossFracPerYear);
|
|
670
|
+
/** @param {number} a @param {number} b @returns {number|null} */
|
|
671
|
+
const mcDraconic = (a, b) => chainCycles.cyclesBetween(nodalMonthPeriodFn, a, b);
|
|
672
|
+
/** @param {number} a @param {number} b @returns {number|null} */
|
|
673
|
+
const mcTropical = (a, b) => chainCycles.cyclesBetween(tropicalMonthPeriodFn, a, b);
|
|
674
|
+
/** @param {number} a @param {number} b @returns {number|null} */
|
|
675
|
+
const mcAnomalistic = (a, b) => chainCycles.cyclesBetween(anomalisticMonthPeriodFn, a, b);
|
|
676
|
+
/** @param {number} a @param {number} b @returns {number|null} */
|
|
677
|
+
const mcJupiter = (a, b) => chainCycles.cyclesBetween(jupiterPeriodFn, a, b);
|
|
678
|
+
/** @param {number} a @param {number} b @returns {number|null} */
|
|
679
|
+
const mcApsidalOfDate = (a, b) => {
|
|
680
|
+
const t = mcTropical(a, b), n = mcAnomalistic(a, b);
|
|
681
|
+
return (t === null || n === null) ? null : t - n;
|
|
682
|
+
};
|
|
683
|
+
/** @param {number} a @param {number} b @returns {number|null} */
|
|
684
|
+
const mcNodalOfDate = (a, b) => {
|
|
685
|
+
const dr = mcDraconic(a, b), t = mcTropical(a, b);
|
|
686
|
+
return (dr === null || t === null) ? null : dr - t;
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
// Snapshot-phase obliquity — the engine's computeObliquityEarth convention
|
|
690
|
+
// for the lunar chain (linear H-lattice phase; orbital-engine.js). NOT the
|
|
691
|
+
// integrated-phase display obliquity above — the chain was certified
|
|
692
|
+
// against this form.
|
|
693
|
+
/** @param {number} year @returns {number} */
|
|
694
|
+
const obliquitySnapshotDeg = (year) => {
|
|
695
|
+
const t = year - balancedYear;
|
|
696
|
+
let obliq = solsticeObliquityMean;
|
|
697
|
+
for (const [div, sinC, cosC] of F.SOLSTICE_OBLIQUITY_HARMONICS) {
|
|
698
|
+
const ph = (2 * Math.PI * t) / (H / div);
|
|
699
|
+
obliq += sinC * Math.sin(ph) + cosC * Math.cos(ph);
|
|
700
|
+
}
|
|
701
|
+
return obliq;
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
// The argument skeleton (the _FW_MOON bundle; Sun secular deviations on
|
|
705
|
+
// the CALENDAR year coordinate — S3)
|
|
706
|
+
const moonArgs = createMoonArguments({
|
|
707
|
+
constants: {
|
|
708
|
+
j2000JD,
|
|
709
|
+
julianCenturyDays,
|
|
710
|
+
holisticYearJ2000: H,
|
|
711
|
+
balancedYearJ2000: balancedYear,
|
|
712
|
+
meanSolarYearDays,
|
|
713
|
+
meanAnomalisticYearDays,
|
|
714
|
+
tropicalYearHarmonics: F.TROPICAL_YEAR_HARMONICS,
|
|
715
|
+
anomalisticYearHarmonics: F.ANOMALISTIC_YEAR_HARMONICS,
|
|
716
|
+
eccentricityJ2000: C.earthOrbital.earthEccentricityJ2000,
|
|
717
|
+
eccentricityDotJ2000: C.earthOrbital.earthEccentricityDotJ2000,
|
|
718
|
+
eccentricityDotDotJ2000: C.earthOrbital.earthEccentricityDotDotJ2000,
|
|
719
|
+
elpEarthFigureJ2ArcsecPerCy2: C.moonMeeus.elpW1T2Decomposition_arcsecPerCy2.earthFigureJ2,
|
|
720
|
+
elpGeneralPrecessionPA_T2ArcsecPerCy2: C.moonMeeus.elpW1T2Decomposition_arcsecPerCy2.generalPrecessionPA_T2_Lieske1976,
|
|
721
|
+
eccE0: moonEcc.e0,
|
|
722
|
+
},
|
|
723
|
+
fns: {
|
|
724
|
+
eccAt: /** @param {number} tYr */ (tYr) => moonEcc.eccAt(tYr),
|
|
725
|
+
channelIntegral: /** @param {number} T @param {number} s */ (T, s) => moonEcc.channelIntegral(T, s),
|
|
726
|
+
computeObliquityEarth: obliquitySnapshotDeg,
|
|
727
|
+
jdToSIyear: yearFromJD,
|
|
728
|
+
tropicalOrbitsBetween: mcTropical,
|
|
729
|
+
apsidalOfDateCyclesBetween: mcApsidalOfDate,
|
|
730
|
+
nodalOfDateCyclesBetween: mcNodalOfDate,
|
|
731
|
+
cyclesBetween,
|
|
732
|
+
isDeepTime: () => true,
|
|
733
|
+
isFrameworkNative: () => true,
|
|
734
|
+
},
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
// UT→TT on the CALENDAR decimal-year coordinate (script.js Phase 9.16 —
|
|
738
|
+
// a linear-year approximation here once cost ~5–6 s of ΔT and ~1e-3° of
|
|
739
|
+
// Moon longitude at the Babylonian epochs)
|
|
740
|
+
/** @param {number} jd @returns {number} */
|
|
741
|
+
const jdTTFromUT = (jd) => {
|
|
742
|
+
const tMa = (startmodelYear - jdToDecimalYear(jd)) / 1e6;
|
|
743
|
+
const dT = meanDeltaTSecondsAtAge(tMa);
|
|
744
|
+
return Number.isFinite(dT) ? jd + dT / 86400 : jd;
|
|
745
|
+
};
|
|
746
|
+
// ΔT at a JD on the CALENDAR-year axis (mirrors deep-time frameworkDeltaT;
|
|
747
|
+
// NOT deltaTSeconds above, which adds the deltaTStart anchor)
|
|
748
|
+
/** @param {number} jd @returns {number} */
|
|
749
|
+
const frameworkDeltaTSecondsAtJD = (jd) => {
|
|
750
|
+
const decYear = startmodelYear + (jd - startmodelJD) / meanSolarYearDays;
|
|
751
|
+
const dT = meanDeltaTSecondsAtAge((startmodelYear - decYear) / 1e6);
|
|
752
|
+
return Number.isFinite(dT) ? dT : 0;
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
// Bounded Meeus E-factor from the H/3 line (framework-native branch only;
|
|
756
|
+
// the pure-Meeus polynomial A/B branch stays engine-local)
|
|
757
|
+
/** @param {number} dDays @returns {number} */
|
|
758
|
+
const fwEFactor = (dDays) => moonEcc.eFactorAt(dDays / C.foundational.inputmeanlengthsolaryearindays);
|
|
759
|
+
|
|
760
|
+
// D2 derived additional-argument rates (deg/cy, J2000 8H-lattice months;
|
|
761
|
+
// record: tools/explore/derive-a1a2a3.js)
|
|
762
|
+
const fwA2RateDegPerCy = 2 * ((360 * 36525) / moonTropicalMonthDays)
|
|
763
|
+
- (360 * 36525) / moonAnomalisticMonthDays
|
|
764
|
+
- 2 * ((360 * 36525) / C.planetOrbitalElements.jupiter.solarYearInput);
|
|
765
|
+
const fwA3RateDegPerCy = (360 * 36525) / moonSiderealMonth;
|
|
766
|
+
|
|
767
|
+
// Meeus Ch. 47 truncated series (framework-native arguments + E-factor)
|
|
768
|
+
const moonSeries = createMoonSeries({
|
|
769
|
+
constants: {
|
|
770
|
+
moonL: F.MEEUS_LONGITUDE_TERMS,
|
|
771
|
+
moonB: F.MEEUS_LATITUDE_TERMS,
|
|
772
|
+
j2000JD,
|
|
773
|
+
julianCenturyDays,
|
|
774
|
+
moonMeeusLpCorrectionDeg: C.moon.moonMeeusLpCorrection,
|
|
775
|
+
fwA2RateDegPerCy,
|
|
776
|
+
fwA3RateDegPerCy,
|
|
777
|
+
},
|
|
778
|
+
fns: {
|
|
779
|
+
argsAt: /** @param {number} jdTT */ (jdTT) => moonArgs.argsAt(jdTT),
|
|
780
|
+
eFactorForD: fwEFactor,
|
|
781
|
+
eFactorAtJdTT: /** @param {number} jdTT */ (jdTT) => fwEFactor(jdTT - j2000JD),
|
|
782
|
+
getMoonDistanceKm: () => moonDistanceKm,
|
|
783
|
+
getEccentricityBase: () => C.moonReference.moonOrbitalEccentricityBase,
|
|
784
|
+
deltaTSeconds: /** @param {number} jd */ (jd) => (jdTTFromUT(jd) - jd) * 86400,
|
|
785
|
+
jdToSIyear: yearFromJD,
|
|
786
|
+
tropicalOrbitsBetween: mcTropical,
|
|
787
|
+
apsidalOfDateCyclesBetween: mcApsidalOfDate,
|
|
788
|
+
cyclesBetween,
|
|
789
|
+
jupiterOrbitsBetween: mcJupiter,
|
|
790
|
+
isDeepTime: () => true,
|
|
791
|
+
isFrameworkNative: () => true,
|
|
792
|
+
},
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
// Eclipse finders — wired like the engine probe (tools/verify/
|
|
796
|
+
// eclipse-audit.js). The finder axis is JD(UT): the series wrapper applies
|
|
797
|
+
// UT→TT internally. Ground-track/umbra paths deliberately absent: the
|
|
798
|
+
// scene-umbra projection navigates the Tychosium-derived scaffold, which
|
|
799
|
+
// never enters this package (§2h).
|
|
800
|
+
const eclipseFinders = createEclipseFinders({
|
|
801
|
+
moonLonDegAt: /** @param {number} jd */ (jd) => moonSeries.truncatedLonDeg(jd),
|
|
802
|
+
moonBetaDegAt: /** @param {number} jd */ (jd) => moonSeries.truncatedBetaDeg(jd),
|
|
803
|
+
moonDistanceKmAt: /** @param {number} jd */ (jd) => moonSeries.truncatedDistanceKm(jd),
|
|
804
|
+
deltaTSecondsAt: frameworkDeltaTSecondsAtJD,
|
|
805
|
+
getSynodicMonthDays: () => moonSynodicMonthDays,
|
|
806
|
+
getSunDistanceKm: () => currentAUDistance,
|
|
807
|
+
constants: {
|
|
808
|
+
rEarthMetres: (C.bodyDiametersKm.earth / 2) * 1000,
|
|
809
|
+
moonDiameterKm: C.bodyDiametersKm.moon,
|
|
810
|
+
sunDiameterKm: C.bodyDiametersKm.sun,
|
|
811
|
+
j2000JD,
|
|
812
|
+
julianCenturyDays,
|
|
813
|
+
},
|
|
814
|
+
});
|
|
815
|
+
|
|
415
816
|
// ── The assembled surface ─────────────────────────────────────────────────
|
|
416
817
|
return Object.freeze({
|
|
417
818
|
time: Object.freeze({
|
|
@@ -458,6 +859,28 @@ export function assembleModel(C, F) {
|
|
|
458
859
|
moon: Object.freeze({
|
|
459
860
|
distanceKmAtYear: /** @param {number} year @returns {number} */ (year) => moonDistanceMetresAtAge(yearToTMa(year)) / 1000,
|
|
460
861
|
siderealMonthDaysAtYear: moonSiderealMonthDaysAt,
|
|
862
|
+
synodicMonthDays: moonSynodicMonthDays,
|
|
863
|
+
// The apparent-position chain (truncated Meeus Ch. 47 series on
|
|
864
|
+
// framework-native arguments). JD(UT) axis — UT→TT applied internally.
|
|
865
|
+
lonDegAtJD: /** @param {number} jd @returns {number} */ (jd) => moonSeries.truncatedLonDeg(jd),
|
|
866
|
+
betaDegAtJD: /** @param {number} jd @returns {number} */ (jd) => moonSeries.truncatedBetaDeg(jd),
|
|
867
|
+
distanceKmAtJD: /** @param {number} jd @returns {number} */ (jd) => moonSeries.truncatedDistanceKm(jd),
|
|
868
|
+
}),
|
|
869
|
+
eclipse: Object.freeze({
|
|
870
|
+
sunLonDegAtJD: /** @param {number} jd @returns {number} */ (jd) => eclipseFinders.sunLonDegAt(jd),
|
|
871
|
+
findLunarInRange: /** @param {number} jdStart @param {number} jdEnd */ (jdStart, jdEnd) => eclipseFinders.findLunarEclipsesInRange(jdStart, jdEnd),
|
|
872
|
+
findSolarInRange: /** @param {number} jdStart @param {number} jdEnd */ (jdStart, jdEnd) => eclipseFinders.findSolarEclipsesInRange(jdStart, jdEnd),
|
|
873
|
+
deltaTSecondsAtJD: frameworkDeltaTSecondsAtJD,
|
|
874
|
+
}),
|
|
875
|
+
climate: Object.freeze({
|
|
876
|
+
l1OrbitalPermil: evalClimateL1,
|
|
877
|
+
}),
|
|
878
|
+
planets: Object.freeze({
|
|
879
|
+
keys: Object.freeze([...PLANET_KEYS]),
|
|
880
|
+
record: /** @param {string} k @returns {Record<string, any>|undefined} */ (k) => PLANET_RECORDS[k],
|
|
881
|
+
perihelionLongitudeDeg: planetPerihelionDeg,
|
|
882
|
+
ascendingNodeInvPlaneDeg: planetAscNodeDeg,
|
|
883
|
+
invPlaneInclinationDeg: planetInclinationDeg,
|
|
461
884
|
}),
|
|
462
885
|
});
|
|
463
886
|
}
|
package/src/moon/month-chain.cjs
CHANGED
|
@@ -67,10 +67,14 @@
|
|
|
67
67
|
* meanSiderealYearSecondsAtAge: (tMa: number) => number,
|
|
68
68
|
* meanHAtAge: (tMa: number) => (number | null),
|
|
69
69
|
* modulation: (tMa: number, s: number) => number,
|
|
70
|
+
* distanceMetresAtAge?: (tMa: number) => number,
|
|
70
71
|
* },
|
|
71
72
|
* }} deps — fns are the ENGINE'S OWN layer-0/1 evaluators and the shared
|
|
72
73
|
* eccentricity channel's modulation; constants are the engine's derived
|
|
73
74
|
* J2000 anchors (both engines derive them from the same JSON).
|
|
75
|
+
* fns.distanceMetresAtAge, when present, OVERRIDES the internal quartic —
|
|
76
|
+
* the Driver-1½ regime-aware history (recession-history.cjs); absent, the
|
|
77
|
+
* internal quartic is the pure twin (bit-identical for t ≤ jointMa).
|
|
74
78
|
*/
|
|
75
79
|
function createMoonMonthChain({ constants, fns }) {
|
|
76
80
|
const {
|
|
@@ -85,10 +89,11 @@ function createMoonMonthChain({ constants, fns }) {
|
|
|
85
89
|
|
|
86
90
|
/** Farhat α₁/α₃/α₄ polynomial — Earth–Moon distance in metres at age t_Ma.
|
|
87
91
|
* @param {number} tMa @returns {number} */
|
|
88
|
-
function
|
|
92
|
+
function quarticMetresAtAge(tMa) {
|
|
89
93
|
const t = tMa;
|
|
90
94
|
return aMoonNowMetres * (1 + alpha1PerMa * t + alpha3PerMa3 * t * t * t + alpha4PerMa4 * t * t * t * t);
|
|
91
95
|
}
|
|
96
|
+
const distanceMetresAtAge = fns.distanceMetresAtAge ?? quarticMetresAtAge;
|
|
92
97
|
|
|
93
98
|
/** @param {number} tMa @returns {number} */
|
|
94
99
|
function distanceKmAtAge(tMa) {
|