@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.
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Planet eccentricity channel — THE shared implementation (Phase 8.3, L3).
3
+ * The law-of-cosines oscillation every body's runtime eccentricity rides
4
+ * (Earth on H/16, each planet on its OWN wobble period — S-P1):
5
+ *
6
+ * e(t) = √(base² + amp² − 2·base·amp·cos θ), θ = 2π·N·∫1/H dt
7
+ *
8
+ * — the distance between two circular orbits (body orbits the wobble
9
+ * centre at radius amp; the centre orbits the Sun at radius base).
10
+ *
11
+ * Extracted from src/script.js computeEccentricityEarth (the browser's
12
+ * 5-arg generic form) and tools/lib/orbital-engine.js computeEccentricity
13
+ * (identical law; R6 aligned its phase to integrated). The structural twin
14
+ * of moon/ecc-channel — but a DIFFERENT law (H/16 orbit law vs the Moon
15
+ * channel's H/3 line; doc 66 keeps the two deliberately distinct).
16
+ *
17
+ * Engine null/toggle semantics stay ENGINE-SIDE, deliberately:
18
+ * - the browser's cyclesBetween carries its DEEP_TIME_MODE toggle and maps
19
+ * null (past tidal lock) → the MEAN eccentricity;
20
+ * - the Node engine pre-computes its snapshot cycles and keeps them when
21
+ * the integrated phase is null (its documented R6 flag polarity).
22
+ * Both call the same law; the dispatch difference is recorded here so
23
+ * nobody "unifies" it without measuring.
24
+ */
25
+
26
+ 'use strict';
27
+
28
+ /**
29
+ * The law of cosines over resolved cycles. null cycles → the MEAN
30
+ * eccentricity √(base² + amp²) (past the tidal-lock asymptote).
31
+ * @param {number | null} cycles @param {number} base @param {number} amplitude
32
+ * @returns {number} */
33
+ function eccentricityFromCycles(cycles, base, amplitude) {
34
+ if (cycles === null) {
35
+ return Math.sqrt(base * base + amplitude * amplitude);
36
+ }
37
+ const phase = cycles * 2 * Math.PI;
38
+ return Math.sqrt(
39
+ base * base + amplitude * amplitude - 2 * base * amplitude * Math.cos(phase)
40
+ );
41
+ }
42
+
43
+ /**
44
+ * Browser-convention evaluator: J2000-FIXED anchor + cycle length, divisor
45
+ * N = H_J2000/cycleLength, integrated phase via the injected engine
46
+ * cyclesBetween (toggle semantics ride along), null → mean.
47
+ * @param {number} currentYear @param {number} anchorYearJ2000
48
+ * @param {number} cycleLengthYearsJ2000 @param {number} base
49
+ * @param {number} amplitude
50
+ * @param {{ holisticYearJ2000: number,
51
+ * cyclesBetween: (yearA: number, yearB: number, divisorN: number) => (number | null) }} env
52
+ * @returns {number} */
53
+ function computeEccentricityIntegrated(currentYear, anchorYearJ2000, cycleLengthYearsJ2000, base, amplitude, env) {
54
+ const divisorN = env.holisticYearJ2000 / cycleLengthYearsJ2000;
55
+ const cycles = env.cyclesBetween(anchorYearJ2000, currentYear, divisorN);
56
+ return eccentricityFromCycles(cycles, base, amplitude);
57
+ }
58
+
59
+ module.exports = { eccentricityFromCycles, computeEccentricityIntegrated };
@@ -0,0 +1,136 @@
1
+ /**
2
+ * The Fibonacci Laws — THE shared implementation (Phase 8.3, layer L2).
3
+ *
4
+ * The scientific heart of the model: the closed derivation loop
5
+ * ψ → inclination amplitude/mean → wobble period → obliquity mean →
6
+ * K → eccentricity amplitude → phase → base
7
+ * that turns per-body Fibonacci divisors and mass fractions into every
8
+ * planet's inclination and eccentricity structure (docs/37, doc 99).
9
+ *
10
+ * Extracted VERBATIM in the BROWSER's expression forms from src/script.js
11
+ * (the ψ loop, calcWobblePeriod, the obliquity-cycle aliases,
12
+ * calcObliquityMean's load-time snapshot branch, the K loop), which
13
+ * tools/lib/constants.js + constants/utils.js hand-mirrored with last-ulp
14
+ * operation-order variants (measured: 32/42 law outputs bit-exact, 10 at
15
+ * the 1e-11..1e-15 relative class) — those variants dissolve here.
16
+ *
17
+ * LOAD-BEARING conventions:
18
+ * - ψ = 3·A_earth·√(M_EARTH_ALONE/M_SUN): the ALONE/SYSTEM mass asymmetry
19
+ * is calibration convention (doc 25) — switching Earth to SYSTEM shifts
20
+ * ψ by 0.612% and would require re-calibrating A_earth.
21
+ * - Wobble = beat of |axial| and |ICRF| RATES (sign-free — Venus's
22
+ * prograde-axial/retrograde-ICRF case); |axial| > 8H ⇒ frozen ⇒
23
+ * wobble = |ICRF| exactly.
24
+ * - The obliquity-mean law here is the SNAPSHOT form (the browser's
25
+ * module-load TDZ fallback — the value both engines actually ship at
26
+ * load); the browser's runtime integrated path stays engine-side with
27
+ * its Phase-8 anchors. Note it uses 1/(H/13), where the runtime
28
+ * integrated form uses 13/H — historical operation orders, preserved.
29
+ * - K-law phase offset: 90° in-phase, 270° anti-phase (Saturn) — the n=7
30
+ * System Reset state (all planets at mean e, Saturn falling).
31
+ * - The Node mirror carried an `else` branch for bodies without a wobble
32
+ * period — DEAD code (the loop is fibonacciD-guarded and all seven
33
+ * carriers have wobble periods); dropped here, recorded in the commit.
34
+ */
35
+
36
+ 'use strict';
37
+
38
+ /**
39
+ * ψ constant from Earth's calibration. @param {{
40
+ * earthInvPlaneInclinationAmplitude: number,
41
+ * massEarthAlone: number, massSun: number }} c @returns {number} */
42
+ function computePsiConstant(c) {
43
+ return 3 * c.earthInvPlaneInclinationAmplitude * Math.sqrt(c.massEarthAlone / c.massSun);
44
+ }
45
+
46
+ /**
47
+ * ψ law: invariable-plane inclination amplitude and mean.
48
+ * @param {{ fibonacciD: number, massFrac: number,
49
+ * invPlaneInclinationJ2000: number, longitudePerihelion: number,
50
+ * inclinationCycleAnchor: number, antiPhase: boolean }} b
51
+ * @param {number} psiConstant
52
+ * @returns {{ amplitude: number, mean: number }} */
53
+ function computeInclinationLaw(b, psiConstant) {
54
+ const amplitude = psiConstant / (b.fibonacciD * Math.sqrt(b.massFrac));
55
+ const antiPhase = b.antiPhase ? -1 : 1;
56
+ const mean = b.invPlaneInclinationJ2000
57
+ - antiPhase * amplitude * Math.cos((b.longitudePerihelion - b.inclinationCycleAnchor) * Math.PI / 180);
58
+ return { amplitude, mean };
59
+ }
60
+
61
+ /**
62
+ * Wobble period: beat of axial precession and perihelion ICRF precession.
63
+ * @param {number} periEclYr @param {number} axialYr @param {number} H
64
+ * @returns {number} years */
65
+ function computeWobblePeriodYears(periEclYr, axialYr, H) {
66
+ const H13 = H / 13;
67
+ const inclICRF = (periEclYr * H13) / (H13 - periEclYr);
68
+ if (Math.abs(axialYr) > 8 * H) return Math.abs(inclICRF);
69
+ const wobbleRate = Math.abs(1 / Math.abs(axialYr) - 1 / Math.abs(inclICRF));
70
+ return 1 / wobbleRate;
71
+ }
72
+
73
+ /**
74
+ * Obliquity cycle with the Venus/Neptune fallback: the record's cycle if
75
+ * present, else |ICRF| (tidally damped — the two-component obliquity
76
+ * formula cancels exactly, constant tilt).
77
+ * @param {number | null | undefined} obliquityCycleYears
78
+ * @param {number} periEclYr @param {number} H @returns {number} */
79
+ function resolveObliquityCycleYears(obliquityCycleYears, periEclYr, H) {
80
+ if (obliquityCycleYears !== undefined && obliquityCycleYears !== null) return obliquityCycleYears;
81
+ return Math.abs(1 / (1 / periEclYr - 13 / H));
82
+ }
83
+
84
+ /**
85
+ * Mean obliquity, SNAPSHOT form (the load-time law both engines ship):
86
+ * mean = tiltJ2000 + amp·cos(ωᵢ·t₂₀₀₀) − amp·cos(ωₒ·t₂₀₀₀).
87
+ * @param {{ axialTiltJ2000: number, invPlaneInclinationAmplitude: number,
88
+ * perihelionEclipticYears: number }} b
89
+ * @param {number | null | undefined} obliqCycleYears — falsy ⇒ static tilt
90
+ * @param {{ H: number, t2000: number }} env — t2000 = 2000 − eccentricity
91
+ * anchor (balancedYear − systemResetN·H)
92
+ * @returns {number} degrees */
93
+ function computeObliquityMeanSnapshot(b, obliqCycleYears, env) {
94
+ if (!obliqCycleYears) return b.axialTiltJ2000;
95
+ const amp = b.invPlaneInclinationAmplitude;
96
+ const genPrecRate = 1 / (env.H / 13);
97
+ const icrfPeriod = 1 / (1 / b.perihelionEclipticYears - genPrecRate);
98
+ return b.axialTiltJ2000 + amp * Math.cos(2 * Math.PI * env.t2000 / icrfPeriod)
99
+ - amp * Math.cos(2 * Math.PI * env.t2000 / obliqCycleYears);
100
+ }
101
+
102
+ /**
103
+ * K constant from Earth's calibration. @param {{
104
+ * eccentricityAmplitude: number, massEarthAlone: number, massSun: number,
105
+ * earthTiltMeanDeg: number }} c @returns {number} */
106
+ function computeKConstant(c) {
107
+ return c.eccentricityAmplitude * Math.sqrt(c.massEarthAlone / c.massSun)
108
+ / (Math.sin(c.earthTiltMeanDeg * Math.PI / 180) * Math.sqrt(3));
109
+ }
110
+
111
+ /**
112
+ * K law: eccentricity amplitude, base and J2000 phase.
113
+ * @param {{ fibonacciD: number, massFrac: number, solarYearInput: number,
114
+ * orbitalEccentricityJ2000: number, antiPhase: boolean }} b
115
+ * @param {{ kConstant: number, obliquityMeanDeg: number,
116
+ * wobblePeriodYears: number, t2000: number, meanSolarYearDays: number }} env
117
+ * @returns {{ amplitude: number, base: number, phaseJ2000: number }} */
118
+ function computeEccentricityLaw(b, env) {
119
+ const a = Math.pow(b.solarYearInput / env.meanSolarYearDays, 2 / 3);
120
+ const amplitude = env.kConstant * Math.sin(Math.abs(env.obliquityMeanDeg) * Math.PI / 180) * Math.sqrt(b.fibonacciD)
121
+ / (Math.sqrt(b.massFrac) * Math.pow(a, 1.5));
122
+ const eJ2000 = b.orbitalEccentricityJ2000;
123
+ const phaseOffset = b.antiPhase ? 270 : 90;
124
+ const phaseDeg = (env.t2000 / env.wobblePeriodYears) * 360 + phaseOffset;
125
+ const cosTheta = Math.cos(phaseDeg * Math.PI / 180);
126
+ const sinTheta = Math.sin(phaseDeg * Math.PI / 180);
127
+ const disc = eJ2000 * eJ2000 - amplitude * amplitude * sinTheta * sinTheta;
128
+ const base = amplitude * cosTheta + Math.sqrt(Math.max(0, disc));
129
+ return { amplitude, base, phaseJ2000: ((phaseDeg % 360) + 360) % 360 };
130
+ }
131
+
132
+ module.exports = {
133
+ computePsiConstant, computeInclinationLaw, computeWobblePeriodYears,
134
+ resolveObliquityCycleYears, computeObliquityMeanSnapshot,
135
+ computeKConstant, computeEccentricityLaw,
136
+ };
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Planet orbital geometry — THE shared implementation (Phase 8.3, layer L1).
3
+ *
4
+ * ONE law set, N body records: the derivation the browser hand-unrolled per
5
+ * planet (mercurySolarYearCount … halleysRotationPeriod, src/script.js
6
+ * ~2145–2237) and Node kept as computePlanetDerived/computeAdditionalDerived
7
+ * (tools/lib/constants.js). This file replaces all three copies with one
8
+ * type-branched function over a body record — the shape the model's claim
9
+ * has always had (six laws, per-body parameters).
10
+ *
11
+ * EXPRESSION FORMS ARE THE BROWSER'S (the certified engine) — e.g.
12
+ * `Math.round((H·mSY)/input)`, `((H/N)**2)**(1/3)` — so browser bit-identity
13
+ * holds by construction; Node's last-ulp variants dissolve here (the
14
+ * cardinal-extraction pattern).
15
+ *
16
+ * Type classification (the survey's measured taxonomy):
17
+ * - Type I (mercury, venus): peri = a·e_real·100, elip = peri/2
18
+ * - Type II (mars, eros): elip first, peri = a·e_base·100 + elip
19
+ * - Type III (jupiter…neptune): elip = geocentric 2·e_E·100·sin(ϖ_E − ϖ_p)
20
+ * — the only formula that reads Earth's elements; peri = e_real·a·100
21
+ * - pluto: peri = e_BASE·a·100 (raw base, no e_real), elip = peri/2
22
+ * - halleys: elip = (e_base·a − e_real·a)·100, peri = e_real·a·2·100
23
+ * - ceres: orbitDistanceOverride, geometry-only (no ellipse family)
24
+ * - mercury rotation: the 3:2 spin-orbit lock (period from N, not a
25
+ * rotation input)
26
+ *
27
+ * Saturn's scene-side negation of elipticOrbit (antiPhase) is a SCENE
28
+ * convention and stays engine-side.
29
+ *
30
+ * MINOR-BODY TYPES ARE PLACEHOLDERS (model-parameters.json _typeNote): the
31
+ * small bodies borrow the planet taxonomy geometrically but are more
32
+ * susceptible to planetary gravity disturbances; a perturbation-aware
33
+ * type is expected from the Phase 18 refit. It lands as ONE new branch
34
+ * here + a JSON type flip per body — both engines pick it up automatically.
35
+ */
36
+
37
+ 'use strict';
38
+
39
+ /**
40
+ * @typedef {{
41
+ * key: string,
42
+ * type?: string,
43
+ * solarYearInput: number,
44
+ * orbitalEccentricityBase?: number,
45
+ * longitudePerihelion?: number,
46
+ * ascendingNode?: number,
47
+ * rotationPeriodDays?: number,
48
+ * orbitDistanceOverride?: number,
49
+ * }} PlanetGeometryBody
50
+ */
51
+
52
+ /**
53
+ * @param {PlanetGeometryBody} body
54
+ * @param {{
55
+ * holisticYears: number,
56
+ * meanSolarYearDays: number,
57
+ * currentAUDistanceKm: number,
58
+ * earthEccentricityJ2000: number,
59
+ * earthPerihelionLongitudeJ2000Deg: number,
60
+ * }} env — the engine's J2000 values (browser passes its live globals at
61
+ * load time, preserving its original initial-derivation semantics; the
62
+ * epoch machinery that later mutates the browser's `let` aliases is
63
+ * untouched by this layer).
64
+ * @returns {{
65
+ * solarYearCount: number, orbitDistance: number, periodYears: number,
66
+ * realOrbitalEccentricity: (number | undefined),
67
+ * elipticOrbit: (number | undefined), perihelionDistance: (number | undefined),
68
+ * speedKmh: number, rotationPeriodHours: (number | undefined),
69
+ * eccentricityPerihelion: (number | undefined), lowestPoint: (number | undefined),
70
+ * }}
71
+ */
72
+ function derivePlanetGeometry(body, env) {
73
+ const { key, type, solarYearInput, orbitalEccentricityBase: base,
74
+ longitudePerihelion, ascendingNode, rotationPeriodDays, orbitDistanceOverride } = body;
75
+ const { holisticYears: H, meanSolarYearDays: mSY, currentAUDistanceKm: AU,
76
+ earthEccentricityJ2000, earthPerihelionLongitudeJ2000Deg } = env;
77
+
78
+ const solarYearCount = Math.round((H * mSY) / solarYearInput);
79
+ const orbitDistance = orbitDistanceOverride !== undefined
80
+ ? orbitDistanceOverride
81
+ : ((H / solarYearCount) ** 2) ** (1 / 3);
82
+ const periodYears = H / solarYearCount;
83
+ const speedKmh = (orbitDistance * AU * Math.PI * 2) / (mSY * (H / solarYearCount)) / 24;
84
+
85
+ /** @type {number | undefined} */
86
+ let realOrbitalEccentricity;
87
+ /** @type {number | undefined} */
88
+ let elipticOrbit;
89
+ /** @type {number | undefined} */
90
+ let perihelionDistance;
91
+ /** @type {number | undefined} */
92
+ let eccentricityPerihelion;
93
+
94
+ if (key === 'ceres') {
95
+ // Geometry-only body — no ellipse family in either engine.
96
+ } else if (key === 'pluto') {
97
+ realOrbitalEccentricity = undefined; // deliberately absent (raw base used)
98
+ perihelionDistance = /** @type {number} */ (base) * orbitDistance * 100;
99
+ elipticOrbit = perihelionDistance / 2;
100
+ } else if (key === 'halleys') {
101
+ const b = /** @type {number} */ (base);
102
+ realOrbitalEccentricity = b / (1 + b);
103
+ elipticOrbit = ((b * orbitDistance) - (realOrbitalEccentricity * orbitDistance)) * 100;
104
+ perihelionDistance = realOrbitalEccentricity * orbitDistance * 2 * 100;
105
+ } else if (type === 'I') {
106
+ const b = /** @type {number} */ (base);
107
+ realOrbitalEccentricity = b / (1 + b);
108
+ perihelionDistance = orbitDistance * realOrbitalEccentricity * 100;
109
+ elipticOrbit = perihelionDistance / 2;
110
+ eccentricityPerihelion = (perihelionDistance / 2) * b;
111
+ } else if (type === 'II') {
112
+ const b = /** @type {number} */ (base);
113
+ realOrbitalEccentricity = b / (1 + b);
114
+ elipticOrbit = ((realOrbitalEccentricity * orbitDistance) / 2) * 100
115
+ + ((b * orbitDistance) - (realOrbitalEccentricity * orbitDistance)) * 100;
116
+ perihelionDistance = (orbitDistance * b * 100) + elipticOrbit;
117
+ } else if (type === 'III') {
118
+ const b = /** @type {number} */ (base);
119
+ realOrbitalEccentricity = b / (1 + b);
120
+ // Geocentric correction: Earth's eccentricity creates annual parallax
121
+ // variation ∝ sin(ϖ_Earth − ϖ_planet); factor 2 from off-centre geometry.
122
+ const dw = (earthPerihelionLongitudeJ2000Deg - /** @type {number} */ (longitudePerihelion)) * Math.PI / 180;
123
+ elipticOrbit = 2 * earthEccentricityJ2000 * 100 * Math.sin(dw);
124
+ perihelionDistance = realOrbitalEccentricity * orbitDistance * 100;
125
+ }
126
+
127
+ /** @type {number | undefined} */
128
+ let rotationPeriodHours;
129
+ if (key === 'mercury') {
130
+ // 3:2 spin-orbit lock — period from the orbit count itself.
131
+ rotationPeriodHours = 24 * (mSY * H) / (solarYearCount * 3 / 2);
132
+ } else if (rotationPeriodDays !== undefined) {
133
+ rotationPeriodHours = 24 * (mSY * H) / Math.round((mSY * H) / rotationPeriodDays);
134
+ }
135
+
136
+ const lowestPoint = ascendingNode !== undefined ? 180 - ascendingNode : undefined;
137
+
138
+ return {
139
+ solarYearCount, orbitDistance, periodYears, realOrbitalEccentricity,
140
+ elipticOrbit, perihelionDistance, speedKmh, rotationPeriodHours,
141
+ eccentricityPerihelion, lowestPoint,
142
+ };
143
+ }
144
+
145
+ module.exports = { derivePlanetGeometry };
@@ -0,0 +1,185 @@
1
+ /**
2
+ * createPlanetModel — the planet composition front door (Phase 8.3, L10).
3
+ *
4
+ * ONE law set, N body records: this factory binds an environment once and
5
+ * runs the certified derivation chain over every body record, in the order
6
+ * the chain requires (each step feeds the next):
7
+ *
8
+ * ψ constant → inclination law (amplitude, mean)
9
+ * → wobble period (beat of |axial| and |ICRF|)
10
+ * K constant → obliquity mean (snapshot form) → eccentricity law
11
+ * (amplitude, base, J2000 phase)
12
+ * geometry (the type-branched ellipse family — the law-derived
13
+ * eccentricity base feeds geometry for the seven carriers;
14
+ * minor bodies use their record's base)
15
+ *
16
+ * THIN BY DESIGN. This is the composition surface, not a rewiring: both
17
+ * engines keep their existing direct call sites into the law modules, and
18
+ * the runtime channels (eccentricity-at-year, orientation, the ascending-
19
+ * node integrator, predictive precession) stay direct module calls because
20
+ * they consume engine-owned state (scene JD, epoch machinery, fitted
21
+ * tables). What this factory adds is the seam future bodies plug into:
22
+ * a new body is a record + (at most) one new geometry branch — see the
23
+ * minor-body placeholder note in geometry.cjs (Phase 18 perturbation
24
+ * types land the same way).
25
+ *
26
+ * Guard semantics mirror tools/lib/constants.js verbatim: the law steps
27
+ * run only where the record carries the required fields (fibonacciD +
28
+ * mass fraction for the ψ/K families, perihelion + axial periods for
29
+ * wobble); geometry runs for every body. The identity gate
30
+ * (test/planet-model-identity.test.mjs) holds this factory bit-exact
31
+ * against the shipped Node derivation.
32
+ */
33
+
34
+ 'use strict';
35
+
36
+ const { derivePlanetGeometry } = require('./geometry.cjs');
37
+ const FL = require('./fibonacci-laws.cjs');
38
+
39
+ /**
40
+ * @typedef {Object} PlanetModelBody
41
+ * @property {string} [type] - geometry type branch ('I' | 'II' | 'III')
42
+ * @property {number} solarYearInput
43
+ * @property {number} [fibonacciD]
44
+ * @property {number} [invPlaneInclinationJ2000]
45
+ * @property {number} [longitudePerihelion]
46
+ * @property {number} [inclinationCycleAnchor]
47
+ * @property {boolean} [antiPhase]
48
+ * @property {number} [perihelionEclipticYears]
49
+ * @property {number} [axialPrecessionYears]
50
+ * @property {number|null} [obliquityCycle]
51
+ * @property {number} [axialTiltJ2000]
52
+ * @property {number} [orbitalEccentricityJ2000]
53
+ * @property {number} [ascendingNode]
54
+ * @property {number} [rotationPeriodDays]
55
+ * @property {number} [orbitalEccentricityBase] - minor bodies: JSON input;
56
+ * carriers: ignored (the K law derives it)
57
+ * @property {number} [orbitDistanceOverride]
58
+ */
59
+
60
+ /**
61
+ * @typedef {Object} PlanetModelEnv
62
+ * @property {number} holisticYears
63
+ * @property {number} meanSolarYearDays
64
+ * @property {number} balancedYear
65
+ * @property {number} systemResetN
66
+ * @property {number} currentAUDistanceKm
67
+ * @property {number} earthEccentricityJ2000
68
+ * @property {number} earthPerihelionLongitudeJ2000Deg
69
+ * @property {{ earthInvPlaneInclinationAmplitude: number,
70
+ * massEarthAlone: number, massSun: number,
71
+ * eccentricityAmplitude: number, earthTiltMeanDeg: number }} calibration
72
+ * @property {Record<string, number>} massFractions
73
+ */
74
+
75
+ /**
76
+ * @typedef {Object} PlanetModelRecord
77
+ * @property {number} [invPlaneInclinationAmplitude]
78
+ * @property {number} [invPlaneInclinationMean]
79
+ * @property {number} [wobblePeriodYears]
80
+ * @property {number} [obliquityMeanDeg]
81
+ * @property {number} [eccentricityAmplitude]
82
+ * @property {number} [eccentricityBase]
83
+ * @property {number} [eccentricityPhaseJ2000Deg]
84
+ * @property {ReturnType<typeof derivePlanetGeometry>} geometry
85
+ */
86
+
87
+ /**
88
+ * Run the full derivation chain over a set of body records.
89
+ *
90
+ * @param {PlanetModelEnv} env
91
+ * @param {Record<string, PlanetModelBody>} bodies - keyed by body name
92
+ * (the key selects the body-unique geometry branches: mercury, pluto,
93
+ * halleys, ceres)
94
+ * @returns {{ psiConstant: number, kConstant: number,
95
+ * eccentricityAnchor: number, t2000: number,
96
+ * bodies: Record<string, PlanetModelRecord> }}
97
+ */
98
+ function createPlanetModel(env, bodies) {
99
+ const psiConstant = FL.computePsiConstant({
100
+ earthInvPlaneInclinationAmplitude: env.calibration.earthInvPlaneInclinationAmplitude,
101
+ massEarthAlone: env.calibration.massEarthAlone,
102
+ massSun: env.calibration.massSun,
103
+ });
104
+ const kConstant = FL.computeKConstant({
105
+ eccentricityAmplitude: env.calibration.eccentricityAmplitude,
106
+ massEarthAlone: env.calibration.massEarthAlone,
107
+ massSun: env.calibration.massSun,
108
+ earthTiltMeanDeg: env.calibration.earthTiltMeanDeg,
109
+ });
110
+ // Anchor = balancedYear − systemResetN·H (n=7: the System Reset state).
111
+ const eccentricityAnchor = env.balancedYear - env.systemResetN * env.holisticYears;
112
+ const t2000 = 2000 - eccentricityAnchor;
113
+
114
+ const geomEnv = {
115
+ holisticYears: env.holisticYears,
116
+ meanSolarYearDays: env.meanSolarYearDays,
117
+ currentAUDistanceKm: env.currentAUDistanceKm,
118
+ earthEccentricityJ2000: env.earthEccentricityJ2000,
119
+ earthPerihelionLongitudeJ2000Deg: env.earthPerihelionLongitudeJ2000Deg,
120
+ };
121
+
122
+ /** @type {Record<string, PlanetModelRecord>} */
123
+ const out = {};
124
+ for (const [key, b] of Object.entries(bodies)) {
125
+ const massFrac = env.massFractions[key];
126
+ /** @type {PlanetModelRecord} */
127
+ const rec = /** @type {PlanetModelRecord} */ ({});
128
+
129
+ if (b.fibonacciD && massFrac && b.invPlaneInclinationJ2000 !== undefined) {
130
+ const il = FL.computeInclinationLaw({
131
+ fibonacciD: b.fibonacciD, massFrac,
132
+ invPlaneInclinationJ2000: b.invPlaneInclinationJ2000,
133
+ longitudePerihelion: /** @type {number} */ (b.longitudePerihelion),
134
+ inclinationCycleAnchor: /** @type {number} */ (b.inclinationCycleAnchor),
135
+ antiPhase: /** @type {boolean} */ (b.antiPhase),
136
+ }, psiConstant);
137
+ rec.invPlaneInclinationAmplitude = il.amplitude;
138
+ rec.invPlaneInclinationMean = il.mean;
139
+ }
140
+
141
+ if (b.perihelionEclipticYears && b.axialPrecessionYears) {
142
+ rec.wobblePeriodYears = FL.computeWobblePeriodYears(
143
+ b.perihelionEclipticYears, b.axialPrecessionYears, env.holisticYears,
144
+ );
145
+ }
146
+
147
+ if (b.fibonacciD && massFrac) {
148
+ rec.obliquityMeanDeg = FL.computeObliquityMeanSnapshot({
149
+ axialTiltJ2000: /** @type {number} */ (b.axialTiltJ2000),
150
+ invPlaneInclinationAmplitude: /** @type {number} */ (rec.invPlaneInclinationAmplitude),
151
+ perihelionEclipticYears: /** @type {number} */ (b.perihelionEclipticYears),
152
+ }, b.obliquityCycle, { H: env.holisticYears, t2000 });
153
+ const el = FL.computeEccentricityLaw({
154
+ fibonacciD: b.fibonacciD, massFrac,
155
+ solarYearInput: b.solarYearInput,
156
+ orbitalEccentricityJ2000: /** @type {number} */ (b.orbitalEccentricityJ2000),
157
+ antiPhase: /** @type {boolean} */ (b.antiPhase),
158
+ }, {
159
+ kConstant, obliquityMeanDeg: rec.obliquityMeanDeg,
160
+ wobblePeriodYears: /** @type {number} */ (rec.wobblePeriodYears), t2000,
161
+ meanSolarYearDays: env.meanSolarYearDays,
162
+ });
163
+ rec.eccentricityAmplitude = el.amplitude;
164
+ rec.eccentricityBase = el.base;
165
+ rec.eccentricityPhaseJ2000Deg = el.phaseJ2000;
166
+ }
167
+
168
+ rec.geometry = derivePlanetGeometry({
169
+ key, type: b.type,
170
+ solarYearInput: b.solarYearInput,
171
+ orbitalEccentricityBase: rec.eccentricityBase !== undefined
172
+ ? rec.eccentricityBase : b.orbitalEccentricityBase,
173
+ longitudePerihelion: b.longitudePerihelion,
174
+ ascendingNode: b.ascendingNode,
175
+ rotationPeriodDays: b.rotationPeriodDays,
176
+ orbitDistanceOverride: b.orbitDistanceOverride,
177
+ }, geomEnv);
178
+
179
+ out[key] = rec;
180
+ }
181
+
182
+ return { psiConstant, kConstant, eccentricityAnchor, t2000, bodies: out };
183
+ }
184
+
185
+ module.exports = { createPlanetModel };
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Deep-time planet orbit chain — THE shared implementation (Phase 8.3, L6).
3
+ * Driver 2: solar mass loss. Three pure laws:
4
+ *
5
+ * - massLossScaledLinearAtAge — adiabatic a·M = const ⇒ a(t) scales
6
+ * LINEARLY with (1 − Δm). Serves Earth's AU (km) and every planet's
7
+ * semi-major axis (units are the caller's — S-P11 resolved: the two
8
+ * engines shared this driver all along, differing only in km vs
9
+ * AU-ratio normalization).
10
+ * - driver2PeriodSecondsAtAge — Kepler under mass loss, dT/T = −2·dM/M ⇒
11
+ * T(t) = T₀·(1 − Δm)². The integrand of the planet cycle chains
12
+ * (chain-cycles) and the scene integrators (S-P2).
13
+ * - synodicPeriodSeconds — the Earth–planet beat Tp·Ty/|Tp − Ty|.
14
+ *
15
+ * t_Ma is AGE in Myr (positive = past); Δm = massLossFracPerYear·t_Ma·1e6.
16
+ * The t_Ma === 0 fast paths return the J2000 values EXACTLY.
17
+ */
18
+
19
+ 'use strict';
20
+
21
+ /**
22
+ * @param {number} tMa @param {number} valueJ2000
23
+ * @param {number} massLossFracPerYear @returns {number} */
24
+ function massLossScaledLinearAtAge(tMa, valueJ2000, massLossFracPerYear) {
25
+ if (tMa === 0) return valueJ2000;
26
+ const massLossFraction = massLossFracPerYear * tMa * 1e6;
27
+ return valueJ2000 * (1 - massLossFraction);
28
+ }
29
+
30
+ /**
31
+ * @param {number} tMa @param {number} periodJ2000Seconds
32
+ * @param {number} massLossFracPerYear @returns {number} */
33
+ function driver2PeriodSecondsAtAge(tMa, periodJ2000Seconds, massLossFracPerYear) {
34
+ if (tMa === 0) return periodJ2000Seconds;
35
+ const massLossFraction = massLossFracPerYear * tMa * 1e6;
36
+ return periodJ2000Seconds * Math.pow(1 - massLossFraction, 2);
37
+ }
38
+
39
+ /**
40
+ * @param {number} planetPeriodSeconds @param {number} yearSeconds
41
+ * @returns {number} */
42
+ function synodicPeriodSeconds(planetPeriodSeconds, yearSeconds) {
43
+ return planetPeriodSeconds * yearSeconds / Math.abs(planetPeriodSeconds - yearSeconds);
44
+ }
45
+
46
+ module.exports = { massLossScaledLinearAtAge, driver2PeriodSecondsAtAge, synodicPeriodSeconds };