1use rayon::prelude::*;
23use std::collections::{HashMap, VecDeque};
24
25pub type NodeId = usize;
27
28#[derive(Debug, Clone, PartialEq)]
30pub struct Edge {
31 pub source: NodeId,
32 pub target: NodeId,
33 pub weight: Option<f64>,
34}
35
36#[derive(Debug)]
48pub struct Graph {
49 row_ptr: Vec<usize>, col_indices: Vec<NodeId>, #[allow(dead_code)]
53 edge_weights: Vec<f64>, #[allow(dead_code)]
57 node_labels: Vec<Option<String>>, #[allow(dead_code)]
59 label_to_id: HashMap<String, NodeId>, is_directed: bool,
62 n_nodes: usize,
63 n_edges: usize,
64}
65
66impl Graph {
67 pub fn new(is_directed: bool) -> Self {
80 Self {
81 row_ptr: vec![0],
82 col_indices: Vec::new(),
83 edge_weights: Vec::new(),
84 node_labels: Vec::new(),
85 label_to_id: HashMap::new(),
86 is_directed,
87 n_nodes: 0,
88 n_edges: 0,
89 }
90 }
91
92 pub fn num_nodes(&self) -> usize {
94 self.n_nodes
95 }
96
97 pub fn num_edges(&self) -> usize {
99 self.n_edges
100 }
101
102 pub fn is_directed(&self) -> bool {
104 self.is_directed
105 }
106
107 pub fn neighbors(&self, v: NodeId) -> &[NodeId] {
123 if v >= self.n_nodes {
124 return &[];
125 }
126 let start = self.row_ptr[v];
127 let end = self.row_ptr[v + 1];
128 &self.col_indices[start..end]
129 }
130
131 pub fn from_edges(edges: &[(NodeId, NodeId)], is_directed: bool) -> Self {
149 if edges.is_empty() {
150 return Self::new(is_directed);
151 }
152
153 let max_node = edges.iter().flat_map(|&(s, t)| [s, t]).max().unwrap_or(0);
155 let n_nodes = max_node + 1;
156
157 let mut adj_list: Vec<Vec<NodeId>> = vec![Vec::new(); n_nodes];
159 for &(source, target) in edges {
160 adj_list[source].push(target);
161 if !is_directed && source != target {
162 adj_list[target].push(source);
164 }
165 }
166
167 for neighbors in &mut adj_list {
169 neighbors.sort_unstable();
170 neighbors.dedup();
171 }
172
173 let mut row_ptr = Vec::with_capacity(n_nodes + 1);
175 let mut col_indices = Vec::new();
176
177 row_ptr.push(0);
178 for neighbors in &adj_list {
179 col_indices.extend_from_slice(neighbors);
180 row_ptr.push(col_indices.len());
181 }
182
183 let n_edges = if is_directed {
184 edges.len()
185 } else {
186 edges.len()
188 };
189
190 Self {
191 row_ptr,
192 col_indices,
193 edge_weights: Vec::new(),
194 node_labels: vec![None; n_nodes],
195 label_to_id: HashMap::new(),
196 is_directed,
197 n_nodes,
198 n_edges,
199 }
200 }
201
202 pub fn degree_centrality(&self) -> HashMap<NodeId, f64> {
224 let mut centrality = HashMap::with_capacity(self.n_nodes);
225
226 if self.n_nodes <= 1 {
227 for v in 0..self.n_nodes {
229 centrality.insert(v, 0.0);
230 }
231 return centrality;
232 }
233
234 let norm = (self.n_nodes - 1) as f64;
235
236 #[allow(clippy::needless_range_loop)]
237 for v in 0..self.n_nodes {
238 let degree = self.neighbors(v).len() as f64;
239 centrality.insert(v, degree / norm);
240 }
241
242 centrality
243 }
244
245 pub fn pagerank(&self, damping: f64, max_iter: usize, tol: f64) -> Result<Vec<f64>, String> {
271 if self.n_nodes == 0 {
272 return Ok(Vec::new());
273 }
274
275 let n = self.n_nodes;
276 let mut ranks = vec![1.0 / n as f64; n];
277 let mut new_ranks = vec![0.0; n];
278
279 for _ in 0..max_iter {
280 let mut dangling_sum = 0.0;
283 #[allow(clippy::needless_range_loop)]
284 for v in 0..n {
285 if self.neighbors(v).is_empty() {
286 dangling_sum += ranks[v];
287 }
288 }
289 let dangling_contribution = damping * dangling_sum / n as f64;
290
291 #[allow(clippy::needless_range_loop)]
293 for v in 0..n {
294 let incoming_neighbors = self.incoming_neighbors(v);
295
296 let mut sum = 0.0;
297 let mut c = 0.0; for u in &incoming_neighbors {
300 let out_degree = self.neighbors(*u).len() as f64;
301 if out_degree > 0.0 {
302 let y = (ranks[*u] / out_degree) - c;
303 let t = sum + y;
304 c = (t - sum) - y; sum = t;
306 }
307 }
308
309 new_ranks[v] = (1.0 - damping) / n as f64 + damping * sum + dangling_contribution;
310 }
311
312 let diff = kahan_diff(&ranks, &new_ranks);
314 if diff < tol {
315 return Ok(new_ranks);
316 }
317
318 std::mem::swap(&mut ranks, &mut new_ranks);
319 }
320
321 Ok(ranks)
322 }
323
324 fn incoming_neighbors(&self, v: NodeId) -> Vec<NodeId> {
329 if !self.is_directed {
330 return self.neighbors(v).to_vec();
332 }
333
334 let mut incoming = Vec::new();
336 for u in 0..self.n_nodes {
337 if self.neighbors(u).contains(&v) {
338 incoming.push(u);
339 }
340 }
341 incoming
342 }
343
344 pub fn betweenness_centrality(&self) -> Vec<f64> {
369 if self.n_nodes == 0 {
370 return Vec::new();
371 }
372
373 let partial_scores: Vec<Vec<f64>> = (0..self.n_nodes)
375 .into_par_iter()
376 .map(|source| self.brandes_bfs_from_source(source))
377 .collect();
378
379 let mut centrality = vec![0.0; self.n_nodes];
381 for partial in partial_scores {
382 for (i, &score) in partial.iter().enumerate() {
383 centrality[i] += score;
384 }
385 }
386
387 if !self.is_directed {
389 for score in &mut centrality {
390 *score /= 2.0;
391 }
392 }
393
394 centrality
395 }
396
397 fn brandes_bfs_from_source(&self, source: NodeId) -> Vec<f64> {
402 let n = self.n_nodes;
403 let mut stack = Vec::new(); let mut paths = vec![0u64; n]; let mut distance = vec![i32::MAX; n]; let mut predecessors: Vec<Vec<NodeId>> = vec![Vec::new(); n]; let mut dependency = vec![0.0; n]; paths[source] = 1;
411 distance[source] = 0;
412 let mut queue = VecDeque::new();
413 queue.push_back(source);
414
415 while let Some(v) = queue.pop_front() {
417 stack.push(v);
418 for &w in self.neighbors(v) {
419 if distance[w] == i32::MAX {
421 distance[w] = distance[v] + 1;
422 queue.push_back(w);
423 }
424 if distance[w] == distance[v] + 1 {
426 paths[w] = paths[w].saturating_add(paths[v]);
427 predecessors[w].push(v);
428 }
429 }
430 }
431
432 while let Some(w) = stack.pop() {
434 for &v in &predecessors[w] {
435 let coeff = (paths[v] as f64 / paths[w] as f64) * (1.0 + dependency[w]);
436 dependency[v] += coeff;
437 }
438 }
439
440 dependency
441 }
442
443 pub fn modularity(&self, communities: &[Vec<NodeId>]) -> f64 {
461 if self.n_nodes == 0 || communities.is_empty() {
462 return 0.0;
463 }
464
465 let mut community_map = vec![None; self.n_nodes];
467 for (comm_id, community) in communities.iter().enumerate() {
468 for &node in community {
469 community_map[node] = Some(comm_id);
470 }
471 }
472
473 let m = self.n_edges as f64;
475
476 if m == 0.0 {
477 return 0.0;
478 }
479
480 let two_m = 2.0 * m;
481
482 let degrees: Vec<f64> = (0..self.n_nodes)
484 .map(|i| self.neighbors(i).len() as f64)
485 .collect();
486
487 let mut q = 0.0;
489
490 for i in 0..self.n_nodes {
491 for j in 0..self.n_nodes {
492 match (community_map[i], community_map[j]) {
494 (Some(ci), Some(cj)) if ci == cj => {
495 let a_ij = if self.neighbors(i).contains(&j) {
497 1.0
498 } else {
499 0.0
500 };
501
502 let expected = (degrees[i] * degrees[j]) / two_m;
504
505 q += a_ij - expected;
506 }
507 _ => {}
508 }
509 }
510 }
511
512 q / two_m
513 }
514
515 pub fn louvain(&self) -> Vec<Vec<NodeId>> {
545 if self.n_nodes == 0 {
546 return Vec::new();
547 }
548
549 let mut node_to_comm: Vec<usize> = (0..self.n_nodes).collect();
551 let mut improved = true;
552 let mut iteration = 0;
553 let max_iterations = 100;
554
555 while improved && iteration < max_iterations {
557 improved = false;
558 iteration += 1;
559
560 for node in 0..self.n_nodes {
561 let current_comm = node_to_comm[node];
562 let neighbors = self.neighbors(node);
563
564 if neighbors.is_empty() {
565 continue;
566 }
567
568 let mut best_comm = current_comm;
570 let mut best_gain = 0.0;
571
572 let mut tried_comms = std::collections::HashSet::new();
574 for &neighbor in neighbors {
575 let neighbor_comm = node_to_comm[neighbor];
576
577 if tried_comms.contains(&neighbor_comm) {
578 continue;
579 }
580 tried_comms.insert(neighbor_comm);
581
582 let gain =
584 self.modularity_gain(node, current_comm, neighbor_comm, &node_to_comm);
585
586 if gain > best_gain {
587 best_gain = gain;
588 best_comm = neighbor_comm;
589 }
590 }
591
592 if best_comm != current_comm && best_gain > 1e-10 {
594 node_to_comm[node] = best_comm;
595 improved = true;
596 }
597 }
598 }
599
600 let mut communities: HashMap<usize, Vec<NodeId>> = HashMap::new();
602
603 for (node, &comm) in node_to_comm.iter().enumerate() {
604 communities.entry(comm).or_default().push(node);
605 }
606
607 communities.into_values().collect()
608 }
609
610 fn modularity_gain(
612 &self,
613 node: NodeId,
614 from_comm: usize,
615 to_comm: usize,
616 node_to_comm: &[usize],
617 ) -> f64 {
618 if from_comm == to_comm {
619 return 0.0;
620 }
621
622 let m = self.n_edges as f64;
623 if m == 0.0 {
624 return 0.0;
625 }
626
627 let two_m = 2.0 * m;
628 let k_i = self.neighbors(node).len() as f64;
629
630 let mut k_i_to = 0.0;
632 for &neighbor in self.neighbors(node) {
633 if node_to_comm[neighbor] == to_comm {
634 k_i_to += 1.0;
635 }
636 }
637
638 let mut k_i_from = 0.0;
640 for &neighbor in self.neighbors(node) {
641 if node_to_comm[neighbor] == from_comm && neighbor != node {
642 k_i_from += 1.0;
643 }
644 }
645
646 let sigma_tot_to: f64 = node_to_comm
648 .iter()
649 .enumerate()
650 .filter(|&(n, &comm)| comm == to_comm && n != node)
651 .map(|(n, _)| self.neighbors(n).len() as f64)
652 .sum();
653
654 let sigma_tot_from: f64 = node_to_comm
656 .iter()
657 .enumerate()
658 .filter(|&(n, &comm)| comm == from_comm && n != node)
659 .map(|(n, _)| self.neighbors(n).len() as f64)
660 .sum();
661
662 (k_i_to - k_i_from) / m - k_i * (sigma_tot_to - sigma_tot_from) / (two_m * m)
664 }
665}
666
667fn kahan_diff(a: &[f64], b: &[f64]) -> f64 {
672 let mut sum = 0.0;
673 let mut c = 0.0;
674 for i in 0..a.len() {
675 let y = (a[i] - b[i]).abs() - c;
676 let t = sum + y;
677 c = (t - sum) - y;
678 sum = t;
679 }
680 sum
681}
682
683#[cfg(test)]
684mod tests {
685 use super::*;
686
687 #[test]
688 fn test_empty_graph() {
689 let g = Graph::new(false);
690 assert_eq!(g.num_nodes(), 0);
691 assert_eq!(g.num_edges(), 0);
692 assert!(!g.is_directed());
693 }
694
695 #[test]
696 fn test_directed_graph() {
697 let g = Graph::new(true);
698 assert!(g.is_directed());
699 }
700
701 #[test]
702 fn test_from_edges_empty() {
703 let g = Graph::from_edges(&[], false);
704 assert_eq!(g.num_nodes(), 0);
705 assert_eq!(g.num_edges(), 0);
706 }
707
708 #[test]
709 fn test_from_edges_undirected() {
710 let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0)], false);
711 assert_eq!(g.num_nodes(), 3);
712 assert_eq!(g.num_edges(), 3);
713
714 assert_eq!(g.neighbors(0), &[1, 2]);
716 assert_eq!(g.neighbors(1), &[0, 2]);
717 assert_eq!(g.neighbors(2), &[0, 1]);
718 }
719
720 #[test]
721 fn test_from_edges_directed() {
722 let g = Graph::from_edges(&[(0, 1), (1, 2)], true);
723 assert_eq!(g.num_nodes(), 3);
724 assert_eq!(g.num_edges(), 2);
725
726 assert_eq!(g.neighbors(0), &[1]);
728 assert_eq!(g.neighbors(1), &[2]);
729 assert!(g.neighbors(2).is_empty()); }
731
732 #[test]
733 fn test_from_edges_with_gaps() {
734 let g = Graph::from_edges(&[(0, 5), (5, 10)], false);
736 assert_eq!(g.num_nodes(), 11); assert_eq!(g.num_edges(), 2);
738
739 assert_eq!(g.neighbors(0), &[5]);
740 assert_eq!(g.neighbors(5), &[0, 10]);
741 assert!(g.neighbors(1).is_empty()); }
743
744 #[test]
745 fn test_from_edges_duplicate_edges() {
746 let g = Graph::from_edges(&[(0, 1), (0, 1), (1, 0)], false);
748 assert_eq!(g.num_nodes(), 2);
749
750 assert_eq!(g.neighbors(0), &[1]);
752 assert_eq!(g.neighbors(1), &[0]);
753 }
754
755 #[test]
756 fn test_from_edges_self_loop() {
757 let g = Graph::from_edges(&[(0, 0), (0, 1)], false);
758 assert_eq!(g.num_nodes(), 2);
759
760 assert_eq!(g.neighbors(0), &[0, 1]);
762 }
763
764 #[test]
765 fn test_neighbors_invalid_node() {
766 let g = Graph::from_edges(&[(0, 1)], false);
767 assert!(g.neighbors(999).is_empty()); }
769
770 #[test]
771 fn test_degree_centrality_empty() {
772 let g = Graph::new(false);
773 let dc = g.degree_centrality();
774 assert_eq!(dc.len(), 0);
775 }
776
777 #[test]
778 fn test_degree_centrality_single_node() {
779 let g = Graph::from_edges(&[(0, 0)], false);
780 let dc = g.degree_centrality();
781 assert_eq!(dc[&0], 0.0); }
783
784 #[test]
785 fn test_degree_centrality_star_graph() {
786 let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3)], false);
788 let dc = g.degree_centrality();
789
790 assert_eq!(dc[&0], 1.0); assert!((dc[&1] - 1.0 / 3.0).abs() < 1e-6); assert!((dc[&2] - 1.0 / 3.0).abs() < 1e-6);
793 assert!((dc[&3] - 1.0 / 3.0).abs() < 1e-6);
794 }
795
796 #[test]
797 fn test_degree_centrality_complete_graph() {
798 let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], false);
800 let dc = g.degree_centrality();
801
802 for v in 0..4 {
804 assert_eq!(dc[&v], 1.0);
805 }
806 }
807
808 #[test]
809 fn test_degree_centrality_path_graph() {
810 let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3)], false);
812 let dc = g.degree_centrality();
813
814 assert!((dc[&0] - 1.0 / 3.0).abs() < 1e-6);
816 assert!((dc[&1] - 2.0 / 3.0).abs() < 1e-6);
817 assert!((dc[&2] - 2.0 / 3.0).abs() < 1e-6);
818 assert!((dc[&3] - 1.0 / 3.0).abs() < 1e-6);
819 }
820
821 #[test]
822 fn test_degree_centrality_directed() {
823 let g = Graph::from_edges(&[(0, 1), (0, 2), (1, 2)], true);
825 let dc = g.degree_centrality();
826
827 assert!((dc[&0] - 2.0 / 2.0).abs() < 1e-6); assert!((dc[&1] - 1.0 / 2.0).abs() < 1e-6); assert_eq!(dc[&2], 0.0); }
831
832 #[test]
835 fn test_pagerank_empty() {
836 let g = Graph::new(true);
837 let pr = g
838 .pagerank(0.85, 100, 1e-6)
839 .expect("pagerank should succeed for empty graph");
840 assert!(pr.is_empty());
841 }
842
843 #[test]
844 fn test_pagerank_single_node() {
845 let g = Graph::from_edges(&[(0, 0)], true);
846 let pr = g
847 .pagerank(0.85, 100, 1e-6)
848 .expect("pagerank should succeed for single node graph");
849 assert_eq!(pr.len(), 1);
850 assert!((pr[0] - 1.0).abs() < 1e-6); }
852
853 #[test]
854 fn test_pagerank_sum_equals_one() {
855 let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0)], true);
857 let pr = g
858 .pagerank(0.85, 100, 1e-6)
859 .expect("pagerank should converge for cycle graph");
860 let sum: f64 = pr.iter().sum();
861 assert!((sum - 1.0).abs() < 1e-10); }
863
864 #[test]
865 fn test_pagerank_cycle_graph() {
866 let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0)], true);
869 let pr = g
870 .pagerank(0.85, 100, 1e-6)
871 .expect("pagerank should converge for symmetric cycle");
872
873 assert_eq!(pr.len(), 3);
874 assert!((pr[0] - 1.0 / 3.0).abs() < 1e-6);
876 assert!((pr[1] - 1.0 / 3.0).abs() < 1e-6);
877 assert!((pr[2] - 1.0 / 3.0).abs() < 1e-6);
878 }
879
880 #[test]
881 fn test_pagerank_star_graph_directed() {
882 let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3)], true);
885 let pr = g
886 .pagerank(0.85, 100, 1e-6)
887 .expect("pagerank should converge for directed star graph");
888
889 assert_eq!(pr.len(), 4);
890 assert!(pr[0] < pr[1]); assert!((pr[1] - pr[2]).abs() < 1e-6); assert!((pr[2] - pr[3]).abs() < 1e-6);
895 }
896
897 #[test]
898 fn test_pagerank_convergence() {
899 let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0), (1, 0)], true);
901 let pr = g
902 .pagerank(0.85, 100, 1e-6)
903 .expect("pagerank should converge within max iterations");
904
905 assert_eq!(pr.len(), 3);
907 assert!((pr.iter().sum::<f64>() - 1.0).abs() < 1e-10);
908 }
909
910 #[test]
911 fn test_pagerank_no_outgoing_edges() {
912 let g = Graph::from_edges(&[(0, 1), (1, 2)], true);
914 let pr = g
915 .pagerank(0.85, 100, 1e-6)
916 .expect("pagerank should handle dangling nodes correctly");
917
918 assert_eq!(pr.len(), 3);
920 assert!(pr[2] > 0.0);
921 assert!((pr.iter().sum::<f64>() - 1.0).abs() < 1e-10);
922 }
923
924 #[test]
925 fn test_pagerank_undirected() {
926 let g = Graph::from_edges(&[(0, 1), (1, 2)], false);
928 let pr = g
929 .pagerank(0.85, 100, 1e-6)
930 .expect("pagerank should converge for undirected path graph");
931
932 assert_eq!(pr.len(), 3);
933 assert!(pr[1] > pr[0]);
935 assert!(pr[1] > pr[2]);
936 assert!((pr[0] - pr[2]).abs() < 1e-6); }
938
939 #[test]
940 fn test_kahan_diff() {
941 let a = vec![1.0, 2.0, 3.0];
942 let b = vec![1.1, 2.1, 2.9];
943 let diff = kahan_diff(&a, &b);
944 assert!((diff - 0.3).abs() < 1e-10);
945 }
946
947 #[test]
950 fn test_betweenness_centrality_empty() {
951 let g = Graph::new(false);
952 let bc = g.betweenness_centrality();
953 assert!(bc.is_empty());
954 }
955
956 #[test]
957 fn test_betweenness_centrality_single_node() {
958 let g = Graph::from_edges(&[(0, 0)], false);
959 let bc = g.betweenness_centrality();
960 assert_eq!(bc.len(), 1);
961 assert_eq!(bc[0], 0.0); }
963
964 #[test]
965 fn test_betweenness_centrality_path_graph() {
966 let g = Graph::from_edges(&[(0, 1), (1, 2)], false);
969 let bc = g.betweenness_centrality();
970
971 assert_eq!(bc.len(), 3);
972 assert!(bc[1] > bc[0]);
974 assert!(bc[1] > bc[2]);
975 assert!((bc[0] - bc[2]).abs() < 1e-6);
977 }
978
979 #[test]
980 fn test_betweenness_centrality_star_graph() {
981 let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3)], false);
984 let bc = g.betweenness_centrality();
985
986 assert_eq!(bc.len(), 4);
987 assert!(bc[0] > bc[1]);
989 assert!(bc[0] > bc[2]);
990 assert!(bc[0] > bc[3]);
991 assert!((bc[1] - bc[2]).abs() < 1e-6);
993 assert!((bc[2] - bc[3]).abs() < 1e-6);
994 }
995
996 #[test]
997 fn test_betweenness_centrality_cycle_graph() {
998 let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false);
1001 let bc = g.betweenness_centrality();
1002
1003 assert_eq!(bc.len(), 4);
1004 for i in 0..4 {
1006 for j in i + 1..4 {
1007 assert!((bc[i] - bc[j]).abs() < 1e-6);
1008 }
1009 }
1010 }
1011
1012 #[test]
1013 fn test_betweenness_centrality_complete_graph() {
1014 let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], false);
1017 let bc = g.betweenness_centrality();
1018
1019 assert_eq!(bc.len(), 4);
1020 for i in 0..4 {
1022 for j in i + 1..4 {
1023 assert!((bc[i] - bc[j]).abs() < 1e-6);
1024 }
1025 }
1026 }
1027
1028 #[test]
1029 fn test_betweenness_centrality_bridge_graph() {
1030 let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 4)], false);
1033 let bc = g.betweenness_centrality();
1034
1035 assert_eq!(bc.len(), 5);
1036 assert!(bc[2] > bc[0]);
1038 assert!(bc[2] > bc[1]);
1039 assert!(bc[2] > bc[3]);
1040 assert!(bc[2] > bc[4]);
1041 assert!(bc[1] > bc[0]);
1043 assert!(bc[3] > bc[4]);
1044 }
1045
1046 #[test]
1047 fn test_betweenness_centrality_directed() {
1048 let g = Graph::from_edges(&[(0, 1), (1, 2)], true);
1050 let bc = g.betweenness_centrality();
1051
1052 assert_eq!(bc.len(), 3);
1053 assert!(bc.iter().any(|&x| x > 0.0));
1057 }
1058
1059 #[test]
1060 fn test_betweenness_centrality_disconnected() {
1061 let g = Graph::from_edges(&[(0, 1), (2, 3)], false);
1063 let bc = g.betweenness_centrality();
1064
1065 assert_eq!(bc.len(), 4);
1066 assert!((bc[0] - bc[1]).abs() < 1e-6);
1068 assert!((bc[2] - bc[3]).abs() < 1e-6);
1069 }
1070
1071 #[test]
1074 fn test_modularity_empty_graph() {
1075 let g = Graph::new(false);
1076 let communities = vec![];
1077 let modularity = g.modularity(&communities);
1078 assert_eq!(modularity, 0.0);
1079 }
1080
1081 #[test]
1082 fn test_modularity_single_community() {
1083 let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0)], false);
1085 let communities = vec![vec![0, 1, 2]];
1086 let modularity = g.modularity(&communities);
1087 assert!((modularity - 0.0).abs() < 1e-6);
1089 }
1090
1091 #[test]
1092 fn test_modularity_two_communities() {
1093 let g = Graph::from_edges(
1095 &[
1096 (0, 1),
1097 (1, 2),
1098 (2, 0), (3, 4),
1100 (4, 5),
1101 (5, 3), (2, 3), ],
1104 false,
1105 );
1106
1107 let communities = vec![vec![0, 1, 2], vec![3, 4, 5]];
1108 let modularity = g.modularity(&communities);
1109
1110 assert!(modularity > 0.0);
1112 assert!(modularity < 1.0); }
1114
1115 #[test]
1116 fn test_modularity_perfect_split() {
1117 let g = Graph::from_edges(
1119 &[
1120 (0, 1),
1121 (1, 2),
1122 (2, 0), (3, 4),
1124 (4, 5),
1125 (5, 3), ],
1127 false,
1128 );
1129
1130 let communities = vec![vec![0, 1, 2], vec![3, 4, 5]];
1131 let modularity = g.modularity(&communities);
1132
1133 assert!(modularity > 0.5);
1135 }
1136
1137 #[test]
1138 fn test_louvain_empty_graph() {
1140 let g = Graph::new(false);
1141 let communities = g.louvain();
1142 assert_eq!(communities.len(), 0);
1143 }
1144
1145 #[test]
1146 fn test_louvain_single_node() {
1148 let g = Graph::from_edges(&[(0, 0)], false);
1150 let communities = g.louvain();
1151 assert_eq!(communities.len(), 1);
1152 assert_eq!(communities[0].len(), 1);
1153 }
1154
1155 #[test]
1156 fn test_louvain_two_nodes() {
1158 let g = Graph::from_edges(&[(0, 1)], false);
1159 let communities = g.louvain();
1160
1161 assert_eq!(communities.len(), 1);
1163 assert_eq!(communities[0].len(), 2);
1164 }
1165
1166 #[test]
1167 fn test_louvain_triangle() {
1169 let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0)], false);
1171 let communities = g.louvain();
1172
1173 assert_eq!(communities.len(), 1);
1174 assert_eq!(communities[0].len(), 3);
1175 }
1176
1177 #[test]
1178 fn test_louvain_two_triangles_connected() {
1180 let g = Graph::from_edges(
1182 &[
1183 (0, 1),
1184 (1, 2),
1185 (2, 0), (3, 4),
1187 (4, 5),
1188 (5, 3), (2, 3), ],
1191 false,
1192 );
1193
1194 let communities = g.louvain();
1195
1196 assert_eq!(communities.len(), 2);
1198
1199 let all_nodes: Vec<_> = communities.iter().flat_map(|c| c.iter()).copied().collect();
1201 assert_eq!(all_nodes.len(), 6);
1202 }
1203
1204 #[test]
1205 fn test_louvain_disconnected_components() {
1207 let g = Graph::from_edges(
1209 &[
1210 (0, 1),
1211 (1, 2),
1212 (2, 0), (3, 4),
1214 (4, 5),
1215 (5, 3), ],
1217 false,
1218 );
1219
1220 let communities = g.louvain();
1221
1222 assert!(communities.len() >= 2);
1224
1225 let comm1_nodes: Vec<_> = communities
1227 .iter()
1228 .find(|c| c.contains(&0))
1229 .expect("node 0 should be assigned to a community")
1230 .clone();
1231 let comm2_nodes: Vec<_> = communities
1232 .iter()
1233 .find(|c| c.contains(&3))
1234 .expect("node 3 should be assigned to a community")
1235 .clone();
1236
1237 assert!(comm1_nodes.contains(&0));
1238 assert!(comm1_nodes.contains(&1));
1239 assert!(comm1_nodes.contains(&2));
1240
1241 assert!(comm2_nodes.contains(&3));
1242 assert!(comm2_nodes.contains(&4));
1243 assert!(comm2_nodes.contains(&5));
1244
1245 assert!(!comm1_nodes.contains(&3));
1247 assert!(!comm2_nodes.contains(&0));
1248 }
1249
1250 #[test]
1251 fn test_louvain_karate_club() {
1253 let g = Graph::from_edges(
1256 &[
1257 (0, 1),
1258 (0, 2),
1259 (1, 2), (2, 3), (3, 4),
1262 (3, 5),
1263 (4, 5), ],
1265 false,
1266 );
1267
1268 let communities = g.louvain();
1269
1270 assert!(communities.len() >= 2);
1272
1273 let all_nodes: Vec<_> = communities.iter().flat_map(|c| c.iter()).copied().collect();
1276 assert_eq!(all_nodes.len(), 6);
1277 }
1278
1279 #[test]
1280 fn test_louvain_star_graph() {
1282 let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (0, 4)], false);
1284
1285 let communities = g.louvain();
1286
1287 assert!(!communities.is_empty());
1290 let all_nodes: Vec<_> = communities.iter().flat_map(|c| c.iter()).copied().collect();
1291 assert_eq!(all_nodes.len(), 5);
1292 }
1293
1294 #[test]
1295 fn test_louvain_complete_graph() {
1297 let g = Graph::from_edges(&[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], false);
1299
1300 let communities = g.louvain();
1301
1302 assert_eq!(communities.len(), 1);
1304 assert_eq!(communities[0].len(), 4);
1305 }
1306
1307 #[test]
1308 fn test_louvain_modularity_improves() {
1310 let g = Graph::from_edges(
1312 &[
1313 (0, 1),
1314 (1, 2),
1315 (2, 0), (3, 4),
1317 (4, 5),
1318 (5, 3), ],
1320 false,
1321 );
1322
1323 let communities = g.louvain();
1324 let modularity = g.modularity(&communities);
1325
1326 assert!(modularity > 0.3);
1328 }
1329
1330 #[test]
1331 fn test_louvain_all_nodes_assigned() {
1333 let g = Graph::from_edges(
1335 &[
1336 (0, 1),
1337 (1, 2),
1338 (2, 3),
1339 (3, 4),
1340 (4, 0), ],
1342 false,
1343 );
1344
1345 let communities = g.louvain();
1346
1347 let mut assigned_nodes: Vec<NodeId> = Vec::new();
1348 for community in &communities {
1349 assigned_nodes.extend(community);
1350 }
1351
1352 assigned_nodes.sort_unstable();
1354 assert_eq!(assigned_nodes, vec![0, 1, 2, 3, 4]);
1355
1356 let unique_count = assigned_nodes.len();
1358 assigned_nodes.dedup();
1359 assert_eq!(assigned_nodes.len(), unique_count);
1360 }
1361
1362 #[test]
1363 fn test_modularity_bad_partition() {
1364 let g = Graph::from_edges(&[(0, 1), (1, 2), (2, 0)], false);
1366
1367 let communities = vec![vec![0], vec![1], vec![2]];
1368 let modularity = g.modularity(&communities);
1369
1370 assert!(modularity < 0.1);
1372 }
1373}