use crate::frametransform::{qgcrf2itrf, qitrf2gcrf_slow_parts, qtirs2cirs};
use crate::jplephem;
use crate::mathtypes::{Quaternion, Vector3};
use crate::Duration;
use crate::Instant;
use crate::SolarSystem;
use crate::TimeLike;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct InterpSample {
pub qgcrf2itrf: Quaternion,
pub sun_pos_gcrf: Vector3,
pub moon_pos_gcrf: Vector3,
pub sun_vel_gcrf: Vector3,
}
pub type InterpType = InterpSample;
use super::error::{Error, Result};
#[derive(Debug, Clone)]
pub struct Precomputed {
pub begin: Instant,
pub end: Instant,
pub step: f64,
data: Vec<InterpType>,
}
pub const DEFAULT_PADDING_SECS: f64 = 240.0;
const SLOW_STEP_SECS: f64 = 3600.0;
pub const MAX_PRECOMPUTE_ENTRIES: usize = 16_777_216;
fn table_len(span_secs: f64, step_secs: f64) -> Result<usize> {
let n = (span_secs / step_secs).ceil();
if !n.is_finite() || n < 0.0 || n > MAX_PRECOMPUTE_ENTRIES as f64 {
return Err(Error::PrecomputeTooLarge {
entries: if n.is_finite() {
(n as u64).saturating_add(2)
} else {
u64::MAX
},
max: MAX_PRECOMPUTE_ENTRIES,
});
}
let entries = n as usize + 2;
if entries > MAX_PRECOMPUTE_ENTRIES {
return Err(Error::PrecomputeTooLarge {
entries: entries as u64,
max: MAX_PRECOMPUTE_ENTRIES,
});
}
Ok(entries)
}
impl Precomputed {
pub fn new<T: TimeLike>(begin: &T, end: &T) -> Result<Self> {
Self::new_padded(begin, end, 60.0, DEFAULT_PADDING_SECS)
}
pub fn new_with_step<T: TimeLike>(begin: &T, end: &T, step_secs: f64) -> Result<Self> {
Self::new_padded(begin, end, step_secs, DEFAULT_PADDING_SECS)
}
pub fn new_padded<T: TimeLike>(
begin: &T,
end: &T,
step_secs: f64,
padding_secs: f64,
) -> Result<Self> {
let begin = begin.as_instant();
let end = end.as_instant();
if !step_secs.is_finite() || step_secs <= 0.0 {
return Err(Error::InvalidPrecomputeStep { step: step_secs });
}
if !padding_secs.is_finite() {
return Err(Error::InvalidPrecomputePadding {
padding: padding_secs,
});
}
let step: f64 = step_secs;
let pad = Duration::from_seconds(padding_secs.max(0.0));
let (pbegin, pend) = match end > begin {
true => (begin - pad, end + pad),
false => (end - pad, begin + pad),
};
let nsteps: usize = table_len((pend - pbegin).as_seconds(), step)?;
let nslow: usize = table_len(((nsteps - 1) as f64) * step, SLOW_STEP_SECS)?;
Ok(Self {
begin: pbegin,
end: pend,
step,
data: {
jplephem::geocentric_pos(SolarSystem::Sun, &pbegin)?;
jplephem::geocentric_pos(SolarSystem::Sun, &pend)?;
crate::frametransform::ierstable::preload()?;
if crate::earth_orientation_params::coverage().is_none() {
return Err(Error::EopUnavailable);
}
let slow: Vec<(Quaternion, Quaternion)> = (0..nslow)
.map(|i| {
let t = pbegin + Duration::from_seconds((i as f64) * SLOW_STEP_SECS);
qitrf2gcrf_slow_parts(&t)
})
.collect();
let mut data = Vec::with_capacity(nsteps);
for idx in 0..nsteps {
let dt = (idx as f64) * step;
let t = pbegin + Duration::from_seconds(dt);
let s = dt / SLOW_STEP_SECS;
let si = (s.floor() as usize).min(nslow - 2);
let frac = s - si as f64;
let q_cirs2gcrs = slow[si].0.slerp(&slow[si + 1].0, frac);
let q_itrf2tirs = slow[si].1.slerp(&slow[si + 1].1, frac);
let q = (q_cirs2gcrs * qtirs2cirs(&t) * q_itrf2tirs).conjugate();
let (psun, vsun) = jplephem::geocentric_state(SolarSystem::Sun, &t)?;
let pmoon = jplephem::geocentric_pos(SolarSystem::Moon, &t)?;
data.push(InterpSample {
qgcrf2itrf: q,
sun_pos_gcrf: psun,
moon_pos_gcrf: pmoon,
sun_vel_gcrf: vsun,
});
}
data
},
})
}
pub fn interp_or_compute<T: TimeLike>(&self, t: &T) -> InterpType {
let t = t.as_instant();
if let Ok(v) = self.interp(&t) {
return v;
}
let q = qgcrf2itrf(&t);
match (
jplephem::geocentric_state(SolarSystem::Sun, &t),
jplephem::geocentric_pos(SolarSystem::Moon, &t),
) {
(Ok((psun, vsun)), Ok(pmoon)) => InterpSample {
qgcrf2itrf: q,
sun_pos_gcrf: psun,
moon_pos_gcrf: pmoon,
sun_vel_gcrf: vsun,
},
_ => {
let edge = if t < self.begin {
self.data.first()
} else {
self.data.last()
};
edge.copied().unwrap_or(InterpSample {
qgcrf2itrf: Quaternion::identity(),
sun_pos_gcrf: Vector3::zeros(),
moon_pos_gcrf: Vector3::zeros(),
sun_vel_gcrf: Vector3::zeros(),
})
}
}
}
pub fn interp<T: TimeLike>(&self, t: &T) -> Result<InterpType> {
let t = t.as_instant();
if t < self.begin || t > self.end {
return Err(Error::PrecomputedOutOfRange {
time: t.to_string(),
begin: self.begin.to_string(),
end: self.end.to_string(),
});
}
let idx = (t - self.begin).as_seconds() / self.step;
let delta = idx - idx.floor();
let idx = idx.floor() as usize;
let (a, b) = (&self.data[idx], &self.data[idx + 1]);
Ok(InterpSample {
qgcrf2itrf: a.qgcrf2itrf.slerp(&b.qgcrf2itrf, delta),
sun_pos_gcrf: a.sun_pos_gcrf + (b.sun_pos_gcrf - a.sun_pos_gcrf) * delta,
moon_pos_gcrf: a.moon_pos_gcrf + (b.moon_pos_gcrf - a.moon_pos_gcrf) * delta,
sun_vel_gcrf: a.sun_vel_gcrf + (b.sun_vel_gcrf - a.sun_vel_gcrf) * delta,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_table_matches_full_transform() {
let t0 = Instant::from_datetime(2023, 5, 16, 20, 0, 0.0).unwrap();
let week = 7.0 * 86400.0;
let tables = [
(
Precomputed::new(&t0, &(t0 + Duration::from_seconds(week))).unwrap(),
week,
"forward",
),
(
Precomputed::new(&(t0 + Duration::from_seconds(week)), &t0).unwrap(),
week,
"reversed",
),
(
Precomputed::new(&t0, &(t0 + Duration::from_seconds(1800.0))).unwrap(),
1800.0,
"sub-hour",
),
(
Precomputed::new_with_step(&t0, &(t0 + Duration::from_seconds(week)), 600.0)
.unwrap(),
week,
"600 s step",
),
];
for (pc, span, label) in &tables {
let mut worst = 0.0f64;
for k in 0..2000 {
let dt = (k as f64 * 302.17 + 7.3) % span;
let t = t0 + Duration::from_seconds(dt);
let q_tab = pc.interp(&t).unwrap().qgcrf2itrf;
let q_full = qgcrf2itrf(&t);
let dq = q_tab.conjugate() * q_full;
let a = dq.to_axis_angle().1.abs();
worst = worst.max(a.min(std::f64::consts::TAU - a));
}
assert!(
worst < 1e-8,
"{label}: table vs full transform: {worst:.3e} rad"
);
}
}
#[test]
fn test_size_cap_and_bad_inputs() {
let t0 = Instant::from_datetime(2023, 5, 16, 20, 0, 0.0).unwrap();
let t1 = t0 + Duration::from_seconds(3600.0);
let start = std::time::Instant::now();
assert!(matches!(
Precomputed::new_with_step(&t0, &t1, 1e-6),
Err(Error::PrecomputeTooLarge { .. })
));
assert!(start.elapsed().as_secs() < 2, "size check must be cheap");
assert!(matches!(
Precomputed::new_with_step(&t0, &t1, 1e-300),
Err(Error::PrecomputeTooLarge { .. })
));
assert!(matches!(
Precomputed::new_padded(&t0, &t1, 60.0, f64::NAN),
Err(Error::InvalidPrecomputePadding { .. })
));
assert!(matches!(
Precomputed::new_padded(&t0, &t1, 60.0, f64::INFINITY),
Err(Error::InvalidPrecomputePadding { .. })
));
let week = Precomputed::new(&t0, &(t0 + Duration::from_days(7.0))).unwrap();
assert!(week.interp(&(t0 + Duration::from_days(3.5))).is_ok());
assert_eq!(table_len(7.0 * 86400.0, 60.0).unwrap(), 2 + 7 * 1440);
}
#[test]
fn test_past_eop_coverage_builds() {
crate::frametransform::ierstable::preload().expect("IERS tables present in tests");
let cov = crate::earth_orientation_params::coverage().expect("EOP loaded in tests");
let t0 = cov.last + Duration::from_days(30.0);
let t1 = t0 + Duration::from_days(1.0);
let pc = Precomputed::new(&t0, &t1).unwrap();
let s = pc.interp(&(t0 + Duration::from_seconds(3600.0))).unwrap();
assert!(s.qgcrf2itrf.to_axis_angle().1.is_finite());
}
#[test]
fn test_invalid_step_errors() {
let t0 = Instant::from_date(2015, 3, 20).unwrap();
let t1 = t0 + Duration::from_seconds(3600.0);
assert!(matches!(
Precomputed::new_with_step(&t0, &t1, 0.0),
Err(Error::InvalidPrecomputeStep { .. })
));
assert!(Precomputed::new_with_step(&t0, &t1, -60.0).is_err());
assert!(Precomputed::new_with_step(&t0, &t1, f64::NAN).is_err());
}
#[test]
fn test_interp_or_compute_out_of_range() {
let t0 = Instant::from_date(2015, 3, 20).unwrap();
let t1 = t0 + Duration::from_seconds(3600.0);
let pc = Precomputed::new(&t0, &t1).unwrap();
let tin = t0 + Duration::from_seconds(100.0);
let a = pc.interp(&tin).unwrap();
let b = pc.interp_or_compute(&tin);
assert_eq!(a.sun_pos_gcrf.as_slice(), b.sun_pos_gcrf.as_slice());
let tout = t1 + Duration::from_seconds(86400.0);
assert!(pc.interp(&tout).is_err());
let s = pc.interp_or_compute(&tout);
assert!(s.sun_pos_gcrf.as_slice().iter().all(|v| v.is_finite()));
assert!(s.moon_pos_gcrf.as_slice().iter().all(|v| v.is_finite()));
assert!(s.sun_vel_gcrf.as_slice().iter().all(|v| v.is_finite()));
}
}