1#![allow(
62 unknown_lints,
63 clippy::cast_possible_truncation,
64 clippy::cast_precision_loss,
65 clippy::cast_sign_loss,
66 clippy::float_cmp,
67 clippy::similar_names,
68 clippy::many_single_char_names
69)]
70
71use std::collections::HashSet;
72
73use crate::core::rng::SplitMix64;
74use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
75
76const MAX_NODES: usize = if usize::BITS >= 64 {
82 1usize.wrapping_shl(53)
83} else {
84 usize::MAX
85};
86
87fn check_fitness(label: &str, fitness: &[f64]) -> IgraphResult<()> {
88 let mut min_v = f64::INFINITY;
89 let mut max_v = f64::NEG_INFINITY;
90 for (i, &x) in fitness.iter().enumerate() {
91 if x.is_nan() {
92 return Err(IgraphError::InvalidArgument(format!(
93 "{label}[{i}] is NaN; static-fitness scores must be finite"
94 )));
95 }
96 if x < min_v {
97 min_v = x;
98 }
99 if x > max_v {
100 max_v = x;
101 }
102 }
103 if fitness.is_empty() {
104 return Ok(());
105 }
106 if min_v < 0.0 {
107 return Err(IgraphError::InvalidArgument(format!(
108 "{label} must be non-negative; got minimum {min_v}"
109 )));
110 }
111 if !max_v.is_finite() {
112 return Err(IgraphError::InvalidArgument(format!(
113 "{label} must be finite; got maximum {max_v}"
114 )));
115 }
116 Ok(())
117}
118
119fn cumulative_sum(fitness: &[f64]) -> Vec<f64> {
121 let mut cum = Vec::with_capacity(fitness.len());
122 let mut acc = 0.0_f64;
123 for &x in fitness {
124 acc += x;
125 cum.push(acc);
126 }
127 cum
128}
129
130fn binsearch_cum(cum: &[f64], target: f64) -> usize {
133 let mut lo = 0usize;
135 let mut hi = cum.len();
136 while lo < hi {
137 let mid = lo + (hi - lo) / 2;
138 if cum[mid] < target {
139 lo = mid + 1;
140 } else {
141 hi = mid;
142 }
143 }
144 if lo >= cum.len() { cum.len() - 1 } else { lo }
145}
146
147fn count_active(fitness: &[f64]) -> usize {
149 fitness.iter().filter(|&&x| x > 0.0).count()
150}
151
152fn max_edges(fitness_out: &[f64], fitness_in: Option<&[f64]>, loops: bool) -> f64 {
157 let n = fitness_out.len();
158 if n == 0 {
159 return 0.0;
160 }
161 if let Some(fin) = fitness_in {
162 let mut outn = 0usize;
166 let mut inn = 0usize;
167 let mut bothn = 0usize;
168 for i in 0..n {
169 let o = fitness_out[i] != 0.0;
170 let ii = fin[i] != 0.0;
171 if o {
172 outn += 1;
173 }
174 if ii {
175 inn += 1;
176 }
177 if o && ii {
178 bothn += 1;
179 }
180 }
181 let prod = outn as f64 * inn as f64;
182 if loops { prod } else { prod - bothn as f64 }
183 } else {
184 let active = count_active(fitness_out) as f64;
187 if loops {
188 active * (active + 1.0) / 2.0
189 } else {
190 active * (active - 1.0) / 2.0
191 }
192 }
193}
194
195pub fn static_fitness_game(
247 no_of_edges: u32,
248 fitness_out: &[f64],
249 fitness_in: Option<&[f64]>,
250 loops: bool,
251 multiple: bool,
252 seed: u64,
253) -> IgraphResult<Graph> {
254 let n = fitness_out.len();
255 let directed = fitness_in.is_some();
256
257 #[allow(clippy::absurd_extreme_comparisons)]
258 if n > MAX_NODES {
259 return Err(IgraphError::InvalidArgument(format!(
260 "static-fitness vertex count {n} exceeds the largest exactly representable f64 integer (2^53)"
261 )));
262 }
263
264 let n_u32 = u32::try_from(n).map_err(|_| {
265 IgraphError::InvalidArgument(format!("static-fitness vertex count {n} exceeds u32::MAX"))
266 })?;
267
268 if n == 0 {
273 return Graph::new(0, directed);
274 }
275
276 if let Some(fin) = fitness_in {
277 if fin.len() != n {
278 return Err(IgraphError::InvalidArgument(format!(
279 "fitness_in length {} does not match fitness_out length {n}",
280 fin.len()
281 )));
282 }
283 }
284
285 check_fitness("fitness_out", fitness_out)?;
286 if let Some(fin) = fitness_in {
287 check_fitness("fitness_in", fin)?;
288 }
289
290 let max_e = max_edges(fitness_out, fitness_in, loops);
293 if max_e <= 0.0 && no_of_edges > 0 {
294 return Err(IgraphError::InvalidArgument(format!(
295 "No edges are possible with the given fitness scores, but {no_of_edges} edges were requested"
296 )));
297 }
298 if !multiple && f64::from(no_of_edges) > max_e {
299 return Err(IgraphError::InvalidArgument(format!(
300 "Requested {no_of_edges} simple edges but only {max_e} are possible with the given fitness scores"
301 )));
302 }
303
304 if no_of_edges == 0 {
305 return Graph::new(n_u32, directed);
306 }
307
308 let cum_out = cumulative_sum(fitness_out);
310 let Some(&max_out) = cum_out.last() else {
311 return Err(IgraphError::Internal("cum_out unexpectedly empty"));
312 };
313 let (cum_in_storage, cum_in_view, max_in): (Vec<f64>, &[f64], f64) = match fitness_in {
314 Some(fin) => {
315 let c = cumulative_sum(fin);
316 let Some(&m) = c.last() else {
317 return Err(IgraphError::Internal("cum_in unexpectedly empty"));
318 };
319 (c, &[], m)
320 }
321 None => (Vec::new(), &cum_out, max_out),
322 };
323 let cum_in: &[f64] = if directed {
326 &cum_in_storage
327 } else {
328 cum_in_view
329 };
330
331 if max_out <= 0.0 || max_in <= 0.0 {
332 return Graph::new(n_u32, directed);
336 }
337
338 let mut rng = SplitMix64::new(seed);
339 let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(no_of_edges as usize);
340
341 if multiple {
342 let mut remaining = no_of_edges;
344 while remaining > 0 {
345 let from = pick(&cum_out, max_out, &mut rng);
346 let to = pick(cum_in, max_in, &mut rng);
347 if !loops && from == to {
348 continue;
349 }
350 edges.push((from as VertexId, to as VertexId));
351 remaining -= 1;
352 }
353 } else {
354 let mut seen: HashSet<(u32, u32)> = HashSet::with_capacity(no_of_edges as usize);
358 let mut remaining = no_of_edges;
359 while remaining > 0 {
360 let from = pick(&cum_out, max_out, &mut rng);
361 let to = pick(cum_in, max_in, &mut rng);
362 if !loops && from == to {
363 continue;
364 }
365 let key = if directed || from <= to {
366 (from as u32, to as u32)
367 } else {
368 (to as u32, from as u32)
369 };
370 if !seen.insert(key) {
371 continue;
372 }
373 edges.push((from as VertexId, to as VertexId));
374 remaining -= 1;
375 }
376 }
377
378 let mut g = Graph::new(n_u32, directed)?;
379 g.add_edges(edges)?;
380 Ok(g)
381}
382
383#[inline]
384fn pick(cum: &[f64], total: f64, rng: &mut SplitMix64) -> usize {
385 let u = rng.gen_unit();
386 let target = u * total;
387 binsearch_cum(cum, target)
388}
389
390#[allow(clippy::too_many_arguments)]
434pub fn static_power_law_game(
435 no_of_nodes: u32,
436 no_of_edges: u32,
437 exponent_out: f64,
438 exponent_in: Option<f64>,
439 loops: bool,
440 multiple: bool,
441 finite_size_correction: bool,
442 seed: u64,
443) -> IgraphResult<Graph> {
444 if exponent_out.is_nan() {
445 return Err(IgraphError::InvalidArgument(
446 "exponent_out must not be NaN".into(),
447 ));
448 }
449 if exponent_out < 2.0 {
450 return Err(IgraphError::InvalidArgument(format!(
451 "Out-degree exponent must be >= 2, got {exponent_out}"
452 )));
453 }
454 if let Some(e) = exponent_in {
455 if e.is_nan() {
456 return Err(IgraphError::InvalidArgument(
457 "exponent_in must not be NaN".into(),
458 ));
459 }
460 if e < 2.0 {
461 return Err(IgraphError::InvalidArgument(format!(
462 "For directed graphs the in-degree exponent must be >= 2, got {e}"
463 )));
464 }
465 }
466
467 let n = no_of_nodes as usize;
468 let directed = exponent_in.is_some();
469 if n == 0 {
470 return Graph::new(0, directed);
471 }
472
473 let fitness_out = build_power_law_fitness(n, exponent_out, finite_size_correction);
474
475 if let Some(e_in) = exponent_in {
476 let mut fitness_in = build_power_law_fitness(n, e_in, finite_size_correction);
477 let mut shuf_rng = SplitMix64::new(seed.wrapping_add(0xDEAD_BEEF_CAFE_BABE));
480 fisher_yates(&mut fitness_in, &mut shuf_rng);
481 static_fitness_game(
482 no_of_edges,
483 &fitness_out,
484 Some(&fitness_in),
485 loops,
486 multiple,
487 seed,
488 )
489 } else {
490 static_fitness_game(no_of_edges, &fitness_out, None, loops, multiple, seed)
491 }
492}
493
494fn build_power_law_fitness(n: usize, exponent: f64, finite_size_correction: bool) -> Vec<f64> {
497 let alpha = if exponent.is_finite() {
498 -1.0 / (exponent - 1.0)
499 } else {
500 0.0
501 };
502
503 let mut j = n as f64;
504 if finite_size_correction && alpha < -0.5 {
505 let shift = (n as f64).powf(1.0 + 0.5 / alpha)
507 * (10.0 * std::f64::consts::SQRT_2 * (1.0 + alpha)).powf(-1.0 / alpha)
508 - 1.0;
509 j += shift;
510 }
511 if j < n as f64 {
512 j = n as f64;
513 }
514
515 let mut fitness = Vec::with_capacity(n);
516 for _ in 0..n {
517 fitness.push(j.powf(alpha));
518 j -= 1.0;
519 }
520 fitness
521}
522
523fn fisher_yates(v: &mut [f64], rng: &mut SplitMix64) {
525 let n = v.len();
526 if n <= 1 {
527 return;
528 }
529 for i in (1..n).rev() {
530 let j = rng.gen_index(i + 1);
531 v.swap(i, j);
532 }
533}
534
535#[cfg(test)]
536mod tests {
537 use super::*;
538
539 fn deg(g: &Graph, n: usize) -> Vec<u32> {
540 let mut d = vec![0u32; n];
541 let m = u32::try_from(g.ecount()).expect("ecount fits in u32");
542 for eid in 0..m {
543 let (u, v) = g.edge(eid).expect("edge id in bounds");
544 d[u as usize] += 1;
545 if u != v {
546 d[v as usize] += 1;
547 }
548 }
549 d
550 }
551
552 fn directed_in_out(g: &Graph, n: usize) -> (Vec<u32>, Vec<u32>) {
553 let mut out = vec![0u32; n];
554 let mut din = vec![0u32; n];
555 let m = u32::try_from(g.ecount()).expect("ecount fits in u32");
556 for eid in 0..m {
557 let (u, v) = g.edge(eid).expect("edge id in bounds");
558 out[u as usize] += 1;
559 din[v as usize] += 1;
560 }
561 (out, din)
562 }
563
564 #[test]
567 fn zero_vertices_zero_edges_undirected() {
568 let g = static_fitness_game(0, &[], None, false, false, 0).unwrap();
569 assert_eq!(g.vcount(), 0);
570 assert_eq!(g.ecount(), 0);
571 assert!(!g.is_directed());
572 }
573
574 #[test]
575 fn zero_vertices_zero_edges_directed() {
576 let in_w: [f64; 0] = [];
577 let g = static_fitness_game(0, &[], Some(&in_w), true, true, 0).unwrap();
578 assert_eq!(g.vcount(), 0);
579 assert!(g.is_directed());
580 }
581
582 #[test]
583 fn zero_edges_returns_isolated_graph() {
584 let f = vec![1.0; 5];
585 let g = static_fitness_game(0, &f, None, false, false, 0).unwrap();
586 assert_eq!(g.vcount(), 5);
587 assert_eq!(g.ecount(), 0);
588 }
589
590 #[test]
591 fn single_vertex_no_loops_zero_edges_ok() {
592 let g = static_fitness_game(0, &[1.0], None, false, false, 0).unwrap();
593 assert_eq!(g.vcount(), 1);
594 assert_eq!(g.ecount(), 0);
595 }
596
597 #[test]
598 fn single_vertex_no_loops_with_edges_errors() {
599 let err = static_fitness_game(1, &[1.0], None, false, false, 0).unwrap_err();
600 let msg = format!("{err:?}");
601 assert!(msg.contains("No edges are possible"), "{msg}");
602 }
603
604 #[test]
605 fn single_vertex_with_loops_works() {
606 let g = static_fitness_game(3, &[1.0], None, true, true, 0).unwrap();
607 assert_eq!(g.vcount(), 1);
608 assert_eq!(g.ecount(), 3);
609 }
610
611 #[test]
614 fn negative_fitness_rejected() {
615 let f = [1.0, -0.1, 2.0];
616 let err = static_fitness_game(1, &f, None, false, true, 0).unwrap_err();
617 assert!(format!("{err:?}").contains("non-negative"));
618 }
619
620 #[test]
621 fn nan_fitness_rejected() {
622 let f = [1.0, f64::NAN, 2.0];
623 let err = static_fitness_game(1, &f, None, false, true, 0).unwrap_err();
624 assert!(format!("{err:?}").contains("NaN"));
625 }
626
627 #[test]
628 fn infinite_fitness_rejected() {
629 let f = [1.0, f64::INFINITY, 2.0];
630 let err = static_fitness_game(1, &f, None, false, true, 0).unwrap_err();
631 assert!(format!("{err:?}").contains("finite"));
632 }
633
634 #[test]
635 fn fitness_in_length_mismatch_rejected() {
636 let fo = [1.0, 2.0, 3.0];
637 let fi = [1.0, 2.0];
638 let err = static_fitness_game(1, &fo, Some(&fi), false, true, 0).unwrap_err();
639 assert!(format!("{err:?}").contains("length"));
640 }
641
642 #[test]
643 fn too_many_simple_edges_rejected() {
644 let f = vec![1.0; 4];
645 let err = static_fitness_game(7, &f, None, false, false, 0).unwrap_err();
647 assert!(format!("{err:?}").contains("simple edges"));
648 }
649
650 #[test]
651 fn many_multi_edges_accepted() {
652 let f = vec![1.0; 4];
653 let g = static_fitness_game(20, &f, None, false, true, 0).unwrap();
655 assert_eq!(g.ecount(), 20);
656 }
657
658 #[test]
661 fn exact_edge_count_simple() {
662 let f = vec![1.0, 2.0, 3.0, 4.0, 5.0];
663 let g = static_fitness_game(7, &f, None, false, false, 11).unwrap();
664 assert_eq!(g.vcount(), 5);
665 assert_eq!(g.ecount(), 7);
666 assert!(!g.is_directed());
667 }
668
669 #[test]
670 fn no_self_loops_when_loops_false() {
671 let f = vec![1.0; 10];
672 let g = static_fitness_game(15, &f, None, false, true, 12).unwrap();
673 let m = u32::try_from(g.ecount()).unwrap();
674 for eid in 0..m {
675 let (u, v) = g.edge(eid).unwrap();
676 assert_ne!(u, v, "self-loop in edge {eid}");
677 }
678 }
679
680 #[test]
681 fn directed_round_trip_preserves_endpoints() {
682 let fo = vec![1.0, 2.0, 3.0, 4.0];
683 let fi = vec![3.0, 1.0, 2.0, 4.0];
684 let g = static_fitness_game(10, &fo, Some(&fi), false, true, 33).unwrap();
685 assert!(g.is_directed());
686 assert_eq!(g.ecount(), 10);
687 let (out, din) = directed_in_out(&g, 4);
689 assert_eq!(out.iter().sum::<u32>(), 10);
690 assert_eq!(din.iter().sum::<u32>(), 10);
691 }
692
693 #[test]
694 fn determinism_same_seed_same_graph() {
695 let f = vec![1.0, 2.0, 3.0, 4.0, 5.0];
696 let g1 = static_fitness_game(8, &f, None, false, true, 99).unwrap();
697 let g2 = static_fitness_game(8, &f, None, false, true, 99).unwrap();
698 let m = u32::try_from(g1.ecount()).unwrap();
699 for eid in 0..m {
700 assert_eq!(g1.edge(eid).unwrap(), g2.edge(eid).unwrap());
701 }
702 }
703
704 #[test]
705 fn determinism_different_seed_differs() {
706 let f = vec![1.0, 2.0, 3.0, 4.0, 5.0];
707 let g1 = static_fitness_game(8, &f, None, false, true, 1).unwrap();
708 let g2 = static_fitness_game(8, &f, None, false, true, 2).unwrap();
709 let mut e1: Vec<_> = (0..u32::try_from(g1.ecount()).unwrap())
710 .map(|e| g1.edge(e).unwrap())
711 .collect();
712 let mut e2: Vec<_> = (0..u32::try_from(g2.ecount()).unwrap())
713 .map(|e| g2.edge(e).unwrap())
714 .collect();
715 e1.sort_unstable();
716 e2.sort_unstable();
717 assert_ne!(e1, e2);
718 }
719
720 #[test]
721 fn degree_correlates_with_fitness() {
722 let n = 50;
726 let fitness: Vec<f64> = (0..n).map(|i| 1.0 + (i as f64)).collect();
727 let g = static_fitness_game(400, &fitness, None, false, true, 0x00C0_FFEE).unwrap();
728 let d = deg(&g, n);
729 let q = n / 4;
731 let bot: f64 = (0..q).map(|i| f64::from(d[i])).sum::<f64>() / (q as f64);
732 let top: f64 = (n - q..n).map(|i| f64::from(d[i])).sum::<f64>() / (q as f64);
733 assert!(
734 top > 2.0 * bot,
735 "top-quartile mean degree {top} should dominate bottom {bot}"
736 );
737 }
738
739 #[test]
742 fn power_law_basic_undirected() {
743 let g = static_power_law_game(100, 30, 2.0, None, true, true, true, 42).unwrap();
744 assert_eq!(g.vcount(), 100);
745 assert_eq!(g.ecount(), 30);
746 assert!(!g.is_directed());
747 }
748
749 #[test]
750 fn power_law_basic_directed() {
751 let g = static_power_law_game(100, 30, 2.0, Some(2.0), true, true, true, 42).unwrap();
752 assert_eq!(g.vcount(), 100);
753 assert_eq!(g.ecount(), 30);
754 assert!(g.is_directed());
755 }
756
757 #[test]
758 fn power_law_with_only_loops_simple() {
759 let g = static_power_law_game(90, 40, 2.0, None, true, false, true, 7).unwrap();
760 assert_eq!(g.vcount(), 90);
761 assert_eq!(g.ecount(), 40);
762 }
763
764 #[test]
765 fn power_law_with_only_multi() {
766 let g = static_power_law_game(110, 50, 2.0, None, false, true, true, 8).unwrap();
767 assert_eq!(g.vcount(), 110);
768 assert_eq!(g.ecount(), 50);
769 }
770
771 #[test]
772 fn power_law_zero_nodes() {
773 let g = static_power_law_game(0, 0, 2.0, Some(2.0), false, false, true, 0).unwrap();
774 assert_eq!(g.vcount(), 0);
775 assert!(g.is_directed());
776 }
777
778 #[test]
779 fn power_law_zero_edges_undirected() {
780 let g = static_power_law_game(10, 0, 2.0, None, false, false, true, 0).unwrap();
781 assert_eq!(g.vcount(), 10);
782 assert_eq!(g.ecount(), 0);
783 assert!(!g.is_directed());
784 }
785
786 #[test]
787 fn power_law_infinite_exponent_yields_uniform_fitness() {
788 let fitness = build_power_law_fitness(20, f64::INFINITY, false);
792 for &f in &fitness {
793 assert!((f - 1.0).abs() < 1e-12);
794 }
795 }
796
797 #[test]
798 fn power_law_exponent_too_low_rejected() {
799 let err = static_power_law_game(100, 30, 1.5, None, true, true, true, 0).unwrap_err();
800 assert!(format!("{err:?}").contains(">= 2"));
801 }
802
803 #[test]
804 fn power_law_in_exponent_too_low_rejected() {
805 let err = static_power_law_game(100, 30, 2.0, Some(0.5), true, true, true, 0).unwrap_err();
806 assert!(format!("{err:?}").contains(">= 2"));
807 }
808
809 #[test]
810 fn power_law_nan_exponent_rejected() {
811 let err = static_power_law_game(100, 30, f64::NAN, None, true, true, true, 0).unwrap_err();
812 assert!(format!("{err:?}").contains("NaN"));
813 }
814
815 #[test]
818 fn binsearch_cum_finds_first_ge() {
819 let cum = vec![1.0, 3.0, 6.0, 10.0];
820 assert_eq!(binsearch_cum(&cum, 0.0), 0);
821 assert_eq!(binsearch_cum(&cum, 0.5), 0);
822 assert_eq!(binsearch_cum(&cum, 1.0), 0);
823 assert_eq!(binsearch_cum(&cum, 1.5), 1);
824 assert_eq!(binsearch_cum(&cum, 3.0), 1);
825 assert_eq!(binsearch_cum(&cum, 5.5), 2);
826 assert_eq!(binsearch_cum(&cum, 10.0), 3);
827 }
828
829 #[test]
830 fn cumulative_sum_basic() {
831 let v = vec![1.0, 2.0, 3.0];
832 let c = cumulative_sum(&v);
833 assert_eq!(c, vec![1.0, 3.0, 6.0]);
834 }
835
836 #[test]
837 fn max_edges_undirected_no_loops() {
838 let f = vec![1.0; 4];
839 let m = max_edges(&f, None, false);
840 assert!((m - 6.0).abs() < 1e-12);
841 }
842
843 #[test]
844 fn max_edges_undirected_loops() {
845 let f = vec![1.0; 4];
846 let m = max_edges(&f, None, true);
847 assert!((m - 10.0).abs() < 1e-12);
848 }
849
850 #[test]
851 fn max_edges_directed_no_loops() {
852 let fo = vec![1.0; 4];
853 let fi = vec![1.0; 4];
854 let m = max_edges(&fo, Some(&fi), false);
855 assert!((m - 12.0).abs() < 1e-12);
856 }
857}
858
859#[cfg(all(test, feature = "proptest-harness"))]
860mod proptests {
861 use super::*;
862 use proptest::prelude::*;
863
864 proptest! {
865 #![proptest_config(ProptestConfig {
866 cases: 64,
867 ..ProptestConfig::default()
868 })]
869
870 #[test]
873 fn fitness_exact_edge_count(
874 n in 2usize..30,
875 seed in any::<u64>(),
876 edges in 1u32..20,
877 multiple in any::<bool>(),
878 loops in any::<bool>(),
879 ) {
880 let f: Vec<f64> = (0..n).map(|i| 1.0 + i as f64).collect();
881 let cap = if loops {
884 (n * (n + 1) / 2) as u32
885 } else {
886 (n * (n - 1) / 2) as u32
887 };
888 let m = if multiple { edges } else { edges.min(cap.max(1)) };
889 if !multiple && cap == 0 {
890 return Ok(());
891 }
892 let g = static_fitness_game(m, &f, None, loops, multiple, seed).unwrap();
893 prop_assert_eq!(g.vcount(), n as u32);
894 prop_assert_eq!(g.ecount(), m as usize);
895 }
896
897 #[test]
899 fn fitness_no_loops_when_disabled(
900 n in 3usize..20,
901 seed in any::<u64>(),
902 edges in 1u32..15,
903 ) {
904 let f: Vec<f64> = (0..n).map(|i| 1.0 + i as f64).collect();
905 let cap = (n * (n - 1) / 2) as u32;
906 let m = edges.min(cap);
907 let g = static_fitness_game(m, &f, None, false, true, seed).unwrap();
908 let me = u32::try_from(g.ecount()).unwrap();
909 for eid in 0..me {
910 let (u, v) = g.edge(eid).unwrap();
911 prop_assert_ne!(u, v);
912 }
913 }
914
915 #[test]
917 fn power_law_exact_edge_count(
918 n in 5u32..60,
919 edges in 1u32..40,
920 seed in any::<u64>(),
921 ) {
922 let g = static_power_law_game(n, edges, 2.5, None, true, true, true, seed).unwrap();
923 prop_assert_eq!(g.vcount(), n);
924 prop_assert_eq!(g.ecount(), edges as usize);
925 }
926 }
927}