Skip to main content

quantrs2_anneal/advanced_testing_framework/
regression_detector.rs

1//! Performance regression detection system
2
3use super::{
4    AlertThresholds, ApplicationError, ApplicationResult, Duration, HashMap, Instant,
5    RegressionAlgorithmType, StatisticalModelType, TrendDirection, VecDeque,
6};
7
8/// Performance regression detector
9#[derive(Debug)]
10pub struct RegressionDetector {
11    /// Performance history database
12    pub performance_history: HashMap<String, VecDeque<PerformanceDataPoint>>,
13    /// Regression detection algorithms
14    pub detection_algorithms: Vec<RegressionAlgorithm>,
15    /// Alert thresholds
16    pub alert_thresholds: AlertThresholds,
17    /// Statistical models for prediction
18    pub statistical_models: HashMap<String, StatisticalModel>,
19}
20
21/// Performance data point
22#[derive(Debug, Clone)]
23pub struct PerformanceDataPoint {
24    /// Timestamp of measurement
25    pub timestamp: Instant,
26    /// Performance value
27    pub value: f64,
28    /// Test configuration
29    pub test_config: TestConfiguration,
30    /// Environmental factors
31    pub environment: EnvironmentalFactors,
32    /// Additional metadata
33    pub metadata: HashMap<String, String>,
34}
35
36/// Test configuration for reproducibility
37#[derive(Debug, Clone)]
38pub struct TestConfiguration {
39    /// Test parameters
40    pub parameters: HashMap<String, f64>,
41    /// Hardware configuration
42    pub hardware: HardwareConfiguration,
43    /// Software configuration
44    pub software: SoftwareConfiguration,
45}
46
47/// Hardware configuration
48#[derive(Debug, Clone)]
49pub struct HardwareConfiguration {
50    /// CPU model
51    pub cpu_model: String,
52    /// Memory size (GB)
53    pub memory_gb: usize,
54    /// Number of cores
55    pub num_cores: usize,
56    /// GPU information
57    pub gpu_info: Option<String>,
58}
59
60/// Software configuration
61#[derive(Debug, Clone)]
62pub struct SoftwareConfiguration {
63    /// Operating system
64    pub os: String,
65    /// Compiler version
66    pub compiler_version: String,
67    /// Optimization flags
68    pub optimization_flags: Vec<String>,
69    /// Library versions
70    pub dependencies: HashMap<String, String>,
71}
72
73/// Environmental factors affecting performance
74#[derive(Debug, Clone)]
75pub struct EnvironmentalFactors {
76    /// System load
77    pub system_load: f64,
78    /// Temperature
79    pub temperature: Option<f64>,
80    /// Network conditions
81    pub network_latency: Option<Duration>,
82    /// Power mode
83    pub power_mode: Option<String>,
84}
85
86/// Regression detection algorithm
87#[derive(Debug)]
88pub struct RegressionAlgorithm {
89    /// Algorithm identifier
90    pub id: String,
91    /// Algorithm type
92    pub algorithm_type: RegressionAlgorithmType,
93    /// Algorithm parameters
94    pub parameters: HashMap<String, f64>,
95    /// Sensitivity level
96    pub sensitivity: f64,
97}
98
99/// Statistical model for regression analysis
100#[derive(Debug)]
101pub struct StatisticalModel {
102    /// Model type
103    pub model_type: StatisticalModelType,
104    /// Model parameters
105    pub parameters: Vec<f64>,
106    /// Model confidence
107    pub confidence: f64,
108    /// Last update time
109    pub last_update: Instant,
110}
111
112impl RegressionDetector {
113    #[must_use]
114    pub fn new() -> Self {
115        Self {
116            performance_history: HashMap::new(),
117            detection_algorithms: Self::create_default_algorithms(),
118            alert_thresholds: AlertThresholds::default(),
119            statistical_models: HashMap::new(),
120        }
121    }
122
123    /// Create default regression detection algorithms
124    fn create_default_algorithms() -> Vec<RegressionAlgorithm> {
125        vec![
126            RegressionAlgorithm {
127                id: "statistical_process_control".to_string(),
128                algorithm_type: RegressionAlgorithmType::StatisticalProcessControl,
129                parameters: {
130                    let mut params = HashMap::new();
131                    params.insert("control_limit_factor".to_string(), 3.0);
132                    params.insert("window_size".to_string(), 50.0);
133                    params
134                },
135                sensitivity: 0.95,
136            },
137            RegressionAlgorithm {
138                id: "change_point_detection".to_string(),
139                algorithm_type: RegressionAlgorithmType::ChangePointDetection,
140                parameters: {
141                    let mut params = HashMap::new();
142                    params.insert("penalty".to_string(), 1.0);
143                    params.insert("min_segment_length".to_string(), 10.0);
144                    params
145                },
146                sensitivity: 0.90,
147            },
148            RegressionAlgorithm {
149                id: "time_series_analysis".to_string(),
150                algorithm_type: RegressionAlgorithmType::TimeSeriesAnalysis,
151                parameters: {
152                    let mut params = HashMap::new();
153                    params.insert("trend_threshold".to_string(), 0.05);
154                    params.insert("seasonality_period".to_string(), 7.0);
155                    params
156                },
157                sensitivity: 0.85,
158            },
159        ]
160    }
161
162    /// Add performance data point
163    pub fn add_data_point(&mut self, test_id: String, data_point: PerformanceDataPoint) {
164        let history = self
165            .performance_history
166            .entry(test_id)
167            .or_insert_with(VecDeque::new);
168        history.push_back(data_point);
169
170        // Keep only recent data points
171        while history.len() > 1000 {
172            history.pop_front();
173        }
174    }
175
176    /// Detect performance regression
177    pub fn detect_regression(
178        &self,
179        test_id: &str,
180    ) -> ApplicationResult<Vec<RegressionDetectionResult>> {
181        let history = self.performance_history.get(test_id).ok_or_else(|| {
182            ApplicationError::ConfigurationError(format!(
183                "No performance history found for test: {test_id}"
184            ))
185        })?;
186
187        if history.len() < self.alert_thresholds.min_sample_size {
188            return Ok(Vec::new());
189        }
190
191        let mut results = Vec::new();
192
193        for algorithm in &self.detection_algorithms {
194            let result = self.run_detection_algorithm(algorithm, history)?;
195            results.push(result);
196        }
197
198        Ok(results)
199    }
200
201    /// Run specific detection algorithm
202    fn run_detection_algorithm(
203        &self,
204        algorithm: &RegressionAlgorithm,
205        history: &VecDeque<PerformanceDataPoint>,
206    ) -> ApplicationResult<RegressionDetectionResult> {
207        match algorithm.algorithm_type {
208            RegressionAlgorithmType::StatisticalProcessControl => {
209                self.run_statistical_process_control(algorithm, history)
210            }
211            RegressionAlgorithmType::ChangePointDetection => {
212                self.run_change_point_detection(algorithm, history)
213            }
214            RegressionAlgorithmType::TimeSeriesAnalysis => {
215                self.run_time_series_analysis(algorithm, history)
216            }
217            _ => Ok(RegressionDetectionResult {
218                algorithm_id: algorithm.id.clone(),
219                regression_detected: false,
220                confidence: 0.0,
221                p_value: 1.0,
222                change_point: None,
223                trend_direction: TrendDirection::Stable,
224                magnitude: 0.0,
225                details: "Algorithm not implemented".to_string(),
226            }),
227        }
228    }
229
230    /// Run statistical process control algorithm
231    fn run_statistical_process_control(
232        &self,
233        algorithm: &RegressionAlgorithm,
234        history: &VecDeque<PerformanceDataPoint>,
235    ) -> ApplicationResult<RegressionDetectionResult> {
236        let window_size = *algorithm.parameters.get("window_size").unwrap_or(&50.0) as usize;
237        let control_limit_factor = algorithm
238            .parameters
239            .get("control_limit_factor")
240            .unwrap_or(&3.0);
241
242        let values: Vec<f64> = history.iter().map(|dp| dp.value).collect();
243
244        if values.len() < window_size {
245            return Ok(RegressionDetectionResult {
246                algorithm_id: algorithm.id.clone(),
247                regression_detected: false,
248                confidence: 0.0,
249                p_value: 1.0,
250                change_point: None,
251                trend_direction: TrendDirection::Stable,
252                magnitude: 0.0,
253                details: "Insufficient data for SPC".to_string(),
254            });
255        }
256
257        // Calculate control limits from baseline window
258        let baseline = &values[..window_size];
259        let mean = baseline.iter().sum::<f64>() / baseline.len() as f64;
260        let variance =
261            baseline.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / (baseline.len() - 1) as f64;
262        let std_dev = variance.sqrt();
263
264        let upper_limit = mean + control_limit_factor * std_dev;
265        let lower_limit = mean - control_limit_factor * std_dev;
266
267        // Check recent values against control limits
268        let recent_values = &values[window_size..];
269        let violations: Vec<usize> = recent_values
270            .iter()
271            .enumerate()
272            .filter(|(_, &value)| value > upper_limit || value < lower_limit)
273            .map(|(i, _)| i + window_size)
274            .collect();
275
276        let regression_detected = !violations.is_empty();
277        let confidence = if regression_detected {
278            algorithm.sensitivity
279        } else {
280            1.0 - algorithm.sensitivity
281        };
282
283        let trend_direction = if recent_values.iter().any(|&v| v < lower_limit) {
284            TrendDirection::Degrading
285        } else if recent_values.iter().any(|&v| v > upper_limit) {
286            TrendDirection::Improving
287        } else {
288            TrendDirection::Stable
289        };
290
291        Ok(RegressionDetectionResult {
292            algorithm_id: algorithm.id.clone(),
293            regression_detected,
294            confidence,
295            p_value: if regression_detected { 0.01 } else { 0.9 },
296            change_point: violations.first().copied(),
297            trend_direction,
298            magnitude: if violations.is_empty() {
299                0.0
300            } else {
301                let worst_violation = recent_values
302                    .iter()
303                    .map(|&v| (v - mean).abs() / std_dev)
304                    .fold(0.0, f64::max);
305                worst_violation
306            },
307            details: format!("SPC analysis: {} violations detected", violations.len()),
308        })
309    }
310
311    /// Run change point detection algorithm
312    fn run_change_point_detection(
313        &self,
314        algorithm: &RegressionAlgorithm,
315        history: &VecDeque<PerformanceDataPoint>,
316    ) -> ApplicationResult<RegressionDetectionResult> {
317        let min_segment_length = *algorithm
318            .parameters
319            .get("min_segment_length")
320            .unwrap_or(&10.0) as usize;
321        let values: Vec<f64> = history.iter().map(|dp| dp.value).collect();
322
323        if values.len() < min_segment_length * 2 {
324            return Ok(RegressionDetectionResult {
325                algorithm_id: algorithm.id.clone(),
326                regression_detected: false,
327                confidence: 0.0,
328                p_value: 1.0,
329                change_point: None,
330                trend_direction: TrendDirection::Stable,
331                magnitude: 0.0,
332                details: "Insufficient data for change point detection".to_string(),
333            });
334        }
335
336        // Simplified change point detection using variance changes
337        let mut best_change_point = None;
338        let mut best_score = 0.0;
339
340        for i in min_segment_length..(values.len() - min_segment_length) {
341            let before = &values[..i];
342            let after = &values[i..];
343
344            let mean_before = before.iter().sum::<f64>() / before.len() as f64;
345            let mean_after = after.iter().sum::<f64>() / after.len() as f64;
346
347            let score = (mean_before - mean_after).abs();
348
349            if score > best_score {
350                best_score = score;
351                best_change_point = Some(i);
352            }
353        }
354
355        let threshold = 0.1; // Simplified threshold
356        let regression_detected = best_score > threshold;
357
358        Ok(RegressionDetectionResult {
359            algorithm_id: algorithm.id.clone(),
360            regression_detected,
361            confidence: if regression_detected {
362                algorithm.sensitivity
363            } else {
364                1.0 - algorithm.sensitivity
365            },
366            p_value: if regression_detected { 0.05 } else { 0.8 },
367            change_point: best_change_point,
368            trend_direction: if regression_detected {
369                if let Some(cp) = best_change_point {
370                    let before_mean = values[..cp].iter().sum::<f64>() / cp as f64;
371                    let after_mean = values[cp..].iter().sum::<f64>() / (values.len() - cp) as f64;
372                    if after_mean < before_mean {
373                        TrendDirection::Degrading
374                    } else {
375                        TrendDirection::Improving
376                    }
377                } else {
378                    TrendDirection::Stable
379                }
380            } else {
381                TrendDirection::Stable
382            },
383            magnitude: best_score,
384            details: format!("Change point detection: score = {best_score:.4}"),
385        })
386    }
387
388    /// Run time series analysis algorithm
389    fn run_time_series_analysis(
390        &self,
391        algorithm: &RegressionAlgorithm,
392        history: &VecDeque<PerformanceDataPoint>,
393    ) -> ApplicationResult<RegressionDetectionResult> {
394        let trend_threshold = algorithm.parameters.get("trend_threshold").unwrap_or(&0.05);
395        let values: Vec<f64> = history.iter().map(|dp| dp.value).collect();
396
397        if values.len() < 10 {
398            return Ok(RegressionDetectionResult {
399                algorithm_id: algorithm.id.clone(),
400                regression_detected: false,
401                confidence: 0.0,
402                p_value: 1.0,
403                change_point: None,
404                trend_direction: TrendDirection::Stable,
405                magnitude: 0.0,
406                details: "Insufficient data for time series analysis".to_string(),
407            });
408        }
409
410        // Simple linear trend calculation
411        let n = values.len() as f64;
412        let x_sum = (0..values.len()).map(|i| i as f64).sum::<f64>();
413        let y_sum = values.iter().sum::<f64>();
414        let xy_sum = values
415            .iter()
416            .enumerate()
417            .map(|(i, &y)| i as f64 * y)
418            .sum::<f64>();
419        let x2_sum = (0..values.len()).map(|i| (i as f64).powi(2)).sum::<f64>();
420
421        let slope = n.mul_add(xy_sum, -(x_sum * y_sum)) / x_sum.mul_add(-x_sum, n * x2_sum);
422        let slope_abs = slope.abs();
423
424        let regression_detected = slope_abs > *trend_threshold;
425
426        let trend_direction = if slope > *trend_threshold {
427            TrendDirection::Improving
428        } else if slope < -*trend_threshold {
429            TrendDirection::Degrading
430        } else {
431            TrendDirection::Stable
432        };
433
434        Ok(RegressionDetectionResult {
435            algorithm_id: algorithm.id.clone(),
436            regression_detected,
437            confidence: if regression_detected {
438                algorithm.sensitivity
439            } else {
440                1.0 - algorithm.sensitivity
441            },
442            p_value: if regression_detected { 0.02 } else { 0.7 },
443            change_point: None,
444            trend_direction,
445            magnitude: slope_abs,
446            details: format!("Time series analysis: slope = {slope:.6}"),
447        })
448    }
449
450    /// Get performance summary for test
451    #[must_use]
452    pub fn get_performance_summary(&self, test_id: &str) -> Option<PerformanceSummary> {
453        let history = self.performance_history.get(test_id)?;
454
455        if history.is_empty() {
456            return None;
457        }
458
459        let values: Vec<f64> = history.iter().map(|dp| dp.value).collect();
460        let mean = values.iter().sum::<f64>() / values.len() as f64;
461        let variance =
462            values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / (values.len() - 1) as f64;
463        let std_dev = variance.sqrt();
464
465        let mut sorted_values = values.clone();
466        sorted_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
467
468        Some(PerformanceSummary {
469            test_id: test_id.to_string(),
470            sample_count: values.len(),
471            mean,
472            std_dev,
473            min: sorted_values[0],
474            max: sorted_values[sorted_values.len() - 1],
475            median: if sorted_values.len() % 2 == 0 {
476                f64::midpoint(
477                    sorted_values[sorted_values.len() / 2 - 1],
478                    sorted_values[sorted_values.len() / 2],
479                )
480            } else {
481                sorted_values[sorted_values.len() / 2]
482            },
483            recent_trend: self.calculate_recent_trend(&values),
484        })
485    }
486
487    /// Calculate recent trend
488    fn calculate_recent_trend(&self, values: &[f64]) -> TrendDirection {
489        if values.len() < 10 {
490            return TrendDirection::Stable;
491        }
492
493        let recent_size = (values.len() / 4).max(5).min(20);
494        let recent = &values[values.len() - recent_size..];
495        let earlier = &values[values.len() - 2 * recent_size..values.len() - recent_size];
496
497        let recent_mean = recent.iter().sum::<f64>() / recent.len() as f64;
498        let earlier_mean = earlier.iter().sum::<f64>() / earlier.len() as f64;
499
500        let change = (recent_mean - earlier_mean) / earlier_mean;
501
502        if change > 0.05 {
503            TrendDirection::Improving
504        } else if change < -0.05 {
505            TrendDirection::Degrading
506        } else {
507            TrendDirection::Stable
508        }
509    }
510}
511
512/// Result from regression detection
513#[derive(Debug, Clone)]
514pub struct RegressionDetectionResult {
515    /// Algorithm identifier
516    pub algorithm_id: String,
517    /// Whether regression was detected
518    pub regression_detected: bool,
519    /// Confidence level
520    pub confidence: f64,
521    /// Statistical p-value
522    pub p_value: f64,
523    /// Change point index (if detected)
524    pub change_point: Option<usize>,
525    /// Direction of trend
526    pub trend_direction: TrendDirection,
527    /// Magnitude of change
528    pub magnitude: f64,
529    /// Additional details
530    pub details: String,
531}
532
533/// Performance summary statistics
534#[derive(Debug, Clone)]
535pub struct PerformanceSummary {
536    /// Test identifier
537    pub test_id: String,
538    /// Number of samples
539    pub sample_count: usize,
540    /// Mean performance
541    pub mean: f64,
542    /// Standard deviation
543    pub std_dev: f64,
544    /// Minimum value
545    pub min: f64,
546    /// Maximum value
547    pub max: f64,
548    /// Median value
549    pub median: f64,
550    /// Recent trend direction
551    pub recent_trend: TrendDirection,
552}