@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,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meeus Ch. 47 lunar periodic series — THE shared implementation (8.2-6).
|
|
3
|
+
*
|
|
4
|
+
* Replaces THREE copies: the browser scene block (moveModel), the tools
|
|
5
|
+
* scene block (both the full production evaluation), and hosts the
|
|
6
|
+
* TRUNCATED eclipse-finder variant beside it. The dual form is DELIBERATE
|
|
7
|
+
* (doc 66 §Layer 1b): the truncated `_eclMoon*` helpers are the form the
|
|
8
|
+
* certified eclipse statistics were produced with (NASA canon recall
|
|
9
|
+
* 99.58/74.62/98.66, knife-edge at the γ ≈ 1.0/1.5 boundaries) — upgrading
|
|
10
|
+
* them to the full series is a RE-CERTIFICATION item, not a refactor.
|
|
11
|
+
*
|
|
12
|
+
* Full scene form: 60 longitude + 60 latitude terms with E/E² scaling on
|
|
13
|
+
* M-bearing terms, A1 (Venus, Meeus-observed rate — the 18V−16E−M′
|
|
14
|
+
* near-resonance makes it hypersensitive; no lattice identity), A2/A3
|
|
15
|
+
* (D2-derived lattice rates, CHAIN-INTEGRATED under deep time through
|
|
16
|
+
* their identified content), all six latitude corrections, the EoC-half
|
|
17
|
+
* subtraction (the off-centre orbit geometry already provides half the
|
|
18
|
+
* equation of centre), and the two-term ellipse distance.
|
|
19
|
+
*
|
|
20
|
+
* The correction literals (3958/1962/318; −2235/382/175/175/127/−115;
|
|
21
|
+
* phases 119.75/53.09/313.45; Meeus A/B rates 479264.290/481266.484) are
|
|
22
|
+
* Meeus Ch. 47 STRUCTURE and live here, like the argument polynomials
|
|
23
|
+
* (8.2-5 S2 precedent). The 60-term tables are INJECTED — they are fitted
|
|
24
|
+
* coefficients (the engines' two sources are byte-identical).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
'use strict';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {Array<[number, number, number, number, number]>} MeeusTermTable
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {{
|
|
35
|
+
* constants: {
|
|
36
|
+
* moonL: MeeusTermTable,
|
|
37
|
+
* moonB: MeeusTermTable,
|
|
38
|
+
* j2000JD: number,
|
|
39
|
+
* julianCenturyDays: number,
|
|
40
|
+
* moonMeeusLpCorrectionDeg: number,
|
|
41
|
+
* fwA2RateDegPerCy: number,
|
|
42
|
+
* fwA3RateDegPerCy: number,
|
|
43
|
+
* },
|
|
44
|
+
* fns: {
|
|
45
|
+
* argsAt: (jdTT: number) => {Lp: number, D: number, M: number, Mp: number, F: number},
|
|
46
|
+
* eFactorForD: (dDaysTT: number, T: number, T2: number) => number,
|
|
47
|
+
* eFactorAtJdTT: (jdTT: number, T: number, T2: number) => number,
|
|
48
|
+
* getMoonDistanceKm: () => number,
|
|
49
|
+
* getEccentricityBase: () => number,
|
|
50
|
+
* deltaTSeconds: (jdUT: number) => number,
|
|
51
|
+
* jdToSIyear: (jd: number) => number,
|
|
52
|
+
* tropicalOrbitsBetween: (yearA: number, yearB: number) => (number | null),
|
|
53
|
+
* apsidalOfDateCyclesBetween: (yearA: number, yearB: number) => (number | null),
|
|
54
|
+
* cyclesBetween: (yearA: number, yearB: number, divisorN: number) => (number | null),
|
|
55
|
+
* jupiterOrbitsBetween: (yearA: number, yearB: number) => (number | null),
|
|
56
|
+
* isDeepTime: () => boolean,
|
|
57
|
+
* isFrameworkNative: () => boolean,
|
|
58
|
+
* },
|
|
59
|
+
* }} deps — argsAt is the engine's dispatcher (probe hook and mode toggle
|
|
60
|
+
* ride along); eFactorForD preserves each engine's exact E-factor call
|
|
61
|
+
* shape (S11: the two convert d→years with differently-associated
|
|
62
|
+
* expressions); distance/eccentricity are GETTERS because the browser's
|
|
63
|
+
* moonDistance is deep-time-mutable.
|
|
64
|
+
*/
|
|
65
|
+
function createMoonSeries({ constants, fns }) {
|
|
66
|
+
const {
|
|
67
|
+
moonL, moonB, j2000JD, julianCenturyDays,
|
|
68
|
+
moonMeeusLpCorrectionDeg, fwA2RateDegPerCy, fwA3RateDegPerCy,
|
|
69
|
+
} = constants;
|
|
70
|
+
const {
|
|
71
|
+
argsAt, eFactorForD, eFactorAtJdTT, getMoonDistanceKm, getEccentricityBase, deltaTSeconds,
|
|
72
|
+
jdToSIyear, tropicalOrbitsBetween, apsidalOfDateCyclesBetween,
|
|
73
|
+
cyclesBetween, jupiterOrbitsBetween, isDeepTime, isFrameworkNative,
|
|
74
|
+
} = fns;
|
|
75
|
+
|
|
76
|
+
const D2R = Math.PI / 180;
|
|
77
|
+
|
|
78
|
+
/** A1/A2/A3 in radians at T centuries TT (deep time: A2/A3 chain-integrated).
|
|
79
|
+
* @param {number} T @param {number} dDaysTT
|
|
80
|
+
* @returns {{A1: number, A2: number, A3: number}} */
|
|
81
|
+
function additionalArgs(T, dDaysTT) {
|
|
82
|
+
const A1 = (119.75 + 131.849 * T) * D2R;
|
|
83
|
+
let a2Deg = 53.09 + (isFrameworkNative() ? fwA2RateDegPerCy : 479264.290) * T;
|
|
84
|
+
let a3Deg = 313.45 + (isFrameworkNative() ? fwA3RateDegPerCy : 481266.484) * T;
|
|
85
|
+
if (isDeepTime() && isFrameworkNative()) {
|
|
86
|
+
const yA0 = jdToSIyear(j2000JD);
|
|
87
|
+
const yA = jdToSIyear(j2000JD + dDaysTT);
|
|
88
|
+
const nT = tropicalOrbitsBetween(yA0, yA);
|
|
89
|
+
const nAps = apsidalOfDateCyclesBetween(yA0, yA);
|
|
90
|
+
const nP13 = cyclesBetween(yA0, yA, 13);
|
|
91
|
+
const nJ = jupiterOrbitsBetween(yA0, yA);
|
|
92
|
+
if (nT !== null && nAps !== null && nP13 !== null && nJ !== null) {
|
|
93
|
+
a3Deg = 313.45 + 360 * (nT - nP13);
|
|
94
|
+
a2Deg = 53.09 + 360 * (nT + nAps - 2 * nJ);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return { A1, A2: a2Deg * D2R, A3: a3Deg * D2R };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** One 60-term table pass with E/E² on M-bearing terms (micro-degrees).
|
|
101
|
+
* @param {MeeusTermTable} table @param {number} Dr @param {number} Mr
|
|
102
|
+
* @param {number} Mpr @param {number} Fr @param {number} E
|
|
103
|
+
* @param {number} E2 @returns {number} */
|
|
104
|
+
function sumTable(table, Dr, Mr, Mpr, Fr, E, E2) {
|
|
105
|
+
let s = 0;
|
|
106
|
+
for (let i = 0; i < table.length; i++) {
|
|
107
|
+
const r = table[i];
|
|
108
|
+
const arg = r[0] * Dr + r[1] * Mr + r[2] * Mpr + r[3] * Fr;
|
|
109
|
+
let term = r[4] * Math.sin(arg);
|
|
110
|
+
const absM = r[1] < 0 ? -r[1] : r[1];
|
|
111
|
+
if (absM === 1) term *= E;
|
|
112
|
+
else if (absM === 2) term *= E2;
|
|
113
|
+
s += term;
|
|
114
|
+
}
|
|
115
|
+
return s;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The PRODUCTION scene evaluation at d days TT from J2000 (the caller
|
|
119
|
+
* owns the UT→TT conversion — engine timing conventions differ).
|
|
120
|
+
* Returns everything the scene blocks write:
|
|
121
|
+
* thetaAddRad — the hierarchy θ increment (EoC-half-subtracted Σl);
|
|
122
|
+
* lonDeg — full ecliptic longitude incl. moonMeeusLpCorrection;
|
|
123
|
+
* latRad/latDeg — Σb with all six corrections (BOTH computed from Σb
|
|
124
|
+
* directly: the browser stores radians, the Node engine degrees, and
|
|
125
|
+
* (x·D2R)/D2R is not bit-exactly x); distKm — two-term ellipse; T.
|
|
126
|
+
* @param {number} dDaysTT
|
|
127
|
+
* @returns {{thetaAddRad: number, lonDeg: number, latRad: number, latDeg: number, distKm: number, T: number}} */
|
|
128
|
+
function sceneEvalAt(dDaysTT) {
|
|
129
|
+
const d = dDaysTT;
|
|
130
|
+
const T = d / julianCenturyDays;
|
|
131
|
+
const T2 = T * T;
|
|
132
|
+
const args = argsAt(j2000JD + d);
|
|
133
|
+
const Lp = args.Lp * D2R;
|
|
134
|
+
const Dr = args.D * D2R, Mr = args.M * D2R, Mpr = args.Mp * D2R, Fr = args.F * D2R;
|
|
135
|
+
const E = eFactorForD(d, T, T2);
|
|
136
|
+
const E2 = E * E;
|
|
137
|
+
const { A1, A2, A3 } = additionalArgs(T, d);
|
|
138
|
+
|
|
139
|
+
let Sl = sumTable(moonL, Dr, Mr, Mpr, Fr, E, E2);
|
|
140
|
+
Sl += 3958 * Math.sin(A1) + 1962 * Math.sin(Lp - Fr) + 318 * Math.sin(A2);
|
|
141
|
+
// Subtract the EoC portion the off-centre orbit geometry already provides.
|
|
142
|
+
const eocHalf = getEccentricityBase() / 2;
|
|
143
|
+
Sl -= (2 * eocHalf / D2R * 1e6) * Math.sin(Mpr);
|
|
144
|
+
Sl -= (1.25 * eocHalf * eocHalf / D2R * 1e6) * Math.sin(2 * Mpr);
|
|
145
|
+
const thetaAddRad = Sl * 1e-6 * D2R;
|
|
146
|
+
|
|
147
|
+
let Sb = sumTable(moonB, Dr, Mr, Mpr, Fr, E, E2);
|
|
148
|
+
Sb += -2235 * Math.sin(Lp) + 382 * Math.sin(A3);
|
|
149
|
+
Sb += 175 * Math.sin(A1 - Fr) + 175 * Math.sin(A1 + Fr);
|
|
150
|
+
Sb += 127 * Math.sin(Lp - Mpr) - 115 * Math.sin(Lp + Mpr);
|
|
151
|
+
const latRad = Sb * 1e-6 * D2R;
|
|
152
|
+
const latDeg = Sb * 1e-6;
|
|
153
|
+
|
|
154
|
+
// Full longitude re-adds the EoC half that θ absorbed.
|
|
155
|
+
const fullSl = Sl + (2 * eocHalf / D2R * 1e6) * Math.sin(Mpr)
|
|
156
|
+
+ (1.25 * eocHalf * eocHalf / D2R * 1e6) * Math.sin(2 * Mpr);
|
|
157
|
+
const lonDeg = Lp / D2R + fullSl * 1e-6 + moonMeeusLpCorrectionDeg;
|
|
158
|
+
const distKm = getMoonDistanceKm() * (1 - getEccentricityBase() * Math.cos(Mpr));
|
|
159
|
+
return { thetaAddRad, lonDeg, latRad, latDeg, distKm, T };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** TRUNCATED eclipse-finder longitude (doc 66 §Layer 1b): the certified
|
|
163
|
+
* form — no EoC subtraction, no A3/latitude-family, no LpCorrection.
|
|
164
|
+
* Accepts JD_UT; converts to TT via the injected ΔT.
|
|
165
|
+
* @param {number} jdUT @returns {number} degrees 0–360 */
|
|
166
|
+
function truncatedLonDeg(jdUT) {
|
|
167
|
+
const jdTT = jdUT + deltaTSeconds(jdUT) / 86400;
|
|
168
|
+
const T = (jdTT - j2000JD) / julianCenturyDays;
|
|
169
|
+
const T2 = T * T;
|
|
170
|
+
const args = argsAt(jdTT);
|
|
171
|
+
const LpMean = args.Lp;
|
|
172
|
+
const Dr = args.D * D2R, Mr = args.M * D2R, Mpr = args.Mp * D2R, Fr = args.F * D2R;
|
|
173
|
+
// Original-JD call shape — (j2000JD + (jdTT − j2000JD)) is NOT bit-exactly
|
|
174
|
+
// jdTT, so the truncated path keeps its own E-factor entry.
|
|
175
|
+
const E = eFactorAtJdTT(jdTT, T, T2);
|
|
176
|
+
const E2 = E * E;
|
|
177
|
+
let Sl = sumTable(moonL, Dr, Mr, Mpr, Fr, E, E2);
|
|
178
|
+
const A1 = (119.75 + 131.849 * T) * D2R;
|
|
179
|
+
const A2 = (53.09 + (isFrameworkNative() ? fwA2RateDegPerCy : 479264.290) * T) * D2R;
|
|
180
|
+
Sl += 3958 * Math.sin(A1) + 1962 * Math.sin(LpMean * D2R - Fr) + 318 * Math.sin(A2);
|
|
181
|
+
return (((LpMean + Sl * 1e-6) % 360) + 360) % 360;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** TRUNCATED eclipse-finder latitude (doc 66 §Layer 1b): table only — the
|
|
185
|
+
* six additional corrections are DELIBERATELY absent (the certified
|
|
186
|
+
* eclipse statistics were produced with this form; the ~2.2 mdeg
|
|
187
|
+
* −2235·sin(Lp) family would move knife-edge canon events).
|
|
188
|
+
* @param {number} jdUT @returns {number} degrees */
|
|
189
|
+
function truncatedBetaDeg(jdUT) {
|
|
190
|
+
const jdTT = jdUT + deltaTSeconds(jdUT) / 86400;
|
|
191
|
+
const T = (jdTT - j2000JD) / julianCenturyDays;
|
|
192
|
+
const T2 = T * T;
|
|
193
|
+
const args = argsAt(jdTT);
|
|
194
|
+
const Dr = args.D * D2R, Mr = args.M * D2R, Mpr = args.Mp * D2R, Fr = args.F * D2R;
|
|
195
|
+
const E = eFactorAtJdTT(jdTT, T, T2);
|
|
196
|
+
const E2 = E * E;
|
|
197
|
+
return sumTable(moonB, Dr, Mr, Mpr, Fr, E, E2) * 1e-6;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Two-term ellipse distance at JD_UT (shared with the scene form).
|
|
201
|
+
* @param {number} jdUT @returns {number} km */
|
|
202
|
+
function truncatedDistanceKm(jdUT) {
|
|
203
|
+
const jdTT = jdUT + deltaTSeconds(jdUT) / 86400;
|
|
204
|
+
const Mpr = argsAt(jdTT).Mp * D2R;
|
|
205
|
+
return getMoonDistanceKm() * (1 - getEccentricityBase() * Math.cos(Mpr));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return { sceneEvalAt, truncatedLonDeg, truncatedBetaDeg, truncatedDistanceKm, additionalArgs };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
module.exports = { createMoonSeries };
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integrated-phase machinery — THE shared implementation (Phase 7.1).
|
|
3
|
+
*
|
|
4
|
+
* The cumulative ∫1/H(t)dt table and everything derived from it: cycle counts,
|
|
5
|
+
* the J2000 drift correction, and the inverse lookup. Extracted VERBATIM from
|
|
6
|
+
* tools/lib/deep-time.js, which was itself the port of src/script.js — this
|
|
7
|
+
* file replaces both copies (and is the reference the website's deepTime.ts
|
|
8
|
+
* must match; Phase E measured what happens when it doesn't).
|
|
9
|
+
*
|
|
10
|
+
* CommonJS on purpose: `tools/lib` is CJS and cannot require an ESM module
|
|
11
|
+
* synchronously; ESM (`src/script.js`, bundlers) imports CJS natively. One
|
|
12
|
+
* file, every engine.
|
|
13
|
+
*
|
|
14
|
+
* THE CONVENTIONS ARE LOAD-BEARING — the fitted coefficients were produced
|
|
15
|
+
* against THIS discretization, so the numerics ship with the coefficients:
|
|
16
|
+
*
|
|
17
|
+
* - TRAPEZOID rule on 10-kyr cells with LINEAR interpolation between cells.
|
|
18
|
+
* An analytically better integrator is WRONG here: the website's adaptive
|
|
19
|
+
* Simpson disagreed by ~1e-6 cycles at −280 kyr = ~18 s of cardinal-point
|
|
20
|
+
* JD through the ×16 braid phase (Gate E measured 24.7 s before the fix).
|
|
21
|
+
* - R2: the table must be built under the LATTICE α — the CONSTANT moment of
|
|
22
|
+
* inertia, not the climate-modulated α(t). The `holisticHAtAgeMa` the
|
|
23
|
+
* caller injects must already be pinned (see each engine's construction
|
|
24
|
+
* site). The always-on climate modulation integrates into a linear phase
|
|
25
|
+
* drift (the same ~18 s class — the dominant Phase E bug).
|
|
26
|
+
* - R3: the drift-correction anchor is identified by CALL SHAPE, never by
|
|
27
|
+
* distance from J2000 — the old distance heuristic put a 2·drift
|
|
28
|
+
* = 6.80e-5-cycle STEP DISCONTINUITY at yearB == yearA, worth 0.393 d of
|
|
29
|
+
* apsis displacement and 100% of 6d's anomalistic residual.
|
|
30
|
+
* - The table zero sits at the cell nearest `tableAnchorYear` (2000.5,
|
|
31
|
+
* startmodelYear); the drift reference is `driftRefYear`
|
|
32
|
+
* (startModelYearWithCorrection ≈ 2000.4977) — TWO different constants,
|
|
33
|
+
* half a day apart. Do not "unify" them.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
'use strict';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {{
|
|
40
|
+
* holisticHAtAgeMa: (tMa: number) => (number | null),
|
|
41
|
+
* tableAnchorYear: number,
|
|
42
|
+
* driftRefYear: number,
|
|
43
|
+
* hJ2000: number,
|
|
44
|
+
* yearMin?: number,
|
|
45
|
+
* yearMax?: number,
|
|
46
|
+
* stepYears?: number,
|
|
47
|
+
* }} cfg — holisticHAtAgeMa MUST be the lattice-α form; tableAnchorYear is
|
|
48
|
+
* startmodelYear (2000.5, table zero + t_Ma convention); driftRefYear is
|
|
49
|
+
* startModelYearWithCorrection (the R3 drift anchor); hJ2000 the snapshot
|
|
50
|
+
* rate the drift compares against; 10-kyr cells match both engines'
|
|
51
|
+
* historic tables.
|
|
52
|
+
*/
|
|
53
|
+
function createPhaseMachinery({
|
|
54
|
+
holisticHAtAgeMa,
|
|
55
|
+
tableAnchorYear,
|
|
56
|
+
driftRefYear,
|
|
57
|
+
hJ2000,
|
|
58
|
+
yearMin = -500e6,
|
|
59
|
+
yearMax = 500e6,
|
|
60
|
+
stepYears = 10000,
|
|
61
|
+
}) {
|
|
62
|
+
/** @type {Float64Array | null} */
|
|
63
|
+
let table = null;
|
|
64
|
+
let j2000Idx = -1;
|
|
65
|
+
|
|
66
|
+
function ensureTable() {
|
|
67
|
+
if (table !== null) return;
|
|
68
|
+
const N = Math.ceil((yearMax - yearMin) / stepYears) + 1;
|
|
69
|
+
const t = new Float64Array(N);
|
|
70
|
+
j2000Idx = Math.round((tableAnchorYear - yearMin) / stepYears);
|
|
71
|
+
|
|
72
|
+
/** @param {number} year */
|
|
73
|
+
const invH = (year) => {
|
|
74
|
+
const t_Ma = (tableAnchorYear - year) / 1e6;
|
|
75
|
+
const H = holisticHAtAgeMa(t_Ma);
|
|
76
|
+
return H === null ? null : 1 / H;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
t[j2000Idx] = 0;
|
|
80
|
+
let prev = invH(yearMin + j2000Idx * stepYears);
|
|
81
|
+
for (let i = j2000Idx + 1; i < N; i++) {
|
|
82
|
+
const year_i = yearMin + i * stepYears;
|
|
83
|
+
const curr = invH(year_i);
|
|
84
|
+
t[i] = (prev !== null && curr !== null)
|
|
85
|
+
? t[i - 1] + 0.5 * (prev + curr) * stepYears
|
|
86
|
+
: NaN;
|
|
87
|
+
prev = curr;
|
|
88
|
+
}
|
|
89
|
+
prev = invH(yearMin + j2000Idx * stepYears);
|
|
90
|
+
for (let i = j2000Idx - 1; i >= 0; i--) {
|
|
91
|
+
const year_i = yearMin + i * stepYears;
|
|
92
|
+
const curr = invH(year_i);
|
|
93
|
+
t[i] = (prev !== null && curr !== null)
|
|
94
|
+
? t[i + 1] - 0.5 * (prev + curr) * stepYears
|
|
95
|
+
: NaN;
|
|
96
|
+
prev = curr;
|
|
97
|
+
}
|
|
98
|
+
table = t;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Cumulative ∫1/H at a year (linear interpolation). null outside the range
|
|
102
|
+
* or where the physics is undefined (past the tidal-lock asymptote).
|
|
103
|
+
* @param {number} year @returns {number | null} */
|
|
104
|
+
function cumulAtYear(year) {
|
|
105
|
+
ensureTable();
|
|
106
|
+
if (year < yearMin || year > yearMax) return null;
|
|
107
|
+
const t = /** @type {Float64Array} */ (table);
|
|
108
|
+
const idx_f = (year - yearMin) / stepYears;
|
|
109
|
+
const idx_lo = Math.floor(idx_f);
|
|
110
|
+
const idx_hi = Math.min(idx_lo + 1, t.length - 1);
|
|
111
|
+
const v_lo = t[idx_lo];
|
|
112
|
+
const v_hi = t[idx_hi];
|
|
113
|
+
if (Number.isNaN(v_lo) || Number.isNaN(v_hi)) return null;
|
|
114
|
+
return v_lo + (idx_f - idx_lo) * (v_hi - v_lo);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** ∫_{yearA}^{yearB} 1/H(t') dt'. null if either endpoint is out of domain.
|
|
118
|
+
* @param {number} yearA @param {number} yearB @returns {number | null} */
|
|
119
|
+
function integralBetween(yearA, yearB) {
|
|
120
|
+
if (yearA === yearB) return 0;
|
|
121
|
+
const cA = cumulAtYear(yearA);
|
|
122
|
+
const cB = cumulAtYear(yearB);
|
|
123
|
+
if (cA === null || cB === null) return null;
|
|
124
|
+
return cB - cA;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** How far the integrated form has drifted from the J2000-snapshot form over
|
|
128
|
+
* (yearA → driftRefYear). Subtracting it at the anchor restores the
|
|
129
|
+
* snapshot-fitted harmonic calibration at J2000 without changing the
|
|
130
|
+
* integrated form's shape at deep time.
|
|
131
|
+
* @param {number} yearA @returns {number} */
|
|
132
|
+
function j2000Drift(yearA) {
|
|
133
|
+
const integral = integralBetween(yearA, driftRefYear);
|
|
134
|
+
if (integral === null) return 0;
|
|
135
|
+
const snapshot = (driftRefYear - yearA) / hJ2000;
|
|
136
|
+
return integral - snapshot;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Total cycles between two years for a cycle of period H/divisorN.
|
|
140
|
+
* R3: the anchor is identified from the CALL SHAPE — two shapes exist,
|
|
141
|
+
* (anchor, movingYear, N) and (J2000, anchor, N) where yearA IS the J2000
|
|
142
|
+
* reference itself. The correction depends only on a FIXED endpoint, so it
|
|
143
|
+
* is constant across any scan. Returns null past the tidal-lock asymptote.
|
|
144
|
+
* @param {number} yearA @param {number} yearB @param {number} divisorN
|
|
145
|
+
* @returns {number | null} */
|
|
146
|
+
function cyclesBetween(yearA, yearB, divisorN) {
|
|
147
|
+
const integral = integralBetween(yearA, yearB);
|
|
148
|
+
if (integral === null) return null;
|
|
149
|
+
const anchorIsA = (yearA !== driftRefYear);
|
|
150
|
+
const correction = anchorIsA ? j2000Drift(yearA) : -j2000Drift(yearB);
|
|
151
|
+
return divisorN * (integral - correction);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Inverse of cumulAtYear — the year at a given cumulative ∫1/H (binary
|
|
155
|
+
* search over the monotone table). null outside the table domain.
|
|
156
|
+
* @param {number} targetCumul @returns {number | null} */
|
|
157
|
+
function yearAtCumul(targetCumul) {
|
|
158
|
+
ensureTable();
|
|
159
|
+
const t = /** @type {Float64Array} */ (table);
|
|
160
|
+
const N = t.length;
|
|
161
|
+
let lo = 0, hi = N - 1;
|
|
162
|
+
while (lo < N && Number.isNaN(t[lo])) lo++;
|
|
163
|
+
while (hi >= 0 && Number.isNaN(t[hi])) hi--;
|
|
164
|
+
if (lo >= hi) return null;
|
|
165
|
+
if (targetCumul < t[lo] || targetCumul > t[hi]) return null;
|
|
166
|
+
while (lo < hi - 1) {
|
|
167
|
+
const mid = (lo + hi) >> 1;
|
|
168
|
+
if (t[mid] <= targetCumul) lo = mid; else hi = mid;
|
|
169
|
+
}
|
|
170
|
+
const v_lo = t[lo], v_hi = t[hi];
|
|
171
|
+
const frac = (v_lo === v_hi) ? 0 : (targetCumul - v_lo) / (v_hi - v_lo);
|
|
172
|
+
return yearMin + (lo + frac) * stepYears;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Grid geometry — consumed by the engines' days/pos tables, which share
|
|
176
|
+
* this grid but keep their own integrands (they move at Phase 8). */
|
|
177
|
+
function grid() {
|
|
178
|
+
ensureTable();
|
|
179
|
+
const t = /** @type {Float64Array} */ (table);
|
|
180
|
+
return { yearMin, yearMax, stepYears, j2000Idx, length: t.length };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return { ensureTable, cumulAtYear, integralBetween, j2000Drift, cyclesBetween, yearAtCumul, grid };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = { createPhaseMachinery };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dynamic ascending node — rate-based segment integration (Phase 8.3, L5).
|
|
3
|
+
*
|
|
4
|
+
* dΩ/dε = −sin(Ω)/tan(i), integrated over obliquity change with segment
|
|
5
|
+
* handling at obliquity extrema and Earth-inclination crossovers (the
|
|
6
|
+
* inclination DIRECTION flips the sign of the effect per segment).
|
|
7
|
+
*
|
|
8
|
+
* Extracted from tools/lib/orbital-engine.js
|
|
9
|
+
* calculateDynamicAscendingNodeFromTilts (itself the mirror of the
|
|
10
|
+
* browser's 6-arg variant — S-P5). The BROWSER variant remains engine-side
|
|
11
|
+
* this slice: neither copy is fixture-pinned, so the signature unification
|
|
12
|
+
* is deferred to the factory pass, WITH probes first (recorded follow-up).
|
|
13
|
+
*
|
|
14
|
+
* §2h BOUNDARY: this module takes {ascendingNodeDeg, inclinationDeg} — the
|
|
15
|
+
* Tychosium `orbitTilta/orbitTiltb` scheme names never enter the package;
|
|
16
|
+
* the engines own the atan2/hypot decomposition at their edge.
|
|
17
|
+
*
|
|
18
|
+
* All time-dependent inputs are INJECTED per call: the engine's obliquity
|
|
19
|
+
* and Earth-inclination evaluators, the precomputed obliquity-extrema
|
|
20
|
+
* lookup, the inclination-crossing finder, and (optionally) the per-planet
|
|
21
|
+
* dynamic ecliptic inclination.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {{ ascendingNodeDeg: number, inclinationDeg: number }} tilt —
|
|
28
|
+
* static node and inclination (engine-decomposed from its scene scheme)
|
|
29
|
+
* @param {number} currentYear
|
|
30
|
+
* @param {{
|
|
31
|
+
* obliquityAt: (year: number) => number,
|
|
32
|
+
* earthInclinationAt: (year: number) => number,
|
|
33
|
+
* obliquityExtremaInRange: (yearMin: number, yearMax: number) => number[],
|
|
34
|
+
* inclinationCrossingsInRange: (inclinationDeg: number, yearMin: number, yearMax: number) => number[],
|
|
35
|
+
* eclipticInclinationAt: ((year: number) => number) | null,
|
|
36
|
+
* earthInclinationMeanDeg: number,
|
|
37
|
+
* earthInclinationAmplitudeDeg: number,
|
|
38
|
+
* epochYear?: number,
|
|
39
|
+
* }} deps — eclipticInclinationAt non-null enables the dynamic-inclination
|
|
40
|
+
* crossover search and per-segment rates (the planetName path).
|
|
41
|
+
* @returns {number} dynamic ascending node longitude, degrees 0–360
|
|
42
|
+
*/
|
|
43
|
+
function integrateAscendingNode(tilt, currentYear, deps) {
|
|
44
|
+
const DEG2RAD = Math.PI / 180;
|
|
45
|
+
const RAD2DEG = 180 / Math.PI;
|
|
46
|
+
|
|
47
|
+
const staticOmega = ((tilt.ascendingNodeDeg % 360) + 360) % 360;
|
|
48
|
+
const planetInclination = tilt.inclinationDeg;
|
|
49
|
+
|
|
50
|
+
if (planetInclination < 1e-6) return staticOmega;
|
|
51
|
+
|
|
52
|
+
const i = planetInclination * DEG2RAD;
|
|
53
|
+
const OmegaRad = staticOmega * DEG2RAD;
|
|
54
|
+
const tanI = Math.tan(i);
|
|
55
|
+
if (Math.abs(tanI) < 1e-10) return staticOmega;
|
|
56
|
+
|
|
57
|
+
const sinOmega = Math.sin(OmegaRad);
|
|
58
|
+
const EPOCH_YEAR = deps.epochYear !== undefined ? deps.epochYear : 2000;
|
|
59
|
+
|
|
60
|
+
/** @param {number} fromYear @param {number} toYear @returns {number} */
|
|
61
|
+
const integrateEffect = (fromYear, toYear) => {
|
|
62
|
+
if (Math.abs(toYear - fromYear) < 0.1) return 0;
|
|
63
|
+
|
|
64
|
+
const yearMin = Math.min(fromYear, toYear);
|
|
65
|
+
const yearMax = Math.max(fromYear, toYear);
|
|
66
|
+
const dir = toYear >= fromYear ? 1 : -1; // integration direction — signs the total
|
|
67
|
+
|
|
68
|
+
/** @type {number[]} */
|
|
69
|
+
let criticalYears = [yearMin, yearMax];
|
|
70
|
+
|
|
71
|
+
criticalYears.push(...deps.obliquityExtremaInRange(yearMin, yearMax));
|
|
72
|
+
|
|
73
|
+
// Find ALL inclination crossings
|
|
74
|
+
const minEarthIncl = deps.earthInclinationMeanDeg - deps.earthInclinationAmplitudeDeg;
|
|
75
|
+
const maxEarthIncl = deps.earthInclinationMeanDeg + deps.earthInclinationAmplitudeDeg;
|
|
76
|
+
|
|
77
|
+
// With a dynamic ecliptic inclination the planet can enter Earth's range
|
|
78
|
+
// even if the static value is outside — always search for crossovers.
|
|
79
|
+
if (deps.eclipticInclinationAt || (planetInclination >= minEarthIncl && planetInclination <= maxEarthIncl)) {
|
|
80
|
+
const crossIncl = deps.eclipticInclinationAt
|
|
81
|
+
? deps.eclipticInclinationAt((yearMin + yearMax) / 2)
|
|
82
|
+
: planetInclination;
|
|
83
|
+
criticalYears.push(...deps.inclinationCrossingsInRange(crossIncl, yearMin, yearMax));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
criticalYears = [...new Set(criticalYears)].sort((a, b) => a - b);
|
|
87
|
+
|
|
88
|
+
// Integrate over segments
|
|
89
|
+
let effect = 0;
|
|
90
|
+
for (let idx = 0; idx < criticalYears.length - 1; idx++) {
|
|
91
|
+
const segStart = criticalYears[idx];
|
|
92
|
+
const segEnd = criticalYears[idx + 1];
|
|
93
|
+
|
|
94
|
+
const oblStart = deps.obliquityAt(segStart);
|
|
95
|
+
const oblEnd = deps.obliquityAt(segEnd);
|
|
96
|
+
const deltaObl = (oblEnd - oblStart) * DEG2RAD;
|
|
97
|
+
|
|
98
|
+
const midYear = (segStart + segEnd) / 2;
|
|
99
|
+
const earthInclAtMid = deps.earthInclinationAt(midYear);
|
|
100
|
+
|
|
101
|
+
const dynIncl = deps.eclipticInclinationAt
|
|
102
|
+
? deps.eclipticInclinationAt(midYear)
|
|
103
|
+
: planetInclination;
|
|
104
|
+
const inclDirection = earthInclAtMid > dynIncl ? 1 : -1;
|
|
105
|
+
const dynTanI = Math.tan(dynIncl * DEG2RAD);
|
|
106
|
+
if (Math.abs(dynTanI) < 1e-10) continue; // skip near-zero inclination
|
|
107
|
+
const segRate = -sinOmega / dynTanI;
|
|
108
|
+
|
|
109
|
+
effect += segRate * inclDirection * deltaObl * RAD2DEG;
|
|
110
|
+
}
|
|
111
|
+
return effect * dir;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const effectFromEpoch = integrateEffect(EPOCH_YEAR, currentYear);
|
|
115
|
+
|
|
116
|
+
let newOmega = staticOmega + effectFromEpoch;
|
|
117
|
+
return ((newOmega % 360) + 360) % 360;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = { integrateAscendingNode };
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fitted post-hoc corrections — THE shared evaluators (Phase 8.3, L8).
|
|
3
|
+
*
|
|
4
|
+
* The parallax RA/Dec basis: ~80 fitted coefficient slots (A..CA) over a
|
|
5
|
+
* geometric state (orbital phase u from the ascending node, inverse
|
|
6
|
+
* geocentric distance, inverse heliocentric distance, Julian centuries,
|
|
7
|
+
* the triple-synodic conjunction phase, the Sun mean longitude, and the
|
|
8
|
+
* inner-planet mean anomaly). ONE basis served four hand-copies: browser
|
|
9
|
+
* dec, browser RA, Node dec, Node RA (~80 duplicated terms each).
|
|
10
|
+
*
|
|
11
|
+
* MATCHED PAIR: these coefficients were fitted against exactly this basis
|
|
12
|
+
* and this state derivation — the expression text is transcribed verbatim
|
|
13
|
+
* (including the (x || 0) missing-slot guards and the AU_ key, which
|
|
14
|
+
* avoids a collision with the AU unit name). Do not simplify.
|
|
15
|
+
*
|
|
16
|
+
* The engines OWN the state derivation (scene vectors, JD conventions) and
|
|
17
|
+
* the application sign convention (RA subtracts, Dec adds) — this module
|
|
18
|
+
* only evaluates.
|
|
19
|
+
*
|
|
20
|
+
* GRAVITATION (planet-planet perturbation harmonics): evaluated here PER
|
|
21
|
+
* TERM — the engines apply each term inside their own loops, because
|
|
22
|
+
* (x−a)−b ≠ x−(a+b) at the last bit and the fixtures pin the per-term
|
|
23
|
+
* application order.
|
|
24
|
+
*
|
|
25
|
+
* ELONGATION (elongation × Earth-perihelion geometry, 21 fitted slots per
|
|
26
|
+
* axis): BROWSER ASSOCIATION FORM — the six d² slots per table use the
|
|
27
|
+
* precomputed invD² ((a·b)·invD²); the Node mirror carried inline
|
|
28
|
+
* invD·invD (((a·b)·invD)·invD) at those sites — same 1-ULP class as the
|
|
29
|
+
* five parallax slots, measured by the fixture recorders at extraction.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
'use strict';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @typedef {{ u: number, invD: number, invS: number, T: number,
|
|
36
|
+
* cp: number, Lsun: number,
|
|
37
|
+
* sinM: number, cosM: number, sin2M: number, cos2M: number }} ParallaxState
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Evaluate the fitted parallax basis for one coefficient table (RA or Dec —
|
|
42
|
+
* same basis, different tables). Returns DEGREES (the engines apply their
|
|
43
|
+
* own sign and rad conversion).
|
|
44
|
+
* @param {Record<string, number>} c @param {ParallaxState} s
|
|
45
|
+
* @returns {number} */
|
|
46
|
+
function evaluateParallaxBasis(c, s) {
|
|
47
|
+
const { u: _u, invD: _invD, invS: _invS, T: _T, Lsun: _Lsun } = s;
|
|
48
|
+
const _invD2 = _invD * _invD;
|
|
49
|
+
const _invD3 = _invD2 * _invD;
|
|
50
|
+
const _invS2 = _invS * _invS;
|
|
51
|
+
const _invDS = _invD * _invS;
|
|
52
|
+
const _sinU = Math.sin(_u), _cosU = Math.cos(_u);
|
|
53
|
+
const _sin2U = Math.sin(2 * _u), _cos2U = Math.cos(2 * _u);
|
|
54
|
+
const _sin3U = Math.sin(3 * _u), _cos3U = Math.cos(3 * _u);
|
|
55
|
+
const _sinCP = Math.sin(s.cp), _cosCP = Math.cos(s.cp);
|
|
56
|
+
const _sin2CP = Math.sin(2 * s.cp), _cos2CP = Math.cos(2 * s.cp);
|
|
57
|
+
const _sinL = Math.sin(_Lsun), _cosL = Math.cos(_Lsun);
|
|
58
|
+
const _sinM = s.sinM, _cosM = s.cosM, _sin2M = s.sin2M, _cos2M = s.cos2M;
|
|
59
|
+
return c.A + c.B * _invD + (c.C || 0) * _T
|
|
60
|
+
+ (c.D * _sinU + c.E * _cosU + c.F * _sin2U + c.G * _cos2U
|
|
61
|
+
+ (c.H || 0) * _sin3U + (c.I || 0) * _cos3U) * _invD
|
|
62
|
+
+ _T * ((c.J || 0) * _sinU + (c.K || 0) * _cosU) * _invD
|
|
63
|
+
+ (c.L || 0) * _invS + (c.M || 0) * _sinU * _invD2
|
|
64
|
+
+ (c.N || 0) * _sin2U * _invS + (c.O || 0) * _cosU * _invS
|
|
65
|
+
+ (c.P || 0) * _T * _sin2U * _invD + (c.Q || 0) * _T * _cos2U * _invD
|
|
66
|
+
+ (c.R || 0) * _T * _sinU * _invS
|
|
67
|
+
+ (c.S || 0) * _T * _invD + (c.U || 0) * _cosU * _invD2
|
|
68
|
+
+ (c.V || 0) * _invS2 + (c.W || 0) * _sinU * _invS2
|
|
69
|
+
+ (c.X || 0) * _cos3U * _invS + (c.Y || 0) * _sin3U * _invS
|
|
70
|
+
+ (c.Z || 0) * _invDS + (c.AA || 0) * _sinU * _invDS
|
|
71
|
+
+ (c.AB || 0) * _cos2U * _invDS + (c.AC || 0) * _T * _sin2U * _invS
|
|
72
|
+
+ (c.AD || 0) * _cos3U * _invD2 + (c.AE || 0) * _sin2U * _invS2
|
|
73
|
+
+ (c.AF || 0) * _sin3U * _invS2 + (c.AG || 0) * _cos3U * _invS2
|
|
74
|
+
+ (c.AH || 0) * _cosU * _invS2 + (c.AI || 0) * _sinU * _invD2 * _invS
|
|
75
|
+
+ (c.AJ || 0) * Math.cos(4 * _u) * _invS + (c.AK || 0) * _sin2U * _invD2 * _invS
|
|
76
|
+
+ (c.AL || 0) * Math.sin(4 * _u) * _invD + (c.AM || 0) * Math.cos(4 * _u) * _invD
|
|
77
|
+
+ (c.AN || 0) * _T * _sinU * _invD2 + (c.AO || 0) * _T * _cosU * _invD2
|
|
78
|
+
+ (c.AP || 0) * _sinU * _invD2 * _invD + (c.AQ || 0) * _cosU * _invD2 * _invD
|
|
79
|
+
+ (c.AR || 0) * _sinCP + (c.AS || 0) * _cosCP
|
|
80
|
+
+ (c.AT || 0) * _sin2CP + (c.AU_ || 0) * _cos2CP
|
|
81
|
+
+ (c.AV || 0) * _sinCP * _invD + (c.AW || 0) * _cosCP * _invD
|
|
82
|
+
+ (c.AX || 0) * _sinL * _invD + (c.AY || 0) * _cosL * _invD
|
|
83
|
+
+ (c.AZ || 0) * _sinL + (c.BA || 0) * _cosL
|
|
84
|
+
+ (c.BB || 0) * _T * _sinL * _invD + (c.BC || 0) * _T * _cosL * _invD
|
|
85
|
+
+ (c.BD || 0) * _T * _sinL + (c.BE || 0) * _T * _cosL
|
|
86
|
+
+ (c.BF || 0) * _cosU * _sinL * _invD2 + (c.BG || 0) * _cosU * _cosL * _invD2
|
|
87
|
+
+ (c.BH || 0) * _sinL * _invD3 + (c.BI || 0) * _cosL * _invD3
|
|
88
|
+
+ (c.BJ || 0) * Math.sin(_u - _Lsun) * _invD2 + (c.BK || 0) * Math.cos(_u - _Lsun) * _invD2
|
|
89
|
+
+ (c.BL || 0) * _T * _T * _invD + (c.BM || 0) * _T * _T * _sinU * _invD + (c.BN || 0) * _T * _T * _cosU * _invD
|
|
90
|
+
+ (c.BO || 0) * _sin2U * _invD3 + (c.BP || 0) * _cos2U * _invD3
|
|
91
|
+
+ (c.BQ || 0) * _sinU * _invD3 * _invD
|
|
92
|
+
+ (c.BR || 0) * _sinM * _invD + (c.BS || 0) * _cosM * _invD
|
|
93
|
+
+ (c.BT || 0) * _sin2M * _invD + (c.BU || 0) * _cos2M * _invD
|
|
94
|
+
+ (c.BV || 0) * _sinM + (c.BW || 0) * _cosM
|
|
95
|
+
+ (c.BX || 0) * _sin2M + (c.BY || 0) * _cos2M
|
|
96
|
+
+ (c.BZ || 0) * _sinM * _invD2 + (c.CA || 0) * _cosM * _invD2;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Gravitation-correction term deltas, in DEGREES, one entry per fitted
|
|
101
|
+
* term. The engines apply these PER TERM (sign convention and the
|
|
102
|
+
* degrees→radians conversion stay engine-side).
|
|
103
|
+
* @param {Array<{ period: number, raSin: number, raCos: number,
|
|
104
|
+
* decSin: number, decCos: number }>} terms
|
|
105
|
+
* @param {number} yearsFrom2000
|
|
106
|
+
* @returns {Array<{ raDeg: number, decDeg: number }>} */
|
|
107
|
+
function gravitationTermDeltasDeg(terms, yearsFrom2000) {
|
|
108
|
+
const out = [];
|
|
109
|
+
for (const term of terms) {
|
|
110
|
+
const phase = 2 * Math.PI * yearsFrom2000 / term.period;
|
|
111
|
+
const sp = Math.sin(phase), cp = Math.cos(phase);
|
|
112
|
+
out.push({
|
|
113
|
+
raDeg: term.raSin * sp + term.raCos * cp,
|
|
114
|
+
decDeg: term.decSin * sp + term.decCos * cp,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @typedef {{ elongRad: number, vFromWERad: number, synPhaseRad: number,
|
|
122
|
+
* invD: number }} ElongationState
|
|
123
|
+
*/
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Evaluate the fitted 21-slot elongation basis for one axis. Returns
|
|
127
|
+
* DEGREES; `suffix` selects the coefficient table ('ra' or 'dec' — same
|
|
128
|
+
* basis, different fitted slots). The engines derive the state (frame Sun
|
|
129
|
+
* RA, Earth-perihelion angle, exact synodic phase from the integer orbit
|
|
130
|
+
* count) and apply their own sign and rad conversion.
|
|
131
|
+
* @param {Record<string, number>} vc @param {ElongationState} s
|
|
132
|
+
* @param {'ra'|'dec'} suffix
|
|
133
|
+
* @returns {number} */
|
|
134
|
+
function evaluateElongationBasis(vc, s, suffix) {
|
|
135
|
+
const sinEl = Math.sin(s.elongRad), cosEl = Math.cos(s.elongRad);
|
|
136
|
+
const cosVwE = Math.cos(s.vFromWERad), sinVwE = Math.sin(s.vFromWERad);
|
|
137
|
+
const sin2VwE = Math.sin(2 * s.vFromWERad), cos2VwE = Math.cos(2 * s.vFromWERad);
|
|
138
|
+
const sin3VwE = Math.sin(3 * s.vFromWERad), cos3VwE = Math.cos(3 * s.vFromWERad);
|
|
139
|
+
const sin4VwE = Math.sin(4 * s.vFromWERad), cos4VwE = Math.cos(4 * s.vFromWERad);
|
|
140
|
+
const invD = s.invD;
|
|
141
|
+
const invD2 = invD * invD;
|
|
142
|
+
/** @param {string} k @returns {number} */
|
|
143
|
+
const c = (k) => vc[k + suffix] || 0;
|
|
144
|
+
return c('cosVwE_sinEl_') * cosVwE * sinEl
|
|
145
|
+
+ c('sinEl_d_') * sinEl * invD
|
|
146
|
+
+ c('sinVwE_sinEl_') * sinVwE * sinEl
|
|
147
|
+
+ c('sin2VwE_sinEl_') * sin2VwE * sinEl
|
|
148
|
+
+ c('cos2VwE_sinEl_') * cos2VwE * sinEl
|
|
149
|
+
+ c('cos4VwE_sinEl_') * cos4VwE * sinEl
|
|
150
|
+
+ c('sin4VwE_sinEl_') * sin4VwE * sinEl
|
|
151
|
+
+ c('sinVwE_sinEl_d2_') * sinVwE * sinEl * invD2
|
|
152
|
+
+ c('cos3VwE_sinEl_') * cos3VwE * sinEl
|
|
153
|
+
+ c('sin3VwE_sinEl_') * sin3VwE * sinEl
|
|
154
|
+
+ c('sin2syn_') * Math.sin(2 * s.synPhaseRad)
|
|
155
|
+
+ c('cos1syn_') * Math.cos(s.synPhaseRad)
|
|
156
|
+
+ c('sin3VwE_sinEl_d2_') * sin3VwE * sinEl * invD2
|
|
157
|
+
+ c('sin2VwE_sinEl_d2_') * sin2VwE * sinEl * invD2
|
|
158
|
+
+ c('cos2VwE_sinEl_d2_') * cos2VwE * sinEl * invD2
|
|
159
|
+
+ c('cosEl_d_') * cosEl * invD
|
|
160
|
+
+ c('cosVwE_cosEl_d_') * cosVwE * cosEl * invD
|
|
161
|
+
+ c('sinVwE_cosEl_d_') * sinVwE * cosEl * invD
|
|
162
|
+
+ c('cosEl_d2_') * cosEl * invD2
|
|
163
|
+
+ c('cosVwE_cosEl_d2_') * cosVwE * cosEl * invD2
|
|
164
|
+
+ c('sinVwE_cosEl_d2_') * sinVwE * cosEl * invD2;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
module.exports = { evaluateParallaxBasis, gravitationTermDeltasDeg, evaluateElongationBasis };
|