Fold a Gliese star into the Gaia source SIMBAD names it as, so none is drawn twice or inside 10 pc by mistake

HYG's Gliese-only rows, with no Hipparcos astrometry and no published error on their distance,
reach the merge with positions off by up to minutes of arc, photometric distances and sometimes
wrong proper motions, and isSameStar's geometry missed 49 of them beside their own Gaia entry. 43
lie within 25 pc and 27 are fainter than V 12, the layer the brief asked to merge without
duplicates. GJ 3478 is 16.3" from its Gaia entry, past the 15" tolerance; GJ 2097 moves 39 %
differently by HYG's motion; GJ 4285 is co-moving but 1.6 magnitudes brighter in HYG's V than
Gaia's G. Two of them were false stars inside 10 pc: GJ 2097 at 6.41 pc and GJ 4285 at 6.80, which
Gaia measures at 24.47 and 28.25. HYG also put Gl 94 31.6 degrees from where it is, and HD 23585
and HD 23713, Pleiades members at 135 pc, at 20.6 and 22.2. 0e9ab6f's "no Gliese row within 25 pc
has a co-moving bare Gaia entry 3-300" away" held only under its own motion rule.

fetchStars now asks SIMBAD once, in one cached TAP query, for the Gaia DR3 designation of every
object it knows by a GJ number (4 868), maps HYG's `gl` column onto it, and foldByIdentity folds
each Gliese-only row into the bare Gaia entry of that source: HYG's name, type and photometry,
Gaia's position and distance, as combine does for any other pair. An ETL run from cache folds 49:
455 571 stars become 455 522, the stars within 10 pc 369 become 367, within 25 pc 5 522 become
5 479, HYG rows without a Gaia counterpart 11 517 become 11 468, distances with no published error
395 become 346. exoplanets.json is unchanged. In the running app GJ 2097 reads "24 pc, HYG, Gaia
DR3 distance", GJ 4285 28 pc, Gl 94 17 pc, and 367 stars lie within 10 pc.

validateMerge now refuses any Gliese-only row beside the bare Gaia entry SIMBAD names as the same
star. Controls: folding with an empty identity map fails the ETL with "49 Gliese stars are drawn
beside the Gaia source SIMBAD names them as, starting with GJ 1033" (the baseline passed); in the
unit suite, folding a row with a Hipparcos error fails "leaves a star with a Hipparcos error, and
a Gaia entry already folded into, alone", and keeping the Gliese position fails "folds a Gliese
entry into the Gaia entry SIMBAD names it as, at Gaia's position and distance". HYG's V is kept
for a folded star, which for GJ 3207 is the wrong one (11.51 where SIMBAD has 13.75).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-30 17:20:32 +02:00
co-authored by Claude Opus 5.5
parent 8d59c720b3
commit 030447b83c
8 changed files with 154 additions and 8 deletions
+25 -1
View File
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest';
import { raDegDecDistanceToXyz } from './coordinates';
import { StarRecord } from '../models/star.model';
import { directionCosine, hipparcosDistancePc, HYG_UNKNOWN_DISTANCE_PC, isSameStar, MERGE_ANGULAR_TOLERANCE_DEG, mergeStarCatalogues, NAKED_EYE_MAGNITUDE, placementDistancePc } from './star-merge';
import { directionCosine, foldByIdentity, hipparcosDistancePc, HYG_UNKNOWN_DISTANCE_PC, isSameStar, MERGE_ANGULAR_TOLERANCE_DEG, mergeStarCatalogues, NAKED_EYE_MAGNITUDE, placementDistancePc } from './star-merge';
/** A star at a given sky position and distance, which is how catalogues actually report them. */
function at(id: number, raDeg: number, decDeg: number, distancePc: number, overrides: Partial<StarRecord> = {}): StarRecord {
@@ -309,6 +309,30 @@ describe('mergeStarCatalogues', () => {
});
});
describe('foldByIdentity', () => {
// GJ 4285 as HYG has it, at its Gliese photometric distance and a magnitude and a half brighter
// in V than Gaia's G, and the Gaia source SIMBAD names as the same star, L 119-44, 50.6″ away.
const gliese = at(119513, 339.5, -65.84, 6.8, { name: 'GJ 4285', source: 'hyg', magnitude: 11.45, magnitudeBand: 'V', spectralType: 'M4' });
const gaia = at(1050005263, 339.5 + arcsecOfRa(50.6, -65.84), -65.84, 28.25, { name: 'Gaia DR3 6392188629658709888', source: 'gaia', magnitude: 13.05, magnitudeBand: 'G', distanceError: 0.0004, distanceFromGaia: true });
const identities = new Map([[gliese.id, gaia.name]]);
it("folds a Gliese entry into the Gaia entry SIMBAD names it as, at Gaia's position and distance", () => {
expect(isSameStar(gaia, gliese)).toBe(false);
const { stars, folded } = foldByIdentity([gliese, gaia], identities);
expect(folded).toBe(1);
expect(stars).toHaveLength(1);
expect(stars[0]).toMatchObject({ id: gliese.id, name: 'GJ 4285', spectralType: 'M4', source: 'gaia', distanceError: 0.0004, distanceFromGaia: true });
expect(Math.hypot(stars[0].x, stars[0].y, stars[0].z)).toBeCloseTo(28.25, 9);
expect(directionCosine(stars[0], gaia)).toBeCloseTo(1, 12);
});
it('leaves a star with a Hipparcos error, and a Gaia entry already folded into, alone', () => {
expect(foldByIdentity([{ ...gliese, distanceError: 0.05 }, gaia], identities).folded).toBe(0);
expect(foldByIdentity([gliese, { ...gaia, name: 'L 119-44' }], identities).folded).toBe(0);
expect(foldByIdentity([gliese, gaia], new Map()).folded).toBe(0);
});
});
describe('hipparcosDistancePc', () => {
it("takes HYG's distance where it gives one", () => {
expect(hipparcosDistancePc(606.06, { parallaxMas: 1.65, relativeError: 0.45 / 1.65 })).toBe(606.06);
+33
View File
@@ -278,6 +278,39 @@ function combine(kept: StarRecord, other: StarRecord): StarRecord {
};
}
/**
* Folds each Gliese-only entry — HYG's, with no Hipparcos astrometry and so no published error on
* its distance — into the Gaia entry of the source SIMBAD names it as, `gaiaDesignationById` by
* HYG id, where that entry is still bare. {@link isSameStar} cannot see these: 42 of them within
* 25 pc stayed beside their own Gaia entry, too far for their positions (GJ 3478, 16″), moving
* differently by HYG's motions (GJ 2097, 39 %), or brighter in HYG's V than Gaia's G by more than a
* primary may be (GJ 4285, 1.6 magnitudes). Two of those were stars that do not exist inside 10 pc:
* GJ 2097 at 6.41 pc and GJ 4285 at 6.80, which Gaia measures at 24.47 and 28.25. The fold keeps
* HYG's description and Gaia's position and distance, as {@link combine} does for any other pair.
*/
export function foldByIdentity(stars: readonly StarRecord[], gaiaDesignationById: ReadonlyMap<number, string>): { stars: StarRecord[]; folded: number } {
// A Gaia entry already folded into carries the other catalogue's name, not its designation.
const bare = new Map<string, number>();
stars.forEach((star, index) => {
if (star.source === 'gaia') {
bare.set(star.name, index);
}
});
const result = [...stars];
const folded = new Set<number>();
stars.forEach((star, index) => {
const designation = star.source === 'hyg' && star.distanceError === undefined ? gaiaDesignationById.get(star.id) : undefined;
const target = designation === undefined ? undefined : bare.get(designation);
if (target === undefined) {
return;
}
result[target] = combine(result[target], star);
bare.delete(designation!);
folded.add(index);
});
return { stars: result.filter((_, index) => !folded.has(index)), folded: folded.size };
}
/**
* Unions the given catalogues, keeping one entry per star.
*
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.