#[derive(Clone, Copy, Debug, PartialEq)]
pub enum StatScope {
All,
OnLimits {
x_range: (f64, f64),
y_range: (f64, f64),
},
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct Stats {
pub count: usize,
pub included_count: usize,
pub min: Option<f64>,
pub max: Option<f64>,
pub delta: Option<f64>,
pub mean: Option<f64>,
pub std: Option<f64>,
pub sum: Option<f64>,
pub com: ComCoord,
pub coord_min: ComCoord,
pub coord_max: ComCoord,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct ComCoord {
pub x: Option<f64>,
pub y: Option<f64>,
}
impl ComCoord {
pub const NONE: ComCoord = ComCoord { x: None, y: None };
}
impl Stats {
pub fn for_curve(xs: &[f64], ys: &[f64], scope: StatScope) -> Self {
Self::curve_inner(xs, ys, scope, None)
}
pub fn for_curve_roi(xs: &[f64], ys: &[f64], from: f64, to: f64) -> Self {
Self::curve_inner(xs, ys, StatScope::All, Some((from, to)))
}
fn curve_inner(xs: &[f64], ys: &[f64], scope: StatScope, roi: Option<(f64, f64)>) -> Self {
let count = xs.len().min(ys.len());
let mut acc = Accumulator::default();
for i in 0..count {
let x = xs[i];
let y = ys[i];
if let Some((from, to)) = roi {
let (lo, hi) = order(from, to);
let inside = x >= lo && x <= hi;
if !inside {
continue;
}
}
if let StatScope::OnLimits { x_range, .. } = scope {
let (lo, hi) = order(x_range.0, x_range.1);
let inside = x >= lo && x <= hi;
if !inside {
continue;
}
}
acc.push(y, x, None);
}
acc.finish(count)
}
pub fn for_scatter(xs: &[f64], ys: &[f64], values: &[f64], scope: StatScope) -> Self {
let count = xs.len().min(ys.len()).min(values.len());
let mut acc = Accumulator::default();
for i in 0..count {
let (x, y, v) = (xs[i], ys[i], values[i]);
if let StatScope::OnLimits { x_range, y_range } = scope {
let (lx, hx) = order(x_range.0, x_range.1);
let (ly, hy) = order(y_range.0, y_range.1);
let inside = x >= lx && x <= hx && y >= ly && y <= hy;
if !inside {
continue;
}
}
acc.push(v, x, Some(y));
}
acc.finish(count)
}
pub fn for_image(
data: &[f64],
width: usize,
height: usize,
origin: (f64, f64),
scale: (f64, f64),
scope: StatScope,
) -> Self {
let count = width.saturating_mul(height);
if width == 0 || height == 0 {
return Stats {
count,
..Stats::default()
};
}
let (xmin, xmax, ymin, ymax) = match scope {
StatScope::All => (0usize, width - 1, 0usize, height - 1),
StatScope::OnLimits { x_range, y_range } => {
if scale.0 == 0.0 || scale.1 == 0.0 {
return Stats {
count,
..Stats::default()
};
}
let (lx, hx) = order(x_range.0, x_range.1);
let (ly, hy) = order(y_range.0, y_range.1);
let to_ix = |v: f64| ((v - origin.0) / scale.0) as i64;
let to_iy = |v: f64| ((v - origin.1) / scale.1) as i64;
let mut ix0 = to_ix(lx);
let mut ix1 = to_ix(hx);
let mut iy0 = to_iy(ly);
let mut iy1 = to_iy(hy);
if ix0 > ix1 {
std::mem::swap(&mut ix0, &mut ix1);
}
if iy0 > iy1 {
std::mem::swap(&mut iy0, &mut iy1);
}
let cx0 = ix0.clamp(0, width as i64 - 1);
let cx1 = ix1.clamp(0, width as i64 - 1);
let cy0 = iy0.clamp(0, height as i64 - 1);
let cy1 = iy1.clamp(0, height as i64 - 1);
if cx1 <= cx0 || cy1 <= cy0 {
return Stats {
count,
..Stats::default()
};
}
(cx0 as usize, cx1 as usize, cy0 as usize, cy1 as usize)
}
};
let mut acc = Accumulator::default();
for row in ymin..=ymax {
for col in xmin..=xmax {
let idx = row * width + col;
if idx >= data.len() {
continue;
}
let v = data[idx];
let x = origin.0 + scale.0 * col as f64;
let y = origin.1 + scale.1 * row as f64;
acc.push(v, x, Some(y));
}
}
acc.finish(count)
}
}
#[derive(Default)]
struct Accumulator {
included: usize,
sum: f64,
welford_mean: f64,
welford_m2: f64,
has_non_finite: bool,
com_x_num: f64,
com_y_num: f64,
nan_pos: Option<(f64, Option<f64>)>,
min: f64,
max: f64,
min_pos: (f64, Option<f64>),
max_pos: (f64, Option<f64>),
inited: bool,
two_d: bool,
}
impl Accumulator {
fn push(&mut self, value: f64, x: f64, y: Option<f64>) {
self.included += 1;
self.two_d |= y.is_some();
self.sum += value;
if !value.is_finite() {
self.has_non_finite = true;
}
let delta = value - self.welford_mean;
self.welford_mean += delta / self.included as f64;
self.welford_m2 += delta * (value - self.welford_mean);
self.com_x_num += value * x;
if let Some(y) = y {
self.com_y_num += value * y;
}
if value.is_nan() {
if self.nan_pos.is_none() {
self.nan_pos = Some((x, y));
}
} else if !self.inited {
self.inited = true;
self.min = value;
self.max = value;
self.min_pos = (x, y);
self.max_pos = (x, y);
} else {
if value < self.min {
self.min = value;
self.min_pos = (x, y);
}
if value > self.max {
self.max = value;
self.max_pos = (x, y);
}
}
}
fn finish(self, count: usize) -> Stats {
if self.included == 0 {
return Stats {
count,
included_count: 0,
..Stats::default()
};
}
let mean = self.sum / self.included as f64;
let coord = |pos: (f64, Option<f64>)| ComCoord {
x: Some(pos.0),
y: pos.1,
};
let (min, max) = if self.inited {
(self.min, self.max)
} else {
(f64::NAN, f64::NAN)
};
let (coord_min, coord_max) = match self.nan_pos {
Some(pos) => (coord(pos), coord(pos)),
None => (coord(self.min_pos), coord(self.max_pos)),
};
let com = if self.sum == 0.0 {
ComCoord::NONE
} else {
ComCoord {
x: Some(self.com_x_num / self.sum),
y: self.two_d.then(|| self.com_y_num / self.sum),
}
};
Stats {
count,
included_count: self.included,
min: Some(min),
max: Some(max),
delta: Some(max - min),
mean: Some(mean),
std: (!self.has_non_finite).then(|| (self.welford_m2 / self.included as f64).sqrt()),
sum: Some(self.sum),
com,
coord_min,
coord_max,
}
}
}
fn order(a: f64, b: f64) -> (f64, f64) {
if a <= b { (a, b) } else { (b, a) }
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f64, b: f64) {
assert!((a - b).abs() < 1e-9, "expected {b}, got {a}");
}
#[test]
fn curve_empty_yields_none() {
let s = Stats::for_curve(&[], &[], StatScope::All);
assert_eq!(s.count, 0);
assert_eq!(s.included_count, 0);
assert_eq!(s.min, None);
assert_eq!(s.max, None);
assert_eq!(s.delta, None);
assert_eq!(s.mean, None);
assert_eq!(s.std, None);
assert_eq!(s.sum, None);
assert_eq!(s.com, ComCoord::NONE);
assert_eq!(s.coord_min, ComCoord::NONE);
assert_eq!(s.coord_max, ComCoord::NONE);
}
#[test]
fn curve_std_is_population_std() {
let ys = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
let xs: Vec<f64> = (0..ys.len()).map(|i| i as f64).collect();
let s = Stats::for_curve(&xs, &ys, StatScope::All);
approx(s.std.unwrap(), 2.0);
}
#[test]
fn curve_std_single_point_is_zero() {
let s = Stats::for_curve(&[1.0], &[5.0], StatScope::All);
approx(s.std.unwrap(), 0.0);
}
#[test]
fn image_std_is_population_std() {
let data = [1.0, 2.0, 3.0, 4.0];
let s = Stats::for_image(&data, 2, 2, (0.0, 0.0), (1.0, 1.0), StatScope::All);
approx(s.std.unwrap(), 1.25f64.sqrt());
}
#[test]
fn curve_single_point() {
let s = Stats::for_curve(&[2.0], &[5.0], StatScope::All);
assert_eq!(s.count, 1);
assert_eq!(s.included_count, 1);
approx(s.min.unwrap(), 5.0);
approx(s.max.unwrap(), 5.0);
approx(s.delta.unwrap(), 0.0);
approx(s.mean.unwrap(), 5.0);
approx(s.sum.unwrap(), 5.0);
approx(s.com.x.unwrap(), 2.0);
assert_eq!(s.com.y, None);
approx(s.coord_min.x.unwrap(), 2.0);
approx(s.coord_max.x.unwrap(), 2.0);
}
#[test]
fn curve_all_nan_min_max_surface_nan_not_none() {
let s = Stats::for_curve(&[1.0, 2.0], &[f64::NAN, f64::NAN], StatScope::All);
assert_eq!(s.count, 2);
assert_eq!(s.included_count, 2);
assert!(s.min.unwrap().is_nan());
assert!(s.max.unwrap().is_nan());
assert!(s.delta.unwrap().is_nan());
assert!(s.mean.unwrap().is_nan());
assert_eq!(s.std, None, "numpy.ma.std is masked with NaN present");
approx(s.coord_min.x.unwrap(), 1.0);
approx(s.coord_max.x.unwrap(), 1.0);
}
#[test]
fn curve_nan_value_propagates_mean_sum_and_masks_std() {
let s = Stats::for_curve(&[0.0, 1.0, 2.0], &[3.0, f64::NAN, 1.0], StatScope::All);
assert_eq!(s.included_count, 3);
approx(s.min.unwrap(), 1.0);
approx(s.max.unwrap(), 3.0);
assert!(s.mean.unwrap().is_nan());
assert!(s.sum.unwrap().is_nan());
assert_eq!(s.std, None);
assert!(s.com.x.unwrap().is_nan());
approx(s.coord_min.x.unwrap(), 1.0);
approx(s.coord_max.x.unwrap(), 1.0);
}
#[test]
fn curve_inf_participates_in_min_max_and_masks_std() {
let s = Stats::for_curve(
&[0.0, 1.0, 2.0],
&[3.0, f64::NEG_INFINITY, 1.0],
StatScope::All,
);
assert_eq!(s.min, Some(f64::NEG_INFINITY));
approx(s.max.unwrap(), 3.0);
approx(s.coord_min.x.unwrap(), 1.0);
approx(s.coord_max.x.unwrap(), 0.0);
assert_eq!(s.mean, Some(f64::NEG_INFINITY));
assert_eq!(s.sum, Some(f64::NEG_INFINITY));
assert_eq!(s.std, None);
}
#[test]
fn curve_nan_x_kept_under_all_scope_pollutes_com_and_coords_only() {
let s = Stats::for_curve(&[f64::NAN, 3.0], &[10.0, 4.0], StatScope::All);
assert_eq!(s.included_count, 2);
approx(s.min.unwrap(), 4.0);
approx(s.max.unwrap(), 10.0);
approx(s.sum.unwrap(), 14.0);
assert!(s.std.is_some(), "values are finite; only x is NaN");
assert!(s.com.x.unwrap().is_nan());
assert!(s.coord_max.x.unwrap().is_nan(), "max y=10 sits at x=NaN");
approx(s.coord_min.x.unwrap(), 3.0);
}
#[test]
fn curve_masks_exclude_nan_x() {
let xs = [0.0, f64::NAN, 2.0];
let ys = [1.0, 100.0, 3.0];
let on_limits = Stats::for_curve(
&xs,
&ys,
StatScope::OnLimits {
x_range: (0.0, 2.0),
y_range: (-1e9, 1e9),
},
);
assert_eq!(on_limits.included_count, 2);
approx(on_limits.sum.unwrap(), 4.0);
assert!(on_limits.std.is_some());
let roi = Stats::for_curve_roi(&xs, &ys, 0.0, 2.0);
assert_eq!(roi.included_count, 2);
approx(roi.sum.unwrap(), 4.0);
}
#[test]
fn curve_com_symmetric_lands_at_center() {
let xs = [0.0, 1.0, 2.0, 3.0, 4.0];
let ys = [1.0, 2.0, 3.0, 2.0, 1.0];
let s = Stats::for_curve(&xs, &ys, StatScope::All);
approx(s.com.x.unwrap(), 2.0);
}
#[test]
fn curve_com_all_zero_is_none() {
let s = Stats::for_curve(&[0.0, 1.0, 2.0], &[0.0, 0.0, 0.0], StatScope::All);
assert_eq!(s.included_count, 3);
approx(s.sum.unwrap(), 0.0);
assert_eq!(s.com, ComCoord::NONE);
}
#[test]
fn curve_argmax_argmin_coordinates() {
let xs = [10.0, 11.0, 12.0, 13.0];
let ys = [3.0, 9.0, -1.0, 5.0];
let s = Stats::for_curve(&xs, &ys, StatScope::All);
approx(s.coord_max.x.unwrap(), 11.0); approx(s.coord_min.x.unwrap(), 12.0); }
#[test]
fn curve_argmax_first_occurrence_on_tie() {
let xs = [0.0, 1.0, 2.0];
let ys = [5.0, 5.0, 1.0];
let s = Stats::for_curve(&xs, &ys, StatScope::All);
approx(s.coord_max.x.unwrap(), 0.0);
}
#[test]
fn curve_on_limits_excludes_out_of_range() {
let xs = [0.0, 1.0, 2.0, 3.0, 4.0];
let ys = [10.0, 20.0, 30.0, 40.0, 50.0];
let s = Stats::for_curve(
&xs,
&ys,
StatScope::OnLimits {
x_range: (1.0, 3.0),
y_range: (-1e9, 1e9),
},
);
assert_eq!(s.included_count, 3);
approx(s.min.unwrap(), 20.0);
approx(s.max.unwrap(), 40.0);
approx(s.sum.unwrap(), 90.0);
}
#[test]
fn curve_on_limits_ignores_y_range() {
let xs = [0.0, 1.0, 2.0];
let ys = [100.0, 200.0, 300.0];
let s = Stats::for_curve(
&xs,
&ys,
StatScope::OnLimits {
x_range: (0.0, 2.0),
y_range: (0.0, 1.0), },
);
assert_eq!(s.included_count, 3);
approx(s.sum.unwrap(), 600.0);
}
#[test]
fn curve_roi_x_range_filters() {
let xs = [0.0, 1.0, 2.0, 3.0];
let ys = [1.0, 2.0, 3.0, 4.0];
let s = Stats::for_curve_roi(&xs, &ys, 1.0, 2.0);
assert_eq!(s.included_count, 2);
approx(s.sum.unwrap(), 5.0);
approx(s.min.unwrap(), 2.0);
approx(s.max.unwrap(), 3.0);
}
#[test]
fn curve_roi_reversed_bounds_ordered() {
let xs = [0.0, 1.0, 2.0, 3.0];
let ys = [1.0, 2.0, 3.0, 4.0];
let s = Stats::for_curve_roi(&xs, &ys, 2.0, 1.0);
assert_eq!(s.included_count, 2);
approx(s.sum.unwrap(), 5.0);
}
#[test]
fn scatter_stats_run_over_the_value_array() {
let xs = [0.0, 1.0, 2.0];
let ys = [10.0, 11.0, 12.0];
let vs = [5.0, 1.0, 3.0];
let s = Stats::for_scatter(&xs, &ys, &vs, StatScope::All);
assert_eq!(s.included_count, 3);
approx(s.min.unwrap(), 1.0);
approx(s.max.unwrap(), 5.0);
approx(s.sum.unwrap(), 9.0);
approx(s.mean.unwrap(), 3.0);
approx(s.coord_min.x.unwrap(), 1.0);
approx(s.coord_min.y.unwrap(), 11.0);
approx(s.coord_max.x.unwrap(), 0.0);
approx(s.coord_max.y.unwrap(), 10.0);
approx(s.com.x.unwrap(), 7.0 / 9.0);
approx(s.com.y.unwrap(), 97.0 / 9.0);
}
#[test]
fn scatter_on_limits_gates_on_x_and_y() {
let xs = [0.0, 1.0, 2.0];
let ys = [0.0, 100.0, 0.0];
let vs = [1.0, 2.0, 4.0];
let s = Stats::for_scatter(
&xs,
&ys,
&vs,
StatScope::OnLimits {
x_range: (0.0, 2.0),
y_range: (-1.0, 1.0), },
);
assert_eq!(s.included_count, 2);
approx(s.sum.unwrap(), 5.0);
}
#[test]
fn scatter_all_scope_keeps_non_finite_components() {
let xs = [0.0, f64::NAN, 2.0, 3.0];
let ys = [0.0, 1.0, f64::INFINITY, 3.0];
let vs = [1.0, 2.0, 4.0, f64::NAN];
let s = Stats::for_scatter(&xs, &ys, &vs, StatScope::All);
assert_eq!(s.count, 4);
assert_eq!(s.included_count, 4);
approx(s.min.unwrap(), 1.0);
approx(s.max.unwrap(), 4.0);
assert!(s.mean.unwrap().is_nan());
assert_eq!(s.std, None);
approx(s.coord_min.x.unwrap(), 3.0);
approx(s.coord_min.y.unwrap(), 3.0);
approx(s.coord_max.x.unwrap(), 3.0);
}
#[test]
fn scatter_on_limits_excludes_nan_coordinates() {
let xs = [0.0, f64::NAN, 2.0];
let ys = [0.0, 0.0, 0.0];
let vs = [1.0, 100.0, 3.0];
let s = Stats::for_scatter(
&xs,
&ys,
&vs,
StatScope::OnLimits {
x_range: (0.0, 2.0),
y_range: (-1.0, 1.0),
},
);
assert_eq!(s.included_count, 2);
approx(s.sum.unwrap(), 4.0);
assert!(s.std.is_some());
}
#[test]
fn scatter_empty_yields_none() {
let s = Stats::for_scatter(&[], &[], &[], StatScope::All);
assert_eq!(s.included_count, 0);
assert_eq!(s.min, None);
assert_eq!(s.com, ComCoord::NONE);
}
#[test]
fn image_empty_dims_yield_none() {
let s = Stats::for_image(&[], 0, 0, (0.0, 0.0), (1.0, 1.0), StatScope::All);
assert_eq!(s.count, 0);
assert_eq!(s.min, None);
assert_eq!(s.com, ComCoord::NONE);
}
#[test]
fn image_single_pixel() {
let s = Stats::for_image(&[7.0], 1, 1, (5.0, 6.0), (1.0, 1.0), StatScope::All);
assert_eq!(s.included_count, 1);
approx(s.min.unwrap(), 7.0);
approx(s.max.unwrap(), 7.0);
approx(s.com.x.unwrap(), 5.0);
approx(s.com.y.unwrap(), 6.0);
approx(s.coord_max.x.unwrap(), 5.0);
approx(s.coord_max.y.unwrap(), 6.0);
}
#[test]
fn image_argmax_coordinate_correct() {
let data = [1.0, 2.0, 9.0, 3.0];
let s = Stats::for_image(&data, 2, 2, (0.0, 0.0), (1.0, 1.0), StatScope::All);
approx(s.max.unwrap(), 9.0);
approx(s.coord_max.x.unwrap(), 0.0);
approx(s.coord_max.y.unwrap(), 1.0);
approx(s.min.unwrap(), 1.0);
approx(s.coord_min.x.unwrap(), 0.0);
approx(s.coord_min.y.unwrap(), 0.0);
}
#[test]
fn image_argmax_with_scale_and_origin() {
let data = [1.0, 2.0, 3.0, 9.0];
let s = Stats::for_image(&data, 2, 2, (10.0, 20.0), (2.0, 3.0), StatScope::All);
approx(s.coord_max.x.unwrap(), 10.0 + 2.0 * 1.0); approx(s.coord_max.y.unwrap(), 20.0 + 3.0 * 1.0); }
#[test]
fn image_com_symmetric_lands_at_center() {
let data = vec![1.0; 9];
let s = Stats::for_image(&data, 3, 3, (0.0, 0.0), (1.0, 1.0), StatScope::All);
approx(s.com.x.unwrap(), 1.0);
approx(s.com.y.unwrap(), 1.0);
}
#[test]
fn image_com_all_zero_is_none() {
let data = vec![0.0; 4];
let s = Stats::for_image(&data, 2, 2, (0.0, 0.0), (1.0, 1.0), StatScope::All);
assert_eq!(s.included_count, 4);
assert_eq!(s.com, ComCoord::NONE);
}
#[test]
fn image_non_finite_pixels_participate() {
let data = [1.0, f64::NAN, 3.0, f64::INFINITY];
let s = Stats::for_image(&data, 2, 2, (0.0, 0.0), (1.0, 1.0), StatScope::All);
assert_eq!(s.included_count, 4);
approx(s.min.unwrap(), 1.0);
assert_eq!(s.max, Some(f64::INFINITY));
assert!(s.sum.unwrap().is_nan());
assert!(s.mean.unwrap().is_nan());
assert_eq!(s.std, None);
approx(s.coord_min.x.unwrap(), 1.0);
approx(s.coord_min.y.unwrap(), 0.0);
assert!(s.com.x.unwrap().is_nan());
assert!(s.com.y.unwrap().is_nan());
}
#[test]
fn image_on_limits_clips_to_window() {
let mut data = vec![0.0; 16];
for (i, v) in data.iter_mut().enumerate() {
*v = i as f64;
}
let s = Stats::for_image(
&data,
4,
4,
(0.0, 0.0),
(1.0, 1.0),
StatScope::OnLimits {
x_range: (1.0, 2.0),
y_range: (1.0, 2.0),
},
);
assert_eq!(s.included_count, 4);
approx(s.min.unwrap(), 5.0);
approx(s.max.unwrap(), 10.0);
approx(s.sum.unwrap(), 30.0);
}
#[test]
fn image_on_limits_zero_scale_yields_empty() {
let data = vec![1.0; 4];
let s = Stats::for_image(
&data,
2,
2,
(0.0, 0.0),
(0.0, 1.0),
StatScope::OnLimits {
x_range: (0.0, 1.0),
y_range: (0.0, 1.0),
},
);
assert_eq!(s.min, None);
assert_eq!(s.included_count, 0);
}
}