Skip to main content

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