Skip to content

feat(interferometer): pass data=None on the sparse precomputed-data-term path (Discussion #13 lens parity) - #757

Merged
Jammy2211 merged 1 commit into
mainfrom
feature/interferometer-sparse-precomputed-data-term
Sep 30, 2026
Merged

Jammy2211 merged 1 commit into
mainfrom
feature/interferometer-sparse-precomputed-data-term

Conversation

@Jammy2211

Copy link
Copy Markdown
Collaborator

Summary

PyAutoLens parity for phase 1 of GitHub Discussion https://github.com/orgs/PyAutoLabs/discussions/13 ("Streaming visibilities for memory efficiency", HRSAstro), mirroring PyAutoLabs/PyAutoGalaxy#637 on top of PyAutoLabs/PyAutoArray#589. Closes #756.

On the sparse interferometer path a lens fit with no non-linear light profile still allocated two N_vis arrays per likelihood evaluation: profile_visibilities (Visibilities.zeros) and profile_subtracted_visibilities (data - zeros), because FitInterferometer.tracer_to_inversion passed the subtracted data unconditionally. This PR:

  • gates tracer_to_inversion on autogalaxy's uses_precomputed_data_term_from(dataset, galaxies, data, noise_map) (sparse operator carries data_term, the fitted data/noise map are the dataset's own, no galaxy has a non-linear light profile) and passes data=None when it holds, so fast_chi_squared reads the cached scalar and neither visibility property is evaluated on the likelihood path; the checks are identity/type checks, so the branch is fixed at trace time and jit-safe;
  • adds FitInterferometer.inversion_with_data: the inversion, or a shallow copy carrying fit.data when the inversion ran with data=None, for output paths (data_subtracted_dict → subplot_of_mapper); the interferometer visualizer uses it;
  • makes profile_visibilities / profile_subtracted_visibilities cached properties (as profile_image already was and as autogalaxy now has them), so "never evaluated" is observable.

Dense-path and light-profile fits are bit-identical to before.

Witness (1e5-vis NUFFT, 616-pixel mask, Isothermal lens + 15x15 rectangular source, InversionInterferometerSparseNumba): figure_of_merit gated vs gate forced off differs by exactly 0.0; tracemalloc peak per figure_of_merit 3.20 MB gated at both 1e5 and 4e5 visibilities (flat in N_vis, below 16·N_vis bytes) vs 7.59 MB / 21.99 MB ungated; 16.2 ms vs 18.0 / 26.6 ms per evaluation.

API Changes

Additive. New public name FitInterferometer.inversion_with_data. Behaviour change: on the sparse path with no non-linear light profile, fit.inversion.dataset.data is now None (use fit.inversion_with_data or fit.data for outputs). profile_visibilities and profile_subtracted_visibilities are now cached per fit instance. Requires PyAutoArray ≥ the #589 merge and PyAutoGalaxy ≥ the #637 merge (both on main, pending release).
See full details below.

Test Plan

  • pytest test_autolens/interferometer — 29 passed (new: gate true + inversion.dataset.data is None; sparse pixelization-only vs dense at rel 1e-8; inversion_with_data shares the reconstruction and carries fit.data; dense fit returns inversion_with_data is inversion; spies on visibilities_from / Visibilities.zeros record no calls and neither profile property is cached after figure_of_merit; light-profile fit gate false and bit-equal; jax.jit sparse fit matches numpy; gate forced off → figure_of_merit bit-equal)
  • pytest test_autolens — 766 passed, 1 xfailed
  • Red check: with the source edits reverted, 5 of the 11 tests in the file fail (all new/extended)
  • Witness script (numbers above)
  • CI green on unittest 3.12 / 3.13 / nojax
Full API Changes (for automation & release notes)

Added

  • autolens.interferometer.fit_interferometer.FitInterferometer.inversion_with_data -> Optional[aa.AbstractInversion] — the inversion carrying the fitted visibilities as its dataset data, for output/plot paths
  • FitInterferometer._uses_precomputed_data_term (private property) — uses_precomputed_data_term_from(dataset, tracer.galaxies, data, noise_map)

Changed Behaviour

  • FitInterferometer.tracer_to_inversion passes data=None to aa.DatasetInterface when _uses_precomputed_data_term holds (sparse operator with data_term, dataset's own data/noise map, no non-linear light profile); profile_visibilities / profile_subtracted_visibilities are then never evaluated on the likelihood path
  • FitInterferometer.profile_visibilities and FitInterferometer.profile_subtracted_visibilities are cached_property (were plain properties); values unchanged
  • autolens/interferometer/model/visualizer.py passes fit.inversion_with_data to the inversion plotter

Migration

  • Code reading fit.inversion.dataset.data / fit.inversion.data_subtracted_dict on a sparse pixelization-only lens fit should use fit.inversion_with_data (or fit.data).

Generated by the PyAutoLabs agent workflow.

🤖 Generated with Claude Code

…erm path (#756)

Mirror PyAutoGalaxy#637 in autolens: gate FitInterferometer.tracer_to_inversion
on uses_precomputed_data_term_from so sparse pixelization-only lens fits build
their inversion with data=None and never allocate profile_visibilities /
profile_subtracted_visibilities per likelihood call. Add inversion_with_data
for output paths, use it in the interferometer visualizer, and cache the two
profile visibility properties. Tests mirror autogalaxy's phase-1 additions.

Phase 1 of https://github.com/orgs/PyAutoLabs/discussions/13.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@Jammy2211

Copy link
Copy Markdown
Collaborator Author

Independent review (Codex gpt-6-astra) — this PR + phase 1 (PyAutoArray#589, PyAutoGalaxy#637)

Three read-only Codex reviews (one per repo) returned 6 findings = 4 distinct claims. Every claim was reproduced with scripts before being reported here; nothing has been edited in response yet.

# Claim Verdict Where Real exposure
B fit.inversion.data_subtracted_dict breaks when the inversion ran with data=None (gate on): multi-object → TypeError: unsupported operand type(s) for -: 'NoneType' and 'complex' at autoarray/inversion/inversion/abstract.py:885; single mapper → {mapper: None} and subplot_of_mapper raises ValueError at plot/array.py:206. The inversion_plots.py:87/:397 handlers catch only (AttributeError, KeyError). Reproduced — introduced by phase 1 / this PR autoarray + plotters Yes. autogalaxy_workspace scripts/interferometer/features/pixelization/fit.py:321-322 passes fit.inversion on a sparse dataset (live on PyAutoGalaxy main now); autolens_workspace same path :293 will hit it once this PR lands. Library visualizers are already on inversion_with_data.
A apply_sparse_operator squares data.real/data.imag in the data's own dtype: complex64 data + complex128 noise gives data_term 700140000.0 vs exact 700140007.0; log_evidence +3.5; chunked path (sparse_terms_from_chunks, promotes to complex128) disagrees with one-shot. Reproduced — pre-existing precision path, exposed by the cache autoarray/dataset/interferometer/dataset.py:391-394 Low. Visibilities.from_fits and the float (N,2) constructor cast to complex128; only a caller passing a complex64 ndarray directly reaches it. complex128 data: all paths bit-equal.
C Replacing/mutating dataset.noise_map after apply_sparse_operator leaves noise_normalization (and data_term) stale: 7.35 vs 12.90 for σ 1→2. Reproduced — but pre-existing: W~, dirty image and the sparse curvature (4× off) were already stale in that scenario before phase 1; phase 1 adds two more stale scalars. autoarray/fit/fit_interferometer.py:142; noise_map is a plain attribute Low. No library or workspace path reassigns noise_map on a sparse dataset (all dataset.noise_map = sites are imaging, pre-operator).
D check_noise_map_real_imag_equal uses np.allclose default atol=1e-8, so σ_real=1e-9 vs σ_imag=2e-9 passes; sparse curvature then 1.5× dense, log_evidence −2.77. Reproduced — check predates phase 1 (1ae2f2f3, 2026-08-28); phase 1 moved it into the shared helper inversion_interferometer_util.py:58,61 Not reachable with Jy-unit data (sdp81 ALMA σ min 6e-5 → tolerance ≈1.7e-4 relative; factor-2 mismatch accepted only below ~1e-8).

Recommended follow-up (not done here — scope decision for the human):

  1. B (fix): in AbstractInversion.data_subtracted_dict raise a clear InversionException when self.data is None; add it (and TypeError) to the inversion_plots.py handlers; switch the two workspace scripts to fit.inversion_with_data. PyAutoArray corrective PR + two workspace PRs.
  2. D (cheap hardening, same PR as 1): atol=0.0 on both allclose/isclose calls.
  3. A (cheap, same PR): cast to complex128 before squaring in apply_sparse_operator, so one-shot == chunked for any input dtype.
  4. C: document as unsupported (mutating a dataset after apply_sparse_operator was already wrong) or make noise_map/data setters drop sparse_operator. Judgment call.

None of the four changes the parity or memory claims in this PR's body for complex128 data. Reproduction scripts: session scratchpad repro_{A,B,C,D}.py.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pending-release PR queued for the next release build

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(interferometer): pass data=None on the sparse precomputed-data-term path (Discussion #13 lens parity)

1 participant