use crate::epoch::{J2000_NOON_TJT, J2000_TAI_TJT, SECONDS_PER_DAY, TAI_TT_OFFSET};
use crate::leap_second::LeapSecondTable;
use crate::time_converter_tai_tdb;
use crate::time_converter_tai_tt;
use crate::time_converter_ut1_gmst;
#[derive(Debug, Clone)]
pub struct SimulationTime {
pub tai_seconds: f64,
pub tai_tjt: f64,
pub tai_tjt_at_epoch: f64,
pub utc_seconds: f64,
pub ut1_seconds: f64,
pub tt_seconds: f64,
pub tdb_seconds: f64,
pub gmst_radians: f64,
pub gmst_seconds: f64,
pub gps_seconds: f64,
pub simtime: f64,
pub leap_second_table: LeapSecondTable,
pub ut1_tai_offset: f64,
pub time_scale_factor: f64,
}
impl SimulationTime {
pub fn new(tai_tjt_at_epoch: f64, leap_table: LeapSecondTable) -> Self {
let tai_utc_s = leap_table.tai_utc_at_tai_tjt(tai_tjt_at_epoch);
let ut1_tai_offset = -tai_utc_s;
let mut sim = Self {
tai_seconds: 0.0,
tai_tjt: tai_tjt_at_epoch,
tai_tjt_at_epoch,
utc_seconds: 0.0,
ut1_seconds: 0.0,
tt_seconds: 0.0,
tdb_seconds: 0.0,
gps_seconds: 0.0,
gmst_radians: 0.0,
gmst_seconds: 0.0,
simtime: 0.0,
leap_second_table: leap_table,
ut1_tai_offset,
time_scale_factor: 1.0,
};
sim.recompute_derived();
sim
}
pub fn at_j2000(leap_table: LeapSecondTable) -> Self {
Self::new(J2000_TAI_TJT, leap_table)
}
pub fn set_ut1_tai_offset(&mut self, offset_seconds: f64) {
self.ut1_tai_offset = offset_seconds;
self.recompute_derived();
}
pub fn advance(&mut self, sim_dt: f64) {
assert!(
sim_dt.is_finite() && sim_dt >= 0.0,
"sim_dt must be finite and >= 0, got {sim_dt}"
);
let dyn_dt = sim_dt * self.time_scale_factor;
self.tai_seconds += dyn_dt;
self.tai_tjt = self.tai_tjt_at_epoch + self.tai_seconds / SECONDS_PER_DAY;
self.simtime += sim_dt;
self.recompute_derived();
}
pub fn tdb_julian_date(&self) -> f64 {
let tdb_tai_offset_s = self.tdb_seconds - self.tai_seconds;
let tdb_tjt = self.tai_tjt + tdb_tai_offset_s / SECONDS_PER_DAY;
tdb_tjt + 40_000.0 + 2_400_000.5
}
pub fn tt_tjt(&self) -> f64 {
self.tai_tjt + TAI_TT_OFFSET / SECONDS_PER_DAY
}
pub fn tt_julian_date(&self) -> f64 {
self.tt_tjt() + 40_000.0 + 2_400_000.5
}
#[inline]
pub fn tai(&self) -> crate::SecondsSince<crate::TAI> {
crate::SecondsSince::from_seconds(self.tai_seconds)
}
#[inline]
pub fn utc(&self) -> crate::SecondsSince<crate::UTC> {
crate::SecondsSince::from_seconds(self.utc_seconds)
}
#[inline]
pub fn ut1(&self) -> crate::SecondsSince<crate::UT1> {
crate::SecondsSince::from_seconds(self.ut1_seconds)
}
#[inline]
pub fn tt(&self) -> crate::SecondsSince<crate::TT> {
crate::SecondsSince::from_seconds(self.tt_seconds)
}
#[inline]
pub fn tdb(&self) -> crate::SecondsSince<crate::TDB> {
crate::SecondsSince::from_seconds(self.tdb_seconds)
}
#[inline]
pub fn gps(&self) -> crate::SecondsSince<crate::GPS> {
crate::SecondsSince::from_seconds(self.gps_seconds)
}
#[inline]
pub fn gmst_angle(&self) -> uom::si::f64::Angle {
uom::si::f64::Angle::new::<uom::si::angle::radian>(self.gmst_radians)
}
fn recompute_derived(&mut self) {
self.tt_seconds = time_converter_tai_tt::tai_to_tt(self.tai_seconds);
self.tdb_seconds = time_converter_tai_tdb::tai_to_tdb(self.tai_seconds, self.tai_tjt);
let utc_tjt = self.leap_second_table.tai_to_utc_tjt(self.tai_tjt);
let utc_tjt_at_epoch = self.leap_second_table.tai_to_utc_tjt(self.tai_tjt_at_epoch);
self.utc_seconds = (utc_tjt - utc_tjt_at_epoch) * SECONDS_PER_DAY;
self.gps_seconds = self.tai_seconds - 19.0;
self.ut1_seconds = self.tai_seconds + self.ut1_tai_offset;
let ut1_tjt = self.tai_tjt + self.ut1_tai_offset / SECONDS_PER_DAY;
let du = ut1_tjt - J2000_NOON_TJT;
self.gmst_seconds = time_converter_ut1_gmst::ut1_to_gmst_seconds(du);
self.gmst_radians = time_converter_ut1_gmst::ut1_to_gmst_radians(du);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::leap_second::default_leap_second_table;
use std::f64::consts::PI;
#[test]
fn initial_state_at_j2000() {
let sim = SimulationTime::at_j2000(default_leap_second_table());
assert_eq!(sim.tai_seconds, 0.0);
assert_eq!(sim.simtime, 0.0);
assert!((sim.tt_seconds - 32.184).abs() < 1e-10);
let gmst_deg = sim.gmst_radians * 180.0 / PI;
assert!(
(gmst_deg - 280.19).abs() < 0.05,
"GMST at J2000: {:.4} degrees, expected ~280.19",
gmst_deg
);
}
#[test]
fn advance_increases_all_scales() {
let mut sim = SimulationTime::at_j2000(default_leap_second_table());
let dt = 3600.0; sim.advance(dt);
assert!((sim.tai_seconds - dt).abs() < 1e-15);
assert!((sim.tt_seconds - (dt + 32.184)).abs() < 1e-10);
assert!((sim.simtime - dt).abs() < 1e-15);
}
#[test]
fn tdb_julian_date_at_j2000() {
let sim = SimulationTime::at_j2000(default_leap_second_table());
let jd = sim.tdb_julian_date();
assert!((jd - 2_451_545.0).abs() < 0.001, "TDB JD at J2000: {}", jd);
}
#[test]
fn tt_julian_date_at_j2000() {
let sim = SimulationTime::at_j2000(default_leap_second_table());
let jd = sim.tt_julian_date();
assert!(
(jd - 2_451_545.0).abs() < 1e-8,
"TT JD at J2000: {}, expected 2451545.0",
jd
);
}
#[test]
fn advance_one_day() {
let mut sim = SimulationTime::at_j2000(default_leap_second_table());
let one_day = 86400.0;
sim.advance(one_day);
assert!(
(sim.tai_tjt - sim.tai_tjt_at_epoch - 1.0).abs() < 1e-12,
"TAI TJT should advance by 1 day"
);
let gmst_advance_days = sim.gmst_seconds / 86400.0
- SimulationTime::at_j2000(default_leap_second_table()).gmst_seconds / 86400.0;
assert!(
(gmst_advance_days - 1.00274).abs() < 0.001,
"GMST advance over 1 solar day: {} sidereal days (expected ~1.00274)",
gmst_advance_days
);
}
#[test]
#[should_panic(expected = "must be finite")]
fn advance_nan_panics() {
let mut sim = SimulationTime::at_j2000(default_leap_second_table());
sim.advance(f64::NAN);
}
#[test]
#[should_panic(expected = "must be finite")]
fn advance_inf_panics() {
let mut sim = SimulationTime::at_j2000(default_leap_second_table());
sim.advance(f64::INFINITY);
}
#[test]
fn advance_time_scale_factor_reversal() {
let mut sim = SimulationTime::at_j2000(default_leap_second_table());
let dt = 3600.0;
sim.advance(dt);
let tai_after_forward = sim.tai_seconds;
let gmst_after_forward = sim.gmst_seconds;
sim.time_scale_factor = -1.0;
sim.advance(dt);
assert!(
sim.tai_seconds.abs() < 1e-15,
"tai_seconds should return to 0, got {}",
sim.tai_seconds
);
assert!(
(sim.gmst_seconds - SimulationTime::at_j2000(default_leap_second_table()).gmst_seconds)
.abs()
< 1e-10,
"GMST should return to initial value"
);
assert!(
(sim.gps_seconds - (-19.0)).abs() < 1e-15,
"GPS should return to initial value (-19.0), got {}",
sim.gps_seconds
);
let _ = (tai_after_forward, gmst_after_forward); }
#[test]
fn gps_offset() {
let sim = SimulationTime::at_j2000(default_leap_second_table());
assert!(
(sim.gps_seconds - (-19.0)).abs() < 1e-15,
"GPS at t=0: expected -19.0, got {}",
sim.gps_seconds
);
}
#[test]
fn gps_advances_with_tai() {
let mut sim = SimulationTime::at_j2000(default_leap_second_table());
sim.advance(100.0);
assert!(
(sim.gps_seconds - 81.0).abs() < 1e-15,
"GPS after 100s: expected 81.0, got {}",
sim.gps_seconds
);
}
#[test]
fn typed_getters_match_f64_fields() {
let mut sim = SimulationTime::at_j2000(default_leap_second_table());
sim.advance(1_000_000.0);
assert_eq!(sim.tai().as_seconds(), sim.tai_seconds);
assert_eq!(sim.utc().as_seconds(), sim.utc_seconds);
assert_eq!(sim.ut1().as_seconds(), sim.ut1_seconds);
assert_eq!(sim.tt().as_seconds(), sim.tt_seconds);
assert_eq!(sim.tdb().as_seconds(), sim.tdb_seconds);
assert_eq!(sim.gps().as_seconds(), sim.gps_seconds);
use uom::si::angle::radian;
let gmst_rad = sim.gmst_angle().get::<radian>();
assert_eq!(gmst_rad, sim.gmst_radians);
}
#[test]
fn typed_tai_tt_roundtrip_via_simulation_time() {
use crate::time_converter_tai_tt::{tai_to_tt_typed, tt_to_tai_typed};
let mut sim = SimulationTime::at_j2000(default_leap_second_table());
sim.advance(1_000_000.0);
let tai = sim.tai();
let tt = tai_to_tt_typed(tai);
let back = tt_to_tai_typed(tt);
let err = (back.as_seconds() - tai.as_seconds()).abs();
assert!(
err < 1e-14,
"TAI->TT->TAI typed roundtrip err={} (tolerance 1e-14 s)",
err
);
}
}