1use crate::error::{Result, TransformError};
8#[cfg(feature = "monitoring")]
9use prometheus::{Counter, Gauge, Histogram, HistogramOpts, Registry};
10use scirs2_core::ndarray::{Array2, ArrayView1, ArrayView2};
11use scirs2_core::validation::check_not_empty;
12use std::collections::{HashMap, VecDeque};
13use std::time::{Duration, SystemTime, UNIX_EPOCH};
14#[derive(Debug, Clone, PartialEq)]
16pub enum DriftMethod {
17 KolmogorovSmirnov,
19 ChiSquare,
21 PopulationStabilityIndex,
23 MaximumMeanDiscrepancy,
25 WassersteinDistance,
27}
28#[derive(Debug, Clone)]
30pub struct DriftDetectionResult {
31 pub feature_name: String,
33 pub method: DriftMethod,
35 pub statistic: f64,
37 pub p_value: Option<f64>,
39 pub is_drift_detected: bool,
41 pub severity: f64,
43 pub timestamp: u64,
45}
46#[derive(Debug, Clone)]
48pub struct PerformanceMetrics {
49 pub processing_time_ms: f64,
51 pub memory_usage_mb: f64,
53 pub error_rate: f64,
55 pub throughput: f64,
57 pub data_quality_score: f64,
59 pub timestamp: u64,
61}
62#[derive(Debug, Clone)]
64pub struct AlertConfig {
65 pub drift_threshold: f64,
67 pub performance_threshold: f64,
69 pub error_rate_threshold: f64,
71 pub memory_threshold_mb: f64,
73 pub cooldown_seconds: u64,
75}
76impl Default for AlertConfig {
77 fn default() -> Self {
78 AlertConfig {
79 drift_threshold: 0.05,
80 performance_threshold: 2.0,
81 error_rate_threshold: 0.05,
82 memory_threshold_mb: 1000.0,
83 cooldown_seconds: 300,
84 }
85 }
86}
87#[derive(Debug, Clone)]
89pub enum AlertType {
90 DataDrift {
92 feature: String,
94 severity: f64,
96 },
97 PerformanceDegradation {
99 metric: String,
101 value: f64,
103 },
104 HighErrorRate {
106 rate: f64,
108 },
109 MemoryExhaustion {
111 usage_mb: f64,
113 },
114 DataQualityIssue {
116 score: f64,
118 },
119}
120pub struct TransformationMonitor {
122 reference_data: Option<Array2<f64>>,
124 feature_names: Vec<String>,
126 drift_methods: HashMap<String, DriftMethod>,
128 performance_history: VecDeque<PerformanceMetrics>,
130 drift_history: VecDeque<DriftDetectionResult>,
132 alert_config: AlertConfig,
134 last_alert_times: HashMap<String, u64>,
136 baseline_metrics: Option<PerformanceMetrics>,
138 #[cfg(feature = "monitoring")]
140 metrics_registry: Registry,
141 #[cfg(feature = "monitoring")]
143 prometheus_metrics: PrometheusMetrics,
144}
145#[cfg(feature = "monitoring")]
146struct PrometheusMetrics {
147 drift_detections: Counter,
148 processing_time: Histogram,
149 memory_usage: Gauge,
150 error_rate: Gauge,
151 throughput: Gauge,
152 data_quality: Gauge,
153}
154impl TransformationMonitor {
155 pub fn new() -> Result<Self> {
157 #[cfg(feature = "monitoring")]
158 let metrics_registry = Registry::new();
159 #[cfg(feature = "monitoring")]
160 let prometheus_metrics = PrometheusMetrics {
161 drift_detections: Counter::new(
162 "transform_drift_detections_total",
163 "Total number of drift detections",
164 )
165 .map_err(|e| {
166 TransformError::ComputationError(format!("Failed to create counter: {}", e))
167 })?,
168 processing_time: Histogram::with_opts(HistogramOpts::new(
169 "transform_processing_time_seconds",
170 "Processing time in seconds",
171 ))
172 .map_err(|e| {
173 TransformError::ComputationError(format!("Failed to create histogram: {}", e))
174 })?,
175 memory_usage: Gauge::new("transform_memory_usage_mb", "Memory usage in MB").map_err(
176 |e| TransformError::ComputationError(format!("Failed to create gauge: {}", e)),
177 )?,
178 error_rate: Gauge::new("transform_error_rate", "Error rate").map_err(|e| {
179 TransformError::ComputationError(format!("Failed to create gauge: {}", e))
180 })?,
181 throughput: Gauge::new(
182 "transform_throughput_samples_per_second",
183 "Throughput in samples per second",
184 )
185 .map_err(|e| {
186 TransformError::ComputationError(format!("Failed to create gauge: {}", e))
187 })?,
188 data_quality: Gauge::new("transform_data_quality_score", "Data quality score")
189 .map_err(|e| {
190 TransformError::ComputationError(format!("Failed to create gauge: {}", e))
191 })?,
192 };
193 #[cfg(feature = "monitoring")]
194 {
195 metrics_registry
196 .register(Box::new(prometheus_metrics.drift_detections.clone()))
197 .map_err(|e| {
198 TransformError::ComputationError(format!("Failed to register counter: {}", e))
199 })?;
200 metrics_registry
201 .register(Box::new(prometheus_metrics.processing_time.clone()))
202 .map_err(|e| {
203 TransformError::ComputationError(format!("Failed to register histogram: {}", e))
204 })?;
205 metrics_registry
206 .register(Box::new(prometheus_metrics.memory_usage.clone()))
207 .map_err(|e| {
208 TransformError::ComputationError(format!("Failed to register gauge: {}", e))
209 })?;
210 metrics_registry
211 .register(Box::new(prometheus_metrics.error_rate.clone()))
212 .map_err(|e| {
213 TransformError::ComputationError(format!("Failed to register gauge: {}", e))
214 })?;
215 metrics_registry
216 .register(Box::new(prometheus_metrics.throughput.clone()))
217 .map_err(|e| {
218 TransformError::ComputationError(format!("Failed to register gauge: {}", e))
219 })?;
220 metrics_registry
221 .register(Box::new(prometheus_metrics.data_quality.clone()))
222 .map_err(|e| {
223 TransformError::ComputationError(format!("Failed to register gauge: {}", e))
224 })?;
225 }
226 Ok(TransformationMonitor {
227 reference_data: None,
228 feature_names: Vec::new(),
229 drift_methods: HashMap::new(),
230 performance_history: VecDeque::with_capacity(1000),
231 drift_history: VecDeque::with_capacity(1000),
232 alert_config: AlertConfig::default(),
233 last_alert_times: HashMap::new(),
234 baseline_metrics: None,
235 #[cfg(feature = "monitoring")]
236 metrics_registry,
237 #[cfg(feature = "monitoring")]
238 prometheus_metrics,
239 })
240 }
241 pub fn set_reference_data(
243 &mut self,
244 data: Array2<f64>,
245 feature_names: Option<Vec<String>>,
246 ) -> Result<()> {
247 self.reference_data = Some(data.clone());
248 if let Some(names) = feature_names {
249 if names.len() != data.ncols() {
250 return Err(TransformError::InvalidInput(
251 "Number of feature names must match number of columns".to_string(),
252 ));
253 }
254 self.feature_names = names;
255 } else {
256 self.feature_names = (0..data.ncols())
257 .map(|i| format!("feature_{}", i))
258 .collect();
259 }
260 for feature_name in &self.feature_names {
261 self.drift_methods
262 .insert(feature_name.clone(), DriftMethod::KolmogorovSmirnov);
263 }
264 Ok(())
265 }
266 pub fn set_drift_method(&mut self, featurename: &str, method: DriftMethod) -> Result<()> {
268 if !self.feature_names.contains(&featurename.to_string()) {
269 return Err(TransformError::InvalidInput(format!(
270 "Unknown feature name: {}",
271 featurename
272 )));
273 }
274 self.drift_methods.insert(featurename.to_string(), method);
275 Ok(())
276 }
277 pub fn set_alert_config(&mut self, config: AlertConfig) {
279 self.alert_config = config;
280 }
281 pub fn set_baseline_metrics(&mut self, metrics: PerformanceMetrics) {
283 self.baseline_metrics = Some(metrics);
284 }
285 pub fn detect_drift(
287 &mut self,
288 new_data: &ArrayView2<f64>,
289 ) -> Result<Vec<DriftDetectionResult>> {
290 let reference_data = self
291 .reference_data
292 .as_ref()
293 .ok_or_else(|| TransformError::InvalidInput("Reference data not set".to_string()))?;
294 if new_data.ncols() != reference_data.ncols() {
295 return Err(TransformError::InvalidInput(
296 "New data must have same number of features as reference data".to_string(),
297 ));
298 }
299 let mut results = Vec::new();
300 let timestamp = current_timestamp();
301 for (i, feature_name) in self.feature_names.iter().enumerate() {
302 let method = self
303 .drift_methods
304 .get(feature_name)
305 .unwrap_or(&DriftMethod::KolmogorovSmirnov);
306 let reference_feature = reference_data.column(i);
307 let new_feature = new_data.column(i);
308 let result = self.detect_feature_drift(
309 &reference_feature,
310 &new_feature,
311 feature_name,
312 method,
313 timestamp,
314 )?;
315 results.push(result.clone());
316 self.drift_history.push_back(result);
317 if self.drift_history.len() > 1000 {
318 self.drift_history.pop_front();
319 }
320 }
321 #[cfg(feature = "monitoring")]
322 {
323 let drift_count = results.iter().filter(|r| r.is_drift_detected).count();
324 self.prometheus_metrics
325 .drift_detections
326 .inc_by(drift_count as f64);
327 }
328 Ok(results)
329 }
330 pub fn record_metrics(&mut self, metrics: PerformanceMetrics) -> Result<Vec<AlertType>> {
332 self.performance_history.push_back(metrics.clone());
333 if self.performance_history.len() > 1000 {
334 self.performance_history.pop_front();
335 }
336 #[cfg(feature = "monitoring")]
337 {
338 self.prometheus_metrics
339 .processing_time
340 .observe(metrics.processing_time_ms / 1000.0);
341 self.prometheus_metrics
342 .memory_usage
343 .set(metrics.memory_usage_mb);
344 self.prometheus_metrics.error_rate.set(metrics.error_rate);
345 self.prometheus_metrics.throughput.set(metrics.throughput);
346 self.prometheus_metrics
347 .data_quality
348 .set(metrics.data_quality_score);
349 }
350 self.check_performance_alerts(&metrics)
351 }
352 pub fn get_drift_summary(&self, lookbackhours: u64) -> Result<HashMap<String, f64>> {
354 let cutoff_time = current_timestamp() - (lookbackhours * 3600);
355 let mut summary = HashMap::new();
356 for feature_name in &self.feature_names {
357 let recent_detections: Vec<_> = self
358 .drift_history
359 .iter()
360 .filter(|r| r.timestamp >= cutoff_time && r.feature_name == *feature_name)
361 .collect();
362 let drift_rate = if recent_detections.is_empty() {
363 0.0
364 } else {
365 recent_detections
366 .iter()
367 .filter(|r| r.is_drift_detected)
368 .count() as f64
369 / recent_detections.len() as f64
370 };
371 summary.insert(feature_name.clone(), drift_rate);
372 }
373 Ok(summary)
374 }
375 pub fn get_performance_trends(&self, lookbackhours: u64) -> Result<HashMap<String, f64>> {
377 let cutoff_time = current_timestamp() - (lookbackhours * 3600);
378 let recent_metrics: Vec<_> = self
379 .performance_history
380 .iter()
381 .filter(|m| m.timestamp >= cutoff_time)
382 .collect();
383 if recent_metrics.is_empty() {
384 return Ok(HashMap::new());
385 }
386 let mut trends = HashMap::new();
387 if recent_metrics.len() >= 2 {
388 let first = recent_metrics.first().expect("Operation failed");
389 let last = recent_metrics.last().expect("Operation failed");
390 trends.insert(
391 "processing_time_trend".to_string(),
392 (last.processing_time_ms - first.processing_time_ms) / first.processing_time_ms,
393 );
394 trends.insert(
395 "memory_usage_trend".to_string(),
396 (last.memory_usage_mb - first.memory_usage_mb) / first.memory_usage_mb,
397 );
398 trends.insert(
399 "error_rate_trend".to_string(),
400 last.error_rate - first.error_rate,
401 );
402 trends.insert(
403 "throughput_trend".to_string(),
404 (last.throughput - first.throughput) / first.throughput,
405 );
406 }
407 Ok(trends)
408 }
409 fn detect_feature_drift(
410 &self,
411 reference: &ArrayView1<f64>,
412 new_data: &ArrayView1<f64>,
413 feature_name: &str,
414 method: &DriftMethod,
415 timestamp: u64,
416 ) -> Result<DriftDetectionResult> {
417 check_not_empty(reference, "reference")?;
418 check_not_empty(new_data, "new_data")?;
419 for &val in reference.iter() {
420 if !val.is_finite() {
421 return Err(crate::error::TransformError::DataValidationError(
422 "Reference data contains non-finite values".to_string(),
423 ));
424 }
425 }
426 for &val in new_data.iter() {
427 if !val.is_finite() {
428 return Err(crate::error::TransformError::DataValidationError(
429 "New data contains non-finite values".to_string(),
430 ));
431 }
432 }
433 let (statistic, p_value, is_drift) = match method {
434 DriftMethod::KolmogorovSmirnov => {
435 let (stat, p_val) = self.kolmogorov_smirnov_test(reference, new_data)?;
436 (stat, Some(p_val), p_val < self.alert_config.drift_threshold)
437 }
438 DriftMethod::ChiSquare => {
439 let (stat, p_val) = self.chi_square_test(reference, new_data)?;
440 (stat, Some(p_val), p_val < self.alert_config.drift_threshold)
441 }
442 DriftMethod::PopulationStabilityIndex => {
443 let psi = self.population_stability_index(reference, new_data)?;
444 (psi, None, psi > 0.1)
445 }
446 DriftMethod::MaximumMeanDiscrepancy => {
447 let mmd = self.maximum_mean_discrepancy(reference, new_data)?;
448 (mmd, None, mmd > self.alert_config.drift_threshold)
449 }
450 DriftMethod::WassersteinDistance => {
451 let distance = self.wasserstein_distance(reference, new_data)?;
452 (distance, None, distance > self.alert_config.drift_threshold)
453 }
454 };
455 let severity = if let Some(p_val) = p_value {
456 1.0 - p_val
457 } else {
458 statistic.min(1.0)
459 };
460 Ok(DriftDetectionResult {
461 feature_name: feature_name.to_string(),
462 method: method.clone(),
463 statistic,
464 p_value,
465 is_drift_detected: is_drift,
466 severity,
467 timestamp,
468 })
469 }
470 fn kolmogorov_smirnov_test(
471 &self,
472 x: &ArrayView1<f64>,
473 y: &ArrayView1<f64>,
474 ) -> Result<(f64, f64)> {
475 let mut x_sorted = x.to_vec();
476 let mut y_sorted = y.to_vec();
477 x_sorted.sort_by(|a, b| a.partial_cmp(b).expect("Operation failed"));
478 y_sorted.sort_by(|a, b| a.partial_cmp(b).expect("Operation failed"));
479 let n1 = x_sorted.len() as f64;
480 let n2 = y_sorted.len() as f64;
481 let mut combined: Vec<(f64, i32)> = Vec::new();
482 for val in &x_sorted {
483 combined.push((*val, 1));
484 }
485 for val in &y_sorted {
486 combined.push((*val, 2));
487 }
488 combined.sort_by(|a, b| a.0.partial_cmp(&b.0).expect("Operation failed"));
489 let mut cdf1 = 0.0;
490 let mut cdf2 = 0.0;
491 let mut max_diff: f64 = 0.0;
492 for (_, sample_id) in combined {
493 if sample_id == 1 {
494 cdf1 += 1.0 / n1;
495 } else {
496 cdf2 += 1.0 / n2;
497 }
498 max_diff = max_diff.max((cdf1 - cdf2).abs());
499 }
500 let statistic = max_diff;
501 let effective_n = (n1 * n2) / (n1 + n2);
502 let lambda = statistic * effective_n.sqrt();
503 let p_value = if lambda < 0.27 {
504 1.0
505 } else if lambda < 1.0 {
506 2.0 * (-2.0 * lambda * lambda).exp()
507 } else {
508 let mut sum = 0.0;
509 for k in 1..=10 {
510 let k_f = k as f64;
511 sum += (-1.0_f64).powi(k - 1) * (-2.0 * k_f * k_f * lambda * lambda).exp();
512 }
513 2.0 * sum
514 };
515 Ok((statistic, p_value.clamp(0.0, 1.0)))
516 }
517 fn population_stability_index(
518 &self,
519 reference: &ArrayView1<f64>,
520 new_data: &ArrayView1<f64>,
521 ) -> Result<f64> {
522 let mut ref_sorted = reference.to_vec();
523 ref_sorted.sort_by(|a, b| a.partial_cmp(b).expect("Operation failed"));
524 let n_bins = 10;
525 let mut bins = Vec::new();
526 for i in 0..=n_bins {
527 let percentile = (i as f64) / (n_bins as f64);
528 let index = ((ref_sorted.len() - 1) as f64 * percentile) as usize;
529 bins.push(ref_sorted[index]);
530 }
531 let ref_freq = self.calculate_bin_frequencies(reference, &bins);
532 let new_freq = self.calculate_bin_frequencies(new_data, &bins);
533 let mut psi = 0.0;
534 for i in 0..n_bins {
535 let ref_pct = ref_freq[i];
536 let new_pct = new_freq[i];
537 if ref_pct > 0.0 && new_pct > 0.0 {
538 psi += (new_pct - ref_pct) * (new_pct / ref_pct).ln();
539 }
540 }
541 Ok(psi)
542 }
543 fn calculate_bin_frequencies(&self, data: &ArrayView1<f64>, bins: &[f64]) -> Vec<f64> {
544 if bins.len() < 2 {
545 return vec![];
546 }
547 let mut frequencies = vec![0; bins.len() - 1];
548 for &value in data.iter() {
549 if !value.is_finite() {
550 continue;
551 }
552 let mut placed = false;
553 for i in 0..bins.len() - 1 {
554 if i == bins.len() - 2 {
555 if value >= bins[i] && value <= bins[i + 1] {
556 frequencies[i] += 1;
557 placed = true;
558 break;
559 }
560 } else if value >= bins[i] && value < bins[i + 1] {
561 frequencies[i] += 1;
562 placed = true;
563 break;
564 }
565 }
566 if !placed {
567 if value < bins[0] {
568 frequencies[0] += 1;
569 } else if value > bins[bins.len() - 1] {
570 let last_idx = frequencies.len() - 1;
571 frequencies[last_idx] += 1;
572 }
573 }
574 }
575 let total = data.iter().filter(|&&v| v.is_finite()).count() as f64;
576 if total == 0.0 {
577 vec![0.0; frequencies.len()]
578 } else {
579 frequencies.iter().map(|&f| f as f64 / total).collect()
580 }
581 }
582 fn wasserstein_distance(&self, x: &ArrayView1<f64>, y: &ArrayView1<f64>) -> Result<f64> {
583 let mut x_sorted: Vec<f64> = x.iter().filter(|&&v| v.is_finite()).copied().collect();
584 let mut y_sorted: Vec<f64> = y.iter().filter(|&&v| v.is_finite()).copied().collect();
585 if x_sorted.is_empty() || y_sorted.is_empty() {
586 return Ok(0.0);
587 }
588 x_sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
589 y_sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
590 let n1 = x_sorted.len();
591 let n2 = y_sorted.len();
592 let max_len = n1.max(n2);
593 let mut distance = 0.0;
594 for i in 0..max_len {
595 let x_val = if i < n1 {
596 x_sorted[i]
597 } else {
598 x_sorted[n1 - 1]
599 };
600 let y_val = if i < n2 {
601 y_sorted[i]
602 } else {
603 y_sorted[n2 - 1]
604 };
605 distance += (x_val - y_val).abs();
606 }
607 Ok(distance / max_len as f64)
608 }
609 fn chi_square_test(
611 &self,
612 reference: &ArrayView1<f64>,
613 new_data: &ArrayView1<f64>,
614 ) -> Result<(f64, f64)> {
615 let n_bins = 10;
616 let mut combined_data: Vec<f64> = reference
617 .iter()
618 .chain(new_data.iter())
619 .filter(|&&v| v.is_finite())
620 .copied()
621 .collect();
622 if combined_data.len() < n_bins {
623 return Ok((0.0, 1.0));
624 }
625 combined_data.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
626 let mut bins = Vec::new();
627 for i in 0..=n_bins {
628 let percentile = i as f64 / n_bins as f64;
629 let index = ((combined_data.len() - 1) as f64 * percentile) as usize;
630 bins.push(combined_data[index]);
631 }
632 bins.dedup_by(|a, b| (*a - *b).abs() < f64::EPSILON);
633 if bins.len() < 2 {
634 return Ok((0.0, 1.0));
635 }
636 let ref_freq = self.calculate_bin_frequencies(reference, &bins);
637 let new_freq = self.calculate_bin_frequencies(new_data, &bins);
638 let ref_total = reference.iter().filter(|&&v| v.is_finite()).count() as f64;
639 let new_total = new_data.iter().filter(|&&v| v.is_finite()).count() as f64;
640 if ref_total == 0.0 || new_total == 0.0 {
641 return Ok((0.0, 1.0));
642 }
643 let mut chi_square = 0.0;
644 let mut degrees_of_freedom = 0;
645 for i in 0..ref_freq.len() {
646 let observed_ref = ref_freq[i] * ref_total;
647 let observed_new = new_freq[i] * new_total;
648 let total_in_bin = observed_ref + observed_new;
649 let expected_ref_null = total_in_bin * ref_total / (ref_total + new_total);
650 let expected_new_null = total_in_bin * new_total / (ref_total + new_total);
651 if expected_ref_null > 5.0 && expected_new_null > 5.0 {
652 chi_square += (observed_ref - expected_ref_null).powi(2) / expected_ref_null;
653 chi_square += (observed_new - expected_new_null).powi(2) / expected_new_null;
654 degrees_of_freedom += 1;
655 }
656 }
657 let p_value = if degrees_of_freedom > 0 {
658 self.chi_square_cdf_complement(chi_square, degrees_of_freedom as f64)
659 } else {
660 1.0
661 };
662 Ok((chi_square, p_value))
663 }
664 fn maximum_mean_discrepancy(&self, x: &ArrayView1<f64>, y: &ArrayView1<f64>) -> Result<f64> {
666 let x_clean: Vec<f64> = x.iter().filter(|&&v| v.is_finite()).copied().collect();
667 let y_clean: Vec<f64> = y.iter().filter(|&&v| v.is_finite()).copied().collect();
668 if x_clean.is_empty() || y_clean.is_empty() {
669 return Ok(0.0);
670 }
671 let n = x_clean.len();
672 let m = y_clean.len();
673 let all_data: Vec<f64> = x_clean.iter().chain(y_clean.iter()).copied().collect();
674 let mut sorted_data = all_data;
675 sorted_data.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
676 let median = sorted_data[sorted_data.len() / 2];
677 let mad: f64 =
678 sorted_data.iter().map(|&x| (x - median).abs()).sum::<f64>() / sorted_data.len() as f64;
679 let bandwidth = mad.max(1.0);
680 let mut kxx = 0.0;
681 let mut kyy = 0.0;
682 let mut kxy = 0.0;
683 if n > 1 {
684 for i in 0..n {
685 for j in (i + 1)..n {
686 kxx += self.rbf_kernel(x_clean[i], x_clean[j], bandwidth);
687 }
688 }
689 kxx = 2.0 * kxx / (n * (n - 1)) as f64;
690 }
691 if m > 1 {
692 for i in 0..m {
693 for j in (i + 1)..m {
694 kyy += self.rbf_kernel(y_clean[i], y_clean[j], bandwidth);
695 }
696 }
697 kyy = 2.0 * kyy / (m * (m - 1)) as f64;
698 }
699 for i in 0..n {
700 for j in 0..m {
701 kxy += self.rbf_kernel(x_clean[i], y_clean[j], bandwidth);
702 }
703 }
704 kxy /= (n * m) as f64;
705 let mmd_squared = kxx + kyy - 2.0 * kxy;
706 Ok(mmd_squared.max(0.0).sqrt())
707 }
708 fn rbf_kernel(&self, x: f64, y: f64, bandwidth: f64) -> f64 {
710 let diff = x - y;
711 (-diff * diff / (2.0 * bandwidth * bandwidth)).exp()
712 }
713 fn chi_square_cdf_complement(&self, x: f64, df: f64) -> f64 {
715 if x <= 0.0 {
716 return 1.0;
717 }
718 if df <= 0.0 {
719 return 0.0;
720 }
721 if df >= 30.0 {
722 let h = 2.0 / (9.0 * df);
723 let z = ((x / df).powf(1.0 / 3.0) - (1.0 - h)) / h.sqrt();
724 return 0.5 * (1.0 - self.erf(z / 2.0_f64.sqrt()));
725 }
726 let alpha = df / 2.0;
727 let x_half = x / 2.0;
728 if x_half < alpha + 1.0 {
729 let mut term = x_half.powf(alpha) * (-x_half).exp();
730 let mut sum = term;
731 for k in 1..=50 {
732 term *= x_half / (alpha + k as f64);
733 sum += term;
734 if term / sum < 1e-10 {
735 break;
736 }
737 }
738 let gamma_cdf = sum / self.gamma(alpha);
739 1.0 - gamma_cdf.min(1.0)
740 } else {
741 let a = alpha;
742 let b = x_half + 1.0 - a;
743 let c = 1e30;
744 let mut d = 1.0 / b;
745 let mut h = d;
746 for i in 1..=100 {
747 let an = -i as f64 * (i as f64 - a);
748 let b = b + 2.0;
749 d = an * d + b;
750 if d.abs() < 1e-30 {
751 d = 1e-30;
752 }
753 let mut c = b + an / c;
754 if c.abs() < 1e-30 {
755 c = 1e-30;
756 }
757 d = 1.0 / d;
758 let del = d * c;
759 h *= del;
760 if (del - 1.0).abs() < 1e-10 {
761 break;
762 }
763 }
764 let gamma_cf = (-x_half).exp() * x_half.powf(a) * h / self.gamma(a);
765 gamma_cf.clamp(0.0, 1.0)
766 }
767 }
768 fn erf(&self, x: f64) -> f64 {
770 let a1 = 0.254829592;
771 let a2 = -0.284496736;
772 let a3 = 1.421413741;
773 let a4 = -1.453152027;
774 let a5 = 1.061405429;
775 let p = 0.3275911;
776 let sign = if x >= 0.0 { 1.0 } else { -1.0 };
777 let x = x.abs();
778 let t = 1.0 / (1.0 + p * x);
779 let y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * (-x * x).exp();
780 sign * y
781 }
782 fn gamma(&self, z: f64) -> f64 {
784 if z < 0.5 {
785 std::f64::consts::PI / ((std::f64::consts::PI * z).sin() * self.gamma(1.0 - z))
786 } else {
787 let g = 7.0;
788 let c = [
789 0.99999999999980993,
790 676.5203681218851,
791 -1259.1392167224028,
792 771.32342877765313,
793 -176.61502916214059,
794 12.507343278686905,
795 -0.13857109526572012,
796 9.9843695780195716e-6,
797 1.5056327351493116e-7,
798 ];
799 let z = z - 1.0;
800 let mut x = c[0];
801 for i in 1..c.len() {
802 x += c[i] / (z + i as f64);
803 }
804 let t = z + g + 0.5;
805 (2.0 * std::f64::consts::PI).sqrt() * t.powf(z + 0.5) * (-t).exp() * x
806 }
807 }
808 fn check_performance_alerts(&mut self, metrics: &PerformanceMetrics) -> Result<Vec<AlertType>> {
809 let mut alerts = Vec::new();
810 let current_time = current_timestamp();
811 let cooldown_key = "performance";
812 if let Some(&last_alert_time) = self.last_alert_times.get(cooldown_key) {
813 if current_time - last_alert_time < self.alert_config.cooldown_seconds {
814 return Ok(alerts);
815 }
816 }
817 if let Some(ref baseline) = self.baseline_metrics {
818 let degradation_ratio = metrics.processing_time_ms / baseline.processing_time_ms;
819 if degradation_ratio > self.alert_config.performance_threshold {
820 alerts.push(AlertType::PerformanceDegradation {
821 metric: "processing_time".to_string(),
822 value: degradation_ratio,
823 });
824 }
825 }
826 if metrics.error_rate > self.alert_config.error_rate_threshold {
827 alerts.push(AlertType::HighErrorRate {
828 rate: metrics.error_rate,
829 });
830 }
831 if metrics.memory_usage_mb > self.alert_config.memory_threshold_mb {
832 alerts.push(AlertType::MemoryExhaustion {
833 usage_mb: metrics.memory_usage_mb,
834 });
835 }
836 if metrics.data_quality_score < 0.8 {
837 alerts.push(AlertType::DataQualityIssue {
838 score: metrics.data_quality_score,
839 });
840 }
841 if !alerts.is_empty() {
842 self.last_alert_times
843 .insert(cooldown_key.to_string(), current_time);
844 }
845 Ok(alerts)
846 }
847 #[cfg(feature = "monitoring")]
849 pub fn export_prometheus_metrics(&self) -> Result<String> {
850 use prometheus::Encoder;
851 let encoder = prometheus::TextEncoder::new();
852 let metric_families = self.metrics_registry.gather();
853 encoder.encode_to_string(&metric_families).map_err(|e| {
854 TransformError::ComputationError(format!("Failed to encode metrics: {}", e))
855 })
856 }
857}
858#[allow(dead_code)]
859fn current_timestamp() -> u64 {
860 SystemTime::now()
861 .duration_since(UNIX_EPOCH)
862 .unwrap_or_else(|_| Duration::from_secs(0))
863 .as_secs()
864}
865#[cfg(feature = "monitoring")]
867pub struct AdvancedAnomalyDetector {
868 statistical_detectors: HashMap<String, StatisticalDetector>,
870 ml_detectors: HashMap<String, MLAnomalyDetector>,
872 time_series_detectors: HashMap<String, TimeSeriesAnomalyDetector>,
874 ensemble_detector: Option<EnsembleAnomalyDetector>,
876 anomaly_history: VecDeque<AnomalyRecord>,
878 thresholds: AnomalyThresholds,
880}
881#[cfg(feature = "monitoring")]
883#[derive(Debug, Clone)]
884pub struct StatisticalDetector {
885 z_score_threshold: f64,
887 iqr_multiplier: f64,
889 modified_z_threshold: f64,
891 data_window: VecDeque<f64>,
893 max_window_size: usize,
895}
896#[cfg(feature = "monitoring")]
898pub struct MLAnomalyDetector {
899 isolation_forest_config: IsolationForestConfig,
901 svm_config: OneClassSVMConfig,
903 lof_config: LOFConfig,
905 training_data: VecDeque<Vec<f64>>,
907 model_trained: bool,
909}
910#[cfg(feature = "monitoring")]
912pub struct TimeSeriesAnomalyDetector {
913 arima_config: ARIMAConfig,
915 seasonal_config: SeasonalConfig,
917 change_point_config: ChangePointConfig,
919 time_series_data: VecDeque<TimeSeriesPoint>,
921 forecast_model: Option<ForecastModel>,
923}
924#[cfg(feature = "monitoring")]
926pub struct EnsembleAnomalyDetector {
927 detector_weights: HashMap<String, f64>,
929 voting_threshold: f64,
931 confidence_threshold: f64,
933}
934#[cfg(feature = "monitoring")]
935impl EnsembleAnomalyDetector {
936 pub fn new(
938 detector_weights: HashMap<String, f64>,
939 voting_threshold: f64,
940 confidence_threshold: f64,
941 ) -> Self {
942 EnsembleAnomalyDetector {
943 detector_weights,
944 voting_threshold,
945 confidence_threshold,
946 }
947 }
948 pub fn detect_ensemble_anomalies(
972 &self,
973 metrics: &HashMap<String, f64>,
974 detector_results: &HashMap<String, HashMap<String, AnomalyRecord>>,
975 timestamp: u64,
976 ) -> Result<Vec<AnomalyRecord>> {
977 let mut ensemble_anomalies = Vec::new();
978 let total_weight: f64 = self.detector_weights.values().sum();
979 if total_weight <= 0.0 {
980 return Ok(ensemble_anomalies);
981 }
982 for (metric_name, &value) in metrics {
983 let Some(per_detector) = detector_results.get(metric_name) else {
984 continue;
985 };
986 if per_detector.is_empty() {
987 continue;
988 }
989 let mut flagged_weight = 0.0;
990 let mut score_sum = 0.0;
991 for (detector_name, record) in per_detector {
992 flagged_weight += self
993 .detector_weights
994 .get(detector_name)
995 .copied()
996 .unwrap_or(0.0);
997 score_sum += record.anomaly_score;
998 }
999 let vote_fraction = flagged_weight / total_weight;
1000 if vote_fraction < self.voting_threshold {
1001 continue;
1002 }
1003 let mean_score = score_sum / per_detector.len() as f64;
1004 let confidence = 1.0 - (-mean_score.max(0.0)).exp();
1005 if confidence < self.confidence_threshold {
1006 continue;
1007 }
1008 let mut contributing: Vec<&str> = per_detector.keys().map(|s| s.as_str()).collect();
1009 contributing.sort_unstable();
1010 let severity = if vote_fraction >= 0.9 {
1011 AnomalySeverity::Critical
1012 } else if vote_fraction >= 0.75 {
1013 AnomalySeverity::High
1014 } else if vote_fraction >= 0.6 {
1015 AnomalySeverity::Medium
1016 } else {
1017 AnomalySeverity::Low
1018 };
1019 ensemble_anomalies.push(AnomalyRecord {
1020 timestamp,
1021 metric_name: metric_name.clone(),
1022 value,
1023 anomaly_score: vote_fraction,
1024 detection_method: "ensemble_weighted_vote".to_string(),
1025 severity,
1026 context: [
1027 ("vote_fraction".to_string(), vote_fraction.to_string()),
1028 ("confidence".to_string(), confidence.to_string()),
1029 ("contributing_detectors".to_string(), contributing.join(",")),
1030 ]
1031 .into_iter()
1032 .collect(),
1033 });
1034 }
1035 Ok(ensemble_anomalies)
1036 }
1037}
1038#[cfg(feature = "monitoring")]
1040#[derive(Debug, Clone)]
1041pub struct AnomalyRecord {
1042 pub timestamp: u64,
1044 pub metric_name: String,
1046 pub value: f64,
1048 pub anomaly_score: f64,
1050 pub detection_method: String,
1052 pub severity: AnomalySeverity,
1054 pub context: HashMap<String, String>,
1056}
1057#[cfg(feature = "monitoring")]
1059#[derive(Debug, Clone, PartialEq)]
1060pub enum AnomalySeverity {
1061 Low,
1063 Medium,
1065 High,
1067 Critical,
1069}
1070#[cfg(feature = "monitoring")]
1072#[derive(Debug, Clone)]
1073pub struct AnomalyThresholds {
1074 pub low_threshold: f64,
1076 pub medium_threshold: f64,
1078 pub high_threshold: f64,
1080 pub critical_threshold: f64,
1082}
1083impl Default for AnomalyThresholds {
1084 fn default() -> Self {
1085 AnomalyThresholds {
1086 low_threshold: 2.0,
1087 medium_threshold: 2.5,
1088 high_threshold: 3.0,
1089 critical_threshold: 4.0,
1090 }
1091 }
1092}
1093#[cfg(feature = "monitoring")]
1095#[derive(Debug, Clone)]
1096pub struct TimeSeriesPoint {
1097 pub timestamp: u64,
1099 pub value: f64,
1101 pub metadata: HashMap<String, String>,
1103}
1104#[cfg(feature = "monitoring")]
1106#[derive(Debug, Clone)]
1107pub struct IsolationForestConfig {
1108 pub n_trees: usize,
1110 pub contamination: f64,
1112 pub max_samples: usize,
1114}
1115#[cfg(feature = "monitoring")]
1117#[derive(Debug, Clone)]
1118pub struct OneClassSVMConfig {
1119 pub nu: f64,
1121 pub gamma: f64,
1123 pub kernel: String,
1125}
1126#[cfg(feature = "monitoring")]
1128#[derive(Debug, Clone)]
1129pub struct LOFConfig {
1130 pub n_neighbors: usize,
1132 pub contamination: f64,
1134}
1135#[cfg(feature = "monitoring")]
1137#[derive(Debug, Clone)]
1138pub struct ARIMAConfig {
1139 pub p: usize,
1141 pub d: usize,
1143 pub q: usize,
1145}
1146#[cfg(feature = "monitoring")]
1148#[derive(Debug, Clone)]
1149pub struct SeasonalConfig {
1150 pub seasonal_period: usize,
1152 pub trend_component: bool,
1154 pub seasonal_component: bool,
1156}
1157#[cfg(feature = "monitoring")]
1159#[derive(Debug, Clone)]
1160pub struct ChangePointConfig {
1161 pub window_size: usize,
1163 pub significance_level: f64,
1165}
1166#[cfg(feature = "monitoring")]
1168#[derive(Debug, Clone)]
1169pub struct ForecastModel {
1170 pub coefficients: Vec<f64>,
1172 pub forecast_horizon: usize,
1174 pub confidence_interval: f64,
1176}
1177#[cfg(feature = "monitoring")]
1178impl AdvancedAnomalyDetector {
1179 pub fn new() -> Self {
1181 AdvancedAnomalyDetector {
1182 statistical_detectors: HashMap::new(),
1183 ml_detectors: HashMap::new(),
1184 time_series_detectors: HashMap::new(),
1185 ensemble_detector: None,
1186 anomaly_history: VecDeque::with_capacity(10000),
1187 thresholds: AnomalyThresholds::default(),
1188 }
1189 }
1190 pub fn add_statistical_detector(&mut self, metricname: String, detector: StatisticalDetector) {
1192 self.statistical_detectors.insert(metricname, detector);
1193 }
1194 pub fn add_ml_detector(&mut self, metricname: String, detector: MLAnomalyDetector) {
1196 self.ml_detectors.insert(metricname, detector);
1197 }
1198 pub fn add_time_series_detector(
1200 &mut self,
1201 metric_name: String,
1202 detector: TimeSeriesAnomalyDetector,
1203 ) {
1204 self.time_series_detectors.insert(metric_name, detector);
1205 }
1206 pub fn configure_ensemble(&mut self, detector: EnsembleAnomalyDetector) {
1208 self.ensemble_detector = Some(detector);
1209 }
1210 pub fn detect_anomalies(
1212 &mut self,
1213 metrics: &HashMap<String, f64>,
1214 ) -> Result<Vec<AnomalyRecord>> {
1215 let mut anomalies = Vec::new();
1216 let mut detector_results: HashMap<String, HashMap<String, AnomalyRecord>> = HashMap::new();
1217 let timestamp = current_timestamp();
1218 for (metric_name, &value) in metrics {
1219 if let Some(detector) = self.statistical_detectors.get_mut(metric_name) {
1220 if let Some(anomaly) = detector.detect_anomaly(value, metric_name, timestamp)? {
1221 detector_results
1222 .entry(metric_name.clone())
1223 .or_default()
1224 .insert("statistical".to_string(), anomaly.clone());
1225 anomalies.push(anomaly);
1226 }
1227 }
1228 if let Some(detector) = self.ml_detectors.get_mut(metric_name) {
1229 if let Some(anomaly) = detector.detect_anomaly(value, metric_name, timestamp)? {
1230 detector_results
1231 .entry(metric_name.clone())
1232 .or_default()
1233 .insert("ml".to_string(), anomaly.clone());
1234 anomalies.push(anomaly);
1235 }
1236 }
1237 if let Some(detector) = self.time_series_detectors.get_mut(metric_name) {
1238 if let Some(anomaly) = detector.detect_anomaly(value, metric_name, timestamp)? {
1239 detector_results
1240 .entry(metric_name.clone())
1241 .or_default()
1242 .insert("time_series".to_string(), anomaly.clone());
1243 anomalies.push(anomaly);
1244 }
1245 }
1246 }
1247 if let Some(ref ensemble) = self.ensemble_detector {
1248 let ensemble_anomalies =
1249 ensemble.detect_ensemble_anomalies(metrics, &detector_results, timestamp)?;
1250 anomalies.extend(ensemble_anomalies);
1251 }
1252 for anomaly in &anomalies {
1253 self.anomaly_history.push_back(anomaly.clone());
1254 if self.anomaly_history.len() > 10000 {
1255 self.anomaly_history.pop_front();
1256 }
1257 }
1258 Ok(anomalies)
1259 }
1260 pub fn get_anomaly_insights(&self, lookbackhours: u64) -> AnomalyInsights {
1262 let cutoff_time = current_timestamp() - (lookbackhours * 3600);
1263 let recent_anomalies: Vec<_> = self
1264 .anomaly_history
1265 .iter()
1266 .filter(|a| a.timestamp >= cutoff_time)
1267 .collect();
1268 let total_anomalies = recent_anomalies.len();
1269 let critical_anomalies = recent_anomalies
1270 .iter()
1271 .filter(|a| a.severity == AnomalySeverity::Critical)
1272 .count();
1273 let mut metric_frequencies = HashMap::new();
1274 for anomaly in &recent_anomalies {
1275 *metric_frequencies
1276 .entry(anomaly.metric_name.clone())
1277 .or_insert(0) += 1;
1278 }
1279 let mut method_frequencies = HashMap::new();
1280 for anomaly in &recent_anomalies {
1281 *method_frequencies
1282 .entry(anomaly.detection_method.clone())
1283 .or_insert(0) += 1;
1284 }
1285 let trending_metrics = self.identify_trending_anomalies(&recent_anomalies);
1286 let most_anomalous_metric = metric_frequencies
1287 .iter()
1288 .max_by_key(|(_, &count)| count)
1289 .map(|(metric_, _)| metric_.clone());
1290 AnomalyInsights {
1291 total_anomalies,
1292 critical_anomalies,
1293 anomaly_rate: total_anomalies as f64 / lookbackhours as f64,
1294 metric_frequencies,
1295 method_frequencies,
1296 trending_metrics,
1297 most_anomalous_metric,
1298 }
1299 }
1300 fn identify_trending_anomalies(&self, anomalies: &[&AnomalyRecord]) -> Vec<String> {
1302 let mut recent_counts = HashMap::new();
1303 let current_time = current_timestamp();
1304 let recent_threshold = 3600;
1305 for anomaly in anomalies {
1306 if current_time - anomaly.timestamp <= recent_threshold {
1307 *recent_counts
1308 .entry(anomaly.metric_name.clone())
1309 .or_insert(0) += 1;
1310 }
1311 }
1312 recent_counts
1313 .into_iter()
1314 .filter(|(_, count)| *count >= 3)
1315 .map(|(metric_, _)| metric_)
1316 .collect()
1317 }
1318 pub fn update_detector_configurations(&mut self, feedback: AnomalyFeedback) -> Result<()> {
1320 match feedback.feedback_type {
1321 FeedbackType::FalsePositive => {
1322 self.adjust_thresholds_for_detector(&feedback.detection_method, 0.1)?;
1323 }
1324 FeedbackType::FalseNegative => {
1325 self.adjust_thresholds_for_detector(&feedback.detection_method, -0.1)?;
1326 }
1327 FeedbackType::ConfirmedAnomaly => {}
1328 }
1329 Ok(())
1330 }
1331 fn adjust_thresholds_for_detector(
1332 &mut self,
1333 detection_method: &str,
1334 adjustment: f64,
1335 ) -> Result<()> {
1336 match detection_method {
1337 "statistical" => {
1338 for detector in self.statistical_detectors.values_mut() {
1339 detector.z_score_threshold += adjustment;
1340 detector.z_score_threshold = detector.z_score_threshold.clamp(1.5, 5.0);
1341 }
1342 }
1343 "ml" => {
1344 for detector in self.ml_detectors.values_mut() {
1345 detector.isolation_forest_config.contamination += adjustment * 0.01;
1346 detector.isolation_forest_config.contamination = detector
1347 .isolation_forest_config
1348 .contamination
1349 .max(0.01)
1350 .min(0.5);
1351 }
1352 }
1353 _ => {}
1354 }
1355 Ok(())
1356 }
1357}
1358#[cfg(feature = "monitoring")]
1359impl StatisticalDetector {
1360 pub fn new(z_score_threshold: f64, iqr_multiplier: f64, max_window_size: usize) -> Self {
1362 StatisticalDetector {
1363 z_score_threshold,
1364 iqr_multiplier,
1365 modified_z_threshold: z_score_threshold * 0.6745,
1366 data_window: VecDeque::with_capacity(max_window_size),
1367 max_window_size,
1368 }
1369 }
1370 pub fn detect_anomaly(
1372 &mut self,
1373 value: f64,
1374 metric_name: &str,
1375 timestamp: u64,
1376 ) -> Result<Option<AnomalyRecord>> {
1377 self.data_window.push_back(value);
1378 if self.data_window.len() > self.max_window_size {
1379 self.data_window.pop_front();
1380 }
1381 if self.data_window.len() < 10 {
1382 return Ok(None);
1383 }
1384 let values: Vec<f64> = self.data_window.iter().copied().collect();
1385 let mean = values.iter().sum::<f64>() / values.len() as f64;
1386 let variance = values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / values.len() as f64;
1387 let std_dev = variance.sqrt();
1388 if std_dev > 0.0 {
1389 let z_score = (value - mean) / std_dev;
1390 if z_score.abs() > self.z_score_threshold {
1391 let severity = if z_score.abs() > 4.0 {
1392 AnomalySeverity::Critical
1393 } else if z_score.abs() > 3.0 {
1394 AnomalySeverity::High
1395 } else if z_score.abs() > 2.5 {
1396 AnomalySeverity::Medium
1397 } else {
1398 AnomalySeverity::Low
1399 };
1400 return Ok(Some(AnomalyRecord {
1401 timestamp,
1402 metric_name: metric_name.to_string(),
1403 value,
1404 anomaly_score: z_score.abs(),
1405 detection_method: "statistical_zscore".to_string(),
1406 severity,
1407 context: [
1408 ("mean".to_string(), mean.to_string()),
1409 ("std_dev".to_string(), std_dev.to_string()),
1410 ("z_score".to_string(), z_score.to_string()),
1411 ]
1412 .iter()
1413 .cloned()
1414 .collect(),
1415 }));
1416 }
1417 }
1418 let mut sorted_values = values.clone();
1419 sorted_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1420 let q1_idx = sorted_values.len() / 4;
1421 let q3_idx = (3 * sorted_values.len()) / 4;
1422 let q1 = sorted_values[q1_idx];
1423 let q3 = sorted_values[q3_idx];
1424 let iqr = q3 - q1;
1425 if iqr > 0.0 {
1426 let lower_bound = q1 - self.iqr_multiplier * iqr;
1427 let upper_bound = q3 + self.iqr_multiplier * iqr;
1428 if value < lower_bound || value > upper_bound {
1429 let distance_from_bounds = if value < lower_bound {
1430 lower_bound - value
1431 } else {
1432 value - upper_bound
1433 };
1434 let severity = if distance_from_bounds > 3.0 * iqr {
1435 AnomalySeverity::Critical
1436 } else if distance_from_bounds > 2.0 * iqr {
1437 AnomalySeverity::High
1438 } else if distance_from_bounds > 1.5 * iqr {
1439 AnomalySeverity::Medium
1440 } else {
1441 AnomalySeverity::Low
1442 };
1443 return Ok(Some(AnomalyRecord {
1444 timestamp,
1445 metric_name: metric_name.to_string(),
1446 value,
1447 anomaly_score: distance_from_bounds / iqr,
1448 detection_method: "statistical_iqr".to_string(),
1449 severity,
1450 context: [
1451 ("q1".to_string(), q1.to_string()),
1452 ("q3".to_string(), q3.to_string()),
1453 ("iqr".to_string(), iqr.to_string()),
1454 (
1455 "distance_from_bounds".to_string(),
1456 distance_from_bounds.to_string(),
1457 ),
1458 ]
1459 .iter()
1460 .cloned()
1461 .collect(),
1462 }));
1463 }
1464 }
1465 Ok(None)
1466 }
1467}
1468#[cfg(feature = "monitoring")]
1469impl MLAnomalyDetector {
1470 pub fn new() -> Self {
1472 MLAnomalyDetector {
1473 isolation_forest_config: IsolationForestConfig {
1474 n_trees: 100,
1475 contamination: 0.1,
1476 max_samples: 256,
1477 },
1478 svm_config: OneClassSVMConfig {
1479 nu: 0.1,
1480 gamma: 0.1,
1481 kernel: "rbf".to_string(),
1482 },
1483 lof_config: LOFConfig {
1484 n_neighbors: 20,
1485 contamination: 0.1,
1486 },
1487 training_data: VecDeque::with_capacity(1000),
1488 model_trained: false,
1489 }
1490 }
1491 pub fn detect_anomaly(
1493 &mut self,
1494 value: f64,
1495 metric_name: &str,
1496 timestamp: u64,
1497 ) -> Result<Option<AnomalyRecord>> {
1498 self.training_data.push_back(vec![value]);
1499 if self.training_data.len() > 1000 {
1500 self.training_data.pop_front();
1501 }
1502 if self.training_data.len() < 50 {
1503 return Ok(None);
1504 }
1505 let anomaly_score = self.isolation_forest_score(value)?;
1506 let threshold = 1.0 - self.isolation_forest_config.contamination;
1507 if anomaly_score > threshold {
1508 let severity = if anomaly_score > 0.9 {
1509 AnomalySeverity::Critical
1510 } else if anomaly_score > 0.8 {
1511 AnomalySeverity::High
1512 } else if anomaly_score > 0.7 {
1513 AnomalySeverity::Medium
1514 } else {
1515 AnomalySeverity::Low
1516 };
1517 return Ok(Some(AnomalyRecord {
1518 timestamp,
1519 metric_name: metric_name.to_string(),
1520 value,
1521 anomaly_score,
1522 detection_method: "ml_isolation_forest".to_string(),
1523 severity,
1524 context: [
1525 ("isolation_score".to_string(), anomaly_score.to_string()),
1526 (
1527 "training_samples".to_string(),
1528 self.training_data.len().to_string(),
1529 ),
1530 (
1531 "n_trees".to_string(),
1532 self.isolation_forest_config.n_trees.to_string(),
1533 ),
1534 ]
1535 .iter()
1536 .cloned()
1537 .collect(),
1538 }));
1539 }
1540 Ok(None)
1541 }
1542 fn isolation_forest_score(&self, value: f64) -> Result<f64> {
1552 use scirs2_core::random::{rng, Rng};
1553 let data: Vec<f64> = self.training_data.iter().map(|v| v[0]).collect();
1554 let n = data.len();
1555 if n < 2 {
1556 return Ok(0.5);
1557 }
1558 let sample_size = self.isolation_forest_config.max_samples.clamp(2, n);
1559 let max_depth = (sample_size as f64).log2().ceil().max(1.0) as usize;
1560 let mut rng = rng();
1561 let mut total_path_length = 0.0;
1562 for _ in 0..self.isolation_forest_config.n_trees.max(1) {
1563 let sample = subsample(&data, sample_size, &mut rng);
1564 let tree = IsolationNode::build(&sample, 0, max_depth, &mut rng);
1565 total_path_length += tree.path_length(value, 0);
1566 }
1567 let avg_path_length =
1568 total_path_length / self.isolation_forest_config.n_trees.max(1) as f64;
1569 let c = average_path_length_correction(sample_size);
1570 if c <= 0.0 {
1571 return Ok(0.5);
1572 }
1573 Ok(2f64.powf(-avg_path_length / c))
1574 }
1575}
1576enum IsolationNode {
1578 Leaf { size: usize },
1583 Split {
1585 split: f64,
1586 left: Box<IsolationNode>,
1587 right: Box<IsolationNode>,
1588 },
1589}
1590impl IsolationNode {
1591 fn build(
1592 data: &[f64],
1593 depth: usize,
1594 max_depth: usize,
1595 rng: &mut (impl scirs2_core::random::Rng + scirs2_core::random::RngExt),
1596 ) -> Self {
1597 if data.len() <= 1 || depth >= max_depth {
1598 return IsolationNode::Leaf { size: data.len() };
1599 }
1600 let min = data.iter().copied().fold(f64::INFINITY, f64::min);
1601 let max = data.iter().copied().fold(f64::NEG_INFINITY, f64::max);
1602 if !(max > min) {
1603 return IsolationNode::Leaf { size: data.len() };
1604 }
1605 let split = rng.random_range(min..max);
1606 let left: Vec<f64> = data.iter().copied().filter(|&v| v < split).collect();
1607 let right: Vec<f64> = data.iter().copied().filter(|&v| v >= split).collect();
1608 if left.is_empty() || right.is_empty() {
1609 return IsolationNode::Leaf { size: data.len() };
1610 }
1611 IsolationNode::Split {
1612 split,
1613 left: Box::new(Self::build(&left, depth + 1, max_depth, rng)),
1614 right: Box::new(Self::build(&right, depth + 1, max_depth, rng)),
1615 }
1616 }
1617 fn path_length(&self, value: f64, depth: usize) -> f64 {
1618 match self {
1619 IsolationNode::Leaf { size } => depth as f64 + average_path_length_correction(*size),
1620 IsolationNode::Split { split, left, right } => {
1621 if value < *split {
1622 left.path_length(value, depth + 1)
1623 } else {
1624 right.path_length(value, depth + 1)
1625 }
1626 }
1627 }
1628 }
1629}
1630fn average_path_length_correction(n: usize) -> f64 {
1637 if n <= 1 {
1638 0.0
1639 } else {
1640 const EULER_MASCHERONI: f64 = 0.5772156649015329;
1641 let n = n as f64;
1642 let harmonic_n_minus_1 = (n - 1.0).ln() + EULER_MASCHERONI;
1643 2.0 * harmonic_n_minus_1 - (2.0 * (n - 1.0) / n)
1644 }
1645}
1646fn subsample(
1649 data: &[f64],
1650 sample_size: usize,
1651 rng: &mut (impl scirs2_core::random::Rng + scirs2_core::random::RngExt),
1652) -> Vec<f64> {
1653 let n = data.len();
1654 let take = sample_size.min(n);
1655 let mut indices: Vec<usize> = (0..n).collect();
1656 for i in 0..take {
1657 let j = rng.random_range(i..n);
1658 indices.swap(i, j);
1659 }
1660 indices.truncate(take);
1661 indices.into_iter().map(|i| data[i]).collect()
1662}
1663#[cfg(feature = "monitoring")]
1664impl TimeSeriesAnomalyDetector {
1665 pub fn new() -> Self {
1667 TimeSeriesAnomalyDetector {
1668 arima_config: ARIMAConfig { p: 1, d: 1, q: 1 },
1669 seasonal_config: SeasonalConfig {
1670 seasonal_period: 24,
1671 trend_component: true,
1672 seasonal_component: true,
1673 },
1674 change_point_config: ChangePointConfig {
1675 window_size: 50,
1676 significance_level: 0.05,
1677 },
1678 time_series_data: VecDeque::with_capacity(1000),
1679 forecast_model: None,
1680 }
1681 }
1682 pub fn detect_anomaly(
1684 &mut self,
1685 value: f64,
1686 metric_name: &str,
1687 timestamp: u64,
1688 ) -> Result<Option<AnomalyRecord>> {
1689 self.time_series_data.push_back(TimeSeriesPoint {
1690 timestamp,
1691 value,
1692 metadata: HashMap::new(),
1693 });
1694 if self.time_series_data.len() > 1000 {
1695 self.time_series_data.pop_front();
1696 }
1697 if self.time_series_data.len() < 50 {
1698 return Ok(None);
1699 }
1700 let anomaly_score = self.detect_change_point(value)?;
1701 if anomaly_score > 2.0 {
1702 let severity = if anomaly_score > 5.0 {
1703 AnomalySeverity::Critical
1704 } else if anomaly_score > 4.0 {
1705 AnomalySeverity::High
1706 } else if anomaly_score > 3.0 {
1707 AnomalySeverity::Medium
1708 } else {
1709 AnomalySeverity::Low
1710 };
1711 return Ok(Some(AnomalyRecord {
1712 timestamp,
1713 metric_name: metric_name.to_string(),
1714 value,
1715 anomaly_score,
1716 detection_method: "time_series_change_point".to_string(),
1717 severity,
1718 context: [
1719 ("change_point_score".to_string(), anomaly_score.to_string()),
1720 (
1721 "window_size".to_string(),
1722 self.change_point_config.window_size.to_string(),
1723 ),
1724 ]
1725 .iter()
1726 .cloned()
1727 .collect(),
1728 }));
1729 }
1730 Ok(None)
1731 }
1732 fn detect_change_point(&self, current_value: f64) -> Result<f64> {
1734 let window_size = self
1735 .change_point_config
1736 .window_size
1737 .min(self.time_series_data.len());
1738 if window_size < 10 {
1739 return Ok(0.0);
1740 }
1741 let recent_data: Vec<f64> = self
1742 .time_series_data
1743 .iter()
1744 .rev()
1745 .take(window_size)
1746 .map(|p| p.value)
1747 .collect();
1748 let half_window = window_size / 2;
1749 let first_half: Vec<f64> = recent_data.iter().take(half_window).copied().collect();
1750 let second_half: Vec<f64> = recent_data.iter().skip(half_window).copied().collect();
1751 if first_half.is_empty() || second_half.is_empty() {
1752 return Ok(0.0);
1753 }
1754 let mean1 = first_half.iter().sum::<f64>() / first_half.len() as f64;
1755 let mean2 = second_half.iter().sum::<f64>() / second_half.len() as f64;
1756 let var1 =
1757 first_half.iter().map(|x| (x - mean1).powi(2)).sum::<f64>() / first_half.len() as f64;
1758 let var2 =
1759 second_half.iter().map(|x| (x - mean2).powi(2)).sum::<f64>() / second_half.len() as f64;
1760 let pooled_std = ((var1 + var2) / 2.0).sqrt();
1761 if pooled_std > 0.0 {
1762 let t_statistic =
1763 (mean2 - mean1).abs() / (pooled_std * (2.0_f64 / window_size as f64).sqrt());
1764 Ok(t_statistic)
1765 } else {
1766 Ok(0.0)
1767 }
1768 }
1769}
1770#[cfg(feature = "monitoring")]
1772#[derive(Debug, Clone)]
1773pub struct AnomalyInsights {
1774 pub total_anomalies: usize,
1776 pub critical_anomalies: usize,
1778 pub anomaly_rate: f64,
1780 pub metric_frequencies: HashMap<String, usize>,
1782 pub method_frequencies: HashMap<String, usize>,
1784 pub trending_metrics: Vec<String>,
1786 pub most_anomalous_metric: Option<String>,
1788}
1789#[cfg(feature = "monitoring")]
1791#[derive(Debug, Clone)]
1792pub struct AnomalyFeedback {
1793 pub anomaly_id: String,
1795 pub feedback_type: FeedbackType,
1797 pub detection_method: String,
1799 pub metric_name: String,
1801 pub timestamp: u64,
1803}
1804#[cfg(feature = "monitoring")]
1806#[derive(Debug, Clone)]
1807pub enum FeedbackType {
1808 FalsePositive,
1810 FalseNegative,
1812 ConfirmedAnomaly,
1814}
1815#[cfg(not(feature = "monitoring"))]
1816pub struct AdvancedAnomalyDetector;
1817#[cfg(not(feature = "monitoring"))]
1818pub struct AnomalyInsights;
1819
1820#[cfg(test)]
1821mod tests;