mirror of
https://github.com/acamarata/moon-sighting.git
synced 2026-06-30 19:04:24 +00:00
Fix critical bug: arcvMinimum polynomial constant was 7.1651 (wrong) instead of 11.8371 (Odeh 2006) in getMoonVisibilityEstimate. Now imports the canonical arcvMinimum() from visibility module. Deduplicate shared code across modules: - arcvMinimum polynomial: single source in visibility/index.ts - dot/norm vector helpers: use vdot/vnorm from math/index.ts - DEG constant: use DEG2RAD from math/index.ts - jdToJSDate: use jdToDate from time/index.ts Add input validation to all public API functions (lat/lon range, valid Date instances). Add ESLint + Prettier with TypeScript support. Convert tests to node:test runner. Fix package.json exports to use nested types-first format. Pin devDependencies to caret ranges. Add noImplicitReturns and noFallthroughCasesInSwitch to tsconfig. Replace .markdownlint.json with .vscode/settings.json. Update CI workflow with lint job. Expand .gitignore coverage.
283 lines
9 KiB
TypeScript
283 lines
9 KiB
TypeScript
/**
|
|
* time — Time scale conversions and Julian Date arithmetic.
|
|
*
|
|
* JPL DE ephemerides use Barycentric Dynamical Time (TDB) as their time argument,
|
|
* stored as seconds past J2000.0 (2000 Jan 1, 12:00:00 TDB = JD 2451545.0 TDB).
|
|
*
|
|
* Conversion chain: UTC → TAI → TT → TDB
|
|
* TAI = UTC + ΔAT (ΔAT from leap-second table, integer seconds)
|
|
* TT = TAI + 32.184s (exact, by definition)
|
|
* TDB ≈ TT + 0.001658 * sin(g) + ... (sub-millisecond correction)
|
|
*
|
|
* For Earth-rotation-based quantities (hour angle, ERA), we also need UT1:
|
|
* UT1 = UTC + (UT1 - UTC) (from IERS Bulletin A, typically < ±0.9s)
|
|
* ΔT = TT - UT1 (historically large; ~69s in 2024)
|
|
*
|
|
* References:
|
|
* IERS Conventions (2010) — Chapter 5, time scales
|
|
* NAIF LSK kernel (naif0012.tls) — ΔAT table + TDB approximation constants
|
|
* Espenak & Meeus — ΔT polynomial expressions
|
|
*/
|
|
|
|
import type { TimeScales } from '../types.js'
|
|
|
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
|
|
/** Julian Date of J2000.0 epoch (2000 Jan 1, 12:00 TT) */
|
|
export const J2000 = 2451545.0
|
|
|
|
/** TT - TAI offset in seconds (exact, by definition) */
|
|
export const TT_MINUS_TAI = 32.184
|
|
|
|
/** Seconds per day */
|
|
export const SECONDS_PER_DAY = 86400.0
|
|
|
|
/** Days per Julian century */
|
|
export const DAYS_PER_JULIAN_CENTURY = 36525.0
|
|
|
|
// ─── Leap-second table ────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* ΔAT table (TAI - UTC in seconds), chronological.
|
|
* Source: NAIF naif0012.tls. Update when new leap seconds are announced.
|
|
*
|
|
* Each entry: [JD(UTC) when step takes effect, new ΔAT value]
|
|
* The step applies for all UTC times >= the entry JD.
|
|
*/
|
|
export const LEAP_SECOND_TABLE: ReadonlyArray<readonly [number, number]> = [
|
|
[2441317.5, 10], // 1972 Jan 1
|
|
[2441499.5, 11], // 1972 Jul 1
|
|
[2441683.5, 12], // 1973 Jan 1
|
|
[2442048.5, 13], // 1974 Jan 1
|
|
[2442413.5, 14], // 1975 Jan 1
|
|
[2442778.5, 15], // 1976 Jan 1
|
|
[2443144.5, 16], // 1977 Jan 1
|
|
[2443509.5, 17], // 1978 Jan 1
|
|
[2443874.5, 18], // 1979 Jan 1
|
|
[2444239.5, 19], // 1980 Jan 1
|
|
[2444786.5, 20], // 1981 Jul 1
|
|
[2445151.5, 21], // 1982 Jul 1
|
|
[2445516.5, 22], // 1983 Jul 1
|
|
[2446247.5, 23], // 1985 Jul 1
|
|
[2447161.5, 24], // 1988 Jan 1
|
|
[2447892.5, 25], // 1990 Jan 1
|
|
[2448257.5, 26], // 1991 Jan 1
|
|
[2448804.5, 27], // 1992 Jul 1
|
|
[2449169.5, 28], // 1993 Jul 1
|
|
[2449534.5, 29], // 1994 Jul 1
|
|
[2450083.5, 30], // 1996 Jan 1
|
|
[2450630.5, 31], // 1997 Jul 1
|
|
[2451179.5, 32], // 1999 Jan 1
|
|
[2453736.5, 33], // 2006 Jan 1
|
|
[2454832.5, 34], // 2009 Jan 1
|
|
[2456109.5, 35], // 2012 Jul 1
|
|
[2457204.5, 36], // 2015 Jul 1
|
|
[2457754.5, 37], // 2017 Jan 1
|
|
] as const
|
|
|
|
/**
|
|
* Get the current leap second count (TAI - UTC) for a given JD in UTC.
|
|
* Returns 10 for dates before 1972 (the first leap second era).
|
|
*/
|
|
export function getDeltaAT(jdUTC: number): number {
|
|
let deltaAT = 10
|
|
for (const [jd, dat] of LEAP_SECOND_TABLE) {
|
|
if (jdUTC >= jd) deltaAT = dat
|
|
else break
|
|
}
|
|
return deltaAT
|
|
}
|
|
|
|
// ─── Julian Date ─────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Convert a JavaScript Date (UTC) to Julian Date in UTC.
|
|
* Uses the standard formula; valid for dates after the Gregorian reform.
|
|
*/
|
|
export function dateToJD(date: Date): number {
|
|
return date.getTime() / 86400000 + 2440587.5
|
|
}
|
|
|
|
/**
|
|
* Convert a Julian Date in UTC to a JavaScript Date.
|
|
*/
|
|
export function jdToDate(jd: number): Date {
|
|
return new Date((jd - 2440587.5) * 86400000)
|
|
}
|
|
|
|
/**
|
|
* Julian centuries from J2000.0 (in TT).
|
|
* Used as the standard argument for precession and nutation polynomials.
|
|
*/
|
|
export function jdTTtoT(jdTT: number): number {
|
|
return (jdTT - J2000) / DAYS_PER_JULIAN_CENTURY
|
|
}
|
|
|
|
// ─── Time scale conversions ───────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Compute all relevant time scales for a given UTC Date.
|
|
*
|
|
* @param utc - Input time in UTC
|
|
* @param ut1utcOverride - UT1 - UTC in seconds (from IERS Bulletin A). Falls
|
|
* back to 0 when not provided, which introduces ~0.9s error in ERA at most.
|
|
* @param deltaTOverride - TT - UT1 in seconds. When provided, overrides the
|
|
* polynomial approximation. Provide this for maximum accuracy.
|
|
*/
|
|
export function computeTimeScales(
|
|
utc: Date,
|
|
ut1utcOverride?: number,
|
|
deltaTOverride?: number,
|
|
): TimeScales {
|
|
const jdUTC = dateToJD(utc)
|
|
const deltaAT = getDeltaAT(jdUTC)
|
|
|
|
// UTC → TAI → TT
|
|
const jdTAI = jdUTC + deltaAT / SECONDS_PER_DAY
|
|
const jdTT = jdTAI + TT_MINUS_TAI / SECONDS_PER_DAY
|
|
|
|
// TT → TDB (periodic correction, sub-millisecond)
|
|
const tdbCorrection = tdbMinusTT(jdTT) / SECONDS_PER_DAY
|
|
const jdTDB = jdTT + tdbCorrection
|
|
|
|
// UT1
|
|
let jdUT1: number
|
|
let deltaT: number
|
|
|
|
if (ut1utcOverride !== undefined) {
|
|
jdUT1 = jdUTC + ut1utcOverride / SECONDS_PER_DAY
|
|
deltaT = (jdTT - jdUT1) * SECONDS_PER_DAY
|
|
} else if (deltaTOverride !== undefined) {
|
|
deltaT = deltaTOverride
|
|
jdUT1 = jdTT - deltaT / SECONDS_PER_DAY
|
|
} else {
|
|
deltaT = deltaTPolynomial(jdTT)
|
|
jdUT1 = jdTT - deltaT / SECONDS_PER_DAY
|
|
}
|
|
|
|
return { utc, jdUTC, jdTT, jdTDB, jdUT1, deltaT, deltaAT }
|
|
}
|
|
|
|
/**
|
|
* Convert Julian Date in TT to ET seconds past J2000.0.
|
|
* ET (Ephemeris Time) is used as the time argument in SPK kernels.
|
|
* TDB and TT differ by at most ~1.7ms; applying the correction here
|
|
* gives the proper SPICE-compatible ET value.
|
|
*/
|
|
export function jdTTtoET(jdTT: number): number {
|
|
const tdbCorr = tdbMinusTT(jdTT)
|
|
return (jdTT - J2000) * SECONDS_PER_DAY + tdbCorr
|
|
}
|
|
|
|
/**
|
|
* Approximate TDB - TT in seconds.
|
|
* Uses the SPICE-consistent formulation from the LSK kernel constants.
|
|
*
|
|
* TDB - TT = 0.001658 * sin(g) + 0.000014 * sin(2g)
|
|
* where g = 357.53° + 0.9856003° * (JD_TT - 2451545.0) [mean anomaly of the Sun]
|
|
*
|
|
* Maximum error: ~30 microseconds (acceptable for crescent work).
|
|
*/
|
|
export function tdbMinusTT(jdTT: number): number {
|
|
const d = jdTT - J2000
|
|
// Mean anomaly of the Sun (degrees)
|
|
const gDeg = 357.53 + 0.9856003 * d
|
|
const g = (gDeg * Math.PI) / 180
|
|
return 0.001658 * Math.sin(g) + 0.000014 * Math.sin(2 * g)
|
|
}
|
|
|
|
/**
|
|
* Delta-T polynomial: TT - UT1 in seconds.
|
|
* Uses Espenak & Meeus expressions, piecewise by year range.
|
|
*
|
|
* Reference: NASA Five Millennium Canon of Solar Eclipses, Espenak & Meeus (2009)
|
|
*/
|
|
export function deltaTPolynomial(jdTT: number): number {
|
|
// Convert JD to decimal year
|
|
const y = 2000 + (jdTT - J2000) / 365.25
|
|
|
|
if (y < -500) {
|
|
const u = (y - 1820) / 100
|
|
return -20 + 32 * u * u
|
|
} else if (y < 500) {
|
|
const u = y / 100
|
|
return (
|
|
10583.6 -
|
|
1014.41 * u +
|
|
33.78311 * u * u -
|
|
5.952053 * u * u * u -
|
|
0.1798452 * u ** 4 +
|
|
0.022174192 * u ** 5 +
|
|
0.0090316521 * u ** 6
|
|
)
|
|
} else if (y < 1600) {
|
|
const u = (y - 1000) / 100
|
|
return (
|
|
1574.2 -
|
|
556.01 * u +
|
|
71.23472 * u * u +
|
|
0.319781 * u ** 3 -
|
|
0.8503463 * u ** 4 -
|
|
0.005050998 * u ** 5 +
|
|
0.0083572073 * u ** 6
|
|
)
|
|
} else if (y < 1700) {
|
|
const t = y - 1600
|
|
return 120 - 0.9808 * t - 0.01532 * t * t + t ** 3 / 7129
|
|
} else if (y < 1800) {
|
|
const t = y - 1700
|
|
return 8.83 + 0.1603 * t - 0.0059285 * t * t + 0.00013336 * t ** 3 - t ** 4 / 1174000
|
|
} else if (y < 1860) {
|
|
const t = y - 1800
|
|
return (
|
|
13.72 -
|
|
0.332447 * t +
|
|
0.0068612 * t * t +
|
|
0.0041116 * t ** 3 -
|
|
0.00037436 * t ** 4 +
|
|
0.0000121272 * t ** 5 -
|
|
0.0000001699 * t ** 6 +
|
|
0.000000000875 * t ** 7
|
|
)
|
|
} else if (y < 1900) {
|
|
const t = y - 1860
|
|
return (
|
|
7.62 +
|
|
0.5737 * t -
|
|
0.251754 * t * t +
|
|
0.01680668 * t ** 3 -
|
|
0.0004473624 * t ** 4 +
|
|
t ** 5 / 233174
|
|
)
|
|
} else if (y < 1920) {
|
|
const t = y - 1900
|
|
return -2.79 + 1.494119 * t - 0.0598939 * t * t + 0.0061966 * t ** 3 - 0.000197 * t ** 4
|
|
} else if (y < 1941) {
|
|
const t = y - 1920
|
|
return 21.2 + 0.84493 * t - 0.0761 * t * t + 0.0020936 * t ** 3
|
|
} else if (y < 1961) {
|
|
const t = y - 1950
|
|
return 29.07 + 0.407 * t - (t * t) / 233 + t ** 3 / 2547
|
|
} else if (y < 1986) {
|
|
const t = y - 1975
|
|
return 45.45 + 1.067 * t - (t * t) / 260 - t ** 3 / 718
|
|
} else if (y < 2005) {
|
|
const t = y - 2000
|
|
return (
|
|
63.86 +
|
|
0.3345 * t -
|
|
0.060374 * t * t +
|
|
0.0017275 * t ** 3 +
|
|
0.000651814 * t ** 4 +
|
|
0.00002373599 * t ** 5
|
|
)
|
|
} else if (y < 2050) {
|
|
const t = y - 2000
|
|
return 62.92 + 0.32217 * t + 0.005589 * t * t
|
|
} else if (y < 2150) {
|
|
return -20 + 32 * ((y - 1820) / 100) ** 2 - 0.5628 * (2150 - y)
|
|
} else {
|
|
const u = (y - 1820) / 100
|
|
return -20 + 32 * u * u
|
|
}
|
|
}
|