qualia_core_db/solvers/statistics/
information.rs1pub fn entropy(p: &[f64]) -> Option<f64> {
13 if p.is_empty() {
14 return None;
15 }
16 let total: f64 = p.iter().sum();
17 if !(total > 0.0) {
18 return None;
19 }
20 let mut h = 0.0;
21 for &pi in p {
22 let q = pi / total; if q > 0.0 {
24 h -= q * q.log2();
25 }
26 }
27 Some(h)
28}
29
30pub fn entropy_from_counts(counts: &[usize]) -> Option<f64> {
32 if counts.is_empty() {
33 return None;
34 }
35 let p: Vec<f64> = counts.iter().map(|&c| c as f64).collect();
36 entropy(&p)
37}
38
39pub fn kl_divergence(p: &[f64], q: &[f64]) -> Option<f64> {
43 if p.is_empty() || p.len() != q.len() {
44 return None;
45 }
46 let (sp, sq): (f64, f64) = (p.iter().sum(), q.iter().sum());
47 if !(sp > 0.0) || !(sq > 0.0) {
48 return None;
49 }
50 let mut d = 0.0;
51 for (&pi, &qi) in p.iter().zip(q) {
52 let pn = pi / sp;
53 let qn = qi / sq;
54 if pn > 0.0 {
55 if qn <= 0.0 {
56 return None; }
58 d += pn * (pn / qn).log2();
59 }
60 }
61 Some(d)
62}
63
64pub fn cross_entropy(p: &[f64], q: &[f64]) -> Option<f64> {
66 Some(entropy(p)? + kl_divergence(p, q)?)
67}
68
69pub fn mutual_information_discrete(x: &[usize], y: &[usize]) -> Option<f64> {
73 let n = x.len();
74 if n == 0 || n != y.len() {
75 return None;
76 }
77 let nx = x.iter().max().copied().unwrap_or(0) + 1;
78 let ny = y.iter().max().copied().unwrap_or(0) + 1;
79 let mut joint = vec![0.0f64; nx * ny];
80 let mut px = vec![0.0f64; nx];
81 let mut py = vec![0.0f64; ny];
82 for (&xi, &yi) in x.iter().zip(y) {
83 joint[xi * ny + yi] += 1.0;
84 px[xi] += 1.0;
85 py[yi] += 1.0;
86 }
87 let nf = n as f64;
88 let mut mi = 0.0;
89 for xi in 0..nx {
90 for yi in 0..ny {
91 let pxy = joint[xi * ny + yi] / nf;
92 if pxy > 0.0 {
93 let pxi = px[xi] / nf;
94 let pyi = py[yi] / nf;
95 mi += pxy * (pxy / (pxi * pyi)).log2();
96 }
97 }
98 }
99 Some(mi.max(0.0))
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 const EPS: f64 = 1e-9;
106
107 #[test]
108 fn entropy_known_values() {
109 assert!((entropy(&[0.5, 0.5]).unwrap() - 1.0).abs() < EPS);
111 assert!((entropy(&[0.25; 4]).unwrap() - 2.0).abs() < EPS);
112 assert!(entropy(&[1.0, 0.0, 0.0]).unwrap().abs() < EPS);
113 assert!((entropy_from_counts(&[1, 1, 1, 1]).unwrap() - 2.0).abs() < EPS);
115 }
116
117 #[test]
118 fn kl_is_zero_for_equal_and_positive_otherwise() {
119 assert!(kl_divergence(&[0.5, 0.5], &[0.5, 0.5]).unwrap().abs() < EPS);
120 assert!(kl_divergence(&[0.9, 0.1], &[0.5, 0.5]).unwrap() > 0.0);
121 assert!(kl_divergence(&[0.5, 0.5], &[1.0, 0.0]).is_none());
123 }
124
125 #[test]
126 fn mutual_information_detects_dependence() {
127 let x = [0usize, 0, 1, 1, 0, 1, 0, 1];
129 let y = x;
130 assert!((mutual_information_discrete(&x, &y).unwrap() - 1.0).abs() < EPS);
131 let xi = [0usize, 0, 1, 1, 0, 0, 1, 1];
133 let yi = [0usize, 1, 0, 1, 0, 1, 0, 1];
134 assert!(mutual_information_discrete(&xi, &yi).unwrap() < 1e-9);
135 }
136
137 #[test]
138 fn cross_entropy_decomposes() {
139 let p = [0.7, 0.3];
141 let q = [0.5, 0.5];
142 let ce = cross_entropy(&p, &q).unwrap();
143 let h = entropy(&p).unwrap();
144 let d = kl_divergence(&p, &q).unwrap();
145 assert!((ce - (h + d)).abs() < EPS);
146 }
147
148 #[test]
149 fn guards() {
150 assert_eq!(entropy(&[]), None);
151 assert_eq!(kl_divergence(&[0.5, 0.5], &[1.0]), None);
152 assert_eq!(mutual_information_discrete(&[0, 1], &[0]), None);
153 }
154}