use nalgebra::{Matrix2, Vector2};
use crate::differential_orbit_correction::{
least_square::{ObservationEquation, OrbitalUncertainty},
obs_fit_data::{ObsFitData, ObsSelection},
};
#[derive(Debug, Clone)]
pub struct OutlierRejectionConfig {
pub chi_squared_rejection_threshold: f64,
pub chi_squared_recovery_threshold: f64,
}
impl Default for OutlierRejectionConfig {
fn default() -> Self {
Self {
chi_squared_rejection_threshold: 25.0,
chi_squared_recovery_threshold: 9.0,
}
}
}
pub fn update_observation_selection(
obs_fit_data: &[ObsFitData],
equations: &[ObservationEquation],
uncertainty: &OrbitalUncertainty,
config: &OutlierRejectionConfig,
) -> (Vec<ObsFitData>, usize) {
assert_eq!(
obs_fit_data.len(),
equations.len(),
"obs_fit_data and equations must have the same length"
);
let covariance = &uncertainty.covariance;
obs_fit_data.iter().zip(equations.iter()).fold(
(Vec::with_capacity(obs_fit_data.len()), 0_usize),
|(mut acc, changes), (fit_data, eq)| {
if fit_data.selection == ObsSelection::ForcedOut {
acc.push(fit_data.clone());
return (acc, changes);
}
let var_ra = fit_data.sigma_ra * fit_data.sigma_ra;
let var_dec = fit_data.sigma_dec * fit_data.sigma_dec;
let cov_cross = -fit_data.sigma_ra * fit_data.sigma_dec * eq.weight_cross
/ (eq.weight_ra * eq.weight_dec);
let g_alpha = &eq.d_ra_d_elem;
let g_delta = &eq.d_dec_d_elem;
let gamma_g_alpha = covariance * g_alpha; let gamma_g_delta = covariance * g_delta;
let proj_aa = g_alpha.dot(&gamma_g_alpha);
let proj_dd = g_delta.dot(&gamma_g_delta);
let proj_ad = g_alpha.dot(&gamma_g_delta);
let projected_var = Matrix2::new(
var_ra - proj_aa,
cov_cross - proj_ad,
cov_cross - proj_ad,
var_dec - proj_dd,
);
let det = projected_var[(0, 0)] * projected_var[(1, 1)]
- projected_var[(0, 1)] * projected_var[(0, 1)];
let scale = projected_var[(0, 0)].abs().max(projected_var[(1, 1)].abs());
let singular_threshold = f64::EPSILON * scale * scale;
if det.abs() < singular_threshold || scale == 0.0 {
acc.push(fit_data.clone());
return (acc, changes);
}
let inv_projected_var = Matrix2::new(
projected_var[(1, 1)] / det,
-projected_var[(0, 1)] / det,
-projected_var[(1, 0)] / det,
projected_var[(0, 0)] / det,
);
let residual = Vector2::new(fit_data.residual_ra, fit_data.residual_dec);
let chi_squared = residual.dot(&(inv_projected_var * residual));
let new_selection = match fit_data.selection {
ObsSelection::Active if chi_squared > config.chi_squared_rejection_threshold => {
Some(ObsSelection::Rejected)
}
ObsSelection::Rejected if chi_squared <= config.chi_squared_recovery_threshold => {
Some(ObsSelection::Active)
}
_ => None,
};
let (updated_fit_data, delta) = match new_selection {
Some(new_sel) => {
let mut updated = fit_data.clone();
updated.selection = new_sel;
(updated, 1)
}
None => (fit_data.clone(), 0),
};
acc.push(updated_fit_data);
(acc, changes + delta)
},
)
}
#[cfg(test)]
mod outlier_rejection_tests {
use super::*;
use crate::differential_orbit_correction::least_square::ObservationEquation;
use nalgebra::{Matrix6, Vector6};
fn identity_uncertainty() -> OrbitalUncertainty {
OrbitalUncertainty {
normal_matrix: Matrix6::identity(),
covariance: Matrix6::identity(),
inversion_succeeded: true,
}
}
fn zero_uncertainty() -> OrbitalUncertainty {
OrbitalUncertainty {
normal_matrix: Matrix6::zeros(),
covariance: Matrix6::zeros(),
inversion_succeeded: false,
}
}
fn make_eq(
g_ra: Vector6<f64>,
g_dec: Vector6<f64>,
res_ra: f64,
res_dec: f64,
sigma: f64,
active: bool,
) -> ObservationEquation {
ObservationEquation::uncorrelated(g_ra, g_dec, res_ra, res_dec, sigma, sigma, active)
}
#[test]
fn test_zero_residual_never_rejected() {
let sigma = 1e-5;
let eq = make_eq(Vector6::zeros(), Vector6::zeros(), 0.0, 0.0, sigma, true);
let uncertainty = zero_uncertainty();
let mut fit_data = ObsFitData::new(sigma, sigma);
fit_data.residual_ra = 0.0;
fit_data.residual_dec = 0.0;
let (updated, changes) = update_observation_selection(
&[fit_data],
&[eq],
&uncertainty,
&OutlierRejectionConfig::default(),
);
assert_eq!(changes, 0);
assert_eq!(updated[0].selection, ObsSelection::Active);
}
#[test]
fn test_large_residual_is_rejected() {
let sigma = 1e-5;
let eq = make_eq(
Vector6::zeros(),
Vector6::zeros(),
100.0 * sigma,
100.0 * sigma,
sigma,
true,
);
let uncertainty = zero_uncertainty();
let mut fit_data = ObsFitData::new(sigma, sigma);
fit_data.residual_ra = 100.0 * sigma;
fit_data.residual_dec = 100.0 * sigma;
let (updated, changes) = update_observation_selection(
&[fit_data],
&[eq],
&uncertainty,
&OutlierRejectionConfig::default(),
);
assert_eq!(changes, 1);
assert_eq!(updated[0].selection, ObsSelection::Rejected);
}
#[test]
fn test_small_residual_recovers_rejected_observation() {
let sigma = 1e-5;
let eq = make_eq(
Vector6::zeros(),
Vector6::zeros(),
0.5 * sigma, 0.5 * sigma,
sigma,
false, );
let uncertainty = zero_uncertainty();
let mut fit_data = ObsFitData::new(sigma, sigma);
fit_data.residual_ra = 0.5 * sigma;
fit_data.residual_dec = 0.5 * sigma;
fit_data.selection = ObsSelection::Rejected;
let (updated, changes) = update_observation_selection(
&[fit_data],
&[eq],
&uncertainty,
&OutlierRejectionConfig::default(),
);
assert_eq!(changes, 1);
assert_eq!(updated[0].selection, ObsSelection::Active);
}
#[test]
fn test_forced_out_is_never_changed() {
let sigma = 1e-5;
let eq = make_eq(Vector6::zeros(), Vector6::zeros(), 0.0, 0.0, sigma, false);
let uncertainty = zero_uncertainty();
let mut fit_data = ObsFitData::new(sigma, sigma);
fit_data.selection = ObsSelection::ForcedOut;
let (updated, changes) = update_observation_selection(
&[fit_data],
&[eq],
&uncertainty,
&OutlierRejectionConfig::default(),
);
assert_eq!(changes, 0);
assert_eq!(updated[0].selection, ObsSelection::ForcedOut);
}
#[test]
fn test_singular_projected_variance_skipped() {
let sigma = 1e-5;
let mut g_ra = Vector6::zeros();
g_ra[0] = 1.0;
let mut g_dec = Vector6::zeros();
g_dec[1] = 1.0;
let eq = make_eq(g_ra, g_dec, sigma, sigma, sigma, true);
let uncertainty = identity_uncertainty();
let mut fit_data = ObsFitData::new(sigma, sigma);
fit_data.residual_ra = sigma;
fit_data.residual_dec = sigma;
let (updated, changes) = update_observation_selection(
&[fit_data],
&[eq],
&uncertainty,
&OutlierRejectionConfig::default(),
);
assert_eq!(changes, 0);
assert_eq!(updated[0].selection, ObsSelection::Active);
}
#[test]
fn test_no_change_when_within_thresholds() {
let sigma = 1e-5;
let eq = make_eq(
Vector6::zeros(),
Vector6::zeros(),
2.0 * sigma,
2.0 * sigma,
sigma,
true,
);
let uncertainty = zero_uncertainty();
let mut fit_data = ObsFitData::new(sigma, sigma);
fit_data.residual_ra = 2.0 * sigma;
fit_data.residual_dec = 2.0 * sigma;
let (updated, changes) = update_observation_selection(
&[fit_data],
&[eq],
&uncertainty,
&OutlierRejectionConfig::default(),
);
assert_eq!(changes, 0);
assert_eq!(updated[0].selection, ObsSelection::Active);
}
#[test]
fn test_custom_thresholds() {
let sigma = 1e-5;
let eq = make_eq(
Vector6::zeros(),
Vector6::zeros(),
3.0 * sigma,
3.0 * sigma,
sigma,
true,
);
let uncertainty = zero_uncertainty();
let mut fit_data = ObsFitData::new(sigma, sigma);
fit_data.residual_ra = 3.0 * sigma;
fit_data.residual_dec = 3.0 * sigma;
let config = OutlierRejectionConfig {
chi_squared_rejection_threshold: 16.0,
chi_squared_recovery_threshold: 4.0,
};
let (updated, changes) =
update_observation_selection(&[fit_data], &[eq], &uncertainty, &config);
assert_eq!(changes, 1);
assert_eq!(updated[0].selection, ObsSelection::Rejected);
}
#[test]
fn test_multiple_changes_counted_correctly() {
let sigma = 1e-5;
let uncertainty = zero_uncertainty();
let eq0 = make_eq(
Vector6::zeros(),
Vector6::zeros(),
sigma,
sigma,
sigma,
true,
);
let mut fd0 = ObsFitData::new(sigma, sigma);
fd0.residual_ra = sigma;
fd0.residual_dec = sigma;
let eq1 = make_eq(
Vector6::zeros(),
Vector6::zeros(),
10.0 * sigma,
10.0 * sigma,
sigma,
true,
);
let mut fd1 = ObsFitData::new(sigma, sigma);
fd1.residual_ra = 10.0 * sigma;
fd1.residual_dec = 10.0 * sigma;
let eq2 = make_eq(
Vector6::zeros(),
Vector6::zeros(),
0.1 * sigma,
0.1 * sigma,
sigma,
false,
);
let mut fd2 = ObsFitData::new(sigma, sigma);
fd2.residual_ra = 0.1 * sigma;
fd2.residual_dec = 0.1 * sigma;
fd2.selection = ObsSelection::Rejected;
let (updated, changes) = update_observation_selection(
&[fd0, fd1, fd2],
&[eq0, eq1, eq2],
&uncertainty,
&OutlierRejectionConfig::default(),
);
assert_eq!(changes, 2);
assert_eq!(updated[0].selection, ObsSelection::Active);
assert_eq!(updated[1].selection, ObsSelection::Rejected);
assert_eq!(updated[2].selection, ObsSelection::Active);
}
}