Skip to content

Commit d1863c4

Browse files
committed
feat: xp-ify ExponentialKernel + document JAX-gradient support in docstrings
ExponentialKernel's covariance build now threads xp (it previously dropped the backend at the call site, raising TracerArrayConversionError under jax.jit/jax.grad) and computes pairwise distances with the same NaN-safe sqrt(d^2 + 1e-20) dot-product identity as matern_cov_matrix_from — linalg.norm's derivative is NaN at the zero diagonal, which would poison every JAX gradient through the kernel. Verified: unit tests pass and JAX gradients are finite, non-zero and eager/jit-consistent on both the rectangular and KNN meshes. Also records the 2026-07 regularization x mesh gradient-sweep findings where users look them up — the class docstrings: which schemes are JAX-differentiable on which mesh family (analytic vs scipy neighbors, split-family compatibility), the Matern/tfp bessel_kve gradient support and its tfp-nightly requirement, the kernel schemes' explicit-inverse conditioning caveat, the adaptive-defaults-are-uniform footnote, and the Delaunay vs KNN mesh gradient split. Behavioural change is limited to ExponentialKernel's distance form (round-off level; its only unit test asserts at 1e-4 and passes unchanged). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013FSqnkgZv97PU9JdkCcthy
1 parent 64d2267 commit d1863c4

13 files changed

Lines changed: 116 additions & 6 deletions

‎autoarray/inversion/mesh/mesh/delaunay.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ def __init__(
2525
according to their barycentric distances, providing a smooth, piecewise-linear
2626
reconstruction.
2727
28+
**JAX & gradient support**: the likelihood runs under ``jax.jit``
29+
(the triangulation is host-called via ``jax.pure_callback``), but
30+
``jax.grad`` is unavailable — ``pure_callback`` has no JVP rule. The
31+
``KNearestNeighbor`` / ``KNNBarycentric`` subclasses are the
32+
gradient-capable members of this mesh family.
33+
2834
Zeroed pixels
2935
-------------
3036
The `zeroed_pixels` parameter specifies a number of mesh vertices that are

‎autoarray/inversion/mesh/mesh/knn.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,16 @@ def __init__(
2626
Neighbour connections may be further restricted using a distance-based criterion,
2727
and optionally subdivided to improve stability for highly irregular meshes.
2828
29+
**JAX & gradient support** (2026-07, FD-certified): the kNN
30+
interpolation is pure JAX (blocked brute-force ``lax.top_k`` +
31+
Wendland weights — no scipy callback), so unlike the parent
32+
``Delaunay`` mesh the full likelihood is differentiable, with
33+
gradients flowing through both the traced query points and the
34+
traced mesh vertices. Pair it with a split-family regularization
35+
(``ConstantSplit`` / ``AdaptSplit``) or a kernel scheme — the
36+
neighbor-based schemes (``Constant`` / ``Adapt``) call scipy on the
37+
traced mesh grid and cannot differentiate.
38+
2939
Parameters
3040
----------
3141
pixels : int
@@ -92,6 +102,11 @@ class KNNBarycentric(KNearestNeighbor):
92102
(``k_neighbors``, ``radius_scale``, ``split_neighbor_division``) are
93103
inherited and still control the regularization-spacing computation, but the
94104
*interpolation* weights always use k=3 + barycentric and ignore them.
105+
Gradients are FD-certified like the parent (2026-07), but this mesh
106+
FAILED its science gate as a Delaunay replacement (PyAutoArray#317 —
107+
~5% of vertices are never any query's nearest-3, drifting the
108+
log-evidence by ~2%): use it for gradient experiments, not production
109+
science.
95110
"""
96111

97112
@property

‎autoarray/inversion/regularization/adapt.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,15 @@ def __init__(
172172
173173
A full description of regularization and this matrix can be found in the parent `AbstractRegularization` class.
174174
175+
**JAX & gradient support** (2026-07 gradient sweep): as for
176+
``Constant`` — JAX-differentiable and FD-certified on the rectangular
177+
mesh family (this is the rectangular production scheme), but raises
178+
``TracerArrayConversionError`` on the Delaunay mesh family, whose
179+
neighbors come from a direct scipy call on the traced mesh grid (use
180+
``AdaptSplit`` there). Note the defaults
181+
``inner_coefficient == outer_coefficient == 1.0`` make the weighting
182+
uniform — numerically identical to ``Constant(coefficient=1.0)``.
183+
175184
Parameters
176185
----------
177186
coefficients

‎autoarray/inversion/regularization/adapt_split.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,13 @@ def __init__(
6161
6262
A full description of regularization and this matrix can be found in the parent `AbstractRegularization` class.
6363
64+
**JAX & gradient support** (2026-07 gradient sweep): as for
65+
``ConstantSplit`` — differentiable and FD-certified on the Delaunay
66+
mesh family (e.g. the KNN meshes), structurally incompatible with the
67+
rectangular meshes. Note the defaults
68+
``inner_coefficient == outer_coefficient == 1.0`` make the weighting
69+
uniform — numerically identical to ``ConstantSplit(coefficient=1.0)``.
70+
6471
Parameters
6572
----------
6673
coefficients

‎autoarray/inversion/regularization/adapt_split_zeroth.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ def __init__(
6060
6161
A full description of regularization and this matrix can be found in the parent `AbstractRegularization` class.
6262
63+
**JAX & gradient support** (2026-07 gradient sweep): not currently
64+
JAX-differentiable on either gradient-capable mesh family — the split
65+
leg is structurally incompatible with the rectangular meshes (shape
66+
error) and the zeroth/adapt legs hit numpy-on-traced-array operations
67+
on the Delaunay mesh family (``TracerArrayConversionError``).
68+
6369
Parameters
6470
----------
6571
coefficients

‎autoarray/inversion/regularization/brightness_zeroth.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ def __init__(
8282
8383
A full description of regularization and this matrix can be found in the parent `AbstractRegularization` class.
8484
85+
**JAX & gradient support** (2026-07 gradient sweep): not yet xp-ported —
86+
the pixel-signal thresholding applies numpy operations to traced
87+
arrays, raising ``TracerArrayConversionError`` under ``jax.jit`` /
88+
``jax.grad`` on every mesh.
89+
8590
Parameters
8691
----------
8792
coefficient

‎autoarray/inversion/regularization/constant.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,15 @@ def __init__(self, coefficient: float = 1.0):
8686
8787
A full description of regularization and this matrix can be found in the parent `AbstractRegularization` class.
8888
89+
**JAX & gradient support** (2026-07 gradient sweep): on meshes with an
90+
analytic neighbor structure (the rectangular family) this scheme is
91+
JAX-differentiable and FD-certified. On the Delaunay mesh family
92+
(``Delaunay``, ``KNearestNeighbor``, ``KNNBarycentric``) the neighbors
93+
come from a direct ``scipy.spatial.Delaunay`` call on the traced
94+
source-plane mesh grid, so it raises ``TracerArrayConversionError``
95+
under ``jax.jit`` / ``jax.grad`` — use a split-family scheme
96+
(e.g. ``ConstantSplit``) there instead.
97+
8998
Parameters
9099
----------
91100
coefficient

‎autoarray/inversion/regularization/constant_split.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ def __init__(self, coefficient: float = 1.0):
3535
3636
A full description of regularization and this matrix can be found in the parent `AbstractRegularization` class.
3737
38+
**JAX & gradient support** (2026-07 gradient sweep): differentiable and
39+
FD-certified on the Delaunay mesh family (e.g. the KNN meshes), whose
40+
interpolators supply pure-xp split mappings. Structurally incompatible
41+
with the rectangular meshes, whose interpolator reuses its per-query
42+
4-corner mappings for the split path (shape error) — use ``Constant``
43+
or ``Adapt`` there.
44+
3845
Parameters
3946
----------
4047
coefficient

‎autoarray/inversion/regularization/exponential_kernel.py‎

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,30 +20,40 @@ def exp_cov_matrix_from(
2020
2121
with a tiny jitter 1e-8 added on the diagonal for numerical stability.
2222
23+
The pairwise distances use the ``||x||^2 + ||y||^2 - 2 x.y`` identity with a
24+
``sqrt(d^2 + 1e-20)`` floor (mirroring ``matern_cov_matrix_from``) rather than
25+
``linalg.norm`` of an (N, N, 2) difference cube: the norm's derivative is NaN at
26+
the zero diagonal, which would poison every JAX gradient through this kernel.
27+
2328
Parameters
2429
----------
2530
scale
2631
The length‐scale of the exponential kernel.
2732
pixel_points
2833
Array of shape (N, 2) giving the (y,x) coordinates of each source‐plane pixel.
34+
xp
35+
Backend (numpy or jax.numpy).
2936
3037
Returns
3138
-------
3239
np.ndarray, shape (N, N)
3340
The exponential covariance matrix.
3441
"""
35-
# pairwise differences: shape (N, N, 2)
36-
diff = pixel_points[:, None, :] - pixel_points[None, :, :]
42+
pts = xp.asarray(pixel_points)
43+
44+
# ||x - y||^2 = ||x||^2 + ||y||^2 - 2 x·y
45+
x2 = xp.sum(pts * pts, axis=1, keepdims=True) # (N, 1)
46+
dist_sq = x2 + x2.T - 2.0 * (pts @ pts.T) # (N, N)
47+
dist_sq = xp.maximum(dist_sq, 0.0) # numerical safety
3748

38-
# Euclidean distances: shape (N, N)
39-
d = xp.linalg.norm(diff, axis=-1)
49+
d = xp.sqrt(dist_sq + 1e-20) # (N, N)
4050

4151
# exponential kernel
4252
cov = xp.exp(-d / scale)
4353

4454
# add a small jitter on the diagonal
45-
N = pixel_points.shape[0]
46-
cov = cov + xp.eye(N) * 1e-8
55+
N = pts.shape[0]
56+
cov = cov + xp.eye(N, dtype=cov.dtype) * 1e-8
4757

4858
return cov
4959

@@ -64,6 +74,14 @@ def __init__(self, coefficient: float = 1.0, scale: float = 1.0):
6474
6575
A full description of regularization and this matrix can be found in the parent `AbstractRegularization` class.
6676
77+
**JAX & gradient support** (2026-07): xp-threaded and
78+
JAX-differentiable end-to-end — the pairwise distances use the
79+
NaN-safe ``sqrt(d^2 + 1e-20)`` form (see ``exp_cov_matrix_from``).
80+
Caveat: the regularization matrix is an explicit dense inverse of the
81+
kernel covariance, whose conditioning on clustered mesh vertices puts
82+
a small numerical noise floor on the likelihood (see ``MaternKernel``
83+
for the measured detail).
84+
6785
Parameters
6886
----------
6987
coefficient
@@ -113,6 +131,7 @@ def regularization_matrix_from(self, linear_obj: LinearObj, xp=np) -> np.ndarray
113131
covariance_matrix = exp_cov_matrix_from(
114132
scale=self.scale,
115133
pixel_points=linear_obj.source_plane_mesh_grid.array,
134+
xp=xp,
116135
)
117136

118137
return self.coefficient * xp.linalg.inv(covariance_matrix)

‎autoarray/inversion/regularization/gaussian_kernel.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ def __init__(self, coefficient: float = 1.0, scale: float = 1.0):
6464
6565
A full description of regularization and this matrix can be found in the parent `AbstractRegularization` class.
6666
67+
**JAX & gradient support** (2026-07 gradient sweep): xp-threaded and
68+
JAX-differentiable end-to-end. Caveat: the regularization matrix is an
69+
explicit dense inverse of the kernel covariance, whose conditioning on
70+
clustered mesh vertices puts a small numerical noise floor on the
71+
likelihood (see ``MaternKernel`` for the measured detail).
72+
6773
Parameters
6874
----------
6975
coefficient

0 commit comments

Comments
 (0)