1#![allow(
59 unknown_lints,
60 clippy::cast_possible_truncation,
61 clippy::cast_precision_loss,
62 clippy::cast_sign_loss,
63 clippy::many_single_char_names,
64 clippy::needless_range_loop,
65 clippy::manual_contains
66)]
67
68use std::collections::BTreeSet;
69
70use crate::core::rng::SplitMix64;
71use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
72
73const MAX_OUTER_ATTEMPTS: u32 = 1024;
75
76pub(crate) fn checked_sum(degrees: &[u32]) -> IgraphResult<u64> {
78 let mut acc: u64 = 0;
79 for &d in degrees {
80 acc = acc
81 .checked_add(u64::from(d))
82 .ok_or(IgraphError::Internal("degree-sum overflow"))?;
83 }
84 Ok(acc)
85}
86
87pub(crate) fn is_graphical_simple_undirected(degrees: &[u32]) -> bool {
89 let n = degrees.len();
90 if n == 0 {
91 return true;
92 }
93 let sum: u64 = degrees.iter().map(|&d| u64::from(d)).sum();
94 if sum % 2 != 0 {
95 return false;
96 }
97 let n_u64 = n as u64;
98 if degrees.iter().any(|&d| u64::from(d) >= n_u64) {
99 return false;
100 }
101 let mut d_desc: Vec<u32> = degrees.to_vec();
102 d_desc.sort_by(|a, b| b.cmp(a));
103 let mut left_sum: u64 = 0;
104 for k in 1..=n {
105 left_sum = left_sum.saturating_add(u64::from(d_desc[k - 1]));
106 let k_u64 = k as u64;
107 let bound_right: u64 = d_desc[k..].iter().map(|&d| u64::from(d).min(k_u64)).sum();
108 let rhs = k_u64
109 .saturating_mul(k_u64.saturating_sub(1))
110 .saturating_add(bound_right);
111 if left_sum > rhs {
112 return false;
113 }
114 }
115 true
116}
117
118pub(crate) fn is_graphical_simple_directed(out_degrees: &[u32], in_degrees: &[u32]) -> bool {
125 let n = out_degrees.len();
126 if n != in_degrees.len() {
127 return false;
128 }
129 if n == 0 {
130 return true;
131 }
132 let out_sum: u64 = out_degrees.iter().map(|&d| u64::from(d)).sum();
133 let in_sum: u64 = in_degrees.iter().map(|&d| u64::from(d)).sum();
134 if out_sum != in_sum {
135 return false;
136 }
137 let n_u64 = n as u64;
138 if out_degrees.iter().any(|&d| u64::from(d) >= n_u64) {
139 return false;
140 }
141 if in_degrees.iter().any(|&d| u64::from(d) >= n_u64) {
142 return false;
143 }
144 let mut idx: Vec<usize> = (0..n).collect();
147 idx.sort_by(|&i, &j| {
148 out_degrees[j]
149 .cmp(&out_degrees[i])
150 .then_with(|| in_degrees[j].cmp(&in_degrees[i]))
151 });
152 let a: Vec<u32> = idx.iter().map(|&i| out_degrees[i]).collect();
153 let b: Vec<u32> = idx.iter().map(|&i| in_degrees[i]).collect();
154 let mut left_sum: u64 = 0;
155 for k in 1..=n {
156 left_sum = left_sum.saturating_add(u64::from(a[k - 1]));
157 let k_u64 = k as u64;
158 let cap_left: u64 = (0..k)
160 .map(|i| u64::from(b[i]).min(k_u64.saturating_sub(1)))
161 .sum();
162 let cap_right: u64 = (k..n).map(|i| u64::from(b[i]).min(k_u64)).sum();
163 let rhs = cap_left.saturating_add(cap_right);
164 if left_sum > rhs {
165 return false;
166 }
167 }
168 true
169}
170
171fn fisher_yates<T>(slice: &mut [T], rng: &mut SplitMix64) {
175 let n = slice.len();
176 if n < 2 {
177 return;
178 }
179 for i in (1..n).rev() {
180 let bound = (i + 1) as u64;
181 let j = (rng.next_u64() % bound) as usize;
182 slice.swap(i, j);
183 }
184}
185
186type Adj = Vec<Vec<u32>>;
191
192fn adj_contains(neis: &[u32], target: u32) -> bool {
193 neis.binary_search(&target).is_ok()
194}
195
196fn adj_insert(neis: &mut Vec<u32>, target: u32) {
197 match neis.binary_search(&target) {
198 Ok(_) => {} Err(pos) => neis.insert(pos, target),
200 }
201}
202
203fn run_undirected(
206 degrees: &[u32],
207 n: u32,
208 rng: &mut SplitMix64,
209) -> IgraphResult<Vec<(VertexId, VertexId)>> {
210 let nu = n as usize;
211 let mut residual: Vec<u32> = degrees.to_vec();
212 let mut adj: Adj = vec![Vec::new(); nu];
213 let mut stubs: Vec<u32> = Vec::with_capacity(checked_sum(degrees)? as usize);
214 let mut incomplete: BTreeSet<u32> = BTreeSet::new();
215
216 for outer in 0..MAX_OUTER_ATTEMPTS {
217 let _ = outer;
218 adj.iter_mut().for_each(Vec::clear);
219 residual.copy_from_slice(degrees);
220 let mut failed = false;
221 let mut finished;
222
223 loop {
224 stubs.clear();
226 for (i, &r) in residual.iter().enumerate() {
227 for _ in 0..r {
228 stubs.push(i as u32);
229 }
230 }
231 residual.fill(0);
233 incomplete.clear();
234
235 fisher_yates(&mut stubs, rng);
236
237 let k = stubs.len();
238 let mut i = 0;
241 while i + 1 < k {
242 let mut from = stubs[i];
243 let mut to = stubs[i + 1];
244 i += 2;
245 if from > to {
246 std::mem::swap(&mut from, &mut to);
247 }
248 if from == to || adj_contains(&adj[from as usize], to) {
249 residual[from as usize] = residual[from as usize].saturating_add(1);
250 residual[to as usize] = residual[to as usize].saturating_add(1);
251 incomplete.insert(from);
252 incomplete.insert(to);
253 } else {
254 adj_insert(&mut adj[from as usize], to);
255 }
256 }
257
258 finished = incomplete.is_empty();
259 if finished {
260 break;
261 }
262
263 let mut feasible = false;
266 let incs: Vec<u32> = incomplete.iter().copied().collect();
267 'outer_check: for ia in 0..incs.len() {
268 let mut from = incs[ia];
269 for ib in (ia + 1)..incs.len() {
270 let mut to = incs[ib];
271 if from > to {
272 std::mem::swap(&mut from, &mut to);
273 }
274 if !adj_contains(&adj[from as usize], to) {
275 feasible = true;
276 break 'outer_check;
277 }
278 from = incs[ia];
280 }
281 }
282 if !feasible {
283 failed = true;
284 break;
285 }
286 }
288
289 if finished {
290 let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(nu * 2);
292 for (u_idx, neis) in adj.iter().enumerate() {
293 let u = u_idx as u32;
294 for &v in neis {
295 edges.push((u, v));
296 }
297 }
298 return Ok(edges);
299 }
300 let _ = failed;
301 }
303
304 Err(IgraphError::InvalidArgument(
305 "degree_sequence_game_fast_heur_simple: heuristic exhausted attempt cap; sequence appears to be hostile to the fast-heuristic; try the VL sampler".to_string(),
306 ))
307}
308
309fn run_directed(
313 out_degrees: &[u32],
314 in_degrees: &[u32],
315 n: u32,
316 rng: &mut SplitMix64,
317) -> IgraphResult<Vec<(VertexId, VertexId)>> {
318 let nu = n as usize;
319 let mut residual_out: Vec<u32> = out_degrees.to_vec();
320 let mut residual_in: Vec<u32> = in_degrees.to_vec();
321 let mut adj: Adj = vec![Vec::new(); nu];
322 let out_total = checked_sum(out_degrees)? as usize;
323 let in_total = checked_sum(in_degrees)? as usize;
324 if out_total != in_total {
325 return Err(IgraphError::InvalidArgument(
326 "degree_sequence_game_fast_heur_simple: directed mode requires Σout == Σin".to_string(),
327 ));
328 }
329 let mut out_stubs: Vec<u32> = Vec::with_capacity(out_total);
330 let mut in_stubs: Vec<u32> = Vec::with_capacity(in_total);
331 let mut incomplete_out: BTreeSet<u32> = BTreeSet::new();
332 let mut incomplete_in: BTreeSet<u32> = BTreeSet::new();
333
334 for outer in 0..MAX_OUTER_ATTEMPTS {
335 let _ = outer;
336 adj.iter_mut().for_each(Vec::clear);
337 residual_out.copy_from_slice(out_degrees);
338 residual_in.copy_from_slice(in_degrees);
339 let mut failed = false;
340 let mut finished;
341
342 loop {
343 out_stubs.clear();
344 in_stubs.clear();
345 for (i, &r) in residual_out.iter().enumerate() {
346 for _ in 0..r {
347 out_stubs.push(i as u32);
348 }
349 }
350 for (i, &r) in residual_in.iter().enumerate() {
351 for _ in 0..r {
352 in_stubs.push(i as u32);
353 }
354 }
355 residual_out.fill(0);
356 residual_in.fill(0);
357 incomplete_out.clear();
358 incomplete_in.clear();
359
360 fisher_yates(&mut out_stubs, rng);
361
362 let k = out_stubs.len();
363 for i in 0..k {
364 let from = out_stubs[i];
365 let to = in_stubs[i];
366 if from == to || adj_contains(&adj[from as usize], to) {
367 residual_out[from as usize] = residual_out[from as usize].saturating_add(1);
368 residual_in[to as usize] = residual_in[to as usize].saturating_add(1);
369 incomplete_out.insert(from);
370 incomplete_in.insert(to);
371 } else {
372 adj_insert(&mut adj[from as usize], to);
373 }
374 }
375
376 finished = incomplete_out.is_empty();
377 if finished {
378 break;
379 }
380 let mut feasible = false;
383 'outer_check: for &from in &incomplete_out {
384 for &to in &incomplete_in {
385 if from != to && !adj_contains(&adj[from as usize], to) {
386 feasible = true;
387 break 'outer_check;
388 }
389 }
390 }
391 if !feasible {
392 failed = true;
393 break;
394 }
395 }
396
397 if finished {
398 let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(out_total);
399 for (u_idx, neis) in adj.iter().enumerate() {
400 let u = u_idx as u32;
401 for &v in neis {
402 edges.push((u, v));
403 }
404 }
405 return Ok(edges);
406 }
407 let _ = failed;
408 }
409
410 Err(IgraphError::InvalidArgument(
411 "degree_sequence_game_fast_heur_simple: heuristic exhausted attempt cap (directed); sequence appears to be hostile to the fast-heuristic".to_string(),
412 ))
413}
414
415pub fn degree_sequence_game_fast_heur_simple(
453 out_degrees: &[u32],
454 in_degrees: Option<&[u32]>,
455 seed: u64,
456) -> IgraphResult<Graph> {
457 let directed = in_degrees.is_some();
458 let n = u32::try_from(out_degrees.len())
459 .map_err(|_| IgraphError::Internal("vertex count exceeds u32"))?;
460 if let Some(in_seq) = in_degrees {
461 if in_seq.len() != out_degrees.len() {
462 return Err(IgraphError::InvalidArgument(
463 "degree_sequence_game_fast_heur_simple: out_degrees and in_degrees must have the same length".to_string(),
464 ));
465 }
466 }
467
468 if directed {
470 let Some(in_seq) = in_degrees else {
471 return Err(IgraphError::InvalidArgument(
472 "directed graph requires in_degrees".to_string(),
473 ));
474 };
475 if !is_graphical_simple_directed(out_degrees, in_seq) {
476 return Err(IgraphError::InvalidArgument(
477 "degree_sequence_game_fast_heur_simple: degree pair is not realisable as a simple directed graph (Fulkerson–Chen–Anstee)"
478 .to_string(),
479 ));
480 }
481 } else if !is_graphical_simple_undirected(out_degrees) {
482 return Err(IgraphError::InvalidArgument(
483 "degree_sequence_game_fast_heur_simple: degree sequence is not realisable as a simple undirected graph (Erdős–Gallai)".to_string(),
484 ));
485 }
486
487 let mut rng = SplitMix64::new(seed);
488
489 let edges = if directed {
490 let Some(in_seq) = in_degrees else {
491 return Err(IgraphError::InvalidArgument(
492 "directed graph requires in_degrees".to_string(),
493 ));
494 };
495 run_directed(out_degrees, in_seq, n, &mut rng)?
496 } else {
497 run_undirected(out_degrees, n, &mut rng)?
498 };
499
500 let mut g = Graph::new(n, directed)?;
501 g.add_edges(edges)?;
502 Ok(g)
503}
504
505#[cfg(test)]
506mod tests {
507 use super::*;
508 use crate::algorithms::properties::{SimpleMode, is_simple_with_mode};
509
510 fn observed_degrees(g: &Graph) -> Vec<u32> {
511 let n = g.vcount() as usize;
512 let mut deg = vec![0u32; n];
513 let ec = u32::try_from(g.ecount()).expect("ecount fits u32");
514 for eid in 0..ec {
515 let (s, t) = g.edge(eid).expect("eid in bounds");
516 deg[s as usize] = deg[s as usize].saturating_add(1);
517 deg[t as usize] = deg[t as usize].saturating_add(1);
518 }
519 deg
520 }
521
522 fn observed_out_in(g: &Graph) -> (Vec<u32>, Vec<u32>) {
523 let n = g.vcount() as usize;
524 let mut out = vec![0u32; n];
525 let mut inv = vec![0u32; n];
526 let ec = u32::try_from(g.ecount()).expect("ecount fits u32");
527 for eid in 0..ec {
528 let (s, t) = g.edge(eid).expect("eid in bounds");
529 out[s as usize] = out[s as usize].saturating_add(1);
530 inv[t as usize] = inv[t as usize].saturating_add(1);
531 }
532 (out, inv)
533 }
534
535 #[test]
536 fn undirected_empty_sequence_yields_empty_graph() {
537 let g = degree_sequence_game_fast_heur_simple(&[], None, 1).expect("empty ok");
538 assert_eq!(g.vcount(), 0);
539 assert_eq!(g.ecount(), 0);
540 assert!(!g.is_directed());
541 }
542
543 #[test]
544 fn undirected_singleton_zero_yields_isolated_vertex() {
545 let g = degree_sequence_game_fast_heur_simple(&[0], None, 1).expect("singleton ok");
546 assert_eq!(g.vcount(), 1);
547 assert_eq!(g.ecount(), 0);
548 }
549
550 #[test]
551 fn undirected_4cycle_preserves_degrees_and_is_simple() {
552 let g = degree_sequence_game_fast_heur_simple(&[2, 2, 2, 2], None, 7).expect("ok");
553 assert_eq!(g.vcount(), 4);
554 assert_eq!(g.ecount(), 4);
555 assert_eq!(observed_degrees(&g), vec![2, 2, 2, 2]);
556 assert!(is_simple_with_mode(&g, SimpleMode::DirectedAsDirected).unwrap());
557 }
558
559 #[test]
560 fn undirected_3regular_n6_preserves_degrees() {
561 let degrees: Vec<u32> = vec![3; 6];
562 let g = degree_sequence_game_fast_heur_simple(°rees, None, 0xABCD_u64).expect("ok");
563 assert_eq!(observed_degrees(&g), degrees);
564 assert!(is_simple_with_mode(&g, SimpleMode::DirectedAsDirected).unwrap());
565 }
566
567 #[test]
568 fn undirected_complete_k4_preserves_degrees() {
569 let g = degree_sequence_game_fast_heur_simple(&[3, 3, 3, 3], None, 42).expect("ok");
570 assert_eq!(g.ecount(), 6);
571 assert_eq!(observed_degrees(&g), vec![3, 3, 3, 3]);
572 assert!(is_simple_with_mode(&g, SimpleMode::DirectedAsDirected).unwrap());
573 }
574
575 #[test]
576 fn undirected_odd_sum_rejected() {
577 let err = degree_sequence_game_fast_heur_simple(&[1, 1, 1], None, 1).unwrap_err();
578 match err {
579 IgraphError::InvalidArgument(msg) => assert!(msg.contains("Erdős–Gallai")),
580 other => panic!("unexpected error: {other:?}"),
581 }
582 }
583
584 #[test]
585 fn undirected_degree_exceeds_n_minus_1_rejected() {
586 let err = degree_sequence_game_fast_heur_simple(&[5, 3, 1, 1], None, 1).unwrap_err();
590 match err {
591 IgraphError::InvalidArgument(msg) => assert!(msg.contains("Erdős–Gallai")),
592 other => panic!("unexpected error: {other:?}"),
593 }
594 }
595
596 #[test]
597 fn undirected_non_graphical_rejected() {
598 let err = degree_sequence_game_fast_heur_simple(&[3, 3, 3, 1], None, 1).unwrap_err();
602 match err {
603 IgraphError::InvalidArgument(msg) => assert!(msg.contains("Erdős–Gallai")),
604 other => panic!("unexpected error: {other:?}"),
605 }
606 }
607
608 #[test]
609 fn undirected_skewed_sequence_preserves_degrees() {
610 let degrees: Vec<u32> = vec![5, 4, 4, 3, 3, 3, 2, 2, 2, 2];
611 let g = degree_sequence_game_fast_heur_simple(°rees, None, 123).expect("ok");
612 assert_eq!(observed_degrees(&g), degrees);
613 assert!(is_simple_with_mode(&g, SimpleMode::DirectedAsDirected).unwrap());
614 }
615
616 #[test]
617 fn undirected_same_seed_same_graph() {
618 let degrees: Vec<u32> = vec![3, 3, 3, 3, 2, 2];
619 let a = degree_sequence_game_fast_heur_simple(°rees, None, 9).expect("a");
620 let b = degree_sequence_game_fast_heur_simple(°rees, None, 9).expect("b");
621 let ea: Vec<(u32, u32)> = (0..a.ecount() as u32)
622 .map(|e| a.edge(e).expect("eid"))
623 .collect();
624 let eb: Vec<(u32, u32)> = (0..b.ecount() as u32)
625 .map(|e| b.edge(e).expect("eid"))
626 .collect();
627 assert_eq!(ea, eb);
628 }
629
630 #[test]
631 fn undirected_distinct_seeds_likely_differ() {
632 let degrees: Vec<u32> = vec![3; 10];
633 let mut a_edges: Vec<(u32, u32)> = (0..)
634 .take(
635 degree_sequence_game_fast_heur_simple(°rees, None, 1)
636 .expect("a")
637 .ecount() as usize,
638 )
639 .map(|_| (0u32, 0u32))
640 .collect();
641 let g_a = degree_sequence_game_fast_heur_simple(°rees, None, 1).expect("a");
642 for e in 0..g_a.ecount() as u32 {
643 a_edges[e as usize] = g_a.edge(e).expect("eid");
644 }
645 let g_b = degree_sequence_game_fast_heur_simple(°rees, None, 2).expect("b");
646 let b_edges: Vec<(u32, u32)> = (0..g_b.ecount() as u32)
647 .map(|e| g_b.edge(e).expect("eid"))
648 .collect();
649 assert_ne!(a_edges, b_edges);
651 }
652
653 #[test]
656 fn directed_balanced_d1_n4_cycle() {
657 let g = degree_sequence_game_fast_heur_simple(&[1, 1, 1, 1], Some(&[1, 1, 1, 1]), 1)
659 .expect("ok");
660 assert!(g.is_directed());
661 assert_eq!(g.vcount(), 4);
662 assert_eq!(g.ecount(), 4);
663 let (out, inv) = observed_out_in(&g);
664 assert_eq!(out, vec![1, 1, 1, 1]);
665 assert_eq!(inv, vec![1, 1, 1, 1]);
666 assert!(is_simple_with_mode(&g, SimpleMode::DirectedAsDirected).unwrap());
667 }
668
669 #[test]
670 fn directed_skewed_n5_preserves_both_degrees() {
671 let out_seq: Vec<u32> = vec![2, 2, 2, 1, 1];
672 let in_seq: Vec<u32> = vec![1, 1, 2, 2, 2];
673 let g = degree_sequence_game_fast_heur_simple(&out_seq, Some(&in_seq), 0x5EED).expect("ok");
674 let (out, inv) = observed_out_in(&g);
675 assert_eq!(out, out_seq);
676 assert_eq!(inv, in_seq);
677 assert!(is_simple_with_mode(&g, SimpleMode::DirectedAsDirected).unwrap());
678 }
679
680 #[test]
681 fn directed_sum_mismatch_rejected() {
682 let err =
683 degree_sequence_game_fast_heur_simple(&[1, 1, 1], Some(&[1, 1, 0]), 1).unwrap_err();
684 match err {
685 IgraphError::InvalidArgument(msg) => {
686 assert!(msg.contains("Fulkerson") || msg.contains("Σout"));
687 }
688 other => panic!("unexpected error: {other:?}"),
689 }
690 }
691
692 #[test]
693 fn directed_length_mismatch_rejected() {
694 let err = degree_sequence_game_fast_heur_simple(&[1, 1, 1], Some(&[1, 1]), 1).unwrap_err();
695 match err {
696 IgraphError::InvalidArgument(msg) => assert!(msg.contains("length")),
697 other => panic!("unexpected error: {other:?}"),
698 }
699 }
700
701 #[test]
702 fn directed_self_loops_avoided_when_residuals_permit() {
703 let g = degree_sequence_game_fast_heur_simple(&[1, 1], Some(&[1, 1]), 1).expect("ok");
705 assert_eq!(g.ecount(), 2);
706 assert!(is_simple_with_mode(&g, SimpleMode::DirectedAsDirected).unwrap());
707 }
708
709 #[test]
710 fn directed_3regular_n5_preserves_degrees() {
711 let degrees: Vec<u32> = vec![3; 5];
712 let g =
713 degree_sequence_game_fast_heur_simple(°rees, Some(°rees), 0xC0DE).expect("ok");
714 let (out, inv) = observed_out_in(&g);
715 assert_eq!(out, degrees);
716 assert_eq!(inv, degrees);
717 assert!(is_simple_with_mode(&g, SimpleMode::DirectedAsDirected).unwrap());
718 }
719}
720
721#[cfg(all(test, feature = "proptest-harness"))]
722mod proptests {
723 use super::*;
724 use crate::algorithms::properties::{SimpleMode, is_simple_with_mode};
725 use proptest::prelude::*;
726
727 fn observed_degrees(g: &Graph) -> Vec<u32> {
728 let n = g.vcount() as usize;
729 let mut deg = vec![0u32; n];
730 let ec = u32::try_from(g.ecount()).expect("ecount fits u32");
731 for eid in 0..ec {
732 let (s, t) = g.edge(eid).expect("eid in bounds");
733 deg[s as usize] = deg[s as usize].saturating_add(1);
734 deg[t as usize] = deg[t as usize].saturating_add(1);
735 }
736 deg
737 }
738
739 fn arb_undirected_degseq() -> impl Strategy<Value = Vec<u32>> {
740 (3usize..=10).prop_flat_map(|n| prop::collection::vec(0u32..(n as u32), n))
741 }
742
743 proptest! {
744 #![proptest_config(ProptestConfig { cases: 64, ..ProptestConfig::default() })]
745
746 #[test]
747 fn degrees_preserved_when_graphical(seq in arb_undirected_degseq(), seed in any::<u64>()) {
748 let result = degree_sequence_game_fast_heur_simple(&seq, None, seed);
749 match result {
750 Ok(g) => {
751 prop_assert_eq!(observed_degrees(&g), seq);
752 prop_assert!(is_simple_with_mode(&g, SimpleMode::DirectedAsDirected).unwrap());
753 }
754 Err(IgraphError::InvalidArgument(msg)) => {
755 prop_assert!(
757 msg.contains("Erdős–Gallai") || msg.contains("attempt cap"),
758 "unexpected rejection: {}",
759 msg
760 );
761 }
762 Err(other) => prop_assert!(false, "unexpected error: {:?}", other),
763 }
764 }
765
766 #[test]
767 fn same_seed_same_graph(seq in arb_undirected_degseq(), seed in any::<u64>()) {
768 let a = degree_sequence_game_fast_heur_simple(&seq, None, seed);
769 let b = degree_sequence_game_fast_heur_simple(&seq, None, seed);
770 match (a, b) {
771 (Ok(ga), Ok(gb)) => {
772 let ea: Vec<(u32, u32)> = (0..ga.ecount() as u32)
773 .map(|e| ga.edge(e).expect("eid"))
774 .collect();
775 let eb: Vec<(u32, u32)> = (0..gb.ecount() as u32)
776 .map(|e| gb.edge(e).expect("eid"))
777 .collect();
778 prop_assert_eq!(ea, eb);
779 }
780 (Err(_), Err(_)) => {}
781 (a, b) => prop_assert!(false, "determinism violated: {:?} vs {:?}", a, b),
782 }
783 }
784 }
785}