1#![allow(
52 unknown_lints,
53 clippy::cast_possible_truncation,
54 clippy::cast_precision_loss,
55 clippy::cast_sign_loss,
56 clippy::float_cmp,
57 clippy::too_many_arguments,
58 clippy::similar_names,
59 clippy::manual_midpoint,
60 clippy::many_single_char_names
61)]
62
63use crate::algorithms::games::sbm::{PairShape, block_offsets, sample_pair_with_max};
64use crate::core::rng::SplitMix64;
65use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
66
67const SUM_TOL: f64 = 1.490_116_119_384_765_6e-8;
70
71fn validate_pref(c: &[Vec<f64>], k: usize, label: &str) -> IgraphResult<()> {
76 if c.len() != k {
77 return Err(IgraphError::InvalidArgument(format!(
78 "{label}: pref matrix has {} rows but rho has length {k}",
79 c.len()
80 )));
81 }
82 for (i, row) in c.iter().enumerate() {
83 if row.len() != k {
84 return Err(IgraphError::InvalidArgument(format!(
85 "{label}: pref matrix row {i} has length {} (expected {k})",
86 row.len()
87 )));
88 }
89 for (j, &val) in row.iter().enumerate() {
90 if !val.is_finite() {
91 return Err(IgraphError::InvalidArgument(format!(
92 "{label}: pref[{i}][{j}] = {val} is not finite"
93 )));
94 }
95 if !(0.0..=1.0).contains(&val) {
96 return Err(IgraphError::InvalidArgument(format!(
97 "{label}: pref[{i}][{j}] = {val} must lie in [0, 1]"
98 )));
99 }
100 }
101 }
102 for (i, row_i) in c.iter().enumerate() {
103 for (j, row_j) in c.iter().enumerate().skip(i + 1) {
104 let pij = row_i[j];
105 let pji = row_j[i];
106 if pij != pji {
107 return Err(IgraphError::InvalidArgument(format!(
108 "{label}: pref matrix is not symmetric: pref[{i}][{j}] = {pij}, \
109 pref[{j}][{i}] = {pji}"
110 )));
111 }
112 }
113 }
114 Ok(())
115}
116
117fn validate_rho(rho: &[f64], label: &str) -> IgraphResult<()> {
119 if rho.is_empty() {
120 return Err(IgraphError::InvalidArgument(format!(
121 "{label}: rho must contain at least one entry"
122 )));
123 }
124 let mut acc = 0.0;
125 for (i, &val) in rho.iter().enumerate() {
126 if !val.is_finite() {
127 return Err(IgraphError::InvalidArgument(format!(
128 "{label}: rho[{i}] = {val} is not finite"
129 )));
130 }
131 if !(0.0..=1.0).contains(&val) {
132 return Err(IgraphError::InvalidArgument(format!(
133 "{label}: rho[{i}] = {val} must lie in [0, 1]"
134 )));
135 }
136 acc += val;
137 }
138 if (acc - 1.0).abs() > SUM_TOL {
139 return Err(IgraphError::InvalidArgument(format!(
140 "{label}: sum(rho) = {acc} must equal 1 (tolerance {SUM_TOL})"
141 )));
142 }
143 Ok(())
144}
145
146fn csizes_for(rho: &[f64], m: u32, label: &str) -> IgraphResult<Vec<u32>> {
152 let m_f = f64::from(m);
153 let mut sizes = Vec::with_capacity(rho.len());
154 for (i, &r) in rho.iter().enumerate() {
155 let s = r * m_f;
156 let rounded = s.round();
157 if (rounded - s).abs() > SUM_TOL {
158 return Err(IgraphError::InvalidArgument(format!(
159 "{label}: rho[{i}] * m = {s} is not an integer (tolerance {SUM_TOL})"
160 )));
161 }
162 if rounded < 0.0 || rounded > f64::from(u32::MAX) {
163 return Err(IgraphError::InvalidArgument(format!(
164 "{label}: rho[{i}] * m = {rounded} is out of u32 range"
165 )));
166 }
167 sizes.push(rounded as u32);
168 }
169 let sum: u64 = sizes.iter().copied().map(u64::from).sum();
170 if sum != u64::from(m) {
171 return Err(IgraphError::InvalidArgument(format!(
172 "{label}: sum of round(rho[i] * m) = {sum} but m = {m}"
173 )));
174 }
175 Ok(sizes)
176}
177
178fn sample_intra_macro(
183 rng: &mut SplitMix64,
184 edges: &mut Vec<(VertexId, VertexId)>,
185 macro_off: u32,
186 csizes: &[u32],
187 c: &[Vec<f64>],
188) {
189 let k = csizes.len();
190 let mut intra_off = vec![0u32; k + 1];
192 let mut acc = 0u32;
193 for (i, &s) in csizes.iter().enumerate() {
194 acc += s;
195 intra_off[i + 1] = acc;
196 }
197 for from in 0..k {
198 let fromsize = csizes[from];
199 if fromsize == 0 {
200 continue;
201 }
202 let fromoff = macro_off + intra_off[from];
203 for to in from..k {
204 let tosize = csizes[to];
205 if tosize == 0 {
206 continue;
207 }
208 let tooff = macro_off + intra_off[to];
209 let prob = c[from][to];
210 if prob <= 0.0 {
211 continue;
212 }
213 let (shape, maxedges) = if from == to {
214 let fs = u64::from(fromsize);
215 (PairShape::TriExclDiag, fs * fs.saturating_sub(1) / 2)
216 } else {
217 (PairShape::Rect, u64::from(fromsize) * u64::from(tosize))
218 };
219 sample_pair_with_max(
220 rng, edges, fromsize, fromoff, tooff, shape, false, prob, maxedges,
221 );
222 }
223 }
224}
225
226fn full_bipartite(
231 edges: &mut Vec<(VertexId, VertexId)>,
232 fromoff: u32,
233 fromsize: u32,
234 tooff: u32,
235 tosize: u32,
236) {
237 for u in 0..fromsize {
238 for v in 0..tosize {
239 edges.push((fromoff + u, tooff + v));
240 }
241 }
242}
243
244fn rect_between_macros(
247 rng: &mut SplitMix64,
248 edges: &mut Vec<(VertexId, VertexId)>,
249 fromoff: u32,
250 fromsize: u32,
251 tooff: u32,
252 tosize: u32,
253 p: f64,
254) {
255 if fromsize == 0 || tosize == 0 || p <= 0.0 {
256 return;
257 }
258 let maxedges = u64::from(fromsize) * u64::from(tosize);
259 sample_pair_with_max(
260 rng,
261 edges,
262 fromsize,
263 fromoff,
264 tooff,
265 PairShape::Rect,
266 false,
267 p,
268 maxedges,
269 );
270}
271
272fn add_inter_macro(
274 rng: &mut SplitMix64,
275 edges: &mut Vec<(VertexId, VertexId)>,
276 macro_offsets: &[u32],
277 macro_sizes: &[u32],
278 p: f64,
279) {
280 if p <= 0.0 {
281 return;
282 }
283 let k = macro_sizes.len();
284 if p >= 1.0 {
285 for b1 in 0..k {
286 let fromoff = macro_offsets[b1];
287 let fromsize = macro_sizes[b1];
288 if fromsize == 0 {
289 continue;
290 }
291 for b2 in (b1 + 1)..k {
292 let tosize = macro_sizes[b2];
293 if tosize == 0 {
294 continue;
295 }
296 full_bipartite(edges, fromoff, fromsize, macro_offsets[b2], tosize);
297 }
298 }
299 return;
300 }
301 for b1 in 0..k {
302 let fromoff = macro_offsets[b1];
303 let fromsize = macro_sizes[b1];
304 if fromsize == 0 {
305 continue;
306 }
307 for b2 in (b1 + 1)..k {
308 let tosize = macro_sizes[b2];
309 if tosize == 0 {
310 continue;
311 }
312 rect_between_macros(rng, edges, fromoff, fromsize, macro_offsets[b2], tosize, p);
313 }
314 }
315}
316
317pub fn hsbm_game(
360 n: u32,
361 m: u32,
362 rho: &[f64],
363 c: &[Vec<f64>],
364 p: f64,
365 seed: u64,
366) -> IgraphResult<Graph> {
367 if n == 0 {
368 return Err(IgraphError::InvalidArgument(
369 "hsbm_game: n must be at least 1".into(),
370 ));
371 }
372 if m == 0 {
373 return Err(IgraphError::InvalidArgument(
374 "hsbm_game: m must be at least 1".into(),
375 ));
376 }
377 if n % m != 0 {
378 return Err(IgraphError::InvalidArgument(format!(
379 "hsbm_game: n ({n}) must be a multiple of m ({m})"
380 )));
381 }
382 if !p.is_finite() || !(0.0..=1.0).contains(&p) {
383 return Err(IgraphError::InvalidArgument(format!(
384 "hsbm_game: p = {p} must lie in [0, 1]"
385 )));
386 }
387 let k = rho.len();
388 validate_rho(rho, "hsbm_game")?;
389 validate_pref(c, k, "hsbm_game")?;
390 let csizes = csizes_for(rho, m, "hsbm_game")?;
391 let no_macros = n / m;
392
393 let mut rng = SplitMix64::new(seed);
394 let mut edges: Vec<(VertexId, VertexId)> = Vec::new();
395
396 for b in 0..no_macros {
398 let macro_off = b * m;
399 sample_intra_macro(&mut rng, &mut edges, macro_off, &csizes, c);
400 }
401
402 let mut macro_offsets = Vec::with_capacity(no_macros as usize + 1);
404 let mut macro_sizes = Vec::with_capacity(no_macros as usize);
405 for b in 0..no_macros {
406 macro_offsets.push(b * m);
407 macro_sizes.push(m);
408 }
409 macro_offsets.push(no_macros * m);
410 add_inter_macro(&mut rng, &mut edges, ¯o_offsets, ¯o_sizes, p);
411
412 let mut g = Graph::new(n, false)?;
413 g.add_edges(edges)?;
414 Ok(g)
415}
416
417pub fn hsbm_list_game(
466 n: u32,
467 m_list: &[u32],
468 rho_list: &[Vec<f64>],
469 c_list: &[Vec<Vec<f64>>],
470 p: f64,
471 seed: u64,
472) -> IgraphResult<Graph> {
473 if n == 0 {
474 return Err(IgraphError::InvalidArgument(
475 "hsbm_list_game: n must be at least 1".into(),
476 ));
477 }
478 if !p.is_finite() || !(0.0..=1.0).contains(&p) {
479 return Err(IgraphError::InvalidArgument(format!(
480 "hsbm_list_game: p = {p} must lie in [0, 1]"
481 )));
482 }
483 let no_macros = rho_list.len();
484 if no_macros == 0 {
485 return Err(IgraphError::InvalidArgument(
486 "hsbm_list_game: rho_list must contain at least one entry".into(),
487 ));
488 }
489 if m_list.len() != no_macros || c_list.len() != no_macros {
490 return Err(IgraphError::InvalidArgument(format!(
491 "hsbm_list_game: rho_list ({no_macros}), m_list ({}) and c_list ({}) \
492 must have the same length",
493 m_list.len(),
494 c_list.len()
495 )));
496 }
497 if m_list.contains(&0) {
498 return Err(IgraphError::InvalidArgument(
499 "hsbm_list_game: every m_list[b] must be at least 1".into(),
500 ));
501 }
502 let sum_m: u64 = m_list.iter().copied().map(u64::from).sum();
503 if sum_m != u64::from(n) {
504 return Err(IgraphError::InvalidArgument(format!(
505 "hsbm_list_game: sum(m_list) = {sum_m} but n = {n}"
506 )));
507 }
508 let (macro_offsets, _total) = block_offsets(m_list)?;
509
510 let mut all_csizes: Vec<Vec<u32>> = Vec::with_capacity(no_macros);
511 for b in 0..no_macros {
512 let k = rho_list[b].len();
513 let label = format!("hsbm_list_game (macro {b})");
514 validate_rho(&rho_list[b], &label)?;
515 validate_pref(&c_list[b], k, &label)?;
516 let csizes = csizes_for(&rho_list[b], m_list[b], &label)?;
517 all_csizes.push(csizes);
518 }
519
520 let mut rng = SplitMix64::new(seed);
521 let mut edges: Vec<(VertexId, VertexId)> = Vec::new();
522
523 for b in 0..no_macros {
525 sample_intra_macro(
526 &mut rng,
527 &mut edges,
528 macro_offsets[b],
529 &all_csizes[b],
530 &c_list[b],
531 );
532 }
533
534 add_inter_macro(&mut rng, &mut edges, ¯o_offsets, m_list, p);
536
537 let mut g = Graph::new(n, false)?;
538 g.add_edges(edges)?;
539 Ok(g)
540}
541
542#[cfg(test)]
543mod tests {
544 use super::*;
545
546 #[test]
549 fn hsbm_rejects_n_not_multiple_of_m() {
550 let rho = vec![1.0];
551 let c = vec![vec![0.5]];
552 let res = hsbm_game(10, 3, &rho, &c, 0.0, 0);
553 assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
554 }
555
556 #[test]
557 fn hsbm_rejects_zero_n() {
558 let rho = vec![1.0];
559 let c = vec![vec![0.5]];
560 let res = hsbm_game(0, 1, &rho, &c, 0.0, 0);
561 assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
562 }
563
564 #[test]
565 fn hsbm_rejects_zero_m() {
566 let rho = vec![1.0];
567 let c = vec![vec![0.5]];
568 let res = hsbm_game(10, 0, &rho, &c, 0.0, 0);
569 assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
570 }
571
572 #[test]
573 fn hsbm_rejects_p_out_of_range() {
574 let rho = vec![1.0];
575 let c = vec![vec![0.5]];
576 let res = hsbm_game(10, 5, &rho, &c, 1.5, 0);
577 assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
578 }
579
580 #[test]
581 fn hsbm_rejects_rho_not_summing_to_one() {
582 let rho = vec![0.3, 0.3];
583 let c = vec![vec![0.5, 0.5], vec![0.5, 0.5]];
584 let res = hsbm_game(10, 5, &rho, &c, 0.0, 0);
585 assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
586 }
587
588 #[test]
589 fn hsbm_rejects_rho_times_m_not_integer() {
590 let rho = vec![0.5, 0.5];
592 let c = vec![vec![0.5, 0.5], vec![0.5, 0.5]];
593 let res = hsbm_game(14, 7, &rho, &c, 0.0, 0);
594 assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
595 }
596
597 #[test]
598 fn hsbm_rejects_asymmetric_c() {
599 let rho = vec![0.5, 0.5];
600 let c = vec![vec![0.5, 0.2], vec![0.3, 0.5]];
601 let res = hsbm_game(10, 5, &rho, &c, 0.0, 0);
602 assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
603 }
604
605 #[test]
606 fn hsbm_rejects_c_out_of_range() {
607 let rho = vec![1.0];
608 let c = vec![vec![1.5]];
609 let res = hsbm_game(10, 5, &rho, &c, 0.0, 0);
610 assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
611 }
612
613 #[test]
614 fn hsbm_rejects_pref_wrong_shape() {
615 let rho = vec![0.5, 0.5];
616 let c = vec![vec![0.5]];
617 let res = hsbm_game(10, 5, &rho, &c, 0.0, 0);
618 assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
619 }
620
621 #[test]
624 fn hsbm_trivial_one_vertex() {
625 let rho = vec![1.0];
627 let c = vec![vec![1.0]];
628 let g = hsbm_game(1, 1, &rho, &c, 0.0, 0).unwrap();
629 assert_eq!(g.vcount(), 1);
630 assert_eq!(g.ecount(), 0);
631 assert!(!g.is_directed());
632 }
633
634 #[test]
635 fn hsbm_complete_bipartite_within_macro() {
636 let rho = vec![0.6, 0.4, 0.0];
640 let c = vec![
641 vec![0.0, 1.0, 0.0],
642 vec![1.0, 0.0, 0.0],
643 vec![0.0, 0.0, 0.0],
644 ];
645 let g = hsbm_game(10, 10, &rho, &c, 0.0, 0).unwrap();
646 assert_eq!(g.vcount(), 10);
647 assert_eq!(g.ecount(), 24);
649 for e in 0..g.ecount() as u32 {
651 let (u, v) = g.edge(e).unwrap();
652 let (lo, hi) = if u < v { (u, v) } else { (v, u) };
653 assert!(lo < 6 && (6..10).contains(&hi));
654 }
655 }
656
657 #[test]
658 fn hsbm_full_inter_macro_complete_minus_clusters() {
659 let rho = vec![0.6, 0.4, 0.0];
664 let c = vec![
665 vec![0.0, 1.0, 0.0],
666 vec![1.0, 0.0, 0.0],
667 vec![0.0, 0.0, 0.0],
668 ];
669 let g = hsbm_game(10, 5, &rho, &c, 1.0, 0).unwrap();
670 assert_eq!(g.vcount(), 10);
671 assert_eq!(g.ecount(), 37);
672 }
673
674 #[test]
675 fn hsbm_p_zero_isolates_macros() {
676 let rho = vec![0.5, 0.5];
678 let c = vec![vec![0.5, 0.1], vec![0.1, 0.5]];
679 let g = hsbm_game(20, 10, &rho, &c, 0.0, 0xDEAD).unwrap();
680 for e in 0..g.ecount() as u32 {
681 let (u, v) = g.edge(e).unwrap();
682 let bu = u / 10;
683 let bv = v / 10;
684 assert_eq!(bu, bv, "edge ({u}, {v}) crosses macros with p=0");
685 }
686 }
687
688 #[test]
689 fn hsbm_p_one_emits_all_inter_macro_edges() {
690 let rho = vec![1.0];
692 let c = vec![vec![0.0]];
693 let g = hsbm_game(20, 5, &rho, &c, 1.0, 0).unwrap();
694 assert_eq!(g.ecount(), 150);
696 }
697
698 #[test]
699 fn hsbm_no_self_loops_and_no_multi() {
700 let rho = vec![0.5, 0.5];
701 let c = vec![vec![1.0, 0.0], vec![0.0, 1.0]];
702 let g = hsbm_game(20, 10, &rho, &c, 0.5, 0).unwrap();
703 let mut seen: std::collections::HashSet<(u32, u32)> = std::collections::HashSet::new();
704 for e in 0..g.ecount() as u32 {
705 let (u, v) = g.edge(e).unwrap();
706 assert_ne!(u, v, "self-loop in HSBM");
707 let key = if u <= v { (u, v) } else { (v, u) };
708 assert!(seen.insert(key), "duplicate edge {u}-{v}");
709 }
710 }
711
712 #[test]
713 fn hsbm_determinism_same_seed_same_graph() {
714 let rho = vec![0.5, 0.5];
715 let c = vec![vec![0.3, 0.05], vec![0.05, 0.3]];
716 let g1 = hsbm_game(40, 10, &rho, &c, 0.05, 0x00C0_FFEE).unwrap();
717 let g2 = hsbm_game(40, 10, &rho, &c, 0.05, 0x00C0_FFEE).unwrap();
718 let edges1: Vec<_> = (0..g1.ecount() as u32)
719 .map(|e| g1.edge(e).unwrap())
720 .collect();
721 let edges2: Vec<_> = (0..g2.ecount() as u32)
722 .map(|e| g2.edge(e).unwrap())
723 .collect();
724 assert_eq!(edges1, edges2);
725 }
726
727 #[test]
730 fn hsbm_list_rejects_length_mismatch() {
731 let res = hsbm_list_game(10, &[10, 0], &[vec![1.0]], &[vec![vec![0.5]]], 0.0, 0);
732 assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
733 }
734
735 #[test]
736 fn hsbm_list_rejects_sum_m_not_n() {
737 let res = hsbm_list_game(
738 10,
739 &[5, 4],
740 &[vec![1.0], vec![1.0]],
741 &[vec![vec![0.5]], vec![vec![0.5]]],
742 0.0,
743 0,
744 );
745 assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
746 }
747
748 #[test]
749 fn hsbm_list_rejects_empty_macros() {
750 let res = hsbm_list_game(10, &[], &[], &[], 0.0, 0);
751 assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
752 }
753
754 #[test]
755 fn hsbm_list_rejects_zero_size_macro() {
756 let res = hsbm_list_game(
757 5,
758 &[5, 0],
759 &[vec![1.0], vec![1.0]],
760 &[vec![vec![0.5]], vec![vec![0.5]]],
761 0.0,
762 0,
763 );
764 assert!(matches!(res, Err(IgraphError::InvalidArgument(_))));
765 }
766
767 #[test]
770 fn hsbm_list_trivial_one_vertex() {
771 let g = hsbm_list_game(1, &[1], &[vec![1.0]], &[vec![vec![1.0]]], 0.0, 0).unwrap();
772 assert_eq!(g.vcount(), 1);
773 assert_eq!(g.ecount(), 0);
774 }
775
776 #[test]
777 fn hsbm_list_complete_bipartite_within_one_macro() {
778 let m_list = vec![10u32];
780 let rho_list = vec![vec![0.6, 0.4, 0.0]];
781 let c_list = vec![vec![
782 vec![0.0, 1.0, 0.0],
783 vec![1.0, 0.0, 0.0],
784 vec![0.0, 0.0, 0.0],
785 ]];
786 let g = hsbm_list_game(10, &m_list, &rho_list, &c_list, 0.0, 0).unwrap();
787 assert_eq!(g.ecount(), 24);
788 }
789
790 #[test]
791 fn hsbm_list_two_macros_p_one_full_inter() {
792 let m_list = vec![5u32, 5];
795 let rho_list = vec![vec![0.6, 0.4, 0.0], vec![0.6, 0.4, 0.0]];
796 let c_list = vec![
797 vec![
798 vec![0.0, 1.0, 0.0],
799 vec![1.0, 0.0, 0.0],
800 vec![0.0, 0.0, 0.0],
801 ],
802 vec![
803 vec![0.0, 1.0, 0.0],
804 vec![1.0, 0.0, 0.0],
805 vec![0.0, 0.0, 0.0],
806 ],
807 ];
808 let g = hsbm_list_game(10, &m_list, &rho_list, &c_list, 1.0, 0).unwrap();
809 assert_eq!(g.ecount(), 37);
810 }
811
812 #[test]
813 fn hsbm_list_heterogeneous_macros_no_panic() {
814 let m_list = vec![3u32, 10, 5, 3];
816 let rho_list = vec![
817 vec![1.0 / 3.0, 2.0 / 3.0],
818 vec![3.0 / 10.0, 3.0 / 10.0, 4.0 / 10.0],
819 vec![1.0],
820 vec![2.0 / 3.0, 1.0 / 3.0],
821 ];
822 let c_list = vec![
823 vec![vec![0.0, 0.0], vec![0.0, 0.0]],
824 vec![
825 vec![0.0, 0.0, 0.0],
826 vec![0.0, 0.0, 0.0],
827 vec![0.0, 0.0, 0.0],
828 ],
829 vec![vec![0.0]],
830 vec![vec![0.0, 0.0], vec![0.0, 0.0]],
831 ];
832 let g = hsbm_list_game(21, &m_list, &rho_list, &c_list, 1.0, 0).unwrap();
833 assert_eq!(g.vcount(), 21);
834 let mut expected = 0u64;
836 for i in 0..m_list.len() {
837 for j in (i + 1)..m_list.len() {
838 expected += u64::from(m_list[i]) * u64::from(m_list[j]);
839 }
840 }
841 assert_eq!(u64::try_from(g.ecount()).unwrap(), expected);
842 }
843
844 #[test]
845 fn hsbm_list_uniform_matches_hsbm_game() {
846 let rho = vec![0.5, 0.5];
849 let c = vec![vec![0.3, 0.05], vec![0.05, 0.3]];
850 let g1 = hsbm_game(40, 10, &rho, &c, 0.05, 0xABCD).unwrap();
851
852 let m_list = vec![10u32; 4];
853 let rho_list = vec![rho.clone(); 4];
854 let c_list = vec![c.clone(); 4];
855 let g2 = hsbm_list_game(40, &m_list, &rho_list, &c_list, 0.05, 0xABCD).unwrap();
856
857 let e1: Vec<_> = (0..g1.ecount() as u32)
858 .map(|e| g1.edge(e).unwrap())
859 .collect();
860 let e2: Vec<_> = (0..g2.ecount() as u32)
861 .map(|e| g2.edge(e).unwrap())
862 .collect();
863 assert_eq!(e1, e2);
864 }
865}
866
867#[cfg(all(test, feature = "proptest-harness"))]
868mod proptest_invariants {
869 use super::*;
870 use proptest::prelude::*;
871
872 proptest! {
873 #![proptest_config(ProptestConfig::with_cases(48))]
874
875 #[test]
877 fn hsbm_vcount_matches_n(
878 no_macros in 1u32..5,
879 cluster_count in 1usize..4,
880 seed: u64,
881 ) {
882 let m = (cluster_count as u32) * 3;
883 let n = no_macros * m;
884 let rho = vec![1.0 / cluster_count as f64; cluster_count];
885 let p_in = 0.2_f64;
886 let p_off = 0.05_f64;
887 let mut c = vec![vec![p_off; cluster_count]; cluster_count];
888 for (i, row) in c.iter_mut().enumerate() {
889 row[i] = p_in;
890 }
891 let g = hsbm_game(n, m, &rho, &c, 0.02, seed).unwrap();
892 prop_assert_eq!(g.vcount(), n);
893 }
894
895 #[test]
897 fn hsbm_is_simple(
898 no_macros in 1u32..4,
899 seed: u64,
900 ) {
901 let m = 6u32;
902 let n = no_macros * m;
903 let rho = vec![0.5_f64, 0.5];
904 let c = vec![vec![0.3, 0.05], vec![0.05, 0.3]];
905 let g = hsbm_game(n, m, &rho, &c, 0.05, seed).unwrap();
906 let mut seen: std::collections::HashSet<(u32, u32)> = std::collections::HashSet::new();
907 for e in 0..g.ecount() as u32 {
908 let (u, v) = g.edge(e).unwrap();
909 prop_assert_ne!(u, v);
910 let key = if u <= v { (u, v) } else { (v, u) };
911 prop_assert!(seen.insert(key));
912 }
913 }
914
915 #[test]
918 fn hsbm_p_zero_keeps_edges_inside_macros(
919 no_macros in 2u32..5,
920 seed: u64,
921 ) {
922 let m = 6u32;
923 let n = no_macros * m;
924 let rho = vec![0.5_f64, 0.5];
925 let c = vec![vec![0.4, 0.1], vec![0.1, 0.4]];
926 let g = hsbm_game(n, m, &rho, &c, 0.0, seed).unwrap();
927 for e in 0..g.ecount() as u32 {
928 let (u, v) = g.edge(e).unwrap();
929 prop_assert_eq!(u / m, v / m);
930 }
931 }
932
933 #[test]
935 fn hsbm_list_vcount_matches_sum_m(
936 sizes in prop::collection::vec(1u32..7, 1usize..4),
937 seed: u64,
938 ) {
939 let n: u32 = sizes.iter().sum();
940 let rho_list: Vec<Vec<f64>> = sizes.iter().map(|_| vec![1.0]).collect();
941 let c_list: Vec<Vec<Vec<f64>>> = sizes.iter().map(|_| vec![vec![0.2_f64]]).collect();
942 let g = hsbm_list_game(n, &sizes, &rho_list, &c_list, 0.05, seed).unwrap();
943 prop_assert_eq!(g.vcount(), n);
944 }
945 }
946}