|
31 | 31 | constants, gradients flow smoothly through the table values. |
32 | 32 | """ |
33 | 33 |
|
| 34 | +import math |
| 35 | + |
34 | 36 | import numpy as np |
35 | 37 | from functools import partial |
36 | 38 | from typing import Optional |
37 | 39 |
|
38 | 40 | from autonerves import cached_property |
39 | 41 |
|
| 42 | +from autoarray import numba_util |
| 43 | + |
40 | 44 | from autoarray.inversion.mesh.interpolator.abstract import AbstractInterpolator |
41 | 45 |
|
42 | 46 |
|
@@ -84,6 +88,43 @@ def reverse_interp_np(xp, yp, x): |
84 | 88 |
|
85 | 89 | _SQRT2 = np.sqrt(2.0) |
86 | 90 |
|
| 91 | +# Phi(t) saturates to exactly-representable 0/1 contributions in fp64 well |
| 92 | +# inside |t| = 9: the dropped tail terms are < 1e-19 of the weight sum, far |
| 93 | +# below the ~1e-13 accumulation noise of the blocked numpy sum the windowed |
| 94 | +# numba kernel replaces (measured max deviation 1e-13 on the hst fiducial). |
| 95 | +_KERNEL_CDF_SATURATION_T = 9.0 |
| 96 | + |
| 97 | + |
| 98 | +@numba_util.jit() |
| 99 | +def _kernel_cdf_dim_windowed(p_sorted, w_sorted, w_prefix, h_d, q, T): |
| 100 | + """ |
| 101 | + Exact 1D weighted kernel CDF ``F(q) = sum_i w_i Phi((q - p_i) / h)`` for |
| 102 | + one dimension, evaluated with a saturation window over sorted points. |
| 103 | +
|
| 104 | + Points below ``q - T h`` contribute exactly their weight (prefix sum); |
| 105 | + points above ``q + T h`` contribute zero; only the window is summed with |
| 106 | + ``erfc``. Replaces the O(M x N) blocked numpy broadcast on the numpy path |
| 107 | + — same values to ~1e-13 (see ``_KERNEL_CDF_SATURATION_T``) at ~3x the |
| 108 | + speed and none of the ~126 MB per-block temporaries; the blocked numpy |
| 109 | + implementation remains the JAX-path/differentiable reference. |
| 110 | +
|
| 111 | + Sorting note: the module docstring's "no sorts anywhere" invariant is a |
| 112 | + JAX-differentiability guarantee. This kernel runs only on the ``xp is |
| 113 | + np`` branch, which carries no gradients — the sort is an internal |
| 114 | + evaluation order and the returned VALUES are those of the sort-free sum. |
| 115 | + """ |
| 116 | + out = np.empty(q.shape[0]) |
| 117 | + inv = 1.0 / (h_d * 1.4142135623730951) |
| 118 | + for m in range(q.shape[0]): |
| 119 | + qm = q[m] |
| 120 | + a = np.searchsorted(p_sorted, qm - T * h_d) |
| 121 | + b = np.searchsorted(p_sorted, qm + T * h_d) |
| 122 | + acc = w_prefix[a] |
| 123 | + for i in range(a, b): |
| 124 | + acc += w_sorted[i] * 0.5 * math.erfc((p_sorted[i] - qm) * inv) |
| 125 | + out[m] = acc |
| 126 | + return out |
| 127 | + |
87 | 128 |
|
88 | 129 | def _norm_cdf(t, xp): |
89 | 130 | """Standard normal CDF, xp-aware (scipy erf on numpy, jax.scipy under jax).""" |
@@ -161,15 +202,32 @@ def F_raw(q): |
161 | 202 | return out.reshape(n_blocks * KERNEL_FORWARD_BLOCK, 2)[:M] |
162 | 203 |
|
163 | 204 | else: |
| 205 | + # numpy fast path: per-dimension sorted points + weight prefix sums, |
| 206 | + # evaluated by the windowed numba kernel. Same values as the blocked |
| 207 | + # broadcast above to ~1e-13; that implementation stays as the JAX |
| 208 | + # branch and the differentiable reference. |
| 209 | + _p_sorted = [] |
| 210 | + _w_sorted = [] |
| 211 | + _w_prefix = [] |
| 212 | + for _d in range(2): |
| 213 | + _order = np.argsort(points[:, _d], kind="stable") |
| 214 | + _p_sorted.append(np.ascontiguousarray(np.asarray(points)[_order, _d])) |
| 215 | + _w_sorted.append(np.ascontiguousarray(np.asarray(w)[_order])) |
| 216 | + _w_prefix.append(np.concatenate([[0.0], np.cumsum(_w_sorted[_d])])) |
164 | 217 |
|
165 | 218 | def F_raw(q): |
166 | | - return np.concatenate( |
167 | | - [ |
168 | | - F_raw_block(q[i : i + KERNEL_FORWARD_BLOCK]) |
169 | | - for i in range(0, q.shape[0], KERNEL_FORWARD_BLOCK) |
170 | | - ], |
171 | | - axis=0, |
172 | | - ) |
| 219 | + q = np.asarray(q) |
| 220 | + out = np.empty_like(q) |
| 221 | + for d in range(2): |
| 222 | + out[:, d] = _kernel_cdf_dim_windowed( |
| 223 | + _p_sorted[d], |
| 224 | + _w_sorted[d], |
| 225 | + _w_prefix[d], |
| 226 | + float(h[d]), |
| 227 | + np.ascontiguousarray(q[:, d]), |
| 228 | + _KERNEL_CDF_SATURATION_T, |
| 229 | + ) |
| 230 | + return out |
173 | 231 |
|
174 | 232 | # The unit square maps onto the data bounding box exactly (the kernel |
175 | 233 | # tails outside [lo, hi] are absorbed by the rescale). |
|
0 commit comments