Skip to main content

celestial_time/scales/conversions/
tcg_tcb.rs

1//! Conversions between Geocentric Coordinate Time (TCG) and Barycentric Coordinate Time (TCB).
2//!
3//! TCG and TCB are coordinate time scales for the geocentric and barycentric reference frames,
4//! respectively. TCB runs faster than TCG because the solar system's gravitational potential
5//! at Earth's orbit causes additional time dilation beyond Earth's own gravitational field.
6//!
7//! # The L_B Rate Factor
8//!
9//! The defining relationship from IAU 2006 Resolution B3 is:
10//!
11//! ```text
12//! TCB - TCG = L_B * (JD_TCG - T0) * 86400
13//! ```
14//!
15//! Where:
16//! - `L_B = 1.550519768e-8` (IAU 2006, exact by definition)
17//! - `T0 = 1977 January 1, 0h TAI` (MJD 43144.0, the common reference epoch)
18//! - The factor 86400 converts days to seconds
19//!
20//! L_B represents the average fractional rate difference between TCB and TCG due to the
21//! Sun's gravitational potential at Earth's orbit. TCB gains about 489 milliseconds per
22//! year relative to TCG.
23//!
24//! # Reference Epoch
25//!
26//! At the reference epoch T0 (1977 January 1, 0h TAI, JD 2443144.5003725), TCG and TCB
27//! are defined to be equal. This is the same epoch used for the TT-TCG relationship.
28//!
29//! Before T0, TCB is behind TCG; after T0, TCB is ahead.
30//!
31//! # Physical Interpretation
32//!
33//! The L_B rate difference arises from general relativity:
34//!
35//! - **TCG**: Proper time for a clock at the geocenter (removing Earth's gravitational
36//!   potential but still in the Sun's potential well)
37//! - **TCB**: Proper time for a clock at the solar system barycenter (outside all
38//!   gravitational potentials of the solar system)
39//!
40//! Since Earth orbits within the Sun's gravitational potential, clocks at the geocenter
41//! run slower than clocks at the barycenter. The L_B value encapsulates this difference.
42//!
43//! # Accumulated Offset at J2000.0
44//!
45//! At J2000.0 (about 23 years after the 1977 reference epoch), TCB is approximately
46//! 11.25 seconds ahead of TCG:
47//!
48//! ```text
49//! TCB - TCG at J2000.0 = L_B * 23 years * 86400 * 365.25 days/year
50//!                      = 1.550519768e-8 * 7.26e8 seconds
51//!                      ≈ 11.25 seconds
52//! ```
53//!
54//! # Precision
55//!
56//! Round-trip conversions (TCG -> TCB -> TCG or TCB -> TCG -> TCB) achieve sub-picosecond
57//! accuracy. The implementation applies corrections to the smaller-magnitude Julian Date
58//! component to preserve precision.
59//!
60//! # Usage
61//!
62//! ```
63//! use celestial_time::scales::{TCG, TCB};
64//! use celestial_time::scales::conversions::{ToTCB, ToTCGFromTCB};
65//! use celestial_time::julian::JulianDate;
66//! use celestial_core::constants::J2000_JD;
67//!
68//! let tcg = TCG::from_julian_date(JulianDate::new(J2000_JD, 0.0));
69//! let tcb = tcg.to_tcb().unwrap();
70//!
71//! // At J2000.0, TCB is about 11.25 seconds ahead of TCG
72//! let offset_days = tcb.to_julian_date().to_f64() - tcg.to_julian_date().to_f64();
73//! assert!(offset_days > 0.0, "TCB should be ahead of TCG after 1977");
74//! ```
75//!
76//! # References
77//!
78//! - IAU 2006 Resolution B3: Re-definition of Barycentric Dynamical Time, TDB
79//! - IAU 2000 Resolution B1.9: Definition of TCG
80//! - IERS Conventions (2010), Chapter 10: General Relativistic Models for Time
81//! - Petit & Luzum (2010): IERS Technical Note 36
82
83use crate::constants::{TCB_RATE_LB, TCB_RATE_RATIO, TCB_REFERENCE_EPOCH};
84use crate::julian::JulianDate;
85use crate::scales::{TCB, TCG};
86use crate::TimeResult;
87use celestial_core::constants::MJD_ZERO_POINT;
88
89/// Convert Geocentric Coordinate Time (TCG) to Barycentric Coordinate Time (TCB).
90///
91/// TCB runs faster than TCG by the L_B rate factor due to the solar system's
92/// gravitational potential at Earth's orbit. This trait applies the rate correction
93/// accumulated since the 1977 reference epoch.
94pub trait ToTCB {
95    /// Convert to Barycentric Coordinate Time (TCB).
96    ///
97    /// Applies: `TCB = TCG + L_B / (1 - L_B) * (TCG - T0)`
98    ///
99    /// At J2000.0, this adds approximately 11.25 seconds.
100    fn to_tcb(&self) -> TimeResult<TCB>;
101}
102
103/// Convert Barycentric Coordinate Time (TCB) to Geocentric Coordinate Time (TCG).
104///
105/// This is the inverse of [`ToTCB`]. The conversion removes the L_B rate difference
106/// that accumulates between the barycentric and geocentric coordinate times.
107pub trait ToTCGFromTCB {
108    /// Convert to Geocentric Coordinate Time (TCG).
109    ///
110    /// Applies: `TCG = TCB - L_B * (TCB - T0)`
111    ///
112    /// At J2000.0, this subtracts approximately 11.25 seconds.
113    fn to_tcg(&self) -> TimeResult<TCG>;
114}
115
116impl ToTCB for TCB {
117    /// Identity conversion. Returns self unchanged.
118    fn to_tcb(&self) -> TimeResult<TCB> {
119        Ok(*self)
120    }
121}
122
123impl ToTCB for TCG {
124    /// Convert TCG to TCB by applying the L_B rate correction.
125    ///
126    /// Uses `L_B / (1 - L_B)` as the rate ratio for the forward transformation.
127    /// This ratio accounts for the fact that we're computing TCB from TCG, not vice versa.
128    ///
129    /// The correction is computed relative to the 1977 reference epoch where TCG = TCB.
130    /// Applies the correction to the smaller-magnitude JD component for precision.
131    fn to_tcb(&self) -> TimeResult<TCB> {
132        let tcg_jd = self.to_julian_date();
133
134        let (tcb_jd1, tcb_jd2) = if tcg_jd.jd1().abs() > tcg_jd.jd2().abs() {
135            let correction = ((tcg_jd.jd1() - MJD_ZERO_POINT)
136                + (tcg_jd.jd2() - TCB_REFERENCE_EPOCH))
137                * TCB_RATE_RATIO;
138            (tcg_jd.jd1(), tcg_jd.jd2() + correction)
139        } else {
140            let correction = ((tcg_jd.jd2() - MJD_ZERO_POINT)
141                + (tcg_jd.jd1() - TCB_REFERENCE_EPOCH))
142                * TCB_RATE_RATIO;
143            (tcg_jd.jd1() + correction, tcg_jd.jd2())
144        };
145
146        let tcb_jd = JulianDate::new(tcb_jd1, tcb_jd2);
147        Ok(TCB::from_julian_date(tcb_jd))
148    }
149}
150
151impl ToTCGFromTCB for TCB {
152    /// Convert TCB to TCG by removing the L_B rate correction.
153    ///
154    /// Computes: `TCG = TCB - L_B * (JD_TCB - T0)`
155    ///
156    /// The correction is subtracted because TCB runs faster than TCG.
157    /// At J2000.0, this removes about 11.25 seconds.
158    ///
159    /// Applies the correction to the smaller-magnitude JD component for precision.
160    fn to_tcg(&self) -> TimeResult<TCG> {
161        let tcb_jd = self.to_julian_date();
162
163        let (tcg_jd1, tcg_jd2) = if tcb_jd.jd1().abs() > tcb_jd.jd2().abs() {
164            let correction = ((tcb_jd.jd1() - MJD_ZERO_POINT)
165                + (tcb_jd.jd2() - TCB_REFERENCE_EPOCH))
166                * TCB_RATE_LB;
167            (tcb_jd.jd1(), tcb_jd.jd2() - correction)
168        } else {
169            let correction = ((tcb_jd.jd2() - MJD_ZERO_POINT)
170                + (tcb_jd.jd1() - TCB_REFERENCE_EPOCH))
171                * TCB_RATE_LB;
172            (tcb_jd.jd1() - correction, tcb_jd.jd2())
173        };
174
175        let tcg_jd = JulianDate::new(tcg_jd1, tcg_jd2);
176        Ok(TCG::from_julian_date(tcg_jd))
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183    use crate::constants::MJD_1977_JAN_1;
184    use celestial_core::constants::{J2000_JD, MJD_ZERO_POINT, SECONDS_PER_DAY_F64};
185
186    #[test]
187    fn test_identity_conversions() {
188        let tcb = TCB::from_julian_date(JulianDate::new(J2000_JD, 0.999999999999999));
189        let identity_tcb = tcb.to_tcb().unwrap();
190
191        assert_eq!(
192            tcb.to_julian_date().jd1(),
193            identity_tcb.to_julian_date().jd1()
194        );
195        assert_eq!(
196            tcb.to_julian_date().jd2(),
197            identity_tcb.to_julian_date().jd2()
198        );
199
200        let tcg = TCG::from_julian_date(JulianDate::new(J2000_JD, 0.999999999999999));
201        let tcb_converted = tcg.to_tcb().unwrap();
202        let tcg_back = tcb_converted.to_tcg().unwrap();
203        let tcb_again = tcg_back.to_tcb().unwrap();
204
205        assert_eq!(
206            tcb_converted.to_julian_date().jd1(),
207            tcb_again.to_julian_date().jd1()
208        );
209        assert_eq!(
210            tcb_converted.to_julian_date().jd2(),
211            tcb_again.to_julian_date().jd2()
212        );
213    }
214
215    #[test]
216    fn test_tcg_tcb_offset_at_j2000() {
217        let tcg = TCG::from_julian_date(JulianDate::new(J2000_JD, 0.0));
218        let tcb = tcg.to_tcb().unwrap();
219        let tcb_jd = tcb.to_julian_date().to_f64();
220
221        assert!(tcb_jd > J2000_JD, "TCB should be ahead of TCG");
222
223        let diff_seconds = (tcb_jd - J2000_JD) * SECONDS_PER_DAY_F64;
224        assert!(
225            diff_seconds > 11.0 && diff_seconds < 12.0,
226            "TCB-TCG at J2000.0 should be ~11.25 seconds: {:.6} seconds",
227            diff_seconds
228        );
229
230        let tcb_at_j2000 = TCB::from_julian_date(JulianDate::new(J2000_JD, 0.0));
231        let tcg_from_tcb = tcb_at_j2000.to_tcg().unwrap();
232        let tcg_jd = tcg_from_tcb.to_julian_date().to_f64();
233
234        assert!(tcg_jd < J2000_JD, "TCG should be behind TCB");
235
236        let reverse_diff = (J2000_JD - tcg_jd) * SECONDS_PER_DAY_F64;
237        assert!(
238            reverse_diff > 11.0 && reverse_diff < 12.0,
239            "TCG-TCB reverse difference should be ~11.25s: {:.6} seconds",
240            reverse_diff
241        );
242    }
243
244    #[test]
245    fn test_tcg_tcb_rate_relationship() {
246        assert_eq!(TCB_RATE_LB, 1.550519768e-8);
247
248        let reference_epoch = TCG::from_julian_date(JulianDate::new(TCB_REFERENCE_EPOCH, 0.0));
249        let one_day_later = TCG::from_julian_date(JulianDate::new(TCB_REFERENCE_EPOCH + 1.0, 0.0));
250
251        let tcb_ref = reference_epoch.to_tcb().unwrap();
252        let tcb_day = one_day_later.to_tcb().unwrap();
253
254        let tcb_diff = tcb_day.to_julian_date().to_f64() - tcb_ref.to_julian_date().to_f64();
255        let expected_diff = 1.0 + TCB_RATE_LB / (1.0 - TCB_RATE_LB);
256
257        let relative_error = (tcb_diff - expected_diff).abs() / expected_diff;
258        assert!(
259            relative_error < 1e-12,
260            "TCB rate should match expected relativistic correction: {:.2e}",
261            relative_error
262        );
263
264        let ten_years_days = 3652.5;
265        let tcg_j2000 = TCG::from_julian_date(JulianDate::new(J2000_JD, 0.0));
266        let tcg_j2010 = TCG::from_julian_date(JulianDate::new(J2000_JD + ten_years_days, 0.0));
267
268        let tcb_j2000 = tcg_j2000.to_tcb().unwrap();
269        let tcb_j2010 = tcg_j2010.to_tcb().unwrap();
270
271        let tcb_interval =
272            tcb_j2010.to_julian_date().to_f64() - tcb_j2000.to_julian_date().to_f64();
273        let expected_drift = ten_years_days * TCB_RATE_RATIO;
274        let actual_drift = tcb_interval - ten_years_days;
275
276        let drift_error = (actual_drift - expected_drift).abs() / expected_drift;
277        assert!(
278            drift_error < 1e-4,
279            "10-year secular drift error: {:.2e}",
280            drift_error
281        );
282    }
283
284    #[test]
285    fn test_tcg_tcb_round_trip_precision() {
286        let tolerance = 1e-14;
287        let test_jd2_values = [0.0, 0.5, 0.123456789012345, -0.123456789012345];
288
289        for jd2 in test_jd2_values {
290            let tcg = TCG::from_julian_date(JulianDate::new(J2000_JD, jd2));
291            let tcb = tcg.to_tcb().unwrap();
292            let back_tcg = tcb.to_tcg().unwrap();
293
294            let total_diff =
295                (tcg.to_julian_date().to_f64() - back_tcg.to_julian_date().to_f64()).abs();
296            assert!(
297                total_diff < tolerance,
298                "TCG round trip for jd2={} exceeded tolerance: {:.2e}",
299                jd2,
300                total_diff
301            );
302
303            let tcb_rt = TCB::from_julian_date(JulianDate::new(J2000_JD, jd2));
304            let tcg_from = tcb_rt.to_tcg().unwrap();
305            let back_tcb = tcg_from.to_tcb().unwrap();
306
307            let tcb_diff =
308                (tcb_rt.to_julian_date().to_f64() - back_tcb.to_julian_date().to_f64()).abs();
309            assert!(
310                tcb_diff < tolerance,
311                "TCB round trip for jd2={} exceeded tolerance: {:.2e}",
312                jd2,
313                tcb_diff
314            );
315        }
316
317        let tcg_alt = TCG::from_julian_date(JulianDate::new(0.5, J2000_JD));
318        let tcb_alt = tcg_alt.to_tcb().unwrap();
319        let back_alt = tcb_alt.to_tcg().unwrap();
320        let alt_diff =
321            (tcg_alt.to_julian_date().to_f64() - back_alt.to_julian_date().to_f64()).abs();
322        assert!(
323            alt_diff < tolerance,
324            "Alternate JD split round trip exceeded tolerance: {:.2e}",
325            alt_diff
326        );
327
328        let tcb_alt2 = TCB::from_julian_date(JulianDate::new(0.5, J2000_JD));
329        let tcg_alt2 = tcb_alt2.to_tcg().unwrap();
330        let back_alt2 = tcg_alt2.to_tcb().unwrap();
331        let alt2_diff =
332            (tcb_alt2.to_julian_date().to_f64() - back_alt2.to_julian_date().to_f64()).abs();
333        assert!(
334            alt2_diff < tolerance,
335            "Alternate TCB split round trip exceeded tolerance: {:.2e}",
336            alt2_diff
337        );
338    }
339
340    #[test]
341    fn test_reference_epoch_behavior() {
342        let tcg_at_ref =
343            TCG::from_julian_date(JulianDate::new(MJD_ZERO_POINT + MJD_1977_JAN_1, 0.0));
344        let tcb_at_ref = tcg_at_ref.to_tcb().unwrap();
345
346        let diff_seconds = (tcb_at_ref.to_julian_date().to_f64()
347            - tcg_at_ref.to_julian_date().to_f64())
348            * SECONDS_PER_DAY_F64;
349
350        assert!(
351            diff_seconds.abs() < 1.0,
352            "At 1977 Jan 1 reference epoch, TCB and TCG should be nearly equal: {:.6} seconds",
353            diff_seconds
354        );
355    }
356}