Skip to main content

rust_igraph/algorithms/games/
islands.rs

1//! Simple interconnected islands random-graph generator (ALGO-GN-007).
2//!
3//! Counterpart of `igraph_simple_interconnected_islands_game()` in
4//! `references/igraph/src/games/islands.c:55-176`.
5//!
6//! ## Model
7//!
8//! Generates `islands_n` Erdős–Rényi G(n, p) "islands" each of size
9//! `islands_size` and probability `islands_pin`, then for every
10//! unordered pair of islands draws exactly `n_inter` bipartite edges
11//! uniformly at random from the `islands_size × islands_size` possible
12//! cross-island pairs. The resulting graph is always undirected and
13//! simple — within an island the geometric-skip walk produces strictly
14//! increasing pair-indices, and across islands the bipartite slice
15//! cannot collide with intra-island edges (the index spaces are
16//! disjoint by construction).
17//!
18//! ## Validation
19//!
20//! * `islands_pin ∈ [0, 1]`. NaN / non-finite rejected.
21//! * `n_inter ≤ islands_size²` (max-saturated bipartite slice).
22//! * `islands_n · islands_size` must fit in `u32`.
23//!
24//! ## Algorithm cost
25//!
26//! Per island: Batagelj–Brandes geometric-skip is `O(islands_size + |E_i|)`.
27//! Per inter-island pair: Floyd distinct-sample is `O(n_inter)` expected.
28//! Total: `O(N + |E|)` where `N = islands_n · islands_size` and `|E|`
29//! aggregates both intra- and inter-island edges.
30//!
31//! ## Determinism
32//!
33//! Fully deterministic in
34//! `(islands_n, islands_size, islands_pin, n_inter, seed)` via
35//! `SplitMix64`.
36
37#![allow(
38    unknown_lints,
39    clippy::cast_possible_truncation,
40    clippy::cast_sign_loss,
41    clippy::cast_precision_loss,
42    clippy::manual_midpoint
43)]
44
45use std::collections::HashSet;
46
47use crate::core::rng::SplitMix64;
48use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
49
50fn validate(islands_n: u32, islands_size: u32, islands_pin: f64, n_inter: u32) -> IgraphResult<()> {
51    if !islands_pin.is_finite() || !(0.0..=1.0).contains(&islands_pin) {
52        return Err(IgraphError::InvalidArgument(format!(
53            "simple_interconnected_islands_game: islands_pin must satisfy 0 <= islands_pin <= 1 (got {islands_pin})"
54        )));
55    }
56    let size_sq = u64::from(islands_size) * u64::from(islands_size);
57    if u64::from(n_inter) > size_sq {
58        return Err(IgraphError::InvalidArgument(format!(
59            "simple_interconnected_islands_game: n_inter ({n_inter}) exceeds islands_size^2 ({size_sq})"
60        )));
61    }
62    let total_nodes = u64::from(islands_n) * u64::from(islands_size);
63    if total_nodes > u64::from(u32::MAX) {
64        return Err(IgraphError::InvalidArgument(format!(
65            "simple_interconnected_islands_game: islands_n * islands_size ({total_nodes}) overflows u32"
66        )));
67    }
68    Ok(())
69}
70
71/// Decode a unit-triangle pair-index `idx` into `(from, to)` with
72/// `from < to`, where indices `0, 1, 2, …` enumerate
73/// `(0,1), (0,2), (1,2), (0,3), …`. This matches the upstream
74/// `to = floor((sqrt(8x+1)+1)/2); from = x − to·(to−1)/2`.
75fn decode_triangle_pair(idx: u64) -> (u32, u32) {
76    let x = idx as f64;
77    let to = ((8.0 * x + 1.0).sqrt() + 1.0) / 2.0;
78    let to = to.floor() as u64;
79    let from = idx - (to * (to - 1)) / 2;
80    (from as u32, to as u32)
81}
82
83/// Floyd distinct-sample of `m` integers from `[0, n_pairs)`. Mirrors
84/// the [`crate::algorithms::games::erdos_renyi`] version but kept local
85/// so the islands module is self-contained.
86fn distinct_sample(rng: &mut SplitMix64, n_pairs: u64, m: u64) -> Vec<u64> {
87    debug_assert!(m <= n_pairs);
88    let cap = usize::try_from(m).unwrap_or(usize::MAX);
89    let mut chosen: HashSet<u64> = HashSet::with_capacity(cap);
90    let mut out: Vec<u64> = Vec::with_capacity(cap);
91    for j in (n_pairs - m)..n_pairs {
92        let span = j + 1;
93        let span_usize = usize::try_from(span).unwrap_or(usize::MAX);
94        let t = rng.gen_index(span_usize) as u64;
95        let pick = if chosen.insert(t) {
96            t
97        } else {
98            chosen.insert(j);
99            j
100        };
101        out.push(pick);
102    }
103    out.sort_unstable();
104    out
105}
106
107/// Generate a random graph made of `islands_n` interconnected
108/// Erdős–Rényi islands.
109///
110/// * `islands_n` — number of islands.
111/// * `islands_size` — vertex count per island (all islands have the
112///   same size).
113/// * `islands_pin ∈ [0, 1]` — within-island edge probability.
114/// * `n_inter` — number of bipartite edges drawn between each
115///   unordered pair of islands. Must satisfy
116///   `n_inter ≤ islands_size²`.
117/// * `seed` — seeds the internal `SplitMix64` PRNG.
118///
119/// The returned graph is always undirected and simple. Vertex
120/// labelling: island `is` occupies the contiguous range
121/// `[is · islands_size, (is + 1) · islands_size)`.
122///
123/// # Errors
124///
125/// Returns [`IgraphError::InvalidArgument`] if `islands_pin` is
126/// outside `[0, 1]`, if `n_inter > islands_size²`, or if the total
127/// vertex count `islands_n · islands_size` overflows `u32`.
128///
129/// # Examples
130///
131/// ```
132/// use rust_igraph::simple_interconnected_islands_game;
133///
134/// // 4 islands of size 25 with p = 0.4 within and 3 cross-island edges
135/// // between each pair → 100 vertices total.
136/// let g = simple_interconnected_islands_game(4, 25, 0.4, 3, 0xA15_1A4D).unwrap();
137/// assert_eq!(g.vcount(), 100);
138/// assert!(!g.is_directed());
139/// // Inter-island contribution alone is C(4, 2) · 3 = 18 edges; the
140/// // intra-island contribution adds many more.
141/// assert!(g.ecount() >= 18);
142/// ```
143pub fn simple_interconnected_islands_game(
144    islands_n: u32,
145    islands_size: u32,
146    islands_pin: f64,
147    n_inter: u32,
148    seed: u64,
149) -> IgraphResult<Graph> {
150    validate(islands_n, islands_size, islands_pin, n_inter)?;
151
152    let total_nodes = islands_n * islands_size;
153    if total_nodes == 0 {
154        return Graph::new(total_nodes, false);
155    }
156
157    let mut rng = SplitMix64::new(seed);
158    let mut edges: Vec<(VertexId, VertexId)> = Vec::new();
159
160    let intra_cap: u64 = if islands_size >= 2 {
161        u64::from(islands_size) * u64::from(islands_size - 1) / 2
162    } else {
163        0
164    };
165    let intra_cap_f = intra_cap as f64;
166
167    let bipartite_cap: u64 = u64::from(islands_size) * u64::from(islands_size);
168
169    for is in 0..islands_n {
170        let start = is * islands_size;
171
172        // Intra-island Batagelj–Brandes geometric-skip over the
173        // strict upper triangle [0, islands_size·(islands_size-1)/2).
174        if islands_pin > 0.0 && intra_cap > 0 {
175            if islands_pin >= 1.0 {
176                // Saturate: emit every upper-triangle edge.
177                for idx in 0..intra_cap {
178                    let (from, to) = decode_triangle_pair(idx);
179                    edges.push(((start + from) as VertexId, (start + to) as VertexId));
180                }
181            } else {
182                let mut last = rng.gen_geom(islands_pin);
183                while last < intra_cap_f {
184                    let idx = last.trunc() as u64;
185                    if idx >= intra_cap {
186                        break;
187                    }
188                    let (from, to) = decode_triangle_pair(idx);
189                    edges.push(((start + from) as VertexId, (start + to) as VertexId));
190                    last += rng.gen_geom(islands_pin);
191                    last += 1.0;
192                }
193            }
194        }
195
196        // Inter-island bipartite slices for every later island i' > is.
197        if n_inter > 0 && islands_size > 0 {
198            for ip in (is + 1)..islands_n {
199                let other_start = ip * islands_size;
200                let picks = distinct_sample(&mut rng, bipartite_cap, u64::from(n_inter));
201                for pick in picks {
202                    let from = (pick / u64::from(islands_size)) as u32;
203                    let to = (pick - u64::from(from) * u64::from(islands_size)) as u32;
204                    edges.push(((start + from) as VertexId, (other_start + to) as VertexId));
205                }
206            }
207        }
208    }
209
210    let mut g = Graph::new(total_nodes, false)?;
211    g.add_edges(edges)?;
212    Ok(g)
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218    use std::collections::HashSet;
219
220    fn canonical_edges(g: &Graph) -> HashSet<(VertexId, VertexId)> {
221        let n_edges = u32::try_from(g.ecount()).expect("ecount fits in u32 for tests");
222        (0..n_edges)
223            .map(|eid| {
224                let (a, b) = g.edge(eid).expect("edge id in bounds");
225                if a <= b { (a, b) } else { (b, a) }
226            })
227            .collect()
228    }
229
230    #[test]
231    fn empty_when_no_islands() {
232        let g = simple_interconnected_islands_game(0, 10, 0.5, 1, 1).unwrap();
233        assert_eq!(g.vcount(), 0);
234        assert_eq!(g.ecount(), 0);
235    }
236
237    #[test]
238    fn empty_when_island_size_zero() {
239        let g = simple_interconnected_islands_game(5, 0, 0.5, 0, 1).unwrap();
240        assert_eq!(g.vcount(), 0);
241        assert_eq!(g.ecount(), 0);
242    }
243
244    #[test]
245    fn single_island_zero_pin_zero_inter_is_edgeless() {
246        let g = simple_interconnected_islands_game(1, 20, 0.0, 0, 7).unwrap();
247        assert_eq!(g.vcount(), 20);
248        assert_eq!(g.ecount(), 0);
249    }
250
251    #[test]
252    fn single_island_pin_one_is_complete_clique() {
253        let g = simple_interconnected_islands_game(1, 10, 1.0, 0, 9).unwrap();
254        assert_eq!(g.vcount(), 10);
255        // K_10 has 10*9/2 = 45 edges.
256        assert_eq!(g.ecount(), 45);
257    }
258
259    #[test]
260    fn multiple_islands_pin_zero_only_inter_island() {
261        // 4 islands of size 5, p=0, n_inter=2 → C(4,2)·2 = 12 edges.
262        let g = simple_interconnected_islands_game(4, 5, 0.0, 2, 3).unwrap();
263        assert_eq!(g.vcount(), 20);
264        assert_eq!(g.ecount(), 12);
265    }
266
267    #[test]
268    fn saturated_bipartite_slice() {
269        // islands_size=3, n_inter = 9 = 3² (max).
270        let g = simple_interconnected_islands_game(2, 3, 0.0, 9, 5).unwrap();
271        assert_eq!(g.vcount(), 6);
272        assert_eq!(g.ecount(), 9);
273    }
274
275    #[test]
276    fn n_inter_above_cap_errors() {
277        let res = simple_interconnected_islands_game(2, 3, 0.0, 10, 5);
278        assert!(res.is_err());
279    }
280
281    #[test]
282    fn invalid_pin_rejected() {
283        assert!(simple_interconnected_islands_game(2, 3, -0.1, 0, 5).is_err());
284        assert!(simple_interconnected_islands_game(2, 3, 1.1, 0, 5).is_err());
285        assert!(simple_interconnected_islands_game(2, 3, f64::NAN, 0, 5).is_err());
286    }
287
288    #[test]
289    fn output_is_undirected_and_simple() {
290        let g = simple_interconnected_islands_game(3, 8, 0.4, 2, 11).unwrap();
291        assert!(!g.is_directed());
292        let n_edges = u32::try_from(g.ecount()).unwrap();
293        let mut seen: HashSet<(VertexId, VertexId)> = HashSet::new();
294        for eid in 0..n_edges {
295            let (a, b) = g.edge(eid).unwrap();
296            assert_ne!(a, b, "self-loop edge {a}-{b}");
297            let pair = if a <= b { (a, b) } else { (b, a) };
298            assert!(seen.insert(pair), "multi-edge {pair:?}");
299        }
300    }
301
302    #[test]
303    fn intra_edges_stay_within_island_boundaries() {
304        // Each emitted edge belongs to one island's intra range or to
305        // a single (island_a, island_b) bipartite slice — never spans
306        // three islands.
307        let islands_n = 4u32;
308        let islands_size = 10u32;
309        let g = simple_interconnected_islands_game(islands_n, islands_size, 0.3, 5, 13).unwrap();
310        let n_edges = u32::try_from(g.ecount()).unwrap();
311        for eid in 0..n_edges {
312            let (a, b) = g.edge(eid).unwrap();
313            let island_a = a / islands_size;
314            let island_b = b / islands_size;
315            assert!(island_a < islands_n);
316            assert!(island_b < islands_n);
317        }
318    }
319
320    #[test]
321    fn determinism_same_seed_same_graph() {
322        let a = simple_interconnected_islands_game(4, 20, 0.35, 3, 0xC0DE).unwrap();
323        let b = simple_interconnected_islands_game(4, 20, 0.35, 3, 0xC0DE).unwrap();
324        assert_eq!(a.ecount(), b.ecount());
325        assert_eq!(canonical_edges(&a), canonical_edges(&b));
326    }
327
328    #[test]
329    fn distinct_seeds_differ() {
330        let a = simple_interconnected_islands_game(4, 20, 0.35, 3, 1).unwrap();
331        let b = simple_interconnected_islands_game(4, 20, 0.35, 3, 2).unwrap();
332        assert_ne!(canonical_edges(&a), canonical_edges(&b));
333    }
334
335    #[test]
336    fn ecount_band_matches_expectation() {
337        // Within an island: E[edges] = p · n(n-1)/2.
338        // Inter-island: exact n_inter per pair, no variance.
339        let islands_n = 5u32;
340        let islands_size = 30u32;
341        let p = 0.2;
342        let n_inter = 4u32;
343        let expected_intra =
344            p * f64::from(islands_n) * f64::from(islands_size) * f64::from(islands_size - 1) / 2.0;
345        let expected_inter = f64::from(n_inter * islands_n * (islands_n - 1) / 2);
346        let expected = expected_intra + expected_inter;
347
348        let g =
349            simple_interconnected_islands_game(islands_n, islands_size, p, n_inter, 4242).unwrap();
350        let m = g.ecount() as f64;
351        let lo = 0.6 * expected;
352        let hi = 1.4 * expected;
353        assert!(m >= lo && m <= hi, "ecount {m} outside band [{lo}, {hi}]");
354    }
355
356    #[test]
357    fn triangle_pair_decoder_round_trips() {
358        // For n = 6, the 15 strict upper-triangle pairs should be
359        // enumerated in the canonical order matching the formula.
360        let expected: Vec<(u32, u32)> = vec![
361            (0, 1),
362            (0, 2),
363            (1, 2),
364            (0, 3),
365            (1, 3),
366            (2, 3),
367            (0, 4),
368            (1, 4),
369            (2, 4),
370            (3, 4),
371            (0, 5),
372            (1, 5),
373            (2, 5),
374            (3, 5),
375            (4, 5),
376        ];
377        for (idx, &want) in expected.iter().enumerate() {
378            assert_eq!(decode_triangle_pair(idx as u64), want);
379        }
380    }
381}