Skip to main content

trustformers_debug/
regression_detector.rs

1//! AI-powered Performance Regression Detection System
2//!
3//! This module provides advanced statistical analysis and machine learning-based
4//! detection of performance regressions in model training and inference, enabling
5//! early detection of performance degradation with high accuracy.
6// reason: debug/profiling scaffolding — structs are constructed and their fields/methods
7// are retained for the data model, serialization completeness, and future consumers that
8// do not yet read every member. Consolidated from many item-level #[allow(dead_code)].
9#![allow(dead_code)]
10
11use anyhow::Result;
12use serde::{Deserialize, Serialize};
13use std::collections::{HashMap, VecDeque};
14use std::time::SystemTime;
15use tracing::info;
16use uuid::Uuid;
17
18/// Configuration for regression detection
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct RegressionDetectionConfig {
21    /// Enable regression detection
22    pub enable_detection: bool,
23    /// Minimum number of data points for analysis
24    pub min_data_points: usize,
25    /// Statistical significance threshold (p-value)
26    pub significance_threshold: f64,
27    /// Minimum performance degradation percentage to trigger alert
28    pub min_degradation_threshold: f64,
29    /// Maximum historical data window in hours
30    pub max_history_hours: u64,
31    /// Smoothing factor for exponential moving averages
32    pub ema_smoothing_factor: f64,
33    /// Enable advanced ML-based detection
34    pub enable_ml_detection: bool,
35    /// Confidence threshold for ML predictions
36    pub ml_confidence_threshold: f64,
37    /// Enable seasonal adjustment
38    pub enable_seasonal_adjustment: bool,
39    /// Enable outlier detection before regression analysis
40    pub enable_outlier_filtering: bool,
41}
42
43impl Default for RegressionDetectionConfig {
44    fn default() -> Self {
45        Self {
46            enable_detection: true,
47            min_data_points: 10,
48            significance_threshold: 0.05,
49            min_degradation_threshold: 5.0, // 5% degradation
50            max_history_hours: 24,
51            ema_smoothing_factor: 0.3,
52            enable_ml_detection: true,
53            ml_confidence_threshold: 0.8,
54            enable_seasonal_adjustment: true,
55            enable_outlier_filtering: true,
56        }
57    }
58}
59
60/// Types of metrics to monitor for regressions
61#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
62pub enum MetricType {
63    /// Training/inference latency
64    Latency,
65    /// Memory usage
66    MemoryUsage,
67    /// CPU utilization
68    CpuUtilization,
69    /// GPU utilization
70    GpuUtilization,
71    /// Throughput (operations per second)
72    Throughput,
73    /// Model accuracy/loss
74    ModelAccuracy,
75    /// Custom metric
76    Custom(String),
77}
78
79/// Performance metric data point
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct MetricDataPoint {
82    pub metric_type: MetricType,
83    pub value: f64,
84    pub timestamp: SystemTime,
85    pub session_id: Uuid,
86    pub metadata: HashMap<String, String>,
87}
88
89/// Historical metric series for analysis
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct MetricSeries {
92    pub metric_type: MetricType,
93    pub data_points: VecDeque<MetricDataPoint>,
94    pub baseline_statistics: BaselineStatistics,
95    pub last_updated: SystemTime,
96}
97
98/// Baseline statistics for comparison
99#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct BaselineStatistics {
101    pub mean: f64,
102    pub std_dev: f64,
103    pub median: f64,
104    pub percentile_95: f64,
105    pub percentile_99: f64,
106    pub trend_slope: f64,
107    pub seasonal_pattern: Option<Vec<f64>>,
108    pub sample_count: usize,
109    pub last_computed: SystemTime,
110}
111
112/// Regression detection result
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct RegressionDetection {
115    pub detection_id: Uuid,
116    pub metric_type: MetricType,
117    pub regression_type: RegressionType,
118    pub severity: RegressionSeverity,
119    pub degradation_percentage: f64,
120    /// Two-sided p-value of the test that produced this detection, or `None`
121    /// when the detection method ran no statistical test at all.
122    ///
123    /// This replaces the pair `confidence` / `statistical_significance`, which
124    /// looked like two independent pieces of evidence and were not: the
125    /// window-dispersion branch published `1 - p` under *both* names, while
126    /// the trend branch published `p` under `statistical_significance` and
127    /// `1 - p` under `confidence` -- so the same field name carried opposite
128    /// quantities depending on which detector fired, and the change-point
129    /// branch filled both in with the invented constants `0.8` and `0.01`.
130    /// One field, one unambiguous quantity, `None` when nothing measured it.
131    pub p_value: Option<f64>,
132    pub affected_period: (SystemTime, SystemTime),
133    pub root_cause_analysis: RootCauseAnalysis,
134    pub recommendations: Vec<String>,
135    pub detected_at: SystemTime,
136}
137
138/// Types of performance regressions
139#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
140pub enum RegressionType {
141    /// Sudden step change in performance
142    StepChange,
143    /// Gradual degradation over time
144    GradualDegradation,
145    /// Increased variance/instability
146    VarianceIncrease,
147    /// Periodic performance drops
148    PeriodicRegression,
149    /// Outlier-driven regression
150    OutlierRegression,
151    /// Complex multi-factorial regression
152    ComplexRegression,
153}
154
155/// Severity levels for regressions
156#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
157pub enum RegressionSeverity {
158    Low,
159    Medium,
160    High,
161    Critical,
162}
163
164/// Root cause analysis results
165#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct RootCauseAnalysis {
167    pub likely_causes: Vec<PotentialCause>,
168    pub correlated_metrics: Vec<String>,
169    pub environmental_factors: Vec<String>,
170    pub change_points: Vec<SystemTime>,
171    pub anomaly_score: f64,
172}
173
174/// Potential cause for performance regression
175#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct PotentialCause {
177    pub cause_type: CauseType,
178    pub description: String,
179    pub confidence: f64,
180    pub supporting_evidence: Vec<String>,
181}
182
183/// Types of potential causes
184#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
185pub enum CauseType {
186    CodeChange,
187    DataChange,
188    ResourceContention,
189    HardwareIssue,
190    ConfigurationChange,
191    EnvironmentalFactor,
192    ModelDrift,
193    Unknown,
194}
195
196/// Advanced regression detector with ML capabilities
197pub struct RegressionDetector {
198    config: RegressionDetectionConfig,
199    metric_series: HashMap<MetricType, MetricSeries>,
200    anomaly_detector: AnomalyDetector,
201    trend_analyzer: TrendAnalyzer,
202    change_point_detector: ChangePointDetector,
203    seasonal_decomposer: SeasonalDecomposer,
204    dispersion_scorer: Option<WindowDispersionScorer>,
205    detection_history: VecDeque<RegressionDetection>,
206}
207
208/// Statistical anomaly detector
209#[derive(Debug)]
210struct AnomalyDetector {
211    z_score_threshold: f64,
212    iqr_multiplier: f64,
213    isolation_forest_threshold: f64,
214}
215
216impl AnomalyDetector {
217    fn new() -> Self {
218        Self {
219            z_score_threshold: 3.0,
220            iqr_multiplier: 1.5,
221            isolation_forest_threshold: 0.1,
222        }
223    }
224
225    /// Detect outliers using multiple methods
226    fn detect_outliers(&self, values: &[f64]) -> Vec<bool> {
227        if values.is_empty() {
228            return vec![];
229        }
230
231        let z_score_outliers = self.detect_z_score_outliers(values);
232        let iqr_outliers = self.detect_iqr_outliers(values);
233
234        // Combine methods using majority voting
235        z_score_outliers
236            .iter()
237            .zip(iqr_outliers.iter())
238            .map(|(&z_outlier, &iqr_outlier)| z_outlier || iqr_outlier)
239            .collect()
240    }
241
242    fn detect_z_score_outliers(&self, values: &[f64]) -> Vec<bool> {
243        let mean = values.iter().sum::<f64>() / values.len() as f64;
244        let variance = values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / values.len() as f64;
245        let std_dev = variance.sqrt();
246
247        values
248            .iter()
249            .map(|&value| {
250                if std_dev > 0.0 {
251                    ((value - mean) / std_dev).abs() > self.z_score_threshold
252                } else {
253                    false
254                }
255            })
256            .collect()
257    }
258
259    fn detect_iqr_outliers(&self, values: &[f64]) -> Vec<bool> {
260        let mut sorted_values = values.to_vec();
261        sorted_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
262
263        let q1 = Self::percentile(&sorted_values, 25.0);
264        let q3 = Self::percentile(&sorted_values, 75.0);
265        let iqr = q3 - q1;
266
267        let lower_bound = q1 - self.iqr_multiplier * iqr;
268        let upper_bound = q3 + self.iqr_multiplier * iqr;
269
270        values.iter().map(|&value| value < lower_bound || value > upper_bound).collect()
271    }
272
273    fn percentile(sorted_values: &[f64], percentile: f64) -> f64 {
274        if sorted_values.is_empty() {
275            return 0.0;
276        }
277
278        let index = (percentile / 100.0) * (sorted_values.len() - 1) as f64;
279        let lower = index.floor() as usize;
280        let upper = index.ceil() as usize;
281
282        if lower == upper {
283            sorted_values[lower]
284        } else {
285            let weight = index - lower as f64;
286            sorted_values[lower] * (1.0 - weight) + sorted_values[upper] * weight
287        }
288    }
289}
290
291/// Trend analysis for regression detection
292#[derive(Debug)]
293struct TrendAnalyzer {
294    window_size: usize,
295    significance_threshold: f64,
296}
297
298impl TrendAnalyzer {
299    fn new(window_size: usize, significance_threshold: f64) -> Self {
300        Self {
301            window_size,
302            significance_threshold,
303        }
304    }
305
306    /// Detect trend changes using linear regression
307    fn detect_trend_change(&self, values: &[f64]) -> Option<TrendChangeResult> {
308        if values.len() < self.window_size {
309            return None;
310        }
311
312        let recent_values = &values[values.len() - self.window_size..];
313        let baseline_values = if values.len() >= 2 * self.window_size {
314            &values[values.len() - 2 * self.window_size..values.len() - self.window_size]
315        } else {
316            &values[0..values.len() - self.window_size]
317        };
318
319        let recent_slope = self.calculate_slope(recent_values);
320        let baseline_slope = self.calculate_slope(baseline_values);
321
322        let slope_change = recent_slope - baseline_slope;
323        let significance = self.calculate_trend_significance(recent_values, recent_slope);
324
325        if significance < self.significance_threshold {
326            Some(TrendChangeResult {
327                slope_change,
328                recent_slope,
329                baseline_slope,
330                significance,
331                is_regression: slope_change > 0.0, // Positive slope = performance degradation
332            })
333        } else {
334            None
335        }
336    }
337
338    fn calculate_slope(&self, values: &[f64]) -> f64 {
339        if values.len() < 2 {
340            return 0.0;
341        }
342
343        let n = values.len() as f64;
344        let sum_x = (0..values.len()).sum::<usize>() as f64;
345        let sum_y = values.iter().sum::<f64>();
346        let sum_xy = values.iter().enumerate().map(|(i, &y)| i as f64 * y).sum::<f64>();
347        let sum_x_squared = (0..values.len()).map(|i| (i as f64).powi(2)).sum::<f64>();
348
349        let denominator = n * sum_x_squared - sum_x.powi(2);
350        if denominator.abs() < 1e-10 {
351            0.0
352        } else {
353            (n * sum_xy - sum_x * sum_y) / denominator
354        }
355    }
356
357    /// Two-sided p-value for `H0: slope == 0` from the ordinary-least-squares
358    /// t-test on the regression slope, with `n - 2` degrees of freedom.
359    ///
360    /// Uses the exact Student-t distribution
361    /// ([`trustformers_core::statistics::student_t_two_sided_p_value`]), not the
362    /// hand-rolled `0.5 + 0.5 * atan(x) * 2/pi` "CDF" this used to call. That
363    /// approximation was not the t CDF at all -- at `t = 1.96, df = 1000` it
364    /// returned `0.52` where the true value is `0.975`, turning a p-value of
365    /// `0.05` into `0.96`, so no trend was ever significant.
366    fn calculate_trend_significance(&self, values: &[f64], slope: f64) -> f64 {
367        if values.len() < 3 {
368            return 1.0;
369        }
370
371        let n = values.len() as f64;
372        let mean_x = (values.len() - 1) as f64 / 2.0;
373        let ss_x = (0..values.len()).map(|i| (i as f64 - mean_x).powi(2)).sum::<f64>();
374
375        // Calculate residuals with proper intercept
376        let mean_y = values.iter().sum::<f64>() / n;
377        let intercept = mean_y - slope * mean_x;
378        let predicted: Vec<f64> = (0..values.len()).map(|i| intercept + slope * i as f64).collect();
379
380        let residuals: Vec<f64> = values
381            .iter()
382            .zip(predicted.iter())
383            .map(|(&actual, &pred)| actual - pred)
384            .collect();
385
386        let mse = residuals.iter().map(|&r| r.powi(2)).sum::<f64>() / (n - 2.0);
387        let se_slope = (mse / ss_x).sqrt();
388
389        if se_slope > 0.0 {
390            let t_stat = slope / se_slope;
391            let df = n - 2.0;
392            // `None` only for a non-positive df, which `values.len() >= 3`
393            // already rules out; fall back to "not significant" if it happens.
394            trustformers_core::statistics::student_t_two_sided_p_value(t_stat, df).unwrap_or(1.0)
395        } else {
396            // Zero residual spread: the fit is exact, so a non-zero slope is
397            // maximally significant and a zero slope is not evidence of a trend.
398            if slope.abs() > 0.0 {
399                0.0
400            } else {
401                1.0
402            }
403        }
404    }
405}
406
407#[derive(Debug)]
408struct TrendChangeResult {
409    slope_change: f64,
410    recent_slope: f64,
411    baseline_slope: f64,
412    significance: f64,
413    is_regression: bool,
414}
415
416/// Change point detection using statistical methods
417#[derive(Debug)]
418struct ChangePointDetector {
419    min_segment_length: usize,
420    penalty_factor: f64,
421}
422
423impl ChangePointDetector {
424    fn new(min_segment_length: usize, penalty_factor: f64) -> Self {
425        Self {
426            min_segment_length,
427            penalty_factor,
428        }
429    }
430
431    /// Detect change points using CUSUM algorithm
432    fn detect_change_points(&self, values: &[f64]) -> Vec<usize> {
433        if values.len() < 2 * self.min_segment_length {
434            return vec![];
435        }
436
437        let mut change_points = vec![];
438        let mut current_start = 0;
439
440        while current_start + 2 * self.min_segment_length <= values.len() {
441            if let Some(change_point) = self.find_next_change_point(&values[current_start..]) {
442                let absolute_change_point = current_start + change_point;
443                change_points.push(absolute_change_point);
444                current_start = absolute_change_point + self.min_segment_length;
445            } else {
446                break;
447            }
448        }
449
450        change_points
451    }
452
453    fn find_next_change_point(&self, values: &[f64]) -> Option<usize> {
454        let n = values.len();
455        if n < 2 * self.min_segment_length {
456            return None;
457        }
458
459        let mut max_statistic = 0.0;
460        let mut best_change_point = None;
461
462        for t in self.min_segment_length..n - self.min_segment_length {
463            let statistic = self.cusum_statistic(values, t);
464            if statistic > max_statistic {
465                max_statistic = statistic;
466                best_change_point = Some(t);
467            }
468        }
469
470        // Apply penalty for multiple change points
471        let threshold = self.penalty_factor * (n as f64).ln();
472        if max_statistic > threshold {
473            best_change_point
474        } else {
475            None
476        }
477    }
478
479    fn cusum_statistic(&self, values: &[f64], change_point: usize) -> f64 {
480        let segment1 = &values[0..change_point];
481        let segment2 = &values[change_point..];
482
483        let mean1 = segment1.iter().sum::<f64>() / segment1.len() as f64;
484        let mean2 = segment2.iter().sum::<f64>() / segment2.len() as f64;
485        let overall_mean = values.iter().sum::<f64>() / values.len() as f64;
486
487        let n1 = segment1.len() as f64;
488        let n2 = segment2.len() as f64;
489        let n = values.len() as f64;
490
491        // Calculate variance
492        let variance = values.iter().map(|&x| (x - overall_mean).powi(2)).sum::<f64>() / (n - 1.0);
493
494        if variance > 0.0 {
495            (n1 * (mean1 - overall_mean).powi(2) + n2 * (mean2 - overall_mean).powi(2)) / variance
496        } else {
497            0.0
498        }
499    }
500}
501
502/// Seasonal decomposition for time series analysis
503#[derive(Debug)]
504struct SeasonalDecomposer {
505    period: usize,
506    enable_decomposition: bool,
507}
508
509impl SeasonalDecomposer {
510    fn new(period: usize) -> Self {
511        Self {
512            period,
513            enable_decomposition: true,
514        }
515    }
516
517    /// Decompose time series into trend, seasonal, and residual components
518    fn decompose(&self, values: &[f64]) -> Option<SeasonalComponents> {
519        if !self.enable_decomposition || values.len() < 2 * self.period {
520            return None;
521        }
522
523        let trend = self.extract_trend(values);
524        let detrended = self.subtract_series(values, &trend);
525        let seasonal = self.extract_seasonal(&detrended);
526        let residual = self.subtract_series(&detrended, &seasonal);
527
528        Some(SeasonalComponents {
529            trend,
530            seasonal,
531            residual,
532        })
533    }
534
535    fn extract_trend(&self, values: &[f64]) -> Vec<f64> {
536        // Moving average for trend extraction
537        let window_size = self.period;
538        let mut trend = vec![0.0; values.len()];
539
540        for i in 0..values.len() {
541            let start = i.saturating_sub(window_size / 2);
542            let end = std::cmp::min(i + window_size / 2 + 1, values.len());
543
544            let sum: f64 = values[start..end].iter().sum();
545            trend[i] = sum / (end - start) as f64;
546        }
547
548        trend
549    }
550
551    fn extract_seasonal(&self, detrended: &[f64]) -> Vec<f64> {
552        let mut seasonal = vec![0.0; detrended.len()];
553        let mut seasonal_pattern = vec![0.0; self.period];
554        let mut pattern_counts = vec![0usize; self.period];
555
556        // Calculate average seasonal pattern
557        for (i, &value) in detrended.iter().enumerate() {
558            let season_index = i % self.period;
559            seasonal_pattern[season_index] += value;
560            pattern_counts[season_index] += 1;
561        }
562
563        // Normalize by counts
564        for i in 0..self.period {
565            if pattern_counts[i] > 0 {
566                seasonal_pattern[i] /= pattern_counts[i] as f64;
567            }
568        }
569
570        // Apply seasonal pattern
571        for (i, seasonal_value) in seasonal.iter_mut().enumerate() {
572            *seasonal_value = seasonal_pattern[i % self.period];
573        }
574
575        seasonal
576    }
577
578    fn subtract_series(&self, series1: &[f64], series2: &[f64]) -> Vec<f64> {
579        series1.iter().zip(series2.iter()).map(|(&a, &b)| a - b).collect()
580    }
581}
582
583#[derive(Debug, Clone, Serialize, Deserialize)]
584struct SeasonalComponents {
585    trend: Vec<f64>,
586    seasonal: Vec<f64>,
587    residual: Vec<f64>,
588}
589
590/// Heuristic dispersion scorer over a sliding window of a metric series.
591///
592/// Despite the name it replaces (`MLPredictor`, constructed with
593/// `MLModelType::IsolationForest`), nothing here is machine learning: there is
594/// no model, no training data and no inference. The `model_type` field was
595/// stored and never read -- no isolation forest, LSTM or autoencoder exists
596/// anywhere in this crate. What the type really computes is the coefficient of
597/// variation of a window's summary features, plus a self-consistency measure;
598/// both are now named for that.
599#[derive(Debug)]
600struct WindowDispersionScorer {
601    feature_extractor: FeatureExtractor,
602    /// Dispersion above which the window is flagged.
603    dispersion_threshold: f64,
604}
605
606#[derive(Debug)]
607struct FeatureExtractor {
608    window_size: usize,
609    statistical_features: bool,
610    frequency_features: bool,
611}
612
613impl WindowDispersionScorer {
614    fn new(dispersion_threshold: f64) -> Self {
615        Self {
616            feature_extractor: FeatureExtractor {
617                window_size: 50,
618                statistical_features: true,
619                frequency_features: true,
620            },
621            dispersion_threshold,
622        }
623    }
624
625    /// Score the most recent window, if there is a full one.
626    ///
627    /// Returns `None` when the window is not full, when the dispersion is below
628    /// the configured threshold, or when there is no prior window to compare
629    /// against (so no real degradation percentage or p-value could be produced).
630    fn score_window(&self, values: &[f64]) -> Option<WindowDispersionScore> {
631        let window = self.feature_extractor.window_size;
632        if values.len() < window {
633            return None;
634        }
635
636        let features = self.feature_extractor.extract_features(values);
637        let dispersion = Self::coefficient_of_variation(&features);
638        if dispersion <= self.dispersion_threshold {
639            return None;
640        }
641
642        // Real degradation and significance, from the actual recent vs prior
643        // window of the series -- not from the dispersion score.
644        let recent = &values[values.len() - window..];
645        let prior_end = values.len() - window;
646        if prior_end < 2 {
647            return None;
648        }
649        let prior_start = prior_end.saturating_sub(window);
650        let prior = &values[prior_start..prior_end];
651
652        let recent_mean = trustformers_core::statistics::mean(recent)?;
653        let prior_mean = trustformers_core::statistics::mean(prior)?;
654        if prior_mean.abs() < f64::EPSILON {
655            return None;
656        }
657        let degradation_percentage = (recent_mean - prior_mean) / prior_mean.abs() * 100.0;
658
659        let test = trustformers_core::statistics::welch_t_test(recent, prior)?;
660
661        Some(WindowDispersionScore {
662            dispersion,
663            degradation_percentage,
664            p_value: test.p_value,
665            feature_magnitudes: Self::normalised_magnitudes(&features),
666            severity: Self::severity_for(dispersion),
667        })
668    }
669
670    /// Coefficient of variation `std / |mean|` of the feature vector.
671    fn coefficient_of_variation(features: &[f64]) -> f64 {
672        if features.is_empty() {
673            return 0.0;
674        }
675        let mean = features.iter().sum::<f64>() / features.len() as f64;
676        let variance =
677            features.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / features.len() as f64;
678        variance.sqrt() / (mean.abs() + 1e-6)
679    }
680
681    /// Each feature's magnitude as a fraction of the largest magnitude.
682    ///
683    /// This was called `feature_importance`, which implies a model attribution;
684    /// it is simply `|x| / max|x|`.
685    fn normalised_magnitudes(features: &[f64]) -> Vec<f64> {
686        let max_magnitude = features.iter().map(|x| x.abs()).fold(0.0, f64::max);
687        if max_magnitude > 0.0 {
688            features.iter().map(|&x| x.abs() / max_magnitude).collect()
689        } else {
690            vec![0.0; features.len()]
691        }
692    }
693
694    fn severity_for(dispersion: f64) -> RegressionSeverity {
695        if dispersion > 0.8 {
696            RegressionSeverity::Critical
697        } else if dispersion > 0.6 {
698            RegressionSeverity::High
699        } else if dispersion > 0.4 {
700            RegressionSeverity::Medium
701        } else {
702            RegressionSeverity::Low
703        }
704    }
705}
706
707impl FeatureExtractor {
708    fn extract_features(&self, values: &[f64]) -> Vec<f64> {
709        let mut features = Vec::new();
710
711        if self.statistical_features {
712            features.extend(self.extract_statistical_features(values));
713        }
714
715        if self.frequency_features {
716            features.extend(self.extract_frequency_features(values));
717        }
718
719        features
720    }
721
722    fn extract_statistical_features(&self, values: &[f64]) -> Vec<f64> {
723        let mean = values.iter().sum::<f64>() / values.len() as f64;
724        let variance = values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / values.len() as f64;
725        let std_dev = variance.sqrt();
726
727        let min = values.iter().fold(f64::INFINITY, |a, &b| a.min(b));
728        let max = values.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
729        let range = max - min;
730
731        // Skewness
732        let skewness = if std_dev > 0.0 {
733            values.iter().map(|x| ((x - mean) / std_dev).powi(3)).sum::<f64>() / values.len() as f64
734        } else {
735            0.0
736        };
737
738        // Kurtosis
739        let kurtosis = if std_dev > 0.0 {
740            values.iter().map(|x| ((x - mean) / std_dev).powi(4)).sum::<f64>() / values.len() as f64
741                - 3.0
742        } else {
743            0.0
744        };
745
746        vec![mean, std_dev, min, max, range, skewness, kurtosis]
747    }
748
749    fn extract_frequency_features(&self, values: &[f64]) -> Vec<f64> {
750        // Simplified frequency domain features
751        let mut features = Vec::new();
752
753        // Calculate differences to get "frequencies"
754        let differences: Vec<f64> = values.windows(2).map(|w| (w[1] - w[0]).abs()).collect();
755
756        if !differences.is_empty() {
757            let mean_diff = differences.iter().sum::<f64>() / differences.len() as f64;
758            let max_diff = differences.iter().fold(0.0f64, |a, &b| a.max(b));
759            features.extend([mean_diff, max_diff]);
760        }
761
762        features
763    }
764}
765
766/// Result of [`WindowDispersionScorer::score_window`].
767#[derive(Debug, Clone, Serialize, Deserialize)]
768struct WindowDispersionScore {
769    /// Coefficient of variation of the window's summary features.
770    dispersion: f64,
771    /// Real percentage change of the window mean against the previous window.
772    degradation_percentage: f64,
773    /// Real two-sided Welch p-value for recent vs previous window.
774    p_value: f64,
775    /// Per-feature `|x| / max|x|`.
776    #[allow(
777        dead_code,
778        reason = "carried for callers that surface the feature breakdown"
779    )]
780    feature_magnitudes: Vec<f64>,
781    severity: RegressionSeverity,
782}
783
784impl RegressionDetector {
785    /// Create a new regression detector
786    pub fn new(config: RegressionDetectionConfig) -> Self {
787        let dispersion_scorer = if config.enable_ml_detection {
788            Some(WindowDispersionScorer::new(config.ml_confidence_threshold))
789        } else {
790            None
791        };
792
793        let trend_analyzer =
794            TrendAnalyzer::new(config.min_data_points, config.significance_threshold);
795
796        Self {
797            config,
798            metric_series: HashMap::new(),
799            anomaly_detector: AnomalyDetector::new(),
800            trend_analyzer,
801            change_point_detector: ChangePointDetector::new(5, 2.0),
802            seasonal_decomposer: SeasonalDecomposer::new(24), // Hourly patterns
803            dispersion_scorer,
804            detection_history: VecDeque::new(),
805        }
806    }
807
808    /// Add a new metric data point
809    pub fn add_metric_data_point(&mut self, data_point: MetricDataPoint) -> Result<()> {
810        let metric_type = data_point.metric_type.clone();
811        let max_data_points = (self.config.max_history_hours * 60) as usize; // Assume 1 point per minute
812        let min_data_points = self.config.min_data_points;
813
814        // Update series data
815        let data_points_len = {
816            let series =
817                self.metric_series.entry(metric_type.clone()).or_insert_with(|| MetricSeries {
818                    metric_type: metric_type.clone(),
819                    data_points: VecDeque::new(),
820                    baseline_statistics: BaselineStatistics::default(),
821                    last_updated: SystemTime::now(),
822                });
823
824            // Add data point
825            series.data_points.push_back(data_point);
826            series.last_updated = SystemTime::now();
827
828            // Maintain window size
829            while series.data_points.len() > max_data_points {
830                series.data_points.pop_front();
831            }
832
833            series.data_points.len()
834        };
835
836        // Update baseline statistics
837        self.update_baseline_statistics(&metric_type)?;
838
839        // Check for regressions
840        if data_points_len >= min_data_points {
841            if let Some(detection) = self.detect_regression(&metric_type)? {
842                self.detection_history.push_back(detection);
843
844                // Maintain detection history size
845                while self.detection_history.len() > 1000 {
846                    self.detection_history.pop_front();
847                }
848            }
849        }
850
851        Ok(())
852    }
853
854    /// Detect regressions for a specific metric
855    pub fn detect_regression(
856        &mut self,
857        metric_type: &MetricType,
858    ) -> Result<Option<RegressionDetection>> {
859        let series = match self.metric_series.get(metric_type) {
860            Some(series) => series,
861            None => return Ok(None),
862        };
863
864        if series.data_points.len() < self.config.min_data_points {
865            return Ok(None);
866        }
867
868        let values: Vec<f64> = series.data_points.iter().map(|dp| dp.value).collect();
869
870        // Filter outliers if enabled
871        let filtered_values = if self.config.enable_outlier_filtering {
872            self.filter_outliers(&values)
873        } else {
874            values.clone()
875        };
876
877        // Multiple detection methods
878        let mut detections = Vec::new();
879
880        // 1. Statistical trend analysis
881        if let Some(trend_result) = self.trend_analyzer.detect_trend_change(&filtered_values) {
882            if trend_result.is_regression {
883                let severity = self.calculate_severity(trend_result.slope_change);
884                detections.push(RegressionDetection {
885                    detection_id: Uuid::new_v4(),
886                    metric_type: metric_type.clone(),
887                    regression_type: RegressionType::GradualDegradation,
888                    severity,
889                    degradation_percentage: trend_result.slope_change * 100.0,
890                    p_value: Some(trend_result.significance),
891                    affected_period: self.calculate_affected_period(series),
892                    root_cause_analysis: self.analyze_root_causes(series, &filtered_values),
893                    recommendations: self.generate_recommendations(
894                        &RegressionType::GradualDegradation,
895                        trend_result.slope_change,
896                    ),
897                    detected_at: SystemTime::now(),
898                });
899            }
900        }
901
902        // 2. Change point detection
903        let change_points = self.change_point_detector.detect_change_points(&filtered_values);
904        if let Some(latest_change_point) = change_points.last() {
905            let before = &filtered_values[0..*latest_change_point];
906            let after = &filtered_values[*latest_change_point..];
907
908            if !before.is_empty() && !after.is_empty() {
909                let before_mean = before.iter().sum::<f64>() / before.len() as f64;
910                let after_mean = after.iter().sum::<f64>() / after.len() as f64;
911                let degradation = ((after_mean - before_mean) / before_mean) * 100.0;
912
913                if degradation > self.config.min_degradation_threshold {
914                    detections.push(RegressionDetection {
915                        detection_id: Uuid::new_v4(),
916                        metric_type: metric_type.clone(),
917                        regression_type: RegressionType::StepChange,
918                        severity: self.calculate_severity(degradation / 100.0),
919                        degradation_percentage: degradation,
920                        // Change-point detection compares two window means
921                        // against `min_degradation_threshold`; it runs no
922                        // significance test, so there is no p-value to report.
923                        // The old code filled these in with the constants 0.8
924                        // and 0.01 ("High confidence for step changes").
925                        p_value: None,
926                        affected_period: self.calculate_affected_period(series),
927                        root_cause_analysis: self.analyze_root_causes(series, &filtered_values),
928                        recommendations: self.generate_recommendations(
929                            &RegressionType::StepChange,
930                            degradation / 100.0,
931                        ),
932                        detected_at: SystemTime::now(),
933                    });
934                }
935            }
936        }
937
938        // 3. Window-dispersion detection (heuristic screen + a real Welch test
939        //    between the recent and previous windows). Previously labelled
940        //    "ML-based": `degradation_percentage` was the dispersion score
941        //    times 100 and `statistical_significance` was `1 - a consistency
942        //    heuristic`, neither of which measured what its name claims.
943        if let Some(ref scorer) = self.dispersion_scorer {
944            if let Some(score) = scorer.score_window(&filtered_values) {
945                detections.push(RegressionDetection {
946                    detection_id: Uuid::new_v4(),
947                    metric_type: metric_type.clone(),
948                    regression_type: RegressionType::ComplexRegression,
949                    severity: score.severity,
950                    degradation_percentage: score.degradation_percentage,
951                    p_value: Some(score.p_value),
952                    affected_period: self.calculate_affected_period(series),
953                    root_cause_analysis: self.analyze_root_causes(series, &filtered_values),
954                    recommendations: self.generate_recommendations(
955                        &RegressionType::ComplexRegression,
956                        score.dispersion,
957                    ),
958                    detected_at: SystemTime::now(),
959                });
960            }
961        }
962
963        // Return the most severe detection
964        if let Some(detection) = detections.into_iter().max_by_key(|d| d.severity.clone()) {
965            info!(
966                "Regression detected for {:?}: {:.2}% degradation",
967                metric_type, detection.degradation_percentage
968            );
969            Ok(Some(detection))
970        } else {
971            Ok(None)
972        }
973    }
974
975    /// Get recent regression detections
976    pub fn get_recent_detections(&self, limit: usize) -> Vec<RegressionDetection> {
977        self.detection_history.iter().rev().take(limit).cloned().collect()
978    }
979
980    /// Get regression detections for a specific metric
981    pub fn get_detections_for_metric(&self, metric_type: &MetricType) -> Vec<RegressionDetection> {
982        self.detection_history
983            .iter()
984            .filter(|d| &d.metric_type == metric_type)
985            .cloned()
986            .collect()
987    }
988
989    /// Update baseline statistics for a metric
990    fn update_baseline_statistics(&mut self, metric_type: &MetricType) -> Result<()> {
991        let series = self.metric_series.get_mut(metric_type).ok_or_else(|| {
992            anyhow::anyhow!("Metric type {:?} not found in metric_series", metric_type)
993        })?;
994        let values: Vec<f64> = series.data_points.iter().map(|dp| dp.value).collect();
995
996        if values.is_empty() {
997            return Ok(());
998        }
999
1000        let mean = values.iter().sum::<f64>() / values.len() as f64;
1001        let variance = values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / values.len() as f64;
1002        let std_dev = variance.sqrt();
1003
1004        let mut sorted_values = values.clone();
1005        sorted_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1006
1007        let median = AnomalyDetector::percentile(&sorted_values, 50.0);
1008        let percentile_95 = AnomalyDetector::percentile(&sorted_values, 95.0);
1009        let percentile_99 = AnomalyDetector::percentile(&sorted_values, 99.0);
1010
1011        let trend_slope = self.trend_analyzer.calculate_slope(&values);
1012
1013        let seasonal_pattern = if self.config.enable_seasonal_adjustment {
1014            self.seasonal_decomposer
1015                .decompose(&values)
1016                .map(|components| components.seasonal)
1017        } else {
1018            None
1019        };
1020
1021        series.baseline_statistics = BaselineStatistics {
1022            mean,
1023            std_dev,
1024            median,
1025            percentile_95,
1026            percentile_99,
1027            trend_slope,
1028            seasonal_pattern,
1029            sample_count: values.len(),
1030            last_computed: SystemTime::now(),
1031        };
1032
1033        Ok(())
1034    }
1035
1036    fn filter_outliers(&self, values: &[f64]) -> Vec<f64> {
1037        let outlier_mask = self.anomaly_detector.detect_outliers(values);
1038        values
1039            .iter()
1040            .zip(outlier_mask.iter())
1041            .filter(|(_, &is_outlier)| !is_outlier)
1042            .map(|(&value, _)| value)
1043            .collect()
1044    }
1045
1046    fn calculate_severity(&self, degradation_ratio: f64) -> RegressionSeverity {
1047        let degradation_percentage = degradation_ratio.abs() * 100.0;
1048
1049        if degradation_percentage > 50.0 {
1050            RegressionSeverity::Critical
1051        } else if degradation_percentage > 25.0 {
1052            RegressionSeverity::High
1053        } else if degradation_percentage > 10.0 {
1054            RegressionSeverity::Medium
1055        } else {
1056            RegressionSeverity::Low
1057        }
1058    }
1059
1060    fn calculate_affected_period(&self, series: &MetricSeries) -> (SystemTime, SystemTime) {
1061        let start = series.data_points.front().map(|dp| dp.timestamp).unwrap_or(SystemTime::now());
1062        let end = series.data_points.back().map(|dp| dp.timestamp).unwrap_or(SystemTime::now());
1063        (start, end)
1064    }
1065
1066    fn analyze_root_causes(&self, series: &MetricSeries, values: &[f64]) -> RootCauseAnalysis {
1067        let mut likely_causes = Vec::new();
1068        let correlated_metrics = Vec::new();
1069        let environmental_factors = Vec::new();
1070
1071        // Analyze patterns to identify potential causes
1072        let change_points = self.change_point_detector.detect_change_points(values);
1073        let change_point_timestamps: Vec<SystemTime> = change_points
1074            .iter()
1075            .filter_map(|&idx| series.data_points.get(idx).map(|dp| dp.timestamp))
1076            .collect();
1077
1078        // Check for sudden changes (potential code/config changes)
1079        if !change_points.is_empty() {
1080            likely_causes.push(PotentialCause {
1081                cause_type: CauseType::CodeChange,
1082                description: "Sudden performance change detected, possibly due to code deployment"
1083                    .to_string(),
1084                confidence: 0.7,
1085                supporting_evidence: vec![format!(
1086                    "Change point detected at {} locations",
1087                    change_points.len()
1088                )],
1089            });
1090        }
1091
1092        // Check for gradual degradation (potential resource issues)
1093        let trend_slope = self.trend_analyzer.calculate_slope(values);
1094        if trend_slope > 0.01 {
1095            likely_causes.push(PotentialCause {
1096                cause_type: CauseType::ResourceContention,
1097                description:
1098                    "Gradual performance degradation suggests resource contention or memory leaks"
1099                        .to_string(),
1100                confidence: 0.6,
1101                supporting_evidence: vec![format!("Positive trend slope: {:.4}", trend_slope)],
1102            });
1103        }
1104
1105        // Calculate anomaly score
1106        let mean = values.iter().sum::<f64>() / values.len() as f64;
1107        let variance = values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / values.len() as f64;
1108        let anomaly_score = variance.sqrt() / (mean + 1e-6);
1109
1110        RootCauseAnalysis {
1111            likely_causes,
1112            correlated_metrics,
1113            environmental_factors,
1114            change_points: change_point_timestamps,
1115            anomaly_score,
1116        }
1117    }
1118
1119    fn generate_recommendations(
1120        &self,
1121        regression_type: &RegressionType,
1122        degradation: f64,
1123    ) -> Vec<String> {
1124        let mut recommendations = Vec::new();
1125
1126        match regression_type {
1127            RegressionType::StepChange => {
1128                recommendations
1129                    .push("Investigate recent deployments or configuration changes".to_string());
1130                recommendations
1131                    .push("Review system logs around the time of performance change".to_string());
1132                recommendations
1133                    .push("Consider rolling back recent changes if possible".to_string());
1134            },
1135            RegressionType::GradualDegradation => {
1136                recommendations
1137                    .push("Monitor resource utilization (CPU, memory, disk)".to_string());
1138                recommendations.push("Check for memory leaks or resource exhaustion".to_string());
1139                recommendations
1140                    .push("Review long-running processes and background tasks".to_string());
1141            },
1142            RegressionType::VarianceIncrease => {
1143                recommendations
1144                    .push("Investigate system stability and hardware issues".to_string());
1145                recommendations.push("Check for intermittent network or I/O problems".to_string());
1146            },
1147            RegressionType::ComplexRegression => {
1148                recommendations.push("Perform detailed profiling and analysis".to_string());
1149                recommendations
1150                    .push("Investigate multiple potential causes simultaneously".to_string());
1151            },
1152            _ => {
1153                recommendations.push("Perform comprehensive system analysis".to_string());
1154            },
1155        }
1156
1157        if degradation > 0.5 {
1158            recommendations.push("URGENT: Consider immediate mitigation actions".to_string());
1159            recommendations.push("Alert on-call team for immediate investigation".to_string());
1160        } else if degradation > 0.25 {
1161            recommendations.push("Schedule investigation within 24 hours".to_string());
1162        }
1163
1164        recommendations
1165    }
1166}
1167
1168impl Default for BaselineStatistics {
1169    fn default() -> Self {
1170        Self {
1171            mean: 0.0,
1172            std_dev: 0.0,
1173            median: 0.0,
1174            percentile_95: 0.0,
1175            percentile_99: 0.0,
1176            trend_slope: 0.0,
1177            seasonal_pattern: None,
1178            sample_count: 0,
1179            last_computed: SystemTime::now(),
1180        }
1181    }
1182}
1183
1184/// Integration with main debug session
1185impl crate::DebugSession {
1186    /// Enable regression detection for this debug session
1187    pub async fn enable_regression_detection(
1188        &mut self,
1189        config: RegressionDetectionConfig,
1190    ) -> Result<RegressionDetector> {
1191        let detector = RegressionDetector::new(config);
1192        info!(
1193            "Enabled regression detection for debug session {}",
1194            self.id()
1195        );
1196        Ok(detector)
1197    }
1198}
1199
1200#[cfg(test)]
1201mod tests {
1202    use super::*;
1203
1204    // ---- Wave 6c debug-sweep2 honesty regressions ------------------------
1205
1206    #[test]
1207    fn trend_significance_uses_the_real_student_t_distribution() {
1208        let detector = TrendAnalyzer::new(3, 0.05);
1209        // A perfectly linear ramp with a tiny wobble: the slope is
1210        // overwhelmingly significant. The old atan "CDF" returned ~0.96 here.
1211        let values: Vec<f64> =
1212            (0..30).map(|i| i as f64 + if i % 2 == 0 { 0.01 } else { -0.01 }).collect();
1213        let slope = detector.calculate_slope(&values);
1214        assert!(
1215            (slope - 1.0).abs() < 0.01,
1216            "slope should be ~1, got {slope}"
1217        );
1218        let p = detector.calculate_trend_significance(&values, slope);
1219        assert!(
1220            p < 1e-6,
1221            "a near-perfect ramp must be highly significant, got p={p}"
1222        );
1223    }
1224
1225    #[test]
1226    fn trend_significance_is_high_for_pure_noise_around_a_flat_line() {
1227        let detector = TrendAnalyzer::new(3, 0.05);
1228        // Symmetric zig-zag: zero slope, so the null cannot be rejected.
1229        let values: Vec<f64> = (0..30).map(|i| if i % 2 == 0 { 1.0 } else { -1.0 }).collect();
1230        let slope = detector.calculate_slope(&values);
1231        let p = detector.calculate_trend_significance(&values, slope);
1232        assert!(p > 0.5, "a flat zig-zag must not be significant, got p={p}");
1233    }
1234
1235    #[test]
1236    fn trend_significance_matches_a_published_t_critical_value() {
1237        // Cross-check the underlying distribution against the standard table:
1238        // t(0.025, df=10) = 2.228 => two-sided p = 0.05.
1239        let p = trustformers_core::statistics::student_t_two_sided_p_value(2.228, 10.0)
1240            .expect("valid df");
1241        assert!(
1242            (p - 0.05).abs() < 1e-3,
1243            "expected p ~= 0.05 for t=2.228, df=10; got {p}"
1244        );
1245        // What the deleted approximation would have produced at the same point.
1246        let bogus = {
1247            let x = 2.228_f64 / (10.0 + 2.228_f64.powi(2)).sqrt();
1248            2.0 * (1.0 - (0.5 + 0.5 * x.atan() * (2.0 / std::f64::consts::PI)))
1249        };
1250        // 0.667 vs the true 0.05: the deleted approximation was off by more
1251        // than an order of magnitude and would never have rejected the null.
1252        assert!(
1253            bogus > 10.0 * p,
1254            "sanity: the old approximation really was that wrong (bogus={bogus}, true={p})"
1255        );
1256    }
1257
1258    #[test]
1259    fn dispersion_scorer_reports_a_real_degradation_and_p_value() {
1260        let scorer = WindowDispersionScorer::new(0.0);
1261        // 50 samples around 1.0 followed by 50 around 2.0: a real +100%
1262        // degradation between the previous and the recent window.
1263        let mut values: Vec<f64> = Vec::new();
1264        for i in 0..50 {
1265            values.push(1.0 + (i % 5) as f64 * 0.01);
1266        }
1267        for i in 0..50 {
1268            values.push(2.0 + (i % 5) as f64 * 0.01);
1269        }
1270        let score = scorer.score_window(&values).expect("dispersion above a zero threshold");
1271        assert!(
1272            (score.degradation_percentage - 100.0).abs() < 2.0,
1273            "expected ~+100% degradation, got {}",
1274            score.degradation_percentage
1275        );
1276        assert!(
1277            score.p_value < 1e-6,
1278            "two clearly separated windows must be highly significant, got p={}",
1279            score.p_value
1280        );
1281    }
1282
1283    #[test]
1284    fn dispersion_scorer_needs_a_previous_window_to_compare_against() {
1285        let scorer = WindowDispersionScorer::new(0.0);
1286        // Exactly one window: there is no prior window, so no honest
1287        // degradation percentage or p-value exists.
1288        let values: Vec<f64> = (0..50).map(|i| i as f64).collect();
1289        assert!(scorer.score_window(&values).is_none());
1290    }
1291
1292    #[tokio::test]
1293    async fn test_regression_detector_creation() {
1294        let config = RegressionDetectionConfig::default();
1295        let detector = RegressionDetector::new(config);
1296
1297        assert!(detector.metric_series.is_empty());
1298        assert!(detector.detection_history.is_empty());
1299    }
1300
1301    #[tokio::test]
1302    async fn test_add_metric_data_point() {
1303        let config = RegressionDetectionConfig::default();
1304        let mut detector = RegressionDetector::new(config);
1305
1306        let data_point = MetricDataPoint {
1307            metric_type: MetricType::Latency,
1308            value: 100.0,
1309            timestamp: SystemTime::now(),
1310            session_id: Uuid::new_v4(),
1311            metadata: HashMap::new(),
1312        };
1313
1314        assert!(detector.add_metric_data_point(data_point).is_ok());
1315        assert_eq!(detector.metric_series.len(), 1);
1316    }
1317
1318    #[test]
1319    fn test_anomaly_detection() {
1320        let detector = AnomalyDetector::new();
1321        let values = vec![1.0, 2.0, 3.0, 2.0, 1.0, 100.0]; // 100.0 is an outlier
1322
1323        let outliers = detector.detect_outliers(&values);
1324        assert_eq!(outliers.len(), values.len());
1325        assert!(outliers[5]); // Last value should be detected as outlier
1326    }
1327
1328    #[test]
1329    fn test_trend_analysis() {
1330        let analyzer = TrendAnalyzer::new(3, 0.9);
1331        let values = [1.0, 1.1, 1.2, 10.0, 20.0, 30.0];
1332
1333        // Test that trend analyzer can calculate slopes
1334        let recent_values = &values[3..6]; // [10.0, 20.0, 30.0]
1335        let baseline_values = &values[0..3]; // [1.0, 1.1, 1.2]
1336
1337        let recent_slope = analyzer.calculate_slope(recent_values);
1338        let baseline_slope = analyzer.calculate_slope(baseline_values);
1339
1340        // Recent slope should be much higher than baseline
1341        assert!(recent_slope > baseline_slope);
1342        assert!(recent_slope > 0.0);
1343    }
1344
1345    #[test]
1346    fn test_change_point_detection() {
1347        let detector = ChangePointDetector::new(3, 2.0);
1348        let values = vec![1.0, 1.0, 1.0, 1.0, 5.0, 5.0, 5.0, 5.0]; // Change at index 4
1349
1350        let change_points = detector.detect_change_points(&values);
1351        assert!(!change_points.is_empty());
1352    }
1353
1354    #[test]
1355    fn test_seasonal_decomposition() {
1356        let decomposer = SeasonalDecomposer::new(4);
1357        let values = vec![1.0, 2.0, 3.0, 4.0, 1.0, 2.0, 3.0, 4.0, 1.0, 2.0, 3.0, 4.0];
1358
1359        let components = decomposer.decompose(&values);
1360        assert!(components.is_some());
1361
1362        let comp = components.expect("operation failed in test");
1363        assert_eq!(comp.trend.len(), values.len());
1364        assert_eq!(comp.seasonal.len(), values.len());
1365        assert_eq!(comp.residual.len(), values.len());
1366    }
1367
1368    #[test]
1369    fn test_feature_extraction() {
1370        let extractor = FeatureExtractor {
1371            window_size: 10,
1372            statistical_features: true,
1373            frequency_features: true,
1374        };
1375
1376        let values = vec![1.0, 2.0, 3.0, 4.0, 5.0, 4.0, 3.0, 2.0, 1.0, 2.0];
1377        let features = extractor.extract_features(&values);
1378
1379        assert!(!features.is_empty());
1380        assert!(features.len() >= 7); // At least statistical features
1381    }
1382}