diff --git a/src/app/shared/astro/spectral.spec.ts b/src/app/shared/astro/spectral.spec.ts index 0300767..acefe11 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 { dwarfSequenceAtColor, isGiant, parseSpectralClass, spectralClassification, SPECTRAL_CLASSES, spectralTypeFromColor, spectralTypeToColorIndex, temperatureToColorIndex } from './spectral'; +import { dwarfSequenceAtColor, dwarfSequenceAtTemperature, isGiant, parseSpectralClass, spectralClassification, SPECTRAL_CLASSES, spectralTypeFromColor, spectralTypeToColorIndex, temperatureToColorIndex } from './spectral'; describe('parseSpectralClass', () => { it('reads a clean class and subclass', () => { @@ -180,7 +180,26 @@ describe('dwarfSequenceAtColor', () => { it('reads the row at the end a colour is past, when asked to', () => { expect(dwarfSequenceAtColor(2.2, 'B-V', true)).toEqual({ bMinusV: 2.16, temperatureK: 2420, bolometricCorrectionV: -5.78, gMinusV: -3.09 }); - expect(dwarfSequenceAtColor(-0.15, 'BP-RP', true)).toEqual({ bMinusV: -0.07, temperatureK: 10700, bolometricCorrectionV: -0.42, gMinusV: 0.018 }); + expect(dwarfSequenceAtColor(5.3, 'BP-RP', true)).toEqual({ bMinusV: 2.16, temperatureK: 2420, bolometricCorrectionV: -5.78, gMinusV: -3.09 }); + expect(dwarfSequenceAtColor(-0.4, 'B-V', true)).toEqual({ bMinusV: -0.301, temperatureK: 31400, bolometricCorrectionV: -2.99, gMinusV: null }); expect(dwarfSequenceAtColor(null, 'B-V', true)).toBeNull(); }); + + it("reads a white dwarf bluer than BP−RP's end at the temperature measured at its colour, and the table's correction there", () => { + // Gentile Fusillo et al. (2021): 15 369 K at −0.15, where B9's row had 10 700; between B6 and B5. + const point = dwarfSequenceAtColor(-0.15, 'BP-RP', true)!; + expect(point.temperatureK).toBeCloseTo(15369, 6); + expect(point.bolometricCorrectionV).toBeCloseTo(-1.13 - 0.21 * (869 / 1200), 6); + expect(dwarfSequenceAtColor(-0.13, 'BP-RP', true)!.temperatureK).toBeCloseTo(15369 - 4669 * (2 / 3), 6); + expect(dwarfSequenceAtColor(-0.6, 'BP-RP', true)!.temperatureK).toBeCloseTo(28585, 6); + }); +}); + +describe('dwarfSequenceAtTemperature', () => { + it("is a type's own row at its temperature, between two rows between them, and the end row past either end", () => { + expect(dwarfSequenceAtTemperature(5770)).toEqual({ bMinusV: 0.65, temperatureK: 5770, bolometricCorrectionV: -0.085, gMinusV: -0.165 }); + expect(dwarfSequenceAtTemperature(3615).bolometricCorrectionV).toBeCloseTo(-1.51, 6); + expect(dwarfSequenceAtTemperature(50000).temperatureK).toBe(31400); + expect(dwarfSequenceAtTemperature(1000).temperatureK).toBe(2420); + }); }); diff --git a/src/app/shared/astro/spectral.ts b/src/app/shared/astro/spectral.ts index 03579ea..af2a240 100644 --- a/src/app/shared/astro/spectral.ts +++ b/src/app/shared/astro/spectral.ts @@ -115,12 +115,7 @@ export function spectralTypeToColorIndex(spectralType: string | null | undefined */ export function temperatureToColorIndex(temperatureK: number): number | null { const [hottest, coolest] = [DWARF_SEQUENCE[0], DWARF_SEQUENCE[DWARF_SEQUENCE.length - 1]]; - if (!(temperatureK <= hottest[3] && temperatureK >= coolest[3])) { - return null; - } - const cooler = Math.max(1, DWARF_SEQUENCE.findIndex((row) => row[3] <= temperatureK)); - const [hot, cool] = [DWARF_SEQUENCE[cooler - 1], DWARF_SEQUENCE[cooler]]; - return hot[1] + ((cool[1] - hot[1]) * (hot[3] - temperatureK)) / (hot[3] - cool[3]); + return temperatureK <= hottest[3] && temperatureK >= coolest[3] ? dwarfSequenceAtTemperature(temperatureK).bMinusV : null; } /** @@ -130,7 +125,9 @@ export function temperatureToColorIndex(temperatureK: number): number | null { * 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_SEQUENCE: readonly (readonly [string, number, number | null, number, number, number | null])[] = [ +type SequenceRow = readonly [string, number, number | null, number, number, number | null]; + +const DWARF_SEQUENCE: readonly SequenceRow[] = [ ['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], @@ -216,7 +213,8 @@ export interface DwarfSequencePoint { * 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 — or, with `clampToTable`, the row at the end the colour is past. + * the table, as for the estimate — or, with `clampToTable`, the row at the end the colour is past, + * except bluer than BP−RP's end, where it is read off {@link WHITE_DWARF_BP_RP}. */ export function dwarfSequenceAtColor(colorIndex: number | null, system: 'B-V' | 'BP-RP' = 'B-V', clampToTable = false): DwarfSequencePoint | null { const column = system === 'B-V' ? 1 : 2; @@ -225,15 +223,41 @@ export function dwarfSequenceAtColor(colorIndex: number | null, system: 'B-V' | if (colorIndex === null || !Number.isFinite(colorIndex) || (!clampToTable && !(colorIndex >= bluest && colorIndex <= reddest))) { return null; } - const colour = Math.min(Math.max(colorIndex, bluest), reddest); - const above = Math.max(1, rows.findIndex((row) => row[column]! >= colour)); - const [blue, red] = [rows[above - 1], rows[above]]; - const t = (colour - blue[column]!) / (red[column]! - blue[column]!); + if (system === 'BP-RP' && colorIndex < bluest) { + const next = Math.max(1, WHITE_DWARF_BP_RP.findIndex(([colour]) => colour >= colorIndex)); + const [[blueColour, blueK], [redColour, redK]] = [WHITE_DWARF_BP_RP[next - 1], WHITE_DWARF_BP_RP[next]]; + return dwarfSequenceAtTemperature(blueK + (redK - blueK) * Math.max((colorIndex - blueColour) / (redColour - blueColour), 0)); + } + return sequenceWhere(rows, (row) => row[column]!, Math.min(Math.max(colorIndex, bluest), reddest)); +} + +/** + * Effective temperature by BP−RP past the blue end of the table, which is B9's −0.12: the median + * pure-hydrogen temperature Gentile Fusillo et al. (2021, MNRAS 508, 3877) fit, in bins of ±0.025, + * to the 104 of the map's stars there that their white dwarf catalogue has, all white dwarfs; with + * the table's end, 10 700 K, and their bluest bin held past it. Every one of them was drawn at B9's + * 10 700 K where they measure 14 266 to 39 304, and at a median 1.53 times the radius their mass + * and gravity give. + */ +const WHITE_DWARF_BP_RP: readonly (readonly [number, number])[] = [ + [-0.4, 28585], [-0.35, 24521], [-0.3, 22090], [-0.25, 19012], [-0.2, 17079], [-0.15, 15369], [-0.12, 10700] +]; + +/** The dwarf sequence at an effective temperature, between the two types it falls between; the end row past either end. */ +export function dwarfSequenceAtTemperature(temperatureK: number): DwarfSequencePoint { + return sequenceWhere(ROWS_WITH_COLOUR[1], (row) => -row[3], -temperatureK); +} + +/** The sequence where `key`, rising down `rows`, reaches `value`: linear between the two rows either side, the end row past either end. */ +function sequenceWhere(rows: readonly SequenceRow[], key: (row: SequenceRow) => number, value: number): DwarfSequencePoint { + const next = rows.findIndex((row) => key(row) >= value); + const [first, second] = next === -1 ? [rows[rows.length - 2], rows[rows.length - 1]] : [rows[Math.max(next, 1) - 1], rows[Math.max(next, 1)]]; + const t = Math.min(Math.max((value - key(first)) / (key(second) - key(first)), 0), 1); const lerp = (from: number, to: number): number => from + (to - from) * t; return { - bMinusV: lerp(blue[1], red[1]), - 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]) + bMinusV: lerp(first[1], second[1]), + temperatureK: lerp(first[3], second[3]), + bolometricCorrectionV: lerp(first[4], second[4]), + gMinusV: first[5] === null || second[5] === null ? null : lerp(first[5], second[5]) }; } diff --git a/src/app/shared/astro/stellar.spec.ts b/src/app/shared/astro/stellar.spec.ts index 8a30ca5..209a541 100644 --- a/src/app/shared/astro/stellar.spec.ts +++ b/src/app/shared/astro/stellar.spec.ts @@ -160,9 +160,10 @@ describe('effectiveTemperatureK', () => { }); it('reads a colour past the table at its end where there is no type, and the type where there is', () => { - // An ultracool dwarf redder than M8.5, a white dwarf bluer than B9, and an O star B−V puts at B0. + // An ultracool dwarf redder than M8.5, a white dwarf bluer than B9 at the 19 012 K Gentile + // Fusillo et al. (2021) measure at its colour, not B9's 10 700, and an O star B−V puts at B0. expect(effectiveTemperatureK({ magnitude: 14.005, distancePc: 4.005, spectralType: 'Unknown', magnitudeBand: 'G', colorIndex: 5.113, colorSystem: 'BP-RP' })).toBe(2420); - expect(effectiveTemperatureK({ magnitude: 14, distancePc: 25, spectralType: 'Unknown', magnitudeBand: 'G', colorIndex: -0.25, colorSystem: 'BP-RP' })).toBe(10700); + expect(effectiveTemperatureK({ magnitude: 14, distancePc: 25, spectralType: 'Unknown', magnitudeBand: 'G', colorIndex: -0.25, colorSystem: 'BP-RP' })).toBeCloseTo(19012, 6); expect(effectiveTemperatureK({ magnitude: 7, distancePc: 121, spectralType: 'O8', colorIndex: -0.31, colorSystem: 'B-V' })).toBe(31400); // HD 49748, G5 V at B−V −0.32: the colour is the one that is wrong. const g5 = effectiveTemperatureK({ magnitude: 9, distancePc: 184, spectralType: 'G5V', colorIndex: null })!; @@ -184,6 +185,15 @@ describe('radiusFromLuminositySolar', () => { expect(radius / 0.104).toBeLessThan(1.2); }); + it('gives a white dwarf bluer than the table the radius its mass and gravity give', () => { + // Gaia DR3 6791196382856581376, 24.5 pc: 19 205 K, log g 8.07 and 0.66 M☉ in Gentile Fusillo et + // al. (2021), so 0.01245 R☉. At B9's 10 700 K it came out about 1.5 times that. + const star = { magnitude: 12.9198, distancePc: 24.5237, spectralType: 'Unknown', magnitudeBand: 'G', colorIndex: -0.2539, colorSystem: 'BP-RP' } as const; + const radius = radiusFromLuminositySolar(luminositySolar(star)!, effectiveTemperatureK(star)!); + expect(radius / 0.01245).toBeGreaterThan(1 / 1.2); + expect(radius / 0.01245).toBeLessThan(1.2); + }); + it('gives Sirius and TRAPPIST-1 their published radii from colour and brightness alone', () => { // 1.711 R☉ (Liebert et al. 2005) and 0.119 R☉ (Agol et al. 2021), each to within a fifth. for (const [star, published] of [ diff --git a/src/app/shared/astro/stellar.ts b/src/app/shared/astro/stellar.ts index 37742c1..0e5f9d9 100644 --- a/src/app/shared/astro/stellar.ts +++ b/src/app/shared/astro/stellar.ts @@ -142,13 +142,14 @@ export function luminositySolar(star: StellarPhotometry): number | null { } /** - * The dwarf sequence at a star's colour — at the end of the table a colour is past, where the star - * has no type to go by instead. Past the red end are the ultracool dwarfs Gaia measures redder than - * BP−RP 5.1, M8.5; past the blue end, its white dwarfs and hot stars bluer than −0.12, B9, and - * B−V's O stars. Unclamped, they had no temperature and were drawn at the Sun's: Gaia DR3 - * 6439125097427143808, an ultracool dwarf 4.0 pc away, and 110 white dwarfs within 50 pc, all at - * 1 R☉. Beside a type, an off-table colour is more often a bad one than an extreme star — HD - * 49748, G5 V, at B−V −0.32 — and the type is read instead. + * The dwarf sequence at a star's colour — past the table's end, where the star has no type to go + * by instead, at the end a colour is past. Past the red end are the ultracool dwarfs Gaia measures + * redder than BP−RP 5.1, M8.5, and past B−V's blue end its O stars; Gaia's white dwarfs, bluer + * than BP−RP −0.12, are read at the temperature white dwarfs of their colour are measured at. + * Unread, they had no temperature and were drawn at the Sun's: Gaia DR3 6439125097427143808, an + * ultracool dwarf 4.0 pc away, and 110 white dwarfs within 50 pc, all at 1 R☉. Beside a type, an + * off-table colour is more often a bad one than an extreme star — HD 49748, G5 V, at B−V −0.32 — + * and the type is read instead. */ function sequenceAtColour(star: StellarPhotometry): DwarfSequencePoint | null { if (star.colorIndex == null) {