Files
star-map/src/app/shared/astro/stellar.ts
T
Claude ac296f5133 Derive a surface for every body that was never photographed
Fifteen bodies here have a real photograph. Every exoplanet does not, and
never will on current instruments — none has ever been imaged — and nor do
several of the solar system's own moons. Those all shared one crude
stand-in: a few noisy bands tinted by category, cached per colour, so
every exoplanet in the app was literally the same picture.

They now get a surface reasoned from what has actually been measured.

The chain is standard at every link. A host star's luminosity comes from
its catalogued apparent magnitude and its parallax distance — that pair is
exactly an absolute magnitude — plus a bolometric correction for its
spectral class. The correction is not optional: an M dwarf radiates most
of its light in the infrared, so its visual magnitude understates it more
than tenfold, and M dwarfs are what most nearby planet hosts are.
Luminosity and the semi-major axis then give an equilibrium temperature,
mass and radius give a bulk density, and size, temperature and density
together give a class of world.

Checked against the solar system the temperatures land on Earth 255 K,
Jupiter 112 K, Neptune 46 K, all within a kelvin or two of published
values, and 51 Pegasi b comes out at 1227 K against a published 1200.

Each class carries a palette reasoned from its chemistry — methane absorbs
red light, which is why the ice giants are blue — and a structure: zonal
bands for a body with a fluid envelope, because a rapidly rotating
atmosphere organises into them, and fractal terrain for one with a solid
surface. Polar caps grow and shrink with the derived temperature, which is
the clearest visible consequence of the whole chain.

The generator samples three-dimensional noise along the sphere rather than
a flat field, so there is no seam to stitch at the antimeridian and no
pinching at the poles, and it writes into a byte array rather than a
canvas — a pure function, testable, with no 2D context to be unavailable.

Two things the derivation cannot do, both stated on screen next to the
measurements it rests on. Equilibrium temperature ignores greenhouse
warming and internal heat, so Venus comes out at 300 K against a real
surface of 737 K and Io, kept molten by tides, classifies as ice. And
these are illustrations: reasoned, but not observations.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G
2026-08-05 06:52:22 +00:00

125 lines
5.2 KiB
TypeScript

import { parseSpectralClass, SpectralClass } from './spectral';
/**
* Stellar luminosity, derived from the two things the star catalogue actually measures.
*
* Nothing here is a published luminosity: HYG carries apparent magnitude and a parallax, and
* the Exoplanet Archive columns that would give a host star's mass or effective temperature are
* not in the shipped dataset. What those two measurements do give, exactly, is absolute
* magnitude — and from there the bolometric correction below turns a V-band brightness into a
* total energy output, which is what a planet's temperature actually depends on.
*/
/** The Sun's absolute magnitude in V — what the distance modulus below is measured against. */
export const SOLAR_ABSOLUTE_MAGNITUDE_V = 4.83;
/**
* The Sun's absolute *bolometric* magnitude, the IAU 2015 zero point. Distinct from the V-band
* figure above by the Sun's own bolometric correction, and it is the one the ratio is taken
* against — mixing the two would leave every luminosity 9% high.
*/
export const SOLAR_BOLOMETRIC_MAGNITUDE = 4.74;
/**
* Bolometric corrections for main-sequence stars, at subclass 0 of each class (Pecaut & Mamajek
* 2013, rounded). Always negative: a star radiates outside the V band as well as in it, so its
* total output always exceeds what a visual magnitude alone implies.
*
* The correction matters most exactly where it is largest. An M dwarf emits the bulk of its
* light in the infrared, so taking its V magnitude at face value understates it by more than a
* factor of ten — and M dwarfs are what most of the nearby planet hosts are.
*/
const BOLOMETRIC_CORRECTION_ANCHORS: Readonly<Record<SpectralClass, number>> = {
O: -4.0,
B: -3.0,
A: -0.25,
F: -0.01,
G: -0.06,
K: -0.24,
M: -1.21
};
/** Correction at the cool end of class M, so the latest subclasses interpolate toward it. */
const BEYOND_M_CORRECTION = -4.6;
/**
* Range the derived luminosity is clamped to, in solar luminosities.
*
* A guard against the one systematic error this method cannot detect on its own: the
* corrections above assume a main-sequence star, and HYG often records a spectral class with no
* luminosity class at all. A red giant read as a K dwarf comes out hundreds of times too
* bright, which is a large error but not an unbounded one — these bounds simply keep a
* pathological record from producing a temperature of a million kelvin.
*/
const MIN_LUMINOSITY_SOLAR = 1e-6;
const MAX_LUMINOSITY_SOLAR = 1e7;
/**
* Absolute magnitude from apparent magnitude and distance — the distance modulus.
*
* Returns `null` for a star at zero distance, which in this catalogue means the Sun: its
* apparent magnitude of -26.7 is a statement about how close it is, not about how bright it is,
* and the formula has no answer there.
*/
export function absoluteMagnitude(apparentMagnitude: number, distancePc: number): number | null {
if (!Number.isFinite(apparentMagnitude) || !Number.isFinite(distancePc) || distancePc <= 0) {
return null;
}
return apparentMagnitude - 5 * Math.log10(distancePc) + 5;
}
/**
* Bolometric correction for a spectral type, interpolated between the class anchors. Falls back
* to the solar value when the catalogue records no usable classification, which biases a
* misclassified red dwarf dim rather than inventing a correction for it.
*/
export function bolometricCorrection(spectralType: string | null | undefined): number {
const parsed = parseSpectralClass(spectralType);
if (!parsed) {
return BOLOMETRIC_CORRECTION_ANCHORS.G;
}
const { spectralClass, subclass } = parsed;
const classes = Object.keys(BOLOMETRIC_CORRECTION_ANCHORS) as SpectralClass[];
const index = classes.indexOf(spectralClass);
const from = BOLOMETRIC_CORRECTION_ANCHORS[spectralClass];
const to = index < classes.length - 1 ? BOLOMETRIC_CORRECTION_ANCHORS[classes[index + 1]] : BEYOND_M_CORRECTION;
const t = Math.min(Math.max(subclass, 0), 10) / 10;
return from + (to - from) * t;
}
/** Everything about a star that bears on how much light it puts out. */
export interface StellarPhotometry {
/** Apparent visual magnitude, as catalogued. */
magnitude: number;
/** Distance from the Sun in parsecs; `0` identifies the Sun itself. */
distancePc: number;
spectralType?: string;
}
/**
* Total luminosity in solar units.
*
* The Sun is returned as exactly 1 rather than derived — it is the definition of the unit, and
* it is the one star whose distance in this catalogue is zero.
*
* Accurate to roughly a factor of two for main-sequence stars, which is better than it sounds
* for what it is used for: a planet's equilibrium temperature goes as the fourth root of this,
* so even a factor of two moves a temperature by less than a fifth.
*/
export function luminositySolar(star: StellarPhotometry): number | null {
if (star.distancePc === 0) {
return 1;
}
const absolute = absoluteMagnitude(star.magnitude, star.distancePc);
if (absolute === null) {
return null;
}
const bolometric = absolute + 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);
}