Skip to main content

oxicuda_rand/
graph_gen.rs

1//! Random graph generation for GNN workloads and network analysis.
2//!
3//! Provides generators for several families of random graphs stored as sparse
4//! adjacency structures (CSR-like: `row_offsets`, `col_indices`):
5//!
6//! - **Erdős–Rényi** G(n, p): each possible edge included independently with
7//!   probability p.
8//! - **Stochastic Block Model**: community-structured graphs with tunable
9//!   intra-/inter-community edge probabilities.
10//! - **Barabási–Albert**: scale-free (power-law degree distribution) graphs via
11//!   preferential attachment.
12//! - **Watts–Strogatz**: small-world graphs interpolating between a ring lattice
13//!   and a random graph.
14//! - **Random Regular**: k-regular graphs where every vertex has the same degree.
15//!
16//! (C) 2026 COOLJAPAN OU (Team KitaSan)
17
18use crate::error::{RandError, RandResult};
19use std::collections::VecDeque;
20
21// ---------------------------------------------------------------------------
22// CPU-side PRNG (SplitMix64) — same algorithm as in matrix_gen.rs
23// ---------------------------------------------------------------------------
24
25/// Simple SplitMix64 PRNG for CPU-side random number generation.
26struct SplitMix64 {
27    state: u64,
28}
29
30impl SplitMix64 {
31    /// Creates a new SplitMix64 with the given seed.
32    fn new(seed: u64) -> Self {
33        Self { state: seed }
34    }
35
36    /// Returns the next u64 random value.
37    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    /// Returns a uniform f64 in [0, 1).
46    fn next_f64(&mut self) -> f64 {
47        (self.next_u64() >> 11) as f64 / ((1u64 << 53) as f64)
48    }
49
50    /// Returns a uniform usize in [0, bound).
51    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// ---------------------------------------------------------------------------
60// GraphType
61// ---------------------------------------------------------------------------
62
63/// Whether the graph is directed or undirected.
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
65pub enum GraphType {
66    /// Undirected graph — edges are symmetric.
67    Undirected,
68    /// Directed graph — edge (i, j) does not imply (j, i).
69    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// ---------------------------------------------------------------------------
82// GraphStats
83// ---------------------------------------------------------------------------
84
85/// Aggregate statistics of a graph.
86#[derive(Debug, Clone)]
87pub struct GraphStats {
88    /// Total number of vertices.
89    pub num_vertices: usize,
90    /// Total number of edges (for undirected graphs, each undirected edge is
91    /// counted once even though both (i,j) and (j,i) appear in the adjacency
92    /// list).
93    pub num_edges: usize,
94    /// Average degree across all vertices.
95    pub avg_degree: f64,
96    /// Maximum degree of any vertex.
97    pub max_degree: usize,
98    /// Minimum degree of any vertex.
99    pub min_degree: usize,
100    /// Whether the graph is approximately connected (BFS from vertex 0 reaches
101    /// >90 % of vertices).
102    pub is_connected: bool,
103    /// Edge density: ratio of existing edges to maximum possible edges.
104    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// ---------------------------------------------------------------------------
125// AdjacencyList
126// ---------------------------------------------------------------------------
127
128/// Sparse adjacency representation of a graph in CSR-like format.
129///
130/// For vertex `v`, its neighbours are stored in
131/// `col_indices[row_offsets[v]..row_offsets[v + 1]]`.
132#[derive(Debug, Clone)]
133pub struct AdjacencyList {
134    /// Number of vertices.
135    pub num_vertices: usize,
136    /// Row offset array of length `num_vertices + 1`.
137    pub row_offsets: Vec<usize>,
138    /// Column index array containing neighbour vertex ids.
139    pub col_indices: Vec<usize>,
140    /// Whether the graph is directed or undirected.
141    pub graph_type: GraphType,
142}
143
144impl AdjacencyList {
145    /// Returns the degree of vertex `v`.
146    ///
147    /// # Errors
148    ///
149    /// Returns `RandError::InvalidSize` if `v >= num_vertices`.
150    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    /// Returns the neighbours of vertex `v`.
161    ///
162    /// # Errors
163    ///
164    /// Returns `RandError::InvalidSize` if `v >= num_vertices`.
165    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    /// Computes aggregate statistics of the graph.
176    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    /// Converts to CSR format with uniform edge weights.
241    ///
242    /// Returns `(row_offsets, col_indices, values)` where every edge receives
243    /// the supplied `default_val`.
244    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
267// ---------------------------------------------------------------------------
268// Helper: approximate connectivity check
269// ---------------------------------------------------------------------------
270
271/// Returns `true` if a BFS from vertex 0 reaches more than 90 % of the
272/// vertices (an approximation of full connectivity).
273pub 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
300// ---------------------------------------------------------------------------
301// Helper: build AdjacencyList from edge list
302// ---------------------------------------------------------------------------
303
304/// Builds an `AdjacencyList` from a list of directed edges.
305///
306/// For undirected graphs the caller must include both `(i, j)` and `(j, i)`.
307fn adjacency_from_edges(
308    num_vertices: usize,
309    edges: &mut [(usize, usize)],
310    graph_type: GraphType,
311) -> AdjacencyList {
312    // Sort by source vertex for CSR construction.
313    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
333// ---------------------------------------------------------------------------
334// ErdosRenyiGenerator
335// ---------------------------------------------------------------------------
336
337/// Generates random graphs according to the Erdős–Rényi G(n, p) model.
338///
339/// Each of the `n*(n-1)/2` possible undirected edges (or `n*(n-1)` directed
340/// edges) is independently included with probability `p`.
341pub struct ErdosRenyiGenerator {
342    num_vertices: usize,
343    edge_probability: f64,
344    seed: u64,
345}
346
347impl ErdosRenyiGenerator {
348    /// Creates a new Erdős–Rényi generator.
349    ///
350    /// # Arguments
351    ///
352    /// * `num_vertices` — number of vertices n
353    /// * `edge_probability` — probability p that any given edge exists
354    /// * `seed` — PRNG seed
355    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    /// Generates an undirected G(n, p) random graph.
364    ///
365    /// # Errors
366    ///
367    /// Returns `RandError::InvalidSize` if `edge_probability` is not in [0, 1].
368    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    /// Generates a directed G(n, p) random graph.
387    ///
388    /// # Errors
389    ///
390    /// Returns `RandError::InvalidSize` if `edge_probability` is not in [0, 1].
391    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    /// Validates generator parameters.
409    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
420// ---------------------------------------------------------------------------
421// StochasticBlockModelGenerator
422// ---------------------------------------------------------------------------
423
424/// Generates random graphs with community structure according to the
425/// Stochastic Block Model.
426///
427/// Vertices are partitioned into blocks (communities). Within the same block,
428/// edges appear with probability `intra_prob`; between different blocks, with
429/// probability `inter_prob`.
430pub struct StochasticBlockModelGenerator {
431    block_sizes: Vec<usize>,
432    intra_prob: f64,
433    inter_prob: f64,
434    seed: u64,
435}
436
437impl StochasticBlockModelGenerator {
438    /// Creates a new SBM generator.
439    ///
440    /// # Arguments
441    ///
442    /// * `block_sizes` — sizes of each community
443    /// * `intra_prob` — edge probability within communities
444    /// * `inter_prob` — edge probability between communities
445    /// * `seed` — PRNG seed
446    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    /// Generates an undirected SBM graph.
456    ///
457    /// # Errors
458    ///
459    /// Returns `RandError::InvalidSize` if probabilities are outside [0, 1] or
460    /// `block_sizes` is empty.
461    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        // Build block assignments: block_of[v] = block index for vertex v
484        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
511// ---------------------------------------------------------------------------
512// BarabasiAlbertGenerator
513// ---------------------------------------------------------------------------
514
515/// Generates scale-free random graphs via the Barabási–Albert preferential
516/// attachment model.
517///
518/// Starting from a small complete graph of `m₀ = attachment_edges + 1`
519/// vertices, each new vertex connects to `attachment_edges` existing vertices
520/// with probability proportional to their current degree.
521pub struct BarabasiAlbertGenerator {
522    num_vertices: usize,
523    attachment_edges: usize,
524    seed: u64,
525}
526
527impl BarabasiAlbertGenerator {
528    /// Creates a new Barabási–Albert generator.
529    ///
530    /// # Arguments
531    ///
532    /// * `num_vertices` — total number of vertices in the final graph
533    /// * `attachment_edges` — number of edges each new vertex adds (m)
534    /// * `seed` — PRNG seed
535    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    /// Generates an undirected Barabási–Albert graph.
544    ///
545    /// # Errors
546    ///
547    /// Returns `RandError::InvalidSize` if `attachment_edges` is zero,
548    /// `num_vertices <= attachment_edges`, or any other inconsistency.
549    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; // initial complete graph size
559        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        // Degree array (used for preferential attachment weights).
568        let mut degree = vec![0usize; n];
569        let mut edges: Vec<(usize, usize)> = Vec::new();
570
571        // Initial complete graph on vertices [0, m0).
572        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        // Preferential attachment: add vertices m0..n one at a time.
582        // We use a repeated-entries array for weighted sampling (fast and simple).
583        // `stubs[k]` = some vertex id; vertex v appears `degree[v]` times.
584        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            // Select m distinct targets proportional to degree.
593            let mut targets: Vec<usize> = Vec::with_capacity(m);
594            let mut target_set = vec![false; n]; // avoid duplicates
595
596            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            // Add edges from new_v to each target.
611            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
625// ---------------------------------------------------------------------------
626// WattsStrogatzGenerator
627// ---------------------------------------------------------------------------
628
629/// Generates small-world random graphs via the Watts–Strogatz model.
630///
631/// Starts from a ring lattice where each vertex is connected to its
632/// `k_neighbors` nearest neighbours (k/2 on each side), then rewires each
633/// edge with probability `rewire_prob`.
634pub struct WattsStrogatzGenerator {
635    num_vertices: usize,
636    k_neighbors: usize,
637    rewire_prob: f64,
638    seed: u64,
639}
640
641impl WattsStrogatzGenerator {
642    /// Creates a new Watts–Strogatz generator.
643    ///
644    /// # Arguments
645    ///
646    /// * `num_vertices` — number of vertices
647    /// * `k_neighbors` — each vertex is connected to its k nearest neighbours
648    ///   in the ring (must be even and < n)
649    /// * `rewire_prob` — probability of rewiring each edge
650    /// * `seed` — PRNG seed
651    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    /// Generates an undirected Watts–Strogatz small-world graph.
661    ///
662    /// # Errors
663    ///
664    /// Returns `RandError::InvalidSize` if parameters are inconsistent.
665    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            // No edges
679            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        // Build adjacency as a set of edges per vertex for efficient rewiring.
707        // Use Vec<Vec<usize>> as a simple adjacency set.
708        let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
709
710        // Ring lattice: connect each vertex to its k/2 nearest on each side.
711        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        // Rewire: for each vertex i and each of its right-side neighbours,
720        // with probability beta, replace (i, j) with (i, j') where j' is
721        // a uniformly random vertex != i and not already a neighbour.
722        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                // Remove edge (i, j) and (j, i)
731                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                // Pick a new target j' != i, not already a neighbour of i
739                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                    // Couldn't find a valid target; re-add original edge.
758                    adj[i].push(j);
759                    adj[j].push(i);
760                }
761            }
762        }
763
764        // Convert adjacency lists to CSR.
765        Ok(adjacency_vecs_to_csr(n, &adj, GraphType::Undirected))
766    }
767}
768
769// ---------------------------------------------------------------------------
770// RandomRegularGenerator
771// ---------------------------------------------------------------------------
772
773/// Generates random k-regular graphs where every vertex has the same degree.
774///
775/// Uses the pairing (configuration) model: create `k` stubs for each vertex,
776/// then randomly match stubs. Self-loops and multi-edges are discarded and
777/// re-attempted.
778pub struct RandomRegularGenerator {
779    num_vertices: usize,
780    degree: usize,
781    seed: u64,
782}
783
784impl RandomRegularGenerator {
785    /// Creates a new random regular graph generator.
786    ///
787    /// # Arguments
788    ///
789    /// * `num_vertices` — number of vertices
790    /// * `degree` — the common degree k (n*k must be even)
791    /// * `seed` — PRNG seed
792    pub fn new(num_vertices: usize, degree: usize, seed: u64) -> Self {
793        Self {
794            num_vertices,
795            degree,
796            seed,
797        }
798    }
799
800    /// Generates a random k-regular undirected graph.
801    ///
802    /// # Errors
803    ///
804    /// Returns `RandError::InvalidSize` if `num_vertices * degree` is odd,
805    /// or `degree >= num_vertices`.
806    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        // Pairing model with retries.
839        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        // Fallback: return best-effort graph from last attempt (very unlikely
850        // to reach here for reasonable parameters).
851        Err(RandError::InternalError(format!(
852            "failed to generate {k}-regular graph on {n} vertices after {max_retries} attempts"
853        )))
854    }
855
856    /// Attempts a single pairing-model generation. Returns `None` if the
857    /// attempt produced too many self-loops or multi-edges and should be
858    /// retried.
859    fn try_pairing(&self, rng: &mut SplitMix64, n: usize, k: usize) -> Option<AdjacencyList> {
860        // Create stub list: vertex v appears k times.
861        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        // Fisher-Yates shuffle.
870        for i in (1..total_stubs).rev() {
871            let j = rng.next_usize(i + 1);
872            stubs.swap(i, j);
873        }
874
875        // Pair up consecutive stubs.
876        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                // Self-loop — invalid.
885                valid = false;
886                break;
887            }
888            if adj[u].contains(&v) {
889                // Multi-edge — invalid.
890                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
905// ---------------------------------------------------------------------------
906// Helper: convert Vec<Vec<usize>> adjacency to AdjacencyList
907// ---------------------------------------------------------------------------
908
909/// Converts per-vertex adjacency vectors into CSR `AdjacencyList`.
910fn 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// ===========================================================================
937// Tests
938// ===========================================================================
939
940#[cfg(test)]
941mod tests {
942    use super::*;
943
944    // -------------------------------------------------------------------
945    // Erdos-Renyi tests
946    // -------------------------------------------------------------------
947
948    #[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        // Allow generous tolerance for random graphs.
958        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    // -------------------------------------------------------------------
1002    // Stochastic Block Model tests
1003    // -------------------------------------------------------------------
1004
1005    #[test]
1006    fn sbm_community_structure() {
1007        // Two blocks of 50 vertices each: intra=0.5, inter=0.05.
1008        let gr = StochasticBlockModelGenerator::new(vec![50, 50], 0.5, 0.05, 42);
1009        let g = gr.generate().expect("SBM generate failed");
1010
1011        // Count intra vs inter edges.
1012        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        // Each undirected edge counted twice in adjacency.
1027        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        // When intra_prob == inter_prob, SBM is equivalent to ER.
1036        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    // -------------------------------------------------------------------
1051    // Barabasi-Albert tests
1052    // -------------------------------------------------------------------
1053
1054    #[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        // Verify power-law-ish: there should be a few high-degree hubs.
1062        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        // Every vertex added after the initial clique should have degree >= m.
1079        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    // -------------------------------------------------------------------
1089    // Watts-Strogatz tests
1090    // -------------------------------------------------------------------
1091
1092    #[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        // Every vertex should have degree exactly k.
1100        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        // Total edges should be approximately n*k/2 (some may fail to rewire).
1114        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    // -------------------------------------------------------------------
1125    // Random Regular tests
1126    // -------------------------------------------------------------------
1127
1128    #[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    // -------------------------------------------------------------------
1142    // GraphStats / AdjacencyList tests
1143    // -------------------------------------------------------------------
1144
1145    #[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        // Complete graph: every vertex has degree n-1.
1161        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        // Out-of-range vertex.
1169        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        // Vertex 0 in K_5 should have neighbours {1, 2, 3, 4}.
1178        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        // Out-of-range.
1184        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        // All edge weights should be 1.0
1196        for &v in &vals {
1197            assert!((v - 1.0).abs() < 1e-15);
1198        }
1199    }
1200
1201    #[test]
1202    fn connectivity_check() {
1203        // Dense ER graph should be connected.
1204        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        // Sparse graph with p=0 is not connected (unless trivial).
1212        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}