@@ -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
0 commit comments