Correct a giant's light by its type, not by the cooler dwarf its colour reads as

3392f06 read every star's bolometric correction off the dwarf sequence at its colour, so a giant
got the correction of the cooler dwarf of that colour, which is larger. Antares, M1 Ib at B−V 1.87,
took an M5 dwarf's −3.26 and came out 172 023 L☉ and 1 516 R☉, a 7.06 AU sphere, against the 680 R☉
Ohnaka et al. (2013) measure; 119 Tau 2 838 against 587, Menkar 204 against 89.

isGiant (spectral.ts) reads luminosity class I to III off a type's primary component, or HYG's g
and c prefixes, and a giant keeps its type's correction. Drawn radius against the published one
(no planets, so derived), before and after: Antares 2.23 -> 1.01, 119 Tau 4.84 -> 1.43, Menkar 2.29
-> 1.80, Scheat 1.92 -> 1.42, Aldebaran 1.95 -> 1.10, Mirach 1.96 -> 1.27, 41 Com 2.14 -> 1.56;
Betelgeuse 0.76 -> 0.89. It is not better everywhere: Gacrux goes from 1.23 to 1.40 and Arcturus
from 1.02 to 0.88. 10 808 stars have a giant's type, 10 794 of them a colour. Against the archive's
own luminosity for the 130 giant hosts, the median error moves from 0.038 to 0.052 dex and the 90th
percentile from 0.246 to 0.204; 17 are off by over 1.5 times, against 19.

Controls, each failing its named test (1 of 781): the giant given the dwarf's correction, a giant
companion read as the primary, a IV read as a I, and the prefixes ignored.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-29 19:53:22 +02:00
co-authored by Claude Opus 5.5
parent d94451e203
commit d097f4b477
4 changed files with 49 additions and 4 deletions
+15 -1
View File
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
import { dwarfSequenceAtColor, parseSpectralClass, SPECTRAL_CLASSES, spectralTypeFromColor, spectralTypeToColorIndex, temperatureToColorIndex } from './spectral';
import { dwarfSequenceAtColor, isGiant, parseSpectralClass, SPECTRAL_CLASSES, spectralTypeFromColor, spectralTypeToColorIndex, temperatureToColorIndex } from './spectral';
describe('parseSpectralClass', () => {
it('reads a clean class and subclass', () => {
@@ -46,6 +46,20 @@ describe('parseSpectralClass', () => {
});
});
describe('isGiant', () => {
it('reads luminosity classes I to III off the primary, and the giant and supergiant prefixes', () => {
for (const type of ['M1Ib + B2.5V', 'K5III', 'M2II-IIIvar', 'C7Iab', 'K0IIIb', 'gK0', 'cM2']) {
expect(isGiant(type), type).toBe(true);
}
});
it('leaves dwarfs, subgiants, a dwarf with a giant companion and the unclassified alone', () => {
for (const type of ['G2V', 'B2IV', 'F0IVn', 'M5Ve', 'K1V + M3III', 'g-k', 'Unknown', 'DA', '']) {
expect(isGiant(type), type).toBe(false);
}
});
});
describe('spectralTypeToColorIndex', () => {
it('places the Sun near its real B-V of 0.65', () => {
expect(spectralTypeToColorIndex('G2V')).toBeCloseTo(0.626, 2);
+12
View File
@@ -69,6 +69,18 @@ export function parseSpectralClass(
return { spectralClass, subclass };
}
/** Luminosity class I (with Ia, Iab, Ib), II or III, not the I of a 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
* HYG's `g` or `c` prefix. Read off the primary only — Antares is `M1Ib + B2.5V`.
*/
export function isGiant(spectralType: string | null | undefined): boolean {
const primary = (spectralType ?? '').split('+')[0].trim();
return /^[gc][OBAFGKM]/.test(primary) || GIANT_LUMINOSITY_CLASS.test(primary);
}
/**
* Approximate B-V colour index for a spectral type, interpolating between the class anchors by
* subclass. Returns `null` when no class can be recognised, which is the honest answer for the
+12
View File
@@ -122,6 +122,18 @@ describe('luminositySolar', () => {
expect(derived / 5.53e-4).toBeLessThan(1.5);
});
it("gives a giant its type's correction, not the cooler dwarf's its colour reads as", () => {
// Antares, M1 Ib at B−V 1.87 and 170 pc, and Aldebaran, K5 III: 680 R☉ (Ohnaka et al. 2013)
// and 44.2 (Richichi & Roccatagliata 2005). As dwarfs they came out 1 516 and 86.
const antares = { magnitude: 1.06, distancePc: 169.78, spectralType: 'M1Ib + B2.5V', magnitudeBand: 'V', colorIndex: 1.865, colorSystem: 'B-V' } as const;
const aldebaran = { magnitude: 0.87, distancePc: 20.433, spectralType: 'K5III', magnitudeBand: 'V', colorIndex: 1.538, colorSystem: 'B-V' } as const;
for (const [star, published] of [[antares, 680], [aldebaran, 44.2]] as const) {
const radius = radiusFromLuminositySolar(luminositySolar(star)!, effectiveTemperatureK(star)!);
expect(radius / published).toBeGreaterThan(1 / 1.2);
expect(radius / published).toBeLessThan(1.2);
}
});
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);
+10 -3
View File
@@ -1,4 +1,4 @@
import { dwarfSequenceAtColor, parseSpectralClass, SpectralClass, spectralTypeToColorIndex } from './spectral';
import { dwarfSequenceAtColor, isGiant, parseSpectralClass, SpectralClass, spectralTypeToColorIndex } from './spectral';
/**
* Stellar luminosity, derived from the two things the star catalogue actually measures.
@@ -128,9 +128,15 @@ export function luminositySolar(star: StellarPhotometry): number | null {
// 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.
//
// Not for a star its type says is a giant, though: at its colour the dwarf sequence is a cooler
// 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;
// its type's −1.55 gives 690.
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 bolometric =
absoluteV + (sequence && !isGiant(star.spectralType) ? 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);
}
@@ -141,7 +147,8 @@ export const SOLAR_EFFECTIVE_TEMPERATURE_K = 5772;
/**
* Effective temperature, off the dwarf sequence at the star's colour, or at the colour its
* spectral type implies where it has none. Exactly the Sun's for the Sun, which is at zero
* distance here. A giant is read as the dwarf of its colour: a few hundred kelvin too cool at K.
* distance here. A giant is read as the dwarf of its colour: a few hundred kelvin too cool at K,
* and Antares, an M1 supergiant, 3 019 K against the 3 660 Ohnaka et al. (2013) measure.
*/
export function effectiveTemperatureK(star: StellarPhotometry): number | null {
if (star.distancePc === 0) {