use crate::architecture_knowledge_graph::{ArchitectureKnowledgeGraph, NodeId, PerformanceRecord};
use crate::error::{OptimError, Result};
use scirs2_core::ndarray::{Array1, Array2};
use scirs2_core::random::Random;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ArchitectureExample {
pub arch_id: String,
pub embedding: Vec<f64>,
pub performance: f64,
pub domain: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum FewShotAlgorithm {
Prototypical,
Matching,
MamlAdaptation,
DistanceWeightedKnn,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum DistanceMetric {
Euclidean,
Cosine,
Manhattan,
}
#[derive(Debug, Clone)]
pub struct FewShotConfig {
pub algorithm: FewShotAlgorithm,
pub k_shot: usize,
pub n_way: usize,
pub temperature: f64,
pub adapter_lr: f64,
pub adapter_steps: usize,
pub distance_metric: DistanceMetric,
pub seed: u64,
}
impl Default for FewShotConfig {
fn default() -> Self {
Self {
algorithm: FewShotAlgorithm::Prototypical,
k_shot: 5,
n_way: 2,
temperature: 1.0,
adapter_lr: 0.01,
adapter_steps: 5,
distance_metric: DistanceMetric::Euclidean,
seed: 42,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FewShotPrediction {
pub predicted_performance: f64,
pub confidence: f64,
pub nearest_support_arch: Option<String>,
pub nearest_distance: f64,
}
#[derive(Debug, Clone)]
pub struct FewShotArchitectureOptimizer {
config: FewShotConfig,
support_set: Vec<ArchitectureExample>,
is_fitted: bool,
adapter_weights: Option<Vec<f64>>,
adapter_bias: Option<f64>,
}
impl Default for FewShotArchitectureOptimizer {
fn default() -> Self {
Self::new()
}
}
impl FewShotArchitectureOptimizer {
pub fn new() -> Self {
Self::with_config(FewShotConfig::default())
}
pub fn with_config(config: FewShotConfig) -> Self {
Self {
config,
support_set: Vec::new(),
is_fitted: false,
adapter_weights: None,
adapter_bias: None,
}
}
pub fn with_algorithm(mut self, algorithm: FewShotAlgorithm) -> Self {
self.config.algorithm = algorithm;
self
}
pub fn with_k_shot(mut self, k: usize) -> Self {
self.config.k_shot = k;
self
}
pub fn with_distance_metric(mut self, metric: DistanceMetric) -> Self {
self.config.distance_metric = metric;
self
}
pub fn with_temperature(mut self, t: f64) -> Self {
self.config.temperature = t;
self
}
pub fn with_seed(mut self, seed: u64) -> Self {
self.config.seed = seed;
self
}
pub fn support_set(&self) -> &[ArchitectureExample] {
&self.support_set
}
pub fn is_fitted(&self) -> bool {
self.is_fitted
}
pub fn config(&self) -> &FewShotConfig {
&self.config
}
pub fn reset(&mut self) {
self.support_set.clear();
self.is_fitted = false;
self.adapter_weights = None;
self.adapter_bias = None;
}
pub fn fit_from_examples(&mut self, examples: Vec<ArchitectureExample>) -> Result<()> {
if examples.len() < self.config.k_shot {
return Err(OptimError::InvalidParameter(format!(
"fit_from_examples: need at least k_shot = {} examples, got {}",
self.config.k_shot,
examples.len()
)));
}
if examples.is_empty() {
return Err(OptimError::InvalidParameter(
"fit_from_examples: support set is empty".to_string(),
));
}
let embedding_dim = examples[0].embedding.len();
if embedding_dim == 0 {
return Err(OptimError::InvalidParameter(
"fit_from_examples: embeddings must be non-empty".to_string(),
));
}
for (i, ex) in examples.iter().enumerate() {
if ex.embedding.len() != embedding_dim {
return Err(OptimError::InvalidParameter(format!(
"fit_from_examples: example {} has embedding dim {}, expected {}",
i,
ex.embedding.len(),
embedding_dim
)));
}
if !ex.performance.is_finite() {
return Err(OptimError::InvalidParameter(format!(
"fit_from_examples: example {} performance is not finite: {}",
i, ex.performance
)));
}
if ex.embedding.iter().any(|v| !v.is_finite()) {
return Err(OptimError::InvalidParameter(format!(
"fit_from_examples: example {} has non-finite embedding entries",
i
)));
}
}
if self.config.n_way == 0 {
return Err(OptimError::InvalidParameter(
"fit_from_examples: n_way must be at least 1".to_string(),
));
}
self.support_set = examples;
if matches!(self.config.algorithm, FewShotAlgorithm::MamlAdaptation) {
self.train_maml_adapter()?;
} else {
self.adapter_weights = None;
self.adapter_bias = None;
}
self.is_fitted = true;
Ok(())
}
pub fn fit_from_graph(
&mut self,
graph: &ArchitectureKnowledgeGraph,
source_domain: &str,
k: usize,
) -> Result<()> {
if k == 0 {
return Err(OptimError::InvalidParameter(
"fit_from_graph: k must be greater than 0".to_string(),
));
}
let mut candidates: Vec<(NodeId, PerformanceRecord)> = graph
.nodes()
.iter()
.filter(|n| n.domain == source_domain)
.filter_map(|n| n.performance.as_ref().map(|p| (n.id, p.clone())))
.collect();
if candidates.is_empty() {
return Err(OptimError::InvalidParameter(format!(
"fit_from_graph: no nodes with performance records in domain '{}'",
source_domain
)));
}
candidates.sort_by(|a, b| {
b.1.accuracy
.partial_cmp(&a.1.accuracy)
.unwrap_or(std::cmp::Ordering::Equal)
});
candidates.truncate(k);
let mut examples: Vec<ArchitectureExample> = Vec::with_capacity(candidates.len());
for (node_id, record) in candidates {
let node = graph.node(node_id).ok_or_else(|| {
OptimError::ArchitectureError(format!(
"fit_from_graph: node {} disappeared while iterating",
node_id
))
})?;
examples.push(ArchitectureExample {
arch_id: node.arch_id.clone(),
embedding: node.embedding.clone(),
performance: record.accuracy,
domain: node.domain.clone(),
});
}
self.fit_from_examples(examples)
}
pub fn predict(&self, query_embedding: &[f64]) -> Result<FewShotPrediction> {
self.check_fitted()?;
if query_embedding.is_empty() {
return Err(OptimError::InvalidParameter(
"predict: query embedding must be non-empty".to_string(),
));
}
let expected_dim = self.support_set[0].embedding.len();
if query_embedding.len() != expected_dim {
return Err(OptimError::InvalidParameter(format!(
"predict: query has {} dims, support uses {}",
query_embedding.len(),
expected_dim
)));
}
if query_embedding.iter().any(|v| !v.is_finite()) {
return Err(OptimError::InvalidParameter(
"predict: query embedding contains non-finite entries".to_string(),
));
}
match self.config.algorithm {
FewShotAlgorithm::Prototypical => self.predict_prototypical(query_embedding),
FewShotAlgorithm::Matching => self.predict_matching(query_embedding),
FewShotAlgorithm::MamlAdaptation => self.predict_maml(query_embedding),
FewShotAlgorithm::DistanceWeightedKnn => self.predict_knn(query_embedding),
}
}
pub fn predict_batch(&self, queries: &[Vec<f64>]) -> Result<Vec<FewShotPrediction>> {
let mut out = Vec::with_capacity(queries.len());
for q in queries {
out.push(self.predict(q)?);
}
Ok(out)
}
pub fn recommend_top_k(
&self,
candidates: &[(&str, Vec<f64>)],
top_k: usize,
) -> Result<Vec<(String, FewShotPrediction)>> {
self.check_fitted()?;
if top_k == 0 {
return Ok(Vec::new());
}
let mut scored: Vec<(String, FewShotPrediction)> = Vec::with_capacity(candidates.len());
for (arch_id, emb) in candidates {
let prediction = self.predict(emb)?;
scored.push(((*arch_id).to_string(), prediction));
}
scored.sort_by(|a, b| {
b.1.predicted_performance
.partial_cmp(&a.1.predicted_performance)
.unwrap_or(std::cmp::Ordering::Equal)
});
scored.truncate(top_k);
Ok(scored)
}
fn check_fitted(&self) -> Result<()> {
if !self.is_fitted || self.support_set.is_empty() {
return Err(OptimError::InvalidParameter(
"not fitted: call fit_from_examples or fit_from_graph first".to_string(),
));
}
Ok(())
}
fn distance(&self, a: &[f64], b: &[f64]) -> Result<f64> {
if a.len() != b.len() {
return Err(OptimError::InvalidParameter(format!(
"distance: length mismatch {} vs {}",
a.len(),
b.len()
)));
}
let d = match self.config.distance_metric {
DistanceMetric::Euclidean => {
let mut s = 0.0;
for (x, y) in a.iter().zip(b.iter()) {
let diff = x - y;
s += diff * diff;
}
s.sqrt()
}
DistanceMetric::Manhattan => {
let mut s = 0.0;
for (x, y) in a.iter().zip(b.iter()) {
s += (x - y).abs();
}
s
}
DistanceMetric::Cosine => {
let mut dot = 0.0;
let mut norm_a = 0.0;
let mut norm_b = 0.0;
for (x, y) in a.iter().zip(b.iter()) {
dot += x * y;
norm_a += x * x;
norm_b += y * y;
}
if norm_a <= 0.0 || norm_b <= 0.0 {
1.0
} else {
let cos = (dot / (norm_a.sqrt() * norm_b.sqrt())).clamp(-1.0, 1.0);
(1.0 - cos).clamp(0.0, 2.0)
}
}
};
if !d.is_finite() {
return Err(OptimError::InvalidParameter(format!(
"distance: produced non-finite result {}",
d
)));
}
Ok(d)
}
fn nearest_support(&self, query: &[f64]) -> Result<(usize, f64)> {
let mut best_idx = 0usize;
let mut best_d = f64::INFINITY;
for (i, ex) in self.support_set.iter().enumerate() {
let d = self.distance(query, &ex.embedding)?;
if d < best_d {
best_d = d;
best_idx = i;
}
}
Ok((best_idx, best_d))
}
fn softmax_neg(distances: &[f64], temperature: f64) -> Vec<f64> {
if distances.is_empty() {
return Vec::new();
}
let t = if temperature.is_finite() && temperature > 0.0 {
temperature
} else {
1.0
};
let mut max_logit = f64::NEG_INFINITY;
for d in distances {
let l = -d / t;
if l > max_logit {
max_logit = l;
}
}
if !max_logit.is_finite() {
let u = 1.0 / distances.len() as f64;
return vec![u; distances.len()];
}
let exps: Vec<f64> = distances
.iter()
.map(|d| ((-d / t) - max_logit).exp())
.collect();
let sum: f64 = exps.iter().sum();
if sum <= 0.0 || !sum.is_finite() {
let u = 1.0 / distances.len() as f64;
return vec![u; distances.len()];
}
exps.into_iter().map(|e| e / sum).collect()
}
fn entropy(p: &[f64]) -> f64 {
let mut h = 0.0;
for &pi in p {
if pi > 0.0 {
h -= pi * pi.ln();
}
}
h
}
fn prototype_buckets(&self) -> Vec<(Vec<f64>, f64)> {
let n_way = self.config.n_way.max(1);
let min_perf = self
.support_set
.iter()
.map(|e| e.performance)
.fold(f64::INFINITY, f64::min);
let max_perf = self
.support_set
.iter()
.map(|e| e.performance)
.fold(f64::NEG_INFINITY, f64::max);
let embedding_dim = self.support_set[0].embedding.len();
if (max_perf - min_perf).abs() < 1e-12 || n_way == 1 {
let mut proto = vec![0.0; embedding_dim];
for ex in &self.support_set {
for (slot, v) in proto.iter_mut().zip(ex.embedding.iter()) {
*slot += v;
}
}
for slot in proto.iter_mut() {
*slot /= self.support_set.len() as f64;
}
return vec![(proto, min_perf)];
}
let width = (max_perf - min_perf) / n_way as f64;
let mut sums: Vec<Vec<f64>> = vec![vec![0.0; embedding_dim]; n_way];
let mut perf_sums: Vec<f64> = vec![0.0; n_way];
let mut counts: Vec<usize> = vec![0; n_way];
for ex in &self.support_set {
let mut bin = ((ex.performance - min_perf) / width).floor() as isize;
if bin < 0 {
bin = 0;
}
if bin >= n_way as isize {
bin = (n_way - 1) as isize;
}
let b = bin as usize;
for (slot, v) in sums[b].iter_mut().zip(ex.embedding.iter()) {
*slot += v;
}
perf_sums[b] += ex.performance;
counts[b] += 1;
}
let mut buckets = Vec::new();
for b in 0..n_way {
if counts[b] == 0 {
continue;
}
let c = counts[b] as f64;
let proto: Vec<f64> = sums[b].iter().map(|v| v / c).collect();
buckets.push((proto, perf_sums[b] / c));
}
buckets
}
fn predict_prototypical(&self, query: &[f64]) -> Result<FewShotPrediction> {
let buckets = self.prototype_buckets();
if buckets.is_empty() {
return Err(OptimError::ConvergenceFailure(
"predict_prototypical: no non-empty prototype buckets".to_string(),
));
}
let distances: Vec<f64> = buckets
.iter()
.map(|(proto, _)| self.distance(query, proto))
.collect::<Result<Vec<_>>>()?;
let mut best_idx = 0;
let mut best_d = f64::INFINITY;
for (i, &d) in distances.iter().enumerate() {
if d < best_d {
best_d = d;
best_idx = i;
}
}
let weights = Self::softmax_neg(&distances, self.config.temperature);
let confidence = weights
.get(best_idx)
.copied()
.unwrap_or(0.0)
.clamp(0.0, 1.0);
let (nearest_idx, nearest_d) = self.nearest_support(query)?;
let nearest = &self.support_set[nearest_idx];
Ok(FewShotPrediction {
predicted_performance: buckets[best_idx].1,
confidence,
nearest_support_arch: Some(nearest.arch_id.clone()),
nearest_distance: nearest_d,
})
}
fn predict_matching(&self, query: &[f64]) -> Result<FewShotPrediction> {
let distances: Vec<f64> = self
.support_set
.iter()
.map(|ex| self.distance(query, &ex.embedding))
.collect::<Result<Vec<_>>>()?;
let weights = Self::softmax_neg(&distances, self.config.temperature);
let predicted: f64 = weights
.iter()
.zip(self.support_set.iter())
.map(|(w, ex)| w * ex.performance)
.sum();
let n = weights.len();
let confidence = if n <= 1 {
1.0
} else {
let h = Self::entropy(&weights);
let log_n = (n as f64).ln();
if log_n > 0.0 {
(1.0 - h / log_n).clamp(0.0, 1.0)
} else {
1.0
}
};
let (nearest_idx, nearest_d) = self.nearest_support(query)?;
let nearest = &self.support_set[nearest_idx];
Ok(FewShotPrediction {
predicted_performance: predicted,
confidence,
nearest_support_arch: Some(nearest.arch_id.clone()),
nearest_distance: nearest_d,
})
}
fn train_maml_adapter(&mut self) -> Result<()> {
let dim = self.support_set[0].embedding.len();
let n = self.support_set.len() as f64;
let lr = if self.config.adapter_lr.is_finite() && self.config.adapter_lr > 0.0 {
self.config.adapter_lr
} else {
1e-2
};
let steps = self.config.adapter_steps.max(1);
let _rng = Random::seed(self.config.seed);
let mut w: Array1<f64> = Array1::zeros(dim);
let mut b = 0.0_f64;
for _ in 0..steps {
let mut grad_w: Array1<f64> = Array1::zeros(dim);
let mut grad_b = 0.0_f64;
for ex in &self.support_set {
let x = Array1::from(ex.embedding.clone());
let y_hat = w.dot(&x) + b;
let err = y_hat - ex.performance;
for i in 0..dim {
grad_w[i] += 2.0 * err * x[i];
}
grad_b += 2.0 * err;
}
for i in 0..dim {
grad_w[i] /= n;
}
grad_b /= n;
for i in 0..dim {
w[i] -= lr * grad_w[i];
}
b -= lr * grad_b;
}
self.adapter_weights = Some(w.to_vec());
self.adapter_bias = Some(b);
Ok(())
}
fn predict_maml(&self, query: &[f64]) -> Result<FewShotPrediction> {
let w = self.adapter_weights.as_ref().ok_or_else(|| {
OptimError::InvalidParameter(
"predict_maml: adapter not trained — refit with FewShotAlgorithm::MamlAdaptation"
.to_string(),
)
})?;
let b = self.adapter_bias.unwrap_or(0.0);
if w.len() != query.len() {
return Err(OptimError::InvalidParameter(format!(
"predict_maml: adapter dim {} vs query dim {}",
w.len(),
query.len()
)));
}
let predicted: f64 = w
.iter()
.zip(query.iter())
.map(|(wi, xi)| wi * xi)
.sum::<f64>()
+ b;
let (nearest_idx, nearest_d) = self.nearest_support(query)?;
let all_d: Vec<f64> = self
.support_set
.iter()
.map(|ex| self.distance(query, &ex.embedding))
.collect::<Result<Vec<_>>>()?;
let mean_d = if all_d.is_empty() {
1.0
} else {
let s: f64 = all_d.iter().sum();
(s / all_d.len() as f64).max(1e-12)
};
let confidence = ((-nearest_d / mean_d).exp()).clamp(0.0, 1.0);
let nearest = &self.support_set[nearest_idx];
Ok(FewShotPrediction {
predicted_performance: predicted,
confidence,
nearest_support_arch: Some(nearest.arch_id.clone()),
nearest_distance: nearest_d,
})
}
fn predict_knn(&self, query: &[f64]) -> Result<FewShotPrediction> {
let mut distances: Vec<(usize, f64)> = Vec::with_capacity(self.support_set.len());
for (i, ex) in self.support_set.iter().enumerate() {
let d = self.distance(query, &ex.embedding)?;
distances.push((i, d));
}
distances.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
let k = self.config.k_shot.min(distances.len()).max(1);
let neighbours = &distances[..k];
if neighbours[0].1 < 1e-12 {
let nearest = &self.support_set[neighbours[0].0];
return Ok(FewShotPrediction {
predicted_performance: nearest.performance,
confidence: 1.0,
nearest_support_arch: Some(nearest.arch_id.clone()),
nearest_distance: neighbours[0].1,
});
}
let eps = 1e-9;
let weights: Vec<f64> = neighbours.iter().map(|(_, d)| 1.0 / (d + eps)).collect();
let weight_sum: f64 = weights.iter().sum();
let predicted: f64 = if weight_sum > 0.0 {
neighbours
.iter()
.zip(weights.iter())
.map(|((idx, _), w)| w * self.support_set[*idx].performance)
.sum::<f64>()
/ weight_sum
} else {
let mean = neighbours
.iter()
.map(|(idx, _)| self.support_set[*idx].performance)
.sum::<f64>()
/ neighbours.len() as f64;
mean
};
let mean_neighbour_d: f64 =
neighbours.iter().map(|(_, d)| d).sum::<f64>() / neighbours.len() as f64;
let max_support_d = distances
.iter()
.map(|(_, d)| *d)
.fold(0.0_f64, f64::max)
.max(1e-12);
let confidence = (1.0 - mean_neighbour_d / max_support_d).clamp(0.0, 1.0);
let nearest_idx = neighbours[0].0;
let nearest_d = neighbours[0].1;
let nearest = &self.support_set[nearest_idx];
Ok(FewShotPrediction {
predicted_performance: predicted,
confidence,
nearest_support_arch: Some(nearest.arch_id.clone()),
nearest_distance: nearest_d,
})
}
}
impl FewShotArchitectureOptimizer {
pub fn support_distance_matrix(&self) -> Result<Array2<f64>> {
self.check_fitted()?;
let n = self.support_set.len();
let mut m: Array2<f64> = Array2::zeros((n, n));
for i in 0..n {
for j in (i + 1)..n {
let d = self.distance(
&self.support_set[i].embedding,
&self.support_set[j].embedding,
)?;
m[[i, j]] = d;
m[[j, i]] = d;
}
}
Ok(m)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::architecture_knowledge_graph::{ArchitectureKnowledgeGraph, RelationType};
fn example(arch_id: &str, embedding: Vec<f64>, performance: f64) -> ArchitectureExample {
ArchitectureExample {
arch_id: arch_id.to_string(),
embedding,
performance,
domain: "vision".to_string(),
}
}
fn make_support_set() -> Vec<ArchitectureExample> {
vec![
example("a", vec![1.0, 0.0, 0.0], 0.90),
example("b", vec![0.9, 0.1, 0.0], 0.85),
example("c", vec![0.0, 1.0, 0.0], 0.50),
example("d", vec![0.0, 0.9, 0.1], 0.45),
example("e", vec![0.1, 0.1, 0.9], 0.20),
]
}
#[test]
fn test_default_config_values() {
let c = FewShotConfig::default();
assert_eq!(c.algorithm, FewShotAlgorithm::Prototypical);
assert_eq!(c.k_shot, 5);
assert_eq!(c.n_way, 2);
assert!((c.temperature - 1.0).abs() < 1e-12);
assert!((c.adapter_lr - 0.01).abs() < 1e-12);
assert_eq!(c.adapter_steps, 5);
assert_eq!(c.distance_metric, DistanceMetric::Euclidean);
assert_eq!(c.seed, 42);
}
#[test]
fn test_builder_pattern_chains() {
let opt = FewShotArchitectureOptimizer::new()
.with_algorithm(FewShotAlgorithm::Matching)
.with_k_shot(3)
.with_distance_metric(DistanceMetric::Cosine)
.with_temperature(0.5)
.with_seed(7);
assert_eq!(opt.config().algorithm, FewShotAlgorithm::Matching);
assert_eq!(opt.config().k_shot, 3);
assert_eq!(opt.config().distance_metric, DistanceMetric::Cosine);
assert!((opt.config().temperature - 0.5).abs() < 1e-12);
assert_eq!(opt.config().seed, 7);
assert!(!opt.is_fitted());
assert!(opt.support_set().is_empty());
}
#[test]
fn test_fit_from_examples_validates_min_size() {
let mut opt = FewShotArchitectureOptimizer::new().with_k_shot(5);
let examples = vec![example("a", vec![1.0, 0.0], 0.5)];
let err = opt.fit_from_examples(examples);
assert!(err.is_err(), "expected InvalidParameter for too-small set");
match err {
Err(OptimError::InvalidParameter(_)) => {}
other => panic!("expected InvalidParameter, got {:?}", other),
}
assert!(!opt.is_fitted());
}
#[test]
fn test_fit_from_examples_rejects_dim_mismatch() {
let mut opt = FewShotArchitectureOptimizer::new().with_k_shot(2);
let examples = vec![
example("a", vec![1.0, 0.0], 0.5),
example("b", vec![1.0, 0.0, 0.0], 0.6),
];
assert!(opt.fit_from_examples(examples).is_err());
}
#[test]
fn test_fit_from_examples_rejects_non_finite() {
let mut opt = FewShotArchitectureOptimizer::new().with_k_shot(2);
let examples = vec![
example("a", vec![1.0, f64::NAN], 0.5),
example("b", vec![1.0, 0.0], 0.6),
];
assert!(opt.fit_from_examples(examples).is_err());
}
#[test]
fn test_prototypical_predicts_high_for_similar_to_high_performers() {
let mut opt = FewShotArchitectureOptimizer::new()
.with_algorithm(FewShotAlgorithm::Prototypical)
.with_k_shot(5);
opt.fit_from_examples(make_support_set()).expect("fit");
let p_high = opt.predict(&[0.95, 0.05, 0.0]).expect("predict high");
let p_low = opt.predict(&[0.1, 0.1, 0.9]).expect("predict low");
assert!(
p_high.predicted_performance > p_low.predicted_performance,
"high query should outscore low: {} vs {}",
p_high.predicted_performance,
p_low.predicted_performance
);
assert!(p_high.confidence >= 0.0 && p_high.confidence <= 1.0);
assert!(p_high.nearest_support_arch.is_some());
}
#[test]
fn test_matching_networks_temperature_affects_sharpness() {
let support = make_support_set();
let mut opt_low = FewShotArchitectureOptimizer::new()
.with_algorithm(FewShotAlgorithm::Matching)
.with_temperature(0.1)
.with_k_shot(5);
opt_low.fit_from_examples(support.clone()).expect("fit low");
let mut opt_high = FewShotArchitectureOptimizer::new()
.with_algorithm(FewShotAlgorithm::Matching)
.with_temperature(10.0)
.with_k_shot(5);
opt_high.fit_from_examples(support).expect("fit high");
let q = vec![1.0, 0.0, 0.0];
let p_low = opt_low.predict(&q).expect("predict low T");
let p_high = opt_high.predict(&q).expect("predict high T");
assert!(
p_low.confidence > p_high.confidence,
"low temperature should be sharper: {} vs {}",
p_low.confidence,
p_high.confidence
);
}
#[test]
fn test_maml_adapter_decreases_training_loss() {
let mut opt = FewShotArchitectureOptimizer::new()
.with_algorithm(FewShotAlgorithm::MamlAdaptation)
.with_k_shot(5);
let support = make_support_set();
let baseline_mse: f64 =
support.iter().map(|ex| ex.performance.powi(2)).sum::<f64>() / support.len() as f64;
opt.fit_from_examples(support.clone()).expect("fit");
let trained_mse: f64 = support
.iter()
.map(|ex| {
let pred = opt
.predict(&ex.embedding)
.expect("predict")
.predicted_performance;
(pred - ex.performance).powi(2)
})
.sum::<f64>()
/ support.len() as f64;
assert!(
trained_mse < baseline_mse,
"MAML should drop MSE below zero-baseline: {} vs {}",
trained_mse,
baseline_mse
);
}
#[test]
fn test_maml_adapter_state_present_after_fit() {
let mut opt = FewShotArchitectureOptimizer::new()
.with_algorithm(FewShotAlgorithm::MamlAdaptation)
.with_k_shot(5);
opt.fit_from_examples(make_support_set()).expect("fit");
assert!(opt.adapter_weights.is_some());
assert!(opt.adapter_bias.is_some());
assert_eq!(
opt.adapter_weights.as_ref().expect("weights").len(),
3,
"adapter dim must match embedding dim"
);
}
#[test]
fn test_knn_predict_with_exact_match_returns_exact_performance() {
let mut opt = FewShotArchitectureOptimizer::new()
.with_algorithm(FewShotAlgorithm::DistanceWeightedKnn)
.with_k_shot(3);
let support = make_support_set();
opt.fit_from_examples(support.clone()).expect("fit");
let pred = opt.predict(&[1.0, 0.0, 0.0]).expect("predict");
assert!(
(pred.predicted_performance - 0.90).abs() < 1e-9,
"exact match should return exact performance, got {}",
pred.predicted_performance
);
assert!(
(pred.confidence - 1.0).abs() < 1e-9,
"confidence on exact match should be 1.0, got {}",
pred.confidence
);
assert_eq!(pred.nearest_support_arch.as_deref(), Some("a"));
}
#[test]
fn test_recommend_top_k_returns_sorted_descending() {
let mut opt = FewShotArchitectureOptimizer::new()
.with_algorithm(FewShotAlgorithm::Matching)
.with_k_shot(5);
opt.fit_from_examples(make_support_set()).expect("fit");
let candidates: Vec<(&str, Vec<f64>)> = vec![
("low-perf", vec![0.1, 0.1, 0.9]),
("mid-perf", vec![0.5, 0.5, 0.0]),
("high-perf", vec![1.0, 0.0, 0.0]),
("med-low", vec![0.0, 0.9, 0.1]),
];
let ranked = opt.recommend_top_k(&candidates, 4).expect("top_k");
assert_eq!(ranked.len(), 4);
for window in ranked.windows(2) {
assert!(
window[0].1.predicted_performance >= window[1].1.predicted_performance,
"ranking not descending: {} then {}",
window[0].1.predicted_performance,
window[1].1.predicted_performance
);
}
assert_eq!(ranked[0].0, "high-perf");
}
#[test]
fn test_recommend_top_k_truncates_to_top_k() {
let mut opt = FewShotArchitectureOptimizer::new()
.with_algorithm(FewShotAlgorithm::Matching)
.with_k_shot(5);
opt.fit_from_examples(make_support_set()).expect("fit");
let candidates: Vec<(&str, Vec<f64>)> = vec![
("a", vec![1.0, 0.0, 0.0]),
("b", vec![0.9, 0.1, 0.0]),
("c", vec![0.0, 1.0, 0.0]),
("d", vec![0.5, 0.5, 0.0]),
("e", vec![0.0, 0.0, 1.0]),
];
let top2 = opt.recommend_top_k(&candidates, 2).expect("top_k=2");
assert_eq!(top2.len(), 2);
let empty = opt.recommend_top_k(&candidates, 0).expect("top_k=0");
assert!(empty.is_empty());
let all = opt.recommend_top_k(&candidates, 100).expect("top_k=100");
assert_eq!(all.len(), candidates.len());
}
#[test]
fn test_fit_from_graph_selects_top_performers() {
let mut graph = ArchitectureKnowledgeGraph::new();
let mut ids = Vec::new();
for i in 0..10 {
let id = graph.add_architecture(
format!("arch-{}", i),
vec![i as f64 / 10.0, 0.0, 0.0],
"vision",
);
graph
.set_performance(
id,
PerformanceRecord {
task_id: "task".into(),
accuracy: 0.10 * (i + 1) as f64,
latency_ms: 1.0,
memory_mb: 1.0,
training_epochs: 1,
},
)
.expect("set_performance");
ids.push(id);
}
graph.add_architecture("nlp-a", vec![1.0, 1.0, 1.0], "nlp");
let mut opt = FewShotArchitectureOptimizer::new()
.with_algorithm(FewShotAlgorithm::DistanceWeightedKnn)
.with_k_shot(3);
opt.fit_from_graph(&graph, "vision", 3)
.expect("fit_from_graph");
assert!(opt.is_fitted());
let support = opt.support_set();
assert_eq!(support.len(), 3);
let arch_ids: Vec<&str> = support.iter().map(|e| e.arch_id.as_str()).collect();
assert!(arch_ids.contains(&"arch-9"));
assert!(arch_ids.contains(&"arch-8"));
assert!(arch_ids.contains(&"arch-7"));
assert!(arch_ids.iter().all(|id| !id.starts_with("nlp")));
}
#[test]
fn test_fit_from_graph_errors_on_empty_domain() {
let graph = ArchitectureKnowledgeGraph::new();
let mut opt = FewShotArchitectureOptimizer::new().with_k_shot(1);
assert!(opt.fit_from_graph(&graph, "vision", 5).is_err());
}
#[test]
fn test_predict_before_fit_errors() {
let opt = FewShotArchitectureOptimizer::new();
let r = opt.predict(&[1.0, 0.0, 0.0]);
assert!(r.is_err(), "predict before fit must error");
match r {
Err(OptimError::InvalidParameter(msg)) => assert!(msg.contains("not fitted")),
other => panic!("expected InvalidParameter, got {:?}", other),
}
}
#[test]
fn test_reset_clears_state() {
let mut opt = FewShotArchitectureOptimizer::new()
.with_algorithm(FewShotAlgorithm::MamlAdaptation)
.with_k_shot(5);
opt.fit_from_examples(make_support_set()).expect("fit");
assert!(opt.is_fitted());
assert!(!opt.support_set().is_empty());
assert!(opt.adapter_weights.is_some());
opt.reset();
assert!(!opt.is_fitted());
assert!(opt.support_set().is_empty());
assert!(opt.adapter_weights.is_none());
assert!(opt.adapter_bias.is_none());
}
#[test]
fn test_distance_metrics_produce_different_predictions() {
let support = make_support_set();
let mut opt_euc = FewShotArchitectureOptimizer::new()
.with_algorithm(FewShotAlgorithm::Matching)
.with_distance_metric(DistanceMetric::Euclidean)
.with_k_shot(5);
opt_euc.fit_from_examples(support.clone()).expect("fit euc");
let mut opt_cos = FewShotArchitectureOptimizer::new()
.with_algorithm(FewShotAlgorithm::Matching)
.with_distance_metric(DistanceMetric::Cosine)
.with_k_shot(5);
opt_cos.fit_from_examples(support.clone()).expect("fit cos");
let mut opt_man = FewShotArchitectureOptimizer::new()
.with_algorithm(FewShotAlgorithm::Matching)
.with_distance_metric(DistanceMetric::Manhattan)
.with_k_shot(5);
opt_man.fit_from_examples(support).expect("fit man");
let q = vec![0.5, 0.5, 0.5];
let p_euc = opt_euc.predict(&q).expect("euc predict");
let p_cos = opt_cos.predict(&q).expect("cos predict");
let p_man = opt_man.predict(&q).expect("man predict");
assert!(
(p_euc.predicted_performance - p_cos.predicted_performance).abs() > 1e-9,
"Euclidean ({}) and Cosine ({}) must differ",
p_euc.predicted_performance,
p_cos.predicted_performance
);
assert!(
(p_euc.predicted_performance - p_man.predicted_performance).abs() > 1e-12
|| (p_euc.confidence - p_man.confidence).abs() > 1e-12,
"Euclidean and Manhattan should differ in at least one summary"
);
}
#[test]
fn test_fewshot_prediction_serde_roundtrip() {
let pred = FewShotPrediction {
predicted_performance: 0.87,
confidence: 0.93,
nearest_support_arch: Some("resnet50".to_string()),
nearest_distance: 0.12,
};
let json = serde_json::to_string(&pred).expect("serialise");
let back: FewShotPrediction = serde_json::from_str(&json).expect("deserialise");
assert!((back.predicted_performance - pred.predicted_performance).abs() < 1e-12);
assert!((back.confidence - pred.confidence).abs() < 1e-12);
assert_eq!(back.nearest_support_arch, pred.nearest_support_arch);
assert!((back.nearest_distance - pred.nearest_distance).abs() < 1e-12);
}
#[test]
fn test_architecture_example_serde_roundtrip() {
let ex = example("resnet", vec![1.0, 2.0, 3.0], 0.91);
let json = serde_json::to_string(&ex).expect("serialise");
let back: ArchitectureExample = serde_json::from_str(&json).expect("deserialise");
assert_eq!(back.arch_id, ex.arch_id);
assert_eq!(back.embedding, ex.embedding);
assert!((back.performance - ex.performance).abs() < 1e-12);
assert_eq!(back.domain, ex.domain);
}
#[test]
fn test_support_distance_matrix_is_symmetric() {
let mut opt = FewShotArchitectureOptimizer::new().with_k_shot(5);
opt.fit_from_examples(make_support_set()).expect("fit");
let m = opt.support_distance_matrix().expect("matrix");
assert_eq!(m.shape(), &[5, 5]);
for i in 0..5 {
assert!(m[[i, i]].abs() < 1e-12, "diagonal {} must be 0", i);
for j in 0..5 {
assert!(
(m[[i, j]] - m[[j, i]]).abs() < 1e-12,
"matrix not symmetric at ({}, {}): {} vs {}",
i,
j,
m[[i, j]],
m[[j, i]]
);
}
}
}
#[test]
fn test_predict_batch_returns_one_per_query() {
let mut opt = FewShotArchitectureOptimizer::new()
.with_algorithm(FewShotAlgorithm::Matching)
.with_k_shot(5);
opt.fit_from_examples(make_support_set()).expect("fit");
let queries = vec![
vec![1.0, 0.0, 0.0],
vec![0.0, 1.0, 0.0],
vec![0.0, 0.0, 1.0],
];
let predictions = opt.predict_batch(&queries).expect("batch");
assert_eq!(predictions.len(), 3);
for p in &predictions {
assert!(p.confidence >= 0.0 && p.confidence <= 1.0);
assert!(p.predicted_performance.is_finite());
}
}
#[test]
fn test_softmax_neg_uniform_for_equal_distances() {
let weights = FewShotArchitectureOptimizer::softmax_neg(&[1.0, 1.0, 1.0, 1.0], 1.0);
for w in &weights {
assert!((w - 0.25).abs() < 1e-12, "expected 0.25 got {}", w);
}
}
#[test]
fn test_softmax_neg_temperature_affects_sharpness() {
let cold = FewShotArchitectureOptimizer::softmax_neg(&[0.0, 1.0, 2.0], 0.1);
let warm = FewShotArchitectureOptimizer::softmax_neg(&[0.0, 1.0, 2.0], 10.0);
assert!(cold[0] > 0.95, "expected near-1 max in cold: {:?}", cold);
for w in &warm {
assert!(
(w - 1.0 / 3.0).abs() < 0.1,
"warm should be near-uniform: {:?}",
warm
);
}
}
#[test]
fn test_entropy_handles_zero_probability() {
let h = FewShotArchitectureOptimizer::entropy(&[0.0, 1.0]);
assert!(h.abs() < 1e-12);
let h_uniform = FewShotArchitectureOptimizer::entropy(&[0.5, 0.5]);
assert!((h_uniform - (2.0_f64).ln()).abs() < 1e-12);
}
#[test]
fn test_graph_with_transfer_edges_does_not_leak() {
let mut graph = ArchitectureKnowledgeGraph::new();
let v1 = graph.add_architecture("v1", vec![1.0, 0.0, 0.0], "vision");
let n1 = graph.add_architecture("n1", vec![1.0, 0.0, 0.0], "nlp");
graph
.set_performance(
v1,
PerformanceRecord {
task_id: "t".into(),
accuracy: 0.9,
latency_ms: 1.0,
memory_mb: 1.0,
training_epochs: 1,
},
)
.expect("perf v1");
graph
.set_performance(
n1,
PerformanceRecord {
task_id: "t".into(),
accuracy: 0.95,
latency_ms: 1.0,
memory_mb: 1.0,
training_epochs: 1,
},
)
.expect("perf n1");
graph
.add_edge(n1, v1, RelationType::Transfers, 0.7)
.expect("transfers");
let mut opt = FewShotArchitectureOptimizer::new()
.with_algorithm(FewShotAlgorithm::DistanceWeightedKnn)
.with_k_shot(1);
opt.fit_from_graph(&graph, "vision", 1)
.expect("fit_from_graph");
let support = opt.support_set();
assert_eq!(support.len(), 1);
assert_eq!(support[0].arch_id, "v1");
assert!((support[0].performance - 0.9).abs() < 1e-12);
}
#[test]
fn test_predict_query_dim_mismatch_errors() {
let mut opt = FewShotArchitectureOptimizer::new().with_k_shot(5);
opt.fit_from_examples(make_support_set()).expect("fit");
let bad = opt.predict(&[1.0, 0.0]);
assert!(bad.is_err(), "expected dim mismatch error");
}
}