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}