Regenerate the star catalogue, fixing 2331 names and 875 colours

Two ETL bugs, both fixed at the source and then re-run against HYG. Star ids,
ordering and positions are all unchanged, so stars.bin is byte-identical and
every exoplanet cross-reference still resolves.

Names. HYG's `gl` column already carries its own catalogue prefix ("Gl 581",
"GJ 3512"), unlike the bare numbers in `hd` and `hip`, so prefixing it again
produced 2331 of 8750 stars named "Gl GJ 1076". That corrupted three surfaces at
once: search, the on-screen labels, and exoplanet host-star name matching, which
compares normalised names and could never match "glgj1076" to "gj1076".

Colours. `Number(row['ci']) || 0` cannot tell a blank cell from a real zero, and
0 is a real B-V colour index meaning a hot blue-white A-type star. All 875
affected stars turned out to be blanks — the catalogue contains no genuine zero
inside the distance cutoff — so several hundred red dwarfs were rendering
blue-white. colorIndex is now `number | null` rather than defaulted, because any
numeric default is indistinguishable from a measurement.

Consumers resolve the gap from the spectral type instead. That needs real
parsing: HYG's `spect` column runs to 134 distinct spellings among the affected
stars alone, including a bare lowercase "m" for 354 of them, plus "k-m" ranges,
"dM4" luminosity prefixes and "K:" uncertainty flags. 622 of the 875 recover a
class this way — 497 of them M-class — and the remaining 253, which carry no
classification at all, fall back to neutral white.

The parse is anchored at the start of the string rather than scanning it. A scan
is the obvious implementation and is quietly wrong: the ETL writes the literal
"Unknown" for unclassified stars, that contains a K, and every one of those 253
would have been classified as an orange K-type. A test covers it.

Also lifts parseOptionalNumber out of fetchExoplanets into lib/csv, where both
fetchers now use it, and gives magnitude a faint default instead of 0 — no
current star is affected, but 0 would mean "as bright as Vega" and render an
unphotometered star as one of the largest points on the map.

Tests: 145 passing, up from 116, including the first coverage of
StarFieldRenderer. Build, both typechecks and the Playwright suite are green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G
This commit is contained in:
Claude
2026-08-04 10:56:30 +00:00
parent 06cf7d2a15
commit 4aca223027
10 changed files with 369 additions and 20 deletions
+14 -5
View File
@@ -2,13 +2,19 @@ import { writeFileSync } from 'node:fs';
import { raDecDistanceToXyz } from '../../src/app/shared/astro/coordinates';
import { StarRecord, SUN_STAR_ID } from '../../src/app/shared/models/star.model';
import { parseCsvObjects } from './lib/csv';
import { parseCsvObjects, parseOptionalNumber } from './lib/csv';
import { fetchTextCached } from './lib/http';
import { dataPath, ensureDataDir } from './lib/paths';
const HYG_CSV_URL = 'https://raw.githubusercontent.com/astronexus/HYG-Database/main/hyg/CURRENT/hygdata_v41.csv';
const HYG_UNKNOWN_DISTANCE_PC = 100000; // HYG's placeholder for unmeasured/unreliable parallax
/**
* Stand-in magnitude for a star with no photometry. Faint rather than 0, because 0 would mean
* "as bright as Vega" and render it as one of the largest points on the map.
*/
const UNKNOWN_MAGNITUDE = 15;
/** Stars within this distance (parsecs) of the Sun are kept for the galaxy view. */
const DISTANCE_CUTOFF_PC = Number(process.env['ETL_STAR_DISTANCE_PC'] ?? 50);
@@ -26,7 +32,10 @@ function resolveName(row: Record<string, string>): string {
return `HD ${row['hd']}`;
}
if (row['gl']) {
return `Gl ${row['gl']}`;
// Already a complete designation ("Gl 581", "GJ 3512"), unlike the bare numbers in `hd`
// and `hip` — prefixing it again produced 2331 stars named "Gl GJ 1076", which broke
// search, the on-screen labels, and exoplanet host-star name matching alike.
return row['gl'];
}
if (row['hip']) {
return `HIP ${row['hip']}`;
@@ -51,7 +60,7 @@ export async function fetchStars(): Promise<StarRecord[]> {
const distancePc = Number(row['dist']);
if (id === SUN_STAR_ID) {
stars.push({ id, name: 'Sol', x: 0, y: 0, z: 0, magnitude: Number(row['mag']), spectralType: row['spect'] || 'G2V', colorIndex: Number(row['ci']) || 0 });
stars.push({ id, name: 'Sol', x: 0, y: 0, z: 0, magnitude: parseOptionalNumber(row['mag']) ?? UNKNOWN_MAGNITUDE, spectralType: row['spect'] || 'G2V', colorIndex: parseOptionalNumber(row['ci']) ?? null });
continue;
}
@@ -73,9 +82,9 @@ export async function fetchStars(): Promise<StarRecord[]> {
x,
y,
z,
magnitude: Number(row['mag']) || 0,
magnitude: parseOptionalNumber(row['mag']) ?? UNKNOWN_MAGNITUDE,
spectralType: row['spect'] || 'Unknown',
colorIndex: Number(row['ci']) || 0
colorIndex: parseOptionalNumber(row['ci']) ?? null
});
}