The system view drew the Sun at its own radius and every other star at 0.45 of its innermost orbit, capped at 0.2 AU: a size chosen so the star would not swallow its planets, not the star's. Proxima Centauri was drawn at 2.8 solar radii, eighteen times its own, and every star without planets at 43. starSurfaceOf (body-view-model.ts) now gives each star a radius and a temperature. A planet host takes the archive's st_rad and st_teff from its planets' rows: 4 439 hosts are drawn at a measured radius, 22 at a derived one. Every other star's is derived: its temperature off Pecaut & Mamajek's dwarf sequence at its colour (the same table the spectral estimate reads, or at the colour its type implies where it has none), its luminosity from its absolute magnitude and the bolometric correction luminositySolar already applies, and R = sqrt(L) / (T / 5772 K)^2. Against the archive's own st_rad for the 1 447 catalogue hosts that have one, the derived radius is within 0.018 dex at the median, 0.071 dex at the 90th percentile, and within a factor of 1.5 for 97.1 %. Sirius comes out 1.79 solar radii (1.711 published, Liebert et al. 2005), Wolf 359 0.117, Betelgeuse 584, the Sun exactly 1. A star with no band has only the ETL's stand-in magnitude, and gets no derived radius: PSR J1719-1438 came out 2.3 solar radii from it, wider than its planet's orbit. With the stars that have neither a colour nor a type, that leaves 3 077 of 455 608 stars (274 of 4 735 hosts) with no radius; they are drawn at the Sun's, and their card gives none. The card says which it is: "Radius 0.141 solar radii" for a published one, "~0.10 solar radii, from colour and brightness" for a derived one, two figures because colour does not give three. A giant drawn at its size can be wider than its system, so systemFramingDistanceAu also makes room for the star, and the controls' closest approach is now three of the star's radii where that is more than the old 0.05 AU. 23 211 stars are drawn wider than 3.6 solar radii, which put 0.05 AU inside three of their radii, and a zoom would have carried the camera through the surface of the largest. The Sun keeps 0.05 AU. starMarkerRadiusAu and the renderer's innermost axis, which only it read, are gone. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
158 lines
5.7 KiB
TypeScript
158 lines
5.7 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
|
||
import { BodyRecord, OrbitalElements } from '../../shared/models/body.model';
|
||
import { ExoplanetRecord } from '../../shared/models/exoplanet.model';
|
||
import { StarRecord, SUN_STAR_ID } from '../../shared/models/star.model';
|
||
import { buildBodyViewModel, heliocentricPeriodDays, starSurfaceOf } from './body-view-model';
|
||
|
||
const orbit = (overrides: Partial<OrbitalElements> = {}): OrbitalElements => ({
|
||
semiMajorAxisAu: 1,
|
||
eccentricity: 0.0167,
|
||
inclinationDeg: 0,
|
||
longitudeOfAscendingNodeDeg: 0,
|
||
argumentOfPeriapsisDeg: 0,
|
||
meanAnomalyAtEpochDeg: 0,
|
||
epochJd: 2451545,
|
||
...overrides,
|
||
});
|
||
|
||
const sun: StarRecord = {
|
||
id: SUN_STAR_ID,
|
||
name: 'Sol',
|
||
x: 0,
|
||
y: 0,
|
||
z: 0,
|
||
magnitude: -26.7,
|
||
spectralType: 'G2V',
|
||
colorIndex: 0.65,
|
||
};
|
||
|
||
const earth: BodyRecord = {
|
||
id: 'earth',
|
||
systemStarId: SUN_STAR_ID,
|
||
name: 'Earth',
|
||
kind: 'planet',
|
||
radiusKm: 6371,
|
||
orbit: orbit(),
|
||
};
|
||
const luna: BodyRecord = {
|
||
id: 'luna',
|
||
systemStarId: SUN_STAR_ID,
|
||
name: 'Moon',
|
||
kind: 'moon',
|
||
radiusKm: 1737,
|
||
parentBodyId: 'earth',
|
||
orbit: orbit({ semiMajorAxisAu: 0.00257 }),
|
||
};
|
||
|
||
describe('heliocentricPeriodDays', () => {
|
||
it('recovers a known period from the semi-major axis alone', () => {
|
||
// P² = a³ in these units, so Earth must come back a year.
|
||
expect(heliocentricPeriodDays(earth)).toBeCloseTo(365.25, 1);
|
||
});
|
||
|
||
it('scales as the three-halves power', () => {
|
||
const jupiter: BodyRecord = {
|
||
...earth,
|
||
id: 'jupiter',
|
||
name: 'Jupiter',
|
||
orbit: orbit({ semiMajorAxisAu: 5.2044 }),
|
||
};
|
||
// Jupiter's real sidereal period is 4332.6 days.
|
||
expect(heliocentricPeriodDays(jupiter)).toBeCloseTo(4335, -1);
|
||
});
|
||
|
||
it('refuses to compute a period for a moon', () => {
|
||
// A moon's elements are relative to its planet, whose mass is not in the catalogue — the
|
||
// same arithmetic would be wrong by the ratio of that planet's mass to the Sun's.
|
||
expect(heliocentricPeriodDays(luna)).toBeUndefined();
|
||
});
|
||
});
|
||
|
||
describe('buildBodyViewModel', () => {
|
||
const catalogues = { bodies: [earth, luna], exoplanets: [] as ExoplanetRecord[], stars: [sun] };
|
||
|
||
it('marks a period computed from the semi-major axis as derived', () => {
|
||
const model = buildBodyViewModel('earth', catalogues);
|
||
expect(model?.orbitalPeriodSource).toBe('derived');
|
||
expect(model?.orbitalPeriodDays).toBeCloseTo(365.25, 1);
|
||
});
|
||
|
||
it('leaves a moon without a period rather than inventing one', () => {
|
||
const model = buildBodyViewModel('luna', catalogues);
|
||
expect(model?.orbitalPeriodDays).toBeUndefined();
|
||
expect(model?.orbitalPeriodSource).toBeUndefined();
|
||
});
|
||
|
||
it('marks a published exoplanet period as measured, not derived', () => {
|
||
const exoplanet: ExoplanetRecord = {
|
||
id: 'kepler-22-b',
|
||
hostStarId: null,
|
||
hostStarName: 'Kepler-22',
|
||
name: 'Kepler-22 b',
|
||
periodDays: 289.9,
|
||
orbit: { semiMajorAxisAu: 0.849 },
|
||
};
|
||
const model = buildBodyViewModel('kepler-22-b', {
|
||
bodies: [],
|
||
exoplanets: [exoplanet],
|
||
stars: [],
|
||
});
|
||
expect(model?.orbitalPeriodSource).toBe('measured');
|
||
expect(model?.orbitalPeriodDays).toBe(289.9);
|
||
});
|
||
|
||
it('leaves an exoplanet with no published period undefined rather than assuming a solar-mass host', () => {
|
||
const exoplanet: ExoplanetRecord = {
|
||
id: 'x',
|
||
hostStarId: null,
|
||
hostStarName: 'X',
|
||
name: 'X b',
|
||
orbit: { semiMajorAxisAu: 0.05 },
|
||
};
|
||
const model = buildBodyViewModel('x', { bodies: [], exoplanets: [exoplanet], stars: [] });
|
||
expect(model?.orbitalPeriodDays).toBeUndefined();
|
||
});
|
||
|
||
it('returns undefined for an id in neither catalogue', () => {
|
||
expect(buildBodyViewModel('nowhere', catalogues)).toBeUndefined();
|
||
});
|
||
|
||
it('carries the host star id, so callers need not rescan the catalogues for it', () => {
|
||
expect(buildBodyViewModel('earth', catalogues)?.hostStarId).toBe(SUN_STAR_ID);
|
||
});
|
||
});
|
||
|
||
describe('starSurfaceOf', () => {
|
||
// Proxima Centauri as HYG describes it, and one of its planets' archive rows.
|
||
const proxima: StarRecord = { id: 70666, name: 'Proxima Centauri', x: 1.2959, y: 0, z: 0, magnitude: 11.01, magnitudeBand: 'V', spectralType: 'M5Ve', colorIndex: 1.807, colorSystem: 'B-V' };
|
||
const proximaB: ExoplanetRecord = { id: 'proxima-cen-b', hostStarId: 70666, hostStarName: 'Proxima Cen', name: 'Proxima Cen b', orbit: { semiMajorAxisAu: 0.0485 } };
|
||
|
||
it("is the Sun's own for the Sun, and not derived", () => {
|
||
expect(starSurfaceOf(sun, [])).toEqual({ radiusSolar: 1, radiusDerived: false, temperatureK: 5772 });
|
||
});
|
||
|
||
it("takes a host's radius and temperature from the archive", () => {
|
||
const surface = starSurfaceOf(proxima, [{ ...proximaB, hostStarRadiusSolar: 0.141, hostStarTemperatureK: 2900 }]);
|
||
expect(surface).toEqual({ radiusSolar: 0.141, radiusDerived: false, temperatureK: 2900 });
|
||
});
|
||
|
||
it('derives both otherwise, and says the radius is derived', () => {
|
||
const surface = starSurfaceOf(proxima, [proximaB]);
|
||
expect(surface.radiusDerived).toBe(true);
|
||
// Its colour reads as an M5 dwarf: 3 068 K and 0.105 R☉, against 2 900 K and 0.154 R☉
|
||
// measured (Kervella et al. 2017). B−V barely changes along the late M dwarfs.
|
||
expect(surface.temperatureK).toBeCloseTo(3068, -1);
|
||
expect(surface.radiusSolar).toBeCloseTo(0.105, 2);
|
||
});
|
||
|
||
it('has no radius for a star with neither a colour nor a type', () => {
|
||
expect(starSurfaceOf({ ...proxima, colorIndex: null, spectralType: 'Unknown' }, []).radiusSolar).toBeNull();
|
||
});
|
||
|
||
it('has none from a magnitude no survey measured', () => {
|
||
// No band: the magnitude is the ETL's stand-in, and the luminosity from it means nothing.
|
||
expect(starSurfaceOf({ ...proxima, magnitudeBand: undefined }, []).radiusSolar).toBeNull();
|
||
});
|
||
});
|