1use crate::error::{RandError, RandResult};
19use std::collections::VecDeque;
20
21struct SplitMix64 {
27 state: u64,
28}
29
30impl SplitMix64 {
31 fn new(seed: u64) -> Self {
33 Self { state: seed }
34 }
35
36 fn next_u64(&mut self) -> u64 {
38 self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15);
39 let mut z = self.state;
40 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
41 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
42 z ^ (z >> 31)
43 }
44
45 fn next_f64(&mut self) -> f64 {
47 (self.next_u64() >> 11) as f64 / ((1u64 << 53) as f64)
48 }
49
50 fn next_usize(&mut self, bound: usize) -> usize {
52 if bound == 0 {
53 return 0;
54 }
55 (self.next_u64() % (bound as u64)) as usize
56 }
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
65pub enum GraphType {
66 Undirected,
68 Directed,
70}
71
72impl std::fmt::Display for GraphType {
73 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 match self {
75 Self::Undirected => write!(f, "Undirected"),
76 Self::Directed => write!(f, "Directed"),
77 }
78 }
79}
80
81#[derive(Debug, Clone)]
87pub struct GraphStats {
88 pub num_vertices: usize,
90 pub num_edges: usize,
94 pub avg_degree: f64,
96 pub max_degree: usize,
98 pub min_degree: usize,
100 pub is_connected: bool,
103 pub density: f64,
105}
106
107impl std::fmt::Display for GraphStats {
108 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109 write!(
110 f,
111 "GraphStats(V={}, E={}, avg_deg={:.2}, max_deg={}, min_deg={}, \
112 connected={}, density={:.4})",
113 self.num_vertices,
114 self.num_edges,
115 self.avg_degree,
116 self.max_degree,
117 self.min_degree,
118 self.is_connected,
119 self.density,
120 )
121 }
122}
123
124#[derive(Debug, Clone)]
133pub struct AdjacencyList {
134 pub num_vertices: usize,
136 pub row_offsets: Vec<usize>,
138 pub col_indices: Vec<usize>,
140 pub graph_type: GraphType,
142}
143
144impl AdjacencyList {
145 pub fn degree(&self, v: usize) -> RandResult<usize> {
151 if v >= self.num_vertices {
152 return Err(RandError::InvalidSize(format!(
153 "vertex {v} out of range [0, {})",
154 self.num_vertices
155 )));
156 }
157 Ok(self.row_offsets[v + 1] - self.row_offsets[v])
158 }
159
160 pub fn neighbors(&self, v: usize) -> RandResult<&[usize]> {
166 if v >= self.num_vertices {
167 return Err(RandError::InvalidSize(format!(
168 "vertex {v} out of range [0, {})",
169 self.num_vertices
170 )));
171 }
172 Ok(&self.col_indices[self.row_offsets[v]..self.row_offsets[v + 1]])
173 }
174
175 pub fn stats(&self) -> GraphStats {
177 if self.num_vertices == 0 {
178 return GraphStats {
179 num_vertices: 0,
180 num_edges: 0,
181 avg_degree: 0.0,
182 max_degree: 0,
183 min_degree: 0,
184 is_connected: true,
185 density: 0.0,
186 };
187 }
188
189 let mut max_degree: usize = 0;
190 let mut min_degree: usize = usize::MAX;
191 let mut total_degree: usize = 0;
192
193 for v in 0..self.num_vertices {
194 let d = self.row_offsets[v + 1] - self.row_offsets[v];
195 total_degree += d;
196 if d > max_degree {
197 max_degree = d;
198 }
199 if d < min_degree {
200 min_degree = d;
201 }
202 }
203
204 let num_edges = match self.graph_type {
205 GraphType::Undirected => total_degree / 2,
206 GraphType::Directed => total_degree,
207 };
208
209 let avg_degree = total_degree as f64 / self.num_vertices as f64;
210
211 let max_possible = match self.graph_type {
212 GraphType::Undirected => {
213 self.num_vertices
214 .saturating_mul(self.num_vertices.saturating_sub(1))
215 / 2
216 }
217 GraphType::Directed => self
218 .num_vertices
219 .saturating_mul(self.num_vertices.saturating_sub(1)),
220 };
221 let density = if max_possible > 0 {
222 num_edges as f64 / max_possible as f64
223 } else {
224 0.0
225 };
226
227 let is_connected = is_approximately_connected(self);
228
229 GraphStats {
230 num_vertices: self.num_vertices,
231 num_edges,
232 avg_degree,
233 max_degree,
234 min_degree,
235 is_connected,
236 density,
237 }
238 }
239
240 pub fn to_csr_values<T: Default + Copy>(
245 &self,
246 default_val: T,
247 ) -> (Vec<usize>, Vec<usize>, Vec<T>) {
248 let values = vec![default_val; self.col_indices.len()];
249 (self.row_offsets.clone(), self.col_indices.clone(), values)
250 }
251}
252
253impl std::fmt::Display for AdjacencyList {
254 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
255 let num_edges = match self.graph_type {
256 GraphType::Undirected => self.col_indices.len() / 2,
257 GraphType::Directed => self.col_indices.len(),
258 };
259 write!(
260 f,
261 "AdjacencyList(V={}, E={}, {})",
262 self.num_vertices, num_edges, self.graph_type
263 )
264 }
265}
266
267pub fn is_approximately_connected(adj: &AdjacencyList) -> bool {
274 if adj.num_vertices <= 1 {
275 return true;
276 }
277
278 let mut visited = vec![false; adj.num_vertices];
279 let mut queue = VecDeque::new();
280 visited[0] = true;
281 queue.push_back(0usize);
282 let mut count: usize = 1;
283
284 while let Some(v) = queue.pop_front() {
285 let start = adj.row_offsets[v];
286 let end = adj.row_offsets[v + 1];
287 for &nbr in &adj.col_indices[start..end] {
288 if !visited[nbr] {
289 visited[nbr] = true;
290 count += 1;
291 queue.push_back(nbr);
292 }
293 }
294 }
295
296 let threshold = (adj.num_vertices as f64 * 0.9).ceil() as usize;
297 count >= threshold
298}
299
300fn adjacency_from_edges(
308 num_vertices: usize,
309 edges: &mut [(usize, usize)],
310 graph_type: GraphType,
311) -> AdjacencyList {
312 edges.sort_unstable_by_key(|&(src, _)| src);
314
315 let mut row_offsets = vec![0usize; num_vertices + 1];
316 for &(src, _) in edges.iter() {
317 row_offsets[src + 1] += 1;
318 }
319 for i in 1..=num_vertices {
320 row_offsets[i] += row_offsets[i - 1];
321 }
322
323 let col_indices: Vec<usize> = edges.iter().map(|&(_, dst)| dst).collect();
324
325 AdjacencyList {
326 num_vertices,
327 row_offsets,
328 col_indices,
329 graph_type,
330 }
331}
332
333pub struct ErdosRenyiGenerator {
342 num_vertices: usize,
343 edge_probability: f64,
344 seed: u64,
345}
346
347impl ErdosRenyiGenerator {
348 pub fn new(num_vertices: usize, edge_probability: f64, seed: u64) -> Self {
356 Self {
357 num_vertices,
358 edge_probability,
359 seed,
360 }
361 }
362
363 pub fn generate(&self) -> RandResult<AdjacencyList> {
369 self.validate_params()?;
370 let mut rng = SplitMix64::new(self.seed);
371 let n = self.num_vertices;
372 let mut edges: Vec<(usize, usize)> = Vec::new();
373
374 for i in 0..n {
375 for j in (i + 1)..n {
376 if rng.next_f64() < self.edge_probability {
377 edges.push((i, j));
378 edges.push((j, i));
379 }
380 }
381 }
382
383 Ok(adjacency_from_edges(n, &mut edges, GraphType::Undirected))
384 }
385
386 pub fn generate_directed(&self) -> RandResult<AdjacencyList> {
392 self.validate_params()?;
393 let mut rng = SplitMix64::new(self.seed);
394 let n = self.num_vertices;
395 let mut edges: Vec<(usize, usize)> = Vec::new();
396
397 for i in 0..n {
398 for j in 0..n {
399 if i != j && rng.next_f64() < self.edge_probability {
400 edges.push((i, j));
401 }
402 }
403 }
404
405 Ok(adjacency_from_edges(n, &mut edges, GraphType::Directed))
406 }
407
408 fn validate_params(&self) -> RandResult<()> {
410 if !(0.0..=1.0).contains(&self.edge_probability) {
411 return Err(RandError::InvalidSize(format!(
412 "edge_probability must be in [0, 1], got {}",
413 self.edge_probability
414 )));
415 }
416 Ok(())
417 }
418}
419
420pub struct StochasticBlockModelGenerator {
431 block_sizes: Vec<usize>,
432 intra_prob: f64,
433 inter_prob: f64,
434 seed: u64,
435}
436
437impl StochasticBlockModelGenerator {
438 pub fn new(block_sizes: Vec<usize>, intra_prob: f64, inter_prob: f64, seed: u64) -> Self {
447 Self {
448 block_sizes,
449 intra_prob,
450 inter_prob,
451 seed,
452 }
453 }
454
455 pub fn generate(&self) -> RandResult<AdjacencyList> {
462 if self.block_sizes.is_empty() {
463 return Err(RandError::InvalidSize(
464 "block_sizes must not be empty".to_string(),
465 ));
466 }
467 if !(0.0..=1.0).contains(&self.intra_prob) {
468 return Err(RandError::InvalidSize(format!(
469 "intra_prob must be in [0, 1], got {}",
470 self.intra_prob
471 )));
472 }
473 if !(0.0..=1.0).contains(&self.inter_prob) {
474 return Err(RandError::InvalidSize(format!(
475 "inter_prob must be in [0, 1], got {}",
476 self.inter_prob
477 )));
478 }
479
480 let n: usize = self.block_sizes.iter().sum();
481 let mut rng = SplitMix64::new(self.seed);
482
483 let mut block_of = Vec::with_capacity(n);
485 for (block_idx, &size) in self.block_sizes.iter().enumerate() {
486 for _ in 0..size {
487 block_of.push(block_idx);
488 }
489 }
490
491 let mut edges: Vec<(usize, usize)> = Vec::new();
492
493 for i in 0..n {
494 for j in (i + 1)..n {
495 let prob = if block_of[i] == block_of[j] {
496 self.intra_prob
497 } else {
498 self.inter_prob
499 };
500 if rng.next_f64() < prob {
501 edges.push((i, j));
502 edges.push((j, i));
503 }
504 }
505 }
506
507 Ok(adjacency_from_edges(n, &mut edges, GraphType::Undirected))
508 }
509}
510
511pub struct BarabasiAlbertGenerator {
522 num_vertices: usize,
523 attachment_edges: usize,
524 seed: u64,
525}
526
527impl BarabasiAlbertGenerator {
528 pub fn new(num_vertices: usize, attachment_edges: usize, seed: u64) -> Self {
536 Self {
537 num_vertices,
538 attachment_edges,
539 seed,
540 }
541 }
542
543 pub fn generate(&self) -> RandResult<AdjacencyList> {
550 let m = self.attachment_edges;
551 let n = self.num_vertices;
552
553 if m == 0 {
554 return Err(RandError::InvalidSize(
555 "attachment_edges must be >= 1".to_string(),
556 ));
557 }
558 let m0 = m + 1; if n <= m {
560 return Err(RandError::InvalidSize(format!(
561 "num_vertices ({n}) must be > attachment_edges ({m})"
562 )));
563 }
564
565 let mut rng = SplitMix64::new(self.seed);
566
567 let mut degree = vec![0usize; n];
569 let mut edges: Vec<(usize, usize)> = Vec::new();
570
571 for i in 0..m0 {
573 for j in (i + 1)..m0 {
574 edges.push((i, j));
575 edges.push((j, i));
576 degree[i] += 1;
577 degree[j] += 1;
578 }
579 }
580
581 let mut stubs: Vec<usize> = Vec::new();
585 for (v, &d) in degree.iter().enumerate().take(m0) {
586 for _ in 0..d {
587 stubs.push(v);
588 }
589 }
590
591 for new_v in m0..n {
592 let mut targets: Vec<usize> = Vec::with_capacity(m);
594 let mut target_set = vec![false; n]; let mut attempts = 0usize;
597 while targets.len() < m && attempts < m * 100 {
598 attempts += 1;
599 if stubs.is_empty() {
600 break;
601 }
602 let idx = rng.next_usize(stubs.len());
603 let target = stubs[idx];
604 if !target_set[target] && target != new_v {
605 target_set[target] = true;
606 targets.push(target);
607 }
608 }
609
610 for &t in &targets {
612 edges.push((new_v, t));
613 edges.push((t, new_v));
614 degree[new_v] += 1;
615 degree[t] += 1;
616 stubs.push(new_v);
617 stubs.push(t);
618 }
619 }
620
621 Ok(adjacency_from_edges(n, &mut edges, GraphType::Undirected))
622 }
623}
624
625pub struct WattsStrogatzGenerator {
635 num_vertices: usize,
636 k_neighbors: usize,
637 rewire_prob: f64,
638 seed: u64,
639}
640
641impl WattsStrogatzGenerator {
642 pub fn new(num_vertices: usize, k_neighbors: usize, rewire_prob: f64, seed: u64) -> Self {
652 Self {
653 num_vertices,
654 k_neighbors,
655 rewire_prob,
656 seed,
657 }
658 }
659
660 pub fn generate(&self) -> RandResult<AdjacencyList> {
666 let n = self.num_vertices;
667 let k = self.k_neighbors;
668
669 if n == 0 {
670 return Ok(AdjacencyList {
671 num_vertices: 0,
672 row_offsets: vec![0],
673 col_indices: Vec::new(),
674 graph_type: GraphType::Undirected,
675 });
676 }
677 if k == 0 {
678 return Ok(AdjacencyList {
680 num_vertices: n,
681 row_offsets: vec![0; n + 1],
682 col_indices: Vec::new(),
683 graph_type: GraphType::Undirected,
684 });
685 }
686 if k % 2 != 0 {
687 return Err(RandError::InvalidSize(format!(
688 "k_neighbors must be even, got {k}"
689 )));
690 }
691 if k >= n {
692 return Err(RandError::InvalidSize(format!(
693 "k_neighbors ({k}) must be < num_vertices ({n})"
694 )));
695 }
696 if !(0.0..=1.0).contains(&self.rewire_prob) {
697 return Err(RandError::InvalidSize(format!(
698 "rewire_prob must be in [0, 1], got {}",
699 self.rewire_prob
700 )));
701 }
702
703 let mut rng = SplitMix64::new(self.seed);
704 let half_k = k / 2;
705
706 let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
709
710 for i in 0..n {
712 for offset in 1..=half_k {
713 let j = (i + offset) % n;
714 adj[i].push(j);
715 adj[j].push(i);
716 }
717 }
718
719 for i in 0..n {
723 for offset in 1..=half_k {
724 if rng.next_f64() >= self.rewire_prob {
725 continue;
726 }
727
728 let j = (i + offset) % n;
729
730 if let Some(pos) = adj[i].iter().position(|&x| x == j) {
732 adj[i].swap_remove(pos);
733 }
734 if let Some(pos) = adj[j].iter().position(|&x| x == i) {
735 adj[j].swap_remove(pos);
736 }
737
738 let mut attempts = 0usize;
740 let mut new_target = j;
741 let found = loop {
742 attempts += 1;
743 if attempts > n * 10 {
744 break false;
745 }
746 let candidate = rng.next_usize(n);
747 if candidate != i && !adj[i].contains(&candidate) {
748 new_target = candidate;
749 break true;
750 }
751 };
752
753 if found {
754 adj[i].push(new_target);
755 adj[new_target].push(i);
756 } else {
757 adj[i].push(j);
759 adj[j].push(i);
760 }
761 }
762 }
763
764 Ok(adjacency_vecs_to_csr(n, &adj, GraphType::Undirected))
766 }
767}
768
769pub struct RandomRegularGenerator {
779 num_vertices: usize,
780 degree: usize,
781 seed: u64,
782}
783
784impl RandomRegularGenerator {
785 pub fn new(num_vertices: usize, degree: usize, seed: u64) -> Self {
793 Self {
794 num_vertices,
795 degree,
796 seed,
797 }
798 }
799
800 pub fn generate(&self) -> RandResult<AdjacencyList> {
807 let n = self.num_vertices;
808 let k = self.degree;
809
810 if n == 0 {
811 return Ok(AdjacencyList {
812 num_vertices: 0,
813 row_offsets: vec![0],
814 col_indices: Vec::new(),
815 graph_type: GraphType::Undirected,
816 });
817 }
818 if k == 0 {
819 return Ok(AdjacencyList {
820 num_vertices: n,
821 row_offsets: vec![0; n + 1],
822 col_indices: Vec::new(),
823 graph_type: GraphType::Undirected,
824 });
825 }
826 if (n * k) % 2 != 0 {
827 return Err(RandError::InvalidSize(format!(
828 "num_vertices * degree ({} * {}) must be even",
829 n, k
830 )));
831 }
832 if k >= n {
833 return Err(RandError::InvalidSize(format!(
834 "degree ({k}) must be < num_vertices ({n})"
835 )));
836 }
837
838 let mut rng = SplitMix64::new(self.seed);
840 let max_retries = 100;
841
842 for _ in 0..max_retries {
843 match self.try_pairing(&mut rng, n, k) {
844 Some(adj) => return Ok(adj),
845 None => continue,
846 }
847 }
848
849 Err(RandError::InternalError(format!(
852 "failed to generate {k}-regular graph on {n} vertices after {max_retries} attempts"
853 )))
854 }
855
856 fn try_pairing(&self, rng: &mut SplitMix64, n: usize, k: usize) -> Option<AdjacencyList> {
860 let total_stubs = n * k;
862 let mut stubs: Vec<usize> = Vec::with_capacity(total_stubs);
863 for v in 0..n {
864 for _ in 0..k {
865 stubs.push(v);
866 }
867 }
868
869 for i in (1..total_stubs).rev() {
871 let j = rng.next_usize(i + 1);
872 stubs.swap(i, j);
873 }
874
875 let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
877 let mut valid = true;
878 let num_pairs = total_stubs / 2;
879
880 for p in 0..num_pairs {
881 let u = stubs[2 * p];
882 let v = stubs[2 * p + 1];
883 if u == v {
884 valid = false;
886 break;
887 }
888 if adj[u].contains(&v) {
889 valid = false;
891 break;
892 }
893 adj[u].push(v);
894 adj[v].push(u);
895 }
896
897 if !valid {
898 return None;
899 }
900
901 Some(adjacency_vecs_to_csr(n, &adj, GraphType::Undirected))
902 }
903}
904
905fn adjacency_vecs_to_csr(
911 num_vertices: usize,
912 adj: &[Vec<usize>],
913 graph_type: GraphType,
914) -> AdjacencyList {
915 let mut row_offsets = Vec::with_capacity(num_vertices + 1);
916 let mut col_indices = Vec::new();
917
918 let mut offset = 0usize;
919 for neighbours in adj.iter() {
920 row_offsets.push(offset);
921 let mut sorted = neighbours.clone();
922 sorted.sort_unstable();
923 col_indices.extend_from_slice(&sorted);
924 offset += sorted.len();
925 }
926 row_offsets.push(offset);
927
928 AdjacencyList {
929 num_vertices,
930 row_offsets,
931 col_indices,
932 graph_type,
933 }
934}
935
936#[cfg(test)]
941mod tests {
942 use super::*;
943
944 #[test]
949 fn erdos_renyi_basic_edge_count() {
950 let n = 200;
951 let p = 0.1;
952 let er = ErdosRenyiGenerator::new(n, p, 42);
953 let g = er.generate().expect("ER generate failed");
954
955 let stats = g.stats();
956 let expected = (n * (n - 1)) as f64 / 2.0 * p;
957 let tolerance = expected * 0.3 + 50.0;
959 assert!(
960 (stats.num_edges as f64 - expected).abs() < tolerance,
961 "expected ~{expected:.0} edges, got {}",
962 stats.num_edges
963 );
964 }
965
966 #[test]
967 fn erdos_renyi_directed() {
968 let n = 50;
969 let p = 0.2;
970 let er = ErdosRenyiGenerator::new(n, p, 99);
971 let g = er.generate_directed().expect("ER directed failed");
972
973 assert_eq!(g.graph_type, GraphType::Directed);
974 let stats = g.stats();
975 let expected = (n * (n - 1)) as f64 * p;
976 let tolerance = expected * 0.35 + 30.0;
977 assert!(
978 (stats.num_edges as f64 - expected).abs() < tolerance,
979 "expected ~{expected:.0} directed edges, got {}",
980 stats.num_edges
981 );
982 }
983
984 #[test]
985 fn erdos_renyi_p_zero_no_edges() {
986 let gr = ErdosRenyiGenerator::new(50, 0.0, 1);
987 let g = gr.generate().expect("ER p=0 failed");
988 assert_eq!(g.col_indices.len(), 0);
989 assert_eq!(g.stats().num_edges, 0);
990 }
991
992 #[test]
993 fn erdos_renyi_p_one_complete() {
994 let n = 20;
995 let gr = ErdosRenyiGenerator::new(n, 1.0, 7);
996 let g = gr.generate().expect("ER p=1 failed");
997 let expected_edges = n * (n - 1) / 2;
998 assert_eq!(g.stats().num_edges, expected_edges);
999 }
1000
1001 #[test]
1006 fn sbm_community_structure() {
1007 let gr = StochasticBlockModelGenerator::new(vec![50, 50], 0.5, 0.05, 42);
1009 let g = gr.generate().expect("SBM generate failed");
1010
1011 let mut intra_edges = 0usize;
1013 let mut inter_edges = 0usize;
1014 for v in 0..100 {
1015 let block_v = if v < 50 { 0 } else { 1 };
1016 let nbrs = g.neighbors(v).expect("neighbors failed");
1017 for &u in nbrs {
1018 let block_u = if u < 50 { 0 } else { 1 };
1019 if block_v == block_u {
1020 intra_edges += 1;
1021 } else {
1022 inter_edges += 1;
1023 }
1024 }
1025 }
1026 assert!(
1028 intra_edges > inter_edges,
1029 "intra_edges ({intra_edges}) should be > inter_edges ({inter_edges})"
1030 );
1031 }
1032
1033 #[test]
1034 fn sbm_equal_probs_like_er() {
1035 let p = 0.15;
1037 let gr = StochasticBlockModelGenerator::new(vec![30, 30], p, p, 123);
1038 let g = gr.generate().expect("SBM equal probs failed");
1039 let stats = g.stats();
1040 let n = 60usize;
1041 let expected = (n * (n - 1)) as f64 / 2.0 * p;
1042 let tolerance = expected * 0.4 + 30.0;
1043 assert!(
1044 (stats.num_edges as f64 - expected).abs() < tolerance,
1045 "expected ~{expected:.0} edges, got {}",
1046 stats.num_edges
1047 );
1048 }
1049
1050 #[test]
1055 fn barabasi_albert_degree_distribution() {
1056 let n = 500;
1057 let m = 3;
1058 let gr = BarabasiAlbertGenerator::new(n, m, 42);
1059 let g = gr.generate().expect("BA generate failed");
1060
1061 let stats = g.stats();
1063 assert!(
1064 stats.max_degree > stats.avg_degree as usize * 2,
1065 "max_degree ({}) should be well above avg_degree ({:.1})",
1066 stats.max_degree,
1067 stats.avg_degree
1068 );
1069 }
1070
1071 #[test]
1072 fn barabasi_albert_minimum_degree() {
1073 let n = 100;
1074 let m = 2;
1075 let gr = BarabasiAlbertGenerator::new(n, m, 77);
1076 let g = gr.generate().expect("BA generate failed");
1077
1078 let stats = g.stats();
1080 assert!(
1081 stats.min_degree >= m,
1082 "min_degree ({}) should be >= m ({})",
1083 stats.min_degree,
1084 m
1085 );
1086 }
1087
1088 #[test]
1093 fn watts_strogatz_ring_p_zero() {
1094 let n = 20;
1095 let k = 4;
1096 let gr = WattsStrogatzGenerator::new(n, k, 0.0, 42);
1097 let g = gr.generate().expect("WS p=0 failed");
1098
1099 for v in 0..n {
1101 let d = g.degree(v).expect("degree failed");
1102 assert_eq!(d, k, "vertex {v} degree = {d}, expected {k}");
1103 }
1104 }
1105
1106 #[test]
1107 fn watts_strogatz_fully_random() {
1108 let n = 50;
1109 let k = 6;
1110 let gr = WattsStrogatzGenerator::new(n, k, 1.0, 99);
1111 let g = gr.generate().expect("WS p=1 failed");
1112
1113 let stats = g.stats();
1115 let expected = n * k / 2;
1116 let tolerance = (expected as f64 * 0.15) as usize + 5;
1117 assert!(
1118 (stats.num_edges as isize - expected as isize).unsigned_abs() < tolerance,
1119 "expected ~{expected} edges, got {}",
1120 stats.num_edges
1121 );
1122 }
1123
1124 #[test]
1129 fn random_regular_degree_check() {
1130 let n = 50;
1131 let k = 4;
1132 let gr = RandomRegularGenerator::new(n, k, 42);
1133 let g = gr.generate().expect("regular graph failed");
1134
1135 for v in 0..n {
1136 let d = g.degree(v).expect("degree failed");
1137 assert_eq!(d, k, "vertex {v} has degree {d}, expected {k}");
1138 }
1139 }
1140
1141 #[test]
1146 fn graph_stats_computation() {
1147 let gr = ErdosRenyiGenerator::new(100, 0.15, 55);
1148 let g = gr.generate().expect("ER failed");
1149 let stats = g.stats();
1150 assert_eq!(stats.num_vertices, 100);
1151 assert!(stats.avg_degree > 0.0);
1152 assert!(stats.max_degree >= stats.min_degree);
1153 assert!(stats.density >= 0.0 && stats.density <= 1.0);
1154 }
1155
1156 #[test]
1157 fn adjacency_list_degree_query() {
1158 let gr = ErdosRenyiGenerator::new(10, 1.0, 42);
1159 let g = gr.generate().expect("ER failed");
1160 for v in 0..10 {
1162 assert_eq!(
1163 g.degree(v).expect("degree failed"),
1164 9,
1165 "vertex {v} should have degree 9 in K_10"
1166 );
1167 }
1168 assert!(g.degree(10).is_err());
1170 }
1171
1172 #[test]
1173 fn adjacency_list_neighbors_query() {
1174 let gr = ErdosRenyiGenerator::new(5, 1.0, 42);
1175 let g = gr.generate().expect("ER failed");
1176
1177 let nbrs = g.neighbors(0).expect("neighbors failed");
1179 assert_eq!(nbrs.len(), 4);
1180 for &expected in &[1usize, 2, 3, 4] {
1181 assert!(nbrs.contains(&expected), "missing neighbour {expected}");
1182 }
1183 assert!(g.neighbors(5).is_err());
1185 }
1186
1187 #[test]
1188 fn adjacency_list_to_csr_values() {
1189 let gr = ErdosRenyiGenerator::new(5, 1.0, 42);
1190 let g = gr.generate().expect("ER failed");
1191
1192 let (row_off, col_idx, vals) = g.to_csr_values(1.0_f64);
1193 assert_eq!(row_off.len(), 6);
1194 assert_eq!(col_idx.len(), vals.len());
1195 for &v in &vals {
1197 assert!((v - 1.0).abs() < 1e-15);
1198 }
1199 }
1200
1201 #[test]
1202 fn connectivity_check() {
1203 let gr = ErdosRenyiGenerator::new(100, 0.1, 42);
1205 let g = gr.generate().expect("ER failed");
1206 assert!(
1207 is_approximately_connected(&g),
1208 "dense ER graph should be approximately connected"
1209 );
1210
1211 let gen2 = ErdosRenyiGenerator::new(50, 0.0, 1);
1213 let g2 = gen2.generate().expect("ER p=0 failed");
1214 assert!(
1215 !is_approximately_connected(&g2),
1216 "graph with no edges should not be connected"
1217 );
1218 }
1219
1220 #[test]
1221 fn empty_graph() {
1222 let gr = ErdosRenyiGenerator::new(0, 0.5, 42);
1223 let g = gr.generate().expect("empty graph failed");
1224 assert_eq!(g.num_vertices, 0);
1225 assert_eq!(g.col_indices.len(), 0);
1226 let stats = g.stats();
1227 assert_eq!(stats.num_edges, 0);
1228 assert!(stats.is_connected);
1229 }
1230
1231 #[test]
1232 fn single_vertex_graph() {
1233 let gr = ErdosRenyiGenerator::new(1, 0.5, 42);
1234 let g = gr.generate().expect("single vertex failed");
1235 assert_eq!(g.num_vertices, 1);
1236 assert_eq!(g.col_indices.len(), 0);
1237 assert_eq!(g.degree(0).expect("degree failed"), 0);
1238 assert!(is_approximately_connected(&g));
1239 }
1240}