1#![allow(clippy::cast_possible_truncation)]
50
51use crate::core::rng::SplitMix64;
52use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
53
54fn checked_sum(degrees: &[u32]) -> IgraphResult<u64> {
56 let mut acc: u64 = 0;
57 for &d in degrees {
58 acc = acc
59 .checked_add(u64::from(d))
60 .ok_or(IgraphError::Internal("degree-sum overflow"))?;
61 }
62 Ok(acc)
63}
64
65fn build_bag(degrees: &[u32], total: u64) -> IgraphResult<Vec<VertexId>> {
68 let cap = usize::try_from(total)
69 .map_err(|_| IgraphError::Internal("degree-sequence stub count exceeds usize"))?;
70 let mut bag: Vec<VertexId> = Vec::with_capacity(cap);
71 for (i, &d) in degrees.iter().enumerate() {
72 let vid = VertexId::try_from(i)
73 .map_err(|_| IgraphError::Internal("degree-sequence vertex index exceeds u32"))?;
74 for _ in 0..d {
75 bag.push(vid);
76 }
77 }
78 Ok(bag)
79}
80
81pub fn degree_sequence_game_configuration(
116 out_degrees: &[u32],
117 in_degrees: Option<&[u32]>,
118 seed: u64,
119) -> IgraphResult<Graph> {
120 let directed = in_degrees.is_some();
121 let n_usize = out_degrees.len();
122 let n = u32::try_from(n_usize)
123 .map_err(|_| IgraphError::Internal("degree-sequence vertex count exceeds u32"))?;
124
125 if let Some(in_seq) = in_degrees {
126 if in_seq.len() != n_usize {
127 return Err(IgraphError::InvalidArgument(format!(
128 "degree_sequence_game_configuration: out_degrees has length {} but in_degrees has length {}",
129 n_usize,
130 in_seq.len()
131 )));
132 }
133 }
134
135 if n == 0 {
136 return Graph::new(0, directed);
137 }
138
139 let outsum = checked_sum(out_degrees)?;
140 let (insum, no_of_edges) = if let Some(in_seq) = in_degrees {
141 let insum = checked_sum(in_seq)?;
142 if outsum != insum {
143 return Err(IgraphError::InvalidArgument(format!(
144 "degree_sequence_game_configuration: out-degree sum {outsum} != in-degree sum {insum} (no directed graph realises the given sequences)"
145 )));
146 }
147 (insum, outsum)
148 } else {
149 if outsum % 2 != 0 {
150 return Err(IgraphError::InvalidArgument(format!(
151 "degree_sequence_game_configuration: undirected degree sum {outsum} is odd (no graph realises an odd-sum degree sequence)"
152 )));
153 }
154 (0u64, outsum / 2)
155 };
156
157 if no_of_edges > u64::from(u32::MAX) {
158 return Err(IgraphError::Internal(
159 "degree_sequence_game_configuration: edge count exceeds u32::MAX",
160 ));
161 }
162 let no_of_edges_usize = no_of_edges as usize;
163
164 let mut bag1 = build_bag(out_degrees, outsum)?;
165 let mut bag2_opt: Option<Vec<VertexId>> = if let Some(in_seq) = in_degrees {
166 Some(build_bag(in_seq, insum)?)
167 } else {
168 None
169 };
170
171 let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(no_of_edges_usize);
172 let mut rng = SplitMix64::new(seed);
173
174 if let Some(bag2) = bag2_opt.as_mut() {
175 for _ in 0..no_of_edges_usize {
176 let from_idx = rng.gen_index(bag1.len());
177 let to_idx = rng.gen_index(bag2.len());
178 let from = bag1.swap_remove(from_idx);
179 let to = bag2.swap_remove(to_idx);
180 edges.push((from, to));
181 }
182 } else {
183 for _ in 0..no_of_edges_usize {
184 let from_idx = rng.gen_index(bag1.len());
185 let from = bag1.swap_remove(from_idx);
186 let to_idx = rng.gen_index(bag1.len());
188 let to = bag1.swap_remove(to_idx);
189 edges.push((from, to));
190 }
191 }
192
193 let mut g = Graph::new(n, directed)?;
194 g.add_edges(edges)?;
195 Ok(g)
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 fn observed_undirected_degrees(graph: &Graph) -> Vec<u32> {
203 let vcount = graph.vcount() as usize;
204 let mut deg = vec![0u32; vcount];
205 let ecount = u32::try_from(graph.ecount()).expect("test ecount fits u32");
206 for eid in 0..ecount {
207 let (src, dst) = graph.edge(eid).expect("test edge id in bounds");
208 deg[src as usize] = deg[src as usize].saturating_add(1);
209 deg[dst as usize] = deg[dst as usize].saturating_add(1);
210 }
211 deg
212 }
213
214 fn observed_directed_degrees(graph: &Graph) -> (Vec<u32>, Vec<u32>) {
215 let vcount = graph.vcount() as usize;
216 let mut outd = vec![0u32; vcount];
217 let mut ind = vec![0u32; vcount];
218 let ecount = u32::try_from(graph.ecount()).expect("test ecount fits u32");
219 for eid in 0..ecount {
220 let (src, dst) = graph.edge(eid).expect("test edge id in bounds");
221 outd[src as usize] = outd[src as usize].saturating_add(1);
222 ind[dst as usize] = ind[dst as usize].saturating_add(1);
223 }
224 (outd, ind)
225 }
226
227 #[test]
228 fn empty_degree_sequence_returns_empty_graph() {
229 let g = degree_sequence_game_configuration(&[], None, 0).unwrap();
230 assert_eq!(g.vcount(), 0);
231 assert_eq!(g.ecount(), 0);
232 assert!(!g.is_directed());
233 }
234
235 #[test]
236 fn empty_directed_degree_sequence_returns_empty_graph() {
237 let g = degree_sequence_game_configuration(&[], Some(&[]), 0).unwrap();
238 assert_eq!(g.vcount(), 0);
239 assert_eq!(g.ecount(), 0);
240 assert!(g.is_directed());
241 }
242
243 #[test]
244 fn all_zero_undirected_produces_edgeless_graph() {
245 let g = degree_sequence_game_configuration(&[0, 0, 0, 0, 0], None, 1).unwrap();
246 assert_eq!(g.vcount(), 5);
247 assert_eq!(g.ecount(), 0);
248 }
249
250 #[test]
251 fn all_zero_directed_produces_edgeless_graph() {
252 let g = degree_sequence_game_configuration(&[0, 0, 0], Some(&[0, 0, 0]), 1).unwrap();
253 assert_eq!(g.vcount(), 3);
254 assert_eq!(g.ecount(), 0);
255 }
256
257 #[test]
258 fn observed_undirected_degrees_match_input() {
259 let seq = [2, 3, 2, 3, 3, 3, 3, 1, 4, 4];
260 let g = degree_sequence_game_configuration(&seq, None, 333).unwrap();
261 assert_eq!(g.vcount(), seq.len() as u32);
262 let observed = observed_undirected_degrees(&g);
263 assert_eq!(observed, seq);
264 }
265
266 #[test]
267 fn observed_directed_degrees_match_input() {
268 let out_seq = [2, 3, 2, 3, 3, 3, 3, 1, 4, 4];
269 let in_seq = [3, 6, 2, 0, 2, 2, 4, 3, 3, 3];
270 let g = degree_sequence_game_configuration(&out_seq, Some(&in_seq), 333).unwrap();
271 assert_eq!(g.vcount(), out_seq.len() as u32);
272 let (outd, ind) = observed_directed_degrees(&g);
273 assert_eq!(outd, out_seq);
274 assert_eq!(ind, in_seq);
275 }
276
277 #[test]
278 fn ecount_equals_half_undirected_sum() {
279 let seq = [4u32, 4, 4, 4, 4, 4, 4, 4]; let g = degree_sequence_game_configuration(&seq, None, 17).unwrap();
281 assert_eq!(g.ecount(), 16);
282 }
283
284 #[test]
285 fn ecount_equals_outsum_directed() {
286 let out_seq = [2u32, 1, 3];
287 let in_seq = [1u32, 3, 2];
288 let g = degree_sequence_game_configuration(&out_seq, Some(&in_seq), 11).unwrap();
289 assert_eq!(g.ecount(), 6);
290 }
291
292 #[test]
293 fn rejects_length_mismatch() {
294 let err = degree_sequence_game_configuration(&[1, 2, 3], Some(&[1, 2]), 1).unwrap_err();
295 match err {
296 IgraphError::InvalidArgument(msg) => assert!(msg.contains("length")),
297 other => panic!("expected InvalidArgument, got {other:?}"),
298 }
299 }
300
301 #[test]
302 fn rejects_odd_undirected_sum() {
303 let err = degree_sequence_game_configuration(&[1, 1, 1], None, 1).unwrap_err();
304 match err {
305 IgraphError::InvalidArgument(msg) => assert!(msg.contains("odd")),
306 other => panic!("expected InvalidArgument, got {other:?}"),
307 }
308 }
309
310 #[test]
311 fn rejects_directed_sum_mismatch() {
312 let err = degree_sequence_game_configuration(&[1, 2, 3], Some(&[1, 1, 1]), 1).unwrap_err();
313 match err {
314 IgraphError::InvalidArgument(msg) => {
315 assert!(msg.contains("out-degree sum") && msg.contains("in-degree sum"));
316 }
317 other => panic!("expected InvalidArgument, got {other:?}"),
318 }
319 }
320
321 #[test]
322 fn deterministic_same_seed_same_edges() {
323 let seq = [3u32, 3, 3, 3, 3, 3];
324 let g1 = degree_sequence_game_configuration(&seq, None, 42).unwrap();
325 let g2 = degree_sequence_game_configuration(&seq, None, 42).unwrap();
326 let collect = |g: &Graph| -> Vec<(VertexId, VertexId)> {
327 let m = u32::try_from(g.ecount()).unwrap();
328 let mut v: Vec<_> = (0..m)
329 .map(|e| {
330 let (u, w) = g.edge(e).unwrap();
331 if u <= w { (u, w) } else { (w, u) }
332 })
333 .collect();
334 v.sort_unstable();
335 v
336 };
337 assert_eq!(collect(&g1), collect(&g2));
338 }
339
340 #[test]
341 fn distinct_seeds_usually_differ() {
342 let seq = [4u32, 4, 4, 4, 4, 4, 4, 4];
343 let g1 = degree_sequence_game_configuration(&seq, None, 1).unwrap();
344 let g2 = degree_sequence_game_configuration(&seq, None, 2).unwrap();
345 let collect = |g: &Graph| -> Vec<(VertexId, VertexId)> {
346 let m = u32::try_from(g.ecount()).unwrap();
347 let mut v: Vec<_> = (0..m)
348 .map(|e| {
349 let (u, w) = g.edge(e).unwrap();
350 if u <= w { (u, w) } else { (w, u) }
351 })
352 .collect();
353 v.sort_unstable();
354 v
355 };
356 assert_ne!(collect(&g1), collect(&g2));
357 }
358
359 #[test]
360 fn single_vertex_with_self_loop_via_d2() {
361 let g = degree_sequence_game_configuration(&[2], None, 1).unwrap();
363 assert_eq!(g.vcount(), 1);
364 assert_eq!(g.ecount(), 1);
365 let (u, v) = g.edge(0).unwrap();
366 assert_eq!((u, v), (0, 0));
367 }
368
369 #[test]
370 fn directed_single_vertex_self_loop() {
371 let g = degree_sequence_game_configuration(&[1], Some(&[1]), 1).unwrap();
373 assert_eq!(g.vcount(), 1);
374 assert_eq!(g.ecount(), 1);
375 let (u, v) = g.edge(0).unwrap();
376 assert_eq!((u, v), (0, 0));
377 }
378
379 #[test]
380 fn directed_path_realisation() {
381 let g = degree_sequence_game_configuration(&[1, 1, 0], Some(&[0, 1, 1]), 1).unwrap();
383 assert_eq!(g.vcount(), 3);
384 assert_eq!(g.ecount(), 2);
385 let edges: Vec<_> = (0..2).map(|e| g.edge(e).unwrap()).collect();
386 let mut got = edges.clone();
387 got.sort_unstable();
388 assert_eq!(got, vec![(0, 1), (1, 2)]);
389 }
390
391 #[test]
392 fn determinism_across_sweep() {
393 for n in [3u32, 8, 20, 50] {
395 let seq: Vec<u32> = (0..n).map(|i| 2 + (i % 3)).collect();
396 let mut seq = seq;
398 let s: u32 = seq.iter().sum();
399 if s % 2 != 0 {
400 seq[0] += 1;
401 }
402 for seed in [0u64, 1, 7, 1_234_567] {
403 let g1 = degree_sequence_game_configuration(&seq, None, seed).unwrap();
404 let g2 = degree_sequence_game_configuration(&seq, None, seed).unwrap();
405 assert_eq!(g1.vcount(), g2.vcount());
406 assert_eq!(g1.ecount(), g2.ecount());
407 assert_eq!(
408 observed_undirected_degrees(&g1),
409 observed_undirected_degrees(&g2)
410 );
411 }
412 }
413 }
414
415 #[test]
416 fn all_ones_undirected_is_perfect_matching_size() {
417 let seq = [1u32; 10];
418 let g = degree_sequence_game_configuration(&seq, None, 99).unwrap();
419 assert_eq!(g.ecount(), 5);
420 let observed = observed_undirected_degrees(&g);
421 assert_eq!(observed, seq);
422 }
423
424 #[test]
425 fn large_uniform_directed_passes_degree_check() {
426 let n = 100u32;
427 let d = 5u32;
428 let out_seq = vec![d; n as usize];
429 let in_seq = vec![d; n as usize];
430 let g = degree_sequence_game_configuration(&out_seq, Some(&in_seq), 0xABCD).unwrap();
431 assert_eq!(g.vcount(), n);
432 assert_eq!(g.ecount(), (n * d) as usize);
433 let (outd, ind) = observed_directed_degrees(&g);
434 assert_eq!(outd, out_seq);
435 assert_eq!(ind, in_seq);
436 }
437
438 #[test]
439 fn directed_zero_in_zero_out_disconnected_vertex_allowed() {
440 let out_seq = [1u32, 1, 0];
442 let in_seq = [1u32, 1, 0];
443 let g = degree_sequence_game_configuration(&out_seq, Some(&in_seq), 0).unwrap();
444 let (outd, ind) = observed_directed_degrees(&g);
445 assert_eq!(outd, out_seq);
446 assert_eq!(ind, in_seq);
447 }
448
449 #[test]
450 fn rejects_directed_with_out_zero_in_nonzero() {
451 let err = degree_sequence_game_configuration(&[0, 0], Some(&[1, 1]), 1).unwrap_err();
453 match err {
454 IgraphError::InvalidArgument(_) => {}
455 other => panic!("expected InvalidArgument, got {other:?}"),
456 }
457 }
458}
459
460#[cfg(all(test, feature = "proptest-harness"))]
461mod proptests {
462 use super::*;
463 use proptest::prelude::*;
464
465 fn even_sum_seq() -> impl Strategy<Value = Vec<u32>> {
466 prop::collection::vec(0u32..6, 0..15).prop_map(|mut v| {
467 let s: u32 = v.iter().sum();
468 if s % 2 != 0 && !v.is_empty() {
469 v[0] += 1;
470 }
471 v
472 })
473 }
474
475 fn matched_directed_seqs() -> impl Strategy<Value = (Vec<u32>, Vec<u32>)> {
476 prop::collection::vec((0u32..5, 0u32..5), 0..12).prop_map(|pairs| {
477 let mut out: Vec<u32> = pairs.iter().map(|p| p.0).collect();
478 let mut inn: Vec<u32> = pairs.iter().map(|p| p.1).collect();
479 let os: u64 = out.iter().map(|&d| u64::from(d)).sum();
480 let is: u64 = inn.iter().map(|&d| u64::from(d)).sum();
481 if !out.is_empty() {
482 if os > is {
483 inn[0] = inn[0].saturating_add(u32::try_from(os - is).unwrap_or(0));
484 } else if is > os {
485 out[0] = out[0].saturating_add(u32::try_from(is - os).unwrap_or(0));
486 }
487 }
488 (out, inn)
489 })
490 }
491
492 proptest! {
493 #![proptest_config(ProptestConfig::with_cases(64))]
494
495 #[test]
496 fn undirected_degree_match(seq in even_sum_seq(), seed in any::<u64>()) {
497 let g = degree_sequence_game_configuration(&seq, None, seed)
498 .expect("valid input must succeed");
499 prop_assert_eq!(g.vcount(), seq.len() as u32);
500 let sum: u64 = seq.iter().map(|&d| u64::from(d)).sum();
501 prop_assert_eq!(g.ecount() as u64, sum / 2);
502 let n = u32::try_from(g.vcount()).unwrap();
503 let mut deg = vec![0u32; n as usize];
504 let m = u32::try_from(g.ecount()).unwrap();
505 for eid in 0..m {
506 let (u, v) = g.edge(eid).unwrap();
507 deg[u as usize] += 1;
508 deg[v as usize] += 1;
509 }
510 prop_assert_eq!(deg, seq);
511 }
512
513 #[test]
514 fn directed_degree_match((out_seq, in_seq) in matched_directed_seqs(), seed in any::<u64>()) {
515 let g = degree_sequence_game_configuration(&out_seq, Some(&in_seq), seed)
516 .expect("valid input must succeed");
517 prop_assert_eq!(g.vcount(), out_seq.len() as u32);
518 let n = u32::try_from(g.vcount()).unwrap();
519 let mut outd = vec![0u32; n as usize];
520 let mut ind = vec![0u32; n as usize];
521 let m = u32::try_from(g.ecount()).unwrap();
522 for eid in 0..m {
523 let (u, v) = g.edge(eid).unwrap();
524 outd[u as usize] += 1;
525 ind[v as usize] += 1;
526 }
527 prop_assert_eq!(outd, out_seq);
528 prop_assert_eq!(ind, in_seq);
529 }
530
531 #[test]
532 fn deterministic_same_seed(seq in even_sum_seq(), seed in any::<u64>()) {
533 let g1 = degree_sequence_game_configuration(&seq, None, seed)
534 .expect("valid input must succeed");
535 let g2 = degree_sequence_game_configuration(&seq, None, seed)
536 .expect("valid input must succeed");
537 let collect = |g: &Graph| -> Vec<(VertexId, VertexId)> {
538 let m = u32::try_from(g.ecount()).unwrap();
539 let mut v: Vec<_> = (0..m).map(|e| g.edge(e).unwrap()).collect();
540 v.sort_unstable();
541 v
542 };
543 prop_assert_eq!(collect(&g1), collect(&g2));
544 }
545
546 #[test]
547 fn odd_sum_rejected(seq in prop::collection::vec(0u32..6, 1..15)) {
548 let s: u32 = seq.iter().sum();
549 prop_assume!(s % 2 != 0);
550 let r = degree_sequence_game_configuration(&seq, None, 0);
551 prop_assert!(r.is_err());
552 }
553 }
554}