libspot_rs/
peaks.rs

1//! Peaks structure for computing statistics over peak data
2//!
3//! This module implements the Peaks structure that computes statistics
4//! about peaks data using an underlying Ubend circular buffer.
5
6use crate::error::SpotResult;
7
8use crate::ubend::Ubend;
9
10/// Structure that computes stats about the peaks
11#[derive(Debug, Clone)]
12pub struct Peaks {
13    /// Sum of the elements
14    e: f64,
15    /// Sum of the square of the elements
16    e2: f64,
17    /// Minimum of the elements
18    min: f64,
19    /// Maximum of the elements
20    max: f64,
21    /// Underlying data container
22    container: Ubend,
23}
24
25impl Peaks {
26    /// Initialize a new Peaks structure with the given size
27    pub fn new(size: usize) -> SpotResult<Self> {
28        Ok(Self {
29            e: 0.0,
30            e2: 0.0,
31            min: f64::NAN,
32            max: f64::NAN,
33            container: Ubend::new(size)?,
34        })
35    }
36
37    /// Get the current size of the peaks container
38    pub fn size(&self) -> usize {
39        self.container.size()
40    }
41
42    /// Add a new data point into the peaks
43    pub fn push(&mut self, x: f64) {
44        let erased = self.container.push(x);
45        let size = self.size();
46
47        // Increment the stats
48        self.e += x;
49        self.e2 += x * x;
50
51        // First we update the stats with the value of x
52        if size == 1 || x < self.min {
53            self.min = x;
54        }
55        if size == 1 || x > self.max {
56            self.max = x;
57        }
58
59        // Then we treat the case where a data has been erased
60        // In this case we must update the accumulators and possibly update the min/max
61        if !erased.is_nan() {
62            self.e -= erased;
63            self.e2 -= erased * erased;
64            if (erased <= self.min) || (erased >= self.max) {
65                // Here we have to loop in the container to ensure having
66                // the right stats (in particular min and max). However, we
67                // also update e and e2 (the in/decrements may create precision errors)
68                self.update_stats();
69            }
70        }
71    }
72
73    /// Compute the mean of the elements
74    pub fn mean(&self) -> f64 {
75        let size = self.size();
76        if size == 0 {
77            f64::NAN
78        } else {
79            self.e / (size as f64)
80        }
81    }
82
83    /// Compute the variance of the elements
84    pub fn variance(&self) -> f64 {
85        let size = self.size();
86        if size == 0 {
87            f64::NAN
88        } else {
89            let size_f = size as f64;
90            let mean = self.e / size_f;
91            (self.e2 / size_f) - (mean * mean)
92        }
93    }
94
95    /// Get the minimum value
96    pub fn min(&self) -> f64 {
97        self.min
98    }
99
100    /// Get the maximum value
101    pub fn max(&self) -> f64 {
102        self.max
103    }
104
105    /// Get the sum of elements
106    pub fn sum(&self) -> f64 {
107        self.e
108    }
109
110    /// Get the sum of squares
111    pub fn sum_squares(&self) -> f64 {
112        self.e2
113    }
114
115    /// Get access to the underlying container
116    pub fn container(&self) -> &Ubend {
117        &self.container
118    }
119
120    /// Update all statistics by iterating through the container
121    /// This is called when we need to recompute min/max after an erasure
122    fn update_stats(&mut self) {
123        // Reset min and max
124        self.min = f64::NAN;
125        self.max = f64::NAN;
126        // Reset accumulators
127        self.e = 0.0;
128        self.e2 = 0.0;
129
130        let max_iteration = self.container.size();
131
132        for i in 0..max_iteration {
133            // Direct access to container data (matches C implementation)
134            let value = self.container.raw_data()[i];
135            self.e += value;
136            self.e2 += value * value;
137
138            if self.min.is_nan() || (value < self.min) {
139                self.min = value;
140            }
141            if self.max.is_nan() || (value > self.max) {
142                self.max = value;
143            }
144        }
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151    use crate::error::SpotError;
152    use approx::assert_relative_eq;
153
154    #[test]
155    fn test_peaks_creation() {
156        let peaks = Peaks::new(5).unwrap();
157        assert_eq!(peaks.size(), 0);
158        assert_relative_eq!(peaks.sum(), 0.0);
159        assert_relative_eq!(peaks.sum_squares(), 0.0);
160        assert!(peaks.min().is_nan());
161        assert!(peaks.max().is_nan());
162        assert!(peaks.mean().is_nan());
163        assert!(peaks.variance().is_nan());
164    }
165
166    #[test]
167    fn test_peaks_zero_size() {
168        let result = Peaks::new(0);
169        assert!(result.is_err());
170        assert_eq!(result.unwrap_err(), SpotError::MemoryAllocationFailed);
171    }
172
173    #[test]
174    fn test_peaks_single_element() {
175        let mut peaks = Peaks::new(3).unwrap();
176
177        peaks.push(5.0);
178        assert_eq!(peaks.size(), 1);
179        assert_relative_eq!(peaks.sum(), 5.0);
180        assert_relative_eq!(peaks.sum_squares(), 25.0);
181        assert_relative_eq!(peaks.min(), 5.0);
182        assert_relative_eq!(peaks.max(), 5.0);
183        assert_relative_eq!(peaks.mean(), 5.0);
184        assert_relative_eq!(peaks.variance(), 0.0);
185    }
186
187    #[test]
188    fn test_peaks_multiple_elements() {
189        let mut peaks = Peaks::new(5).unwrap();
190
191        peaks.push(1.0);
192        peaks.push(2.0);
193        peaks.push(3.0);
194
195        assert_eq!(peaks.size(), 3);
196        assert_relative_eq!(peaks.sum(), 6.0);
197        assert_relative_eq!(peaks.sum_squares(), 14.0);
198        assert_relative_eq!(peaks.min(), 1.0);
199        assert_relative_eq!(peaks.max(), 3.0);
200        assert_relative_eq!(peaks.mean(), 2.0);
201
202        // Variance = E[X²] - (E[X])² = 14/3 - 4 = 14/3 - 12/3 = 2/3
203        assert_relative_eq!(peaks.variance(), 2.0 / 3.0, epsilon = 1e-14);
204    }
205
206    #[test]
207    fn test_peaks_overflow_and_min_max_update() {
208        let mut peaks = Peaks::new(3).unwrap();
209
210        // Fill with 1, 2, 3
211        peaks.push(1.0); // min=1, max=1
212        peaks.push(2.0); // min=1, max=2
213        peaks.push(3.0); // min=1, max=3
214
215        assert_relative_eq!(peaks.min(), 1.0);
216        assert_relative_eq!(peaks.max(), 3.0);
217
218        // Add 0.5, which should erase 1.0 and become new minimum
219        peaks.push(0.5); // should erase 1.0, so we have [2, 3, 0.5]
220
221        assert_eq!(peaks.size(), 3);
222        assert_relative_eq!(peaks.min(), 0.5);
223        assert_relative_eq!(peaks.max(), 3.0);
224        assert_relative_eq!(peaks.sum(), 5.5);
225
226        // Add 4.0, which should erase 2.0 and become new maximum
227        peaks.push(4.0); // should erase 2.0, so we have [3, 0.5, 4.0]
228
229        assert_relative_eq!(peaks.min(), 0.5);
230        assert_relative_eq!(peaks.max(), 4.0);
231        assert_relative_eq!(peaks.sum(), 7.5);
232    }
233
234    #[test]
235    fn test_peaks_stats_after_min_erasure() {
236        let mut peaks = Peaks::new(3).unwrap();
237
238        // Add values where the minimum will be erased
239        peaks.push(2.0);
240        peaks.push(1.0); // This is the minimum
241        peaks.push(3.0);
242
243        assert_relative_eq!(peaks.min(), 1.0);
244        assert_relative_eq!(peaks.max(), 3.0);
245
246        // Add a value that will force erasure of the minimum
247        peaks.push(2.5); // This should erase 2.0, leaving [1, 3, 2.5]
248
249        assert_relative_eq!(peaks.min(), 1.0); // Still 1.0
250        assert_relative_eq!(peaks.max(), 3.0); // Still 3.0
251
252        // Add another value that will erase the minimum
253        peaks.push(2.7); // This should erase 1.0, leaving [3, 2.5, 2.7]
254
255        // Now the minimum should be recalculated
256        assert_relative_eq!(peaks.min(), 2.5);
257        assert_relative_eq!(peaks.max(), 3.0);
258    }
259
260    #[test]
261    fn test_peaks_stats_after_max_erasure() {
262        let mut peaks = Peaks::new(3).unwrap();
263
264        // Add values where the maximum will be erased
265        peaks.push(1.0);
266        peaks.push(3.0); // This is the maximum
267        peaks.push(2.0);
268
269        assert_relative_eq!(peaks.min(), 1.0);
270        assert_relative_eq!(peaks.max(), 3.0);
271
272        // Add values that will eventually force erasure of the maximum
273        peaks.push(1.5); // This should erase 1.0, leaving [3, 2, 1.5]
274        peaks.push(1.7); // This should erase 3.0, leaving [2, 1.5, 1.7]
275
276        // Now the maximum should be recalculated
277        assert_relative_eq!(peaks.min(), 1.5);
278        assert_relative_eq!(peaks.max(), 2.0);
279    }
280}