Skip to main content

rust_igraph/algorithms/games/
erdos_renyi.rs

1//! Erdős–Rényi random graph generators (ALGO-GN-001).
2//!
3//! Counterpart of `igraph_erdos_renyi_game_gnp()` and
4//! `igraph_erdos_renyi_game_gnm()` from
5//! `references/igraph/src/games/erdos_renyi.c:715` and `:429`.
6//!
7//! Two classic models — both fix the vertex count `n`:
8//!
9//! * **G(n, p)**: every possible vertex pair is independently connected
10//!   with probability `p`. The expected edge count is
11//!   `p · max_edges`. Sampled via Batagelj & Brandes 2005's
12//!   geometric-skip algorithm so the cost is `O(|V| + |E|)` rather than
13//!   `O(|V|²)`.
14//!
15//! * **G(n, m)**: exactly `m` distinct edges are chosen uniformly at
16//!   random from the `max_edges` possible. Sampled with Floyd's
17//!   algorithm for an `O(m)` distinct-pair draw, then each pair-index is
18//!   decoded into a `(from, to)` pair using the same formulas the
19//!   upstream C implementation uses.
20//!
21//! Both functions are deterministic given the `seed` argument and run
22//! against the shared `SplitMix64` PRNG.
23//!
24//! ## Scope
25//!
26//! MVP scope ports the most-used path: **simple graphs**, with `loops`
27//! optionally enabled. Multigraphs (`multiple = true`) and the
28//! edge-labeled IEA variant are out of scope for this AWU — they will
29//! land as follow-up AWUs if real users ask for them. Returning an
30//! `IgraphError::Unimplemented`-style variant from a hypothetical
31//! multigraph flag would be the wrong API shape (the flag could never
32//! be true), so we simply omit it from the public surface.
33//!
34//! ## References
35//!
36//! * V. Batagelj and U. Brandes,
37//!   *"Efficient generation of large random networks"*,
38//!   Phys. Rev. E **71**, 036113 (2005).
39//! * R. W. Floyd, *Algorithm 489: The algorithm `SELECT` …*, CACM (1972)
40//!   — the distinct-sample technique used in G(n, m).
41
42#![allow(
43    unknown_lints,
44    clippy::cast_possible_truncation,
45    clippy::cast_precision_loss,
46    clippy::cast_sign_loss,
47    clippy::float_cmp,
48    clippy::manual_midpoint
49)]
50
51use std::collections::HashSet;
52
53use crate::core::rng::SplitMix64;
54use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
55
56/// Decode a linear pair-index `idx ∈ [0, max_edges)` into a `(from, to)`
57/// vertex pair. The four cases mirror the upstream C decoders in
58/// `erdos_renyi.c:339-374` and `:806-841`.
59fn decode_pair(idx: u64, n: u32, directed: bool, loops: bool) -> (VertexId, VertexId) {
60    let n_u64 = u64::from(n);
61    if directed && loops {
62        // n × n grid, row = `to`, col = `from`.
63        let to = idx / n_u64;
64        let from = idx - to * n_u64;
65        debug_assert!(from < n_u64 && to < n_u64);
66        #[allow(clippy::cast_possible_truncation)]
67        ((from as VertexId), (to as VertexId))
68    } else if directed && !loops {
69        // n × n grid sampled at n*(n-1) cells; diagonal elements get
70        // remapped to column `n-1`. Matches `erdos_renyi.c:814-824`.
71        let to = idx / n_u64;
72        let from = idx - to * n_u64;
73        let to = if from == to { n_u64 - 1 } else { to };
74        debug_assert!(from < n_u64 && to < n_u64 && from != to);
75        #[allow(clippy::cast_possible_truncation)]
76        ((from as VertexId), (to as VertexId))
77    } else if !directed && loops {
78        // Triangular numbering INCLUDING the diagonal:
79        //   to = floor((sqrt(8·idx + 1) - 1) / 2)
80        //   from = idx - to·(to+1)/2
81        // produces `0 <= from <= to < n`.
82        #[allow(clippy::cast_precision_loss)]
83        let idx_f = idx as f64;
84        let to_f = ((8.0 * idx_f + 1.0).sqrt() - 1.0) / 2.0;
85        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
86        let mut to = to_f.trunc() as u64;
87        // Recompute `from` exactly in integer arithmetic; the float
88        // truncation may have rounded down by one, so verify the
89        // result lies in [0, to] and bump if needed.
90        let mut from = idx - to * (to + 1) / 2;
91        while from > to {
92            to += 1;
93            from = idx - to * (to + 1) / 2;
94        }
95        debug_assert!(from <= to && to < n_u64);
96        #[allow(clippy::cast_possible_truncation)]
97        ((from as VertexId), (to as VertexId))
98    } else {
99        // !directed && !loops — triangular numbering EXCLUDING the diagonal:
100        //   to = floor((sqrt(8·idx + 1) + 1) / 2)
101        //   from = idx - to·(to-1)/2
102        // produces `0 <= from < to < n`.
103        #[allow(clippy::cast_precision_loss)]
104        let idx_f = idx as f64;
105        let to_f = ((8.0 * idx_f + 1.0).sqrt() + 1.0) / 2.0;
106        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
107        let mut to = to_f.trunc() as u64;
108        if to < 1 {
109            to = 1;
110        }
111        let mut from = idx - to * (to - 1) / 2;
112        while from >= to {
113            to += 1;
114            from = idx - to * (to - 1) / 2;
115        }
116        debug_assert!(from < to && to < n_u64);
117        #[allow(clippy::cast_possible_truncation)]
118        ((from as VertexId), (to as VertexId))
119    }
120}
121
122/// Number of *possible* edges for the four `(directed, loops)` cases.
123/// Returned as `u64` so callers walking pair-index space don't
124/// silently truncate at `n ≈ 65536` (where `n² > u32::MAX`).
125fn max_edges(n: u32, directed: bool, loops: bool) -> u64 {
126    let n = u64::from(n);
127    match (directed, loops) {
128        (true, true) => n * n,
129        (true, false) => n * n.saturating_sub(1),
130        (false, true) => n * (n + 1) / 2,
131        (false, false) => n * n.saturating_sub(1) / 2,
132    }
133}
134
135/// Sample `m` distinct integers from `[0, n_pairs)` using Floyd's
136/// algorithm.
137///
138/// Floyd's invariant: after step `j ∈ [n-m, n)`, the set `S` contains a
139/// uniform random `m`-subset of `[0, j+1)`. The procedure performs
140/// exactly `m` PRNG draws and `m` `HashSet` insertions — no rejection
141/// retries — and uses `O(m)` memory.
142///
143/// Returns the sampled indices in *sorted* order for deterministic
144/// edge ordering (handy for property tests and reproducible bench
145/// inputs).
146fn distinct_sample(rng: &mut SplitMix64, n_pairs: u64, m: u64) -> Vec<u64> {
147    debug_assert!(m <= n_pairs);
148    let cap = usize::try_from(m).unwrap_or(usize::MAX);
149    let mut chosen: HashSet<u64> = HashSet::with_capacity(cap);
150    let mut out: Vec<u64> = Vec::with_capacity(cap);
151    for j in (n_pairs - m)..n_pairs {
152        // Uniform in [0, j+1). For typical graphs `j+1 < 2^53` so the
153        // double-precision intermediate is exact.
154        let span = j + 1;
155        let span_usize = usize::try_from(span).unwrap_or(usize::MAX);
156        // gen_index takes usize. For n_pairs > usize::MAX we'd overflow;
157        // we already gate that out below by capping m.
158        let t_usize = rng.gen_index(span_usize);
159        let t = t_usize as u64;
160        let pick = if chosen.insert(t) {
161            t
162        } else {
163            chosen.insert(j);
164            j
165        };
166        out.push(pick);
167    }
168    out.sort_unstable();
169    out
170}
171
172fn validate_gnp(p: f64) -> IgraphResult<()> {
173    if !p.is_finite() {
174        return Err(IgraphError::InvalidArgument(format!(
175            "G(n,p) probability must be finite (got {p})"
176        )));
177    }
178    if !(0.0..=1.0).contains(&p) {
179        return Err(IgraphError::InvalidArgument(format!(
180            "G(n,p) probability must be in [0, 1] (got {p})"
181        )));
182    }
183    Ok(())
184}
185
186fn validate_gnm(n: u32, m: u64, directed: bool, loops: bool) -> IgraphResult<()> {
187    let cap = max_edges(n, directed, loops);
188    if m > cap {
189        return Err(IgraphError::InvalidArgument(format!(
190            "G(n,m) requested {m} edges but the {} graph on {n} vertices admits at most {cap}",
191            if directed { "directed" } else { "undirected" }
192        )));
193    }
194    Ok(())
195}
196
197/// Build a `Graph` with `n` vertices, `directed`, and the given edge
198/// list (already decoded from pair-indices).
199fn finalize(n: u32, directed: bool, edges: Vec<(VertexId, VertexId)>) -> IgraphResult<Graph> {
200    let mut g = Graph::new(n, directed)?;
201    g.add_edges(edges)?;
202    Ok(g)
203}
204
205/// Generate a complete graph `K_n` (with self-loops if `loops`).
206fn complete(n: u32, directed: bool, loops: bool) -> IgraphResult<Graph> {
207    let cap = max_edges(n, directed, loops);
208    let cap_usize = usize::try_from(cap)
209        .map_err(|_| IgraphError::Internal("complete graph edge count exceeds usize"))?;
210    let mut edges = Vec::with_capacity(cap_usize);
211    for i in 0..n {
212        if directed {
213            for j in 0..n {
214                if i == j && !loops {
215                    continue;
216                }
217                edges.push((i, j));
218            }
219        } else {
220            let start = if loops { i } else { i + 1 };
221            for j in start..n {
222                edges.push((i, j));
223            }
224        }
225    }
226    finalize(n, directed, edges)
227}
228
229/// Generate a random graph from the **G(n, p)** Erdős–Rényi model.
230///
231/// Every possible edge is included independently with probability `p`.
232/// The expected number of edges is `p · max_edges(n, directed, loops)`.
233///
234/// * `n` — vertex count.
235/// * `p ∈ [0, 1]` — edge probability.
236/// * `directed` — generate a directed graph (ordered pairs); when
237///   `false`, edges are undirected.
238/// * `loops` — when `true`, self-loops are allowed; when `false`, no
239///   self-loop edges are sampled.
240/// * `seed` — initialises an internal `SplitMix64` PRNG. Same
241///   `(n, p, directed, loops, seed)` always yields the same graph.
242///
243/// # Errors
244///
245/// Returns [`IgraphError::InvalidArgument`] if `p` is NaN, infinite,
246/// or outside `[0, 1]`.
247///
248/// # Examples
249///
250/// ```
251/// use rust_igraph::erdos_renyi_gnp;
252/// // Expected edges ≈ p · n(n-1)/2 = 0.5 · 100·99/2 = 2475.
253/// let g = erdos_renyi_gnp(100, 0.5, false, false, 42).unwrap();
254/// assert_eq!(g.vcount(), 100);
255/// // The actual count fluctuates around the expectation, but for
256/// // p = 0.5 it lies in a narrow band.
257/// let m = g.ecount();
258/// assert!(m > 2200 && m < 2700, "ecount={m}");
259/// ```
260pub fn erdos_renyi_gnp(
261    n: u32,
262    p: f64,
263    directed: bool,
264    loops: bool,
265    seed: u64,
266) -> IgraphResult<Graph> {
267    validate_gnp(p)?;
268
269    let cap = max_edges(n, directed, loops);
270    if n == 0 || p == 0.0 || cap == 0 {
271        return Graph::new(n, directed);
272    }
273    if p == 1.0 {
274        return complete(n, directed, loops);
275    }
276
277    let mut rng = SplitMix64::new(seed);
278    // Batagelj–Brandes geometric-skip walk over the linear pair-index
279    // space [0, cap). At each step, advance by `RNG_GEOM(p) + 1` (the
280    // +1 enforces strictly-increasing indices so the sample is a
281    // *simple* graph).
282    #[allow(clippy::cast_precision_loss)]
283    let cap_f = cap as f64;
284    let mut last = rng.gen_geom(p);
285    let mut indices: Vec<u64> = Vec::new();
286    while last < cap_f {
287        // Convert via trunc — last is always non-negative.
288        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
289        let idx = last.trunc() as u64;
290        if idx >= cap {
291            break;
292        }
293        indices.push(idx);
294        last += rng.gen_geom(p);
295        last += 1.0; // simple-graph step
296    }
297
298    let edges: Vec<(VertexId, VertexId)> = indices
299        .into_iter()
300        .map(|idx| decode_pair(idx, n, directed, loops))
301        .collect();
302    finalize(n, directed, edges)
303}
304
305/// Generate a random graph from the **G(n, m)** Erdős–Rényi model.
306///
307/// Exactly `m` edges are drawn uniformly at random from the
308/// `max_edges(n, directed, loops)` possible edges. Sampling is
309/// without replacement (simple graph).
310///
311/// * `n` — vertex count.
312/// * `m` — edge count.
313/// * `directed`, `loops`, `seed` — see [`erdos_renyi_gnp`].
314///
315/// # Errors
316///
317/// Returns [`IgraphError::InvalidArgument`] if `m` exceeds
318/// `max_edges(n, directed, loops)`.
319///
320/// # Examples
321///
322/// ```
323/// use rust_igraph::erdos_renyi_gnm;
324/// let g = erdos_renyi_gnm(20, 30, false, false, 7).unwrap();
325/// assert_eq!(g.vcount(), 20);
326/// assert_eq!(g.ecount(), 30);
327/// ```
328pub fn erdos_renyi_gnm(
329    n: u32,
330    m: u64,
331    directed: bool,
332    loops: bool,
333    seed: u64,
334) -> IgraphResult<Graph> {
335    validate_gnm(n, m, directed, loops)?;
336
337    if m == 0 {
338        return Graph::new(n, directed);
339    }
340
341    let cap = max_edges(n, directed, loops);
342    if m == cap {
343        return complete(n, directed, loops);
344    }
345
346    let mut rng = SplitMix64::new(seed);
347    let picks = distinct_sample(&mut rng, cap, m);
348    let edges: Vec<(VertexId, VertexId)> = picks
349        .into_iter()
350        .map(|idx| decode_pair(idx, n, directed, loops))
351        .collect();
352    finalize(n, directed, edges)
353}
354
355#[cfg(test)]
356mod tests {
357    use super::*;
358    use std::collections::HashSet;
359
360    // ------- decode_pair sanity -------
361
362    #[test]
363    fn decode_undirected_no_loops_covers_strict_upper_triangle() {
364        // For n=5, max_edges = 10. Every idx in [0, 10) must produce a
365        // pair (from < to) with both vertices in [0, 5).
366        let n = 5u32;
367        let cap = max_edges(n, false, false);
368        assert_eq!(cap, 10);
369        let mut seen: HashSet<(u32, u32)> = HashSet::new();
370        for idx in 0..cap {
371            let (u, v) = decode_pair(idx, n, false, false);
372            assert!(u < v && v < n);
373            assert!(seen.insert((u, v)), "duplicate pair at idx {idx}");
374        }
375        assert_eq!(seen.len(), cap as usize);
376    }
377
378    #[test]
379    fn decode_undirected_with_loops_covers_lower_triangle_incl_diagonal() {
380        let n = 4u32;
381        let cap = max_edges(n, false, true);
382        assert_eq!(cap, 10);
383        let mut seen: HashSet<(u32, u32)> = HashSet::new();
384        for idx in 0..cap {
385            let (u, v) = decode_pair(idx, n, false, true);
386            assert!(u <= v && v < n);
387            assert!(seen.insert((u, v)));
388        }
389        assert_eq!(seen.len(), cap as usize);
390    }
391
392    #[test]
393    fn decode_directed_no_loops_covers_off_diagonal() {
394        let n = 5u32;
395        let cap = max_edges(n, true, false);
396        assert_eq!(cap, 20);
397        let mut seen: HashSet<(u32, u32)> = HashSet::new();
398        for idx in 0..cap {
399            let (u, v) = decode_pair(idx, n, true, false);
400            assert!(u < n && v < n && u != v);
401            assert!(seen.insert((u, v)));
402        }
403        assert_eq!(seen.len(), cap as usize);
404    }
405
406    #[test]
407    fn decode_directed_with_loops_covers_full_grid() {
408        let n = 4u32;
409        let cap = max_edges(n, true, true);
410        assert_eq!(cap, 16);
411        let mut seen: HashSet<(u32, u32)> = HashSet::new();
412        for idx in 0..cap {
413            let (u, v) = decode_pair(idx, n, true, true);
414            assert!(u < n && v < n);
415            assert!(seen.insert((u, v)));
416        }
417        assert_eq!(seen.len(), cap as usize);
418    }
419
420    // ------- gnp boundary cases -------
421
422    #[test]
423    fn gnp_p_zero_is_empty() {
424        let g = erdos_renyi_gnp(10, 0.0, false, false, 1).unwrap();
425        assert_eq!(g.vcount(), 10);
426        assert_eq!(g.ecount(), 0);
427    }
428
429    #[test]
430    fn gnp_p_one_is_complete() {
431        let g = erdos_renyi_gnp(6, 1.0, false, false, 1).unwrap();
432        assert_eq!(g.vcount(), 6);
433        assert_eq!(g.ecount(), 15); // 6 choose 2
434    }
435
436    #[test]
437    fn gnp_directed_p_one_loops_is_full_grid() {
438        let g = erdos_renyi_gnp(4, 1.0, true, true, 1).unwrap();
439        assert_eq!(g.ecount(), 16);
440    }
441
442    #[test]
443    fn gnp_invalid_p_rejected() {
444        assert!(erdos_renyi_gnp(5, -0.1, false, false, 1).is_err());
445        assert!(erdos_renyi_gnp(5, 1.1, false, false, 1).is_err());
446        assert!(erdos_renyi_gnp(5, f64::NAN, false, false, 1).is_err());
447        assert!(erdos_renyi_gnp(5, f64::INFINITY, false, false, 1).is_err());
448    }
449
450    #[test]
451    fn gnp_zero_vertices_is_empty() {
452        let g = erdos_renyi_gnp(0, 0.5, false, false, 1).unwrap();
453        assert_eq!(g.vcount(), 0);
454        assert_eq!(g.ecount(), 0);
455    }
456
457    // ------- gnp determinism + distribution sanity -------
458
459    #[test]
460    fn gnp_deterministic_with_seed() {
461        let a = erdos_renyi_gnp(50, 0.3, false, false, 12345).unwrap();
462        let b = erdos_renyi_gnp(50, 0.3, false, false, 12345).unwrap();
463        assert_eq!(a.vcount(), b.vcount());
464        assert_eq!(a.ecount(), b.ecount());
465        // Edge sets must match exactly.
466        let edges_a: Vec<_> = (0..a.ecount()).map(|e| a.edge(e as u32).unwrap()).collect();
467        let edges_b: Vec<_> = (0..b.ecount()).map(|e| b.edge(e as u32).unwrap()).collect();
468        assert_eq!(edges_a, edges_b);
469    }
470
471    #[test]
472    fn gnp_different_seeds_differ() {
473        let a = erdos_renyi_gnp(200, 0.05, false, false, 1).unwrap();
474        let b = erdos_renyi_gnp(200, 0.05, false, false, 2).unwrap();
475        // Tiny chance of accidental equality but vanishingly small here.
476        assert_ne!(a.ecount(), b.ecount());
477    }
478
479    #[test]
480    fn gnp_expected_ecount_in_band() {
481        // E[m] = p · n(n-1)/2 = 0.2 · 100·99/2 = 990. Sample stddev ≈
482        // sqrt(p(1-p) · n(n-1)/2) ≈ sqrt(0.16 · 4950) ≈ 28; ±100 is
483        // ~3.5σ — generous but tight enough to catch a broken sampler.
484        let g = erdos_renyi_gnp(100, 0.2, false, false, 31_415).unwrap();
485        let m = g.ecount();
486        assert!(m > 890 && m < 1090, "ecount = {m}");
487    }
488
489    #[test]
490    fn gnp_is_simple_no_loops() {
491        let g = erdos_renyi_gnp(50, 0.4, false, false, 7).unwrap();
492        for e in 0..g.ecount() {
493            let (u, v) = g.edge(e as u32).unwrap();
494            assert_ne!(u, v, "self-loop generated at edge {e}");
495        }
496    }
497
498    #[test]
499    fn gnp_no_parallel_edges_simple() {
500        let g = erdos_renyi_gnp(40, 0.3, false, false, 11).unwrap();
501        let mut seen: HashSet<(u32, u32)> = HashSet::new();
502        for e in 0..g.ecount() {
503            let pair = g.edge(e as u32).unwrap();
504            assert!(seen.insert(pair), "parallel edge at {e}: {pair:?}");
505        }
506    }
507
508    // ------- gnm boundary cases -------
509
510    #[test]
511    fn gnm_m_zero_is_empty() {
512        let g = erdos_renyi_gnm(10, 0, false, false, 1).unwrap();
513        assert_eq!(g.ecount(), 0);
514    }
515
516    #[test]
517    fn gnm_m_max_is_complete() {
518        let g = erdos_renyi_gnm(6, 15, false, false, 1).unwrap();
519        assert_eq!(g.ecount(), 15);
520    }
521
522    #[test]
523    fn gnm_m_exceeds_capacity_rejected() {
524        // K_5 has 10 undirected edges; asking for 11 is impossible.
525        assert!(erdos_renyi_gnm(5, 11, false, false, 1).is_err());
526    }
527
528    #[test]
529    fn gnm_exact_edge_count() {
530        let g = erdos_renyi_gnm(50, 100, false, false, 42).unwrap();
531        assert_eq!(g.ecount(), 100);
532        assert_eq!(g.vcount(), 50);
533    }
534
535    #[test]
536    fn gnm_deterministic_with_seed() {
537        let a = erdos_renyi_gnm(30, 60, false, false, 7).unwrap();
538        let b = erdos_renyi_gnm(30, 60, false, false, 7).unwrap();
539        let edges_a: Vec<_> = (0..a.ecount()).map(|e| a.edge(e as u32).unwrap()).collect();
540        let edges_b: Vec<_> = (0..b.ecount()).map(|e| b.edge(e as u32).unwrap()).collect();
541        assert_eq!(edges_a, edges_b);
542    }
543
544    #[test]
545    fn gnm_is_simple_no_loops() {
546        let g = erdos_renyi_gnm(20, 40, false, false, 99).unwrap();
547        let mut seen: HashSet<(u32, u32)> = HashSet::new();
548        for e in 0..g.ecount() {
549            let (u, v) = g.edge(e as u32).unwrap();
550            assert_ne!(u, v);
551            assert!(seen.insert((u, v)), "parallel edge: {u} {v}");
552        }
553    }
554
555    #[test]
556    fn gnm_directed_loops_full_grid() {
557        // n=3, max directed-with-loops edges = 9. Drawing all 9 must
558        // yield exactly one of each (i, j) pair.
559        let g = erdos_renyi_gnm(3, 9, true, true, 0).unwrap();
560        assert_eq!(g.ecount(), 9);
561        let mut seen: HashSet<(u32, u32)> = HashSet::new();
562        for e in 0..g.ecount() {
563            seen.insert(g.edge(e as u32).unwrap());
564        }
565        assert_eq!(seen.len(), 9);
566    }
567
568    #[test]
569    fn gnm_directed_no_loops() {
570        // n=4 directed without loops: max = 12. Drawing all 12.
571        let g = erdos_renyi_gnm(4, 12, true, false, 0).unwrap();
572        assert_eq!(g.ecount(), 12);
573        let mut seen: HashSet<(u32, u32)> = HashSet::new();
574        for e in 0..g.ecount() {
575            let (u, v) = g.edge(e as u32).unwrap();
576            assert_ne!(u, v);
577            seen.insert((u, v));
578        }
579        assert_eq!(seen.len(), 12);
580    }
581
582    // ------- proptest harness (gated; runs under `cargo test --features proptest-harness`) -------
583
584    #[cfg(all(test, feature = "proptest-harness"))]
585    mod prop {
586        use super::super::*;
587        use proptest::prelude::*;
588
589        proptest! {
590            #[test]
591            fn gnp_vcount_always_matches_n(
592                n in 0u32..50,
593                p in 0.0..=1.0,
594                directed in any::<bool>(),
595                loops in any::<bool>(),
596                seed in any::<u64>(),
597            ) {
598                let g = erdos_renyi_gnp(n, p, directed, loops, seed).unwrap();
599                prop_assert_eq!(g.vcount(), n);
600                prop_assert!(g.ecount() as u64 <= max_edges(n, directed, loops));
601            }
602
603            #[test]
604            fn gnp_simple_no_self_loops_no_multi(
605                n in 1u32..30,
606                p in 0.0..=0.7,
607                seed in any::<u64>(),
608            ) {
609                let g = erdos_renyi_gnp(n, p, false, false, seed).unwrap();
610                let mut seen = std::collections::HashSet::new();
611                for e in 0..g.ecount() {
612                    let (u, v) = g.edge(e as u32).unwrap();
613                    prop_assert!(u != v);
614                    prop_assert!(seen.insert((u, v)));
615                }
616            }
617
618            #[test]
619            fn gnm_exact_count_and_simple(
620                n in 2u32..30,
621                seed in any::<u64>(),
622            ) {
623                // Pick m up to max-1 to exercise non-trivial Floyd path.
624                let cap = max_edges(n, false, false);
625                if cap == 0 { return Ok(()); }
626                // Bound m so we stay deterministic w.r.t. seed.
627                let m = (seed as u64) % cap;
628                let g = erdos_renyi_gnm(n, m, false, false, seed).unwrap();
629                prop_assert_eq!(g.ecount(), m as usize);
630                let mut seen = std::collections::HashSet::new();
631                for e in 0..g.ecount() {
632                    let (u, v) = g.edge(e as u32).unwrap();
633                    prop_assert!(u != v);
634                    prop_assert!(seen.insert((u, v)));
635                }
636            }
637
638            #[test]
639            fn gnp_determinism(
640                n in 1u32..40,
641                p in 0.05..0.95,
642                directed in any::<bool>(),
643                loops in any::<bool>(),
644                seed in any::<u64>(),
645            ) {
646                let a = erdos_renyi_gnp(n, p, directed, loops, seed).unwrap();
647                let b = erdos_renyi_gnp(n, p, directed, loops, seed).unwrap();
648                prop_assert_eq!(a.ecount(), b.ecount());
649                for e in 0..a.ecount() {
650                    prop_assert_eq!(a.edge(e as u32).unwrap(), b.edge(e as u32).unwrap());
651                }
652            }
653        }
654    }
655}