use ndarray::ArrayView1;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WindowMode {
Circular,
Square,
}
impl WindowMode {
pub fn parse(value: &str) -> Result<Self, String> {
match value.trim().to_ascii_lowercase().as_str() {
"square" => Ok(Self::Square),
"circular" => Ok(Self::Circular),
other => Err(format!(
"Invalid window_mode '{other}'. Expected 'square' or 'circular'."
)),
}
}
}
#[inline]
pub(crate) fn square_frac(
r0: f64,
r1: f64,
c0: f64,
c1: f64,
center_r: f64,
center_c: f64,
inner: f64,
outer: f64,
) -> f32 {
let footprint_area = (r1 - r0) * (c1 - c0);
if footprint_area <= 0.0 {
return 0.0;
}
let outer_area = square_overlap(r0, r1, c0, c1, center_r, center_c, outer);
if outer_area <= 0.0 {
return 0.0;
}
let inner_area = square_overlap(r0, r1, c0, c1, center_r, center_c, inner);
let annulus_area = (outer_area - inner_area).max(0.0);
(annulus_area / footprint_area).clamp(0.0, 1.0) as f32
}
#[inline]
fn square_overlap(
r0: f64,
r1: f64,
c0: f64,
c1: f64,
center_r: f64,
center_c: f64,
half_extent: f64,
) -> f64 {
if half_extent <= 0.0 {
return 0.0;
}
let sr0 = center_r - half_extent;
let sr1 = center_r + half_extent;
let sc0 = center_c - half_extent;
let sc1 = center_c + half_extent;
let rows = r1.min(sr1) - r0.max(sr0);
if rows <= 0.0 {
return 0.0;
}
let cols = c1.min(sc1) - c0.max(sc0);
if cols <= 0.0 {
return 0.0;
}
rows * cols
}
#[inline]
pub(crate) fn circle_frac(
r0: f64,
r1: f64,
c0: f64,
c1: f64,
center_r: f64,
center_c: f64,
inner: f64,
outer: f64,
) -> (f32, bool) {
let footprint_area = (r1 - r0) * (c1 - c0);
if footprint_area <= 0.0 {
return (0.0, false);
}
let outer_area = circle_overlap(r0, r1, c0, c1, center_r, center_c, outer);
if outer_area <= 0.0 {
return (0.0, false);
}
let inner_area = circle_overlap(r0, r1, c0, c1, center_r, center_c, inner);
let annulus_area = (outer_area - inner_area).max(0.0);
let fraction = (annulus_area / footprint_area).clamp(0.0, 1.0) as f32;
let is_outer_edge =
annulus_area > 0.0 && crosses_circle(r0, r1, c0, c1, center_r, center_c, outer);
(fraction, is_outer_edge)
}
#[inline]
fn circle_overlap(
r0: f64,
r1: f64,
c0: f64,
c1: f64,
center_r: f64,
center_c: f64,
radius: f64,
) -> f64 {
let rect_area = (r1 - r0) * (c1 - c0);
if rect_area <= 0.0 || radius <= 0.0 {
return 0.0;
}
let x0 = c0 - center_c;
let x1 = c1 - center_c;
let y0 = r0 - center_r;
let y1 = r1 - center_r;
let radius_sq = radius * radius;
if min_dist_sq(x0, x1, y0, y1) >= radius_sq {
return 0.0;
}
if max_dist_sq(x0, x1, y0, y1) <= radius_sq {
return rect_area;
}
let x_parts = split_at_zero(x0, x1);
let y_parts = split_at_zero(y0, y1);
let mut area = 0.0;
for &(xa, xb) in &x_parts {
if xb <= xa {
continue;
}
let (qx0, qx1) = if xb <= 0.0 { (-xb, -xa) } else { (xa, xb) };
for &(ya, yb) in &y_parts {
if yb <= ya {
continue;
}
let (qy0, qy1) = if yb <= 0.0 { (-yb, -ya) } else { (ya, yb) };
area += q1_rect_area(qx0, qx1, qy0, qy1, radius);
}
}
area.clamp(0.0, rect_area)
}
#[inline]
fn q1_rect_area(x0: f64, x1: f64, y0: f64, y1: f64, radius: f64) -> f64 {
if x1 <= x0 || y1 <= y0 {
return 0.0;
}
q1_corner_area(x1, y1, radius) - q1_corner_area(x0, y1, radius) - q1_corner_area(x1, y0, radius)
+ q1_corner_area(x0, y0, radius)
}
#[inline]
fn q1_corner_area(x: f64, y: f64, radius: f64) -> f64 {
if x <= 0.0 || y <= 0.0 || radius <= 0.0 {
return 0.0;
}
let x = x.min(radius);
if x <= 0.0 {
return 0.0;
}
if y >= radius {
return arc_integral(x, radius);
}
let x_flat = (radius * radius - y * y).max(0.0).sqrt();
let rect_width = x.min(x_flat);
let mut area = rect_width * y;
if x > x_flat {
area += arc_integral(x, radius) - arc_integral(x_flat, radius);
}
area
}
#[inline]
fn arc_integral(x: f64, radius: f64) -> f64 {
let x = x.clamp(0.0, radius);
let y = (radius * radius - x * x).max(0.0).sqrt();
let angle = (x / radius).clamp(-1.0, 1.0).asin();
0.5 * (x * y + radius * radius * angle)
}
#[inline]
fn split_at_zero(a: f64, b: f64) -> [(f64, f64); 2] {
if a < 0.0 && b > 0.0 {
[(a, 0.0), (0.0, b)]
} else {
[(a, b), (0.0, 0.0)]
}
}
#[inline]
fn crosses_circle(
r0: f64,
r1: f64,
c0: f64,
c1: f64,
center_r: f64,
center_c: f64,
radius: f64,
) -> bool {
if radius <= 0.0 {
return false;
}
let x0 = c0 - center_c;
let x1 = c1 - center_c;
let y0 = r0 - center_r;
let y1 = r1 - center_r;
let radius_sq = radius * radius;
let eps = (radius_sq.abs() + 1.0) * 1.0e-12;
min_dist_sq(x0, x1, y0, y1) <= radius_sq + eps && max_dist_sq(x0, x1, y0, y1) >= radius_sq - eps
}
#[inline]
fn min_dist_sq(x0: f64, x1: f64, y0: f64, y1: f64) -> f64 {
let dx = if x0 > 0.0 {
x0
} else if x1 < 0.0 {
-x1
} else {
0.0
};
let dy = if y0 > 0.0 {
y0
} else if y1 < 0.0 {
-y1
} else {
0.0
};
dx * dx + dy * dy
}
#[inline]
fn max_dist_sq(x0: f64, x1: f64, y0: f64, y1: f64) -> f64 {
let dx = x0.abs().max(x1.abs());
let dy = y0.abs().max(y1.abs());
dx * dx + dy * dy
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rect_inside_disk() {
let area = circle_overlap(9.0, 11.0, 9.0, 11.0, 10.0, 10.0, 3.0);
assert!((area - 4.0).abs() < 1.0e-12);
}
#[test]
fn quarter_disk() {
let radius = 3.0;
let area = circle_overlap(0.0, radius, 0.0, radius, 0.0, 0.0, radius);
let expected = std::f64::consts::PI * radius * radius * 0.25;
assert!((area - expected).abs() < 1.0e-12);
}
#[test]
fn sims_match_scalar() {
let mut rng = crate::testkit::Rng::new(3);
let value = |rng: &mut crate::testkit::Rng| match rng.below(40) {
0 => f32::NAN,
1 => f32::INFINITY,
_ => (rng.unit() * 4.0 - 2.0) as f32,
};
for _ in 0..2000 {
let bands = 1 + rng.below(25);
let current: Vec<f32> = (0..bands).map(|_| value(&mut rng)).collect();
let n_scen = 1 + rng.below(12);
let cells: Vec<Vec<f32>> = (0..n_scen)
.map(|_| {
let len = if rng.chance(0.1) { rng.below(30) } else { bands };
(0..len).map(|_| value(&mut rng)).collect()
})
.collect();
let refs: Vec<&[f32]> = cells.iter().map(|c| c.as_slice()).collect();
let mut out = Vec::new();
for group in refs.chunks(8) {
similarities(¤t, group, f32::NAN, &mut out);
}
for (k, cell) in cells.iter().enumerate() {
let expected = similarity(&ArrayView1::from(¤t[..]), &ArrayView1::from(&cell[..]));
assert_eq!(out[k].to_bits(), expected.unwrap_or(f32::NAN).to_bits());
}
}
}
#[test]
fn flags_outer_rim_only() {
let (_, outer_edge) = circle_frac(-0.5, 0.5, 2.5, 3.5, 0.0, 0.0, 1.0, 3.0);
assert!(outer_edge);
let (_, inner_edge) = circle_frac(-0.5, 0.5, 0.5, 1.5, 0.0, 0.0, 1.0, 3.0);
assert!(!inner_edge);
}
}
#[inline]
pub(crate) fn similarity(a: &ArrayView1<f32>, b: &ArrayView1<f32>) -> Option<f32> {
let mut l1: f32 = 0.0;
for (&x, &y) in a.iter().zip(b.iter()) {
if x.is_finite() && y.is_finite() {
l1 += (x - y).abs();
} else {
return None;
}
}
Some((-l1).exp())
}
#[inline]
pub(crate) fn similarities(current: &[f32], cells: &[&[f32]], missing: f32, out: &mut Vec<f32>) {
const LANES: usize = 8;
for group in cells.chunks(LANES) {
let n = current.len().min(group[0].len());
if group.iter().any(|cell| cell.len().min(current.len()) != n) {
for cell in group {
let sim = similarity(&ArrayView1::from(current), &ArrayView1::from(*cell));
out.push(sim.unwrap_or(missing));
}
continue;
}
let mut l1 = [0.0f32; LANES];
let mut finite = [true; LANES];
for (b, &x) in current[..n].iter().enumerate() {
for (k, cell) in group.iter().enumerate() {
let y = cell[b];
finite[k] &= x.is_finite() & y.is_finite();
l1[k] += (x - y).abs();
}
}
for k in 0..group.len() {
out.push(if finite[k] { (-l1[k]).exp() } else { missing });
}
}
}