Skip to main content

rust_igraph/algorithms/constructors/
regular_tree.rs

1//! Regular tree constructor (ALGO-CN-006).
2//!
3//! Counterpart of `igraph_regular_tree()` in
4//! `references/igraph/src/constructors/regular.c:806-865`.
5//!
6//! A *regular tree* (a.k.a. **Bethe lattice**) of height `h` and degree
7//! `k` is a rooted tree where every non-leaf vertex has total degree
8//! exactly `k`. Concretely:
9//!
10//! * the root has `k` children (its total degree is `k`),
11//! * every internal non-root vertex has `k - 1` children plus its parent,
12//!   so its total degree is also `k`,
13//! * leaves sit at depth `h` and have degree 1.
14//!
15//! This is different from a k-ary tree (where every internal vertex —
16//! including the root — has the same number of *children*), and from a
17//! symmetric tree (where the branching factor varies per level).
18//!
19//! Upstream C builds it as a thin wrapper over `igraph_symmetric_tree`
20//! by constructing `branches = [k, k - 1, k - 1, ...]` of length `h` and
21//! delegating. The Rust port mirrors that.
22//!
23//! Vertex count: `1 + k * Σ_{i=0..h} (k-1)^i = 1 + k * ((k-1)^h - 1) / (k - 2)`
24//! for `k >= 3`; `1 + h` for `k = 2` (degenerate linear chain).
25//!
26//! Edge count: `n - 1` (a tree).
27//!
28//! Time complexity: O(|V|).
29//!
30//! See also [`kary_tree`] / [`symmetric_tree`] for the related shapes.
31//!
32//! [`kary_tree`]: super::kary_tree::kary_tree
33//! [`symmetric_tree`]: super::symmetric_tree::symmetric_tree
34
35use super::kary_tree::TreeMode;
36use super::symmetric_tree::symmetric_tree;
37use crate::core::{Graph, IgraphError, IgraphResult};
38
39/// Build a regular tree (Bethe lattice) of height `h` and degree `k`.
40///
41/// Vertex `0` is the root and has `k` children; every internal non-root
42/// vertex has `k - 1` children plus its parent, giving total degree `k`.
43/// Leaves sit at depth `h`.
44///
45/// # Errors
46///
47/// * [`IgraphError::InvalidArgument`] — `h < 1` (height must be at least
48///   one; height zero would be a singleton, which is not a Bethe
49///   lattice — upstream rejects the same input).
50/// * [`IgraphError::InvalidArgument`] — `k < 2` (degree must be at least
51///   two; degree one would mean every internal vertex has exactly one
52///   neighbour, which is degenerate).
53/// * [`IgraphError::InvalidArgument`] — vertex count overflows `u32`
54///   (propagated through the underlying [`symmetric_tree`] call).
55///
56/// # Examples
57///
58/// ```
59/// use rust_igraph::{regular_tree, TreeMode};
60///
61/// // h=2, k=3 — root has 3 kids, each internal has 2 kids: 1 + 3 + 6 = 10.
62/// let t = regular_tree(2, 3, TreeMode::Undirected).unwrap();
63/// assert_eq!(t.vcount(), 10);
64/// assert_eq!(t.ecount(), 9);
65/// assert!(!t.is_directed());
66/// ```
67pub fn regular_tree(h: u32, k: u32, mode: TreeMode) -> IgraphResult<Graph> {
68    if h < 1 {
69        return Err(IgraphError::InvalidArgument(format!(
70            "regular_tree: height must be at least 1, got {h}"
71        )));
72    }
73    if k < 2 {
74        return Err(IgraphError::InvalidArgument(format!(
75            "regular_tree: degree must be at least 2, got {k}"
76        )));
77    }
78
79    // branches = [k, k-1, k-1, ...] of length h.
80    // The root contributes k children; every non-root non-leaf level
81    // contributes k-1 children per parent (the missing one is the parent
82    // edge), keeping total degree at exactly k.
83    let mut branches: Vec<u32> = vec![k - 1; h as usize];
84    branches[0] = k;
85
86    symmetric_tree(&branches, mode)
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    fn dump_edges(g: &Graph) -> Vec<(u32, u32)> {
94        let m = u32::try_from(g.ecount()).expect("ecount fits in u32 in tests");
95        (0..m)
96            .map(|e| g.edge(e).expect("edge id in bounds in tests"))
97            .collect()
98    }
99
100    #[test]
101    fn h1_k3_is_star_k1_3() {
102        // h=1, k=3 — root with 3 leaves. Equivalent to star K1,3.
103        let t = regular_tree(1, 3, TreeMode::Out).expect("star-like");
104        assert_eq!(t.vcount(), 4);
105        assert_eq!(t.ecount(), 3);
106        assert_eq!(dump_edges(&t), vec![(0, 1), (0, 2), (0, 3)]);
107    }
108
109    #[test]
110    fn h2_k3_root_has_three_kids_each_has_two() {
111        // h=2, k=3 — root has 3 children, each child has 2 children.
112        // 1 + 3 + 6 = 10 vertices.
113        let t = regular_tree(2, 3, TreeMode::Out).expect("h2 k3");
114        assert_eq!(t.vcount(), 10);
115        assert_eq!(t.ecount(), 9);
116        // Same as symmetric_tree([3,2]).
117        assert_eq!(
118            dump_edges(&t),
119            vec![
120                (0, 1),
121                (0, 2),
122                (0, 3),
123                (1, 4),
124                (1, 5),
125                (2, 6),
126                (2, 7),
127                (3, 8),
128                (3, 9),
129            ]
130        );
131    }
132
133    #[test]
134    fn h3_k2_is_linear_chain() {
135        // k=2 forces branches=[2,1,1] — root has 2 kids, then a chain.
136        let t = regular_tree(3, 2, TreeMode::Out).expect("h3 k2");
137        assert_eq!(t.vcount(), 1 + 2 + 2 + 2);
138        assert_eq!(t.ecount(), 6);
139    }
140
141    #[test]
142    fn h1_k2_is_single_edge_pair() {
143        // h=1, k=2: branches=[2] — root with 2 leaves (path P3 rooted
144        // at centre). 1 + 2 = 3 vertices.
145        let t = regular_tree(1, 2, TreeMode::Undirected).expect("h1 k2");
146        assert_eq!(t.vcount(), 3);
147        assert_eq!(t.ecount(), 2);
148    }
149
150    #[test]
151    fn in_mode_reverses_arcs() {
152        let t = regular_tree(1, 3, TreeMode::In).expect("in h1 k3");
153        assert_eq!(dump_edges(&t), vec![(1, 0), (2, 0), (3, 0)]);
154    }
155
156    #[test]
157    fn undirected_is_undirected() {
158        let t = regular_tree(2, 3, TreeMode::Undirected).expect("undirected");
159        assert!(!t.is_directed());
160    }
161
162    #[test]
163    fn h_zero_rejected() {
164        let err = regular_tree(0, 3, TreeMode::Out).expect_err("h=0");
165        match err {
166            IgraphError::InvalidArgument(msg) => assert!(msg.contains("height")),
167            other => panic!("unexpected error: {other:?}"),
168        }
169    }
170
171    #[test]
172    fn k_less_than_two_rejected() {
173        let err = regular_tree(3, 1, TreeMode::Out).expect_err("k=1");
174        match err {
175            IgraphError::InvalidArgument(msg) => assert!(msg.contains("degree")),
176            other => panic!("unexpected error: {other:?}"),
177        }
178        let err0 = regular_tree(3, 0, TreeMode::Out).expect_err("k=0");
179        match err0 {
180            IgraphError::InvalidArgument(msg) => assert!(msg.contains("degree")),
181            other => panic!("unexpected error: {other:?}"),
182        }
183    }
184
185    #[test]
186    fn vertex_count_overflow_propagates() {
187        // h=32, k=3 → branches=[3, 2; 31] → 3 * 2^31 = ~6.4 billion leaves
188        // alone, well over u32::MAX. Should bubble up as InvalidArgument.
189        let err = regular_tree(40, 3, TreeMode::Out).expect_err("overflow");
190        match err {
191            IgraphError::InvalidArgument(msg) => assert!(msg.contains("overflow")),
192            other => panic!("unexpected error: {other:?}"),
193        }
194    }
195
196    #[test]
197    fn root_has_degree_k() {
198        // In the directed (Out) tree, root's out-degree equals k.
199        let t = regular_tree(3, 4, TreeMode::Out).expect("h3 k4");
200        let mut root_out_degree = 0u32;
201        let m = u32::try_from(t.ecount()).unwrap();
202        for e in 0..m {
203            let (u, _v) = t.edge(e).unwrap();
204            if u == 0 {
205                root_out_degree += 1;
206            }
207        }
208        assert_eq!(root_out_degree, 4);
209    }
210
211    #[test]
212    fn internal_non_root_vertex_has_total_degree_k_undirected() {
213        // For k=4, h=3, each non-leaf non-root vertex must have 4
214        // incident edges (1 parent + 3 children).
215        let t = regular_tree(3, 4, TreeMode::Undirected).expect("h3 k4");
216        let m = u32::try_from(t.ecount()).unwrap();
217        // Vertex 1 is depth 1 (non-root, non-leaf). Count its incident
218        // edges.
219        let mut deg1 = 0u32;
220        for e in 0..m {
221            let (u, v) = t.edge(e).unwrap();
222            if u == 1 || v == 1 {
223                deg1 += 1;
224            }
225        }
226        assert_eq!(deg1, 4);
227    }
228
229    #[test]
230    fn h2_k4_vertex_and_edge_counts() {
231        // h=2, k=4 — branches=[4, 3]. 1 + 4 + 12 = 17 vertices, 16 edges.
232        let t = regular_tree(2, 4, TreeMode::Out).expect("h2 k4");
233        assert_eq!(t.vcount(), 17);
234        assert_eq!(t.ecount(), 16);
235    }
236}
237
238#[cfg(all(test, feature = "proptest-harness"))]
239mod proptests {
240    use super::*;
241    use proptest::prelude::*;
242
243    proptest! {
244        // Always a tree: ecount = n - 1 for any valid (h, k).
245        #[test]
246        fn edge_count_is_n_minus_one(h in 1u32..=4, k in 2u32..=4) {
247            let t = regular_tree(h, k, TreeMode::Out).unwrap();
248            let n = t.vcount();
249            prop_assert_eq!(t.ecount(), (n as usize) - 1);
250        }
251
252        // Mode round-trip Out vs In: same edge count, endpoints swapped.
253        #[test]
254        fn out_and_in_endpoints_swap(h in 1u32..=3, k in 2u32..=3) {
255            let out = regular_tree(h, k, TreeMode::Out).unwrap();
256            let inn = regular_tree(h, k, TreeMode::In).unwrap();
257            prop_assert_eq!(out.vcount(), inn.vcount());
258            prop_assert_eq!(out.ecount(), inn.ecount());
259            let m = u32::try_from(out.ecount()).unwrap();
260            for e in 0..m {
261                let (a, b) = out.edge(e).unwrap();
262                let (c, d) = inn.edge(e).unwrap();
263                prop_assert_eq!((a, b), (d, c));
264            }
265        }
266
267        // For k>=2, root out-degree equals k in the Out mode.
268        #[test]
269        fn root_out_degree_equals_k(h in 1u32..=3, k in 2u32..=4) {
270            let t = regular_tree(h, k, TreeMode::Out).unwrap();
271            let m = u32::try_from(t.ecount()).unwrap();
272            let mut root_deg = 0u32;
273            for e in 0..m {
274                let (u, _v) = t.edge(e).unwrap();
275                if u == 0 { root_deg += 1; }
276            }
277            prop_assert_eq!(root_deg, k);
278        }
279    }
280}