#![allow(clippy::needless_range_loop)]
use std::ffi::CStr;
use crate::coordinate::{Frame, Origin};
use crate::error::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CovarianceKind {
Linear,
SecondOrder,
ThirdOrder,
Mixture,
MonteCarlo,
SigmaPoint,
}
impl CovarianceKind {
pub(crate) fn from_u8(tag: u8) -> Result<Self> {
Ok(match tag as u32 {
empyrean_sys::EMPYREAN_COVARIANCE_KIND_LINEAR => Self::Linear,
empyrean_sys::EMPYREAN_COVARIANCE_KIND_SECOND_ORDER => Self::SecondOrder,
empyrean_sys::EMPYREAN_COVARIANCE_KIND_THIRD_ORDER => Self::ThirdOrder,
empyrean_sys::EMPYREAN_COVARIANCE_KIND_MIXTURE => Self::Mixture,
empyrean_sys::EMPYREAN_COVARIANCE_KIND_MONTE_CARLO => Self::MonteCarlo,
empyrean_sys::EMPYREAN_COVARIANCE_KIND_SIGMA_POINT => Self::SigmaPoint,
other => {
return Err(Error::invalid_input(format!(
"C ABI returned unknown covariance kind tag: {other}"
)));
}
})
}
pub(crate) fn to_u8(self) -> u8 {
let tag = match self {
Self::Linear => empyrean_sys::EMPYREAN_COVARIANCE_KIND_LINEAR,
Self::SecondOrder => empyrean_sys::EMPYREAN_COVARIANCE_KIND_SECOND_ORDER,
Self::ThirdOrder => empyrean_sys::EMPYREAN_COVARIANCE_KIND_THIRD_ORDER,
Self::Mixture => empyrean_sys::EMPYREAN_COVARIANCE_KIND_MIXTURE,
Self::MonteCarlo => empyrean_sys::EMPYREAN_COVARIANCE_KIND_MONTE_CARLO,
Self::SigmaPoint => empyrean_sys::EMPYREAN_COVARIANCE_KIND_SIGMA_POINT,
};
tag as u8
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CovarianceQuality {
PositiveDefinite,
Indefinite {
min_eig: f64,
},
Repaired {
min_eig: f64,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TargetFunctional {
CartesianState,
CloseApproachMissDistance,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TaggedCovariance {
pub epoch: crate::Epoch,
pub state: [f64; 6],
pub matrix: [[f64; 6]; 6],
pub kind: CovarianceKind,
pub mc_seed: Option<u64>,
pub mean_shift_prop: Option<[f64; 6]>,
pub mean_shift_input: Option<[f64; 6]>,
pub quality: CovarianceQuality,
pub origin: Origin,
pub frame: Frame,
pub non_grav: [bool; 3],
pub thrust_segments: u32,
pub solved_width: u32,
pub target_functional: TargetFunctional,
}
impl TaggedCovariance {
pub(crate) fn from_ffi(s: &empyrean_sys::EmpyreanTaggedCovariance) -> Result<Self> {
let origin = Origin::from_naif_id(s.origin).ok_or_else(|| {
Error::invalid_input(format!(
"C ABI returned unknown NAIF id for tagged-covariance origin: {}",
s.origin
))
})?;
let frame = crate::coordinate::int_to_frame(s.frame)?;
let quality = match s.quality as u32 {
empyrean_sys::EMPYREAN_COVARIANCE_QUALITY_POSITIVE_DEFINITE => {
CovarianceQuality::PositiveDefinite
}
empyrean_sys::EMPYREAN_COVARIANCE_QUALITY_INDEFINITE => CovarianceQuality::Indefinite {
min_eig: s.quality_min_eig,
},
empyrean_sys::EMPYREAN_COVARIANCE_QUALITY_REPAIRED => CovarianceQuality::Repaired {
min_eig: s.quality_min_eig,
},
other => {
return Err(Error::invalid_input(format!(
"C ABI returned unknown covariance quality tag: {other}"
)));
}
};
let target_functional = match s.target_functional as u32 {
empyrean_sys::EMPYREAN_TARGET_FUNCTIONAL_CARTESIAN_STATE => {
TargetFunctional::CartesianState
}
empyrean_sys::EMPYREAN_TARGET_FUNCTIONAL_CLOSE_APPROACH_MISS_DISTANCE => {
TargetFunctional::CloseApproachMissDistance
}
other => {
return Err(Error::invalid_input(format!(
"C ABI returned unknown target functional tag: {other}"
)));
}
};
Ok(Self {
epoch: crate::Epoch::from_mjd_tdb(s.epoch_mjd_tdb),
state: s.state,
matrix: s.matrix,
kind: CovarianceKind::from_u8(s.kind)?,
mc_seed: (s.has_mc_seed != 0).then_some(s.mc_seed),
mean_shift_prop: (s.has_mean_shift_prop != 0).then_some(s.mean_shift_prop),
mean_shift_input: (s.has_mean_shift_input != 0).then_some(s.mean_shift_input),
quality,
origin,
frame,
non_grav: [s.non_grav[0] != 0, s.non_grav[1] != 0, s.non_grav[2] != 0],
thrust_segments: s.thrust_segments,
solved_width: s.solved_width,
target_functional,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PropagatedState {
pub epoch: crate::Epoch,
pub position: [f64; 3],
pub velocity: [f64; 3],
pub origin: Origin,
pub frame: Frame,
pub covariance: Option<[[f64; 6]; 6]>,
pub stm: Option<[[f64; 6]; 6]>,
pub stt: Option<[[[f64; 6]; 6]; 6]>,
pub resolved_kind: CovarianceKind,
}
impl PropagatedState {
pub(crate) fn from_ffi(s: &empyrean_sys::EmpyreanPropagatedState) -> Result<Self> {
let origin = Origin::from_naif_id(s.origin).ok_or_else(|| {
Error::invalid_input(format!(
"C ABI returned unknown NAIF id for origin: {}",
s.origin
))
})?;
let frame = crate::coordinate::int_to_frame(s.frame)?;
Ok(Self {
epoch: crate::Epoch::from_mjd_tdb(s.epoch_mjd_tdb),
position: [s.x, s.y, s.z],
velocity: [s.vx, s.vy, s.vz],
origin,
frame,
covariance: (s.has_covariance != 0).then_some(s.covariance),
stm: (s.has_stm != 0).then_some(s.stm),
stt: (s.has_stt != 0).then_some(s.stt),
resolved_kind: CovarianceKind::from_u8(s.resolved_kind)?,
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Event {
pub event_type: String,
pub orbit_id: String,
pub object_id: String,
pub body: Option<Origin>,
pub epoch: crate::Epoch,
pub distance_au: f64,
pub distance_km: f64,
pub relative_velocity_au_day: f64,
pub two_body_energy: f64,
pub jacobi_constant: f64,
pub jacobi_constant_sigma: f64,
pub jacobi_constant_l1: f64,
pub jacobi_constant_l2: f64,
pub n_periapses: Option<u32>,
pub impact_latitude_deg: f64,
pub impact_longitude_deg: f64,
pub impact_altitude_km: f64,
pub shadow_fraction: f64,
pub illumination: f64,
pub relative_x: f64,
pub relative_y: f64,
pub relative_z: f64,
pub relative_vx: f64,
pub relative_vy: f64,
pub relative_vz: f64,
pub effective_radius_au: f64,
pub effective_radius_km: f64,
pub sigma_distance_au: f64,
pub ip_linear: f64,
pub ip_second_order: f64,
pub nonlinearity: f64,
pub ip_agm: f64,
pub ip_mc: f64,
pub previous_kind: Option<CovarianceKind>,
pub regime_resolved_kind: Option<CovarianceKind>,
pub kappa: f64,
pub threshold_below: f64,
pub threshold_above: f64,
}
impl Event {
pub(crate) fn from_ffi(e: &empyrean_sys::EmpyreanEvent) -> Self {
fn cstr_to_string(ptr: *const std::ffi::c_char) -> String {
if ptr.is_null() {
String::new()
} else {
unsafe { CStr::from_ptr(ptr).to_string_lossy().into_owned() }
}
}
let body = if e.body_naif_id < 0 {
None
} else {
Origin::from_naif_id(e.body_naif_id)
};
let kind_opt = |tag: u8| {
if tag == 0xFF {
None
} else {
CovarianceKind::from_u8(tag).ok()
}
};
Self {
event_type: cstr_to_string(e.event_type),
orbit_id: cstr_to_string(e.orbit_id),
object_id: cstr_to_string(e.object_id),
body,
epoch: crate::Epoch::from_mjd_tdb(e.epoch_mjd_tdb),
distance_au: e.distance_au,
distance_km: e.distance_km,
relative_velocity_au_day: e.relative_velocity_au_day,
two_body_energy: e.two_body_energy,
jacobi_constant: e.jacobi_constant,
jacobi_constant_sigma: e.jacobi_constant_sigma,
jacobi_constant_l1: e.jacobi_constant_l1,
jacobi_constant_l2: e.jacobi_constant_l2,
n_periapses: (e.n_periapses >= 0).then_some(e.n_periapses as u32),
impact_latitude_deg: e.impact_latitude_deg,
impact_longitude_deg: e.impact_longitude_deg,
impact_altitude_km: e.impact_altitude_km,
shadow_fraction: e.shadow_fraction,
illumination: e.illumination,
relative_x: e.relative_x,
relative_y: e.relative_y,
relative_z: e.relative_z,
relative_vx: e.relative_vx,
relative_vy: e.relative_vy,
relative_vz: e.relative_vz,
effective_radius_au: e.effective_radius_au,
effective_radius_km: e.effective_radius_km,
sigma_distance_au: e.sigma_distance_au,
ip_linear: e.ip_linear,
ip_second_order: e.ip_second_order,
nonlinearity: e.nonlinearity,
ip_agm: e.ip_agm,
ip_mc: e.ip_mc,
previous_kind: kind_opt(e.previous_kind),
regime_resolved_kind: kind_opt(e.resolved_kind),
kappa: e.kappa,
threshold_below: e.threshold_below,
threshold_above: e.threshold_above,
}
}
}
#[derive(Debug)]
pub struct PropagationResult {
pub states: Vec<PropagatedState>,
pub object_ids: Vec<String>,
pub events: Vec<Event>,
ffi: Box<empyrean_sys::EmpyreanPropagationResult>,
}
unsafe impl Send for PropagationResult {}
impl Drop for PropagationResult {
fn drop(&mut self) {
unsafe { empyrean_sys::empyrean_propagation_result_free(&mut *self.ffi) };
}
}
impl PropagationResult {
pub(crate) fn new(
states: Vec<PropagatedState>,
object_ids: Vec<String>,
events: Vec<Event>,
ffi: empyrean_sys::EmpyreanPropagationResult,
) -> Self {
Self {
states,
object_ids,
events,
ffi: Box::new(ffi),
}
}
pub fn covariance_series_cartesian(&self, orbit_index: usize) -> Result<Vec<TaggedCovariance>> {
let mut out_series: *mut empyrean_sys::EmpyreanTaggedCovarianceSeries =
std::ptr::null_mut();
let code = unsafe {
empyrean_sys::empyrean_propagation_covariance_series_cartesian(
&*self.ffi,
orbit_index,
&mut out_series,
)
};
if code != 0 {
return Err(Error::capture(code));
}
let result = {
let series = unsafe { &*out_series };
unsafe { std::slice::from_raw_parts(series.entries, series.num_entries) }
.iter()
.map(TaggedCovariance::from_ffi)
.collect::<Result<Vec<_>>>()
};
unsafe { empyrean_sys::empyrean_tagged_covariance_series_free(out_series) };
result
}
pub fn covariance_at_cartesian(
&self,
orbit_index: usize,
epoch_index: usize,
) -> Result<TaggedCovariance> {
let mut out = std::mem::MaybeUninit::<empyrean_sys::EmpyreanTaggedCovariance>::uninit();
let code = unsafe {
empyrean_sys::empyrean_propagation_covariance_at_cartesian(
&*self.ffi,
orbit_index,
epoch_index,
out.as_mut_ptr(),
)
};
if code != 0 {
return Err(Error::capture(code));
}
let init = unsafe { out.assume_init() };
TaggedCovariance::from_ffi(&init)
}
}
#[cfg(test)]
mod order_lock_tests {
use super::*;
use crate::coordinate::CoordinateState;
use crate::{Context, Epoch, Orbit, PropagationConfig};
use std::path::PathBuf;
fn try_context() -> Option<Context> {
let candidates = [
std::env::var("EMPYREAN_DATA_DIR").ok().map(PathBuf::from),
std::env::var("HOME")
.ok()
.map(|h| PathBuf::from(h).join(".empyrean/data")),
];
for dir in candidates.into_iter().flatten() {
if let Ok(ctx) = Context::from_data_dir(Some(&dir)) {
return Some(ctx);
}
}
None
}
#[test]
fn covariance_series_is_index_ordered_with_states() {
let Some(ctx) = try_context() else {
eprintln!("skipping covariance_series_is_index_ordered_with_states: no data dir");
return;
};
let t0_mjd = 60000.0;
let t0 = Epoch::from_mjd_tdb(t0_mjd);
let mut cov = [[0.0_f64; 6]; 6];
for i in 0..3 {
cov[i][i] = 1e-12;
}
for i in 3..6 {
cov[i][i] = 1e-16;
}
let state = CoordinateState::cartesian(
t0,
[2.0, 0.0, 0.0, 0.0, 0.012_17, 0.0],
Frame::EclipticJ2000,
Origin::Sun,
)
.with_covariance(cov);
let orbit = Orbit::new(state).with_orbit_id("order-lock");
let offsets = [0.0, 10.0, 30.0, 60.0];
let epochs: Vec<Epoch> = offsets
.iter()
.map(|d| Epoch::from_mjd_tdb(t0_mjd + d))
.collect();
let result = ctx
.propagate(&[orbit], &epochs, &PropagationConfig::default())
.expect("propagation should succeed");
let series = result
.covariance_series_cartesian(0)
.expect("covariance series should be produced for a covariance-bearing orbit");
let n = epochs.len();
assert_eq!(series.len(), n, "one tagged covariance per output epoch");
assert_eq!(result.states.len(), n, "one orbit × n epochs");
for (k, tagged) in series.iter().enumerate() {
let st = &result.states[k];
let s_epoch = tagged.epoch.mjd_tdb().unwrap();
let st_epoch = st.epoch.mjd_tdb().unwrap();
assert!(
(s_epoch - st_epoch).abs() < 1e-9,
"series[{k}] epoch {s_epoch} != state epoch {st_epoch}"
);
let st_state = [
st.position[0],
st.position[1],
st.position[2],
st.velocity[0],
st.velocity[1],
st.velocity[2],
];
assert_eq!(
tagged.state, st_state,
"series[{k}] co-located state mismatch"
);
assert_eq!(
tagged.kind, st.resolved_kind,
"series[{k}] kind != per-state resolved_kind"
);
assert_eq!(tagged.kind, CovarianceKind::Linear);
}
}
}
#[cfg(test)]
mod tests {
use super::CovarianceKind;
#[test]
fn covariance_kind_round_trips_c_tags() {
for kind in [
CovarianceKind::Linear,
CovarianceKind::SecondOrder,
CovarianceKind::ThirdOrder,
CovarianceKind::Mixture,
CovarianceKind::MonteCarlo,
CovarianceKind::SigmaPoint,
] {
assert_eq!(CovarianceKind::from_u8(kind.to_u8()).unwrap(), kind);
}
assert_eq!(
CovarianceKind::from_u8(5).unwrap(),
CovarianceKind::SigmaPoint
);
assert!(CovarianceKind::from_u8(6).is_err(), "unknown tags reject");
}
}