Measure every moon and dwarf planet against Horizons from 1950 to 2100, and say on its card how far it strays

The clock reaches AD 1 to AD 3000, but only the planets' cards named a span their elements hold
over; the 25 moons and the four SBDB dwarf planets gave a source and an epoch, though
BodyRecord.orbitSource is documented as "the span they hold over". And the worst offsets the ETL
stated came from twelve New Year's Days: Nereid's year is 360 days, so all twelve fell far from
its periapsis, where a mean ellipse is furthest out. The one date the ETL checked, 2025-01-01, saw
Nereid at 2.6 degrees; it reaches 11.19.

For each moon and each SBDB dwarf planet the ETL now fetches Horizons' ICRF vectors from 1950 to
2100, every other day (daily for Nereid, at an eccentricity of 0.75, and Hyperion, whose row's
eccentricity is a quarter of its real one: every other day gave it 22.14, daily 22.23), and
measures how far the mean elements stray, at the same TDB dates. The card appends it: "JPL SBDB
osculating elements, epoch 2026 Jun 9, within 7.2 degrees of Horizons from 1950 to 2100". Worst
offsets on the real catalogue: the Moon 2.62 (2010 March 27), Phoebe 2.58 (1969, where a comment
claimed "within 2.0"), Phobos 1.26, Mimas 7.43, Iapetus 10.34, Nereid 11.19 (2039 Nov 1),
Hyperion 22.23 (2055 Feb 26), Ceres 7.12 (1953); Io 0.07, Titan 0.06, Eris 0.06.

build.ts recomputes each from the same Horizons positions and fails if an orbit other than
Standish's names no span, if a card states less than it strays, or if a body passes its ceiling:
3 degrees, and Hyperion 23, Nereid 12, Iapetus 11, Mimas 8 and Ceres 8, each explained. The
2025-01-01 check stays for reading errors, its comment no longer passing one date's offsets off as
worst ones. The Sun's note says the moons' and those four's elements were checked from 1950 to
2100, and the date field's description that each card says how far its orbit strays over that span.
In the running app Ceres's, Phobos's and Nereid's cards end "within 7.2", "1.3" and "11.2 degrees
of Horizons from 1950 to 2100".

The CLOCK_WINDOW comment also had the calendars the wrong way at AD 1: proleptic Gregorian dates
are two days behind the Julian calendar there, level from AD 200 to 300, and ten days ahead by
1582. It now says so, and names Ceres's drift where it named Phobos's, which its orbit now carries.

Controls: the ETL measuring nothing fails ("Ceres's orbit ... names no span it holds over"),
rounding the stated figure down fails on Ceres (7.1 against 7.12), and Nereid held to the general
ceiling fails at 11.19; the note and the date field without the span fail their named tests.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-29 19:57:16 +02:00
co-authored by Claude Opus 5.5
parent 2d4b8a98d5
commit a281f22f34
9 changed files with 167 additions and 67 deletions
@@ -837,7 +837,7 @@ describe('GalaxySystemSceneComponent camera-flight transitions', () => {
navigationStore.selectStar(SUN.id); navigationStore.selectStar(SUN.id);
await flushAsync(); await flushAsync();
await advanceFrames(engine, 2.5); await advanceFrames(engine, 2.5);
expect(note()).toMatch(/^Orbits propagated from JPL mean elements, the planets’ fit for 3000 BC to AD 3000, and the SBDB’s osculating ones for Ceres, Eris, Haumea and Makemake, to now, \d{4}-\d\d-\d\d \d\d:\d\d UTC\.$/); expect(note()).toMatch(/^Orbits propagated from JPL mean elements, the planets’ fit for 3000 BC to AD 3000 and the moons’ checked from 1950 to 2100, and the SBDB’s osculating ones for Ceres, Eris, Haumea and Makemake, checked over the same span, to now, \d{4}-\d\d-\d\d \d\d:\d\d UTC\.$/);
navigationStore.selectStar(ALPHA_CENTAURI.id); navigationStore.selectStar(ALPHA_CENTAURI.id);
await flushAsync(); await flushAsync();
@@ -1768,8 +1768,9 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
: []), : []),
]); ]);
// Where the orbits come from, and for the Sun how far from the present they hold: each // Where the orbits come from, and for the Sun how far from the present they hold: each
// body's card names its own source and epoch. // body's card names its own source, and for a moon or dwarf planet how far it strays from
const source = this.bodies.some((body) => body.systemStarId === star.id) ? 'JPL mean elements, the planets’ fit for 3000 BC to AD 3000, and the SBDB’s osculating ones for Ceres, Eris, Haumea and Makemake,' : 'published elements'; // Horizons over the span it was checked.
const source = this.bodies.some((body) => body.systemStarId === star.id) ? 'JPL mean elements, the planets’ fit for 3000 BC to AD 3000 and the moons’ checked from 1950 to 2100, and the SBDB’s osculating ones for Ceres, Eris, Haumea and Makemake, checked over the same span,' : 'published elements';
// Named to the minute, in UTC like the date field: a jump to 18:00 on a given day is a // Named to the minute, in UTC like the date field: a jump to 18:00 on a given day is a
// question about that hour, and the note is where the answer says which sky it is. // question about that hour, and the note is where the answer says which sky it is.
const drawnFor = `${this.time.date().toISOString().slice(0, 16).replace('T', ' ')} UTC`; const drawnFor = `${this.time.date().toISOString().slice(0, 16).replace('T', ' ')} UTC`;
@@ -416,6 +416,7 @@ describe('HudDockComponent', () => {
expect(field.min).toBe('0001-01-01T00:00'); expect(field.min).toBe('0001-01-01T00:00');
expect(field.max).toBe('3000-01-01T00:00'); expect(field.max).toBe('3000-01-01T00:00');
expect(host().querySelector(`#${field.getAttribute('aria-describedby')}`)?.textContent).toContain('AD 1 to AD 3000'); expect(host().querySelector(`#${field.getAttribute('aria-describedby')}`)?.textContent).toContain('AD 1 to AD 3000');
expect(host().querySelector(`#${field.getAttribute('aria-describedby')}`)?.textContent).toContain('how far its orbit strays from 1950 to 2100');
}); });
it('jumps the clock to the date submitted, read as UTC', () => { it('jumps the clock to the date submitted, read as UTC', () => {
+1 -1
View File
@@ -377,7 +377,7 @@ function isWideViewport(): boolean {
Go Go
</button> </button>
<p id="clock-date-window" class="w-full text-[10px] text-muted"> <p id="clock-date-window" class="w-full text-[10px] text-muted">
AD 1 to AD 3000, where the planets’ elements hold. AD 1 to AD 3000, where the planets’ elements hold. Each moon’s and dwarf planet’s card says how far its orbit strays from 1950 to 2100.
</p> </p>
</form> </form>
</section> </section>
+5 -3
View File
@@ -31,9 +31,11 @@ const JULIAN_DATE_AT_EPOCH = 2440587.5;
* The end is where Standish's Table 2, the mean elements that carry the planets, stops being * The end is where Standish's Table 2, the mean elements that carry the planets, stops being
* fitted: it covers 3000 BC to AD 3000, and every planet was within 0.29 degrees of Horizons at * fitted: it covers 3000 BC to AD 3000, and every planet was within 0.29 degrees of Horizons at
* each date measured out to 3000. The start is not the fit's but the date input's, which cannot * each date measured out to 3000. The start is not the fit's but the date input's, which cannot
* go before 0001-01-01. Both are proleptic Gregorian, as a `Date` is, so before 1582 they run * go before 0001-01-01. Both are proleptic Gregorian, as a `Date` is, so before 1582 they part from
* ahead of the Julian-calendar dates history gives: two days at AD 1, ten by 1582. The moons and * the Julian-calendar dates history gives: two days behind them at AD 1, level from AD 200 to 300,
* dwarf planets hold for far less of it: Phobos is 11 degrees out by 2100, Ceres 11.6 by 2200. * ten days ahead by 1582. The moons and dwarf planets hold for far less of it: each card says how
* far its orbit strays from Horizons from 1950 to 2100 (Ceres 7.1 degrees there, 11.6 by 2200 and
* 39 by 1600).
*/ */
export const CLOCK_WINDOW = { min: '0001-01-01T00:00', max: '3000-01-01T00:00' } as const; export const CLOCK_WINDOW = { min: '0001-01-01T00:00', max: '3000-01-01T00:00' } as const;
const WINDOW_MS = { const WINDOW_MS = {
+29 -29
View File
@@ -499,7 +499,7 @@
"longitudeOfAscendingNodeDegPerDay": 0, "longitudeOfAscendingNodeDegPerDay": 0,
"argumentOfPeriapsisDegPerDay": 0 "argumentOfPeriapsisDegPerDay": 0
}, },
"orbitSource": "JPL SBDB osculating elements, epoch 2026 Jun 9", "orbitSource": "JPL SBDB osculating elements, epoch 2026 Jun 9, within 7.2 degrees of Horizons from 1950 to 2100",
"rotationPeriodHours": 9.07417, "rotationPeriodHours": 9.07417,
"rotationalElements": { "rotationalElements": {
"poleRaDeg": [ "poleRaDeg": [
@@ -539,7 +539,7 @@
"longitudeOfAscendingNodeDegPerDay": 0, "longitudeOfAscendingNodeDegPerDay": 0,
"argumentOfPeriapsisDegPerDay": 0 "argumentOfPeriapsisDegPerDay": 0
}, },
"orbitSource": "JPL SBDB osculating elements, epoch 2026 Jun 9", "orbitSource": "JPL SBDB osculating elements, epoch 2026 Jun 9, within 0.1 degrees of Horizons from 1950 to 2100",
"rotationPeriodHours": 378.504 "rotationPeriodHours": 378.504
}, },
{ {
@@ -562,7 +562,7 @@
"longitudeOfAscendingNodeDegPerDay": 0, "longitudeOfAscendingNodeDegPerDay": 0,
"argumentOfPeriapsisDegPerDay": 0 "argumentOfPeriapsisDegPerDay": 0
}, },
"orbitSource": "JPL SBDB osculating elements, epoch 2026 Jun 9", "orbitSource": "JPL SBDB osculating elements, epoch 2026 Jun 9, within 0.4 degrees of Horizons from 1950 to 2100",
"rotationPeriodHours": 3.9154 "rotationPeriodHours": 3.9154
}, },
{ {
@@ -585,7 +585,7 @@
"longitudeOfAscendingNodeDegPerDay": 0, "longitudeOfAscendingNodeDegPerDay": 0,
"argumentOfPeriapsisDegPerDay": 0 "argumentOfPeriapsisDegPerDay": 0
}, },
"orbitSource": "JPL SBDB osculating elements, epoch 2026 Jun 9", "orbitSource": "JPL SBDB osculating elements, epoch 2026 Jun 9, within 0.3 degrees of Horizons from 1950 to 2100",
"rotationPeriodHours": 22.8266 "rotationPeriodHours": 22.8266
}, },
{ {
@@ -608,7 +608,7 @@
"longitudeOfAscendingNodeDegPerDay": -0.052990660396105185, "longitudeOfAscendingNodeDegPerDay": -0.052990660396105185,
"argumentOfPeriapsisDegPerDay": 0.164353223839846 "argumentOfPeriapsisDegPerDay": 0.164353223839846
}, },
"orbitSource": "JPL SSD satellite mean elements, epoch 2000 Jan 1", "orbitSource": "JPL SSD satellite mean elements, epoch 2000 Jan 1, within 2.7 degrees of Horizons from 1950 to 2100",
"parentBodyId": "earth", "parentBodyId": "earth",
"rotationPeriodHours": 655.7198886065481, "rotationPeriodHours": 655.7198886065481,
"obliquityDeg": 6.67, "obliquityDeg": 6.67,
@@ -707,7 +707,7 @@
"raDeg": 317.671, "raDeg": 317.671,
"decDeg": 52.893 "decDeg": 52.893
}, },
"orbitSource": "JPL SSD satellite mean elements, epoch 1950 Jan 1", "orbitSource": "JPL SSD satellite mean elements, epoch 1950 Jan 1, within 1.3 degrees of Horizons from 1950 to 2100",
"parentBodyId": "mars", "parentBodyId": "mars",
"rotationPeriodHours": 7.653844882637157, "rotationPeriodHours": 7.653844882637157,
"rotationalElements": { "rotationalElements": {
@@ -794,7 +794,7 @@
"raDeg": 316.657, "raDeg": 316.657,
"decDeg": 53.529 "decDeg": 53.529
}, },
"orbitSource": "JPL SSD satellite mean elements, epoch 1950 Jan 1", "orbitSource": "JPL SSD satellite mean elements, epoch 1950 Jan 1, within 0.6 degrees of Horizons from 1950 to 2100",
"parentBodyId": "mars", "parentBodyId": "mars",
"rotationPeriodHours": 30.29857998656265, "rotationPeriodHours": 30.29857998656265,
"rotationalElements": { "rotationalElements": {
@@ -891,7 +891,7 @@
"raDeg": 268.057, "raDeg": 268.057,
"decDeg": 64.495 "decDeg": 64.495
}, },
"orbitSource": "JPL SSD satellite mean elements, epoch 1997 Jan 16", "orbitSource": "JPL SSD satellite mean elements, epoch 1997 Jan 16, within 0.1 degrees of Horizons from 1950 to 2100",
"parentBodyId": "jupiter", "parentBodyId": "jupiter",
"rotationPeriodHours": 42.45930625514436, "rotationPeriodHours": 42.45930625514436,
"rotationalElements": { "rotationalElements": {
@@ -956,7 +956,7 @@
"raDeg": 268.084, "raDeg": 268.084,
"decDeg": 64.506 "decDeg": 64.506
}, },
"orbitSource": "JPL SSD satellite mean elements, epoch 1997 Jan 16", "orbitSource": "JPL SSD satellite mean elements, epoch 1997 Jan 16, within 0.3 degrees of Horizons from 1950 to 2100",
"parentBodyId": "jupiter", "parentBodyId": "jupiter",
"rotationPeriodHours": 85.22834531173994, "rotationPeriodHours": 85.22834531173994,
"rotationalElements": { "rotationalElements": {
@@ -1030,7 +1030,7 @@
"raDeg": 268.168, "raDeg": 268.168,
"decDeg": 64.543 "decDeg": 64.543
}, },
"orbitSource": "JPL SSD satellite mean elements, epoch 1997 Jan 16", "orbitSource": "JPL SSD satellite mean elements, epoch 1997 Jan 16, within 0.3 degrees of Horizons from 1950 to 2100",
"parentBodyId": "jupiter", "parentBodyId": "jupiter",
"rotationPeriodHours": 171.70927794038664, "rotationPeriodHours": 171.70927794038664,
"rotationalElements": { "rotationalElements": {
@@ -1104,7 +1104,7 @@
"raDeg": 268.639, "raDeg": 268.639,
"decDeg": 64.749 "decDeg": 64.749
}, },
"orbitSource": "JPL SSD satellite mean elements, epoch 1997 Jan 16", "orbitSource": "JPL SSD satellite mean elements, epoch 1997 Jan 16, within 0.2 degrees of Horizons from 1950 to 2100",
"parentBodyId": "jupiter", "parentBodyId": "jupiter",
"rotationPeriodHours": 400.5364072574082, "rotationPeriodHours": 400.5364072574082,
"rotationalElements": { "rotationalElements": {
@@ -1184,7 +1184,7 @@
"raDeg": 40.589, "raDeg": 40.589,
"decDeg": 83.536 "decDeg": 83.536
}, },
"orbitSource": "JPL SSD satellite mean elements, epoch 2000 Jan 1", "orbitSource": "JPL SSD satellite mean elements, epoch 2000 Jan 1, within 7.5 degrees of Horizons from 1950 to 2100",
"parentBodyId": "saturn", "parentBodyId": "saturn",
"rotationPeriodHours": 22.618127008672275, "rotationPeriodHours": 22.618127008672275,
"rotationalElements": { "rotationalElements": {
@@ -1249,7 +1249,7 @@
"raDeg": 40.586, "raDeg": 40.586,
"decDeg": 83.536 "decDeg": 83.536
}, },
"orbitSource": "JPL SSD satellite mean elements, epoch 2000 Jan 1", "orbitSource": "JPL SSD satellite mean elements, epoch 2000 Jan 1, within 1.0 degrees of Horizons from 1950 to 2100",
"parentBodyId": "saturn", "parentBodyId": "saturn",
"rotationPeriodHours": 32.88523423439451, "rotationPeriodHours": 32.88523423439451,
"rotationalElements": { "rotationalElements": {
@@ -1300,7 +1300,7 @@
"raDeg": 40.578, "raDeg": 40.578,
"decDeg": 83.537 "decDeg": 83.537
}, },
"orbitSource": "JPL SSD satellite mean elements, epoch 2000 Jan 1", "orbitSource": "JPL SSD satellite mean elements, epoch 2000 Jan 1, within 0.3 degrees of Horizons from 1950 to 2100",
"parentBodyId": "saturn", "parentBodyId": "saturn",
"rotationPeriodHours": 45.30726088829954, "rotationPeriodHours": 45.30726088829954,
"rotationalElements": { "rotationalElements": {
@@ -1365,7 +1365,7 @@
"raDeg": 40.544, "raDeg": 40.544,
"decDeg": 83.54 "decDeg": 83.54
}, },
"orbitSource": "JPL SSD satellite mean elements, epoch 2000 Jan 1", "orbitSource": "JPL SSD satellite mean elements, epoch 2000 Jan 1, within 0.1 degrees of Horizons from 1950 to 2100",
"parentBodyId": "saturn", "parentBodyId": "saturn",
"rotationPeriodHours": 65.68597371070803, "rotationPeriodHours": 65.68597371070803,
"rotationalElements": { "rotationalElements": {
@@ -1410,7 +1410,7 @@
"raDeg": 40.328, "raDeg": 40.328,
"decDeg": 83.559 "decDeg": 83.559
}, },
"orbitSource": "JPL SSD satellite mean elements, epoch 2000 Jan 1", "orbitSource": "JPL SSD satellite mean elements, epoch 2000 Jan 1, within 0.2 degrees of Horizons from 1950 to 2100",
"parentBodyId": "saturn", "parentBodyId": "saturn",
"rotationPeriodHours": 108.42006554798584, "rotationPeriodHours": 108.42006554798584,
"rotationalElements": { "rotationalElements": {
@@ -1466,7 +1466,7 @@
"raDeg": 36.214, "raDeg": 36.214,
"decDeg": 83.949 "decDeg": 83.949
}, },
"orbitSource": "JPL SSD satellite mean elements, epoch 2000 Jan 1", "orbitSource": "JPL SSD satellite mean elements, epoch 2000 Jan 1, within 0.1 degrees of Horizons from 1950 to 2100",
"parentBodyId": "saturn", "parentBodyId": "saturn",
"rotationPeriodHours": 382.69076217631203, "rotationPeriodHours": 382.69076217631203,
"rotationalElements": { "rotationalElements": {
@@ -1511,7 +1511,7 @@
"raDeg": 36.372, "raDeg": 36.372,
"decDeg": 83.862 "decDeg": 83.862
}, },
"orbitSource": "JPL SSD satellite mean elements, epoch 2000 Jan 1", "orbitSource": "JPL SSD satellite mean elements, epoch 2000 Jan 1, within 22.3 degrees of Horizons from 1950 to 2100",
"measuredEccentricity": 0.105, "measuredEccentricity": 0.105,
"parentBodyId": "saturn" "parentBodyId": "saturn"
}, },
@@ -1539,7 +1539,7 @@
"raDeg": 284.715, "raDeg": 284.715,
"decDeg": 78.749 "decDeg": 78.749
}, },
"orbitSource": "JPL SSD satellite mean elements, epoch 2000 Jan 1", "orbitSource": "JPL SSD satellite mean elements, epoch 2000 Jan 1, within 10.4 degrees of Horizons from 1950 to 2100",
"parentBodyId": "saturn", "parentBodyId": "saturn",
"rotationPeriodHours": 1903.9469348834284, "rotationPeriodHours": 1903.9469348834284,
"rotationalElements": { "rotationalElements": {
@@ -1584,7 +1584,7 @@
"raDeg": 275.954, "raDeg": 275.954,
"decDeg": 67.455 "decDeg": 67.455
}, },
"orbitSource": "JPL SSD satellite mean elements, epoch 2000 Jan 1", "orbitSource": "JPL SSD satellite mean elements, epoch 2000 Jan 1, within 2.6 degrees of Horizons from 1950 to 2100",
"parentBodyId": "saturn", "parentBodyId": "saturn",
"rotationPeriodHours": 9.273966666666666, "rotationPeriodHours": 9.273966666666666,
"rotationalElements": { "rotationalElements": {
@@ -1629,7 +1629,7 @@
"raDeg": 77.311, "raDeg": 77.311,
"decDeg": 15.175 "decDeg": 15.175
}, },
"orbitSource": "JPL SSD satellite mean elements, epoch 1980 Jan 1", "orbitSource": "JPL SSD satellite mean elements, epoch 1980 Jan 1, within 1.8 degrees of Horizons from 1950 to 2100",
"parentBodyId": "uranus", "parentBodyId": "uranus",
"rotationPeriodHours": 33.9235057988244, "rotationPeriodHours": 33.9235057988244,
"rotationalElements": { "rotationalElements": {
@@ -1712,7 +1712,7 @@
"raDeg": 77.311, "raDeg": 77.311,
"decDeg": 15.175 "decDeg": 15.175
}, },
"orbitSource": "JPL SSD satellite mean elements, epoch 1980 Jan 1", "orbitSource": "JPL SSD satellite mean elements, epoch 1980 Jan 1, within 0.7 degrees of Horizons from 1950 to 2100",
"parentBodyId": "uranus", "parentBodyId": "uranus",
"rotationPeriodHours": 60.48909723963263, "rotationPeriodHours": 60.48909723963263,
"rotationalElements": { "rotationalElements": {
@@ -1777,7 +1777,7 @@
"raDeg": 77.311, "raDeg": 77.311,
"decDeg": 15.175 "decDeg": 15.175
}, },
"orbitSource": "JPL SSD satellite mean elements, epoch 1980 Jan 1", "orbitSource": "JPL SSD satellite mean elements, epoch 1980 Jan 1, within 0.8 degrees of Horizons from 1950 to 2100",
"parentBodyId": "uranus", "parentBodyId": "uranus",
"rotationPeriodHours": 99.46023494563465, "rotationPeriodHours": 99.46023494563465,
"rotationalElements": { "rotationalElements": {
@@ -1842,7 +1842,7 @@
"raDeg": 77.311, "raDeg": 77.311,
"decDeg": 15.175 "decDeg": 15.175
}, },
"orbitSource": "JPL SSD satellite mean elements, epoch 1980 Jan 1", "orbitSource": "JPL SSD satellite mean elements, epoch 1980 Jan 1, within 0.7 degrees of Horizons from 1950 to 2100",
"parentBodyId": "uranus", "parentBodyId": "uranus",
"rotationPeriodHours": 208.94080635857947, "rotationPeriodHours": 208.94080635857947,
"rotationalElements": { "rotationalElements": {
@@ -1898,7 +1898,7 @@
"raDeg": 77.311, "raDeg": 77.311,
"decDeg": 15.175 "decDeg": 15.175
}, },
"orbitSource": "JPL SSD satellite mean elements, epoch 1980 Jan 1", "orbitSource": "JPL SSD satellite mean elements, epoch 1980 Jan 1, within 0.5 degrees of Horizons from 1950 to 2100",
"parentBodyId": "uranus", "parentBodyId": "uranus",
"rotationPeriodHours": 323.1176207078424, "rotationPeriodHours": 323.1176207078424,
"rotationalElements": { "rotationalElements": {
@@ -1954,7 +1954,7 @@
"raDeg": 299.456, "raDeg": 299.456,
"decDeg": 43.414 "decDeg": 43.414
}, },
"orbitSource": "JPL SSD satellite mean elements, epoch 2000 Jan 1", "orbitSource": "JPL SSD satellite mean elements, epoch 2000 Jan 1, within 0.2 degrees of Horizons from 1950 to 2100",
"parentBodyId": "neptune", "parentBodyId": "neptune",
"rotationPeriodHours": 141.0444976486201, "rotationPeriodHours": 141.0444976486201,
"rotationalElements": { "rotationalElements": {
@@ -2082,7 +2082,7 @@
"raDeg": 269.302, "raDeg": 269.302,
"decDeg": 69.117 "decDeg": 69.117
}, },
"orbitSource": "JPL SSD satellite mean elements, epoch 2000 Jan 1", "orbitSource": "JPL SSD satellite mean elements, epoch 2000 Jan 1, within 11.2 degrees of Horizons from 1950 to 2100",
"parentBodyId": "neptune" "parentBodyId": "neptune"
}, },
{ {
@@ -2109,7 +2109,7 @@
"raDeg": 299.406, "raDeg": 299.406,
"decDeg": 42.432 "decDeg": 42.432
}, },
"orbitSource": "JPL SSD satellite mean elements, epoch 2000 Jan 1", "orbitSource": "JPL SSD satellite mean elements, epoch 2000 Jan 1, within 1.0 degrees of Horizons from 1950 to 2100",
"parentBodyId": "neptune", "parentBodyId": "neptune",
"rotationPeriodHours": 26.93555462331033, "rotationPeriodHours": 26.93555462331033,
"rotationalElements": { "rotationalElements": {
@@ -2174,7 +2174,7 @@
"raDeg": 132.993, "raDeg": 132.993,
"decDeg": -6.163 "decDeg": -6.163
}, },
"orbitSource": "JPL SSD satellite mean elements, epoch 2000 Jan 1", "orbitSource": "JPL SSD satellite mean elements, epoch 2000 Jan 1, within 0.4 degrees of Horizons from 1950 to 2100",
"parentBodyId": "pluto", "parentBodyId": "pluto",
"massRatio": 0.1220485755631374, "massRatio": 0.1220485755631374,
"rotationPeriodHours": 153.29335605836368, "rotationPeriodHours": 153.29335605836368,
+51 -23
View File
@@ -9,7 +9,8 @@ import { ExoplanetRecord } from '../../src/app/shared/models/exoplanet.model';
import { StarRecord, SUN_STAR_ID } from '../../src/app/shared/models/star.model'; import { StarRecord, SUN_STAR_ID } from '../../src/app/shared/models/star.model';
import { fetchDeepSky } from './fetchDeepSky'; import { fetchDeepSky } from './fetchDeepSky';
import { fetchExoplanets } from './fetchExoplanets'; import { fetchExoplanets } from './fetchExoplanets';
import { fetchSolarSystem, FREELY_SPINNING_MOONS } from './fetchSolarSystem'; import { fetchSolarSystem, FREELY_SPINNING_MOONS, offsetFromTrackDeg } from './fetchSolarSystem';
import { TrackPoint } from './lib/horizons';
import { BYTES_PER_STAR_META, BYTES_PER_STAR_POSITION, decodeStarCatalog, encodeStarCatalog } from '../../src/app/shared/models/star-catalog'; import { BYTES_PER_STAR_META, BYTES_PER_STAR_POSITION, decodeStarCatalog, encodeStarCatalog } from '../../src/app/shared/models/star-catalog';
import { fetchStars } from './fetchStars'; import { fetchStars } from './fetchStars';
import { describeSources } from './sources/registry'; import { describeSources } from './sources/registry';
@@ -152,28 +153,38 @@ const KM_PER_AU = 149597870.7;
const DEG_TO_RAD = Math.PI / 180; const DEG_TO_RAD = Math.PI / 180;
/** /**
* The moons whose table row cannot come within that on this one date, each for a reason no mean * The moons whose table row cannot come within that on this one date, each with a ceiling just
* ellipse carries, with a ceiling just above its offset here. What each reaches elsewhere is * above its offset here; see {@link TRACK_OFFSET_CEILINGS_DEG} for what they reach from 1950 to 2100.
* larger, and neither this check nor twelve New Year's Days sampled from 1980 to 2100 see it:
*
* - Hyperion, 9.4 degrees here and 22.2 at worst, sampled every other day from 1980 to 2100: held
* in a 4:3 resonance by Titan; the row's eccentricity, 0.0232, is less than a quarter of the
* 0.105 JPL's current table gives.
* - Iapetus, 9.6 here and 10.1 at worst: the row sits 9.4 degrees behind Horizons at its own epoch,
* 2000 Jan 1.5, and keeps that offset; its plane agrees with Horizons' to 0.07 degrees and its
* period to 0.001 per cent, so the fault is in the row's longitude, which this has no second
* source to correct.
* - Nereid, 2.6 here and 11.2 at worst, sampled daily: an eccentricity of 0.75, the largest here,
* which a mean ellipse follows least well near periapsis, where the true anomaly runs ten times
* faster than the mean. Its 360-day year put all twelve New Year's Days, under 2.6, far from
* periapsis; sampled daily it is past 2.6 on 92 days in 2010-2020 and on 295 in 2040-2050, each
* near a periapsis, and under 0.4 on most days.
*
* Mimas's swing of 44 degrees either way, the libration of its resonance with Tethys, which also
* needed a ceiling here once, is now in its orbit; see `orbitFromW` in `fetchSolarSystem.ts`.
*/ */
const MOON_OFFSET_CEILINGS_DEG: Record<string, number> = { hyperion: 21, iapetus: 11, nereid: 3 }; const MOON_OFFSET_CEILINGS_DEG: Record<string, number> = { hyperion: 21, iapetus: 11, nereid: 3 };
/**
* How far a moon's or dwarf planet's orbit may stray from Horizons from 1950 to 2100, sampled every
* other day (Nereid and Hyperion daily). One date showed each at its best: twelve New Year's Days
* gave Nereid 2.6 degrees, and 2025-01-01 alone is all the check above sees. Each card says how
* far its own orbit strays over the span (`fetchSolarSystem`), and this holds that figure to
* account.
*
* Measured on this catalogue: at most 2.62 degrees (the Moon, 2010 March 27: no mean ellipse has
* its evection or variation; Phoebe reaches 2.58 in 1969, where "within 2.0" was once claimed for
* it). Five need their own:
*
* - Hyperion, 22.23 (2055 Feb 26): held in a 4:3 resonance by Titan; the row's eccentricity,
* 0.0232, is less than a quarter of the 0.105 JPL's current table gives.
* - Nereid, 11.19 (2039 Nov 1): an eccentricity of 0.75, the largest here, which a mean ellipse
* follows least well near periapsis, where the true anomaly runs ten times faster than the mean;
* its 360-day year kept every New Year's Day far from one.
* - Iapetus, 10.34: the row sits 9.4 degrees behind Horizons at its own epoch, 2000 Jan 1.5, and
* keeps that offset; its plane agrees with Horizons' to 0.07 degrees and its period to 0.001 per
* cent, so the fault is in the row's longitude, which this has no second source to correct.
* - Mimas, 7.43: its orbit carries the 44-degree libration of its resonance with Tethys (see
* `orbitFromW` in `fetchSolarSystem.ts`), but not the rest of what Horizons integrates.
* - Ceres, 7.12 (1953): the SBDB's elements are osculating, exact at 2026 Jun 9 and drifting
* either side; 1.9 by 2050, 5.3 by 2100, and 39 at 1600 on Horizons' own figures.
*/
const MAX_TRACK_OFFSET_DEG = 3;
const TRACK_OFFSET_CEILINGS_DEG: Record<string, number> = { hyperion: 23, nereid: 12, iapetus: 11, mimas: 8, ceres: 8 };
/** /**
* The bodies the IAU WGCCRE 2015 report gives no rotational elements for: Hyperion tumbles, and * The bodies the IAU WGCCRE 2015 report gives no rotational elements for: Hyperion tumbles, and
* Nereid, Eris, Haumea and Makemake have no model. Every other body must carry them, or the * Nereid, Eris, Haumea and Makemake have no model. Every other body must carry them, or the
@@ -236,7 +247,7 @@ function angleBetweenDeg(a: { x: number; y: number; z: number }, b: { x: number;
return (Math.acos(Math.min(1, Math.max(-1, cosine))) * 180) / Math.PI; return (Math.acos(Math.min(1, Math.max(-1, cosine))) * 180) / Math.PI;
} }
function validateBodies(bodies: BodyRecord[], horizonsOrbits: Map<string, OrbitalElements>): void { function validateBodies(bodies: BodyRecord[], horizonsOrbits: Map<string, OrbitalElements>, horizonsTracks: Map<string, TrackPoint[]>): void {
assertCondition(bodies.length > 0, 'No solar-system bodies were produced.'); assertCondition(bodies.length > 0, 'No solar-system bodies were produced.');
const ids = new Set(bodies.map((body) => body.id)); const ids = new Set(bodies.map((body) => body.id));
@@ -261,6 +272,23 @@ function validateBodies(bodies: BodyRecord[], horizonsOrbits: Map<string, Orbita
`${body.name}'s mean elements put it ${offset.toFixed(2)} degrees from where Horizons has it (at most ${ceiling} expected) — the elements were read wrongly.` `${body.name}'s mean elements put it ${offset.toFixed(2)} degrees from where Horizons has it (at most ${ceiling} expected) — the elements were read wrongly.`
); );
offsets.push(`${body.id} ${offset.toFixed(3)}`); offsets.push(`${body.id} ${offset.toFixed(3)}`);
// Standish's fit names its own span; every other orbit is measured over 1950-2100, and says so.
const track = horizonsTracks.get(body.id);
assertCondition(
(track !== undefined) === !body.orbitSource.startsWith('JPL approximate mean elements (Standish)'),
`${body.name}'s orbit, "${body.orbitSource}", ${track ? 'names its own span' : 'names no span it holds over'}.`
);
if (track) {
const worst = Math.max(...track.map((point) => offsetFromTrackDeg(body, point)));
const trackCeiling = TRACK_OFFSET_CEILINGS_DEG[body.id] ?? MAX_TRACK_OFFSET_DEG;
const stated = Number(body.orbitSource.match(/within ([\d.]+) degrees of Horizons/)?.[1]);
assertCondition(
worst <= trackCeiling && stated >= worst,
`${body.name}'s mean elements put it up to ${worst.toFixed(2)} degrees from Horizons between 1950 and 2100 (at most ${trackCeiling} expected), and its card says "${body.orbitSource}".`
);
offsets.push(`${body.id} ${worst.toFixed(2)} at worst`);
}
// The card prints this under "Measured". An osculating eccentricity swings about its mean — the // The card prints this under "Measured". An osculating eccentricity swings about its mean — the
// Moon's by 0.015 here, Phoebe's by as much — but not by the 0.087 Hyperion's older row was out. // Moon's by 0.015 here, Phoebe's by as much — but not by the 0.087 Hyperion's older row was out.
const printed = body.measuredEccentricity ?? body.orbit.eccentricity; const printed = body.measuredEccentricity ?? body.orbit.eccentricity;
@@ -440,7 +468,7 @@ async function build(): Promise<void> {
const stars = await fetchStars(); const stars = await fetchStars();
console.log(); console.log();
const { bodies, horizonsOrbits } = await fetchSolarSystem(); const { bodies, horizonsOrbits, horizonsTracks } = await fetchSolarSystem();
console.log(); console.log();
const exoplanets = await fetchExoplanets(stars); const exoplanets = await fetchExoplanets(stars);
console.log(); console.log();
@@ -450,7 +478,7 @@ async function build(): Promise<void> {
console.log('Validating output...'); console.log('Validating output...');
validateStars(stars); validateStars(stars);
validateMerge(stars); validateMerge(stars);
validateBodies(bodies, horizonsOrbits); validateBodies(bodies, horizonsOrbits, horizonsTracks);
validateExoplanets(exoplanets, new Set(stars.map((star) => star.id))); validateExoplanets(exoplanets, new Set(stars.map((star) => star.id)));
validateDeepSky(deepSky); validateDeepSky(deepSky);
+39 -8
View File
@@ -2,7 +2,9 @@ import { writeFileSync } from 'node:fs';
import { BodyRecord, OrbitalElements } from '../../src/app/shared/models/body.model'; import { BodyRecord, OrbitalElements } from '../../src/app/shared/models/body.model';
import { SUN_STAR_ID } from '../../src/app/shared/models/star.model'; import { SUN_STAR_ID } from '../../src/app/shared/models/star.model';
import { fetchHorizonsBody } from './lib/horizons'; import { fetchHorizonsBody, fetchHorizonsTrack, TRACK_START_YEAR, TRACK_STOP_YEAR, TrackPoint } from './lib/horizons';
import { eclipticToEquatorial, laplacePlaneToEquatorial } from '../../src/app/shared/astro/coordinates';
import { meanElementsAt, positionAtEpoch } from '../../src/app/shared/astro/kepler';
import { MeanOrbit, parsePlanetMeanElements, parseSatelliteMeanElements, parseSmallBodyElements } from '../../src/app/shared/astro/mean-elements'; import { MeanOrbit, parsePlanetMeanElements, parseSatelliteMeanElements, parseSmallBodyElements } from '../../src/app/shared/astro/mean-elements';
import { fetchPlanetMeanElementsText, fetchSatelliteMeanElementsHtml, fetchSmallBodyAnswer } from './lib/mean-elements'; import { fetchPlanetMeanElementsText, fetchSatelliteMeanElementsHtml, fetchSmallBodyAnswer } from './lib/mean-elements';
import { MIN_PERIODIC_TERM_DEG, orbitalTermsOfPrimeMeridian, parsePckRotationalElements, SUN_ROTATIONAL_ELEMENTS } from '../../src/app/shared/astro/rotational-elements'; import { MIN_PERIODIC_TERM_DEG, orbitalTermsOfPrimeMeridian, parsePckRotationalElements, SUN_ROTATIONAL_ELEMENTS } from '../../src/app/shared/astro/rotational-elements';
@@ -52,6 +54,14 @@ interface BodySpec {
* given. See `orbitalTermsOfPrimeMeridian`. * given. See `orbitalTermsOfPrimeMeridian`.
*/ */
orbitFromW?: { angleRateDegPerCentury?: number }; orbitFromW?: { angleRateDegPerCentury?: number };
/**
* Days between the Horizons positions the orbit is checked against from 1950 to 2100; 2 unless
* the error changes faster than that. Nereid, at an eccentricity of 0.75, sweeps through its
* periapsis, where the mean ellipse is furthest out, in days; Hyperion, on a row whose
* eccentricity is a quarter of its real one, peaks within a day too (22.23 degrees sampled daily
* where every other day gave 22.14).
*/
trackStepDays?: number;
} }
/** S5 in pck00011.tpc, 316.45 + 506.2 T: the libration of Mimas and Tethys in their 4:2 resonance. */ /** S5 in pck00011.tpc, 316.45 + 506.2 T: the libration of Mimas and Tethys in their 4:2 resonance. */
@@ -114,13 +124,13 @@ const BODY_SPECS: BodySpec[] = [
{ id: 'titan', name: 'Titan', kind: 'moon', horizonsCommand: '606', center: '500@699', parentBodyId: 'saturn' }, { id: 'titan', name: 'Titan', kind: 'moon', horizonsCommand: '606', center: '500@699', parentBodyId: 'saturn' },
// Hyperion tumbles ("Rotational period = Chaotic") and Phoebe, captured, turns in 9.27 hours. // Hyperion tumbles ("Rotational period = Chaotic") and Phoebe, captured, turns in 9.27 hours.
// Hyperion's eccentricity is 0.105 in JPL's current table (ssd.jpl.nasa.gov/sats/elem, SAT441). // Hyperion's eccentricity is 0.105 in JPL's current table (ssd.jpl.nasa.gov/sats/elem, SAT441).
{ id: 'hyperion', name: 'Hyperion', kind: 'moon', horizonsCommand: '607', center: '500@699', parentBodyId: 'saturn', spinsFreely: true, measuredEccentricity: 0.105 }, { id: 'hyperion', name: 'Hyperion', kind: 'moon', horizonsCommand: '607', center: '500@699', parentBodyId: 'saturn', spinsFreely: true, measuredEccentricity: 0.105, trackStepDays: 1 },
{ id: 'iapetus', name: 'Iapetus', kind: 'moon', horizonsCommand: '608', center: '500@699', parentBodyId: 'saturn' }, { id: 'iapetus', name: 'Iapetus', kind: 'moon', horizonsCommand: '608', center: '500@699', parentBodyId: 'saturn' },
// Phoebe's row gives a mean motion of 0.6569114 degrees a day, a 548.02-day year, where its // Phoebe's row gives a mean motion of 0.6569114 degrees a day, a 548.02-day year, where its
// Horizons page and JPL's current table (SAT441) give 550.30: the table's own note warns that // Horizons page and JPL's current table (SAT441) give 550.30: the table's own note warns that
// its source misstated the mean motions of retrograde moons. On the row's figure Phoebe was // its source misstated the mean motions of retrograde moons. On the row's figure Phoebe was
// 25 degrees from Horizons by 2025 and 100 by 2075; on the current period, within 2.0 from 1980 // 25 degrees from Horizons by 2025 and 100 by 2075; on the current period, within 2.6 from 1950
// to 2100. // to 2100 (2.58 in 1969).
{ id: 'phoebe', name: 'Phoebe', kind: 'moon', horizonsCommand: '609', center: '500@699', parentBodyId: 'saturn', spinsFreely: true, periodDays: 550.30391 }, { id: 'phoebe', name: 'Phoebe', kind: 'moon', horizonsCommand: '609', center: '500@699', parentBodyId: 'saturn', spinsFreely: true, periodDays: 550.30391 },
{ id: 'miranda', name: 'Miranda', horizonsCommand: '705', ...URANUS_MOON }, { id: 'miranda', name: 'Miranda', horizonsCommand: '705', ...URANUS_MOON },
{ id: 'ariel', name: 'Ariel', horizonsCommand: '701', ...URANUS_MOON }, { id: 'ariel', name: 'Ariel', horizonsCommand: '701', ...URANUS_MOON },
@@ -129,7 +139,7 @@ const BODY_SPECS: BodySpec[] = [
{ id: 'oberon', name: 'Oberon', horizonsCommand: '704', ...URANUS_MOON }, { id: 'oberon', name: 'Oberon', horizonsCommand: '704', ...URANUS_MOON },
{ id: 'triton', name: 'Triton', kind: 'moon', horizonsCommand: '801', center: '500@899', parentBodyId: 'neptune' }, { id: 'triton', name: 'Triton', kind: 'moon', horizonsCommand: '801', center: '500@899', parentBodyId: 'neptune' },
// Nereid's eccentric orbit, 0.75, cannot hold a face to Neptune; its page states no spin. // Nereid's eccentric orbit, 0.75, cannot hold a face to Neptune; its page states no spin.
{ id: 'nereid', name: 'Nereid', kind: 'moon', horizonsCommand: '802', center: '500@899', parentBodyId: 'neptune', spinsFreely: true }, { id: 'nereid', name: 'Nereid', kind: 'moon', horizonsCommand: '802', center: '500@899', parentBodyId: 'neptune', spinsFreely: true, trackStepDays: 1 },
{ id: 'proteus', name: 'Proteus', kind: 'moon', horizonsCommand: '808', center: '500@899', parentBodyId: 'neptune' }, { id: 'proteus', name: 'Proteus', kind: 'moon', horizonsCommand: '808', center: '500@899', parentBodyId: 'neptune' },
// Pluto's section prints its epoch as 2000 Jan 1.0; JPL's current table gives Charon's as // Pluto's section prints its epoch as 2000 Jan 1.0; JPL's current table gives Charon's as
// 2000-01-01.5, and read at 1.0 Charon sat 27.8 to 28.2 degrees — half a day of its motion is // 2000-01-01.5, and read at 1.0 Charon sat 27.8 to 28.2 degrees — half a day of its motion is
@@ -148,10 +158,11 @@ export const FREELY_SPINNING_MOONS = new Set(BODY_SPECS.filter((spec) => spec.sp
* point and which face is where. Horizons' osculating elements for the same date come back * point and which face is where. Horizons' osculating elements for the same date come back
* alongside, for `build.ts` to check the mean ones against. * alongside, for `build.ts` to check the mean ones against.
*/ */
export async function fetchSolarSystem(): Promise<{ bodies: BodyRecord[]; horizonsOrbits: Map<string, OrbitalElements> }> { export async function fetchSolarSystem(): Promise<{ bodies: BodyRecord[]; horizonsOrbits: Map<string, OrbitalElements>; horizonsTracks: Map<string, TrackPoint[]> }> {
console.log(`Fetching ${BODY_SPECS.length} solar-system bodies from JPL (mean elements, Horizons, NAIF's PCK)...`); console.log(`Fetching ${BODY_SPECS.length} solar-system bodies from JPL (mean elements, Horizons, NAIF's PCK)...`);
const bodies: BodyRecord[] = []; const bodies: BodyRecord[] = [];
const horizonsOrbits = new Map<string, OrbitalElements>(); const horizonsOrbits = new Map<string, OrbitalElements>();
const horizonsTracks = new Map<string, TrackPoint[]>();
const planetElements = await fetchPlanetMeanElementsText(); const planetElements = await fetchPlanetMeanElementsText();
const satelliteElements = await fetchSatelliteMeanElementsHtml(); const satelliteElements = await fetchSatelliteMeanElementsHtml();
const pck = await fetchPckText(); const pck = await fetchPckText();
@@ -207,6 +218,18 @@ export async function fetchSolarSystem(): Promise<{ bodies: BodyRecord[]; horizo
rates: { ...corrected.rates, meanMotionDegPerDay: corrected.rates.meanMotionDegPerDay + fromW.meanMotionDegPerDay, meanAnomalyTerms: fromW.meanAnomalyTerms } rates: { ...corrected.rates, meanMotionDegPerDay: corrected.rates.meanMotionDegPerDay + fromW.meanMotionDegPerDay, meanAnomalyTerms: fromW.meanAnomalyTerms }
} }
: corrected; : corrected;
// Standish's fit states its own span, 3000 BC to AD 3000. The moons' table and the SBDB state
// none, and hold for far less: each card says how far its orbit stays from Horizons over the
// span it was measured, where the clock reaches AD 1 to AD 3000.
let orbitSource = mean.orbitSource;
if (parentName || smallBody) {
const stepDays = spec.trackStepDays ?? 2;
const track = await fetchHorizonsTrack(spec.horizonsCommand, spec.center, stepDays, `horizons-track-${spec.id}-${stepDays}d.txt`);
horizonsTracks.set(spec.id, track);
const worst = Math.max(...track.map((point) => offsetFromTrackDeg(mean, point)));
orbitSource += `, within ${(Math.ceil(worst * 10) / 10).toFixed(1)} degrees of Horizons from ${TRACK_START_YEAR} to ${TRACK_STOP_YEAR}`;
}
const radiusKm = smallBody?.radiusKm ?? spec.radiusKm ?? result.radiusKm; const radiusKm = smallBody?.radiusKm ?? spec.radiusKm ?? result.radiusKm;
if (radiusKm === undefined) { if (radiusKm === undefined) {
console.warn(` no physical radius found for ${spec.name}; defaulting to 0.`); console.warn(` no physical radius found for ${spec.name}; defaulting to 0.`);
@@ -238,7 +261,7 @@ export async function fetchSolarSystem(): Promise<{ bodies: BodyRecord[]; horizo
orbit: mean.orbit, orbit: mean.orbit,
rates: mean.rates, rates: mean.rates,
...(mean.laplacePole ? { laplacePole: mean.laplacePole } : {}), ...(mean.laplacePole ? { laplacePole: mean.laplacePole } : {}),
orbitSource: mean.orbitSource, orbitSource,
...(spec.measuredEccentricity !== undefined ? { measuredEccentricity: spec.measuredEccentricity } : {}), ...(spec.measuredEccentricity !== undefined ? { measuredEccentricity: spec.measuredEccentricity } : {}),
...(spec.parentBodyId ? { parentBodyId: spec.parentBodyId } : {}), ...(spec.parentBodyId ? { parentBodyId: spec.parentBodyId } : {}),
...(parentGm !== undefined ? { massRatio: result.gmKm3PerS2! / parentGm } : {}), ...(parentGm !== undefined ? { massRatio: result.gmKm3PerS2! / parentGm } : {}),
@@ -251,7 +274,15 @@ export async function fetchSolarSystem(): Promise<{ bodies: BodyRecord[]; horizo
ensureDataDir(); ensureDataDir();
writeFileSync(dataPath('bodies.json'), JSON.stringify(bodies, null, 2)); writeFileSync(dataPath('bodies.json'), JSON.stringify(bodies, null, 2));
console.log(` wrote ${bodies.length} bodies.`); console.log(` wrote ${bodies.length} bodies.`);
return { bodies, horizonsOrbits }; return { bodies, horizonsOrbits, horizonsTracks };
}
/** Degrees between where a moon's or dwarf planet's mean elements put it and where Horizons has it. */
export function offsetFromTrackDeg(mean: Pick<MeanOrbit, 'orbit' | 'rates' | 'laplacePole'>, point: TrackPoint): number {
const own = positionAtEpoch(meanElementsAt(mean.orbit, mean.rates, point.jd));
const place = mean.laplacePole ? laplacePlaneToEquatorial(own, mean.laplacePole) : eclipticToEquatorial(own);
const cosine = (place.x * point.x + place.y * point.y + place.z * point.z) / (Math.hypot(place.x, place.y, place.z) * Math.hypot(point.x, point.y, point.z));
return (Math.acos(Math.min(1, Math.max(-1, cosine))) * 180) / Math.PI;
} }
if (require.main === module) { if (require.main === module) {
+37
View File
@@ -93,3 +93,40 @@ function extractNumber(text: string, pattern: RegExp): number {
} }
return Number(match[1]); return Number(match[1]);
} }
/** The span a moon's or dwarf planet's mean elements are checked against Horizons over, and the card names. */
export const TRACK_START_YEAR = 1950;
export const TRACK_STOP_YEAR = 2100;
/** One Horizons position: TDB Julian date, and ICRF equatorial coordinates in AU from the centre. */
export interface TrackPoint {
jd: number;
x: number;
y: number;
z: number;
}
/**
* Where Horizons has a body, from its centre, every `stepDays` from 1950 to 2100: the ephemeris the
* mean elements are checked against over the whole span, where one date saw a moon at its best.
*/
export async function fetchHorizonsTrack(command: string, center: string, stepDays: number, cacheKey: string): Promise<TrackPoint[]> {
const url =
`${HORIZONS_URL}?format=text&COMMAND='${encodeURIComponent(command)}'&OBJ_DATA='NO'&MAKE_EPHEM='YES'` +
`&EPHEM_TYPE='VECTORS'&CENTER='${center}'&START_TIME='${TRACK_START_YEAR}-01-01'&STOP_TIME='${TRACK_STOP_YEAR}-01-01'` +
`&STEP_SIZE='${stepDays}%20d'&REF_PLANE='FRAME'&REF_SYSTEM='ICRF'&VEC_TABLE='1'&OUT_UNITS='AU-D'&CSV_FORMAT='YES'&VEC_CORR='NONE'`;
const text = await fetchTextCached(url, cacheKey);
const startIndex = text.indexOf('$$SOE');
const endIndex = text.indexOf('$$EOE');
if (startIndex === -1 || endIndex === -1) {
throw new Error(`Horizons gave no vectors for ${command} from ${center}: ${text.slice(0, 300)}`);
}
return text
.slice(startIndex + '$$SOE'.length, endIndex)
.trim()
.split(/\r?\n/)
.map((row) => {
const [jd, , x, y, z] = row.split(',').map((field) => field.trim());
return { jd: Number(jd), x: Number(x), y: Number(y), z: Number(z) };
});
}