Read a colour past either end of the dwarf table at that end, where the star has no type instead

dwarfSequenceAtColor answers null outside Pecaut & Mamajek's table, B−V −0.301 to 2.16 and BP−RP
−0.12 to 5.1, and effectiveTemperatureK then fell back on the type, which Gaia's stars do not have
and carbon stars' parser does not read. 219 stars with a measured colour got no temperature and so
no radius, and were drawn at the Sun's radius in the Sun's colour: 110 white dwarfs within 50 pc,
38 Gaia stars redder than BP−RP 5.1 (Gaia DR3 6439125097427143808, an ultracool dwarf 4.0 pc away),
HD 46687, La Superba and the other carbon stars, and an O8 star.

Past the table, the colour is now read at the row it is past (clampToTable), but only for a star
with no readable type: beside a type an off-table colour is more often the bad measurement — HD
49748 is G5 V at B−V −0.32 — so the type still wins there, as it did. The luminosity reads the same
point, so a star past the red end also gets the M8.5 row's G−V and correction. A type's own colour
past the table, which only O types have, is read at B0. Carbon and S stars (C, N, R, S) now count
as giants, so their correction stays their type's, not an M8.5 dwarf's −5.78.

Measured on the shipped catalogue: stars without a temperature 3 053 -> 2 834, without a radius
3 077 -> 2 858; all 292 stars with an off-table colour now have a temperature, against 73. Gaia DR3
6439125097427143808 is 2 420 K and 0.110 R☉ (M8.5 V: 0.104); the 110 white dwarfs a median 0.018 R☉
at 10 700 K (0.0013 to 0.034); HD 46687 212 R☉ at 2 420 K and La Superba 133; audit #16's Gaia DR3
5612323414549657984, k1 Pup, B6 V at BP−RP −0.15, 203 L☉ and 4.1 R☉ against 143 and none (FLAME
gives 336 and 3.49). The 2 851 stars left without a radius have neither a colour nor a readable
type, or no band.

Controls, each failing its named test: no clamp for an untyped star (2 of 784 failed), the clamp
winning over a type, an O type's colour unclamped, the luminosity off the unclamped sequence, and
carbon stars not counted as giants (1 of 784 each); clampToTable ignored (3 of 784).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-29 20:01:07 +02:00
co-authored by Claude Opus 5.5
parent d097f4b477
commit 31e0c04fe1
4 changed files with 59 additions and 14 deletions
+8 -2
View File
@@ -47,8 +47,8 @@ describe('parseSpectralClass', () => {
}); });
describe('isGiant', () => { describe('isGiant', () => {
it('reads luminosity classes I to III off the primary, and the giant and supergiant prefixes', () => { it('reads luminosity classes I to III off the primary, the giant and supergiant prefixes, and carbon and S stars', () => {
for (const type of ['M1Ib + B2.5V', 'K5III', 'M2II-IIIvar', 'C7Iab', 'K0IIIb', 'gK0', 'cM2']) { for (const type of ['M1Ib + B2.5V', 'K5III', 'M2II-IIIvar', 'C7Iab', 'K0IIIb', 'gK0', 'cM2', 'N5', 'Ce+', 'S57:']) {
expect(isGiant(type), type).toBe(true); expect(isGiant(type), type).toBe(true);
} }
}); });
@@ -166,4 +166,10 @@ describe('dwarfSequenceAtColor', () => {
expect(dwarfSequenceAtColor(null)).toBeNull(); expect(dwarfSequenceAtColor(null)).toBeNull();
expect(dwarfSequenceAtColor(-0.29, 'B-V')!.gMinusV).toBeNull(); expect(dwarfSequenceAtColor(-0.29, 'B-V')!.gMinusV).toBeNull();
}); });
it('reads the row at the end a colour is past, when asked to', () => {
expect(dwarfSequenceAtColor(2.2, 'B-V', true)).toEqual({ temperatureK: 2420, bolometricCorrectionV: -5.78, gMinusV: -3.09 });
expect(dwarfSequenceAtColor(-0.15, 'BP-RP', true)).toEqual({ temperatureK: 10700, bolometricCorrectionV: -0.42, gMinusV: 0.018 });
expect(dwarfSequenceAtColor(null, 'B-V', true)).toBeNull();
});
}); });
+12 -8
View File
@@ -73,12 +73,14 @@ export function parseSpectralClass(
const GIANT_LUMINOSITY_CLASS = /(?<![IV])(?:III|II|I)(?![IV])/; const GIANT_LUMINOSITY_CLASS = /(?<![IV])(?:III|II|I)(?![IV])/;
/** /**
* Whether a spectral type says its star is a giant or supergiant: luminosity class I to III, or * Whether a spectral type says its star is a giant or supergiant: luminosity class I to III,
* HYG's `g` or `c` prefix. Read off the primary only — Antares is `M1Ib + B2.5V`. * HYG's `g` or `c` prefix, or a carbon or S star (C, N, R, S), which are all giants on the
* asymptotic branch whether or not a class is given. Read off the primary only — Antares is
* `M1Ib + B2.5V`.
*/ */
export function isGiant(spectralType: string | null | undefined): boolean { export function isGiant(spectralType: string | null | undefined): boolean {
const primary = (spectralType ?? '').split('+')[0].trim(); const primary = (spectralType ?? '').split('+')[0].trim();
return /^[gc][OBAFGKM]/.test(primary) || GIANT_LUMINOSITY_CLASS.test(primary); return /^(?:[gc][OBAFGKM]|[CNRS])/.test(primary) || GIANT_LUMINOSITY_CLASS.test(primary);
} }
/** /**
@@ -191,17 +193,19 @@ export interface DwarfSequencePoint {
* same table the spectral estimate reads, so a star's temperature and its estimated type agree. * 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 * 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 * M3, over which the temperature drops 190 K and the correction 0.4 magnitudes. `null` outside
* the table, as for the estimate. * the table, as for the estimate — or, with `clampToTable`, the row at the end the colour is past.
*/ */
export function dwarfSequenceAtColor(colorIndex: number | null, system: 'B-V' | 'BP-RP' = 'B-V'): DwarfSequencePoint | null { export function dwarfSequenceAtColor(colorIndex: number | null, system: 'B-V' | 'BP-RP' = 'B-V', clampToTable = false): DwarfSequencePoint | null {
const column = system === 'B-V' ? 1 : 2; const column = system === 'B-V' ? 1 : 2;
const rows = DWARF_SEQUENCE.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]!)) { const [bluest, reddest] = [rows[0][column]!, rows[rows.length - 1][column]!];
if (colorIndex === null || !Number.isFinite(colorIndex) || (!clampToTable && !(colorIndex >= bluest && colorIndex <= reddest))) {
return null; return null;
} }
const above = Math.max(1, rows.findIndex((row) => row[column]! >= colorIndex)); 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 [blue, red] = [rows[above - 1], rows[above]];
const t = (colorIndex - blue[column]!) / (red[column]! - blue[column]!); const t = (colour - blue[column]!) / (red[column]! - blue[column]!);
const lerp = (from: number, to: number): number => from + (to - from) * t; const lerp = (from: number, to: number): number => from + (to - from) * t;
return { return {
temperatureK: lerp(blue[3], red[3]), temperatureK: lerp(blue[3], red[3]),
+19
View File
@@ -158,6 +158,17 @@ describe('effectiveTemperatureK', () => {
expect(effectiveTemperatureK({ magnitude: 11, distancePc: 5, spectralType: 'M5Ve', colorIndex: null })).toBeCloseTo(3106, 0); expect(effectiveTemperatureK({ magnitude: 11, distancePc: 5, spectralType: 'M5Ve', colorIndex: null })).toBeCloseTo(3106, 0);
expect(effectiveTemperatureK({ magnitude: 11, distancePc: 5, spectralType: 'Unknown', colorIndex: null })).toBeNull(); expect(effectiveTemperatureK({ magnitude: 11, distancePc: 5, spectralType: 'Unknown', colorIndex: null })).toBeNull();
}); });
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.
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: 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 })!;
expect(effectiveTemperatureK({ magnitude: 9, distancePc: 184, spectralType: 'G5V', colorIndex: -0.319, colorSystem: 'B-V' })).toBe(g5);
expect(g5).toBeGreaterThan(5500);
});
}); });
describe('radiusFromLuminositySolar', () => { describe('radiusFromLuminositySolar', () => {
@@ -165,6 +176,14 @@ describe('radiusFromLuminositySolar', () => {
expect(radiusFromLuminositySolar(1, SOLAR_EFFECTIVE_TEMPERATURE_K)).toBeCloseTo(1, 12); expect(radiusFromLuminositySolar(1, SOLAR_EFFECTIVE_TEMPERATURE_K)).toBeCloseTo(1, 12);
}); });
it("gives an ultracool dwarf redder than the table an M8.5 dwarf's radius, not none", () => {
// Gaia DR3 6439125097427143808, 4.0 pc away at BP−RP 5.11; M8.5 V is 0.104 R☉ (Mamajek).
const star = { magnitude: 14.005, distancePc: 4.005, spectralType: 'Unknown', magnitudeBand: 'G', colorIndex: 5.113, colorSystem: 'BP-RP' } as const;
const radius = radiusFromLuminositySolar(luminositySolar(star)!, effectiveTemperatureK(star)!);
expect(radius / 0.104).toBeGreaterThan(1 / 1.2);
expect(radius / 0.104).toBeLessThan(1.2);
});
it('gives Sirius and TRAPPIST-1 their published radii from colour and brightness alone', () => { 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. // 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 [ for (const [star, published] of [
+20 -4
View File
@@ -1,4 +1,4 @@
import { dwarfSequenceAtColor, isGiant, parseSpectralClass, SpectralClass, spectralTypeToColorIndex } from './spectral'; import { DwarfSequencePoint, dwarfSequenceAtColor, isGiant, parseSpectralClass, SpectralClass, spectralTypeToColorIndex } from './spectral';
/** /**
* Stellar luminosity, derived from the two things the star catalogue actually measures. * Stellar luminosity, derived from the two things the star catalogue actually measures.
@@ -133,7 +133,7 @@ export function luminositySolar(star: StellarPhotometry): number | null {
// dwarf, whose correction is larger. Antares, M1 Ib at B−V 1.87, read as an M5 dwarf's −3.26 // dwarf, whose correction is larger. Antares, M1 Ib at B−V 1.87, read as an M5 dwarf's −3.26
// came out 1 516 R☉ against the 680 Ohnaka et al. (2013) measure, and 119 Tau 2 838 against 587; // came out 1 516 R☉ against the 680 Ohnaka et al. (2013) measure, and 119 Tau 2 838 against 587;
// its type's −1.55 gives 690. // its type's −1.55 gives 690.
const sequence = star.colorIndex != null ? dwarfSequenceAtColor(star.colorIndex, star.colorSystem) : null; const sequence = sequenceAtColour(star);
const absoluteV = absolute - (star.magnitudeBand === 'G' ? (sequence?.gMinusV ?? 0) : 0); const absoluteV = absolute - (star.magnitudeBand === 'G' ? (sequence?.gMinusV ?? 0) : 0);
const bolometric = const bolometric =
absoluteV + (sequence && !isGiant(star.spectralType) ? sequence.bolometricCorrectionV : bolometricCorrection(star.spectralType)); absoluteV + (sequence && !isGiant(star.spectralType) ? sequence.bolometricCorrectionV : bolometricCorrection(star.spectralType));
@@ -141,6 +141,22 @@ export function luminositySolar(star: StellarPhotometry): number | null {
return Math.min(Math.max(luminosity, MIN_LUMINOSITY_SOLAR), MAX_LUMINOSITY_SOLAR); return Math.min(Math.max(luminosity, MIN_LUMINOSITY_SOLAR), MAX_LUMINOSITY_SOLAR);
} }
/**
* 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.
*/
function sequenceAtColour(star: StellarPhotometry): DwarfSequencePoint | null {
if (star.colorIndex == null) {
return null;
}
return dwarfSequenceAtColor(star.colorIndex, star.colorSystem) ?? (parseSpectralClass(star.spectralType) ? null : dwarfSequenceAtColor(star.colorIndex, star.colorSystem, true));
}
/** The Sun's effective temperature, the IAU 2015 nominal value. */ /** The Sun's effective temperature, the IAU 2015 nominal value. */
export const SOLAR_EFFECTIVE_TEMPERATURE_K = 5772; export const SOLAR_EFFECTIVE_TEMPERATURE_K = 5772;
@@ -154,8 +170,8 @@ export function effectiveTemperatureK(star: StellarPhotometry): number | null {
if (star.distancePc === 0) { if (star.distancePc === 0) {
return SOLAR_EFFECTIVE_TEMPERATURE_K; return SOLAR_EFFECTIVE_TEMPERATURE_K;
} }
const measured = star.colorIndex != null ? dwarfSequenceAtColor(star.colorIndex, star.colorSystem) : null; // A type's colour past the table is an O star's, which B−V no longer tells apart from B0.
return (measured ?? dwarfSequenceAtColor(spectralTypeToColorIndex(star.spectralType)))?.temperatureK ?? null; return (sequenceAtColour(star) ?? dwarfSequenceAtColor(spectralTypeToColorIndex(star.spectralType), 'B-V', true))?.temperatureK ?? null;
} }
/** /**