diff --git a/src/app/features/body-detail/body-view-model.ts b/src/app/features/body-detail/body-view-model.ts index becca17..b580d9f 100644 --- a/src/app/features/body-detail/body-view-model.ts +++ b/src/app/features/body-detail/body-view-model.ts @@ -16,7 +16,8 @@ export interface BodyCatalogues { /** * Bolometric luminosity of a star in solar units, from what the catalogue measured: apparent - * magnitude, parallax distance, and a bolometric correction read off the spectral type. + * magnitude in its band, parallax distance, and a bolometric correction read off the colour, or + * off the spectral type where there is no colour. */ export function luminosityOf(star: StarRecord | undefined): number | null { if (!star) { @@ -26,6 +27,9 @@ export function luminosityOf(star: StarRecord | undefined): number | null { magnitude: star.magnitude, distancePc: Math.hypot(star.x, star.y, star.z), spectralType: star.spectralType, + magnitudeBand: star.magnitudeBand, + colorIndex: star.colorIndex, + colorSystem: star.colorSystem, }); } diff --git a/src/app/shared/astro/spectral.spec.ts b/src/app/shared/astro/spectral.spec.ts index b1dcf22..00056a8 100644 --- a/src/app/shared/astro/spectral.spec.ts +++ b/src/app/shared/astro/spectral.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { parseSpectralClass, SPECTRAL_CLASSES, spectralTypeFromColor, spectralTypeToColorIndex, temperatureToColorIndex } from './spectral'; +import { dwarfSequenceAtColor, parseSpectralClass, SPECTRAL_CLASSES, spectralTypeFromColor, spectralTypeToColorIndex, temperatureToColorIndex } from './spectral'; describe('parseSpectralClass', () => { it('reads a clean class and subclass', () => { @@ -128,3 +128,28 @@ describe('spectralTypeFromColor', () => { expect(spectralTypeFromColor(-0.301, 'B-V')).toBe('B0'); }); }); + +describe('dwarfSequenceAtColor', () => { + it("puts the Sun's colour in either system at the Sun's temperature and correction", () => { + for (const [colour, system] of [[0.65, 'B-V'], [0.823, 'BP-RP']] as const) { + const point = dwarfSequenceAtColor(colour, system)!; + expect(point.temperatureK).toBeCloseTo(5770, 0); + expect(point.bolometricCorrectionV).toBeCloseTo(-0.085, 3); + expect(point.gMinusV).toBeCloseTo(-0.165, 3); + } + }); + + it('interpolates between the two types a colour falls between', () => { + // Halfway from M1.5 (B−V 1.495, 3 620 K, −1.50) to M2 (1.505, 3 560 K, −1.62). + const point = dwarfSequenceAtColor(1.5, 'B-V')!; + expect(point.temperatureK).toBeCloseTo(3590, 6); + expect(point.bolometricCorrectionV).toBeCloseTo(-1.56, 6); + }); + + it('has no answer past either end of the table, and no G−V where none is tabulated', () => { + expect(dwarfSequenceAtColor(2.2, 'B-V')).toBeNull(); + expect(dwarfSequenceAtColor(-0.15, 'BP-RP')).toBeNull(); + expect(dwarfSequenceAtColor(null)).toBeNull(); + expect(dwarfSequenceAtColor(-0.29, 'B-V')!.gMinusV).toBeNull(); + }); +}); diff --git a/src/app/shared/astro/spectral.ts b/src/app/shared/astro/spectral.ts index 00456c0..a079918 100644 --- a/src/app/shared/astro/spectral.ts +++ b/src/app/shared/astro/spectral.ts @@ -109,31 +109,38 @@ export function temperatureToColorIndex(temperatureK: number): number | null { } /** - * Mean dwarf colours by spectral type: B−V and Gaia BP−RP, from Pecaut & Mamajek (2013, ApJS 208, - * 9, table 5) as Mamajek maintains it online (version 2022.04.16), where the BP−RP column was - * added. From B0, where B−V stops telling types apart — the whole O sequence spans 0.03 of it — - * to M8.5, past which BP−RP turns back; BP−RP starts at B9, the bluest it is tabulated for. + * The mean dwarf sequence: B−V, Gaia BP−RP, effective temperature (K), bolometric correction to V + * and Gaia G−V by spectral type, from Pecaut & Mamajek (2013, ApJS 208, 9, table 5) as Mamajek + * maintains it online (version 2022.04.16), where the Gaia columns were added. From B0, where B−V + * stops telling types apart — the whole O sequence spans 0.03 of it — to M8.5, past which BP−RP + * turns back; BP−RP starts at B9, the bluest it is tabulated for, and G−V at B1.5. */ -const DWARF_COLOURS: readonly (readonly [string, number, number | null])[] = [ - ['B0', -0.301, null], ['B0.5', -0.289, null], ['B1', -0.278, null], ['B1.5', -0.252, null], - ['B2', -0.215, null], ['B2.5', -0.198, null], ['B3', -0.178, null], ['B4', -0.165, null], - ['B5', -0.156, null], ['B6', -0.14, null], ['B7', -0.128, null], ['B8', -0.109, null], - ['B9', -0.07, -0.12], ['B9.5', -0.05, -0.087], ['A0', 0, -0.037], ['A1', 0.035, 0.005], - ['A2', 0.07, 0.068], ['A3', 0.1, 0.11], ['A4', 0.14, 0.166], ['A5', 0.16, 0.194], - ['A6', 0.185, 0.222], ['A7', 0.21, 0.263], ['A8', 0.25, 0.32], ['A9', 0.27, 0.327], - ['F0', 0.295, 0.377], ['F1', 0.33, 0.434], ['F2', 0.37, 0.49], ['F3', 0.39, 0.518], - ['F4', 0.41, 0.546], ['F5', 0.44, 0.587], ['F6', 0.486, 0.64], ['F7', 0.5, 0.67], - ['F8', 0.53, 0.694], ['F9', 0.56, 0.719], ['F9.5', 0.58, 0.767], ['G0', 0.595, 0.784], - ['G1', 0.622, 0.803], ['G2', 0.65, 0.823], ['G3', 0.66, 0.832], ['G4', 0.67, 0.841], - ['G5', 0.68, 0.85], ['G6', 0.7, 0.869], ['G7', 0.71, 0.88], ['G8', 0.73, 0.9], - ['G9', 0.775, 0.95], ['K0', 0.816, 0.983], ['K1', 0.857, 1.01], ['K2', 0.884, 1.1], - ['K3', 0.99, 1.21], ['K4', 1.09, 1.34], ['K5', 1.15, 1.43], ['K6', 1.24, 1.53], - ['K7', 1.34, 1.7], ['K8', 1.363, 1.73], ['K9', 1.4, 1.79], ['M0', 1.42, 1.84], - ['M0.5', 1.445, 1.97], ['M1', 1.485, 2.09], ['M1.5', 1.495, 2.13], ['M2', 1.505, 2.23], - ['M2.5', 1.522, 2.39], ['M3', 1.53, 2.5], ['M3.5', 1.6, 2.78], ['M4', 1.65, 2.94], - ['M4.5', 1.69, 3.16], ['M5', 1.83, 3.35], ['M5.5', 1.94, 3.71], ['M6', 2.01, 4.16], - ['M6.5', 2.07, 4.5], ['M7', 2.12, 4.65], ['M7.5', 2.14, 4.72], ['M8', 2.15, 4.86], - ['M8.5', 2.16, 5.1] +const DWARF_SEQUENCE: readonly (readonly [string, number, number | null, number, number, number | null])[] = [ + ['B0', -0.301, null, 31400, -2.99, null], ['B0.5', -0.289, null, 29000, -2.83, null], ['B1', -0.278, null, 26000, -2.58, null], + ['B1.5', -0.252, null, 24500, -2.44, -0.021], ['B2', -0.215, null, 20600, -2.03, -0.008], ['B2.5', -0.198, null, 18500, -1.77, -0.003], + ['B3', -0.178, null, 17000, -1.54, 0.001], ['B4', -0.165, null, 16400, -1.49, 0.004], ['B5', -0.156, null, 15700, -1.34, 0.007], + ['B6', -0.14, null, 14500, -1.13, 0.01], ['B7', -0.128, null, 14000, -1.05, 0.012], ['B8', -0.109, null, 12300, -0.73, 0.016], + ['B9', -0.07, -0.12, 10700, -0.42, 0.018], ['B9.5', -0.05, -0.087, 10400, -0.36, 0.017], ['A0', 0, -0.037, 9700, -0.21, 0.015], + ['A1', 0.035, 0.005, 9300, -0.14, 0.01], ['A2', 0.07, 0.068, 8800, -0.07, 0], ['A3', 0.1, 0.11, 8600, -0.04, -0.005], + ['A4', 0.14, 0.166, 8250, -0.02, -0.01], ['A5', 0.16, 0.194, 8100, 0, -0.015], ['A6', 0.185, 0.222, 7910, 0.005, -0.02], + ['A7', 0.21, 0.263, 7760, 0.01, -0.03], ['A8', 0.25, 0.32, 7590, 0.02, -0.04], ['A9', 0.27, 0.327, 7400, 0.02, -0.05], + ['F0', 0.295, 0.377, 7220, 0.01, -0.06], ['F1', 0.33, 0.434, 7020, 0.005, -0.07], ['F2', 0.37, 0.49, 6820, -0.005, -0.08], + ['F3', 0.39, 0.518, 6750, -0.01, -0.09], ['F4', 0.41, 0.546, 6670, -0.015, -0.1], ['F5', 0.44, 0.587, 6550, -0.02, -0.11], + ['F6', 0.486, 0.64, 6350, -0.03, -0.13], ['F7', 0.5, 0.67, 6280, -0.035, -0.14], ['F8', 0.53, 0.694, 6180, -0.04, -0.15], + ['F9', 0.56, 0.719, 6050, -0.05, -0.145], ['F9.5', 0.58, 0.767, 5990, -0.06, -0.155], ['G0', 0.595, 0.784, 5930, -0.065, -0.155], + ['G1', 0.622, 0.803, 5860, -0.073, -0.158], ['G2', 0.65, 0.823, 5770, -0.085, -0.165], ['G3', 0.66, 0.832, 5720, -0.095, -0.167], + ['G4', 0.67, 0.841, 5680, -0.1, -0.173], ['G5', 0.68, 0.85, 5660, -0.105, -0.179], ['G6', 0.7, 0.869, 5600, -0.115, -0.186], + ['G7', 0.71, 0.88, 5550, -0.125, -0.194], ['G8', 0.73, 0.9, 5480, -0.14, -0.202], ['G9', 0.775, 0.95, 5380, -0.16, -0.21], + ['K0', 0.816, 0.983, 5270, -0.195, -0.227], ['K1', 0.857, 1.01, 5170, -0.23, -0.25], ['K2', 0.884, 1.1, 5100, -0.26, -0.27], + ['K3', 0.99, 1.21, 4830, -0.375, -0.32], ['K4', 1.09, 1.34, 4600, -0.52, -0.42], ['K5', 1.15, 1.43, 4440, -0.63, -0.44], + ['K6', 1.24, 1.53, 4300, -0.75, -0.51], ['K7', 1.34, 1.7, 4100, -0.93, -0.58], ['K8', 1.363, 1.73, 3990, -1.03, -0.625], + ['K9', 1.4, 1.79, 3930, -1.07, -0.66], ['M0', 1.42, 1.84, 3850, -1.15, -0.7], ['M0.5', 1.445, 1.97, 3770, -1.29, -0.76], + ['M1', 1.485, 2.09, 3660, -1.42, -0.82], ['M1.5', 1.495, 2.13, 3620, -1.5, -0.87], ['M2', 1.505, 2.23, 3560, -1.62, -0.925], + ['M2.5', 1.522, 2.39, 3470, -1.78, -1.02], ['M3', 1.53, 2.5, 3430, -1.93, -1.1], ['M3.5', 1.6, 2.78, 3270, -2.28, -1.28], + ['M4', 1.65, 2.94, 3210, -2.51, -1.4], ['M4.5', 1.69, 3.16, 3110, -2.84, -1.54], ['M5', 1.83, 3.35, 3060, -3.11, -1.7], + ['M5.5', 1.94, 3.71, 2930, -3.58, -1.95], ['M6', 2.01, 4.16, 2810, -4.13, -2.37], ['M6.5', 2.07, 4.5, 2740, -4.62, -2.7], + ['M7', 2.12, 4.65, 2680, -4.99, -2.98], ['M7.5', 2.14, 4.72, 2630, -5.32, -3.15], ['M8', 2.15, 4.86, 2570, -5.65, -3.11], + ['M8.5', 2.16, 5.1, 2420, -5.78, -3.09] ]; /** @@ -145,7 +152,7 @@ const DWARF_COLOURS: readonly (readonly [string, number, number | null])[] = [ */ export function spectralTypeFromColor(colorIndex: number | null, system: 'B-V' | 'BP-RP' = 'B-V'): string | null { const column = system === 'B-V' ? 1 : 2; - const rows = DWARF_COLOURS.filter((row) => row[column] !== null); + const rows = DWARF_SEQUENCE.filter((row) => row[column] !== null); if (colorIndex === null || !(colorIndex >= rows[0][column]! && colorIndex <= rows[rows.length - 1][column]!)) { return null; } @@ -157,3 +164,36 @@ export function spectralTypeFromColor(colorIndex: number | null, system: 'B-V' | } return nearest[0]; } + +/** What the dwarf sequence says of a star of a given colour. */ +export interface DwarfSequencePoint { + temperatureK: number; + /** Bolometric correction to V: what V leaves out of the star's total output, in magnitudes. */ + bolometricCorrectionV: number; + /** Gaia G − Johnson V; `null` bluer than B1.5, where it is not tabulated. */ + gMinusV: number | null; +} + +/** + * The dwarf sequence read at a colour, interpolated between the two types it falls between — the + * same table the spectral estimate reads, so a star's temperature and its estimated type agree. + * Linear rather than nearest, because the red end is steep: B−V runs 1.495 to 1.53 from M1.5 to + * M3, over which the temperature drops 190 K and the correction 0.4 magnitudes. `null` outside + * the table, as for the estimate. + */ +export function dwarfSequenceAtColor(colorIndex: number | null, system: 'B-V' | 'BP-RP' = 'B-V'): DwarfSequencePoint | null { + const column = system === 'B-V' ? 1 : 2; + const rows = DWARF_SEQUENCE.filter((row) => row[column] !== null); + if (colorIndex === null || !(colorIndex >= rows[0][column]! && colorIndex <= rows[rows.length - 1][column]!)) { + return null; + } + const above = Math.max(1, rows.findIndex((row) => row[column]! >= colorIndex)); + const [blue, red] = [rows[above - 1], rows[above]]; + const t = (colorIndex - blue[column]!) / (red[column]! - blue[column]!); + const lerp = (from: number, to: number): number => from + (to - from) * t; + return { + temperatureK: lerp(blue[3], red[3]), + bolometricCorrectionV: lerp(blue[4], red[4]), + gMinusV: blue[5] === null || red[5] === null ? null : lerp(blue[5], red[5]) + }; +} diff --git a/src/app/shared/astro/stellar.spec.ts b/src/app/shared/astro/stellar.spec.ts index ea7c8ca..04b9a08 100644 --- a/src/app/shared/astro/stellar.spec.ts +++ b/src/app/shared/astro/stellar.spec.ts @@ -96,6 +96,22 @@ describe('luminositySolar', () => { expect(luminositySolar(PROXIMA)!).toBeGreaterThan(uncorrected * 5); }); + it('reads the correction off the colour where the catalogue has no type', () => { + // Barnard's Star, 0.0035 L☉ (Dawson & De Robertis 2004), as a star no one classified: the + // Sun's correction left it at an eighth of that. + const derived = luminositySolar({ magnitude: 9.54, distancePc: 1.8266, spectralType: 'Unknown', magnitudeBand: 'V', colorIndex: 1.57, colorSystem: 'B-V' })!; + expect(derived / 0.0035).toBeGreaterThan(1 / 1.5); + expect(derived / 0.0035).toBeLessThan(1.5); + }); + + it('carries a Gaia G magnitude to V before correcting it', () => { + // TRAPPIST-1 as Gaia has it, 5.53e-4 L☉ (Agol et al. 2021). Read as V its G is 3.1 + // magnitudes too bright, and its luminosity comes out seventeen times too high. + const derived = luminositySolar({ magnitude: 15.6226, distancePc: 12.467, spectralType: 'Unknown', magnitudeBand: 'G', colorIndex: 4.902, colorSystem: 'BP-RP' })!; + expect(derived / 5.53e-4).toBeGreaterThan(1 / 1.5); + expect(derived / 5.53e-4).toBeLessThan(1.5); + }); + it('clamps a pathological record instead of producing an absurd luminosity', () => { const absurd = luminositySolar({ magnitude: -40, distancePc: 5000, spectralType: 'O5V' })!; expect(Number.isFinite(absurd)).toBe(true); diff --git a/src/app/shared/astro/stellar.ts b/src/app/shared/astro/stellar.ts index 04f847a..07a51f8 100644 --- a/src/app/shared/astro/stellar.ts +++ b/src/app/shared/astro/stellar.ts @@ -1,4 +1,4 @@ -import { parseSpectralClass, SpectralClass } from './spectral'; +import { dwarfSequenceAtColor, parseSpectralClass, SpectralClass } from './spectral'; /** * Stellar luminosity, derived from the two things the star catalogue actually measures. @@ -91,11 +91,15 @@ export function bolometricCorrection(spectralType: string | null | undefined): n /** Everything about a star that bears on how much light it puts out. */ export interface StellarPhotometry { - /** Apparent visual magnitude, as catalogued. */ + /** Apparent magnitude, as catalogued, in `magnitudeBand`. */ magnitude: number; /** Distance from the Sun in parsecs; `0` identifies the Sun itself. */ distancePc: number; spectralType?: string; + /** V, or Gaia's G — which for an M5 dwarf reads 1.7 magnitudes brighter. Taken as V if absent. */ + magnitudeBand?: 'V' | 'G'; + colorIndex?: number | null; + colorSystem?: 'B-V' | 'BP-RP'; } /** @@ -118,7 +122,16 @@ export function luminositySolar(star: StellarPhotometry): number | null { return null; } - const bolometric = absolute + bolometricCorrection(star.spectralType); + // Where the star has a colour the dwarf sequence covers, its correction is read off that colour, + // and a G magnitude is carried to V first; only otherwise is the spectral type used, and a G + // magnitude taken as V. Gaia classifies none of its stars, so every one of them used to be + // given the Sun's correction, and TRAPPIST-1 came out at a seventh of its luminosity. Against + // the archive's own figure for 1 449 hosts, the worst tenth was off by 0.29 dex or more, and is + // now off by 0.12. + const sequence = star.colorIndex != null ? dwarfSequenceAtColor(star.colorIndex, star.colorSystem) : null; + const absoluteV = absolute - (star.magnitudeBand === 'G' ? (sequence?.gMinusV ?? 0) : 0); + const bolometric = absoluteV + (sequence?.bolometricCorrectionV ?? bolometricCorrection(star.spectralType)); const luminosity = Math.pow(10, (SOLAR_BOLOMETRIC_MAGNITUDE - bolometric) / 2.5); return Math.min(Math.max(luminosity, MIN_LUMINOSITY_SOLAR), MAX_LUMINOSITY_SOLAR); } +