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