1#![allow(
59 clippy::cast_possible_truncation,
60 clippy::cast_precision_loss,
61 clippy::cast_sign_loss,
62 clippy::float_cmp
63)]
64
65use std::collections::HashSet;
66
67use crate::core::rng::SplitMix64;
68use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
69
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
77pub enum BipartiteMode {
78 Out,
80 In,
82 All,
85}
86
87#[derive(Clone, Debug)]
91pub struct BipartiteGraph {
92 pub graph: Graph,
94 pub types: Vec<bool>,
98}
99
100fn max_edges(n1: u32, n2: u32, directed: bool, mode: BipartiteMode) -> u64 {
104 let prod = u64::from(n1) * u64::from(n2);
105 if directed && matches!(mode, BipartiteMode::All) {
106 prod.saturating_mul(2)
107 } else {
108 prod
109 }
110}
111
112fn types_vector(n1: u32, n2: u32) -> Vec<bool> {
115 let total = (n1 as usize).saturating_add(n2 as usize);
116 let mut v = Vec::with_capacity(total);
117 v.resize(n1 as usize, false);
118 v.resize(total, true);
119 v
120}
121
122fn decode_pair(
129 idx: u64,
130 n1: u32,
131 n2: u32,
132 directed: bool,
133 mode: BipartiteMode,
134) -> (VertexId, VertexId) {
135 let n1_u64 = u64::from(n1);
136 let n2_u64 = u64::from(n2);
137 let n1n2 = n1_u64 * n2_u64;
138
139 let (from, to) = if !directed || !matches!(mode, BipartiteMode::All) {
140 let to_off = idx / n1_u64;
143 let from_off = idx - to_off * n1_u64;
144 debug_assert!(from_off < n1_u64 && to_off < n2_u64);
145 (from_off, to_off + n1_u64)
146 } else if idx < n1n2 {
147 let to_off = idx / n1_u64;
149 let from_off = idx - to_off * n1_u64;
150 debug_assert!(from_off < n1_u64 && to_off < n2_u64);
151 (from_off, to_off + n1_u64)
152 } else {
153 let rel = idx - n1n2;
155 let to_off = rel / n2_u64;
156 let from_off = rel - to_off * n2_u64;
157 debug_assert!(from_off < n2_u64 && to_off < n1_u64);
158 (from_off + n1_u64, to_off)
159 };
160
161 let (from, to) = if matches!(mode, BipartiteMode::In) && directed {
163 (to, from)
164 } else {
165 (from, to)
166 };
167
168 #[allow(clippy::cast_possible_truncation)]
169 (from as VertexId, to as VertexId)
170}
171
172fn distinct_sample(rng: &mut SplitMix64, n_pairs: u64, m: u64) -> Vec<u64> {
177 debug_assert!(m <= n_pairs);
178 let cap = usize::try_from(m).unwrap_or(usize::MAX);
179 let mut chosen: HashSet<u64> = HashSet::with_capacity(cap);
180 let mut out: Vec<u64> = Vec::with_capacity(cap);
181 for j in (n_pairs - m)..n_pairs {
182 let span = j + 1;
183 let span_usize = usize::try_from(span).unwrap_or(usize::MAX);
184 let t_usize = rng.gen_index(span_usize);
185 let t = t_usize as u64;
186 let pick = if chosen.insert(t) {
187 t
188 } else {
189 chosen.insert(j);
190 j
191 };
192 out.push(pick);
193 }
194 out.sort_unstable();
195 out
196}
197
198fn validate_gnp(p: f64) -> IgraphResult<()> {
199 if !p.is_finite() {
200 return Err(IgraphError::InvalidArgument(format!(
201 "bipartite G(n,p) probability must be finite (got {p})"
202 )));
203 }
204 if !(0.0..=1.0).contains(&p) {
205 return Err(IgraphError::InvalidArgument(format!(
206 "bipartite G(n,p) probability must be in [0, 1] (got {p})"
207 )));
208 }
209 Ok(())
210}
211
212fn validate_gnm(n1: u32, n2: u32, m: u64, directed: bool, mode: BipartiteMode) -> IgraphResult<()> {
213 let cap = max_edges(n1, n2, directed, mode);
214 if m > cap {
215 return Err(IgraphError::InvalidArgument(format!(
216 "bipartite G(n,m) requested {m} edges but the {} graph on {n1}+{n2} vertices admits at most {cap}",
217 if directed { "directed" } else { "undirected" }
218 )));
219 }
220 if cap == 0 && m > 0 {
221 return Err(IgraphError::InvalidArgument(format!(
222 "bipartite G(n,m) requested {m} edges but n1={n1} or n2={n2} is zero"
223 )));
224 }
225 Ok(())
226}
227
228fn finalize(
230 n1: u32,
231 n2: u32,
232 directed: bool,
233 edges: Vec<(VertexId, VertexId)>,
234) -> IgraphResult<BipartiteGraph> {
235 let n_total = u32::try_from(u64::from(n1) + u64::from(n2)).map_err(|_| {
236 IgraphError::InvalidArgument(format!("n1 + n2 overflows u32 (n1={n1}, n2={n2})"))
237 })?;
238 let mut g = Graph::new(n_total, directed)?;
239 g.add_edges(edges)?;
240 Ok(BipartiteGraph {
241 graph: g,
242 types: types_vector(n1, n2),
243 })
244}
245
246fn complete_bipartite(
250 n1: u32,
251 n2: u32,
252 directed: bool,
253 mode: BipartiteMode,
254) -> IgraphResult<BipartiteGraph> {
255 let cap = max_edges(n1, n2, directed, mode);
256 let cap_usize = usize::try_from(cap)
257 .map_err(|_| IgraphError::Internal("complete bipartite edge count exceeds usize"))?;
258 let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(cap_usize);
259 for idx in 0..cap {
260 edges.push(decode_pair(idx, n1, n2, directed, mode));
261 }
262 finalize(n1, n2, directed, edges)
263}
264
265pub fn bipartite_game_gnp(
301 n1: u32,
302 n2: u32,
303 p: f64,
304 directed: bool,
305 mode: BipartiteMode,
306 seed: u64,
307) -> IgraphResult<BipartiteGraph> {
308 validate_gnp(p)?;
309
310 let n_total = u32::try_from(u64::from(n1) + u64::from(n2)).map_err(|_| {
311 IgraphError::InvalidArgument(format!("n1 + n2 overflows u32 (n1={n1}, n2={n2})"))
312 })?;
313
314 let cap = max_edges(n1, n2, directed, mode);
315 if cap == 0 || p == 0.0 {
316 let g = Graph::new(n_total, directed)?;
317 return Ok(BipartiteGraph {
318 graph: g,
319 types: types_vector(n1, n2),
320 });
321 }
322 if p == 1.0 {
323 return complete_bipartite(n1, n2, directed, mode);
324 }
325
326 let mut rng = SplitMix64::new(seed);
327 #[allow(clippy::cast_precision_loss)]
332 let cap_f = cap as f64;
333 let mut last = rng.gen_geom(p);
334 let mut indices: Vec<u64> = Vec::new();
335 while last < cap_f {
336 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
337 let idx = last.trunc() as u64;
338 if idx >= cap {
339 break;
340 }
341 indices.push(idx);
342 last += rng.gen_geom(p);
343 last += 1.0; }
345
346 let edges: Vec<(VertexId, VertexId)> = indices
347 .into_iter()
348 .map(|idx| decode_pair(idx, n1, n2, directed, mode))
349 .collect();
350 finalize(n1, n2, directed, edges)
351}
352
353pub fn bipartite_game_gnm(
382 n1: u32,
383 n2: u32,
384 m: u64,
385 directed: bool,
386 mode: BipartiteMode,
387 seed: u64,
388) -> IgraphResult<BipartiteGraph> {
389 validate_gnm(n1, n2, m, directed, mode)?;
390
391 let n_total = u32::try_from(u64::from(n1) + u64::from(n2)).map_err(|_| {
392 IgraphError::InvalidArgument(format!("n1 + n2 overflows u32 (n1={n1}, n2={n2})"))
393 })?;
394
395 if m == 0 {
396 let g = Graph::new(n_total, directed)?;
397 return Ok(BipartiteGraph {
398 graph: g,
399 types: types_vector(n1, n2),
400 });
401 }
402
403 let cap = max_edges(n1, n2, directed, mode);
404 if m == cap {
405 return complete_bipartite(n1, n2, directed, mode);
406 }
407
408 let mut rng = SplitMix64::new(seed);
409 let picks = distinct_sample(&mut rng, cap, m);
410 let edges: Vec<(VertexId, VertexId)> = picks
411 .into_iter()
412 .map(|idx| decode_pair(idx, n1, n2, directed, mode))
413 .collect();
414 finalize(n1, n2, directed, edges)
415}
416
417pub fn bipartite_iea_game(
457 n1: u32,
458 n2: u32,
459 m: u64,
460 directed: bool,
461 mode: BipartiteMode,
462 seed: u64,
463) -> IgraphResult<BipartiteGraph> {
464 let n_total = u32::try_from(u64::from(n1) + u64::from(n2)).map_err(|_| {
465 IgraphError::InvalidArgument(format!("n1 + n2 overflows u32 (n1={n1}, n2={n2})"))
466 })?;
467
468 if m == 0 {
469 let g = Graph::new(n_total, directed)?;
470 return Ok(BipartiteGraph {
471 graph: g,
472 types: types_vector(n1, n2),
473 });
474 }
475
476 let cap = max_edges(n1, n2, directed, mode);
477 if cap == 0 {
478 return Err(IgraphError::InvalidArgument(format!(
479 "bipartite IEA requested {m} edges but n1={n1} or n2={n2} is zero"
480 )));
481 }
482
483 let cap_usize = usize::try_from(cap)
484 .map_err(|_| IgraphError::Internal("bipartite IEA pair space exceeds usize"))?;
485 let m_usize = usize::try_from(m)
486 .map_err(|_| IgraphError::Internal("bipartite IEA edge count exceeds usize"))?;
487
488 let mut rng = SplitMix64::new(seed);
489 let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(m_usize);
490 for _ in 0..m {
491 let idx = rng.gen_index(cap_usize) as u64;
492 edges.push(decode_pair(idx, n1, n2, directed, mode));
493 }
494 finalize(n1, n2, directed, edges)
495}
496
497#[cfg(test)]
498mod tests {
499 use super::*;
500 use std::collections::HashSet;
501
502 #[test]
505 fn types_vector_bottom_then_top() {
506 let t = types_vector(3, 5);
507 assert_eq!(t.len(), 8);
508 for (i, &ty) in t.iter().enumerate() {
509 if i < 3 {
510 assert!(!ty, "vertex {i} should be bottom (false)");
511 } else {
512 assert!(ty, "vertex {i} should be top (true)");
513 }
514 }
515 }
516
517 #[test]
518 fn max_edges_undirected_is_product() {
519 assert_eq!(max_edges(3, 5, false, BipartiteMode::All), 15);
520 assert_eq!(max_edges(0, 5, false, BipartiteMode::All), 0);
521 assert_eq!(max_edges(3, 0, false, BipartiteMode::Out), 0);
522 }
523
524 #[test]
525 fn max_edges_directed_out_or_in_is_product() {
526 assert_eq!(max_edges(3, 5, true, BipartiteMode::Out), 15);
527 assert_eq!(max_edges(3, 5, true, BipartiteMode::In), 15);
528 }
529
530 #[test]
531 fn max_edges_directed_all_doubles() {
532 assert_eq!(max_edges(3, 5, true, BipartiteMode::All), 30);
533 }
534
535 #[test]
538 fn decode_undirected_covers_every_cross_pair_once() {
539 let (n1, n2) = (3u32, 4u32);
540 let cap = max_edges(n1, n2, false, BipartiteMode::All);
541 let mut seen: HashSet<(u32, u32)> = HashSet::new();
542 for idx in 0..cap {
543 let (u, v) = decode_pair(idx, n1, n2, false, BipartiteMode::All);
544 assert!(u < n1 && v >= n1 && v < n1 + n2);
545 assert!(seen.insert((u, v)), "duplicate at idx {idx}: {u},{v}");
546 }
547 assert_eq!(seen.len(), (n1 * n2) as usize);
548 }
549
550 #[test]
551 fn decode_directed_out_emits_bottom_to_top() {
552 let (n1, n2) = (3u32, 4u32);
553 let cap = max_edges(n1, n2, true, BipartiteMode::Out);
554 for idx in 0..cap {
555 let (u, v) = decode_pair(idx, n1, n2, true, BipartiteMode::Out);
556 assert!(u < n1, "from must be bottom, got {u}");
557 assert!(v >= n1 && v < n1 + n2, "to must be top, got {v}");
558 }
559 }
560
561 #[test]
562 fn decode_directed_in_emits_top_to_bottom() {
563 let (n1, n2) = (3u32, 4u32);
564 let cap = max_edges(n1, n2, true, BipartiteMode::In);
565 for idx in 0..cap {
566 let (u, v) = decode_pair(idx, n1, n2, true, BipartiteMode::In);
567 assert!(u >= n1 && u < n1 + n2, "from must be top, got {u}");
568 assert!(v < n1, "to must be bottom, got {v}");
569 }
570 }
571
572 #[test]
573 fn decode_directed_all_emits_each_ordered_pair_once() {
574 let (n1, n2) = (3u32, 4u32);
575 let cap = max_edges(n1, n2, true, BipartiteMode::All);
576 let mut seen: HashSet<(u32, u32)> = HashSet::new();
577 for idx in 0..cap {
578 let (u, v) = decode_pair(idx, n1, n2, true, BipartiteMode::All);
579 let u_bot = u < n1;
581 let v_bot = v < n1;
582 assert_ne!(u_bot, v_bot, "endpoints must cross partition");
583 assert!(seen.insert((u, v)), "duplicate ordered pair at idx {idx}");
584 }
585 assert_eq!(seen.len(), 2 * (n1 * n2) as usize);
586 }
587
588 #[test]
591 fn gnp_p_zero_is_empty() {
592 let bg = bipartite_game_gnp(5, 6, 0.0, false, BipartiteMode::All, 1).unwrap();
593 assert_eq!(bg.graph.vcount(), 11);
594 assert_eq!(bg.graph.ecount(), 0);
595 }
596
597 #[test]
598 fn gnp_p_one_undirected_is_complete_bipartite() {
599 let bg = bipartite_game_gnp(3, 4, 1.0, false, BipartiteMode::All, 1).unwrap();
600 assert_eq!(bg.graph.ecount(), 12);
601 }
602
603 #[test]
604 fn gnp_p_one_directed_out_is_complete_bipartite() {
605 let bg = bipartite_game_gnp(3, 4, 1.0, true, BipartiteMode::Out, 1).unwrap();
606 assert_eq!(bg.graph.ecount(), 12);
607 for eid in 0..bg.graph.ecount() {
608 let (u, v) = bg.graph.edge(eid as u32).unwrap();
609 assert!(u < 3, "Out arc must start in bottom");
610 assert!(v >= 3, "Out arc must end in top");
611 }
612 }
613
614 #[test]
615 fn gnp_p_one_directed_all_doubles_edge_count() {
616 let bg = bipartite_game_gnp(3, 4, 1.0, true, BipartiteMode::All, 1).unwrap();
617 assert_eq!(bg.graph.ecount(), 24);
618 }
619
620 #[test]
621 fn gnp_invalid_p_rejected() {
622 assert!(bipartite_game_gnp(3, 4, -0.1, false, BipartiteMode::All, 1).is_err());
623 assert!(bipartite_game_gnp(3, 4, 1.1, false, BipartiteMode::All, 1).is_err());
624 assert!(bipartite_game_gnp(3, 4, f64::NAN, false, BipartiteMode::All, 1).is_err());
625 assert!(bipartite_game_gnp(3, 4, f64::INFINITY, false, BipartiteMode::All, 1).is_err());
626 }
627
628 #[test]
629 fn gnp_zero_n1_is_edgeless_with_n2_vertices() {
630 let bg = bipartite_game_gnp(0, 5, 0.5, false, BipartiteMode::All, 1).unwrap();
631 assert_eq!(bg.graph.vcount(), 5);
632 assert_eq!(bg.graph.ecount(), 0);
633 for &t in &bg.types {
634 assert!(t, "all 5 must be top");
635 }
636 }
637
638 #[test]
639 fn gnp_zero_n2_is_edgeless_with_n1_vertices() {
640 let bg = bipartite_game_gnp(7, 0, 0.5, false, BipartiteMode::All, 1).unwrap();
641 assert_eq!(bg.graph.vcount(), 7);
642 assert_eq!(bg.graph.ecount(), 0);
643 for &t in &bg.types {
644 assert!(!t, "all 7 must be bottom");
645 }
646 }
647
648 #[test]
649 fn gnp_deterministic_with_seed() {
650 let a = bipartite_game_gnp(10, 15, 0.4, false, BipartiteMode::All, 12345).unwrap();
651 let b = bipartite_game_gnp(10, 15, 0.4, false, BipartiteMode::All, 12345).unwrap();
652 assert_eq!(a.graph.vcount(), b.graph.vcount());
653 assert_eq!(a.graph.ecount(), b.graph.ecount());
654 let edges_a: Vec<_> = (0..a.graph.ecount())
655 .map(|e| a.graph.edge(e as u32).unwrap())
656 .collect();
657 let edges_b: Vec<_> = (0..b.graph.ecount())
658 .map(|e| b.graph.edge(e as u32).unwrap())
659 .collect();
660 assert_eq!(edges_a, edges_b);
661 }
662
663 #[test]
664 fn gnp_different_seeds_typically_differ() {
665 let a = bipartite_game_gnp(40, 60, 0.05, false, BipartiteMode::All, 1).unwrap();
666 let b = bipartite_game_gnp(40, 60, 0.05, false, BipartiteMode::All, 2).unwrap();
667 assert_ne!(a.graph.ecount(), b.graph.ecount());
668 }
669
670 #[test]
671 fn gnp_expected_ecount_in_band() {
672 let bg = bipartite_game_gnp(30, 30, 0.2, false, BipartiteMode::All, 31_415).unwrap();
676 let m = bg.graph.ecount();
677 assert!(m > 120 && m < 240, "ecount = {m}");
678 }
679
680 #[test]
681 fn gnp_is_simple_and_bipartite() {
682 let bg = bipartite_game_gnp(20, 20, 0.3, false, BipartiteMode::All, 7).unwrap();
683 let mut seen: HashSet<(u32, u32)> = HashSet::new();
684 for e in 0..bg.graph.ecount() {
685 let (u, v) = bg.graph.edge(e as u32).unwrap();
686 assert_ne!(u, v, "self-loop in bipartite output");
687 assert_ne!(bg.types[u as usize], bg.types[v as usize]);
689 let canon = if u <= v { (u, v) } else { (v, u) };
690 assert!(seen.insert(canon), "parallel edge {canon:?}");
691 }
692 }
693
694 #[test]
695 fn gnp_directed_in_arcs_top_to_bottom() {
696 let bg = bipartite_game_gnp(5, 6, 0.5, true, BipartiteMode::In, 999).unwrap();
697 for e in 0..bg.graph.ecount() {
698 let (u, v) = bg.graph.edge(e as u32).unwrap();
699 assert!((5..11).contains(&u), "In source must be top, got {u}");
700 assert!(v < 5, "In target must be bottom, got {v}");
701 }
702 }
703
704 #[test]
707 fn gnm_m_zero_is_empty() {
708 let bg = bipartite_game_gnm(5, 6, 0, false, BipartiteMode::All, 1).unwrap();
709 assert_eq!(bg.graph.vcount(), 11);
710 assert_eq!(bg.graph.ecount(), 0);
711 }
712
713 #[test]
714 fn gnm_m_max_is_complete_bipartite() {
715 let bg = bipartite_game_gnm(3, 4, 12, false, BipartiteMode::All, 1).unwrap();
716 assert_eq!(bg.graph.ecount(), 12);
717 }
718
719 #[test]
720 fn gnm_m_max_directed_all_doubles() {
721 let bg = bipartite_game_gnm(3, 4, 24, true, BipartiteMode::All, 1).unwrap();
722 assert_eq!(bg.graph.ecount(), 24);
723 }
724
725 #[test]
726 fn gnm_m_exceeds_capacity_rejected() {
727 assert!(bipartite_game_gnm(5, 6, 31, false, BipartiteMode::All, 1).is_err());
728 }
729
730 #[test]
731 fn gnm_zero_partition_with_m_positive_is_error() {
732 assert!(bipartite_game_gnm(0, 5, 1, false, BipartiteMode::All, 1).is_err());
733 assert!(bipartite_game_gnm(5, 0, 1, false, BipartiteMode::All, 1).is_err());
734 }
735
736 #[test]
737 fn gnm_exact_edge_count() {
738 let bg = bipartite_game_gnm(10, 20, 50, false, BipartiteMode::All, 42).unwrap();
739 assert_eq!(bg.graph.ecount(), 50);
740 assert_eq!(bg.graph.vcount(), 30);
741 }
742
743 #[test]
744 fn gnm_deterministic_with_seed() {
745 let a = bipartite_game_gnm(15, 20, 80, false, BipartiteMode::All, 7).unwrap();
746 let b = bipartite_game_gnm(15, 20, 80, false, BipartiteMode::All, 7).unwrap();
747 let edges_a: Vec<_> = (0..a.graph.ecount())
748 .map(|e| a.graph.edge(e as u32).unwrap())
749 .collect();
750 let edges_b: Vec<_> = (0..b.graph.ecount())
751 .map(|e| b.graph.edge(e as u32).unwrap())
752 .collect();
753 assert_eq!(edges_a, edges_b);
754 }
755
756 #[test]
757 fn gnm_is_simple_and_bipartite() {
758 let bg = bipartite_game_gnm(12, 10, 40, false, BipartiteMode::All, 99).unwrap();
759 let mut seen: HashSet<(u32, u32)> = HashSet::new();
760 for e in 0..bg.graph.ecount() {
761 let (u, v) = bg.graph.edge(e as u32).unwrap();
762 assert_ne!(u, v);
763 assert_ne!(bg.types[u as usize], bg.types[v as usize]);
764 let canon = if u <= v { (u, v) } else { (v, u) };
765 assert!(seen.insert(canon), "parallel edge {canon:?}");
766 }
767 }
768
769 #[test]
770 fn gnm_directed_out_arcs_bottom_to_top() {
771 let bg = bipartite_game_gnm(4, 5, 15, true, BipartiteMode::Out, 33).unwrap();
772 assert_eq!(bg.graph.ecount(), 15);
773 for e in 0..bg.graph.ecount() {
774 let (u, v) = bg.graph.edge(e as u32).unwrap();
775 assert!(u < 4, "Out source must be bottom, got {u}");
776 assert!((4..9).contains(&v), "Out target must be top, got {v}");
777 }
778 }
779
780 #[test]
781 fn gnm_directed_all_allows_mutual_pairs() {
782 let bg = bipartite_game_gnm(3, 4, 24, true, BipartiteMode::All, 1).unwrap();
786 let mut canonical_counts: std::collections::HashMap<(u32, u32), u32> =
787 std::collections::HashMap::new();
788 for e in 0..bg.graph.ecount() {
789 let (u, v) = bg.graph.edge(e as u32).unwrap();
790 let key = if u <= v { (u, v) } else { (v, u) };
791 *canonical_counts.entry(key).or_insert(0) += 1;
792 }
793 for (_, c) in canonical_counts {
794 assert_eq!(
795 c, 2,
796 "every unordered cross pair appears twice in K_(3,4)·2"
797 );
798 }
799 }
800
801 #[test]
802 fn gnp_full_directed_all_is_bipartite_and_has_2n1n2_edges() {
803 let bg = bipartite_game_gnp(2, 3, 1.0, true, BipartiteMode::All, 0).unwrap();
806 assert_eq!(bg.graph.ecount(), 12);
807 let mut bottom_to_top = 0u32;
808 let mut top_to_bottom = 0u32;
809 for e in 0..bg.graph.ecount() {
810 let (u, v) = bg.graph.edge(e as u32).unwrap();
811 if u < 2 && v >= 2 {
812 bottom_to_top += 1;
813 } else if u >= 2 && v < 2 {
814 top_to_bottom += 1;
815 } else {
816 panic!("non-bipartite arc {u}->{v}");
817 }
818 }
819 assert_eq!(bottom_to_top, 6);
820 assert_eq!(top_to_bottom, 6);
821 }
822
823 #[test]
826 fn iea_m_zero_is_empty() {
827 let bg = bipartite_iea_game(3, 4, 0, false, BipartiteMode::All, 1).unwrap();
828 assert_eq!(bg.graph.vcount(), 7);
829 assert_eq!(bg.graph.ecount(), 0);
830 }
831
832 #[test]
833 fn iea_exact_edge_count_with_multiplicity() {
834 let cap = max_edges(2, 2, false, BipartiteMode::All); let m = cap + 10;
837 let bg = bipartite_iea_game(2, 2, m, false, BipartiteMode::All, 9).unwrap();
838 assert_eq!(bg.graph.ecount() as u64, m);
839 }
840
841 #[test]
842 fn iea_zero_partition_with_m_positive_is_error() {
843 assert!(bipartite_iea_game(0, 5, 3, false, BipartiteMode::All, 0).is_err());
844 assert!(bipartite_iea_game(5, 0, 3, false, BipartiteMode::Out, 0).is_err());
845 }
846
847 #[test]
848 fn iea_all_edges_cross_partition() {
849 let bg = bipartite_iea_game(4, 3, 30, false, BipartiteMode::All, 42).unwrap();
850 assert_eq!(bg.graph.ecount(), 30);
851 for e in 0..bg.graph.ecount() {
852 let (u, v) = bg.graph.edge(e as u32).unwrap();
853 assert_ne!(bg.types[u as usize], bg.types[v as usize]);
854 }
855 }
856
857 #[test]
858 fn iea_deterministic_with_seed() {
859 let a = bipartite_iea_game(5, 6, 25, true, BipartiteMode::Out, 7).unwrap();
860 let b = bipartite_iea_game(5, 6, 25, true, BipartiteMode::Out, 7).unwrap();
861 assert_eq!(a.graph.ecount(), b.graph.ecount());
862 for e in 0..a.graph.ecount() {
863 assert_eq!(
864 a.graph.edge(e as u32).unwrap(),
865 b.graph.edge(e as u32).unwrap()
866 );
867 }
868 }
869
870 #[test]
871 fn iea_directed_out_arcs_bottom_to_top() {
872 let bg = bipartite_iea_game(3, 3, 40, true, BipartiteMode::Out, 3).unwrap();
873 for e in 0..bg.graph.ecount() {
874 let (u, v) = bg.graph.edge(e as u32).unwrap();
875 assert!(u < 3 && v >= 3, "arc {u}->{v} is not bottom→top");
876 }
877 }
878
879 #[test]
880 fn iea_directed_in_arcs_top_to_bottom() {
881 let bg = bipartite_iea_game(3, 3, 40, true, BipartiteMode::In, 3).unwrap();
882 for e in 0..bg.graph.ecount() {
883 let (u, v) = bg.graph.edge(e as u32).unwrap();
884 assert!(u >= 3 && v < 3, "arc {u}->{v} is not top→bottom");
885 }
886 }
887
888 #[test]
889 fn iea_likely_produces_multi_edge() {
890 let bg = bipartite_iea_game(2, 2, 50, false, BipartiteMode::All, 5).unwrap();
893 let mut seen: HashSet<(u32, u32)> = HashSet::new();
894 let mut found_dup = false;
895 for e in 0..bg.graph.ecount() {
896 let (u, v) = bg.graph.edge(e as u32).unwrap();
897 let key = if u <= v { (u, v) } else { (v, u) };
898 if !seen.insert(key) {
899 found_dup = true;
900 }
901 }
902 assert!(found_dup, "expected a parallel edge with m=50 over 4 pairs");
903 }
904
905 #[cfg(all(test, feature = "proptest-harness"))]
908 mod prop {
909 use super::super::*;
910 use proptest::prelude::*;
911
912 fn arb_mode() -> impl Strategy<Value = BipartiteMode> {
913 prop_oneof![
914 Just(BipartiteMode::Out),
915 Just(BipartiteMode::In),
916 Just(BipartiteMode::All),
917 ]
918 }
919
920 proptest! {
921 #[test]
922 fn gnp_vcount_always_matches_sum(
923 n1 in 0u32..15,
924 n2 in 0u32..15,
925 p in 0.0..=1.0,
926 directed in any::<bool>(),
927 mode in arb_mode(),
928 seed in any::<u64>(),
929 ) {
930 let bg = bipartite_game_gnp(n1, n2, p, directed, mode, seed).unwrap();
931 prop_assert_eq!(bg.graph.vcount(), n1 + n2);
932 prop_assert_eq!(bg.types.len() as u32, n1 + n2);
933 prop_assert!(bg.graph.ecount() as u64 <= max_edges(n1, n2, directed, mode));
934 }
935
936 #[test]
937 fn gnp_simple_and_bipartite(
938 n1 in 1u32..12,
939 n2 in 1u32..12,
940 p in 0.0..=0.6,
941 directed in any::<bool>(),
942 mode in arb_mode(),
943 seed in any::<u64>(),
944 ) {
945 let bg = bipartite_game_gnp(n1, n2, p, directed, mode, seed).unwrap();
946 let mut seen: std::collections::HashSet<(u32, u32)> =
950 std::collections::HashSet::new();
951 for e in 0..bg.graph.ecount() {
952 let (u, v) = bg.graph.edge(e as u32).unwrap();
953 prop_assert!(u != v);
954 prop_assert!(bg.types[u as usize] != bg.types[v as usize]);
955 let key = if directed { (u, v) } else if u <= v { (u, v) } else { (v, u) };
956 prop_assert!(seen.insert(key));
957 }
958 }
959
960 #[test]
961 fn gnm_exact_count_and_bipartite(
962 n1 in 1u32..10,
963 n2 in 1u32..10,
964 seed in any::<u64>(),
965 ) {
966 let cap = max_edges(n1, n2, false, BipartiteMode::All);
967 if cap == 0 { return Ok(()); }
968 let m = (seed as u64) % (cap + 1);
969 let bg = bipartite_game_gnm(n1, n2, m, false, BipartiteMode::All, seed).unwrap();
970 prop_assert_eq!(bg.graph.ecount() as u64, m);
971 let mut seen: std::collections::HashSet<(u32, u32)> =
972 std::collections::HashSet::new();
973 for e in 0..bg.graph.ecount() {
974 let (u, v) = bg.graph.edge(e as u32).unwrap();
975 prop_assert!(u != v);
976 prop_assert!(bg.types[u as usize] != bg.types[v as usize]);
977 let key = if u <= v { (u, v) } else { (v, u) };
978 prop_assert!(seen.insert(key));
979 }
980 }
981
982 #[test]
983 fn gnp_determinism(
984 n1 in 1u32..10,
985 n2 in 1u32..10,
986 p in 0.05..0.95,
987 directed in any::<bool>(),
988 mode in arb_mode(),
989 seed in any::<u64>(),
990 ) {
991 let a = bipartite_game_gnp(n1, n2, p, directed, mode, seed).unwrap();
992 let b = bipartite_game_gnp(n1, n2, p, directed, mode, seed).unwrap();
993 prop_assert_eq!(a.graph.ecount(), b.graph.ecount());
994 for e in 0..a.graph.ecount() {
995 prop_assert_eq!(
996 a.graph.edge(e as u32).unwrap(),
997 b.graph.edge(e as u32).unwrap()
998 );
999 }
1000 }
1001 }
1002 }
1003}