1use ndarray::ArrayView1;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum WindowMode {
7 Circular,
9 Square,
11}
12
13impl WindowMode {
14 pub fn parse(value: &str) -> Result<Self, String> {
17 match value.trim().to_ascii_lowercase().as_str() {
18 "square" => Ok(Self::Square),
19 "circular" => Ok(Self::Circular),
20 other => Err(format!(
21 "Invalid window_mode '{other}'. Expected 'square' or 'circular'."
22 )),
23 }
24 }
25}
26
27#[inline]
28pub(crate) fn square_frac(
29 r0: f64,
30 r1: f64,
31 c0: f64,
32 c1: f64,
33 center_r: f64,
34 center_c: f64,
35 inner: f64,
36 outer: f64,
37) -> f32 {
38 let footprint_area = (r1 - r0) * (c1 - c0);
39 if footprint_area <= 0.0 {
40 return 0.0;
41 }
42
43 let outer_area = square_overlap(r0, r1, c0, c1, center_r, center_c, outer);
44 if outer_area <= 0.0 {
45 return 0.0;
46 }
47
48 let inner_area = square_overlap(r0, r1, c0, c1, center_r, center_c, inner);
49 let annulus_area = (outer_area - inner_area).max(0.0);
50 (annulus_area / footprint_area).clamp(0.0, 1.0) as f32
51}
52
53#[inline]
54fn square_overlap(
55 r0: f64,
56 r1: f64,
57 c0: f64,
58 c1: f64,
59 center_r: f64,
60 center_c: f64,
61 half_extent: f64,
62) -> f64 {
63 if half_extent <= 0.0 {
64 return 0.0;
65 }
66
67 let sr0 = center_r - half_extent;
68 let sr1 = center_r + half_extent;
69 let sc0 = center_c - half_extent;
70 let sc1 = center_c + half_extent;
71
72 let rows = r1.min(sr1) - r0.max(sr0);
73 if rows <= 0.0 {
74 return 0.0;
75 }
76
77 let cols = c1.min(sc1) - c0.max(sc0);
78 if cols <= 0.0 {
79 return 0.0;
80 }
81
82 rows * cols
83}
84
85#[inline]
86pub(crate) fn circle_frac(
87 r0: f64,
88 r1: f64,
89 c0: f64,
90 c1: f64,
91 center_r: f64,
92 center_c: f64,
93 inner: f64,
94 outer: f64,
95) -> (f32, bool) {
96 let footprint_area = (r1 - r0) * (c1 - c0);
97 if footprint_area <= 0.0 {
98 return (0.0, false);
99 }
100
101 let outer_area = circle_overlap(r0, r1, c0, c1, center_r, center_c, outer);
102 if outer_area <= 0.0 {
103 return (0.0, false);
104 }
105
106 let inner_area = circle_overlap(r0, r1, c0, c1, center_r, center_c, inner);
107 let annulus_area = (outer_area - inner_area).max(0.0);
108 let fraction = (annulus_area / footprint_area).clamp(0.0, 1.0) as f32;
109 let is_outer_edge =
110 annulus_area > 0.0 && crosses_circle(r0, r1, c0, c1, center_r, center_c, outer);
111
112 (fraction, is_outer_edge)
113}
114
115#[inline]
116fn circle_overlap(
117 r0: f64,
118 r1: f64,
119 c0: f64,
120 c1: f64,
121 center_r: f64,
122 center_c: f64,
123 radius: f64,
124) -> f64 {
125 let rect_area = (r1 - r0) * (c1 - c0);
126 if rect_area <= 0.0 || radius <= 0.0 {
127 return 0.0;
128 }
129
130 let x0 = c0 - center_c;
131 let x1 = c1 - center_c;
132 let y0 = r0 - center_r;
133 let y1 = r1 - center_r;
134 let radius_sq = radius * radius;
135
136 if min_dist_sq(x0, x1, y0, y1) >= radius_sq {
137 return 0.0;
138 }
139 if max_dist_sq(x0, x1, y0, y1) <= radius_sq {
140 return rect_area;
141 }
142
143 let x_parts = split_at_zero(x0, x1);
144 let y_parts = split_at_zero(y0, y1);
145 let mut area = 0.0;
146
147 for &(xa, xb) in &x_parts {
148 if xb <= xa {
149 continue;
150 }
151 let (qx0, qx1) = if xb <= 0.0 { (-xb, -xa) } else { (xa, xb) };
152
153 for &(ya, yb) in &y_parts {
154 if yb <= ya {
155 continue;
156 }
157 let (qy0, qy1) = if yb <= 0.0 { (-yb, -ya) } else { (ya, yb) };
158 area += q1_rect_area(qx0, qx1, qy0, qy1, radius);
159 }
160 }
161
162 area.clamp(0.0, rect_area)
163}
164
165#[inline]
166fn q1_rect_area(x0: f64, x1: f64, y0: f64, y1: f64, radius: f64) -> f64 {
167 if x1 <= x0 || y1 <= y0 {
168 return 0.0;
169 }
170
171 q1_corner_area(x1, y1, radius) - q1_corner_area(x0, y1, radius) - q1_corner_area(x1, y0, radius)
172 + q1_corner_area(x0, y0, radius)
173}
174
175#[inline]
176fn q1_corner_area(x: f64, y: f64, radius: f64) -> f64 {
177 if x <= 0.0 || y <= 0.0 || radius <= 0.0 {
178 return 0.0;
179 }
180
181 let x = x.min(radius);
182 if x <= 0.0 {
183 return 0.0;
184 }
185
186 if y >= radius {
187 return arc_integral(x, radius);
188 }
189
190 let x_flat = (radius * radius - y * y).max(0.0).sqrt();
191 let rect_width = x.min(x_flat);
192 let mut area = rect_width * y;
193 if x > x_flat {
194 area += arc_integral(x, radius) - arc_integral(x_flat, radius);
195 }
196
197 area
198}
199
200#[inline]
201fn arc_integral(x: f64, radius: f64) -> f64 {
202 let x = x.clamp(0.0, radius);
203 let y = (radius * radius - x * x).max(0.0).sqrt();
204 let angle = (x / radius).clamp(-1.0, 1.0).asin();
205 0.5 * (x * y + radius * radius * angle)
206}
207
208#[inline]
209fn split_at_zero(a: f64, b: f64) -> [(f64, f64); 2] {
210 if a < 0.0 && b > 0.0 {
211 [(a, 0.0), (0.0, b)]
212 } else {
213 [(a, b), (0.0, 0.0)]
214 }
215}
216
217#[inline]
218fn crosses_circle(
219 r0: f64,
220 r1: f64,
221 c0: f64,
222 c1: f64,
223 center_r: f64,
224 center_c: f64,
225 radius: f64,
226) -> bool {
227 if radius <= 0.0 {
228 return false;
229 }
230
231 let x0 = c0 - center_c;
232 let x1 = c1 - center_c;
233 let y0 = r0 - center_r;
234 let y1 = r1 - center_r;
235 let radius_sq = radius * radius;
236 let eps = (radius_sq.abs() + 1.0) * 1.0e-12;
237
238 min_dist_sq(x0, x1, y0, y1) <= radius_sq + eps && max_dist_sq(x0, x1, y0, y1) >= radius_sq - eps
239}
240
241#[inline]
242fn min_dist_sq(x0: f64, x1: f64, y0: f64, y1: f64) -> f64 {
243 let dx = if x0 > 0.0 {
244 x0
245 } else if x1 < 0.0 {
246 -x1
247 } else {
248 0.0
249 };
250 let dy = if y0 > 0.0 {
251 y0
252 } else if y1 < 0.0 {
253 -y1
254 } else {
255 0.0
256 };
257 dx * dx + dy * dy
258}
259
260#[inline]
261fn max_dist_sq(x0: f64, x1: f64, y0: f64, y1: f64) -> f64 {
262 let dx = x0.abs().max(x1.abs());
263 let dy = y0.abs().max(y1.abs());
264 dx * dx + dy * dy
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270
271 #[test]
272 fn rect_inside_disk() {
273 let area = circle_overlap(9.0, 11.0, 9.0, 11.0, 10.0, 10.0, 3.0);
274 assert!((area - 4.0).abs() < 1.0e-12);
275 }
276
277 #[test]
278 fn quarter_disk() {
279 let radius = 3.0;
280 let area = circle_overlap(0.0, radius, 0.0, radius, 0.0, 0.0, radius);
281 let expected = std::f64::consts::PI * radius * radius * 0.25;
282 assert!((area - expected).abs() < 1.0e-12);
283 }
284
285 #[test]
287 fn sims_match_scalar() {
288 let mut rng = crate::testkit::Rng::new(3);
289 let value = |rng: &mut crate::testkit::Rng| match rng.below(40) {
290 0 => f32::NAN,
291 1 => f32::INFINITY,
292 _ => (rng.unit() * 4.0 - 2.0) as f32,
293 };
294 for _ in 0..2000 {
295 let bands = 1 + rng.below(25);
296 let current: Vec<f32> = (0..bands).map(|_| value(&mut rng)).collect();
297 let n_scen = 1 + rng.below(12);
298 let cells: Vec<Vec<f32>> = (0..n_scen)
299 .map(|_| {
300 let len = if rng.chance(0.1) { rng.below(30) } else { bands };
301 (0..len).map(|_| value(&mut rng)).collect()
302 })
303 .collect();
304 let refs: Vec<&[f32]> = cells.iter().map(|c| c.as_slice()).collect();
305 let mut out = Vec::new();
306 for group in refs.chunks(8) {
307 similarities(¤t, group, f32::NAN, &mut out);
308 }
309 for (k, cell) in cells.iter().enumerate() {
310 let expected = similarity(&ArrayView1::from(¤t[..]), &ArrayView1::from(&cell[..]));
311 assert_eq!(out[k].to_bits(), expected.unwrap_or(f32::NAN).to_bits());
312 }
313 }
314 }
315
316 #[test]
317 fn flags_outer_rim_only() {
318 let (_, outer_edge) = circle_frac(-0.5, 0.5, 2.5, 3.5, 0.0, 0.0, 1.0, 3.0);
319 assert!(outer_edge);
320
321 let (_, inner_edge) = circle_frac(-0.5, 0.5, 0.5, 1.5, 0.0, 0.0, 1.0, 3.0);
322 assert!(!inner_edge);
323 }
324}
325
326#[inline]
330pub(crate) fn similarity(a: &ArrayView1<f32>, b: &ArrayView1<f32>) -> Option<f32> {
331 let mut l1: f32 = 0.0;
332
333 for (&x, &y) in a.iter().zip(b.iter()) {
335 if x.is_finite() && y.is_finite() {
336 l1 += (x - y).abs();
337 } else {
338 return None;
339 }
340 }
341
342 Some((-l1).exp())
343}
344
345#[inline]
350pub(crate) fn similarities(current: &[f32], cells: &[&[f32]], missing: f32, out: &mut Vec<f32>) {
351 const LANES: usize = 8;
352 for group in cells.chunks(LANES) {
353 let n = current.len().min(group[0].len());
354 if group.iter().any(|cell| cell.len().min(current.len()) != n) {
355 for cell in group {
356 let sim = similarity(&ArrayView1::from(current), &ArrayView1::from(*cell));
357 out.push(sim.unwrap_or(missing));
358 }
359 continue;
360 }
361 let mut l1 = [0.0f32; LANES];
362 let mut finite = [true; LANES];
363 for (b, &x) in current[..n].iter().enumerate() {
364 for (k, cell) in group.iter().enumerate() {
365 let y = cell[b];
366 finite[k] &= x.is_finite() & y.is_finite();
367 l1[k] += (x - y).abs();
368 }
369 }
370 for k in 0..group.len() {
371 out.push(if finite[k] { (-l1[k]).exp() } else { missing });
372 }
373 }
374}