Skip to content

Commit 4332ec2

Browse files
Jammy2211claude
authored andcommitted
fix: make the interferometer simulator's @jax.jit path work
Follow-up to #420/#421, which fixed the imaging simulator and deliberately excluded the interferometer. Two causes, and one of them was not what #422 predicted. 1. Interferometer was not a registered pytree, so it could not cross the jit RETURN boundary. Added _register_interferometer_pytrees, mirroring _register_imaging_pytrees: `data` and `noise_map` dynamic; uv_wavelengths, real_space_mask, transformer, grids, the over-sample sizes and the Nones as aux. It must also register Visibilities and VisibilitiesNoiseMap — unlike Array2D on the imaging path, nothing else registers those, so they surfaced as bare leaves ("returned a value of type Visibilities at output component [0]"). 2. `via_image_from` called `transformer.visibilities_from(image=image)` WITHOUT `xp=xp`. That is the whole of the second fix — one line. #422 characterised (2) as `TransformerNUFFT._forward_native` hard-converting to NumPy and needing restructuring. That was wrong, and transformer.py is deliberately untouched here: `_forward_native` already has a complete, jittable JAX branch (lax.scan + dynamic_slice). The reported failure at transformer.py:660 was in the *NumPy* branch — the traceback pointed at the symptom while the cause was one frame up, in a caller that never threaded xp. Same root shape as site 1 of #421. TransformerDFT had been passing by luck: its arithmetic flows through tracers, so the missing xp never showed. Only NUFFT, which calls into _nufftax and then converts, exposed it. Verified: - DFT and NUFFT, numpy-eager vs jax-jit, agree to ~1e-11 on real AND imaginary parts (complex visibilities) - both NUFFT chunk branches under jit match NumPy to ~5e-13 — single-shot and the lax.scan path (chunk_size is a transformer arg the simulator never sets, so that branch was exercised on the transformer directly) - the returned Interferometer keeps uv_wavelengths, real_space_mask, noise map - NumPy path still ndarray-backed - test_autoarray 929, test_autogalaxy 1009, test_autolens 488 — all unchanged TransformerNUFFTPyNUFFT remains out of scope: the legacy pynufft backend is not JAX-traceable and is not expected to be. Closes #422 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CWqjHGXUut25TEB8octU8H
1 parent e994485 commit 4332ec2

1 file changed

Lines changed: 56 additions & 1 deletion

File tree

autoarray/dataset/interferometer/simulator.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,58 @@
77
from autoarray import exc
88
from autoarray.dataset import preprocess
99

10+
_INTERFEROMETER_PYTREES_REGISTERED = False
11+
12+
13+
def _register_interferometer_pytrees() -> None:
14+
"""Register ``Interferometer`` so ``jax.jit(via_image_from)`` can flatten its return.
15+
16+
Counterpart of ``_register_imaging_pytrees`` in the imaging simulator (and of
17+
``AnalysisImaging._register_fit_imaging_pytrees``). Without it a jitted
18+
interferometer call raises ``TypeError: ... returned a value of type
19+
Interferometer, which is not a valid JAX type``.
20+
21+
``data`` and ``noise_map`` are the only per-simulation dynamic values.
22+
Everything else rides as aux: ``uv_wavelengths`` and ``real_space_mask`` are
23+
the fixed observation geometry, ``transformer`` and ``grids`` are constants for
24+
a given simulation, the over-sample sizes are static integer geometry, and the
25+
remaining slots are ``None``.
26+
27+
Unlike registration for a jitted function's *arguments* — which must happen
28+
before the first call, since JAX flattens arguments at trace time — this is in
29+
time, because the return value is flattened only after the body has run.
30+
Idempotent via the module-level flag.
31+
"""
32+
global _INTERFEROMETER_PYTREES_REGISTERED
33+
if _INTERFEROMETER_PYTREES_REGISTERED:
34+
return
35+
36+
from autoarray.abstract_ndarray import register_instance_pytree
37+
from autoarray.structures.visibilities import Visibilities
38+
39+
# The two dynamic children must themselves be pytrees, or they surface as
40+
# bare leaves and JAX rejects the return value ("... returned a value of type
41+
# Visibilities ... at output component [0]"). Unlike ``Array2D`` — which the
42+
# imaging path gets auto-registered — these are not registered anywhere else.
43+
register_instance_pytree(Visibilities)
44+
register_instance_pytree(VisibilitiesNoiseMap)
45+
46+
register_instance_pytree(
47+
Interferometer,
48+
no_flatten=(
49+
"uv_wavelengths",
50+
"real_space_mask",
51+
"transformer",
52+
"grids",
53+
"over_sample_size_lp",
54+
"over_sample_size_pixelization",
55+
"noise_covariance_matrix",
56+
"sparse_operator",
57+
),
58+
)
59+
60+
_INTERFEROMETER_PYTREES_REGISTERED = True
61+
1062

1163
class SimulatorInterferometer:
1264
def __init__(
@@ -115,11 +167,14 @@ def via_image_from(self, image, xp=None):
115167
if xp is None:
116168
xp = self._xp
117169

170+
if xp is not np:
171+
_register_interferometer_pytrees()
172+
118173
transformer = self.transformer_class(
119174
uv_wavelengths=self.uv_wavelengths, real_space_mask=image.mask
120175
)
121176

122-
visibilities = transformer.visibilities_from(image=image)
177+
visibilities = transformer.visibilities_from(image=image, xp=xp)
123178

124179
if self.noise_sigma is not None:
125180
visibilities = preprocess.data_with_complex_gaussian_noise_added(

0 commit comments

Comments
 (0)