use crate::core::scalar::ControlScalar;
#[derive(Debug, Clone, Copy)]
pub struct CauchyEstimator<S: ControlScalar> {
x: S,
gamma: S,
pub process_noise: S,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CauchyError {
NonPositiveScale,
ZeroMeasurementCoefficient,
NumericalFailure,
}
impl core::fmt::Display for CauchyError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
CauchyError::NonPositiveScale => {
write!(f, "CauchyEstimator: scale parameter must be > 0")
}
CauchyError::ZeroMeasurementCoefficient => {
write!(
f,
"CauchyEstimator: measurement coefficient h must be non-zero"
)
}
CauchyError::NumericalFailure => {
write!(
f,
"CauchyEstimator: numerical failure in product rule denominator"
)
}
}
}
}
impl<S: ControlScalar> CauchyEstimator<S> {
pub fn new(x0: S, gamma0: S, process_noise: S) -> Result<Self, CauchyError> {
if gamma0 <= S::ZERO {
return Err(CauchyError::NonPositiveScale);
}
if process_noise <= S::ZERO {
return Err(CauchyError::NonPositiveScale);
}
Ok(Self {
x: x0,
gamma: gamma0,
process_noise,
})
}
pub fn predict(&mut self, a: S, b: S) -> Result<(), CauchyError> {
self.x = a * self.x + b;
let abs_a = num_traits::Float::abs(a);
self.gamma = abs_a * self.gamma + self.process_noise;
if self.gamma <= S::ZERO {
return Err(CauchyError::NonPositiveScale);
}
Ok(())
}
pub fn update(&mut self, h: S, z: S, meas_scale: S) -> Result<(), CauchyError> {
if h == S::ZERO {
return Err(CauchyError::ZeroMeasurementCoefficient);
}
if meas_scale <= S::ZERO {
return Err(CauchyError::NonPositiveScale);
}
let abs_h = num_traits::Float::abs(h);
let mu2 = z / h;
let gamma2 = meas_scale / abs_h;
let mu1 = self.x;
let gamma1 = self.gamma;
let g1sq = gamma1 * gamma1;
let g2sq = gamma2 * gamma2;
let denom_sq = g1sq + g2sq;
if denom_sq <= S::ZERO {
return Err(CauchyError::NumericalFailure);
}
let mu_post = (mu1 * g2sq + mu2 * g1sq) / denom_sq;
let denom = num_traits::Float::sqrt(denom_sq);
if denom <= S::ZERO {
return Err(CauchyError::NumericalFailure);
}
let gamma_post = (gamma1 * gamma2) / denom;
self.x = mu_post;
self.gamma = gamma_post;
Ok(())
}
pub fn state(&self) -> S {
self.x
}
pub fn scale(&self) -> S {
self.gamma
}
pub fn reset(&mut self, x0: S, gamma0: S) -> Result<(), CauchyError> {
if gamma0 <= S::ZERO {
return Err(CauchyError::NonPositiveScale);
}
self.x = x0;
self.gamma = gamma0;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn construction_requires_positive_scale() {
assert!(CauchyEstimator::new(0.0_f64, 0.0, 1.0).is_err());
assert!(CauchyEstimator::new(0.0_f64, -1.0, 1.0).is_err());
assert!(CauchyEstimator::new(0.0_f64, 1.0, 0.0).is_err());
assert!(CauchyEstimator::new(0.0_f64, 1.0, -0.5).is_err());
assert!(CauchyEstimator::new(0.0_f64, 1.0, 1.0).is_ok());
}
#[test]
fn predict_propagates_correctly() {
let mut est = CauchyEstimator::new(2.0_f64, 1.0, 0.1).expect("valid");
est.predict(0.5, 1.0).expect("predict");
assert!((est.state() - 2.0).abs() < 1e-12, "state = {}", est.state());
assert!((est.scale() - 0.6).abs() < 1e-12, "scale = {}", est.scale());
}
#[test]
fn update_zero_h_rejected() {
let mut est = CauchyEstimator::new(0.0_f64, 1.0, 0.1).expect("valid");
assert!(est.update(0.0, 1.0, 1.0).is_err());
}
#[test]
fn update_negative_meas_scale_rejected() {
let mut est = CauchyEstimator::new(0.0_f64, 1.0, 0.1).expect("valid");
assert!(est.update(1.0, 1.0, -1.0).is_err());
}
#[test]
fn scale_decreases_after_update() {
let gamma0 = 5.0_f64;
let mut est = CauchyEstimator::new(0.0_f64, gamma0, 0.01).expect("valid");
est.update(1.0, 0.0, 2.0).expect("update");
assert!(
est.scale() < gamma0,
"Scale should decrease after update: {} ≥ {}",
est.scale(),
gamma0
);
}
#[test]
fn convergence_to_true_state() {
let true_x = 7.5_f64;
let mut est = CauchyEstimator::new(0.0_f64, 10.0, 1e-3).expect("valid");
for _ in 0..100 {
est.predict(1.0, 0.0).expect("predict");
est.update(1.0, true_x, 0.1).expect("update");
}
assert!(
(est.state() - true_x).abs() < 0.5,
"Expected convergence to {true_x}, got {}",
est.state()
);
}
#[test]
fn compare_cauchy_vs_gaussian_on_clean_data() {
let true_x = 3.0_f64;
let mut cauchy = CauchyEstimator::new(0.0_f64, 20.0, 1e-4).expect("valid");
let mut kf_x = 0.0_f64;
let mut kf_p = 400.0_f64; let kf_q = 1e-4_f64;
let kf_r = 0.25_f64;
for k in 0..200 {
let sign = if k % 2 == 0 { 1.0_f64 } else { -1.0_f64 };
let z = true_x + sign * 0.3;
cauchy.predict(1.0, 0.0).expect("predict");
cauchy.update(1.0, z, 0.5).expect("update");
kf_p += kf_q;
let k_gain = kf_p / (kf_p + kf_r);
kf_x += k_gain * (z - kf_x);
kf_p *= 1.0 - k_gain;
}
let cauchy_err = (cauchy.state() - true_x).abs();
let kf_err = (kf_x - true_x).abs();
assert!(
cauchy_err < 0.5,
"Cauchy estimator error too large: {cauchy_err}"
);
assert!(kf_err < 0.5, "KF error too large: {kf_err}");
let _ = (cauchy_err, kf_err);
}
#[test]
fn robustness_to_large_outlier() {
let true_x = 1.0_f64;
let mut est = CauchyEstimator::new(true_x, 0.1, 1e-4).expect("valid");
est.predict(1.0, 0.0).expect("predict");
est.update(1.0, true_x, 0.5).expect("update");
let pre_outlier = est.state();
let pre_scale = est.scale();
let outlier_z = 1e6_f64;
est.predict(1.0, 0.0).expect("predict");
est.update(1.0, outlier_z, 0.5).expect("update");
let post_outlier = est.state();
assert!(
post_outlier.is_finite(),
"State became non-finite after outlier"
);
let gamma1 = pre_scale + 1e-4; let gamma2 = 0.5_f64; let mu2 = outlier_z; let g1sq = gamma1 * gamma1;
let g2sq = gamma2 * gamma2;
let expected_post = (pre_outlier * g2sq + mu2 * g1sq) / (g1sq + g2sq);
assert!(
(post_outlier - expected_post).abs() < 1.0,
"Post-outlier state {post_outlier} does not match expected {expected_post}"
);
}
#[test]
fn reset_works() {
let mut est = CauchyEstimator::new(5.0_f64, 1.0, 0.1).expect("valid");
for _ in 0..20 {
est.predict(1.0, 0.0).expect("predict");
est.update(1.0, 5.0, 0.5).expect("update");
}
est.reset(0.0, 10.0).expect("reset");
assert!(
(est.state()).abs() < 1e-12,
"state after reset = {}",
est.state()
);
assert!((est.scale() - 10.0).abs() < 1e-12);
}
#[test]
fn negative_state_and_negative_h() {
let mut est = CauchyEstimator::new(-2.0_f64, 1.0, 0.01).expect("valid");
est.predict(1.0, 0.0).expect("predict");
est.update(-1.0, 2.0, 0.5).expect("update");
assert!(
est.state() < 0.0,
"State should remain negative, got {}",
est.state()
);
}
}