capability_skeleton/
tree_balance_symmetry_measurer.rs

1// ---------------- [ File: capability-skeleton/src/tree_balance_symmetry_measurer.rs ]
2crate::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        /// Count the number of *structural* leaves under `start_id`, but:
11        /// - if we ever revisit a node → we’ve detected a cycle and return 0
12        /// - if a child_id isn’t found in `self.nodes()` → we treat it as one leaf
13        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                // cycle detection
21                if !visited.insert(nid) {
22                    cycle_seen = true;
23                    continue;
24                }
25                // try find the node
26                if let Some(node) = skel.nodes().iter().find(|n| n.id() == nid) {
27                    let kids = node.child_ids();
28                    if kids.is_empty() {
29                        // a real structural leaf
30                        leaf_count += 1;
31                    } else {
32                        // traverse its children
33                        for &cid in kids {
34                            stack.push(cid);
35                        }
36                    }
37                } else {
38                    // invalid child reference = treat as one leaf
39                    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            // collect leaf‐counts per child‐subtree
51            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        // 0->1->2 : no node has >=2 children
98        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        //      0
123        //     / \
124        //    1   2
125        //   /\   /\
126        //  3 4  5 6
127
128        // We'll define a helper that picks Dispatch if child_ids non-empty, else LeafHolder:
129        fn children(id: u16, kids: Vec<u16>, use_aggregate: bool) -> SkeletonNode {
130            // If `use_aggregate` is true, we do NodeKind::Aggregate, else NodeKind::Dispatch
131            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);   // dispatch
152        let n2 = children(2, vec![5,6], true);    // let's pick aggregator for variety
153        let n0 = children(0, vec![1,2], false);   // dispatch
154
155        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        //  0
167        // / \
168        //1   2
169        //|   /|\
170        //3  4,5,6
171        //
172        // Branching nodes:
173        //  • Node 0 → leaves-under-1 = 1, leaves-under-2 = 3 ⇒ ratio = 1/3
174        //  • Node 2 → children 4,5,6 are all true leaves ⇒ min/max = 1/1 ⇒ 1.0
175        // average = (1/3 + 1.0)/2 ≈ 0.6666667
176
177        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); // aggregator
200        let n0 = make(0, vec![1,2], false);  // dispatch
201
202        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        // Now *all* branching nodes count:
215        //  • Node 0 → children 1 (2 leaves) & 2 (4 leaves) ⇒ 2/4 = 0.5
216        //  • Node 1 → children 3,4 both leaves ⇒ 1/1 = 1.0
217        //  • Node 2 → children 5,6,7,8 all leaves ⇒ 1/1 = 1.0
218        // average = (0.5 + 1.0 + 1.0) / 3 = 0.8333333…
219
220        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);  // aggregator
244        let n3 = leaves(3);
245        let n4 = leaves(4);
246        let n1 = mk(1, vec![3,4], false);     // dispatch
247        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        // 0→1, 1→0, 0→2, 2→()
262        // Node 0 has children [1,2].  Subtrees:
263        //   via 1: cycle -> 0 leaves
264        //   via 2: 1 leaf => ratio=0
265
266        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        // 0→[1,99], 1→() => child=99 is invalid => ratio=1
294
295        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}