@essrt/physics 1.4.0 → 1.5.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 +37 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@essrt/physics",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.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
|
/**
|
|
@@ -816,6 +817,39 @@ export function assembleModel(C, F) {
|
|
|
816
817
|
},
|
|
817
818
|
});
|
|
818
819
|
|
|
820
|
+
// 20.3g — the solar-eclipse LOCATION tier: shadow geometry on the FULL
|
|
821
|
+
// series (sceneEvalAt — the truncated finder forms omit the fitted Lp
|
|
822
|
+
// anchor and the −2235·sin(Lp) β family, deliberate for finder
|
|
823
|
+
// statistics, required here) at the model's ABSOLUTE TT (deltaTStart +
|
|
824
|
+
// curve, the same convention every other ΔT consumer reads). Both axis
|
|
825
|
+
// conventions are measured against the NASA path-table centerlines —
|
|
826
|
+
// see eclipse/besselian.cjs.
|
|
827
|
+
const besselian = createBesselian({
|
|
828
|
+
moonFullAtDaysTT: /** @param {number} dDaysTT */ (dDaysTT) => {
|
|
829
|
+
const ev = moonSeries.sceneEvalAt(dDaysTT);
|
|
830
|
+
return { lonDeg: ev.lonDeg, latDeg: ev.latDeg, distKm: ev.distKm };
|
|
831
|
+
},
|
|
832
|
+
sunLonDegAt: /** @param {number} jdUT */ (jdUT) => eclipseFinders.sunLonDegAt(jdUT),
|
|
833
|
+
deltaTSecondsAt: /** @param {number} jd */ (jd) => (jdTTFromUT(jd) - jd) * 86400,
|
|
834
|
+
obliquityDegAt: obliquityDeg,
|
|
835
|
+
eccentricityAt,
|
|
836
|
+
perihelionLongitudeDegAt: earthPerihelionDeg,
|
|
837
|
+
yearFromJD,
|
|
838
|
+
constants: {
|
|
839
|
+
j2000JD,
|
|
840
|
+
julianCenturyDays,
|
|
841
|
+
earthDiameterKm: C.bodyDiametersKm.earth,
|
|
842
|
+
moonDiameterKm: C.bodyDiametersKm.moon,
|
|
843
|
+
sunDiameterKm: C.bodyDiametersKm.sun,
|
|
844
|
+
sunDistanceKm: currentAUDistance,
|
|
845
|
+
earthFlatteningInverse: C.physicalConstants.earthFlatteningInverseWGS84,
|
|
846
|
+
ttBridgeSeconds: C.earthOrbital.deltaTStart,
|
|
847
|
+
gmstMeanSiderealT0Deg: C.physicalConstants.gmstMeanSiderealT0Deg,
|
|
848
|
+
gmstMeanSiderealRateDegPerDay: C.physicalConstants.gmstMeanSiderealRateDegPerDay,
|
|
849
|
+
gmstMeanSiderealT2Deg: C.physicalConstants.gmstMeanSiderealT2Deg,
|
|
850
|
+
},
|
|
851
|
+
});
|
|
852
|
+
|
|
819
853
|
// ── The assembled surface ─────────────────────────────────────────────────
|
|
820
854
|
return Object.freeze({
|
|
821
855
|
time: Object.freeze({
|
|
@@ -874,6 +908,9 @@ export function assembleModel(C, F) {
|
|
|
874
908
|
findLunarInRange: /** @param {number} jdStart @param {number} jdEnd */ (jdStart, jdEnd) => eclipseFinders.findLunarEclipsesInRange(jdStart, jdEnd),
|
|
875
909
|
findSolarInRange: /** @param {number} jdStart @param {number} jdEnd */ (jdStart, jdEnd) => eclipseFinders.findSolarEclipsesInRange(jdStart, jdEnd),
|
|
876
910
|
deltaTSecondsAtJD: frameworkDeltaTSecondsAtJD,
|
|
911
|
+
// 20.3g location tier (see eclipse/besselian.cjs):
|
|
912
|
+
umbraGroundAtJD: /** @param {number} jd @returns {{latDeg: number, lonDeg: number} | null} */ (jd) => besselian.umbraGroundAt(jd),
|
|
913
|
+
solarLocalCircumstances: /** @param {number} jdGreatest @param {number} latDeg @param {number} lonDeg */ (jdGreatest, latDeg, lonDeg) => besselian.localCircumstances(jdGreatest, latDeg, lonDeg),
|
|
877
914
|
}),
|
|
878
915
|
climate: Object.freeze({
|
|
879
916
|
l1OrbitalPermil: evalClimateL1,
|