use crate::core::{CoreError, CoreResult};
fn finite_positive(name: &str, value: f64) -> CoreResult<f64> {
if !value.is_finite() || value <= 0.0 {
return Err(CoreError::InvalidInput(format!(
"{name} must be finite and positive, got {value}"
)));
}
Ok(value)
}
fn finite(name: &str, value: f64) -> CoreResult<f64> {
if !value.is_finite() {
return Err(CoreError::InvalidInput(format!(
"{name} must be finite, got {value}"
)));
}
Ok(value)
}
pub trait LossFunction: Send + Sync {
fn evaluate(&self, s: f64) -> [f64; 3];
}
#[derive(Debug, Clone, Copy)]
pub struct L2Loss;
impl L2Loss {
pub fn new() -> Self {
L2Loss
}
}
impl Default for L2Loss {
fn default() -> Self {
Self::new()
}
}
impl LossFunction for L2Loss {
#[inline]
fn evaluate(&self, s: f64) -> [f64; 3] {
[s, 1.0, 0.0]
}
}
#[derive(Debug, Clone, Copy)]
pub struct L1Loss;
impl L1Loss {
pub fn new() -> Self {
L1Loss
}
}
impl Default for L1Loss {
fn default() -> Self {
Self::new()
}
}
const ORIGIN_SMOOTHING_EPS: f64 = 1e-12;
impl LossFunction for L1Loss {
#[inline]
fn evaluate(&self, s: f64) -> [f64; 3] {
let shifted = (s + ORIGIN_SMOOTHING_EPS).sqrt();
[
2.0 * (shifted - ORIGIN_SMOOTHING_EPS.sqrt()), 1.0 / shifted, -0.5 / (shifted * shifted * shifted), ]
}
}
#[derive(Debug, Clone)]
pub struct HuberLoss {
scale: f64,
scale2: f64,
}
impl HuberLoss {
pub fn new(scale: f64) -> CoreResult<Self> {
let scale = finite_positive("scale", scale)?;
Ok(HuberLoss {
scale,
scale2: scale * scale,
})
}
}
impl LossFunction for HuberLoss {
#[inline]
fn evaluate(&self, s: f64) -> [f64; 3] {
if s > self.scale2 {
let r = s.sqrt(); let rho1 = (self.scale / r).max(f64::MIN); [
2.0 * self.scale * r - self.scale2, rho1, -rho1 / (2.0 * s), ]
} else {
[s, 1.0, 0.0]
}
}
}
pub struct CauchyLoss {
scale2: f64,
c: f64,
}
impl CauchyLoss {
pub fn new(scale: f64) -> CoreResult<Self> {
let scale = finite_positive("scale", scale)?;
let scale2 = scale * scale;
Ok(CauchyLoss {
scale2,
c: 1.0 / scale2,
})
}
}
impl LossFunction for CauchyLoss {
#[inline]
fn evaluate(&self, s: f64) -> [f64; 3] {
let sum = 1.0 + s * self.c; let inv = 1.0 / sum;
[
self.scale2 * sum.ln(), inv.max(f64::MIN), -self.c * (inv * inv), ]
}
}
#[derive(Debug, Clone)]
pub struct FairLoss {
scale: f64,
}
impl FairLoss {
pub fn new(scale: f64) -> CoreResult<Self> {
let scale = finite_positive("scale", scale)?;
Ok(FairLoss { scale })
}
}
impl LossFunction for FairLoss {
#[inline]
fn evaluate(&self, s: f64) -> [f64; 3] {
if s < f64::EPSILON {
return [s, 1.0, 0.0];
}
let x = s.sqrt(); let abs_x = x.abs();
let c_plus_x = self.scale + abs_x;
let rho = self.scale * self.scale * (abs_x / self.scale - (1.0 + abs_x / self.scale).ln());
let rho_prime = self.scale / (2.0 * c_plus_x);
let rho_double_prime = -self.scale / (4.0 * abs_x * c_plus_x * c_plus_x);
[rho, rho_prime, rho_double_prime]
}
}
#[derive(Debug, Clone)]
pub struct GemanMcClureLoss {
c: f64, }
impl GemanMcClureLoss {
pub fn new(scale: f64) -> CoreResult<Self> {
let scale = finite_positive("scale", scale)?;
let scale2 = scale * scale;
Ok(GemanMcClureLoss { c: 1.0 / scale2 })
}
}
impl LossFunction for GemanMcClureLoss {
#[inline]
fn evaluate(&self, s: f64) -> [f64; 3] {
let denom = 1.0 + s * self.c; let inv = 1.0 / denom;
let inv2 = inv * inv;
[
s * inv, inv2, -2.0 * self.c * inv2 * inv, ]
}
}
#[derive(Debug, Clone)]
pub struct DcsLoss {
phi: f64,
}
impl DcsLoss {
pub fn new(phi: f64) -> CoreResult<Self> {
let phi = finite_positive("phi", phi)?;
Ok(DcsLoss { phi })
}
}
impl LossFunction for DcsLoss {
#[inline]
fn evaluate(&self, s: f64) -> [f64; 3] {
if s <= self.phi {
[s, 1.0, 0.0]
} else {
[self.phi, 0.0, 0.0]
}
}
}
#[derive(Debug, Clone)]
pub struct WelschLoss {
scale2: f64,
inv_scale2: f64,
}
impl WelschLoss {
pub fn new(scale: f64) -> CoreResult<Self> {
let scale = finite_positive("scale", scale)?;
let scale2 = scale * scale;
Ok(WelschLoss {
scale2,
inv_scale2: 1.0 / scale2,
})
}
}
impl LossFunction for WelschLoss {
#[inline]
fn evaluate(&self, s: f64) -> [f64; 3] {
let exp_term = (-s * self.inv_scale2).exp();
[
(self.scale2 / 2.0) * (1.0 - exp_term), 0.5 * exp_term, -0.5 * self.inv_scale2 * exp_term, ]
}
}
#[derive(Debug, Clone)]
pub struct TukeyBiweightLoss {
scale: f64,
scale2: f64,
}
impl TukeyBiweightLoss {
pub fn new(scale: f64) -> CoreResult<Self> {
let scale = finite_positive("scale", scale)?;
Ok(TukeyBiweightLoss {
scale,
scale2: scale * scale,
})
}
}
impl LossFunction for TukeyBiweightLoss {
#[inline]
fn evaluate(&self, s: f64) -> [f64; 3] {
let x = s.sqrt();
if x > self.scale {
[self.scale2 / 6.0, 0.0, 0.0]
} else {
let ratio = x / self.scale;
let ratio2 = ratio * ratio;
let one_minus_ratio2 = 1.0 - ratio2;
let one_minus_ratio2_sq = one_minus_ratio2 * one_minus_ratio2;
[
(self.scale2 / 6.0) * (1.0 - one_minus_ratio2 * one_minus_ratio2_sq), 0.5 * one_minus_ratio2_sq, -one_minus_ratio2 / self.scale2, ]
}
}
}
#[derive(Debug, Clone)]
pub struct AndrewsWaveLoss {
scale: f64,
scale2: f64,
threshold: f64, }
impl AndrewsWaveLoss {
pub fn new(scale: f64) -> CoreResult<Self> {
let scale = finite_positive("scale", scale)?;
Ok(AndrewsWaveLoss {
scale,
scale2: scale * scale,
threshold: std::f64::consts::PI * scale,
})
}
}
impl LossFunction for AndrewsWaveLoss {
#[inline]
fn evaluate(&self, s: f64) -> [f64; 3] {
let x = s.sqrt();
if x > self.threshold {
[2.0 * self.scale2, 0.0, 0.0]
} else if s < 1e-6 {
let rho = self.scale2 * (1.0 - (x / self.scale).cos());
let rho_prime = 0.5 - s / (12.0 * self.scale2);
let rho_double_prime =
-1.0 / (12.0 * self.scale2) + s / (120.0 * self.scale2 * self.scale2);
[rho, rho_prime, rho_double_prime]
} else {
let arg = x / self.scale;
let sin_val = arg.sin();
let cos_val = arg.cos();
[
self.scale2 * (1.0 - cos_val), self.scale * sin_val / (2.0 * x), (x * cos_val - self.scale * sin_val) / (4.0 * x * x * x), ]
}
}
}
#[derive(Debug, Clone)]
pub struct RamsayEaLoss {
scale: f64,
inv_scale2: f64,
}
impl RamsayEaLoss {
pub fn new(scale: f64) -> CoreResult<Self> {
let scale = finite_positive("scale", scale)?;
Ok(RamsayEaLoss {
scale,
inv_scale2: 1.0 / (scale * scale),
})
}
}
impl LossFunction for RamsayEaLoss {
#[inline]
fn evaluate(&self, s: f64) -> [f64; 3] {
let x = s.sqrt();
let ax = self.scale * x;
let exp_term = (-ax).exp();
let rho = self.inv_scale2 * (1.0 - exp_term * (1.0 + ax));
let rho_prime = 0.5 * exp_term;
let rho_double_prime = -(self.scale / (4.0 * x.max(f64::EPSILON))) * exp_term;
[rho, rho_prime, rho_double_prime]
}
}
#[derive(Debug, Clone)]
pub struct TrimmedMeanLoss {
scale2: f64,
}
impl TrimmedMeanLoss {
pub fn new(scale: f64) -> CoreResult<Self> {
let scale = finite_positive("scale", scale)?;
Ok(TrimmedMeanLoss {
scale2: scale * scale,
})
}
}
impl LossFunction for TrimmedMeanLoss {
#[inline]
fn evaluate(&self, s: f64) -> [f64; 3] {
if s <= self.scale2 {
[s / 2.0, 0.5, 0.0]
} else {
[self.scale2 / 2.0, 0.0, 0.0]
}
}
}
#[derive(Debug, Clone)]
pub struct LpNormLoss {
p: f64,
}
impl LpNormLoss {
pub fn new(p: f64) -> CoreResult<Self> {
let p = finite_positive("p", p)?;
Ok(LpNormLoss { p })
}
}
impl LossFunction for LpNormLoss {
#[inline]
fn evaluate(&self, s: f64) -> [f64; 3] {
let shifted = s + ORIGIN_SMOOTHING_EPS;
let exp_rho = self.p / 2.0;
let exp_rho_prime = exp_rho - 1.0;
let exp_rho_double_prime = exp_rho_prime - 1.0;
[
shifted.powf(exp_rho) - ORIGIN_SMOOTHING_EPS.powf(exp_rho), exp_rho * shifted.powf(exp_rho_prime), exp_rho * exp_rho_prime * shifted.powf(exp_rho_double_prime), ]
}
}
#[derive(Debug, Clone)]
pub struct BarronGeneralLoss {
alpha: f64,
scale2: f64,
}
impl BarronGeneralLoss {
pub fn new(alpha: f64, scale: f64) -> CoreResult<Self> {
let scale = finite_positive("scale", scale)?;
let alpha = finite("alpha", alpha)?;
Ok(BarronGeneralLoss {
alpha,
scale2: scale * scale,
})
}
}
impl LossFunction for BarronGeneralLoss {
#[inline]
fn evaluate(&self, s: f64) -> [f64; 3] {
let c2 = self.scale2;
if self.alpha.abs() < 1e-6 {
let inner = s / (2.0 * c2) + 1.0;
let rho = inner.ln();
let rho_prime = 1.0 / (2.0 * c2 * inner);
let rho_double_prime = -1.0 / (4.0 * c2 * c2 * inner * inner);
return [rho, rho_prime, rho_double_prime];
}
if (self.alpha - 2.0).abs() < 1e-6 {
return [s / (2.0 * c2), 1.0 / (2.0 * c2), 0.0];
}
let b = (self.alpha - 2.0).abs();
let inner = s / (c2 * b) + 1.0;
let half_alpha = self.alpha / 2.0;
let rho = (b / self.alpha) * (inner.powf(half_alpha) - 1.0);
let rho_prime = inner.powf(half_alpha - 1.0) / (2.0 * c2);
let sign = if self.alpha > 2.0 { 1.0 } else { -1.0 };
let rho_double_prime = sign * inner.powf(half_alpha - 2.0) / (4.0 * c2 * c2);
[rho, rho_prime, rho_double_prime]
}
}
#[derive(Debug, Clone)]
pub struct TDistributionLoss {
nu: f64, half_nu_plus_1: f64, }
impl TDistributionLoss {
pub fn new(nu: f64) -> CoreResult<Self> {
let nu = finite_positive("nu", nu)?;
Ok(TDistributionLoss {
nu,
half_nu_plus_1: (nu + 1.0) / 2.0,
})
}
}
impl LossFunction for TDistributionLoss {
#[inline]
fn evaluate(&self, s: f64) -> [f64; 3] {
let inner = 1.0 + s / self.nu;
let rho = self.half_nu_plus_1 * inner.ln();
let denom = self.nu + s;
let rho_prime = self.half_nu_plus_1 / denom;
let rho_double_prime = -self.half_nu_plus_1 / (denom * denom);
[rho, rho_prime, rho_double_prime]
}
}
#[derive(Debug, Clone)]
pub struct AdaptiveBarronLoss {
inner: BarronGeneralLoss,
}
impl AdaptiveBarronLoss {
pub fn new(alpha: f64, scale: f64) -> CoreResult<Self> {
Ok(AdaptiveBarronLoss {
inner: BarronGeneralLoss::new(alpha, scale)?,
})
}
const fn new_default() -> Self {
AdaptiveBarronLoss {
inner: BarronGeneralLoss {
alpha: 0.0,
scale2: 1.0,
},
}
}
}
impl LossFunction for AdaptiveBarronLoss {
#[inline]
fn evaluate(&self, s: f64) -> [f64; 3] {
self.inner.evaluate(s)
}
}
impl Default for AdaptiveBarronLoss {
fn default() -> Self {
Self::new_default()
}
}
pub fn loss_from_name(
name: &str,
scale: Option<f64>,
) -> CoreResult<Box<dyn LossFunction + Send + Sync>> {
let s = |default: f64| scale.unwrap_or(default);
let loss: Box<dyn LossFunction + Send + Sync> = match name.to_lowercase().as_str() {
"l2" => Box::new(L2Loss),
"l1" => Box::new(L1Loss),
"huber" => Box::new(HuberLoss::new(s(1.345))?),
"cauchy" => Box::new(CauchyLoss::new(s(2.3849))?),
"fair" => Box::new(FairLoss::new(s(1.3999))?),
"welsch" => Box::new(WelschLoss::new(s(2.9846))?),
"tukey" => Box::new(TukeyBiweightLoss::new(s(4.6851))?),
"geman" | "gemanmcclure" => Box::new(GemanMcClureLoss::new(s(1.0))?),
"dcs" => Box::new(DcsLoss::new(s(1.0))?),
"andrews" => Box::new(AndrewsWaveLoss::new(s(1.339))?),
"ramsay" => Box::new(RamsayEaLoss::new(s(0.3))?),
"trimmed" | "trimmedmean" => Box::new(TrimmedMeanLoss::new(s(2.0))?),
"lp" => Box::new(LpNormLoss::new(s(1.5))?),
"barron0" => Box::new(BarronGeneralLoss::new(0.0, s(1.0))?),
"barron1" => Box::new(BarronGeneralLoss::new(1.0, s(1.0))?),
"barron-2" => Box::new(BarronGeneralLoss::new(-2.0, s(1.0))?),
"t-distribution" | "tdistribution" => Box::new(TDistributionLoss::new(s(5.0))?),
"adaptive-barron" | "adaptivebarron" => Box::new(AdaptiveBarronLoss::new(0.0, s(1.0))?),
other => {
return Err(CoreError::InvalidInput(format!(
"unknown loss function: {other}. Valid options: {}",
loss_canonical_names().join(", ")
)));
}
};
Ok(loss)
}
pub fn loss_canonical_names() -> Vec<&'static str> {
vec![
"l2",
"l1",
"huber",
"cauchy",
"fair",
"welsch",
"tukey",
"geman",
"dcs",
"andrews",
"ramsay",
"trimmed",
"lp",
"barron0",
"barron1",
"barron-2",
"t-distribution",
"adaptive-barron",
]
}
#[cfg(test)]
mod tests {
use super::*;
type TestResult = Result<(), Box<dyn std::error::Error>>;
const EPSILON: f64 = 1e-6;
fn numerical_derivative(loss: &dyn LossFunction, s: f64, h: f64) -> (f64, f64) {
let [rho_plus, _, _] = loss.evaluate(s + h);
let [rho_minus, _, _] = loss.evaluate(s - h);
let [rho, _, _] = loss.evaluate(s);
let rho_prime_numerical = (rho_plus - rho_minus) / (2.0 * h);
let rho_double_prime_numerical = (rho_plus - 2.0 * rho + rho_minus) / (h * h);
(rho_prime_numerical, rho_double_prime_numerical)
}
#[test]
fn test_l2_loss() -> TestResult {
let loss = L2Loss;
let [rho, rho_prime, rho_double_prime] = loss.evaluate(0.0);
assert_eq!(rho, 0.0);
assert_eq!(rho_prime, 1.0);
assert_eq!(rho_double_prime, 0.0);
let [rho, rho_prime, rho_double_prime] = loss.evaluate(4.0);
assert_eq!(rho, 4.0);
assert_eq!(rho_prime, 1.0);
assert_eq!(rho_double_prime, 0.0);
Ok(())
}
#[test]
fn test_l1_loss() -> TestResult {
let loss = L1Loss;
let [rho, rho_prime, rho_double_prime] = loss.evaluate(0.0);
assert_eq!(rho, 0.0);
assert!(rho_prime.is_finite());
assert!(rho_double_prime.is_finite());
let [rho, rho_prime, _] = loss.evaluate(4.0);
assert!((rho - 4.0).abs() < 1e-5); assert!((rho_prime - 0.5).abs() < EPSILON);
Ok(())
}
const CONTRACT_S: &[f64] = &[
0.0, 1e-3, 0.05, 0.1, 0.3, 0.7, 1.0, 2.0, 4.0, 10.0, 25.0, 50.0, 200.0, 1e4,
];
const KINK_RADIUS: f64 = 1e-3;
fn derivative_kinks(name: &str) -> Vec<f64> {
match name {
"huber" => vec![1.345f64 * 1.345],
"dcs" => vec![1.0],
"andrews" => {
let t = std::f64::consts::PI * 1.339;
vec![t * t]
}
"trimmed" => vec![4.0],
_ => Vec::new(),
}
}
fn near_kink(name: &str, s: f64) -> bool {
derivative_kinks(name)
.iter()
.any(|k| (s - k).abs() < KINK_RADIUS)
}
#[test]
fn test_robust_loss_derivative_contract_table() -> TestResult {
const H: f64 = 1e-5;
let names = loss_canonical_names();
assert!(
names.len() >= 16,
"registry shrank to {} kernels — the contract table only means \
something if it covers them all",
names.len()
);
for name in &names {
let loss = loss_from_name(name, None)?;
let kinks = derivative_kinks(name);
let mut derivative_samples = 0usize;
for &s in CONTRACT_S {
let [rho, rho_prime, rho_double_prime] = loss.evaluate(s);
assert!(
rho.is_finite() && rho_prime.is_finite() && rho_double_prime.is_finite(),
"{name} s={s}: non-finite output [{rho}, {rho_prime}, {rho_double_prime}]"
);
assert!(rho >= 0.0, "{name} s={s}: negative robust cost rho={rho}");
assert!(
rho_prime >= 0.0,
"{name} s={s}: negative weight rho'={rho_prime} — Corrector would clamp \
it to zero and the block would stop moving while the cost still did"
);
if s < 10.0 * H || near_kink(name, s) {
continue;
}
derivative_samples += 1;
let (num_prime, num_double_prime) = numerical_derivative(loss.as_ref(), s, H);
let prime_err = (rho_prime - num_prime).abs();
let double_prime_err = (rho_double_prime - num_double_prime).abs();
assert!(
prime_err < 1e-3 * (1.0 + rho_prime.abs()),
"{name} s={s}: rho'={rho_prime} vs numeric {num_prime} (err={prime_err})"
);
assert!(
double_prime_err < 1e-2 * (1.0 + rho_double_prime.abs()),
"{name} s={s}: rho''={rho_double_prime} vs numeric {num_double_prime} \
(err={double_prime_err})"
);
}
assert!(
derivative_samples >= 10,
"{name}: only {derivative_samples} admissible derivative samples — \
too close to skipped samples for the sweep to mean anything"
);
for k in &kinks {
assert!(
CONTRACT_S.iter().any(|&s| (s - k).abs() < 10.0),
"{name}: declared cliff at s={k} is nowhere near the sample grid"
);
}
}
Ok(())
}
#[test]
fn test_corrector_gradient_consistency_for_every_loss() -> TestResult {
use crate::core::corrector::Corrector;
const H: f64 = 1e-6;
fn true_gradient(rho_prime: f64, r0: &[f64; 2], j: &[f64; 2]) -> f64 {
rho_prime * (r0[0] * j[0] + r0[1] * j[1])
}
let mut configs: Vec<(String, Box<dyn LossFunction + Send + Sync>)> = Vec::new();
for name in loss_canonical_names() {
configs.push((name.to_string(), loss_from_name(name, None)?));
}
configs.push((
"lp p=3 (rho''>0)".to_string(),
Box::new(LpNormLoss::new(3.0)?),
));
configs.push((
"barron alpha=3 (rho''>0)".to_string(),
Box::new(BarronGeneralLoss::new(3.0, 1.0)?),
));
let j = [1.0, -0.3];
let mut suppressed_cases = 0usize;
let mut curved_cases = 0usize;
let mut numeric_checks = 0usize;
let mut samples = 0usize;
for (name, loss) in &configs {
for &s in CONTRACT_S {
samples += 1;
let rho = loss.evaluate(s);
if rho[1] <= 0.0 {
suppressed_cases += 1;
} else if rho[2] > 0.0 && s > 0.0 {
curved_cases += 1;
}
let half = (s / 2.0).sqrt();
let r0 = [half, half];
let corrector = Corrector::new(loss.as_ref(), s);
assert!(
corrector.robust_cost().is_finite(),
"{name} s={s}: non-finite robust cost"
);
assert_eq!(
corrector.robust_cost(),
0.5 * rho[0],
"{name} s={s}: robust_cost must be 0.5*rho, not 0.5*||r_tilde||^2"
);
let mut modeled_residual = r0;
let mut modeled_jacobian = j;
corrector.correct_jacobian_in_place(&modeled_residual, &mut modeled_jacobian, 2, 1);
corrector.correct_residual_in_place(&mut modeled_residual);
let modeled = modeled_jacobian[0] * modeled_residual[0]
+ modeled_jacobian[1] * modeled_residual[1];
assert!(
modeled.is_finite() && modeled_residual.iter().all(|v| v.is_finite()),
"{name} s={s}: non-finite corrected residual/Jacobian [{modeled:?}]"
);
let cost = |x: f64| {
let rx = [r0[0] + j[0] * x, r0[1] + j[1] * x];
let sx = rx[0] * rx[0] + rx[1] * rx[1];
Corrector::new(loss.as_ref(), sx).robust_cost()
};
let expected = true_gradient(rho[1], &r0, &j);
assert!(
(modeled - expected).abs() <= 1e-6 * (1.0 + expected.abs()),
"{name} s={s}: modeled gradient {modeled} vs analytic \
rho'*(r.J) = {expected} — the correction algebra does not \
reproduce the loss's own weight"
);
let delta = cost(H) - cost(-H);
if near_kink(name, s) || delta.abs() <= 1e-14 * (1.0 + cost(0.0).abs()) {
continue;
}
numeric_checks += 1;
let numeric = delta / (2.0 * H);
assert!(
(modeled - numeric).abs() <= 1e-4 * (1.0 + numeric.abs()),
"{name} s={s}: modeled gradient {modeled} vs numeric derivative of \
robust_cost {numeric} — the solver would descend a cost it is \
not measuring"
);
}
}
assert!(
suppressed_cases > 0,
"no sampled (kernel, s) had rho' <= 0 — the suppression path went untested \
({suppressed_cases} suppressed, {curved_cases} curved)"
);
assert!(
curved_cases > 0,
"no sampled (kernel, s) had rho'' > 0 — the general alpha path went untested \
({suppressed_cases} suppressed, {curved_cases} curved)"
);
assert!(
numeric_checks * 4 >= samples,
"only {numeric_checks}/{samples} samples had a resolvable cost change — \
the numeric half of the property went largely unexercised"
);
Ok(())
}
#[test]
fn test_andrews_wave_loss_small_s_continuity() -> TestResult {
let scale = 1.339;
let loss = AndrewsWaveLoss::new(scale)?;
let [_, rho_prime_0, rho_double_prime_0] = loss.evaluate(0.0);
assert!((rho_prime_0 - 0.5).abs() < 1e-12);
assert!((rho_double_prime_0 - (-1.0 / (12.0 * scale * scale))).abs() < 1e-12);
for &s in &[1e-7, 1e-6, 1e-5, 1e-4] {
let [_, rho_prime, rho_double_prime] = loss.evaluate(s);
assert!(
(rho_prime - 0.5).abs() < 1e-4,
"s={s}: rho'={rho_prime} far from the s=0 limit 0.5"
);
assert!(
rho_double_prime.is_finite(),
"s={s}: rho'' must stay finite near the branch boundary"
);
}
Ok(())
}
#[test]
fn test_fair_loss() -> TestResult {
let loss = FairLoss::new(1.3999)?;
let [rho, rho_prime, rho_double_prime] = loss.evaluate(0.0);
assert_eq!(rho, 0.0);
assert_eq!(rho_prime, 1.0);
assert_eq!(rho_double_prime, 0.0);
let [_, rho_prime, _] = loss.evaluate(1.0);
assert!(rho_prime > 0.28 && rho_prime < 0.30);
let [_, rho_prime_outlier, _] = loss.evaluate(100.0);
assert!(rho_prime_outlier < rho_prime);
let [_, rho_prime_4, rho_double_prime_4] = loss.evaluate(4.0);
assert!(rho_prime_4.is_finite() && rho_prime_4 > 0.0);
assert!(rho_double_prime_4.is_finite() && rho_double_prime_4 < 0.0);
Ok(())
}
#[test]
fn test_geman_mcclure_loss() -> TestResult {
let loss = GemanMcClureLoss::new(1.0)?;
let [rho, rho_prime, _] = loss.evaluate(0.0);
assert_eq!(rho, 0.0);
assert!((rho_prime - 1.0).abs() < EPSILON);
let [_, rho_prime_small, _] = loss.evaluate(1.0);
let [_, rho_prime_large, _] = loss.evaluate(100.0);
assert!(rho_prime_large < rho_prime_small);
assert!(rho_prime_large < 0.1);
let s = 2.0;
let [_, rho_prime, rho_double_prime] = loss.evaluate(s);
let (rho_prime_num, rho_double_prime_num) = numerical_derivative(&loss, s, 1e-5);
assert!((rho_prime - rho_prime_num).abs() < 1e-4);
assert!((rho_double_prime - rho_double_prime_num).abs() < 1e-3);
Ok(())
}
#[test]
fn test_welsch_loss() -> TestResult {
let loss = WelschLoss::new(2.9846)?;
let [rho, rho_prime, _] = loss.evaluate(0.0);
assert_eq!(rho, 0.0);
assert!((rho_prime - 0.5).abs() < EPSILON);
let [_, rho_prime_10, _] = loss.evaluate(10.0);
let [_, rho_prime_100, _] = loss.evaluate(100.0);
assert!(rho_prime_100 < rho_prime_10);
assert!(rho_prime_100 < 0.01);
let s = 5.0;
let [_, rho_prime, rho_double_prime] = loss.evaluate(s);
let (rho_prime_num, rho_double_prime_num) = numerical_derivative(&loss, s, 1e-5);
assert!((rho_prime - rho_prime_num).abs() < 1e-4);
assert!((rho_double_prime - rho_double_prime_num).abs() < 1e-3);
Ok(())
}
#[test]
fn test_tukey_biweight_loss() -> TestResult {
let loss = TukeyBiweightLoss::new(4.6851)?;
let [rho, rho_prime, _] = loss.evaluate(0.0);
assert_eq!(rho, 0.0);
assert!((rho_prime - 0.5).abs() < EPSILON);
let scale2 = 4.6851 * 4.6851;
let [_, rho_prime_in, _] = loss.evaluate(scale2 * 0.5);
assert!(rho_prime_in > 0.05);
let [_, rho_prime_out, _] = loss.evaluate(scale2 * 1.5);
assert_eq!(rho_prime_out, 0.0);
let [_, rho_prime_5, rho_double_prime_5] = loss.evaluate(5.0);
assert!(rho_prime_5.is_finite() && rho_prime_5 > 0.0);
assert!(rho_double_prime_5.is_finite() && rho_double_prime_5 < 0.0);
Ok(())
}
#[test]
fn test_andrews_wave_loss() -> TestResult {
let loss = AndrewsWaveLoss::new(1.339)?;
let [rho, rho_prime, _] = loss.evaluate(0.0);
assert_eq!(rho, 0.0);
assert!((rho_prime - 0.5).abs() < EPSILON);
let [_, rho_prime_in, _] = loss.evaluate(1.0);
assert!(rho_prime_in > 0.44 && rho_prime_in < 0.47);
let scale = 1.339;
let [_, rho_prime_out, _] = loss.evaluate((scale * std::f64::consts::PI + 0.1).powi(2));
assert!(rho_prime_out.abs() < 0.01);
let [_, rho_prime_1, rho_double_prime_1] = loss.evaluate(1.0);
assert!(rho_prime_1.is_finite() && rho_prime_1 > 0.0);
assert!(rho_double_prime_1.is_finite());
Ok(())
}
#[test]
fn test_ramsay_ea_loss() -> TestResult {
let loss = RamsayEaLoss::new(0.3)?;
let [rho, _, _] = loss.evaluate(0.0);
assert_eq!(rho, 0.0);
let [_, rho_prime_small, _] = loss.evaluate(1.0);
let [_, rho_prime_large, _] = loss.evaluate(100.0);
assert!(rho_prime_large < rho_prime_small);
let s = 4.0;
let [_, rho_prime, rho_double_prime] = loss.evaluate(s);
let (rho_prime_num, rho_double_prime_num) = numerical_derivative(&loss, s, 1e-5);
assert!((rho_prime - rho_prime_num).abs() < 1e-4);
assert!((rho_double_prime - rho_double_prime_num).abs() < 1e-3);
Ok(())
}
#[test]
fn test_trimmed_mean_loss() -> TestResult {
let loss = TrimmedMeanLoss::new(2.0)?;
let scale2 = 4.0;
let [rho, rho_prime, rho_double_prime] = loss.evaluate(2.0);
assert!((rho - 1.0).abs() < EPSILON);
assert!((rho_prime - 0.5).abs() < EPSILON);
assert_eq!(rho_double_prime, 0.0);
let [rho_out, rho_prime_out, rho_double_prime_out] = loss.evaluate(10.0);
assert!((rho_out - scale2 / 2.0).abs() < EPSILON);
assert_eq!(rho_prime_out, 0.0);
assert_eq!(rho_double_prime_out, 0.0);
Ok(())
}
#[test]
fn test_lp_norm_loss() -> TestResult {
let l1 = LpNormLoss::new(1.0)?;
let [rho_l1, _, _] = l1.evaluate(4.0);
assert!((rho_l1 - 2.0).abs() < 1e-5);
let l2 = LpNormLoss::new(2.0)?;
let [rho_l2, rho_prime_l2, rho_double_prime_l2] = l2.evaluate(4.0);
assert!((rho_l2 - 4.0).abs() < EPSILON);
assert!((rho_prime_l2 - 1.0).abs() < EPSILON);
assert_eq!(rho_double_prime_l2, 0.0);
let l05 = LpNormLoss::new(0.5)?;
let [_, rho_prime_05, _] = l05.evaluate(4.0);
assert!(rho_prime_05 < 1.0);
let loss = LpNormLoss::new(1.5)?;
let s = 4.0;
let [_, rho_prime, rho_double_prime] = loss.evaluate(s);
let (rho_prime_num, rho_double_prime_num) = numerical_derivative(&loss, s, 1e-5);
assert!((rho_prime - rho_prime_num).abs() < 1e-4);
assert!((rho_double_prime - rho_double_prime_num).abs() < 1e-3);
Ok(())
}
#[test]
fn test_l1_lp_continuous_across_old_guard_boundary() -> TestResult {
let eps = f64::EPSILON;
for p in [0.5, 1.0, 1.5, 2.0] {
let loss = LpNormLoss::new(p)?;
let below = loss.evaluate(eps / 2.0);
let above = loss.evaluate(eps * 2.0);
let prime_scale = 1.0 + below[1].abs().max(above[1].abs());
assert!(
(above[1] - below[1]).abs() < 1e-3 * prime_scale,
"p={p}: rho' jumps from {} to {} across the old guard boundary",
below[1],
above[1]
);
}
let l1 = L1Loss::new();
let below = l1.evaluate(eps / 2.0);
let above = l1.evaluate(eps * 2.0);
let prime_scale = 1.0 + below[1].abs().max(above[1].abs());
assert!((above[1] - below[1]).abs() < 1e-3 * prime_scale);
Ok(())
}
#[test]
fn test_lp_norm_smooth_sweep_through_origin() -> TestResult {
for p in [0.5, 1.0, 1.5, 2.0] {
let loss = LpNormLoss::new(p)?;
let mut prev: Option<[f64; 3]> = None;
let mut s = 1e-20;
while s < 1.0 {
let cur = loss.evaluate(s);
assert!(
cur.iter().all(|v| v.is_finite()),
"p={p} s={s}: non-finite output {cur:?}"
);
if let Some(prev_vals) = prev {
let d_rho = (cur[0] - prev_vals[0]).abs();
assert!(
d_rho < 0.5,
"p={p} s={s}: rho jumped by {d_rho} between geometrically \
adjacent samples (10x apart in s)"
);
}
prev = Some(cur);
s *= 10.0;
}
}
Ok(())
}
#[test]
fn test_l1_robust_cost_continuous_across_guard_boundary() {
use crate::core::corrector::Corrector;
let loss = L1Loss::new();
let eps = f64::EPSILON;
let mut prev_cost: Option<f64> = None;
let mut s = eps / 8.0;
while s < eps * 8.0 {
let cost = Corrector::new(&loss, s).robust_cost();
if let Some(p) = prev_cost {
assert!(
(cost - p).abs() < 1e-8,
"s={s}: robust_cost jumped from {p} to {cost}"
);
}
prev_cost = Some(cost);
s *= 1.2;
}
}
#[test]
fn test_barron_general_loss_special_cases() -> TestResult {
let cauchy = BarronGeneralLoss::new(0.0, 1.0)?;
let [_, rho_prime_small, _] = cauchy.evaluate(1.0);
let [_, rho_prime_large, _] = cauchy.evaluate(100.0);
assert!(rho_prime_large < rho_prime_small);
let l2 = BarronGeneralLoss::new(2.0, 1.0)?;
let [rho, rho_prime, rho_double_prime] = l2.evaluate(4.0);
assert!((rho - 2.0).abs() < EPSILON);
assert!((rho_prime - 0.5).abs() < EPSILON);
assert!(rho_double_prime.abs() < EPSILON);
let charbonnier = BarronGeneralLoss::new(1.0, 1.0)?;
let [_, rho_prime_char, _] = charbonnier.evaluate(4.0);
assert!(rho_prime_char > 0.0 && rho_prime_char < 1.0);
let gm = BarronGeneralLoss::new(-2.0, 1.0)?;
let [_, rho_prime_small, _] = gm.evaluate(1.0);
let [_, rho_prime_large, _] = gm.evaluate(100.0);
assert!(rho_prime_large < rho_prime_small); assert!(rho_prime_large < 0.1);
Ok(())
}
#[test]
fn test_barron_general_loss_derivative_contract() -> TestResult {
for alpha in [2.0, 1.0, 0.5, 0.0, -1.0, -2.0, -10.0, -100.0] {
let loss = BarronGeneralLoss::new(alpha, 1.0)?;
let [rho0, _, _] = loss.evaluate(0.0);
assert!(
rho0.abs() < 1e-9,
"alpha={alpha}: rho(0) = {rho0}, expected 0"
);
for &s in &[1e-3, 0.5, 1.0, 4.0, 25.0, 100.0] {
let [rho, rho_prime, rho_double_prime] = loss.evaluate(s);
assert!(rho.is_finite() && rho_prime.is_finite() && rho_double_prime.is_finite());
assert!(
rho >= -1e-9,
"alpha={alpha} s={s}: rho={rho} is negative (ISSUE-0006 reproduction)"
);
let (num_prime, num_double_prime) = numerical_derivative(&loss, s, 1e-4);
let prime_err = (rho_prime - num_prime).abs();
let double_prime_err = (rho_double_prime - num_double_prime).abs();
assert!(
prime_err < 1e-3 * (1.0 + rho_prime.abs()),
"alpha={alpha} s={s}: rho'={rho_prime} vs numeric {num_prime}"
);
assert!(
double_prime_err < 1e-2 * (1.0 + rho_double_prime.abs()),
"alpha={alpha} s={s}: rho''={rho_double_prime} vs numeric {num_double_prime}"
);
}
}
Ok(())
}
#[test]
fn test_barron_general_loss_continuous_at_shape_limits() -> TestResult {
let scale = 1.7;
let s = 3.0;
for &(alpha, name) in &[(0.0, "alpha=0"), (2.0, "alpha=2")] {
let at_limit = BarronGeneralLoss::new(alpha, scale)?.evaluate(s);
for delta in [1e-5, 1e-4, 1e-3] {
let near = BarronGeneralLoss::new(alpha + delta, scale)?.evaluate(s);
let rho_err = (at_limit[0] - near[0]).abs();
let prime_err = (at_limit[1] - near[1]).abs();
assert!(
rho_err < 10.0 * delta,
"{name}: rho discontinuous approaching from +{delta}: {rho_err}"
);
assert!(
prime_err < 10.0 * delta,
"{name}: rho' discontinuous approaching from +{delta}: {prime_err}"
);
let near_below = BarronGeneralLoss::new(alpha - delta, scale)?.evaluate(s);
let rho_err_below = (at_limit[0] - near_below[0]).abs();
assert!(
rho_err_below < 10.0 * delta,
"{name}: rho discontinuous approaching from -{delta}: {rho_err_below}"
);
}
}
Ok(())
}
#[test]
fn test_constructor_validation() -> TestResult {
assert!(FairLoss::new(0.0).is_err());
assert!(FairLoss::new(-1.0).is_err());
assert!(GemanMcClureLoss::new(0.0).is_err());
assert!(WelschLoss::new(-1.0).is_err());
assert!(TukeyBiweightLoss::new(0.0).is_err());
assert!(AndrewsWaveLoss::new(-1.0).is_err());
assert!(RamsayEaLoss::new(0.0).is_err());
assert!(TrimmedMeanLoss::new(-1.0).is_err());
assert!(BarronGeneralLoss::new(0.0, 0.0).is_err());
assert!(BarronGeneralLoss::new(1.0, -1.0).is_err());
assert!(LpNormLoss::new(0.0).is_err());
assert!(LpNormLoss::new(-1.0).is_err());
assert!(FairLoss::new(1.0).is_ok());
assert!(LpNormLoss::new(1.5).is_ok());
assert!(BarronGeneralLoss::new(1.0, 1.0).is_ok());
Ok(())
}
#[test]
fn test_constructor_rejects_nonfinite_scale_parameters() {
for bad in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, -0.5, 0.0] {
assert!(HuberLoss::new(bad).is_err(), "Huber({bad})");
assert!(CauchyLoss::new(bad).is_err(), "Cauchy({bad})");
assert!(FairLoss::new(bad).is_err(), "Fair({bad})");
assert!(GemanMcClureLoss::new(bad).is_err(), "GemanMcClure({bad})");
assert!(DcsLoss::new(bad).is_err(), "Dcs({bad})");
assert!(WelschLoss::new(bad).is_err(), "Welsch({bad})");
assert!(TukeyBiweightLoss::new(bad).is_err(), "Tukey({bad})");
assert!(AndrewsWaveLoss::new(bad).is_err(), "Andrews({bad})");
assert!(RamsayEaLoss::new(bad).is_err(), "Ramsay({bad})");
assert!(TrimmedMeanLoss::new(bad).is_err(), "TrimmedMean({bad})");
assert!(LpNormLoss::new(bad).is_err(), "Lp({bad})");
assert!(TDistributionLoss::new(bad).is_err(), "TDistribution({bad})");
assert!(
BarronGeneralLoss::new(1.0, bad).is_err(),
"Barron(1,{bad}) scale"
);
assert!(
AdaptiveBarronLoss::new(1.0, bad).is_err(),
"AdaptiveBarron(1,{bad})"
);
}
assert!(BarronGeneralLoss::new(f64::NAN, 1.0).is_err());
assert!(BarronGeneralLoss::new(f64::INFINITY, 1.0).is_err());
assert!(BarronGeneralLoss::new(f64::NEG_INFINITY, 1.0).is_err());
assert!(BarronGeneralLoss::new(-2.0, 1.0).is_ok());
assert!(HuberLoss::new(1.345).is_ok());
assert!(CauchyLoss::new(2.3849).is_ok());
assert!(DcsLoss::new(1.0).is_ok());
assert!(TDistributionLoss::new(5.0).is_ok());
}
#[test]
fn test_loss_comparison() -> TestResult {
let s_outlier = 100.0;
let l2 = L2Loss;
let huber = HuberLoss::new(1.345)?;
let cauchy = CauchyLoss::new(2.3849)?;
let [_, w_l2, _] = l2.evaluate(s_outlier);
let [_, w_huber, _] = huber.evaluate(s_outlier);
let [_, w_cauchy, _] = cauchy.evaluate(s_outlier);
assert!(w_l2 > w_huber);
assert!(w_huber > w_cauchy);
assert!(w_cauchy < 0.1);
Ok(())
}
#[test]
fn test_t_distribution_loss() -> TestResult {
let loss = TDistributionLoss::new(5.0)?;
let [rho, rho_prime, _] = loss.evaluate(0.0);
assert_eq!(rho, 0.0);
assert!((rho_prime - 0.6).abs() < 0.01);
let [_, rho_prime_small, _] = loss.evaluate(1.0);
let [_, rho_prime_large, _] = loss.evaluate(100.0);
assert!(rho_prime_large < rho_prime_small);
assert!(rho_prime_large < 0.1);
let s = 4.0;
let [_, rho_prime, rho_double_prime] = loss.evaluate(s);
let (rho_prime_num, rho_double_prime_num) = numerical_derivative(&loss, s, 1e-5);
assert!((rho_prime - rho_prime_num).abs() < 1e-4);
assert!((rho_double_prime - rho_double_prime_num).abs() < 1e-4);
Ok(())
}
#[test]
fn test_t_distribution_loss_different_nu() -> TestResult {
let t3 = TDistributionLoss::new(3.0)?;
let t10 = TDistributionLoss::new(10.0)?;
let s_outlier = 100.0;
let [_, w_t3, _] = t3.evaluate(s_outlier);
let [_, w_t10, _] = t10.evaluate(s_outlier);
assert!(w_t3 < w_t10);
Ok(())
}
#[test]
fn test_adaptive_barron_loss() -> TestResult {
let adaptive = AdaptiveBarronLoss::new(0.0, 1.0)?;
let [rho, _, _] = adaptive.evaluate(0.0);
assert!(rho.abs() < EPSILON);
let [_, rho_prime_small, _] = adaptive.evaluate(1.0);
let [_, rho_prime_large, _] = adaptive.evaluate(100.0);
assert!(rho_prime_large < rho_prime_small);
let barron = BarronGeneralLoss::new(0.0, 1.0)?;
let [rho_a, rho_prime_a, rho_double_prime_a] = adaptive.evaluate(4.0);
let [rho_b, rho_prime_b, rho_double_prime_b] = barron.evaluate(4.0);
assert!((rho_a - rho_b).abs() < EPSILON);
assert!((rho_prime_a - rho_prime_b).abs() < EPSILON);
assert!((rho_double_prime_a - rho_double_prime_b).abs() < EPSILON);
Ok(())
}
#[test]
fn test_adaptive_barron_default() -> TestResult {
let adaptive = AdaptiveBarronLoss::default();
let [_, rho_prime, _] = adaptive.evaluate(4.0);
assert!(rho_prime > 0.0 && rho_prime < 1.0);
Ok(())
}
#[test]
fn test_new_loss_constructor_validation() -> TestResult {
assert!(TDistributionLoss::new(0.0).is_err());
assert!(TDistributionLoss::new(-1.0).is_err());
assert!(TDistributionLoss::new(5.0).is_ok());
assert!(AdaptiveBarronLoss::new(0.0, 0.0).is_err());
assert!(AdaptiveBarronLoss::new(1.0, -1.0).is_err());
assert!(AdaptiveBarronLoss::new(0.0, 1.0).is_ok());
Ok(())
}
#[test]
fn test_loss_registry_resolves_every_canonical_name() -> TestResult {
for name in loss_canonical_names() {
let loss = loss_from_name(name, None)?;
let [rho, rho_prime, _] = loss.evaluate(0.0);
assert_eq!(rho, 0.0, "{name}: ρ(0) must be 0");
if name == "l1" || name == "lp" {
assert!(
rho_prime.is_finite() && rho_prime > 0.0,
"{name}: ρ'(0) must be finite and positive, got {rho_prime}"
);
} else {
assert!(
(0.0..=1.0).contains(&rho_prime),
"{name}: ρ'(0) must be a sane weight, got {rho_prime}"
);
}
assert_eq!(
name.to_lowercase(),
name,
"{name}: canonical names must already be lowercase"
);
}
for alias in [
"gemanmcclure",
"trimmedmean",
"tdistribution",
"adaptivebarron",
"HUBER",
] {
loss_from_name(alias, None)?;
}
let Err(err) = loss_from_name("not-a-loss", None) else {
panic!("unknown loss name must not resolve");
};
let message = err.to_string();
for name in loss_canonical_names() {
assert!(
message.contains(name),
"error must list valid names, missing {name}: {message}"
);
}
Ok(())
}
#[test]
fn test_dcs_saturates_past_phi_instead_of_declining() -> TestResult {
let dcs = DcsLoss::new(1.0)?;
for s in [1.5, 4.0, 100.0, 1e12] {
let [rho, rho_prime, rho_double_prime] = dcs.evaluate(s);
assert!((rho - 1.0).abs() < 1e-12, "ρ({s}) = {rho}, expected Φ=1");
assert_eq!(rho_prime, 0.0, "ρ'({s})");
assert_eq!(rho_double_prime, 0.0, "ρ''({s})");
}
Ok(())
}
#[test]
fn test_dcs_inlier_identity_and_saturation() -> TestResult {
let dcs = DcsLoss::new(1.0)?;
for s in [0.0, 0.5, 1.0] {
let [rho, rho_prime, rho_double_prime] = dcs.evaluate(s);
assert_eq!(rho, s, "inlier ρ({s})");
assert_eq!(rho_prime, 1.0, "inlier ρ'({s})");
assert_eq!(rho_double_prime, 0.0, "inlier ρ''({s})");
}
let [rho, _, _] = dcs.evaluate(1e12);
assert!(rho.is_finite() && rho >= 0.0, "saturated ρ = {rho}");
assert!(
(rho - 1.0).abs() < 1e-12,
"saturated ρ must equal Φ, got {rho}"
);
assert!(DcsLoss::new(0.0).is_err());
assert!(DcsLoss::new(-1.0).is_err());
assert!(DcsLoss::new(f64::INFINITY).is_err());
Ok(())
}
#[test]
fn test_dcs_negative_weight_suppresses_without_nan() -> TestResult {
use crate::core::corrector::Corrector;
let dcs = DcsLoss::new(1.0)?;
for s in [1.5, 4.0, 100.0] {
let corrector = Corrector::new(&dcs, s);
let mut residual = vec![s.sqrt(), 0.0];
corrector.correct_residual_in_place(&mut residual);
assert!(
residual.iter().all(|v| v.is_finite()),
"NaN at s = {s}: {residual:?}"
);
assert!(
residual.iter().all(|&v| v == 0.0),
"fully suppressed at s = {s}: {residual:?}"
);
assert!(corrector.robust_cost().is_finite());
}
Ok(())
}
}