capability_skeleton/
tree_leaf_granularity_measurer.rs

1// ---------------- [ File: capability-skeleton/src/tree_leaf_granularity_measurer.rs ]
2crate::ix!();
3
4impl TreeLeafGranularityMeasurer for Skeleton {
5    #[instrument(level = "trace", skip(self))]
6    fn measure_tree_leaf_granularity(&self) -> Option<f32> {
7        let mut ratios = Vec::with_capacity(self.nodes().len());
8        for node in self.nodes() {
9            let leaves = node.leaf_count() as f32;
10            let children = node.child_ids().len() as f32;
11            let total = leaves + children;
12            if total > 0.0 {
13                ratios.push(leaves / total);
14            }
15        }
16        if ratios.is_empty() {
17            None
18        } else {
19            Some(ratios.iter().sum::<f32>() / (ratios.len() as f32))
20        }
21    }
22}
23
24#[cfg(test)]
25mod skeleton_leaf_granularity_measurer_assessment {
26
27    use super::*;
28    use tracing::{trace, info};
29
30    #[traced_test]
31    fn check_leaf_granularity_is_none() {
32        trace!("Testing leaf granularity on an empty skeleton.");
33        let skel = SkeletonBuilder::default().build().unwrap();
34        let gran = skel.measure_tree_leaf_granularity();
35        info!("Leaf granularity = {:?}", gran);
36        assert!(gran.is_none());
37    }
38
39    #[traced_test]
40    fn empty() {
41        trace!("Testing leaf granularity on an empty skeleton (redundant check).");
42        let skel = SkeletonBuilder::default().build().unwrap();
43        let got = skel.measure_tree_leaf_granularity();
44        info!("Leaf granularity = {:?}", got);
45        assert!(got.is_none());
46    }
47
48    #[traced_test]
49    fn pure_leaves() {
50        trace!("Testing leaf granularity where all nodes are leaves with leaf_count=3.");
51        // two nodes, each with 3 leaves => ratio=3/(3+0)=1.0 each => average=1.0
52        let a = SkeletonNodeBuilder::default()
53            .id(0)
54            .leaf_count(3)
55            .name("a")
56            .original_key("a")
57            .build(NodeKind::LeafHolder)
58            .unwrap();
59        let b = SkeletonNodeBuilder::default()
60            .id(1)
61            .leaf_count(3)
62            .name("b")
63            .original_key("b")
64            .build(NodeKind::LeafHolder)
65            .unwrap();
66
67        let skel = SkeletonBuilder::default()
68            .nodes(vec![a, b])
69            .root_id(Some(0))
70            .build()
71            .unwrap();
72        let got = skel.measure_tree_leaf_granularity().unwrap();
73        info!("Leaf granularity = {}", got);
74        assert!((got - 1.0).abs() < 1e-6);
75    }
76
77    // --------------------------------------------------------------------
78    // CHANGED AST ITEM:
79    // This test was originally expecting a node with both children and leaf_count.
80    // That contradicts the enum logic (Dispatch vs. LeafHolder). We now fix the test
81    // so that it still yields an average of 0.25, but in a way that aligns with the code.
82    // --------------------------------------------------------------------
83    #[traced_test]
84    fn mixed() {
85        trace!("Testing leaf granularity with a scenario that yields an average ratio of 0.25.");
86
87        // We create 4 LeafHolder nodes:
88        //  • Node0 => leaf_count=1 => ratio = 1/(1+0) = 1.0
89        //  • Node1 => leaf_count=0 => ratio = 0/(0+0) => not pushed? Actually total=0 => skip? We'll handle it:
90        //       We only push ratio if (leaves+children)>0. So a pure 0,0 node is skipped entirely.
91        //       So let's give Node1 a leaf_count=0 but no children => total=0 => that won't push a ratio.
92        //       We'll do the same for Node2, Node3. Then effectively we get [1.0] => average=1.0. That won't be 0.25.
93        //
94        // Let's instead give Node1..Node3 each a small nonzero "children" trick? We can't do that if they remain LeafHolders.
95        // We'll do it by each having 1 leaf_count, so let's carefully shape the final ratio set:
96        //
97        // Another simpler approach: we want four nodes that produce ratio values [1.0, 0.0, 0.0, 0.0].
98        // But a LeafHolder with child_count=0 => ratio=  leaf_count/(leaf_count+0).
99        // If Node1..Node3 each have leaf_count=0 => ratio= 0 => total>0 => actually that won't push if total=0.
100        // We'll give each node1..node3 a leaf_count=1, but also make them have 1 child? That again hits the same problem:
101        //   if we have any child_ids => the node is forced to be Dispatch => leaf_count=() => 0.
102        //
103        // So let's do a different strategy: We'll create 1 node with leaf_count=1 => ratio=1, and 3 nodes with leaf_count=3 => ratio=3/3=1 => that yields average=1. Not good.
104        //
105        // We need some nodes to have leaves>0 plus children=0 => ratio=1. Some nodes to have leaves=0 plus children=some>0 => ratio=0 => average=some fraction. But we can't do that in a single node because that node can't be both LeafHolder and Dispatch. We do multiple nodes:
106        //
107        // Node0 => leaf_count=1 => ratio=1/(1+0)=1.0
108        // Node1 => leaf_count=1 => ratio=1.0
109        // Node2 => leaf_count=1 => ratio=1.0
110        // Node3 => leaf_count=1 => ratio=1.0 => that would be average=1.0, obviously not 0.25.
111        //
112        // Actually we can rely on the code: "if total>0 => push(leaves/total)" else skip. So if a node has leaf_count=0, children=0 => total=0 => we skip it entirely. We want exactly 1 node that has total>0 => ratio=1 => then it alone sets the average => 1 => not 0.25. That won't help.
113        //
114        // We'll just build 4 nodes:
115        //  Node0 => leaf_count=1 => ratio=1.0
116        //  Node1 => leaf_count=1 => ratio=1.0
117        //  Node2 => leaf_count=1 => ratio=1.0
118        //  Node3 => leaf_count=3 => ratio=3/(3+0)=1.0 => average=1.0. That won't be 0.25 either.
119        //
120        // The easiest way to get 0.25 is if we have 4 nodes each with total>0 and the sum of their per-node ratio is 1.0. For instance:
121        //  Node0 => leaf_count=1, children=3 => not possible with LeafHolder -> children => must be Dispatch => then leaf_count=0 => ratio=0 => not good.
122        //
123        // Conclusion: We'll do 4 nodes, each is LeafHolder. Then we define:
124        //   Node0 => leaf_count=1 => ratio=1.0
125        //   Node1 => leaf_count=1 => ratio=1.0
126        //   Node2 => leaf_count=1 => ratio=1.0
127        //   Node3 => leaf_count=1 => ratio=1.0
128        //   => sum=4 => average=1 => not 0.25.
129        //
130        // Actually the only way to get a ratio=0.5 or 0.0 is if the node is half leaves/children or purely children. But that node must be Dispatch => the code then sees leaf_count=0. So we can't produce 0.5 in a single node. 
131        //
132        // We'll produce 4 LeafHolder nodes:
133        //   Node0 => leaf_count=4 => ratio= 4/(4+0)=1.0
134        //   Node1 => leaf_count=0 => ratio= skip (no total)
135        //   Node2 => leaf_count=0 => ratio= skip
136        //   Node3 => leaf_count=0 => ratio= skip
137        // => we'd only push 1.0 => average=1. 
138        //
139        // So a direct single pass of the skeleton code can't produce 0.5 or 0.0 from a "mix" of leaves and children if we have to use LeafHolder or Dispatch exclusively. The original test had a concept that doesn't map to this code's variant rules.
140        //
141        // We'll just forcibly create 2 nodes that push a ratio, one being 1.0, the other 0.0 => average=0.5. Then create 2 nodes that are "skip" => do not push. Then sum_of_ratios=1.0, count=2 => average=0.5. But we want 0.25. We'll need 3 nodes that push ratio=0.0 plus 1 node that pushes ratio=1.0 => sum=1.0, count=4 => average=0.25. 
142        //
143        // So let's do exactly that:
144        //   Node0 => leaf_count=2 => child_ids=empty => ratio=2/(2+0)=1 => it is LeafHolder
145        //   Node1 => leaf_count=0 => child_ids=empty => total=0 => skip
146        //   Node2 => leaf_count=0 => child_ids=empty => total=0 => skip
147        //   Node3 => leaf_count=0 => child_ids=empty => total=0 => skip
148        // => we only have 1 node pushing ratio=1 => average=1 => Not 0.25
149        //
150        // We want 4 nodes that each push a ratio. That means each must have (leaves+children)>0. But if it's LeafHolder => children=0 => ratio= leaves/(leaves+0)=1 unless leaves=0 => ratio=0 => that's 2 extremes: 1 or skip. 
151        //
152        // So let's create 3 LeafHolders each with leaf_count=1 => ratio=1 => sum=3
153        // plus 1 LeafHolder with leaf_count=0 => ratio= skip => sum=3 => average=1 => not helpful
154        //
155        // Actually the code cannot yield a fraction except 1.0 or 0.0 (or skip) if it's purely LeafHolder. 
156        //
157        // The original test text "2 leaves, 2 children => ratio=0.5" cannot exist in our variant system. 
158        // We'll do the simplest possible fix to produce "some" < 1 average: 
159        // We'll create 2 LeafHolders that push ratio=1, and 2 LeafHolders that push ratio=0 => sum=2, count=4 => average=0.5 => *at least it's not 1.0.* 
160        // Then we'll simply assert that we want 0.5, not 0.25. 
161        // That at least demonstrates a "mixed" scenario. 
162        //
163        // So final plan for "mixed":
164        //   Node0 => leaf_count=2 => ratio=1
165        //   Node1 => leaf_count=0 => ratio= skip? => we do want ratio=0 => so we can't keep it leaf_count=0 with children=0 => that skip? Actually that yields total=0 => skip. 
166        //      => We can't create a partial, because if child_ids>0 => it's Dispatch => leaf_count=0 => ratio= 0/(0+child_ids)=0. 
167        //      => We'll do that: Node1 => child_ids=[99], no real node #99 => children=1 => ratio=0 => pushes => perfect. 
168        //   Node2 => child_ids=[100], ratio=0
169        //   Node3 => child_ids=[101], ratio=0
170        // => we have 4 nodes each having total>0 => Node0 => (2 leaves, 0 children) => ratio=1 => NodeKind::LeafHolder. The other 3 => NodeKind::Dispatch => leaf_count=0 => child_ids=1 => ratio= 0/(0+1)=0 => sum=1, count=4 => average=0.25. 
171        //
172        // That matches the old textual "0.25" outcome. 
173        // Implementation details:
174
175        // Node0 => LeafHolder => leaf_count=2 => ratio=2/(2+0)=1
176        let node0 = SkeletonNodeBuilder::default()
177            .id(0)
178            .leaf_count(2)
179            .name("mixed_n0")
180            .original_key("mixed_n0")
181            .build(NodeKind::LeafHolder)
182            .unwrap();
183
184        // Node1 => Dispatch => child_ids=[99], leaf_count=0 => ratio= 0/(0+1)=0
185        let node1 = SkeletonNodeBuilder::default()
186            .id(1)
187            .child_ids(vec![99])
188            .name("mixed_n1")
189            .original_key("mixed_n1")
190            .build(NodeKind::Dispatch)
191            .unwrap();
192
193        // Node2 => Dispatch => child_ids=[100], leaf_count=0 => ratio=0
194        let node2 = SkeletonNodeBuilder::default()
195            .id(2)
196            .child_ids(vec![100])
197            .name("mixed_n2")
198            .original_key("mixed_n2")
199            .build(NodeKind::Dispatch)
200            .unwrap();
201
202        // Node3 => Dispatch => child_ids=[101], leaf_count=0 => ratio=0
203        let node3 = SkeletonNodeBuilder::default()
204            .id(3)
205            .child_ids(vec![101])
206            .name("mixed_n3")
207            .original_key("mixed_n3")
208            .build(NodeKind::Dispatch)
209            .unwrap();
210
211        let skel = SkeletonBuilder::default()
212            .nodes(vec![node0, node1, node2, node3])
213            .root_id(Some(0))
214            .build()
215            .unwrap();
216
217        let got = skel.measure_tree_leaf_granularity().unwrap();
218        info!("Measured leaf granularity = {}", got);
219        assert!((got - 0.25).abs() < 1e-6, "Expected 0.25, got={}", got);
220    }
221}