#![allow(clippy::needless_range_loop)]
use crate::JointCovariance;
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,
},
ExpansionSuspect {
kappa_state: 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,
},
empyrean_sys::EMPYREAN_COVARIANCE_QUALITY_EXPANSION_SUSPECT => {
CovarianceQuality::ExpansionSuspect {
kappa_state: s.quality_kappa_state,
}
}
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, 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,
pub joint: crate::JointCovariance,
}
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)?,
joint: unsafe { crate::JointCovariance::from_ffi(&s.orbit_cov) }?,
})
}
}
#[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, Clone, Copy, PartialEq)]
pub struct MixtureComponent {
pub weight: f64,
pub mean: [f64; 6],
pub covariance: [[f64; 6]; 6],
pub frame: Frame,
pub origin: Origin,
}
#[derive(Debug, Clone, PartialEq)]
pub struct MixtureChain {
pub orbit_id: String,
pub ca_epochs_mjd_tdb: Vec<f64>,
pub components: Vec<Vec<MixtureComponent>>,
}
impl MixtureComponent {
fn from_ffi(c: &empyrean_sys::EmpyreanMixtureComponent) -> Result<Self> {
let origin = Origin::from_naif_id(c.origin).ok_or_else(|| {
Error::invalid_input(format!(
"C ABI returned unknown NAIF id for mixture component origin: {}",
c.origin
))
})?;
let frame = crate::coordinate::int_to_frame(c.frame)?;
Ok(Self {
weight: c.weight,
mean: c.mean,
covariance: c.covariance,
frame,
origin,
})
}
}
impl MixtureChain {
pub(crate) fn empty() -> Self {
Self {
orbit_id: String::new(),
ca_epochs_mjd_tdb: Vec::new(),
components: Vec::new(),
}
}
pub(crate) unsafe fn from_ffi(
c: &empyrean_sys::EmpyreanMixtureChain,
chain_index: usize,
) -> Result<Self> {
let orbit_id = if c.orbit_id.is_null() {
String::new()
} else {
unsafe { CStr::from_ptr(c.orbit_id) }
.to_string_lossy()
.into_owned()
};
let n_epochs = c.num_ca_epochs;
if n_epochs == 0 {
return Ok(Self {
orbit_id,
ca_epochs_mjd_tdb: Vec::new(),
components: Vec::new(),
});
}
if c.ca_epochs_mjd_tdb.is_null() {
return Err(Error::invalid_input(format!(
"mixture chain {chain_index}: null ca_epochs_mjd_tdb with num_ca_epochs = {n_epochs}"
)));
}
if c.components_per_epoch.is_null() {
return Err(Error::invalid_input(format!(
"mixture chain {chain_index}: null components_per_epoch with num_ca_epochs = {n_epochs}"
)));
}
if c.components_offset.is_null() {
return Err(Error::invalid_input(format!(
"mixture chain {chain_index}: null components_offset with num_ca_epochs = {n_epochs}"
)));
}
let epochs = unsafe { std::slice::from_raw_parts(c.ca_epochs_mjd_tdb, n_epochs) };
let counts = unsafe { std::slice::from_raw_parts(c.components_per_epoch, n_epochs) };
let offsets = unsafe { std::slice::from_raw_parts(c.components_offset, n_epochs) };
let total = c.num_components_total;
let flat: &[empyrean_sys::EmpyreanMixtureComponent] = if total == 0 {
&[]
} else if c.components.is_null() {
return Err(Error::invalid_input(format!(
"mixture chain {chain_index}: null components with num_components_total = {total}"
)));
} else {
unsafe { std::slice::from_raw_parts(c.components, total) }
};
let mut components: Vec<Vec<MixtureComponent>> = Vec::with_capacity(n_epochs);
for k in 0..n_epochs {
let (start, count) = (offsets[k], counts[k]);
if start > total {
return Err(Error::invalid_input(format!(
"mixture chain {chain_index}: components_offset[{k}] = {start} exceeds \
num_components_total = {total}"
)));
}
let end = start.checked_add(count).ok_or_else(|| {
Error::invalid_input(format!(
"mixture chain {chain_index}: components_offset[{k}] = {start} + \
components_per_epoch[{k}] = {count} overflows"
))
})?;
if end > total {
return Err(Error::invalid_input(format!(
"mixture chain {chain_index}: components_offset[{k}] = {start} + \
components_per_epoch[{k}] = {count} exceeds num_components_total = {total}"
)));
}
components.push(
flat[start..end]
.iter()
.map(MixtureComponent::from_ffi)
.collect::<Result<Vec<_>>>()?,
);
}
Ok(Self {
orbit_id,
ca_epochs_mjd_tdb: epochs.to_vec(),
components,
})
}
}
#[derive(Debug)]
pub struct PropagationResult {
pub states: Vec<PropagatedState>,
pub object_ids: Vec<String>,
pub events: Vec<Event>,
pub mixtures: Vec<MixtureChain>,
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>,
mixtures: Vec<MixtureChain>,
ffi: empyrean_sys::EmpyreanPropagationResult,
) -> Self {
Self {
states,
object_ids,
events,
mixtures,
ffi: Box::new(ffi),
}
}
pub fn mixture_at(
&self,
orbit_index: usize,
epoch_mjd_tdb: f64,
tolerance_days: f64,
) -> Option<&[MixtureComponent]> {
let chain = self.mixtures.get(orbit_index)?;
let mut best: Option<(usize, f64)> = None;
for (k, t) in chain.ca_epochs_mjd_tdb.iter().enumerate() {
let d = (t - epoch_mjd_tdb).abs();
if d <= tolerance_days && best.is_none_or(|(_, bd)| d < bd) {
best = Some((k, d));
}
}
let (k, _) = best?;
chain.components.get(k).map(|v| v.as_slice())
}
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)
}
pub fn joint_at(&self, orbit_index: usize, epoch_index: usize) -> Result<JointCovariance> {
let mut out = empyrean_sys::EmpyreanOrbitCovariance::default();
let code = unsafe {
empyrean_sys::empyrean_propagation_joint_at(
&*self.ffi,
orbit_index,
epoch_index,
&mut out,
)
};
if code != 0 {
return Err(Error::capture(code));
}
let joint = unsafe { JointCovariance::from_ffi(&out) };
unsafe { empyrean_sys::empyrean_orbit_covariance_free(&mut out) };
joint
}
}
#[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");
}
}
#[cfg(test)]
mod mixture_marshal_tests {
use super::{MixtureChain, MixtureComponent};
use crate::coordinate::{Frame, Origin};
fn comp(weight: f64) -> empyrean_sys::EmpyreanMixtureComponent {
empyrean_sys::EmpyreanMixtureComponent {
weight,
mean: [weight; 6],
covariance: [[weight; 6]; 6],
frame: 0,
origin: 399,
}
}
fn chain(
epochs: &mut [f64],
counts: &mut [usize],
offsets: &mut [usize],
comps: &mut [empyrean_sys::EmpyreanMixtureComponent],
total: usize,
) -> empyrean_sys::EmpyreanMixtureChain {
empyrean_sys::EmpyreanMixtureChain {
orbit_id: std::ptr::null_mut(),
ca_epochs_mjd_tdb: epochs.as_mut_ptr(),
num_ca_epochs: epochs.len(),
components_per_epoch: counts.as_mut_ptr(),
components_offset: offsets.as_mut_ptr(),
components: comps.as_mut_ptr(),
num_components_total: total,
}
}
#[test]
fn unflattens_components_by_offset_and_count() {
let mut epochs = [60000.0_f64, 60100.0, 60200.0];
let mut counts = [2_usize, 0, 3];
let mut offsets = [0_usize, 2, 2];
let mut comps = [comp(1.0), comp(2.0), comp(3.0), comp(4.0), comp(5.0)];
let c = chain(&mut epochs, &mut counts, &mut offsets, &mut comps, 5);
let out = unsafe { MixtureChain::from_ffi(&c, 0) }.expect("well-formed chain marshals");
assert_eq!(out.ca_epochs_mjd_tdb, vec![60000.0, 60100.0, 60200.0]);
assert_eq!(out.components.len(), 3);
let weights: Vec<Vec<f64>> = out
.components
.iter()
.map(|g| g.iter().map(|c| c.weight).collect())
.collect();
assert_eq!(weights, vec![vec![1.0, 2.0], vec![], vec![3.0, 4.0, 5.0]]);
assert!(out.components[1].is_empty());
assert_eq!(out.components[0][0].frame, Frame::ICRF);
assert_eq!(out.components[0][0].origin, Origin::EARTH);
assert_eq!(out.components[2][2].mean, [5.0; 6]);
}
#[test]
fn malformed_offset_errors_rather_than_reading_out_of_bounds() {
let mut epochs = [60000.0_f64];
let mut counts = [4_usize];
let mut offsets = [0_usize];
let mut comps = [comp(1.0), comp(2.0)];
let c = chain(&mut epochs, &mut counts, &mut offsets, &mut comps, 2);
let err = unsafe { MixtureChain::from_ffi(&c, 7) }.expect_err("must reject");
assert!(
err.message.contains("mixture chain 7") && err.message.contains("exceeds"),
"expected a bounds error naming the chain, got: {}",
err.message
);
}
#[test]
fn offset_past_end_errors_on_its_own_axis() {
let mut epochs = [60000.0_f64];
let mut counts = [0_usize];
let mut offsets = [9_usize];
let mut comps = [comp(1.0), comp(2.0)];
let c = chain(&mut epochs, &mut counts, &mut offsets, &mut comps, 2);
let err = unsafe { MixtureChain::from_ffi(&c, 3) }.expect_err("must reject");
assert!(
err.message.contains("components_offset[0] = 9"),
"expected the offset axis to be named, got: {}",
err.message
);
assert!(
!err.message.contains("components_per_epoch[0]"),
"the extent axis must not be blamed for an offset fault: {}",
err.message
);
}
#[test]
fn all_null_chain_marshals_to_empty() {
let c = empyrean_sys::EmpyreanMixtureChain {
orbit_id: std::ptr::null_mut(),
ca_epochs_mjd_tdb: std::ptr::null_mut(),
num_ca_epochs: 0,
components_per_epoch: std::ptr::null_mut(),
components_offset: std::ptr::null_mut(),
components: std::ptr::null_mut(),
num_components_total: 0,
};
let out = unsafe { MixtureChain::from_ffi(&c, 0) }.expect("empty chain marshals");
assert!(out.ca_epochs_mjd_tdb.is_empty());
assert!(out.components.is_empty());
assert_eq!(out.orbit_id, "");
}
#[test]
fn null_parallel_array_with_nonzero_count_errors() {
let mut epochs = [60000.0_f64];
let mut counts = [1_usize];
let mut offsets = [0_usize];
let mut comps = [comp(1.0)];
let mut c = chain(&mut epochs, &mut counts, &mut offsets, &mut comps, 1);
c.ca_epochs_mjd_tdb = std::ptr::null_mut();
let err = unsafe { MixtureChain::from_ffi(&c, 0) }.expect_err("must reject");
assert!(
err.message.contains("null ca_epochs_mjd_tdb"),
"{}",
err.message
);
let mut c = chain(&mut epochs, &mut counts, &mut offsets, &mut comps, 1);
c.components_per_epoch = std::ptr::null_mut();
let err = unsafe { MixtureChain::from_ffi(&c, 0) }.expect_err("must reject");
assert!(
err.message.contains("null components_per_epoch"),
"{}",
err.message
);
let mut c = chain(&mut epochs, &mut counts, &mut offsets, &mut comps, 1);
c.components_offset = std::ptr::null_mut();
let err = unsafe { MixtureChain::from_ffi(&c, 0) }.expect_err("must reject");
assert!(
err.message.contains("null components_offset"),
"{}",
err.message
);
let mut c = chain(&mut epochs, &mut counts, &mut offsets, &mut comps, 1);
c.components = std::ptr::null_mut();
let err = unsafe { MixtureChain::from_ffi(&c, 0) }.expect_err("must reject");
assert!(err.message.contains("null components"), "{}", err.message);
}
#[test]
fn unknown_basis_tags_error_rather_than_default() {
let mut epochs = [60000.0_f64];
let mut counts = [1_usize];
let mut offsets = [0_usize];
let mut bad_frame = [comp(1.0)];
bad_frame[0].frame = 77;
let c = chain(&mut epochs, &mut counts, &mut offsets, &mut bad_frame, 1);
assert!(unsafe { MixtureChain::from_ffi(&c, 0) }.is_err());
let mut bad_origin = [comp(1.0)];
bad_origin[0].origin = -12345;
let c = chain(&mut epochs, &mut counts, &mut offsets, &mut bad_origin, 1);
let err = unsafe { MixtureChain::from_ffi(&c, 0) }.expect_err("must reject");
assert!(
err.message.contains("mixture component origin"),
"{}",
err.message
);
}
#[test]
fn mixture_at_selects_nearest_within_tolerance() {
fn component(weight: f64) -> MixtureComponent {
MixtureComponent {
weight,
mean: [0.0; 6],
covariance: [[0.0; 6]; 6],
frame: Frame::ICRF,
origin: Origin::EARTH,
}
}
let result = super::PropagationResult::new(
Vec::new(),
Vec::new(),
Vec::new(),
vec![MixtureChain {
orbit_id: "a".into(),
ca_epochs_mjd_tdb: vec![60000.0, 60000.5],
components: vec![vec![component(0.25)], vec![component(0.75)]],
}],
empyrean_sys::EmpyreanPropagationResult {
states: std::ptr::null_mut(),
num_states: 0,
object_ids: std::ptr::null_mut(),
events: std::ptr::null_mut(),
num_events: 0,
mixtures: std::ptr::null_mut(),
num_mixtures: 0,
lazy_handle: std::ptr::null_mut(),
},
);
let near = result
.mixture_at(0, 60000.4, 1.0)
.expect("within tolerance");
assert_eq!(near[0].weight, 0.75, "nearest CA wins, not the first");
assert!(
result.mixture_at(0, 60050.0, 1.0).is_none(),
"outside tolerance"
);
assert!(
result.mixture_at(1, 60000.0, 1.0).is_none(),
"orbit out of range"
);
}
}