Skip to content

feat: datacube shared-state consumer for AnalysisInterferometer (sub-task B) #565

Closed

Description

@Jammy2211

Overview

Sub-task B of the analysis_shared_state epic (tracker:
PyAutoPrompt/z_features/analysis_shared_state.md). Now that the generic mechanism
(PyAutoFit Analysis.shared_state_from + shared= kwarg) is on main (sub-task A, #1307),
this wires up the motivating consumer: the ALMA datacube likelihood, where N spectral
channels share one lens model and ~97% of each channel's inversion-setup work (ray-trace +
Delaunay mapper + mapping matrix L + curvature F = LᵀW̃L) is identical and currently rebuilt
N times. Implementing the lensing shared_state_from collapses that to one build per
evaluation, targeting ~17x on the inversion-setup block for a 34-channel cube.

Plan

  • Implement AnalysisInterferometer.shared_state_from(instance): build the Tracer and the
    channel-invariant inversion pieces (mapper + L, and F where uv/noise are channel-invariant)
    once, returning them as a JAX pytree.
  • Make AnalysisInterferometer.log_likelihood_function(instance, shared=None) consume the
    shared pieces in fit_from instead of rebuilding; shared is None falls back to today's path.
  • Resolve the architectural question: FitInterferometer / the autoarray Inversion/mapper
    must accept pre-built shared pieces — determine whether this needs PyAutoArray changes or can
    be threaded purely from PyAutoLens.
  • Add the channel-invariance precondition + per-channel fallback (the consumer owns correctness).
  • Phase 5 (after library ships): opt the datacube workspace scripts in, add an
    autolens_workspace_test fast-assert, and re-measure in autolens_profiling.
Detailed implementation plan

Affected Repositories

  • Jammy2211/PyAutoLens (primary — AnalysisInterferometer + FitInterferometer)
  • Jammy2211/PyAutoArray (likely — Inversion/mapper "use precomputed pieces" path; confirm at /start_library)
  • Jammy2211/autolens_workspace (Phase 5 — datacube scripts)
  • Jammy2211/autolens_workspace_test (Phase 5 — fast-assert)
  • PyAutoLabs/autolens_profiling (Phase 5 — re-measure)

Work Classification

Both (library: PyAutoLens [+ likely PyAutoArray] first; workspaces/profiling at Phase 5)

Branch Survey

Repository Current Branch Dirty?
./PyAutoLens main clean
./autolens_workspace main clean
./autolens_workspace_test main dirty (unregistered, Phase-5 only)
./autolens_profiling main clean

Suggested branch: feature/datacube-shared-state
Worktree root: ~/Code/PyAutoLabs-wt/datacube-shared-state/

Implementation Steps (Phase 4 — library)

  1. Scope the inversion-setup path: FitInterferometer -> autoarray Inversion/mapper/curvature build. Decide where a "shared precomputed mapper+L+F" injection point goes (PyAutoLens-only vs PyAutoArray).
  2. AnalysisInterferometer.shared_state_from(instance) — build Tracer + shared inversion pieces once; return a registered pytree (recomputed inside jit each eval, not memoised).
  3. AnalysisInterferometer.log_likelihood_function(self, instance, shared=None) + fit_from(instance, shared=None) — consume shared pieces; shared is None rebuilds (single-channel path unchanged).
  4. Channel-invariance precondition + per-channel fallback when uv_wavelengths/noise_map aren't ~channel-invariant.
  5. Tests: test_autolens/interferometer/ — shared vs unshared FitInterferometer give identical figure_of_merit; compute-once.

Key Files

  • PyAutoLens/autolens/interferometer/model/analysis.py — log_likelihood_function, fit_from, pytree registration
  • PyAutoLens/autolens/interferometer/fit_interferometer.py — FitInterferometer (where the inversion is built)
  • PyAutoArray Inversion/mapper/curvature (TBD — confirm injection point)
  • Phase 5: autolens_workspace/scripts/interferometer/features/datacube/{likelihood_function,modeling,delaunay}.py, autolens_profiling/likelihood_breakdown/datacube/delaunay.py

Original Prompt

Click to expand starting prompt

Datacube consumer for the cross-Analysis shared-state mechanism

Sub-task B of the analysis_shared_state epic (see
PyAutoPrompt/z_features/analysis_shared_state.md). Do not /start_dev this
until sub-task A (the PyAutoFit mechanism + 1D Gaussian toy + autofit workspace
tutorial/tests, PyAutoFit#1307) is close to shipping — the mechanism it consumes
is delivered there.

Primary repo: @PyAutoLens. Consumers/proof: @autolens_workspace,
@autolens_workspace_test, @autolens_profiling.

Depends on (delivered by sub-task A)

PyAutoFit ships the generic, domain-agnostic protocol:

  • Analysis.shared_state_from(instance) -> None (opt-in, default None), the
    per-evaluation cross-factor sibling of modify_before_fit.
  • log_likelihood_function(self, instance, shared=None, ...) — defaulted kwarg.
  • FactorGraphModel.log_likelihood_function computes the shared object once
    from the lead factor before the per-factor loop and forwards it to each factor
    only when non-None (so non-cube graphs are byte-for-byte unchanged).

The 1D Gaussian toy in af.ex + autofit_workspace + autofit_workspace_test
is the worked, tested reference for how a consumer implements shared_state_from
and a shared-aware log_likelihood_function. Mirror it.

Scope (Phases 4-5 of the original prompt)

Phase 4 — PyAutoLens: the datacube consumer

  • On the interferometer datacube path, implement the lensing-specific
    shared_state_from: ray-trace the shared lens model once, build the Delaunay
    mapper + mapping matrix L (and, where uv_wavelengths/noise_map are
    channel-invariant, the curvature F = LᵀW̃L) once, returning them as a normal
    JAX pytree of traced arrays (recomputed inside the jitted region each eval,
    never memoised on the instance — see feedback_jax_closure_cache_busts).
  • Make AnalysisInterferometer.log_likelihood_function(self, instance, shared=None)
    consume the shared object in place of its own rebuild; the shared is None
    fallback rebuilds everything so the single-channel path is unchanged.
  • Per-channel work that remains: data vector D = Lᵀ·dirty_image (channel
    visibilities), NNLS reconstruction, log-evidence.
  • Fall back to the current per-channel path when the channel-invariance
    precondition does not hold (uv_wavelengths/noise_map not ~channel-invariant —
    i.e. outside the narrow-emission-line regime). The consumer owns this guard;
    PyAutoFit trusts the provider.

Phase 5 — autolens_workspace + autolens_workspace_test + profiling

  • Update the datacube modeling/likelihood scripts
    (autolens_workspace/scripts/interferometer/features/datacube/{likelihood_function,modeling,delaunay}.py)
    to opt into the shared path.
  • Add a fast-assert datacube script to autolens_workspace_test mirroring the
    autofit_workspace_test pattern: prove shared_state_from runs once per eval
    (counter), shared-vs-unshared likelihood equality, and a tiny end-to-end run.
  • Re-run autolens_profiling/likelihood_breakdown/datacube/delaunay.py (carrying
    the inversion-setup sub-decomposition step) and record the new cube cost. Per
    the decomposition, ~97% of the per-channel inversion work is shareable, so the
    inversion-setup block should drop ~17× for a 34-channel cube (≈60 s → ≈3.5 s);
    the cube total drops from ~170 s toward the per-channel residual (data-vector
    matmul + NNLS + log-ev) plus one shared mapper+L+F build. Compare against the
    inversion_setup_decompose_*.json artifact for the channel-invariant/variant
    split that sets the ceiling. Re-measure at ALMA scale on a quiet A100 to pin the
    cube-level number (laptop SMA seconds are provisional; the ratios are robust).

Critical files

PyAutoLens (Phase 4):

  • the AnalysisInterferometer likelihood path + interferometer Inversion/mapper construction

Workspace / test / profiling (Phase 5):

  • autolens_workspace/scripts/interferometer/features/datacube/{likelihood_function,modeling,delaunay}.py
  • autolens_workspace_test/ — new fast-assert datacube script
  • autolens_profiling/likelihood_breakdown/datacube/delaunay.py
  • autolens_profiling/likelihood_runtime/OPTIMIZATION_NOTES.md

Out of scope

  • A bespoke DataCube data class / cube-specific Inversion (the rejected option).
  • The dense-route variant (production uses the sparse / w̃ route; dense is not the target).
  • Cross-factor gradients — likelihood value only.

Cross-references

  • PyAutoPrompt/z_features/analysis_shared_state.md — the epic tracker
  • PyAutoFit#1307 — sub-task A (the mechanism + toy this consumes)
  • autolens_workspace#120 — Aris's shared-Lᵀ W̃ L optimisation, the origin
  • PyAutoPrompt/issued/alma_datacube.md — Aris's Slack design + channel-invariance caveat
  • PyAutoPrompt/autoarray/datacube.md — the "analysis list API does not share information" problem statement

Activity

  1. Jammy2211 commented on Jun 8, 2026

    @Jammy2211
    CollaboratorAuthor

    Scoping confirmed — 2026-06-07 (at /start_library)

    Traced the inversion-setup path: AnalysisInterferometer.fit_from → FitInterferometer.inversion (cached_property) → TracerToInversion.inversion (PyAutoLens) → aa.AbstractInversion (PyAutoArray). The mapper, mapping matrix L and curvature F live in PyAutoArray (autoarray/inversion/inversion/), so this is confirmed a PyAutoLens + PyAutoArray library task (not PyAutoLens-only).

    The autoarray AbstractInversion already splits cleanly along the share boundary (all @cached_property):

    • Channel-invariant / shareable: mapping_matrix (L), curvature_matrix (F = LᵀW̃L), the mapper, and the curvature/reg log-det terms.
    • Per-channel: data_vector (D), reconstruction (s), mapped_reconstructed_data, the data chi-squared term.

    Injection design: AnalysisInterferometer.shared_state_from(instance) builds the Tracer + the channel-invariant inversion pieces once; each channel's Inversion is constructed reusing those (pre-seeding the cached mapper/L/F) so only data_vector/reconstruction run per channel. shared is None rebuilds everything (single-channel path unchanged). Per-channel fallback when uv_wavelengths/noise_map aren't ~channel-invariant.

    Worktree set up: ~/Code/PyAutoLabs-wt/datacube-shared-state (PyAutoLens + PyAutoArray on feature/datacube-shared-state).

  2. Jammy2211 commented on Jun 8, 2026

    @Jammy2211
    CollaboratorAuthor

    Library PR Created (curvature-sharing) — 2026-06-08

    The curvature-sharing increment of sub-task B is shipped across both library repos. All PRs are pending-release.

    PRs

    What shipped

    • Preloads API (PyAutoArray): aa.AbstractPreloads / aa.PreloadsInterferometer(curvature_matrix=...); the inversion stack + factory take an optional preloads=; the sparse interferometer inversion returns the preloaded curvature_matrix (F = LᵀW̃L) directly, skipping the dominant rebuild. The naming/history (preloads were removed once for being bug-prone; safe in the explicit-invariance shared-likelihood case) is recorded in the AbstractPreloads docstring.
    • datacube shared-state (PyAutoLens): AnalysisInterferometer.shared_preloads flag + shared_state_from(instance) building the channel-invariant PreloadsInterferometer once; preloads/shared threaded through fit_from → FitInterferometer → TracerToInversion → inversion_from; PreloadsInterferometer registered as a JAX pytree.
    • Verified: test_autoarray 841 + test_autolens 331 pass; shared vs unshared figure_of_merit identical; JAX jit shared-path likelihood matches numpy.

    This is the dominant ~86% of the shareable inversion-setup cost (F).

    Remaining

    • Slice 2b (library): mapper-sharing — add a mapper field to PreloadsInterferometer + a reuse path in TracerToInversion so the Delaunay triangulation isn't rebuilt per channel (→ ~97% shareable).
    • Phase 5 (workspace): datacube scripts opt into shared_preloads=True; autolens_workspace_test fast-assert + the committed JAX assertion; autolens_profiling re-measure.
  3. Jammy2211 commented on Jun 8, 2026

    @Jammy2211
    CollaboratorAuthor

    Shipped — 2026-06-08

    PRs

    Summary

    The datacube shared-state consumer work has landed across the library and workspace repositories. The remaining PyAutoPrompt active-task entry was stale after the previous session hit a limit; it has been moved to complete so follow-up workspace-test work can proceed without a false repo claim.

  4. Jammy2211 commented on Jul 10, 2026

    @Jammy2211
    CollaboratorAuthor

    Closing — measurement recorded (2026-07-10)

    The one outstanding Phase 5 item (the autolens_profiling re-measure) is now recorded on main (autolens_profiling 675a902):

    Config unshared shared speed-up
    sma × 4 ch, local_cpu_fp64 (v2026.7.6.649) 1282.7 ms/eval 316.1 ms/eval 4.06×

    At N = 4 identical channels this is effectively the ideal ceiling — the per-channel inversion rebuild collapses entirely. Run on current main a month after the merge, so it doubles as a regression check on the shipped path (curvature and Slice-2b mapper sharing both verified present).

    Remaining follow-up (tracked in likelihood_runtime/OPTIMIZATION_NOTES.md, not this issue): the authoritative --instrument alma_high run on a quiet A100, queued for the next RAL HPC session.

    All library + workspace PRs (#344, #566, workspace #218, workspace_test #138, profiling #47) merged 2026-06-08. Closing.

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