Skip to main content

harmos_storage/signal/waveform/
summary.rs

1/* ***
2 *
3 * Harmos
4 *
5 * SPDX-License-Identifier: Apache-2.0
6 *
7 * ***
8 */
9
10/* === Modules === */
11
12/* === Re-Exports === */
13
14/* === Imports === */
15
16// Standard Library Imports
17
18// Third Party Imports
19
20// Local Imports
21
22/* === Constants === */
23
24/* === Macros === */
25
26/* === Type Aliases === */
27
28/* === Structs === */
29
30// The streaming, mergeable reduction: count, sum, extremes, mean and spread
31// kept in constant space, so partial summaries of disjoint runs merge into the
32// summary of their union. Non-finite values are skipped, the same values
33// `Series` skips, so both answer the shared statistics identically.
34#[derive(Clone, Copy, Debug, PartialEq)]
35pub struct Summary {
36    count: u64,
37    sum: f64,
38    mean: f64,
39    lowest: f64,
40    highest: f64,
41    deviations: f64,
42}
43
44/* === Enums === */
45
46/* === Traits === */
47
48/* === Implementations === */
49
50impl Default for Summary {
51    fn default() -> Self {
52        Self {
53            count: 0,
54            sum: 0.0,
55            mean: 0.0,
56            lowest: f64::INFINITY,
57            highest: f64::NEG_INFINITY,
58            deviations: 0.0,
59        }
60    }
61}
62
63impl Summary {
64    #[must_use]
65    pub fn new() -> Self {
66        Self::default()
67    }
68
69    #[must_use]
70    pub fn of(values: &[f64]) -> Self {
71        let mut summary = Self::new();
72        for value in values {
73            summary.push(*value);
74        }
75        summary
76    }
77
78    pub fn push(&mut self, value: f64) {
79        if !value.is_finite() {
80            return;
81        }
82        let before = self.mean;
83
84        self.count += 1;
85        self.sum += value;
86        self.lowest = self.lowest.min(value);
87        self.highest = self.highest.max(value);
88
89        // The running mean never forms the sum, so it stays finite wherever
90        // the values do; opposite signs are weighted apart so the step itself
91        // cannot overflow.
92        let n = self.count as f64;
93        self.mean = if self.count == 1 {
94            value
95        } else if before.signum() == value.signum() {
96            before + (value - before) / n
97        } else {
98            before * ((n - 1.0) / n) + value / n
99        };
100        self.deviations += (value - before) * (value - self.mean);
101    }
102
103    #[must_use]
104    pub fn merge(&self, other: &Self) -> Self {
105        if self.count == 0 {
106            return *other;
107        }
108        if other.count == 0 {
109            return *self;
110        }
111
112        let count = self.count + other.count;
113        let total = count as f64;
114        let mean = self.mean * (self.count as f64 / total) + other.mean * (other.count as f64 / total);
115        let apart = other.mean - self.mean;
116        let weight = (self.count as f64 * other.count as f64) / total;
117
118        Self {
119            count,
120            sum: self.sum + other.sum,
121            mean,
122            lowest: self.lowest.min(other.lowest),
123            highest: self.highest.max(other.highest),
124            deviations: self.deviations + other.deviations + apart * apart * weight,
125        }
126    }
127
128    #[must_use]
129    pub fn count(&self) -> u64 {
130        self.count
131    }
132
133    #[must_use]
134    pub fn is_empty(&self) -> bool {
135        self.count == 0
136    }
137
138    #[must_use]
139    pub fn sum(&self) -> f64 {
140        self.sum
141    }
142
143    #[must_use]
144    pub fn min(&self) -> Option<f64> {
145        (self.count > 0).then_some(self.lowest)
146    }
147
148    #[must_use]
149    pub fn max(&self) -> Option<f64> {
150        (self.count > 0).then_some(self.highest)
151    }
152
153    #[must_use]
154    pub fn span(&self) -> Option<f64> {
155        (self.count > 0).then_some(self.highest - self.lowest)
156    }
157
158    #[must_use]
159    pub fn mean(&self) -> Option<f64> {
160        (self.count > 0).then_some(self.mean)
161    }
162
163    // Population variance: the spread of exactly these values, not an
164    // estimate of a wider population's.
165    #[must_use]
166    pub fn variance(&self) -> Option<f64> {
167        (self.count > 0).then(|| (self.deviations / self.count as f64).max(0.0))
168    }
169
170    #[must_use]
171    pub fn std(&self) -> Option<f64> {
172        self.variance().map(f64::sqrt)
173    }
174
175    // The mean square is the squared mean plus the variance; `hypot` takes the
176    // root of that sum without squaring either term into overflow.
177    #[must_use]
178    pub fn rms(&self) -> Option<f64> {
179        Some(self.mean()?.hypot(self.std()?))
180    }
181}
182
183/* === Public Functions === */
184
185/* === Private Functions === */
186
187/* === Tests === */
188
189#[cfg(test)]
190mod tests {
191    use super::Summary;
192    use crate::signal::Series;
193
194    const CLOSE: f64 = 1e-9;
195
196    fn close(found: Option<f64>, expected: f64) {
197        let found = found.expect("an answer");
198        assert!((found - expected).abs() <= CLOSE * expected.abs().max(1.0), "{found} is not {expected}");
199    }
200
201    // Series answers over the whole run at once; Summary is folded from chunks and
202    // merged, so agreement proves the streaming form matches the exact one.
203    #[test]
204    fn chunked_summaries_agree() {
205        let values: Vec<f64> = (0..97).map(|index| ((index * 37) % 23) as f64 * 1.5 - 11.0).chain([f64::NAN, f64::INFINITY]).collect();
206        let series = Series::new(&values);
207        let summary = values.chunks(10).map(Summary::of).fold(Summary::new(), |held, next| held.merge(&next));
208
209        assert_eq!(series.count(), summary.count());
210        close(Some(series.sum()), summary.sum());
211        close(series.mean(), summary.mean().unwrap());
212        assert_eq!(series.min(), summary.min());
213        assert_eq!(series.max(), summary.max());
214        close(series.rms(), summary.rms().unwrap());
215        close(series.std(), summary.std().unwrap());
216    }
217}