Stop telling the reader that the hundred directly imaged exoplanets were never imaged
Every exoplanet card without a map ended "Not an observation — no image of this world exists.",
and 395b613 wrote that rule into a comment ("Only an exoplanet has never been imaged") and a test.
The NASA Exoplanet Archive flags 102 planets as detected by imaging (ima_flag), all 102 of them in
exoplanets.json: HR 8799 b to e (Marois et al. 2008), bet Pic b, 51 Eri b, AF Lep b, and bet Pic c
and eps Ind A b, found by radial velocity and imaged since. The review read the sentence on the
live pages of HR 8799 b, 51 Eri b and bet Pic b.
The ETL now asks the archive for those names in a query of its own, cached apart from the main
table so the other 6 252 planets stay on the snapshot they were built from, and carries
`imaged: true` on the matching records. exoplanets.json changes by that field on 102 records and
nothing else (compared record by record). Their cards now end "Not an observation — it has been
imaged only as a point of light beside its star, and no map of it exists."; the rest keep "no
image of this world exists". The provenance comment and the texture catalogue's comment say which
is which.
Checked live on :4301: HR 8799 b and eps Ind A b carry the new sentence, Kepler-22 b the old one.
Tests: body-view-model.spec 'says a directly imaged exoplanet was seen as a point of light, not
that no image of it exists'; the old test is renamed 'says an exoplanet the archive does not flag
as imaged has no image'. Unit controls, each failing that test only (1 failed, 840 passed): the
provenance ignoring the flag; the view model dropping it. build.ts now requires at least 95
imaged planets (measured 102); control, the full ETL with the join made on the host's name
instead of the planet's, fails with "Only 0 exoplanets are marked as imaged".
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -32,6 +32,8 @@ export interface BodyDetailViewModel {
|
|||||||
appearance: PlanetAppearance;
|
appearance: PlanetAppearance;
|
||||||
/** True when a real photograph is being shown rather than the derived surface. */
|
/** True when a real photograph is being shown rather than the derived surface. */
|
||||||
hasPhotography: boolean;
|
hasPhotography: boolean;
|
||||||
|
/** An exoplanet photographed by direct imaging, as a point of light; see `ExoplanetRecord.imaged`. */
|
||||||
|
imaged?: boolean;
|
||||||
/**
|
/**
|
||||||
* Sidereal orbital period. For a solar-system body, 360 degrees over JPL's published mean
|
* Sidereal orbital period. For a solar-system body, 360 degrees over JPL's published mean
|
||||||
* motion; for an exoplanet, the archive's period where it published one, and undefined where not.
|
* motion; for an exoplanet, the archive's period where it published one, and undefined where not.
|
||||||
|
|||||||
@@ -75,15 +75,22 @@ export function bodyReadouts(body: BodyDetailViewModel): BodyReadouts {
|
|||||||
* The derived surface is a reasoned illustration, and a panel of real measurements sitting next
|
* The derived surface is a reasoned illustration, and a panel of real measurements sitting next
|
||||||
* to it is exactly the context in which it could be mistaken for another one.
|
* to it is exactly the context in which it could be mistaken for another one.
|
||||||
*
|
*
|
||||||
* Only an exoplanet has never been imaged. A moon or dwarf planet drawn this way has been — Voyager
|
* A moon or dwarf planet drawn this way has been imaged — Voyager 2 photographed Uranus's five
|
||||||
* 2 photographed Uranus's five large moons, Proteus and Nereid, Cassini Hyperion, and Hubble sees
|
* large moons, Proteus and Nereid, Cassini Hyperion, and Hubble sees Eris, Haumea and Makemake as
|
||||||
* Eris, Haumea and Makemake as points — but has no global map this app can use.
|
* points — but has no global map this app can use. So have the hundred or so exoplanets the
|
||||||
|
* archive flags as imaged, HR 8799's four among them, though only as points of light beside their
|
||||||
|
* star. Only the other exoplanets, known from what they do to starlight, have no image at all.
|
||||||
*/
|
*/
|
||||||
function provenanceFor(body: BodyDetailViewModel): string {
|
function provenanceFor(body: BodyDetailViewModel): string {
|
||||||
if (body.hasPhotography) {
|
if (body.hasPhotography) {
|
||||||
return 'Surface: NASA/ESA/USGS photography.';
|
return 'Surface: NASA/ESA/USGS photography.';
|
||||||
}
|
}
|
||||||
const why = body.kind === 'exoplanet' ? 'no image of this world exists' : 'no global map of this world is used here';
|
const why =
|
||||||
|
body.kind !== 'exoplanet'
|
||||||
|
? 'no global map of this world is used here'
|
||||||
|
: body.imaged
|
||||||
|
? 'it has been imaged only as a point of light beside its star, and no map of it exists'
|
||||||
|
: 'no image of this world exists';
|
||||||
return body.appearance.equilibriumTemperatureK === null
|
return body.appearance.equilibriumTemperatureK === null
|
||||||
? `Surface illustrated from this body’s measured size and mass. Its host star is not in the catalogue, so no temperature could be derived. Not an observation — ${why}.`
|
? `Surface illustrated from this body’s measured size and mass. Its host star is not in the catalogue, so no temperature could be derived. Not an observation — ${why}.`
|
||||||
: `Surface illustrated from the measurements above — size, density and the temperature derived from its star’s output and its orbit. Not an observation — ${why}.`;
|
: `Surface illustrated from the measurements above — size, density and the temperature derived from its star’s output and its orbit. Not an observation — ${why}.`;
|
||||||
|
|||||||
@@ -91,12 +91,20 @@ describe('buildBodyViewModel', () => {
|
|||||||
expect(provenance).not.toContain('no image of this world exists');
|
expect(provenance).not.toContain('no image of this world exists');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('says an exoplanet has never been imaged', () => {
|
it('says an exoplanet the archive does not flag as imaged has no image', () => {
|
||||||
const exoplanet: ExoplanetRecord = { id: 'x', hostStarId: SUN_STAR_ID, hostStarName: 'Sol', name: 'X b', orbit: { semiMajorAxisAu: 0.05 } };
|
const exoplanet: ExoplanetRecord = { id: 'x', hostStarId: SUN_STAR_ID, hostStarName: 'Sol', name: 'X b', orbit: { semiMajorAxisAu: 0.05 } };
|
||||||
const model = buildBodyViewModel('x', { bodies: [], exoplanets: [exoplanet], stars: [sun] })!;
|
const model = buildBodyViewModel('x', { bodies: [], exoplanets: [exoplanet], stars: [sun] })!;
|
||||||
expect(bodyReadouts(model).provenance).toContain('Not an observation — no image of this world exists.');
|
expect(bodyReadouts(model).provenance).toContain('Not an observation — no image of this world exists.');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('says a directly imaged exoplanet was seen as a point of light, not that no image of it exists', () => {
|
||||||
|
// HR 8799 b: photographed beside its star at Gemini and Keck (Marois et al. 2008).
|
||||||
|
const exoplanet: ExoplanetRecord = { id: 'HR 8799 b', hostStarId: SUN_STAR_ID, hostStarName: 'HR 8799', name: 'HR 8799 b', imaged: true, orbit: { semiMajorAxisAu: 68 } };
|
||||||
|
const provenance = bodyReadouts(buildBodyViewModel('HR 8799 b', { bodies: [], exoplanets: [exoplanet], stars: [sun] })!).provenance;
|
||||||
|
expect(provenance).toContain('Not an observation — it has been imaged only as a point of light beside its star, and no map of it exists.');
|
||||||
|
expect(provenance).not.toContain('no image of this world exists');
|
||||||
|
});
|
||||||
|
|
||||||
it('marks a published exoplanet period as measured, not derived', () => {
|
it('marks a published exoplanet period as measured, not derived', () => {
|
||||||
const exoplanet: ExoplanetRecord = {
|
const exoplanet: ExoplanetRecord = {
|
||||||
id: 'kepler-22-b',
|
id: 'kepler-22-b',
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ export function buildBodyViewModel(id: string, catalogues: BodyCatalogues): Body
|
|||||||
orbit: exoplanet.orbit,
|
orbit: exoplanet.orbit,
|
||||||
appearance: appearanceForExoplanet(exoplanet, luminosityOf(hostStar)),
|
appearance: appearanceForExoplanet(exoplanet, luminosityOf(hostStar)),
|
||||||
hasPhotography: bodyTexturePath(exoplanet.id) !== undefined,
|
hasPhotography: bodyTexturePath(exoplanet.id) !== undefined,
|
||||||
|
imaged: exoplanet.imaged,
|
||||||
// `periodDays` is populated for none of the shipped records, and deriving one would need the
|
// `periodDays` is populated for none of the shipped records, and deriving one would need the
|
||||||
// host star's mass, which is equally absent. Left undefined rather than assuming a solar-mass
|
// host star's mass, which is equally absent. Left undefined rather than assuming a solar-mass
|
||||||
// host, which would silently mis-state the period of every planet around an M dwarf.
|
// host, which would silently mis-state the period of every planet around an M dwarf.
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ export interface ExoplanetRecord {
|
|||||||
radiusEarth?: number;
|
radiusEarth?: number;
|
||||||
massEarth?: number;
|
massEarth?: number;
|
||||||
discoveryYear?: number;
|
discoveryYear?: number;
|
||||||
|
/**
|
||||||
|
* True where the archive flags the planet as detected by imaging (`ima_flag`): photographed as a
|
||||||
|
* point of light beside its star, as HR 8799's four planets were. Absent for every other planet.
|
||||||
|
*/
|
||||||
|
imaged?: true;
|
||||||
/**
|
/**
|
||||||
* Measured orbital period in days (`pl_orbper`). Together with the semi-major axis this
|
* Measured orbital period in days (`pl_orbper`). Together with the semi-major axis this
|
||||||
* pins the host star's gravitational parameter exactly, so the planet can be propagated at
|
* pins the host star's gravitational parameter exactly, so the planet can be propagated at
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import * as THREE from 'three/webgpu';
|
|||||||
* keyed by the same ids used in `bodies.json`.
|
* keyed by the same ids used in `bodies.json`.
|
||||||
*
|
*
|
||||||
* Only surface *maps* belong here: equirectangular images, twice as wide as tall, that wrap a
|
* Only surface *maps* belong here: equirectangular images, twice as wide as tall, that wrap a
|
||||||
* sphere. Everything else — every exoplanet, since not one has ever been imaged, and every moon
|
* sphere. Everything else — every exoplanet, since the few imaged were seen only as points of
|
||||||
|
* light, and every moon
|
||||||
* or dwarf planet with no such map in the repository — falls through to
|
* or dwarf planet with no such map in the repository — falls through to
|
||||||
* `procedural-planet-texture.ts`, which derives a surface from the body's own measured size,
|
* `procedural-planet-texture.ts`, which derives a surface from the body's own measured size,
|
||||||
* mass, orbit and host star instead.
|
* mass, orbit and host star instead.
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -421,8 +421,17 @@ function validateExoplanets(exoplanets: ExoplanetRecord[], starIds: Set<number>)
|
|||||||
const withPeriod = exoplanets.filter((exoplanet) => exoplanet.periodDays !== undefined).length;
|
const withPeriod = exoplanets.filter((exoplanet) => exoplanet.periodDays !== undefined).length;
|
||||||
const withHostMass = exoplanets.filter((exoplanet) => exoplanet.hostStarMassSolar !== undefined).length;
|
const withHostMass = exoplanets.filter((exoplanet) => exoplanet.hostStarMassSolar !== undefined).length;
|
||||||
console.log(` ${withPeriod}/${exoplanets.length} have a measured period, ${withHostMass} a host star mass.`);
|
console.log(` ${withPeriod}/${exoplanets.length} have a measured period, ${withHostMass} a host star mass.`);
|
||||||
|
|
||||||
|
// The planets photographed by direct imaging, whose card must not say no image of them exists.
|
||||||
|
// Measured: 102 of 102 flagged in the archive are in the catalogue. What this catches is the
|
||||||
|
// join by name failing, which would put every one of them back under "no image".
|
||||||
|
const imaged = exoplanets.filter((exoplanet) => exoplanet.imaged).length;
|
||||||
|
assertCondition(imaged >= MIN_IMAGED_EXOPLANETS, `Only ${imaged} exoplanets are marked as imaged (at least ${MIN_IMAGED_EXOPLANETS} expected).`);
|
||||||
|
console.log(` ${imaged} were imaged directly.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MIN_IMAGED_EXOPLANETS = 95;
|
||||||
|
|
||||||
const UNIT_VECTOR_TOLERANCE = 1e-6;
|
const UNIT_VECTOR_TOLERANCE = 1e-6;
|
||||||
|
|
||||||
function validateDeepSky(objects: DeepSkyRecord[]): void {
|
function validateDeepSky(objects: DeepSkyRecord[]): void {
|
||||||
|
|||||||
@@ -41,6 +41,13 @@ const TAP_URL = `${TAP_BASE_URL}?query=${TAP_QUERY}`;
|
|||||||
// silently wrong rather than visibly broken.
|
// silently wrong rather than visibly broken.
|
||||||
const CACHE_FILE = `exoplanet-archive-ps-${createHash('sha1').update(TAP_URL).digest('hex').slice(0, 8)}.csv`;
|
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`): 102 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.
|
||||||
|
const IMAGED_URL = `${TAP_BASE_URL}?query=select+pl_name+from+ps+where+default_flag=1+and+ima_flag=1+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
|
* 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`.
|
* table), cross-references each host star to the HYG index, and writes `exoplanets.json`.
|
||||||
@@ -52,6 +59,7 @@ export async function fetchExoplanets(stars?: StarRecord[]): Promise<ExoplanetRe
|
|||||||
|
|
||||||
const csv = await fetchTextCached(TAP_URL, CACHE_FILE);
|
const csv = await fetchTextCached(TAP_URL, CACHE_FILE);
|
||||||
const rows = parseCsvObjects(csv);
|
const rows = parseCsvObjects(csv);
|
||||||
|
const imaged = new Set(parseCsvObjects(await fetchTextCached(IMAGED_URL, IMAGED_CACHE_FILE)).map((row) => row['pl_name']));
|
||||||
|
|
||||||
let matched = 0;
|
let matched = 0;
|
||||||
const exoplanets: ExoplanetRecord[] = rows.map((row, index) => {
|
const exoplanets: ExoplanetRecord[] = rows.map((row, index) => {
|
||||||
@@ -80,6 +88,7 @@ export async function fetchExoplanets(stars?: StarRecord[]): Promise<ExoplanetRe
|
|||||||
radiusEarth: parseOptionalNumber(row['pl_rade']),
|
radiusEarth: parseOptionalNumber(row['pl_rade']),
|
||||||
massEarth: parseOptionalNumber(row['pl_bmasse']),
|
massEarth: parseOptionalNumber(row['pl_bmasse']),
|
||||||
discoveryYear: parseOptionalNumber(row['disc_year']),
|
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
|
// 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
|
// 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.
|
// propagating a planet at its real rate and pretending every host is the Sun.
|
||||||
|
|||||||
Reference in New Issue
Block a user