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>
258 lines
15 KiB
TypeScript
258 lines
15 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
|
||
import {
|
||
absoluteMagnitude,
|
||
blackbodyColor,
|
||
bolometricCorrection,
|
||
effectiveTemperatureK,
|
||
luminositySolar,
|
||
radiusFromLuminositySolar,
|
||
SOLAR_ABSOLUTE_MAGNITUDE_V,
|
||
SOLAR_BOLOMETRIC_MAGNITUDE,
|
||
SOLAR_EFFECTIVE_TEMPERATURE_K
|
||
} from './stellar';
|
||
|
||
/** Real catalogue rows, with the published luminosity each one should reproduce. */
|
||
const SIRIUS = { magnitude: -1.44, distancePc: 2.6371, spectralType: 'A0m...', publishedLuminosity: 25.4 };
|
||
const VEGA = { magnitude: 0.03, distancePc: 7.68, spectralType: 'A0Vvar', publishedLuminosity: 40 };
|
||
const PROXIMA = { magnitude: 11.01, distancePc: 1.2959, spectralType: 'M5Ve', publishedLuminosity: 0.0015 };
|
||
const ALPHA_CEN_A = { magnitude: -0.01, distancePc: 1.3247, spectralType: 'G2V', publishedLuminosity: 1.52 };
|
||
|
||
describe('absoluteMagnitude', () => {
|
||
it('is the apparent magnitude at the reference distance of ten parsecs', () => {
|
||
expect(absoluteMagnitude(5, 10)).toBeCloseTo(5, 12);
|
||
});
|
||
|
||
it('brightens a star as it is placed further away for the same apparent magnitude', () => {
|
||
expect(absoluteMagnitude(5, 100)).toBeLessThan(absoluteMagnitude(5, 10)!);
|
||
});
|
||
|
||
it('reproduces the published absolute magnitude of Sirius', () => {
|
||
expect(absoluteMagnitude(SIRIUS.magnitude, SIRIUS.distancePc)).toBeCloseTo(1.45, 1);
|
||
});
|
||
|
||
it('has no answer at zero distance, which in this catalogue is the Sun', () => {
|
||
expect(absoluteMagnitude(-26.7, 0)).toBeNull();
|
||
expect(absoluteMagnitude(5, -3)).toBeNull();
|
||
expect(absoluteMagnitude(Number.NaN, 10)).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe('bolometricCorrection', () => {
|
||
it('is never positive: a star always radiates outside the V band as well as in it', () => {
|
||
for (const type of ['O5V', 'B2V', 'A0V', 'F5V', 'G2V', 'K5V', 'M5V', 'M9V', 'Unknown', '']) {
|
||
expect(bolometricCorrection(type)).toBeLessThanOrEqual(0);
|
||
}
|
||
});
|
||
|
||
it('is small for the Sun and large for a red dwarf, which is the whole reason it is applied', () => {
|
||
// An M dwarf emits most of its light in the infrared: taking its V magnitude at face value
|
||
// understates it by more than a factor of ten.
|
||
expect(Math.abs(bolometricCorrection('G2V'))).toBeLessThan(0.2);
|
||
expect(bolometricCorrection('M5V')).toBeLessThan(-2);
|
||
});
|
||
|
||
it('reproduces the Sun own correction closely enough to close the loop on the zero point', () => {
|
||
// The two solar magnitudes differ by exactly this correction, so a solar twin must come out
|
||
// at one solar luminosity.
|
||
expect(SOLAR_ABSOLUTE_MAGNITUDE_V + bolometricCorrection('G2V')).toBeCloseTo(SOLAR_BOLOMETRIC_MAGNITUDE, 1);
|
||
});
|
||
|
||
it('deepens monotonically from F through M, following the shift into the infrared', () => {
|
||
const sequence = ['F0V', 'G0V', 'K0V', 'M0V', 'M5V'].map((type) => bolometricCorrection(type));
|
||
for (let index = 1; index < sequence.length; index++) {
|
||
expect(sequence[index]).toBeLessThan(sequence[index - 1]);
|
||
}
|
||
});
|
||
|
||
it('falls back to a solar correction for an unclassified star rather than inventing one', () => {
|
||
expect(bolometricCorrection('Unknown')).toBeCloseTo(bolometricCorrection('G0V'), 6);
|
||
expect(bolometricCorrection(undefined)).toBeCloseTo(bolometricCorrection('G0V'), 6);
|
||
});
|
||
});
|
||
|
||
describe('luminositySolar', () => {
|
||
it('returns exactly one for the Sun, which defines the unit', () => {
|
||
expect(luminositySolar({ magnitude: -26.7, distancePc: 0, spectralType: 'G2V' })).toBe(1);
|
||
});
|
||
|
||
it('lands within a factor of two of the published luminosity for real stars', () => {
|
||
// The documented tolerance. It is looser than it sounds: equilibrium temperature goes as the
|
||
// fourth root of this, so a factor of two is under a fifth in temperature.
|
||
for (const star of [SIRIUS, VEGA, PROXIMA, ALPHA_CEN_A]) {
|
||
const derived = luminositySolar(star)!;
|
||
const ratio = derived / star.publishedLuminosity;
|
||
expect(ratio).toBeGreaterThan(0.5);
|
||
expect(ratio).toBeLessThan(2);
|
||
}
|
||
});
|
||
|
||
it('gets a solar analogue essentially exactly right', () => {
|
||
// Alpha Centauri A is the nearest star to a second Sun there is, so this is the case where
|
||
// an error would be a mistake rather than a tolerance.
|
||
expect(luminositySolar(ALPHA_CEN_A)!).toBeCloseTo(ALPHA_CEN_A.publishedLuminosity, 0);
|
||
});
|
||
|
||
it('orders stars the way their published luminosities do', () => {
|
||
const derived = [PROXIMA, ALPHA_CEN_A, SIRIUS, VEGA].map((star) => luminositySolar(star)!);
|
||
for (let index = 1; index < derived.length; index++) {
|
||
expect(derived[index]).toBeGreaterThan(derived[index - 1]);
|
||
}
|
||
});
|
||
|
||
it('applies the bolometric correction rather than taking V at face value', () => {
|
||
// Without it a red dwarf comes out more than ten times too dim.
|
||
const uncorrected = Math.pow(10, (SOLAR_BOLOMETRIC_MAGNITUDE - absoluteMagnitude(PROXIMA.magnitude, PROXIMA.distancePc)!) / 2.5);
|
||
expect(luminositySolar(PROXIMA)!).toBeGreaterThan(uncorrected * 5);
|
||
});
|
||
|
||
it('reads the correction off the colour where the catalogue has no type', () => {
|
||
// Barnard's Star, 0.0035 L☉ (Dawson & De Robertis 2004), as a star no one classified: the
|
||
// Sun's correction left it at an eighth of that.
|
||
const derived = luminositySolar({ magnitude: 9.54, distancePc: 1.8266, spectralType: 'Unknown', magnitudeBand: 'V', colorIndex: 1.57, colorSystem: 'B-V' })!;
|
||
expect(derived / 0.0035).toBeGreaterThan(1 / 1.5);
|
||
expect(derived / 0.0035).toBeLessThan(1.5);
|
||
});
|
||
|
||
it('carries a Gaia G magnitude to V before correcting it', () => {
|
||
// TRAPPIST-1 as Gaia has it, 5.53e-4 L☉ (Agol et al. 2021). Read as V its G is 3.1
|
||
// magnitudes too bright, and its luminosity comes out seventeen times too high.
|
||
const derived = luminositySolar({ magnitude: 15.6226, distancePc: 12.467, spectralType: 'Unknown', magnitudeBand: 'G', colorIndex: 4.902, colorSystem: 'BP-RP' })!;
|
||
expect(derived / 5.53e-4).toBeGreaterThan(1 / 1.5);
|
||
expect(derived / 5.53e-4).toBeLessThan(1.5);
|
||
});
|
||
|
||
it("reads a giant's temperature and correction both off its type, not the cooler dwarf's its colour reads as", () => {
|
||
// Antares, M1 Ib at B−V 1.87: 3 660 K (Ohnaka et al. 2013), where its colour's dwarf is 3 019.
|
||
const antares = { magnitude: 1.06, distancePc: 169.78, spectralType: 'M1Ib + B2.5V', magnitudeBand: 'V', colorIndex: 1.865, colorSystem: 'B-V' } as const;
|
||
expect(Math.abs(effectiveTemperatureK(antares)! - 3660)).toBeLessThan(100);
|
||
// Aldebaran, K5 III, 44.2 R☉ (Richichi & Roccatagliata 2005), to a tenth; Rigel, B8 Ia, 74.1
|
||
// (Baines et al. 2018), to a fifth. With a correction off the type beside the colour's
|
||
// temperature, Rigel came out 101.6; with K5's own correction at 3 902 K, Aldebaran 52.
|
||
const aldebaran = { magnitude: 0.87, distancePc: 20.433, spectralType: 'K5III', magnitudeBand: 'V', colorIndex: 1.538, colorSystem: 'B-V' } as const;
|
||
const rigel = { magnitude: 0.18, distancePc: 264.55, spectralType: 'B8Ia', magnitudeBand: 'V', colorIndex: -0.03, colorSystem: 'B-V' } as const;
|
||
for (const [star, published, tolerance] of [[aldebaran, 44.2, 1.1], [rigel, 74.1, 1.2]] as const) {
|
||
const radius = radiusFromLuminositySolar(luminositySolar(star)!, effectiveTemperatureK(star)!);
|
||
expect(radius / published).toBeGreaterThan(1 / tolerance);
|
||
expect(radius / published).toBeLessThan(tolerance);
|
||
}
|
||
});
|
||
|
||
it('reads a hot giant reddened by dust at its type, not at the cool star its colour reads as', () => {
|
||
// Menkib, O7.5 Iab at B−V 0.02: 14 R☉ (Krtička & Kubát 2010). At its colour's 9 517 K and its
|
||
// type's correction it was drawn at 95; the dust it is behind still leaves it dimmer than it is.
|
||
const menkib = { magnitude: 3.98, distancePc: 408.881, spectralType: 'O7.5Iab:', magnitudeBand: 'V', colorIndex: 0.016, colorSystem: 'B-V' } as const;
|
||
expect(effectiveTemperatureK(menkib)).toBeCloseTo(36100, 6);
|
||
const radius = radiusFromLuminositySolar(luminositySolar(menkib)!, effectiveTemperatureK(menkib)!);
|
||
expect(radius / 14).toBeGreaterThan(1 / 2.5);
|
||
expect(radius / 14).toBeLessThan(2.5);
|
||
});
|
||
|
||
it("gives a carbon star the carbon stars' correction and temperature, not the Sun's correction at an M dwarf's", () => {
|
||
// La Superba, C7 Iab: Bergeat et al. (2001) have it at bolometric magnitude 2.43, which at the
|
||
// catalogue's 310 pc is 8 090 L☉. The Sun's −0.06 at 2 420 K gave 544 L☉ and 133 R☉.
|
||
const laSuperba = { magnitude: 5.42, distancePc: 310.342, spectralType: 'C7Iab', magnitudeBand: 'V', colorIndex: 2.994, colorSystem: 'B-V' } as const;
|
||
expect(luminositySolar(laSuperba)! / 8090).toBeGreaterThan(1 / 1.2);
|
||
expect(luminositySolar(laSuperba)! / 8090).toBeLessThan(1.2);
|
||
expect(effectiveTemperatureK(laSuperba)).toBe(2990);
|
||
});
|
||
|
||
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);
|
||
expect(absurd).toBeLessThanOrEqual(1e7);
|
||
});
|
||
|
||
it('has no answer for a star with no usable distance', () => {
|
||
expect(luminositySolar({ magnitude: 5, distancePc: -1 })).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe('effectiveTemperatureK', () => {
|
||
it("is the Sun's own for the Sun", () => {
|
||
expect(effectiveTemperatureK({ magnitude: -26.7, distancePc: 0, colorIndex: 0.7 })).toBe(SOLAR_EFFECTIVE_TEMPERATURE_K);
|
||
});
|
||
|
||
it('reads a colour in its own system, and a spectral type where there is no colour', () => {
|
||
// An M5 dwarf is 3 060 K at B−V 1.83 or BP−RP 3.35. Its type alone goes through the colour
|
||
// `spectralTypeToColorIndex` gives it, B−V 1.70, and comes out a little warmer.
|
||
expect(effectiveTemperatureK({ magnitude: 11, distancePc: 5, colorIndex: 3.35, colorSystem: 'BP-RP' })).toBeCloseTo(3060, 0);
|
||
expect(effectiveTemperatureK({ magnitude: 11, distancePc: 5, colorIndex: 1.83, colorSystem: 'B-V' })).toBeCloseTo(3060, 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();
|
||
});
|
||
|
||
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 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' })).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 })!;
|
||
expect(effectiveTemperatureK({ magnitude: 9, distancePc: 184, spectralType: 'G5V', colorIndex: -0.319, colorSystem: 'B-V' })).toBe(g5);
|
||
expect(g5).toBeGreaterThan(5500);
|
||
});
|
||
});
|
||
|
||
describe('radiusFromLuminositySolar', () => {
|
||
it('is one for the Sun', () => {
|
||
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 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 [
|
||
[{ magnitude: -1.44, distancePc: 2.6371, magnitudeBand: 'V', colorIndex: 0.009, colorSystem: 'B-V' }, 1.711],
|
||
[{ magnitude: 15.6226, distancePc: 12.467, magnitudeBand: 'G', colorIndex: 4.902, colorSystem: 'BP-RP' }, 0.119]
|
||
] as const) {
|
||
const radius = radiusFromLuminositySolar(luminositySolar(star)!, effectiveTemperatureK(star)!);
|
||
expect(radius / published).toBeGreaterThan(0.8);
|
||
expect(radius / published).toBeLessThan(1.2);
|
||
}
|
||
});
|
||
});
|
||
|
||
describe('blackbodyColor', () => {
|
||
/** As the display shows it: sRGB-encoded, 0 to 255. */
|
||
const displayed = (rgb: readonly number[]) => rgb.map((v) => Math.round(255 * (v <= 0.0031308 ? 12.92 * v : 1.055 * v ** (1 / 2.4) - 0.055)));
|
||
|
||
it('gives the colours of the stars against the display white', () => {
|
||
// Charity's blackbody colour table (CIE 1931 2°, D65): 2 900 K #ffb662, 5 800 K #fff1e7, 9 600 K #d3ddff.
|
||
for (const [temperatureK, expected] of [[2900, [255, 182, 98]], [5800, [255, 241, 231]], [9600, [211, 221, 255]]] as const) {
|
||
displayed(blackbodyColor(temperatureK)).forEach((channel, i) => expect(Math.abs(channel - expected[i])).toBeLessThanOrEqual(5));
|
||
}
|
||
});
|
||
|
||
it("is white at the white point it is given, and an M dwarf's light orange-red against the Sun's", () => {
|
||
expect(blackbodyColor(SOLAR_EFFECTIVE_TEMPERATURE_K, SOLAR_EFFECTIVE_TEMPERATURE_K)).toEqual([1, 1, 1]);
|
||
const [r, g, b] = blackbodyColor(2566, SOLAR_EFFECTIVE_TEMPERATURE_K);
|
||
expect(r).toBe(1);
|
||
expect(g).toBeCloseTo(0.44, 2);
|
||
expect(b).toBeCloseTo(0.1, 2);
|
||
});
|
||
|
||
it('holds the ends of the fit, and never goes negative', () => {
|
||
expect(blackbodyColor(800)).toEqual(blackbodyColor(1667));
|
||
expect(blackbodyColor(60000)).toEqual(blackbodyColor(25000));
|
||
expect(Math.min(...blackbodyColor(1667))).toBe(0);
|
||
});
|
||
});
|