1#![allow(
77 unknown_lints,
78 clippy::cast_possible_truncation,
79 clippy::cast_sign_loss,
80 clippy::many_single_char_names,
81 clippy::needless_range_loop
82)]
83
84use std::collections::HashSet;
85
86use crate::algorithms::games::degree_sequence_fast_heur::{
87 checked_sum, is_graphical_simple_directed, is_graphical_simple_undirected,
88};
89use crate::core::rng::SplitMix64;
90use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
91
92pub const MAX_OUTER_ATTEMPTS: u32 = 1024;
94
95fn rng_integer_inclusive(rng: &mut SplitMix64, low: usize, high: usize) -> usize {
98 debug_assert!(low <= high, "rng_integer_inclusive: low ≤ high");
99 let span = (high - low) as u64 + 1;
100 low + (rng.next_u64() % span) as usize
101}
102
103fn build_stubs(degrees: &[u32], total: usize) -> Vec<u32> {
104 let mut stubs: Vec<u32> = Vec::with_capacity(total);
105 for (i, &d) in degrees.iter().enumerate() {
106 let v = i as u32;
107 for _ in 0..d {
108 stubs.push(v);
109 }
110 }
111 stubs
112}
113
114fn run_undirected(
115 degrees: &[u32],
116 n: u32,
117 rng: &mut SplitMix64,
118) -> IgraphResult<Vec<(VertexId, VertexId)>> {
119 let stub_count_u64 = checked_sum(degrees)?;
120 if stub_count_u64 % 2 != 0 {
121 return Err(IgraphError::InvalidArgument(
122 "degree_sequence_game_configuration_simple: undirected degree sum must be even"
123 .to_string(),
124 ));
125 }
126 let stub_count = stub_count_u64 as usize;
127 let ecount = stub_count / 2;
128 if ecount == 0 {
129 return Ok(Vec::new());
130 }
131
132 let mut stubs = build_stubs(degrees, stub_count);
133 let vcount = n as usize;
134 let mut adjacency: Vec<HashSet<u32>> = (0..vcount)
135 .map(|i| HashSet::with_capacity(degrees[i] as usize))
136 .collect();
137
138 for _attempt in 0..MAX_OUTER_ATTEMPTS {
139 let mut success = true;
140
141 for i in 0..ecount {
142 let k1 = rng_integer_inclusive(rng, 2 * i, stub_count - 1);
144 stubs.swap(2 * i, k1);
145 let k2 = rng_integer_inclusive(rng, 2 * i + 1, stub_count - 1);
146 stubs.swap(2 * i + 1, k2);
147
148 let from = stubs[2 * i];
149 let to = stubs[2 * i + 1];
150
151 if from == to {
152 success = false;
153 break;
154 }
155 if adjacency[to as usize].contains(&from) {
156 success = false;
157 break;
158 }
159 adjacency[to as usize].insert(from);
160 adjacency[from as usize].insert(to);
161 }
162
163 if success {
164 let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(ecount);
165 for i in 0..ecount {
166 edges.push((stubs[2 * i], stubs[2 * i + 1]));
167 }
168 return Ok(edges);
169 }
170
171 for set in &mut adjacency {
172 set.clear();
173 }
174 }
175
176 Err(IgraphError::InvalidArgument(format!(
177 "degree_sequence_game_configuration_simple: exhausted {MAX_OUTER_ATTEMPTS} rejection-sampling restarts; the sequence is graphical but very hostile to rejection sampling — try degree_sequence_game_fast_heur_simple or degree_sequence_game_vl",
178 )))
179}
180
181fn run_directed(
182 out_degrees: &[u32],
183 in_degrees: &[u32],
184 n: u32,
185 rng: &mut SplitMix64,
186) -> IgraphResult<Vec<(VertexId, VertexId)>> {
187 let out_total = checked_sum(out_degrees)? as usize;
188 let in_total = checked_sum(in_degrees)? as usize;
189 if out_total != in_total {
190 return Err(IgraphError::InvalidArgument(
191 "degree_sequence_game_configuration_simple: directed sums Σout and Σin must match"
192 .to_string(),
193 ));
194 }
195 let ecount = out_total;
196 if ecount == 0 {
197 return Ok(Vec::new());
198 }
199
200 let mut out_stubs = build_stubs(out_degrees, ecount);
201 let in_stubs = build_stubs(in_degrees, ecount);
202 let vcount = n as usize;
203
204 let mut vertex_done: Vec<u64> = vec![0u64; vcount];
209 let mut vertex_done_mark: u64 = 0;
210
211 for _attempt in 0..MAX_OUTER_ATTEMPTS {
212 let mut success = true;
213 let mut previous_to: i64 = -1;
214
215 for i in 0..ecount {
216 let k = rng_integer_inclusive(rng, i, ecount - 1);
217 out_stubs.swap(i, k);
218
219 let from = out_stubs[i];
220 let to = in_stubs[i];
221
222 if to == from {
223 success = false;
224 break;
225 }
226
227 if i64::from(to) != previous_to {
228 vertex_done_mark = vertex_done_mark.wrapping_add(1);
229 previous_to = i64::from(to);
230 }
231
232 if vertex_done[from as usize] == vertex_done_mark {
233 success = false;
234 break;
235 }
236 vertex_done[from as usize] = vertex_done_mark;
237 }
238
239 if success {
240 let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(ecount);
241 for i in 0..ecount {
242 edges.push((out_stubs[i], in_stubs[i]));
243 }
244 return Ok(edges);
245 }
246 }
251
252 Err(IgraphError::InvalidArgument(format!(
253 "degree_sequence_game_configuration_simple: exhausted {MAX_OUTER_ATTEMPTS} rejection-sampling restarts; the directed sequence pair is graphical but very hostile to rejection sampling",
254 )))
255}
256
257pub fn degree_sequence_game_configuration_simple(
297 out_degrees: &[u32],
298 in_degrees: Option<&[u32]>,
299 seed: u64,
300) -> IgraphResult<Graph> {
301 let directed = in_degrees.is_some();
302 let n = u32::try_from(out_degrees.len())
303 .map_err(|_| IgraphError::Internal("vertex count exceeds u32"))?;
304 if let Some(in_seq) = in_degrees {
305 if in_seq.len() != out_degrees.len() {
306 return Err(IgraphError::InvalidArgument(
307 "degree_sequence_game_configuration_simple: out_degrees and in_degrees must have the same length".to_string(),
308 ));
309 }
310 }
311
312 if directed {
313 let Some(in_seq) = in_degrees else {
314 return Err(IgraphError::InvalidArgument(
315 "directed graph requires in_degrees".to_string(),
316 ));
317 };
318 if !is_graphical_simple_directed(out_degrees, in_seq) {
319 return Err(IgraphError::InvalidArgument(
320 "degree_sequence_game_configuration_simple: degree pair is not realisable as a simple directed graph (Fulkerson–Chen–Anstee)"
321 .to_string(),
322 ));
323 }
324 } else if !is_graphical_simple_undirected(out_degrees) {
325 return Err(IgraphError::InvalidArgument(
326 "degree_sequence_game_configuration_simple: degree sequence is not realisable as a simple undirected graph (Erdős–Gallai)"
327 .to_string(),
328 ));
329 }
330
331 let mut rng = SplitMix64::new(seed);
332
333 let edges = if directed {
334 let Some(in_seq) = in_degrees else {
335 return Err(IgraphError::InvalidArgument(
336 "directed graph requires in_degrees".to_string(),
337 ));
338 };
339 run_directed(out_degrees, in_seq, n, &mut rng)?
340 } else {
341 run_undirected(out_degrees, n, &mut rng)?
342 };
343
344 let mut g = Graph::new(n, directed)?;
345 g.add_edges(edges)?;
346 Ok(g)
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352 use crate::algorithms::properties::{SimpleMode, is_simple_with_mode};
353
354 fn observed_degrees(g: &Graph) -> Vec<u32> {
355 let n = g.vcount() as usize;
356 let mut deg = vec![0u32; n];
357 let ec = u32::try_from(g.ecount()).expect("ecount fits u32");
358 for eid in 0..ec {
359 let (s, t) = g.edge(eid).expect("eid in bounds");
360 deg[s as usize] = deg[s as usize].saturating_add(1);
361 deg[t as usize] = deg[t as usize].saturating_add(1);
362 }
363 deg
364 }
365
366 fn observed_out_in(g: &Graph) -> (Vec<u32>, Vec<u32>) {
367 let n = g.vcount() as usize;
368 let mut out = vec![0u32; n];
369 let mut inv = vec![0u32; n];
370 let ec = u32::try_from(g.ecount()).expect("ecount fits u32");
371 for eid in 0..ec {
372 let (s, t) = g.edge(eid).expect("eid in bounds");
373 out[s as usize] = out[s as usize].saturating_add(1);
374 inv[t as usize] = inv[t as usize].saturating_add(1);
375 }
376 (out, inv)
377 }
378
379 #[test]
380 fn undirected_empty_sequence_yields_empty_graph() {
381 let g = degree_sequence_game_configuration_simple(&[], None, 1).expect("empty ok");
382 assert_eq!(g.vcount(), 0);
383 assert_eq!(g.ecount(), 0);
384 assert!(!g.is_directed());
385 }
386
387 #[test]
388 fn undirected_singleton_zero_yields_isolated_vertex() {
389 let g = degree_sequence_game_configuration_simple(&[0], None, 1).expect("singleton ok");
390 assert_eq!(g.vcount(), 1);
391 assert_eq!(g.ecount(), 0);
392 }
393
394 #[test]
395 fn undirected_all_isolated_n5_yields_no_edges() {
396 let g = degree_sequence_game_configuration_simple(&[0; 5], None, 42).expect("ok");
397 assert_eq!(g.vcount(), 5);
398 assert_eq!(g.ecount(), 0);
399 }
400
401 #[test]
402 fn undirected_4cycle_preserves_degrees_and_is_simple() {
403 let g = degree_sequence_game_configuration_simple(&[2, 2, 2, 2], None, 7).expect("ok");
404 assert_eq!(g.vcount(), 4);
405 assert_eq!(g.ecount(), 4);
406 assert_eq!(observed_degrees(&g), vec![2, 2, 2, 2]);
407 assert!(is_simple_with_mode(&g, SimpleMode::DirectedAsDirected).unwrap());
408 }
409
410 #[test]
411 fn undirected_3regular_n6_preserves_degrees() {
412 let degrees: Vec<u32> = vec![3; 6];
413 let g = degree_sequence_game_configuration_simple(°rees, None, 0xABCD_u64).expect("ok");
414 assert_eq!(observed_degrees(&g), degrees);
415 assert!(is_simple_with_mode(&g, SimpleMode::DirectedAsDirected).unwrap());
416 }
417
418 #[test]
419 fn undirected_skewed_powerlaw_preserves_degrees_sorted() {
420 let degrees: Vec<u32> = vec![5, 4, 4, 3, 3, 3, 2, 2, 2, 2];
421 let g = degree_sequence_game_configuration_simple(°rees, None, 0xC0FE_u64).expect("ok");
422 assert_eq!(observed_degrees(&g), degrees);
423 assert!(is_simple_with_mode(&g, SimpleMode::DirectedAsDirected).unwrap());
424 }
425
426 #[test]
427 fn undirected_3regular_n30_preserves_degrees() {
428 let degrees: Vec<u32> = vec![3; 30];
429 let g =
430 degree_sequence_game_configuration_simple(°rees, None, 0xDEAD_F00D_u64).expect("ok");
431 assert_eq!(observed_degrees(&g), degrees);
432 assert!(is_simple_with_mode(&g, SimpleMode::DirectedAsDirected).unwrap());
433 }
434
435 #[test]
436 fn undirected_odd_sum_rejected() {
437 let err = degree_sequence_game_configuration_simple(&[1, 1, 1], None, 1).unwrap_err();
438 matches!(err, IgraphError::InvalidArgument(_));
439 }
440
441 #[test]
442 fn undirected_negative_eg_rejected_max_too_large() {
443 let err = degree_sequence_game_configuration_simple(&[5, 3, 1, 1], None, 1).unwrap_err();
445 matches!(err, IgraphError::InvalidArgument(_));
446 }
447
448 #[test]
449 fn deterministic_same_seed_undirected() {
450 let degrees: Vec<u32> = vec![3; 8];
451 let g1 = degree_sequence_game_configuration_simple(°rees, None, 4242).expect("ok");
452 let g2 = degree_sequence_game_configuration_simple(°rees, None, 4242).expect("ok");
453 let mut e1: Vec<(u32, u32)> = (0..u32::try_from(g1.ecount()).unwrap())
454 .map(|i| {
455 let (a, b) = g1.edge(i).unwrap();
456 if a < b { (a, b) } else { (b, a) }
457 })
458 .collect();
459 let mut e2: Vec<(u32, u32)> = (0..u32::try_from(g2.ecount()).unwrap())
460 .map(|i| {
461 let (a, b) = g2.edge(i).unwrap();
462 if a < b { (a, b) } else { (b, a) }
463 })
464 .collect();
465 e1.sort_unstable();
466 e2.sort_unstable();
467 assert_eq!(e1, e2);
468 }
469
470 #[test]
471 fn directed_empty_sequence_yields_empty_graph() {
472 let g = degree_sequence_game_configuration_simple(&[], Some(&[]), 1).expect("ok");
473 assert_eq!(g.vcount(), 0);
474 assert_eq!(g.ecount(), 0);
475 assert!(g.is_directed());
476 }
477
478 #[test]
479 fn directed_2cycle_preserves_in_out() {
480 let g = degree_sequence_game_configuration_simple(&[1, 1], Some(&[1, 1]), 9).expect("ok");
481 let (out, inv) = observed_out_in(&g);
482 assert_eq!(out, vec![1, 1]);
483 assert_eq!(inv, vec![1, 1]);
484 assert!(is_simple_with_mode(&g, SimpleMode::DirectedAsDirected).unwrap());
485 }
486
487 #[test]
488 fn directed_balanced_n6_d2_preserves_degrees() {
489 let n = 6;
490 let out = vec![2u32; n];
491 let inv = vec![2u32; n];
492 let g =
493 degree_sequence_game_configuration_simple(&out, Some(&inv), 0xC0DE_u64).expect("ok");
494 let (got_out, got_in) = observed_out_in(&g);
495 assert_eq!(got_out, out);
496 assert_eq!(got_in, inv);
497 assert!(is_simple_with_mode(&g, SimpleMode::DirectedAsDirected).unwrap());
498 }
499
500 #[test]
501 fn directed_unequal_sums_rejected() {
502 let err =
503 degree_sequence_game_configuration_simple(&[1, 1, 1], Some(&[1, 1, 0]), 1).unwrap_err();
504 matches!(err, IgraphError::InvalidArgument(_));
505 }
506
507 #[test]
508 fn directed_length_mismatch_rejected() {
509 let err =
510 degree_sequence_game_configuration_simple(&[1, 1], Some(&[1, 1, 0]), 1).unwrap_err();
511 matches!(err, IgraphError::InvalidArgument(_));
512 }
513
514 #[test]
515 fn directed_deterministic_same_seed() {
516 let out = vec![2u32; 5];
517 let inv = vec![2u32; 5];
518 let g1 = degree_sequence_game_configuration_simple(&out, Some(&inv), 12345).expect("ok");
519 let g2 = degree_sequence_game_configuration_simple(&out, Some(&inv), 12345).expect("ok");
520 let e1: Vec<(u32, u32)> = (0..u32::try_from(g1.ecount()).unwrap())
521 .map(|i| g1.edge(i).unwrap())
522 .collect();
523 let e2: Vec<(u32, u32)> = (0..u32::try_from(g2.ecount()).unwrap())
524 .map(|i| g2.edge(i).unwrap())
525 .collect();
526 assert_eq!(e1, e2);
527 }
528}
529
530#[cfg(all(test, feature = "proptest-harness"))]
531mod proptest_invariants {
532 use super::*;
533 use proptest::prelude::*;
534
535 fn graphical_undirected_strategy() -> impl Strategy<Value = Vec<u32>> {
544 (4usize..=8).prop_flat_map(|n| {
545 let cap = ((n as u32) / 2).max(1);
546 prop::collection::vec(0u32..=cap, n).prop_filter(
547 "must be graphical (even sum + EG)",
548 move |seq| {
549 let sum: u64 = seq.iter().map(|&d| u64::from(d)).sum();
550 sum % 2 == 0 && is_graphical_simple_undirected(seq)
551 },
552 )
553 })
554 }
555
556 proptest! {
557 #[test]
558 fn degrees_preserved_undirected(seq in graphical_undirected_strategy(), seed in any::<u64>()) {
559 let g = degree_sequence_game_configuration_simple(&seq, None, seed)
560 .expect("graphical sequence must succeed");
561 let n = g.vcount() as usize;
562 let mut deg = vec![0u32; n];
563 let ec = u32::try_from(g.ecount()).unwrap();
564 for eid in 0..ec {
565 let (s, t) = g.edge(eid).unwrap();
566 deg[s as usize] += 1;
567 deg[t as usize] += 1;
568 }
569 prop_assert_eq!(deg, seq);
570 }
571
572 #[test]
573 fn simple_no_loops_no_multi_undirected(seq in graphical_undirected_strategy(), seed in any::<u64>()) {
574 use crate::algorithms::properties::{SimpleMode, is_simple_with_mode};
575 let g = degree_sequence_game_configuration_simple(&seq, None, seed)
576 .expect("graphical sequence must succeed");
577 prop_assert!(is_simple_with_mode(&g, SimpleMode::DirectedAsDirected).unwrap());
578 }
579
580 #[test]
581 fn same_seed_same_graph(seq in graphical_undirected_strategy(), seed in any::<u64>()) {
582 let g1 = degree_sequence_game_configuration_simple(&seq, None, seed).unwrap();
583 let g2 = degree_sequence_game_configuration_simple(&seq, None, seed).unwrap();
584 let e1: Vec<(u32, u32)> = (0..u32::try_from(g1.ecount()).unwrap())
585 .map(|i| g1.edge(i).unwrap())
586 .collect();
587 let e2: Vec<(u32, u32)> = (0..u32::try_from(g2.ecount()).unwrap())
588 .map(|i| g2.edge(i).unwrap())
589 .collect();
590 prop_assert_eq!(e1, e2);
591 }
592 }
593}