rust-igraph 0.0.1-alpha.4

Pure-Rust, high-performance graph & network analysis library — 400+ algorithms, zero unsafe, igraph-compatible
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
//! Uniform random labelled tree generators (ALGO-GN-004).
//!
//! Counterpart of `igraph_tree_game()` in
//! `references/igraph/src/games/tree.c`. Two methods are available:
//!
//! * **LERW** ([`tree_game_lerw`]) — Wilson's loop-erased random walk on
//!   `K_n`. Supports both directed (out-rooted) and undirected trees.
//! * **Prüfer** ([`tree_game_prufer`]) — samples a random Prüfer sequence,
//!   then decodes it via [`from_prufer`]. Undirected
//!   only, matching the C upstream restriction.
//!
//! Both methods produce each labelled tree with equal probability.
//!
//! ## LERW algorithm
//!
//! Wilson's loop-erased random walk on the complete graph `K_n` uniformly
//! samples spanning trees. The igraph implementation collapses the naive
//! "walk until you hit a visited vertex, then erase the loop" formulation
//! into a single linear pass:
//!
//! 1. Maintain an array `vertices = [0, 1, …, n - 1]` partitioned so that
//!    positions `[0, k)` hold the visited vertices and `[k, n)` hold the
//!    unvisited ones.
//! 2. Pick an initial vertex `i` uniformly, mark it visited, swap it to
//!    position `0`, set `k = 1`.
//! 3. For each remaining step `k = 1 .. n`: draw `j ∈ [0, n)`. If
//!    `vertices[j]` is already visited (its slot lies in `[0, k)`),
//!    advance the walk by setting `i = vertices[j]` and resample
//!    `j ∈ [k, n)` so the next visited vertex is guaranteed to be a
//!    fresh one. Then mark `vertices[j]` visited, swap it to position
//!    `k`, emit the edge `(i, vertices[k])`, and update
//!    `i = vertices[k]`.
//!
//! Each iteration produces exactly one edge, so the tree has `n - 1`
//! edges. Runtime is `O(n)` walk steps amortised — there is no rejection
//! loop because step 3a covers the "already visited" branch in one shot.
//!
//! ## Prüfer algorithm
//!
//! Generate `n - 2` uniform random values in `[0, n)`, forming a Prüfer
//! sequence, then decode it with the linear-time algorithm from
//! [`from_prufer`]. The result is always an undirected
//! labelled tree.

#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]

use crate::algorithms::constructors::prufer::from_prufer;
use crate::core::rng::SplitMix64;
use crate::core::{Graph, IgraphError, IgraphResult, VertexId};

/// Generate a uniformly random labelled tree on `n` vertices using
/// Wilson's loop-erased random walk.
///
/// * `n` — vertex count. `n = 0` returns an empty graph, `n = 1`
///   returns a single isolated vertex.
/// * `directed` — if `true`, edges are stored with parent-to-child
///   orientation in walk order (the resulting tree is rooted at the
///   randomly chosen initial vertex). If `false`, the same edges are
///   stored as an undirected tree.
/// * `seed` — initialises an internal `SplitMix64` PRNG. Same
///   `(n, directed, seed)` always yields the same tree.
///
/// The output has exactly `max(0, n - 1)` edges, is acyclic, has no
/// self-loops, and (in the undirected case) is connected.
///
/// # Examples
///
/// ```
/// use rust_igraph::tree_game_lerw;
///
/// // 30-vertex undirected uniform random tree.
/// let g = tree_game_lerw(30, false, 0xC0FF_EE00).unwrap();
/// assert_eq!(g.vcount(), 30);
/// assert_eq!(g.ecount(), 29);  // n - 1 edges
/// assert!(!g.is_directed());
/// ```
pub fn tree_game_lerw(n: u32, directed: bool, seed: u64) -> IgraphResult<Graph> {
    if n < 2 {
        return Graph::new(n, directed);
    }

    let n_usize = n as usize;
    let no_edges = n_usize - 1;

    let mut vertices: Vec<VertexId> = (0..n).collect();
    let mut visited: Vec<bool> = vec![false; n_usize];
    let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(no_edges);

    let mut rng = SplitMix64::new(seed);

    // Pick the initial vertex uniformly, mark it visited, swap to slot 0.
    let i0 = rng.gen_index(n_usize);
    visited[vertices[i0] as usize] = true;
    vertices.swap(0, i0);

    let mut prev: VertexId = vertices[0];

    for k in 1..n_usize {
        // Draw a candidate slot from [0, n).
        let mut j = rng.gen_index(n_usize);
        if visited[vertices[j] as usize] {
            // Already visited — advance the walk and resample from the
            // unvisited tail [k, n) so the next emit is fresh.
            prev = vertices[j];
            j = k + rng.gen_index(n_usize - k);
        }
        visited[vertices[j] as usize] = true;
        vertices.swap(k, j);
        let new_v = vertices[k];
        edges.push((prev, new_v));
        prev = new_v;
    }

    let mut g = Graph::new(n, directed)?;
    g.add_edges(edges)?;
    Ok(g)
}

/// Generate a uniformly random labelled tree on `n` vertices using the
/// Prüfer sequence method.
///
/// Samples a random Prüfer sequence of length `n - 2` (each entry
/// uniform in `[0, n)`), then decodes it with [`from_prufer`] to
/// produce an undirected labelled tree.
///
/// * `n` — vertex count. `n = 0` returns an empty graph, `n = 1`
///   returns a single isolated vertex.
/// * `seed` — initialises an internal `SplitMix64` PRNG. Same
///   `(n, seed)` always yields the same tree.
///
/// The output has exactly `max(0, n - 1)` edges, is acyclic, has no
/// self-loops, and is connected.
///
/// Counterpart of the `IGRAPH_RANDOM_TREE_PRUFER` branch of
/// `igraph_tree_game()` in `references/igraph/src/games/tree.c:37-56`.
///
/// # Errors
///
/// * [`IgraphError::InvalidArgument`] — `n` overflows internal
///   allocation limits.
///
/// # Examples
///
/// ```
/// use rust_igraph::tree_game_prufer;
///
/// let g = tree_game_prufer(30, 0xC0FF_EE00).unwrap();
/// assert_eq!(g.vcount(), 30);
/// assert_eq!(g.ecount(), 29);
/// assert!(!g.is_directed());
/// ```
pub fn tree_game_prufer(n: u32, seed: u64) -> IgraphResult<Graph> {
    if n < 2 {
        return Graph::new(n, false);
    }

    let seq_len = (n - 2) as usize;
    let mut rng = SplitMix64::new(seed);
    let n_usize = n as usize;

    let mut prufer: Vec<u32> = Vec::with_capacity(seq_len);
    for _ in 0..seq_len {
        let val = rng.gen_index(n_usize);
        let val_u32 = u32::try_from(val).map_err(|_| {
            IgraphError::InvalidArgument("tree_game_prufer: vertex index overflow".to_string())
        })?;
        prufer.push(val_u32);
    }

    from_prufer(&prufer)
}

#[cfg(test)]
mod tests {
    use super::*;

    fn collect_edges(g: &Graph) -> Vec<(VertexId, VertexId)> {
        let n_edges = u32::try_from(g.ecount()).expect("ecount fits in u32 in tests");
        (0..n_edges)
            .map(|eid| g.edge(eid).expect("edge id in bounds"))
            .collect()
    }

    /// Union-find for connectivity / acyclicity invariants.
    struct UnionFind {
        parent: Vec<u32>,
    }
    impl UnionFind {
        fn new(n: usize) -> Self {
            Self {
                parent: (0..n as u32).collect(),
            }
        }
        fn find(&mut self, mut x: u32) -> u32 {
            while self.parent[x as usize] != x {
                let p = self.parent[x as usize];
                self.parent[x as usize] = self.parent[p as usize];
                x = self.parent[x as usize];
            }
            x
        }
        /// Returns `true` when the union created a new bridge,
        /// `false` if the two ends were already in the same component.
        fn union(&mut self, a: u32, b: u32) -> bool {
            let ra = self.find(a);
            let rb = self.find(b);
            if ra == rb {
                false
            } else {
                self.parent[ra as usize] = rb;
                true
            }
        }
    }

    #[test]
    fn n_zero_returns_empty_graph() {
        let g = tree_game_lerw(0, false, 1).unwrap();
        assert_eq!(g.vcount(), 0);
        assert_eq!(g.ecount(), 0);
    }

    #[test]
    fn n_one_returns_singleton() {
        let g = tree_game_lerw(1, false, 1).unwrap();
        assert_eq!(g.vcount(), 1);
        assert_eq!(g.ecount(), 0);
    }

    #[test]
    fn n_two_returns_single_edge() {
        let g = tree_game_lerw(2, false, 0xBEEF).unwrap();
        assert_eq!(g.vcount(), 2);
        assert_eq!(g.ecount(), 1);
        let (a, b) = g.edge(0).unwrap();
        assert_ne!(a, b, "tree edge must not be a self-loop");
        let lo = a.min(b);
        let hi = a.max(b);
        assert_eq!(lo, 0);
        assert_eq!(hi, 1);
    }

    #[test]
    fn exact_edge_count() {
        for &n in &[5u32, 10, 50, 200, 1000] {
            let g = tree_game_lerw(n, false, 0xC0FF_EE00 + u64::from(n)).unwrap();
            assert_eq!(g.vcount(), n);
            assert_eq!(g.ecount() as u32, n - 1);
        }
    }

    #[test]
    fn no_self_loops() {
        let g = tree_game_lerw(150, false, 0xFACE).unwrap();
        for (a, b) in collect_edges(&g) {
            assert_ne!(a, b, "Wilson LERW must not emit self-loops");
        }
    }

    #[test]
    fn output_is_acyclic_and_connected_undirected() {
        let g = tree_game_lerw(200, false, 0xCAFE).unwrap();
        let n = g.vcount() as usize;
        let mut uf = UnionFind::new(n);
        for (a, b) in collect_edges(&g) {
            assert!(
                uf.union(a, b),
                "edge ({a}, {b}) closed a cycle — output is not a tree"
            );
        }
        // After n - 1 distinct unions all vertices must share one root.
        let root = uf.find(0);
        for v in 1..n as u32 {
            assert_eq!(
                uf.find(v),
                root,
                "vertex {v} is in a disconnected component"
            );
        }
    }

    #[test]
    fn output_is_acyclic_and_connected_directed() {
        // Directed mode stores edges with parent→child orientation, but
        // connectivity should still hold on the underlying undirected
        // graph.
        let g = tree_game_lerw(150, true, 0xDEAD).unwrap();
        assert!(g.is_directed());
        let n = g.vcount() as usize;
        let mut uf = UnionFind::new(n);
        for (a, b) in collect_edges(&g) {
            assert!(
                uf.union(a, b),
                "directed tree edge ({a}, {b}) closed a cycle"
            );
        }
        let root = uf.find(0);
        for v in 1..n as u32 {
            assert_eq!(uf.find(v), root);
        }
    }

    #[test]
    fn directed_mode_has_no_duplicate_targets() {
        // Each freshly visited vertex appears exactly once as a target,
        // which means in-degree of every non-root vertex is exactly 1.
        let g = tree_game_lerw(100, true, 0xBAD_F00D).unwrap();
        let n = g.vcount() as usize;
        let mut indeg = vec![0u32; n];
        for (_a, b) in collect_edges(&g) {
            indeg[b as usize] += 1;
        }
        let roots: Vec<_> = indeg.iter().enumerate().filter(|&(_, &d)| d == 0).collect();
        assert_eq!(roots.len(), 1, "directed Wilson tree has exactly one root");
        for (v, &d) in indeg.iter().enumerate() {
            if v == roots[0].0 {
                continue;
            }
            assert_eq!(d, 1, "non-root vertex {v} must have in-degree 1, got {d}");
        }
    }

    #[test]
    fn deterministic_with_seed() {
        let a = tree_game_lerw(80, false, 0xABCD).unwrap();
        let b = tree_game_lerw(80, false, 0xABCD).unwrap();
        assert_eq!(a.vcount(), b.vcount());
        assert_eq!(a.ecount(), b.ecount());
        assert_eq!(collect_edges(&a), collect_edges(&b));
    }

    #[test]
    fn different_seeds_yield_different_trees() {
        let a = tree_game_lerw(60, false, 1).unwrap();
        let b = tree_game_lerw(60, false, 2).unwrap();
        assert_ne!(
            collect_edges(&a),
            collect_edges(&b),
            "different seeds must produce different trees"
        );
    }

    #[test]
    fn all_vertices_appear_in_some_edge_for_n_ge_2() {
        // A spanning tree touches every vertex.
        let g = tree_game_lerw(40, false, 0x5EED).unwrap();
        let mut seen = vec![false; g.vcount() as usize];
        for (a, b) in collect_edges(&g) {
            seen[a as usize] = true;
            seen[b as usize] = true;
        }
        for (v, &s) in seen.iter().enumerate() {
            assert!(s, "vertex {v} missing from spanning tree");
        }
    }

    // ── tree_game_prufer tests ──────────────────────────────────────

    #[test]
    fn prufer_n_zero() {
        let g = tree_game_prufer(0, 1).unwrap();
        assert_eq!(g.vcount(), 0);
        assert_eq!(g.ecount(), 0);
    }

    #[test]
    fn prufer_n_one() {
        let g = tree_game_prufer(1, 1).unwrap();
        assert_eq!(g.vcount(), 1);
        assert_eq!(g.ecount(), 0);
    }

    #[test]
    fn prufer_n_two() {
        let g = tree_game_prufer(2, 0xBEEF).unwrap();
        assert_eq!(g.vcount(), 2);
        assert_eq!(g.ecount(), 1);
        assert!(!g.is_directed());
    }

    #[test]
    fn prufer_always_undirected() {
        let g = tree_game_prufer(50, 0xFACE).unwrap();
        assert!(!g.is_directed());
    }

    #[test]
    fn prufer_exact_edge_count() {
        for &n in &[5u32, 10, 50, 200, 1000] {
            let g = tree_game_prufer(n, 0xC0DE + u64::from(n)).unwrap();
            assert_eq!(g.vcount(), n);
            assert_eq!(g.ecount() as u32, n - 1);
        }
    }

    #[test]
    fn prufer_no_self_loops() {
        let g = tree_game_prufer(150, 0xFACE).unwrap();
        for (a, b) in collect_edges(&g) {
            assert_ne!(a, b, "Prüfer tree must not emit self-loops");
        }
    }

    #[test]
    fn prufer_is_acyclic_and_connected() {
        let g = tree_game_prufer(200, 0xCAFE).unwrap();
        let n = g.vcount() as usize;
        let mut uf = UnionFind::new(n);
        for (a, b) in collect_edges(&g) {
            assert!(
                uf.union(a, b),
                "edge ({a}, {b}) closed a cycle — not a tree"
            );
        }
        let root = uf.find(0);
        for v in 1..n as u32 {
            assert_eq!(uf.find(v), root, "vertex {v} in disconnected component");
        }
    }

    #[test]
    fn prufer_deterministic_with_seed() {
        let a = tree_game_prufer(80, 0xABCD).unwrap();
        let b = tree_game_prufer(80, 0xABCD).unwrap();
        assert_eq!(collect_edges(&a), collect_edges(&b));
    }

    #[test]
    fn prufer_different_seeds_yield_different_trees() {
        let a = tree_game_prufer(60, 1).unwrap();
        let b = tree_game_prufer(60, 2).unwrap();
        assert_ne!(
            collect_edges(&a),
            collect_edges(&b),
            "different seeds must produce different trees"
        );
    }

    #[test]
    fn prufer_all_vertices_touched() {
        let g = tree_game_prufer(40, 0x5EED).unwrap();
        let mut seen = vec![false; g.vcount() as usize];
        for (a, b) in collect_edges(&g) {
            seen[a as usize] = true;
            seen[b as usize] = true;
        }
        for (v, &s) in seen.iter().enumerate() {
            assert!(s, "vertex {v} missing from spanning tree");
        }
    }
}