use crate::linalg::allocator::Allocator;
use crate::linalg::{DefaultAllocator, DimMin, DimName, DimSub};
use crate::md::trajectory::{Interpolatable, Traj};
pub use crate::od::estimate::*;
pub use crate::od::*;
use log::warn;
use msr::sensitivity::TrackerSensitivity;
use nalgebra::Const;
use snafu::ResultExt;
use statrs::distribution::{ContinuousCDF, Normal};
use std::ops::Add;
use super::ODSolution;
impl<StateType, EstType, MsrSize, Trk> ODSolution<StateType, EstType, MsrSize, Trk>
where
StateType: Interpolatable + Add<OVector<f64, <StateType as State>::Size>, Output = StateType>,
EstType: Estimate<StateType>,
MsrSize: DimName,
Trk: TrackerSensitivity<StateType, StateType>,
<DefaultAllocator as Allocator<<StateType as State>::VecLength>>::Buffer<f64>: Send,
DefaultAllocator: Allocator<<StateType as State>::Size>
+ Allocator<<StateType as State>::VecLength>
+ Allocator<MsrSize>
+ Allocator<MsrSize, <StateType as State>::Size>
+ Allocator<MsrSize, MsrSize>
+ Allocator<<StateType as State>::Size, <StateType as State>::Size>
+ Allocator<<StateType as State>::Size, MsrSize>,
{
pub fn rms_prefit_residuals(&self) -> f64 {
let mut sum = 0.0;
for residual in self.residuals.iter().flatten() {
sum += residual.prefit.dot(&residual.prefit);
}
(sum / (self.residuals.len() as f64)).sqrt()
}
pub fn rms_postfit_residuals(&self) -> f64 {
let mut sum = 0.0;
for residual in self.residuals.iter().flatten() {
sum += residual.postfit.dot(&residual.postfit);
}
(sum / (self.residuals.len() as f64)).sqrt()
}
pub fn rms_residual_ratios(&self) -> f64 {
let mut sum = 0.0;
for residual in self.residuals.iter().flatten() {
sum += residual.ratio.powi(2);
}
(sum / (self.residuals.len() as f64)).sqrt()
}
pub fn residual_ratio_within_threshold(&self, threshold: f64) -> Result<f64, ODError> {
let mut total = 0;
let mut count_within = 0;
for residual in self.residuals.iter().flatten() {
total += 1;
if residual.ratio.abs() <= threshold {
count_within += 1;
}
}
if total > 0 {
Ok(count_within as f64 / total as f64)
} else {
Err(ODError::ODNoResiduals {
action: "percentage of residuals within threshold",
})
}
}
pub fn ks_test_normality(&self) -> Result<f64, ODError> {
let mut residual_ratios = self
.accepted_residuals()
.iter()
.flat_map(|resid| resid.whitened_resid.into_iter().copied())
.collect::<Vec<f64>>();
if residual_ratios.is_empty() {
return Err(ODError::ODNoResiduals {
action: "percentage of residuals within threshold",
});
}
residual_ratios.sort_by(|a, b| a.partial_cmp(b).unwrap());
let n = residual_ratios.len() as f64;
let mean = residual_ratios.iter().sum::<f64>() / n;
let variance = residual_ratios
.iter()
.map(|v| (v - mean).powi(2))
.sum::<f64>()
/ n;
let std_dev = variance.sqrt();
let normal_dist = Normal::new(mean, std_dev).unwrap();
let mut d_stat = 0.0;
for (i, &value) in residual_ratios.iter().enumerate() {
let empirical_cdf = (i + 1) as f64 / n;
let model_cdf = normal_dist.cdf(value);
let diff = (empirical_cdf - model_cdf).abs();
if diff > d_stat {
d_stat = diff;
}
}
Ok(d_stat)
}
pub fn is_normal(&self, alpha: Option<f64>) -> Result<bool, ODError> {
let n = self.residuals.iter().flatten().count();
if n == 0 {
return Err(ODError::ODNoResiduals {
action: "evaluating residual normality",
});
} else if n < 35 {
warn!("KS normality test unreliable for n={n} < 35; skipping");
}
let ks_stat = self.ks_test_normality()?;
let alpha = alpha.unwrap_or(0.05);
let c_alpha = (-(alpha * 0.5).ln() * 0.5).sqrt();
let d_critical = c_alpha / (n as f64).sqrt();
Ok(ks_stat <= d_critical)
}
pub fn is_nis_consistent(&self, alpha: Option<f64>) -> Result<bool, ODError> {
let n = self.accepted_residuals().len();
if n == 0 {
return Err(ODError::ODNoResiduals {
action: "evaluating NIS consistency",
});
}
let nis_sum: f64 = self.accepted_residuals().iter().map(|r| r.nis()).sum();
let k = (n * MsrSize::DIM) as f64;
if k < 35.0 {
warn!("NIS consistency test lacks statistical power for n*MsrSize={k} < 35");
}
let alpha = alpha.unwrap_or(0.05);
let z_critical = Normal::new(0.0, 1.0)
.unwrap()
.inverse_cdf(1.0 - alpha / 2.0);
let factor = 2.0 / (9.0 * k);
let lower_bound = k * (1.0 - factor - z_critical * factor.sqrt()).powi(3);
let upper_bound = k * (1.0 - factor + z_critical * factor.sqrt()).powi(3);
if nis_sum > upper_bound {
warn!(
"NIS consistency test failed high: NIS sum {nis_sum:.6} > upper bound {upper_bound:.6}. Innovations are larger than expected."
);
warn!(
"Filter may be overconfident: P, R, or Q may be too small, or the dynamics/measurement model may be biased."
);
Ok(false)
} else if nis_sum < lower_bound {
warn!(
"NIS consistency test failed low: NIS sum {nis_sum:.6} < lower bound {lower_bound:.6}. Innovations are smaller than expected."
);
warn!("Filter may be underconfident: P, R, or Q may be too large.");
Ok(false)
} else {
Ok(true)
}
}
pub fn is_nees_consistent(
&self,
truth_traj: &Traj<StateType>,
alpha: Option<f64>,
) -> Result<bool, ODError>
where
StateType::Size: DimMin<StateType::Size>,
<StateType::Size as DimMin<StateType::Size>>::Output: DimSub<Const<1>>,
<<StateType as State>::Size as DimMin<<StateType as State>::Size>>::Output:
DimSub<Const<1>>,
DefaultAllocator: Allocator<StateType::Size, Const<1>>
+ Allocator<Const<1>, <StateType as State>::Size>
+ Allocator<<StateType::Size as DimMin<StateType::Size>>::Output, StateType::Size>
+ Allocator<StateType::Size, <StateType::Size as DimMin<StateType::Size>>::Output>
+ Allocator<<StateType::Size as DimMin<StateType::Size>>::Output>
+ Allocator<
<<StateType::Size as DimMin<StateType::Size>>::Output as DimSub<Const<1>>>::Output,
>,
{
let n = self.estimates.len();
if n == 0 {
return Err(ODError::ODNoResiduals {
action: "evaluating NEES consistency",
});
}
let mut nees_sum = 0.0;
let mut state_dim_f64 = 0.0_f64;
for est in &self.estimates {
let epoch = est.epoch();
let truth_state = truth_traj.at(epoch).context(ODTrajSnafu {
details: "interpolating truth during NEES test",
})?;
let x_est = est.state().to_state_vector();
let x_true = truth_state.to_state_vector();
let error = x_est - x_true;
let p_mat = est.covar();
let svd = p_mat.svd(true, true);
let epsilon = 1e-12;
let p_inv = svd
.clone()
.pseudo_inverse(epsilon)
.map_err(|_| ODError::SingularInformationMatrix)?;
let active_dim = svd.rank(epsilon) as f64;
state_dim_f64 = state_dim_f64.max(active_dim);
let nees_matrix = error.transpose() * p_inv * &error;
nees_sum += nees_matrix[(0, 0)];
if state_dim_f64 == 0.0 {
state_dim_f64 = error.nrows() as f64;
}
}
let k = (n as f64) * state_dim_f64;
if k < 35.0 {
warn!("NEES consistency test lacks statistical power for n*StateDim={k} < 35");
}
let alpha = alpha.unwrap_or(0.05);
let z_critical = Normal::new(0.0, 1.0)
.unwrap()
.inverse_cdf(1.0 - alpha / 2.0);
let factor = 2.0 / (9.0 * k);
let lower_bound = k * (1.0 - factor - z_critical * factor.sqrt()).powi(3);
let upper_bound = k * (1.0 - factor + z_critical * factor.sqrt()).powi(3);
if nees_sum > upper_bound {
warn!(
"NEES consistency test failed high: NEES sum {nees_sum:.6} > upper bound {upper_bound:.6}. Estimation errors are larger than the covariance suggests."
);
warn!(
"Filter is overconfident: P is too small. Process noise Q may be too low, or there are unmodeled dynamic biases."
);
Ok(false)
} else if nees_sum < lower_bound {
warn!(
"NEES consistency test failed low: NEES sum {nees_sum:.6} < lower bound {lower_bound:.6}. Estimation errors are smaller than expected."
);
warn!(
"Filter is underconfident: P is too large. Process noise Q may be too high, or measurement noise R is overestimated."
);
Ok(false)
} else {
Ok(true)
}
}
}