Skip to content

Commit 821fb1b

Browse files
committed
feat: unlock jax.grad through the Delaunay mesh via frozen integer tables
Wraps the qhull pure_callback input in stop_gradient in _jax_delaunay_tables. pure_callback has no JVP rule and previously raised under jax.grad; with the visibility walk running point location, barycentric weights, dual areas and split points in-graph from the traced arrays, the callback returns only int32 connectivity tables, which are piecewise-constant in the vertex positions — their true derivative is zero everywhere except the measure-zero triangle- flip events, where the likelihood itself is discontinuous and no gradient exists for any method. Freezing them under differentiation therefore yields the exact almost-everywhere derivative, not an approximation. Values are bit-identical (the primal is untouched): the jax_likelihood delaunay regression literal passes unchanged. FD-certified on the production shape (Hilbert + edge zeroing + AdaptSplit) by the new autolens_workspace_test scripts/imaging/jax_grad/delaunay.py — 14/14 params live, lens light at 1e-8..1e-10, mass/shear at 1e-5..2e-3 (FD steps straddling flip events; documented rtol=1e-2). Mesh docstrings updated: Delaunay is now gradient-capable; the KNN meshes' remaining edge is batched throughput (the tables callback is vmap_method=sequential, one host qhull call per vmap lane). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013FSqnkgZv97PU9JdkCcthy
1 parent 2897ded commit 821fb1b

3 files changed

Lines changed: 37 additions & 14 deletions

File tree

autoarray/inversion/mesh/interpolator/delaunay.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,22 @@ def scipy_delaunay_tri_only(points_np):
118118

119119

120120
def _jax_delaunay_tables(points):
121-
"""Run the qhull-only callback with fixed output shapes."""
121+
"""Run the qhull-only callback with fixed output shapes.
122+
123+
The callback input is wrapped in ``stop_gradient`` so ``jax.grad`` /
124+
``jax.value_and_grad`` flow through the Delaunay likelihood
125+
(``pure_callback`` has no JVP rule and would otherwise raise). This is
126+
NOT an approximation: the callback returns only int32 connectivity
127+
tables, which are piecewise-constant in the vertex positions — their
128+
true derivative is exactly zero everywhere except the measure-zero
129+
re-wiring (triangle-flip) events, where the likelihood itself is
130+
discontinuous and no gradient exists for any method. Every quantity
131+
with a non-zero derivative (point location via the visibility walk,
132+
barycentric weights, dual areas, split points) is computed in-graph
133+
from the traced ``points``, so the frozen-tables gradient is the exact
134+
almost-everywhere derivative. FD-certified 2026-07-26
135+
(autolens_workspace_test ``scripts/imaging/jax_grad/delaunay.py``).
136+
"""
122137
import jax
123138
import jax.numpy as jnp
124139

@@ -130,7 +145,7 @@ def _jax_delaunay_tables(points):
130145
jax.ShapeDtypeStruct((2 * N, 3), jnp.int32),
131146
jax.ShapeDtypeStruct((N,), jnp.int32),
132147
),
133-
points,
148+
jax.lax.stop_gradient(points),
134149
vmap_method="sequential",
135150
)
136151

autoarray/inversion/mesh/mesh/delaunay.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,17 @@ 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.
28+
**JAX & gradient support** (2026-07-26, FD-certified): the likelihood
29+
runs under ``jax.jit`` and is differentiable — the host-called qhull
30+
``pure_callback`` returns only integer connectivity tables (frozen
31+
under differentiation via ``stop_gradient``; their true derivative is
32+
zero between re-wiring events), while point location, barycentric
33+
weights, dual areas and split points are computed in-graph from the
34+
traced vertices, so ``jax.grad`` returns the exact almost-everywhere
35+
derivative. Caveat for batched samplers: the callback is
36+
``vmap_method="sequential"`` (one host qhull call per vmap lane) —
37+
the ``KNearestNeighbor`` / ``KNNBarycentric`` subclasses avoid the
38+
callback entirely and remain the batched-throughput option.
3339
3440
Zeroed pixels
3541
-------------

autoarray/inversion/mesh/mesh/knn.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,15 @@ def __init__(
2828
2929
**JAX & gradient support** (2026-07, FD-certified): the kNN
3030
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.
31+
Wendland weights — no scipy callback), so the full likelihood is
32+
differentiable, with gradients flowing through both the traced query
33+
points and the traced mesh vertices. (The parent ``Delaunay`` mesh
34+
is also differentiable as of 2026-07-26 via its frozen integer
35+
tables; this mesh's remaining edge is batched throughput — no
36+
per-vmap-lane host callback.) Pair it with a split-family
37+
regularization (``ConstantSplit`` / ``AdaptSplit``) or a kernel
38+
scheme — the neighbor-based schemes (``Constant`` / ``Adapt``) call
39+
scipy on the traced mesh grid and cannot differentiate.
3840
3941
Parameters
4042
----------

0 commit comments

Comments
 (0)