Skip to main content

rust_igraph/algorithms/games/
growing_random.rs

1//! Growing random graph (ALGO-GN-003).
2//!
3//! Counterpart of `igraph_growing_random_game()` in
4//! `references/igraph/src/games/growing_random.c:55-105`.
5//!
6//! The model starts with a single vertex and grows the graph one step
7//! at a time. On each step a fresh vertex is added together with `m`
8//! new edges. Two endpoint-selection rules are supported:
9//!
10//! * **Citation mode** (`citation = true`): every new edge originates
11//!   at the freshly-added vertex and connects to a uniformly random
12//!   earlier vertex. Each new edge therefore points from `i` (the
13//!   freshly-added id) to some `to ∈ [0, i - 1]`.
14//! * **Free mode** (`citation = false`): both endpoints are uniformly
15//!   sampled — `from` from `[0, i]` (the new vertex itself is
16//!   allowed) and `to` from `[1, i]`. The asymmetric bounds are
17//!   inherited from upstream, where the closed interval `[0, i]`
18//!   includes the new vertex and `[1, i]` excludes vertex 0 from the
19//!   sink side to avoid pinning every edge at the seed.
20//!
21//! Compared with the BAG variant of Barabási–Albert
22//! ([`crate::barabasi_game_bag`]) the per-step kernel here is *uniform*
23//! rather than degree-proportional — the bag bookkeeping disappears and
24//! every step is a constant-time RNG draw. The resulting degree
25//! distribution decays exponentially (not as a power law).
26//!
27//! Time complexity: `O(|V| + |E|) = O(n · m)`. Memory is the edge list
28//! only: `O(n · m)`.
29//!
30//! ## Scope
31//!
32//! The full upstream call signature is `(graph, n, m, directed,
33//! citation)` — exposed here as four arguments plus a `seed`. There are
34//! no special parameters or `start_from` hooks to translate.
35
36#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
37
38use crate::core::rng::SplitMix64;
39use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
40
41fn validate_inputs(n: u32, m: u32) -> IgraphResult<()> {
42    let n_u64 = u64::from(n);
43    let m_u64 = u64::from(m);
44    let steps = n_u64.saturating_sub(1);
45    let edges = steps
46        .checked_mul(m_u64)
47        .ok_or(IgraphError::Internal("growing_random edge count overflow"))?;
48    // Allocation bound: each (VertexId, VertexId) is 8 bytes; the
49    // allocator caps total bytes at isize::MAX. We use a conservative
50    // hard cap of u32::MAX edges (matching the upstream IGRAPH_ECOUNT_MAX
51    // convention) — well below any platform's allocator limit and still
52    // far beyond what practical graphs ever ask for.
53    if edges > u64::from(u32::MAX) {
54        return Err(IgraphError::Internal(
55            "growing_random edge count exceeds u32::MAX",
56        ));
57    }
58    Ok(())
59}
60
61/// Generate a growing random graph on `n` vertices, adding `m` new
62/// edges per step.
63///
64/// * `n` — vertex count. `n = 0` returns an empty graph; `n = 1`
65///   returns a single isolated vertex.
66/// * `m` — number of new edges contributed per added vertex.
67///   `m = 0` yields `n` isolated vertices.
68/// * `directed` — generate a directed graph.
69/// * `citation` — if `true`, every new edge originates at the
70///   freshly-added vertex; if `false`, both endpoints are uniformly
71///   sampled within the current frontier.
72/// * `seed` — initialises an internal `SplitMix64` PRNG. Same
73///   `(n, m, directed, citation, seed)` always yields the same graph.
74///
75/// # Errors
76///
77/// Returns [`IgraphError::Internal`] if `(n - 1) · m` overflows `u64`
78/// (only possible at `(u32::MAX, u32::MAX)` scale).
79///
80/// # Examples
81///
82/// ```
83/// use rust_igraph::growing_random_game;
84///
85/// // 50-vertex directed citation graph with 2 new edges per step.
86/// // Exactly (n - 1) · m = 49 · 2 = 98 edges.
87/// let g = growing_random_game(50, 2, true, true, 0xCAFE).unwrap();
88/// assert_eq!(g.vcount(), 50);
89/// assert_eq!(g.ecount(), 98);
90/// assert!(g.is_directed());
91/// ```
92pub fn growing_random_game(
93    n: u32,
94    m: u32,
95    directed: bool,
96    citation: bool,
97    seed: u64,
98) -> IgraphResult<Graph> {
99    validate_inputs(n, m)?;
100
101    if n == 0 {
102        return Graph::new(0, directed);
103    }
104    if n == 1 || m == 0 {
105        return Graph::new(n, directed);
106    }
107
108    let m_usize = m as usize;
109    let steps = (n as usize) - 1;
110    let total_edges = steps
111        .checked_mul(m_usize)
112        .ok_or(IgraphError::Internal("growing_random edge total overflow"))?;
113
114    let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(total_edges);
115    let mut rng = SplitMix64::new(seed);
116
117    for i in 1..n {
118        for _ in 0..m {
119            let (from, to) = if citation {
120                // RNG_INTEGER(0, i - 1) in C — closed interval, so 0 ≤ to ≤ i - 1.
121                let to = rng.gen_index(i as usize) as VertexId;
122                (i, to)
123            } else {
124                // RNG_INTEGER(0, i) and RNG_INTEGER(1, i) — both closed intervals.
125                let from = rng.gen_index((i as usize) + 1) as VertexId;
126                let to = (rng.gen_index(i as usize) as VertexId) + 1;
127                (from, to)
128            };
129            edges.push((from, to));
130        }
131    }
132
133    let mut g = Graph::new(n, directed)?;
134    g.add_edges(edges)?;
135    Ok(g)
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    fn collect_edges(g: &Graph) -> Vec<(VertexId, VertexId)> {
143        let n_edges = u32::try_from(g.ecount()).expect("ecount fits in u32 in tests");
144        (0..n_edges)
145            .map(|eid| g.edge(eid).expect("edge id in bounds"))
146            .collect()
147    }
148
149    #[test]
150    fn n_zero_returns_empty_graph() {
151        let g = growing_random_game(0, 3, true, true, 1).unwrap();
152        assert_eq!(g.vcount(), 0);
153        assert_eq!(g.ecount(), 0);
154    }
155
156    #[test]
157    fn n_one_returns_singleton() {
158        let g = growing_random_game(1, 5, true, true, 1).unwrap();
159        assert_eq!(g.vcount(), 1);
160        assert_eq!(g.ecount(), 0);
161    }
162
163    #[test]
164    fn m_zero_returns_edgeless() {
165        let g = growing_random_game(20, 0, true, false, 1).unwrap();
166        assert_eq!(g.vcount(), 20);
167        assert_eq!(g.ecount(), 0);
168    }
169
170    #[test]
171    fn exact_edge_count_directed_constant_m() {
172        for &(n, m) in &[(10u32, 1u32), (10, 3), (50, 2), (200, 5)] {
173            let g = growing_random_game(n, m, true, true, 0x00C0_FFEE).unwrap();
174            assert_eq!(g.vcount(), n);
175            assert_eq!(g.ecount(), (n as usize - 1) * m as usize);
176        }
177    }
178
179    #[test]
180    fn exact_edge_count_undirected_free_mode() {
181        // Edge count is independent of mode/direction.
182        let g = growing_random_game(100, 4, false, false, 0xDEAD_BEEF).unwrap();
183        assert_eq!(g.vcount(), 100);
184        assert_eq!(g.ecount(), 99 * 4);
185        assert!(!g.is_directed());
186    }
187
188    #[test]
189    fn deterministic_with_seed() {
190        let a = growing_random_game(80, 3, true, true, 0xABCD).unwrap();
191        let b = growing_random_game(80, 3, true, true, 0xABCD).unwrap();
192        assert_eq!(a.vcount(), b.vcount());
193        assert_eq!(a.ecount(), b.ecount());
194        let ea: Vec<_> = collect_edges(&a);
195        let eb: Vec<_> = collect_edges(&b);
196        assert_eq!(ea, eb);
197    }
198
199    #[test]
200    fn different_seeds_yield_different_graphs() {
201        let a = growing_random_game(60, 2, true, false, 1).unwrap();
202        let b = growing_random_game(60, 2, true, false, 2).unwrap();
203        let ea: Vec<_> = collect_edges(&a);
204        let eb: Vec<_> = collect_edges(&b);
205        assert_ne!(ea, eb, "different seeds must produce different graphs");
206    }
207
208    #[test]
209    fn citation_mode_directed_has_strict_temporal_order() {
210        // Citation edges: (i, to) with 0 ≤ to < i for every edge.
211        let g = growing_random_game(100, 3, true, true, 0xBEEF).unwrap();
212        for (src, dst) in collect_edges(&g) {
213            assert!(
214                dst < src,
215                "citation edge ({src} -> {dst}) violates temporal ordering"
216            );
217        }
218    }
219
220    #[test]
221    fn citation_mode_directed_no_self_loops() {
222        // dst < src ⇒ src ≠ dst.
223        let g = growing_random_game(120, 4, true, true, 0xFACE).unwrap();
224        for (src, dst) in collect_edges(&g) {
225            assert_ne!(src, dst, "citation mode must not produce self-loops");
226        }
227    }
228
229    #[test]
230    fn citation_mode_first_vertex_never_source() {
231        // Vertex 0 is the seed; its row in citation mode is always empty.
232        let g = growing_random_game(75, 2, true, true, 0x42).unwrap();
233        for (src, _dst) in collect_edges(&g) {
234            assert_ne!(src, 0, "seed vertex must never be the source in citation");
235        }
236    }
237
238    #[test]
239    fn citation_mode_per_step_outdegree_equals_m() {
240        // Citation mode emits exactly m edges with src = i on step i.
241        let n: u32 = 50;
242        let m: u32 = 3;
243        let g = growing_random_game(n, m, true, true, 0xC1A0).unwrap();
244        let mut per_src = vec![0u32; n as usize];
245        for (src, _dst) in collect_edges(&g) {
246            per_src[src as usize] += 1;
247        }
248        // Vertex 0 = 0 (seed). Vertices 1..n = exactly m each.
249        assert_eq!(per_src[0], 0);
250        for (i, &out) in per_src.iter().enumerate().skip(1) {
251            assert_eq!(out, m, "vertex {i} out-degree should be m={m}");
252        }
253    }
254
255    #[test]
256    fn free_mode_directed_endpoints_within_frontier() {
257        // (from, to) generated at step i must satisfy from ∈ [0, i] and
258        // to ∈ [1, i]. After construction we can't observe step i
259        // directly, but we know that for every edge (f, t):
260        //   - t ≥ 1 (sink never lands on vertex 0).
261        //   - max(f, t) was the current frontier upper bound at insert
262        //     time, so max(f, t) > 0.
263        let g = growing_random_game(100, 3, true, false, 0xBABE).unwrap();
264        for (src, dst) in collect_edges(&g) {
265            assert!(
266                dst >= 1,
267                "free-mode sink should never be 0 (got edge ({src}, {dst}))"
268            );
269        }
270    }
271
272    #[test]
273    fn validate_huge_overflow_is_rejected_or_handled() {
274        // Worst case: u32::MAX × u32::MAX would overflow u64 (≈1.8e19 ≈ u64::MAX),
275        // so the multiplication is tight. We just assert that very large
276        // inputs do not panic — they either error or return.
277        let huge = u32::MAX;
278        let res = growing_random_game(huge, huge, true, true, 0);
279        let _ = res;
280    }
281}