Skip to main content

celestial_time/scales/conversions/
tt_tcg.rs

1//! Conversions between Terrestrial Time (TT) and Geocentric Coordinate Time (TCG).
2//!
3//! TT and TCG differ by a constant rate defined by the IAU. TCG runs faster than TT
4//! because TT accounts for gravitational time dilation at Earth's geoid, while TCG
5//! is the proper time for a clock at the geocenter (in the absence of Earth's mass).
6//!
7//! # The L_G Rate Factor
8//!
9//! The defining relationship is:
10//!
11//! ```text
12//! TCG - TT = L_G * (JD_TT - T0) * 86400
13//! ```
14//!
15//! Where:
16//! - `L_G = 6.969290134e-10` (IAU 2000 Resolution B1.9, exact by definition)
17//! - `T0 = 1977 January 1, 0h TAI` (reference epoch where TCG = TT)
18//! - The factor 86400 converts days to seconds
19//!
20//! This means TCG gains about 22 milliseconds per year relative to TT.
21//!
22//! # Reference Epoch
23//!
24//! At the reference epoch T0 (MJD 43144.0003725 in TT), TCG and TT are equal.
25//! Before T0, TCG is behind TT; after T0, TCG is ahead.
26//!
27//! # Precision
28//!
29//! Round-trip conversions (TT -> TCG -> TT or TCG -> TT -> TCG) achieve sub-picosecond
30//! accuracy for dates within a few centuries of J2000.0. The implementation applies
31//! corrections to the smaller-magnitude Julian Date component to preserve precision.
32//!
33//! # Usage
34//!
35//! ```
36//! use celestial_time::scales::{TT, TCG};
37//! use celestial_time::scales::conversions::{ToTT, ToTCG};
38//! use celestial_time::julian::JulianDate;
39//! use celestial_core::constants::J2000_JD;
40//!
41//! let tt = TT::from_julian_date(JulianDate::new(J2000_JD, 0.0));
42//! let tcg = tt.to_tcg().unwrap();
43//!
44//! // At J2000.0, TCG is about 0.506 seconds ahead of TT
45//! let offset_days = tcg.to_julian_date().jd2() - tt.to_julian_date().jd2();
46//! ```
47
48use super::{ToTCG, ToTT};
49use crate::constants::{TCG_RATE_LG, TCG_RATE_RATIO, TCG_REFERENCE_EPOCH};
50use crate::julian::JulianDate;
51use crate::scales::{TCG, TT};
52use crate::TimeResult;
53use celestial_core::constants::MJD_ZERO_POINT;
54
55impl ToTCG for TCG {
56    /// Identity conversion. Returns self unchanged.
57    fn to_tcg(&self) -> TimeResult<TCG> {
58        Ok(*self)
59    }
60}
61
62impl ToTT for TCG {
63    /// Convert TCG to TT by removing the L_G rate correction.
64    ///
65    /// Computes: `TT = TCG - L_G * (JD_TCG - T0) * 86400 / 86400`
66    ///
67    /// The correction is subtracted because TCG runs faster than TT.
68    /// At J2000.0, this removes about 0.506 seconds.
69    fn to_tt(&self) -> TimeResult<TT> {
70        let tcg_jd = self.to_julian_date();
71
72        let (tt_jd1, tt_jd2) = if tcg_jd.jd1().abs() > tcg_jd.jd2().abs() {
73            let correction = ((tcg_jd.jd1() - MJD_ZERO_POINT)
74                + (tcg_jd.jd2() - TCG_REFERENCE_EPOCH))
75                * TCG_RATE_LG;
76            (tcg_jd.jd1(), tcg_jd.jd2() - correction)
77        } else {
78            let correction = ((tcg_jd.jd2() - MJD_ZERO_POINT)
79                + (tcg_jd.jd1() - TCG_REFERENCE_EPOCH))
80                * TCG_RATE_LG;
81            (tcg_jd.jd1() - correction, tcg_jd.jd2())
82        };
83
84        let tt_jd = JulianDate::new(tt_jd1, tt_jd2);
85        Ok(TT::from_julian_date(tt_jd))
86    }
87}
88
89impl ToTCG for TT {
90    /// Convert TT to TCG by applying the L_G rate correction.
91    ///
92    /// Uses `L_G / (1 - L_G)` as the rate ratio for the forward transformation.
93    /// This ratio accounts for the fact that we're computing TCG from TT, not vice versa.
94    ///
95    /// At J2000.0, this adds about 0.506 seconds.
96    fn to_tcg(&self) -> TimeResult<TCG> {
97        let tt_jd = self.to_julian_date();
98
99        let (tcg_jd1, tcg_jd2) = if tt_jd.jd1().abs() > tt_jd.jd2().abs() {
100            let correction = ((tt_jd.jd1() - MJD_ZERO_POINT) + (tt_jd.jd2() - TCG_REFERENCE_EPOCH))
101                * TCG_RATE_RATIO;
102            (tt_jd.jd1(), tt_jd.jd2() + correction)
103        } else {
104            let correction = ((tt_jd.jd2() - MJD_ZERO_POINT) + (tt_jd.jd1() - TCG_REFERENCE_EPOCH))
105                * TCG_RATE_RATIO;
106            (tt_jd.jd1() + correction, tt_jd.jd2())
107        };
108
109        let tcg_jd = JulianDate::new(tcg_jd1, tcg_jd2);
110        Ok(TCG::from_julian_date(tcg_jd))
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117    use celestial_core::constants::{J2000_JD, MJD_ZERO_POINT, SECONDS_PER_DAY_F64};
118
119    #[test]
120    fn test_tcg_identity_conversion() {
121        let tcg = TCG::from_julian_date(JulianDate::new(J2000_JD, 0.999999999999999));
122        let identity_tcg = tcg.to_tcg().unwrap();
123
124        assert_eq!(
125            tcg.to_julian_date().jd1(),
126            identity_tcg.to_julian_date().jd1(),
127            "TCG identity conversion should preserve JD1"
128        );
129        assert_eq!(
130            tcg.to_julian_date().jd2(),
131            identity_tcg.to_julian_date().jd2(),
132            "TCG identity conversion should preserve JD2"
133        );
134    }
135
136    #[test]
137    fn test_tt_tcg_offset() {
138        let test_cases = [
139            (J2000_JD, 0.5058332857, "J2000.0"),
140            (2455197.5, 0.7257673560, "2010-01-01"),
141            (2458849.5, 0.9456713190, "2020-01-01"),
142            (2469807.5, 1.6055036373, "2050-01-01"),
143        ];
144
145        let tolerance_seconds = 1e-6;
146
147        for (jd, expected_offset_seconds, description) in test_cases {
148            let tt = TT::from_julian_date(JulianDate::new(jd, 0.0));
149            let tcg = tt.to_tcg().unwrap();
150
151            let tt_jd = tt.to_julian_date();
152            let tcg_jd = tcg.to_julian_date();
153
154            let offset_days = (tcg_jd.jd1() - tt_jd.jd1()) + (tcg_jd.jd2() - tt_jd.jd2());
155            let offset_seconds = offset_days * SECONDS_PER_DAY_F64;
156
157            let diff = (offset_seconds - expected_offset_seconds).abs();
158            assert!(
159                diff < tolerance_seconds,
160                "{}: TT->TCG offset should be {:.10}s, got {:.10}s (diff: {:.2e}s)",
161                description,
162                expected_offset_seconds,
163                offset_seconds,
164                diff
165            );
166
167            let tcg = TCG::from_julian_date(JulianDate::new(jd, 0.0));
168            let tt = tcg.to_tt().unwrap();
169
170            let tcg_jd = tcg.to_julian_date();
171            let tt_jd = tt.to_julian_date();
172
173            let offset_days = (tcg_jd.jd1() - tt_jd.jd1()) + (tcg_jd.jd2() - tt_jd.jd2());
174            let offset_seconds = offset_days * SECONDS_PER_DAY_F64;
175
176            let diff = (offset_seconds - expected_offset_seconds).abs();
177            assert!(
178                diff < tolerance_seconds,
179                "{}: TCG->TT means TCG is {:.10}s ahead, got {:.10}s (diff: {:.2e}s)",
180                description,
181                expected_offset_seconds,
182                offset_seconds,
183                diff
184            );
185        }
186    }
187
188    #[test]
189    fn test_tt_tcg_at_reference_epoch() {
190        let reference_epoch_jd = MJD_ZERO_POINT + TCG_REFERENCE_EPOCH;
191
192        let tt = TT::from_julian_date(JulianDate::new(reference_epoch_jd, 0.0));
193        let tcg = tt.to_tcg().unwrap();
194
195        let tt_jd = tt.to_julian_date();
196        let tcg_jd = tcg.to_julian_date();
197
198        let offset_days = (tcg_jd.jd1() - tt_jd.jd1()) + (tcg_jd.jd2() - tt_jd.jd2());
199        let offset_seconds = offset_days * SECONDS_PER_DAY_F64;
200
201        let tolerance_seconds = 1e-12;
202        assert!(
203            offset_seconds.abs() < tolerance_seconds,
204            "At reference epoch T0, TCG-TT should be 0, got {:.2e}s",
205            offset_seconds
206        );
207    }
208
209    #[test]
210    fn test_tt_tcg_round_trip_precision() {
211        // TCG conversions involve multiplicative scaling (LG rate). Precision loss
212        // varies by jd2 magnitude: ~220 attoseconds for jd2~0, up to ~2 picoseconds
213        // for jd2~+/-0.25 due to f64 magnitude mismatch when adding small corrections.
214        const TOLERANCE_DAYS: f64 = 1e-14; // ~1 picosecond
215
216        let test_jd2_values = [0.0, 0.5, 0.123456789012345, -0.123456789012345, 0.987654321];
217
218        for jd2 in test_jd2_values {
219            let original_tt = TT::from_julian_date(JulianDate::new(J2000_JD, jd2));
220            let tcg = original_tt.to_tcg().unwrap();
221            let round_trip_tt = tcg.to_tt().unwrap();
222
223            assert_eq!(
224                original_tt.to_julian_date().jd1(),
225                round_trip_tt.to_julian_date().jd1(),
226                "TT->TCG->TT JD1 must be exact for jd2={}",
227                jd2
228            );
229            let jd2_diff =
230                (original_tt.to_julian_date().jd2() - round_trip_tt.to_julian_date().jd2()).abs();
231            assert!(
232                jd2_diff <= TOLERANCE_DAYS,
233                "TT->TCG->TT JD2 difference {} exceeds tolerance {} for jd2={}",
234                jd2_diff,
235                TOLERANCE_DAYS,
236                jd2
237            );
238
239            let original_tcg = TCG::from_julian_date(JulianDate::new(J2000_JD, jd2));
240            let tt = original_tcg.to_tt().unwrap();
241            let round_trip_tcg = tt.to_tcg().unwrap();
242
243            assert_eq!(
244                original_tcg.to_julian_date().jd1(),
245                round_trip_tcg.to_julian_date().jd1(),
246                "TCG->TT->TCG JD1 must be exact for jd2={}",
247                jd2
248            );
249            let jd2_diff =
250                (original_tcg.to_julian_date().jd2() - round_trip_tcg.to_julian_date().jd2()).abs();
251            assert!(
252                jd2_diff <= TOLERANCE_DAYS,
253                "TCG->TT->TCG JD2 difference {} exceeds tolerance {} for jd2={}",
254                jd2_diff,
255                TOLERANCE_DAYS,
256                jd2
257            );
258        }
259
260        let alt_tt = TT::from_julian_date(JulianDate::new(0.5, J2000_JD));
261        let alt_tcg = alt_tt.to_tcg().unwrap();
262        let alt_round_trip = alt_tcg.to_tt().unwrap();
263
264        assert_eq!(
265            alt_tt.to_julian_date().jd1(),
266            alt_round_trip.to_julian_date().jd1(),
267            "Alternate split TT->TCG->TT JD1 must be exact"
268        );
269        let jd2_diff =
270            (alt_tt.to_julian_date().jd2() - alt_round_trip.to_julian_date().jd2()).abs();
271        assert!(
272            jd2_diff <= TOLERANCE_DAYS,
273            "Alternate split TT->TCG->TT JD2 difference {} exceeds tolerance {}",
274            jd2_diff,
275            TOLERANCE_DAYS
276        );
277    }
278}