Hold the published catalogue to its naked-eye stars, its host luminosities and five other things no check saw
Seven properties of the catalogue could be lost by a one-line slip in the ETL with every validator passing and the weekly job publishing the result. validateStars and validateExoplanets now refuse: - fewer than 8 800 stars of V 6.5 or brighter (8 886 measured), or Rigel, Deneb or Alnilam gone; with no magnitude handed to placementDistancePc, 7 378 are left; - a host luminosity that is not positive, or fewer than 90 % of planets carrying one (6 036 of 6 354): st_lum is published as log10(L/L_sun), and stored unconverted 3 307 read <= 0 and Proxima's -2.82; - fewer than 40 colours marked as read off a temperature (54 measured); - an archive-placed star more than 1 mas from its planets' published position carried from J2015.5 to J2000 (6.6e-8 mas at most measured; not carried, 6 277.9); - fewer than 300 HYG stars folded into Gaia keeping their more precise Hipparcos distance (384), or Tarazed and Eta Leo off theirs; - more than 5 archive stars numbered other than their name hashes to (1 measured). These come from an interrupted earlier attempt left uncommitted in this worktree; its other half, asymmetric archive distance errors that nothing in the ETL wrote, and a placementDistancePc test that failed, was discarded (saved in the scratchpad as uncommitted-at-start.patch). The naked-eye check is new, and runs before the Hipparcos one, which the same slip also trips. Controls, each an ETL run from cache in a throwaway worktree that reached "Validating output..." and failed with the named check's own message: no magnitude to placementDistancePc (naked-eye floor); `10 ** logLuminosity` -> `logLuminosity`; colorFromTemperature never set; the archive epoch offset times 0; combine never taking the other entry's distance; archive ids in arrival order. The unmutated baseline passed. star.model.ts's count of flagged hosts, 57, is now 54. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -35,7 +35,8 @@ export interface StarRecord {
|
|||||||
colorSystem?: 'B-V' | 'BP-RP';
|
colorSystem?: 'B-V' | 'BP-RP';
|
||||||
/**
|
/**
|
||||||
* Whether `colorIndex` was read off the dwarf sequence at the star's effective temperature rather
|
* Whether `colorIndex` was read off the dwarf sequence at the star's effective temperature rather
|
||||||
* than measured: the 57 archive-placed hosts with a temperature and no B magnitude.
|
* than measured: 54 archive-placed hosts with a temperature and no B or V magnitude. Three more,
|
||||||
|
* whose temperatures are outside the table, have no colour at all.
|
||||||
*/
|
*/
|
||||||
colorFromTemperature?: boolean;
|
colorFromTemperature?: boolean;
|
||||||
/**
|
/**
|
||||||
|
|||||||
+112
-1
@@ -9,6 +9,8 @@ import { fetchExoplanets } from './fetchExoplanets';
|
|||||||
import { fetchSolarSystem } from './fetchSolarSystem';
|
import { fetchSolarSystem } from './fetchSolarSystem';
|
||||||
import { BYTES_PER_STAR_META, BYTES_PER_STAR_POSITION, decodeStarCatalog, encodeStarCatalog, isDesignation } from '../../src/app/shared/models/star-catalog';
|
import { BYTES_PER_STAR_META, BYTES_PER_STAR_POSITION, decodeStarCatalog, encodeStarCatalog, isDesignation } from '../../src/app/shared/models/star-catalog';
|
||||||
import { fetchStars } from './fetchStars';
|
import { fetchStars } from './fetchStars';
|
||||||
|
import { ARCHIVE_EPOCH, archiveStarId, CATALOGUE_EPOCH } from '../../src/app/shared/astro/host-star-matching';
|
||||||
|
import { propagateProperMotion, raDegDecDistanceToXyz } from '../../src/app/shared/astro/coordinates';
|
||||||
import { describeSources } from './sources/registry';
|
import { describeSources } from './sources/registry';
|
||||||
import { dataPath } from './lib/paths';
|
import { dataPath } from './lib/paths';
|
||||||
|
|
||||||
@@ -43,6 +45,42 @@ const MAX_RANGED_DISTANCE_SHARE = 0.01;
|
|||||||
* notices — the other 62 002 carry it from their Gaia row.
|
* notices — the other 62 002 carry it from their Gaia row.
|
||||||
*/
|
*/
|
||||||
const MIN_HYG_STARS_AT_GAIA_DISTANCE = 6_000;
|
const MIN_HYG_STARS_AT_GAIA_DISTANCE = 6_000;
|
||||||
|
/**
|
||||||
|
* The other half of placing a star at its more precise distance (8c3a860): a HYG star folded into
|
||||||
|
* its Gaia entry that keeps its Hipparcos distance, which Gaia saturates on. Measured 384, none
|
||||||
|
* before 8c3a860, and 148 with fetchStars handing combine Gaia's error with the Hipparcos distance,
|
||||||
|
* where the two errors tie and Gaia's distance wins — which the floor above cannot see, since that
|
||||||
|
* raises its count to 8 129. The two stars below are what that looks like: Tarazed went back to
|
||||||
|
* Gaia's 178.9 pc, and Eta Leo kept its 389 pc but read "±17 %, HYG, Gaia DR3 distance".
|
||||||
|
*/
|
||||||
|
const MIN_GAIA_STARS_AT_HIPPARCOS_DISTANCE = 300;
|
||||||
|
const HIPPARCOS_PLACED_STARS = [
|
||||||
|
{ id: 96970, name: 'Tarazed', distancePc: 121.07 },
|
||||||
|
{ id: 49441, name: 'Eta Leo', distancePc: 389.11 }
|
||||||
|
];
|
||||||
|
/** Their distances' errors, 2.1 % and 6.3 %, against Gaia's 6.9 % and 17 %. */
|
||||||
|
const MAX_HIPPARCOS_PLACED_ERROR = 0.065;
|
||||||
|
/**
|
||||||
|
* Archive hosts whose colour is read off their temperature, which the card marks "from its
|
||||||
|
* temperature" (23547de). Measured 54; the flag's write dropped from fetchExoplanets leaves none,
|
||||||
|
* and every other check passes, since the round trip compares the flag with itself.
|
||||||
|
*/
|
||||||
|
const MIN_COLOURS_FROM_TEMPERATURE = 40;
|
||||||
|
/**
|
||||||
|
* Archive stars numbered after their host's name (62f81f2), so a refresh keeps each one's id and a
|
||||||
|
* bookmark its star. Measured: 3 276 of the 3 277 take the id their name hashes to, one having
|
||||||
|
* probed past a taken one. Numbered in arrival order, as before, none do, and nothing else fails.
|
||||||
|
*/
|
||||||
|
const MAX_ARCHIVE_IDS_OFF_THEIR_NAME = 5;
|
||||||
|
/**
|
||||||
|
* Every star the naked eye sees, kept at any distance (c64eea0): measured 8 886 of V 6.5 or
|
||||||
|
* brighter, 1 653 of them past 250 pc. With no magnitude handed to placementDistancePc, 7 379 are
|
||||||
|
* left and Rigel, Deneb and Alnilam are gone — and every other check passed, the HYG survivors
|
||||||
|
* going down rather than up.
|
||||||
|
*/
|
||||||
|
const MIN_NAKED_EYE_STARS = 8_800;
|
||||||
|
const NAKED_EYE_MAGNITUDE_V = 6.5;
|
||||||
|
const REQUIRED_DISTANT_STARS = ['Rigel', 'Deneb', 'Alnilam'];
|
||||||
|
|
||||||
function validateStars(stars: StarRecord[]): void {
|
function validateStars(stars: StarRecord[]): void {
|
||||||
assertCondition(stars.length > 0, 'No stars were produced.');
|
assertCondition(stars.length > 0, 'No stars were produced.');
|
||||||
@@ -123,6 +161,45 @@ function validateStars(stars: StarRecord[]): void {
|
|||||||
` Gaia distances a median ${(medianGaiaError * 100).toFixed(2)} % uncertain; ${ranged} parallax distances ranged; ${hygAtGaiaDistance} HYG stars at Gaia's distance.`
|
` Gaia distances a median ${(medianGaiaError * 100).toFixed(2)} % uncertain; ${ranged} parallax distances ranged; ${hygAtGaiaDistance} HYG stars at Gaia's distance.`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const nakedEye = stars.filter((star) => star.magnitudeBand === 'V' && star.magnitude <= NAKED_EYE_MAGNITUDE_V && star.id !== SUN_STAR_ID).length;
|
||||||
|
assertCondition(
|
||||||
|
nakedEye >= MIN_NAKED_EYE_STARS,
|
||||||
|
`Only ${nakedEye} stars of V ${NAKED_EYE_MAGNITUDE_V} or brighter (at least ${MIN_NAKED_EYE_STARS} expected) — naked-eye stars are no longer kept at any distance.`
|
||||||
|
);
|
||||||
|
for (const name of REQUIRED_DISTANT_STARS) {
|
||||||
|
assertCondition(stars.some((star) => star.name === name), `${name} is missing — a naked-eye star past the survey is no longer kept.`);
|
||||||
|
}
|
||||||
|
console.log(` ${nakedEye} naked-eye stars.`);
|
||||||
|
|
||||||
|
const gaiaAtHipparcosDistance = stars.filter((star) => star.source === 'gaia' && star.magnitudeBand === 'V' && !star.distanceFromGaia).length;
|
||||||
|
assertCondition(
|
||||||
|
gaiaAtHipparcosDistance >= MIN_GAIA_STARS_AT_HIPPARCOS_DISTANCE,
|
||||||
|
`Only ${gaiaAtHipparcosDistance} HYG stars folded into Gaia keep their Hipparcos distance (at least ${MIN_GAIA_STARS_AT_HIPPARCOS_DISTANCE} expected) — the more precise distance no longer wins.`
|
||||||
|
);
|
||||||
|
for (const expected of HIPPARCOS_PLACED_STARS) {
|
||||||
|
const star = stars.find((candidate) => candidate.id === expected.id);
|
||||||
|
const distancePc = star && Math.hypot(star.x, star.y, star.z);
|
||||||
|
assertCondition(
|
||||||
|
star !== undefined && Math.abs(distancePc! / expected.distancePc - 1) < 0.01 && !star.distanceFromGaia && (star.distanceError ?? 1) < MAX_HIPPARCOS_PLACED_ERROR,
|
||||||
|
`${expected.name} is at ${distancePc?.toFixed(1)} pc, error ${star?.distanceError}, Gaia's: ${star?.distanceFromGaia} — expected its Hipparcos ${expected.distancePc} pc and error.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const coloursFromTemperature = stars.filter((star) => star.colorFromTemperature).length;
|
||||||
|
assertCondition(
|
||||||
|
coloursFromTemperature >= MIN_COLOURS_FROM_TEMPERATURE,
|
||||||
|
`Only ${coloursFromTemperature} colours are marked as read off a temperature (at least ${MIN_COLOURS_FROM_TEMPERATURE} expected) — fetchExoplanets no longer says so.`
|
||||||
|
);
|
||||||
|
const archiveStars = stars.filter((star) => star.source === 'exoplanet-archive');
|
||||||
|
const offTheirName = archiveStars.filter((star) => star.id !== archiveStarId(star.name, new Set())).length;
|
||||||
|
assertCondition(
|
||||||
|
offTheirName <= MAX_ARCHIVE_IDS_OFF_THEIR_NAME,
|
||||||
|
`${offTheirName} of the ${archiveStars.length} archive stars have an id other than their name's (at most ${MAX_ARCHIVE_IDS_OFF_THEIR_NAME} expected) — a refresh would renumber them.`
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
` ${gaiaAtHipparcosDistance} Gaia stars at their Hipparcos distance; ${coloursFromTemperature} colours from a temperature; ${offTheirName} archive ids off their name.`
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
// Hipparcos's mark on a classification it does not print in full reached the card as "Spectral
|
// Hipparcos's mark on a classification it does not print in full reached the card as "Spectral
|
||||||
// type A0m...", for Sirius and 2 126 other stars, which reads as text the app cut short.
|
// type A0m...", for Sirius and 2 126 other stars, which reads as text the app cut short.
|
||||||
const dotted = stars.filter((star) => star.spectralType.endsWith('...')).length;
|
const dotted = stars.filter((star) => star.spectralType.endsWith('...')).length;
|
||||||
@@ -247,6 +324,12 @@ const MIN_HOSTED_SHARE = 0.995;
|
|||||||
* Proxima 0.105 R☉ instead of 0.141 — and nothing else fails.
|
* Proxima 0.105 R☉ instead of 0.141 — and nothing else fails.
|
||||||
*/
|
*/
|
||||||
const MIN_HOST_SURFACE_SHARE = 0.9;
|
const MIN_HOST_SURFACE_SHARE = 0.9;
|
||||||
|
/**
|
||||||
|
* How far, in milliarcseconds, a star placed from the archive may sit from its planets' published
|
||||||
|
* position carried from the archive's epoch to the catalogue's. Measured 0.0001 at most, rounding;
|
||||||
|
* carried from J2016 instead, TOI-2406 moves 203 mas and 2 287 archive stars more than 1.
|
||||||
|
*/
|
||||||
|
const MAX_ARCHIVE_EPOCH_OFFSET_MAS = 1;
|
||||||
|
|
||||||
function validateExoplanets(exoplanets: ExoplanetRecord[], stars: StarRecord[]): void {
|
function validateExoplanets(exoplanets: ExoplanetRecord[], stars: StarRecord[]): void {
|
||||||
assertCondition(exoplanets.length > 0, 'No exoplanets were produced.');
|
assertCondition(exoplanets.length > 0, 'No exoplanets were produced.');
|
||||||
@@ -293,7 +376,35 @@ function validateExoplanets(exoplanets: ExoplanetRecord[], stars: StarRecord[]):
|
|||||||
Math.min(withRadius, withTemperature) >= exoplanets.length * MIN_HOST_SURFACE_SHARE,
|
Math.min(withRadius, withTemperature) >= exoplanets.length * MIN_HOST_SURFACE_SHARE,
|
||||||
`Only ${withRadius} of ${exoplanets.length} exoplanets carry their host's radius and ${withTemperature} its temperature (at least ${MIN_HOST_SURFACE_SHARE * 100} % expected) — st_rad or st_teff is being lost.`
|
`Only ${withRadius} of ${exoplanets.length} exoplanets carry their host's radius and ${withTemperature} its temperature (at least ${MIN_HOST_SURFACE_SHARE * 100} % expected) — st_rad or st_teff is being lost.`
|
||||||
);
|
);
|
||||||
console.log(` ${withRadius}/${exoplanets.length} carry their host's radius, ${withTemperature} its temperature.`);
|
// Since d94451e st_lum warms the planets of 4 441 hosts and is what their cards print; lost, each
|
||||||
|
// falls back to a derived luminosity, 757 of them more than 1.5 times off it. Measured 6 036.
|
||||||
|
const luminosities = exoplanets.map((exoplanet) => exoplanet.hostStarLuminositySolar).filter((luminosity) => luminosity !== undefined);
|
||||||
|
assertCondition(
|
||||||
|
luminosities.length >= exoplanets.length * MIN_HOST_SURFACE_SHARE,
|
||||||
|
`Only ${luminosities.length} of ${exoplanets.length} exoplanets carry their host's luminosity (at least ${MIN_HOST_SURFACE_SHARE * 100} % expected) — st_lum is being lost.`
|
||||||
|
);
|
||||||
|
// Published as a logarithm, most of them negative: stored unconverted, they would not be.
|
||||||
|
assertCondition(
|
||||||
|
luminosities.every((luminosity) => Number.isFinite(luminosity) && luminosity! > 0),
|
||||||
|
'A host luminosity is not a positive number — st_lum is no longer converted from its logarithm.'
|
||||||
|
);
|
||||||
|
console.log(` ${withRadius}/${exoplanets.length} carry their host's radius, ${withTemperature} its temperature, ${luminosities.length} its luminosity.`);
|
||||||
|
|
||||||
|
let worstOffsetMas = 0;
|
||||||
|
for (const star of stars.filter((candidate) => candidate.source === 'exoplanet-archive')) {
|
||||||
|
const planet = exoplanets.find((candidate) => candidate.hostStarId === star.id)!;
|
||||||
|
const at = propagateProperMotion(planet.hostRaDeg!, planet.hostDecDeg!, planet.hostPmRaMasPerYear ?? 0, planet.hostPmDecMasPerYear ?? 0, CATALOGUE_EPOCH - ARCHIVE_EPOCH);
|
||||||
|
const expected = raDegDecDistanceToXyz(at.raDeg, at.decDeg, 1);
|
||||||
|
const length = Math.hypot(star.x, star.y, star.z);
|
||||||
|
// The chord between the two directions, which unlike an arccosine resolves a milliarcsecond.
|
||||||
|
const chord = Math.hypot(star.x / length - expected.x, star.y / length - expected.y, star.z / length - expected.z);
|
||||||
|
worstOffsetMas = Math.max(worstOffsetMas, (chord * 180 * 3_600_000) / Math.PI);
|
||||||
|
}
|
||||||
|
assertCondition(
|
||||||
|
worstOffsetMas <= MAX_ARCHIVE_EPOCH_OFFSET_MAS,
|
||||||
|
`A star placed from the archive sits ${worstOffsetMas.toFixed(1)} mas from its published position carried to J2000 (at most ${MAX_ARCHIVE_EPOCH_OFFSET_MAS} expected) — it is carried from another epoch.`
|
||||||
|
);
|
||||||
|
console.log(` Stars placed from the archive at most ${worstOffsetMas.toExponential(1)} mas from their published position carried to J2000.`);
|
||||||
|
|
||||||
// How many can be propagated at their real rate rather than as if the host were the Sun.
|
// How many can be propagated at their real rate rather than as if the host were the Sun.
|
||||||
const withPeriod = exoplanets.filter((exoplanet) => exoplanet.periodDays !== undefined).length;
|
const withPeriod = exoplanets.filter((exoplanet) => exoplanet.periodDays !== undefined).length;
|
||||||
|
|||||||
Reference in New Issue
Block a user