pub const SIGMA_THRESHOLD: f64 = 3.0;
pub const NULL_OVER_3_SIGMA_RATE: f64 = 0.0027;
pub const MAX_CLUSTER_PIXELS: usize = 6;
#[derive(Debug, Clone, Copy, Default)]
pub struct Welford {
pub n: u64,
pub mean: f64,
pub m2: f64,
}
impl Welford {
pub fn update(&mut self, x: f64) {
self.n += 1;
let delta = x - self.mean;
self.mean += delta / self.n as f64;
let delta2 = x - self.mean;
self.m2 = delta.mul_add(delta2, self.m2);
}
#[cfg(test)]
#[must_use]
pub fn from_moments(n: u64, mean: f64, sum: f64, sum_sq: f64) -> Self {
let n_f = n as f64;
let m2 = (n_f * mean).mul_add(mean, 2.0f64.mul_add(-mean * sum, sum_sq));
Self {
n,
mean,
m2: m2.max(0.0),
}
}
#[must_use]
pub fn variance(&self) -> f64 {
if self.n < 2 {
0.0
} else {
self.m2 / (self.n as f64 - 1.0)
}
}
#[must_use]
pub fn standard_error_sq(&self) -> f64 {
if self.n == 0 {
0.0
} else {
self.variance() / self.n as f64
}
}
}
#[must_use]
pub fn z_score(a: &Welford, b: &Welford) -> f64 {
let se2 = a.standard_error_sq() + b.standard_error_sq();
if se2 <= 0.0 {
return 0.0;
}
(b.mean - a.mean) / se2.sqrt()
}
#[must_use]
pub fn passes_z_criteria(
over_sigma_count: usize,
total_pixels: usize,
expected_rate: f64,
cluster_sizes: &[usize],
) -> bool {
let expected_count = expected_rate * total_pixels as f64;
let observed_ok = (over_sigma_count as f64) <= (expected_count * 5.0).max(10.0);
let largest = cluster_sizes.first().copied().unwrap_or(0);
observed_ok && largest <= MAX_CLUSTER_PIXELS
}
#[must_use]
pub fn connected_components(z_grid: &[f64], width: u32, height: u32, threshold: f64) -> Vec<usize> {
let w = width as usize;
let h = height as usize;
let mut visited = vec![false; z_grid.len()];
let mut sizes = Vec::new();
let mut stack = Vec::new();
for start in 0..z_grid.len() {
if visited[start] || z_grid[start].abs() <= threshold {
continue;
}
stack.push(start);
visited[start] = true;
let mut size = 0usize;
while let Some(idx) = stack.pop() {
size += 1;
let x = idx % w;
let y = idx / w;
let neighbors = [
(x.checked_sub(1), Some(y)),
(Some(x + 1).filter(|&nx| nx < w), Some(y)),
(Some(x), y.checked_sub(1)),
(Some(x), Some(y + 1).filter(|&ny| ny < h)),
];
for (nx, ny) in neighbors {
if let (Some(nx), Some(ny)) = (nx, ny) {
let nidx = ny * w + nx;
if !visited[nidx] && z_grid[nidx].abs() > threshold {
visited[nidx] = true;
stack.push(nidx);
}
}
}
}
sizes.push(size);
}
sizes.sort_unstable_by(|a, b| b.cmp(a));
sizes
}