libspot-rs 0.4.0-rc.2

Pure Rust implementation of the SPOT algorithm for time series anomaly detection
Documentation
//! Configuration types for SPOT detector

/// GPD parameter estimator used by SPOT.
///
/// The default keeps the historical libspot-rs behavior: try the supported
/// estimators and keep the fit with the best log-likelihood.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SpotEstimator {
    /// Try the available estimators and keep the best log-likelihood.
    #[default]
    Best,
    /// Force Method of Moments estimation.
    Mom,
}

/// Initial excess-threshold selection strategy.
///
/// The default keeps the historical libspot-rs behavior.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SpotInitialThreshold {
    /// Use the P2 streaming quantile estimator.
    #[default]
    P2,
    /// Use the empirical sorted quantile from the initial batch.
    Empirical,
}

/// Streaming excess update condition.
///
/// The default keeps the historical libspot-rs behavior.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SpotExcessUpdate {
    /// Update the tail when `value >= excess_threshold`.
    #[default]
    GreaterOrEqual,
    /// Update the tail only when `value > excess_threshold`.
    Greater,
}

/// Configuration parameters for SPOT detector
///
/// # Serialization
///
/// When the `serde` feature is enabled, this struct can be serialized and deserialized.
/// This is useful for saving detector configurations or loading them from files.
///
/// # Example with serde (requires `serde` feature)
///
/// ```ignore
/// use libspot_rs::SpotConfig;
/// use serde_json;
///
/// let config = SpotConfig::default();
/// let json = serde_json::to_string(&config).unwrap();
/// let loaded: SpotConfig = serde_json::from_str(&json).unwrap();
/// ```
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SpotConfig {
    /// Anomaly probability threshold (must be between 0 and 1-level)
    pub q: f64,
    /// Whether to observe lower tail (false = upper tail, true = lower tail)
    pub low_tail: bool,
    /// Whether to discard anomalies from model updates
    pub discard_anomalies: bool,
    /// Excess level - high quantile that delimits the tail (must be between 0 and 1)
    pub level: f64,
    /// Maximum number of excess data points to keep
    pub max_excess: usize,
}

impl Default for SpotConfig {
    /// Default configuration that matches the C implementation
    fn default() -> Self {
        Self {
            q: 0.0001,
            low_tail: false,
            discard_anomalies: true,
            level: 0.998,
            max_excess: 200,
        }
    }
}

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

    #[test]
    fn test_spot_config_default() {
        let config = SpotConfig::default();

        assert_relative_eq!(config.q, 0.0001);
        assert!(!config.low_tail);
        assert!(config.discard_anomalies);
        assert_relative_eq!(config.level, 0.998);
        assert_eq!(config.max_excess, 200);
    }

    #[test]
    fn test_spot_config_clone() {
        let config1 = SpotConfig::default();
        let config2 = config1.clone();

        assert_relative_eq!(config1.q, config2.q);
        assert_eq!(config1.low_tail, config2.low_tail);
        assert_eq!(config1.discard_anomalies, config2.discard_anomalies);
        assert_relative_eq!(config1.level, config2.level);
        assert_eq!(config1.max_excess, config2.max_excess);
    }

    #[test]
    fn test_spot_estimator_default_keeps_existing_behavior() {
        assert_eq!(SpotEstimator::default(), SpotEstimator::Best);
    }

    #[test]
    fn test_spot_initial_threshold_default_keeps_existing_behavior() {
        assert_eq!(SpotInitialThreshold::default(), SpotInitialThreshold::P2);
    }

    #[test]
    fn test_spot_excess_update_default_keeps_existing_behavior() {
        assert_eq!(
            SpotExcessUpdate::default(),
            SpotExcessUpdate::GreaterOrEqual
        );
    }
}