1use super::{
4 AlertThresholds, ApplicationError, ApplicationResult, Duration, HashMap, Instant,
5 RegressionAlgorithmType, StatisticalModelType, TrendDirection, VecDeque,
6};
7
8#[derive(Debug)]
10pub struct RegressionDetector {
11 pub performance_history: HashMap<String, VecDeque<PerformanceDataPoint>>,
13 pub detection_algorithms: Vec<RegressionAlgorithm>,
15 pub alert_thresholds: AlertThresholds,
17 pub statistical_models: HashMap<String, StatisticalModel>,
19}
20
21#[derive(Debug, Clone)]
23pub struct PerformanceDataPoint {
24 pub timestamp: Instant,
26 pub value: f64,
28 pub test_config: TestConfiguration,
30 pub environment: EnvironmentalFactors,
32 pub metadata: HashMap<String, String>,
34}
35
36#[derive(Debug, Clone)]
38pub struct TestConfiguration {
39 pub parameters: HashMap<String, f64>,
41 pub hardware: HardwareConfiguration,
43 pub software: SoftwareConfiguration,
45}
46
47#[derive(Debug, Clone)]
49pub struct HardwareConfiguration {
50 pub cpu_model: String,
52 pub memory_gb: usize,
54 pub num_cores: usize,
56 pub gpu_info: Option<String>,
58}
59
60#[derive(Debug, Clone)]
62pub struct SoftwareConfiguration {
63 pub os: String,
65 pub compiler_version: String,
67 pub optimization_flags: Vec<String>,
69 pub dependencies: HashMap<String, String>,
71}
72
73#[derive(Debug, Clone)]
75pub struct EnvironmentalFactors {
76 pub system_load: f64,
78 pub temperature: Option<f64>,
80 pub network_latency: Option<Duration>,
82 pub power_mode: Option<String>,
84}
85
86#[derive(Debug)]
88pub struct RegressionAlgorithm {
89 pub id: String,
91 pub algorithm_type: RegressionAlgorithmType,
93 pub parameters: HashMap<String, f64>,
95 pub sensitivity: f64,
97}
98
99#[derive(Debug)]
101pub struct StatisticalModel {
102 pub model_type: StatisticalModelType,
104 pub parameters: Vec<f64>,
106 pub confidence: f64,
108 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 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 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 while history.len() > 1000 {
172 history.pop_front();
173 }
174 }
175
176 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 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 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 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 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 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 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; 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 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 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 #[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 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#[derive(Debug, Clone)]
514pub struct RegressionDetectionResult {
515 pub algorithm_id: String,
517 pub regression_detected: bool,
519 pub confidence: f64,
521 pub p_value: f64,
523 pub change_point: Option<usize>,
525 pub trend_direction: TrendDirection,
527 pub magnitude: f64,
529 pub details: String,
531}
532
533#[derive(Debug, Clone)]
535pub struct PerformanceSummary {
536 pub test_id: String,
538 pub sample_count: usize,
540 pub mean: f64,
542 pub std_dev: f64,
544 pub min: f64,
546 pub max: f64,
548 pub median: f64,
550 pub recent_trend: TrendDirection,
552}