d097f4b gave a giant its type's bolometric correction but left its temperature at the dwarf its
colour reads as, and the radius drawn from the two paired a correction for one star with the
temperature of another. The M giants stayed too cool (610 of them at a median 3 275 K, Antares
3 019 against Ohnaka et al.'s 3 660), and a hot giant behind dust got a 30 000 K star's correction
at the temperature of an A star: Menkib, O7.5 Iab at B−V 0.02, was drawn at 9 517 K and 95 R☉,
Alp Cam, O9.5 Ia, at 338, where 14 and 21 are published. Rigel went from 81 to 102 R☉, Alnilam
from 57 to 108.
giantSurface now reads both off the type: G to M giants off van Belle et al.'s (2021, table 8)
interferometric scale, fitted to 191 giants from G1 to M7.75 III, with the correction the dwarf
sequence has at that temperature; O to F giants off the dwarf of their type, for which the table
gains Mamajek's O3 to O9.5 rows (without colours, which do not tell O types apart); carbon and S
stars, which no row reads and which got the Sun's −0.06 at 2 420 K, off the medians of Bergeat et
al. (2001): 2 990 K over the 441 stars of their table 10 and −2.83 over the 383 with a V magnitude,
counted again from VizieR here.
On the shipped catalogue (drawn radius in R☉, before → after, published): Antares 690 → 410 at
3 730 K (680; its luminosity from V is 0.4 dex under Ohnaka's), Aldebaran 48.5 → 44.0 (44.2),
Arcturus 22.5 → 24.1 (25.4), Menkar 160 → 103, Gacrux 118 → 73, Rigel 102 → 67 (74.1), Alnilam
108 → 33, Menkib 95 → 6.9 (14, the dust still dims it), Alp Cam 338 → 31, La Superba 133 → 311
(315) and 544 → 6 977 L☉ (8 090 from Bergeat's bolometric magnitude), 19 Psc 130 → 305 (295).
The 610 M giants now sit at a median 3 644 K (p10 3 386, p90 3 816). Against their own radii
before, the O giants' fall to a median 0.08, the B giants' to 0.62, the M giants' to 0.68, and the
K giants' rise by 8 %. It is not better everywhere: Pollux goes from 8.6 to 10.1 against 8.8,
119 Tau from 700 to 326 against 587, and Mintaka and Alnitak, placed by Hipparcos at 212 and 226
pc where they are about 380, come out 8.8 and 11.8 against 13 to 20.
Tests: the giant case in stellar.spec now checks Antares's temperature against Ohnaka's, and
Aldebaran (to a tenth) and Rigel (to a fifth) against their interferometric radii; new cases give
Menkib its type's 36 100 K and a radius within 2.5 times the published one, and La Superba a
luminosity within a fifth of Bergeat's and 2 990 K; spectral.spec covers dwarfSequenceAtType. The
scene's supergiant case now expects Antares at 350-480 R☉ where it pinned 600-760. Controls:
the luminosity or the temperature ignoring giantSurface, G-M giants read as the dwarf of their
type, their correction taken off the type instead of their temperature, O giants through the
textbook colour clamped at B0, the type index off by one subclass and carbon stars unhandled each
fail the named test.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
216 lines
10 KiB
TypeScript
216 lines
10 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
|
||
import { dwarfSequenceAtColor, dwarfSequenceAtTemperature, dwarfSequenceAtType, isGiant, parseSpectralClass, spectralClassification, SPECTRAL_CLASSES, spectralTypeFromColor, spectralTypeToColorIndex, temperatureToColorIndex } from './spectral';
|
||
|
||
describe('parseSpectralClass', () => {
|
||
it('reads a clean class and subclass', () => {
|
||
expect(parseSpectralClass('M3.5')).toEqual({ spectralClass: 'M', subclass: 3.5 });
|
||
expect(parseSpectralClass('G2V')).toEqual({ spectralClass: 'G', subclass: 2 });
|
||
});
|
||
|
||
it('defaults the subclass to 0 when only a class is given', () => {
|
||
expect(parseSpectralClass('K')).toEqual({ spectralClass: 'K', subclass: 0 });
|
||
});
|
||
|
||
it('accepts the lowercase forms HYG actually ships', () => {
|
||
// 354 nearby stars are classified as a bare lowercase "m".
|
||
expect(parseSpectralClass('m')).toEqual({ spectralClass: 'M', subclass: 0 });
|
||
expect(parseSpectralClass('k')).toEqual({ spectralClass: 'K', subclass: 0 });
|
||
});
|
||
|
||
it('skips a luminosity prefix to find the class', () => {
|
||
expect(parseSpectralClass('dM4')?.spectralClass).toBe('M');
|
||
expect(parseSpectralClass('sdM')?.spectralClass).toBe('M');
|
||
expect(parseSpectralClass('gK5')?.spectralClass).toBe('K');
|
||
});
|
||
|
||
it('takes the warmer end of a range', () => {
|
||
expect(parseSpectralClass('k-m')?.spectralClass).toBe('K');
|
||
expect(parseSpectralClass('g-k')?.spectralClass).toBe('G');
|
||
});
|
||
|
||
it('tolerates uncertainty flags and luminosity suffixes', () => {
|
||
expect(parseSpectralClass('K:')).toEqual({ spectralClass: 'K', subclass: 0 });
|
||
expect(parseSpectralClass('K5 V')).toEqual({ spectralClass: 'K', subclass: 5 });
|
||
expect(parseSpectralClass('m+')).toEqual({ spectralClass: 'M', subclass: 0 });
|
||
});
|
||
|
||
it('returns null when there is no recognisable class', () => {
|
||
for (const input of ['', ' ', '...', undefined, null]) {
|
||
expect(parseSpectralClass(input)).toBeNull();
|
||
}
|
||
});
|
||
|
||
it('ignores an out-of-range subclass rather than trusting it', () => {
|
||
expect(parseSpectralClass('M42')).toEqual({ spectralClass: 'M', subclass: 0 });
|
||
});
|
||
});
|
||
|
||
describe('isGiant', () => {
|
||
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', 'N5', 'Ce+', 'S57:']) {
|
||
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);
|
||
});
|
||
|
||
it('makes hot classes blue (negative) and cool classes red (positive)', () => {
|
||
expect(spectralTypeToColorIndex('O5')).toBeLessThan(0);
|
||
expect(spectralTypeToColorIndex('B0')).toBeLessThan(0);
|
||
expect(spectralTypeToColorIndex('M5')).toBeGreaterThan(1);
|
||
});
|
||
|
||
it('increases monotonically from hot to cool across the sequence', () => {
|
||
const values = SPECTRAL_CLASSES.map((spectralClass) => spectralTypeToColorIndex(spectralClass)!);
|
||
expect([...values].sort((a, b) => a - b)).toEqual(values);
|
||
});
|
||
|
||
it('interpolates between class anchors by subclass', () => {
|
||
const g0 = spectralTypeToColorIndex('G0')!;
|
||
const g5 = spectralTypeToColorIndex('G5')!;
|
||
const k0 = spectralTypeToColorIndex('K0')!;
|
||
|
||
expect(g5).toBeGreaterThan(g0);
|
||
expect(g5).toBeLessThan(k0);
|
||
expect(g5).toBeCloseTo((g0 + k0) / 2, 6);
|
||
});
|
||
|
||
it('keeps the coolest subclasses inside a sane range', () => {
|
||
const m9 = spectralTypeToColorIndex('M9')!;
|
||
expect(m9).toBeGreaterThan(spectralTypeToColorIndex('M0')!);
|
||
expect(m9).toBeLessThanOrEqual(2);
|
||
});
|
||
|
||
it('returns null for an unclassified star', () => {
|
||
expect(spectralTypeToColorIndex('Unknown')).toBeNull();
|
||
expect(spectralTypeToColorIndex('')).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe('temperatureToColorIndex', () => {
|
||
it("puts the Sun's temperature at its own B-V and a cool dwarf where the dwarf sequence has it", () => {
|
||
expect(temperatureToColorIndex(5772)).toBeCloseTo(0.65, 2);
|
||
// Two thirds of the way from M2 (3 560 K, B−V 1.505) to M2.5 (3 470 K, 1.522).
|
||
expect(temperatureToColorIndex(3500)).toBeCloseTo(1.5163, 4);
|
||
});
|
||
|
||
it('reads back as the temperature it came from, so the correction is the one at that temperature', () => {
|
||
for (const temperatureK of [31400, 12000, 7000, 5772, 4000, 3500, 3157, 2566, 2420]) {
|
||
expect(dwarfSequenceAtColor(temperatureToColorIndex(temperatureK))!.temperatureK).toBeCloseTo(temperatureK, 6);
|
||
}
|
||
});
|
||
|
||
it('has no answer outside the table, nor for a temperature that is not one', () => {
|
||
for (const temperatureK of [580, 2419, 31401, 50000, 0, Number.NaN]) {
|
||
expect(temperatureToColorIndex(temperatureK)).toBeNull();
|
||
}
|
||
});
|
||
});
|
||
|
||
describe('spectralTypeFromColor', () => {
|
||
it("reads the Sun's type off either colour", () => {
|
||
expect(spectralTypeFromColor(0.65, 'B-V')).toBe('G2');
|
||
expect(spectralTypeFromColor(0.82, 'BP-RP')).toBe('G2');
|
||
});
|
||
|
||
it('reads a red dwarf the way it was classified', () => {
|
||
// TRAPPIST-1 is M8 V, and Gaia has it at BP−RP 4.90; Proxima is M5.5 Ve at B−V 1.81.
|
||
expect(spectralTypeFromColor(4.902, 'BP-RP')).toBe('M8');
|
||
expect(spectralTypeFromColor(1.807, 'B-V')).toBe('M5');
|
||
});
|
||
|
||
it('does not read one colour as the other', () => {
|
||
// 1.43 is a K5 dwarf in BP−RP and an M0 in B−V.
|
||
expect(spectralTypeFromColor(1.43, 'BP-RP')).toBe('K5');
|
||
expect(spectralTypeFromColor(1.43, 'B-V')).toBe('M0');
|
||
expect(spectralTypeFromColor(1.43)).toBe('M0');
|
||
});
|
||
|
||
it('has no answer past either end of the table, nor without a colour', () => {
|
||
expect(spectralTypeFromColor(-0.35, 'B-V')).toBeNull();
|
||
expect(spectralTypeFromColor(-0.15, 'BP-RP')).toBeNull();
|
||
expect(spectralTypeFromColor(5.5, 'BP-RP')).toBeNull();
|
||
expect(spectralTypeFromColor(null, 'B-V')).toBeNull();
|
||
expect(spectralTypeFromColor(-0.301, 'B-V')).toBe('B0');
|
||
});
|
||
});
|
||
|
||
describe('spectralClassification', () => {
|
||
it("gives the catalogue's type, else the colour's marked as an estimate, else nothing", () => {
|
||
expect(spectralClassification({ spectralType: 'M5Ve', colorIndex: 1.807, colorSystem: 'B-V' })).toBe('M5Ve');
|
||
expect(spectralClassification({ spectralType: 'Unknown', colorIndex: 4.902, colorSystem: 'BP-RP' })).toBe('~M8');
|
||
expect(spectralClassification({ spectralType: 'Unknown', colorIndex: null })).toBe('');
|
||
});
|
||
});
|
||
|
||
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();
|
||
});
|
||
|
||
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(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('dwarfSequenceAtType', () => {
|
||
it("reads an O type off Mamajek's O rows, which carry no colour, and any other off its own row", () => {
|
||
expect(dwarfSequenceAtType('O7.5Iab:')).toEqual({ bMinusV: null, temperatureK: 36100, bolometricCorrectionV: -3.33, gMinusV: null });
|
||
expect(dwarfSequenceAtType('B8Ia')!.temperatureK).toBe(12300);
|
||
expect(dwarfSequenceAtType('O9.7')!.temperatureK).toBeCloseTo(31900 - 500 * (0.2 / 0.5), 6);
|
||
expect(dwarfSequenceAtType('M9')!.temperatureK).toBe(2420);
|
||
expect(dwarfSequenceAtType('Unknown')).toBeNull();
|
||
});
|
||
});
|
||
|
||
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);
|
||
});
|
||
});
|