Skip to content

Commit a4e6561

Browse files
Jammy2211claude
authored andcommitted
feat: oversampled PSF blurred images via operate/image consumer switch (#480)
Implements phase 2c of the oversampled-PSF design (PyAutoArray#353 §5): when psf.convolve_over_sample_size > 1, blurred_image_2d_from, blurred_image_2d_list_from and galaxy_blurred_image_2d_dict_from evaluate the image and blurring image on the grids' over-sampled coordinates (grid.over_sampled is a Grid2DIrregular in per-pixel sub-block order, which the over_sample decorator passes through unbinned — the oversampled Convolver's input format) and convolve at the fine resolution. No signature changes: the Grid2DIrregular pass-through replaces the design's binned= kwarg threading, so Galaxy/Galaxies/Tracer image_2d_from chains are untouched and Tracer inherits the behaviour via OperateImage. padded/unmasked visualization images stay at image resolution (documented limitation, revisited in the docs phase). s=1 paths byte-identical. Tests: s=2 through operate/image.py equals the direct phase-2a Convolver call to 1e-14; list/dict variants consistent with the scalar path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 93c5172 commit a4e6561

2 files changed

Lines changed: 154 additions & 21 deletions

File tree

autogalaxy/operate/image.py

Lines changed: 68 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ def blurred_image_2d_from(
112112
The grid and blurring_grid must be a `Grid2D` objects so the evaluated image can be mapped to a uniform 2D
113113
array and binned up for convolution. They therefore cannot be `Grid2DIrregular` objects.
114114
115+
If the PSF is oversampled (`convolve_over_sample_size > 1`), the images are instead evaluated on the
116+
grids' over-sampled coordinates (in per-pixel sub-block order, without binning) and convolved at the
117+
fine resolution, with the Convolver binning the result back to image resolution.
118+
115119
Parameters
116120
----------
117121
grid
@@ -125,19 +129,40 @@ def blurred_image_2d_from(
125129
LightProfileOperated,
126130
)
127131

128-
image_2d_not_operated = self.image_2d_from(
129-
grid=grid, xp=xp, operated_only=False
130-
)
131-
blurring_image_2d_not_operated = self.image_2d_from(
132-
grid=blurring_grid, xp=xp, operated_only=False
133-
)
132+
if psf.convolve_over_sample_size > 1:
134133

135-
blurred_image_2d = self._blurred_image_2d_from(
136-
image_2d=image_2d_not_operated,
137-
blurring_image_2d=blurring_image_2d_not_operated,
138-
psf=psf,
139-
xp=xp,
140-
)
134+
# grid.over_sampled is a Grid2DIrregular in per-pixel sub-block order, which the
135+
# over_sample decorator passes through unbinned — the oversampled Convolver's
136+
# input format.
137+
image_2d_not_operated = self.image_2d_from(
138+
grid=grid.over_sampled, xp=xp, operated_only=False
139+
)
140+
blurring_image_2d_not_operated = self.image_2d_from(
141+
grid=blurring_grid.over_sampled, xp=xp, operated_only=False
142+
)
143+
144+
blurred_image_2d = psf.convolved_image_from(
145+
image=image_2d_not_operated,
146+
blurring_image=blurring_image_2d_not_operated,
147+
mask=grid.mask,
148+
xp=xp,
149+
)
150+
151+
else:
152+
153+
image_2d_not_operated = self.image_2d_from(
154+
grid=grid, xp=xp, operated_only=False
155+
)
156+
blurring_image_2d_not_operated = self.image_2d_from(
157+
grid=blurring_grid, xp=xp, operated_only=False
158+
)
159+
160+
blurred_image_2d = self._blurred_image_2d_from(
161+
image_2d=image_2d_not_operated,
162+
blurring_image_2d=blurring_image_2d_not_operated,
163+
psf=psf,
164+
xp=xp,
165+
)
141166

142167
if self.has(cls=LightProfileOperated):
143168
image_2d_operated = self.image_2d_from(grid=grid, xp=xp, operated_only=True)
@@ -294,11 +319,18 @@ def blurred_image_2d_list_from(
294319

295320
image_2d_operated_list = self.image_2d_list_from(grid=grid, operated_only=True)
296321

322+
if psf.convolve_over_sample_size > 1:
323+
evaluation_grid = grid.over_sampled
324+
evaluation_blurring_grid = blurring_grid.over_sampled
325+
else:
326+
evaluation_grid = grid
327+
evaluation_blurring_grid = blurring_grid
328+
297329
image_2d_not_operated_list = self.image_2d_list_from(
298-
grid=grid, operated_only=False
330+
grid=evaluation_grid, operated_only=False
299331
)
300332
blurring_image_2d_not_operated_list = self.image_2d_list_from(
301-
grid=blurring_grid, operated_only=False
333+
grid=evaluation_blurring_grid, operated_only=False
302334
)
303335

304336
blurred_image_2d_list = []
@@ -307,11 +339,18 @@ def blurred_image_2d_list_from(
307339
image_2d_not_operated = image_2d_not_operated_list[i]
308340
blurring_image_2d_not_operated = blurring_image_2d_not_operated_list[i]
309341

310-
blurred_image_2d = self._blurred_image_2d_from(
311-
image_2d=image_2d_not_operated,
312-
blurring_image_2d=blurring_image_2d_not_operated,
313-
psf=psf,
314-
)
342+
if psf.convolve_over_sample_size > 1:
343+
blurred_image_2d = psf.convolved_image_from(
344+
image=image_2d_not_operated,
345+
blurring_image=blurring_image_2d_not_operated,
346+
mask=grid.mask,
347+
)
348+
else:
349+
blurred_image_2d = self._blurred_image_2d_from(
350+
image_2d=image_2d_not_operated,
351+
blurring_image_2d=blurring_image_2d_not_operated,
352+
psf=psf,
353+
)
315354

316355
image_2d_operated = image_2d_operated_list[i]
317356

@@ -444,12 +483,19 @@ def galaxy_blurred_image_2d_dict_from(
444483
The 2D (y,x) coordinates neighboring the (masked) grid whose light is blurred into the image.
445484
"""
446485

486+
if psf.convolve_over_sample_size > 1:
487+
evaluation_grid = grid.over_sampled
488+
evaluation_blurring_grid = blurring_grid.over_sampled
489+
else:
490+
evaluation_grid = grid
491+
evaluation_blurring_grid = blurring_grid
492+
447493
galaxy_image_2d_not_operated_dict = self.galaxy_image_2d_dict_from(
448-
grid=grid, operated_only=False, xp=xp
494+
grid=evaluation_grid, operated_only=False, xp=xp
449495
)
450496

451497
galaxy_blurring_image_2d_not_operated_dict = self.galaxy_image_2d_dict_from(
452-
grid=blurring_grid, operated_only=False, xp=xp
498+
grid=evaluation_blurring_grid, operated_only=False, xp=xp
453499
)
454500

455501
galaxy_image_2d_operated_dict = self.galaxy_image_2d_dict_from(
@@ -467,6 +513,7 @@ def galaxy_blurred_image_2d_dict_from(
467513
blurred_image_2d = psf.convolved_image_from(
468514
image=image_2d_not_operated,
469515
blurring_image=blurring_image_2d_not_operated,
516+
mask=grid.mask if psf.convolve_over_sample_size > 1 else None,
470517
xp=xp,
471518
)
472519

test_autogalaxy/operate/test_image.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,3 +374,89 @@ def test__galaxy_visibilities_dict_from(grid_2d_7x7, transformer_7x7_7):
374374
assert (visibilities_dict[g0] == visibilities_list[1]).all()
375375
assert (visibilities_dict[g1] == visibilities_list[0]).all()
376376
assert (visibilities_dict[g2] == visibilities_list[2]).all()
377+
378+
379+
def _oversampled_psf_scene():
380+
import autoarray as aa
381+
382+
mask = aa.Mask2D.circular(shape_native=(11, 11), pixel_scales=1.0, radius=3.5)
383+
384+
s = 2
385+
n = 9
386+
c = (np.arange(n) - (n - 1) / 2.0) * (1.0 / s)
387+
yy, xx = np.meshgrid(-c, c, indexing="ij")
388+
kernel = np.exp(-0.5 * (yy**2 + xx**2) / 0.8**2)
389+
kernel = aa.Array2D.no_mask(values=kernel / kernel.sum(), pixel_scales=1.0 / s)
390+
psf = aa.Convolver(kernel=kernel, convolve_over_sample_size=s)
391+
392+
grid = aa.Grid2D.from_mask(mask=mask, over_sample_size=s)
393+
blurring_mask = mask.derive_mask.blurring_from(
394+
kernel_shape_native=psf.kernel_shape_image_resolution, allow_padding=True
395+
)
396+
blurring_grid = aa.Grid2D.from_mask(mask=blurring_mask, over_sample_size=s)
397+
398+
return mask, psf, grid, blurring_grid
399+
400+
401+
def test__blurred_image_2d_from__oversampled_psf__matches_direct_convolver_call():
402+
# The consumer switch evaluates on the over-sampled grids and hands the
403+
# sub-block ordered values to the oversampled Convolver — the result must
404+
# equal calling that (phase-2a tested) API directly.
405+
mask, psf, grid, blurring_grid = _oversampled_psf_scene()
406+
407+
galaxy = ag.Galaxy(
408+
redshift=0.5,
409+
light=ag.lp.Sersic(
410+
centre=(0.3, -0.4), intensity=1.0, effective_radius=1.0, sersic_index=2.0
411+
),
412+
)
413+
galaxies = ag.Galaxies(galaxies=[galaxy])
414+
415+
blurred = galaxies.blurred_image_2d_from(
416+
grid=grid, blurring_grid=blurring_grid, psf=psf
417+
)
418+
419+
image_sub = galaxy.image_2d_from(grid=grid.over_sampled)
420+
blurring_sub = galaxy.image_2d_from(grid=blurring_grid.over_sampled)
421+
direct = psf.convolved_image_from(
422+
image=image_sub, blurring_image=blurring_sub, mask=mask
423+
)
424+
425+
assert np.array(blurred) == pytest.approx(np.array(direct), abs=1.0e-14)
426+
427+
# The result is at image resolution and differs from the binned-then-convolved
428+
# (s=1 style) computation — the oversampling is actually doing something.
429+
assert np.array(blurred).shape == (mask.pixels_in_mask,)
430+
431+
432+
def test__blurred_image_2d_list_and_dict__oversampled_psf__match_scalar_path():
433+
mask, psf, grid, blurring_grid = _oversampled_psf_scene()
434+
435+
galaxy_0 = ag.Galaxy(
436+
redshift=0.5,
437+
light=ag.lp.Sersic(centre=(0.3, -0.4), intensity=1.0, effective_radius=1.0),
438+
)
439+
galaxy_1 = ag.Galaxy(
440+
redshift=0.5,
441+
light=ag.lp.Gaussian(centre=(-0.5, 0.2), intensity=2.0, sigma=0.7),
442+
)
443+
galaxies = ag.Galaxies(galaxies=[galaxy_0, galaxy_1])
444+
445+
blurred_list = galaxies.blurred_image_2d_list_from(
446+
grid=grid, blurring_grid=blurring_grid, psf=psf
447+
)
448+
blurred_dict = galaxies.galaxy_blurred_image_2d_dict_from(
449+
grid=grid, blurring_grid=blurring_grid, psf=psf
450+
)
451+
452+
total_from_list = np.sum([np.array(b) for b in blurred_list], axis=0)
453+
blurred_total = galaxies.blurred_image_2d_from(
454+
grid=grid, blurring_grid=blurring_grid, psf=psf
455+
)
456+
457+
assert total_from_list == pytest.approx(np.array(blurred_total), abs=1.0e-12)
458+
459+
for galaxy, blurred_scalar in zip([galaxy_0, galaxy_1], blurred_list):
460+
assert np.array(blurred_dict[galaxy]) == pytest.approx(
461+
np.array(blurred_scalar), abs=1.0e-14
462+
)

0 commit comments

Comments
 (0)