use std::collections::BTreeMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ParamColumn {
Marsden(usize),
Dt,
Amrat,
Thrust {
segment: usize,
component: usize,
},
}
impl ParamColumn {
pub fn as_tag(self) -> String {
match self {
ParamColumn::Marsden(i) => format!("A{}", i + 1),
ParamColumn::Dt => "DT".to_string(),
ParamColumn::Amrat => "AMRAT".to_string(),
ParamColumn::Thrust { segment, component } => {
let axis = ["x", "y", "z"].get(component).copied().unwrap_or("?");
format!("thrust[{segment}].{axis}")
}
}
}
pub fn from_tag(tag: &str) -> crate::error::Result<Self> {
let bad = || {
crate::error::Error::invalid_input(format!(
"unknown parameter-column tag {tag:?}; expected A1/A2/A3, DT, AMRAT, \
or thrust[<segment>].<x|y|z>"
))
};
match tag {
"A1" => return Ok(ParamColumn::Marsden(0)),
"A2" => return Ok(ParamColumn::Marsden(1)),
"A3" => return Ok(ParamColumn::Marsden(2)),
"DT" => return Ok(ParamColumn::Dt),
"AMRAT" => return Ok(ParamColumn::Amrat),
_ => {}
}
let rest = tag.strip_prefix("thrust[").ok_or_else(bad)?;
let (seg, rest) = rest.split_once(']').ok_or_else(bad)?;
let axis = rest.strip_prefix('.').ok_or_else(bad)?;
let component = match axis {
"x" => 0,
"y" => 1,
"z" => 2,
_ => return Err(bad()),
};
Ok(ParamColumn::Thrust {
segment: seg.parse::<usize>().map_err(|_| bad())?,
component,
})
}
pub(crate) fn to_ffi(self) -> empyrean_sys::EmpyreanParamColumn {
let (kind, index, segment, component) = match self {
ParamColumn::Marsden(i) => {
(empyrean_sys::EMPYREAN_PARAM_COLUMN_MARSDEN, i as u32, 0, 0)
}
ParamColumn::Dt => (empyrean_sys::EMPYREAN_PARAM_COLUMN_DT, 0, 0, 0),
ParamColumn::Amrat => (empyrean_sys::EMPYREAN_PARAM_COLUMN_AMRAT, 0, 0, 0),
ParamColumn::Thrust { segment, component } => (
empyrean_sys::EMPYREAN_PARAM_COLUMN_THRUST,
0,
segment as u32,
component as u32,
),
};
empyrean_sys::EmpyreanParamColumn {
kind,
index,
segment,
component,
}
}
pub(crate) fn from_ffi(c: &empyrean_sys::EmpyreanParamColumn) -> crate::error::Result<Self> {
match c.kind {
empyrean_sys::EMPYREAN_PARAM_COLUMN_MARSDEN => {
Ok(ParamColumn::Marsden(c.index as usize))
}
empyrean_sys::EMPYREAN_PARAM_COLUMN_DT => Ok(ParamColumn::Dt),
empyrean_sys::EMPYREAN_PARAM_COLUMN_AMRAT => Ok(ParamColumn::Amrat),
empyrean_sys::EMPYREAN_PARAM_COLUMN_THRUST => Ok(ParamColumn::Thrust {
segment: c.segment as usize,
component: c.component as usize,
}),
other => Err(crate::error::Error::invalid_input(format!(
"the engine returned an unknown parameter-column kind {other}"
))),
}
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct WideCross {
state: BTreeMap<ParamColumn, [f64; 6]>,
params: BTreeMap<(ParamColumn, ParamColumn), f64>,
}
impl WideCross {
pub fn new() -> Self {
Self::default()
}
pub fn is_empty(&self) -> bool {
self.state.is_empty() && self.params.is_empty()
}
pub fn set_state_cross(&mut self, column: ParamColumn, values: [f64; 6]) {
self.state.insert(column, values);
}
pub fn state_cross(&self, column: ParamColumn) -> Option<&[f64; 6]> {
self.state.get(&column)
}
pub fn state_crosses(&self) -> impl Iterator<Item = (ParamColumn, &[f64; 6])> {
self.state
.iter()
.map(|(k, v): (&ParamColumn, &[f64; 6])| (*k, v))
}
pub fn set_param_cross(&mut self, a: ParamColumn, b: ParamColumn, value: f64) {
let key = if a <= b { (a, b) } else { (b, a) };
self.params.insert(key, value);
}
pub fn param_cross(&self, a: ParamColumn, b: ParamColumn) -> Option<f64> {
let key = if a <= b { (a, b) } else { (b, a) };
self.params.get(&key).copied()
}
pub fn param_crosses(&self) -> impl Iterator<Item = (ParamColumn, ParamColumn, f64)> + '_ {
self.params
.iter()
.map(|((a, b), v): (&(ParamColumn, ParamColumn), &f64)| (*a, *b, *v))
}
pub(crate) fn to_ffi_arrays(
&self,
) -> (
Vec<empyrean_sys::EmpyreanStateParamCross>,
Vec<empyrean_sys::EmpyreanParamPairCross>,
) {
let state = self
.state_crosses()
.map(|(column, values)| empyrean_sys::EmpyreanStateParamCross {
column: column.to_ffi(),
values: *values,
})
.collect();
let pairs = self
.param_crosses()
.map(|(a, b, value)| empyrean_sys::EmpyreanParamPairCross {
a: a.to_ffi(),
b: b.to_ffi(),
value,
})
.collect();
(state, pairs)
}
pub(crate) unsafe fn from_ffi_arrays(
state: *const empyrean_sys::EmpyreanStateParamCross,
n_state: usize,
pairs: *const empyrean_sys::EmpyreanParamPairCross,
n_pairs: usize,
) -> crate::error::Result<Option<Self>> {
if (state.is_null() || n_state == 0) && (pairs.is_null() || n_pairs == 0) {
return Ok(None);
}
let mut out = WideCross::new();
if !state.is_null() {
for e in unsafe { std::slice::from_raw_parts(state, n_state) } {
out.set_state_cross(ParamColumn::from_ffi(&e.column)?, e.values);
}
}
if !pairs.is_null() {
for e in unsafe { std::slice::from_raw_parts(pairs, n_pairs) } {
out.set_param_cross(
ParamColumn::from_ffi(&e.a)?,
ParamColumn::from_ffi(&e.b)?,
e.value,
);
}
}
Ok(Some(out))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ParamDisposition {
Fixed,
Solved,
Considered,
}
impl ParamDisposition {
pub const fn is_solved(self) -> bool {
matches!(self, ParamDisposition::Solved)
}
pub const fn is_considered(self) -> bool {
matches!(self, ParamDisposition::Considered)
}
pub const fn as_tag(self) -> &'static str {
match self {
ParamDisposition::Fixed => "fixed",
ParamDisposition::Solved => "solved",
ParamDisposition::Considered => "considered",
}
}
pub fn from_tag(tag: &str) -> crate::error::Result<Self> {
match tag {
"fixed" => Ok(ParamDisposition::Fixed),
"solved" => Ok(ParamDisposition::Solved),
"considered" => Ok(ParamDisposition::Considered),
other => Err(crate::error::Error::invalid_input(format!(
"unknown parameter disposition {other:?}; expected \"fixed\", \
\"solved\" or \"considered\""
))),
}
}
pub(crate) const fn to_ffi(self) -> u8 {
match self {
ParamDisposition::Fixed => empyrean_sys::EMPYREAN_PARAM_FIXED,
ParamDisposition::Solved => empyrean_sys::EMPYREAN_PARAM_SOLVED,
ParamDisposition::Considered => empyrean_sys::EMPYREAN_PARAM_CONSIDERED,
}
}
pub(crate) fn from_ffi(v: u8, field: &str) -> crate::error::Result<Self> {
match v {
empyrean_sys::EMPYREAN_PARAM_FIXED => Ok(ParamDisposition::Fixed),
empyrean_sys::EMPYREAN_PARAM_SOLVED => Ok(ParamDisposition::Solved),
empyrean_sys::EMPYREAN_PARAM_CONSIDERED => Ok(ParamDisposition::Considered),
other => Err(crate::error::Error::invalid_input(format!(
"the engine returned {other} for the {field} disposition, which is not \
fixed (0), solved (1) or considered (2)"
))),
}
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct JointCovariance {
pub non_grav_cross: Option<[[f64; 3]; 6]>,
pub wide_cross: Option<WideCross>,
}
impl JointCovariance {
pub fn is_empty(&self) -> bool {
self.non_grav_cross.is_none() && self.wide_cross.as_ref().is_none_or(WideCross::is_empty)
}
pub(crate) unsafe fn from_ffi(
cov: &empyrean_sys::EmpyreanOrbitCovariance,
) -> crate::error::Result<Self> {
Ok(Self {
non_grav_cross: (cov.has_non_grav_cross != 0).then_some(cov.non_grav_cross),
wide_cross: unsafe {
WideCross::from_ffi_arrays(
cov.state_param_cross,
cov.n_state_param_cross,
cov.param_pair_cross,
cov.n_param_pair_cross,
)
}?,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_tag_round_trips() {
let mut cases = vec![ParamColumn::Dt, ParamColumn::Amrat];
cases.extend((0..3).map(ParamColumn::Marsden));
for s in 0..3 {
cases.extend((0..3).map(|c| ParamColumn::Thrust {
segment: s,
component: c,
}));
}
for c in cases {
let tag = c.as_tag();
assert_eq!(
ParamColumn::from_tag(&tag).expect("round trips"),
c,
"tag {tag:?} must parse back to the identity that rendered it"
);
assert_eq!(
ParamColumn::from_ffi(&c.to_ffi()).expect("ffi round trips"),
c
);
}
}
#[test]
fn the_tags_match_the_engines_rendering() {
assert_eq!(ParamColumn::Marsden(0).as_tag(), "A1");
assert_eq!(ParamColumn::Marsden(2).as_tag(), "A3");
assert_eq!(ParamColumn::Dt.as_tag(), "DT");
assert_eq!(ParamColumn::Amrat.as_tag(), "AMRAT");
assert_eq!(
ParamColumn::Thrust {
segment: 0,
component: 0
}
.as_tag(),
"thrust[0].x"
);
assert_eq!(
ParamColumn::Thrust {
segment: 2,
component: 2
}
.as_tag(),
"thrust[2].z"
);
}
#[test]
fn an_unknown_tag_is_an_error_never_a_default() {
for bad in [
"A4",
"A0",
"dt",
"thrust[0].w",
"thrust[].x",
"thrust0.x",
"",
] {
assert!(
ParamColumn::from_tag(bad).is_err(),
"{bad:?} must not parse — a mis-parsed tag attaches one parameter's \
correlations to another"
);
}
}
#[test]
fn a_pair_is_symmetric_and_stored_once() {
let mut w = WideCross::new();
w.set_param_cross(ParamColumn::Amrat, ParamColumn::Dt, 1.0);
assert_eq!(
w.param_cross(ParamColumn::Dt, ParamColumn::Amrat),
Some(1.0)
);
w.set_param_cross(ParamColumn::Dt, ParamColumn::Amrat, 2.0);
assert_eq!(w.param_crosses().count(), 1);
assert_eq!(
w.param_cross(ParamColumn::Amrat, ParamColumn::Dt),
Some(2.0)
);
}
#[test]
fn entry_order_is_not_contract() {
let a = {
let mut w = WideCross::new();
w.set_state_cross(ParamColumn::Dt, [1.0; 6]);
w.set_state_cross(ParamColumn::Amrat, [2.0; 6]);
w
};
let b = {
let mut w = WideCross::new();
w.set_state_cross(ParamColumn::Amrat, [2.0; 6]);
w.set_state_cross(ParamColumn::Dt, [1.0; 6]);
w
};
assert_eq!(a, b, "identity keying makes build order irrelevant");
}
#[test]
fn a_supplied_zero_entry_is_not_absence() {
let mut w = WideCross::new();
w.set_state_cross(ParamColumn::Dt, [0.0; 6]);
assert!(
!w.is_empty(),
"a supplied zero correlation is a claim; only omitting the entry means absent"
);
}
#[test]
fn dispositions_round_trip_and_reject_unknowns() {
for d in [
ParamDisposition::Fixed,
ParamDisposition::Solved,
ParamDisposition::Considered,
] {
assert_eq!(ParamDisposition::from_tag(d.as_tag()).unwrap(), d);
assert_eq!(ParamDisposition::from_ffi(d.to_ffi(), "x").unwrap(), d);
}
assert!(ParamDisposition::from_tag("true").is_err());
assert!(ParamDisposition::from_ffi(3, "marsden").is_err());
assert_eq!(
ParamDisposition::from_ffi(0, "x").unwrap(),
ParamDisposition::Fixed
);
assert_eq!(
ParamDisposition::from_ffi(1, "x").unwrap(),
ParamDisposition::Solved
);
}
}