1use super::config::*;
8use super::optimizer::{Adaptation, AdaptationType};
9use super::resource_management::ResourceUsage;
10
11use crate::utils::{scalar_or, try_scalar_str};
12use scirs2_core::numeric::Float;
13use std::collections::{HashMap, VecDeque};
14use std::iter::Sum;
15use std::time::{Duration, Instant};
16
17#[derive(Debug, Clone)]
19pub struct PerformanceSnapshot<A: Float + Send + Sync> {
20 pub timestamp: Instant,
22 pub processing_duration: Duration,
28 pub loss: A,
30 pub accuracy: Option<A>,
32 pub convergence_rate: Option<A>,
34 pub gradient_norm: Option<A>,
36 pub parameter_update_magnitude: Option<A>,
38 pub data_statistics: DataStatistics<A>,
40 pub resource_usage: ResourceUsage,
42 pub custom_metrics: HashMap<String, A>,
44}
45
46#[derive(Debug, Clone)]
48pub struct DataStatistics<A: Float + Send + Sync> {
49 pub sample_count: usize,
51 pub feature_means: scirs2_core::ndarray::Array1<A>,
53 pub feature_stds: scirs2_core::ndarray::Array1<A>,
55 pub average_quality: A,
57 pub timestamp: Instant,
59}
60
61impl<A: Float + Send + Sync> Default for DataStatistics<A> {
62 fn default() -> Self {
63 Self {
64 sample_count: 0,
65 feature_means: scirs2_core::ndarray::Array1::zeros(0),
66 feature_stds: scirs2_core::ndarray::Array1::zeros(0),
67 average_quality: A::zero(),
68 timestamp: Instant::now(),
69 }
70 }
71}
72
73#[derive(Debug, Clone)]
75pub enum PerformanceMetric<A: Float + Send + Sync> {
76 Loss(A),
78 Accuracy(A),
80 ConvergenceRate(A),
82 GradientNorm(A),
84 LearningRateEffectiveness(A),
86 ResourceEfficiency(A),
88 DataQuality(A),
90 Custom(String, A),
92}
93
94#[derive(Debug, Clone)]
96pub struct PerformanceContext<A: Float + Send + Sync> {
97 pub learning_rate: A,
99 pub batch_size: usize,
101 pub buffer_size: usize,
103 pub drift_detected: bool,
105 pub resource_constraints: ResourceUsage,
107 pub time_since_adaptation: Duration,
109}
110
111pub struct PerformanceTracker<A: Float + Send + Sync + std::iter::Sum> {
113 config: PerformanceConfig,
115 performance_history: VecDeque<PerformanceSnapshot<A>>,
117 trend_analyzer: PerformanceTrendAnalyzer<A>,
119 predictor: PerformancePredictor<A>,
121 baseline: Option<PerformanceSnapshot<A>>,
123 current_context: Option<PerformanceContext<A>>,
125 improvement_tracker: PerformanceImprovementTracker<A>,
127 performance_anomaly_detector: PerformanceAnomalyDetector<A>,
129 snapshots_since_baseline: usize,
132}
133
134pub struct PerformanceTrendAnalyzer<A: Float + Send + Sync> {
136 window_size: usize,
138 trends: HashMap<String, TrendData<A>>,
140}
141
142#[derive(Debug, Clone)]
144pub struct TrendData<A: Float + Send + Sync> {
145 pub slope: A,
147 pub correlation: A,
149 pub volatility: A,
151 pub confidence: A,
153 pub recent_values: VecDeque<A>,
155 pub last_update: Instant,
157}
158
159#[derive(Debug, Clone)]
161pub enum TrendMethod {
162 LinearRegression,
164 MovingAverage { window: usize },
166 ExponentialSmoothing { alpha: f64 },
168 SeasonalDecomposition,
170}
171
172pub struct PerformancePredictor<A: Float + Send + Sync> {
174 prediction_methods: Vec<PredictionMethod>,
176 prediction_history: VecDeque<PredictionResult<A>>,
178 model_accuracies: HashMap<String, A>,
181 ensemble_weights: HashMap<String, A>,
183 pending_method_forecasts: VecDeque<MethodForecast<A>>,
189 snapshots_since_start: usize,
192}
193
194#[derive(Debug, Clone)]
196struct MethodForecast<A: Float + Send + Sync> {
197 label: String,
199 predicted_value: A,
201 steps_ahead: usize,
203 issued_at_index: usize,
205}
206
207#[derive(Debug, Clone)]
209pub enum PredictionMethod {
210 Linear,
212 Exponential { alpha: f64, beta: f64 },
214 ARIMA { p: usize, d: usize, q: usize },
216 NeuralNetwork { hidden_layers: Vec<usize> },
218 Ensemble,
220}
221
222#[derive(Debug, Clone)]
224pub struct PredictionResult<A: Float + Send + Sync> {
225 pub predicted_value: A,
227 pub confidence_interval: (A, A),
229 pub method: String,
231 pub steps_ahead: usize,
233 pub issued_at_index: usize,
236 pub timestamp: Instant,
238 pub actual_value: Option<A>,
240}
241
242pub struct PerformanceImprovementTracker<A: Float + Send + Sync> {
244 baseline_metrics: HashMap<String, A>,
246 improvement_rates: HashMap<String, A>,
248 improvement_history: VecDeque<ImprovementEvent<A>>,
250 plateau_detector: PlateauDetector<A>,
252}
253
254#[derive(Debug, Clone)]
256pub struct ImprovementEvent<A: Float + Send + Sync> {
257 pub timestamp: Instant,
259 pub metric_name: String,
261 pub improvement: A,
263 pub improvement_rate: A,
265 pub context: String,
267}
268
269pub struct PlateauDetector<A: Float + Send + Sync> {
271 window_size: usize,
273 plateau_threshold: A,
275 recent_values: VecDeque<A>,
277 is_plateau: bool,
279 plateau_duration: Duration,
281 plateau_started: Option<Instant>,
283 last_significant_change: Option<Instant>,
285}
286
287pub struct PerformanceAnomalyDetector<A: Float + Send + Sync> {
289 threshold: A,
291 historical_stats: HashMap<String, MetricStatistics<A>>,
293 recent_anomalies: VecDeque<PerformanceAnomaly<A>>,
295 adaptive_threshold: bool,
297}
298
299#[derive(Debug, Clone)]
301pub struct MetricStatistics<A: Float + Send + Sync> {
302 pub mean: A,
304 pub variance: A,
306 pub min_value: A,
308 pub max_value: A,
310 pub count: usize,
312 pub last_update: Instant,
314}
315
316#[derive(Debug, Clone)]
318pub struct PerformanceAnomaly<A: Float + Send + Sync> {
319 pub timestamp: Instant,
321 pub metric_name: String,
323 pub observed_value: A,
325 pub expected_range: (A, A),
327 pub severity: AnomalySeverity,
329 pub anomaly_type: AnomalyType,
331}
332
333#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
335pub enum AnomalySeverity {
336 Minor,
338 Moderate,
340 Major,
342 Critical,
344}
345
346#[derive(Debug, Clone, PartialEq, Eq)]
348pub enum AnomalyType {
349 High,
351 Low,
353 TrendChange,
355 Oscillation,
357 Degradation,
359 Plateau,
361}
362
363impl<A: Float + Default + Clone + std::iter::Sum + Send + Sync + std::fmt::Debug>
364 PerformanceTracker<A>
365{
366 pub fn new(config: &StreamingConfig) -> Result<Self, String> {
368 let performance_config = config.performance_config.clone();
369
370 let trend_analyzer = PerformanceTrendAnalyzer::new(performance_config.trend_window_size);
371 let predictor = PerformancePredictor::new();
372 let improvement_tracker = PerformanceImprovementTracker::new();
373 let performance_anomaly_detector = PerformanceAnomalyDetector::new(2.0); Ok(Self {
376 config: performance_config,
377 performance_history: VecDeque::with_capacity(1000),
378 trend_analyzer,
379 predictor,
380 baseline: None,
381 snapshots_since_baseline: 0,
382 current_context: None,
383 improvement_tracker,
384 performance_anomaly_detector,
385 })
386 }
387
388 #[cfg(test)]
390 pub(crate) fn baseline_loss_for_test(&self) -> Option<A> {
391 self.baseline.as_ref().map(|snapshot| snapshot.loss)
392 }
393
394 pub fn add_performance(&mut self, snapshot: PerformanceSnapshot<A>) -> Result<(), String> {
408 if !self.config.enable_tracking {
409 return Ok(());
410 }
411
412 if self.performance_history.len() >= self.config.history_size {
414 self.performance_history.pop_front();
415 }
416 self.performance_history.push_back(snapshot.clone());
417
418 self.snapshots_since_baseline = self.snapshots_since_baseline.saturating_add(1);
421 let refresh_due = self.config.baseline_update_frequency > 0
422 && self.snapshots_since_baseline >= self.config.baseline_update_frequency;
423 if self.baseline.is_none() || refresh_due {
424 self.baseline = Some(snapshot.clone());
425 if refresh_due {
431 self.snapshots_since_baseline = 0;
432 }
433 }
434
435 if self.config.enable_trend_analysis {
437 self.trend_analyzer.update(&snapshot)?;
438 }
439
440 self.improvement_tracker.update(&snapshot)?;
442
443 let anomalies = self
445 .performance_anomaly_detector
446 .check_for_anomalies(&snapshot)?;
447 if !anomalies.is_empty() {
448 self.handle_performance_anomalies(&anomalies)?;
450 }
451
452 if self.config.enable_prediction {
454 self.predictor.update_with_actual(&snapshot)?;
455 }
456
457 Ok(())
458 }
459
460 pub fn get_recent_performance(&self, count: usize) -> Vec<PerformanceSnapshot<A>> {
462 self.performance_history
463 .iter()
464 .rev()
465 .take(count)
466 .cloned()
467 .collect()
468 }
469
470 pub fn get_recent_losses(&self, count: usize) -> Vec<A> {
472 self.performance_history
473 .iter()
474 .rev()
475 .take(count)
476 .map(|snapshot| snapshot.loss)
477 .collect()
478 }
479
480 pub fn predict_performance(
482 &mut self,
483 steps_ahead: usize,
484 ) -> Result<PredictionResult<A>, String> {
485 if !self.config.enable_prediction {
486 return Err("Performance prediction is disabled".to_string());
487 }
488
489 self.predictor
490 .predict(steps_ahead, &self.performance_history)
491 }
492
493 pub fn get_performance_trends(&self) -> HashMap<String, TrendData<A>> {
495 self.trend_analyzer.get_current_trends()
496 }
497
498 pub fn apply_threshold_adaptation(&mut self, adaptation: &Adaptation<A>) -> Result<(), String> {
500 if adaptation.adaptation_type == AdaptationType::PerformanceThreshold {
501 let new_threshold = self.performance_anomaly_detector.threshold + adaptation.magnitude;
503 self.performance_anomaly_detector
504 .update_threshold(new_threshold);
505 }
506 Ok(())
507 }
508
509 fn handle_performance_anomalies(
511 &mut self,
512 anomalies: &[PerformanceAnomaly<A>],
513 ) -> Result<(), String> {
514 for anomaly in anomalies {
515 match anomaly.severity {
516 AnomalySeverity::Critical | AnomalySeverity::Major => {
517 println!("Critical performance anomaly detected: {:?}", anomaly);
519 }
520 _ => {
521 self.performance_anomaly_detector
523 .recent_anomalies
524 .push_back(anomaly.clone());
525 }
526 }
527 }
528 Ok(())
529 }
530
531 pub fn reset(&mut self) -> Result<(), String> {
533 self.performance_history.clear();
534 self.baseline = None;
535 self.snapshots_since_baseline = 0;
536 self.current_context = None;
537 self.trend_analyzer.reset();
538 self.predictor.reset();
539 self.improvement_tracker.reset();
540 self.performance_anomaly_detector.reset();
541 Ok(())
542 }
543
544 pub fn get_diagnostics(&self) -> PerformanceDiagnostics {
546 PerformanceDiagnostics {
547 history_size: self.performance_history.len(),
548 baseline_set: self.baseline.is_some(),
549 trends_available: !self.trend_analyzer.trends.is_empty(),
550 anomalies_detected: self.performance_anomaly_detector.recent_anomalies.len(),
551 plateau_detected: self.improvement_tracker.plateau_detector.is_plateau,
552 prediction_accuracy: self.predictor.get_average_accuracy(),
553 }
554 }
555}
556
557impl<A: Float + Default + Clone + Send + Sync + std::iter::Sum> PerformanceTrendAnalyzer<A> {
558 fn new(window_size: usize) -> Self {
559 Self {
560 window_size,
561 trends: HashMap::new(),
562 }
563 }
564
565 fn update(&mut self, snapshot: &PerformanceSnapshot<A>) -> Result<(), String> {
566 self.update_metric_trend("loss", snapshot.loss)?;
568
569 if let Some(accuracy) = snapshot.accuracy {
570 self.update_metric_trend("accuracy", accuracy)?;
571 }
572
573 if let Some(convergence) = snapshot.convergence_rate {
574 self.update_metric_trend("convergence", convergence)?;
575 }
576
577 self.compute_trends()?;
578 Ok(())
579 }
580
581 fn update_metric_trend(&mut self, metric_name: &str, value: A) -> Result<(), String> {
582 let trend_data = self
583 .trends
584 .entry(metric_name.to_string())
585 .or_insert_with(|| TrendData {
586 slope: A::zero(),
587 correlation: A::zero(),
588 volatility: A::zero(),
589 confidence: A::zero(),
590 recent_values: VecDeque::with_capacity(self.window_size),
591 last_update: Instant::now(),
592 });
593
594 if trend_data.recent_values.len() >= self.window_size {
595 trend_data.recent_values.pop_front();
596 }
597 trend_data.recent_values.push_back(value);
598 trend_data.last_update = Instant::now();
599
600 Ok(())
601 }
602
603 fn compute_trends(&mut self) -> Result<(), String> {
604 let keys: Vec<_> = self.trends.keys().cloned().collect();
605
606 let mut computed_values = Vec::new();
608 for metric_name in &keys {
609 if let Some(trend_data) = self.trends.get(metric_name) {
610 if trend_data.recent_values.len() >= 3 {
611 let values = trend_data.recent_values.clone();
612 let slope = self.compute_slope(&values)?;
613 let correlation = self.compute_correlation(&values)?;
614 let volatility = self.compute_volatility(&values)?;
615 let confidence = self.compute_confidence(&values)?;
616 computed_values.push((
617 metric_name.clone(),
618 slope,
619 correlation,
620 volatility,
621 confidence,
622 ));
623 }
624 }
625 }
626
627 for (metric_name, slope, correlation, volatility, confidence) in computed_values {
629 if let Some(trend_data) = self.trends.get_mut(&metric_name) {
630 trend_data.slope = slope;
631 trend_data.correlation = correlation;
632 trend_data.volatility = volatility;
633 trend_data.confidence = confidence;
634 }
635 }
636
637 Ok(())
638 }
639
640 fn compute_slope(&self, values: &VecDeque<A>) -> Result<A, String> {
641 if values.len() < 2 {
642 return Ok(A::zero());
643 }
644
645 let n = try_scalar_str::<A, _>(values.len())?;
646 let sum_x = n * (n + A::one()) / try_scalar_str::<A, _>(2.0)?;
648 let sum_y = values.iter().cloned().sum::<A>();
649 let sum_xy = values
650 .iter()
651 .enumerate()
652 .map(|(i, &y)| try_scalar_str::<A, _>(i + 1).map(|x| x * y))
653 .collect::<Result<Vec<A>, String>>()?
654 .into_iter()
655 .sum::<A>();
656 let two = try_scalar_str::<A, _>(2.0)?;
658 let six = try_scalar_str::<A, _>(6.0)?;
659 let sum_x_squared = n * (n + A::one()) * (two * n + A::one()) / six;
660
661 let denominator = n * sum_x_squared - sum_x * sum_x;
662 if denominator == A::zero() {
663 return Ok(A::zero());
664 }
665
666 let slope = (n * sum_xy - sum_x * sum_y) / denominator;
667 Ok(slope)
668 }
669
670 fn compute_correlation(&self, values: &VecDeque<A>) -> Result<A, String> {
671 if values.len() < 2 {
672 return Ok(A::zero());
673 }
674
675 let n = values.len();
677 let time_values: Vec<A> = (1..=n)
678 .map(try_scalar_str::<A, _>)
679 .collect::<Result<Vec<A>, String>>()?;
680 let value_vec: Vec<A> = values.iter().cloned().collect();
681
682 let mean_time = time_values.iter().cloned().sum::<A>() / try_scalar_str::<A, _>(n)?;
683 let mean_value = value_vec.iter().cloned().sum::<A>() / try_scalar_str::<A, _>(n)?;
684
685 let numerator = time_values
686 .iter()
687 .zip(value_vec.iter())
688 .map(|(&t, &v)| (t - mean_time) * (v - mean_value))
689 .sum::<A>();
690
691 let time_variance = time_values
692 .iter()
693 .map(|&t| (t - mean_time) * (t - mean_time))
694 .sum::<A>();
695
696 let value_variance = value_vec
697 .iter()
698 .map(|&v| (v - mean_value) * (v - mean_value))
699 .sum::<A>();
700
701 let denominator = (time_variance * value_variance).sqrt();
702 if denominator == A::zero() {
703 return Ok(A::zero());
704 }
705
706 Ok(numerator / denominator)
707 }
708
709 fn compute_volatility(&self, values: &VecDeque<A>) -> Result<A, String> {
710 if values.len() < 2 {
711 return Ok(A::zero());
712 }
713
714 let mean = values.iter().cloned().sum::<A>() / try_scalar_str::<A, _>(values.len())?;
715 let variance = values.iter().map(|&v| (v - mean) * (v - mean)).sum::<A>()
716 / try_scalar_str::<A, _>(values.len())?;
717
718 Ok(variance.sqrt())
719 }
720
721 fn compute_confidence(&self, values: &VecDeque<A>) -> Result<A, String> {
722 if values.len() < 3 {
724 return Ok(A::zero());
725 }
726
727 let slope = self.compute_slope(values)?;
728 let correlation = self.compute_correlation(values)?;
729
730 let confidence = correlation.abs() * (A::one() - (slope.abs() / (slope.abs() + A::one())));
732 Ok(confidence)
733 }
734
735 fn get_current_trends(&self) -> HashMap<String, TrendData<A>> {
736 self.trends.clone()
737 }
738
739 fn reset(&mut self) {
740 self.trends.clear();
741 }
742}
743
744impl<A: Float + Default + Clone + Send + Sync + std::iter::Sum> PerformancePredictor<A> {
745 fn new() -> Self {
746 Self {
747 prediction_methods: vec![
748 PredictionMethod::Linear,
749 PredictionMethod::Exponential {
750 alpha: 0.3,
751 beta: 0.1,
752 },
753 ],
754 prediction_history: VecDeque::with_capacity(1000),
755 model_accuracies: HashMap::new(),
756 ensemble_weights: HashMap::new(),
757 pending_method_forecasts: VecDeque::with_capacity(1000),
758 snapshots_since_start: 0,
759 }
760 }
761
762 fn predict(
773 &mut self,
774 steps_ahead: usize,
775 history: &VecDeque<PerformanceSnapshot<A>>,
776 ) -> Result<PredictionResult<A>, String> {
777 if history.len() < 2 {
778 return Err("Insufficient history for prediction".to_string());
779 }
780
781 let loss_values: Vec<A> = history.iter().map(|s| s.loss).collect();
783
784 let methods = self.prediction_methods.clone();
786 let mut forecasts: Vec<(String, A)> = Vec::with_capacity(methods.len());
787 for method in &methods {
788 let (label, value) = match method {
789 PredictionMethod::Linear => (
790 "linear".to_string(),
791 self.linear_prediction(&loss_values, steps_ahead)?,
792 ),
793 PredictionMethod::Exponential { alpha, beta } => (
794 format!("exponential(alpha={alpha},beta={beta})"),
795 self.exponential_prediction(&loss_values, steps_ahead, *alpha, *beta)?,
796 ),
797 other => {
798 return Err(format!(
801 "prediction method {other:?} has no implementation; remove it from \
802 `prediction_methods` or implement it"
803 ));
804 }
805 };
806 if value.is_finite() {
807 forecasts.push((label, value));
808 }
809 }
810
811 if forecasts.is_empty() {
812 return Err("no prediction method produced a finite forecast".to_string());
813 }
814
815 let mut total_weight = A::zero();
818 let mut weighted_sum = A::zero();
819 for (label, value) in &forecasts {
820 let accuracy = self
823 .model_accuracies
824 .get(label)
825 .copied()
826 .unwrap_or_else(A::one);
827 let floor = A::from(0.05).ok_or_else(|| "0.05 is not representable".to_string())?;
828 let weight = accuracy.max(floor);
829 self.ensemble_weights.insert(label.clone(), weight);
830 total_weight = total_weight + weight;
831 weighted_sum = weighted_sum + weight * *value;
832 }
833 if total_weight <= A::zero() {
834 return Err("ensemble weights sum to zero".to_string());
835 }
836 let predicted_value = weighted_sum / total_weight;
837
838 let recent_volatility = self.compute_recent_volatility(&loss_values)?;
842 let spread = if forecasts.len() > 1 {
843 let values: Vec<A> = forecasts.iter().map(|(_, value)| *value).collect();
844 let count = A::from(values.len())
845 .ok_or_else(|| "sample count not representable".to_string())?;
846 let mean = values.iter().fold(A::zero(), |acc, &v| acc + v) / count;
847 (values
848 .iter()
849 .fold(A::zero(), |acc, &v| acc + (v - mean) * (v - mean))
850 / count)
851 .sqrt()
852 } else {
853 A::zero()
854 };
855 let half_width = recent_volatility + spread;
856 let confidence_interval = (predicted_value - half_width, predicted_value + half_width);
857
858 let method = if forecasts.len() == 1 {
859 forecasts[0].0.clone()
860 } else {
861 format!(
862 "ensemble[{}]",
863 forecasts
864 .iter()
865 .map(|(label, _)| label.as_str())
866 .collect::<Vec<_>>()
867 .join(",")
868 )
869 };
870
871 let prediction = PredictionResult {
872 predicted_value,
873 confidence_interval,
874 method,
875 steps_ahead,
876 issued_at_index: self.snapshots_since_start,
877 timestamp: Instant::now(),
878 actual_value: None,
879 };
880
881 if self.prediction_history.len() >= 1000 {
883 self.prediction_history.pop_front();
884 }
885 self.prediction_history.push_back(prediction.clone());
886
887 let issued_at_index = self.snapshots_since_start;
890 for (label, value) in forecasts {
891 if self.pending_method_forecasts.len() >= 1000 {
892 self.pending_method_forecasts.pop_front();
893 }
894 self.pending_method_forecasts.push_back(MethodForecast {
895 label,
896 predicted_value: value,
897 steps_ahead,
898 issued_at_index,
899 });
900 }
901
902 Ok(prediction)
903 }
904
905 fn linear_prediction(&self, values: &[A], steps_ahead: usize) -> Result<A, String> {
912 let n = values.len();
913 if n == 0 {
914 return Err("linear prediction requires at least one observation".to_string());
915 }
916 if n < 2 {
917 return Ok(values[0]);
918 }
919
920 let count = A::from(n).ok_or_else(|| "sample count not representable".to_string())?;
921 let two = A::from(2.0).ok_or_else(|| "2.0 not representable".to_string())?;
922 let six = A::from(6.0).ok_or_else(|| "6.0 not representable".to_string())?;
923
924 let sum_x = count * (count + A::one()) / two;
926 let sum_x_squared = count * (count + A::one()) * (two * count + A::one()) / six;
927 let mut sum_y = A::zero();
928 let mut sum_xy = A::zero();
929 for (index, &value) in values.iter().enumerate() {
930 let x = A::from(index + 1).ok_or_else(|| format!("index {index} not representable"))?;
931 sum_y = sum_y + value;
932 sum_xy = sum_xy + x * value;
933 }
934
935 let denominator = count * sum_x_squared - sum_x * sum_x;
936 if denominator == A::zero() {
937 return Ok(values[n - 1]);
938 }
939 let slope = (count * sum_xy - sum_x * sum_y) / denominator;
940 let intercept = (sum_y - slope * sum_x) / count;
941
942 let horizon = A::from(n + steps_ahead)
943 .ok_or_else(|| "forecast horizon not representable".to_string())?;
944 Ok(intercept + slope * horizon)
945 }
946
947 fn exponential_prediction(
955 &self,
956 values: &[A],
957 steps_ahead: usize,
958 alpha: f64,
959 beta: f64,
960 ) -> Result<A, String> {
961 if values.is_empty() {
962 return Err("exponential prediction requires at least one observation".to_string());
963 }
964 if values.len() < 2 {
965 return Ok(values[0]);
966 }
967
968 let alpha = A::from(alpha.clamp(f64::MIN_POSITIVE, 1.0))
969 .ok_or_else(|| "alpha not representable".to_string())?;
970 let beta =
971 A::from(beta.clamp(0.0, 1.0)).ok_or_else(|| "beta not representable".to_string())?;
972
973 let mut level = values[0];
974 let mut trend = values[1] - values[0];
975 for &value in values.iter().skip(1) {
976 let previous_level = level;
977 level = alpha * value + (A::one() - alpha) * (previous_level + trend);
978 trend = beta * (level - previous_level) + (A::one() - beta) * trend;
979 }
980
981 let horizon =
982 A::from(steps_ahead).ok_or_else(|| "forecast horizon not representable".to_string())?;
983 Ok(level + horizon * trend)
984 }
985
986 fn compute_recent_volatility(&self, values: &[A]) -> Result<A, String> {
987 if values.len() < 2 {
988 return Ok(A::zero());
989 }
990
991 let recent_count = values.len().min(10);
992 let recent_values = &values[values.len() - recent_count..];
993
994 let mean = recent_values.iter().cloned().sum::<A>() / try_scalar_str::<A, _>(recent_count)?;
995 let variance = recent_values
996 .iter()
997 .map(|&v| (v - mean) * (v - mean))
998 .sum::<A>()
999 / try_scalar_str::<A, _>(recent_count)?;
1000
1001 Ok(variance.sqrt())
1002 }
1003
1004 fn update_with_actual(&mut self, snapshot: &PerformanceSnapshot<A>) -> Result<(), String> {
1014 self.snapshots_since_start = self.snapshots_since_start.saturating_add(1);
1015 let now = self.snapshots_since_start;
1016
1017 let mut matured: Vec<MethodForecast<A>> = Vec::new();
1020 self.pending_method_forecasts.retain(|forecast| {
1021 let due_at = forecast.issued_at_index + forecast.steps_ahead.max(1);
1022 if now >= due_at {
1023 matured.push(forecast.clone());
1024 false
1025 } else {
1026 true
1027 }
1028 });
1029
1030 for forecast in &matured {
1031 let accuracy = Self::accuracy_of(forecast.predicted_value, snapshot.loss)?;
1032 let smoothing = A::from(0.3).ok_or_else(|| "0.3 is not representable".to_string())?;
1035 let updated = match self.model_accuracies.get(&forecast.label) {
1036 Some(&previous) => smoothing * accuracy + (A::one() - smoothing) * previous,
1037 None => accuracy,
1038 };
1039 self.model_accuracies
1040 .insert(forecast.label.clone(), updated);
1041 }
1042
1043 let mut index = 0usize;
1046 while index < self.prediction_history.len() {
1047 if let Some(prediction) = self.prediction_history.get_mut(index) {
1048 if prediction.actual_value.is_none()
1049 && now >= prediction.issued_at_index + prediction.steps_ahead.max(1)
1050 {
1051 prediction.actual_value = Some(snapshot.loss);
1052 }
1053 }
1054 index += 1;
1055 }
1056
1057 Ok(())
1058 }
1059
1060 fn accuracy_of(predicted: A, actual: A) -> Result<A, String> {
1064 let epsilon = A::from(1e-8).ok_or_else(|| "1e-8 is not representable".to_string())?;
1065 let error = (predicted - actual).abs();
1066 let scale = actual.abs().max(epsilon);
1067 Ok((A::one() - (error / scale).min(A::one())).max(A::zero()))
1068 }
1069
1070 pub fn method_accuracy(&self, label: &str) -> Option<A> {
1072 self.model_accuracies.get(label).copied()
1073 }
1074
1075 pub fn ensemble_weights(&self) -> &HashMap<String, A> {
1077 &self.ensemble_weights
1078 }
1079
1080 fn get_average_accuracy(&self) -> f64 {
1081 if self.model_accuracies.is_empty() {
1082 return 0.0;
1083 }
1084
1085 let sum: A = self.model_accuracies.values().cloned().sum();
1086 let avg = sum / scalar_or(self.model_accuracies.len(), A::one());
1087 avg.to_f64().unwrap_or(0.0)
1088 }
1089
1090 fn reset(&mut self) {
1091 self.prediction_history.clear();
1092 self.model_accuracies.clear();
1093 self.ensemble_weights.clear();
1094 self.pending_method_forecasts.clear();
1095 self.snapshots_since_start = 0;
1096 }
1097}
1098
1099impl<A: Float + Default + Clone + Sum + Send + Sync + Send + Sync>
1100 PerformanceImprovementTracker<A>
1101{
1102 fn new() -> Self {
1103 Self {
1104 baseline_metrics: HashMap::new(),
1105 improvement_rates: HashMap::new(),
1106 improvement_history: VecDeque::with_capacity(1000),
1107 plateau_detector: PlateauDetector::new(50, scalar_or(0.01, A::zero())),
1108 }
1109 }
1110
1111 fn update(&mut self, snapshot: &PerformanceSnapshot<A>) -> Result<(), String> {
1112 if self.baseline_metrics.is_empty() {
1114 self.baseline_metrics
1115 .insert("loss".to_string(), snapshot.loss);
1116 if let Some(accuracy) = snapshot.accuracy {
1117 self.baseline_metrics
1118 .insert("accuracy".to_string(), accuracy);
1119 }
1120 }
1121
1122 if let Some(&baseline_loss) = self.baseline_metrics.get("loss") {
1124 if snapshot.loss < baseline_loss {
1125 let improvement = baseline_loss - snapshot.loss;
1126
1127 let elapsed = match self.improvement_history.back() {
1133 Some(previous) => snapshot
1134 .timestamp
1135 .saturating_duration_since(previous.timestamp),
1136 None => snapshot.processing_duration,
1137 };
1138 let seconds = elapsed.as_secs_f64();
1139 let improvement_rate = if seconds > 0.0 {
1140 let divisor = A::from(seconds)
1141 .ok_or_else(|| format!("elapsed {seconds}s is not representable"))?;
1142 improvement / divisor
1143 } else {
1144 improvement
1147 };
1148
1149 let improvement_event = ImprovementEvent {
1150 timestamp: snapshot.timestamp,
1151 metric_name: "loss".to_string(),
1152 improvement,
1153 improvement_rate,
1154 context: "optimization_step".to_string(),
1155 };
1156
1157 if self.improvement_history.len() >= 1000 {
1158 self.improvement_history.pop_front();
1159 }
1160 self.improvement_history.push_back(improvement_event);
1161 self.improvement_rates
1162 .insert("loss".to_string(), improvement_rate);
1163
1164 self.baseline_metrics
1166 .insert("loss".to_string(), snapshot.loss);
1167 }
1168 }
1169
1170 self.plateau_detector.update(snapshot.loss);
1172
1173 Ok(())
1174 }
1175
1176 fn reset(&mut self) {
1177 self.baseline_metrics.clear();
1178 self.improvement_rates.clear();
1179 self.improvement_history.clear();
1180 self.plateau_detector.reset();
1181 }
1182}
1183
1184impl<A: Float + Default + Clone + Send + Sync + std::iter::Sum> PlateauDetector<A> {
1185 fn new(window_size: usize, threshold: A) -> Self {
1186 Self {
1187 window_size,
1188 plateau_threshold: threshold,
1189 recent_values: VecDeque::with_capacity(window_size),
1190 is_plateau: false,
1191 plateau_duration: Duration::ZERO,
1192 plateau_started: None,
1193 last_significant_change: None,
1194 }
1195 }
1196
1197 fn update(&mut self, value: A) {
1198 if self.recent_values.len() >= self.window_size {
1199 self.recent_values.pop_front();
1200 }
1201 self.recent_values.push_back(value);
1202
1203 if self.recent_values.len() >= self.window_size {
1204 self.detect_plateau();
1205 }
1206 }
1207
1208 fn detect_plateau(&mut self) {
1223 let mut values = self.recent_values.iter().copied();
1224 let Some(first) = values.next() else {
1225 return;
1226 };
1227 if self.recent_values.len() < 2 {
1228 return;
1229 }
1230
1231 let mut min_val = first;
1232 let mut max_val = first;
1233 for value in values {
1234 if value < min_val {
1235 min_val = value;
1236 }
1237 if value > max_val {
1238 max_val = value;
1239 }
1240 }
1241 let range = max_val - min_val;
1242
1243 let was_plateau = self.is_plateau;
1244 self.is_plateau = range < self.plateau_threshold;
1245
1246 if self.is_plateau {
1247 if !was_plateau {
1248 self.plateau_started = Some(Instant::now());
1250 self.plateau_duration = Duration::ZERO;
1251 } else if let Some(started) = self.plateau_started {
1252 self.plateau_duration = started.elapsed();
1253 }
1254 } else {
1255 self.last_significant_change = Some(Instant::now());
1256 self.plateau_started = None;
1257 self.plateau_duration = Duration::ZERO;
1258 }
1259 }
1260
1261 pub fn is_plateau(&self) -> bool {
1263 self.is_plateau
1264 }
1265
1266 pub fn plateau_duration(&self) -> Duration {
1269 self.plateau_duration
1270 }
1271
1272 pub fn last_significant_change(&self) -> Option<Instant> {
1274 self.last_significant_change
1275 }
1276
1277 fn reset(&mut self) {
1278 self.recent_values.clear();
1279 self.is_plateau = false;
1280 self.plateau_duration = Duration::ZERO;
1281 self.last_significant_change = None;
1282 }
1283}
1284
1285impl<A: Float + Default + Clone + Sum + Send + Sync + Send + Sync> PerformanceAnomalyDetector<A> {
1286 fn new(threshold: f64) -> Self {
1287 Self {
1288 threshold: scalar_or(threshold, A::zero()),
1289 historical_stats: HashMap::new(),
1290 recent_anomalies: VecDeque::with_capacity(100),
1291 adaptive_threshold: true,
1292 }
1293 }
1294
1295 fn check_for_anomalies(
1296 &mut self,
1297 snapshot: &PerformanceSnapshot<A>,
1298 ) -> Result<Vec<PerformanceAnomaly<A>>, String> {
1299 let mut anomalies = Vec::new();
1300
1301 let loss_anomaly = self.check_metric_anomaly("loss", snapshot.loss, snapshot.timestamp)?;
1303 if let Some(anomaly) = loss_anomaly {
1304 anomalies.push(anomaly);
1305 }
1306
1307 if let Some(accuracy) = snapshot.accuracy {
1309 let accuracy_anomaly =
1310 self.check_metric_anomaly("accuracy", accuracy, snapshot.timestamp)?;
1311 if let Some(anomaly) = accuracy_anomaly {
1312 anomalies.push(anomaly);
1313 }
1314 }
1315
1316 Ok(anomalies)
1317 }
1318
1319 fn check_metric_anomaly(
1320 &mut self,
1321 metric_name: &str,
1322 value: A,
1323 timestamp: Instant,
1324 ) -> Result<Option<PerformanceAnomaly<A>>, String> {
1325 let stats = self
1327 .historical_stats
1328 .entry(metric_name.to_string())
1329 .or_insert_with(|| MetricStatistics {
1330 mean: value,
1331 variance: A::zero(),
1332 min_value: value,
1333 max_value: value,
1334 count: 0,
1335 last_update: timestamp,
1336 });
1337
1338 stats.count += 1;
1340 let delta = value - stats.mean;
1341 stats.mean = stats.mean + delta / try_scalar_str::<A, _>(stats.count)?;
1342 let delta2 = value - stats.mean;
1343 stats.variance = stats.variance + delta * delta2;
1344 stats.min_value = stats.min_value.min(value);
1345 stats.max_value = stats.max_value.max(value);
1346 stats.last_update = timestamp;
1347
1348 if stats.count >= 10 {
1350 let std_dev = (stats.variance / try_scalar_str::<A, _>(stats.count - 1)?).sqrt();
1351 let z_score = (value - stats.mean) / std_dev.max(try_scalar_str::<A, _>(1e-8)?);
1352
1353 if z_score.abs() > self.threshold {
1354 let severity = if z_score.abs() > try_scalar_str::<A, _>(3.0)? {
1355 AnomalySeverity::Critical
1356 } else if z_score.abs() > try_scalar_str::<A, _>(2.5)? {
1357 AnomalySeverity::Major
1358 } else {
1359 AnomalySeverity::Moderate
1360 };
1361
1362 let anomaly_type = if z_score > A::zero() {
1363 AnomalyType::High
1364 } else {
1365 AnomalyType::Low
1366 };
1367
1368 let expected_range = (
1369 stats.mean - self.threshold * std_dev,
1370 stats.mean + self.threshold * std_dev,
1371 );
1372
1373 let anomaly = PerformanceAnomaly {
1374 timestamp,
1375 metric_name: metric_name.to_string(),
1376 observed_value: value,
1377 expected_range,
1378 severity,
1379 anomaly_type,
1380 };
1381
1382 return Ok(Some(anomaly));
1383 }
1384 }
1385
1386 Ok(None)
1387 }
1388
1389 fn update_threshold(&mut self, new_threshold: A) -> bool {
1397 if !self.adaptive_threshold {
1398 return false;
1399 }
1400 self.threshold = new_threshold;
1401 true
1402 }
1403
1404 pub fn is_threshold_adaptive(&self) -> bool {
1406 self.adaptive_threshold
1407 }
1408
1409 fn reset(&mut self) {
1410 self.historical_stats.clear();
1411 self.recent_anomalies.clear();
1412 }
1413}
1414
1415#[derive(Debug, Clone)]
1417pub struct PerformanceDiagnostics {
1418 pub history_size: usize,
1419 pub baseline_set: bool,
1420 pub trends_available: bool,
1421 pub anomalies_detected: usize,
1422 pub plateau_detected: bool,
1423 pub prediction_accuracy: f64,
1424}
1425
1426#[cfg(test)]
1427mod plateau_and_prediction_regression_tests {
1428 use super::*;
1429 use scirs2_core::ndarray::Array1;
1430
1431 fn snapshot(loss: f64) -> PerformanceSnapshot<f64> {
1432 PerformanceSnapshot {
1433 timestamp: Instant::now(),
1434 processing_duration: Duration::from_millis(3),
1435 loss,
1436 accuracy: Some(1.0 - loss.min(1.0)),
1437 convergence_rate: None,
1438 gradient_norm: Some(loss.sqrt()),
1439 parameter_update_magnitude: Some(loss / 10.0),
1440 data_statistics: DataStatistics {
1441 sample_count: 1,
1442 feature_means: Array1::from_vec(vec![loss]),
1443 feature_stds: Array1::from_vec(vec![0.0]),
1444 average_quality: 1.0,
1445 timestamp: Instant::now(),
1446 },
1447 resource_usage: ResourceUsage::default(),
1448 custom_metrics: HashMap::new(),
1449 }
1450 }
1451
1452 #[test]
1459 fn plateau_is_detected_for_a_flat_series_away_from_zero() {
1460 let mut detector = PlateauDetector::<f64>::new(10, 0.01);
1461 for i in 0..10 {
1462 detector.update(5.0 + 0.001 * ((i % 2) as f64));
1464 }
1465 assert!(
1466 detector.is_plateau(),
1467 "P1f regression: a flat series at level 5.0 was not detected as a \
1468 plateau (the min seed was still 0)"
1469 );
1470 }
1471
1472 #[test]
1475 fn plateau_is_not_detected_for_a_varying_series() {
1476 let mut detector = PlateauDetector::<f64>::new(10, 0.01);
1477 for i in 0..10 {
1478 detector.update(5.0 + i as f64);
1479 }
1480 assert!(
1481 !detector.is_plateau(),
1482 "a series spanning 9.0 must not be a plateau under a 0.01 threshold"
1483 );
1484 assert_eq!(detector.plateau_duration(), Duration::ZERO);
1485 assert!(detector.last_significant_change().is_some());
1486 }
1487
1488 #[test]
1493 fn plateau_duration_is_real_elapsed_time_not_one_second_per_sample() {
1494 let mut detector = PlateauDetector::<f64>::new(5, 1.0);
1495 for _ in 0..25 {
1497 detector.update(3.0);
1498 }
1499 assert!(detector.is_plateau());
1500
1501 let duration = detector.plateau_duration();
1502 assert!(
1503 duration < Duration::from_secs(1),
1504 "P2f regression: plateau_duration is {duration:?} — the fabricated \
1505 one-second-per-sample clock is still in use (20 in-plateau updates \
1506 would have claimed ~20s)"
1507 );
1508 }
1509
1510 #[test]
1512 fn plateau_duration_grows_with_wall_clock_time() {
1513 let mut detector = PlateauDetector::<f64>::new(3, 1.0);
1514 for _ in 0..3 {
1515 detector.update(2.0);
1516 }
1517 assert!(detector.is_plateau());
1518 let first = detector.plateau_duration();
1519
1520 std::thread::sleep(Duration::from_millis(25));
1521 detector.update(2.0);
1522 let second = detector.plateau_duration();
1523
1524 assert!(
1525 second > first,
1526 "the plateau duration must advance with real time ({first:?} -> {second:?})"
1527 );
1528 assert!(
1529 second >= Duration::from_millis(20),
1530 "expected at least the ~25ms that actually elapsed, got {second:?}"
1531 );
1532 }
1533
1534 #[test]
1539 fn prediction_runs_every_configured_method_and_records_weights() {
1540 let config = StreamingConfig::default();
1541 let mut tracker = PerformanceTracker::<f64>::new(&config).expect("tracker");
1542
1543 for i in 0..12 {
1544 tracker
1545 .add_performance(snapshot(10.0 - 0.5 * i as f64))
1546 .expect("add_performance");
1547 }
1548
1549 let prediction = tracker.predict_performance(3).expect("prediction");
1550 assert!(
1551 prediction.method.starts_with("ensemble["),
1552 "P3f regression: only one method ran (method={})",
1553 prediction.method
1554 );
1555 assert!(
1556 prediction.method.contains("linear") && prediction.method.contains("exponential"),
1557 "both configured methods must appear in the ensemble label (got {})",
1558 prediction.method
1559 );
1560 assert!(
1561 prediction.confidence_interval.0 < prediction.predicted_value
1562 && prediction.predicted_value < prediction.confidence_interval.1,
1563 "the interval must bracket the point forecast"
1564 );
1565 }
1566
1567 #[test]
1570 fn linear_prediction_extrapolates_a_known_trend_exactly() {
1571 let predictor = PerformancePredictor::<f64>::new();
1572 let values: Vec<f64> = (1..=10).map(|x| 2.0 * x as f64).collect();
1574 let forecast = predictor
1575 .linear_prediction(&values, 3)
1576 .expect("linear_prediction");
1577 assert!(
1578 (forecast - 26.0).abs() < 1e-9,
1579 "expected 26.0 for a perfect y = 2x fit, got {forecast}"
1580 );
1581 }
1582
1583 #[test]
1586 fn exponential_prediction_follows_the_trend_not_a_fixed_decay() {
1587 let predictor = PerformancePredictor::<f64>::new();
1588 let rising: Vec<f64> = (1..=20).map(|x| x as f64).collect();
1590 let up = predictor
1591 .exponential_prediction(&rising, 5, 0.5, 0.5)
1592 .expect("exponential_prediction");
1593 assert!(
1594 up > 20.0,
1595 "a rising series must forecast above its last value, got {up}"
1596 );
1597
1598 let falling: Vec<f64> = (1..=20).map(|x| 21.0 - x as f64).collect();
1600 let down = predictor
1601 .exponential_prediction(&falling, 5, 0.5, 0.5)
1602 .expect("exponential_prediction");
1603 assert!(
1604 down < 1.0,
1605 "a falling series must forecast below its last value, got {down}"
1606 );
1607 }
1608
1609 #[test]
1617 fn prediction_accuracy_is_scored_without_waiting_ten_seconds_per_step() {
1618 let config = StreamingConfig::default();
1619 let mut tracker = PerformanceTracker::<f64>::new(&config).expect("tracker");
1620
1621 for i in 0..12 {
1622 tracker
1623 .add_performance(snapshot(10.0 - 0.5 * i as f64))
1624 .expect("add_performance");
1625 }
1626 tracker.predict_performance(1).expect("prediction");
1627
1628 for i in 0..2 {
1630 tracker
1631 .add_performance(snapshot(4.0 - 0.5 * i as f64))
1632 .expect("add_performance");
1633 }
1634
1635 let diagnostics = tracker.get_diagnostics();
1636 assert!(
1637 diagnostics.prediction_accuracy > 0.0,
1638 "P4f regression: no prediction was ever scored, so accuracy is still \
1639 {} after the horizon elapsed",
1640 diagnostics.prediction_accuracy
1641 );
1642 }
1643
1644 #[test]
1647 fn unimplemented_prediction_methods_are_an_error() {
1648 let config = StreamingConfig::default();
1649 let mut tracker = PerformanceTracker::<f64>::new(&config).expect("tracker");
1650 tracker.predictor.prediction_methods = vec![PredictionMethod::ARIMA { p: 1, d: 1, q: 1 }];
1651
1652 for i in 0..12 {
1653 tracker
1654 .add_performance(snapshot(10.0 - 0.5 * i as f64))
1655 .expect("add_performance");
1656 }
1657
1658 assert!(
1659 tracker.predict_performance(2).is_err(),
1660 "an unimplemented forecaster must not silently produce a value"
1661 );
1662 }
1663 #[test]
1669 fn improvement_rate_is_a_real_per_second_rate() {
1670 let config = StreamingConfig::default();
1671 let mut tracker = PerformanceTracker::<f64>::new(&config).expect("tracker");
1672
1673 tracker.add_performance(snapshot(10.0)).expect("first");
1674 std::thread::sleep(Duration::from_millis(40));
1676 tracker.add_performance(snapshot(9.0)).expect("second");
1677 std::thread::sleep(Duration::from_millis(40));
1678 tracker.add_performance(snapshot(8.0)).expect("third");
1679
1680 let events = &tracker.improvement_tracker.improvement_history;
1681 assert!(
1682 events.len() >= 2,
1683 "at least two improvements should have been recorded, got {}",
1684 events.len()
1685 );
1686 let last = events
1687 .back()
1688 .expect("an improvement event must have been recorded");
1689 assert!(
1690 (last.improvement - 1.0).abs() < 1e-12,
1691 "the raw improvement should be 1.0, got {}",
1692 last.improvement
1693 );
1694 assert!(
1695 (last.improvement_rate - last.improvement).abs() > 1e-9,
1696 "the rate must differ from the raw improvement once a real interval \
1697 has elapsed (improvement={}, rate={})",
1698 last.improvement,
1699 last.improvement_rate
1700 );
1701 assert!(
1704 last.improvement_rate > 1.0,
1705 "expected a rate well above 1/s for a 40ms interval, got {}",
1706 last.improvement_rate
1707 );
1708 }
1709}