@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
package/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# @essrt/physics
|
|
2
|
+
|
|
3
|
+
The physics core of **ESSRT** — the Expanding Solar System Resonance Theory —
|
|
4
|
+
as implemented by the [Holistic Universe Model](https://3d.holisticuniverse.com)
|
|
5
|
+
([source, AGPL-3.0](https://github.com/dvansonsbeek/3d)). Pure computation:
|
|
6
|
+
no I/O, no globals, no DOM, constants injected. The package ships the
|
|
7
|
+
**complete model**, including the fitted coefficients, so an installed copy
|
|
8
|
+
reproduces the hosted simulator bit-for-bit on the shared golden masters.
|
|
9
|
+
|
|
10
|
+
## Versioning — two axes, deliberately
|
|
11
|
+
|
|
12
|
+
- **Package semver** (this package's `version`) tracks the **API axis** —
|
|
13
|
+
what `import` gives you.
|
|
14
|
+
- **Model identity** rides *inside* every published version: the
|
|
15
|
+
`essrt.modelVersion` field in `package.json` plus `CONSTANTS_HASH` and
|
|
16
|
+
`COEFFICIENTS_HASH` in `src/constants/`. A refit or structural model
|
|
17
|
+
change always lands as a new package version — npm's immutability then
|
|
18
|
+
guarantees a pinned version's results never change.
|
|
19
|
+
|
|
20
|
+
Cite results as *model vX.Y (package A.B.C)*.
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
import { DEFAULT_CONSTANTS, FITTED_COEFFICIENTS, createEpochPrimitives } from '@essrt/physics';
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Domain modules are CJS subpath exports, each a `create*` factory taking its
|
|
29
|
+
dependencies explicitly (the injectable-constants design — counterfactual
|
|
30
|
+
runs are first-class):
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
const { createEclipseFinders } = require('@essrt/physics/eclipse/finders');
|
|
34
|
+
const { createPredictivePrecession } = require('@essrt/physics/planets/predict');
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
See the `exports` map in `package.json` for the full surface: moon
|
|
38
|
+
(arguments/series/apparent/ecc-channel/month-chain), planets (geometry,
|
|
39
|
+
corrections, predict, model, fibonacci-laws, …), deltat (cycles, deep-time,
|
|
40
|
+
historical), cardinal points, phase, chain-cycles, sun, climate, eclipse
|
|
41
|
+
finders, and the published reference curves.
|
|
42
|
+
|
|
43
|
+
## Provenance
|
|
44
|
+
|
|
45
|
+
Every value derives from the model parameters and fitted coefficients in
|
|
46
|
+
this package; the fitting pipeline, verification gates, dataset manifest and
|
|
47
|
+
documentation live in the [public repository](https://github.com/dvansonsbeek/3d).
|
|
48
|
+
Preprint: <https://doi.org/10.21203/rs.3.rs-8758810/v4>.
|
|
49
|
+
|
|
50
|
+
## Licence
|
|
51
|
+
|
|
52
|
+
AGPL-3.0-or-later. Commercial licensing: dennis@holisticuniverse.com.
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@essrt/physics",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "ESSRT (Expanding Solar System Resonance Theory) physics core \u2014 pure, injectable-constants, no I/O, no globals, no DOM. The complete model incl. fitted coefficients; every published version immutably ships one recorded model identity.",
|
|
5
|
+
"essrt": {
|
|
6
|
+
"modelVersion": "v10.0",
|
|
7
|
+
"_note": "The model identity this package version ships (IP-unified-architecture \u00a710: package semver tracks the API axis; the model version + CONSTANTS_HASH/COEFFICIENTS_HASH in src/constants/ carry the scientific identity). The version-pinning gate enforces this pairing against public/input/model-version.json."
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "src/index.js",
|
|
11
|
+
"files": [
|
|
12
|
+
"src",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/dvansonsbeek/3d.git",
|
|
18
|
+
"directory": "packages/physics"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://3d.holisticuniverse.com",
|
|
21
|
+
"keywords": [
|
|
22
|
+
"essrt",
|
|
23
|
+
"astronomy",
|
|
24
|
+
"solar-system",
|
|
25
|
+
"deep-time",
|
|
26
|
+
"eclipse",
|
|
27
|
+
"precession",
|
|
28
|
+
"geocentric-model"
|
|
29
|
+
],
|
|
30
|
+
"exports": {
|
|
31
|
+
".": "./src/index.js",
|
|
32
|
+
"./phase": "./src/phase/index.cjs",
|
|
33
|
+
"./cardinal": "./src/cardinal/index.cjs",
|
|
34
|
+
"./moon/ecc-channel": "./src/moon/ecc-channel.cjs",
|
|
35
|
+
"./moon/month-chain": "./src/moon/month-chain.cjs",
|
|
36
|
+
"./chain-cycles": "./src/chain-cycles/index.cjs",
|
|
37
|
+
"./moon/arguments": "./src/moon/arguments.cjs",
|
|
38
|
+
"./moon/series": "./src/moon/series.cjs",
|
|
39
|
+
"./moon/apparent": "./src/moon/apparent.cjs",
|
|
40
|
+
"./planets/geometry": "./src/planets/geometry.cjs",
|
|
41
|
+
"./planets/fibonacci-laws": "./src/planets/fibonacci-laws.cjs",
|
|
42
|
+
"./planets/ecc-channel": "./src/planets/ecc-channel.cjs",
|
|
43
|
+
"./planets/orientation": "./src/planets/orientation.cjs",
|
|
44
|
+
"./planets/asc-node-integrator": "./src/planets/asc-node-integrator.cjs",
|
|
45
|
+
"./planets/orbit-chain": "./src/planets/orbit-chain.cjs",
|
|
46
|
+
"./planets/corrections": "./src/planets/corrections.cjs",
|
|
47
|
+
"./planets/predict": "./src/planets/predict.cjs",
|
|
48
|
+
"./planets/model": "./src/planets/model.cjs",
|
|
49
|
+
"./deltat/cycles": "./src/deltat/cycles.cjs",
|
|
50
|
+
"./deltat/deep-time": "./src/deltat/deep-time.cjs",
|
|
51
|
+
"./deltat/historical": "./src/deltat/historical.cjs",
|
|
52
|
+
"./climate/l1-orbital": "./src/climate/l1-orbital.cjs",
|
|
53
|
+
"./eclipse/finders": "./src/eclipse/finders.cjs",
|
|
54
|
+
"./reference/published-curves": "./src/reference/published-curves.cjs",
|
|
55
|
+
"./sun/longitude-correction": "./src/sun/longitude-correction.cjs"
|
|
56
|
+
},
|
|
57
|
+
"license": "AGPL-3.0-or-later"
|
|
58
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cardinal-point model — THE shared implementation (Phase 7.2).
|
|
3
|
+
*
|
|
4
|
+
* The §10 derived form and everything on it, extracted verbatim from
|
|
5
|
+
* tools/lib/orbital-engine.js (which the browser mirrored function-for-
|
|
6
|
+
* function — this file replaces both copies):
|
|
7
|
+
*
|
|
8
|
+
* JD_X(Y) = anchor_X + ΣT_trop(Y) + δ_X(Y) − δ_X(2000)
|
|
9
|
+
* ΣT_trop = lincoef·(Y−2000) + driftTerm(Y) + Ih(Y)
|
|
10
|
+
* δ_X = sinusoids + equation-of-centre orders (e(t)ⁿ·sin(nM), e(t)
|
|
11
|
+
* the LAW OF COSINES) + §10g quadrature-locked joint sidebands
|
|
12
|
+
* T_X(Y) = the EXACT term-by-term derivative of JD_X
|
|
13
|
+
*
|
|
14
|
+
* LOAD-BEARING, do not "improve":
|
|
15
|
+
* - lincoef/h0/h1 come from the fit and are used VERBATIM — recomputing
|
|
16
|
+
* lincoef from the 1-year anchor injects a −12,276 s ramp; holding H
|
|
17
|
+
* constant inside Ih costs up to 5.2 s (§10c/§10e-quinquies).
|
|
18
|
+
* - The drift term is Simpson on 2000-yr NODE SPACING (not a fixed node
|
|
19
|
+
* count: that was exact in-window and −33,758 s at −380 Ma) plus the
|
|
20
|
+
* Euler–Maclaurin endpoint term (omitting it costs 6–13 s).
|
|
21
|
+
* - The equation-of-centre terms are {order,sin,cos}, NOT sinusoids — read
|
|
22
|
+
* as H/16+H/32 harmonics they are wrong by the whole ~1.78 d braid.
|
|
23
|
+
* - §10g joint sidebands are SHARED across the four points with phase
|
|
24
|
+
* order·λ_X − 2π·div·c — COUNTER-rotating. The minus sign is load-bearing:
|
|
25
|
+
* the co-rotating sense captures NOTHING (measured — the sign experiment,
|
|
26
|
+
* doc 99 "the braid law").
|
|
27
|
+
* - The year-length derivative keeps ONE deliberate divergence from the
|
|
28
|
+
* exact derivative: its drift part uses the real-LOD convention
|
|
29
|
+
* (`meanYearRealLodDays`) — the tweakpane's epoch-local days ("~400 days
|
|
30
|
+
* at the Devonian") — where the JD form integrates the SI form. Equal at
|
|
31
|
+
* J2000 up to the known 118 ms fit-basis gap.
|
|
32
|
+
*
|
|
33
|
+
* The `cyclesBetween` the caller injects is the ENGINE'S OWN function, so
|
|
34
|
+
* each engine's deep-time/snapshot toggle semantics are preserved exactly;
|
|
35
|
+
* under deep time both engines now route it through @essrt/physics/phase.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
'use strict';
|
|
39
|
+
|
|
40
|
+
/** @type {Record<string, number>} */
|
|
41
|
+
const JOINT_LAMBDA = { SS: 0, AE: Math.PI / 2, WS: Math.PI, VE: 1.5 * Math.PI };
|
|
42
|
+
|
|
43
|
+
const DRIFT_NODE_SPACING_YEARS = 2000;
|
|
44
|
+
const DRIFT_SIMPSON_N_MIN = 64;
|
|
45
|
+
|
|
46
|
+
/** @typedef {{ lincoef: number, h0: number, h1: number }} DerivedCoefs */
|
|
47
|
+
/** @typedef {{ order: number, sin: number, cos: number }} EccTerm */
|
|
48
|
+
/** @typedef {{ order: number, div: number, sin: number, cos: number }} JointTerm */
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {{
|
|
52
|
+
* isDeepTime: () => boolean,
|
|
53
|
+
* constants: {
|
|
54
|
+
* anchors: Record<string, number>,
|
|
55
|
+
* harmonics: Record<string, Array<[number, number, number]>>,
|
|
56
|
+
* eccTerms: (Record<string, EccTerm[]> | null),
|
|
57
|
+
* jointTerms: ({ terms: JointTerm[] } | null),
|
|
58
|
+
* derived: (DerivedCoefs | null),
|
|
59
|
+
* tropicalHarmonics: Array<[number, number, number]>,
|
|
60
|
+
* balancedYear: number,
|
|
61
|
+
* meanSolarYearDays: number,
|
|
62
|
+
* hJ2000: number,
|
|
63
|
+
* eccentricityBase: number,
|
|
64
|
+
* eccentricityAmplitude: number,
|
|
65
|
+
* tiltMeanDeg: number,
|
|
66
|
+
* raAngleDeg: number,
|
|
67
|
+
* inclAmplitudeDeg: number,
|
|
68
|
+
* },
|
|
69
|
+
* fns: {
|
|
70
|
+
* cyclesBetween: (yearA: number, yearB: number, divisorN: number) => (number | null),
|
|
71
|
+
* analyticTropicalDays: (year: number) => (number | null),
|
|
72
|
+
* meanHAtAgeMa: (tMa: number) => (number | null),
|
|
73
|
+
* meanYearRealLodDays: (tMa: number) => (number | null),
|
|
74
|
+
* eccentricityAt: (year: number) => number,
|
|
75
|
+
* },
|
|
76
|
+
* }} deps
|
|
77
|
+
*/
|
|
78
|
+
function createCardinalModel({ isDeepTime, constants, fns }) {
|
|
79
|
+
const {
|
|
80
|
+
anchors, // { SS, WS, VE, AE } — J2000 event JDs
|
|
81
|
+
harmonics, // { type: [[div, sin, cos], …] } — δ sinusoids
|
|
82
|
+
eccTerms, // { type: [{order, sin, cos}, …] } | null
|
|
83
|
+
jointTerms, // { terms: [{order, div, sin, cos}, …] } | null
|
|
84
|
+
derived, // { lincoef, h0, h1 } — fit-calibrated, VERBATIM
|
|
85
|
+
tropicalHarmonics, // TROPICAL_YEAR_HARMONICS [[div, sin, cos], …]
|
|
86
|
+
balancedYear,
|
|
87
|
+
meanSolarYearDays,
|
|
88
|
+
hJ2000,
|
|
89
|
+
eccentricityBase,
|
|
90
|
+
eccentricityAmplitude,
|
|
91
|
+
tiltMeanDeg, // for RA
|
|
92
|
+
raAngleDeg,
|
|
93
|
+
inclAmplitudeDeg,
|
|
94
|
+
} = constants;
|
|
95
|
+
const {
|
|
96
|
+
cyclesBetween, // (yearA, yearB, divisorN) => cycles | null
|
|
97
|
+
analyticTropicalDays, // (year) => SI days | null — the drift integrand base
|
|
98
|
+
meanHAtAgeMa, // (t_Ma) => H | null — the derivative's dc/dY
|
|
99
|
+
meanYearRealLodDays, // (t_Ma) => days | null — real-LOD drift convention
|
|
100
|
+
eccentricityAt, // (year) => e(t), law of cosines on the H/16 phase
|
|
101
|
+
} = fns;
|
|
102
|
+
|
|
103
|
+
/** @param {number} year */
|
|
104
|
+
const cycleOf = (year) => {
|
|
105
|
+
const c = cyclesBetween(balancedYear, year, 1);
|
|
106
|
+
return c === null ? 0 : c;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/** @param {number} year */
|
|
110
|
+
const driftIntegrand = (year) => {
|
|
111
|
+
const a = analyticTropicalDays(year);
|
|
112
|
+
return a === null ? 0 : (a - meanSolarYearDays);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/** @param {number} year */
|
|
116
|
+
function driftTerm(year) {
|
|
117
|
+
const span = year - 2000;
|
|
118
|
+
if (span === 0) return 0;
|
|
119
|
+
let n = Math.ceil(Math.abs(span) / DRIFT_NODE_SPACING_YEARS);
|
|
120
|
+
if (n % 2) n++;
|
|
121
|
+
if (n < DRIFT_SIMPSON_N_MIN) n = DRIFT_SIMPSON_N_MIN;
|
|
122
|
+
const h = span / n;
|
|
123
|
+
const f0 = driftIntegrand(2000), fN = driftIntegrand(year);
|
|
124
|
+
let acc = f0 + fN;
|
|
125
|
+
for (let i = 1; i < n; i++) acc += driftIntegrand(2000 + i * h) * ((i % 2) ? 4 : 2);
|
|
126
|
+
return acc * h / 3 - (fN - f0) / 2;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Step 6d's self-corrected tropical harmonic series, as a length deviation.
|
|
130
|
+
* @param {number} year */
|
|
131
|
+
function tropHarmonicsAt(year) {
|
|
132
|
+
const c = cycleOf(year), c0 = cycleOf(2000);
|
|
133
|
+
let s = 0;
|
|
134
|
+
for (const [div, sinC, cosC] of tropicalHarmonics) {
|
|
135
|
+
const th = 2 * Math.PI * div * c, th0 = 2 * Math.PI * div * c0;
|
|
136
|
+
s += sinC * (Math.sin(th) - Math.sin(th0)) + cosC * (Math.cos(th) - Math.cos(th0));
|
|
137
|
+
}
|
|
138
|
+
return s;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Ih(Y) = Σ_{2000→Y} of the tropical harmonics, closed form. H stays INSIDE
|
|
142
|
+
// the integral (h0 + h1·c) — it moves 27.5 yr across the window and
|
|
143
|
+
// multiplies the amplitude. Deep-time only, so `derived` is present here.
|
|
144
|
+
/** @param {number} year */
|
|
145
|
+
function integratedTropHarmonics(year) {
|
|
146
|
+
const D = /** @type {DerivedCoefs} */ (derived);
|
|
147
|
+
const cY = cycleOf(year), c0 = cycleOf(2000);
|
|
148
|
+
let tot = 0, k0 = 0;
|
|
149
|
+
for (const [div, sinC, cosC] of tropicalHarmonics) {
|
|
150
|
+
const k = 2 * Math.PI * div;
|
|
151
|
+
/** @param {number} c */
|
|
152
|
+
const F = (c) => {
|
|
153
|
+
const sn = Math.sin(k * c), cs = Math.cos(k * c), Hc = D.h0 + D.h1 * c;
|
|
154
|
+
return [-Hc * cs / k + D.h1 * sn / (k * k), Hc * sn / k + D.h1 * cs / (k * k)];
|
|
155
|
+
};
|
|
156
|
+
const a = F(c0), b = F(cY);
|
|
157
|
+
tot += sinC * (b[0] - a[0]) + cosC * (b[1] - a[1]);
|
|
158
|
+
k0 += sinC * Math.sin(k * c0) + cosC * Math.cos(k * c0);
|
|
159
|
+
}
|
|
160
|
+
return tot - k0 * (year - 2000)
|
|
161
|
+
- (tropHarmonicsAt(year) - tropHarmonicsAt(2000)) / 2;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** @param {number} year */
|
|
165
|
+
function sigmaTropical(year) {
|
|
166
|
+
const D = /** @type {DerivedCoefs} */ (derived);
|
|
167
|
+
return D.lincoef * (year - 2000) + driftTerm(year) + integratedTropHarmonics(year);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// δ_X(2000) — the self-correction that pins JD(2000) to the anchor exactly
|
|
171
|
+
// (sinusoids + ecc orders; the §10g joint terms self-correct inline).
|
|
172
|
+
// Computed lazily so the phase table builds on the caller's schedule.
|
|
173
|
+
/** @type {Record<string, number> | null} */
|
|
174
|
+
let _selfCorr = null;
|
|
175
|
+
function selfCorr() {
|
|
176
|
+
if (_selfCorr !== null) return _selfCorr;
|
|
177
|
+
_selfCorr = {};
|
|
178
|
+
const c2000 = cycleOf(2000);
|
|
179
|
+
for (const type of ['SS', 'WS', 'VE', 'AE']) {
|
|
180
|
+
let h2000 = 0;
|
|
181
|
+
for (const [div, sinC, cosC] of harmonics[type]) {
|
|
182
|
+
const th0 = 2 * Math.PI * div * c2000;
|
|
183
|
+
h2000 += sinC * Math.sin(th0) + cosC * Math.cos(th0);
|
|
184
|
+
}
|
|
185
|
+
const ecc = eccTerms && eccTerms[type];
|
|
186
|
+
if (ecc) {
|
|
187
|
+
const e0 = eccentricityAt(2000);
|
|
188
|
+
const th0 = 2 * Math.PI * 16 * c2000;
|
|
189
|
+
for (const t of ecc) {
|
|
190
|
+
const eN0 = Math.pow(e0, t.order);
|
|
191
|
+
h2000 += t.sin * eN0 * Math.sin(t.order * th0) + t.cos * eN0 * Math.cos(t.order * th0);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
_selfCorr[type] = h2000;
|
|
195
|
+
}
|
|
196
|
+
return _selfCorr;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** @param {number} year @param {string} [type] */
|
|
200
|
+
function computeSolsticeJD(year, type) {
|
|
201
|
+
const cp = type || 'SS';
|
|
202
|
+
const anchor = anchors[cp];
|
|
203
|
+
const deep = isDeepTime();
|
|
204
|
+
|
|
205
|
+
let jd = deep
|
|
206
|
+
? anchor + sigmaTropical(year)
|
|
207
|
+
: anchor + meanSolarYearDays * (year - 2000);
|
|
208
|
+
|
|
209
|
+
const cY = cycleOf(year);
|
|
210
|
+
for (const [div, sinC, cosC] of harmonics[cp]) {
|
|
211
|
+
jd += sinC * Math.sin(2 * Math.PI * div * cY) + cosC * Math.cos(2 * Math.PI * div * cY);
|
|
212
|
+
}
|
|
213
|
+
const ecc = eccTerms && eccTerms[cp];
|
|
214
|
+
if (ecc) {
|
|
215
|
+
const e = eccentricityAt(year);
|
|
216
|
+
const th = 2 * Math.PI * 16 * cY;
|
|
217
|
+
for (const t of ecc) {
|
|
218
|
+
const eN = Math.pow(e, t.order);
|
|
219
|
+
jd += t.sin * eN * Math.sin(t.order * th) + t.cos * eN * Math.cos(t.order * th);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (jointTerms) {
|
|
223
|
+
const lam = JOINT_LAMBDA[cp];
|
|
224
|
+
const c2000 = cycleOf(2000);
|
|
225
|
+
for (const t of jointTerms.terms) {
|
|
226
|
+
const th = t.order * lam - 2 * Math.PI * t.div * cY;
|
|
227
|
+
const th0 = t.order * lam - 2 * Math.PI * t.div * c2000;
|
|
228
|
+
jd += t.sin * (Math.sin(th) - Math.sin(th0)) + t.cos * (Math.cos(th) - Math.cos(th0));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
jd -= selfCorr()[cp];
|
|
232
|
+
return jd;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** The EXACT term-by-term derivative of computeSolsticeJD — except the
|
|
236
|
+
* drift part, which deliberately stays in the real-LOD convention (see
|
|
237
|
+
* the file header). Neglected: the drift Euler–Maclaurin half-sample
|
|
238
|
+
* term's own derivative (f′/2, sub-µs).
|
|
239
|
+
* @param {number} year @param {string} [type] */
|
|
240
|
+
function computeSolsticeYearLength(year, type) {
|
|
241
|
+
const cp = type || 'SS';
|
|
242
|
+
const deep = isDeepTime();
|
|
243
|
+
const t_Ma = (2000 - year) / 1e6;
|
|
244
|
+
|
|
245
|
+
const H_at = deep ? (meanHAtAgeMa(t_Ma) ?? hJ2000) : hJ2000;
|
|
246
|
+
const dcdY = 1 / H_at;
|
|
247
|
+
const cY = cycleOf(year);
|
|
248
|
+
|
|
249
|
+
let length;
|
|
250
|
+
if (deep && derived) {
|
|
251
|
+
length = derived.lincoef;
|
|
252
|
+
const mSY_at = meanYearRealLodDays(t_Ma);
|
|
253
|
+
if (mSY_at !== null) length += (mSY_at - meanSolarYearDays);
|
|
254
|
+
length += tropHarmonicsAt(year);
|
|
255
|
+
let dTrop = 0;
|
|
256
|
+
for (const [div, sinC, cosC] of tropicalHarmonics) {
|
|
257
|
+
const k = 2 * Math.PI * div, th = k * cY;
|
|
258
|
+
dTrop += k * dcdY * (sinC * Math.cos(th) - cosC * Math.sin(th));
|
|
259
|
+
}
|
|
260
|
+
length -= dTrop / 2;
|
|
261
|
+
} else {
|
|
262
|
+
length = meanSolarYearDays;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
for (const [div, sinC, cosC] of harmonics[cp]) {
|
|
266
|
+
const k = 2 * Math.PI * div, th = k * cY;
|
|
267
|
+
length += k * dcdY * (sinC * Math.cos(th) - cosC * Math.sin(th));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const ecc = eccTerms && eccTerms[cp];
|
|
271
|
+
if (ecc) {
|
|
272
|
+
const th16 = 2 * Math.PI * 16 * cY;
|
|
273
|
+
const thp = 2 * Math.PI * 16 * dcdY;
|
|
274
|
+
const e = eccentricityAt(year);
|
|
275
|
+
const de = eccentricityBase * eccentricityAmplitude * Math.sin(th16) * thp / e;
|
|
276
|
+
for (const t of ecc) {
|
|
277
|
+
const n = t.order, nth = n * th16;
|
|
278
|
+
const eN = Math.pow(e, n), eN1 = Math.pow(e, n - 1);
|
|
279
|
+
length += t.sin * (n * eN1 * de * Math.sin(nth) + eN * n * thp * Math.cos(nth))
|
|
280
|
+
+ t.cos * (n * eN1 * de * Math.cos(nth) - eN * n * thp * Math.sin(nth));
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (jointTerms) {
|
|
285
|
+
const lam = JOINT_LAMBDA[cp];
|
|
286
|
+
for (const t of jointTerms.terms) {
|
|
287
|
+
const k = 2 * Math.PI * t.div;
|
|
288
|
+
const th = t.order * lam - k * cY;
|
|
289
|
+
length += k * dcdY * (-t.sin * Math.cos(th) + t.cos * Math.sin(th));
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return length;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** RA where a cardinal point occurs — fully derived, zero fitted constants.
|
|
296
|
+
* INTEGRATED phase: the formula describes where the SCENE puts the point,
|
|
297
|
+
* and the scene's H/3 and H/8 objects rotate on integrated phase.
|
|
298
|
+
* @param {number} year @param {string} [type] */
|
|
299
|
+
function computeSolsticeRA(year, type) {
|
|
300
|
+
const sinE = Math.sin(tiltMeanDeg * Math.PI / 180);
|
|
301
|
+
const baseRA = /** @type {Record<string, number>} */ ({ SS: 90, WS: 270, VE: 0, AE: 180 })[type || 'SS'];
|
|
302
|
+
const raMean = baseRA - raAngleDeg / sinE;
|
|
303
|
+
const amp = inclAmplitudeDeg / sinE;
|
|
304
|
+
const cY = cycleOf(year);
|
|
305
|
+
const phase3 = 2 * Math.PI * 3 * cY;
|
|
306
|
+
const phase8 = 2 * Math.PI * 8 * cY;
|
|
307
|
+
return raMean + amp * (-Math.sin(phase3) + Math.sin(phase8));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Tropical year as the mean of the four cardinal intervals — the
|
|
311
|
+
* physically correct definition (Σδ_X ≡ 0 makes the braid cancel).
|
|
312
|
+
* @param {number} year */
|
|
313
|
+
function computeTropicalYearLength(year) {
|
|
314
|
+
return (computeSolsticeYearLength(year, 'SS') +
|
|
315
|
+
computeSolsticeYearLength(year, 'WS') +
|
|
316
|
+
computeSolsticeYearLength(year, 'VE') +
|
|
317
|
+
computeSolsticeYearLength(year, 'AE')) / 4;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return { computeSolsticeJD, computeSolsticeYearLength, computeSolsticeRA, computeTropicalYearLength };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
module.exports = { createCardinalModel, JOINT_LAMBDA };
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic chain-cycle integrator — THE shared implementation (Phase 8.2-4).
|
|
3
|
+
*
|
|
4
|
+
* Counts cycles of any evolving period T(t) between two calendar years:
|
|
5
|
+
* ∫ (T_yr_SI / T_period_SI)(y) dy. Serves every Moon scene-graph chain
|
|
6
|
+
* (tropical month, perigee/node precession, the beat cycles) AND the seven
|
|
7
|
+
* planet OrbitalCyclesBetween functions — it is chain-generic, keyed by the
|
|
8
|
+
* period function's identity.
|
|
9
|
+
*
|
|
10
|
+
* Extracted VERBATIM from src/script.js _moonCycleTable/_moonCycleTableAt/
|
|
11
|
+
* _moonChainCycles, which tools/lib/scene-graph.js mirrored. Three mirror
|
|
12
|
+
* gaps closed by this move (each measured against the lunar golden masters):
|
|
13
|
+
*
|
|
14
|
+
* - S5: the snapshot branch (deep-time OFF → linear at the J2000 rate),
|
|
15
|
+
* the memoized periodFn(0), and the bounded FIFO cache existed only in
|
|
16
|
+
* the browser. They are the implementation now; the Node engine gains
|
|
17
|
+
* them (its snapshot semantics thereby ALIGN with the browser's).
|
|
18
|
+
* - S12: the browser maps year → age with ageAnchorYear = startmodelYear
|
|
19
|
+
* (2000.5, the scene's t_Ma convention) while the tools mirror used a
|
|
20
|
+
* literal 2000 — a half-year skew in the integrand argument. The anchor
|
|
21
|
+
* is INJECTED; both engines pass startmodelYear (the certified browser
|
|
22
|
+
* convention).
|
|
23
|
+
* - The cumulative table stays anchored C(gridAnchorYear) = 0 at calendar
|
|
24
|
+
* 2000 (NOT 2000.5) in both engines — grid anchor and age anchor are two
|
|
25
|
+
* different constants, deliberately (cf. the phase machinery's
|
|
26
|
+
* tableAnchorYear vs driftRefYear pair).
|
|
27
|
+
*
|
|
28
|
+
* NUMERICS ARE LOAD-BEARING: fixed 10-yr grid over ±250 kyr, per-cell
|
|
29
|
+
* 3-point Simpson at build, LINEAR interpolation on read; adaptive Simpson
|
|
30
|
+
* (~1 sample/kyr, n ∈ [32, 1024], even) as the out-of-range fallback. The
|
|
31
|
+
* table replaced the per-call Simpson for in-range spans (the L-4
|
|
32
|
+
* 92 s/century full-canon regression); an "analytically better" integrator
|
|
33
|
+
* here would detach the runtime from the certified numbers.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
'use strict';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @typedef {(tMa: number) => (number | null)} PeriodSecondsAtAge
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {{
|
|
44
|
+
* ageAnchorYear: number,
|
|
45
|
+
* tropicalYearSecondsAtAge: (tMa: number) => (number | null),
|
|
46
|
+
* tropicalYearJ2000Seconds: number,
|
|
47
|
+
* isDeepTime: () => boolean,
|
|
48
|
+
* gridMinYear?: number,
|
|
49
|
+
* gridMaxYear?: number,
|
|
50
|
+
* gridStepYears?: number,
|
|
51
|
+
* gridAnchorYear?: number,
|
|
52
|
+
* maxCacheEntries?: number,
|
|
53
|
+
* }} deps — ageAnchorYear is the year whose age is 0 in the period
|
|
54
|
+
* functions' t_Ma coordinate (startmodelYear = 2000.5, the certified
|
|
55
|
+
* convention); tropicalYearJ2000Seconds feeds the snapshot branch;
|
|
56
|
+
* isDeepTime is read PER CALL so the engine's runtime toggle works.
|
|
57
|
+
*/
|
|
58
|
+
function createChainCycleIntegrator({
|
|
59
|
+
ageAnchorYear,
|
|
60
|
+
tropicalYearSecondsAtAge,
|
|
61
|
+
tropicalYearJ2000Seconds,
|
|
62
|
+
isDeepTime,
|
|
63
|
+
gridMinYear = 2000 - 250000,
|
|
64
|
+
gridMaxYear = 2000 + 250000,
|
|
65
|
+
gridStepYears = 10,
|
|
66
|
+
gridAnchorYear = 2000,
|
|
67
|
+
maxCacheEntries = 512,
|
|
68
|
+
}) {
|
|
69
|
+
/** @type {WeakMap<PeriodSecondsAtAge, Map<string, number>>} */
|
|
70
|
+
const caches = new WeakMap();
|
|
71
|
+
/** @type {WeakMap<PeriodSecondsAtAge, number>} */
|
|
72
|
+
const j2000Periods = new WeakMap(); // memoized periodFn(0)
|
|
73
|
+
/** @type {Map<PeriodSecondsAtAge, Float64Array | null>} */
|
|
74
|
+
const tables = new Map();
|
|
75
|
+
|
|
76
|
+
/** Lazy cumulative table for one chain; null when the chain is undefined
|
|
77
|
+
* anywhere in range (→ Simpson fallback).
|
|
78
|
+
* @param {PeriodSecondsAtAge} periodFnSeconds
|
|
79
|
+
* @returns {Float64Array | null} */
|
|
80
|
+
function table(periodFnSeconds) {
|
|
81
|
+
let tab = tables.get(periodFnSeconds);
|
|
82
|
+
if (tab !== undefined) return tab;
|
|
83
|
+
const N = Math.round((gridMaxYear - gridMinYear) / gridStepYears);
|
|
84
|
+
const cum = new Float64Array(N + 1);
|
|
85
|
+
const anchorIdx = Math.round((gridAnchorYear - gridMinYear) / gridStepYears);
|
|
86
|
+
/** @param {number} y @returns {number | null} */
|
|
87
|
+
const f = (y) => {
|
|
88
|
+
const tMa = (ageAnchorYear - y) / 1e6;
|
|
89
|
+
const tPeriodS = periodFnSeconds(tMa);
|
|
90
|
+
if (tPeriodS === null) return null;
|
|
91
|
+
const tYrS = tropicalYearSecondsAtAge(tMa);
|
|
92
|
+
return tYrS === null ? null : tYrS / tPeriodS;
|
|
93
|
+
};
|
|
94
|
+
let ok = true;
|
|
95
|
+
let fPrev = f(gridMinYear);
|
|
96
|
+
for (let i = 1; i <= N; i++) {
|
|
97
|
+
const fMid = f(gridMinYear + (i - 0.5) * gridStepYears);
|
|
98
|
+
const fCur = f(gridMinYear + i * gridStepYears);
|
|
99
|
+
if (fPrev === null || fMid === null || fCur === null) { ok = false; break; }
|
|
100
|
+
cum[i] = cum[i - 1] + (fPrev + 4 * fMid + fCur) * (gridStepYears / 6);
|
|
101
|
+
fPrev = fCur;
|
|
102
|
+
}
|
|
103
|
+
if (ok) {
|
|
104
|
+
const c0 = cum[anchorIdx];
|
|
105
|
+
for (let i = 0; i <= N; i++) cum[i] -= c0; // anchor C(gridAnchorYear) = 0
|
|
106
|
+
tab = cum;
|
|
107
|
+
} else {
|
|
108
|
+
tab = null;
|
|
109
|
+
}
|
|
110
|
+
tables.set(periodFnSeconds, tab);
|
|
111
|
+
return tab;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** @param {Float64Array} tab @param {number} y @returns {number} */
|
|
115
|
+
function tableAt(tab, y) {
|
|
116
|
+
const idxF = (y - gridMinYear) / gridStepYears;
|
|
117
|
+
const i = Math.floor(idxF);
|
|
118
|
+
return tab[i] + (idxF - i) * (tab[i + 1] - tab[i]);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Cycles of the chain between two calendar years. Snapshot mode: linear
|
|
122
|
+
* at the J2000 rate. Deep time: table lookup in ±range, adaptive Simpson
|
|
123
|
+
* beyond. null past the tidal-lock asymptote.
|
|
124
|
+
* @param {PeriodSecondsAtAge} periodFnSeconds
|
|
125
|
+
* @param {number} yearA @param {number} yearB
|
|
126
|
+
* @returns {number | null} */
|
|
127
|
+
function cyclesBetween(periodFnSeconds, yearA, yearB) {
|
|
128
|
+
const dy = yearB - yearA;
|
|
129
|
+
if (dy === 0) return 0;
|
|
130
|
+
|
|
131
|
+
let tJ2000 = j2000Periods.get(periodFnSeconds);
|
|
132
|
+
if (tJ2000 === undefined) {
|
|
133
|
+
const t0 = periodFnSeconds(0);
|
|
134
|
+
if (t0 === null) return null;
|
|
135
|
+
tJ2000 = t0;
|
|
136
|
+
j2000Periods.set(periodFnSeconds, tJ2000);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!isDeepTime()) {
|
|
140
|
+
return dy * tropicalYearJ2000Seconds / tJ2000;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Cumulative-table fast path (deterministic, call-order independent)
|
|
144
|
+
if (yearA > gridMinYear && yearA < gridMaxYear && yearB > gridMinYear && yearB < gridMaxYear) {
|
|
145
|
+
const tab = table(periodFnSeconds);
|
|
146
|
+
if (tab !== null) return tableAt(tab, yearB) - tableAt(tab, yearA);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let cache = caches.get(periodFnSeconds);
|
|
150
|
+
if (!cache) {
|
|
151
|
+
cache = new Map();
|
|
152
|
+
caches.set(periodFnSeconds, cache);
|
|
153
|
+
}
|
|
154
|
+
const cacheKey = yearA + '|' + yearB;
|
|
155
|
+
const hit = cache.get(cacheKey);
|
|
156
|
+
if (hit !== undefined) return hit;
|
|
157
|
+
|
|
158
|
+
// Adaptive Simpson: ~1 sample per 1 kyr, capped at 1024 to bound per-call cost.
|
|
159
|
+
let n = Math.max(32, Math.ceil(Math.abs(dy) / 1000));
|
|
160
|
+
if (n > 1024) n = 1024;
|
|
161
|
+
if (n % 2 === 1) n++;
|
|
162
|
+
const h = dy / n;
|
|
163
|
+
|
|
164
|
+
let sum = 0;
|
|
165
|
+
for (let i = 0; i <= n; i++) {
|
|
166
|
+
const y = yearA + i * h;
|
|
167
|
+
const tMa = (ageAnchorYear - y) / 1e6;
|
|
168
|
+
const tPeriodS = periodFnSeconds(tMa);
|
|
169
|
+
if (tPeriodS === null) return null;
|
|
170
|
+
const tYrS = tropicalYearSecondsAtAge(tMa);
|
|
171
|
+
if (tYrS === null) return null;
|
|
172
|
+
const integrand = tYrS / tPeriodS; // cycles per SI year
|
|
173
|
+
const w = (i === 0 || i === n) ? 1 : (i % 2 === 1 ? 4 : 2);
|
|
174
|
+
sum += w * integrand;
|
|
175
|
+
}
|
|
176
|
+
const result = (sum * h) / 3;
|
|
177
|
+
|
|
178
|
+
if (cache.size >= maxCacheEntries) {
|
|
179
|
+
const firstKey = cache.keys().next().value;
|
|
180
|
+
cache.delete(/** @type {string} */ (firstKey));
|
|
181
|
+
}
|
|
182
|
+
cache.set(cacheKey, result);
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return { cyclesBetween };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
module.exports = { createChainCycleIntegrator };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Climate Formula L1 orbital layer — THE shared evaluator (Phase 8.4,
|
|
3
|
+
* slice 4). The δ¹⁸O contribution from the L1 (orbital/Milankovitch)
|
|
4
|
+
* harmonic layer only, in ‰ — excludes the intercept, L2 (405-kyr
|
|
5
|
+
* carbon), L3 (regime steps), y_mean, and trend_slope: consumers want
|
|
6
|
+
* the orbital FLUCTUATION around J2000, not the secular baseline.
|
|
7
|
+
*
|
|
8
|
+
* Primary consumer: the GIA α(t) chain — one physical mechanism, two
|
|
9
|
+
* observables: the same L1 signal that fits δ¹⁸O also drives α via
|
|
10
|
+
* Milankovitch forcing → ice sheets → GIA J₂/α → LOD (doc 99
|
|
11
|
+
* §prediction-7). Both engines hand-mirrored this loop; it lives once
|
|
12
|
+
* now.
|
|
13
|
+
*
|
|
14
|
+
* ENGINE-SIDE, by design: the regime selection (CLIMATE_FORMULA_COEFFS
|
|
15
|
+
* lookup, the ALPHA_CLIMATE_REGIME_KEY choice), the α formula itself
|
|
16
|
+
* (one subtraction around engine state), and the lattice-α pin machinery
|
|
17
|
+
* (_withLatticeAlpha — mutable engine state with try/finally semantics
|
|
18
|
+
* and, in the browser, a TDZ-history guard).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {Object} ClimateL1Regime
|
|
25
|
+
* @property {Array<{n: number, a: number, b: number}>} l1Terms - 8H-lattice
|
|
26
|
+
* harmonics (n = cycles per 8H)
|
|
27
|
+
* @property {number} yStdDenormalization - the fit's y_std scale-back
|
|
28
|
+
* @property {number} eightHKyr - 8H in kyr (the fit's period base)
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {number} year - calendar year
|
|
33
|
+
* @param {ClimateL1Regime} regime
|
|
34
|
+
* @returns {number} L1 orbital δ¹⁸O contribution, ‰
|
|
35
|
+
*/
|
|
36
|
+
function evalClimateL1OrbitalPermil(year, regime) {
|
|
37
|
+
const t_kyr_BP = (2000 - year) / 1000;
|
|
38
|
+
let L1_sum = 0;
|
|
39
|
+
for (const c of regime.l1Terms) {
|
|
40
|
+
const omega = 2 * Math.PI * c.n / regime.eightHKyr;
|
|
41
|
+
L1_sum += c.a * Math.cos(omega * t_kyr_BP) + c.b * Math.sin(omega * t_kyr_BP);
|
|
42
|
+
}
|
|
43
|
+
return L1_sum * regime.yStdDenormalization;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { evalClimateL1OrbitalPermil };
|