Skip to main content

celestial_time/scales/conversions/
tcb_tdb.rs

1//! Conversions between Barycentric Coordinate Time (TCB) and Barycentric Dynamical Time (TDB).
2//!
3//! TCB and TDB are both barycentric time scales used for solar system dynamics, but they
4//! differ in rate. TDB was introduced to provide a time scale that, when observed from
5//! Earth's surface, ticks at approximately the same rate as TT on average.
6//!
7//! # The L_B Rate Factor
8//!
9//! The defining relationship from IAU 2006 Resolution B3 is:
10//!
11//! ```text
12//! TDB = TCB - L_B * (JD_TCB - T0) * 86400 + TDB_0
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//! - `TDB_0 = -6.55e-5 seconds` (offset to align TDB with TT at J2000.0 on average)
19//!
20//! The L_B value represents the average fractional rate difference between TCB and TDB.
21//! TCB gains about 0.49 seconds per year relative to TDB.
22//!
23//! # Reference Epoch (TDB_0)
24//!
25//! The reference epoch for TCB-TDB conversions is 1977 January 1, 0h TAI (JD 2443144.5003725),
26//! the same epoch used for TT-TCG. At this epoch, with the TDB_0 offset applied, TDB and
27//! TCB are related by definition.
28//!
29//! The TDB_0 constant (-6.55e-5 seconds) was chosen so that TDB matches TT on average at
30//! the geocenter. This makes TDB a "scaled" version of TCB that tracks TT's rate.
31//!
32//! # Why TDB Exists
33//!
34//! TCB is the natural coordinate time for the barycentric frame, but its rate differs
35//! from TT by about 490 ms/year. For continuity with historical ephemerides and to
36//! avoid confusion, TDB was defined to match TT's average rate while remaining suitable
37//! for barycentric calculations.
38//!
39//! In practice:
40//! - TCB is used in relativistic equations of motion
41//! - TDB is used in JPL ephemerides (DE series) and for practical timekeeping
42//! - The difference grows linearly: ~17 seconds at J2000.0 relative to 1977
43//!
44//! # Precision
45//!
46//! Round-trip conversions (TCB -> TDB -> TCB or TDB -> TCB -> TDB) achieve sub-picosecond
47//! accuracy. The implementation applies corrections to the smaller-magnitude Julian Date
48//! component to preserve precision.
49//!
50//! # Usage
51//!
52//! ```
53//! use celestial_time::scales::{TCB, TDB};
54//! use celestial_time::scales::conversions::{TcbToTdb, TdbToTcb};
55//! use celestial_time::julian::JulianDate;
56//! use celestial_core::constants::J2000_JD;
57//!
58//! let tcb = TCB::from_julian_date(JulianDate::new(J2000_JD, 0.0));
59//! let tdb = tcb.tcb_to_tdb().unwrap();
60//!
61//! // At J2000.0, TDB is about 11.3 ms behind TCB (accumulated since 1977)
62//! let offset_days = tdb.to_julian_date().to_f64() - tcb.to_julian_date().to_f64();
63//! assert!(offset_days < 0.0, "TDB should be behind TCB after 1977");
64//! ```
65//!
66//! # References
67//!
68//! - IAU 2006 Resolution B3: Re-definition of Barycentric Dynamical Time, TDB
69//! - IERS Conventions (2010), Chapter 10: General Relativistic Models for Time
70//! - Soffel et al. (2003): The IAU 2000 Resolutions for Astrometry
71
72use crate::constants::TT_TAI_OFFSET;
73use crate::julian::JulianDate;
74use crate::scales::{TCB, TDB};
75use crate::TimeResult;
76use celestial_core::constants::{MJD_ZERO_POINT, SECONDS_PER_DAY_F64};
77
78/// L_B rate factor from IAU 2006 Resolution B3.
79/// Represents the fractional rate difference: TCB runs faster than TDB by this amount.
80const TCB_RATE: f64 = 1.550519768e-8;
81
82/// MJD of the reference epoch: 1977 January 1, 0h TAI.
83const MJD_1977: f64 = 43144.0;
84
85/// TDB_0 offset in seconds. Chosen so TDB matches TT rate on average at geocenter.
86const TDB_OFFSET: f64 = -6.55e-5;
87
88/// Reference epoch as full Julian Date (MJD_ZERO_POINT + MJD_1977).
89const T77TD: f64 = MJD_ZERO_POINT + MJD_1977;
90
91/// TT-TAI offset in days (32.184s / 86400), for epoch alignment.
92const T77TF: f64 = TT_TAI_OFFSET / SECONDS_PER_DAY_F64;
93
94/// TDB_0 offset in days (-6.55e-5s / 86400).
95const TDB0: f64 = TDB_OFFSET / SECONDS_PER_DAY_F64;
96
97/// Derived rate ratio: L_B / (1 - L_B).
98/// Used for TDB -> TCB conversion to invert the rate scaling.
99const TCB_RATE_RATIO: f64 = TCB_RATE / (1.0 - TCB_RATE);
100
101/// Convert Barycentric Coordinate Time (TCB) to Barycentric Dynamical Time (TDB).
102///
103/// TDB is a rescaled version of TCB designed to match TT's average rate at the geocenter.
104/// This conversion removes the L_B rate difference accumulated since 1977.
105pub trait TcbToTdb {
106    /// Convert TCB to TDB.
107    ///
108    /// Applies: `TDB = TCB - L_B * (TCB - T0) + TDB_0`
109    ///
110    /// At J2000.0, TDB is approximately 11 milliseconds behind TCB due to the
111    /// accumulated rate difference since 1977.
112    fn tcb_to_tdb(&self) -> TimeResult<TDB>;
113}
114
115/// Convert Barycentric Dynamical Time (TDB) to Barycentric Coordinate Time (TCB).
116///
117/// This is the inverse of [`TcbToTdb`]. Uses the rate ratio `L_B / (1 - L_B)`
118/// to correctly invert the scaling.
119pub trait TdbToTcb {
120    /// Convert TDB to TCB.
121    ///
122    /// Applies the inverse transformation using the derived rate ratio.
123    /// At J2000.0, TCB is approximately 11 milliseconds ahead of TDB.
124    fn tdb_to_tcb(&self) -> TimeResult<TCB>;
125}
126
127impl TcbToTdb for TCB {
128    /// Convert TCB to TDB by removing the L_B rate correction.
129    ///
130    /// The correction is computed as: `L_B * (TCB - T0)` where T0 is the 1977 epoch.
131    /// The TDB_0 offset is added to align with TT at the geocenter.
132    ///
133    /// Applies the correction to the smaller-magnitude JD component for precision.
134    fn tcb_to_tdb(&self) -> TimeResult<TDB> {
135        let tcb_jd = self.to_julian_date();
136        let (tcb1, tcb2) = (tcb_jd.jd1(), tcb_jd.jd2());
137
138        let (big, small) = if tcb1.abs() > tcb2.abs() {
139            (tcb1, tcb2)
140        } else {
141            (tcb2, tcb1)
142        };
143        let d = big - T77TD;
144        let corrected = small + TDB0 - (d + (small - T77TF)) * TCB_RATE;
145        let (tdb1, tdb2) = if tcb1.abs() > tcb2.abs() {
146            (big, corrected)
147        } else {
148            (corrected, big)
149        };
150
151        Ok(TDB::from_julian_date(JulianDate::new(tdb1, tdb2)))
152    }
153}
154
155impl TdbToTcb for TDB {
156    /// Convert TDB to TCB by applying the inverse L_B rate correction.
157    ///
158    /// Uses the rate ratio `L_B / (1 - L_B)` to properly invert the scaling.
159    /// First removes the TDB_0 offset, then applies the inverse rate correction.
160    ///
161    /// Applies the correction to the smaller-magnitude JD component for precision.
162    fn tdb_to_tcb(&self) -> TimeResult<TCB> {
163        let tdb_jd = self.to_julian_date();
164        let (tdb1, tdb2) = (tdb_jd.jd1(), tdb_jd.jd2());
165
166        let (big, small) = if tdb1.abs() > tdb2.abs() {
167            (tdb1, tdb2)
168        } else {
169            (tdb2, tdb1)
170        };
171        let d = T77TD - big;
172        let f = small - TDB0;
173        let corrected = f - (d - (f - T77TF)) * TCB_RATE_RATIO;
174        let (tcb1, tcb2) = if tdb1.abs() > tdb2.abs() {
175            (big, corrected)
176        } else {
177            (corrected, big)
178        };
179
180        Ok(TCB::from_julian_date(JulianDate::new(tcb1, tcb2)))
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187    use celestial_core::constants::J2000_JD;
188
189    #[test]
190    fn test_tcb_tdb_relationship() {
191        // Identity conversions
192        let tcb = TCB::from_julian_date(JulianDate::new(J2000_JD, 0.0));
193        let tdb = tcb.tcb_to_tdb().unwrap();
194        let tcb_jd = tcb.to_julian_date();
195        let tdb_jd = tdb.to_julian_date();
196
197        // TCB runs faster than TDB, so at J2000 (after 1977 epoch), TDB < TCB
198        assert!(
199            tdb_jd.to_f64() < tcb_jd.to_f64(),
200            "TDB should be behind TCB at J2000"
201        );
202
203        // Verify inverse relationship holds
204        let tdb = TDB::from_julian_date(JulianDate::new(J2000_JD, 0.0));
205        let tcb = tdb.tdb_to_tcb().unwrap();
206        let tdb_jd = tdb.to_julian_date();
207        let tcb_jd = tcb.to_julian_date();
208
209        assert!(
210            tcb_jd.to_f64() > tdb_jd.to_f64(),
211            "TCB should be ahead of TDB at J2000"
212        );
213    }
214
215    #[test]
216    fn test_tcb_tdb_round_trip_precision() {
217        // TCB/TDB conversions involve rate scaling. 1e-14 days = ~1 picosecond tolerance.
218        const TOLERANCE_DAYS: f64 = 1e-14;
219
220        let test_jd2_values = [0.0, 0.5, 0.123456789012345, -0.123456789012345, 0.987654321];
221
222        for jd2 in test_jd2_values {
223            // TCB -> TDB -> TCB
224            let original_tcb = TCB::from_julian_date(JulianDate::new(J2000_JD, jd2));
225            let tdb = original_tcb.tcb_to_tdb().unwrap();
226            let round_trip_tcb = tdb.tdb_to_tcb().unwrap();
227
228            assert_eq!(
229                original_tcb.to_julian_date().jd1(),
230                round_trip_tcb.to_julian_date().jd1(),
231                "TCB->TDB->TCB JD1 must be exact for jd2={}",
232                jd2
233            );
234            let jd2_diff =
235                (original_tcb.to_julian_date().jd2() - round_trip_tcb.to_julian_date().jd2()).abs();
236            assert!(
237                jd2_diff <= TOLERANCE_DAYS,
238                "TCB->TDB->TCB JD2 diff {} exceeds tolerance {} for jd2={}",
239                jd2_diff,
240                TOLERANCE_DAYS,
241                jd2
242            );
243
244            // TDB -> TCB -> TDB
245            let original_tdb = TDB::from_julian_date(JulianDate::new(J2000_JD, jd2));
246            let tcb = original_tdb.tdb_to_tcb().unwrap();
247            let round_trip_tdb = tcb.tcb_to_tdb().unwrap();
248
249            assert_eq!(
250                original_tdb.to_julian_date().jd1(),
251                round_trip_tdb.to_julian_date().jd1(),
252                "TDB->TCB->TDB JD1 must be exact for jd2={}",
253                jd2
254            );
255            let jd2_diff =
256                (original_tdb.to_julian_date().jd2() - round_trip_tdb.to_julian_date().jd2()).abs();
257            assert!(
258                jd2_diff <= TOLERANCE_DAYS,
259                "TDB->TCB->TDB JD2 diff {} exceeds tolerance {} for jd2={}",
260                jd2_diff,
261                TOLERANCE_DAYS,
262                jd2
263            );
264        }
265
266        // Alternate JD split case (jd2 > jd1)
267        let alt_tcb = TCB::from_julian_date(JulianDate::new(0.5, J2000_JD));
268        let alt_tdb = alt_tcb.tcb_to_tdb().unwrap();
269        let alt_round_trip = alt_tdb.tdb_to_tcb().unwrap();
270
271        assert_eq!(
272            alt_tcb.to_julian_date().jd1(),
273            alt_round_trip.to_julian_date().jd1(),
274            "Alternate split TCB->TDB->TCB JD1 must be exact"
275        );
276        let jd2_diff =
277            (alt_tcb.to_julian_date().jd2() - alt_round_trip.to_julian_date().jd2()).abs();
278        assert!(
279            jd2_diff <= TOLERANCE_DAYS,
280            "Alternate split TCB->TDB->TCB JD2 diff {} exceeds tolerance {}",
281            jd2_diff,
282            TOLERANCE_DAYS
283        );
284    }
285}