use std::time::{Duration, Instant};
use super::types::{TimeSeriesFeatures, WorkloadClass};
#[derive(Debug, Clone)]
pub struct LearnedWorkloadThreshold {
pub workload_class: WorkloadClass,
pub cv_threshold: f64,
pub confidence: f64,
pub training_samples: usize,
pub last_updated: Instant,
pub feature_means: Vec<f64>,
pub feature_stds: Vec<f64>,
}
impl LearnedWorkloadThreshold {
pub fn new(workload_class: WorkloadClass) -> Self {
Self {
workload_class,
cv_threshold: workload_class.default_cv_threshold(),
confidence: 0.0,
training_samples: 0,
last_updated: Instant::now(),
feature_means: Vec::new(),
feature_stds: Vec::new(),
}
}
pub fn update(&mut self, features: &TimeSeriesFeatures, is_anomaly: bool) {
self.training_samples += 1;
self.last_updated = Instant::now();
if !is_anomaly {
let observed_cv = features.cv;
let margin = 1.2;
let weight = 0.1; let target = observed_cv * margin;
if target > self.cv_threshold {
self.cv_threshold = self.cv_threshold * (1.0 - weight) + target * weight;
}
}
self.confidence = (self.training_samples as f64 / 100.0).min(1.0);
let fv = features.to_vec();
if self.feature_means.is_empty() {
self.feature_means = fv.clone();
self.feature_stds = vec![0.0; fv.len()];
} else {
let n = self.training_samples as f64;
for (i, &val) in fv.iter().enumerate() {
let delta = val - self.feature_means[i];
self.feature_means[i] += delta / n;
let delta2 = val - self.feature_means[i];
if n > 1.0 {
self.feature_stds[i] = ((n - 2.0) / (n - 1.0) * self.feature_stds[i].powi(2)
+ delta * delta2 / n)
.sqrt();
}
}
}
}
pub fn check_drift(&self, features: &TimeSeriesFeatures) -> Option<f64> {
if self.feature_means.is_empty() {
return None;
}
let fv = features.to_vec();
let mut max_zscore = 0.0_f64;
for (i, &val) in fv.iter().enumerate() {
if self.feature_stds[i] > 1e-10 {
let zscore = ((val - self.feature_means[i]) / self.feature_stds[i]).abs();
max_zscore = max_zscore.max(zscore);
}
}
if max_zscore > 3.0 {
Some(max_zscore)
} else {
None
}
}
pub fn is_stale(&self, max_age: Duration) -> bool {
self.last_updated.elapsed() > max_age
}
}
#[derive(Debug, Clone)]
pub struct MlThresholdConfig {
pub min_training_samples: usize,
pub min_confidence: f64,
pub max_threshold_age: Duration,
pub drift_zscore_threshold: f64,
pub cold_start_multiplier: f64,
}
impl Default for MlThresholdConfig {
fn default() -> Self {
Self {
min_training_samples: 50,
min_confidence: 0.7,
max_threshold_age: Duration::from_secs(24 * 60 * 60), drift_zscore_threshold: 3.0,
cold_start_multiplier: 1.5,
}
}
}