• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

LSSTDESC / Spectractor / 7207821764

14 Dec 2023 10:49AM UTC coverage: 90.688% (-0.7%) from 91.426%
7207821764

push

github

web-flow
Merge pull request #145 from LSSTDESC/144-run_module_suite-deprecated

test remove run_module_suite

2 of 7 new or added lines in 2 files covered. (28.57%)

93 existing lines in 21 files now uncovered.

7226 of 7968 relevant lines covered (90.69%)

0.91 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

81.14
/spectractor/simulation/image_simulation.py
1
from spectractor import parameters
1✔
2
from spectractor.config import set_logger
1✔
3
from spectractor.tools import (pixel_rotation, set_wcs_file_name, set_sources_file_name,
1✔
4
                               set_gaia_catalog_file_name, load_wcs_from_file, ensure_dir,
5
                               plot_image_simple, iraf_source_detection)
6
from spectractor.extractor.images import Image, find_target
1✔
7
from spectractor.astrometry import get_gaia_coords_after_proper_motion
1✔
8
from spectractor.extractor.background import remove_image_background_sextractor
1✔
9
from spectractor.simulation.simulator import SpectrogramModel
1✔
10
from spectractor.simulation.atmosphere import Atmosphere
1✔
11
from spectractor.extractor.spectrum import Spectrum
1✔
12
from spectractor.extractor.psf import PSF
1✔
13

14
from astropy.io import fits, ascii
1✔
15
import astropy.units as units
1✔
16
from astropy.coordinates import SkyCoord
1✔
17
from astropy.table import Table
1✔
18
from scipy.signal import fftconvolve, gaussian
1✔
19
import matplotlib.pyplot as plt
1✔
20
from matplotlib.ticker import MaxNLocator
1✔
21
import numpy as np
1✔
22
import copy
1✔
23
import os
1✔
24

25

26
class StarModel:
1✔
27
    """Class to model a star in the image simulation process.
1✔
28

29
    Attributes
30
    ----------
31
    x0: float
32
        X position of the star centroid in pixels.
33
    y0: float
34
        Y position of the star centroid in pixels.
35
    amplitude: amplitude
36
        The amplitude of the star in image units.
37
    """
38

39
    def __init__(self, centroid_coords, psf, amplitude):
1✔
40
        """Create a StarModel instance.
41

42
        The model is based on an Astropy Fittable2DModel. The centroid and amplitude
43
        parameters of the given model are updated by the dedicated arguments.
44

45
        Parameters
46
        ----------
47
        centroid_coords: array_like
48
            Tuple of (x,y) coordinates of the desired star centroid in pixels.
49
        psf: PSF
50
            PSF model
51
        amplitude: float
52
            The desired amplitude of the star in image units.
53

54
        Examples
55
        --------
56
        >>> from spectractor.extractor.psf import Moffat
57
        >>> p = (100, 50, 50, 5, 2, 200)
58
        >>> psf = Moffat(p)
59
        >>> s = StarModel((20, 10), psf, 200)
60
        >>> s.plot_model()
61
        >>> s.x0
62
        20
63
        >>> s.y0
64
        10
65
        >>> s.amplitude
66
        200
67
        """
68
        self.my_logger = set_logger(self.__class__.__name__)
1✔
69
        self.x0 = centroid_coords[0]
1✔
70
        self.y0 = centroid_coords[1]
1✔
71
        self.amplitude = amplitude
1✔
72
        # self.target = target
73
        self.psf = copy.deepcopy(psf)
1✔
74
        self.psf.params.values[1] = self.x0
1✔
75
        self.psf.params.values[2] = self.y0
1✔
76
        self.psf.params.values[0] = amplitude
1✔
77
        # to be realistic, usually fitted fwhm is too big, divide gamma by 2
78
        self.fwhm = self.psf.params.values[3]
1✔
79
        # self.sigma = self.model.stddev / 2
80

81
    def plot_model(self):
1✔
82
        """
83
        Plot the star model.
84
        """
85
        x = np.arange(self.x0 - 5 * self.fwhm, self.x0 + 5 * self.fwhm)
1✔
86
        y = np.arange(self.y0 - 5 * self.fwhm, self.y0 + 5 * self.fwhm)
1✔
87
        xx, yy = np.meshgrid(x, y)
1✔
88
        star = self.psf.evaluate(np.array([xx, yy]))
1✔
89
        fig, ax = plt.subplots(1, 1)
1✔
90
        plot_image_simple(ax, star, title=f'Star model: A={self.amplitude:.2f}, fwhm={self.fwhm:.2f}',
1✔
91
                          units='Arbitrary units')
92
        if parameters.DISPLAY:
1✔
93
            plt.show()
×
94
        if parameters.PdfPages:
1✔
95
            parameters.PdfPages.savefig()
×
96

97

98
class StarFieldModel:
1✔
99

100
    def __init__(self, base_image, flux_factor=1):
1✔
101
        """
102
        Examples
103
        --------
104

105
        >>> from spectractor.extractor.images import Image, find_target
106
        >>> im = Image('tests/data/reduc_20170530_134.fits', target_label="HD111980")
107
        >>> x0, y0 = find_target(im, guess=(740, 680))
108
        >>> s = StarFieldModel(im)
109
        >>> s.plot_model()
110

111
        """
112
        self.my_logger = set_logger(self.__class__.__name__)
1✔
113
        self.image = base_image
1✔
114
        self.target = base_image.target
1✔
115
        self.field = None
1✔
116
        self.stars = []
1✔
117
        self.pixcoords = []
1✔
118
        self.fwhm = base_image.target_star2D.params.values[3]
1✔
119
        self.flux_factor = flux_factor
1✔
120
        self.set_star_list()
1✔
121

122
    # noinspection PyUnresolvedReferences
123
    def set_star_list(self):
1✔
124
        x0, y0 = self.image.target_pixcoords
1✔
125
        sources_file_name = set_sources_file_name(self.image.file_name)
1✔
126
        if os.path.isfile(sources_file_name):
1✔
127
            # load sources positions and flux
128
            sources = Table.read(sources_file_name)
1✔
129
            sources['X'].name = "xcentroid"
1✔
130
            sources['Y'].name = "ycentroid"
1✔
131
            sources['FLUX'].name = "flux"
1✔
132
            # test presence of WCS and gaia catalog files
133
            wcs_file_name = set_wcs_file_name(self.image.file_name)
1✔
134
            gaia_catalog_file_name = set_gaia_catalog_file_name(self.image.file_name)
1✔
135
            if os.path.isfile(wcs_file_name) and os.path.isfile(gaia_catalog_file_name):
1✔
136
                # load gaia catalog
137
                gaia_catalog = ascii.read(gaia_catalog_file_name, format="ecsv")
1✔
138
                gaia_coord_after_motion = get_gaia_coords_after_proper_motion(gaia_catalog, self.image.date_obs)
1✔
139
                # load WCS
140
                wcs = load_wcs_from_file(wcs_file_name)
1✔
141
                # catalog matching to set star positions using Gaia
142
                sources_coord = wcs.all_pix2world(sources['xcentroid'], sources['ycentroid'], 0)
1✔
143
                sources_coord = SkyCoord(ra=sources_coord[0] * units.deg, dec=sources_coord[1] * units.deg,
1✔
144
                                         frame="icrs", obstime=self.image.date_obs, equinox="J2000")
145
                gaia_index, dist_2d, dist_3d = sources_coord.match_to_catalog_sky(gaia_coord_after_motion)
1✔
146
                for k, gaia_i in enumerate(gaia_index):
1✔
147
                    x, y = wcs.all_world2pix(gaia_coord_after_motion[gaia_i].ra, gaia_coord_after_motion[gaia_i].dec, 0)
1✔
148
                    A = sources['flux'][k] * self.flux_factor
1✔
149
                    self.stars.append(StarModel([x, y], self.image.target_star2D, A))
1✔
150
                    self.pixcoords.append([x, y])
1✔
151
            else:
152
                for k, source in enumerate(sources):
×
153
                    x, y = sources['xcentroid'][k], sources['ycentroid'][k]
×
154
                    A = sources['flux'][k] * self.flux_factor
×
155
                    self.stars.append(StarModel([x, y], self.image.target_star2D, A))
×
156
                    self.pixcoords.append([x, y])
×
157
        else:
158
            # mask background, faint stars, and saturated pixels
159
            data = np.copy(self.image.data)
×
160
            # self.saturation = 0.99 * parameters.CCD_MAXADU / base_image.expo
161
            # self.saturated_pixels = np.where(image_thresholded > self.saturation)
162
            # image_thresholded[self.saturated_pixels] = 0.
163
            # image_thresholded -= threshold
164
            # image_thresholded[np.where(image_thresholded < 0)] = 0.
165
            # mask order0 and spectrum
166
            margin = 30
×
167
            mask = np.zeros(data.shape, dtype=bool)
×
168
            for y in range(int(y0) - 100, int(y0) + 100):
×
169
                for x in range(parameters.CCD_IMSIZE):
×
170
                    u, v = pixel_rotation(x, y, self.image.disperser.theta(x0, y0) * np.pi / 180., x0, y0)
×
171
                    if margin > v > -margin:
×
172
                        mask[y, x] = True
×
173
            # remove background and detect sources
174
            data_wo_bkg = remove_image_background_sextractor(data)
×
175
            sources = iraf_source_detection(data_wo_bkg, mask=mask)
×
176
            for k, source in enumerate(sources):
×
177
                x, y = sources['xcentroid'][k], sources['ycentroid'][k]
×
178
                A = sources['flux'][k] * self.flux_factor
×
179
                self.stars.append(StarModel([x, y], self.image.target_star2D, A))
×
180
                self.pixcoords.append([x, y])
×
181
        self.pixcoords = np.array(self.pixcoords).T
1✔
182

183
    def model(self, x, y):
1✔
184
        if self.field is None:
1✔
185
            window = int(20 * self.fwhm)
1✔
186
            self.field = self.stars[0].psf.evaluate(np.array([x, y]))
1✔
187
            for k in range(1, len(self.stars)):
1✔
188
                left = max(0, int(self.pixcoords[0][k]) - window)
1✔
189
                right = min(parameters.CCD_IMSIZE, int(self.pixcoords[0][k]) + window)
1✔
190
                low = max(0, int(self.pixcoords[1][k]) - window)
1✔
191
                up = min(parameters.CCD_IMSIZE, int(self.pixcoords[1][k]) + window)
1✔
192
                if up < low or left > right:
1✔
193
                    continue
×
194
                yy, xx = np.mgrid[low:up, left:right]
1✔
195
                self.field[low:up, left:right] += self.stars[k].psf.evaluate(np.array([xx, yy]))
1✔
196
        return self.field
1✔
197

198
    def plot_model(self):
1✔
199
        xx, yy = np.mgrid[0:parameters.CCD_IMSIZE:1, 0:parameters.CCD_IMSIZE:1]
1✔
200
        starfield = self.model(xx, yy)
1✔
201
        fig, ax = plt.subplots(1, 1)
1✔
202
        plot_image_simple(ax, starfield, scale="log10", target_pixcoords=self.pixcoords)
1✔
203
        # im = plt.imshow(starfield, origin='lower', cmap='jet')
204
        # ax.grid(color='white', ls='solid')
205
        # ax.grid(True)
206
        # ax.set_xlabel('X [pixels]')
207
        # ax.set_ylabel('Y [pixels]')
208
        # ax.set_title(f'Star field model: fwhm={self.fwhm.value:.2f}')
209
        # cb = plt.colorbar(im, ax=ax)
210
        # cb.formatter.set_powerlimits((0, 0))
211
        # cb.locator = MaxNLocator(7, prune=None)
212
        # cb.update_ticks()
213
        # cb.set_label('Arbitrary units')  # ,fontsize=16)
214
        if parameters.DISPLAY:
1✔
215
            plt.show()
×
216
        if parameters.PdfPages:
1✔
217
            parameters.PdfPages.savefig()
×
218

219

220
class BackgroundModel:
1✔
221
    """Class to model the background of the simulated image.
1✔
222

223
    The background model size is set with the parameters.CCD_IMSIZE global keyword.
224

225
    Attributes
226
    ----------
227
    level: float
228
        The mean level of the background in image units.
229
    frame: array_like
230
        (x, y, smooth) right and upper limits in pixels of a vignetting frame,
231
        and the smoothing gaussian width (default: None).
232
    """
233

234
    def __init__(self, level, frame=None):
1✔
235
        """Create a BackgroundModel instance.
236

237
        The background model size is set with the parameters.CCD_IMSIZE global keyword.
238

239
        Parameters
240
        ----------
241
        level: float
242
            The mean level of the background in image units.
243
        frame: array_like, None
244
            (x, y, smooth) right and upper limits in pixels of a vignetting frame,
245
            and the smoothing gaussian width (default: None).
246

247
        Examples
248
        --------
249
        >>> from spectractor import parameters
250
        >>> parameters.CCD_IMSIZE = 200
251
        >>> bgd = BackgroundModel(10)
252
        >>> model = bgd.model()
253
        >>> np.all(model==10)
254
        True
255
        >>> model.shape
256
        (200, 200)
257
        >>> bgd = BackgroundModel(10, frame=(160, 180, 3))
258
        >>> bgd.plot_model()
259
        """
260
        self.my_logger = set_logger(self.__class__.__name__)
1✔
261
        self.level = level
1✔
262
        if self.level <= 0:
1✔
263
            self.my_logger.warning('\n\tBackground level must be strictly positive.')
×
264
        else:
265
            self.my_logger.info(f'\n\tBackground set to {level:.3f} ADU/s.')
1✔
266
        self.frame = frame
1✔
267

268
    def model(self):
1✔
269
        """Compute the background model for the image simulation in image units.
270

271
        A shadowing vignetting frame is roughly simulated if self.frame is set.
272
        The background model size is set with the parameters.CCD_IMSIZE global keyword.
273

274
        Returns
275
        -------
276
        bkgd: array_like
277
            The array of the background model.
278

279
        """
280
        yy, xx = np.mgrid[0:parameters.CCD_IMSIZE:1, 0:parameters.CCD_IMSIZE:1]
1✔
281
        bkgd = self.level * np.ones_like(xx)
1✔
282
        if self.frame is None:
1✔
283
            return bkgd
1✔
284
        else:
285
            xlim, ylim, width = self.frame
1✔
286
            bkgd[ylim:, :] = self.level / 100
1✔
287
            bkgd[:, xlim:] = self.level / 100
1✔
288
            kernel = np.outer(gaussian(parameters.CCD_IMSIZE, width), gaussian(parameters.CCD_IMSIZE, width))
1✔
289
            bkgd = fftconvolve(bkgd, kernel, mode='same')
1✔
290
            bkgd *= self.level / bkgd[parameters.CCD_IMSIZE // 2, parameters.CCD_IMSIZE // 2]
1✔
291
            return bkgd
1✔
292

293
    def plot_model(self):
1✔
294
        """Plot the background model.
295

296
        """
297
        bkgd = self.model()
1✔
298
        fig, ax = plt.subplots(1, 1)
1✔
299
        im = plt.imshow(bkgd, origin='lower', cmap='jet')
1✔
300
        ax.grid(color='white', ls='solid')
1✔
301
        ax.grid(True)
1✔
302
        ax.set_xlabel('X [pixels]')
1✔
303
        ax.set_ylabel('Y [pixels]')
1✔
304
        ax.set_title('Background model')
1✔
305
        cb = plt.colorbar(im, ax=ax)
1✔
306
        cb.formatter.set_powerlimits((0, 0))
1✔
307
        cb.locator = MaxNLocator(7, prune=None)
1✔
308
        cb.update_ticks()
1✔
309
        cb.set_label('Arbitrary units')  # ,fontsize=16)
1✔
310
        if parameters.DISPLAY:
1✔
311
            plt.show()
×
312
        if parameters.PdfPages:
1✔
313
            parameters.PdfPages.savefig()
×
314

315

316
class ImageModel(Image):
1✔
317

318
    def __init__(self, filename, target_label=None):
1✔
319
        self.my_logger = set_logger(self.__class__.__name__)
1✔
320
        Image.__init__(self, filename, target_label=target_label)
1✔
321
        self.true_lambdas = None
1✔
322
        self.true_spectrum = None
1✔
323

324
    def compute(self, star, background, spectrogram, starfield=None):
1✔
325
        yy, xx = np.mgrid[0:parameters.CCD_IMSIZE:1, 0:parameters.CCD_IMSIZE:1]
1✔
326
        self.data = star.psf.evaluate(np.array([xx, yy])) + background.model()
1✔
327
        if spectrogram.full_image:
1✔
328
            self.data[spectrogram.spectrogram_ymin:spectrogram.spectrogram_ymax, :] += spectrogram.spectrogram
1✔
329
        else:
330
            self.data[spectrogram.spectrogram_ymin:spectrogram.spectrogram_ymax,
×
331
                      spectrogram.spectrogram_xmin:spectrogram.spectrogram_xmax] += spectrogram.spectrogram
332
        # - spectrogram.spectrogram_bgd)
333
        if starfield is not None:
1✔
334
            self.data += starfield.model(xx, yy)
×
335

336
    def add_poisson_and_read_out_noise(self):  # pragma: no cover
337
        if self.units != 'ADU':
338
            raise AttributeError('Poisson noise procedure has to be applied on map in ADU units')
339
        d = np.copy(self.data).astype(float)
340
        # convert to electron counts
341
        d *= self.gain
342
        # Poisson noise
343
        noisy = np.random.poisson(d).astype(float)
344
        # Add read-out noise is available
345
        if self.read_out_noise is not None:
346
            noisy += np.random.normal(scale=self.read_out_noise)
347
        # reconvert to ADU
348
        self.data = noisy / self.gain
349
        # removes zeros
350
        min_noz = np.min(self.data[self.data > 0])
351
        self.data[self.data <= 0] = min_noz
352

353
    def save_image(self, output_filename, overwrite=False):
1✔
354
        hdu0 = fits.PrimaryHDU()
1✔
355
        hdu0.data = self.data
1✔
356
        hdu0.header = self.header
1✔
357
        hdu1 = fits.ImageHDU()
1✔
358
        # hdu1.data = [self.true_lambdas, self.true_spectrum]
359
        hdulist = fits.HDUList([hdu0, hdu1])
1✔
360
        hdulist.writeto(output_filename, overwrite=overwrite)
1✔
361
        self.my_logger.info('\n\tImage saved in %s' % output_filename)
1✔
362

363
    def load_image(self, filename):
1✔
364
        super(ImageModel, self).load_image(filename)
1✔
365
        # hdu_list = fits.open(filename)
366
        # self.true_lambdas, self.true_spectrum = hdu_list[1].data
367

368

369
def ImageSim(image_filename, spectrum_filename, outputdir, pwv=5, ozone=300, aerosols=0.03, A1=1, A2=1, A3=1, angstrom_exponent=None,
1✔
370
             psf_poly_params=None, psf_type=None,  diffraction_orders=None, with_rotation=True, with_stars=True, with_adr=True, with_noise=True):
371
    """ The basic use of the extractor consists first to define:
372
    - the path to the fits image from which to extract the image,
373
    - the path of the output directory to save the extracted spectrum (created automatically if does not exist yet),
374
    - the rough position of the object in the image,
375
    - the name of the target (to search for the extra-atmospheric spectrum if available).
376
    Then different parameters and systematics can be set:
377
    - pwv: the pressure water vapor (in mm)
378
    - ozone: the ozone quantity (in XX)
379
    - aerosols: the vertical aerosol optical depth
380
    - A1: a global grey absorption parameter for the spectrum
381
    - A2: the relative amplitude of second order compared with first order
382
    - with_rotation: rotate the spectrum according to the disperser characteristics (True by default)
383
    - with_stars: include stars in the image field (True by default)
384
    - with_adr: include ADR effect (True by default)
385
    """
386
    my_logger = set_logger(__name__)
1✔
387
    my_logger.info(f'\n\tStart IMAGE SIMULATOR')
1✔
388
    # Load reduced image
389
    spectrum = Spectrum(spectrum_filename)
1✔
390
    if diffraction_orders is None:
1✔
391
        diffraction_orders = np.arange(spectrum.order, spectrum.order + 3 * np.sign(spectrum.order), np.sign(spectrum.order))
1✔
392
    image = ImageModel(image_filename, target_label=spectrum.target.label)
1✔
393
    guess = np.array([spectrum.header['TARGETX'], spectrum.header['TARGETY']])
1✔
394
    if "CCDREBIN" in spectrum.header:
1✔
395
        guess *= spectrum.header["CCDREBIN"]
1✔
396
    if parameters.DEBUG:
1✔
397
        image.plot_image(scale='symlog', target_pixcoords=guess)
×
398
    # Fit the star 2D profile
399
    my_logger.info('\n\tSearch for the target in the image...')
1✔
400
    target_pixcoords = find_target(image, guess)
1✔
401
    # Background model
402
    my_logger.info('\n\tBackground model...')
1✔
403
    bgd_level = float(np.mean(spectrum.spectrogram_bgd))
1✔
404
    background = BackgroundModel(level=bgd_level, frame=None)  # (1600, 1650, 100))
1✔
405
    if parameters.DEBUG:
1✔
406
        background.plot_model()
×
407

408
    # Target model
409
    my_logger.info('\n\tStar model...')
1✔
410
    # Spectrogram is simulated with spectrum.x0 target position: must be this position to simualte the target.
411
    star = StarModel(image.target_pixcoords, image.target_star2D, image.target_star2D.params.values[0])
1✔
412
    # reso = star.fwhm
413
    if parameters.DEBUG:
1✔
414
        star.plot_model()
×
415
    # Star field model
416
    starfield = None
1✔
417
    if with_stars:
1✔
418
        my_logger.info('\n\tStar field model...')
×
419
        starfield = StarFieldModel(image)
×
420
        if parameters.DEBUG:
×
421
            image.plot_image(scale='symlog', target_pixcoords=starfield.pixcoords)
×
422
            starfield.plot_model()
×
423

424
    # Spectrum model
425
    my_logger.info('\n\tSpectrum model...')
1✔
426
    airmass = image.header['AIRMASS']
1✔
427
    pressure = image.header['OUTPRESS']
1✔
428
    temperature = image.header['OUTTEMP']
1✔
429

430
    # Rotation: useful only to fill the Dy_disp_axis column in PSF table
431
    if not with_rotation:
1✔
432
        rotation_angle = 0
×
433
    else:
434
        rotation_angle = spectrum.rotation_angle
1✔
435

436
    # Load PSF
437
    if psf_type is not None:
1✔
438
        from spectractor.extractor.psf import load_PSF
×
439
        parameters.PSF_TYPE = psf_type
×
440
        psf = load_PSF(psf_type=psf_type)
×
441
        spectrum.psf = psf
×
442
        spectrum.chromatic_psf.psf = psf
×
443
    if psf_poly_params is None:
1✔
444
        my_logger.info('\n\tUse PSF parameters from _table.csv file.')
×
445
        psf_poly_params = spectrum.chromatic_psf.from_table_to_poly_params()
×
446
    else:
447
        spectrum.chromatic_psf.deg = ((len(psf_poly_params) - 1) // (len(spectrum.chromatic_psf.psf.params.labels) - 2) - 1) // len(diffraction_orders)
1✔
448
        spectrum.chromatic_psf.set_polynomial_degrees(spectrum.chromatic_psf.deg)
1✔
449
        if spectrum.chromatic_psf.deg == 0:  # x_c must have deg >= 1
1✔
450
            psf_poly_params.insert(1, 0)
×
451
        my_logger.info(f'\n\tUse PSF parameters {psf_poly_params} as polynoms of '
1✔
452
                       f'degree {spectrum.chromatic_psf.degrees}')
453
    if psf_type is not None and psf_poly_params is not None:
1✔
454
        spectrum.chromatic_psf.init_from_table()
×
455

456
    # Simulate spectrogram
457
    atmosphere = Atmosphere(airmass, pressure, temperature)
1✔
458
    spectrogram = SpectrogramModel(spectrum, atmosphere=atmosphere, with_background=False, fast_sim=False,
1✔
459
                                   full_image=True, with_adr=with_adr, diffraction_orders=diffraction_orders)
460
    spectrogram.simulate(A1, A2, A3, aerosols, angstrom_exponent, ozone, pwv,
1✔
461
                         spectrum.disperser.D, 0, 0, rotation_angle, 1, psf_poly_params)
462

463
    # Image model
464
    my_logger.info('\n\tImage model...')
1✔
465
    image.compute(star, background, spectrogram, starfield=starfield)
1✔
466

467
    # Recover true spectrum
468
    spectrogram.set_true_spectrum(spectrogram.lambdas, aerosols, ozone, pwv, shift_t=0)
1✔
469
    true_lambdas = np.copy(spectrogram.true_lambdas)
1✔
470
    true_spectrum = np.copy(spectrogram.true_spectrum)
1✔
471

472
    # Saturation effects
473
    saturated_pixels = np.where(spectrogram.spectrogram > image.saturation)[0]
1✔
474
    if len(saturated_pixels) > 0:
1✔
475
        my_logger.warning(f"\n\t{len(saturated_pixels)} saturated pixels detected above saturation "
×
476
                          f"level at {image.saturation} ADU/s in the spectrogram."
477
                          f"\n\tSpectrogram maximum is at {np.max(spectrogram.spectrogram)} ADU/s.")
478
    image.data[image.data > image.saturation] = image.saturation
1✔
479

480
    # Convert data from ADU/s in ADU
481
    image.convert_to_ADU_units()
1✔
482

483
    # Add Poisson and read-out noise
484
    if with_noise:
1✔
485
        image.add_poisson_and_read_out_noise()
×
486

487
    # Round float ADU into closest integers
488
    # image.data = np.around(image.data)
489

490
    # Plot
491
    if parameters.VERBOSE and parameters.DISPLAY:  # pragma: no cover
492
        image.convert_to_ADU_rate_units()
493
        image.plot_image(scale="symlog", title="Image simulation", target_pixcoords=target_pixcoords, units=image.units)
494
        image.convert_to_ADU_units()
495

496
    # Set output path
497
    ensure_dir(outputdir)
1✔
498
    output_filename = image_filename.split('/')[-1]
1✔
499
    output_filename = (output_filename.replace('reduc', 'sim')).replace('trim', 'sim')
1✔
500
    output_filename = os.path.join(outputdir, output_filename)
1✔
501

502
    # Save images and parameters
503
    image.header['A1_T'] = A1
1✔
504
    image.header['A2_T'] = A2
1✔
505
    image.header['A3_T'] = A3
1✔
506
    image.header['X0_T'] = spectrum.x0[0]
1✔
507
    image.header['Y0_T'] = spectrum.x0[1]
1✔
508
    image.header['D2CCD_T'] = float(spectrum.disperser.D)
1✔
509
    image.header['OZONE_T'] = ozone
1✔
510
    image.header['PWV_T'] = pwv
1✔
511
    image.header['VAOD_T'] = aerosols
1✔
512
    image.header['ROT_T'] = rotation_angle
1✔
513
    image.header['ROTATION'] = int(with_rotation)
1✔
514
    image.header['STARS'] = int(with_stars)
1✔
515
    image.header['BKGD_LEV'] = background.level
1✔
516
    image.header['PSF_DEG'] = spectrogram.chromatic_psf.deg
1✔
517
    image.header['PSF_TYPE'] = parameters.PSF_TYPE
1✔
518
    psf_poly_params_truth = np.array(psf_poly_params)
1✔
519
    if psf_poly_params_truth.size > spectrum.spectrogram_Nx:
1✔
520
        psf_poly_params_truth = psf_poly_params_truth[spectrum.spectrogram_Nx:]
×
521
    image.header['LBDAS_T'] = np.array_str(true_lambdas, max_line_width=1000000, precision=2)
1✔
522
    image.header['AMPLIS_T'] = np.array_str(true_spectrum, max_line_width=1000000, precision=2)
1✔
523
    image.header['PSF_P_T'] = np.array_str(psf_poly_params_truth, max_line_width=1000000, precision=4)
1✔
524
    image.save_image(output_filename, overwrite=True)
1✔
525
    return image
1✔
526

527

528
if __name__ == "__main__":
1✔
UNCOV
529
    import doctest
×
530

UNCOV
531
    doctest.testmod()
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc