@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,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Planet orientation channel — THE shared implementation (Phase 8.3, L4):
|
|
3
|
+
* invariable-plane inclination, ascending nodes, and the scene ecliptic
|
|
4
|
+
* inclination (the balanced-year dot-product form canonicalized at S-P4).
|
|
5
|
+
*
|
|
6
|
+
* Time is EXPLICIT here (yearsSinceBalanced / calendar year). A finding
|
|
7
|
+
* from extraction, preserved not fixed: the browser's
|
|
8
|
+
* computePlanetInvPlaneInclinationDynamic(planet, currentYear) IGNORED its
|
|
9
|
+
* currentYear parameter and read the live scene JD — the 8.3-0 fixtures
|
|
10
|
+
* recorded identical values at every probed epoch. The browser wrapper
|
|
11
|
+
* keeps that scene-coupling (visibly, at the wrapper); the Node engine
|
|
12
|
+
* honors the year as before. Unifying them is a measured decision for the
|
|
13
|
+
* factory layer, not a silent side effect.
|
|
14
|
+
*
|
|
15
|
+
* Two ascending-node conventions exist and BOTH are quantities (the S-P4
|
|
16
|
+
* lesson): the year-2000-anchored linear node here
|
|
17
|
+
* (ascendingNodeInvPlaneLinearAt — the node-integrator/dashboard mirror)
|
|
18
|
+
* and the balanced-year −8H/N node inside the scene tilt
|
|
19
|
+
* (eclipticInclinationFromBalanced). Do not merge them.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Invariable-plane inclination at explicit time (browser form): the ICRF
|
|
26
|
+
* perihelion-linked oscillation i(t) = mean + s·A·cos(ϖ_ICRF(t) − anchor).
|
|
27
|
+
* @param {{ key?: string, isEarth?: boolean,
|
|
28
|
+
* invPlaneInclinationJ2000: number, invPlaneInclinationMean: number,
|
|
29
|
+
* invPlaneInclinationAmplitude: number, inclinationCycleAnchor: number,
|
|
30
|
+
* longitudePerihelion: number, perihelionEclipticYears: number,
|
|
31
|
+
* antiPhase?: boolean }} body
|
|
32
|
+
* @param {number} yearsSinceBalanced
|
|
33
|
+
* @param {{ H: number, yearsFromBalancedToJ2000: number }} env
|
|
34
|
+
* @returns {number} degrees */
|
|
35
|
+
function invPlaneInclinationAt(body, yearsSinceBalanced, env) {
|
|
36
|
+
const iJ2000 = body.invPlaneInclinationJ2000;
|
|
37
|
+
const amplitude = body.invPlaneInclinationAmplitude;
|
|
38
|
+
if (iJ2000 === undefined || amplitude === undefined) return iJ2000 || 0;
|
|
39
|
+
if (amplitude === 0) return iJ2000;
|
|
40
|
+
|
|
41
|
+
// ICRF period: Earth = H/3 directly; others = 1/(1/eclP − 1/(H/13)).
|
|
42
|
+
const genPrecRate = 1 / (env.H / 13);
|
|
43
|
+
const icrfPeriod = body.isEarth
|
|
44
|
+
? env.H / 3
|
|
45
|
+
: 1 / (1 / body.perihelionEclipticYears - genPrecRate);
|
|
46
|
+
const icrfRate = 360 / icrfPeriod;
|
|
47
|
+
|
|
48
|
+
const periAtBalanced = body.longitudePerihelion - icrfRate * env.yearsFromBalancedToJ2000;
|
|
49
|
+
const periCurrent = periAtBalanced + icrfRate * yearsSinceBalanced;
|
|
50
|
+
const currentPhaseRad = (periCurrent - body.inclinationCycleAnchor) * Math.PI / 180;
|
|
51
|
+
const antiPhaseSign = body.antiPhase ? -1 : 1;
|
|
52
|
+
return body.invPlaneInclinationMean + antiPhaseSign * amplitude * Math.cos(currentPhaseRad);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Ascending node on the invariable plane — the LINEAR year-2000-anchored
|
|
57
|
+
* convention (node-integrator/dashboard; see the header).
|
|
58
|
+
* @param {{ ascendingNodeInvPlane?: number, ascendingNodePeriod?: number,
|
|
59
|
+
* perihelionEclipticYears: number }} body
|
|
60
|
+
* @param {number} year @returns {number} degrees 0–360 */
|
|
61
|
+
function ascendingNodeInvPlaneLinearAt(body, year) {
|
|
62
|
+
if (!body.ascendingNodeInvPlane) return 0;
|
|
63
|
+
const period = body.ascendingNodePeriod || body.perihelionEclipticYears;
|
|
64
|
+
const rate = 360 / period;
|
|
65
|
+
return ((body.ascendingNodeInvPlane + rate * (year - 2000)) % 360 + 360) % 360;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Scene ecliptic inclination — normal-vector dot product, balanced-year
|
|
70
|
+
* anchoring, planet Ω on the −8H/N assignment (canonicalized at S-P4; the
|
|
71
|
+
* mirror of src/script.js updateDynamicInclinations).
|
|
72
|
+
* @param {{ perihelionEclipticYears: number, longitudePerihelion: number,
|
|
73
|
+
* inclinationCycleAnchor: number, antiPhase?: boolean,
|
|
74
|
+
* invPlaneInclinationMean: number, invPlaneInclinationAmplitude: number,
|
|
75
|
+
* ascendingNodeInvPlane: number, ascendingNodeCyclesIn8H?: number }} body
|
|
76
|
+
* @param {{ invPlanePrecessionYears: number, inclinationMean: number,
|
|
77
|
+
* inclinationAmplitude: number, ascendingNodeInvPlane: number }} earth
|
|
78
|
+
* @param {number} yearsSinceBalanced
|
|
79
|
+
* @param {{ H: number, yearsFromBalancedToJ2000: number }} env
|
|
80
|
+
* @returns {number} degrees */
|
|
81
|
+
function eclipticInclinationFromBalanced(body, earth, yearsSinceBalanced, env) {
|
|
82
|
+
const d2r = Math.PI / 180;
|
|
83
|
+
const genPrecRate = 1 / (env.H / 13);
|
|
84
|
+
|
|
85
|
+
// --- Earth's orbital plane ---
|
|
86
|
+
const earthPhaseRad = (yearsSinceBalanced / earth.invPlanePrecessionYears) * 2 * Math.PI;
|
|
87
|
+
const earthI = (earth.inclinationMean
|
|
88
|
+
- earth.inclinationAmplitude * Math.cos(earthPhaseRad)) * d2r;
|
|
89
|
+
|
|
90
|
+
// Earth Ω regresses at -H/5 (ecliptic precession rate), NOT at H/3.
|
|
91
|
+
const earthAscNodePeriod = -env.H / 5;
|
|
92
|
+
const earthOmegaRate = 360 / earthAscNodePeriod;
|
|
93
|
+
const earthOmega = (earth.ascendingNodeInvPlane
|
|
94
|
+
- earthOmegaRate * env.yearsFromBalancedToJ2000
|
|
95
|
+
+ earthOmegaRate * yearsSinceBalanced) * d2r;
|
|
96
|
+
|
|
97
|
+
// --- Planet's orbital plane ---
|
|
98
|
+
const eclRate = 1 / body.perihelionEclipticYears;
|
|
99
|
+
const icrfRate = (eclRate - genPrecRate) * 360; // deg/yr
|
|
100
|
+
const periICRFDeg = body.longitudePerihelion
|
|
101
|
+
- icrfRate * env.yearsFromBalancedToJ2000
|
|
102
|
+
+ icrfRate * yearsSinceBalanced;
|
|
103
|
+
|
|
104
|
+
const planetPhaseDeg = periICRFDeg - body.inclinationCycleAnchor;
|
|
105
|
+
const antiPhaseSign = body.antiPhase ? -1 : 1;
|
|
106
|
+
const planetI = (body.invPlaneInclinationMean
|
|
107
|
+
+ antiPhaseSign * body.invPlaneInclinationAmplitude * Math.cos(planetPhaseDeg * d2r)) * d2r;
|
|
108
|
+
|
|
109
|
+
// Planet Ω advances at the asc-node period (-8H/N), NOT the ecliptic
|
|
110
|
+
// perihelion period — they are different angles.
|
|
111
|
+
const planetAscNodePeriod = body.ascendingNodeCyclesIn8H
|
|
112
|
+
? -(8 * env.H) / body.ascendingNodeCyclesIn8H
|
|
113
|
+
: body.perihelionEclipticYears;
|
|
114
|
+
const planetOmegaRate = 360 / planetAscNodePeriod;
|
|
115
|
+
const planetOmegaDeg = body.ascendingNodeInvPlane
|
|
116
|
+
- planetOmegaRate * env.yearsFromBalancedToJ2000
|
|
117
|
+
+ planetOmegaRate * yearsSinceBalanced;
|
|
118
|
+
const planetOmega = planetOmegaDeg * d2r;
|
|
119
|
+
|
|
120
|
+
// --- Dot product of normal vectors → angle between orbital planes ---
|
|
121
|
+
const eNx = Math.sin(earthI) * Math.sin(earthOmega);
|
|
122
|
+
const eNy = Math.sin(earthI) * Math.cos(earthOmega);
|
|
123
|
+
const eNz = Math.cos(earthI);
|
|
124
|
+
|
|
125
|
+
const pNx = Math.sin(planetI) * Math.sin(planetOmega);
|
|
126
|
+
const pNy = Math.sin(planetI) * Math.cos(planetOmega);
|
|
127
|
+
const pNz = Math.cos(planetI);
|
|
128
|
+
|
|
129
|
+
const cosAngle = eNx * pNx + eNy * pNy + eNz * pNz;
|
|
130
|
+
return Math.acos(Math.max(-1, Math.min(1, cosAngle))) * (180 / Math.PI);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = { invPlaneInclinationAt, ascendingNodeInvPlaneLinearAt, eclipticInclinationFromBalanced };
|
|
@@ -0,0 +1,324 @@
|
|
|
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 - sqrt(base² + amplitude²)
|
|
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 };
|