Find the naked-eye stars Gaia's HIP cross-match lacks by position, and give two unnamed ones back their names

c64eea0 and ff744a0 left 34 of HYG's stars of V 6.5 or brighter off the map for want of a distance,
saying no survey gave them one. Gaia DR3 does, for twelve, above the ETL's own cut of five times
the parallax error: fetchStars looks Gaia up only by HIP number through hipparcos2_best_neighbour,
which lacks some of theirs (66 Ori's among them), and never looks up a HYG row with no HIP number.
HD 197770 is 1.102 +- 0.029 mas; HD 45291 and HD 124953 were on the map only as bare Gaia entries
a second of arc from where HYG has them, so searching their names found nothing.

fetchStars now fetches, once and cached, Gaia DR3's 35 910 sources brighter than G 7.5 with a
usable parallax, and for a naked-eye row nothing else places takes the nearest within 5" and a
magnitude of its V. At 15" theta-1 Ori (HIP 26220) took the source of theta-1 Ori C, 12.9" away and
already on the map; the twelve found are within 4.1". An ETL run from cache: 455 522 stars become
455 532, the naked-eye stars 8 886 become 8 898 (1 663 past 250 pc), with 66 Ori, HD 49567, HD 54309,
HD 74455, HD 114461, 10 Sge, HD 197770, HD 33948, HD 152249 and HD 162678 added, and HD 45291 and
HD 124953 folded into their Gaia entries under their names. 22 stay out, with no distance anywhere.
In the running app HD 197770 reads "907 +- 25 pc", 66 Ori "469 +- 61 pc", HD 45291 "108 pc" and
HD 124953 "46 pc". Four of the twelve have a RUWE above 3.7 in Gaia; the ETL has never filtered on it.

validateStars now also requires HD 197770 and HD 45291. Control: without the positional lookup,
the ETL fails with "HD 197770 is missing" (the naked-eye floor alone would not see twelve stars go),
and the baseline passes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-30 17:36:44 +02:00
co-authored by Claude Opus 5.5
parent 030447b83c
commit 4d47896b4a
6 changed files with 85 additions and 11 deletions
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
+8 -6
View File
@@ -73,14 +73,16 @@ const MIN_COLOURS_FROM_TEMPERATURE = 40;
*/ */
const MAX_ARCHIVE_IDS_OFF_THEIR_NAME = 5; 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 * Every star the naked eye sees, kept at any distance (c64eea0): measured 8 898 of V 6.5 or
* brighter, 1 653 of them past 250 pc. With no magnitude handed to placementDistancePc, 7 379 are * brighter, 1 663 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 * left and Rigel, Deneb and Alnilam are gone — and every other check passed, the HYG survivors
* going down rather than up. * going down rather than up. HD 197770 and HD 45291 are two of the twelve only Gaia's bright
* sources place, by position; without that lookup they are gone and the count drops by twelve,
* which the floor alone would not see.
*/ */
const MIN_NAKED_EYE_STARS = 8_800; const MIN_NAKED_EYE_STARS = 8_800;
const NAKED_EYE_MAGNITUDE_V = 6.5; const NAKED_EYE_MAGNITUDE_V = 6.5;
const REQUIRED_DISTANT_STARS = ['Rigel', 'Deneb', 'Alnilam']; const REQUIRED_NAKED_EYE_STARS = ['Rigel', 'Deneb', 'Alnilam', 'HD 197770', 'HD 45291'];
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.');
@@ -166,8 +168,8 @@ function validateStars(stars: StarRecord[]): void {
nakedEye >= MIN_NAKED_EYE_STARS, 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.` `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) { for (const name of REQUIRED_NAKED_EYE_STARS) {
assertCondition(stars.some((star) => star.name === name), `${name} is missing — a naked-eye star past the survey is no longer kept.`); assertCondition(stars.some((star) => star.name === name), `${name} is missing — naked-eye stars are no longer kept wherever a survey places them.`);
} }
console.log(` ${nakedEye} naked-eye stars.`); console.log(` ${nakedEye} naked-eye stars.`);
+32 -4
View File
@@ -1,9 +1,9 @@
import { writeFileSync } from 'node:fs'; import { writeFileSync } from 'node:fs';
import { foldByIdentity, hipparcosDistancePc, mergeStarCatalogues, placementDistancePc } from '../../src/app/shared/astro/star-merge'; import { foldByIdentity, hipparcosDistancePc, mergeStarCatalogues, NAKED_EYE_MAGNITUDE, placementDistancePc } from '../../src/app/shared/astro/star-merge';
import { encodeStarCatalog } from '../../src/app/shared/models/star-catalog'; import { encodeStarCatalog } from '../../src/app/shared/models/star-catalog';
import { StarRecord, SUN_STAR_ID } from '../../src/app/shared/models/star.model'; import { StarRecord, SUN_STAR_ID } from '../../src/app/shared/models/star.model';
import { fetchGaiaDistancesByHip, fetchHipparcosParallaxErrors, GaiaAnswerError } from './sources/gaia'; import { BrightGaiaSource, fetchBrightGaiaSources, fetchGaiaDistancesByHip, fetchHipparcosParallaxErrors, GaiaAnswerError } from './sources/gaia';
import { positionalSources } from './sources/registry'; import { positionalSources } from './sources/registry';
import { fetchGaiaDesignationsByGj } from './sources/simbad'; import { fetchGaiaDesignationsByGj } from './sources/simbad';
import { PARALLAX_PRECISION_MAS } from './sources/star-sources'; import { PARALLAX_PRECISION_MAS } from './sources/star-sources';
@@ -85,6 +85,7 @@ export async function fetchStars(): Promise<StarRecord[]> {
// Not skipped when unreachable, unlike the positional sources below; see its own comment. // Not skipped when unreachable, unlike the positional sources below; see its own comment.
const gaiaByHip = await fetchGaiaDistancesByHip(); const gaiaByHip = await fetchGaiaDistancesByHip();
const hipparcosErrors = await fetchHipparcosParallaxErrors(); const hipparcosErrors = await fetchHipparcosParallaxErrors();
const brightGaia = await fetchBrightGaiaSources();
const stars: StarRecord[] = []; const stars: StarRecord[] = [];
let atGaiaDistance = 0; let atGaiaDistance = 0;
@@ -101,10 +102,12 @@ export async function fetchStars(): Promise<StarRecord[]> {
const hygPc = Number(row['dist']); const hygPc = Number(row['dist']);
const hipparcos = row['hip'] ? hipparcosErrors.get(Number(row['hip'])) : undefined; const hipparcos = row['hip'] ? hipparcosErrors.get(Number(row['hip'])) : undefined;
const hipparcosPc = hipparcosDistancePc(hygPc, hipparcos); const hipparcosPc = hipparcosDistancePc(hygPc, hipparcos);
const gaia = row['hip'] ? gaiaByHip.get(Number(row['hip'])) : undefined;
const gaiaPc = gaia?.distancePc;
const magnitudeV = parseOptionalNumber(row['mag']); const magnitudeV = parseOptionalNumber(row['mag']);
const magnitude = magnitudeV ?? UNKNOWN_MAGNITUDE; const magnitude = magnitudeV ?? UNKNOWN_MAGNITUDE;
const gaia =
(row['hip'] ? gaiaByHip.get(Number(row['hip'])) : undefined) ??
(hipparcosPc === undefined && magnitude <= NAKED_EYE_MAGNITUDE ? brightCounterpart(row, magnitude, brightGaia) : undefined);
const gaiaPc = gaia?.distancePc;
const hipparcosError = hipparcos?.relativeError; const hipparcosError = hipparcos?.relativeError;
const distancePc = placementDistancePc(hipparcosPc, gaiaPc, magnitude, DISTANCE_CUTOFF_PC, hipparcosError, gaia?.relativeError); const distancePc = placementDistancePc(hipparcosPc, gaiaPc, magnitude, DISTANCE_CUTOFF_PC, hipparcosError, gaia?.relativeError);
if (distancePc === null) { if (distancePc === null) {
@@ -170,6 +173,31 @@ export async function fetchStars(): Promise<StarRecord[]> {
return merged; return merged;
} }
/** How far a naked-eye star may be from its Gaia source, and how much brighter or fainter in G. */
const BRIGHT_COUNTERPART_TOLERANCE_RAD = (5 / 3600) * (Math.PI / 180);
const BRIGHT_COUNTERPART_MAGNITUDES = 1;
/**
* The Gaia source that is a naked-eye HYG star neither survey places otherwise: the nearest within
* 5″ of its direction and a magnitude of its V. Found this way, HD 152249 is 4.1″ from where HYG
* has it, the rest within 1.1″. At 15″, θ¹ Ori (HIP 26220) took the source of θ¹ Ori C, 12.9″ away
* and already on the map.
*/
function brightCounterpart(row: Record<string, string>, magnitudeV: number, sources: readonly BrightGaiaSource[]): { distancePc: number; relativeError: number } | undefined {
const [x, y, z] = [Number(row['x']), Number(row['y']), Number(row['z'])];
const length = Math.hypot(x, y, z);
let best: BrightGaiaSource | undefined;
let bestCosine = Math.cos(BRIGHT_COUNTERPART_TOLERANCE_RAD);
for (const source of sources) {
const cosine = (x * source.direction.x + y * source.direction.y + z * source.direction.z) / length;
if (cosine >= bestCosine && Math.abs(source.magnitudeG - magnitudeV) <= BRIGHT_COUNTERPART_MAGNITUDES) {
best = source;
bestCosine = cosine;
}
}
return best;
}
/** HYG's Gliese designation as SIMBAD writes it: "Gl 734B" is "GJ 734 B". */ /** HYG's Gliese designation as SIMBAD writes it: "Gl 734B" is "GJ 734 B". */
function simbadGliese(gl: string): string { function simbadGliese(gl: string): string {
return gl.trim().replace(/^Gl\s+/, 'GJ ').replace(/^(GJ \d+(?:\.\d+)?)\s*([A-Z]+)$/, '$1 $2'); return gl.trim().replace(/^Gl\s+/, 'GJ ').replace(/^(GJ \d+(?:\.\d+)?)\s*([A-Z]+)$/, '$1 $2');
+44
View File
@@ -291,6 +291,50 @@ export async function fetchGaiaDistancesByHip(): Promise<Map<number, { distanceP
return distances; return distances;
} }
/**
* How bright a Gaia source must be to stand in for a naked-eye HYG star, in G, and how many the
* archive holds that bright with a usable parallax: 35 910 on 2026-09-30.
*/
const BRIGHT_MAGNITUDE_LIMIT = 7.5;
const BRIGHT_QUERY_ROWS = 35_910;
/** A bright Gaia source's J2000 direction, G magnitude and distance. */
export interface BrightGaiaSource {
direction: { x: number; y: number; z: number };
magnitudeG: number;
distancePc: number;
relativeError: number;
}
/**
* Every Gaia DR3 source brighter than G 7.5 with a usable parallax, at J2000: for the naked-eye
* HYG stars nothing else places, found by position. The Hipparcos cross-match above lacks some of
* their HIP numbers, and the HYG rows with no HIP number were never looked up at all. Of the 34
* HYG stars of V 6.5 or brighter left off the map for want of a distance, 12 are found here: ten
* that were missing, 66 Ori and HD 197770 (1.102 ± 0.029 mas) among them, and HD 45291 and
* HD 124953, which were on the map only as bare Gaia entries a second of arc from where HYG has them.
*/
export async function fetchBrightGaiaSources(): Promise<BrightGaiaSource[]> {
const query = [
`select top ${ROW_LIMIT} source_id, ra, dec, pmra, pmdec, parallax, parallax_over_error, phot_g_mean_mag`,
'from gaiadr3.gaia_source',
`where phot_g_mean_mag < ${BRIGHT_MAGNITUDE_LIMIT} and parallax_over_error > ${(1 / MAX_PARALLAX_ERROR_RATIO).toFixed(1)}`,
'order by source_id'
].join(' ');
console.log(`Fetching Gaia DR3's sources brighter than G ${BRIGHT_MAGNITUDE_LIMIT}, for the naked-eye stars the cross-match lacks...`);
const rows = await fetchQueryRows(query, BRIGHT_QUERY_ROWS);
const sources: BrightGaiaSource[] = [];
for (const row of rows) {
const [raDeg, decDeg, parallaxMas, overError, magnitudeG] = ['ra', 'dec', 'parallax', 'parallax_over_error', 'phot_g_mean_mag'].map((column) => parseOptionalNumber(row[column]));
if (raDeg === undefined || decDeg === undefined || !parallaxMas || parallaxMas <= 0 || overError === undefined || magnitudeG === undefined) {
continue;
}
const j2000 = propagateProperMotion(raDeg, decDeg, parseOptionalNumber(row['pmra']) ?? 0, parseOptionalNumber(row['pmdec']) ?? 0, CATALOGUE_EPOCH - GAIA_DR3_EPOCH);
sources.push({ direction: raDegDecDistanceToXyz(j2000.raDeg, j2000.decDeg, 1), magnitudeG, distancePc: 1000 / parallaxMas, relativeError: 1 / overError });
}
return sources;
}
/** The new Hipparcos reduction holds 117 955 stars, and every one has a parallax error. */ /** The new Hipparcos reduction holds 117 955 stars, and every one has a parallax error. */
const MIN_HIPPARCOS_ERRORS = 110_000; const MIN_HIPPARCOS_ERRORS = 110_000;