1crate::ix!();
3
4impl TreeBalanceSymmetryMeasurer for Skeleton {
5
6 #[instrument(level = "trace", skip(self))]
7 fn measure_tree_balance_symmetry(&self) -> Option<f32> {
8 use std::collections::HashSet;
9
10 fn count_leaves(skel: &Skeleton, start_id: u16) -> usize {
14 let mut visited = HashSet::new();
15 let mut stack = vec![start_id];
16 let mut leaf_count = 0;
17 let mut cycle_seen = false;
18
19 while let Some(nid) = stack.pop() {
20 if !visited.insert(nid) {
22 cycle_seen = true;
23 continue;
24 }
25 if let Some(node) = skel.nodes().iter().find(|n| n.id() == nid) {
27 let kids = node.child_ids();
28 if kids.is_empty() {
29 leaf_count += 1;
31 } else {
32 for &cid in kids {
34 stack.push(cid);
35 }
36 }
37 } else {
38 leaf_count += 1;
40 }
41 }
42
43 if cycle_seen { 0 } else { leaf_count }
44 }
45
46 let mut ratios = Vec::new();
47 for node in self.nodes() {
48 let kids = node.child_ids();
49 if kids.len() < 2 { continue; }
50 let mut counts = Vec::with_capacity(kids.len());
52 for &cid in kids {
53 counts.push(count_leaves(self, cid) as f32);
54 }
55 let max = counts.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
56 if max == 0.0 { continue; }
57 let min = counts.iter().cloned().fold(f32::INFINITY, f32::min);
58 ratios.push(min / max);
59 }
60
61 if ratios.is_empty() {
62 None
63 } else {
64 Some(ratios.iter().sum::<f32>() / (ratios.len() as f32))
65 }
66 }
67}
68
69#[cfg(test)]
70mod skeleton_balance_symmetry_measurer_assessment {
71 use super::*;
72
73 #[traced_test]
74 fn empty_skeleton_returns_none() {
75 let skel = SkeletonBuilder::default().build().unwrap();
76 assert!(skel.measure_tree_balance_symmetry().is_none());
77 }
78
79 #[traced_test]
80 fn single_node_returns_none() {
81 let node = SkeletonNodeBuilder::default()
82 .id(0_u16)
83 .name("solo")
84 .original_key("solo")
85 .build(NodeKind::LeafHolder)
86 .unwrap();
87 let skel = SkeletonBuilder::default()
88 .nodes(vec![node])
89 .root_id(Some(0))
90 .build()
91 .unwrap();
92 assert!(skel.measure_tree_balance_symmetry().is_none());
93 }
94
95 #[traced_test]
96 fn linear_chain_returns_none() {
97 let n0 = SkeletonNodeBuilder::default()
99 .id(0_u16).child_ids(vec![1])
100 .name("n0").original_key("n0")
101 .build(NodeKind::Dispatch)
102 .unwrap();
103 let n1 = SkeletonNodeBuilder::default()
104 .id(1_u16).child_ids(vec![2])
105 .name("n1").original_key("n1")
106 .build(NodeKind::Dispatch)
107 .unwrap();
108 let n2 = SkeletonNodeBuilder::default()
109 .id(2_u16)
110 .name("n2").original_key("n2")
111 .build(NodeKind::LeafHolder)
112 .unwrap();
113 let skel = SkeletonBuilder::default()
114 .nodes(vec![n0,n1,n2])
115 .root_id(Some(0))
116 .build().unwrap();
117 assert!(skel.measure_tree_balance_symmetry().is_none());
118 }
119
120 #[traced_test]
121 fn perfectly_balanced_binary_tree_returns_one() {
122 fn children(id: u16, kids: Vec<u16>, use_aggregate: bool) -> SkeletonNode {
130 let kind = if kids.is_empty() {
132 NodeKind::LeafHolder
133 } else if use_aggregate {
134 NodeKind::Aggregate
135 } else {
136 NodeKind::Dispatch
137 };
138 SkeletonNodeBuilder::default()
139 .id(id)
140 .child_ids(kids)
141 .name(format!("n{}", id))
142 .original_key(format!("n{}", id))
143 .build(kind)
144 .unwrap()
145 }
146
147 let n3 = children(3, vec![], false);
148 let n4 = children(4, vec![], false);
149 let n5 = children(5, vec![], false);
150 let n6 = children(6, vec![], false);
151 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()
156 .nodes(vec![n0,n1,n2,n3,n4,n5,n6])
157 .root_id(Some(0))
158 .build().unwrap();
159
160 let sym = skel.measure_tree_balance_symmetry().unwrap();
161 assert!((sym - 1.0).abs() < f32::EPSILON);
162 }
163
164 #[traced_test]
165 fn unbalanced_tree_returns_fraction() {
166 fn make(id: u16, kids: Vec<u16>, use_agg: bool) -> SkeletonNode {
178 let k = if kids.is_empty() {
179 NodeKind::LeafHolder
180 } else if use_agg {
181 NodeKind::Aggregate
182 } else {
183 NodeKind::Dispatch
184 };
185 SkeletonNodeBuilder::default()
186 .id(id)
187 .child_ids(kids)
188 .name(format!("n{}", id))
189 .original_key(format!("n{}", id))
190 .build(k)
191 .unwrap()
192 }
193
194 let n3 = make(3, vec![], false);
195 let n1 = make(1, vec![3], false);
196 let n4 = make(4, vec![], false);
197 let n5 = make(5, vec![], false);
198 let n6 = make(6, vec![], false);
199 let n2 = make(2, vec![4,5,6], true); let n0 = make(0, vec![1,2], false); let skel = SkeletonBuilder::default()
203 .nodes(vec![n0,n1,n2,n3,n4,n5,n6])
204 .root_id(Some(0))
205 .build().unwrap();
206
207 let sym = skel.measure_tree_balance_symmetry().unwrap();
208 let expected = (1.0/3.0 + 1.0) / 2.0;
209 assert!((sym - expected).abs() < 1e-6);
210 }
211
212 #[traced_test]
213 fn multiple_branching_nodes_average_correctly() {
214 fn mk(id: u16, kids: Vec<u16>, agg: bool) -> SkeletonNode {
221 let k = if kids.is_empty() {
222 NodeKind::LeafHolder
223 } else if agg {
224 NodeKind::Aggregate
225 } else {
226 NodeKind::Dispatch
227 };
228 SkeletonNodeBuilder::default()
229 .id(id)
230 .child_ids(kids)
231 .name(format!("n{}", id))
232 .original_key(format!("n{}", id))
233 .build(k)
234 .unwrap()
235 }
236 let leaves = |id: u16| mk(id, vec![], false);
237
238 let n5 = leaves(5);
239 let n6 = leaves(6);
240 let n7 = leaves(7);
241 let n8 = leaves(8);
242
243 let n2 = mk(2, vec![5,6,7,8], true); let n3 = leaves(3);
245 let n4 = leaves(4);
246 let n1 = mk(1, vec![3,4], false); let n0 = mk(0, vec![1,2], false);
248
249 let skel = SkeletonBuilder::default()
250 .nodes(vec![n0,n1,n2,n3,n4,n5,n6,n7,n8])
251 .root_id(Some(0))
252 .build().unwrap();
253
254 let sym = skel.measure_tree_balance_symmetry().unwrap();
255 let expected = (0.5 + 1.0 + 1.0) / 3.0;
256 assert!((sym - expected).abs() < 1e-6);
257 }
258
259 #[traced_test]
260 fn cycles_do_not_infinite_loop_and_are_ignored() {
261 let n0 = SkeletonNodeBuilder::default()
267 .id(0_u16).child_ids(vec![1,2])
268 .name("n0").original_key("n0")
269 .build(NodeKind::Dispatch)
270 .unwrap();
271 let n1 = SkeletonNodeBuilder::default()
272 .id(1_u16).child_ids(vec![0])
273 .name("n1").original_key("n1")
274 .build(NodeKind::Dispatch)
275 .unwrap();
276 let n2 = SkeletonNodeBuilder::default()
277 .id(2_u16).child_ids(vec![])
278 .name("n2").original_key("n2")
279 .build(NodeKind::LeafHolder)
280 .unwrap();
281
282 let skel = SkeletonBuilder::default()
283 .nodes(vec![n0,n1,n2])
284 .root_id(Some(0))
285 .build().unwrap();
286
287 let sym = skel.measure_tree_balance_symmetry().unwrap();
288 assert!((sym - 0.0).abs() < 1e-6);
289 }
290
291 #[traced_test]
292 fn invalid_children_are_skipped() {
293 let n0 = SkeletonNodeBuilder::default()
296 .id(0_u16).child_ids(vec![1,99])
297 .name("n0").original_key("n0")
298 .build(NodeKind::Dispatch)
299 .unwrap();
300 let n1 = SkeletonNodeBuilder::default()
301 .id(1_u16).child_ids(vec![])
302 .name("n1").original_key("n1")
303 .build(NodeKind::LeafHolder)
304 .unwrap();
305
306 let skel = SkeletonBuilder::default()
307 .nodes(vec![n0,n1])
308 .root_id(Some(0))
309 .build().unwrap();
310
311 let sym = skel.measure_tree_balance_symmetry().unwrap();
312 assert!((sym - 1.0).abs() < 1e-6);
313 }
314}