Repository navigation
Description
Overview
An MGE-only FitInterferometer always takes the dense transform_mapping_matrix path, even after apply_sparse_operator(), because factory.py:204-208 turns the sparse operator off for func-list-only inversions. On the A100 the W~ steps take 1.9–13.5 ms against 0.86–23.8 s for the dense chain, and the dense path runs out of memory at alma and above (autolens_profiling#308). This task switches that guard off. It also fixes a seam in the sparse data vector: it reads a dirty image built from the unsubtracted visibilities, so fits that also have regular light profiles get the wrong data vector.
Plan
- PyAutoArray: let func-list-only interferometer inversions use the sparse operator. Imaging is unchanged.
- PyAutoArray: let an inversion dataset carry a dirty image for the subtracted data. The sparse data vector uses it when it is present and falls back to the cached one otherwise.
- PyAutoGalaxy and PyAutoLens: when the fit has regular light profiles and a sparse operator, compute the dirty image of the profile-subtracted visibilities (one adjoint NUFFT) and pass it in. Pure linear fits keep the cached image at zero extra cost.
- Tests:
- MGE-only sparse vs dense parity, with and without a regular light profile.
- A class-routing assertion.
- A JAX-vs-NumPy check.
- Ship the three library PRs, stacked PyAutoArray → PyAutoGalaxy → PyAutoLens with the same branch name.
- Workspace follow-on in autolens_profiling:
- Re-run the MGE breakdown cell on the library path (CPU and A100).
- Update the
("interferometer","mge", alma+)VRAM rows. - Start only after PR feat(DatasetModel): add grid_rotation_angle for multi-band rotation #312 has merged, which releases the repo claim. It can wait for this release or run against library mains.
Detailed implementation plan
Affected Repositories
- PyAutoArray (primary)
- PyAutoGalaxy
- PyAutoLens
- autolens_profiling (workspace phase, after PR feat(DatasetModel): add grid_rotation_angle for multi-band rotation #312)
Branch Survey
| Repository | Current Branch | Dirty? |
|---|---|---|
| ./PyAutoArray | main | clean |
| ./PyAutoGalaxy | main | clean |
| ./PyAutoLens | main | clean |
| ./autolens_profiling | main | untracked dataset/abell_1201 (unrelated); claimed by interferometer-mge-breakdown until #312 merges |
Suggested branch: feature/interferometer-mge-w-tilde-route
Context
autoarray/inversion/inversion/factory.py:204-208 turns the sparse operator off whenever every linear object is a func-list. As a result, an MGE-only FitInterferometer always takes the dense transform_mapping_matrix path, even after apply_sparse_operator().
The #308 breakdown measured what that costs:
- W~ steps vs dense chain on the A100: 1.9–13.5 ms against 0.86–23.8 s.
- W~ vs dense on CPU at alma: 28.6 ms against 36.6 s.
- The dense path runs out of A100 memory at alma and above.
A code survey found that InversionInterferometerSparse already handles zero mappers:
data_vectorismapping_matrix.T @ dirty_image._curvature_matrix_func_list_and_mapperuses zeros when there is no mapper and then adds the func–func blocks fromcurvature_matrix_func_list_from.- The solver and preconditioning code already checks
has(Mapper). mapped_reconstructed_operated_data_dictis overridden as one NUFFT per linear object.
No test asserts that MGE-only fits take the dense path.
The survey also found a correctness seam, which you chose to fix in this task. The sparse data_vector reads sparse_operator.dirty_image, which is built from the ORIGINAL visibilities (dataset/interferometer/dataset.py:409). FitInterferometer in both galaxy and lens swaps in profile_subtracted_visibilities but passes the operator through unchanged. So any sparse fit that also has regular (non-linear) light profiles gets a wrong D, and its chi-squared terms become inconsistent with each other. Mixed mapper + MGE fits already hit this today.
The imaging version of the seam stays in its own existing prompt, draft/bug/autoarray/sparse_inversion_ignores_profile_subtracted_image.md.
Detailed plan
Branch feature/interferometer-mge-w-tilde-route in PyAutoArray, PyAutoGalaxy and PyAutoLens. Worktree ~/Code/PyAutoLabs-wt/interferometer-mge-w-tilde-route/, created by /start_library. The conflict guard passes for all three.
PyAutoArray
inversion/inversion/factory.py(interferometer branch, around lines 202-236): remove theall(isinstance(..., AbstractLinearObjFuncList))guard, sodataset.sparse_operator is not Nonealone selects sparse._use_interferometer_numbaalready returns False when func-lists are present (:288-291), so MGE-only goes toInversionInterferometerSparse. Leave the imaging guard at:128-160alone and add a comment saying why it differs.inversion/inversion/dataset_interface.py: add an optional kwargsparse_dirty_image=None, documented as the dirty image ofdatafor the sparse operator. It is needed whendatadiffers from the dataset the operator was built from. Do not reuse the namedirty_image, becauseInterferometer.dirty_image(dataset.py:550) is a different quantity used in plots.inversion/inversion/interferometer/sparse.py:92:data_vectorusesgetattr(self.dataset, "sparse_dirty_image", None)when it is not None, and falls back toself.dataset.sparse_operator.dirty_imageotherwise. This is the only reader (the numba subclass inherits it). Update the docstring.- Tests in
test_autoarray/inversion/inversion/interferometer/test_interferometer.py, reusing_sparse_parity_setupand_assert_sparse_matches_mapping(:453-528):test__interferometer_sparse_operator__func_list_only__identical_to_mapping, with a[MockLinearObjFuncList]parity check and an assertion that the class isInversionInterferometerSparse...._sparse_dirty_image_override__used_by_data_vector, which runs DatasetInterface with subtracted data plussparse_dirty_imageand checks it against dense to 1e-10.- Extend the JAX-vs-NumPy test (
:751) to the func-list-only case.
- Add a factory test in
test_factory.py: MGE-only on a dataset with a sparse operator gives the sparse class.
PyAutoGalaxy (autogalaxy/interferometer/fit_interferometer.py, galaxies_to_inversion)
- If
self.dataset.sparse_operator is not Noneand any galaxy has a regular light profile, passsparse_dirty_image=transformer.image_from(visibilities=d.real*σr⁻² + 1j*d.imag*σi⁻², xp=...)withd = profile_subtracted_visibilities. The regular-light-profile test iscls_list_from(cls=LightProfile, cls_filtered=LightProfileLinear), the same check asgalaxy.py:248. The formula mirrorsapply_sparse_operator. Pass.arrayas that site does. - The branch is decided structurally in Python, so it is JIT-safe.
- Check that
transformer.image_fromaccepts or dispatchesxpfor jax. If it does not, use linearity instead:sparse_operator.dirty_image − image_from(profile_visibilities weighted). The executor verifies which applies. - Test in
test_autogalaxy/interferometer/test_fit_interferometer.py: Sersic (regular) plus a linear Gaussian on a sparse dataset against the dense dataset, comparing log_likelihood and log_evidence to 1e-8. Control: all-linear.
PyAutoLens (autolens/interferometer/fit_interferometer.py:134, tracer_to_inversion)
- Apply the same change, with the regular-profile test over
self.tracer.galaxies. Factor a small shared helper into autogalaxy (for example inautogalaxy/interferometer/fit_interferometer.pyor theoperateutils) so the formula lives in one place, and call it from lens. - Test in
test_autolens/interferometer/test_fit_interferometer.py: lens Sersic plus source MGE, sparse vs dense, to 1e-8.
Checks before shipping
- Full unit suites for the three libraries.
- Heart smoke via
/smoke_teston the interferometer MGE and pixelization scripts in autolens_workspace, and onautolens_workspace_testinterferometer sparse scripts. - Workspace impact grep for
apply_sparse_operatorwith MGE: the autolens_workspacemodeling.py:322-324docs prompt becomes accurate once this lands, so note it on that prompt.
Workspace phase (autolens_profiling, later, /start_workspace)
- Re-run
scripts/interferometer/likelihood_breakdown/mge.pyon the library path: CPU sma/alma, and A100 alma/alma_high/jvla via the existing submits. - Witness: the library-path log_likelihood matches dense within 1e-6 nats where dense runs, and
jax.jit(FitInterferometer)no longer runs out of memory. - Update the VRAM rows in
scripts/misc/vram/config.py, add a results addendum to the feat: add aa.interp_2d (NumPy + JAX bilinear interpolation) #308 note, and regenerate the README.
Verification
- Unit tests: the new parity tests pass, and the full suites for PyAutoArray, PyAutoGalaxy and PyAutoLens stay green.
- Local script: the alma MGE-only fit with
apply_sparse_operator()has inversion classInversionInterferometerSparseand a log_likelihood equal to the dense path within 1e-6 nats. With a regular Sersic added, sparse and dense still agree, which is the seam fix. - A100, in the workspace phase: the jitted library fit runs at alma, alma_high and jvla.
Risks
transformer.image_fromunder JAX (see the PyAutoGalaxy step).- Stacked PRs across three libraries: merge them in order, PyAutoArray first.
log_likelihoodstill does one NUFFT per linear object for the model visibilities, which is O(N_vis) but fits in memory. Measure it in the workspace phase.
Original Prompt
Click to expand starting prompt
Interferometer likelihood campaign: route MGE-only interferometer fits through the W~ sparse operator
Type: feature
Target: PyAutoArray
Repos:
- PyAutoArray
- autolens_profiling
Themes: - interferometer
- mge
- sparse-operator
- jax-gpu
Difficulty: medium
Autonomy: supervised
Priority: high
Status: draft
Consequence: glance
Witness: withapply_sparse_operator()applied, an MGE-onlyFitInterferometeron the alma dataset takes the func-list W~ path (inversion class is the sparse/W~ interferometer inversion, notInversionInterferometerMapping), itslog_likelihoodmatches the dense path within 1e-6 nats, andjax.jit(FitInterferometer)no longer OOMs the A100 at alma/alma_high/jvla.
Review-minutes: 10
Epic: interferometer-likelihood-campaign
Source: autolens_profiling/results/notes/interferometer_mge_breakdown_2026_09.md, lever 1
(autolens_profiling#308).
Why
autoarray/inversion/inversion/factory.py:202-208 sets use_sparse_operator = False when
every linear object is an AbstractLinearObjFuncList, so an MGE-only interferometer fit
always pays the O(N_vis * n) dense NUFFT path even after apply_sparse_operator(). The
func-list W~ blocks already exist and are used by mixed mapper + MGE inversions
(inversion_interferometer_util.py:1466 operated_matrix_slim_from, :1628
curvature_matrix_func_list_from).
Measured on the #308 breakdown cell's measurement-only W~ arm (fp64, nufftax 0.6.1):
dense chain from the mapping matrix vs W~ chain — A100 sma 855 ms -> 2.40 ms, alma 937 ms
-> 1.90 ms, alma_high 3.81 s -> 3.35 ms, jvla 23.62 s -> 13.53 ms; CPU alma 36.58 s ->
28.6 ms. F~ vs dense F agrees to <= 1.6e-11 rel; figure of merit to <= 7.1e-7 nats. The
library path cannot run above sma on the A100 at all (65.9 GB alma one-shot transform).
What
- Let the factory keep the sparse operator on for func-list-only interferometer inversions
whendataset.sparse_operatoris set (imaging keeps today's behaviour unless measured). - Data vector from the cached dirty image; the
fast_chi_squaredidentity with per-dataset
constants, so no transformed mapping matrix is formed. - Check the mapped visibilities / residual map path (plots,
FitInterferometerattributes
that needtransformed_mapping_matrix) stays correct — likely a lazy dense fallback. - Re-run
scripts/interferometer/likelihood_breakdown/mge.py(library path) CPU + A100 and
update the VRAM table rows("interferometer", "mge", alma+)in
scripts/misc/vram/config.py.
Watch
- The one-off operator build (1.1-8.2 s A100, 18.5 s CPU alma) must be done once per
dataset, not per likelihood. - After this lands PDIP becomes the largest W~ step on the A100 (1.46 of 2.32 ms at alma).
Activity
Jammy2211 commentedon Sep 26, 2026
Library PRs Created — 2026-09-26
Deviations from the plan, both within its intent:
d~ − W~ i_p(one FFT convolution, about 2 ms at alma) rather than an adjoint NUFFT (about 4 s at alma on CPU). The PR carries the diagnosis.Basis, so MGE-only fits pay nothing extra.Incidental:
TransformerDFT.image_fromnow honoursxp, so it no longer crashes under jit.Workspace follow-on is pending: re-run the autolens_profiling library-path breakdown and update the VRAM rows, after autolens_profiling#312 merges.
Heart RED override (development only)
The human authorized the AUTONOMY.md "Human override for Heart RED (development only)" for this task (PyAutoArray#575, branch
feature/interferometer-mge-w-tilde-route) on 2026-09-26. When asked how to proceed with the three library PRs, they chose "Override, open PRs". The override covers commit, push and opening pending-release PRs only. Merging needs a separate/prmwith every required check green. This branch does not claim to fix Heart.Exact RED reasons at ship time:
Branch gates passed: 24/24 targeted interferometer smoke scripts pass on the branch (autolens_workspace 11, autogalaxy_workspace 5, autolens_workspace_test 5, autogalaxy_workspace_test 3: MGE, pixelization, linear light profiles, sparse-operator JAX assertions and likelihood pins).
autolens_workspace_test/.../jax_likelihood/mge.pyfirst failed identically on the branch and on the mains because the local dataset was stale (2026-08-06, older than the 09-18 simulator change); after regenerating it, the script passes on both. Unit suites: PyAutoArray 1706 passed, PyAutoGalaxy 1240 passed, PyAutoLens 757 passed + 1 xfailed. My own diff review is in-session only; the human reviews at merge.perf: skip the zero-image Fourier transform when galaxies have no ord…
perf: skip the zero-image Fourier transform when the tracer has no or…
Jammy2211 commentedon Sep 26, 2026
Workspace PR Created — 2026-09-26
The library route re-ran on the branch, both locally and on RAL. The full MGE likelihood matches dense to 0.0 nats wherever dense runs.
A100 per-call times, library path vs dense:
These numbers are with the zero-image NUFFT fix, now folded into PyAutoGalaxy#629 (36d1b436) and PyAutoLens#750 (ac333b17).
Details: the "Library W~ route (2026-09-26)" section of
results/notes/interferometer_mge_breakdown_2026_09.md.Merge pull request #576 from PyAutoLabs/feature/interferometer-mge-w-…
Jammy2211 commentedon Sep 26, 2026
Shipped
Merged 2026-09-26, library-first:
What changed
apply_sparse_operator()is applied (the factory guard is removed).DatasetInterface.sparse_dirty_image. Galaxy/lens fits with ordinary light profiles pass d~ − W~ i_p. This fixes a silent bug: the unsubtracted dirty image gave a 23% wrong D at sma (logL −26.77 vs −24.34 dense).profile_visibilitiesskips the zero-image NUFFT when no ordinary light is present.TransformerDFT.image_fromnow honoursxp(incidental).Results
Known limits
apply_sparse_operatorbuild (10.8 GB).Libraries are merged but not yet released (pending-release).