rust_igraph/algorithms/constructors/
symmetric_tree.rs1use super::kary_tree::TreeMode;
44use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
45
46pub 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 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 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 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 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 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 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 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 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 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 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 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 #[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 #[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 #[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}