Skip to content

Commit 2ebe48e

Browse files
Jammy2211claude
authored andcommitted
feat: linear light profiles under an oversampled PSF (#480)
User-directed scope addition at sign-off: LightProfileLinearObjFuncList. operated_mapping_matrix_override evaluates each profile on the grids' over-sampled coordinates and convolves at the fine resolution (same Grid2DIrregular pass-through as the operate/image consumer switch), so linear light profiles work at convolve_over_sample_size > 1 instead of raising the binned-input guard. Operated linear profiles keep returning the unblurred mapping matrix (no convolution by definition). s=1 path unchanged. Column-level test vs the direct oversampled Convolver (1e-14). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent a4e6561 commit 2ebe48e

2 files changed

Lines changed: 68 additions & 2 deletions

File tree

autogalaxy/profiles/light/linear/abstract.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -340,19 +340,33 @@ def operated_mapping_matrix_override(self) -> Optional[np.ndarray]:
340340
if isinstance(self.light_profile_list[0], LightProfileOperated):
341341
return self.mapping_matrix
342342

343+
if self.psf.convolve_over_sample_size > 1:
344+
# Evaluate each profile on the over-sampled coordinates (per-pixel
345+
# sub-block order, unbinned — the oversampled Convolver's input format)
346+
# so convolution runs at the fine resolution, mirroring
347+
# OperateImage.blurred_image_2d_from.
348+
evaluation_grid = self.grid.over_sampled
349+
evaluation_blurring_grid = self.blurring_grid.over_sampled
350+
convolution_mask = self.grid.mask
351+
else:
352+
evaluation_grid = self.grid
353+
evaluation_blurring_grid = self.blurring_grid
354+
convolution_mask = None
355+
343356
blurred_image_2d_list = []
344357

345358
for pixel, light_profile in enumerate(self.light_profile_list):
346-
image_2d = light_profile.image_2d_from(grid=self.grid, xp=self._xp)
359+
image_2d = light_profile.image_2d_from(grid=evaluation_grid, xp=self._xp)
347360

348361
blurring_image_2d = light_profile.image_2d_from(
349-
grid=self.blurring_grid, xp=self._xp
362+
grid=evaluation_blurring_grid, xp=self._xp
350363
)
351364

352365
blurred_image_2d = self.psf.convolved_image_from(
353366
image=image_2d,
354367
blurring_image=blurring_image_2d,
355368
use_mixed_precision=self.settings.use_mixed_precision,
369+
mask=convolution_mask,
356370
xp=self._xp,
357371
)
358372

test_autogalaxy/profiles/light/linear/test_abstract.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,3 +151,55 @@ def test__setstate__preserves_pytree_token_when_present():
151151
restored.__setstate__(state_with_token)
152152

153153
assert restored.pytree_token == lp.pytree_token
154+
155+
156+
def test__operated_mapping_matrix_override__oversampled_psf__matches_direct_convolver():
157+
# With an oversampled PSF each linear light profile is evaluated on the
158+
# over-sampled coordinates and convolved at the fine resolution — the column
159+
# must equal calling the (phase-2a tested) oversampled Convolver directly.
160+
import numpy as np
161+
import autoarray as aa
162+
from autogalaxy.profiles.light.linear.abstract import (
163+
LightProfileLinearObjFuncList,
164+
)
165+
166+
mask = aa.Mask2D.circular(shape_native=(11, 11), pixel_scales=1.0, radius=3.5)
167+
168+
s = 2
169+
n = 9
170+
c = (np.arange(n) - (n - 1) / 2.0) * (1.0 / s)
171+
yy, xx = np.meshgrid(-c, c, indexing="ij")
172+
kernel = np.exp(-0.5 * (yy**2 + xx**2) / 0.8**2)
173+
kernel = aa.Array2D.no_mask(values=kernel / kernel.sum(), pixel_scales=1.0 / s)
174+
psf = aa.Convolver(kernel=kernel, convolve_over_sample_size=s)
175+
176+
grid = aa.Grid2D.from_mask(mask=mask, over_sample_size=s)
177+
blurring_mask = mask.derive_mask.blurring_from(
178+
kernel_shape_native=psf.kernel_shape_image_resolution, allow_padding=True
179+
)
180+
blurring_grid = aa.Grid2D.from_mask(mask=blurring_mask, over_sample_size=s)
181+
182+
lp_0 = ag.lp_linear.Sersic(
183+
centre=(0.3, -0.4), effective_radius=1.0, sersic_index=2.0
184+
)
185+
lp_1 = ag.lp_linear.Gaussian(centre=(-0.5, 0.2), sigma=0.7)
186+
187+
func_list = LightProfileLinearObjFuncList(
188+
grid=grid,
189+
blurring_grid=blurring_grid,
190+
psf=psf,
191+
light_profile_list=[lp_0, lp_1],
192+
regularization=None,
193+
)
194+
195+
override = np.array(func_list.operated_mapping_matrix_override)
196+
197+
assert override.shape == (mask.pixels_in_mask, 2)
198+
199+
for i, lp in enumerate([lp_0, lp_1]):
200+
image_sub = lp.image_2d_from(grid=grid.over_sampled)
201+
blurring_sub = lp.image_2d_from(grid=blurring_grid.over_sampled)
202+
direct = psf.convolved_image_from(
203+
image=image_sub, blurring_image=blurring_sub, mask=mask
204+
)
205+
assert override[:, i] == pytest.approx(np.array(direct), abs=1.0e-14)

0 commit comments

Comments
 (0)