Skip to main content

nereids_physics/
detector_spread.rs

1//! Counts the detector records away from where they arrive.
2//!
3//! The detector is a grid of square cells of side 1.  Counts arrive evenly
4//! over a cell, and the share of them the detector spreads is recorded at a
5//! displacement `u` drawn from a radial kernel normalized over the plane,
6//!
7//! ```text
8//! K(r) = Σ_c w_c·exp(−r/s_c)/(2π s_c²),   Σ_c w_c = 1,
9//! ```
10//!
11//! lengths `s_c` in cells.  The share of a cell's spread counts recorded in the
12//! cell offset by `(dx, dy)` is
13//!
14//! ```text
15//! T(dx, dy) = ∫ K(u)·Λ(u_x − dx)·Λ(u_y − dy) du,   Λ(t) = max(0, 1 − |t|),
16//! ```
17//!
18//! `Λ(u_x − dx)·Λ(u_y − dy)` the overlap of the two cells displaced by `u`.
19
20use std::f64::consts::{FRAC_PI_4, PI};
21
22/// Eight-point Gauss–Legendre abscissae on `[−1, 1]`.
23const ABSCISSAE: [f64; 8] = [
24    -0.960_289_856_497_536_3,
25    -0.796_666_477_413_626_7,
26    -0.525_532_409_916_329,
27    -0.183_434_642_495_649_8,
28    0.183_434_642_495_649_8,
29    0.525_532_409_916_329,
30    0.796_666_477_413_626_7,
31    0.960_289_856_497_536_3,
32];
33/// Eight-point Gauss–Legendre weights on `[−1, 1]`.
34const WEIGHTS: [f64; 8] = [
35    0.101_228_536_290_376_26,
36    0.222_381_034_453_374_47,
37    0.313_706_645_877_887_3,
38    0.362_683_783_378_362,
39    0.362_683_783_378_362,
40    0.313_706_645_877_887_3,
41    0.222_381_034_453_374_47,
42    0.101_228_536_290_376_26,
43];
44
45/// `∫₀^R r^k e^(−r/s) dr` for `k` of 1, 2 or 3.
46fn moment(k: i32, reach: f64, length: f64) -> f64 {
47    let x = reach / length;
48    let order = f64::from(k + 1);
49    let gamma = if x < 1.0 {
50        let mut term = x.powi(k + 1);
51        let mut sum = term / order;
52        for j in 1..40 {
53            term *= -x / f64::from(j);
54            sum += term / (order + f64::from(j));
55        }
56        sum
57    } else {
58        let mut term = 1.0;
59        let mut partial = 1.0;
60        for j in 1..=k {
61            term *= x / f64::from(j);
62            partial += term;
63        }
64        let factorial: f64 = (1..=k).map(f64::from).product();
65        factorial * (1.0 - (-x).exp() * partial)
66    };
67    length.powi(k + 1) * gamma
68}
69
70/// `∫ exp(−r/s)/(2π s²)·(a + b·x + c·y + d·x·y)` over the unit square
71/// `[0, 1]²`, in polar coordinates about its corner at the origin.
72fn corner(length: f64, [a, b, c, d]: [f64; 4]) -> f64 {
73    let mut sum = 0.0;
74    for half in 0..2 {
75        let (lo, hi) = (f64::from(half) * FRAC_PI_4, f64::from(half + 1) * FRAC_PI_4);
76        let (mid, width) = (0.5 * (lo + hi), 0.5 * (hi - lo));
77        for (x, w) in ABSCISSAE.iter().zip(WEIGHTS) {
78            let theta = mid + width * x;
79            let (cos, sin) = (theta.cos(), theta.sin());
80            let reach = 1.0 / if half == 0 { cos } else { sin };
81            sum += width
82                * w
83                * (a * moment(1, reach, length)
84                    + (b * cos + c * sin) * moment(2, reach, length)
85                    + d * cos * sin * moment(3, reach, length));
86        }
87    }
88    sum / (2.0 * PI * length * length)
89}
90
91/// `T(dx, dy)` of an exponential kernel of length `length`, in cells.
92fn transfer_one(length: f64, [dx, dy]: [i64; 2]) -> f64 {
93    let kernel = |x: f64, y: f64| (-(x * x + y * y).sqrt() / length).exp();
94    let mut sum = 0.0;
95    for side_x in [-1, 1] {
96        for side_y in [-1, 1] {
97            let x0 = dx + i64::from(side_x.min(0));
98            let y0 = dy + i64::from(side_y.min(0));
99            let (alpha_x, beta_x) = (1.0 + f64::from(side_x) * dx as f64, f64::from(-side_x));
100            let (alpha_y, beta_y) = (1.0 + f64::from(side_y) * dy as f64, f64::from(-side_y));
101            if (x0 == 0 || x0 == -1) && (y0 == 0 || y0 == -1) {
102                let (flip_x, flip_y) = (
103                    if x0 == 0 { 1.0 } else { -1.0 },
104                    if y0 == 0 { 1.0 } else { -1.0 },
105                );
106                sum += corner(
107                    length,
108                    [
109                        alpha_x * alpha_y,
110                        beta_x * flip_x * alpha_y,
111                        alpha_x * beta_y * flip_y,
112                        beta_x * flip_x * beta_y * flip_y,
113                    ],
114                );
115            } else {
116                let mut cell = 0.0;
117                for (x, w) in ABSCISSAE.iter().zip(WEIGHTS) {
118                    let u = x0 as f64 + 0.5 * (x + 1.0);
119                    for (y, v) in ABSCISSAE.iter().zip(WEIGHTS) {
120                        let t = y0 as f64 + 0.5 * (y + 1.0);
121                        cell += 0.25
122                            * w
123                            * v
124                            * kernel(u, t)
125                            * (alpha_x + beta_x * u)
126                            * (alpha_y + beta_y * t);
127                    }
128                }
129                sum += cell / (2.0 * PI * length * length);
130            }
131        }
132    }
133    sum
134}
135
136/// `T(dx, dy)` of the kernel `(w_c, s_c)`, weights normalized by their sum.
137#[must_use]
138pub fn transfer(kernel: &[(f64, f64)], offset: [i64; 2]) -> f64 {
139    let total: f64 = kernel.iter().map(|&(w, _)| w).sum();
140    kernel
141        .iter()
142        .map(|&(w, s)| w * transfer_one(s, offset))
143        .sum::<f64>()
144        / total
145}
146
147/// `F[r][q]`, the share of the spread counts arriving evenly over the cells of
148/// region `q` that the detector records in the cells of region `r`, for the
149/// regions' cells `cells` as `[row, column]` on a grid of `grid` rows and
150/// columns.  Counts spread to cells in no region, or off the grid, are not
151/// recorded in any region.  Each region's spread image is its cells convolved
152/// with `T`, by FFT on a grid padded so that no offset wraps.
153#[must_use]
154pub fn region_shares(
155    kernel: &[(f64, f64)],
156    grid: [usize; 2],
157    cells: &[Vec<[usize; 2]>],
158) -> Vec<Vec<f64>> {
159    let [rows, columns] = grid;
160    let padded = [
161        (2 * rows - 1).next_power_of_two(),
162        (2 * columns - 1).next_power_of_two(),
163    ];
164    let quadrant: Vec<f64> = (0..rows)
165        .flat_map(|dy| (0..columns).map(move |dx| [dx as i64, dy as i64]))
166        .map(|offset| transfer(kernel, offset))
167        .collect();
168    let mut table = vec![[0.0; 2]; padded[0] * padded[1]];
169    for dy in 1 - rows as i64..rows as i64 {
170        for dx in 1 - columns as i64..columns as i64 {
171            let at = dy.rem_euclid(padded[0] as i64) as usize * padded[1]
172                + dx.rem_euclid(padded[1] as i64) as usize;
173            table[at][0] =
174                quadrant[dy.unsigned_abs() as usize * columns + dx.unsigned_abs() as usize];
175        }
176    }
177    fft2(&mut table, padded, false);
178    let scale = 1.0 / (padded[0] * padded[1]) as f64;
179    let mut shares = vec![vec![0.0; cells.len()]; cells.len()];
180    for (q, sources) in cells.iter().enumerate() {
181        let mut image = vec![[0.0; 2]; padded[0] * padded[1]];
182        for &[row, column] in sources {
183            image[row * padded[1] + column][0] = 1.0;
184        }
185        fft2(&mut image, padded, false);
186        for (a, b) in image.iter_mut().zip(&table) {
187            *a = [a[0] * b[0] - a[1] * b[1], a[0] * b[1] + a[1] * b[0]];
188        }
189        fft2(&mut image, padded, true);
190        let n = sources.len() as f64;
191        for (r, targets) in cells.iter().enumerate() {
192            let landed: f64 = targets
193                .iter()
194                .map(|&[row, column]| image[row * padded[1] + column][0])
195                .sum();
196            shares[r][q] = (scale * landed / n).max(0.0);
197        }
198    }
199    shares
200}
201
202/// The discrete Fourier transform of `data` in place, its length a power of
203/// 2, with `e^(+2πi·jk/n)` when `inverse` and without the `1/n`.
204fn fft(data: &mut [[f64; 2]], inverse: bool) {
205    let n = data.len();
206    let mut j = 0;
207    for i in 1..n {
208        let mut bit = n >> 1;
209        while j & bit != 0 {
210            j ^= bit;
211            bit >>= 1;
212        }
213        j |= bit;
214        if i < j {
215            data.swap(i, j);
216        }
217    }
218    let sign = if inverse { 1.0 } else { -1.0 };
219    let twiddles: Vec<[f64; 2]> = (0..n / 2)
220        .map(|k| {
221            let (sin, cos) = (sign * 2.0 * PI * k as f64 / n as f64).sin_cos();
222            [cos, sin]
223        })
224        .collect();
225    let mut len = 2;
226    while len <= n {
227        let stride = n / len;
228        for start in (0..n).step_by(len) {
229            for k in 0..len / 2 {
230                let [c, s] = twiddles[k * stride];
231                let [ar, ai] = data[start + k];
232                let [br, bi] = data[start + k + len / 2];
233                let (tr, ti) = (br * c - bi * s, br * s + bi * c);
234                data[start + k] = [ar + tr, ai + ti];
235                data[start + k + len / 2] = [ar - tr, ai - ti];
236            }
237        }
238        len <<= 1;
239    }
240}
241
242/// [`fft`] along both axes of `data`, `rows × columns` row by row.
243fn fft2(data: &mut [[f64; 2]], [rows, columns]: [usize; 2], inverse: bool) {
244    for row in data.chunks_mut(columns) {
245        fft(row, inverse);
246    }
247    let mut line = vec![[0.0; 2]; rows];
248    for column in 0..columns {
249        for (row, value) in line.iter_mut().enumerate() {
250            *value = data[row * columns + column];
251        }
252        fft(&mut line, inverse);
253        for (row, value) in line.iter().enumerate() {
254            data[row * columns + column] = *value;
255        }
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262
263    fn midpoint(length: f64, [dx, dy]: [i64; 2], steps: usize) -> f64 {
264        let h = 2.0 / steps as f64;
265        let mut sum = 0.0;
266        for i in 0..steps {
267            let u = dx as f64 - 1.0 + h * (i as f64 + 0.5);
268            let along_x = 1.0 - (u - dx as f64).abs();
269            for j in 0..steps {
270                let t = dy as f64 - 1.0 + h * (j as f64 + 0.5);
271                let along_y = 1.0 - (t - dy as f64).abs();
272                let r = (u * u + t * t).sqrt();
273                sum += (-r / length).exp() * along_x * along_y;
274            }
275        }
276        sum * h * h / (2.0 * PI * length * length)
277    }
278
279    #[test]
280    fn the_transfer_is_the_kernel_integral_over_the_cell_overlap() {
281        for length in [0.3, 1.0, 4.0] {
282            for offset in [[0, 0], [1, 0], [0, -1], [1, 1], [-2, 1], [5, 3]] {
283                let t = transfer(&[(1.0, length)], offset);
284                let oracle = midpoint(length, offset, 4000);
285                assert!(
286                    (t - oracle).abs() <= 1e-6 * oracle.max(1e-12),
287                    "s {length}, {offset:?}: {t} vs {oracle}"
288                );
289            }
290        }
291    }
292
293    #[test]
294    fn the_shares_are_the_sums_of_the_transfer_over_cell_pairs() {
295        let kernel = [(0.3, 0.4), (0.7, 2.0)];
296        let (rows, columns) = (7, 9);
297        let cells: Vec<Vec<[usize; 2]>> = vec![
298            vec![[0, 0], [0, 1], [1, 0]],
299            (2..5)
300                .flat_map(|row| (3..7).map(move |column| [row, column]))
301                .collect(),
302            vec![[6, 8], [5, 8], [6, 2]],
303        ];
304        let shares = region_shares(&kernel, [rows, columns], &cells);
305        for (r, targets) in cells.iter().enumerate() {
306            for (q, sources) in cells.iter().enumerate() {
307                let oracle = sources
308                    .iter()
309                    .flat_map(|&[sr, sc]| {
310                        targets.iter().map(move |&[tr, tc]| {
311                            transfer(&kernel, [tc as i64 - sc as i64, tr as i64 - sr as i64])
312                        })
313                    })
314                    .sum::<f64>()
315                    / sources.len() as f64;
316                assert!(
317                    (shares[r][q] - oracle).abs() <= 1e-13,
318                    "{r} {q}: {} vs {oracle}",
319                    shares[r][q]
320                );
321            }
322        }
323    }
324
325    #[test]
326    fn every_spread_count_lands_somewhere_on_the_plane() {
327        let kernel = [(0.3, 0.4), (0.7, 2.0)];
328        let total: f64 = (-60..=60)
329            .flat_map(|dy| (-60..=60).map(move |dx| [dx, dy]))
330            .map(|offset| transfer(&kernel, offset))
331            .sum();
332        assert!((total - 1.0).abs() <= 1e-9, "{total}");
333    }
334}