Files
star-map/tools/etl/fetchExoplanets.ts
SenrokaiandClaude Opus 5.5 d449214309 Stop calling WASP-108 b imaged, and stop saying no map exists of an imaged exoplanet
48fd6fd took the archive's ima_flag as it stood, so WASP-108 b's card said it had been imaged as a
point of light beside its star. It is a transiting hot Jupiter (period 2.676 d, 0.04 AU out, 0.15 mas
at 258.8 pc). Its flag comes from Bohn et al. 2020 (A&A 635, A73), a VLT/SPHERE survey of transiting
planets' host stars, which imaged a 0.35 solar-mass companion 0.124" from WASP-108, not the planet.
The imaged query now also asks for tran_flag=0. The archive flags exactly one transiting planet as
imaged (queried today: WASP-108 b); with it left out the list has 101 names, the old 102 less that
one, and still above the validator's floor of 95. The new URL is cached under its own name, and
exoplanets.json, regenerated by the full ETL, changes by that one record's field and nothing else
(checked record by record against HEAD).

build.ts now fails if WASP-108 b is marked imaged. The count cannot see a false positive, and its
record carries neither a period nor an axis, so no separation check could. Control: the full ETL on
the old query (its cache still present) fails with "WASP-108 b is marked as imaged; it transits, and
only a companion star beside it was imaged (Bohn et al. 2020)."

The imaged sentence ended "and no map of it exists". Luhman 16 b, one of the 101 and a brown dwarf,
was mapped by Doppler imaging (Crossfield et al. 2014, Nature 505, 654). It now reads "and no map of
it is used here", which holds for all of them; the doc comment names Luhman 16 b. Control: the old
sentence fails "says a directly imaged exoplanet was seen as a point of light" only (1 failed, 843
passed of 844). Live on :4301: WASP-108 b and Kepler-22 b read "no image of this world exists";
Luhman 16 b and HR 8799 b read the new sentence.

48fd6fd missed a third copy of the claim it corrected: the body page's comment still said every
exoplanet gets a derived surface "since none has ever been imaged". It now says none has had its
surface imaged, as planet-appearance.ts and the README do.

A correction to 48fd6fd's message: HR 8799 b, c and d are Marois et al. 2008 (Science 322, 1348),
and e is Marois et al. 2010 (Nature 468, 1080), not "b to e (Marois et al. 2008)".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 23:23:11 +02:00

128 lines
6.0 KiB
TypeScript

import { createHash } from 'node:crypto';
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',
'sy_pmra',
'sy_pmdec',
'pl_orbsmax',
'pl_orbeccen',
'pl_orbincl',
'pl_orblper',
'pl_orbper',
'pl_rade',
'pl_bmasse',
'st_mass',
'disc_year'
].join(',');
// Ordered explicitly: without it the archive is free to return rows in any order, and a
// scheduled re-run of the ETL would then rewrite exoplanets.json — and commit a diff — when
// nothing was actually published. pl_name is unique among default_flag=1 rows, so the order
// is total and the output is a pure function of the archive's content.
const TAP_QUERY = `select+${TAP_COLUMNS}+from+ps+where+default_flag=1+order+by+pl_name&format=csv`;
const TAP_URL = `${TAP_BASE_URL}?query=${TAP_QUERY}`;
// The cache is keyed by the request it answers — endpoint included, since the cache records
// only that some response arrived: one cached before a column was added would otherwise keep
// serving rows without it, and a missing proper-motion cell reads as "does not move",
// silently wrong rather than visibly broken.
const CACHE_FILE = `exoplanet-archive-ps-${createHash('sha1').update(TAP_URL).digest('hex').slice(0, 8)}.csv`;
// The planets the archive flags as detected by imaging (`ima_flag`): 101 of them in September 2026,
// HR 8799's four and 51 Eri b among them, and bet Pic c and eps Ind A b, found by radial velocity
// and imaged since. Asked for on its own, so adding it did not refetch the table above and move
// every other planet to a newer snapshot. A transiting planet is left out: the one flagged,
// WASP-108 b, takes its flag from Bohn et al. 2020, a VLT/SPHERE survey of transiting planets' host
// stars that imaged a 0.35 solar-mass companion 0.124" from its star. The planet goes round in 2.68
// days, 0.04 AU out, 0.15 mas at its 259 pc, and no imager has resolved it.
const IMAGED_URL = `${TAP_BASE_URL}?query=select+pl_name+from+ps+where+default_flag=1+and+ima_flag=1+and+tran_flag=0+order+by+pl_name&format=csv`;
const IMAGED_CACHE_FILE = `exoplanet-archive-imaged-${createHash('sha1').update(IMAGED_URL).digest('hex').slice(0, 8)}.csv`;
/**
* 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, CACHE_FILE);
const rows = parseCsvObjects(csv);
const imaged = new Set(parseCsvObjects(await fetchTextCached(IMAGED_URL, IMAGED_CACHE_FILE)).map((row) => row['pl_name']));
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 pmRaMasPerYear = parseOptionalNumber(row['sy_pmra']);
const pmDecMasPerYear = parseOptionalNumber(row['sy_pmdec']);
const hostStarId = resolveHostStarId(
{ hostname: row['hostname'], raDeg, decDeg, distancePc, pmRaMasPerYear, pmDecMasPerYear },
knownStars,
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']),
imaged: imaged.has(row['pl_name']) || undefined,
// 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']),
hostPmRaMasPerYear: pmRaMasPerYear,
hostPmDecMasPerYear: pmDecMasPerYear,
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;
});
}