Skip to main content

entropy_conservation/
gradient.rs

1//! Entropy gradient descent: suggest code changes that reduce H.
2//!
3//! Split large functions, add missing tests, reduce branching.
4
5use crate::entropy::shannon_entropy;
6use crate::flow::ModuleId;
7
8/// A concrete suggestion for reducing verification entropy.
9#[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/// Engine for computing entropy-reducing suggestions.
18#[derive(Debug, Clone)]
19pub struct GradientDescent {
20    /// Current entropy per module.
21    pub module_entropy: std::collections::HashMap<ModuleId, f64>,
22    /// Branching factor per module (e.g. cyclomatic complexity).
23    pub branch_factor: std::collections::HashMap<ModuleId, f64>,
24    /// Test coverage per module ∈ [0, 1].
25    pub test_coverage: std::collections::HashMap<ModuleId, f64>,
26}
27
28impl GradientDescent {
29    /// Create a new gradient descent engine.
30    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    /// Set entropy for a module.
39    pub fn with_entropy(mut self, module: &str, h: f64) -> Self {
40        self.module_entropy.insert(module.into(), h);
41        self
42    }
43
44    /// Set branching factor for a module.
45    pub fn with_branches(mut self, module: &str, b: f64) -> Self {
46        self.branch_factor.insert(module.into(), b);
47        self
48    }
49
50    /// Set test coverage for a module.
51    pub fn with_coverage(mut self, module: &str, c: f64) -> Self {
52        self.test_coverage.insert(module.into(), c);
53        self
54    }
55
56    /// Compute the entropy gradient: partial derivative of total H w.r.t. each module.
57    ///
58    /// Higher gradient → more entropy reduction potential.
59    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                // Gradient magnitude: entropy × branching / coverage
67                // High entropy + high branching + low coverage → biggest opportunity
68                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    /// Generate actionable suggestions sorted by expected impact.
77    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            // Low coverage → suggest adding tests
87            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            // High branching → suggest splitting
101            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            // High absolute entropy → general reduction
115            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
133/// Simulate one step of entropy gradient descent.
134/// Returns updated probability distribution after a step of size `lr`.
135pub 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    // Gradient of H w.r.t. pᵢ: ∂H/∂pᵢ = -(log₂ pᵢ + 1)
141    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    // Step: p_new = p - lr * grad, then project back to simplex
153    let mut new_p: Vec<f64> = probs.iter().zip(grad.iter()).map(|(&p, &g)| p - lr * g).collect();
154
155    // Project onto probability simplex: clip negatives, renormalise
156    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}