Skip to main content

deep_time/dt/
lunar.rs

1//! Lunar time-scale constants and conversion methods.
2
3use crate::{ATTOS_PER_SEC, Dt, Real, Scale, sin};
4
5/// TCL secular rate vs TDB (value from LTE440).
6pub const TL_NUM: i128 = 6_798_355_240;
7pub const TL_DEN: i128 = 10_000_000_000_000_000_000; // 10^19
8/// L_M = 6.48378 × 10^{-10} (secular rate from Ashby & Patla 2024 NIST for LTC ↔ TT)
9/// as fixed-point fraction.
10pub const LM_NUM: i128 = 648_378;
11pub const LM_DEN: i128 = 1_000_000_000_000_000; // 10^15
12
13/// LTE440 periodic terms (Lu et al. 2025, A&A 704, A76; arXiv:2509.18511)
14/// A_i * sin(2π * (t_J2000_days / T_i) + ϕ_i)  with A_i in µs.
15/// These are the 13 dominant terms (>1 µs) after removing the linear secular drift.
16/// Accuracy: < 0.15 ns (before 2050) when combined with the secular rate.
17#[derive(Copy, Clone)]
18pub struct LunarPeriodicTerm {
19    period_days: Real,  // T_i
20    amplitude_us: Real, // A_i
21    phase_rad: Real,    // ϕ_i
22}
23
24pub const LUNAR_PERIODIC_TERMS: [LunarPeriodicTerm; 13] = [
25    LunarPeriodicTerm {
26        period_days: 365.26590909,
27        amplitude_us: 1651.36355077,
28        phase_rad: 3.10895165,
29    },
30    LunarPeriodicTerm {
31        period_days: 29.53053800,
32        amplitude_us: 126.30813184,
33        phase_rad: 5.18472464,
34    },
35    LunarPeriodicTerm {
36        period_days: 398.99950348,
37        amplitude_us: 19.37467715,
38        phase_rad: 1.33855843,
39    },
40    LunarPeriodicTerm {
41        period_days: 182.63295455,
42        amplitude_us: 13.70088760,
43        phase_rad: 3.07602294,
44    },
45    LunarPeriodicTerm {
46        period_days: 411.67264344,
47        amplitude_us: 7.47520418,
48        phase_rad: 3.32446352,
49    },
50    LunarPeriodicTerm {
51        period_days: 4320.34946237,
52        amplitude_us: 4.24397312,
53        phase_rad: 3.43186281,
54    },
55    LunarPeriodicTerm {
56        period_days: 377.97977422,
57        amplitude_us: 3.76051430,
58        phase_rad: 0.92358639,
59    },
60    LunarPeriodicTerm {
61        period_days: 14.25402654,
62        amplitude_us: 2.93368121,
63        phase_rad: 1.09317212,
64    },
65    LunarPeriodicTerm {
66        period_days: 369.63431463,
67        amplitude_us: 2.67752983,
68        phase_rad: 1.51225314,
69    },
70    LunarPeriodicTerm {
71        period_days: 32.12797857,
72        amplitude_us: 2.36687890,
73        phase_rad: 5.21748801,
74    },
75    LunarPeriodicTerm {
76        period_days: 10859.25675676,
77        amplitude_us: 1.85820098,
78        phase_rad: 2.56843762,
79    },
80    LunarPeriodicTerm {
81        period_days: 584.00072674,
82        amplitude_us: 1.09742615,
83        phase_rad: 4.67635157,
84    },
85    LunarPeriodicTerm {
86        period_days: 292.00036337,
87        amplitude_us: 1.08850698,
88        phase_rad: 2.99248981,
89    },
90];
91
92impl Dt {
93    #[inline]
94    pub(crate) const fn mul_lm(attos: i128) -> i128 {
95        Self::mul_rate(attos, LM_NUM, LM_DEN)
96    }
97
98    pub(crate) const fn tt_to_ltc(tt: Self) -> Self {
99        let elapsed = Self::to_attos_since_tcg_tcb_epoch(tt);
100        let secular_attos = Self::mul_lm(elapsed);
101        let periodic = Self::ltc_periodic_correction(tt);
102
103        tt.add(Dt::from_attos(secular_attos, Scale::TAI))
104            .add(periodic)
105    }
106
107    /// Converts LTC → TT using fixed-point iteration to account for the
108    /// time-dependent periodic correction.
109    ///
110    /// This mirrors the strategy used in `tdb_to_tai` for consistency
111    /// and sub-attosecond numerical stability. The LTE440 periodic terms
112    /// (Lu et al. 2025) are evaluated at the current TT guess on each iteration.
113    ///
114    /// Convergence: the periodic amplitude is only ~±1.65 ms, so 6 iterations
115    /// are more than enough (error drops below 10^{-18} s after ~3–4 steps).
116    pub(crate) const fn ltc_to_tt(ltc: Self) -> Self {
117        let mut tt = ltc; // initial guess (already within ~2 ms)
118        let mut i = 0u32;
119        while i < 6 {
120            let elapsed = Self::to_attos_since_tcg_tcb_epoch(tt);
121            let secular_attos = Self::mul_rate(elapsed, LM_NUM, LM_DEN + LM_NUM);
122            let periodic = Self::ltc_periodic_correction(tt);
123
124            tt = ltc
125                .sub(Dt::from_attos(secular_attos, Scale::TAI))
126                .sub(periodic);
127            i += 1;
128        }
129        tt
130    }
131
132    #[inline]
133    pub(crate) const fn mul_tl(attos: i128) -> i128 {
134        Self::mul_rate(attos, TL_NUM, TL_DEN)
135    }
136
137    /// Returns the periodic part of (LTC − TT) in Dt (µs-level, evaluated at the TT instant).
138    const fn ltc_periodic_correction(tt: Self) -> Dt {
139        let seconds_since_j2000_tt = f!(tt.sec) + f!(tt.attos) / f!(ATTOS_PER_SEC);
140        let t_days = seconds_since_j2000_tt / f!(86400.0); // days since J2000.0 TT
141
142        let mut delta_us = f!(0.0);
143        let two_pi = f!(2.0) * f!(core::f64::consts::PI);
144
145        let mut i = 0usize;
146        while i < LUNAR_PERIODIC_TERMS.len() {
147            let term = LUNAR_PERIODIC_TERMS[i];
148            let arg = two_pi * (t_days / term.period_days) + term.phase_rad;
149            delta_us += term.amplitude_us * sin(arg);
150            i += 1;
151        }
152
153        // Convert µs → Dt (positive = lunar time runs ahead)
154        Dt::from_sec_f(delta_us * 1e-6)
155    }
156
157    /// Zero-point calibration constant for TCL so that our implementation
158    /// reproduces the official LTE440 reference value at every epoch.
159    ///
160    /// LTE440 (Lu et al. 2025) states that at J2000.0 TDB:
161    ///
162    /// ```text
163    /// published reference: TCL − TDB = +0.49330749643254945 s
164    /// ```
165    ///
166    /// At this epoch the secular term is zero, so our code produces only
167    /// the periodic contribution from the 13-term LTE440 series:
168    ///
169    /// ```text
170    /// our computed periodic sum = −0.000035111965426382064 s
171    /// ```
172    ///
173    /// The required constant bias is therefore:
174    ///
175    /// ```text
176    /// bias = published_reference − periodic_sum
177    ///      = 0.49330749643254945 − (−0.000035111965426382064)
178    ///      = +0.49334260839797583 s
179    /// ```
180    ///
181    /// This bias is a pure constant (no rate or higher-order terms) and remains
182    /// valid across the entire validity range of the LTE440 model.
183    ///
184    /// Reference: https://github.com/xlucn/LTE440
185    /// (README and demo output)
186    pub(crate) const TCL_TDB_BIAS_SPAN: Dt = Dt::from_sec_f(0.49334260839797583);
187
188    /// Integer helper: elapsed attoseconds since J2000.0 TDB.
189    /// Used exclusively for the TCL pathway to match LTE440
190    /// (TCL = TDB + L_D^M × (JD_TDB − 2451545.0) × 86400 + periodic).
191    #[inline]
192    pub(crate) const fn to_attos_since_j2000_tdb_epoch(numerical_tdb: Self) -> i128 {
193        numerical_tdb.to_attos()
194    }
195
196    pub(crate) const fn tai_to_tcl(tai: Self) -> Self {
197        let tdb = Self::tai_to_tdb(tai);
198
199        let elapsed = Self::to_attos_since_j2000_tdb_epoch(tdb);
200        let secular_attos = Self::mul_tl(elapsed);
201        let periodic = Self::ltc_periodic_correction(tdb);
202
203        tdb.add(Dt::from_attos(secular_attos, Scale::TAI))
204            .add(periodic)
205            .add(Self::TCL_TDB_BIAS_SPAN)
206    }
207
208    /// Dedicated inverse for TCL → TT.
209    /// Returns a Dt on the TT scale (consistent with ltc_to_tt, tcg_to_tt, etc.).
210    pub(crate) const fn tcl_to_tai(tcl: Self) -> Self {
211        let mut tdb = tcl;
212        let mut i = 0u32;
213        while i < 6 {
214            let elapsed = Self::to_attos_since_j2000_tdb_epoch(tdb);
215            let secular_attos = Self::mul_rate(elapsed, TL_NUM, TL_DEN + TL_NUM);
216            let periodic = Self::ltc_periodic_correction(tdb);
217
218            tdb = tcl
219                .sub(Dt::from_attos(secular_attos, Scale::TAI))
220                .sub(periodic)
221                .sub(Self::TCL_TDB_BIAS_SPAN);
222            i += 1;
223        }
224        Self::tdb_to_tai(tdb)
225    }
226}