1#![allow(
91 unknown_lints,
92 clippy::cast_possible_truncation,
93 clippy::cast_sign_loss,
94 clippy::many_single_char_names,
95 clippy::needless_range_loop
96)]
97
98use std::collections::HashSet;
99
100use crate::algorithms::games::degree_sequence_fast_heur::{
101 checked_sum, is_graphical_simple_directed, is_graphical_simple_undirected,
102};
103use crate::core::rng::SplitMix64;
104use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
105
106pub const REWIRE_TRIALS_PER_EDGE: u64 = 10;
110
111fn rng_integer_inclusive(rng: &mut SplitMix64, low: usize, high: usize) -> usize {
114 debug_assert!(low <= high, "rng_integer_inclusive: low ≤ high");
115 let span = (high - low) as u64 + 1;
116 low + (rng.next_u64() % span) as usize
117}
118
119fn rng_bool(rng: &mut SplitMix64) -> bool {
121 (rng.next_u64() & 1) == 1
122}
123
124fn build_seed_undirected(degrees: &[u32]) -> IgraphResult<Vec<(VertexId, VertexId)>> {
127 let n = degrees.len();
128 let total_u64 = checked_sum(degrees)?;
129 if total_u64 % 2 != 0 {
130 return Err(IgraphError::InvalidArgument(
131 "degree_sequence_game_edge_switching_simple: undirected degree sum must be even"
132 .to_string(),
133 ));
134 }
135 let ecount = (total_u64 / 2) as usize;
136 if ecount == 0 {
137 return Ok(Vec::new());
138 }
139
140 let mut residual: Vec<i64> = degrees.iter().map(|&d| i64::from(d)).collect();
141 let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(ecount);
142 let mut order: Vec<u32> = Vec::with_capacity(n);
144
145 for i in 0..n {
146 let r = residual[i];
147 if r <= 0 {
148 continue;
149 }
150 residual[i] = 0;
153
154 order.clear();
157 for j in 0..n {
158 if j == i {
159 continue;
160 }
161 if residual[j] > 0 {
162 order.push(u32::try_from(j).map_err(|_| {
163 IgraphError::Internal("vertex index exceeds u32 (seed builder)")
164 })?);
165 }
166 }
167 let r_usize = usize::try_from(r).map_err(|_| {
168 IgraphError::Internal(
169 "residual degree negative in Havel–Hakimi (should be unreachable)",
170 )
171 })?;
172 if order.len() < r_usize {
173 return Err(IgraphError::InvalidArgument(
176 "degree_sequence_game_edge_switching_simple: degree sequence is not simply realisable (Havel–Hakimi)"
177 .to_string(),
178 ));
179 }
180 order.sort_by(|&a, &b| {
183 residual[b as usize]
184 .cmp(&residual[a as usize])
185 .then(a.cmp(&b))
186 });
187
188 for &spoke in &order[..r_usize] {
189 edges.push((
190 u32::try_from(i)
191 .map_err(|_| IgraphError::Internal("vertex index exceeds u32 (seed hub)"))?,
192 spoke,
193 ));
194 residual[spoke as usize] -= 1;
195 }
196 }
197
198 Ok(edges)
199}
200
201fn build_seed_directed(
204 out_degrees: &[u32],
205 in_degrees: &[u32],
206) -> IgraphResult<Vec<(VertexId, VertexId)>> {
207 let n = out_degrees.len();
208 let out_total = checked_sum(out_degrees)? as usize;
209 let in_total = checked_sum(in_degrees)? as usize;
210 if out_total != in_total {
211 return Err(IgraphError::InvalidArgument(
212 "degree_sequence_game_edge_switching_simple: directed sums Σout and Σin must match"
213 .to_string(),
214 ));
215 }
216 if out_total == 0 {
217 return Ok(Vec::new());
218 }
219
220 let mut out_residual: Vec<i64> = out_degrees.iter().map(|&d| i64::from(d)).collect();
221 let mut in_residual: Vec<i64> = in_degrees.iter().map(|&d| i64::from(d)).collect();
222 let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(out_total);
223 let mut order: Vec<u32> = Vec::with_capacity(n);
224
225 for i in 0..n {
226 let r = out_residual[i];
227 if r <= 0 {
228 continue;
229 }
230 out_residual[i] = 0;
231
232 order.clear();
233 for j in 0..n {
234 if j == i {
235 continue;
236 }
237 if in_residual[j] > 0 {
238 order.push(u32::try_from(j).map_err(|_| {
239 IgraphError::Internal("vertex index exceeds u32 (directed seed)")
240 })?);
241 }
242 }
243 let r_usize = usize::try_from(r).map_err(|_| {
244 IgraphError::Internal("out residual negative in Kleitman–Wang (should be unreachable)")
245 })?;
246 if order.len() < r_usize {
247 return Err(IgraphError::InvalidArgument(
248 "degree_sequence_game_edge_switching_simple: directed degree pair is not simply realisable (Kleitman–Wang)"
249 .to_string(),
250 ));
251 }
252 order.sort_by(|&a, &b| {
257 in_residual[b as usize]
258 .cmp(&in_residual[a as usize])
259 .then(out_residual[b as usize].cmp(&out_residual[a as usize]))
260 .then(a.cmp(&b))
261 });
262
263 for &target in &order[..r_usize] {
264 edges.push((
265 u32::try_from(i).map_err(|_| {
266 IgraphError::Internal("vertex index exceeds u32 (directed hub)")
267 })?,
268 target,
269 ));
270 in_residual[target as usize] -= 1;
271 }
272 }
273
274 Ok(edges)
275}
276
277fn rewire_undirected(
280 n_vertices: u32,
281 edges: &mut [(VertexId, VertexId)],
282 rng: &mut SplitMix64,
283 trials: u64,
284) {
285 let m = edges.len();
286 if m < 2 {
287 return;
288 }
289 let vcount = n_vertices as usize;
290 let mut adj: Vec<HashSet<u32>> = (0..vcount).map(|_| HashSet::new()).collect();
291 for &(u, v) in edges.iter() {
292 adj[u as usize].insert(v);
293 adj[v as usize].insert(u);
294 }
295
296 let mut trial: u64 = 0;
297 while trial < trials {
298 trial += 1;
299
300 let e1 = rng_integer_inclusive(rng, 0, m - 1);
301 let mut e2;
302 loop {
303 e2 = rng_integer_inclusive(rng, 0, m - 1);
304 if e2 != e1 {
305 break;
306 }
307 }
308
309 let (a, b) = edges[e1];
310 let (mut c, mut d) = edges[e2];
311
312 if rng_bool(rng) {
317 std::mem::swap(&mut c, &mut d);
318 }
319
320 if a == c || b == d {
322 continue;
323 }
324 if a == d || b == c {
325 continue;
326 }
327 if adj[a as usize].contains(&d) {
329 continue;
330 }
331 if adj[c as usize].contains(&b) {
332 continue;
333 }
334
335 adj[a as usize].remove(&b);
339 adj[b as usize].remove(&a);
340 adj[c as usize].remove(&d);
341 adj[d as usize].remove(&c);
342 adj[a as usize].insert(d);
343 adj[d as usize].insert(a);
344 adj[c as usize].insert(b);
345 adj[b as usize].insert(c);
346
347 edges[e1] = (a, d);
351 edges[e2] = (c, b);
352 }
353}
354
355fn rewire_directed(
356 n_vertices: u32,
357 edges: &mut [(VertexId, VertexId)],
358 rng: &mut SplitMix64,
359 trials: u64,
360) {
361 let m = edges.len();
362 if m < 2 {
363 return;
364 }
365 let vcount = n_vertices as usize;
366 let mut out_adj: Vec<HashSet<u32>> = (0..vcount).map(|_| HashSet::new()).collect();
369 for &(u, v) in edges.iter() {
370 out_adj[u as usize].insert(v);
371 }
372
373 let mut trial: u64 = 0;
374 while trial < trials {
375 trial += 1;
376
377 let e1 = rng_integer_inclusive(rng, 0, m - 1);
378 let mut e2;
379 loop {
380 e2 = rng_integer_inclusive(rng, 0, m - 1);
381 if e2 != e1 {
382 break;
383 }
384 }
385
386 let (a, b) = edges[e1];
387 let (c, d) = edges[e2];
388
389 if a == c || b == d {
392 continue; }
394 if a == d || b == c {
395 continue; }
397 if out_adj[a as usize].contains(&d) {
398 continue; }
400 if out_adj[c as usize].contains(&b) {
401 continue; }
403
404 out_adj[a as usize].remove(&b);
405 out_adj[c as usize].remove(&d);
406 out_adj[a as usize].insert(d);
407 out_adj[c as usize].insert(b);
408
409 edges[e1] = (a, d);
410 edges[e2] = (c, b);
411 }
412}
413
414pub fn degree_sequence_game_edge_switching_simple(
464 out_degrees: &[u32],
465 in_degrees: Option<&[u32]>,
466 seed: u64,
467) -> IgraphResult<Graph> {
468 let directed = in_degrees.is_some();
469 let n = u32::try_from(out_degrees.len())
470 .map_err(|_| IgraphError::Internal("vertex count exceeds u32"))?;
471
472 if let Some(in_seq) = in_degrees {
473 if in_seq.len() != out_degrees.len() {
474 return Err(IgraphError::InvalidArgument(
475 "degree_sequence_game_edge_switching_simple: out_degrees and in_degrees must have the same length"
476 .to_string(),
477 ));
478 }
479 }
480
481 if directed {
482 let Some(in_seq) = in_degrees else {
483 return Err(IgraphError::InvalidArgument(
484 "directed graph requires in_degrees".to_string(),
485 ));
486 };
487 if !is_graphical_simple_directed(out_degrees, in_seq) {
488 return Err(IgraphError::InvalidArgument(
489 "degree_sequence_game_edge_switching_simple: degree pair is not realisable as a simple directed graph (Fulkerson–Chen–Anstee)"
490 .to_string(),
491 ));
492 }
493 } else if !is_graphical_simple_undirected(out_degrees) {
494 return Err(IgraphError::InvalidArgument(
495 "degree_sequence_game_edge_switching_simple: degree sequence is not realisable as a simple undirected graph (Erdős–Gallai)"
496 .to_string(),
497 ));
498 }
499
500 let mut rng = SplitMix64::new(seed);
501
502 let mut edges = if directed {
503 let Some(in_seq) = in_degrees else {
504 return Err(IgraphError::InvalidArgument(
505 "directed graph requires in_degrees".to_string(),
506 ));
507 };
508 build_seed_directed(out_degrees, in_seq)?
509 } else {
510 build_seed_undirected(out_degrees)?
511 };
512
513 let trials = REWIRE_TRIALS_PER_EDGE
514 .checked_mul(edges.len() as u64)
515 .ok_or(IgraphError::Internal(
516 "rewire trial count overflowed u64 (edge count too large)",
517 ))?;
518
519 if directed {
520 rewire_directed(n, &mut edges, &mut rng, trials);
521 } else {
522 rewire_undirected(n, &mut edges, &mut rng, trials);
523 }
524
525 let mut g = Graph::new(n, directed)?;
526 g.add_edges(edges)?;
527 Ok(g)
528}
529
530#[cfg(test)]
531mod tests {
532 use super::*;
533 use crate::algorithms::properties::{SimpleMode, is_simple_with_mode};
534
535 fn observed_degrees(g: &Graph) -> Vec<u32> {
536 let n = g.vcount() as usize;
537 let mut deg = vec![0u32; n];
538 let ec = u32::try_from(g.ecount()).expect("ecount fits u32");
539 for eid in 0..ec {
540 let (s, t) = g.edge(eid).expect("eid in bounds");
541 deg[s as usize] = deg[s as usize].saturating_add(1);
542 deg[t as usize] = deg[t as usize].saturating_add(1);
543 }
544 deg
545 }
546
547 fn observed_out_in(g: &Graph) -> (Vec<u32>, Vec<u32>) {
548 let n = g.vcount() as usize;
549 let mut out = vec![0u32; n];
550 let mut inv = vec![0u32; n];
551 let ec = u32::try_from(g.ecount()).expect("ecount fits u32");
552 for eid in 0..ec {
553 let (s, t) = g.edge(eid).expect("eid in bounds");
554 out[s as usize] = out[s as usize].saturating_add(1);
555 inv[t as usize] = inv[t as usize].saturating_add(1);
556 }
557 (out, inv)
558 }
559
560 #[test]
561 fn undirected_empty_sequence_yields_empty_graph() {
562 let g = degree_sequence_game_edge_switching_simple(&[], None, 1).expect("empty ok");
563 assert_eq!(g.vcount(), 0);
564 assert_eq!(g.ecount(), 0);
565 assert!(!g.is_directed());
566 }
567
568 #[test]
569 fn undirected_singleton_zero_yields_isolated_vertex() {
570 let g = degree_sequence_game_edge_switching_simple(&[0], None, 1).expect("singleton ok");
571 assert_eq!(g.vcount(), 1);
572 assert_eq!(g.ecount(), 0);
573 }
574
575 #[test]
576 fn undirected_all_isolated_n5_yields_no_edges() {
577 let g = degree_sequence_game_edge_switching_simple(&[0; 5], None, 42).expect("ok");
578 assert_eq!(g.vcount(), 5);
579 assert_eq!(g.ecount(), 0);
580 }
581
582 #[test]
583 fn undirected_4cycle_preserves_degrees_and_is_simple() {
584 let g = degree_sequence_game_edge_switching_simple(&[2, 2, 2, 2], None, 7).expect("ok");
585 assert_eq!(g.vcount(), 4);
586 assert_eq!(g.ecount(), 4);
587 assert_eq!(observed_degrees(&g), vec![2, 2, 2, 2]);
588 assert!(is_simple_with_mode(&g, SimpleMode::DirectedAsDirected).unwrap());
589 }
590
591 #[test]
592 fn undirected_3regular_n6_preserves_degrees() {
593 let degrees: Vec<u32> = vec![3; 6];
594 let g = degree_sequence_game_edge_switching_simple(°rees, None, 0xABCD_u64).expect("ok");
595 assert_eq!(observed_degrees(&g), degrees);
596 assert!(is_simple_with_mode(&g, SimpleMode::DirectedAsDirected).unwrap());
597 }
598
599 #[test]
600 fn undirected_skewed_powerlaw_preserves_degrees() {
601 let degrees: Vec<u32> = vec![5, 4, 4, 3, 3, 3, 2, 2, 2, 2];
602 let g = degree_sequence_game_edge_switching_simple(°rees, None, 0xC0FE_u64).expect("ok");
603 assert_eq!(observed_degrees(&g), degrees);
604 assert!(is_simple_with_mode(&g, SimpleMode::DirectedAsDirected).unwrap());
605 }
606
607 #[test]
608 fn undirected_3regular_n30_preserves_degrees() {
609 let degrees: Vec<u32> = vec![3; 30];
610 let g = degree_sequence_game_edge_switching_simple(°rees, None, 0xDEAD_F00D_u64)
611 .expect("ok");
612 assert_eq!(observed_degrees(&g), degrees);
613 assert!(is_simple_with_mode(&g, SimpleMode::DirectedAsDirected).unwrap());
614 }
615
616 #[test]
620 fn undirected_dense_5regular_n20_preserves_degrees() {
621 let degrees: Vec<u32> = vec![5; 20];
622 let g = degree_sequence_game_edge_switching_simple(°rees, None, 9001).expect("ok");
623 assert_eq!(observed_degrees(&g), degrees);
624 assert!(is_simple_with_mode(&g, SimpleMode::DirectedAsDirected).unwrap());
625 }
626
627 #[test]
628 fn undirected_odd_sum_rejected() {
629 let err = degree_sequence_game_edge_switching_simple(&[1, 1, 1], None, 1).unwrap_err();
630 matches!(err, IgraphError::InvalidArgument(_));
631 }
632
633 #[test]
634 fn undirected_non_graphical_rejected_max_too_large() {
635 let err = degree_sequence_game_edge_switching_simple(&[5, 3, 1, 1], None, 1).unwrap_err();
636 matches!(err, IgraphError::InvalidArgument(_));
637 }
638
639 #[test]
640 fn deterministic_same_seed_undirected() {
641 let degrees: Vec<u32> = vec![3; 8];
642 let g1 = degree_sequence_game_edge_switching_simple(°rees, None, 4242).expect("ok");
643 let g2 = degree_sequence_game_edge_switching_simple(°rees, None, 4242).expect("ok");
644 let mut e1: Vec<(u32, u32)> = (0..u32::try_from(g1.ecount()).unwrap())
645 .map(|i| {
646 let (a, b) = g1.edge(i).unwrap();
647 if a < b { (a, b) } else { (b, a) }
648 })
649 .collect();
650 let mut e2: Vec<(u32, u32)> = (0..u32::try_from(g2.ecount()).unwrap())
651 .map(|i| {
652 let (a, b) = g2.edge(i).unwrap();
653 if a < b { (a, b) } else { (b, a) }
654 })
655 .collect();
656 e1.sort_unstable();
657 e2.sort_unstable();
658 assert_eq!(e1, e2);
659 }
660
661 #[test]
662 fn directed_empty_sequence_yields_empty_graph() {
663 let g = degree_sequence_game_edge_switching_simple(&[], Some(&[]), 1).expect("ok");
664 assert_eq!(g.vcount(), 0);
665 assert_eq!(g.ecount(), 0);
666 assert!(g.is_directed());
667 }
668
669 #[test]
670 fn directed_2cycle_preserves_in_out() {
671 let g = degree_sequence_game_edge_switching_simple(&[1, 1], Some(&[1, 1]), 9).expect("ok");
672 let (out, inv) = observed_out_in(&g);
673 assert_eq!(out, vec![1, 1]);
674 assert_eq!(inv, vec![1, 1]);
675 assert!(is_simple_with_mode(&g, SimpleMode::DirectedAsDirected).unwrap());
676 }
677
678 #[test]
679 fn directed_balanced_n6_d2_preserves_degrees() {
680 let n = 6;
681 let out = vec![2u32; n];
682 let inv = vec![2u32; n];
683 let g =
684 degree_sequence_game_edge_switching_simple(&out, Some(&inv), 0xC0DE_u64).expect("ok");
685 let (got_out, got_in) = observed_out_in(&g);
686 assert_eq!(got_out, out);
687 assert_eq!(got_in, inv);
688 assert!(is_simple_with_mode(&g, SimpleMode::DirectedAsDirected).unwrap());
689 }
690
691 #[test]
692 fn directed_unequal_sums_rejected() {
693 let err = degree_sequence_game_edge_switching_simple(&[1, 1, 1], Some(&[1, 1, 0]), 1)
694 .unwrap_err();
695 matches!(err, IgraphError::InvalidArgument(_));
696 }
697
698 #[test]
699 fn directed_length_mismatch_rejected() {
700 let err =
701 degree_sequence_game_edge_switching_simple(&[1, 1], Some(&[1, 1, 0]), 1).unwrap_err();
702 matches!(err, IgraphError::InvalidArgument(_));
703 }
704
705 #[test]
706 fn directed_deterministic_same_seed() {
707 let out = vec![2u32; 5];
708 let inv = vec![2u32; 5];
709 let g1 = degree_sequence_game_edge_switching_simple(&out, Some(&inv), 12345).expect("ok");
710 let g2 = degree_sequence_game_edge_switching_simple(&out, Some(&inv), 12345).expect("ok");
711 let e1: Vec<(u32, u32)> = (0..u32::try_from(g1.ecount()).unwrap())
712 .map(|i| g1.edge(i).unwrap())
713 .collect();
714 let e2: Vec<(u32, u32)> = (0..u32::try_from(g2.ecount()).unwrap())
715 .map(|i| g2.edge(i).unwrap())
716 .collect();
717 assert_eq!(e1, e2);
718 }
719}
720
721#[cfg(all(test, feature = "proptest-harness"))]
722mod proptest_invariants {
723 use super::*;
724 use proptest::prelude::*;
725
726 fn graphical_undirected_strategy() -> impl Strategy<Value = Vec<u32>> {
731 (4usize..=12).prop_flat_map(|n| {
732 let cap = (n as u32).saturating_sub(1);
733 prop::collection::vec(0u32..=cap, n).prop_filter(
734 "must be graphical (even sum + EG)",
735 move |seq| {
736 let sum: u64 = seq.iter().map(|&d| u64::from(d)).sum();
737 sum % 2 == 0 && is_graphical_simple_undirected(seq)
738 },
739 )
740 })
741 }
742
743 proptest! {
744 #[test]
745 fn degrees_preserved_undirected(
746 seq in graphical_undirected_strategy(),
747 seed in any::<u64>(),
748 ) {
749 let g = degree_sequence_game_edge_switching_simple(&seq, None, seed)
750 .expect("graphical sequence must succeed");
751 let n = g.vcount() as usize;
752 let mut deg = vec![0u32; n];
753 let ec = u32::try_from(g.ecount()).unwrap();
754 for eid in 0..ec {
755 let (s, t) = g.edge(eid).unwrap();
756 deg[s as usize] += 1;
757 deg[t as usize] += 1;
758 }
759 prop_assert_eq!(deg, seq);
760 }
761
762 #[test]
763 fn simple_no_loops_no_multi_undirected(
764 seq in graphical_undirected_strategy(),
765 seed in any::<u64>(),
766 ) {
767 use crate::algorithms::properties::{SimpleMode, is_simple_with_mode};
768 let g = degree_sequence_game_edge_switching_simple(&seq, None, seed)
769 .expect("graphical sequence must succeed");
770 prop_assert!(is_simple_with_mode(&g, SimpleMode::DirectedAsDirected).unwrap());
771 }
772
773 #[test]
774 fn same_seed_same_graph(
775 seq in graphical_undirected_strategy(),
776 seed in any::<u64>(),
777 ) {
778 let g1 = degree_sequence_game_edge_switching_simple(&seq, None, seed).unwrap();
779 let g2 = degree_sequence_game_edge_switching_simple(&seq, None, seed).unwrap();
780 let e1: Vec<(u32, u32)> = (0..u32::try_from(g1.ecount()).unwrap())
781 .map(|i| g1.edge(i).unwrap())
782 .collect();
783 let e2: Vec<(u32, u32)> = (0..u32::try_from(g2.ecount()).unwrap())
784 .map(|i| g2.edge(i).unwrap())
785 .collect();
786 prop_assert_eq!(e1, e2);
787 }
788 }
789}