Skip to main content

tempoch_core/
delta_t.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2// Copyright (C) 2026 Vallés Puig, Ramon
3
4//! # ΔT (Delta T) — UT↔TT Correction Layer
5//!
6//! This module implements a piecewise model for **ΔT = TT − UT** combining:
7//!
8//! * **Pre-1620**: Stephenson & Houlden (1986) quadratic approximations.
9//! * **1620–1992**: Biennial interpolation table (Meeus ch. 9).
10//! * **1992–2025**: Annual observed ΔT values from IERS/USNO (Bulletin A).
11//! * **Post-2025**: Linear extrapolation at the current observed rate
12//!   (~+0.1 s/yr), far more accurate than the Meeus quadratic formula
13//!   which diverges to ~120 s by 2020. The IERS-observed value for 2025
14//!   is ~69.36 s.
15//!
16//! ## Integration with Time Scales
17//!
18//! The correction is applied **automatically** by the [`UT`](super::UT) time
19//! scale marker.  When you convert from `Time<UT>` to any TT-based scale
20//! (`.to::<JD>()`, `.to::<MJD>()`, etc.), `UT::to_jd_tt` adds ΔT.
21//! The inverse (`UT::from_jd_tt`) uses a three-iteration fixed-point solver.
22//!
23//! [`Time::from_utc`](super::Time::from_utc) creates a `Time<UT>` internally
24//! and then converts to the target scale, so external callers get the ΔT
25//! correction without calling any function from this module.
26//!
27//! ## Quick Example
28//! ```rust
29//! # use tempoch_core as tempoch;
30//! use tempoch::{UT, JD, Time};
31//!
32//! // UT-based Julian Day -> JD(TT) with ΔT applied
33//! let ut = Time::<UT>::new(2_451_545.0);
34//! let jd_tt = ut.to::<JD>();
35//! println!("JD(TT) = {jd_tt}");
36//!
37//! // Query the raw ΔT value
38//! let dt = ut.delta_t();
39//! println!("ΔT = {dt}");
40//! ```
41//!
42//! ## Scientific References
43//! * Stephenson & Houlden (1986): *Atlas of Historical Eclipse Maps*.
44//! * Morrison & Stephenson (2004): "Historical values of the Earth's clock error".
45//! * IERS Conventions (2020): official ΔT data tables.
46//! * IERS Bulletin A (2025): observed ΔT values.
47//!
48//! ## Valid Time Range
49//! The algorithm is valid from ancient times through approximately 2035, with
50//! typical uncertainties ≤ ±2 s before 1800 CE, ≤ ±0.5 s since 1900, and
51//! ≤ ±0.1 s for 2000–2025 (observed data).
52
53use super::instant::Time;
54use super::scales::UT;
55use super::JulianDate;
56use qtty::{Days, Seconds, Simplify};
57
58/// Total number of tabulated terms (biennial 1620–1992).
59const TERMS: usize = 187;
60
61/// Biennial ΔT table from 1620 to 1992 (in seconds), compiled by J. Meeus.
62#[rustfmt::skip]
63const DELTA_T: [Seconds; TERMS] = qtty::qtty_vec!(
64    Seconds;
65    124.0,115.0,106.0, 98.0, 91.0, 85.0, 79.0, 74.0, 70.0, 65.0,
66     62.0, 58.0, 55.0, 53.0, 50.0, 48.0, 46.0, 44.0, 42.0, 40.0,
67     37.0, 35.0, 33.0, 31.0, 28.0, 26.0, 24.0, 22.0, 20.0, 18.0,
68     16.0, 14.0, 13.0, 12.0, 11.0, 10.0,  9.0,  9.0,  9.0,  9.0,
69      9.0,  9.0,  9.0,  9.0, 10.0, 10.0, 10.0, 10.0, 10.0, 11.0,
70     11.0, 11.0, 11.0, 11.0, 11.0, 11.0, 12.0, 12.0, 12.0, 12.0,
71     12.0, 12.0, 13.0, 13.0, 13.0, 13.0, 14.0, 14.0, 14.0, 15.0,
72     15.0, 15.0, 15.0, 16.0, 16.0, 16.0, 16.0, 16.0, 17.0, 17.0,
73     17.0, 17.0, 17.0, 17.0, 17.0, 17.0, 16.0, 16.0, 15.0, 14.0,
74     13.7, 13.1, 12.7, 12.5, 12.5, 12.5, 12.5, 12.5, 12.5, 12.3,
75     12.0, 11.4, 10.6,  9.6,  8.6,  7.5,  6.6,  6.0,  5.7,  5.6,
76      5.7,  5.9,  6.2,  6.5,  6.8,  7.1,  7.3,  7.5,  7.7,  7.8,
77      7.9,  7.5,  6.4,  5.4,  2.9,  1.6, -1.0, -2.7, -3.6, -4.7,
78     -5.4, -5.2, -5.5, -5.6, -5.8, -5.9, -6.2, -6.4, -6.1, -4.7,
79     -2.7,  0.0,  2.6,  5.4,  7.7, 10.5, 13.4, 16.0, 18.2, 20.2,
80     21.2, 22.4, 23.5, 23.9, 24.3, 24.0, 23.9, 23.9, 23.7, 24.0,
81     24.3, 25.3, 26.2, 27.3, 28.2, 29.1, 30.0, 30.7, 31.4, 32.2,
82     33.1, 34.0, 35.0, 36.5, 38.3, 40.2, 42.2, 44.5, 46.5, 48.5,
83     50.5, 52.2, 53.8, 54.9, 55.8, 56.9, 58.3,
84);
85
86// ------------------------------------------------------------------------------------
87// Annual observed ΔT table 1992–2025 (IERS/USNO Bulletin A)
88// ------------------------------------------------------------------------------------
89
90/// Annual ΔT values (seconds) from IERS/USNO observations, 1992.0–2025.0.
91/// Index 0 = year 1992, index 33 = year 2025.
92/// Source: IERS Bulletin A, USNO finals2000A data.
93const OBSERVED_TERMS: usize = 34;
94const OBSERVED_START_YEAR: f64 = 1992.0;
95
96#[rustfmt::skip]
97const OBSERVED_DT: [Seconds; OBSERVED_TERMS] = qtty::qtty_vec!(
98    Seconds;
99    // 1992  1993   1994   1995   1996   1997   1998   1999
100    58.31, 59.12, 59.98, 60.78, 61.63, 62.30, 62.97, 63.47,
101    // 2000  2001   2002   2003   2004   2005   2006   2007
102    63.83, 64.09, 64.30, 64.47, 64.57, 64.69, 64.85, 65.15,
103    // 2008  2009   2010   2011   2012   2013   2014   2015
104    65.46, 65.78, 66.07, 66.32, 66.60, 66.91, 67.28, 67.64,
105    // 2016  2017   2018   2019   2020   2021   2022   2023
106    68.10, 68.59, 68.97, 69.22, 69.36, 69.36, 69.29, 69.18,
107    // 2024  2025
108    69.09, 69.36,
109);
110
111/// The year after the last observed data point. Beyond this we extrapolate.
112const OBSERVED_END_YEAR: f64 = OBSERVED_START_YEAR + OBSERVED_TERMS as f64;
113
114/// Last observed ΔT rate (seconds/year). Computed from the last 5 years of
115/// observed data. The rate has been nearly flat 2019–2025 (~+0.02 s/yr).
116const EXTRAPOLATION_RATE: f64 = 0.02;
117
118// ------------------------------------------------------------------------------------
119// ΔT Approximation Sections by Time Interval
120// ------------------------------------------------------------------------------------
121
122/// **Years < 948 CE**
123/// Quadratic formula from Stephenson & Houlden (1986).
124#[inline]
125fn delta_t_ancient(jd: JulianDate) -> Seconds {
126    const DT_A0_S: Seconds = Seconds::new(1_830.0);
127    const DT_A1_S: Seconds = Seconds::new(-405.0);
128    const DT_A2_S: Seconds = Seconds::new(46.5);
129    const JD_EPOCH_948_UT: JulianDate = JulianDate::new(2_067_314.5);
130    let c = days_ratio(jd - JD_EPOCH_948_UT, JulianDate::JULIAN_CENTURY);
131    DT_A0_S + DT_A1_S * c + DT_A2_S * c * c
132}
133
134/// **Years 948–1600 CE**
135/// Second polynomial from Stephenson & Houlden (1986).
136#[inline]
137fn delta_t_medieval(jd: JulianDate) -> Seconds {
138    const JD_EPOCH_1850_UT: JulianDate = JulianDate::new(2_396_758.5);
139    const DT_A2_S: Seconds = Seconds::new(22.5);
140
141    let c = days_ratio(jd - JD_EPOCH_1850_UT, JulianDate::JULIAN_CENTURY);
142    DT_A2_S * c * c
143}
144
145/// **Years 1600–1992**
146/// Bicubic interpolation from the biennial `DELTA_T` table.
147#[inline]
148fn delta_t_table(jd: JulianDate) -> Seconds {
149    const JD_TABLE_START_1620: JulianDate = JulianDate::new(2_312_752.5);
150    const BIENNIAL_STEP_D: Days = Days::new(730.5);
151
152    let mut i = days_ratio(jd - JD_TABLE_START_1620, BIENNIAL_STEP_D) as usize;
153    if i > TERMS - 3 {
154        i = TERMS - 3;
155    }
156    let a: Seconds = DELTA_T[i + 1] - DELTA_T[i];
157    let b: Seconds = DELTA_T[i + 2] - DELTA_T[i + 1];
158    let c: Seconds = a - b;
159    let n = days_ratio(
160        jd - (JD_TABLE_START_1620 + BIENNIAL_STEP_D * i as f64),
161        BIENNIAL_STEP_D,
162    );
163    DELTA_T[i + 1] + n / 2.0 * (a + b + n * c)
164}
165
166/// **Years 1992–2026**
167/// Linear interpolation from annual IERS/USNO observed ΔT values.
168#[inline]
169fn delta_t_observed(jd: JulianDate) -> Seconds {
170    // Convert JD to fractional year
171    let year = 2000.0 + (jd - JulianDate::J2000).value() / 365.25;
172    let idx_f = year - OBSERVED_START_YEAR;
173    let idx = idx_f as usize;
174
175    if idx + 1 >= OBSERVED_TERMS {
176        // At the very end of the table, return the last value
177        return OBSERVED_DT[OBSERVED_TERMS - 1];
178    }
179
180    // Linear interpolation between annual values
181    let frac = idx_f - idx as f64;
182    OBSERVED_DT[idx] + frac * (OBSERVED_DT[idx + 1] - OBSERVED_DT[idx])
183}
184
185/// **Years > 2026**
186/// Linear extrapolation from the last observed value at the current rate.
187///
188/// The observed ΔT trend 2019–2025 is nearly flat (~+0.02 s/yr), which is
189/// far more accurate than the Meeus quadratic that predicted ~121 s for 2020
190/// vs the observed ~69.36 s.
191#[inline]
192fn delta_t_extrapolated(jd: JulianDate) -> Seconds {
193    let year = 2000.0 + (jd - JulianDate::J2000).value() / 365.25;
194    let dt_last = OBSERVED_DT[OBSERVED_TERMS - 1];
195    let years_past = year - OBSERVED_END_YEAR;
196    dt_last + Seconds::new(EXTRAPOLATION_RATE * years_past)
197}
198
199#[inline]
200fn days_ratio(num: Days, den: Days) -> f64 {
201    (num / den).simplify().value()
202}
203
204/// JD boundary: start of year 1992.0
205const JD_1992: JulianDate = JulianDate::new(2_448_622.5);
206
207/// JD boundary: start of year 2026.0
208const JD_2026: JulianDate = JulianDate::new(2_461_041.5);
209
210/// Returns **ΔT** in seconds for a Julian Day on the **UT** axis.
211#[inline]
212pub(crate) fn delta_t_seconds_from_ut(jd_ut: JulianDate) -> Seconds {
213    match jd_ut {
214        jd if jd < JulianDate::new(2_067_314.5) => delta_t_ancient(jd),
215        jd if jd < JulianDate::new(2_305_447.5) => delta_t_medieval(jd),
216        jd if jd < JD_1992 => delta_t_table(jd),
217        jd if jd < JD_2026 => delta_t_observed(jd),
218        _ => delta_t_extrapolated(jd_ut),
219    }
220}
221
222// ── Time<UT> convenience method ───────────────────────────────────────────
223
224impl Time<UT> {
225    /// Returns **ΔT = TT − UT** in seconds for this UT epoch.
226    ///
227    /// This is a convenience accessor; the same correction is applied
228    /// automatically when converting to any TT-based scale (`.to::<JD>()`).
229    #[inline]
230    pub fn delta_t(&self) -> Seconds {
231        delta_t_seconds_from_ut(JulianDate::from_days(self.quantity()))
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238    use qtty::{Day, Days};
239
240    #[test]
241    fn delta_t_ancient_sample() {
242        let dt = delta_t_seconds_from_ut(JulianDate::new(2_000_000.0));
243        assert!((dt - Seconds::new(2_734.342_214_024_879_5)).abs() < Seconds::new(1e-6));
244    }
245
246    #[test]
247    fn delta_t_medieval_sample() {
248        let dt = delta_t_seconds_from_ut(JulianDate::new(2_100_000.0));
249        assert!((dt - Seconds::new(1_485.280_240_204_242_3)).abs() < Seconds::new(1e-6));
250    }
251
252    #[test]
253    fn delta_t_table_sample() {
254        let dt = delta_t_seconds_from_ut(JulianDate::new(2_312_752.5));
255        assert!((dt - Seconds::new(115.0)).abs() < Seconds::new(1e-6));
256    }
257
258    #[test]
259    fn delta_t_table_upper_clip() {
260        let dt = delta_t_table(JulianDate::new(2_449_356.0));
261        assert!((dt - Seconds::new(59.3)).abs() < Seconds::new(1e-6));
262    }
263
264    #[test]
265    fn delta_t_2000() {
266        // IERS observed value: 63.83 s
267        let dt = delta_t_seconds_from_ut(JulianDate::J2000);
268        assert!(
269            (dt - Seconds::new(63.83)).abs() < Seconds::new(0.1),
270            "ΔT at J2000 = {dt}, expected 63.83 s"
271        );
272    }
273
274    #[test]
275    fn delta_t_2010() {
276        // IERS observed value for 2010.0: ~66.07 s
277        // JD 2455197.5 ≈ 2010-01-01
278        let dt = delta_t_seconds_from_ut(JulianDate::new(2_455_197.5));
279        assert!(
280            (dt - Seconds::new(66.07)).abs() < Seconds::new(0.5),
281            "ΔT at 2010. = {dt}, expected ~66.07 s"
282        );
283    }
284
285    #[test]
286    fn delta_t_2020() {
287        // IERS observed value for 2020.0: ~69.36 s
288        // The old Meeus extrapolation gave ~121 s here — way off.
289        // JD for 2020-01-01 ≈ 2458849.5
290        let dt = delta_t_seconds_from_ut(JulianDate::new(2_458_849.5));
291        assert!(
292            (dt - Seconds::new(69.36)).abs() < Seconds::new(0.5),
293            "ΔT at 2020.0 = {dt}, expected ~69.36 s"
294        );
295    }
296
297    #[test]
298    fn delta_t_2025() {
299        // IERS observed value for 2025.0: ~69.36 s
300        // JD for 2025-01-01 ≈ 2460676.5
301        let dt = delta_t_seconds_from_ut(JulianDate::new(2_460_676.5));
302        assert!(
303            (dt - Seconds::new(69.36)).abs() < Seconds::new(0.5),
304            "ΔT at 2025.0 = {dt}, expected ~69.36 s"
305        );
306    }
307
308    #[test]
309    fn delta_t_extrapolated_near_future() {
310        // Beyond 2026, linear extrapolation at ~0.02 s/yr
311        // At 2030.0 (4 yr past end), ΔT ≈ 69.36 + 0.02*4 ≈ 69.44
312        let jd_2030 = JulianDate::new(2_462_502.5);
313        let dt = delta_t_seconds_from_ut(jd_2030);
314        assert!(
315            (dt - Seconds::new(69.44)).abs() < Seconds::new(1.0),
316            "ΔT at 2030. = {dt}, expected ~69.44 s"
317        );
318        // Must NOT be the old ~135+ s value
319        assert!(dt < Seconds::new(75.0), "ΔT at 2030 is too large: {dt}");
320    }
321
322    #[test]
323    fn ut_scale_applies_delta_t() {
324        let ut = Time::<UT>::new(2_451_545.0);
325        let jd_tt = ut.to::<crate::JD>();
326        let offset = jd_tt - JulianDate::new(2_451_545.0);
327        let expected = delta_t_seconds_from_ut(JulianDate::new(2_451_545.0)).to::<Day>();
328        assert!((offset - expected).abs() < Days::new(1e-9));
329    }
330
331    #[test]
332    fn ut_scale_roundtrip() {
333        let jd_tt = JulianDate::new(2_451_545.0);
334        let ut: Time<UT> = jd_tt.to::<UT>();
335        let back: JulianDate = ut.to::<crate::JD>();
336        assert!((back - jd_tt).abs() < Days::new(1e-12));
337    }
338
339    #[test]
340    fn delta_t_convenience_method() {
341        let ut = Time::<UT>::new(2_451_545.0);
342        let dt = ut.delta_t();
343        assert!((dt - Seconds::new(63.83)).abs() < Seconds::new(0.5));
344    }
345}