crate::ix!();
impl TreeBalanceSymmetryMeasurer for Skeleton {
#[instrument(level = "trace", skip(self))]
fn measure_tree_balance_symmetry(&self) -> Option<f32> {
use std::collections::HashSet;
fn count_leaves(skel: &Skeleton, start_id: u16) -> usize {
let mut visited = HashSet::new();
let mut stack = vec![start_id];
let mut leaf_count = 0;
let mut cycle_seen = false;
while let Some(nid) = stack.pop() {
if !visited.insert(nid) {
cycle_seen = true;
continue;
}
if let Some(node) = skel.nodes().iter().find(|n| n.id() == nid) {
let kids = node.child_ids();
if kids.is_empty() {
leaf_count += 1;
} else {
for &cid in kids {
stack.push(cid);
}
}
} else {
leaf_count += 1;
}
}
if cycle_seen { 0 } else { leaf_count }
}
let mut ratios = Vec::new();
for node in self.nodes() {
let kids = node.child_ids();
if kids.len() < 2 { continue; }
let mut counts = Vec::with_capacity(kids.len());
for &cid in kids {
counts.push(count_leaves(self, cid) as f32);
}
let max = counts.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
if max == 0.0 { continue; }
let min = counts.iter().cloned().fold(f32::INFINITY, f32::min);
ratios.push(min / max);
}
if ratios.is_empty() {
None
} else {
Some(ratios.iter().sum::<f32>() / (ratios.len() as f32))
}
}
}
#[cfg(test)]
mod skeleton_balance_symmetry_measurer_assessment {
use super::*;
#[traced_test]
fn empty_skeleton_returns_none() {
let skel = SkeletonBuilder::default().build().unwrap();
assert!(skel.measure_tree_balance_symmetry().is_none());
}
#[traced_test]
fn single_node_returns_none() {
let node = SkeletonNodeBuilder::default()
.id(0_u16)
.name("solo")
.original_key("solo")
.build(NodeKind::LeafHolder)
.unwrap();
let skel = SkeletonBuilder::default()
.nodes(vec![node])
.root_id(Some(0))
.build()
.unwrap();
assert!(skel.measure_tree_balance_symmetry().is_none());
}
#[traced_test]
fn linear_chain_returns_none() {
let n0 = SkeletonNodeBuilder::default()
.id(0_u16).child_ids(vec![1])
.name("n0").original_key("n0")
.build(NodeKind::Dispatch)
.unwrap();
let n1 = SkeletonNodeBuilder::default()
.id(1_u16).child_ids(vec![2])
.name("n1").original_key("n1")
.build(NodeKind::Dispatch)
.unwrap();
let n2 = SkeletonNodeBuilder::default()
.id(2_u16)
.name("n2").original_key("n2")
.build(NodeKind::LeafHolder)
.unwrap();
let skel = SkeletonBuilder::default()
.nodes(vec![n0,n1,n2])
.root_id(Some(0))
.build().unwrap();
assert!(skel.measure_tree_balance_symmetry().is_none());
}
#[traced_test]
fn perfectly_balanced_binary_tree_returns_one() {
fn children(id: u16, kids: Vec<u16>, use_aggregate: bool) -> SkeletonNode {
let kind = if kids.is_empty() {
NodeKind::LeafHolder
} else if use_aggregate {
NodeKind::Aggregate
} else {
NodeKind::Dispatch
};
SkeletonNodeBuilder::default()
.id(id)
.child_ids(kids)
.name(format!("n{}", id))
.original_key(format!("n{}", id))
.build(kind)
.unwrap()
}
let n3 = children(3, vec![], false);
let n4 = children(4, vec![], false);
let n5 = children(5, vec![], false);
let n6 = children(6, vec![], false);
let n1 = children(1, vec![3,4], false); let n2 = children(2, vec![5,6], true); let n0 = children(0, vec![1,2], false);
let skel = SkeletonBuilder::default()
.nodes(vec![n0,n1,n2,n3,n4,n5,n6])
.root_id(Some(0))
.build().unwrap();
let sym = skel.measure_tree_balance_symmetry().unwrap();
assert!((sym - 1.0).abs() < f32::EPSILON);
}
#[traced_test]
fn unbalanced_tree_returns_fraction() {
fn make(id: u16, kids: Vec<u16>, use_agg: bool) -> SkeletonNode {
let k = if kids.is_empty() {
NodeKind::LeafHolder
} else if use_agg {
NodeKind::Aggregate
} else {
NodeKind::Dispatch
};
SkeletonNodeBuilder::default()
.id(id)
.child_ids(kids)
.name(format!("n{}", id))
.original_key(format!("n{}", id))
.build(k)
.unwrap()
}
let n3 = make(3, vec![], false);
let n1 = make(1, vec![3], false);
let n4 = make(4, vec![], false);
let n5 = make(5, vec![], false);
let n6 = make(6, vec![], false);
let n2 = make(2, vec![4,5,6], true); let n0 = make(0, vec![1,2], false);
let skel = SkeletonBuilder::default()
.nodes(vec![n0,n1,n2,n3,n4,n5,n6])
.root_id(Some(0))
.build().unwrap();
let sym = skel.measure_tree_balance_symmetry().unwrap();
let expected = (1.0/3.0 + 1.0) / 2.0;
assert!((sym - expected).abs() < 1e-6);
}
#[traced_test]
fn multiple_branching_nodes_average_correctly() {
fn mk(id: u16, kids: Vec<u16>, agg: bool) -> SkeletonNode {
let k = if kids.is_empty() {
NodeKind::LeafHolder
} else if agg {
NodeKind::Aggregate
} else {
NodeKind::Dispatch
};
SkeletonNodeBuilder::default()
.id(id)
.child_ids(kids)
.name(format!("n{}", id))
.original_key(format!("n{}", id))
.build(k)
.unwrap()
}
let leaves = |id: u16| mk(id, vec![], false);
let n5 = leaves(5);
let n6 = leaves(6);
let n7 = leaves(7);
let n8 = leaves(8);
let n2 = mk(2, vec![5,6,7,8], true); let n3 = leaves(3);
let n4 = leaves(4);
let n1 = mk(1, vec![3,4], false); let n0 = mk(0, vec![1,2], false);
let skel = SkeletonBuilder::default()
.nodes(vec![n0,n1,n2,n3,n4,n5,n6,n7,n8])
.root_id(Some(0))
.build().unwrap();
let sym = skel.measure_tree_balance_symmetry().unwrap();
let expected = (0.5 + 1.0 + 1.0) / 3.0;
assert!((sym - expected).abs() < 1e-6);
}
#[traced_test]
fn cycles_do_not_infinite_loop_and_are_ignored() {
let n0 = SkeletonNodeBuilder::default()
.id(0_u16).child_ids(vec![1,2])
.name("n0").original_key("n0")
.build(NodeKind::Dispatch)
.unwrap();
let n1 = SkeletonNodeBuilder::default()
.id(1_u16).child_ids(vec![0])
.name("n1").original_key("n1")
.build(NodeKind::Dispatch)
.unwrap();
let n2 = SkeletonNodeBuilder::default()
.id(2_u16).child_ids(vec![])
.name("n2").original_key("n2")
.build(NodeKind::LeafHolder)
.unwrap();
let skel = SkeletonBuilder::default()
.nodes(vec![n0,n1,n2])
.root_id(Some(0))
.build().unwrap();
let sym = skel.measure_tree_balance_symmetry().unwrap();
assert!((sym - 0.0).abs() < 1e-6);
}
#[traced_test]
fn invalid_children_are_skipped() {
let n0 = SkeletonNodeBuilder::default()
.id(0_u16).child_ids(vec![1,99])
.name("n0").original_key("n0")
.build(NodeKind::Dispatch)
.unwrap();
let n1 = SkeletonNodeBuilder::default()
.id(1_u16).child_ids(vec![])
.name("n1").original_key("n1")
.build(NodeKind::LeafHolder)
.unwrap();
let skel = SkeletonBuilder::default()
.nodes(vec![n0,n1])
.root_id(Some(0))
.build().unwrap();
let sym = skel.measure_tree_balance_symmetry().unwrap();
assert!((sym - 1.0).abs() < 1e-6);
}
}