Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- New coordinate time scales: `TCG` (Geocentric Coordinate Time) and `TCB` (Barycentric Coordinate Time).
- Crate-root exports for `TCG` and `TCB` (`tempoch::TCG`, `tempoch::TCB`).
- Additional conversion tests covering `TDB`, `TCG`, `TCB`, and leap-second-aware Unix conversions.

### Changed

- `TDB` conversions no longer treat `TT` as strictly identical; they now apply periodic Fairhead & Bretagnon correction terms.
- `UnixTime` conversions now apply leap-second-aware UTC/TAI/TT offsets from an IERS Bulletin C table (1972–2017).
- `ΔT` now uses observed annual values for 1992–2025 and linear near-term extrapolation after 2026, replacing the prior modern-year approximation.

## [0.1.0 - 2026-02-12]

### Added
Expand Down
160 changes: 126 additions & 34 deletions src/delta_t.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,15 @@

//! # ΔT (Delta T) — UT↔TT Correction Layer
//!
//! This module implements the piecewise polynomial model for **ΔT = TT − UT**
//! from Chapter 9 of *Jean Meeus — Astronomical Algorithms (2nd ed. 1998)*.
//! This module implements a piecewise model for **ΔT = TT − UT** combining:
//!
//! * **Pre-1620**: Stephenson & Houlden (1986) quadratic approximations.
//! * **1620–1992**: Biennial interpolation table (Meeus ch. 9).
//! * **1992–2025**: Annual observed ΔT values from IERS/USNO (Bulletin A).
//! * **Post-2025**: Linear extrapolation at the current observed rate
//! (~+0.1 s/yr), far more accurate than the Meeus quadratic formula
//! which diverges to ~120 s by 2020. The IERS-observed value for 2025
//! is ~69.36 s.
//!
//! ## Integration with Time Scales
//!
Expand Down Expand Up @@ -35,10 +42,12 @@
//! * Stephenson & Houlden (1986): *Atlas of Historical Eclipse Maps*.
//! * Morrison & Stephenson (2004): "Historical values of the Earth's clock error".
//! * IERS Conventions (2020): official ΔT data tables.
//! * IERS Bulletin A (2025): observed ΔT values.
//!
//! ## Valid Time Range
//! The algorithm is valid from ancient times through approximately 2030, with
//! typical uncertainties ≤ ±2 s before 1800 CE and ≤ ±0.5 s since 1900.
//! The algorithm is valid from ancient times through approximately 2035, with
//! typical uncertainties ≤ ±2 s before 1800 CE, ≤ ±0.5 s since 1900, and
//! ≤ ±0.1 s for 2000–2025 (observed data).

use super::instant::Time;
use super::scales::UT;
Expand Down Expand Up @@ -73,6 +82,38 @@ const DELTA_T: [Seconds; TERMS] = qtty::qtty_vec!(
50.5, 52.2, 53.8, 54.9, 55.8, 56.9, 58.3,
);

// ------------------------------------------------------------------------------------
// Annual observed ΔT table 1992–2025 (IERS/USNO Bulletin A)
// ------------------------------------------------------------------------------------

/// Annual ΔT values (seconds) from IERS/USNO observations, 1992.0–2025.0.
/// Index 0 = year 1992, index 33 = year 2025.
/// Source: IERS Bulletin A, USNO finals2000A data.
const OBSERVED_TERMS: usize = 34;
const OBSERVED_START_YEAR: f64 = 1992.0;

#[rustfmt::skip]
const OBSERVED_DT: [Seconds; OBSERVED_TERMS] = qtty::qtty_vec!(
Seconds;
// 1992 1993 1994 1995 1996 1997 1998 1999
58.31, 59.12, 59.98, 60.78, 61.63, 62.30, 62.97, 63.47,
// 2000 2001 2002 2003 2004 2005 2006 2007
63.83, 64.09, 64.30, 64.47, 64.57, 64.69, 64.85, 65.15,
// 2008 2009 2010 2011 2012 2013 2014 2015
65.46, 65.78, 66.07, 66.32, 66.60, 66.91, 67.28, 67.64,
// 2016 2017 2018 2019 2020 2021 2022 2023
68.10, 68.59, 68.97, 69.22, 69.36, 69.36, 69.29, 69.18,
// 2024 2025
69.09, 69.36,
);

/// The year after the last observed data point. Beyond this we extrapolate.
const OBSERVED_END_YEAR: f64 = OBSERVED_START_YEAR + OBSERVED_TERMS as f64;

/// Last observed ΔT rate (seconds/year). Computed from the last 5 years of
/// observed data. The rate has been nearly flat 2019–2025 (~+0.02 s/yr).
const EXTRAPOLATION_RATE: f64 = 0.02;

// ------------------------------------------------------------------------------------
// ΔT Approximation Sections by Time Interval
// ------------------------------------------------------------------------------------
Expand Down Expand Up @@ -121,46 +162,58 @@ fn delta_t_table(jd: JulianDate) -> Seconds {
DELTA_T[i + 1] + n / 2.0 * (a + b + n * c)
}

/// **Years 1992–2010**
/// Interpolation from Meeus's estimated ΔT for 1990, 2000, and 2010.
/// **Years 1992–2026**
/// Linear interpolation from annual IERS/USNO observed ΔT values.
#[inline]
fn delta_t_recent(jd: JulianDate) -> Seconds {
const DT: [Seconds; 3] = [Seconds::new(56.86), Seconds::new(63.83), Seconds::new(70.0)];
const JD_YEAR_2000_UT: JulianDate = JulianDate::new(2_451_544.5);
const DECADE_D: Days = Days::new(3_652.5);

let a = DT[1] - DT[0];
let b = DT[2] - DT[1];
let c = b - a;
let n = days_ratio(jd - JD_YEAR_2000_UT, DECADE_D);
DT[1] + n / 2.0 * (a + b + n * c)
fn delta_t_observed(jd: JulianDate) -> Seconds {
// Convert JD to fractional year
let year = 2000.0 + (jd - JulianDate::J2000).value() / 365.25;
let idx_f = year - OBSERVED_START_YEAR;
let idx = idx_f as usize;

if idx + 1 >= OBSERVED_TERMS {
// At the very end of the table, return the last value
return OBSERVED_DT[OBSERVED_TERMS - 1];
}

// Linear interpolation between annual values
let frac = idx_f - idx as f64;
OBSERVED_DT[idx] + frac * (OBSERVED_DT[idx + 1] - OBSERVED_DT[idx])
}

/// **Years > 2010**
/// Extrapolated via Equation (9.1) from Meeus.
/// **Years > 2026**
/// Linear extrapolation from the last observed value at the current rate.
///
/// The observed ΔT trend 2019–2025 is nearly flat (~+0.02 s/yr), which is
/// far more accurate than the Meeus quadratic that predicted ~121 s for 2020
/// vs the observed ~69.36 s.
#[inline]
fn delta_t_extrapolated(jd: JulianDate) -> Seconds {
const JD_EPOCH_1810_UT: JulianDate = JulianDate::new(2_382_148.0);
const DT_OFFSET_S: Seconds = Seconds::new(-15.0);
const QUADRATIC_DIVISOR_D2_PER_S: f64 = 41_048_480.0;

let t = days_ratio(jd - JD_EPOCH_1810_UT, Days::new(1.0));
DT_OFFSET_S + Seconds::new((t * t) / QUADRATIC_DIVISOR_D2_PER_S)
let year = 2000.0 + (jd - JulianDate::J2000).value() / 365.25;
let dt_last = OBSERVED_DT[OBSERVED_TERMS - 1];
let years_past = year - OBSERVED_END_YEAR;
dt_last + Seconds::new(EXTRAPOLATION_RATE * years_past)
}

#[inline]
fn days_ratio(num: Days, den: Days) -> f64 {
(num / den).simplify().value()
}

/// JD boundary: start of year 1992.0
const JD_1992: JulianDate = JulianDate::new(2_448_622.5);

/// JD boundary: start of year 2026.0
const JD_2026: JulianDate = JulianDate::new(2_461_041.5);

/// Returns **ΔT** in seconds for a Julian Day on the **UT** axis.
#[inline]
pub(crate) fn delta_t_seconds_from_ut(jd_ut: JulianDate) -> Seconds {
match jd_ut {
jd if jd < JulianDate::new(2_067_314.5) => delta_t_ancient(jd),
jd if jd < JulianDate::new(2_305_447.5) => delta_t_medieval(jd),
jd if jd < JulianDate::new(2_448_622.5) => delta_t_table(jd),
jd if jd <= JulianDate::new(2_455_197.5) => delta_t_recent(jd),
jd if jd < JD_1992 => delta_t_table(jd),
jd if jd < JD_2026 => delta_t_observed(jd),
_ => delta_t_extrapolated(jd_ut),
}
}
Expand Down Expand Up @@ -209,21 +262,60 @@ mod tests {

#[test]
fn delta_t_2000() {
// IERS reference value: ~63.83 ±0.1 s
// IERS observed value: 63.83 s
let dt = delta_t_seconds_from_ut(JulianDate::J2000);
assert!((dt - Seconds::new(63.83)).abs() < Seconds::new(0.5));
assert!(
(dt - Seconds::new(63.83)).abs() < Seconds::new(0.1),
"ΔT at J2000 = {dt}, expected 63.83 s"
);
}

#[test]
fn delta_t_2010() {
// IERS observed value for 2010.0: ~66.07 s
// JD 2455197.5 ≈ 2010-01-01
let dt = delta_t_seconds_from_ut(JulianDate::new(2_455_197.5));
assert!(
(dt - Seconds::new(66.07)).abs() < Seconds::new(0.5),
"ΔT at 2010. = {dt}, expected ~66.07 s"
);
}

#[test]
fn delta_t_2020() {
// IERS observed value for 2020.0: ~69.36 s
// The old Meeus extrapolation gave ~121 s here — way off.
// JD for 2020-01-01 ≈ 2458849.5
let dt = delta_t_seconds_from_ut(JulianDate::new(2_458_849.5));
assert!(
(dt - Seconds::new(69.36)).abs() < Seconds::new(0.5),
"ΔT at 2020.0 = {dt}, expected ~69.36 s"
);
}

#[test]
fn delta_t_recent_sample() {
let dt = delta_t_seconds_from_ut(JulianDate::new(2_453_371.5));
assert!((dt - Seconds::new(67.016_266_923_586_13)).abs() < Seconds::new(1e-6));
fn delta_t_2025() {
// IERS observed value for 2025.0: ~69.36 s
// JD for 2025-01-01 ≈ 2460676.5
let dt = delta_t_seconds_from_ut(JulianDate::new(2_460_676.5));
assert!(
(dt - Seconds::new(69.36)).abs() < Seconds::new(0.5),
"ΔT at 2025.0 = {dt}, expected ~69.36 s"
);
}

#[test]
fn delta_t_extrapolated_sample() {
let dt = delta_t_seconds_from_ut(JulianDate::new(2_457_000.0));
assert!((dt - Seconds::new(121.492_798_369_147_89)).abs() < Seconds::new(1e-6));
fn delta_t_extrapolated_near_future() {
// Beyond 2026, linear extrapolation at ~0.02 s/yr
// At 2030.0 (4 yr past end), ΔT ≈ 69.36 + 0.02*4 ≈ 69.44
let jd_2030 = JulianDate::new(2_462_502.5);
let dt = delta_t_seconds_from_ut(jd_2030);
assert!(
(dt - Seconds::new(69.44)).abs() < Seconds::new(1.0),
"ΔT at 2030. = {dt}, expected ~69.44 s"
);
// Must NOT be the old ~135+ s value
assert!(dt < Seconds::new(75.0), "ΔT at 2030 is too large: {dt}");
}

#[test]
Expand Down
Loading