1use crate::error::{OptimError, Result};
42use serde::{Deserialize, Serialize};
43use std::collections::HashMap;
44
45pub type NodeId = usize;
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
54pub enum RelationType {
55 Similar,
58 Derived,
61 Outperforms,
63 Transfers,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
70pub struct PerformanceRecord {
71 pub task_id: String,
73 pub accuracy: f64,
75 pub latency_ms: f64,
77 pub memory_mb: f64,
79 pub training_epochs: usize,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct ArchKnowledgeNode {
86 pub id: NodeId,
88 pub arch_id: String,
90 pub embedding: Vec<f64>,
92 pub performance: Option<PerformanceRecord>,
94 pub domain: String,
96 pub metadata: HashMap<String, String>,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct ArchKnowledgeEdge {
103 pub from: NodeId,
105 pub to: NodeId,
107 pub relation: RelationType,
109 pub weight: f64,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct ArchitectureKnowledgeGraph {
121 nodes: Vec<ArchKnowledgeNode>,
123 edges: Vec<ArchKnowledgeEdge>,
125 #[serde(skip)]
128 adjacency: HashMap<NodeId, Vec<usize>>,
129 next_id: NodeId,
131}
132
133impl Default for ArchitectureKnowledgeGraph {
134 fn default() -> Self {
135 Self::new()
136 }
137}
138
139impl ArchitectureKnowledgeGraph {
140 pub fn new() -> Self {
142 Self {
143 nodes: Vec::new(),
144 edges: Vec::new(),
145 adjacency: HashMap::new(),
146 next_id: 0,
147 }
148 }
149
150 pub fn with_capacity(n: usize) -> Self {
152 Self {
153 nodes: Vec::with_capacity(n),
154 edges: Vec::with_capacity(n),
155 adjacency: HashMap::with_capacity(n),
156 next_id: 0,
157 }
158 }
159
160 pub fn len(&self) -> usize {
162 self.nodes.len()
163 }
164
165 pub fn is_empty(&self) -> bool {
167 self.nodes.is_empty()
168 }
169
170 pub fn edge_count(&self) -> usize {
172 self.edges.len()
173 }
174
175 fn rebuild_adjacency(&mut self) {
179 self.adjacency.clear();
180 for (idx, edge) in self.edges.iter().enumerate() {
181 self.adjacency.entry(edge.from).or_default().push(idx);
182 }
183 }
184
185 pub fn add_architecture(
187 &mut self,
188 arch_id: impl Into<String>,
189 embedding: Vec<f64>,
190 domain: impl Into<String>,
191 ) -> NodeId {
192 let id = self.next_id;
193 self.next_id += 1;
194 let node = ArchKnowledgeNode {
195 id,
196 arch_id: arch_id.into(),
197 embedding,
198 performance: None,
199 domain: domain.into(),
200 metadata: HashMap::new(),
201 };
202 self.nodes.push(node);
203 id
204 }
205
206 pub fn set_performance(&mut self, node: NodeId, record: PerformanceRecord) -> Result<()> {
208 let target = self.nodes.get_mut(node).ok_or_else(|| {
209 OptimError::InvalidParameter(format!(
210 "set_performance: node id {} does not exist",
211 node
212 ))
213 })?;
214 target.performance = Some(record);
215 Ok(())
216 }
217
218 pub fn add_edge(
223 &mut self,
224 from: NodeId,
225 to: NodeId,
226 relation: RelationType,
227 weight: f64,
228 ) -> Result<()> {
229 if from >= self.nodes.len() {
230 return Err(OptimError::InvalidParameter(format!(
231 "add_edge: source node {} does not exist",
232 from
233 )));
234 }
235 if to >= self.nodes.len() {
236 return Err(OptimError::InvalidParameter(format!(
237 "add_edge: target node {} does not exist",
238 to
239 )));
240 }
241 if !weight.is_finite() {
242 return Err(OptimError::InvalidParameter(format!(
243 "add_edge: weight must be finite, got {}",
244 weight
245 )));
246 }
247 if matches!(relation, RelationType::Similar) && !(-1.0..=1.0).contains(&weight) {
248 return Err(OptimError::InvalidParameter(format!(
249 "add_edge: Similar weight must be in [-1, 1], got {}",
250 weight
251 )));
252 }
253
254 let edge_index = self.edges.len();
255 self.edges.push(ArchKnowledgeEdge {
256 from,
257 to,
258 relation,
259 weight,
260 });
261 self.adjacency.entry(from).or_default().push(edge_index);
262 Ok(())
263 }
264
265 pub fn node(&self, id: NodeId) -> Option<&ArchKnowledgeNode> {
267 self.nodes.get(id)
268 }
269
270 pub fn nodes(&self) -> &[ArchKnowledgeNode] {
272 &self.nodes
273 }
274
275 pub fn neighbors(&self, id: NodeId) -> Vec<(NodeId, RelationType, f64)> {
280 match self.adjacency.get(&id) {
281 None => Vec::new(),
282 Some(indices) => indices
283 .iter()
284 .filter_map(|&i| self.edges.get(i))
285 .map(|e| (e.to, e.relation, e.weight))
286 .collect(),
287 }
288 }
289
290 pub fn cosine_similarity(a: &[f64], b: &[f64]) -> f64 {
296 if a.len() != b.len() || a.is_empty() {
297 return 0.0;
298 }
299 let mut dot = 0.0;
300 let mut norm_a = 0.0;
301 let mut norm_b = 0.0;
302 for (x, y) in a.iter().zip(b.iter()) {
303 if !x.is_finite() || !y.is_finite() {
304 return 0.0;
305 }
306 dot += x * y;
307 norm_a += x * x;
308 norm_b += y * y;
309 }
310 if norm_a <= 0.0 || norm_b <= 0.0 {
311 return 0.0;
312 }
313 let denom = norm_a.sqrt() * norm_b.sqrt();
314 if denom <= 0.0 {
315 return 0.0;
316 }
317 dot / denom
318 }
319
320 pub fn find_similar(&self, query_embedding: &[f64], k: usize) -> Vec<(NodeId, f64)> {
326 if k == 0 || self.nodes.is_empty() {
327 return Vec::new();
328 }
329 let mut scored: Vec<(NodeId, f64)> = self
330 .nodes
331 .iter()
332 .map(|n| (n.id, Self::cosine_similarity(query_embedding, &n.embedding)))
333 .collect();
334 scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
335 scored.truncate(k);
336 scored
337 }
338
339 pub fn auto_add_similar_edges(&mut self, threshold: f64) -> usize {
347 if self.nodes.len() < 2 {
348 return 0;
349 }
350 let mut to_add: Vec<(NodeId, NodeId, f64)> = Vec::new();
353 for i in 0..self.nodes.len() {
354 for j in (i + 1)..self.nodes.len() {
355 let sim =
356 Self::cosine_similarity(&self.nodes[i].embedding, &self.nodes[j].embedding);
357 if sim >= threshold && sim.is_finite() {
358 to_add.push((self.nodes[i].id, self.nodes[j].id, sim));
359 }
360 }
361 }
362
363 let mut added = 0usize;
364 for (from, to, sim) in to_add {
365 let exists = self
367 .adjacency
368 .get(&from)
369 .map(|indices| {
370 indices.iter().any(|&idx| {
371 let edge = &self.edges[idx];
372 edge.to == to && edge.relation == RelationType::Similar
373 })
374 })
375 .unwrap_or(false);
376 if exists {
377 continue;
378 }
379 let clamped = sim.clamp(-1.0, 1.0);
382 if self
383 .add_edge(from, to, RelationType::Similar, clamped)
384 .is_ok()
385 {
386 added += 1;
387 }
388 }
389 added
390 }
391
392 pub fn propagate_knowledge(
401 &self,
402 start: NodeId,
403 decay: f64,
404 depth: usize,
405 ) -> Result<HashMap<NodeId, f64>> {
406 if !decay.is_finite() || decay <= 0.0 || decay >= 1.0 {
407 return Err(OptimError::InvalidParameter(format!(
408 "propagate_knowledge: decay must be in (0, 1), got {}",
409 decay
410 )));
411 }
412 if depth == 0 {
413 return Err(OptimError::InvalidParameter(
414 "propagate_knowledge: depth must be > 0".to_string(),
415 ));
416 }
417 if start >= self.nodes.len() {
418 return Err(OptimError::InvalidParameter(format!(
419 "propagate_knowledge: start node {} does not exist",
420 start
421 )));
422 }
423
424 let mut scores: HashMap<NodeId, f64> = HashMap::new();
425 scores.insert(start, 1.0);
426
427 for _ in 0..depth {
428 let mut new_scores: HashMap<NodeId, f64> = HashMap::new();
433 new_scores.insert(start, 1.0);
434
435 for (&node, &score) in scores.iter() {
436 if score == 0.0 {
437 continue;
438 }
439 let neighbours = self.neighbors(node);
440 if neighbours.is_empty() {
441 *new_scores.entry(start).or_insert(0.0) += decay * score;
444 continue;
445 }
446 let weight_sum: f64 = neighbours.iter().map(|(_, _, w)| w.abs()).sum();
447 if weight_sum > 0.0 {
448 for (neigh, _, w) in &neighbours {
449 let contribution = decay * score * w.abs() / weight_sum;
450 *new_scores.entry(*neigh).or_insert(0.0) += contribution;
451 }
452 } else {
453 let share = decay * score / neighbours.len() as f64;
455 for (neigh, _, _) in &neighbours {
456 *new_scores.entry(*neigh).or_insert(0.0) += share;
457 }
458 }
459 }
460 scores = new_scores;
461 }
462 Ok(scores)
463 }
464
465 pub fn extract_top_k_transferable(
474 &self,
475 target_domain: &str,
476 query_embedding: &[f64],
477 k: usize,
478 ) -> Vec<(NodeId, f64)> {
479 if k == 0 || self.nodes.is_empty() {
480 return Vec::new();
481 }
482 let mut transferable: std::collections::HashSet<NodeId> = std::collections::HashSet::new();
485 for node in &self.nodes {
486 if node.domain == target_domain {
487 transferable.insert(node.id);
488 }
489 }
490 for edge in &self.edges {
491 if edge.relation != RelationType::Transfers {
492 continue;
493 }
494 let from_domain = self.nodes.get(edge.from).map(|n| n.domain.as_str());
495 let to_domain = self.nodes.get(edge.to).map(|n| n.domain.as_str());
496 if from_domain == Some(target_domain) {
497 transferable.insert(edge.to);
498 }
499 if to_domain == Some(target_domain) {
500 transferable.insert(edge.from);
501 }
502 }
503
504 let mut scored: Vec<(NodeId, f64)> = self
505 .nodes
506 .iter()
507 .filter(|n| transferable.contains(&n.id))
508 .map(|n| (n.id, Self::cosine_similarity(query_embedding, &n.embedding)))
509 .collect();
510 scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
511 scored.truncate(k);
512 scored
513 }
514
515 pub fn to_json(&self) -> Result<String> {
517 serde_json::to_string_pretty(self).map_err(|e| {
518 OptimError::ArchitectureError(format!("Failed to serialise knowledge graph: {}", e))
519 })
520 }
521
522 pub fn from_json(s: &str) -> Result<Self> {
524 let mut graph: Self = serde_json::from_str(s).map_err(|e| {
525 OptimError::ArchitectureError(format!("Failed to deserialise knowledge graph: {}", e))
526 })?;
527 graph.rebuild_adjacency();
528 if !graph.nodes.is_empty() {
530 let max = graph.nodes.iter().map(|n| n.id).max().unwrap_or(0);
531 if graph.next_id <= max {
532 graph.next_id = max + 1;
533 }
534 }
535 Ok(graph)
536 }
537}
538
539#[cfg(test)]
540mod tests {
541 use super::*;
542
543 fn make_record(task: &str, accuracy: f64) -> PerformanceRecord {
544 PerformanceRecord {
545 task_id: task.to_string(),
546 accuracy,
547 latency_ms: 10.0,
548 memory_mb: 64.0,
549 training_epochs: 5,
550 }
551 }
552
553 fn make_linear_graph(n: usize) -> (ArchitectureKnowledgeGraph, Vec<NodeId>) {
554 let mut graph = ArchitectureKnowledgeGraph::with_capacity(n);
555 let mut ids = Vec::new();
556 for i in 0..n {
557 let emb = vec![i as f64 + 1.0, 0.0, 0.0];
558 ids.push(graph.add_architecture(format!("arch-{}", i), emb, "vision"));
559 }
560 for i in 0..(n - 1) {
561 graph
562 .add_edge(ids[i], ids[i + 1], RelationType::Derived, 1.0)
563 .expect("linear edge");
564 }
565 (graph, ids)
566 }
567
568 #[test]
569 fn test_add_architecture_returns_increasing_ids() {
570 let mut graph = ArchitectureKnowledgeGraph::new();
571 let a = graph.add_architecture("a", vec![1.0, 2.0], "vision");
572 let b = graph.add_architecture("b", vec![3.0, 4.0], "vision");
573 let c = graph.add_architecture("c", vec![5.0, 6.0], "nlp");
574 assert_eq!(a, 0);
575 assert_eq!(b, 1);
576 assert_eq!(c, 2);
577 assert_eq!(graph.len(), 3);
578 assert!(!graph.is_empty());
579 assert_eq!(graph.edge_count(), 0);
580 assert_eq!(graph.node(a).expect("present").arch_id, "a");
581 assert!(graph.node(99).is_none());
582 }
583
584 #[test]
585 fn test_add_edge_validates_endpoints() {
586 let mut graph = ArchitectureKnowledgeGraph::new();
587 let a = graph.add_architecture("a", vec![1.0, 0.0], "vision");
588 let b = graph.add_architecture("b", vec![0.0, 1.0], "vision");
589 graph
590 .add_edge(a, b, RelationType::Outperforms, 0.5)
591 .expect("valid edge");
592 assert_eq!(graph.edge_count(), 1);
593 assert!(graph.add_edge(a, 42, RelationType::Derived, 1.0).is_err());
594 assert!(graph.add_edge(99, b, RelationType::Derived, 1.0).is_err());
595 assert!(graph
597 .add_edge(a, b, RelationType::Derived, f64::NAN)
598 .is_err());
599 assert!(graph.add_edge(a, b, RelationType::Similar, 1.5).is_err());
601 graph
603 .add_edge(a, b, RelationType::Similar, 0.9)
604 .expect("similar in range");
605 }
606
607 #[test]
608 fn test_cosine_similarity_identical_is_one() {
609 let v = vec![1.0, 2.0, 3.0, -4.0];
610 let sim = ArchitectureKnowledgeGraph::cosine_similarity(&v, &v);
611 assert!((sim - 1.0).abs() < 1e-12, "expected 1.0, got {}", sim);
612 }
613
614 #[test]
615 fn test_cosine_similarity_orthogonal_is_zero() {
616 let a = [1.0, 0.0, 0.0];
617 let b = [0.0, 1.0, 0.0];
618 let sim = ArchitectureKnowledgeGraph::cosine_similarity(&a, &b);
619 assert!(sim.abs() < 1e-12, "expected 0.0, got {}", sim);
620 }
621
622 #[test]
623 fn test_cosine_similarity_zero_norm_returns_zero() {
624 let zero = [0.0, 0.0, 0.0];
625 let other = [1.0, 2.0, 3.0];
626 assert_eq!(
627 ArchitectureKnowledgeGraph::cosine_similarity(&zero, &other),
628 0.0
629 );
630 assert_eq!(
631 ArchitectureKnowledgeGraph::cosine_similarity(&other, &zero),
632 0.0
633 );
634 assert_eq!(
635 ArchitectureKnowledgeGraph::cosine_similarity(&zero, &zero),
636 0.0
637 );
638 assert_eq!(
640 ArchitectureKnowledgeGraph::cosine_similarity(&[1.0, 0.0], &[1.0, 0.0, 0.0]),
641 0.0
642 );
643 assert_eq!(
645 ArchitectureKnowledgeGraph::cosine_similarity(&[] as &[f64], &[] as &[f64]),
646 0.0
647 );
648 }
649
650 #[test]
651 fn test_find_similar_returns_top_k_sorted_descending() {
652 let mut graph = ArchitectureKnowledgeGraph::new();
653 graph.add_architecture("a", vec![1.0, 0.0, 0.0], "vision");
654 graph.add_architecture("b", vec![0.9, 0.1, 0.0], "vision");
655 graph.add_architecture("c", vec![0.0, 1.0, 0.0], "vision");
656 graph.add_architecture("d", vec![-1.0, 0.0, 0.0], "vision");
657
658 let top = graph.find_similar(&[1.0, 0.0, 0.0], 3);
659 assert_eq!(top.len(), 3);
660 assert!(top[0].1 >= top[1].1);
662 assert!(top[1].1 >= top[2].1);
663 assert_eq!(top[0].0, 0);
665 assert!(graph.find_similar(&[1.0, 0.0, 0.0], 0).is_empty());
667 let empty = ArchitectureKnowledgeGraph::new();
669 assert!(empty.find_similar(&[1.0, 0.0, 0.0], 5).is_empty());
670 }
671
672 #[test]
673 fn test_auto_add_similar_edges_threshold_respected() {
674 let mut graph = ArchitectureKnowledgeGraph::new();
675 graph.add_architecture("a", vec![1.0, 0.0, 0.0], "vision");
676 graph.add_architecture("b", vec![0.99, 0.01, 0.0], "vision"); graph.add_architecture("c", vec![0.0, 1.0, 0.0], "vision"); let added = graph.auto_add_similar_edges(0.95);
680 assert_eq!(added, 1, "only the a-b pair should pass 0.95 threshold");
681 assert_eq!(graph.edge_count(), 1);
682 let edge = &graph.edges[0];
683 assert_eq!(edge.relation, RelationType::Similar);
684 assert!(edge.weight >= 0.95);
685
686 let added_again = graph.auto_add_similar_edges(0.95);
688 assert_eq!(added_again, 0);
689 assert_eq!(graph.edge_count(), 1);
690
691 let added_low = graph.auto_add_similar_edges(-1.0);
695 assert!(added_low >= 1);
696 }
697
698 #[test]
699 fn test_propagate_knowledge_start_is_one() {
700 let (graph, ids) = make_linear_graph(3);
701 let scores = graph
702 .propagate_knowledge(ids[0], 0.85, 5)
703 .expect("propagation");
704 let start_score = scores.get(&ids[0]).copied().unwrap_or(0.0);
705 assert!(
706 start_score >= 1.0 - 1e-9,
707 "start score should be >= 1.0, got {}",
708 start_score
709 );
710 }
711
712 #[test]
713 fn test_propagate_knowledge_closer_higher_score() {
714 let (graph, ids) = make_linear_graph(4);
715 let scores = graph
716 .propagate_knowledge(ids[0], 0.85, 8)
717 .expect("propagation");
718 let s1 = scores.get(&ids[1]).copied().unwrap_or(0.0);
719 let s2 = scores.get(&ids[2]).copied().unwrap_or(0.0);
720 let s3 = scores.get(&ids[3]).copied().unwrap_or(0.0);
721 assert!(
722 s1 > s2,
723 "direct neighbour score ({}) should exceed 2-hop ({})",
724 s1,
725 s2
726 );
727 assert!(s2 > s3, "2-hop ({}) should exceed 3-hop ({})", s2, s3);
728 }
729
730 #[test]
731 fn test_propagate_knowledge_invalid_decay_errors() {
732 let (graph, ids) = make_linear_graph(2);
733 assert!(graph.propagate_knowledge(ids[0], 0.0, 3).is_err());
734 assert!(graph.propagate_knowledge(ids[0], 1.0, 3).is_err());
735 assert!(graph.propagate_knowledge(ids[0], -0.1, 3).is_err());
736 assert!(graph.propagate_knowledge(ids[0], 1.1, 3).is_err());
737 assert!(graph.propagate_knowledge(ids[0], f64::NAN, 3).is_err());
738 assert!(graph.propagate_knowledge(ids[0], 0.5, 0).is_err());
740 assert!(graph.propagate_knowledge(99, 0.5, 3).is_err());
742 }
743
744 #[test]
745 fn test_propagate_knowledge_dangling_returns_to_start() {
746 let mut graph = ArchitectureKnowledgeGraph::new();
750 let s = graph.add_architecture("solo", vec![1.0, 0.0], "vision");
751 let scores = graph
752 .propagate_knowledge(s, 0.5, 3)
753 .expect("propagation on dangling");
754 let start_score = scores.get(&s).copied().unwrap_or(0.0);
755 assert!(
758 start_score >= 1.0 - 1e-9,
759 "expected dangling start to be >= 1.0, got {}",
760 start_score
761 );
762 assert_eq!(scores.len(), 1);
764 }
765
766 #[test]
767 fn test_json_roundtrip_preserves_graph() {
768 let mut graph = ArchitectureKnowledgeGraph::new();
769 let a = graph.add_architecture("a", vec![1.0, 0.0, 0.0], "vision");
770 let b = graph.add_architecture("b", vec![0.9, 0.1, 0.0], "vision");
771 let c = graph.add_architecture("c", vec![0.0, 1.0, 0.0], "nlp");
772 graph
773 .add_edge(a, b, RelationType::Similar, 0.99)
774 .expect("similar");
775 graph
776 .add_edge(b, c, RelationType::Transfers, 0.5)
777 .expect("transfers");
778 graph
779 .set_performance(a, make_record("imagenet", 0.78))
780 .expect("perf");
781
782 let json = graph.to_json().expect("to_json");
783 let restored = ArchitectureKnowledgeGraph::from_json(&json).expect("from_json");
784
785 assert_eq!(restored.len(), graph.len());
786 assert_eq!(restored.edge_count(), graph.edge_count());
787 for (orig, back) in graph.nodes().iter().zip(restored.nodes().iter()) {
788 assert_eq!(orig.id, back.id);
789 assert_eq!(orig.arch_id, back.arch_id);
790 assert_eq!(orig.domain, back.domain);
791 assert_eq!(orig.embedding, back.embedding);
792 assert_eq!(orig.performance, back.performance);
793 }
794 let neigh = restored.neighbors(a);
796 assert_eq!(neigh.len(), 1);
797 assert_eq!(neigh[0].0, b);
798 assert_eq!(neigh[0].1, RelationType::Similar);
799 }
800
801 #[test]
802 fn test_extract_top_k_transferable_filters_by_domain() {
803 let mut graph = ArchitectureKnowledgeGraph::new();
804 let v1 = graph.add_architecture("v1", vec![1.0, 0.0, 0.0], "vision");
805 let v2 = graph.add_architecture("v2", vec![0.9, 0.1, 0.0], "vision");
806 let n1 = graph.add_architecture("n1", vec![0.95, 0.0, 0.05], "nlp");
807 let n2 = graph.add_architecture("n2", vec![0.0, 0.0, 1.0], "nlp");
808 graph
810 .add_edge(n1, v1, RelationType::Transfers, 0.7)
811 .expect("transfers");
812
813 let top = graph.extract_top_k_transferable("vision", &[1.0, 0.0, 0.0], 5);
814 let ids: Vec<NodeId> = top.iter().map(|t| t.0).collect();
815 assert!(ids.contains(&v1));
817 assert!(ids.contains(&v2));
818 assert!(ids.contains(&n1));
819 assert!(!ids.contains(&n2));
821 for window in top.windows(2) {
823 assert!(window[0].1 >= window[1].1);
824 }
825 assert!(graph
827 .extract_top_k_transferable("vision", &[1.0, 0.0, 0.0], 0)
828 .is_empty());
829 }
830
831 #[test]
832 fn test_set_performance_on_valid_node() {
833 let mut graph = ArchitectureKnowledgeGraph::new();
834 let a = graph.add_architecture("a", vec![1.0, 0.0], "vision");
835 let record = make_record("cifar10", 0.91);
836 graph
837 .set_performance(a, record.clone())
838 .expect("set_performance");
839 let node = graph.node(a).expect("present");
840 let attached = node.performance.as_ref().expect("performance");
841 assert_eq!(attached.task_id, "cifar10");
842 assert!((attached.accuracy - 0.91).abs() < 1e-12);
843 assert_eq!(attached.training_epochs, 5);
844 }
845
846 #[test]
847 fn test_set_performance_on_invalid_node_errors() {
848 let mut graph = ArchitectureKnowledgeGraph::new();
849 graph.add_architecture("a", vec![1.0, 0.0], "vision");
850 let err = graph.set_performance(42, make_record("cifar10", 0.5));
851 assert!(err.is_err(), "expected error for missing node");
852 }
853
854 #[test]
855 fn test_neighbors_on_unknown_node_is_empty() {
856 let graph = ArchitectureKnowledgeGraph::new();
857 assert!(graph.neighbors(0).is_empty());
858 let mut graph = ArchitectureKnowledgeGraph::new();
859 graph.add_architecture("a", vec![1.0, 0.0], "vision");
860 assert!(graph.neighbors(0).is_empty()); assert!(graph.neighbors(99).is_empty()); }
863}