#![allow(
unknown_lints,
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
clippy::cast_sign_loss,
clippy::many_single_char_names,
clippy::needless_range_loop,
clippy::manual_contains,
clippy::double_comparisons
)]
use std::collections::{HashSet, VecDeque};
use crate::core::rng::SplitMix64;
use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
fn checked_sum_u64(degrees: &[u32]) -> IgraphResult<u64> {
let mut acc: u64 = 0;
for &d in degrees {
acc = acc
.checked_add(u64::from(d))
.ok_or(IgraphError::Internal("degree-sum overflow"))?;
}
Ok(acc)
}
fn is_graphical_simple_undirected(degrees: &[u32]) -> bool {
let n = degrees.len();
if n == 0 {
return true;
}
let sum: u64 = degrees.iter().map(|&d| u64::from(d)).sum();
if sum % 2 != 0 {
return false;
}
let n_u64 = n as u64;
if degrees.iter().any(|&d| u64::from(d) >= n_u64) {
return false;
}
let mut d_desc: Vec<u32> = degrees.to_vec();
d_desc.sort_by(|a, b| b.cmp(a));
let mut left_sum: u64 = 0;
let mut right_sum: u64 = d_desc.iter().map(|&d| u64::from(d)).sum();
for k in 1..=n {
left_sum = left_sum
.checked_add(u64::from(d_desc[k - 1]))
.expect("left_sum bounded by Σd ≤ 2|E|");
right_sum = right_sum.saturating_sub(u64::from(d_desc[k - 1]));
let k_u64 = k as u64;
let bound_right: u64 = d_desc[k..].iter().map(|&d| u64::from(d).min(k_u64)).sum();
let rhs = k_u64
.saturating_mul(k_u64.saturating_sub(1))
.saturating_add(bound_right);
if left_sum > rhs {
return false;
}
let _ = right_sum;
}
true
}
fn havel_hakimi_realize(degrees: &[u32], n: u32) -> IgraphResult<Vec<(VertexId, VertexId)>> {
let m: usize = degrees.iter().map(|&d| d as usize).sum::<usize>() / 2;
let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(m);
let mut residual: Vec<(u32, VertexId)> = degrees
.iter()
.enumerate()
.map(|(i, &d)| (d, i as VertexId))
.collect();
loop {
residual.sort_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)));
let Some(&(r, v)) = residual.first() else {
break;
};
if r == 0 {
break;
}
let r_us = r as usize;
if residual.len() - 1 < r_us {
return Err(IgraphError::Internal(
"degree_sequence_game_vl: Havel–Hakimi realization failed (non-graphical input that passed Erdős–Gallai — please report)",
));
}
for j in 1..=r_us {
let (rd, u) = residual[j];
if rd == 0 {
return Err(IgraphError::Internal(
"degree_sequence_game_vl: Havel–Hakimi residual zero (should not happen for graphical input)",
));
}
let (a, b) = if v < u { (v, u) } else { (u, v) };
edges.push((a, b));
residual[j].0 = rd - 1;
}
residual[0].0 = 0;
let _ = n;
}
Ok(edges)
}
struct Adj {
nbr_set: Vec<HashSet<VertexId>>,
edges: Vec<(VertexId, VertexId)>,
}
impl Adj {
fn from_edges(n: u32, edges: Vec<(VertexId, VertexId)>) -> Self {
let mut nbr_set: Vec<HashSet<VertexId>> = (0..n).map(|_| HashSet::new()).collect();
for &(a, b) in &edges {
nbr_set[a as usize].insert(b);
nbr_set[b as usize].insert(a);
}
Self { nbr_set, edges }
}
fn has_edge(&self, u: VertexId, v: VertexId) -> bool {
if self.nbr_set[u as usize].len() <= self.nbr_set[v as usize].len() {
self.nbr_set[u as usize].contains(&v)
} else {
self.nbr_set[v as usize].contains(&u)
}
}
fn swap_edge(
&mut self,
eid: usize,
old_a: VertexId,
old_b: VertexId,
new_a: VertexId,
new_b: VertexId,
) {
self.nbr_set[old_a as usize].remove(&old_b);
self.nbr_set[old_b as usize].remove(&old_a);
self.nbr_set[new_a as usize].insert(new_b);
self.nbr_set[new_b as usize].insert(new_a);
let (a, b) = if new_a < new_b {
(new_a, new_b)
} else {
(new_b, new_a)
};
self.edges[eid] = (a, b);
}
fn snapshot(&self) -> Vec<(VertexId, VertexId)> {
self.edges.clone()
}
fn restore(&mut self, snapshot: Vec<(VertexId, VertexId)>) {
for set in &mut self.nbr_set {
set.clear();
}
for &(a, b) in &snapshot {
self.nbr_set[a as usize].insert(b);
self.nbr_set[b as usize].insert(a);
}
self.edges = snapshot;
}
}
fn is_connected_ignoring_isolated(adj: &Adj) -> bool {
let n = adj.nbr_set.len();
let mut start: Option<usize> = None;
let mut active = 0usize;
for (v, set) in adj.nbr_set.iter().enumerate() {
if !set.is_empty() {
if start.is_none() {
start = Some(v);
}
active += 1;
}
}
let Some(start_v) = start else {
return true; };
let mut visited = vec![false; n];
let mut queue: VecDeque<usize> = VecDeque::with_capacity(active);
visited[start_v] = true;
queue.push_back(start_v);
let mut count = 1usize;
while let Some(v) = queue.pop_front() {
for &nbr in &adj.nbr_set[v] {
let nu = nbr as usize;
if !visited[nu] {
visited[nu] = true;
count += 1;
queue.push_back(nu);
}
}
}
count == active
}
fn label_components(adj: &Adj) -> (Vec<i32>, Vec<usize>) {
let n = adj.nbr_set.len();
let mut comp: Vec<i32> = vec![-1; n];
let mut component_count: i32 = 0;
let mut queue: VecDeque<usize> = VecDeque::new();
for v in 0..n {
if comp[v] != -1 || adj.nbr_set[v].is_empty() {
continue;
}
comp[v] = component_count;
queue.push_back(v);
while let Some(u) = queue.pop_front() {
for &nbr in &adj.nbr_set[u] {
let nu = nbr as usize;
if comp[nu] == -1 {
comp[nu] = component_count;
queue.push_back(nu);
}
}
}
component_count += 1;
}
let mut representative_edge: Vec<usize> = vec![usize::MAX; component_count as usize];
for (eid, &(a, _)) in adj.edges.iter().enumerate() {
let cid = comp[a as usize];
if cid >= 0 && representative_edge[cid as usize] == usize::MAX {
representative_edge[cid as usize] = eid;
}
}
(comp, representative_edge)
}
fn make_connected(adj: &mut Adj, rng: &mut SplitMix64) -> IgraphResult<()> {
if is_connected_ignoring_isolated(adj) {
return Ok(());
}
let max_outer = adj.edges.len().saturating_mul(8).max(64);
for _ in 0..max_outer {
let (comp, rep_edge) = label_components(adj);
if rep_edge.len() <= 1 {
return Ok(());
}
let e0 = rep_edge[0];
let (a, b) = adj.edges[e0];
let mut merged = false;
'outer: for other in 1..rep_edge.len() {
let e1 = rep_edge[other];
let (c, d) = adj.edges[e1];
let bit = rng.next_u64() & 1 == 1;
let plans = if bit {
[(a, c, b, d), (a, d, b, c)]
} else {
[(a, d, b, c), (a, c, b, d)]
};
for &(na, nb, nc, nd) in &plans {
if na != nb && nc != nd && !adj.has_edge(na, nb) && !adj.has_edge(nc, nd) {
adj.swap_edge(e0, a, b, na, nb);
adj.swap_edge(e1, c, d, nc, nd);
merged = true;
break 'outer;
}
}
let _ = comp[a as usize];
}
if !merged {
let scan_n = adj.edges.len();
let mut found = false;
for i in 0..scan_n {
let (x, y) = adj.edges[i];
let cx = comp[x as usize];
if cx != 0 {
continue;
}
for j in 0..scan_n {
if i == j {
continue;
}
let (u, v) = adj.edges[j];
let cu = comp[u as usize];
if cu == 0 || cu < 0 {
continue;
}
if x != u && y != v && !adj.has_edge(x, u) && !adj.has_edge(y, v) {
adj.swap_edge(i, x, y, x, u);
adj.swap_edge(j, u, v, y, v);
found = true;
break;
}
if x != v && y != u && !adj.has_edge(x, v) && !adj.has_edge(y, u) {
adj.swap_edge(i, x, y, x, v);
adj.swap_edge(j, u, v, y, u);
found = true;
break;
}
}
if found {
break;
}
}
if !found {
return Err(IgraphError::InvalidArgument(
"degree_sequence_game_vl: cannot realize given degree sequence as a connected simple graph".to_string(),
));
}
}
if is_connected_ignoring_isolated(adj) {
return Ok(());
}
}
Err(IgraphError::Internal(
"degree_sequence_game_vl: connectivity merging did not converge (please report)",
))
}
fn try_swap(adj: &mut Adj, rng: &mut SplitMix64) -> bool {
let m = adj.edges.len();
if m < 2 {
return false;
}
let e1 = rng.gen_index(m);
let mut e2 = rng.gen_index(m);
if e1 == e2 {
e2 = (e2 + 1) % m;
}
let (a, b) = adj.edges[e1];
let (c, d) = adj.edges[e2];
let bit = rng.next_u64() & 1 == 1;
let (na, nb, nc, nd) = if bit { (a, c, b, d) } else { (a, d, b, c) };
if na == nb || nc == nd {
return false;
}
if adj.has_edge(na, nb) || adj.has_edge(nc, nd) {
return false;
}
adj.swap_edge(e1, a, b, na, nb);
adj.swap_edge(e2, c, d, nc, nd);
true
}
fn shuffle_mcmc(adj: &mut Adj, rng: &mut SplitMix64, total_attempts: u64, window: u64) {
if total_attempts == 0 || adj.edges.len() < 2 {
return;
}
let mut remaining = total_attempts;
while remaining > 0 {
let this_window = remaining.min(window);
let snapshot = adj.snapshot();
for _ in 0..this_window {
try_swap(adj, rng);
}
if !is_connected_ignoring_isolated(adj) {
adj.restore(snapshot);
}
remaining -= this_window;
}
}
pub fn degree_sequence_game_vl(degrees: &[u32], seed: u64) -> IgraphResult<Graph> {
let n_usize = degrees.len();
let n = u32::try_from(n_usize)
.map_err(|_| IgraphError::Internal("degree_sequence_game_vl: vertex count exceeds u32"))?;
if n == 0 {
return Graph::new(0, false);
}
if !is_graphical_simple_undirected(degrees) {
return Err(IgraphError::InvalidArgument(
"degree_sequence_game_vl: cannot realize the given degree sequence as an undirected, simple graph".to_string(),
));
}
let positive = degrees.iter().any(|&d| d > 0);
let any_zero = degrees.iter().any(|&d| d == 0);
if positive && any_zero {
return Err(IgraphError::InvalidArgument(
"degree_sequence_game_vl: cannot make a connected graph from a degree sequence containing both zero and positive degrees".to_string(),
));
}
let sum = checked_sum_u64(degrees)?;
let no_of_edges_u64 = sum / 2;
if no_of_edges_u64 > u64::from(u32::MAX) {
return Err(IgraphError::Internal(
"degree_sequence_game_vl: edge count exceeds u32::MAX",
));
}
if positive && n >= 2 && sum < 2 * u64::from(n - 1) {
return Err(IgraphError::InvalidArgument(
"degree_sequence_game_vl: degree sum is below 2·(n−1); sequence cannot be realised as a connected simple graph".to_string(),
));
}
let mut graph = Graph::new(n, false)?;
if no_of_edges_u64 == 0 {
return Ok(graph);
}
let edges_init = havel_hakimi_realize(degrees, n)?;
let mut adj = Adj::from_edges(n, edges_init);
let mut rng = SplitMix64::new(seed);
make_connected(&mut adj, &mut rng)?;
let two_m: u64 = sum; let total = two_m.saturating_mul(5);
let window = two_m.max(16);
shuffle_mcmc(&mut adj, &mut rng, total, window);
if !is_connected_ignoring_isolated(&adj) {
return Err(IgraphError::Internal(
"degree_sequence_game_vl: post-shuffle graph is disconnected (please report)",
));
}
graph.add_edges(adj.edges.iter().copied())?;
Ok(graph)
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
fn observed_degrees(g: &Graph) -> Vec<u32> {
let vcount = g.vcount() as usize;
let mut deg = vec![0u32; vcount];
let ecount = u32::try_from(g.ecount()).expect("ecount fits u32 in tests");
for eid in 0..ecount {
let (src, dst) = g.edge(eid).expect("edge id in bounds");
deg[src as usize] = deg[src as usize].saturating_add(1);
deg[dst as usize] = deg[dst as usize].saturating_add(1);
}
deg
}
fn count_self_loops(g: &Graph) -> u32 {
let ecount = u32::try_from(g.ecount()).expect("ecount fits u32 in tests");
let mut loops = 0u32;
for eid in 0..ecount {
let (src, dst) = g.edge(eid).expect("edge id in bounds");
if src == dst {
loops += 1;
}
}
loops
}
fn count_multi_edges(g: &Graph) -> u32 {
let ecount = u32::try_from(g.ecount()).expect("ecount fits u32 in tests");
let mut bag: HashMap<(u32, u32), u32> = HashMap::new();
for eid in 0..ecount {
let (src, dst) = g.edge(eid).expect("edge id in bounds");
let key = if src <= dst { (src, dst) } else { (dst, src) };
*bag.entry(key).or_insert(0) += 1;
}
bag.values().map(|c| c.saturating_sub(1)).sum()
}
fn is_connected_simple(g: &Graph) -> bool {
let vcount = g.vcount() as usize;
if vcount == 0 {
return true;
}
let ecount = u32::try_from(g.ecount()).expect("ecount fits u32 in tests");
let mut adj: Vec<Vec<u32>> = vec![Vec::new(); vcount];
let mut deg = vec![0u32; vcount];
for eid in 0..ecount {
let (src, dst) = g.edge(eid).expect("edge id in bounds");
adj[src as usize].push(dst);
adj[dst as usize].push(src);
deg[src as usize] += 1;
deg[dst as usize] += 1;
}
let mut start: Option<usize> = None;
let mut active = 0usize;
for (v, &d) in deg.iter().enumerate() {
if d > 0 {
if start.is_none() {
start = Some(v);
}
active += 1;
}
}
let Some(s) = start else {
return true;
};
let mut visited = vec![false; vcount];
let mut queue = VecDeque::new();
visited[s] = true;
queue.push_back(s);
let mut count = 1usize;
while let Some(v) = queue.pop_front() {
for &nbr in &adj[v] {
let nu = nbr as usize;
if !visited[nu] {
visited[nu] = true;
count += 1;
queue.push_back(nu);
}
}
}
count == active
}
#[test]
fn empty_sequence_returns_empty_graph() {
let g = degree_sequence_game_vl(&[], 0).unwrap();
assert_eq!(g.vcount(), 0);
assert_eq!(g.ecount(), 0);
assert!(!g.is_directed());
}
#[test]
fn singleton_with_zero_degree() {
let g = degree_sequence_game_vl(&[0], 0).unwrap();
assert_eq!(g.vcount(), 1);
assert_eq!(g.ecount(), 0);
}
#[test]
fn all_isolated_returns_no_edges() {
let g = degree_sequence_game_vl(&[0, 0, 0, 0], 42).unwrap();
assert_eq!(g.vcount(), 4);
assert_eq!(g.ecount(), 0);
}
#[test]
fn cycle_4_uniform_degree_2() {
let g = degree_sequence_game_vl(&[2, 2, 2, 2], 1).unwrap();
assert_eq!(g.vcount(), 4);
assert_eq!(g.ecount(), 4);
assert_eq!(observed_degrees(&g), vec![2, 2, 2, 2]);
assert_eq!(count_self_loops(&g), 0);
assert_eq!(count_multi_edges(&g), 0);
assert!(is_connected_simple(&g));
}
#[test]
fn complete_graph_k4() {
let g = degree_sequence_game_vl(&[3, 3, 3, 3], 11).unwrap();
assert_eq!(g.vcount(), 4);
assert_eq!(g.ecount(), 6);
assert_eq!(observed_degrees(&g), vec![3, 3, 3, 3]);
assert_eq!(count_self_loops(&g), 0);
assert_eq!(count_multi_edges(&g), 0);
assert!(is_connected_simple(&g));
}
#[test]
fn path_4_endpoints_degree_one() {
let g = degree_sequence_game_vl(&[1, 2, 2, 1], 5).unwrap();
assert_eq!(g.vcount(), 4);
assert_eq!(g.ecount(), 3);
let mut deg = observed_degrees(&g);
deg.sort_unstable();
assert_eq!(deg, vec![1, 1, 2, 2]);
assert_eq!(count_self_loops(&g), 0);
assert_eq!(count_multi_edges(&g), 0);
assert!(is_connected_simple(&g));
}
#[test]
fn star_k1_n_realises_exact_degrees() {
let seq = vec![5u32, 1, 1, 1, 1, 1];
let g = degree_sequence_game_vl(&seq, 99).unwrap();
assert_eq!(g.vcount(), 6);
assert_eq!(g.ecount(), 5);
let mut deg = observed_degrees(&g);
deg.sort_unstable();
assert_eq!(deg, vec![1, 1, 1, 1, 1, 5]);
assert!(is_connected_simple(&g));
}
#[test]
fn powerlaw_like_sequence_preserves_degrees() {
let seq: Vec<u32> = vec![5, 4, 4, 3, 3, 3, 2, 2, 2, 2];
let g = degree_sequence_game_vl(&seq, 0xABCD_1234).unwrap();
assert_eq!(g.vcount(), 10);
assert_eq!(observed_degrees(&g), seq);
assert_eq!(count_self_loops(&g), 0);
assert_eq!(count_multi_edges(&g), 0);
assert!(is_connected_simple(&g));
}
#[test]
fn larger_uniform_3_regular_n10() {
let seq: Vec<u32> = vec![3; 10];
let g = degree_sequence_game_vl(&seq, 0xDEAD_BEEF).unwrap();
assert_eq!(g.vcount(), 10);
assert_eq!(g.ecount(), 15);
assert_eq!(observed_degrees(&g), seq);
assert_eq!(count_self_loops(&g), 0);
assert_eq!(count_multi_edges(&g), 0);
assert!(is_connected_simple(&g));
}
#[test]
fn odd_degree_sum_rejected() {
let r = degree_sequence_game_vl(&[1, 1, 1], 0);
assert!(matches!(r, Err(IgraphError::InvalidArgument(_))));
}
#[test]
fn degree_exceeds_n_minus_1_rejected() {
let r = degree_sequence_game_vl(&[4, 1, 1, 1], 0);
assert!(matches!(r, Err(IgraphError::InvalidArgument(_))));
}
#[test]
fn mixed_zero_with_positive_rejected() {
let r = degree_sequence_game_vl(&[2, 2, 2, 0], 0);
assert!(matches!(r, Err(IgraphError::InvalidArgument(_))));
}
#[test]
fn non_graphical_erdos_gallai_rejected() {
let r = degree_sequence_game_vl(&[3, 3, 1, 1], 0);
assert!(matches!(r, Err(IgraphError::InvalidArgument(_))));
}
#[test]
fn determinism_same_seed_yields_same_graph() {
let seq = vec![3u32; 10];
let g1 = degree_sequence_game_vl(&seq, 0xCAFE_BABE).unwrap();
let g2 = degree_sequence_game_vl(&seq, 0xCAFE_BABE).unwrap();
assert_eq!(g1.ecount(), g2.ecount());
let ecount = u32::try_from(g1.ecount()).unwrap();
let mut e1: Vec<(u32, u32)> = (0..ecount).map(|i| g1.edge(i).unwrap()).collect();
let mut e2: Vec<(u32, u32)> = (0..ecount).map(|i| g2.edge(i).unwrap()).collect();
e1.sort_unstable();
e2.sort_unstable();
assert_eq!(e1, e2);
}
#[test]
fn different_seeds_can_yield_different_graphs() {
let seq = vec![3u32; 10];
let g1 = degree_sequence_game_vl(&seq, 1).unwrap();
let g2 = degree_sequence_game_vl(&seq, 2).unwrap();
let ecount = u32::try_from(g1.ecount()).unwrap();
let mut e1: Vec<(u32, u32)> = (0..ecount).map(|i| g1.edge(i).unwrap()).collect();
let mut e2: Vec<(u32, u32)> = (0..ecount).map(|i| g2.edge(i).unwrap()).collect();
e1.sort_unstable();
e2.sort_unstable();
assert!(e1 != e2 || g1.ecount() < 5);
}
#[test]
fn full_connected_sweep_n6_3regular() {
let seq = vec![3u32; 6];
for seed in 0..20u64 {
let g = degree_sequence_game_vl(&seq, seed).unwrap();
assert_eq!(g.vcount(), 6, "seed={seed}");
assert_eq!(g.ecount(), 9, "seed={seed}");
assert_eq!(observed_degrees(&g), seq, "seed={seed}");
assert_eq!(count_self_loops(&g), 0, "seed={seed}");
assert_eq!(count_multi_edges(&g), 0, "seed={seed}");
assert!(is_connected_simple(&g), "seed={seed}");
}
}
#[test]
fn invariants_hold_for_skewed_n12() {
let seq: Vec<u32> = vec![5, 4, 4, 3, 3, 3, 2, 2, 2, 2, 1, 1];
let g = degree_sequence_game_vl(&seq, 0xC001_D00D).unwrap();
assert_eq!(g.vcount(), 12);
assert_eq!(g.ecount(), 16);
let mut got = observed_degrees(&g);
let mut want = seq.clone();
got.sort_unstable();
want.sort_unstable();
assert_eq!(got, want);
assert_eq!(count_self_loops(&g), 0);
assert_eq!(count_multi_edges(&g), 0);
assert!(is_connected_simple(&g));
}
#[cfg(all(test, feature = "proptest-harness"))]
mod proptests {
use super::*;
use proptest::prelude::*;
fn gen_graphical_seq() -> impl Strategy<Value = Vec<u32>> {
(3usize..=12).prop_flat_map(|n| {
let max_d = (n - 1) as u32;
prop::collection::vec(1u32..=max_d, n).prop_map(move |mut v| {
let sum: u64 = v.iter().map(|&d| u64::from(d)).sum();
if sum % 2 != 0 {
if v[0] < max_d {
v[0] += 1;
} else {
v[0] -= 1;
}
}
v
})
})
}
proptest! {
#[test]
fn proptest_invariants_on_graphical_seqs(
seq in gen_graphical_seq(),
seed in any::<u64>(),
) {
let result = degree_sequence_game_vl(&seq, seed);
match result {
Ok(g) => {
prop_assert_eq!(g.vcount() as usize, seq.len());
let sum: u64 = seq.iter().map(|&d| u64::from(d)).sum();
prop_assert_eq!(g.ecount(), (sum / 2) as usize);
let mut got = observed_degrees(&g);
let mut want = seq.clone();
got.sort_unstable();
want.sort_unstable();
prop_assert_eq!(got, want);
prop_assert_eq!(count_self_loops(&g), 0);
prop_assert_eq!(count_multi_edges(&g), 0);
prop_assert!(is_connected_simple(&g));
}
Err(_) => {
let n = seq.len() as u64;
let sum: u64 = seq.iter().map(|&d| u64::from(d)).sum();
let has_zero = seq.iter().any(|&d| d == 0);
let has_pos = seq.iter().any(|&d| d > 0);
let below_hakimi = has_pos && n >= 2 && sum < 2 * (n - 1);
prop_assert!(
!is_graphical_simple_undirected(&seq)
|| (has_zero && has_pos)
|| below_hakimi,
"unexpected error for seq = {:?}, sum = {}, n = {}",
seq, sum, n
);
}
}
}
#[test]
fn proptest_determinism_same_seed(
seq in gen_graphical_seq(),
seed in any::<u64>(),
) {
let g1 = degree_sequence_game_vl(&seq, seed);
let g2 = degree_sequence_game_vl(&seq, seed);
match (g1, g2) {
(Ok(g1), Ok(g2)) => {
prop_assert_eq!(g1.ecount(), g2.ecount());
let ecount = u32::try_from(g1.ecount()).unwrap();
let mut e1: Vec<(u32, u32)> =
(0..ecount).map(|i| g1.edge(i).unwrap()).collect();
let mut e2: Vec<(u32, u32)> =
(0..ecount).map(|i| g2.edge(i).unwrap()).collect();
e1.sort_unstable();
e2.sort_unstable();
prop_assert_eq!(e1, e2);
}
(Err(_), Err(_)) => {}
_ => prop_assert!(false, "outcome differs between identical calls"),
}
}
}
}
}