Skip to content

Commit a0a2fad

Browse files
committed
v4: pin JPL Horizons topocentric reference for the Moon
Owner-supplied Horizons capture (DE441; Tokyo site, airless apparent RA/Dec of date, azimuth/elevation and range, 2026-07-18 00:00..24:00 UTC, 6h steps) pinned as a reference test for the observer pipeline. Measured residuals: RA -8 arcsec (constant), Dec ~+1 arcsec, Az/El within 8 arcsec, range -30 km — the documented accuracy class of the Meeus 60-term lunar series against a full numerical ephemeris. The test additionally asserts that the equatorial and horizontal residuals are the same sky-plane error, i.e. the topocentric geometry (parallax, frames, sidereal time) contributes nothing beyond the underlying series. Mars and Sun blocks will be appended when their captures arrive. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0169Ya4WJEvmot5RKG4nFP5N
1 parent f4ca6fb commit a0a2fad

2 files changed

Lines changed: 78 additions & 1 deletion

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"MIT-LICENSE"
1313
],
1414
"scripts": {
15-
"test": "node test/smoke.js && node test/v4/run.mjs && node test/v4/bodies.mjs && node test/v4/observer.mjs",
15+
"test": "node test/smoke.js && node test/v4/run.mjs && node test/v4/bodies.mjs && node test/v4/observer.mjs && node test/v4/horizons-ref.mjs",
1616
"build": "rollup -c rollup.config.js",
1717
"edu:snippets": "node tools/snippets.js",
1818
"edu:check": "node tools/snippets.js --check",

test/v4/horizons-ref.mjs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// JPL Horizons reference comparison. Run with: node test/v4/horizons-ref.mjs
2+
//
3+
// Reference data supplied by the project owner from the Horizons API
4+
// (source DE441), observer site Tokyo E139.741 N35.658 h=25m,
5+
// airless apparent quantities 2 (RA/Dec of date, topocentric),
6+
// 4 (azimuth/elevation) and 20 (range), 2026-07-18 00:00..24:00 UTC.
7+
//
8+
// Tolerances state each body's SERIES accuracy class, not the pipeline's:
9+
// the residual must look like the documented truncation error of the
10+
// underlying theory (a smooth offset shared by the equatorial and
11+
// horizontal coordinates), and anything beyond it fails the build.
12+
// moon: Meeus 60-term series, ~10" in longitude, tens of km in range.
13+
// Measured on capture: RA -8", Dec +1", Az/El within 8",
14+
// range -30 km — consistent with the series class.
15+
import assert from 'assert';
16+
17+
import { Instant } from '../../src/time/instant.js';
18+
import { DEG } from '../../src/math/angles.js';
19+
import { observer } from '../../src/observer/observer.js';
20+
import { moon } from '../../src/bodies/moon.js';
21+
22+
const AU_KM = 149597870.7;
23+
24+
let failed = 0;
25+
const test = (name, fn) => {
26+
try {
27+
fn();
28+
console.log('ok - ' + name);
29+
} catch (e) {
30+
failed++;
31+
console.error('NG - ' + name + ': ' + e.message);
32+
}
33+
};
34+
35+
const TOKYO = observer({ latitude: 35.658, longitude: 139.741, height: 25 });
36+
37+
// rows: [iso, RA deg, Dec deg, Az deg, El deg, delta au]
38+
const CASES = [
39+
{
40+
body: moon, name: 'moon',
41+
skyTolArcsec: 15, rangeTolKm: 50,
42+
rows: [
43+
['2026-07-18T00:00:00Z', 165.423160268, 4.665362159, 86.287282626, 2.830527623, 0.00251170278392],
44+
['2026-07-18T06:00:00Z', 167.688729940, 3.118631144, 176.512941294, 57.414144138, 0.00248826202850],
45+
['2026-07-18T12:00:00Z', 169.876940753, 1.492171722, 268.984296789, 3.974817572, 0.00253126073634],
46+
['2026-07-18T18:00:00Z', 173.527125098, -0.085041911, 347.705201508, -53.796385069, 0.00257917480388],
47+
['2026-07-19T00:00:00Z', 177.328839749, -1.646624904, 85.015043564, -9.710551650, 0.00256196526041]
48+
]
49+
}
50+
// mars / sun blocks are appended as their Horizons captures arrive.
51+
];
52+
53+
for (const c of CASES) {
54+
test(`horizons: ${c.name} topocentric apparent place (Tokyo)`, () => {
55+
for (const [iso, ra, dec, az, el, delta] of c.rows) {
56+
const o = TOKYO.observe(c.body, Instant.fromISO(iso));
57+
const dRa = (o.ra - ra) * Math.cos(dec * DEG) * 3600;
58+
const dDec = (o.dec - dec) * 3600;
59+
const dAz = (o.azimuth - az) * Math.cos(el * DEG) * 3600;
60+
const dEl = (o.elevation - el) * 3600;
61+
const skyEq = Math.hypot(dRa, dDec);
62+
const skyHor = Math.hypot(dAz, dEl);
63+
assert.ok(skyEq < c.skyTolArcsec, `${iso} radec off ${skyEq.toFixed(1)}"`);
64+
assert.ok(skyHor < c.skyTolArcsec, `${iso} azel off ${skyHor.toFixed(1)}"`);
65+
// the equatorial and horizontal offsets must be the SAME sky error,
66+
// i.e. the topocentric geometry itself adds nothing
67+
assert.ok(Math.abs(skyEq - skyHor) < 3, `${iso} geometry residual`);
68+
assert.ok(Math.abs(o.range - delta * AU_KM) < c.rangeTolKm, `${iso} range`);
69+
}
70+
});
71+
}
72+
73+
if (failed > 0) {
74+
console.error(failed + ' test(s) failed');
75+
process.exit(1);
76+
}
77+
console.log('all horizons reference tests passed');

0 commit comments

Comments
 (0)