Skip to main content

rust_igraph/algorithms/games/
degree_sequence_configuration_simple.rs

1//! Configuration-model **simple-graph** degree-sequence generator
2//! (ALGO-GN-027).
3//!
4//! Counterpart of the `IGRAPH_DEGSEQ_CONFIGURATION_SIMPLE` branch of
5//! `igraph_degree_sequence_game()` in
6//! `references/igraph/src/games/degree_sequence.c`
7//! (`configuration_simple_undirected` lines 552-594,
8//! `configuration_simple_directed` lines 597-708).
9//!
10//! Like the multigraph configuration model (ALGO-GN-024) the algorithm
11//! builds a flat **stub bag** of size `Σd` and pairs stubs by an
12//! incremental Fisher–Yates shuffle. The key difference is that any
13//! self-loop or multi-edge produced during the inner shuffle aborts the
14//! attempt: the adjacency tracker is cleared and the entire attempt is
15//! restarted. The resulting distribution is exactly uniform on the set
16//! of simple realisations of the prescribed sequence — at the cost of an
17//! unbounded expected restart count for sequences that are hard to
18//! realise simply.
19//!
20//! ## Undirected branch
21//!
22//! Build `stubs = [v repeated d_v times]` of length `Σd`. Then for each
23//! `i ∈ [0, |E|)`:
24//!
25//! 1. Pick `k ← RNG(2i, |stubs|−1)`, swap `stubs[2i]` with `stubs[k]`.
26//! 2. Pick `k ← RNG(2i+1, |stubs|−1)`, swap `stubs[2i+1]` with `stubs[k]`.
27//! 3. Let `from = stubs[2i]`, `to = stubs[2i+1]`. If `from == to`
28//!    (self-loop) **or** the pair is already adjacent, fail the attempt,
29//!    clear adjacency, restart the inner loop. Otherwise record the
30//!    edge in the adjacency tracker and continue.
31//!
32//! This mirrors the C `configuration_simple_undirected_set` /
33//! `configuration_simple_undirected_bitset` helpers; we use a single
34//! `Vec<HashSet<u32>>` backend, which is what `_set` uses internally
35//! (the `_bitset` variant in C is only a memory-saving alternative for
36//! `vcount ≤ 1024` and offers no algorithmic advantage).
37//!
38//! ## Directed branch
39//!
40//! Build `out_stubs` and `in_stubs` separately. Only `out_stubs` is
41//! shuffled (one FY swap per edge) — the in-bag is consumed in
42//! construction order, so `to = in_stubs[i]` is monotonically
43//! non-decreasing. This allows an `O(1)` multi-arc test via a
44//! `vertex_done_mark` counter trick: whenever `to` advances, bump the
45//! mark; a duplicate source is detected when `vertex_done[from] ==
46//! current_mark`.
47//!
48//! ## vs. siblings
49//!
50//! * [`crate::degree_sequence_game_configuration`] (ALGO-GN-024) — no
51//!   simplicity check; faster but produces a multigraph.
52//! * [`crate::degree_sequence_game_fast_heur_simple`] (ALGO-GN-026) —
53//!   biased single-pass heuristic with back-off-and-continue; 21–54×
54//!   faster but NOT uniform on the simple-graph space.
55//! * [`crate::degree_sequence_game_vl`] (ALGO-GN-025) — Viger–Latapy
56//!   uniform sampler on the **simple connected** subspace; trades the
57//!   connectivity guarantee for an MCMC mixing phase.
58//!
59//! ## Determinism
60//!
61//! A single `SplitMix64` seed drives every shuffle and every retry.
62//! The PRNG is not bitwise portable to igraph C / `NumPy` / R, so the
63//! three-source conformance harness asserts structural invariants only
64//! (vcount, ecount = Σd/2 or Σout, exact out/in degree match,
65//! simplicity).
66//!
67//! ## Failure modes
68//!
69//! Non-graphical input is rejected up front by Erdős–Gallai (undirected)
70//! or Fulkerson–Chen–Anstee (directed). For sequences that pass the
71//! graphicality test but happen to be hostile to the rejection sampler
72//! (very dense, near-regular sequences), the attempt-restart counter is
73//! bounded by [`MAX_OUTER_ATTEMPTS`]; the function returns
74//! `InvalidArgument` once exhausted.
75
76#![allow(
77    unknown_lints,
78    clippy::cast_possible_truncation,
79    clippy::cast_sign_loss,
80    clippy::many_single_char_names,
81    clippy::needless_range_loop
82)]
83
84use std::collections::HashSet;
85
86use crate::algorithms::games::degree_sequence_fast_heur::{
87    checked_sum, is_graphical_simple_directed, is_graphical_simple_undirected,
88};
89use crate::core::rng::SplitMix64;
90use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
91
92/// Cap on attempt restarts before giving up.
93pub const MAX_OUTER_ATTEMPTS: u32 = 1024;
94
95/// Sample `k` uniformly from `[low, high]` inclusive. Mirrors the C
96/// reference's `RNG_INTEGER(low, high)` semantics.
97fn rng_integer_inclusive(rng: &mut SplitMix64, low: usize, high: usize) -> usize {
98    debug_assert!(low <= high, "rng_integer_inclusive: low ≤ high");
99    let span = (high - low) as u64 + 1;
100    low + (rng.next_u64() % span) as usize
101}
102
103fn build_stubs(degrees: &[u32], total: usize) -> Vec<u32> {
104    let mut stubs: Vec<u32> = Vec::with_capacity(total);
105    for (i, &d) in degrees.iter().enumerate() {
106        let v = i as u32;
107        for _ in 0..d {
108            stubs.push(v);
109        }
110    }
111    stubs
112}
113
114fn run_undirected(
115    degrees: &[u32],
116    n: u32,
117    rng: &mut SplitMix64,
118) -> IgraphResult<Vec<(VertexId, VertexId)>> {
119    let stub_count_u64 = checked_sum(degrees)?;
120    if stub_count_u64 % 2 != 0 {
121        return Err(IgraphError::InvalidArgument(
122            "degree_sequence_game_configuration_simple: undirected degree sum must be even"
123                .to_string(),
124        ));
125    }
126    let stub_count = stub_count_u64 as usize;
127    let ecount = stub_count / 2;
128    if ecount == 0 {
129        return Ok(Vec::new());
130    }
131
132    let mut stubs = build_stubs(degrees, stub_count);
133    let vcount = n as usize;
134    let mut adjacency: Vec<HashSet<u32>> = (0..vcount)
135        .map(|i| HashSet::with_capacity(degrees[i] as usize))
136        .collect();
137
138    for _attempt in 0..MAX_OUTER_ATTEMPTS {
139        let mut success = true;
140
141        for i in 0..ecount {
142            // Two independent Fisher–Yates picks in the un-consumed tail.
143            let k1 = rng_integer_inclusive(rng, 2 * i, stub_count - 1);
144            stubs.swap(2 * i, k1);
145            let k2 = rng_integer_inclusive(rng, 2 * i + 1, stub_count - 1);
146            stubs.swap(2 * i + 1, k2);
147
148            let from = stubs[2 * i];
149            let to = stubs[2 * i + 1];
150
151            if from == to {
152                success = false;
153                break;
154            }
155            if adjacency[to as usize].contains(&from) {
156                success = false;
157                break;
158            }
159            adjacency[to as usize].insert(from);
160            adjacency[from as usize].insert(to);
161        }
162
163        if success {
164            let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(ecount);
165            for i in 0..ecount {
166                edges.push((stubs[2 * i], stubs[2 * i + 1]));
167            }
168            return Ok(edges);
169        }
170
171        for set in &mut adjacency {
172            set.clear();
173        }
174    }
175
176    Err(IgraphError::InvalidArgument(format!(
177        "degree_sequence_game_configuration_simple: exhausted {MAX_OUTER_ATTEMPTS} rejection-sampling restarts; the sequence is graphical but very hostile to rejection sampling — try degree_sequence_game_fast_heur_simple or degree_sequence_game_vl",
178    )))
179}
180
181fn run_directed(
182    out_degrees: &[u32],
183    in_degrees: &[u32],
184    n: u32,
185    rng: &mut SplitMix64,
186) -> IgraphResult<Vec<(VertexId, VertexId)>> {
187    let out_total = checked_sum(out_degrees)? as usize;
188    let in_total = checked_sum(in_degrees)? as usize;
189    if out_total != in_total {
190        return Err(IgraphError::InvalidArgument(
191            "degree_sequence_game_configuration_simple: directed sums Σout and Σin must match"
192                .to_string(),
193        ));
194    }
195    let ecount = out_total;
196    if ecount == 0 {
197        return Ok(Vec::new());
198    }
199
200    let mut out_stubs = build_stubs(out_degrees, ecount);
201    let in_stubs = build_stubs(in_degrees, ecount);
202    let vcount = n as usize;
203
204    // Per-source mark: bumped whenever `to` advances. A source is a
205    // duplicate of the current target iff its stored mark equals the
206    // current `vertex_done_mark`. This avoids clearing the array between
207    // targets — clear via a fresh mark instead.
208    let mut vertex_done: Vec<u64> = vec![0u64; vcount];
209    let mut vertex_done_mark: u64 = 0;
210
211    for _attempt in 0..MAX_OUTER_ATTEMPTS {
212        let mut success = true;
213        let mut previous_to: i64 = -1;
214
215        for i in 0..ecount {
216            let k = rng_integer_inclusive(rng, i, ecount - 1);
217            out_stubs.swap(i, k);
218
219            let from = out_stubs[i];
220            let to = in_stubs[i];
221
222            if to == from {
223                success = false;
224                break;
225            }
226
227            if i64::from(to) != previous_to {
228                vertex_done_mark = vertex_done_mark.wrapping_add(1);
229                previous_to = i64::from(to);
230            }
231
232            if vertex_done[from as usize] == vertex_done_mark {
233                success = false;
234                break;
235            }
236            vertex_done[from as usize] = vertex_done_mark;
237        }
238
239        if success {
240            let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(ecount);
241            for i in 0..ecount {
242                edges.push((out_stubs[i], in_stubs[i]));
243            }
244            return Ok(edges);
245        }
246        // No need to clear `vertex_done` between attempts: the mark is
247        // bumped per target, and the wrap-on-overflow guarantee makes
248        // false positives statistically negligible until 2^64 marks
249        // (which we cannot reach within MAX_OUTER_ATTEMPTS · ecount).
250    }
251
252    Err(IgraphError::InvalidArgument(format!(
253        "degree_sequence_game_configuration_simple: exhausted {MAX_OUTER_ATTEMPTS} rejection-sampling restarts; the directed sequence pair is graphical but very hostile to rejection sampling",
254    )))
255}
256
257/// Uniform random simple graph realising the given degree sequence via
258/// the configuration-model rejection sampler (ALGO-GN-027).
259///
260/// Returns a [`Graph`] guaranteed to be simple (no self-loops, no
261/// multi-edges / multi-arcs) and to exactly match the prescribed
262/// out-degrees (and in-degrees in the directed case). Connectivity is
263/// *not* guaranteed — use [`crate::degree_sequence_game_vl`] for
264/// uniform-connected sampling.
265///
266/// # Arguments
267///
268/// * `out_degrees` — for undirected mode, the degree of each vertex;
269///   for directed mode, the out-degree of each vertex.
270/// * `in_degrees`:
271///   * `None` → undirected graph; requires `Σ out_degrees` to be even
272///     and the sequence to satisfy Erdős–Gallai.
273///   * `Some(in_seq)` → directed graph; requires `in_seq.len() ==
274///     out_degrees.len()`, `Σ in_seq == Σ out_degrees`, and the pair to
275///     satisfy Fulkerson–Chen–Anstee.
276/// * `seed` — drives a `SplitMix64` PRNG; the same `(out_degrees,
277///   in_degrees, seed)` triple always produces the same graph.
278///
279/// # Errors
280///
281/// Returns `IgraphError::InvalidArgument` if the input sequence is
282/// non-graphical or if the rejection sampler exhausts
283/// [`MAX_OUTER_ATTEMPTS`] without finding a simple realisation.
284///
285/// # Examples
286///
287/// ```
288/// use rust_igraph::degree_sequence_game_configuration_simple;
289///
290/// // 4-cycle: every vertex degree 2 ⇒ 4 simple edges.
291/// let g = degree_sequence_game_configuration_simple(&[2, 2, 2, 2], None, 17).unwrap();
292/// assert_eq!(g.vcount(), 4);
293/// assert_eq!(g.ecount(), 4);
294/// assert!(!g.is_directed());
295/// ```
296pub fn degree_sequence_game_configuration_simple(
297    out_degrees: &[u32],
298    in_degrees: Option<&[u32]>,
299    seed: u64,
300) -> IgraphResult<Graph> {
301    let directed = in_degrees.is_some();
302    let n = u32::try_from(out_degrees.len())
303        .map_err(|_| IgraphError::Internal("vertex count exceeds u32"))?;
304    if let Some(in_seq) = in_degrees {
305        if in_seq.len() != out_degrees.len() {
306            return Err(IgraphError::InvalidArgument(
307                "degree_sequence_game_configuration_simple: out_degrees and in_degrees must have the same length".to_string(),
308            ));
309        }
310    }
311
312    if directed {
313        let Some(in_seq) = in_degrees else {
314            return Err(IgraphError::InvalidArgument(
315                "directed graph requires in_degrees".to_string(),
316            ));
317        };
318        if !is_graphical_simple_directed(out_degrees, in_seq) {
319            return Err(IgraphError::InvalidArgument(
320                "degree_sequence_game_configuration_simple: degree pair is not realisable as a simple directed graph (Fulkerson–Chen–Anstee)"
321                    .to_string(),
322            ));
323        }
324    } else if !is_graphical_simple_undirected(out_degrees) {
325        return Err(IgraphError::InvalidArgument(
326            "degree_sequence_game_configuration_simple: degree sequence is not realisable as a simple undirected graph (Erdős–Gallai)"
327                .to_string(),
328        ));
329    }
330
331    let mut rng = SplitMix64::new(seed);
332
333    let edges = if directed {
334        let Some(in_seq) = in_degrees else {
335            return Err(IgraphError::InvalidArgument(
336                "directed graph requires in_degrees".to_string(),
337            ));
338        };
339        run_directed(out_degrees, in_seq, n, &mut rng)?
340    } else {
341        run_undirected(out_degrees, n, &mut rng)?
342    };
343
344    let mut g = Graph::new(n, directed)?;
345    g.add_edges(edges)?;
346    Ok(g)
347}
348
349#[cfg(test)]
350mod tests {
351    use super::*;
352    use crate::algorithms::properties::{SimpleMode, is_simple_with_mode};
353
354    fn observed_degrees(g: &Graph) -> Vec<u32> {
355        let n = g.vcount() as usize;
356        let mut deg = vec![0u32; n];
357        let ec = u32::try_from(g.ecount()).expect("ecount fits u32");
358        for eid in 0..ec {
359            let (s, t) = g.edge(eid).expect("eid in bounds");
360            deg[s as usize] = deg[s as usize].saturating_add(1);
361            deg[t as usize] = deg[t as usize].saturating_add(1);
362        }
363        deg
364    }
365
366    fn observed_out_in(g: &Graph) -> (Vec<u32>, Vec<u32>) {
367        let n = g.vcount() as usize;
368        let mut out = vec![0u32; n];
369        let mut inv = vec![0u32; n];
370        let ec = u32::try_from(g.ecount()).expect("ecount fits u32");
371        for eid in 0..ec {
372            let (s, t) = g.edge(eid).expect("eid in bounds");
373            out[s as usize] = out[s as usize].saturating_add(1);
374            inv[t as usize] = inv[t as usize].saturating_add(1);
375        }
376        (out, inv)
377    }
378
379    #[test]
380    fn undirected_empty_sequence_yields_empty_graph() {
381        let g = degree_sequence_game_configuration_simple(&[], None, 1).expect("empty ok");
382        assert_eq!(g.vcount(), 0);
383        assert_eq!(g.ecount(), 0);
384        assert!(!g.is_directed());
385    }
386
387    #[test]
388    fn undirected_singleton_zero_yields_isolated_vertex() {
389        let g = degree_sequence_game_configuration_simple(&[0], None, 1).expect("singleton ok");
390        assert_eq!(g.vcount(), 1);
391        assert_eq!(g.ecount(), 0);
392    }
393
394    #[test]
395    fn undirected_all_isolated_n5_yields_no_edges() {
396        let g = degree_sequence_game_configuration_simple(&[0; 5], None, 42).expect("ok");
397        assert_eq!(g.vcount(), 5);
398        assert_eq!(g.ecount(), 0);
399    }
400
401    #[test]
402    fn undirected_4cycle_preserves_degrees_and_is_simple() {
403        let g = degree_sequence_game_configuration_simple(&[2, 2, 2, 2], None, 7).expect("ok");
404        assert_eq!(g.vcount(), 4);
405        assert_eq!(g.ecount(), 4);
406        assert_eq!(observed_degrees(&g), vec![2, 2, 2, 2]);
407        assert!(is_simple_with_mode(&g, SimpleMode::DirectedAsDirected).unwrap());
408    }
409
410    #[test]
411    fn undirected_3regular_n6_preserves_degrees() {
412        let degrees: Vec<u32> = vec![3; 6];
413        let g = degree_sequence_game_configuration_simple(&degrees, None, 0xABCD_u64).expect("ok");
414        assert_eq!(observed_degrees(&g), degrees);
415        assert!(is_simple_with_mode(&g, SimpleMode::DirectedAsDirected).unwrap());
416    }
417
418    #[test]
419    fn undirected_skewed_powerlaw_preserves_degrees_sorted() {
420        let degrees: Vec<u32> = vec![5, 4, 4, 3, 3, 3, 2, 2, 2, 2];
421        let g = degree_sequence_game_configuration_simple(&degrees, None, 0xC0FE_u64).expect("ok");
422        assert_eq!(observed_degrees(&g), degrees);
423        assert!(is_simple_with_mode(&g, SimpleMode::DirectedAsDirected).unwrap());
424    }
425
426    #[test]
427    fn undirected_3regular_n30_preserves_degrees() {
428        let degrees: Vec<u32> = vec![3; 30];
429        let g =
430            degree_sequence_game_configuration_simple(&degrees, None, 0xDEAD_F00D_u64).expect("ok");
431        assert_eq!(observed_degrees(&g), degrees);
432        assert!(is_simple_with_mode(&g, SimpleMode::DirectedAsDirected).unwrap());
433    }
434
435    #[test]
436    fn undirected_odd_sum_rejected() {
437        let err = degree_sequence_game_configuration_simple(&[1, 1, 1], None, 1).unwrap_err();
438        matches!(err, IgraphError::InvalidArgument(_));
439    }
440
441    #[test]
442    fn undirected_negative_eg_rejected_max_too_large() {
443        // n=4, max degree 5 > n-1=3 — EG fails at k=1.
444        let err = degree_sequence_game_configuration_simple(&[5, 3, 1, 1], None, 1).unwrap_err();
445        matches!(err, IgraphError::InvalidArgument(_));
446    }
447
448    #[test]
449    fn deterministic_same_seed_undirected() {
450        let degrees: Vec<u32> = vec![3; 8];
451        let g1 = degree_sequence_game_configuration_simple(&degrees, None, 4242).expect("ok");
452        let g2 = degree_sequence_game_configuration_simple(&degrees, None, 4242).expect("ok");
453        let mut e1: Vec<(u32, u32)> = (0..u32::try_from(g1.ecount()).unwrap())
454            .map(|i| {
455                let (a, b) = g1.edge(i).unwrap();
456                if a < b { (a, b) } else { (b, a) }
457            })
458            .collect();
459        let mut e2: Vec<(u32, u32)> = (0..u32::try_from(g2.ecount()).unwrap())
460            .map(|i| {
461                let (a, b) = g2.edge(i).unwrap();
462                if a < b { (a, b) } else { (b, a) }
463            })
464            .collect();
465        e1.sort_unstable();
466        e2.sort_unstable();
467        assert_eq!(e1, e2);
468    }
469
470    #[test]
471    fn directed_empty_sequence_yields_empty_graph() {
472        let g = degree_sequence_game_configuration_simple(&[], Some(&[]), 1).expect("ok");
473        assert_eq!(g.vcount(), 0);
474        assert_eq!(g.ecount(), 0);
475        assert!(g.is_directed());
476    }
477
478    #[test]
479    fn directed_2cycle_preserves_in_out() {
480        let g = degree_sequence_game_configuration_simple(&[1, 1], Some(&[1, 1]), 9).expect("ok");
481        let (out, inv) = observed_out_in(&g);
482        assert_eq!(out, vec![1, 1]);
483        assert_eq!(inv, vec![1, 1]);
484        assert!(is_simple_with_mode(&g, SimpleMode::DirectedAsDirected).unwrap());
485    }
486
487    #[test]
488    fn directed_balanced_n6_d2_preserves_degrees() {
489        let n = 6;
490        let out = vec![2u32; n];
491        let inv = vec![2u32; n];
492        let g =
493            degree_sequence_game_configuration_simple(&out, Some(&inv), 0xC0DE_u64).expect("ok");
494        let (got_out, got_in) = observed_out_in(&g);
495        assert_eq!(got_out, out);
496        assert_eq!(got_in, inv);
497        assert!(is_simple_with_mode(&g, SimpleMode::DirectedAsDirected).unwrap());
498    }
499
500    #[test]
501    fn directed_unequal_sums_rejected() {
502        let err =
503            degree_sequence_game_configuration_simple(&[1, 1, 1], Some(&[1, 1, 0]), 1).unwrap_err();
504        matches!(err, IgraphError::InvalidArgument(_));
505    }
506
507    #[test]
508    fn directed_length_mismatch_rejected() {
509        let err =
510            degree_sequence_game_configuration_simple(&[1, 1], Some(&[1, 1, 0]), 1).unwrap_err();
511        matches!(err, IgraphError::InvalidArgument(_));
512    }
513
514    #[test]
515    fn directed_deterministic_same_seed() {
516        let out = vec![2u32; 5];
517        let inv = vec![2u32; 5];
518        let g1 = degree_sequence_game_configuration_simple(&out, Some(&inv), 12345).expect("ok");
519        let g2 = degree_sequence_game_configuration_simple(&out, Some(&inv), 12345).expect("ok");
520        let e1: Vec<(u32, u32)> = (0..u32::try_from(g1.ecount()).unwrap())
521            .map(|i| g1.edge(i).unwrap())
522            .collect();
523        let e2: Vec<(u32, u32)> = (0..u32::try_from(g2.ecount()).unwrap())
524            .map(|i| g2.edge(i).unwrap())
525            .collect();
526        assert_eq!(e1, e2);
527    }
528}
529
530#[cfg(all(test, feature = "proptest-harness"))]
531mod proptest_invariants {
532    use super::*;
533    use proptest::prelude::*;
534
535    // Restrict the strategy to *moderate-density* graphical sequences. The
536    // configuration_simple rejection sampler is exact-uniform but its
537    // expected attempt count grows roughly as `exp(O((Σd/n)²))`, so dense
538    // near-regular sequences blow past MAX_OUTER_ATTEMPTS = 1024 in
539    // practice. The dense regime is exactly what the fast-heur (GN-026)
540    // and VL (GN-025) siblings cover; the proptest fixtures here check
541    // the property contract in the regime where this sampler is the
542    // right tool.
543    fn graphical_undirected_strategy() -> impl Strategy<Value = Vec<u32>> {
544        (4usize..=8).prop_flat_map(|n| {
545            let cap = ((n as u32) / 2).max(1);
546            prop::collection::vec(0u32..=cap, n).prop_filter(
547                "must be graphical (even sum + EG)",
548                move |seq| {
549                    let sum: u64 = seq.iter().map(|&d| u64::from(d)).sum();
550                    sum % 2 == 0 && is_graphical_simple_undirected(seq)
551                },
552            )
553        })
554    }
555
556    proptest! {
557        #[test]
558        fn degrees_preserved_undirected(seq in graphical_undirected_strategy(), seed in any::<u64>()) {
559            let g = degree_sequence_game_configuration_simple(&seq, None, seed)
560                .expect("graphical sequence must succeed");
561            let n = g.vcount() as usize;
562            let mut deg = vec![0u32; n];
563            let ec = u32::try_from(g.ecount()).unwrap();
564            for eid in 0..ec {
565                let (s, t) = g.edge(eid).unwrap();
566                deg[s as usize] += 1;
567                deg[t as usize] += 1;
568            }
569            prop_assert_eq!(deg, seq);
570        }
571
572        #[test]
573        fn simple_no_loops_no_multi_undirected(seq in graphical_undirected_strategy(), seed in any::<u64>()) {
574            use crate::algorithms::properties::{SimpleMode, is_simple_with_mode};
575            let g = degree_sequence_game_configuration_simple(&seq, None, seed)
576                .expect("graphical sequence must succeed");
577            prop_assert!(is_simple_with_mode(&g, SimpleMode::DirectedAsDirected).unwrap());
578        }
579
580        #[test]
581        fn same_seed_same_graph(seq in graphical_undirected_strategy(), seed in any::<u64>()) {
582            let g1 = degree_sequence_game_configuration_simple(&seq, None, seed).unwrap();
583            let g2 = degree_sequence_game_configuration_simple(&seq, None, seed).unwrap();
584            let e1: Vec<(u32, u32)> = (0..u32::try_from(g1.ecount()).unwrap())
585                .map(|i| g1.edge(i).unwrap())
586                .collect();
587            let e2: Vec<(u32, u32)> = (0..u32::try_from(g2.ecount()).unwrap())
588                .map(|i| g2.edge(i).unwrap())
589                .collect();
590            prop_assert_eq!(e1, e2);
591        }
592    }
593}