Skip to main content

robust_rs_core/rho/
huber.rs

1//! Huber's loss: quadratic near zero, linear in the tails. Monotone `ψ` ⇒ convex.
2
3use crate::error::RobustError;
4use crate::rho::RhoFunction;
5
6/// Huber's loss with tuning `k` (default `1.345`, ≈ 95% Gaussian efficiency).
7/// `ρ(r) = r²/2` for `|r| ≤ k`, else `k|r| − k²/2`.
8#[derive(Debug, Clone, Copy)]
9pub struct Huber {
10    k: f64,
11}
12
13impl Huber {
14    /// Create a Huber loss with the given positive tuning constant.
15    pub fn new(k: f64) -> Result<Self, RobustError> {
16        if k.is_finite() && k > 0.0 {
17            Ok(Self { k })
18        } else {
19            Err(RobustError::InvalidTuning { value: k })
20        }
21    }
22}
23
24impl Default for Huber {
25    /// `k = 1.345`.
26    fn default() -> Self {
27        Self { k: 1.345 }
28    }
29}
30
31impl RhoFunction for Huber {
32    fn rho(&self, r: f64) -> f64 {
33        if r.abs() <= self.k {
34            r.powi(2) / 2.0
35        } else {
36            self.k * r.abs() - self.k.powi(2) / 2.0
37        }
38    }
39    fn psi(&self, r: f64) -> f64 {
40        r.clamp(-self.k, self.k)
41    }
42    fn weight(&self, r: f64) -> f64 {
43        if r == 0.0 {
44            1.0
45        } else {
46            (self.k / r.abs()).min(1.0)
47        }
48    }
49    fn psi_prime(&self, r: f64) -> f64 {
50        if r.abs() <= self.k {
51            1.0
52        } else {
53            0.0
54        }
55    }
56    fn tuning(&self) -> f64 {
57        self.k
58    }
59    fn is_redescending(&self) -> bool {
60        false
61    }
62    fn rho_sup(&self) -> Option<f64> {
63        None
64    }
65}