Skip to content

Commit 35d686f

Browse files
Jammy2211Jammy2211claude
authored
feat: oversampled PSF support in SimulatorImaging (#482) (#483)
OperateImage.convolved_padded_image_2d_from (shared by Galaxies and Tracer): builds the padded all-false frame from the kernel's image-resolution footprint with a uniform over-sample size (the regular padded grid pads its border with size-1 entries, which the uniformity guard correctly rejects), evaluates unbinned on the padded over-sampled coordinates, convolves at the fine resolution and returns the image-resolution convolved padded image; operated profiles are added unblurred as elsewhere. via_galaxies_from switches on psf.convolve_over_sample_size > 1 and passes image_is_convolved=True to the base simulator, trimming with kernel_shape_image_resolution. s=1 path byte-identical. Delta-kernel identity unit test for the padded-frame geometry. Co-authored-by: Jammy2211 <JNightingale2211@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 6c33c76 commit 35d686f

4 files changed

Lines changed: 183 additions & 0 deletions

File tree

autogalaxy/imaging/simulator.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,27 @@ def via_galaxies_from(
6161
psf=self.psf,
6262
)
6363

64+
if self.psf.convolve_over_sample_size > 1:
65+
66+
image = galaxies.convolved_padded_image_2d_from(
67+
grid=grid, psf=self.psf, xp=xp
68+
)
69+
70+
over_sample_size = grid.over_sample_size.resized_from(
71+
new_shape=image.shape_native, mask_pad_value=1
72+
)
73+
74+
dataset = self.via_image_from(
75+
image=image,
76+
over_sample_size=over_sample_size,
77+
image_is_convolved=True,
78+
xp=xp,
79+
)
80+
81+
return dataset.trimmed_after_convolution_from(
82+
kernel_shape=self.psf.kernel_shape_image_resolution
83+
)
84+
6485
image = galaxies.padded_image_2d_from(
6586
grid=grid, psf_shape_2d=self.psf.kernel.shape_native, xp=xp
6687
)

autogalaxy/operate/image.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,70 @@ def padded_image_2d_from(self, grid, psf_shape_2d, xp=np):
194194

195195
return self.image_2d_from(grid=padded_grid, xp=xp)
196196

197+
def convolved_padded_image_2d_from(self, grid, psf: aa.Convolver, xp=np):
198+
"""
199+
Evaluate the light object's 2D image on a padded grid and convolve it with an
200+
oversampled PSF at the fine resolution, returning the convolved padded image
201+
at image resolution (still requiring trimming, as with
202+
`padded_image_2d_from` + external convolution).
203+
204+
This is the simulation path for `convolve_over_sample_size > 1`: the padded
205+
frame is sized by the kernel's image-resolution footprint and carries a
206+
*uniform* over-sample size equal to the PSF's (the regular padded grid pads
207+
its border with size-1 entries, which oversampled convolution correctly
208+
rejects). The image is evaluated unbinned on the padded grid's over-sampled
209+
coordinates and convolved by the oversampled Convolver, which bins back to
210+
image resolution. No blurring image is needed — the padding guarantees all
211+
flux that blurs into the frame is evaluated, exactly as in the existing
212+
padded flow.
213+
214+
Parameters
215+
----------
216+
grid
217+
The 2D (y,x) coordinates of the grid the simulation is performed on, in
218+
its original geometric reference frame.
219+
psf
220+
The oversampled PSF (`convolve_over_sample_size > 1`) the padded image
221+
is convolved with.
222+
"""
223+
s = psf.convolve_over_sample_size
224+
225+
kernel_shape_2d = psf.kernel_shape_image_resolution
226+
227+
padded_shape = (
228+
grid.mask.shape_native[0] + kernel_shape_2d[0] - 1,
229+
grid.mask.shape_native[1] + kernel_shape_2d[1] - 1,
230+
)
231+
232+
padded_mask = aa.Mask2D.all_false(
233+
shape_native=padded_shape,
234+
pixel_scales=grid.mask.pixel_scales,
235+
origin=grid.origin,
236+
)
237+
238+
padded_grid = aa.Grid2D.from_mask(mask=padded_mask, over_sample_size=s)
239+
240+
image_over_sampled = self.image_2d_from(
241+
grid=padded_grid.over_sampled, xp=xp, operated_only=False
242+
)
243+
244+
convolved = psf.convolved_image_from(
245+
image=image_over_sampled,
246+
blurring_image=None,
247+
mask=padded_mask,
248+
xp=xp,
249+
)
250+
251+
from autogalaxy.profiles.light.operated import LightProfileOperated
252+
253+
if self.has(cls=LightProfileOperated):
254+
image_2d_operated = self.image_2d_from(
255+
grid=padded_grid, xp=xp, operated_only=True
256+
)
257+
return convolved + image_2d_operated
258+
259+
return convolved
260+
197261
def unmasked_blurred_image_2d_from(self, grid, psf):
198262
"""
199263
Evaluate the light object's 2D image from a input 2D grid of coordinates and convolve it with a PSF, using a

test_autogalaxy/imaging/test_simulate_and_fit_imaging.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,3 +490,60 @@ def test__linear_light_profiles_agree_with_standard__galaxy_model_image_matches_
490490
assert fit_linear.galaxy_model_image_dict[galaxy_linear] == pytest.approx(
491491
galaxy_image.array, 1.0e-4
492492
)
493+
494+
495+
def test__perfect_fit__chi_squared_0__oversampled_psf():
496+
# Simulate with an oversampled PSF (convolution at 2x the image resolution)
497+
# via via_galaxies_from and fit the same galaxies at s=2: exact round trip.
498+
s = 2
499+
pixel_scales = 0.2
500+
501+
grid = ag.Grid2D.uniform(
502+
shape_native=(21, 21), pixel_scales=pixel_scales, over_sample_size=s
503+
)
504+
505+
psf = ag.Convolver.from_gaussian(
506+
shape_native=(11, 11),
507+
pixel_scales=pixel_scales / s,
508+
sigma=0.15,
509+
normalize=True,
510+
convolve_over_sample_size=s,
511+
)
512+
513+
galaxy_0 = ag.Galaxy(
514+
redshift=0.5,
515+
light=ag.lp.Sersic(centre=(0.0, 0.0), intensity=0.5, effective_radius=0.4),
516+
)
517+
galaxy_1 = ag.Galaxy(
518+
redshift=0.5,
519+
light=ag.lp.Exponential(centre=(0.05, 0.05), intensity=0.3, effective_radius=0.2),
520+
)
521+
522+
simulator = ag.SimulatorImaging(
523+
exposure_time=300.0, psf=psf, add_poisson_noise_to_data=False
524+
)
525+
dataset = simulator.via_galaxies_from(galaxies=[galaxy_0, galaxy_1], grid=grid)
526+
527+
dataset.noise_map = ag.Array2D.ones(
528+
shape_native=dataset.data.shape_native, pixel_scales=pixel_scales
529+
)
530+
531+
mask = ag.Mask2D.circular(
532+
shape_native=dataset.data.shape_native, pixel_scales=pixel_scales, radius=1.8
533+
)
534+
535+
masked = ag.Imaging(
536+
data=dataset.data,
537+
noise_map=dataset.noise_map,
538+
psf=psf,
539+
over_sample_size_lp=s,
540+
over_sample_size_pixelization=s,
541+
convolve_over_sample_size_lp=s,
542+
convolve_over_sample_size_pixelization=s,
543+
).apply_mask(mask=mask)
544+
545+
fit = ag.FitImaging(
546+
dataset=masked, galaxies=ag.Galaxies(galaxies=[galaxy_0, galaxy_1])
547+
)
548+
549+
assert fit.chi_squared == pytest.approx(0.0, abs=1.0e-10)

test_autogalaxy/operate/test_image.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,3 +460,44 @@ def test__blurred_image_2d_list_and_dict__oversampled_psf__match_scalar_path():
460460
assert np.array(blurred_dict[galaxy]) == pytest.approx(
461461
np.array(blurred_scalar), abs=1.0e-14
462462
)
463+
464+
465+
def test__convolved_padded_image_2d_from__delta_kernel__equals_binned_padded_image():
466+
# With a delta fine kernel the fine convolution is the identity, so the
467+
# convolved padded image must equal the binned padded evaluation — testing
468+
# the padded-frame geometry and bin-down independently of PSF numerics.
469+
import autoarray as aa
470+
471+
s = 2
472+
pixel_scales = 1.0
473+
474+
grid = aa.Grid2D.uniform(
475+
shape_native=(7, 7), pixel_scales=pixel_scales, over_sample_size=s
476+
)
477+
478+
delta = np.zeros((5, 5))
479+
delta[2, 2] = 1.0
480+
psf = aa.Convolver(
481+
kernel=aa.Array2D.no_mask(values=delta, pixel_scales=pixel_scales / s),
482+
convolve_over_sample_size=s,
483+
)
484+
485+
galaxy = ag.Galaxy(
486+
redshift=0.5,
487+
light=ag.lp.Sersic(centre=(0.2, -0.1), intensity=1.0, effective_radius=0.8),
488+
)
489+
galaxies = ag.Galaxies(galaxies=[galaxy])
490+
491+
convolved_padded = galaxies.convolved_padded_image_2d_from(grid=grid, psf=psf)
492+
493+
kernel_shape = psf.kernel_shape_image_resolution
494+
padded_shape = (7 + kernel_shape[0] - 1, 7 + kernel_shape[1] - 1)
495+
padded_mask = aa.Mask2D.all_false(
496+
shape_native=padded_shape, pixel_scales=pixel_scales, origin=grid.origin
497+
)
498+
padded_grid = aa.Grid2D.from_mask(mask=padded_mask, over_sample_size=s)
499+
500+
image_sub = galaxy.image_2d_from(grid=padded_grid.over_sampled)
501+
binned = np.array(image_sub).reshape(-1, s**2).mean(axis=1)
502+
503+
assert np.array(convolved_padded) == pytest.approx(binned, abs=1.0e-14)

0 commit comments

Comments
 (0)