Skip to main content

optirs_core/streaming/
enhanced_adaptive_lr.rs

1// Enhanced adaptive learning rate mechanisms for streaming optimization
2//
3// This module provides advanced adaptive learning rate controllers that can
4// dynamically adjust learning rates based on multiple signals including
5// gradient statistics, performance metrics, concept drift, and resource constraints.
6
7use scirs2_core::ndarray::Array1;
8use scirs2_core::numeric::Float;
9use std::collections::{HashMap, VecDeque};
10use std::time::{Duration, Instant};
11
12use crate::error::Result;
13use crate::utils::scalar_or;
14
15/// Performance metric types for adaptation
16#[derive(Debug, Clone)]
17pub enum PerformanceMetric<A: Float + Send + Sync> {
18    Loss(A),
19    Accuracy(A),
20    F1Score(A),
21    AUC(A),
22    Custom { name: String, value: A },
23}
24
25/// Enhanced adaptive learning rate controller with multiple adaptation mechanisms
26#[derive(Debug, Clone)]
27pub struct EnhancedAdaptiveLRController<A: Float + Send + Sync> {
28    /// Current learning rate
29    current_lr: A,
30
31    /// Base learning rate
32    base_lr: A,
33
34    /// Multi-signal adaptation strategy
35    adaptation_strategy: MultiSignalAdaptationStrategy<A>,
36
37    /// Gradient-based adaptation state
38    gradient_adapter: GradientBasedAdapter<A>,
39
40    /// Performance-based adaptation state
41    performance_adapter: PerformanceBasedAdapter<A>,
42
43    /// Drift-aware adaptation
44    drift_adapter: DriftAwareAdapter<A>,
45
46    /// Resource-aware adaptation
47    resource_adapter: ResourceAwareAdapter<A>,
48
49    /// Meta-learning for hyperparameter optimization
50    meta_optimizer: MetaOptimizer<A>,
51
52    /// Adaptation history for analysis
53    adaptation_history: VecDeque<AdaptationEvent<A>>,
54
55    /// Configuration
56    config: AdaptiveLRConfig<A>,
57}
58
59/// Configuration for adaptive learning rate controller
60#[derive(Debug, Clone)]
61pub struct AdaptiveLRConfig<A: Float + Send + Sync> {
62    /// Base learning rate
63    pub base_lr: A,
64
65    /// Minimum allowed learning rate
66    pub min_lr: A,
67
68    /// Maximum allowed learning rate  
69    pub max_lr: A,
70
71    /// Enable gradient-based adaptation
72    pub enable_gradient_adaptation: bool,
73
74    /// Enable performance-based adaptation
75    pub enable_performance_adaptation: bool,
76
77    /// Enable drift-aware adaptation
78    pub enable_drift_adaptation: bool,
79
80    /// Enable resource-aware adaptation
81    pub enable_resource_adaptation: bool,
82
83    /// Enable meta-learning optimization
84    pub enable_meta_learning: bool,
85
86    /// History window size
87    pub history_window_size: usize,
88
89    /// Adaptation frequency (steps)
90    pub adaptation_frequency: usize,
91
92    /// Sensitivity to changes
93    pub adaptation_sensitivity: A,
94
95    /// Use ensemble voting for conflicting signals
96    pub use_ensemble_voting: bool,
97
98    /// Wall-clock budget for a single optimizer step, when the deployment has
99    /// one. Without it there is nothing to measure time pressure against, so
100    /// the resource signal reports no time term rather than inventing one.
101    pub step_time_budget: Option<Duration>,
102
103    /// Memory budget in MB, when the deployment has one.
104    pub memory_budget_mb: Option<f64>,
105}
106
107/// Multi-signal adaptation strategy
108#[derive(Debug, Clone)]
109pub struct MultiSignalAdaptationStrategy<A: Float + Send + Sync> {
110    /// Weighted voting system for adaptation signals
111    pub(crate) signal_weights: HashMap<AdaptationSignalType, A>,
112
113    /// Signal voting history
114    pub(crate) voting_history: VecDeque<SignalVote<A>>,
115
116    /// Conflict resolution method
117    pub(crate) conflict_resolution: ConflictResolution,
118
119    /// Signal reliability scores
120    pub(crate) signal_reliability: HashMap<AdaptationSignalType, A>,
121
122    /// Last adaptation decision
123    pub(crate) last_decision: Option<AdaptationDecision<A>>,
124}
125
126/// Types of adaptation signals
127#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
128pub enum AdaptationSignalType {
129    GradientMagnitude,
130    GradientVariance,
131    LossProgression,
132    AccuracyTrend,
133    ConceptDrift,
134    ResourceUtilization,
135    ModelComplexity,
136    DataQuality,
137}
138
139/// Signal vote for learning rate adaptation
140#[derive(Debug, Clone)]
141pub struct SignalVote<A: Float + Send + Sync> {
142    signal_type: AdaptationSignalType,
143    recommended_lr_change: A, // Multiplier (1.0 = no change)
144    confidence: A,
145    reasoning: String,
146    timestamp: Instant,
147}
148
149impl<A: Float + Send + Sync> SignalVote<A> {
150    /// Which signal cast this vote.
151    pub fn signal_type(&self) -> AdaptationSignalType {
152        self.signal_type
153    }
154
155    /// Multiplier this signal recommends applying to the learning rate.
156    pub fn recommended_lr_change(&self) -> A {
157        self.recommended_lr_change
158    }
159
160    /// Confidence the signal attaches to its recommendation.
161    pub fn confidence(&self) -> A {
162        self.confidence
163    }
164
165    /// Human-readable justification the signal produced for this vote.
166    ///
167    /// Each adapter builds this string from the statistics it actually
168    /// measured; it had no reader outside the tests, so callers had no way to
169    /// see *why* a learning-rate change was proposed.
170    pub fn reasoning(&self) -> &str {
171        &self.reasoning
172    }
173
174    /// When the vote was cast.
175    pub fn timestamp(&self) -> Instant {
176        self.timestamp
177    }
178}
179
180/// Conflict resolution methods for contradictory signals
181#[derive(Debug, Clone, Copy)]
182pub enum ConflictResolution {
183    /// Use weighted average of all signals
184    WeightedAverage,
185    /// Use signal with highest confidence
186    HighestConfidence,
187    /// Use majority vote (requires threshold)
188    MajorityVote { threshold: f64 },
189    /// Use conservative approach (smallest change)
190    Conservative,
191    /// Use meta-learning to resolve conflicts
192    MetaLearned,
193}
194
195/// Adaptation decision with rationale
196#[derive(Debug, Clone)]
197pub struct AdaptationDecision<A: Float + Send + Sync> {
198    new_lr: A,
199    lr_multiplier: A,
200    contributing_signals: Vec<AdaptationSignalType>,
201    confidence: A,
202    rationale: String,
203    timestamp: Instant,
204}
205
206impl<A: Float + Send + Sync> AdaptationDecision<A> {
207    /// Learning rate this decision settled on.
208    pub fn new_lr(&self) -> A {
209        self.new_lr
210    }
211
212    /// Multiplier applied to the previous learning rate.
213    pub fn lr_multiplier(&self) -> A {
214        self.lr_multiplier
215    }
216
217    /// Signals that contributed to the decision.
218    pub fn contributing_signals(&self) -> &[AdaptationSignalType] {
219        &self.contributing_signals
220    }
221
222    /// Confidence in the decision, aggregated across contributing signals.
223    pub fn confidence(&self) -> A {
224        self.confidence
225    }
226
227    /// Human-readable explanation of how the contributing signals were
228    /// reconciled, built by the configured conflict-resolution rule.
229    pub fn rationale(&self) -> &str {
230        &self.rationale
231    }
232
233    /// When the decision was taken.
234    pub fn timestamp(&self) -> Instant {
235        self.timestamp
236    }
237}
238
239/// Gradient-based adaptation using statistical analysis
240#[derive(Debug, Clone)]
241pub struct GradientBasedAdapter<A: Float + Send + Sync> {
242    /// Gradient magnitude history
243    magnitude_history: VecDeque<A>,
244
245    /// Gradient direction variance
246    direction_variance_history: VecDeque<A>,
247
248    /// Gradient norm statistics
249    norm_statistics: GradientNormStatistics<A>,
250
251    /// Signal-to-noise ratio estimation
252    snr_estimator: SignalToNoiseEstimator<A>,
253
254    /// Gradient staleness detection
255    staleness_detector: GradientStalenessDetector,
256}
257
258/// Performance-based adaptation using multiple metrics
259#[derive(Debug, Clone)]
260pub struct PerformanceBasedAdapter<A: Float + Send + Sync> {
261    /// Performance metric history
262    metric_history: HashMap<String, VecDeque<A>>,
263
264    /// Performance trend analysis
265    trend_analyzer: PerformanceTrendAnalyzer<A>,
266
267    /// Plateau detection
268    plateau_detector: PlateauDetector<A>,
269
270    /// Overfitting detection
271    overfitting_detector: OverfittingDetector<A>,
272
273    /// Learning efficiency tracker
274    efficiency_tracker: LearningEfficiencyTracker<A>,
275}
276
277/// Drift-aware adaptation for non-stationary data
278#[derive(Debug, Clone)]
279pub struct DriftAwareAdapter<A: Float + Send + Sync> {
280    /// Concept drift detection methods
281    drift_detectors: Vec<ConceptDriftDetector<A>>,
282
283    /// Data distribution shift detection
284    distribution_tracker: DistributionTracker<A>,
285
286    /// Adaptation speed controller
287    adaptation_speed: AdaptationSpeedController<A>,
288
289    /// Drift severity assessment
290    drift_severity: DriftSeverityAssessor<A>,
291}
292
293/// Resource-aware adaptation based on computational constraints
294#[derive(Debug, Clone)]
295pub struct ResourceAwareAdapter<A: Float + Send + Sync> {
296    /// Memory usage tracker
297    memory_tracker: MemoryUsageTracker,
298
299    /// Computation time tracker
300    compute_tracker: ComputationTimeTracker,
301
302    /// Energy consumption tracker
303    energy_tracker: EnergyConsumptionTracker,
304
305    /// Throughput requirements
306    throughput_requirements: ThroughputRequirements<A>,
307
308    /// Resource budget manager
309    budget_manager: ResourceBudgetManager<A>,
310}
311
312/// Meta-learning optimizer for hyperparameter adaptation
313#[derive(Debug, Clone)]
314pub struct MetaOptimizer<A: Float + Send + Sync> {
315    /// Hyperparameter optimization history
316    optimization_history: VecDeque<HyperparameterUpdate<A>>,
317
318    /// Multi-armed bandit for exploration
319    exploration_strategy: ExplorationStrategy<A>,
320
321    /// Transfer learning from similar tasks
322    transfer_learner: TransferLearner<A>,
323}
324
325/// Adaptation event for tracking and analysis
326#[derive(Debug, Clone)]
327pub struct AdaptationEvent<A: Float + Send + Sync> {
328    timestamp: Instant,
329    old_lr: A,
330    new_lr: A,
331    trigger_signals: Vec<AdaptationSignalType>,
332    effectiveness_score: Option<A>, // Measured retrospectively
333}
334
335/// Gradient norm statistics for adaptation
336#[derive(Debug, Clone)]
337pub struct GradientNormStatistics<A: Float + Send + Sync> {
338    mean: A,
339    variance: A,
340    skewness: A,
341    kurtosis: A,
342    percentiles: Vec<A>, // 5th, 25th, 50th, 75th, 95th
343    autocorrelation: A,
344}
345
346/// Signal-to-noise ratio estimation for gradients
347#[derive(Debug, Clone)]
348pub struct SignalToNoiseEstimator<A: Float + Send + Sync> {
349    signal_estimate: A,
350    noise_estimate: A,
351    snr_history: VecDeque<A>,
352}
353
354#[derive(Debug, Clone, Copy)]
355pub enum SNREstimationMethod {
356    MovingAverage,
357    ExponentialSmoothing,
358    RobustEstimation,
359    WaveletDenoising,
360}
361
362/// Gradient staleness detection for distributed settings
363#[derive(Debug, Clone, Default)]
364pub struct GradientStalenessDetector {
365    gradient_timestamps: VecDeque<Instant>,
366}
367
368/// Performance trend analysis for learning rate adaptation
369#[derive(Debug, Clone)]
370pub struct PerformanceTrendAnalyzer<A: Float + Send + Sync> {
371    trend_detection_window: usize,
372    trend_types: Vec<TrendType>,
373    trend_strength: A,
374}
375
376#[derive(Debug, Clone, Copy)]
377pub enum TrendType {
378    Improving,
379    Degrading,
380    Oscillating,
381    Plateau,
382    Volatile,
383}
384
385/// Plateau detection in learning curves
386#[derive(Debug, Clone)]
387pub struct PlateauDetector<A: Float + Send + Sync> {
388    plateau_threshold: A,
389    min_plateau_duration: usize,
390    current_plateau_length: usize,
391    plateau_confidence: A,
392}
393
394/// Overfitting detection mechanism
395#[derive(Debug, Clone)]
396pub struct OverfittingDetector<A: Float + Send + Sync> {
397    train_loss_history: VecDeque<A>,
398    val_loss_history: VecDeque<A>,
399}
400
401/// Learning efficiency tracking
402#[derive(Debug, Clone)]
403pub struct LearningEfficiencyTracker<A: Float + Send + Sync> {
404    loss_reduction_per_step: VecDeque<A>,
405    efficiency_score: A,
406    efficiency_trend: TrendType,
407}
408
409/// Concept drift detection over the loss/gradient stream.
410///
411/// E3: this struct had no methods at all and `DriftAwareAdapter::drift_detectors`
412/// was `vec![]`, so `generate_signal` returned a hardcoded "No drift detected"
413/// vote on every call. It now delegates to the real detectors implemented in
414/// [`crate::streaming::concept_drift`] rather than reimplementing them.
415#[derive(Debug, Clone)]
416pub struct ConceptDriftDetector<A: Float + Send + Sync> {
417    pub(crate) detection_method: DriftDetectionMethod,
418    pub(crate) drift_threshold: A,
419    pub(crate) window_size: usize,
420    pub(crate) drift_confidence: A,
421    pub(crate) last_drift_time: Option<Instant>,
422    pub(crate) inner: LossDriftDetector<A>,
423}
424
425#[derive(Debug, Clone, Copy)]
426pub enum DriftDetectionMethod {
427    ADWIN,
428    DDM,
429    EDDM,
430    PageHinkley,
431    KSWIN,
432    Statistical,
433}
434
435/// Data distribution tracking
436#[derive(Debug, Clone)]
437pub struct DistributionTracker<A: Float + Send + Sync> {
438    feature_distributions: HashMap<usize, FeatureDistribution<A>>,
439    distribution_drift_score: A,
440}
441
442#[derive(Debug, Clone)]
443pub struct FeatureDistribution<A: Float + Send + Sync> {
444    mean: A,
445    variance: A,
446    histogram: Vec<A>,
447    last_update: Instant,
448}
449
450/// Adaptation speed controller for drift response
451#[derive(Debug, Clone)]
452pub struct AdaptationSpeedController<A: Float + Send + Sync> {
453    base_adaptation_rate: A,
454    current_adaptation_rate: A,
455    acceleration_factor: A,
456    deceleration_factor: A,
457    momentum: A,
458}
459
460/// Drift severity assessment
461#[derive(Debug, Clone)]
462pub struct DriftSeverityAssessor<A: Float + Send + Sync> {
463    severity_levels: Vec<DriftSeverityLevel<A>>,
464    current_severity: DriftSeverityLevel<A>,
465    severity_history: VecDeque<DriftSeverityLevel<A>>,
466}
467
468#[derive(Debug, Clone)]
469pub struct DriftSeverityLevel<A: Float + Send + Sync> {
470    level: DriftSeverity,
471    recommended_lr_adjustment: A,
472}
473
474#[derive(Debug, Clone, Copy, PartialEq)]
475pub enum DriftSeverity {
476    None,
477    Mild,
478    Moderate,
479    Severe,
480    Critical,
481}
482
483/// Resource usage tracking components
484#[derive(Debug, Clone, Default)]
485pub struct MemoryUsageTracker {
486    pub(crate) current_usage_mb: f64,
487    pub(crate) peak_usage_mb: f64,
488    pub(crate) usage_history: VecDeque<f64>,
489    /// E4: `memory_pressure` was never written to, so the resource signal
490    /// always read a 0.0 that it then interpreted as "plenty of head-room" and
491    /// pushed the learning rate *up*. It is now `None` until a real usage
492    /// figure and a budget are both available.
493    pub(crate) memory_pressure: Option<f64>,
494}
495
496#[derive(Debug, Clone, Default)]
497pub struct ComputationTimeTracker {
498    pub(crate) step_times: VecDeque<Duration>,
499    pub(crate) average_step_time: Duration,
500    pub(crate) time_budget: Option<Duration>,
501    /// `None` until both a measured step time and a configured budget exist.
502    pub(crate) time_pressure: Option<f64>,
503}
504
505/// Energy consumption tracker.
506///
507/// There is no portable, pure-Rust way to read energy draw, so every field here
508/// stays empty unless a caller feeds real measurements through
509/// [`EnhancedAdaptiveLRController::record_energy_sample`].
510#[derive(Debug, Clone, Default)]
511pub struct EnergyConsumptionTracker {
512    pub(crate) energy_per_step: VecDeque<f64>,
513    pub(crate) cumulative_energy: f64,
514    pub(crate) energy_efficiency: Option<f64>,
515}
516
517#[derive(Debug, Clone)]
518pub struct ThroughputRequirements<A: Float + Send + Sync> {
519    min_samples_per_second: A,
520    current_throughput: A,
521    throughput_deficit: A,
522}
523
524#[derive(Debug, Clone)]
525pub struct ResourceBudgetManager<A: Float + Send + Sync> {
526    memory_budget_mb: f64,
527    compute_budget_seconds: f64,
528    budget_utilization: A,
529    budget_violations: usize,
530}
531
532/// Hyperparameter update record
533#[derive(Debug, Clone)]
534pub struct HyperparameterUpdate<A: Float + Send + Sync> {
535    features: Array1<A>,
536    reward: A, // Performance improvement
537}
538
539/// Exploration strategy for hyperparameter optimization
540#[derive(Debug, Clone)]
541pub struct ExplorationStrategy<A: Float + Send + Sync> {
542    exploration_rate: A,
543    arm_rewards: HashMap<usize, A>,
544    arm_counts: HashMap<usize, usize>,
545}
546
547#[derive(Debug, Clone, Copy)]
548pub enum ExplorationStrategyType {
549    EpsilonGreedy,
550    UCB1,
551    ThompsonSampling,
552    LinUCB,
553    ContextualBandit,
554}
555
556/// Transfer learning for hyperparameter optimization
557#[derive(Debug, Clone)]
558pub struct TransferLearner<A: Float + Send + Sync> {
559    source_task_data: Vec<TaskData<A>>,
560    transfer_confidence: A,
561}
562
563#[derive(Debug, Clone)]
564pub struct TaskData<A: Float + Send + Sync> {
565    optimal_lr_sequence: Vec<A>,
566}
567
568/// Adaptation statistics for monitoring and analysis
569#[derive(Debug, Clone, Default)]
570pub struct AdaptationStatistics<A: Float + Send + Sync> {
571    /// Total number of adaptations
572    pub total_adaptations: usize,
573
574    /// Successful adaptations (led to improvement)
575    pub successful_adaptations: usize,
576
577    /// Average adaptation frequency
578    pub avg_adaptation_frequency: A,
579
580    /// Learning rate volatility
581    pub lr_volatility: A,
582
583    /// Signal reliability scores
584    pub signal_reliability_scores: HashMap<AdaptationSignalType, A>,
585
586    /// Adaptation effectiveness by signal type
587    pub signal_effectiveness: HashMap<AdaptationSignalType, A>,
588
589    /// Resource efficiency improvements
590    pub resource_efficiency_gains: A,
591
592    /// Convergence speed improvement
593    pub convergence_speed_improvement: A,
594}
595
596impl<A: Float + Default + Clone + std::iter::Sum + Send + Sync> EnhancedAdaptiveLRController<A> {
597    /// Create a new enhanced adaptive learning rate controller
598    pub fn new(config: AdaptiveLRConfig<A>) -> Result<Self> {
599        let adaptation_strategy = MultiSignalAdaptationStrategy::new(&config)?;
600        let gradient_adapter = GradientBasedAdapter::new(&config)?;
601        let performance_adapter = PerformanceBasedAdapter::new(&config)?;
602        let drift_adapter = DriftAwareAdapter::new(&config)?;
603        let resource_adapter = ResourceAwareAdapter::new(&config)?;
604        let meta_optimizer = MetaOptimizer::new(&config)?;
605
606        Ok(Self {
607            current_lr: config.base_lr,
608            base_lr: config.base_lr,
609            adaptation_strategy,
610            gradient_adapter,
611            performance_adapter,
612            drift_adapter,
613            resource_adapter,
614            meta_optimizer,
615            adaptation_history: VecDeque::with_capacity(config.history_window_size),
616            config,
617        })
618    }
619
620    /// Report the memory this optimizer's workload is currently using, so the
621    /// resource signal has a real figure to work from (E4).
622    pub fn record_memory_usage_mb(&mut self, usage_mb: f64) {
623        self.resource_adapter
624            .record_memory_usage(usage_mb, self.config.memory_budget_mb);
625    }
626
627    /// Report measured energy consumption for the most recent step.
628    pub fn record_energy_sample(&mut self, joules: f64) {
629        self.resource_adapter.record_energy(joules);
630    }
631
632    /// Report the observed sample throughput so the resource signal can compare
633    /// it against the configured requirement.
634    pub fn record_throughput(&mut self, samples_per_second: A) {
635        self.resource_adapter.record_throughput(samples_per_second);
636    }
637
638    /// Register a previously solved task so the meta-optimizer can transfer its
639    /// learning-rate schedule (it contributes nothing while none are known).
640    pub fn add_source_task(&mut self, task: TaskData<A>) {
641        self.meta_optimizer.add_source_task(task);
642    }
643
644    /// Update learning rate based on multiple adaptation signals
645    pub fn update_learning_rate(
646        &mut self,
647        gradients: &Array1<A>,
648        loss: A,
649        metrics: &HashMap<String, A>,
650        step: usize,
651    ) -> Result<A> {
652        let step_started = Instant::now();
653
654        // Honour the configured adaptation cadence: outside an adaptation step
655        // the learning rate is left exactly as it is (`adaptation_frequency`
656        // used to be ignored entirely).
657        let frequency = self.config.adaptation_frequency.max(1);
658        if !step.is_multiple_of(frequency) {
659            self.gradient_adapter.observe_only(gradients);
660            self.performance_adapter.observe_only(loss);
661            self.resource_adapter
662                .record_step_time(step_started.elapsed(), self.config.step_time_budget);
663            return Ok(self.current_lr);
664        }
665
666        // Collect adaptation signals from all components
667        let mut signals = Vec::new();
668
669        if self.config.enable_gradient_adaptation {
670            if let Ok(signal) = self.gradient_adapter.generate_signal(gradients, step) {
671                signals.push(signal);
672            }
673        }
674
675        if self.config.enable_performance_adaptation {
676            if let Ok(signal) = self
677                .performance_adapter
678                .generate_signal(loss, metrics, step)
679            {
680                signals.push(signal);
681            }
682        }
683
684        if self.config.enable_drift_adaptation {
685            if let Ok(signal) = self.drift_adapter.generate_signal(gradients, step) {
686                signals.push(signal);
687            }
688        }
689
690        if self.config.enable_resource_adaptation {
691            if let Ok(signal) = self.resource_adapter.generate_signal(step) {
692                signals.push(signal);
693            }
694        }
695
696        // Resolve conflicts and make adaptation decision. E1: the resolver used
697        // to ignore the live learning rate entirely and multiply the hardcoded
698        // literal 0.001 by the vote, so every adaptation snapped the learning
699        // rate back to ~0.001 no matter where it actually was.
700        let previous_lr = self.current_lr;
701        let decision = self.adaptation_strategy.resolve_signals(
702            signals,
703            previous_lr,
704            self.config.adaptation_sensitivity,
705            self.config.use_ensemble_voting,
706            step,
707        )?;
708
709        // Apply meta-learning if enabled
710        if self.config.enable_meta_learning {
711            let meta_adjustment = self.meta_optimizer.meta_optimize(&decision, step)?;
712            self.current_lr = self.apply_meta_adjustment(decision.new_lr, meta_adjustment);
713        } else {
714            self.current_lr = decision.new_lr;
715        }
716
717        // Ensure learning rate is within bounds
718        self.current_lr = self
719            .current_lr
720            .clamp(self.config.min_lr, self.config.max_lr);
721
722        // Record adaptation event
723        let event = AdaptationEvent {
724            timestamp: Instant::now(),
725            // The learning rate *before* this adaptation; storing the decision's
726            // own output here made every recorded event look like a no-op.
727            old_lr: previous_lr,
728            new_lr: self.current_lr,
729            trigger_signals: decision.contributing_signals,
730            effectiveness_score: None, // Will be updated later
731        };
732
733        self.adaptation_history.push_back(event);
734        if self.adaptation_history.len() > self.config.history_window_size {
735            self.adaptation_history.pop_front();
736        }
737
738        self.resource_adapter
739            .record_step_time(step_started.elapsed(), self.config.step_time_budget);
740
741        Ok(self.current_lr)
742    }
743
744    /// Learning rate before the most recent adaptation, if there was one.
745    pub fn previous_lr(&self) -> Option<A> {
746        self.adaptation_history.back().map(|event| event.old_lr)
747    }
748
749    /// The most recent adaptation decision, including its rationale.
750    pub fn last_decision(&self) -> Option<&AdaptationDecision<A>> {
751        self.adaptation_strategy.last_decision.as_ref()
752    }
753
754    /// Recorded signal votes, newest last.
755    pub fn voting_history(&self) -> &VecDeque<SignalVote<A>> {
756        &self.adaptation_strategy.voting_history
757    }
758
759    /// Get current learning rate
760    pub fn get_current_lr(&self) -> A {
761        self.current_lr
762    }
763
764    /// Get adaptation statistics
765    pub fn get_adaptation_statistics(&self) -> AdaptationStatistics<A> {
766        let total_adaptations = self.adaptation_history.len();
767        let successful_adaptations = self
768            .adaptation_history
769            .iter()
770            .filter(|event| {
771                event
772                    .effectiveness_score
773                    .is_some_and(|score| score > A::zero())
774            })
775            .count();
776
777        let lr_volatility = if !self.adaptation_history.is_empty() {
778            let lr_values: Vec<A> = self
779                .adaptation_history
780                .iter()
781                .map(|event| event.new_lr)
782                .collect();
783
784            let mean_lr = lr_values.iter().fold(A::zero(), |acc, &lr| acc + lr)
785                / scalar_or(lr_values.len(), A::one());
786
787            let variance = lr_values
788                .iter()
789                .map(|&lr| {
790                    let diff = lr - mean_lr;
791                    diff * diff
792                })
793                .fold(A::zero(), |acc, var| acc + var)
794                / scalar_or(lr_values.len(), A::one());
795
796            variance.sqrt()
797        } else {
798            A::zero()
799        };
800
801        // Real per-signal reliability and effectiveness rather than the empty
802        // maps `..Default::default()` used to leave behind.
803        let signal_reliability_scores = self.adaptation_strategy.signal_reliability.clone();
804        let mut signal_effectiveness: HashMap<AdaptationSignalType, A> = HashMap::new();
805        let mut signal_counts: HashMap<AdaptationSignalType, usize> = HashMap::new();
806        for event in &self.adaptation_history {
807            let Some(score) = event.effectiveness_score else {
808                continue;
809            };
810            for signal_type in &event.trigger_signals {
811                let count = signal_counts.entry(*signal_type).or_insert(0);
812                *count += 1;
813                let steps = A::from(*count).unwrap_or_else(A::one);
814                let entry = signal_effectiveness
815                    .entry(*signal_type)
816                    .or_insert_with(A::zero);
817                *entry = *entry + (score - *entry) / steps;
818            }
819        }
820
821        let avg_adaptation_frequency = if let (Some(first), Some(last)) = (
822            self.adaptation_history.front(),
823            self.adaptation_history.back(),
824        ) {
825            let span = last.timestamp.saturating_duration_since(first.timestamp);
826            if span.as_secs_f64() > 0.0 {
827                A::from(total_adaptations as f64 / span.as_secs_f64()).unwrap_or_else(A::zero)
828            } else {
829                A::zero()
830            }
831        } else {
832            A::zero()
833        };
834
835        let convergence_speed_improvement = {
836            let scores: Vec<A> = self
837                .adaptation_history
838                .iter()
839                .filter_map(|event| event.effectiveness_score)
840                .collect();
841            if scores.is_empty() {
842                A::zero()
843            } else {
844                let count = A::from(scores.len()).unwrap_or_else(A::one);
845                scores.iter().fold(A::zero(), |acc, score| acc + *score) / count
846            }
847        };
848
849        AdaptationStatistics {
850            total_adaptations,
851            successful_adaptations,
852            avg_adaptation_frequency,
853            lr_volatility,
854            signal_reliability_scores,
855            signal_effectiveness,
856            resource_efficiency_gains: A::from(self.resource_adapter.budget_violations() as f64)
857                .map(|violations| A::zero() - violations)
858                .unwrap_or_else(A::zero),
859            convergence_speed_improvement,
860        }
861    }
862
863    /// Apply meta-learning adjustment to base decision
864    fn apply_meta_adjustment(&self, base_lr: A, meta_adjustment: A) -> A {
865        // Combine base decision with meta-learning recommendation
866        let alpha = scalar_or(0.7, A::zero()); // Weight for base decision
867        let beta = scalar_or(0.3, A::zero()); // Weight for meta-learning
868
869        alpha * base_lr + beta * meta_adjustment
870    }
871
872    /// Evaluate adaptation effectiveness retrospectively
873    pub fn evaluate_adaptation_effectiveness(&mut self, performance_improvement: A) {
874        let mut signals = Vec::new();
875        if let Some(last_event) = self.adaptation_history.back_mut() {
876            last_event.effectiveness_score = Some(performance_improvement);
877            signals = last_event.trigger_signals.clone();
878        }
879        // Update signal reliability based on effectiveness
880        for signal_type in signals {
881            self.adaptation_strategy
882                .update_signal_reliability(signal_type, performance_improvement);
883        }
884        // E2: the meta-optimizer's bandit learns from the same measurement, so
885        // its arm values come from observed effectiveness rather than a constant.
886        self.meta_optimizer.record_reward(performance_improvement);
887    }
888
889    /// Measured mean reward per meta-learning arm.
890    pub fn meta_arm_rewards(&self) -> &HashMap<usize, A> {
891        self.meta_optimizer.arm_rewards()
892    }
893
894    /// Times each meta-learning arm has been played.
895    pub fn meta_arm_counts(&self) -> &HashMap<usize, usize> {
896        self.meta_optimizer.arm_counts()
897    }
898
899    /// Measured reliability of each adaptation signal.
900    pub fn signal_reliability(&self) -> &HashMap<AdaptationSignalType, A> {
901        &self.adaptation_strategy.signal_reliability
902    }
903
904    /// Reset controller state
905    pub fn reset(&mut self) {
906        self.current_lr = self.base_lr;
907        self.adaptation_history.clear();
908        self.gradient_adapter.reset();
909        self.performance_adapter.reset();
910        self.drift_adapter.reset();
911        self.resource_adapter.reset();
912        self.meta_optimizer.reset();
913    }
914}
915
916mod meta;
917mod signals;
918
919#[cfg(test)]
920mod adaptive_lr_tests;
921
922pub(crate) use signals::LossDriftDetector;
923
924// Default implementations for various structures
925impl<A: Float + Default + Send + Sync + Send + Sync> Default for GradientNormStatistics<A> {
926    fn default() -> Self {
927        Self {
928            mean: A::default(),
929            variance: A::default(),
930            skewness: A::default(),
931            kurtosis: A::default(),
932            percentiles: vec![A::default(); 5],
933            autocorrelation: A::default(),
934        }
935    }
936}
937
938impl<A: Float + Default + Send + Sync + Send + Sync> Default for SignalToNoiseEstimator<A> {
939    fn default() -> Self {
940        Self {
941            signal_estimate: A::default(),
942            noise_estimate: A::default(),
943            snr_history: VecDeque::new(),
944        }
945    }
946}
947
948impl<A: Float + Default + Send + Sync + Send + Sync> Default for PerformanceTrendAnalyzer<A> {
949    fn default() -> Self {
950        Self {
951            trend_detection_window: 10,
952            trend_types: vec![],
953            trend_strength: A::default(),
954        }
955    }
956}
957
958impl<A: Float + Default + Send + Sync + Send + Sync> Default for PlateauDetector<A> {
959    fn default() -> Self {
960        Self {
961            plateau_threshold: A::default(),
962            min_plateau_duration: 5,
963            current_plateau_length: 0,
964            plateau_confidence: A::default(),
965        }
966    }
967}
968
969impl<A: Float + Default + Send + Sync + Send + Sync> Default for OverfittingDetector<A> {
970    fn default() -> Self {
971        Self {
972            train_loss_history: VecDeque::new(),
973            val_loss_history: VecDeque::new(),
974        }
975    }
976}
977
978impl<A: Float + Default + Send + Sync + Send + Sync> Default for LearningEfficiencyTracker<A> {
979    fn default() -> Self {
980        Self {
981            loss_reduction_per_step: VecDeque::new(),
982            efficiency_score: A::default(),
983            efficiency_trend: TrendType::Improving,
984        }
985    }
986}
987
988impl<A: Float + Default + Send + Sync + Send + Sync> Default for DistributionTracker<A> {
989    fn default() -> Self {
990        Self {
991            feature_distributions: HashMap::new(),
992            distribution_drift_score: A::default(),
993        }
994    }
995}
996
997impl<A: Float + Default + Send + Sync + Send + Sync> Default for AdaptationSpeedController<A> {
998    fn default() -> Self {
999        Self {
1000            base_adaptation_rate: A::from(0.1).unwrap_or_default(),
1001            current_adaptation_rate: A::from(0.1).unwrap_or_default(),
1002            acceleration_factor: A::from(1.1).unwrap_or_default(),
1003            deceleration_factor: A::from(0.9).unwrap_or_default(),
1004            momentum: A::default(),
1005        }
1006    }
1007}
1008
1009impl<A: Float + Default + Send + Sync + Send + Sync> Default for DriftSeverityAssessor<A> {
1010    fn default() -> Self {
1011        Self {
1012            severity_levels: vec![],
1013            current_severity: DriftSeverityLevel::default(),
1014            severity_history: VecDeque::new(),
1015        }
1016    }
1017}
1018
1019impl<A: Float + Default + Send + Sync + Send + Sync> Default for DriftSeverityLevel<A> {
1020    fn default() -> Self {
1021        Self {
1022            level: DriftSeverity::None,
1023            recommended_lr_adjustment: A::one(),
1024        }
1025    }
1026}
1027
1028impl<A: Float + Default + Send + Sync + Send + Sync> Default for ExplorationStrategy<A> {
1029    fn default() -> Self {
1030        Self {
1031            exploration_rate: A::from(0.1).unwrap_or_default(),
1032            arm_rewards: HashMap::new(),
1033            arm_counts: HashMap::new(),
1034        }
1035    }
1036}
1037
1038impl<A: Float + Default + Send + Sync + Send + Sync> Default for TransferLearner<A> {
1039    fn default() -> Self {
1040        Self {
1041            source_task_data: vec![],
1042            transfer_confidence: A::default(),
1043        }
1044    }
1045}
1046
1047#[cfg(test)]
1048mod tests {
1049    use super::*;
1050    use scirs2_core::ndarray::Array1;
1051
1052    #[test]
1053    fn test_enhanced_adaptive_lr_controller_creation() {
1054        let config = AdaptiveLRConfig {
1055            base_lr: 0.01,
1056            min_lr: 1e-6,
1057            max_lr: 1.0,
1058            enable_gradient_adaptation: true,
1059            enable_performance_adaptation: true,
1060            enable_drift_adaptation: false,
1061            enable_resource_adaptation: false,
1062            enable_meta_learning: false,
1063            history_window_size: 100,
1064            adaptation_frequency: 10,
1065            adaptation_sensitivity: 0.1,
1066            use_ensemble_voting: true,
1067            step_time_budget: None,
1068            memory_budget_mb: None,
1069        };
1070
1071        let controller = EnhancedAdaptiveLRController::<f32>::new(config);
1072        assert!(controller.is_ok());
1073    }
1074
1075    #[test]
1076    fn test_learning_rate_update() {
1077        let config = AdaptiveLRConfig {
1078            base_lr: 0.01,
1079            min_lr: 1e-6,
1080            max_lr: 1.0,
1081            enable_gradient_adaptation: true,
1082            enable_performance_adaptation: true,
1083            enable_drift_adaptation: false,
1084            enable_resource_adaptation: false,
1085            enable_meta_learning: false,
1086            history_window_size: 100,
1087            adaptation_frequency: 10,
1088            adaptation_sensitivity: 0.1,
1089            use_ensemble_voting: true,
1090            step_time_budget: None,
1091            memory_budget_mb: None,
1092        };
1093
1094        let mut controller =
1095            EnhancedAdaptiveLRController::<f32>::new(config).expect("unwrap failed");
1096        let gradients = Array1::from_vec(vec![0.1, 0.2, 0.05]);
1097        let loss = 0.5;
1098        let metrics = HashMap::new();
1099
1100        let new_lr = controller.update_learning_rate(&gradients, loss, &metrics, 1);
1101        assert!(new_lr.is_ok());
1102        assert!(new_lr.expect("unwrap failed") > 0.0);
1103    }
1104
1105    #[test]
1106    fn test_adaptation_statistics() {
1107        let config = AdaptiveLRConfig {
1108            base_lr: 0.01,
1109            min_lr: 1e-6,
1110            max_lr: 1.0,
1111            enable_gradient_adaptation: true,
1112            enable_performance_adaptation: true,
1113            enable_drift_adaptation: false,
1114            enable_resource_adaptation: false,
1115            enable_meta_learning: false,
1116            history_window_size: 100,
1117            adaptation_frequency: 10,
1118            adaptation_sensitivity: 0.1,
1119            use_ensemble_voting: true,
1120            step_time_budget: None,
1121            memory_budget_mb: None,
1122        };
1123
1124        let controller = EnhancedAdaptiveLRController::<f32>::new(config).expect("unwrap failed");
1125        let stats = controller.get_adaptation_statistics();
1126
1127        assert_eq!(stats.total_adaptations, 0);
1128        assert_eq!(stats.successful_adaptations, 0);
1129    }
1130}