1#![allow(
79 unknown_lints,
80 clippy::cast_possible_truncation,
81 clippy::cast_precision_loss,
82 clippy::cast_sign_loss,
83 clippy::many_single_char_names,
84 clippy::needless_range_loop,
85 clippy::manual_contains,
86 clippy::double_comparisons
87)]
88
89use std::collections::{HashSet, VecDeque};
90
91use crate::core::rng::SplitMix64;
92use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
93
94fn checked_sum_u64(degrees: &[u32]) -> IgraphResult<u64> {
96 let mut acc: u64 = 0;
97 for &d in degrees {
98 acc = acc
99 .checked_add(u64::from(d))
100 .ok_or(IgraphError::Internal("degree-sum overflow"))?;
101 }
102 Ok(acc)
103}
104
105fn is_graphical_simple_undirected(degrees: &[u32]) -> bool {
113 let n = degrees.len();
114 if n == 0 {
115 return true;
116 }
117 let sum: u64 = degrees.iter().map(|&d| u64::from(d)).sum();
118 if sum % 2 != 0 {
119 return false;
120 }
121 let n_u64 = n as u64;
123 if degrees.iter().any(|&d| u64::from(d) >= n_u64) {
124 return false;
125 }
126 let mut d_desc: Vec<u32> = degrees.to_vec();
127 d_desc.sort_by(|a, b| b.cmp(a));
128 let mut left_sum: u64 = 0;
129 let mut right_sum: u64 = d_desc.iter().map(|&d| u64::from(d)).sum();
130 for k in 1..=n {
131 left_sum = match left_sum.checked_add(u64::from(d_desc[k - 1])) {
132 Some(v) => v,
133 None => return false,
134 };
135 right_sum = right_sum.saturating_sub(u64::from(d_desc[k - 1]));
136 let k_u64 = k as u64;
137 let bound_right: u64 = d_desc[k..].iter().map(|&d| u64::from(d).min(k_u64)).sum();
138 let rhs = k_u64
139 .saturating_mul(k_u64.saturating_sub(1))
140 .saturating_add(bound_right);
141 if left_sum > rhs {
142 return false;
143 }
144 let _ = right_sum;
145 }
146 true
147}
148
149fn havel_hakimi_realize(degrees: &[u32], n: u32) -> IgraphResult<Vec<(VertexId, VertexId)>> {
153 let m: usize = degrees.iter().map(|&d| d as usize).sum::<usize>() / 2;
154 let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(m);
155 let mut residual: Vec<(u32, VertexId)> = degrees
156 .iter()
157 .enumerate()
158 .map(|(i, &d)| (d, i as VertexId))
159 .collect();
160 loop {
163 residual.sort_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1)));
164 let Some(&(r, v)) = residual.first() else {
165 break;
166 };
167 if r == 0 {
168 break;
169 }
170 let r_us = r as usize;
171 if residual.len() - 1 < r_us {
172 return Err(IgraphError::Internal(
173 "degree_sequence_game_vl: Havel–Hakimi realization failed (non-graphical input that passed Erdős–Gallai — please report)",
174 ));
175 }
176 for j in 1..=r_us {
178 let (rd, u) = residual[j];
179 if rd == 0 {
180 return Err(IgraphError::Internal(
181 "degree_sequence_game_vl: Havel–Hakimi residual zero (should not happen for graphical input)",
182 ));
183 }
184 let (a, b) = if v < u { (v, u) } else { (u, v) };
185 edges.push((a, b));
186 residual[j].0 = rd - 1;
187 }
188 residual[0].0 = 0;
189 let _ = n;
192 }
193 Ok(edges)
194}
195
196struct Adj {
200 nbr_set: Vec<HashSet<VertexId>>,
201 edges: Vec<(VertexId, VertexId)>,
202}
203
204impl Adj {
205 fn from_edges(n: u32, edges: Vec<(VertexId, VertexId)>) -> Self {
206 let mut nbr_set: Vec<HashSet<VertexId>> = (0..n).map(|_| HashSet::new()).collect();
207 for &(a, b) in &edges {
208 nbr_set[a as usize].insert(b);
209 nbr_set[b as usize].insert(a);
210 }
211 Self { nbr_set, edges }
212 }
213
214 fn has_edge(&self, u: VertexId, v: VertexId) -> bool {
215 if self.nbr_set[u as usize].len() <= self.nbr_set[v as usize].len() {
216 self.nbr_set[u as usize].contains(&v)
217 } else {
218 self.nbr_set[v as usize].contains(&u)
219 }
220 }
221
222 fn swap_edge(
225 &mut self,
226 eid: usize,
227 old_a: VertexId,
228 old_b: VertexId,
229 new_a: VertexId,
230 new_b: VertexId,
231 ) {
232 self.nbr_set[old_a as usize].remove(&old_b);
233 self.nbr_set[old_b as usize].remove(&old_a);
234 self.nbr_set[new_a as usize].insert(new_b);
235 self.nbr_set[new_b as usize].insert(new_a);
236 let (a, b) = if new_a < new_b {
237 (new_a, new_b)
238 } else {
239 (new_b, new_a)
240 };
241 self.edges[eid] = (a, b);
242 }
243
244 fn snapshot(&self) -> Vec<(VertexId, VertexId)> {
245 self.edges.clone()
246 }
247
248 fn restore(&mut self, snapshot: Vec<(VertexId, VertexId)>) {
249 for set in &mut self.nbr_set {
250 set.clear();
251 }
252 for &(a, b) in &snapshot {
253 self.nbr_set[a as usize].insert(b);
254 self.nbr_set[b as usize].insert(a);
255 }
256 self.edges = snapshot;
257 }
258}
259
260fn is_connected_ignoring_isolated(adj: &Adj) -> bool {
264 let n = adj.nbr_set.len();
265 let mut start: Option<usize> = None;
266 let mut active = 0usize;
267 for (v, set) in adj.nbr_set.iter().enumerate() {
268 if !set.is_empty() {
269 if start.is_none() {
270 start = Some(v);
271 }
272 active += 1;
273 }
274 }
275 let Some(start_v) = start else {
276 return true; };
278 let mut visited = vec![false; n];
279 let mut queue: VecDeque<usize> = VecDeque::with_capacity(active);
280 visited[start_v] = true;
281 queue.push_back(start_v);
282 let mut count = 1usize;
283 while let Some(v) = queue.pop_front() {
284 for &nbr in &adj.nbr_set[v] {
285 let nu = nbr as usize;
286 if !visited[nu] {
287 visited[nu] = true;
288 count += 1;
289 queue.push_back(nu);
290 }
291 }
292 }
293 count == active
294}
295
296fn label_components(adj: &Adj) -> (Vec<i32>, Vec<usize>) {
301 let n = adj.nbr_set.len();
302 let mut comp: Vec<i32> = vec![-1; n];
303 let mut component_count: i32 = 0;
304 let mut queue: VecDeque<usize> = VecDeque::new();
305 for v in 0..n {
306 if comp[v] != -1 || adj.nbr_set[v].is_empty() {
307 continue;
308 }
309 comp[v] = component_count;
310 queue.push_back(v);
311 while let Some(u) = queue.pop_front() {
312 for &nbr in &adj.nbr_set[u] {
313 let nu = nbr as usize;
314 if comp[nu] == -1 {
315 comp[nu] = component_count;
316 queue.push_back(nu);
317 }
318 }
319 }
320 component_count += 1;
321 }
322 let mut representative_edge: Vec<usize> = vec![usize::MAX; component_count as usize];
324 for (eid, &(a, _)) in adj.edges.iter().enumerate() {
325 let cid = comp[a as usize];
326 if cid >= 0 && representative_edge[cid as usize] == usize::MAX {
327 representative_edge[cid as usize] = eid;
328 }
329 }
330 (comp, representative_edge)
331}
332
333fn make_connected(adj: &mut Adj, rng: &mut SplitMix64) -> IgraphResult<()> {
338 if is_connected_ignoring_isolated(adj) {
339 return Ok(());
340 }
341 let max_outer = adj.edges.len().saturating_mul(8).max(64);
342 for _ in 0..max_outer {
343 let (comp, rep_edge) = label_components(adj);
344 if rep_edge.len() <= 1 {
345 return Ok(());
346 }
347 let e0 = rep_edge[0];
350 let (a, b) = adj.edges[e0];
351 let mut merged = false;
352 'outer: for other in 1..rep_edge.len() {
353 let e1 = rep_edge[other];
354 let (c, d) = adj.edges[e1];
355 let bit = rng.next_u64() & 1 == 1;
357 let plans = if bit {
358 [(a, c, b, d), (a, d, b, c)]
359 } else {
360 [(a, d, b, c), (a, c, b, d)]
361 };
362 for &(na, nb, nc, nd) in &plans {
363 if na != nb && nc != nd && !adj.has_edge(na, nb) && !adj.has_edge(nc, nd) {
364 adj.swap_edge(e0, a, b, na, nb);
365 adj.swap_edge(e1, c, d, nc, nd);
366 merged = true;
367 break 'outer;
368 }
369 }
370 let _ = comp[a as usize];
374 }
375 if !merged {
376 let scan_n = adj.edges.len();
378 let mut found = false;
379 for i in 0..scan_n {
380 let (x, y) = adj.edges[i];
381 let cx = comp[x as usize];
382 if cx != 0 {
383 continue;
384 }
385 for j in 0..scan_n {
386 if i == j {
387 continue;
388 }
389 let (u, v) = adj.edges[j];
390 let cu = comp[u as usize];
391 if cu == 0 || cu < 0 {
392 continue;
393 }
394 if x != u && y != v && !adj.has_edge(x, u) && !adj.has_edge(y, v) {
396 adj.swap_edge(i, x, y, x, u);
397 adj.swap_edge(j, u, v, y, v);
398 found = true;
399 break;
400 }
401 if x != v && y != u && !adj.has_edge(x, v) && !adj.has_edge(y, u) {
403 adj.swap_edge(i, x, y, x, v);
404 adj.swap_edge(j, u, v, y, u);
405 found = true;
406 break;
407 }
408 }
409 if found {
410 break;
411 }
412 }
413 if !found {
414 return Err(IgraphError::InvalidArgument(
415 "degree_sequence_game_vl: cannot realize given degree sequence as a connected simple graph".to_string(),
416 ));
417 }
418 }
419 if is_connected_ignoring_isolated(adj) {
420 return Ok(());
421 }
422 }
423 Err(IgraphError::Internal(
424 "degree_sequence_game_vl: connectivity merging did not converge (please report)",
425 ))
426}
427
428fn try_swap(adj: &mut Adj, rng: &mut SplitMix64) -> bool {
430 let m = adj.edges.len();
431 if m < 2 {
432 return false;
433 }
434 let e1 = rng.gen_index(m);
435 let mut e2 = rng.gen_index(m);
436 if e1 == e2 {
437 e2 = (e2 + 1) % m;
438 }
439 let (a, b) = adj.edges[e1];
440 let (c, d) = adj.edges[e2];
441 let bit = rng.next_u64() & 1 == 1;
443 let (na, nb, nc, nd) = if bit { (a, c, b, d) } else { (a, d, b, c) };
444 if na == nb || nc == nd {
446 return false;
447 }
448 if adj.has_edge(na, nb) || adj.has_edge(nc, nd) {
450 return false;
451 }
452 adj.swap_edge(e1, a, b, na, nb);
453 adj.swap_edge(e2, c, d, nc, nd);
454 true
455}
456
457fn shuffle_mcmc(adj: &mut Adj, rng: &mut SplitMix64, total_attempts: u64, window: u64) {
461 if total_attempts == 0 || adj.edges.len() < 2 {
462 return;
463 }
464 let mut remaining = total_attempts;
465 while remaining > 0 {
466 let this_window = remaining.min(window);
467 let snapshot = adj.snapshot();
468 for _ in 0..this_window {
469 try_swap(adj, rng);
470 }
471 if !is_connected_ignoring_isolated(adj) {
472 adj.restore(snapshot);
473 }
474 remaining -= this_window;
475 }
476}
477
478pub fn degree_sequence_game_vl(degrees: &[u32], seed: u64) -> IgraphResult<Graph> {
509 let n_usize = degrees.len();
510 let n = u32::try_from(n_usize)
511 .map_err(|_| IgraphError::Internal("degree_sequence_game_vl: vertex count exceeds u32"))?;
512
513 if n == 0 {
514 return Graph::new(0, false);
515 }
516
517 if !is_graphical_simple_undirected(degrees) {
518 return Err(IgraphError::InvalidArgument(
519 "degree_sequence_game_vl: cannot realize the given degree sequence as an undirected, simple graph".to_string(),
520 ));
521 }
522
523 let positive = degrees.iter().any(|&d| d > 0);
527 let any_zero = degrees.iter().any(|&d| d == 0);
528 if positive && any_zero {
529 return Err(IgraphError::InvalidArgument(
530 "degree_sequence_game_vl: cannot make a connected graph from a degree sequence containing both zero and positive degrees".to_string(),
531 ));
532 }
533
534 let sum = checked_sum_u64(degrees)?;
535 let no_of_edges_u64 = sum / 2;
536 if no_of_edges_u64 > u64::from(u32::MAX) {
537 return Err(IgraphError::Internal(
538 "degree_sequence_game_vl: edge count exceeds u32::MAX",
539 ));
540 }
541
542 if positive && n >= 2 && sum < 2 * u64::from(n - 1) {
546 return Err(IgraphError::InvalidArgument(
547 "degree_sequence_game_vl: degree sum is below 2·(n−1); sequence cannot be realised as a connected simple graph".to_string(),
548 ));
549 }
550
551 let mut graph = Graph::new(n, false)?;
552 if no_of_edges_u64 == 0 {
553 return Ok(graph);
554 }
555
556 let edges_init = havel_hakimi_realize(degrees, n)?;
557 let mut adj = Adj::from_edges(n, edges_init);
558 let mut rng = SplitMix64::new(seed);
559
560 make_connected(&mut adj, &mut rng)?;
561
562 let two_m: u64 = sum; let total = two_m.saturating_mul(5);
569 let window = two_m.max(16);
570 shuffle_mcmc(&mut adj, &mut rng, total, window);
571
572 if !is_connected_ignoring_isolated(&adj) {
576 return Err(IgraphError::Internal(
577 "degree_sequence_game_vl: post-shuffle graph is disconnected (please report)",
578 ));
579 }
580
581 graph.add_edges(adj.edges.iter().copied())?;
582 Ok(graph)
583}
584
585#[cfg(test)]
586mod tests {
587 use super::*;
588 use std::collections::HashMap;
589
590 fn observed_degrees(g: &Graph) -> Vec<u32> {
591 let vcount = g.vcount() as usize;
592 let mut deg = vec![0u32; vcount];
593 let ecount = u32::try_from(g.ecount()).expect("ecount fits u32 in tests");
594 for eid in 0..ecount {
595 let (src, dst) = g.edge(eid).expect("edge id in bounds");
596 deg[src as usize] = deg[src as usize].saturating_add(1);
597 deg[dst as usize] = deg[dst as usize].saturating_add(1);
598 }
599 deg
600 }
601
602 fn count_self_loops(g: &Graph) -> u32 {
603 let ecount = u32::try_from(g.ecount()).expect("ecount fits u32 in tests");
604 let mut loops = 0u32;
605 for eid in 0..ecount {
606 let (src, dst) = g.edge(eid).expect("edge id in bounds");
607 if src == dst {
608 loops += 1;
609 }
610 }
611 loops
612 }
613
614 fn count_multi_edges(g: &Graph) -> u32 {
615 let ecount = u32::try_from(g.ecount()).expect("ecount fits u32 in tests");
616 let mut bag: HashMap<(u32, u32), u32> = HashMap::new();
617 for eid in 0..ecount {
618 let (src, dst) = g.edge(eid).expect("edge id in bounds");
619 let key = if src <= dst { (src, dst) } else { (dst, src) };
620 *bag.entry(key).or_insert(0) += 1;
621 }
622 bag.values().map(|c| c.saturating_sub(1)).sum()
623 }
624
625 fn is_connected_simple(g: &Graph) -> bool {
626 let vcount = g.vcount() as usize;
627 if vcount == 0 {
628 return true;
629 }
630 let ecount = u32::try_from(g.ecount()).expect("ecount fits u32 in tests");
631 let mut adj: Vec<Vec<u32>> = vec![Vec::new(); vcount];
632 let mut deg = vec![0u32; vcount];
633 for eid in 0..ecount {
634 let (src, dst) = g.edge(eid).expect("edge id in bounds");
635 adj[src as usize].push(dst);
636 adj[dst as usize].push(src);
637 deg[src as usize] += 1;
638 deg[dst as usize] += 1;
639 }
640 let mut start: Option<usize> = None;
641 let mut active = 0usize;
642 for (v, &d) in deg.iter().enumerate() {
643 if d > 0 {
644 if start.is_none() {
645 start = Some(v);
646 }
647 active += 1;
648 }
649 }
650 let Some(s) = start else {
651 return true;
652 };
653 let mut visited = vec![false; vcount];
654 let mut queue = VecDeque::new();
655 visited[s] = true;
656 queue.push_back(s);
657 let mut count = 1usize;
658 while let Some(v) = queue.pop_front() {
659 for &nbr in &adj[v] {
660 let nu = nbr as usize;
661 if !visited[nu] {
662 visited[nu] = true;
663 count += 1;
664 queue.push_back(nu);
665 }
666 }
667 }
668 count == active
669 }
670
671 #[test]
672 fn empty_sequence_returns_empty_graph() {
673 let g = degree_sequence_game_vl(&[], 0).unwrap();
674 assert_eq!(g.vcount(), 0);
675 assert_eq!(g.ecount(), 0);
676 assert!(!g.is_directed());
677 }
678
679 #[test]
680 fn singleton_with_zero_degree() {
681 let g = degree_sequence_game_vl(&[0], 0).unwrap();
682 assert_eq!(g.vcount(), 1);
683 assert_eq!(g.ecount(), 0);
684 }
685
686 #[test]
687 fn all_isolated_returns_no_edges() {
688 let g = degree_sequence_game_vl(&[0, 0, 0, 0], 42).unwrap();
689 assert_eq!(g.vcount(), 4);
690 assert_eq!(g.ecount(), 0);
691 }
692
693 #[test]
694 fn cycle_4_uniform_degree_2() {
695 let g = degree_sequence_game_vl(&[2, 2, 2, 2], 1).unwrap();
696 assert_eq!(g.vcount(), 4);
697 assert_eq!(g.ecount(), 4);
698 assert_eq!(observed_degrees(&g), vec![2, 2, 2, 2]);
699 assert_eq!(count_self_loops(&g), 0);
700 assert_eq!(count_multi_edges(&g), 0);
701 assert!(is_connected_simple(&g));
702 }
703
704 #[test]
705 fn complete_graph_k4() {
706 let g = degree_sequence_game_vl(&[3, 3, 3, 3], 11).unwrap();
707 assert_eq!(g.vcount(), 4);
708 assert_eq!(g.ecount(), 6);
709 assert_eq!(observed_degrees(&g), vec![3, 3, 3, 3]);
710 assert_eq!(count_self_loops(&g), 0);
711 assert_eq!(count_multi_edges(&g), 0);
712 assert!(is_connected_simple(&g));
713 }
714
715 #[test]
716 fn path_4_endpoints_degree_one() {
717 let g = degree_sequence_game_vl(&[1, 2, 2, 1], 5).unwrap();
719 assert_eq!(g.vcount(), 4);
720 assert_eq!(g.ecount(), 3);
721 let mut deg = observed_degrees(&g);
722 deg.sort_unstable();
723 assert_eq!(deg, vec![1, 1, 2, 2]);
724 assert_eq!(count_self_loops(&g), 0);
725 assert_eq!(count_multi_edges(&g), 0);
726 assert!(is_connected_simple(&g));
727 }
728
729 #[test]
730 fn star_k1_n_realises_exact_degrees() {
731 let seq = vec![5u32, 1, 1, 1, 1, 1];
733 let g = degree_sequence_game_vl(&seq, 99).unwrap();
734 assert_eq!(g.vcount(), 6);
735 assert_eq!(g.ecount(), 5);
736 let mut deg = observed_degrees(&g);
737 deg.sort_unstable();
738 assert_eq!(deg, vec![1, 1, 1, 1, 1, 5]);
739 assert!(is_connected_simple(&g));
740 }
741
742 #[test]
743 fn powerlaw_like_sequence_preserves_degrees() {
744 let seq: Vec<u32> = vec![5, 4, 4, 3, 3, 3, 2, 2, 2, 2];
746 let g = degree_sequence_game_vl(&seq, 0xABCD_1234).unwrap();
747 assert_eq!(g.vcount(), 10);
748 assert_eq!(observed_degrees(&g), seq);
749 assert_eq!(count_self_loops(&g), 0);
750 assert_eq!(count_multi_edges(&g), 0);
751 assert!(is_connected_simple(&g));
752 }
753
754 #[test]
755 fn larger_uniform_3_regular_n10() {
756 let seq: Vec<u32> = vec![3; 10];
757 let g = degree_sequence_game_vl(&seq, 0xDEAD_BEEF).unwrap();
758 assert_eq!(g.vcount(), 10);
759 assert_eq!(g.ecount(), 15);
760 assert_eq!(observed_degrees(&g), seq);
761 assert_eq!(count_self_loops(&g), 0);
762 assert_eq!(count_multi_edges(&g), 0);
763 assert!(is_connected_simple(&g));
764 }
765
766 #[test]
767 fn odd_degree_sum_rejected() {
768 let r = degree_sequence_game_vl(&[1, 1, 1], 0);
769 assert!(matches!(r, Err(IgraphError::InvalidArgument(_))));
770 }
771
772 #[test]
773 fn degree_exceeds_n_minus_1_rejected() {
774 let r = degree_sequence_game_vl(&[4, 1, 1, 1], 0);
776 assert!(matches!(r, Err(IgraphError::InvalidArgument(_))));
777 }
778
779 #[test]
780 fn mixed_zero_with_positive_rejected() {
781 let r = degree_sequence_game_vl(&[2, 2, 2, 0], 0);
783 assert!(matches!(r, Err(IgraphError::InvalidArgument(_))));
784 }
785
786 #[test]
787 fn non_graphical_erdos_gallai_rejected() {
788 let r = degree_sequence_game_vl(&[3, 3, 1, 1], 0);
792 assert!(matches!(r, Err(IgraphError::InvalidArgument(_))));
793 }
794
795 #[test]
796 fn determinism_same_seed_yields_same_graph() {
797 let seq = vec![3u32; 10];
798 let g1 = degree_sequence_game_vl(&seq, 0xCAFE_BABE).unwrap();
799 let g2 = degree_sequence_game_vl(&seq, 0xCAFE_BABE).unwrap();
800 assert_eq!(g1.ecount(), g2.ecount());
801 let ecount = u32::try_from(g1.ecount()).unwrap();
802 let mut e1: Vec<(u32, u32)> = (0..ecount).map(|i| g1.edge(i).unwrap()).collect();
803 let mut e2: Vec<(u32, u32)> = (0..ecount).map(|i| g2.edge(i).unwrap()).collect();
804 e1.sort_unstable();
805 e2.sort_unstable();
806 assert_eq!(e1, e2);
807 }
808
809 #[test]
810 fn different_seeds_can_yield_different_graphs() {
811 let seq = vec![3u32; 10];
812 let g1 = degree_sequence_game_vl(&seq, 1).unwrap();
813 let g2 = degree_sequence_game_vl(&seq, 2).unwrap();
814 let ecount = u32::try_from(g1.ecount()).unwrap();
815 let mut e1: Vec<(u32, u32)> = (0..ecount).map(|i| g1.edge(i).unwrap()).collect();
816 let mut e2: Vec<(u32, u32)> = (0..ecount).map(|i| g2.edge(i).unwrap()).collect();
817 e1.sort_unstable();
818 e2.sort_unstable();
819 assert!(e1 != e2 || g1.ecount() < 5);
823 }
824
825 #[test]
826 fn full_connected_sweep_n6_3regular() {
827 let seq = vec![3u32; 6];
829 for seed in 0..20u64 {
830 let g = degree_sequence_game_vl(&seq, seed).unwrap();
831 assert_eq!(g.vcount(), 6, "seed={seed}");
832 assert_eq!(g.ecount(), 9, "seed={seed}");
833 assert_eq!(observed_degrees(&g), seq, "seed={seed}");
834 assert_eq!(count_self_loops(&g), 0, "seed={seed}");
835 assert_eq!(count_multi_edges(&g), 0, "seed={seed}");
836 assert!(is_connected_simple(&g), "seed={seed}");
837 }
838 }
839
840 #[test]
841 fn invariants_hold_for_skewed_n12() {
842 let seq: Vec<u32> = vec![5, 4, 4, 3, 3, 3, 2, 2, 2, 2, 1, 1];
843 let g = degree_sequence_game_vl(&seq, 0xC001_D00D).unwrap();
844 assert_eq!(g.vcount(), 12);
845 assert_eq!(g.ecount(), 16);
846 let mut got = observed_degrees(&g);
847 let mut want = seq.clone();
848 got.sort_unstable();
849 want.sort_unstable();
850 assert_eq!(got, want);
851 assert_eq!(count_self_loops(&g), 0);
852 assert_eq!(count_multi_edges(&g), 0);
853 assert!(is_connected_simple(&g));
854 }
855
856 #[cfg(all(test, feature = "proptest-harness"))]
857 mod proptests {
858 use super::*;
859 use proptest::prelude::*;
860
861 fn gen_graphical_seq() -> impl Strategy<Value = Vec<u32>> {
862 (3usize..=12).prop_flat_map(|n| {
865 let max_d = (n - 1) as u32;
866 prop::collection::vec(1u32..=max_d, n).prop_map(move |mut v| {
867 let sum: u64 = v.iter().map(|&d| u64::from(d)).sum();
868 if sum % 2 != 0 {
869 if v[0] < max_d {
870 v[0] += 1;
871 } else {
872 v[0] -= 1;
873 }
874 }
875 v
876 })
877 })
878 }
879
880 proptest! {
881 #[test]
882 fn proptest_invariants_on_graphical_seqs(
883 seq in gen_graphical_seq(),
884 seed in any::<u64>(),
885 ) {
886 let result = degree_sequence_game_vl(&seq, seed);
889 match result {
890 Ok(g) => {
891 prop_assert_eq!(g.vcount() as usize, seq.len());
892 let sum: u64 = seq.iter().map(|&d| u64::from(d)).sum();
893 prop_assert_eq!(g.ecount(), (sum / 2) as usize);
894 let mut got = observed_degrees(&g);
895 let mut want = seq.clone();
896 got.sort_unstable();
897 want.sort_unstable();
898 prop_assert_eq!(got, want);
899 prop_assert_eq!(count_self_loops(&g), 0);
900 prop_assert_eq!(count_multi_edges(&g), 0);
901 prop_assert!(is_connected_simple(&g));
902 }
903 Err(_) => {
904 let n = seq.len() as u64;
908 let sum: u64 = seq.iter().map(|&d| u64::from(d)).sum();
909 let has_zero = seq.iter().any(|&d| d == 0);
910 let has_pos = seq.iter().any(|&d| d > 0);
911 let below_hakimi = has_pos && n >= 2 && sum < 2 * (n - 1);
912 prop_assert!(
913 !is_graphical_simple_undirected(&seq)
914 || (has_zero && has_pos)
915 || below_hakimi,
916 "unexpected error for seq = {:?}, sum = {}, n = {}",
917 seq, sum, n
918 );
919 }
920 }
921 }
922
923 #[test]
924 fn proptest_determinism_same_seed(
925 seq in gen_graphical_seq(),
926 seed in any::<u64>(),
927 ) {
928 let g1 = degree_sequence_game_vl(&seq, seed);
929 let g2 = degree_sequence_game_vl(&seq, seed);
930 match (g1, g2) {
931 (Ok(g1), Ok(g2)) => {
932 prop_assert_eq!(g1.ecount(), g2.ecount());
933 let ecount = u32::try_from(g1.ecount()).unwrap();
934 let mut e1: Vec<(u32, u32)> =
935 (0..ecount).map(|i| g1.edge(i).unwrap()).collect();
936 let mut e2: Vec<(u32, u32)> =
937 (0..ecount).map(|i| g2.edge(i).unwrap()).collect();
938 e1.sort_unstable();
939 e2.sort_unstable();
940 prop_assert_eq!(e1, e2);
941 }
942 (Err(_), Err(_)) => {}
943 _ => prop_assert!(false, "outcome differs between identical calls"),
944 }
945 }
946 }
947 }
948}