@essrt/physics 1.4.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/constants/generated.d.ts +5 -2
- package/src/constants/generated.js +5 -2
- package/src/eclipse/besselian.cjs +278 -0
- package/src/model.js +51 -1
- package/src/moon/series.cjs +13 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@essrt/physics",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.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
6
|
"modelVersion": "v11.0",
|
|
@@ -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: "fa3230753655d190";
|
|
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: "fa3230753655d190";
|
|
13
13
|
readonly additionalBodies: {
|
|
14
14
|
"pluto": {
|
|
15
15
|
"name": string;
|
|
@@ -241,6 +241,9 @@ export declare const DEFAULT_CONSTANTS: {
|
|
|
241
241
|
"earthMoiFactorJ2000": number;
|
|
242
242
|
"earthFlatteningInverseWGS84": number;
|
|
243
243
|
"aberrationConstantArcsec": number;
|
|
244
|
+
"gmstMeanSiderealT0Deg": number;
|
|
245
|
+
"gmstMeanSiderealRateDegPerDay": number;
|
|
246
|
+
"gmstMeanSiderealT2Deg": number;
|
|
244
247
|
"nutationLeadingTermsArcsec": {
|
|
245
248
|
"psiOmega": number;
|
|
246
249
|
"psi2Ls": number;
|
|
@@ -32,7 +32,7 @@
|
|
|
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 = "fa3230753655d190";
|
|
36
36
|
|
|
37
37
|
/** Model version label — single source: public/input/model-version.json (§10 two-axis scheme). */
|
|
38
38
|
export const MODEL_VERSION = "v11.0";
|
|
@@ -42,7 +42,7 @@ 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: "fa3230753655d190",
|
|
46
46
|
additionalBodies: {
|
|
47
47
|
"pluto": {
|
|
48
48
|
"name": "Pluto",
|
|
@@ -298,6 +298,9 @@ export const DEFAULT_CONSTANTS = Object.freeze({
|
|
|
298
298
|
"earthMoiFactorJ2000": 0.3306947,
|
|
299
299
|
"earthFlatteningInverseWGS84": 298.257223563,
|
|
300
300
|
"aberrationConstantArcsec": 20.4955,
|
|
301
|
+
"gmstMeanSiderealT0Deg": 280.46061837,
|
|
302
|
+
"gmstMeanSiderealRateDegPerDay": 360.98564736629,
|
|
303
|
+
"gmstMeanSiderealT2Deg": 0.000387933,
|
|
301
304
|
"nutationLeadingTermsArcsec": {
|
|
302
305
|
"psiOmega": -17.2,
|
|
303
306
|
"psi2Ls": -1.32,
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Solar-eclipse shadow geometry — the location tier (20.3g).
|
|
3
|
+
*
|
|
4
|
+
* Instantaneous shadow-axis evaluation on the FULL shared series (no
|
|
5
|
+
* precomputed polynomial elements): at any JD(UT) the geocentric Sun and
|
|
6
|
+
* Moon are placed in the equatorial-of-date frame, the umbra axis is
|
|
7
|
+
* intersected with the ellipsoid, and observer local circumstances follow
|
|
8
|
+
* from the observer's perpendicular offset to the axis versus the
|
|
9
|
+
* penumbra/umbra cone radii at the observer's axial plane.
|
|
10
|
+
*
|
|
11
|
+
* THE TWO AXIS CONVENTIONS (both measured against the 15 NASA path-table
|
|
12
|
+
* centerline points, tools/verify/eclipse-audit.js `centerlines`; the raw
|
|
13
|
+
* finder chain read ~61″ shadow-plane, this chain 6.3″ mean / 9.1″ max —
|
|
14
|
+
* within every NASA path half-width):
|
|
15
|
+
*
|
|
16
|
+
* 1. TT bridge. The framework ΔT curve is ZEROED AT J2000 by construction
|
|
17
|
+
* (it integrates LOD excess from the epoch anchor); the model's
|
|
18
|
+
* absolute ΔT is deltaTStart + curve — exactly how every other
|
|
19
|
+
* consumer reads it (the browser accumulator, Meeus geometry, the ΔT
|
|
20
|
+
* charts; deltaTStart is the joint-world trend anchor, deliberately
|
|
21
|
+
* ~8 s below the IERS instantaneous 63.6 s — the industrial-era
|
|
22
|
+
* acceleration the cyclic model does not capture). The eclipse
|
|
23
|
+
* FINDERS deliberately stay on the zeroed convention — their fitted
|
|
24
|
+
* anchors (SUN_HARMONICS, canon statistics) were produced through it,
|
|
25
|
+
* and eclipse TIMING is elongation-class, where the convention
|
|
26
|
+
* cancels against the fitted phases. GROUND LOCATION is absolute: the
|
|
27
|
+
* Earth-fixed frame rides true UT rotation, so the bodies are
|
|
28
|
+
* evaluated at the model's absolute TT. ttBridgeSeconds = deltaTStart
|
|
29
|
+
* (no new constant; measured BETTER on the NASA reference points than
|
|
30
|
+
* the observed 63.8/64.2 s — mean 6.3″ vs 7.3″). ~34″ of the raw
|
|
31
|
+
* offset.
|
|
32
|
+
*
|
|
33
|
+
* 2. Full series, not the truncated finder forms. truncatedLonDeg omits
|
|
34
|
+
* the fitted moonMeeusLpCorrection anchor (+33.8″) and truncatedBetaDeg
|
|
35
|
+
* omits the −2235·sin(Lp) latitude family (~8″) — both deliberate for
|
|
36
|
+
* the certified finder statistics (knife-edge canon events), both
|
|
37
|
+
* required here. The injected moonFullAtDaysTT is the series'
|
|
38
|
+
* sceneEvalAt — the same evaluation the browser scene ships. Measured:
|
|
39
|
+
* ~27″ of the raw offset.
|
|
40
|
+
*
|
|
41
|
+
* Annual aberration is deliberately ABSENT: for ground location a
|
|
42
|
+
* common-mode rotation of both bodies moves the ground point by the raw
|
|
43
|
+
* angle (~0.6 km), and the elongation content cancels at syzygy (the
|
|
44
|
+
* round-3 result). Both bodies here are geometric mean-of-date, matching
|
|
45
|
+
* the MEAN sidereal time used for the Earth-fixed mapping.
|
|
46
|
+
*
|
|
47
|
+
* The scene-umbra conventions (THREE scaffold navigation) never enter this
|
|
48
|
+
* package (§2h) — this is an independent construction on the shared
|
|
49
|
+
* series, cross-checked against the scene twins through the same NASA
|
|
50
|
+
* reference points.
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
'use strict';
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @typedef {Object} BesselianDeps
|
|
57
|
+
* @property {(dDaysTT: number) => {lonDeg: number, latDeg: number, distKm: number}} moonFullAtDaysTT
|
|
58
|
+
* full-series Moon: ecliptic-of-date longitude/latitude (deg) + distance (km), TT axis (days since J2000)
|
|
59
|
+
* @property {(jdUT: number) => number} sunLonDegAt - geometric mean sun longitude (deg), JD(UT) axis with the finder's internal ΔT
|
|
60
|
+
* @property {(jd: number) => number} deltaTSecondsAt - framework ΔT (J2000-zeroed convention)
|
|
61
|
+
* @property {(year: number) => number} obliquityDegAt - framework obliquity (deg) at calendar year
|
|
62
|
+
* @property {(year: number) => number} eccentricityAt - framework Earth-orbit eccentricity at calendar year
|
|
63
|
+
* @property {(year: number) => number} perihelionLongitudeDegAt - framework perihelion longitude (deg) at calendar year
|
|
64
|
+
* @property {(jd: number) => number} yearFromJD - the model's own JD → calendar-year conversion
|
|
65
|
+
* @property {{ j2000JD: number, julianCenturyDays: number, earthDiameterKm: number,
|
|
66
|
+
* moonDiameterKm: number, sunDiameterKm: number, sunDistanceKm: number,
|
|
67
|
+
* earthFlatteningInverse: number, ttBridgeSeconds: number,
|
|
68
|
+
* gmstMeanSiderealT0Deg: number, gmstMeanSiderealRateDegPerDay: number,
|
|
69
|
+
* gmstMeanSiderealT2Deg: number }} constants
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
/** @param {BesselianDeps} deps */
|
|
73
|
+
function createBesselian(deps) {
|
|
74
|
+
const K = deps.constants;
|
|
75
|
+
const D2R = Math.PI / 180;
|
|
76
|
+
const R_E_KM = K.earthDiameterKm / 2;
|
|
77
|
+
const R_SUN_KM = K.sunDiameterKm / 2;
|
|
78
|
+
const R_MOON_KM = K.moonDiameterKm / 2;
|
|
79
|
+
const F = 1 / K.earthFlatteningInverse;
|
|
80
|
+
|
|
81
|
+
/** Mean sidereal time at Greenwich, degrees (mean equinox — pairs with the
|
|
82
|
+
* mean-of-date body longitudes; coefficients single-sourced from
|
|
83
|
+
* astro-reference.json physicalConstants). @param {number} jdUT @returns {number} */
|
|
84
|
+
function gmstDeg(jdUT) {
|
|
85
|
+
const T = (jdUT - K.j2000JD) / K.julianCenturyDays;
|
|
86
|
+
return ((K.gmstMeanSiderealT0Deg + K.gmstMeanSiderealRateDegPerDay * (jdUT - K.j2000JD)
|
|
87
|
+
+ K.gmstMeanSiderealT2Deg * T * T) % 360 + 360) % 360;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Ecliptic-of-date → equatorial-of-date vector (km).
|
|
91
|
+
* @param {number} lonDeg @param {number} betDeg @param {number} distKm
|
|
92
|
+
* @param {number} epsRad @returns {[number, number, number]} */
|
|
93
|
+
function eclToEq(lonDeg, betDeg, distKm, epsRad) {
|
|
94
|
+
const lam = lonDeg * D2R, bet = betDeg * D2R;
|
|
95
|
+
const x = Math.cos(bet) * Math.cos(lam);
|
|
96
|
+
const y = Math.cos(bet) * Math.sin(lam) * Math.cos(epsRad) - Math.sin(bet) * Math.sin(epsRad);
|
|
97
|
+
const z = Math.cos(bet) * Math.sin(lam) * Math.sin(epsRad) + Math.sin(bet) * Math.cos(epsRad);
|
|
98
|
+
return [distKm * x, distKm * y, distKm * z];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Elliptical Sun distance — FRAMEWORK-NATIVE: the model's own
|
|
102
|
+
* eccentricity (Law-5-locked base + drift) and perihelion longitude
|
|
103
|
+
* (H/16 precession) close the ellipse, with the true anomaly from the
|
|
104
|
+
* already-computed sun longitude (v = λ − (ϖ + 180°), geocentric-perigee
|
|
105
|
+
* convention). Agrees with the Meeus Ch. 25 polynomial to ±0.001% over a
|
|
106
|
+
* year, with no fitted external coefficients, and stays honest at deep
|
|
107
|
+
* time (e and ϖ evolve with the model). The distance is LOAD-BEARING for
|
|
108
|
+
* the umbral cone radius — a small difference of large terms: the
|
|
109
|
+
* mean-AU approximation's ±1.7% seasonal error became tens of percent of
|
|
110
|
+
* umbra radius (measured: Carbondale 2017 central duration 95 s vs the
|
|
111
|
+
* real 158 s; elliptical gives 157 s).
|
|
112
|
+
* @param {number} year @param {number} sunLonDeg @returns {number} km */
|
|
113
|
+
function sunDistanceKm(year, sunLonDeg) {
|
|
114
|
+
const e = deps.eccentricityAt(year);
|
|
115
|
+
const v = (sunLonDeg - (deps.perihelionLongitudeDegAt(year) + 180)) * D2R;
|
|
116
|
+
return K.sunDistanceKm * (1 - e * e) / (1 + e * Math.cos(v));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Sun and Moon geocentric equatorial-of-date vectors at true TT.
|
|
120
|
+
* @param {number} jdUT @returns {{S: [number,number,number], M: [number,number,number]}} */
|
|
121
|
+
function bodiesAt(jdUT) {
|
|
122
|
+
const jb = jdUT + K.ttBridgeSeconds / 86400; // finder-axis input → its internal ΔT lands on true TT
|
|
123
|
+
const year = deps.yearFromJD(jb);
|
|
124
|
+
const eps = deps.obliquityDegAt(year) * D2R;
|
|
125
|
+
const sunLon = deps.sunLonDegAt(jb);
|
|
126
|
+
const dTT = (jb - K.j2000JD) + deps.deltaTSecondsAt(jb) / 86400;
|
|
127
|
+
const moon = deps.moonFullAtDaysTT(dTT);
|
|
128
|
+
return {
|
|
129
|
+
S: eclToEq(sunLon, 0, sunDistanceKm(year, sunLon), eps),
|
|
130
|
+
M: eclToEq(moon.lonDeg, moon.latDeg, moon.distKm, eps),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Umbra-axis ground intersection at JD(UT): geodetic latitude, east
|
|
135
|
+
* longitude — or null when the axis misses the ellipsoid.
|
|
136
|
+
* @param {number} jdUT @returns {{latDeg: number, lonDeg: number} | null} */
|
|
137
|
+
function umbraGroundAt(jdUT) {
|
|
138
|
+
const { S, M } = bodiesAt(jdUT);
|
|
139
|
+
const dv = [M[0] - S[0], M[1] - S[1], M[2] - S[2]];
|
|
140
|
+
const dl = Math.hypot(dv[0], dv[1], dv[2]);
|
|
141
|
+
const d = [dv[0] / dl, dv[1] / dl, dv[2] / dl];
|
|
142
|
+
const MdotD = M[0] * d[0] + M[1] * d[1] + M[2] * d[2];
|
|
143
|
+
const MdotM = M[0] * M[0] + M[1] * M[1] + M[2] * M[2];
|
|
144
|
+
const disc = MdotD * MdotD - (MdotM - R_E_KM * R_E_KM);
|
|
145
|
+
if (disc < 0) return null;
|
|
146
|
+
const s = -MdotD - Math.sqrt(disc);
|
|
147
|
+
const hit = [M[0] + s * d[0], M[1] + s * d[1], M[2] + s * d[2]];
|
|
148
|
+
const r = Math.hypot(hit[0], hit[1], hit[2]);
|
|
149
|
+
const latGc = Math.asin(Math.max(-1, Math.min(1, hit[2] / r)));
|
|
150
|
+
const latDeg = Math.atan(Math.tan(latGc) / ((1 - F) * (1 - F))) / D2R;
|
|
151
|
+
let lonDeg = Math.atan2(hit[1], hit[0]) / D2R - gmstDeg(jdUT);
|
|
152
|
+
lonDeg = ((lonDeg + 540) % 360) - 180;
|
|
153
|
+
return { latDeg, lonDeg };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Observer geocentric position in the equatorial-of-date frame (km).
|
|
157
|
+
* Geodetic latitude on the ellipsoid (sea level).
|
|
158
|
+
* @param {number} jdUT @param {number} latDeg @param {number} lonDeg
|
|
159
|
+
* @returns {[number, number, number]} */
|
|
160
|
+
function observerVec(jdUT, latDeg, lonDeg) {
|
|
161
|
+
const u = Math.atan((1 - F) * Math.tan(latDeg * D2R)); // reduced latitude
|
|
162
|
+
const rho = [Math.cos(u), (1 - F) * Math.sin(u)]; // [equatorial, polar] components / R_E
|
|
163
|
+
const lst = (gmstDeg(jdUT) + lonDeg) * D2R; // local mean sidereal angle
|
|
164
|
+
return [
|
|
165
|
+
R_E_KM * rho[0] * Math.cos(lst),
|
|
166
|
+
R_E_KM * rho[0] * Math.sin(lst),
|
|
167
|
+
R_E_KM * rho[1],
|
|
168
|
+
];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Instantaneous shadow state at the observer: perpendicular offset from
|
|
173
|
+
* the axis vs cone radii at the observer's axial plane.
|
|
174
|
+
* @param {number} jdUT @param {number} latDeg @param {number} lonDeg
|
|
175
|
+
* @returns {{offsetKm: number, penumbraKm: number, umbraKm: number, magnitude: number}}
|
|
176
|
+
* umbraKm is SIGNED: positive = annular (beyond the umbral vertex),
|
|
177
|
+
* negative = total; |umbraKm| is the central-zone radius either way.
|
|
178
|
+
*/
|
|
179
|
+
function shadowStateAt(jdUT, latDeg, lonDeg) {
|
|
180
|
+
const { S, M } = bodiesAt(jdUT);
|
|
181
|
+
const O = observerVec(jdUT, latDeg, lonDeg);
|
|
182
|
+
const dv = [M[0] - S[0], M[1] - S[1], M[2] - S[2]];
|
|
183
|
+
const dl = Math.hypot(dv[0], dv[1], dv[2]);
|
|
184
|
+
const d = [dv[0] / dl, dv[1] / dl, dv[2] / dl]; // Sun→Moon (toward Earth)
|
|
185
|
+
const OM = [O[0] - M[0], O[1] - M[1], O[2] - M[2]];
|
|
186
|
+
const along = OM[0] * d[0] + OM[1] * d[1] + OM[2] * d[2]; // axial distance Moon→observer plane
|
|
187
|
+
const w = [OM[0] - along * d[0], OM[1] - along * d[1], OM[2] - along * d[2]];
|
|
188
|
+
const offsetKm = Math.hypot(w[0], w[1], w[2]);
|
|
189
|
+
const tanF1 = (R_SUN_KM + R_MOON_KM) / dl; // penumbral half-angle
|
|
190
|
+
const tanF2 = (R_SUN_KM - R_MOON_KM) / dl; // umbral half-angle
|
|
191
|
+
const penumbraKm = R_MOON_KM + along * tanF1;
|
|
192
|
+
const umbraKm = along * tanF2 - R_MOON_KM; // signed: >0 annular, <0 total
|
|
193
|
+
// Besselian magnitude convention: SIGNED umbral radius in the
|
|
194
|
+
// denominator — total events read >1 at the centre, annular <1.
|
|
195
|
+
const magnitude = (penumbraKm - offsetKm) / (penumbraKm + umbraKm);
|
|
196
|
+
return { offsetKm, penumbraKm, umbraKm, magnitude };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Local circumstances for an observer around one solar eclipse.
|
|
201
|
+
* Scans ±4 h around jdGreatest at 30 s, bisects contacts to ~1 s.
|
|
202
|
+
* @param {number} jdGreatest - the finder's greatest-eclipse JD(UT)
|
|
203
|
+
* @param {number} latDeg @param {number} lonDeg
|
|
204
|
+
* @returns {{kind: 'none'|'partial'|'annular'|'total',
|
|
205
|
+
* magnitude: number, maxJd: number,
|
|
206
|
+
* contacts: {c1: number|null, c2: number|null, c3: number|null, c4: number|null},
|
|
207
|
+
* centralDurationSeconds: number|null}}
|
|
208
|
+
*/
|
|
209
|
+
function localCircumstances(jdGreatest, latDeg, lonDeg) {
|
|
210
|
+
const HALF_WIN = 4 / 24, STEP = 30 / 86400;
|
|
211
|
+
// maximum: minimize offset − nothing fancier than a fine scan + refine
|
|
212
|
+
let best = { t: jdGreatest, magnitude: -Infinity };
|
|
213
|
+
for (let t = jdGreatest - HALF_WIN; t <= jdGreatest + HALF_WIN; t += STEP) {
|
|
214
|
+
const st = shadowStateAt(t, latDeg, lonDeg);
|
|
215
|
+
if (st.magnitude > best.magnitude) best = { t, magnitude: st.magnitude };
|
|
216
|
+
}
|
|
217
|
+
// golden-section-ish refine on magnitude around the best sample
|
|
218
|
+
let lo = best.t - STEP, hi = best.t + STEP;
|
|
219
|
+
for (let i = 0; i < 30; i++) {
|
|
220
|
+
const m1 = lo + (hi - lo) / 3, m2 = hi - (hi - lo) / 3;
|
|
221
|
+
if (shadowStateAt(m1, latDeg, lonDeg).magnitude
|
|
222
|
+
< shadowStateAt(m2, latDeg, lonDeg).magnitude) lo = m1; else hi = m2;
|
|
223
|
+
}
|
|
224
|
+
const maxJd = (lo + hi) / 2;
|
|
225
|
+
const atMax = shadowStateAt(maxJd, latDeg, lonDeg);
|
|
226
|
+
|
|
227
|
+
/** Bisect a sign change of fn between a (negative) and b (positive).
|
|
228
|
+
* @param {(t: number) => number} fn @param {number} a @param {number} b
|
|
229
|
+
* @returns {number} */
|
|
230
|
+
const bisect = (fn, a, b) => {
|
|
231
|
+
for (let i = 0; i < 40; i++) {
|
|
232
|
+
const m = (a + b) / 2;
|
|
233
|
+
if (fn(m) < 0) a = m; else b = m;
|
|
234
|
+
}
|
|
235
|
+
return (a + b) / 2;
|
|
236
|
+
};
|
|
237
|
+
/** @param {number} t @returns {number} >0 inside the penumbra */
|
|
238
|
+
const penEdge = (t) => {
|
|
239
|
+
const st = shadowStateAt(t, latDeg, lonDeg);
|
|
240
|
+
return st.penumbraKm - st.offsetKm;
|
|
241
|
+
};
|
|
242
|
+
/** @param {number} t @returns {number} >0 inside the central zone */
|
|
243
|
+
const umbEdge = (t) => {
|
|
244
|
+
const st = shadowStateAt(t, latDeg, lonDeg);
|
|
245
|
+
return Math.abs(st.umbraKm) - st.offsetKm;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
/** @type {{c1: number|null, c2: number|null, c3: number|null, c4: number|null}} */
|
|
249
|
+
const contacts = { c1: null, c2: null, c3: null, c4: null };
|
|
250
|
+
let kind = /** @type {'none'|'partial'|'annular'|'total'} */ ('none');
|
|
251
|
+
if (penEdge(maxJd) > 0) {
|
|
252
|
+
kind = 'partial';
|
|
253
|
+
// outer contacts: walk out from max until outside, then bisect
|
|
254
|
+
for (let t = maxJd; t >= maxJd - HALF_WIN; t -= STEP) {
|
|
255
|
+
if (penEdge(t) < 0) { contacts.c1 = bisect(penEdge, t, t + STEP); break; }
|
|
256
|
+
}
|
|
257
|
+
for (let t = maxJd; t <= maxJd + HALF_WIN; t += STEP) {
|
|
258
|
+
if (penEdge(t) < 0) { contacts.c4 = bisect(/** @param {number} x */ (x) => -penEdge(x), t - STEP, t); break; }
|
|
259
|
+
}
|
|
260
|
+
if (umbEdge(maxJd) > 0) {
|
|
261
|
+
kind = atMax.umbraKm < 0 ? 'total' : 'annular';
|
|
262
|
+
for (let t = maxJd; t >= maxJd - HALF_WIN; t -= STEP) {
|
|
263
|
+
if (umbEdge(t) < 0) { contacts.c2 = bisect(umbEdge, t, t + STEP); break; }
|
|
264
|
+
}
|
|
265
|
+
for (let t = maxJd; t <= maxJd + HALF_WIN; t += STEP) {
|
|
266
|
+
if (umbEdge(t) < 0) { contacts.c3 = bisect(/** @param {number} x */ (x) => -umbEdge(x), t - STEP, t); break; }
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const centralDurationSeconds = (contacts.c2 !== null && contacts.c3 !== null)
|
|
271
|
+
? (contacts.c3 - contacts.c2) * 86400 : null;
|
|
272
|
+
return { kind, magnitude: Math.max(0, atMax.magnitude), maxJd, contacts, centralDurationSeconds };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return { umbraGroundAt, shadowStateAt, localCircumstances };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
module.exports = { createBesselian };
|
package/src/model.js
CHANGED
|
@@ -32,6 +32,7 @@ import { createChainCycleIntegrator } from './chain-cycles/index.cjs';
|
|
|
32
32
|
import { createMoonArguments, jdToDecimalYear } from './moon/arguments.cjs';
|
|
33
33
|
import { createMoonSeries } from './moon/series.cjs';
|
|
34
34
|
import { createEclipseFinders } from './eclipse/finders.cjs';
|
|
35
|
+
import { createBesselian } from './eclipse/besselian.cjs';
|
|
35
36
|
import { driver2PeriodSecondsAtAge } from './planets/orbit-chain.cjs';
|
|
36
37
|
|
|
37
38
|
/**
|
|
@@ -782,7 +783,20 @@ export function assembleModel(C, F) {
|
|
|
782
783
|
argsAt: /** @param {number} jdTT */ (jdTT) => moonArgs.argsAt(jdTT),
|
|
783
784
|
eFactorForD: fwEFactor,
|
|
784
785
|
eFactorAtJdTT: /** @param {number} jdTT */ (jdTT) => fwEFactor(jdTT - j2000JD),
|
|
785
|
-
|
|
786
|
+
// 20.3d(i): the Driver-1 ratio at the EVALUATED epoch (per-jd pure
|
|
787
|
+
// evaluator). MATCHED TRIPLE with the tools-lib and browser getters:
|
|
788
|
+
// identical age arithmetic — the SI-linear year (yearFromJD, never
|
|
789
|
+
// the calendar year: the recorded linear-vs-calendar mirror trap)
|
|
790
|
+
// against the MODEL-START anchor (startModelYearWithCorrection, the
|
|
791
|
+
// browser's J2000_CALENDAR_YEAR — NOT this model's 2000.0 yearToTMa
|
|
792
|
+
// convention: the 0.4977-yr difference is 4.95e-11 of distance and
|
|
793
|
+
// failed the bit-exact parity gate). Falls back to the J2000
|
|
794
|
+
// constant without a jd (legacy call shape) or past the domain.
|
|
795
|
+
getMoonDistanceKm: /** @param {number} [jdTT] */ (jdTT) => {
|
|
796
|
+
if (jdTT === undefined) return moonDistanceKm;
|
|
797
|
+
const d = moonDistanceMetresAtAge((startModelYearWithCorrection - yearFromJD(jdTT)) / 1e6);
|
|
798
|
+
return d === null ? moonDistanceKm : d / 1000;
|
|
799
|
+
},
|
|
786
800
|
getEccentricityBase: () => C.moonReference.moonOrbitalEccentricityBase,
|
|
787
801
|
deltaTSeconds: /** @param {number} jd */ (jd) => (jdTTFromUT(jd) - jd) * 86400,
|
|
788
802
|
jdToSIyear: yearFromJD,
|
|
@@ -816,6 +830,39 @@ export function assembleModel(C, F) {
|
|
|
816
830
|
},
|
|
817
831
|
});
|
|
818
832
|
|
|
833
|
+
// 20.3g — the solar-eclipse LOCATION tier: shadow geometry on the FULL
|
|
834
|
+
// series (sceneEvalAt — the truncated finder forms omit the fitted Lp
|
|
835
|
+
// anchor and the −2235·sin(Lp) β family, deliberate for finder
|
|
836
|
+
// statistics, required here) at the model's ABSOLUTE TT (deltaTStart +
|
|
837
|
+
// curve, the same convention every other ΔT consumer reads). Both axis
|
|
838
|
+
// conventions are measured against the NASA path-table centerlines —
|
|
839
|
+
// see eclipse/besselian.cjs.
|
|
840
|
+
const besselian = createBesselian({
|
|
841
|
+
moonFullAtDaysTT: /** @param {number} dDaysTT */ (dDaysTT) => {
|
|
842
|
+
const ev = moonSeries.sceneEvalAt(dDaysTT);
|
|
843
|
+
return { lonDeg: ev.lonDeg, latDeg: ev.latDeg, distKm: ev.distKm };
|
|
844
|
+
},
|
|
845
|
+
sunLonDegAt: /** @param {number} jdUT */ (jdUT) => eclipseFinders.sunLonDegAt(jdUT),
|
|
846
|
+
deltaTSecondsAt: /** @param {number} jd */ (jd) => (jdTTFromUT(jd) - jd) * 86400,
|
|
847
|
+
obliquityDegAt: obliquityDeg,
|
|
848
|
+
eccentricityAt,
|
|
849
|
+
perihelionLongitudeDegAt: earthPerihelionDeg,
|
|
850
|
+
yearFromJD,
|
|
851
|
+
constants: {
|
|
852
|
+
j2000JD,
|
|
853
|
+
julianCenturyDays,
|
|
854
|
+
earthDiameterKm: C.bodyDiametersKm.earth,
|
|
855
|
+
moonDiameterKm: C.bodyDiametersKm.moon,
|
|
856
|
+
sunDiameterKm: C.bodyDiametersKm.sun,
|
|
857
|
+
sunDistanceKm: currentAUDistance,
|
|
858
|
+
earthFlatteningInverse: C.physicalConstants.earthFlatteningInverseWGS84,
|
|
859
|
+
ttBridgeSeconds: C.earthOrbital.deltaTStart,
|
|
860
|
+
gmstMeanSiderealT0Deg: C.physicalConstants.gmstMeanSiderealT0Deg,
|
|
861
|
+
gmstMeanSiderealRateDegPerDay: C.physicalConstants.gmstMeanSiderealRateDegPerDay,
|
|
862
|
+
gmstMeanSiderealT2Deg: C.physicalConstants.gmstMeanSiderealT2Deg,
|
|
863
|
+
},
|
|
864
|
+
});
|
|
865
|
+
|
|
819
866
|
// ── The assembled surface ─────────────────────────────────────────────────
|
|
820
867
|
return Object.freeze({
|
|
821
868
|
time: Object.freeze({
|
|
@@ -874,6 +921,9 @@ export function assembleModel(C, F) {
|
|
|
874
921
|
findLunarInRange: /** @param {number} jdStart @param {number} jdEnd */ (jdStart, jdEnd) => eclipseFinders.findLunarEclipsesInRange(jdStart, jdEnd),
|
|
875
922
|
findSolarInRange: /** @param {number} jdStart @param {number} jdEnd */ (jdStart, jdEnd) => eclipseFinders.findSolarEclipsesInRange(jdStart, jdEnd),
|
|
876
923
|
deltaTSecondsAtJD: frameworkDeltaTSecondsAtJD,
|
|
924
|
+
// 20.3g location tier (see eclipse/besselian.cjs):
|
|
925
|
+
umbraGroundAtJD: /** @param {number} jd @returns {{latDeg: number, lonDeg: number} | null} */ (jd) => besselian.umbraGroundAt(jd),
|
|
926
|
+
solarLocalCircumstances: /** @param {number} jdGreatest @param {number} latDeg @param {number} lonDeg */ (jdGreatest, latDeg, lonDeg) => besselian.localCircumstances(jdGreatest, latDeg, lonDeg),
|
|
877
927
|
}),
|
|
878
928
|
climate: Object.freeze({
|
|
879
929
|
l1OrbitalPermil: evalClimateL1,
|
package/src/moon/series.cjs
CHANGED
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
* argsAt: (jdTT: number) => {Lp: number, D: number, M: number, Mp: number, F: number},
|
|
49
49
|
* eFactorForD: (dDaysTT: number, T: number, T2: number) => number,
|
|
50
50
|
* eFactorAtJdTT: (jdTT: number, T: number, T2: number) => number,
|
|
51
|
-
* getMoonDistanceKm: () => number,
|
|
51
|
+
* getMoonDistanceKm: (jdTT?: number) => number,
|
|
52
52
|
* getEccentricityBase: () => number,
|
|
53
53
|
* deltaTSeconds: (jdUT: number) => number,
|
|
54
54
|
* jdToSIyear: (jd: number) => number,
|
|
@@ -147,9 +147,17 @@ function createMoonSeries({ constants, fns }) {
|
|
|
147
147
|
* @param {number} Dr @param {number} Mr @param {number} Mpr
|
|
148
148
|
* @param {number} Fr @param {number} E @param {number} E2
|
|
149
149
|
* @returns {number} km */
|
|
150
|
-
|
|
150
|
+
// 20.3d(i): the Driver-1 ratio is evaluated AT THE EVALUATED EPOCH — the
|
|
151
|
+
// getter receives the evaluation jdTT so each engine can answer with its
|
|
152
|
+
// pure a_M(t) evaluator (per-jd deterministic; previously the browser
|
|
153
|
+
// answered with whatever epoch the render loop last set — a scene-state
|
|
154
|
+
// timing accident — while the Node engines pinned the J2000 constant).
|
|
155
|
+
// Engines whose getter ignores the argument keep their old behavior.
|
|
156
|
+
/** @param {number} Dr @param {number} Mr @param {number} Mpr @param {number} Fr
|
|
157
|
+
* @param {number} E @param {number} E2 @param {number} [jdTT] @returns {number} */
|
|
158
|
+
function fullDistanceKm(Dr, Mr, Mpr, Fr, E, E2, jdTT) {
|
|
151
159
|
const Sr = sumTableCos(moonR, Dr, Mr, Mpr, Fr, E, E2);
|
|
152
|
-
return (moonRMeanKm + Sr * 1e-3) * (getMoonDistanceKm() / moonDistanceJ2000Km);
|
|
160
|
+
return (moonRMeanKm + Sr * 1e-3) * (getMoonDistanceKm(jdTT) / moonDistanceJ2000Km);
|
|
153
161
|
}
|
|
154
162
|
|
|
155
163
|
/** The PRODUCTION scene evaluation at d days TT from J2000 (the caller
|
|
@@ -192,7 +200,7 @@ function createMoonSeries({ constants, fns }) {
|
|
|
192
200
|
const fullSl = Sl + (2 * eocHalf / D2R * 1e6) * Math.sin(Mpr)
|
|
193
201
|
+ (1.25 * eocHalf * eocHalf / D2R * 1e6) * Math.sin(2 * Mpr);
|
|
194
202
|
const lonDeg = Lp / D2R + fullSl * 1e-6 + moonMeeusLpCorrectionDeg;
|
|
195
|
-
const distKm = fullDistanceKm(Dr, Mr, Mpr, Fr, E, E2);
|
|
203
|
+
const distKm = fullDistanceKm(Dr, Mr, Mpr, Fr, E, E2, j2000JD + d);
|
|
196
204
|
return { thetaAddRad, lonDeg, latRad, latDeg, distKm, T };
|
|
197
205
|
}
|
|
198
206
|
|
|
@@ -242,7 +250,7 @@ function createMoonSeries({ constants, fns }) {
|
|
|
242
250
|
const args = argsAt(jdTT);
|
|
243
251
|
const Dr = args.D * D2R, Mr = args.M * D2R, Mpr = args.Mp * D2R, Fr = args.F * D2R;
|
|
244
252
|
const E = eFactorAtJdTT(jdTT, T, T * T);
|
|
245
|
-
return fullDistanceKm(Dr, Mr, Mpr, Fr, E, E * E);
|
|
253
|
+
return fullDistanceKm(Dr, Mr, Mpr, Fr, E, E * E, jdTT);
|
|
246
254
|
}
|
|
247
255
|
|
|
248
256
|
return { sceneEvalAt, truncatedLonDeg, truncatedBetaDeg, truncatedDistanceKm, additionalArgs };
|