entropy_conservation/
entropy.rs1use std::collections::BTreeMap;
5
6use crate::EntropyError;
7
8#[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#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
49pub struct VerificationEntropy {
50 pub shannon: f64,
52 pub renyi: BTreeMap<OrderedFloat, f64>,
54 pub tsallis: f64,
56}
57
58impl VerificationEntropy {
59 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 let shannon = shannon_entropy(&p);
82
83 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 let tsallis = tsallis_entropy(&p, 2.0);
93
94 Ok(Self {
95 shannon,
96 renyi,
97 tsallis,
98 })
99 }
100
101 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 pub fn max_shannon(n: usize) -> f64 {
109 if n == 0 { 0.0 } else { (n as f64).log2() }
110 }
111
112 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
120pub 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
128pub 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 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
152pub 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
158pub 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
167pub 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
179pub 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
191pub 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}