1#[cfg(feature = "parallel")]
17use scirs2_core::parallel_ops::*;
18
19use crate::compressed::CsrGraph;
20use crate::error::{GraphError, Result};
21
22#[derive(Debug, Clone)]
28pub struct BfsResult {
29 pub distances: Vec<usize>,
31 pub parents: Vec<usize>,
33 pub num_visited: usize,
35 pub num_levels: usize,
37}
38
39#[cfg(feature = "parallel")]
56pub fn parallel_bfs(graph: &CsrGraph, source: usize) -> Result<BfsResult> {
57 let n = graph.num_nodes();
58 if source >= n {
59 return Err(GraphError::node_not_found_with_context(
60 source,
61 n,
62 "parallel_bfs source",
63 ));
64 }
65
66 let not_visited = usize::MAX;
67 let mut distances = vec![not_visited; n];
68 let mut parents = vec![not_visited; n];
69
70 distances[source] = 0;
71
72 let mut frontier = vec![source];
73 let mut num_visited = 1;
74 let mut max_level = 0;
75
76 while !frontier.is_empty() {
77 let next_level = max_level + 1;
78
79 let next_frontiers: Vec<Vec<(usize, usize)>> = frontier
82 .par_iter()
83 .map(|&node| {
84 let mut local_discovered = Vec::new();
85 for (neighbor, _weight) in graph.neighbors(node) {
86 if distances[neighbor] == not_visited {
88 local_discovered.push((neighbor, node));
89 }
90 }
91 local_discovered
92 })
93 .collect();
94
95 let mut next_frontier = Vec::new();
97 for discovered in next_frontiers {
98 for (neighbor, parent) in discovered {
99 if distances[neighbor] == not_visited {
100 distances[neighbor] = next_level;
101 parents[neighbor] = parent;
102 next_frontier.push(neighbor);
103 num_visited += 1;
104 }
105 }
106 }
107
108 if !next_frontier.is_empty() {
109 max_level = next_level;
110 }
111 frontier = next_frontier;
112 }
113
114 Ok(BfsResult {
115 distances,
116 parents,
117 num_visited,
118 num_levels: max_level,
119 })
120}
121
122pub fn bfs(graph: &CsrGraph, source: usize) -> Result<BfsResult> {
124 let n = graph.num_nodes();
125 if source >= n {
126 return Err(GraphError::node_not_found_with_context(
127 source,
128 n,
129 "bfs source",
130 ));
131 }
132
133 let not_visited = usize::MAX;
134 let mut distances = vec![not_visited; n];
135 let mut parents = vec![not_visited; n];
136
137 distances[source] = 0;
138
139 let mut frontier = vec![source];
140 let mut num_visited = 1;
141 let mut max_level = 0;
142
143 while !frontier.is_empty() {
144 let next_level = max_level + 1;
145 let mut next_frontier = Vec::new();
146 for &node in &frontier {
147 for (neighbor, _weight) in graph.neighbors(node) {
148 if distances[neighbor] == not_visited {
149 distances[neighbor] = next_level;
150 parents[neighbor] = node;
151 next_frontier.push(neighbor);
152 num_visited += 1;
153 }
154 }
155 }
156 if !next_frontier.is_empty() {
157 max_level = next_level;
158 }
159 frontier = next_frontier;
160 }
161
162 Ok(BfsResult {
163 distances,
164 parents,
165 num_visited,
166 num_levels: max_level,
167 })
168}
169
170#[derive(Debug, Clone)]
176pub struct ComponentsResult {
177 pub labels: Vec<usize>,
179 pub num_components: usize,
181 pub component_sizes: Vec<usize>,
183}
184
185struct UnionFind {
187 parent: Vec<std::sync::atomic::AtomicUsize>,
188}
189
190impl UnionFind {
191 fn new(n: usize) -> Self {
192 let parent: Vec<_> = (0..n).map(std::sync::atomic::AtomicUsize::new).collect();
193 Self { parent }
194 }
195
196 fn find(&self, mut x: usize) -> usize {
197 loop {
198 let p = self.parent[x].load(std::sync::atomic::Ordering::Relaxed);
199 if p == x {
200 return x;
201 }
202 let gp = self.parent[p].load(std::sync::atomic::Ordering::Relaxed);
203 let _ = self.parent[x].compare_exchange_weak(
205 p,
206 gp,
207 std::sync::atomic::Ordering::Relaxed,
208 std::sync::atomic::Ordering::Relaxed,
209 );
210 x = gp;
211 }
212 }
213
214 fn union(&self, x: usize, y: usize) {
215 loop {
216 let rx = self.find(x);
217 let ry = self.find(y);
218 if rx == ry {
219 return;
220 }
221 let (small, large) = if rx < ry { (rx, ry) } else { (ry, rx) };
223 match self.parent[large].compare_exchange_weak(
224 large,
225 small,
226 std::sync::atomic::Ordering::Relaxed,
227 std::sync::atomic::Ordering::Relaxed,
228 ) {
229 Ok(_) => return,
230 Err(_) => continue,
231 }
232 }
233 }
234}
235
236#[cfg(feature = "parallel")]
251pub fn parallel_connected_components(graph: &CsrGraph) -> ComponentsResult {
252 let n = graph.num_nodes();
253 if n == 0 {
254 return ComponentsResult {
255 labels: vec![],
256 num_components: 0,
257 component_sizes: vec![],
258 };
259 }
260
261 let uf = UnionFind::new(n);
262
263 (0..n).into_par_iter().for_each(|node| {
265 for (neighbor, _) in graph.neighbors(node) {
266 uf.union(node, neighbor);
267 }
268 });
269
270 let labels: Vec<usize> = (0..n).into_par_iter().map(|i| uf.find(i)).collect();
272
273 relabel_components(&labels)
275}
276
277pub fn connected_components(graph: &CsrGraph) -> ComponentsResult {
279 let n = graph.num_nodes();
280 if n == 0 {
281 return ComponentsResult {
282 labels: vec![],
283 num_components: 0,
284 component_sizes: vec![],
285 };
286 }
287
288 let not_visited = usize::MAX;
289 let mut labels = vec![not_visited; n];
290 let mut component_id = 0;
291
292 for start in 0..n {
293 if labels[start] != not_visited {
294 continue;
295 }
296 let mut queue = vec![start];
298 labels[start] = component_id;
299 let mut head = 0;
300 while head < queue.len() {
301 let node = queue[head];
302 head += 1;
303 for (neighbor, _) in graph.neighbors(node) {
304 if labels[neighbor] == not_visited {
305 labels[neighbor] = component_id;
306 queue.push(neighbor);
307 }
308 }
309 }
310 component_id += 1;
311 }
312
313 let num_components = component_id;
314 let mut component_sizes = vec![0usize; num_components];
315 for &label in &labels {
316 if label < num_components {
317 component_sizes[label] += 1;
318 }
319 }
320
321 ComponentsResult {
322 labels,
323 num_components,
324 component_sizes,
325 }
326}
327
328fn relabel_components(raw_labels: &[usize]) -> ComponentsResult {
330 let n = raw_labels.len();
331 let mut root_to_id = std::collections::HashMap::new();
332 let mut next_id = 0usize;
333 let mut labels = vec![0usize; n];
334
335 for (i, &root) in raw_labels.iter().enumerate() {
336 let id = root_to_id.entry(root).or_insert_with(|| {
337 let id = next_id;
338 next_id += 1;
339 id
340 });
341 labels[i] = *id;
342 }
343
344 let num_components = next_id;
345 let mut component_sizes = vec![0usize; num_components];
346 for &label in &labels {
347 component_sizes[label] += 1;
348 }
349
350 ComponentsResult {
351 labels,
352 num_components,
353 component_sizes,
354 }
355}
356
357#[derive(Debug, Clone)]
363pub struct PageRankConfig {
364 pub damping: f64,
366 pub max_iterations: usize,
368 pub tolerance: f64,
370}
371
372impl Default for PageRankConfig {
373 fn default() -> Self {
374 Self {
375 damping: 0.85,
376 max_iterations: 100,
377 tolerance: 1e-6,
378 }
379 }
380}
381
382#[derive(Debug, Clone)]
384pub struct PageRankResult {
385 pub scores: Vec<f64>,
387 pub iterations: usize,
389 pub residual: f64,
391 pub converged: bool,
393}
394
395#[cfg(feature = "parallel")]
411pub fn parallel_pagerank(graph: &CsrGraph, config: &PageRankConfig) -> Result<PageRankResult> {
412 let n = graph.num_nodes();
413 if n == 0 {
414 return Ok(PageRankResult {
415 scores: vec![],
416 iterations: 0,
417 residual: 0.0,
418 converged: true,
419 });
420 }
421
422 let d = config.damping;
423 let inv_n = 1.0 / n as f64;
424
425 let out_degree: Vec<usize> = (0..n).into_par_iter().map(|i| graph.degree(i)).collect();
427
428 let mut scores = vec![inv_n; n];
430 let mut new_scores = vec![0.0f64; n];
431 let mut iterations = 0;
432 let mut residual = f64::MAX;
433
434 while iterations < config.max_iterations && residual > config.tolerance {
435 iterations += 1;
436
437 let dangling_sum: f64 = (0..n)
439 .into_par_iter()
440 .filter(|&i| out_degree[i] == 0)
441 .map(|i| scores[i])
442 .sum();
443
444 let teleport = (1.0 - d) * inv_n + d * dangling_sum * inv_n;
445
446 new_scores.iter_mut().for_each(|x| *x = 0.0);
453
454 for src in 0..n {
458 if out_degree[src] == 0 {
459 continue;
460 }
461 let contrib = scores[src] / out_degree[src] as f64;
462 for (dst, _) in graph.neighbors(src) {
463 new_scores[dst] += contrib;
464 }
465 }
466
467 let final_scores: Vec<f64> = new_scores
469 .par_iter()
470 .map(|&incoming| teleport + d * incoming)
471 .collect();
472
473 residual = scores
475 .par_iter()
476 .zip(final_scores.par_iter())
477 .map(|(&old, &new)| (old - new).abs())
478 .sum();
479
480 scores = final_scores;
481 }
482
483 Ok(PageRankResult {
484 scores,
485 iterations,
486 residual,
487 converged: residual <= config.tolerance,
488 })
489}
490
491pub fn pagerank(graph: &CsrGraph, config: &PageRankConfig) -> Result<PageRankResult> {
493 let n = graph.num_nodes();
494 if n == 0 {
495 return Ok(PageRankResult {
496 scores: vec![],
497 iterations: 0,
498 residual: 0.0,
499 converged: true,
500 });
501 }
502
503 let d = config.damping;
504 let inv_n = 1.0 / n as f64;
505
506 let out_degree: Vec<usize> = (0..n).map(|i| graph.degree(i)).collect();
507
508 let mut scores = vec![inv_n; n];
509 let mut new_scores = vec![0.0f64; n];
510 let mut iterations = 0;
511 let mut residual = f64::MAX;
512
513 while iterations < config.max_iterations && residual > config.tolerance {
514 iterations += 1;
515
516 let dangling_sum: f64 = (0..n)
517 .filter(|&i| out_degree[i] == 0)
518 .map(|i| scores[i])
519 .sum();
520
521 let teleport = (1.0 - d) * inv_n + d * dangling_sum * inv_n;
522
523 new_scores.iter_mut().for_each(|x| *x = 0.0);
524
525 for src in 0..n {
526 if out_degree[src] == 0 {
527 continue;
528 }
529 let contrib = scores[src] / out_degree[src] as f64;
530 for (dst, _) in graph.neighbors(src) {
531 new_scores[dst] += contrib;
532 }
533 }
534
535 residual = 0.0;
536 for i in 0..n {
537 let new_val = teleport + d * new_scores[i];
538 residual += (scores[i] - new_val).abs();
539 scores[i] = new_val;
540 }
541 }
542
543 Ok(PageRankResult {
544 scores,
545 iterations,
546 residual,
547 converged: residual <= config.tolerance,
548 })
549}
550
551#[derive(Debug, Clone)]
557pub struct TriangleCountResult {
558 pub total_triangles: usize,
560 pub per_node_triangles: Vec<usize>,
562}
563
564#[cfg(feature = "parallel")]
575pub fn parallel_triangle_count(graph: &CsrGraph) -> TriangleCountResult {
576 let n = graph.num_nodes();
577 if n < 3 {
578 return TriangleCountResult {
579 total_triangles: 0,
580 per_node_triangles: vec![0; n],
581 };
582 }
583
584 let per_node: Vec<usize> = (0..n)
588 .into_par_iter()
589 .map(|u| {
590 let mut count = 0;
591 let neighbors_u: Vec<usize> = graph.neighbors(u).map(|(v, _)| v).collect();
592
593 for &v in &neighbors_u {
594 if v <= u {
595 continue;
596 }
597 let neighbors_v: Vec<usize> = graph.neighbors(v).map(|(w, _)| w).collect();
599
600 let mut iu = 0;
602 let mut iv = 0;
603 while iu < neighbors_u.len() && iv < neighbors_v.len() {
604 let nu = neighbors_u[iu];
605 let nv = neighbors_v[iv];
606 if nu <= v {
607 iu += 1;
608 } else if nv <= v {
609 iv += 1;
610 } else if nu == nv {
611 count += 1;
612 iu += 1;
613 iv += 1;
614 } else if nu < nv {
615 iu += 1;
616 } else {
617 iv += 1;
618 }
619 }
620 }
621 count
622 })
623 .collect();
624
625 let total: usize = per_node.iter().sum();
626
627 let mut per_node_triangles = vec![0usize; n];
635
636 for u in 0..n {
638 let neighbors_u: Vec<usize> = graph.neighbors(u).map(|(v, _)| v).collect();
639 for &v in &neighbors_u {
640 if v <= u {
641 continue;
642 }
643 let neighbors_v: Vec<usize> = graph.neighbors(v).map(|(w, _)| w).collect();
644 let mut iu = 0;
645 let mut iv = 0;
646 while iu < neighbors_u.len() && iv < neighbors_v.len() {
647 let nu = neighbors_u[iu];
648 let nv = neighbors_v[iv];
649 if nu <= v {
650 iu += 1;
651 } else if nv <= v {
652 iv += 1;
653 } else if nu == nv {
654 per_node_triangles[u] += 1;
656 per_node_triangles[v] += 1;
657 per_node_triangles[nu] += 1;
658 iu += 1;
659 iv += 1;
660 } else if nu < nv {
661 iu += 1;
662 } else {
663 iv += 1;
664 }
665 }
666 }
667 }
668
669 TriangleCountResult {
670 total_triangles: total,
671 per_node_triangles,
672 }
673}
674
675pub fn triangle_count(graph: &CsrGraph) -> TriangleCountResult {
677 let n = graph.num_nodes();
678 if n < 3 {
679 return TriangleCountResult {
680 total_triangles: 0,
681 per_node_triangles: vec![0; n],
682 };
683 }
684
685 let mut total = 0usize;
686 let mut per_node_triangles = vec![0usize; n];
687
688 for u in 0..n {
689 let neighbors_u: Vec<usize> = graph.neighbors(u).map(|(v, _)| v).collect();
690 for &v in &neighbors_u {
691 if v <= u {
692 continue;
693 }
694 let neighbors_v: Vec<usize> = graph.neighbors(v).map(|(w, _)| w).collect();
695
696 let mut iu = 0;
698 let mut iv = 0;
699 while iu < neighbors_u.len() && iv < neighbors_v.len() {
700 let nu = neighbors_u[iu];
701 let nv = neighbors_v[iv];
702 if nu <= v {
703 iu += 1;
704 } else if nv <= v {
705 iv += 1;
706 } else if nu == nv {
707 total += 1;
708 per_node_triangles[u] += 1;
709 per_node_triangles[v] += 1;
710 per_node_triangles[nu] += 1;
711 iu += 1;
712 iv += 1;
713 } else if nu < nv {
714 iu += 1;
715 } else {
716 iv += 1;
717 }
718 }
719 }
720 }
721
722 TriangleCountResult {
723 total_triangles: total,
724 per_node_triangles,
725 }
726}
727
728#[cfg(test)]
733mod tests {
734 use super::*;
735
736 fn make_path_graph(n: usize) -> CsrGraph {
737 let edges: Vec<(usize, usize, f64)> = (0..n - 1).map(|i| (i, i + 1, 1.0)).collect();
738 CsrGraph::from_edges(n, edges, false).expect("build path")
739 }
740
741 fn make_cycle_graph(n: usize) -> CsrGraph {
742 let mut edges: Vec<(usize, usize, f64)> = (0..n - 1).map(|i| (i, i + 1, 1.0)).collect();
743 edges.push((n - 1, 0, 1.0));
744 CsrGraph::from_edges(n, edges, false).expect("build cycle")
745 }
746
747 fn make_complete_graph(n: usize) -> CsrGraph {
748 let mut edges = Vec::new();
749 for i in 0..n {
750 for j in i + 1..n {
751 edges.push((i, j, 1.0));
752 }
753 }
754 CsrGraph::from_edges(n, edges, false).expect("build complete")
755 }
756
757 fn make_disconnected_graph() -> CsrGraph {
758 let edges = vec![(0, 1, 1.0), (1, 2, 1.0), (0, 2, 1.0), (3, 4, 1.0)];
760 CsrGraph::from_edges(5, edges, false).expect("build disconnected")
761 }
762
763 #[test]
766 fn test_bfs_path() {
767 let g = make_path_graph(5);
768 let result = bfs(&g, 0).expect("bfs");
769
770 assert_eq!(result.distances[0], 0);
771 assert_eq!(result.distances[1], 1);
772 assert_eq!(result.distances[4], 4);
773 assert_eq!(result.num_visited, 5);
774 assert_eq!(result.num_levels, 4);
775 }
776
777 #[test]
778 fn test_bfs_cycle() {
779 let g = make_cycle_graph(6);
780 let result = bfs(&g, 0).expect("bfs");
781
782 assert_eq!(result.distances[0], 0);
783 assert_eq!(result.distances[1], 1);
784 assert_eq!(result.distances[3], 3); assert_eq!(result.num_visited, 6);
786 }
787
788 #[test]
789 fn test_bfs_disconnected() {
790 let g = make_disconnected_graph();
791 let result = bfs(&g, 0).expect("bfs");
792
793 assert_eq!(result.distances[0], 0);
794 assert_eq!(result.distances[1], 1);
795 assert_eq!(result.distances[2], 1);
796 assert_eq!(result.distances[3], usize::MAX); assert_eq!(result.distances[4], usize::MAX);
798 assert_eq!(result.num_visited, 3);
799 }
800
801 #[test]
802 fn test_bfs_invalid_source() {
803 let g = make_path_graph(3);
804 assert!(bfs(&g, 10).is_err());
805 }
806
807 #[test]
808 fn test_bfs_single_node() {
809 let g = CsrGraph::from_edges(1, vec![], false).expect("build");
810 let result = bfs(&g, 0).expect("bfs");
811 assert_eq!(result.distances[0], 0);
812 assert_eq!(result.num_visited, 1);
813 assert_eq!(result.num_levels, 0);
814 }
815
816 #[cfg(feature = "parallel")]
817 #[test]
818 fn test_parallel_bfs_path() {
819 let g = make_path_graph(10);
820 let result = parallel_bfs(&g, 0).expect("parallel bfs");
821
822 assert_eq!(result.distances[0], 0);
823 assert_eq!(result.distances[9], 9);
824 assert_eq!(result.num_visited, 10);
825 }
826
827 #[cfg(feature = "parallel")]
828 #[test]
829 fn test_parallel_bfs_complete() {
830 let g = make_complete_graph(5);
831 let result = parallel_bfs(&g, 0).expect("parallel bfs");
832
833 for i in 1..5 {
835 assert_eq!(result.distances[i], 1);
836 }
837 assert_eq!(result.num_visited, 5);
838 }
839
840 #[test]
843 fn test_cc_connected() {
844 let g = make_path_graph(5);
845 let result = connected_components(&g);
846
847 assert_eq!(result.num_components, 1);
848 let label = result.labels[0];
850 for &l in &result.labels {
851 assert_eq!(l, label);
852 }
853 assert_eq!(result.component_sizes[0], 5);
854 }
855
856 #[test]
857 fn test_cc_disconnected() {
858 let g = make_disconnected_graph();
859 let result = connected_components(&g);
860
861 assert_eq!(result.num_components, 2);
862 assert_eq!(result.labels[0], result.labels[1]);
864 assert_eq!(result.labels[0], result.labels[2]);
865 assert_eq!(result.labels[3], result.labels[4]);
866 assert_ne!(result.labels[0], result.labels[3]);
867 }
868
869 #[test]
870 fn test_cc_isolated_nodes() {
871 let g = CsrGraph::from_edges(5, vec![], false).expect("build");
872 let result = connected_components(&g);
873 assert_eq!(result.num_components, 5);
874 for &size in &result.component_sizes {
875 assert_eq!(size, 1);
876 }
877 }
878
879 #[test]
880 fn test_cc_empty() {
881 let g = CsrGraph::from_edges(0, vec![], false).expect("build");
882 let result = connected_components(&g);
883 assert_eq!(result.num_components, 0);
884 }
885
886 #[cfg(feature = "parallel")]
887 #[test]
888 fn test_parallel_cc_disconnected() {
889 let g = make_disconnected_graph();
890 let result = parallel_connected_components(&g);
891
892 assert_eq!(result.num_components, 2);
893 assert_eq!(result.labels[0], result.labels[1]);
894 assert_eq!(result.labels[0], result.labels[2]);
895 assert_eq!(result.labels[3], result.labels[4]);
896 assert_ne!(result.labels[0], result.labels[3]);
897 }
898
899 #[cfg(feature = "parallel")]
900 #[test]
901 fn test_parallel_cc_connected() {
902 let g = make_complete_graph(10);
903 let result = parallel_connected_components(&g);
904 assert_eq!(result.num_components, 1);
905 assert_eq!(result.component_sizes[0], 10);
906 }
907
908 #[test]
911 fn test_pagerank_simple() {
912 let g = CsrGraph::from_edges(3, vec![(0, 1, 1.0), (1, 2, 1.0), (2, 0, 1.0)], true)
914 .expect("build");
915
916 let config = PageRankConfig {
917 damping: 0.85,
918 max_iterations: 100,
919 tolerance: 1e-8,
920 };
921 let result = pagerank(&g, &config).expect("pagerank");
922
923 let expected = 1.0 / 3.0;
925 for &score in &result.scores {
926 assert!(
927 (score - expected).abs() < 1e-4,
928 "score {score} != expected {expected}"
929 );
930 }
931 assert!(result.converged);
932 }
933
934 #[test]
935 fn test_pagerank_star() {
936 let edges: Vec<(usize, usize, f64)> = (1..5).map(|i| (i, 0, 1.0)).collect();
938 let g = CsrGraph::from_edges(5, edges, true).expect("build");
939
940 let result = pagerank(&g, &PageRankConfig::default()).expect("pagerank");
941 let max_node = result
943 .scores
944 .iter()
945 .enumerate()
946 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
947 .map(|(i, _)| i)
948 .unwrap_or(0);
949 assert_eq!(max_node, 0);
950 }
951
952 #[test]
953 fn test_pagerank_empty() {
954 let g = CsrGraph::from_edges(0, vec![], true).expect("build");
955 let result = pagerank(&g, &PageRankConfig::default()).expect("pagerank");
956 assert!(result.scores.is_empty());
957 assert!(result.converged);
958 }
959
960 #[test]
961 fn test_pagerank_dangling() {
962 let g = CsrGraph::from_edges(3, vec![(0, 1, 1.0), (1, 2, 1.0)], true).expect("build");
964 let result = pagerank(&g, &PageRankConfig::default()).expect("pagerank");
965 let total: f64 = result.scores.iter().sum();
967 assert!(
968 (total - 1.0).abs() < 0.01,
969 "scores should sum to ~1.0, got {total}"
970 );
971 }
972
973 #[cfg(feature = "parallel")]
974 #[test]
975 fn test_parallel_pagerank_cycle() {
976 let g = CsrGraph::from_edges(
977 4,
978 vec![(0, 1, 1.0), (1, 2, 1.0), (2, 3, 1.0), (3, 0, 1.0)],
979 true,
980 )
981 .expect("build");
982
983 let result = parallel_pagerank(&g, &PageRankConfig::default()).expect("pagerank");
984
985 let expected = 0.25;
986 for &score in &result.scores {
987 assert!((score - expected).abs() < 1e-4);
988 }
989 assert!(result.converged);
990 }
991
992 #[test]
995 fn test_triangle_count_k3() {
996 let g = make_complete_graph(3);
997 let result = triangle_count(&g);
998 assert_eq!(result.total_triangles, 1);
999 for &count in &result.per_node_triangles {
1000 assert_eq!(count, 1);
1001 }
1002 }
1003
1004 #[test]
1005 fn test_triangle_count_k4() {
1006 let g = make_complete_graph(4);
1007 let result = triangle_count(&g);
1008 assert_eq!(result.total_triangles, 4); for &count in &result.per_node_triangles {
1010 assert_eq!(count, 3); }
1012 }
1013
1014 #[test]
1015 fn test_triangle_count_k5() {
1016 let g = make_complete_graph(5);
1017 let result = triangle_count(&g);
1018 assert_eq!(result.total_triangles, 10); }
1020
1021 #[test]
1022 fn test_triangle_count_path() {
1023 let g = make_path_graph(5);
1024 let result = triangle_count(&g);
1025 assert_eq!(result.total_triangles, 0); }
1027
1028 #[test]
1029 fn test_triangle_count_single_triangle() {
1030 let g = CsrGraph::from_edges(
1031 4,
1032 vec![(0, 1, 1.0), (1, 2, 1.0), (0, 2, 1.0), (2, 3, 1.0)],
1033 false,
1034 )
1035 .expect("build");
1036 let result = triangle_count(&g);
1037 assert_eq!(result.total_triangles, 1);
1038 assert_eq!(result.per_node_triangles[0], 1);
1039 assert_eq!(result.per_node_triangles[1], 1);
1040 assert_eq!(result.per_node_triangles[2], 1);
1041 assert_eq!(result.per_node_triangles[3], 0); }
1043
1044 #[test]
1045 fn test_triangle_count_empty() {
1046 let g = CsrGraph::from_edges(2, vec![], false).expect("build");
1047 let result = triangle_count(&g);
1048 assert_eq!(result.total_triangles, 0);
1049 }
1050
1051 #[cfg(feature = "parallel")]
1052 #[test]
1053 fn test_parallel_triangle_count_k4() {
1054 let g = make_complete_graph(4);
1055 let result = parallel_triangle_count(&g);
1056 assert_eq!(result.total_triangles, 4);
1057 }
1058
1059 #[cfg(feature = "parallel")]
1060 #[test]
1061 fn test_parallel_triangle_count_k5() {
1062 let g = make_complete_graph(5);
1063 let result = parallel_triangle_count(&g);
1064 assert_eq!(result.total_triangles, 10);
1065 }
1066
1067 #[test]
1070 fn test_bfs_parent_tree() {
1071 let g = make_path_graph(5);
1072 let result = bfs(&g, 0).expect("bfs");
1073
1074 assert_eq!(result.parents[0], usize::MAX); for i in 1..5 {
1077 let parent = result.parents[i];
1078 assert!(parent < 5);
1079 assert_eq!(result.distances[i], result.distances[parent] + 1);
1080 }
1081 }
1082
1083 #[test]
1084 fn test_cc_label_consistency() {
1085 let g = make_disconnected_graph();
1086 let result = connected_components(&g);
1087
1088 for node in 0..g.num_nodes() {
1090 for (neighbor, _) in g.neighbors(node) {
1091 assert_eq!(
1092 result.labels[node], result.labels[neighbor],
1093 "nodes {node} and {neighbor} should be in same component"
1094 );
1095 }
1096 }
1097 }
1098
1099 #[test]
1100 fn test_pagerank_scores_sum_to_one() {
1101 let g = make_complete_graph(5);
1102 let config = PageRankConfig {
1104 max_iterations: 200,
1105 tolerance: 1e-10,
1106 ..Default::default()
1107 };
1108 let result = pagerank(&g, &config).expect("pagerank");
1109 let total: f64 = result.scores.iter().sum();
1110 assert!(
1111 (total - 1.0).abs() < 0.01,
1112 "PageRank scores should sum to ~1.0, got {total}"
1113 );
1114 }
1115}