smooth_buffer/
lib.rs

1#![warn(clippy::std_instead_of_core, clippy::std_instead_of_alloc)]
2#![no_std]
3
4mod num;
5pub use num::{Float, Num};
6
7/// Simple fixed size ringbuffer with fast averaging.
8pub struct SmoothBuffer<const CAP: usize, T: Num> {
9    data: [T; CAP],
10    head: usize,
11    sum: Option<T>,
12    max: Option<T>,
13    min: Option<T>,
14    filled_len: usize,
15    dirty_minmax: bool,
16}
17
18impl<const CAP: usize, T: Num> Default for SmoothBuffer<CAP, T> {
19    fn default() -> Self {
20        Self::new()
21    }
22}
23
24impl<const CAP: usize, T: Num> SmoothBuffer<CAP, T> {
25    /// Creates a new, empty buffer.
26    pub fn new() -> Self {
27        assert!(
28            CAP > 0,
29            "SmoothBuffer Error: Capacity must be larger than zero"
30        );
31        SmoothBuffer {
32            data: [T::default(); CAP],
33            head: 0,
34            sum: None,
35            max: None,
36            min: None,
37            filled_len: 0,
38            dirty_minmax: false,
39        }
40    }
41
42    /// Creates a new buffer pre-populated with a value, filled to capacity.
43    pub fn pre_filled(value: T) -> Self {
44        assert!(
45            CAP > 0,
46            "SmoothBuffer Error: Capacity must be larger than zero"
47        );
48        SmoothBuffer {
49            data: [value; CAP],
50            head: CAP - 1,
51            sum: Some(value * T::from_usize(CAP)),
52            max: Some(value),
53            min: Some(value),
54            filled_len: CAP,
55            dirty_minmax: false,
56        }
57    }
58
59    /// Fast! Sum is always kept up to date on push. No need to iterate.
60    pub fn average(&self) -> T {
61        if self.filled_len > 0 {
62            return self.sum.unwrap_or(T::zero()) / T::from_usize(self.filled_len);
63        }
64        T::zero()
65    }
66
67    /// Resets buffer to its default empty state.
68    pub fn clear(&mut self) {
69        for n in 0..self.data.len() {
70            self.data[n] = T::zero();
71        }
72        self.sum = None;
73        self.max = None;
74        self.min = None;
75        self.filled_len = 0;
76        self.head = 0;
77    }
78
79    /// True is buffer is empty.
80    pub fn is_empty(&self) -> bool {
81        self.filled_len == 0
82    }
83
84    /// The largest value so far, if any.
85    pub fn max(&mut self) -> T {
86        if self.dirty_minmax {
87            // Recalculate max only when needed
88            self.recalculate_minmax();
89        }
90        self.max.unwrap_or(T::zero())
91    }
92
93    /// The smallest value so far, if any.
94    pub fn min(&mut self) -> T {
95        if self.dirty_minmax {
96            // Recalculate min only when needed
97            self.recalculate_minmax();
98        }
99        self.min.unwrap_or(T::zero())
100    }
101
102    fn recalculate_minmax(&mut self) {
103        if self.filled_len == 0 {
104            self.min = None;
105            self.max = None;
106            return;
107        }
108
109        // First find the index of the oldest element
110        let start_idx = if self.filled_len < CAP {
111            0
112        } else {
113            (self.head + CAP - self.filled_len) % CAP
114        };
115
116        // Initialize with the first valid element
117        let mut new_min = self.data[start_idx];
118        let mut new_max = self.data[start_idx];
119
120        // Check all valid elements
121        for i in 1..self.filled_len {
122            let idx = (start_idx + i) % CAP;
123            new_min = T::get_min(new_min, self.data[idx]);
124            new_max = T::get_max(new_max, self.data[idx]);
125        }
126
127        self.min = Some(new_min);
128        self.max = Some(new_max);
129        self.dirty_minmax = false;
130    }
131
132    /// The maximum number of items. Older items are discarded in favor of newer ones
133    /// if capacity is exceeded.
134    pub fn capacity(&self) -> usize {
135        CAP
136    }
137
138    /// Current value count, will always be lower or equal to capacity.
139    pub fn len(&self) -> usize {
140        self.filled_len
141    }
142
143    /// Push a single value.
144    pub fn push(&mut self, value: T) {
145        // Fast path for sum calculation
146        if self.filled_len < CAP {
147            self.sum = match self.sum {
148                None => Some(value),
149                Some(sum) => Some(sum + value),
150            };
151        } else {
152            // Buffer is full, subtract old value and add new
153            self.sum = Some(self.sum.unwrap_or(T::zero()) - self.data[self.head] + value);
154        }
155
156        // Push data into storage
157        self.data[self.head] = value;
158        self.head = (self.head + 1) % CAP; // More efficient than if check
159        if self.filled_len < CAP {
160            self.filled_len += 1;
161        }
162
163        // Update min/max lazily
164        if self.filled_len == CAP {
165            // Only mark dirty if we're overwriting, not when adding
166            self.dirty_minmax = true;
167        } else {
168            match self.max {
169                None => self.max = Some(value),
170                Some(max) if value > max => self.max = Some(value),
171                _ => {}
172            }
173
174            match self.min {
175                None => self.min = Some(value),
176                Some(min) if value < min => self.min = Some(value),
177                _ => {}
178            }
179        }
180    }
181
182    /// Pushes multiple values at once.
183    pub fn push_slice(&mut self, slice: &[T]) {
184        for item in slice {
185            self.push(*item);
186        }
187    }
188
189    /// Iterates through all values. Order of retrieval will likely NOT match order of input.
190    pub fn iter(&self) -> impl Iterator<Item = &T> {
191        let head = self.head;
192        let len = self.filled_len;
193        let cap = CAP;
194
195        (0..len).map(move |i| &self.data[(head + cap - len + i) % cap])
196    }
197}
198
199impl<const CAP: usize, T: Float> SmoothBuffer<CAP, T> {
200    /// Gaussian smoothing. Much slower than a simple average, will actually
201    /// iterate through all values and return a weighted sum.
202    pub fn gaussian_filter(&self) -> T {
203        if self.filled_len == 0 {
204            return T::zero();
205        }
206
207        // Standard deviation for the Gaussian kernel
208        let sigma = T::from_usize(self.filled_len) / T::four();
209        let center = T::from_usize(self.filled_len - 1) / T::two();
210
211        // First pass: calculate all weights and their sum
212        let mut weights = [T::zero(); CAP]; // Using fixed array instead of Vec
213        let mut total_weight = T::zero();
214
215        for i in 0..self.filled_len {
216            let distance = T::from_usize(i) - center;
217            let exp_term = -distance * distance / (T::two() * sigma * sigma);
218            let weight = T::exp(exp_term);
219            weights[i] = weight;
220            total_weight += weight;
221        }
222
223        // Second pass: apply normalized weights to values
224        let mut sum = T::zero();
225        for i in 0..self.filled_len {
226            // Calculate the actual index considering the circular buffer
227            let idx = if self.filled_len < CAP {
228                i
229            } else {
230                (self.head + CAP - self.filled_len + i) % CAP
231            };
232
233            // Use the pre-calculated weight
234            let normalized_weight = weights[i] / total_weight;
235            sum += self.data[idx] * normalized_weight;
236        }
237
238        sum
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245    const MARGIN: f64 = 0.000001;
246
247    #[test]
248    fn create_and_push() {
249        const CAP: usize = 10;
250        let mut buf = SmoothBuffer::<CAP, f32>::new();
251        for _ in 0..5 {
252            buf.push(10.0);
253        }
254
255        assert_eq!(buf.capacity(), CAP);
256        assert_eq!(buf.len(), 5);
257        assert_eq!(buf.average(), 10.0);
258
259        for _ in 0..10 {
260            buf.push(5.0);
261        }
262        assert_eq!(buf.len(), CAP);
263        assert_eq!(buf.average(), 5.0);
264    }
265
266    #[test]
267    fn clearing() {
268        let mut buf = SmoothBuffer::<10, f32>::new();
269        for n in 0..buf.capacity() {
270            buf.push(n as f32);
271        }
272        buf.clear();
273        assert_eq!(buf.capacity(), 10);
274        assert_eq!(buf.len(), 0);
275        assert_eq!(buf.average(), 0.0);
276        assert_eq!(buf.iter().next(), None);
277    }
278
279    #[test]
280    fn iteration() {
281        let mut buf = SmoothBuffer::<10, f64>::new();
282        let len = 7;
283        for n in 0..len {
284            buf.push(n as f64);
285        }
286
287        for (i, value) in buf.iter().enumerate() {
288            assert_eq!(i as f64, *value);
289        }
290    }
291
292    #[test]
293    fn gaussian_smoothing_simple() {
294        let mut buf = SmoothBuffer::<10, f64>::new();
295        for _ in 0..100 {
296            buf.push(3.0);
297        }
298        // Smoothed value won't be exactly the same! Will be correct to a few decimal places though
299        // println!("{}", buf.gaussian_filter(0.5));
300        assert!(buf.gaussian_filter() - 3.0 < MARGIN);
301    }
302
303    #[test]
304    fn gaussian_smoothing_with_negative_values() {
305        let mut buf = SmoothBuffer::<10, f64>::new();
306        let mid = buf.len() / 2;
307        for v in 0..buf.len() {
308            buf.push(if v < mid {
309                1.0
310            } else if v > mid {
311                -1.0
312            } else {
313                0.0
314            });
315        }
316        // println!("{}", buf.gaussian_filter());
317        assert!(buf.gaussian_filter().abs() < MARGIN);
318    }
319
320    #[test]
321    fn gaussian_smoothing_slice() {
322        let mut buf = SmoothBuffer::<10, f64>::new();
323        buf.push_slice(&[0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]);
324        assert!(buf.gaussian_filter() - 0.55 < MARGIN);
325    }
326
327    #[test]
328    fn pre_filled_buffer() {
329        fn test_value(x: f64) {
330            // println!("testing {}", x);
331            let buf = SmoothBuffer::<10, f64>::pre_filled(x);
332            assert!(buf.len() == 10);
333            assert!((buf.gaussian_filter() - x).abs() < MARGIN);
334            assert!((buf.average() - x).abs() < MARGIN);
335        }
336
337        for n in 0..=10 {
338            test_value(n as f64 / 10.0);
339        }
340    }
341
342    #[test]
343    fn progressive_fill() {
344        let mut buf = SmoothBuffer::<10, f64>::pre_filled(0.0);
345        // println!("{}", buf.gaussian_filter());
346        assert!(buf.gaussian_filter() < MARGIN);
347        for _n in 0..10 {
348            buf.push(1.0);
349            // println!("{:.2}", buf.gaussian_filter());
350        }
351        assert!(buf.gaussian_filter() - 1.0 < MARGIN);
352    }
353
354    #[test]
355    fn test_min_max_recalculation() {
356        let mut buf = SmoothBuffer::<5, f64>::new();
357
358        // Fill buffer with increasing values
359        buf.push(1.0);
360        buf.push(2.0);
361        buf.push(3.0);
362        buf.push(4.0);
363        buf.push(5.0);
364
365        // Initial min/max should be correct
366        assert_eq!(buf.min(), 1.0);
367        assert_eq!(buf.max(), 5.0);
368
369        // Now overwrite the min value
370        buf.push(2.5); // This overwrites 1.0
371
372        // Min should be recalculated
373        assert_eq!(buf.min(), 2.0);
374        assert_eq!(buf.max(), 5.0);
375
376        // Now overwrite the max value
377        buf.push(3.5); // This overwrites 2.0
378        buf.push(4.5); // This overwrites 3.0
379        buf.push(3.0); // This overwrites 4.0
380        buf.push(2.0); // This overwrites 5.0 (the max)
381
382        // Max should be recalculated
383        assert_eq!(buf.min(), 2.0);
384        assert_eq!(buf.max(), 4.5);
385    }
386
387    #[test]
388    fn test_buffer_wrapping() {
389        let mut buf = SmoothBuffer::<3, i32>::new();
390
391        // Fill buffer
392        buf.push(1);
393        buf.push(2);
394        buf.push(3);
395
396        // Check initial state
397        assert_eq!(buf.average(), 2);
398
399        // Push more values to wrap around
400        buf.push(4); // Overwrites 1
401        assert_eq!(buf.average(), 3);
402
403        buf.push(5); // Overwrites 2
404        assert_eq!(buf.average(), 4);
405
406        buf.push(6); // Overwrites 3
407        assert_eq!(buf.average(), 5);
408
409        // Check iteration order (should be in insertion order: 4,5,6)
410        let mut collected = [0; 3];
411        let mut count = 0;
412
413        for &val in buf.iter() {
414            if count < 3 {
415                collected[count] = val;
416                count += 1;
417            }
418        }
419
420        assert_eq!(count, 3);
421        assert_eq!(collected, [4, 5, 6]);
422    }
423
424    #[test]
425    fn test_dirty_flag_behavior() {
426        let mut buf = SmoothBuffer::<5, f64>::new();
427
428        // Fill buffer
429        for i in 0..5 {
430            buf.push(i as f64);
431        }
432
433        // First access should use cached values
434        assert_eq!(buf.min(), 0.0);
435        assert_eq!(buf.max(), 4.0);
436
437        // Overwrite min
438        buf.push(2.0); // Overwrites 0.0
439
440        // Min should be recalculated
441        assert_eq!(buf.min(), 1.0);
442
443        // Overwrite multiple values including max
444        buf.push(3.0); // Overwrites 1.0
445        buf.push(2.0); // Overwrites 2.0
446        buf.push(1.0); // Overwrites 3.0
447        buf.push(0.5); // Overwrites 4.0 (max)
448
449        // Max should be recalculated
450        assert_eq!(buf.max(), 3.0);
451    }
452
453    #[test]
454    fn test_pre_filled_edge_cases() {
455        // Test with very large values
456        let buf_large = SmoothBuffer::<5, f64>::pre_filled(1e10);
457        assert!((buf_large.average() - 1e10).abs() < MARGIN);
458
459        // Test with very small values
460        let buf_small = SmoothBuffer::<5, f64>::pre_filled(1e-10);
461        assert!((buf_small.average() - 1e-10).abs() < MARGIN);
462
463        // Test with negative values
464        let mut buf_neg = SmoothBuffer::<5, f64>::pre_filled(-5.0);
465        assert!((buf_neg.average() - (-5.0)).abs() < MARGIN);
466        assert!((buf_neg.min() - (-5.0)).abs() < MARGIN);
467        assert!((buf_neg.max() - (-5.0)).abs() < MARGIN);
468    }
469
470    #[test]
471    fn test_single_element_buffer() {
472        let mut buf = SmoothBuffer::<1, i32>::new();
473
474        // Push and check
475        buf.push(42);
476        assert_eq!(buf.average(), 42);
477        assert_eq!(buf.min(), 42);
478        assert_eq!(buf.max(), 42);
479
480        // Overwrite and check
481        buf.push(17);
482        assert_eq!(buf.average(), 17);
483        assert_eq!(buf.min(), 17);
484        assert_eq!(buf.max(), 17);
485
486        // Check iteration
487        let mut has_value = false;
488
489        for &val in buf.iter() {
490            assert_eq!(val, 17);
491            has_value = true;
492        }
493
494        assert!(has_value);
495    }
496
497    #[test]
498    #[should_panic(expected = "Capacity must be larger than zero")]
499    fn test_zero_capacity() {
500        let _buf = SmoothBuffer::<0, f32>::new();
501        // This should panic with the message in the attribute
502    }
503
504    #[test]
505    fn test_gaussian_filter_with_spikes() {
506        let mut buf = SmoothBuffer::<10, f64>::new();
507
508        // Fill with a baseline value
509        for _ in 0..8 {
510            buf.push(1.0);
511        }
512
513        // Add a spike
514        buf.push(10.0);
515        buf.push(1.0);
516
517        // Gaussian filter should reduce the impact of the spike compared to simple average
518        let avg = buf.average();
519        let gaussian = buf.gaussian_filter();
520
521        // The gaussian value should be closer to 1.0 than the average
522        assert!(gaussian < avg);
523        assert!(gaussian > 1.0);
524
525        // Another test case: values on both extremes
526        let mut buf2 = SmoothBuffer::<5, f64>::new();
527        buf2.push(1.0);
528        buf2.push(10.0);
529        buf2.push(5.0);
530        buf2.push(5.0);
531        buf2.push(1.0);
532
533        // The gaussian filter should be weighted toward the center
534        let gaussian2 = buf2.gaussian_filter();
535        assert!(gaussian2 > 4.0); // average is 4.4
536        assert!(gaussian2 < 5.5); // weighted toward center values
537    }
538}