From 69cada7914de4b2670787aaad6267b1ea7b2d33d Mon Sep 17 00:00:00 2001 From: Senrokai Date: Wed, 30 Sep 2026 19:24:49 +0200 Subject: [PATCH] Fail the ETL when re-rating a locked moon moves its pole or W off the kernel's at the present lockedToOrbit re-rates a locked moon's W and node terms and moves their constants so that on 2025-01-01, the date the IAU's elements were fitted near, the pole and W are the kernel's own. Nothing checked it: with the constants left where they were, the solar ETL passed and the suite passed, though every re-rated moon moved (Rhea's pole 0.018 degrees, Triton 0.0054, Miranda 0.0037, Europa 0.0027, Callisto 0.0021, Deimos 0.0016, Ganymede 0.0016, measured on the bodies.json it wrote; before the drawn node rates were corrected, Mimas's W moved 0.21 and Miranda's pole 0.12). lockedToOrbit now compares orientationAt at PRESENT_JD before and after, Iapetus's pole round its orbit included, and throws past 1e-6 degrees. Measured on the shipped catalogue: at most 4.7e-10 (Deimos's W, some 2.6 million degrees round), the pole exactly. Guarded mutants, each through the solar ETL on the real catalogue: - node terms' constants not moved: "Deimos's pole or W on 2460676.5 is 1.60e-3 degrees from the IAU's". - W's constant not moved: Deimos, 9.12e-2. - the check disabled with the first: the ETL passes (control). The unit suite does not see it: the check runs on the kernel, which only the ETL reads. The ETL writes the same bodies.json as before. Unit suite 866 passed. Co-Authored-By: Claude Opus 5.5 (1M context) --- tools/etl/lib/locked-spin.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tools/etl/lib/locked-spin.ts b/tools/etl/lib/locked-spin.ts index ed61980..9c50a15 100644 --- a/tools/etl/lib/locked-spin.ts +++ b/tools/etl/lib/locked-spin.ts @@ -31,6 +31,9 @@ const POLE_HARMONICS = 5; const MAX_NODE_RATE_OFFSET = 0.05; const MAX_NODE_HARMONIC = 9; +/** How far, in degrees, re-rating may move a locked moon's pole or W at {@link PRESENT_JD}; see {@link lockedToOrbit}. */ +const MAX_PRESENT_OFFSET_DEG = 1e-6; + /** The planet's east longitude on a moon's IAU body-fixed frame, from the moon's mean place, at a TDB date. */ export function subPlanetLongitudeDeg(body: Pick, elements: RotationalElements, jd: number): number { const own = positionAtEpoch(meanElementsAt(body.orbit, body.rates, jd)); @@ -106,7 +109,15 @@ export function lockedToOrbit(elements: RotationalElements, mean: Pick orientationAt(each, PRESENT_JD)); + const moved = Math.max(...(['poleRaDeg', 'poleDecDeg', 'primeMeridianDeg'] as const).map((key) => Math.abs(own[key] - iau[key]))); + if (moved > MAX_PRESENT_OFFSET_DEG) { + throw new Error(`${name}'s pole or W on ${PRESENT_JD} is ${moved.toExponential(2)} degrees from the IAU's: re-rated, it should be where the kernel has it today.`); + } + return turned; } function poleRoundOrbit(elements: RotationalElements, mean: Pick): RotationalElements {