Skip to main content

optirs_core/streaming/adaptive_streaming/
optimizer.rs

1// Core adaptive streaming optimizer implementation
2//
3// This module contains the main AdaptiveStreamingOptimizer that orchestrates
4// all streaming optimization components including drift detection, performance
5// tracking, resource management, and adaptive learning rate control.
6
7use super::anomaly_detection::{AnomalyDetector, AnomalyDiagnostics};
8use super::buffering::{AdaptiveBuffer, BufferDiagnostics};
9use super::config::*;
10use super::drift_detection::{DriftDiagnostics, EnhancedDriftDetector};
11use super::meta_learning::{MetaAction, MetaLearner, MetaLearningDiagnostics, MetaState};
12use super::performance::{
13    DataStatistics, PerformanceDiagnostics, PerformanceSnapshot, PerformanceTracker,
14};
15use super::resource_management::{ResourceDiagnostics, ResourceManager, ResourceUsage};
16
17use crate::optimizers::Optimizer;
18use crate::utils::try_scalar_str;
19use scirs2_core::ndarray::{Array, Array1, Dimension};
20use scirs2_core::numeric::Float;
21use scirs2_core::ScientificNumber;
22use serde::Serialize;
23use std::collections::{HashMap, VecDeque};
24use std::marker::PhantomData;
25use std::time::{Duration, Instant};
26
27/// Window used by [`AdaptiveStreamingStats::recent_adaptations`]: adaptations
28/// older than this no longer count as "recent" activity.
29pub const RECENT_ADAPTATION_WINDOW: Duration = Duration::from_secs(300);
30
31/// Adaptive learning-rate controller.
32///
33/// O1: this used to be a stub whose every method ignored its arguments — the
34/// rate never moved, `compute_adaptation` echoed the base rate back and
35/// `last_change` was hard-coded to `None`, so the whole `AdaptationType::
36/// LearningRate` pipeline was a no-op that "applied" the same value forever.
37///
38/// The real controller combines two established online signals, both computed
39/// from data the caller already supplies:
40///
41/// - **Gradient-norm normalisation** (an AdaGrad-style trust region): the rate
42///   is scaled by `1 / (1 + sqrt(accumulated squared gradient norm))`, so a
43///   burst of large gradients shrinks the step and a quiet stretch restores it.
44/// - **Performance feedback**: the sign of the recent loss trend, estimated by
45///   ordinary least squares over the supplied metric window, nudges the rate
46///   up while the loss is falling and down while it is rising.
47///
48/// Every update is clamped to `[min_rate, max_rate]` from the configuration and
49/// recorded, so `last_change` reports the real delta that was applied.
50#[derive(Debug, Clone)]
51pub struct AdaptiveLearningRateController<A: Float> {
52    /// Current learning rate.
53    current_lr: A,
54    /// Rate the controller was constructed with.
55    initial_lr: A,
56    /// Lower bound on the rate.
57    min_lr: A,
58    /// Upper bound on the rate.
59    max_lr: A,
60    /// AdaGrad-style accumulator of squared gradient norms.
61    squared_gradient_norm_sum: A,
62    /// Multiplicative step used to act on the performance trend.
63    trend_step: A,
64    /// Change applied by the most recent update, if any.
65    last_change: Option<A>,
66    /// Number of updates applied. Doubles as the iteration counter the
67    /// cyclical schedule is evaluated against.
68    updates: usize,
69    /// Cyclical-schedule state, present only when
70    /// `LearningRateConfig::enable_cyclical_rates` is set.
71    cyclical: Option<CyclicalSchedule<A>>,
72}
73
74/// Cyclical learning-rate schedule (Smith, "Cyclical Learning Rates for
75/// Training Neural Networks", WACV 2017), driven entirely by
76/// `CyclicalRateConfig` (CF1).
77///
78/// Every field of `CyclicalRateConfig` — `base_rate`, `max_rate`,
79/// `cycle_length`, `cycle_mode` and `scale_function` — previously had no reader
80/// anywhere in the crate, and `enable_cyclical_rates` was never consulted, so
81/// configuring a cyclical schedule did nothing at all.
82#[derive(Debug, Clone)]
83struct CyclicalSchedule<A: Float> {
84    /// Lower bound of the cycle.
85    base_rate: A,
86    /// Upper bound of the cycle.
87    max_rate: A,
88    /// Half-cycle length in iterations (`cycle_length / 2`, at least 1).
89    step_size: A,
90    /// Amplitude policy across successive cycles.
91    cycle_mode: CycleMode,
92    /// Within-cycle ramp shape.
93    scale_function: ScaleFunction,
94}
95
96impl<A: Float> CyclicalSchedule<A> {
97    /// Learning rate for iteration `iteration` (0-based).
98    ///
99    /// `lr = base + (max - base) * ramp(x) * amplitude(cycle)` where `x` is the
100    /// normalised distance from the current half-cycle boundary, exactly as in
101    /// the reference formulation.
102    fn rate_at(&self, iteration: usize) -> A {
103        let Some(step) = A::from(iteration) else {
104            return self.base_rate;
105        };
106        let two = match A::from(2.0) {
107            Some(two) => two,
108            None => return self.base_rate,
109        };
110
111        // cycle = floor(1 + step / (2 * step_size)); x = |step/step_size - 2*cycle + 1|
112        let cycle = (A::one() + step / (two * self.step_size)).floor();
113        let x = (step / self.step_size - two * cycle + A::one()).abs();
114        let position = (A::one() - x).max(A::zero()).min(A::one());
115
116        let ramp = match &self.scale_function {
117            ScaleFunction::Linear => position,
118            ScaleFunction::Polynomial { power } => match A::from(*power) {
119                Some(power) => position.powf(power),
120                None => position,
121            },
122            // `factor^x`: 1 at the cycle peak, `factor` at the trough.
123            ScaleFunction::Exponential { factor } => match A::from(*factor) {
124                Some(factor) if factor > A::zero() => factor.powf(A::one() - position),
125                _ => position,
126            },
127            // Rejected at construction time.
128            ScaleFunction::Custom(_) => position,
129        };
130
131        let amplitude = match &self.cycle_mode {
132            CycleMode::Triangular => A::one(),
133            // Halve the amplitude on each successive cycle.
134            CycleMode::Triangular2 => {
135                let exponent = (cycle - A::one()).max(A::zero());
136                A::one() / two.powf(exponent)
137            }
138            // `gamma^iteration`, with gamma supplied by the exponential scale
139            // function (guaranteed present by the constructor's validation).
140            CycleMode::ExponentialRange => match &self.scale_function {
141                ScaleFunction::Exponential { factor } => match A::from(*factor) {
142                    Some(gamma) if gamma > A::zero() => gamma.powf(step),
143                    _ => A::one(),
144                },
145                _ => A::one(),
146            },
147            // Rejected at construction time.
148            CycleMode::Custom(_) => A::one(),
149        };
150
151        let span = self.max_rate - self.base_rate;
152        (self.base_rate + span * ramp * amplitude)
153            .max(self.base_rate.min(self.max_rate))
154            .min(self.base_rate.max(self.max_rate))
155    }
156}
157
158impl<A: Float> AdaptiveLearningRateController<A> {
159    /// Builds a controller from the streaming learning-rate configuration.
160    pub fn new(config: &StreamingConfig) -> Result<Self, crate::error::OptimError> {
161        let lr_config = &config.learning_rate_config;
162        let convert = |value: f64, name: &str| -> Result<A, crate::error::OptimError> {
163            A::from(value).ok_or_else(|| {
164                crate::error::OptimError::InvalidConfig(format!(
165                    "learning rate {name} ({value}) is not representable in the element type"
166                ))
167            })
168        };
169
170        let initial_lr = convert(lr_config.initial_rate, "initial_rate")?;
171        let min_lr = convert(lr_config.min_rate, "min_rate")?;
172        let max_lr = convert(lr_config.max_rate, "max_rate")?;
173        if min_lr > max_lr {
174            return Err(crate::error::OptimError::InvalidConfig(format!(
175                "learning rate min_rate ({}) exceeds max_rate ({})",
176                lr_config.min_rate, lr_config.max_rate
177            )));
178        }
179        let trend_step = convert(
180            lr_config.performance_sensitivity.clamp(1e-6, 0.5),
181            "performance_sensitivity",
182        )?;
183
184        let cyclical = if lr_config.enable_cyclical_rates {
185            let cycle = &lr_config.cycle_config;
186            if let ScaleFunction::Custom(name) = &cycle.scale_function {
187                return Err(crate::error::OptimError::InvalidConfig(format!(
188                    "cyclical learning-rate scale_function Custom(\"{name}\") has no \
189                     registered implementation; use Linear, Exponential or Polynomial"
190                )));
191            }
192            if let CycleMode::Custom(name) = &cycle.cycle_mode {
193                return Err(crate::error::OptimError::InvalidConfig(format!(
194                    "cyclical learning-rate cycle_mode Custom(\"{name}\") has no \
195                     registered implementation; use Triangular, Triangular2 or \
196                     ExponentialRange"
197                )));
198            }
199            if matches!(cycle.cycle_mode, CycleMode::ExponentialRange)
200                && !matches!(cycle.scale_function, ScaleFunction::Exponential { .. })
201            {
202                return Err(crate::error::OptimError::InvalidConfig(
203                    "cyclical cycle_mode ExponentialRange needs its decay factor from \
204                     scale_function = Exponential { factor }"
205                        .to_string(),
206                ));
207            }
208            if cycle.cycle_length == 0 {
209                return Err(crate::error::OptimError::InvalidConfig(
210                    "cyclical learning-rate cycle_length must be greater than zero".to_string(),
211                ));
212            }
213            let base_rate = convert(cycle.base_rate, "cycle_config.base_rate")?;
214            let cycle_max_rate = convert(cycle.max_rate, "cycle_config.max_rate")?;
215            if base_rate > cycle_max_rate {
216                return Err(crate::error::OptimError::InvalidConfig(format!(
217                    "cyclical base_rate ({}) exceeds cycle max_rate ({})",
218                    cycle.base_rate, cycle.max_rate
219                )));
220            }
221            let step_size = convert(
222                ((cycle.cycle_length as f64) / 2.0).max(1.0),
223                "cycle_config.cycle_length",
224            )?;
225            Some(CyclicalSchedule {
226                base_rate,
227                max_rate: cycle_max_rate,
228                step_size,
229                cycle_mode: cycle.cycle_mode.clone(),
230                scale_function: cycle.scale_function.clone(),
231            })
232        } else {
233            None
234        };
235
236        let initial_current = match cyclical.as_ref() {
237            // A cyclical schedule owns the rate outright, so start on the
238            // schedule rather than at `initial_rate`.
239            Some(schedule) => schedule.rate_at(0).max(min_lr).min(max_lr),
240            None => initial_lr.max(min_lr).min(max_lr),
241        };
242
243        Ok(Self {
244            current_lr: initial_current,
245            initial_lr,
246            min_lr,
247            max_lr,
248            squared_gradient_norm_sum: A::zero(),
249            trend_step,
250            last_change: None,
251            updates: 0,
252            cyclical,
253        })
254    }
255
256    /// Folds a real gradient into the controller and returns the resulting rate.
257    ///
258    /// The gradient's squared L2 norm feeds an AdaGrad accumulator, so the
259    /// effective rate is `initial / (1 + sqrt(sum of squared norms))` — large or
260    /// repeated gradients genuinely shrink the step.
261    pub fn update_learning_rate(&mut self, gradient: &Array1<A>) -> A {
262        let squared_norm = gradient.iter().fold(A::zero(), |acc, &g| acc + g * g);
263        if squared_norm.is_finite() {
264            self.squared_gradient_norm_sum = self.squared_gradient_norm_sum + squared_norm;
265        }
266
267        // A configured cyclical schedule owns the rate: its whole purpose is to
268        // sweep between bounds on a fixed cadence, which an AdaGrad decay would
269        // flatten out. The gradient accumulator is still maintained above so
270        // `accumulated_squared_gradient_norm` stays meaningful either way.
271        let proposed = match self.cyclical.as_ref() {
272            Some(schedule) => schedule.rate_at(self.updates),
273            None => {
274                self.initial_lr * (A::one() / (A::one() + self.squared_gradient_norm_sum.sqrt()))
275            }
276        };
277        let proposed = proposed.max(self.min_lr).min(self.max_lr);
278        self.set_rate(proposed);
279        self.current_lr
280    }
281
282    /// Test-only view of the cyclical schedule's rate at a given iteration.
283    #[cfg(test)]
284    pub(crate) fn rate_at_for_test(&self, iteration: usize) -> A {
285        match self.cyclical.as_ref() {
286            Some(schedule) => schedule.rate_at(iteration),
287            None => self.current_lr,
288        }
289    }
290
291    /// Whether a cyclical schedule is driving this controller.
292    pub fn is_cyclical(&self) -> bool {
293        self.cyclical.is_some()
294    }
295
296    /// Current learning rate.
297    pub fn current_rate(&self) -> A {
298        self.current_lr
299    }
300
301    /// Accumulated squared gradient norm (the AdaGrad state).
302    pub fn accumulated_squared_gradient_norm(&self) -> A {
303        self.squared_gradient_norm_sum
304    }
305
306    /// Number of updates the controller has applied.
307    pub fn update_count(&self) -> usize {
308        self.updates
309    }
310
311    /// Proposes the next learning rate from a window of recent performance
312    /// metrics, most-recent-last.
313    ///
314    /// The trend is the ordinary-least-squares slope of the metric against its
315    /// index. A falling metric (negative slope) means the current rate is
316    /// working, so the rate is grown by `1 + performance_sensitivity`; a rising
317    /// metric shrinks it by `1 - performance_sensitivity`. With fewer than two
318    /// samples there is no trend to read and the current rate is returned
319    /// unchanged.
320    pub fn compute_adaptation(&self, performance_metrics: &[A]) -> A {
321        // Under a cyclical schedule the next rate is a function of the iteration
322        // counter, not of the performance trend.
323        if let Some(schedule) = self.cyclical.as_ref() {
324            return schedule
325                .rate_at(self.updates.saturating_add(1))
326                .max(self.min_lr)
327                .min(self.max_lr);
328        }
329        if performance_metrics.len() < 2 {
330            return self.current_lr;
331        }
332
333        let n = match A::from(performance_metrics.len()) {
334            Some(value) => value,
335            None => return self.current_lr,
336        };
337        let (Some(one_half), Some(six), Some(two)) = (A::from(0.5), A::from(6.0), A::from(2.0))
338        else {
339            return self.current_lr;
340        };
341
342        // Closed forms for sum(x), sum(x^2) over x = 1..=n.
343        let sum_x = n * (n + A::one()) * one_half;
344        let sum_x_squared = n * (n + A::one()) * (two * n + A::one()) / six;
345        let mut sum_y = A::zero();
346        let mut sum_xy = A::zero();
347        for (index, &value) in performance_metrics.iter().enumerate() {
348            let x = match A::from(index + 1) {
349                Some(x) => x,
350                None => return self.current_lr,
351            };
352            sum_y = sum_y + value;
353            sum_xy = sum_xy + x * value;
354        }
355
356        let denominator = n * sum_x_squared - sum_x * sum_x;
357        if denominator == A::zero() {
358            return self.current_lr;
359        }
360        let slope = (n * sum_xy - sum_x * sum_y) / denominator;
361
362        let factor = if slope < A::zero() {
363            A::one() + self.trend_step
364        } else if slope > A::zero() {
365            A::one() - self.trend_step
366        } else {
367            A::one()
368        };
369
370        (self.current_lr * factor).max(self.min_lr).min(self.max_lr)
371    }
372
373    /// Applies a proposed rate, recording the real delta.
374    pub fn apply_adaptation(&mut self, adaptation: A) {
375        if !adaptation.is_finite() || adaptation <= A::zero() {
376            // A non-positive or non-finite rate would silently destroy the
377            // optimizer; ignore it rather than adopting it.
378            return;
379        }
380        self.set_rate(adaptation.max(self.min_lr).min(self.max_lr));
381    }
382
383    fn set_rate(&mut self, new_rate: A) {
384        let delta = new_rate - self.current_lr;
385        if delta != A::zero() {
386            self.last_change = Some(delta);
387            self.updates += 1;
388        }
389        self.current_lr = new_rate;
390    }
391
392    /// Delta applied by the most recent rate change, or `None` if the rate has
393    /// never moved.
394    pub fn last_change(&self) -> Option<A> {
395        self.last_change
396    }
397
398    /// Resets the controller to its configured initial rate.
399    pub fn reset(&mut self) {
400        self.current_lr = self.initial_lr.max(self.min_lr).min(self.max_lr);
401        self.squared_gradient_norm_sum = A::zero();
402        self.last_change = None;
403        self.updates = 0;
404    }
405}
406
407/// Streaming data point for optimization
408#[derive(Debug, Clone)]
409pub struct StreamingDataPoint<A: Float + Send + Sync> {
410    /// Input features
411    pub features: Array1<A>,
412    /// Target values (optional for unsupervised learning)
413    pub target: Option<Array1<A>>,
414    /// Timestamp when data was received
415    pub timestamp: Instant,
416    /// Data source identifier
417    pub source_id: Option<String>,
418    /// Data quality score (0.0 to 1.0)
419    pub quality_score: A,
420    /// Additional metadata
421    pub metadata: HashMap<String, String>,
422}
423
424/// Adaptation instruction for optimizer components
425#[derive(Debug, Clone)]
426pub struct Adaptation<A: Float + Send + Sync> {
427    /// Type of adaptation
428    pub adaptation_type: AdaptationType,
429    /// Magnitude of adaptation
430    pub magnitude: A,
431    /// Target component for adaptation
432    pub target_component: String,
433    /// Adaptation parameters
434    pub parameters: HashMap<String, A>,
435    /// Priority of this adaptation
436    pub priority: AdaptationPriority,
437    /// Timestamp when adaptation was computed
438    pub timestamp: Instant,
439}
440
441/// Types of adaptations that can be applied
442#[derive(Debug, Clone, PartialEq, Eq)]
443pub enum AdaptationType {
444    /// Adjust learning rate
445    LearningRate,
446    /// Modify buffer size
447    BufferSize,
448    /// Change drift sensitivity
449    DriftSensitivity,
450    /// Update resource allocation
451    ResourceAllocation,
452    /// Adjust performance thresholds
453    PerformanceThreshold,
454    /// Modify anomaly detection parameters
455    AnomalyDetection,
456    /// Update meta-learning parameters
457    MetaLearning,
458    /// Custom adaptation type
459    Custom(String),
460}
461
462/// Priority levels for adaptations
463#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
464pub enum AdaptationPriority {
465    /// Low priority adaptation
466    Low = 0,
467    /// Normal priority adaptation
468    Normal = 1,
469    /// High priority adaptation
470    High = 2,
471    /// Critical adaptation that must be applied immediately
472    Critical = 3,
473}
474
475/// Statistics for adaptive streaming optimization
476#[derive(Debug, Clone, Serialize)]
477pub struct AdaptiveStreamingStats {
478    /// Total number of data points processed
479    pub total_data_points: usize,
480    /// Total number of optimization steps performed
481    pub optimization_steps: usize,
482    /// Number of drift events detected
483    pub drift_events: usize,
484    /// Number of anomalies detected
485    pub anomalies_detected: usize,
486    /// Number of adaptations applied over the optimizer's whole lifetime
487    pub adaptations_applied: usize,
488    /// Number of adaptations applied within the last
489    /// [`RECENT_ADAPTATION_WINDOW`], recomputed on each
490    /// `get_adaptive_stats()` call
491    pub recent_adaptations: usize,
492    /// Current buffer size
493    pub current_buffer_size: usize,
494    /// Current learning rate
495    pub current_learning_rate: f64,
496    /// Average processing time per batch
497    pub avg_processing_time_ms: f64,
498    /// Resource utilization statistics
499    pub resource_utilization: ResourceUsage,
500    /// Performance trend (improvement/degradation)
501    pub performance_trend: f64,
502    /// Meta-learning effectiveness score
503    pub meta_learning_score: f64,
504}
505
506/// Main adaptive streaming optimizer
507pub struct AdaptiveStreamingOptimizer<O, A, D>
508where
509    A: Float + Default + Clone + Send + Sync + std::iter::Sum,
510    D: Dimension,
511{
512    /// Base optimizer instance
513    base_optimizer: O,
514    /// Streaming configuration
515    config: StreamingConfig,
516    /// Adaptive buffer for incoming data
517    buffer: AdaptiveBuffer<A>,
518    /// Drift detection system
519    drift_detector: EnhancedDriftDetector<A>,
520    /// Performance tracking system
521    performance_tracker: PerformanceTracker<A>,
522    /// Resource management system
523    resource_manager: ResourceManager,
524    /// Meta-learning system
525    meta_learner: MetaLearner<A>,
526    /// Anomaly detection system
527    anomaly_detector: AnomalyDetector<A>,
528    /// Learning rate controller
529    learning_rate_controller: AdaptiveLearningRateController<A>,
530    /// Current model parameters
531    parameters: Option<Array<A, D>>,
532    /// Optimization statistics
533    stats: AdaptiveStreamingStats,
534    /// Last adaptation timestamp
535    last_adaptation: Instant,
536    /// Adaptation history
537    adaptation_history: VecDeque<Adaptation<A>>,
538    /// Performance baseline for comparison
539    performance_baseline: Option<A>,
540    /// Rolling window of recently observed feature vectors, used to compute the
541    /// real feature-wise median that `adapt_for_anomaly` clips against.
542    recent_feature_window: VecDeque<Vec<A>>,
543    /// L2 norm of the gradient used by the most recent optimization step.
544    last_gradient_norm: Option<A>,
545    /// L2 norm of the parameter delta applied by the most recent step.
546    last_update_magnitude: Option<A>,
547    /// Wall-clock time the most recent optimization step took.
548    last_step_duration: Duration,
549    /// Phantom data for dimension type
550    _phantom: PhantomData<D>,
551}
552
553/// Number of data points retained for the rolling feature median.
554const FEATURE_WINDOW_CAPACITY: usize = 256;
555
556/// Number of recent performance snapshots the learning-rate controller fits its
557/// loss trend over.
558const LR_TREND_WINDOW: usize = 10;
559
560/// Relative tolerance within which a regression prediction counts as correct
561/// for the reported accuracy metric.
562const ACCURACY_RELATIVE_TOLERANCE: f64 = 0.1;
563
564impl<O, A, D> AdaptiveStreamingOptimizer<O, A, D>
565where
566    A: Float
567        + Default
568        + Clone
569        + Send
570        + Sync
571        + std::iter::Sum
572        + std::fmt::Debug
573        + std::ops::DivAssign
574        + scirs2_core::ndarray::ScalarOperand
575        + 'static,
576    D: Dimension,
577    O: Optimizer<A, D> + Clone,
578{
579    /// Creates a new adaptive streaming optimizer
580    pub fn new(base_optimizer: O, config: StreamingConfig) -> Result<Self, String> {
581        // Validate configuration
582        config.validate()?;
583
584        let buffer = AdaptiveBuffer::new(&config)?;
585        let drift_detector = EnhancedDriftDetector::new(&config)?;
586        let performance_tracker = PerformanceTracker::new(&config)?;
587        let resource_manager = ResourceManager::new(&config)?;
588        let meta_learner = MetaLearner::new(&config)?;
589        let anomaly_detector = AnomalyDetector::new(&config)?;
590        let learning_rate_controller =
591            AdaptiveLearningRateController::new(&config).map_err(|e| e.to_string())?;
592
593        let stats = AdaptiveStreamingStats {
594            total_data_points: 0,
595            optimization_steps: 0,
596            drift_events: 0,
597            anomalies_detected: 0,
598            adaptations_applied: 0,
599            recent_adaptations: 0,
600            current_buffer_size: config.buffer_config.initial_size,
601            current_learning_rate: config.learning_rate_config.initial_rate,
602            avg_processing_time_ms: 0.0,
603            resource_utilization: ResourceUsage::default(),
604            performance_trend: 0.0,
605            meta_learning_score: 0.0,
606        };
607
608        Ok(Self {
609            base_optimizer,
610            config,
611            buffer,
612            drift_detector,
613            performance_tracker,
614            resource_manager,
615            meta_learner,
616            anomaly_detector,
617            learning_rate_controller,
618            parameters: None,
619            stats,
620            last_adaptation: Instant::now(),
621            adaptation_history: VecDeque::with_capacity(1000),
622            performance_baseline: None,
623            recent_feature_window: VecDeque::with_capacity(FEATURE_WINDOW_CAPACITY),
624            last_gradient_norm: None,
625            last_update_magnitude: None,
626            last_step_duration: Duration::ZERO,
627            _phantom: PhantomData,
628        })
629    }
630
631    /// Performs an adaptive optimization step with streaming data
632    pub fn adaptive_step(
633        &mut self,
634        data_batch: Vec<StreamingDataPoint<A>>,
635    ) -> Result<Array<A, D>, String> {
636        let start_time = Instant::now();
637
638        // Update resource utilization tracking
639        self.resource_manager.update_utilization()?;
640
641        // Add data to buffer and check for anomalies
642        let filtered_batch = self.filter_anomalies(data_batch)?;
643        self.buffer.add_batch(filtered_batch)?;
644
645        // Check if buffer should be processed
646        if !self.should_process_buffer()? {
647            return self
648                .parameters
649                .clone()
650                .ok_or("No parameters available".to_string());
651        }
652
653        // Get batch from buffer for processing
654        let processing_batch = self.buffer.get_batch_for_processing()?;
655        self.stats.total_data_points += processing_batch.len();
656
657        // Detect drift in the data
658        let drift_detected = self.drift_detector.detect_drift(&processing_batch)?;
659        if drift_detected {
660            self.stats.drift_events += 1;
661        }
662
663        // Compute necessary adaptations
664        let adaptations = self.compute_adaptations(&processing_batch, drift_detected)?;
665
666        // Apply adaptations to system components.
667        //
668        // The lifetime counter is bumped here rather than with the rest of the
669        // statistics further down: `apply_adaptations` is what records them
670        // into `adaptation_history`, and the statistics block sits behind four
671        // `?` operators. A step that failed after adapting therefore left the
672        // adaptations in the history but uncounted, so `adaptations_applied`
673        // drifted permanently below the number of adaptations really applied
674        // (and below the recent-window count derived from the history).
675        self.apply_adaptations(&adaptations)?;
676        self.stats.adaptations_applied += adaptations.len();
677
678        // Perform actual optimization step
679        let updated_parameters = self.perform_optimization_step(&processing_batch)?;
680
681        // Evaluate performance of the optimization step
682        let performance = self.evaluate_performance(&processing_batch, &updated_parameters)?;
683
684        // Feed the buffer the real per-batch processing cost so its latency
685        // statistics (and the batch-size decisions that read them) are based on
686        // measurements rather than the zero they were stuck at.
687        self.buffer
688            .record_processing_duration(self.last_step_duration);
689
690        // Update performance tracking
691        self.performance_tracker
692            .add_performance(performance.clone())?;
693
694        // Update meta-learner with experience
695        self.update_meta_learner(&adaptations, &performance)?;
696
697        // Update statistics
698        self.stats.optimization_steps += 1;
699        self.stats.current_buffer_size = self.buffer.current_size();
700        self.stats.current_learning_rate = self
701            .learning_rate_controller
702            .current_rate()
703            .to_f64()
704            .unwrap_or(0.0);
705        self.stats.performance_trend = self.compute_performance_trend();
706        self.stats.meta_learning_score = self
707            .meta_learner
708            .get_effectiveness_score()
709            .to_f64()
710            .unwrap_or(0.0);
711
712        let processing_time = start_time.elapsed().as_millis() as f64;
713        self.stats.avg_processing_time_ms = (self.stats.avg_processing_time_ms
714            * (self.stats.optimization_steps - 1) as f64
715            + processing_time)
716            / self.stats.optimization_steps as f64;
717
718        // Store updated parameters
719        self.parameters = Some(updated_parameters.clone());
720
721        Ok(updated_parameters)
722    }
723
724    /// Filters out anomalous data points
725    fn filter_anomalies(
726        &mut self,
727        data_batch: Vec<StreamingDataPoint<A>>,
728    ) -> Result<Vec<StreamingDataPoint<A>>, String> {
729        if !self.config.anomaly_config.enable_detection {
730            return Ok(data_batch);
731        }
732
733        // Feed the detector the context it cannot observe for itself, from real
734        // current state. A3: `AnomalyContext` used to be built from hard-coded
735        // 0.8/0.7, 0.6/0.5 and 0.1 placeholders.
736        self.publish_anomaly_context_signals()?;
737
738        let mut filtered_batch = Vec::new();
739
740        for data_point in data_batch {
741            // Retain the point in the rolling median window *before* it is
742            // classified, so `compute_feature_median` is computed against real
743            // history rather than the point itself.
744            self.remember_features(&data_point.features);
745            let is_anomaly = self.anomaly_detector.detect_anomaly(&data_point)?;
746
747            if is_anomaly {
748                self.stats.anomalies_detected += 1;
749
750                // Apply anomaly response strategy
751                match &self.config.anomaly_config.response_strategy {
752                    AnomalyResponseStrategy::Ignore => {
753                        // Include the data point anyway
754                        filtered_batch.push(data_point);
755                    }
756                    AnomalyResponseStrategy::Filter => {
757                        // Skip this data point
758                        continue;
759                    }
760                    AnomalyResponseStrategy::Adaptive => {
761                        // Adapt the data point or model
762                        let adapted_point = self.adapt_for_anomaly(data_point)?;
763                        filtered_batch.push(adapted_point);
764                    }
765                    AnomalyResponseStrategy::Reset => {
766                        // Reset relevant components (implemented in apply_adaptations)
767                        filtered_batch.push(data_point);
768                    }
769                    AnomalyResponseStrategy::Custom(_) => {
770                        // Custom handling (simplified)
771                        filtered_batch.push(data_point);
772                    }
773                }
774            } else {
775                filtered_batch.push(data_point);
776            }
777        }
778
779        Ok(filtered_batch)
780    }
781
782    /// Publishes the current performance, resource and drift state into the
783    /// anomaly detector, which has no direct handle on any of them.
784    fn publish_anomaly_context_signals(&mut self) -> Result<(), String> {
785        let performance_metrics: Vec<A> =
786            match self.performance_tracker.get_recent_performance(1).first() {
787                Some(snapshot) => vec![
788                    snapshot.loss,
789                    snapshot.accuracy.unwrap_or_else(A::zero),
790                    snapshot.convergence_rate.unwrap_or_else(A::zero),
791                ],
792                None => Vec::new(),
793            };
794
795        let usage = self.resource_manager.current_usage()?;
796        let mut resource_usage = Vec::new();
797        if let Some(memory_mb) = A::from(usage.memory_usage_mb as f64) {
798            resource_usage.push(memory_mb);
799        }
800        if let Some(cpu) = A::from(usage.cpu_usage_percent) {
801            resource_usage.push(cpu);
802        }
803
804        // The drift detector's live state and observed false-positive rate are
805        // the two drift signals this module genuinely knows.
806        let diagnostics = self.drift_detector.get_diagnostics();
807        let mut drift_indicators = Vec::new();
808        if let Some(state) = A::from(match diagnostics.current_state {
809            crate::streaming::adaptive_streaming::drift_detection::DriftState::Stable => 0.0,
810            crate::streaming::adaptive_streaming::drift_detection::DriftState::Warning => 1.0,
811            crate::streaming::adaptive_streaming::drift_detection::DriftState::Drift => 2.0,
812            crate::streaming::adaptive_streaming::drift_detection::DriftState::Recovery => 3.0,
813        }) {
814            drift_indicators.push(state);
815        }
816        if let Some(fp_rate) = A::from(diagnostics.false_positive_rate) {
817            drift_indicators.push(fp_rate);
818        }
819
820        // The meta-learner's bandit context needs the same real signals.
821        self.meta_learner
822            .update_context_signals(resource_usage.clone(), drift_indicators.clone());
823
824        self.anomaly_detector.update_context_signals(
825            performance_metrics,
826            resource_usage,
827            drift_indicators,
828        );
829        Ok(())
830    }
831
832    /// Adapts a data point that was detected as anomalous
833    fn adapt_for_anomaly(
834        &self,
835        mut data_point: StreamingDataPoint<A>,
836    ) -> Result<StreamingDataPoint<A>, String> {
837        // Simple adaptation: reduce the influence of extreme values
838        let median = self.compute_feature_median(&data_point.features)?;
839
840        for (i, value) in data_point.features.iter_mut().enumerate() {
841            let diff = (*value - median[i]).abs();
842            let threshold =
843                median[i] * try_scalar_str::<A, _>(self.config.anomaly_config.threshold)?;
844
845            if diff > threshold {
846                // Clip the value to be within the threshold
847                let sign = if *value > median[i] {
848                    A::one()
849                } else {
850                    -A::one()
851                };
852                *value = median[i] + sign * threshold;
853            }
854        }
855
856        // Reduce quality score for adapted anomalous data
857        data_point.quality_score = data_point.quality_score * try_scalar_str::<A, _>(0.5)?;
858
859        Ok(data_point)
860    }
861
862    /// Computes the feature-wise median over the rolling window of recently
863    /// observed data points.
864    ///
865    /// O2: this used to `return Ok(features.clone())`, which made
866    /// `adapt_for_anomaly` a guaranteed no-op — every `diff` was
867    /// `|value - value| == 0`, so nothing was ever clipped and the
868    /// `AnomalyResponseStrategy::Adaptive` branch silently did nothing beyond
869    /// halving the quality score.
870    ///
871    /// The median is now a genuine per-coordinate order statistic over the
872    /// retained window, selected in expected linear time with
873    /// `select_nth_unstable_by`. Coordinates the window has never seen fall back
874    /// to the incoming value, which is the only defensible estimate available
875    /// for them.
876    fn compute_feature_median(&self, features: &Array1<A>) -> Result<Array1<A>, String> {
877        let window = &self.recent_feature_window;
878        if window.is_empty() {
879            // No history yet: the point is its own best estimate of the centre.
880            return Ok(features.clone());
881        }
882
883        let mut medians = Array1::zeros(features.len());
884        for index in 0..features.len() {
885            let mut column: Vec<A> = window
886                .iter()
887                .filter_map(|point| point.get(index).copied())
888                .filter(|value| !value.is_nan())
889                .collect();
890            medians[index] = match super::statistics::median_in_place(&mut column) {
891                Some(median) => median,
892                None => features[index],
893            };
894        }
895        Ok(medians)
896    }
897
898    /// Records a data point's features in the rolling window backing
899    /// [`Self::compute_feature_median`].
900    fn remember_features(&mut self, features: &Array1<A>) {
901        if self.recent_feature_window.len() >= FEATURE_WINDOW_CAPACITY {
902            self.recent_feature_window.pop_front();
903        }
904        self.recent_feature_window.push_back(features.to_vec());
905    }
906
907    /// Number of data points retained in the median window.
908    pub fn feature_window_len(&self) -> usize {
909        self.recent_feature_window.len()
910    }
911
912    /// Checks if the buffer should be processed
913    fn should_process_buffer(&self) -> Result<bool, String> {
914        let buffer_quality = self.buffer.get_quality_metrics();
915        let buffer_size = self.buffer.current_size();
916
917        // Check size threshold
918        let size_threshold = self.config.buffer_config.initial_size;
919        let size_ready = buffer_size >= size_threshold;
920
921        // Check quality threshold
922        let quality_ready = buffer_quality.average_quality
923            >= try_scalar_str::<A, _>(self.config.buffer_config.quality_threshold)?;
924
925        // Check timeout
926        let timeout_ready = self.buffer.time_since_last_processing()
927            >= self.config.buffer_config.processing_timeout;
928
929        // Check resource availability
930        let resources_available = self
931            .resource_manager
932            .has_sufficient_resources_for_processing()?;
933
934        Ok((size_ready && quality_ready) || timeout_ready && resources_available)
935    }
936
937    /// Computes necessary adaptations based on current state
938    fn compute_adaptations(
939        &mut self,
940        batch: &[StreamingDataPoint<A>],
941        drift_detected: bool,
942    ) -> Result<Vec<Adaptation<A>>, String> {
943        let mut adaptations = Vec::new();
944
945        // Learning rate adaptation, driven by the real recent loss history
946        // (oldest first, which is the order `compute_adaptation` fits its
947        // trend over). Previously an empty slice was passed, so the controller
948        // could never see anything and always echoed its own rate back.
949        let mut recent_losses: Vec<A> = self
950            .performance_tracker
951            .get_recent_performance(LR_TREND_WINDOW)
952            .iter()
953            .map(|snapshot| snapshot.loss)
954            .collect();
955        recent_losses.reverse();
956        let lr_value = self
957            .learning_rate_controller
958            .compute_adaptation(&recent_losses);
959        let lr_adaptation = Adaptation {
960            adaptation_type: AdaptationType::LearningRate,
961            magnitude: lr_value,
962            target_component: String::from("learning_rate"),
963            parameters: HashMap::new(),
964            priority: AdaptationPriority::Normal,
965            timestamp: Instant::now(),
966        };
967        adaptations.push(lr_adaptation);
968
969        // Drift-based adaptations
970        if drift_detected {
971            if let Some(drift_adaptation) = self.drift_detector.compute_sensitivity_adaptation()? {
972                adaptations.push(drift_adaptation);
973            }
974        }
975
976        // Buffer size adaptation
977        if let Some(buffer_adaptation) = self
978            .buffer
979            .compute_size_adaptation(&self.performance_tracker)?
980        {
981            adaptations.push(buffer_adaptation);
982        }
983
984        // Resource allocation adaptation.
985        //
986        // O3: this was commented out with a "type mismatch (f32 vs A)" note,
987        // which silently disabled every memory- and CPU-pressure response the
988        // resource manager computes. `ResourceManager` works in `f32`, so the
989        // adaptation is converted across the boundary here — the target
990        // component string is preserved verbatim because
991        // `apply_allocation_adaptation` dispatches on it.
992        if let Some(resource_adaptation) = self.resource_manager.compute_allocation_adaptation()? {
993            let magnitude = A::from(resource_adaptation.magnitude).ok_or_else(|| {
994                format!(
995                    "resource adaptation magnitude {} is not representable in the element type",
996                    resource_adaptation.magnitude
997                )
998            })?;
999            let mut parameters = HashMap::new();
1000            for (key, value) in &resource_adaptation.parameters {
1001                let converted = A::from(*value).ok_or_else(|| {
1002                    format!("resource adaptation parameter '{key}' ({value}) is not representable")
1003                })?;
1004                parameters.insert(key.clone(), converted);
1005            }
1006            adaptations.push(Adaptation {
1007                adaptation_type: resource_adaptation.adaptation_type.clone(),
1008                magnitude,
1009                target_component: resource_adaptation.target_component.clone(),
1010                parameters,
1011                priority: resource_adaptation.priority.clone(),
1012                timestamp: resource_adaptation.timestamp,
1013            });
1014        }
1015
1016        // Meta-learning based adaptations
1017        let meta_adaptations = self
1018            .meta_learner
1019            .recommend_adaptations(batch, &self.performance_tracker)?;
1020        adaptations.extend(meta_adaptations);
1021
1022        // Sort adaptations by priority
1023        adaptations.sort_by(|a, b| b.priority.cmp(&a.priority));
1024
1025        Ok(adaptations)
1026    }
1027
1028    /// Applies computed adaptations to system components
1029    fn apply_adaptations(&mut self, adaptations: &[Adaptation<A>]) -> Result<(), String> {
1030        for adaptation in adaptations {
1031            match &adaptation.adaptation_type {
1032                AdaptationType::LearningRate => {
1033                    self.learning_rate_controller
1034                        .apply_adaptation(adaptation.magnitude);
1035                }
1036                AdaptationType::BufferSize => {
1037                    self.buffer.apply_size_adaptation(adaptation)?;
1038                }
1039                AdaptationType::DriftSensitivity => {
1040                    self.drift_detector
1041                        .apply_sensitivity_adaptation(adaptation)?;
1042                }
1043                AdaptationType::ResourceAllocation => {
1044                    // Convert back into the `f32` domain the resource manager
1045                    // works in and apply it for real.
1046                    let magnitude = adaptation.magnitude.to_f32().ok_or_else(|| {
1047                        "resource adaptation magnitude is not representable as f32".to_string()
1048                    })?;
1049                    let mut parameters = HashMap::new();
1050                    for (key, value) in &adaptation.parameters {
1051                        let converted = value.to_f32().ok_or_else(|| {
1052                            format!(
1053                                "resource adaptation parameter '{key}' is not representable as f32"
1054                            )
1055                        })?;
1056                        parameters.insert(key.clone(), converted);
1057                    }
1058                    let converted = Adaptation::<f32> {
1059                        adaptation_type: adaptation.adaptation_type.clone(),
1060                        magnitude,
1061                        target_component: adaptation.target_component.clone(),
1062                        parameters,
1063                        priority: adaptation.priority.clone(),
1064                        timestamp: adaptation.timestamp,
1065                    };
1066                    self.resource_manager
1067                        .apply_allocation_adaptation(&converted)?;
1068                }
1069                AdaptationType::PerformanceThreshold => {
1070                    self.performance_tracker
1071                        .apply_threshold_adaptation(adaptation)?;
1072                }
1073                AdaptationType::AnomalyDetection => {
1074                    self.anomaly_detector.apply_adaptation(adaptation)?;
1075                }
1076                AdaptationType::MetaLearning => {
1077                    self.meta_learner.apply_adaptation(adaptation)?;
1078                }
1079                AdaptationType::Custom(name) => {
1080                    // There is no registry of custom adaptation handlers, so
1081                    // accepting one silently (or merely printing it to stdout
1082                    // from library code) would let it look applied when nothing
1083                    // happened.
1084                    return Err(format!(
1085                        "no handler is registered for custom adaptation '{name}'"
1086                    ));
1087                }
1088            }
1089
1090            // Store adaptation in history
1091            if self.adaptation_history.len() >= 1000 {
1092                self.adaptation_history.pop_front();
1093            }
1094            self.adaptation_history.push_back(adaptation.clone());
1095        }
1096
1097        self.last_adaptation = Instant::now();
1098        Ok(())
1099    }
1100
1101    /// Performs the actual optimization step
1102    fn perform_optimization_step(
1103        &mut self,
1104        batch: &[StreamingDataPoint<A>],
1105    ) -> Result<Array<A, D>, String> {
1106        let started = Instant::now();
1107
1108        // Compute gradients from the batch
1109        let gradients = self.compute_batch_gradients(batch)?;
1110
1111        // Fold the real gradient into the learning-rate controller before the
1112        // step, so the AdaGrad-style trust region actually sees it. The
1113        // controller previously never received a gradient at all.
1114        let learning_rate = self
1115            .learning_rate_controller
1116            .update_learning_rate(&gradients);
1117
1118        let parameters = if let Some(params) = self.parameters.clone() {
1119            params
1120        } else {
1121            // Cannot initialize parameters without proper dimension info
1122            return Err("Parameters not initialized".to_string());
1123        };
1124
1125        // Hand the step to the base optimizer the caller supplied.
1126        //
1127        // This used to be an inline `param -= lr * grad` loop with the comment
1128        // "in practice would use the base optimizer": the `O` type parameter
1129        // and the `base_optimizer` constructor argument were accepted and then
1130        // ignored, so an `AdaptiveStreamingOptimizer<Adam<_>, ..>` silently ran
1131        // plain SGD and none of Adam's moments existed. The adaptive
1132        // learning-rate controller drives the base optimizer's rate, exactly as
1133        // the streaming optimizer in `streaming::types` does.
1134        if parameters.len() != gradients.len() {
1135            return Err(format!(
1136                "parameter/gradient dimensionality mismatch: {} parameters but {} gradients",
1137                parameters.len(),
1138                gradients.len()
1139            ));
1140        }
1141        let gradients_d = gradients
1142            .clone()
1143            .into_dimensionality::<D>()
1144            .map_err(|e| format!("gradient does not fit the parameter dimensionality: {e}"))?;
1145        self.base_optimizer.set_learning_rate(learning_rate);
1146        let updated_parameters = self
1147            .base_optimizer
1148            .step(&parameters, &gradients_d)
1149            .map_err(|e| format!("base optimizer step failed: {e}"))?;
1150
1151        let squared_update = parameters.iter().zip(updated_parameters.iter()).fold(
1152            A::zero(),
1153            |acc, (&before, &after)| {
1154                let delta = after - before;
1155                acc + delta * delta
1156            },
1157        );
1158
1159        // Record the real magnitudes so `evaluate_performance` reports
1160        // measurements instead of the fixed 1.0 / 0.1 placeholders.
1161        let squared_gradient = gradients.iter().fold(A::zero(), |acc, &g| acc + g * g);
1162        self.last_gradient_norm = Some(squared_gradient.sqrt());
1163        self.last_update_magnitude = Some(squared_update.sqrt());
1164        self.last_step_duration = started.elapsed();
1165
1166        Ok(updated_parameters)
1167    }
1168
1169    /// Computes batch gradients from streaming data
1170    fn compute_batch_gradients(
1171        &self,
1172        batch: &[StreamingDataPoint<A>],
1173    ) -> Result<Array1<A>, String> {
1174        if batch.is_empty() {
1175            return Err("Cannot compute gradients from empty batch".to_string());
1176        }
1177
1178        let feature_dim = batch[0].features.len();
1179        let mut gradients = Array1::zeros(feature_dim);
1180
1181        // Simplified gradient computation (in practice would depend on loss function)
1182        for data_point in batch {
1183            for (i, &feature) in data_point.features.iter().enumerate() {
1184                gradients[i] = gradients[i] + feature * data_point.quality_score;
1185            }
1186        }
1187
1188        // Normalize by batch size
1189        let batch_size = try_scalar_str::<A, _>(batch.len())?;
1190        gradients /= batch_size;
1191
1192        Ok(gradients)
1193    }
1194
1195    /// Evaluates performance of the optimization step
1196    fn evaluate_performance(
1197        &self,
1198        batch: &[StreamingDataPoint<A>],
1199        parameters: &Array<A, D>,
1200    ) -> Result<PerformanceSnapshot<A>, String> {
1201        // Compute various performance metrics
1202        let loss = self.compute_loss(batch, parameters)?;
1203        let accuracy = self.compute_accuracy(batch, parameters)?;
1204        let convergence_rate = self.compute_convergence_rate(parameters)?;
1205
1206        // Compute data statistics
1207        let data_stats = self.compute_data_statistics(batch)?;
1208
1209        // Get resource usage
1210        let resource_usage = self.resource_manager.current_usage()?;
1211
1212        let performance = PerformanceSnapshot {
1213            timestamp: Instant::now(),
1214            // Real wall-clock cost of the step that produced this snapshot.
1215            // B2: `compute_size_adaptation` used to read `timestamp.elapsed()`
1216            // (the snapshot's *age*) as if it were the processing time.
1217            processing_duration: self.last_step_duration,
1218            loss,
1219            accuracy: Some(accuracy),
1220            convergence_rate: Some(convergence_rate),
1221            // Measured in `perform_optimization_step`; `None` before the first
1222            // step rather than a fabricated 1.0 / 0.1.
1223            gradient_norm: self.last_gradient_norm,
1224            parameter_update_magnitude: self.last_update_magnitude,
1225            data_statistics: data_stats,
1226            resource_usage,
1227            custom_metrics: HashMap::new(),
1228        };
1229
1230        Ok(performance)
1231    }
1232
1233    /// Linear prediction of the model for one data point.
1234    ///
1235    /// `perform_optimization_step` updates `parameters` coordinate-wise against
1236    /// the per-feature gradient, so the parameter array is aligned with the
1237    /// feature vector in row-major order and the model this optimizer is
1238    /// actually fitting is the linear one `y_hat = <w, x>`. Computing the
1239    /// prediction that way makes the loss a genuine function of the parameters
1240    /// rather than of the input alone.
1241    fn linear_prediction(&self, features: &Array1<A>, parameters: &Array<A, D>) -> A {
1242        parameters
1243            .iter()
1244            .zip(features.iter())
1245            .fold(A::zero(), |acc, (&weight, &feature)| acc + weight * feature)
1246    }
1247
1248    /// Computes mean squared error for the current batch and parameters.
1249    fn compute_loss(
1250        &self,
1251        batch: &[StreamingDataPoint<A>],
1252        parameters: &Array<A, D>,
1253    ) -> Result<A, String> {
1254        // Mean squared error of the model's own prediction. This used to take
1255        // `prediction = &data_point.features`, i.e. it scored the *input*
1256        // against the target and ignored `parameters` entirely — so the reported
1257        // loss never moved when the model improved.
1258        let mut total_loss = A::zero();
1259        let mut count = 0usize;
1260
1261        for data_point in batch {
1262            let Some(target) = data_point.target.as_ref() else {
1263                continue;
1264            };
1265            let Some(&target_value) = target.iter().next() else {
1266                continue;
1267            };
1268            let prediction = self.linear_prediction(&data_point.features, parameters);
1269            let residual = prediction - target_value;
1270            total_loss = total_loss + residual * residual;
1271            count += 1;
1272        }
1273
1274        if count == 0 {
1275            // No labelled point in the batch: there is no loss to report, which
1276            // is honestly zero contribution rather than a made-up figure.
1277            return Ok(A::zero());
1278        }
1279        let divisor =
1280            A::from(count).ok_or_else(|| format!("batch size {count} is not representable"))?;
1281        Ok(total_loss / divisor)
1282    }
1283
1284    /// Computes accuracy for the current batch and parameters.
1285    ///
1286    /// For a regression model "accuracy" is the fraction of predictions that
1287    /// land within a tolerance of the target. The tolerance is the configured
1288    /// convergence threshold scaled by the target magnitude, so it is
1289    /// scale-free. This used to count a point as "correct" whenever its
1290    /// `quality_score > 0.5`, which measured the *input data quality* and had
1291    /// nothing to do with the model's predictions.
1292    fn compute_accuracy(
1293        &self,
1294        batch: &[StreamingDataPoint<A>],
1295        parameters: &Array<A, D>,
1296    ) -> Result<A, String> {
1297        let relative_tolerance = A::from(ACCURACY_RELATIVE_TOLERANCE).ok_or_else(|| {
1298            format!("accuracy tolerance {ACCURACY_RELATIVE_TOLERANCE} is not representable")
1299        })?;
1300        let epsilon = A::from(1e-8).ok_or_else(|| "1e-8 is not representable".to_string())?;
1301
1302        let mut correct = 0usize;
1303        let mut total = 0usize;
1304
1305        for data_point in batch {
1306            let Some(target) = data_point.target.as_ref() else {
1307                continue;
1308            };
1309            let Some(&target_value) = target.iter().next() else {
1310                continue;
1311            };
1312            let prediction = self.linear_prediction(&data_point.features, parameters);
1313            let tolerance = relative_tolerance * target_value.abs().max(epsilon);
1314            if (prediction - target_value).abs() <= tolerance {
1315                correct += 1;
1316            }
1317            total += 1;
1318        }
1319
1320        if total == 0 {
1321            // Nothing labelled to score against: report zero rather than the
1322            // perfect `1.0` this used to claim for an unlabelled batch.
1323            return Ok(A::zero());
1324        }
1325        let numerator =
1326            A::from(correct).ok_or_else(|| format!("{correct} is not representable"))?;
1327        let denominator = A::from(total).ok_or_else(|| format!("{total} is not representable"))?;
1328        Ok(numerator / denominator)
1329    }
1330
1331    /// Computes convergence rate
1332    fn compute_convergence_rate(&self, _parameters: &Array<A, D>) -> Result<A, String> {
1333        // `get_recent_losses` returns most-recent-first (it reverses the
1334        // history buffer), so index 0 is the newest loss and the last
1335        // index is the oldest loss in the window (O5 fix). "Convergence
1336        // rate" should be positive when loss is decreasing: that requires
1337        // `oldest - newest`, not `newest - oldest` (which the previous code
1338        // computed, inverting the sign — a genuinely converging model
1339        // reported a *negative* rate and a diverging one a *positive* rate).
1340        let recent_losses = self.performance_tracker.get_recent_losses(10);
1341        if recent_losses.len() >= 2 {
1342            let newest = recent_losses[0];
1343            let oldest = recent_losses[recent_losses.len() - 1];
1344            let improvement = oldest - newest;
1345            if oldest != A::zero() {
1346                Ok(improvement / oldest)
1347            } else {
1348                Ok(A::zero())
1349            }
1350        } else {
1351            Ok(A::zero())
1352        }
1353    }
1354
1355    /// Computes comprehensive data statistics
1356    fn compute_data_statistics(
1357        &self,
1358        batch: &[StreamingDataPoint<A>],
1359    ) -> Result<DataStatistics<A>, String> {
1360        if batch.is_empty() {
1361            return Ok(DataStatistics::default());
1362        }
1363
1364        let feature_dim = batch[0].features.len();
1365        let mut feature_means = Array1::zeros(feature_dim);
1366        let mut feature_stds = Array1::zeros(feature_dim);
1367        let mut quality_scores = Vec::new();
1368
1369        // Compute means
1370        for data_point in batch {
1371            feature_means = feature_means + &data_point.features;
1372            quality_scores.push(data_point.quality_score);
1373        }
1374        feature_means /= try_scalar_str::<A, _>(batch.len())?;
1375
1376        // Compute standard deviations
1377        for data_point in batch {
1378            let diff = &data_point.features - &feature_means;
1379            feature_stds = feature_stds + &diff.mapv(|x| x * x);
1380        }
1381        feature_stds /= try_scalar_str::<A, _>(batch.len())?;
1382        feature_stds = feature_stds.mapv(|x| x.sqrt());
1383
1384        let avg_quality = quality_scores.iter().copied().sum::<A>()
1385            / try_scalar_str::<A, _>(quality_scores.len())?;
1386
1387        Ok(DataStatistics {
1388            sample_count: batch.len(),
1389            feature_means,
1390            feature_stds,
1391            average_quality: avg_quality,
1392            timestamp: Instant::now(),
1393        })
1394    }
1395
1396    /// Updates meta-learner with experience from this optimization step.
1397    ///
1398    /// Deliberately takes no data batch: `MetaState` has no slot for per-batch
1399    /// data characteristics (its features are performance, resource and drift
1400    /// signals, whose layout the bandit's feature scaler depends on), so a batch
1401    /// argument could only be discarded — which is what it used to be.
1402    fn update_meta_learner(
1403        &mut self,
1404        adaptations: &[Adaptation<A>],
1405        performance: &PerformanceSnapshot<A>,
1406    ) -> Result<(), String> {
1407        if !self.config.meta_learning_config.enable_meta_learning {
1408            return Ok(());
1409        }
1410
1411        // Extract meta-state from current situation
1412        let meta_state = self.extract_meta_state(performance)?;
1413
1414        // Extract meta-action from applied adaptations
1415        let meta_action = self.extract_meta_action(adaptations)?;
1416
1417        // Compute reward based on performance improvement
1418        let reward = self.compute_meta_reward(performance)?;
1419
1420        // Update meta-learner
1421        self.meta_learner
1422            .update_experience(meta_state, meta_action, reward)?;
1423
1424        Ok(())
1425    }
1426
1427    /// Extracts meta-state representation from performance data
1428    fn extract_meta_state(
1429        &self,
1430        performance: &PerformanceSnapshot<A>,
1431    ) -> Result<MetaState<A>, String> {
1432        let state = MetaState {
1433            performance_metrics: vec![
1434                performance.loss,
1435                performance.accuracy.unwrap_or(A::zero()),
1436                performance.convergence_rate.unwrap_or(A::zero()),
1437            ],
1438            resource_state: vec![
1439                try_scalar_str::<A, _>(performance.resource_usage.memory_usage_mb as f64)?,
1440                try_scalar_str::<A, _>(performance.resource_usage.cpu_usage_percent)?,
1441            ],
1442            drift_indicators: vec![try_scalar_str::<A, _>(
1443                if self.drift_detector.is_drift_detected() {
1444                    1.0
1445                } else {
1446                    0.0
1447                },
1448            )?],
1449            adaptation_history: self.adaptation_history.len(),
1450            timestamp: Instant::now(),
1451        };
1452
1453        Ok(state)
1454    }
1455
1456    /// Extracts meta-action representation from adaptations
1457    fn extract_meta_action(&self, adaptations: &[Adaptation<A>]) -> Result<MetaAction<A>, String> {
1458        let mut adaptation_vector = Vec::new();
1459        let mut adaptation_types = Vec::new();
1460
1461        for adaptation in adaptations {
1462            adaptation_vector.push(adaptation.magnitude);
1463            adaptation_types.push(adaptation.adaptation_type.clone());
1464        }
1465
1466        let action = MetaAction {
1467            adaptation_magnitudes: adaptation_vector,
1468            adaptation_types,
1469            learning_rate_change: self
1470                .learning_rate_controller
1471                .last_change()
1472                .unwrap_or(A::zero()),
1473            buffer_size_change: A::from(self.buffer.last_size_change()).unwrap_or(A::zero()),
1474            timestamp: Instant::now(),
1475        };
1476
1477        Ok(action)
1478    }
1479
1480    /// Computes reward for meta-learning based on performance improvement
1481    fn compute_meta_reward(&self, performance: &PerformanceSnapshot<A>) -> Result<A, String> {
1482        // Compare with baseline or previous performance
1483        let reward = if let Some(baseline) = self.performance_baseline {
1484            performance.loss - baseline // Negative reward for higher loss
1485        } else {
1486            A::zero()
1487        };
1488
1489        Ok(reward)
1490    }
1491
1492    /// Gets current adaptive streaming statistics
1493    pub fn get_adaptive_stats(&self) -> AdaptiveStreamingStats {
1494        let mut stats = self.stats.clone();
1495        stats.resource_utilization = self.resource_manager.current_usage().unwrap_or_default();
1496        // `adaptations_applied` is a lifetime counter; the recent-window count
1497        // is derived live from `adaptation_history` so callers can tell a
1498        // currently-thrashing optimizer from one that adapted long ago.
1499        stats.recent_adaptations = self.count_adaptations_applied(RECENT_ADAPTATION_WINDOW);
1500        stats
1501    }
1502
1503    /// Counts the adaptations recorded within `window` of now.
1504    ///
1505    /// Uses a forward `duration_since` comparison rather than materialising an
1506    /// `Instant::now() - window` cutoff: subtracting a `Duration` from an
1507    /// `Instant` panics when the process has been up for less than `window`.
1508    fn count_adaptations_applied(&self, window: Duration) -> usize {
1509        let now = Instant::now();
1510        self.adaptation_history
1511            .iter()
1512            .filter(|adaptation| now.duration_since(adaptation.timestamp) <= window)
1513            .count()
1514    }
1515
1516    /// Computes performance trend over recent optimization steps
1517    fn compute_performance_trend(&self) -> f64 {
1518        let recent_performance = self.performance_tracker.get_recent_performance(20);
1519        if recent_performance.len() >= 2 {
1520            let recent_avg = recent_performance
1521                .iter()
1522                .rev()
1523                .take(5)
1524                .map(|p| p.loss.to_f64().unwrap_or(0.0))
1525                .sum::<f64>()
1526                / 5.0;
1527
1528            let older_avg = recent_performance
1529                .iter()
1530                .take(5)
1531                .map(|p| p.loss.to_f64().unwrap_or(0.0))
1532                .sum::<f64>()
1533                / 5.0;
1534
1535            // Negative trend means improvement (lower loss)
1536            (recent_avg - older_avg) / older_avg
1537        } else {
1538            0.0
1539        }
1540    }
1541
1542    /// Forces an adaptation cycle even if normal triggers haven't fired
1543    pub fn force_adaptation(&mut self) -> Result<(), String> {
1544        let empty_batch = Vec::new();
1545        let adaptations = self.compute_adaptations(&empty_batch, false)?;
1546        self.apply_adaptations(&adaptations)?;
1547        Ok(())
1548    }
1549
1550    /// Resets the optimizer to initial state while preserving learned knowledge
1551    pub fn soft_reset(&mut self) -> Result<(), String> {
1552        // Reset components while preserving meta-learning knowledge
1553        self.buffer.reset()?;
1554        self.drift_detector.reset()?;
1555        self.performance_tracker.reset()?;
1556
1557        // Don't reset meta-learner to preserve learned adaptations
1558        // self.meta_learner.reset()?;
1559
1560        self.stats = AdaptiveStreamingStats {
1561            total_data_points: 0,
1562            optimization_steps: 0,
1563            drift_events: 0,
1564            anomalies_detected: 0,
1565            adaptations_applied: 0,
1566            recent_adaptations: 0,
1567            current_buffer_size: self.config.buffer_config.initial_size,
1568            current_learning_rate: self.config.learning_rate_config.initial_rate,
1569            avg_processing_time_ms: 0.0,
1570            resource_utilization: ResourceUsage::default(),
1571            performance_trend: 0.0,
1572            meta_learning_score: self.meta_learner.get_effectiveness_score() as f64,
1573        };
1574
1575        self.adaptation_history.clear();
1576        self.performance_baseline = None;
1577
1578        Ok(())
1579    }
1580
1581    /// Gets detailed diagnostic information
1582    pub fn get_diagnostics(&self) -> StreamingDiagnostics {
1583        StreamingDiagnostics {
1584            buffer_diagnostics: self.buffer.get_diagnostics(),
1585            drift_diagnostics: self.drift_detector.get_diagnostics(),
1586            performance_diagnostics: self.performance_tracker.get_diagnostics(),
1587            resource_diagnostics: self.resource_manager.get_diagnostics(),
1588            meta_learning_diagnostics: self.meta_learner.get_diagnostics(),
1589            anomaly_diagnostics: self.anomaly_detector.get_diagnostics(),
1590        }
1591    }
1592}
1593
1594/// Comprehensive diagnostic information for streaming optimizer
1595#[derive(Debug, Clone)]
1596pub struct StreamingDiagnostics {
1597    pub buffer_diagnostics: BufferDiagnostics,
1598    pub drift_diagnostics: DriftDiagnostics,
1599    pub performance_diagnostics: PerformanceDiagnostics,
1600    pub resource_diagnostics: ResourceDiagnostics,
1601    pub meta_learning_diagnostics: MetaLearningDiagnostics,
1602    pub anomaly_diagnostics: AnomalyDiagnostics,
1603}
1604
1605#[cfg(test)]
1606#[path = "optimizer_convergence_tests.rs"]
1607mod o5_convergence_rate_tests;
1608
1609#[cfg(test)]
1610#[path = "optimizer_regression_tests.rs"]
1611mod regression_tests;