import { describe, expect, it } from 'vitest'; import { parsePlanetMeanElements, parseSatelliteMeanElements, parseSmallBodyElements, SbdbAnswer } from './mean-elements'; /** Standish's p_elem_t2.txt, cut to the lines that matter here, as JPL published them. */ const TABLE_2 = `Keplerian elements and their rates, with respect to the mean ecliptic and equinox of J2000, valid for the time-interval 3000 BC -- 3000 AD. NOTE: the computation of M for Jupiter through Pluto *must* be augmented by the additional terms given in Table 2b (below). EM Bary 1.00000018 0.01673163 -0.00054346 100.46691572 102.93005885 -5.11260389 -0.00000003 -0.00003661 -0.01337178 35999.37306329 0.31795260 -0.24123856 Jupiter 5.20248019 0.04853590 1.29861416 34.33479152 14.27495244 100.29282654 -0.00002864 0.00018026 -0.00322699 3034.90371757 0.18199196 0.13024619 Pluto 39.48686035 0.24885238 17.14104260 238.96535011 224.09702598 110.30167986 0.00449751 0.00006016 0.00000501 145.18042903 -0.00968827 -0.00809981 Table 2b. Jupiter -0.00012452 0.06064060 -0.35635438 38.35125000 Pluto -0.01262724 `; /** The satellite page's markup around three rows, as the 2021 page served it. */ const SATELLITES = ` Satellites of Earth jump to: Earth, Mars

Mean ecliptic orbital elements

Epoch 2000 Jan. 1.50 TT
Moon 384400.0.0554318.15135.275.16125.08 13.17635827.3225.99718.600 1 Satellites of Jupiter jump to: Earth, Mars

Mean orbital elements referred to the local Laplace planes

Epoch 1997 Jan. 16.00 TT
Io421800.0.0041 84.129342.0210.03643.977203.4889583 1.7691.6257.420268.05764.495 0.000 11 Satellites of Neptune jump to: Earth, Mars

Mean orbital elements referred to the local Laplace planes

Epoch 2000 Jan. 1.50 TT
Triton354759.0.0000 66.142352.257156.865177.608 61.25726385.877386.371687.446 299.45643.4140.010 54 Satellites of Uranus jump to: Earth, Mars

Mean equatorial orbital elements

Epoch 1980 Jan. 1.0 TT
Titania436300.0.0011 284.40024.6140.07999.77141.3514246 8.706161.525195.369 10 `; /** Ceres as the SBDB API answers `sstr=Ceres&phys-par=1&full-prec=1`, cut to what is read. */ const CERES: SbdbAnswer = { orbit: { epoch: '2461200.5', elements: [ { name: 'e', value: '.07969229514816586' }, { name: 'a', value: '2.765552595034094' }, { name: 'q', value: '2.545159361382861' }, { name: 'i', value: '10.58802780183462' }, { name: 'om', value: '80.24862682043221' }, { name: 'w', value: '73.29421453021587' }, { name: 'ma', value: '274.4193463761342' }, { name: 'tp', value: '2461599.841466614066' }, { name: 'per', value: '1679.853119758983' }, { name: 'n', value: '.21430445064843' }, { name: 'ad', value: '2.985945828685327' } ] }, phys_par: [ { name: 'H', value: '3.34' }, { name: 'diameter', value: '939.4' }, { name: 'GM', value: '62.6284' }, { name: 'rot_per', value: '9.074170' } ] }; describe('parsePlanetMeanElements', () => { it('turns Standish’s longitudes into the argument of periapsis and mean anomaly', () => { const { orbit } = parsePlanetMeanElements(TABLE_2, 'jupiter'); expect(orbit.argumentOfPeriapsisDeg).toBeCloseTo(14.27495244 - 100.29282654, 8); expect(orbit.meanAnomalyAtEpochDeg).toBeCloseTo(34.33479152 - 14.27495244, 8); expect(orbit.epochJd).toBe(2451545); }); it('gives the rates per day, the mean motion being the mean longitude’s', () => { const { rates } = parsePlanetMeanElements(TABLE_2, 'earth'); // 35 999.373 degrees a century is the sidereal year. expect(360 / rates.meanMotionDegPerDay).toBeCloseTo(365.2564, 4); expect(rates.argumentOfPeriapsisDegPerDay * 36525).toBeCloseTo(0.3179526 + 0.24123856, 8); // And every other rate the row gives, a century's worth: dropped, Saturn moved 0.66 degrees by // AD 1 without its node's and 0.36 by AD 3000 without its a, e and i, where no date from 1950 to // 2100 shows more than 0.036. expect(rates.longitudeOfAscendingNodeDegPerDay * 36525).toBeCloseTo(-0.24123856, 8); expect(rates.semiMajorAxisAuPerDay! * 36525).toBeCloseTo(-0.00000003, 8); expect(rates.eccentricityPerDay! * 36525).toBeCloseTo(-0.00003661, 8); expect(rates.inclinationDegPerDay! * 36525).toBeCloseTo(-0.01337178, 8); }); it('carries Table 2b’s terms for Jupiter and beyond, and none for the inner planets', () => { expect(parsePlanetMeanElements(TABLE_2, 'jupiter').rates.meanAnomalyTerms).toEqual({ b: -0.00012452, c: 0.0606406, s: -0.35635438, f: 38.35125 }); expect(parsePlanetMeanElements(TABLE_2, 'earth').rates.meanAnomalyTerms).toBeUndefined(); }); it('reads Pluto’s row, not the note above the table that starts a line with its name', () => { const pluto = parsePlanetMeanElements(TABLE_2, 'pluto'); expect(pluto.orbit.semiMajorAxisAu).toBe(39.48686035); expect(pluto.rates.meanAnomalyTerms).toEqual({ b: -0.01262724, c: 0, s: 0, f: 0 }); }); }); describe('parseSatelliteMeanElements', () => { it('reads a Laplace-plane row with its pole and its section’s epoch', () => { const io = parseSatelliteMeanElements(SATELLITES, 'Jupiter', 'Io', true); expect(io.laplacePole).toEqual({ raDeg: 268.057, decDeg: 64.495 }); expect(io.orbit.epochJd).toBe(2450464.5); expect(io.rates.meanMotionDegPerDay).toBe(203.4889583); expect(io.orbitSource).toBe('JPL SSD satellite mean elements, epoch 1997 Jan 16'); }); it('reads the Moon against the ecliptic, with no pole', () => { const moon = parseSatelliteMeanElements(SATELLITES, 'Earth', 'Moon', false); expect(moon.laplacePole).toBeUndefined(); expect(moon.orbit.epochJd).toBe(2451545); expect(moon.orbit.semiMajorAxisAu * 149597870.7).toBeCloseTo(384400, 3); }); it('regresses a prograde node and advances a periapsis, as the planet’s oblateness turns them', () => { const { rates } = parseSatelliteMeanElements(SATELLITES, 'Earth', 'Moon', false); expect(rates.longitudeOfAscendingNodeDegPerDay).toBeCloseTo(-360 / (18.6 * 365.25), 9); expect(rates.argumentOfPeriapsisDegPerDay).toBeCloseTo(360 / (5.997 * 365.25), 9); }); it('advances the node of a retrograde orbit', () => { const { rates } = parseSatelliteMeanElements(SATELLITES, 'Neptune', 'Triton', false); expect(rates.longitudeOfAscendingNodeDegPerDay).toBeCloseTo(360 / (687.446 * 365.25), 9); }); it('reads a section referred to the planet’s equator against the pole it is given', () => { const pole = { raDeg: 77.311, decDeg: 15.175 }; const titania = parseSatelliteMeanElements(SATELLITES, 'Uranus', 'Titania', false, pole); expect(titania.laplacePole).toEqual(pole); expect(titania.orbit.epochJd).toBe(2444239.5); expect(titania.rates.meanMotionDegPerDay).toBe(41.3514246); // Read as ecliptic elements, which is what a missing pole would mean, Titania is 88 degrees // from Horizons on 2025-01-01. expect(() => parseSatelliteMeanElements(SATELLITES, 'Uranus', 'Titania', false)).toThrow(/equator/); expect(() => parseSatelliteMeanElements(SATELLITES, 'Jupiter', 'Io', true, pole)).toThrow(/equator/); }); it('turns the periapsis backwards where a resonance holds it', () => { const { rates } = parseSatelliteMeanElements(SATELLITES, 'Jupiter', 'Io', true); expect(rates.argumentOfPeriapsisDegPerDay).toBeCloseTo(-360 / (1.625 * 365.25), 9); }); }); describe('parseSmallBodyElements', () => { it('carries a dwarf planet on its osculating elements at their own mean motion', () => { const ceres = parseSmallBodyElements(CERES); expect(ceres.orbit).toEqual({ semiMajorAxisAu: 2.765552595034094, eccentricity: 0.07969229514816586, inclinationDeg: 10.58802780183462, longitudeOfAscendingNodeDeg: 80.24862682043221, argumentOfPeriapsisDeg: 73.29421453021587, meanAnomalyAtEpochDeg: 274.4193463761342, epochJd: 2461200.5 }); expect(ceres.rates).toEqual({ meanMotionDegPerDay: 0.21430445064843, longitudeOfAscendingNodeDegPerDay: 0, argumentOfPeriapsisDegPerDay: 0 }); expect(ceres.laplacePole).toBeUndefined(); expect(ceres.orbitSource).toBe('JPL SBDB osculating elements, epoch 2026 Jun 9'); }); it('takes half the diameter as the radius, and the rotation period in hours', () => { const ceres = parseSmallBodyElements(CERES); expect(ceres.radiusKm).toBe(469.7); expect(ceres.rotationPeriodHours).toBe(9.07417); }); it('leaves out what the answer does not publish', () => { const eris = parseSmallBodyElements({ ...CERES, phys_par: [{ name: 'rot_per', value: '25.9' }] }); expect(eris.radiusKm).toBeUndefined(); expect(eris.rotationPeriodHours).toBe(25.9); }); });