1use super::*;
22
23const MAX_VOTING_HISTORY: usize = 512;
25
26const MAX_METRIC_HISTORY: usize = 512;
28
29fn to_scalar<A: Float>(value: f64) -> A {
30 A::from(value).unwrap_or_else(A::zero)
31}
32
33fn from_scalar<A: Float>(value: A) -> f64 {
34 value.to_f64().unwrap_or(0.0)
35}
36
37fn mean(values: &[f64]) -> f64 {
38 if values.is_empty() {
39 0.0
40 } else {
41 values.iter().sum::<f64>() / values.len() as f64
42 }
43}
44
45fn variance(values: &[f64]) -> f64 {
46 if values.len() < 2 {
47 return 0.0;
48 }
49 let m = mean(values);
50 values.iter().map(|value| (value - m).powi(2)).sum::<f64>() / values.len() as f64
51}
52
53fn slope(values: &[f64]) -> f64 {
55 if values.len() < 2 {
56 return 0.0;
57 }
58 let x_mean = (values.len() - 1) as f64 / 2.0;
59 let y_mean = mean(values);
60 let mut numerator = 0.0;
61 let mut denominator = 0.0;
62 for (index, value) in values.iter().enumerate() {
63 let dx = index as f64 - x_mean;
64 numerator += dx * (value - y_mean);
65 denominator += dx * dx;
66 }
67 if denominator == 0.0 {
68 0.0
69 } else {
70 numerator / denominator
71 }
72}
73
74impl<A: Float + Default + Clone + Send + Sync> MultiSignalAdaptationStrategy<A> {
79 pub(crate) fn new(config: &AdaptiveLRConfig<A>) -> Result<Self> {
80 let mut signal_weights = HashMap::new();
84 let enabled_weight = A::one();
85 let disabled_weight = A::zero();
86 signal_weights.insert(
87 AdaptationSignalType::GradientMagnitude,
88 if config.enable_gradient_adaptation {
89 enabled_weight
90 } else {
91 disabled_weight
92 },
93 );
94 signal_weights.insert(
95 AdaptationSignalType::GradientVariance,
96 if config.enable_gradient_adaptation {
97 enabled_weight
98 } else {
99 disabled_weight
100 },
101 );
102 signal_weights.insert(
103 AdaptationSignalType::LossProgression,
104 if config.enable_performance_adaptation {
105 enabled_weight
106 } else {
107 disabled_weight
108 },
109 );
110 signal_weights.insert(
111 AdaptationSignalType::AccuracyTrend,
112 if config.enable_performance_adaptation {
113 enabled_weight
114 } else {
115 disabled_weight
116 },
117 );
118 signal_weights.insert(
119 AdaptationSignalType::ConceptDrift,
120 if config.enable_drift_adaptation {
121 enabled_weight
122 } else {
123 disabled_weight
124 },
125 );
126 signal_weights.insert(
127 AdaptationSignalType::ResourceUtilization,
128 if config.enable_resource_adaptation {
129 enabled_weight
130 } else {
131 disabled_weight
132 },
133 );
134 signal_weights.insert(AdaptationSignalType::ModelComplexity, enabled_weight);
135 signal_weights.insert(AdaptationSignalType::DataQuality, enabled_weight);
136
137 Ok(Self {
138 signal_weights,
139 voting_history: VecDeque::with_capacity(MAX_VOTING_HISTORY),
140 conflict_resolution: if config.use_ensemble_voting {
141 ConflictResolution::WeightedAverage
142 } else {
143 ConflictResolution::HighestConfidence
144 },
145 signal_reliability: HashMap::new(),
146 last_decision: None,
147 })
148 }
149
150 fn effective_weight(&self, signal_type: AdaptationSignalType) -> f64 {
153 let configured = self
154 .signal_weights
155 .get(&signal_type)
156 .map(|weight| from_scalar(*weight))
157 .unwrap_or(1.0);
158 let reliability = self
159 .signal_reliability
160 .get(&signal_type)
161 .map(|value| from_scalar(*value))
162 .unwrap_or(1.0);
163 (configured * reliability).max(0.0)
164 }
165
166 pub(crate) fn resolve_signals(
167 &mut self,
168 signals: Vec<SignalVote<A>>,
169 current_lr: A,
170 sensitivity: A,
171 use_ensemble_voting: bool,
172 _step: usize,
173 ) -> Result<AdaptationDecision<A>> {
174 for signal in &signals {
176 self.voting_history.push_back(signal.clone());
177 }
178 while self.voting_history.len() > MAX_VOTING_HISTORY {
179 self.voting_history.pop_front();
180 }
181
182 if signals.is_empty() {
183 let decision = AdaptationDecision {
184 new_lr: current_lr,
187 lr_multiplier: A::one(),
188 contributing_signals: vec![],
189 confidence: A::zero(),
190 rationale: "No signals available; learning rate left unchanged".to_string(),
191 timestamp: Instant::now(),
192 };
193 self.last_decision = Some(decision.clone());
194 return Ok(decision);
195 }
196
197 self.conflict_resolution = if use_ensemble_voting {
198 self.conflict_resolution
199 } else {
200 ConflictResolution::HighestConfidence
201 };
202
203 let candidates: Vec<(f64, f64, f64, AdaptationSignalType)> = signals
205 .iter()
206 .map(|signal| {
207 (
208 from_scalar(signal.recommended_lr_change),
209 self.effective_weight(signal.signal_type) * from_scalar(signal.confidence),
210 from_scalar(signal.confidence),
211 signal.signal_type,
212 )
213 })
214 .filter(|(multiplier, weight, _, _)| multiplier.is_finite() && *weight > 0.0)
215 .collect();
216
217 if candidates.is_empty() {
218 let decision = AdaptationDecision {
219 new_lr: current_lr,
220 lr_multiplier: A::one(),
221 contributing_signals: signals.iter().map(|s| s.signal_type).collect(),
222 confidence: A::zero(),
223 rationale: "All adaptation signals are disabled or unreliable".to_string(),
224 timestamp: Instant::now(),
225 };
226 self.last_decision = Some(decision.clone());
227 return Ok(decision);
228 }
229
230 let (raw_multiplier, rationale) = match self.conflict_resolution {
232 ConflictResolution::WeightedAverage => {
233 let total_weight: f64 = candidates.iter().map(|(_, weight, _, _)| weight).sum();
234 let weighted: f64 = candidates
235 .iter()
236 .map(|(multiplier, weight, _, _)| multiplier * weight)
237 .sum();
238 (
239 weighted / total_weight,
240 "Reliability-weighted average of adaptation signals".to_string(),
241 )
242 }
243 ConflictResolution::HighestConfidence => {
244 let best = candidates
245 .iter()
246 .max_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal))
247 .copied()
248 .unwrap_or((1.0, 1.0, 0.0, AdaptationSignalType::LossProgression));
249 (best.0, format!("Highest-confidence signal ({:?})", best.3))
250 }
251 ConflictResolution::MajorityVote { threshold } => {
252 let total_weight: f64 = candidates.iter().map(|(_, weight, _, _)| weight).sum();
255 let up: f64 = candidates
256 .iter()
257 .filter(|(multiplier, _, _, _)| *multiplier > 1.0)
258 .map(|(_, weight, _, _)| weight)
259 .sum();
260 let down: f64 = candidates
261 .iter()
262 .filter(|(multiplier, _, _, _)| *multiplier < 1.0)
263 .map(|(_, weight, _, _)| weight)
264 .sum();
265 let share = |value: f64| {
266 if total_weight > 0.0 {
267 value / total_weight
268 } else {
269 0.0
270 }
271 };
272 if share(up) >= threshold {
273 let mean_up = mean(
274 &candidates
275 .iter()
276 .filter(|(multiplier, _, _, _)| *multiplier > 1.0)
277 .map(|(multiplier, _, _, _)| *multiplier)
278 .collect::<Vec<f64>>(),
279 );
280 (mean_up, "Majority voted to increase".to_string())
281 } else if share(down) >= threshold {
282 let mean_down = mean(
283 &candidates
284 .iter()
285 .filter(|(multiplier, _, _, _)| *multiplier < 1.0)
286 .map(|(multiplier, _, _, _)| *multiplier)
287 .collect::<Vec<f64>>(),
288 );
289 (mean_down, "Majority voted to decrease".to_string())
290 } else {
291 (
292 1.0,
293 "No direction reached the majority threshold".to_string(),
294 )
295 }
296 }
297 ConflictResolution::Conservative => {
298 let smallest = candidates
300 .iter()
301 .min_by(|a, b| {
302 (a.0 - 1.0)
303 .abs()
304 .partial_cmp(&(b.0 - 1.0).abs())
305 .unwrap_or(std::cmp::Ordering::Equal)
306 })
307 .map(|(multiplier, _, _, _)| *multiplier)
308 .unwrap_or(1.0);
309 (
310 smallest,
311 "Conservative resolution: smallest requested change".to_string(),
312 )
313 }
314 ConflictResolution::MetaLearned => {
315 let mut total = 0.0;
319 let mut weighted = 0.0;
320 for (multiplier, _, _, signal_type) in &candidates {
321 let reliability = self
322 .signal_reliability
323 .get(signal_type)
324 .map(|value| from_scalar(*value))
325 .unwrap_or(0.0);
326 total += reliability;
327 weighted += multiplier * reliability;
328 }
329 if total > 0.0 {
330 (
331 weighted / total,
332 "Meta-learned resolution from measured signal reliability".to_string(),
333 )
334 } else {
335 (
337 1.0,
338 "Meta-learned resolution has no measured reliability yet".to_string(),
339 )
340 }
341 }
342 };
343
344 let sensitivity = from_scalar(sensitivity).abs().clamp(0.0, 1.0);
347 let scale = if sensitivity > 0.0 { sensitivity } else { 1.0 };
348 let multiplier = 1.0 + (raw_multiplier - 1.0) * scale;
349 let multiplier = if multiplier.is_finite() && multiplier > 0.0 {
350 multiplier
351 } else {
352 1.0
353 };
354
355 let confidence = mean(
356 &candidates
357 .iter()
358 .map(|(_, _, confidence, _)| *confidence)
359 .collect::<Vec<f64>>(),
360 );
361
362 let decision = AdaptationDecision {
363 new_lr: current_lr * to_scalar(multiplier),
366 lr_multiplier: to_scalar(multiplier),
367 contributing_signals: candidates
368 .iter()
369 .map(|(_, _, _, signal_type)| *signal_type)
370 .collect(),
371 confidence: to_scalar(confidence),
372 rationale,
373 timestamp: Instant::now(),
374 };
375 self.last_decision = Some(decision.clone());
376 Ok(decision)
377 }
378
379 pub(crate) fn update_signal_reliability(
380 &mut self,
381 signal_type: AdaptationSignalType,
382 effectiveness: A,
383 ) {
384 let reliability = self
385 .signal_reliability
386 .entry(signal_type)
387 .or_insert_with(|| A::from(0.5).unwrap_or_else(A::zero));
388
389 let alpha = A::from(0.1).unwrap_or_else(A::zero);
392 let updated = (*reliability) * (A::one() - alpha) + effectiveness * alpha;
393 *reliability = updated.max(A::zero());
394 }
395}
396
397impl<A: Float + Default + Clone + Send + Sync> GradientBasedAdapter<A> {
402 pub(crate) fn new(config: &AdaptiveLRConfig<A>) -> Result<Self> {
403 let window = config.history_window_size.clamp(4, MAX_METRIC_HISTORY);
405 Ok(Self {
406 magnitude_history: VecDeque::with_capacity(window),
407 direction_variance_history: VecDeque::with_capacity(window),
408 norm_statistics: GradientNormStatistics::default(),
409 snr_estimator: SignalToNoiseEstimator::default(),
410 staleness_detector: GradientStalenessDetector::default(),
411 })
412 }
413
414 pub(crate) fn observe_only(&mut self, gradients: &Array1<A>) {
416 let magnitude = gradients
417 .iter()
418 .map(|&g| g * g)
419 .fold(A::zero(), |acc, x| acc + x)
420 .sqrt();
421 self.push_magnitude(magnitude);
422 }
423
424 fn push_magnitude(&mut self, magnitude: A) {
425 self.magnitude_history.push_back(magnitude);
426 while self.magnitude_history.len() > MAX_METRIC_HISTORY {
427 self.magnitude_history.pop_front();
428 }
429 self.staleness_detector
430 .gradient_timestamps
431 .push_back(Instant::now());
432 while self.staleness_detector.gradient_timestamps.len() > MAX_METRIC_HISTORY {
433 self.staleness_detector.gradient_timestamps.pop_front();
434 }
435 self.refresh_statistics();
436 }
437
438 fn refresh_statistics(&mut self) {
442 let values: Vec<f64> = self
443 .magnitude_history
444 .iter()
445 .map(|value| from_scalar(*value))
446 .collect();
447 if values.is_empty() {
448 return;
449 }
450 let m = mean(&values);
451 let var = variance(&values);
452 let std_dev = var.sqrt();
453
454 self.norm_statistics.mean = to_scalar(m);
455 self.norm_statistics.variance = to_scalar(var);
456 if std_dev > f64::EPSILON {
457 let third = values.iter().map(|v| (v - m).powi(3)).sum::<f64>() / values.len() as f64;
458 let fourth = values.iter().map(|v| (v - m).powi(4)).sum::<f64>() / values.len() as f64;
459 self.norm_statistics.skewness = to_scalar(third / std_dev.powi(3));
460 self.norm_statistics.kurtosis = to_scalar(fourth / var.powi(2) - 3.0);
461 }
462
463 let mut sorted = values.clone();
464 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
465 let last = sorted.len() - 1;
466 self.norm_statistics.percentiles = [0.05, 0.25, 0.50, 0.75, 0.95]
467 .iter()
468 .map(|q| to_scalar(sorted[((sorted.len() as f64 * q) as usize).min(last)]))
469 .collect();
470
471 if values.len() >= 3 && var > f64::EPSILON {
472 let numerator: f64 = values
473 .windows(2)
474 .map(|pair| (pair[0] - m) * (pair[1] - m))
475 .sum();
476 let denominator: f64 = values.iter().map(|v| (v - m).powi(2)).sum();
477 self.norm_statistics.autocorrelation =
478 to_scalar((numerator / denominator).clamp(-1.0, 1.0));
479 }
480
481 self.snr_estimator.signal_estimate = to_scalar(m);
483 self.snr_estimator.noise_estimate = to_scalar(std_dev);
484 let snr = if std_dev > f64::EPSILON {
485 m / std_dev
486 } else {
487 0.0
488 };
489 self.snr_estimator.snr_history.push_back(to_scalar(snr));
490 while self.snr_estimator.snr_history.len() > MAX_METRIC_HISTORY {
491 self.snr_estimator.snr_history.pop_front();
492 }
493 }
494
495 pub(crate) fn generate_signal(
496 &mut self,
497 gradients: &Array1<A>,
498 _step: usize,
499 ) -> Result<SignalVote<A>> {
500 let magnitude = gradients
501 .iter()
502 .map(|&g| g * g)
503 .fold(A::zero(), |acc, x| acc + x)
504 .sqrt();
505 self.push_magnitude(magnitude);
506
507 let magnitude_value = from_scalar(magnitude);
508 if !magnitude_value.is_finite() {
509 return Err(crate::error::OptimError::InvalidParameter(
510 "gradient magnitude is not finite".to_string(),
511 ));
512 }
513
514 let median = self
518 .norm_statistics
519 .percentiles
520 .get(2)
521 .map(|value| from_scalar(*value))
522 .unwrap_or(magnitude_value);
523 let snr = from_scalar(self.snr_estimator.signal_estimate)
524 / from_scalar(self.snr_estimator.noise_estimate).max(f64::EPSILON);
525
526 let recommended = if median <= f64::EPSILON {
527 1.0
528 } else {
529 let ratio = magnitude_value / median;
530 (1.0 / ratio.clamp(0.5, 2.0)).clamp(0.8, 1.2)
532 };
533
534 let coverage = (self.magnitude_history.len() as f64 / 32.0).min(1.0);
537 let confidence = (0.5 * coverage + 0.5 * (snr / (1.0 + snr))).clamp(0.0, 1.0);
538
539 Ok(SignalVote {
540 signal_type: AdaptationSignalType::GradientMagnitude,
541 recommended_lr_change: to_scalar(recommended),
542 confidence: to_scalar(confidence),
543 reasoning: format!(
544 "gradient magnitude {magnitude_value:.6} vs median {median:.6}, snr {snr:.3}"
545 ),
546 timestamp: Instant::now(),
547 })
548 }
549
550 pub(crate) fn reset(&mut self) {
551 self.magnitude_history.clear();
552 self.direction_variance_history.clear();
553 self.norm_statistics = GradientNormStatistics::default();
554 self.snr_estimator = SignalToNoiseEstimator::default();
555 self.staleness_detector = GradientStalenessDetector::default();
556 }
557}
558
559impl<A: Float + Default + Clone + Send + Sync> PerformanceBasedAdapter<A> {
564 pub(crate) fn new(config: &AdaptiveLRConfig<A>) -> Result<Self> {
565 let plateau_detector = PlateauDetector {
567 plateau_threshold: config.adaptation_sensitivity,
568 min_plateau_duration: config.adaptation_frequency.max(2),
569 ..PlateauDetector::default()
570 };
571
572 let trend_analyzer = PerformanceTrendAnalyzer {
573 trend_detection_window: config.history_window_size.clamp(4, 128),
574 ..PerformanceTrendAnalyzer::default()
575 };
576
577 Ok(Self {
578 metric_history: HashMap::new(),
579 trend_analyzer,
580 plateau_detector,
581 overfitting_detector: OverfittingDetector::default(),
582 efficiency_tracker: LearningEfficiencyTracker::default(),
583 })
584 }
585
586 pub(crate) fn observe_only(&mut self, loss: A) {
588 let history = self.metric_history.entry("loss".to_string()).or_default();
589 history.push_back(loss);
590 while history.len() > MAX_METRIC_HISTORY {
591 history.pop_front();
592 }
593 }
594
595 pub(crate) fn generate_signal(
596 &mut self,
597 loss: A,
598 metrics: &HashMap<String, A>,
599 _step: usize,
600 ) -> Result<SignalVote<A>> {
601 self.observe_only(loss);
602 for (name, value) in metrics {
603 let history = self.metric_history.entry(name.clone()).or_default();
604 history.push_back(*value);
605 while history.len() > MAX_METRIC_HISTORY {
606 history.pop_front();
607 }
608 }
609
610 let losses: Vec<f64> = self
611 .metric_history
612 .get("loss")
613 .map(|history| history.iter().map(|value| from_scalar(*value)).collect())
614 .unwrap_or_default();
615
616 let window = self.trend_analyzer.trend_detection_window.max(4);
617 let recent: Vec<f64> = losses.iter().rev().take(window).rev().copied().collect();
618
619 if recent.len() < 2 {
620 return Err(crate::error::OptimError::InvalidState(
621 "not enough loss history for a performance signal".to_string(),
622 ));
623 }
624
625 let trend = slope(&recent);
626 let spread = variance(&recent).sqrt();
627 let level = mean(&recent).abs().max(f64::EPSILON);
628 let relative_trend = trend / level;
629 let relative_spread = spread / level;
630
631 let plateau_band =
636 (0.01 * from_scalar(self.plateau_detector.plateau_threshold).abs()).clamp(1e-6, 0.5);
637 if relative_trend.abs() < plateau_band {
638 self.plateau_detector.current_plateau_length += 1;
639 } else {
640 self.plateau_detector.current_plateau_length = 0;
641 }
642 let on_plateau = self.plateau_detector.current_plateau_length
643 >= self.plateau_detector.min_plateau_duration;
644 self.plateau_detector.plateau_confidence = to_scalar(
645 (self.plateau_detector.current_plateau_length as f64
646 / self.plateau_detector.min_plateau_duration.max(1) as f64)
647 .min(1.0),
648 );
649
650 self.overfitting_detector.train_loss_history.push_back(loss);
652 while self.overfitting_detector.train_loss_history.len() > MAX_METRIC_HISTORY {
653 self.overfitting_detector.train_loss_history.pop_front();
654 }
655 let mut overfitting = false;
656 if let Some(validation) = metrics.get("val_loss") {
657 self.overfitting_detector
658 .val_loss_history
659 .push_back(*validation);
660 while self.overfitting_detector.val_loss_history.len() > MAX_METRIC_HISTORY {
661 self.overfitting_detector.val_loss_history.pop_front();
662 }
663 let validation_values: Vec<f64> = self
664 .overfitting_detector
665 .val_loss_history
666 .iter()
667 .map(|value| from_scalar(*value))
668 .collect();
669 if validation_values.len() >= 4 {
670 overfitting = relative_trend < 0.0 && slope(&validation_values) > 0.0;
672 }
673 }
674
675 if recent.len() >= 2 {
677 let reduction = recent[0] - recent[recent.len() - 1];
678 self.efficiency_tracker
679 .loss_reduction_per_step
680 .push_back(to_scalar(reduction / recent.len() as f64));
681 while self.efficiency_tracker.loss_reduction_per_step.len() > MAX_METRIC_HISTORY {
682 self.efficiency_tracker.loss_reduction_per_step.pop_front();
683 }
684 self.efficiency_tracker.efficiency_score = to_scalar(mean(
685 &self
686 .efficiency_tracker
687 .loss_reduction_per_step
688 .iter()
689 .map(|value| from_scalar(*value))
690 .collect::<Vec<f64>>(),
691 ));
692 }
693
694 let trend_type = if overfitting {
695 TrendType::Degrading
696 } else if on_plateau {
697 TrendType::Plateau
698 } else if relative_spread > 0.5 {
699 TrendType::Volatile
700 } else if relative_trend < 0.0 {
701 TrendType::Improving
702 } else {
703 TrendType::Degrading
704 };
705 self.trend_analyzer.trend_types = vec![trend_type];
706 self.trend_analyzer.trend_strength = to_scalar(relative_trend.abs().min(1.0));
707 self.efficiency_tracker.efficiency_trend = trend_type;
708
709 let recommended = match trend_type {
710 TrendType::Plateau => 1.1,
712 TrendType::Degrading => 0.9,
714 TrendType::Volatile => 0.8,
716 TrendType::Improving => 1.02,
718 TrendType::Oscillating => 0.85,
719 };
720
721 let coverage = (recent.len() as f64 / window as f64).min(1.0);
722 let confidence = (0.4 + 0.6 * coverage).clamp(0.0, 1.0);
723
724 Ok(SignalVote {
725 signal_type: AdaptationSignalType::LossProgression,
726 recommended_lr_change: to_scalar(recommended),
727 confidence: to_scalar(confidence),
728 reasoning: format!(
729 "{trend_type:?}: relative trend {relative_trend:.5}, spread {relative_spread:.5}"
730 ),
731 timestamp: Instant::now(),
732 })
733 }
734
735 pub(crate) fn reset(&mut self) {
736 self.metric_history.clear();
737 self.plateau_detector.current_plateau_length = 0;
738 self.overfitting_detector.train_loss_history.clear();
739 self.overfitting_detector.val_loss_history.clear();
740 self.efficiency_tracker.loss_reduction_per_step.clear();
741 }
742}
743
744#[derive(Debug, Clone)]
751pub enum LossDriftDetector<A: Float + Send + Sync> {
752 PageHinkley(crate::streaming::concept_drift::PageHinkleyDetector<A>),
754 Adwin(crate::streaming::concept_drift::AdwinDetector<A>),
756 Ddm {
758 detector: crate::streaming::concept_drift::DdmDetector<A>,
759 running_mean: A,
760 samples: usize,
761 },
762}
763
764impl<A: Float + std::iter::Sum + Send + Sync> LossDriftDetector<A> {
765 fn for_method(method: DriftDetectionMethod, threshold: A, window: usize) -> Self {
766 let warning = threshold * A::from(0.5).unwrap_or_else(A::one);
767 match method {
768 DriftDetectionMethod::PageHinkley => LossDriftDetector::PageHinkley(
769 crate::streaming::concept_drift::PageHinkleyDetector::new(threshold, warning),
770 ),
771 DriftDetectionMethod::ADWIN | DriftDetectionMethod::KSWIN => {
772 let lower = A::from(1e-9).unwrap_or_else(A::zero);
776 let upper = A::from(0.5).unwrap_or_else(A::one);
777 let delta = if threshold > A::zero() {
778 (A::one() / threshold).clamp(lower, upper)
779 } else {
780 upper
781 };
782 LossDriftDetector::Adwin(crate::streaming::concept_drift::AdwinDetector::new(
783 delta,
784 window.max(16),
785 ))
786 }
787 DriftDetectionMethod::DDM
788 | DriftDetectionMethod::EDDM
789 | DriftDetectionMethod::Statistical => LossDriftDetector::Ddm {
790 detector: crate::streaming::concept_drift::DdmDetector::with_warmup(
791 window.clamp(8, 64),
792 ),
793 running_mean: A::zero(),
794 samples: 0,
795 },
796 }
797 }
798
799 fn update(&mut self, value: A) -> crate::streaming::concept_drift::DriftStatus {
800 match self {
801 LossDriftDetector::PageHinkley(detector) => detector.update(value),
802 LossDriftDetector::Adwin(detector) => detector.update(value),
803 LossDriftDetector::Ddm {
804 detector,
805 running_mean,
806 samples,
807 } => {
808 *samples += 1;
809 let count = A::from(*samples).unwrap_or_else(A::one);
810 *running_mean = *running_mean + (value - *running_mean) / count;
811 detector.update(value > *running_mean)
812 }
813 }
814 }
815}
816
817impl<A: Float + Default + Clone + std::iter::Sum + Send + Sync> ConceptDriftDetector<A> {
818 pub fn new(method: DriftDetectionMethod, threshold: A, window: usize) -> Self {
820 Self {
821 detection_method: method,
822 drift_threshold: threshold,
823 window_size: window,
824 drift_confidence: A::zero(),
825 last_drift_time: None,
826 inner: LossDriftDetector::for_method(method, threshold, window),
827 }
828 }
829
830 pub fn update(&mut self, value: A) -> crate::streaming::concept_drift::DriftStatus {
832 let status = self.inner.update(value);
833 self.drift_confidence = match status {
834 crate::streaming::concept_drift::DriftStatus::Drift => A::one(),
835 crate::streaming::concept_drift::DriftStatus::Warning => {
836 A::from(0.5).unwrap_or_else(A::zero)
837 }
838 crate::streaming::concept_drift::DriftStatus::Stable => A::zero(),
839 };
840 if status == crate::streaming::concept_drift::DriftStatus::Drift {
841 self.last_drift_time = Some(Instant::now());
842 }
843 status
844 }
845
846 pub fn confidence(&self) -> A {
848 self.drift_confidence
849 }
850
851 pub fn last_drift_time(&self) -> Option<Instant> {
853 self.last_drift_time
854 }
855}
856
857impl<A: Float + Default + Clone + std::iter::Sum + Send + Sync> DriftAwareAdapter<A> {
858 pub(crate) fn new(config: &AdaptiveLRConfig<A>) -> Result<Self> {
859 let threshold = A::from(3.0).unwrap_or_else(A::one);
862 let window = config.history_window_size.clamp(16, 512);
863 let drift_detectors = vec![
864 ConceptDriftDetector::new(DriftDetectionMethod::PageHinkley, threshold, window),
865 ConceptDriftDetector::new(DriftDetectionMethod::ADWIN, threshold, window),
866 ConceptDriftDetector::new(DriftDetectionMethod::DDM, threshold, window),
867 ];
868
869 let distribution_tracker = DistributionTracker {
870 ..DistributionTracker::default()
871 };
872
873 let adaptation_speed = AdaptationSpeedController {
874 base_adaptation_rate: config.adaptation_sensitivity,
875 current_adaptation_rate: config.adaptation_sensitivity,
876 ..AdaptationSpeedController::default()
877 };
878
879 Ok(Self {
880 drift_detectors,
881 distribution_tracker,
882 adaptation_speed,
883 drift_severity: DriftSeverityAssessor::default(),
884 })
885 }
886
887 pub(crate) fn generate_signal(
888 &mut self,
889 gradients: &Array1<A>,
890 _step: usize,
891 ) -> Result<SignalVote<A>> {
892 let magnitude = gradients
893 .iter()
894 .map(|&g| g * g)
895 .fold(A::zero(), |acc, x| acc + x)
896 .sqrt();
897 if !from_scalar(magnitude).is_finite() {
898 return Err(crate::error::OptimError::InvalidParameter(
899 "gradient magnitude is not finite".to_string(),
900 ));
901 }
902
903 let mut drift_votes = 0usize;
904 let mut warning_votes = 0usize;
905 for detector in self.drift_detectors.iter_mut() {
906 match detector.update(magnitude) {
907 crate::streaming::concept_drift::DriftStatus::Drift => drift_votes += 1,
908 crate::streaming::concept_drift::DriftStatus::Warning => warning_votes += 1,
909 crate::streaming::concept_drift::DriftStatus::Stable => {}
910 }
911 }
912
913 let divergence = self.distribution_tracker.observe(gradients);
916
917 let detectors = self.drift_detectors.len().max(1) as f64;
918 let agreement = (drift_votes as f64 + 0.5 * warning_votes as f64) / detectors;
919 let severity = self.drift_severity.assess(agreement, divergence);
920 let recommended = from_scalar(severity.recommended_lr_adjustment);
921 let rate = self.adaptation_speed.update(agreement);
922
923 let scaled = 1.0 + (recommended - 1.0) * rate;
926 let confidence = (0.3 + 0.7 * agreement).clamp(0.0, 1.0);
927
928 Ok(SignalVote {
929 signal_type: AdaptationSignalType::ConceptDrift,
930 recommended_lr_change: to_scalar(scaled),
931 confidence: to_scalar(confidence),
932 reasoning: format!(
933 "{:?} drift ({drift_votes}/{} detectors, KL {:.4})",
934 severity.level,
935 self.drift_detectors.len(),
936 divergence
937 ),
938 timestamp: Instant::now(),
939 })
940 }
941
942 pub(crate) fn reset(&mut self) {
943 for detector in self.drift_detectors.iter_mut() {
944 detector.drift_confidence = A::zero();
945 detector.last_drift_time = None;
946 detector.inner = LossDriftDetector::for_method(
947 detector.detection_method,
948 detector.drift_threshold,
949 detector.window_size,
950 );
951 }
952 self.distribution_tracker = DistributionTracker::default();
953 self.drift_severity = DriftSeverityAssessor::default();
954 }
955
956 pub fn drift_detected(&self) -> bool {
958 self.drift_detectors
959 .iter()
960 .any(|detector| from_scalar(detector.drift_confidence) >= 1.0)
961 }
962}
963
964const DISTRIBUTION_BINS: usize = 16;
966
967const DISTRIBUTION_Z_RANGE: f64 = 4.0;
969
970impl<A: Float + Default + Send + Sync> DistributionTracker<A> {
971 pub(crate) fn observe(&mut self, gradients: &Array1<A>) -> f64 {
980 let values: Vec<f64> = gradients.iter().map(|value| from_scalar(*value)).collect();
981 if values.len() < 2 || values.iter().any(|value| !value.is_finite()) {
982 return 0.0;
983 }
984
985 let reference =
986 self.feature_distributions
987 .entry(0)
988 .or_insert_with(|| FeatureDistribution {
989 mean: A::zero(),
990 variance: A::zero(),
991 histogram: vec![A::zero(); DISTRIBUTION_BINS],
992 last_update: Instant::now(),
993 });
994
995 let reference_mean = from_scalar(reference.mean);
996 let reference_std = from_scalar(reference.variance).max(0.0).sqrt();
997 let scale = if reference_std > f64::EPSILON {
998 reference_std
999 } else {
1000 variance(&values).sqrt().max(f64::EPSILON)
1001 };
1002
1003 let width = 2.0 * DISTRIBUTION_Z_RANGE / DISTRIBUTION_BINS as f64;
1004 let mut counts = [0.0f64; DISTRIBUTION_BINS];
1005 for value in &values {
1006 let z = ((value - reference_mean) / scale)
1007 .clamp(-DISTRIBUTION_Z_RANGE, DISTRIBUTION_Z_RANGE);
1008 let index = (((z + DISTRIBUTION_Z_RANGE) / width) as usize).min(DISTRIBUTION_BINS - 1);
1009 counts[index] += 1.0;
1010 }
1011 let total: f64 = counts.iter().sum();
1012 let current: Vec<f64> = counts.iter().map(|count| count / total).collect();
1013
1014 let smoothing = 1.0 / DISTRIBUTION_BINS as f64;
1017 let mut divergence = 0.0;
1018 let mut initialised = false;
1019 for (index, probability) in current.iter().enumerate() {
1020 let previous = from_scalar(reference.histogram[index]);
1021 if previous > 0.0 {
1022 initialised = true;
1023 }
1024 let q = (previous + smoothing) / (1.0 + 1.0);
1025 let p = (probability + smoothing) / (1.0 + 1.0);
1026 if p > 0.0 && q > 0.0 {
1027 divergence += p * (p / q).ln();
1028 }
1029 }
1030
1031 for (index, probability) in current.iter().enumerate() {
1033 let previous = from_scalar(reference.histogram[index]);
1034 reference.histogram[index] = to_scalar(0.9 * previous + 0.1 * probability);
1035 }
1036 reference.mean = to_scalar(0.9 * reference_mean + 0.1 * mean(&values));
1039 let observed_variance = variance(&values);
1040 let previous_variance = from_scalar(reference.variance);
1041 reference.variance = to_scalar(if previous_variance > 0.0 {
1042 0.9 * previous_variance + 0.1 * observed_variance
1043 } else {
1044 observed_variance
1045 });
1046 reference.last_update = Instant::now();
1047
1048 let divergence = if initialised && divergence.is_finite() {
1049 divergence.max(0.0)
1050 } else {
1051 0.0
1053 };
1054 self.distribution_drift_score = to_scalar(divergence);
1055 divergence
1056 }
1057}
1058
1059impl<A: Float + Default + Send + Sync> AdaptationSpeedController<A> {
1060 pub(crate) fn update(&mut self, drift_agreement: f64) -> f64 {
1063 let base = from_scalar(self.base_adaptation_rate).max(1e-6);
1064 let acceleration = from_scalar(self.acceleration_factor).max(1.0);
1065 let deceleration = from_scalar(self.deceleration_factor).clamp(0.0, 1.0);
1066 let current = from_scalar(self.current_adaptation_rate).max(1e-6);
1067
1068 let target = if drift_agreement > 0.0 {
1069 current * acceleration
1070 } else {
1071 current * deceleration
1072 };
1073 let momentum = from_scalar(self.momentum).clamp(0.0, 0.95);
1074 let updated = (momentum * current + (1.0 - momentum) * target).clamp(base * 0.1, 1.0);
1075 self.current_adaptation_rate = to_scalar(updated);
1076 self.momentum = to_scalar(0.9 * momentum + 0.1 * drift_agreement.clamp(0.0, 1.0));
1077 updated
1078 }
1079}
1080
1081impl<A: Float + Default + Send + Sync> DriftSeverityAssessor<A> {
1082 pub(crate) fn assess(&mut self, agreement: f64, divergence: f64) -> DriftSeverityLevel<A> {
1084 let magnitude = (agreement + divergence.min(1.0)) / 2.0;
1085 let magnitude = if magnitude < 0.05 { 0.0 } else { magnitude };
1088 let (level, adjustment) = if magnitude >= 0.75 {
1089 (DriftSeverity::Critical, 2.0)
1090 } else if magnitude >= 0.5 {
1091 (DriftSeverity::Severe, 1.5)
1092 } else if magnitude >= 0.25 {
1093 (DriftSeverity::Moderate, 1.2)
1094 } else if magnitude > 0.0 {
1095 (DriftSeverity::Mild, 1.05)
1096 } else {
1097 (DriftSeverity::None, 1.0)
1098 };
1099
1100 let assessed = DriftSeverityLevel {
1101 level,
1102 recommended_lr_adjustment: to_scalar(adjustment),
1103 };
1104 self.current_severity = assessed.clone();
1105 self.severity_history.push_back(assessed.clone());
1106 while self.severity_history.len() > MAX_METRIC_HISTORY {
1107 self.severity_history.pop_front();
1108 }
1109 if self.severity_levels.len() < 5
1110 && !self
1111 .severity_levels
1112 .iter()
1113 .any(|existing| existing.level == level)
1114 {
1115 self.severity_levels.push(assessed.clone());
1116 }
1117 assessed
1118 }
1119}
1120
1121impl<A: Float + Default + Clone + Send + Sync> ResourceAwareAdapter<A> {
1126 pub(crate) fn new(config: &AdaptiveLRConfig<A>) -> Result<Self> {
1127 let compute_tracker = ComputationTimeTracker {
1128 time_budget: config.step_time_budget,
1129 ..ComputationTimeTracker::default()
1130 };
1131
1132 Ok(Self {
1133 memory_tracker: MemoryUsageTracker::default(),
1134 compute_tracker,
1135 energy_tracker: EnergyConsumptionTracker::default(),
1136 throughput_requirements: ThroughputRequirements {
1137 min_samples_per_second: A::zero(),
1138 current_throughput: A::zero(),
1141 throughput_deficit: A::zero(),
1142 },
1143 budget_manager: ResourceBudgetManager {
1144 memory_budget_mb: config.memory_budget_mb.unwrap_or(0.0),
1145 compute_budget_seconds: config
1146 .step_time_budget
1147 .map(|budget| budget.as_secs_f64())
1148 .unwrap_or(0.0),
1149 budget_utilization: A::zero(),
1150 budget_violations: 0,
1151 },
1152 })
1153 }
1154
1155 pub(crate) fn record_step_time(&mut self, elapsed: Duration, budget: Option<Duration>) {
1157 self.compute_tracker.time_budget = budget;
1158 self.compute_tracker.step_times.push_back(elapsed);
1159 while self.compute_tracker.step_times.len() > MAX_METRIC_HISTORY {
1160 self.compute_tracker.step_times.pop_front();
1161 }
1162 let seconds: Vec<f64> = self
1163 .compute_tracker
1164 .step_times
1165 .iter()
1166 .map(|duration| duration.as_secs_f64())
1167 .collect();
1168 let average = mean(&seconds);
1169 self.compute_tracker.average_step_time = Duration::from_secs_f64(average.max(0.0));
1170 self.compute_tracker.time_pressure = match budget {
1171 Some(budget) if budget.as_secs_f64() > 0.0 => {
1172 let pressure = average / budget.as_secs_f64();
1173 if pressure > 1.0 {
1174 self.budget_manager.budget_violations += 1;
1175 }
1176 Some(pressure)
1177 }
1178 _ => None,
1179 };
1180 if self.budget_manager.compute_budget_seconds > 0.0 {
1181 self.budget_manager.budget_utilization = to_scalar(
1182 (average / self.budget_manager.compute_budget_seconds).clamp(0.0, f64::MAX),
1183 );
1184 }
1185 if average > 0.0 {
1187 self.throughput_requirements.current_throughput = to_scalar(1.0 / average);
1188 self.refresh_throughput_deficit();
1189 }
1190 }
1191
1192 pub(crate) fn record_memory_usage(&mut self, usage_mb: f64, budget_mb: Option<f64>) {
1194 self.memory_tracker.current_usage_mb = usage_mb;
1195 self.memory_tracker.peak_usage_mb = self.memory_tracker.peak_usage_mb.max(usage_mb);
1196 self.memory_tracker.usage_history.push_back(usage_mb);
1197 while self.memory_tracker.usage_history.len() > MAX_METRIC_HISTORY {
1198 self.memory_tracker.usage_history.pop_front();
1199 }
1200 if let Some(budget) = budget_mb.filter(|budget| *budget > 0.0) {
1201 self.budget_manager.memory_budget_mb = budget;
1202 let pressure = usage_mb / budget;
1203 if pressure > 1.0 {
1204 self.budget_manager.budget_violations += 1;
1205 }
1206 self.memory_tracker.memory_pressure = Some(pressure);
1207 } else {
1208 self.memory_tracker.memory_pressure = None;
1209 }
1210 }
1211
1212 pub(crate) fn record_energy(&mut self, joules: f64) {
1214 self.energy_tracker.energy_per_step.push_back(joules);
1215 while self.energy_tracker.energy_per_step.len() > MAX_METRIC_HISTORY {
1216 self.energy_tracker.energy_per_step.pop_front();
1217 }
1218 self.energy_tracker.cumulative_energy += joules;
1219 let average = mean(
1220 &self
1221 .energy_tracker
1222 .energy_per_step
1223 .iter()
1224 .copied()
1225 .collect::<Vec<f64>>(),
1226 );
1227 self.energy_tracker.energy_efficiency = if average > 0.0 {
1228 Some(1.0 / average)
1229 } else {
1230 None
1231 };
1232 }
1233
1234 pub(crate) fn record_throughput(&mut self, samples_per_second: A) {
1236 self.throughput_requirements.current_throughput = samples_per_second;
1237 self.refresh_throughput_deficit();
1238 }
1239
1240 pub fn set_throughput_requirement(&mut self, minimum: A) {
1246 self.throughput_requirements.min_samples_per_second = minimum;
1247 self.refresh_throughput_deficit();
1248 }
1249
1250 fn refresh_throughput_deficit(&mut self) {
1251 let minimum = from_scalar(self.throughput_requirements.min_samples_per_second);
1252 let current = from_scalar(self.throughput_requirements.current_throughput);
1253 self.throughput_requirements.throughput_deficit = to_scalar((minimum - current).max(0.0));
1254 }
1255
1256 pub(crate) fn generate_signal(&mut self, _step: usize) -> Result<SignalVote<A>> {
1257 let mut terms: Vec<(f64, String)> = Vec::new();
1261
1262 if let Some(pressure) = self.memory_tracker.memory_pressure {
1263 let recommended = if pressure > 0.8 {
1264 0.9
1265 } else if pressure < 0.3 {
1266 1.05
1267 } else {
1268 1.0
1269 };
1270 terms.push((recommended, format!("memory pressure {pressure:.2}")));
1271 }
1272
1273 if let Some(pressure) = self.compute_tracker.time_pressure {
1274 let recommended = if pressure > 1.0 {
1278 0.95
1279 } else if pressure < 0.5 {
1280 1.02
1281 } else {
1282 1.0
1283 };
1284 terms.push((recommended, format!("time pressure {pressure:.2}")));
1285 }
1286
1287 let deficit = from_scalar(self.throughput_requirements.throughput_deficit);
1288 if deficit > 0.0 {
1289 terms.push((0.95, format!("throughput deficit {deficit:.2}/s")));
1290 }
1291
1292 if terms.is_empty() {
1293 return Err(crate::error::OptimError::InvalidState(
1294 "no resource measurements have been reported, so no resource signal can be \
1295 produced"
1296 .to_string(),
1297 ));
1298 }
1299
1300 let recommended = mean(&terms.iter().map(|(value, _)| *value).collect::<Vec<f64>>());
1301 let confidence = (terms.len() as f64 / 3.0).clamp(0.0, 1.0);
1304
1305 Ok(SignalVote {
1306 signal_type: AdaptationSignalType::ResourceUtilization,
1307 recommended_lr_change: to_scalar(recommended),
1308 confidence: to_scalar(confidence),
1309 reasoning: terms
1310 .iter()
1311 .map(|(_, reason)| reason.clone())
1312 .collect::<Vec<String>>()
1313 .join(", "),
1314 timestamp: Instant::now(),
1315 })
1316 }
1317
1318 pub(crate) fn reset(&mut self) {
1319 self.memory_tracker = MemoryUsageTracker::default();
1320 self.compute_tracker = ComputationTimeTracker {
1321 time_budget: self.compute_tracker.time_budget,
1322 ..ComputationTimeTracker::default()
1323 };
1324 self.energy_tracker = EnergyConsumptionTracker::default();
1325 self.budget_manager.budget_violations = 0;
1326 self.budget_manager.budget_utilization = A::zero();
1327 }
1328
1329 pub(crate) fn budget_violations(&self) -> usize {
1331 self.budget_manager.budget_violations
1332 }
1333}