• 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

92.31
/spectractor/simulation/simulator.py
1
import copy
1✔
2
import numpy as np
1✔
3

4
from scipy.interpolate import interp1d
1✔
5

6
from spectractor.extractor.spectrum import Spectrum
1✔
7
from spectractor.extractor.targets import Target
1✔
8
from spectractor.extractor.psf import load_PSF
1✔
9
from spectractor.tools import fftconvolve_gaussian
1✔
10
from spectractor.config import set_logger
1✔
11
from spectractor.simulation.atmosphere import Atmosphere, AtmosphereGrid
1✔
12
import spectractor.parameters as parameters
1✔
13

14

15
class SpectrumSimulation(Spectrum):
1✔
16

17
    def __init__(self, spectrum, target=None, disperser=None, throughput=None, atmosphere=None, fast_sim=True, with_adr=True):
1✔
18
        """Class to simulate cross spectrum.
19

20
        Parameters
21
        ----------
22
        spectrum: Spectrum
23
            Spectrum instance.
24
        target: Target
25
            Target instance.
26
        throughput: TelescopeTransmission, optional
27
            Telescope throughput (default: None).
28
        disperser: Grating, optional
29
            Disperser instance (default: None).
30
        atmosphere: Atmosphere, optional
31
            Atmosphere or AtmosphereGrid instance to make the atmospheric simulation (default: None).
32
        fast_sim: bool, optional
33
            If True, do a fast simulation without integrating within the wavelength bins (default: True).
34
        with_adr: bool, optional
35
            If True, use ADR model to build lambda array (default: False).
36

37
        Examples
38
        --------
39
        >>> spectrum = Spectrum("./tests/data/reduc_20170530_134_spectrum.fits")
40
        >>> atmosphere = Atmosphere(airmass=1.2, pressure=800, temperature=10)
41
        >>> sim = SpectrumSimulation(spectrum, atmosphere=atmosphere, fast_sim=True)
42

43
        """
44
        Spectrum.__init__(self)
1✔
45
        for k, v in list(spectrum.__dict__.items()):
1✔
46
            self.__dict__[k] = copy.copy(v)
1✔
47
        self.my_logger = set_logger(self.__class__.__name__)
1✔
48
        # if parameters.CCD_REBIN > 1:
49
        #     self.chromatic_psf.table['Dx'] *= parameters.CCD_REBIN
50
        #     apply_rebinning_to_parameters(reverse=True)
51
        if target is not None:
1✔
52
            self.target = target
1✔
53
        if disperser is not None:
1✔
54
            self.disperser = disperser
×
55
        if throughput is not None:
1✔
56
            self.throughput = throughput
×
57
        self.atmosphere = atmosphere
1✔
58
        self.fast_sim = fast_sim
1✔
59
        self.with_adr = with_adr
1✔
60
        # save original pixel distances to zero order
61
        # self.disperser.grating_lambda_to_pixel(self.lambdas, x0=self.x0, order=1)
62
        # now reset data
63
        self.lambdas = None
1✔
64
        self.lambdas_order2 = None
1✔
65
        self.err = None
1✔
66
        self.model = lambda x: np.zeros_like(x)
1✔
67
        self.model_err = lambda x: np.zeros_like(x)
1✔
68
        lbdas_sed = self.target.wavelengths[0]
1✔
69
        sub = np.where((lbdas_sed > parameters.LAMBDA_MIN) & (lbdas_sed < parameters.LAMBDA_MAX))
1✔
70
        self.lambdas_step = min(parameters.LAMBDA_STEP, np.min(lbdas_sed[sub]))
1✔
71

72
    def simulate_without_atmosphere(self, lambdas):
1✔
73
        """Compute the spectrum of an object and its uncertainties
74
        after its transmission throught the instrument except the atmosphere.
75
        The units remain the ones of the Target instance.
76

77
        Parameters
78
        ----------
79
        lambdas: array_like
80
            The wavelength array in nm
81

82
        Returns
83
        -------
84
        spectrum: array_like
85
            The spectrum in Target units.
86
        spectrum_err: array_like
87
            The spectrum uncertainties in Target units.
88
        """
89
        self.lambdas = lambdas
1✔
90
        self.lambdas_binwidths = np.gradient(lambdas)
1✔
91
        self.data = self.disperser.transmission(lambdas)
1✔
92
        self.data *= self.throughput.transmission(lambdas)
1✔
93
        self.data *= self.target.sed(lambdas)
1✔
94
        self.err = np.zeros_like(self.data)
1✔
95
        idx = np.where(self.throughput.transmission(lambdas) > 0)[0]
1✔
96
        self.err[idx] = self.throughput.transmission_err(lambdas)[idx] / self.throughput.transmission(lambdas)[idx]
1✔
97
        self.err[idx] *= self.data[idx]
1✔
98
        idx = np.where(self.throughput.transmission(lambdas) <= 0)[0]
1✔
99
        self.err[idx] = 1e6 * np.max(self.err)
1✔
100
        return self.data, self.err
1✔
101

102
    def simulate(self, A1=1.0, A2=0., aerosols=0.05, angstrom_exponent=None, ozone=300, pwv=5, reso=0.,
1✔
103
                 D=parameters.DISTANCE2CCD, shift_x=0., B=0.):
104
        """Simulate the cross spectrum of an object and its uncertainties
105
        after its transmission throught the instrument and the atmosphere.
106

107
        Parameters
108
        ----------
109
        A1: float
110
            Global amplitude of the spectrum (default: 1).
111
        A2: float
112
            Relative amplitude of the order 2 spectrum contamination (default: 0).
113
        aerosols: float
114
            VAOD Vertical Aerosols Optical Depth
115
        angstrom_exponent: float, optional
116
            Angstrom exponent for aerosols. If negative or None, default aerosol model from Libradtran is used.
117
            If value is 0.0192, the atmospheric transmission is very close to the case with angstrom_exponent=None (default: None).
118
        ozone: float
119
            Ozone quantity in Dobson
120
        pwv: float
121
            Precipitable Water Vapor quantity in mm
122
        reso: float
123
            Gaussian kernel size for the convolution (default: 0).
124
        D: float
125
            Distance between the CCD and the disperser in mm (default: parameters.DISTANCE2CCD)
126
        shift_x: float
127
            Shift in pixels of the order 0 position estimate (default: 0).
128
        B: float
129
            Amplitude level for the background (default: 0).
130

131
        Returns
132
        -------
133
        lambdas: array_like
134
            The wavelength array in nm used for the interpolation.
135
        spectrum: array_like
136
            The spectrum interpolated function in Target units.
137
        spectrum_err: array_like
138
            The spectrum uncertainties interpolated function in Target units.
139

140
        Examples
141
        --------
142
        >>> spectrum = Spectrum("./tests/data/reduc_20170530_134_spectrum.fits")
143
        >>> atmosphere = AtmosphereGrid(atmgrid_filename="./tests/data/reduc_20170530_134_atmsim.fits")
144
        >>> sim = SpectrumSimulation(spectrum, atmosphere=atmosphere, fast_sim=True)
145
        >>> lambdas, model, model_err = sim.simulate(A1=1, A2=1, ozone=300, pwv=5, aerosols=0.05, reso=0.,
146
        ... D=parameters.DISTANCE2CCD, shift_x=0., B=0.)
147
        >>> sim.plot_spectrum()
148

149
        .. doctest::
150
            :hide:
151

152
            >>> assert np.sum(lambdas) > 0
153
            >>> assert np.sum(model) > 0
154
            >>> assert np.sum(model) < 1e-10
155
            >>> assert np.sum(sim.data_next_order) > 0
156
            >>> assert np.sum(sim.data_next_order) < 1e-11
157

158
        """
159
        # find lambdas including ADR effect
160
        # must add ADR to get perfect result on atmospheric fit in full chain test with SpectrumSimulation()
161
        lambdas = self.compute_lambdas_in_spectrogram(D, shift_x=shift_x, shift_y=0, angle=self.rotation_angle,
1✔
162
                                                      order=1, with_adr=self.with_adr, niter=5)
163
        lambdas_order2 = self.compute_lambdas_in_spectrogram(D, shift_x=shift_x, shift_y=0, angle=self.rotation_angle,
1✔
164
                                                             order=2, with_adr=self.with_adr, niter=5)
165
        self.lambdas = lambdas
1✔
166
        if self.atmosphere is not None:
1✔
167
            self.atmosphere.set_lambda_range(lambdas)
1✔
168
            atmospheric_transmission = self.atmosphere.simulate(aerosols=aerosols, ozone=ozone, pwv=pwv, angstrom_exponent=angstrom_exponent)
1✔
169
        else:
170
            def atmospheric_transmission(lbda):
×
171
                return 1
×
172
        if self.fast_sim:
1✔
173
            self.data, self.err = self.simulate_without_atmosphere(lambdas)
1✔
174
            self.data *= A1 * atmospheric_transmission(lambdas)
1✔
175
            self.err *= A1 * atmospheric_transmission(lambdas)
1✔
176
        else:
177
            def integrand(lbda):
1✔
178
                return self.target.sed(lbda) * self.throughput.transmission(lbda) \
1✔
179
                       * self.disperser.transmission(lbda) * atmospheric_transmission(lbda)
180

181
            self.data = np.zeros_like(lambdas)
1✔
182
            self.err = np.zeros_like(lambdas)
1✔
183
            for i in range(len(lambdas) - 1):
1✔
184
                lbdas = np.arange(lambdas[i], lambdas[i + 1], self.lambdas_step)
1✔
185
                self.data[i] = A1 * np.mean(integrand(lbdas))
1✔
186
                # self.data[i] = A1 * quad(integrand, lambdas[i], lambdas[i + 1])[0]
187
            self.data[-1] = self.data[-2]
1✔
188
            # self.data /= np.gradient(lambdas)
189
            telescope_transmission = self.throughput.transmission(lambdas)
1✔
190
            idx = telescope_transmission > 0
1✔
191
            self.err[idx] = self.data[idx] * self.throughput.transmission_err(lambdas)[idx] / telescope_transmission[idx]
1✔
192
            idx = telescope_transmission <= 0
1✔
193
            self.err[idx] = 1e6 * np.max(self.err)
1✔
194
        # Now add the systematics
195
        if reso > 0.1:
1✔
196
            self.data = fftconvolve_gaussian(self.data, reso)
1✔
197
            # self.err = np.sqrt(np.abs(fftconvolve_gaussian(self.err ** 2, reso)))
198
        if A2 > 0:
1✔
199
            lambdas_binwidths_order2 = np.gradient(lambdas_order2)
1✔
200
            sim_conv = interp1d(lambdas, self.data * lambdas, kind="linear", bounds_error=False, fill_value=(0, 0))
1✔
201
            err_conv = interp1d(lambdas, self.err * lambdas, kind="linear", bounds_error=False, fill_value=(0, 0))
1✔
202
            spectrum_order2 = self.disperser.ratio_order_2over1(lambdas_order2) * sim_conv(lambdas_order2) \
1✔
203
                              * lambdas_binwidths_order2 / self.lambdas_binwidths
204
            err_order2 = err_conv(lambdas_order2) * lambdas_binwidths_order2 / self.lambdas_binwidths
1✔
205
            self.data = (sim_conv(lambdas) + A2 * spectrum_order2) / lambdas
1✔
206
            self.data_order2 = A2 * spectrum_order2 / lambdas
1✔
207
            self.err = (err_conv(lambdas) + A2 * err_order2) / lambdas
1✔
208
        if B != 0:
1✔
209
            self.data += B / (lambdas * np.gradient(lambdas))
×
210
        if np.any(self.err <= 0) and not np.all(self.err<=0):
1✔
211
            min_positive = np.min(self.err[self.err > 0])
1✔
212
            self.err[np.isclose(self.err, 0., atol=0.01 * min_positive)] = min_positive
1✔
213
        # Save the truth parameters
214
        self.header['OZONE_T'] = ozone
1✔
215
        self.header['LOG10A_T'] = angstrom_exponent
1✔
216
        self.header['PWV_T'] = pwv
1✔
217
        self.header['VAOD_T'] = aerosols
1✔
218
        self.header['A1_T'] = A1
1✔
219
        self.header['A2_T'] = A2
1✔
220
        self.header['RESO_T'] = reso
1✔
221
        self.header['D2CCD_T'] = D
1✔
222
        self.header['X0_T'] = shift_x
1✔
223
        return self.lambdas, self.data, self.err
1✔
224

225

226
class SpectrogramModel(Spectrum):
1✔
227

228
    def __init__(self, spectrum, target=None, disperser=None, throughput=None, atmosphere=None, diffraction_orders=None,
1✔
229
                 with_background=True, fast_sim=True, full_image=False, with_adr=True):
230
        """Class to simulate a spectrogram.
231

232
        Parameters
233
        ----------
234
        spectrum: Spectrum
235
            Spectrum instance to load main properties before simulation.
236
        target: Target
237
            Target instance.
238
        throughput: TelescopeTransmission, optional
239
            Telescope throughput (default: None).
240
        disperser: Grating, optional
241
            Disperser instance (default: None).
242
        atmosphere: Atmosphere, optional
243
            Atmosphere or AtmosphereGrid instance to make the atmospheric simulation (default: None).
244
        diffraction_orders: array_like, optional
245
            List of diffraction orders to simulate. If None, takes first three (default: None).
246
        with_background: bool, optional
247
            If True, add the background model to the simulated spectrogram (default: True).
248
        fast_sim: bool, optional
249
            If True, perform a fast simulation of the spectrum without integrated the spectrum
250
            in pixel bins (default: True).
251
        full_image: bool, optional
252
            If True, simulate the spectrogram on the full CCD size,
253
            otherwise only the cropped spectrogram (default: False).
254
        with_adr: bool, optional
255
            If True, simulate the spectrogram with ADR effect (default: True).
256

257
        Examples
258
        --------
259
        >>> spectrum = Spectrum("./tests/data/reduc_20170530_134_spectrum.fits")
260
        >>> atmosphere = Atmosphere(airmass=1.2, pressure=800, temperature=10)
261
        >>> sim = SpectrogramModel(spectrum, atmosphere=atmosphere, with_background=True, fast_sim=True)
262
        """
263
        Spectrum.__init__(self)
1✔
264
        if diffraction_orders is None:
1✔
265
            self.diffraction_orders = np.arange(spectrum.order, spectrum.order + 3 * np.sign(spectrum.order), np.sign(spectrum.order))
1✔
266
        else:
267
            self.diffraction_orders = diffraction_orders
1✔
268
        if self.diffraction_orders[0] != 1:
1✔
269
            raise NotImplementedError("Spectrogram simulations are only implemented for 1st diffraction order and followings.")
×
270
        if len(self.diffraction_orders) == 0:
1✔
271
            raise ValueError(f"At least one diffraction order must be given for spectrogram simulation.")
×
272
        for k, v in list(spectrum.__dict__.items()):
1✔
273
            self.__dict__[k] = copy.copy(v)
1✔
274
        if target is not None:
1✔
275
            self.target = target
×
276
        if disperser is not None:
1✔
277
            self.disperser = disperser
×
278
        if throughput is not None:
1✔
279
            self.throughput = throughput
×
280
        self.atmosphere = atmosphere
1✔
281

282
        # load the disperser relative transmissions
283
        self.tr_ratio = interp1d(parameters.LAMBDAS, np.ones_like(parameters.LAMBDAS), bounds_error=False, fill_value=1.)
1✔
284
        if abs(self.order) == 1:
1✔
285
            self.tr_ratio_next_order = self.disperser.ratio_order_2over1
1✔
286
            self.tr_ratio_next_next_order = self.disperser.ratio_order_3over1
1✔
287
        elif abs(self.order) == 2:
×
288
            self.tr_ratio_next_order = self.disperser.ratio_order_3over2
×
289
            self.tr_ratio_next_next_order = None
×
290
        elif abs(self.order) == 3:
×
291
            self.tr_ratio_next_order = None
×
292
            self.tr_ratio_next_next_order = None
×
293
        else:
294
            raise ValueError(f"{abs(self.order)=}: must be 1, 2 or 3. "
×
295
                             f"Higher diffraction orders not implemented yet in full forward model.")
296
        self.tr = [self.tr_ratio, self.tr_ratio_next_order, self.tr_ratio_next_next_order]
1✔
297

298
        self.true_lambdas = None
1✔
299
        self.true_spectrum = None
1✔
300
        self.lambdas = None
1✔
301
        self.model = lambda x, y: np.zeros((x.size, y.size))
1✔
302
        self.psf = load_PSF(psf_type=parameters.PSF_TYPE)
1✔
303
        self.fix_psf_cube = False
1✔
304

305
        # PSF cube computation
306
        self.psf_cubes = {}
1✔
307
        self.psf_cubes_masked = {}
1✔
308
        self.boundaries = {}
1✔
309
        self.profile_params = {}
1✔
310
        self.M_sparse_indices = {}
1✔
311
        self.psf_cube_sparse_indices = {}
1✔
312
        for order in self.diffraction_orders:
1✔
313
            self.psf_cubes[order] = None
1✔
314
            self.psf_cubes_masked[order] = None
1✔
315
            self.boundaries[order] = {}
1✔
316
            self.profile_params[order] = None
1✔
317
            self.M_sparse_indices[order] = None
1✔
318
            self.psf_cube_sparse_indices[order] = None
1✔
319
        self.fix_psf_cube = False
1✔
320

321
        self.fix_atm_sim = False
1✔
322
        self.atmosphere_sim = None
1✔
323
        self.fast_sim = fast_sim
1✔
324
        self.with_background = with_background
1✔
325
        self.full_image = full_image
1✔
326
        self.with_adr = with_adr
1✔
327
        if self.full_image:
1✔
328
            self.Nx = parameters.CCD_IMSIZE
1✔
329
            self.Ny = self.spectrogram_Ny  # too long if =parameters.CCD_IMSIZE
1✔
330
            self.r0 = self.x0[0] + 1j * self.spectrogram_y0
1✔
331
        else:
332
            self.Nx = self.spectrogram_Nx
1✔
333
            self.Ny = self.spectrogram_Ny
1✔
334
            self.r0 = self.spectrogram_x0 + 1j * self.spectrogram_y0
1✔
335
        lbdas_sed = self.target.wavelengths[0]
1✔
336
        sub = np.where((lbdas_sed > parameters.LAMBDA_MIN) & (lbdas_sed < parameters.LAMBDA_MAX))
1✔
337
        self.lambdas_step = min(parameters.LAMBDA_STEP, np.min(lbdas_sed[sub]))
1✔
338
        self.yy, self.xx = np.mgrid[:self.Ny, :self.Nx]
1✔
339
        self.pixels = np.asarray([self.xx, self.yy])
1✔
340

341
    def set_true_spectrum(self, lambdas, aerosols, ozone, pwv, shift_t=0.):
1✔
342
        atmosphere = self.atmosphere.simulate(aerosols=aerosols, ozone=ozone, pwv=pwv)
1✔
343
        spectrum = self.target.sed(lambdas)
1✔
344
        spectrum *= self.disperser.transmission(lambdas - shift_t)
1✔
345
        spectrum *= self.throughput.transmission(lambdas - shift_t)
1✔
346
        spectrum *= atmosphere(lambdas)
1✔
347
        self.true_spectrum = spectrum
1✔
348
        self.true_lambdas = lambdas
1✔
349
        return spectrum
1✔
350

351
    def simulate_spectrum(self, lambdas, atmosphere, shift_t=0.):
1✔
352
        """
353
        Simulate the spectrum of the object and return the result in Target units.
354

355
        Parameters
356
        ----------
357
        lambdas: array_like
358
            The wavelength array in nm.
359
        atmosphere: callable
360
            A callable function of the atmospheric transmission.
361
        shift_t: float
362
            Shift of the transmission in nm (default: 0).
363

364
        Returns
365
        -------
366
        spectrum: array_like
367
            The spectrum array in ADU/s units.
368
        spectrum_err: array_like
369
            The spectrum uncertainty array in ADU/s units.
370

371
        """
372
        spectrum = np.zeros_like(lambdas)
1✔
373
        telescope_transmission = self.throughput.transmission(lambdas - shift_t)
1✔
374
        if self.fast_sim:
1✔
375
            spectrum = self.target.sed(lambdas)
1✔
376
            spectrum *= self.disperser.transmission(lambdas - shift_t)
1✔
377
            spectrum *= telescope_transmission
1✔
378
            spectrum *= atmosphere(lambdas)
1✔
379
            spectrum *= parameters.FLAM_TO_ADURATE * lambdas * np.gradient(lambdas)
1✔
380
        else:
381
            def integrand(lbda):
1✔
382
                return lbda * self.target.sed(lbda) * self.throughput.transmission(lbda - shift_t) \
1✔
383
                       * self.disperser.transmission(lbda - shift_t) * atmosphere(lbda)
384

385
            for i in range(len(lambdas) - 1):
1✔
386
                # spectrum[i] = parameters.FLAM_TO_ADURATE * quad(integrand, lambdas[i], lambdas[i + 1])[0]
387
                lbdas = np.arange(lambdas[i], lambdas[i + 1], self.lambdas_step)
1✔
388
                spectrum[i] = parameters.FLAM_TO_ADURATE * np.mean(integrand(lbdas)) * (lambdas[i + 1] - lambdas[i])
1✔
389
            spectrum[-1] = spectrum[-2]
1✔
390
        spectrum_err = np.zeros_like(spectrum)
1✔
391
        idx = telescope_transmission > 0
1✔
392
        spectrum_err[idx] = self.throughput.transmission_err(lambdas)[idx] / telescope_transmission[idx] * spectrum[idx]
1✔
393
        # idx = telescope_transmission <= 0: not ready yet to be implemented
394
        # spectrum_err[idx] = 1e6 * np.max(spectrum_err)
395
        return spectrum, spectrum_err
1✔
396

397
    def simulate(self, A1=1.0, A2=0., A3=0., aerosols=0.05, angstrom_exponent=None, ozone=300, pwv=5,
1✔
398
                 D=parameters.DISTANCE2CCD, shift_x=0., shift_y=0., angle=0., B=1., psf_poly_params=None):
399
        """
400

401
        Parameters
402
        ----------
403
        A1: float
404
            Global amplitude of the spectrum (default: 1).
405
        A2: float
406
            Relative amplitude of the order 2 spectrum contamination (default: 0).
407
        A3: float
408
            Relative amplitude of the order 3 spectrum contamination (default: 0).
409
        aerosols: float
410
            VAOD Vertical Aerosols Optical Depth.
411
        angstrom_exponent: float, optional
412
            Angstrom exponent for aerosols. If negative or None, default aerosol model from Libradtran is used.
413
            If value is 0.0192, the atmospheric transmission is very close to the case with angstrom_exponent=None (default: None).
414
        ozone: float
415
            Ozone quantity in Dobson.
416
        pwv: float
417
            Precipitable Water Vapor quantity in mm.
418
        D: float
419
            Distance between the CCD and the disperser in mm (default: parameters.DISTANCE2CCD)
420
        shift_x: float
421
            Shift in pixels along x axis of the order 0 position estimate (default: 0).
422
        shift_y: float
423
            Shift in pixels along y axis of the order 0 position estimate (default: 0).
424
        angle: float
425
            Angle of the dispersion axis in degree (default: 0).
426
        B: float
427
            Amplitude level for the background (default: 0).
428
        psf_poly_params: array_like
429
            Polynomial parameters describing the PSF dependence in wavelength (default: None).
430

431
        Returns
432
        -------
433
        lambdas: array_like
434
            The wavelength array in nm used for the interpolation.
435
        spectrogram: array_like
436
            The spectrogram array in ADU/s units.
437
        spectrogram_err: array_like
438
            The spectrogram uncertainty array in ADU/s units.
439

440
        Example
441
        -------
442
        >>> spec = Spectrum("./tests/data/reduc_20170530_134_spectrum.fits")
443
        >>> spec.disperser.ratio_ratio_order_3over2 = lambda lbda: 0.1
444
        >>> psf_poly_params = list(spec.chromatic_psf.from_table_to_poly_params()) * 3
445
        >>> atmosphere = Atmosphere(airmass=1.2, pressure=800, temperature=10)
446
        >>> sim = SpectrogramModel(spec, atmosphere=atmosphere, with_background=True, fast_sim=True)
447
        >>> lambdas, model, model_err = sim.simulate(A2=1, angle=-1.5, psf_poly_params=psf_poly_params)
448
        >>> sim.plot_spectrogram()
449

450
        .. doctest::
451
            :hide:
452

453
            >>> assert np.sum(lambdas) > 0
454
            >>> assert np.sum(model) > 20
455
        """
456
        poly_params = np.array(psf_poly_params).reshape((len(self.diffraction_orders), -1))
1✔
457
        self.rotation_angle = angle
1✔
458
        self.lambdas = self.compute_lambdas_in_spectrogram(D, shift_x, shift_y, angle, niter=5, with_adr=True,
1✔
459
                                                           order=self.diffraction_orders[0])
460
        self.lambdas_binwidths = np.gradient(self.lambdas)
1✔
461
        if self.atmosphere_sim is None or not self.fix_atm_sim:
1✔
462
            self.atmosphere.set_lambda_range(self.lambdas)
1✔
463
            self.atmosphere_sim = self.atmosphere.simulate(aerosols=aerosols, ozone=ozone, pwv=pwv,
1✔
464
                                                           angstrom_exponent=angstrom_exponent)
465
        spectrum, spectrum_err = self.simulate_spectrum(self.lambdas, self.atmosphere_sim)
1✔
466

467
        As = [1, A2, A3]
1✔
468
        ima = np.zeros((self.Ny, self.Nx), dtype="float32")
1✔
469
        ima_err2 = np.zeros((self.Ny, self.Nx), dtype="float32")
1✔
470
        for k, order in enumerate(self.diffraction_orders):
1✔
471
            if self.tr[k] is None or As[k] == 0:  # diffraction order undefined
1✔
472
                continue
1✔
473
            # Dispersion law
474
            dispersion_law = self.compute_dispersion_in_spectrogram(self.lambdas, shift_x, shift_y, angle,
1✔
475
                                                                    niter=5, with_adr=True, order=order)
476

477
            # Spectrum amplitude is in ADU/s
478
            spec = As[k] * self.tr[k](self.lambdas) * spectrum
1✔
479
            spec_err = As[k] * self.tr[k](self.lambdas) * spectrum_err
1✔
480
            if np.any(np.isnan(spec)):
1✔
481
                spec[np.isnan(spec)] = 0.
×
482

483
            # Evaluate PSF profile
484
            if self.profile_params[order] is None or not self.fix_psf_cube:
1✔
485
                if k==0:
1✔
486
                    self.profile_params[order] = self.chromatic_psf.update(poly_params[k], x0=self.r0.real + shift_x,
1✔
487
                                                                           y0=self.r0.imag + shift_y, angle=angle, plot=False, apply_bounds=True)
488
                else:
489
                    self.profile_params[order] = self.chromatic_psf.from_poly_params_to_profile_params(poly_params[k], apply_bounds=True)
1✔
490
                self.profile_params[order][:, 0] = 1
1✔
491
                self.profile_params[order][:, 1] = dispersion_law.real + self.r0.real
1✔
492
                self.profile_params[order][:, 2] += dispersion_law.imag
1✔
493
            if k == 0:
1✔
494
                self.chromatic_psf.table["Dx"] = self.profile_params[order][:, 1] - self.r0.real
1✔
495
                self.chromatic_psf.table["Dy"] = self.profile_params[order][:, 2] - self.r0.imag
1✔
496

497
            # Fill the PSF cube for each diffraction order
498
            argmin = max(0, int(np.argmin(np.abs(self.profile_params[order][:, 1]))))
1✔
499
            argmax = min(self.Nx, np.argmin(np.abs(self.profile_params[order][:, 1]-self.Nx)) + 1)
1✔
500
            if self.psf_cubes[order] is None or not self.fix_psf_cube:
1✔
501
                self.psf_cubes[order] = self.chromatic_psf.build_psf_cube(self.pixels, self.profile_params[order],
1✔
502
                                                                          fwhmx_clip=3 * parameters.PSF_FWHM_CLIP,
503
                                                                          fwhmy_clip=parameters.PSF_FWHM_CLIP,
504
                                                                          dtype="float32",
505
                                                                          mask=self.psf_cubes_masked[order],
506
                                                                          boundaries=self.boundaries[order])
507

508
            # Assemble all diffraction orders in simulation
509
            for x in range(argmin, argmax):
1✔
510
                if self.boundaries[order]:
1✔
511
                    xmin = self.boundaries[order]["xmin"][x]
1✔
512
                    xmax = self.boundaries[order]["xmax"][x]
1✔
513
                    ymin = self.boundaries[order]["ymin"][x]
1✔
514
                    ymax = self.boundaries[order]["ymax"][x]
1✔
515
                else:
516
                    xmin, xmax = 0, self.Nx
1✔
517
                    ymin, ymax = 0, self.Ny
1✔
518
                ima[ymin:ymax, xmin:xmax] += spec[x] * self.psf_cubes[order][x, ymin:ymax, xmin:xmax]
1✔
519
                ima_err2[ymin:ymax, xmin:xmax] += (spec_err[x] * self.psf_cubes[order][x, ymin:ymax, xmin:xmax])**2
1✔
520
            if np.allclose(self.profile_params[order][:, 0] , 1):
1✔
521
                self.profile_params[order][:, 0] = spec
1✔
522

523
        # self.spectrogram is in ADU/s units here
524
        self.spectrogram = A1 * ima
1✔
525
        self.spectrogram_err = A1 * np.sqrt(ima_err2)
1✔
526

527
        if self.with_background:
1✔
528
            self.spectrogram += B * self.spectrogram_bgd
1✔
529
        # Save the simulation parameters
530
        self.psf_poly_params = np.copy(poly_params[0])
1✔
531
        self.header['OZONE_T'] = ozone
1✔
532
        self.header['PWV_T'] = pwv
1✔
533
        self.header['VAOD_T'] = aerosols
1✔
534
        self.header['A1_T'] = A1
1✔
535
        self.header['A2_T'] = A2
1✔
536
        self.header['A3_T'] = A3
1✔
537
        self.header['D2CCD_T'] = D
1✔
538
        self.header['X0_T'] = shift_x
1✔
539
        self.header['Y0_T'] = shift_y
1✔
540
        self.header['ROTANGLE'] = angle
1✔
541

542
        return self.lambdas, self.spectrogram, self.spectrogram_err
1✔
543

544

545
if __name__ == "__main__":
1✔
UNCOV
546
    import doctest
×
547

UNCOV
548
    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