1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::traits::Indicator;
7
8fn population_stddev(window: &[f64]) -> f64 {
10 let n = window.len() as f64;
11 let mean = window.iter().sum::<f64>() / n;
12 let var = window.iter().map(|&v| (v - mean) * (v - mean)).sum::<f64>() / n;
13 var.max(0.0).sqrt()
14}
15
16fn templates_match(window: &[f64], i: usize, j: usize, len: usize, tol: f64) -> bool {
19 for k in 0..len {
20 if (window[i + k] - window[j + k]).abs() > tol {
21 return false;
22 }
23 }
24 true
25}
26
27#[derive(Debug, Clone)]
65pub struct SampleEntropy {
66 period: usize,
67 emb_dim: usize,
68 r_factor: f64,
69 window: VecDeque<f64>,
70 last: Option<f64>,
71}
72
73impl SampleEntropy {
74 pub fn new(period: usize, m: usize, r_factor: f64) -> Result<Self> {
84 if period == 0 || m == 0 {
85 return Err(Error::PeriodZero);
86 }
87 if period < m + 2 {
88 return Err(Error::InvalidPeriod {
89 message: "sample entropy needs period >= m + 2",
90 });
91 }
92 if !r_factor.is_finite() || r_factor <= 0.0 {
93 return Err(Error::InvalidParameter {
94 message: "sample entropy r_factor must be finite and positive",
95 });
96 }
97 Ok(Self {
98 period,
99 emb_dim: m,
100 r_factor,
101 window: VecDeque::with_capacity(period),
102 last: None,
103 })
104 }
105
106 pub const fn params(&self) -> (usize, usize, f64) {
108 (self.period, self.emb_dim, self.r_factor)
109 }
110
111 pub const fn value(&self) -> Option<f64> {
113 self.last
114 }
115
116 fn compute(&self) -> f64 {
117 let window: Vec<f64> = self.window.iter().copied().collect();
118 let std = population_stddev(&window);
119 if std == 0.0 {
120 return 0.0;
121 }
122 let tol = self.r_factor * std;
123 let m = self.emb_dim;
124 let count = self.period - m;
127 let mut matches_m = 0u64;
128 let mut matches_m1 = 0u64;
129 for i in 0..count {
130 for j in (i + 1)..count {
131 if templates_match(&window, i, j, m, tol) {
132 matches_m += 1;
133 if templates_match(&window, i, j, m + 1, tol) {
134 matches_m1 += 1;
135 }
136 }
137 }
138 }
139 if matches_m == 0 {
140 return 0.0;
141 }
142 if matches_m1 == 0 {
143 return (matches_m as f64).ln();
145 }
146 -((matches_m1 as f64) / (matches_m as f64)).ln()
147 }
148}
149
150impl Indicator for SampleEntropy {
151 type Input = f64;
152 type Output = f64;
153
154 fn update(&mut self, input: f64) -> Option<f64> {
155 if !input.is_finite() {
156 return self.last;
157 }
158 if self.window.len() == self.period {
159 self.window.pop_front();
160 }
161 self.window.push_back(input);
162 if self.window.len() < self.period {
163 return None;
164 }
165 let out = self.compute();
166 self.last = Some(out);
167 Some(out)
168 }
169
170 fn reset(&mut self) {
171 self.window.clear();
172 self.last = None;
173 }
174
175 fn warmup_period(&self) -> usize {
176 self.period
177 }
178
179 fn is_ready(&self) -> bool {
180 self.last.is_some()
181 }
182
183 fn name(&self) -> &'static str {
184 "SampleEntropy"
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191 use crate::traits::BatchExt;
192 use approx::assert_relative_eq;
193
194 #[test]
195 fn rejects_invalid_params() {
196 assert!(matches!(
197 SampleEntropy::new(0, 2, 0.2),
198 Err(Error::PeriodZero)
199 ));
200 assert!(matches!(
201 SampleEntropy::new(50, 0, 0.2),
202 Err(Error::PeriodZero)
203 ));
204 assert!(matches!(
205 SampleEntropy::new(3, 2, 0.2),
206 Err(Error::InvalidPeriod { .. })
207 ));
208 assert!(matches!(
209 SampleEntropy::new(50, 2, 0.0),
210 Err(Error::InvalidParameter { .. })
211 ));
212 }
213
214 #[test]
215 fn accessors_and_metadata() {
216 let s = SampleEntropy::new(50, 2, 0.2).unwrap();
217 assert_eq!(s.params(), (50, 2, 0.2));
218 assert_eq!(s.warmup_period(), 50);
219 assert_eq!(s.name(), "SampleEntropy");
220 assert!(!s.is_ready());
221 assert_eq!(s.value(), None);
222 }
223
224 #[test]
225 fn first_emission_at_warmup_period() {
226 let mut s = SampleEntropy::new(10, 2, 0.2).unwrap();
227 let xs: Vec<f64> = (0..14).map(|i| (f64::from(i) * 0.5).sin()).collect();
228 let out = s.batch(&xs);
229 for v in out.iter().take(9) {
230 assert!(v.is_none());
231 }
232 assert!(out[9].is_some());
233 }
234
235 #[test]
236 fn constant_window_is_zero() {
237 let mut s = SampleEntropy::new(20, 2, 0.2).unwrap();
238 let last = s.batch(&[5.0; 30]).into_iter().flatten().last().unwrap();
239 assert_relative_eq!(last, 0.0, epsilon = 1e-12);
240 }
241
242 #[test]
243 fn output_is_non_negative() {
244 let mut s = SampleEntropy::new(40, 2, 0.2).unwrap();
245 for v in s
246 .batch(
247 &(0..200)
248 .map(|i| (f64::from(i) * 0.3).sin() * 5.0)
249 .collect::<Vec<_>>(),
250 )
251 .into_iter()
252 .flatten()
253 {
254 assert!(v >= 0.0, "sample entropy must be non-negative, got {v}");
255 }
256 }
257
258 #[test]
259 fn regular_below_irregular() {
260 let smooth: Vec<f64> = (0..60).map(|i| (f64::from(i) * 0.2).sin() * 5.0).collect();
264 let mut x = 0.37_f64;
265 let chaotic: Vec<f64> = (0..60)
266 .map(|_| {
267 x = 3.99 * x * (1.0 - x);
268 x * 5.0
269 })
270 .collect();
271 let s_smooth = SampleEntropy::new(50, 2, 0.2)
272 .unwrap()
273 .batch(&smooth)
274 .into_iter()
275 .flatten()
276 .last()
277 .unwrap();
278 let s_chaotic = SampleEntropy::new(50, 2, 0.2)
279 .unwrap()
280 .batch(&chaotic)
281 .into_iter()
282 .flatten()
283 .last()
284 .unwrap();
285 assert!(
286 s_smooth <= s_chaotic,
287 "smooth ({s_smooth}) should be <= chaotic ({s_chaotic})"
288 );
289 }
290
291 #[test]
292 fn ignores_non_finite() {
293 let mut s = SampleEntropy::new(10, 2, 0.2).unwrap();
294 let xs: Vec<f64> = (0..10).map(|i| (f64::from(i) * 0.5).sin()).collect();
295 let ready = s.batch(&xs).into_iter().flatten().last().unwrap();
296 assert_eq!(s.update(f64::NAN), Some(ready));
297 }
298
299 #[test]
300 fn reset_clears_state() {
301 let mut s = SampleEntropy::new(10, 2, 0.2).unwrap();
302 let xs: Vec<f64> = (0..10).map(|i| (f64::from(i) * 0.5).sin()).collect();
303 s.batch(&xs);
304 assert!(s.is_ready());
305 s.reset();
306 assert!(!s.is_ready());
307 assert_eq!(s.value(), None);
308 assert_eq!(s.update(1.0), None);
309 }
310
311 #[test]
312 fn batch_equals_streaming() {
313 let xs: Vec<f64> = (0..120)
314 .map(|i| (f64::from(i) * 0.25).sin() * 9.0)
315 .collect();
316 let batch = SampleEntropy::new(40, 2, 0.2).unwrap().batch(&xs);
317 let mut b = SampleEntropy::new(40, 2, 0.2).unwrap();
318 let streamed: Vec<_> = xs.iter().map(|x| b.update(*x)).collect();
319 assert_eq!(batch, streamed);
320 }
321
322 #[test]
323 fn falls_back_when_no_m_plus_one_matches() {
324 let xs = [1.0, 1.0, 1.0, 5.0];
328 let v = SampleEntropy::new(4, 2, 0.2)
329 .unwrap()
330 .batch(&xs)
331 .into_iter()
332 .flatten()
333 .last()
334 .unwrap();
335 assert!(v.is_finite() && v >= 0.0, "got {v}");
336 }
337}