Number the stars the archive places after their host's name, so a refresh keeps each one's id
4c8e4a0 numbered each star it adds from the Exoplanet Archive by its place among the unmatched
hosts, in pl_name order, and the refresh workflow re-queries the archive every Monday. One host
added or dropped ahead of another renumbers it: in the review, removing a single planet row
renamed 3 276 of the 3 277 ids, and a bookmark kept on Kepler-186 (1070001620) opened Kepler-1860
under Kepler-186's stored name. HYG's and Gaia's ids do not move between refreshes; the merge's own
comment says ids are meant to hold.
archiveStarId (host-star-matching.ts, beside the matcher the ETL already imports from there) hashes
the host name with FNV-1a into the 3.7 million ids between 1 070 000 000 and 2^30, and moves a name
whose id is taken to the next free one. The added stars are sorted by id before they are appended,
so the published list stays in id order. Measured: all 4 237 archive-hosted planets change host id
once (Kepler-186 is now 1073671518), none of the others; one of the 3 277 names was probed past a
collision, and one new id falls in the old 1070000000-1070003276 range, so a bookmark saved on that
old id would open the wrong star once. Dropping the same row from a copy of the cached answer and
rerunning the exoplanet step in a scratch copy of the ETL now leaves 3 276 of 3 276 ids unchanged.
The validators pass: no duplicate id, all under 2^30.
Controls, each failing its named test: the id taken from the order of arrival, and no probing past
a taken id (1 of 801 each).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import { buildStarNameIndex, normalizeStarName, resolveHostStarId } from './host-star-matching';
|
import { ARCHIVE_ID_BASE, archiveStarId, buildStarNameIndex, normalizeStarName, resolveHostStarId } from './host-star-matching';
|
||||||
import { propagateProperMotion, raDegDecDistanceToXyz } from './coordinates';
|
import { propagateProperMotion, raDegDecDistanceToXyz } from './coordinates';
|
||||||
import { StarRecord } from '../models/star.model';
|
import { StarRecord } from '../models/star.model';
|
||||||
|
|
||||||
@@ -214,3 +214,19 @@ describe('resolveHostStarId', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('archiveStarId', () => {
|
||||||
|
it('gives a host the same id whatever else the archive holds, inside the range left for it', () => {
|
||||||
|
const kepler186 = archiveStarId('Kepler-186', new Set());
|
||||||
|
const others = new Set(['1RXS J160929.1-210524', 'Kepler-1860', 'Kepler-452', 'TOI-700'].map((name) => archiveStarId(name, new Set())));
|
||||||
|
expect(archiveStarId('Kepler-186', others)).toBe(kepler186);
|
||||||
|
expect(others.has(kepler186)).toBe(false);
|
||||||
|
expect(kepler186).toBeGreaterThanOrEqual(ARCHIVE_ID_BASE);
|
||||||
|
expect(kepler186).toBeLessThan(2 ** 30);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('moves a host whose id is taken to the next free one', () => {
|
||||||
|
const kepler186 = archiveStarId('Kepler-186', new Set());
|
||||||
|
expect(archiveStarId('Kepler-186', new Set([kepler186, kepler186 + 1]))).toBe(kepler186 + 2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -171,3 +171,30 @@ export function resolveHostStarId(
|
|||||||
|
|
||||||
return best ? best.id : null;
|
return best ? best.id : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Where the ids of the stars only the archive places begin: past Gaia's two ranges, and under
|
||||||
|
* the 2^30 `validateStars` holds every id to — which leaves 3.7 million.
|
||||||
|
*/
|
||||||
|
export const ARCHIVE_ID_BASE = 1_070_000_000;
|
||||||
|
const ARCHIVE_ID_RANGE = 2 ** 30 - ARCHIVE_ID_BASE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id of a star the ETL places from the archive, from its host's name rather than from its
|
||||||
|
* place in the answer. Numbered in pl_name order, a refresh that added or dropped one host
|
||||||
|
* renumbered every host after it — one row dropped renamed 3 276 of 3 277 ids, and a bookmark kept
|
||||||
|
* on Kepler-186 opened Kepler-1860 — while HYG's and Gaia's ids hold. FNV-1a over the name, into
|
||||||
|
* the range above; a name whose id is taken takes the next free one, the one case a refresh can
|
||||||
|
* still move, and only between the two names that collided.
|
||||||
|
*/
|
||||||
|
export function archiveStarId(hostname: string, taken: ReadonlySet<number>): number {
|
||||||
|
let hash = 0x811c9dc5;
|
||||||
|
for (let i = 0; i < hostname.length; i++) {
|
||||||
|
hash = Math.imul(hash ^ hostname.charCodeAt(i), 0x01000193) >>> 0;
|
||||||
|
}
|
||||||
|
let offset = hash % ARCHIVE_ID_RANGE;
|
||||||
|
while (taken.has(ARCHIVE_ID_BASE + offset)) {
|
||||||
|
offset = (offset + 1) % ARCHIVE_ID_RANGE;
|
||||||
|
}
|
||||||
|
return ARCHIVE_ID_BASE + offset;
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto';
|
|||||||
import { writeFileSync } from 'node:fs';
|
import { writeFileSync } from 'node:fs';
|
||||||
|
|
||||||
import { propagateProperMotion, raDegDecDistanceToXyz } from '../../src/app/shared/astro/coordinates';
|
import { propagateProperMotion, raDegDecDistanceToXyz } from '../../src/app/shared/astro/coordinates';
|
||||||
import { buildStarNameIndex, resolveHostStarId } from '../../src/app/shared/astro/host-star-matching';
|
import { archiveStarId, buildStarNameIndex, resolveHostStarId } from '../../src/app/shared/astro/host-star-matching';
|
||||||
import { temperatureToColorIndex } from '../../src/app/shared/astro/spectral';
|
import { temperatureToColorIndex } from '../../src/app/shared/astro/spectral';
|
||||||
import { ExoplanetRecord } from '../../src/app/shared/models/exoplanet.model';
|
import { ExoplanetRecord } from '../../src/app/shared/models/exoplanet.model';
|
||||||
import { isDesignation } from '../../src/app/shared/models/star-catalog';
|
import { isDesignation } from '../../src/app/shared/models/star-catalog';
|
||||||
@@ -85,11 +85,6 @@ const COMPOSITE_CACHE_FILE = `exoplanet-archive-pscomppars-${createHash('sha1').
|
|||||||
const DISTANCE_ERRORS_URL = `${TAP_BASE_URL}?query=select+pl_name,sy_disterr1,sy_disterr2+from+pscomppars+order+by+pl_name&format=csv`;
|
const DISTANCE_ERRORS_URL = `${TAP_BASE_URL}?query=select+pl_name,sy_disterr1,sy_disterr2+from+pscomppars+order+by+pl_name&format=csv`;
|
||||||
const DISTANCE_ERRORS_CACHE_FILE = `exoplanet-archive-disterr-${createHash('sha1').update(DISTANCE_ERRORS_URL).digest('hex').slice(0, 8)}.csv`;
|
const DISTANCE_ERRORS_CACHE_FILE = `exoplanet-archive-disterr-${createHash('sha1').update(DISTANCE_ERRORS_URL).digest('hex').slice(0, 8)}.csv`;
|
||||||
|
|
||||||
/**
|
|
||||||
* Where the ids of the stars only the archive places begin: past Gaia's two ranges, and under
|
|
||||||
* the 2^30 `validateStars` holds every id to.
|
|
||||||
*/
|
|
||||||
const ARCHIVE_ID_BASE = 1_070_000_000;
|
|
||||||
const ARCHIVE_SOURCE = 'exoplanet-archive';
|
const ARCHIVE_SOURCE = 'exoplanet-archive';
|
||||||
/**
|
/**
|
||||||
* The archive's positions are at Gaia's epoch, J2016, not the catalogue's J2000: of the 746
|
* The archive's positions are at Gaia's epoch, J2016, not the catalogue's J2000: of the 746
|
||||||
@@ -123,6 +118,7 @@ export async function fetchExoplanets(stars?: StarRecord[]): Promise<{ exoplanet
|
|||||||
// the only way "TRAPPIST-1" or "Teegarden's Star" can be found by search.
|
// the only way "TRAPPIST-1" or "Teegarden's Star" can be found by search.
|
||||||
const renamed = new Map<number, string>();
|
const renamed = new Map<number, string>();
|
||||||
const archiveStars = new Map<string, StarRecord>();
|
const archiveStars = new Map<string, StarRecord>();
|
||||||
|
const archiveIds = new Set<number>();
|
||||||
const bands = { V: 0, G: 0, none: 0 };
|
const bands = { V: 0, G: 0, none: 0 };
|
||||||
const exoplanets: ExoplanetRecord[] = rows.map((row, index) => {
|
const exoplanets: ExoplanetRecord[] = rows.map((row, index) => {
|
||||||
const compositeRow = composite.get(row['pl_name']);
|
const compositeRow = composite.get(row['pl_name']);
|
||||||
@@ -166,7 +162,7 @@ export async function fetchExoplanets(stars?: StarRecord[]): Promise<{ exoplanet
|
|||||||
const errors = distanceErrors.get(row['pl_name']);
|
const errors = distanceErrors.get(row['pl_name']);
|
||||||
const [above, below] = [parseOptionalNumber(errors?.['sy_disterr1']), parseOptionalNumber(errors?.['sy_disterr2'])];
|
const [above, below] = [parseOptionalNumber(errors?.['sy_disterr1']), parseOptionalNumber(errors?.['sy_disterr2'])];
|
||||||
archiveStar = {
|
archiveStar = {
|
||||||
id: ARCHIVE_ID_BASE + archiveStars.size,
|
id: archiveStarId(row['hostname'], archiveIds),
|
||||||
name: row['hostname'],
|
name: row['hostname'],
|
||||||
...raDegDecDistanceToXyz(j2000.raDeg, j2000.decDeg, distancePc),
|
...raDegDecDistanceToXyz(j2000.raDeg, j2000.decDeg, distancePc),
|
||||||
magnitude: v ?? g ?? UNKNOWN_MAGNITUDE,
|
magnitude: v ?? g ?? UNKNOWN_MAGNITUDE,
|
||||||
@@ -178,6 +174,7 @@ export async function fetchExoplanets(stars?: StarRecord[]): Promise<{ exoplanet
|
|||||||
source: ARCHIVE_SOURCE
|
source: ARCHIVE_SOURCE
|
||||||
};
|
};
|
||||||
archiveStars.set(row['hostname'], archiveStar);
|
archiveStars.set(row['hostname'], archiveStar);
|
||||||
|
archiveIds.add(archiveStar.id);
|
||||||
}
|
}
|
||||||
hostStarId = archiveStar.id;
|
hostStarId = archiveStar.id;
|
||||||
}
|
}
|
||||||
@@ -215,9 +212,9 @@ export async function fetchExoplanets(stars?: StarRecord[]): Promise<{ exoplanet
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Appended after the catalogue, whose ids all sit below ARCHIVE_ID_BASE, so the list stays in
|
// Appended after the catalogue, whose ids all sit below ARCHIVE_ID_BASE, and sorted, so the list
|
||||||
// the id order `fetchStars` sorted it into.
|
// stays in the id order `fetchStars` sorted it into.
|
||||||
const added = [...archiveStars.values()];
|
const added = [...archiveStars.values()].sort((a, b) => a.id - b.id);
|
||||||
const allStars = [...knownStars.map((star) => (renamed.has(star.id) ? { ...star, name: renamed.get(star.id)! } : star)), ...added];
|
const allStars = [...knownStars.map((star) => (renamed.has(star.id) ? { ...star, name: renamed.get(star.id)! } : star)), ...added];
|
||||||
writeStarAssets(allStars);
|
writeStarAssets(allStars);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user