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

LSSTDESC / Spectractor / 5018356637

pending completion
5018356637

push

github

GitHub
Merge pull request #129 from LSSTDESC/stardice

4 of 4 new or added lines in 2 files covered. (100.0%)

7189 of 8010 relevant lines covered (89.75%)

0.9 hits per line

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

82.61
/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.
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.
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, angstrom_exponent=None,
1✔
370
             psf_poly_params=None, psf_type=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
    image = ImageModel(image_filename, target_label=spectrum.target.label)
1✔
391
    guess = np.array([spectrum.header['TARGETX'], spectrum.header['TARGETY']])
1✔
392
    if "CCDREBIN" in spectrum.header:
1✔
393
        guess *= spectrum.header["CCDREBIN"]
1✔
394
    if parameters.DEBUG:
1✔
395
        image.plot_image(scale='symlog', target_pixcoords=guess)
1✔
396
    # Fit the star 2D profile
397
    my_logger.info('\n\tSearch for the target in the image...')
1✔
398
    target_pixcoords = find_target(image, guess)
1✔
399
    # Background model
400
    my_logger.info('\n\tBackground model...')
1✔
401
    bgd_level = float(np.mean(spectrum.spectrogram_bgd))
1✔
402
    background = BackgroundModel(level=bgd_level, frame=None)  # (1600, 1650, 100))
1✔
403
    if parameters.DEBUG:
1✔
404
        background.plot_model()
1✔
405

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

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

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

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

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

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

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

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

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

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

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

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

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

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

524

525
if __name__ == "__main__":
1✔
526
    import doctest
1✔
527

528
    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