use super::kary_tree::TreeMode;
use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
pub fn symmetric_tree(branches: &[u32], mode: TreeMode) -> IgraphResult<Graph> {
let directed = matches!(mode, TreeMode::Out | TreeMode::In);
if branches.is_empty() {
return Graph::new(1, directed);
}
for &b in branches {
if b == 0 {
return Err(IgraphError::InvalidArgument(
"symmetric_tree: number of branches must be positive at each level".to_string(),
));
}
}
let mut n: u32 = 1;
let mut level_width: u32 = 1;
for &b in branches {
level_width = level_width.checked_mul(b).ok_or_else(|| {
IgraphError::InvalidArgument("symmetric_tree: vertex count overflows u32".to_string())
})?;
n = n.checked_add(level_width).ok_or_else(|| {
IgraphError::InvalidArgument("symmetric_tree: vertex count overflows u32".to_string())
})?;
}
let edge_count = (n as usize) - 1;
let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(edge_count);
let mut child: u32 = 1;
let mut parent: u32 = 0;
for &b in branches {
let level_end = child;
while parent < level_end {
for _ in 0..b {
match mode {
TreeMode::Out | TreeMode::Undirected => edges.push((parent, child)),
TreeMode::In => edges.push((child, parent)),
}
child += 1;
}
parent += 1;
}
}
let mut g = Graph::new(n, directed)?;
g.add_edges(edges)?;
Ok(g)
}
#[cfg(test)]
mod tests {
use super::*;
fn dump_edges(g: &Graph) -> Vec<(u32, u32)> {
let m = u32::try_from(g.ecount()).expect("ecount fits in u32 in tests");
(0..m)
.map(|e| g.edge(e).expect("edge id in bounds in tests"))
.collect()
}
#[test]
fn empty_branches_yields_singleton() {
let t = symmetric_tree(&[], TreeMode::Out).expect("empty branches");
assert_eq!(t.vcount(), 1);
assert_eq!(t.ecount(), 0);
assert!(t.is_directed());
let u = symmetric_tree(&[], TreeMode::Undirected).expect("empty branches undirected");
assert!(!u.is_directed());
}
#[test]
fn single_level_three_out_is_star_k1_3() {
let t = symmetric_tree(&[3], TreeMode::Out).expect("star-like");
assert_eq!(t.vcount(), 4);
assert_eq!(t.ecount(), 3);
assert_eq!(dump_edges(&t), vec![(0, 1), (0, 2), (0, 3)]);
}
#[test]
fn single_level_three_in_reverses_arcs() {
let t = symmetric_tree(&[3], TreeMode::In).expect("star-like in");
assert_eq!(dump_edges(&t), vec![(1, 0), (2, 0), (3, 0)]);
}
#[test]
fn binary_depth_two_matches_kary_tree_seven_two() {
let t = symmetric_tree(&[2, 2], TreeMode::Out).expect("binary depth 2");
assert_eq!(t.vcount(), 7);
assert_eq!(t.ecount(), 6);
assert_eq!(
dump_edges(&t),
vec![(0, 1), (0, 2), (1, 3), (1, 4), (2, 5), (2, 6)]
);
}
#[test]
fn ternary_then_binary_layout() {
let t = symmetric_tree(&[3, 2], TreeMode::Out).expect("ternary then binary");
assert_eq!(t.vcount(), 10);
assert_eq!(t.ecount(), 9);
assert_eq!(
dump_edges(&t),
vec![
(0, 1),
(0, 2),
(0, 3),
(1, 4),
(1, 5),
(2, 6),
(2, 7),
(3, 8),
(3, 9),
]
);
}
#[test]
fn chain_branches_one_one_one_is_linear_path() {
let t = symmetric_tree(&[1, 1, 1], TreeMode::Undirected).expect("chain");
assert_eq!(t.vcount(), 4);
assert_eq!(t.ecount(), 3);
let mut got = dump_edges(&t);
got.sort_unstable();
assert_eq!(got, vec![(0, 1), (1, 2), (2, 3)]);
}
#[test]
fn three_levels_mixed_three_two_one() {
let t = symmetric_tree(&[3, 2, 1], TreeMode::Out).expect("3-level mixed");
assert_eq!(t.vcount(), 16);
assert_eq!(t.ecount(), 15);
let edges = dump_edges(&t);
for (i, parent) in (4..=9u32).enumerate() {
#[allow(clippy::cast_possible_truncation)]
let child = 10u32 + i as u32;
assert!(edges.contains(&(parent, child)));
}
}
#[test]
fn zero_branch_rejected() {
let err = symmetric_tree(&[2, 0, 2], TreeMode::Out).expect_err("zero branch");
match err {
IgraphError::InvalidArgument(msg) => assert!(msg.contains("positive")),
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn vertex_count_overflow_rejected() {
let bad = [2u32; 32];
let err = symmetric_tree(&bad, TreeMode::Out).expect_err("overflow");
match err {
IgraphError::InvalidArgument(msg) => assert!(msg.contains("overflow")),
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn undirected_canonicalises_edges() {
let t = symmetric_tree(&[2, 2], TreeMode::Undirected).expect("undirected");
for e in 0..u32::try_from(t.ecount()).unwrap() {
let (u, v) = t.edge(e).unwrap();
assert!(u < v, "edge ({u},{v}) is not canonical (min, max)");
}
}
#[test]
fn single_leaf_branches_one() {
let t = symmetric_tree(&[1], TreeMode::Out).expect("single leaf");
assert_eq!(t.vcount(), 2);
assert_eq!(t.ecount(), 1);
assert_eq!(dump_edges(&t), vec![(0, 1)]);
}
#[test]
fn out_and_undirected_share_edge_endpoints() {
let out = symmetric_tree(&[2, 3], TreeMode::Out).expect("out");
let und = symmetric_tree(&[2, 3], TreeMode::Undirected).expect("und");
assert_eq!(out.vcount(), und.vcount());
assert_eq!(out.ecount(), und.ecount());
let mut out_pairs: Vec<(u32, u32)> = dump_edges(&out)
.into_iter()
.map(|(u, v)| (u.min(v), u.max(v)))
.collect();
let mut und_pairs = dump_edges(&und);
out_pairs.sort_unstable();
und_pairs.sort_unstable();
assert_eq!(out_pairs, und_pairs);
}
}
#[cfg(all(test, feature = "proptest-harness"))]
mod proptests {
use super::*;
use proptest::prelude::*;
fn branches_strategy() -> impl Strategy<Value = Vec<u32>> {
prop::collection::vec(1u32..=3, 0..=4)
}
proptest! {
#[test]
fn edge_count_matches_n_minus_one(branches in branches_strategy()) {
let t = symmetric_tree(&branches, TreeMode::Out).unwrap();
let n: u32 = t.vcount();
let m: usize = t.ecount();
if branches.is_empty() {
prop_assert_eq!(n, 1);
prop_assert_eq!(m, 0);
} else {
prop_assert_eq!(m, (n as usize).saturating_sub(1));
}
}
#[test]
fn vertex_count_matches_cumulative_product(branches in branches_strategy()) {
let t = symmetric_tree(&branches, TreeMode::Undirected).unwrap();
let mut expected: u32 = 1;
let mut level: u32 = 1;
for &b in &branches {
level = level.checked_mul(b).expect("no overflow in small proptest range");
expected = expected.checked_add(level).expect("no overflow");
}
prop_assert_eq!(t.vcount(), expected);
}
#[test]
fn out_and_in_have_swapped_endpoints(branches in branches_strategy()) {
let out = symmetric_tree(&branches, TreeMode::Out).unwrap();
let inn = symmetric_tree(&branches, TreeMode::In).unwrap();
prop_assert_eq!(out.vcount(), inn.vcount());
prop_assert_eq!(out.ecount(), inn.ecount());
let m = u32::try_from(out.ecount()).unwrap();
for e in 0..m {
let (a, b) = out.edge(e).unwrap();
let (c, d) = inn.edge(e).unwrap();
prop_assert_eq!((a, b), (d, c));
}
}
}
}