Skip to content

fix: apply_over_sampling drops sparse operator; cache operated_mapping_matrix_list #585

Closed

Description

@Jammy2211

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_operator through apply_over_sampling (it depends only on noise map, PSF and mask).
  • Warn, not silently drop, when apply_mask / apply_noise_scaling invalidate an attached operator.
  • Add the missing convolve_over_sample_size > 1 guard to apply_sparse_operator_cpu; carry convolve_over_sample_size_* through the sparse methods and apply_noise_scaling.
  • Cache operated_mapping_matrix_list per 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: pass sparse_operator=self.sparse_operator to the new Imaging.
    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 > 1 check as apply_sparse_operator
      (reuse its error text);
    • pass convolve_over_sample_size_lp/pixelization through.
  • apply_sparse_operator: pass the convolve sizes through (currently defaulted to 1).
  • apply_mask, apply_noise_scaling: if self.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 pass convolve_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)

  1. apply_sparse_operator_cpu().apply_over_sampling(...) keeps a non-None sparse_operator.
    The inversion built from it is the sparse Numba class, and its log likelihood matches the
    operator-applied-last order.
  2. apply_mask / apply_noise_scaling on an operator-bearing dataset log the warning (caplog).
  3. apply_sparse_operator_cpu on a dataset with convolve_over_sample_size_pixelization=2
    raises the same exception as the JAX variant.
  4. One dense log_likelihood / inversion.reconstruction + mapped_reconstructed_data pass
    calls psf.convolved_mapping_matrix_from exactly 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_autoarray in 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.
  • Workspace impact: grep workspaces and *_workspace_test for apply_sparse_operator /
    apply_over_sampling ordering 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_scaling
  • autoarray/inversion/inversion/imaging/abstract.py — operated_mapping_matrix_list
  • autoarray/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() then apply_over_sampling(...) keeps a non-None
    sparse_operator and the inversion factory selects the sparse Numba class (red on current main).
  • One likelihood evaluation on the dense route calls psf.convolved_mapping_matrix_from once per linear
    object, not twice (red on current main).

Evidence: session scratchpad lever_probe.log, sparse_ab.py, ab_*.json.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions