Files
star-map/src/app/shared/astro/coordinates.ts
T
SenrokaiandClaude Fable 5 08534279fb Bring Gaia to HYG's epoch before merging, and keep a star's name when it matches
Gaia DR3 gives positions for J2016.0, HYG for 2000.0, and the merge matched
them on the sky to one arcsecond without propagating any proper motion.
Sixteen years of motion is 62" for Proxima and 166" for Barnard's Star, so
every star faster than ~62 mas/yr — most of the nearest ones — was kept twice,
some 23 000 in all. The slow ones were matched, and lost: the merge kept
Gaia's row whole, so 102 proper names, 1 336 Bayer/Flamsteed names and
32 000 spectral types became "Gaia DR3 <id>" and "Unknown", and 92 named
exoplanet hosts handed their planets to their anonymous twin.

Gaia is now asked for its proper motions and carried back to J2000 before it
leaves the fetcher. HYG is placed from its own x/y/z columns, which are right
where its `ra` is not: that column was carried from the Hipparcos epoch
without the cos δ its motion needs, 17.9" off for Proxima. A match combines
the two entries — Gaia's position, HYG's name, type, magnitude, colour and id
— instead of choosing one. The tolerance is 15" with a five-magnitude guard,
both set by measurement: 55 457 pairs sit under 1" once the epochs agree, the
Gliese-only entries up to 12" (Ross 248), shifting every entry a quarter of
a degree finds 16 chance neighbours at 15", and the guard keeps Sirius out
of Sirius B's entry. Entries of one source are never merged with each other:
the 1 411 Gaia doubles resolved under 1" are two stars, not one.

Regenerated: 425 071 stars (was 447 410), 56 082 of them Gaia positions
carrying HYG identities; no HYG id or name lost; the sixteen stars nearest
the Sun carry no survey designation; 196 residual doubles, all components
17" or more from their counterpart. Five planets of four bright giants
(7 CMa, HD 81688, omi UMa, xi Aql) lose their host link: their Gaia distance
sits 0.7–1.1 pc from the archive's Hipparcos-based one, past the 0.5 pc the
host match allows. Matching hosts on the sky rather than in space, as the
merge does, is the follow-up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QL6F9Bgfh8SgAiAAcPB9Hw
2026-08-28 20:22:49 +02:00

146 lines
5.7 KiB
TypeScript

const DEG_TO_RAD = Math.PI / 180;
const HOURS_TO_DEG = 15;
export interface CartesianCoordinates {
x: number;
y: number;
z: number;
}
/**
* Converts right ascension (hours), declination (degrees) and distance (parsecs) into
* equatorial Cartesian coordinates (parsecs). Matches the HYG database convention:
* +X toward the vernal equinox (epoch 2000), +Z toward the north celestial pole,
* +Y toward RA 6h / Dec 0.
*/
export function raDecDistanceToXyz(raHours: number, decDeg: number, distancePc: number): CartesianCoordinates {
const raRad = raHours * HOURS_TO_DEG * DEG_TO_RAD;
const decRad = decDeg * DEG_TO_RAD;
const cosDec = Math.cos(decRad);
return {
x: distancePc * cosDec * Math.cos(raRad),
y: distancePc * cosDec * Math.sin(raRad),
z: distancePc * Math.sin(decRad)
};
}
/**
* Same conversion as {@link raDecDistanceToXyz}, but for sources (e.g. exoplanet host
* stars, deep-sky catalogs) that report right ascension in degrees rather than hours.
*/
export function raDegDecDistanceToXyz(raDeg: number, decDeg: number, distancePc: number): CartesianCoordinates {
return raDecDistanceToXyz(raDeg / HOURS_TO_DEG, decDeg, distancePc);
}
const MAS_TO_DEG = 1 / 3_600_000;
/**
* Moves a sky position along its proper motion by `years` — negative to go back in time — so
* catalogues that observed at different epochs can be compared at one. `pmRaMasPerYear` is
* μα cos δ, the on-sky rate Hipparcos and Gaia both publish, hence the division by cos δ to turn
* it back into right ascension.
*/
export function propagateProperMotion(raDeg: number, decDeg: number, pmRaMasPerYear: number, pmDecMasPerYear: number, years: number): { raDeg: number; decDeg: number } {
return {
raDeg: raDeg + (years * pmRaMasPerYear * MAS_TO_DEG) / Math.cos(decDeg * DEG_TO_RAD),
decDeg: decDeg + years * pmDecMasPerYear * MAS_TO_DEG
};
}
/**
* Obliquity of the ecliptic at J2000.0, in degrees — the tilt of Earth's orbital plane against
* its equator, and so the angle between this app's two source frames.
*/
export const OBLIQUITY_J2000_DEG = 23.4392911;
/**
* Rotates a vector from the **ecliptic** frame into the **equatorial** frame, both J2000.
*
* The app has to span both because its two sources disagree. Star positions come from HYG as
* equatorial coordinates, which `raDecDistanceToXyz` produces and which the galaxy view renders
* directly. Orbital elements come from JPL Horizons, whose default reference plane for element
* output is the ecliptic — the ETL never overrides it. The two are tilted
* {@link OBLIQUITY_J2000_DEG} apart about the shared vernal-equinox axis, so orbits have to be
* rotated before they can share a scene with the stars.
*/
export function eclipticToEquatorial(position: CartesianCoordinates): CartesianCoordinates {
const obliquity = OBLIQUITY_J2000_DEG * DEG_TO_RAD;
const cos = Math.cos(obliquity);
const sin = Math.sin(obliquity);
// A rotation about +X, which both frames share: it points at the vernal equinox, where the
// ecliptic and the celestial equator cross.
return {
x: position.x,
y: position.y * cos - position.z * sin,
z: position.y * sin + position.z * cos
};
}
/** Inverse of {@link eclipticToEquatorial}. */
export function equatorialToEcliptic(position: CartesianCoordinates): CartesianCoordinates {
const obliquity = OBLIQUITY_J2000_DEG * DEG_TO_RAD;
const cos = Math.cos(obliquity);
const sin = Math.sin(obliquity);
return {
x: position.x,
y: position.y * cos + position.z * sin,
z: -position.y * sin + position.z * cos
};
}
/**
* Direction to a point on the celestial sphere as a unit vector, in the same equatorial frame
* as {@link raDecDistanceToXyz}. Used for sources whose distance is unknown or irrelevant —
* e.g. deep-sky objects rendered as a backdrop, where only the line of sight matters.
*/
export function raDecToUnitVector(raHours: number, decDeg: number): CartesianCoordinates {
return raDecDistanceToXyz(raHours, decDeg, 1);
}
const SEXAGESIMAL_PATTERN = /^([+-])?(\d+):(\d+):(\d+(?:\.\d+)?)$/;
/**
* Parses a sexagesimal angle ("HH:MM:SS.ss" or "+DD:MM:SS.s", as published by OpenNGC and
* most catalogs) into a decimal value carrying the unit of its first field — hours for right
* ascension, degrees for declination.
*
* The sign is read from the string rather than from the parsed degrees field: `Number('-00')`
* is `-0`, which compares equal to `0`, so a declination like "-00:24:54.8" would otherwise
* come out positive and place the object in the wrong hemisphere.
*
* Returns `null` for anything that isn't a well-formed sexagesimal triple, including the empty
* strings that catalogs use for missing values.
*/
export function parseSexagesimal(text: string | undefined | null): number | null {
const match = SEXAGESIMAL_PATTERN.exec((text ?? '').trim());
if (!match) {
return null;
}
const [, sign, degreesOrHours, minutes, seconds] = match;
const magnitude = Number(degreesOrHours) + Number(minutes) / 60 + Number(seconds) / 3600;
return sign === '-' ? -magnitude : magnitude;
}
/**
* Converts a parallax (milliarcseconds) into a distance in parsecs.
* Returns `Infinity` for non-positive parallax (unmeasured/negative parallax).
*/
export function parallaxMasToParsecs(parallaxMas: number): number {
return parallaxMas > 0 ? 1000 / parallaxMas : Infinity;
}
/**
* Euclidean distance (parsecs) between two Cartesian points, e.g. for nearest-neighbour
* star matching during exoplanet host-star cross-referencing.
*/
export function distanceBetween(a: CartesianCoordinates, b: CartesianCoordinates): number {
const dx = a.x - b.x;
const dy = a.y - b.y;
const dz = a.z - b.z;
return Math.sqrt(dx * dx + dy * dy + dz * dz);
}