resopt 0.3.0

Declarative constrained residual optimization in Rust
Documentation
use crate::core::Error;

/// Convex residual losses.
#[derive(Debug, Clone, PartialEq)]
pub enum Loss {
    /// 1/2 * ||r||_2^2
    L2Squared,
    /// ||r||_1
    L1,
    /// ||r||_inf
    LInf,
    /// Sum_i huber_delta(r_i)
    Huber { delta: f64 },
}

impl Loss {
    /// Validates the loss parameters.
    pub fn validate(&self) -> Result<(), Error> {
        match self {
            Self::Huber { delta } if *delta <= 0.0 => Err(Error::InvalidParameter {
                message: "Huber delta must be strictly positive".to_string(),
            }),
            _ => Ok(()),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn l2_squared_validates() {
        assert!(Loss::L2Squared.validate().is_ok());
    }

    #[test]
    fn l1_validates() {
        assert!(Loss::L1.validate().is_ok());
    }

    #[test]
    fn linf_validates() {
        assert!(Loss::LInf.validate().is_ok());
    }

    #[test]
    fn huber_positive_validates() {
        assert!(Loss::Huber { delta: 1.5 }.validate().is_ok());
    }

    #[test]
    fn huber_zero_rejects() {
        let err = Loss::Huber { delta: 0.0 }.validate().unwrap_err();
        match err {
            Error::InvalidParameter { message } => assert!(message.contains("Huber")),
            other => panic!("unexpected: {:?}", other),
        }
    }

    #[test]
    fn huber_negative_rejects() {
        assert!(Loss::Huber { delta: -1.0 }.validate().is_err());
    }

    #[test]
    fn clone_and_eq() {
        assert_eq!(Loss::L2Squared, Loss::L2Squared.clone());
        assert_ne!(Loss::L1, Loss::L2Squared);
    }
}