1use std::f64::consts::{FRAC_PI_4, PI};
21
22const 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];
33const 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
45fn 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
70fn 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
91fn 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#[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#[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
202fn 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
242fn 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}