use super::{DispersedState, StateDispersion};
use crate::cosmic::AstroPhysicsSnafu;
use crate::errors::StateError;
use crate::md::prelude::BPlane;
use crate::md::{AstroSnafu, StateParameter};
use crate::{NyxError, Spacecraft, State, pseudo_inverse};
use anise::analysis::prelude::OrbitalElement;
use anise::astro::orbit_gradient::OrbitGrad;
use nalgebra::{DMatrix, DVector, SMatrix, SVector};
use rand_distr::{Distribution, Normal};
use snafu::ResultExt;
use std::error::Error;
#[cfg(feature = "python")]
use pyo3::prelude::*;
#[derive(Clone, Debug)]
#[cfg_attr(feature = "python", pyclass(from_py_object))]
pub struct MvnSpacecraft {
pub template: Spacecraft,
pub dispersions: Vec<StateDispersion>,
pub mean: SVector<f64, 9>,
pub sqrt_s_v: SMatrix<f64, 9, 9>,
pub std_norm_distr: Normal<f64>,
}
impl MvnSpacecraft {
pub fn new(
template: Spacecraft,
dispersions: Vec<StateDispersion>,
) -> Result<Self, Box<dyn Error>> {
let mut cov = SMatrix::<f64, 9, 9>::zeros();
let mut mean = SVector::<f64, 9>::zeros();
let orbit_dual = OrbitGrad::from(template.orbit);
let mut b_plane = None;
for obj in &dispersions {
if obj.param.is_b_plane() {
b_plane = Some(
BPlane::from_dual(orbit_dual)
.context(AstroSnafu)
.map_err(Box::new)?,
);
break;
}
}
let num_orbital = dispersions
.iter()
.filter(|disp| disp.param.is_orbital())
.count();
if num_orbital > 0 {
let mut jac = DMatrix::from_element(num_orbital, 6, 0.0);
let mut covar = DMatrix::from_element(num_orbital, num_orbital, 0.0);
let mut means = DVector::from_element(num_orbital, 0.0);
let orbit_dual = OrbitGrad::from(template.orbit);
for (rno, disp) in dispersions
.iter()
.filter(|d| d.param.is_orbital())
.enumerate()
{
let partial = if let StateParameter::Element(oe) = disp.param {
orbit_dual.partial_for(oe).context(AstroPhysicsSnafu)?
} else if disp.param.is_b_plane() {
match disp.param {
StateParameter::BdotR() => b_plane.unwrap().b_r_km,
StateParameter::BdotT() => b_plane.unwrap().b_t_km,
StateParameter::BLTOF() => b_plane.unwrap().ltof_s,
_ => unreachable!(),
}
} else {
unreachable!()
};
for (cno, val) in [
partial.wrt_x(),
partial.wrt_y(),
partial.wrt_z(),
partial.wrt_vx(),
partial.wrt_vy(),
partial.wrt_vz(),
]
.iter()
.copied()
.enumerate()
{
jac[(rno, cno)] = val;
}
covar[(rno, rno)] = disp.std_dev.unwrap_or(0.0).powi(2);
means[rno] = disp.mean.unwrap_or(0.0);
}
let jac_inv = pseudo_inverse!(&jac)?;
let orbit_cov = &jac_inv * &covar * jac_inv.transpose();
let cartesian_mean = jac_inv * means;
for ii in 0..6 {
for jj in 0..6 {
cov[(ii, jj)] = orbit_cov[(ii, jj)];
}
mean[ii] = cartesian_mean[ii];
}
};
if dispersions.len() > num_orbital {
for disp in &dispersions {
if disp.param.is_orbital() {
continue;
} else {
match disp.param {
StateParameter::Cr() => {
cov[(7, 7)] = disp.mean.unwrap_or(0.0).powi(2);
}
StateParameter::Cd() => {
cov[(8, 8)] = disp.mean.unwrap_or(0.0).powi(2);
}
StateParameter::DryMass() | StateParameter::PropMass() => {
cov[(9, 9)] = disp.mean.unwrap_or(0.0).powi(2);
}
_ => return Err(Box::new(StateError::ReadOnly { param: disp.param })),
}
}
}
}
let svd = cov.svd_unordered(false, true);
if svd.v_t.is_none() {
return Err(Box::new(NyxError::CovarianceMatrixNotPsd));
}
let sqrt_s = svd.singular_values.map(|x| x.sqrt());
let mut sqrt_s_v_t = svd.v_t.unwrap().transpose();
for (i, mut col) in sqrt_s_v_t.column_iter_mut().enumerate() {
col *= sqrt_s[i];
}
Ok(Self {
template,
dispersions,
mean,
sqrt_s_v: sqrt_s_v_t,
std_norm_distr: Normal::new(0.0, 1.0).unwrap(),
})
}
pub fn zero_mean(
template: Spacecraft,
mut dispersions: Vec<StateDispersion>,
) -> Result<Self, Box<dyn Error>> {
for disp in &mut dispersions {
disp.mean = Some(0.0);
}
Self::new(template, dispersions)
}
pub fn from_spacecraft_cov(
template: Spacecraft,
cov: SMatrix<f64, 9, 9>,
mean: SVector<f64, 9>,
) -> Result<Self, Box<NyxError>> {
match cov.eigenvalues() {
None => return Err(Box::new(NyxError::CovarianceMatrixNotPsd)),
Some(evals) => {
for eigenval in &evals {
if *eigenval < 0.0 {
return Err(Box::new(NyxError::CovarianceMatrixNotPsd));
}
}
}
};
let svd = cov.svd_unordered(false, true);
if svd.v_t.is_none() {
return Err(Box::new(NyxError::CovarianceMatrixNotPsd));
}
let s = svd.singular_values;
let mut sqrt_s_v = svd.v_t.unwrap().transpose();
for (i, mut col) in sqrt_s_v.column_iter_mut().enumerate() {
col *= s[i].sqrt();
}
let dispersions = vec![
StateDispersion::builder()
.param(StateParameter::Element(OrbitalElement::X))
.std_dev(cov[(0, 0)])
.build(),
StateDispersion::builder()
.param(StateParameter::Element(OrbitalElement::Y))
.std_dev(cov[(1, 1)])
.build(),
StateDispersion::builder()
.param(StateParameter::Element(OrbitalElement::Z))
.std_dev(cov[(2, 2)])
.build(),
StateDispersion::builder()
.param(StateParameter::Element(OrbitalElement::VX))
.std_dev(cov[(3, 3)])
.build(),
StateDispersion::builder()
.param(StateParameter::Element(OrbitalElement::VY))
.std_dev(cov[(4, 4)])
.build(),
StateDispersion::builder()
.param(StateParameter::Element(OrbitalElement::VZ))
.std_dev(cov[(5, 5)])
.build(),
StateDispersion::builder()
.param(StateParameter::Cr())
.std_dev(cov[(6, 6)])
.build(),
StateDispersion::builder()
.param(StateParameter::Cd())
.std_dev(cov[(7, 7)])
.build(),
StateDispersion::builder()
.param(StateParameter::PropMass())
.std_dev(cov[(8, 8)])
.build(),
];
Ok(Self {
template,
dispersions,
mean,
sqrt_s_v,
std_norm_distr: Normal::new(0.0, 1.0).unwrap(),
})
}
}
impl Distribution<DispersedState<Spacecraft>> for MvnSpacecraft {
fn sample<R: rand::Rng + ?Sized>(&self, rng: &mut R) -> DispersedState<Spacecraft> {
let x_rng = SVector::<f64, 9>::from_fn(|_, _| self.std_norm_distr.sample(rng));
let x = self.sqrt_s_v * x_rng + self.mean;
let mut state = self.template;
for (coord, val) in x.iter().copied().enumerate() {
if coord < 3 {
state.orbit.radius_km[coord] += val;
} else if coord < 6 {
state.orbit.velocity_km_s[coord % 3] += val;
} else if coord == 6 {
state.srp.coeff_reflectivity += val;
} else if coord == 7 {
state.drag.coeff_drag += val;
} else if coord == 8 {
state.mass.prop_mass_kg += val;
}
}
let mut actual_dispersions = Vec::new();
for disp in &self.dispersions {
let delta = self.template.value(disp.param).unwrap() - state.value(disp.param).unwrap();
actual_dispersions.push((disp.param, delta));
}
DispersedState {
state,
actual_dispersions,
}
}
}
#[cfg(test)]
mod multivariate_ut {
use super::*;
use crate::GMAT_EARTH_GM;
use crate::Spacecraft;
use crate::time::Epoch;
use anise::constants::frames::EARTH_J2000;
use anise::prelude::Orbit;
use rand::RngExt;
use statrs;
use statrs::distribution::ContinuousCDF;
fn mahalanobis_distance<const T: usize>(
x: &SVector<f64, T>,
mu: &SVector<f64, T>,
cov_inv: &SMatrix<f64, T, T>,
) -> f64 {
let delta = x - mu;
(delta.transpose() * cov_inv * delta)[(0, 0)]
}
#[test]
fn mahalanobis_distance_test() {
let x = SVector::<f64, 2>::new(1.0, 2.0);
let mu = SVector::<f64, 2>::new(0.0, 0.0);
let cov = SMatrix::<f64, 2, 2>::from_diagonal(&SVector::<f64, 2>::new(2.0, 3.0));
let cov_inv = cov.pseudo_inverse(1e-12).unwrap();
let md = mahalanobis_distance(&x, &mu, &cov_inv);
assert!((md - 1.8333333333333333).abs() < 1e-9);
}
#[test]
fn test_mvn_generator() {
let mut rng = rand::rng();
let cov = SMatrix::<f64, 6, 6>::from_fn(|_, _| rng.random());
let cov = cov * cov.transpose();
let mut cov_resized = SMatrix::<f64, 9, 9>::zeros();
cov_resized.fixed_view_mut::<6, 6>(0, 0).copy_from(&cov);
let eme2k = EARTH_J2000.with_mu_km3_s2(GMAT_EARTH_GM);
let dt = Epoch::from_gregorian_utc_at_midnight(2021, 1, 31);
let state = Spacecraft::builder()
.orbit(Orbit::keplerian(
8_191.93, 1e-6, 12.85, 306.614, 314.19, 99.887_7, dt, eme2k,
))
.build();
let mvn = MvnSpacecraft::from_spacecraft_cov(state, cov_resized, SVector::zeros()).unwrap();
let n = 1000;
let samples = mvn.sample_iter(&mut rng).take(n);
let cov_inv = cov_resized.pseudo_inverse(1e-12).unwrap();
let md: Vec<f64> = samples
.map(|sample| {
let mut v = SVector::<f64, 9>::zeros();
for i in 0..6 {
v[i] = sample.state.orbit.to_cartesian_pos_vel()[i]
- state.orbit.to_cartesian_pos_vel()[i];
}
mahalanobis_distance(&v, &SVector::zeros(), &cov_inv)
})
.collect();
let mut md = md;
md.sort_by(|a, b| a.partial_cmp(b).unwrap());
let p95_md = md[(0.95 * n as f64) as usize];
let chi_squared = statrs::distribution::ChiSquared::new(6.0).unwrap();
let p95_chi_squared = chi_squared.inverse_cdf(0.95);
assert!((p95_md - p95_chi_squared).abs() / p95_chi_squared < 0.2);
}
#[test]
fn disperse_r_mag() {
use anise::constants::frames::EARTH_J2000;
use anise::prelude::Orbit;
use crate::time::Epoch;
use rand_pcg::Pcg64Mcg;
let seed = 0;
let eme2k = EARTH_J2000.with_mu_km3_s2(GMAT_EARTH_GM);
let dt = Epoch::from_gregorian_utc_at_midnight(2021, 1, 31);
let state = Spacecraft::builder()
.orbit(Orbit::keplerian(
8_191.93, 1e-6, 12.85, 306.614, 314.19, 99.887_7, dt, eme2k,
))
.build();
let std_dev = 1.0;
let generator = MvnSpacecraft::new(
state,
vec![
StateDispersion::builder()
.param(StateParameter::Element(OrbitalElement::Rmag))
.std_dev(std_dev)
.build(),
],
)
.unwrap();
let rng = Pcg64Mcg::new(seed);
let init_rmag = state.orbit.rmag_km();
let cnt_too_far: u16 = generator
.sample_iter(rng)
.take(1000)
.map(|dispersed_state| {
if (init_rmag - dispersed_state.state.orbit.rmag_km()).abs() >= 3.0 * std_dev {
1
} else {
0
}
})
.sum::<u16>();
assert_eq!(
cnt_too_far,
6, "Should have about 3% of samples being more than 3 sigma away, got {cnt_too_far}"
);
}
#[test]
fn disperse_full_cartesian() {
use anise::constants::frames::EARTH_J2000;
use anise::prelude::Orbit;
use crate::GMAT_EARTH_GM;
use crate::Spacecraft;
use crate::time::Epoch;
use rand_pcg::Pcg64Mcg;
let eme2k = EARTH_J2000.with_mu_km3_s2(GMAT_EARTH_GM);
let dt = Epoch::from_gregorian_utc_at_midnight(2021, 1, 31);
let state = Orbit::keplerian(8_191.93, 1e-6, 12.85, 306.614, 314.19, 99.887_7, dt, eme2k);
let std_dev = [10.0, 10.0, 10.0, 0.2, 0.2, 0.2, 0.0, 0.0, 0.0];
let generator = MvnSpacecraft::new(
Spacecraft {
orbit: state,
..Default::default()
},
vec![
StateDispersion::builder()
.param(StateParameter::Element(OrbitalElement::X))
.std_dev(10.0)
.build(),
StateDispersion::builder()
.param(StateParameter::Element(OrbitalElement::Y))
.std_dev(10.0)
.build(),
StateDispersion::builder()
.param(StateParameter::Element(OrbitalElement::Z))
.std_dev(10.0)
.build(),
StateDispersion::builder()
.param(StateParameter::Element(OrbitalElement::VX))
.std_dev(0.2)
.build(),
StateDispersion::builder()
.param(StateParameter::Element(OrbitalElement::VY))
.std_dev(0.2)
.build(),
StateDispersion::builder()
.param(StateParameter::Element(OrbitalElement::VZ))
.std_dev(0.2)
.build(),
],
)
.unwrap();
let seed = 0;
let rng = Pcg64Mcg::new(seed);
let cnt_too_far: u16 = generator
.sample_iter(rng)
.take(1000)
.map(|dispersed_state| {
let mut cnt = 0;
for (idx, val_std_dev) in std_dev.iter().take(6).enumerate() {
let cur_val = dispersed_state.state.to_vector()[idx];
let nom_val = state.to_cartesian_pos_vel()[idx];
if (cur_val - nom_val).abs() > *val_std_dev {
cnt += 1;
}
}
cnt
})
.sum::<u16>();
assert_eq!(
cnt_too_far / 6,
312,
"Should have about 3% of samples being more than 3 sigma away, got {cnt_too_far}"
);
}
#[test]
fn disperse_raan_only() {
use anise::constants::frames::EARTH_J2000;
use anise::prelude::Orbit;
use crate::time::Epoch;
use rand_pcg::Pcg64Mcg;
let eme2k = EARTH_J2000.with_mu_km3_s2(GMAT_EARTH_GM);
let dt = Epoch::from_gregorian_utc_at_midnight(2021, 1, 31);
let state = Spacecraft::builder()
.orbit(Orbit::keplerian(
8_100.0, 1e-6, 12.85, 356.614, 14.19, 199.887_7, dt, eme2k,
))
.build();
let angle_sigma_deg = 0.2;
assert!(StateParameter::Element(OrbitalElement::RAAN).is_orbital());
let generator = MvnSpacecraft::new(
state,
vec![StateDispersion::zero_mean(
StateParameter::Element(OrbitalElement::RAAN),
angle_sigma_deg,
)],
)
.unwrap();
let seed = 0;
let rng = Pcg64Mcg::new(seed);
let n = 1000;
let samples = generator.sample_iter(rng).take(n);
let cov = SMatrix::<f64, 1, 1>::from_diagonal(&SVector::<f64, 1>::from_vec(vec![
angle_sigma_deg.powi(2),
]));
let cov_inv = cov.pseudo_inverse(1e-12).unwrap();
let md: Vec<f64> = samples
.map(|dispersed_state| {
for param in [
StateParameter::Element(OrbitalElement::SemiMajorAxis),
StateParameter::Element(OrbitalElement::Inclination),
] {
let orig = state.value(param).unwrap();
let new = dispersed_state.state.value(param).unwrap();
let prct_change = 100.0 * (orig - new).abs() / orig;
assert!(
prct_change < 5.0,
"{param} changed by {prct_change:.3} % ({orig:.3e} -> {new:.3e})"
);
}
let delta =
SVector::<f64, 1>::from_vec(vec![dispersed_state.actual_dispersions[0].1]);
mahalanobis_distance(&delta, &SVector::zeros(), &cov_inv)
})
.collect();
let mut md = md;
md.sort_by(|a, b| a.partial_cmp(b).unwrap());
let p95_md = md[(0.95 * n as f64) as usize];
let chi_squared = statrs::distribution::ChiSquared::new(1.0).unwrap();
let p95_chi_squared = chi_squared.inverse_cdf(0.95);
assert!((p95_md - p95_chi_squared).abs() / p95_chi_squared < 0.2);
}
#[test]
fn disperse_keplerian() {
use anise::constants::frames::EARTH_J2000;
use anise::prelude::Orbit;
use crate::time::Epoch;
use rand_pcg::Pcg64Mcg;
let eme2k = EARTH_J2000.with_mu_km3_s2(GMAT_EARTH_GM);
let dt = Epoch::from_gregorian_utc_at_midnight(2021, 1, 31);
let state = Spacecraft::builder()
.orbit(Orbit::keplerian(
8_100.0, 1e-6, 12.85, 356.614, 14.19, 199.887_7, dt, eme2k,
))
.build();
let sma_sigma_km = 10.0;
let inc_sigma_deg = 0.15;
let angle_sigma_deg = 0.02;
let generator = MvnSpacecraft::new(
state,
vec![
StateDispersion::zero_mean(
StateParameter::Element(OrbitalElement::SemiMajorAxis),
sma_sigma_km,
),
StateDispersion::zero_mean(
StateParameter::Element(OrbitalElement::Inclination),
inc_sigma_deg,
),
StateDispersion::zero_mean(
StateParameter::Element(OrbitalElement::RAAN),
angle_sigma_deg,
),
StateDispersion::zero_mean(
StateParameter::Element(OrbitalElement::AoP),
angle_sigma_deg,
),
],
)
.unwrap();
let expected_cart_cov = generator.sqrt_s_v * generator.sqrt_s_v.transpose();
let seed = 0;
let rng = Pcg64Mcg::new(seed);
let n = 2000; let samples: Vec<SVector<f64, 6>> = generator
.sample_iter(rng)
.take(n)
.map(|s| s.state.orbit.to_cartesian_pos_vel())
.collect();
let nominal_cart = state.orbit.to_cartesian_pos_vel();
let mut mean_cart = SVector::<f64, 6>::zeros();
for sample in &samples {
mean_cart += sample;
}
mean_cart /= n as f64;
let mean_diff = mean_cart - nominal_cart;
assert!(mean_diff.norm() < 1.0);
let mut sample_cov = SMatrix::<f64, 6, 6>::zeros();
for sample in &samples {
let disp_vec = sample - mean_cart;
sample_cov += disp_vec * disp_vec.transpose();
}
sample_cov /= (n - 1) as f64;
let expected_cov_6x6 = expected_cart_cov.fixed_view::<6, 6>(0, 0);
let diff = sample_cov - expected_cov_6x6;
assert!(diff.norm() / expected_cov_6x6.norm() < 0.2);
}
}