|
| 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