Skip to main content

deep_time/dt/
lunar.rs

1//! Lunar time-scale constants and conversion methods.
2
3use crate::{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(always)]
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) -> Dt {
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::span(secular_attos)).add(periodic)
104    }
105
106    /// Converts from the Lunar Time Coordinate (LTC) to Terrestrial Time (TT).
107    ///
108    /// The conversion includes both a constant rate offset and periodic
109    /// corrections from lunar motion. Because the periodic terms depend on
110    /// the TT instant, a short fixed-point iteration is used. The secular
111    /// rate is inverted with the same one-step method used by the TCG and
112    /// TCB conversions.
113    pub(crate) const fn ltc_to_tt(ltc: Self) -> Dt {
114        let mut tt = ltc; // initial guess (already within ~2 ms)
115        let mut i = 0u32;
116        while i < 6 {
117            let periodic = Self::ltc_periodic_correction(tt);
118
119            // effective target after removing periodic evaluated at current guess
120            let eff = ltc.sub(periodic);
121
122            // exact one-step secular inverse on the effective value
123            // (identical to the formula used in tcg_to_tt)
124            let elapsed_eff = Self::to_attos_since_tcg_tcb_epoch(eff);
125            let sec_inv_attos = Self::mul_rate(elapsed_eff, LM_NUM, LM_DEN + LM_NUM);
126
127            tt = eff.sub(Dt::span(sec_inv_attos));
128            i += 1;
129        }
130        tt
131    }
132
133    #[inline(always)]
134    pub(crate) const fn mul_tl(attos: i128) -> i128 {
135        Self::mul_rate(attos, TL_NUM, TL_DEN)
136    }
137
138    /// Returns the periodic part of (LTC − TT) in Dt (µs-level, evaluated at the TT instant).
139    const fn ltc_periodic_correction(tt: Self) -> Dt {
140        let seconds_since_j2000_tt = tt.to_sec_f();
141        let t_days = seconds_since_j2000_tt / f!(86400.0); // days since J2000.0 TT
142
143        let mut delta_us = f!(0.0);
144        let two_pi = f!(2.0) * f!(core::f64::consts::PI);
145
146        let mut i = 0usize;
147        while i < LUNAR_PERIODIC_TERMS.len() {
148            let term = LUNAR_PERIODIC_TERMS[i];
149            let arg = two_pi * (t_days / term.period_days) + term.phase_rad;
150            delta_us += term.amplitude_us * sin(arg);
151            i += 1;
152        }
153
154        // Convert µs → Dt (positive = lunar time runs ahead)
155        Dt::from_sec_f(delta_us * 1e-6, Scale::TAI)
156    }
157
158    /// Zero-point calibration constant for TCL so that our implementation
159    /// reproduces the official LTE440 reference value at every epoch.
160    ///
161    /// LTE440 (Lu et al. 2025) states that at J2000.0 TDB:
162    ///
163    /// ```text
164    /// published reference: TCL − TDB = +0.49330749643254945 s
165    /// ```
166    ///
167    /// At this epoch the secular term is zero, so our code produces only
168    /// the periodic contribution from the 13-term LTE440 series:
169    ///
170    /// ```text
171    /// our computed periodic sum = −0.000035111965426382064 s
172    /// ```
173    ///
174    /// The required constant bias is therefore:
175    ///
176    /// ```text
177    /// bias = published_reference − periodic_sum
178    ///      = 0.49330749643254945 − (−0.000035111965426382064)
179    ///      = +0.49334260839797583 s
180    /// ```
181    ///
182    /// This bias is a pure constant (no rate or higher-order terms) and remains
183    /// valid across the entire validity range of the LTE440 model.
184    ///
185    /// Reference: <https://github.com/xlucn/LTE440>
186    /// (README and demo output)
187    pub(crate) const TCL_TDB_BIAS_SPAN: Dt = Dt::from_sec_f(0.49334260839797583, Scale::TAI);
188
189    /// Integer helper: elapsed attoseconds since J2000.0 TDB.
190    /// Used exclusively for the TCL pathway to match LTE440
191    /// (TCL = TDB + L_D^M × (JD_TDB − 2451545.0) × 86400 + periodic).
192    #[inline(always)]
193    pub(crate) const fn to_attos_since_j2000_tdb_epoch(numerical_tdb: Self) -> i128 {
194        numerical_tdb.to_attos()
195    }
196
197    pub(crate) const fn tai_to_tcl(tai: Self) -> Dt {
198        let tdb = Self::tai_to_tdb(tai);
199
200        let elapsed = Self::to_attos_since_j2000_tdb_epoch(tdb);
201        let secular_attos = Self::mul_tl(elapsed);
202        let periodic = Self::ltc_periodic_correction(tdb);
203
204        tdb.add(Dt::span(secular_attos))
205            .add(periodic)
206            .add(Self::TCL_TDB_BIAS_SPAN)
207    }
208
209    /// Converts TCL to TAI.
210    ///
211    /// The conversion goes via TDB and includes the secular rate from
212    /// LTE440 plus the periodic lunar corrections. Because the periodic
213    /// terms depend on the TDB instant, a short fixed-point iteration is
214    /// used. The secular rate is inverted with the exact one-step method.
215    pub(crate) const fn tcl_to_tai(tcl: Self) -> Dt {
216        let mut tdb = tcl;
217        let mut i = 0u32;
218        while i < 6 {
219            let periodic = Self::ltc_periodic_correction(tdb);
220
221            // effective target after removing periodic + constant bias
222            let eff = tcl.sub(periodic).sub(Self::TCL_TDB_BIAS_SPAN);
223
224            // exact one-step secular inverse on the effective value
225            let elapsed_eff = Self::to_attos_since_j2000_tdb_epoch(eff);
226            let sec_inv_attos = Self::mul_rate(elapsed_eff, TL_NUM, TL_DEN + TL_NUM);
227
228            tdb = eff.sub(Dt::span(sec_inv_attos));
229            i += 1;
230        }
231        Self::tdb_to_tai(tdb)
232    }
233}