use crate::{
geometry::Box4, geometry_risk::quantile, geometry_style::Prepared, median,
statistics::ordered_clusters,
};
use std::collections::HashMap;
pub struct World {
pub tight: Box4,
pub canonical: bool,
pub y_eligible: bool,
}
pub type Summary = (Vec<((i64, i64), Box4)>, Vec<((i64, i64), f64)>, bool);
fn union(boxes: impl Iterator<Item = Box4>) -> Option<Box4> {
let mut boxes = boxes;
let mut result = boxes.next()?;
for bbox in boxes {
for axis in 0..4 {
if (axis < 2 && bbox[axis] < result[axis]) || (axis >= 2 && bbox[axis] > result[axis]) {
result[axis] = bbox[axis];
}
}
}
Some(result)
}
fn width_sum(values: impl Iterator<Item = f64>, compensated: bool) -> Option<f64> {
let (mut hi, mut lo) = (0.0_f64, 0.0_f64);
for value in values {
let next = hi + value;
if compensated {
lo += if hi.abs() >= value.abs() {
(hi - next) + value
} else {
(value - next) + hi
};
}
hi = next;
if !hi.is_finite() || !lo.is_finite() {
return None;
}
}
let total = if compensated && lo != 0.0 {
hi + lo
} else {
hi
};
total.is_finite().then_some(total)
}
pub fn summarize(prepared: &Prepared, world: &[World], compensated: bool) -> Option<Summary> {
if world.len() != prepared.samples.len()
|| world.iter().any(|w| w.tight.iter().any(|v| !v.is_finite()))
{
return None;
}
let samples = &prepared.samples;
let mut inks = Vec::new();
let mut baselines = Vec::new();
let mut ratios = Vec::new();
let mut extremes = HashMap::<usize, usize>::new();
let mut y_risk = false;
for (key, line) in prepared.line_keys.iter().zip(&prepared.lines) {
let Some(&first) = line.first() else {
continue;
};
inks.push((*key, union(line.iter().map(|i| world[*i].tight))?));
let anchors: Vec<_> = line
.iter()
.copied()
.filter(|i| samples[*i].anchor)
.collect();
let canonical = world[first].canonical && anchors.len() >= 2;
let risk = !y_risk && world[first].y_eligible && anchors.len() >= 4;
if !canonical && !risk {
continue;
}
let heights: Vec<_> = anchors
.iter()
.map(|i| samples[*i].tight[3] - samples[*i].tight[1])
.collect();
let tolerance = 0.5_f64.max(0.25 * quantile(heights, 0.75));
let groups = ordered_clusters(
anchors.iter().map(|i| samples[*i].origin[1]).collect(),
tolerance,
0.0,
false,
)?;
if canonical {
let mut best = None;
for group in &groups {
let width = width_sum(
group.iter().map(|i| {
let b = samples[anchors[*i]].tight;
b[2] - b[0]
}),
compensated,
)?;
let score = (width, group.len());
if best.as_ref().is_none_or(|(previous, _)| score > *previous) {
best = Some((score, group));
}
}
if let Some((_, dominant)) = best {
if dominant.len() as f64 / anchors.len() as f64 >= 0.5 {
baselines.push((
*key,
median(
dominant
.iter()
.map(|i| samples[anchors[*i]].origin[1])
.collect(),
),
));
}
}
}
if !risk {
continue;
}
if groups
.iter()
.filter(|g| g.len() >= 3 && g.len() as f64 / anchors.len() as f64 >= 0.20)
.count()
>= 2
{
y_risk = true;
continue;
}
let mut dominant = &groups[0];
for group in &groups[1..] {
if group.len() > dominant.len() {
dominant = group;
}
}
if dominant.len() as f64 / (anchors.len() as f64) < 0.80 {
continue;
}
let source = union(dominant.iter().map(|i| samples[anchors[*i]].source))?;
let tight = union(dominant.iter().map(|i| samples[anchors[*i]].tight))?;
let height = tight[3] - tight[1];
if height <= 0.0 {
continue;
}
let ratio = (source[3] - source[1]) / height;
if !ratio.is_finite() {
return None;
}
ratios.push(ratio);
if ratio >= 3.0 {
let mut counts = HashMap::<usize, usize>::new();
for i in dominant {
*counts.entry(samples[anchors[*i]].run).or_default() += 1;
}
let mut run = samples[anchors[dominant[0]]].run;
for i in dominant {
let candidate = samples[anchors[*i]].run;
if counts[&candidate] > counts[&run] {
run = candidate;
}
}
*extremes.entry(run).or_default() += 1;
}
}
y_risk |= !ratios.is_empty()
&& (quantile(ratios, 0.95) >= 2.20 || extremes.values().any(|n| *n >= 3));
Some((inks, baselines, y_risk))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn width_sum_matches_python_versions() {
assert_eq!(width_sum([1e16, 1.0, 1.0].into_iter(), false), Some(1e16));
assert_eq!(
width_sum([1e16, 1.0, 1.0].into_iter(), true),
Some(1e16 + 2.0)
);
assert_eq!(width_sum([f64::MAX, f64::MAX].into_iter(), true), None);
}
#[test]
fn union_preserves_first_equal_coordinate() {
let result = union([[0.0, -0.0, 1.0, 2.0], [-0.0, 0.0, 1.0, 2.0]].into_iter()).unwrap();
assert_eq!(result[0].to_bits(), 0.0_f64.to_bits());
assert_eq!(result[1].to_bits(), (-0.0_f64).to_bits());
}
}