1use crate::cost_curve::CostCurvePoint;
27use crate::transfer::{ArmId, BetaParams, ContextBucket};
28use serde::{Deserialize, Serialize};
29use std::collections::HashMap;
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct BucketRegret {
38 pub best_mean: f32,
40 pub best_arm: ArmId,
42 pub cumulative_regret: f64,
44 pub observations: u64,
46 pub regret_history: Vec<f64>,
48 arm_means: HashMap<ArmId, (f64, u64)>,
50}
51
52impl BucketRegret {
53 fn new() -> Self {
54 Self {
55 best_mean: 0.0,
56 best_arm: ArmId("unknown".into()),
57 cumulative_regret: 0.0,
58 observations: 0,
59 regret_history: Vec::new(),
60 arm_means: HashMap::new(),
61 }
62 }
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct RegretTracker {
72 buckets: HashMap<ContextBucket, BucketRegret>,
73 pub total_regret: f64,
75 pub total_observations: u64,
77 snapshot_interval: u64,
79}
80
81impl RegretTracker {
82 pub fn new(snapshot_interval: u64) -> Self {
84 Self {
85 buckets: HashMap::new(),
86 total_regret: 0.0,
87 total_observations: 0,
88 snapshot_interval: snapshot_interval.max(1),
89 }
90 }
91
92 pub fn record(&mut self, bucket: &ContextBucket, arm: &ArmId, reward: f32) {
94 if !self.buckets.contains_key(bucket) {
96 self.buckets.insert(bucket.clone(), BucketRegret::new());
97 }
98 let entry = self.buckets.get_mut(bucket).unwrap();
99
100 if !entry.arm_means.contains_key(arm) {
102 entry.arm_means.insert(arm.clone(), (0.0, 0));
103 }
104 let (sum, count) = entry.arm_means.get_mut(arm).unwrap();
105 *sum += reward as f64;
106 *count += 1;
107 let arm_mean = *sum / *count as f64;
108
109 if arm_mean > entry.best_mean as f64 {
111 entry.best_mean = arm_mean as f32;
112 entry.best_arm = arm.clone();
113 }
114
115 let instant_regret = (entry.best_mean as f64 - reward as f64).max(0.0);
117 entry.cumulative_regret += instant_regret;
118 entry.observations += 1;
119 self.total_regret += instant_regret;
120 self.total_observations += 1;
121
122 if entry.observations % self.snapshot_interval == 0 {
124 entry.regret_history.push(entry.cumulative_regret);
125 }
126 }
127
128 pub fn regret_growth_rate(&self, bucket: &ContextBucket) -> Option<f32> {
133 let entry = self.buckets.get(bucket)?;
134 if entry.observations < 10 || entry.cumulative_regret < 1e-10 {
135 return None;
136 }
137 let log_regret = (entry.cumulative_regret).ln();
138 let log_t = (entry.observations as f64).ln();
139 Some((log_regret / log_t) as f32)
140 }
141
142 pub fn average_regret(&self) -> f64 {
144 if self.total_observations == 0 {
145 return 0.0;
146 }
147 self.total_regret / self.total_observations as f64
148 }
149
150 pub fn has_converged(&self, bucket: &ContextBucket, threshold: f32) -> bool {
152 self.regret_growth_rate(bucket)
153 .map_or(false, |rate| rate < threshold)
154 }
155
156 pub fn summary(&self) -> RegretSummary {
158 let bucket_rates: Vec<(ContextBucket, f32)> = self
159 .buckets
160 .keys()
161 .filter_map(|b| self.regret_growth_rate(b).map(|r| (b.clone(), r)))
162 .collect();
163
164 let mean_rate = if bucket_rates.is_empty() {
165 1.0
166 } else {
167 bucket_rates.iter().map(|(_, r)| r).sum::<f32>() / bucket_rates.len() as f32
168 };
169
170 RegretSummary {
171 total_regret: self.total_regret,
172 total_observations: self.total_observations,
173 average_regret: self.average_regret(),
174 mean_growth_rate: mean_rate,
175 bucket_count: self.buckets.len(),
176 converged_buckets: bucket_rates.iter().filter(|(_, r)| *r < 0.7).count(),
177 }
178 }
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct RegretSummary {
184 pub total_regret: f64,
185 pub total_observations: u64,
186 pub average_regret: f64,
187 pub mean_growth_rate: f32,
189 pub bucket_count: usize,
190 pub converged_buckets: usize,
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct DecayingBeta {
209 pub alpha: f32,
210 pub beta: f32,
211 pub decay_factor: f32,
213 pub effective_n: f32,
215}
216
217impl DecayingBeta {
218 pub fn new(decay_factor: f32) -> Self {
220 Self {
221 alpha: 1.0,
222 beta: 1.0,
223 decay_factor: decay_factor.clamp(0.9, 1.0),
224 effective_n: 0.0,
225 }
226 }
227
228 pub fn from_beta(params: &BetaParams, decay_factor: f32) -> Self {
230 Self {
231 alpha: params.alpha,
232 beta: params.beta,
233 decay_factor: decay_factor.clamp(0.9, 1.0),
234 effective_n: params.alpha + params.beta - 2.0,
235 }
236 }
237
238 pub fn update(&mut self, reward: f32) {
240 self.alpha = 1.0 + (self.alpha - 1.0) * self.decay_factor;
242 self.beta = 1.0 + (self.beta - 1.0) * self.decay_factor;
243
244 self.alpha += reward;
246 self.beta += 1.0 - reward;
247
248 self.effective_n = self.effective_n * self.decay_factor + 1.0;
250 }
251
252 pub fn mean(&self) -> f32 {
254 self.alpha / (self.alpha + self.beta)
255 }
256
257 pub fn variance(&self) -> f32 {
259 let total = self.alpha + self.beta;
260 (self.alpha * self.beta) / (total * total * (total + 1.0))
261 }
262
263 pub fn to_beta_params(&self) -> BetaParams {
265 BetaParams {
266 alpha: self.alpha,
267 beta: self.beta,
268 }
269 }
270
271 pub fn effective_window(&self) -> f32 {
273 if self.decay_factor >= 1.0 {
274 self.effective_n
275 } else {
276 1.0 / (1.0 - self.decay_factor)
277 }
278 }
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize)]
291pub struct PlateauDetector {
292 pub window_size: usize,
294 pub improvement_threshold: f32,
296 pub consecutive_plateaus: u32,
298 pub total_plateaus: u32,
300}
301
302#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
304pub enum PlateauAction {
305 Continue,
307 IncreaseExploration,
309 TriggerTransfer,
311 InjectDiversity,
313 Reset,
315}
316
317impl PlateauDetector {
318 pub fn new(window_size: usize, improvement_threshold: f32) -> Self {
320 Self {
321 window_size: window_size.max(3),
322 improvement_threshold: improvement_threshold.max(0.001),
323 consecutive_plateaus: 0,
324 total_plateaus: 0,
325 }
326 }
327
328 pub fn check(&mut self, points: &[CostCurvePoint]) -> PlateauAction {
330 if points.len() < self.window_size * 2 {
331 self.consecutive_plateaus = 0;
332 return PlateauAction::Continue;
333 }
334
335 let n = points.len();
336 let recent = &points[n - self.window_size..];
337 let prior = &points[n - 2 * self.window_size..n - self.window_size];
338
339 let recent_mean = recent.iter().map(|p| p.accuracy).sum::<f32>() / recent.len() as f32;
340 let prior_mean = prior.iter().map(|p| p.accuracy).sum::<f32>() / prior.len() as f32;
341
342 let improvement = recent_mean - prior_mean;
343
344 if improvement.abs() < self.improvement_threshold {
345 self.consecutive_plateaus += 1;
346 self.total_plateaus += 1;
347
348 match self.consecutive_plateaus {
349 1 => PlateauAction::IncreaseExploration,
350 2..=3 => PlateauAction::TriggerTransfer,
351 4..=6 => PlateauAction::InjectDiversity,
352 _ => PlateauAction::Reset,
353 }
354 } else {
355 self.consecutive_plateaus = 0;
356 PlateauAction::Continue
357 }
358 }
359
360 pub fn check_cost(&self, points: &[CostCurvePoint]) -> bool {
362 if points.len() < self.window_size * 2 {
363 return false;
364 }
365
366 let n = points.len();
367 let recent = &points[n - self.window_size..];
368 let prior = &points[n - 2 * self.window_size..n - self.window_size];
369
370 let recent_cost =
371 recent.iter().map(|p| p.cost_per_solve).sum::<f32>() / recent.len() as f32;
372 let prior_cost = prior.iter().map(|p| p.cost_per_solve).sum::<f32>() / prior.len() as f32;
373
374 (prior_cost - recent_cost).abs() < self.improvement_threshold
376 }
377
378 pub fn learning_velocity(&self, points: &[CostCurvePoint]) -> f32 {
380 if points.len() < 2 {
381 return 0.0;
382 }
383 let n = points.len();
384 let window = self.window_size.min(n);
385 let recent = &points[n - window..];
386
387 if recent.len() < 2 {
388 return 0.0;
389 }
390
391 let first = recent.first().unwrap();
392 let last = recent.last().unwrap();
393 let dt = (last.cycle - first.cycle).max(1) as f32;
394
395 (last.accuracy - first.accuracy) / dt
396 }
397}
398
399#[derive(Debug, Clone, Serialize, Deserialize)]
405pub struct ParetoPoint {
406 pub kernel_id: String,
408 pub objectives: Vec<f32>,
411 pub generation: u32,
413}
414
415#[derive(Debug, Clone, Default, Serialize, Deserialize)]
421pub struct ParetoFront {
422 pub front: Vec<ParetoPoint>,
424 pub evaluated: u64,
426 pub front_updates: u64,
428}
429
430impl ParetoFront {
431 pub fn new() -> Self {
432 Self::default()
433 }
434
435 pub fn dominates(a: &[f32], b: &[f32]) -> bool {
440 if a.len() != b.len() {
441 return false;
442 }
443 let mut at_least_equal = true;
444 let mut strictly_better = false;
445
446 for (ai, bi) in a.iter().zip(b.iter()) {
447 if ai < bi {
448 at_least_equal = false;
449 break;
450 }
451 if ai > bi {
452 strictly_better = true;
453 }
454 }
455
456 at_least_equal && strictly_better
457 }
458
459 pub fn insert(&mut self, point: ParetoPoint) -> bool {
463 self.evaluated += 1;
464
465 for existing in &self.front {
467 if Self::dominates(&existing.objectives, &point.objectives) {
468 return false; }
470 }
471
472 self.front
474 .retain(|existing| !Self::dominates(&point.objectives, &existing.objectives));
475
476 self.front.push(point);
477 self.front_updates += 1;
478 true
479 }
480
481 pub fn hypervolume(&self, reference: &[f32]) -> f32 {
487 if self.front.is_empty() || reference.is_empty() {
488 return 0.0;
489 }
490
491 let dim = reference.len();
492 if dim == 2 {
493 self.hypervolume_2d(reference)
494 } else {
495 self.front
497 .iter()
498 .map(|p| {
499 p.objectives
500 .iter()
501 .zip(reference.iter())
502 .map(|(oi, ri)| (oi - ri).max(0.0))
503 .product::<f32>()
504 })
505 .sum()
506 }
507 }
508
509 fn hypervolume_2d(&self, reference: &[f32]) -> f32 {
511 if self.front.is_empty() {
512 return 0.0;
513 }
514
515 let mut points: Vec<(f32, f32)> = self
516 .front
517 .iter()
518 .map(|p| {
519 let x = p.objectives.first().copied().unwrap_or(0.0);
520 let y = p.objectives.get(1).copied().unwrap_or(0.0);
521 (x, y)
522 })
523 .collect();
524
525 points.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
527
528 let ref_x = reference.first().copied().unwrap_or(0.0);
529 let ref_y = reference.get(1).copied().unwrap_or(0.0);
530
531 let mut volume = 0.0f32;
532 let mut prev_y = ref_y;
533
534 for &(x, y) in &points {
535 if y > prev_y {
536 volume += (x - ref_x) * (y - prev_y);
537 prev_y = y;
538 }
539 }
540
541 volume
542 }
543
544 pub fn len(&self) -> usize {
546 self.front.len()
547 }
548
549 pub fn is_empty(&self) -> bool {
551 self.front.is_empty()
552 }
553
554 pub fn best_on(&self, objective_index: usize) -> Option<&ParetoPoint> {
556 self.front.iter().max_by(|a, b| {
557 let va = a.objectives.get(objective_index).copied().unwrap_or(0.0);
558 let vb = b.objectives.get(objective_index).copied().unwrap_or(0.0);
559 va.partial_cmp(&vb).unwrap_or(std::cmp::Ordering::Equal)
560 })
561 }
562
563 pub fn spread(&self) -> Vec<f32> {
565 if self.front.is_empty() {
566 return Vec::new();
567 }
568 let dim = self.front[0].objectives.len();
569 (0..dim)
570 .map(|i| {
571 let vals: Vec<f32> = self.front.iter().map(|p| p.objectives[i]).collect();
572 let min = vals.iter().cloned().fold(f32::INFINITY, f32::min);
573 let max = vals.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
574 max - min
575 })
576 .collect()
577 }
578}
579
580#[derive(Debug, Clone, Serialize, Deserialize)]
590pub struct CuriosityBonus {
591 visit_counts: HashMap<ContextBucket, HashMap<ArmId, u64>>,
593 pub total_visits: u64,
595 pub exploration_coeff: f32,
597}
598
599impl CuriosityBonus {
600 pub fn new(exploration_coeff: f32) -> Self {
603 Self {
604 visit_counts: HashMap::new(),
605 total_visits: 0,
606 exploration_coeff: exploration_coeff.max(0.0),
607 }
608 }
609
610 pub fn record_visit(&mut self, bucket: &ContextBucket, arm: &ArmId) {
612 if let Some(arms) = self.visit_counts.get_mut(bucket) {
614 if let Some(count) = arms.get_mut(arm) {
615 *count += 1;
616 } else {
617 arms.insert(arm.clone(), 1);
618 }
619 } else {
620 let mut arms = HashMap::new();
621 arms.insert(arm.clone(), 1u64);
622 self.visit_counts.insert(bucket.clone(), arms);
623 }
624 self.total_visits += 1;
625 }
626
627 pub fn bonus(&self, bucket: &ContextBucket, arm: &ArmId) -> f32 {
634 if self.total_visits < 2 {
635 return self.exploration_coeff; }
637
638 let arm_visits = self
639 .visit_counts
640 .get(bucket)
641 .and_then(|arms| arms.get(arm))
642 .copied()
643 .unwrap_or(0);
644
645 if arm_visits == 0 {
646 return self.exploration_coeff * 2.0; }
648
649 let log_n = (self.total_visits as f32).ln();
650 self.exploration_coeff * (log_n / arm_visits as f32).sqrt()
651 }
652
653 pub fn most_curious_bucket(&self) -> Option<&ContextBucket> {
655 let mut min_visits = u64::MAX;
657 let mut most_curious = None;
658
659 for (bucket, arms) in &self.visit_counts {
660 let total: u64 = arms.values().sum();
661 if total < min_visits {
662 min_visits = total;
663 most_curious = Some(bucket);
664 }
665 }
666
667 most_curious
668 }
669
670 pub fn novelty_score(&self, bucket: &ContextBucket) -> f32 {
673 if self.total_visits == 0 {
674 return 1.0;
675 }
676
677 let bucket_visits: u64 = self
678 .visit_counts
679 .get(bucket)
680 .map(|arms| arms.values().sum())
681 .unwrap_or(0);
682
683 if bucket_visits == 0 {
684 return 1.0;
685 }
686
687 1.0 - (bucket_visits as f32 / self.total_visits as f32)
688 }
689}
690
691#[derive(Debug, Clone, Serialize, Deserialize)]
701pub struct MetaLearningEngine {
702 pub regret: RegretTracker,
703 pub plateau: PlateauDetector,
704 pub pareto: ParetoFront,
705 pub curiosity: CuriosityBonus,
706 pub decaying_betas: HashMap<(ContextBucket, ArmId), DecayingBeta>,
708 decay_factor: f32,
710}
711
712impl MetaLearningEngine {
713 pub fn new() -> Self {
715 Self {
716 regret: RegretTracker::new(50),
717 plateau: PlateauDetector::new(5, 0.005),
718 pareto: ParetoFront::new(),
719 curiosity: CuriosityBonus::new(1.41),
720 decaying_betas: HashMap::new(),
721 decay_factor: 0.995,
722 }
723 }
724
725 pub fn with_config(
727 regret_snapshot_interval: u64,
728 plateau_window: usize,
729 plateau_threshold: f32,
730 exploration_coeff: f32,
731 decay_factor: f32,
732 ) -> Self {
733 Self {
734 regret: RegretTracker::new(regret_snapshot_interval),
735 plateau: PlateauDetector::new(plateau_window, plateau_threshold),
736 pareto: ParetoFront::new(),
737 curiosity: CuriosityBonus::new(exploration_coeff),
738 decaying_betas: HashMap::new(),
739 decay_factor,
740 }
741 }
742
743 pub fn record_decision(&mut self, bucket: &ContextBucket, arm: &ArmId, reward: f32) {
745 self.regret.record(bucket, arm, reward);
747
748 self.curiosity.record_visit(bucket, arm);
750
751 let key = (bucket.clone(), arm.clone());
754 if let Some(db) = self.decaying_betas.get_mut(&key) {
755 db.update(reward);
756 } else {
757 let mut db = DecayingBeta::new(self.decay_factor);
758 db.update(reward);
759 self.decaying_betas.insert(key, db);
760 }
761 }
762
763 pub fn record_kernel(
765 &mut self,
766 kernel_id: &str,
767 accuracy: f32,
768 cost: f32,
769 robustness: f32,
770 generation: u32,
771 ) {
772 let point = ParetoPoint {
773 kernel_id: kernel_id.to_string(),
774 objectives: vec![accuracy, -cost, robustness],
776 generation,
777 };
778 self.pareto.insert(point);
779 }
780
781 pub fn check_plateau(&mut self, points: &[CostCurvePoint]) -> PlateauAction {
783 self.plateau.check(points)
784 }
785
786 pub fn boosted_score(&self, bucket: &ContextBucket, arm: &ArmId, thompson_sample: f32) -> f32 {
790 let bonus = self.curiosity.bonus(bucket, arm);
791 thompson_sample + bonus
792 }
793
794 pub fn decaying_mean(&self, bucket: &ContextBucket, arm: &ArmId) -> Option<f32> {
796 let key = (bucket.clone(), arm.clone());
797 self.decaying_betas.get(&key).map(|db| db.mean())
798 }
799
800 pub fn health_check(&self) -> MetaLearningHealth {
802 let regret_summary = self.regret.summary();
803 let pareto_size = self.pareto.len();
804
805 let is_learning = regret_summary.mean_growth_rate < 0.8;
806 let is_diverse = pareto_size >= 3;
807 let is_exploring = self.curiosity.total_visits > 0;
808
809 MetaLearningHealth {
810 regret: regret_summary,
811 pareto_size,
812 pareto_hypervolume: self.pareto.hypervolume(&[0.0, -1.0, 0.0]),
813 consecutive_plateaus: self.plateau.consecutive_plateaus,
814 total_plateaus: self.plateau.total_plateaus,
815 curiosity_total_visits: self.curiosity.total_visits,
816 decaying_beta_count: self.decaying_betas.len(),
817 is_learning,
818 is_diverse,
819 is_exploring,
820 }
821 }
822}
823
824impl Default for MetaLearningEngine {
825 fn default() -> Self {
826 Self::new()
827 }
828}
829
830#[derive(Debug, Clone, Serialize, Deserialize)]
832pub struct MetaLearningHealth {
833 pub regret: RegretSummary,
834 pub pareto_size: usize,
835 pub pareto_hypervolume: f32,
836 pub consecutive_plateaus: u32,
837 pub total_plateaus: u32,
838 pub curiosity_total_visits: u64,
839 pub decaying_beta_count: usize,
840 pub is_learning: bool,
842 pub is_diverse: bool,
844 pub is_exploring: bool,
846}
847
848#[cfg(test)]
853mod tests {
854 use super::*;
855
856 fn test_bucket(tier: &str, cat: &str) -> ContextBucket {
857 ContextBucket {
858 difficulty_tier: tier.into(),
859 category: cat.into(),
860 }
861 }
862
863 #[test]
866 fn test_regret_tracker_empty() {
867 let tracker = RegretTracker::new(10);
868 assert_eq!(tracker.total_regret, 0.0);
869 assert_eq!(tracker.average_regret(), 0.0);
870 }
871
872 #[test]
873 fn test_regret_tracker_optimal_arm() {
874 let mut tracker = RegretTracker::new(10);
875 let bucket = test_bucket("easy", "test");
876 let arm = ArmId("best".into());
877
878 for _ in 0..100 {
880 tracker.record(&bucket, &arm, 0.9);
881 }
882
883 assert_eq!(tracker.total_observations, 100);
884 assert!(tracker.total_regret < 1e-6);
886 }
887
888 #[test]
889 fn test_regret_tracker_suboptimal_arm() {
890 let mut tracker = RegretTracker::new(10);
891 let bucket = test_bucket("medium", "test");
892 let good = ArmId("good".into());
893 let bad = ArmId("bad".into());
894
895 for _ in 0..50 {
897 tracker.record(&bucket, &good, 0.9);
898 }
899
900 for _ in 0..50 {
902 tracker.record(&bucket, &bad, 0.3);
903 }
904
905 assert!(tracker.total_regret > 0.0);
906 assert!(tracker.average_regret() > 0.0);
907 }
908
909 #[test]
910 fn test_regret_growth_rate() {
911 let mut tracker = RegretTracker::new(5);
912 let bucket = test_bucket("hard", "test");
913 let arm_a = ArmId("a".into());
914 let arm_b = ArmId("b".into());
915
916 for _ in 0..50 {
917 tracker.record(&bucket, &arm_a, 0.8);
918 }
919 for _ in 0..50 {
920 tracker.record(&bucket, &arm_b, 0.4);
921 }
922
923 let rate = tracker.regret_growth_rate(&bucket);
924 assert!(rate.is_some());
925 }
927
928 #[test]
929 fn test_regret_summary() {
930 let mut tracker = RegretTracker::new(10);
931 let bucket = test_bucket("easy", "algo");
932 let arm = ArmId("test".into());
933
934 for _ in 0..20 {
935 tracker.record(&bucket, &arm, 0.7);
936 }
937
938 let summary = tracker.summary();
939 assert_eq!(summary.total_observations, 20);
940 assert_eq!(summary.bucket_count, 1);
941 }
942
943 #[test]
946 fn test_decaying_beta_initial() {
947 let db = DecayingBeta::new(0.995);
948 assert!((db.mean() - 0.5).abs() < 1e-6); assert_eq!(db.effective_n, 0.0);
950 }
951
952 #[test]
953 fn test_decaying_beta_update() {
954 let mut db = DecayingBeta::new(0.995);
955
956 for _ in 0..100 {
957 db.update(0.9); }
959
960 assert!(db.mean() > 0.7); assert!(db.effective_n > 50.0); }
963
964 #[test]
965 fn test_decaying_beta_adapts() {
966 let mut db = DecayingBeta::new(0.99); for _ in 0..100 {
970 db.update(0.95);
971 }
972 let mean_after_good = db.mean();
973 assert!(mean_after_good > 0.8);
974
975 for _ in 0..100 {
977 db.update(0.1);
978 }
979 let mean_after_bad = db.mean();
980
981 assert!(mean_after_bad < mean_after_good);
983 assert!(mean_after_bad < 0.5); }
985
986 #[test]
987 fn test_decaying_beta_window() {
988 let db = DecayingBeta::new(0.99);
989 let window = db.effective_window();
990 assert!((window - 100.0).abs() < 1.0); let db2 = DecayingBeta::new(0.995);
993 let window2 = db2.effective_window();
994 assert!((window2 - 200.0).abs() < 1.0); }
996
997 #[test]
998 fn test_decaying_to_standard() {
999 let mut db = DecayingBeta::new(0.995);
1000 for _ in 0..10 {
1001 db.update(0.8);
1002 }
1003 let params = db.to_beta_params();
1004 assert!(params.alpha > 1.0);
1005 assert!(params.beta > 1.0);
1006 assert!((params.mean() - db.mean()).abs() < 1e-6);
1007 }
1008
1009 #[test]
1012 fn test_plateau_no_data() {
1013 let mut detector = PlateauDetector::new(3, 0.01);
1014 let action = detector.check(&[]);
1015 assert_eq!(action, PlateauAction::Continue);
1016 }
1017
1018 #[test]
1019 fn test_plateau_not_enough_data() {
1020 let mut detector = PlateauDetector::new(3, 0.01);
1021 let points: Vec<CostCurvePoint> = (0..4)
1022 .map(|i| CostCurvePoint {
1023 cycle: i as u64,
1024 accuracy: 0.5 + i as f32 * 0.1,
1025 cost_per_solve: 0.1,
1026 robustness: 0.8,
1027 policy_violations: 0,
1028 timestamp: i as f64,
1029 })
1030 .collect();
1031
1032 let action = detector.check(&points);
1033 assert_eq!(action, PlateauAction::Continue);
1034 }
1035
1036 #[test]
1037 fn test_plateau_detected() {
1038 let mut detector = PlateauDetector::new(3, 0.01);
1039
1040 let points: Vec<CostCurvePoint> = (0..6)
1042 .map(|i| CostCurvePoint {
1043 cycle: i as u64,
1044 accuracy: 0.80 + (i as f32 * 0.001), cost_per_solve: 0.1,
1046 robustness: 0.8,
1047 policy_violations: 0,
1048 timestamp: i as f64,
1049 })
1050 .collect();
1051
1052 let action = detector.check(&points);
1053 assert_ne!(action, PlateauAction::Continue);
1054 }
1055
1056 #[test]
1057 fn test_plateau_improving() {
1058 let mut detector = PlateauDetector::new(3, 0.01);
1059
1060 let points: Vec<CostCurvePoint> = (0..6)
1062 .map(|i| CostCurvePoint {
1063 cycle: i as u64,
1064 accuracy: 0.50 + i as f32 * 0.08, cost_per_solve: 0.1,
1066 robustness: 0.8,
1067 policy_violations: 0,
1068 timestamp: i as f64,
1069 })
1070 .collect();
1071
1072 let action = detector.check(&points);
1073 assert_eq!(action, PlateauAction::Continue);
1074 }
1075
1076 #[test]
1077 fn test_plateau_escalation() {
1078 let mut detector = PlateauDetector::new(3, 0.01);
1079
1080 let flat_points: Vec<CostCurvePoint> = (0..6)
1081 .map(|i| CostCurvePoint {
1082 cycle: i as u64,
1083 accuracy: 0.80,
1084 cost_per_solve: 0.1,
1085 robustness: 0.8,
1086 policy_violations: 0,
1087 timestamp: i as f64,
1088 })
1089 .collect();
1090
1091 assert_eq!(
1092 detector.check(&flat_points),
1093 PlateauAction::IncreaseExploration
1094 );
1095 assert_eq!(detector.check(&flat_points), PlateauAction::TriggerTransfer);
1096 assert_eq!(detector.check(&flat_points), PlateauAction::TriggerTransfer);
1097 assert_eq!(detector.check(&flat_points), PlateauAction::InjectDiversity);
1098 }
1099
1100 #[test]
1101 fn test_learning_velocity() {
1102 let detector = PlateauDetector::new(3, 0.01);
1103
1104 let points: Vec<CostCurvePoint> = (0..6)
1105 .map(|i| CostCurvePoint {
1106 cycle: i as u64,
1107 accuracy: 0.50 + i as f32 * 0.1,
1108 cost_per_solve: 0.1,
1109 robustness: 0.8,
1110 policy_violations: 0,
1111 timestamp: i as f64,
1112 })
1113 .collect();
1114
1115 let velocity = detector.learning_velocity(&points);
1116 assert!(velocity > 0.0); }
1118
1119 #[test]
1122 fn test_pareto_dominates() {
1123 assert!(ParetoFront::dominates(&[0.9, -0.1, 0.8], &[0.8, -0.2, 0.7]));
1124 assert!(!ParetoFront::dominates(
1125 &[0.9, -0.3, 0.8],
1126 &[0.8, -0.1, 0.7]
1127 ));
1128 assert!(!ParetoFront::dominates(
1129 &[0.9, -0.1, 0.8],
1130 &[0.9, -0.1, 0.8]
1131 )); }
1133
1134 #[test]
1135 fn test_pareto_insert_non_dominated() {
1136 let mut front = ParetoFront::new();
1137
1138 assert!(front.insert(ParetoPoint {
1140 kernel_id: "a".into(),
1141 objectives: vec![0.9, -0.3, 0.7],
1142 generation: 0,
1143 }));
1144 assert!(front.insert(ParetoPoint {
1145 kernel_id: "b".into(),
1146 objectives: vec![0.7, -0.1, 0.9],
1147 generation: 0,
1148 }));
1149
1150 assert_eq!(front.len(), 2);
1151 }
1152
1153 #[test]
1154 fn test_pareto_insert_dominated() {
1155 let mut front = ParetoFront::new();
1156
1157 front.insert(ParetoPoint {
1158 kernel_id: "good".into(),
1159 objectives: vec![0.9, -0.1, 0.9],
1160 generation: 0,
1161 });
1162
1163 let added = front.insert(ParetoPoint {
1165 kernel_id: "bad".into(),
1166 objectives: vec![0.5, -0.5, 0.5],
1167 generation: 0,
1168 });
1169
1170 assert!(!added);
1171 assert_eq!(front.len(), 1);
1172 }
1173
1174 #[test]
1175 fn test_pareto_removes_dominated() {
1176 let mut front = ParetoFront::new();
1177
1178 front.insert(ParetoPoint {
1179 kernel_id: "old".into(),
1180 objectives: vec![0.5, -0.3, 0.5],
1181 generation: 0,
1182 });
1183
1184 front.insert(ParetoPoint {
1186 kernel_id: "new".into(),
1187 objectives: vec![0.9, -0.1, 0.9],
1188 generation: 1,
1189 });
1190
1191 assert_eq!(front.len(), 1);
1192 assert_eq!(front.front[0].kernel_id, "new");
1193 }
1194
1195 #[test]
1196 fn test_pareto_best_on_objective() {
1197 let mut front = ParetoFront::new();
1198
1199 front.insert(ParetoPoint {
1200 kernel_id: "accurate".into(),
1201 objectives: vec![0.95, -0.5, 0.6],
1202 generation: 0,
1203 });
1204 front.insert(ParetoPoint {
1205 kernel_id: "cheap".into(),
1206 objectives: vec![0.7, -0.05, 0.7],
1207 generation: 0,
1208 });
1209 front.insert(ParetoPoint {
1210 kernel_id: "robust".into(),
1211 objectives: vec![0.8, -0.3, 0.95],
1212 generation: 0,
1213 });
1214
1215 assert_eq!(front.best_on(0).unwrap().kernel_id, "accurate");
1216 assert_eq!(front.best_on(1).unwrap().kernel_id, "cheap"); assert_eq!(front.best_on(2).unwrap().kernel_id, "robust");
1218 }
1219
1220 #[test]
1221 fn test_pareto_spread() {
1222 let mut front = ParetoFront::new();
1223
1224 front.insert(ParetoPoint {
1226 kernel_id: "a".into(),
1227 objectives: vec![0.9, -0.5],
1228 generation: 0,
1229 });
1230 front.insert(ParetoPoint {
1231 kernel_id: "b".into(),
1232 objectives: vec![0.5, -0.1],
1233 generation: 0,
1234 });
1235
1236 assert_eq!(front.len(), 2); let spread = front.spread();
1238 assert_eq!(spread.len(), 2);
1239 assert!((spread[0] - 0.4).abs() < 1e-4); assert!((spread[1] - 0.4).abs() < 1e-4); }
1242
1243 #[test]
1244 fn test_pareto_hypervolume_2d() {
1245 let mut front = ParetoFront::new();
1246
1247 front.insert(ParetoPoint {
1248 kernel_id: "a".into(),
1249 objectives: vec![1.0, 1.0],
1250 generation: 0,
1251 });
1252
1253 let hv = front.hypervolume(&[0.0, 0.0]);
1254 assert!((hv - 1.0).abs() < 1e-4); }
1256
1257 #[test]
1260 fn test_curiosity_bonus_unvisited() {
1261 let curiosity = CuriosityBonus::new(1.41);
1262 let bucket = test_bucket("hard", "novel");
1263 let arm = ArmId("new".into());
1264
1265 let bonus = curiosity.bonus(&bucket, &arm);
1266 assert!(bonus > 0.0); }
1268
1269 #[test]
1270 fn test_curiosity_bonus_decays_with_visits() {
1271 let mut curiosity = CuriosityBonus::new(1.41);
1272 let bucket = test_bucket("easy", "test");
1273 let arm = ArmId("a".into());
1274
1275 let bonus_before = curiosity.bonus(&bucket, &arm);
1276
1277 for _ in 0..50 {
1278 curiosity.record_visit(&bucket, &arm);
1279 }
1280
1281 let bonus_after = curiosity.bonus(&bucket, &arm);
1282 assert!(bonus_after < bonus_before); }
1284
1285 #[test]
1286 fn test_curiosity_novelty_score() {
1287 let mut curiosity = CuriosityBonus::new(1.41);
1288 let explored = test_bucket("easy", "common");
1289 let novel = test_bucket("hard", "rare");
1290 let arm = ArmId("a".into());
1291
1292 for _ in 0..100 {
1293 curiosity.record_visit(&explored, &arm);
1294 }
1295 curiosity.record_visit(&novel, &arm);
1296
1297 let explored_novelty = curiosity.novelty_score(&explored);
1298 let novel_novelty = curiosity.novelty_score(&novel);
1299
1300 assert!(novel_novelty > explored_novelty);
1301 }
1302
1303 #[test]
1306 fn test_meta_engine_creation() {
1307 let engine = MetaLearningEngine::new();
1308 assert_eq!(engine.regret.total_observations, 0);
1309 assert!(engine.pareto.is_empty());
1310 assert_eq!(engine.curiosity.total_visits, 0);
1311 }
1312
1313 #[test]
1314 fn test_meta_engine_record_decision() {
1315 let mut engine = MetaLearningEngine::new();
1316 let bucket = test_bucket("medium", "algo");
1317 let arm = ArmId("greedy".into());
1318
1319 for _ in 0..50 {
1320 engine.record_decision(&bucket, &arm, 0.85);
1321 }
1322
1323 assert_eq!(engine.regret.total_observations, 50);
1324 assert_eq!(engine.curiosity.total_visits, 50);
1325 assert!(engine.decaying_mean(&bucket, &arm).unwrap() > 0.7);
1326 }
1327
1328 #[test]
1329 fn test_meta_engine_boosted_score() {
1330 let mut engine = MetaLearningEngine::new();
1331 let explored = test_bucket("easy", "common");
1332 let novel = test_bucket("hard", "rare");
1333 let arm = ArmId("a".into());
1334
1335 for _ in 0..100 {
1337 engine.record_decision(&explored, &arm, 0.8);
1338 }
1339
1340 let score_explored = engine.boosted_score(&explored, &arm, 0.5);
1341 let score_novel = engine.boosted_score(&novel, &arm, 0.5);
1342
1343 assert!(score_novel > score_explored);
1345 }
1346
1347 #[test]
1348 fn test_meta_engine_kernel_recording() {
1349 let mut engine = MetaLearningEngine::new();
1350
1351 engine.record_kernel("k1", 0.9, 0.3, 0.7, 0);
1352 engine.record_kernel("k2", 0.7, 0.1, 0.9, 0);
1353 engine.record_kernel("k3", 0.5, 0.5, 0.5, 0); assert!(engine.pareto.len() <= 2);
1357 }
1358
1359 #[test]
1360 fn test_meta_engine_health_check() {
1361 let mut engine = MetaLearningEngine::new();
1362 let bucket = test_bucket("medium", "test");
1363 let arm = ArmId("a".into());
1364
1365 for _ in 0..100 {
1366 engine.record_decision(&bucket, &arm, 0.8);
1367 }
1368
1369 let health = engine.health_check();
1370 assert_eq!(health.curiosity_total_visits, 100);
1371 assert!(health.is_exploring);
1372 }
1373
1374 #[test]
1375 fn test_meta_engine_plateau_check() {
1376 let mut engine = MetaLearningEngine::new();
1377
1378 let flat_points: Vec<CostCurvePoint> = (0..10)
1379 .map(|i| CostCurvePoint {
1380 cycle: i as u64,
1381 accuracy: 0.80,
1382 cost_per_solve: 0.1,
1383 robustness: 0.8,
1384 policy_violations: 0,
1385 timestamp: i as f64,
1386 })
1387 .collect();
1388
1389 let action = engine.check_plateau(&flat_points);
1390 assert_ne!(action, PlateauAction::Continue);
1391 }
1392
1393 #[test]
1394 fn test_meta_engine_default() {
1395 let engine = MetaLearningEngine::default();
1396 assert_eq!(engine.curiosity.exploration_coeff, 1.41);
1397 }
1398}