Skip to main content

rust_igraph/algorithms/hrg/
mod.rs

1//! Hierarchical Random Graph (HRG) models.
2//!
3//! A hierarchical random graph is an ensemble of undirected graphs
4//! defined via a binary tree with `n` leaf vertices and `n-1` internal
5//! vertices labelled with connection probabilities. The probability
6//! that two leaf vertices are connected equals the probability at
7//! their lowest common ancestor.
8//!
9//! Reference: A. Clauset, C. Moore, and M.E.J. Newman. "Hierarchical
10//! structure and the prediction of missing links in networks." Nature
11//! 453, 98–101 (2008).
12
13mod 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/// A hierarchical random graph represented as a binary dendrogram.
22///
23/// Internal vertices are identified by negative indices starting at `-1`
24/// (the root). Leaf vertices use non-negative indices `0..n`. Each
25/// internal vertex `-(i+1)` has its data at index `i` in the vectors.
26///
27/// # Example
28///
29/// ```
30/// use rust_igraph::HrgTree;
31///
32/// let hrg = HrgTree::new(3);
33/// assert_eq!(hrg.size(), 3);
34/// assert_eq!(hrg.num_internal(), 2);
35/// ```
36#[derive(Debug, Clone)]
37pub struct HrgTree {
38    /// Number of leaf vertices.
39    n: u32,
40    /// Left child of each internal vertex. Negative = internal, non-negative = leaf.
41    pub left: Vec<i32>,
42    /// Right child of each internal vertex.
43    pub right: Vec<i32>,
44    /// Connection probability at each internal vertex.
45    pub prob: Vec<f64>,
46    /// Number of leaf vertices in the subtree of each internal vertex.
47    pub vertices: Vec<i32>,
48    /// Number of tree edges in the subtree below each internal vertex.
49    pub edges: Vec<i32>,
50}
51
52impl HrgTree {
53    /// Create a new HRG with `n` leaf vertices, all arrays zero-initialized.
54    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    /// The number of leaf vertices.
67    pub fn size(&self) -> u32 {
68        self.n
69    }
70
71    /// The number of internal vertices (`size - 1`, or 0 for trivial trees).
72    pub fn num_internal(&self) -> usize {
73        self.left.len()
74    }
75
76    /// Resize the HRG to hold `new_size` leaf vertices.
77    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
102/// Validate preconditions and compute in/out degrees for `hrg_create`.
103fn 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
152/// Find the root (in-degree 0) and validate degree structure.
153fn 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
216/// Build the HRG tree structure (children, probs, subtree counts).
217fn 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    // Build index: root first, then remaining internals in ascending order, then leaves
227    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    // Build out-edge adjacency
253    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    // Fill left/right children and assign probabilities
261    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    // Compute subtree counts via iterative post-order traversal
289    let mut stack: Vec<i32> = vec![-1];
290    while let Some(&current) = 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
342/// Create an [`HrgTree`] from a directed binary tree graph.
343///
344/// The input `graph` must be a directed, simple binary tree with an odd
345/// number of vertices `2n-1`: `n-1` internal vertices (out-degree 2) and
346/// `n` leaves (out-degree 0). Exactly one vertex (the root) must have
347/// in-degree 0; all others must have in-degree 1.
348///
349/// `prob` has one entry per internal vertex. Internal vertices are
350/// enumerated as: root first, then remaining internals in ascending
351/// vertex-id order.
352///
353/// # Example
354///
355/// ```
356/// use rust_igraph::{Graph, hrg_create};
357///
358/// // 5-vertex binary tree: root(0) -> 1,2; vertex 1 -> 3,4
359/// let g = Graph::from_edges(&[(0,1),(0,2),(1,3),(1,4)], true, Some(5)).unwrap();
360/// let prob = vec![0.3, 0.7]; // root=0.3, vertex1=0.7
361/// let hrg = hrg_create(&g, &prob).unwrap();
362/// assert_eq!(hrg.size(), 3);
363/// ```
364pub 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/// Result of converting an [`HrgTree`] to a dendrogram graph.
372#[derive(Debug, Clone)]
373pub struct HrgDendrogram {
374    /// The dendrogram as a directed graph. Vertices `0..n` are leaves,
375    /// vertices `n..2n-1` are internal nodes.
376    pub graph: Graph,
377    /// Connection probability per vertex. Leaves have `f64::NAN`.
378    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
393/// Convert an [`HrgTree`] into a directed graph dendrogram.
394///
395/// The graph has `2n - 1` vertices (`n = hrg.size()`): vertices `0..n`
396/// are leaves, `n..2n-1` are internal. Each internal vertex has two
397/// directed edges to its children.
398///
399/// # Example
400///
401/// ```
402/// use rust_igraph::{HrgTree, from_hrg_dendrogram};
403///
404/// let mut hrg = HrgTree::new(3);
405/// hrg.left[0] = 0;   hrg.right[0] = -2;  hrg.prob[0] = 0.5;
406/// hrg.left[1] = 1;   hrg.right[1] = 2;   hrg.prob[1] = 0.8;
407/// hrg.vertices = vec![3, 2];
408/// hrg.edges = vec![4, 2];
409///
410/// let d = from_hrg_dendrogram(&hrg).unwrap();
411/// assert_eq!(d.graph.vcount(), 5);
412/// assert_eq!(d.graph.ecount(), 4);
413/// assert!(d.prob[0].is_nan()); // leaf
414/// assert!((d.prob[3] - 0.5).abs() < 1e-10);
415/// ```
416pub 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/// Find the lowest common ancestor of two leaves in an HRG tree.
466///
467/// Returns the internal index of the LCA. Works by precomputing parent
468/// pointers and walking up from both leaves until they meet.
469#[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    // Walk both up via parent pointers, mark visited from one path
474    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; // reached root
483            }
484            cur = parent[idx];
485        } else {
486            // leaf → go to parent (cur is non-negative here)
487            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; // root is always LCA
500            }
501            cur = parent[idx];
502        } else {
503            cur = parent[num_internal + cur as usize];
504        }
505    }
506}
507
508/// Build a parent-pointer array for all nodes in the HRG.
509///
510/// Layout: `parent[0..num_internal]` = parent of internal node i,
511/// `parent[num_internal..num_internal+n]` = parent of leaf i.
512/// Root's parent is stored as 0 (unused sentinel).
513#[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    // parent[i] for i in 0..num_internal -> parent internal id of internal node i
522    // parent[num_internal + leaf_id] -> parent internal id of leaf
523    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
562/// Sample a random graph from a hierarchical random graph model.
563///
564/// For each pair of leaf vertices `(i, j)`, an undirected edge is added
565/// with probability equal to the connection probability at their lowest
566/// common ancestor in the dendrogram.
567///
568/// `seed` initialises the internal PRNG. Same `(hrg, seed)` always
569/// produces the same graph.
570///
571/// # Example
572///
573/// ```
574/// use rust_igraph::{HrgTree, hrg_sample};
575///
576/// let mut hrg = HrgTree::new(4);
577/// hrg.left[0] = -2;  hrg.right[0] = -3;  hrg.prob[0] = 0.5;
578/// hrg.left[1] = 0;   hrg.right[1] = 1;   hrg.prob[1] = 1.0;
579/// hrg.left[2] = 2;   hrg.right[2] = 3;   hrg.prob[2] = 1.0;
580/// hrg.vertices = vec![4, 2, 2];
581/// hrg.edges = vec![6, 2, 2];
582///
583/// let g = hrg_sample(&hrg, 42).unwrap();
584/// assert_eq!(g.vcount(), 4);
585/// // With prob=1.0 within subtrees, leaves 0-1 and 2-3 are always connected
586/// ```
587pub 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
597/// Sample multiple random graphs from a hierarchical random graph model.
598///
599/// Generates `num_samples` independent draws from the HRG ensemble.
600///
601/// # Example
602///
603/// ```
604/// use rust_igraph::{HrgTree, hrg_sample_many};
605///
606/// let mut hrg = HrgTree::new(3);
607/// hrg.left[0] = 0;   hrg.right[0] = -2;  hrg.prob[0] = 0.5;
608/// hrg.left[1] = 1;   hrg.right[1] = 2;   hrg.prob[1] = 1.0;
609/// hrg.vertices = vec![3, 2];
610/// hrg.edges = vec![4, 2];
611///
612/// let graphs = hrg_sample_many(&hrg, 5, 123).unwrap();
613/// assert_eq!(graphs.len(), 5);
614/// for g in &graphs {
615///     assert_eq!(g.vcount(), 3);
616/// }
617/// ```
618pub 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
633/// Generate a hierarchical random graph (alias for [`hrg_sample`]).
634pub 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    // ── HRG-002: hrg_sample / hrg_sample_many / hrg_game ──
773
774    fn make_sample_hrg() -> HrgTree {
775        // 3-leaf HRG: root splits into leaf 0 and internal node 1;
776        // internal node 1 splits into leaves 1 and 2.
777        let mut hrg = HrgTree::new(3);
778        hrg.left[0] = 0;
779        hrg.right[0] = -2; // internal node 1
780        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        // All probs = 1.0 → complete graph on 3 vertices (3 edges)
816        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        // With prob[0]=0.5, prob[1]=1.0, leaf pair (1,2) always
904        // connects (via internal 1, prob=1.0), while pairs (0,1) and
905        // (0,2) each connect with prob 0.5 (via root, prob=0.5).
906        // Expected edges = 1 + 0.5 + 0.5 = 2.0
907        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}