1use std::collections::HashMap;
9use std::fmt;
10
11#[derive(Debug, Clone, PartialEq)]
15pub enum MetaError {
16 EmptySupportSet,
18 EmptyQuerySet,
20 DimensionMismatch {
22 expected: usize,
24 got: usize,
26 },
27 NoAdaptations,
29}
30
31impl fmt::Display for MetaError {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 match self {
34 MetaError::EmptySupportSet => write!(f, "support set must not be empty"),
35 MetaError::EmptyQuerySet => write!(f, "query set must not be empty"),
36 MetaError::DimensionMismatch { expected, got } => {
37 write!(f, "dimension mismatch: expected {expected}, got {got}")
38 }
39 MetaError::NoAdaptations => write!(f, "no task adaptations provided for meta-update"),
40 }
41 }
42}
43
44impl std::error::Error for MetaError {}
45
46#[derive(Debug, Clone, PartialEq, Eq, Hash)]
50pub struct TaskId(pub String);
51
52impl TaskId {
53 pub fn new(id: impl Into<String>) -> Self {
55 TaskId(id.into())
56 }
57}
58
59impl fmt::Display for TaskId {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 write!(f, "{}", self.0)
62 }
63}
64
65#[derive(Debug, Clone)]
67pub struct TaskExample {
68 pub features: Vec<f64>,
70 pub label: f64,
72}
73
74impl TaskExample {
75 pub fn new(features: Vec<f64>, label: f64) -> Self {
77 TaskExample { features, label }
78 }
79}
80
81#[derive(Debug, Clone, PartialEq)]
83pub enum TaskType {
84 Classification {
86 n_classes: usize,
88 },
89 Regression,
91 Ranking,
93}
94
95#[derive(Debug, Clone)]
98pub struct MetaTask {
99 pub id: TaskId,
101 pub support_set: Vec<TaskExample>,
103 pub query_set: Vec<TaskExample>,
105 pub task_type: TaskType,
107}
108
109impl MetaTask {
110 pub fn new(
112 id: TaskId,
113 support_set: Vec<TaskExample>,
114 query_set: Vec<TaskExample>,
115 task_type: TaskType,
116 ) -> Self {
117 MetaTask {
118 id,
119 support_set,
120 query_set,
121 task_type,
122 }
123 }
124}
125
126#[derive(Debug, Clone)]
130pub struct MetaParameters {
131 pub weights: Vec<f64>,
133 pub bias: f64,
135 pub dims: usize,
137}
138
139impl MetaParameters {
140 pub fn zeros(dims: usize) -> Self {
142 MetaParameters {
143 weights: vec![0.0; dims],
144 bias: 0.0,
145 dims,
146 }
147 }
148}
149
150#[derive(Debug, Clone)]
152pub struct TaskAdaptation {
153 pub task_id: TaskId,
155 pub adapted_weights: Vec<f64>,
157 pub adapted_bias: f64,
159 pub support_loss: f64,
161 pub query_loss: f64,
163 pub steps: u32,
165}
166
167#[derive(Debug, Clone)]
171pub struct MetaLearnerConfig {
172 pub inner_lr: f64,
174 pub meta_lr: f64,
176 pub inner_steps: u32,
178 pub dims: usize,
180 pub seed: u64,
182}
183
184impl Default for MetaLearnerConfig {
185 fn default() -> Self {
186 MetaLearnerConfig {
187 inner_lr: 0.01,
188 meta_lr: 0.001,
189 inner_steps: 5,
190 dims: 10,
191 seed: 42,
192 }
193 }
194}
195
196#[derive(Debug, Clone)]
200pub struct MetaLearnerStats {
201 pub total_tasks: usize,
203 pub meta_steps: u64,
205 pub avg_support_loss: f64,
207 pub avg_query_loss: f64,
209 pub best_query_loss: f64,
211}
212
213#[inline]
217fn xorshift64(state: &mut u64) -> u64 {
218 *state ^= *state << 13;
219 *state ^= *state >> 7;
220 *state ^= *state << 17;
221 *state
222}
223
224pub struct MetaLearner {
240 pub config: MetaLearnerConfig,
242 pub meta_params: MetaParameters,
244 pub task_history: HashMap<TaskId, TaskAdaptation>,
246 pub meta_step: u64,
248}
249
250impl MetaLearner {
251 pub fn new(config: MetaLearnerConfig) -> Self {
256 let mut state = config.seed.max(1); let dims = config.dims;
258 let weights: Vec<f64> = (0..dims)
259 .map(|_| {
260 let raw = xorshift64(&mut state);
261 (raw as f64 / u64::MAX as f64) * 0.01
262 })
263 .collect();
264
265 let meta_params = MetaParameters {
266 weights,
267 bias: 0.0,
268 dims,
269 };
270
271 MetaLearner {
272 config,
273 meta_params,
274 task_history: HashMap::new(),
275 meta_step: 0,
276 }
277 }
278
279 pub fn loss(prediction: f64, label: f64, task_type: &TaskType) -> f64 {
289 match task_type {
290 TaskType::Classification { .. } => (1.0 - label * prediction.tanh()).max(0.0),
291 TaskType::Regression => (prediction - label).powi(2),
292 TaskType::Ranking => (1.0 - prediction * label).max(0.0),
293 }
294 }
295
296 pub fn gradient(features: &[f64], prediction: f64, label: f64) -> (Vec<f64>, f64) {
304 let residual = 2.0 * (prediction - label);
305 let dw: Vec<f64> = features.iter().map(|&x| residual * x).collect();
306 let db = residual;
307 (dw, db)
308 }
309
310 fn batch_gradient(
312 weights: &[f64],
313 bias: f64,
314 examples: &[TaskExample],
315 dims: usize,
316 ) -> Result<(Vec<f64>, f64), MetaError> {
317 if examples.is_empty() {
318 return Err(MetaError::EmptySupportSet);
319 }
320 let n = examples.len() as f64;
321 let mut dw_sum = vec![0.0f64; dims];
322 let mut db_sum = 0.0f64;
323
324 for ex in examples {
325 if ex.features.len() != dims {
326 return Err(MetaError::DimensionMismatch {
327 expected: dims,
328 got: ex.features.len(),
329 });
330 }
331 let pred = Self::linear_predict(weights, bias, &ex.features);
332 let (dw, db) = Self::gradient(&ex.features, pred, ex.label);
333 for (acc, g) in dw_sum.iter_mut().zip(dw.iter()) {
334 *acc += g;
335 }
336 db_sum += db;
337 }
338
339 let dw_avg: Vec<f64> = dw_sum.iter().map(|&v| v / n).collect();
340 Ok((dw_avg, db_sum / n))
341 }
342
343 fn linear_predict(weights: &[f64], bias: f64, features: &[f64]) -> f64 {
347 weights
348 .iter()
349 .zip(features.iter())
350 .map(|(&w, &x)| w * x)
351 .sum::<f64>()
352 + bias
353 }
354
355 pub fn predict(
364 &self,
365 features: &[f64],
366 adaptation: Option<&TaskAdaptation>,
367 ) -> Result<f64, MetaError> {
368 if features.len() != self.config.dims {
369 return Err(MetaError::DimensionMismatch {
370 expected: self.config.dims,
371 got: features.len(),
372 });
373 }
374 let (weights, bias) = match adaptation {
375 Some(a) => (a.adapted_weights.as_slice(), a.adapted_bias),
376 None => (self.meta_params.weights.as_slice(), self.meta_params.bias),
377 };
378 Ok(Self::linear_predict(weights, bias, features))
379 }
380
381 pub fn adapt_to_task(&mut self, task: &MetaTask) -> Result<TaskAdaptation, MetaError> {
394 if task.support_set.is_empty() {
395 return Err(MetaError::EmptySupportSet);
396 }
397 if task.query_set.is_empty() {
398 return Err(MetaError::EmptyQuerySet);
399 }
400
401 let dims = self.config.dims;
402 let inner_lr = self.config.inner_lr;
403 let inner_steps = self.config.inner_steps;
404
405 let mut w = self.meta_params.weights.clone();
407 let mut b = self.meta_params.bias;
408
409 for _ in 0..inner_steps {
410 let (dw, db) = Self::batch_gradient(&w, b, &task.support_set, dims)?;
412
413 for (wi, &gi) in w.iter_mut().zip(dw.iter()) {
415 *wi -= inner_lr * gi;
416 }
417 b -= inner_lr * db;
418 }
419
420 let support_loss = self.mean_loss_raw(&w, b, &task.support_set, &task.task_type)?;
422
423 let query_loss = self.mean_loss_raw(&w, b, &task.query_set, &task.task_type)?;
425
426 let adaptation = TaskAdaptation {
427 task_id: task.id.clone(),
428 adapted_weights: w,
429 adapted_bias: b,
430 support_loss,
431 query_loss,
432 steps: inner_steps,
433 };
434
435 self.task_history
436 .insert(task.id.clone(), adaptation.clone());
437 Ok(adaptation)
438 }
439
440 fn mean_loss_raw(
442 &self,
443 weights: &[f64],
444 bias: f64,
445 examples: &[TaskExample],
446 task_type: &TaskType,
447 ) -> Result<f64, MetaError> {
448 if examples.is_empty() {
449 return Err(MetaError::EmptySupportSet);
450 }
451 let mut total = 0.0;
452 for ex in examples {
453 if ex.features.len() != self.config.dims {
454 return Err(MetaError::DimensionMismatch {
455 expected: self.config.dims,
456 got: ex.features.len(),
457 });
458 }
459 let pred = Self::linear_predict(weights, bias, &ex.features);
460 total += Self::loss(pred, ex.label, task_type);
461 }
462 Ok(total / examples.len() as f64)
463 }
464
465 pub fn meta_update(&mut self, adaptations: &[TaskAdaptation]) -> Result<(), MetaError> {
477 if adaptations.is_empty() {
478 return Err(MetaError::NoAdaptations);
479 }
480
481 let dims = self.config.dims;
482 let meta_lr = self.config.meta_lr;
483 let n = adaptations.len() as f64;
484
485 let mut avg_w = vec![0.0f64; dims];
487 let mut avg_b = 0.0f64;
488
489 for a in adaptations {
490 if a.adapted_weights.len() != dims {
491 return Err(MetaError::DimensionMismatch {
492 expected: dims,
493 got: a.adapted_weights.len(),
494 });
495 }
496 for (acc, &v) in avg_w.iter_mut().zip(a.adapted_weights.iter()) {
497 *acc += v;
498 }
499 avg_b += a.adapted_bias;
500 }
501 for v in avg_w.iter_mut() {
502 *v /= n;
503 }
504 avg_b /= n;
505
506 for (mw, &aw) in self.meta_params.weights.iter_mut().zip(avg_w.iter()) {
509 let meta_grad = *mw - aw; *mw -= meta_lr * meta_grad;
511 }
512 self.meta_params.bias -= meta_lr * (self.meta_params.bias - avg_b);
513
514 self.meta_step += 1;
515 Ok(())
516 }
517
518 pub fn task_similarity(a: &TaskAdaptation, b: &TaskAdaptation) -> f64 {
521 let dot: f64 = a
522 .adapted_weights
523 .iter()
524 .zip(b.adapted_weights.iter())
525 .map(|(&x, &y)| x * y)
526 .sum();
527 let norm_a: f64 = a.adapted_weights.iter().map(|&x| x * x).sum::<f64>().sqrt();
528 let norm_b: f64 = b.adapted_weights.iter().map(|&x| x * x).sum::<f64>().sqrt();
529 let denom = norm_a * norm_b;
530 if denom == 0.0 {
531 0.0
532 } else {
533 (dot / denom).clamp(-1.0, 1.0)
534 }
535 }
536
537 pub fn best_task(&self) -> Option<(&TaskId, &TaskAdaptation)> {
540 self.task_history.iter().min_by(|(_, a), (_, b)| {
541 a.query_loss
542 .partial_cmp(&b.query_loss)
543 .unwrap_or(std::cmp::Ordering::Equal)
544 })
545 }
546
547 pub fn reset_task(&mut self, task_id: &TaskId) {
549 self.task_history.remove(task_id);
550 }
551
552 pub fn stats(&self) -> MetaLearnerStats {
554 let total_tasks = self.task_history.len();
555 if total_tasks == 0 {
556 return MetaLearnerStats {
557 total_tasks: 0,
558 meta_steps: self.meta_step,
559 avg_support_loss: 0.0,
560 avg_query_loss: 0.0,
561 best_query_loss: f64::INFINITY,
562 };
563 }
564
565 let mut sum_support = 0.0;
566 let mut sum_query = 0.0;
567 let mut best = f64::INFINITY;
568
569 for a in self.task_history.values() {
570 sum_support += a.support_loss;
571 sum_query += a.query_loss;
572 if a.query_loss < best {
573 best = a.query_loss;
574 }
575 }
576
577 MetaLearnerStats {
578 total_tasks,
579 meta_steps: self.meta_step,
580 avg_support_loss: sum_support / total_tasks as f64,
581 avg_query_loss: sum_query / total_tasks as f64,
582 best_query_loss: best,
583 }
584 }
585}
586
587#[cfg(test)]
590mod tests {
591 use crate::meta_learner::{
592 xorshift64, MetaError, MetaLearner, MetaLearnerConfig, MetaParameters, MetaTask,
593 TaskAdaptation, TaskExample, TaskId, TaskType,
594 };
595
596 fn simple_config(dims: usize) -> MetaLearnerConfig {
599 MetaLearnerConfig {
600 inner_lr: 0.1,
601 meta_lr: 0.01,
602 inner_steps: 3,
603 dims,
604 seed: 7,
605 }
606 }
607
608 fn make_regression_task(id: &str, dims: usize, n_support: usize, n_query: usize) -> MetaTask {
609 let support_set: Vec<TaskExample> = (0..n_support)
610 .map(|i| {
611 let features: Vec<f64> = (0..dims).map(|j| (i + j) as f64 * 0.1).collect();
612 let label = features.iter().sum::<f64>(); TaskExample::new(features, label)
614 })
615 .collect();
616 let query_set: Vec<TaskExample> = (n_support..n_support + n_query)
617 .map(|i| {
618 let features: Vec<f64> = (0..dims).map(|j| (i + j) as f64 * 0.1).collect();
619 let label = features.iter().sum::<f64>();
620 TaskExample::new(features, label)
621 })
622 .collect();
623 MetaTask::new(
624 TaskId::new(id),
625 support_set,
626 query_set,
627 TaskType::Regression,
628 )
629 }
630
631 fn make_classification_task(id: &str, dims: usize) -> MetaTask {
632 let make_ex = |v: f64| TaskExample::new(vec![v; dims], if v > 0.0 { 1.0 } else { -1.0 });
633 MetaTask::new(
634 TaskId::new(id),
635 vec![make_ex(0.5), make_ex(-0.5)],
636 vec![make_ex(0.3), make_ex(-0.3)],
637 TaskType::Classification { n_classes: 2 },
638 )
639 }
640
641 fn make_ranking_task(id: &str, dims: usize) -> MetaTask {
642 let make_ex = |v: f64| TaskExample::new(vec![v; dims], if v > 0.5 { 1.0 } else { -1.0 });
643 MetaTask::new(
644 TaskId::new(id),
645 vec![make_ex(0.9), make_ex(0.1)],
646 vec![make_ex(0.8), make_ex(0.2)],
647 TaskType::Ranking,
648 )
649 }
650
651 #[test]
654 fn test_task_id_new() {
655 let id = TaskId::new("task_1");
656 assert_eq!(id.0, "task_1");
657 }
658
659 #[test]
660 fn test_task_id_display() {
661 let id = TaskId::new("hello");
662 assert_eq!(format!("{id}"), "hello");
663 }
664
665 #[test]
666 fn test_task_id_equality() {
667 assert_eq!(TaskId::new("a"), TaskId::new("a"));
668 assert_ne!(TaskId::new("a"), TaskId::new("b"));
669 }
670
671 #[test]
672 fn test_task_id_hash_in_map() {
673 let mut map = std::collections::HashMap::new();
674 map.insert(TaskId::new("k"), 42u32);
675 assert_eq!(map[&TaskId::new("k")], 42);
676 }
677
678 #[test]
681 fn test_task_example_new() {
682 let ex = TaskExample::new(vec![1.0, 2.0], 3.0);
683 assert_eq!(ex.features.len(), 2);
684 assert!((ex.label - 3.0).abs() < 1e-12);
685 }
686
687 #[test]
690 fn test_meta_parameters_zeros() {
691 let p = MetaParameters::zeros(5);
692 assert_eq!(p.dims, 5);
693 assert_eq!(p.weights.len(), 5);
694 assert!(p.weights.iter().all(|&w| w == 0.0));
695 assert_eq!(p.bias, 0.0);
696 }
697
698 #[test]
701 fn test_config_defaults() {
702 let cfg = MetaLearnerConfig::default();
703 assert!((cfg.inner_lr - 0.01).abs() < 1e-12);
704 assert!((cfg.meta_lr - 0.001).abs() < 1e-12);
705 assert_eq!(cfg.inner_steps, 5);
706 assert_eq!(cfg.dims, 10);
707 assert_eq!(cfg.seed, 42);
708 }
709
710 #[test]
713 fn test_xorshift64_non_zero() {
714 let mut state = 1u64;
715 for _ in 0..100 {
716 let v = xorshift64(&mut state);
717 assert_ne!(v, 0, "xorshift64 must never produce 0");
718 }
719 }
720
721 #[test]
722 fn test_xorshift64_deterministic() {
723 let mut s1 = 42u64;
724 let mut s2 = 42u64;
725 for _ in 0..50 {
726 assert_eq!(xorshift64(&mut s1), xorshift64(&mut s2));
727 }
728 }
729
730 #[test]
733 fn test_new_initialises_weights() {
734 let cfg = simple_config(4);
735 let ml = MetaLearner::new(cfg);
736 assert_eq!(ml.meta_params.weights.len(), 4);
737 for &w in &ml.meta_params.weights {
739 assert!((0.0..0.01).contains(&w), "weight out of range: {w}");
740 }
741 }
742
743 #[test]
744 fn test_new_bias_is_zero() {
745 let ml = MetaLearner::new(simple_config(3));
746 assert_eq!(ml.meta_params.bias, 0.0);
747 }
748
749 #[test]
750 fn test_new_history_empty() {
751 let ml = MetaLearner::new(simple_config(3));
752 assert!(ml.task_history.is_empty());
753 }
754
755 #[test]
756 fn test_new_meta_step_zero() {
757 let ml = MetaLearner::new(simple_config(3));
758 assert_eq!(ml.meta_step, 0);
759 }
760
761 #[test]
762 fn test_new_seed_determines_weights() {
763 let cfg1 = MetaLearnerConfig {
764 seed: 99,
765 ..simple_config(5)
766 };
767 let cfg2 = MetaLearnerConfig {
768 seed: 99,
769 ..simple_config(5)
770 };
771 let ml1 = MetaLearner::new(cfg1);
772 let ml2 = MetaLearner::new(cfg2);
773 assert_eq!(ml1.meta_params.weights, ml2.meta_params.weights);
774 }
775
776 #[test]
779 fn test_loss_regression_zero_residual() {
780 let l = MetaLearner::loss(2.0, 2.0, &TaskType::Regression);
781 assert!(l.abs() < 1e-12);
782 }
783
784 #[test]
785 fn test_loss_regression_positive() {
786 let l = MetaLearner::loss(3.0, 1.0, &TaskType::Regression);
787 assert!((l - 4.0).abs() < 1e-10);
788 }
789
790 #[test]
791 fn test_loss_classification_correct_sign() {
792 let l = MetaLearner::loss(5.0, 1.0, &TaskType::Classification { n_classes: 2 });
794 assert!(l < 0.01);
795 }
796
797 #[test]
798 fn test_loss_classification_wrong_sign() {
799 let l = MetaLearner::loss(5.0, -1.0, &TaskType::Classification { n_classes: 2 });
801 assert!(l > 0.5);
802 }
803
804 #[test]
805 fn test_loss_classification_non_negative() {
806 for pred in [-2.0, 0.0, 2.0] {
807 for label in [-1.0, 1.0] {
808 let l = MetaLearner::loss(pred, label, &TaskType::Classification { n_classes: 3 });
809 assert!(l >= 0.0, "loss was {l}");
810 }
811 }
812 }
813
814 #[test]
815 fn test_loss_ranking_margin_satisfied() {
816 let l = MetaLearner::loss(2.0, 1.0, &TaskType::Ranking);
818 assert_eq!(l, 0.0);
819 }
820
821 #[test]
822 fn test_loss_ranking_margin_violated() {
823 let l = MetaLearner::loss(-1.0, 1.0, &TaskType::Ranking);
825 assert!(l > 0.0);
826 }
827
828 #[test]
831 fn test_gradient_zero_residual() {
832 let (dw, db) = MetaLearner::gradient(&[1.0, 2.0], 3.0, 3.0);
833 assert!(dw.iter().all(|&g| g.abs() < 1e-12));
834 assert!(db.abs() < 1e-12);
835 }
836
837 #[test]
838 fn test_gradient_direction() {
839 let (dw, db) = MetaLearner::gradient(&[1.0], 2.0, 1.0);
841 assert!(dw[0] > 0.0);
842 assert!(db > 0.0);
843 }
844
845 #[test]
846 fn test_gradient_length_matches_features() {
847 let features = vec![0.1, 0.2, 0.3, 0.4];
848 let (dw, _) = MetaLearner::gradient(&features, 1.0, 0.0);
849 assert_eq!(dw.len(), features.len());
850 }
851
852 #[test]
855 fn test_predict_zero_weights() {
856 let ml = MetaLearner::new(MetaLearnerConfig {
857 seed: 1,
858 dims: 3,
859 ..MetaLearnerConfig::default()
860 });
861 let features = vec![1.0, 2.0, 3.0];
863 let adaptation = TaskAdaptation {
866 task_id: TaskId::new("t"),
867 adapted_weights: vec![0.0, 0.0, 0.0],
868 adapted_bias: 5.0,
869 support_loss: 0.0,
870 query_loss: 0.0,
871 steps: 0,
872 };
873 let result = ml
874 .predict(&features, Some(&adaptation))
875 .expect("predict should succeed");
876 assert!((result - 5.0).abs() < 1e-12);
877 }
878
879 #[test]
880 fn test_predict_uses_adaptation_weights() {
881 let ml = MetaLearner::new(simple_config(2));
882 let adaptation = TaskAdaptation {
883 task_id: TaskId::new("t"),
884 adapted_weights: vec![1.0, 2.0],
885 adapted_bias: 0.5,
886 support_loss: 0.0,
887 query_loss: 0.0,
888 steps: 0,
889 };
890 let result = ml.predict(&[3.0, 4.0], Some(&adaptation)).expect("ok");
892 assert!((result - 11.5).abs() < 1e-10);
893 }
894
895 #[test]
896 fn test_predict_dimension_mismatch() {
897 let ml = MetaLearner::new(simple_config(3));
898 let err = ml.predict(&[1.0, 2.0], None).unwrap_err();
899 assert_eq!(
900 err,
901 MetaError::DimensionMismatch {
902 expected: 3,
903 got: 2
904 }
905 );
906 }
907
908 #[test]
911 fn test_adapt_regression_task_stores_history() {
912 let mut ml = MetaLearner::new(simple_config(3));
913 let task = make_regression_task("t1", 3, 4, 2);
914 ml.adapt_to_task(&task).expect("adapt should succeed");
915 assert!(ml.task_history.contains_key(&TaskId::new("t1")));
916 }
917
918 #[test]
919 fn test_adapt_returns_correct_task_id() {
920 let mut ml = MetaLearner::new(simple_config(3));
921 let task = make_regression_task("my_task", 3, 3, 2);
922 let adaptation = ml.adapt_to_task(&task).expect("ok");
923 assert_eq!(adaptation.task_id, TaskId::new("my_task"));
924 }
925
926 #[test]
927 fn test_adapt_steps_count() {
928 let cfg = MetaLearnerConfig {
929 inner_steps: 7,
930 ..simple_config(3)
931 };
932 let mut ml = MetaLearner::new(cfg);
933 let task = make_regression_task("t", 3, 3, 2);
934 let a = ml.adapt_to_task(&task).expect("ok");
935 assert_eq!(a.steps, 7);
936 }
937
938 #[test]
939 fn test_adapt_empty_support_set_error() {
940 let mut ml = MetaLearner::new(simple_config(3));
941 let task = MetaTask::new(
942 TaskId::new("empty"),
943 vec![],
944 vec![TaskExample::new(vec![0.0, 0.0, 0.0], 0.0)],
945 TaskType::Regression,
946 );
947 assert_eq!(
948 ml.adapt_to_task(&task).unwrap_err(),
949 MetaError::EmptySupportSet
950 );
951 }
952
953 #[test]
954 fn test_adapt_empty_query_set_error() {
955 let mut ml = MetaLearner::new(simple_config(3));
956 let task = MetaTask::new(
957 TaskId::new("empty_q"),
958 vec![TaskExample::new(vec![0.0, 0.0, 0.0], 0.0)],
959 vec![],
960 TaskType::Regression,
961 );
962 assert_eq!(
963 ml.adapt_to_task(&task).unwrap_err(),
964 MetaError::EmptyQuerySet
965 );
966 }
967
968 #[test]
969 fn test_adapt_dimension_mismatch_error() {
970 let mut ml = MetaLearner::new(simple_config(3));
971 let task = MetaTask::new(
972 TaskId::new("bad_dim"),
973 vec![TaskExample::new(vec![1.0, 2.0], 0.0)], vec![TaskExample::new(vec![1.0, 2.0, 3.0], 0.0)],
975 TaskType::Regression,
976 );
977 assert!(matches!(
978 ml.adapt_to_task(&task).unwrap_err(),
979 MetaError::DimensionMismatch {
980 expected: 3,
981 got: 2
982 }
983 ));
984 }
985
986 #[test]
987 fn test_adapt_classification_task() {
988 let mut ml = MetaLearner::new(simple_config(2));
989 let task = make_classification_task("cls", 2);
990 let a = ml.adapt_to_task(&task).expect("ok");
991 assert!(a.support_loss >= 0.0);
992 assert!(a.query_loss >= 0.0);
993 }
994
995 #[test]
996 fn test_adapt_ranking_task() {
997 let mut ml = MetaLearner::new(simple_config(2));
998 let task = make_ranking_task("rnk", 2);
999 let a = ml.adapt_to_task(&task).expect("ok");
1000 assert!(a.support_loss >= 0.0);
1001 assert!(a.query_loss >= 0.0);
1002 }
1003
1004 #[test]
1005 fn test_adapt_does_not_change_meta_params() {
1006 let mut ml = MetaLearner::new(simple_config(3));
1007 let before = ml.meta_params.weights.clone();
1008 let task = make_regression_task("t", 3, 3, 2);
1009 ml.adapt_to_task(&task).expect("ok");
1010 assert_eq!(ml.meta_params.weights, before);
1011 }
1012
1013 #[test]
1016 fn test_meta_update_increments_step() {
1017 let mut ml = MetaLearner::new(simple_config(3));
1018 let task = make_regression_task("t", 3, 3, 2);
1019 let a = ml.adapt_to_task(&task).expect("ok");
1020 ml.meta_update(&[a]).expect("ok");
1021 assert_eq!(ml.meta_step, 1);
1022 }
1023
1024 #[test]
1025 fn test_meta_update_empty_error() {
1026 let mut ml = MetaLearner::new(simple_config(3));
1027 assert_eq!(ml.meta_update(&[]).unwrap_err(), MetaError::NoAdaptations);
1028 }
1029
1030 #[test]
1031 fn test_meta_update_moves_weights_toward_adapted() {
1032 let cfg = MetaLearnerConfig {
1033 inner_lr: 0.1,
1034 meta_lr: 1.0, dims: 2,
1036 ..MetaLearnerConfig::default()
1037 };
1038 let mut ml = MetaLearner::new(cfg);
1039 ml.meta_params.weights = vec![0.0; 2];
1041 ml.meta_params.bias = 0.0;
1042
1043 let adaptation = TaskAdaptation {
1044 task_id: TaskId::new("t"),
1045 adapted_weights: vec![1.0, 1.0],
1046 adapted_bias: 1.0,
1047 support_loss: 0.0,
1048 query_loss: 0.0,
1049 steps: 1,
1050 };
1051 ml.meta_update(&[adaptation]).expect("ok");
1052 assert!((ml.meta_params.weights[0] - 1.0).abs() < 1e-10);
1054 assert!((ml.meta_params.bias - 1.0).abs() < 1e-10);
1055 }
1056
1057 #[test]
1058 fn test_meta_update_dimension_mismatch() {
1059 let mut ml = MetaLearner::new(simple_config(3));
1060 let bad = TaskAdaptation {
1061 task_id: TaskId::new("bad"),
1062 adapted_weights: vec![1.0, 2.0], adapted_bias: 0.0,
1064 support_loss: 0.0,
1065 query_loss: 0.0,
1066 steps: 1,
1067 };
1068 assert!(matches!(
1069 ml.meta_update(&[bad]).unwrap_err(),
1070 MetaError::DimensionMismatch {
1071 expected: 3,
1072 got: 2
1073 }
1074 ));
1075 }
1076
1077 #[test]
1080 fn test_task_similarity_identical() {
1081 let a = TaskAdaptation {
1082 task_id: TaskId::new("a"),
1083 adapted_weights: vec![1.0, 0.0, 1.0],
1084 adapted_bias: 0.0,
1085 support_loss: 0.0,
1086 query_loss: 0.0,
1087 steps: 1,
1088 };
1089 let sim = MetaLearner::task_similarity(&a, &a);
1090 assert!((sim - 1.0).abs() < 1e-10);
1091 }
1092
1093 #[test]
1094 fn test_task_similarity_orthogonal() {
1095 let a = TaskAdaptation {
1096 task_id: TaskId::new("a"),
1097 adapted_weights: vec![1.0, 0.0],
1098 adapted_bias: 0.0,
1099 support_loss: 0.0,
1100 query_loss: 0.0,
1101 steps: 1,
1102 };
1103 let b = TaskAdaptation {
1104 task_id: TaskId::new("b"),
1105 adapted_weights: vec![0.0, 1.0],
1106 adapted_bias: 0.0,
1107 support_loss: 0.0,
1108 query_loss: 0.0,
1109 steps: 1,
1110 };
1111 let sim = MetaLearner::task_similarity(&a, &b);
1112 assert!(sim.abs() < 1e-10);
1113 }
1114
1115 #[test]
1116 fn test_task_similarity_opposite() {
1117 let a = TaskAdaptation {
1118 task_id: TaskId::new("a"),
1119 adapted_weights: vec![1.0, 0.0],
1120 adapted_bias: 0.0,
1121 support_loss: 0.0,
1122 query_loss: 0.0,
1123 steps: 1,
1124 };
1125 let b = TaskAdaptation {
1126 task_id: TaskId::new("b"),
1127 adapted_weights: vec![-1.0, 0.0],
1128 adapted_bias: 0.0,
1129 support_loss: 0.0,
1130 query_loss: 0.0,
1131 steps: 1,
1132 };
1133 let sim = MetaLearner::task_similarity(&a, &b);
1134 assert!((sim + 1.0).abs() < 1e-10);
1135 }
1136
1137 #[test]
1138 fn test_task_similarity_zero_vector() {
1139 let a = TaskAdaptation {
1140 task_id: TaskId::new("a"),
1141 adapted_weights: vec![0.0, 0.0],
1142 adapted_bias: 0.0,
1143 support_loss: 0.0,
1144 query_loss: 0.0,
1145 steps: 1,
1146 };
1147 let sim = MetaLearner::task_similarity(&a, &a);
1148 assert_eq!(sim, 0.0);
1149 }
1150
1151 #[test]
1154 fn test_best_task_empty_history() {
1155 let ml = MetaLearner::new(simple_config(3));
1156 assert!(ml.best_task().is_none());
1157 }
1158
1159 #[test]
1160 fn test_best_task_returns_lowest_query_loss() {
1161 let mut ml = MetaLearner::new(simple_config(3));
1162 for (id, ql) in [("t1", 0.5), ("t2", 0.1), ("t3", 0.8)] {
1163 ml.task_history.insert(
1164 TaskId::new(id),
1165 TaskAdaptation {
1166 task_id: TaskId::new(id),
1167 adapted_weights: vec![0.0; 3],
1168 adapted_bias: 0.0,
1169 support_loss: 0.0,
1170 query_loss: ql,
1171 steps: 1,
1172 },
1173 );
1174 }
1175 let (best_id, best_a) = ml.best_task().expect("should have a best task");
1176 assert_eq!(best_id, &TaskId::new("t2"));
1177 assert!((best_a.query_loss - 0.1).abs() < 1e-10);
1178 }
1179
1180 #[test]
1183 fn test_reset_task_removes_entry() {
1184 let mut ml = MetaLearner::new(simple_config(3));
1185 let task = make_regression_task("to_remove", 3, 3, 2);
1186 ml.adapt_to_task(&task).expect("ok");
1187 assert!(ml.task_history.contains_key(&TaskId::new("to_remove")));
1188 ml.reset_task(&TaskId::new("to_remove"));
1189 assert!(!ml.task_history.contains_key(&TaskId::new("to_remove")));
1190 }
1191
1192 #[test]
1193 fn test_reset_task_nonexistent_is_noop() {
1194 let mut ml = MetaLearner::new(simple_config(3));
1195 ml.reset_task(&TaskId::new("ghost")); }
1197
1198 #[test]
1201 fn test_stats_empty() {
1202 let ml = MetaLearner::new(simple_config(3));
1203 let s = ml.stats();
1204 assert_eq!(s.total_tasks, 0);
1205 assert_eq!(s.meta_steps, 0);
1206 assert_eq!(s.avg_support_loss, 0.0);
1207 assert_eq!(s.avg_query_loss, 0.0);
1208 assert!(s.best_query_loss.is_infinite());
1209 }
1210
1211 #[test]
1212 fn test_stats_after_adapt() {
1213 let mut ml = MetaLearner::new(simple_config(3));
1214 let t1 = make_regression_task("t1", 3, 4, 2);
1215 let t2 = make_regression_task("t2", 3, 4, 2);
1216 ml.adapt_to_task(&t1).expect("ok");
1217 ml.adapt_to_task(&t2).expect("ok");
1218 let s = ml.stats();
1219 assert_eq!(s.total_tasks, 2);
1220 }
1221
1222 #[test]
1223 fn test_stats_best_query_loss_decreases() {
1224 let mut ml = MetaLearner::new(simple_config(3));
1225 ml.task_history.insert(
1226 TaskId::new("t1"),
1227 TaskAdaptation {
1228 task_id: TaskId::new("t1"),
1229 adapted_weights: vec![0.0; 3],
1230 adapted_bias: 0.0,
1231 support_loss: 1.0,
1232 query_loss: 0.3,
1233 steps: 1,
1234 },
1235 );
1236 let s = ml.stats();
1237 assert!((s.best_query_loss - 0.3).abs() < 1e-10);
1238 }
1239
1240 #[test]
1243 fn test_meta_error_display() {
1244 assert!(!MetaError::EmptySupportSet.to_string().is_empty());
1245 assert!(!MetaError::EmptyQuerySet.to_string().is_empty());
1246 assert!(!MetaError::NoAdaptations.to_string().is_empty());
1247 assert!(!MetaError::DimensionMismatch {
1248 expected: 3,
1249 got: 2
1250 }
1251 .to_string()
1252 .is_empty());
1253 }
1254
1255 #[test]
1258 fn test_full_maml_cycle() {
1259 let mut ml = MetaLearner::new(simple_config(4));
1260 let tasks: Vec<MetaTask> = (0..3)
1261 .map(|i| make_regression_task(&format!("task_{i}"), 4, 5, 3))
1262 .collect();
1263
1264 let adaptations: Vec<_> = tasks
1265 .iter()
1266 .map(|t| ml.adapt_to_task(t).expect("adapt ok"))
1267 .collect();
1268
1269 ml.meta_update(&adaptations).expect("meta_update ok");
1270
1271 assert_eq!(ml.meta_step, 1);
1272 assert_eq!(ml.task_history.len(), 3);
1273
1274 let s = ml.stats();
1275 assert_eq!(s.total_tasks, 3);
1276 assert_eq!(s.meta_steps, 1);
1277 assert!(s.best_query_loss < f64::INFINITY);
1278 }
1279}