wickra_core/indicators/
shannon_entropy.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
43pub struct ShannonEntropy {
44 period: usize,
45 bins: usize,
46 window: VecDeque<f64>,
47 last: Option<f64>,
48}
49
50impl ShannonEntropy {
51 pub fn new(period: usize, bins: usize) -> Result<Self> {
59 if period == 0 || bins == 0 {
60 return Err(Error::PeriodZero);
61 }
62 if bins < 2 {
63 return Err(Error::InvalidPeriod {
64 message: "Shannon entropy needs bins >= 2",
65 });
66 }
67 Ok(Self {
68 period,
69 bins,
70 window: VecDeque::with_capacity(period),
71 last: None,
72 })
73 }
74
75 pub const fn params(&self) -> (usize, usize) {
77 (self.period, self.bins)
78 }
79
80 pub const fn value(&self) -> Option<f64> {
82 self.last
83 }
84}
85
86impl Indicator for ShannonEntropy {
87 type Input = f64;
88 type Output = f64;
89
90 fn update(&mut self, input: f64) -> Option<f64> {
91 if !input.is_finite() {
92 return self.last;
93 }
94 if self.window.len() == self.period {
95 self.window.pop_front();
96 }
97 self.window.push_back(input);
98 if self.window.len() < self.period {
99 return None;
100 }
101
102 let mut min = f64::INFINITY;
103 let mut max = f64::NEG_INFINITY;
104 for &v in &self.window {
105 min = min.min(v);
106 max = max.max(v);
107 }
108 if max <= min {
109 self.last = Some(0.0);
111 return Some(0.0);
112 }
113 let width = (max - min) / self.bins as f64;
114 let mut counts = vec![0usize; self.bins];
115 for &v in &self.window {
116 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
120 let raw = ((v - min) / width) as usize;
121 let idx = raw.min(self.bins - 1);
122 counts[idx] += 1;
123 }
124 let n = self.period as f64;
125 let mut h = 0.0;
126 for &count in &counts {
127 if count > 0 {
128 let p = count as f64 / n;
129 h -= p * p.log2();
130 }
131 }
132 self.last = Some(h);
133 Some(h)
134 }
135
136 fn reset(&mut self) {
137 self.window.clear();
138 self.last = None;
139 }
140
141 fn warmup_period(&self) -> usize {
142 self.period
143 }
144
145 fn is_ready(&self) -> bool {
146 self.last.is_some()
147 }
148
149 fn name(&self) -> &'static str {
150 "ShannonEntropy"
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157 use crate::traits::BatchExt;
158 use approx::assert_relative_eq;
159
160 #[test]
161 fn rejects_invalid_params() {
162 assert!(matches!(ShannonEntropy::new(0, 8), Err(Error::PeriodZero)));
163 assert!(matches!(ShannonEntropy::new(32, 0), Err(Error::PeriodZero)));
164 assert!(matches!(
165 ShannonEntropy::new(32, 1),
166 Err(Error::InvalidPeriod { .. })
167 ));
168 }
169
170 #[test]
171 fn accessors_and_metadata() {
172 let e = ShannonEntropy::new(32, 8).unwrap();
173 assert_eq!(e.params(), (32, 8));
174 assert_eq!(e.warmup_period(), 32);
175 assert_eq!(e.name(), "ShannonEntropy");
176 assert!(!e.is_ready());
177 assert_eq!(e.value(), None);
178 }
179
180 #[test]
181 fn first_emission_at_warmup_period() {
182 let mut e = ShannonEntropy::new(4, 4).unwrap();
183 let out = e.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
184 for v in out.iter().take(3) {
185 assert!(v.is_none());
186 }
187 assert!(out[3].is_some());
188 }
189
190 #[test]
191 fn constant_window_is_zero() {
192 let mut e = ShannonEntropy::new(8, 4).unwrap();
193 let last = e.batch(&[5.0; 12]).into_iter().flatten().last().unwrap();
194 assert_relative_eq!(last, 0.0, epsilon = 1e-12);
195 }
196
197 #[test]
198 fn uniform_window_is_max_entropy() {
199 let mut e = ShannonEntropy::new(4, 4).unwrap();
201 let last = e
203 .batch(&[0.0, 1.0, 2.0, 3.0])
204 .into_iter()
205 .flatten()
206 .last()
207 .unwrap();
208 assert_relative_eq!(last, 2.0, epsilon = 1e-9); }
210
211 #[test]
212 fn output_in_range() {
213 let mut e = ShannonEntropy::new(32, 8).unwrap();
214 let max_h = 8f64.log2();
215 for v in e
216 .batch(
217 &(0..200)
218 .map(|i| (f64::from(i) * 0.3).sin() * 10.0)
219 .collect::<Vec<_>>(),
220 )
221 .into_iter()
222 .flatten()
223 {
224 assert!((0.0..=max_h + 1e-9).contains(&v));
225 }
226 }
227
228 #[test]
229 fn ignores_non_finite() {
230 let mut e = ShannonEntropy::new(4, 4).unwrap();
231 let ready = e
232 .batch(&[1.0, 2.0, 3.0, 4.0])
233 .into_iter()
234 .flatten()
235 .last()
236 .unwrap();
237 assert_eq!(e.update(f64::NAN), Some(ready));
238 }
239
240 #[test]
241 fn reset_clears_state() {
242 let mut e = ShannonEntropy::new(4, 4).unwrap();
243 e.batch(&[1.0, 2.0, 3.0, 4.0]);
244 assert!(e.is_ready());
245 e.reset();
246 assert!(!e.is_ready());
247 assert_eq!(e.value(), None);
248 assert_eq!(e.update(1.0), None);
249 }
250
251 #[test]
252 fn batch_equals_streaming() {
253 let xs: Vec<f64> = (0..120)
254 .map(|i| (f64::from(i) * 0.25).sin() * 9.0)
255 .collect();
256 let batch = ShannonEntropy::new(32, 8).unwrap().batch(&xs);
257 let mut b = ShannonEntropy::new(32, 8).unwrap();
258 let streamed: Vec<_> = xs.iter().map(|x| b.update(*x)).collect();
259 assert_eq!(batch, streamed);
260 }
261}