use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ResidualStats {
pub n: usize,
pub mean: f64,
pub std: f64,
pub rms: f64,
pub min: f64,
pub max: f64,
}
impl ResidualStats {
pub fn from_slice(values: &[f64]) -> Self {
let n = values.len();
if n == 0 {
return Self::default();
}
let sum: f64 = values.iter().sum();
let mean = sum / n as f64;
let mut var = 0.0;
let mut sq = 0.0;
let mut min = f64::INFINITY;
let mut max = f64::NEG_INFINITY;
for &v in values {
let d = v - mean;
var += d * d;
sq += v * v;
if v < min {
min = v;
}
if v > max {
max = v;
}
}
let std = if n > 1 {
(var / (n as f64 - 1.0)).sqrt()
} else {
0.0
};
let rms = (sq / n as f64).sqrt();
Self {
n,
mean,
std,
rms,
min,
max,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ResidualsByGroup {
pub groups: BTreeMap<String, ResidualStats>,
pub overall: ResidualStats,
}
impl ResidualsByGroup {
pub fn from_pairs<'a, I>(items: I) -> Self
where
I: IntoIterator<Item = (&'a str, f64)>,
{
let mut buckets: BTreeMap<String, Vec<f64>> = BTreeMap::new();
let mut all = Vec::new();
for (g, v) in items {
buckets.entry(g.to_string()).or_default().push(v);
all.push(v);
}
let groups = buckets
.into_iter()
.map(|(k, v)| (k, ResidualStats::from_slice(&v)))
.collect();
Self {
groups,
overall: ResidualStats::from_slice(&all),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic_stats() {
let s = ResidualStats::from_slice(&[1.0, -1.0, 1.0, -1.0]);
assert_eq!(s.n, 4);
assert!((s.mean).abs() < 1e-12);
assert!((s.rms - 1.0).abs() < 1e-12);
}
#[test]
fn grouped() {
let g = ResidualsByGroup::from_pairs(vec![
("code", 0.5),
("code", -0.5),
("phase", 0.01),
("phase", -0.01),
]);
assert_eq!(g.groups.len(), 2);
assert!((g.groups["code"].rms - 0.5).abs() < 1e-12);
assert!((g.overall.n) == 4);
}
}