1#![allow(
43 unknown_lints,
44 clippy::cast_possible_truncation,
45 clippy::cast_precision_loss,
46 clippy::cast_sign_loss,
47 clippy::float_cmp,
48 clippy::manual_midpoint
49)]
50
51use std::collections::HashSet;
52
53use crate::core::rng::SplitMix64;
54use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
55
56fn decode_pair(idx: u64, n: u32, directed: bool, loops: bool) -> (VertexId, VertexId) {
60 let n_u64 = u64::from(n);
61 if directed && loops {
62 let to = idx / n_u64;
64 let from = idx - to * n_u64;
65 debug_assert!(from < n_u64 && to < n_u64);
66 #[allow(clippy::cast_possible_truncation)]
67 ((from as VertexId), (to as VertexId))
68 } else if directed && !loops {
69 let to = idx / n_u64;
72 let from = idx - to * n_u64;
73 let to = if from == to { n_u64 - 1 } else { to };
74 debug_assert!(from < n_u64 && to < n_u64 && from != to);
75 #[allow(clippy::cast_possible_truncation)]
76 ((from as VertexId), (to as VertexId))
77 } else if !directed && loops {
78 #[allow(clippy::cast_precision_loss)]
83 let idx_f = idx as f64;
84 let to_f = ((8.0 * idx_f + 1.0).sqrt() - 1.0) / 2.0;
85 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
86 let mut to = to_f.trunc() as u64;
87 let mut from = idx - to * (to + 1) / 2;
91 while from > to {
92 to += 1;
93 from = idx - to * (to + 1) / 2;
94 }
95 debug_assert!(from <= to && to < n_u64);
96 #[allow(clippy::cast_possible_truncation)]
97 ((from as VertexId), (to as VertexId))
98 } else {
99 #[allow(clippy::cast_precision_loss)]
104 let idx_f = idx as f64;
105 let to_f = ((8.0 * idx_f + 1.0).sqrt() + 1.0) / 2.0;
106 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
107 let mut to = to_f.trunc() as u64;
108 if to < 1 {
109 to = 1;
110 }
111 let mut from = idx - to * (to - 1) / 2;
112 while from >= to {
113 to += 1;
114 from = idx - to * (to - 1) / 2;
115 }
116 debug_assert!(from < to && to < n_u64);
117 #[allow(clippy::cast_possible_truncation)]
118 ((from as VertexId), (to as VertexId))
119 }
120}
121
122fn max_edges(n: u32, directed: bool, loops: bool) -> u64 {
126 let n = u64::from(n);
127 match (directed, loops) {
128 (true, true) => n * n,
129 (true, false) => n * n.saturating_sub(1),
130 (false, true) => n * (n + 1) / 2,
131 (false, false) => n * n.saturating_sub(1) / 2,
132 }
133}
134
135fn distinct_sample(rng: &mut SplitMix64, n_pairs: u64, m: u64) -> Vec<u64> {
147 debug_assert!(m <= n_pairs);
148 let cap = usize::try_from(m).unwrap_or(usize::MAX);
149 let mut chosen: HashSet<u64> = HashSet::with_capacity(cap);
150 let mut out: Vec<u64> = Vec::with_capacity(cap);
151 for j in (n_pairs - m)..n_pairs {
152 let span = j + 1;
155 let span_usize = usize::try_from(span).unwrap_or(usize::MAX);
156 let t_usize = rng.gen_index(span_usize);
159 let t = t_usize as u64;
160 let pick = if chosen.insert(t) {
161 t
162 } else {
163 chosen.insert(j);
164 j
165 };
166 out.push(pick);
167 }
168 out.sort_unstable();
169 out
170}
171
172fn validate_gnp(p: f64) -> IgraphResult<()> {
173 if !p.is_finite() {
174 return Err(IgraphError::InvalidArgument(format!(
175 "G(n,p) probability must be finite (got {p})"
176 )));
177 }
178 if !(0.0..=1.0).contains(&p) {
179 return Err(IgraphError::InvalidArgument(format!(
180 "G(n,p) probability must be in [0, 1] (got {p})"
181 )));
182 }
183 Ok(())
184}
185
186fn validate_gnm(n: u32, m: u64, directed: bool, loops: bool) -> IgraphResult<()> {
187 let cap = max_edges(n, directed, loops);
188 if m > cap {
189 return Err(IgraphError::InvalidArgument(format!(
190 "G(n,m) requested {m} edges but the {} graph on {n} vertices admits at most {cap}",
191 if directed { "directed" } else { "undirected" }
192 )));
193 }
194 Ok(())
195}
196
197fn finalize(n: u32, directed: bool, edges: Vec<(VertexId, VertexId)>) -> IgraphResult<Graph> {
200 let mut g = Graph::new(n, directed)?;
201 g.add_edges(edges)?;
202 Ok(g)
203}
204
205fn complete(n: u32, directed: bool, loops: bool) -> IgraphResult<Graph> {
207 let cap = max_edges(n, directed, loops);
208 let cap_usize = usize::try_from(cap)
209 .map_err(|_| IgraphError::Internal("complete graph edge count exceeds usize"))?;
210 let mut edges = Vec::with_capacity(cap_usize);
211 for i in 0..n {
212 if directed {
213 for j in 0..n {
214 if i == j && !loops {
215 continue;
216 }
217 edges.push((i, j));
218 }
219 } else {
220 let start = if loops { i } else { i + 1 };
221 for j in start..n {
222 edges.push((i, j));
223 }
224 }
225 }
226 finalize(n, directed, edges)
227}
228
229pub fn erdos_renyi_gnp(
261 n: u32,
262 p: f64,
263 directed: bool,
264 loops: bool,
265 seed: u64,
266) -> IgraphResult<Graph> {
267 validate_gnp(p)?;
268
269 let cap = max_edges(n, directed, loops);
270 if n == 0 || p == 0.0 || cap == 0 {
271 return Graph::new(n, directed);
272 }
273 if p == 1.0 {
274 return complete(n, directed, loops);
275 }
276
277 let mut rng = SplitMix64::new(seed);
278 #[allow(clippy::cast_precision_loss)]
283 let cap_f = cap as f64;
284 let mut last = rng.gen_geom(p);
285 let mut indices: Vec<u64> = Vec::new();
286 while last < cap_f {
287 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
289 let idx = last.trunc() as u64;
290 if idx >= cap {
291 break;
292 }
293 indices.push(idx);
294 last += rng.gen_geom(p);
295 last += 1.0; }
297
298 let edges: Vec<(VertexId, VertexId)> = indices
299 .into_iter()
300 .map(|idx| decode_pair(idx, n, directed, loops))
301 .collect();
302 finalize(n, directed, edges)
303}
304
305pub fn erdos_renyi_gnm(
329 n: u32,
330 m: u64,
331 directed: bool,
332 loops: bool,
333 seed: u64,
334) -> IgraphResult<Graph> {
335 validate_gnm(n, m, directed, loops)?;
336
337 if m == 0 {
338 return Graph::new(n, directed);
339 }
340
341 let cap = max_edges(n, directed, loops);
342 if m == cap {
343 return complete(n, directed, loops);
344 }
345
346 let mut rng = SplitMix64::new(seed);
347 let picks = distinct_sample(&mut rng, cap, m);
348 let edges: Vec<(VertexId, VertexId)> = picks
349 .into_iter()
350 .map(|idx| decode_pair(idx, n, directed, loops))
351 .collect();
352 finalize(n, directed, edges)
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358 use std::collections::HashSet;
359
360 #[test]
363 fn decode_undirected_no_loops_covers_strict_upper_triangle() {
364 let n = 5u32;
367 let cap = max_edges(n, false, false);
368 assert_eq!(cap, 10);
369 let mut seen: HashSet<(u32, u32)> = HashSet::new();
370 for idx in 0..cap {
371 let (u, v) = decode_pair(idx, n, false, false);
372 assert!(u < v && v < n);
373 assert!(seen.insert((u, v)), "duplicate pair at idx {idx}");
374 }
375 assert_eq!(seen.len(), cap as usize);
376 }
377
378 #[test]
379 fn decode_undirected_with_loops_covers_lower_triangle_incl_diagonal() {
380 let n = 4u32;
381 let cap = max_edges(n, false, true);
382 assert_eq!(cap, 10);
383 let mut seen: HashSet<(u32, u32)> = HashSet::new();
384 for idx in 0..cap {
385 let (u, v) = decode_pair(idx, n, false, true);
386 assert!(u <= v && v < n);
387 assert!(seen.insert((u, v)));
388 }
389 assert_eq!(seen.len(), cap as usize);
390 }
391
392 #[test]
393 fn decode_directed_no_loops_covers_off_diagonal() {
394 let n = 5u32;
395 let cap = max_edges(n, true, false);
396 assert_eq!(cap, 20);
397 let mut seen: HashSet<(u32, u32)> = HashSet::new();
398 for idx in 0..cap {
399 let (u, v) = decode_pair(idx, n, true, false);
400 assert!(u < n && v < n && u != v);
401 assert!(seen.insert((u, v)));
402 }
403 assert_eq!(seen.len(), cap as usize);
404 }
405
406 #[test]
407 fn decode_directed_with_loops_covers_full_grid() {
408 let n = 4u32;
409 let cap = max_edges(n, true, true);
410 assert_eq!(cap, 16);
411 let mut seen: HashSet<(u32, u32)> = HashSet::new();
412 for idx in 0..cap {
413 let (u, v) = decode_pair(idx, n, true, true);
414 assert!(u < n && v < n);
415 assert!(seen.insert((u, v)));
416 }
417 assert_eq!(seen.len(), cap as usize);
418 }
419
420 #[test]
423 fn gnp_p_zero_is_empty() {
424 let g = erdos_renyi_gnp(10, 0.0, false, false, 1).unwrap();
425 assert_eq!(g.vcount(), 10);
426 assert_eq!(g.ecount(), 0);
427 }
428
429 #[test]
430 fn gnp_p_one_is_complete() {
431 let g = erdos_renyi_gnp(6, 1.0, false, false, 1).unwrap();
432 assert_eq!(g.vcount(), 6);
433 assert_eq!(g.ecount(), 15); }
435
436 #[test]
437 fn gnp_directed_p_one_loops_is_full_grid() {
438 let g = erdos_renyi_gnp(4, 1.0, true, true, 1).unwrap();
439 assert_eq!(g.ecount(), 16);
440 }
441
442 #[test]
443 fn gnp_invalid_p_rejected() {
444 assert!(erdos_renyi_gnp(5, -0.1, false, false, 1).is_err());
445 assert!(erdos_renyi_gnp(5, 1.1, false, false, 1).is_err());
446 assert!(erdos_renyi_gnp(5, f64::NAN, false, false, 1).is_err());
447 assert!(erdos_renyi_gnp(5, f64::INFINITY, false, false, 1).is_err());
448 }
449
450 #[test]
451 fn gnp_zero_vertices_is_empty() {
452 let g = erdos_renyi_gnp(0, 0.5, false, false, 1).unwrap();
453 assert_eq!(g.vcount(), 0);
454 assert_eq!(g.ecount(), 0);
455 }
456
457 #[test]
460 fn gnp_deterministic_with_seed() {
461 let a = erdos_renyi_gnp(50, 0.3, false, false, 12345).unwrap();
462 let b = erdos_renyi_gnp(50, 0.3, false, false, 12345).unwrap();
463 assert_eq!(a.vcount(), b.vcount());
464 assert_eq!(a.ecount(), b.ecount());
465 let edges_a: Vec<_> = (0..a.ecount()).map(|e| a.edge(e as u32).unwrap()).collect();
467 let edges_b: Vec<_> = (0..b.ecount()).map(|e| b.edge(e as u32).unwrap()).collect();
468 assert_eq!(edges_a, edges_b);
469 }
470
471 #[test]
472 fn gnp_different_seeds_differ() {
473 let a = erdos_renyi_gnp(200, 0.05, false, false, 1).unwrap();
474 let b = erdos_renyi_gnp(200, 0.05, false, false, 2).unwrap();
475 assert_ne!(a.ecount(), b.ecount());
477 }
478
479 #[test]
480 fn gnp_expected_ecount_in_band() {
481 let g = erdos_renyi_gnp(100, 0.2, false, false, 31_415).unwrap();
485 let m = g.ecount();
486 assert!(m > 890 && m < 1090, "ecount = {m}");
487 }
488
489 #[test]
490 fn gnp_is_simple_no_loops() {
491 let g = erdos_renyi_gnp(50, 0.4, false, false, 7).unwrap();
492 for e in 0..g.ecount() {
493 let (u, v) = g.edge(e as u32).unwrap();
494 assert_ne!(u, v, "self-loop generated at edge {e}");
495 }
496 }
497
498 #[test]
499 fn gnp_no_parallel_edges_simple() {
500 let g = erdos_renyi_gnp(40, 0.3, false, false, 11).unwrap();
501 let mut seen: HashSet<(u32, u32)> = HashSet::new();
502 for e in 0..g.ecount() {
503 let pair = g.edge(e as u32).unwrap();
504 assert!(seen.insert(pair), "parallel edge at {e}: {pair:?}");
505 }
506 }
507
508 #[test]
511 fn gnm_m_zero_is_empty() {
512 let g = erdos_renyi_gnm(10, 0, false, false, 1).unwrap();
513 assert_eq!(g.ecount(), 0);
514 }
515
516 #[test]
517 fn gnm_m_max_is_complete() {
518 let g = erdos_renyi_gnm(6, 15, false, false, 1).unwrap();
519 assert_eq!(g.ecount(), 15);
520 }
521
522 #[test]
523 fn gnm_m_exceeds_capacity_rejected() {
524 assert!(erdos_renyi_gnm(5, 11, false, false, 1).is_err());
526 }
527
528 #[test]
529 fn gnm_exact_edge_count() {
530 let g = erdos_renyi_gnm(50, 100, false, false, 42).unwrap();
531 assert_eq!(g.ecount(), 100);
532 assert_eq!(g.vcount(), 50);
533 }
534
535 #[test]
536 fn gnm_deterministic_with_seed() {
537 let a = erdos_renyi_gnm(30, 60, false, false, 7).unwrap();
538 let b = erdos_renyi_gnm(30, 60, false, false, 7).unwrap();
539 let edges_a: Vec<_> = (0..a.ecount()).map(|e| a.edge(e as u32).unwrap()).collect();
540 let edges_b: Vec<_> = (0..b.ecount()).map(|e| b.edge(e as u32).unwrap()).collect();
541 assert_eq!(edges_a, edges_b);
542 }
543
544 #[test]
545 fn gnm_is_simple_no_loops() {
546 let g = erdos_renyi_gnm(20, 40, false, false, 99).unwrap();
547 let mut seen: HashSet<(u32, u32)> = HashSet::new();
548 for e in 0..g.ecount() {
549 let (u, v) = g.edge(e as u32).unwrap();
550 assert_ne!(u, v);
551 assert!(seen.insert((u, v)), "parallel edge: {u} {v}");
552 }
553 }
554
555 #[test]
556 fn gnm_directed_loops_full_grid() {
557 let g = erdos_renyi_gnm(3, 9, true, true, 0).unwrap();
560 assert_eq!(g.ecount(), 9);
561 let mut seen: HashSet<(u32, u32)> = HashSet::new();
562 for e in 0..g.ecount() {
563 seen.insert(g.edge(e as u32).unwrap());
564 }
565 assert_eq!(seen.len(), 9);
566 }
567
568 #[test]
569 fn gnm_directed_no_loops() {
570 let g = erdos_renyi_gnm(4, 12, true, false, 0).unwrap();
572 assert_eq!(g.ecount(), 12);
573 let mut seen: HashSet<(u32, u32)> = HashSet::new();
574 for e in 0..g.ecount() {
575 let (u, v) = g.edge(e as u32).unwrap();
576 assert_ne!(u, v);
577 seen.insert((u, v));
578 }
579 assert_eq!(seen.len(), 12);
580 }
581
582 #[cfg(all(test, feature = "proptest-harness"))]
585 mod prop {
586 use super::super::*;
587 use proptest::prelude::*;
588
589 proptest! {
590 #[test]
591 fn gnp_vcount_always_matches_n(
592 n in 0u32..50,
593 p in 0.0..=1.0,
594 directed in any::<bool>(),
595 loops in any::<bool>(),
596 seed in any::<u64>(),
597 ) {
598 let g = erdos_renyi_gnp(n, p, directed, loops, seed).unwrap();
599 prop_assert_eq!(g.vcount(), n);
600 prop_assert!(g.ecount() as u64 <= max_edges(n, directed, loops));
601 }
602
603 #[test]
604 fn gnp_simple_no_self_loops_no_multi(
605 n in 1u32..30,
606 p in 0.0..=0.7,
607 seed in any::<u64>(),
608 ) {
609 let g = erdos_renyi_gnp(n, p, false, false, seed).unwrap();
610 let mut seen = std::collections::HashSet::new();
611 for e in 0..g.ecount() {
612 let (u, v) = g.edge(e as u32).unwrap();
613 prop_assert!(u != v);
614 prop_assert!(seen.insert((u, v)));
615 }
616 }
617
618 #[test]
619 fn gnm_exact_count_and_simple(
620 n in 2u32..30,
621 seed in any::<u64>(),
622 ) {
623 let cap = max_edges(n, false, false);
625 if cap == 0 { return Ok(()); }
626 let m = (seed as u64) % cap;
628 let g = erdos_renyi_gnm(n, m, false, false, seed).unwrap();
629 prop_assert_eq!(g.ecount(), m as usize);
630 let mut seen = std::collections::HashSet::new();
631 for e in 0..g.ecount() {
632 let (u, v) = g.edge(e as u32).unwrap();
633 prop_assert!(u != v);
634 prop_assert!(seen.insert((u, v)));
635 }
636 }
637
638 #[test]
639 fn gnp_determinism(
640 n in 1u32..40,
641 p in 0.05..0.95,
642 directed in any::<bool>(),
643 loops in any::<bool>(),
644 seed in any::<u64>(),
645 ) {
646 let a = erdos_renyi_gnp(n, p, directed, loops, seed).unwrap();
647 let b = erdos_renyi_gnp(n, p, directed, loops, seed).unwrap();
648 prop_assert_eq!(a.ecount(), b.ecount());
649 for e in 0..a.ecount() {
650 prop_assert_eq!(a.edge(e as u32).unwrap(), b.edge(e as u32).unwrap());
651 }
652 }
653 }
654 }
655}