Warm a host's planets by the luminosity the archive publishes for it, not one derived beside it

The ETL has carried each host's st_lum since 494fb56 and nothing read it: the card, the system
view's planet temperatures and the body pages all derived a luminosity from the star's magnitude
and colour. For 757 of the 4 440 hosts the archive gives one for, the two differ by more than
1.5 times, 667 of them stars the ETL placed from the archive's own V and B−V. Proxima read
8.9×10⁻⁴ L☉ beside the archive's radius and temperature, which imply 1.27×10⁻³; the archive gives
1.51×10⁻³, as Ribas et al. 2017 do.

starSurfaceOf now returns the luminosity with the radius and temperature, taken from the first of
the host's planets that gives one, as st_rad and st_teff already are, and derived only otherwise.
The system view lights its planets with it, the body page uses the same host rule, and the card
marks it derived only when it is. The derived radius still comes from the derived luminosity, so
its "from colour and brightness" stays true; only 3 hosts have st_lum and no st_rad.

Measured on the shipped catalogue against the parent commit: 544 of 3 639 planets with an
equilibrium temperature move by more than 10 %, and 98 change class. Kepler-186 f goes from a 292 K
rocky world to a 175 K icy one, LHS 1140 b from 152 K icy to 206 K temperate, LTT 1445 A b from
298 K rocky to 402 K scorched, Proxima Cen b from 200 to 228 K and d from 259 to 296 K.

starReadouts now takes the surface alone, which carries the luminosity. Controls, each failing its
named test: the archive's luminosity ignored (2 of 778 failed), called derived, the body page
warmed by the derived one, the body page reading the planet's own row only, and the card marking
a published luminosity derived (1 of 778 each).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-29 19:47:34 +02:00
co-authored by Claude Opus 5.5
parent 869635bec1
commit d94451e203
5 changed files with 75 additions and 45 deletions
@@ -146,17 +146,28 @@ describe('starSurfaceOf', () => {
const proximaB: ExoplanetRecord = { id: 'proxima-cen-b', hostStarId: 70666, hostStarName: 'Proxima Cen', name: 'Proxima Cen b', orbit: { semiMajorAxisAu: 0.0485 } }; 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", () => { it("is the Sun's own for the Sun, and not derived", () => {
expect(starSurfaceOf(sun, [])).toEqual({ radiusSolar: 1, radiusDerived: false, temperatureK: 5772 }); expect(starSurfaceOf(sun, [])).toEqual({ radiusSolar: 1, radiusDerived: false, temperatureK: 5772, luminositySolar: 1, luminosityDerived: false });
}); });
it("takes a host's radius and temperature from the archive", () => { it("takes a host's radius, temperature and luminosity from the archive", () => {
const surface = starSurfaceOf(proxima, [{ ...proximaB, hostStarRadiusSolar: 0.141, hostStarTemperatureK: 2900 }]); const surface = starSurfaceOf(proxima, [{ ...proximaB, hostStarRadiusSolar: 0.141, hostStarTemperatureK: 2900, hostStarLuminositySolar: 0.00151 }]);
expect(surface).toEqual({ radiusSolar: 0.141, radiusDerived: false, temperatureK: 2900 }); expect(surface).toEqual({ radiusSolar: 0.141, radiusDerived: false, temperatureK: 2900, luminositySolar: 0.00151, luminosityDerived: false });
});
it('warms a planet by the luminosity the archive gives its host, on its own page as in its system', () => {
// 8.9×10⁻⁴ L☉ from Proxima's V and B−V, which put b at 200 K; the archive's 1.51×10⁻³ at 228 K.
const b = { ...proximaB, hostStarLuminositySolar: 0.00151 };
const catalogues = { bodies: [], exoplanets: [b, { ...proximaB, id: 'proxima-cen-d', name: 'Proxima Cen d', orbit: { semiMajorAxisAu: 0.02881 } }], stars: [proxima] };
expect(buildBodyViewModel('proxima-cen-b', catalogues)?.appearance.equilibriumTemperatureK).toBeCloseTo(228, 0);
// d's own row gives none; its host's luminosity is still the archive's, from b's.
expect(buildBodyViewModel('proxima-cen-d', catalogues)?.appearance.equilibriumTemperatureK).toBeCloseTo(296, 0);
}); });
it('derives both otherwise, and says the radius is derived', () => { it('derives both otherwise, and says the radius is derived', () => {
const surface = starSurfaceOf(proxima, [proximaB]); const surface = starSurfaceOf(proxima, [proximaB]);
expect(surface.radiusDerived).toBe(true); expect(surface.radiusDerived).toBe(true);
expect(surface.luminosityDerived).toBe(true);
expect(surface.luminositySolar).toBeCloseTo(0.00088, 5);
// Its colour reads as an M5 dwarf: 3 068 K and 0.105 R☉, against 2 900 K and 0.154 R☉ // 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. // measured (Kervella et al. 2017). B−V barely changes along the late M dwarfs.
expect(surface.temperatureK).toBeCloseTo(3068, -1); expect(surface.temperatureK).toBeCloseTo(3068, -1);
+28 -14
View File
@@ -39,39 +39,49 @@ function photometryOf(star: StarRecord): StellarPhotometry {
}; };
} }
/** How big and how hot a star is, and whether the radius was measured or derived here. */ /** How big, how hot and how bright a star is, and whether each was measured or derived here. */
export interface StarSurface { export interface StarSurface {
/** Solar radii; `null` without a published radius, a measured magnitude, and a colour or type. */ /** Solar radii; `null` without a published radius, a measured magnitude, and a colour or type. */
radiusSolar: number | null; radiusSolar: number | null;
radiusDerived: boolean; radiusDerived: boolean;
temperatureK: number | null; temperatureK: number | null;
/** Solar luminosities, what its planets are warmed by; `null` where there is neither. */
luminositySolar: number | null;
luminosityDerived: boolean;
} }
/** /**
* A star's radius and effective temperature: the archive's `st_rad` and `st_teff` for a planet * A star's radius, effective temperature and luminosity: the archive's `st_rad`, `st_teff` and
* host, from any of its planets' rows, and otherwise derived — the temperature off the dwarf * `st_lum` for a planet host, from any of its planets' rows, and otherwise derived — the
* sequence at the star's colour, the radius from that and the luminosity (Stefan-Boltzmann). * temperature off the dwarf sequence at the star's colour, the luminosity from its magnitude
* The Sun's are its own, the nominal values the rest are measured in. * (`luminosityOf`), the radius from those two (Stefan-Boltzmann). The Sun's are its own, the
* nominal values the rest are measured in.
* *
* Derived radii land within a factor of 1.5 of the archive's for 97 % of the 1 447 catalogue * Derived radii land within a factor of 1.5 of the archive's for 97 % of the 1 447 catalogue
* hosts that have both, and within 0.018 dex at the median. * hosts that have both, and within 0.018 dex at the median. Derived luminosities fare worse: 757
* of the 4 440 hosts the archive gives one for were off by more than that factor, 667 of them
* stars placed from the archive's own V and B−V, and Proxima read 8.9×10⁻⁴ L☉ against the
* archive's 1.51×10⁻³ beside the radius and temperature it was drawn with, which imply 1.27×10⁻³.
*/ */
export function starSurfaceOf(star: StarRecord, planets: readonly ExoplanetRecord[]): StarSurface { export function starSurfaceOf(star: StarRecord, planets: readonly ExoplanetRecord[]): StarSurface {
if (star.id === SUN_STAR_ID) { if (star.id === SUN_STAR_ID) {
return { radiusSolar: 1, radiusDerived: false, temperatureK: SOLAR_EFFECTIVE_TEMPERATURE_K }; return { radiusSolar: 1, radiusDerived: false, temperatureK: SOLAR_EFFECTIVE_TEMPERATURE_K, luminositySolar: 1, luminosityDerived: false };
} }
const temperatureK = planets.find((planet) => planet.hostStarTemperatureK)?.hostStarTemperatureK ?? effectiveTemperatureK(photometryOf(star)); const temperatureK = planets.find((planet) => planet.hostStarTemperatureK)?.hostStarTemperatureK ?? effectiveTemperatureK(photometryOf(star));
const measured = planets.find((planet) => planet.hostStarRadiusSolar)?.hostStarRadiusSolar; const published = planets.find((planet) => planet.hostStarLuminositySolar)?.hostStarLuminositySolar;
if (measured) {
return { radiusSolar: measured, radiusDerived: false, temperatureK };
}
// None from a stand-in magnitude: PSR J1719-1438 came out 2.3 solar radii, wider than its // None from a stand-in magnitude: PSR J1719-1438 came out 2.3 solar radii, wider than its
// planet's orbit. // planet's orbit.
const luminosity = luminosityOf(star); const derived = luminosityOf(star);
const luminosity = { luminositySolar: published ?? derived, luminosityDerived: published === undefined };
const measured = planets.find((planet) => planet.hostStarRadiusSolar)?.hostStarRadiusSolar;
if (measured) {
return { radiusSolar: measured, radiusDerived: false, temperatureK, ...luminosity };
}
return { return {
radiusSolar: luminosity !== null && temperatureK !== null ? radiusFromLuminositySolar(luminosity, temperatureK) : null, radiusSolar: derived !== null && temperatureK !== null ? radiusFromLuminositySolar(derived, temperatureK) : null,
radiusDerived: true, radiusDerived: true,
temperatureK, temperatureK,
...luminosity,
}; };
} }
@@ -118,7 +128,11 @@ export function buildBodyViewModel(id: string, catalogues: BodyCatalogues): Body
massEarth: exoplanet.massEarth, massEarth: exoplanet.massEarth,
discoveryYear: exoplanet.discoveryYear, discoveryYear: exoplanet.discoveryYear,
orbit: exoplanet.orbit, orbit: exoplanet.orbit,
appearance: appearanceForExoplanet(exoplanet, luminosityOf(hostStar)), // Warmed by what the system view warms it by: the archive's luminosity where it has one.
appearance: appearanceForExoplanet(
exoplanet,
hostStar ? starSurfaceOf(hostStar, catalogues.exoplanets.filter((candidate) => candidate.hostStarId === hostStar.id)).luminositySolar : null,
),
hasPhotography: bodyTexturePath(exoplanet.id) !== undefined, hasPhotography: bodyTexturePath(exoplanet.id) !== undefined,
// `periodDays` is populated for none of the shipped records, and deriving one would need the // `periodDays` is populated for none of the shipped records, and deriving one would need the
// host star's mass, which is equally absent. Left undefined rather than assuming a solar-mass // host star's mass, which is equally absent. Left undefined rather than assuming a solar-mass
@@ -57,7 +57,7 @@ import {
type ScaleBar, type ScaleBar,
} from '../../shared/format/scale-bar'; } from '../../shared/format/scale-bar';
import { BodyDetailViewModel } from '../body-detail/body-detail.model'; import { BodyDetailViewModel } from '../body-detail/body-detail.model';
import { buildBodyViewModel, luminosityOf, starSurfaceOf, StarSurface } from '../body-detail/body-view-model'; import { buildBodyViewModel, starSurfaceOf, StarSurface } from '../body-detail/body-view-model';
import { import {
DEFAULT_HUD_DISPLAY, DEFAULT_HUD_DISPLAY,
HudDisplay, HudDisplay,
@@ -1784,7 +1784,7 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
label: 'Bodies', label: 'Bodies',
value: moonCount > 0 ? `${planetCount} + ${moonCount} moons` : `${planetCount}`, value: moonCount > 0 ? `${planetCount} + ${moonCount} moons` : `${planetCount}`,
}, },
...starReadouts(star, luminosityOf(star), this.currentStarSurface), ...starReadouts(star, this.currentStarSurface),
]); ]);
this.hudNote.set(this.time.atNow() ? 'Orbits propagated from published elements to the current date.' : 'Orbits propagated from published elements to the date on the clock.'); this.hudNote.set(this.time.atNow() ? 'Orbits propagated from published elements to the current date.' : 'Orbits propagated from published elements to the date on the clock.');
this.hudRange.set( this.hudRange.set(
@@ -2220,19 +2220,18 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
// The star's own position is the line of sight to it, which is the plane the archive // The star's own position is the line of sight to it, which is the plane the archive
// measures exoplanet inclinations against. The Sun sits at the origin and has no // measures exoplanet inclinations against. The Sun sits at the origin and has no
// exoplanets, so it has no meaningful direction and the renderer falls back. // exoplanets, so it has no meaningful direction and the renderer falls back.
// The star's luminosity, derived from its own catalogued magnitude and distance, is what
// decides how hot each body in the system is — and so what each of them looks like.
const hostLuminosity = luminosityOf(star);
// Every star at its own radius: the archive's for a planet host, otherwise derived from its // Every star at its own radius: the archive's for a planet host, otherwise derived from its
// colour and brightness — or the Sun's, for the 3 077 stars with no measured magnitude or with // colour and brightness — or the Sun's, for the 3 077 stars with no measured magnitude or with
// neither a colour nor a type, which the card then gives no radius. Its temperature is the // neither a colour nor a type, which the card then gives no radius. Its temperature is the
// colour of its disc and of the light it casts. // colour of its disc and of the light it casts, and its luminosity — the archive's, or else
// derived from its magnitude and distance — decides how hot each body in the system is, and
// so what each of them looks like.
this.currentStarSurface = starSurfaceOf(star, systemExoplanets); this.currentStarSurface = starSurfaceOf(star, systemExoplanets);
this.systemRenderer = new SystemOrbitsRenderer( this.systemRenderer = new SystemOrbitsRenderer(
systemBodies, systemBodies,
systemExoplanets, systemExoplanets,
{ x: star.x, y: star.y, z: star.z }, { x: star.x, y: star.y, z: star.z },
hostLuminosity, this.currentStarSurface.luminositySolar,
this.currentStarSurface.temperatureK, this.currentStarSurface.temperatureK,
); );
this.systemGroup.add(this.systemRenderer.object); this.systemGroup.add(this.systemRenderer.object);
@@ -44,43 +44,46 @@ function value(readouts: { label: string; value: string }[], label: string): str
describe('starReadouts', () => { describe('starReadouts', () => {
it('names the band of the magnitude, and which colour the colour index is', () => { it('names the band of the magnitude, and which colour the colour index is', () => {
expect(value(starReadouts(HYG_STAR, null), 'Magnitude')).toBe('V -1.44'); expect(value(starReadouts(HYG_STAR), 'Magnitude')).toBe('V -1.44');
expect(value(starReadouts(GAIA_STAR, null), 'Magnitude')).toBe('G 11.20'); expect(value(starReadouts(GAIA_STAR), 'Magnitude')).toBe('G 11.20');
expect(value(starReadouts(HYG_STAR, null), 'Colour')).toBe('B−V 0.01'); expect(value(starReadouts(HYG_STAR), 'Colour')).toBe('B−V 0.01');
expect(value(starReadouts(GAIA_STAR, null), 'Colour')).toBe('BP−RP 1.43'); expect(value(starReadouts(GAIA_STAR), 'Colour')).toBe('BP−RP 1.43');
}); });
it('says a stand-in magnitude was not measured, and leaves out a colour there is none of', () => { it('says a stand-in magnitude was not measured, and leaves out a colour there is none of', () => {
const unmeasured = starReadouts({ ...GAIA_STAR, magnitude: 12, magnitudeBand: undefined, colorIndex: null, colorSystem: undefined }, null); const unmeasured = starReadouts({ ...GAIA_STAR, magnitude: 12, magnitudeBand: undefined, colorIndex: null, colorSystem: undefined });
expect(value(unmeasured, 'Magnitude')).toBe('Not measured'); expect(value(unmeasured, 'Magnitude')).toBe('Not measured');
expect(value(unmeasured, 'Colour')).toBeUndefined(); expect(value(unmeasured, 'Colour')).toBeUndefined();
}); });
it('gives the distance with its uncertainty', () => { it('gives the distance with its uncertainty', () => {
expect(value(starReadouts(GAIA_STAR, null), 'Distance')).toBe('117 ± 12 pc'); expect(value(starReadouts(GAIA_STAR), 'Distance')).toBe('117 ± 12 pc');
}); });
it('names the catalogue a star comes from, and whose distance it has', () => { it('names the catalogue a star comes from, and whose distance it has', () => {
expect(value(starReadouts(GAIA_STAR, null), 'Source')).toBe('Gaia DR3'); expect(value(starReadouts(GAIA_STAR), 'Source')).toBe('Gaia DR3');
expect(value(starReadouts(HYG_STAR, null), 'Source')).toBe('HYG'); expect(value(starReadouts(HYG_STAR), 'Source')).toBe('HYG');
expect(value(starReadouts(PLACED_BY_GAIA, null), 'Source')).toBe('HYG, Gaia DR3 distance'); expect(value(starReadouts(PLACED_BY_GAIA), 'Source')).toBe('HYG, Gaia DR3 distance');
expect(value(starReadouts({ ...HYG_STAR, source: 'exoplanet-archive' }, null), 'Source')).toBe('NASA Exoplanet Archive'); expect(value(starReadouts({ ...HYG_STAR, source: 'exoplanet-archive' }), 'Source')).toBe('NASA Exoplanet Archive');
}); });
it('says a radius was derived, and from what; a published one plainly', () => { it('says a radius was derived, and from what; a published one plainly', () => {
expect(starReadouts(PLACED_BY_GAIA, null, { radiusSolar: 0.1049, radiusDerived: true, temperatureK: 3068 }).find((readout) => readout.label === 'Radius')).toEqual({ expect(starReadouts(PLACED_BY_GAIA, { radiusSolar: 0.1049, radiusDerived: true, temperatureK: 3068, luminositySolar: null, luminosityDerived: true }).find((readout) => readout.label === 'Radius')).toEqual({
label: 'Radius', label: 'Radius',
value: '~0.10 solar radii, from colour and brightness', value: '~0.10 solar radii, from colour and brightness',
derived: true derived: true
}); });
expect(value(starReadouts(PLACED_BY_GAIA, null, { radiusSolar: 0.141, radiusDerived: false, temperatureK: 2900 }), 'Radius')).toBe('0.141 solar radii'); expect(value(starReadouts(PLACED_BY_GAIA, { radiusSolar: 0.141, radiusDerived: false, temperatureK: 2900, luminositySolar: null, luminosityDerived: true }), 'Radius')).toBe('0.141 solar radii');
expect(value(starReadouts(HYG_STAR, null, { radiusSolar: 584.3, radiusDerived: true, temperatureK: 3590 }), 'Radius')).toBe('~580 solar radii, from colour and brightness'); expect(value(starReadouts(HYG_STAR, { radiusSolar: 584.3, radiusDerived: true, temperatureK: 3590, luminositySolar: null, luminosityDerived: true }), 'Radius')).toBe('~580 solar radii, from colour and brightness');
expect(value(starReadouts(HYG_STAR, null, { radiusSolar: 1, radiusDerived: false, temperatureK: 5772 }), 'Radius')).toBe('1.00 solar radii'); expect(value(starReadouts(HYG_STAR, { radiusSolar: 1, radiusDerived: false, temperatureK: 5772, luminositySolar: null, luminosityDerived: true }), 'Radius')).toBe('1.00 solar radii');
expect(starReadouts(HYG_STAR, null, { radiusSolar: null, radiusDerived: true, temperatureK: null }).some((readout) => readout.label === 'Radius')).toBe(false); expect(starReadouts(HYG_STAR, { radiusSolar: null, radiusDerived: true, temperatureK: null, luminositySolar: null, luminosityDerived: true }).some((readout) => readout.label === 'Radius')).toBe(false);
}); });
it('marks the luminosity as derived', () => { it('marks a derived luminosity so, and a published one not', () => {
expect(starReadouts(HYG_STAR, 25.4).find((readout) => readout.label === 'Luminosity')).toEqual({ label: 'Luminosity', value: '25.40 L☉', derived: true }); const surface = { radiusSolar: null, radiusDerived: true, temperatureK: null };
expect(starReadouts(HYG_STAR, { ...surface, luminositySolar: 25.4, luminosityDerived: true }).find((readout) => readout.label === 'Luminosity')).toEqual({ label: 'Luminosity', value: '25.40 L☉', derived: true });
expect(starReadouts(PLACED_BY_GAIA, { ...surface, luminositySolar: 0.00151, luminosityDerived: false }).find((readout) => readout.label === 'Luminosity')).toEqual({ label: 'Luminosity', value: '0.002 L☉' });
expect(starReadouts(HYG_STAR, { ...surface, luminositySolar: null, luminosityDerived: true }).some((readout) => readout.label === 'Luminosity')).toBe(false);
}); });
}); });
@@ -34,9 +34,10 @@ export function starSubtitle(star: StarRecord): string {
/** /**
* A star's measured readouts, each with what it was measured in: the band of its magnitude, which * A star's measured readouts, each with what it was measured in: the band of its magnitude, which
* colour its colour index is, the distance's uncertainty, and the catalogues they come from. * colour its colour index is, the distance's uncertainty, and the catalogues they come from.
* `luminosity` is derived, and marked so; so is a radius derived from it, and it says from what. * The luminosity and radius are the archive's where it publishes them, and otherwise derived and
* marked so; a derived radius also says from what.
*/ */
export function starReadouts(star: StarRecord, luminosity: number | null, surface?: StarSurface): HudReadout[] { export function starReadouts(star: StarRecord, surface?: StarSurface): HudReadout[] {
const distancePc = Math.hypot(star.x, star.y, star.z); const distancePc = Math.hypot(star.x, star.y, star.z);
const catalogue = describingCatalogue(star); const catalogue = describingCatalogue(star);
return [ return [
@@ -48,7 +49,9 @@ export function starReadouts(star: StarRecord, luminosity: number | null, surfac
...(star.colorIndex !== null ...(star.colorIndex !== null
? [{ label: 'Colour', value: `${star.colorSystem === 'BP-RP' ? 'BP−RP' : 'B−V'} ${star.colorIndex.toFixed(2)}` }] ? [{ label: 'Colour', value: `${star.colorSystem === 'BP-RP' ? 'BP−RP' : 'B−V'} ${star.colorIndex.toFixed(2)}` }]
: []), : []),
...(luminosity !== null ? [{ label: 'Luminosity', value: formatLuminosity(luminosity), derived: true }] : []), ...(surface?.luminositySolar
? [{ label: 'Luminosity', value: formatLuminosity(surface.luminositySolar), ...(surface.luminosityDerived ? { derived: true } : {}) }]
: []),
...(surface?.radiusSolar ? [radiusReadout(surface.radiusSolar, surface.radiusDerived)] : []), ...(surface?.radiusSolar ? [radiusReadout(surface.radiusSolar, surface.radiusDerived)] : []),
{ label: 'Source', value: catalogue === 'HYG' && star.distanceFromGaia ? 'HYG, Gaia DR3 distance' : catalogue } { label: 'Source', value: catalogue === 'HYG' && star.distanceFromGaia ? 'HYG, Gaia DR3 distance' : catalogue }
]; ];