Skip to main content

rust_igraph/algorithms/constructors/
symmetric_tree.rs

1//! Symmetric tree constructor (ALGO-CN-005).
2//!
3//! Counterpart of `igraph_symmetric_tree()` in
4//! `references/igraph/src/constructors/regular.c:706-808`.
5//!
6//! A *symmetric tree* is a rooted tree where every vertex at depth `d`
7//! has exactly `branches[d]` children. Unlike [`kary_tree`], where every
8//! internal vertex has the same number of children, a symmetric tree
9//! lets the branching factor vary per level — for example
10//! `branches = [3, 2, 2]` produces a tree where the root has 3 kids,
11//! each of those has 2, and each of those has 2 again (so the leaves
12//! sit at depth 3).
13//!
14//! Vertex layout follows the same BFS scheme as `kary_tree`: vertex `0`
15//! is the root, then all depth-1 vertices in arrival order, then all
16//! depth-2 vertices in their arrival order, and so on. Arc direction is
17//! controlled by the shared [`TreeMode`] enum:
18//!
19//! * [`TreeMode::Out`] — directed, every edge flows **from parent to
20//!   child** (`parent → child`).
21//! * [`TreeMode::In`] — directed, every edge flows **from child to
22//!   parent** (`child → parent`).
23//! * [`TreeMode::Undirected`] — undirected; raw endpoints are
24//!   `(parent, child)` but stored as canonical `(min, max)` by
25//!   `Graph::add_edges`.
26//!
27//! The C implementation walks one BFS level at a time, expanding every
28//! parent of that level in turn, pushing exactly `branches[k]` outgoing
29//! arcs per parent. The Rust port mirrors that level-by-level loop.
30//!
31//! Vertex count is `1 + Σ_{i=0..d} Π_{j=0..=i} branches[j]`. Empty
32//! `branches` collapses to the singleton root (n = 1, 0 edges).
33//!
34//! Edge counts:
35//!
36//! * `branches.is_empty()` — singleton root, no edges.
37//! * non-empty `branches` — `n - 1` edges (always a tree).
38//!
39//! Time complexity: O(|V|).
40//!
41//! [`kary_tree`]: super::kary_tree::kary_tree
42
43use super::kary_tree::TreeMode;
44use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
45
46/// Build a symmetric tree where each vertex at depth `d` has
47/// `branches[d]` children.
48///
49/// Vertex `0` is the root. Children at each level are added in BFS
50/// order, and the per-level branching count is read from `branches`.
51/// Empty `branches` yields the singleton tree on one vertex.
52///
53/// # Errors
54///
55/// * [`IgraphError::InvalidArgument`] — any entry of `branches` is
56///   zero (a zero branching count yields a degenerate tree with no
57///   vertices at the next level, which upstream rejects).
58/// * [`IgraphError::InvalidArgument`] — vertex count overflows `u32`.
59///
60/// # Examples
61///
62/// ```
63/// use rust_igraph::{symmetric_tree, TreeMode};
64///
65/// // Root + 2 kids + 4 grandkids = 7 vertices (perfect binary depth-2).
66/// let t = symmetric_tree(&[2, 2], TreeMode::Undirected).unwrap();
67/// assert_eq!(t.vcount(), 7);
68/// assert_eq!(t.ecount(), 6);
69/// assert!(!t.is_directed());
70/// ```
71pub fn symmetric_tree(branches: &[u32], mode: TreeMode) -> IgraphResult<Graph> {
72    let directed = matches!(mode, TreeMode::Out | TreeMode::In);
73
74    if branches.is_empty() {
75        return Graph::new(1, directed);
76    }
77
78    for &b in branches {
79        if b == 0 {
80            return Err(IgraphError::InvalidArgument(
81                "symmetric_tree: number of branches must be positive at each level".to_string(),
82            ));
83        }
84    }
85
86    // Vertex count: 1 + b0 + b0*b1 + b0*b1*b2 + ... — checked-mul the
87    // running level width so overflow surfaces as an InvalidArgument
88    // rather than panicking.
89    let mut n: u32 = 1;
90    let mut level_width: u32 = 1;
91    for &b in branches {
92        level_width = level_width.checked_mul(b).ok_or_else(|| {
93            IgraphError::InvalidArgument("symmetric_tree: vertex count overflows u32".to_string())
94        })?;
95        n = n.checked_add(level_width).ok_or_else(|| {
96            IgraphError::InvalidArgument("symmetric_tree: vertex count overflows u32".to_string())
97        })?;
98    }
99
100    let edge_count = (n as usize) - 1;
101    let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(edge_count);
102
103    // BFS level walk mirroring the upstream C loop: at each level k,
104    // expand every parent in [parent .. level_end), pushing exactly
105    // branches[k] arcs apiece. `level_end` is the future value of
106    // `child` once that level finishes, so we snapshot it before the
107    // inner loop and only advance `parent` past it after.
108    let mut child: u32 = 1;
109    let mut parent: u32 = 0;
110    for &b in branches {
111        let level_end = child;
112        while parent < level_end {
113            for _ in 0..b {
114                match mode {
115                    TreeMode::Out | TreeMode::Undirected => edges.push((parent, child)),
116                    TreeMode::In => edges.push((child, parent)),
117                }
118                child += 1;
119            }
120            parent += 1;
121        }
122    }
123
124    let mut g = Graph::new(n, directed)?;
125    g.add_edges(edges)?;
126    Ok(g)
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    fn dump_edges(g: &Graph) -> Vec<(u32, u32)> {
134        let m = u32::try_from(g.ecount()).expect("ecount fits in u32 in tests");
135        (0..m)
136            .map(|e| g.edge(e).expect("edge id in bounds in tests"))
137            .collect()
138    }
139
140    #[test]
141    fn empty_branches_yields_singleton() {
142        let t = symmetric_tree(&[], TreeMode::Out).expect("empty branches");
143        assert_eq!(t.vcount(), 1);
144        assert_eq!(t.ecount(), 0);
145        assert!(t.is_directed());
146        let u = symmetric_tree(&[], TreeMode::Undirected).expect("empty branches undirected");
147        assert!(!u.is_directed());
148    }
149
150    #[test]
151    fn single_level_three_out_is_star_k1_3() {
152        // [3] → root with 3 leaves, 4 vertices, 3 edges. Equivalent to
153        // an out-star on 4 vertices centred at 0.
154        let t = symmetric_tree(&[3], TreeMode::Out).expect("star-like");
155        assert_eq!(t.vcount(), 4);
156        assert_eq!(t.ecount(), 3);
157        assert_eq!(dump_edges(&t), vec![(0, 1), (0, 2), (0, 3)]);
158    }
159
160    #[test]
161    fn single_level_three_in_reverses_arcs() {
162        let t = symmetric_tree(&[3], TreeMode::In).expect("star-like in");
163        assert_eq!(dump_edges(&t), vec![(1, 0), (2, 0), (3, 0)]);
164    }
165
166    #[test]
167    fn binary_depth_two_matches_kary_tree_seven_two() {
168        // [2, 2] → 1 + 2 + 4 = 7 vertices, identical to kary_tree(7, 2).
169        let t = symmetric_tree(&[2, 2], TreeMode::Out).expect("binary depth 2");
170        assert_eq!(t.vcount(), 7);
171        assert_eq!(t.ecount(), 6);
172        assert_eq!(
173            dump_edges(&t),
174            vec![(0, 1), (0, 2), (1, 3), (1, 4), (2, 5), (2, 6)]
175        );
176    }
177
178    #[test]
179    fn ternary_then_binary_layout() {
180        // [3, 2] → 1 + 3 + 6 = 10 vertices.
181        // Root 0 has children 1,2,3; each of those has 2 kids.
182        let t = symmetric_tree(&[3, 2], TreeMode::Out).expect("ternary then binary");
183        assert_eq!(t.vcount(), 10);
184        assert_eq!(t.ecount(), 9);
185        assert_eq!(
186            dump_edges(&t),
187            vec![
188                (0, 1),
189                (0, 2),
190                (0, 3),
191                (1, 4),
192                (1, 5),
193                (2, 6),
194                (2, 7),
195                (3, 8),
196                (3, 9),
197            ]
198        );
199    }
200
201    #[test]
202    fn chain_branches_one_one_one_is_linear_path() {
203        // [1, 1, 1] → 1 + 1 + 1 + 1 = 4 vertices in a line.
204        let t = symmetric_tree(&[1, 1, 1], TreeMode::Undirected).expect("chain");
205        assert_eq!(t.vcount(), 4);
206        assert_eq!(t.ecount(), 3);
207        let mut got = dump_edges(&t);
208        got.sort_unstable();
209        assert_eq!(got, vec![(0, 1), (1, 2), (2, 3)]);
210    }
211
212    #[test]
213    fn three_levels_mixed_three_two_one() {
214        // [3, 2, 1] → 1 + 3 + 6 + 6 = 16 vertices.
215        let t = symmetric_tree(&[3, 2, 1], TreeMode::Out).expect("3-level mixed");
216        assert_eq!(t.vcount(), 16);
217        assert_eq!(t.ecount(), 15);
218        // Spot-check: depth-2 vertices 4..=9 each have exactly one
219        // grandchild (10..=15) attached one-to-one in BFS order.
220        let edges = dump_edges(&t);
221        for (i, parent) in (4..=9u32).enumerate() {
222            #[allow(clippy::cast_possible_truncation)]
223            let child = 10u32 + i as u32;
224            assert!(edges.contains(&(parent, child)));
225        }
226    }
227
228    #[test]
229    fn zero_branch_rejected() {
230        let err = symmetric_tree(&[2, 0, 2], TreeMode::Out).expect_err("zero branch");
231        match err {
232            IgraphError::InvalidArgument(msg) => assert!(msg.contains("positive")),
233            other => panic!("unexpected error: {other:?}"),
234        }
235    }
236
237    #[test]
238    fn vertex_count_overflow_rejected() {
239        // 1 + 2^32 → overflow. branches=[2; 32] yields 2^32 leaves
240        // alone, well past u32::MAX.
241        let bad = [2u32; 32];
242        let err = symmetric_tree(&bad, TreeMode::Out).expect_err("overflow");
243        match err {
244            IgraphError::InvalidArgument(msg) => assert!(msg.contains("overflow")),
245            other => panic!("unexpected error: {other:?}"),
246        }
247    }
248
249    #[test]
250    fn undirected_canonicalises_edges() {
251        let t = symmetric_tree(&[2, 2], TreeMode::Undirected).expect("undirected");
252        for e in 0..u32::try_from(t.ecount()).unwrap() {
253            let (u, v) = t.edge(e).unwrap();
254            assert!(u < v, "edge ({u},{v}) is not canonical (min, max)");
255        }
256    }
257
258    #[test]
259    fn single_leaf_branches_one() {
260        // [1] → root + 1 leaf.
261        let t = symmetric_tree(&[1], TreeMode::Out).expect("single leaf");
262        assert_eq!(t.vcount(), 2);
263        assert_eq!(t.ecount(), 1);
264        assert_eq!(dump_edges(&t), vec![(0, 1)]);
265    }
266
267    #[test]
268    fn out_and_undirected_share_edge_endpoints() {
269        // Same (parent, child) tuples regardless of mode — the only
270        // difference for undirected is canonical ordering inside the
271        // Graph storage.
272        let out = symmetric_tree(&[2, 3], TreeMode::Out).expect("out");
273        let und = symmetric_tree(&[2, 3], TreeMode::Undirected).expect("und");
274        assert_eq!(out.vcount(), und.vcount());
275        assert_eq!(out.ecount(), und.ecount());
276        let mut out_pairs: Vec<(u32, u32)> = dump_edges(&out)
277            .into_iter()
278            .map(|(u, v)| (u.min(v), u.max(v)))
279            .collect();
280        let mut und_pairs = dump_edges(&und);
281        out_pairs.sort_unstable();
282        und_pairs.sort_unstable();
283        assert_eq!(out_pairs, und_pairs);
284    }
285}
286
287#[cfg(all(test, feature = "proptest-harness"))]
288mod proptests {
289    use super::*;
290    use proptest::prelude::*;
291
292    fn branches_strategy() -> impl Strategy<Value = Vec<u32>> {
293        prop::collection::vec(1u32..=3, 0..=4)
294    }
295
296    proptest! {
297        // Edge count is always n - 1 for non-empty branches and 0 for
298        // an empty branches slice.
299        #[test]
300        fn edge_count_matches_n_minus_one(branches in branches_strategy()) {
301            let t = symmetric_tree(&branches, TreeMode::Out).unwrap();
302            let n: u32 = t.vcount();
303            let m: usize = t.ecount();
304            if branches.is_empty() {
305                prop_assert_eq!(n, 1);
306                prop_assert_eq!(m, 0);
307            } else {
308                prop_assert_eq!(m, (n as usize).saturating_sub(1));
309            }
310        }
311
312        // Vertex count equals the cumulative-product BFS sum.
313        #[test]
314        fn vertex_count_matches_cumulative_product(branches in branches_strategy()) {
315            let t = symmetric_tree(&branches, TreeMode::Undirected).unwrap();
316            let mut expected: u32 = 1;
317            let mut level: u32 = 1;
318            for &b in &branches {
319                level = level.checked_mul(b).expect("no overflow in small proptest range");
320                expected = expected.checked_add(level).expect("no overflow");
321            }
322            prop_assert_eq!(t.vcount(), expected);
323        }
324
325        // Mode round-trip: Out and In must yield the same |E| and the
326        // same parent-child multiset modulo flipping endpoints.
327        #[test]
328        fn out_and_in_have_swapped_endpoints(branches in branches_strategy()) {
329            let out = symmetric_tree(&branches, TreeMode::Out).unwrap();
330            let inn = symmetric_tree(&branches, TreeMode::In).unwrap();
331            prop_assert_eq!(out.vcount(), inn.vcount());
332            prop_assert_eq!(out.ecount(), inn.ecount());
333            let m = u32::try_from(out.ecount()).unwrap();
334            for e in 0..m {
335                let (a, b) = out.edge(e).unwrap();
336                let (c, d) = inn.edge(e).unwrap();
337                prop_assert_eq!((a, b), (d, c));
338            }
339        }
340    }
341}