1use crate::architecture_knowledge_graph::{ArchitectureKnowledgeGraph, NodeId, PerformanceRecord};
74use crate::error::{OptimError, Result};
75use scirs2_core::ndarray::{Array1, Array2};
76use scirs2_core::random::Random;
77use serde::{Deserialize, Serialize};
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct ArchitectureExample {
87 pub arch_id: String,
89 pub embedding: Vec<f64>,
93 pub performance: f64,
96 pub domain: String,
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
106pub enum FewShotAlgorithm {
107 Prototypical,
110 Matching,
113 MamlAdaptation,
116 DistanceWeightedKnn,
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
129pub enum DistanceMetric {
130 Euclidean,
132 Cosine,
134 Manhattan,
136}
137
138#[derive(Debug, Clone)]
146pub struct FewShotConfig {
147 pub algorithm: FewShotAlgorithm,
149 pub k_shot: usize,
152 pub n_way: usize,
155 pub temperature: f64,
159 pub adapter_lr: f64,
161 pub adapter_steps: usize,
163 pub distance_metric: DistanceMetric,
165 pub seed: u64,
167}
168
169impl Default for FewShotConfig {
170 fn default() -> Self {
171 Self {
172 algorithm: FewShotAlgorithm::Prototypical,
173 k_shot: 5,
174 n_way: 2,
175 temperature: 1.0,
176 adapter_lr: 0.01,
177 adapter_steps: 5,
178 distance_metric: DistanceMetric::Euclidean,
179 seed: 42,
180 }
181 }
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct FewShotPrediction {
192 pub predicted_performance: f64,
194 pub confidence: f64,
196 pub nearest_support_arch: Option<String>,
198 pub nearest_distance: f64,
201}
202
203#[derive(Debug, Clone)]
210pub struct FewShotArchitectureOptimizer {
211 config: FewShotConfig,
212 support_set: Vec<ArchitectureExample>,
213 is_fitted: bool,
214 adapter_weights: Option<Vec<f64>>,
217 adapter_bias: Option<f64>,
219}
220
221impl Default for FewShotArchitectureOptimizer {
222 fn default() -> Self {
223 Self::new()
224 }
225}
226
227impl FewShotArchitectureOptimizer {
228 pub fn new() -> Self {
230 Self::with_config(FewShotConfig::default())
231 }
232
233 pub fn with_config(config: FewShotConfig) -> Self {
235 Self {
236 config,
237 support_set: Vec::new(),
238 is_fitted: false,
239 adapter_weights: None,
240 adapter_bias: None,
241 }
242 }
243
244 pub fn with_algorithm(mut self, algorithm: FewShotAlgorithm) -> Self {
246 self.config.algorithm = algorithm;
247 self
248 }
249
250 pub fn with_k_shot(mut self, k: usize) -> Self {
252 self.config.k_shot = k;
253 self
254 }
255
256 pub fn with_distance_metric(mut self, metric: DistanceMetric) -> Self {
258 self.config.distance_metric = metric;
259 self
260 }
261
262 pub fn with_temperature(mut self, t: f64) -> Self {
264 self.config.temperature = t;
265 self
266 }
267
268 pub fn with_seed(mut self, seed: u64) -> Self {
270 self.config.seed = seed;
271 self
272 }
273
274 pub fn support_set(&self) -> &[ArchitectureExample] {
276 &self.support_set
277 }
278
279 pub fn is_fitted(&self) -> bool {
282 self.is_fitted
283 }
284
285 pub fn config(&self) -> &FewShotConfig {
287 &self.config
288 }
289
290 pub fn reset(&mut self) {
295 self.support_set.clear();
296 self.is_fitted = false;
297 self.adapter_weights = None;
298 self.adapter_bias = None;
299 }
300
301 pub fn fit_from_examples(&mut self, examples: Vec<ArchitectureExample>) -> Result<()> {
311 if examples.len() < self.config.k_shot {
312 return Err(OptimError::InvalidParameter(format!(
313 "fit_from_examples: need at least k_shot = {} examples, got {}",
314 self.config.k_shot,
315 examples.len()
316 )));
317 }
318 if examples.is_empty() {
319 return Err(OptimError::InvalidParameter(
320 "fit_from_examples: support set is empty".to_string(),
321 ));
322 }
323
324 let embedding_dim = examples[0].embedding.len();
328 if embedding_dim == 0 {
329 return Err(OptimError::InvalidParameter(
330 "fit_from_examples: embeddings must be non-empty".to_string(),
331 ));
332 }
333 for (i, ex) in examples.iter().enumerate() {
334 if ex.embedding.len() != embedding_dim {
335 return Err(OptimError::InvalidParameter(format!(
336 "fit_from_examples: example {} has embedding dim {}, expected {}",
337 i,
338 ex.embedding.len(),
339 embedding_dim
340 )));
341 }
342 if !ex.performance.is_finite() {
343 return Err(OptimError::InvalidParameter(format!(
344 "fit_from_examples: example {} performance is not finite: {}",
345 i, ex.performance
346 )));
347 }
348 if ex.embedding.iter().any(|v| !v.is_finite()) {
349 return Err(OptimError::InvalidParameter(format!(
350 "fit_from_examples: example {} has non-finite embedding entries",
351 i
352 )));
353 }
354 }
355
356 if self.config.n_way == 0 {
357 return Err(OptimError::InvalidParameter(
358 "fit_from_examples: n_way must be at least 1".to_string(),
359 ));
360 }
361
362 self.support_set = examples;
363
364 if matches!(self.config.algorithm, FewShotAlgorithm::MamlAdaptation) {
368 self.train_maml_adapter()?;
369 } else {
370 self.adapter_weights = None;
371 self.adapter_bias = None;
372 }
373
374 self.is_fitted = true;
375 Ok(())
376 }
377
378 pub fn fit_from_graph(
386 &mut self,
387 graph: &ArchitectureKnowledgeGraph,
388 source_domain: &str,
389 k: usize,
390 ) -> Result<()> {
391 if k == 0 {
392 return Err(OptimError::InvalidParameter(
393 "fit_from_graph: k must be greater than 0".to_string(),
394 ));
395 }
396
397 let mut candidates: Vec<(NodeId, PerformanceRecord)> = graph
401 .nodes()
402 .iter()
403 .filter(|n| n.domain == source_domain)
404 .filter_map(|n| n.performance.as_ref().map(|p| (n.id, p.clone())))
405 .collect();
406
407 if candidates.is_empty() {
408 return Err(OptimError::InvalidParameter(format!(
409 "fit_from_graph: no nodes with performance records in domain '{}'",
410 source_domain
411 )));
412 }
413
414 candidates.sort_by(|a, b| {
415 b.1.accuracy
416 .partial_cmp(&a.1.accuracy)
417 .unwrap_or(std::cmp::Ordering::Equal)
418 });
419 candidates.truncate(k);
420
421 let mut examples: Vec<ArchitectureExample> = Vec::with_capacity(candidates.len());
426 for (node_id, record) in candidates {
427 let node = graph.node(node_id).ok_or_else(|| {
428 OptimError::ArchitectureError(format!(
429 "fit_from_graph: node {} disappeared while iterating",
430 node_id
431 ))
432 })?;
433 examples.push(ArchitectureExample {
434 arch_id: node.arch_id.clone(),
435 embedding: node.embedding.clone(),
436 performance: record.accuracy,
437 domain: node.domain.clone(),
438 });
439 }
440
441 self.fit_from_examples(examples)
442 }
443
444 pub fn predict(&self, query_embedding: &[f64]) -> Result<FewShotPrediction> {
448 self.check_fitted()?;
449 if query_embedding.is_empty() {
450 return Err(OptimError::InvalidParameter(
451 "predict: query embedding must be non-empty".to_string(),
452 ));
453 }
454 let expected_dim = self.support_set[0].embedding.len();
455 if query_embedding.len() != expected_dim {
456 return Err(OptimError::InvalidParameter(format!(
457 "predict: query has {} dims, support uses {}",
458 query_embedding.len(),
459 expected_dim
460 )));
461 }
462 if query_embedding.iter().any(|v| !v.is_finite()) {
463 return Err(OptimError::InvalidParameter(
464 "predict: query embedding contains non-finite entries".to_string(),
465 ));
466 }
467
468 match self.config.algorithm {
469 FewShotAlgorithm::Prototypical => self.predict_prototypical(query_embedding),
470 FewShotAlgorithm::Matching => self.predict_matching(query_embedding),
471 FewShotAlgorithm::MamlAdaptation => self.predict_maml(query_embedding),
472 FewShotAlgorithm::DistanceWeightedKnn => self.predict_knn(query_embedding),
473 }
474 }
475
476 pub fn predict_batch(&self, queries: &[Vec<f64>]) -> Result<Vec<FewShotPrediction>> {
481 let mut out = Vec::with_capacity(queries.len());
482 for q in queries {
483 out.push(self.predict(q)?);
484 }
485 Ok(out)
486 }
487
488 pub fn recommend_top_k(
494 &self,
495 candidates: &[(&str, Vec<f64>)],
496 top_k: usize,
497 ) -> Result<Vec<(String, FewShotPrediction)>> {
498 self.check_fitted()?;
499 if top_k == 0 {
500 return Ok(Vec::new());
501 }
502 let mut scored: Vec<(String, FewShotPrediction)> = Vec::with_capacity(candidates.len());
503 for (arch_id, emb) in candidates {
504 let prediction = self.predict(emb)?;
505 scored.push(((*arch_id).to_string(), prediction));
506 }
507 scored.sort_by(|a, b| {
508 b.1.predicted_performance
509 .partial_cmp(&a.1.predicted_performance)
510 .unwrap_or(std::cmp::Ordering::Equal)
511 });
512 scored.truncate(top_k);
513 Ok(scored)
514 }
515
516 fn check_fitted(&self) -> Result<()> {
522 if !self.is_fitted || self.support_set.is_empty() {
523 return Err(OptimError::InvalidParameter(
524 "not fitted: call fit_from_examples or fit_from_graph first".to_string(),
525 ));
526 }
527 Ok(())
528 }
529
530 fn distance(&self, a: &[f64], b: &[f64]) -> Result<f64> {
537 if a.len() != b.len() {
538 return Err(OptimError::InvalidParameter(format!(
539 "distance: length mismatch {} vs {}",
540 a.len(),
541 b.len()
542 )));
543 }
544 let d = match self.config.distance_metric {
545 DistanceMetric::Euclidean => {
546 let mut s = 0.0;
547 for (x, y) in a.iter().zip(b.iter()) {
548 let diff = x - y;
549 s += diff * diff;
550 }
551 s.sqrt()
552 }
553 DistanceMetric::Manhattan => {
554 let mut s = 0.0;
555 for (x, y) in a.iter().zip(b.iter()) {
556 s += (x - y).abs();
557 }
558 s
559 }
560 DistanceMetric::Cosine => {
561 let mut dot = 0.0;
562 let mut norm_a = 0.0;
563 let mut norm_b = 0.0;
564 for (x, y) in a.iter().zip(b.iter()) {
565 dot += x * y;
566 norm_a += x * x;
567 norm_b += y * y;
568 }
569 if norm_a <= 0.0 || norm_b <= 0.0 {
570 1.0
572 } else {
573 let cos = (dot / (norm_a.sqrt() * norm_b.sqrt())).clamp(-1.0, 1.0);
574 (1.0 - cos).clamp(0.0, 2.0)
575 }
576 }
577 };
578 if !d.is_finite() {
579 return Err(OptimError::InvalidParameter(format!(
580 "distance: produced non-finite result {}",
581 d
582 )));
583 }
584 Ok(d)
585 }
586
587 fn nearest_support(&self, query: &[f64]) -> Result<(usize, f64)> {
589 let mut best_idx = 0usize;
590 let mut best_d = f64::INFINITY;
591 for (i, ex) in self.support_set.iter().enumerate() {
592 let d = self.distance(query, &ex.embedding)?;
593 if d < best_d {
594 best_d = d;
595 best_idx = i;
596 }
597 }
598 Ok((best_idx, best_d))
599 }
600
601 fn softmax_neg(distances: &[f64], temperature: f64) -> Vec<f64> {
608 if distances.is_empty() {
609 return Vec::new();
610 }
611 let t = if temperature.is_finite() && temperature > 0.0 {
612 temperature
613 } else {
614 1.0
615 };
616 let mut max_logit = f64::NEG_INFINITY;
618 for d in distances {
619 let l = -d / t;
620 if l > max_logit {
621 max_logit = l;
622 }
623 }
624 if !max_logit.is_finite() {
625 let u = 1.0 / distances.len() as f64;
628 return vec![u; distances.len()];
629 }
630 let exps: Vec<f64> = distances
631 .iter()
632 .map(|d| ((-d / t) - max_logit).exp())
633 .collect();
634 let sum: f64 = exps.iter().sum();
635 if sum <= 0.0 || !sum.is_finite() {
636 let u = 1.0 / distances.len() as f64;
637 return vec![u; distances.len()];
638 }
639 exps.into_iter().map(|e| e / sum).collect()
640 }
641
642 fn entropy(p: &[f64]) -> f64 {
644 let mut h = 0.0;
645 for &pi in p {
646 if pi > 0.0 {
647 h -= pi * pi.ln();
648 }
649 }
650 h
651 }
652
653 fn prototype_buckets(&self) -> Vec<(Vec<f64>, f64)> {
664 let n_way = self.config.n_way.max(1);
665 let min_perf = self
666 .support_set
667 .iter()
668 .map(|e| e.performance)
669 .fold(f64::INFINITY, f64::min);
670 let max_perf = self
671 .support_set
672 .iter()
673 .map(|e| e.performance)
674 .fold(f64::NEG_INFINITY, f64::max);
675 let embedding_dim = self.support_set[0].embedding.len();
676
677 if (max_perf - min_perf).abs() < 1e-12 || n_way == 1 {
680 let mut proto = vec![0.0; embedding_dim];
681 for ex in &self.support_set {
682 for (slot, v) in proto.iter_mut().zip(ex.embedding.iter()) {
683 *slot += v;
684 }
685 }
686 for slot in proto.iter_mut() {
687 *slot /= self.support_set.len() as f64;
688 }
689 return vec![(proto, min_perf)];
690 }
691
692 let width = (max_perf - min_perf) / n_way as f64;
696 let mut sums: Vec<Vec<f64>> = vec![vec![0.0; embedding_dim]; n_way];
697 let mut perf_sums: Vec<f64> = vec![0.0; n_way];
698 let mut counts: Vec<usize> = vec![0; n_way];
699
700 for ex in &self.support_set {
701 let mut bin = ((ex.performance - min_perf) / width).floor() as isize;
702 if bin < 0 {
703 bin = 0;
704 }
705 if bin >= n_way as isize {
706 bin = (n_way - 1) as isize;
707 }
708 let b = bin as usize;
709 for (slot, v) in sums[b].iter_mut().zip(ex.embedding.iter()) {
710 *slot += v;
711 }
712 perf_sums[b] += ex.performance;
713 counts[b] += 1;
714 }
715
716 let mut buckets = Vec::new();
717 for b in 0..n_way {
718 if counts[b] == 0 {
719 continue;
720 }
721 let c = counts[b] as f64;
722 let proto: Vec<f64> = sums[b].iter().map(|v| v / c).collect();
723 buckets.push((proto, perf_sums[b] / c));
724 }
725 buckets
726 }
727
728 fn predict_prototypical(&self, query: &[f64]) -> Result<FewShotPrediction> {
729 let buckets = self.prototype_buckets();
730 if buckets.is_empty() {
731 return Err(OptimError::ConvergenceFailure(
732 "predict_prototypical: no non-empty prototype buckets".to_string(),
733 ));
734 }
735 let distances: Vec<f64> = buckets
736 .iter()
737 .map(|(proto, _)| self.distance(query, proto))
738 .collect::<Result<Vec<_>>>()?;
739 let mut best_idx = 0;
741 let mut best_d = f64::INFINITY;
742 for (i, &d) in distances.iter().enumerate() {
743 if d < best_d {
744 best_d = d;
745 best_idx = i;
746 }
747 }
748 let weights = Self::softmax_neg(&distances, self.config.temperature);
749 let confidence = weights
750 .get(best_idx)
751 .copied()
752 .unwrap_or(0.0)
753 .clamp(0.0, 1.0);
754
755 let (nearest_idx, nearest_d) = self.nearest_support(query)?;
756 let nearest = &self.support_set[nearest_idx];
757
758 Ok(FewShotPrediction {
759 predicted_performance: buckets[best_idx].1,
760 confidence,
761 nearest_support_arch: Some(nearest.arch_id.clone()),
762 nearest_distance: nearest_d,
763 })
764 }
765
766 fn predict_matching(&self, query: &[f64]) -> Result<FewShotPrediction> {
769 let distances: Vec<f64> = self
770 .support_set
771 .iter()
772 .map(|ex| self.distance(query, &ex.embedding))
773 .collect::<Result<Vec<_>>>()?;
774 let weights = Self::softmax_neg(&distances, self.config.temperature);
775 let predicted: f64 = weights
776 .iter()
777 .zip(self.support_set.iter())
778 .map(|(w, ex)| w * ex.performance)
779 .sum();
780
781 let n = weights.len();
785 let confidence = if n <= 1 {
786 1.0
787 } else {
788 let h = Self::entropy(&weights);
789 let log_n = (n as f64).ln();
790 if log_n > 0.0 {
791 (1.0 - h / log_n).clamp(0.0, 1.0)
792 } else {
793 1.0
794 }
795 };
796
797 let (nearest_idx, nearest_d) = self.nearest_support(query)?;
798 let nearest = &self.support_set[nearest_idx];
799
800 Ok(FewShotPrediction {
801 predicted_performance: predicted,
802 confidence,
803 nearest_support_arch: Some(nearest.arch_id.clone()),
804 nearest_distance: nearest_d,
805 })
806 }
807
808 fn train_maml_adapter(&mut self) -> Result<()> {
819 let dim = self.support_set[0].embedding.len();
820 let n = self.support_set.len() as f64;
821 let lr = if self.config.adapter_lr.is_finite() && self.config.adapter_lr > 0.0 {
822 self.config.adapter_lr
823 } else {
824 1e-2
825 };
826 let steps = self.config.adapter_steps.max(1);
827
828 let _rng = Random::seed(self.config.seed);
833
834 let mut w: Array1<f64> = Array1::zeros(dim);
835 let mut b = 0.0_f64;
836
837 for _ in 0..steps {
838 let mut grad_w: Array1<f64> = Array1::zeros(dim);
839 let mut grad_b = 0.0_f64;
840 for ex in &self.support_set {
841 let x = Array1::from(ex.embedding.clone());
842 let y_hat = w.dot(&x) + b;
843 let err = y_hat - ex.performance;
844 for i in 0..dim {
846 grad_w[i] += 2.0 * err * x[i];
847 }
848 grad_b += 2.0 * err;
849 }
850 for i in 0..dim {
851 grad_w[i] /= n;
852 }
853 grad_b /= n;
854 for i in 0..dim {
855 w[i] -= lr * grad_w[i];
856 }
857 b -= lr * grad_b;
858 }
859
860 self.adapter_weights = Some(w.to_vec());
861 self.adapter_bias = Some(b);
862 Ok(())
863 }
864
865 fn predict_maml(&self, query: &[f64]) -> Result<FewShotPrediction> {
866 let w = self.adapter_weights.as_ref().ok_or_else(|| {
867 OptimError::InvalidParameter(
868 "predict_maml: adapter not trained — refit with FewShotAlgorithm::MamlAdaptation"
869 .to_string(),
870 )
871 })?;
872 let b = self.adapter_bias.unwrap_or(0.0);
873 if w.len() != query.len() {
874 return Err(OptimError::InvalidParameter(format!(
875 "predict_maml: adapter dim {} vs query dim {}",
876 w.len(),
877 query.len()
878 )));
879 }
880 let predicted: f64 = w
881 .iter()
882 .zip(query.iter())
883 .map(|(wi, xi)| wi * xi)
884 .sum::<f64>()
885 + b;
886
887 let (nearest_idx, nearest_d) = self.nearest_support(query)?;
892 let all_d: Vec<f64> = self
893 .support_set
894 .iter()
895 .map(|ex| self.distance(query, &ex.embedding))
896 .collect::<Result<Vec<_>>>()?;
897 let mean_d = if all_d.is_empty() {
898 1.0
899 } else {
900 let s: f64 = all_d.iter().sum();
901 (s / all_d.len() as f64).max(1e-12)
902 };
903 let confidence = ((-nearest_d / mean_d).exp()).clamp(0.0, 1.0);
904
905 let nearest = &self.support_set[nearest_idx];
906 Ok(FewShotPrediction {
907 predicted_performance: predicted,
908 confidence,
909 nearest_support_arch: Some(nearest.arch_id.clone()),
910 nearest_distance: nearest_d,
911 })
912 }
913
914 fn predict_knn(&self, query: &[f64]) -> Result<FewShotPrediction> {
917 let mut distances: Vec<(usize, f64)> = Vec::with_capacity(self.support_set.len());
918 for (i, ex) in self.support_set.iter().enumerate() {
919 let d = self.distance(query, &ex.embedding)?;
920 distances.push((i, d));
921 }
922 distances.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
923
924 let k = self.config.k_shot.min(distances.len()).max(1);
925 let neighbours = &distances[..k];
926
927 if neighbours[0].1 < 1e-12 {
931 let nearest = &self.support_set[neighbours[0].0];
932 return Ok(FewShotPrediction {
933 predicted_performance: nearest.performance,
934 confidence: 1.0,
935 nearest_support_arch: Some(nearest.arch_id.clone()),
936 nearest_distance: neighbours[0].1,
937 });
938 }
939
940 let eps = 1e-9;
941 let weights: Vec<f64> = neighbours.iter().map(|(_, d)| 1.0 / (d + eps)).collect();
942 let weight_sum: f64 = weights.iter().sum();
943 let predicted: f64 = if weight_sum > 0.0 {
944 neighbours
945 .iter()
946 .zip(weights.iter())
947 .map(|((idx, _), w)| w * self.support_set[*idx].performance)
948 .sum::<f64>()
949 / weight_sum
950 } else {
951 let mean = neighbours
953 .iter()
954 .map(|(idx, _)| self.support_set[*idx].performance)
955 .sum::<f64>()
956 / neighbours.len() as f64;
957 mean
958 };
959
960 let mean_neighbour_d: f64 =
965 neighbours.iter().map(|(_, d)| d).sum::<f64>() / neighbours.len() as f64;
966 let max_support_d = distances
967 .iter()
968 .map(|(_, d)| *d)
969 .fold(0.0_f64, f64::max)
970 .max(1e-12);
971 let confidence = (1.0 - mean_neighbour_d / max_support_d).clamp(0.0, 1.0);
972
973 let nearest_idx = neighbours[0].0;
974 let nearest_d = neighbours[0].1;
975 let nearest = &self.support_set[nearest_idx];
976
977 Ok(FewShotPrediction {
978 predicted_performance: predicted,
979 confidence,
980 nearest_support_arch: Some(nearest.arch_id.clone()),
981 nearest_distance: nearest_d,
982 })
983 }
984}
985
986impl FewShotArchitectureOptimizer {
991 pub fn support_distance_matrix(&self) -> Result<Array2<f64>> {
997 self.check_fitted()?;
998 let n = self.support_set.len();
999 let mut m: Array2<f64> = Array2::zeros((n, n));
1000 for i in 0..n {
1001 for j in (i + 1)..n {
1002 let d = self.distance(
1003 &self.support_set[i].embedding,
1004 &self.support_set[j].embedding,
1005 )?;
1006 m[[i, j]] = d;
1007 m[[j, i]] = d;
1008 }
1009 }
1010 Ok(m)
1011 }
1012}
1013
1014#[cfg(test)]
1015mod tests {
1016 use super::*;
1017 use crate::architecture_knowledge_graph::{ArchitectureKnowledgeGraph, RelationType};
1018
1019 fn example(arch_id: &str, embedding: Vec<f64>, performance: f64) -> ArchitectureExample {
1020 ArchitectureExample {
1021 arch_id: arch_id.to_string(),
1022 embedding,
1023 performance,
1024 domain: "vision".to_string(),
1025 }
1026 }
1027
1028 fn make_support_set() -> Vec<ArchitectureExample> {
1029 vec![
1030 example("a", vec![1.0, 0.0, 0.0], 0.90),
1031 example("b", vec![0.9, 0.1, 0.0], 0.85),
1032 example("c", vec![0.0, 1.0, 0.0], 0.50),
1033 example("d", vec![0.0, 0.9, 0.1], 0.45),
1034 example("e", vec![0.1, 0.1, 0.9], 0.20),
1035 ]
1036 }
1037
1038 #[test]
1039 fn test_default_config_values() {
1040 let c = FewShotConfig::default();
1041 assert_eq!(c.algorithm, FewShotAlgorithm::Prototypical);
1042 assert_eq!(c.k_shot, 5);
1043 assert_eq!(c.n_way, 2);
1044 assert!((c.temperature - 1.0).abs() < 1e-12);
1045 assert!((c.adapter_lr - 0.01).abs() < 1e-12);
1046 assert_eq!(c.adapter_steps, 5);
1047 assert_eq!(c.distance_metric, DistanceMetric::Euclidean);
1048 assert_eq!(c.seed, 42);
1049 }
1050
1051 #[test]
1052 fn test_builder_pattern_chains() {
1053 let opt = FewShotArchitectureOptimizer::new()
1054 .with_algorithm(FewShotAlgorithm::Matching)
1055 .with_k_shot(3)
1056 .with_distance_metric(DistanceMetric::Cosine)
1057 .with_temperature(0.5)
1058 .with_seed(7);
1059 assert_eq!(opt.config().algorithm, FewShotAlgorithm::Matching);
1060 assert_eq!(opt.config().k_shot, 3);
1061 assert_eq!(opt.config().distance_metric, DistanceMetric::Cosine);
1062 assert!((opt.config().temperature - 0.5).abs() < 1e-12);
1063 assert_eq!(opt.config().seed, 7);
1064 assert!(!opt.is_fitted());
1066 assert!(opt.support_set().is_empty());
1067 }
1068
1069 #[test]
1070 fn test_fit_from_examples_validates_min_size() {
1071 let mut opt = FewShotArchitectureOptimizer::new().with_k_shot(5);
1072 let examples = vec![example("a", vec![1.0, 0.0], 0.5)];
1073 let err = opt.fit_from_examples(examples);
1074 assert!(err.is_err(), "expected InvalidParameter for too-small set");
1075 match err {
1076 Err(OptimError::InvalidParameter(_)) => {}
1077 other => panic!("expected InvalidParameter, got {:?}", other),
1078 }
1079 assert!(!opt.is_fitted());
1080 }
1081
1082 #[test]
1083 fn test_fit_from_examples_rejects_dim_mismatch() {
1084 let mut opt = FewShotArchitectureOptimizer::new().with_k_shot(2);
1085 let examples = vec![
1086 example("a", vec![1.0, 0.0], 0.5),
1087 example("b", vec![1.0, 0.0, 0.0], 0.6),
1088 ];
1089 assert!(opt.fit_from_examples(examples).is_err());
1090 }
1091
1092 #[test]
1093 fn test_fit_from_examples_rejects_non_finite() {
1094 let mut opt = FewShotArchitectureOptimizer::new().with_k_shot(2);
1095 let examples = vec![
1096 example("a", vec![1.0, f64::NAN], 0.5),
1097 example("b", vec![1.0, 0.0], 0.6),
1098 ];
1099 assert!(opt.fit_from_examples(examples).is_err());
1100 }
1101
1102 #[test]
1103 fn test_prototypical_predicts_high_for_similar_to_high_performers() {
1104 let mut opt = FewShotArchitectureOptimizer::new()
1105 .with_algorithm(FewShotAlgorithm::Prototypical)
1106 .with_k_shot(5);
1107 opt.fit_from_examples(make_support_set()).expect("fit");
1108
1109 let p_high = opt.predict(&[0.95, 0.05, 0.0]).expect("predict high");
1113 let p_low = opt.predict(&[0.1, 0.1, 0.9]).expect("predict low");
1114 assert!(
1115 p_high.predicted_performance > p_low.predicted_performance,
1116 "high query should outscore low: {} vs {}",
1117 p_high.predicted_performance,
1118 p_low.predicted_performance
1119 );
1120 assert!(p_high.confidence >= 0.0 && p_high.confidence <= 1.0);
1121 assert!(p_high.nearest_support_arch.is_some());
1122 }
1123
1124 #[test]
1125 fn test_matching_networks_temperature_affects_sharpness() {
1126 let support = make_support_set();
1128 let mut opt_low = FewShotArchitectureOptimizer::new()
1129 .with_algorithm(FewShotAlgorithm::Matching)
1130 .with_temperature(0.1)
1131 .with_k_shot(5);
1132 opt_low.fit_from_examples(support.clone()).expect("fit low");
1133 let mut opt_high = FewShotArchitectureOptimizer::new()
1134 .with_algorithm(FewShotAlgorithm::Matching)
1135 .with_temperature(10.0)
1136 .with_k_shot(5);
1137 opt_high.fit_from_examples(support).expect("fit high");
1138
1139 let q = vec![1.0, 0.0, 0.0];
1140 let p_low = opt_low.predict(&q).expect("predict low T");
1141 let p_high = opt_high.predict(&q).expect("predict high T");
1142 assert!(
1143 p_low.confidence > p_high.confidence,
1144 "low temperature should be sharper: {} vs {}",
1145 p_low.confidence,
1146 p_high.confidence
1147 );
1148 }
1149
1150 #[test]
1151 fn test_maml_adapter_decreases_training_loss() {
1152 let mut opt = FewShotArchitectureOptimizer::new()
1153 .with_algorithm(FewShotAlgorithm::MamlAdaptation)
1154 .with_k_shot(5);
1155 let support = make_support_set();
1156 let baseline_mse: f64 =
1158 support.iter().map(|ex| ex.performance.powi(2)).sum::<f64>() / support.len() as f64;
1159
1160 opt.fit_from_examples(support.clone()).expect("fit");
1161 let trained_mse: f64 = support
1163 .iter()
1164 .map(|ex| {
1165 let pred = opt
1166 .predict(&ex.embedding)
1167 .expect("predict")
1168 .predicted_performance;
1169 (pred - ex.performance).powi(2)
1170 })
1171 .sum::<f64>()
1172 / support.len() as f64;
1173 assert!(
1174 trained_mse < baseline_mse,
1175 "MAML should drop MSE below zero-baseline: {} vs {}",
1176 trained_mse,
1177 baseline_mse
1178 );
1179 }
1180
1181 #[test]
1182 fn test_maml_adapter_state_present_after_fit() {
1183 let mut opt = FewShotArchitectureOptimizer::new()
1184 .with_algorithm(FewShotAlgorithm::MamlAdaptation)
1185 .with_k_shot(5);
1186 opt.fit_from_examples(make_support_set()).expect("fit");
1187 assert!(opt.adapter_weights.is_some());
1188 assert!(opt.adapter_bias.is_some());
1189 assert_eq!(
1190 opt.adapter_weights.as_ref().expect("weights").len(),
1191 3,
1192 "adapter dim must match embedding dim"
1193 );
1194 }
1195
1196 #[test]
1197 fn test_knn_predict_with_exact_match_returns_exact_performance() {
1198 let mut opt = FewShotArchitectureOptimizer::new()
1199 .with_algorithm(FewShotAlgorithm::DistanceWeightedKnn)
1200 .with_k_shot(3);
1201 let support = make_support_set();
1202 opt.fit_from_examples(support.clone()).expect("fit");
1203
1204 let pred = opt.predict(&[1.0, 0.0, 0.0]).expect("predict");
1206 assert!(
1207 (pred.predicted_performance - 0.90).abs() < 1e-9,
1208 "exact match should return exact performance, got {}",
1209 pred.predicted_performance
1210 );
1211 assert!(
1212 (pred.confidence - 1.0).abs() < 1e-9,
1213 "confidence on exact match should be 1.0, got {}",
1214 pred.confidence
1215 );
1216 assert_eq!(pred.nearest_support_arch.as_deref(), Some("a"));
1217 }
1218
1219 #[test]
1220 fn test_recommend_top_k_returns_sorted_descending() {
1221 let mut opt = FewShotArchitectureOptimizer::new()
1222 .with_algorithm(FewShotAlgorithm::Matching)
1223 .with_k_shot(5);
1224 opt.fit_from_examples(make_support_set()).expect("fit");
1225
1226 let candidates: Vec<(&str, Vec<f64>)> = vec![
1227 ("low-perf", vec![0.1, 0.1, 0.9]),
1228 ("mid-perf", vec![0.5, 0.5, 0.0]),
1229 ("high-perf", vec![1.0, 0.0, 0.0]),
1230 ("med-low", vec![0.0, 0.9, 0.1]),
1231 ];
1232 let ranked = opt.recommend_top_k(&candidates, 4).expect("top_k");
1233 assert_eq!(ranked.len(), 4);
1234 for window in ranked.windows(2) {
1235 assert!(
1236 window[0].1.predicted_performance >= window[1].1.predicted_performance,
1237 "ranking not descending: {} then {}",
1238 window[0].1.predicted_performance,
1239 window[1].1.predicted_performance
1240 );
1241 }
1242 assert_eq!(ranked[0].0, "high-perf");
1245 }
1246
1247 #[test]
1248 fn test_recommend_top_k_truncates_to_top_k() {
1249 let mut opt = FewShotArchitectureOptimizer::new()
1250 .with_algorithm(FewShotAlgorithm::Matching)
1251 .with_k_shot(5);
1252 opt.fit_from_examples(make_support_set()).expect("fit");
1253
1254 let candidates: Vec<(&str, Vec<f64>)> = vec![
1255 ("a", vec![1.0, 0.0, 0.0]),
1256 ("b", vec![0.9, 0.1, 0.0]),
1257 ("c", vec![0.0, 1.0, 0.0]),
1258 ("d", vec![0.5, 0.5, 0.0]),
1259 ("e", vec![0.0, 0.0, 1.0]),
1260 ];
1261 let top2 = opt.recommend_top_k(&candidates, 2).expect("top_k=2");
1262 assert_eq!(top2.len(), 2);
1263 let empty = opt.recommend_top_k(&candidates, 0).expect("top_k=0");
1265 assert!(empty.is_empty());
1266 let all = opt.recommend_top_k(&candidates, 100).expect("top_k=100");
1268 assert_eq!(all.len(), candidates.len());
1269 }
1270
1271 #[test]
1272 fn test_fit_from_graph_selects_top_performers() {
1273 let mut graph = ArchitectureKnowledgeGraph::new();
1274 let mut ids = Vec::new();
1276 for i in 0..10 {
1277 let id = graph.add_architecture(
1278 format!("arch-{}", i),
1279 vec![i as f64 / 10.0, 0.0, 0.0],
1280 "vision",
1281 );
1282 graph
1283 .set_performance(
1284 id,
1285 PerformanceRecord {
1286 task_id: "task".into(),
1287 accuracy: 0.10 * (i + 1) as f64,
1288 latency_ms: 1.0,
1289 memory_mb: 1.0,
1290 training_epochs: 1,
1291 },
1292 )
1293 .expect("set_performance");
1294 ids.push(id);
1295 }
1296 graph.add_architecture("nlp-a", vec![1.0, 1.0, 1.0], "nlp");
1298
1299 let mut opt = FewShotArchitectureOptimizer::new()
1300 .with_algorithm(FewShotAlgorithm::DistanceWeightedKnn)
1301 .with_k_shot(3);
1302 opt.fit_from_graph(&graph, "vision", 3)
1303 .expect("fit_from_graph");
1304 assert!(opt.is_fitted());
1305 let support = opt.support_set();
1306 assert_eq!(support.len(), 3);
1307 let arch_ids: Vec<&str> = support.iter().map(|e| e.arch_id.as_str()).collect();
1310 assert!(arch_ids.contains(&"arch-9"));
1311 assert!(arch_ids.contains(&"arch-8"));
1312 assert!(arch_ids.contains(&"arch-7"));
1313 assert!(arch_ids.iter().all(|id| !id.starts_with("nlp")));
1315 }
1316
1317 #[test]
1318 fn test_fit_from_graph_errors_on_empty_domain() {
1319 let graph = ArchitectureKnowledgeGraph::new();
1320 let mut opt = FewShotArchitectureOptimizer::new().with_k_shot(1);
1321 assert!(opt.fit_from_graph(&graph, "vision", 5).is_err());
1322 }
1323
1324 #[test]
1325 fn test_predict_before_fit_errors() {
1326 let opt = FewShotArchitectureOptimizer::new();
1327 let r = opt.predict(&[1.0, 0.0, 0.0]);
1328 assert!(r.is_err(), "predict before fit must error");
1329 match r {
1330 Err(OptimError::InvalidParameter(msg)) => assert!(msg.contains("not fitted")),
1331 other => panic!("expected InvalidParameter, got {:?}", other),
1332 }
1333 }
1334
1335 #[test]
1336 fn test_reset_clears_state() {
1337 let mut opt = FewShotArchitectureOptimizer::new()
1338 .with_algorithm(FewShotAlgorithm::MamlAdaptation)
1339 .with_k_shot(5);
1340 opt.fit_from_examples(make_support_set()).expect("fit");
1341 assert!(opt.is_fitted());
1342 assert!(!opt.support_set().is_empty());
1343 assert!(opt.adapter_weights.is_some());
1344
1345 opt.reset();
1346 assert!(!opt.is_fitted());
1347 assert!(opt.support_set().is_empty());
1348 assert!(opt.adapter_weights.is_none());
1349 assert!(opt.adapter_bias.is_none());
1350 }
1351
1352 #[test]
1353 fn test_distance_metrics_produce_different_predictions() {
1354 let support = make_support_set();
1355 let mut opt_euc = FewShotArchitectureOptimizer::new()
1356 .with_algorithm(FewShotAlgorithm::Matching)
1357 .with_distance_metric(DistanceMetric::Euclidean)
1358 .with_k_shot(5);
1359 opt_euc.fit_from_examples(support.clone()).expect("fit euc");
1360
1361 let mut opt_cos = FewShotArchitectureOptimizer::new()
1362 .with_algorithm(FewShotAlgorithm::Matching)
1363 .with_distance_metric(DistanceMetric::Cosine)
1364 .with_k_shot(5);
1365 opt_cos.fit_from_examples(support.clone()).expect("fit cos");
1366
1367 let mut opt_man = FewShotArchitectureOptimizer::new()
1368 .with_algorithm(FewShotAlgorithm::Matching)
1369 .with_distance_metric(DistanceMetric::Manhattan)
1370 .with_k_shot(5);
1371 opt_man.fit_from_examples(support).expect("fit man");
1372
1373 let q = vec![0.5, 0.5, 0.5];
1374 let p_euc = opt_euc.predict(&q).expect("euc predict");
1375 let p_cos = opt_cos.predict(&q).expect("cos predict");
1376 let p_man = opt_man.predict(&q).expect("man predict");
1377
1378 assert!(
1382 (p_euc.predicted_performance - p_cos.predicted_performance).abs() > 1e-9,
1383 "Euclidean ({}) and Cosine ({}) must differ",
1384 p_euc.predicted_performance,
1385 p_cos.predicted_performance
1386 );
1387 assert!(
1390 (p_euc.predicted_performance - p_man.predicted_performance).abs() > 1e-12
1391 || (p_euc.confidence - p_man.confidence).abs() > 1e-12,
1392 "Euclidean and Manhattan should differ in at least one summary"
1393 );
1394 }
1395
1396 #[test]
1397 fn test_fewshot_prediction_serde_roundtrip() {
1398 let pred = FewShotPrediction {
1399 predicted_performance: 0.87,
1400 confidence: 0.93,
1401 nearest_support_arch: Some("resnet50".to_string()),
1402 nearest_distance: 0.12,
1403 };
1404 let json = serde_json::to_string(&pred).expect("serialise");
1405 let back: FewShotPrediction = serde_json::from_str(&json).expect("deserialise");
1406 assert!((back.predicted_performance - pred.predicted_performance).abs() < 1e-12);
1407 assert!((back.confidence - pred.confidence).abs() < 1e-12);
1408 assert_eq!(back.nearest_support_arch, pred.nearest_support_arch);
1409 assert!((back.nearest_distance - pred.nearest_distance).abs() < 1e-12);
1410 }
1411
1412 #[test]
1413 fn test_architecture_example_serde_roundtrip() {
1414 let ex = example("resnet", vec![1.0, 2.0, 3.0], 0.91);
1415 let json = serde_json::to_string(&ex).expect("serialise");
1416 let back: ArchitectureExample = serde_json::from_str(&json).expect("deserialise");
1417 assert_eq!(back.arch_id, ex.arch_id);
1418 assert_eq!(back.embedding, ex.embedding);
1419 assert!((back.performance - ex.performance).abs() < 1e-12);
1420 assert_eq!(back.domain, ex.domain);
1421 }
1422
1423 #[test]
1424 fn test_support_distance_matrix_is_symmetric() {
1425 let mut opt = FewShotArchitectureOptimizer::new().with_k_shot(5);
1426 opt.fit_from_examples(make_support_set()).expect("fit");
1427 let m = opt.support_distance_matrix().expect("matrix");
1428 assert_eq!(m.shape(), &[5, 5]);
1429 for i in 0..5 {
1430 assert!(m[[i, i]].abs() < 1e-12, "diagonal {} must be 0", i);
1431 for j in 0..5 {
1432 assert!(
1433 (m[[i, j]] - m[[j, i]]).abs() < 1e-12,
1434 "matrix not symmetric at ({}, {}): {} vs {}",
1435 i,
1436 j,
1437 m[[i, j]],
1438 m[[j, i]]
1439 );
1440 }
1441 }
1442 }
1443
1444 #[test]
1445 fn test_predict_batch_returns_one_per_query() {
1446 let mut opt = FewShotArchitectureOptimizer::new()
1447 .with_algorithm(FewShotAlgorithm::Matching)
1448 .with_k_shot(5);
1449 opt.fit_from_examples(make_support_set()).expect("fit");
1450
1451 let queries = vec![
1452 vec![1.0, 0.0, 0.0],
1453 vec![0.0, 1.0, 0.0],
1454 vec![0.0, 0.0, 1.0],
1455 ];
1456 let predictions = opt.predict_batch(&queries).expect("batch");
1457 assert_eq!(predictions.len(), 3);
1458 for p in &predictions {
1459 assert!(p.confidence >= 0.0 && p.confidence <= 1.0);
1460 assert!(p.predicted_performance.is_finite());
1461 }
1462 }
1463
1464 #[test]
1465 fn test_softmax_neg_uniform_for_equal_distances() {
1466 let weights = FewShotArchitectureOptimizer::softmax_neg(&[1.0, 1.0, 1.0, 1.0], 1.0);
1467 for w in &weights {
1468 assert!((w - 0.25).abs() < 1e-12, "expected 0.25 got {}", w);
1469 }
1470 }
1471
1472 #[test]
1473 fn test_softmax_neg_temperature_affects_sharpness() {
1474 let cold = FewShotArchitectureOptimizer::softmax_neg(&[0.0, 1.0, 2.0], 0.1);
1475 let warm = FewShotArchitectureOptimizer::softmax_neg(&[0.0, 1.0, 2.0], 10.0);
1476 assert!(cold[0] > 0.95, "expected near-1 max in cold: {:?}", cold);
1478 for w in &warm {
1480 assert!(
1481 (w - 1.0 / 3.0).abs() < 0.1,
1482 "warm should be near-uniform: {:?}",
1483 warm
1484 );
1485 }
1486 }
1487
1488 #[test]
1489 fn test_entropy_handles_zero_probability() {
1490 let h = FewShotArchitectureOptimizer::entropy(&[0.0, 1.0]);
1492 assert!(h.abs() < 1e-12);
1493 let h_uniform = FewShotArchitectureOptimizer::entropy(&[0.5, 0.5]);
1494 assert!((h_uniform - (2.0_f64).ln()).abs() < 1e-12);
1495 }
1496
1497 #[test]
1498 fn test_graph_with_transfer_edges_does_not_leak() {
1499 let mut graph = ArchitectureKnowledgeGraph::new();
1502 let v1 = graph.add_architecture("v1", vec![1.0, 0.0, 0.0], "vision");
1503 let n1 = graph.add_architecture("n1", vec![1.0, 0.0, 0.0], "nlp");
1504 graph
1505 .set_performance(
1506 v1,
1507 PerformanceRecord {
1508 task_id: "t".into(),
1509 accuracy: 0.9,
1510 latency_ms: 1.0,
1511 memory_mb: 1.0,
1512 training_epochs: 1,
1513 },
1514 )
1515 .expect("perf v1");
1516 graph
1517 .set_performance(
1518 n1,
1519 PerformanceRecord {
1520 task_id: "t".into(),
1521 accuracy: 0.95,
1522 latency_ms: 1.0,
1523 memory_mb: 1.0,
1524 training_epochs: 1,
1525 },
1526 )
1527 .expect("perf n1");
1528 graph
1529 .add_edge(n1, v1, RelationType::Transfers, 0.7)
1530 .expect("transfers");
1531
1532 let mut opt = FewShotArchitectureOptimizer::new()
1533 .with_algorithm(FewShotAlgorithm::DistanceWeightedKnn)
1534 .with_k_shot(1);
1535 opt.fit_from_graph(&graph, "vision", 1)
1536 .expect("fit_from_graph");
1537 let support = opt.support_set();
1538 assert_eq!(support.len(), 1);
1539 assert_eq!(support[0].arch_id, "v1");
1542 assert!((support[0].performance - 0.9).abs() < 1e-12);
1543 }
1544
1545 #[test]
1546 fn test_predict_query_dim_mismatch_errors() {
1547 let mut opt = FewShotArchitectureOptimizer::new().with_k_shot(5);
1548 opt.fit_from_examples(make_support_set()).expect("fit");
1549 let bad = opt.predict(&[1.0, 0.0]);
1550 assert!(bad.is_err(), "expected dim mismatch error");
1551 }
1552}