Skip to main content

optirs_core/streaming/adaptive_streaming/
performance.rs

1// Performance tracking and prediction for streaming optimization
2//
3// This module provides comprehensive performance monitoring, trend analysis,
4// and prediction capabilities for streaming optimization scenarios, including
5// real-time metrics collection, statistical analysis, and predictive modeling.
6
7use 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/// Performance snapshot representing metrics at a specific point in time
18#[derive(Debug, Clone)]
19pub struct PerformanceSnapshot<A: Float + Send + Sync> {
20    /// Timestamp when snapshot was taken
21    pub timestamp: Instant,
22    /// Wall-clock time the optimization step that produced this snapshot took.
23    ///
24    /// This is a measured duration, distinct from `timestamp.elapsed()` (which
25    /// is the snapshot's *age*). Consumers reasoning about processing cost must
26    /// read this field.
27    pub processing_duration: Duration,
28    /// Primary loss metric
29    pub loss: A,
30    /// Accuracy metric (if applicable)
31    pub accuracy: Option<A>,
32    /// Convergence rate
33    pub convergence_rate: Option<A>,
34    /// Gradient norm
35    pub gradient_norm: Option<A>,
36    /// Parameter update magnitude
37    pub parameter_update_magnitude: Option<A>,
38    /// Data quality statistics
39    pub data_statistics: DataStatistics<A>,
40    /// Resource usage at snapshot time
41    pub resource_usage: ResourceUsage,
42    /// Custom performance metrics
43    pub custom_metrics: HashMap<String, A>,
44}
45
46/// Data quality and distribution statistics
47#[derive(Debug, Clone)]
48pub struct DataStatistics<A: Float + Send + Sync> {
49    /// Number of samples in this batch
50    pub sample_count: usize,
51    /// Feature-wise means
52    pub feature_means: scirs2_core::ndarray::Array1<A>,
53    /// Feature-wise standard deviations
54    pub feature_stds: scirs2_core::ndarray::Array1<A>,
55    /// Average data quality score
56    pub average_quality: A,
57    /// Timestamp of statistics computation
58    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/// Performance metrics for tracking and analysis
74#[derive(Debug, Clone)]
75pub enum PerformanceMetric<A: Float + Send + Sync> {
76    /// Loss function value
77    Loss(A),
78    /// Classification/regression accuracy
79    Accuracy(A),
80    /// Rate of convergence
81    ConvergenceRate(A),
82    /// Gradient magnitude
83    GradientNorm(A),
84    /// Learning rate effectiveness
85    LearningRateEffectiveness(A),
86    /// Resource utilization efficiency
87    ResourceEfficiency(A),
88    /// Data quality score
89    DataQuality(A),
90    /// Custom metric with name and value
91    Custom(String, A),
92}
93
94/// Context information for performance evaluation
95#[derive(Debug, Clone)]
96pub struct PerformanceContext<A: Float + Send + Sync> {
97    /// Current learning rate
98    pub learning_rate: A,
99    /// Current batch size
100    pub batch_size: usize,
101    /// Current buffer size
102    pub buffer_size: usize,
103    /// Recent drift detection status
104    pub drift_detected: bool,
105    /// Resource constraints
106    pub resource_constraints: ResourceUsage,
107    /// Time since last adaptation
108    pub time_since_adaptation: Duration,
109}
110
111/// Performance tracker for streaming optimization
112pub struct PerformanceTracker<A: Float + Send + Sync + std::iter::Sum> {
113    /// Configuration for performance tracking
114    config: PerformanceConfig,
115    /// Performance history
116    performance_history: VecDeque<PerformanceSnapshot<A>>,
117    /// Trend analyzer
118    trend_analyzer: PerformanceTrendAnalyzer<A>,
119    /// Performance predictor
120    predictor: PerformancePredictor<A>,
121    /// Performance baseline
122    baseline: Option<PerformanceSnapshot<A>>,
123    /// Current performance context
124    current_context: Option<PerformanceContext<A>>,
125    /// Performance improvement tracker
126    improvement_tracker: PerformanceImprovementTracker<A>,
127    /// Anomaly detector for performance
128    performance_anomaly_detector: PerformanceAnomalyDetector<A>,
129    /// Number of snapshots accepted since the baseline was last refreshed,
130    /// driving `PerformanceConfig::baseline_update_frequency`.
131    snapshots_since_baseline: usize,
132}
133
134/// Trend analysis for performance metrics
135pub struct PerformanceTrendAnalyzer<A: Float + Send + Sync> {
136    /// Window size for trend analysis
137    window_size: usize,
138    /// Current trends for different metrics
139    trends: HashMap<String, TrendData<A>>,
140}
141
142/// Trend data for a specific metric
143#[derive(Debug, Clone)]
144pub struct TrendData<A: Float + Send + Sync> {
145    /// Linear trend slope
146    pub slope: A,
147    /// Trend correlation coefficient
148    pub correlation: A,
149    /// Trend volatility
150    pub volatility: A,
151    /// Trend confidence
152    pub confidence: A,
153    /// Recent values used for trend calculation
154    pub recent_values: VecDeque<A>,
155    /// Last update timestamp
156    pub last_update: Instant,
157}
158
159/// Methods for trend calculation
160#[derive(Debug, Clone)]
161pub enum TrendMethod {
162    /// Linear regression
163    LinearRegression,
164    /// Moving average
165    MovingAverage { window: usize },
166    /// Exponential smoothing
167    ExponentialSmoothing { alpha: f64 },
168    /// Seasonal decomposition
169    SeasonalDecomposition,
170}
171
172/// Performance predictor using various forecasting methods
173pub struct PerformancePredictor<A: Float + Send + Sync> {
174    /// Prediction methods to use
175    prediction_methods: Vec<PredictionMethod>,
176    /// Historical predictions for accuracy tracking
177    prediction_history: VecDeque<PredictionResult<A>>,
178    /// Model accuracy scores, keyed by the per-method label used in
179    /// `ensemble_weights`.
180    model_accuracies: HashMap<String, A>,
181    /// Ensemble weights for combining predictions
182    ensemble_weights: HashMap<String, A>,
183    /// Per-method forecasts still awaiting their ground-truth observation.
184    ///
185    /// `PredictionResult` carries only the combined ensemble label, so scoring
186    /// individual methods (which is what the ensemble weights need) requires
187    /// keeping each method's own forecast until the actual value arrives.
188    pending_method_forecasts: VecDeque<MethodForecast<A>>,
189    /// Number of snapshots observed, used as the clock a forecast horizon is
190    /// measured against (`steps_ahead` counts snapshots, not seconds).
191    snapshots_since_start: usize,
192}
193
194/// A single method's forecast, held until its horizon elapses.
195#[derive(Debug, Clone)]
196struct MethodForecast<A: Float + Send + Sync> {
197    /// Per-method label matching the `model_accuracies` key.
198    label: String,
199    /// Forecast value.
200    predicted_value: A,
201    /// Number of steps ahead the forecast was made for.
202    steps_ahead: usize,
203    /// Snapshot index at which the forecast was issued.
204    issued_at_index: usize,
205}
206
207/// Prediction methods for performance forecasting
208#[derive(Debug, Clone)]
209pub enum PredictionMethod {
210    /// Linear extrapolation
211    Linear,
212    /// Exponential smoothing
213    Exponential { alpha: f64, beta: f64 },
214    /// ARIMA model
215    ARIMA { p: usize, d: usize, q: usize },
216    /// Neural network
217    NeuralNetwork { hidden_layers: Vec<usize> },
218    /// Ensemble of multiple methods
219    Ensemble,
220}
221
222/// Result of performance prediction
223#[derive(Debug, Clone)]
224pub struct PredictionResult<A: Float + Send + Sync> {
225    /// Predicted metric value
226    pub predicted_value: A,
227    /// Prediction confidence interval
228    pub confidence_interval: (A, A),
229    /// Prediction method used
230    pub method: String,
231    /// Steps ahead predicted
232    pub steps_ahead: usize,
233    /// Snapshot index at which the prediction was issued. `steps_ahead` counts
234    /// snapshots, so this is the clock its horizon is measured against.
235    pub issued_at_index: usize,
236    /// Prediction timestamp
237    pub timestamp: Instant,
238    /// Actual value (filled in later for accuracy assessment)
239    pub actual_value: Option<A>,
240}
241
242/// Performance improvement tracking
243pub struct PerformanceImprovementTracker<A: Float + Send + Sync> {
244    /// Baseline performance metrics
245    baseline_metrics: HashMap<String, A>,
246    /// Current improvement rates
247    improvement_rates: HashMap<String, A>,
248    /// Improvement history
249    improvement_history: VecDeque<ImprovementEvent<A>>,
250    /// Plateau detection
251    plateau_detector: PlateauDetector<A>,
252}
253
254/// Performance improvement event
255#[derive(Debug, Clone)]
256pub struct ImprovementEvent<A: Float + Send + Sync> {
257    /// Event timestamp
258    pub timestamp: Instant,
259    /// Metric that improved
260    pub metric_name: String,
261    /// Improvement magnitude
262    pub improvement: A,
263    /// Improvement rate (per unit time)
264    pub improvement_rate: A,
265    /// Context when improvement occurred
266    pub context: String,
267}
268
269/// Plateau detection for performance metrics
270pub struct PlateauDetector<A: Float + Send + Sync> {
271    /// Window size for plateau detection
272    window_size: usize,
273    /// Plateau threshold (minimum change for non-plateau)
274    plateau_threshold: A,
275    /// Recent performance values
276    recent_values: VecDeque<A>,
277    /// Current plateau status
278    is_plateau: bool,
279    /// Plateau duration, measured from `plateau_started`.
280    plateau_duration: Duration,
281    /// Instant the current plateau began, if any.
282    plateau_started: Option<Instant>,
283    /// Last significant change timestamp
284    last_significant_change: Option<Instant>,
285}
286
287/// Anomaly detection for performance metrics
288pub struct PerformanceAnomalyDetector<A: Float + Send + Sync> {
289    /// Anomaly detection threshold (standard deviations)
290    threshold: A,
291    /// Historical statistics for anomaly detection
292    historical_stats: HashMap<String, MetricStatistics<A>>,
293    /// Recent anomalies detected
294    recent_anomalies: VecDeque<PerformanceAnomaly<A>>,
295    /// Adaptive threshold adjustment
296    adaptive_threshold: bool,
297}
298
299/// Statistics for a performance metric
300#[derive(Debug, Clone)]
301pub struct MetricStatistics<A: Float + Send + Sync> {
302    /// Running mean
303    pub mean: A,
304    /// Running variance
305    pub variance: A,
306    /// Minimum observed value
307    pub min_value: A,
308    /// Maximum observed value
309    pub max_value: A,
310    /// Number of observations
311    pub count: usize,
312    /// Last update timestamp
313    pub last_update: Instant,
314}
315
316/// Performance anomaly event
317#[derive(Debug, Clone)]
318pub struct PerformanceAnomaly<A: Float + Send + Sync> {
319    /// Anomaly timestamp
320    pub timestamp: Instant,
321    /// Affected metric
322    pub metric_name: String,
323    /// Observed value
324    pub observed_value: A,
325    /// Expected value range
326    pub expected_range: (A, A),
327    /// Anomaly severity
328    pub severity: AnomalySeverity,
329    /// Anomaly type
330    pub anomaly_type: AnomalyType,
331}
332
333/// Severity levels for performance anomalies
334#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
335pub enum AnomalySeverity {
336    /// Minor anomaly
337    Minor,
338    /// Moderate anomaly requiring attention
339    Moderate,
340    /// Major anomaly requiring intervention
341    Major,
342    /// Critical anomaly requiring immediate action
343    Critical,
344}
345
346/// Types of performance anomalies
347#[derive(Debug, Clone, PartialEq, Eq)]
348pub enum AnomalyType {
349    /// Value significantly higher than expected
350    High,
351    /// Value significantly lower than expected
352    Low,
353    /// Sudden change in trend
354    TrendChange,
355    /// Unexpected oscillation
356    Oscillation,
357    /// Performance degradation
358    Degradation,
359    /// Performance plateau
360    Plateau,
361}
362
363impl<A: Float + Default + Clone + std::iter::Sum + Send + Sync + std::fmt::Debug>
364    PerformanceTracker<A>
365{
366    /// Creates a new performance tracker
367    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); // 2 sigma threshold
374
375        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    /// Test-only view of the current baseline's loss.
389    #[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    /// Adds a new performance snapshot.
395    ///
396    /// Two `PerformanceConfig` fields that previously had no reader at all now
397    /// govern this (CF1):
398    ///
399    /// * `enable_tracking == false` makes this a no-op, so turning tracking off
400    ///   actually stops history, trend, prediction and anomaly work instead of
401    ///   silently doing all of it anyway.
402    /// * `baseline_update_frequency` re-bases the comparison baseline every N
403    ///   accepted snapshots. Before, the baseline was pinned to the very first
404    ///   measurement forever, so every "improvement over baseline" figure was
405    ///   measured against the start of the run no matter how long it had been
406    ///   running.
407    pub fn add_performance(&mut self, snapshot: PerformanceSnapshot<A>) -> Result<(), String> {
408        if !self.config.enable_tracking {
409            return Ok(());
410        }
411
412        // Store in history
413        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        // Set baseline if this is the first measurement, then refresh it on the
419        // configured cadence.
420        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            // Only a *refresh* starts a new window. Establishing the very first
426            // baseline must not also consume a window slot: the sample that
427            // set it is the first sample of the window, so zeroing here made
428            // every cadence one sample too long (a frequency of 3 re-based on
429            // the 4th sample, then the 7th).
430            if refresh_due {
431                self.snapshots_since_baseline = 0;
432            }
433        }
434
435        // Update trend analysis
436        if self.config.enable_trend_analysis {
437            self.trend_analyzer.update(&snapshot)?;
438        }
439
440        // Update improvement tracking
441        self.improvement_tracker.update(&snapshot)?;
442
443        // Check for performance anomalies
444        let anomalies = self
445            .performance_anomaly_detector
446            .check_for_anomalies(&snapshot)?;
447        if !anomalies.is_empty() {
448            // Handle detected anomalies
449            self.handle_performance_anomalies(&anomalies)?;
450        }
451
452        // Update predictions if enabled
453        if self.config.enable_prediction {
454            self.predictor.update_with_actual(&snapshot)?;
455        }
456
457        Ok(())
458    }
459
460    /// Gets recent performance snapshots
461    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    /// Gets recent loss values for trend analysis
471    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    /// Predicts future performance
481    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    /// Gets current performance trends
494    pub fn get_performance_trends(&self) -> HashMap<String, TrendData<A>> {
495        self.trend_analyzer.get_current_trends()
496    }
497
498    /// Computes adaptation for performance thresholds
499    pub fn apply_threshold_adaptation(&mut self, adaptation: &Adaptation<A>) -> Result<(), String> {
500        if adaptation.adaptation_type == AdaptationType::PerformanceThreshold {
501            // Adjust anomaly detection thresholds
502            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    /// Handles detected performance anomalies
510    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                    // Log critical anomalies for immediate attention
518                    println!("Critical performance anomaly detected: {:?}", anomaly);
519                }
520                _ => {
521                    // Store for analysis
522                    self.performance_anomaly_detector
523                        .recent_anomalies
524                        .push_back(anomaly.clone());
525                }
526            }
527        }
528        Ok(())
529    }
530
531    /// Resets performance tracking
532    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    /// Gets diagnostic information
545    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        // Update trends for different metrics
567        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        // Collect all computed values first
607        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        // Now update the trend data
628        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        // Compute sum_x = 1 + 2 + ... + n = n*(n+1)/2
647        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        // Compute sum_x_squared = 1^2 + 2^2 + ... + n^2 = n*(n+1)*(2n+1)/6
657        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        // Simplified correlation with time index
676        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        // Simple confidence based on trend consistency
723        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        // Confidence increases with stronger correlation and consistent slope direction
731        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    /// Forecasts the loss `steps_ahead` batches into the future.
763    ///
764    /// P3f/P4f: this used to run only `linear_prediction`, leaving
765    /// `exponential_prediction`, the `prediction_methods` list and the
766    /// `ensemble_weights` map as permanently-unread dead state. Every
767    /// configured method now runs, and their forecasts are combined by weights
768    /// derived from each method's *measured* accuracy (see
769    /// `update_accuracy_metrics`), so a method that has been predicting badly
770    /// loses influence. Methods with no accuracy history yet are weighted
771    /// equally.
772    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        // Extract loss values for prediction
782        let loss_values: Vec<A> = history.iter().map(|s| s.loss).collect();
783
784        // Run every configured method.
785        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                    // An unimplemented forecaster must not silently contribute a
799                    // made-up number to the ensemble.
800                    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        // Weight each method by its measured accuracy, refreshing the stored
816        // ensemble weights so they are real, inspectable state.
817        let mut total_weight = A::zero();
818        let mut weighted_sum = A::zero();
819        for (label, value) in &forecasts {
820            // An unseen method starts at the neutral weight of 1; a measured
821            // accuracy in [0, 1] is floored so a method is never fully muted.
822            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        // Confidence interval from the real recent volatility of the series,
839        // widened by the disagreement between the methods (a genuine measure of
840        // model uncertainty).
841        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        // Store prediction for later accuracy assessment
882        if self.prediction_history.len() >= 1000 {
883            self.prediction_history.pop_front();
884        }
885        self.prediction_history.push_back(prediction.clone());
886
887        // Keep each method's own forecast so its accuracy — and therefore its
888        // ensemble weight — can be scored against the real outcome.
889        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    /// Ordinary-least-squares extrapolation over the whole observed series.
906    ///
907    /// The previous version bound four locals (`x1`, `y1`, `x2`, `y2`) it never
908    /// read and then estimated the slope from a two-point finite difference over
909    /// `values[n-1] - values[n-3]`, which is dominated by noise on a jittery
910    /// stream. A least-squares fit uses every observation.
911    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        // x = 1..=n
925        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    /// Holt's linear (double exponential) smoothing.
948    ///
949    /// `alpha` smooths the level and `beta` the trend; the forecast is
950    /// `level + steps_ahead * trend`. The previous implementation used single
951    /// exponential smoothing and then multiplied the forecast by `0.99` once per
952    /// step ahead — a fabricated "assume slight improvement" factor unrelated to
953    /// the data — and ignored `beta` entirely.
954    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    /// Matches issued forecasts against the observed value and updates each
1005    /// method's measured accuracy.
1006    ///
1007    /// P4f: the previous version gated on a fabricated
1008    /// `Duration::from_secs(steps_ahead * 10)` — "assume 10s per step" — which
1009    /// bears no relation to the real batch cadence, so on a fast stream no
1010    /// prediction was ever scored and `model_accuracies` stayed empty forever.
1011    /// The horizon is now counted in *snapshots*, which is the unit
1012    /// `steps_ahead` is actually expressed in.
1013    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        // Score every per-method forecast whose horizon has elapsed, counted in
1018        // snapshots rather than wall-clock seconds.
1019        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            // Exponentially-weighted so a method's score reflects its recent
1033            // behaviour rather than only its latest observation.
1034            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        // Fill in the actual value on the combined predictions whose horizon has
1044        // also elapsed, so the history is a complete record.
1045        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    /// Accuracy of a single forecast: `1 - min(1, |error| / scale)`, where the
1061    /// scale is the magnitude of the observed value (floored so a near-zero
1062    /// observation cannot make the relative error explode).
1063    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    /// Measured accuracy of a single named method, if it has been scored.
1071    pub fn method_accuracy(&self, label: &str) -> Option<A> {
1072        self.model_accuracies.get(label).copied()
1073    }
1074
1075    /// Current ensemble weights, keyed by method label.
1076    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        // Update baseline if not set
1113        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        // Check for improvements
1123        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                // Real rate: improvement per second since the previous
1128                // improvement (or since this snapshot's own step, for the first
1129                // one). Dividing by a literal `1.0` made `improvement_rate` an
1130                // exact duplicate of `improvement`, so the field carried no
1131                // information about how *fast* the optimizer was improving.
1132                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                    // No measurable interval yet: report the raw improvement
1145                    // rather than dividing by zero.
1146                    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                // Update baseline
1165                self.baseline_metrics
1166                    .insert("loss".to_string(), snapshot.loss);
1167            }
1168        }
1169
1170        // Update plateau detector
1171        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    /// Detects whether the tracked metric has flattened out.
1209    ///
1210    /// P1f: the range used to be computed with `fold(A::zero(), A::max)` and
1211    /// `fold(A::zero(), A::min)`, i.e. seeded at zero. For any all-positive
1212    /// metric (loss, latency, error rate — essentially all of them) the seeded
1213    /// minimum stayed at `0`, so `range == max_val` and the detector only ever
1214    /// fired when the *largest observed value* fell below the plateau
1215    /// threshold. The seed is now the first observation, which is the only
1216    /// correct identity for a min/max reduction.
1217    ///
1218    /// P2f: `plateau_duration` was advanced by a hard-coded
1219    /// `Duration::from_secs(1)` per update, so it reported "one second per
1220    /// sample" regardless of how fast or slow samples actually arrived. It is
1221    /// now measured from the real `Instant` at which the plateau began.
1222    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                // Plateau just began: stamp the real clock.
1249                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    /// Whether the metric is currently plateaued.
1262    pub fn is_plateau(&self) -> bool {
1263        self.is_plateau
1264    }
1265
1266    /// Real elapsed duration of the current plateau, measured from the instant
1267    /// it was first detected. `Duration::ZERO` when not plateaued.
1268    pub fn plateau_duration(&self) -> Duration {
1269        self.plateau_duration
1270    }
1271
1272    /// Instant of the most recent significant (non-plateau) change.
1273    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        // Check loss anomaly
1302        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        // Check accuracy anomaly if available
1308        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        // Update statistics for this metric
1326        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        // Update running statistics
1339        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        // Check for anomaly after sufficient samples
1349        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    /// Move the anomaly-detection threshold, if this detector is configured to
1390    /// adapt it.
1391    ///
1392    /// `adaptive_threshold` was set at construction and never consulted, so a
1393    /// detector configured with a fixed threshold still had it moved by every
1394    /// `AdaptationType::PerformanceThreshold` adaptation. Returns whether the
1395    /// threshold actually moved.
1396    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    /// Whether this detector adapts its threshold.
1405    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/// Diagnostic information for performance tracking
1416#[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    /// P1f: `detect_plateau` reduced with `fold(A::zero(), A::min)`, seeding the
1453    /// minimum at `0`. For an all-positive metric the observed minimum could
1454    /// never be anything but `0`, so `range == max_val` and the detector only
1455    /// fired when the *largest* value fell under the threshold. A genuinely flat
1456    /// series at level 5.0 with a 0.01 threshold must be reported as a plateau —
1457    /// under the old code `range` would have been `5.0` and it never would be.
1458    #[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            // Flat at 5.0, jitter of 0.001 — total range 0.001 < 0.01.
1463            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    /// P1f: the mirror case — a series with genuine variation must not be
1473    /// reported as a plateau.
1474    #[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    /// P2f: `plateau_duration` was advanced by a hard-coded
1489    /// `Duration::from_secs(1)` per update, so after N updates it always claimed
1490    /// exactly N-1 seconds regardless of how fast samples arrived. It must now
1491    /// reflect real elapsed time — far less than a second for a tight loop.
1492    #[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        // 5 samples to trigger, then 20 more updates inside the plateau.
1496        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    /// P2f: the duration must genuinely grow with wall-clock time.
1511    #[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    /// P3f: `predict` only ever ran `linear_prediction`;
1535    /// `exponential_prediction`, `prediction_methods` and `ensemble_weights`
1536    /// were dead. Every configured method must now run and be combined, which
1537    /// shows up as a populated `ensemble_weights` map and an ensemble label.
1538    #[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    /// P3f: the linear forecaster must extrapolate a real least-squares trend.
1568    /// A perfectly linear series must be predicted almost exactly.
1569    #[test]
1570    fn linear_prediction_extrapolates_a_known_trend_exactly() {
1571        let predictor = PerformancePredictor::<f64>::new();
1572        // y = 2x for x = 1..=10, so the value at x = 13 is 26.
1573        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    /// P3f: Holt's linear smoothing must follow the trend, not multiply the
1584    /// forecast by a fabricated 0.99 "assume slight improvement" factor.
1585    #[test]
1586    fn exponential_prediction_follows_the_trend_not_a_fixed_decay() {
1587        let predictor = PerformancePredictor::<f64>::new();
1588        // Steadily rising series: the forecast must be above the last value.
1589        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        // Steadily falling series: the forecast must be below the last value.
1599        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    /// P4f: accuracy scoring was gated on a fabricated
1610    /// `Duration::from_secs(steps_ahead * 10)`, so on any stream faster than
1611    /// "10 seconds per batch" no prediction was ever scored and
1612    /// `model_accuracies` stayed permanently empty — which in turn meant the
1613    /// ensemble weights had nothing to be derived from. The horizon is now
1614    /// counted in snapshots, so accuracy is measured within a handful of
1615    /// batches with no sleeping at all.
1616    #[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        // Two more snapshots is more than the 1-step horizon.
1629        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    /// P4f: a forecaster with no implementation must be an honest error rather
1645    /// than contributing a fabricated number to the ensemble.
1646    #[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    /// `improvement_rate` was `improvement / 1.0`, an exact duplicate of
1664    /// `improvement`, so it said nothing about how *fast* the optimizer was
1665    /// improving. It must now be a real per-second rate, which for two
1666    /// improvements separated by a measurable interval differs from the raw
1667    /// improvement.
1668    #[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        // Second improvement, well after the first.
1675        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        // 1.0 of improvement over ~40ms is roughly 25 per second, and certainly
1702        // more than 1 per second.
1703        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}