use serde::{Deserialize, Serialize};
use crate::anomaly::error::AnomalyError;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DetectorType {
ZScore,
ModifiedZScore,
Iqr,
ForecastResidual,
MovingAverageResidual,
DynamicThreshold,
Cusum,
Custom(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnomalyScore {
pub timestamp: i64,
pub value: f64,
pub score: f64,
pub is_anomaly: bool,
pub method: DetectorType,
pub threshold: f64,
pub details: String,
}
pub trait AnomalyDetector: Send + Sync {
fn fit(&mut self, timestamps: &[i64], values: &[f64]) -> Result<(), AnomalyError>;
fn detect(
&mut self,
timestamps: &[i64],
values: &[f64],
) -> Result<Vec<AnomalyScore>, AnomalyError>;
fn detect_point(&mut self, timestamp: i64, value: f64) -> Result<AnomalyScore, AnomalyError>;
fn detector_type(&self) -> DetectorType;
}
#[inline]
pub fn validate_lengths(timestamps: &[i64], values: &[f64]) -> Result<(), AnomalyError> {
if timestamps.len() != values.len() {
return Err(AnomalyError::InvalidInput(format!(
"timestamps length ({}) != values length ({})",
timestamps.len(),
values.len(),
)));
}
Ok(())
}
#[inline]
#[must_use]
pub fn scale_floor(scale: f64, level: f64) -> f64 {
const RELATIVE: f64 = 1e-9;
const ABSOLUTE: f64 = 1e-12;
scale.max(level.abs() * RELATIVE).max(ABSOLUTE)
}