Skip to content

Commit e2bc05f

Browse files
authored
Merge pull request #413 from ahuang314/main
Changes to image simulation and SNR calculation
2 parents 9b4084f + 99d53c7 commit e2bc05f

5 files changed

Lines changed: 210 additions & 148 deletions

File tree

slsim/ImageSimulation/image_simulation.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ def simulate_image(
1919
observatory="LSST",
2020
t_obs=None,
2121
exposure_time=None,
22+
num_exposures=None,
2223
kwargs_psf=None,
2324
kwargs_numerics=None,
2425
kwargs_single_band=None,
@@ -45,8 +46,11 @@ def simulate_image(
4546
variable source. In case of point source, if we do not provide
4647
t_obs, considers no variability in the lens.
4748
:param exposure_time: exposure time in seconds for computing SNR. If None, will use defaults
48-
from lenstronomy.SimulationAPI.ObservationConfig
49+
from lenstronomy.SimulationAPI.ObservationConfig based on the observatory and survey mode.
4950
:type exposure_time: float or None
51+
:param num_exposures: number of exposures. If None, a default number will be retrieved from
52+
lenstronomy's SimulationAPI.ObservationConfig based on the observatory and survey mode.
53+
:type num_exposures: int or None
5054
:param kwargs_psf: (optional) specific PSF quantities to overwrite
5155
default options ("psf_type", "kernel_point_source",
5256
"point_source_supersampling_factor")
@@ -84,7 +88,8 @@ def simulate_image(
8488
)
8589
if exposure_time is not None:
8690
kwargs_single_band["exposure_time"] = exposure_time
87-
kwargs_single_band["num_exposures"] = 1
91+
if num_exposures is not None:
92+
kwargs_single_band["num_exposures"] = num_exposures
8893
if kwargs_psf is not None:
8994
kwargs_single_band.update(kwargs_psf)
9095
sim_api = SimAPI(

slsim/ImageSimulation/roman_image_simulation.py

Lines changed: 80 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ def simulate_roman_image(
4040
with_source=True,
4141
with_deflector=True,
4242
exposure_time=None,
43+
num_exposures=None,
4344
t_obs=None,
4445
survey_mode="wide_area",
4546
detector=None,
@@ -49,9 +50,9 @@ def simulate_roman_image(
4950
dec=None,
5051
date=datetime.datetime(year=2027, month=7, day=7, hour=0, minute=0, second=0),
5152
psf_directory=None,
52-
galsim_convolve=True,
5353
):
54-
"""Creates an image of a selected lens with noise.
54+
"""Creates a Roman-simulated image of a selected lens with noise, using
55+
galsim's noise settings and PSFs from STPSF.
5556
5657
:param lens_class: class object containing all information of the lensing system
5758
(e.g., Lens())
@@ -69,8 +70,12 @@ def simulate_roman_image(
6970
:type with_source: bool
7071
:param with_deflector: determines whether deflector is included in image
7172
:type with_deflector: bool
72-
:param exposure_time: exposure time of image. If None, a default exposure time will be retrieved from lenstronomy's SimulationAPI.
73+
:param exposure_time: exposure time of image. If None, a default exposure time will be retrieved from
74+
lenstronomy's SimulationAPI.ObservationConfig based on the Roman survey mode.
7375
:type exposure_time: int or None
76+
:param num_exposures: number of exposures. If None, a default number will be retrieved from
77+
lenstronomy's SimulationAPI.ObservationConfig based on the Roman survey mode.
78+
:type num_exposures: int or None
7479
:param t_obs: an observation time in units of days. This is applicable only for
7580
variable source. In case of point source, if we do not provide
7681
t_obs, considers no variability in the lens.
@@ -94,9 +99,7 @@ def simulate_roman_image(
9499
psf_file_name = f"{band}_{detector}_{detector_pos[0]}_{detector_pos[1]}_{oversample}.pkl"
95100
For example, psf_file_name = "F106_SCA03_1934_1293_5.pkl"
96101
:type psf_directory: string
97-
:param galsim_convolve: if True, uses GalSim for the numerics, otherwise uses lenstronomy
98-
:type galsim_convolve: bool
99-
:return: simulated image
102+
:return: simulated image in units of flux per second
100103
:rtype: 2d numpy array
101104
"""
102105

@@ -122,15 +125,15 @@ def simulate_roman_image(
122125
)
123126
galsim_psf = get_psf(band, detector, detector_pos, oversample, psf_directory)
124127

125-
if exposure_time is None:
126-
_exposure_time = (
127-
kwargs_single_band["exposure_time"] * kwargs_single_band["num_exposures"]
128-
)
129-
else:
130-
_exposure_time = exposure_time
128+
# Will use Galsim to handle individual exposures then add them up
129+
_exposure_time = (
130+
kwargs_single_band["exposure_time"] if exposure_time is None else exposure_time
131+
)
132+
_num_exposures = (
133+
kwargs_single_band["num_exposures"] if num_exposures is None else num_exposures
134+
)
131135

132136
# Unconvolved image will be drawn at oversampled pixel scale
133-
kwargs_single_band_point_source = copy.deepcopy(kwargs_single_band)
134137
kwargs_single_band["pixel_scale"] /= oversample
135138

136139
sim_api = SimAPI(
@@ -146,97 +149,84 @@ def simulate_roman_image(
146149

147150
kwargs_lens = kwargs_params.get("kwargs_lens", None)
148151

149-
if galsim_convolve:
150-
kwargs_numerics = {
151-
"point_source_supersampling_factor": 1,
152-
"supersampling_factor": 1,
153-
}
154-
image_model = sim_api.image_model_class(kwargs_numerics)
155-
# Draws the unconvolved image with point source painted on single pixel
156-
array = _exposure_time * image_model.image(
157-
kwargs_lens=kwargs_lens,
158-
kwargs_source=kwargs_source,
159-
kwargs_lens_light=kwargs_lens_light,
160-
kwargs_ps=kwargs_ps,
161-
unconvolved=True,
162-
source_add=with_source,
163-
lens_light_add=with_deflector,
164-
point_source_add=True,
165-
)
166-
# Converts image to the galsim InterpolatedImage class
167-
interp = InterpolatedImage(
168-
Image(array, xmin=0, ymin=0),
169-
scale=0.11 / oversample,
170-
flux=np.sum(array),
171-
)
172-
# Gets psf and convolve
173-
convolved = galsim.Convolve(interp, galsim_psf)
152+
kwargs_numerics = {
153+
"point_source_supersampling_factor": 1,
154+
"supersampling_factor": 1,
155+
}
156+
image_model = sim_api.image_model_class(kwargs_numerics)
157+
# Draws the unconvolved image with point source painted on single pixel
158+
array = _exposure_time * image_model.image(
159+
kwargs_lens=kwargs_lens,
160+
kwargs_source=kwargs_source,
161+
kwargs_lens_light=kwargs_lens_light,
162+
kwargs_ps=kwargs_ps,
163+
unconvolved=True,
164+
source_add=with_source,
165+
lens_light_add=with_deflector,
166+
point_source_add=True,
167+
)
168+
# Converts image to the galsim InterpolatedImage class
169+
interp = InterpolatedImage(
170+
Image(array, xmin=0, ymin=0),
171+
scale=0.11 / oversample,
172+
flux=np.sum(array),
173+
)
174+
# Gets psf and convolve
175+
convolved = galsim.Convolve(interp, galsim_psf)
174176

175-
# Draw interpolated image at the original (not oversampled) pixel scale
176-
im = galsim.ImageF(num_pix, num_pix, scale=0.11)
177-
im.setOrigin(0, 0)
178-
image = convolved.drawImage(im)
179-
else:
180-
kwargs_single_band_point_source["psf_type"] = "PIXEL"
181-
# get array of supersampled PSF kernel
182-
kwargs_single_band_point_source["kernel_point_source"] = galsim_psf.image.array
183-
kwargs_single_band_point_source["point_source_supersampling_factor"] = (
184-
oversample
185-
)
186-
# the PSFs are intentionally not normalized to one
187-
kwargs_single_band_point_source["kernel_point_source_normalisation"] = False
188-
189-
# generates point source image with lenstronomy
190-
sim_api_ps = SimAPI(
191-
numpix=num_pix,
192-
kwargs_single_band=kwargs_single_band_point_source,
193-
kwargs_model=kwargs_model,
194-
)
195-
kwargs_numerics_ps = {
196-
"point_source_supersampling_factor": oversample,
197-
"supersampling_factor": oversample,
198-
}
199-
image_model_ps = sim_api_ps.image_model_class(kwargs_numerics_ps)
200-
201-
# Draws the point source
202-
image = _exposure_time * image_model_ps.image(
203-
kwargs_lens=kwargs_lens,
204-
kwargs_source=kwargs_source,
205-
kwargs_lens_light=kwargs_lens_light,
206-
kwargs_ps=kwargs_ps,
207-
unconvolved=False,
208-
source_add=True,
209-
lens_light_add=True,
210-
point_source_add=True,
211-
)
212-
image = galsim.ImageF(image, scale=0.11)
177+
# Draw interpolated image at the original (not oversampled) pixel scale
178+
im = galsim.ImageF(num_pix, num_pix, scale=0.11)
179+
im.setOrigin(0, 0)
180+
image_no_noise = convolved.drawImage(im)
213181

214182
if add_noise:
215-
# Obtain sky background corresponding to certain band and add it to the image
216-
# Requires stpsf data files to use
217-
image = add_roman_background(
218-
image,
183+
# Obtain sky and thermal background corresponding to certain band and add it to the image
184+
# Poisson noise realization is not handled until later
185+
image_with_background = add_sky_plus_thermal_background(
186+
image_no_noise,
219187
band,
220188
detector,
221189
num_pix,
222190
_exposure_time,
223191
ra,
224192
dec,
225193
date,
226-
subtract_mean_background=subtract_mean_background,
227194
)
228195

229-
# Add detector effects and get the resulting array
196+
# Add noise realizations and detector effects
197+
# Need to handle each exposure separately to properly take into account read noise and persistence
230198
rng = galsim.UniformDeviate(seed)
231-
roman.allDetectorEffects(
232-
image, prev_exposures=(), rng=rng, exptime=_exposure_time
233-
)
234199

235-
array = image.array
200+
# includes all noise
201+
final_image_list = []
202+
203+
# does not include readout noise; necessary to include the effects of persistence
204+
prev_exposures = []
236205

237-
final_array = array[3:-3, 3:-3]
238-
final_array = final_array / _exposure_time
239-
return final_array
206+
for i in range(_num_exposures):
207+
208+
# Create new realizations of image + noise
209+
final_image_list.append(copy.deepcopy(image_with_background))
210+
prev_exposures = roman.allDetectorEffects(
211+
final_image_list[i], # this gets modified in-place
212+
prev_exposures=prev_exposures, # this gets updated with each call
213+
rng=rng, # rng updates are automatically done
214+
exptime=_exposure_time,
215+
)
216+
217+
if subtract_mean_background:
218+
mean_noise = np.mean(final_image_list[i].array - image_no_noise.array)
219+
final_image_list[i].array -= mean_noise
220+
221+
# Combine exposures and compute flux per second
222+
array_list = np.array([image_i.array for image_i in final_image_list])
223+
array = np.sum(array_list, axis=0) / (_exposure_time * _num_exposures)
224+
else:
225+
array = image_no_noise.array / _exposure_time
226+
227+
final_image = array[3:-3, 3:-3]
228+
229+
return final_image
240230

241231

242232
# The following functions have been copy-pasted from the mejiro repo
@@ -294,7 +284,7 @@ def get_psf(band, detector, detector_pos, oversample, psf_directory):
294284
return galsim.InterpolatedImage(psf_image)
295285

296286

297-
def add_roman_background(
287+
def add_sky_plus_thermal_background(
298288
image,
299289
band,
300290
detector,
@@ -303,7 +293,6 @@ def add_roman_background(
303293
ra,
304294
dec,
305295
date,
306-
subtract_mean_background=True,
307296
):
308297
"""Adds a sky and thermal background to image, corresponding to a specific
309298
band, detector, date, and coordinate in the sky.
@@ -323,9 +312,6 @@ def add_roman_background(
323312
:type dec: float between -45 and -15
324313
:param date: Date used to generate sky background
325314
:type date: datetime.datetime class
326-
:param subtract_mean_background: whether to subtract the mean
327-
background from the image
328-
:type subtract_mean_background: bool
329315
:return: image with added background
330316
:rtype: galsim Image class
331317
"""
@@ -348,10 +334,6 @@ def add_roman_background(
348334
thermal_bkg = roman.thermal_backgrounds[get_bandpass_key(band)] * exposure_time
349335

350336
image = image + sky_image + thermal_bkg
351-
# image.quantize()
352-
if subtract_mean_background:
353-
mean_bkg = np.mean(sky_image.array + thermal_bkg)
354-
image -= mean_bkg
355337

356338
return image
357339

slsim/Lenses/lens.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,7 @@ def snr(
403403
observatory="LSST",
404404
snr_per_pixel_threshold=1,
405405
exposure_time=None,
406+
num_exposures=None,
406407
):
407408
"""Calculate the signal-to-noise ratio (SNR) using
408409
the method of `Holloway et al. (2023) <https://doi.org/10.1093/mnras/stad2371>`_.
@@ -452,8 +453,11 @@ def snr(
452453
a pixel in a region (default is 1)
453454
:type snr_per_pixel_threshold: float
454455
:param exposure_time: exposure time in seconds for computing SNR. If None, will use defaults
455-
from lenstronomy.SimulationAPI.ObservationConfig
456+
from lenstronomy.SimulationAPI.ObservationConfig based on the observatory.
456457
:type exposure_time: float or None
458+
:param num_exposures: number of exposures. If None, a default number will be retrieved from
459+
lenstronomy's SimulationAPI.ObservationConfig based on the observatory.
460+
:type num_exposures: int or None
457461
:return: maximum SNR found among the regions above the threshold.
458462
Returns ``None`` if no pixels are above the threshold or if no
459463
multi-pixel regions are found.
@@ -480,6 +484,7 @@ def snr(
480484
with_point_source=True,
481485
image_units_counts=True, # units of counts, not counts/sec
482486
exposure_time=exposure_time,
487+
num_exposures=num_exposures,
483488
)
484489

485490
# get simulated image (source + deflector + background noise)
@@ -498,6 +503,7 @@ def snr(
498503
with_point_source=True,
499504
image_units_counts=True, # units of counts, not counts/sec
500505
exposure_time=exposure_time,
506+
num_exposures=num_exposures,
501507
)
502508

503509
# calculate the denominator of the SNR

0 commit comments

Comments
 (0)