smooth_buffer/
lib.rs

1#![warn(clippy::std_instead_of_core, clippy::std_instead_of_alloc)]
2#![no_std]
3use core::slice::Iter;
4
5mod num;
6pub use num::{Num, Float};
7
8/// Simple fixed size ringbuffer with fast averaging.
9pub struct SmoothBuffer<const CAP: usize, T: Num> {
10    data: [T; CAP],
11    head: usize,
12    sum: Option<T>,
13    max: Option<T>,
14    min: Option<T>,
15    filled_len: usize,
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        SmoothBuffer {
28            data: [T::default(); CAP],
29            head: 0,
30            sum: None,
31            max: None,
32            min: None,
33            filled_len: 0,
34        }
35    }
36
37    /// Creates a new buffer pre-populated with a value, filled to capacity.
38    pub fn pre_filled(value: T) -> Self {
39        SmoothBuffer {
40            data: [value; CAP],
41            head: CAP - 1,
42            sum: Some(value * T::from_usize(CAP)),
43            max: Some(value),
44            min: Some(value),
45            filled_len: CAP,
46        }
47    }
48
49    /// Fast! Sum is always kept up to date on push. No need to iterate.
50    pub fn average(&self) -> T {
51        if self.filled_len > 0 {
52            return self.sum.unwrap_or(T::zero()) / T::from_usize(self.filled_len);
53        }
54        T::zero()
55    }
56
57    /// Resets buffer to its default empty state.
58    pub fn clear(&mut self) {
59        for n in 0..self.data.len() {
60            self.data[n] = T::zero();
61        }
62        self.sum = None;
63        self.max = None;
64        self.min = None;
65        self.filled_len = 0;
66        self.head = 0;
67    }
68
69    /// True is buffer is empty.
70    pub fn is_empty(&self) -> bool {
71        self.filled_len == 0
72    }
73
74    /// The largest value so far, if any.
75    pub fn max(&self) -> T {
76        self.max.unwrap_or(T::zero())
77    }
78
79    /// The smallest value so far, if any.
80    pub fn min(&self) -> T {
81        self.min.unwrap_or(T::zero())
82    }
83
84    /// The maximum number of items. Older items are discarded in favor of newer ones
85    /// if capacity is exceeded.
86    pub fn capacity(&self) -> usize {
87        CAP
88    }
89
90    /// Current value count, will always be lower or equal to capacity.
91    pub fn len(&self) -> usize {
92        self.filled_len
93    }
94
95    /// Push a single value.
96    pub fn push(&mut self, value: T) {
97        match self.max {
98            None => self.max = Some(value),
99            Some(max) => self.max = Some(T::get_max(max, value)),
100        }
101        match self.min {
102            None => self.min = Some(value),
103            Some(min) => self.min = Some(T::get_min(min, value)),
104        }
105        match self.sum {
106            None => self.sum = Some(value),
107            Some(sum) => self.sum = Some(sum - self.data[self.head] + value),
108        }
109
110        // Push data into storage
111        self.data[self.head] = value;
112        self.head += 1;
113        if self.head == CAP {
114            self.head = 0
115        }
116        if self.filled_len < CAP {
117            self.filled_len += 1;
118        }
119    }
120
121    /// Pushes multiple values at once.
122    pub fn push_slice(&mut self, slice: &[T]) {
123        for item in slice {
124            self.push(*item);
125        }
126    }
127
128    /// Iterates through all values. Order of retrieval will likely NOT match order of input.
129    pub fn iter(&self) -> Iter<T> {
130        self.data[0..self.filled_len].iter()
131    }
132}
133
134impl<const CAP: usize, T: Float> SmoothBuffer<CAP, T> {
135    /// Gaussian smoothing. Much slower than a simple average, will actually
136    /// iterate through all values and return a weighted sum.
137    pub fn gaussian_filter(&self) -> T {
138        if CAP == 0 {
139            return T::zero();
140        }
141
142        // Standard deviation for the Gaussian kernel
143        let sigma = T::from_usize(CAP) / T::four();
144        let mut weights = [T::zero(); CAP];
145
146        // Calculate Gaussian weights
147        let mut total_weight = T::zero();
148        let center = T::from_usize(CAP - 1) / T::two();
149        for i in 0..CAP {
150            let distance = T::from_usize(i) - center;
151            let weight = T::exp(-distance * distance / (T::two() * sigma * sigma));
152            weights[i] = weight;
153            total_weight += weight;
154        }
155
156        // Normalize weights
157        for weight in weights.iter_mut() {
158            *weight /= total_weight;
159        }
160
161        // Compute the weighted sum
162        let mut sum = T::zero();
163        self.data.iter()
164            .zip(weights.iter())
165            .for_each(|(value, weight)| sum += *value * *weight);
166        sum
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173    const MARGIN: f64 = 0.000001;
174
175    #[test]
176    fn create_and_push() {
177        const CAP: usize = 10;
178        let mut buf = SmoothBuffer::<CAP, f32>::new();
179        for _ in 0..5 {
180            buf.push(10.0);
181        }
182
183        assert_eq!(buf.capacity(), CAP);
184        assert_eq!(buf.len(), 5);
185        assert_eq!(buf.average(), 10.0);
186
187        for _ in 0..10 {
188            buf.push(5.0);
189        }
190        assert_eq!(buf.len(), CAP);
191        assert_eq!(buf.average(), 5.0);
192    }
193
194    #[test]
195    fn clearing() {
196        let mut buf = SmoothBuffer::<10, f32>::new();
197        for n in 0..buf.capacity() {
198            buf.push(n as f32);
199        }
200        buf.clear();
201        assert_eq!(buf.capacity(), 10);
202        assert_eq!(buf.len(), 0);
203        assert_eq!(buf.average(), 0.0);
204        assert_eq!(buf.iter().next(), None);
205    }
206
207    #[test]
208    fn iteration() {
209        let mut buf = SmoothBuffer::<10, f64>::new();
210        let len = 7;
211        for n in 0..len {
212            buf.push(n as f64);
213        }
214
215        for (i, value) in buf.iter().enumerate() {
216            assert_eq!(i as f64, *value);
217        }
218
219        assert!(buf.iter().len() == len);
220    }
221
222    #[test]
223    fn gaussian_smoothing_simple() {
224        let mut buf = SmoothBuffer::<10, f64>::new();
225        for _ in 0..100 {
226            buf.push(3.0);
227        }
228        // Smoothed value won't be exactly the same! Will be correct to a few decimal places though
229        // println!("{}", buf.gaussian_filter(0.5));
230        assert!(buf.gaussian_filter() - 3.0 < MARGIN);
231    }
232
233    #[test]
234    fn gaussian_smoothing_with_negative_values() {
235        let mut buf = SmoothBuffer::<10, f64>::new();
236        let mid = buf.len() / 2;
237        for v in 0..buf.len() {
238            buf.push(if v < mid {
239                1.0
240            } else if v > mid {
241                -1.0
242            } else {
243                0.0
244            });
245        }
246        // println!("{}", buf.gaussian_filter());
247        assert!(buf.gaussian_filter().abs() < MARGIN);
248    }
249
250    #[test]
251    fn gaussian_smoothing_slice() {
252        let mut buf = SmoothBuffer::<10, f64>::new();
253        buf.push_slice(&[0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]);
254        assert!(buf.gaussian_filter() - 0.55 < MARGIN);
255    }
256
257    #[test]
258    fn pre_filled_buffer() {
259        fn test_value(x:f64){
260            // println!("testing {}", x);
261            let buf = SmoothBuffer::<10, f64>::pre_filled(x);
262            assert!(buf.len() == 10);
263            assert!((buf.gaussian_filter() - x).abs() < MARGIN);
264            assert!((buf.average() - x).abs() < MARGIN);
265        }
266
267        for n in 0 ..= 10 {
268            test_value(n as f64 / 10.0);
269        }
270    }
271
272    #[test]
273    fn progressive_fill() {
274        let mut buf = SmoothBuffer::<10, f64>::pre_filled(0.0);
275        // println!("{}", buf.gaussian_filter());
276        assert!(buf.gaussian_filter() < MARGIN);
277        for _n in 0..10 {
278            buf.push(1.0);
279            // println!("{:.2}", buf.gaussian_filter());
280        }
281        assert!(buf.gaussian_filter() - 1.0 < MARGIN);
282    }
283}