use std::f64::consts::PI;
pub trait RobustNorm {
fn rho(&self, z: f64) -> f64;
fn psi(&self, z: f64) -> f64;
fn weights(&self, z: f64) -> f64;
fn psi_deriv(&self, z: f64) -> f64;
fn rho_arr(&self, z: &[f64]) -> Vec<f64> {
z.iter().map(|&v| self.rho(v)).collect()
}
fn psi_arr(&self, z: &[f64]) -> Vec<f64> {
z.iter().map(|&v| self.psi(v)).collect()
}
fn weights_arr(&self, z: &[f64]) -> Vec<f64> {
z.iter().map(|&v| self.weights(v)).collect()
}
fn psi_deriv_arr(&self, z: &[f64]) -> Vec<f64> {
z.iter().map(|&v| self.psi_deriv(v)).collect()
}
}
#[derive(Clone, Copy, Debug)]
pub struct HuberT {
pub t: f64,
}
impl Default for HuberT {
fn default() -> Self {
HuberT { t: 1.345 }
}
}
impl HuberT {
pub fn new(t: f64) -> Self {
HuberT { t }
}
#[inline]
fn subset(&self, z: f64) -> bool {
z.abs() <= self.t
}
}
impl RobustNorm for HuberT {
fn rho(&self, z: f64) -> f64 {
if self.subset(z) {
0.5 * z * z
} else {
z.abs() * self.t - 0.5 * self.t * self.t
}
}
fn psi(&self, z: f64) -> f64 {
if self.subset(z) {
z
} else {
self.t * z.signum()
}
}
fn weights(&self, z: f64) -> f64 {
if self.subset(z) {
1.0
} else {
self.t / z.abs()
}
}
fn psi_deriv(&self, z: f64) -> f64 {
if z.abs() <= self.t {
1.0
} else {
0.0
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct TukeyBiweight {
pub c: f64,
}
impl Default for TukeyBiweight {
fn default() -> Self {
TukeyBiweight { c: 4.685 }
}
}
impl TukeyBiweight {
pub fn new(c: f64) -> Self {
TukeyBiweight { c }
}
#[inline]
fn subset(&self, z: f64) -> bool {
z.abs() <= self.c
}
}
impl RobustNorm for TukeyBiweight {
fn rho(&self, z: f64) -> f64 {
let factor = self.c * self.c / 6.0;
if self.subset(z) {
let u = 1.0 - (z / self.c).powi(2);
-u.powi(3) * factor + factor
} else {
factor
}
}
fn psi(&self, z: f64) -> f64 {
if self.subset(z) {
let u = 1.0 - (z / self.c).powi(2);
z * u * u
} else {
0.0
}
}
fn weights(&self, z: f64) -> f64 {
if self.subset(z) {
let u = 1.0 - (z / self.c).powi(2);
u * u
} else {
0.0
}
}
fn psi_deriv(&self, z: f64) -> f64 {
if self.subset(z) {
let r2 = (z / self.c).powi(2);
let u = 1.0 - r2;
u * u - (4.0 * z * z / (self.c * self.c)) * u
} else {
0.0
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct AndrewWave {
pub a: f64,
}
impl Default for AndrewWave {
fn default() -> Self {
AndrewWave { a: 1.339 }
}
}
impl AndrewWave {
pub fn new(a: f64) -> Self {
AndrewWave { a }
}
#[inline]
fn subset(&self, z: f64) -> bool {
z.abs() <= self.a * PI
}
}
impl RobustNorm for AndrewWave {
fn rho(&self, z: f64) -> f64 {
let a = self.a;
if self.subset(z) {
a * a * (1.0 - (z / a).cos())
} else {
a * a * 2.0
}
}
fn psi(&self, z: f64) -> f64 {
if self.subset(z) {
self.a * (z / self.a).sin()
} else {
0.0
}
}
fn weights(&self, z: f64) -> f64 {
let ratio = z / self.a;
if ratio.abs() < f64::EPSILON {
1.0
} else if self.subset(z) {
ratio.sin() / ratio
} else {
0.0
}
}
fn psi_deriv(&self, z: f64) -> f64 {
if self.subset(z) {
(z / self.a).cos()
} else {
0.0
}
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct LeastSquares;
impl RobustNorm for LeastSquares {
fn rho(&self, z: f64) -> f64 {
0.5 * z * z
}
fn psi(&self, z: f64) -> f64 {
z
}
fn weights(&self, _z: f64) -> f64 {
1.0
}
fn psi_deriv(&self, _z: f64) -> f64 {
1.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn huber_is_quadratic_then_linear() {
let h = HuberT::default();
assert!((h.psi(0.5) - 0.5).abs() < 1e-15);
assert!((h.weights(0.5) - 1.0).abs() < 1e-15);
assert!((h.psi_deriv(0.5) - 1.0).abs() < 1e-15);
assert!((h.psi(10.0) - h.t).abs() < 1e-15);
assert!((h.weights(10.0) - h.t / 10.0).abs() < 1e-15);
assert!(h.psi_deriv(10.0) == 0.0);
}
#[test]
fn psi_equals_z_times_weights() {
let z = 1.7_f64;
for (psi, w) in [
(HuberT::default().psi(z), HuberT::default().weights(z)),
(
TukeyBiweight::default().psi(z),
TukeyBiweight::default().weights(z),
),
(
AndrewWave::default().psi(z),
AndrewWave::default().weights(z),
),
(LeastSquares.psi(z), LeastSquares.weights(z)),
] {
assert!((psi - z * w).abs() < 1e-12);
}
}
#[test]
fn redescending_norms_reject_far_outliers() {
let far = 1e3;
assert_eq!(TukeyBiweight::default().weights(far), 0.0);
assert_eq!(TukeyBiweight::default().psi(far), 0.0);
assert_eq!(AndrewWave::default().weights(far), 0.0);
assert_eq!(AndrewWave::default().psi(far), 0.0);
}
#[test]
fn andrew_weight_at_zero_is_one() {
assert!((AndrewWave::default().weights(0.0) - 1.0).abs() < 1e-12);
}
}