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}