Skip to content

feat: primary beam for interferometer datasets, transformers and the sparse inversion - #644

Merged
Jammy2211 merged 5 commits into
mainfrom
feature/interferometer-primary-beam
Oct 21, 2026
Merged

Jammy2211 merged 5 commits into
mainfrom
feature/interferometer-primary-beam

Conversation

@Jammy2211

Copy link
Copy Markdown
Collaborator

Closes #640. Community proposal: PyAutoLabs/.github#35 (@HRSAstro).

Summary

Adds an optional primary beam P to interferometer fitting. P lives on the Interferometer dataset as an image on the real-space mask (user-supplied, or the new Gaussian helper with FWHM = 1.13 λ/D). The visibilities are modelled as those of the apparent sky P × I, so every light profile, linear light profile and pixelization reconstructs the true sky I: the transformers apply P on the slim grid in their forward transforms (visibilities_from, transform_mapping_matrix), and the sparse w-tilde inversion carries it on the sparse operator (primary_beam_slim), folding P into the mapper triplet weights (A' = P·A) and the linear-function blocks so F' = Mᵀ P W̃ P M, D' = Mᵀ P D̃. The dirty image, dirty beam and w-tilde kernel are unchanged (image_from stays the un-beamed adjoint), so cached SparseTerms serve any beam. The simulator accepts P. mapped_reconstructed_data_dict stays the true-sky image; model visibilities pick P up in the transformer.

A warning is emitted when any unmasked P is 0 (curvature rank-deficient for source pixels seen only there); no floor is applied (human decision on the plan).

Deviation from the plan: InversionInterferometerSparseNumba (the default CPU route) uses the raw mapper weights and would have silently ignored P; the factory now routes there only when P is None or exactly all ones (primary_beam_is_trivial), and the class raises if built with a non-trivial P. A precomputed sparse_operator passed to an Interferometer that has a P is re-created with P attached.

API Changes

Additive only: Interferometer(primary_beam=) (+ from_fits(primary_beam_path=, primary_beam_hdu=), from_sparse_terms(primary_beam=), from_stream), SimulatorInterferometer(primary_beam=), TransformerDFT/TransformerNUFFT(primary_beam=) + .with_primary_beam(), aa.primary_beam_gaussian_from(mask, frequency_hz, dish_diameter_m, pb_factor=1.13, centre=), InterferometerSparseOperator.primary_beam_slim, mapper_util.sparse_triplets_from(slim_weights=). Results with no beam are bit-identical to before.
See full details below.

Test Plan

  • Witness: P=None and P=ones bit-for-bit (np.array_equal) on numpy and under jax.jit for transformer outputs, sparse/mapping curvature and data vectors, reconstruction, fast_chi_squared, mapped images; brute force on the 7×7 Delaunay fixture with a non-trivial P: sparse curvature == Lᵀ diag(P) W̃ diag(P) L and data vector == Lᵀ diag(P) D̃ to 1e-10 for one mapper, func list + mapper, two mappers, func list + two mappers, func list only.
  • Dense vs sparse with P; DFT vs NUFFT; array-free from_sparse_terms(primary_beam=) vs in-memory; transformer equivalences (DFT, NUFFT, JAX); P through all six dataset rebuild sites; shape mismatch raises; P=0 warns; dirty image/beam unchanged; simulator parity incl. jit; Gaussian helper peak/half-max; slim_weights; dirty_model_image_natural_from.
  • Worktree runs: pytest test_autoarray -q → 2136 passed, 4 xfailed; pytest test_autogalaxy/interferometer test_autogalaxy/operate -q against this branch → 141 passed; import autoarray with jax/jaxlib/nufftax blocked by an import hook works and never imports them.

Heart YELLOW acknowledged by the human on 2026-10-21 (manifest drift 1 mismatch; release validation stale: source moved since rehearsal (PyAutoArray) — a consequence of #641).

Follow-ups (not in this PR): persist P in PyAutoGalaxy analysis.py save_attributes + aggregator rebuild; workspace example + docs; a forced interferometer_route="numba" with a non-trivial P falls back to the FFT route instead of raising; the P=0 warning repeats on each dataset rebuild.

Full API Changes (for automation & release notes)

Added

  • aa.primary_beam_gaussian_from(mask, frequency_hz, dish_diameter_m, pb_factor=1.13, centre=(0.0, 0.0)) — Gaussian primary beam Array2D on the mask
  • Interferometer(..., primary_beam=None); Interferometer.primary_beam, .primary_beam_slim; from_fits(primary_beam_path=None, primary_beam_hdu=0); from_sparse_terms(..., primary_beam=None); from_stream(..., primary_beam=None)
  • SimulatorInterferometer(..., primary_beam=None)
  • TransformerDFT(..., primary_beam=None), TransformerNUFFT(..., primary_beam=None), .with_primary_beam(primary_beam)
  • InterferometerSparseOperator.primary_beam_slim (field); inversion_interferometer_util.primary_beam_is_trivial(primary_beam_slim)
  • mapper_util.sparse_triplets_from(..., slim_weights=None)

Changed Behaviour

  • InversionInterferometerSparseNumba raises when built with a non-trivial primary beam; the factory routes such datasets to the FFT sparse path.
  • transformer.visibilities_from / transform_mapping_matrix multiply by P on the slim grid when the transformer carries one; image_from does not (documented).

Generated by the PyAutoLabs agent workflow.

🤖 Generated with Claude Code

Jammy2211 and others added 5 commits October 21, 2026 11:59
Add autoarray/dataset/interferometer/primary_beam.py with
primary_beam_gaussian_from (FWHM = pb_factor c / nu / D) and the slim
conversion/validation helper. TransformerDFT and TransformerNUFFT take
primary_beam=None, expose with_primary_beam (shallow copy) and multiply
by P on the slim grid in visibilities_from / transform_mapping_matrix
(numpy and JAX); image_from stays the un-beamed adjoint.

Refs #640

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Interferometer takes primary_beam (native or slim, validated against the
real-space mask, warns when any unmasked value is 0), applies it to its
transformer via with_primary_beam and forwards it through the pooled-noise
branch, apply_sparse_operator, apply_sparse_operator_from_chunks,
from_sparse_terms, from_stream and from_fits (primary_beam_path).
InterferometerSparseOperator gains primary_beam_slim; sparse_profile_terms_from
applies image <- P image. SimulatorInterferometer takes primary_beam.

Refs #640

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
mapper_util.sparse_triplets_from gains slim_weights; the sparse inversion
passes the primary beam so curvature = A^T P W~ P A and data vector =
A^T P d~, scales linear-function mapping matrices by P, and keeps the
mapped reconstructed image un-beamed (true sky). dirty_model_image_natural_from
applies P. The numba direct_conv route cannot apply P, so the factory skips
it for beamed datasets and the class raises. Document that P is the caller's
job for operated_mapping_matrix_override.

Refs #640

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
primary_beam_is_trivial (None or every value 1) gates the numba direct_conv
route and its precondition, so primary_beam=ones stays bit-for-bit
primary_beam=None on the default CPU route.

Refs #640

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Bit-for-bit P=None / P=ones parity (numpy and jit), brute-force
F = L^T P W~ P L and D = L^T P d~ on the 7x7 Delaunay fixture for every
block structure, dense vs sparse, DFT vs NUFFT, array-free vs in-memory,
transformer, dataset propagation / validation / warning, simulator,
Gaussian helper and slim_weights tests.

Refs #640

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@Jammy2211
Jammy2211 merged commit ea8a55f into main Oct 21, 2026
3 checks passed
@Jammy2211
Jammy2211 deleted the feature/interferometer-primary-beam branch October 21, 2026 11:28
@Jammy2211 Jammy2211 added the pending-release PR queued for the next release build label Oct 21, 2026
@Jammy2211 Jammy2211 removed the pending-release PR queued for the next release build label Oct 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: primary beam for interferometer datasets, transformers and the sparse inversion

1 participant