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