Skip to main content

lox_time/
offsets.rs

1// SPDX-FileCopyrightText: 2024 Helge Eichhorn <git@helgeeichhorn.de>
2//
3// SPDX-License-Identifier: MPL-2.0
4
5use std::convert::Infallible;
6
7use crate::Time;
8use crate::deltas::TimeDelta;
9use crate::time_scales::{Tai, TimeScale};
10use crate::utc::Utc;
11use crate::utc::leap_seconds::{DefaultLeapSecondsProvider, LeapSecondsProvider};
12
13mod impls;
14
15/// Fallible time scale offset computation.
16pub trait TryOffset<Origin, Target>
17where
18    Origin: TimeScale,
19    Target: TimeScale,
20{
21    /// The error type returned when the offset cannot be computed.
22    type Error: std::error::Error + Send + Sync + 'static;
23
24    /// Computes the offset from `origin` to `target` at the given `delta` since J2000.
25    fn try_offset(
26        &self,
27        origin: Origin,
28        target: Target,
29        delta: TimeDelta,
30    ) -> Result<TimeDelta, Self::Error>;
31}
32
33/// Infallible time scale offset computation.
34pub trait Offset<Origin, Target>
35where
36    Origin: TimeScale,
37    Target: TimeScale,
38{
39    /// Computes the offset from `origin` to `target` at the given `delta` since J2000.
40    fn offset(&self, origin: Origin, target: Target, delta: TimeDelta) -> TimeDelta;
41}
42
43impl<T, Origin, Target> Offset<Origin, Target> for T
44where
45    Origin: TimeScale,
46    Target: TimeScale,
47    T: TryOffset<Origin, Target, Error = Infallible>,
48{
49    fn offset(&self, origin: Origin, target: Target, delta: TimeDelta) -> TimeDelta {
50        self.try_offset(origin, target, delta).unwrap()
51    }
52}
53
54/// Provides time scale offset computations for all supported scale pairs.
55pub trait OffsetProvider {
56    /// The error type for fallible offset computations (e.g. UT1).
57    type Error: std::error::Error + Send + Sync + 'static;
58
59    /// Returns the TAI→UT1 offset at the given delta.
60    fn tai_to_ut1(&self, delta: TimeDelta) -> Result<TimeDelta, Self::Error>;
61    /// Returns the UT1→TAI offset at the given delta.
62    fn ut1_to_tai(&self, delta: TimeDelta) -> Result<TimeDelta, Self::Error>;
63
64    /// Returns the constant TAI→TT offset.
65    fn tai_to_tt(&self) -> TimeDelta {
66        D_TAI_TT
67    }
68
69    /// Returns the constant TT→TAI offset.
70    fn tt_to_tai(&self) -> TimeDelta {
71        -D_TAI_TT
72    }
73
74    /// Returns the constant TAI→GPS offset.
75    fn tai_to_gps(&self) -> TimeDelta {
76        D_TAI_GPS
77    }
78
79    /// Returns the constant GPS→TAI offset.
80    fn gps_to_tai(&self) -> TimeDelta {
81        -D_TAI_GPS
82    }
83
84    /// Returns the TT→TCG offset at the given delta.
85    fn tt_to_tcg(&self, delta: TimeDelta) -> TimeDelta {
86        tt_to_tcg(delta)
87    }
88
89    /// Returns the TCG→TT offset at the given delta.
90    fn tcg_to_tt(&self, delta: TimeDelta) -> TimeDelta {
91        tcg_to_tt(delta)
92    }
93
94    /// Returns the TDB→TCB offset at the given delta.
95    fn tdb_to_tcb(&self, delta: TimeDelta) -> TimeDelta {
96        tdb_to_tcb(delta)
97    }
98
99    /// Returns the TCB→TDB offset at the given delta.
100    fn tcb_to_tdb(&self, delta: TimeDelta) -> TimeDelta {
101        tcb_to_tdb(delta)
102    }
103
104    /// Returns the TT→TDB offset at the given delta.
105    fn tt_to_tdb(&self, delta: TimeDelta) -> TimeDelta {
106        tt_to_tdb(delta)
107    }
108
109    /// Returns the TDB→TT offset at the given delta.
110    fn tdb_to_tt(&self, delta: TimeDelta) -> TimeDelta {
111        tdb_to_tt(delta)
112    }
113}
114
115/// Default offset provider using built-in leap second tables and standard algorithms.
116#[derive(Debug, Clone, Copy, Default)]
117pub struct DefaultOffsetProvider;
118
119impl LeapSecondsProvider for DefaultOffsetProvider {}
120
121impl OffsetProvider for DefaultOffsetProvider {
122    type Error = Infallible;
123
124    fn tai_to_ut1(&self, delta: TimeDelta) -> Result<TimeDelta, Self::Error> {
125        let Some(_) = delta.seconds() else {
126            return Ok(TimeDelta::ZERO);
127        };
128        let tai = Time::from_delta(Tai, delta);
129        Ok(DefaultLeapSecondsProvider.delta_tai_utc(tai))
130    }
131
132    fn ut1_to_tai(&self, delta: TimeDelta) -> Result<TimeDelta, Self::Error> {
133        let Ok(utc) = Utc::from_delta(delta) else {
134            return Ok(TimeDelta::ZERO);
135        };
136        Ok(DefaultLeapSecondsProvider.delta_utc_tai(utc))
137    }
138}
139
140// TAI <-> TT
141
142/// The constant offset between TAI and TT.
143pub const D_TAI_TT: TimeDelta = TimeDelta::builder().seconds(32).milliseconds(184).build();
144
145// TAI <-> GPS
146
147/// Constant offset from TAI to GPS time: `GPS = TAI − 19s`.
148///
149/// This is the value added when converting TAI to GPS (negative 19
150/// seconds, since GPS-time labels are 19 seconds less than the
151/// corresponding TAI-time labels).
152pub const D_TAI_GPS: TimeDelta = TimeDelta::builder().seconds(-19).build();
153
154// TT <-> TCG
155
156/// The difference between J2000 TT and 1977 January 1.0 TAI as TT.
157const J77_TT: TimeDelta = TimeDelta::builder()
158    .seconds(-725803167)
159    .milliseconds(816)
160    .build();
161
162/// The rate of change of TCG with respect to TT.
163const LG: f64 = 6.969290134e-10;
164
165/// The rate of change of TT with respect to TCG.
166const INV_LG: f64 = LG / (1.0 - LG);
167
168fn tt_to_tcg(delta: TimeDelta) -> TimeDelta {
169    INV_LG * (delta - J77_TT)
170}
171
172fn tcg_to_tt(delta: TimeDelta) -> TimeDelta {
173    -LG * (delta - J77_TT)
174}
175
176// TDB <-> TCB
177
178/// The rate of change of TDB with respect to TCB.
179const LB: f64 = 1.550519768e-8;
180
181/// The rate of change of TCB with respect to TDB.
182const INV_LB: f64 = LB / (1.0 - LB);
183
184/// Scale factor for TDB to TCB constant term: 1 / (1 - LB).
185const ONE_MINUS_LB_INV: f64 = 1.0 / (1.0 - LB);
186
187/// Constant term of TDB − TT formula of Fairhead & Bretagnon (1990).
188// const TDB_0: f64 = -6.55e-5;
189const TDB_0: TimeDelta = TimeDelta::builder()
190    .seconds(0)
191    .microseconds(65)
192    .nanoseconds(500)
193    .negative()
194    .build();
195
196const TCB_77: TimeDelta = TDB_0.add_const(J77_TT.mul_const(LB));
197
198fn tdb_to_tcb(delta: TimeDelta) -> TimeDelta {
199    INV_LB * delta - ONE_MINUS_LB_INV * TCB_77
200}
201
202fn tcb_to_tdb(delta: TimeDelta) -> TimeDelta {
203    TCB_77 - LB * delta
204}
205
206// TT <-> TDB
207
208const K: f64 = 1.657e-3;
209const EB: f64 = 1.671e-2;
210const M_0: f64 = 6.239996;
211const M_1: f64 = 1.99096871e-7;
212
213fn tt_to_tdb(delta: TimeDelta) -> TimeDelta {
214    let tt = delta.to_seconds().to_f64();
215    let g = M_0 + M_1 * tt;
216    TimeDelta::from_seconds_f64(K * (g + EB * g.sin()).sin())
217}
218
219fn tdb_to_tt(delta: TimeDelta) -> TimeDelta {
220    let tdb = delta.to_seconds().to_f64();
221    let mut offset = 0.0;
222    for _ in 1..3 {
223        let g = M_0 + M_1 * (tdb + offset);
224        offset = -K * (g + EB * g.sin()).sin();
225    }
226    TimeDelta::from_seconds_f64(offset)
227}
228
229// Two-step transformations
230
231fn two_step_offset<P, T1, T2, T3>(
232    provider: &P,
233    origin: T1,
234    via: T2,
235    target: T3,
236    delta: TimeDelta,
237) -> TimeDelta
238where
239    T1: TimeScale,
240    T2: TimeScale + Copy,
241    T3: TimeScale,
242    P: Offset<T1, T2> + Offset<T2, T3>,
243{
244    let mut offset = provider.offset(origin, via, delta);
245    offset += provider.offset(via, target, delta + offset);
246    offset
247}
248
249#[cfg(test)]
250mod tests {
251    use lox_test_utils::assert_approx_eq;
252    use rstest::rstest;
253
254    use super::*;
255    use crate::offsets::TryOffset;
256    use crate::time_scales::DynTimeScale;
257    use crate::{DynTime, calendar_dates::Date, deltas::ToDelta, time_of_day::TimeOfDay};
258
259    const DEFAULT_TOL: f64 = 1e-7;
260    const TCB_TOL: f64 = 1e-4;
261
262    // Reference values from Orekit
263    //
264    // Since we use different algorithms for TCB and UT1 we need to
265    // adjust the tolerances accordingly.
266    //
267    #[rstest]
268    #[case::tai_tai("TAI", "TAI", 0.0, None)]
269    #[case::tai_tcb("TAI", "TCB", 55.66851419888016, Some(TCB_TOL))]
270    #[case::tai_tcg("TAI", "TCG", 33.239589335894145, None)]
271    #[case::tai_tdb("TAI", "TDB", 32.183882324981056, None)]
272    #[case::tai_tt("TAI", "TT", 32.184, None)]
273    #[case::tcb_tai("TCB", "TAI", -55.668513317090046, Some(TCB_TOL))]
274    #[case::tcb_tcb("TCB", "TCB", 0.0, Some(TCB_TOL))]
275    #[case::tcb_tcg("TCB", "TCG", -22.4289240199929, Some(TCB_TOL))]
276    #[case::tcb_tdb("TCB", "TDB", -23.484631010747805, Some(TCB_TOL))]
277    #[case::tcb_tt("TCB", "TT", -23.484513317090048, Some(TCB_TOL))]
278    #[case::tcg_tai("TCG", "TAI", -33.23958931272851, None)]
279    #[case::tcg_tcb("TCG", "TCB", 22.428924359636042, Some(TCB_TOL))]
280    #[case::tcg_tcg("TCG", "TCG", 0.0, None)]
281    #[case::tcg_tdb("TCG", "TDB", -1.0557069988766656, None)]
282    #[case::tcg_tt("TCG", "TT", -1.0555893127285145, None)]
283    #[case::tdb_tai("TDB", "TAI", -32.18388231420531, None)]
284    #[case::tdb_tcb("TDB", "TCB", 23.48463137488165, Some(TCB_TOL))]
285    #[case::tdb_tcg("TDB", "TCG", 1.0557069992589518, None)]
286    #[case::tdb_tdb("TDB", "TDB", 0.0, None)]
287    #[case::tdb_tt("TDB", "TT", 1.176857946845189E-4, None)]
288    #[case::tt_tai("TT", "TAI", -32.184, None)]
289    #[case::tt_tcb("TT", "TCB", 23.484513689085105, Some(TCB_TOL))]
290    #[case::tt_tcg("TT", "TCG", 1.055589313464182, None)]
291    #[case::tt_tdb("TT", "TDB", -1.1768579472004603E-4, None)]
292    #[case::tt_tt("TT", "TT", 0.0, None)]
293    fn test_dyn_time_scale_offsets_new(
294        #[case] scale1: &str,
295        #[case] scale2: &str,
296        #[case] exp: f64,
297        #[case] tol: Option<f64>,
298    ) {
299        let provider = &DefaultOffsetProvider;
300        let scale1: DynTimeScale = scale1.parse().unwrap();
301        let scale2: DynTimeScale = scale2.parse().unwrap();
302        let date = Date::new(2024, 12, 30).unwrap();
303        let time = TimeOfDay::from_hms(10, 27, 13.145).unwrap();
304        let dt = DynTime::from_date_and_time(scale1, date, time)
305            .unwrap()
306            .to_delta();
307        let act = provider
308            .try_offset(scale1, scale2, dt)
309            .unwrap()
310            .to_seconds()
311            .to_f64();
312        assert_approx_eq!(act, exp, atol <= tol.unwrap_or(DEFAULT_TOL));
313    }
314
315    // Test round-trip conversions for reversibility
316    #[rstest]
317    #[case::tt_tcg_tt("TT", "TCG", 1e-15)]
318    #[case::tcg_tt_tcg("TCG", "TT", 1e-15)]
319    #[case::tdb_tcb_tdb("TDB", "TCB", 1e-14)]
320    #[case::tcb_tdb_tcb("TCB", "TDB", 1e-14)]
321    fn test_time_scale_roundtrip(#[case] scale1: &str, #[case] scale2: &str, #[case] tol: f64) {
322        let provider = &DefaultOffsetProvider;
323        let scale1: DynTimeScale = scale1.parse().unwrap();
324        let scale2: DynTimeScale = scale2.parse().unwrap();
325        let date = Date::new(2024, 12, 30).unwrap();
326        let time = TimeOfDay::from_hms(10, 27, 13.145).unwrap();
327        let original_delta = DynTime::from_date_and_time(scale1, date, time)
328            .unwrap()
329            .to_delta();
330
331        // Forward conversion
332        let offset1 = provider.try_offset(scale1, scale2, original_delta).unwrap();
333        let intermediate_delta = original_delta + offset1;
334
335        // Reverse conversion
336        let offset2 = provider
337            .try_offset(scale2, scale1, intermediate_delta)
338            .unwrap();
339        let final_delta = intermediate_delta + offset2;
340
341        let diff = (final_delta - original_delta).to_seconds().to_f64().abs();
342        assert!(
343            diff < tol,
344            "Round-trip conversion {} -> {} -> {} failed: difference = {:.2e} seconds, tolerance = {:.2e} seconds",
345            scale1,
346            scale2,
347            scale1,
348            diff,
349            tol
350        );
351    }
352
353    #[test]
354    fn test_offset_constants() {
355        let tdb_0 = TDB_0.to_seconds();
356        assert!((tdb_0.to_f64() - (-6.55e-5)).abs() < 1e-15);
357
358        let j77 = J77_TT.to_seconds();
359        // For negative times, internal representation stores one less second
360        // and a positive subsecond fraction: -725803167.816 = -725803168 + 0.184
361        assert_eq!(j77.hi, -725803168.0);
362        assert!((j77.lo - 0.184).abs() < 1e-15);
363        // But the total should be correct
364        assert!((j77.to_f64() - (-725803167.816)).abs() < 1e-9);
365    }
366}