Skip to content

Commit 54aba6e

Browse files
committed
feat(lox-time): add attosecond-resolution deltas
1 parent 59a54d7 commit 54aba6e

File tree

29 files changed

+1597
-1197
lines changed

29 files changed

+1597
-1197
lines changed

crates/lox-earth/src/eop.rs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@ use lox_math::series::{Series, SeriesError};
1212
use lox_time::{
1313
OffsetProvider,
1414
deltas::TimeDelta,
15-
julian_dates::{JulianDate, SECONDS_BETWEEN_MJD_AND_J2000},
15+
julian_dates::JulianDate,
1616
utc::{
1717
Utc, UtcError,
1818
leap_seconds::{BuiltinLeapSeconds, LeapSecondsProvider},
1919
transformations::TryToUtc,
2020
},
2121
};
22-
use lox_units::f64::consts::SECONDS_PER_DAY;
22+
use lox_units::f64::consts::{SECONDS_BETWEEN_MJD_AND_J2000, SECONDS_PER_DAY};
2323
use serde::Deserialize;
2424
use thiserror::Error;
2525

@@ -151,9 +151,7 @@ impl EopParser {
151151
let r2: EopRecord = raw2.deserialize(Some(&headers))?;
152152
let r = r1.merge(&r2);
153153

154-
j2000.push(
155-
r.modified_julian_date * SECONDS_PER_DAY - SECONDS_BETWEEN_MJD_AND_J2000 as f64,
156-
);
154+
j2000.push(r.modified_julian_date * SECONDS_PER_DAY - SECONDS_BETWEEN_MJD_AND_J2000);
157155

158156
if let Some(delta_ut1_utc) = r.delta_ut1_utc {
159157
let utc = Utc::builder().with_ymd(r.year, r.month, r.day).build()?;
@@ -164,7 +162,7 @@ impl EopParser {
164162
.or_else(|| Some(BuiltinLeapSeconds.delta_utc_tai(utc)))
165163
.flatten()
166164
.ok_or(EopParserError::LeapSecond(utc))?;
167-
delta_ut1_tai.push(delta_ut1_utc + delta_tai_utc.to_decimal_seconds())
165+
delta_ut1_tai.push(delta_ut1_utc + delta_tai_utc.as_seconds_f64())
168166
}
169167

170168
if let (Some(xp), Some(yp)) = (r.x_pole, r.y_pole) {
@@ -315,7 +313,7 @@ impl EopProvider {
315313
if seconds < t0 || seconds > tn {
316314
return Err(EopProviderError::ExtrapolatedValue(val));
317315
}
318-
Ok(TimeDelta::try_from_decimal_seconds(val).unwrap())
316+
Ok(TimeDelta::from_seconds_f64(val))
319317
}
320318

321319
pub fn delta_tai_ut1(&self, ut1: TimeDelta) -> Result<TimeDelta, EopProviderError> {
@@ -331,7 +329,7 @@ impl EopProvider {
331329
if seconds < t0 || seconds > tn {
332330
return Err(EopProviderError::ExtrapolatedValue(val));
333331
}
334-
Ok(-TimeDelta::try_from_decimal_seconds(val).unwrap())
332+
Ok(-TimeDelta::from_seconds_f64(val))
335333
}
336334
}
337335

crates/lox-earth/src/ut1.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -175,12 +175,12 @@ mod tests {
175175
let actual = provider
176176
.delta_ut1_tai(tai.to_delta())
177177
.unwrap()
178-
.to_decimal_seconds();
178+
.as_seconds_f64();
179179
assert_approx_eq!(actual, expected, rtol <= 1e-6);
180180
let actual = provider
181181
.delta_tai_ut1(ut1.to_delta())
182182
.unwrap()
183-
.to_decimal_seconds();
183+
.as_seconds_f64();
184184
assert_approx_eq!(actual, -expected, rtol <= 1e-6);
185185
}
186186

@@ -257,7 +257,7 @@ mod tests {
257257
let act = provider
258258
.try_offset(scale1, scale2, dt)
259259
.unwrap()
260-
.to_decimal_seconds();
260+
.as_seconds_f64();
261261
assert_approx_eq!(act, exp, rtol <= UT1_TOL);
262262
}
263263

crates/lox-orbits/src/analysis.rs

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ impl<T: TimeScale> Pass<T> {
183183
if times.is_empty() {
184184
// Create default points at window start and end
185185
let start_seconds = 0.0;
186-
let end_seconds = window.duration().to_decimal_seconds();
186+
let end_seconds = window.duration().as_seconds_f64();
187187
let default_obs = observables
188188
.first()
189189
.cloned()
@@ -197,7 +197,7 @@ impl<T: TimeScale> Pass<T> {
197197
)
198198
} else {
199199
// Duplicate the single point
200-
let time_sec = (times[0].clone() - window.start()).to_decimal_seconds();
200+
let time_sec = (times[0].clone() - window.start()).as_seconds_f64();
201201
let obs = &observables[0];
202202
(
203203
vec![time_sec, time_sec + 1.0], // Add 1 second offset for second point
@@ -211,7 +211,7 @@ impl<T: TimeScale> Pass<T> {
211211
// Normal case with multiple points
212212
let time_seconds: Vec<f64> = times
213213
.iter()
214-
.map(|t| (t.clone() - window.start()).to_decimal_seconds())
214+
.map(|t| (t.clone() - window.start()).as_seconds_f64())
215215
.collect();
216216

217217
// Extract observable arrays
@@ -267,7 +267,7 @@ impl<T: TimeScale> Pass<T> {
267267
}
268268

269269
// Convert time to seconds since window start for interpolation
270-
let target_seconds = (time - self.window.start()).to_decimal_seconds();
270+
let target_seconds = (time - self.window.start()).as_seconds_f64();
271271

272272
// Use Series interpolation for each observable
273273
let azimuth = self.azimuth_series.interpolate(target_seconds);
@@ -309,18 +309,11 @@ pub fn visibility_dyn(
309309
let end = *times.last().unwrap();
310310
let times: Vec<f64> = times
311311
.iter()
312-
.map(|t| (*t - start).to_decimal_seconds())
312+
.map(|t| (*t - start).as_seconds_f64())
313313
.collect();
314314
let root_finder = Brent::default();
315315
find_windows(
316-
|t| {
317-
elevation_dyn(
318-
start + TimeDelta::try_from_decimal_seconds(t).unwrap(),
319-
gs,
320-
mask,
321-
sc,
322-
)
323-
},
316+
|t| elevation_dyn(start + TimeDelta::from_seconds_f64(t), gs, mask, sc),
324317
start,
325318
end,
326319
&times,
@@ -342,12 +335,12 @@ pub fn visibility_los(
342335
let end = *times.last().unwrap();
343336
let times: Vec<f64> = times
344337
.iter()
345-
.map(|t| (*t - start).to_decimal_seconds())
338+
.map(|t| (*t - start).as_seconds_f64())
346339
.collect();
347340
let root_finder = Brent::default();
348341
find_windows(
349342
|t| {
350-
let time = start + TimeDelta::from_decimal_seconds(t);
343+
let time = start + TimeDelta::from_seconds_f64(t);
351344
let epoch = time
352345
.try_to_scale(Tdb, &DefaultOffsetProvider)
353346
.unwrap()
@@ -400,8 +393,7 @@ pub fn visibility_combined<E: Ephemeris + Send + Sync>(
400393
let time_resolution = if times.len() >= 2 {
401394
times[1] - times[0]
402395
} else {
403-
// Default to 60 seconds if we don't have enough times
404-
TimeDelta::try_from_decimal_seconds(60.0).expect("60 seconds should be valid")
396+
TimeDelta::from_seconds_f64(60.0)
405397
};
406398

407399
for window in windows {
@@ -457,13 +449,13 @@ where
457449
let end = *times.last().unwrap();
458450
let times: Vec<f64> = times
459451
.iter()
460-
.map(|t| (*t - start).to_decimal_seconds())
452+
.map(|t| (*t - start).as_seconds_f64())
461453
.collect();
462454
let root_finder = Brent::default();
463455
find_windows(
464456
|t| {
465457
elevation(
466-
start + TimeDelta::try_from_decimal_seconds(t).unwrap(),
458+
start + TimeDelta::from_seconds_f64(t),
467459
gs,
468460
mask,
469461
sc,

crates/lox-orbits/src/elements.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ where
208208
pub fn orbital_period(&self) -> TimeDelta {
209209
let mu = self.gravitational_parameter();
210210
let a = self.semi_major_axis();
211-
TimeDelta::try_from_decimal_seconds(TAU * (a.powi(3) / mu).sqrt()).unwrap()
211+
TimeDelta::from_seconds_f64(TAU * (a.powi(3) / mu).sqrt())
212212
}
213213
}
214214

crates/lox-orbits/src/events.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ pub fn find_events<F: Fn(f64) -> f64 + Copy, T: TimeScale + Clone, R: FindBracke
9797
let t = root_finder
9898
.find_in_bracket(func, (t0, t1))
9999
.expect("sign changed but root finder failed");
100-
let time = start.clone() + TimeDelta::try_from_decimal_seconds(t).unwrap();
100+
let time = start.clone() + TimeDelta::from_seconds_f64(t);
101101

102102
events.push(Event { crossing, time });
103103
}
@@ -264,13 +264,13 @@ mod tests {
264264
assert_eq!(events[0].crossing, ZeroCrossing::Down);
265265
assert_approx_eq!(
266266
events[0].time,
267-
start + TimeDelta::try_from_decimal_seconds(PI).unwrap(),
267+
start + TimeDelta::from_seconds_f64(PI),
268268
rtol <= 1e-6
269269
);
270270
assert_eq!(events[1].crossing, ZeroCrossing::Up);
271271
assert_approx_eq!(
272272
events[1].time,
273-
start + TimeDelta::try_from_decimal_seconds(TAU).unwrap(),
273+
start + TimeDelta::from_seconds_f64(TAU),
274274
rtol <= 1e-6
275275
);
276276
}
@@ -290,7 +290,7 @@ mod tests {
290290
assert_eq!(windows[0].start, start);
291291
assert_approx_eq!(
292292
windows[0].end,
293-
start + TimeDelta::try_from_decimal_seconds(PI).unwrap(),
293+
start + TimeDelta::from_seconds_f64(PI),
294294
rtol <= 1e-6
295295
);
296296
}

crates/lox-orbits/src/propagators/semi_analytical.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ where
106106
let origin = self.origin();
107107
let mu = self.gravitational_parameter();
108108
let t0 = self.initial_state.time();
109-
let dt = (time.clone() - t0).to_decimal_seconds();
109+
let dt = (time.clone() - t0).as_seconds_f64();
110110
let sqrt_mu = mu.sqrt();
111111
let p0 = self.initial_state.position();
112112
let v0 = self.initial_state.velocity();
@@ -241,7 +241,7 @@ mod tests {
241241
);
242242
let s0 = k0.to_cartesian();
243243
let period = k0.orbital_period();
244-
let t_end = period.to_decimal_seconds().ceil() as i64;
244+
let t_end = period.as_seconds_f64().ceil() as i64;
245245
let steps = TimeDelta::range(0..=t_end).map(|dt| time + dt);
246246
let trajectory = Vallado::new(s0).propagate_all(steps).unwrap();
247247
let s1 = trajectory.interpolate(period);

crates/lox-orbits/src/propagators/sgp4.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use lox_bodies::Earth;
1111
use lox_time::Time;
1212
use lox_time::deltas::TimeDelta;
1313
use lox_time::time_scales::Tai;
14-
use lox_time::utc::Utc;
14+
use lox_time::utc::{Utc, UtcError};
1515
use lox_units::f64::consts::SECONDS_PER_MINUTE;
1616

1717
use crate::propagators::Propagator;
@@ -21,10 +21,14 @@ use lox_frames::Icrf;
2121

2222
#[derive(Debug, Clone, Error)]
2323
pub enum Sgp4Error {
24+
#[error(transparent)]
25+
ElementsError(#[from] ElementsError),
2426
#[error(transparent)]
2527
TrajectoryError(#[from] TrajectoryError),
2628
#[error(transparent)]
2729
Sgp4(#[from] sgp4::Error),
30+
#[error(transparent)]
31+
Utc(#[from] UtcError),
2832
}
2933

3034
pub struct Sgp4 {
@@ -33,9 +37,9 @@ pub struct Sgp4 {
3337
}
3438

3539
impl Sgp4 {
36-
pub fn new(initial_state: Elements) -> Result<Self, ElementsError> {
40+
pub fn new(initial_state: Elements) -> Result<Self, Sgp4Error> {
3741
let epoch = initial_state.epoch();
38-
let time = Utc::from_delta(TimeDelta::from_julian_years(epoch).unwrap()).to_time();
42+
let time = Utc::from_delta(TimeDelta::from_julian_years(epoch))?.to_time();
3943
let constants = Constants::from_elements(&initial_state)?;
4044
Ok(Self { constants, time })
4145
}
@@ -49,7 +53,7 @@ impl Propagator<Tai, Earth, Icrf> for Sgp4 {
4953
type Error = Sgp4Error;
5054

5155
fn propagate(&self, time: Time<Tai>) -> Result<State<Tai, Earth, Icrf>, Self::Error> {
52-
let dt = (time - self.time).to_decimal_seconds() / SECONDS_PER_MINUTE;
56+
let dt = (time - self.time).as_seconds_f64() / SECONDS_PER_MINUTE;
5357
let prediction = self.constants.propagate(MinutesSinceEpoch(dt))?;
5458
let position = DVec3::from_array(prediction.position);
5559
let velocity = DVec3::from_array(prediction.velocity);
@@ -73,11 +77,11 @@ mod tests {
7377
.unwrap();
7478
let sgp4 = Sgp4::new(tle).unwrap();
7579
let orbital_period = 92.821;
76-
let t1 = sgp4.time() + TimeDelta::from_minutes(orbital_period).unwrap();
80+
let t1 = sgp4.time() + TimeDelta::from_minutes(orbital_period);
7781
let s1 = sgp4.propagate(t1).unwrap();
7882
let k1 = s1.to_keplerian();
7983
assert_approx_eq!(
80-
k1.orbital_period().to_decimal_seconds() / SECONDS_PER_MINUTE,
84+
k1.orbital_period().as_seconds_f64() / SECONDS_PER_MINUTE,
8185
orbital_period,
8286
rtol <= 1e-4
8387
);

crates/lox-orbits/src/trajectories.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ where
6666
let start_time = states[0].time();
6767
let t: Vec<f64> = states
6868
.iter()
69-
.map(|s| (s.time() - start_time.clone()).to_decimal_seconds())
69+
.map(|s| (s.time() - start_time.clone()).as_seconds_f64())
7070
.collect();
7171
// let t = ArcVecF64(Arc::new(t));
7272
let x: Vec<f64> = states.iter().map(|s| s.position().x).collect();
@@ -149,7 +149,7 @@ where
149149
}
150150

151151
pub fn interpolate(&self, dt: TimeDelta) -> State<T, O, R> {
152-
let t = dt.to_decimal_seconds();
152+
let t = dt.as_seconds_f64();
153153
State::new(
154154
self.start_time() + dt,
155155
self.position(t),
@@ -168,7 +168,7 @@ where
168168
find_events(
169169
|t| {
170170
func(State::new(
171-
self.start_time() + TimeDelta::try_from_decimal_seconds(t).unwrap(),
171+
self.start_time() + TimeDelta::from_seconds_f64(t),
172172
self.position(t),
173173
self.velocity(t),
174174
self.origin(),
@@ -187,7 +187,7 @@ where
187187
find_windows(
188188
|t| {
189189
func(State::new(
190-
self.start_time() + TimeDelta::try_from_decimal_seconds(t).unwrap(),
190+
self.start_time() + TimeDelta::from_seconds_f64(t),
191191
self.position(t),
192192
self.velocity(t),
193193
self.origin(),

crates/lox-space/src/orbits/python.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ use crate::orbits::{
3434
use crate::time::DynTime;
3535
use crate::time::deltas::TimeDelta;
3636
use crate::time::offsets::DefaultOffsetProvider;
37-
use crate::time::python::deltas::{PyTimeDelta, PyTimeDeltaError};
37+
use crate::time::python::deltas::PyTimeDelta;
3838
use crate::time::python::time::PyTime;
3939
use crate::time::python::time_scales::PyMissingEopProviderError;
4040
use crate::time::time_scales::{DynTimeScale, Tai};
@@ -370,8 +370,7 @@ impl PyTrajectory {
370370
}
371371
let mut states: Vec<DynState> = Vec::with_capacity(array.nrows());
372372
for row in array.rows() {
373-
let time = start_time.0
374-
+ TimeDelta::try_from_decimal_seconds(row[0]).map_err(PyTimeDeltaError)?;
373+
let time = start_time.0 + TimeDelta::from_seconds_f64(row[0]);
375374
let position = DVec3::new(row[1], row[2], row[3]);
376375
let velocity = DVec3::new(row[4], row[5], row[6]);
377376
states.push(State::new(time, position, velocity, origin.0, frame.0));

crates/lox-space/src/python.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ use crate::orbits::python::{
1515
find_windows, visibility, visibility_all,
1616
};
1717
use crate::time::python::{
18-
deltas::PyTimeDelta, time::PyTime, time_scales::PyTimeScale, utc::PyUtc,
18+
deltas::{NonFiniteTimeDeltaError, PyTimeDelta},
19+
time::PyTime,
20+
time_scales::PyTimeScale,
21+
utc::PyUtc,
1922
};
2023
use crate::units::{
2124
ASTRONOMICAL_UNIT,
@@ -65,6 +68,10 @@ fn lox_space(m: &Bound<'_, PyModule>) -> PyResult<()> {
6568
m.add_class::<PyTimeDelta>()?;
6669
m.add_class::<PyTimeScale>()?;
6770
m.add_class::<PyUtc>()?;
71+
m.add(
72+
"NonFiniteTimeDeltaError",
73+
m.py().get_type::<NonFiniteTimeDeltaError>(),
74+
)?;
6875

6976
// units
7077
m.add_class::<PyAngle>()?;

0 commit comments

Comments
 (0)