From 06b4ff64b52a6bec2163fece66ef430308d83fa5 Mon Sep 17 00:00:00 2001 From: Senrokai Date: Tue, 29 Sep 2026 23:38:16 +0200 Subject: [PATCH] Read a white dwarf past the table's blue end at the temperature white dwarfs of its colour have MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 31e0c04 clamped an untyped star bluer than BP−RP −0.12 to the table's B9 row, so every one of the 110 white dwarfs within 50 pc was drawn at 10 700 K. Gentile Fusillo et al. (2021, MNRAS 508, 3877) fit 104 of them at 14 266 to 39 304 K, and the radius their mass and gravity give was a median 0.65 of the one drawn. Past the table's end, dwarfSequenceAtColor now reads BP−RP off the median pure-hydrogen temperature they fit in bins of ±0.025 around −0.15 to −0.40 (15 369 to 28 585 K, counted again from the cross-match: 27, 24, 22, 15, 10 and 2 stars), and the correction and G−V off the table's own rows at that temperature, through a new dwarfSequenceAtTemperature that temperatureToColorIndex now shares. Against GF21's R = sqrt(GM/g) over the same 104, the drawn radius goes from a median 1.53 (p10 1.33, p90 1.96) to 0.96 (0.94, 1.05), and the temperature from 0.59 of theirs to 1.00 (0.88, 1.02). Gaia DR3 6791196382856581376 is now 19 251 K and 0.0120 R☉, against their 19 205 K and 0.01245. Tests: stellar.spec's −0.25 case now expects 19 012 K where it pinned 10 700, and a new case gives that white dwarf its radius to within a fifth; spectral.spec reads −0.15, −0.13 and −0.6, keeps the red end and B−V's blue end at their rows, and covers dwarfSequenceAtTemperature. Controls: no white-dwarf branch, the bin's temperature without interpolating, B9's correction kept at the new temperature, and the temperature read the wrong way round each fail the named test. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/app/shared/astro/spectral.spec.ts | 23 ++++++++++- src/app/shared/astro/spectral.ts | 56 +++++++++++++++++++-------- src/app/shared/astro/stellar.spec.ts | 14 ++++++- src/app/shared/astro/stellar.ts | 15 +++---- 4 files changed, 81 insertions(+), 27 deletions(-) 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) {