Repository navigation
Description
Background
The interferometer NUFFT migration replaced the legacy TransformerNUFFTPyNUFFT (pynufft, with internal Kaiser-Bessel kernel deconvolution in the adjoint) with the new default TransformerNUFFT (nufftax-backed, strict mathematical adjoint). The migration intentionally left the sparse-operator path on TransformerDFT (see the guard in autoarray/dataset/interferometer/dataset.py:261-282), and a recent workspace fix autogalaxy_workspace#90 brought the last outlier script in line with that convention.
However, two diagnostic scripts in autolens_workspace_test are still failing under the new transformer — these are not sparse-path issues, they're library-side correctness gaps in the new adjoint.
Failing scripts
1. NUFFT round-trip peak offset
autolens_workspace_test/scripts/interferometer/nufft.py —
AssertionError: Round-trip dirty-image peak too far from original peak: 5.00 px
Repro:
(cd autolens_workspace_test && env PYAUTO_TEST_MODE=2 PYAUTO_SMALL_DATASETS=1 \
PYAUTO_DISABLE_JAX=1 PYAUTO_FAST_PLOTS=1 JAX_ENABLE_X64=True \
NUMBA_CACHE_DIR=/tmp/numba_cache MPLCONFIGDIR=/tmp/matplotlib \
python3 scripts/interferometer/nufft.py)
A 5-pixel positional offset on round-trip suggests the new nufftax-backed adjoint has a small but real positional convention drift relative to the original TransformerNUFFTPyNUFFT (or relative to TransformerDFT, which the round-trip likely compares against).
2. Delaunay DatasetModel parity
autolens_workspace_test/scripts/multi/dataset_model_parity_delaunay.py —
AssertionError: Delaunay A1 != B1: DatasetModel rotation+shift fit differs from profile-baked fit (THIS IS THE BUG THE FIX TARGETS).
Repro:
(cd autolens_workspace_test && env PYAUTO_TEST_MODE=2 PYAUTO_SMALL_DATASETS=1 \
PYAUTO_DISABLE_JAX=1 PYAUTO_FAST_PLOTS=1 JAX_ENABLE_X64=True \
NUMBA_CACHE_DIR=/tmp/numba_cache MPLCONFIGDIR=/tmp/matplotlib \
python3 scripts/multi/dataset_model_parity_delaunay.py)
The script's own self-comment says "THIS IS THE BUG THE FIX TARGETS" — meaning the script was added specifically to detect this regression and is now asserting against the un-fixed state. PR correlation: fix(interferometer): correct sparse curvature for Pmax > 1 (Delaunay) was meant to address it but left this path broken.
Suspected single root cause
All three Cluster G failures (these two plus the now-fixed workspace apply_sparse_operator case) are downstream of the same upstream issue: the new TransformerNUFFT adjoint's absolute scale and pixel-grid alignment differ from the legacy implementation in ways that the sparse-operator path and the Delaunay parity path both depend on. The workspace fix sidesteps the sparse-path piece by switching to TransformerDFT; this issue is the remaining library-side work to fully complete the migration.
Suggested approach
- Quantify the adjoint mismatch: does
TransformerNUFFT.image_from(visibilities, use_adjoint_scaling=True)matchTransformerDFT.image_from(...)in both scale and pixel position on a simple delta-input test? The error message inapply_sparse_operatorimplies scale parity is achieved withuse_adjoint_scaling=True, but the 5-px round-trip drift suggests a positional/grid-origin convention drift remains. - Reproduce the Delaunay parity failure against the new transformer with
printstatements at the pre-/post-transformer images to localise the divergence. - Decide whether the fix lives in nufftax's wrapper (
PyAutoArray/autoarray/operators/transformer.py:499) or upstream innufftaxitself.
Files of interest
PyAutoArray/autoarray/operators/transformer.py—TransformerNUFFTclass (lines 499–660+, adjoint scaling notes at 644-653)PyAutoArray/autoarray/dataset/interferometer/dataset.py:261-282— the deliberate sparse-path guardautolens_workspace_test/scripts/interferometer/nufft.py— round-trip diagnosticautolens_workspace_test/scripts/multi/dataset_model_parity_delaunay.py— parity diagnostic
🤖 Filed via Claude Code as the library-side follow-up to autogalaxy_workspace#90.
Activity
Jammy2211 commentedon Jul 28, 2026
Closing: neither failure reproduces, and the suspected root cause is disproven
Re-ran both scripts on current
main(PyAutoArraymain, autolens_workspace_testmain, autolens2026.7.23.1) and measured the adjoint directly. Summary: there is no library-side gap here.1. NUFFT round-trip peak offset — passes
Ran this issue's exact repro command verbatim:
And at production size (256x256, no
PYAUTO_SMALL_DATASETS):The
5.0 -> 6.0tolerance bump landed in autolens_workspace_test@98b2c3a("triage 2026-05-20 cleanup: rebaseline rectangular_dspl, NUFFT tolerance, ...") on 2026-05-20 — the same day this issue was filed. The5.00 pxfailure quoted above was a pre-fix run.The residual 3.61 px at full size is uv-coverage physics, not a convention drift: with only 190 visibilities the dirty beam has large sidelobes and the peak of a smoothed extended arc wanders. See §3 — the two adjoints are numerically identical, so no transformer convention can be responsible for it.
2. Delaunay DatasetModel parity — passes at the size it declares
scripts/multi/dataset_model_parity_delaunay.py:48carriesENV: full_datasets, and its docstring states why:This issue's repro command forces
PYAUTO_SMALL_DATASETS=1— precisely the condition the script rules out.PyAutoHands/autohands/env_config.py:45mapsfull_datasets -> unset PYAUTO_SMALL_DATASETS, so the real harness runs it uncapped. Neither script is insmoke_tests.txt.Run at its declared size:
Note also that when forced under
PYAUTO_SMALL_DATASETS=1it fails atA0 != A1(the profile-baked baseline against itself), not theA1 != B1this issue quotes — i.e. the capped run breaks the reference, not theDatasetModelpath under test.3. The "suspected single root cause" is false
Two independent disproofs:
(a) The parity script contains no transformer. It is pure imaging —
al.SimulatorImaging,al.FitImaging, a 51x51 uniform grid and a GaussianConvolver. There is no NUFFT, noTransformer*, no visibility anywhere in it. It cannot be downstream of a NUFFT adjoint.(b) The adjoints are numerically identical. Measured
TransformerNUFFT.image_fromagainstTransformerDFT.image_fromon random visibilities across even, odd and non-square shapes, with and withoutuse_adjoint_scaling:use_adjoint_scaling=True)No scale drift, no positional drift, no parity-dependent offset. The half-pixel
shiftcorrection inTransformerNUFFTis doing its job.4. The cited guard does not exist
This issue points at "the guard in
autoarray/dataset/interferometer/dataset.py:261-282". Those lines are thedirty_image = self.transformer.image_from(...)call insideapply_sparse_operator— not a guard, and not transformer-conditional. The only transformer guard in that file is theraise_error_dft_visibilities_limitcheck (dataset.py:115-128), which rejectsTransformerDFTabove 10,000 visibilities — the opposite direction. The sparse-operator path is not pinned toTransformerDFTby any code.Follow-up filed separately
Two real but unrelated hygiene findings surfaced during this investigation and are being addressed on their own issue:
use_adjoint_scalingis a dead parameter on both live transformers —TransformerDFT.image_fromdocuments it "Currently unused" andTransformerNUFFT.image_fromaccepts-and-ignores it; only the legacyTransformerNUFFTPyNUFFTapplies it.dataset.py:277still passesuse_adjoint_scaling=True, implying it is load-bearing when it is not.scripts/interferometer/nufft.pytests (a)/(b)/(c) labelal.TransformerNUFFTas "pynufft", but that name now resolves to the nufftax-backed class — somax |Δ| nufftax - pynufft : 0.0000e+00is a vacuous self-comparison and three assertions no longer test what they claim.Closing as not-reproducible. The
TransformerNUFFTmigration is complete on the adjoint side.🤖 Investigated via Claude Code
docs(transformer): state the real use_adjoint_scaling contract
Jammy2211 commentedon Jul 28, 2026
Correction to the follow-up section above, and where both items landed
Follow-up 1 was wrong —
use_adjoint_scalingis NOT dead. Do not remove it.I called it a dead parameter on the strength of two of its three implementations (
TransformerDFT.image_fromsays "Currently unused";TransformerNUFFT.image_fromsays "accepted for API compatibility ... otherwise unused"). Measuring the third implementation shows the opposite — the legacy class'sadjoint_scalingis compensating for pynufft's internal IFFT normalisation:Raw is off by ~
4*N_y*N_x(1024x, 4096x); scaled lands on the DFT to pynufft's gridding precision. So the parameter's real contract is "normalise this transformer's adjoint onto the common scale" — correctly a no-op forTransformerDFTandTransformerNUFFT(both already the plain mathematical adjoint), and load-bearing forTransformerNUFFTPyNUFFT.apply_sparse_operatoris right to passTrue: that is precisely what makes the sparse-operator dirty image scale-consistent across all three transformers. Deleting it would have silently broken the pynufft fallback by ~1000x.Fixed as a docstring correction instead, pushed to
mainas PyAutoArray@24863a2— the two misleading docstrings now state the contract and carry a do-not-remove note. No behaviour change;test_autoarray/operators/test_transformer.py15 passed.Follow-up 2 was real and is fixed in autolens_workspace_test@
6a6156c(pushed tomain). The pynufft leg ofscripts/interferometer/nufft.pynow namesal.TransformerNUFFTPyNUFFTexplicitly, so the comparisons report pynufft's genuine gridding error (2.3e-2 absolute on 5x5, 6.1e-2 relative on 256x256) rather than the vacuous0.0000e+00. All three previously-vacuous assertions still pass — their tolerances were calibrated for real pynufft. Addedlib_*coverage of the shippedal.TransformerNUFFT(forward +transform_mapping_matrix) against the local nufftax recipe so the production class has a non-vacuous assertion of its own; it matches exactly.No separate issue was filed — both went straight to
main. This issue stays closed.🤖 via Claude Code