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

LSSTDESC / Spectractor / 13366817993

17 Feb 2025 09:18AM UTC coverage: 74.269% (-14.7%) from 88.985%
13366817993

push

github

web-flow
Merge pull request #170 from LSSTDESC/169-micromamba-failing-in-ci

Update build.yaml with conda

25 of 34 new or added lines in 1 file covered. (73.53%)

1262 existing lines in 20 files now uncovered.

6402 of 8620 relevant lines covered (74.27%)

0.74 hits per line

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

81.67
/spectractor/simulation/image_simulation.py
1
from spectractor import parameters
1✔
2
from spectractor.config import set_logger
1✔
3
from spectractor.tools import (rebin, 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
1✔
19
from scipy.signal.windows import gaussian
1✔
20
import matplotlib.pyplot as plt
1✔
21
from matplotlib.ticker import MaxNLocator
1✔
22
import numpy as np
1✔
23
import copy
1✔
24
import os
1✔
25

26

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

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

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

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

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

55
        Examples
56
        --------
57
        >>> from spectractor.extractor.psf import Moffat
58
        >>> p = (100, 50, 50, 5, 2, 200)
59
        >>> psf = Moffat(p)
60
        >>> s = StarModel((20, 10), psf, 200)
61
        >>> s.plot_model()
62
        >>> s.x0
63
        20
64
        >>> s.y0
65
        10
66
        >>> s.amplitude
67
        200
68
        """
69
        self.my_logger = set_logger(self.__class__.__name__)
1✔
70
        self.x0 = centroid_coords[0]
1✔
71
        self.y0 = centroid_coords[1]
1✔
72
        self.amplitude = amplitude
1✔
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] = self.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
        wcs_file_name = set_wcs_file_name(self.image.file_name)
1✔
127
        gaia_catalog_file_name = set_gaia_catalog_file_name(self.image.file_name)
1✔
128
        if os.path.isfile(wcs_file_name) and os.path.isfile(gaia_catalog_file_name):
1✔
129
            # load gaia catalog
130
            gaia_catalog = ascii.read(gaia_catalog_file_name, format="ecsv")
1✔
131
            gaia_coord_after_motion = get_gaia_coords_after_proper_motion(gaia_catalog, self.image.date_obs)
1✔
132
            # load WCS
133
            wcs = load_wcs_from_file(wcs_file_name)
1✔
134
            # catalog matching to set star positions using Gaia
135
            target_coord = wcs.all_pix2world([x0 * parameters.CCD_REBIN], [y0 * parameters.CCD_REBIN], 0)
1✔
136
            target_coord = SkyCoord(ra=target_coord[0] * units.deg, dec=target_coord[1] * units.deg,
1✔
137
                                    frame="icrs", obstime=self.image.date_obs, equinox="J2000")
138
            gaia_target_index, dist_2d, dist_3d = target_coord.match_to_catalog_sky(gaia_coord_after_motion)
1✔
139
            dx, dy = 0, 0
1✔
140
            for gaia_i in range(len(gaia_catalog)):
1✔
141
                x, y = np.array(wcs.all_world2pix(gaia_coord_after_motion[gaia_i].ra,
1✔
142
                                                  gaia_coord_after_motion[gaia_i].dec, 0)) / parameters.CCD_REBIN
143
                if gaia_i == gaia_target_index[0]:
1✔
144
                    dx = x0 - x
1✔
145
                    dy = y0 - y
1✔
146
                A = 10 ** (-gaia_catalog['phot_g_mean_mag'][gaia_i] / 2.5)
1✔
147
                self.stars.append(StarModel([x, y], self.image.target_star2D, A))
1✔
148
                self.pixcoords.append([x, y])
1✔
149
            # rescale using target fitted amplitude
150
            amplitudes = np.array([star.amplitude for star in self.stars])
1✔
151
            target_flux = self.image.target_star2D.params.values[0]
1✔
152
            amplitudes *= target_flux / self.stars[gaia_target_index[0]].amplitude * self.flux_factor
1✔
153
            for k, star in enumerate(self.stars):
1✔
154
                star.amplitude = amplitudes[k]
1✔
155
                # shift x,y star positions according to target position
156
                star.x0 += dx
1✔
157
                star.y0 += dy
1✔
158
                star.psf.params.values[1] += dx
1✔
159
                star.psf.params.values[2] += dy
1✔
160
                star.psf.params.values[0] = amplitudes[k]
1✔
UNCOV
161
        elif os.path.isfile(sources_file_name):
×
162
            # load sources positions and flux
163
            sources = Table.read(sources_file_name)
×
164
            sources['X'].name = "xcentroid"
×
165
            sources['Y'].name = "ycentroid"
×
166
            sources['FLUX'].name = "flux"
×
167
            for k, source in enumerate(sources):
×
168
                x, y = np.array([sources['xcentroid'][k], sources['ycentroid'][k]]) / parameters.CCD_REBIN
×
169
                A = sources['flux'][k] * self.flux_factor
×
170
                self.stars.append(StarModel([x, y], self.image.target_star2D, A))
×
171
                self.pixcoords.append([x, y])
×
172
        else:
173
            # try extraction using iraf source detection
174
            # mask background, faint stars, and saturated pixels
UNCOV
175
            data = np.copy(self.image.data)
×
176
            # mask order0 and spectrum
UNCOV
177
            margin = 30
×
UNCOV
178
            mask = np.zeros(data.shape, dtype=bool)
×
UNCOV
179
            for y in range(int(y0) - 100, int(y0) + 100):
×
UNCOV
180
                for x in range(self.image.data.shape[1]):
×
UNCOV
181
                    u, v = pixel_rotation(x, y, self.image.disperser.theta([x0, y0]) * np.pi / 180., x0, y0)
×
UNCOV
182
                    if margin > v > -margin:
×
UNCOV
183
                        mask[y, x] = True
×
184
            # remove background and detect sources
UNCOV
185
            data_wo_bkg = remove_image_background_sextractor(data)
×
UNCOV
186
            sources = iraf_source_detection(data_wo_bkg, mask=mask)
×
UNCOV
187
            for k, source in enumerate(sources):
×
UNCOV
188
                x, y = sources['xcentroid'][k], sources['ycentroid'][k]
×
UNCOV
189
                A = sources['flux'][k] * self.flux_factor
×
UNCOV
190
                self.stars.append(StarModel([x, y], self.image.target_star2D, A))
×
UNCOV
191
                self.pixcoords.append([x, y])
×
192
        self.pixcoords = np.array(self.pixcoords).T
1✔
193

194
    def model(self, x, y):
1✔
195
        if self.field is None:
1✔
196
            window = int(20 * self.fwhm)
1✔
197
            self.field = self.stars[0].psf.evaluate(np.array([x, y]))
1✔
198
            for k in range(1, len(self.stars)):
1✔
199
                left = max(0, int(self.pixcoords[0][k]) - window)
1✔
200
                right = min(np.max(x), int(self.pixcoords[0][k]) + window)
1✔
201
                low = max(0, int(self.pixcoords[1][k]) - window)
1✔
202
                up = min(np.max(y), int(self.pixcoords[1][k]) + window)
1✔
203
                if up < low or left > right:
1✔
204
                    continue
1✔
205
                yy, xx = np.mgrid[low:up, left:right]
1✔
206
                self.field[low:up, left:right] += self.stars[k].psf.evaluate(np.array([xx, yy]))
1✔
207
        return self.field
1✔
208

209
    def plot_model(self):
1✔
210
        fig, ax = plt.subplots(1, 1)
1✔
211
        plot_image_simple(ax, self.field, scale="log10", target_pixcoords=self.pixcoords)
1✔
212
        if parameters.DISPLAY:
1✔
213
            plt.show()
×
214
        if parameters.PdfPages:
1✔
215
            parameters.PdfPages.savefig()
×
216

217

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

221
    Attributes
222
    ----------
223
    Nx: int
224
        Size of the background along X axis in pixels.
225
    Ny: int
226
        Size of the background along Y axis in pixels.
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, Nx, Ny, level, frame=None):
1✔
235
        """Create a BackgroundModel instance.
236

237
        Parameters
238
        ----------
239
        Nx: int
240
            Size of the background along X axis in pixels.
241
        Ny: int
242
            Size of the background along Y axis in pixels.
243
        level: float
244
            The mean level of the background in image units.
245
        frame: array_like, None
246
            (x, y, smooth) right and upper limits in pixels of a vignetting frame,
247
            and the smoothing gaussian width (default: None).
248

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

272
    def model(self):
1✔
273
        """Compute the background model for the image simulation in image units.
274

275
        A shadowing vignetting frame is roughly simulated if self.frame is set.
276

277
        Returns
278
        -------
279
        bkgd: array_like
280
            The array of the background model.
281

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

296
    def plot_model(self):
1✔
297
        """Plot the background model.
298

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

318

319
class FlatModel:
1✔
320
    """Class to model the pixel flat of the simulated image. Flat is dimensionless and its average must be one.
321

322
    Attributes
323
    ----------
324
    Nx: int
325
        Size of the background along X axis in pixels.
326
    Ny: int
327
        Size of the background along Y axis in pixels.
328
    gains: array_like
329
        The list of gains to apply. The average must be one.
330
    randomness_level: float
331
        Level of random quantum efficiency to apply to pixels (default: 0.).
332
    """
333

334
    def __init__(self, Nx, Ny, gains, randomness_level=0.):
1✔
335
        """Create a FlatModel instance. Flat is dimensionless and its average must be one.
336

337
        Parameters
338
        ----------
339
        Nx: int
340
            Size of the background along X axis in pixels.
341
        Ny: int
342
            Size of the background along Y axis in pixels.
343
        gains: array_like
344
            The list of gains to apply. The average must be one.
345
        randomness_level: float
346
            Level of random quantum efficiency to apply to pixels (default: 0.).
347

348
        Examples
349
        --------
350
        >>> from spectractor import parameters
351
        >>> Nx, Ny = 200, 300
352
        >>> flat = FlatModel(Nx, Ny, gains=[[1, 2, 3, 4], [4, 3, 2, 1]])
353
        >>> model = flat.model()
354
        >>> print(f"{np.mean(model):.4f}")
355
        1.0000
356
        >>> model.shape
357
        (200, 200)
358
        >>> flat.plot_model()
359
        """
360
        self.my_logger = set_logger(self.__class__.__name__)
1✔
361
        self.Nx = Nx
1✔
362
        self.Ny = Ny
1✔
363
        self.gains = np.atleast_2d(gains).astype(float)
1✔
364
        if len(self.gains) <= 0:
1✔
365
            raise ValueError(f"Gains list is empty")
×
366
        if np.any(self.gains <= 0):
1✔
367
            raise ValueError(f"One the gain values is negative. Got {self.gains}.")
×
368
        if np.mean(self.gains) != 1.:
1✔
369
            self.my_logger.warning(f"\n\tGains list average is not one but {np.mean(self.gains)}. "
1✔
370
                                   "I scaled them to have an average of one.")
371
            self.gains /= np.mean(self.gains)
1✔
372
        self.my_logger.warning(f'\n\tRelative gains are set to {self.gains}.')
1✔
373
        self.randomness_level = randomness_level
1✔
374

375
    def model(self):
1✔
376
        """Compute the flat model for the image simulation (no units).
377

378
        Returns
379
        -------
380
        flat: array_like
381
            The array of the flat model.
382
        """
383
        yy, xx = np.mgrid[0:self.Nx:1, 0:self.Ny:1]
1✔
384
        flat = np.ones_like(xx, dtype=float)
1✔
385
        hflats = np.array_split(flat, self.gains.shape[0])
1✔
386
        for h in range(self.gains.shape[0]):
1✔
387
            vflats = np.array_split(hflats[h].T, self.gains.shape[1])
1✔
388
            for v in range(self.gains.shape[1]):
1✔
389
                vflats[v] *= self.gains[h,v]
1✔
390
            hflats[h] = np.concatenate(vflats).T
1✔
391
        flat = np.concatenate(hflats).T
1✔
392
        if self.randomness_level != 0:
1✔
393
            flat += np.random.uniform(-self.randomness_level, self.randomness_level, size=flat.shape)
1✔
394

395
        return flat
1✔
396

397
    def plot_model(self):
1✔
398
        """Plot the flat model.
399

400
        """
401
        flat = self.model()
1✔
402
        fig, ax = plt.subplots(1, 1)
1✔
403
        im = plt.imshow(flat, origin='lower', cmap='jet')
1✔
404
        ax.grid(color='white', ls='solid')
1✔
405
        ax.grid(True)
1✔
406
        ax.set_xlabel('X [pixels]')
1✔
407
        ax.set_ylabel('Y [pixels]')
1✔
408
        ax.set_title('Flat model')
1✔
409
        cb = plt.colorbar(im, ax=ax)
1✔
410
        cb.formatter.set_powerlimits((0, 0))
1✔
411
        cb.locator = MaxNLocator(7, prune=None)
1✔
412
        cb.update_ticks()
1✔
413
        cb.set_label('Dimensionless')  # ,fontsize=16)
1✔
414
        if parameters.DISPLAY:
1✔
415
            plt.show()
×
416
        if parameters.PdfPages:
1✔
417
            parameters.PdfPages.savefig()
×
418

419

420

421
class ImageModel(Image):
1✔
422

423
    def __init__(self, filename, target_label=None):
1✔
424
        self.my_logger = set_logger(self.__class__.__name__)
1✔
425
        Image.__init__(self, filename, target_label=target_label)
1✔
426
        self.true_lambdas = None
1✔
427
        self.true_spectrum = None
1✔
428

429
    def compute(self, star, background, spectrogram, starfield=None, flat=None):
1✔
430
        yy, xx = np.mgrid[0:parameters.CCD_IMSIZE:1, 0:parameters.CCD_IMSIZE:1]
1✔
431
        if starfield is not None:
1✔
432
            starfield_mod = starfield.model(xx, yy)
1✔
433
            self.data = starfield_mod
1✔
434
            self.starfield = np.copy(starfield_mod)
1✔
435
            if parameters.DEBUG:
1✔
436
                self.plot_image(scale='symlog', target_pixcoords=starfield.pixcoords)
1✔
437
                starfield.plot_model()
1✔
438
        else:
439
            self.data = star.psf.evaluate(np.array([xx, yy]))
×
440
        self.data += background.model()
1✔
441
        if spectrogram.full_image:
1✔
442
            self.data[spectrogram.spectrogram_ymin:spectrogram.spectrogram_ymax, :] += spectrogram.spectrogram_data
1✔
443
        else:
444
            self.data[spectrogram.spectrogram_ymin:spectrogram.spectrogram_ymax,
×
445
                      spectrogram.spectrogram_xmin:spectrogram.spectrogram_xmax] += spectrogram.spectrogram_data
446
        if flat is not None:
1✔
447
            flat_mod = flat.model()
1✔
448
            self.data *= flat_mod
1✔
449
            self.flat = flat_mod
1✔
450

451
    def add_poisson_and_read_out_noise(self):  # pragma: no cover
452
        if self.units != 'ADU':
453
            raise AttributeError('Poisson noise procedure has to be applied on map in ADU units')
454
        d = np.copy(self.data).astype(float)
455
        # convert to electron counts
456
        d *= self.gain
457
        # Poisson noise
458
        noisy = np.random.poisson(d).astype(float)
459
        # Add read-out noise is available
460
        if self.read_out_noise is not None:
461
            noisy += np.random.normal(scale=self.read_out_noise)
462
        # reconvert to ADU
463
        self.data = noisy / self.gain
464
        # removes zeros
465
        min_noz = np.min(self.data[self.data > 0])
466
        self.data[self.data <= 0] = min_noz
467

468
    def save_image(self, output_filename, overwrite=False):
1✔
469
        hdu0 = fits.PrimaryHDU()
1✔
470
        hdu0.data = self.data
1✔
471
        hdu0.header = self.header
1✔
472
        hdu1 = fits.ImageHDU()
1✔
473
        # hdu1.data = [self.true_lambdas, self.true_spectrum]
474
        hdulist = fits.HDUList([hdu0, hdu1])
1✔
475
        hdulist.writeto(output_filename, overwrite=overwrite)
1✔
476
        self.my_logger.info('\n\tImage saved in %s' % output_filename)
1✔
477

478
    def load_image(self, filename):
1✔
479
        super(ImageModel, self).load_image(filename)
1✔
480
        # hdu_list = fits.open(filename)
481
        # self.true_lambdas, self.true_spectrum = hdu_list[1].data
482

483

484
def ImageSim(image_filename, spectrum_filename, outputdir, pwv=5, ozone=300, aerosols=0.03, A1=1, A2=1, A3=1, angstrom_exponent=None,
1✔
485
             psf_poly_params=None, psf_type=None, diffraction_orders=None, with_rotation=True, with_starfield=True, with_adr=True, with_noise=True, with_flat=True):
486
    """ The basic use of the extractor consists first to define:
487
    - the path to the fits image from which to extract the image,
488
    - the path of the output directory to save the extracted spectrum (created automatically if does not exist yet),
489
    - the rough position of the object in the image,
490
    - the name of the target (to search for the extra-atmospheric spectrum if available).
491
    Then different parameters and systematics can be set:
492
    - pwv: the pressure water vapor (in mm)
493
    - ozone: the ozone quantity (in XX)
494
    - aerosols: the vertical aerosol optical depth
495
    - A1: a global grey absorption parameter for the spectrum
496
    - A2: the relative amplitude of second order compared with first order
497
    - with_rotation: rotate the spectrum according to the disperser characteristics (True by default)
498
    - with_stars: include stars in the image field (True by default)
499
    - with_adr: include ADR effect (True by default)
500
    - with_flat: include flat (True by default)
501
    """
502
    my_logger = set_logger(__name__)
1✔
503
    my_logger.info(f'\n\tStart IMAGE SIMULATOR')
1✔
504
    # Load reduced image
505
    spectrum = Spectrum(spectrum_filename)
1✔
506
    parameters.CALLING_CODE = ""
1✔
507
    if diffraction_orders is None:
1✔
508
        diffraction_orders = np.arange(spectrum.order, spectrum.order + 3 * np.sign(spectrum.order), np.sign(spectrum.order))
1✔
509
    image = ImageModel(image_filename, target_label=spectrum.target.label)
1✔
510
    guess = np.array([spectrum.header['TARGETX'], spectrum.header['TARGETY']])
1✔
511
    if parameters.CCD_REBIN != 1:
1✔
512
        # these lines allow to simulate images using rebinned spectrum files
513
        guess *= parameters.CCD_REBIN
×
514
        new_shape = np.asarray((parameters.CCD_IMSIZE, parameters.CCD_IMSIZE))
×
515
        old_edge = parameters.CCD_IMSIZE * parameters.CCD_REBIN
×
516
        image.gain = rebin(image.gain[:old_edge, :old_edge], new_shape, FLAG_MAKESUM=False)
×
517
        image.read_out_noise = rebin(image.read_out_noise[:old_edge, :old_edge], new_shape, FLAG_MAKESUM=False)
×
518

519
    if parameters.DEBUG:
1✔
520
        image.plot_image(scale='symlog', target_pixcoords=guess)
1✔
521
    # Fit the star 2D profile
522
    my_logger.info('\n\tSearch for the target in the image...')
1✔
523
    target_pixcoords = find_target(image, guess)
1✔
524
    # Background model
525
    my_logger.info('\n\tBackground model...')
1✔
526
    bgd_level = float(np.mean(spectrum.spectrogram_bgd))
1✔
527
    background = BackgroundModel(parameters.CCD_IMSIZE, parameters.CCD_IMSIZE, level=bgd_level, frame=None)
1✔
528
    if parameters.DEBUG:
1✔
529
        background.plot_model()
1✔
530

531
    # Target model
532
    my_logger.info('\n\tStar model...')
1✔
533
    # Spectrogram is simulated with spectrum.x0 target position: must be this position to simulate the target.
534
    star = StarModel(np.array(image.target_pixcoords) / parameters.CCD_REBIN, image.target_star2D, image.target_star2D.params.values[0])
1✔
535
    # reso = star.fwhm
536
    if parameters.DEBUG:
1✔
537
        star.plot_model()
1✔
538

539
    # Star field model
540
    starfield = None
1✔
541
    if with_starfield:
1✔
542
        my_logger.info('\n\tStar field model...')
1✔
543
        starfield = StarFieldModel(image, flux_factor=1)
1✔
544

545
    # Flat model
546
    flat = None
1✔
547
    if with_flat:
1✔
548
        my_logger.info('\n\tFlat model...')
1✔
549
        flat = FlatModel(parameters.CCD_IMSIZE, parameters.CCD_IMSIZE, gains=[[1, 2, 3, 4], [4, 3, 2, 1]], randomness_level=1e-2)
1✔
550
        if parameters.DEBUG:
1✔
551
            flat.plot_model()
1✔
552

553
    # Spectrum model
554
    my_logger.info('\n\tSpectrum model...')
1✔
555
    airmass = image.header['AIRMASS']
1✔
556
    pressure = image.header['OUTPRESS']
1✔
557
    temperature = image.header['OUTTEMP']
1✔
558

559
    # Rotation: useful only to fill the Dy_disp_axis column in PSF table
560
    if not with_rotation:
1✔
561
        rotation_angle = 0
×
562
    else:
563
        rotation_angle = spectrum.rotation_angle
1✔
564

565
    # Load PSF
566
    if psf_type is not None:
1✔
567
        from spectractor.extractor.psf import load_PSF
×
568
        parameters.PSF_TYPE = psf_type
×
569
        psf = load_PSF(psf_type=psf_type)
×
570
        spectrum.psf = psf
×
571
        spectrum.chromatic_psf.psf = psf
×
572
    if psf_poly_params is None:
1✔
573
        my_logger.info('\n\tUse PSF parameters from _table.csv file.')
×
574
        psf_poly_params = spectrum.chromatic_psf.from_table_to_poly_params()
×
575
    else:
576
        spectrum.chromatic_psf.deg = ((len(psf_poly_params) - 1) // (len(spectrum.chromatic_psf.psf.params.labels) - 2) - 1) // len(diffraction_orders)
1✔
577
        spectrum.chromatic_psf.set_polynomial_degrees(spectrum.chromatic_psf.deg)
1✔
578
        if spectrum.chromatic_psf.deg == 0:  # x_c must have deg >= 1
1✔
579
            psf_poly_params.insert(1, 0)
×
580
        my_logger.info(f'\n\tUse PSF parameters {psf_poly_params} as polynoms of '
1✔
581
                       f'degree {spectrum.chromatic_psf.degrees}')
582
    if psf_type is not None and psf_poly_params is not None:
1✔
583
        spectrum.chromatic_psf.init_from_table()
×
584

585
    # Simulate spectrogram
586
    atmosphere = Atmosphere(airmass, pressure, temperature)
1✔
587
    spectrogram = SpectrogramModel(spectrum, atmosphere=atmosphere, fast_sim=False, full_image=True,
1✔
588
                                   with_adr=with_adr, diffraction_orders=diffraction_orders)
589
    spectrogram.simulate(A1, A2, A3, aerosols, angstrom_exponent, ozone, pwv,
1✔
590
                         spectrum.disperser.D, 0, 0, rotation_angle, psf_poly_params)
591

592
    # Image model
593
    my_logger.info('\n\tImage model...')
1✔
594
    image.compute(star, background, spectrogram, starfield=starfield, flat=flat)
1✔
595

596
    # Recover true spectrum
597
    spectrogram.set_true_spectrum(spectrogram.lambdas, aerosols, ozone, pwv, shift_t=0)
1✔
598
    true_lambdas = np.copy(spectrogram.true_lambdas)
1✔
599
    true_spectrum = np.copy(spectrogram.true_spectrum)
1✔
600

601
    # Saturation effects
602
    saturated_pixels = np.where(spectrogram.spectrogram_data > image.saturation)[0]
1✔
603
    if len(saturated_pixels) > 0:
1✔
604
        my_logger.warning(f"\n\t{len(saturated_pixels)} saturated pixels detected above saturation "
×
605
                          f"level at {image.saturation} ADU/s in the spectrogram."
606
                          f"\n\tSpectrogram maximum is at {np.max(spectrogram.spectrogram_data)} ADU/s.")
607
    image.data[image.data > image.saturation] = image.saturation
1✔
608

609
    # Convert data from ADU/s in ADU
610
    image.convert_to_ADU_units()
1✔
611

612
    # Add Poisson and read-out noise
613
    if with_noise:
1✔
614
        image.add_poisson_and_read_out_noise()
×
615

616
    # Round float ADU into closest integers
617
    # image.data = np.around(image.data)
618
    if parameters.OBS_NAME == "AUXTEL":
1✔
619
        image.data = image.data.T[::-1, ::-1]
×
620

621
    # Plot
622
    if parameters.VERBOSE and parameters.DISPLAY:  # pragma: no cover
623
        image.convert_to_ADU_rate_units()
624
        image.plot_image(scale="symlog", title="Image simulation", target_pixcoords=target_pixcoords, units=image.units)
625
        image.convert_to_ADU_units()
626

627
    # Set output path
628
    ensure_dir(outputdir)
1✔
629
    output_filename = image_filename.split('/')[-1]
1✔
630
    output_filename = (output_filename.replace('reduc', 'sim')).replace('trim', 'sim').replace('exposure', 'sim')
1✔
631
    output_filename = os.path.join(outputdir, output_filename)
1✔
632

633
    # Save images and parameters
634
    image.header['A1_T'] = A1
1✔
635
    image.header['A2_T'] = A2
1✔
636
    image.header['A3_T'] = A3
1✔
637
    image.header['X0_T'] = spectrum.x0[0]
1✔
638
    image.header['Y0_T'] = spectrum.x0[1]
1✔
639
    image.header['D2CCD_T'] = float(spectrum.disperser.D)
1✔
640
    image.header['OZONE_T'] = ozone
1✔
641
    image.header['PWV_T'] = pwv
1✔
642
    image.header['VAOD_T'] = aerosols
1✔
643
    image.header['ROT_T'] = rotation_angle
1✔
644
    image.header['ROTATION'] = int(with_rotation)
1✔
645
    image.header['STARS'] = int(with_starfield)
1✔
646
    image.header['BKGD_LEV'] = background.level
1✔
647
    image.header['PSF_DEG'] = spectrogram.chromatic_psf.deg
1✔
648
    image.header['PSF_TYPE'] = parameters.PSF_TYPE
1✔
649
    psf_poly_params_truth = np.array(psf_poly_params)
1✔
650
    if psf_poly_params_truth.size > spectrum.spectrogram_Nx:
1✔
651
        psf_poly_params_truth = psf_poly_params_truth[spectrum.spectrogram_Nx:]
×
652
    image.header['LBDAS_T'] = str(np.round(true_lambdas, decimals=2).tolist())
1✔
653
    image.header['AMPLIS_T'] = str(true_spectrum.tolist())
1✔
654
    image.header['PSF_P_T'] = str(psf_poly_params_truth.tolist())
1✔
655
    image.save_image(output_filename, overwrite=True)
1✔
656
    return image
1✔
657

658

659
if __name__ == "__main__":
1✔
660
    import doctest
×
661

662
    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