Skip to main content

scirs2_transform/monitoring/core/
mod.rs

1//! Production monitoring with drift detection and model degradation alerts
2//!
3//! This module provides comprehensive monitoring capabilities for transformation
4//! pipelines in production environments, including data drift detection,
5//! performance monitoring, and automated alerting.
6
7use 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/// Drift detection methods
15#[derive(Debug, Clone, PartialEq)]
16pub enum DriftMethod {
17    /// Kolmogorov-Smirnov test for continuous features
18    KolmogorovSmirnov,
19    /// Chi-square test for categorical features
20    ChiSquare,
21    /// Population Stability Index (PSI)
22    PopulationStabilityIndex,
23    /// Maximum Mean Discrepancy (MMD)
24    MaximumMeanDiscrepancy,
25    /// Wasserstein distance
26    WassersteinDistance,
27}
28/// Data drift detection result
29#[derive(Debug, Clone)]
30pub struct DriftDetectionResult {
31    /// Feature name or index
32    pub feature_name: String,
33    /// Drift detection method used
34    pub method: DriftMethod,
35    /// Test statistic value
36    pub statistic: f64,
37    /// P-value (if applicable)
38    pub p_value: Option<f64>,
39    /// Whether drift is detected
40    pub is_drift_detected: bool,
41    /// Severity level (0.0 = no drift, 1.0 = severe drift)
42    pub severity: f64,
43    /// Timestamp of detection
44    pub timestamp: u64,
45}
46/// Performance degradation metrics
47#[derive(Debug, Clone)]
48pub struct PerformanceMetrics {
49    /// Processing time in milliseconds
50    pub processing_time_ms: f64,
51    /// Memory usage in MB
52    pub memory_usage_mb: f64,
53    /// Error rate (0.0 to 1.0)
54    pub error_rate: f64,
55    /// Throughput (samples per second)
56    pub throughput: f64,
57    /// Data quality score (0.0 to 1.0)
58    pub data_quality_score: f64,
59    /// Timestamp
60    pub timestamp: u64,
61}
62/// Alert configuration
63#[derive(Debug, Clone)]
64pub struct AlertConfig {
65    /// Drift detection threshold
66    pub drift_threshold: f64,
67    /// Performance degradation threshold
68    pub performance_threshold: f64,
69    /// Error rate threshold
70    pub error_rate_threshold: f64,
71    /// Memory usage threshold in MB
72    pub memory_threshold_mb: f64,
73    /// Alert cooldown period in seconds
74    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/// Alert types
88#[derive(Debug, Clone)]
89pub enum AlertType {
90    /// Statistical drift detected in feature distribution
91    DataDrift {
92        /// Name of the drifting feature
93        feature: String,
94        /// Severity score of the drift (0.0 to 1.0)
95        severity: f64,
96    },
97    /// Performance degradation detected in metrics
98    PerformanceDegradation {
99        /// Name of the degraded metric
100        metric: String,
101        /// Current degraded value
102        value: f64,
103    },
104    /// Error rate exceeds acceptable threshold
105    HighErrorRate {
106        /// Current error rate (0.0 to 1.0)
107        rate: f64,
108    },
109    /// Memory usage approaching limits
110    MemoryExhaustion {
111        /// Current memory usage in megabytes
112        usage_mb: f64,
113    },
114    /// Data quality below acceptable standards
115    DataQualityIssue {
116        /// Quality score (0.0 to 1.0, lower is worse)
117        score: f64,
118    },
119}
120/// Production monitoring system
121pub struct TransformationMonitor {
122    /// Reference data for drift detection
123    reference_data: Option<Array2<f64>>,
124    /// Feature names
125    feature_names: Vec<String>,
126    /// Drift detection methods per feature
127    drift_methods: HashMap<String, DriftMethod>,
128    /// Historical performance metrics
129    performance_history: VecDeque<PerformanceMetrics>,
130    /// Historical drift results
131    drift_history: VecDeque<DriftDetectionResult>,
132    /// Alert configuration
133    alert_config: AlertConfig,
134    /// Last alert timestamps (for cooldown)
135    last_alert_times: HashMap<String, u64>,
136    /// Baseline performance metrics
137    baseline_metrics: Option<PerformanceMetrics>,
138    /// Prometheus metrics registry
139    #[cfg(feature = "monitoring")]
140    metrics_registry: Registry,
141    /// Prometheus counters and gauges
142    #[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    /// Create a new transformation monitor
156    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    /// Set reference data for drift detection
242    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    /// Configure drift detection method for a specific feature
267    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    /// Set alert configuration
278    pub fn set_alert_config(&mut self, config: AlertConfig) {
279        self.alert_config = config;
280    }
281    /// Set baseline performance metrics
282    pub fn set_baseline_metrics(&mut self, metrics: PerformanceMetrics) {
283        self.baseline_metrics = Some(metrics);
284    }
285    /// Detect data drift in new data
286    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    /// Record performance metrics
331    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    /// Get drift detection summary
353    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    /// Get performance trends
376    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    /// Chi-square test for categorical data drift detection
610    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    /// Maximum Mean Discrepancy (MMD) test for distribution comparison
665    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    /// RBF (Gaussian) kernel function
709    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    /// Complement of chi-square CDF using improved approximations
714    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    /// Error function approximation
769    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    /// Gamma function using Lanczos approximation
783    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    /// Export metrics in Prometheus format
848    #[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/// Advanced anomaly detection system
866#[cfg(feature = "monitoring")]
867pub struct AdvancedAnomalyDetector {
868    /// Statistical anomaly detectors
869    statistical_detectors: HashMap<String, StatisticalDetector>,
870    /// Machine learning anomaly detectors
871    ml_detectors: HashMap<String, MLAnomalyDetector>,
872    /// Time series anomaly detectors
873    time_series_detectors: HashMap<String, TimeSeriesAnomalyDetector>,
874    /// Ensemble anomaly detector
875    ensemble_detector: Option<EnsembleAnomalyDetector>,
876    /// Anomaly history for learning
877    anomaly_history: VecDeque<AnomalyRecord>,
878    /// Alert thresholds
879    thresholds: AnomalyThresholds,
880}
881/// Statistical anomaly detector using multiple statistical methods
882#[cfg(feature = "monitoring")]
883#[derive(Debug, Clone)]
884pub struct StatisticalDetector {
885    /// Z-score threshold
886    z_score_threshold: f64,
887    /// IQR multiplier
888    iqr_multiplier: f64,
889    /// Modified Z-score threshold
890    modified_z_threshold: f64,
891    /// Historical data window
892    data_window: VecDeque<f64>,
893    /// Maximum window size
894    max_window_size: usize,
895}
896/// Machine learning anomaly detector
897#[cfg(feature = "monitoring")]
898pub struct MLAnomalyDetector {
899    /// Isolation forest parameters
900    isolation_forest_config: IsolationForestConfig,
901    /// One-class SVM parameters
902    svm_config: OneClassSVMConfig,
903    /// Local outlier factor parameters
904    lof_config: LOFConfig,
905    /// Training data for ML models
906    training_data: VecDeque<Vec<f64>>,
907    /// Model state
908    model_trained: bool,
909}
910/// Time series anomaly detector
911#[cfg(feature = "monitoring")]
912pub struct TimeSeriesAnomalyDetector {
913    /// ARIMA parameters
914    arima_config: ARIMAConfig,
915    /// Seasonal decomposition parameters
916    seasonal_config: SeasonalConfig,
917    /// Change point detection parameters
918    change_point_config: ChangePointConfig,
919    /// Historical time series data
920    time_series_data: VecDeque<TimeSeriesPoint>,
921    /// Forecast model
922    forecast_model: Option<ForecastModel>,
923}
924/// Ensemble anomaly detector combining multiple methods
925#[cfg(feature = "monitoring")]
926pub struct EnsembleAnomalyDetector {
927    /// Detector weights
928    detector_weights: HashMap<String, f64>,
929    /// Voting threshold
930    voting_threshold: f64,
931    /// Confidence threshold
932    confidence_threshold: f64,
933}
934#[cfg(feature = "monitoring")]
935impl EnsembleAnomalyDetector {
936    /// Create a new ensemble anomaly detector
937    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    /// Detect ensemble anomalies by combining the *actual* results of the
949    /// individual member detectors that ran for each metric.
950    ///
951    /// `detector_results` maps `metric_name -> (detector_name -> the
952    /// AnomalyRecord that detector produced for this metric)`; a detector
953    /// that did not flag a metric (or was never configured for it) simply
954    /// has no entry, rather than a fabricated zero. The expected
955    /// `detector_name` keys match the private `detector_weights` field (by
956    /// convention, `"statistical"`, `"ml"`, `"time_series"` -- see
957    /// [`AdvancedAnomalyDetector::detect_anomalies`], the sole real caller,
958    /// which builds this map from its own per-metric detector runs).
959    ///
960    /// For each metric with at least one flagging detector:
961    /// - `vote_fraction` = (sum of weights of flagging detectors) / (sum of
962    ///   all configured weights). Must be `>= self.voting_threshold`.
963    /// - `confidence` = a saturating `[0, 1)` transform of the mean raw
964    ///   `anomaly_score` reported by the flagging detectors (their scores
965    ///   live on different, unbounded scales -- z-score, IQR-distance-ratio,
966    ///   etc. -- so this puts them on a common, comparable footing). Must
967    ///   be `>= self.confidence_threshold`.
968    ///
969    /// Only metrics that clear *both* real, configured thresholds produce an
970    /// ensemble `AnomalyRecord` (`detection_method = "ensemble_weighted_vote"`).
971    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/// Anomaly record for historical analysis
1039#[cfg(feature = "monitoring")]
1040#[derive(Debug, Clone)]
1041pub struct AnomalyRecord {
1042    /// Timestamp
1043    pub timestamp: u64,
1044    /// Metric name
1045    pub metric_name: String,
1046    /// Anomaly value
1047    pub value: f64,
1048    /// Anomaly score
1049    pub anomaly_score: f64,
1050    /// Detection method
1051    pub detection_method: String,
1052    /// Severity level
1053    pub severity: AnomalySeverity,
1054    /// Context information
1055    pub context: HashMap<String, String>,
1056}
1057/// Anomaly severity levels
1058#[cfg(feature = "monitoring")]
1059#[derive(Debug, Clone, PartialEq)]
1060pub enum AnomalySeverity {
1061    /// Low severity - informational anomaly
1062    Low,
1063    /// Medium severity - notable deviation
1064    Medium,
1065    /// High severity - significant anomaly requiring attention
1066    High,
1067    /// Critical severity - severe anomaly requiring immediate action
1068    Critical,
1069}
1070/// Anomaly detection thresholds
1071#[cfg(feature = "monitoring")]
1072#[derive(Debug, Clone)]
1073pub struct AnomalyThresholds {
1074    /// Low severity threshold
1075    pub low_threshold: f64,
1076    /// Medium severity threshold
1077    pub medium_threshold: f64,
1078    /// High severity threshold
1079    pub high_threshold: f64,
1080    /// Critical severity threshold
1081    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/// Time series data point
1094#[cfg(feature = "monitoring")]
1095#[derive(Debug, Clone)]
1096pub struct TimeSeriesPoint {
1097    /// Unix timestamp in milliseconds
1098    pub timestamp: u64,
1099    /// Numeric value at this timestamp
1100    pub value: f64,
1101    /// Additional metadata key-value pairs
1102    pub metadata: HashMap<String, String>,
1103}
1104/// Configuration structures for various anomaly detection methods
1105#[cfg(feature = "monitoring")]
1106#[derive(Debug, Clone)]
1107pub struct IsolationForestConfig {
1108    /// Number of isolation trees in the forest
1109    pub n_trees: usize,
1110    /// Expected proportion of outliers (0.0 to 0.5)
1111    pub contamination: f64,
1112    /// Maximum number of samples to use per tree
1113    pub max_samples: usize,
1114}
1115/// Configuration for One-Class SVM anomaly detection
1116#[cfg(feature = "monitoring")]
1117#[derive(Debug, Clone)]
1118pub struct OneClassSVMConfig {
1119    /// Upper bound on the fraction of training errors (0 < nu <= 1)
1120    pub nu: f64,
1121    /// Kernel coefficient for RBF kernel
1122    pub gamma: f64,
1123    /// Kernel type (e.g., "rbf", "linear", "poly")
1124    pub kernel: String,
1125}
1126/// Configuration for Local Outlier Factor (LOF) detection
1127#[cfg(feature = "monitoring")]
1128#[derive(Debug, Clone)]
1129pub struct LOFConfig {
1130    /// Number of neighbors to use for LOF computation
1131    pub n_neighbors: usize,
1132    /// Expected proportion of outliers in the dataset
1133    pub contamination: f64,
1134}
1135/// Configuration for ARIMA (AutoRegressive Integrated Moving Average) model
1136#[cfg(feature = "monitoring")]
1137#[derive(Debug, Clone)]
1138pub struct ARIMAConfig {
1139    /// Order of the autoregressive (AR) component
1140    pub p: usize,
1141    /// Degree of differencing (integration order)
1142    pub d: usize,
1143    /// Order of the moving average (MA) component
1144    pub q: usize,
1145}
1146/// Configuration for seasonal time series decomposition
1147#[cfg(feature = "monitoring")]
1148#[derive(Debug, Clone)]
1149pub struct SeasonalConfig {
1150    /// Length of the seasonal cycle (e.g., 12 for monthly data with yearly seasonality)
1151    pub seasonal_period: usize,
1152    /// Whether to include trend component in decomposition
1153    pub trend_component: bool,
1154    /// Whether to include seasonal component in decomposition
1155    pub seasonal_component: bool,
1156}
1157/// Configuration for change point detection
1158#[cfg(feature = "monitoring")]
1159#[derive(Debug, Clone)]
1160pub struct ChangePointConfig {
1161    /// Size of the sliding window for detection
1162    pub window_size: usize,
1163    /// Statistical significance level threshold
1164    pub significance_level: f64,
1165}
1166/// Time series forecasting model configuration
1167#[cfg(feature = "monitoring")]
1168#[derive(Debug, Clone)]
1169pub struct ForecastModel {
1170    /// Model coefficients for prediction
1171    pub coefficients: Vec<f64>,
1172    /// Number of time steps to forecast ahead
1173    pub forecast_horizon: usize,
1174    /// Confidence interval for predictions (e.g., 0.95 for 95%)
1175    pub confidence_interval: f64,
1176}
1177#[cfg(feature = "monitoring")]
1178impl AdvancedAnomalyDetector {
1179    /// Create a new advanced anomaly detector
1180    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    /// Add a statistical detector for a metric
1191    pub fn add_statistical_detector(&mut self, metricname: String, detector: StatisticalDetector) {
1192        self.statistical_detectors.insert(metricname, detector);
1193    }
1194    /// Add a machine learning detector for a metric
1195    pub fn add_ml_detector(&mut self, metricname: String, detector: MLAnomalyDetector) {
1196        self.ml_detectors.insert(metricname, detector);
1197    }
1198    /// Add a time series detector for a metric
1199    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    /// Configure ensemble detector
1207    pub fn configure_ensemble(&mut self, detector: EnsembleAnomalyDetector) {
1208        self.ensemble_detector = Some(detector);
1209    }
1210    /// Detect anomalies in new data
1211    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    /// Get anomaly patterns and insights
1261    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    /// Identify trending anomalies
1301    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    /// Update detector configurations based on feedback
1319    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    /// Create a new statistical detector
1361    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    /// Detect anomaly using statistical methods
1371    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    /// Create a new ML anomaly detector
1471    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    /// Detect anomaly using ML methods
1492    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    /// Real Isolation Forest anomaly score for `value` against the current
1543    /// training window, following Liu, Ting & Zhou (2008): build
1544    /// `n_trees` isolation trees (each over an independent random
1545    /// sub-sample of up to `max_samples` points, height-limited to
1546    /// `ceil(log2(subsample_size))`), measure `value`'s average path
1547    /// length to isolation across all trees, and normalize it by `c(n)`
1548    /// (the expected path length of an unsuccessful BST search over `n`
1549    /// points) into the standard `2^(-E[h(x)] / c(n))` score in `[0, 1]`
1550    /// (values near 1 are anomalous, near 0.5 are typical).
1551    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}
1576/// A node of a single isolation tree over 1-D data.
1577enum IsolationNode {
1578    /// A leaf reached by exhausting the depth budget or isolating down to a
1579    /// single point; `size` is the number of points still in this node
1580    /// (>1 only when the depth limit was hit first), used for the standard
1581    /// `c(size)` path-length correction.
1582    Leaf { size: usize },
1583    /// An internal split: values `< split` go left, others go right.
1584    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}
1630/// `c(n)`: expected path length of an unsuccessful search in a Binary
1631/// Search Tree built from `n` points (Liu, Ting & Zhou 2008, eq. 1),
1632/// used to normalize raw isolation-tree path lengths into a comparable
1633/// anomaly score. `H(i)` (harmonic number) is approximated by
1634/// `ln(i) + gamma` (Euler-Mascheroni), the same approximation used in the
1635/// original paper for non-trivial `n`.
1636fn 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}
1646/// Draw a uniform random sub-sample of `sample_size` values from `data`
1647/// without replacement (Fisher-Yates partial shuffle).
1648fn 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    /// Create a new time series anomaly detector
1666    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    /// Detect anomaly using time series methods
1683    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    /// Simple change point detection
1733    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/// Anomaly insights summary
1771#[cfg(feature = "monitoring")]
1772#[derive(Debug, Clone)]
1773pub struct AnomalyInsights {
1774    /// Total number of detected anomalies
1775    pub total_anomalies: usize,
1776    /// Number of critical severity anomalies
1777    pub critical_anomalies: usize,
1778    /// Rate of anomalies relative to total data points
1779    pub anomaly_rate: f64,
1780    /// Frequency count of anomalies per metric name
1781    pub metric_frequencies: HashMap<String, usize>,
1782    /// Frequency count of anomalies per detection method
1783    pub method_frequencies: HashMap<String, usize>,
1784    /// Metrics with increasing anomaly trends
1785    pub trending_metrics: Vec<String>,
1786    /// Metric with the highest anomaly count
1787    pub most_anomalous_metric: Option<String>,
1788}
1789/// Feedback for anomaly detection tuning
1790#[cfg(feature = "monitoring")]
1791#[derive(Debug, Clone)]
1792pub struct AnomalyFeedback {
1793    /// Unique identifier for the anomaly
1794    pub anomaly_id: String,
1795    /// Type of feedback (false positive, false negative, confirmed)
1796    pub feedback_type: FeedbackType,
1797    /// Detection method that identified this anomaly
1798    pub detection_method: String,
1799    /// Name of the metric that triggered detection
1800    pub metric_name: String,
1801    /// Unix timestamp when feedback was provided
1802    pub timestamp: u64,
1803}
1804/// Type of feedback for anomaly detection accuracy
1805#[cfg(feature = "monitoring")]
1806#[derive(Debug, Clone)]
1807pub enum FeedbackType {
1808    /// Detection was incorrect (false alarm)
1809    FalsePositive,
1810    /// Anomaly was missed by detection
1811    FalseNegative,
1812    /// Detection correctly identified an anomaly
1813    ConfirmedAnomaly,
1814}
1815#[cfg(not(feature = "monitoring"))]
1816pub struct AdvancedAnomalyDetector;
1817#[cfg(not(feature = "monitoring"))]
1818pub struct AnomalyInsights;
1819
1820#[cfg(test)]
1821mod tests;