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

LSSTDESC / Spectractor / 4192582316

pending completion
4192582316

push

github

GitHub
Merge pull request #116 from LSSTDESC/stardice

65 of 65 new or added lines in 7 files covered. (100.0%)

7163 of 7979 relevant lines covered (89.77%)

0.9 hits per line

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

84.93
/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)
6
from spectractor.extractor.images import Image, find_target
1✔
7
from spectractor.astrometry import get_gaia_coords_after_proper_motion, source_detection
1✔
8
from spectractor.extractor.background import remove_image_background_sextractor
1✔
9
from spectractor.simulation.throughput import TelescopeTransmission
1✔
10
from spectractor.simulation.simulator import SpectrogramSimulatorCore, SimulatorInit
1✔
11
from spectractor.extractor.psf import PSF
1✔
12

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

24

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

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

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

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

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

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

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

96

97
class StarFieldModel:
1✔
98

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

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

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

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

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

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

216

217
class BackgroundModel:
1✔
218
    """Class to model the background of the simulated image.
219

220
    The background model size is set with the parameters.CCD_IMSIZE global keyword.
221

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

231
    def __init__(self, level, frame=None):
1✔
232
        """Create a BackgroundModel instance.
233

234
        The background model size is set with the parameters.CCD_IMSIZE global keyword.
235

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

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

265
    def model(self):
1✔
266
        """Compute the background model for the image simulation in image units.
267

268
        A shadowing vignetting frame is roughly simulated if self.frame is set.
269
        The background model size is set with the parameters.CCD_IMSIZE global keyword.
270

271
        Returns
272
        -------
273
        bkgd: array_like
274
            The array of the background model.
275

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

290
    def plot_model(self):
1✔
291
        """Plot the background model.
292

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

312

313
class ImageModel(Image):
1✔
314

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

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

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

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

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

365

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

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

419
    # Spectrum model
420
    my_logger.info('\n\tSpectrum model...')
1✔
421
    airmass = image.header['AIRMASS']
1✔
422
    pressure = image.header['OUTPRESS']
1✔
423
    temperature = image.header['OUTTEMP']
1✔
424
    telescope = TelescopeTransmission(image.filter_label)
1✔
425

426
    # Rotation: useful only to fill the Dy_disp_axis column in PSF table
427
    if not with_rotation:
1✔
428
        rotation_angle = 0
×
429
    else:
430
        rotation_angle = spectrum.rotation_angle
1✔
431

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

452
    # Simulate spectrogram
453
    spectrogram = SpectrogramSimulatorCore(spectrum, telescope, disperser, airmass, pressure,
1✔
454
                                           temperature, pwv=pwv, ozone=ozone, aerosols=aerosols, A1=A1, A2=A2,
455
                                           D=spectrum.disperser.D, shift_x=0., shift_y=0., shift_t=0., B=1.,
456
                                           psf_poly_params=psf_poly_params, angle=rotation_angle, with_background=False,
457
                                           fast_sim=False, full_image=True, with_adr=with_adr)
458

459
    # now we include effects related to the wrong extraction of the spectrum:
460
    # wrong estimation of the order 0 position and wrong DISTANCE2CCD
461
    # distance = spectrum.chromatic_psf.get_algebraic_distance_along_dispersion_axis()
462
    # spectrum.disperser.D = parameters.DISTANCE2CCD
463
    # spectrum.lambdas = spectrum.disperser.grating_pixel_to_lambda(distance, spectrum.x0, order=1)
464

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

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

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

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

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

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

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

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

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

528

529
if __name__ == "__main__":
1✔
530
    import doctest
1✔
531

532
    doctest.testmod()
1✔
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