cbtop/adaptive_ml/
threshold.rs1use std::time::{Duration, Instant};
4
5use super::types::{TimeSeriesFeatures, WorkloadClass};
6
7#[derive(Debug, Clone)]
9pub struct LearnedWorkloadThreshold {
10 pub workload_class: WorkloadClass,
12 pub cv_threshold: f64,
14 pub confidence: f64,
16 pub training_samples: usize,
18 pub last_updated: Instant,
20 pub feature_means: Vec<f64>,
22 pub feature_stds: Vec<f64>,
24}
25
26impl LearnedWorkloadThreshold {
27 pub fn new(workload_class: WorkloadClass) -> Self {
29 Self {
30 workload_class,
31 cv_threshold: workload_class.default_cv_threshold(),
32 confidence: 0.0,
33 training_samples: 0,
34 last_updated: Instant::now(),
35 feature_means: Vec::new(),
36 feature_stds: Vec::new(),
37 }
38 }
39
40 pub fn update(&mut self, features: &TimeSeriesFeatures, is_anomaly: bool) {
42 self.training_samples += 1;
43 self.last_updated = Instant::now();
44
45 if !is_anomaly {
47 let observed_cv = features.cv;
49 let margin = 1.2; let weight = 0.1; let target = observed_cv * margin;
54
55 if target > self.cv_threshold {
56 self.cv_threshold = self.cv_threshold * (1.0 - weight) + target * weight;
57 }
58 }
59
60 self.confidence = (self.training_samples as f64 / 100.0).min(1.0);
62
63 let fv = features.to_vec();
65 if self.feature_means.is_empty() {
66 self.feature_means = fv.clone();
67 self.feature_stds = vec![0.0; fv.len()];
68 } else {
69 let n = self.training_samples as f64;
71 for (i, &val) in fv.iter().enumerate() {
72 let delta = val - self.feature_means[i];
73 self.feature_means[i] += delta / n;
74 let delta2 = val - self.feature_means[i];
75 if n > 1.0 {
77 self.feature_stds[i] = ((n - 2.0) / (n - 1.0) * self.feature_stds[i].powi(2)
78 + delta * delta2 / n)
79 .sqrt();
80 }
81 }
82 }
83 }
84
85 pub fn check_drift(&self, features: &TimeSeriesFeatures) -> Option<f64> {
87 if self.feature_means.is_empty() {
88 return None;
89 }
90
91 let fv = features.to_vec();
92 let mut max_zscore = 0.0_f64;
93
94 for (i, &val) in fv.iter().enumerate() {
95 if self.feature_stds[i] > 1e-10 {
96 let zscore = ((val - self.feature_means[i]) / self.feature_stds[i]).abs();
97 max_zscore = max_zscore.max(zscore);
98 }
99 }
100
101 if max_zscore > 3.0 {
102 Some(max_zscore)
103 } else {
104 None
105 }
106 }
107
108 pub fn is_stale(&self, max_age: Duration) -> bool {
110 self.last_updated.elapsed() > max_age
111 }
112}
113
114#[derive(Debug, Clone)]
116pub struct MlThresholdConfig {
117 pub min_training_samples: usize,
119 pub min_confidence: f64,
121 pub max_threshold_age: Duration,
123 pub drift_zscore_threshold: f64,
125 pub cold_start_multiplier: f64,
127}
128
129impl Default for MlThresholdConfig {
130 fn default() -> Self {
131 Self {
132 min_training_samples: 50,
133 min_confidence: 0.7,
134 max_threshold_age: Duration::from_secs(24 * 60 * 60), drift_zscore_threshold: 3.0,
136 cold_start_multiplier: 1.5,
137 }
138 }
139}