1use std::fmt;
43
44#[derive(Debug, Clone, PartialEq)]
48pub enum GnnActivation {
49 Relu,
51 Sigmoid,
53 Tanh,
55 Linear,
57}
58
59impl GnnActivation {
60 #[inline]
62 pub fn apply(&self, x: f64) -> f64 {
63 match self {
64 GnnActivation::Relu => x.max(0.0),
65 GnnActivation::Sigmoid => 1.0 / (1.0 + (-x).exp()),
66 GnnActivation::Tanh => x.tanh(),
67 GnnActivation::Linear => x,
68 }
69 }
70}
71
72#[derive(Debug, Clone, PartialEq)]
79pub enum GnnAggregation {
80 Sum,
82 Mean,
85 Max,
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
94pub struct GnnNodeId(pub usize);
95
96impl fmt::Display for GnnNodeId {
97 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98 write!(f, "GnnNodeId({})", self.0)
99 }
100}
101
102#[derive(Debug, Clone)]
104pub struct NodeFeatures {
105 pub id: GnnNodeId,
107 pub features: Vec<f64>,
109}
110
111#[derive(Debug, Clone)]
113pub struct GnnEdge {
114 pub from: GnnNodeId,
116 pub to: GnnNodeId,
118 pub weight: f64,
120}
121
122#[derive(Debug, Clone)]
129pub struct GnnLayer {
130 pub weights: Vec<Vec<f64>>,
132 pub bias: Vec<f64>,
134 pub activation: GnnActivation,
136}
137
138#[derive(Debug, Clone)]
142pub struct GnnConfig {
143 pub layers: Vec<GnnLayer>,
145 pub aggregation: GnnAggregation,
147 pub num_iterations: usize,
149}
150
151#[derive(Debug, Clone, PartialEq)]
155pub enum GnnError {
156 NodeNotFound(usize),
158 DimensionMismatch {
160 layer: usize,
162 expected: usize,
164 got: usize,
166 },
167 EmptyGraph,
169}
170
171impl fmt::Display for GnnError {
172 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173 match self {
174 GnnError::NodeNotFound(id) => write!(f, "node not found: {id}"),
175 GnnError::DimensionMismatch {
176 layer,
177 expected,
178 got,
179 } => {
180 write!(
181 f,
182 "dimension mismatch at layer {layer}: expected {expected}, got {got}"
183 )
184 }
185 GnnError::EmptyGraph => write!(f, "the graph is empty"),
186 }
187 }
188}
189
190impl std::error::Error for GnnError {}
191
192#[derive(Debug, Clone)]
196pub struct GnnStats {
197 pub num_nodes: usize,
199 pub num_edges: usize,
201 pub avg_degree: f64,
203 pub feature_dim: usize,
205 pub output_dim: usize,
208 pub trained_iterations: u64,
210}
211
212pub struct GraphNeuralNetwork {
219 pub config: GnnConfig,
221 pub nodes: Vec<NodeFeatures>,
223 pub adjacency: Vec<Vec<(GnnNodeId, f64)>>,
225 pub trained_iterations: u64,
227}
228
229impl GraphNeuralNetwork {
230 pub fn new(config: GnnConfig) -> Self {
234 Self {
235 config,
236 nodes: Vec::new(),
237 adjacency: Vec::new(),
238 trained_iterations: 0,
239 }
240 }
241
242 pub fn add_node(&mut self, features: Vec<f64>) -> GnnNodeId {
248 let id = GnnNodeId(self.nodes.len());
249 self.nodes.push(NodeFeatures { id, features });
250 self.adjacency.push(Vec::new());
251 id
252 }
253
254 pub fn add_edge(
263 &mut self,
264 from: GnnNodeId,
265 to: GnnNodeId,
266 weight: f64,
267 ) -> Result<(), GnnError> {
268 let n = self.nodes.len();
269 if from.0 >= n {
270 return Err(GnnError::NodeNotFound(from.0));
271 }
272 if to.0 >= n {
273 return Err(GnnError::NodeNotFound(to.0));
274 }
275 self.adjacency[from.0].push((to, weight));
276 self.adjacency[to.0].push((from, weight));
277 Ok(())
278 }
279
280 pub fn remove_node(&mut self, id: GnnNodeId) -> bool {
287 let idx = id.0;
288 if idx >= self.nodes.len() {
289 return false;
290 }
291
292 self.nodes.remove(idx);
294 self.adjacency.remove(idx);
295
296 for adj in self.adjacency.iter_mut() {
300 adj.retain(|(nb, _)| nb.0 != idx);
301 for (nb, _) in adj.iter_mut() {
302 if nb.0 > idx {
303 nb.0 -= 1;
304 }
305 }
306 }
307
308 for (i, node) in self.nodes.iter_mut().enumerate() {
310 node.id = GnnNodeId(i);
311 }
312
313 true
314 }
315
316 pub fn forward(&self) -> Vec<Vec<f64>> {
328 if self.nodes.is_empty() {
329 return Vec::new();
330 }
331
332 let mut embeddings: Vec<Vec<f64>> = self.nodes.iter().map(|n| n.features.clone()).collect();
334
335 for _ in 0..self.config.num_iterations {
336 let prev = embeddings.clone();
337 for (node_idx, emb) in embeddings.iter_mut().enumerate() {
338 let agg = self.aggregate_neighbors(GnnNodeId(node_idx), &prev);
339 let mut h = agg;
340 for layer in &self.config.layers {
341 h = Self::apply_layer(&h, layer);
342 }
343 *emb = h;
344 }
345 }
346
347 embeddings
348 }
349
350 pub fn aggregate_neighbors(&self, node_id: GnnNodeId, features: &[Vec<f64>]) -> Vec<f64> {
355 let neighbours = &self.adjacency[node_id.0];
356 if neighbours.is_empty() {
357 return features[node_id.0].clone();
358 }
359
360 let feat_dim = features[node_id.0].len();
361 if feat_dim == 0 {
363 return Vec::new();
364 }
365
366 match &self.config.aggregation {
367 GnnAggregation::Sum => {
368 let mut acc = vec![0.0f64; feat_dim];
369 for &(nb_id, weight) in neighbours {
370 let nb_feat = &features[nb_id.0];
371 let effective_dim = nb_feat.len().min(feat_dim);
372 for i in 0..effective_dim {
373 acc[i] += weight * nb_feat[i];
374 }
375 }
376 acc
377 }
378 GnnAggregation::Mean => {
379 let mut acc = vec![0.0f64; feat_dim];
380 let mut total_weight = 0.0f64;
381 for &(nb_id, weight) in neighbours {
382 let nb_feat = &features[nb_id.0];
383 let effective_dim = nb_feat.len().min(feat_dim);
384 for i in 0..effective_dim {
385 acc[i] += weight * nb_feat[i];
386 }
387 total_weight += weight;
388 }
389 if total_weight.abs() > f64::EPSILON {
390 for v in acc.iter_mut() {
391 *v /= total_weight;
392 }
393 } else {
394 return features[node_id.0].clone();
396 }
397 acc
398 }
399 GnnAggregation::Max => {
400 let mut acc = vec![f64::NEG_INFINITY; feat_dim];
401 for &(nb_id, _weight) in neighbours {
402 let nb_feat = &features[nb_id.0];
403 let effective_dim = nb_feat.len().min(feat_dim);
404 for i in 0..effective_dim {
405 if nb_feat[i] > acc[i] {
406 acc[i] = nb_feat[i];
407 }
408 }
409 }
410 let own = &features[node_id.0];
413 for (i, slot) in acc.iter_mut().enumerate() {
414 if *slot == f64::NEG_INFINITY {
415 *slot = *own.get(i).unwrap_or(&0.0);
416 }
417 }
418 acc
419 }
420 }
421 }
422
423 pub fn apply_layer(input: &[f64], layer: &GnnLayer) -> Vec<f64> {
429 layer
430 .weights
431 .iter()
432 .zip(layer.bias.iter())
433 .map(|(row, &b)| {
434 let dot: f64 = row.iter().zip(input.iter()).map(|(&w, &x)| w * x).sum();
435 layer.activation.apply(dot + b)
436 })
437 .collect()
438 }
439
440 pub fn node_embedding(&self, id: GnnNodeId) -> Result<Vec<f64>, GnnError> {
449 if self.nodes.is_empty() {
450 return Err(GnnError::EmptyGraph);
451 }
452 if id.0 >= self.nodes.len() {
453 return Err(GnnError::NodeNotFound(id.0));
454 }
455 let embeddings = self.forward();
456 Ok(embeddings[id.0].clone())
457 }
458
459 pub fn graph_embedding(&self) -> Vec<f64> {
465 if self.nodes.is_empty() {
466 return Vec::new();
467 }
468 let embeddings = self.forward();
469 if embeddings.is_empty() {
470 return Vec::new();
471 }
472 let dim = embeddings[0].len();
473 if dim == 0 {
474 return Vec::new();
475 }
476 let n = embeddings.len() as f64;
477 let mut mean = vec![0.0f64; dim];
478 for emb in &embeddings {
479 for (i, &v) in emb.iter().enumerate() {
480 if i < dim {
481 mean[i] += v;
482 }
483 }
484 }
485 for v in mean.iter_mut() {
486 *v /= n;
487 }
488 mean
489 }
490
491 pub fn num_nodes(&self) -> usize {
495 self.nodes.len()
496 }
497
498 pub fn num_edges(&self) -> usize {
500 self.adjacency.iter().map(|adj| adj.len()).sum()
501 }
502
503 pub fn stats(&self) -> GnnStats {
505 let num_nodes = self.nodes.len();
506 let num_edges = self.num_edges();
507 let avg_degree = if num_nodes > 0 {
508 num_edges as f64 / num_nodes as f64
509 } else {
510 0.0
511 };
512 let feature_dim = self.nodes.first().map(|n| n.features.len()).unwrap_or(0);
513 let output_dim = self
514 .config
515 .layers
516 .last()
517 .map(|l| l.bias.len())
518 .unwrap_or(feature_dim);
519 GnnStats {
520 num_nodes,
521 num_edges,
522 avg_degree,
523 feature_dim,
524 output_dim,
525 trained_iterations: self.trained_iterations,
526 }
527 }
528}
529
530pub fn xorshift64(state: &mut u64) -> u64 {
536 let mut x = *state;
537 x ^= x << 13;
538 x ^= x >> 7;
539 x ^= x << 17;
540 *state = x;
541 x
542}
543
544#[cfg(test)]
547mod tests {
548 use crate::graph_neural_network::{
549 xorshift64, GnnActivation, GnnAggregation, GnnConfig, GnnError, GnnLayer, GnnNodeId,
550 GnnStats, GraphNeuralNetwork, NodeFeatures,
551 };
552
553 fn identity_layer(dim: usize) -> GnnLayer {
556 let weights: Vec<Vec<f64>> = (0..dim)
557 .map(|i| {
558 let mut row = vec![0.0f64; dim];
559 row[i] = 1.0;
560 row
561 })
562 .collect();
563 GnnLayer {
564 weights,
565 bias: vec![0.0; dim],
566 activation: GnnActivation::Linear,
567 }
568 }
569
570 fn linear_config(dim: usize, iters: usize) -> GnnConfig {
571 GnnConfig {
572 layers: vec![identity_layer(dim)],
573 aggregation: GnnAggregation::Mean,
574 num_iterations: iters,
575 }
576 }
577
578 fn two_node_gnn() -> GraphNeuralNetwork {
579 let config = linear_config(2, 1);
580 let mut gnn = GraphNeuralNetwork::new(config);
581 gnn.add_node(vec![1.0, 0.0]);
582 gnn.add_node(vec![0.0, 1.0]);
583 gnn.add_edge(GnnNodeId(0), GnnNodeId(1), 1.0)
584 .expect("test: should succeed");
585 gnn
586 }
587
588 fn relu_layer(in_dim: usize, out_dim: usize) -> GnnLayer {
589 let mut state: u64 = 0xDEAD_BEEF_1234_5678;
591 let weights: Vec<Vec<f64>> = (0..out_dim)
592 .map(|_| {
593 (0..in_dim)
594 .map(|_| {
595 let r = xorshift64(&mut state);
596 (r as f64 / u64::MAX as f64) - 0.5
597 })
598 .collect()
599 })
600 .collect();
601 GnnLayer {
602 weights,
603 bias: vec![0.0; out_dim],
604 activation: GnnActivation::Relu,
605 }
606 }
607
608 #[test]
611 fn test_new_empty_graph() {
612 let config = GnnConfig {
613 layers: vec![],
614 aggregation: GnnAggregation::Mean,
615 num_iterations: 1,
616 };
617 let gnn = GraphNeuralNetwork::new(config);
618 assert_eq!(gnn.num_nodes(), 0);
619 assert_eq!(gnn.num_edges(), 0);
620 }
621
622 #[test]
625 fn test_add_node_increments_count() {
626 let mut gnn = GraphNeuralNetwork::new(linear_config(3, 1));
627 let id0 = gnn.add_node(vec![1.0, 2.0, 3.0]);
628 let id1 = gnn.add_node(vec![4.0, 5.0, 6.0]);
629 assert_eq!(id0.0, 0);
630 assert_eq!(id1.0, 1);
631 assert_eq!(gnn.num_nodes(), 2);
632 }
633
634 #[test]
637 fn test_add_edge_invalid_source() {
638 let mut gnn = GraphNeuralNetwork::new(linear_config(2, 1));
639 gnn.add_node(vec![1.0, 0.0]);
640 let result = gnn.add_edge(GnnNodeId(5), GnnNodeId(0), 1.0);
641 assert_eq!(result, Err(GnnError::NodeNotFound(5)));
642 }
643
644 #[test]
647 fn test_add_edge_invalid_dest() {
648 let mut gnn = GraphNeuralNetwork::new(linear_config(2, 1));
649 gnn.add_node(vec![1.0, 0.0]);
650 let result = gnn.add_edge(GnnNodeId(0), GnnNodeId(99), 1.0);
651 assert_eq!(result, Err(GnnError::NodeNotFound(99)));
652 }
653
654 #[test]
657 fn test_add_edge_undirected() {
658 let gnn = two_node_gnn();
659 assert_eq!(gnn.num_edges(), 2);
661 }
662
663 #[test]
666 fn test_remove_node_missing() {
667 let mut gnn = two_node_gnn();
668 assert!(!gnn.remove_node(GnnNodeId(99)));
669 }
670
671 #[test]
674 fn test_remove_node_cleans_edges() {
675 let mut gnn = two_node_gnn();
676 assert!(gnn.remove_node(GnnNodeId(0)));
677 assert_eq!(gnn.num_nodes(), 1);
678 assert_eq!(gnn.num_edges(), 0);
680 }
681
682 #[test]
685 fn test_remove_node_renumbers() {
686 let mut gnn = GraphNeuralNetwork::new(linear_config(2, 1));
687 gnn.add_node(vec![1.0, 0.0]);
688 gnn.add_node(vec![0.0, 1.0]);
689 gnn.add_node(vec![1.0, 1.0]);
690 gnn.remove_node(GnnNodeId(0));
691 assert_eq!(gnn.nodes[0].id, GnnNodeId(0));
693 assert_eq!(gnn.nodes[1].id, GnnNodeId(1));
694 }
695
696 #[test]
699 fn test_forward_empty_graph() {
700 let gnn = GraphNeuralNetwork::new(linear_config(2, 1));
701 let out = gnn.forward();
702 assert!(out.is_empty());
703 }
704
705 #[test]
708 fn test_forward_identity_layer_single_node() {
709 let mut gnn = GraphNeuralNetwork::new(linear_config(3, 2));
710 gnn.add_node(vec![1.0, 2.0, 3.0]);
711 let out = gnn.forward();
712 assert_eq!(out.len(), 1);
713 assert!((out[0][0] - 1.0).abs() < 1e-9);
715 assert!((out[0][1] - 2.0).abs() < 1e-9);
716 assert!((out[0][2] - 3.0).abs() < 1e-9);
717 }
718
719 #[test]
722 fn test_forward_output_dimension() {
723 let layer = relu_layer(3, 5);
724 let config = GnnConfig {
725 layers: vec![layer],
726 aggregation: GnnAggregation::Sum,
727 num_iterations: 1,
728 };
729 let mut gnn = GraphNeuralNetwork::new(config);
730 gnn.add_node(vec![1.0, 2.0, 3.0]);
731 let out = gnn.forward();
732 assert_eq!(out[0].len(), 5);
733 }
734
735 #[test]
738 fn test_aggregate_mean_equal_weights() {
739 let gnn = two_node_gnn();
740 let features = vec![vec![2.0, 4.0], vec![6.0, 8.0]];
741 let agg = gnn.aggregate_neighbors(GnnNodeId(0), &features);
743 assert!((agg[0] - 6.0).abs() < 1e-9);
744 assert!((agg[1] - 8.0).abs() < 1e-9);
745 }
746
747 #[test]
750 fn test_aggregate_sum() {
751 let config = GnnConfig {
752 layers: vec![],
753 aggregation: GnnAggregation::Sum,
754 num_iterations: 1,
755 };
756 let mut gnn = GraphNeuralNetwork::new(config);
757 gnn.add_node(vec![1.0, 0.0]);
758 gnn.add_node(vec![3.0, 4.0]);
759 gnn.add_edge(GnnNodeId(0), GnnNodeId(1), 2.0)
760 .expect("test: should succeed");
761
762 let features = vec![vec![1.0, 0.0], vec![3.0, 4.0]];
763 let agg = gnn.aggregate_neighbors(GnnNodeId(0), &features);
764 assert!((agg[0] - 6.0).abs() < 1e-9);
766 assert!((agg[1] - 8.0).abs() < 1e-9);
767 }
768
769 #[test]
772 fn test_aggregate_max() {
773 let config = GnnConfig {
774 layers: vec![],
775 aggregation: GnnAggregation::Max,
776 num_iterations: 1,
777 };
778 let mut gnn = GraphNeuralNetwork::new(config);
779 gnn.add_node(vec![1.0, 5.0]);
780 gnn.add_node(vec![3.0, 2.0]);
781 gnn.add_node(vec![0.5, 7.0]);
782 gnn.add_edge(GnnNodeId(0), GnnNodeId(1), 1.0)
783 .expect("test: should succeed");
784 gnn.add_edge(GnnNodeId(0), GnnNodeId(2), 1.0)
785 .expect("test: should succeed");
786
787 let features = vec![vec![1.0, 5.0], vec![3.0, 2.0], vec![0.5, 7.0]];
788 let agg = gnn.aggregate_neighbors(GnnNodeId(0), &features);
789 assert!((agg[0] - 3.0).abs() < 1e-9);
791 assert!((agg[1] - 7.0).abs() < 1e-9);
792 }
793
794 #[test]
797 fn test_isolated_node_mean_returns_own() {
798 let config = GnnConfig {
799 layers: vec![],
800 aggregation: GnnAggregation::Mean,
801 num_iterations: 1,
802 };
803 let mut gnn = GraphNeuralNetwork::new(config);
804 gnn.add_node(vec![9.0, 8.0]);
805 let features = vec![vec![9.0, 8.0]];
806 let agg = gnn.aggregate_neighbors(GnnNodeId(0), &features);
807 assert!((agg[0] - 9.0).abs() < 1e-9);
808 assert!((agg[1] - 8.0).abs() < 1e-9);
809 }
810
811 #[test]
814 fn test_apply_layer_linear() {
815 let layer = GnnLayer {
816 weights: vec![vec![1.0, 2.0], vec![3.0, 4.0]],
817 bias: vec![0.5, -0.5],
818 activation: GnnActivation::Linear,
819 };
820 let input = vec![1.0, 1.0];
821 let out = GraphNeuralNetwork::apply_layer(&input, &layer);
822 assert!((out[0] - 3.5).abs() < 1e-9);
825 assert!((out[1] - 6.5).abs() < 1e-9);
826 }
827
828 #[test]
831 fn test_apply_layer_relu() {
832 let layer = GnnLayer {
833 weights: vec![vec![-1.0, 0.0], vec![1.0, 0.0]],
834 bias: vec![0.0, 0.0],
835 activation: GnnActivation::Relu,
836 };
837 let input = vec![1.0, 0.0];
838 let out = GraphNeuralNetwork::apply_layer(&input, &layer);
839 assert!((out[0] - 0.0).abs() < 1e-9); assert!((out[1] - 1.0).abs() < 1e-9);
841 }
842
843 #[test]
846 fn test_apply_layer_sigmoid_range() {
847 let layer = GnnLayer {
848 weights: vec![vec![1.0]],
849 bias: vec![0.0],
850 activation: GnnActivation::Sigmoid,
851 };
852 let out_neg = GraphNeuralNetwork::apply_layer(&[-20.0], &layer);
854 assert!(out_neg[0] >= 0.0 && out_neg[0] < 0.01);
855
856 let out_pos = GraphNeuralNetwork::apply_layer(&[20.0], &layer);
858 assert!(out_pos[0] > 0.99 && out_pos[0] <= 1.0);
859
860 let mid = GraphNeuralNetwork::apply_layer(&[0.0], &layer);
862 assert!((mid[0] - 0.5).abs() < 1e-9);
863 }
864
865 #[test]
868 fn test_apply_layer_tanh_range() {
869 let layer = GnnLayer {
870 weights: vec![vec![1.0]],
871 bias: vec![0.0],
872 activation: GnnActivation::Tanh,
873 };
874 let out_neg = GraphNeuralNetwork::apply_layer(&[-20.0], &layer);
876 assert!(out_neg[0] >= -1.0 && out_neg[0] < -0.99);
877
878 let out_zero = GraphNeuralNetwork::apply_layer(&[0.0], &layer);
879 assert!((out_zero[0]).abs() < 1e-9);
880
881 let out_pos = GraphNeuralNetwork::apply_layer(&[20.0], &layer);
883 assert!(out_pos[0] > 0.99 && out_pos[0] <= 1.0);
884 }
885
886 #[test]
889 fn test_node_embedding_valid() {
890 let gnn = two_node_gnn();
891 let emb = gnn.node_embedding(GnnNodeId(0));
892 assert!(emb.is_ok());
893 assert_eq!(emb.expect("test: should succeed").len(), 2);
894 }
895
896 #[test]
899 fn test_node_embedding_not_found() {
900 let gnn = two_node_gnn();
901 let result = gnn.node_embedding(GnnNodeId(10));
902 assert_eq!(result, Err(GnnError::NodeNotFound(10)));
903 }
904
905 #[test]
908 fn test_node_embedding_empty_graph() {
909 let gnn = GraphNeuralNetwork::new(linear_config(2, 1));
910 let result = gnn.node_embedding(GnnNodeId(0));
911 assert_eq!(result, Err(GnnError::EmptyGraph));
912 }
913
914 #[test]
917 fn test_graph_embedding_empty() {
918 let gnn = GraphNeuralNetwork::new(linear_config(2, 1));
919 let emb = gnn.graph_embedding();
920 assert!(emb.is_empty());
921 }
922
923 #[test]
926 fn test_graph_embedding_dimension() {
927 let gnn = two_node_gnn();
928 let emb = gnn.graph_embedding();
929 assert_eq!(emb.len(), 2);
930 }
931
932 #[test]
935 fn test_graph_embedding_is_mean() {
936 let config = GnnConfig {
937 layers: vec![],
938 aggregation: GnnAggregation::Mean,
939 num_iterations: 0,
940 };
941 let mut gnn = GraphNeuralNetwork::new(config);
942 gnn.add_node(vec![2.0, 4.0]);
943 gnn.add_node(vec![6.0, 8.0]);
944
945 let emb = gnn.graph_embedding();
947 assert!((emb[0] - 4.0).abs() < 1e-9);
948 assert!((emb[1] - 6.0).abs() < 1e-9);
949 }
950
951 #[test]
954 fn test_num_edges() {
955 let mut gnn = GraphNeuralNetwork::new(linear_config(2, 1));
956 let a = gnn.add_node(vec![1.0, 0.0]);
957 let b = gnn.add_node(vec![0.0, 1.0]);
958 let c = gnn.add_node(vec![1.0, 1.0]);
959 gnn.add_edge(a, b, 1.0).expect("test: should succeed");
960 gnn.add_edge(b, c, 1.0).expect("test: should succeed");
961 assert_eq!(gnn.num_edges(), 4);
963 }
964
965 #[test]
968 fn test_stats() {
969 let mut gnn = GraphNeuralNetwork::new(linear_config(2, 1));
970 let a = gnn.add_node(vec![1.0, 0.0]);
971 let b = gnn.add_node(vec![0.0, 1.0]);
972 gnn.add_edge(a, b, 1.0).expect("test: should succeed");
973 let s: GnnStats = gnn.stats();
974 assert_eq!(s.num_nodes, 2);
975 assert_eq!(s.num_edges, 2);
976 assert!((s.avg_degree - 1.0).abs() < 1e-9);
977 assert_eq!(s.feature_dim, 2);
978 assert_eq!(s.output_dim, 2);
979 }
980
981 #[test]
984 fn test_gnn_error_display() {
985 let e = GnnError::DimensionMismatch {
986 layer: 0,
987 expected: 4,
988 got: 3,
989 };
990 let s = format!("{e}");
991 assert!(s.contains("dimension mismatch"));
992 assert!(s.contains("layer 0"));
993 }
994
995 #[test]
998 fn test_gnn_node_id_display() {
999 let id = GnnNodeId(42);
1000 assert_eq!(format!("{id}"), "GnnNodeId(42)");
1001 }
1002
1003 #[test]
1006 fn test_multi_layer_forward() {
1007 let l1 = relu_layer(2, 4);
1009 let l2 = GnnLayer {
1010 weights: vec![vec![0.25, 0.25, 0.25, 0.25], vec![0.25, 0.25, 0.25, 0.25]],
1011 bias: vec![0.0, 0.0],
1012 activation: GnnActivation::Linear,
1013 };
1014 let config = GnnConfig {
1015 layers: vec![l1, l2],
1016 aggregation: GnnAggregation::Mean,
1017 num_iterations: 2,
1018 };
1019 let mut gnn = GraphNeuralNetwork::new(config);
1020 let a = gnn.add_node(vec![1.0, 0.0]);
1021 let b = gnn.add_node(vec![0.0, 1.0]);
1022 gnn.add_edge(a, b, 1.0).expect("test: should succeed");
1023 let out = gnn.forward();
1024 assert_eq!(out.len(), 2);
1025 assert_eq!(out[0].len(), 2);
1026 assert_eq!(out[1].len(), 2);
1027 }
1028
1029 #[test]
1032 fn test_xorshift64_nonzero() {
1033 let mut state = 1u64;
1034 for _ in 0..10_000 {
1035 let v = xorshift64(&mut state);
1036 assert_ne!(v, 0);
1037 }
1038 }
1039
1040 #[test]
1043 fn test_xorshift64_state_advances() {
1044 let mut state = 0xABCD_1234u64;
1045 let v1 = xorshift64(&mut state);
1046 let v2 = xorshift64(&mut state);
1047 assert_ne!(v1, v2);
1048 }
1049
1050 #[test]
1053 fn test_activation_apply_all_variants() {
1054 assert!((GnnActivation::Relu.apply(-5.0) - 0.0).abs() < 1e-9);
1055 assert!((GnnActivation::Relu.apply(3.0) - 3.0).abs() < 1e-9);
1056
1057 let sig = GnnActivation::Sigmoid.apply(0.0);
1058 assert!((sig - 0.5).abs() < 1e-9);
1059
1060 let t = GnnActivation::Tanh.apply(0.0);
1061 assert!(t.abs() < 1e-9);
1062
1063 assert!((GnnActivation::Linear.apply(7.77) - 7.77).abs() < 1e-9);
1064 }
1065
1066 #[test]
1069 fn test_mean_zero_weight_fallback() {
1070 let config = GnnConfig {
1071 layers: vec![],
1072 aggregation: GnnAggregation::Mean,
1073 num_iterations: 1,
1074 };
1075 let mut gnn = GraphNeuralNetwork::new(config);
1076 gnn.add_node(vec![3.0, 7.0]);
1077 gnn.add_node(vec![1.0, 2.0]);
1078 gnn.add_edge(GnnNodeId(0), GnnNodeId(1), 0.0)
1080 .expect("test: should succeed");
1081
1082 let features = vec![vec![3.0, 7.0], vec![1.0, 2.0]];
1083 let agg = gnn.aggregate_neighbors(GnnNodeId(0), &features);
1084 assert!((agg[0] - 3.0).abs() < 1e-9);
1086 assert!((agg[1] - 7.0).abs() < 1e-9);
1087 }
1088
1089 #[test]
1092 fn test_node_features_accessible() {
1093 let nf = NodeFeatures {
1094 id: GnnNodeId(3),
1095 features: vec![0.1, 0.2, 0.3],
1096 };
1097 assert_eq!(nf.id.0, 3);
1098 assert!((nf.features[2] - 0.3).abs() < 1e-9);
1099 }
1100
1101 #[test]
1104 fn test_large_graph_forward() {
1105 let layer = relu_layer(4, 8);
1106 let config = GnnConfig {
1107 layers: vec![layer],
1108 aggregation: GnnAggregation::Sum,
1109 num_iterations: 3,
1110 };
1111 let mut gnn = GraphNeuralNetwork::new(config);
1112 let mut state: u64 = 0x1234_5678_9ABC_DEF0;
1113
1114 for _ in 0..20 {
1116 let features: Vec<f64> = (0..4)
1117 .map(|_| (xorshift64(&mut state) as f64 / u64::MAX as f64) * 2.0 - 1.0)
1118 .collect();
1119 gnn.add_node(features);
1120 }
1121
1122 for i in 0..20 {
1124 gnn.add_edge(GnnNodeId(i), GnnNodeId((i + 1) % 20), 1.0)
1125 .expect("test: should succeed");
1126 }
1127
1128 let out = gnn.forward();
1129 assert_eq!(out.len(), 20);
1130 for emb in &out {
1131 assert_eq!(emb.len(), 8);
1132 for &v in emb {
1134 assert!(v >= 0.0);
1135 }
1136 }
1137 }
1138
1139 #[test]
1142 fn test_forward_after_remove_node() {
1143 let mut gnn = GraphNeuralNetwork::new(linear_config(2, 1));
1144 gnn.add_node(vec![1.0, 0.0]);
1145 gnn.add_node(vec![0.0, 1.0]);
1146 gnn.add_node(vec![0.5, 0.5]);
1147 gnn.add_edge(GnnNodeId(0), GnnNodeId(1), 1.0)
1148 .expect("test: should succeed");
1149 gnn.add_edge(GnnNodeId(1), GnnNodeId(2), 1.0)
1150 .expect("test: should succeed");
1151 gnn.remove_node(GnnNodeId(1));
1152 assert_eq!(gnn.num_nodes(), 2);
1153 assert_eq!(gnn.num_edges(), 0);
1155 let out = gnn.forward();
1156 assert_eq!(out.len(), 2);
1157 }
1158
1159 #[test]
1162 fn test_self_loop_treated_as_edge() {
1163 let config = GnnConfig {
1164 layers: vec![],
1165 aggregation: GnnAggregation::Sum,
1166 num_iterations: 1,
1167 };
1168 let mut gnn = GraphNeuralNetwork::new(config);
1169 gnn.add_node(vec![2.0, 3.0]);
1170 let res = gnn.add_edge(GnnNodeId(0), GnnNodeId(0), 1.0);
1172 assert!(res.is_ok());
1173 }
1174
1175 #[test]
1178 fn test_trained_iterations_initial() {
1179 let gnn = GraphNeuralNetwork::new(linear_config(2, 1));
1180 assert_eq!(gnn.trained_iterations, 0);
1181 let s = gnn.stats();
1182 assert_eq!(s.trained_iterations, 0);
1183 }
1184
1185 #[test]
1188 fn test_gnn_error_node_not_found_display() {
1189 let e = GnnError::NodeNotFound(7);
1190 let s = format!("{e}");
1191 assert!(s.contains("7"));
1192 }
1193
1194 #[test]
1197 fn test_gnn_error_empty_graph_display() {
1198 let e = GnnError::EmptyGraph;
1199 let s = format!("{e}");
1200 assert!(s.contains("empty"));
1201 }
1202
1203 #[test]
1206 fn test_gnn_error_is_std_error() {
1207 let e: Box<dyn std::error::Error> = Box::new(GnnError::EmptyGraph);
1208 assert!(!e.to_string().is_empty());
1209 }
1210
1211 #[test]
1214 fn test_zero_iterations_returns_raw_features() {
1215 let config = GnnConfig {
1216 layers: vec![],
1217 aggregation: GnnAggregation::Mean,
1218 num_iterations: 0,
1219 };
1220 let mut gnn = GraphNeuralNetwork::new(config);
1221 gnn.add_node(vec![5.0, 6.0]);
1222 gnn.add_node(vec![7.0, 8.0]);
1223 gnn.add_edge(GnnNodeId(0), GnnNodeId(1), 1.0)
1224 .expect("test: should succeed");
1225 let out = gnn.forward();
1226 assert!((out[0][0] - 5.0).abs() < 1e-9);
1228 assert!((out[1][1] - 8.0).abs() < 1e-9);
1229 }
1230
1231 #[test]
1234 fn test_weighted_edge_mean() {
1235 let config = GnnConfig {
1236 layers: vec![],
1237 aggregation: GnnAggregation::Mean,
1238 num_iterations: 1,
1239 };
1240 let mut gnn = GraphNeuralNetwork::new(config);
1241 gnn.add_node(vec![0.0]);
1242 gnn.add_node(vec![2.0]);
1243 gnn.add_node(vec![8.0]);
1244 gnn.add_edge(GnnNodeId(0), GnnNodeId(1), 1.0)
1246 .expect("test: should succeed");
1247 gnn.add_edge(GnnNodeId(0), GnnNodeId(2), 3.0)
1248 .expect("test: should succeed");
1249
1250 let features = vec![vec![0.0], vec![2.0], vec![8.0]];
1251 let agg = gnn.aggregate_neighbors(GnnNodeId(0), &features);
1252 assert!((agg[0] - 6.5).abs() < 1e-9);
1254 }
1255
1256 #[test]
1259 fn test_graph_embedding_single_node() {
1260 let config = GnnConfig {
1261 layers: vec![],
1262 aggregation: GnnAggregation::Mean,
1263 num_iterations: 0,
1264 };
1265 let mut gnn = GraphNeuralNetwork::new(config);
1266 gnn.add_node(vec![3.0, 9.0]);
1267 let ge = gnn.graph_embedding();
1268 let ne = gnn
1269 .node_embedding(GnnNodeId(0))
1270 .expect("test: should succeed");
1271 for (g, n) in ge.iter().zip(ne.iter()) {
1272 assert!((g - n).abs() < 1e-9);
1273 }
1274 }
1275
1276 #[test]
1279 fn test_stats_output_dim_last_layer() {
1280 let l1 = relu_layer(2, 16);
1281 let l2 = relu_layer(16, 4);
1282 let config = GnnConfig {
1283 layers: vec![l1, l2],
1284 aggregation: GnnAggregation::Mean,
1285 num_iterations: 1,
1286 };
1287 let mut gnn = GraphNeuralNetwork::new(config);
1288 gnn.add_node(vec![1.0, 2.0]);
1289 let s = gnn.stats();
1290 assert_eq!(s.output_dim, 4);
1291 }
1292}