1mod mcmc;
14
15pub use mcmc::{hrg_consensus, hrg_fit, hrg_predict};
16
17use crate::core::error::{IgraphError, IgraphResult};
18use crate::core::graph::{Graph, VertexId};
19use crate::core::rng::SplitMix64;
20
21#[derive(Debug, Clone)]
37pub struct HrgTree {
38 n: u32,
40 pub left: Vec<i32>,
42 pub right: Vec<i32>,
44 pub prob: Vec<f64>,
46 pub vertices: Vec<i32>,
48 pub edges: Vec<i32>,
50}
51
52impl HrgTree {
53 pub fn new(n: u32) -> Self {
55 let internal = if n <= 1 { 0 } else { (n - 1) as usize };
56 HrgTree {
57 n,
58 left: vec![0; internal],
59 right: vec![0; internal],
60 prob: vec![0.0; internal],
61 vertices: vec![0; internal],
62 edges: vec![0; internal],
63 }
64 }
65
66 pub fn size(&self) -> u32 {
68 self.n
69 }
70
71 pub fn num_internal(&self) -> usize {
73 self.left.len()
74 }
75
76 pub fn resize(&mut self, new_size: u32) {
78 self.n = new_size;
79 let internal = if new_size <= 1 {
80 0
81 } else {
82 (new_size - 1) as usize
83 };
84 self.left.resize(internal, 0);
85 self.right.resize(internal, 0);
86 self.prob.resize(internal, 0.0);
87 self.vertices.resize(internal, 0);
88 self.edges.resize(internal, 0);
89 }
90}
91
92#[allow(clippy::cast_sign_loss)]
93fn internal_idx(neg: i32) -> usize {
94 debug_assert!(neg < 0);
95 (-neg - 1) as usize
96}
97
98fn to_u32(v: usize) -> IgraphResult<u32> {
99 u32::try_from(v).map_err(|_| IgraphError::Internal("value exceeds u32::MAX"))
100}
101
102fn hrg_validate_and_degrees(graph: &Graph, prob: &[f64]) -> IgraphResult<(Vec<u32>, Vec<u32>)> {
104 let n = graph.vcount();
105
106 if n < 3 {
107 return Err(IgraphError::InvalidArgument(
108 "HRG tree must have at least three vertices".into(),
109 ));
110 }
111 if !graph.is_directed() {
112 return Err(IgraphError::InvalidArgument(
113 "HRG graph must be directed".into(),
114 ));
115 }
116 if n % 2 == 0 {
117 return Err(IgraphError::InvalidArgument(
118 "Complete HRG graph must have an odd number of vertices".into(),
119 ));
120 }
121 if !graph.is_simple()? {
122 return Err(IgraphError::InvalidArgument(
123 "HRG graph must be a simple graph".into(),
124 ));
125 }
126
127 let no_of_internal = (n as usize) / 2;
128 if prob.len() != no_of_internal {
129 return Err(IgraphError::InvalidArgument(format!(
130 "HRG probability vector size ({}) should equal the number of internal nodes ({})",
131 prob.len(),
132 no_of_internal,
133 )));
134 }
135
136 let mut in_deg = vec![0u32; n as usize];
137 let mut out_deg = vec![0u32; n as usize];
138 for eid in 0..graph.ecount() {
139 let src = graph.edge_source(to_u32(eid)?)?;
140 let tgt = graph.edge_target(to_u32(eid)?)?;
141 out_deg[src as usize] = out_deg[src as usize]
142 .checked_add(1)
143 .ok_or(IgraphError::Internal("degree overflow"))?;
144 in_deg[tgt as usize] = in_deg[tgt as usize]
145 .checked_add(1)
146 .ok_or(IgraphError::Internal("degree overflow"))?;
147 }
148
149 Ok((in_deg, out_deg))
150}
151
152fn hrg_find_root_and_validate(
154 n: u32,
155 in_deg: &[u32],
156 out_deg: &[u32],
157) -> IgraphResult<(VertexId, u32)> {
158 let mut root: Option<VertexId> = None;
159 let mut root_count = 0u32;
160 for v in 0..n {
161 match in_deg[v as usize] {
162 0 => {
163 root_count = root_count
164 .checked_add(1)
165 .ok_or(IgraphError::Internal("count overflow"))?;
166 root = Some(v);
167 }
168 1 => {}
169 _ => {
170 return Err(IgraphError::InvalidArgument(
171 "HRG nodes must have in-degree 0 or 1".into(),
172 ));
173 }
174 }
175 }
176 if root_count != 1 {
177 return Err(IgraphError::InvalidArgument(
178 "HRG must have exactly one root vertex (in-degree 0)".into(),
179 ));
180 }
181 let root = root.ok_or_else(|| IgraphError::InvalidArgument("HRG has no root vertex".into()))?;
182
183 let mut leaf_count = 0u32;
184 let mut internal_count = 0u32;
185 for v in 0..n {
186 match out_deg[v as usize] {
187 0 => {
188 leaf_count = leaf_count
189 .checked_add(1)
190 .ok_or(IgraphError::Internal("count overflow"))?;
191 }
192 2 => {
193 internal_count = internal_count
194 .checked_add(1)
195 .ok_or(IgraphError::Internal("count overflow"))?;
196 }
197 _ => {
198 return Err(IgraphError::InvalidArgument(
199 "HRG nodes must have out-degree 2 (internal) or 0 (leaf)".into(),
200 ));
201 }
202 }
203 }
204 let expected_leaves = internal_count
205 .checked_add(1)
206 .ok_or(IgraphError::Internal("count overflow"))?;
207 if leaf_count != expected_leaves {
208 return Err(IgraphError::InvalidArgument(
209 "HRG degrees are incorrect, maybe multiple components?".into(),
210 ));
211 }
212
213 Ok((root, internal_count))
214}
215
216fn hrg_build_tree(
218 graph: &Graph,
219 prob: &[f64],
220 root: VertexId,
221 out_deg: &[u32],
222 internal_count: u32,
223) -> IgraphResult<HrgTree> {
224 let n = graph.vcount();
225
226 let mut idx = vec![0i32; n as usize];
228 let mut ii: i32 = 0;
229 let mut il: i32 = 0;
230
231 idx[root as usize] = -(ii + 1);
232 ii += 1;
233
234 for v in 0..n {
235 if v == root {
236 continue;
237 }
238 if out_deg[v as usize] == 2 {
239 idx[v as usize] = -(ii + 1);
240 ii += 1;
241 } else {
242 idx[v as usize] = il;
243 il += 1;
244 }
245 }
246
247 let leaf_total = internal_count
248 .checked_add(1)
249 .ok_or(IgraphError::Internal("size overflow"))?;
250 let mut hrg = HrgTree::new(leaf_total);
251
252 let mut out_edges: Vec<Vec<VertexId>> = vec![Vec::new(); n as usize];
254 for eid in 0..graph.ecount() {
255 let src = graph.edge_source(to_u32(eid)?)?;
256 let tgt = graph.edge_target(to_u32(eid)?)?;
257 out_edges[src as usize].push(tgt);
258 }
259
260 let mut prob_idx = 0usize;
262 hrg.prob[0] = prob[prob_idx];
263 prob_idx += 1;
264
265 for v in 0..n {
266 let ri = idx[v as usize];
267 if ri >= 0 {
268 continue;
269 }
270 let children = &out_edges[v as usize];
271 if children.len() != 2 {
272 return Err(IgraphError::InvalidArgument(format!(
273 "Internal vertex {} has {} out-edges, expected 2",
274 v,
275 children.len()
276 )));
277 }
278 let hi = internal_idx(ri);
279 hrg.left[hi] = idx[children[0] as usize];
280 hrg.right[hi] = idx[children[1] as usize];
281
282 if v != root {
283 hrg.prob[hi] = prob[prob_idx];
284 prob_idx += 1;
285 }
286 }
287
288 let mut stack: Vec<i32> = vec![-1];
290 while let Some(¤t) = stack.last() {
291 let ci = internal_idx(current);
292 let lc = hrg.left[ci];
293 let rc = hrg.right[ci];
294
295 if lc < 0 && hrg.vertices[internal_idx(lc)] == 0 {
296 stack.push(lc);
297 continue;
298 }
299 if rc < 0 && hrg.vertices[internal_idx(rc)] == 0 {
300 stack.push(rc);
301 continue;
302 }
303
304 let lv = if lc < 0 {
305 hrg.vertices[internal_idx(lc)]
306 } else {
307 1
308 };
309 let rv = if rc < 0 {
310 hrg.vertices[internal_idx(rc)]
311 } else {
312 1
313 };
314 hrg.vertices[ci] = lv
315 .checked_add(rv)
316 .ok_or(IgraphError::Internal("vertex count overflow"))?;
317
318 let le = if lc < 0 {
319 hrg.edges[internal_idx(lc)]
320 .checked_add(1)
321 .ok_or(IgraphError::Internal("edge count overflow"))?
322 } else {
323 1
324 };
325 let re = if rc < 0 {
326 hrg.edges[internal_idx(rc)]
327 .checked_add(1)
328 .ok_or(IgraphError::Internal("edge count overflow"))?
329 } else {
330 1
331 };
332 hrg.edges[ci] = le
333 .checked_add(re)
334 .ok_or(IgraphError::Internal("edge count overflow"))?;
335
336 stack.pop();
337 }
338
339 Ok(hrg)
340}
341
342pub fn hrg_create(graph: &Graph, prob: &[f64]) -> IgraphResult<HrgTree> {
365 let (in_deg, out_deg) = hrg_validate_and_degrees(graph, prob)?;
366 let n = graph.vcount();
367 let (root, internal_count) = hrg_find_root_and_validate(n, &in_deg, &out_deg)?;
368 hrg_build_tree(graph, prob, root, &out_deg, internal_count)
369}
370
371#[derive(Debug, Clone)]
373pub struct HrgDendrogram {
374 pub graph: Graph,
377 pub prob: Vec<f64>,
379}
380
381fn hrg_child_vertex(orig_nodes: u32, child: i32) -> IgraphResult<u32> {
382 if child < 0 {
383 let ci =
384 u32::try_from(-child - 1).map_err(|_| IgraphError::Internal("vertex id overflow"))?;
385 orig_nodes
386 .checked_add(ci)
387 .ok_or(IgraphError::Internal("vertex id overflow"))
388 } else {
389 u32::try_from(child).map_err(|_| IgraphError::Internal("vertex id overflow"))
390 }
391}
392
393pub fn from_hrg_dendrogram(hrg: &HrgTree) -> IgraphResult<HrgDendrogram> {
417 let orig_nodes = hrg.size();
418 if orig_nodes <= 1 && hrg.num_internal() == 0 {
419 let g = Graph::new(orig_nodes, true)?;
420 let prob_vec = if orig_nodes == 1 {
421 vec![f64::NAN]
422 } else {
423 vec![]
424 };
425 return Ok(HrgDendrogram {
426 graph: g,
427 prob: prob_vec,
428 });
429 }
430
431 let no_of_nodes = orig_nodes
432 .checked_mul(2)
433 .and_then(|x| x.checked_sub(1))
434 .ok_or(IgraphError::Internal("node count overflow"))?;
435
436 let mut prob_vec = Vec::with_capacity(no_of_nodes as usize);
437 for _ in 0..orig_nodes {
438 prob_vec.push(f64::NAN);
439 }
440 for i in 0..hrg.num_internal() {
441 prob_vec.push(hrg.prob[i]);
442 }
443
444 let mut edges: Vec<(u32, u32)> = Vec::with_capacity(2 * hrg.num_internal());
445 for i in 0..hrg.num_internal() {
446 let parent = orig_nodes
447 .checked_add(to_u32(i)?)
448 .ok_or(IgraphError::Internal("vertex id overflow"))?;
449
450 let left_v = hrg_child_vertex(orig_nodes, hrg.left[i])?;
451 let right_v = hrg_child_vertex(orig_nodes, hrg.right[i])?;
452
453 edges.push((parent, left_v));
454 edges.push((parent, right_v));
455 }
456
457 let graph = Graph::from_edges(&edges, true, Some(no_of_nodes))?;
458
459 Ok(HrgDendrogram {
460 graph,
461 prob: prob_vec,
462 })
463}
464
465#[allow(clippy::cast_sign_loss)]
470fn find_lca(hrg: &HrgTree, parent: &[i32], leaf_a: i32, leaf_b: i32) -> usize {
471 let num_internal = hrg.num_internal();
472
473 let mut visited = vec![false; num_internal];
475
476 let mut cur = leaf_a;
477 loop {
478 if cur < 0 {
479 let idx = internal_idx(cur);
480 visited[idx] = true;
481 if idx == 0 {
482 break; }
484 cur = parent[idx];
485 } else {
486 cur = parent[num_internal + cur as usize];
488 }
489 }
490
491 let mut cur = leaf_b;
492 loop {
493 if cur < 0 {
494 let idx = internal_idx(cur);
495 if visited[idx] {
496 return idx;
497 }
498 if idx == 0 {
499 return 0; }
501 cur = parent[idx];
502 } else {
503 cur = parent[num_internal + cur as usize];
504 }
505 }
506}
507
508#[allow(
514 clippy::cast_possible_truncation,
515 clippy::cast_possible_wrap,
516 clippy::cast_sign_loss
517)]
518fn build_parent_map(hrg: &HrgTree) -> Vec<i32> {
519 let num_internal = hrg.num_internal();
520 let n = hrg.size() as usize;
521 let mut parent = vec![0i32; num_internal + n];
524
525 for i in 0..num_internal {
526 let self_id = -(i as i32 + 1);
527 let lc = hrg.left[i];
528 let rc = hrg.right[i];
529
530 if lc < 0 {
531 parent[internal_idx(lc)] = self_id;
532 } else {
533 parent[num_internal + lc as usize] = self_id;
534 }
535 if rc < 0 {
536 parent[internal_idx(rc)] = self_id;
537 } else {
538 parent[num_internal + rc as usize] = self_id;
539 }
540 }
541
542 parent
543}
544
545#[allow(clippy::cast_possible_wrap)]
546fn sample_one(hrg: &HrgTree, parent: &[i32], rng: &mut SplitMix64) -> IgraphResult<Graph> {
547 let n = hrg.size();
548 let mut edges: Vec<(u32, u32)> = Vec::new();
549
550 for i in 0..n {
551 for j in (i + 1)..n {
552 let lca = find_lca(hrg, parent, i as i32, j as i32);
553 if rng.gen_unit() < hrg.prob[lca] {
554 edges.push((i, j));
555 }
556 }
557 }
558
559 Graph::from_edges(&edges, false, Some(n))
560}
561
562pub fn hrg_sample(hrg: &HrgTree, seed: u64) -> IgraphResult<Graph> {
588 let n = hrg.size();
589 if n <= 1 {
590 return Graph::new(n, false);
591 }
592 let parent = build_parent_map(hrg);
593 let mut rng = SplitMix64::new(seed);
594 sample_one(hrg, &parent, &mut rng)
595}
596
597pub fn hrg_sample_many(hrg: &HrgTree, num_samples: usize, seed: u64) -> IgraphResult<Vec<Graph>> {
619 let n = hrg.size();
620 if n <= 1 {
621 let g = Graph::new(n, false)?;
622 return Ok(vec![g; num_samples]);
623 }
624 let parent = build_parent_map(hrg);
625 let mut rng = SplitMix64::new(seed);
626 let mut results = Vec::with_capacity(num_samples);
627 for _ in 0..num_samples {
628 results.push(sample_one(hrg, &parent, &mut rng)?);
629 }
630 Ok(results)
631}
632
633pub fn hrg_game(hrg: &HrgTree, seed: u64) -> IgraphResult<Graph> {
635 hrg_sample(hrg, seed)
636}
637
638#[cfg(test)]
639mod tests {
640 use super::*;
641
642 #[test]
643 fn hrg_tree_new_and_size() {
644 let hrg = HrgTree::new(5);
645 assert_eq!(hrg.size(), 5);
646 assert_eq!(hrg.num_internal(), 4);
647 }
648
649 #[test]
650 fn hrg_tree_single_vertex() {
651 let hrg = HrgTree::new(1);
652 assert_eq!(hrg.size(), 1);
653 assert_eq!(hrg.num_internal(), 0);
654 }
655
656 #[test]
657 fn hrg_tree_resize() {
658 let mut hrg = HrgTree::new(3);
659 assert_eq!(hrg.num_internal(), 2);
660 hrg.resize(5);
661 assert_eq!(hrg.num_internal(), 4);
662 assert_eq!(hrg.size(), 5);
663 }
664
665 #[test]
666 fn from_hrg_dendrogram_three_leaves() {
667 let mut hrg = HrgTree::new(3);
668 hrg.left[0] = 0;
669 hrg.right[0] = -2;
670 hrg.prob[0] = 0.5;
671 hrg.left[1] = 1;
672 hrg.right[1] = 2;
673 hrg.prob[1] = 0.8;
674 hrg.vertices = vec![3, 2];
675 hrg.edges = vec![4, 2];
676
677 let d = from_hrg_dendrogram(&hrg).expect("should succeed");
678 assert_eq!(d.graph.vcount(), 5);
679 assert_eq!(d.graph.ecount(), 4);
680 assert!(d.graph.is_directed());
681
682 assert!(d.prob[0].is_nan());
683 assert!(d.prob[1].is_nan());
684 assert!(d.prob[2].is_nan());
685 assert!((d.prob[3] - 0.5).abs() < 1e-10);
686 assert!((d.prob[4] - 0.8).abs() < 1e-10);
687 }
688
689 #[test]
690 fn hrg_create_five_vertex_tree() {
691 let g = Graph::from_edges(&[(0, 1), (0, 2), (1, 3), (1, 4)], true, Some(5))
692 .expect("graph creation");
693 let prob = vec![0.3, 0.7];
694 let hrg = hrg_create(&g, &prob).expect("hrg_create");
695
696 assert_eq!(hrg.size(), 3);
697 assert_eq!(hrg.num_internal(), 2);
698 assert!((hrg.prob[0] - 0.3).abs() < 1e-10);
699 assert!((hrg.prob[1] - 0.7).abs() < 1e-10);
700 assert_eq!(hrg.vertices[0], 3);
701 }
702
703 #[test]
704 fn hrg_create_rejects_undirected() {
705 let g = Graph::from_edges(&[(0, 1), (0, 2)], false, Some(3)).expect("graph creation");
706 assert!(hrg_create(&g, &[0.5]).is_err());
707 }
708
709 #[test]
710 fn hrg_create_rejects_even_vertices() {
711 let g =
712 Graph::from_edges(&[(0, 1), (0, 2), (2, 3)], true, Some(4)).expect("graph creation");
713 assert!(hrg_create(&g, &[0.5, 0.6]).is_err());
714 }
715
716 #[test]
717 fn hrg_create_rejects_too_small() {
718 let g = Graph::new(1, true).expect("graph creation");
719 assert!(hrg_create(&g, &[]).is_err());
720 }
721
722 #[test]
723 fn hrg_create_rejects_wrong_prob_len() {
724 let g = Graph::from_edges(&[(0, 1), (0, 2), (1, 3), (1, 4)], true, Some(5))
725 .expect("graph creation");
726 assert!(hrg_create(&g, &[0.3, 0.7, 0.9]).is_err());
727 }
728
729 #[test]
730 fn roundtrip_create_and_dendrogram() {
731 let g = Graph::from_edges(&[(0, 1), (0, 2), (1, 3), (1, 4)], true, Some(5))
732 .expect("graph creation");
733 let prob = vec![0.3, 0.7];
734 let hrg = hrg_create(&g, &prob).expect("hrg_create");
735 let d = from_hrg_dendrogram(&hrg).expect("from_hrg_dendrogram");
736
737 assert_eq!(d.graph.vcount(), 5);
738 assert_eq!(d.graph.ecount(), 4);
739 for i in 0..3u32 {
740 assert!(d.prob[i as usize].is_nan());
741 }
742 assert!((d.prob[3] - 0.3).abs() < 1e-10);
743 assert!((d.prob[4] - 0.7).abs() < 1e-10);
744 }
745
746 #[test]
747 fn from_hrg_dendrogram_empty() {
748 let hrg = HrgTree::new(0);
749 let d = from_hrg_dendrogram(&hrg).expect("should succeed");
750 assert_eq!(d.graph.vcount(), 0);
751 }
752
753 #[test]
754 fn hrg_create_seven_vertex_tree() {
755 let g = Graph::from_edges(
756 &[(0, 1), (0, 2), (1, 3), (1, 4), (2, 5), (2, 6)],
757 true,
758 Some(7),
759 )
760 .expect("graph creation");
761 let prob = vec![0.1, 0.2, 0.3];
762 let hrg = hrg_create(&g, &prob).expect("hrg_create");
763
764 assert_eq!(hrg.size(), 4);
765 assert_eq!(hrg.num_internal(), 3);
766 assert_eq!(hrg.vertices[0], 4);
767 assert!((hrg.prob[0] - 0.1).abs() < 1e-10);
768 assert!((hrg.prob[1] - 0.2).abs() < 1e-10);
769 assert!((hrg.prob[2] - 0.3).abs() < 1e-10);
770 }
771
772 fn make_sample_hrg() -> HrgTree {
775 let mut hrg = HrgTree::new(3);
778 hrg.left[0] = 0;
779 hrg.right[0] = -2; hrg.prob[0] = 0.5;
781 hrg.left[1] = 1;
782 hrg.right[1] = 2;
783 hrg.prob[1] = 1.0;
784 hrg.vertices = vec![3, 2];
785 hrg.edges = vec![4, 2];
786 hrg
787 }
788
789 #[test]
790 fn hrg_sample_correct_vcount() {
791 let hrg = make_sample_hrg();
792 let g = hrg_sample(&hrg, 42).expect("hrg_sample");
793 assert_eq!(g.vcount(), 3);
794 assert!(!g.is_directed());
795 }
796
797 #[test]
798 fn hrg_sample_deterministic() {
799 let hrg = make_sample_hrg();
800 let g1 = hrg_sample(&hrg, 99).expect("hrg_sample");
801 let g2 = hrg_sample(&hrg, 99).expect("hrg_sample");
802 assert_eq!(g1.ecount(), g2.ecount());
803 for eid in 0..g1.ecount() {
804 #[allow(clippy::cast_possible_truncation)]
805 let eid32 = eid as u32;
806 let (s1, t1) = g1.edge(eid32).expect("edge");
807 let (s2, t2) = g2.edge(eid32).expect("edge");
808 assert_eq!(s1, s2);
809 assert_eq!(t1, t2);
810 }
811 }
812
813 #[test]
814 fn hrg_sample_prob_one_always_connects() {
815 let mut hrg = HrgTree::new(3);
817 hrg.left[0] = 0;
818 hrg.right[0] = -2;
819 hrg.prob[0] = 1.0;
820 hrg.left[1] = 1;
821 hrg.right[1] = 2;
822 hrg.prob[1] = 1.0;
823 hrg.vertices = vec![3, 2];
824 hrg.edges = vec![4, 2];
825
826 for seed in 0..20u64 {
827 let g = hrg_sample(&hrg, seed).expect("hrg_sample");
828 assert_eq!(g.ecount(), 3, "prob=1.0 should yield K3");
829 }
830 }
831
832 #[test]
833 fn hrg_sample_prob_zero_never_connects() {
834 let mut hrg = HrgTree::new(3);
835 hrg.left[0] = 0;
836 hrg.right[0] = -2;
837 hrg.prob[0] = 0.0;
838 hrg.left[1] = 1;
839 hrg.right[1] = 2;
840 hrg.prob[1] = 0.0;
841 hrg.vertices = vec![3, 2];
842 hrg.edges = vec![4, 2];
843
844 for seed in 0..20u64 {
845 let g = hrg_sample(&hrg, seed).expect("hrg_sample");
846 assert_eq!(g.ecount(), 0, "prob=0.0 should yield empty graph");
847 }
848 }
849
850 #[test]
851 fn hrg_sample_single_vertex() {
852 let hrg = HrgTree::new(1);
853 let g = hrg_sample(&hrg, 0).expect("hrg_sample");
854 assert_eq!(g.vcount(), 1);
855 assert_eq!(g.ecount(), 0);
856 }
857
858 #[test]
859 fn hrg_sample_empty() {
860 let hrg = HrgTree::new(0);
861 let g = hrg_sample(&hrg, 0).expect("hrg_sample");
862 assert_eq!(g.vcount(), 0);
863 assert_eq!(g.ecount(), 0);
864 }
865
866 #[test]
867 fn hrg_sample_many_correct_count() {
868 let hrg = make_sample_hrg();
869 let graphs = hrg_sample_many(&hrg, 10, 42).expect("hrg_sample_many");
870 assert_eq!(graphs.len(), 10);
871 for g in &graphs {
872 assert_eq!(g.vcount(), 3);
873 }
874 }
875
876 #[test]
877 fn hrg_sample_many_zero_samples() {
878 let hrg = make_sample_hrg();
879 let graphs = hrg_sample_many(&hrg, 0, 42).expect("hrg_sample_many");
880 assert!(graphs.is_empty());
881 }
882
883 #[test]
884 fn hrg_sample_many_deterministic() {
885 let hrg = make_sample_hrg();
886 let g1 = hrg_sample_many(&hrg, 5, 77).expect("hrg_sample_many");
887 let g2 = hrg_sample_many(&hrg, 5, 77).expect("hrg_sample_many");
888 for (a, b) in g1.iter().zip(g2.iter()) {
889 assert_eq!(a.ecount(), b.ecount());
890 }
891 }
892
893 #[test]
894 fn hrg_game_is_alias() {
895 let hrg = make_sample_hrg();
896 let g1 = hrg_sample(&hrg, 123).expect("hrg_sample");
897 let g2 = hrg_game(&hrg, 123).expect("hrg_game");
898 assert_eq!(g1.ecount(), g2.ecount());
899 }
900
901 #[test]
902 fn hrg_sample_statistical_edge_count() {
903 let hrg = make_sample_hrg();
908 let n = 1000;
909 let graphs = hrg_sample_many(&hrg, n, 42).expect("hrg_sample_many");
910 let total_edges: usize = graphs.iter().map(Graph::ecount).sum();
911 #[allow(clippy::cast_precision_loss)]
912 let mean = total_edges as f64 / n as f64;
913 assert!((mean - 2.0).abs() < 0.2, "expected mean ~2.0, got {mean}");
914 }
915}