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