Fold a Gliese star into its Gaia source in Gaia's photometry, wherever HYG already put it, and place two naked-eye stars by SIMBAD's name for them

The Gliese fold (030447b) kept HYG's magnitude, band and colour over the Gaia entry's, putting
CNS3's V at Gaia's distance. Of the 49 folded, 15 lost a measured BP-RP and 6 their temperature and
radius (Gl 700.1C, Gl 632.2B, GJ 9800B, Gl 734B, Gl 323B, GJ 3605), and GJ 4285, V 11.45 where
SIMBAD has G 13.05, was drawn five times too luminous. A folded star now keeps the photometry of the
entry already there; HYG's name, id and type still come with the fold. Published catalogue, before
-> after: GJ 4285 V 11.45, 3 850 K, 0.491 R☉, 4.76e-2 L☉ -> G 13.05, BP-RP 2.74, 3 291 K, 0.297 R☉,
9.29e-3 L☉; GJ 3207 0.574 -> 0.278 R☉; Gl 700.1C no temperature, 4.54 L☉ -> 5 155 K, 1.214 R☉,
0.937 L☉; Gl 632.2B -> 9 044 K, 0.018 R☉; Gl 734B -> 3 360 K, 0.381 R☉.

The fold also looked its target up by name, so it saw only Gaia entries still bare. Where a
Hipparcos row of HYG's had already taken the source, the Gliese-only row of the same star stayed
beside it and the check counted none: Gl 251 at 5.76 pc beside HD 265866 (the host of GJ 251 b and
c) at 5.58, Gl 422 beside HD 304043. Gaia entries now carry their DR3 designation through the merge
(ETL only; the assets do not store it) and the fold looks it up; into an entry a HYG star already
describes it folds only where the two V agree within 0.5, so a companion SIMBAD gives its primary's
source stays. 14 more rows fold, 63 in all, each within 0.47 of the star it joins: Gl 251, Gl 422,
Gl 162, Gl 794, Gl 225.2C, Gl 905.2B, GJ 3232, GJ 4046, GJ 9490C, GJ 9608, 69 Tau Oph (Gl 700.1A)
and the second rows of HD 65277, HD 120237 and HD 336196. Stars within 10 pc 367 -> 366, within
25 pc 5 479 -> 5 468; distances without an error 346 -> 332; HYG survivors 11 478 -> 11 465.

validateMerge's count of Gliese rows beside their source took SIMBAD's map from the function the
fold takes it from, so a slip in the map turned both off: with it empty, GJ 2097 and GJ 4285 were
back inside 10 pc and the ETL passed. It now also asks for GJ 2097 and GJ 4285 beyond 20 pc and for
no Gl 251 or Gl 422 by name, and glieseGaiaDesignations refuses fewer than 3 200 HYG rows matched to
SIMBAD (3 352 of HYG's 3 801 measured).

4d47896 left 22 naked-eye HYG rows out as having no distance anywhere; two had one. HD 45951 (K0 III,
V 6.2) was on the map only as "Gaia DR3 3369454521490604416", HYG's declination being 31.7' out,
and θ¹ Ori A (HD 37020, HIP 26220) sits 0.1" from Gaia DR3 3017364132050194688, 2.643 ± 0.072 mas,
which the magnitude test refused because HYG gives V 4.98 against G 6.63. One cached SIMBAD query
now names the Gaia DR3 source of each naked-eye HYG row with no distance (199 of 206 HD numbers),
and a row nothing else places takes that source from the bright list, at its position: HD 45951 at
112.0 pc, under its name, and θ¹ Ori A at 378.4 pc. HD 45951 joins the required names. The twenty
still left out are named in build.ts; none has a bright source with a parallax five times its error
within a minute of arc nor under its SIMBAD name. Naked-eye stars 8 898 -> 8 899; 455 532 -> 455 519
stars.

The fold and the identity lookup are tested in star-merge.spec.ts. Guarded mutants (1 of 833
failed unless noted):
- the Gliese row's photometry kept: caught by "...in Gaia's photometry" (and the Hipparcos-row fold test, 2 failed)
- combine dropping the designation: caught by "folds one into a Gaia entry a HYG star of the same brightness..."
- folding whatever the brightness; tolerance 2 magnitudes: caught by "leaves a star with a Hipparcos error alone..."
- folding into bare entries only: caught by the Hipparcos-row fold test (and the leave-alone test, 2 failed)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-30 19:55:25 +02:00
co-authored by Claude Opus 5.5
parent c8d1bbb879
commit e7c420b296
11 changed files with 211 additions and 66 deletions
+34 -10
View File
@@ -1,11 +1,11 @@
import { writeFileSync } from 'node:fs';
import { foldByIdentity, hipparcosDistancePc, mergeStarCatalogues, NAKED_EYE_MAGNITUDE, placementDistancePc } from '../../src/app/shared/astro/star-merge';
import { foldByIdentity, hipparcosDistancePc, HYG_UNKNOWN_DISTANCE_PC, mergeStarCatalogues, NAKED_EYE_MAGNITUDE, placementDistancePc } from '../../src/app/shared/astro/star-merge';
import { encodeStarCatalog } from '../../src/app/shared/models/star-catalog';
import { StarRecord, SUN_STAR_ID } from '../../src/app/shared/models/star.model';
import { BrightGaiaSource, fetchBrightGaiaSources, fetchGaiaDistancesByHip, fetchHipparcosParallaxErrors, GaiaAnswerError } from './sources/gaia';
import { positionalSources } from './sources/registry';
import { fetchGaiaDesignationsByGj } from './sources/simbad';
import { fetchGaiaDesignationsByGj, fetchGaiaDesignationsByHd } from './sources/simbad';
import { PARALLAX_PRECISION_MAS } from './sources/star-sources';
import { parseCsvObjects, parseOptionalNumber } from './lib/csv';
import { fetchTextCached } from './lib/http';
@@ -86,10 +86,15 @@ export async function fetchStars(): Promise<StarRecord[]> {
const gaiaByHip = await fetchGaiaDistancesByHip();
const hipparcosErrors = await fetchHipparcosParallaxErrors();
const brightGaia = await fetchBrightGaiaSources();
const brightByDesignation = new Map(brightGaia.map((source) => [source.designation, source]));
const nakedEyeDesignations = await fetchGaiaDesignationsByHd(
rows.filter((row) => row['hd'] && Number(row['dist']) >= HYG_UNKNOWN_DISTANCE_PC && (parseOptionalNumber(row['mag']) ?? Infinity) <= NAKED_EYE_MAGNITUDE).map((row) => row['hd'])
);
const stars: StarRecord[] = [];
let atGaiaDistance = 0;
let pastCutoff = 0;
let identified = 0;
for (const row of rows) {
const id = Number(row['id']);
@@ -104,9 +109,13 @@ export async function fetchStars(): Promise<StarRecord[]> {
const hipparcosPc = hipparcosDistancePc(hygPc, hipparcos);
const magnitudeV = parseOptionalNumber(row['mag']);
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 unplaced = hipparcosPc === undefined && magnitude <= NAKED_EYE_MAGNITUDE;
const crossMatched = row['hip'] ? gaiaByHip.get(Number(row['hip'])) : undefined;
const positional = crossMatched === undefined && unplaced ? brightCounterpart(row, magnitude, brightGaia) : undefined;
// Where HYG's position leads to no bright source, the one SIMBAD names it as: HD 45951, whose
// declination HYG has 31.7′ out, and θ¹ Ori A (HD 37020), whose V it has 1.65 brighter than G.
const byIdentity = crossMatched === undefined && positional === undefined && unplaced ? brightByDesignation.get(nakedEyeDesignations.get(`HD ${row['hd']}`) ?? '') : undefined;
const gaia = crossMatched ?? positional ?? byIdentity;
const gaiaPc = gaia?.distancePc;
const hipparcosError = hipparcos?.relativeError;
const distancePc = placementDistancePc(hipparcosPc, gaiaPc, magnitude, DISTANCE_CUTOFF_PC, hipparcosError, gaia?.relativeError);
@@ -124,10 +133,9 @@ export async function fetchStars(): Promise<StarRecord[]> {
// HIP 57146, has x/y/z 161″ from its own ra/dec and stays double).
//
// Only their direction is used. They sit at HYG's own distance, or at its 100 000 pc
// placeholder where it has none, and are carried along that direction to the one chosen above.
const x = Number(row['x']);
const y = Number(row['y']);
const z = Number(row['z']);
// placeholder where it has none, and are carried along that direction to the one chosen above —
// Gaia's, for a star found by its identity, where HYG's may be the thing that is wrong.
const [x, y, z] = byIdentity ? [byIdentity.direction.x, byIdentity.direction.y, byIdentity.direction.z] : [Number(row['x']), Number(row['y']), Number(row['z'])];
const length = Math.hypot(x, y, z);
if (![x, y, z].every(Number.isFinite) || length === 0) {
continue;
@@ -136,6 +144,9 @@ export async function fetchStars(): Promise<StarRecord[]> {
if (fromGaia) {
atGaiaDistance++;
}
if (byIdentity) {
identified++;
}
if (distancePc > DISTANCE_CUTOFF_PC) {
pastCutoff++;
}
@@ -165,7 +176,9 @@ export async function fetchStars(): Promise<StarRecord[]> {
});
}
console.log(` kept ${stars.length} stars (of ${rows.length} in the catalog): ${atGaiaDistance} at Gaia's distance, ${pastCutoff} of them past ${DISTANCE_CUTOFF_PC} pc.`);
console.log(
` kept ${stars.length} stars (of ${rows.length} in the catalog): ${atGaiaDistance} at Gaia's distance, ${pastCutoff} of them past ${DISTANCE_CUTOFF_PC} pc; ${identified} naked-eye stars placed by the Gaia source SIMBAD names them as.`
);
const { stars: merged, folded } = foldByIdentity(await mergeWithOtherSources(stars), await glieseGaiaDesignations(rows));
console.log(` ${folded} Gliese entries folded into the Gaia source SIMBAD names them as.`);
@@ -203,6 +216,14 @@ function simbadGliese(gl: string): string {
return gl.trim().replace(/^Gl\s+/, 'GJ ').replace(/^(GJ \d+(?:\.\d+)?)\s*([A-Z]+)$/, '$1 $2');
}
/**
* How many HYG rows with a Gliese number SIMBAD names a Gaia source for: 3 352 of HYG's 3 801 on
* 2026-09-30. SIMBAD's side has its own floor; this one is on the join, where HYG's "Gl 94" has to
* become SIMBAD's "GJ 94". Joined on HYG's names as they stand, 38 rows were folded instead of 49
* and the check in build.ts, which takes this map too, still counted none left.
*/
const MIN_GLIESE_IDENTITIES = 3_200;
/**
* The Gaia DR3 designation SIMBAD gives each HYG star with a Gliese number, by HYG id: what the
* merge folds its Gliese-only rows by, and what build.ts checks it did. `rows` are HYG's, read
@@ -218,6 +239,9 @@ export async function glieseGaiaDesignations(rows?: Record<string, string>[]): P
designations.set(Number(row['id']), designation);
}
}
if (designations.size < MIN_GLIESE_IDENTITIES) {
throw new Error(`Only ${designations.size} HYG stars with a Gliese number were matched to SIMBAD's (at least ${MIN_GLIESE_IDENTITIES} expected) — HYG's designations are no longer written as SIMBAD writes them.`);
}
return designations;
}