1#[derive(Clone, Copy, Debug, PartialEq)]
34pub struct Summary {
35 count: u64,
36 sum: f64,
37 sum_sq: f64,
38 lowest: f64,
39 highest: f64,
40 deviations: f64,
41}
42
43impl Default for Summary {
44 fn default() -> Self {
45 Self {
46 count: 0,
47 sum: 0.0,
48 sum_sq: 0.0,
49 lowest: f64::INFINITY,
50 highest: f64::NEG_INFINITY,
51 deviations: 0.0,
52 }
53 }
54}
55
56impl Summary {
57 #[must_use]
59 pub fn new() -> Self {
60 Self::default()
61 }
62
63 #[must_use]
65 pub fn of(values: &[f64]) -> Self {
66 let mut summary = Self::new();
67 for value in values {
68 summary.push(*value);
69 }
70 summary
71 }
72
73 pub fn push(&mut self, value: f64) {
79 let before = self.mean().unwrap_or(0.0);
80
81 self.count += 1;
82 self.sum += value;
83 self.sum_sq += value * value;
84 self.lowest = self.lowest.min(value);
85 self.highest = self.highest.max(value);
86
87 let after = self.sum / self.count as f64;
88 self.deviations += (value - before) * (value - after);
89 }
90
91 #[must_use]
98 pub fn merge(&self, other: &Self) -> Self {
99 if self.count == 0 {
100 return *other;
101 }
102 if other.count == 0 {
103 return *self;
104 }
105
106 let count = self.count + other.count;
107 let apart = (other.sum / other.count as f64) - (self.sum / self.count as f64);
108 let weight = (self.count as f64 * other.count as f64) / count as f64;
109
110 Self {
111 count,
112 sum: self.sum + other.sum,
113 sum_sq: self.sum_sq + other.sum_sq,
114 lowest: self.lowest.min(other.lowest),
115 highest: self.highest.max(other.highest),
116 deviations: self.deviations + other.deviations + apart * apart * weight,
117 }
118 }
119
120 #[must_use]
122 pub fn count(&self) -> u64 {
123 self.count
124 }
125
126 #[must_use]
128 pub fn is_empty(&self) -> bool {
129 self.count == 0
130 }
131
132 #[must_use]
134 pub fn sum(&self) -> f64 {
135 self.sum
136 }
137
138 #[must_use]
140 pub fn sum_sq(&self) -> f64 {
141 self.sum_sq
142 }
143
144 #[must_use]
146 pub fn min(&self) -> Option<f64> {
147 (self.count > 0).then_some(self.lowest)
148 }
149
150 #[must_use]
152 pub fn max(&self) -> Option<f64> {
153 (self.count > 0).then_some(self.highest)
154 }
155
156 #[must_use]
158 pub fn span(&self) -> Option<f64> {
159 (self.count > 0).then_some(self.highest - self.lowest)
160 }
161
162 #[must_use]
164 pub fn mean(&self) -> Option<f64> {
165 (self.count > 0).then(|| self.sum / self.count as f64)
166 }
167
168 #[must_use]
173 pub fn variance(&self) -> Option<f64> {
174 (self.count > 0).then(|| self.deviations / self.count as f64)
175 }
176
177 #[must_use]
179 pub fn deviation(&self) -> Option<f64> {
180 self.variance().map(f64::sqrt)
181 }
182
183 #[must_use]
185 pub fn rms(&self) -> Option<f64> {
186 (self.count > 0).then(|| (self.sum_sq / self.count as f64).sqrt())
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use std::f64::consts::TAU;
193
194 use super::*;
195
196 #[test]
197 fn a_summary_of_nothing_answers_nothing() {
198 let empty = Summary::new();
199
200 assert!(empty.is_empty());
201 assert_eq!(empty.count(), 0);
202 assert_eq!(empty.sum(), 0.0);
203 assert_eq!(empty.mean(), None);
204 assert_eq!(empty.min(), None);
205 assert_eq!(empty.max(), None);
206 assert_eq!(empty.span(), None);
207 assert_eq!(empty.variance(), None);
208 assert_eq!(empty.deviation(), None);
209 assert_eq!(empty.rms(), None);
210 }
211
212 #[test]
213 fn a_summary_reports_the_run_it_folded() {
214 let summary = Summary::of(&[3.0, 1.0, 4.0, 1.0, 5.0, 9.0, 2.0, 6.0]);
215
216 assert_eq!(summary.count(), 8);
217 assert_eq!(summary.sum(), 31.0);
218 assert_eq!(summary.min(), Some(1.0));
219 assert_eq!(summary.max(), Some(9.0));
220 assert_eq!(summary.span(), Some(8.0));
221 assert_eq!(summary.mean(), Some(31.0 / 8.0));
222 }
223
224 #[test]
225 fn the_rms_of_a_unit_sine_is_one_over_root_two() {
226 let samples: Vec<f64> = (0..10_000)
227 .map(|index| (TAU * f64::from(index) / 10_000.0).sin())
228 .collect();
229
230 let rms = Summary::of(&samples)
231 .rms()
232 .expect("a full period has an rms");
233
234 assert!((rms - 1.0 / 2.0_f64.sqrt()).abs() < 1e-4, "rms was {rms}");
235 }
236
237 #[test]
238 fn a_constant_run_has_no_spread() {
239 let summary = Summary::of(&[5.0; 100]);
240
241 assert_eq!(summary.variance(), Some(0.0));
242 assert_eq!(summary.deviation(), Some(0.0));
243 assert_eq!(summary.rms(), Some(5.0));
244 }
245
246 #[test]
247 fn variance_survives_a_mean_far_larger_than_the_spread() {
248 let summary = Summary::of(&[1e9, 1e9 + 1.0, 1e9 + 2.0]);
252
253 let variance = summary.variance().expect("three samples have a variance");
254 assert!(
255 (variance - 2.0 / 3.0).abs() < 1e-9,
256 "variance was {variance}"
257 );
258
259 let naive = summary.sum_sq() / 3.0 - (summary.sum() / 3.0).powi(2);
260 assert!(
261 naive < 0.0 || (naive - 2.0 / 3.0).abs() > 1e-3,
262 "the naive form was accurate"
263 );
264 }
265
266 #[test]
267 fn a_split_run_summarizes_to_the_whole_run() {
268 let samples: Vec<f64> = (0..64).map(f64::from).collect();
269
270 let whole = Summary::of(&samples);
271 let split = Summary::of(&samples[..17]).merge(&Summary::of(&samples[17..]));
272
273 assert_eq!(whole.count(), split.count());
274 assert_eq!(whole.sum(), split.sum());
275 assert_eq!(whole.sum_sq(), split.sum_sq());
276 assert_eq!(whole.min(), split.min());
277 assert_eq!(whole.max(), split.max());
278 assert!(
279 (whole.variance().unwrap() - split.variance().unwrap()).abs() < 1e-12,
280 "the split variance drifted"
281 );
282 }
283
284 #[test]
285 fn merging_is_associative() {
286 let (left, middle, right) = (
289 Summary::of(&[1.0, 2.0, 3.0, 4.0]),
290 Summary::of(&[8.0, 16.0]),
291 Summary::of(&[32.0, 64.0, 128.0]),
292 );
293
294 let (one, other) = (
295 left.merge(&middle).merge(&right),
296 left.merge(&middle.merge(&right)),
297 );
298
299 assert_eq!(one.count(), other.count());
300 assert_eq!(one.sum(), other.sum());
301 assert_eq!(one.sum_sq(), other.sum_sq());
302 assert_eq!(one.min(), other.min());
303 assert_eq!(one.max(), other.max());
304 assert!(
308 (one.variance().unwrap() - other.variance().unwrap()).abs() < 1e-9,
309 "{one:?} against {other:?}"
310 );
311 }
312
313 #[test]
314 fn an_empty_summary_is_the_identity_of_a_merge() {
315 let summary = Summary::of(&[1.0, 2.0, 3.0]);
316 let empty = Summary::new();
317
318 assert_eq!(summary.merge(&empty), summary);
319 assert_eq!(empty.merge(&summary), summary);
320 assert_eq!(empty.merge(&empty), empty);
321 }
322
323 #[test]
324 fn pushing_one_sample_at_a_time_is_summarizing_the_run() {
325 let samples = [2.5, -1.0, 7.25, 0.0];
326
327 let mut pushed = Summary::new();
328 for sample in samples {
329 pushed.push(sample);
330 }
331
332 assert_eq!(pushed, Summary::of(&samples));
333 }
334}