moon-sighting/src/time/index.ts
Aric Camarata 8bf34fb696 refactor: code quality improvements and fix arcvMinimum constant
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.
2026-03-08 11:39:28 -04:00

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
}
}