celestial_time/scales/conversions/
tcg_tcb.rs1use 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
89pub trait ToTCB {
95 fn to_tcb(&self) -> TimeResult<TCB>;
101}
102
103pub trait ToTCGFromTCB {
108 fn to_tcg(&self) -> TimeResult<TCG>;
114}
115
116impl ToTCB for TCB {
117 fn to_tcb(&self) -> TimeResult<TCB> {
119 Ok(*self)
120 }
121}
122
123impl ToTCB for TCG {
124 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 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}