Repository navigation
Description
Overview
Two PyAutoArray performance defects found during the Euclid DR1 campaign (2026-09-27). Imaging.apply_over_sampling rebuilds the dataset without the sparse_operator, so applying the CPU sparse operator first silently falls back to the dense InversionImagingMapping route (~5.4x slower per evaluation, identical likelihood). Every DR1 vis_pix fit ran dense because of it. Separately, operated_mapping_matrix_list is an uncached property, so the dense route PSF-convolves the mapping matrix more than once per likelihood call (~800 of 1005 ms per eval on a DR1 tile).
Plan
- Carry
sparse_operatorthroughapply_over_sampling(it depends only on noise map, PSF and mask). - Warn, not silently drop, when
apply_mask/apply_noise_scalinginvalidate an attached operator. - Add the missing
convolve_over_sample_size > 1guard toapply_sparse_operator_cpu; carryconvolve_over_sample_size_*through the sparse methods andapply_noise_scaling. - Cache
operated_mapping_matrix_listper inversion instance (imaging, and interferometer if it has the same pattern). - Witness tests red on current main first, then green; full suite; workspace impact grep.
Detailed implementation plan
Affected Repositories
- PyAutoArray (primary)
Branch Survey
| Repository | Current Branch | Dirty? |
|---|---|---|
| ./PyAutoArray | main | clean (level with origin) |
Suggested branch: feature/sparse-operator-oversampling-cache
Detailed changes
autoarray/dataset/imaging/dataset.py
apply_over_sampling: passsparse_operator=self.sparse_operatorto the newImaging.
The existing__init__guard still rejects an operator with
convolve_over_sample_size_pixelization > 1.apply_sparse_operator_cpu:- add the same
convolve_over_sample_size > 1check asapply_sparse_operator
(reuse its error text); - pass
convolve_over_sample_size_lp/pixelizationthrough.
- add the same
apply_sparse_operator: pass the convolve sizes through (currently defaulted to 1).apply_mask,apply_noise_scaling: ifself.sparse_operator is not None, emit
logger.warning(...)saying the operator was discarded because the mask or noise changed,
and must be re-applied last. Do not carry it: it is stale.apply_noise_scaling: also passconvolve_over_sample_size_*through.
autoarray/inversion/inversion/imaging/abstract.py
operated_mapping_matrix_list:@property→@cached_property(already imported, l.4).
autoarray/inversion/inversion/interferometer/abstract.py
- Same change for its
operated_mapping_matrix_list, after confirming it is reached more than
once per likelihood evaluation. - If the property relies on anything mutable after construction, leave it and note why.
Tests (test_autoarray/dataset/imaging/test_dataset.py,
test_autoarray/inversion/inversion/imaging/test_imaging.py, interferometer analogue)
apply_sparse_operator_cpu().apply_over_sampling(...)keeps a non-Nonesparse_operator.
The inversion built from it is the sparse Numba class, and its log likelihood matches the
operator-applied-last order.apply_mask/apply_noise_scalingon an operator-bearing dataset log the warning (caplog).apply_sparse_operator_cpuon a dataset withconvolve_over_sample_size_pixelization=2
raises the same exception as the JAX variant.- One dense
log_likelihood/inversion.reconstruction+mapped_reconstructed_datapass
callspsf.convolved_mapping_matrix_fromexactly once per convolved linear object
(monkeypatch counter). Same for the interferometer transformer.
- Each witness is run red on unfixed source first (the memory rule: a witness can already pass
on main).
Workflow
start_dev → /create_issue on PyAutoArray (title: "fix: apply_over_sampling drops sparse
operator; cache operated_mapping_matrix_list") → register library-dev → /start_library
worktree ~/Code/PyAutoLabs-wt/sparse-operator-oversampling-cache/, branch
feature/sparse-operator-oversampling-cache → implementation delegated to an Opus subagent
(progress file + Monitor) → /ship_library (PR only; merge stays human).
- Heart is STALE (no report / install / release validation), which is informational at start.
Verification
- Full
pytest test_autoarrayin the worktree. JAX tests stay green: the cache is per-instance,
and an inversion is built per evaluation or trace. - Re-run the DR1 A/B harness (scratchpad
sparse_ab.py) against the worktree:- mode A with the old script order must now select
InversionImagingSparseNumba; - dense-route per-eval time should drop by ~40% (987 → ~600 ms) with logL bit-identical.
- mode A with the old script order must now select
- Workspace impact: grep workspaces and
*_workspace_testforapply_sparse_operator/
apply_over_samplingordering and for assertions on the inversion class. No change expected,
because correct-order scripts are unaffected.
Key Files
autoarray/dataset/imaging/dataset.py—apply_over_sampling,apply_sparse_operator(_cpu),apply_mask,apply_noise_scalingautoarray/inversion/inversion/imaging/abstract.py—operated_mapping_matrix_listautoarray/inversion/inversion/interferometer/abstract.py— interferometer analogue
Original Prompt
Click to expand starting prompt
apply_over_sampling drops the sparse operator; dense route convolves the mapping matrix twice
Type: bug
Target: @PyAutoArray
Autonomy: human-required
Request (verbatim)
"file both and then do them now" — the two PyAutoArray fixes found during the Euclid DR1 campaign (2026-09-27).
1. Imaging.apply_over_sampling silently drops sparse_operator
autoarray/dataset/imaging/dataset.py apply_over_sampling (~l.539-577) rebuilds Imaging without
sparse_operator=. Calling apply_sparse_operator_cpu() before apply_over_sampling() therefore
falls back to dense InversionImagingMapping with no warning. euclid_dr1 vis_pix ran dense the whole
campaign: 987 -> 180 ms and 799 -> 152 ms per eval once fixed (science-script reorder, euclid_dr1 32fbf62),
logL identical (<=2.7e-10 nats), identifier unchanged. The operator depends only on noise_map, PSF kernel and
mask, so carrying it through over-sampling is valid. Adjacent: apply_sparse_operator_cpu lacks the
convolve_over_sample_size > 1 guard apply_sparse_operator has, and neither sparse method nor
apply_noise_scaling carries convolve_over_sample_size_*; apply_mask / apply_noise_scaling change
inputs the operator depends on, so they must not carry it but should not drop it silently either.
2. operated_mapping_matrix_list is an uncached @property
autoarray/inversion/inversion/imaging/abstract.py:119. On the dense route it is evaluated twice per
likelihood call (curvature/data-vector path and mapped_reconstructed_operated_data_dict), each a full PSF
fftconvolve of the mapping matrix: ~425 ms each, ~800 of 1005 ms per eval on a DR1 tile. Caching per
inversion instance saves ~40% on the dense route. Check the interferometer analogue for the same pattern.
Witness
- A dataset with
apply_sparse_operator_cpu()thenapply_over_sampling(...)keeps a non-None
sparse_operatorand the inversion factory selects the sparse Numba class (red on current main). - One likelihood evaluation on the dense route calls
psf.convolved_mapping_matrix_fromonce per linear
object, not twice (red on current main).
Evidence: session scratchpad lever_probe.log, sparse_ab.py, ab_*.json.
Activity