1#![allow(
52 clippy::cast_possible_truncation,
53 clippy::cast_sign_loss,
54 clippy::many_single_char_names
55)]
56
57use std::collections::HashSet;
58
59use crate::core::rng::SplitMix64;
60use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
61
62const FAST_HEUR_MAX_RESTARTS: u32 = 1024;
69
70fn validate(n: u32, k: u32, directed: bool, multiple: bool) -> IgraphResult<()> {
71 let total = u64::from(n) * u64::from(k);
72 if total > u64::from(u32::MAX) {
73 return Err(IgraphError::InvalidArgument(format!(
74 "k_regular_game: n * k ({total}) overflows u32"
75 )));
76 }
77 if !directed && !multiple && k % 2 != 0 && n % 2 != 0 {
78 return Err(IgraphError::InvalidArgument(format!(
79 "k_regular_game: undirected simple requires n*k even (got n={n}, k={k})"
80 )));
81 }
82 if !directed && multiple && total % 2 != 0 {
83 return Err(IgraphError::InvalidArgument(format!(
84 "k_regular_game: undirected multigraph requires n*k even (got n={n}, k={k})"
85 )));
86 }
87 if !multiple && k > 0 && k > n.saturating_sub(1) {
88 return Err(IgraphError::InvalidArgument(format!(
89 "k_regular_game: simple graph requires k <= n - 1 (got n={n}, k={k})"
90 )));
91 }
92 Ok(())
93}
94
95fn fisher_yates_shuffle<T>(slice: &mut [T], rng: &mut SplitMix64) {
96 let len = slice.len();
97 if len < 2 {
98 return;
99 }
100 for i in (1..len).rev() {
101 let j = rng.gen_index(i + 1);
102 slice.swap(i, j);
103 }
104}
105
106fn build_stubs(n: u32, k: u32) -> Vec<VertexId> {
109 let n_us = n as usize;
110 let k_us = k as usize;
111 let mut stubs = Vec::with_capacity(n_us * k_us);
112 for v in 0..n {
113 for _ in 0..k {
114 stubs.push(v);
115 }
116 }
117 stubs
118}
119
120fn configuration(
125 n: u32,
126 k: u32,
127 directed: bool,
128 rng: &mut SplitMix64,
129) -> Vec<(VertexId, VertexId)> {
130 if k == 0 {
131 return Vec::new();
132 }
133 let n_us = n as usize;
134 let k_us = k as usize;
135 let total_stubs = n_us * k_us;
136
137 if directed {
138 let mut out_bag = build_stubs(n, k);
139 let mut in_bag = build_stubs(n, k);
140 fisher_yates_shuffle(&mut out_bag, rng);
141 fisher_yates_shuffle(&mut in_bag, rng);
142 out_bag.into_iter().zip(in_bag).collect()
143 } else {
144 let mut bag = build_stubs(n, k);
148 let m = total_stubs / 2;
149 let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(m);
150 for _ in 0..m {
151 let i = rng.gen_index(bag.len());
152 let from = bag[i];
153 let last = bag.len() - 1;
154 bag.swap(i, last);
155 bag.pop();
156 let j = rng.gen_index(bag.len());
157 let to = bag[j];
158 let last = bag.len() - 1;
159 bag.swap(j, last);
160 bag.pop();
161 edges.push((from, to));
162 }
163 edges
164 }
165}
166
167fn fast_heur_undirected(
169 n: u32,
170 k: u32,
171 rng: &mut SplitMix64,
172) -> IgraphResult<Vec<(VertexId, VertexId)>> {
173 if k == 0 {
174 return Ok(Vec::new());
175 }
176 let n_us = n as usize;
177 let total_edges = (n_us * k as usize) / 2;
178
179 for _restart in 0..FAST_HEUR_MAX_RESTARTS {
180 let mut residual: Vec<u32> = vec![k; n_us];
181 let mut adj: Vec<HashSet<VertexId>> = vec![HashSet::new(); n_us];
182 let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(total_edges);
183
184 let mut attempt_failed = false;
185
186 loop {
187 let mut stubs: Vec<VertexId> = Vec::new();
189 for (v, &d) in residual.iter().enumerate() {
190 for _ in 0..d {
191 stubs.push(v as VertexId);
192 }
193 }
194 if stubs.is_empty() {
195 break;
196 }
197 fisher_yates_shuffle(&mut stubs, rng);
198
199 residual.fill(0);
202 let mut incomplete: HashSet<VertexId> = HashSet::new();
203
204 for pair in stubs.chunks_exact(2) {
206 let mut from = pair[0];
207 let mut to = pair[1];
208 if from > to {
209 std::mem::swap(&mut from, &mut to);
210 }
211 if from == to || adj[from as usize].contains(&to) {
212 residual[from as usize] += 1;
213 residual[to as usize] += 1;
214 incomplete.insert(from);
215 incomplete.insert(to);
216 } else {
217 adj[from as usize].insert(to);
218 edges.push((from, to));
219 }
220 }
221
222 if incomplete.is_empty() {
223 return Ok(edges);
224 }
225
226 if !undirected_pair_feasible(&incomplete, &adj) {
230 attempt_failed = true;
231 break;
232 }
233 }
234
235 if !attempt_failed {
236 return Ok(edges);
243 }
244 }
245
246 Err(IgraphError::Internal(
247 "k_regular_game: fast-heur exceeded restart budget; try multiple=true",
248 ))
249}
250
251fn undirected_pair_feasible(incomplete: &HashSet<VertexId>, adj: &[HashSet<VertexId>]) -> bool {
252 let mut sorted: Vec<VertexId> = incomplete.iter().copied().collect();
253 sorted.sort_unstable();
254 for (i, &a) in sorted.iter().enumerate() {
255 for &b in sorted.iter().skip(i + 1) {
256 if !adj[a as usize].contains(&b) {
260 return true;
261 }
262 }
263 }
264 false
265}
266
267fn fast_heur_directed(
269 n: u32,
270 k: u32,
271 rng: &mut SplitMix64,
272) -> IgraphResult<Vec<(VertexId, VertexId)>> {
273 if k == 0 {
274 return Ok(Vec::new());
275 }
276 let n_us = n as usize;
277 let total_edges = n_us * k as usize;
278
279 for _restart in 0..FAST_HEUR_MAX_RESTARTS {
280 let mut residual_out: Vec<u32> = vec![k; n_us];
281 let mut residual_in: Vec<u32> = vec![k; n_us];
282 let mut adj: Vec<HashSet<VertexId>> = vec![HashSet::new(); n_us];
283 let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(total_edges);
284
285 let mut attempt_failed = false;
286
287 loop {
288 let mut out_stubs: Vec<VertexId> = Vec::new();
289 let mut in_stubs: Vec<VertexId> = Vec::new();
290 for v in 0..n_us {
291 for _ in 0..residual_out[v] {
292 out_stubs.push(v as VertexId);
293 }
294 for _ in 0..residual_in[v] {
295 in_stubs.push(v as VertexId);
296 }
297 }
298 if out_stubs.is_empty() {
299 break;
300 }
301 fisher_yates_shuffle(&mut out_stubs, rng);
302
303 residual_out.fill(0);
304 residual_in.fill(0);
305 let mut incomplete_out: HashSet<VertexId> = HashSet::new();
306 let mut incomplete_in: HashSet<VertexId> = HashSet::new();
307
308 for (&from, &to) in out_stubs.iter().zip(in_stubs.iter()) {
309 if from == to || adj[from as usize].contains(&to) {
310 residual_out[from as usize] += 1;
311 residual_in[to as usize] += 1;
312 incomplete_out.insert(from);
313 incomplete_in.insert(to);
314 } else {
315 adj[from as usize].insert(to);
316 edges.push((from, to));
317 }
318 }
319
320 if incomplete_out.is_empty() {
321 return Ok(edges);
322 }
323 if !directed_pair_feasible(&incomplete_out, &incomplete_in, &adj) {
324 attempt_failed = true;
325 break;
326 }
327 }
328
329 if !attempt_failed {
330 return Ok(edges);
331 }
332 }
333
334 Err(IgraphError::Internal(
335 "k_regular_game: fast-heur exceeded restart budget; try multiple=true",
336 ))
337}
338
339fn directed_pair_feasible(
340 incomplete_out: &HashSet<VertexId>,
341 incomplete_in: &HashSet<VertexId>,
342 adj: &[HashSet<VertexId>],
343) -> bool {
344 for &from in incomplete_out {
345 for &to in incomplete_in {
346 if from != to && !adj[from as usize].contains(&to) {
347 return true;
348 }
349 }
350 }
351 false
352}
353
354pub fn k_regular_game(
392 n: u32,
393 k: u32,
394 directed: bool,
395 multiple: bool,
396 seed: u64,
397) -> IgraphResult<Graph> {
398 validate(n, k, directed, multiple)?;
399 if n == 0 {
400 return Graph::new(0, directed);
401 }
402 let mut rng = SplitMix64::new(seed);
403 let edges = if multiple {
404 configuration(n, k, directed, &mut rng)
405 } else if directed {
406 fast_heur_directed(n, k, &mut rng)?
407 } else {
408 fast_heur_undirected(n, k, &mut rng)?
409 };
410
411 let mut g = Graph::new(n, directed)?;
412 g.add_edges(edges)?;
413 Ok(g)
414}
415
416#[cfg(test)]
417mod tests {
418 use super::*;
419
420 fn degree_sequence(g: &Graph) -> Vec<u32> {
421 let mut deg = vec![0u32; g.vcount() as usize];
422 let m = u32::try_from(g.ecount()).expect("ecount fits in u32 in tests");
423 for eid in 0..m {
424 let (u, v) = g.edge(eid).expect("edge id in bounds");
425 if g.is_directed() {
426 deg[u as usize] += 1;
427 } else {
428 deg[u as usize] += 1;
431 deg[v as usize] += 1;
432 }
433 }
434 deg
435 }
436
437 fn out_degree_sequence(g: &Graph) -> Vec<u32> {
438 let mut deg = vec![0u32; g.vcount() as usize];
439 let m = u32::try_from(g.ecount()).expect("ecount fits in u32 in tests");
440 for eid in 0..m {
441 let (u, _v) = g.edge(eid).expect("edge id in bounds");
442 deg[u as usize] += 1;
443 }
444 deg
445 }
446
447 fn in_degree_sequence(g: &Graph) -> Vec<u32> {
448 let mut deg = vec![0u32; g.vcount() as usize];
449 let m = u32::try_from(g.ecount()).expect("ecount fits in u32 in tests");
450 for eid in 0..m {
451 let (_u, v) = g.edge(eid).expect("edge id in bounds");
452 deg[v as usize] += 1;
453 }
454 deg
455 }
456
457 fn is_simple(g: &Graph) -> bool {
458 let mut seen: HashSet<(VertexId, VertexId)> = HashSet::new();
459 let m = u32::try_from(g.ecount()).expect("ecount fits in u32 in tests");
460 for eid in 0..m {
461 let (u, v) = g.edge(eid).expect("edge id in bounds");
462 if u == v {
463 return false;
464 }
465 let key = if g.is_directed() {
466 (u, v)
467 } else {
468 (u.min(v), u.max(v))
469 };
470 if !seen.insert(key) {
471 return false;
472 }
473 }
474 true
475 }
476
477 #[test]
478 fn empty_graph() {
479 let g = k_regular_game(0, 0, false, false, 1).expect("empty");
480 assert_eq!(g.vcount(), 0);
481 assert_eq!(g.ecount(), 0);
482 }
483
484 #[test]
485 fn k_zero_returns_edgeless() {
486 let g = k_regular_game(7, 0, false, false, 1).expect("k=0");
487 assert_eq!(g.vcount(), 7);
488 assert_eq!(g.ecount(), 0);
489 }
490
491 #[test]
492 fn undirected_simple_2regular_on_6() {
493 let g = k_regular_game(6, 2, false, false, 0xABCD).expect("ok");
494 assert_eq!(g.vcount(), 6);
495 assert_eq!(g.ecount(), 6); assert!(is_simple(&g));
497 for d in degree_sequence(&g) {
498 assert_eq!(d, 2);
499 }
500 }
501
502 #[test]
503 fn undirected_simple_4regular_on_10() {
504 let g = k_regular_game(10, 4, false, false, 0xDEAD_BEEF).expect("ok");
505 assert_eq!(g.ecount(), 20);
506 assert!(is_simple(&g));
507 for d in degree_sequence(&g) {
508 assert_eq!(d, 4);
509 }
510 }
511
512 #[test]
513 fn directed_simple_3regular_on_8() {
514 let g = k_regular_game(8, 3, true, false, 0x1234_5678).expect("ok");
515 assert_eq!(g.ecount(), 24); assert!(is_simple(&g));
517 for d in out_degree_sequence(&g) {
518 assert_eq!(d, 3);
519 }
520 for d in in_degree_sequence(&g) {
521 assert_eq!(d, 3);
522 }
523 }
524
525 #[test]
526 fn undirected_multi_2regular_on_3() {
527 let g = k_regular_game(3, 2, false, true, 0xFEED).expect("ok");
530 assert_eq!(g.ecount(), 3);
531 for d in degree_sequence(&g) {
532 assert_eq!(d, 2);
533 }
534 }
535
536 #[test]
537 fn directed_multi_3regular_on_4() {
538 let g = k_regular_game(4, 3, true, true, 0xCAFE).expect("ok");
539 assert_eq!(g.ecount(), 12);
540 for d in out_degree_sequence(&g) {
541 assert_eq!(d, 3);
542 }
543 for d in in_degree_sequence(&g) {
544 assert_eq!(d, 3);
545 }
546 }
547
548 #[test]
549 fn undirected_simple_handshake_violation() {
550 let err = k_regular_game(5, 3, false, false, 1).unwrap_err();
552 assert!(matches!(err, IgraphError::InvalidArgument(_)));
553 }
554
555 #[test]
556 fn undirected_multi_handshake_violation() {
557 let err = k_regular_game(5, 3, false, true, 1).unwrap_err();
558 assert!(matches!(err, IgraphError::InvalidArgument(_)));
559 }
560
561 #[test]
562 fn simple_degree_too_high() {
563 let g = k_regular_game(5, 4, false, false, 7).expect("k = n - 1 ok");
565 assert_eq!(g.ecount(), 10);
566 assert!(is_simple(&g));
567
568 let err = k_regular_game(5, 5, false, false, 7).unwrap_err();
569 assert!(matches!(err, IgraphError::InvalidArgument(_)));
570 }
571
572 #[test]
573 fn determinism_same_seed_same_graph() {
574 let a = k_regular_game(20, 4, false, false, 0x42).expect("a");
575 let b = k_regular_game(20, 4, false, false, 0x42).expect("b");
576 assert_eq!(a.vcount(), b.vcount());
577 assert_eq!(a.ecount(), b.ecount());
578 let collect = |g: &Graph| -> Vec<(VertexId, VertexId)> {
579 let m = u32::try_from(g.ecount()).expect("fits");
580 (0..m).map(|e| g.edge(e).expect("ok")).collect()
581 };
582 assert_eq!(collect(&a), collect(&b));
583 }
584
585 #[test]
586 fn different_seeds_different_graphs() {
587 let a = k_regular_game(20, 4, false, false, 1).expect("a");
588 let b = k_regular_game(20, 4, false, false, 2).expect("b");
589 let collect = |g: &Graph| -> Vec<(VertexId, VertexId)> {
590 let m = u32::try_from(g.ecount()).expect("fits");
591 (0..m).map(|e| g.edge(e).expect("ok")).collect()
592 };
593 assert_ne!(collect(&a), collect(&b));
597 }
598
599 #[test]
600 fn undirected_simple_k_equals_n_minus_one_is_complete() {
601 let g = k_regular_game(5, 4, false, false, 0).expect("ok");
603 assert_eq!(g.ecount(), 10); assert!(is_simple(&g));
605 let mut seen: HashSet<(VertexId, VertexId)> = HashSet::new();
607 let m = u32::try_from(g.ecount()).expect("fits");
608 for eid in 0..m {
609 let (u, v) = g.edge(eid).expect("ok");
610 let key = (u.min(v), u.max(v));
611 seen.insert(key);
612 }
613 for a in 0..5u32 {
614 for b in (a + 1)..5 {
615 assert!(seen.contains(&(a, b)), "missing edge {a}-{b}");
616 }
617 }
618 }
619
620 #[test]
621 fn n_times_k_overflow() {
622 let err = k_regular_game(u32::MAX, 2, false, false, 1).unwrap_err();
623 assert!(matches!(err, IgraphError::InvalidArgument(_)));
624 }
625
626 #[test]
627 fn directed_simple_no_self_loops() {
628 let g = k_regular_game(7, 3, true, false, 0x7777).expect("ok");
629 let m = u32::try_from(g.ecount()).expect("fits");
630 for eid in 0..m {
631 let (u, v) = g.edge(eid).expect("ok");
632 assert_ne!(u, v, "self-loop in simple directed run");
633 }
634 }
635}
636
637#[cfg(all(test, feature = "proptest-harness"))]
638mod proptests {
639 use super::*;
640 use proptest::prelude::*;
641
642 proptest! {
643 #![proptest_config(ProptestConfig {
644 cases: 64,
645 .. ProptestConfig::default()
646 })]
647
648 #[test]
651 fn undirected_simple_invariants(
652 n in 2u32..=24,
653 k_in in 0u32..=6,
654 seed in any::<u64>(),
655 ) {
656 let k = if n % 2 == 1 && k_in % 2 == 1 { k_in + 1 } else { k_in };
657 let k = if k > n.saturating_sub(1) { n.saturating_sub(1) } else { k };
658 let k = if n % 2 == 1 && k % 2 == 1 { k.saturating_sub(1) } else { k };
659
660 let g = k_regular_game(n, k, false, false, seed)
661 .expect("valid params");
662 prop_assert_eq!(g.vcount() as u32, n);
663
664 let n_us = n as usize;
665 let m_expected = (n_us * k as usize) / 2;
666 prop_assert_eq!(g.ecount(), m_expected);
667
668 let mut deg = vec![0u32; n_us];
669 let mut seen: std::collections::HashSet<(u32, u32)> =
670 std::collections::HashSet::new();
671 let m = u32::try_from(g.ecount()).expect("fits");
672 for eid in 0..m {
673 let (u, v) = g.edge(eid).expect("ok");
674 prop_assert_ne!(u, v, "self-loop in simple run");
675 let key = (u.min(v), u.max(v));
676 prop_assert!(seen.insert(key), "duplicate edge");
677 deg[u as usize] += 1;
678 deg[v as usize] += 1;
679 }
680 for d in deg {
681 prop_assert_eq!(d, k);
682 }
683 }
684
685 #[test]
688 fn directed_simple_invariants(
689 n in 2u32..=18,
690 k in 0u32..=5,
691 seed in any::<u64>(),
692 ) {
693 let k = if k > n.saturating_sub(1) { n.saturating_sub(1) } else { k };
694 let g = k_regular_game(n, k, true, false, seed)
695 .expect("valid params");
696 let n_us = n as usize;
697 prop_assert_eq!(g.ecount(), n_us * k as usize);
698
699 let mut out_d = vec![0u32; n_us];
700 let mut in_d = vec![0u32; n_us];
701 let mut seen: std::collections::HashSet<(u32, u32)> =
702 std::collections::HashSet::new();
703 let m = u32::try_from(g.ecount()).expect("fits");
704 for eid in 0..m {
705 let (u, v) = g.edge(eid).expect("ok");
706 prop_assert_ne!(u, v, "self-loop");
707 prop_assert!(seen.insert((u, v)), "parallel directed edge");
708 out_d[u as usize] += 1;
709 in_d[v as usize] += 1;
710 }
711 for d in out_d {
712 prop_assert_eq!(d, k);
713 }
714 for d in in_d {
715 prop_assert_eq!(d, k);
716 }
717 }
718
719 #[test]
722 fn undirected_multi_invariants(
723 n in 1u32..=20,
724 k in 0u32..=8,
725 seed in any::<u64>(),
726 ) {
727 let n_us = n as usize;
728 let total = n_us * k as usize;
729 if total % 2 != 0 {
730 return Ok(());
731 }
732 let g = k_regular_game(n, k, false, true, seed)
733 .expect("parity ok");
734 prop_assert_eq!(g.ecount(), total / 2);
735 let mut deg = vec![0u32; n_us];
736 let m = u32::try_from(g.ecount()).expect("fits");
737 for eid in 0..m {
738 let (u, v) = g.edge(eid).expect("ok");
739 deg[u as usize] += 1;
740 deg[v as usize] += 1; }
742 for d in deg {
743 prop_assert_eq!(d, k);
744 }
745 }
746
747 #[test]
749 fn determinism(
750 n in 2u32..=16,
751 k in 0u32..=4,
752 directed in any::<bool>(),
753 multiple in any::<bool>(),
754 seed in any::<u64>(),
755 ) {
756 let n_us = n as usize;
757 let total = n_us * k as usize;
758 if !directed && (total % 2 != 0) {
759 return Ok(());
760 }
761 if !multiple && k > n.saturating_sub(1) {
762 return Ok(());
763 }
764 let a = k_regular_game(n, k, directed, multiple, seed)
765 .expect("valid");
766 let b = k_regular_game(n, k, directed, multiple, seed)
767 .expect("valid");
768 let collect = |g: &Graph| -> Vec<(u32, u32)> {
769 let m = u32::try_from(g.ecount()).expect("fits");
770 (0..m).map(|e| g.edge(e).expect("ok")).collect()
771 };
772 prop_assert_eq!(collect(&a), collect(&b));
773 }
774 }
775}