@essrt/physics 1.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 +52 -0
- package/package.json +58 -0
- package/src/cardinal/index.cjs +323 -0
- package/src/chain-cycles/index.cjs +189 -0
- package/src/climate/l1-orbital.cjs +46 -0
- package/src/constants/coefficients.js +52 -0
- package/src/constants/generated.d.ts +710 -0
- package/src/constants/generated.js +868 -0
- package/src/constants/index.js +30 -0
- package/src/deltat/cycles.cjs +256 -0
- package/src/deltat/deep-time.cjs +266 -0
- package/src/deltat/historical.cjs +84 -0
- package/src/eclipse/finders.cjs +311 -0
- package/src/index.js +252 -0
- package/src/layer0/derive-params.js +85 -0
- package/src/layer0/index.js +166 -0
- package/src/layer1/index.js +86 -0
- package/src/layer2/index.js +6 -0
- package/src/moon/apparent.cjs +153 -0
- package/src/moon/arguments.cjs +320 -0
- package/src/moon/ecc-channel.cjs +108 -0
- package/src/moon/month-chain.cjs +251 -0
- package/src/moon/series.cjs +211 -0
- package/src/phase/index.cjs +186 -0
- package/src/planets/asc-node-integrator.cjs +120 -0
- package/src/planets/corrections.cjs +167 -0
- package/src/planets/ecc-channel.cjs +59 -0
- package/src/planets/fibonacci-laws.cjs +136 -0
- package/src/planets/geometry.cjs +145 -0
- package/src/planets/model.cjs +185 -0
- package/src/planets/orbit-chain.cjs +46 -0
- package/src/planets/orientation.cjs +133 -0
- package/src/planets/predict.cjs +324 -0
- package/src/reference/published-curves.cjs +247 -0
- package/src/sun/longitude-correction.cjs +68 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Eclipse geometry — sun longitude + the predictive finders (Phase 8.5).
|
|
3
|
+
*
|
|
4
|
+
* Single-copy extraction from the browser (there was never a Node twin;
|
|
5
|
+
* the moon series beneath is shared since 8.2 — the engines inject their
|
|
6
|
+
* own truncated-series evaluators and ΔT convention).
|
|
7
|
+
*
|
|
8
|
+
* sunLonDegAt — Meeus Ch. 25 low-precision Sun geocentric ecliptic
|
|
9
|
+
* longitude. Accepts JD_UT; converts internally to JD_TT via the injected
|
|
10
|
+
* ΔT. Returns MEAN GEOMETRIC longitude, NOT apparent — LOAD-BEARING: the
|
|
11
|
+
* framework's SUN_HARMONICS was fitted against observed eclipses that
|
|
12
|
+
* already include aberration/nutation, so adding raw aberration here
|
|
13
|
+
* double-counted the correction (measured 2026-07: 2024 Dallas 12→24 km
|
|
14
|
+
* worse). Upgrading to apparent longitude requires refitting
|
|
15
|
+
* SUN_HARMONICS with the correction active.
|
|
16
|
+
*
|
|
17
|
+
* findLunarEclipsesInRange — per-opposition shadow geometry (the Danjon
|
|
18
|
+
* ~2% atmospheric enlargement, as used by the NASA Lunar Canon and Meeus
|
|
19
|
+
* Ch. 54 — the pure geometric shadow under-classifies borderline events,
|
|
20
|
+
* e.g. 2021-05-26), zero-crossing + 40-step bisection to ~1 s.
|
|
21
|
+
*
|
|
22
|
+
* findSolarEclipsesInRange — per-conjunction geometry (topocentric Moon
|
|
23
|
+
* disk distinguishes total from annular), then refinement from
|
|
24
|
+
* longitude-conjunction to MINIMUM γ — NASA's "greatest eclipse"
|
|
25
|
+
* convention, typically 5–15 min apart.
|
|
26
|
+
*
|
|
27
|
+
* Sun distance and the synodic month arrive as GETTERS (both are
|
|
28
|
+
* epoch-mutable engine globals). Sun-distance variation over the orbit
|
|
29
|
+
* (~0.5%) is currently neglected — refinement recorded (Phase L-4).
|
|
30
|
+
*
|
|
31
|
+
* The scene-umbra conventions (piercing point / NASA radial projection)
|
|
32
|
+
* are NOT here: they navigate the THREE scene through the Tychosium-
|
|
33
|
+
* derived scaffold, which never enters this package (§2h).
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
'use strict';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @typedef {Object} EclipseFinderDeps
|
|
40
|
+
* @property {(jd: number) => number} moonLonDegAt - truncated-series ecliptic longitude
|
|
41
|
+
* @property {(jd: number) => number} moonBetaDegAt - truncated-series ecliptic latitude
|
|
42
|
+
* @property {(jd: number) => number} moonDistanceKmAt - truncated-series distance
|
|
43
|
+
* @property {(jd: number) => number} deltaTSecondsAt - the engine's ΔT convention
|
|
44
|
+
* @property {() => number} getSynodicMonthDays - live (epoch-mutable)
|
|
45
|
+
* @property {() => number} getSunDistanceKm - live (epoch-mutable)
|
|
46
|
+
* @property {{ rEarthMetres: number, moonDiameterKm: number,
|
|
47
|
+
* sunDiameterKm: number, j2000JD: number, julianCenturyDays: number }} constants
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/** @param {EclipseFinderDeps} deps */
|
|
51
|
+
function createEclipseFinders(deps) {
|
|
52
|
+
const K = deps.constants;
|
|
53
|
+
|
|
54
|
+
/** Sun's geocentric ecliptic longitude in degrees (0–360) at given JD_UT.
|
|
55
|
+
* MEAN GEOMETRIC — see the module header. @param {number} jd @returns {number} */
|
|
56
|
+
function sunLonDegAt(jd) {
|
|
57
|
+
const _d2r = Math.PI / 180;
|
|
58
|
+
const T = (jd + deps.deltaTSecondsAt(jd) / 86400 - K.j2000JD) / K.julianCenturyDays;
|
|
59
|
+
const L0 = 280.46646 + 36000.76983 * T + 0.0003032 * T * T;
|
|
60
|
+
const M = (357.52911 + 35999.05029 * T - 0.0001537 * T * T) * _d2r;
|
|
61
|
+
const C = (1.914602 - 0.004817 * T - 0.000014 * T * T) * Math.sin(M)
|
|
62
|
+
+ (0.019993 - 0.000101 * T) * Math.sin(2 * M)
|
|
63
|
+
+ 0.000289 * Math.sin(3 * M);
|
|
64
|
+
return ((L0 + C) % 360 + 360) % 360;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Find all lunar-eclipse-class oppositions in [jdStart, jdEnd].
|
|
69
|
+
* @param {number} jdStart @param {number} jdEnd
|
|
70
|
+
* @returns {Array<{jd: number, beta: number, moonDistance_km: number,
|
|
71
|
+
* type: string, magnitudeUmbral: number, magnitudePenumbral: number}>}
|
|
72
|
+
*/
|
|
73
|
+
function findLunarEclipsesInRange(jdStart, jdEnd) {
|
|
74
|
+
// Step size: small fraction of synodic month for reliable zero-crossing
|
|
75
|
+
// detection (~6° diff change per step, well clear of the 30° wrap filter).
|
|
76
|
+
const STEP_DAYS = deps.getSynodicMonthDays() / 60;
|
|
77
|
+
|
|
78
|
+
// Per-event-distance geometry — the shadow angular radii are computed
|
|
79
|
+
// PER OPPOSITION using each event's Moon distance (perigee vs apogee
|
|
80
|
+
// varies the threshold by ~5.5%, which dominates type-classification
|
|
81
|
+
// at boundary cases).
|
|
82
|
+
const _rad2deg = 180 / Math.PI;
|
|
83
|
+
const R_EARTH_KM = K.rEarthMetres / 1000;
|
|
84
|
+
const R_MOON_KM = K.moonDiameterKm / 2;
|
|
85
|
+
const R_SUN_KM = K.sunDiameterKm / 2;
|
|
86
|
+
const D_SUN_KM = deps.getSunDistanceKm();
|
|
87
|
+
const umbraApex_rad = Math.atan((R_SUN_KM - R_EARTH_KM) / D_SUN_KM);
|
|
88
|
+
const penumbraApex_rad = Math.atan((R_SUN_KM + R_EARTH_KM) / D_SUN_KM);
|
|
89
|
+
|
|
90
|
+
// Shadow radii carry the standard ~2% atmospheric enlargement (Danjon
|
|
91
|
+
// rule / Chauvenet's 1/50, as used by the NASA Lunar Canon and Meeus
|
|
92
|
+
// Ch. 54).
|
|
93
|
+
const SHADOW_ENLARGEMENT = 1.02;
|
|
94
|
+
/** @param {number} D_MOON_KM */
|
|
95
|
+
const _shadowGeometry = (D_MOON_KM) => {
|
|
96
|
+
const moonR = Math.atan(R_MOON_KM / D_MOON_KM) * _rad2deg;
|
|
97
|
+
const umbraR = Math.atan((R_EARTH_KM - D_MOON_KM * Math.tan(umbraApex_rad)) / D_MOON_KM) * _rad2deg * SHADOW_ENLARGEMENT;
|
|
98
|
+
const penumR = Math.atan((R_EARTH_KM + D_MOON_KM * Math.tan(penumbraApex_rad)) / D_MOON_KM) * _rad2deg * SHADOW_ENLARGEMENT;
|
|
99
|
+
return {
|
|
100
|
+
moonR, umbraR, penumR,
|
|
101
|
+
totalMax: umbraR - moonR,
|
|
102
|
+
partialMax: umbraR + moonR,
|
|
103
|
+
penumMax: penumR + moonR,
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// Wrapped opposition diff: 0 means Moon at opposition (Sun + 180°)
|
|
108
|
+
/** @param {number} jd */
|
|
109
|
+
const oppDiff = (jd) => {
|
|
110
|
+
let d = deps.moonLonDegAt(jd) - sunLonDegAt(jd) - 180;
|
|
111
|
+
while (d > 180) d -= 360;
|
|
112
|
+
while (d <= -180) d += 360;
|
|
113
|
+
return d;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const results = [];
|
|
117
|
+
let prevJD = jdStart;
|
|
118
|
+
let prevDiff = oppDiff(prevJD);
|
|
119
|
+
|
|
120
|
+
for (let jd = jdStart + STEP_DAYS; jd <= jdEnd; jd += STEP_DAYS) {
|
|
121
|
+
const d = oppDiff(jd);
|
|
122
|
+
// Zero-crossing: sign change from − to + with no wrap discontinuity
|
|
123
|
+
if (prevDiff < 0 && d >= 0 && (d - prevDiff) < 30) {
|
|
124
|
+
// Bisect to refine opposition to ~1-second precision
|
|
125
|
+
let lo = prevJD, hi = jd;
|
|
126
|
+
for (let i = 0; i < 40; i++) {
|
|
127
|
+
const mid = (lo + hi) / 2;
|
|
128
|
+
if (oppDiff(mid) < 0) lo = mid; else hi = mid;
|
|
129
|
+
if (hi - lo < 1 / 86400) break;
|
|
130
|
+
}
|
|
131
|
+
const jdOpp = (lo + hi) / 2;
|
|
132
|
+
const beta = deps.moonBetaDegAt(jdOpp);
|
|
133
|
+
const absB = Math.abs(beta);
|
|
134
|
+
|
|
135
|
+
// Per-event shadow geometry using the actual Moon distance at this
|
|
136
|
+
// opposition — closes the ~5.5% perigee/apogee threshold variation.
|
|
137
|
+
const D_moon_jd = deps.moonDistanceKmAt(jdOpp);
|
|
138
|
+
const G = _shadowGeometry(D_moon_jd);
|
|
139
|
+
|
|
140
|
+
let type = null;
|
|
141
|
+
if (absB <= G.totalMax) type = 'Total';
|
|
142
|
+
else if (absB <= G.partialMax) type = 'Partial';
|
|
143
|
+
else if (absB <= G.penumMax) type = 'Penumbral';
|
|
144
|
+
|
|
145
|
+
if (type) {
|
|
146
|
+
const magUmbral = Math.max(0, (G.partialMax - absB) / (2 * G.moonR));
|
|
147
|
+
const magPenumbral = Math.max(0, (G.penumMax - absB) / (2 * G.moonR));
|
|
148
|
+
results.push({
|
|
149
|
+
jd: jdOpp,
|
|
150
|
+
beta: beta,
|
|
151
|
+
moonDistance_km: D_moon_jd,
|
|
152
|
+
type: type,
|
|
153
|
+
magnitudeUmbral: magUmbral, // 0 for Penumbral-only; (0,1) Partial; ≥1 Total
|
|
154
|
+
magnitudePenumbral: magPenumbral,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
prevDiff = d;
|
|
159
|
+
prevJD = jd;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return results;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Find all solar-eclipse-class conjunctions in [jdStart, jdEnd].
|
|
167
|
+
* Events are timed at MINIMUM γ (NASA "greatest eclipse" convention).
|
|
168
|
+
* @param {number} jdStart @param {number} jdEnd
|
|
169
|
+
* @returns {Array<{jd: number, beta: number, moonDistance_km: number,
|
|
170
|
+
* type: string, moonAppR_topo: number, sunAppR: number, moonSunRatio: number}>}
|
|
171
|
+
*/
|
|
172
|
+
function findSolarEclipsesInRange(jdStart, jdEnd) {
|
|
173
|
+
const STEP_DAYS = deps.getSynodicMonthDays() / 60;
|
|
174
|
+
const _rad2deg = 180 / Math.PI;
|
|
175
|
+
const R_EARTH_KM = K.rEarthMetres / 1000;
|
|
176
|
+
const R_MOON_KM = K.moonDiameterKm / 2;
|
|
177
|
+
const R_SUN_KM = K.sunDiameterKm / 2;
|
|
178
|
+
const D_SUN_KM = deps.getSunDistanceKm();
|
|
179
|
+
const sunAppR = Math.atan(R_SUN_KM / D_SUN_KM) * _rad2deg; // ~0.266°
|
|
180
|
+
|
|
181
|
+
// Per-event geometry at conjunction (Meeus Ch. 54 simplified geocentric
|
|
182
|
+
// form). "Topocentric" Moon size: at the sub-Moon point an observer is
|
|
183
|
+
// 1 Earth-radius closer than the geocenter (ratio ≈ 1.017) — this is
|
|
184
|
+
// what distinguishes total from annular when geocentric Moon ≈ Sun.
|
|
185
|
+
/** @param {number} D_MOON_KM */
|
|
186
|
+
const _solarGeometry = (D_MOON_KM) => {
|
|
187
|
+
const moonAppR_geo = Math.atan(R_MOON_KM / D_MOON_KM) * _rad2deg;
|
|
188
|
+
const moonAppR_topo = Math.atan(R_MOON_KM / (D_MOON_KM - R_EARTH_KM)) * _rad2deg;
|
|
189
|
+
const parallax = Math.atan(R_EARTH_KM / D_MOON_KM) * _rad2deg; // ~0.95°
|
|
190
|
+
return {
|
|
191
|
+
moonAppR_geo, moonAppR_topo, parallax, sunAppR,
|
|
192
|
+
partialLim: sunAppR + moonAppR_geo + parallax,
|
|
193
|
+
centralLim: parallax - Math.abs(moonAppR_topo - sunAppR),
|
|
194
|
+
isTotal: moonAppR_topo > sunAppR,
|
|
195
|
+
};
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// Wrapped conjunction diff: 0 means Moon at conjunction with the Sun
|
|
199
|
+
/** @param {number} jd */
|
|
200
|
+
const conjDiff = (jd) => {
|
|
201
|
+
let d = deps.moonLonDegAt(jd) - sunLonDegAt(jd);
|
|
202
|
+
while (d > 180) d -= 360;
|
|
203
|
+
while (d <= -180) d += 360;
|
|
204
|
+
return d;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// Geocentric γ (Earth-radii): perpendicular distance from Earth's center
|
|
208
|
+
// to the Sun–Moon line. NASA's "greatest eclipse" TD is the moment of
|
|
209
|
+
// MINIMUM γ — typically 5–15 min from longitude conjunction, because at
|
|
210
|
+
// conjunction the Moon may still be approaching in the β direction.
|
|
211
|
+
const _d2r = Math.PI / 180;
|
|
212
|
+
/** @param {number} jd */
|
|
213
|
+
const gammaAtJd = (jd) => {
|
|
214
|
+
const sunLonR = sunLonDegAt(jd) * _d2r;
|
|
215
|
+
const moonLonR = deps.moonLonDegAt(jd) * _d2r;
|
|
216
|
+
const moonBetR = deps.moonBetaDegAt(jd) * _d2r;
|
|
217
|
+
const D_moon = deps.moonDistanceKmAt(jd);
|
|
218
|
+
const D_sun = D_SUN_KM;
|
|
219
|
+
const sX = D_sun * Math.cos(sunLonR), sY = D_sun * Math.sin(sunLonR);
|
|
220
|
+
const cb = Math.cos(moonBetR);
|
|
221
|
+
const mX = D_moon * cb * Math.cos(moonLonR);
|
|
222
|
+
const mY = D_moon * cb * Math.sin(moonLonR);
|
|
223
|
+
const mZ = D_moon * Math.sin(moonBetR);
|
|
224
|
+
const dX = mX - sX, dY = mY - sY, dZ = mZ;
|
|
225
|
+
const dLen = Math.sqrt(dX*dX + dY*dY + dZ*dZ);
|
|
226
|
+
const ux = dX/dLen, uy = dY/dLen, uz = dZ/dLen;
|
|
227
|
+
// Perpendicular distance from origin to the line through Moon along u
|
|
228
|
+
const proj = mX*ux + mY*uy + mZ*uz;
|
|
229
|
+
const perpX = mX - proj*ux, perpY = mY - proj*uy, perpZ = mZ - proj*uz;
|
|
230
|
+
return Math.sqrt(perpX*perpX + perpY*perpY + perpZ*perpZ) / R_EARTH_KM;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// Refine conjunction-JD to true min-γ JD: coarse 1-min scan over ±20 min,
|
|
234
|
+
// then parabolic interpolation on the 3 points around the minimum.
|
|
235
|
+
/** @param {number} jdConj */
|
|
236
|
+
const refineMinGamma = (jdConj) => {
|
|
237
|
+
const stepMin = 1 / (24 * 60);
|
|
238
|
+
let bestJD = jdConj, bestG = gammaAtJd(jdConj);
|
|
239
|
+
for (let dt = -20; dt <= 20; dt++) {
|
|
240
|
+
if (dt === 0) continue;
|
|
241
|
+
const jd = jdConj + dt * stepMin;
|
|
242
|
+
const g = gammaAtJd(jd);
|
|
243
|
+
if (g < bestG) { bestG = g; bestJD = jd; }
|
|
244
|
+
}
|
|
245
|
+
const yL = gammaAtJd(bestJD - stepMin);
|
|
246
|
+
const y0 = bestG;
|
|
247
|
+
const yR = gammaAtJd(bestJD + stepMin);
|
|
248
|
+
const denom = yL - 2*y0 + yR;
|
|
249
|
+
if (Math.abs(denom) > 1e-15) {
|
|
250
|
+
const delta = 0.5 * (yL - yR) / denom; // fractional offset in [-1, +1]
|
|
251
|
+
if (Math.abs(delta) < 1) {
|
|
252
|
+
const refinedJD = bestJD + delta * stepMin;
|
|
253
|
+
const refinedG = gammaAtJd(refinedJD);
|
|
254
|
+
if (refinedG < bestG) return refinedJD;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return bestJD;
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const results = [];
|
|
261
|
+
let prevJD = jdStart;
|
|
262
|
+
let prevDiff = conjDiff(prevJD);
|
|
263
|
+
|
|
264
|
+
for (let jd = jdStart + STEP_DAYS; jd <= jdEnd; jd += STEP_DAYS) {
|
|
265
|
+
const d = conjDiff(jd);
|
|
266
|
+
if (prevDiff < 0 && d >= 0 && (d - prevDiff) < 30) {
|
|
267
|
+
// Bisect to ~1-second precision on conjunction
|
|
268
|
+
let lo = prevJD, hi = jd;
|
|
269
|
+
for (let i = 0; i < 40; i++) {
|
|
270
|
+
const mid = (lo + hi) / 2;
|
|
271
|
+
if (conjDiff(mid) < 0) lo = mid; else hi = mid;
|
|
272
|
+
if (hi - lo < 1 / 86400) break;
|
|
273
|
+
}
|
|
274
|
+
const jdConj = (lo + hi) / 2;
|
|
275
|
+
// Refine to NASA's "greatest eclipse" convention (minimum γ).
|
|
276
|
+
const jdGreatest = refineMinGamma(jdConj);
|
|
277
|
+
const beta = deps.moonBetaDegAt(jdGreatest);
|
|
278
|
+
const absB = Math.abs(beta);
|
|
279
|
+
const D_moon = deps.moonDistanceKmAt(jdGreatest);
|
|
280
|
+
const G = _solarGeometry(D_moon);
|
|
281
|
+
|
|
282
|
+
let type = null;
|
|
283
|
+
if (absB <= G.centralLim) {
|
|
284
|
+
type = G.isTotal ? 'Total' : 'Annular';
|
|
285
|
+
} else if (absB <= G.partialLim) {
|
|
286
|
+
type = 'Partial';
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (type) {
|
|
290
|
+
results.push({
|
|
291
|
+
jd: jdGreatest,
|
|
292
|
+
beta: beta,
|
|
293
|
+
moonDistance_km: D_moon,
|
|
294
|
+
type: type,
|
|
295
|
+
moonAppR_topo: G.moonAppR_topo,
|
|
296
|
+
sunAppR: G.sunAppR,
|
|
297
|
+
moonSunRatio: G.moonAppR_topo / G.sunAppR,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
prevDiff = d;
|
|
302
|
+
prevJD = jd;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return results;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return { sunLonDegAt, findLunarEclipsesInRange, findSolarEclipsesInRange };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
module.exports = { createEclipseFinders };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @essrt/physics — the pure physics core.
|
|
3
|
+
*
|
|
4
|
+
* PHASE 2 SKELETON. No physics has moved yet; `script.js` remains the
|
|
5
|
+
* implementation. This file exists so the architecture rules have something to
|
|
6
|
+
* enforce before code arrives — that ordering is the whole point of Phase 2.
|
|
7
|
+
*
|
|
8
|
+
* Three rules this package must never break (IP-unified-architecture.md §2b, §2h):
|
|
9
|
+
*
|
|
10
|
+
* 1. It imports NOTHING external. Not `three`, not `fs`, not `next`, not
|
|
11
|
+
* `document`, not `process`. Language built-ins only.
|
|
12
|
+
* 2. Constants are INJECTED, never imported. A `import { H } from './constants'`
|
|
13
|
+
* inside a layer is a design defect, not a shortcut — it makes
|
|
14
|
+
* counterfactuals impossible, which is the one capability DE440 and Laskar
|
|
15
|
+
* cannot offer.
|
|
16
|
+
* 3. No scene-graph scaffold. `containerObj`, `pivotObj`, `planetObj`,
|
|
17
|
+
* `orbitCentera/b/c`, `orbitTilta/b`, `startPos` are Tychosium-derived (GPL)
|
|
18
|
+
* and belong to `simulator`. Nothing GPL-derived may be reachable from here,
|
|
19
|
+
* because this package is the one that can be licensed commercially.
|
|
20
|
+
*
|
|
21
|
+
* All three are enforced: `npm run lint` and `npm run check:boundaries`.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { DEFAULT_CONSTANTS as GENERATED, CONSTANTS_HASH, REFERENCE_DATA } from './constants/index.js';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Keys `createModel` refuses. Derived from REFERENCE_DATA rather than listed by
|
|
28
|
+
* hand, so classifying a new block in the generator automatically protects it —
|
|
29
|
+
* a hand-written list would drift the moment someone added a bound.
|
|
30
|
+
* @type {string[]}
|
|
31
|
+
*/
|
|
32
|
+
const NEVER_INJECTABLE = Object.keys(REFERENCE_DATA);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @typedef {Record<string, unknown> & { hash?: string }} Constants
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @typedef {Object} Model
|
|
40
|
+
* @property {Constants} constants the resolved context, frozen
|
|
41
|
+
* @property {string} hash identifies this context; differs for a counterfactual
|
|
42
|
+
* @property {() => {axialPrecessionPeriodYears: number, inclinationPrecessionPeriodYears: number, perihelionPrecessionPeriodYears: number}} computeLatticePeriodsYears
|
|
43
|
+
* @property {(year: number) => number} eccentricity
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Generated at build time from `public/input/{model-parameters,astro-reference}.json`
|
|
48
|
+
* (§2g) by `tools/constants/generate.mjs`. 324 values across 15 blocks.
|
|
49
|
+
*
|
|
50
|
+
* Free parameters and measured anchors only. Validation targets are excluded by
|
|
51
|
+
* the generator's CLASSIFICATION map, so a counterfactual cannot move the
|
|
52
|
+
* goalposts it is judged by (§2d).
|
|
53
|
+
* @type {Constants}
|
|
54
|
+
*/
|
|
55
|
+
export { GENERATED as DEFAULT_CONSTANTS, CONSTANTS_HASH };
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Validation targets and presentation data. Single-sourced so nothing keeps a
|
|
59
|
+
* duplicate copy, but NOT part of the model context — `createModel` refuses
|
|
60
|
+
* these keys (§2d).
|
|
61
|
+
*/
|
|
62
|
+
export { REFERENCE_DATA };
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Fitting-pipeline output at full precision, with its own hash (§2j).
|
|
66
|
+
* Not part of the injectable context: a counterfactual perturbs the parameters
|
|
67
|
+
* we chose, not the 2,400-term output of a fit.
|
|
68
|
+
*/
|
|
69
|
+
export { FITTED_COEFFICIENTS, COEFFICIENTS_HASH } from './constants/index.js';
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Phase 6 surface — the epoch layer. `createEpochPrimitives` + `deriveEpochParams`
|
|
73
|
+
* are what `src/script.js` and `tools/lib/deep-time.js` converge on: one
|
|
74
|
+
* derivation of the parameter bundle, one implementation of the chain. The
|
|
75
|
+
* browser imports these; the Node engine is held bit-identical by the layer0
|
|
76
|
+
* identity gate until Phase C rewrites it as an adapter.
|
|
77
|
+
*/
|
|
78
|
+
export { createEpochPrimitives } from './layer0/index.js';
|
|
79
|
+
export { deriveEpochParams } from './layer0/derive-params.js';
|
|
80
|
+
export { createDerivedViews } from './layer1/index.js';
|
|
81
|
+
// Phase 7 — the shared integrated-phase and cardinal-point machinery (CJS on
|
|
82
|
+
// purpose: tools/lib requires the same files via the exports-map subpaths;
|
|
83
|
+
// re-exported here so bundled ESM consumers need only the package root).
|
|
84
|
+
export { createPhaseMachinery } from './phase/index.cjs';
|
|
85
|
+
export { createCardinalModel } from './cardinal/index.cjs';
|
|
86
|
+
// Phase 8.2 — the lunar machinery, extracted layer by layer (survey order:
|
|
87
|
+
// eccentricity channel → month chain → cycle tables → arguments → series →
|
|
88
|
+
// apparent). Same CJS + root re-export convention.
|
|
89
|
+
export { createMoonEccChannel } from './moon/ecc-channel.cjs';
|
|
90
|
+
export { createMoonMonthChain } from './moon/month-chain.cjs';
|
|
91
|
+
export { createChainCycleIntegrator } from './chain-cycles/index.cjs';
|
|
92
|
+
export { createMoonArguments } from './moon/arguments.cjs';
|
|
93
|
+
export { createMoonSeries } from './moon/series.cjs';
|
|
94
|
+
export { createMoonApparent } from './moon/apparent.cjs';
|
|
95
|
+
// Phase 8.3 — the planet machinery, extracted by LAW over body records
|
|
96
|
+
// (survey order: geometry → Fibonacci laws → channels → chains → corrections).
|
|
97
|
+
export { derivePlanetGeometry } from './planets/geometry.cjs';
|
|
98
|
+
export * as planetFibonacciLaws from './planets/fibonacci-laws.cjs';
|
|
99
|
+
export { eccentricityFromCycles, computeEccentricityIntegrated } from './planets/ecc-channel.cjs';
|
|
100
|
+
export * as planetOrientation from './planets/orientation.cjs';
|
|
101
|
+
export { integrateAscendingNode } from './planets/asc-node-integrator.cjs';
|
|
102
|
+
export * as planetOrbitChain from './planets/orbit-chain.cjs';
|
|
103
|
+
export { evaluateParallaxBasis, gravitationTermDeltasDeg, evaluateElongationBasis } from './planets/corrections.cjs';
|
|
104
|
+
export { createPredictivePrecession, calcPlanetPerihelionLongDeg } from './planets/predict.cjs';
|
|
105
|
+
// L10 — the composition front door: one law set, N body records. Thin by
|
|
106
|
+
// design; engines keep their direct call sites (see planets/model.cjs).
|
|
107
|
+
export { createPlanetModel } from './planets/model.cjs';
|
|
108
|
+
// Phase 8.4 — the climate/ΔT machinery, extracted layer by layer.
|
|
109
|
+
export { createDeltaTCycles } from './deltat/cycles.cjs';
|
|
110
|
+
export { createDeepTimeLod } from './deltat/deep-time.cjs';
|
|
111
|
+
export { deltaTEspenakMeeusCanonSeconds } from './deltat/historical.cjs';
|
|
112
|
+
export { evalClimateL1OrbitalPermil } from './climate/l1-orbital.cjs';
|
|
113
|
+
// Phase 8.5 — eclipse geometry (single-copy: the browser had no Node twin).
|
|
114
|
+
export { createEclipseFinders } from './eclipse/finders.cjs';
|
|
115
|
+
// Phase 8.6 — the published reference curves (external comparison formulas
|
|
116
|
+
// and datasets, exactly as published; comparison references, never inputs).
|
|
117
|
+
export * as publishedCurves from './reference/published-curves.cjs';
|
|
118
|
+
// Phase 9 — S-P8: the fitted sun-longitude harmonic stack (three copies → one).
|
|
119
|
+
export { createSunLongitudeCorrection } from './sun/longitude-correction.cjs';
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Build a model bound to a set of constants.
|
|
123
|
+
*
|
|
124
|
+
* Dependency injection rather than import is the key decision (§2d): it is what
|
|
125
|
+
* makes `createModel({ ...DEFAULT_CONSTANTS, neptuneMassRatio: x })` express a
|
|
126
|
+
* counterfactual. Retrofitting it later is prohibitive, so the shape lands now
|
|
127
|
+
* even though the body is empty.
|
|
128
|
+
*
|
|
129
|
+
* @param {Constants} [constants]
|
|
130
|
+
* @returns {Model}
|
|
131
|
+
*/
|
|
132
|
+
export const createModel = (constants = GENERATED) => {
|
|
133
|
+
// Validation targets are not merely absent from DEFAULT_CONSTANTS — they are
|
|
134
|
+
// REFUSED here. Absence alone only stops the spread form
|
|
135
|
+
// `{...DEFAULT_CONSTANTS, x}`; nothing stopped a caller passing a bound
|
|
136
|
+
// explicitly. Saturn fails its Laplace-Lagrange bound in verify-laws (44/45),
|
|
137
|
+
// and a counterfactual that could widen that bound would be measuring itself.
|
|
138
|
+
for (const key of NEVER_INJECTABLE) {
|
|
139
|
+
if (constants && Object.prototype.hasOwnProperty.call(constants, key)) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`physics: "${key}" is a validation target and cannot be injected (§2d). `
|
|
142
|
+
+ 'It is exported as REFERENCE_DATA, which createModel does not accept — '
|
|
143
|
+
+ 'a counterfactual must not be able to move the goalposts it is judged by.',
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Test the ARGUMENT, not the copy. `ctx` below is a fresh frozen object, so
|
|
149
|
+
// `ctx === GENERATED` is never true and the fast path never fired — every
|
|
150
|
+
// call fell through to isDefault(), which serialises the whole context twice.
|
|
151
|
+
// Harmless at 10 KB (~0.4 ms); ruinous once the fitted coefficients arrive,
|
|
152
|
+
// which are ~400 KB.
|
|
153
|
+
const isGeneratedDefault = constants === GENERATED;
|
|
154
|
+
|
|
155
|
+
const ctx = Object.freeze({ ...constants });
|
|
156
|
+
|
|
157
|
+
// The hash identifies THIS context, not the default one. A counterfactual
|
|
158
|
+
// that reported the default hash would be unreproducible — you could not tell
|
|
159
|
+
// from a stored result which constants produced it, which is the whole point
|
|
160
|
+
// of carrying a hash (§2d). Recomputed rather than copied for that reason.
|
|
161
|
+
//
|
|
162
|
+
// isDefault() still runs for a caller who passes a value-identical COPY
|
|
163
|
+
// (`{...DEFAULT_CONSTANTS}`); only the identity case is short-circuited.
|
|
164
|
+
const hash = isGeneratedDefault || isDefault(ctx) ? CONSTANTS_HASH : hashOf(ctx);
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
constants: ctx,
|
|
168
|
+
hash,
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* H-lattice periods — the structural identities, in years.
|
|
172
|
+
*
|
|
173
|
+
* PURE ALGEBRA OVER THE CONTEXT. No epoch, no formula, no fit. These are
|
|
174
|
+
* the divisor relationships H/13, H/3, H/16 that define the lattice, and
|
|
175
|
+
* they exist here in Phase 5 for one reason: a hash-only counterfactual
|
|
176
|
+
* test would still pass if `createModel` ignored its argument entirely.
|
|
177
|
+
* Something has to READ the context and return a number for injection to be
|
|
178
|
+
* demonstrated end to end.
|
|
179
|
+
*
|
|
180
|
+
* The motion model is Phase 6. This is not it.
|
|
181
|
+
*
|
|
182
|
+
* `divisor` and `period` are never interchangeable (CLAUDE.md): 13 is the
|
|
183
|
+
* divisor, H/13 years is the period. The names say which.
|
|
184
|
+
*
|
|
185
|
+
* @returns {{axialPrecessionPeriodYears: number, inclinationPrecessionPeriodYears: number, perihelionPrecessionPeriodYears: number}}
|
|
186
|
+
*/
|
|
187
|
+
computeLatticePeriodsYears: () => {
|
|
188
|
+
const H = /** @type {{holisticyearLength: number}} */ (
|
|
189
|
+
/** @type {Record<string, unknown>} */ (ctx).foundational
|
|
190
|
+
).holisticyearLength;
|
|
191
|
+
return {
|
|
192
|
+
axialPrecessionPeriodYears: H / 13,
|
|
193
|
+
inclinationPrecessionPeriodYears: H / 3,
|
|
194
|
+
perihelionPrecessionPeriodYears: H / 16,
|
|
195
|
+
};
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* @param {number} year
|
|
200
|
+
* @returns {number}
|
|
201
|
+
*/
|
|
202
|
+
eccentricity: (year) => {
|
|
203
|
+
void year; void ctx;
|
|
204
|
+
throw new Error('physics: not implemented until Phase 6 — see IP-technical-design.md §6');
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Key-sorted canonical form, so the digest depends on values and not on
|
|
211
|
+
* property insertion order — `{...DEFAULT, x}` and `{x, ...DEFAULT}` describe
|
|
212
|
+
* the same counterfactual and must hash alike.
|
|
213
|
+
*
|
|
214
|
+
* @param {unknown} v
|
|
215
|
+
* @returns {unknown}
|
|
216
|
+
*/
|
|
217
|
+
const canonical = (v) => {
|
|
218
|
+
if (Array.isArray(v)) return v.map(canonical);
|
|
219
|
+
if (v && typeof v === 'object') {
|
|
220
|
+
const o = /** @type {Record<string, unknown>} */ (v);
|
|
221
|
+
return Object.fromEntries(Object.keys(o).sort().filter((k) => k !== 'hash').map((k) => [k, canonical(o[k])]));
|
|
222
|
+
}
|
|
223
|
+
return v;
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* @param {Constants} ctx
|
|
228
|
+
* @returns {boolean} true when ctx is value-identical to the generated set
|
|
229
|
+
*/
|
|
230
|
+
const isDefault = (ctx) =>
|
|
231
|
+
JSON.stringify(canonical(ctx)) === JSON.stringify(canonical(GENERATED));
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* FNV-1a over the canonical form. Not cryptographic and does not need to be —
|
|
235
|
+
* it distinguishes constant sets, it does not authenticate them. `node:crypto`
|
|
236
|
+
* is unavailable here by design: physics imports no Node builtins (§2b),
|
|
237
|
+
* because it runs in a browser too.
|
|
238
|
+
*
|
|
239
|
+
* @param {Constants} ctx
|
|
240
|
+
* @returns {string} 16 hex chars, prefixed to mark it as a derived context
|
|
241
|
+
*/
|
|
242
|
+
const hashOf = (ctx) => {
|
|
243
|
+
const s = JSON.stringify(canonical(ctx));
|
|
244
|
+
let h1 = 0x811c9dc5;
|
|
245
|
+
let h2 = 0x01000193;
|
|
246
|
+
for (let i = 0; i < s.length; i += 1) {
|
|
247
|
+
const c = s.charCodeAt(i);
|
|
248
|
+
h1 = Math.imul(h1 ^ c, 0x01000193) >>> 0;
|
|
249
|
+
h2 = Math.imul(h2 ^ c, 0x85ebca6b) >>> 0;
|
|
250
|
+
}
|
|
251
|
+
return `cf-${h1.toString(16).padStart(8, '0')}${h2.toString(16).padStart(8, '0')}`;
|
|
252
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derive the Layer 0 parameter bundle from raw catalogue constants.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `tools/lib/deep-time.js` lines ~60–101 OPERATION FOR OPERATION —
|
|
5
|
+
* grouping, order, the speed-of-light fallback ternary, all of it. Floating
|
|
6
|
+
* point is not associative, so "algebraically the same" is not the standard
|
|
7
|
+
* here; bit-identical is, and the layer0 identity test compares every field of
|
|
8
|
+
* this function's output against the values deep-time.js derives internally.
|
|
9
|
+
* If the two ever disagree, one of them changed and the other did not — which
|
|
10
|
+
* is the exact drift mode that produced five diverging implementations.
|
|
11
|
+
*
|
|
12
|
+
* Pure function, imports nothing. Lives in physics so the browser and the Node
|
|
13
|
+
* engine can both build Layer 0 from ONE derivation (deep-time.js keeps its
|
|
14
|
+
* own inline copy only until Phase C rewrites it; until then the test is the
|
|
15
|
+
* enforcement).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {Object} RawEpochConstants
|
|
20
|
+
* @property {number} solarLuminosityW IAU 2015 nominal (W)
|
|
21
|
+
* @property {number} solarWindKgPerS Ulysses/ACE/Wind
|
|
22
|
+
* @property {number} speedOfLightKmPerS km/s (fallback 299792458 m/s if falsy)
|
|
23
|
+
* @property {number} alpha1PerMa
|
|
24
|
+
* @property {number} alpha3PerMa3
|
|
25
|
+
* @property {number} alpha4PerMa4
|
|
26
|
+
* @property {number} holisticYearJ2000 H
|
|
27
|
+
* @property {number} meanSiderealYearSeconds
|
|
28
|
+
* @property {number} meanSiderealYearDaysKinematic
|
|
29
|
+
* @property {number} sunMassKg M_SUN
|
|
30
|
+
* @property {number} gmEarthAloneKm3S2
|
|
31
|
+
* @property {number} gmMoonAloneKm3S2
|
|
32
|
+
* @property {number} gravitationalConstantKm3KgS2
|
|
33
|
+
* @property {number} earthMoiFactorJ2000
|
|
34
|
+
* @property {number} earthDiameterKm
|
|
35
|
+
* @property {number} moonDistanceKm
|
|
36
|
+
* @property {number} moonOrbitalEccentricity
|
|
37
|
+
* @property {number} gmEarthMoonSystemKm3S2
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {RawEpochConstants} raw
|
|
42
|
+
* @returns {import('./index.js').EpochParams}
|
|
43
|
+
*/
|
|
44
|
+
export const deriveEpochParams = (raw) => {
|
|
45
|
+
// Solar physics — Driver 2 mass loss
|
|
46
|
+
const cSiMPerS = raw.speedOfLightKmPerS ? raw.speedOfLightKmPerS * 1000 : 299792458;
|
|
47
|
+
const dmDtTotalKgS = raw.solarLuminosityW / (cSiMPerS * cSiMPerS) + raw.solarWindKgPerS;
|
|
48
|
+
|
|
49
|
+
// J2000 anchors derived from framework constants
|
|
50
|
+
const lodNowH13Seconds = raw.meanSiderealYearSeconds / raw.meanSiderealYearDaysKinematic;
|
|
51
|
+
const solarMassLossFracPerYear = dmDtTotalKgS * raw.meanSiderealYearSeconds / raw.sunMassKg;
|
|
52
|
+
|
|
53
|
+
// Earth mass, moments
|
|
54
|
+
const earthMassKg = raw.gmEarthAloneKm3S2 / raw.gravitationalConstantKm3KgS2;
|
|
55
|
+
const moonMassKg = raw.gmMoonAloneKm3S2 / raw.gravitationalConstantKm3KgS2;
|
|
56
|
+
const earthRadiusM = (raw.earthDiameterKm / 2) * 1000;
|
|
57
|
+
const iEarthJ2000 = raw.earthMoiFactorJ2000 * earthMassKg * earthRadiusM * earthRadiusM;
|
|
58
|
+
|
|
59
|
+
// Moon constants
|
|
60
|
+
const moonDistanceNowM = raw.moonDistanceKm * 1000;
|
|
61
|
+
const moonEccentricityFactor = Math.sqrt(1 - raw.moonOrbitalEccentricity * raw.moonOrbitalEccentricity);
|
|
62
|
+
const gmEarthMoonM3S2 = raw.gmEarthMoonSystemKm3S2 * 1e9;
|
|
63
|
+
const totalAngularMomentumKgM2S = (iEarthJ2000 * 2 * Math.PI / lodNowH13Seconds)
|
|
64
|
+
+ (moonMassKg * Math.sqrt(gmEarthMoonM3S2 * moonDistanceNowM) * moonEccentricityFactor);
|
|
65
|
+
const moonLockDistanceM = (totalAngularMomentumKgM2S / (moonMassKg * Math.sqrt(gmEarthMoonM3S2) * moonEccentricityFactor)) ** 2;
|
|
66
|
+
|
|
67
|
+
return Object.freeze({
|
|
68
|
+
epochYear: 2000,
|
|
69
|
+
alpha1PerMa: raw.alpha1PerMa,
|
|
70
|
+
alpha3PerMa3: raw.alpha3PerMa3,
|
|
71
|
+
alpha4PerMa4: raw.alpha4PerMa4,
|
|
72
|
+
moonDistanceNowM,
|
|
73
|
+
moonLockDistanceM,
|
|
74
|
+
totalAngularMomentumKgM2S,
|
|
75
|
+
moonMassKg,
|
|
76
|
+
gmEarthMoonM3S2,
|
|
77
|
+
moonEccentricityFactor,
|
|
78
|
+
earthMassKg,
|
|
79
|
+
earthRadiusM,
|
|
80
|
+
holisticYearJ2000: raw.holisticYearJ2000,
|
|
81
|
+
lodNowH13Seconds,
|
|
82
|
+
siderealYearJ2000Seconds: raw.meanSiderealYearSeconds,
|
|
83
|
+
solarMassLossFracPerYear,
|
|
84
|
+
});
|
|
85
|
+
};
|