Skip to main content

rust_igraph/algorithms/games/
establishment.rs

1//! Establishment / sample-traits game (ALGO-GN-015).
2//!
3//! Counterpart of `igraph_establishment_game()` from
4//! `references/igraph/src/games/establishment.c:57`. Models a growing
5//! graph with vertex types: a single vertex is added at each step and
6//! tries to connect to `k` already-present vertices, with each candidate
7//! edge accepted independently with probability
8//! `pref_matrix[type_new][type_old]`.
9//!
10//! ## Algorithm
11//!
12//! 1. Build a cumulative distribution from `type_dist` (or the uniform
13//!    `[1, 1, …, 1]` distribution when `type_dist == None`).
14//! 2. Assign every vertex `v ∈ [0, nodes)` a type by drawing a uniform
15//!    sample in `[0, total)` and binary-searching the cumulative table.
16//! 3. For each `v ∈ [k, nodes)`, draw `k` distinct previous vertices
17//!    `[0, v)` via Floyd's distinct-sample. For each picked neighbour
18//!    `u`, accept the edge `(v, u)` with probability
19//!    `pref_matrix[type[v]][type[u]]` using a single uniform draw.
20//!
21//! ## Determinism
22//!
23//! Reproducible given the inputs and `seed` against the shared
24//! `SplitMix64` PRNG. The stream is **not** portable
25//! to upstream igraph's GLIBC RNG, so conformance assertions are
26//! structural (vertex/edge counts, type-vector range, support of the
27//! preference matrix) rather than bit-exact.
28//!
29//! ## References
30//!
31//! * G. Caldarelli, A. Capocci, P. De Los Rios, and M. A. Muñoz, *"Scale-free
32//!   networks from varying vertex intrinsic fitness"*, Phys. Rev. Lett.
33//!   **89**, 258702 (2002).
34
35#![allow(
36    unknown_lints,
37    clippy::cast_possible_truncation,
38    clippy::cast_precision_loss,
39    clippy::cast_sign_loss,
40    clippy::float_cmp,
41    clippy::too_many_arguments,
42    clippy::similar_names,
43    clippy::many_single_char_names,
44    clippy::needless_range_loop
45)]
46
47use std::collections::HashSet;
48
49use crate::core::rng::SplitMix64;
50use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
51
52fn validate(
53    nodes: u32,
54    types: u32,
55    type_dist: Option<&[f64]>,
56    pref_matrix: &[Vec<f64>],
57    directed: bool,
58) -> IgraphResult<()> {
59    if types < 1 {
60        return Err(IgraphError::InvalidArgument(
61            "The number of vertex types must be at least 1.".into(),
62        ));
63    }
64    let k = types as usize;
65    if let Some(td) = type_dist {
66        if td.len() != k {
67            return Err(IgraphError::InvalidArgument(format!(
68                "type_dist length ({}) does not match number of types ({k})",
69                td.len()
70            )));
71        }
72        for (i, &v) in td.iter().enumerate() {
73            if v.is_nan() {
74                return Err(IgraphError::InvalidArgument(format!(
75                    "type_dist[{i}] must not be NaN"
76                )));
77            }
78            if v < 0.0 {
79                return Err(IgraphError::InvalidArgument(format!(
80                    "type_dist[{i}] = {v} must be non-negative"
81                )));
82            }
83        }
84    }
85    if pref_matrix.len() != k {
86        return Err(IgraphError::InvalidArgument(format!(
87            "preference matrix has {} rows (expected {k})",
88            pref_matrix.len()
89        )));
90    }
91    for (i, row) in pref_matrix.iter().enumerate() {
92        if row.len() != k {
93            return Err(IgraphError::InvalidArgument(format!(
94                "preference matrix row {i} has length {} (expected {k})",
95                row.len()
96            )));
97        }
98        for (j, &p) in row.iter().enumerate() {
99            if p.is_nan() {
100                return Err(IgraphError::InvalidArgument(format!(
101                    "preference matrix entry [{i}][{j}] must not be NaN"
102                )));
103            }
104            if !(0.0..=1.0).contains(&p) {
105                return Err(IgraphError::InvalidArgument(format!(
106                    "preference matrix entry [{i}][{j}] = {p} must lie in [0, 1]"
107                )));
108            }
109        }
110    }
111    if !directed {
112        for (i, row_i) in pref_matrix.iter().enumerate() {
113            for (j, row_j) in pref_matrix.iter().enumerate().skip(i + 1) {
114                if row_i[j] != row_j[i] {
115                    return Err(IgraphError::InvalidArgument(format!(
116                        "preference matrix must be symmetric for undirected graphs: \
117                         pref[{i}][{j}] = {} but pref[{j}][{i}] = {}",
118                        row_i[j], row_j[i],
119                    )));
120                }
121            }
122        }
123    }
124    // nodes is u32, automatically non-negative; sentinel u32 ceiling
125    // is below `2^53`, so the f64-exact concern from preference.rs does
126    // not apply here.
127    let _ = nodes;
128    Ok(())
129}
130
131/// Smallest `k ≥ 1` such that `cumdist[k] > u`. Mirrors
132/// `igraph_vector_binsearch` for the cumulative-distribution case.
133fn cumdist_lookup(cumdist: &[f64], u: f64) -> usize {
134    let mut lo = 1usize;
135    let mut hi = cumdist.len();
136    while lo < hi {
137        let mid = lo + (hi - lo) / 2;
138        if cumdist[mid] > u {
139            hi = mid;
140        } else {
141            lo = mid + 1;
142        }
143    }
144    lo.min(cumdist.len() - 1).max(1)
145}
146
147/// Floyd distinct-sample of `m` integers from `[0, n)`. Result is sorted
148/// ascending. Caller must ensure `m <= n`.
149fn floyd_distinct_sample(rng: &mut SplitMix64, n: u32, m: u32) -> Vec<u32> {
150    debug_assert!(m <= n);
151    let cap = m as usize;
152    let mut chosen: HashSet<u32> = HashSet::with_capacity(cap);
153    let mut out: Vec<u32> = Vec::with_capacity(cap);
154    for j in (n - m)..n {
155        let span = (j as usize) + 1;
156        let t = rng.gen_index(span) as u32;
157        let pick = if chosen.insert(t) {
158            t
159        } else {
160            chosen.insert(j);
161            j
162        };
163        out.push(pick);
164    }
165    out.sort_unstable();
166    out
167}
168
169/// Generates a random graph with vertex types via the establishment
170/// model.
171///
172/// Vertices are added one-by-one. Once `i ≥ k`, a new vertex tries to
173/// connect to `k` previously added vertices uniformly at random; for
174/// each candidate, the edge is accepted with probability
175/// `pref_matrix[type[i]][type[j]]`. The result is a simple (no parallel
176/// edges, no self-loops by construction) graph whose density is shaped
177/// jointly by the type distribution and the preference matrix.
178///
179/// * `nodes` — number of vertices.
180/// * `types ≥ 1` — number of vertex types.
181/// * `k` — connections attempted per new vertex.
182/// * `type_dist` — categorical weights over types; `None` means uniform.
183///   Must be the same length as `types`, all entries non-negative,
184///   total mass strictly positive.
185/// * `pref_matrix` — `types × types` matrix of acceptance probabilities
186///   in `[0, 1]`. When `directed = false` it must be symmetric.
187/// * `directed` — generate a directed graph.
188/// * `seed` — `SplitMix64` seed.
189///
190/// Returns the generated [`Graph`] and the per-vertex type vector.
191///
192/// # Errors
193///
194/// Returns [`IgraphError::InvalidArgument`] when the preference matrix
195/// or type distribution is malformed (wrong size, NaN, out-of-range
196/// probability, asymmetric for an undirected graph) or when the type
197/// distribution is non-positive everywhere.
198///
199/// # Example
200///
201/// ```
202/// use rust_igraph::establishment_game;
203///
204/// // Two types, only-cross prefs ⇒ bipartite directed graph.
205/// let pref = vec![vec![0.0, 1.0], vec![1.0, 0.0]];
206/// let (g, types) =
207///     establishment_game(20, 2, 5, Some(&[1.0, 1.0]), &pref, true, 42).unwrap();
208/// assert_eq!(g.vcount(), 20);
209/// assert_eq!(types.len(), 20);
210/// assert!(types.iter().all(|&t| t < 2));
211/// ```
212pub fn establishment_game(
213    nodes: u32,
214    types: u32,
215    k: u32,
216    type_dist: Option<&[f64]>,
217    pref_matrix: &[Vec<f64>],
218    directed: bool,
219    seed: u64,
220) -> IgraphResult<(Graph, Vec<u32>)> {
221    validate(nodes, types, type_dist, pref_matrix, directed)?;
222
223    let n_types = types as usize;
224    let mut rng = SplitMix64::new(seed);
225
226    let mut cumdist = vec![0.0f64; n_types + 1];
227    if let Some(td) = type_dist {
228        for i in 0..n_types {
229            cumdist[i + 1] = cumdist[i] + td[i];
230        }
231    } else {
232        for i in 0..n_types {
233            cumdist[i + 1] = (i + 1) as f64;
234        }
235    }
236    let maxcum = cumdist[n_types];
237    if maxcum <= 0.0 {
238        return Err(IgraphError::InvalidArgument(
239            "type_dist must contain at least one positive value".into(),
240        ));
241    }
242
243    let mut node_types = vec![0u32; nodes as usize];
244    for v in 0..(nodes as usize) {
245        let u = rng.gen_unit() * maxcum;
246        let pos = cumdist_lookup(&cumdist, u);
247        let t = (pos - 1).min(n_types - 1);
248        node_types[v] = t as u32;
249    }
250
251    let mut edges: Vec<(VertexId, VertexId)> = Vec::new();
252    if k > 0 && nodes > k {
253        for i in k..nodes {
254            let type1 = node_types[i as usize] as usize;
255            let picks = floyd_distinct_sample(&mut rng, i, k);
256            for &j in &picks {
257                let type2 = node_types[j as usize] as usize;
258                let p = pref_matrix[type1][type2];
259                if p > 0.0 && rng.gen_unit() < p {
260                    edges.push((i, j));
261                }
262            }
263        }
264    }
265
266    let mut g = Graph::new(nodes, directed)?;
267    g.add_edges(edges)?;
268    Ok((g, node_types))
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274    use std::collections::HashSet as Set;
275
276    fn diag_pref(types: usize, p: f64) -> Vec<Vec<f64>> {
277        (0..types)
278            .map(|i| (0..types).map(|j| if i == j { p } else { 0.0 }).collect())
279            .collect()
280    }
281
282    fn full_pref(types: usize, p: f64) -> Vec<Vec<f64>> {
283        vec![vec![p; types]; types]
284    }
285
286    #[test]
287    fn nodes_zero_returns_empty_graph() {
288        let pref = full_pref(2, 0.5);
289        let (g, types) = establishment_game(0, 2, 5, None, &pref, false, 42).unwrap();
290        assert_eq!(g.vcount(), 0);
291        assert_eq!(g.ecount(), 0);
292        assert_eq!(types.len(), 0);
293        assert!(!g.is_directed());
294    }
295
296    #[test]
297    fn nodes_le_k_yields_no_edges() {
298        let pref = full_pref(2, 1.0);
299        let (g, types) = establishment_game(5, 2, 5, None, &pref, false, 7).unwrap();
300        assert_eq!(g.vcount(), 5);
301        assert_eq!(g.ecount(), 0);
302        assert_eq!(types.len(), 5);
303    }
304
305    #[test]
306    fn k_zero_yields_no_edges() {
307        let pref = full_pref(2, 1.0);
308        let (g, _) = establishment_game(20, 2, 0, None, &pref, false, 7).unwrap();
309        assert_eq!(g.ecount(), 0);
310    }
311
312    #[test]
313    fn zero_pref_means_no_edges() {
314        // Single vertex type, p_00 = 0 ⇒ growth never accepts.
315        let pref = vec![vec![0.0]];
316        let (g, types) = establishment_game(20, 1, 5, Some(&[1.0]), &pref, false, 11).unwrap();
317        assert_eq!(g.vcount(), 20);
318        assert_eq!(g.ecount(), 0);
319        assert!(types.iter().all(|&t| t == 0));
320    }
321
322    #[test]
323    fn full_pref_saturates_edges() {
324        // p == 1 across all type pairs ⇒ every Floyd pick becomes an edge.
325        let pref = full_pref(2, 1.0);
326        let (g, _) = establishment_game(30, 2, 5, None, &pref, false, 13).unwrap();
327        // Vertices [k, n) each contribute exactly k edges.
328        let n: usize = 30;
329        let k: usize = 5;
330        assert_eq!(g.ecount(), (n - k) * k);
331    }
332
333    #[test]
334    fn cross_only_pref_yields_bipartite_directed() {
335        // Two types, MOAT-like: only cross pairs accept.
336        let pref = vec![vec![0.0, 1.0], vec![1.0, 0.0]];
337        let (g, types) = establishment_game(40, 2, 5, Some(&[1.0, 1.0]), &pref, true, 19).unwrap();
338        assert_eq!(g.vcount(), 40);
339        assert!(g.is_directed());
340        // All edges must cross types.
341        let m = u32::try_from(g.ecount()).expect("ecount fits in u32");
342        for eid in 0..m {
343            let (u, v) = g.edge(eid).expect("edge id in bounds");
344            assert_ne!(types[u as usize], types[v as usize]);
345        }
346    }
347
348    #[test]
349    fn deterministic_for_fixed_seed() {
350        let pref = diag_pref(3, 0.4);
351        let (g1, t1) =
352            establishment_game(50, 3, 4, Some(&[1.0, 2.0, 3.0]), &pref, false, 0xABCDu64).unwrap();
353        let (g2, t2) =
354            establishment_game(50, 3, 4, Some(&[1.0, 2.0, 3.0]), &pref, false, 0xABCDu64).unwrap();
355        assert_eq!(g1.ecount(), g2.ecount());
356        assert_eq!(t1, t2);
357        let m = u32::try_from(g1.ecount()).expect("fits");
358        for eid in 0..m {
359            assert_eq!(g1.edge(eid).unwrap(), g2.edge(eid).unwrap());
360        }
361    }
362
363    #[test]
364    fn distinct_seeds_differ() {
365        let pref = full_pref(2, 0.4);
366        let (g1, _) = establishment_game(50, 2, 4, None, &pref, false, 1).unwrap();
367        let (g2, _) = establishment_game(50, 2, 4, None, &pref, false, 2).unwrap();
368        // Strong probability the edge counts or per-edge content differ.
369        let m1 = u32::try_from(g1.ecount()).unwrap();
370        let m2 = u32::try_from(g2.ecount()).unwrap();
371        let same_edges = m1 == m2 && {
372            let mut all_eq = true;
373            for eid in 0..m1 {
374                if g1.edge(eid).unwrap() != g2.edge(eid).unwrap() {
375                    all_eq = false;
376                    break;
377                }
378            }
379            all_eq
380        };
381        assert!(!same_edges);
382    }
383
384    #[test]
385    fn diagonal_pref_keeps_edges_within_types() {
386        // p > 0 only on the diagonal ⇒ every accepted edge connects
387        // same-type vertices.
388        let pref = diag_pref(3, 0.6);
389        let (g, types) =
390            establishment_game(80, 3, 4, Some(&[1.0, 1.0, 1.0]), &pref, false, 23).unwrap();
391        let m = u32::try_from(g.ecount()).unwrap();
392        for eid in 0..m {
393            let (u, v) = g.edge(eid).unwrap();
394            assert_eq!(types[u as usize], types[v as usize]);
395        }
396    }
397
398    #[test]
399    fn graph_is_simple_no_loops_no_multi() {
400        // Floyd's sample picks distinct neighbours, and edges only go
401        // i → j with j < i, so no self-loops and no parallel edges.
402        let pref = full_pref(3, 0.7);
403        let (g, _) = establishment_game(60, 3, 4, None, &pref, false, 31).unwrap();
404        let m = u32::try_from(g.ecount()).unwrap();
405        let mut seen: Set<(VertexId, VertexId)> = Set::with_capacity(m as usize);
406        for eid in 0..m {
407            let (a, b) = g.edge(eid).unwrap();
408            assert_ne!(a, b, "no self-loops");
409            let key = if a <= b { (a, b) } else { (b, a) };
410            assert!(seen.insert(key), "edge {key:?} appears twice");
411        }
412    }
413
414    #[test]
415    fn types_within_range_uniform() {
416        let pref = full_pref(4, 0.0);
417        let (_g, types) = establishment_game(200, 4, 1, None, &pref, false, 99).unwrap();
418        assert!(types.iter().all(|&t| t < 4));
419        // With uniform None and 200 nodes, all 4 types should appear.
420        let mut seen = [false; 4];
421        for &t in &types {
422            seen[t as usize] = true;
423        }
424        assert!(seen.iter().all(|&x| x));
425    }
426
427    #[test]
428    fn types_zero_rejected() {
429        let pref: Vec<Vec<f64>> = vec![];
430        let err = establishment_game(5, 0, 1, None, &pref, false, 1).unwrap_err();
431        assert!(matches!(err, IgraphError::InvalidArgument(_)));
432    }
433
434    #[test]
435    fn type_dist_wrong_length_rejected() {
436        let pref = full_pref(2, 0.1);
437        let err = establishment_game(10, 2, 1, Some(&[1.0]), &pref, false, 1).unwrap_err();
438        assert!(matches!(err, IgraphError::InvalidArgument(_)));
439    }
440
441    #[test]
442    fn type_dist_negative_rejected() {
443        let pref = full_pref(2, 0.1);
444        let err = establishment_game(10, 2, 1, Some(&[1.0, -0.1]), &pref, false, 1).unwrap_err();
445        assert!(matches!(err, IgraphError::InvalidArgument(_)));
446    }
447
448    #[test]
449    fn type_dist_all_zero_rejected() {
450        let pref = full_pref(2, 0.1);
451        let err = establishment_game(10, 2, 1, Some(&[0.0, 0.0]), &pref, false, 1).unwrap_err();
452        assert!(matches!(err, IgraphError::InvalidArgument(_)));
453    }
454
455    #[test]
456    fn pref_out_of_range_rejected() {
457        let pref = vec![vec![1.5, 0.1], vec![0.1, 0.5]];
458        let err = establishment_game(10, 2, 1, None, &pref, false, 1).unwrap_err();
459        assert!(matches!(err, IgraphError::InvalidArgument(_)));
460        let pref = vec![vec![-0.1, 0.1], vec![0.1, 0.5]];
461        let err = establishment_game(10, 2, 1, None, &pref, false, 1).unwrap_err();
462        assert!(matches!(err, IgraphError::InvalidArgument(_)));
463    }
464
465    #[test]
466    fn pref_nan_rejected() {
467        let pref = vec![vec![f64::NAN, 0.1], vec![0.1, 0.5]];
468        let err = establishment_game(10, 2, 1, None, &pref, false, 1).unwrap_err();
469        assert!(matches!(err, IgraphError::InvalidArgument(_)));
470    }
471
472    #[test]
473    fn pref_asymmetric_undirected_rejected() {
474        let pref = vec![vec![0.5, 0.1], vec![0.2, 0.5]];
475        let err = establishment_game(10, 2, 1, None, &pref, false, 1).unwrap_err();
476        assert!(matches!(err, IgraphError::InvalidArgument(_)));
477    }
478
479    #[test]
480    fn pref_asymmetric_directed_ok() {
481        let pref = vec![vec![0.5, 0.1], vec![0.2, 0.5]];
482        let (g, _) = establishment_game(10, 2, 1, None, &pref, true, 1).unwrap();
483        assert_eq!(g.vcount(), 10);
484        assert!(g.is_directed());
485    }
486
487    #[test]
488    fn pref_wrong_shape_rejected() {
489        let pref = vec![vec![0.5, 0.1, 0.1], vec![0.1, 0.5, 0.1]];
490        let err = establishment_game(10, 2, 1, None, &pref, true, 1).unwrap_err();
491        assert!(matches!(err, IgraphError::InvalidArgument(_)));
492    }
493
494    #[test]
495    fn directed_no_loops_construction_invariant() {
496        // i > j by construction for every edge, so no self-loops even
497        // when `directed = true` and pref allows them.
498        let pref = full_pref(2, 1.0);
499        let (g, _) = establishment_game(30, 2, 3, None, &pref, true, 5).unwrap();
500        let m = u32::try_from(g.ecount()).unwrap();
501        for eid in 0..m {
502            let (a, b) = g.edge(eid).unwrap();
503            assert_ne!(a, b);
504            assert!(a > b, "growth direction always points back in time");
505        }
506    }
507}
508
509#[cfg(all(test, feature = "proptest-harness"))]
510mod proptest_harness {
511    use super::*;
512    use proptest::prelude::*;
513
514    proptest! {
515        #![proptest_config(ProptestConfig::with_cases(40))]
516
517        #[test]
518        fn types_in_range(
519            seed in any::<u64>(),
520            n in 0u32..120,
521            t in 1u32..5,
522            k in 0u32..6,
523            directed in any::<bool>(),
524        ) {
525            let types_usize = t as usize;
526            let pref = vec![vec![0.5; types_usize]; types_usize];
527            let (g, types) = establishment_game(n, t, k, None, &pref, directed, seed).unwrap();
528            prop_assert_eq!(g.vcount(), n);
529            prop_assert_eq!(types.len() as u32, n);
530            for &tt in &types {
531                prop_assert!(tt < t);
532            }
533        }
534
535        #[test]
536        fn ecount_band(
537            seed in any::<u64>(),
538            n in 5u32..80,
539            t in 1u32..4,
540            k in 0u32..5,
541            p in 0.0f64..=1.0f64,
542        ) {
543            let types_usize = t as usize;
544            let pref = vec![vec![p; types_usize]; types_usize];
545            let (g, _) = establishment_game(n, t, k, None, &pref, false, seed).unwrap();
546            // 0 ≤ m ≤ (n - k) * k when n > k, else 0.
547            if n > k {
548                let bound = u64::from(n - k) * u64::from(k);
549                prop_assert!(g.ecount() as u64 <= bound);
550            } else {
551                prop_assert_eq!(g.ecount(), 0);
552            }
553        }
554
555        #[test]
556        fn deterministic(
557            seed in any::<u64>(),
558            n in 0u32..60,
559            t in 1u32..4,
560            k in 0u32..5,
561        ) {
562            let types_usize = t as usize;
563            let pref = vec![vec![0.3; types_usize]; types_usize];
564            let (g1, types1) = establishment_game(n, t, k, None, &pref, false, seed).unwrap();
565            let (g2, types2) = establishment_game(n, t, k, None, &pref, false, seed).unwrap();
566            prop_assert_eq!(types1, types2);
567            prop_assert_eq!(g1.ecount(), g2.ecount());
568            let m = u32::try_from(g1.ecount()).unwrap();
569            for eid in 0..m {
570                prop_assert_eq!(g1.edge(eid).unwrap(), g2.edge(eid).unwrap());
571            }
572        }
573
574        #[test]
575        fn diagonal_pref_stays_within_types(
576            seed in any::<u64>(),
577            n in 10u32..80,
578            t in 2u32..5,
579            k in 1u32..5,
580        ) {
581            let types_usize = t as usize;
582            let pref: Vec<Vec<f64>> = (0..types_usize)
583                .map(|i| {
584                    (0..types_usize)
585                        .map(|j| if i == j { 0.6 } else { 0.0 })
586                        .collect()
587                })
588                .collect();
589            let (g, types) =
590                establishment_game(n, t, k, None, &pref, false, seed).unwrap();
591            let m = u32::try_from(g.ecount()).unwrap();
592            for eid in 0..m {
593                let (u, v) = g.edge(eid).unwrap();
594                prop_assert_eq!(types[u as usize], types[v as usize]);
595            }
596        }
597
598        #[test]
599        fn cross_only_pref_yields_cross_edges(
600            seed in any::<u64>(),
601            n in 10u32..80,
602            k in 1u32..5,
603        ) {
604            // 2 types, only off-diagonal accepts.
605            let pref = vec![vec![0.0, 0.7], vec![0.7, 0.0]];
606            let (g, types) =
607                establishment_game(n, 2, k, Some(&[1.0, 1.0]), &pref, false, seed).unwrap();
608            let m = u32::try_from(g.ecount()).unwrap();
609            for eid in 0..m {
610                let (u, v) = g.edge(eid).unwrap();
611                prop_assert_ne!(types[u as usize], types[v as usize]);
612            }
613        }
614    }
615}