@essrt/physics 6.0.1 → 8.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.
@@ -27,13 +27,17 @@ export function createSunPlanetaryCompletion({ embWobbleArcsec, carrierRatesDegP
27
27
  * landings. */
28
28
  export const PAIRED_SUN_HARMONICS_SHA256: "cbc189cea1c20292";
29
29
  /** sha256/16 of JSON.stringify([...planets, moonElongation]) — the seven
30
- * full-precision carrier rates (deg/cy TT) the TERMS table was extracted
31
- * under at N3. The model wiring recomputes the rates live from the planet
32
- * records, so a planet-period / year / month input change moves the
33
- * carriers automatically while the table stays frozen — a silent few-
34
- * arcsec stale below the api gate's ≤8″ backstop. test:model recomputes
35
- * this fingerprint from live constants (identical arithmetic to model.js)
36
- * and fails on mismatch: re-run the N3 extraction chain
37
- * (tools/explore/n2-sun-framework-carriers.mjs →
38
- * n3-carrier-swap-preview.mjs), re-embed TERMS, and update this value. */
39
- export const PAIRED_CARRIER_RATES_SHA256: "893e055ee12343bd";
30
+ * full-precision carrier rates (deg/cy TT) the TERMS table pairs with:
31
+ * the SIDEREAL carriers of plan 06 I2 (record rate − the model's J2000
32
+ * precession; Earth the framework sidereal year; the N3 literals kept,
33
+ * the two long-period rows derived on these rates). The model wiring
34
+ * recomputes the rates live from the planet records, so a planet-period /
35
+ * year / month input change moves the carriers automatically while the
36
+ * table stays frozen — a silent few-arcsec stale below the api gate's
37
+ * ≤12″ backstop. test:model recomputes this fingerprint from live
38
+ * constants (identical arithmetic to model.js) and fails on mismatch:
39
+ * re-run the extraction chain (tools/explore/n2-sun-framework-carriers.mjs
40
+ * → i2-sidereal-carrier-table.mjs for the short-period rows,
41
+ * i2-long-inequality.mjs for the long-period rows), re-embed TERMS, and
42
+ * update this value. */
43
+ export const PAIRED_CARRIER_RATES_SHA256: "2d066e92bae955e4";
package/types/index.d.ts CHANGED
@@ -66,6 +66,7 @@ import { REFERENCE_DATA } from './constants/index.js';
66
66
  import { assembleModel } from './model.js';
67
67
  export { GENERATED as DEFAULT_CONSTANTS, CONSTANTS_HASH, MODEL_VERSION, PREPRINT_DOI, REFERENCE_DATA };
68
68
  export { FITTED_COEFFICIENTS, COEFFICIENTS_HASH } from "./constants/index.js";
69
+ export { computeEarthFrameOfDate, solveWheelAngleForLongitude } from "./earth/frame-of-date.cjs";
69
70
  export { eccentricityFromCycles, computeEccentricityIntegrated } from "./planets/ecc-channel.cjs";
70
71
  export { buildPlanetChainsFromArtifactData, computePlanetElementsAtYear, computeHeliocentricEclipticFromElements, computePoissonArgRad, computeOsculatingElements, solveKeplerRad, computeApsidalSecularDegPerYr, ANCHOR_EPOCH_YEAR, ANCHOR_EPOCH_JD } from "./planets/keplerian-chain.cjs";
71
72
  export { computeEquatorNodeOriginSFrameDeg, convertNodeSFrameToEquatorOriginDeg } from "./planets/inv-plane-frame.cjs";
@@ -73,6 +74,5 @@ export { CHAIN_ARTIFACT, CHAIN_ARTIFACT_HASH } from "./planets/chain-artifact.js
73
74
  export { DEEP_MODES_ARTIFACT, DEEP_MODES_ARTIFACT_HASH } from "./moon/deep-modes-artifact.cjs";
74
75
  export { createYearLengths, ONE_FAMILY_WINDOW_YEARS } from "./earth/year-lengths.cjs";
75
76
  export { computeSolarTorqueShare, createComposedPrecession } from "./earth/precession-composed.cjs";
76
- export { createPredictivePrecession, calcPlanetPerihelionLongDeg } from "./planets/predict.cjs";
77
77
  export { createMoonRecessionHistory, createSolarChannelBudget } from "./deltat/recession-history.cjs";
78
78
  export { evalClimateL1OrbitalPermil, laggedL1Terms, createAlphaGiaChannel } from "./climate/l1-orbital.cjs";
package/types/model.d.ts CHANGED
@@ -89,6 +89,7 @@ export function assembleModel(C: Readonly<Record<string, any>>, F: Readonly<Reco
89
89
  }>;
90
90
  cardinal: Readonly<{
91
91
  jd: (year: number, type: string) => number;
92
+ jdNearUT: (jdUT: number, type: string) => number;
92
93
  raDeg: (year: number, type: string) => number;
93
94
  yearLengthDays: (year: number, type: string) => number;
94
95
  }>;
@@ -135,6 +136,8 @@ export function assembleModel(C: Readonly<Record<string, any>>, F: Readonly<Reco
135
136
  eclipse: Readonly<{
136
137
  sunLonDegAtJD: (jd: number) => number;
137
138
  sunLonCompletedDegAtJD: (jd: number) => number;
139
+ /** The APPARENT Sun (completed geometric − the derived aberration constant + the leading nutation terms on the model's own arguments) — the quantity Horizons' observer ecliptic longitude is; the cardinal instants are its crossings (plan 06 R1/I1). @param {number} jdUT @returns {number} */
140
+ sunApparentLonDegAtJD: (jdUT: number) => number;
138
141
  findLunarInRange: (jdStart: number, jdEnd: number) => {
139
142
  jd: number;
140
143
  beta: number;
@@ -156,7 +159,7 @@ export function assembleModel(C: Readonly<Record<string, any>>, F: Readonly<Reco
156
159
  frameworkSunDeps: Readonly<{
157
160
  sunMeanLongitudeJ2000Deg: any;
158
161
  tropicalRateDegPerCy: number;
159
- eccentricityAt: (year: number) => number;
162
+ eccentricityAt: (year: number) => any;
160
163
  perihelionLongitudeDegAt: (year: number) => number;
161
164
  meanLongitudeDegAt: (year: number) => number;
162
165
  }>;
@@ -1,75 +1,14 @@
1
- export type MoonCorrectionTable = {
2
- raSinD: number;
3
- raCosD: number;
4
- raSinMp: number;
5
- raCosMp: number;
6
- raSinMs: number;
7
- raCosMs: number;
8
- decSinD: number;
9
- decCosD: number;
10
- decSinMp: number;
11
- decCosMp: number;
12
- decSinMs: number;
13
- decCosMs: number;
14
- };
15
- /**
16
- * @typedef {{raSinD: number, raCosD: number, raSinMp: number, raCosMp: number,
17
- * raSinMs: number, raCosMs: number, decSinD: number, decCosD: number,
18
- * decSinMp: number, decCosMp: number, decSinMs: number, decCosMs: number}} MoonCorrectionTable
19
- */
20
1
  /**
21
- * @param {{
22
- * constants: {
23
- * j2000JD: number,
24
- * julianCenturyDays: number,
25
- * sunMeanLongitudeJ2000Deg: number,
26
- * perihelionLongitudeJ2000Deg: number,
27
- * eccentricityJ2000: number,
28
- * eccentricityDotJ2000: number,
29
- * d5RateLDegPerDay: number,
30
- * d5RatePeriDegPerDay: number,
31
- * speedOfLight: number,
32
- * },
33
- * fns: {
34
- * computeObliquityEarth: (year: number) => number,
35
- * getAuDistanceKm: () => number,
36
- * isFrameworkNative: () => boolean,
37
- * getCorrectionResidual: () => (MoonCorrectionTable | null),
38
- * getCorrectionLegacy: () => (MoonCorrectionTable | null),
39
- * },
40
- * }} deps — the D5 rates are the engines' J2000-frozen values (the year
41
- * globals are deep-time-mutable, same pattern as FW_A2_RATE); the AU
42
- * distance is a GETTER (browser-mutable under deep time).
2
+ * No dependencies since R3 item 1 (the former D5 optics constants and the
3
+ * fitted-patch getters are gone); the factory shape is kept for the two
4
+ * scene twins' call sites.
5
+ * @param {object} [_deps] ignored
43
6
  */
44
- export function createMoonApparent({ constants, fns }: {
45
- constants: {
46
- j2000JD: number;
47
- julianCenturyDays: number;
48
- sunMeanLongitudeJ2000Deg: number;
49
- perihelionLongitudeJ2000Deg: number;
50
- eccentricityJ2000: number;
51
- eccentricityDotJ2000: number;
52
- d5RateLDegPerDay: number;
53
- d5RatePeriDegPerDay: number;
54
- speedOfLight: number;
55
- };
56
- fns: {
57
- computeObliquityEarth: (year: number) => number;
58
- getAuDistanceKm: () => number;
59
- isFrameworkNative: () => boolean;
60
- getCorrectionResidual: () => (MoonCorrectionTable | null);
61
- getCorrectionLegacy: () => (MoonCorrectionTable | null);
62
- };
63
- }): {
64
- sunGeoVecEqD5: (jd: number) => [number, number, number];
65
- moonAberrationRaDec: (jd: number, ra: number, dec: number) => {
66
- dRA: number;
67
- dDec: number;
68
- };
69
- overrideRaDec: ({ lonDeg, betRad, meeusT, obliquityDeg }: {
7
+ export function createMoonApparent(_deps?: object): {
8
+ overrideRaDec: ({ lonDeg, betRad, obliquityDeg }: {
70
9
  lonDeg: number;
71
10
  betRad: number;
72
- meeusT: (number | undefined);
11
+ meeusT?: (number | undefined);
73
12
  obliquityDeg: number;
74
13
  }) => {
75
14
  raRad: number;
@@ -1,324 +0,0 @@
1
- /**
2
- * Predictive geocentric precession — the physical-beat feature basis
3
- * (Phase 8.3, L9). Ported originally from
4
- * tools/lib/python/predictive_formula_physical.py; this module is now THE
5
- * shared implementation behind what were two hand-mirrors (browser +
6
- * Node engine, ~220 duplicated lines each).
7
- *
8
- * GROUPS: A (Earth ref), B (planet angle), C+D+E (periods+beats × 7
9
- * harmonics), F (period×angle), I (period×2δ), J (fund×Earth sidebands),
10
- * L (high-harmonic fund×Earth ecl), K (ecl±icrf carriers × Earth ecl).
11
- * Groups G+H (ERD×period) were removed after a 0.00%-impact diagnostic.
12
- *
13
- * MATCHED PAIR: PREDICT_COEFFS was trained against exactly this basis in
14
- * exactly this push order — reordering or "simplifying" any group scrambles
15
- * the ridge-regression dot product silently. Do not touch.
16
- *
17
- * The engines OWN the Earth scalar derivations (θ_E, ERD, obliquity,
18
- * eccentricity) — injected, because the two engines deliberately differ
19
- * there: the browser evaluates the deep-time epoch-aware forms
20
- * (H_at_year, phaseAdvanceRadians), the Node engine the J2000-anchored
21
- * forms. The predictor dot-products (PREDICT_PLANETS / PREDICT_COEFFS
22
- * lookup and their guard semantics) also stay engine-side.
23
- *
24
- * Planet fields and H / balancedYear arrive as GETTERS: the browser
25
- * mutates all of them on setEpoch and invalidates the feature-template
26
- * cache via resetTemplateCache() at that site.
27
- */
28
-
29
- 'use strict';
30
-
31
- const PHYSICAL_PERIOD_KEYS = ['ecl', 'icrf', 'obliq', 'asc', 'axial', 'wobble'];
32
- const PHYSICAL_HARMONIC_ORDERS = [1, 2, 4, 6, 8, 12, 16];
33
- const SIDEBAND_J_HARMONICS = [1, 2, 3, 4];
34
- const CARRIER_HARMONICS_K = [2, 4, 6, 8, 10, 12];
35
- const SIDEBAND_K_HARMONICS = [1, 2, 3];
36
- const CARRIER_HARMONICS_L = [6, 10, 12, 16];
37
- const SIDEBAND_L_HARMONICS = [1, 2, 3];
38
- const MAX_BEAT_YEARS = 1e12;
39
-
40
- /**
41
- * Any planet's perihelion longitude — simple linear precession from J2000.
42
- *
43
- * @param {number} theta0Deg - perihelion longitude at J2000 (degrees)
44
- * @param {number} periodYears - precession period in years
45
- * @param {number} year - decimal year
46
- * @returns {number} longitude in degrees [0, 360)
47
- */
48
- function calcPlanetPerihelionLongDeg(theta0Deg, periodYears, year) {
49
- return ((theta0Deg + 360.0 * (year - 2000) / periodYears) % 360 + 360) % 360;
50
- }
51
-
52
- /**
53
- * Sum/difference beat periods of two cycles, null where degenerate or
54
- * beyond MAX_BEAT_YEARS.
55
- *
56
- * @param {number|null} t1 @param {number|null} t2
57
- * @returns {{ sum: number|null, diff: number|null }}
58
- */
59
- function beatPair(t1, t2) {
60
- if (t1 == null || t2 == null) return { sum: null, diff: null };
61
- const sr = 1 / t1 + 1 / t2;
62
- const dr = 1 / t1 - 1 / t2;
63
- let sp = Math.abs(sr) > 1e-20 ? 1 / sr : null;
64
- let dp = Math.abs(dr) > 1e-20 ? 1 / dr : null;
65
- if (sp != null && Math.abs(sp) > MAX_BEAT_YEARS) sp = null;
66
- if (dp != null && Math.abs(dp) > MAX_BEAT_YEARS) dp = null;
67
- return { sum: sp, diff: dp };
68
- }
69
-
70
- /**
71
- * @typedef {Object} PredictPlanetFields
72
- * @property {number} perihelionEclipticYears
73
- * @property {number} longitudePerihelion
74
- * @property {number|null|undefined} ascendingNodeCyclesIn8H
75
- * @property {number} axialPrecessionYears
76
- * @property {number|null|undefined} obliquityCycle
77
- * @property {number|null|undefined} wobblePeriod
78
- */
79
-
80
- /**
81
- * @typedef {Object} PredictDeps
82
- * @property {() => number} getHYears - holistic year length (live: setEpoch mutates it)
83
- * @property {() => number} getBalancedYear
84
- * @property {(planetKey: string) => PredictPlanetFields} getPlanetFields - live per-call reads
85
- * @property {(year: number) => number} calcEarthPerihelionDeg - θ_E, engine-owned form
86
- * @property {(year: number) => number} calcErdRate - ERD deg/yr, engine-owned form
87
- * @property {(year: number) => number} computeObliquityEarthDeg
88
- * @property {(year: number) => number} computeEccentricityEarth
89
- * @property {number} obliquityMeanDeg - the fitted solstice-obliquity mean
90
- * @property {number} eccentricityMean - Earth's mean eccentricity (the one law's base′)
91
- */
92
-
93
- /**
94
- * Build the predictive-precession machinery over one engine's state.
95
- * @param {PredictDeps} deps
96
- */
97
- function createPredictivePrecession(deps) {
98
- /** @type {Record<string, number[]>} */
99
- const templates = {};
100
-
101
- /**
102
- * The six fundamental periods (years) feeding the feature basis.
103
- * Earth is pure H-lattice; planets derive icrf/asc from their fields.
104
- * @param {string} planetName
105
- * @returns {Record<string, number|null>}
106
- */
107
- function getPlanetFundamentalPeriodsYears(planetName) {
108
- const H = deps.getHYears();
109
- const H13 = H / 13;
110
- if (planetName === 'earth') {
111
- return { ecl: H / 16, icrf: H / 3, obliq: H / 8, asc: -8 * H / 40, axial: -H / 13, wobble: H / 16 };
112
- }
113
- const p = deps.getPlanetFields(planetName);
114
- const tEcl = p.perihelionEclipticYears;
115
- const tIcrf = (tEcl * H13) / (H13 - tEcl);
116
- const ascN = p.ascendingNodeCyclesIn8H;
117
- const tAsc = ascN ? -8 * H / ascN : null;
118
- return {
119
- ecl: tEcl,
120
- icrf: tIcrf,
121
- obliq: p.obliquityCycle || null,
122
- asc: tAsc,
123
- axial: p.axialPrecessionYears,
124
- wobble: p.wobblePeriod || null,
125
- };
126
- }
127
-
128
- /**
129
- * Fundamentals + pairwise internal beats + planet×Earth beats, cached
130
- * per planet until resetTemplateCache().
131
- * @param {string} planetName
132
- * @returns {number[]}
133
- */
134
- function getFeatureTemplate(planetName) {
135
- if (templates[planetName]) return templates[planetName];
136
- const pp = getPlanetFundamentalPeriodsYears(planetName);
137
- const ep = getPlanetFundamentalPeriodsYears('earth');
138
- const template = [];
139
- for (const k of PHYSICAL_PERIOD_KEYS) {
140
- if (pp[k] != null) template.push(pp[k]);
141
- }
142
- for (let i = 0; i < PHYSICAL_PERIOD_KEYS.length; i++) {
143
- for (let j = i + 1; j < PHYSICAL_PERIOD_KEYS.length; j++) {
144
- const b = beatPair(pp[PHYSICAL_PERIOD_KEYS[i]], pp[PHYSICAL_PERIOD_KEYS[j]]);
145
- if (b.sum != null) template.push(b.sum);
146
- if (b.diff != null) template.push(b.diff);
147
- }
148
- }
149
- for (const pk of PHYSICAL_PERIOD_KEYS) {
150
- for (const ek of PHYSICAL_PERIOD_KEYS) {
151
- const b = beatPair(pp[pk], ep[ek]);
152
- if (b.sum != null) template.push(b.sum);
153
- if (b.diff != null) template.push(b.diff);
154
- }
155
- }
156
- templates[planetName] = template;
157
- return template;
158
- }
159
-
160
- /**
161
- * The full feature vector (~2421 elements for Venus). Push order is the
162
- * training order — see the module header.
163
- * @param {number} year - calendar year
164
- * @param {string} planetKey - e.g. 'venus'
165
- * @returns {number[]}
166
- */
167
- function buildPredictiveFeatures(year, planetKey) {
168
- const t = year - deps.getBalancedYear();
169
- const p = deps.getPlanetFields(planetKey);
170
- const planetPeriod = Math.abs(p.perihelionEclipticYears);
171
- const planetTheta0 = p.longitudePerihelion;
172
-
173
- const thetaE = deps.calcEarthPerihelionDeg(year);
174
- const thetaP = calcPlanetPerihelionLongDeg(planetTheta0, planetPeriod, year);
175
- const erd = deps.calcErdRate(year);
176
- const obliq = deps.computeObliquityEarthDeg(year);
177
- const ecc = deps.computeEccentricityEarth(year);
178
-
179
- const thetaERad = thetaE * Math.PI / 180;
180
- const thetaPRad = thetaP * Math.PI / 180;
181
- const diff = thetaERad - thetaPRad;
182
- const sumAngle = thetaERad + thetaPRad;
183
-
184
- const obliqNorm = obliq - deps.obliquityMeanDeg;
185
- const eccNorm = ecc - deps.eccentricityMean;
186
- const erd2 = erd * erd;
187
- const erd3 = erd2 * erd;
188
-
189
- const f = [];
190
-
191
- // GROUP A: Earth reference (49)
192
- for (const n of [1, 2, 3, 4]) { f.push(Math.cos(n * diff), Math.sin(n * diff)); }
193
- f.push(Math.cos(sumAngle), Math.sin(sumAngle));
194
- for (const n of [1, 2]) { f.push(Math.cos(n * thetaERad), Math.sin(n * thetaERad)); }
195
- for (const n of [1, 2]) { f.push(Math.cos(n * thetaPRad), Math.sin(n * thetaPRad)); }
196
- f.push(obliqNorm, eccNorm);
197
- f.push(obliqNorm * Math.cos(diff), obliqNorm * Math.sin(diff));
198
- f.push(eccNorm * Math.cos(diff), eccNorm * Math.sin(diff));
199
- f.push(erd, erd2, erd3);
200
- f.push(erd * Math.cos(diff), erd * Math.sin(diff));
201
- f.push(erd * Math.cos(2 * diff), erd * Math.sin(2 * diff));
202
- f.push(erd * Math.cos(sumAngle), erd * Math.sin(sumAngle));
203
- f.push(erd * obliqNorm, erd * eccNorm);
204
- f.push(erd2 * Math.cos(diff), erd2 * Math.sin(diff));
205
- f.push(erd2 * Math.cos(2 * diff));
206
- f.push(erd * Math.cos(3 * diff), erd * Math.sin(3 * diff));
207
- f.push(Math.cos(3 * diff), Math.sin(3 * diff));
208
- f.push(Math.cos(4 * diff), Math.sin(4 * diff));
209
- f.push(erd * Math.cos(2 * sumAngle), erd * Math.sin(2 * sumAngle));
210
- f.push(erd2 * Math.cos(sumAngle), erd2 * Math.sin(sumAngle));
211
- f.push(1.0);
212
-
213
- // GROUP B: planet angle cross-terms (10)
214
- for (const n of [3, 4]) { f.push(Math.cos(n * thetaPRad), Math.sin(n * thetaPRad)); }
215
- f.push(obliqNorm * Math.cos(2 * diff), obliqNorm * Math.sin(2 * diff));
216
- f.push(eccNorm * Math.cos(2 * diff), eccNorm * Math.sin(2 * diff));
217
- f.push(Math.cos(thetaERad) * Math.cos(thetaPRad));
218
- f.push(Math.cos(thetaERad) * Math.sin(thetaPRad));
219
-
220
- // C+D+E: physical periods + beats × 7 harmonics
221
- const template = getFeatureTemplate(planetKey);
222
- for (let i = 0; i < template.length; i++) {
223
- const basePhase = 2 * Math.PI * t / template[i];
224
- for (const n of PHYSICAL_HARMONIC_ORDERS) {
225
- f.push(Math.cos(n * basePhase), Math.sin(n * basePhase));
226
- }
227
- }
228
-
229
- const pp = getPlanetFundamentalPeriodsYears(planetKey);
230
-
231
- // GROUP F: period × angle
232
- for (const k of PHYSICAL_PERIOD_KEYS) {
233
- const period = pp[k];
234
- if (period == null) continue;
235
- const phase = 2 * Math.PI * t / period;
236
- f.push(Math.cos(phase) * Math.cos(diff));
237
- f.push(Math.sin(phase) * Math.sin(diff));
238
- f.push(Math.cos(phase) * Math.cos(2 * diff));
239
- f.push(Math.sin(phase) * Math.sin(2 * diff));
240
- }
241
-
242
- // GROUP I: period × 2δ
243
- for (const k of PHYSICAL_PERIOD_KEYS) {
244
- const period = pp[k];
245
- if (period == null) continue;
246
- const phase = 2 * Math.PI * t / period;
247
- const sinP = Math.sin(phase), cosP = Math.cos(phase);
248
- f.push(sinP * Math.cos(2 * diff), sinP * Math.sin(2 * diff));
249
- f.push(cosP * Math.cos(2 * diff), cosP * Math.sin(2 * diff));
250
- }
251
-
252
- // GROUP J: fund × Earth cycles (4 harmonics)
253
- // Earth's fundamentals are pure H-lattice — never null; the Record type
254
- // can't express that, hence the casts here and below.
255
- const ep = getPlanetFundamentalPeriodsYears('earth');
256
- const earthCyclesJ = /** @type {number[]} */ ([ep.ecl, ep.obliq, ep.icrf, ep.axial]);
257
- for (const k of PHYSICAL_PERIOD_KEYS) {
258
- const period = pp[k];
259
- if (period == null) continue;
260
- const phaseP = 2 * Math.PI * t / period;
261
- const sinP = Math.sin(phaseP), cosP = Math.cos(phaseP);
262
- for (const TE of earthCyclesJ) {
263
- for (const harm of SIDEBAND_J_HARMONICS) {
264
- const phaseE = harm * 2 * Math.PI * t / TE;
265
- const sinE = Math.sin(phaseE), cosE = Math.cos(phaseE);
266
- f.push(sinP * sinE, sinP * cosE, cosP * sinE, cosP * cosE);
267
- }
268
- }
269
- }
270
-
271
- // GROUP L: high-harmonic fundamental × Earth ecl
272
- const phiEEcl = 2 * Math.PI * t / /** @type {number} */ (ep.ecl);
273
- for (const k of PHYSICAL_PERIOD_KEYS) {
274
- const period = pp[k];
275
- if (period == null) continue;
276
- const phiFund = 2 * Math.PI * t / period;
277
- for (const nL of CARRIER_HARMONICS_L) {
278
- const ccL = Math.cos(nL * phiFund), scL = Math.sin(nL * phiFund);
279
- for (const kh of SIDEBAND_L_HARMONICS) {
280
- const modPhase = kh * phiEEcl;
281
- const smL = Math.sin(modPhase), cmL = Math.cos(modPhase);
282
- f.push(scL * smL, scL * cmL, ccL * smL, ccL * cmL);
283
- }
284
- }
285
- }
286
-
287
- // GROUP K: ICRF internal-beat carriers × Earth ecl sidebands
288
- if (pp.ecl != null && pp.icrf != null) {
289
- const phiEcl = 2 * Math.PI * t / pp.ecl;
290
- const phiIcrf = 2 * Math.PI * t / pp.icrf;
291
- for (const sign of [+1, -1]) {
292
- for (const n of CARRIER_HARMONICS_K) {
293
- const carrierPhase = n * (phiEcl + sign * phiIcrf);
294
- const sc = Math.sin(carrierPhase), cc = Math.cos(carrierPhase);
295
- for (const kh of SIDEBAND_K_HARMONICS) {
296
- const modPhase = kh * phiEEcl;
297
- const sm = Math.sin(modPhase), cm = Math.cos(modPhase);
298
- f.push(sc * sm, sc * cm, cc * sm, cc * cm);
299
- }
300
- }
301
- }
302
- }
303
-
304
- return f;
305
- }
306
-
307
- /**
308
- * Invalidate cached feature templates after the underlying planet fields
309
- * mutate (the browser's setEpoch rebuilds obliquity cycles, wobble
310
- * periods, and the perihelion periods the templates froze at first touch).
311
- */
312
- function resetTemplateCache() {
313
- for (const k in templates) delete templates[k];
314
- }
315
-
316
- return {
317
- buildPredictiveFeatures,
318
- getPlanetFundamentalPeriodsYears,
319
- getFeatureTemplate,
320
- resetTemplateCache,
321
- };
322
- }
323
-
324
- module.exports = { createPredictivePrecession, calcPlanetPerihelionLongDeg, beatPair };
@@ -1,88 +0,0 @@
1
- export type PredictPlanetFields = {
2
- perihelionEclipticYears: number;
3
- longitudePerihelion: number;
4
- ascendingNodeCyclesIn8H: number | null | undefined;
5
- axialPrecessionYears: number;
6
- obliquityCycle: number | null | undefined;
7
- wobblePeriod: number | null | undefined;
8
- };
9
- export type PredictDeps = {
10
- /**
11
- * - holistic year length (live: setEpoch mutates it)
12
- */
13
- getHYears: () => number;
14
- getBalancedYear: () => number;
15
- /**
16
- * - live per-call reads
17
- */
18
- getPlanetFields: (planetKey: string) => PredictPlanetFields;
19
- /**
20
- * - θ_E, engine-owned form
21
- */
22
- calcEarthPerihelionDeg: (year: number) => number;
23
- /**
24
- * - ERD deg/yr, engine-owned form
25
- */
26
- calcErdRate: (year: number) => number;
27
- computeObliquityEarthDeg: (year: number) => number;
28
- computeEccentricityEarth: (year: number) => number;
29
- /**
30
- * - the fitted solstice-obliquity mean
31
- */
32
- obliquityMeanDeg: number;
33
- /**
34
- * - Earth's mean eccentricity (the one law's base′)
35
- */
36
- eccentricityMean: number;
37
- };
38
- /**
39
- * @typedef {Object} PredictPlanetFields
40
- * @property {number} perihelionEclipticYears
41
- * @property {number} longitudePerihelion
42
- * @property {number|null|undefined} ascendingNodeCyclesIn8H
43
- * @property {number} axialPrecessionYears
44
- * @property {number|null|undefined} obliquityCycle
45
- * @property {number|null|undefined} wobblePeriod
46
- */
47
- /**
48
- * @typedef {Object} PredictDeps
49
- * @property {() => number} getHYears - holistic year length (live: setEpoch mutates it)
50
- * @property {() => number} getBalancedYear
51
- * @property {(planetKey: string) => PredictPlanetFields} getPlanetFields - live per-call reads
52
- * @property {(year: number) => number} calcEarthPerihelionDeg - θ_E, engine-owned form
53
- * @property {(year: number) => number} calcErdRate - ERD deg/yr, engine-owned form
54
- * @property {(year: number) => number} computeObliquityEarthDeg
55
- * @property {(year: number) => number} computeEccentricityEarth
56
- * @property {number} obliquityMeanDeg - the fitted solstice-obliquity mean
57
- * @property {number} eccentricityMean - Earth's mean eccentricity (the one law's base′)
58
- */
59
- /**
60
- * Build the predictive-precession machinery over one engine's state.
61
- * @param {PredictDeps} deps
62
- */
63
- export function createPredictivePrecession(deps: PredictDeps): {
64
- buildPredictiveFeatures: (year: number, planetKey: string) => number[];
65
- getPlanetFundamentalPeriodsYears: (planetName: string) => Record<string, number | null>;
66
- getFeatureTemplate: (planetName: string) => number[];
67
- resetTemplateCache: () => void;
68
- };
69
- /**
70
- * Any planet's perihelion longitude — simple linear precession from J2000.
71
- *
72
- * @param {number} theta0Deg - perihelion longitude at J2000 (degrees)
73
- * @param {number} periodYears - precession period in years
74
- * @param {number} year - decimal year
75
- * @returns {number} longitude in degrees [0, 360)
76
- */
77
- export function calcPlanetPerihelionLongDeg(theta0Deg: number, periodYears: number, year: number): number;
78
- /**
79
- * Sum/difference beat periods of two cycles, null where degenerate or
80
- * beyond MAX_BEAT_YEARS.
81
- *
82
- * @param {number|null} t1 @param {number|null} t2
83
- * @returns {{ sum: number|null, diff: number|null }}
84
- */
85
- export function beatPair(t1: number | null, t2: number | null): {
86
- sum: number | null;
87
- diff: number | null;
88
- };