Skip to main content

deep_time/dt/
conversions.rs

1use crate::historical_sofa::{historical_sofa_for_tai_to_utc, historical_sofa_for_utc_to_tai};
2use crate::leap_seconds::get_leap_seconds;
3use crate::{
4    ATTOS_PER_SEC, ATTOS_PER_SEC_I128, ClockDrift, ClockModel, Dt, J2000_JD_TT,
5    J2000_SEC_PER_CENTURY, LB_DEN, LB_NUM, LG_DEN, LG_NUM, LM_DEN, LM_NUM, MARS_MSD_REF_JD_INT,
6    MARS_MSD_REF_TOD_SEC, MARS_MSD_REF_TOD_SUBSEC, MARS_REF_TT, MARS_SOL_ATTOS,
7    MARS_SOL_LENGTH_SEC, Real, SEC_PER_DAYI64, SEC_PER_DAYI128, Scale, TAI_SEC_AT_1972,
8    TCG_TCB_REF_JD_INT, TCG_TCB_REF_TOD_SEC, TCG_TCB_REF_TOD_SUBSEC, TDB0_ATTOS, TSpan,
9    TT_TAI_OFFSET_SPAN, floor_f, sin_approx, to_sec_f,
10};
11
12impl Dt {
13    #[inline(always)]
14    pub const fn to_span(&self) -> TSpan {
15        TSpan {
16            sec: self.sec,
17            attos: self.attos,
18        }
19    }
20
21    pub const fn from(sec: i64, attos: u64, scale: Scale) -> Dt {
22        // Create a raw Dt with the input numbers on the requested scale
23        let raw = Dt::new(sec, attos);
24
25        match scale {
26            Scale::TAI | Scale::Custom | Scale::UT1 => raw,
27
28            Scale::TT => raw.sub(TT_TAI_OFFSET_SPAN),
29
30            Scale::UTC => raw.add(TSpan::from_sec(get_leap_seconds(&raw, true).offset)),
31
32            Scale::UTCSpice => {
33                let tai = raw.add(TSpan::from_sec(get_leap_seconds(&raw, true).offset));
34                if sec < TAI_SEC_AT_1972 - 10 {
35                    tai.add(TSpan::from_sec_f(f!(9.0)))
36                } else {
37                    tai
38                }
39            }
40            Scale::UTCSofa => {
41                let tai = raw.add(TSpan::from_sec(get_leap_seconds(&raw, true).offset));
42                if let Some(offset) = historical_sofa_for_utc_to_tai(&raw) {
43                    tai.add(TSpan::from_sec_f(offset))
44                } else {
45                    tai
46                }
47            }
48
49            Scale::GPS | Scale::QZSS | Scale::GST => raw.add(TSpan::SEC_19),
50
51            Scale::BDT => raw.add(TSpan::SEC_33),
52
53            Scale::TDB | Scale::ET => Self::tdb_to_tai(raw),
54
55            Scale::TCG => {
56                let tt = Self::tcg_to_tt(raw);
57                tt.sub(TT_TAI_OFFSET_SPAN)
58            }
59
60            Scale::TCB => {
61                let tdb = Self::tcb_to_tdb(raw);
62                Self::tdb_to_tai(tdb)
63            }
64
65            Scale::LTC => {
66                let tt = Self::ltc_to_tt(raw);
67                tt.sub(TT_TAI_OFFSET_SPAN)
68            }
69        }
70    }
71
72    /// Returns a bare [`TSpan`] containing the numerical `sec`/`attos` values
73    /// of this instant **on its own [`Scale`]** (same physical moment).
74    ///
75    /// This is the recommended way for callers to obtain the representation on
76    /// a particular scale after construction via [`Self::from`].
77    pub const fn to(&self, scale: Scale) -> TSpan {
78        match scale {
79            Scale::TAI | Scale::Custom | Scale::UT1 => self.to_span(),
80
81            Scale::TT => self.add(TT_TAI_OFFSET_SPAN).to_span(),
82
83            Scale::UTC => self
84                .sub(TSpan::from_sec(get_leap_seconds(&self, false).offset))
85                .to_span(),
86
87            Scale::UTCSpice => {
88                if self.sec < TAI_SEC_AT_1972 {
89                    self.sub(TSpan::from_sec(get_leap_seconds(&self, false).offset))
90                        .sub(TSpan::from_sec_f(f!(9.0)))
91                        .to_span()
92                } else {
93                    self.sub(TSpan::from_sec(get_leap_seconds(&self, false).offset))
94                        .to_span()
95                }
96            }
97            Scale::UTCSofa => {
98                if let Some(offset) = historical_sofa_for_tai_to_utc(&self) {
99                    self.sub(TSpan::from_sec(get_leap_seconds(&self, false).offset))
100                        .sub(TSpan::from_sec_f(offset))
101                        .to_span()
102                } else {
103                    self.sub(TSpan::from_sec(get_leap_seconds(&self, false).offset))
104                        .to_span()
105                }
106            }
107
108            Scale::GPS | Scale::QZSS | Scale::GST => self.sub(TSpan::SEC_19).to_span(),
109
110            Scale::BDT => self.sub(TSpan::SEC_33).to_span(),
111
112            Scale::TDB | Scale::ET => Self::tai_to_tdb(*self).to_span(),
113
114            Scale::TCG => Self::tai_to_tcg(*self).to_span(),
115
116            Scale::TCB => Self::tai_to_tcb(*self).to_span(),
117
118            Scale::LTC => {
119                let tt = self.add(TT_TAI_OFFSET_SPAN);
120                Self::tt_to_ltc(tt).to_span()
121            }
122        }
123    }
124
125    /// Converts this instant to any other [`Scale`] while applying an exact quadratic relativistic
126    /// or clock-drift correction defined by a [`ClockDrift`] model relative to a reference instant.
127    #[inline]
128    pub const fn convert_using_drift(self, reference: Self, drift: ClockDrift) -> Self {
129        let span = self.to_tai_since(reference);
130        let correction = drift.time_diff_after(&span);
131        self.add(correction)
132    }
133
134    /// Performs the inverse conversion of [`Self::convert_using_drift`], recovering the original proper
135    /// time on the source clock scale.
136    ///
137    /// A fixed-point iteration (at most 16 steps) is used to solve the implicit equation. For the common
138    /// case of a pure constant offset the function returns immediately without iteration.
139    pub const fn convert_back_using_drift(self, reference: Self, drift: ClockDrift) -> Self {
140        if drift.rate().is_zero() && drift.accel().is_zero() {
141            return self.sub(*drift.constant());
142        }
143        let mut guess = self;
144        let mut i = 0u32;
145        while i < 16 {
146            let span = guess.to_tai_since(reference);
147            let correction = drift.time_diff_after(&span);
148            guess = self.sub(correction);
149            i += 1;
150        }
151        guess
152    }
153
154    /// Converts this instant using a self-describing [`ClockModel`].
155    ///
156    /// This is the recommended high-level API for onboard or custom time scales (Proper, Custom,
157    /// or any model with a defined base and drift).
158    #[inline]
159    pub const fn convert_using_model(self, model: ClockModel) -> Self {
160        self.convert_using_drift(model.reference, model.drift)
161    }
162
163    /// Performs the inverse conversion of [`Self::convert_using_model`].
164    #[inline]
165    pub const fn convert_back_using_model(self, model: ClockModel) -> Self {
166        self.convert_back_using_drift(model.reference, model.drift)
167    }
168
169    /// Computes the difference TDB − TT (in seconds) using the four dominant
170    /// periodic terms from the Fairhead & Bretagnon (1990) analytical series,
171    /// as extracted from the SOFA/ERFA library (`eraDtdb`).
172    ///
173    /// This is currently the most accurate practical analytical model for the
174    /// periodic part of TDB−TT. It captures approximately 99.85% of the total
175    /// power present in the full 787-term Fairhead-Bretagnon series while
176    /// remaining extremely fast and fully `const fn` compatible.
177    ///
178    /// The model includes:
179    /// - Main annual term (Earth's orbital eccentricity)
180    /// - Semi-annual harmonic
181    /// - 11.86-year perturbation term (lunar/planetary)
182    /// - Venus perturbation term
183    ///
184    /// **Accuracy**: better than ±0.5 µs near J2000.0 for the periodic component
185    /// (this 4-term model captures the dominant variation), with slow degradation
186    /// over millennia. For nanosecond-level work over long timescales, numerical
187    /// integration against a modern solar-system ephemeris (DE440/DE441, INPOP,
188    /// etc.) remains the definitive method.
189    ///
190    /// References (all directly from the SOFA/ERFA implementation):
191    ///
192    /// - Fairhead, L. & Bretagnon, P., "An analytical formula for the time
193    ///   transformation TB-TT", Astron. Astrophys. 229, 240-247 (1990).
194    ///
195    /// - IAU 2006 Resolution 3 (re-definition of Barycentric Dynamical Time).
196    ///
197    /// - McCarthy, D. D. & Petit, G. (eds.), IERS Conventions (2003),
198    ///   IERS Technical Note No. 32, BKG (2004).
199    ///
200    /// - Moyer, T. D., "Transformation from proper time on Earth to coordinate
201    ///   time in solar system barycentric space", Cel. Mech. 23, 33 (1981).
202    ///
203    /// - Murray, C. A., Vectorial Astrometry, Adam Hilger (1983).
204    ///
205    /// - Seidelmann, P. K. et al., Explanatory Supplement to the Astronomical
206    ///   Almanac, Chapter 2, University Science Books (1992).
207    ///
208    /// - Simon, J. L., Bretagnon, P., Chapront, J., Chapront-Touze, M.,
209    ///   Francou, G. & Laskar, J., "Numerical expressions for precession
210    ///   formulae and mean elements for the Moon and planets",
211    ///   Astron. Astrophys. 282, 663-683 (1994).
212    ///
213    /// - SOFA/ERFA `eraDtdb` implementation (2021 May 11 revision):
214    ///   https://raw.githubusercontent.com/liberfa/erfa/master/src/dtdb.c
215    const fn tdb_minus_tt(sec: i64, attos: u64) -> TSpan {
216        let seconds_since_j2000_tt = f!(sec) + f!(attos) / f!(ATTOS_PER_SEC);
217        let t = seconds_since_j2000_tt / J2000_SEC_PER_CENTURY;
218
219        // Mean anomaly of Earth (from Fairhead & Bretagnon 1990 / Simon et al. 1994)
220        let g =
221            f!(2.0) * core::f64::consts::PI * (f!(357.52910918) + f!(35999.050290) * t) / f!(360.0);
222
223        // Main annual term with first-order eccentricity correction
224        let sin_g = sin_approx(g + f!(0.01671) * sin_approx(g));
225
226        // Semi-annual harmonic
227        let sin_2g = sin_approx(f!(2.0) * g);
228
229        // Lunar monthly term (27.3 days) — amplitude 4.770086 µs
230        let lunar = sin_approx(f!(52.9690965) * t + f!(0.444401603));
231
232        // Venus perturbation — amplitude 4.676740 µs
233        let venus = sin_approx(f!(606.977675) * t + f!(4.021195093));
234
235        let correction = f!(0.001656674564) * sin_g
236            + f!(0.000022417471) * sin_2g
237            + f!(0.000004770086) * lunar
238            + f!(0.000004676740) * venus;
239
240        TSpan::from_sec_f(correction)
241    }
242
243    const fn tai_to_tdb(tai: Self) -> Self {
244        let tt = tai.add(TT_TAI_OFFSET_SPAN);
245        let span = Self::tdb_minus_tt(tt.sec, tt.attos);
246        tt.add(span)
247    }
248
249    const fn tdb_to_tai(tdb: Self) -> Self {
250        let mut tt = tdb;
251        let mut i = 0u32;
252        while i < 8 {
253            tt = tdb.sub(Self::tdb_minus_tt(tt.sec, tt.attos));
254            i += 1;
255        }
256        tt.sub(TT_TAI_OFFSET_SPAN)
257    }
258
259    const fn tai_to_tcg(tai: Self) -> Self {
260        let tt = tai.add(TT_TAI_OFFSET_SPAN);
261        Self::tt_to_tcg(tt)
262    }
263
264    const fn tai_to_tcb(tai: Self) -> Self {
265        let tdb = Self::tai_to_tdb(tai);
266        Self::tdb_to_tcb(tdb)
267    }
268
269    /// Exact integer helper: elapsed attoseconds since the TCG/TCB reference epoch (1977-01-01.0 TAI),
270    /// using only the numerical `sec`/`attos` of the supplied `Dt` (scale is ignored).
271    const fn elapsed_to_attos_since_ref(numerical: Self) -> i128 {
272        let days_since_j2000 = numerical.sec.div_euclid(SEC_PER_DAYI64);
273        let tod_sec = numerical.sec.rem_euclid(SEC_PER_DAYI64);
274
275        let jd_days = J2000_JD_TT + days_since_j2000;
276        let days_diff = jd_days - TCG_TCB_REF_JD_INT;
277
278        let mut sec_diff =
279            (days_diff as i128) * SEC_PER_DAYI128 + (tod_sec as i128 - TCG_TCB_REF_TOD_SEC as i128);
280        let mut attos_diff = (numerical.attos as i128) - (TCG_TCB_REF_TOD_SUBSEC as i128);
281
282        if attos_diff < 0 {
283            attos_diff += ATTOS_PER_SEC_I128;
284            sec_diff -= 1;
285        }
286
287        sec_diff * ATTOS_PER_SEC_I128 + attos_diff
288    }
289
290    /// Exact fixed-point multiplication: `attos * num / den` (handles negative values safely, no overflow for library time range).
291    const fn mul_rate(attos: i128, num: i128, den: i128) -> i128 {
292        if attos == 0 {
293            return 0;
294        }
295        let sign = if attos < 0 { -1i128 } else { 1i128 };
296        let a = if attos < 0 { -attos } else { attos };
297        let q = a / den;
298        let r = a % den;
299        sign * (q * num + (r * num) / den)
300    }
301
302    #[inline]
303    const fn mul_lg(attos: i128) -> i128 {
304        Self::mul_rate(attos, LG_NUM, LG_DEN)
305    }
306
307    #[inline]
308    const fn mul_lb(attos: i128) -> i128 {
309        Self::mul_rate(attos, LB_NUM, LB_DEN)
310    }
311
312    #[inline]
313    const fn mul_lm(attos: i128) -> i128 {
314        Self::mul_rate(attos, LM_NUM, LM_DEN)
315    }
316
317    const fn tt_to_tcg(tt: Self) -> Self {
318        let elapsed = Self::elapsed_to_attos_since_ref(tt);
319        let span_attos = Self::mul_lg(elapsed);
320        tt.add(TSpan::from_attos(span_attos))
321    }
322
323    const fn tcg_to_tt(tcg: Self) -> Self {
324        let elapsed_cg = Self::elapsed_to_attos_since_ref(tcg);
325        let span_attos = Self::mul_rate(elapsed_cg, LG_NUM, LG_DEN + LG_NUM);
326        tcg.sub(TSpan::from_attos(span_attos))
327    }
328
329    const fn tcb_to_tdb(tcb: Self) -> Self {
330        let elapsed_cg = Self::elapsed_to_attos_since_ref(tcb);
331        let span_attos = Self::mul_rate(elapsed_cg, LB_NUM, LB_DEN + LB_NUM);
332        tcb.sub(TSpan::from_attos(span_attos))
333            .sub(TSpan::from_attos(TDB0_ATTOS))
334    }
335
336    const fn tdb_to_tcb(tdb: Self) -> Self {
337        let elapsed = Self::elapsed_to_attos_since_ref(tdb);
338        let span_attos = Self::mul_lb(elapsed);
339        tdb.add(TSpan::from_attos(span_attos))
340            .add(TSpan::from_attos(TDB0_ATTOS))
341    }
342
343    const fn tt_to_ltc(tt: Self) -> Self {
344        let elapsed = Self::elapsed_to_attos_since_ref(tt);
345        let span_attos = Self::mul_lm(elapsed);
346        tt.add(TSpan::from_attos(span_attos))
347    }
348
349    const fn ltc_to_tt(ltc: Self) -> Self {
350        let elapsed = Self::elapsed_to_attos_since_ref(ltc);
351        let span_attos = Self::mul_rate(elapsed, LM_NUM, LM_DEN + LM_NUM);
352        ltc.sub(TSpan::from_attos(span_attos))
353    }
354
355    /// Exact helper: elapsed attoseconds since the Mars MSD reference epoch (JD 2405522.0028779 TT).
356    const fn elapsed_to_attos_since_mars_ref(numerical_tt: TSpan) -> i128 {
357        let days_since_j2000 = numerical_tt.sec.div_euclid(SEC_PER_DAYI64);
358        let tod_sec = numerical_tt.sec.rem_euclid(SEC_PER_DAYI64);
359
360        let jd_days = J2000_JD_TT + days_since_j2000;
361        let days_diff = jd_days - MARS_MSD_REF_JD_INT;
362
363        let mut sec_diff = (days_diff as i128) * SEC_PER_DAYI128
364            + (tod_sec as i128 - MARS_MSD_REF_TOD_SEC as i128);
365        let mut attos_diff = (numerical_tt.attos as i128) - (MARS_MSD_REF_TOD_SUBSEC as i128);
366
367        if attos_diff < 0 {
368            attos_diff += ATTOS_PER_SEC_I128;
369            sec_diff -= 1;
370        }
371
372        sec_diff * ATTOS_PER_SEC_I128 + attos_diff
373    }
374
375    /// Returns the exact Mars Sol Date (MSD) as a tuple of integer sols and the fractional part of a sol.
376    ///
377    /// The computation follows the canonical NASA GISS / AM2000 formulation and works for any input
378    /// [`Scale`]. Leap seconds are automatically accounted for when converting from UTC.
379    pub const fn to_msd_exact(self) -> (i64, u128) {
380        let tt = self.to(Scale::TT);
381        let elapsed = Self::elapsed_to_attos_since_mars_ref(tt);
382        let attos_per_sol = MARS_SOL_ATTOS;
383
384        let whole_sols = elapsed.div_euclid(attos_per_sol) as i64;
385        let frac_attos = elapsed.rem_euclid(attos_per_sol) as u128;
386
387        (whole_sols, frac_attos)
388    }
389
390    /// Returns Mars Coordinated Time (MTC) as a [`TSpan`] representing
391    /// seconds into the current sol (range `[0, one Martian sol)`).
392    #[inline]
393    pub const fn to_mtc(self) -> TSpan {
394        let (_, frac_attos) = self.to_msd_exact();
395        TSpan::from_attos(frac_attos as i128)
396    }
397
398    /// Creates a `Dt` (in TT) from an exact Mars Sol Date using full library precision.
399    pub const fn from_msd_exact(whole_sols: i64, frac_attos: u128) -> Self {
400        let elapsed_attos = (whole_sols as i128) * MARS_SOL_ATTOS + frac_attos as i128;
401
402        let tt = MARS_REF_TT.add(TSpan::from_attos(elapsed_attos));
403        Self::from(tt.sec, tt.attos, Scale::TT)
404    }
405
406    /// Creates a `Dt` (in TT) from a floating-point Mars Sol Date.
407    /// Non-exact Real.
408    pub const fn from_msd(msd: Real) -> Self {
409        let whole = floor_f(msd) as i64;
410        let frac = msd - f!(whole);
411        let frac_span = TSpan::from_sec_f(frac * MARS_SOL_LENGTH_SEC);
412        Self::from_msd_exact(whole, frac_span.to_attos() as u128)
413    }
414
415    /// Returns the Mars Sol Date (MSD) as a floating-point value (matches NASA Mars24 output).
416    /// Non-exact Real.
417    #[inline]
418    pub const fn to_msd(self) -> Real {
419        let (whole, frac) = self.to_msd_exact();
420        f!(whole) + to_sec_f(frac) / MARS_SOL_LENGTH_SEC
421    }
422}