Skip to main content

harmos_storage/signal/waveform/
series.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
17use std::borrow::Cow;
18
19// Third Party Imports
20
21// Local Imports
22use super::Summary;
23
24/* === Constants === */
25
26/* === Macros === */
27
28/* === Type Aliases === */
29
30/* === Structs === */
31
32// A finished run of values answered exactly: everything `Summary` answers,
33// plus the order statistics that need every value at once. Non-finite values
34// are skipped on construction, so a slice that is already finite is borrowed
35// and only a slice holding NaN or an infinity is copied.
36#[derive(Clone, Debug, PartialEq)]
37pub struct Series<'a> {
38    values: Cow<'a, [f64]>,
39    sorted: bool,
40}
41
42/* === Enums === */
43
44/* === Traits === */
45
46/* === Implementations === */
47
48impl<'a> Series<'a> {
49    #[must_use]
50    pub fn new(values: &'a [f64]) -> Self {
51        let values =
52            if values.iter().all(|value| value.is_finite()) { Cow::Borrowed(values) } else { Cow::Owned(finite(values.iter().copied())) };
53
54        Self { values, sorted: false }
55    }
56
57    // Orders the values in place, so every later median and percentile reads
58    // them without copying. A borrowed slice is copied once here.
59    pub fn sort(&mut self) {
60        if !self.sorted {
61            self.values.to_mut().sort_by(f64::total_cmp);
62            self.sorted = true;
63        }
64    }
65
66    #[must_use]
67    pub fn values(&self) -> &[f64] {
68        &self.values
69    }
70
71    #[must_use]
72    pub fn summary(&self) -> Summary {
73        Summary::of(&self.values)
74    }
75
76    #[must_use]
77    pub fn count(&self) -> u64 {
78        self.values.len() as u64
79    }
80
81    #[must_use]
82    pub fn is_empty(&self) -> bool {
83        self.values.is_empty()
84    }
85
86    #[must_use]
87    pub fn sum(&self) -> f64 {
88        self.summary().sum()
89    }
90
91    #[must_use]
92    pub fn mean(&self) -> Option<f64> {
93        self.summary().mean()
94    }
95
96    #[must_use]
97    pub fn min(&self) -> Option<f64> {
98        self.summary().min()
99    }
100
101    #[must_use]
102    pub fn max(&self) -> Option<f64> {
103        self.summary().max()
104    }
105
106    #[must_use]
107    pub fn rms(&self) -> Option<f64> {
108        self.summary().rms()
109    }
110
111    // Population standard deviation, the same spread `Summary::std` answers.
112    #[must_use]
113    pub fn std(&self) -> Option<f64> {
114        self.summary().std()
115    }
116
117    #[must_use]
118    pub fn median(&self) -> Option<f64> {
119        self.percentile(50.0)
120    }
121
122    // Linear interpolation between the two closest ranks: rank `p / 100 *
123    // (n - 1)` over the ascending values, so 0 is the minimum, 100 the maximum
124    // and 50 the median of an even count is the midpoint of the middle pair.
125    // A rank outside 0..=100 has no answer.
126    #[must_use]
127    pub fn percentile(&self, rank: f64) -> Option<f64> {
128        if self.values.is_empty() || !(0.0..=100.0).contains(&rank) {
129            return None;
130        }
131        let ordered: Cow<'_, [f64]> = if self.sorted {
132            Cow::Borrowed(&self.values)
133        } else {
134            let mut copy = self.values.to_vec();
135            copy.sort_by(f64::total_cmp);
136            Cow::Owned(copy)
137        };
138
139        let position = rank / 100.0 * (ordered.len() - 1) as f64;
140        let lower = position.floor() as usize;
141        let upper = position.ceil() as usize;
142
143        Some(between(ordered[lower], ordered[upper], position - lower as f64))
144    }
145
146    // Every value satisfies the predicate; vacuously true for no values, the
147    // way `Iterator::all` is, so a caller that needs evidence checks `count`.
148    pub fn all(&self, mut holds: impl FnMut(f64) -> bool) -> bool {
149        self.values.iter().all(|value| holds(*value))
150    }
151
152    pub fn any(&self, mut holds: impl FnMut(f64) -> bool) -> bool {
153        self.values.iter().any(|value| holds(*value))
154    }
155
156    #[must_use]
157    pub fn all_below(&self, limit: f64) -> bool {
158        self.all(|value| value < limit)
159    }
160
161    #[must_use]
162    pub fn all_within(&self, min: f64, max: f64) -> bool {
163        self.all(|value| (min..=max).contains(&value))
164    }
165}
166
167impl<'a> From<&'a [f64]> for Series<'a> {
168    fn from(values: &'a [f64]) -> Self {
169        Self::new(values)
170    }
171}
172
173impl From<Vec<f64>> for Series<'static> {
174    fn from(mut values: Vec<f64>) -> Self {
175        values.retain(|value| value.is_finite());
176        Self { values: Cow::Owned(values), sorted: false }
177    }
178}
179
180impl FromIterator<f64> for Series<'static> {
181    fn from_iter<I: IntoIterator<Item = f64>>(values: I) -> Self {
182        Self { values: Cow::Owned(finite(values)), sorted: false }
183    }
184}
185
186/* === Public Functions === */
187
188/* === Private Functions === */
189
190fn finite(values: impl IntoIterator<Item = f64>) -> Vec<f64> {
191    values.into_iter().filter(|value| value.is_finite()).collect()
192}
193
194// A point a fraction of the way from one value to the next that stays finite
195// for any finite pair: the difference is formed only when both share a sign.
196fn between(low: f64, high: f64, fraction: f64) -> f64 {
197    if fraction == 0.0 || low == high {
198        low
199    } else if low.signum() == high.signum() {
200        low + (high - low) * fraction
201    } else {
202        low * (1.0 - fraction) + high * fraction
203    }
204}
205
206/* === Tests === */
207
208#[cfg(test)]
209mod tests {
210    use super::Series;
211    use crate::signal::Summary;
212
213    const CLOSE: f64 = 1e-9;
214
215    fn close(found: Option<f64>, expected: f64) {
216        let found = found.expect("an answer");
217        assert!((found - expected).abs() <= CLOSE * expected.abs().max(1.0), "{found} is not {expected}");
218    }
219
220    #[test]
221    fn empty_series_undefined() {
222        let series = Series::new(&[]);
223
224        assert_eq!(series.count(), 0);
225        assert_eq!(series.sum(), 0.0);
226        assert_eq!(series.mean(), None);
227        assert_eq!(series.median(), None);
228        assert_eq!(series.min(), None);
229        assert_eq!(series.max(), None);
230        assert_eq!(series.rms(), None);
231        assert_eq!(series.std(), None);
232        assert_eq!(series.percentile(0.0), None);
233        assert_eq!(series.percentile(100.0), None);
234        assert!(series.all(|_| false), "no value refutes a predicate");
235        assert!(!series.any(|_| true));
236    }
237
238    #[test]
239    fn single_value_degenerates() {
240        let series = Series::new(&[-4.0]);
241
242        assert_eq!(series.count(), 1);
243        assert_eq!(series.sum(), -4.0);
244        assert_eq!(series.mean(), Some(-4.0));
245        assert_eq!(series.median(), Some(-4.0));
246        assert_eq!(series.min(), Some(-4.0));
247        assert_eq!(series.max(), Some(-4.0));
248        assert_eq!(series.rms(), Some(4.0));
249        assert_eq!(series.std(), Some(0.0));
250        assert_eq!(series.percentile(0.0), Some(-4.0));
251        assert_eq!(series.percentile(37.0), Some(-4.0));
252        assert_eq!(series.percentile(100.0), Some(-4.0));
253    }
254
255    #[test]
256    fn known_run_statistics() {
257        let series = Series::new(&[2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0]);
258
259        assert_eq!(series.count(), 8);
260        assert_eq!(series.sum(), 40.0);
261        close(series.mean(), 5.0);
262        assert_eq!(series.min(), Some(2.0));
263        assert_eq!(series.max(), Some(9.0));
264        close(series.std(), 2.0);
265        close(series.rms(), (232.0_f64 / 8.0).sqrt());
266    }
267
268    #[test]
269    fn odd_even_medians() {
270        assert_eq!(Series::new(&[9.0, 1.0, 5.0]).median(), Some(5.0));
271        assert_eq!(Series::new(&[9.0, 1.0, 5.0, 3.0]).median(), Some(4.0));
272    }
273
274    #[test]
275    fn percentiles_interpolate_ranks() {
276        let series = Series::new(&[40.0, 10.0, 30.0, 20.0]);
277
278        assert_eq!(series.percentile(0.0), Some(10.0));
279        assert_eq!(series.percentile(100.0), Some(40.0));
280        close(series.percentile(50.0), 25.0);
281        close(series.percentile(25.0), 17.5);
282        close(series.percentile(90.0), 37.0);
283        assert_eq!(series.percentile(100.5), None);
284        assert_eq!(series.percentile(-1.0), None);
285        assert_eq!(series.percentile(f64::NAN), None);
286    }
287
288    #[test]
289    fn sorting_preserves_percentiles() {
290        let values = [3.0, -1.0, 8.0, 2.5, 0.0];
291        let unsorted = Series::new(&values);
292        let mut sorted = Series::new(&values);
293        sorted.sort();
294
295        for rank in [0.0, 12.5, 50.0, 77.0, 100.0] {
296            assert_eq!(unsorted.percentile(rank), sorted.percentile(rank));
297        }
298    }
299
300    #[test]
301    fn extremes_avoid_overflow() {
302        let series = Series::new(&[-f64::MAX, f64::MAX]);
303
304        assert_eq!(series.median(), Some(0.0));
305        assert_eq!(Series::new(&[f64::MAX, f64::MAX]).mean(), Some(f64::MAX));
306    }
307
308    #[test]
309    fn non_finite_skipped() {
310        let values = [1.0, f64::NAN, 3.0, f64::INFINITY, f64::NEG_INFINITY];
311        let series = Series::new(&values);
312        let collected: Series<'static> = values.iter().copied().collect();
313        let owned = Series::from(values.to_vec());
314
315        for series in [&series, &collected, &owned] {
316            assert_eq!(series.values(), &[1.0, 3.0]);
317            assert_eq!(series.count(), 2);
318            assert_eq!(series.median(), Some(2.0));
319            assert_eq!(series.max(), Some(3.0));
320        }
321        assert_eq!(Summary::of(&values), Summary::of(&[1.0, 3.0]));
322    }
323
324    #[test]
325    fn predicates_bound_inclusively() {
326        let series = Series::new(&[1.0, 2.5, 4.0]);
327
328        assert!(series.all_below(4.5));
329        assert!(!series.all_below(4.0), "the limit itself is not below");
330        assert!(series.all_within(1.0, 4.0), "the bounds are inclusive");
331        assert!(!series.all_within(1.5, 4.0));
332        assert!(series.any(|value| value > 3.0));
333        assert!(series.all(|value| value > 0.0));
334    }
335}