use std::collections::BTreeMap;
use crate::EntropyError;
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
pub struct OrderedFloat(pub f64);
impl PartialEq for OrderedFloat {
fn eq(&self, other: &Self) -> bool {
if self.0.is_nan() && other.0.is_nan() { true } else { self.0 == other.0 }
}
}
impl Eq for OrderedFloat {}
impl PartialOrd for OrderedFloat {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { Some(self.cmp(other)) }
}
impl Ord for OrderedFloat {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.0.partial_cmp(&other.0).unwrap_or_else(|| {
if self.0.is_nan() && !other.0.is_nan() { std::cmp::Ordering::Greater } else { std::cmp::Ordering::Less }
})
}
}
impl std::hash::Hash for OrderedFloat {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
if self.0.is_nan() {
0x7ff8000000000000u64.hash(state);
} else {
self.0.to_bits().hash(state);
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct VerificationEntropy {
pub shannon: f64,
pub renyi: BTreeMap<OrderedFloat, f64>,
pub tsallis: f64,
}
impl VerificationEntropy {
pub fn from_probabilities(probs: &[f64]) -> Result<Self, EntropyError> {
if probs.is_empty() {
return Err(EntropyError::EmptyVector);
}
for &p in probs {
if p < 0.0 {
return Err(EntropyError::NegativeProbability(p));
}
}
let total: f64 = probs.iter().sum();
if total <= 0.0 {
return Err(EntropyError::InvalidDistribution(total));
}
let p: Vec<f64> = probs.iter().map(|&x| x / total).collect();
let shannon = shannon_entropy(&p);
let mut renyi = BTreeMap::new();
for &alpha in &[0.5, 1.5, 2.0, 3.0, f64::INFINITY] {
if let Ok(h) = renyi_entropy(&p, alpha) {
renyi.insert(OrderedFloat(alpha), h);
}
}
let tsallis = tsallis_entropy(&p, 2.0);
Ok(Self {
shannon,
renyi,
tsallis,
})
}
pub fn from_counts(counts: &[u64]) -> Result<Self, EntropyError> {
let probs: Vec<f64> = counts.iter().map(|&c| c as f64).collect();
Self::from_probabilities(&probs)
}
pub fn max_shannon(n: usize) -> f64 {
if n == 0 { 0.0 } else { (n as f64).log2() }
}
pub fn normalised(&self) -> f64 {
let n = self.renyi.len().max(1);
let max = Self::max_shannon(n);
if max == 0.0 { 0.0 } else { self.shannon / max }
}
}
pub fn shannon_entropy(p: &[f64]) -> f64 {
p.iter()
.filter(|pi| **pi > 0.0)
.map(|pi| -pi * pi.log2())
.sum()
}
pub fn renyi_entropy(p: &[f64], alpha: f64) -> Result<f64, EntropyError> {
if alpha <= 0.0 {
return Err(EntropyError::InvalidRenyiOrder(alpha));
}
if (alpha - 1.0).abs() < 1e-12 {
return Ok(shannon_entropy(p));
}
if alpha == f64::INFINITY {
return Ok(min_entropy(p));
}
let sum: f64 = p.iter().filter(|pi| **pi > 0.0).map(|pi| pi.powf(alpha)).sum();
if sum <= 0.0 {
return Ok(0.0);
}
Ok((1.0 / (1.0 - alpha)) * sum.log2())
}
pub fn min_entropy(p: &[f64]) -> f64 {
let max_p = p.iter().cloned().fold(0.0_f64, f64::max);
if max_p <= 0.0 { 0.0 } else { -max_p.log2() }
}
pub fn tsallis_entropy(p: &[f64], q: f64) -> f64 {
if (q - 1.0).abs() < 1e-12 {
return shannon_entropy(p);
}
let sum: f64 = p.iter().filter(|pi| **pi > 0.0).map(|pi| pi.powf(q)).sum();
(1.0 / (q - 1.0)) * (1.0 - sum)
}
pub fn kl_divergence(p: &[f64], q: &[f64]) -> f64 {
p.iter()
.zip(q.iter())
.filter(|(pi, _)| **pi > 0.0)
.map(|(pi, qi)| {
let qi_safe = if *qi > 0.0 { *qi } else { 1e-300 };
pi * (pi / qi_safe).log2()
})
.sum()
}
pub fn cross_entropy(p: &[f64], q: &[f64]) -> f64 {
p.iter()
.zip(q.iter())
.filter(|(pi, _)| **pi > 0.0)
.map(|(pi, qi)| {
let qi_safe = if *qi > 0.0 { *qi } else { 1e-300 };
-pi * qi_safe.log2()
})
.sum()
}
pub fn js_divergence(p: &[f64], q: &[f64]) -> f64 {
let m: Vec<f64> = p.iter().zip(q.iter()).map(|(a, b)| 0.5 * (a + b)).collect();
0.5 * kl_divergence(p, &m) + 0.5 * kl_divergence(q, &m)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn entropy_non_negative_uniform() {
let p = vec![0.25, 0.25, 0.25, 0.25];
let h = shannon_entropy(&p);
assert!(h >= 0.0, "Shannon entropy must be non-negative");
}
#[test]
fn entropy_non_negative_degenerate() {
let p = vec![1.0, 0.0, 0.0];
let h = shannon_entropy(&p);
assert!(h >= 0.0);
assert!(h < 1e-12, "deterministic distribution has ~0 entropy");
}
#[test]
fn uniform_is_maximum_entropy() {
let p = vec![0.25; 4];
let h = shannon_entropy(&p);
assert!((h - 2.0).abs() < 1e-12, "4 equiprobable paths → 2 bits");
}
#[test]
fn verification_entropy_from_counts() {
let ve = VerificationEntropy::from_counts(&[10, 20, 30, 40]).unwrap();
assert!(ve.shannon >= 0.0);
assert!(ve.shannon < 2.0, "non-uniform should be < max");
}
#[test]
fn renyi_converges_to_shannon_as_alpha_1() {
let p = vec![0.1, 0.2, 0.3, 0.4];
let shannon = shannon_entropy(&p);
let renyi_near1 = renyi_entropy(&p, 1.0 + 1e-10).unwrap();
assert!(
(shannon - renyi_near1).abs() < 1e-6,
"Rényi(α→1) should ≈ Shannon: shannon={shannon}, renyi={renyi_near1}"
);
}
#[test]
fn renyi_monotone_decreasing_in_alpha() {
let p = vec![0.1, 0.2, 0.3, 0.4];
let h05 = renyi_entropy(&p, 0.5).unwrap();
let h1 = shannon_entropy(&p);
let h2 = renyi_entropy(&p, 2.0).unwrap();
let h3 = renyi_entropy(&p, 3.0).unwrap();
assert!(h05 >= h1 - 1e-12, "Rényi(0.5) ≥ Shannon");
assert!(h1 >= h2 - 1e-12, "Shannon ≥ Rényi(2)");
assert!(h2 >= h3 - 1e-12, "Rényi(2) ≥ Rényi(3)");
}
#[test]
fn tsallis_non_negative() {
let p = vec![0.3, 0.7];
let t = tsallis_entropy(&p, 2.0);
assert!(t >= 0.0, "Tsallis entropy must be non-negative for q>1");
}
#[test]
fn kl_divergence_non_negative() {
let p = vec![0.3, 0.7];
let q = vec![0.5, 0.5];
let d = kl_divergence(&p, &q);
assert!(d >= -1e-12, "KL divergence is non-negative");
}
#[test]
fn kl_divergence_zero_for_same() {
let p = vec![0.3, 0.7];
assert!((kl_divergence(&p, &p)).abs() < 1e-12);
}
#[test]
fn js_divergence_bounded() {
let p = vec![1.0, 0.0];
let q = vec![0.0, 1.0];
let js = js_divergence(&p, &q);
assert!(js > 0.0);
assert!(js <= 1.0, "JS divergence ≤ 1 bit");
}
#[test]
fn min_entropy_leq_shannon() {
let p = vec![0.1, 0.2, 0.3, 0.4];
let h_min = min_entropy(&p);
let h_shannon = shannon_entropy(&p);
assert!(h_min <= h_shannon + 1e-12);
}
#[test]
fn empty_vector_errors() {
assert!(VerificationEntropy::from_probabilities(&[]).is_err());
}
#[test]
fn negative_probability_errors() {
assert!(VerificationEntropy::from_probabilities(&[-0.1, 0.5]).is_err());
}
}