1#![allow(
39 unknown_lints,
40 clippy::cast_possible_truncation,
41 clippy::cast_possible_wrap,
42 clippy::cast_precision_loss,
43 clippy::cast_sign_loss,
44 clippy::float_cmp,
45 clippy::many_single_char_names,
46 clippy::manual_midpoint,
47 clippy::too_many_lines
48)]
49
50use crate::algorithms::games::erdos_renyi::erdos_renyi_gnp;
51use crate::algorithms::properties::is_simple::is_simple;
52use crate::core::rng::SplitMix64;
53use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
54
55fn u_code(from: VertexId, to: VertexId) -> u64 {
58 let (lo, hi) = if from < to { (from, to) } else { (to, from) };
59 let hi64 = u64::from(hi);
60 let lo64 = u64::from(lo);
61 hi64 * (hi64 - 1) / 2 + lo64
62}
63
64fn d_code(from: VertexId, to: VertexId, n: u32) -> u64 {
67 let n64 = u64::from(n);
68 let from64 = u64::from(from);
69 let to64 = u64::from(to);
70 if to64 == n64 - 1 {
75 from64 * n64 + from64
76 } else {
77 to64 * n64 + from64
78 }
79}
80
81fn u_decode(code: u64) -> (VertexId, VertexId) {
84 let code_f = code as f64;
88 let to_f = ((8.0 * code_f + 1.0).sqrt() + 1.0) / 2.0;
89 let mut to = to_f.trunc() as u64;
90 if to < 1 {
91 to = 1;
92 }
93 let mut from = code - to * (to - 1) / 2;
94 while from >= to {
95 to += 1;
96 from = code - to * (to - 1) / 2;
97 }
98 debug_assert!(from < to);
99 (from as VertexId, to as VertexId)
100}
101
102fn d_decode(code: u64, n: u32) -> (VertexId, VertexId) {
105 let n64 = u64::from(n);
106 let to0 = code / n64;
107 let from = code - to0 * n64;
108 let to = if from == to0 { n64 - 1 } else { to0 };
111 debug_assert!(from < n64 && to < n64 && from != to);
112 (from as VertexId, to as VertexId)
113}
114
115fn code(from: VertexId, to: VertexId, n: u32, directed: bool) -> u64 {
117 if directed {
118 d_code(from, to, n)
119 } else {
120 u_code(from, to)
121 }
122}
123
124fn decode(c: u64, n: u32, directed: bool) -> (VertexId, VertexId) {
126 if directed {
127 d_decode(c, n)
128 } else {
129 u_decode(c)
130 }
131}
132
133fn slot_count(n: u32, directed: bool) -> u64 {
135 let n64 = u64::from(n);
136 if n64 < 2 {
137 0
138 } else if directed {
139 n64 * (n64 - 1)
140 } else {
141 n64 * (n64 - 1) / 2
142 }
143}
144
145fn coded_edges(old_graph: &Graph) -> IgraphResult<Vec<(VertexId, VertexId, u64)>> {
148 let n = old_graph.vcount();
149 let directed = old_graph.is_directed();
150 let m = u32::try_from(old_graph.ecount()).map_err(|_| {
151 IgraphError::Internal("correlated_game: edge count exceeds u32 — bug in caller")
152 })?;
153 let mut out: Vec<(VertexId, VertexId, u64)> = Vec::with_capacity(m as usize);
154 for eid in 0..m {
155 let (f, t) = old_graph.edge(eid)?;
156 out.push((f, t, code(f, t, n, directed)));
157 }
158 out.sort_unstable_by_key(|&(_, _, c)| c);
159 Ok(out)
160}
161
162fn compute_conditionals(corr: f64, p: f64) -> (f64, f64) {
165 let q = p + corr * (1.0 - p);
166 let p_del = 1.0 - q;
167 let p_add = (1.0 - q) * (p / (1.0 - p));
168 let p_del = p_del.clamp(0.0, 1.0);
170 let p_add = p_add.clamp(0.0, 1.0);
171 (p_del, p_add)
172}
173
174fn apply_permutation(
180 edges: &mut [(VertexId, VertexId)],
181 permutation: &[VertexId],
182) -> IgraphResult<()> {
183 let n = permutation.len();
184 let n_u32 = u32::try_from(n)
185 .map_err(|_| IgraphError::Internal("correlated_game: permutation length exceeds u32"))?;
186 let mut inv: Vec<i64> = vec![-1; n];
187 for (i, &v) in permutation.iter().enumerate() {
188 if v >= n_u32 {
189 return Err(IgraphError::InvalidArgument(format!(
190 "correlated_game: permutation entry {v} out of range for n = {n}"
191 )));
192 }
193 let slot = v as usize;
194 if inv[slot] != -1 {
195 return Err(IgraphError::InvalidArgument(format!(
196 "correlated_game: permutation entry {v} appears more than once"
197 )));
198 }
199 inv[slot] = i as i64;
200 }
201 for &val in &inv {
203 if val == -1 {
204 return Err(IgraphError::InvalidArgument(
205 "correlated_game: permutation is not a bijection".to_string(),
206 ));
207 }
208 }
209 for edge in edges.iter_mut() {
210 edge.0 = inv[edge.0 as usize] as VertexId;
211 edge.1 = inv[edge.1 as usize] as VertexId;
212 }
213 Ok(())
214}
215
216fn skip_indices(rng: &mut SplitMix64, prob: f64, cap: u64) -> Vec<u64> {
219 if prob <= 0.0 || cap == 0 {
220 return Vec::new();
221 }
222 let cap_f = cap as f64;
223 let mut last = rng.gen_geom(prob);
224 let mut out: Vec<u64> = Vec::new();
225 while last < cap_f {
226 let idx = last.trunc() as u64;
227 if idx >= cap {
228 break;
229 }
230 out.push(idx);
231 last += rng.gen_geom(prob);
232 last += 1.0;
233 }
234 out
235}
236
237pub fn correlated_game(
269 old_graph: &Graph,
270 corr: f64,
271 p: f64,
272 permutation: Option<&[VertexId]>,
273 seed: u64,
274) -> IgraphResult<Graph> {
275 if !corr.is_finite() || !(0.0..=1.0).contains(&corr) {
276 return Err(IgraphError::InvalidArgument(format!(
277 "correlated_game: corr must be in [0, 1] (got {corr})"
278 )));
279 }
280 if !p.is_finite() || p <= 0.0 || p >= 1.0 {
281 return Err(IgraphError::InvalidArgument(format!(
282 "correlated_game: p must be in the open interval (0, 1) (got {p})"
283 )));
284 }
285
286 let n = old_graph.vcount();
287 let directed = old_graph.is_directed();
288
289 if let Some(perm) = permutation {
290 if perm.len() != n as usize {
291 return Err(IgraphError::InvalidArgument(format!(
292 "correlated_game: permutation length {} does not match vertex count {}",
293 perm.len(),
294 n
295 )));
296 }
297 }
298
299 if !is_simple(old_graph)? {
301 return Err(IgraphError::InvalidArgument(
302 "correlated_game: old_graph must be a simple graph".to_string(),
303 ));
304 }
305
306 if corr == 0.0 {
308 let mut g = erdos_renyi_gnp(n, p, directed, false, seed)?;
311 if let Some(perm) = permutation {
312 apply_permutation_in_place(&mut g, perm)?;
313 }
314 return Ok(g);
315 }
316 if corr == 1.0 {
317 let mut edges: Vec<(VertexId, VertexId)> =
319 (0..u32::try_from(old_graph.ecount()).map_err(|_| {
320 IgraphError::Internal("correlated_game: ecount exceeds u32 in corr=1 branch")
321 })?)
322 .map(|eid| old_graph.edge(eid))
323 .collect::<IgraphResult<Vec<_>>>()?;
324 if let Some(perm) = permutation {
325 apply_permutation(&mut edges, perm)?;
326 }
327 let mut g = Graph::new(n, directed)?;
328 g.add_edges(edges)?;
329 return Ok(g);
330 }
331
332 let (p_del, p_add) = compute_conditionals(corr, p);
333 let coded = coded_edges(old_graph)?;
334 let m = coded.len() as u64;
335 let cap = slot_count(n, directed);
336 let missing = cap - m;
337
338 let mut rng = SplitMix64::new(seed);
339
340 let delete_positions = skip_indices(&mut rng, p_del, m);
343 let mut delete_codes: Vec<u64> = delete_positions
344 .into_iter()
345 .map(|pos| coded[pos as usize].2)
346 .collect();
347 delete_codes.sort_unstable();
348
349 let add_positions = skip_indices(&mut rng, p_add, missing);
354
355 let mut new_edges: Vec<(VertexId, VertexId)> =
358 Vec::with_capacity(coded.len() + add_positions.len());
359 let mut p_e: usize = 0;
360 let mut p_d: usize = 0;
361 let mut p_a: usize = 0;
362 let no_e = coded.len();
365 let no_d = delete_codes.len();
366 let no_a = add_positions.len();
367 let inf: u128 = u128::MAX;
368 let mut next_e: u128 = if p_e < no_e {
369 u128::from(coded[p_e].2)
370 } else {
371 inf
372 };
373 let mut next_a: u128 = if p_a < no_a {
374 u128::from(add_positions[p_a]) + u128::from(p_e as u64)
375 } else {
376 inf
377 };
378 let mut next_d: u128 = if p_d < no_d {
379 u128::from(delete_codes[p_d])
380 } else {
381 inf
382 };
383 while next_e != inf || next_a != inf || next_d != inf {
384 if next_e <= next_a && next_e < next_d {
385 let (f, t, _) = coded[p_e];
387 new_edges.push((f, t));
388 p_e += 1;
389 next_e = if p_e < no_e {
390 u128::from(coded[p_e].2)
391 } else {
392 inf
393 };
394 next_a = if p_a < no_a {
396 u128::from(add_positions[p_a]) + u128::from(p_e as u64)
397 } else {
398 inf
399 };
400 } else if next_e <= next_a && next_e == next_d {
401 p_e += 1;
403 next_e = if p_e < no_e {
404 u128::from(coded[p_e].2)
405 } else {
406 inf
407 };
408 next_a = if p_a < no_a {
409 u128::from(add_positions[p_a]) + u128::from(p_e as u64)
410 } else {
411 inf
412 };
413 p_d += 1;
414 next_d = if p_d < no_d {
415 u128::from(delete_codes[p_d])
416 } else {
417 inf
418 };
419 } else {
420 let code64 = u64::try_from(next_a)
422 .map_err(|_| IgraphError::Internal("correlated_game: add-code overflow"))?;
423 let (f, t) = decode(code64, n, directed);
424 new_edges.push((f, t));
425 p_a += 1;
426 next_a = if p_a < no_a {
427 u128::from(add_positions[p_a]) + u128::from(p_e as u64)
428 } else {
429 inf
430 };
431 }
432 }
433
434 if let Some(perm) = permutation {
435 apply_permutation(&mut new_edges, perm)?;
436 }
437
438 let mut g = Graph::new(n, directed)?;
439 g.add_edges(new_edges)?;
440 Ok(g)
441}
442
443fn apply_permutation_in_place(g: &mut Graph, perm: &[VertexId]) -> IgraphResult<()> {
448 let m = u32::try_from(g.ecount())
449 .map_err(|_| IgraphError::Internal("correlated_game: ecount exceeds u32"))?;
450 let mut edges: Vec<(VertexId, VertexId)> = (0..m)
451 .map(|eid| g.edge(eid))
452 .collect::<IgraphResult<Vec<_>>>()?;
453 apply_permutation(&mut edges, perm)?;
454 let n = g.vcount();
455 let directed = g.is_directed();
456 let mut fresh = Graph::new(n, directed)?;
457 fresh.add_edges(edges)?;
458 *g = fresh;
459 Ok(())
460}
461
462pub fn correlated_pair_game(
492 n: u32,
493 corr: f64,
494 p: f64,
495 directed: bool,
496 permutation: Option<&[VertexId]>,
497 seed: u64,
498) -> IgraphResult<(Graph, Graph)> {
499 let g1 = erdos_renyi_gnp(n, p, directed, false, seed)?;
500 let g2_seed = seed.wrapping_add(0x9E37_79B9_7F4A_7C15);
504 let g2 = correlated_game(&g1, corr, p, permutation, g2_seed)?;
505 Ok((g1, g2))
506}
507
508#[cfg(test)]
509mod tests {
510 use super::*;
511
512 fn ecount_of(g: &Graph) -> usize {
513 g.ecount()
514 }
515
516 fn edge_set(g: &Graph) -> std::collections::HashSet<(VertexId, VertexId)> {
519 let mut s = std::collections::HashSet::new();
520 let m = u32::try_from(g.ecount()).unwrap();
521 for eid in 0..m {
522 let (f, t) = g.edge(eid).unwrap();
523 let key = if g.is_directed() || f < t {
524 (f, t)
525 } else {
526 (t, f)
527 };
528 s.insert(key);
529 }
530 s
531 }
532
533 fn assert_no_self_loops(g: &Graph) {
534 let m = u32::try_from(g.ecount()).unwrap();
535 for eid in 0..m {
536 let (f, t) = g.edge(eid).unwrap();
537 assert_ne!(f, t, "self-loop at edge {eid}");
538 }
539 }
540
541 fn assert_simple(g: &Graph) {
542 assert!(is_simple(g).unwrap(), "not simple");
543 }
544
545 #[test]
546 fn u_code_round_trip() {
547 for to in 1u32..30 {
548 for from in 0..to {
549 let c = u_code(from, to);
550 let (f2, t2) = u_decode(c);
551 assert_eq!((f2, t2), (from, to), "u_code({from}, {to}) = {c}");
552 }
553 }
554 }
555
556 #[test]
557 fn d_code_round_trip() {
558 for n in [2u32, 3, 5, 8, 13] {
559 for from in 0..n {
560 for to in 0..n {
561 if from == to {
562 continue;
563 }
564 let c = d_code(from, to, n);
565 let cap = u64::from(n) * (u64::from(n) - 1);
566 assert!(c < cap, "d_code({from}, {to}, {n}) = {c} >= cap {cap}");
567 let (f2, t2) = d_decode(c, n);
568 assert_eq!((f2, t2), (from, to), "d_code({from}, {to}, {n}) = {c}");
569 }
570 }
571 }
572 }
573
574 #[test]
575 fn d_code_is_bijection() {
576 for n in [2u32, 3, 5, 10] {
578 let cap = u64::from(n) * (u64::from(n) - 1);
579 let mut seen = std::collections::HashSet::new();
580 for c in 0..cap {
581 let (f, t) = d_decode(c, n);
582 assert_ne!(f, t);
583 assert!(seen.insert((f, t)), "duplicate decoded pair at code {c}");
584 }
585 assert_eq!(seen.len(), cap as usize);
586 }
587 }
588
589 #[test]
590 fn empty_old_graph_returns_empty_new() {
591 let old = Graph::new(0, false).unwrap();
592 let new_g = correlated_game(&old, 0.5, 0.5, None, 1).unwrap();
593 assert_eq!(new_g.vcount(), 0);
594 assert_eq!(new_g.ecount(), 0);
595 }
596
597 #[test]
598 fn singleton_old_graph_returns_singleton() {
599 let old = Graph::new(1, false).unwrap();
600 let new_g = correlated_game(&old, 0.5, 0.5, None, 1).unwrap();
601 assert_eq!(new_g.vcount(), 1);
602 assert_eq!(new_g.ecount(), 0);
603 }
604
605 #[test]
606 fn corr_zero_independent_density() {
607 let old = erdos_renyi_gnp(50, 0.5, false, false, 7).unwrap();
610 let new_g = correlated_game(&old, 0.0, 0.5, None, 11).unwrap();
611 assert_eq!(new_g.vcount(), old.vcount());
612 let overlap = edge_set(&old).intersection(&edge_set(&new_g)).count();
614 assert!(
615 (200..420).contains(&overlap),
616 "independent overlap should hover near 306, got {overlap}"
617 );
618 }
619
620 #[test]
621 fn corr_one_exact_copy() {
622 let old = erdos_renyi_gnp(40, 0.3, false, false, 5).unwrap();
623 let new_g = correlated_game(&old, 1.0, 0.3, None, 11).unwrap();
624 assert_eq!(new_g.vcount(), old.vcount());
625 assert_eq!(new_g.ecount(), old.ecount());
626 assert_eq!(edge_set(&new_g), edge_set(&old));
627 }
628
629 #[test]
630 fn corr_one_with_permutation_relabels() {
631 let old = erdos_renyi_gnp(10, 0.4, false, false, 3).unwrap();
632 let n = old.vcount();
634 let perm: Vec<VertexId> = (0..n).rev().collect();
635 let new_g = correlated_game(&old, 1.0, 0.4, Some(&perm), 17).unwrap();
636 assert_eq!(new_g.ecount(), old.ecount());
637 let mut expected = std::collections::HashSet::new();
639 let m = u32::try_from(old.ecount()).unwrap();
640 for eid in 0..m {
641 let (f, t) = old.edge(eid).unwrap();
642 let (fn_, tn) = (n - 1 - f, n - 1 - t);
643 let key = if fn_ < tn { (fn_, tn) } else { (tn, fn_) };
644 expected.insert(key);
645 }
646 assert_eq!(edge_set(&new_g), expected);
647 }
648
649 #[test]
650 fn high_corr_preserves_most_edges() {
651 let old = erdos_renyi_gnp(100, 0.2, false, false, 23).unwrap();
652 let new_g = correlated_game(&old, 0.95, 0.2, None, 31).unwrap();
653 let intersection = edge_set(&old).intersection(&edge_set(&new_g)).count();
654 let frac = intersection as f64 / ecount_of(&old) as f64;
655 assert!(
656 frac > 0.85,
657 "corr=0.95 should preserve >85% of old edges, got {frac:.3}"
658 );
659 }
660
661 #[test]
662 fn low_corr_preserves_few_edges() {
663 let old = erdos_renyi_gnp(100, 0.2, false, false, 23).unwrap();
664 let new_g = correlated_game(&old, 0.1, 0.2, None, 31).unwrap();
665 let intersection = edge_set(&old).intersection(&edge_set(&new_g)).count();
666 let frac = intersection as f64 / ecount_of(&old) as f64;
667 assert!(
670 frac < 0.45,
671 "corr=0.1 should preserve <45% of old edges, got {frac:.3}"
672 );
673 }
674
675 #[test]
676 fn output_is_simple_no_self_loops() {
677 let old = erdos_renyi_gnp(40, 0.3, false, false, 9).unwrap();
678 let new_g = correlated_game(&old, 0.5, 0.3, None, 13).unwrap();
679 assert_no_self_loops(&new_g);
680 assert_simple(&new_g);
681 }
682
683 #[test]
684 fn deterministic_same_seed() {
685 let old = erdos_renyi_gnp(30, 0.3, false, false, 1).unwrap();
686 let a = correlated_game(&old, 0.5, 0.3, None, 999).unwrap();
687 let b = correlated_game(&old, 0.5, 0.3, None, 999).unwrap();
688 assert_eq!(edge_set(&a), edge_set(&b));
689 }
690
691 #[test]
692 fn different_seed_different_output() {
693 let old = erdos_renyi_gnp(30, 0.3, false, false, 1).unwrap();
694 let a = correlated_game(&old, 0.5, 0.3, None, 999).unwrap();
695 let b = correlated_game(&old, 0.5, 0.3, None, 1000).unwrap();
696 assert_ne!(edge_set(&a), edge_set(&b));
697 }
698
699 #[test]
700 fn directed_input_yields_directed_output() {
701 let old = erdos_renyi_gnp(20, 0.3, true, false, 5).unwrap();
702 let new_g = correlated_game(&old, 0.5, 0.3, None, 7).unwrap();
703 assert!(new_g.is_directed());
704 assert_no_self_loops(&new_g);
705 assert_simple(&new_g);
706 }
707
708 #[test]
709 fn invalid_corr_rejected() {
710 let old = erdos_renyi_gnp(5, 0.3, false, false, 1).unwrap();
711 assert!(correlated_game(&old, -0.1, 0.3, None, 1).is_err());
712 assert!(correlated_game(&old, 1.1, 0.3, None, 1).is_err());
713 assert!(correlated_game(&old, f64::NAN, 0.3, None, 1).is_err());
714 }
715
716 #[test]
717 fn invalid_p_rejected() {
718 let old = erdos_renyi_gnp(5, 0.3, false, false, 1).unwrap();
719 assert!(correlated_game(&old, 0.5, 0.0, None, 1).is_err());
720 assert!(correlated_game(&old, 0.5, 1.0, None, 1).is_err());
721 assert!(correlated_game(&old, 0.5, -0.1, None, 1).is_err());
722 assert!(correlated_game(&old, 0.5, f64::NAN, None, 1).is_err());
723 }
724
725 #[test]
726 fn invalid_permutation_rejected() {
727 let old = erdos_renyi_gnp(5, 0.3, false, false, 1).unwrap();
728 let bad_len: Vec<VertexId> = vec![0, 1, 2];
730 assert!(correlated_game(&old, 0.5, 0.3, Some(&bad_len), 1).is_err());
731 let dup: Vec<VertexId> = vec![0, 1, 2, 1, 4];
733 assert!(correlated_game(&old, 0.5, 0.3, Some(&dup), 1).is_err());
734 let oor: Vec<VertexId> = vec![0, 1, 2, 3, 99];
736 assert!(correlated_game(&old, 0.5, 0.3, Some(&oor), 1).is_err());
737 }
738
739 #[test]
740 fn non_simple_old_rejected() {
741 let mut old = Graph::new(3, false).unwrap();
742 old.add_edge(0, 1).unwrap();
743 old.add_edge(0, 1).unwrap(); assert!(correlated_game(&old, 0.5, 0.3, None, 1).is_err());
745 }
746
747 #[test]
748 fn correlated_pair_basic() {
749 let (g1, g2) = correlated_pair_game(50, 0.8, 0.3, false, None, 42).unwrap();
750 assert_eq!(g1.vcount(), 50);
751 assert_eq!(g2.vcount(), 50);
752 assert_simple(&g1);
753 assert_simple(&g2);
754 let cap = 50.0 * 49.0 / 2.0;
756 let baseline = 0.3_f64.powi(2) * cap;
757 let overlap = edge_set(&g1).intersection(&edge_set(&g2)).count() as f64;
758 assert!(
759 overlap > baseline * 1.5,
760 "high-corr overlap {overlap} should exceed baseline {baseline}"
761 );
762 }
763
764 #[test]
765 fn correlated_pair_directed() {
766 let (g1, g2) = correlated_pair_game(30, 0.5, 0.3, true, None, 7).unwrap();
767 assert!(g1.is_directed());
768 assert!(g2.is_directed());
769 assert_simple(&g1);
770 assert_simple(&g2);
771 }
772}
773
774#[cfg(all(test, feature = "proptest-harness"))]
775mod prop_tests {
776 use super::*;
777 use proptest::prelude::*;
778
779 proptest! {
780 #[test]
781 fn vcount_preserved(n in 2u32..40, p in 0.05f64..0.95, corr in 0.0f64..=1.0, seed in any::<u64>()) {
782 let old = erdos_renyi_gnp(n, p, false, false, seed).unwrap();
783 let new_g = correlated_game(&old, corr, p, None, seed.wrapping_add(1)).unwrap();
784 prop_assert_eq!(new_g.vcount(), n);
785 prop_assert_eq!(new_g.is_directed(), false);
786 }
787
788 #[test]
789 fn always_simple(n in 2u32..30, p in 0.1f64..0.9, corr in 0.0f64..=1.0, seed in any::<u64>()) {
790 let old = erdos_renyi_gnp(n, p, false, false, seed).unwrap();
791 let new_g = correlated_game(&old, corr, p, None, seed.wrapping_add(1)).unwrap();
792 prop_assert!(is_simple(&new_g).unwrap());
793 let m = u32::try_from(new_g.ecount()).unwrap();
794 for eid in 0..m {
795 let (f, t) = new_g.edge(eid).unwrap();
796 prop_assert_ne!(f, t);
797 }
798 }
799
800 #[test]
801 fn corr_one_is_exact_copy(n in 2u32..30, p in 0.1f64..0.9, seed in any::<u64>()) {
802 let old = erdos_renyi_gnp(n, p, false, false, seed).unwrap();
803 let new_g = correlated_game(&old, 1.0, p, None, seed.wrapping_add(1)).unwrap();
804 prop_assert_eq!(new_g.ecount(), old.ecount());
805 }
806
807 #[test]
808 fn deterministic(n in 2u32..30, p in 0.1f64..0.9, corr in 0.0f64..=1.0, seed in any::<u64>()) {
809 let old = erdos_renyi_gnp(n, p, false, false, seed).unwrap();
810 let a = correlated_game(&old, corr, p, None, seed.wrapping_add(1)).unwrap();
811 let b = correlated_game(&old, corr, p, None, seed.wrapping_add(1)).unwrap();
812 prop_assert_eq!(a.ecount(), b.ecount());
813 }
814
815 #[test]
816 fn directed_round_trip(n in 2u32..25, p in 0.1f64..0.9, corr in 0.0f64..=1.0, seed in any::<u64>()) {
817 let old = erdos_renyi_gnp(n, p, true, false, seed).unwrap();
818 let new_g = correlated_game(&old, corr, p, None, seed.wrapping_add(1)).unwrap();
819 prop_assert_eq!(new_g.vcount(), n);
820 prop_assert!(new_g.is_directed());
821 prop_assert!(is_simple(&new_g).unwrap());
822 }
823 }
824}