diff --git a/src/app/features/galaxy-system/galaxy-system-scene.component.spec.ts b/src/app/features/galaxy-system/galaxy-system-scene.component.spec.ts index 97142b2..bfbd71d 100644 --- a/src/app/features/galaxy-system/galaxy-system-scene.component.spec.ts +++ b/src/app/features/galaxy-system/galaxy-system-scene.component.spec.ts @@ -836,7 +836,7 @@ describe('GalaxySystemSceneComponent camera-flight transitions', () => { navigationStore.selectStar(SUN.id); await flushAsync(); await advanceFrames(engine, 2.5); - expect(note()).toBe('Orbits propagated from JPL mean elements, the planets’ fit for 3000 BC to AD 3000, to the current date.'); + expect(note()).toBe('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 the current date.'); navigationStore.selectStar(ALPHA_CENTAURI.id); await flushAsync(); diff --git a/src/app/features/galaxy-system/galaxy-system-scene.component.ts b/src/app/features/galaxy-system/galaxy-system-scene.component.ts index 45ca160..d8630d1 100644 --- a/src/app/features/galaxy-system/galaxy-system-scene.component.ts +++ b/src/app/features/galaxy-system/galaxy-system-scene.component.ts @@ -1762,7 +1762,7 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy { ]); // 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. - const source = this.bodies.some((body) => body.systemStarId === star.id) ? 'JPL mean elements, the planets’ fit for 3000 BC to AD 3000,' : 'published elements'; + 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'; this.hudNote.set(`Orbits propagated from ${source} to ${this.time.atNow() ? 'the current date' : 'the date on the clock'}.`); this.hudRange.set( formatAu( diff --git a/src/assets/data/bodies.json b/src/assets/data/bodies.json index c3971f7..db4912e 100644 --- a/src/assets/data/bodies.json +++ b/src/assets/data/bodies.json @@ -272,6 +272,98 @@ "rotationPeriodHours": 153.29335198, "obliquityDeg": 119.6 }, + { + "id": "ceres", + "systemStarId": 0, + "name": "Ceres", + "kind": "dwarf", + "radiusKm": 469.7, + "orbit": { + "semiMajorAxisAu": 2.765552595034094, + "eccentricity": 0.07969229514816586, + "inclinationDeg": 10.58802780183462, + "longitudeOfAscendingNodeDeg": 80.24862682043221, + "argumentOfPeriapsisDeg": 73.29421453021587, + "meanAnomalyAtEpochDeg": 274.4193463761342, + "epochJd": 2461200.5 + }, + "rates": { + "meanMotionDegPerDay": 0.21430445064843, + "longitudeOfAscendingNodeDegPerDay": 0, + "argumentOfPeriapsisDegPerDay": 0 + }, + "orbitSource": "JPL SBDB osculating elements, epoch 2026 Jun 9", + "rotationPeriodHours": 9.07417 + }, + { + "id": "eris", + "systemStarId": 0, + "name": "Eris", + "kind": "dwarf", + "radiusKm": 1163, + "orbit": { + "semiMajorAxisAu": 67.93394687853566, + "eccentricity": 0.4382385347971672, + "inclinationDeg": 43.9258279471791, + "longitudeOfAscendingNodeDeg": 36.00477044417249, + "argumentOfPeriapsisDeg": 150.7949235840312, + "meanAnomalyAtEpochDeg": 211.774434275007, + "epochJd": 2461200.5 + }, + "rates": { + "meanMotionDegPerDay": 0.001760247770619088, + "longitudeOfAscendingNodeDegPerDay": 0, + "argumentOfPeriapsisDegPerDay": 0 + }, + "orbitSource": "JPL SBDB osculating elements, epoch 2026 Jun 9", + "rotationPeriodHours": 25.9 + }, + { + "id": "haumea", + "systemStarId": 0, + "name": "Haumea", + "kind": "dwarf", + "radiusKm": 797.6, + "orbit": { + "semiMajorAxisAu": 43.06029023650952, + "eccentricity": 0.1944430148898797, + "inclinationDeg": 28.20847393040364, + "longitudeOfAscendingNodeDeg": 121.7860561329425, + "argumentOfPeriapsisDeg": 240.6905472508661, + "meanAnomalyAtEpochDeg": 223.2104118812299, + "epochJd": 2461200.5 + }, + "rates": { + "meanMotionDegPerDay": 0.003488097731816818, + "longitudeOfAscendingNodeDegPerDay": 0, + "argumentOfPeriapsisDegPerDay": 0 + }, + "orbitSource": "JPL SBDB osculating elements, epoch 2026 Jun 9", + "rotationPeriodHours": 3.9154 + }, + { + "id": "makemake", + "systemStarId": 0, + "name": "Makemake", + "kind": "dwarf", + "radiusKm": 715, + "orbit": { + "semiMajorAxisAu": 45.57093317300052, + "eccentricity": 0.1588889953992523, + "inclinationDeg": 29.02785603743067, + "longitudeOfAscendingNodeDeg": 79.2948338209406, + "argumentOfPeriapsisDeg": 297.0922733397207, + "meanAnomalyAtEpochDeg": 169.9379962048232, + "epochJd": 2461200.5 + }, + "rates": { + "meanMotionDegPerDay": 0.003203850120050116, + "longitudeOfAscendingNodeDegPerDay": 0, + "argumentOfPeriapsisDegPerDay": 0 + }, + "orbitSource": "JPL SBDB osculating elements, epoch 2026 Jun 9", + "rotationPeriodHours": 22.8266 + }, { "id": "moon", "systemStarId": 0, @@ -465,6 +557,146 @@ "parentBodyId": "jupiter", "rotationPeriodHours": 400.5364072574082 }, + { + "id": "mimas", + "systemStarId": 0, + "name": "Mimas", + "kind": "moon", + "radiusKm": 198.8, + "orbit": { + "semiMajorAxisAu": 0.0012402516100785653, + "eccentricity": 0.0196, + "inclinationDeg": 1.574, + "longitudeOfAscendingNodeDeg": 173.027, + "argumentOfPeriapsisDeg": 332.499, + "meanAnomalyAtEpochDeg": 14.848, + "epochJd": 2451545 + }, + "rates": { + "meanMotionDegPerDay": 381.9944948, + "longitudeOfAscendingNodeDegPerDay": -0.9996209770462032, + "argumentOfPeriapsisDegPerDay": 1.9992419540924065 + }, + "laplacePole": { + "raDeg": 40.589, + "decDeg": 83.536 + }, + "orbitSource": "JPL SSD satellite mean elements, epoch 2000 Jan 1", + "parentBodyId": "saturn", + "rotationPeriodHours": 22.618127008672275 + }, + { + "id": "enceladus", + "systemStarId": 0, + "name": "Enceladus", + "kind": "moon", + "radiusKm": 252.3, + "orbit": { + "semiMajorAxisAu": 0.0015912124877590255, + "eccentricity": 0, + "inclinationDeg": 0.003, + "longitudeOfAscendingNodeDeg": 342.507, + "argumentOfPeriapsisDeg": 0.076, + "meanAnomalyAtEpochDeg": 199.686, + "epochJd": 2451545 + }, + "rates": { + "meanMotionDegPerDay": 262.7318978, + "longitudeOfAscendingNodeDegPerDay": -0.41763825566421886, + "argumentOfPeriapsisDegPerDay": 0.8324546312225984 + }, + "laplacePole": { + "raDeg": 40.586, + "decDeg": 83.536 + }, + "orbitSource": "JPL SSD satellite mean elements, epoch 2000 Jan 1", + "parentBodyId": "saturn", + "rotationPeriodHours": 32.88523423439451 + }, + { + "id": "tethys", + "systemStarId": 0, + "name": "Tethys", + "kind": "moon", + "radiusKm": 536.3, + "orbit": { + "semiMajorAxisAu": 0.0019697606564930874, + "eccentricity": 0.0001, + "inclinationDeg": 1.091, + "longitudeOfAscendingNodeDeg": 259.842, + "argumentOfPeriapsisDeg": 45.202, + "meanAnomalyAtEpochDeg": 243.367, + "epochJd": 2451545 + }, + "rates": { + "meanMotionDegPerDay": 190.6979109, + "longitudeOfAscendingNodeDegPerDay": -0.1978374715711675, + "argumentOfPeriapsisDegPerDay": 0.3958338487419905 + }, + "laplacePole": { + "raDeg": 40.578, + "decDeg": 83.537 + }, + "orbitSource": "JPL SSD satellite mean elements, epoch 2000 Jan 1", + "parentBodyId": "saturn", + "rotationPeriodHours": 45.30726088829954 + }, + { + "id": "dione", + "systemStarId": 0, + "name": "Dione", + "kind": "moon", + "radiusKm": 562.5, + "orbit": { + "semiMajorAxisAu": 0.0025228634487509454, + "eccentricity": 0.0022, + "inclinationDeg": 0.028, + "longitudeOfAscendingNodeDeg": 290.415, + "argumentOfPeriapsisDeg": 284.315, + "meanAnomalyAtEpochDeg": 322.232, + "epochJd": 2451545 + }, + "rates": { + "meanMotionDegPerDay": 131.5349307, + "longitudeOfAscendingNodeDegPerDay": -0.08417681128768952, + "argumentOfPeriapsisDegPerDay": 0.1684255439794184 + }, + "laplacePole": { + "raDeg": 40.544, + "decDeg": 83.54 + }, + "orbitSource": "JPL SSD satellite mean elements, epoch 2000 Jan 1", + "parentBodyId": "saturn", + "rotationPeriodHours": 65.68597371070803 + }, + { + "id": "rhea", + "systemStarId": 0, + "name": "Rhea", + "kind": "moon", + "radiusKm": 764.5, + "orbit": { + "semiMajorAxisAu": 0.003523231965359785, + "eccentricity": 0.0002, + "inclinationDeg": 0.333, + "longitudeOfAscendingNodeDeg": 351.042, + "argumentOfPeriapsisDeg": 241.619, + "meanAnomalyAtEpochDeg": 179.781, + "epochJd": 2451545 + }, + "rates": { + "meanMotionDegPerDay": 79.6900459, + "longitudeOfAscendingNodeDegPerDay": -0.02750687327996083, + "argumentOfPeriapsisDegPerDay": 0.054943212183932014 + }, + "laplacePole": { + "raDeg": 40.328, + "decDeg": 83.559 + }, + "orbitSource": "JPL SSD satellite mean elements, epoch 2000 Jan 1", + "parentBodyId": "saturn", + "rotationPeriodHours": 108.42006554798584 + }, { "id": "titan", "systemStarId": 0, @@ -493,6 +725,229 @@ "parentBodyId": "saturn", "rotationPeriodHours": 382.69076217631203 }, + { + "id": "hyperion", + "systemStarId": 0, + "name": "Hyperion", + "kind": "moon", + "radiusKm": 133, + "orbit": { + "semiMajorAxisAu": 0.010033117403187745, + "eccentricity": 0.0232, + "inclinationDeg": 0.615, + "longitudeOfAscendingNodeDeg": 263.847, + "argumentOfPeriapsisDeg": 303.178, + "meanAnomalyAtEpochDeg": 86.342, + "epochJd": 2451545 + }, + "rates": { + "meanMotionDegPerDay": 16.9199503, + "longitudeOfAscendingNodeDegPerDay": -0.006372858420842858, + "argumentOfPeriapsisDegPerDay": 0.007700806964353125 + }, + "laplacePole": { + "raDeg": 36.372, + "decDeg": 83.862 + }, + "orbitSource": "JPL SSD satellite mean elements, epoch 2000 Jan 1", + "parentBodyId": "saturn" + }, + { + "id": "iapetus", + "systemStarId": 0, + "name": "Iapetus", + "kind": "moon", + "radiusKm": 734.5, + "orbit": { + "semiMajorAxisAu": 0.023802838792678086, + "eccentricity": 0.0293, + "inclinationDeg": 8.298, + "longitudeOfAscendingNodeDeg": 81.105, + "argumentOfPeriapsisDeg": 271.606, + "meanAnomalyAtEpochDeg": 201.789, + "epochJd": 2451545 + }, + "rates": { + "meanMotionDegPerDay": 4.5379416, + "longitudeOfAscendingNodeDegPerDay": -0.00028662508640328154, + "argumentOfPeriapsisDegPerDay": 0.0005878404972699524 + }, + "laplacePole": { + "raDeg": 284.715, + "decDeg": 78.749 + }, + "orbitSource": "JPL SSD satellite mean elements, epoch 2000 Jan 1", + "parentBodyId": "saturn", + "rotationPeriodHours": 1903.9469348834284 + }, + { + "id": "phoebe", + "systemStarId": 0, + "name": "Phoebe", + "kind": "moon", + "radiusKm": 106.6, + "orbit": { + "semiMajorAxisAu": 0.08655148592298781, + "eccentricity": 0.1634, + "inclinationDeg": 175.243, + "longitudeOfAscendingNodeDeg": 241.086, + "argumentOfPeriapsisDeg": 342.5, + "meanAnomalyAtEpochDeg": 53.038, + "epochJd": 2451545 + }, + "rates": { + "meanMotionDegPerDay": 0.6541839762686767, + "longitudeOfAscendingNodeDegPerDay": 0.0013629247388132204, + "argumentOfPeriapsisDegPerDay": 0.002138389055297137 + }, + "laplacePole": { + "raDeg": 275.954, + "decDeg": 67.455 + }, + "orbitSource": "JPL SSD satellite mean elements, epoch 2000 Jan 1", + "parentBodyId": "saturn", + "rotationPeriodHours": 9.273966666666666 + }, + { + "id": "miranda", + "systemStarId": 0, + "name": "Miranda", + "kind": "moon", + "radiusKm": 235.67989734197351, + "orbit": { + "semiMajorAxisAu": 0.0008683278671826712, + "eccentricity": 0.0013, + "inclinationDeg": 4.338, + "longitudeOfAscendingNodeDeg": 506.438, + "argumentOfPeriapsisDeg": 68.312, + "meanAnomalyAtEpochDeg": 311.33, + "epochJd": 2444239.5 + }, + "rates": { + "meanMotionDegPerDay": 254.6906576, + "longitudeOfAscendingNodeDegPerDay": -0.05560028675847895, + "argumentOfPeriapsisDegPerDay": 0.11058300049002093 + }, + "laplacePole": { + "raDeg": 77.311, + "decDeg": 15.175 + }, + "orbitSource": "JPL SSD satellite mean elements, epoch 1980 Jan 1", + "parentBodyId": "uranus", + "rotationPeriodHours": 33.9235057988244 + }, + { + "id": "ariel", + "systemStarId": 0, + "name": "Ariel", + "kind": "moon", + "radiusKm": 578.8979066892538, + "orbit": { + "semiMajorAxisAu": 0.0012760876816410463, + "eccentricity": 0.0012, + "inclinationDeg": 0.041, + "longitudeOfAscendingNodeDeg": 202.394, + "argumentOfPeriapsisDeg": 115.349, + "meanAnomalyAtEpochDeg": 39.481, + "epochJd": 2444239.5 + }, + "rates": { + "meanMotionDegPerDay": 142.8356579, + "longitudeOfAscendingNodeDegPerDay": -0.017216781081741836, + "argumentOfPeriapsisDegPerDay": 0.034237400422660706 + }, + "laplacePole": { + "raDeg": 77.311, + "decDeg": 15.175 + }, + "orbitSource": "JPL SSD satellite mean elements, epoch 1980 Jan 1", + "parentBodyId": "uranus", + "rotationPeriodHours": 60.48909723963263 + }, + { + "id": "umbriel", + "systemStarId": 0, + "name": "Umbriel", + "kind": "moon", + "radiusKm": 584.7, + "orbit": { + "semiMajorAxisAu": 0.0017781001745234067, + "eccentricity": 0.0039, + "inclinationDeg": 0.128, + "longitudeOfAscendingNodeDeg": 213.485, + "argumentOfPeriapsisDeg": 84.709, + "meanAnomalyAtEpochDeg": 12.469, + "epochJd": 2444239.5 + }, + "rates": { + "meanMotionDegPerDay": 86.8688879, + "longitudeOfAscendingNodeDegPerDay": -0.007763832371289367, + "argumentOfPeriapsisDegPerDay": 0.015608689123104496 + }, + "laplacePole": { + "raDeg": 77.311, + "decDeg": 15.175 + }, + "orbitSource": "JPL SSD satellite mean elements, epoch 1980 Jan 1", + "parentBodyId": "uranus", + "rotationPeriodHours": 99.46023494563465 + }, + { + "id": "titania", + "systemStarId": 0, + "name": "Titania", + "kind": "moon", + "radiusKm": 788.9, + "orbit": { + "semiMajorAxisAu": 0.002916485361445723, + "eccentricity": 0.0011, + "inclinationDeg": 0.079, + "longitudeOfAscendingNodeDeg": 279.771, + "argumentOfPeriapsisDeg": 284.4, + "meanAnomalyAtEpochDeg": 24.614, + "epochJd": 2444239.5 + }, + "rates": { + "meanMotionDegPerDay": 41.3514246, + "longitudeOfAscendingNodeDegPerDay": -0.005044947168524978, + "argumentOfPeriapsisDegPerDay": 0.006102004540272753 + }, + "laplacePole": { + "raDeg": 77.311, + "decDeg": 15.175 + }, + "orbitSource": "JPL SSD satellite mean elements, epoch 1980 Jan 1", + "parentBodyId": "uranus", + "rotationPeriodHours": 208.94080635857947 + }, + { + "id": "oberon", + "systemStarId": 0, + "name": "Oberon", + "kind": "moon", + "radiusKm": 761.4, + "orbit": { + "semiMajorAxisAu": 0.0039004565858436384, + "eccentricity": 0.0014, + "inclinationDeg": 0.068, + "longitudeOfAscendingNodeDeg": 459.771, + "argumentOfPeriapsisDeg": 104.4, + "meanAnomalyAtEpochDeg": 283.088, + "epochJd": 2444239.5 + }, + "rates": { + "meanMotionDegPerDay": 26.7394888, + "longitudeOfAscendingNodeDegPerDay": -0.005044921345997627, + "argumentOfPeriapsisDegPerDay": 0.006102193433429646 + }, + "laplacePole": { + "raDeg": 77.311, + "decDeg": 15.175 + }, + "orbitSource": "JPL SSD satellite mean elements, epoch 1980 Jan 1", + "parentBodyId": "uranus", + "rotationPeriodHours": 323.1176207078424 + }, { "id": "triton", "systemStarId": 0, @@ -520,5 +975,89 @@ "orbitSource": "JPL SSD satellite mean elements, epoch 2000 Jan 1", "parentBodyId": "neptune", "rotationPeriodHours": 141.0444976486201 + }, + { + "id": "nereid", + "systemStarId": 0, + "name": "Nereid", + "kind": "moon", + "radiusKm": 170, + "orbit": { + "semiMajorAxisAu": 0.03685759679733196, + "eccentricity": 0.7507, + "inclinationDeg": 7.09, + "longitudeOfAscendingNodeDeg": 335.57, + "argumentOfPeriapsisDeg": 281.117, + "meanAnomalyAtEpochDeg": 216.692, + "epochJd": 2451545 + }, + "rates": { + "meanMotionDegPerDay": 0.9996276, + "longitudeOfAscendingNodeDegPerDay": -0.0001042358742653985, + "argumentOfPeriapsisDegPerDay": 0.0001218108353098093 + }, + "laplacePole": { + "raDeg": 269.302, + "decDeg": 69.117 + }, + "orbitSource": "JPL SSD satellite mean elements, epoch 2000 Jan 1", + "parentBodyId": "neptune" + }, + { + "id": "proteus", + "systemStarId": 0, + "name": "Proteus", + "kind": "moon", + "radiusKm": 208, + "orbit": { + "semiMajorAxisAu": 0.0007864149365863937, + "eccentricity": 0.0005, + "inclinationDeg": 0.075, + "longitudeOfAscendingNodeDeg": 315.131, + "argumentOfPeriapsisDeg": 67.968, + "meanAnomalyAtEpochDeg": 250.938, + "epochJd": 2451545 + }, + "rates": { + "meanMotionDegPerDay": 320.7656245, + "longitudeOfAscendingNodeDegPerDay": -0.0774437246301215, + "argumentOfPeriapsisDegPerDay": 0.15448687827077687 + }, + "laplacePole": { + "raDeg": 299.406, + "decDeg": 42.432 + }, + "orbitSource": "JPL SSD satellite mean elements, epoch 2000 Jan 1", + "parentBodyId": "neptune", + "rotationPeriodHours": 26.93555462331033 + }, + { + "id": "charon", + "systemStarId": 0, + "name": "Charon", + "kind": "moon", + "radiusKm": 606, + "orbit": { + "semiMajorAxisAu": 0.00013095774631236113, + "eccentricity": 0.0002, + "inclinationDeg": 0.08, + "longitudeOfAscendingNodeDeg": 26.928, + "argumentOfPeriapsisDeg": 146.106, + "meanAnomalyAtEpochDeg": 131.07, + "epochJd": 2451545 + }, + "rates": { + "meanMotionDegPerDay": 56.362521, + "longitudeOfAscendingNodeDegPerDay": -0.00010926638529337138, + "argumentOfPeriapsisDegPerDay": 0.00009683851540842405 + }, + "laplacePole": { + "raDeg": 132.993, + "decDeg": -6.163 + }, + "orbitSource": "JPL SSD satellite mean elements, epoch 2000 Jan 1", + "parentBodyId": "pluto", + "massRatio": 0.1220485755631374, + "rotationPeriodHours": 153.29335605836368 } ] \ No newline at end of file diff --git a/tools/etl/build.ts b/tools/etl/build.ts index 791e384..f95df3a 100644 --- a/tools/etl/build.ts +++ b/tools/etl/build.ts @@ -8,7 +8,7 @@ import { ExoplanetRecord } from '../../src/app/shared/models/exoplanet.model'; import { StarRecord, SUN_STAR_ID } from '../../src/app/shared/models/star.model'; import { fetchDeepSky } from './fetchDeepSky'; import { fetchExoplanets } from './fetchExoplanets'; -import { fetchSolarSystem } from './fetchSolarSystem'; +import { fetchSolarSystem, FREELY_SPINNING_MOONS } from './fetchSolarSystem'; import { BYTES_PER_STAR_META, BYTES_PER_STAR_POSITION, decodeStarCatalog, encodeStarCatalog } from '../../src/app/shared/models/star-catalog'; import { fetchStars } from './fetchStars'; import { describeSources } from './sources/registry'; @@ -145,6 +145,23 @@ function validateMerge(stars: StarRecord[]): void { */ const MAX_PLANET_OFFSET_DEG = 0.25; const MAX_MOON_OFFSET_DEG = 2.5; +const KM_PER_AU = 149597870.7; + +/** + * The moons whose table row cannot come within that, each for a reason no mean ellipse carries, + * with a ceiling just above its worst offset from Horizons at twelve dates from 1980 to 2100: + * + * - Mimas, 44.7 degrees: its resonance with Tethys swings its mean longitude 44 degrees either + * way over 70.8 years, and the table has no column for it (Tethys, on the other end, swings 2). + * - Hyperion, 20.2: 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, 10.1: 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: an eccentricity of 0.75, the largest here, which a mean ellipse follows least + * well: under 0.9 degrees in every year measured but 2025 and 2030 (2.6 and 2.3) and 2100 (1.7). + */ +const MOON_OFFSET_CEILINGS_DEG: Record = { mimas: 46, hyperion: 21, iapetus: 11, nereid: 3 }; function angleBetweenDeg(a: { x: number; y: number; z: number }, b: { x: number; y: number; z: number }): number { const cosine = (a.x * b.x + a.y * b.y + a.z * b.z) / (Math.hypot(a.x, a.y, a.z) * Math.hypot(b.x, b.y, b.z)); @@ -169,28 +186,52 @@ function validateBodies(bodies: BodyRecord[], horizonsOrbits: Map 0, `Body ${body.id} has no radius; its page states it in a form the ETL does not read.`); + if (body.kind === 'moon') { - assertCondition(!!body.parentBodyId && ids.has(body.parentBodyId), `Moon ${body.id} has no valid parentBodyId.`); - // Every moon here is tidally locked: its day is its orbit, from the same mean motion that - // carries it round, or its face turns away from its planet: the Kepler period of the - // osculating orbit this used to take would turn the Moon's five degrees an orbit. + const parent = bodies.find((candidate) => candidate.id === body.parentBodyId); + assertCondition(parent !== undefined, `Moon ${body.id} has no valid parentBodyId.`); const orbitHours = (360 / body.rates.meanMotionDegPerDay) * 24; - assertCondition( - body.rotationPeriodHours !== undefined && Math.abs(body.rotationPeriodHours - orbitHours) <= orbitHours * 1e-9, - `Moon ${body.id} turns once in ${body.rotationPeriodHours} hours but goes round in ${orbitHours} — it will not keep one face to its planet.` - ); + if (FREELY_SPINNING_MOONS.has(body.id)) { + // Hyperion tumbles and Nereid's page gives no spin, so they have none; Phoebe turns in + // 9.27 hours against a 550-day orbit. A lock here would be the rule below misapplied. + assertCondition( + body.rotationPeriodHours === undefined || Math.abs(body.rotationPeriodHours - orbitHours) > orbitHours * 0.1, + `Moon ${body.id} does not keep one face to its planet, yet turns once in ${body.rotationPeriodHours} hours against an orbit of ${orbitHours}.` + ); + } else { + // Every other moon here is tidally locked: its day is its orbit, from the same mean motion + // that carries it round, or its face turns away from its planet: the Kepler period of the + // osculating orbit this used to take would turn the Moon's five degrees an orbit. + assertCondition( + body.rotationPeriodHours !== undefined && Math.abs(body.rotationPeriodHours - orbitHours) <= orbitHours * 1e-9, + `Moon ${body.id} turns once in ${body.rotationPeriodHours} hours but goes round in ${orbitHours} — it will not keep one face to its planet.` + ); + } + if (body.massRatio !== undefined) { + // The pair's barycentre, which the planet's elements place, must lie outside the planet — + // that is why the two are drawn going round it — and nearer the planet than the moon. + const offsetKm = (body.orbit.semiMajorAxisAu * KM_PER_AU * body.massRatio) / (1 + body.massRatio); + assertCondition( + body.massRatio > 0 && body.massRatio < 1 && offsetKm > parent!.radiusKm, + `${body.name}'s mass ratio ${body.massRatio} puts its barycentre ${offsetKm.toFixed(0)} km from ${parent!.name}'s centre, which is not between its surface, ${parent!.radiusKm} km out, and the moon.` + ); + } } } const planetCount = bodies.filter((body) => body.kind === 'planet').length; assertCondition(planetCount === 8, `Expected 8 planets, found ${planetCount}.`); + const dwarfCount = bodies.filter((body) => body.kind === 'dwarf').length; + assertCondition(dwarfCount === 5, `Expected the IAU's 5 dwarf planets, found ${dwarfCount}.`); console.log(` mean elements against Horizons, degrees: ${offsets.join(', ')}.`); } diff --git a/tools/etl/fetchSolarSystem.ts b/tools/etl/fetchSolarSystem.ts index 516e839..bbfb8b1 100644 --- a/tools/etl/fetchSolarSystem.ts +++ b/tools/etl/fetchSolarSystem.ts @@ -3,8 +3,8 @@ import { writeFileSync } from 'node:fs'; import { BodyRecord, OrbitalElements } from '../../src/app/shared/models/body.model'; import { SUN_STAR_ID } from '../../src/app/shared/models/star.model'; import { fetchHorizonsBody } from './lib/horizons'; -import { parsePlanetMeanElements, parseSatelliteMeanElements } from '../../src/app/shared/astro/mean-elements'; -import { fetchPlanetMeanElementsText, fetchSatelliteMeanElementsHtml } from './lib/mean-elements'; +import { MeanOrbit, parsePlanetMeanElements, parseSatelliteMeanElements, parseSmallBodyElements } from '../../src/app/shared/astro/mean-elements'; +import { fetchPlanetMeanElementsText, fetchSatelliteMeanElementsHtml, fetchSmallBodyAnswer } from './lib/mean-elements'; import { dataPath, ensureDataDir } from './lib/paths'; const HOURS_PER_DAY = 24; @@ -23,8 +23,41 @@ interface BodySpec { obliquityDeg?: number; /** The periapsis turns backwards; see `parseSatelliteMeanElements`. */ apsidesRegress?: boolean; + /** The pole of the planet's equator, where JPL gives its moons against that plane. */ + equatorPole?: { raDeg: number; decDeg: number }; + /** The Small-Body Database's name for a dwarf planet past Standish's tables: its orbit comes from there. */ + sbdb?: string; + /** A measured mean radius, in km, for a body neither Horizons nor the SBDB gives one for. */ + radiusKm?: number; + /** A moon that does not keep one face to its planet: its page's own spin, or none, is kept. */ + spinsFreely?: boolean; + /** A moon heavy enough to move its planet round their barycentre visibly; see `BodyRecord.massRatio`. */ + barycentric?: boolean; + /** + * Corrections to a row of the satellite table, each where the row disagrees with JPL's own + * Horizons ephemeris and the reason is known; see the specs that carry them. + */ + nodeOffsetDeg?: number; + epochJd?: number; + periodDays?: number; } +/** + * The poles of the equators JPL refers Uranus's and Pluto's moons to, from the IAU WGCCRE 2015 + * report, each taken at the end the table's inclinations are measured from (Titania 0.079 + * degrees, Charon 0.080): the end the moons go round anticlockwise. For Pluto that is the pole + * the IAU gives, 132.993 / -6.163, which for dwarf planets follows the right-hand rule. For + * Uranus the IAU gives the other end, 257.311 / -15.175, named north because it lies on the + * ecliptic's north side; the table measures inclinations from 77.311 / 15.175 but counts its + * nodes from where the equator rises through the ICRF equator going round the IAU's pole, which + * is 180 degrees from where it rises going round this one: hence Uranus's moons' 180-degree node + * offset. Read with this pole and no offset, Ariel was 180 degrees from Horizons at every date + * from 1980 to 2100; read against the IAU's pole, anywhere from 1 to 179. + */ +const URANUS_EQUATOR_POLE = { raDeg: 77.311, decDeg: 15.175 }; +const PLUTO_EQUATOR_POLE = { raDeg: 132.993, decDeg: -6.163 }; +const URANUS_MOON = { kind: 'moon', center: '500@799', parentBodyId: 'uranus', equatorPole: URANUS_EQUATOR_POLE, nodeOffsetDeg: 180 } as const; + // Sun-centered planets/dwarf, then their major moons (planetocentric elements). const BODY_SPECS: BodySpec[] = [ { id: 'mercury', name: 'Mercury', kind: 'planet', horizonsCommand: '199', center: '500@10' }, @@ -36,6 +69,16 @@ const BODY_SPECS: BodySpec[] = [ { id: 'uranus', name: 'Uranus', kind: 'planet', horizonsCommand: '799', center: '500@10' }, { id: 'neptune', name: 'Neptune', kind: 'planet', horizonsCommand: '899', center: '500@10' }, { id: 'pluto', name: 'Pluto', kind: 'dwarf', horizonsCommand: '999', center: '500@10', obliquityDeg: 119.6 }, + { id: 'ceres', name: 'Ceres', kind: 'dwarf', horizonsCommand: '1;', center: '500@10', sbdb: 'Ceres' }, + // Eris, Haumea and Makemake have no radius in the SBDB, the Horizons pages ("RAD= n.a.") or the + // IAU WGCCRE 2015 report, so each carries its stellar-occultation measurement. Eris: 1163 km, + // Sicardy et al. 2011 (Nature 478, 493). Haumea is triaxial, 1161 x 852 x 513 km, Ortiz et al. + // 2017 (Nature 550, 219); drawn as a sphere, at the radius of the sphere of the same volume. + // Makemake: 1434 km across its equator and 1422 across its projected pole, Brown 2013 (ApJ 767, + // L7); the same mean. + { id: 'eris', name: 'Eris', kind: 'dwarf', horizonsCommand: '136199;', center: '500@10', sbdb: 'Eris', radiusKm: 1163 }, + { id: 'haumea', name: 'Haumea', kind: 'dwarf', horizonsCommand: '136108;', center: '500@10', sbdb: 'Haumea', radiusKm: 797.6 }, + { id: 'makemake', name: 'Makemake', kind: 'dwarf', horizonsCommand: '136472;', center: '500@10', sbdb: 'Makemake', radiusKm: 715 }, { id: 'moon', name: 'Moon', kind: 'moon', horizonsCommand: '301', center: '500@399', parentBodyId: 'earth' }, { id: 'phobos', name: 'Phobos', kind: 'moon', horizonsCommand: '401', center: '500@499', parentBodyId: 'mars' }, { id: 'deimos', name: 'Deimos', kind: 'moon', horizonsCommand: '402', center: '500@499', parentBodyId: 'mars' }, @@ -43,15 +86,45 @@ const BODY_SPECS: BodySpec[] = [ { id: 'europa', name: 'Europa', kind: 'moon', horizonsCommand: '502', center: '500@599', parentBodyId: 'jupiter', apsidesRegress: true }, { id: 'ganymede', name: 'Ganymede', kind: 'moon', horizonsCommand: '503', center: '500@599', parentBodyId: 'jupiter' }, { id: 'callisto', name: 'Callisto', kind: 'moon', horizonsCommand: '504', center: '500@599', parentBodyId: 'jupiter' }, + { id: 'mimas', name: 'Mimas', kind: 'moon', horizonsCommand: '601', center: '500@699', parentBodyId: 'saturn' }, + { id: 'enceladus', name: 'Enceladus', kind: 'moon', horizonsCommand: '602', center: '500@699', parentBodyId: 'saturn' }, + { id: 'tethys', name: 'Tethys', kind: 'moon', horizonsCommand: '603', center: '500@699', parentBodyId: 'saturn' }, + { id: 'dione', name: 'Dione', kind: 'moon', horizonsCommand: '604', center: '500@699', parentBodyId: 'saturn' }, + { id: 'rhea', name: 'Rhea', kind: 'moon', horizonsCommand: '605', center: '500@699', parentBodyId: 'saturn' }, { id: 'titan', name: 'Titan', kind: 'moon', horizonsCommand: '606', center: '500@699', parentBodyId: 'saturn' }, - { id: 'triton', name: 'Triton', kind: 'moon', horizonsCommand: '801', center: '500@899', parentBodyId: 'neptune' } + // Hyperion tumbles ("Rotational period = Chaotic") and Phoebe, captured, turns in 9.27 hours. + { id: 'hyperion', name: 'Hyperion', kind: 'moon', horizonsCommand: '607', center: '500@699', parentBodyId: 'saturn', spinsFreely: true }, + { 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 + // 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 + // 25 degrees from Horizons by 2025 and 100 by 2075; on the current period, within 2.0 from 1980 + // to 2100. + { 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: 'ariel', name: 'Ariel', horizonsCommand: '701', ...URANUS_MOON }, + { id: 'umbriel', name: 'Umbriel', horizonsCommand: '702', ...URANUS_MOON }, + { id: 'titania', name: 'Titania', horizonsCommand: '703', ...URANUS_MOON }, + { id: 'oberon', name: 'Oberon', horizonsCommand: '704', ...URANUS_MOON }, + { 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. + { id: 'nereid', name: 'Nereid', kind: 'moon', horizonsCommand: '802', center: '500@899', parentBodyId: 'neptune', spinsFreely: true }, + { 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 + // 2000-01-01.5, and read at 1.0 Charon sat 27.8 to 28.2 degrees — half a day of its motion is + // 28.2 — from Horizons at every date from 1980 to 2100. At 1.5 it is within 0.4. + { id: 'charon', name: 'Charon', kind: 'moon', horizonsCommand: '901', center: '500@999', parentBodyId: 'pluto', equatorPole: PLUTO_EQUATOR_POLE, epochJd: 2451545.0, barycentric: true } ]; +/** The moons whose day is not their orbit; `build.ts` holds every other moon to its lock. */ +export const FREELY_SPINNING_MOONS = new Set(BODY_SPECS.filter((spec) => spec.spinsFreely).map((spec) => spec.id)); + /** - * Writes `bodies.json` for the major planets, Pluto, and a curated set of major moons: JPL's - * mean orbital elements for where they go, and JPL Horizons for their size and spin. Horizons' - * osculating elements for the same date come back alongside, for `build.ts` to check the mean - * ones against. + * Writes `bodies.json` for the major planets, the five dwarf planets, and every moon in JPL's + * mean-element table more than 100 km in mean radius — Phoebe, at 106.6, the smallest: JPL's + * mean orbital elements for where they go, or the SBDB's osculating ones where there are none, + * and JPL Horizons for their size and spin. Horizons' osculating elements for the same date come + * back alongside, for `build.ts` to check the mean ones against. */ export async function fetchSolarSystem(): Promise<{ bodies: BodyRecord[]; horizonsOrbits: Map }> { console.log(`Fetching ${BODY_SPECS.length} solar-system bodies from JPL (mean elements, Horizons)...`); @@ -59,6 +132,7 @@ export async function fetchSolarSystem(): Promise<{ bodies: BodyRecord[]; horizo const horizonsOrbits = new Map(); const planetElements = await fetchPlanetMeanElementsText(); const satelliteElements = await fetchSatelliteMeanElementsHtml(); + const gmById = new Map(); for (const spec of BODY_SPECS) { const result = await fetchHorizonsBody({ @@ -68,23 +142,44 @@ export async function fetchSolarSystem(): Promise<{ bodies: BodyRecord[]; horizo }); horizonsOrbits.set(spec.id, result.orbit); - if (result.radiusKm === undefined) { + gmById.set(spec.id, result.gmKm3PerS2); + + const parentName = BODY_SPECS.find((candidate) => candidate.id === spec.parentBodyId)?.name; + const smallBody = spec.sbdb ? parseSmallBodyElements(await fetchSmallBodyAnswer(spec.sbdb, `sbdb-${spec.id}.json`)) : undefined; + const read: MeanOrbit = + smallBody ?? + (parentName + ? parseSatelliteMeanElements(satelliteElements, parentName, spec.name, spec.apsidesRegress ?? false, spec.equatorPole) + : parsePlanetMeanElements(planetElements, spec.id)); + const mean: MeanOrbit = { + ...read, + orbit: { + ...read.orbit, + longitudeOfAscendingNodeDeg: read.orbit.longitudeOfAscendingNodeDeg + (spec.nodeOffsetDeg ?? 0), + epochJd: spec.epochJd ?? read.orbit.epochJd + }, + rates: spec.periodDays ? { ...read.rates, meanMotionDegPerDay: 360 / spec.periodDays } : read.rates + }; + const radiusKm = smallBody?.radiusKm ?? spec.radiusKm ?? result.radiusKm; + if (radiusKm === undefined) { console.warn(` no physical radius found for ${spec.name}; defaulting to 0.`); } - const parentName = BODY_SPECS.find((candidate) => candidate.id === spec.parentBodyId)?.name; - const mean = parentName - ? parseSatelliteMeanElements(satelliteElements, parentName, spec.name, spec.apsidesRegress ?? false) - : parsePlanetMeanElements(planetElements, spec.id); - - // Every moon listed here is tidally locked, so its day is its orbit: the sidereal period from - // the same mean motion that carries it round, which keeps one face towards the parent however - // long the clock runs. Not every page says so — the Moon's gives a rate, Titan's nothing. The - // Kepler period of the osculating orbit this used to take, 27.70 days for the Moon, would now - // turn its face five degrees an orbit away from the orbit it is drawn on. - const rotationPeriodHours = result.tidallyLocked || spec.kind === 'moon' + // A moon listed here is tidally locked unless its spec says otherwise, so its day is its + // orbit: the sidereal period from the same mean motion that carries it round, which keeps one + // face towards the parent however long the clock runs. Not every page says so — the Moon's + // gives a rate, Titan's and Proteus's nothing. The Kepler period of the osculating orbit this + // used to take, 27.70 days for the Moon, would now turn its face five degrees an orbit away + // from the orbit it is drawn on. + const rotationPeriodHours = result.tidallyLocked || (spec.kind === 'moon' && !spec.spinsFreely) ? (360 / mean.rates.meanMotionDegPerDay) * HOURS_PER_DAY - : result.rotationPeriodHours; + : smallBody + ? smallBody.rotationPeriodHours + : result.rotationPeriodHours; + const parentGm = spec.barycentric && spec.parentBodyId ? gmById.get(spec.parentBodyId) : undefined; + if (spec.barycentric && (result.gmKm3PerS2 === undefined || parentGm === undefined)) { + throw new Error(`${spec.name} and its planet need a GM each to place their barycentre.`); + } if (rotationPeriodHours === undefined) { console.warn(` no rotation period found for ${spec.name}; it will not turn.`); } @@ -94,12 +189,13 @@ export async function fetchSolarSystem(): Promise<{ bodies: BodyRecord[]; horizo systemStarId: SUN_STAR_ID, name: spec.name, kind: spec.kind, - radiusKm: result.radiusKm ?? 0, + radiusKm: radiusKm ?? 0, orbit: mean.orbit, rates: mean.rates, ...(mean.laplacePole ? { laplacePole: mean.laplacePole } : {}), orbitSource: mean.orbitSource, ...(spec.parentBodyId ? { parentBodyId: spec.parentBodyId } : {}), + ...(parentGm !== undefined ? { massRatio: result.gmKm3PerS2! / parentGm } : {}), ...(rotationPeriodHours !== undefined ? { rotationPeriodHours } : {}), ...((result.obliquityDeg ?? spec.obliquityDeg) !== undefined ? { obliquityDeg: result.obliquityDeg ?? spec.obliquityDeg } : {}) });