Skip to main content

entropy_conservation/
entropy.rs

1//! Shannon entropy, Rényi entropy (order α), Tsallis entropy, and
2//! verification-entropy calculation from test-coverage vectors.
3
4use std::collections::BTreeMap;
5
6use crate::EntropyError;
7
8/// Wrapper to use f64 as a map key (total ordering with NaN handling).
9#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
10pub struct OrderedFloat(pub f64);
11
12impl PartialEq for OrderedFloat {
13    fn eq(&self, other: &Self) -> bool {
14        if self.0.is_nan() && other.0.is_nan() { true } else { self.0 == other.0 }
15    }
16}
17
18impl Eq for OrderedFloat {}
19
20impl PartialOrd for OrderedFloat {
21    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { Some(self.cmp(other)) }
22}
23
24impl Ord for OrderedFloat {
25    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
26        self.0.partial_cmp(&other.0).unwrap_or_else(|| {
27            if self.0.is_nan() && !other.0.is_nan() { std::cmp::Ordering::Greater } else { std::cmp::Ordering::Less }
28        })
29    }
30}
31
32impl std::hash::Hash for OrderedFloat {
33    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
34        if self.0.is_nan() {
35            0x7ff8000000000000u64.hash(state);
36        } else {
37            self.0.to_bits().hash(state);
38        }
39    }
40}
41
42/// Core entropy measurement for a verification system.
43///
44/// Combines three entropy families:
45/// - **Shannon** — the canonical H = -Σ pᵢ log pᵢ
46/// - **Rényi** — generalised entropy H_α = (1/(1-α)) log Σ pᵢ^α
47/// - **Tsallis** — S_q = (1/(q-1)) (1 - Σ pᵢ^q)
48#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
49pub struct VerificationEntropy {
50    /// Shannon entropy H = -Σ pᵢ log₂ pᵢ
51    pub shannon: f64,
52    /// Rényi entropy keyed by order α
53    pub renyi: BTreeMap<OrderedFloat, f64>,
54    /// Tsallis entropy (q = 2 by default, can be recomputed)
55    pub tsallis: f64,
56}
57
58impl VerificationEntropy {
59    /// Compute verification entropy from a probability distribution.
60    ///
61    /// `probs` need not be normalised — we normalise internally.
62    /// Uses base-2 logarithm (bits).
63    pub fn from_probabilities(probs: &[f64]) -> Result<Self, EntropyError> {
64        if probs.is_empty() {
65            return Err(EntropyError::EmptyVector);
66        }
67        for &p in probs {
68            if p < 0.0 {
69                return Err(EntropyError::NegativeProbability(p));
70            }
71        }
72
73        let total: f64 = probs.iter().sum();
74        if total <= 0.0 {
75            return Err(EntropyError::InvalidDistribution(total));
76        }
77
78        let p: Vec<f64> = probs.iter().map(|&x| x / total).collect();
79
80        // Shannon
81        let shannon = shannon_entropy(&p);
82
83        // Rényi at several orders
84        let mut renyi = BTreeMap::new();
85        for &alpha in &[0.5, 1.5, 2.0, 3.0, f64::INFINITY] {
86            if let Ok(h) = renyi_entropy(&p, alpha) {
87                renyi.insert(OrderedFloat(alpha), h);
88            }
89        }
90
91        // Tsallis (q = 2)
92        let tsallis = tsallis_entropy(&p, 2.0);
93
94        Ok(Self {
95            shannon,
96            renyi,
97            tsallis,
98        })
99    }
100
101    /// Convenience: compute from raw hit counts (e.g. per-path execution counts).
102    pub fn from_counts(counts: &[u64]) -> Result<Self, EntropyError> {
103        let probs: Vec<f64> = counts.iter().map(|&c| c as f64).collect();
104        Self::from_probabilities(&probs)
105    }
106
107    /// Maximum possible Shannon entropy for n equiprobable paths.
108    pub fn max_shannon(n: usize) -> f64 {
109        if n == 0 { 0.0 } else { (n as f64).log2() }
110    }
111
112    /// Normalised Shannon entropy ∈ [0, 1].
113    pub fn normalised(&self) -> f64 {
114        let n = self.renyi.len().max(1);
115        let max = Self::max_shannon(n);
116        if max == 0.0 { 0.0 } else { self.shannon / max }
117    }
118}
119
120/// Shannon entropy H = -Σ pᵢ log₂ pᵢ  (base-2, bits).
121pub fn shannon_entropy(p: &[f64]) -> f64 {
122    p.iter()
123        .filter(|pi| **pi > 0.0)
124        .map(|pi| -pi * pi.log2())
125        .sum()
126}
127
128/// Rényi entropy of order α: H_α = (1/(1-α)) log₂(Σ pᵢ^α).
129///
130/// α → 1 limit recovers Shannon entropy.
131/// α = 0  → Hartley (max) entropy log₂ n.
132/// α = 2  → collision entropy.
133/// α → ∞  → min-entropy.
134pub fn renyi_entropy(p: &[f64], alpha: f64) -> Result<f64, EntropyError> {
135    if alpha <= 0.0 {
136        return Err(EntropyError::InvalidRenyiOrder(alpha));
137    }
138    if (alpha - 1.0).abs() < 1e-12 {
139        // α ≈ 1 → Shannon
140        return Ok(shannon_entropy(p));
141    }
142    if alpha == f64::INFINITY {
143        return Ok(min_entropy(p));
144    }
145    let sum: f64 = p.iter().filter(|pi| **pi > 0.0).map(|pi| pi.powf(alpha)).sum();
146    if sum <= 0.0 {
147        return Ok(0.0);
148    }
149    Ok((1.0 / (1.0 - alpha)) * sum.log2())
150}
151
152/// Min-entropy: H_∞ = -log₂ max(pᵢ).
153pub fn min_entropy(p: &[f64]) -> f64 {
154    let max_p = p.iter().cloned().fold(0.0_f64, f64::max);
155    if max_p <= 0.0 { 0.0 } else { -max_p.log2() }
156}
157
158/// Tsallis entropy S_q = (1/(q-1))(1 - Σ pᵢ^q).
159pub fn tsallis_entropy(p: &[f64], q: f64) -> f64 {
160    if (q - 1.0).abs() < 1e-12 {
161        return shannon_entropy(p);
162    }
163    let sum: f64 = p.iter().filter(|pi| **pi > 0.0).map(|pi| pi.powf(q)).sum();
164    (1.0 / (q - 1.0)) * (1.0 - sum)
165}
166
167/// Kullback-Leibler divergence D_KL(P ‖ Q).
168pub fn kl_divergence(p: &[f64], q: &[f64]) -> f64 {
169    p.iter()
170        .zip(q.iter())
171        .filter(|(pi, _)| **pi > 0.0)
172        .map(|(pi, qi)| {
173            let qi_safe = if *qi > 0.0 { *qi } else { 1e-300 };
174            pi * (pi / qi_safe).log2()
175        })
176        .sum()
177}
178
179/// Cross-entropy H(P, Q) = -Σ pᵢ log₂ qᵢ.
180pub fn cross_entropy(p: &[f64], q: &[f64]) -> f64 {
181    p.iter()
182        .zip(q.iter())
183        .filter(|(pi, _)| **pi > 0.0)
184        .map(|(pi, qi)| {
185            let qi_safe = if *qi > 0.0 { *qi } else { 1e-300 };
186            -pi * qi_safe.log2()
187        })
188        .sum()
189}
190
191/// Jensen-Shannon divergence (symmetric, bounded).
192pub fn js_divergence(p: &[f64], q: &[f64]) -> f64 {
193    let m: Vec<f64> = p.iter().zip(q.iter()).map(|(a, b)| 0.5 * (a + b)).collect();
194    0.5 * kl_divergence(p, &m) + 0.5 * kl_divergence(q, &m)
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn entropy_non_negative_uniform() {
203        let p = vec![0.25, 0.25, 0.25, 0.25];
204        let h = shannon_entropy(&p);
205        assert!(h >= 0.0, "Shannon entropy must be non-negative");
206    }
207
208    #[test]
209    fn entropy_non_negative_degenerate() {
210        let p = vec![1.0, 0.0, 0.0];
211        let h = shannon_entropy(&p);
212        assert!(h >= 0.0);
213        assert!(h < 1e-12, "deterministic distribution has ~0 entropy");
214    }
215
216    #[test]
217    fn uniform_is_maximum_entropy() {
218        let p = vec![0.25; 4];
219        let h = shannon_entropy(&p);
220        assert!((h - 2.0).abs() < 1e-12, "4 equiprobable paths → 2 bits");
221    }
222
223    #[test]
224    fn verification_entropy_from_counts() {
225        let ve = VerificationEntropy::from_counts(&[10, 20, 30, 40]).unwrap();
226        assert!(ve.shannon >= 0.0);
227        assert!(ve.shannon < 2.0, "non-uniform should be < max");
228    }
229
230    #[test]
231    fn renyi_converges_to_shannon_as_alpha_1() {
232        let p = vec![0.1, 0.2, 0.3, 0.4];
233        let shannon = shannon_entropy(&p);
234        let renyi_near1 = renyi_entropy(&p, 1.0 + 1e-10).unwrap();
235        assert!(
236            (shannon - renyi_near1).abs() < 1e-6,
237            "Rényi(α→1) should ≈ Shannon: shannon={shannon}, renyi={renyi_near1}"
238        );
239    }
240
241    #[test]
242    fn renyi_monotone_decreasing_in_alpha() {
243        let p = vec![0.1, 0.2, 0.3, 0.4];
244        let h05 = renyi_entropy(&p, 0.5).unwrap();
245        let h1 = shannon_entropy(&p);
246        let h2 = renyi_entropy(&p, 2.0).unwrap();
247        let h3 = renyi_entropy(&p, 3.0).unwrap();
248        assert!(h05 >= h1 - 1e-12, "Rényi(0.5) ≥ Shannon");
249        assert!(h1 >= h2 - 1e-12, "Shannon ≥ Rényi(2)");
250        assert!(h2 >= h3 - 1e-12, "Rényi(2) ≥ Rényi(3)");
251    }
252
253    #[test]
254    fn tsallis_non_negative() {
255        let p = vec![0.3, 0.7];
256        let t = tsallis_entropy(&p, 2.0);
257        assert!(t >= 0.0, "Tsallis entropy must be non-negative for q>1");
258    }
259
260    #[test]
261    fn kl_divergence_non_negative() {
262        let p = vec![0.3, 0.7];
263        let q = vec![0.5, 0.5];
264        let d = kl_divergence(&p, &q);
265        assert!(d >= -1e-12, "KL divergence is non-negative");
266    }
267
268    #[test]
269    fn kl_divergence_zero_for_same() {
270        let p = vec![0.3, 0.7];
271        assert!((kl_divergence(&p, &p)).abs() < 1e-12);
272    }
273
274    #[test]
275    fn js_divergence_bounded() {
276        let p = vec![1.0, 0.0];
277        let q = vec![0.0, 1.0];
278        let js = js_divergence(&p, &q);
279        assert!(js > 0.0);
280        assert!(js <= 1.0, "JS divergence ≤ 1 bit");
281    }
282
283    #[test]
284    fn min_entropy_leq_shannon() {
285        let p = vec![0.1, 0.2, 0.3, 0.4];
286        let h_min = min_entropy(&p);
287        let h_shannon = shannon_entropy(&p);
288        assert!(h_min <= h_shannon + 1e-12);
289    }
290
291    #[test]
292    fn empty_vector_errors() {
293        assert!(VerificationEntropy::from_probabilities(&[]).is_err());
294    }
295
296    #[test]
297    fn negative_probability_errors() {
298        assert!(VerificationEntropy::from_probabilities(&[-0.1, 0.5]).is_err());
299    }
300}