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) <noreply@anthropic.com>
This commit is contained in:
2026-09-30 19:24:49 +02:00
co-authored by Claude Opus 5.5
parent 24c7be1bae
commit 69cada7914
+12 -1
View File
@@ -31,6 +31,9 @@ const POLE_HARMONICS = 5;
const MAX_NODE_RATE_OFFSET = 0.05; const MAX_NODE_RATE_OFFSET = 0.05;
const MAX_NODE_HARMONIC = 9; 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. */ /** 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<BodyRecord, 'orbit' | 'rates' | 'laplacePole'>, elements: RotationalElements, jd: number): number { export function subPlanetLongitudeDeg(body: Pick<BodyRecord, 'orbit' | 'rates' | 'laplacePole'>, elements: RotationalElements, jd: number): number {
const own = positionAtEpoch(meanElementsAt(body.orbit, body.rates, jd)); const own = positionAtEpoch(meanElementsAt(body.orbit, body.rates, jd));
@@ -106,7 +109,15 @@ export function lockedToOrbit(elements: RotationalElements, mean: Pick<MeanOrbit
return { ...term, angleDeg: [constant + (rate - k * nodeRate) * present, k * nodeRate] }; return { ...term, angleDeg: [constant + (rate - k * nodeRate) * present, k * nodeRate] };
}); });
const locked: RotationalElements = { ...elements, primeMeridianDeg: [w0 + (w1 - n) * (PRESENT_JD - J2000_JD), n, 0], ...(terms ? { terms } : {}) }; const locked: RotationalElements = { ...elements, primeMeridianDeg: [w0 + (w1 - n) * (PRESENT_JD - J2000_JD), n, 0], ...(terms ? { terms } : {}) };
return poleFollowsOrbit ? poleRoundOrbit(locked, mean) : locked; const turned = poleFollowsOrbit ? poleRoundOrbit(locked, mean) : locked;
// Whatever is re-rated, the pole and W at the present are the kernel's, which is what they were
// fitted to. Measured: at most 4.7e-10 degrees (Deimos's W, some 2.6 million degrees round).
const [iau, own] = [elements, turned].map((each) => 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<MeanOrbit, 'orbit' | 'rates' | 'laplacePole'>): RotationalElements { function poleRoundOrbit(elements: RotationalElements, mean: Pick<MeanOrbit, 'orbit' | 'rates' | 'laplacePole'>): RotationalElements {