celestial-time 0.1.1-alpha.2

Pure Rust astronomical time scales
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
//! TT (Terrestrial Time) and TDB (Barycentric Dynamical Time) conversions.
//!
//! TDB is the independent time argument for solar system barycentric ephemerides.
//! Unlike most time scale pairs, the TT-TDB relationship is **location-dependent**
//! because it accounts for relativistic effects at the observer's position.
//!
//! # The TT-TDB Difference
//!
//! TDB tracks time at the solar system barycenter. An observer on Earth experiences
//! periodic variations due to:
//!
//! - Earth's orbital eccentricity (main ~1.66ms annual term)
//! - Lunar and planetary perturbations (smaller terms)
//! - Observer's position on Earth (diurnal terms, ~microsecond level)
//!
//! The difference TDB-TT oscillates with a peak-to-peak amplitude of about 3.3ms,
//! dominated by a ~1.66ms sinusoidal annual variation.
//!
//! # Algorithm
//!
//! Uses the Fairhead & Bretagnon (1990) series with 787 terms (the FAIRHD coefficients).
//! This provides sub-microsecond accuracy for dates within a few centuries of J2000.0.
//!
//! # Usage Patterns
//!
//! Two traits provide conversion:
//!
//! - [`ToTDB`]:       Convert TT to TDB (or TDB to itself)
//! - [`ToTTFromTDB`]: Convert TDB to TT with location awareness
//!
//! ```
//! use celestial_time::scales::{TT, TDB};
//! use celestial_time::scales::conversions::{ToTDB, ToTTFromTDB};
//! use celestial_time::JulianDate;
//! use celestial_core::Location;
//!
//! // TT → TDB: requires observer location
//! let tt = TT::from_julian_date(JulianDate::new(2451545.0, 0.5));
//! let tdb = tt.to_tdb_greenwich().unwrap();  // Uses Greenwich as reference
//!
//! // Or with explicit location
//! let tokyo = Location::from_degrees(35.6762, 139.6503, 40.0).unwrap();
//! let tdb = tt.to_tdb_with_location(&tokyo).unwrap();
//!
//! // TDB → TT: also requires location
//! let tdb = TDB::from_julian_date(JulianDate::new(2451545.0, 0.5));
//! let tt = tdb.to_tt_greenwich().unwrap();
//! ```
//!
//! # Why `TDB.to_tt()` Returns an Error
//!
//! The base [`ToTT`] trait method deliberately returns an error for TDB because
//! location-independent conversion would silently introduce errors of up to ~1.7ms.
//! Use `to_tt_greenwich()` or `to_tt_with_location()` instead.
//!
//! # UT1 Offset Parameter
//!
//! For highest precision (~microsecond), provide the UT1-UTC offset. The diurnal
//! terms in the TDB-TT difference depend on the observer's local sidereal time,
//! which requires UT1. If omitted (set to 0), the error is typically < 10 microseconds.

use super::ToTT;
use crate::{
    constants::FAIRHD,
    julian::JulianDate,
    scales::{TDB, TT},
};

use crate::{TimeError, TimeResult};
use celestial_core::constants::{
    DAYS_PER_JULIAN_MILLENNIUM, DEG_TO_RAD, J2000_JD, SECONDS_PER_DAY_F64, TWOPI,
};
use celestial_core::math::fmod;
use celestial_core::Location;

/// Returns the Royal Observatory Greenwich location.
///
/// Used as the default reference point for TT-TDB conversions when no
/// observer location is specified.
fn greenwich_location() -> Location {
    Location::from_degrees(51.477928, 0.0, 46.0).expect("Greenwich coordinates should be valid")
}

const DEFAULT_UT1_OFFSET_SECONDS: f64 = 0.0;

/// Compute the TDB-TT offset in seconds for a given date and observer location.
///
/// This is the core calculation using Fairhead & Bretagnon (1990) coefficients.
/// The result is TDB - TT in seconds; add to TT to get TDB, subtract from TDB to get TT.
///
/// # Arguments
///
/// - `date_jd`: Julian Date (two-part for precision)
/// - `ut1_fraction`: Fraction of UT1 day (0.0 to 1.0), used for diurnal terms
/// - `location`: Observer's geographic location
///
/// # Returns
///
/// TDB - TT offset in seconds. Typical magnitude is < 0.002 seconds.
pub fn compute_tdb_tt_offset(
    date_jd: &JulianDate,
    ut1_fraction: f64,
    location: &Location,
) -> TimeResult<f64> {
    let (u, v) = location.to_geocentric_km()?;

    let dtr = calculate_tdb_tt_difference(
        date_jd.jd1(),
        date_jd.jd2(),
        ut1_fraction,
        location.longitude,
        u,
        v,
    );

    Ok(dtr)
}

/// Convert a time scale to TDB (Barycentric Dynamical Time).
///
/// Implemented for TT and TDB. TT conversion requires observer location; TDB-to-TDB
/// is an identity operation.
///
/// # Methods
///
/// - `to_tdb_greenwich()` - Convert using Greenwich as reference location
/// - `to_tdb_with_location()` - Convert using explicit observer location
/// - `to_tdb_with_location_and_ut1_offset()` - Convert with location and UT1-UTC offset
/// - `to_tdb_with_offset()` - Convert using pre-computed TDB-TT offset in seconds
pub trait ToTDB {
    /// Convert to TDB using Greenwich Observatory as reference.
    fn to_tdb_greenwich(&self) -> TimeResult<TDB>;
    /// Convert to TDB using the specified observer location.
    fn to_tdb_with_location(&self, location: &Location) -> TimeResult<TDB>;
    /// Convert to TDB with location and UT1-UTC offset for maximum precision.
    fn to_tdb_with_location_and_ut1_offset(
        &self,
        location: &Location,
        ut1_offset_seconds: f64,
    ) -> TimeResult<TDB>;
    /// Convert to TDB using a pre-computed offset (TDB-TT) in seconds.
    fn to_tdb_with_offset(&self, dtr_seconds: f64) -> TimeResult<TDB>;
}

impl ToTDB for TDB {
    fn to_tdb_greenwich(&self) -> TimeResult<TDB> {
        Ok(*self)
    }

    fn to_tdb_with_location(&self, _location: &Location) -> TimeResult<TDB> {
        Ok(*self)
    }

    fn to_tdb_with_location_and_ut1_offset(
        &self,
        _location: &Location,
        _ut1_offset_seconds: f64,
    ) -> TimeResult<TDB> {
        Ok(*self)
    }

    fn to_tdb_with_offset(&self, _dtr_seconds: f64) -> TimeResult<TDB> {
        Ok(*self)
    }
}

impl ToTDB for TT {
    fn to_tdb_greenwich(&self) -> TimeResult<TDB> {
        let location = greenwich_location();
        self.to_tdb_with_location_and_ut1_offset(&location, DEFAULT_UT1_OFFSET_SECONDS)
    }

    fn to_tdb_with_location(&self, location: &Location) -> TimeResult<TDB> {
        self.to_tdb_with_location_and_ut1_offset(location, DEFAULT_UT1_OFFSET_SECONDS)
    }

    fn to_tdb_with_location_and_ut1_offset(
        &self,
        location: &Location,
        ut1_offset_seconds: f64,
    ) -> TimeResult<TDB> {
        let tt_jd = self.to_julian_date();

        let tt_f64 = tt_jd.to_f64();
        let ut1_fraction = ((tt_f64 - libm::trunc(tt_f64)) * SECONDS_PER_DAY_F64
            + ut1_offset_seconds)
            / SECONDS_PER_DAY_F64;
        let ut1_fraction = ut1_fraction - libm::floor(ut1_fraction);

        let dtr = compute_tdb_tt_offset(&tt_jd, ut1_fraction, location)?;
        self.to_tdb_with_offset(dtr)
    }

    fn to_tdb_with_offset(&self, dtr_seconds: f64) -> TimeResult<TDB> {
        let tt_jd = self.to_julian_date();

        let dtr_days = dtr_seconds / celestial_core::constants::SECONDS_PER_DAY_F64;

        let (tdb_jd1, tdb_jd2) = if tt_jd.jd1().abs() > tt_jd.jd2().abs() {
            (tt_jd.jd1(), tt_jd.jd2() + dtr_days)
        } else {
            (tt_jd.jd1() + dtr_days, tt_jd.jd2())
        };

        let tdb_jd = JulianDate::new(tdb_jd1, tdb_jd2);
        Ok(TDB::from_julian_date(tdb_jd))
    }
}

impl ToTT for TDB {
    fn to_tt(&self) -> TimeResult<TT> {
        Err(TimeError::ConversionError(
            "TDB→TT conversion requires observer location. \
             Use to_tt_greenwich() for Greenwich or to_tt_with_location() for other locations."
                .to_string(),
        ))
    }
}

/// Convert TDB to TT with location awareness.
///
/// This trait exists because the generic [`ToTT`] trait cannot provide accurate
/// TDB→TT conversion without knowing the observer's location. Rather than
/// silently use a default, the design requires explicit location specification.
///
/// # Methods
///
/// - `to_tt_greenwich()` - Convert using Greenwich as reference location
/// - `to_tt_with_location()` - Convert using explicit observer location
/// - `to_tt_with_location_and_ut1_offset()` - Convert with location and UT1-UTC offset
/// - `to_tt_with_offset()` - Convert using pre-computed TDB-TT offset in seconds
///
/// # Inverse Operation
///
/// The TDB→TT conversion uses iterative refinement because the offset depends on
/// TT (which we're solving for). Three iterations achieve sub-nanosecond convergence.
pub trait ToTTFromTDB {
    /// Convert to TT using Greenwich Observatory as reference.
    fn to_tt_greenwich(&self) -> TimeResult<TT>;
    /// Convert to TT using the specified observer location.
    fn to_tt_with_location(&self, location: &Location) -> TimeResult<TT>;
    /// Convert to TT with location and UT1-UTC offset for maximum precision.
    fn to_tt_with_location_and_ut1_offset(
        &self,
        location: &Location,
        ut1_offset_seconds: f64,
    ) -> TimeResult<TT>;
    /// Convert to TT using a pre-computed offset (TDB-TT) in seconds.
    fn to_tt_with_offset(&self, dtr_seconds: f64) -> TimeResult<TT>;
}

impl ToTTFromTDB for TDB {
    fn to_tt_greenwich(&self) -> TimeResult<TT> {
        let location = greenwich_location();
        self.to_tt_with_location_and_ut1_offset(&location, DEFAULT_UT1_OFFSET_SECONDS)
    }

    fn to_tt_with_location(&self, location: &Location) -> TimeResult<TT> {
        self.to_tt_with_location_and_ut1_offset(location, DEFAULT_UT1_OFFSET_SECONDS)
    }

    fn to_tt_with_location_and_ut1_offset(
        &self,
        location: &Location,
        ut1_offset_seconds: f64,
    ) -> TimeResult<TT> {
        let tdb_jd = self.to_julian_date();

        let tdb_f64 = tdb_jd.to_f64();
        let ut1_fraction_approx = ((tdb_f64 - libm::trunc(tdb_f64)) * SECONDS_PER_DAY_F64
            + ut1_offset_seconds)
            / SECONDS_PER_DAY_F64;
        let ut1_fraction_approx = ut1_fraction_approx - libm::floor(ut1_fraction_approx);

        let dtr_approx = compute_tdb_tt_offset(&tdb_jd, ut1_fraction_approx, location)?;

        let tt_approx = self.to_tt_with_offset(dtr_approx)?;
        let tt_jd_approx = tt_approx.to_julian_date();

        let tt_jd_approx_f64 = tt_jd_approx.jd1() + tt_jd_approx.jd2();
        let ut1_fraction = ((tt_jd_approx_f64 - libm::trunc(tt_jd_approx_f64))
            * SECONDS_PER_DAY_F64
            + ut1_offset_seconds)
            / SECONDS_PER_DAY_F64;
        let ut1_fraction = ut1_fraction - libm::floor(ut1_fraction);

        let dtr_refined = compute_tdb_tt_offset(&tt_jd_approx, ut1_fraction, location)?;

        let tt_refined = self.to_tt_with_offset(dtr_refined)?;
        let tt_jd_refined = tt_refined.to_julian_date();

        let tt_jd_refined_f64 = tt_jd_refined.jd1() + tt_jd_refined.jd2();
        let ut1_fraction_final = ((tt_jd_refined_f64 - libm::trunc(tt_jd_refined_f64))
            * SECONDS_PER_DAY_F64
            + ut1_offset_seconds)
            / SECONDS_PER_DAY_F64;
        let ut1_fraction_final = ut1_fraction_final - libm::floor(ut1_fraction_final);

        let dtr_final = compute_tdb_tt_offset(&tt_jd_refined, ut1_fraction_final, location)?;

        self.to_tt_with_offset(dtr_final)
    }

    fn to_tt_with_offset(&self, dtr_seconds: f64) -> TimeResult<TT> {
        let tdb_jd = self.to_julian_date();

        let dtr_days = dtr_seconds / celestial_core::constants::SECONDS_PER_DAY_F64;

        let (tt_jd1, tt_jd2) = if tdb_jd.jd1().abs() > tdb_jd.jd2().abs() {
            (tdb_jd.jd1(), tdb_jd.jd2() - dtr_days)
        } else {
            (tdb_jd.jd1() - dtr_days, tdb_jd.jd2())
        };

        let tt_jd = JulianDate::new(tt_jd1, tt_jd2);
        Ok(TT::from_julian_date(tt_jd))
    }
}

/// Compute TDB-TT difference using Fairhead & Bretagnon (1990) model.
///
/// This implements the full 787-term series for the TDB-TT difference, including:
/// - Fundamental arguments (Sun, Moon, planets mean longitudes)
/// - Diurnal terms from observer's geocentric position
/// - Jupiter/Saturn secular terms
///
/// # Arguments
///
/// - `date1`, `date2`: Two-part Julian Date
/// - `ut`: UT1 fraction of day (for local sidereal time calculation)
/// - `elong`: Observer's east longitude in radians
/// - `u`, `v`: Geocentric cylindrical coordinates in km (from Location::to_geocentric_km)
///
/// # Returns
///
/// TDB - TT in seconds. Range is approximately -0.00166 to +0.00166 seconds.
fn calculate_tdb_tt_difference(date1: f64, date2: f64, ut: f64, elong: f64, u: f64, v: f64) -> f64 {
    let t = ((date1 - J2000_JD) + date2) / DAYS_PER_JULIAN_MILLENNIUM;

    let tsol = fmod(ut, 1.0) * TWOPI + elong;

    let w = t / 3600.0;

    let elsun = fmod(280.46645683 + 1296027711.03429 * w, 360.0) * DEG_TO_RAD;

    let emsun = fmod(357.52910918 + 1295965810.481 * w, 360.0) * DEG_TO_RAD;

    let d = fmod(297.85019547 + 16029616012.090 * w, 360.0) * DEG_TO_RAD;

    let elj = fmod(34.35151874 + 109306899.89453 * w, 360.0) * DEG_TO_RAD;

    let els = fmod(50.07744430 + 44046398.47038 * w, 360.0) * DEG_TO_RAD;

    let wt = 0.00029e-10 * u * libm::sin(tsol + elsun - els)
        + 0.00100e-10 * u * libm::sin(tsol - 2.0 * emsun)
        + 0.00133e-10 * u * libm::sin(tsol - d)
        + 0.00133e-10 * u * libm::sin(tsol + elsun - elj)
        - 0.00229e-10 * u * libm::sin(tsol + 2.0 * elsun + emsun)
        - 0.02200e-10 * v * libm::cos(elsun + emsun)
        + 0.05312e-10 * u * libm::sin(tsol - emsun)
        - 0.13677e-10 * u * libm::sin(tsol + 2.0 * elsun)
        - 1.31840e-10 * v * libm::cos(elsun)
        + 3.17679e-10 * u * libm::sin(tsol);

    let mut w0 = 0.0;
    for j in (0..474).rev() {
        w0 += FAIRHD[j][0] * libm::sin(FAIRHD[j][1] * t + FAIRHD[j][2]);
    }

    let mut w1 = 0.0;
    for j in (474..679).rev() {
        w1 += FAIRHD[j][0] * libm::sin(FAIRHD[j][1] * t + FAIRHD[j][2]);
    }

    let mut w2 = 0.0;
    for j in (679..764).rev() {
        if FAIRHD[j][0] != 0.0 {
            w2 += FAIRHD[j][0] * libm::sin(FAIRHD[j][1] * t + FAIRHD[j][2]);
        }
    }

    let mut w3 = 0.0;
    for j in (764..784).rev() {
        if FAIRHD[j][0] != 0.0 {
            w3 += FAIRHD[j][0] * libm::sin(FAIRHD[j][1] * t + FAIRHD[j][2]);
        }
    }

    let mut w4 = 0.0;
    for j in (784..787).rev() {
        if FAIRHD[j][0] != 0.0 {
            w4 += FAIRHD[j][0] * libm::sin(FAIRHD[j][1] * t + FAIRHD[j][2]);
        }
    }

    let wf = t * (t * (t * (t * w4 + w3) + w2) + w1) + w0;

    let wj = 0.00065e-6 * libm::sin(6069.776754 * t + 4.021194)
        + 0.00033e-6 * libm::sin(213.299095 * t + 5.543132)
        - 0.00196e-6 * libm::sin(6208.294251 * t + 5.696701)
        - 0.00173e-6 * libm::sin(74.781599 * t + 2.435900)
        + 0.03638e-6 * t * t;

    wt + wf + wj
}

#[cfg(test)]
mod tests {
    use super::*;
    use celestial_core::constants::{DAYS_PER_JULIAN_CENTURY, J2000_JD};

    #[test]
    fn test_identity_conversions() {
        let tdb = TDB::from_julian_date(JulianDate::new(J2000_JD, 0.999999999999999));
        let location = Location::from_degrees(45.0, 90.0, 100.0).unwrap();

        let via_greenwich = tdb.to_tdb_greenwich().unwrap();
        assert_eq!(
            tdb.to_julian_date().jd1(),
            via_greenwich.to_julian_date().jd1(),
            "TDB→TDB via Greenwich should preserve JD1"
        );
        assert_eq!(
            tdb.to_julian_date().jd2(),
            via_greenwich.to_julian_date().jd2(),
            "TDB→TDB via Greenwich should preserve JD2"
        );

        let via_location = tdb.to_tdb_with_location(&location).unwrap();
        assert_eq!(
            tdb.to_julian_date().jd1(),
            via_location.to_julian_date().jd1(),
            "TDB→TDB via location should preserve JD1"
        );
        assert_eq!(
            tdb.to_julian_date().jd2(),
            via_location.to_julian_date().jd2(),
            "TDB→TDB via location should preserve JD2"
        );

        let via_ut1_offset = tdb
            .to_tdb_with_location_and_ut1_offset(&location, 0.3)
            .unwrap();
        assert_eq!(
            tdb.to_julian_date().jd1(),
            via_ut1_offset.to_julian_date().jd1(),
            "TDB→TDB via UT1 offset should preserve JD1"
        );
        assert_eq!(
            tdb.to_julian_date().jd2(),
            via_ut1_offset.to_julian_date().jd2(),
            "TDB→TDB via UT1 offset should preserve JD2"
        );

        let via_offset = tdb.to_tdb_with_offset(0.001).unwrap();
        assert_eq!(
            tdb.to_julian_date().jd1(),
            via_offset.to_julian_date().jd1(),
            "TDB→TDB via offset should preserve JD1"
        );
        assert_eq!(
            tdb.to_julian_date().jd2(),
            via_offset.to_julian_date().jd2(),
            "TDB→TDB via offset should preserve JD2"
        );
    }

    #[test]
    fn test_tt_tdb_offset_verification() {
        let test_cases = [
            (J2000_JD, "J2000.0", greenwich_location()),
            (
                J2000_JD - DAYS_PER_JULIAN_CENTURY,
                "1900",
                greenwich_location(),
            ),
            (J2000_JD + 18262.5, "2050", greenwich_location()),
            (
                J2000_JD + DAYS_PER_JULIAN_CENTURY,
                "2100",
                greenwich_location(),
            ),
            (
                J2000_JD,
                "J2000 Tokyo",
                Location::from_degrees(35.6762, 139.6503, 40.0).unwrap(),
            ),
            (
                J2000_JD,
                "J2000 Sydney",
                Location::from_degrees(-33.8688, 151.2093, 58.0).unwrap(),
            ),
        ];

        for (jd, description, location) in test_cases {
            let tt = TT::from_julian_date(JulianDate::new(jd, 0.0));
            let tdb = tt.to_tdb_with_location(&location).unwrap();

            let diff_seconds = (tdb.to_julian_date().to_f64() - tt.to_julian_date().to_f64())
                * SECONDS_PER_DAY_F64;

            assert!(
                diff_seconds.abs() < 0.002,
                "{}: TT→TDB offset should be < 2ms, got {:.6} seconds",
                description,
                diff_seconds
            );

            let tdb = TDB::from_julian_date(JulianDate::new(jd, 0.0));
            let tt = tdb.to_tt_with_location(&location).unwrap();

            let diff_seconds = (tdb.to_julian_date().to_f64() - tt.to_julian_date().to_f64())
                * SECONDS_PER_DAY_F64;

            assert!(
                diff_seconds.abs() < 0.002,
                "{}: TDB→TT offset should be < 2ms, got {:.6} seconds",
                description,
                diff_seconds
            );
        }
    }

    #[test]
    fn test_tt_tdb_round_trip_precision() {
        const TOLERANCE_DAYS: f64 = 1e-11;

        let test_jd2_values = [0.0, 0.5, 0.123456789012345, -0.123456789012345];

        for jd2 in test_jd2_values {
            let original_tt = TT::from_julian_date(JulianDate::new(J2000_JD, jd2));
            let tdb = original_tt.to_tdb_greenwich().unwrap();
            let round_trip_tt = tdb.to_tt_greenwich().unwrap();

            let jd1_diff =
                (original_tt.to_julian_date().jd1() - round_trip_tt.to_julian_date().jd1()).abs();
            let jd2_diff =
                (original_tt.to_julian_date().jd2() - round_trip_tt.to_julian_date().jd2()).abs();

            assert!(
                jd1_diff < TOLERANCE_DAYS,
                "TT→TDB→TT JD1 diff {} exceeds tolerance for jd2={}",
                jd1_diff,
                jd2
            );
            assert!(
                jd2_diff < TOLERANCE_DAYS,
                "TT→TDB→TT JD2 diff {} exceeds tolerance for jd2={}",
                jd2_diff,
                jd2
            );

            let original_tdb = TDB::from_julian_date(JulianDate::new(J2000_JD, jd2));
            let tt = original_tdb.to_tt_greenwich().unwrap();
            let round_trip_tdb = tt.to_tdb_greenwich().unwrap();

            let jd1_diff =
                (original_tdb.to_julian_date().jd1() - round_trip_tdb.to_julian_date().jd1()).abs();
            let jd2_diff =
                (original_tdb.to_julian_date().jd2() - round_trip_tdb.to_julian_date().jd2()).abs();

            assert!(
                jd1_diff < TOLERANCE_DAYS,
                "TDB→TT→TDB JD1 diff {} exceeds tolerance for jd2={}",
                jd1_diff,
                jd2
            );
            assert!(
                jd2_diff < TOLERANCE_DAYS,
                "TDB→TT→TDB JD2 diff {} exceeds tolerance for jd2={}",
                jd2_diff,
                jd2
            );
        }

        let alt_tt = TT::from_julian_date(JulianDate::new(0.5, J2000_JD));
        let alt_tdb = alt_tt.to_tdb_greenwich().unwrap();
        let alt_round_trip = alt_tdb.to_tt_greenwich().unwrap();

        let jd1_diff =
            (alt_tt.to_julian_date().jd1() - alt_round_trip.to_julian_date().jd1()).abs();
        let jd2_diff =
            (alt_tt.to_julian_date().jd2() - alt_round_trip.to_julian_date().jd2()).abs();

        assert!(
            jd1_diff < TOLERANCE_DAYS,
            "Alternate split TT→TDB→TT JD1 diff {} exceeds tolerance",
            jd1_diff
        );
        assert!(
            jd2_diff < TOLERANCE_DAYS,
            "Alternate split TT→TDB→TT JD2 diff {} exceeds tolerance",
            jd2_diff
        );
    }

    #[test]
    fn test_api_equivalence() {
        let tt = TT::from_julian_date(JulianDate::new(J2000_JD, 0.123456));

        let tdb_greenwich = tt.to_tdb_greenwich().unwrap();
        let tdb_explicit = tt.to_tdb_with_location(&greenwich_location()).unwrap();

        assert_eq!(
            tdb_greenwich.to_julian_date().jd1(),
            tdb_explicit.to_julian_date().jd1(),
            "TT: to_tdb_greenwich() should match to_tdb_with_location(greenwich) JD1"
        );
        assert_eq!(
            tdb_greenwich.to_julian_date().jd2(),
            tdb_explicit.to_julian_date().jd2(),
            "TT: to_tdb_greenwich() should match to_tdb_with_location(greenwich) JD2"
        );

        let tdb = TDB::from_julian_date(JulianDate::new(J2000_JD, 0.987654));

        let tt_greenwich = tdb.to_tt_greenwich().unwrap();
        let tt_explicit = tdb.to_tt_with_location(&greenwich_location()).unwrap();

        assert_eq!(
            tt_greenwich.to_julian_date().jd1(),
            tt_explicit.to_julian_date().jd1(),
            "TDB: to_tt_greenwich() should match to_tt_with_location(greenwich) JD1"
        );
        assert_eq!(
            tt_greenwich.to_julian_date().jd2(),
            tt_explicit.to_julian_date().jd2(),
            "TDB: to_tt_greenwich() should match to_tt_with_location(greenwich) JD2"
        );
    }

    #[test]
    fn test_tdb_to_tt_requires_location() {
        let tdb = TDB::from_julian_date(JulianDate::new(J2000_JD, 0.0));
        let result = tdb.to_tt();

        assert!(result.is_err(), "TDB.to_tt() should return error");

        match result {
            Err(TimeError::ConversionError(msg)) => {
                assert!(
                    msg.contains("location"),
                    "Error message should mention location requirement: {}",
                    msg
                );
                assert!(
                    msg.contains("greenwich") || msg.contains("Greenwich"),
                    "Error message should mention Greenwich option: {}",
                    msg
                );
            }
            _ => panic!("Expected ConversionError, got {:?}", result),
        }
    }
}