diff --git a/src/app/features/body-detail/body-view-model.spec.ts b/src/app/features/body-detail/body-view-model.spec.ts index b15fa55..797df68 100644 --- a/src/app/features/body-detail/body-view-model.spec.ts +++ b/src/app/features/body-detail/body-view-model.spec.ts @@ -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 } }; 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", () => { - const surface = starSurfaceOf(proxima, [{ ...proximaB, hostStarRadiusSolar: 0.141, hostStarTemperatureK: 2900 }]); - expect(surface).toEqual({ radiusSolar: 0.141, radiusDerived: false, temperatureK: 2900 }); + it("takes a host's radius, temperature and luminosity from the archive", () => { + const surface = starSurfaceOf(proxima, [{ ...proximaB, hostStarRadiusSolar: 0.141, hostStarTemperatureK: 2900, hostStarLuminositySolar: 0.00151 }]); + 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', () => { const surface = starSurfaceOf(proxima, [proximaB]); 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☉ // measured (Kervella et al. 2017). B−V barely changes along the late M dwarfs. expect(surface.temperatureK).toBeCloseTo(3068, -1); diff --git a/src/app/features/body-detail/body-view-model.ts b/src/app/features/body-detail/body-view-model.ts index f0786e8..dae4bb0 100644 --- a/src/app/features/body-detail/body-view-model.ts +++ b/src/app/features/body-detail/body-view-model.ts @@ -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 { /** Solar radii; `null` without a published radius, a measured magnitude, and a colour or type. */ radiusSolar: number | null; radiusDerived: boolean; 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 - * host, from any of its planets' rows, and otherwise derived — the temperature off the dwarf - * sequence at the star's colour, the radius from that and the luminosity (Stefan-Boltzmann). - * The Sun's are its own, the nominal values the rest are measured in. + * A star's radius, effective temperature and luminosity: the archive's `st_rad`, `st_teff` and + * `st_lum` for a planet host, from any of its planets' rows, and otherwise derived — the + * temperature off the dwarf sequence at the star's colour, the luminosity from its magnitude + * (`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 - * 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 { 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 measured = planets.find((planet) => planet.hostStarRadiusSolar)?.hostStarRadiusSolar; - if (measured) { - return { radiusSolar: measured, radiusDerived: false, temperatureK }; - } + const published = planets.find((planet) => planet.hostStarLuminositySolar)?.hostStarLuminositySolar; // None from a stand-in magnitude: PSR J1719-1438 came out 2.3 solar radii, wider than its // 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 { - radiusSolar: luminosity !== null && temperatureK !== null ? radiusFromLuminositySolar(luminosity, temperatureK) : null, + radiusSolar: derived !== null && temperatureK !== null ? radiusFromLuminositySolar(derived, temperatureK) : null, radiusDerived: true, temperatureK, + ...luminosity, }; } @@ -118,7 +128,11 @@ export function buildBodyViewModel(id: string, catalogues: BodyCatalogues): Body massEarth: exoplanet.massEarth, discoveryYear: exoplanet.discoveryYear, 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, // `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 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 c57cb80..c1ca4f4 100644 --- a/src/app/features/galaxy-system/galaxy-system-scene.component.ts +++ b/src/app/features/galaxy-system/galaxy-system-scene.component.ts @@ -57,7 +57,7 @@ import { type ScaleBar, } from '../../shared/format/scale-bar'; 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 { DEFAULT_HUD_DISPLAY, HudDisplay, @@ -1784,7 +1784,7 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy { label: 'Bodies', 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.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 // 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. - // 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 // 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 - // 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.systemRenderer = new SystemOrbitsRenderer( systemBodies, systemExoplanets, { x: star.x, y: star.y, z: star.z }, - hostLuminosity, + this.currentStarSurface.luminositySolar, this.currentStarSurface.temperatureK, ); this.systemGroup.add(this.systemRenderer.object); diff --git a/src/app/features/galaxy-system/star-readouts.spec.ts b/src/app/features/galaxy-system/star-readouts.spec.ts index 4ad65ae..b4ac341 100644 --- a/src/app/features/galaxy-system/star-readouts.spec.ts +++ b/src/app/features/galaxy-system/star-readouts.spec.ts @@ -44,43 +44,46 @@ function value(readouts: { label: string; value: string }[], label: string): str describe('starReadouts', () => { 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(GAIA_STAR, null), 'Magnitude')).toBe('G 11.20'); - expect(value(starReadouts(HYG_STAR, null), 'Colour')).toBe('B−V 0.01'); - expect(value(starReadouts(GAIA_STAR, null), 'Colour')).toBe('BP−RP 1.43'); + expect(value(starReadouts(HYG_STAR), 'Magnitude')).toBe('V -1.44'); + expect(value(starReadouts(GAIA_STAR), 'Magnitude')).toBe('G 11.20'); + expect(value(starReadouts(HYG_STAR), 'Colour')).toBe('B−V 0.01'); + 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', () => { - 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, 'Colour')).toBeUndefined(); }); 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', () => { - expect(value(starReadouts(GAIA_STAR, null), 'Source')).toBe('Gaia DR3'); - expect(value(starReadouts(HYG_STAR, null), 'Source')).toBe('HYG'); - expect(value(starReadouts(PLACED_BY_GAIA, null), 'Source')).toBe('HYG, Gaia DR3 distance'); - expect(value(starReadouts({ ...HYG_STAR, source: 'exoplanet-archive' }, null), 'Source')).toBe('NASA Exoplanet Archive'); + expect(value(starReadouts(GAIA_STAR), 'Source')).toBe('Gaia DR3'); + expect(value(starReadouts(HYG_STAR), 'Source')).toBe('HYG'); + expect(value(starReadouts(PLACED_BY_GAIA), 'Source')).toBe('HYG, Gaia DR3 distance'); + 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', () => { - 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', value: '~0.10 solar radii, from colour and brightness', 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(HYG_STAR, null, { radiusSolar: 584.3, radiusDerived: true, temperatureK: 3590 }), '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(starReadouts(HYG_STAR, null, { radiusSolar: null, radiusDerived: true, temperatureK: null }).some((readout) => readout.label === 'Radius')).toBe(false); + 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, { 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, { radiusSolar: 1, radiusDerived: false, temperatureK: 5772, luminositySolar: null, luminosityDerived: true }), 'Radius')).toBe('1.00 solar radii'); + 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', () => { - expect(starReadouts(HYG_STAR, 25.4).find((readout) => readout.label === 'Luminosity')).toEqual({ label: 'Luminosity', value: '25.40 L☉', derived: true }); + it('marks a derived luminosity so, and a published one not', () => { + 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); }); }); diff --git a/src/app/features/galaxy-system/star-readouts.ts b/src/app/features/galaxy-system/star-readouts.ts index 5f34149..290859d 100644 --- a/src/app/features/galaxy-system/star-readouts.ts +++ b/src/app/features/galaxy-system/star-readouts.ts @@ -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 * 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 catalogue = describingCatalogue(star); return [ @@ -48,7 +49,9 @@ export function starReadouts(star: StarRecord, luminosity: number | null, surfac ...(star.colorIndex !== null ? [{ 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)] : []), { label: 'Source', value: catalogue === 'HYG' && star.distanceFromGaia ? 'HYG, Gaia DR3 distance' : catalogue } ];