Give Haumea's card its three semi-axes beside its mean radius
Haumea is triaxial, 1161 x 852 x 513 km (Ortiz et al. 2017, Nature 550, 219), and is drawn as the
sphere of its volume, 797.6 km. Its card listed "Radius 798 km" under Measured and said nothing of
its shape, which only a code comment and a commit message gave: its long semi-axis is 1.46 times
that radius and its short one 0.64.
BodyRecord takes semiAxesKm, set by the ETL from Haumea's spec, and the card then reads "Mean
radius 798 km" and "Semi-axes 1,161 x 852 x 513 km". Every other body keeps its one Radius row. The
ETL checks that a body's radius is the mean of its semi-axes, the radius of the sphere of the same
volume, to 0.1 per cent (797.6 against 797.62).
Measured on :4301, Haumea's page: "Mean radius 798 km, Semi-axes 1,161 x 852 x 513 km". Test: the
card of Haumea as shipped in bodies.json. Guarded mutants: the semi-axes dropped from the spec (run
through the solar ETL), the row, or the view model, or the label left as Radius, each fail it and
only it; a radius that is not their mean fails the ETL ("Haumea's radius, 1161 km, is not the mean
of its semi-axes").
Horizons gives triaxial radii for Phobos, Deimos, Miranda and Ariel too, which the page parser
already reads into their mean; they are not carried here.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,8 @@ export interface BodyDetailViewModel {
|
||||
*/
|
||||
hostStarId?: number;
|
||||
radiusKm?: number;
|
||||
/** A triaxial body's semi-axes, where `radiusKm` is the mean of them; see `BodyRecord.semiAxesKm`. */
|
||||
semiAxesKm?: readonly [number, number, number];
|
||||
massEarth?: number;
|
||||
discoveryYear?: number;
|
||||
orbit: Partial<OrbitalElements>;
|
||||
|
||||
@@ -31,7 +31,11 @@ export interface BodyReadouts {
|
||||
export function bodyReadouts(body: BodyDetailViewModel): BodyReadouts {
|
||||
const measured: Readout[] = [];
|
||||
if (body.radiusKm !== undefined) {
|
||||
measured.push({ label: 'Radius', value: formatRadiusKm(body.radiusKm) });
|
||||
// A triaxial body is drawn as the sphere of its volume; a radius alone would hide its shape.
|
||||
measured.push({ label: body.semiAxesKm ? 'Mean radius' : 'Radius', value: formatRadiusKm(body.radiusKm) });
|
||||
}
|
||||
if (body.semiAxesKm) {
|
||||
measured.push({ label: 'Semi-axes', value: `${body.semiAxesKm.map((axis) => axis.toLocaleString('en-GB')).join(' × ')} km` });
|
||||
}
|
||||
if (body.massEarth !== undefined) {
|
||||
measured.push({ label: 'Mass', value: formatMassEarth(body.massEarth) });
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/// <reference types="node" />
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { BodyRecord, OrbitalElements } from '../../shared/models/body.model';
|
||||
@@ -80,6 +83,18 @@ describe('buildBodyViewModel', () => {
|
||||
expect(bodyReadouts(model).measured.find((row) => row.label === 'Eccentricity')?.value).toBe('0.105');
|
||||
});
|
||||
|
||||
it('gives a triaxial body its semi-axes beside its mean radius, not a radius alone', () => {
|
||||
// As shipped: Haumea's shape (Ortiz et al. 2017) travels from the ETL's spec to its card.
|
||||
const shipped: BodyRecord[] = JSON.parse(readFileSync(`${process.cwd()}/src/assets/data/bodies.json`, 'utf8'));
|
||||
const haumea = shipped.find((body) => body.id === 'haumea')!;
|
||||
const measured = bodyReadouts(buildBodyViewModel('haumea', { ...catalogues, bodies: [earth, haumea] })!).measured;
|
||||
expect(measured.find((row) => row.label === 'Mean radius')?.value).toBe('798 km');
|
||||
expect(measured.find((row) => row.label === 'Semi-axes')?.value).toBe('1,161 × 852 × 513 km');
|
||||
expect(measured.find((row) => row.label === 'Radius')).toBeUndefined();
|
||||
// Every other body keeps its one radius.
|
||||
expect(bodyReadouts(buildBodyViewModel('earth', catalogues)!).measured.find((row) => row.label === 'Radius')?.value).toBe('6,371 km');
|
||||
});
|
||||
|
||||
it('says where the orbit comes from, in the card’s provenance', () => {
|
||||
expect(bodyReadouts(buildBodyViewModel('luna', catalogues)!).provenance).toContain('Orbit: JPL SSD satellite mean elements, epoch 2000 Jan 1.');
|
||||
});
|
||||
|
||||
@@ -52,6 +52,7 @@ export function buildBodyViewModel(id: string, catalogues: BodyCatalogues): Body
|
||||
hostStarName: hostStar?.name ?? 'Unknown star',
|
||||
hostStarId: body.systemStarId,
|
||||
radiusKm: body.radiusKm,
|
||||
semiAxesKm: body.semiAxesKm,
|
||||
orbit: body.measuredEccentricity === undefined ? body.orbit : { ...body.orbit, eccentricity: body.measuredEccentricity },
|
||||
appearance: appearanceForBody(body, catalogues.bodies, luminosityOf(hostStar)),
|
||||
hasPhotography: bodyTexturePath(body.id) !== undefined,
|
||||
|
||||
@@ -50,7 +50,14 @@ export interface BodyRecord {
|
||||
systemStarId: number;
|
||||
name: string;
|
||||
kind: 'planet' | 'moon' | 'dwarf';
|
||||
/** Mean radius: for a triaxial body, the radius of the sphere of its volume, which is how it is drawn. */
|
||||
radiusKm: number;
|
||||
/**
|
||||
* A triaxial body's three semi-axes, in km, largest first, where its shape is too far from a
|
||||
* sphere for one radius to say it: Haumea's 1161 x 852 x 513 (Ortiz et al. 2017), whose mean
|
||||
* radius is 798.
|
||||
*/
|
||||
semiAxesKm?: readonly [number, number, number];
|
||||
/** Mean elements at `orbit.epochJd`, moving at `rates`. */
|
||||
orbit: OrbitalElements;
|
||||
rates: MeanElementRates;
|
||||
|
||||
@@ -548,6 +548,11 @@
|
||||
"name": "Haumea",
|
||||
"kind": "dwarf",
|
||||
"radiusKm": 797.6,
|
||||
"semiAxesKm": [
|
||||
1161,
|
||||
852,
|
||||
513
|
||||
],
|
||||
"orbit": {
|
||||
"semiMajorAxisAu": 43.06029023650952,
|
||||
"eccentricity": 0.1944430148898797,
|
||||
|
||||
@@ -325,6 +325,15 @@ function validateBodies(bodies: BodyRecord[], horizonsOrbits: Map<string, Orbita
|
||||
|
||||
// A radius of 0 is what a page whose radius no pattern reads comes out as — Charon's did.
|
||||
assertCondition(body.radiusKm > 0, `Body ${body.id} has no radius; its page states it in a form the ETL does not read.`);
|
||||
// A triaxial body's card gives its mean radius beside its semi-axes, so the two must agree: the
|
||||
// radius of the sphere of the same volume. Measured: Haumea's 797.6 against 797.62.
|
||||
if (body.semiAxesKm) {
|
||||
const volumeRadius = Math.cbrt(body.semiAxesKm[0] * body.semiAxesKm[1] * body.semiAxesKm[2]);
|
||||
assertCondition(
|
||||
Math.abs(volumeRadius / body.radiusKm - 1) < 0.001,
|
||||
`${body.name}'s radius, ${body.radiusKm} km, is not the mean of its semi-axes ${body.semiAxesKm.join(' x ')}, ${volumeRadius.toFixed(1)} km.`
|
||||
);
|
||||
}
|
||||
|
||||
const rotation = body.rotationalElements;
|
||||
assertCondition(
|
||||
|
||||
@@ -34,6 +34,8 @@ interface BodySpec {
|
||||
sbdb?: string;
|
||||
/** A measured mean radius, in km, for a body neither Horizons nor the SBDB gives one for. */
|
||||
radiusKm?: number;
|
||||
/** A triaxial body's semi-axes, in km, largest first, where its card should give its shape; see `BodyRecord.semiAxesKm`. */
|
||||
semiAxesKm?: [number, number, number];
|
||||
/** A measured sidereal day, in hours, where a later measurement overturns the one its source gives. */
|
||||
rotationPeriodHours?: number;
|
||||
/** The eccentricity for the card, where the row the orbit is drawn from gives an outdated one; see `BodyRecord.measuredEccentricity`. */
|
||||
@@ -116,7 +118,7 @@ const BODY_SPECS: BodySpec[] = [
|
||||
// +/- 0.08 hour single peak again with TESS and Gaia, cannot confirm the 22.8, and take 11.4 as
|
||||
// their default. Neither overturns the other; the SBDB's 22.83 is kept, and may be twice the day.
|
||||
{ id: 'eris', name: 'Eris', kind: 'dwarf', horizonsCommand: '136199;', center: '500@10', sbdb: 'Eris', radiusKm: 1163, rotationPeriodHours: 15.771 * 24 },
|
||||
{ id: 'haumea', name: 'Haumea', kind: 'dwarf', horizonsCommand: '136108;', center: '500@10', sbdb: 'Haumea', radiusKm: 797.6 },
|
||||
{ id: 'haumea', name: 'Haumea', kind: 'dwarf', horizonsCommand: '136108;', center: '500@10', sbdb: 'Haumea', radiusKm: 797.6, semiAxesKm: [1161, 852, 513] },
|
||||
{ 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', orbitFromW: {} },
|
||||
@@ -271,6 +273,7 @@ export async function fetchSolarSystem(): Promise<{ bodies: BodyRecord[]; horizo
|
||||
name: spec.name,
|
||||
kind: spec.kind,
|
||||
radiusKm: radiusKm ?? 0,
|
||||
...(spec.semiAxesKm ? { semiAxesKm: spec.semiAxesKm } : {}),
|
||||
orbit: mean.orbit,
|
||||
rates: mean.rates,
|
||||
...(mean.laplacePole ? { laplacePole: mean.laplacePole } : {}),
|
||||
|
||||
Reference in New Issue
Block a user