#[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 finite_count: usize,
pub min: Option<f64>,
pub max: Option<f64>,
pub delta: Option<f64>,
pub mean: 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 };
fn x_only(x: f64) -> Self {
ComCoord {
x: Some(x),
y: None,
}
}
fn xy(x: f64, y: f64) -> Self {
ComCoord {
x: Some(x),
y: Some(y),
}
}
}
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 !x.is_finite() || !y.is_finite() {
continue;
}
if let Some((from, to)) = roi {
let (lo, hi) = order(from, to);
if x < lo || x > hi {
continue;
}
}
if let StatScope::OnLimits { x_range, .. } = scope {
let (lo, hi) = order(x_range.0, x_range.1);
if x < lo || x > hi {
continue;
}
}
acc.push(y, x, f64::NAN);
}
acc.finish(count, false)
}
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];
if !v.is_finite() {
continue;
}
let x = origin.0 + scale.0 * col as f64;
let y = origin.1 + scale.1 * row as f64;
acc.push(v, x, y);
}
}
acc.finish(count, true)
}
}
#[derive(Default)]
struct Accumulator {
finite_count: usize,
sum: f64,
com_x_num: f64,
com_y_num: f64,
min: f64,
max: f64,
min_pos: (f64, f64),
max_pos: (f64, f64),
first: bool,
}
impl Accumulator {
fn push(&mut self, value: f64, x: f64, y: f64) {
self.finite_count += 1;
self.sum += value;
self.com_x_num += value * x;
if y.is_finite() {
self.com_y_num += value * y;
}
if !self.first {
self.first = 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, is_image: bool) -> Stats {
if self.finite_count == 0 {
return Stats {
count,
finite_count: 0,
..Stats::default()
};
}
let mean = self.sum / self.finite_count as f64;
let coord = |pos: (f64, f64)| {
if is_image {
ComCoord::xy(pos.0, pos.1)
} else {
ComCoord::x_only(pos.0)
}
};
let com = if self.sum == 0.0 {
ComCoord::NONE
} else if is_image {
ComCoord::xy(self.com_x_num / self.sum, self.com_y_num / self.sum)
} else {
ComCoord::x_only(self.com_x_num / self.sum)
};
Stats {
count,
finite_count: self.finite_count,
min: Some(self.min),
max: Some(self.max),
delta: Some(self.max - self.min),
mean: Some(mean),
sum: Some(self.sum),
com,
coord_min: coord(self.min_pos),
coord_max: coord(self.max_pos),
}
}
}
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.finite_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.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_single_point() {
let s = Stats::for_curve(&[2.0], &[5.0], StatScope::All);
assert_eq!(s.count, 1);
assert_eq!(s.finite_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_yields_none() {
let s = Stats::for_curve(&[1.0, 2.0], &[f64::NAN, f64::INFINITY], StatScope::All);
assert_eq!(s.count, 2);
assert_eq!(s.finite_count, 0);
assert_eq!(s.min, None);
assert_eq!(s.com, ComCoord::NONE);
}
#[test]
fn curve_drops_non_finite_x() {
let s = Stats::for_curve(&[f64::NAN, 3.0], &[10.0, 4.0], StatScope::All);
assert_eq!(s.finite_count, 1);
approx(s.sum.unwrap(), 4.0);
approx(s.coord_max.x.unwrap(), 3.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.finite_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.finite_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.finite_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.finite_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.finite_count, 2);
approx(s.sum.unwrap(), 5.0);
}
#[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.finite_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.finite_count, 4);
assert_eq!(s.com, ComCoord::NONE);
}
#[test]
fn image_skips_non_finite() {
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.finite_count, 2);
approx(s.sum.unwrap(), 4.0);
approx(s.max.unwrap(), 3.0);
}
#[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.finite_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.finite_count, 0);
}
}