diff --git a/src/app/shared/astro/spectral.spec.ts b/src/app/shared/astro/spectral.spec.ts index daa67ec..6e63e9b 100644 --- a/src/app/shared/astro/spectral.spec.ts +++ b/src/app/shared/astro/spectral.spec.ts @@ -47,8 +47,8 @@ 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']) { + 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); } }); @@ -166,4 +166,10 @@ describe('dwarfSequenceAtColor', () => { 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({ 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(); + }); }); diff --git a/src/app/shared/astro/spectral.ts b/src/app/shared/astro/spectral.ts index 3169075..e1f7728 100644 --- a/src/app/shared/astro/spectral.ts +++ b/src/app/shared/astro/spectral.ts @@ -73,12 +73,14 @@ export function parseSpectralClass( const GIANT_LUMINOSITY_CLASS = /(? 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; } - 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 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; return { temperatureK: lerp(blue[3], red[3]), diff --git a/src/app/shared/astro/stellar.spec.ts b/src/app/shared/astro/stellar.spec.ts index b1dec4c..8a30ca5 100644 --- a/src/app/shared/astro/stellar.spec.ts +++ b/src/app/shared/astro/stellar.spec.ts @@ -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: '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', () => { @@ -165,6 +176,14 @@ describe('radiusFromLuminositySolar', () => { 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', () => { // 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 [ diff --git a/src/app/shared/astro/stellar.ts b/src/app/shared/astro/stellar.ts index 963226a..37742c1 100644 --- a/src/app/shared/astro/stellar.ts +++ b/src/app/shared/astro/stellar.ts @@ -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. @@ -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 // 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 sequence = sequenceAtColour(star); const absoluteV = absolute - (star.magnitudeBand === 'G' ? (sequence?.gMinusV ?? 0) : 0); const bolometric = 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); } +/** + * 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. */ export const SOLAR_EFFECTIVE_TEMPERATURE_K = 5772; @@ -154,8 +170,8 @@ export function effectiveTemperatureK(star: StellarPhotometry): number | null { if (star.distancePc === 0) { return SOLAR_EFFECTIVE_TEMPERATURE_K; } - const measured = star.colorIndex != null ? dwarfSequenceAtColor(star.colorIndex, star.colorSystem) : null; - return (measured ?? dwarfSequenceAtColor(spectralTypeToColorIndex(star.spectralType)))?.temperatureK ?? null; + // A type's colour past the table is an O star's, which B−V no longer tells apart from B0. + return (sequenceAtColour(star) ?? dwarfSequenceAtColor(spectralTypeToColorIndex(star.spectralType), 'B-V', true))?.temperatureK ?? null; } /**