entropy_conservation/
gradient.rs1use crate::entropy::shannon_entropy;
6use crate::flow::ModuleId;
7
8#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
10pub struct GradientSuggestion {
11 pub module: ModuleId,
12 pub action: String,
13 pub expected_reduction: f64,
14 pub priority: f64,
15}
16
17#[derive(Debug, Clone)]
19pub struct GradientDescent {
20 pub module_entropy: std::collections::HashMap<ModuleId, f64>,
22 pub branch_factor: std::collections::HashMap<ModuleId, f64>,
24 pub test_coverage: std::collections::HashMap<ModuleId, f64>,
26}
27
28impl GradientDescent {
29 pub fn new() -> Self {
31 Self {
32 module_entropy: Default::default(),
33 branch_factor: Default::default(),
34 test_coverage: Default::default(),
35 }
36 }
37
38 pub fn with_entropy(mut self, module: &str, h: f64) -> Self {
40 self.module_entropy.insert(module.into(), h);
41 self
42 }
43
44 pub fn with_branches(mut self, module: &str, b: f64) -> Self {
46 self.branch_factor.insert(module.into(), b);
47 self
48 }
49
50 pub fn with_coverage(mut self, module: &str, c: f64) -> Self {
52 self.test_coverage.insert(module.into(), c);
53 self
54 }
55
56 pub fn gradient(&self) -> Vec<(ModuleId, f64)> {
60 let mut grad: Vec<(ModuleId, f64)> = self
61 .module_entropy
62 .iter()
63 .map(|(m, &h)| {
64 let branches = self.branch_factor.get(m).copied().unwrap_or(1.0);
65 let coverage = self.test_coverage.get(m).copied().unwrap_or(1.0);
66 let g = h * branches / coverage.max(0.01);
69 (m.clone(), g)
70 })
71 .collect();
72 grad.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
73 grad
74 }
75
76 pub fn suggest(&self) -> Vec<GradientSuggestion> {
78 let grad = self.gradient();
79 let mut suggestions = Vec::new();
80
81 for (module, g) in &grad {
82 let coverage = self.test_coverage.get(module).copied().unwrap_or(1.0);
83 let branches = self.branch_factor.get(module).copied().unwrap_or(1.0);
84 let h = self.module_entropy.get(module).copied().unwrap_or(0.0);
85
86 if coverage < 0.8 {
88 let uncov = 1.0 - coverage;
89 suggestions.push(GradientSuggestion {
90 module: module.clone(),
91 action: format!(
92 "Add tests to '{}': coverage is {:.0}%, expected ~{:.1} new paths to cover",
93 module, coverage * 100.0, branches * uncov
94 ),
95 expected_reduction: h * uncov * 0.5,
96 priority: g * uncov,
97 });
98 }
99
100 if branches > 5.0 {
102 let excess = branches - 5.0;
103 suggestions.push(GradientSuggestion {
104 module: module.clone(),
105 action: format!(
106 "Split '{}': branching factor {:.0} exceeds threshold, refactor into {} smaller functions",
107 module, branches, (branches / 3.0).ceil() as usize
108 ),
109 expected_reduction: h * (excess / branches) * 0.3,
110 priority: g * (excess / branches),
111 });
112 }
113
114 if h > 2.0 {
116 suggestions.push(GradientSuggestion {
117 module: module.clone(),
118 action: format!(
119 "Reduce entropy in '{}': H={:.2} bits is high, consider simplifying control flow",
120 module, h
121 ),
122 expected_reduction: (h - 2.0) * 0.2,
123 priority: g * 0.5,
124 });
125 }
126 }
127
128 suggestions.sort_by(|a, b| b.priority.partial_cmp(&a.priority).unwrap_or(std::cmp::Ordering::Equal));
129 suggestions
130 }
131}
132
133pub fn descend(probs: &[f64], lr: f64) -> Vec<f64> {
136 let h = shannon_entropy(probs);
137 if h < 1e-12 {
138 return probs.to_vec();
139 }
140 let grad: Vec<f64> = probs
142 .iter()
143 .map(|&p| {
144 if p > 0.0 {
145 -(p.log2() + 1.0)
146 } else {
147 0.0
148 }
149 })
150 .collect();
151
152 let mut new_p: Vec<f64> = probs.iter().zip(grad.iter()).map(|(&p, &g)| p - lr * g).collect();
154
155 for p in &mut new_p {
157 *p = p.max(0.0);
158 }
159 let total: f64 = new_p.iter().sum();
160 if total > 0.0 {
161 for p in &mut new_p {
162 *p /= total;
163 }
164 }
165 new_p
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 #[test]
173 fn gradient_highlights_highest_entropy() {
174 let gd = GradientDescent::new()
175 .with_entropy("big_mod", 3.5)
176 .with_entropy("small_mod", 0.5)
177 .with_branches("big_mod", 10.0)
178 .with_branches("small_mod", 2.0)
179 .with_coverage("big_mod", 0.3)
180 .with_coverage("small_mod", 0.9);
181
182 let grad = gd.gradient();
183 assert_eq!(grad[0].0, "big_mod");
184 assert!(grad[0].1 > grad[1].1);
185 }
186
187 #[test]
188 fn suggest_adds_tests_for_low_coverage() {
189 let gd = GradientDescent::new()
190 .with_entropy("mod_a", 2.0)
191 .with_coverage("mod_a", 0.4)
192 .with_branches("mod_a", 3.0);
193
194 let suggestions = gd.suggest();
195 assert!(suggestions.iter().any(|s| s.action.contains("Add tests")));
196 }
197
198 #[test]
199 fn suggest_splits_high_branches() {
200 let gd = GradientDescent::new()
201 .with_entropy("mod_b", 2.5)
202 .with_coverage("mod_b", 0.9)
203 .with_branches("mod_b", 12.0);
204
205 let suggestions = gd.suggest();
206 assert!(suggestions.iter().any(|s| s.action.contains("Split")));
207 }
208
209 #[test]
210 fn descent_reduces_entropy() {
211 let p = vec![0.1, 0.2, 0.3, 0.4];
212 let h_before = shannon_entropy(&p);
213 let p_new = descend(&p, 0.01);
214 let h_after = shannon_entropy(&p_new);
215 assert!(h_after < h_before + 1e-12, "descent should not increase entropy");
216 }
217
218 #[test]
219 fn descent_preserves_simplex() {
220 let p = vec![0.1, 0.2, 0.3, 0.4];
221 let p_new = descend(&p, 0.05);
222 let sum: f64 = p_new.iter().sum();
223 assert!((sum - 1.0).abs() < 1e-10, "probabilities should sum to 1");
224 for &pi in &p_new {
225 assert!(pi >= 0.0, "probabilities must be non-negative");
226 }
227 }
228}