Skip to content

operated_mapping_matrix_override is silently ignored for Interferometer #459

Closed

Description

@HRSAstro

Description

Imaging checks the override, interferometer doesn't:

e.g. in operated_mapping_matrix_list:
self.psf.convolved_mapping_matrix_from(...)
if linear_obj.operated_mapping_matrix_override is None
else self.linear_func_operated_mapping_matrix_dict[linear_obj]
(same pattern again in linear_func_operated_mapping_matrix_dict).

autoarray/inversion/inversion/interferometer/abstract.py:63-78 has no such branch:
@Property
def operated_mapping_matrix_list(self) -> List[np.ndarray]:
return [
self.transformer.transform_mapping_matrix(
mapping_matrix=linear_obj.mapping_matrix, xp=self._xp
)
for linear_obj in self.linear_obj_list
]
It always pulls linear_obj.mapping_matrix and pushes it through the NUFFT — linear_obj.operated_mapping_matrix_override is never read on this path.

So if you set operated_mapping_matrix_override on a LinearObj/LinearObjFuncList, it works for imaging fits and is silently ignored for interferometer fits — the raw mapping_matrix is transformed regardless.

Steps to Reproduce

Define a LinearObjFuncList subclass whose operated_mapping_matrix_override returns a distinguishable sentinel matrix, then build an Inversion from it against both an Imaging dataset and an Interferometer dataset and compare inversion.operated_mapping_matrix_list[0] to the sentinel in each case:

import numpy as np
import autoarray as aa
from autoarray.inversion.linear_obj.func_list import AbstractLinearObjFuncList

class OverridingLinearObjFuncList(AbstractLinearObjFuncList):
def init(self, grid, n_data_points):
super().init(grid=grid, regularization=None)
self._n_data_points = n_data_points

@property
def params(self):
    return 1

@property
def mapping_matrix(self):
    return np.ones((self._n_data_points, 1))

@property
def operated_mapping_matrix_override(self):
    return 999.0 * np.ones((self._n_data_points, 1))  # sentinel

def pixel_signals_from(self, signal_scale):
    return np.ones(self.params)

mask = aa.Mask2D.circular(shape_native=(9, 9), radius=3.0, pixel_scales=0.1)
grid = aa.Grid2D.from_mask(mask=mask)

--- Imaging: override IS respected ---

imaging = aa.Imaging(
data=aa.Array2D.full(1.0, shape_native=(9, 9), pixel_scales=0.1).apply_mask(mask=mask),
noise_map=aa.Array2D.full(1.0, shape_native=(9, 9), pixel_scales=0.1).apply_mask(mask=mask),
psf=aa.Convolver.no_blur(pixel_scales=(0.1, 0.1)),
)
inv_imaging = aa.Inversion(
dataset=imaging,
linear_obj_list=[OverridingLinearObjFuncList(grid=grid, n_data_points=mask.pixels_in_mask)],
)
print(np.allclose(inv_imaging.operated_mapping_matrix_list[0], 999.0)) # True

--- Interferometer: override is IGNORED ---

n_vis = 5
interferometer = aa.Interferometer(
data=aa.Visibilities.full(1.0, shape_slim=(n_vis,)),
noise_map=aa.VisibilitiesNoiseMap.full(1.0, shape_slim=(n_vis,)),
uv_wavelengths=np.ones((n_vis, 2)),
real_space_mask=mask,
transformer_class=aa.TransformerDFT,
)
inv_interferometer = aa.Inversion(
dataset=interferometer,
linear_obj_list=[OverridingLinearObjFuncList(grid=grid, n_data_points=mask.pixels_in_mask)],
)
print(np.allclose(inv_interferometer.operated_mapping_matrix_list[0], 999.0)) # False — bug

Expected Behavior

The second print should be True — operated_mapping_matrix_override should either be honored consistently across both dataset types, or the property/docstring on LinearObj should say explicitly that it is imaging-only.

Actual Behavior

Imaging operated_mapping_matrix matches override: True
Interferometer operated_mapping_matrix matches override: False
Interferometer operated_mapping_matrix (actual, first row): [80.99999999-4.1e-20j]

Environment

Python -- 3.11.15
OS -- macOS 26.6.1, arm64 
autoarray -- 2026.7.29.1

Activity

  1. Jammy2211 commented on Aug 21, 2026

    @Jammy2211
    Collaborator

    Hi @HRSAstro — thanks for the report. I'm Jammy's CLI assistant; I'll take a look and post back shortly with a plan, or follow-up questions if I need more detail.


    Generated by Claude Code

  2. Jammy2211 commented on Aug 21, 2026

    @Jammy2211
    Collaborator

    Here's what I'm planning to do — let me know if anything looks off.

    You're right, and thanks for the clean sentinel reproducer — the interferometer inversion never reads operated_mapping_matrix_override, so it silently falls back to transforming the raw mapping_matrix. We're going to support the override properly for interferometer datasets rather than document it away as imaging-only.

    One thing worth flagging on semantics: to stay consistent with imaging (where the override is the already-operated matrix placed directly into operated_mapping_matrix_list), the interferometer override will be treated as data-space — i.e. a complex matrix of shape [total_visibilities, params] that bypasses the NUFFT entirely. Your reproducer's second print then comes back True with a visibility-shaped sentinel. If your use case instead needs a real-space matrix that still goes through the transform, say so — that's the mapping_matrix path, and we can talk about whether a separate hook makes sense.

    Plan

    • Honor the override in InversionInterferometer (single landing site feeding both formalisms' matrices), with a clear error rather than silent ignoring anywhere it can't apply.
    • Guard PyAutoGalaxy's linear light profiles so interferometer fits keep their current NUFFT path (their override is image-space/PSF-based and doesn't apply).
    • New unit tests either side, plus operated_light_profile example packages for interferometer in autogalaxy_workspace and autolens_workspace, mirroring the imaging ones.
    Detailed implementation plan

    Affected repositories

    • PyAutoArray (primary)
    • PyAutoGalaxy
    • autogalaxy_workspace, autolens_workspace

    Work classification

    Both (library + workspace)

    Implementation steps

    1. autoarray/inversion/inversion/interferometer/abstract.py — operated_mapping_matrix_list returns linear_obj.operated_mapping_matrix_override when not None, else the current transform_mapping_matrix(...); validate the override's shape/dtype against [total_visibilities, params] with an informative error.
    2. autoarray/inversion/linear_obj/linear_obj.py — document the per-dataset contract on operated_mapping_matrix_override (imaging: real [mask_pixels, params]; interferometer: complex [visibilities, params]).
    3. Audit the sparse (w-tilde) interferometer formalism: the all-func-list case routes to InversionInterferometerMapping (factory), but a mixed mapper + func-list inversion on the sparse path must raise clearly if a func list carries an override it can't honor.
    4. autogalaxy/profiles/light/linear/abstract.py — LightProfileLinearObjFuncList.operated_mapping_matrix_override returns None when self.psf is None (interferometer), preventing an attribute crash and preserving the existing mapping-matrix → NUFFT behaviour for linear and operated profiles.
    5. Tests: test_autoarray interferometer inversion tests using the existing override-aware mocks; test_autogalaxy test for the psf=None guard.
    6. Workspace: add scripts/interferometer/features/operated_light_profile/ (README, modeling.py, simulator.py) to autogalaxy_workspace and autolens_workspace, mirrored from the imaging packages with interferometer idioms taken from scripts/interferometer/features/linear_light_profiles/; regenerate notebooks via the standard tooling.

    Branch

    feature/interferometer-operated-override

    I'll post back when the work is in progress and again when there's a PR.


    Generated by Claude Code

  3. Jammy2211 commented on Aug 21, 2026

    @Jammy2211
    Collaborator

    @HRSAstro the fix is implemented and up for review — the override is now honored for interferometer inversions:

    One heads-up for your code: as flagged above, the override is treated as data-space, consistent with imaging — for an interferometer inversion it bypasses the NUFFT entirely, so it must be a complex matrix of shape [total_visibilities, params] (e.g. an analytic Fourier transform of your profile). Your reproducer's real-space-shaped sentinel would now raise a shape error pointing you at exactly this. If you actually need a real-space override that still goes through the transform, that's the mapping_matrix path — tell us about your use case and we can look at a separate hook.

    I'll post back here once the PRs are merged and the change is in a release.


    Generated by Claude Code

  4. Jammy2211 commented on Aug 21, 2026

    @Jammy2211
    Collaborator

    @HRSAstro all four PRs are merged — the interferometer inversion now honors operated_mapping_matrix_override on main, alongside the new interferometer operated_light_profile example packages in both workspaces.

    The change reaches PyPI with the next nightly release; until then you can use it by installing autoarray from main. Remember the shape contract from my previous comment: for interferometer fits your override must be a complex [total_visibilities, params] matrix (it bypasses the NUFFT entirely), and a real-space-shaped override now raises an error explaining exactly that.

    Thanks again for the sharp report — the sentinel reproducer made this an easy one to pin down. If the data-space semantics don't fit your use case, or anything misbehaves once you're on the new version, just reply here.


    Generated by Claude Code

  5. Jammy2211 commented on Aug 22, 2026

    @Jammy2211
    Collaborator

    @HRSAstro — the fix is out. autoarray 2026.8.22.1 is on PyPI and contains #460, so operated_mapping_matrix_override is now honored for interferometer inversions on a released version:

    pip install --upgrade autoarray
    

    PyAutoGalaxy and PyAutoLens went out at the same version if you're on the wider stack. The interferometer operated_light_profile example packages are live in both workspaces too (PyAutoLabs/autogalaxy_workspace#220, PyAutoLabs/autolens_workspace#494).

    Recapping the one contract that bites: for interferometer fits the override is data-space — a complex [total_visibilities, params] matrix that bypasses the NUFFT entirely. The real-space-shaped sentinel from your reproducer now raises a shape error spelling that out rather than being silently ignored.

    Leaving this closed, but do reply here if the data-space semantics turn out to be wrong for your use case — a real-space override that still goes through the transform would be a separate hook, and it'd be worth hearing what you're actually computing.


    Generated by Claude Code

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

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions