The map held 8750 stars within 50 pc and rendered 371 systems. Both were lower than they needed to be, for different reasons. The star catalogue was capped by its own encoding as much as by the cutoff: one JSON object per star, eight key names repeated each time, 157 bytes a star. At the range HYG actually reaches that is 17 MB to download and parse before the first frame. So the numbers move into two binary column stores — positions in stars.bin, which the GPU is handed verbatim, and id/magnitude/colour/spectral index in stars-meta.bin — and the JSON keeps only the strings, with 2600 distinct spectral classifications collapsed to a dictionary. The layout is defined once, in star-catalog.ts, and the ETL and the app both use it, so the writer and the reader cannot drift. The cutoff then goes to 250 pc: 68388 stars, 7.8x as many for 1.7x the bytes. That is where HYG's measurements stop rather than a round number — 98.6% of its rows are Hipparcos, whose parallaxes are good to about a milliarcsecond, so beyond 250 pc it would be plotting noise. Drawing all of them is a separate question from knowing them, and it is answered separately. The field draws a budget: every star inside 25 pc, because the nearest are faint red dwarfs and Proxima Centauri is magnitude 11, then the brightest of everything beyond. Search, navigation and the planet cross-reference still see the whole catalogue. A real GPU would draw all 68388 without noticing; the budget is for the machines that would not, and it is one constant. Systems were limited by something else entirely. The archive data already shipped named 4735 host stars and only 388 resolved, because the rest lay outside a 50 pc catalogue — and the cross-reference kept only its own result, so redoing it meant re-downloading an archive that is not reachable from here. Host coordinates are now stored with each planet, and the match is re-resolved at build time against whatever catalogue the run produced. Even name matching alone, which needs no coordinates and so works on the records already shipped, rescues 335 planets across 238 systems: 371 renderable systems become 609. Two selection rules were tuned for a 50 pc bubble and no longer fit. Tethers followed the Sun's nearest neighbours, which are a speck at this range, and now follow the brightest; labels were ranked by proximity, which named whatever sat nearest the middle of the screen, and are now ranked by brightness — so the view names Canopus, Achernar and Spica rather than a clump of catalogue designations. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaySiNst4HhDXBHnMy8p5G
104 lines
4.0 KiB
TypeScript
104 lines
4.0 KiB
TypeScript
import { writeFileSync } from 'node:fs';
|
|
|
|
import { buildStarNameIndex, resolveHostStarId } from '../../src/app/shared/astro/host-star-matching';
|
|
import { ExoplanetRecord } from '../../src/app/shared/models/exoplanet.model';
|
|
import { StarRecord } from '../../src/app/shared/models/star.model';
|
|
import { fetchStars } from './fetchStars';
|
|
import { parseCsvObjects, parseOptionalNumber } from './lib/csv';
|
|
import { fetchTextCached } from './lib/http';
|
|
import { dataPath, ensureDataDir } from './lib/paths';
|
|
|
|
const TAP_BASE_URL = 'https://exoplanetarchive.ipac.caltech.edu/TAP/sync';
|
|
const TAP_COLUMNS = [
|
|
'pl_name',
|
|
'hostname',
|
|
'ra',
|
|
'dec',
|
|
'sy_dist',
|
|
'pl_orbsmax',
|
|
'pl_orbeccen',
|
|
'pl_orbincl',
|
|
'pl_orblper',
|
|
'pl_orbper',
|
|
'pl_rade',
|
|
'pl_bmasse',
|
|
'st_mass',
|
|
'disc_year'
|
|
].join(',');
|
|
const TAP_QUERY = `select+${TAP_COLUMNS}+from+ps+where+default_flag=1&format=csv`;
|
|
const TAP_URL = `${TAP_BASE_URL}?query=${TAP_QUERY}`;
|
|
|
|
// A host star match must be within this many parsecs of the catalog position to be
|
|
// accepted as a cross-reference (guards against coincidental name/position collisions).
|
|
const MATCH_TOLERANCE_PC = 0.5;
|
|
|
|
/**
|
|
* Downloads confirmed exoplanets from the NASA Exoplanet Archive (`Planetary Systems` TAP
|
|
* table), cross-references each host star to the HYG index, and writes `exoplanets.json`.
|
|
*/
|
|
export async function fetchExoplanets(stars?: StarRecord[]): Promise<ExoplanetRecord[]> {
|
|
console.log('Fetching confirmed exoplanets from the NASA Exoplanet Archive...');
|
|
const knownStars = stars ?? (await fetchStars());
|
|
const nameIndex = buildStarNameIndex(knownStars);
|
|
|
|
const csv = await fetchTextCached(TAP_URL, 'exoplanet-archive-ps.csv');
|
|
const rows = parseCsvObjects(csv);
|
|
|
|
let matched = 0;
|
|
const exoplanets: ExoplanetRecord[] = rows.map((row, index) => {
|
|
// `parseOptionalNumber`, not `Number`: a blank cell would otherwise become 0, which is a
|
|
// finite, plausible-looking coordinate rather than the "not measured" it actually means.
|
|
const raDeg = parseOptionalNumber(row['ra']) ?? Number.NaN;
|
|
const decDeg = parseOptionalNumber(row['dec']) ?? Number.NaN;
|
|
const distancePc = parseOptionalNumber(row['sy_dist']) ?? Number.NaN;
|
|
|
|
const hostStarId = resolveHostStarId(
|
|
{ hostname: row['hostname'], raDeg, decDeg, distancePc },
|
|
knownStars,
|
|
MATCH_TOLERANCE_PC,
|
|
nameIndex
|
|
);
|
|
if (hostStarId !== null) {
|
|
matched++;
|
|
}
|
|
|
|
return {
|
|
id: row['pl_name'] || `exoplanet-${index}`,
|
|
hostStarId,
|
|
hostStarName: row['hostname'],
|
|
name: row['pl_name'],
|
|
radiusEarth: parseOptionalNumber(row['pl_rade']),
|
|
massEarth: parseOptionalNumber(row['pl_bmasse']),
|
|
discoveryYear: parseOptionalNumber(row['disc_year']),
|
|
// The period was already being downloaded and thrown away. With the semi-major axis it
|
|
// determines the host's gravitational parameter, so keeping it is the difference between
|
|
// propagating a planet at its real rate and pretending every host is the Sun.
|
|
periodDays: parseOptionalNumber(row['pl_orbper']),
|
|
hostStarMassSolar: parseOptionalNumber(row['st_mass']),
|
|
// Kept so the cross-reference can be redone without the archive; see the record's own
|
|
// documentation. Undefined rather than NaN, which JSON cannot represent.
|
|
hostRaDeg: parseOptionalNumber(row['ra']),
|
|
hostDecDeg: parseOptionalNumber(row['dec']),
|
|
hostDistancePc: parseOptionalNumber(row['sy_dist']),
|
|
orbit: {
|
|
semiMajorAxisAu: parseOptionalNumber(row['pl_orbsmax']),
|
|
eccentricity: parseOptionalNumber(row['pl_orbeccen']),
|
|
inclinationDeg: parseOptionalNumber(row['pl_orbincl']),
|
|
argumentOfPeriapsisDeg: parseOptionalNumber(row['pl_orblper'])
|
|
}
|
|
};
|
|
});
|
|
|
|
ensureDataDir();
|
|
writeFileSync(dataPath('exoplanets.json'), JSON.stringify(exoplanets));
|
|
console.log(` wrote ${exoplanets.length} exoplanets (${matched} cross-referenced to a HYG host star).`);
|
|
return exoplanets;
|
|
}
|
|
|
|
if (require.main === module) {
|
|
fetchExoplanets().catch((error) => {
|
|
console.error(error);
|
|
process.exitCode = 1;
|
|
});
|
|
}
|