Skip to main content

rust_igraph/algorithms/games/
preference.rs

1//! Preference-based block model (ALGO-GN-014).
2//!
3//! Counterparts of `igraph_preference_game()` and
4//! `igraph_asymmetric_preference_game()` from
5//! `references/igraph/src/games/preference.c:83` and
6//! `references/igraph/src/games/preference.c:381`.
7//!
8//! The *symmetric* variant is essentially the non-growing version of
9//! the establishment game / a stochastic block model with a randomly
10//! assigned per-vertex type:
11//!
12//! 1. Each vertex draws a type from the categorical distribution
13//!    `type_dist`, or — when `fixed_sizes = true` — gets pinned to a
14//!    deterministic block-by-position assignment.
15//! 2. Per type-pair `(i, j)`, edges are sampled with probability
16//!    `pref_matrix[i][j]` over the rectangular `|V_i| × |V_j|` (or
17//!    triangular) slot space using Batagelj–Brandes geometric skip,
18//!    then mapped back to global vertex IDs through `vids_by_type`.
19//!
20//! The *asymmetric* variant gives every vertex an `(out_type, in_type)`
21//! pair drawn jointly from `type_dist_matrix`. Sampling is per
22//! `(out_type i, in_type j)` cell of the connection matrix; with
23//! `loops = false` the sampler subtracts the count of vertices that
24//! appear in both `vids_by_outtype[i]` and `vids_by_intype[j]` from the
25//! local `maxedges`, then uses an end-corner remap to reroute any
26//! sampled diagonal slot to a unique non-loop slot.
27//!
28//! Both variants share `PairShape::decode` and the geometric-skip
29//! pattern with the SBM module ([`crate::algorithms::games::sbm`]); the
30//! only addition is the indirection through `vids_by_type` (since
31//! same-type vertices are not contiguous in vertex-id space) and the
32//! intersection-based loop handling for the asymmetric path.
33//!
34//! ## Determinism
35//!
36//! Reproducible given the inputs and `seed` against the shared
37//! `SplitMix64` PRNG. The stream is **not** portable
38//! to upstream igraph's GLIBC RNG, so conformance assertions are
39//! structural (vertex/edge counts, type-vector range, `loops` respect,
40//! pref-matrix support) rather than bit-exact.
41//!
42//! ## References
43//!
44//! * V. Batagelj and U. Brandes, *"Efficient generation of large random
45//!   networks"*, Phys. Rev. E **71**, 036113 (2005) — the geometric-skip
46//!   sampler reused for each block-pair.
47//! * K. Faust and S. Wasserman, *"Blockmodels: Interpretation and
48//!   evaluation"*, Social Networks **14** (1992), 5–61.
49
50#![allow(
51    unknown_lints,
52    clippy::cast_possible_truncation,
53    clippy::cast_precision_loss,
54    clippy::cast_sign_loss,
55    clippy::float_cmp,
56    clippy::too_many_arguments,
57    clippy::similar_names,
58    clippy::many_single_char_names,
59    clippy::needless_range_loop
60)]
61
62use crate::algorithms::games::sbm::PairShape;
63use crate::core::rng::SplitMix64;
64use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
65
66/// Largest vertex count we accept; bounded by the largest `f64`-exact
67/// integer (`2^53 - 1`), which is the `IGRAPH_MAX_EXACT_REAL` ceiling
68/// upstream uses for the `maxedges` overflow check.
69const MAX_NODES: u64 = 1u64 << 53;
70
71// -------------------------------------------------------------------
72// Shared shape selector (mirrors sbm::pair_shape but local for clarity)
73// -------------------------------------------------------------------
74
75fn pair_shape(directed: bool, loops: bool, on_diagonal: bool) -> PairShape {
76    match (directed, loops, on_diagonal) {
77        (true, false, true) => PairShape::RectNoDiag,
78        (false, true, true) => PairShape::TriInclDiag,
79        (false, false, true) => PairShape::TriExclDiag,
80        _ => PairShape::Rect,
81    }
82}
83
84fn shape_maxedges(shape: PairShape, fromsize: u32, tosize: u32) -> u64 {
85    match shape {
86        PairShape::Rect => u64::from(fromsize) * u64::from(tosize),
87        PairShape::RectNoDiag => {
88            let fs = u64::from(fromsize);
89            fs * fs.saturating_sub(1)
90        }
91        PairShape::TriInclDiag => {
92            let fs = u64::from(fromsize);
93            fs * (fs + 1) / 2
94        }
95        PairShape::TriExclDiag => {
96            let fs = u64::from(fromsize);
97            fs * fs.saturating_sub(1) / 2
98        }
99    }
100}
101
102// -------------------------------------------------------------------
103// Symmetric: igraph_preference_game
104// -------------------------------------------------------------------
105
106fn validate_symmetric(
107    nodes: u32,
108    types: u32,
109    type_dist: Option<&[f64]>,
110    fixed_sizes: bool,
111    pref_matrix: &[Vec<f64>],
112    directed: bool,
113) -> IgraphResult<()> {
114    if types < 1 {
115        return Err(IgraphError::InvalidArgument(
116            "The number of vertex types must be at least 1.".into(),
117        ));
118    }
119    let k = types as usize;
120    if let Some(td) = type_dist {
121        if td.len() != k {
122            return Err(IgraphError::InvalidArgument(format!(
123                "type_dist length ({}) does not match number of types ({k})",
124                td.len()
125            )));
126        }
127        for (i, &v) in td.iter().enumerate() {
128            if v.is_nan() {
129                return Err(IgraphError::InvalidArgument(format!(
130                    "type_dist[{i}] must not be NaN"
131                )));
132            }
133            if v < 0.0 {
134                return Err(IgraphError::InvalidArgument(format!(
135                    "type_dist[{i}] = {v} must be non-negative"
136                )));
137            }
138        }
139    }
140    if pref_matrix.len() != k {
141        return Err(IgraphError::InvalidArgument(format!(
142            "preference matrix has {} rows (expected {k})",
143            pref_matrix.len()
144        )));
145    }
146    for (i, row) in pref_matrix.iter().enumerate() {
147        if row.len() != k {
148            return Err(IgraphError::InvalidArgument(format!(
149                "preference matrix row {i} has length {} (expected {k})",
150                row.len()
151            )));
152        }
153        for (j, &p) in row.iter().enumerate() {
154            if p.is_nan() {
155                return Err(IgraphError::InvalidArgument(format!(
156                    "preference matrix entry [{i}][{j}] must not be NaN"
157                )));
158            }
159            if !(0.0..=1.0).contains(&p) {
160                return Err(IgraphError::InvalidArgument(format!(
161                    "preference matrix entry [{i}][{j}] = {p} must lie in [0, 1]"
162                )));
163            }
164        }
165    }
166    if !directed {
167        for (i, row_i) in pref_matrix.iter().enumerate() {
168            for (j, row_j) in pref_matrix.iter().enumerate().skip(i + 1) {
169                let pij = row_i[j];
170                let pji = row_j[i];
171                if pij != pji {
172                    return Err(IgraphError::InvalidArgument(format!(
173                        "preference matrix must be symmetric for undirected graphs: \
174                         pref[{i}][{j}] = {pij} but pref[{j}][{i}] = {pji}"
175                    )));
176                }
177            }
178        }
179    }
180    if fixed_sizes {
181        if let Some(td) = type_dist {
182            // Sum must equal nodes (treated as integer counts).
183            let mut sum: u64 = 0;
184            for (i, &v) in td.iter().enumerate() {
185                if v < 0.0 || !v.is_finite() {
186                    return Err(IgraphError::InvalidArgument(format!(
187                        "type_dist[{i}] = {v} must be a non-negative finite count when fixed_sizes is set"
188                    )));
189                }
190                let c = v as u64;
191                #[allow(clippy::float_cmp)]
192                if (c as f64) != v {
193                    return Err(IgraphError::InvalidArgument(format!(
194                        "type_dist[{i}] = {v} must be an integer count when fixed_sizes is set"
195                    )));
196                }
197                sum = sum.checked_add(c).ok_or_else(|| {
198                    IgraphError::InvalidArgument("sum of type_dist overflows u64".into())
199                })?;
200            }
201            if sum != u64::from(nodes) {
202                return Err(IgraphError::InvalidArgument(format!(
203                    "fixed_sizes requires sum(type_dist) = nodes; got {sum} vs {nodes}"
204                )));
205            }
206        }
207    }
208    if u64::from(nodes) > MAX_NODES {
209        return Err(IgraphError::InvalidArgument(format!(
210            "nodes = {nodes} exceeds f64-exact ceiling 2^53"
211        )));
212    }
213    Ok(())
214}
215
216/// Smallest `k` such that `cumdist[k] > u`. Mirrors
217/// `igraph_vector_binsearch` for the cumulative-distribution case.
218fn cumdist_lookup(cumdist: &[f64], u: f64) -> usize {
219    // cumdist is non-decreasing with cumdist[0] == 0. We want the
220    // smallest k >= 1 such that u < cumdist[k]. Equivalent to
221    // upper_bound (first k with cumdist[k] > u).
222    let mut lo = 1usize;
223    let mut hi = cumdist.len();
224    while lo < hi {
225        let mid = lo + (hi - lo) / 2;
226        if cumdist[mid] > u {
227            hi = mid;
228        } else {
229            lo = mid + 1;
230        }
231    }
232    lo.min(cumdist.len() - 1).max(1)
233}
234
235fn assign_types_random(
236    rng: &mut SplitMix64,
237    nodes: u32,
238    types: u32,
239    type_dist: Option<&[f64]>,
240    node_types: &mut [u32],
241    vids_by_type: &mut [Vec<u32>],
242) -> IgraphResult<()> {
243    let k = types as usize;
244    let mut cumdist = vec![0.0f64; k + 1];
245    if let Some(td) = type_dist {
246        for i in 0..k {
247            cumdist[i + 1] = cumdist[i] + td[i];
248        }
249    } else {
250        for i in 0..k {
251            cumdist[i + 1] = (i + 1) as f64;
252        }
253    }
254    let maxcum = cumdist[k];
255    if maxcum <= 0.0 {
256        return Err(IgraphError::InvalidArgument(
257            "type_dist must have a positive total mass".into(),
258        ));
259    }
260    for v in 0..nodes {
261        let u = rng.gen_unit() * maxcum;
262        let pos = cumdist_lookup(&cumdist, u);
263        let t = (pos - 1).min(k - 1);
264        node_types[v as usize] = t as u32;
265        vids_by_type[t].push(v);
266    }
267    Ok(())
268}
269
270fn assign_types_fixed(
271    nodes: u32,
272    types: u32,
273    type_dist: Option<&[f64]>,
274    node_types: &mut [u32],
275    vids_by_type: &mut [Vec<u32>],
276) {
277    let mut an: u32 = 0;
278    if let Some(td) = type_dist {
279        for (i, &cnt_f) in td.iter().enumerate() {
280            let cnt = cnt_f as u32;
281            for _ in 0..cnt {
282                if an >= nodes {
283                    break;
284                }
285                node_types[an as usize] = i as u32;
286                vids_by_type[i].push(an);
287                an += 1;
288            }
289        }
290    } else {
291        let size_one = nodes / types;
292        let extra = nodes - size_one * types;
293        for (i, slot) in vids_by_type.iter_mut().enumerate() {
294            for _ in 0..size_one {
295                node_types[an as usize] = i as u32;
296                slot.push(an);
297                an += 1;
298            }
299            if (i as u32) < extra {
300                node_types[an as usize] = i as u32;
301                slot.push(an);
302                an += 1;
303            }
304        }
305    }
306}
307
308/// Geometric-skip sampler over a single type-pair block. Decodes each
309/// sampled index through `PairShape` and emits global edges via the
310/// `v1` / `v2` indirection arrays.
311fn sample_block_indirect(
312    rng: &mut SplitMix64,
313    edges: &mut Vec<(VertexId, VertexId)>,
314    v1: &[u32],
315    v2: &[u32],
316    shape: PairShape,
317    p: f64,
318    maxedges: u64,
319) {
320    if maxedges == 0 || p <= 0.0 {
321        return;
322    }
323    let v1size = v1.len() as u32;
324    let max_f = maxedges as f64;
325    let mut last = rng.gen_geom(p);
326    while last < max_f {
327        let idx = last.trunc() as u64;
328        if idx >= maxedges {
329            break;
330        }
331        let (vfrom, vto) = shape.decode(idx, v1size);
332        edges.push((v1[vfrom as usize], v2[vto as usize]));
333        last += rng.gen_geom(p);
334        last += 1.0;
335    }
336}
337
338/// Generate a random graph from a **type-aware preference (block) model**.
339///
340/// Each vertex is assigned to one of `types` types (uniformly, by
341/// `type_dist`, or pinned by `fixed_sizes`); each ordered/unordered
342/// vertex pair `(u, v)` is then connected with probability
343/// `pref_matrix[type(u)][type(v)]`.
344///
345/// * `nodes` — number of vertices in the graph.
346/// * `types` — number of vertex types (≥ 1).
347/// * `type_dist` — categorical weights over types; `None` ⇒ uniform.
348/// * `fixed_sizes` — when `true`, types are assigned by deterministic
349///   block-by-position rather than sampled. With `type_dist = Some(td)`
350///   the block sizes are `td[i]` (must be integer and sum to `nodes`).
351///   With `type_dist = None` the function tries to make groups of
352///   equal size.
353/// * `pref_matrix` — `types × types` connection probabilities in
354///   `[0, 1]`. Must be symmetric when `directed = false`.
355/// * `directed` — generate a directed graph.
356/// * `loops` — allow self-loop edges.
357/// * `seed` — initialises an internal `SplitMix64` PRNG.
358///
359/// # Returns
360///
361/// `(graph, node_types)` where `node_types[i]` is the type of vertex
362/// `i` in `0..types`.
363///
364/// # Errors
365///
366/// Returns [`IgraphError::InvalidArgument`] when:
367/// * `types < 1`,
368/// * `type_dist` length disagrees with `types`, contains NaN or a
369///   negative value,
370/// * `pref_matrix` is non-square or has the wrong dimensions,
371/// * any pref entry is NaN or outside `[0, 1]`,
372/// * `pref_matrix` is not symmetric when `!directed`,
373/// * `fixed_sizes` and `type_dist` are both supplied but the entries
374///   are not integer or do not sum to `nodes`,
375/// * `nodes` exceeds the `2^53` `f64`-exact ceiling.
376///
377/// # Examples
378///
379/// ```
380/// use rust_igraph::preference_game;
381///
382/// // Three types, equal mixture; dense within types, sparse between.
383/// let pref = vec![
384///     vec![0.20, 0.02, 0.02],
385///     vec![0.02, 0.20, 0.02],
386///     vec![0.02, 0.02, 0.20],
387/// ];
388/// let (g, types) = preference_game(60, 3, None, false, &pref, false, false, 42).unwrap();
389/// assert_eq!(g.vcount(), 60);
390/// assert_eq!(types.len(), 60);
391/// assert!(types.iter().all(|&t| t < 3));
392/// ```
393pub fn preference_game(
394    nodes: u32,
395    types: u32,
396    type_dist: Option<&[f64]>,
397    fixed_sizes: bool,
398    pref_matrix: &[Vec<f64>],
399    directed: bool,
400    loops: bool,
401    seed: u64,
402) -> IgraphResult<(Graph, Vec<u32>)> {
403    validate_symmetric(nodes, types, type_dist, fixed_sizes, pref_matrix, directed)?;
404
405    let k = types as usize;
406    let mut node_types = vec![0u32; nodes as usize];
407    let mut vids_by_type: Vec<Vec<u32>> = vec![Vec::new(); k];
408    let mut rng = SplitMix64::new(seed);
409
410    if fixed_sizes {
411        assign_types_fixed(nodes, types, type_dist, &mut node_types, &mut vids_by_type);
412    } else {
413        assign_types_random(
414            &mut rng,
415            nodes,
416            types,
417            type_dist,
418            &mut node_types,
419            &mut vids_by_type,
420        )?;
421    }
422
423    let mut edges: Vec<(VertexId, VertexId)> = Vec::new();
424    for i in 0..k {
425        for j in 0..k {
426            if !directed && i > j {
427                continue;
428            }
429            let p = pref_matrix[i][j];
430            if p <= 0.0 {
431                continue;
432            }
433            let v1 = &vids_by_type[i];
434            let v2 = &vids_by_type[j];
435            let v1s = v1.len() as u32;
436            let v2s = v2.len() as u32;
437            if v1s == 0 || v2s == 0 {
438                continue;
439            }
440            let on_diag = i == j;
441            let shape = pair_shape(directed, loops, on_diag);
442            let maxedges = shape_maxedges(shape, v1s, v2s);
443            if maxedges as f64 > MAX_NODES as f64 {
444                return Err(IgraphError::InvalidArgument(
445                    "block maxedges overflows f64-exact representation".into(),
446                ));
447            }
448            sample_block_indirect(&mut rng, &mut edges, v1, v2, shape, p, maxedges);
449        }
450    }
451
452    let mut g = Graph::new(nodes, directed)?;
453    g.add_edges(edges)?;
454    Ok((g, node_types))
455}
456
457// -------------------------------------------------------------------
458// Asymmetric: igraph_asymmetric_preference_game
459// -------------------------------------------------------------------
460
461fn validate_asymmetric(
462    nodes: u32,
463    no_out_types: u32,
464    no_in_types: u32,
465    type_dist_matrix: Option<&[Vec<f64>]>,
466    pref_matrix: &[Vec<f64>],
467) -> IgraphResult<()> {
468    if no_out_types < 1 {
469        return Err(IgraphError::InvalidArgument(
470            "no_out_types must be at least 1".into(),
471        ));
472    }
473    if no_in_types < 1 {
474        return Err(IgraphError::InvalidArgument(
475            "no_in_types must be at least 1".into(),
476        ));
477    }
478    let no_out = no_out_types as usize;
479    let no_in = no_in_types as usize;
480    if let Some(tdm) = type_dist_matrix {
481        if tdm.len() != no_out {
482            return Err(IgraphError::InvalidArgument(format!(
483                "type_dist_matrix has {} rows (expected {no_out})",
484                tdm.len()
485            )));
486        }
487        for (i, row) in tdm.iter().enumerate() {
488            if row.len() != no_in {
489                return Err(IgraphError::InvalidArgument(format!(
490                    "type_dist_matrix row {i} has length {} (expected {no_in})",
491                    row.len()
492                )));
493            }
494            for (j, &v) in row.iter().enumerate() {
495                if v.is_nan() {
496                    return Err(IgraphError::InvalidArgument(format!(
497                        "type_dist_matrix[{i}][{j}] must not be NaN"
498                    )));
499                }
500                if v < 0.0 {
501                    return Err(IgraphError::InvalidArgument(format!(
502                        "type_dist_matrix[{i}][{j}] = {v} must be non-negative"
503                    )));
504                }
505            }
506        }
507    }
508    if pref_matrix.len() != no_out {
509        return Err(IgraphError::InvalidArgument(format!(
510            "preference matrix has {} rows (expected {no_out})",
511            pref_matrix.len()
512        )));
513    }
514    for (i, row) in pref_matrix.iter().enumerate() {
515        if row.len() != no_in {
516            return Err(IgraphError::InvalidArgument(format!(
517                "preference matrix row {i} has length {} (expected {no_in})",
518                row.len()
519            )));
520        }
521        for (j, &p) in row.iter().enumerate() {
522            if p.is_nan() {
523                return Err(IgraphError::InvalidArgument(format!(
524                    "preference matrix entry [{i}][{j}] must not be NaN"
525                )));
526            }
527            if !(0.0..=1.0).contains(&p) {
528                return Err(IgraphError::InvalidArgument(format!(
529                    "preference matrix entry [{i}][{j}] = {p} must lie in [0, 1]"
530                )));
531            }
532        }
533    }
534    if u64::from(nodes) > MAX_NODES {
535        return Err(IgraphError::InvalidArgument(format!(
536            "nodes = {nodes} exceeds f64-exact ceiling 2^53"
537        )));
538    }
539    Ok(())
540}
541
542/// Sorted-merge intersection of two ascending `&[u32]` slices.
543fn sorted_intersection(a: &[u32], b: &[u32]) -> Vec<u32> {
544    let mut out = Vec::new();
545    let (mut i, mut j) = (0usize, 0usize);
546    while i < a.len() && j < b.len() {
547        match a[i].cmp(&b[j]) {
548            std::cmp::Ordering::Less => i += 1,
549            std::cmp::Ordering::Greater => j += 1,
550            std::cmp::Ordering::Equal => {
551                out.push(a[i]);
552                i += 1;
553                j += 1;
554            }
555        }
556    }
557    out
558}
559
560/// Position of `target` in the sorted slice `xs`, or `None`.
561fn binsearch_pos(xs: &[u32], target: u32) -> Option<usize> {
562    xs.binary_search(&target).ok()
563}
564
565/// Sample one asymmetric type-pair block, emitting edges into `edges`.
566/// Uses the upstream end-corner remap to reroute loop slots when
567/// `loops = false` and `intersect` is non-empty.
568fn sample_asym_pair(
569    rng: &mut SplitMix64,
570    edges: &mut Vec<(VertexId, VertexId)>,
571    v1: &[u32],
572    v2: &[u32],
573    intersect: &[u32],
574    p: f64,
575    maxedges: u64,
576    loops: bool,
577) {
578    if maxedges == 0 || p <= 0.0 {
579        return;
580    }
581    let v1size = v1.len() as u32;
582    let v2size = v2.len() as u32;
583    let max_f = maxedges as f64;
584    let intersect_count = intersect.len();
585
586    let mut last = rng.gen_geom(p);
587    while last < max_f {
588        let idx = last.trunc() as u64;
589        if idx >= maxedges {
590            break;
591        }
592        let to = idx / u64::from(v1size);
593        let from = idx - to * u64::from(v1size);
594        let (mut vfrom, mut vto) = (from as u32, to as u32);
595
596        if !loops && intersect_count > 0 && v1[vfrom as usize] == v2[vto as usize] {
597            // Remap this loop slot to a non-loop slot in the rightmost
598            // column. The k-th loop in `intersect` (0-indexed) is
599            // mapped to a unique non-loop slot at column v2_size - 1.
600            vto = v2size - 1;
601            let loop_vid = v1[vfrom as usize];
602            let mut c = binsearch_pos(intersect, loop_vid).unwrap_or(intersect_count);
603            let mut from_idx: i64 = i64::from(v1size) - 1;
604            // Skip the rightmost-column slot if it would itself be a loop.
605            if from_idx >= 0 && v1[from_idx as usize] == v2[vto as usize] {
606                from_idx -= 1;
607            }
608            while c > 0 && from_idx >= 0 {
609                c -= 1;
610                from_idx -= 1;
611                if from_idx >= 0 && v1[from_idx as usize] == v2[vto as usize] {
612                    from_idx -= 1;
613                }
614            }
615            // After the walk, from_idx should point to a non-loop slot.
616            // If we ran off the end (no remaining valid slot), skip
617            // emitting this edge — the index is at the cap and the next
618            // sample will bring `last >= max_f`.
619            if from_idx < 0 {
620                last += rng.gen_geom(p);
621                last += 1.0;
622                continue;
623            }
624            vfrom = from_idx as u32;
625        }
626        edges.push((v1[vfrom as usize], v2[vto as usize]));
627        last += rng.gen_geom(p);
628        last += 1.0;
629    }
630}
631
632/// Generate a random directed graph with **asymmetric vertex types and
633/// connection preferences**.
634///
635/// Each vertex is assigned an `(out_type, in_type)` pair drawn jointly
636/// from `type_dist_matrix` (or uniformly when `None`). A directed edge
637/// `u → v` is then sampled with probability
638/// `pref_matrix[out_type(u)][in_type(v)]`.
639///
640/// * `nodes` — number of vertices.
641/// * `no_out_types`, `no_in_types` — number of out- and in-types
642///   (≥ 1).
643/// * `type_dist_matrix` — `out_types × in_types` joint weights;
644///   `None` ⇒ uniform.
645/// * `pref_matrix` — `out_types × in_types` connection probabilities
646///   in `[0, 1]`.
647/// * `loops` — allow self-loop edges.
648/// * `seed` — initialises an internal `SplitMix64` PRNG.
649///
650/// The resulting graph is **always directed**.
651///
652/// # Returns
653///
654/// `(graph, node_type_out, node_type_in)` — the per-vertex out- and
655/// in-type assignments.
656///
657/// # Errors
658///
659/// Returns [`IgraphError::InvalidArgument`] when:
660/// * `no_out_types < 1` or `no_in_types < 1`,
661/// * `type_dist_matrix` has wrong dimensions, NaN, or negative entries,
662/// * `pref_matrix` has wrong dimensions, NaN, or entries outside `[0, 1]`,
663/// * `nodes` exceeds the `2^53` `f64`-exact ceiling.
664///
665/// # Examples
666///
667/// ```
668/// use rust_igraph::asymmetric_preference_game;
669///
670/// // 2 out-types × 2 in-types, uniform mixing, p ≈ 0.1 across the board.
671/// let pref = vec![vec![0.1, 0.1], vec![0.1, 0.1]];
672/// let (g, out, inp) =
673///     asymmetric_preference_game(40, 2, 2, None, &pref, true, 42).unwrap();
674/// assert_eq!(g.vcount(), 40);
675/// assert!(g.is_directed());
676/// assert!(out.iter().all(|&t| t < 2));
677/// assert!(inp.iter().all(|&t| t < 2));
678/// ```
679pub fn asymmetric_preference_game(
680    nodes: u32,
681    no_out_types: u32,
682    no_in_types: u32,
683    type_dist_matrix: Option<&[Vec<f64>]>,
684    pref_matrix: &[Vec<f64>],
685    loops: bool,
686    seed: u64,
687) -> IgraphResult<(Graph, Vec<u32>, Vec<u32>)> {
688    validate_asymmetric(
689        nodes,
690        no_out_types,
691        no_in_types,
692        type_dist_matrix,
693        pref_matrix,
694    )?;
695
696    let no_out = no_out_types as usize;
697    let no_in = no_in_types as usize;
698    let mut node_out = vec![0u32; nodes as usize];
699    let mut node_in = vec![0u32; nodes as usize];
700    let mut vids_by_outtype: Vec<Vec<u32>> = vec![Vec::new(); no_out];
701    let mut vids_by_intype: Vec<Vec<u32>> = vec![Vec::new(); no_in];
702    let mut rng = SplitMix64::new(seed);
703
704    // Joint cumulative distribution: cumdist[k+1] = cumdist[k] +
705    // matrix[out, in]; iteration order matches upstream so the decoded
706    // type cell is `(cell % no_out_types, cell / no_out_types)` for
707    // (out_type, in_type).
708    let total_cells = no_out * no_in;
709    let mut cumdist = vec![0.0f64; total_cells + 1];
710    let mut k = 0usize;
711    if let Some(tdm) = type_dist_matrix {
712        // Iteration order matches upstream so the decoded type cell is
713        // (cell % no_out_types, cell / no_out_types) for (out, in).
714        for j_in in 0..no_in {
715            for (i_out, row) in tdm.iter().enumerate().take(no_out) {
716                cumdist[k + 1] = cumdist[k] + row[j_in];
717                k += 1;
718                let _ = i_out; // index used implicitly by enumerate
719            }
720        }
721    } else {
722        for i in 0..total_cells {
723            cumdist[i + 1] = (i + 1) as f64;
724        }
725    }
726    let maxcum = cumdist[total_cells];
727    if maxcum <= 0.0 {
728        return Err(IgraphError::InvalidArgument(
729            "type_dist_matrix must have a positive total mass".into(),
730        ));
731    }
732
733    for v in 0..nodes {
734        let u = rng.gen_unit() * maxcum;
735        let pos = cumdist_lookup(&cumdist, u);
736        let cell = (pos - 1).min(total_cells - 1);
737        let out_t = (cell % no_out) as u32;
738        let in_t = (cell / no_out) as u32;
739        node_out[v as usize] = out_t;
740        node_in[v as usize] = in_t;
741        vids_by_outtype[out_t as usize].push(v);
742        vids_by_intype[in_t as usize].push(v);
743    }
744
745    let mut edges: Vec<(VertexId, VertexId)> = Vec::new();
746    for i in 0..no_out {
747        for j in 0..no_in {
748            let p = pref_matrix[i][j];
749            if p <= 0.0 {
750                continue;
751            }
752            let v1 = &vids_by_outtype[i];
753            let v2 = &vids_by_intype[j];
754            let v1s = v1.len() as u32;
755            let v2s = v2.len() as u32;
756            if v1s == 0 || v2s == 0 {
757                continue;
758            }
759            let mut maxedges: u64 = u64::from(v1s) * u64::from(v2s);
760            if maxedges as f64 > MAX_NODES as f64 {
761                return Err(IgraphError::InvalidArgument(
762                    "asymmetric block maxedges overflows f64-exact representation".into(),
763                ));
764            }
765            let intersect = if loops {
766                Vec::new()
767            } else {
768                sorted_intersection(v1, v2)
769            };
770            if !loops {
771                maxedges -= intersect.len() as u64;
772            }
773            sample_asym_pair(&mut rng, &mut edges, v1, v2, &intersect, p, maxedges, loops);
774        }
775    }
776
777    let mut g = Graph::new(nodes, true)?;
778    g.add_edges(edges)?;
779    Ok((g, node_out, node_in))
780}
781
782// ===================================================================
783// Tests
784// ===================================================================
785
786#[cfg(test)]
787mod tests {
788    use super::*;
789
790    fn diag_pref_sym(k: usize, p: f64) -> Vec<Vec<f64>> {
791        let mut m = vec![vec![0.0; k]; k];
792        for (i, row) in m.iter_mut().enumerate() {
793            row[i] = p;
794        }
795        m
796    }
797
798    fn uniform_pref_sym(k: usize, p: f64) -> Vec<Vec<f64>> {
799        vec![vec![p; k]; k]
800    }
801
802    // -- symmetric: validation ----------------------------------------
803
804    #[test]
805    fn rejects_zero_types() {
806        let res = preference_game(10, 0, None, false, &[], false, false, 0);
807        assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
808    }
809
810    #[test]
811    fn rejects_pref_wrong_rows() {
812        let pref = vec![vec![0.1, 0.1]];
813        let res = preference_game(10, 2, None, false, &pref, false, false, 0);
814        assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
815    }
816
817    #[test]
818    fn rejects_pref_wrong_cols() {
819        let pref = vec![vec![0.1], vec![0.1, 0.1]];
820        let res = preference_game(10, 2, None, false, &pref, false, false, 0);
821        assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
822    }
823
824    #[test]
825    fn rejects_nan_pref() {
826        let pref = vec![vec![f64::NAN, 0.1], vec![0.1, 0.1]];
827        let res = preference_game(10, 2, None, false, &pref, false, false, 0);
828        assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
829    }
830
831    #[test]
832    fn rejects_pref_above_one() {
833        let pref = vec![vec![0.5, 1.5], vec![1.5, 0.5]];
834        let res = preference_game(10, 2, None, false, &pref, false, false, 0);
835        assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
836    }
837
838    #[test]
839    fn rejects_negative_pref() {
840        let pref = vec![vec![0.5, -0.1], vec![-0.1, 0.5]];
841        let res = preference_game(10, 2, None, false, &pref, false, false, 0);
842        assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
843    }
844
845    #[test]
846    fn rejects_asymmetric_pref_when_undirected() {
847        let pref = vec![vec![0.1, 0.2], vec![0.3, 0.1]];
848        let res = preference_game(10, 2, None, false, &pref, false, false, 0);
849        assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
850    }
851
852    #[test]
853    fn accepts_asymmetric_pref_when_directed() {
854        let pref = vec![vec![0.1, 0.2], vec![0.3, 0.1]];
855        let g = preference_game(20, 2, None, false, &pref, true, false, 7).unwrap();
856        assert_eq!(g.0.vcount(), 20);
857        assert!(g.0.is_directed());
858    }
859
860    #[test]
861    fn rejects_nan_type_dist() {
862        let pref = uniform_pref_sym(2, 0.1);
863        let td = vec![1.0, f64::NAN];
864        let res = preference_game(10, 2, Some(&td), false, &pref, false, false, 0);
865        assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
866    }
867
868    #[test]
869    fn rejects_negative_type_dist() {
870        let pref = uniform_pref_sym(2, 0.1);
871        let td = vec![1.0, -0.5];
872        let res = preference_game(10, 2, Some(&td), false, &pref, false, false, 0);
873        assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
874    }
875
876    #[test]
877    fn rejects_type_dist_length_mismatch() {
878        let pref = uniform_pref_sym(2, 0.1);
879        let td = vec![1.0, 1.0, 1.0];
880        let res = preference_game(10, 2, Some(&td), false, &pref, false, false, 0);
881        assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
882    }
883
884    #[test]
885    fn rejects_zero_total_mass() {
886        let pref = uniform_pref_sym(2, 0.1);
887        let td = vec![0.0, 0.0];
888        let res = preference_game(10, 2, Some(&td), false, &pref, false, false, 0);
889        assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
890    }
891
892    #[test]
893    fn fixed_sizes_rejects_wrong_sum() {
894        let pref = uniform_pref_sym(2, 0.1);
895        let td = vec![3.0, 3.0]; // sum 6 != nodes 10
896        let res = preference_game(10, 2, Some(&td), true, &pref, false, false, 0);
897        assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
898    }
899
900    #[test]
901    fn fixed_sizes_rejects_non_integer_counts() {
902        let pref = uniform_pref_sym(2, 0.1);
903        let td = vec![5.5, 4.5];
904        let res = preference_game(10, 2, Some(&td), true, &pref, false, false, 0);
905        assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
906    }
907
908    // -- symmetric: structural ----------------------------------------
909
910    #[test]
911    fn empty_pref_yields_no_edges() {
912        let pref = uniform_pref_sym(3, 0.0);
913        let (g, types) = preference_game(15, 3, None, false, &pref, false, false, 42).unwrap();
914        assert_eq!(g.vcount(), 15);
915        assert_eq!(g.ecount(), 0);
916        assert!(types.iter().all(|&t| t < 3));
917    }
918
919    #[test]
920    fn no_self_loops_when_loops_false_undirected() {
921        let pref = uniform_pref_sym(3, 0.5);
922        let (g, _types) = preference_game(20, 3, None, false, &pref, false, false, 7).unwrap();
923        assert!(!g.is_directed());
924        for e in 0..g.ecount() as u32 {
925            let (u, v) = g.edge(e).unwrap();
926            assert_ne!(u, v);
927        }
928    }
929
930    #[test]
931    fn no_self_loops_when_loops_false_directed() {
932        let pref = uniform_pref_sym(3, 0.5);
933        let (g, _types) = preference_game(20, 3, None, false, &pref, true, false, 7).unwrap();
934        assert!(g.is_directed());
935        for e in 0..g.ecount() as u32 {
936            let (u, v) = g.edge(e).unwrap();
937            assert_ne!(u, v);
938        }
939    }
940
941    #[test]
942    fn loops_allowed_when_loops_true() {
943        // p = 1.0 + loops = true ⇒ every diagonal slot becomes an edge,
944        // including the self-loops.
945        let pref = diag_pref_sym(2, 1.0);
946        let (g, _types) = preference_game(8, 2, None, false, &pref, false, true, 1).unwrap();
947        let mut found_loop = false;
948        for e in 0..g.ecount() as u32 {
949            let (u, v) = g.edge(e).unwrap();
950            if u == v {
951                found_loop = true;
952                break;
953            }
954        }
955        assert!(
956            found_loop,
957            "expected at least one self-loop with p=1, loops=true"
958        );
959    }
960
961    #[test]
962    fn fixed_sizes_equal_split_no_dist() {
963        let pref = uniform_pref_sym(3, 0.0);
964        let (g, types) = preference_game(9, 3, None, true, &pref, false, false, 0).unwrap();
965        assert_eq!(g.vcount(), 9);
966        // Equal split: 3, 3, 3
967        let mut counts = [0u32; 3];
968        for &t in &types {
969            counts[t as usize] += 1;
970        }
971        assert_eq!(counts, [3, 3, 3]);
972    }
973
974    #[test]
975    fn fixed_sizes_explicit_distribution() {
976        let pref = uniform_pref_sym(3, 0.0);
977        let td = vec![5.0, 3.0, 2.0];
978        let (g, types) = preference_game(10, 3, Some(&td), true, &pref, false, false, 0).unwrap();
979        assert_eq!(g.vcount(), 10);
980        let mut counts = [0u32; 3];
981        for &t in &types {
982            counts[t as usize] += 1;
983        }
984        assert_eq!(counts, [5, 3, 2]);
985    }
986
987    #[test]
988    fn deterministic_same_seed() {
989        let pref = uniform_pref_sym(3, 0.3);
990        let (g1, t1) = preference_game(20, 3, None, false, &pref, false, false, 12345).unwrap();
991        let (g2, t2) = preference_game(20, 3, None, false, &pref, false, false, 12345).unwrap();
992        assert_eq!(g1.ecount(), g2.ecount());
993        assert_eq!(t1, t2);
994        let edges1: Vec<_> = (0..g1.ecount() as u32)
995            .map(|e| g1.edge(e).unwrap())
996            .collect();
997        let edges2: Vec<_> = (0..g2.ecount() as u32)
998            .map(|e| g2.edge(e).unwrap())
999            .collect();
1000        assert_eq!(edges1, edges2);
1001    }
1002
1003    #[test]
1004    fn diag_pref_keeps_edges_within_type() {
1005        let pref = diag_pref_sym(3, 0.5);
1006        let (g, types) = preference_game(30, 3, None, false, &pref, false, false, 42).unwrap();
1007        for e in 0..g.ecount() as u32 {
1008            let (u, v) = g.edge(e).unwrap();
1009            assert_eq!(types[u as usize], types[v as usize]);
1010        }
1011    }
1012
1013    #[test]
1014    fn type_dist_skews_assignment() {
1015        // Heavily skewed dist: type 0 should dominate.
1016        let pref = uniform_pref_sym(2, 0.0);
1017        let td = vec![100.0, 1.0];
1018        let (_g, types) =
1019            preference_game(50, 2, Some(&td), false, &pref, false, false, 99).unwrap();
1020        let count0 = types.iter().filter(|&&t| t == 0).count();
1021        let count1 = types.iter().filter(|&&t| t == 1).count();
1022        assert!(
1023            count0 > count1,
1024            "expected type 0 to dominate, got {count0} vs {count1}"
1025        );
1026    }
1027
1028    // -- asymmetric: validation ---------------------------------------
1029
1030    #[test]
1031    fn asym_rejects_zero_out_types() {
1032        let pref: Vec<Vec<f64>> = vec![];
1033        let res = asymmetric_preference_game(10, 0, 2, None, &pref, false, 0);
1034        assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
1035    }
1036
1037    #[test]
1038    fn asym_rejects_zero_in_types() {
1039        let pref = vec![vec![]];
1040        let res = asymmetric_preference_game(10, 1, 0, None, &pref, false, 0);
1041        assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
1042    }
1043
1044    #[test]
1045    fn asym_rejects_pref_wrong_dim() {
1046        let pref = vec![vec![0.1, 0.1]];
1047        let res = asymmetric_preference_game(10, 2, 2, None, &pref, false, 0);
1048        assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
1049    }
1050
1051    #[test]
1052    fn asym_rejects_nan_pref() {
1053        let pref = vec![vec![0.1, f64::NAN], vec![0.1, 0.1]];
1054        let res = asymmetric_preference_game(10, 2, 2, None, &pref, false, 0);
1055        assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
1056    }
1057
1058    #[test]
1059    fn asym_rejects_pref_out_of_range() {
1060        let pref = vec![vec![0.1, 1.5], vec![0.1, 0.1]];
1061        let res = asymmetric_preference_game(10, 2, 2, None, &pref, false, 0);
1062        assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
1063    }
1064
1065    #[test]
1066    fn asym_rejects_tdm_wrong_dim() {
1067        let pref = vec![vec![0.1, 0.1], vec![0.1, 0.1]];
1068        let tdm = vec![vec![1.0, 1.0]]; // 1×2, expected 2×2
1069        let res = asymmetric_preference_game(10, 2, 2, Some(&tdm), &pref, false, 0);
1070        assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
1071    }
1072
1073    #[test]
1074    fn asym_rejects_negative_tdm() {
1075        let pref = vec![vec![0.1, 0.1], vec![0.1, 0.1]];
1076        let tdm = vec![vec![1.0, -1.0], vec![1.0, 1.0]];
1077        let res = asymmetric_preference_game(10, 2, 2, Some(&tdm), &pref, false, 0);
1078        assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
1079    }
1080
1081    #[test]
1082    fn asym_rejects_zero_total_mass() {
1083        let pref = vec![vec![0.1, 0.1], vec![0.1, 0.1]];
1084        let tdm = vec![vec![0.0, 0.0], vec![0.0, 0.0]];
1085        let res = asymmetric_preference_game(10, 2, 2, Some(&tdm), &pref, false, 0);
1086        assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
1087    }
1088
1089    // -- asymmetric: structural ---------------------------------------
1090
1091    #[test]
1092    fn asym_empty_pref_yields_no_edges() {
1093        let pref = vec![vec![0.0, 0.0], vec![0.0, 0.0]];
1094        let (g, out, inp) = asymmetric_preference_game(15, 2, 2, None, &pref, false, 42).unwrap();
1095        assert_eq!(g.vcount(), 15);
1096        assert_eq!(g.ecount(), 0);
1097        assert!(g.is_directed());
1098        assert!(out.iter().all(|&t| t < 2));
1099        assert!(inp.iter().all(|&t| t < 2));
1100    }
1101
1102    #[test]
1103    fn asym_always_directed() {
1104        let pref = vec![vec![0.3, 0.1], vec![0.1, 0.3]];
1105        let (g, _, _) = asymmetric_preference_game(20, 2, 2, None, &pref, false, 1).unwrap();
1106        assert!(g.is_directed());
1107    }
1108
1109    #[test]
1110    fn asym_no_self_loops_when_loops_false() {
1111        let pref = vec![vec![0.7, 0.3], vec![0.3, 0.7]];
1112        let (g, _out, _inp) = asymmetric_preference_game(30, 2, 2, None, &pref, false, 11).unwrap();
1113        for e in 0..g.ecount() as u32 {
1114            let (u, v) = g.edge(e).unwrap();
1115            assert_ne!(u, v);
1116        }
1117    }
1118
1119    #[test]
1120    fn asym_loops_allowed_when_loops_true() {
1121        // Single (out, in) cell at p = 1.0 ⇒ all v1 × v2 slots filled,
1122        // which include self-pairs.
1123        let pref = vec![vec![1.0]];
1124        let (g, _out, _inp) = asymmetric_preference_game(5, 1, 1, None, &pref, true, 0).unwrap();
1125        // 5 vertices, all 25 directed slots present (with loops).
1126        assert_eq!(g.ecount(), 25);
1127        let mut found_loop = false;
1128        for e in 0..g.ecount() as u32 {
1129            let (u, v) = g.edge(e).unwrap();
1130            if u == v {
1131                found_loop = true;
1132                break;
1133            }
1134        }
1135        assert!(found_loop);
1136    }
1137
1138    #[test]
1139    fn asym_loops_false_full_pref_yields_no_loops_full_off_diag() {
1140        let pref = vec![vec![1.0]];
1141        let (g, _out, _inp) = asymmetric_preference_game(5, 1, 1, None, &pref, false, 0).unwrap();
1142        // 5 vertices, no loops ⇒ 5*5 - 5 = 20 directed off-diagonal edges.
1143        assert_eq!(g.ecount(), 20);
1144        for e in 0..g.ecount() as u32 {
1145            let (u, v) = g.edge(e).unwrap();
1146            assert_ne!(u, v);
1147        }
1148    }
1149
1150    #[test]
1151    fn asym_deterministic_same_seed() {
1152        let pref = vec![vec![0.3, 0.1], vec![0.1, 0.3]];
1153        let (g1, o1, i1) = asymmetric_preference_game(20, 2, 2, None, &pref, false, 9999).unwrap();
1154        let (g2, o2, i2) = asymmetric_preference_game(20, 2, 2, None, &pref, false, 9999).unwrap();
1155        assert_eq!(g1.ecount(), g2.ecount());
1156        assert_eq!(o1, o2);
1157        assert_eq!(i1, i2);
1158        let e1: Vec<_> = (0..g1.ecount() as u32)
1159            .map(|e| g1.edge(e).unwrap())
1160            .collect();
1161        let e2: Vec<_> = (0..g2.ecount() as u32)
1162            .map(|e| g2.edge(e).unwrap())
1163            .collect();
1164        assert_eq!(e1, e2);
1165    }
1166
1167    // -- helpers tested directly -------------------------------------
1168
1169    #[test]
1170    fn cumdist_lookup_handles_endpoints() {
1171        let cd = vec![0.0, 1.0, 3.0, 6.0];
1172        assert_eq!(cumdist_lookup(&cd, 0.0), 1);
1173        assert_eq!(cumdist_lookup(&cd, 0.5), 1);
1174        assert_eq!(cumdist_lookup(&cd, 1.0), 2);
1175        assert_eq!(cumdist_lookup(&cd, 2.99), 2);
1176        assert_eq!(cumdist_lookup(&cd, 3.0), 3);
1177        assert_eq!(cumdist_lookup(&cd, 5.5), 3);
1178    }
1179
1180    #[test]
1181    fn sorted_intersection_basic() {
1182        assert_eq!(
1183            sorted_intersection(&[1, 3, 5, 7], &[2, 3, 5, 9]),
1184            vec![3, 5]
1185        );
1186        assert_eq!(sorted_intersection(&[], &[1, 2]), Vec::<u32>::new());
1187        assert_eq!(sorted_intersection(&[1, 2, 3], &[4, 5]), Vec::<u32>::new());
1188        assert_eq!(sorted_intersection(&[1, 2, 3], &[1, 2, 3]), vec![1, 2, 3]);
1189    }
1190}
1191
1192#[cfg(all(test, feature = "proptest-harness"))]
1193mod proptest_invariants {
1194    use super::*;
1195    use proptest::prelude::*;
1196
1197    fn uniform_pref_sym(k: usize, p: f64) -> Vec<Vec<f64>> {
1198        vec![vec![p; k]; k]
1199    }
1200
1201    fn diag_pref_sym(k: usize, p: f64) -> Vec<Vec<f64>> {
1202        let mut m = vec![vec![0.0; k]; k];
1203        for (i, row) in m.iter_mut().enumerate() {
1204            row[i] = p;
1205        }
1206        m
1207    }
1208
1209    proptest! {
1210        #![proptest_config(ProptestConfig::with_cases(48))]
1211
1212        #[test]
1213        fn vcount_equals_nodes(
1214            n in 1u32..40,
1215            k in 1u32..5,
1216            seed: u64,
1217            directed: bool,
1218            loops: bool,
1219        ) {
1220            let pref = uniform_pref_sym(k as usize, 0.2);
1221            let (g, types) = preference_game(n, k, None, false, &pref, directed, loops, seed).unwrap();
1222            prop_assert_eq!(g.vcount(), n);
1223            prop_assert_eq!(types.len(), n as usize);
1224            for &t in &types {
1225                prop_assert!(t < k);
1226            }
1227        }
1228
1229        #[test]
1230        fn no_self_loops_when_loops_false(
1231            n in 1u32..30,
1232            k in 1u32..4,
1233            seed: u64,
1234            directed: bool,
1235        ) {
1236            let pref = uniform_pref_sym(k as usize, 0.4);
1237            let (g, _types) = preference_game(n, k, None, false, &pref, directed, false, seed).unwrap();
1238            for e in 0..g.ecount() as u32 {
1239                let (u, v) = g.edge(e).unwrap();
1240                prop_assert_ne!(u, v);
1241            }
1242        }
1243
1244        #[test]
1245        fn diag_pref_keeps_edges_within_type(
1246            n in 4u32..30,
1247            k in 2u32..4,
1248            seed: u64,
1249        ) {
1250            let pref = diag_pref_sym(k as usize, 0.5);
1251            let (g, types) = preference_game(n, k, None, false, &pref, false, false, seed).unwrap();
1252            for e in 0..g.ecount() as u32 {
1253                let (u, v) = g.edge(e).unwrap();
1254                prop_assert_eq!(types[u as usize], types[v as usize]);
1255            }
1256        }
1257
1258        #[test]
1259        fn fixed_sizes_equal_split_counts_match(
1260            size_one in 1u32..6,
1261            k in 1u32..5,
1262            seed: u64,
1263        ) {
1264            let pref = uniform_pref_sym(k as usize, 0.0);
1265            let n = size_one * k;
1266            let (_g, types) = preference_game(n, k, None, true, &pref, false, false, seed).unwrap();
1267            let mut counts = vec![0u32; k as usize];
1268            for &t in &types {
1269                counts[t as usize] += 1;
1270            }
1271            for &c in &counts {
1272                prop_assert_eq!(c, size_one);
1273            }
1274        }
1275
1276        #[test]
1277        fn determinism_seed(
1278            n in 1u32..25,
1279            k in 1u32..4,
1280            seed: u64,
1281        ) {
1282            let pref = uniform_pref_sym(k as usize, 0.3);
1283            let (g1, t1) = preference_game(n, k, None, false, &pref, false, false, seed).unwrap();
1284            let (g2, t2) = preference_game(n, k, None, false, &pref, false, false, seed).unwrap();
1285            prop_assert_eq!(g1.ecount(), g2.ecount());
1286            prop_assert_eq!(t1, t2);
1287        }
1288
1289        #[test]
1290        fn asym_vcount_matches(
1291            n in 1u32..30,
1292            no_out in 1u32..4,
1293            no_in in 1u32..4,
1294            seed: u64,
1295            loops: bool,
1296        ) {
1297            let pref = vec![vec![0.2; no_in as usize]; no_out as usize];
1298            let (g, out, inp) =
1299                asymmetric_preference_game(n, no_out, no_in, None, &pref, loops, seed).unwrap();
1300            prop_assert_eq!(g.vcount(), n);
1301            prop_assert!(g.is_directed());
1302            prop_assert_eq!(out.len(), n as usize);
1303            prop_assert_eq!(inp.len(), n as usize);
1304            for &t in &out {
1305                prop_assert!(t < no_out);
1306            }
1307            for &t in &inp {
1308                prop_assert!(t < no_in);
1309            }
1310        }
1311
1312        #[test]
1313        fn asym_no_self_loops_when_loops_false(
1314            n in 1u32..30,
1315            no_out in 1u32..3,
1316            no_in in 1u32..3,
1317            seed: u64,
1318        ) {
1319            let pref = vec![vec![0.5; no_in as usize]; no_out as usize];
1320            let (g, _out, _inp) =
1321                asymmetric_preference_game(n, no_out, no_in, None, &pref, false, seed).unwrap();
1322            for e in 0..g.ecount() as u32 {
1323                let (u, v) = g.edge(e).unwrap();
1324                prop_assert_ne!(u, v);
1325            }
1326        }
1327
1328        #[test]
1329        fn asym_determinism_seed(
1330            n in 1u32..25,
1331            no_out in 1u32..3,
1332            no_in in 1u32..3,
1333            seed: u64,
1334        ) {
1335            let pref = vec![vec![0.3; no_in as usize]; no_out as usize];
1336            let (g1, o1, i1) =
1337                asymmetric_preference_game(n, no_out, no_in, None, &pref, false, seed).unwrap();
1338            let (g2, o2, i2) =
1339                asymmetric_preference_game(n, no_out, no_in, None, &pref, false, seed).unwrap();
1340            prop_assert_eq!(g1.ecount(), g2.ecount());
1341            prop_assert_eq!(o1, o2);
1342            prop_assert_eq!(i1, i2);
1343        }
1344    }
1345}