From eeb55d0f091755ceccc182c4c5d92d7f56debb95 Mon Sep 17 00:00:00 2001 From: AngusGMorrison Date: Thu, 8 Feb 2024 21:20:46 +0000 Subject: [PATCH 01/14] Add new time types --- crates/lox_core/src/time/continuous.rs | 464 ++++++++++++++++++------- 1 file changed, 347 insertions(+), 117 deletions(-) diff --git a/crates/lox_core/src/time/continuous.rs b/crates/lox_core/src/time/continuous.rs index e482802a..d20fd0ac 100644 --- a/crates/lox_core/src/time/continuous.rs +++ b/crates/lox_core/src/time/continuous.rs @@ -14,7 +14,8 @@ use std::fmt; use std::fmt::{Display, Formatter}; -use std::ops::{Add, Sub}; +use std::marker::PhantomData; +use std::ops::{Add, AddAssign, Sub}; use num::{abs, ToPrimitive}; @@ -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,71 @@ pub struct RawTime { attoseconds: u64, } -impl RawTime { +impl UnscaledTime { fn is_negative(&self) -> bool { self.seconds < 0 } +} + +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,43 +174,212 @@ impl RawTime { } } -impl Add for RawTime { - type Output = Self; +pub trait TimeScaleTrait { + const ABBREVIATION: &'static str; + const NAME: &'static str; +} - /// 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; +/// Barycentric Coordinate Time. Defaults to the J2000 epoch. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct TCB2; + +impl TimeScaleTrait for TCB2 { + const ABBREVIATION: &'static str = "TCB"; + const NAME: &'static str = "Barycentric Coordinate Time"; +} + +/// Geocentric Coordinate Time. Defaults to the J2000 epoch. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct TCG2; + +impl TimeScaleTrait for TCG2 { + const ABBREVIATION: &'static str = "TCG"; + const NAME: &'static str = "Geocentric Coordinate Time"; +} + +/// Barycentric Dynamical Time. Defaults to the J2000 epoch. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct TDB2; + +impl TimeScaleTrait for TDB2 { + const ABBREVIATION: &'static str = "TDB"; + const NAME: &'static str = "Barycentric Dynamical Time"; +} + +/// Terrestrial Time. Defaults to the J2000 epoch. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct TT2; + +impl TimeScaleTrait for TT2 { + const ABBREVIATION: &'static str = "TT"; + const NAME: &'static str = "Terrestrial Time"; +} + +/// Universal Time. Defaults to the J2000 epoch. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct UT12; + +impl TimeScaleTrait for UT12 { + const ABBREVIATION: &'static str = "UT1"; + const NAME: &'static str = "Universal Time"; +} + +#[derive(Debug, Default, Eq, PartialEq)] +pub struct TimeGeneric { + scale: PhantomData, + timestamp: UnscaledTime, +} + +// Must be manually implemented, since derive macros always bound the generic parameters by the given trait, not the +// tightest possible bound, which in this case is given by PhantomData. +// See https://github.com/rust-lang/rust/issues/108894#issuecomment-1459943821 +impl Clone for TimeGeneric { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for TimeGeneric {} + +impl From for TimeGeneric { + fn from(timestamp: UnscaledTime) -> Self { + Self::from_unscaled(timestamp) + } +} + +impl TimeGeneric { + pub fn new(seconds: i64, attoseconds: u64) -> Self { + Self { + scale: PhantomData, + timestamp: UnscaledTime { + seconds, + attoseconds, + }, } + } + + pub fn from_unscaled(timestamp: UnscaledTime) -> Self { Self { + scale: PhantomData, + timestamp, + } + } + + /// Instantiates a `Time` of the given scale from a date and UTC timestamp. + pub fn from_date_and_utc_timestamp(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 unscaled = UnscaledTime { seconds, attoseconds, + }; + Self::from_unscaled(unscaled) + } + + /// Instantiates a `Time` of the given scale from a UTC datetime. + pub fn from_utc_datetime(dt: UTCDateTime) -> Self { + Self::from_date_and_utc_timestamp(dt.date(), dt.time()) + } + + pub fn unscaled(&self) -> UnscaledTime { + self.timestamp + } + + /// Returns the J2000 epoch in the given timescale. + pub fn j2000() -> Self { + Self { + scale: PhantomData, + timestamp: UnscaledTime::default(), } } + + /// Returns, as an epoch in the given timescale, midday on the first day of the proleptic Julian + /// calendar. + pub fn jd0() -> 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(first_proleptic_day, midday) + } + + /// The number of whole seconds since J2000. + pub fn seconds(self) -> i64 { + self.timestamp.seconds + } + + /// The number of attoseconds from the last whole second. + pub fn attoseconds(self) -> u64 { + 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 + } +} + +impl Display for TimeGeneric { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{} {}", self.timestamp, T::ABBREVIATION) + } } -impl Sub for RawTime { +impl Add for TimeGeneric { + type Output = Self; + + fn add(self, rhs: TimeDelta) -> Self::Output { + Self::from_unscaled(self.timestamp + rhs) + } +} + +impl Sub for TimeGeneric { type Output = Self; - /// 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, - } + Self::from_unscaled(self.timestamp - rhs) + } +} + +impl WallClock for TimeGeneric { + fn hour(&self) -> i64 { + self.timestamp.hour() + } + + fn minute(&self) -> i64 { + self.timestamp.minute() + } + + fn second(&self) -> i64 { + self.timestamp.second() + } + + fn millisecond(&self) -> i64 { + self.timestamp.millisecond() + } + + fn microsecond(&self) -> i64 { + self.timestamp.microsecond() + } + + fn nanosecond(&self) -> i64 { + self.timestamp.nanosecond() + } + + fn picosecond(&self) -> i64 { + self.timestamp.picosecond() + } + + fn femtosecond(&self) -> i64 { + self.timestamp.femtosecond() + } + + fn attosecond(&self) -> i64 { + self.timestamp.attosecond() } } @@ -191,7 +421,7 @@ pub trait CalendarDate { /// International Atomic Time. Defaults to the J2000 epoch. #[derive(Debug, Copy, Default, Clone, Eq, PartialEq)] -pub struct TAI(RawTime); +pub struct TAI(UnscaledTime); impl TAI { pub fn to_ut1(&self, _dut: TimeDelta, _dat: TimeDelta) -> UT1 { @@ -207,25 +437,25 @@ impl CalendarDate for TAI { /// Barycentric Coordinate Time. Defaults to the J2000 epoch. #[derive(Debug, Default, Copy, Clone, Eq, PartialEq)] -pub struct TCB(RawTime); +pub struct TCB(UnscaledTime); /// Geocentric Coordinate Time. Defaults to the J2000 epoch. #[derive(Debug, Default, Copy, Clone, Eq, PartialEq)] -pub struct TCG(RawTime); +pub struct TCG(UnscaledTime); /// Barycentric Dynamical Time. Defaults to the J2000 epoch. #[derive(Debug, Default, Copy, Clone, Eq, PartialEq)] -pub struct TDB(RawTime); +pub struct TDB(UnscaledTime); /// Terrestrial Time. Defaults to the J2000 epoch. #[derive(Debug, Default, Copy, Clone, Eq, PartialEq)] -pub struct TT(RawTime); +pub struct TT(UnscaledTime); /// Universal Time. Defaults to the J2000 epoch. #[derive(Debug, Default, Copy, Clone, Eq, PartialEq)] -pub struct UT1(RawTime); +pub struct UT1(UnscaledTime); -/// Implements the `WallClock` trait for the a time scale based on [RawTime] in terms of the underlying +/// Implements the `WallClock` trait for the a time scale based on [UnscaledTime] in terms of the underlying /// raw time. macro_rules! wall_clock { ($time_scale:ident, $test_module:ident) => { @@ -269,10 +499,10 @@ macro_rules! wall_clock { #[cfg(test)] mod $test_module { - use super::{$time_scale, RawTime}; + use super::{$time_scale, UnscaledTime}; use crate::time::WallClock; - const RAW_TIME: RawTime = RawTime { + const RAW_TIME: UnscaledTime = UnscaledTime { seconds: 1234, attoseconds: 5678, }; @@ -354,7 +584,7 @@ impl Time { 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 raw = UnscaledTime { seconds, attoseconds, }; @@ -379,7 +609,7 @@ impl Time { /// Returns the J2000 epoch in the given timescale. pub fn j2000(scale: TimeScale) -> Self { - Self::from_raw(scale, RawTime::default()) + Self::from_raw(scale, UnscaledTime::default()) } /// Returns, as an epoch in the given timescale, midday on the first day of the proleptic Julian @@ -391,7 +621,7 @@ impl Time { Self::from_date_and_utc_timestamp(scale, first_proleptic_day, midday) } - fn from_raw(scale: TimeScale, raw: RawTime) -> Self { + fn from_raw(scale: TimeScale, raw: UnscaledTime) -> Self { match scale { TimeScale::TAI => Time::TAI(TAI(raw)), TimeScale::TCB => Time::TCB(TCB(raw)), @@ -402,7 +632,7 @@ impl Time { } } - fn raw(&self) -> RawTime { + fn raw(&self) -> UnscaledTime { match self { Time::TAI(tai) => tai.0, Time::TCB(tcb) => tcb.0, @@ -567,17 +797,17 @@ mod tests { #[test] fn test_raw_time_is_negative() { - assert!(RawTime { + 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 } @@ -588,14 +818,14 @@ mod tests { fn test_raw_time_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 +833,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 +841,7 @@ mod tests { }, TestCase { desc: "exactly one hour", - time: RawTime { + time: UnscaledTime { seconds: SECONDS_PER_HOUR, attoseconds: 0, }, @@ -619,7 +849,7 @@ mod tests { }, TestCase { desc: "one day and one hour", - time: RawTime { + time: UnscaledTime { seconds: SECONDS_PER_HOUR * 25, attoseconds: 0, }, @@ -627,7 +857,7 @@ mod tests { }, TestCase { desc: "one attosecond less than the epoch", - time: RawTime { + time: UnscaledTime { seconds: -1, attoseconds: ATTOSECONDS_PER_SECOND - 1, }, @@ -635,7 +865,7 @@ mod tests { }, TestCase { desc: "one hour less than the epoch", - time: RawTime { + time: UnscaledTime { seconds: -SECONDS_PER_HOUR, attoseconds: 0, }, @@ -643,7 +873,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 +881,7 @@ mod tests { }, TestCase { desc: "one day less than the epoch", - time: RawTime { + time: UnscaledTime { seconds: -SECONDS_PER_DAY, attoseconds: 0, }, @@ -660,7 +890,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, }, @@ -682,14 +912,14 @@ mod tests { fn test_raw_time_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 +927,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 +935,7 @@ mod tests { }, TestCase { desc: "one minute", - time: RawTime { + time: UnscaledTime { seconds: SECONDS_PER_MINUTE, attoseconds: 0, }, @@ -713,7 +943,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 +951,7 @@ mod tests { }, TestCase { desc: "exactly one hour", - time: RawTime { + time: UnscaledTime { seconds: SECONDS_PER_HOUR, attoseconds: 0, }, @@ -729,7 +959,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 +967,7 @@ mod tests { }, TestCase { desc: "one attosecond less than the epoch", - time: RawTime { + time: UnscaledTime { seconds: -1, attoseconds: ATTOSECONDS_PER_SECOND - 1, }, @@ -745,7 +975,7 @@ mod tests { }, TestCase { desc: "one minute less than the epoch", - time: RawTime { + time: UnscaledTime { seconds: -SECONDS_PER_MINUTE, attoseconds: 0, }, @@ -753,7 +983,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, }, @@ -775,14 +1005,14 @@ mod tests { fn test_raw_time_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 +1020,7 @@ mod tests { }, TestCase { desc: "one attosecond less than one second", - time: RawTime { + time: UnscaledTime { seconds: 0, attoseconds: ATTOSECONDS_PER_SECOND - 1, }, @@ -798,7 +1028,7 @@ mod tests { }, TestCase { desc: "one second", - time: RawTime { + time: UnscaledTime { seconds: 1, attoseconds: 0, }, @@ -806,7 +1036,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 +1044,7 @@ mod tests { }, TestCase { desc: "exactly one minute", - time: RawTime { + time: UnscaledTime { seconds: SECONDS_PER_MINUTE, attoseconds: 0, }, @@ -822,7 +1052,7 @@ mod tests { }, TestCase { desc: "one minute and one second", - time: RawTime { + time: UnscaledTime { seconds: SECONDS_PER_MINUTE + 1, attoseconds: 0, }, @@ -830,7 +1060,7 @@ mod tests { }, TestCase { desc: "one attosecond less than the epoch", - time: RawTime { + time: UnscaledTime { seconds: -1, attoseconds: ATTOSECONDS_PER_SECOND - 1, }, @@ -838,7 +1068,7 @@ mod tests { }, TestCase { desc: "one second less than the epoch", - time: RawTime { + time: UnscaledTime { seconds: -1, attoseconds: 0, }, @@ -846,7 +1076,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, }, @@ -866,7 +1096,7 @@ mod tests { #[test] fn test_raw_time_subseconds_with_positive_seconds() { - let time = RawTime { + let time = UnscaledTime { seconds: 0, attoseconds: 123_456_789_012_345_678, }; @@ -921,7 +1151,7 @@ mod tests { #[test] fn test_raw_time_subseconds_with_negative_seconds() { - let time = RawTime { + let time = UnscaledTime { seconds: -1, attoseconds: 123_456_789_012_345_678, }; @@ -979,8 +1209,8 @@ mod tests { struct TestCase { desc: &'static str, delta: TimeDelta, - time: RawTime, - expected: RawTime, + time: UnscaledTime, + expected: UnscaledTime, } let test_cases = [ @@ -990,11 +1220,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 +1235,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 +1250,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 +1265,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, }, @@ -1061,8 +1291,8 @@ mod tests { struct TestCase { desc: &'static str, delta: TimeDelta, - time: RawTime, - expected: RawTime, + time: UnscaledTime, + expected: UnscaledTime, } let test_cases = [ @@ -1072,11 +1302,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 +1317,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 +1332,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 +1347,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 +1362,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, }, @@ -1212,42 +1442,42 @@ mod tests { [ ( TimeScale::TAI, - Time::TAI(TAI(RawTime { + Time::TAI(TAI(UnscaledTime { seconds: -211813488000, attoseconds: 0, })), ), ( TimeScale::TCB, - Time::TCB(TCB(RawTime { + Time::TCB(TCB(UnscaledTime { seconds: -211813488000, attoseconds: 0, })), ), ( TimeScale::TCG, - Time::TCG(TCG(RawTime { + Time::TCG(TCG(UnscaledTime { seconds: -211813488000, attoseconds: 0, })), ), ( TimeScale::TDB, - Time::TDB(TDB(RawTime { + Time::TDB(TDB(UnscaledTime { seconds: -211813488000, attoseconds: 0, })), ), ( TimeScale::TT, - Time::TT(TT(RawTime { + Time::TT(TT(UnscaledTime { seconds: -211813488000, attoseconds: 0, })), ), ( TimeScale::UT1, - Time::UT1(UT1(RawTime { + Time::UT1(UT1(UnscaledTime { seconds: -211813488000, attoseconds: 0, })), @@ -1278,7 +1508,7 @@ mod tests { #[test] fn test_time_wall_clock_hour() { - let raw_time = RawTime::default(); + let raw_time = UnscaledTime::default(); let expected = raw_time.hour(); for scale in TIME_SCALES { let time = Time::from_raw(scale, raw_time); @@ -1293,7 +1523,7 @@ mod tests { #[test] fn test_time_wall_clock_minute() { - let raw_time = RawTime::default(); + let raw_time = UnscaledTime::default(); let expected = raw_time.minute(); for scale in TIME_SCALES { let time = Time::from_raw(scale, raw_time); @@ -1308,7 +1538,7 @@ mod tests { #[test] fn test_time_wall_clock_second() { - let raw_time = RawTime::default(); + let raw_time = UnscaledTime::default(); let expected = raw_time.second(); for scale in TIME_SCALES { let time = Time::from_raw(scale, raw_time); @@ -1323,7 +1553,7 @@ mod tests { #[test] fn test_time_wall_clock_millisecond() { - let raw_time = RawTime::default(); + let raw_time = UnscaledTime::default(); let expected = raw_time.millisecond(); for scale in TIME_SCALES { let time = Time::from_raw(scale, raw_time); @@ -1338,7 +1568,7 @@ mod tests { #[test] fn test_time_wall_clock_microsecond() { - let raw_time = RawTime::default(); + let raw_time = UnscaledTime::default(); let expected = raw_time.microsecond(); for scale in TIME_SCALES { let time = Time::from_raw(scale, raw_time); @@ -1353,7 +1583,7 @@ mod tests { #[test] fn test_time_wall_clock_nanosecond() { - let raw_time = RawTime::default(); + let raw_time = UnscaledTime::default(); let expected = raw_time.nanosecond(); for scale in TIME_SCALES { let time = Time::from_raw(scale, raw_time); @@ -1368,7 +1598,7 @@ mod tests { #[test] fn test_time_wall_clock_picosecond() { - let raw_time = RawTime::default(); + let raw_time = UnscaledTime::default(); let expected = raw_time.picosecond(); for scale in TIME_SCALES { let time = Time::from_raw(scale, raw_time); @@ -1383,7 +1613,7 @@ mod tests { #[test] fn test_time_wall_clock_femtosecond() { - let raw_time = RawTime::default(); + let raw_time = UnscaledTime::default(); let expected = raw_time.femtosecond(); for scale in TIME_SCALES { let time = Time::from_raw(scale, raw_time); @@ -1398,7 +1628,7 @@ mod tests { #[test] fn test_time_wall_clock_attosecond() { - let raw_time = RawTime::default(); + let raw_time = UnscaledTime::default(); let expected = raw_time.attosecond(); for scale in TIME_SCALES { let time = Time::from_raw(scale, raw_time); From 477e73fa81aa1b0d693c3e4cd5e5920749ca3e7b Mon Sep 17 00:00:00 2001 From: AngusGMorrison Date: Fri, 9 Feb 2024 09:18:05 +0000 Subject: [PATCH 02/14] Update Time call sites --- crates/lox_core/src/coords/states.rs | 58 +- crates/lox_core/src/coords/two_body.rs | 133 ++-- crates/lox_core/src/time/continuous.rs | 843 ++++++------------------- crates/lox_core/src/time/intervals.rs | 2 +- 4 files changed, 300 insertions(+), 736 deletions(-) diff --git a/crates/lox_core/src/coords/states.rs b/crates/lox_core/src/coords/states.rs index 55244cf8..37102214 100644 --- a/crates/lox_core/src/coords/states.rs +++ b/crates/lox_core/src/coords/states.rs @@ -10,23 +10,23 @@ use float_eq::float_eq; use glam::{DMat3, DVec3}; use crate::math::{mod_two_pi, normalize_two_pi}; -use crate::time::continuous::Time; +use crate::time::continuous::{Time, TimeScale}; -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; +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)] -pub struct CartesianState { - time: Time, +pub struct CartesianState { + time: Time, position: DVec3, velocity: DVec3, } -impl CartesianState { - pub fn new(time: Time, position: DVec3, velocity: DVec3) -> Self { +impl CartesianState { + pub fn new(time: Time, position: DVec3, velocity: DVec3) -> Self { Self { time, position, @@ -43,16 +43,16 @@ impl CartesianState { } } -impl TwoBodyState for CartesianState { - fn time(&self) -> Time { +impl TwoBodyState for CartesianState { + fn time(&self) -> Time { self.time } - fn to_cartesian_state(&self, _grav_param: f64) -> CartesianState { + fn to_cartesian_state(&self, _grav_param: f64) -> CartesianState { *self } - fn to_keplerian_state(&self, grav_param: f64) -> KeplerianState { + fn to_keplerian_state(&self, grav_param: f64) -> KeplerianState { let r = self.position.length(); let v = self.velocity.length(); let h = self.position.cross(self.velocity); @@ -119,8 +119,8 @@ impl TwoBodyState for CartesianState { } #[derive(Debug, Copy, Clone, PartialEq)] -pub struct KeplerianState { - time: Time, +pub struct KeplerianState { + time: Time, semi_major: f64, eccentricity: f64, inclination: f64, @@ -129,9 +129,9 @@ pub struct KeplerianState { true_anomaly: f64, } -impl KeplerianState { +impl KeplerianState { pub fn new( - time: Time, + time: Time, semi_major: f64, eccentricity: f64, inclination: f64, @@ -195,12 +195,12 @@ impl KeplerianState { } } -impl TwoBodyState for KeplerianState { - fn time(&self) -> Time { +impl TwoBodyState for KeplerianState { + fn time(&self) -> Time { self.time } - fn to_cartesian_state(&self, grav_param: f64) -> CartesianState { + 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) @@ -208,7 +208,7 @@ impl TwoBodyState for KeplerianState { CartesianState::new(self.time, rot * pos, rot * vel) } - fn to_keplerian_state(&self, _grav_param: f64) -> KeplerianState { + fn to_keplerian_state(&self, _grav_param: f64) -> KeplerianState { *self } } @@ -235,13 +235,13 @@ mod tests { use glam::DVec3; use crate::bodies::{Earth, PointMass}; - use crate::time::continuous::TimeScale; + use crate::time::continuous::{TimeScale, TDB}; use super::*; #[test] fn test_elliptic() { - let time = Time::j2000(TimeScale::TDB); + let time = Time::::j2000(); let grav_param = 3.9860047e14; let semi_major = 24464560.0; let eccentricity = 0.7311; @@ -297,7 +297,7 @@ mod tests { #[test] fn test_circular() { - let time = Time::j2000(TimeScale::TDB); + let time = Time::::j2000(); let grav_param = 3.986004418e14; let semi_major = 6778136.6; let eccentricity = 0.0; @@ -338,7 +338,7 @@ mod tests { #[test] fn test_circular_orekit() { - let time = Time::j2000(TimeScale::TDB); + let time = Time::::j2000(); let grav_param = 3.9860047e14; let semi_major = 24464560.0; let eccentricity = 0.0; @@ -370,7 +370,7 @@ mod tests { #[test] fn test_hyperbolic_orekit() { - let time = Time::j2000(TimeScale::TDB); + let time = Time::::j2000(); let grav_param = 3.9860047e14; let semi_major = -24464560.0; let eccentricity = 1.7311; @@ -402,7 +402,7 @@ mod tests { #[test] fn test_equatorial() { - let time = Time::j2000(TimeScale::TDB); + let time = Time::::j2000(); let grav_param = 3.9860047e14; let semi_major = 24464560.0; let eccentricity = 0.7311; @@ -434,7 +434,7 @@ mod tests { #[test] fn test_circular_equatorial() { - let time = Time::j2000(TimeScale::TDB); + let time = Time::::j2000(); let grav_param = 3.9860047e14; let semi_major = 24464560.0; let eccentricity = 0.0; @@ -466,7 +466,7 @@ mod tests { #[test] fn test_iss() { - let time = Time::j2000(TimeScale::TDB); + let time = Time::::j2000(); 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(); diff --git a/crates/lox_core/src/coords/two_body.rs b/crates/lox_core/src/coords/two_body.rs index 4484f669..4a08c07f 100644 --- a/crates/lox_core/src/coords/two_body.rs +++ b/crates/lox_core/src/coords/two_body.rs @@ -12,35 +12,38 @@ 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, + O: PointMass + Copy, + F: InertialFrame + Copy, { - fn to_cartesian(&self) -> Cartesian; + fn to_cartesian(&self) -> Cartesian; - fn to_keplerian(&self) -> Keplerian; + fn to_keplerian(&self) -> Keplerian; } #[derive(Debug, Copy, Clone, PartialEq)] -pub struct Cartesian +pub struct Cartesian where - T: PointMass + Copy, - S: ReferenceFrame + Copy, + T: TimeScale, + O: PointMass + Copy, + F: ReferenceFrame + Copy, { - state: CartesianState, - origin: T, - frame: S, + state: CartesianState, + origin: O, + frame: F, } -impl Cartesian +impl Cartesian where - T: PointMass + Copy, - S: ReferenceFrame + Copy, + T: TimeScale, + O: PointMass + Copy, + F: ReferenceFrame + Copy, { - pub fn new(time: Time, origin: T, frame: S, position: DVec3, velocity: DVec3) -> Self { + pub fn new(time: Time, origin: O, frame: F, position: DVec3, velocity: DVec3) -> Self { let state = CartesianState::new(time, position, velocity); Self { state, @@ -49,7 +52,7 @@ where } } - pub fn time(&self) -> Time { + pub fn time(&self) -> Time { self.state.time() } @@ -62,27 +65,29 @@ where } } -impl TwoBody for Cartesian +impl TwoBody for Cartesian where - T: PointMass + Copy, - S: InertialFrame + Copy, + T: TimeScale, + O: PointMass + Copy, + F: InertialFrame + Copy, { - fn to_cartesian(&self) -> Cartesian { + 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, + 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,12 +98,13 @@ where } } -impl From> for Cartesian +impl From> for Cartesian where - T: PointMass + Copy, - S: InertialFrame + Copy, + T: TimeScale, + 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 { @@ -110,26 +116,28 @@ where } #[derive(Debug, Copy, Clone, PartialEq)] -pub struct Keplerian +pub struct Keplerian where - T: PointMass + Copy, - S: InertialFrame + Copy, + T: TimeScale, + O: PointMass + Copy, + F: InertialFrame + Copy, { - state: KeplerianState, - origin: T, - frame: S, + state: KeplerianState, + origin: O, + frame: F, } -impl Keplerian +impl Keplerian where - T: PointMass + Copy, - S: InertialFrame + Copy, + T: TimeScale, + 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, @@ -153,7 +161,7 @@ where } } - pub fn time(&self) -> Time { + pub fn time(&self) -> Time { self.state.time() } @@ -182,27 +190,29 @@ where } } -impl TwoBody for Keplerian +impl TwoBody for Keplerian where - T: PointMass + Copy, - S: InertialFrame + Copy, + T: TimeScale, + O: PointMass + Copy, + F: InertialFrame + Copy, { - fn to_cartesian(&self) -> Cartesian { + 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, + 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,12 +223,13 @@ where } } -impl From> for Keplerian +impl From> for Keplerian where - T: PointMass + Copy, - S: InertialFrame + Copy, + T: TimeScale, + 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 { @@ -236,7 +247,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 +255,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(date, utc); let pos = DVec3::new( -0.107622532467967e7, -0.676589636432773e7, @@ -277,7 +288,7 @@ mod tests { 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(date, utc); let semi_major = 24464560.0e-3; let eccentricity = 0.7311; let inclination = 0.122138; diff --git a/crates/lox_core/src/time/continuous.rs b/crates/lox_core/src/time/continuous.rs index d20fd0ac..1d915741 100644 --- a/crates/lox_core/src/time/continuous.rs +++ b/crates/lox_core/src/time/continuous.rs @@ -15,10 +15,11 @@ use std::fmt; use std::fmt::{Display, Formatter}; use std::marker::PhantomData; -use std::ops::{Add, AddAssign, Sub}; +use std::ops::{Add, Sub}; use num::{abs, ToPrimitive}; +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, }; @@ -61,6 +62,18 @@ impl UnscaledTime { fn is_negative(&self) -> bool { self.seconds < 0 } + + /// The fractional number of Julian days since J2000. + 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 + } + + /// The fractional number of Julian centuries since J2000. + fn centuries_since_j2000(&self) -> f64 { + self.days_since_j2000() / DAYS_PER_JULIAN_CENTURY + } } impl Display for UnscaledTime { @@ -174,80 +187,95 @@ impl WallClock for UnscaledTime { } } -pub trait TimeScaleTrait { +/// Marker trait with associated constants denoting a continuous astronomical time scale. +pub trait TimeScale { const ABBREVIATION: &'static str; const NAME: &'static str; } -/// Barycentric Coordinate Time. Defaults to the J2000 epoch. +/// International Atomic Time. #[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub struct TCB2; +pub struct TAI; -impl TimeScaleTrait for TCB2 { +impl TimeScale for TAI { + const ABBREVIATION: &'static str = "TAI"; + const NAME: &'static str = "International Atomic Time"; +} + +/// Barycentric Coordinate Time. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct TCB; + +impl TimeScale for TCB { const ABBREVIATION: &'static str = "TCB"; const NAME: &'static str = "Barycentric Coordinate Time"; } -/// Geocentric Coordinate Time. Defaults to the J2000 epoch. +/// Geocentric Coordinate Time. #[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub struct TCG2; +pub struct TCG; -impl TimeScaleTrait for TCG2 { +impl TimeScale for TCG { const ABBREVIATION: &'static str = "TCG"; const NAME: &'static str = "Geocentric Coordinate Time"; } -/// Barycentric Dynamical Time. Defaults to the J2000 epoch. +/// Barycentric Dynamical Time. #[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub struct TDB2; +pub struct TDB; -impl TimeScaleTrait for TDB2 { +impl TimeScale for TDB { const ABBREVIATION: &'static str = "TDB"; const NAME: &'static str = "Barycentric Dynamical Time"; } -/// Terrestrial Time. Defaults to the J2000 epoch. +/// Terrestrial Time. #[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub struct TT2; +pub struct TT; -impl TimeScaleTrait for TT2 { +impl TimeScale for TT { const ABBREVIATION: &'static str = "TT"; const NAME: &'static str = "Terrestrial Time"; } -/// Universal Time. Defaults to the J2000 epoch. +/// Universal Time. #[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub struct UT12; +pub struct UT1; -impl TimeScaleTrait for UT12 { +impl TimeScale for UT1 { const ABBREVIATION: &'static str = "UT1"; const NAME: &'static str = "Universal Time"; } +/// An instant in time in a given time scale. #[derive(Debug, Default, Eq, PartialEq)] -pub struct TimeGeneric { +pub struct Time { + // The `TimeScale` is always known statically, and all data related to the `TimeScale` required by `Time` is + // accessed via the scale's associated constants. Hence, we don't need an actual `T` at runtime, and we don't + // require additional bounds on `T` such as `Copy` and `Default`. scale: PhantomData, timestamp: UnscaledTime, } // Must be manually implemented, since derive macros always bound the generic parameters by the given trait, not the -// tightest possible bound, which in this case is given by PhantomData. +// tightest possible bound. I.e., `TimeScale` is not inherently `Copy`, but `PhantomData` is. // See https://github.com/rust-lang/rust/issues/108894#issuecomment-1459943821 -impl Clone for TimeGeneric { +impl Clone for Time { fn clone(&self) -> Self { *self } } -impl Copy for TimeGeneric {} +impl Copy for Time {} -impl From for TimeGeneric { +impl From for Time { fn from(timestamp: UnscaledTime) -> Self { Self::from_unscaled(timestamp) } } -impl TimeGeneric { +impl Time { + /// Instantiates a [Time] in the given scale from seconds and attoseconds since the epoch. pub fn new(seconds: i64, attoseconds: u64) -> Self { Self { scale: PhantomData, @@ -258,6 +286,7 @@ impl TimeGeneric { } } + /// Instantiates a [Time] in the given scale from an [UnscaledTime]. pub fn from_unscaled(timestamp: UnscaledTime) -> Self { Self { scale: PhantomData, @@ -265,7 +294,7 @@ impl TimeGeneric { } } - /// Instantiates a `Time` of the given scale from a date and UTC timestamp. + /// Instantiates a [Time] in the given scale from a date and UTC timestamp. pub fn from_date_and_utc_timestamp(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; @@ -279,15 +308,11 @@ impl TimeGeneric { Self::from_unscaled(unscaled) } - /// Instantiates a `Time` of the given scale from a UTC datetime. + /// Instantiates a [Time] in the given scale from a UTC datetime. pub fn from_utc_datetime(dt: UTCDateTime) -> Self { Self::from_date_and_utc_timestamp(dt.date(), dt.time()) } - pub fn unscaled(&self) -> UnscaledTime { - self.timestamp - } - /// Returns the J2000 epoch in the given timescale. pub fn j2000() -> Self { Self { @@ -305,31 +330,39 @@ impl TimeGeneric { Self::from_date_and_utc_timestamp(first_proleptic_day, midday) } + /// The underlying unscaled timestamp. + pub fn unscaled(&self) -> UnscaledTime { + self.timestamp + } + /// The number of whole seconds since J2000. - pub fn seconds(self) -> i64 { + pub fn seconds(&self) -> i64 { self.timestamp.seconds } /// The number of attoseconds from the last whole second. - pub fn attoseconds(self) -> u64 { + pub fn attoseconds(&self) -> u64 { 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 + pub fn days_since_j2000(&self) -> f64 { + 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 TimeGeneric { +impl Display for Time { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "{} {}", self.timestamp, T::ABBREVIATION) } } -impl Add for TimeGeneric { +impl Add for Time { type Output = Self; fn add(self, rhs: TimeDelta) -> Self::Output { @@ -337,7 +370,7 @@ impl Add for TimeGeneric { } } -impl Sub for TimeGeneric { +impl Sub for Time { type Output = Self; fn sub(self, rhs: TimeDelta) -> Self::Output { @@ -345,7 +378,7 @@ impl Sub for TimeGeneric { } } -impl WallClock for TimeGeneric { +impl WallClock for Time { fn hour(&self) -> i64 { self.timestamp.hour() } @@ -383,420 +416,19 @@ impl WallClock for TimeGeneric { } } -/// The continuous time scales supported by Lox. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum TimeScale { - TAI, - TCB, - TCG, - TDB, - TT, - UT1, -} - -impl Display for TimeScale { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "{}", Into::<&str>::into(*self)) - } -} - -#[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", - } - } -} - /// CalendarDate allows continuous time formats to report their date in their respective calendar. pub trait CalendarDate { fn date(&self) -> Date; } -/// International Atomic Time. Defaults to the J2000 epoch. -#[derive(Debug, Copy, Default, Clone, Eq, PartialEq)] -pub struct TAI(UnscaledTime); - -impl TAI { - pub fn to_ut1(&self, _dut: TimeDelta, _dat: TimeDelta) -> UT1 { - todo!() - } -} - -impl CalendarDate for TAI { - fn date(&self) -> Date { - todo!() - } -} - -/// Barycentric Coordinate Time. Defaults to the J2000 epoch. -#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)] -pub struct TCB(UnscaledTime); - -/// Geocentric Coordinate Time. Defaults to the J2000 epoch. -#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)] -pub struct TCG(UnscaledTime); - -/// Barycentric Dynamical Time. Defaults to the J2000 epoch. -#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)] -pub struct TDB(UnscaledTime); - -/// Terrestrial Time. Defaults to the J2000 epoch. -#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)] -pub struct TT(UnscaledTime); - -/// Universal Time. Defaults to the J2000 epoch. -#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)] -pub struct UT1(UnscaledTime); - -/// Implements the `WallClock` trait for the a time scale based on [UnscaledTime] 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() - } - } - - #[cfg(test)] - mod $test_module { - use super::{$time_scale, UnscaledTime}; - use crate::time::WallClock; - - const RAW_TIME: UnscaledTime = UnscaledTime { - 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()); - } - } - }; -} - -// 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); - -/// `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), -} - -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 { - 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 = UnscaledTime { - seconds, - attoseconds, - }; - Self::from_raw(scale, raw) - } - - /// Instantiates a `Time` of the given scale from a UTC datetime. - pub fn from_utc_datetime(scale: TimeScale, 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, 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 { - // 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: UnscaledTime) -> 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) -> UnscaledTime { - 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 number of whole seconds since J2000. - pub fn seconds(&self) -> i64 { - self.raw().seconds - } - - /// The number of attoseconds from the last whole second. - pub fn attoseconds(&self) -> u64 { - self.raw().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 - } -} - -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(), - ) - } -} - -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(), - } - } - - 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(), - } - } - - 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(), - } - } - - 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(), - } - } - - 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(), - } - } - - 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(), - } - } - - 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(), - } - } - - 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(), - } - } - - 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(), - } - } -} - #[cfg(test)] mod tests { - use super::*; use crate::time::dates::Calendar::Gregorian; - 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() { + fn test_unscaled_time_is_negative() { assert!(UnscaledTime { seconds: -1, attoseconds: 0 @@ -815,7 +447,7 @@ mod tests { } #[test] - fn test_raw_time_hour() { + fn test_unscaled_time_hour() { struct TestCase { desc: &'static str, time: UnscaledTime, @@ -909,7 +541,7 @@ mod tests { } #[test] - fn test_raw_time_minute() { + fn test_unscaled_time_minute() { struct TestCase { desc: &'static str, time: UnscaledTime, @@ -1002,7 +634,7 @@ mod tests { } #[test] - fn test_raw_time_second() { + fn test_unscaled_time_second() { struct TestCase { desc: &'static str, time: UnscaledTime, @@ -1095,7 +727,7 @@ mod tests { } #[test] - fn test_raw_time_subseconds_with_positive_seconds() { + fn test_unscaled_time_subseconds_with_positive_seconds() { let time = UnscaledTime { seconds: 0, attoseconds: 123_456_789_012_345_678, @@ -1150,7 +782,7 @@ mod tests { } #[test] - fn test_raw_time_subseconds_with_negative_seconds() { + fn test_unscaled_time_subseconds_with_negative_seconds() { let time = UnscaledTime { seconds: -1, attoseconds: 123_456_789_012_345_678, @@ -1205,7 +837,7 @@ mod tests { } #[test] - fn test_raw_time_add_time_delta() { + fn test_unscaled_time_add_time_delta() { struct TestCase { desc: &'static str, delta: TimeDelta, @@ -1287,7 +919,7 @@ mod tests { } #[test] - fn test_raw_time_sub_time_delta() { + fn test_unscaled_time_sub_time_delta() { struct TestCase { desc: &'static str, delta: TimeDelta, @@ -1384,37 +1016,24 @@ mod tests { } #[test] - fn test_timescale_into_str() { - let test_cases = [ - (TimeScale::TAI, "TAI"), - (TimeScale::TCB, "TCB"), - (TimeScale::TCG, "TCG"), - (TimeScale::TDB, "TDB"), - (TimeScale::TT, "TT"), - (TimeScale::UT1, "UT1"), - ]; + fn test_unscaled_time_days_since_j2000() {} - for (scale, expected) in test_cases { - assert_eq!(Into::<&str>::into(scale), expected); - } - } + #[test] + fn test_unscaled_time_centuries_since_j2000() {} #[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(date, utc); + let expected = Time::::from_utc_datetime(datetime); + assert_eq!(actual, expected); } #[test] fn test_time_display() { - let time = Time::TAI(TAI::default()); + let time = Time::::j2000(); let expected = "12:00:00.000.000.000.000.000.000 TAI".to_string(); let actual = time.to_string(); assert_eq!(actual, expected); @@ -1422,222 +1041,156 @@ 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(); + let expected = Time { + scale: PhantomData::::default(), + timestamp: UnscaledTime::default(), + }; + assert_eq!(*expected, actual); } #[test] fn test_time_jd0() { - [ - ( - TimeScale::TAI, - Time::TAI(TAI(UnscaledTime { - seconds: -211813488000, - attoseconds: 0, - })), - ), - ( - TimeScale::TCB, - Time::TCB(TCB(UnscaledTime { - seconds: -211813488000, - attoseconds: 0, - })), - ), - ( - TimeScale::TCG, - Time::TCG(TCG(UnscaledTime { - seconds: -211813488000, - attoseconds: 0, - })), - ), - ( - TimeScale::TDB, - Time::TDB(TDB(UnscaledTime { - seconds: -211813488000, - attoseconds: 0, - })), - ), - ( - TimeScale::TT, - Time::TT(TT(UnscaledTime { - seconds: -211813488000, - attoseconds: 0, - })), - ), - ( - TimeScale::UT1, - Time::UT1(UT1(UnscaledTime { - seconds: -211813488000, - attoseconds: 0, - })), - ), - ] - .iter() - .for_each(|(scale, expected)| { - let actual = Time::jd0(*scale); - assert_eq!(*expected, actual); + let actual = Time::::jd0(); + let expected = Time::::from_unscaled(UnscaledTime { + seconds: -211813488000, + attoseconds: 0, }); - } - - #[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), - ]; - - for (time, expected) in test_cases { - assert_eq!(time.scale(), expected); - } + assert_eq!(*expected, actual); } #[test] fn test_time_wall_clock_hour() { - let raw_time = UnscaledTime::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::::j2000().hour(); + assert_eq!( + actual, expected, + "expected Time to have hour {}, but got {}", + expected, actual + ); } #[test] fn test_time_wall_clock_minute() { - let raw_time = UnscaledTime::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::::j2000().minute(); + assert_eq!( + actual, expected, + "expected Time to have minute {}, but got {}", + expected, actual, + ); } #[test] fn test_time_wall_clock_second() { - let raw_time = UnscaledTime::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::::j2000().second(); + assert_eq!( + actual, expected, + "expected Time to have second {}, but got {}", + expected, actual, + ); } #[test] fn test_time_wall_clock_millisecond() { - let raw_time = UnscaledTime::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::::j2000().millisecond(); + assert_eq!( + actual, expected, + "expected Time to have millisecond {}, but got {}", + expected, actual, + ); } #[test] fn test_time_wall_clock_microsecond() { - let raw_time = UnscaledTime::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::::j2000().microsecond(); + assert_eq!( + actual, expected, + "expected Time to have microsecond {}, but got {}", + expected, actual, + ); } #[test] fn test_time_wall_clock_nanosecond() { - let raw_time = UnscaledTime::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::::j2000().nanosecond(); + assert_eq!( + actual, expected, + "expected Time to have nanosecond {}, but got {}", + expected, actual, + ); } #[test] fn test_time_wall_clock_picosecond() { - let raw_time = UnscaledTime::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::::j2000().picosecond(); + assert_eq!( + actual, expected, + "expected Time to have picosecond {}, but got {}", + expected, actual, + ); } #[test] fn test_time_wall_clock_femtosecond() { - let raw_time = UnscaledTime::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::::j2000().femtosecond(); + assert_eq!( + actual, expected, + "expected Time to have femtosecond {}, but got {}", + expected, actual, + ); } #[test] fn test_time_wall_clock_attosecond() { - let raw_time = UnscaledTime::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::::j2000().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..6467b151 100644 --- a/crates/lox_core/src/time/intervals.rs +++ b/crates/lox_core/src/time/intervals.rs @@ -4,7 +4,7 @@ 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 { +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 From ef42e29a6f725092015ba2b20f4eb6dfcb39d5f5 Mon Sep 17 00:00:00 2001 From: AngusGMorrison Date: Fri, 9 Feb 2024 13:17:40 +0000 Subject: [PATCH 03/14] Update Python bindings --- crates/lox_core/src/coords/states.rs | 28 ++++++- crates/lox_core/src/coords/two_body.rs | 48 +++++++++++- crates/lox_core/src/earth/nutation.rs | 17 ++--- crates/lox_core/src/time/continuous.rs | 48 ++++++++++-- crates/lox_core/src/time/intervals.rs | 55 -------------- crates/lox_py/src/time.rs | 100 +++++++++++++++++-------- 6 files changed, 189 insertions(+), 107 deletions(-) diff --git a/crates/lox_core/src/coords/states.rs b/crates/lox_core/src/coords/states.rs index 37102214..ce561792 100644 --- a/crates/lox_core/src/coords/states.rs +++ b/crates/lox_core/src/coords/states.rs @@ -18,13 +18,24 @@ pub trait TwoBodyState { fn to_keplerian_state(&self, grav_param: f64) -> KeplerianState; } -#[derive(Debug, Copy, Clone, PartialEq)] +#[derive(Debug, PartialEq)] pub struct CartesianState { time: Time, position: DVec3, velocity: DVec3, } +// Must be manually implemented, since derive macros always bound the generic parameters by the given trait, not the +// tightest possible bound. I.e., `TimeScale` is not inherently `Copy`, but `Time` is. +// See https://github.com/rust-lang/rust/issues/108894#issuecomment-1459943821 +impl Clone for CartesianState { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for CartesianState {} + impl CartesianState { pub fn new(time: Time, position: DVec3, velocity: DVec3) -> Self { Self { @@ -118,7 +129,7 @@ impl TwoBodyState for CartesianState { } } -#[derive(Debug, Copy, Clone, PartialEq)] +#[derive(Debug, PartialEq)] pub struct KeplerianState { time: Time, semi_major: f64, @@ -129,6 +140,17 @@ pub struct KeplerianState { true_anomaly: f64, } +// Must be manually implemented, since derive macros always bound the generic parameters by the given trait, not the +// tightest possible bound. I.e., `TimeScale` is not inherently `Copy`, but `Time` is. +// See https://github.com/rust-lang/rust/issues/108894#issuecomment-1459943821 +impl Clone for KeplerianState { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for KeplerianState {} + impl KeplerianState { pub fn new( time: Time, @@ -235,7 +257,7 @@ mod tests { use glam::DVec3; use crate::bodies::{Earth, PointMass}; - use crate::time::continuous::{TimeScale, TDB}; + use crate::time::continuous::TDB; use super::*; diff --git a/crates/lox_core/src/coords/two_body.rs b/crates/lox_core/src/coords/two_body.rs index 4a08c07f..9a3c6600 100644 --- a/crates/lox_core/src/coords/two_body.rs +++ b/crates/lox_core/src/coords/two_body.rs @@ -25,7 +25,7 @@ where fn to_keplerian(&self) -> Keplerian; } -#[derive(Debug, Copy, Clone, PartialEq)] +#[derive(Debug, PartialEq)] pub struct Cartesian where T: TimeScale, @@ -37,6 +37,28 @@ where frame: F, } +// Must be manually implemented, since derive macros always bound the generic parameters by the given trait, not the +// tightest possible bound. I.e., `TimeScale` is not inherently `Copy`, but `Cartesian` is. +// See https://github.com/rust-lang/rust/issues/108894#issuecomment-1459943821 +impl Clone for Cartesian +where + T: TimeScale, + O: PointMass + Copy, + F: ReferenceFrame + Copy, +{ + fn clone(&self) -> Self { + *self + } +} + +impl Copy for Cartesian +where + T: TimeScale, + O: PointMass + Copy, + F: ReferenceFrame + Copy, +{ +} + impl Cartesian where T: TimeScale, @@ -115,7 +137,7 @@ where } } -#[derive(Debug, Copy, Clone, PartialEq)] +#[derive(Debug, PartialEq)] pub struct Keplerian where T: TimeScale, @@ -127,6 +149,28 @@ where frame: F, } +// Must be manually implemented, since derive macros always bound the generic parameters by the given trait, not the +// tightest possible bound. I.e., `TimeScale` is not inherently `Copy`, but `Keplerian` is. +// See https://github.com/rust-lang/rust/issues/108894#issuecomment-1459943821 +impl Clone for Keplerian +where + T: TimeScale, + O: PointMass + Copy, + F: InertialFrame + Copy, +{ + fn clone(&self) -> Self { + *self + } +} + +impl Copy for Keplerian +where + T: TimeScale, + O: PointMass + Copy, + F: InertialFrame + Copy, +{ +} + impl Keplerian where T: TimeScale, diff --git a/crates/lox_core/src/earth/nutation.rs b/crates/lox_core/src/earth/nutation.rs index 9500a694..1fab714b 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,7 @@ fn point1_microarcsec_to_rad(p1_uas: Point1Microarcsec) -> Radians { #[cfg(test)] mod tests { - use crate::time::continuous::TimeScale; + use crate::time::continuous::{TimeScale, TT}; use float_eq::assert_float_eq; use super::*; @@ -107,7 +106,7 @@ mod tests { #[test] fn test_nutation_iau1980() { - let time = Time::j2000(TimeScale::TT); + let time = Time::::j2000(); let expected = Nutation { longitude: -0.00006750247617532478, obliquity: -0.00002799221238377013, @@ -118,7 +117,7 @@ mod tests { } #[test] fn test_nutation_iau2000a() { - let time = Time::j2000(TimeScale::TT); + let time = Time::::j2000(); let expected = Nutation { longitude: -0.00006754422426417299, obliquity: -0.00002797083119237414, @@ -130,7 +129,7 @@ mod tests { #[test] fn test_nutation_iau2000b() { - let time = Time::j2000(TimeScale::TT); + let time = Time::::j2000(); let expected = Nutation { longitude: -0.00006754261253992235, obliquity: -0.00002797092331098565, @@ -142,7 +141,7 @@ mod tests { #[test] fn test_nutation_iau2006a() { - let time = Time::j2000(TimeScale::TT); + let time = Time::::j2000(); let expected = Nutation { longitude: -0.00006754425598969513, obliquity: -0.00002797083119237414, diff --git a/crates/lox_core/src/time/continuous.rs b/crates/lox_core/src/time/continuous.rs index 1d915741..9a089a11 100644 --- a/crates/lox_core/src/time/continuous.rs +++ b/crates/lox_core/src/time/continuous.rs @@ -17,7 +17,7 @@ use std::fmt::{Display, Formatter}; use std::marker::PhantomData; 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::{ @@ -64,14 +64,14 @@ impl UnscaledTime { } /// The fractional number of Julian days since J2000. - 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; + 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. - fn centuries_since_j2000(&self) -> f64 { + pub fn centuries_since_j2000(&self) -> f64 { self.days_since_j2000() / DAYS_PER_JULIAN_CENTURY } } @@ -424,6 +424,7 @@ pub trait CalendarDate { #[cfg(test)] mod tests { use crate::time::dates::Calendar::Gregorian; + use float_eq::assert_float_eq; use super::*; @@ -1016,7 +1017,42 @@ mod tests { } #[test] - fn test_unscaled_time_days_since_j2000() {} + 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, + }, + ]; + + for tc in test_cases { + let actual = tc.time.days_since_j2000(); + assert_float_eq!(tc.expected, actual, abs <= 1e-12) + } + } #[test] fn test_unscaled_time_centuries_since_j2000() {} diff --git a/crates/lox_core/src/time/intervals.rs b/crates/lox_core/src/time/intervals.rs index 6467b151..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/time.rs b/crates/lox_py/src/time.rs index 565bff01..22100dcb 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,60 @@ use lox_core::time::PerMille; use crate::LoxPyError; #[pyclass(name = "TimeScale")] -pub struct PyTimeScale(pub TimeScale); +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +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 +92,7 @@ impl PyTime { ))] #[new] pub fn new( - scale: &str, + scale: PyTimeScale, year: i64, month: i64, day: i64, @@ -80,7 +106,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 +130,30 @@ 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(Self::from_date_and_utc_timestamp(scale, date, utc)) + } + + pub fn from_date_and_utc_timestamp(scale: PyTimeScale, date: Date, utc: UTC) -> Self { + let timestamp = match scale { + PyTimeScale::TAI => Time::::from_date_and_utc_timestamp(date, utc), + PyTimeScale::TCB => Time::::from_date_and_utc_timestamp(date, utc), + PyTimeScale::TCG => Time::::from_date_and_utc_timestamp(date, utc), + PyTimeScale::TDB => Time::::from_date_and_utc_timestamp(date, utc), + PyTimeScale::TT => Time::::from_date_and_utc_timestamp(date, utc), + PyTimeScale::UT1 => Time::::from_date_and_utc_timestamp(date, utc), + } + .unscaled(); + + PyTime { timestamp, scale } } - 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 } } @@ -129,15 +165,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 +187,7 @@ mod tests { #[test] fn test_time() { let time = PyTime::new( - "TDB", + PyTimeScale::TDB, 2024, 1, 1, @@ -166,7 +202,7 @@ 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"); } From e773bdd2bce9566f74b27eb5f072a4a007e0d17e Mon Sep 17 00:00:00 2001 From: AngusGMorrison Date: Fri, 9 Feb 2024 15:48:59 +0000 Subject: [PATCH 04/14] Dead end with Python integration --- crates/lox_py/src/coords.rs | 40 +++++++++++++++++++++++++++++++++---- crates/lox_py/src/time.rs | 11 +++++++++- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/crates/lox_py/src/coords.rs b/crates/lox_py/src/coords.rs index 7747188b..ed01c20d 100644 --- a/crates/lox_py/src/coords.rs +++ b/crates/lox_py/src/coords.rs @@ -13,6 +13,7 @@ use pyo3::prelude::*; use lox_core::bodies::PointMass; use lox_core::coords::states::{CartesianState, KeplerianState, TwoBodyState}; use lox_core::coords::DVec3; +use lox_core::time::continuous::{Time, TimeScale}; use crate::bodies::PyBody; use crate::frames::PyFrame; @@ -20,17 +21,44 @@ use crate::time::PyTime; #[pyclass(name = "Cartesian")] pub struct PyCartesian { - state: CartesianState, + state: PyCartesianState, origin: PyBody, frame: PyFrame, } +#[pyclass(name = "CartesianState")] +pub struct PyCartesianState { + time: PyTime, + position: DVec3, + velocity: DVec3, +} + +impl PyCartesianState { + pub fn to_keplerian_state(&self) -> PyKeplerianState { + unimplemented!( + "There's no way to create a Rust `Time` dynamically from a `PyTime` enum variant." + ) + } +} + +#[derive(Debug, PartialEq)] +#[pyclass(name = "KeplerianState")] +pub struct PyKeplerianState { + time: PyTime, + semi_major: f64, + eccentricity: f64, + inclination: f64, + ascending_node: f64, + periapsis_argument: f64, + true_anomaly: f64, +} + #[pymethods] impl PyCartesian { #[allow(clippy::too_many_arguments)] #[new] fn new( - time: &PyTime, + time: PyTime, body: PyObject, frame: &str, x: f64, @@ -42,7 +70,11 @@ 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 = PyCartesianState { + time, + position: DVec3::new(x, y, z), + veclocity: DVec3::new(vx, vy, vz), + }; Ok(Self { state, origin, @@ -51,7 +83,7 @@ impl PyCartesian { } fn time(&self) -> PyTime { - PyTime(self.state.time()) + self.state.time } fn reference_frame(&self) -> String { diff --git a/crates/lox_py/src/time.rs b/crates/lox_py/src/time.rs index 22100dcb..a1a664f6 100644 --- a/crates/lox_py/src/time.rs +++ b/crates/lox_py/src/time.rs @@ -9,7 +9,7 @@ use pyo3::{pyclass, pymethods}; use std::fmt::{Display, Formatter}; -use lox_core::time::continuous::{Time, UnscaledTime, TAI, TCB, TCG, TDB, TT, UT1}; +use lox_core::time::continuous::{Time, TimeScale, UnscaledTime, TAI, TCB, TCG, TDB, TT, UT1}; use lox_core::time::dates::Date; use lox_core::time::utc::UTC; use lox_core::time::PerMille; @@ -157,6 +157,15 @@ impl PyTime { } } +impl From> for PyTime { + fn from(time: Time) -> Self { + PyTime { + scale: PyTimeScale::TDB, + timestamp: time.unscaled(), + } + } +} + #[cfg(test)] mod tests { use float_eq::assert_float_eq; From 152944a215c23bfbfb2d887090eb3213f26448c7 Mon Sep 17 00:00:00 2001 From: AngusGMorrison Date: Sat, 10 Feb 2024 12:42:40 +0000 Subject: [PATCH 05/14] Replace PhantomData with Copy bound for TimeScale --- crates/lox_core/src/coords/states.rs | 22 ++--- crates/lox_core/src/time/continuous.rs | 109 ++++++++++--------------- 2 files changed, 55 insertions(+), 76 deletions(-) diff --git a/crates/lox_core/src/coords/states.rs b/crates/lox_core/src/coords/states.rs index ce561792..548567af 100644 --- a/crates/lox_core/src/coords/states.rs +++ b/crates/lox_core/src/coords/states.rs @@ -12,14 +12,14 @@ use glam::{DMat3, DVec3}; use crate::math::{mod_two_pi, normalize_two_pi}; use crate::time::continuous::{Time, TimeScale}; -pub trait TwoBodyState { +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, PartialEq)] -pub struct CartesianState { +pub struct CartesianState { time: Time, position: DVec3, velocity: DVec3, @@ -28,15 +28,15 @@ pub struct CartesianState { // Must be manually implemented, since derive macros always bound the generic parameters by the given trait, not the // tightest possible bound. I.e., `TimeScale` is not inherently `Copy`, but `Time` is. // See https://github.com/rust-lang/rust/issues/108894#issuecomment-1459943821 -impl Clone for CartesianState { +impl Clone for CartesianState { fn clone(&self) -> Self { *self } } -impl Copy for CartesianState {} +impl Copy for CartesianState {} -impl CartesianState { +impl CartesianState { pub fn new(time: Time, position: DVec3, velocity: DVec3) -> Self { Self { time, @@ -54,7 +54,7 @@ impl CartesianState { } } -impl TwoBodyState for CartesianState { +impl TwoBodyState for CartesianState { fn time(&self) -> Time { self.time } @@ -130,7 +130,7 @@ impl TwoBodyState for CartesianState { } #[derive(Debug, PartialEq)] -pub struct KeplerianState { +pub struct KeplerianState { time: Time, semi_major: f64, eccentricity: f64, @@ -143,15 +143,15 @@ pub struct KeplerianState { // Must be manually implemented, since derive macros always bound the generic parameters by the given trait, not the // tightest possible bound. I.e., `TimeScale` is not inherently `Copy`, but `Time` is. // See https://github.com/rust-lang/rust/issues/108894#issuecomment-1459943821 -impl Clone for KeplerianState { +impl Clone for KeplerianState { fn clone(&self) -> Self { *self } } -impl Copy for KeplerianState {} +impl Copy for KeplerianState {} -impl KeplerianState { +impl KeplerianState { pub fn new( time: Time, semi_major: f64, @@ -217,7 +217,7 @@ impl KeplerianState { } } -impl TwoBodyState for KeplerianState { +impl TwoBodyState for KeplerianState { fn time(&self) -> Time { self.time } diff --git a/crates/lox_core/src/time/continuous.rs b/crates/lox_core/src/time/continuous.rs index 9a089a11..723d0ef6 100644 --- a/crates/lox_core/src/time/continuous.rs +++ b/crates/lox_core/src/time/continuous.rs @@ -14,7 +14,6 @@ use std::fmt; use std::fmt::{Display, Formatter}; -use std::marker::PhantomData; use std::ops::{Add, Sub}; use num::abs; @@ -248,37 +247,17 @@ impl TimeScale for UT1 { } /// An instant in time in a given time scale. -#[derive(Debug, Default, Eq, PartialEq)] -pub struct Time { - // The `TimeScale` is always known statically, and all data related to the `TimeScale` required by `Time` is - // accessed via the scale's associated constants. Hence, we don't need an actual `T` at runtime, and we don't - // require additional bounds on `T` such as `Copy` and `Default`. - scale: PhantomData, +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct Time { + scale: T, timestamp: UnscaledTime, } -// Must be manually implemented, since derive macros always bound the generic parameters by the given trait, not the -// tightest possible bound. I.e., `TimeScale` is not inherently `Copy`, but `PhantomData` is. -// See https://github.com/rust-lang/rust/issues/108894#issuecomment-1459943821 -impl Clone for Time { - fn clone(&self) -> Self { - *self - } -} - -impl Copy for Time {} - -impl From for Time { - fn from(timestamp: UnscaledTime) -> Self { - Self::from_unscaled(timestamp) - } -} - -impl Time { +impl Time { /// Instantiates a [Time] in the given scale from seconds and attoseconds since the epoch. - pub fn new(seconds: i64, attoseconds: u64) -> Self { + pub fn new(scale: T, seconds: i64, attoseconds: u64) -> Self { Self { - scale: PhantomData, + scale, timestamp: UnscaledTime { seconds, attoseconds, @@ -287,15 +266,12 @@ impl Time { } /// Instantiates a [Time] in the given scale from an [UnscaledTime]. - pub fn from_unscaled(timestamp: UnscaledTime) -> Self { - Self { - scale: PhantomData, - timestamp, - } + pub fn from_unscaled(scale: T, timestamp: UnscaledTime) -> Self { + Self { scale, timestamp } } /// Instantiates a [Time] in the given scale from a date and UTC timestamp. - pub fn from_date_and_utc_timestamp(date: Date, time: UTC) -> Self { + 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; @@ -305,29 +281,29 @@ impl Time { seconds, attoseconds, }; - Self::from_unscaled(unscaled) + Self::from_unscaled(scale, unscaled) } /// Instantiates a [Time] in the given scale from a UTC datetime. - pub fn from_utc_datetime(dt: UTCDateTime) -> Self { - Self::from_date_and_utc_timestamp(dt.date(), dt.time()) + pub fn from_utc_datetime(scale: T, dt: UTCDateTime) -> Self { + Self::from_date_and_utc_timestamp(scale, dt.date(), dt.time()) } /// Returns the J2000 epoch in the given timescale. - pub fn j2000() -> Self { + pub fn j2000(scale: T) -> Self { Self { - scale: PhantomData, + 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() -> 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(first_proleptic_day, midday) + Self::from_date_and_utc_timestamp(scale, first_proleptic_day, midday) } /// The underlying unscaled timestamp. @@ -356,29 +332,29 @@ impl Time { } } -impl Display for Time { +impl Display for Time { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "{} {}", self.timestamp, T::ABBREVIATION) } } -impl Add for Time { +impl Add for Time { type Output = Self; fn add(self, rhs: TimeDelta) -> Self::Output { - Self::from_unscaled(self.timestamp + rhs) + Self::from_unscaled(self.scale, self.timestamp + rhs) } } -impl Sub for Time { +impl Sub for Time { type Output = Self; fn sub(self, rhs: TimeDelta) -> Self::Output { - Self::from_unscaled(self.timestamp - rhs) + Self::from_unscaled(self.scale, self.timestamp - rhs) } } -impl WallClock for Time { +impl WallClock for Time { fn hour(&self) -> i64 { self.timestamp.hour() } @@ -1062,14 +1038,14 @@ mod tests { 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); - let actual = Time::::from_date_and_utc_timestamp(date, utc); - let expected = Time::::from_utc_datetime(datetime); + 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::::j2000(); + 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); @@ -1077,9 +1053,9 @@ mod tests { #[test] fn test_time_j2000() { - let actual = Time::::j2000(); + let actual = Time::j2000(TAI); let expected = Time { - scale: PhantomData::::default(), + scale: TAI, timestamp: UnscaledTime::default(), }; assert_eq!(*expected, actual); @@ -1087,11 +1063,14 @@ mod tests { #[test] fn test_time_jd0() { - let actual = Time::::jd0(); - let expected = Time::::from_unscaled(UnscaledTime { - seconds: -211813488000, - attoseconds: 0, - }); + let actual = Time::jd0(TAI); + let expected = Time::from_unscaled( + TAI, + UnscaledTime { + seconds: -211813488000, + attoseconds: 0, + }, + ); assert_eq!(*expected, actual); } @@ -1102,7 +1081,7 @@ mod tests { attoseconds: 9876543210, }; let expected = unscaled_time.hour(); - let actual = Time::::j2000().hour(); + let actual = Time::j2000(TAI).hour(); assert_eq!( actual, expected, "expected Time to have hour {}, but got {}", @@ -1117,7 +1096,7 @@ mod tests { attoseconds: 9876543210, }; let expected = unscaled_time.minute(); - let actual = Time::::j2000().minute(); + let actual = Time::j2000(TAI).minute(); assert_eq!( actual, expected, "expected Time to have minute {}, but got {}", @@ -1132,7 +1111,7 @@ mod tests { attoseconds: 9876543210, }; let expected = unscaled_time.second(); - let actual = Time::::j2000().second(); + let actual = Time::j2000(TAI).second(); assert_eq!( actual, expected, "expected Time to have second {}, but got {}", @@ -1147,7 +1126,7 @@ mod tests { attoseconds: 9876543210, }; let expected = unscaled_time.millisecond(); - let actual = Time::::j2000().millisecond(); + let actual = Time::j2000(TAI).millisecond(); assert_eq!( actual, expected, "expected Time to have millisecond {}, but got {}", @@ -1162,7 +1141,7 @@ mod tests { attoseconds: 9876543210, }; let expected = unscaled_time.microsecond(); - let actual = Time::::j2000().microsecond(); + let actual = Time::j2000(TAI).microsecond(); assert_eq!( actual, expected, "expected Time to have microsecond {}, but got {}", @@ -1177,7 +1156,7 @@ mod tests { attoseconds: 9876543210, }; let expected = unscaled_time.nanosecond(); - let actual = Time::::j2000().nanosecond(); + let actual = Time::j2000(TAI).nanosecond(); assert_eq!( actual, expected, "expected Time to have nanosecond {}, but got {}", @@ -1192,7 +1171,7 @@ mod tests { attoseconds: 9876543210, }; let expected = unscaled_time.picosecond(); - let actual = Time::::j2000().picosecond(); + let actual = Time::j2000(TAI).picosecond(); assert_eq!( actual, expected, "expected Time to have picosecond {}, but got {}", @@ -1207,7 +1186,7 @@ mod tests { attoseconds: 9876543210, }; let expected = unscaled_time.femtosecond(); - let actual = Time::::j2000().femtosecond(); + let actual = Time::j2000(TAI).femtosecond(); assert_eq!( actual, expected, "expected Time to have femtosecond {}, but got {}", @@ -1222,7 +1201,7 @@ mod tests { attoseconds: 9876543210, }; let expected = unscaled_time.attosecond(); - let actual = Time::::j2000().attosecond(); + let actual = Time::j2000(TAI).attosecond(); assert_eq!( actual, expected, "expected Time to have attosecond {}, but got {}", From f6420b2631dea48b241b53c7f078176880cb3956 Mon Sep 17 00:00:00 2001 From: AngusGMorrison Date: Sat, 10 Feb 2024 12:51:11 +0000 Subject: [PATCH 06/14] Remove time from TwoBody def and impls --- crates/lox_core/src/coords/states.rs | 100 ++++++--------------------- 1 file changed, 21 insertions(+), 79 deletions(-) diff --git a/crates/lox_core/src/coords/states.rs b/crates/lox_core/src/coords/states.rs index 548567af..c0b6ec67 100644 --- a/crates/lox_core/src/coords/states.rs +++ b/crates/lox_core/src/coords/states.rs @@ -10,39 +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, TimeScale}; -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; +pub trait TwoBodyState { + fn to_cartesian_state(&self, grav_param: f64) -> CartesianState; + fn to_keplerian_state(&self, grav_param: f64) -> KeplerianState; } -#[derive(Debug, PartialEq)] -pub struct CartesianState { - time: Time, +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct CartesianState { position: DVec3, velocity: DVec3, } -// Must be manually implemented, since derive macros always bound the generic parameters by the given trait, not the -// tightest possible bound. I.e., `TimeScale` is not inherently `Copy`, but `Time` is. -// See https://github.com/rust-lang/rust/issues/108894#issuecomment-1459943821 -impl Clone for CartesianState { - fn clone(&self) -> Self { - *self - } -} - -impl Copy for CartesianState {} - -impl CartesianState { - pub fn new(time: Time, position: DVec3, velocity: DVec3) -> Self { - Self { - time, - position, - velocity, - } +impl CartesianState { + pub fn new(position: DVec3, velocity: DVec3) -> Self { + Self { position, velocity } } pub fn position(&self) -> DVec3 { @@ -54,16 +36,12 @@ impl CartesianState { } } -impl TwoBodyState for CartesianState { - fn time(&self) -> Time { - self.time - } - - fn to_cartesian_state(&self, _grav_param: f64) -> CartesianState { +impl TwoBodyState for CartesianState { + fn to_cartesian_state(&self, _grav_param: f64) -> CartesianState { *self } - fn to_keplerian_state(&self, grav_param: f64) -> KeplerianState { + fn to_keplerian_state(&self, grav_param: f64) -> KeplerianState { let r = self.position.length(); let v = self.velocity.length(); let h = self.position.cross(self.velocity); @@ -118,7 +96,6 @@ impl TwoBodyState for CartesianState { } KeplerianState::new( - self.time, semi_major, eccentricity, inclination, @@ -129,9 +106,8 @@ impl TwoBodyState for CartesianState { } } -#[derive(Debug, PartialEq)] -pub struct KeplerianState { - time: Time, +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct KeplerianState { semi_major: f64, eccentricity: f64, inclination: f64, @@ -140,20 +116,8 @@ pub struct KeplerianState { true_anomaly: f64, } -// Must be manually implemented, since derive macros always bound the generic parameters by the given trait, not the -// tightest possible bound. I.e., `TimeScale` is not inherently `Copy`, but `Time` is. -// See https://github.com/rust-lang/rust/issues/108894#issuecomment-1459943821 -impl Clone for KeplerianState { - fn clone(&self) -> Self { - *self - } -} - -impl Copy for KeplerianState {} - -impl KeplerianState { +impl KeplerianState { pub fn new( - time: Time, semi_major: f64, eccentricity: f64, inclination: f64, @@ -162,7 +126,6 @@ impl KeplerianState { true_anomaly: f64, ) -> Self { Self { - time, semi_major, eccentricity, inclination, @@ -217,20 +180,16 @@ impl KeplerianState { } } -impl TwoBodyState for KeplerianState { - fn time(&self) -> Time { - self.time - } - - fn to_cartesian_state(&self, grav_param: f64) -> CartesianState { +impl TwoBodyState for KeplerianState { + 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 { + fn to_keplerian_state(&self, _grav_param: f64) -> KeplerianState { *self } } @@ -257,13 +216,11 @@ mod tests { use glam::DVec3; use crate::bodies::{Earth, PointMass}; - use crate::time::continuous::TDB; use super::*; #[test] fn test_elliptic() { - let time = Time::::j2000(); let grav_param = 3.9860047e14; let semi_major = 24464560.0; let eccentricity = 0.7311; @@ -282,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, @@ -299,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); @@ -319,7 +272,6 @@ mod tests { #[test] fn test_circular() { - let time = Time::::j2000(); let grav_param = 3.986004418e14; let semi_major = 6778136.6; let eccentricity = 0.0; @@ -329,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, @@ -360,7 +311,6 @@ mod tests { #[test] fn test_circular_orekit() { - let time = Time::::j2000(); let grav_param = 3.9860047e14; let semi_major = 24464560.0; let eccentricity = 0.0; @@ -369,7 +319,6 @@ mod tests { let periapsis_arg = 0.0; let true_anomaly = 0.048363; let keplerian = KeplerianState::new( - time, semi_major, eccentricity, inclination, @@ -392,7 +341,6 @@ mod tests { #[test] fn test_hyperbolic_orekit() { - let time = Time::::j2000(); let grav_param = 3.9860047e14; let semi_major = -24464560.0; let eccentricity = 1.7311; @@ -401,7 +349,6 @@ mod tests { let periapsis_arg = 3.10686; let true_anomaly = 0.12741601769795755; let keplerian = KeplerianState::new( - time, semi_major, eccentricity, inclination, @@ -424,7 +371,6 @@ mod tests { #[test] fn test_equatorial() { - let time = Time::::j2000(); let grav_param = 3.9860047e14; let semi_major = 24464560.0; let eccentricity = 0.7311; @@ -433,7 +379,6 @@ mod tests { let periapsis_arg = 3.10686; let true_anomaly = 0.44369564302687126; let keplerian = KeplerianState::new( - time, semi_major, eccentricity, inclination, @@ -456,7 +401,6 @@ mod tests { #[test] fn test_circular_equatorial() { - let time = Time::::j2000(); let grav_param = 3.9860047e14; let semi_major = 24464560.0; let eccentricity = 0.0; @@ -465,7 +409,6 @@ mod tests { let periapsis_arg = 0.0; let true_anomaly = 0.44369564302687126; let keplerian = KeplerianState::new( - time, semi_major, eccentricity, inclination, @@ -488,11 +431,10 @@ mod tests { #[test] fn test_iss() { - let time = Time::::j2000(); 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); From 7143961ffee3638dd1e177dd0c06eae469502c1e Mon Sep 17 00:00:00 2001 From: AngusGMorrison Date: Sat, 10 Feb 2024 13:02:57 +0000 Subject: [PATCH 07/14] Add Time to TwoBody implementations --- crates/lox_core/src/coords/two_body.rs | 105 ++++++++----------------- 1 file changed, 34 insertions(+), 71 deletions(-) diff --git a/crates/lox_core/src/coords/two_body.rs b/crates/lox_core/src/coords/two_body.rs index 9a3c6600..3cec4446 100644 --- a/crates/lox_core/src/coords/two_body.rs +++ b/crates/lox_core/src/coords/two_body.rs @@ -16,68 +16,46 @@ use crate::time::continuous::{Time, TimeScale}; pub trait TwoBody where - T: TimeScale, + T: TimeScale + Copy, O: PointMass + Copy, F: InertialFrame + Copy, { + fn time(&self) -> Time; + fn to_cartesian(&self) -> Cartesian; fn to_keplerian(&self) -> Keplerian; } -#[derive(Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq)] pub struct Cartesian where - T: TimeScale, + T: TimeScale + Copy, O: PointMass + Copy, F: ReferenceFrame + Copy, { - state: CartesianState, + time: Time, + state: CartesianState, origin: O, frame: F, } -// Must be manually implemented, since derive macros always bound the generic parameters by the given trait, not the -// tightest possible bound. I.e., `TimeScale` is not inherently `Copy`, but `Cartesian` is. -// See https://github.com/rust-lang/rust/issues/108894#issuecomment-1459943821 -impl Clone for Cartesian -where - T: TimeScale, - O: PointMass + Copy, - F: ReferenceFrame + Copy, -{ - fn clone(&self) -> Self { - *self - } -} - -impl Copy for Cartesian -where - T: TimeScale, - O: PointMass + Copy, - F: ReferenceFrame + Copy, -{ -} - impl Cartesian where - T: TimeScale, + T: TimeScale + Copy, O: PointMass + Copy, F: ReferenceFrame + Copy, { pub fn new(time: Time, origin: O, frame: F, position: DVec3, velocity: DVec3) -> Self { - let state = CartesianState::new(time, position, velocity); + let state = CartesianState::new(position, velocity); Self { + time, state, origin, frame, } } - pub fn time(&self) -> Time { - self.state.time() - } - pub fn position(&self) -> DVec3 { self.state.position() } @@ -89,10 +67,14 @@ where impl TwoBody for Cartesian where - T: TimeScale, + T: TimeScale + Copy, O: PointMass + Copy, F: InertialFrame + Copy, { + fn time(&self) -> Time { + self.time + } + fn to_cartesian(&self) -> Cartesian { *self } @@ -104,7 +86,7 @@ where impl CoordinateSystem for Cartesian where - T: TimeScale, + T: TimeScale + Copy, O: PointMass + Copy, F: ReferenceFrame + Copy, { @@ -122,7 +104,7 @@ where impl From> for Cartesian where - T: TimeScale, + T: TimeScale + Copy, O: PointMass + Copy, F: InertialFrame + Copy, { @@ -131,49 +113,29 @@ where let state = keplerian.state.to_cartesian_state(grav_param); Cartesian { state, + time: keplerian.time(), origin: keplerian.origin, frame: keplerian.frame, } } } -#[derive(Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq)] pub struct Keplerian where - T: TimeScale, + T: TimeScale + Copy, O: PointMass + Copy, F: InertialFrame + Copy, { - state: KeplerianState, + time: Time, + state: KeplerianState, origin: O, frame: F, } -// Must be manually implemented, since derive macros always bound the generic parameters by the given trait, not the -// tightest possible bound. I.e., `TimeScale` is not inherently `Copy`, but `Keplerian` is. -// See https://github.com/rust-lang/rust/issues/108894#issuecomment-1459943821 -impl Clone for Keplerian -where - T: TimeScale, - O: PointMass + Copy, - F: InertialFrame + Copy, -{ - fn clone(&self) -> Self { - *self - } -} - -impl Copy for Keplerian -where - T: TimeScale, - O: PointMass + Copy, - F: InertialFrame + Copy, -{ -} - impl Keplerian where - T: TimeScale, + T: TimeScale + Copy, O: PointMass + Copy, F: InertialFrame + Copy, { @@ -190,7 +152,6 @@ where true_anomaly: f64, ) -> Self { let state = KeplerianState::new( - time, semi_major, eccentricity, inclination, @@ -199,16 +160,13 @@ where true_anomaly, ); Self { + time, state, origin, frame, } } - pub fn time(&self) -> Time { - self.state.time() - } - pub fn semi_major_axis(&self) -> f64 { self.state.semi_major_axis() } @@ -236,10 +194,14 @@ where impl TwoBody for Keplerian where - T: TimeScale, + T: TimeScale + Copy, O: PointMass + Copy, F: InertialFrame + Copy, { + fn time(&self) -> Time { + self.time + } + fn to_cartesian(&self) -> Cartesian { Cartesian::from(*self) } @@ -251,7 +213,7 @@ where impl CoordinateSystem for Keplerian where - T: TimeScale, + T: TimeScale + Copy, O: PointMass + Copy, F: InertialFrame + Copy, { @@ -269,7 +231,7 @@ where impl From> for Keplerian where - T: TimeScale, + T: TimeScale + Copy, O: PointMass + Copy, F: InertialFrame + Copy, { @@ -278,6 +240,7 @@ where let state = cartesian.state.to_keplerian_state(grav_param); Self { state, + time: cartesian.time, origin: cartesian.origin, frame: cartesian.frame, } @@ -299,7 +262,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(date, utc); + let time = Time::from_date_and_utc_timestamp(TDB, date, utc); let pos = DVec3::new( -0.107622532467967e7, -0.676589636432773e7, @@ -332,7 +295,7 @@ mod tests { 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(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; From f88ae51d68a22cfd5ee45bac935f7f70cdfae562 Mon Sep 17 00:00:00 2001 From: AngusGMorrison Date: Sat, 10 Feb 2024 16:15:09 +0000 Subject: [PATCH 08/14] Fixup remaining Time references --- crates/lox-space/examples/iss.rs | 2 +- crates/lox_core/src/coords/two_body.rs | 8 +++ crates/lox_core/src/earth/nutation.rs | 9 ++-- crates/lox_core/src/time/continuous.rs | 12 ++++- crates/lox_py/src/coords.rs | 68 ++++++++------------------ crates/lox_py/src/time.rs | 45 ++++++++++------- 6 files changed, 72 insertions(+), 72 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/two_body.rs b/crates/lox_core/src/coords/two_body.rs index 3cec4446..a74be8b5 100644 --- a/crates/lox_core/src/coords/two_body.rs +++ b/crates/lox_core/src/coords/two_body.rs @@ -56,6 +56,10 @@ where } } + pub fn time(&self) -> Time { + self.time + } + pub fn position(&self) -> DVec3 { self.state.position() } @@ -167,6 +171,10 @@ where } } + pub fn time(&self) -> Time { + self.time + } + pub fn semi_major_axis(&self) -> f64 { self.state.semi_major_axis() } diff --git a/crates/lox_core/src/earth/nutation.rs b/crates/lox_core/src/earth/nutation.rs index 1fab714b..f9805316 100644 --- a/crates/lox_core/src/earth/nutation.rs +++ b/crates/lox_core/src/earth/nutation.rs @@ -97,7 +97,6 @@ fn point1_microarcsec_to_rad(p1_uas: Point1Microarcsec) -> Radians { #[cfg(test)] mod tests { - use crate::time::continuous::{TimeScale, TT}; use float_eq::assert_float_eq; use super::*; @@ -106,7 +105,7 @@ mod tests { #[test] fn test_nutation_iau1980() { - let time = Time::::j2000(); + let time = Time::j2000(TDB); let expected = Nutation { longitude: -0.00006750247617532478, obliquity: -0.00002799221238377013, @@ -117,7 +116,7 @@ mod tests { } #[test] fn test_nutation_iau2000a() { - let time = Time::::j2000(); + let time = Time::j2000(TDB); let expected = Nutation { longitude: -0.00006754422426417299, obliquity: -0.00002797083119237414, @@ -129,7 +128,7 @@ mod tests { #[test] fn test_nutation_iau2000b() { - let time = Time::::j2000(); + let time = Time::j2000(TDB); let expected = Nutation { longitude: -0.00006754261253992235, obliquity: -0.00002797092331098565, @@ -141,7 +140,7 @@ mod tests { #[test] fn test_nutation_iau2006a() { - let time = Time::::j2000(); + let time = Time::j2000(TDB); let expected = Nutation { longitude: -0.00006754425598969513, obliquity: -0.00002797083119237414, diff --git a/crates/lox_core/src/time/continuous.rs b/crates/lox_core/src/time/continuous.rs index 723d0ef6..e0a9ec4c 100644 --- a/crates/lox_core/src/time/continuous.rs +++ b/crates/lox_core/src/time/continuous.rs @@ -62,6 +62,14 @@ impl UnscaledTime { 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; @@ -1058,7 +1066,7 @@ mod tests { scale: TAI, timestamp: UnscaledTime::default(), }; - assert_eq!(*expected, actual); + assert_eq!(expected, actual); } #[test] @@ -1071,7 +1079,7 @@ mod tests { attoseconds: 0, }, ); - assert_eq!(*expected, actual); + assert_eq!(expected, actual); } #[test] diff --git a/crates/lox_py/src/coords.rs b/crates/lox_py/src/coords.rs index ed01c20d..ea2547ad 100644 --- a/crates/lox_py/src/coords.rs +++ b/crates/lox_py/src/coords.rs @@ -13,7 +13,6 @@ use pyo3::prelude::*; use lox_core::bodies::PointMass; use lox_core::coords::states::{CartesianState, KeplerianState, TwoBodyState}; use lox_core::coords::DVec3; -use lox_core::time::continuous::{Time, TimeScale}; use crate::bodies::PyBody; use crate::frames::PyFrame; @@ -21,38 +20,12 @@ use crate::time::PyTime; #[pyclass(name = "Cartesian")] pub struct PyCartesian { - state: PyCartesianState, + time: PyTime, + state: CartesianState, origin: PyBody, frame: PyFrame, } -#[pyclass(name = "CartesianState")] -pub struct PyCartesianState { - time: PyTime, - position: DVec3, - velocity: DVec3, -} - -impl PyCartesianState { - pub fn to_keplerian_state(&self) -> PyKeplerianState { - unimplemented!( - "There's no way to create a Rust `Time` dynamically from a `PyTime` enum variant." - ) - } -} - -#[derive(Debug, PartialEq)] -#[pyclass(name = "KeplerianState")] -pub struct PyKeplerianState { - time: PyTime, - semi_major: f64, - eccentricity: f64, - inclination: f64, - ascending_node: f64, - periapsis_argument: f64, - true_anomaly: f64, -} - #[pymethods] impl PyCartesian { #[allow(clippy::too_many_arguments)] @@ -70,12 +43,9 @@ impl PyCartesian { ) -> PyResult { let origin: PyBody = body.try_into()?; let frame = PyFrame::from_str(frame)?; - let state = PyCartesianState { - time, - position: DVec3::new(x, y, z), - veclocity: 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, @@ -83,7 +53,7 @@ impl PyCartesian { } fn time(&self) -> PyTime { - self.state.time + self.time } fn reference_frame(&self) -> String { @@ -108,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, @@ -127,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, @@ -140,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, @@ -149,6 +120,7 @@ impl PyKeplerian { true_anomaly, ); Ok(Self { + time, state, origin, frame, @@ -156,7 +128,7 @@ impl PyKeplerian { } fn time(&self) -> PyTime { - PyTime(self.state.time()) + self.time } fn reference_frame(&self) -> String { @@ -196,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(), } @@ -207,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, @@ -245,7 +219,7 @@ mod tests { ) * 1e-3; let cartesian = PyCartesian::new( - &epoch, + time, body.clone(), "ICRF", pos.x, @@ -262,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"); @@ -276,8 +250,8 @@ mod tests { #[test] fn test_keplerian() { - let epoch = PyTime::new( - "TDB", + let time = PyTime::new( + PyTimeScale::TDB, 2023, 3, 25, @@ -305,7 +279,7 @@ mod tests { let true_anomaly = 0.44369564302687126; let keplerian = PyKeplerian::new( - &epoch, + time, body.clone(), "ICRF", semi_major, @@ -322,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 a1a664f6..79cc0799 100644 --- a/crates/lox_py/src/time.rs +++ b/crates/lox_py/src/time.rs @@ -9,7 +9,7 @@ use pyo3::{pyclass, pymethods}; use std::fmt::{Display, Formatter}; -use lox_core::time::continuous::{Time, TimeScale, UnscaledTime, TAI, TCB, TCG, TDB, TT, UT1}; +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; @@ -131,21 +131,7 @@ impl PyTime { utc.atto = PerMille::new(atto)?; } - Ok(Self::from_date_and_utc_timestamp(scale, date, utc)) - } - - pub fn from_date_and_utc_timestamp(scale: PyTimeScale, date: Date, utc: UTC) -> Self { - let timestamp = match scale { - PyTimeScale::TAI => Time::::from_date_and_utc_timestamp(date, utc), - PyTimeScale::TCB => Time::::from_date_and_utc_timestamp(date, utc), - PyTimeScale::TCG => Time::::from_date_and_utc_timestamp(date, utc), - PyTimeScale::TDB => Time::::from_date_and_utc_timestamp(date, utc), - PyTimeScale::TT => Time::::from_date_and_utc_timestamp(date, utc), - PyTimeScale::UT1 => Time::::from_date_and_utc_timestamp(date, utc), - } - .unscaled(); - - PyTime { timestamp, scale } + Ok(pytime_from_date_and_utc_timestamp(scale, date, utc)) } pub fn days_since_j2000(&self) -> f64 { @@ -166,6 +152,31 @@ impl From> for PyTime { } } +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(), + } +} + #[cfg(test)] mod tests { use float_eq::assert_float_eq; @@ -213,6 +224,6 @@ mod tests { .expect("time should be valid"); 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); } } From 255bb54ad8ee8ad23f961aa7ee000278c99e6831 Mon Sep 17 00:00:00 2001 From: AngusGMorrison Date: Sat, 10 Feb 2024 16:29:09 +0000 Subject: [PATCH 09/14] Test days and centuries since J2000 --- crates/lox_core/src/time/constants/i64.rs | 4 + crates/lox_core/src/time/continuous.rs | 126 ++++++++++++++++++++-- 2 files changed, 119 insertions(+), 11 deletions(-) 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 e0a9ec4c..cae53aa2 100644 --- a/crates/lox_core/src/time/continuous.rs +++ b/crates/lox_core/src/time/continuous.rs @@ -407,6 +407,7 @@ pub trait CalendarDate { #[cfg(test)] mod tests { + use crate::time::constants::i64::SECONDS_PER_JULIAN_CENTURY; use crate::time::dates::Calendar::Gregorian; use float_eq::assert_float_eq; @@ -1030,16 +1031,83 @@ mod tests { }, 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) + assert_float_eq!( + tc.expected, + actual, + abs <= 1e-12, + "{}: expected {}, got {}", + tc.desc, + tc.expected, + actual + ); } } #[test] - fn test_unscaled_time_centuries_since_j2000() {} + fn test_unscaled_time_centuries_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 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 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_from_date_and_utc_timestamp() { @@ -1082,6 +1150,42 @@ mod tests { assert_eq!(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 unscaled_time = UnscaledTime { @@ -1089,7 +1193,7 @@ mod tests { attoseconds: 9876543210, }; let expected = unscaled_time.hour(); - let actual = Time::j2000(TAI).hour(); + let actual = Time::from_unscaled(TAI, unscaled_time).hour(); assert_eq!( actual, expected, "expected Time to have hour {}, but got {}", @@ -1104,7 +1208,7 @@ mod tests { attoseconds: 9876543210, }; let expected = unscaled_time.minute(); - let actual = Time::j2000(TAI).minute(); + let actual = Time::from_unscaled(TAI, unscaled_time).minute(); assert_eq!( actual, expected, "expected Time to have minute {}, but got {}", @@ -1119,7 +1223,7 @@ mod tests { attoseconds: 9876543210, }; let expected = unscaled_time.second(); - let actual = Time::j2000(TAI).second(); + let actual = Time::from_unscaled(TAI, unscaled_time).second(); assert_eq!( actual, expected, "expected Time to have second {}, but got {}", @@ -1134,7 +1238,7 @@ mod tests { attoseconds: 9876543210, }; let expected = unscaled_time.millisecond(); - let actual = Time::j2000(TAI).millisecond(); + let actual = Time::from_unscaled(TAI, unscaled_time).millisecond(); assert_eq!( actual, expected, "expected Time to have millisecond {}, but got {}", @@ -1149,7 +1253,7 @@ mod tests { attoseconds: 9876543210, }; let expected = unscaled_time.microsecond(); - let actual = Time::j2000(TAI).microsecond(); + let actual = Time::from_unscaled(TAI, unscaled_time).microsecond(); assert_eq!( actual, expected, "expected Time to have microsecond {}, but got {}", @@ -1164,7 +1268,7 @@ mod tests { attoseconds: 9876543210, }; let expected = unscaled_time.nanosecond(); - let actual = Time::j2000(TAI).nanosecond(); + let actual = Time::from_unscaled(TAI, unscaled_time).nanosecond(); assert_eq!( actual, expected, "expected Time to have nanosecond {}, but got {}", @@ -1179,7 +1283,7 @@ mod tests { attoseconds: 9876543210, }; let expected = unscaled_time.picosecond(); - let actual = Time::j2000(TAI).picosecond(); + let actual = Time::from_unscaled(TAI, unscaled_time).picosecond(); assert_eq!( actual, expected, "expected Time to have picosecond {}, but got {}", @@ -1194,7 +1298,7 @@ mod tests { attoseconds: 9876543210, }; let expected = unscaled_time.femtosecond(); - let actual = Time::j2000(TAI).femtosecond(); + let actual = Time::from_unscaled(TAI, unscaled_time).femtosecond(); assert_eq!( actual, expected, "expected Time to have femtosecond {}, but got {}", @@ -1209,7 +1313,7 @@ mod tests { attoseconds: 9876543210, }; let expected = unscaled_time.attosecond(); - let actual = Time::j2000(TAI).attosecond(); + let actual = Time::from_unscaled(TAI, unscaled_time).attosecond(); assert_eq!( actual, expected, "expected Time to have attosecond {}, but got {}", From cbb421894bc1d8a40a0aa7f694d9557c38acd261 Mon Sep 17 00:00:00 2001 From: AngusGMorrison Date: Sat, 10 Feb 2024 16:33:54 +0000 Subject: [PATCH 10/14] Update Python test --- crates/lox_py/tests/test_coords.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 7329c039d7b2f44ceb9777054dae85fd5707ae50 Mon Sep 17 00:00:00 2001 From: AngusGMorrison Date: Sat, 10 Feb 2024 16:38:10 +0000 Subject: [PATCH 11/14] Appease clippy --- crates/lox_py/src/time.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/lox_py/src/time.rs b/crates/lox_py/src/time.rs index 79cc0799..50a76ef2 100644 --- a/crates/lox_py/src/time.rs +++ b/crates/lox_py/src/time.rs @@ -18,6 +18,7 @@ use crate::LoxPyError; #[pyclass(name = "TimeScale")] #[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[allow(clippy::upper_case_acronyms)] pub enum PyTimeScale { TAI, TCB, From 58c97c2432acb235d90144f5f318ddce44a61198 Mon Sep 17 00:00:00 2001 From: AngusGMorrison Date: Sat, 10 Feb 2024 16:47:22 +0000 Subject: [PATCH 12/14] Add missing tests --- crates/lox_core/src/time/continuous.rs | 64 ++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 3 deletions(-) diff --git a/crates/lox_core/src/time/continuous.rs b/crates/lox_core/src/time/continuous.rs index cae53aa2..de08aa6b 100644 --- a/crates/lox_core/src/time/continuous.rs +++ b/crates/lox_core/src/time/continuous.rs @@ -433,7 +433,25 @@ mod tests { } #[test] - fn test_unscaled_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: UnscaledTime, @@ -527,7 +545,7 @@ mod tests { } #[test] - fn test_unscaled_time_minute() { + fn test_unscaled_time_wall_clock_minute() { struct TestCase { desc: &'static str, time: UnscaledTime, @@ -620,7 +638,7 @@ mod tests { } #[test] - fn test_unscaled_time_second() { + fn test_unscaled_time_wall_clock_second() { struct TestCase { desc: &'static str, time: UnscaledTime, @@ -1109,6 +1127,22 @@ mod tests { } } + #[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); @@ -1150,6 +1184,30 @@ mod tests { assert_eq!(expected, actual); } + #[test] + 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 + ); + } + + #[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 { From 195e539685b478df79e2416ef64fe67d2583d62d Mon Sep 17 00:00:00 2001 From: AngusGMorrison Date: Sat, 10 Feb 2024 16:48:06 +0000 Subject: [PATCH 13/14] Delete unused example code --- crates/lox_py/src/time.rs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/crates/lox_py/src/time.rs b/crates/lox_py/src/time.rs index 50a76ef2..0527bbe7 100644 --- a/crates/lox_py/src/time.rs +++ b/crates/lox_py/src/time.rs @@ -144,15 +144,6 @@ impl PyTime { } } -impl From> for PyTime { - fn from(time: Time) -> Self { - PyTime { - scale: PyTimeScale::TDB, - timestamp: time.unscaled(), - } - } -} - 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), From fce045e81f43bf05b469a21a670b16e2fee1f426 Mon Sep 17 00:00:00 2001 From: AngusGMorrison Date: Sat, 10 Feb 2024 16:51:25 +0000 Subject: [PATCH 14/14] Improve test coverage --- crates/lox_core/src/coords/two_body.rs | 46 ++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/crates/lox_core/src/coords/two_body.rs b/crates/lox_core/src/coords/two_body.rs index a74be8b5..7447d250 100644 --- a/crates/lox_core/src/coords/two_body.rs +++ b/crates/lox_core/src/coords/two_body.rs @@ -299,6 +299,26 @@ 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"); @@ -361,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); + } }