#[must_use]
pub fn shannon(probs: &[f64]) -> f64 {
if probs.len() < 2 {
return 0.0;
}
let mut h = 0.0;
for &p in probs {
if p > 0.0 && p <= 1.0 && p.is_finite() {
h -= p * p.log2();
}
}
if h.is_finite() { h.max(0.0) } else { 0.0 }
}
#[must_use]
pub fn binary_shannon(p: f64) -> f64 {
if !(0.0..=1.0).contains(&p) || !p.is_finite() {
return 0.0;
}
if p == 0.0 || p == 1.0 {
return 0.0;
}
let q = 1.0 - p;
-(p * p.log2() + q * q.log2())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn peaks_at_one_bit_when_p_is_half() {
assert_eq!(binary_shannon(0.5), 1.0);
}
#[test]
fn zero_at_endpoints() {
assert_eq!(binary_shannon(0.0), 0.0);
assert_eq!(binary_shannon(1.0), 0.0);
}
#[test]
fn symmetric_around_half() {
for &p in &[0.1, 0.2, 0.3, 0.4, 0.45, 0.49] {
let lhs = binary_shannon(p);
let rhs = binary_shannon(1.0 - p);
assert!(
(lhs - rhs).abs() < 1e-12,
"asymmetric at p={p}: H(p)={lhs} H(1-p)={rhs}"
);
}
}
#[test]
fn monotonically_decreases_from_half_to_endpoints() {
let mut prev = binary_shannon(0.5);
for step in 1..=10 {
let p = 0.5 + step as f64 * 0.05;
let h = binary_shannon(p);
assert!(h <= prev, "non-monotone descent at p={p}: {h} > {prev}");
prev = h;
}
}
#[test]
fn out_of_range_returns_zero() {
assert_eq!(binary_shannon(-0.1), 0.0);
assert_eq!(binary_shannon(1.1), 0.0);
assert_eq!(binary_shannon(-1.0), 0.0);
assert_eq!(binary_shannon(2.0), 0.0);
}
#[test]
fn nan_and_infinity_return_zero() {
assert_eq!(binary_shannon(f64::NAN), 0.0);
assert_eq!(binary_shannon(f64::INFINITY), 0.0);
assert_eq!(binary_shannon(f64::NEG_INFINITY), 0.0);
}
#[test]
fn never_returns_negative_or_above_one() {
for step in 0..=1000 {
let p = step as f64 / 1000.0;
let h = binary_shannon(p);
assert!((0.0..=1.0).contains(&h), "H({p})={h} escaped [0,1]");
}
}
#[test]
fn matches_known_table_within_tolerance() {
let table = [
(0.10, 0.4689955935892812),
(0.25, 0.8112781244591328),
(0.40, 0.9709505944546686),
(0.50, 1.0000000000000000),
(0.60, 0.9709505944546686),
(0.75, 0.8112781244591328),
(0.90, 0.4689955935892812),
];
for (p, expected) in table {
let got = binary_shannon(p);
assert!(
(got - expected).abs() < 1e-9,
"H({p})={got} expected={expected}"
);
}
}
#[test]
fn near_endpoints_does_not_underflow_or_explode() {
let near_zero = binary_shannon(1e-12);
assert!(near_zero.is_finite() && (0.0..=1.0).contains(&near_zero));
let near_one = binary_shannon(1.0 - 1e-12);
assert!(near_one.is_finite() && (0.0..=1.0).contains(&near_one));
}
#[test]
fn shannon_binary_case_matches_binary_shannon() {
for p in [0.0, 0.1, 0.25, 0.5, 0.75, 0.9, 1.0] {
let bin = binary_shannon(p);
let nary = shannon(&[p, 1.0 - p]);
assert!(
(bin - nary).abs() < 1e-12,
"binary_shannon({p}) = {bin}, shannon([p,1-p]) = {nary}"
);
}
}
#[test]
fn shannon_uniform_three_state_is_log2_3() {
let got = shannon(&[1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0]);
let expected = (3f64).log2();
assert!((got - expected).abs() < 1e-9, "got {got}, want {expected}");
}
#[test]
fn shannon_uniform_n_state_is_log2_n() {
for n in 2usize..=8 {
let probs: Vec<f64> = vec![1.0 / n as f64; n];
let got = shannon(&probs);
let expected = (n as f64).log2();
assert!(
(got - expected).abs() < 1e-9,
"n={n}: got {got}, want {expected}"
);
}
}
#[test]
fn shannon_certain_outcome_is_zero() {
assert_eq!(shannon(&[1.0, 0.0, 0.0, 0.0]), 0.0);
assert_eq!(shannon(&[0.0, 1.0]), 0.0);
}
#[test]
fn shannon_empty_slice_is_zero() {
assert_eq!(shannon(&[]), 0.0);
}
#[test]
fn shannon_single_outcome_is_zero() {
assert_eq!(shannon(&[1.0]), 0.0);
assert_eq!(shannon(&[0.7]), 0.0);
}
#[test]
fn shannon_handles_zero_probability_components() {
let got = shannon(&[0.5, 0.5, 0.0, 0.0, 0.0]);
assert_eq!(got, 1.0);
}
#[test]
fn shannon_non_normalised_input_does_not_panic() {
let got = shannon(&[0.3, 0.3, 0.3]);
assert!(got.is_finite());
assert!(got >= 0.0);
}
#[test]
fn shannon_rejects_out_of_range_components_no_negative_entropy() {
let got = shannon(&[2.0, 0.0]);
assert!(got >= 0.0, "negative entropy from p > 1: {got}");
let got2 = shannon(&[1.5, 0.5]);
assert!(got2 >= 0.0, "negative entropy from p > 1: {got2}");
let got3 = shannon(&[0.3, 0.3]);
assert!(got3 > 0.0 && got3.is_finite());
}
#[test]
fn shannon_skips_non_finite_components_without_panicking() {
let got = shannon(&[0.5, 0.5, f64::NAN, f64::INFINITY]);
assert_eq!(got, 1.0);
}
}