Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 27 additions & 3 deletions autoarray/operators/convolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@ def _fine_state_from(self, mask) -> ConvolverState:
mask
The image-resolution mask the over-sampled inputs are defined on.
"""
from autoarray.mask.mask_2d import Mask2D
from autoarray.operators.over_sampling.over_sample_util import (
mask_2d_upscaled_from,
sub_slim_to_fine_slim_from,
Expand All @@ -359,12 +360,35 @@ def _fine_state_from(self, mask) -> ConvolverState:
f"the PSF sampled at the fine resolution."
)

mask_fine = mask_2d_upscaled_from(mask_2d=mask, over_sample_size=s)

blurring_mask = mask.derive_mask.blurring_from(
kernel_shape_native=self.kernel_shape_image_resolution,
allow_padding=True,
)

# When the mask sits close to the image edge, blurring_from pads its output
# to a larger frame (symmetric, parity-preserving). The fine geometry needs
# the image mask and blurring mask on one common frame, so the image mask is
# embedded with the same padding arithmetic; symmetric padding preserves the
# row-major slim ordering of the unmasked pixels, so the permutations and the
# original image mask (used to wrap outputs) remain valid.
if blurring_mask.shape_native != mask.shape_native:
dy = blurring_mask.shape_native[0] - mask.shape_native[0]
dx = blurring_mask.shape_native[1] - mask.shape_native[1]

mask_frame = Mask2D(
mask=np.pad(
np.array(mask),
((dy // 2, dy - dy // 2), (dx // 2, dx - dx // 2)),
constant_values=True,
),
pixel_scales=mask.pixel_scales,
origin=mask.origin,
)
else:
mask_frame = mask

mask_fine = mask_2d_upscaled_from(mask_2d=mask_frame, over_sample_size=s)

blurring_mask_fine = mask_2d_upscaled_from(
mask_2d=blurring_mask, over_sample_size=s
)
Expand All @@ -374,7 +398,7 @@ def _fine_state_from(self, mask) -> ConvolverState:
)

state.sub_slim_to_fine_slim = sub_slim_to_fine_slim_from(
mask_2d=mask, over_sample_size=s
mask_2d=mask_frame, over_sample_size=s
)
state.blurring_sub_slim_to_fine_slim = sub_slim_to_fine_slim_from(
mask_2d=blurring_mask, over_sample_size=s
Expand Down
35 changes: 35 additions & 0 deletions test_autoarray/operators/test_convolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -490,3 +490,38 @@ def test__kernel_shape_image_resolution():
kernel_fine = aa.Array2D.no_mask(values=np.ones((15, 15)), pixel_scales=1.0 / 3.0)
convolver = aa.Convolver(kernel=kernel_fine, convolve_over_sample_size=3)
assert convolver.kernel_shape_image_resolution == (7, 7)


def test__convolve_over_sample_size__blurring_mask_padding__delta_kernel_identity():
# A mask close to the image edge forces blurring_from to pad its output to a
# larger frame; the fine-state geometry must embed the image mask in that same
# frame. With a delta fine kernel, convolution then reduces to binning the
# over-sampled input — any frame/permutation misalignment breaks the identity.
import warnings

mask = aa.Mask2D.circular(shape_native=(11, 11), pixel_scales=1.0, radius=5.0)
s = 2

delta = np.zeros((9, 9))
delta[4, 4] = 1.0
kernel = aa.Array2D.no_mask(values=delta, pixel_scales=1.0 / s)
convolver = aa.Convolver(kernel=kernel, convolve_over_sample_size=s)

with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
state = convolver.state_from(mask=mask)
assert any("Mask padded" in str(x.message) for x in w)

assert state.image_mask.shape_native == (11, 11)

rng = np.random.default_rng(2)
values_sub = rng.random(mask.pixels_in_mask * s**2)

convolver = aa.Convolver(kernel=kernel, state=state, convolve_over_sample_size=s)
convolved = convolver.convolved_image_from(
image=values_sub, blurring_image=None, mask=mask
)

binned = values_sub.reshape(mask.pixels_in_mask, s**2).mean(axis=1)

assert np.array(convolved) == pytest.approx(binned, abs=1.0e-14)
Loading