From 39e31a118967530b44c6e0fafb214c22bd492c8c Mon Sep 17 00:00:00 2001 From: Angus Morrison Date: Sun, 11 Feb 2024 11:37:16 +0000 Subject: [PATCH] Refactor Time (#56) Refactors Time by making it generic over implementations of the TimeScale marker trait. --- crates/lox-space/examples/iss.rs | 2 +- crates/lox_core/src/coords/states.rs | 52 +- crates/lox_core/src/coords/two_body.rs | 202 ++-- crates/lox_core/src/earth/nutation.rs | 16 +- crates/lox_core/src/time/constants/i64.rs | 4 + crates/lox_core/src/time/continuous.rs | 1198 ++++++++++----------- crates/lox_core/src/time/intervals.rs | 55 - crates/lox_py/src/coords.rs | 36 +- crates/lox_py/src/time.rs | 114 +- crates/lox_py/tests/test_coords.py | 2 +- 10 files changed, 843 insertions(+), 838 deletions(-) diff --git a/crates/lox-space/examples/iss.rs b/crates/lox-space/examples/iss.rs index 8232d3aa..bb75291e 100644 --- a/crates/lox-space/examples/iss.rs +++ b/crates/lox-space/examples/iss.rs @@ -12,7 +12,7 @@ use lox_space::prelude::*; fn main() { let date = Date::new(2016, 5, 30).unwrap(); let utc = UTC::new(12, 0, 0).unwrap(); - let time = Time::from_date_and_utc_timestamp(TimeScale::TDB, date, utc); + let time = Time::from_date_and_utc_timestamp(TDB, date, utc); let position = DVec3::new(6068.27927, -1692.84394, -2516.61918); let velocity = DVec3::new(-0.660415582, 5.495938726, -5.303093233); let iss_cartesian = Cartesian::new(time, Earth, Icrf, position, velocity); diff --git a/crates/lox_core/src/coords/states.rs b/crates/lox_core/src/coords/states.rs index 55244cf8..c0b6ec67 100644 --- a/crates/lox_core/src/coords/states.rs +++ b/crates/lox_core/src/coords/states.rs @@ -10,28 +10,21 @@ use float_eq::float_eq; use glam::{DMat3, DVec3}; use crate::math::{mod_two_pi, normalize_two_pi}; -use crate::time::continuous::Time; pub trait TwoBodyState { - fn time(&self) -> Time; fn to_cartesian_state(&self, grav_param: f64) -> CartesianState; fn to_keplerian_state(&self, grav_param: f64) -> KeplerianState; } -#[derive(Debug, Copy, Clone, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq)] pub struct CartesianState { - time: Time, position: DVec3, velocity: DVec3, } impl CartesianState { - pub fn new(time: Time, position: DVec3, velocity: DVec3) -> Self { - Self { - time, - position, - velocity, - } + pub fn new(position: DVec3, velocity: DVec3) -> Self { + Self { position, velocity } } pub fn position(&self) -> DVec3 { @@ -44,10 +37,6 @@ impl CartesianState { } impl TwoBodyState for CartesianState { - fn time(&self) -> Time { - self.time - } - fn to_cartesian_state(&self, _grav_param: f64) -> CartesianState { *self } @@ -107,7 +96,6 @@ impl TwoBodyState for CartesianState { } KeplerianState::new( - self.time, semi_major, eccentricity, inclination, @@ -118,9 +106,8 @@ impl TwoBodyState for CartesianState { } } -#[derive(Debug, Copy, Clone, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq)] pub struct KeplerianState { - time: Time, semi_major: f64, eccentricity: f64, inclination: f64, @@ -131,7 +118,6 @@ pub struct KeplerianState { impl KeplerianState { pub fn new( - time: Time, semi_major: f64, eccentricity: f64, inclination: f64, @@ -140,7 +126,6 @@ impl KeplerianState { true_anomaly: f64, ) -> Self { Self { - time, semi_major, eccentricity, inclination, @@ -196,16 +181,12 @@ impl KeplerianState { } impl TwoBodyState for KeplerianState { - fn time(&self) -> Time { - self.time - } - fn to_cartesian_state(&self, grav_param: f64) -> CartesianState { let (pos, vel) = self.to_perifocal(grav_param); let rot = DMat3::from_rotation_z(self.ascending_node) * DMat3::from_rotation_x(self.inclination) * DMat3::from_rotation_z(self.periapsis_argument); - CartesianState::new(self.time, rot * pos, rot * vel) + CartesianState::new(rot * pos, rot * vel) } fn to_keplerian_state(&self, _grav_param: f64) -> KeplerianState { @@ -235,13 +216,11 @@ mod tests { use glam::DVec3; use crate::bodies::{Earth, PointMass}; - use crate::time::continuous::TimeScale; use super::*; #[test] fn test_elliptic() { - let time = Time::j2000(TimeScale::TDB); let grav_param = 3.9860047e14; let semi_major = 24464560.0; let eccentricity = 0.7311; @@ -260,11 +239,10 @@ mod tests { -0.118801577532701e4, ); - let cartesian = CartesianState::new(time, pos, vel); + let cartesian = CartesianState::new(pos, vel); assert_eq!(cartesian.to_cartesian_state(grav_param), cartesian); let keplerian = KeplerianState::new( - time, semi_major, eccentricity, inclination, @@ -277,9 +255,6 @@ mod tests { let cartesian1 = keplerian.to_cartesian_state(grav_param); let keplerian1 = cartesian.to_keplerian_state(grav_param); - assert_eq!(cartesian1.time(), time); - assert_eq!(keplerian1.time(), time); - assert_float_eq!(pos.x, cartesian1.position.x, rel <= 1e-8); assert_float_eq!(pos.y, cartesian1.position.y, rel <= 1e-8); assert_float_eq!(pos.z, cartesian1.position.z, rel <= 1e-8); @@ -297,7 +272,6 @@ mod tests { #[test] fn test_circular() { - let time = Time::j2000(TimeScale::TDB); let grav_param = 3.986004418e14; let semi_major = 6778136.6; let eccentricity = 0.0; @@ -307,9 +281,8 @@ mod tests { let true_anomaly = 30f64.to_radians(); let pos = DVec3::new(4396398.60746266, 5083838.45333733, 877155.42119322); let vel = DVec3::new(-5797.06004014, 4716.60916063, 1718.86034246); - let cartesian = CartesianState::new(time, pos, vel); + let cartesian = CartesianState::new(pos, vel); let keplerian = KeplerianState::new( - time, semi_major, eccentricity, inclination, @@ -338,7 +311,6 @@ mod tests { #[test] fn test_circular_orekit() { - let time = Time::j2000(TimeScale::TDB); let grav_param = 3.9860047e14; let semi_major = 24464560.0; let eccentricity = 0.0; @@ -347,7 +319,6 @@ mod tests { let periapsis_arg = 0.0; let true_anomaly = 0.048363; let keplerian = KeplerianState::new( - time, semi_major, eccentricity, inclination, @@ -370,7 +341,6 @@ mod tests { #[test] fn test_hyperbolic_orekit() { - let time = Time::j2000(TimeScale::TDB); let grav_param = 3.9860047e14; let semi_major = -24464560.0; let eccentricity = 1.7311; @@ -379,7 +349,6 @@ mod tests { let periapsis_arg = 3.10686; let true_anomaly = 0.12741601769795755; let keplerian = KeplerianState::new( - time, semi_major, eccentricity, inclination, @@ -402,7 +371,6 @@ mod tests { #[test] fn test_equatorial() { - let time = Time::j2000(TimeScale::TDB); let grav_param = 3.9860047e14; let semi_major = 24464560.0; let eccentricity = 0.7311; @@ -411,7 +379,6 @@ mod tests { let periapsis_arg = 3.10686; let true_anomaly = 0.44369564302687126; let keplerian = KeplerianState::new( - time, semi_major, eccentricity, inclination, @@ -434,7 +401,6 @@ mod tests { #[test] fn test_circular_equatorial() { - let time = Time::j2000(TimeScale::TDB); let grav_param = 3.9860047e14; let semi_major = 24464560.0; let eccentricity = 0.0; @@ -443,7 +409,6 @@ mod tests { let periapsis_arg = 0.0; let true_anomaly = 0.44369564302687126; let keplerian = KeplerianState::new( - time, semi_major, eccentricity, inclination, @@ -466,11 +431,10 @@ mod tests { #[test] fn test_iss() { - let time = Time::j2000(TimeScale::TDB); let position = DVec3::new(6068.27927, -1692.84394, -2516.61918); let velocity = DVec3::new(-0.660415582, 5.495938726, -5.303093233); let grav_param = Earth.gravitational_parameter(); - let cartesian = CartesianState::new(time, position, velocity); + let cartesian = CartesianState::new(position, velocity); let cartesian1 = cartesian .to_keplerian_state(grav_param) .to_cartesian_state(grav_param); diff --git a/crates/lox_core/src/coords/two_body.rs b/crates/lox_core/src/coords/two_body.rs index 4484f669..7447d250 100644 --- a/crates/lox_core/src/coords/two_body.rs +++ b/crates/lox_core/src/coords/two_body.rs @@ -12,45 +12,52 @@ use crate::bodies::PointMass; use crate::coords::states::{CartesianState, KeplerianState, TwoBodyState}; use crate::coords::CoordinateSystem; use crate::frames::{InertialFrame, ReferenceFrame}; -use crate::time::continuous::Time; +use crate::time::continuous::{Time, TimeScale}; -pub trait TwoBody +pub trait TwoBody where - T: PointMass + Copy, - S: InertialFrame + Copy, + T: TimeScale + Copy, + O: PointMass + Copy, + F: InertialFrame + Copy, { - fn to_cartesian(&self) -> Cartesian; + fn time(&self) -> Time; - fn to_keplerian(&self) -> Keplerian; + fn to_cartesian(&self) -> Cartesian; + + fn to_keplerian(&self) -> Keplerian; } -#[derive(Debug, Copy, Clone, PartialEq)] -pub struct Cartesian +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Cartesian where - T: PointMass + Copy, - S: ReferenceFrame + Copy, + T: TimeScale + Copy, + O: PointMass + Copy, + F: ReferenceFrame + Copy, { + time: Time, state: CartesianState, - origin: T, - frame: S, + origin: O, + frame: F, } -impl Cartesian +impl Cartesian where - T: PointMass + Copy, - S: ReferenceFrame + Copy, + T: TimeScale + Copy, + O: PointMass + Copy, + F: ReferenceFrame + Copy, { - pub fn new(time: Time, origin: T, frame: S, position: DVec3, velocity: DVec3) -> Self { - let state = CartesianState::new(time, position, velocity); + pub fn new(time: Time, origin: O, frame: F, position: DVec3, velocity: DVec3) -> Self { + let state = CartesianState::new(position, velocity); Self { + time, state, origin, frame, } } - pub fn time(&self) -> Time { - self.state.time() + pub fn time(&self) -> Time { + self.time } pub fn position(&self) -> DVec3 { @@ -62,27 +69,33 @@ where } } -impl TwoBody for Cartesian +impl TwoBody for Cartesian where - T: PointMass + Copy, - S: InertialFrame + Copy, + T: TimeScale + Copy, + O: PointMass + Copy, + F: InertialFrame + Copy, { - fn to_cartesian(&self) -> Cartesian { + fn time(&self) -> Time { + self.time + } + + fn to_cartesian(&self) -> Cartesian { *self } - fn to_keplerian(&self) -> Keplerian { + fn to_keplerian(&self) -> Keplerian { Keplerian::from(*self) } } -impl CoordinateSystem for Cartesian +impl CoordinateSystem for Cartesian where - T: PointMass + Copy, - S: ReferenceFrame + Copy, + T: TimeScale + Copy, + O: PointMass + Copy, + F: ReferenceFrame + Copy, { - type Origin = T; - type Frame = S; + type Origin = O; + type Frame = F; fn origin(&self) -> Self::Origin { self.origin @@ -93,43 +106,48 @@ where } } -impl From> for Cartesian +impl From> for Cartesian where - T: PointMass + Copy, - S: InertialFrame + Copy, + T: TimeScale + Copy, + O: PointMass + Copy, + F: InertialFrame + Copy, { - fn from(keplerian: Keplerian) -> Self { + fn from(keplerian: Keplerian) -> Self { let grav_param = keplerian.origin.gravitational_parameter(); let state = keplerian.state.to_cartesian_state(grav_param); Cartesian { state, + time: keplerian.time(), origin: keplerian.origin, frame: keplerian.frame, } } } -#[derive(Debug, Copy, Clone, PartialEq)] -pub struct Keplerian +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Keplerian where - T: PointMass + Copy, - S: InertialFrame + Copy, + T: TimeScale + Copy, + O: PointMass + Copy, + F: InertialFrame + Copy, { + time: Time, state: KeplerianState, - origin: T, - frame: S, + origin: O, + frame: F, } -impl Keplerian +impl Keplerian where - T: PointMass + Copy, - S: InertialFrame + Copy, + T: TimeScale + Copy, + O: PointMass + Copy, + F: InertialFrame + Copy, { #[allow(clippy::too_many_arguments)] pub fn new( - time: Time, - origin: T, - frame: S, + time: Time, + origin: O, + frame: F, semi_major: f64, eccentricity: f64, inclination: f64, @@ -138,7 +156,6 @@ where true_anomaly: f64, ) -> Self { let state = KeplerianState::new( - time, semi_major, eccentricity, inclination, @@ -147,14 +164,15 @@ where true_anomaly, ); Self { + time, state, origin, frame, } } - pub fn time(&self) -> Time { - self.state.time() + pub fn time(&self) -> Time { + self.time } pub fn semi_major_axis(&self) -> f64 { @@ -182,27 +200,33 @@ where } } -impl TwoBody for Keplerian +impl TwoBody for Keplerian where - T: PointMass + Copy, - S: InertialFrame + Copy, + T: TimeScale + Copy, + O: PointMass + Copy, + F: InertialFrame + Copy, { - fn to_cartesian(&self) -> Cartesian { + fn time(&self) -> Time { + self.time + } + + fn to_cartesian(&self) -> Cartesian { Cartesian::from(*self) } - fn to_keplerian(&self) -> Keplerian { + fn to_keplerian(&self) -> Keplerian { *self } } -impl CoordinateSystem for Keplerian +impl CoordinateSystem for Keplerian where - T: PointMass + Copy, - S: InertialFrame + Copy, + T: TimeScale + Copy, + O: PointMass + Copy, + F: InertialFrame + Copy, { - type Origin = T; - type Frame = S; + type Origin = O; + type Frame = F; fn origin(&self) -> Self::Origin { self.origin @@ -213,16 +237,18 @@ where } } -impl From> for Keplerian +impl From> for Keplerian where - T: PointMass + Copy, - S: InertialFrame + Copy, + T: TimeScale + Copy, + O: PointMass + Copy, + F: InertialFrame + Copy, { - fn from(cartesian: Cartesian) -> Self { + fn from(cartesian: Cartesian) -> Self { let grav_param = cartesian.origin.gravitational_parameter(); let state = cartesian.state.to_keplerian_state(grav_param); Self { state, + time: cartesian.time, origin: cartesian.origin, frame: cartesian.frame, } @@ -236,7 +262,7 @@ mod tests { use super::*; use crate::bodies::Earth; use crate::frames::Icrf; - use crate::time::continuous::{Time, TimeScale}; + use crate::time::continuous::{Time, TDB}; use crate::time::dates::Date; use crate::time::utc::UTC; @@ -244,7 +270,7 @@ mod tests { fn test_cartesian() { let date = Date::new(2023, 3, 25).expect("Date should be valid"); let utc = UTC::new(21, 8, 0).expect("Time should be valid"); - let time = Time::from_date_and_utc_timestamp(TimeScale::TDB, date, utc); + let time = Time::from_date_and_utc_timestamp(TDB, date, utc); let pos = DVec3::new( -0.107622532467967e7, -0.676589636432773e7, @@ -273,11 +299,31 @@ mod tests { assert_float_eq!(cartesian.velocity().z, cartesian1.velocity().z, rel <= 1e-6); } + #[test] + fn test_cartesian_two_body_time() { + let date = Date::new(2023, 3, 25).expect("Date should be valid"); + let utc = UTC::new(21, 8, 0).expect("Time should be valid"); + let time = Time::from_date_and_utc_timestamp(TDB, date, utc); + let pos = DVec3::new( + -0.107622532467967e7, + -0.676589636432773e7, + -0.332308783350379e6, + ) * 1e-3; + let vel = DVec3::new( + 0.935685775154103e4, + -0.331234775037644e4, + -0.118801577532701e4, + ) * 1e-3; + + let cartesian = Cartesian::new(time, Earth, Icrf, pos, vel); + assert_eq!(TwoBody::time(&cartesian), time); + } + #[test] fn test_keplerian() { let date = Date::new(2023, 3, 25).expect("Date should be valid"); let utc = UTC::new(21, 8, 0).expect("Time should be valid"); - let time = Time::from_date_and_utc_timestamp(TimeScale::TDB, date, utc); + let time = Time::from_date_and_utc_timestamp(TDB, date, utc); let semi_major = 24464560.0e-3; let eccentricity = 0.7311; let inclination = 0.122138; @@ -335,4 +381,30 @@ mod tests { rel <= 1e-6 ); } + + #[test] + fn test_keplerian_two_body_time() { + let date = Date::new(2023, 3, 25).expect("Date should be valid"); + let utc = UTC::new(21, 8, 0).expect("Time should be valid"); + let time = Time::from_date_and_utc_timestamp(TDB, date, utc); + let semi_major = 24464560.0e-3; + let eccentricity = 0.7311; + let inclination = 0.122138; + let ascending_node = 1.00681; + let periapsis_arg = 3.10686; + let true_anomaly = 0.44369564302687126; + + let keplerian = Keplerian::new( + time, + Earth, + Icrf, + semi_major, + eccentricity, + inclination, + ascending_node, + periapsis_arg, + true_anomaly, + ); + assert_eq!(TwoBody::time(&keplerian), time); + } } diff --git a/crates/lox_core/src/earth/nutation.rs b/crates/lox_core/src/earth/nutation.rs index 9500a694..f9805316 100644 --- a/crates/lox_core/src/earth/nutation.rs +++ b/crates/lox_core/src/earth/nutation.rs @@ -16,8 +16,7 @@ use crate::earth::nutation::iau2000::nutation_iau2000a; use crate::earth::nutation::iau2000::nutation_iau2000b; use crate::earth::nutation::iau2006::nutation_iau2006a; use crate::math::RADIANS_IN_ARCSECOND; -use crate::time::continuous::Time; -use crate::time::intervals::tdb_julian_centuries_since_j2000; +use crate::time::continuous::{Time, TDB}; use crate::types::Radians; mod iau1980; @@ -64,8 +63,8 @@ impl Add<&Self> for Nutation { } /// Calculate nutation coefficients at `time` using the given [Model]. -pub fn nutation(model: Model, time: Time) -> Nutation { - let t = tdb_julian_centuries_since_j2000(time); +pub fn nutation(model: Model, time: Time) -> Nutation { + let t = time.centuries_since_j2000(); match model { Model::IAU1980 => nutation_iau1980(t), Model::IAU2000A => nutation_iau2000a(t), @@ -98,7 +97,6 @@ fn point1_microarcsec_to_rad(p1_uas: Point1Microarcsec) -> Radians { #[cfg(test)] mod tests { - use crate::time::continuous::TimeScale; use float_eq::assert_float_eq; use super::*; @@ -107,7 +105,7 @@ mod tests { #[test] fn test_nutation_iau1980() { - let time = Time::j2000(TimeScale::TT); + let time = Time::j2000(TDB); let expected = Nutation { longitude: -0.00006750247617532478, obliquity: -0.00002799221238377013, @@ -118,7 +116,7 @@ mod tests { } #[test] fn test_nutation_iau2000a() { - let time = Time::j2000(TimeScale::TT); + let time = Time::j2000(TDB); let expected = Nutation { longitude: -0.00006754422426417299, obliquity: -0.00002797083119237414, @@ -130,7 +128,7 @@ mod tests { #[test] fn test_nutation_iau2000b() { - let time = Time::j2000(TimeScale::TT); + let time = Time::j2000(TDB); let expected = Nutation { longitude: -0.00006754261253992235, obliquity: -0.00002797092331098565, @@ -142,7 +140,7 @@ mod tests { #[test] fn test_nutation_iau2006a() { - let time = Time::j2000(TimeScale::TT); + let time = Time::j2000(TDB); let expected = Nutation { longitude: -0.00006754425598969513, obliquity: -0.00002797083119237414, diff --git a/crates/lox_core/src/time/constants/i64.rs b/crates/lox_core/src/time/constants/i64.rs index 65644b7e..6701ee97 100644 --- a/crates/lox_core/src/time/constants/i64.rs +++ b/crates/lox_core/src/time/constants/i64.rs @@ -14,6 +14,10 @@ pub const SECONDS_PER_DAY: i64 = 24 * SECONDS_PER_HOUR; pub const SECONDS_PER_HALF_DAY: i64 = SECONDS_PER_DAY / 2; +pub const SECONDS_PER_JULIAN_YEAR: i64 = 31_557_600; + +pub const SECONDS_PER_JULIAN_CENTURY: i64 = SECONDS_PER_JULIAN_YEAR * 100; + pub const MILLISECONDS_PER_SECOND: i64 = 1_000; pub const MICROSECONDS_PER_SECOND: i64 = 1_000_000; diff --git a/crates/lox_core/src/time/continuous.rs b/crates/lox_core/src/time/continuous.rs index e482802a..de08aa6b 100644 --- a/crates/lox_core/src/time/continuous.rs +++ b/crates/lox_core/src/time/continuous.rs @@ -16,8 +16,9 @@ use std::fmt; use std::fmt::{Display, Formatter}; use std::ops::{Add, Sub}; -use num::{abs, ToPrimitive}; +use num::abs; +use crate::time::constants::f64::DAYS_PER_JULIAN_CENTURY; use crate::time::constants::i64::{ SECONDS_PER_DAY, SECONDS_PER_HALF_DAY, SECONDS_PER_HOUR, SECONDS_PER_MINUTE, }; @@ -38,16 +39,16 @@ pub struct TimeDelta { } #[derive(Debug, Default, Copy, Clone, Eq, PartialEq)] -/// `RawTime` is the base time representation for time scales without leap seconds. It is measured relative to -/// J2000. `RawTime::default()` represents the epoch itself. +/// `UnscaledTime` is the base time representation for time scales without leap seconds. It is measured relative to +/// J2000. `UnscaledTime::default()` represents the epoch itself. /// -/// `RawTime` has attosecond precision, and supports times within 292 billion years either side of the epoch. -pub struct RawTime { +/// `UnscaledTime` has attosecond precision, and supports times within 292 billion years either side of the epoch. +pub struct UnscaledTime { // The sign of the time is determined exclusively by the sign of the `second` field. `attoseconds` is always the // positive count of attoseconds since the last whole second. For example, one attosecond before the epoch is // represented as // ``` - // let time = RawTime { + // let time = UnscaledTime { // seconds: -1, // attoseconds: ATTOSECONDS_PER_SECOND - 1, // }; @@ -56,11 +57,91 @@ pub struct RawTime { attoseconds: u64, } -impl RawTime { +impl UnscaledTime { fn is_negative(&self) -> bool { self.seconds < 0 } + pub fn seconds(&self) -> i64 { + self.seconds + } + + pub fn attoseconds(&self) -> u64 { + self.attoseconds + } + + /// The fractional number of Julian days since J2000. + pub fn days_since_j2000(&self) -> f64 { + let d1 = self.seconds as f64 / constants::f64::SECONDS_PER_DAY; + let d2 = self.attoseconds as f64 / constants::f64::ATTOSECONDS_PER_DAY; + d2 + d1 + } + + /// The fractional number of Julian centuries since J2000. + pub fn centuries_since_j2000(&self) -> f64 { + self.days_since_j2000() / DAYS_PER_JULIAN_CENTURY + } +} + +impl Display for UnscaledTime { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!( + f, + "{:02}:{:02}:{:02}.{:03}.{:03}.{:03}.{:03}.{:03}.{:03}", + self.hour(), + self.minute(), + self.second(), + self.millisecond(), + self.microsecond(), + self.nanosecond(), + self.picosecond(), + self.femtosecond(), + self.attosecond(), + ) + } +} + +impl Add for UnscaledTime { + type Output = Self; + + /// The implementation of [Add] for [UnscaledTime] follows the default Rust rules for integer overflow, which + /// should be sufficient for all practical purposes. + fn add(self, rhs: TimeDelta) -> Self::Output { + let mut attoseconds = self.attoseconds + rhs.attoseconds; + let mut seconds = self.seconds + rhs.seconds as i64; + if attoseconds >= ATTOSECONDS_PER_SECOND { + seconds += 1; + attoseconds -= ATTOSECONDS_PER_SECOND; + } + Self { + seconds, + attoseconds, + } + } +} + +impl Sub for UnscaledTime { + type Output = Self; + + /// The implementation of [Sub] for [UnscaledTime] follows the default Rust rules for integer overflow, which + /// should be sufficient for all practical purposes. + fn sub(self, rhs: TimeDelta) -> Self::Output { + let mut seconds = self.seconds - rhs.seconds as i64; + let mut attoseconds = self.attoseconds; + if rhs.attoseconds > self.attoseconds { + seconds -= 1; + attoseconds = ATTOSECONDS_PER_SECOND - (rhs.attoseconds - self.attoseconds); + } else { + attoseconds -= rhs.attoseconds; + } + Self { + seconds, + attoseconds, + } + } +} + +impl WallClock for UnscaledTime { fn hour(&self) -> i64 { // Since J2000 is taken from midday, we offset by half a day to get the wall clock hour. let day_seconds: i64 = if self.is_negative() { @@ -113,471 +194,238 @@ impl RawTime { } } -impl Add for RawTime { - type Output = Self; - - /// The implementation of [Add] for [RawTime] follows the default Rust rules for integer overflow, which - /// should be sufficient for all practical purposes. - fn add(self, rhs: TimeDelta) -> Self::Output { - let mut attoseconds = self.attoseconds + rhs.attoseconds; - let mut seconds = self.seconds + rhs.seconds as i64; - if attoseconds >= ATTOSECONDS_PER_SECOND { - seconds += 1; - attoseconds -= ATTOSECONDS_PER_SECOND; - } - Self { - seconds, - attoseconds, - } - } +/// Marker trait with associated constants denoting a continuous astronomical time scale. +pub trait TimeScale { + const ABBREVIATION: &'static str; + const NAME: &'static str; } -impl Sub for RawTime { - type Output = Self; +/// International Atomic Time. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct TAI; - /// The implementation of [Sub] for [RawTime] follows the default Rust rules for integer overflow, which - /// should be sufficient for all practical purposes. - fn sub(self, rhs: TimeDelta) -> Self::Output { - let mut seconds = self.seconds - rhs.seconds as i64; - let mut attoseconds = self.attoseconds; - if rhs.attoseconds > self.attoseconds { - seconds -= 1; - attoseconds = ATTOSECONDS_PER_SECOND - (rhs.attoseconds - self.attoseconds); - } else { - attoseconds -= rhs.attoseconds; - } - Self { - seconds, - attoseconds, - } - } +impl TimeScale for TAI { + const ABBREVIATION: &'static str = "TAI"; + const NAME: &'static str = "International Atomic Time"; } -/// The continuous time scales supported by Lox. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum TimeScale { - TAI, - TCB, - TCG, - TDB, - TT, - UT1, -} +/// Barycentric Coordinate Time. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct TCB; -impl Display for TimeScale { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "{}", Into::<&str>::into(*self)) - } +impl TimeScale for TCB { + const ABBREVIATION: &'static str = "TCB"; + const NAME: &'static str = "Barycentric Coordinate Time"; } -#[allow(clippy::from_over_into)] // Into is infallible, but From is not -impl Into<&str> for TimeScale { - fn into(self) -> &'static str { - match self { - TimeScale::TAI => "TAI", - TimeScale::TCB => "TCB", - TimeScale::TCG => "TCG", - TimeScale::TDB => "TDB", - TimeScale::TT => "TT", - TimeScale::UT1 => "UT1", - } - } -} +/// Geocentric Coordinate Time. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct TCG; -/// CalendarDate allows continuous time formats to report their date in their respective calendar. -pub trait CalendarDate { - fn date(&self) -> Date; +impl TimeScale for TCG { + const ABBREVIATION: &'static str = "TCG"; + const NAME: &'static str = "Geocentric Coordinate Time"; } -/// International Atomic Time. Defaults to the J2000 epoch. -#[derive(Debug, Copy, Default, Clone, Eq, PartialEq)] -pub struct TAI(RawTime); - -impl TAI { - pub fn to_ut1(&self, _dut: TimeDelta, _dat: TimeDelta) -> UT1 { - todo!() - } -} +/// Barycentric Dynamical Time. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct TDB; -impl CalendarDate for TAI { - fn date(&self) -> Date { - todo!() - } +impl TimeScale for TDB { + const ABBREVIATION: &'static str = "TDB"; + const NAME: &'static str = "Barycentric Dynamical Time"; } -/// Barycentric Coordinate Time. Defaults to the J2000 epoch. -#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)] -pub struct TCB(RawTime); - -/// Geocentric Coordinate Time. Defaults to the J2000 epoch. -#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)] -pub struct TCG(RawTime); +/// Terrestrial Time. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct TT; -/// Barycentric Dynamical Time. Defaults to the J2000 epoch. -#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)] -pub struct TDB(RawTime); +impl TimeScale for TT { + const ABBREVIATION: &'static str = "TT"; + const NAME: &'static str = "Terrestrial Time"; +} -/// Terrestrial Time. Defaults to the J2000 epoch. -#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)] -pub struct TT(RawTime); +/// Universal Time. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct UT1; -/// Universal Time. Defaults to the J2000 epoch. -#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)] -pub struct UT1(RawTime); - -/// Implements the `WallClock` trait for the a time scale based on [RawTime] in terms of the underlying -/// raw time. -macro_rules! wall_clock { - ($time_scale:ident, $test_module:ident) => { - impl WallClock for $time_scale { - fn hour(&self) -> i64 { - self.0.hour() - } - - fn minute(&self) -> i64 { - self.0.minute() - } - - fn second(&self) -> i64 { - self.0.second() - } - - fn millisecond(&self) -> i64 { - self.0.millisecond() - } - - fn microsecond(&self) -> i64 { - self.0.microsecond() - } - - fn nanosecond(&self) -> i64 { - self.0.nanosecond() - } - - fn picosecond(&self) -> i64 { - self.0.picosecond() - } - - fn femtosecond(&self) -> i64 { - self.0.femtosecond() - } - - fn attosecond(&self) -> i64 { - self.0.attosecond() - } - } +impl TimeScale for UT1 { + const ABBREVIATION: &'static str = "UT1"; + const NAME: &'static str = "Universal Time"; +} - #[cfg(test)] - mod $test_module { - use super::{$time_scale, RawTime}; - use crate::time::WallClock; - - const RAW_TIME: RawTime = RawTime { - seconds: 1234, - attoseconds: 5678, - }; - - const TIME: $time_scale = $time_scale(RAW_TIME); - - #[test] - fn test_hour_delegation() { - assert_eq!(TIME.hour(), RAW_TIME.hour()); - } - - #[test] - fn test_minute_delegation() { - assert_eq!(TIME.minute(), RAW_TIME.minute()); - } - - #[test] - fn test_second_delegation() { - assert_eq!(TIME.second(), RAW_TIME.second()); - } - - #[test] - fn test_millisecond_delegation() { - assert_eq!(TIME.millisecond(), RAW_TIME.millisecond()); - } - - #[test] - fn test_microsecond_delegation() { - assert_eq!(TIME.microsecond(), RAW_TIME.microsecond()); - } - - #[test] - fn test_nanosecond_delegation() { - assert_eq!(TIME.nanosecond(), RAW_TIME.nanosecond()); - } - - #[test] - fn test_picosecond_delegation() { - assert_eq!(TIME.picosecond(), RAW_TIME.picosecond()); - } - - #[test] - fn test_femtosecond_delegation() { - assert_eq!(TIME.femtosecond(), RAW_TIME.femtosecond()); - } - - #[test] - fn test_attosecond_delegation() { - assert_eq!(TIME.attosecond(), RAW_TIME.attosecond()); - } - } - }; +/// An instant in time in a given time scale. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct Time { + scale: T, + timestamp: UnscaledTime, } -// Implement WallClock for all continuous time scales. -wall_clock!(TAI, tai_wall_clock_tests); -wall_clock!(TCB, tcb_wall_clock_tests); -wall_clock!(TCG, tcg_wall_clock_tests); -wall_clock!(TDB, tdb_wall_clock_tests); -wall_clock!(TT, tt_wall_clock_tests); -wall_clock!(UT1, ut1_wall_clock_tests); +impl Time { + /// Instantiates a [Time] in the given scale from seconds and attoseconds since the epoch. + pub fn new(scale: T, seconds: i64, attoseconds: u64) -> Self { + Self { + scale, + timestamp: UnscaledTime { + seconds, + attoseconds, + }, + } + } -/// `Time` represents a time in any of the supported continuous timescales. -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub enum Time { - TAI(TAI), - TCB(TCB), - TCG(TCG), - TDB(TDB), - TT(TT), - UT1(UT1), -} + /// Instantiates a [Time] in the given scale from an [UnscaledTime]. + pub fn from_unscaled(scale: T, timestamp: UnscaledTime) -> Self { + Self { scale, timestamp } + } -impl Time { - /// Instantiates a `Time` of the given scale from a date and UTC timestamp. - pub fn from_date_and_utc_timestamp(scale: TimeScale, date: Date, time: UTC) -> Self { + /// Instantiates a [Time] in the given scale from a date and UTC timestamp. + pub fn from_date_and_utc_timestamp(scale: T, date: Date, time: UTC) -> Self { let day_in_seconds = date.j2000() * SECONDS_PER_DAY - SECONDS_PER_DAY / 2; let hour_in_seconds = time.hour() * SECONDS_PER_HOUR; let minute_in_seconds = time.minute() * SECONDS_PER_MINUTE; let seconds = day_in_seconds + hour_in_seconds + minute_in_seconds + time.second(); let attoseconds = time.subsecond_as_attoseconds(); - let raw = RawTime { + let unscaled = UnscaledTime { seconds, attoseconds, }; - Self::from_raw(scale, raw) + Self::from_unscaled(scale, unscaled) } - /// Instantiates a `Time` of the given scale from a UTC datetime. - pub fn from_utc_datetime(scale: TimeScale, dt: UTCDateTime) -> Self { + /// Instantiates a [Time] in the given scale from a UTC datetime. + pub fn from_utc_datetime(scale: T, dt: UTCDateTime) -> Self { Self::from_date_and_utc_timestamp(scale, dt.date(), dt.time()) } - pub fn scale(&self) -> TimeScale { - match &self { - Time::TAI(_) => TimeScale::TAI, - Time::TCB(_) => TimeScale::TCB, - Time::TCG(_) => TimeScale::TCG, - Time::TDB(_) => TimeScale::TDB, - Time::TT(_) => TimeScale::TT, - Time::UT1(_) => TimeScale::UT1, - } - } - /// Returns the J2000 epoch in the given timescale. - pub fn j2000(scale: TimeScale) -> Self { - Self::from_raw(scale, RawTime::default()) + pub fn j2000(scale: T) -> Self { + Self { + scale, + timestamp: UnscaledTime::default(), + } } /// Returns, as an epoch in the given timescale, midday on the first day of the proleptic Julian /// calendar. - pub fn jd0(scale: TimeScale) -> Self { + pub fn jd0(scale: T) -> Self { // This represents 4713 BC, since there is no year 0 separating BC and AD. let first_proleptic_day = Date::new_unchecked(ProlepticJulian, -4712, 1, 1); let midday = UTC::new(12, 0, 0).expect("midday should be a valid time"); Self::from_date_and_utc_timestamp(scale, first_proleptic_day, midday) } - fn from_raw(scale: TimeScale, raw: RawTime) -> Self { - match scale { - TimeScale::TAI => Time::TAI(TAI(raw)), - TimeScale::TCB => Time::TCB(TCB(raw)), - TimeScale::TCG => Time::TCG(TCG(raw)), - TimeScale::TDB => Time::TDB(TDB(raw)), - TimeScale::TT => Time::TT(TT(raw)), - TimeScale::UT1 => Time::UT1(UT1(raw)), - } - } - - fn raw(&self) -> RawTime { - match self { - Time::TAI(tai) => tai.0, - Time::TCB(tcb) => tcb.0, - Time::TCG(tcg) => tcg.0, - Time::TDB(tdb) => tdb.0, - Time::TT(tt) => tt.0, - Time::UT1(ut1) => ut1.0, - } + /// The underlying unscaled timestamp. + pub fn unscaled(&self) -> UnscaledTime { + self.timestamp } /// The number of whole seconds since J2000. pub fn seconds(&self) -> i64 { - self.raw().seconds + self.timestamp.seconds } /// The number of attoseconds from the last whole second. pub fn attoseconds(&self) -> u64 { - self.raw().attoseconds + self.timestamp.attoseconds } /// The fractional number of Julian days since J2000. pub fn days_since_j2000(&self) -> f64 { - let d1 = self.seconds().to_f64().unwrap_or_default() / constants::f64::SECONDS_PER_DAY; - let d2 = self.attoseconds().to_f64().unwrap() / constants::f64::ATTOSECONDS_PER_DAY; - d2 + d1 + self.timestamp.days_since_j2000() + } + + /// The fractional number of Julian centuries since J2000. + pub fn centuries_since_j2000(&self) -> f64 { + self.timestamp.centuries_since_j2000() } } -impl Display for Time { +impl Display for Time { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!( - f, - "{:02}:{:02}:{:02}.{:03}.{:03}.{:03}.{:03}.{:03}.{:03} {}", - self.hour(), - self.minute(), - self.second(), - self.millisecond(), - self.microsecond(), - self.nanosecond(), - self.picosecond(), - self.femtosecond(), - self.attosecond(), - self.scale(), - ) + write!(f, "{} {}", self.timestamp, T::ABBREVIATION) + } +} + +impl Add for Time { + type Output = Self; + + fn add(self, rhs: TimeDelta) -> Self::Output { + Self::from_unscaled(self.scale, self.timestamp + rhs) + } +} + +impl Sub for Time { + type Output = Self; + + fn sub(self, rhs: TimeDelta) -> Self::Output { + Self::from_unscaled(self.scale, self.timestamp - rhs) } } -impl WallClock for Time { +impl WallClock for Time { fn hour(&self) -> i64 { - match self { - Time::TAI(t) => t.hour(), - Time::TCB(t) => t.hour(), - Time::TCG(t) => t.hour(), - Time::TDB(t) => t.hour(), - Time::TT(t) => t.hour(), - Time::UT1(t) => t.hour(), - } + self.timestamp.hour() } fn minute(&self) -> i64 { - match self { - Time::TAI(t) => t.minute(), - Time::TCB(t) => t.minute(), - Time::TCG(t) => t.minute(), - Time::TDB(t) => t.minute(), - Time::TT(t) => t.minute(), - Time::UT1(t) => t.minute(), - } + self.timestamp.minute() } fn second(&self) -> i64 { - match self { - Time::TAI(t) => t.second(), - Time::TCB(t) => t.second(), - Time::TCG(t) => t.second(), - Time::TDB(t) => t.second(), - Time::TT(t) => t.second(), - Time::UT1(t) => t.second(), - } + self.timestamp.second() } fn millisecond(&self) -> i64 { - match self { - Time::TAI(t) => t.millisecond(), - Time::TCB(t) => t.millisecond(), - Time::TCG(t) => t.millisecond(), - Time::TDB(t) => t.millisecond(), - Time::TT(t) => t.millisecond(), - Time::UT1(t) => t.millisecond(), - } + self.timestamp.millisecond() } fn microsecond(&self) -> i64 { - match self { - Time::TAI(t) => t.microsecond(), - Time::TCB(t) => t.microsecond(), - Time::TCG(t) => t.microsecond(), - Time::TDB(t) => t.microsecond(), - Time::TT(t) => t.microsecond(), - Time::UT1(t) => t.microsecond(), - } + self.timestamp.microsecond() } fn nanosecond(&self) -> i64 { - match self { - Time::TAI(t) => t.nanosecond(), - Time::TCB(t) => t.nanosecond(), - Time::TCG(t) => t.nanosecond(), - Time::TDB(t) => t.nanosecond(), - Time::TT(t) => t.nanosecond(), - Time::UT1(t) => t.nanosecond(), - } + self.timestamp.nanosecond() } fn picosecond(&self) -> i64 { - match self { - Time::TAI(t) => t.picosecond(), - Time::TCB(t) => t.picosecond(), - Time::TCG(t) => t.picosecond(), - Time::TDB(t) => t.picosecond(), - Time::TT(t) => t.picosecond(), - Time::UT1(t) => t.picosecond(), - } + self.timestamp.picosecond() } fn femtosecond(&self) -> i64 { - match self { - Time::TAI(t) => t.femtosecond(), - Time::TCB(t) => t.femtosecond(), - Time::TCG(t) => t.femtosecond(), - Time::TDB(t) => t.femtosecond(), - Time::TT(t) => t.femtosecond(), - Time::UT1(t) => t.femtosecond(), - } + self.timestamp.femtosecond() } fn attosecond(&self) -> i64 { - match self { - Time::TAI(t) => t.attosecond(), - Time::TCB(t) => t.attosecond(), - Time::TCG(t) => t.attosecond(), - Time::TDB(t) => t.attosecond(), - Time::TT(t) => t.attosecond(), - Time::UT1(t) => t.attosecond(), - } + self.timestamp.attosecond() } } +/// CalendarDate allows continuous time formats to report their date in their respective calendar. +pub trait CalendarDate { + fn date(&self) -> Date; +} + #[cfg(test)] mod tests { - use super::*; + use crate::time::constants::i64::SECONDS_PER_JULIAN_CENTURY; use crate::time::dates::Calendar::Gregorian; + use float_eq::assert_float_eq; - const TIME_SCALES: [TimeScale; 6] = [ - TimeScale::TAI, - TimeScale::TCB, - TimeScale::TCG, - TimeScale::TDB, - TimeScale::TT, - TimeScale::UT1, - ]; + use super::*; #[test] - fn test_raw_time_is_negative() { - assert!(RawTime { + fn test_unscaled_time_is_negative() { + assert!(UnscaledTime { seconds: -1, attoseconds: 0 } .is_negative()); - assert!(!RawTime { + assert!(!UnscaledTime { seconds: 0, attoseconds: 0 } .is_negative()); - assert!(!RawTime { + assert!(!UnscaledTime { seconds: 1, attoseconds: 0 } @@ -585,17 +433,35 @@ mod tests { } #[test] - fn test_raw_time_hour() { + fn test_unscaled_time_seconds() { + let time = UnscaledTime { + seconds: 123, + attoseconds: 0, + }; + assert_eq!(time.seconds(), 123); + } + + #[test] + fn test_unscaled_time_attoseconds() { + let time = UnscaledTime { + seconds: 0, + attoseconds: 123, + }; + assert_eq!(time.attoseconds(), 123); + } + + #[test] + fn test_unscaled_time_wall_clock_hour() { struct TestCase { desc: &'static str, - time: RawTime, + time: UnscaledTime, expected_hour: i64, } let test_cases = [ TestCase { desc: "zero value", - time: RawTime { + time: UnscaledTime { seconds: 0, attoseconds: 0, }, @@ -603,7 +469,7 @@ mod tests { }, TestCase { desc: "one attosecond less than an hour", - time: RawTime { + time: UnscaledTime { seconds: SECONDS_PER_HOUR - 1, attoseconds: ATTOSECONDS_PER_SECOND - 1, }, @@ -611,7 +477,7 @@ mod tests { }, TestCase { desc: "exactly one hour", - time: RawTime { + time: UnscaledTime { seconds: SECONDS_PER_HOUR, attoseconds: 0, }, @@ -619,7 +485,7 @@ mod tests { }, TestCase { desc: "one day and one hour", - time: RawTime { + time: UnscaledTime { seconds: SECONDS_PER_HOUR * 25, attoseconds: 0, }, @@ -627,7 +493,7 @@ mod tests { }, TestCase { desc: "one attosecond less than the epoch", - time: RawTime { + time: UnscaledTime { seconds: -1, attoseconds: ATTOSECONDS_PER_SECOND - 1, }, @@ -635,7 +501,7 @@ mod tests { }, TestCase { desc: "one hour less than the epoch", - time: RawTime { + time: UnscaledTime { seconds: -SECONDS_PER_HOUR, attoseconds: 0, }, @@ -643,7 +509,7 @@ mod tests { }, TestCase { desc: "one hour and one attosecond less than the epoch", - time: RawTime { + time: UnscaledTime { seconds: -SECONDS_PER_HOUR - 1, attoseconds: ATTOSECONDS_PER_SECOND - 1, }, @@ -651,7 +517,7 @@ mod tests { }, TestCase { desc: "one day less than the epoch", - time: RawTime { + time: UnscaledTime { seconds: -SECONDS_PER_DAY, attoseconds: 0, }, @@ -660,7 +526,7 @@ mod tests { TestCase { // Exercises the case where the number of seconds exceeds the number of seconds in a day. desc: "two days less than the epoch", - time: RawTime { + time: UnscaledTime { seconds: -SECONDS_PER_DAY * 2, attoseconds: 0, }, @@ -679,17 +545,17 @@ mod tests { } #[test] - fn test_raw_time_minute() { + fn test_unscaled_time_wall_clock_minute() { struct TestCase { desc: &'static str, - time: RawTime, + time: UnscaledTime, expected_minute: i64, } let test_cases = [ TestCase { desc: "zero value", - time: RawTime { + time: UnscaledTime { seconds: 0, attoseconds: 0, }, @@ -697,7 +563,7 @@ mod tests { }, TestCase { desc: "one attosecond less than one minute", - time: RawTime { + time: UnscaledTime { seconds: SECONDS_PER_MINUTE - 1, attoseconds: ATTOSECONDS_PER_SECOND - 1, }, @@ -705,7 +571,7 @@ mod tests { }, TestCase { desc: "one minute", - time: RawTime { + time: UnscaledTime { seconds: SECONDS_PER_MINUTE, attoseconds: 0, }, @@ -713,7 +579,7 @@ mod tests { }, TestCase { desc: "one attosecond less than an hour", - time: RawTime { + time: UnscaledTime { seconds: SECONDS_PER_HOUR - 1, attoseconds: ATTOSECONDS_PER_SECOND - 1, }, @@ -721,7 +587,7 @@ mod tests { }, TestCase { desc: "exactly one hour", - time: RawTime { + time: UnscaledTime { seconds: SECONDS_PER_HOUR, attoseconds: 0, }, @@ -729,7 +595,7 @@ mod tests { }, TestCase { desc: "one hour and one minute", - time: RawTime { + time: UnscaledTime { seconds: SECONDS_PER_HOUR + SECONDS_PER_MINUTE, attoseconds: 0, }, @@ -737,7 +603,7 @@ mod tests { }, TestCase { desc: "one attosecond less than the epoch", - time: RawTime { + time: UnscaledTime { seconds: -1, attoseconds: ATTOSECONDS_PER_SECOND - 1, }, @@ -745,7 +611,7 @@ mod tests { }, TestCase { desc: "one minute less than the epoch", - time: RawTime { + time: UnscaledTime { seconds: -SECONDS_PER_MINUTE, attoseconds: 0, }, @@ -753,7 +619,7 @@ mod tests { }, TestCase { desc: "one minute and one attosecond less than the epoch", - time: RawTime { + time: UnscaledTime { seconds: -SECONDS_PER_MINUTE - 1, attoseconds: ATTOSECONDS_PER_SECOND - 1, }, @@ -772,17 +638,17 @@ mod tests { } #[test] - fn test_raw_time_second() { + fn test_unscaled_time_wall_clock_second() { struct TestCase { desc: &'static str, - time: RawTime, + time: UnscaledTime, expected_second: i64, } let test_cases = [ TestCase { desc: "zero value", - time: RawTime { + time: UnscaledTime { seconds: 0, attoseconds: 0, }, @@ -790,7 +656,7 @@ mod tests { }, TestCase { desc: "one attosecond less than one second", - time: RawTime { + time: UnscaledTime { seconds: 0, attoseconds: ATTOSECONDS_PER_SECOND - 1, }, @@ -798,7 +664,7 @@ mod tests { }, TestCase { desc: "one second", - time: RawTime { + time: UnscaledTime { seconds: 1, attoseconds: 0, }, @@ -806,7 +672,7 @@ mod tests { }, TestCase { desc: "one attosecond less than a minute", - time: RawTime { + time: UnscaledTime { seconds: SECONDS_PER_MINUTE - 1, attoseconds: ATTOSECONDS_PER_SECOND - 1, }, @@ -814,7 +680,7 @@ mod tests { }, TestCase { desc: "exactly one minute", - time: RawTime { + time: UnscaledTime { seconds: SECONDS_PER_MINUTE, attoseconds: 0, }, @@ -822,7 +688,7 @@ mod tests { }, TestCase { desc: "one minute and one second", - time: RawTime { + time: UnscaledTime { seconds: SECONDS_PER_MINUTE + 1, attoseconds: 0, }, @@ -830,7 +696,7 @@ mod tests { }, TestCase { desc: "one attosecond less than the epoch", - time: RawTime { + time: UnscaledTime { seconds: -1, attoseconds: ATTOSECONDS_PER_SECOND - 1, }, @@ -838,7 +704,7 @@ mod tests { }, TestCase { desc: "one second less than the epoch", - time: RawTime { + time: UnscaledTime { seconds: -1, attoseconds: 0, }, @@ -846,7 +712,7 @@ mod tests { }, TestCase { desc: "one second and one attosecond less than the epoch", - time: RawTime { + time: UnscaledTime { seconds: -2, attoseconds: ATTOSECONDS_PER_SECOND - 1, }, @@ -865,8 +731,8 @@ mod tests { } #[test] - fn test_raw_time_subseconds_with_positive_seconds() { - let time = RawTime { + fn test_unscaled_time_subseconds_with_positive_seconds() { + let time = UnscaledTime { seconds: 0, attoseconds: 123_456_789_012_345_678, }; @@ -920,8 +786,8 @@ mod tests { } #[test] - fn test_raw_time_subseconds_with_negative_seconds() { - let time = RawTime { + fn test_unscaled_time_subseconds_with_negative_seconds() { + let time = UnscaledTime { seconds: -1, attoseconds: 123_456_789_012_345_678, }; @@ -975,12 +841,12 @@ mod tests { } #[test] - fn test_raw_time_add_time_delta() { + fn test_unscaled_time_add_time_delta() { struct TestCase { desc: &'static str, delta: TimeDelta, - time: RawTime, - expected: RawTime, + time: UnscaledTime, + expected: UnscaledTime, } let test_cases = [ @@ -990,11 +856,11 @@ mod tests { seconds: 1, attoseconds: 1, }, - time: RawTime { + time: UnscaledTime { seconds: 1, attoseconds: 0, }, - expected: RawTime { + expected: UnscaledTime { seconds: 2, attoseconds: 1, }, @@ -1005,11 +871,11 @@ mod tests { seconds: 1, attoseconds: 2, }, - time: RawTime { + time: UnscaledTime { seconds: 1, attoseconds: ATTOSECONDS_PER_SECOND - 1, }, - expected: RawTime { + expected: UnscaledTime { seconds: 3, attoseconds: 1, }, @@ -1020,11 +886,11 @@ mod tests { seconds: 1, attoseconds: 1, }, - time: RawTime { + time: UnscaledTime { seconds: -1, attoseconds: 0, }, - expected: RawTime { + expected: UnscaledTime { seconds: 0, attoseconds: 1, }, @@ -1035,11 +901,11 @@ mod tests { seconds: 1, attoseconds: 2, }, - time: RawTime { + time: UnscaledTime { seconds: -1, attoseconds: ATTOSECONDS_PER_SECOND - 1, }, - expected: RawTime { + expected: UnscaledTime { seconds: 1, attoseconds: 1, }, @@ -1057,12 +923,12 @@ mod tests { } #[test] - fn test_raw_time_sub_time_delta() { + fn test_unscaled_time_sub_time_delta() { struct TestCase { desc: &'static str, delta: TimeDelta, - time: RawTime, - expected: RawTime, + time: UnscaledTime, + expected: UnscaledTime, } let test_cases = [ @@ -1072,11 +938,11 @@ mod tests { seconds: 1, attoseconds: 1, }, - time: RawTime { + time: UnscaledTime { seconds: 2, attoseconds: 2, }, - expected: RawTime { + expected: UnscaledTime { seconds: 1, attoseconds: 1, }, @@ -1087,11 +953,11 @@ mod tests { seconds: 1, attoseconds: 2, }, - time: RawTime { + time: UnscaledTime { seconds: 2, attoseconds: 1, }, - expected: RawTime { + expected: UnscaledTime { seconds: 0, attoseconds: ATTOSECONDS_PER_SECOND - 1, }, @@ -1102,11 +968,11 @@ mod tests { seconds: 1, attoseconds: 1, }, - time: RawTime { + time: UnscaledTime { seconds: -1, attoseconds: 2, }, - expected: RawTime { + expected: UnscaledTime { seconds: -2, attoseconds: 1, }, @@ -1117,11 +983,11 @@ mod tests { seconds: 1, attoseconds: 2, }, - time: RawTime { + time: UnscaledTime { seconds: -1, attoseconds: 1, }, - expected: RawTime { + expected: UnscaledTime { seconds: -3, attoseconds: ATTOSECONDS_PER_SECOND - 1, }, @@ -1132,11 +998,11 @@ mod tests { seconds: 1, attoseconds: 2, }, - time: RawTime { + time: UnscaledTime { seconds: 0, attoseconds: 1, }, - expected: RawTime { + expected: UnscaledTime { seconds: -2, attoseconds: ATTOSECONDS_PER_SECOND - 1, }, @@ -1154,37 +1020,142 @@ mod tests { } #[test] - fn test_timescale_into_str() { + fn test_unscaled_time_days_since_j2000() { + struct TestCase { + desc: &'static str, + time: UnscaledTime, + expected: f64, + } + + let test_cases = [ + TestCase { + desc: "at the epoch", + time: UnscaledTime::default(), + expected: 0.0, + }, + TestCase { + desc: "exactly one day after the epoch", + time: UnscaledTime { + seconds: SECONDS_PER_DAY, + attoseconds: 0, + }, + expected: 1.0, + }, + TestCase { + desc: "exactly one day before the epoch", + time: UnscaledTime { + seconds: -SECONDS_PER_DAY, + attoseconds: 0, + }, + expected: -1.0, + }, + TestCase { + desc: "a partial number of days after the epoch", + time: UnscaledTime { + seconds: (SECONDS_PER_DAY / 2) * 3, + attoseconds: ATTOSECONDS_PER_SECOND / 2, + }, + expected: 1.5000057870370371, + }, + ]; + + for tc in test_cases { + let actual = tc.time.days_since_j2000(); + assert_float_eq!( + tc.expected, + actual, + abs <= 1e-12, + "{}: expected {}, got {}", + tc.desc, + tc.expected, + actual + ); + } + } + + #[test] + fn test_unscaled_time_centuries_since_j2000() { + struct TestCase { + desc: &'static str, + time: UnscaledTime, + expected: f64, + } + let test_cases = [ - (TimeScale::TAI, "TAI"), - (TimeScale::TCB, "TCB"), - (TimeScale::TCG, "TCG"), - (TimeScale::TDB, "TDB"), - (TimeScale::TT, "TT"), - (TimeScale::UT1, "UT1"), + TestCase { + desc: "at the epoch", + time: UnscaledTime::default(), + expected: 0.0, + }, + TestCase { + desc: "exactly one century after the epoch", + time: UnscaledTime { + seconds: SECONDS_PER_JULIAN_CENTURY, + attoseconds: 0, + }, + expected: 1.0, + }, + TestCase { + desc: "exactly one century before the epoch", + time: UnscaledTime { + seconds: -SECONDS_PER_JULIAN_CENTURY, + attoseconds: 0, + }, + expected: -1.0, + }, + TestCase { + desc: "a partial number of centuries after the epoch", + time: UnscaledTime { + seconds: (SECONDS_PER_JULIAN_CENTURY / 2) * 3, + attoseconds: ATTOSECONDS_PER_SECOND / 2, + }, + expected: 1.5000000001584404, + }, ]; - for (scale, expected) in test_cases { - assert_eq!(Into::<&str>::into(scale), expected); + for tc in test_cases { + let actual = tc.time.centuries_since_j2000(); + assert_float_eq!( + tc.expected, + actual, + abs <= 1e-12, + "{}: expected {}, got {}", + tc.desc, + tc.expected, + actual + ); } } + #[test] + fn test_time_new() { + let scale = TAI; + let seconds = 1234567890; + let attoseconds = 9876543210; + let expected = Time { + scale, + timestamp: UnscaledTime { + seconds, + attoseconds, + }, + }; + let actual = Time::new(scale, seconds, attoseconds); + assert_eq!(actual, expected); + } + #[test] fn test_time_from_date_and_utc_timestamp() { let date = Date::new_unchecked(Gregorian, 2021, 1, 1); let utc = UTC::new(12, 34, 56).expect("time should be valid"); let datetime = UTCDateTime::new(date, utc); - - for scale in TIME_SCALES { - let actual = Time::from_date_and_utc_timestamp(scale, date, utc); - let expected = Time::from_utc_datetime(scale, datetime); - assert_eq!(actual, expected); - } + let actual = Time::from_date_and_utc_timestamp(TAI, date, utc); + let expected = Time::from_utc_datetime(TAI, datetime); + assert_eq!(actual, expected); } #[test] fn test_time_display() { - let time = Time::TAI(TAI::default()); + let time = Time::j2000(TAI); let expected = "12:00:00.000.000.000.000.000.000 TAI".to_string(); let actual = time.to_string(); assert_eq!(actual, expected); @@ -1192,222 +1163,219 @@ mod tests { #[test] fn test_time_j2000() { - [ - (TimeScale::TAI, Time::TAI(TAI::default())), - (TimeScale::TCB, Time::TCB(TCB::default())), - (TimeScale::TCG, Time::TCG(TCG::default())), - (TimeScale::TDB, Time::TDB(TDB::default())), - (TimeScale::TT, Time::TT(TT::default())), - (TimeScale::UT1, Time::UT1(UT1::default())), - ] - .iter() - .for_each(|(scale, expected)| { - let actual = Time::j2000(*scale); - assert_eq!(*expected, actual); - }); + let actual = Time::j2000(TAI); + let expected = Time { + scale: TAI, + timestamp: UnscaledTime::default(), + }; + assert_eq!(expected, actual); } #[test] fn test_time_jd0() { - [ - ( - TimeScale::TAI, - Time::TAI(TAI(RawTime { - seconds: -211813488000, - attoseconds: 0, - })), - ), - ( - TimeScale::TCB, - Time::TCB(TCB(RawTime { - seconds: -211813488000, - attoseconds: 0, - })), - ), - ( - TimeScale::TCG, - Time::TCG(TCG(RawTime { - seconds: -211813488000, - attoseconds: 0, - })), - ), - ( - TimeScale::TDB, - Time::TDB(TDB(RawTime { - seconds: -211813488000, - attoseconds: 0, - })), - ), - ( - TimeScale::TT, - Time::TT(TT(RawTime { - seconds: -211813488000, - attoseconds: 0, - })), - ), - ( - TimeScale::UT1, - Time::UT1(UT1(RawTime { - seconds: -211813488000, - attoseconds: 0, - })), - ), - ] - .iter() - .for_each(|(scale, expected)| { - let actual = Time::jd0(*scale); - assert_eq!(*expected, actual); - }); + let actual = Time::jd0(TAI); + let expected = Time::from_unscaled( + TAI, + UnscaledTime { + seconds: -211813488000, + attoseconds: 0, + }, + ); + assert_eq!(expected, actual); } #[test] - fn test_time_scale() { - let test_cases = [ - (Time::TAI(TAI::default()), TimeScale::TAI), - (Time::TCB(TCB::default()), TimeScale::TCB), - (Time::TCG(TCG::default()), TimeScale::TCG), - (Time::TDB(TDB::default()), TimeScale::TDB), - (Time::TT(TT::default()), TimeScale::TT), - (Time::UT1(UT1::default()), TimeScale::UT1), - ]; + fn test_time_seconds() { + let time = Time::new(TAI, 1234567890, 9876543210); + let expected = 1234567890; + let actual = time.seconds(); + assert_eq!( + actual, expected, + "expected Time to have {} seconds, but got {}", + expected, actual + ); + } - for (time, expected) in test_cases { - assert_eq!(time.scale(), expected); - } + #[test] + fn test_time_attoseconds() { + let time = Time::new(TAI, 1234567890, 9876543210); + let expected = 9876543210; + let actual = time.attoseconds(); + assert_eq!( + actual, expected, + "expected Time to have {} attoseconds, but got {}", + expected, actual + ); + } + + #[test] + fn test_time_days_since_j2000() { + let unscaled = UnscaledTime { + seconds: 1234567890, + attoseconds: 9876543210, + }; + let expected = unscaled.days_since_j2000(); + let actual = Time::from_unscaled(TAI, unscaled).days_since_j2000(); + assert_float_eq!( + actual, + expected, + rel <= 1e-15, + "expected {} days since J2000, but got {}", + expected, + actual + ); + } + + #[test] + fn test_time_centuries_since_j2000() { + let unscaled = UnscaledTime { + seconds: 1234567890, + attoseconds: 9876543210, + }; + let expected = unscaled.centuries_since_j2000(); + let actual = Time::from_unscaled(TAI, unscaled).centuries_since_j2000(); + assert_float_eq!( + actual, + expected, + rel <= 1e-15, + "expected {} centuries since J2000, but got {}", + expected, + actual + ); } #[test] fn test_time_wall_clock_hour() { - let raw_time = RawTime::default(); - let expected = raw_time.hour(); - for scale in TIME_SCALES { - let time = Time::from_raw(scale, raw_time); - let actual = time.hour(); - assert_eq!( - actual, expected, - "expected time in scale {} to have hour {}, but got {}", - scale, expected, actual - ); - } + let unscaled_time = UnscaledTime { + seconds: 1234567890, + attoseconds: 9876543210, + }; + let expected = unscaled_time.hour(); + let actual = Time::from_unscaled(TAI, unscaled_time).hour(); + assert_eq!( + actual, expected, + "expected Time to have hour {}, but got {}", + expected, actual + ); } #[test] fn test_time_wall_clock_minute() { - let raw_time = RawTime::default(); - let expected = raw_time.minute(); - for scale in TIME_SCALES { - let time = Time::from_raw(scale, raw_time); - let actual = time.minute(); - assert_eq!( - actual, expected, - "expected time in scale {} to have minute {}, but got {}", - scale, expected, actual - ); - } + let unscaled_time = UnscaledTime { + seconds: 1234567890, + attoseconds: 9876543210, + }; + let expected = unscaled_time.minute(); + let actual = Time::from_unscaled(TAI, unscaled_time).minute(); + assert_eq!( + actual, expected, + "expected Time to have minute {}, but got {}", + expected, actual, + ); } #[test] fn test_time_wall_clock_second() { - let raw_time = RawTime::default(); - let expected = raw_time.second(); - for scale in TIME_SCALES { - let time = Time::from_raw(scale, raw_time); - let actual = time.second(); - assert_eq!( - actual, expected, - "expected time in scale {} to have second {}, but got {}", - scale, expected, actual - ); - } + let unscaled_time = UnscaledTime { + seconds: 1234567890, + attoseconds: 9876543210, + }; + let expected = unscaled_time.second(); + let actual = Time::from_unscaled(TAI, unscaled_time).second(); + assert_eq!( + actual, expected, + "expected Time to have second {}, but got {}", + expected, actual, + ); } #[test] fn test_time_wall_clock_millisecond() { - let raw_time = RawTime::default(); - let expected = raw_time.millisecond(); - for scale in TIME_SCALES { - let time = Time::from_raw(scale, raw_time); - let actual = time.millisecond(); - assert_eq!( - actual, expected, - "expected time in scale {} to have millisecond {}, but got {}", - scale, expected, actual - ); - } + let unscaled_time = UnscaledTime { + seconds: 1234567890, + attoseconds: 9876543210, + }; + let expected = unscaled_time.millisecond(); + let actual = Time::from_unscaled(TAI, unscaled_time).millisecond(); + assert_eq!( + actual, expected, + "expected Time to have millisecond {}, but got {}", + expected, actual, + ); } #[test] fn test_time_wall_clock_microsecond() { - let raw_time = RawTime::default(); - let expected = raw_time.microsecond(); - for scale in TIME_SCALES { - let time = Time::from_raw(scale, raw_time); - let actual = time.microsecond(); - assert_eq!( - actual, expected, - "expected time in scale {} to have microsecond {}, but got {}", - scale, expected, actual - ); - } + let unscaled_time = UnscaledTime { + seconds: 1234567890, + attoseconds: 9876543210, + }; + let expected = unscaled_time.microsecond(); + let actual = Time::from_unscaled(TAI, unscaled_time).microsecond(); + assert_eq!( + actual, expected, + "expected Time to have microsecond {}, but got {}", + expected, actual, + ); } #[test] fn test_time_wall_clock_nanosecond() { - let raw_time = RawTime::default(); - let expected = raw_time.nanosecond(); - for scale in TIME_SCALES { - let time = Time::from_raw(scale, raw_time); - let actual = time.nanosecond(); - assert_eq!( - actual, expected, - "expected time in scale {} to have nanosecond {}, but got {}", - scale, expected, actual - ); - } + let unscaled_time = UnscaledTime { + seconds: 1234567890, + attoseconds: 9876543210, + }; + let expected = unscaled_time.nanosecond(); + let actual = Time::from_unscaled(TAI, unscaled_time).nanosecond(); + assert_eq!( + actual, expected, + "expected Time to have nanosecond {}, but got {}", + expected, actual, + ); } #[test] fn test_time_wall_clock_picosecond() { - let raw_time = RawTime::default(); - let expected = raw_time.picosecond(); - for scale in TIME_SCALES { - let time = Time::from_raw(scale, raw_time); - let actual = time.picosecond(); - assert_eq!( - actual, expected, - "expected time in scale {} to have picosecond {}, but got {}", - scale, expected, actual - ); - } + let unscaled_time = UnscaledTime { + seconds: 1234567890, + attoseconds: 9876543210, + }; + let expected = unscaled_time.picosecond(); + let actual = Time::from_unscaled(TAI, unscaled_time).picosecond(); + assert_eq!( + actual, expected, + "expected Time to have picosecond {}, but got {}", + expected, actual, + ); } #[test] fn test_time_wall_clock_femtosecond() { - let raw_time = RawTime::default(); - let expected = raw_time.femtosecond(); - for scale in TIME_SCALES { - let time = Time::from_raw(scale, raw_time); - let actual = time.femtosecond(); - assert_eq!( - actual, expected, - "expected time in scale {} to have femtosecond {}, but got {}", - scale, expected, actual - ); - } + let unscaled_time = UnscaledTime { + seconds: 1234567890, + attoseconds: 9876543210, + }; + let expected = unscaled_time.femtosecond(); + let actual = Time::from_unscaled(TAI, unscaled_time).femtosecond(); + assert_eq!( + actual, expected, + "expected Time to have femtosecond {}, but got {}", + expected, actual, + ); } #[test] fn test_time_wall_clock_attosecond() { - let raw_time = RawTime::default(); - let expected = raw_time.attosecond(); - for scale in TIME_SCALES { - let time = Time::from_raw(scale, raw_time); - let actual = time.attosecond(); - assert_eq!( - actual, expected, - "expected time in scale {} to have attosecond {}, but got {}", - scale, expected, actual - ); - } + let unscaled_time = UnscaledTime { + seconds: 1234567890, + attoseconds: 9876543210, + }; + let expected = unscaled_time.attosecond(); + let actual = Time::from_unscaled(TAI, unscaled_time).attosecond(); + assert_eq!( + actual, expected, + "expected Time to have attosecond {}, but got {}", + expected, actual, + ); } } diff --git a/crates/lox_core/src/time/intervals.rs b/crates/lox_core/src/time/intervals.rs index b9800894..d25689f1 100644 --- a/crates/lox_core/src/time/intervals.rs +++ b/crates/lox_core/src/time/intervals.rs @@ -1,61 +1,6 @@ -use crate::time::constants; -use crate::time::continuous::Time; - /// Although strictly TDB, TT is sufficient for most applications. pub type TDBJulianCenturiesSinceJ2000 = f64; -pub fn tdb_julian_centuries_since_j2000(time: Time) -> TDBJulianCenturiesSinceJ2000 { - match time { - Time::TT(_) | Time::TDB(_) => { - time.days_since_j2000() / constants::f64::DAYS_PER_JULIAN_CENTURY - } - _ => todo!("perform the simpler of the conversions to TT or TDB first"), - } -} - pub type TTJulianCenturiesSinceJ2000 = f64; pub type UT1DaysSinceJ2000 = f64; - -#[cfg(test)] -mod tests { - use float_eq::assert_float_eq; - - use crate::time::continuous::{Time, TimeScale}; - use crate::time::dates::Calendar::Gregorian; - use crate::time::dates::Date; - use crate::time::utc::UTC; - - use super::tdb_julian_centuries_since_j2000; - - /// A somewhat arbitrary tolerance for floating point comparisons. - const TOLERANCE: f64 = 1e-12; - - #[test] - fn test_tdb_julian_centuries_since_j2000_tt() { - let jd0 = Time::jd0(TimeScale::TT); - assert_float_eq!( - -67.11964407939767, - tdb_julian_centuries_since_j2000(jd0), - rel <= TOLERANCE - ); - - let j2000 = Time::j2000(TimeScale::TT); - assert_float_eq!( - 0.0, - tdb_julian_centuries_since_j2000(j2000), - rel <= TOLERANCE - ); - - let j2100 = Time::from_date_and_utc_timestamp( - TimeScale::TT, - Date::new_unchecked(Gregorian, 2100, 1, 1), - UTC::new(12, 0, 0).expect("midday should be a valid time"), - ); - assert_float_eq!( - 1.0, - tdb_julian_centuries_since_j2000(j2100), - rel <= TOLERANCE - ); - } -} diff --git a/crates/lox_py/src/coords.rs b/crates/lox_py/src/coords.rs index 7747188b..ea2547ad 100644 --- a/crates/lox_py/src/coords.rs +++ b/crates/lox_py/src/coords.rs @@ -20,6 +20,7 @@ use crate::time::PyTime; #[pyclass(name = "Cartesian")] pub struct PyCartesian { + time: PyTime, state: CartesianState, origin: PyBody, frame: PyFrame, @@ -30,7 +31,7 @@ impl PyCartesian { #[allow(clippy::too_many_arguments)] #[new] fn new( - time: &PyTime, + time: PyTime, body: PyObject, frame: &str, x: f64, @@ -42,8 +43,9 @@ impl PyCartesian { ) -> PyResult { let origin: PyBody = body.try_into()?; let frame = PyFrame::from_str(frame)?; - let state = CartesianState::new(time.0, DVec3::new(x, y, z), DVec3::new(vx, vy, vz)); + let state = CartesianState::new(DVec3::new(x, y, z), DVec3::new(vx, vy, vz)); Ok(Self { + time, state, origin, frame, @@ -51,7 +53,7 @@ impl PyCartesian { } fn time(&self) -> PyTime { - PyTime(self.state.time()) + self.time } fn reference_frame(&self) -> String { @@ -76,15 +78,17 @@ impl PyCartesian { let mu = self.origin.gravitational_parameter(); let state = self.state.to_keplerian_state(mu); PyKeplerian { + state, + time: self.time, origin: self.origin.clone(), frame: self.frame.clone(), - state, } } } #[pyclass(name = "Keplerian")] pub struct PyKeplerian { + time: PyTime, state: KeplerianState, origin: PyBody, frame: PyFrame, @@ -95,7 +99,7 @@ impl PyKeplerian { #[new] #[allow(clippy::too_many_arguments)] fn new( - t: &PyTime, + time: PyTime, body: PyObject, frame: &str, semi_major_axis: f64, @@ -108,7 +112,6 @@ impl PyKeplerian { let origin: PyBody = body.try_into()?; let frame = PyFrame::from_str(frame)?; let state = KeplerianState::new( - t.0, semi_major_axis, eccentricity, inclination, @@ -117,6 +120,7 @@ impl PyKeplerian { true_anomaly, ); Ok(Self { + time, state, origin, frame, @@ -124,7 +128,7 @@ impl PyKeplerian { } fn time(&self) -> PyTime { - PyTime(self.state.time()) + self.time } fn reference_frame(&self) -> String { @@ -164,6 +168,7 @@ impl PyKeplerian { let state = self.state.to_cartesian_state(mu); PyCartesian { state, + time: self.time, origin: self.origin.clone(), frame: self.frame.clone(), } @@ -175,13 +180,14 @@ mod tests { use float_eq::assert_float_eq; use crate::bodies::PyPlanet; + use crate::time::PyTimeScale; use super::*; #[test] fn test_cartesian() { - let epoch = PyTime::new( - "TDB", + let time = PyTime::new( + PyTimeScale::TDB, 2023, 3, 25, @@ -213,7 +219,7 @@ mod tests { ) * 1e-3; let cartesian = PyCartesian::new( - &epoch, + time, body.clone(), "ICRF", pos.x, @@ -230,7 +236,7 @@ mod tests { Python::with_gil(|py| body.extract::(py)).expect("origin should be a planet"); let origin1 = Python::with_gil(|py| cartesian1.origin().extract::(py)) .expect("origin should be a planet"); - assert_eq!(cartesian1.time(), epoch); + assert_eq!(cartesian1.time(), time); assert_eq!(origin1.name(), origin.name()); assert_eq!(cartesian1.reference_frame(), "ICRF"); @@ -244,8 +250,8 @@ mod tests { #[test] fn test_keplerian() { - let epoch = PyTime::new( - "TDB", + let time = PyTime::new( + PyTimeScale::TDB, 2023, 3, 25, @@ -273,7 +279,7 @@ mod tests { let true_anomaly = 0.44369564302687126; let keplerian = PyKeplerian::new( - &epoch, + time, body.clone(), "ICRF", semi_major, @@ -290,7 +296,7 @@ mod tests { Python::with_gil(|py| body.extract::(py)).expect("origin should be a planet"); let origin1 = Python::with_gil(|py| keplerian1.origin().extract::(py)) .expect("origin should be a planet"); - assert_eq!(keplerian1.time(), epoch); + assert_eq!(keplerian1.time(), time); assert_eq!(origin1.name(), origin.name()); assert_eq!(keplerian1.reference_frame(), "ICRF"); diff --git a/crates/lox_py/src/time.rs b/crates/lox_py/src/time.rs index 565bff01..0527bbe7 100644 --- a/crates/lox_py/src/time.rs +++ b/crates/lox_py/src/time.rs @@ -7,8 +7,9 @@ */ use pyo3::{pyclass, pymethods}; +use std::fmt::{Display, Formatter}; -use lox_core::time::continuous::{Time, TimeScale}; +use lox_core::time::continuous::{Time, UnscaledTime, TAI, TCB, TCG, TDB, TT, UT1}; use lox_core::time::dates::Date; use lox_core::time::utc::UTC; use lox_core::time::PerMille; @@ -16,35 +17,61 @@ use lox_core::time::PerMille; use crate::LoxPyError; #[pyclass(name = "TimeScale")] -pub struct PyTimeScale(pub TimeScale); +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[allow(clippy::upper_case_acronyms)] +pub enum PyTimeScale { + TAI, + TCB, + TCG, + TDB, + TT, + UT1, +} #[pymethods] impl PyTimeScale { #[new] fn new(name: &str) -> Result { match name { - "TAI" => Ok(PyTimeScale(TimeScale::TAI)), - "TCB" => Ok(PyTimeScale(TimeScale::TCB)), - "TCG" => Ok(PyTimeScale(TimeScale::TCG)), - "TDB" => Ok(PyTimeScale(TimeScale::TDB)), - "TT" => Ok(PyTimeScale(TimeScale::TT)), - "UT1" => Ok(PyTimeScale(TimeScale::UT1)), + "TAI" => Ok(PyTimeScale::TAI), + "TCB" => Ok(PyTimeScale::TCB), + "TCG" => Ok(PyTimeScale::TCG), + "TDB" => Ok(PyTimeScale::TDB), + "TT" => Ok(PyTimeScale::TT), + "UT1" => Ok(PyTimeScale::UT1), _ => Err(LoxPyError::InvalidTimeScale(name.to_string())), } } fn __repr__(&self) -> String { - format!("TimeScale(\"{}\")", self.0) + format!("TimeScale(\"{}\")", self) } fn __str__(&self) -> String { - format!("{}", self.0) + format!("{}", self) + } +} + +impl Display for PyTimeScale { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let s = match self { + PyTimeScale::TAI => "TAI", + PyTimeScale::TCB => "TCB", + PyTimeScale::TCG => "TCG", + PyTimeScale::TDB => "TDB", + PyTimeScale::TT => "TT", + PyTimeScale::UT1 => "UT1", + }; + write!(f, "{}", s) } } #[pyclass(name = "Time")] #[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub struct PyTime(pub Time); +pub struct PyTime { + pub scale: PyTimeScale, + pub timestamp: UnscaledTime, +} #[pymethods] impl PyTime { @@ -66,7 +93,7 @@ impl PyTime { ))] #[new] pub fn new( - scale: &str, + scale: PyTimeScale, year: i64, month: i64, day: i64, @@ -80,7 +107,6 @@ impl PyTime { femto: Option, atto: Option, ) -> Result { - let time_scale = PyTimeScale::new(scale)?; let date = Date::new(year, month, day)?; let hour = hour.unwrap_or(0); @@ -105,19 +131,41 @@ impl PyTime { if let Some(atto) = atto { utc.atto = PerMille::new(atto)?; } - Ok(PyTime(Time::from_date_and_utc_timestamp( - time_scale.0, - date, - utc, - ))) + + Ok(pytime_from_date_and_utc_timestamp(scale, date, utc)) } - fn days_since_j2000(&self) -> f64 { - self.0.days_since_j2000() + pub fn days_since_j2000(&self) -> f64 { + self.timestamp.days_since_j2000() } - fn scale(&self) -> &str { - self.0.scale().into() + pub fn scale(&self) -> PyTimeScale { + self.scale + } +} + +fn pytime_from_date_and_utc_timestamp(scale: PyTimeScale, date: Date, utc: UTC) -> PyTime { + PyTime { + timestamp: unscaled_time_from_date_and_utc_timestamp(scale, date, utc), + scale, + } +} + +/// Invokes the appropriate [Time::from_date_and_utc_timestamp] method based on the time scale, and returns the +/// result as an [UnscaledTime]. The Rust time library performs the appropriate transformation while keeping +/// generics out of the Python interface. +fn unscaled_time_from_date_and_utc_timestamp( + scale: PyTimeScale, + date: Date, + utc: UTC, +) -> UnscaledTime { + match scale { + PyTimeScale::TAI => Time::from_date_and_utc_timestamp(TAI, date, utc).unscaled(), + PyTimeScale::TCB => Time::from_date_and_utc_timestamp(TCB, date, utc).unscaled(), + PyTimeScale::TCG => Time::from_date_and_utc_timestamp(TCG, date, utc).unscaled(), + PyTimeScale::TDB => Time::from_date_and_utc_timestamp(TDB, date, utc).unscaled(), + PyTimeScale::TT => Time::from_date_and_utc_timestamp(TT, date, utc).unscaled(), + PyTimeScale::UT1 => Time::from_date_and_utc_timestamp(UT1, date, utc).unscaled(), } } @@ -129,15 +177,15 @@ mod tests { use super::*; #[rstest] - #[case("TAI", TimeScale::TAI)] - #[case("TCB", TimeScale::TCB)] - #[case("TCG", TimeScale::TCG)] - #[case("TDB", TimeScale::TDB)] - #[case("TT", TimeScale::TT)] - #[case("UT1", TimeScale::UT1)] - fn test_scale(#[case] name: &str, #[case] scale: TimeScale) { + #[case("TAI", PyTimeScale::TAI)] + #[case("TCB", PyTimeScale::TCB)] + #[case("TCG", PyTimeScale::TCG)] + #[case("TDB", PyTimeScale::TDB)] + #[case("TT", PyTimeScale::TT)] + #[case("UT1", PyTimeScale::UT1)] + fn test_scale(#[case] name: &str, #[case] scale: PyTimeScale) { let py_scale = PyTimeScale::new(name).expect("time scale should be valid"); - assert_eq!(py_scale.0, scale); + assert_eq!(py_scale, scale); assert_eq!(py_scale.__str__(), name); assert_eq!(py_scale.__repr__(), format!("TimeScale(\"{}\")", name)); } @@ -151,7 +199,7 @@ mod tests { #[test] fn test_time() { let time = PyTime::new( - "TDB", + PyTimeScale::TDB, 2024, 1, 1, @@ -166,8 +214,8 @@ mod tests { Some(789), ) .expect("time should be valid"); - assert_eq!(time.0.attoseconds(), 123456789123456789); + assert_eq!(time.timestamp.attoseconds(), 123456789123456789); assert_float_eq!(time.days_since_j2000(), 8765.542374114084, rel <= 1e-8); - assert_eq!(time.scale(), "TDB"); + assert_eq!(time.scale(), PyTimeScale::TDB); } } diff --git a/crates/lox_py/tests/test_coords.py b/crates/lox_py/tests/test_coords.py index 7f5c50b2..cfda0036 100644 --- a/crates/lox_py/tests/test_coords.py +++ b/crates/lox_py/tests/test_coords.py @@ -8,7 +8,7 @@ def test_coords(): - time = lox.Time("TDB", 2016, 5, 30, 12) + time = lox.Time(lox.TimeScale.TDB, 2016, 5, 30, 12) x = 6068.27927 y = -1692.84394 z = -2516.61918