Repository navigation
feat: primary beam for interferometer datasets, transformers and the sparse inversion - #644
Merged
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #640. Community proposal: PyAutoLabs/.github#35 (@HRSAstro).
Summary
Adds an optional primary beam
Pto interferometer fitting.Plives on theInterferometerdataset 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 skyP × I, so every light profile, linear light profile and pixelization reconstructs the true skyI: the transformers applyPon 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), foldingPinto the mapper triplet weights (A' = P·A) and the linear-function blocks soF' = Mᵀ P W̃ P M,D' = Mᵀ P D̃. The dirty image, dirty beam and w-tilde kernel are unchanged (image_fromstays the un-beamed adjoint), so cachedSparseTermsserve any beam. The simulator acceptsP.mapped_reconstructed_data_dictstays the true-sky image; model visibilities pickPup in the transformer.A warning is emitted when any unmasked
Pis 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 ignoredP; the factory now routes there only whenPisNoneor exactly all ones (primary_beam_is_trivial), and the class raises if built with a non-trivialP. A precomputedsparse_operatorpassed to anInterferometerthat has aPis re-created withPattached.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
np.array_equal) on numpy and underjax.jitfor 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.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.pytest test_autoarray -q→ 2136 passed, 4 xfailed;pytest test_autogalaxy/interferometer test_autogalaxy/operate -qagainst this branch → 141 passed;import autoarraywith 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.pysave_attributes + aggregator rebuild; workspace example + docs; a forcedinterferometer_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 beamArray2Don the maskInterferometer(..., 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
InversionInterferometerSparseNumbaraises when built with a non-trivial primary beam; the factory routes such datasets to the FFT sparse path.transformer.visibilities_from/transform_mapping_matrixmultiply byPon the slim grid when the transformer carries one;image_fromdoes not (documented).Generated by the PyAutoLabs agent workflow.
🤖 Generated with Claude Code