Skip to main content

celestial_time/scales/conversions/
tai_tt.rs

1//! Conversions between TAI, TT, and TCG time scales.
2//!
3//! This module implements the fixed-offset and linear-rate conversions between:
4//!
5//! - **TAI (International Atomic Time)**: The reference atomic time scale.
6//! - **TT (Terrestrial Time)**: Idealized time on the geoid. TT = TAI + 32.184s exactly.
7//! - **TCG (Geocentric Coordinate Time)**: Coordinate time at the geocenter.
8//!
9//! # Conversion Relationships
10//!
11//! ```text
12//! TAI <-> TT     Fixed offset: TT = TAI + 32.184 seconds
13//! TAI <-> TCG   Chains through TT: TAI → TT → TCG
14//! ```
15//!
16//! The TAI-TT offset is defined by the IAU to be exactly 32.184 seconds. This offset
17//! accounts for the historical difference between atomic time and ephemeris time.
18//!
19//! # Precision Preservation
20//!
21//! All conversions add offsets to the smaller-magnitude Julian Date component to
22//! preserve full f64 precision. Round-trip conversions (TAI → TT → TAI) are exact
23//! to the bit level.
24//!
25//! # Usage
26//!
27//! ```
28//! use celestial_time::{JulianDate, TAI, TT};
29//! use celestial_time::scales::conversions::{ToTT, ToTAI};
30//! use celestial_core::constants::J2000_JD;
31//!
32//! let tai = TAI::from_julian_date(JulianDate::new(J2000_JD, 0.0));
33//! let tt = tai.to_tt().unwrap();
34//!
35//! // TT is 32.184 seconds ahead of TAI
36//! let tai_jd = tai.to_julian_date();
37//! let tt_jd = tt.to_julian_date();
38//! let diff_days = (tt_jd.jd1() - tai_jd.jd1()) + (tt_jd.jd2() - tai_jd.jd2());
39//! let diff_seconds = diff_days * 86400.0;
40//! assert_eq!(diff_seconds, 32.184);
41//!
42//! // Round-trip is exact
43//! let back_to_tai = tt.to_tai().unwrap();
44//! assert_eq!(tai.to_julian_date().jd1(), back_to_tai.to_julian_date().jd1());
45//! assert_eq!(tai.to_julian_date().jd2(), back_to_tai.to_julian_date().jd2());
46//! ```
47
48use super::{ToTAI, ToTCG, ToTT};
49use crate::constants::TT_TAI_OFFSET;
50use crate::scales::{TAI, TCG, TT};
51use crate::TimeResult;
52use celestial_core::constants::SECONDS_PER_DAY_F64;
53
54/// Identity conversion for TAI.
55impl ToTAI for TAI {
56    fn to_tai(&self) -> TimeResult<TAI> {
57        Ok(*self)
58    }
59}
60
61/// Identity conversion for TT.
62impl ToTT for TT {
63    fn to_tt(&self) -> TimeResult<TT> {
64        Ok(*self)
65    }
66}
67
68/// Convert TAI to TT by adding the fixed 32.184 second offset.
69///
70/// The offset is added to whichever Julian Date component has smaller magnitude
71/// to preserve maximum precision in the two-part representation.
72impl ToTT for TAI {
73    fn to_tt(&self) -> TimeResult<TT> {
74        let tai_jd = self.to_julian_date();
75        let dtat = TT_TAI_OFFSET / SECONDS_PER_DAY_F64;
76
77        let jd1_raw = tai_jd.jd1().to_bits();
78        let jd2_raw = tai_jd.jd2().to_bits();
79        let jd1_magnitude = jd1_raw & 0x7FFFFFFFFFFFFFFF;
80        let jd2_magnitude = jd2_raw & 0x7FFFFFFFFFFFFFFF;
81        let (tt_jd1, tt_jd2) = if jd1_magnitude > jd2_magnitude {
82            (tai_jd.jd1(), tai_jd.jd2() + dtat)
83        } else {
84            (tai_jd.jd1() + dtat, tai_jd.jd2())
85        };
86
87        Ok(TT::from_julian_date_raw(tt_jd1, tt_jd2))
88    }
89}
90
91/// Convert TT to TAI by subtracting the fixed 32.184 second offset.
92///
93/// The offset is subtracted from whichever Julian Date component has smaller magnitude
94/// to preserve maximum precision in the two-part representation.
95impl ToTAI for TT {
96    fn to_tai(&self) -> TimeResult<TAI> {
97        let tt_jd = self.to_julian_date();
98        let dtat = TT_TAI_OFFSET / SECONDS_PER_DAY_F64;
99
100        let jd1_raw = tt_jd.jd1().to_bits();
101        let jd2_raw = tt_jd.jd2().to_bits();
102        let jd1_magnitude = jd1_raw & 0x7FFFFFFFFFFFFFFF;
103        let jd2_magnitude = jd2_raw & 0x7FFFFFFFFFFFFFFF;
104        let (tai_jd1, tai_jd2) = if jd1_magnitude > jd2_magnitude {
105            (tt_jd.jd1(), tt_jd.jd2() - dtat)
106        } else {
107            (tt_jd.jd1() - dtat, tt_jd.jd2())
108        };
109
110        Ok(TAI::from_julian_date_raw(tai_jd1, tai_jd2))
111    }
112}
113
114/// Convert TAI to TCG by chaining through TT.
115///
116/// TAI has no direct conversion to TCG. This chains: TAI → TT → TCG.
117impl ToTCG for TAI {
118    fn to_tcg(&self) -> TimeResult<TCG> {
119        self.to_tt()?.to_tcg()
120    }
121}
122
123/// Convert TCG to TAI by chaining through TT.
124///
125/// TCG has no direct conversion to TAI. This chains: TCG → TT → TAI.
126impl ToTAI for TCG {
127    fn to_tai(&self) -> TimeResult<TAI> {
128        self.to_tt()?.to_tai()
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use crate::JulianDate;
136    use celestial_core::constants::J2000_JD;
137
138    #[test]
139    fn test_identity_conversions() {
140        let tai = TAI::from_julian_date(JulianDate::new(J2000_JD, 0.999999999999999));
141        let identity_tai = tai.to_tai().unwrap();
142
143        assert_eq!(
144            tai.to_julian_date().jd1(),
145            identity_tai.to_julian_date().jd1(),
146            "TAI identity conversion should preserve JD1"
147        );
148        assert_eq!(
149            tai.to_julian_date().jd2(),
150            identity_tai.to_julian_date().jd2(),
151            "TAI identity conversion should preserve JD2"
152        );
153
154        let tt = TT::from_julian_date(JulianDate::new(J2000_JD, 0.999999999999999));
155        let identity_tt = tt.to_tt().unwrap();
156
157        assert_eq!(
158            tt.to_julian_date().jd1(),
159            identity_tt.to_julian_date().jd1(),
160            "TT identity conversion should preserve JD1"
161        );
162        assert_eq!(
163            tt.to_julian_date().jd2(),
164            identity_tt.to_julian_date().jd2(),
165            "TT identity conversion should preserve JD2"
166        );
167    }
168
169    #[test]
170    fn test_tai_tt_offset_32_184_seconds() {
171        let test_dates = [
172            (J2000_JD, "J2000.0"),
173            (2455197.5, "2010-01-01"),
174            (2459580.5, "2022-01-01"),
175            (2440587.5, "1970-01-01 Unix epoch"),
176        ];
177
178        for (jd, description) in test_dates {
179            let tai = TAI::from_julian_date(JulianDate::new(jd, 0.0));
180            let tt = tai.to_tt().unwrap();
181
182            let tai_jd = tai.to_julian_date();
183            let tt_jd = tt.to_julian_date();
184
185            let offset_days = (tt_jd.jd1() - tai_jd.jd1()) + (tt_jd.jd2() - tai_jd.jd2());
186            let offset_seconds = offset_days * SECONDS_PER_DAY_F64;
187
188            assert_eq!(
189                offset_seconds, 32.184,
190                "{}: TAI->TT offset must be exactly 32.184 seconds",
191                description
192            );
193
194            let tt = TT::from_julian_date(JulianDate::new(jd, 0.0));
195            let tai = tt.to_tai().unwrap();
196
197            let tt_jd = tt.to_julian_date();
198            let tai_jd = tai.to_julian_date();
199
200            let offset_days = (tt_jd.jd1() - tai_jd.jd1()) + (tt_jd.jd2() - tai_jd.jd2());
201            let offset_seconds = offset_days * SECONDS_PER_DAY_F64;
202
203            assert_eq!(
204                offset_seconds, 32.184,
205                "{}: TT->TAI means TT is 32.184 seconds ahead",
206                description
207            );
208        }
209    }
210
211    #[test]
212    fn test_tai_tt_round_trip_precision() {
213        let test_jd2_values = [0.0, 0.5, 0.123456789012345, -0.123456789012345, 0.987654321];
214
215        for jd2 in test_jd2_values {
216            let original_tai = TAI::from_julian_date(JulianDate::new(J2000_JD, jd2));
217            let tt = original_tai.to_tt().unwrap();
218            let round_trip_tai = tt.to_tai().unwrap();
219
220            assert_eq!(
221                original_tai.to_julian_date().jd1(),
222                round_trip_tai.to_julian_date().jd1(),
223                "TAI->TT->TAI JD1 must be exact for jd2={}",
224                jd2
225            );
226            assert_eq!(
227                original_tai.to_julian_date().jd2(),
228                round_trip_tai.to_julian_date().jd2(),
229                "TAI->TT->TAI JD2 must be exact for jd2={}",
230                jd2
231            );
232
233            let original_tt = TT::from_julian_date(JulianDate::new(J2000_JD, jd2));
234            let tai = original_tt.to_tai().unwrap();
235            let round_trip_tt = tai.to_tt().unwrap();
236
237            assert_eq!(
238                original_tt.to_julian_date().jd1(),
239                round_trip_tt.to_julian_date().jd1(),
240                "TT->TAI->TT JD1 must be exact for jd2={}",
241                jd2
242            );
243            assert_eq!(
244                original_tt.to_julian_date().jd2(),
245                round_trip_tt.to_julian_date().jd2(),
246                "TT->TAI->TT JD2 must be exact for jd2={}",
247                jd2
248            );
249        }
250
251        let alt_tai = TAI::from_julian_date(JulianDate::new(0.5, J2000_JD));
252        let alt_tt = alt_tai.to_tt().unwrap();
253        let alt_round_trip = alt_tt.to_tai().unwrap();
254
255        assert_eq!(
256            alt_tai.to_julian_date().jd1(),
257            alt_round_trip.to_julian_date().jd1(),
258            "Alternate split TAI->TT->TAI JD1 must be exact"
259        );
260        assert_eq!(
261            alt_tai.to_julian_date().jd2(),
262            alt_round_trip.to_julian_date().jd2(),
263            "Alternate split TAI->TT->TAI JD2 must be exact"
264        );
265    }
266
267    #[test]
268    fn test_tai_tcg_chain_round_trip() {
269        // TCG conversions involve multiplicative scaling (LG rate). Precision loss
270        // varies by jd2 magnitude: ~220 attoseconds for jd2≈0, up to ~2 picoseconds
271        // for jd2≈±0.25 due to f64 magnitude mismatch when adding small corrections.
272        const TOLERANCE_DAYS: f64 = 1e-14; // ~1 picosecond
273
274        let test_jd2_values = [0.0, 0.123456789, 0.5, -0.25];
275
276        for jd2 in test_jd2_values {
277            let original_tai = TAI::from_julian_date(JulianDate::new(J2000_JD, jd2));
278            let tcg = original_tai.to_tcg().unwrap();
279            let round_trip_tai = tcg.to_tai().unwrap();
280
281            assert_eq!(
282                original_tai.to_julian_date().jd1(),
283                round_trip_tai.to_julian_date().jd1(),
284                "TAI->TCG->TAI JD1 must be exact for jd2={}",
285                jd2
286            );
287            let jd2_diff =
288                (original_tai.to_julian_date().jd2() - round_trip_tai.to_julian_date().jd2()).abs();
289            assert!(
290                jd2_diff <= TOLERANCE_DAYS,
291                "TAI->TCG->TAI JD2 difference {} exceeds tolerance {} for jd2={}",
292                jd2_diff,
293                TOLERANCE_DAYS,
294                jd2
295            );
296
297            let original_tcg = TCG::from_julian_date(JulianDate::new(J2000_JD, jd2));
298            let tai = original_tcg.to_tai().unwrap();
299            let round_trip_tcg = tai.to_tcg().unwrap();
300
301            assert_eq!(
302                original_tcg.to_julian_date().jd1(),
303                round_trip_tcg.to_julian_date().jd1(),
304                "TCG->TAI->TCG JD1 must be exact for jd2={}",
305                jd2
306            );
307            let jd2_diff =
308                (original_tcg.to_julian_date().jd2() - round_trip_tcg.to_julian_date().jd2()).abs();
309            assert!(
310                jd2_diff <= TOLERANCE_DAYS,
311                "TCG->TAI->TCG JD2 difference {} exceeds tolerance {} for jd2={}",
312                jd2_diff,
313                TOLERANCE_DAYS,
314                jd2
315            );
316        }
317    }
318}