pub trait RobustKernel {
fn weight(&self, r_sq: f64, c_sq: f64) -> f64;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct IdentityKernel;
impl RobustKernel for IdentityKernel {
#[inline]
fn weight(&self, _r_sq: f64, _c_sq: f64) -> f64 {
1.0
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct HuberKernel;
impl RobustKernel for HuberKernel {
#[inline]
fn weight(&self, r_sq: f64, c_sq: f64) -> f64 {
if r_sq <= c_sq {
1.0
} else if c_sq <= 0.0 {
1.0
} else {
(c_sq / r_sq).sqrt()
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct CauchyKernel;
impl RobustKernel for CauchyKernel {
#[inline]
fn weight(&self, r_sq: f64, c_sq: f64) -> f64 {
if c_sq <= 0.0 {
return 1.0;
}
1.0 / (1.0 + r_sq / c_sq)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct TukeyKernel;
impl RobustKernel for TukeyKernel {
#[inline]
fn weight(&self, r_sq: f64, c_sq: f64) -> f64 {
if c_sq <= 0.0 || r_sq >= c_sq {
if c_sq <= 0.0 {
return 1.0;
}
return 0.0;
}
let u = 1.0 - r_sq / c_sq;
u * u
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum RobustKernelKind {
#[default]
Identity,
Huber,
Cauchy,
Tukey,
}
impl RobustKernel for RobustKernelKind {
#[inline]
fn weight(&self, r_sq: f64, c_sq: f64) -> f64 {
match self {
RobustKernelKind::Identity => IdentityKernel.weight(r_sq, c_sq),
RobustKernelKind::Huber => HuberKernel.weight(r_sq, c_sq),
RobustKernelKind::Cauchy => CauchyKernel.weight(r_sq, c_sq),
RobustKernelKind::Tukey => TukeyKernel.weight(r_sq, c_sq),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn weight_at_zero_is_one_for_all() {
let c_sq = 1.0;
assert!((IdentityKernel.weight(0.0, c_sq) - 1.0).abs() < 1e-15);
assert!((HuberKernel.weight(0.0, c_sq) - 1.0).abs() < 1e-15);
assert!((CauchyKernel.weight(0.0, c_sq) - 1.0).abs() < 1e-15);
assert!((TukeyKernel.weight(0.0, c_sq) - 1.0).abs() < 1e-15);
}
#[test]
fn tukey_is_hard_redescender() {
assert_eq!(TukeyKernel.weight(1.0, 1.0), 0.0);
assert_eq!(TukeyKernel.weight(4.0, 1.0), 0.0);
}
#[test]
fn cauchy_never_hits_zero() {
let w = CauchyKernel.weight(10000.0, 1.0);
assert!(w > 0.0, "Cauchy weight should be strictly positive: {w}");
assert!(
w < 1e-3,
"Cauchy weight should be small at far residuals: {w}"
);
}
#[test]
fn huber_weight_continuous_at_transition() {
let c_sq = 4.0;
let w_in = HuberKernel.weight(c_sq - 1e-12, c_sq);
let w_out = HuberKernel.weight(c_sq + 1e-12, c_sq);
assert!((w_in - 1.0).abs() < 1e-9);
assert!((w_out - 1.0).abs() < 1e-6);
}
#[test]
fn weights_monotonic_in_residual() {
let c_sq = 1.0;
for kernel in [
RobustKernelKind::Huber,
RobustKernelKind::Cauchy,
RobustKernelKind::Tukey,
] {
let pts = [0.1, 0.5, 0.9, 1.5, 4.0, 16.0];
let mut prev = f64::INFINITY;
for &r_sq in &pts {
let w = kernel.weight(r_sq, c_sq);
assert!(
w <= prev + 1e-12,
"{kernel:?} not monotonic at r²={r_sq}: w={w}, prev={prev}"
);
prev = w;
}
}
}
#[test]
fn weights_bounded() {
let c_sq = 1.0;
for kernel in [
RobustKernelKind::Identity,
RobustKernelKind::Huber,
RobustKernelKind::Cauchy,
RobustKernelKind::Tukey,
] {
for &r_sq in &[0.0, 0.01, 0.5, 1.0, 1.5, 100.0] {
let w = kernel.weight(r_sq, c_sq);
assert!(
(0.0..=1.0 + 1e-12).contains(&w),
"{kernel:?} produced out-of-range weight {w} at r²={r_sq}"
);
}
}
}
#[test]
fn degenerate_scale_is_safe() {
for kernel in [
RobustKernelKind::Identity,
RobustKernelKind::Huber,
RobustKernelKind::Cauchy,
RobustKernelKind::Tukey,
] {
let w = kernel.weight(1.0, 0.0);
assert!(w.is_finite(), "{kernel:?} produced non-finite weight: {w}");
}
}
}