Answer the review: direction settles distance, brightness is one-sided, and a lost id stops the scene

Three findings from the adversarial review of the merge, all reproduced.

The distance test was hiding 1 489 stars that sit under an arcsecond from
their Gaia entry with a Hipparcos parallax off by half — thirty of them at a
false few parsecs from the Sun (HIP 82724 at 3.7 pc, where Gaia has it at
62.8) — and the first audit did not see them because it counted residual
doubles through the same 50 % filter. Under three arcseconds the distances
are now not consulted: a coincidence of direction that close is never chance
at this depth (the quarter-degree shift finds none), and the parallax is the
thing to fix. Brightness keeps its say at any separation, and is now
one-sided: a folded entry may be five magnitudes fainter (a red dwarf in V
against G) but not one brighter, because an entry a magnitude brighter than
what is already at that spot is a primary Gaia does not carry — Almach,
Alfirk and Ashlesha had all been folded into their companions' entries,
93 in all. The sky grid wraps at 0h.

The Gaia query orders by source_id after G, so the row order — and the ids
assigned from it — is a function of the archive's content rather than of the
server's plan for 20 064 ties; the cache key is a hash of the query.

And a bookmark to a star id the catalogue no longer holds — 56 000 Gaia ids
change with this — sent the scene through reconcileSelection, enterSystem,
its decline, finishTransition and reconcileSelection again until the stack
overflowed. The selection is cleared instead, at the one place every path
goes through.

Regenerated: 423 641 stars, 57 512 HYG identities on Gaia positions, no HYG
id or name lost, no star within 20 pc left with an unclaimed Gaia entry under
an arcsecond. 403 HYG survivors still have an unclaimed Gaia entry within
60": 13 under an arcsecond, where the brightness guard does not trust HYG's
magnitude, and the rest components 3" to 60" from their counterpart.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QL6F9Bgfh8SgAiAAcPB9Hw
This commit is contained in:
2026-08-28 21:20:45 +02:00
co-authored by Claude Fable 5
parent 08534279fb
commit dc2ce08694
10 changed files with 116 additions and 31 deletions
+40 -7
View File
@@ -38,19 +38,45 @@ describe('isSameStar', () => {
it('keeps a bright primary out of the entry of its faint companion', () => {
// Gaia has no Sirius — it saturates — but has Sirius B, 6″ away at the same distance and ten
// magnitudes fainter. Direction and distance say "same star"; the brightness says otherwise.
const sirius = at(32263, 101.2875, -16.7161, 2.637, { name: 'Sirius', magnitude: -1.44 });
const siriusB = at(1, 101.2875 + arcsecOfRa(6.1, -16.7161), -16.7161, 2.67, { name: 'Gaia DR3 2947050466531873024', magnitude: 8.5, source: 'gaia' });
expect(isSameStar(sirius, siriusB)).toBe(false);
expect(isSameStar(sirius, { ...siriusB, magnitude: -1.3 })).toBe(true);
const siriusB = at(1, 101.2875, -16.7161, 2.67, { name: 'Gaia DR3 2947050466531873024', magnitude: 8.5, source: 'gaia' });
const sirius = at(32263, 101.2875 + arcsecOfRa(6.1, -16.7161), -16.7161, 2.637, { name: 'Sirius', magnitude: -1.44 });
expect(isSameStar(siriusB, sirius)).toBe(false);
expect(isSameStar(siriusB, { ...sirius, magnitude: 8.6 })).toBe(true);
});
it('lets the folded entry be fainter, as a red star is in V, but not much brighter', () => {
// Wolf 359 is V 13.45 in HYG and G 11.0 in Gaia — the same star, 5″ apart on a Gliese
// position. Almach is V 2.1 and sits 10″ from γ² And, G 4.9: Gaia has no Almach, and its
// name must not land on the companion.
const wolf359 = at(1, 164.1, 7.0, 2.41, { name: 'Gaia DR3 3864972938605115520', magnitude: 11.0, source: 'gaia' });
expect(isSameStar(wolf359, at(118720, 164.1 + arcsecOfRa(5, 7), 7.0, 2.39, { name: 'Wolf 359', magnitude: 13.45 }))).toBe(true);
const gamma2And = at(2, 30.97, 42.33, 50, { name: 'Gaia DR3 346231302441905920', magnitude: 4.9, source: 'gaia' });
expect(isSameStar(gamma2And, at(9640, 30.97 + arcsecOfRa(9.9, 42.33), 42.33, 50, { name: 'Almach', magnitude: 2.1 }))).toBe(false);
});
it('does not match two different stars that happen to be at the same distance', () => {
expect(isSameStar(at(1, 200, 10, 200), at(2, 200.5, 10, 200))).toBe(false);
});
it('does not match along a line of sight when the distances genuinely conflict', () => {
// Same direction, one three times further away: a background star, not the same object.
expect(isSameStar(at(1, 200, 10, 100), at(2, 200, 10, 300))).toBe(false);
it('takes two entries within three arcseconds for one star, whatever their distances say', () => {
// HD 225021: 143.7 pc by its Hipparcos parallax, 239.4 by Gaia's, 0.01″ apart; HIP 82724:
// 3.7 pc by Hipparcos, 62.8 by Gaia, 2.3″ apart. A coincidence of direction that close is
// never chance at this depth; the parallax is what is wrong.
const gaia = at(1, 1.72, -8.9, 239.4, { name: 'Gaia DR3 395581679270412160', source: 'gaia' });
expect(isSameStar(gaia, at(213, 1.72 + arcsecOfRa(0.1, -8.9), -8.9, 143.7, { name: 'HD 225021' }))).toBe(true);
expect(isSameStar(at(2, 253.6, -38.1, 62.8, { source: 'gaia' }), at(82724, 253.6 + arcsecOfRa(2.3, -38.1), -38.1, 3.7))).toBe(true);
});
it('past those three arcseconds, does not match along a line of sight when the distances conflict', () => {
// Nearly the same direction, one three times further away: a background star, not the same object.
expect(isSameStar(at(1, 200, 10, 100), at(2, 200 + arcsecOfRa(5, 10), 10, 300))).toBe(false);
});
it('still hears the brightness inside those three arcseconds', () => {
// Ashlesha (ε Hya, V 3.38) has a companion 2.7″ away that Gaia does carry, three magnitudes
// fainter, while it does not carry Ashlesha. Direction alone would put the name on the companion.
const companion = at(1, 131.69, 6.42, 40, { name: 'Gaia DR3 1', magnitude: 6.7, source: 'gaia' });
expect(isSameStar(companion, at(43109, 131.69 + arcsecOfRa(2.7, 6.42), 6.42, 40, { name: 'Ashlesha', magnitude: 3.38 }))).toBe(false);
});
it('matches on direction rather than on 3D proximity', () => {
@@ -181,6 +207,13 @@ describe('mergeStarCatalogues', () => {
]);
expect(stars).toHaveLength(1);
}
// And the one edge the grid has to wrap: 3.6″ apart, either side of 0h.
const { stars } = mergeStarCatalogues([
{ ...HIPPARCOS, stars: [at(1, 359.9995, 0, 100)] },
{ ...GAIA, stars: [at(2, 0.0005, 0, 100)] }
]);
expect(stars).toHaveLength(1);
});
it('handles a single catalogue as a plain pass-through', () => {
+41 -16
View File
@@ -37,13 +37,28 @@ const DEG_TO_RAD = Math.PI / 180;
export const MERGE_ANGULAR_TOLERANCE_DEG = 15 / 3600;
/**
* How far two catalogues may disagree about a star's brightness and still describe the same
* star. Bands differ — HYG's V and Gaia's G are three magnitudes apart for the reddest dwarfs —
* but five is not a band, it is a companion: Sirius B sits 6″ from Sirius and ten magnitudes
* fainter, Polaris B 18″ and seven. Gaia saturates below G ≈ 3, so without this a bright primary
* it does not carry is folded into its companion's entry, and the companion is gone.
* Angular separation, in degrees, under which the distances are not consulted. A coincidence of
* direction this close is never chance at this depth — the quarter-degree shift finds none under
* 3″ — so two entries this close are one star whatever their parallaxes say, and what they say
* is often a Hipparcos parallax off by half: 1 500 stars sat within this of their Gaia entry and
* were kept twice by the distance test, thirty of them at a false few parsecs from the Sun
* (HIP 82724 at 3.7 pc, where Gaia has it at 62.8). Brightness keeps its say at any separation,
* because a companion can sit this close: Ashlesha's is 2.7″ away and three magnitudes fainter.
*/
export const MERGE_MAGNITUDE_TOLERANCE = 5;
export const MERGE_CERTAIN_ANGULAR_TOLERANCE_DEG = 3 / 3600;
/**
* How much fainter, and how much brighter, an entry may be than the one it is folded into and
* still be the same star. Bands differ, and not symmetrically: a red dwarf is three magnitudes
* fainter in HYG's V than in Gaia's G, so the folded entry may be up to five fainter. A star is
* never much brighter in V than in G, though, and an entry a magnitude brighter than what is
* already at that spot is a primary Gaia does not carry — it saturates below G ≈ 3 — sitting
* beside its companion: Sirius 6″ from Sirius B and ten magnitudes brighter, Almach 10″ from
* γ² And, Alfirk 13″ from β Cep B. Without this the primary's name lands on the companion's
* entry, and the companion is gone.
*/
export const MERGE_FAINTER_TOLERANCE = 5;
export const MERGE_BRIGHTER_TOLERANCE = 1;
/**
* How far two distances may disagree, as a ratio, and still describe the same star. Generous on
@@ -85,8 +100,13 @@ function distanceOf(star: StarRecord): number {
*/
const SKY_CELL_DEG = 0.5;
const RA_CELLS = 360 / SKY_CELL_DEG;
function cellKey(raDeg: number, decDeg: number): string {
return `${Math.floor(raDeg / SKY_CELL_DEG)}:${Math.floor(decDeg / SKY_CELL_DEG)}`;
// Right ascension wraps: the cell after 359.5° is 0°, so a pair straddling 0h shares a
// neighbourhood rather than sitting 719 cells apart.
const raCell = ((Math.floor(raDeg / SKY_CELL_DEG) % RA_CELLS) + RA_CELLS) % RA_CELLS;
return `${raCell}:${Math.floor(decDeg / SKY_CELL_DEG)}`;
}
function skyAngles(star: StarRecord): { raDeg: number; decDeg: number } {
@@ -112,15 +132,12 @@ export function directionCosine(a: StarRecord, b: StarRecord): number {
}
/**
* Whether two entries describe the same star: same direction, and neither distance nor
* brightness in conflict.
* Whether `entry` describes the star already `kept`: the same direction, the brightness not in
* conflict and — unless the directions agree closely enough to settle it — the distance not in
* conflict either.
*/
export function isSameStar(a: StarRecord, b: StarRecord): boolean {
if (Math.abs(a.magnitude - b.magnitude) > MERGE_MAGNITUDE_TOLERANCE) {
return false;
}
const [near, far] = [distanceOf(a), distanceOf(b)].sort((p, q) => p - q);
export function isSameStar(kept: StarRecord, entry: StarRecord): boolean {
const [near, far] = [distanceOf(kept), distanceOf(entry)].sort((p, q) => p - q);
// The Sun sits at the origin of this coordinate system and so has no direction at all, which
// the angular test below cannot speak about. Every catalogue contains it, so without this the
@@ -129,10 +146,18 @@ export function isSameStar(a: StarRecord, b: StarRecord): boolean {
return far === 0;
}
const separationDeg = Math.acos(directionCosine(a, b)) / DEG_TO_RAD;
const separationDeg = Math.acos(directionCosine(kept, entry)) / DEG_TO_RAD;
if (separationDeg > MERGE_ANGULAR_TOLERANCE_DEG) {
return false;
}
const fainterBy = entry.magnitude - kept.magnitude;
if (fainterBy < -MERGE_BRIGHTER_TOLERANCE || fainterBy > MERGE_FAINTER_TOLERANCE) {
return false;
}
if (separationDeg <= MERGE_CERTAIN_ANGULAR_TOLERANCE_DEG) {
return true;
}
return (far - near) / near <= MERGE_DISTANCE_RATIO_TOLERANCE;
}