use std::collections::HashMap;
use std::io::Read;
use affn::cartesian::{Position, Velocity};
use affn::interpolation::{CubicHermiteTable, HermiteNode, InterpolationError};
use affn::{ReferenceCenter, ReferenceFrame};
use qtty::unit::Kilometer;
use qtty::{Day, KmPerSecond, Quantity};
use tempoch::{Time, TDB};
use crate::formats::ccsds::oem::read_oem;
use crate::formats::FormatError;
#[cfg(feature = "pod")]
use crate::pod::providers::EphemerisProvider;
const J2000_JD: f64 = 2_451_545.0;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TabulatedCartesianState<C, F>
where
C: ReferenceCenter<Params = ()>,
F: ReferenceFrame,
{
pub epoch: Time<TDB>,
pub position: Position<C, F, Kilometer>,
pub velocity: Velocity<F, KmPerSecond>,
}
pub struct TabulatedEphemeris<C, F>
where
C: ReferenceCenter<Params = ()>,
F: ReferenceFrame,
{
body_naif_id: i32,
table: CubicHermiteTable<qtty::Second, Position<C, F, Kilometer>>,
}
impl<C, F> TabulatedEphemeris<C, F>
where
C: ReferenceCenter<Params = ()>,
F: ReferenceFrame,
{
pub fn new(
body_naif_id: i32,
states: Vec<TabulatedCartesianState<C, F>>,
) -> Result<Self, InterpolationError> {
let nodes = states
.into_iter()
.map(|state| HermiteNode {
abscissa: state.epoch.to::<tempoch::J2000s>().raw(),
value: state.position,
derivative: state.velocity,
})
.collect();
Ok(Self {
body_naif_id,
table: CubicHermiteTable::new(nodes)?,
})
}
pub fn body_naif_id(&self) -> i32 {
self.body_naif_id
}
pub fn coverage(&self) -> Result<(Time<TDB>, Time<TDB>), TabulatedEphemerisError> {
let samples = self.table.samples();
let start = samples[0].abscissa.value();
let stop = samples[samples.len() - 1].abscissa.value();
Ok((seconds_to_time_tdb(start)?, seconds_to_time_tdb(stop)?))
}
pub fn state_at(
&self,
epoch: Time<TDB>,
) -> Result<TabulatedCartesianState<C, F>, InterpolationError> {
let evaluated = self.table.evaluate(epoch.to::<tempoch::J2000s>().raw())?;
Ok(TabulatedCartesianState {
epoch,
position: evaluated.value,
velocity: evaluated.derivative,
})
}
pub fn state_seconds(
&self,
epoch_seconds_tdb: f64,
) -> Result<TabulatedCartesianState<C, F>, TabulatedEphemerisError> {
let epoch = seconds_to_time_tdb(epoch_seconds_tdb)?;
self.state_at(epoch).map_err(TabulatedEphemerisError::from)
}
}
pub struct TabulatedEphemerisProvider<C, F>
where
C: ReferenceCenter<Params = ()>,
F: ReferenceFrame,
{
ephemerides: HashMap<i32, TabulatedEphemeris<C, F>>,
}
impl<C, F> TabulatedEphemerisProvider<C, F>
where
C: ReferenceCenter<Params = ()>,
F: ReferenceFrame,
{
pub fn new(
ephemerides: Vec<TabulatedEphemeris<C, F>>,
) -> Result<Self, TabulatedEphemerisError> {
let mut by_id = HashMap::new();
for ephemeris in ephemerides {
let body_naif_id = ephemeris.body_naif_id();
if by_id.insert(body_naif_id, ephemeris).is_some() {
return Err(TabulatedEphemerisError::DuplicateBody(body_naif_id));
}
}
Ok(Self { ephemerides: by_id })
}
pub fn ephemeris(&self, body_naif_id: i32) -> Option<&TabulatedEphemeris<C, F>> {
self.ephemerides.get(&body_naif_id)
}
pub fn state_at(
&self,
body_naif_id: i32,
epoch: Time<TDB>,
) -> Result<TabulatedCartesianState<C, F>, TabulatedEphemerisError> {
self.ephemeris(body_naif_id)
.ok_or(TabulatedEphemerisError::UnknownBody(body_naif_id))?
.state_at(epoch)
.map_err(TabulatedEphemerisError::from)
}
}
#[cfg(feature = "pod")]
impl<C, F> EphemerisProvider for TabulatedEphemerisProvider<C, F>
where
C: ReferenceCenter<Params = ()>,
F: ReferenceFrame,
{
type State = TabulatedCartesianState<C, F>;
type Error = TabulatedEphemerisError;
fn state(&self, body_naif_id: i32, epoch_seconds_tdb: f64) -> Result<Self::State, Self::Error> {
self.ephemeris(body_naif_id)
.ok_or(TabulatedEphemerisError::UnknownBody(body_naif_id))?
.state_seconds(epoch_seconds_tdb)
}
}
#[derive(Debug, thiserror::Error)]
pub enum TabulatedEphemerisError {
#[error("unknown tabulated body id {0}")]
UnknownBody(i32),
#[error("duplicate tabulated body id {0}")]
DuplicateBody(i32),
#[error("invalid TDB epoch: {0}")]
InvalidEpoch(String),
#[error("interpolation failed: {0}")]
Interpolation(#[from] InterpolationError),
#[error("OEM conversion failed: {0}")]
Format(#[from] FormatError),
}
pub fn read_oem_tdb_ephemeris<C, F, R>(
body_naif_id: i32,
reader: R,
) -> Result<TabulatedEphemeris<C, F>, TabulatedEphemerisError>
where
C: ReferenceCenter<Params = ()>,
F: ReferenceFrame,
R: Read,
{
let oem_file = read_oem(reader)?;
let mut states = Vec::new();
for segment in oem_file.segments {
if !segment.metadata.time_system.eq_ignore_ascii_case("TDB") {
return Err(TabulatedEphemerisError::Format(FormatError::Format(
"read_oem_tdb_ephemeris: expected TIME_SYSTEM = TDB".into(),
)));
}
for state in segment.states {
let epoch = jd_to_time_tdb(state.epoch_jd)?;
let [x, y, z] = state.position_km;
let [vx, vy, vz] = state.velocity_km_s;
states.push(TabulatedCartesianState {
epoch,
position: Position::<C, F, Kilometer>::new(
qtty::Kilometer::new(x),
qtty::Kilometer::new(y),
qtty::Kilometer::new(z),
),
velocity: Velocity::<F, KmPerSecond>::new(
Quantity::<KmPerSecond>::new(vx),
Quantity::<KmPerSecond>::new(vy),
Quantity::<KmPerSecond>::new(vz),
),
});
}
}
Ok(TabulatedEphemeris::new(body_naif_id, states)?)
}
fn jd_to_time_tdb(jd: f64) -> Result<Time<TDB>, TabulatedEphemerisError> {
tempoch::JulianDate::<TDB>::try_new(Day::new(jd))
.map(Into::into)
.map_err(|e| TabulatedEphemerisError::InvalidEpoch(e.to_string()))
}
fn seconds_to_time_tdb(seconds: f64) -> Result<Time<TDB>, TabulatedEphemerisError> {
Time::<TDB>::from_raw_j2000_seconds(qtty::Second::new(seconds))
.map_err(|e| TabulatedEphemerisError::InvalidEpoch(e.to_string()))
}
pub fn offset_tdb_epoch(
epoch: Time<TDB>,
offset: qtty::Second,
) -> Result<Time<TDB>, TabulatedEphemerisError> {
seconds_to_time_tdb(epoch.to::<tempoch::J2000s>().raw().value() + offset.value())
}
pub fn j2000_seconds_to_tdb_jd(seconds: qtty::Second) -> f64 {
seconds.value() / 86_400.0 + J2000_JD
}
#[cfg(test)]
mod tests {
use super::*;
use crate::coordinates::centers::Heliocentric;
use crate::coordinates::frames::EME2000;
const OEM_SAMPLE: &str = "\
CCSDS_OEM_VERS = 2.0\n\
CREATION_DATE = 2024-01-01T00:00:00\n\
ORIGINATOR = TEST\n\
\n\
META_START\n\
OBJECT_NAME = TEST\n\
OBJECT_ID = -1001\n\
CENTER_NAME = SUN\n\
REF_FRAME = EME2000\n\
TIME_SYSTEM = TDB\n\
START_TIME = 2036-02-12T12:00:00.000\n\
STOP_TIME = 2036-02-12T13:00:00.000\n\
META_STOP\n\
\n\
2036-02-12T12:00:00.000 100000000.0 50000000.0 10000000.0 10.0 20.0 5.0\n\
2036-02-12T13:00:00.000 100036000.0 50072000.0 10018000.0 10.0 20.0 5.0\n";
type LisaEphemeris = TabulatedEphemeris<Heliocentric, EME2000>;
type LisaProvider = TabulatedEphemerisProvider<Heliocentric, EME2000>;
#[test]
fn reads_oem_into_typed_ephemeris() {
let ephemeris: LisaEphemeris =
read_oem_tdb_ephemeris(-1001, OEM_SAMPLE.as_bytes()).unwrap();
let state = ephemeris.state_seconds(epoch0_seconds()).unwrap();
assert!((state.position.x().value() - 100_000_000.0).abs() < 1e-9);
assert!((state.velocity.y().value() - 20.0).abs() < 1e-12);
}
#[test]
fn provider_reports_unknown_body() {
let ephemeris: LisaEphemeris =
read_oem_tdb_ephemeris(-1001, OEM_SAMPLE.as_bytes()).unwrap();
let provider = LisaProvider::new(vec![ephemeris]).unwrap();
assert!(matches!(
provider
.state_at(999, seconds_to_time_tdb(epoch0_seconds()).unwrap())
.unwrap_err(),
TabulatedEphemerisError::UnknownBody(999)
));
}
#[test]
fn provider_rejects_duplicate_body() {
let a: LisaEphemeris = read_oem_tdb_ephemeris(-1001, OEM_SAMPLE.as_bytes()).unwrap();
let b: LisaEphemeris = read_oem_tdb_ephemeris(-1001, OEM_SAMPLE.as_bytes()).unwrap();
match LisaProvider::new(vec![a, b]) {
Err(TabulatedEphemerisError::DuplicateBody(-1001)) => {}
Err(other) => panic!("unexpected error: {other}"),
Ok(_) => panic!("duplicate body should fail"),
}
}
#[test]
fn provider_reports_out_of_range() {
let ephemeris: LisaEphemeris =
read_oem_tdb_ephemeris(-1001, OEM_SAMPLE.as_bytes()).unwrap();
let err = ephemeris.state_seconds(epoch0_seconds() - 1.0).unwrap_err();
assert!(matches!(
err,
TabulatedEphemerisError::Interpolation(InterpolationError::OutOfRange { .. })
));
}
fn epoch0_seconds() -> f64 {
let ephemeris: LisaEphemeris =
read_oem_tdb_ephemeris(-1001, OEM_SAMPLE.as_bytes()).unwrap();
ephemeris.table.samples()[0].abscissa.value()
}
}