1use std::collections::HashMap;
13use std::fmt;
14
15#[inline]
19fn xorshift64(state: &mut u64) -> u64 {
20 let mut x = *state;
21 x ^= x << 13;
22 x ^= x >> 7;
23 x ^= x << 17;
24 *state = x;
25 x
26}
27
28#[inline]
30fn xorshift_f64(state: &mut u64) -> f64 {
31 (xorshift64(state) >> 11) as f64 / (1u64 << 53) as f64
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, Hash)]
41pub struct TaskId(pub String);
42
43impl TaskId {
44 pub fn new(id: impl Into<String>) -> Self {
46 TaskId(id.into())
47 }
48
49 pub fn as_str(&self) -> &str {
51 &self.0
52 }
53}
54
55impl fmt::Display for TaskId {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 write!(f, "{}", self.0)
58 }
59}
60
61#[derive(Debug, Clone)]
67pub struct TaskExample {
68 pub features: Vec<f64>,
70 pub label: f64,
72 pub task_id: TaskId,
74}
75
76impl TaskExample {
77 pub fn new(features: Vec<f64>, label: f64, task_id: TaskId) -> Self {
79 TaskExample {
80 features,
81 label,
82 task_id,
83 }
84 }
85}
86
87#[derive(Debug, Clone)]
91pub struct ModelParams {
92 pub weights: Vec<f64>,
94 pub bias: f64,
96 pub dim: usize,
98}
99
100impl ModelParams {
101 pub fn zeros(dim: usize) -> Self {
103 ModelParams {
104 weights: vec![0.0; dim],
105 bias: 0.0,
106 dim,
107 }
108 }
109
110 fn predict(&self, x: &[f64]) -> f64 {
112 let dot: f64 = self
113 .weights
114 .iter()
115 .zip(x.iter())
116 .map(|(w, xi)| w * xi)
117 .sum();
118 dot + self.bias
119 }
120
121 fn mse_and_grads(&self, examples: &[TaskExample]) -> (f64, Vec<f64>, f64) {
123 let n = examples.len() as f64;
124 let mut grad_w = vec![0.0; self.dim];
125 let mut grad_b = 0.0_f64;
126 let mut loss = 0.0_f64;
127
128 for ex in examples {
129 let pred = self.predict(&ex.features);
130 let residual = pred - ex.label;
131 loss += residual * residual;
132 let coeff = 2.0 * residual / n;
133 for (gw, xi) in grad_w.iter_mut().zip(ex.features.iter()) {
134 *gw += coeff * xi;
135 }
136 grad_b += coeff;
137 }
138 loss /= n;
139 (loss, grad_w, grad_b)
140 }
141}
142
143#[derive(Debug, Clone)]
147pub struct AdaptationStep {
148 pub params: ModelParams,
150 pub loss: f64,
152 pub gradient: Vec<f64>,
154 pub step_num: usize,
156}
157
158#[derive(Debug, Clone)]
164pub struct MetaTask {
165 pub id: TaskId,
167 pub support_set: Vec<TaskExample>,
169 pub query_set: Vec<TaskExample>,
171 pub adapted_params: Option<ModelParams>,
173}
174
175impl MetaTask {
176 pub fn new(id: TaskId, support_set: Vec<TaskExample>, query_set: Vec<TaskExample>) -> Self {
178 MetaTask {
179 id,
180 support_set,
181 query_set,
182 adapted_params: None,
183 }
184 }
185
186 pub fn feature_dim(&self) -> Option<usize> {
189 self.support_set.first().map(|ex| ex.features.len())
190 }
191}
192
193#[derive(Debug, Clone)]
197pub enum MetaAlgorithm {
198 MAML {
200 inner_lr: f64,
202 inner_steps: u8,
204 },
205 ProtoNet,
207 Reptile {
209 step_size: f64,
211 },
212 FOMAML {
214 inner_lr: f64,
216 },
217}
218
219#[derive(Debug, Clone)]
223pub struct OptimizerConfig {
224 pub algorithm: MetaAlgorithm,
226 pub meta_lr: f64,
228 pub n_tasks_per_batch: usize,
230 pub max_params_dim: usize,
232}
233
234impl OptimizerConfig {
235 pub fn default_maml(dim: usize) -> Self {
237 OptimizerConfig {
238 algorithm: MetaAlgorithm::MAML {
239 inner_lr: 0.01,
240 inner_steps: 5,
241 },
242 meta_lr: 0.001,
243 n_tasks_per_batch: 4,
244 max_params_dim: dim,
245 }
246 }
247
248 pub fn default_reptile(dim: usize) -> Self {
250 OptimizerConfig {
251 algorithm: MetaAlgorithm::Reptile { step_size: 0.1 },
252 meta_lr: 0.001,
253 n_tasks_per_batch: 4,
254 max_params_dim: dim,
255 }
256 }
257}
258
259#[derive(Debug, Clone, Default)]
263pub struct MetaStats {
264 pub tasks_trained: u64,
266 pub meta_updates: u64,
268 pub avg_adaptation_loss: f64,
270 pub avg_query_loss: f64,
272 pub convergence_delta: f64,
274}
275
276#[derive(Debug, Clone, PartialEq)]
282pub enum MetaError {
283 InsufficientTasks(usize),
285 DimensionMismatch {
287 expected: usize,
289 got: usize,
291 },
292 AdaptationFailed(String),
294 InvalidConfig(String),
296}
297
298impl fmt::Display for MetaError {
299 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
300 match self {
301 MetaError::InsufficientTasks(n) => {
302 write!(f, "insufficient tasks: need at least {n}")
303 }
304 MetaError::DimensionMismatch { expected, got } => {
305 write!(f, "dimension mismatch: expected {expected}, got {got}")
306 }
307 MetaError::AdaptationFailed(msg) => write!(f, "adaptation failed: {msg}"),
308 MetaError::InvalidConfig(msg) => write!(f, "invalid config: {msg}"),
309 }
310 }
311}
312
313impl std::error::Error for MetaError {}
314
315pub struct MetaLearningOptimizer {
345 config: OptimizerConfig,
346 tasks: HashMap<TaskId, MetaTask>,
347 feature_dim: Option<usize>,
349 stats: MetaStats,
350 adaptation_loss_sum: f64,
352 adaptation_loss_count: u64,
354 query_loss_sum: f64,
356 query_loss_count: u64,
358 prev_query_loss: Option<f64>,
360}
361
362impl MetaLearningOptimizer {
363 pub fn new(config: OptimizerConfig) -> Self {
365 MetaLearningOptimizer {
366 config,
367 tasks: HashMap::new(),
368 feature_dim: None,
369 stats: MetaStats::default(),
370 adaptation_loss_sum: 0.0,
371 adaptation_loss_count: 0,
372 query_loss_sum: 0.0,
373 query_loss_count: 0,
374 prev_query_loss: None,
375 }
376 }
377
378 pub fn add_task(&mut self, task: MetaTask) -> Result<(), MetaError> {
385 if let Some(dim) = task.feature_dim() {
387 match self.feature_dim {
388 None => {
389 if dim > self.config.max_params_dim {
390 return Err(MetaError::InvalidConfig(format!(
391 "feature dim {dim} exceeds max_params_dim {}",
392 self.config.max_params_dim
393 )));
394 }
395 self.feature_dim = Some(dim);
396 }
397 Some(expected) => {
398 if dim != expected {
399 return Err(MetaError::DimensionMismatch { expected, got: dim });
400 }
401 }
402 }
403 }
404 for qex in &task.query_set {
406 let got = qex.features.len();
407 if let Some(expected) = self.feature_dim {
408 if got != expected {
409 return Err(MetaError::DimensionMismatch { expected, got });
410 }
411 }
412 }
413 self.tasks.insert(task.id.clone(), task);
414 self.stats.tasks_trained += 1;
415 Ok(())
416 }
417
418 pub fn adapt_to_task(
424 &self,
425 task_id: &TaskId,
426 init_params: &ModelParams,
427 steps: u8,
428 lr: f64,
429 ) -> Result<Vec<AdaptationStep>, MetaError> {
430 let task = self
431 .tasks
432 .get(task_id)
433 .ok_or_else(|| MetaError::AdaptationFailed(format!("unknown task: {task_id}")))?;
434
435 if task.support_set.is_empty() {
436 return Err(MetaError::AdaptationFailed(
437 "support set is empty".to_string(),
438 ));
439 }
440
441 let expected_dim = init_params.dim;
443 for ex in &task.support_set {
444 let got = ex.features.len();
445 if got != expected_dim {
446 return Err(MetaError::DimensionMismatch {
447 expected: expected_dim,
448 got,
449 });
450 }
451 }
452
453 let mut params = init_params.clone();
454 let mut history = Vec::with_capacity(steps as usize);
455
456 for step in 0..steps {
457 let (loss, grad_w, grad_b) = params.mse_and_grads(&task.support_set);
458
459 for (w, gw) in params.weights.iter_mut().zip(grad_w.iter()) {
461 *w -= lr * gw;
462 }
463 params.bias -= lr * grad_b;
464
465 history.push(AdaptationStep {
466 params: params.clone(),
467 loss,
468 gradient: grad_w,
469 step_num: step as usize,
470 });
471 }
472
473 Ok(history)
474 }
475
476 pub fn meta_update(
489 &mut self,
490 task_ids: &[TaskId],
491 current_params: &ModelParams,
492 ) -> Result<ModelParams, MetaError> {
493 if task_ids.is_empty() {
494 return Err(MetaError::InsufficientTasks(1));
495 }
496 let dim = current_params.dim;
497
498 let result = match &self.config.algorithm.clone() {
499 MetaAlgorithm::MAML {
500 inner_lr,
501 inner_steps,
502 } => self.meta_update_maml(task_ids, current_params, *inner_lr, *inner_steps, dim)?,
503 MetaAlgorithm::FOMAML { inner_lr } => {
504 self.meta_update_fomaml(task_ids, current_params, *inner_lr, dim)?
505 }
506 MetaAlgorithm::Reptile { step_size } => {
507 self.meta_update_reptile(task_ids, current_params, *step_size, dim)?
508 }
509 MetaAlgorithm::ProtoNet => self.meta_update_protonet(task_ids, current_params, dim)?,
510 };
511
512 let avg_q = self.compute_avg_query_loss(task_ids, &result);
514 self.query_loss_sum += avg_q;
515 self.query_loss_count += 1;
516 let new_avg = self.query_loss_sum / self.query_loss_count as f64;
517 let delta = match self.prev_query_loss {
518 Some(prev) => (new_avg - prev).abs(),
519 None => 0.0,
520 };
521 self.prev_query_loss = Some(new_avg);
522 self.stats.avg_query_loss = new_avg;
523 self.stats.convergence_delta = delta;
524 self.stats.meta_updates += 1;
525
526 Ok(result)
527 }
528
529 pub fn evaluate_task(&self, task_id: &TaskId, params: &ModelParams) -> Result<f64, MetaError> {
533 let task = self
534 .tasks
535 .get(task_id)
536 .ok_or_else(|| MetaError::AdaptationFailed(format!("unknown task: {task_id}")))?;
537
538 if task.query_set.is_empty() {
539 return Err(MetaError::AdaptationFailed(
540 "query set is empty".to_string(),
541 ));
542 }
543
544 let (loss, _, _) = params.mse_and_grads(&task.query_set);
545 Ok(loss)
546 }
547
548 pub fn initialize_params(dim: usize, seed: u64) -> ModelParams {
553 let mut state = if seed == 0 { 0xdeadbeef_cafebabe } else { seed };
554 let weights: Vec<f64> = (0..dim)
555 .map(|_| (xorshift_f64(&mut state) - 0.5) * 0.01)
556 .collect();
557 let bias = (xorshift_f64(&mut state) - 0.5) * 0.01;
558 ModelParams { weights, bias, dim }
559 }
560
561 pub fn few_shot_predict(
565 &self,
566 task: &MetaTask,
567 x: &[f64],
568 init_params: &ModelParams,
569 ) -> Result<f64, MetaError> {
570 let (steps, lr) = match &self.config.algorithm {
572 MetaAlgorithm::MAML {
573 inner_lr,
574 inner_steps,
575 } => (*inner_steps, *inner_lr),
576 MetaAlgorithm::FOMAML { inner_lr } => (5u8, *inner_lr),
577 MetaAlgorithm::Reptile { step_size } => (5u8, *step_size),
578 MetaAlgorithm::ProtoNet => (1u8, 0.01),
579 };
580
581 if task.support_set.is_empty() {
582 return Err(MetaError::AdaptationFailed(
583 "support set is empty for few_shot_predict".to_string(),
584 ));
585 }
586
587 let dim = init_params.dim;
588 if x.len() != dim {
589 return Err(MetaError::DimensionMismatch {
590 expected: dim,
591 got: x.len(),
592 });
593 }
594
595 for ex in &task.support_set {
597 let got = ex.features.len();
598 if got != dim {
599 return Err(MetaError::DimensionMismatch { expected: dim, got });
600 }
601 }
602
603 let mut params = init_params.clone();
605 for _ in 0..steps {
606 let (_, grad_w, grad_b) = params.mse_and_grads(&task.support_set);
607 for (w, gw) in params.weights.iter_mut().zip(grad_w.iter()) {
608 *w -= lr * gw;
609 }
610 params.bias -= lr * grad_b;
611 }
612
613 Ok(params.predict(x))
614 }
615
616 pub fn stats(&self) -> MetaStats {
620 self.stats.clone()
621 }
622
623 fn meta_update_maml(
628 &mut self,
629 task_ids: &[TaskId],
630 current_params: &ModelParams,
631 inner_lr: f64,
632 inner_steps: u8,
633 dim: usize,
634 ) -> Result<ModelParams, MetaError> {
635 let mut meta_grad_w = vec![0.0_f64; dim];
636 let mut meta_grad_b = 0.0_f64;
637 let mut valid_count = 0usize;
638
639 for tid in task_ids {
640 let history = self.adapt_to_task(tid, current_params, inner_steps, inner_lr)?;
641 if let Some(last) = history.last() {
642 for (mg, (aw, iw)) in meta_grad_w.iter_mut().zip(
644 last.params
645 .weights
646 .iter()
647 .zip(current_params.weights.iter()),
648 ) {
649 *mg += aw - iw;
650 }
651 meta_grad_b += last.params.bias - current_params.bias;
652
653 let adapt_loss = last.loss;
655 self.adaptation_loss_sum += adapt_loss;
656 self.adaptation_loss_count += 1;
657 self.stats.avg_adaptation_loss =
658 self.adaptation_loss_sum / self.adaptation_loss_count as f64;
659
660 valid_count += 1;
661 }
662 }
663
664 if valid_count == 0 {
665 return Err(MetaError::InsufficientTasks(1));
666 }
667
668 let inv = 1.0 / valid_count as f64;
669 let meta_lr = self.config.meta_lr;
670 let mut new_w = current_params.weights.clone();
671 for (w, mg) in new_w.iter_mut().zip(meta_grad_w.iter()) {
672 *w += meta_lr * mg * inv;
673 }
674 let new_b = current_params.bias + meta_lr * meta_grad_b * inv;
675
676 Ok(ModelParams {
677 weights: new_w,
678 bias: new_b,
679 dim,
680 })
681 }
682
683 fn meta_update_fomaml(
685 &mut self,
686 task_ids: &[TaskId],
687 current_params: &ModelParams,
688 inner_lr: f64,
689 dim: usize,
690 ) -> Result<ModelParams, MetaError> {
691 let inner_steps: u8 = 1;
693 let mut meta_grad_w = vec![0.0_f64; dim];
694 let mut meta_grad_b = 0.0_f64;
695 let mut valid_count = 0usize;
696
697 for tid in task_ids {
698 let task = self
699 .tasks
700 .get(tid)
701 .ok_or_else(|| MetaError::AdaptationFailed(format!("unknown task: {tid}")))?;
702 if task.support_set.is_empty() {
703 continue;
704 }
705 for ex in &task.support_set {
707 let got = ex.features.len();
708 if got != dim {
709 return Err(MetaError::DimensionMismatch { expected: dim, got });
710 }
711 }
712 let history = self.adapt_to_task(tid, current_params, inner_steps, inner_lr)?;
714 if let Some(last) = history.last() {
715 let task2 = self
717 .tasks
718 .get(tid)
719 .ok_or_else(|| MetaError::AdaptationFailed(format!("unknown task: {tid}")))?;
720 let (qloss, qgrad_w, qgrad_b) = last.params.mse_and_grads(&task2.query_set);
721
722 for (mg, qg) in meta_grad_w.iter_mut().zip(qgrad_w.iter()) {
724 *mg += qg;
725 }
726 meta_grad_b += qgrad_b;
727
728 self.adaptation_loss_sum += qloss;
729 self.adaptation_loss_count += 1;
730 self.stats.avg_adaptation_loss =
731 self.adaptation_loss_sum / self.adaptation_loss_count as f64;
732
733 valid_count += 1;
734 }
735 }
736
737 if valid_count == 0 {
738 return Err(MetaError::InsufficientTasks(1));
739 }
740
741 let inv = 1.0 / valid_count as f64;
742 let meta_lr = self.config.meta_lr;
743 let mut new_w = current_params.weights.clone();
744 for (w, mg) in new_w.iter_mut().zip(meta_grad_w.iter()) {
745 *w -= meta_lr * mg * inv;
746 }
747 let new_b = current_params.bias - meta_lr * meta_grad_b * inv;
748
749 Ok(ModelParams {
750 weights: new_w,
751 bias: new_b,
752 dim,
753 })
754 }
755
756 fn meta_update_reptile(
758 &mut self,
759 task_ids: &[TaskId],
760 current_params: &ModelParams,
761 step_size: f64,
762 dim: usize,
763 ) -> Result<ModelParams, MetaError> {
764 let inner_steps = 5u8;
765 let inner_lr = 0.01;
766 let mut result = current_params.clone();
767 let mut valid_count = 0usize;
768
769 for tid in task_ids {
770 let history = self.adapt_to_task(tid, current_params, inner_steps, inner_lr)?;
771 if let Some(last) = history.last() {
772 for (idx, rw) in result.weights.iter_mut().enumerate() {
774 let init_w = current_params.weights[idx];
775 let adapted_w = last.params.weights[idx];
776 *rw += step_size * (adapted_w - init_w);
777 }
778 result.bias += step_size * (last.params.bias - current_params.bias);
779
780 self.adaptation_loss_sum += last.loss;
781 self.adaptation_loss_count += 1;
782 self.stats.avg_adaptation_loss =
783 self.adaptation_loss_sum / self.adaptation_loss_count as f64;
784
785 valid_count += 1;
786 }
787 }
788
789 if valid_count == 0 {
790 return Err(MetaError::InsufficientTasks(1));
791 }
792
793 let _ = dim; Ok(result)
795 }
796
797 fn meta_update_protonet(
802 &mut self,
803 task_ids: &[TaskId],
804 current_params: &ModelParams,
805 dim: usize,
806 ) -> Result<ModelParams, MetaError> {
807 let mut proto_w = vec![0.0_f64; dim];
808 let mut proto_b = 0.0_f64;
809 let mut valid_count = 0usize;
810
811 for tid in task_ids {
812 let task = self
813 .tasks
814 .get(tid)
815 .ok_or_else(|| MetaError::AdaptationFailed(format!("unknown task: {tid}")))?;
816 if task.support_set.is_empty() {
817 continue;
818 }
819 let n = task.support_set.len() as f64;
820 let mean_label: f64 = task.support_set.iter().map(|e| e.label).sum::<f64>() / n;
821 let mut mean_feat = vec![0.0_f64; dim];
822 for ex in &task.support_set {
823 if ex.features.len() != dim {
824 return Err(MetaError::DimensionMismatch {
825 expected: dim,
826 got: ex.features.len(),
827 });
828 }
829 for (mf, xi) in mean_feat.iter_mut().zip(ex.features.iter()) {
830 *mf += xi / n;
831 }
832 }
833 for (pw, mf) in proto_w.iter_mut().zip(mean_feat.iter()) {
835 *pw += mf * mean_label;
836 }
837 proto_b += mean_label;
838 valid_count += 1;
839 }
840
841 if valid_count == 0 {
842 return Err(MetaError::InsufficientTasks(1));
843 }
844
845 let inv = 1.0 / valid_count as f64;
846 let meta_lr = self.config.meta_lr;
847 let mut new_w = current_params.weights.clone();
848 for (w, pw) in new_w.iter_mut().zip(proto_w.iter()) {
849 *w += meta_lr * pw * inv;
850 }
851 let new_b = current_params.bias + meta_lr * proto_b * inv;
852
853 Ok(ModelParams {
854 weights: new_w,
855 bias: new_b,
856 dim,
857 })
858 }
859
860 fn compute_avg_query_loss(&self, task_ids: &[TaskId], params: &ModelParams) -> f64 {
862 let mut sum = 0.0;
863 let mut count = 0usize;
864 for tid in task_ids {
865 if let Ok(loss) = self.evaluate_task(tid, params) {
866 sum += loss;
867 count += 1;
868 }
869 }
870 if count == 0 {
871 0.0
872 } else {
873 sum / count as f64
874 }
875 }
876}
877
878#[cfg(test)]
881mod tests {
882 use super::*;
883
884 fn make_regression_task(
888 id: &str,
889 slope: f64,
890 intercept: f64,
891 n_support: usize,
892 n_query: usize,
893 seed: u64,
894 ) -> MetaTask {
895 let tid = TaskId::new(id);
896 let mut state = seed;
897 let mut support = Vec::with_capacity(n_support);
898 for _ in 0..n_support {
899 let x = xorshift_f64(&mut state) * 4.0 - 2.0;
900 let y = slope * x + intercept;
901 support.push(TaskExample::new(vec![x], y, tid.clone()));
902 }
903 let mut query = Vec::with_capacity(n_query);
904 for _ in 0..n_query {
905 let x = xorshift_f64(&mut state) * 4.0 - 2.0;
906 let y = slope * x + intercept;
907 query.push(TaskExample::new(vec![x], y, tid.clone()));
908 }
909 MetaTask::new(tid, support, query)
910 }
911
912 fn make_2d_task(
914 id: &str,
915 w0: f64,
916 w1: f64,
917 bias: f64,
918 n_support: usize,
919 n_query: usize,
920 seed: u64,
921 ) -> MetaTask {
922 let tid = TaskId::new(id);
923 let mut state = seed;
924 let mut support = Vec::with_capacity(n_support);
925 for _ in 0..n_support {
926 let x0 = xorshift_f64(&mut state) * 2.0 - 1.0;
927 let x1 = xorshift_f64(&mut state) * 2.0 - 1.0;
928 let y = w0 * x0 + w1 * x1 + bias;
929 support.push(TaskExample::new(vec![x0, x1], y, tid.clone()));
930 }
931 let mut query = Vec::with_capacity(n_query);
932 for _ in 0..n_query {
933 let x0 = xorshift_f64(&mut state) * 2.0 - 1.0;
934 let x1 = xorshift_f64(&mut state) * 2.0 - 1.0;
935 let y = w0 * x0 + w1 * x1 + bias;
936 query.push(TaskExample::new(vec![x0, x1], y, tid.clone()));
937 }
938 MetaTask::new(tid, support, query)
939 }
940
941 #[test]
944 fn test_add_task_basic() {
945 let config = OptimizerConfig::default_maml(1);
946 let mut opt = MetaLearningOptimizer::new(config);
947 let task = make_regression_task("t1", 2.0, 1.0, 5, 5, 1);
948 assert!(opt.add_task(task).is_ok());
949 assert_eq!(opt.stats().tasks_trained, 1);
950 }
951
952 #[test]
953 fn test_add_multiple_tasks() {
954 let config = OptimizerConfig::default_maml(1);
955 let mut opt = MetaLearningOptimizer::new(config);
956 for i in 0..5 {
957 let task = make_regression_task(&format!("t{i}"), i as f64, 0.0, 4, 4, i as u64 + 1);
958 assert!(opt.add_task(task).is_ok());
959 }
960 assert_eq!(opt.stats().tasks_trained, 5);
961 }
962
963 #[test]
964 fn test_add_task_dimension_consistency() {
965 let config = OptimizerConfig::default_maml(2);
966 let mut opt = MetaLearningOptimizer::new(config);
967 let t1 = make_2d_task("t1", 1.0, 2.0, 0.5, 4, 4, 10);
968 assert!(opt.add_task(t1).is_ok());
969 let t2 = make_regression_task("t2", 1.0, 0.0, 4, 4, 20);
971 let err = opt.add_task(t2).unwrap_err();
972 assert!(matches!(
973 err,
974 MetaError::DimensionMismatch {
975 expected: 2,
976 got: 1
977 }
978 ));
979 }
980
981 #[test]
982 fn test_add_task_dim_exceeds_max() {
983 let config = OptimizerConfig {
984 algorithm: MetaAlgorithm::MAML {
985 inner_lr: 0.01,
986 inner_steps: 3,
987 },
988 meta_lr: 0.001,
989 n_tasks_per_batch: 2,
990 max_params_dim: 2,
991 };
992 let mut opt = MetaLearningOptimizer::new(config);
993 let tid = TaskId::new("too-big");
995 let ex = TaskExample::new(vec![1.0, 2.0, 3.0], 0.5, tid.clone());
996 let task = MetaTask::new(tid, vec![ex.clone()], vec![ex]);
997 let err = opt.add_task(task).unwrap_err();
998 assert!(matches!(err, MetaError::InvalidConfig(_)));
999 }
1000
1001 #[test]
1002 fn test_add_task_empty_support_allowed() {
1003 let config = OptimizerConfig::default_maml(1);
1005 let mut opt = MetaLearningOptimizer::new(config);
1006 let tid = TaskId::new("empty");
1007 let qex = TaskExample::new(vec![1.0], 1.0, tid.clone());
1008 let task = MetaTask::new(tid, vec![], vec![qex]);
1009 assert!(opt.add_task(task).is_ok());
1011 }
1012
1013 #[test]
1016 fn test_adapt_returns_correct_step_count() {
1017 let config = OptimizerConfig::default_maml(1);
1018 let mut opt = MetaLearningOptimizer::new(config);
1019 let task = make_regression_task("t1", 3.0, 0.5, 10, 5, 42);
1020 opt.add_task(task).expect("test: should succeed");
1021 let init = MetaLearningOptimizer::initialize_params(1, 1);
1022 let steps = opt
1023 .adapt_to_task(&TaskId::new("t1"), &init, 7, 0.01)
1024 .expect("test: should succeed");
1025 assert_eq!(steps.len(), 7);
1026 }
1027
1028 #[test]
1029 fn test_adapt_step_numbers_sequential() {
1030 let config = OptimizerConfig::default_maml(1);
1031 let mut opt = MetaLearningOptimizer::new(config);
1032 let task = make_regression_task("t1", 1.0, 0.0, 8, 4, 5);
1033 opt.add_task(task).expect("test: should succeed");
1034 let init = MetaLearningOptimizer::initialize_params(1, 7);
1035 let steps = opt
1036 .adapt_to_task(&TaskId::new("t1"), &init, 5, 0.05)
1037 .expect("test: should succeed");
1038 for (i, step) in steps.iter().enumerate() {
1039 assert_eq!(step.step_num, i);
1040 }
1041 }
1042
1043 #[test]
1044 fn test_adapt_loss_non_negative() {
1045 let config = OptimizerConfig::default_maml(1);
1046 let mut opt = MetaLearningOptimizer::new(config);
1047 let task = make_regression_task("t1", 2.0, -1.0, 10, 5, 11);
1048 opt.add_task(task).expect("test: should succeed");
1049 let init = MetaLearningOptimizer::initialize_params(1, 99);
1050 let steps = opt
1051 .adapt_to_task(&TaskId::new("t1"), &init, 10, 0.01)
1052 .expect("test: should succeed");
1053 for step in &steps {
1054 assert!(step.loss >= 0.0, "loss must be non-negative");
1055 }
1056 }
1057
1058 #[test]
1059 fn test_adapt_loss_decreases_over_steps() {
1060 let config = OptimizerConfig::default_maml(1);
1062 let mut opt = MetaLearningOptimizer::new(config);
1063 let task = make_regression_task("t1", 1.5, 0.3, 20, 5, 7);
1064 opt.add_task(task).expect("test: should succeed");
1065 let init = MetaLearningOptimizer::initialize_params(1, 3);
1066 let steps = opt
1067 .adapt_to_task(&TaskId::new("t1"), &init, 20, 0.05)
1068 .expect("test: should succeed");
1069 let first_loss = steps.first().map(|s| s.loss).unwrap_or(f64::MAX);
1070 let last_loss = steps.last().map(|s| s.loss).unwrap_or(f64::MAX);
1071 assert!(
1072 last_loss <= first_loss + 1e-10,
1073 "loss should decrease: {first_loss} -> {last_loss}"
1074 );
1075 }
1076
1077 #[test]
1078 fn test_adapt_2d_loss_decreases() {
1079 let config = OptimizerConfig::default_maml(2);
1080 let mut opt = MetaLearningOptimizer::new(config);
1081 let task = make_2d_task("t1", 1.0, -1.0, 0.5, 15, 5, 42);
1082 opt.add_task(task).expect("test: should succeed");
1083 let init = MetaLearningOptimizer::initialize_params(2, 9);
1084 let steps = opt
1085 .adapt_to_task(&TaskId::new("t1"), &init, 30, 0.02)
1086 .expect("test: should succeed");
1087 let first = steps.first().map(|s| s.loss).expect("test: should succeed");
1088 let last = steps.last().map(|s| s.loss).expect("test: should succeed");
1089 assert!(last <= first + 1e-9);
1090 }
1091
1092 #[test]
1093 fn test_adapt_unknown_task_error() {
1094 let config = OptimizerConfig::default_maml(1);
1095 let opt = MetaLearningOptimizer::new(config);
1096 let init = MetaLearningOptimizer::initialize_params(1, 1);
1097 let err = opt
1098 .adapt_to_task(&TaskId::new("no-such"), &init, 5, 0.01)
1099 .unwrap_err();
1100 assert!(matches!(err, MetaError::AdaptationFailed(_)));
1101 }
1102
1103 #[test]
1104 fn test_adapt_empty_support_error() {
1105 let config = OptimizerConfig::default_maml(1);
1106 let mut opt = MetaLearningOptimizer::new(config);
1107 let tid = TaskId::new("empty");
1108 let qex = TaskExample::new(vec![1.0], 1.0, tid.clone());
1109 let task = MetaTask::new(tid.clone(), vec![], vec![qex]);
1110 opt.add_task(task).expect("test: should succeed");
1111 let init = MetaLearningOptimizer::initialize_params(1, 1);
1112 let err = opt.adapt_to_task(&tid, &init, 3, 0.01).unwrap_err();
1113 assert!(matches!(err, MetaError::AdaptationFailed(_)));
1114 }
1115
1116 #[test]
1117 fn test_adapt_dim_mismatch_error() {
1118 let config = OptimizerConfig::default_maml(2);
1119 let mut opt = MetaLearningOptimizer::new(config);
1120 let task = make_2d_task("t1", 1.0, 1.0, 0.0, 5, 5, 1);
1121 opt.add_task(task).expect("test: should succeed");
1122 let bad_init = MetaLearningOptimizer::initialize_params(3, 1);
1124 let err = opt
1125 .adapt_to_task(&TaskId::new("t1"), &bad_init, 3, 0.01)
1126 .unwrap_err();
1127 assert!(matches!(err, MetaError::DimensionMismatch { .. }));
1128 }
1129
1130 #[test]
1133 fn test_meta_update_maml_returns_new_params() {
1134 let config = OptimizerConfig::default_maml(1);
1135 let mut opt = MetaLearningOptimizer::new(config);
1136 for i in 0..4 {
1137 let task = make_regression_task(
1138 &format!("t{i}"),
1139 (i + 1) as f64,
1140 0.1,
1141 8,
1142 4,
1143 (i * 7 + 1) as u64,
1144 );
1145 opt.add_task(task).expect("test: should succeed");
1146 }
1147 let init = MetaLearningOptimizer::initialize_params(1, 42);
1148 let ids: Vec<TaskId> = (0..4).map(|i| TaskId::new(format!("t{i}"))).collect();
1149 let new_params = opt.meta_update(&ids, &init).expect("test: should succeed");
1150 assert_eq!(new_params.dim, 1);
1151 assert_eq!(opt.stats().meta_updates, 1);
1152 }
1153
1154 #[test]
1155 fn test_meta_update_maml_params_changed() {
1156 let config = OptimizerConfig::default_maml(1);
1157 let mut opt = MetaLearningOptimizer::new(config);
1158 for i in 0..4 {
1159 let task = make_regression_task(
1160 &format!("t{i}"),
1161 (i as f64 + 1.0) * 0.7,
1162 0.3,
1163 10,
1164 5,
1165 i as u64 + 11,
1166 );
1167 opt.add_task(task).expect("test: should succeed");
1168 }
1169 let init = MetaLearningOptimizer::initialize_params(1, 17);
1170 let ids: Vec<TaskId> = (0..4).map(|i| TaskId::new(format!("t{i}"))).collect();
1171 let new_params = opt.meta_update(&ids, &init).expect("test: should succeed");
1172 let changed = new_params.weights[0] != init.weights[0] || new_params.bias != init.bias;
1174 assert!(changed, "meta_update should change parameters");
1175 }
1176
1177 #[test]
1178 fn test_meta_update_maml_multiple_rounds_converge() {
1179 let config = OptimizerConfig {
1180 algorithm: MetaAlgorithm::MAML {
1181 inner_lr: 0.05,
1182 inner_steps: 5,
1183 },
1184 meta_lr: 0.1,
1185 n_tasks_per_batch: 4,
1186 max_params_dim: 1,
1187 };
1188 let mut opt = MetaLearningOptimizer::new(config);
1189 for i in 0..4 {
1190 let task = make_regression_task(&format!("t{i}"), 1.0, 0.0, 10, 5, i as u64 + 1);
1191 opt.add_task(task).expect("test: should succeed");
1192 }
1193 let mut params = MetaLearningOptimizer::initialize_params(1, 5);
1194 let ids: Vec<TaskId> = (0..4).map(|i| TaskId::new(format!("t{i}"))).collect();
1195 for _ in 0..20 {
1196 params = opt
1197 .meta_update(&ids, ¶ms)
1198 .expect("test: should succeed");
1199 }
1200 assert_eq!(opt.stats().meta_updates, 20);
1201 }
1202
1203 #[test]
1204 fn test_meta_update_maml_empty_task_list_error() {
1205 let config = OptimizerConfig::default_maml(1);
1206 let mut opt = MetaLearningOptimizer::new(config);
1207 let init = MetaLearningOptimizer::initialize_params(1, 1);
1208 let err = opt.meta_update(&[], &init).unwrap_err();
1209 assert!(matches!(err, MetaError::InsufficientTasks(_)));
1210 }
1211
1212 #[test]
1215 fn test_meta_update_reptile_basic() {
1216 let config = OptimizerConfig::default_reptile(1);
1217 let mut opt = MetaLearningOptimizer::new(config);
1218 for i in 0..3 {
1219 let task =
1220 make_regression_task(&format!("r{i}"), (i + 1) as f64, 0.0, 8, 4, i as u64 + 5);
1221 opt.add_task(task).expect("test: should succeed");
1222 }
1223 let init = MetaLearningOptimizer::initialize_params(1, 42);
1224 let ids: Vec<TaskId> = (0..3).map(|i| TaskId::new(format!("r{i}"))).collect();
1225 let new_p = opt.meta_update(&ids, &init).expect("test: should succeed");
1226 assert_eq!(new_p.dim, 1);
1227 }
1228
1229 #[test]
1230 fn test_meta_update_reptile_params_change() {
1231 let config = OptimizerConfig::default_reptile(1);
1232 let mut opt = MetaLearningOptimizer::new(config);
1233 for i in 0..3 {
1234 let task = make_regression_task(&format!("r{i}"), 2.0, 1.0, 10, 5, i as u64 + 100);
1235 opt.add_task(task).expect("test: should succeed");
1236 }
1237 let init = MetaLearningOptimizer::initialize_params(1, 77);
1238 let ids: Vec<TaskId> = (0..3).map(|i| TaskId::new(format!("r{i}"))).collect();
1239 let new_p = opt.meta_update(&ids, &init).expect("test: should succeed");
1240 let changed = new_p.weights[0] != init.weights[0] || new_p.bias != init.bias;
1241 assert!(changed);
1242 }
1243
1244 #[test]
1245 fn test_meta_update_reptile_multiple_rounds() {
1246 let config = OptimizerConfig::default_reptile(1);
1247 let mut opt = MetaLearningOptimizer::new(config);
1248 for i in 0..3 {
1249 let task = make_regression_task(&format!("r{i}"), 1.0, 0.5, 8, 4, i as u64 + 3);
1250 opt.add_task(task).expect("test: should succeed");
1251 }
1252 let mut params = MetaLearningOptimizer::initialize_params(1, 17);
1253 let ids: Vec<TaskId> = (0..3).map(|i| TaskId::new(format!("r{i}"))).collect();
1254 for _ in 0..10 {
1255 params = opt
1256 .meta_update(&ids, ¶ms)
1257 .expect("test: should succeed");
1258 }
1259 assert_eq!(opt.stats().meta_updates, 10);
1260 }
1261
1262 #[test]
1265 fn test_meta_update_fomaml_basic() {
1266 let config = OptimizerConfig {
1267 algorithm: MetaAlgorithm::FOMAML { inner_lr: 0.02 },
1268 meta_lr: 0.01,
1269 n_tasks_per_batch: 3,
1270 max_params_dim: 1,
1271 };
1272 let mut opt = MetaLearningOptimizer::new(config);
1273 for i in 0..3 {
1274 let task = make_regression_task(
1275 &format!("f{i}"),
1276 i as f64 + 0.5,
1277 0.0,
1278 8,
1279 4,
1280 (i * 3 + 2) as u64,
1281 );
1282 opt.add_task(task).expect("test: should succeed");
1283 }
1284 let init = MetaLearningOptimizer::initialize_params(1, 55);
1285 let ids: Vec<TaskId> = (0..3).map(|i| TaskId::new(format!("f{i}"))).collect();
1286 let new_p = opt.meta_update(&ids, &init).expect("test: should succeed");
1287 assert_eq!(new_p.dim, 1);
1288 }
1289
1290 #[test]
1291 fn test_meta_update_fomaml_params_change() {
1292 let config = OptimizerConfig {
1293 algorithm: MetaAlgorithm::FOMAML { inner_lr: 0.05 },
1294 meta_lr: 0.1,
1295 n_tasks_per_batch: 3,
1296 max_params_dim: 1,
1297 };
1298 let mut opt = MetaLearningOptimizer::new(config);
1299 for i in 0..3 {
1300 let task = make_regression_task(&format!("f{i}"), 2.0, 0.5, 10, 5, i as u64 + 20);
1301 opt.add_task(task).expect("test: should succeed");
1302 }
1303 let init = MetaLearningOptimizer::initialize_params(1, 3);
1304 let ids: Vec<TaskId> = (0..3).map(|i| TaskId::new(format!("f{i}"))).collect();
1305 let new_p = opt.meta_update(&ids, &init).expect("test: should succeed");
1306 let changed = new_p.weights[0] != init.weights[0] || new_p.bias != init.bias;
1307 assert!(changed);
1308 }
1309
1310 #[test]
1313 fn test_meta_update_protonet_basic() {
1314 let config = OptimizerConfig {
1315 algorithm: MetaAlgorithm::ProtoNet,
1316 meta_lr: 0.01,
1317 n_tasks_per_batch: 3,
1318 max_params_dim: 2,
1319 };
1320 let mut opt = MetaLearningOptimizer::new(config);
1321 for i in 0..3 {
1322 let task = make_2d_task(&format!("p{i}"), 1.0, 1.0, 0.0, 6, 4, (i * 5 + 1) as u64);
1323 opt.add_task(task).expect("test: should succeed");
1324 }
1325 let init = MetaLearningOptimizer::initialize_params(2, 11);
1326 let ids: Vec<TaskId> = (0..3).map(|i| TaskId::new(format!("p{i}"))).collect();
1327 let new_p = opt.meta_update(&ids, &init).expect("test: should succeed");
1328 assert_eq!(new_p.dim, 2);
1329 assert_eq!(opt.stats().meta_updates, 1);
1330 }
1331
1332 #[test]
1335 fn test_evaluate_task_perfect_params() {
1336 let config = OptimizerConfig::default_maml(1);
1337 let mut opt = MetaLearningOptimizer::new(config);
1338 let task = make_regression_task("t1", 2.0, 1.0, 5, 10, 13);
1340 opt.add_task(task).expect("test: should succeed");
1341 let params = ModelParams {
1343 weights: vec![2.0],
1344 bias: 1.0,
1345 dim: 1,
1346 };
1347 let loss = opt
1348 .evaluate_task(&TaskId::new("t1"), ¶ms)
1349 .expect("test: should succeed");
1350 assert!(
1351 loss < 1e-20,
1352 "perfect params should give ~0 MSE, got {loss}"
1353 );
1354 }
1355
1356 #[test]
1357 fn test_evaluate_task_non_negative() {
1358 let config = OptimizerConfig::default_maml(1);
1359 let mut opt = MetaLearningOptimizer::new(config);
1360 let task = make_regression_task("t1", 3.0, -1.0, 5, 10, 17);
1361 opt.add_task(task).expect("test: should succeed");
1362 let params = MetaLearningOptimizer::initialize_params(1, 7);
1363 let loss = opt
1364 .evaluate_task(&TaskId::new("t1"), ¶ms)
1365 .expect("test: should succeed");
1366 assert!(loss >= 0.0);
1367 }
1368
1369 #[test]
1370 fn test_evaluate_task_unknown_error() {
1371 let config = OptimizerConfig::default_maml(1);
1372 let opt = MetaLearningOptimizer::new(config);
1373 let params = MetaLearningOptimizer::initialize_params(1, 1);
1374 let err = opt
1375 .evaluate_task(&TaskId::new("no-such"), ¶ms)
1376 .unwrap_err();
1377 assert!(matches!(err, MetaError::AdaptationFailed(_)));
1378 }
1379
1380 #[test]
1381 fn test_evaluate_task_empty_query_error() {
1382 let config = OptimizerConfig::default_maml(1);
1383 let mut opt = MetaLearningOptimizer::new(config);
1384 let tid = TaskId::new("empty-q");
1385 let sex = TaskExample::new(vec![1.0], 1.0, tid.clone());
1386 let task = MetaTask::new(tid.clone(), vec![sex], vec![]);
1387 opt.add_task(task).expect("test: should succeed");
1388 let params = MetaLearningOptimizer::initialize_params(1, 1);
1389 let err = opt.evaluate_task(&tid, ¶ms).unwrap_err();
1390 assert!(matches!(err, MetaError::AdaptationFailed(_)));
1391 }
1392
1393 #[test]
1396 fn test_initialize_params_dim() {
1397 let params = MetaLearningOptimizer::initialize_params(4, 42);
1398 assert_eq!(params.dim, 4);
1399 assert_eq!(params.weights.len(), 4);
1400 }
1401
1402 #[test]
1403 fn test_initialize_params_small_values() {
1404 let params = MetaLearningOptimizer::initialize_params(100, 123);
1406 for w in ¶ms.weights {
1407 assert!(w.abs() <= 0.01, "weight {w} exceeds 0.01");
1408 }
1409 assert!(
1410 params.bias.abs() <= 0.01,
1411 "bias {} exceeds 0.01",
1412 params.bias
1413 );
1414 }
1415
1416 #[test]
1417 fn test_initialize_params_deterministic() {
1418 let p1 = MetaLearningOptimizer::initialize_params(5, 77);
1419 let p2 = MetaLearningOptimizer::initialize_params(5, 77);
1420 assert_eq!(p1.weights, p2.weights);
1421 assert_eq!(p1.bias, p2.bias);
1422 }
1423
1424 #[test]
1425 fn test_initialize_params_different_seeds() {
1426 let p1 = MetaLearningOptimizer::initialize_params(5, 1);
1427 let p2 = MetaLearningOptimizer::initialize_params(5, 2);
1428 assert_ne!(
1429 p1.weights, p2.weights,
1430 "different seeds should give different weights"
1431 );
1432 }
1433
1434 #[test]
1435 fn test_initialize_params_zero_seed_fallback() {
1436 let params = MetaLearningOptimizer::initialize_params(3, 0);
1438 assert_eq!(params.dim, 3);
1439 }
1440
1441 #[test]
1444 fn test_few_shot_predict_basic() {
1445 let config = OptimizerConfig::default_maml(1);
1446 let opt = MetaLearningOptimizer::new(config);
1447 let tid = TaskId::new("fs1");
1449 let support: Vec<TaskExample> = (0..8)
1450 .map(|i| {
1451 let x = i as f64;
1452 TaskExample::new(vec![x], 2.0 * x + 1.0, tid.clone())
1453 })
1454 .collect();
1455 let task = MetaTask::new(tid, support, vec![]);
1456 let init = MetaLearningOptimizer::initialize_params(1, 42);
1457 let pred = opt
1458 .few_shot_predict(&task, &[3.0], &init)
1459 .expect("test: should succeed");
1460 assert!(pred.is_finite(), "prediction should be finite");
1462 }
1463
1464 #[test]
1465 fn test_few_shot_predict_dim_mismatch() {
1466 let config = OptimizerConfig::default_maml(2);
1467 let opt = MetaLearningOptimizer::new(config);
1468 let tid = TaskId::new("fs2");
1469 let support = vec![TaskExample::new(vec![1.0, 2.0], 3.0, tid.clone())];
1470 let task = MetaTask::new(tid, support, vec![]);
1471 let init = MetaLearningOptimizer::initialize_params(2, 9);
1472 let err = opt.few_shot_predict(&task, &[1.0], &init).unwrap_err();
1474 assert!(matches!(
1475 err,
1476 MetaError::DimensionMismatch {
1477 expected: 2,
1478 got: 1
1479 }
1480 ));
1481 }
1482
1483 #[test]
1484 fn test_few_shot_predict_empty_support_error() {
1485 let config = OptimizerConfig::default_maml(1);
1486 let opt = MetaLearningOptimizer::new(config);
1487 let tid = TaskId::new("fse");
1488 let task = MetaTask::new(tid, vec![], vec![]);
1489 let init = MetaLearningOptimizer::initialize_params(1, 1);
1490 let err = opt.few_shot_predict(&task, &[1.0], &init).unwrap_err();
1491 assert!(matches!(err, MetaError::AdaptationFailed(_)));
1492 }
1493
1494 #[test]
1495 fn test_few_shot_predict_reptile() {
1496 let config = OptimizerConfig::default_reptile(1);
1497 let opt = MetaLearningOptimizer::new(config);
1498 let tid = TaskId::new("rfs");
1499 let support: Vec<TaskExample> = (0..5)
1500 .map(|i| {
1501 let x = i as f64 * 0.5;
1502 TaskExample::new(vec![x], 3.0 * x, tid.clone())
1503 })
1504 .collect();
1505 let task = MetaTask::new(tid, support, vec![]);
1506 let init = MetaLearningOptimizer::initialize_params(1, 7);
1507 let pred = opt
1508 .few_shot_predict(&task, &[2.0], &init)
1509 .expect("test: should succeed");
1510 assert!(pred.is_finite());
1511 }
1512
1513 #[test]
1514 fn test_few_shot_predict_adapts_correctly_2d() {
1515 let config = OptimizerConfig {
1516 algorithm: MetaAlgorithm::MAML {
1517 inner_lr: 0.05,
1518 inner_steps: 20,
1519 },
1520 meta_lr: 0.01,
1521 n_tasks_per_batch: 2,
1522 max_params_dim: 2,
1523 };
1524 let opt = MetaLearningOptimizer::new(config);
1525 let tid = TaskId::new("2dfs");
1526 let support: Vec<TaskExample> = (0..20)
1528 .map(|i| {
1529 let x0 = i as f64 * 0.1;
1530 let x1 = (i as f64) * 0.2 - 1.0;
1531 TaskExample::new(vec![x0, x1], 1.5 * x0 + 0.5 * x1, tid.clone())
1532 })
1533 .collect();
1534 let task = MetaTask::new(tid, support, vec![]);
1535 let init = MetaLearningOptimizer::initialize_params(2, 42);
1536 let pred = opt
1537 .few_shot_predict(&task, &[1.0, 0.0], &init)
1538 .expect("test: should succeed");
1539 assert!((pred - 1.5).abs() < 1.0, "pred {pred} should be near 1.5");
1541 }
1542
1543 #[test]
1546 fn test_stats_initial_state() {
1547 let config = OptimizerConfig::default_maml(1);
1548 let opt = MetaLearningOptimizer::new(config);
1549 let stats = opt.stats();
1550 assert_eq!(stats.tasks_trained, 0);
1551 assert_eq!(stats.meta_updates, 0);
1552 assert_eq!(stats.avg_adaptation_loss, 0.0);
1553 assert_eq!(stats.avg_query_loss, 0.0);
1554 }
1555
1556 #[test]
1557 fn test_stats_tasks_trained_increments() {
1558 let config = OptimizerConfig::default_maml(1);
1559 let mut opt = MetaLearningOptimizer::new(config);
1560 for i in 0..5 {
1561 let task = make_regression_task(&format!("t{i}"), 1.0, 0.0, 5, 5, i as u64 + 1);
1562 opt.add_task(task).expect("test: should succeed");
1563 }
1564 assert_eq!(opt.stats().tasks_trained, 5);
1565 }
1566
1567 #[test]
1568 fn test_stats_meta_updates_increments() {
1569 let config = OptimizerConfig::default_maml(1);
1570 let mut opt = MetaLearningOptimizer::new(config);
1571 for i in 0..3 {
1572 let task = make_regression_task(&format!("t{i}"), 1.0, 0.0, 5, 5, i as u64 + 1);
1573 opt.add_task(task).expect("test: should succeed");
1574 }
1575 let mut params = MetaLearningOptimizer::initialize_params(1, 42);
1576 let ids: Vec<TaskId> = (0..3).map(|i| TaskId::new(format!("t{i}"))).collect();
1577 for n in 1..=5 {
1578 params = opt
1579 .meta_update(&ids, ¶ms)
1580 .expect("test: should succeed");
1581 assert_eq!(opt.stats().meta_updates, n);
1582 }
1583 }
1584
1585 #[test]
1586 fn test_stats_avg_adaptation_loss_non_negative() {
1587 let config = OptimizerConfig::default_maml(1);
1588 let mut opt = MetaLearningOptimizer::new(config);
1589 for i in 0..4 {
1590 let task =
1591 make_regression_task(&format!("t{i}"), (i + 1) as f64, 0.5, 8, 4, i as u64 + 2);
1592 opt.add_task(task).expect("test: should succeed");
1593 }
1594 let init = MetaLearningOptimizer::initialize_params(1, 9);
1595 let ids: Vec<TaskId> = (0..4).map(|i| TaskId::new(format!("t{i}"))).collect();
1596 opt.meta_update(&ids, &init).expect("test: should succeed");
1597 assert!(opt.stats().avg_adaptation_loss >= 0.0);
1598 }
1599
1600 #[test]
1601 fn test_stats_query_loss_non_negative() {
1602 let config = OptimizerConfig::default_maml(1);
1603 let mut opt = MetaLearningOptimizer::new(config);
1604 for i in 0..4 {
1605 let task = make_regression_task(&format!("t{i}"), 1.0, 0.5, 8, 4, i as u64 + 3);
1606 opt.add_task(task).expect("test: should succeed");
1607 }
1608 let init = MetaLearningOptimizer::initialize_params(1, 11);
1609 let ids: Vec<TaskId> = (0..4).map(|i| TaskId::new(format!("t{i}"))).collect();
1610 opt.meta_update(&ids, &init).expect("test: should succeed");
1611 assert!(opt.stats().avg_query_loss >= 0.0);
1612 }
1613
1614 #[test]
1617 fn test_error_display_insufficient_tasks() {
1618 let err = MetaError::InsufficientTasks(2);
1619 assert!(err.to_string().contains("insufficient"));
1620 }
1621
1622 #[test]
1623 fn test_error_display_dimension_mismatch() {
1624 let err = MetaError::DimensionMismatch {
1625 expected: 4,
1626 got: 3,
1627 };
1628 let s = err.to_string();
1629 assert!(s.contains("4") && s.contains("3"));
1630 }
1631
1632 #[test]
1633 fn test_error_display_adaptation_failed() {
1634 let err = MetaError::AdaptationFailed("oops".to_string());
1635 assert!(err.to_string().contains("oops"));
1636 }
1637
1638 #[test]
1639 fn test_error_display_invalid_config() {
1640 let err = MetaError::InvalidConfig("bad lr".to_string());
1641 assert!(err.to_string().contains("bad lr"));
1642 }
1643
1644 #[test]
1645 fn test_error_is_clone() {
1646 let err = MetaError::InsufficientTasks(3);
1647 let err2 = err.clone();
1648 assert_eq!(err, err2);
1649 }
1650
1651 #[test]
1654 fn test_xorshift64_deterministic() {
1655 let mut s1 = 12345u64;
1656 let mut s2 = 12345u64;
1657 assert_eq!(xorshift64(&mut s1), xorshift64(&mut s2));
1658 }
1659
1660 #[test]
1661 fn test_xorshift_f64_range() {
1662 let mut state = 99999u64;
1663 for _ in 0..1000 {
1664 let v = xorshift_f64(&mut state);
1665 assert!((0.0..1.0).contains(&v), "out of range: {v}");
1666 }
1667 }
1668
1669 #[test]
1672 fn test_model_params_predict() {
1673 let p = ModelParams {
1674 weights: vec![2.0, -1.0],
1675 bias: 0.5,
1676 dim: 2,
1677 };
1678 let pred = p.predict(&[1.0, 1.0]);
1679 assert!((pred - 1.5).abs() < 1e-12);
1680 }
1681
1682 #[test]
1683 fn test_model_params_zeros() {
1684 let p = ModelParams::zeros(3);
1685 assert_eq!(p.weights, vec![0.0, 0.0, 0.0]);
1686 assert_eq!(p.bias, 0.0);
1687 assert_eq!(p.dim, 3);
1688 }
1689
1690 #[test]
1691 fn test_mse_zero_on_perfect_fit() {
1692 let p = ModelParams {
1694 weights: vec![3.0],
1695 bias: 0.0,
1696 dim: 1,
1697 };
1698 let tid = TaskId::new("t");
1699 let examples: Vec<TaskExample> = (0..5)
1700 .map(|i| {
1701 let x = i as f64;
1702 TaskExample::new(vec![x], 3.0 * x, tid.clone())
1703 })
1704 .collect();
1705 let (loss, _, _) = p.mse_and_grads(&examples);
1706 assert!(loss < 1e-20, "MSE should be ~0 for perfect fit, got {loss}");
1707 }
1708
1709 #[test]
1712 fn test_end_to_end_maml_regression() {
1713 let config = OptimizerConfig {
1715 algorithm: MetaAlgorithm::MAML {
1716 inner_lr: 0.05,
1717 inner_steps: 5,
1718 },
1719 meta_lr: 0.1,
1720 n_tasks_per_batch: 4,
1721 max_params_dim: 1,
1722 };
1723 let mut opt = MetaLearningOptimizer::new(config);
1724 for i in 0..4 {
1726 let task = make_regression_task(
1727 &format!("t{i}"),
1728 2.0,
1729 i as f64 * 0.5,
1730 15,
1731 5,
1732 (i * 13 + 7) as u64,
1733 );
1734 opt.add_task(task).expect("test: should succeed");
1735 }
1736 let mut meta_params = MetaLearningOptimizer::initialize_params(1, 42);
1737 let ids: Vec<TaskId> = (0..4).map(|i| TaskId::new(format!("t{i}"))).collect();
1738 for _ in 0..30 {
1739 meta_params = opt
1740 .meta_update(&ids, &meta_params)
1741 .expect("test: should succeed");
1742 }
1743 let tid = TaskId::new("new");
1745 let new_support: Vec<TaskExample> = (0..5)
1746 .map(|i| {
1747 let x = i as f64 * 0.5;
1748 TaskExample::new(vec![x], 2.0 * x + 0.3, tid.clone())
1749 })
1750 .collect();
1751 let new_query: Vec<TaskExample> = (0..5)
1752 .map(|i| {
1753 let x = i as f64 * 0.5 + 0.1;
1754 TaskExample::new(vec![x], 2.0 * x + 0.3, tid.clone())
1755 })
1756 .collect();
1757 let new_task = MetaTask::new(tid.clone(), new_support, new_query);
1758 opt.add_task(new_task).expect("test: should succeed");
1759 let adapted = opt
1760 .adapt_to_task(&tid, &meta_params, 10, 0.05)
1761 .expect("test: should succeed");
1762 let init_loss = adapted.first().map(|s| s.loss).unwrap_or(f64::MAX);
1763 let final_loss = adapted.last().map(|s| s.loss).unwrap_or(f64::MAX);
1764 assert!(
1765 final_loss <= init_loss + 1e-6,
1766 "adaptation should reduce loss: {init_loss} -> {final_loss}"
1767 );
1768 }
1769
1770 #[test]
1771 fn test_end_to_end_reptile() {
1772 let config = OptimizerConfig::default_reptile(1);
1773 let mut opt = MetaLearningOptimizer::new(config);
1774 for i in 0..4 {
1775 let task = make_regression_task(
1776 &format!("t{i}"),
1777 1.5,
1778 i as f64 * 0.2,
1779 10,
1780 5,
1781 (i + 1) as u64 * 7,
1782 );
1783 opt.add_task(task).expect("test: should succeed");
1784 }
1785 let mut params = MetaLearningOptimizer::initialize_params(1, 33);
1786 let ids: Vec<TaskId> = (0..4).map(|i| TaskId::new(format!("t{i}"))).collect();
1787 for _ in 0..15 {
1788 params = opt
1789 .meta_update(&ids, ¶ms)
1790 .expect("test: should succeed");
1791 }
1792 assert_eq!(opt.stats().meta_updates, 15);
1793 assert!(opt.stats().avg_query_loss >= 0.0);
1794 }
1795
1796 #[test]
1797 fn test_task_id_display() {
1798 let tid = TaskId::new("hello");
1799 assert_eq!(tid.to_string(), "hello");
1800 assert_eq!(tid.as_str(), "hello");
1801 }
1802
1803 #[test]
1804 fn test_meta_task_feature_dim() {
1805 let tid = TaskId::new("t");
1806 let ex = TaskExample::new(vec![1.0, 2.0, 3.0], 0.0, tid.clone());
1807 let task = MetaTask::new(tid, vec![ex], vec![]);
1808 assert_eq!(task.feature_dim(), Some(3));
1809 }
1810
1811 #[test]
1812 fn test_meta_task_feature_dim_empty() {
1813 let tid = TaskId::new("t");
1814 let task = MetaTask::new(tid, vec![], vec![]);
1815 assert_eq!(task.feature_dim(), None);
1816 }
1817}