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) <noreply@anthropic.com>
206 lines
9.5 KiB
TypeScript
206 lines
9.5 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
|
||
import { dwarfSequenceAtColor, dwarfSequenceAtTemperature, 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('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);
|
||
});
|
||
});
|