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

LSSTDESC / Spectractor / 4890731301

pending completion
4890731301

Pull #125

github

GitHub
Merge 79e1b14f6 into 3549ae5c3
Pull Request #125: Fitparams

892 of 892 new or added lines in 16 files covered. (100.0%)

7127 of 7963 relevant lines covered (89.5%)

0.9 hits per line

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

85.54
/spectractor/extractor/extractor.py
1
import os
1✔
2
import copy
1✔
3
import numpy as np
1✔
4
import matplotlib.pyplot as plt
1✔
5
from matplotlib import cm
1✔
6
from scipy import sparse
1✔
7
from scipy.signal import convolve2d
1✔
8
import time
1✔
9

10
from spectractor import parameters
1✔
11
from spectractor.config import set_logger, load_config
1✔
12
from spectractor.extractor.images import Image, find_target, turn_image
1✔
13
from spectractor.extractor.spectrum import Spectrum, calibrate_spectrum
1✔
14
from spectractor.extractor.background import extract_spectrogram_background_sextractor
1✔
15
from spectractor.extractor.chromaticpsf import ChromaticPSF
1✔
16
from spectractor.extractor.psf import load_PSF
1✔
17
from spectractor.tools import ensure_dir, plot_image_simple, from_lambda_to_colormap, plot_spectrum_simple
1✔
18
from spectractor.fit.fitter import (run_minimisation, run_minimisation_sigma_clipping, write_fitparameter_json,
1✔
19
                                    RegFitWorkspace, FitWorkspace, FitParameters)
20

21

22
def dumpParameters():
1✔
23
    for item in dir(parameters):
×
24
        if not item.startswith("__"):
×
25
            print(item, getattr(parameters, item))
×
26

27

28
class FullForwardModelFitWorkspace(FitWorkspace):
1✔
29

30
    def __init__(self, spectrum, amplitude_priors_method="noprior", verbose=False, plot=False, live_fit=False, truth=None):
1✔
31
        """Class to fit a full forward model on data to extract a spectrum, with ADR prediction and order 2 subtraction.
32

33
        Parameters
34
        ----------
35
        spectrum: Spectrum
36
        amplitude_priors_method
37
        verbose
38
        plot
39
        live_fit
40
        truth
41

42
        Examples
43
        --------
44
        >>> spec = Spectrum("./tests/data/sim_20170530_134_spectrum.fits")
45
        >>> w = FullForwardModelFitWorkspace(spectrum=spec, amplitude_priors_method="spectrum")
46
        """
47
        self.my_logger = set_logger(self.__class__.__name__)
1✔
48
        # prepare parameters to fit
49
        length = len(spectrum.chromatic_psf.table)
1✔
50
        self.psf_poly_params = np.copy(spectrum.chromatic_psf.from_table_to_poly_params())
1✔
51
        self.psf_profile_params = np.copy(spectrum.chromatic_psf.from_table_to_profile_params())
1✔
52
        self.psf_poly_params = self.psf_poly_params[length:]
1✔
53
        psf_poly_params_labels = np.copy(spectrum.chromatic_psf.poly_params_labels[length:])
1✔
54
        psf_poly_params_names = np.copy(spectrum.chromatic_psf.poly_params_names[length:])
1✔
55
        spectrum.chromatic_psf.psf.apply_max_width_to_bounds(max_half_width=spectrum.spectrogram_Ny)
1✔
56
        psf_poly_params_bounds = spectrum.chromatic_psf.set_bounds()
1✔
57
        p = np.array([1, np.copy(spectrum.header['D2CCD']), np.copy(spectrum.header['PIXSHIFT']), 0,
1✔
58
                      np.copy(spectrum.rotation_angle), 1, parameters.OBS_CAMERA_ROTATION,
59
                      np.copy(spectrum.pressure),  np.copy(spectrum.temperature),  np.copy(spectrum.airmass)])
60
        self.psf_params_start_index = p.size
1✔
61
        self.psf_params_index = np.arange(0, self.psf_params_start_index+len(self.psf_poly_params))
1✔
62
        self.psf_params_index_next_order = np.concatenate([np.arange(0, self.psf_params_start_index), np.arange(np.max(self.psf_params_index) + 1, len(p))])
1✔
63
        self.psf_params_start_index_next_order = np.max(self.psf_params_index) + 1
1✔
64
        self.saturation = spectrum.spectrogram_saturation
1✔
65
        p = np.concatenate([p, self.psf_poly_params, self.psf_poly_params])
1✔
66
        input_labels = ["A2", r"D_CCD [mm]", r"shift_x [pix]", r"shift_y [pix]", r"angle [deg]", "B", "R", "P [hPa]",
1✔
67
                        "T [Celsius]", "z"] + list(psf_poly_params_labels) + [label+"_2" for label in psf_poly_params_labels]
68
        axis_names = ["$A_2$", r"$D_{CCD}$ [mm]", r"$\delta_{\mathrm{x}}^{(\mathrm{fit})}$ [pix]",
1✔
69
                      r"$\delta_{\mathrm{y}}^{(\mathrm{fit})}$ [pix]", r"$\alpha$ [deg]", "$B$", "R",
70
                      r"$P_{\mathrm{atm}}$ [hPa]", r"$T_{\mathrm{atm}}$ [Celcius]", "$z$"] + \
71
                     list(psf_poly_params_names) + [label+r"$\!_2$" for label in psf_poly_params_names]
72
        bounds = [[0, 2 / parameters.GRATING_ORDER_2OVER1],
1✔
73
                  [p[1] - 3 * parameters.DISTANCE2CCD_ERR, p[1] + 3 * parameters.DISTANCE2CCD_ERR],
74
                  [-parameters.PIXSHIFT_PRIOR, parameters.PIXSHIFT_PRIOR],
75
                  [-10 * parameters.PIXSHIFT_PRIOR, 10 * parameters.PIXSHIFT_PRIOR],
76
                  [-90, 90], [0.2, 5], [-360, 360], [300, 1100], [-100, 100], [1.001, 3]] + list(psf_poly_params_bounds) * 2
77
        fixed = [False] * p.size
1✔
78
        for k, par in enumerate(input_labels):
1✔
79
            if "x_c" in par or "saturation" in par:  # or "y_c" in par:
1✔
80
                fixed[k] = True
1✔
81
        for k, par in enumerate(input_labels):
1✔
82
            if "y_c" in par:
1✔
83
                fixed[k] = False
1✔
84
                p[k] = 0
1✔
85
        # self.set_y_c()
86
        # This set of fixed parameters was determined so that the reconstructed spectrum has a ZERO bias
87
        # with respect to the true spectrum injected in the simulation
88
        # A2 is free only if spectrogram is a simulation or if the order 2/1 ratio is not known and flat
89
        fixed[0] = (not spectrum.disperser.flat_ratio_order_2over1) and (not ("A2_T" in spectrum.header))
1✔
90
        fixed[1] = True  # D2CCD: spectrogram can not tell something on this parameter: rely on calibrate_spectrum
1✔
91
        fixed[2] = True  # delta x: if False, extracted spectrum is biased compared with truth
1✔
92
        fixed[3] = True  # delta y
1✔
93
        fixed[4] = True  # angle
1✔
94
        fixed[5] = True  # B: not needed in simulations, to check with data
1✔
95
        fixed[6] = True  # camera rot
1✔
96
        fixed[7] = True  # pressure
1✔
97
        fixed[8] = True  # temperature
1✔
98
        fixed[9] = True  # airmass
1✔
99
        fixed[-1] = True  # saturation
1✔
100

101
        params = FitParameters(p, input_labels=input_labels, axis_names=axis_names, fixed=fixed, bounds=bounds,
1✔
102
                               truth=truth, filename=spectrum.filename)
103
        FitWorkspace.__init__(self, params, spectrum.filename, verbose, plot, live_fit, truth=truth)
1✔
104
        self.spectrum = spectrum
1✔
105

106
        # crop data to fit faster
107
        self.lambdas = self.spectrum.lambdas
1✔
108
        self.bgd_width = parameters.PIXWIDTH_BACKGROUND + parameters.PIXDIST_BACKGROUND - parameters.PIXWIDTH_SIGNAL
1✔
109
        self.data = spectrum.spectrogram[self.bgd_width:-self.bgd_width, :]
1✔
110
        self.err = spectrum.spectrogram_err[self.bgd_width:-self.bgd_width, :]
1✔
111
        self.bgd = spectrum.spectrogram_bgd[self.bgd_width:-self.bgd_width, :]
1✔
112
        self.bgd_flat = self.bgd.flatten()
1✔
113
        self.Ny, self.Nx = self.data.shape
1✔
114
        yy, xx = np.mgrid[:self.Ny, :self.Nx]
1✔
115
        self.pixels = np.asarray([xx, yy])
1✔
116

117
        # adapt the ChromaticPSF table shape
118
        if self.Nx != self.spectrum.chromatic_psf.Nx:
1✔
119
            self.spectrum.chromatic_psf.resize_table(new_Nx=self.Nx)
×
120

121
        # load the disperser relative transmissions
122
        if abs(self.spectrum.order) == 1:
1✔
123
            self.tr_ratio_next_order = self.spectrum.disperser.ratio_order_2over1
1✔
124
            self.tr_ratio_next_next_order = self.spectrum.disperser.ratio_order_3over2
1✔
125
        elif abs(self.spectrum.order) == 2:
×
126
            self.tr_ratio_next_order = self.spectrum.disperser.ratio_order_3over2
×
127
            self.tr_ratio_next_next_order = None
×
128
        elif abs(self.spectrum.order) == 3:
×
129
            self.tr_ratio_next_order = None
×
130
            self.tr_ratio_next_next_order = None
×
131
        else:
132
            raise ValueError(f"{abs(self.spectrum.order)=}: must be 1, 2 or 3. "
×
133
                             f"Higher diffraction orders not implemented yet in full forward model.")
134

135
        # PSF cube computation
136
        self.psf_cube_masked = None
1✔
137
        self.psf_cube = None
1✔
138
        self.psf_cube_next_order = None
1✔
139
        self.fix_psf_cube = False
1✔
140
        self.fix_psf_cube_next_order = False
1✔
141

142
        # prepare the background, data and errors
143
        self.bgd_std = float(np.std(np.random.poisson(np.abs(self.bgd))))
1✔
144

145
        # error matrix
146
        # here image uncertainties are assumed to be uncorrelated
147
        # (which is not exactly true in rotated images)
148
        self.W = 1. / (self.err * self.err)
1✔
149
        self.W = self.W.flatten()
1✔
150

151
        # flat data for fitworkspace
152
        self.data = self.data.flatten() - self.bgd_flat
1✔
153
        self.err = self.err.flatten()
1✔
154
        self.data_before_mask = np.copy(self.data)
1✔
155
        self.W_before_mask = np.copy(self.W)
1✔
156

157
        # create mask
158
        self.sqrtW = np.sqrt(sparse.diags(self.W))
1✔
159
        self.sparse_indices = None
1✔
160
        self.set_mask(fwhmx_clip=3*parameters.PSF_FWHM_CLIP, fwhmy_clip=2*parameters.PSF_FWHM_CLIP)  # not a narrow mask for first fit
1✔
161

162
        # design matrix
163
        self.M = np.zeros((self.Nx, self.data.size))
1✔
164
        self.M_dot_W_dot_M = np.zeros((self.Nx, self.Nx))
1✔
165

166
        # prepare results
167
        self.amplitude_params = np.zeros(self.Nx)
1✔
168
        self.amplitude_params_err = np.zeros(self.Nx)
1✔
169
        self.amplitude_cov_matrix = np.zeros((self.Nx, self.Nx))
1✔
170

171
        # priors on amplitude parameters
172
        self.amplitude_priors_list = ['noprior', 'positive', 'smooth', 'spectrum', 'fixed']
1✔
173
        self.amplitude_priors_method = amplitude_priors_method
1✔
174
        self.fwhm_priors = np.copy(spectrum.chromatic_psf.table['fwhm'])
1✔
175
        self.reg = spectrum.chromatic_psf.opt_reg
1✔
176
        if 'PSF_REG' in spectrum.header and float(spectrum.header["PSF_REG"]) > 0:
1✔
177
            self.reg = float(spectrum.header['PSF_REG'])
1✔
178
        if self.reg < 0:
1✔
179
            self.reg = parameters.PSF_FIT_REG_PARAM
×
180
        self.my_logger.info(f"\n\tFull forward model fitting with regularisation parameter r={self.reg}.")
1✔
181
        self.Q = np.zeros((self.Nx, self.Nx))
1✔
182
        self.Q_dot_A0 = np.zeros(self.Nx)
1✔
183
        if amplitude_priors_method not in self.amplitude_priors_list:
1✔
184
            raise ValueError(f"Unknown prior method for the amplitude fitting: {self.amplitude_priors_method}. "
×
185
                             f"Must be either {self.amplitude_priors_list}.")
186
        self.spectrum.convert_from_flam_to_ADUrate()
1✔
187
        self.amplitude_priors = np.copy(self.spectrum.data)
1✔
188
        if self.amplitude_priors_method == "spectrum":
1✔
189
            self.amplitude_priors_cov_matrix = np.copy(self.spectrum.cov_matrix)
1✔
190
        if self.spectrum.data.size != self.Nx:  # must rebin the priors
1✔
191
            old_x = np.linspace(0, 1, self.spectrum.data.size)
×
192
            new_x = np.linspace(0, 1, self.Nx)
×
193
            self.spectrum.lambdas = np.interp(new_x, old_x, self.spectrum.lambdas)
×
194
            self.amplitude_priors = np.interp(new_x, old_x, self.amplitude_priors)
×
195
            if self.amplitude_priors_method == "spectrum":
×
196
                # rebin prior cov matrix with monte-carlo
197
                niter = 10000
×
198
                samples = np.random.multivariate_normal(np.zeros_like(old_x), cov=self.amplitude_priors_cov_matrix, size=niter)
×
199
                new_samples = np.zeros((niter, new_x.size))
×
200
                for i in range(niter):
×
201
                    new_samples[i] = np.interp(new_x, old_x, samples[i])
×
202
                self.amplitude_priors_cov_matrix = np.cov(new_samples.T)
×
203
                # self.amplitude_priors_cov_matrix[np.abs(self.amplitude_priors_cov_matrix) < 1e-3 * np.max(self.amplitude_priors_cov_matrix)] = 0
204
        # regularisation matrices
205
        if amplitude_priors_method == "spectrum":
1✔
206
            # U = np.diag([1 / np.sqrt(np.sum(self.err[:, x]**2)) for x in range(self.Nx)])
207
            # self.U = np.diag([1 / np.sqrt(self.amplitude_priors_cov_matrix[x, x]) for x in range(self.Nx)])
208
            self.U = np.linalg.inv(self.amplitude_priors_cov_matrix)
1✔
209
            L = np.diag(-2 * np.ones(self.Nx)) + np.diag(np.ones(self.Nx), -1)[:-1, :-1] \
1✔
210
                + np.diag(np.ones(self.Nx), 1)[:-1, :-1]
211
            L.astype(float)
1✔
212
            L[0, 0] = -1
1✔
213
            L[-1, -1] = -1
1✔
214
            self.L = L
1✔
215
            self.Q = L.T @ np.linalg.inv(self.amplitude_priors_cov_matrix) @ L
1✔
216
            # self.Q = L.T @ U.T @ U @ L
217
            self.Q_dot_A0 = self.Q @ self.amplitude_priors
1✔
218

219
    def set_y_c(self):
1✔
220
        for k, par in enumerate(self.params.input_labels):
×
221
            if "y_c" in par:
×
222
                if par == "y_c_0":
×
223
                    self.params.p[k] = self.params.p[3]
×
224
                elif par == "y_c_1":
×
225
                    self.params.p[k] = np.tan(self.params.p[4] * np.pi / 180)
×
226
                else:
227
                    self.params.p[k] = 0
×
228

229
    def set_mask(self, params=None, fwhmx_clip=3*parameters.PSF_FWHM_CLIP, fwhmy_clip=parameters.PSF_FWHM_CLIP):
1✔
230
        """
231

232
        Parameters
233
        ----------
234
        params
235
        fwhmx_clip
236
        fwhmy_clip
237

238
        Returns
239
        -------
240

241
        Examples
242
        --------
243
        >>> spec = Spectrum("./tests/data/reduc_20170530_134_spectrum.fits")
244
        >>> w = FullForwardModelFitWorkspace(spectrum=spec, amplitude_priors_method="fixed", verbose=True)
245
        >>> _ = w.simulate(*w.params.p)
246
        >>> w.plot_fit()
247

248
        """
249
        self.my_logger.info("\n\tReset spectrogram mask with current parameters.")
1✔
250
        if params is None:
1✔
251
            params = self.params.p
1✔
252
        A2, D2CCD, dx0, dy0, angle, B, rot, pressure, temperature, airmass, *psf_poly_params = params
1✔
253
        if len(psf_poly_params) % 2 != 0:
1✔
254
            raise ValueError(f"Argument psf_poly_params must be even size, to be split in parameters"
×
255
                             f"for first order and next order spectrograms. Got {len(psf_poly_params)=}.")
256
        poly_params = params[self.psf_params_start_index:self.psf_params_start_index_next_order]
1✔
257
        poly_params_next_order = params[self.psf_params_start_index_next_order:]
1✔
258
        profile_params = self.spectrum.chromatic_psf.from_poly_params_to_profile_params(poly_params,
1✔
259
                                                                                        apply_bounds=True)
260
        profile_params_next_order = self.spectrum.chromatic_psf.from_poly_params_to_profile_params(poly_params_next_order,
1✔
261
                                                                                                   apply_bounds=True)
262
        self.spectrum.chromatic_psf.from_profile_params_to_shape_params(profile_params)
1✔
263
        lambdas = self.spectrum.compute_lambdas_in_spectrogram(D2CCD, dx0, dy0, angle, niter=5, with_adr=True,
1✔
264
                                                               order=self.spectrum.order)
265
        dispersion_law = self.spectrum.compute_dispersion_in_spectrogram(lambdas, dx0, dy0, angle,
1✔
266
                                                                         niter=5, with_adr=True,
267
                                                                         order=self.spectrum.order)
268
        dispersion_law_next_order = self.spectrum.compute_dispersion_in_spectrogram(lambdas, dx0, dy0, angle, niter=5, with_adr=True,
1✔
269
                                                                                order=self.spectrum.order+np.sign(self.spectrum.order))
270
        profile_params[:, 0] = 1
1✔
271
        profile_params[:, 1] = dispersion_law.real + self.spectrum.spectrogram_x0
1✔
272
        profile_params[:, 2] += dispersion_law.imag - self.bgd_width
1✔
273
        psf_cube = self.spectrum.chromatic_psf.build_psf_cube(self.pixels, profile_params,
1✔
274
                                                              fwhmx_clip=fwhmx_clip,
275
                                                              fwhmy_clip=fwhmy_clip, dtype="float32")
276
        profile_params_next_order[:, 0] = 1
1✔
277
        profile_params_next_order[:, 1] = dispersion_law_next_order.real + self.spectrum.spectrogram_x0
1✔
278
        profile_params_next_order[:, 2] += dispersion_law_next_order.imag - self.bgd_width
1✔
279
        psf_cube_next_order = self.spectrum.chromatic_psf.build_psf_cube(self.pixels, profile_params_next_order,
1✔
280
                                                                     fwhmx_clip=fwhmx_clip,
281
                                                                     fwhmy_clip=fwhmy_clip, dtype="float32")
282

283
        self.psf_cube_masked = psf_cube > 0
1✔
284
        self.psf_cube_masked_next_order = psf_cube_next_order > 0
1✔
285
        flat_spectrogram = np.sum(self.psf_cube_masked.reshape(len(profile_params), self.pixels[0].size), axis=0)
1✔
286
        mask = flat_spectrogram == 0  # < 1e-2 * np.max(flat_spectrogram)
1✔
287
        mask = mask.reshape(self.pixels[0].shape)
1✔
288
        kernel = np.ones((3, self.spectrum.spectrogram_Nx//10))  # enlarge a bit more the edges of the mask
1✔
289
        mask = convolve2d(mask, kernel, 'same').astype(bool)
1✔
290
        for k in range(self.psf_cube_masked.shape[0]):
1✔
291
            self.psf_cube_masked[k] *= ~mask
1✔
292
            self.psf_cube_masked_next_order[k] *= ~mask
1✔
293
        mask = mask.reshape((self.pixels[0].size,))
1✔
294
        self.W = np.copy(self.W_before_mask)
1✔
295
        self.W[mask] = 0
1✔
296
        self.sqrtW = np.sqrt(sparse.diags(self.W))
1✔
297
        self.sparse_indices = None
1✔
298
        self.mask = list(np.where(mask)[0])
1✔
299

300
    def simulate(self, *params):
1✔
301
        r"""
302
        Compute a ChromaticPSF2D model given PSF shape parameters and minimizing
303
        amplitude parameters using a spectrogram data array.
304

305
        The ChromaticPSF2D model :math:`\vec{m}(\vec{x},\vec{p})` can be written as
306

307
        .. math ::
308
            :label: chromaticpsf2d
309

310
            \vec{m}(\vec{x},\vec{p}) = \sum_{i=0}^{N_x} A_i \phi\left(\vec{x},\vec{p}_i\right)
311

312
        with :math:`\vec{x}` the 2D array  of the pixel coordinates, :math:`\vec{A}` the amplitude parameter array
313
        along the x axis of the spectrogram, :math:`\phi\left(\vec{x},\vec{p}_i\right)` the 2D PSF kernel whose integral
314
        is normalised to one parametrized with the :math:`\vec{p}_i` non-linear parameter array. If the :math:`\vec{x}`
315
        2D array is flatten in 1D, equation :eq:`chromaticpsf2d` is
316

317
        .. math ::
318
            :label: chromaticpsf2d_matrix
319
            :nowrap:
320

321
            \begin{align}
322
            \vec{m}(\vec{x},\vec{p}) & = \mathbf{M}\left(\vec{x},\vec{p}\right) \mathbf{A} \\
323

324
            \mathbf{M}\left(\vec{x},\vec{p}\right) & = \left(\begin{array}{cccc}
325
             \phi\left(\vec{x}_1,\vec{p}_1\right) & \phi\left(\vec{x}_2,\vec{p}_1\right) & ...
326
             & \phi\left(\vec{x}_{N_x},\vec{p}_1\right) \\
327
             ... & ... & ... & ...\\
328
             \phi\left(\vec{x}_1,\vec{p}_{N_x}\right) & \phi\left(\vec{x}_2,\vec{p}_{N_x}\right) & ...
329
             & \phi\left(\vec{x}_{N_x},\vec{p}_{N_x}\right) \\
330
            \end{array}\right)
331
            \end{align}
332

333

334
        with :math:`\mathbf{M}` the design matrix.
335

336
        The goal of this function is to perform a minimisation of the amplitude vector :math:`\mathbf{A}` given
337
        a set of non-linear parameters :math:`\mathbf{p}` and a spectrogram data array :math:`mathbf{y}` modelise as
338

339
        .. math:: \mathbf{y} = \mathbf{m}(\vec{x},\vec{p}) + \vec{\epsilon}
340

341
        with :math:`\vec{\epsilon}` a random noise vector. The :math:`\chi^2` function to minimise is
342

343
        .. math::
344
            :label: chromaticspsf2d_chi2
345

346
            \chi^2(\mathbf{A})= \left(\mathbf{y} - \mathbf{M}\left(\vec{x},\vec{p}\right) \mathbf{A}\right)^T \mathbf{W}
347
            \left(\mathbf{y} - \mathbf{M}\left(\vec{x},\vec{p}\right) \mathbf{A} \right)
348

349

350
        with :math:`\mathbf{W}` the weight matrix, inverse of the covariance matrix. In our case this matrix is diagonal
351
        as the pixels are considered all independent. The minimum of equation :eq:`chromaticspsf2d_chi2` is reached for
352
        a the set of amplitude parameters :math:`\hat{\mathbf{A}}` given by
353

354
        .. math::
355

356
            \hat{\mathbf{A}} =  (\mathbf{M}^T \mathbf{W} \mathbf{M})^{-1} \mathbf{M}^T \mathbf{W} \mathbf{y}
357

358
        The error matrix on the :math:`\hat{\mathbf{A}}` coefficient is simply
359
        :math:`(\mathbf{M}^T \mathbf{W} \mathbf{M})^{-1}`.
360

361
        See Also
362
        --------
363
        ChromaticPSF2DFitWorkspace.simulate
364

365
        Parameters
366
        ----------
367
        params: array_like
368
            Full forward model parameter array.
369

370
        Examples
371
        --------
372

373
        Load data:
374

375
        >>> spec = Spectrum("./tests/data/sim_20170530_134_spectrum.fits")
376

377
        Simulate the data with fixed amplitude priors:
378

379
        .. doctest::
380

381
            >>> w = FullForwardModelFitWorkspace(spectrum=spec, amplitude_priors_method="fixed", verbose=True)
382
            >>> y, mod, mod_err = w.simulate(*w.params.p)
383
            >>> w.plot_fit()
384

385
        .. doctest::
386
            :hide:
387

388
            >>> assert mod is not None
389

390
        Simulate the data with a Tikhonov prior on amplitude parameters:
391

392
        .. doctest::
393

394
            >>> spec = Spectrum("./tests/data/sim_20170530_134_spectrum.fits")
395
            >>> w = FullForwardModelFitWorkspace(spectrum=spec, amplitude_priors_method="spectrum", verbose=True)
396
            >>> y, mod, mod_err = w.simulate(*w.params.p)
397
            >>> w.plot_fit()
398

399
        """
400
        # linear regression for the amplitude parameters
401
        # prepare the vectors
402
        self.params.p = np.asarray(params)
1✔
403
        A2, D2CCD, dx0, dy0, angle, B, rot, pressure, temperature, airmass, *poly_params_all = params
1✔
404
        poly_params = params[self.psf_params_start_index:self.psf_params_start_index_next_order]
1✔
405
        poly_params_next_order = params[self.psf_params_start_index_next_order:]
1✔
406
        self.spectrum.adr_params[2] = temperature
1✔
407
        self.spectrum.adr_params[3] = pressure
1✔
408
        self.spectrum.adr_params[-1] = airmass
1✔
409

410
        parameters.OBS_CAMERA_ROTATION = rot
1✔
411
        W_dot_data = self.W * (self.data + (1 - B) * self.bgd_flat)
1✔
412

413
        # Evaluate PSF profile
414
        profile_params = self.spectrum.chromatic_psf.update(poly_params, self.spectrum.spectrogram_x0 + dx0,
1✔
415
                                                            self.spectrum.spectrogram_y0 + dy0, angle, plot=False)
416

417
        # Evaluate ADR and compute wavelength arrays
418
        self.lambdas = self.spectrum.compute_lambdas_in_spectrogram(D2CCD, dx0, dy0, angle, niter=5, with_adr=True,
1✔
419
                                                                    order=self.spectrum.order)
420
        dispersion_law = self.spectrum.compute_dispersion_in_spectrogram(self.lambdas, dx0, dy0, angle,
1✔
421
                                                                         niter=5, with_adr=True,
422
                                                                         order=self.spectrum.order)
423
        # Fill spectrogram trace as a function of the pixel column x
424
        profile_params[:, 0] = 1
1✔
425
        profile_params[:, 1] = dispersion_law.real + self.spectrum.spectrogram_x0
1✔
426
        profile_params[:, 2] += dispersion_law.imag - self.bgd_width
1✔
427

428
        # Matrix filling
429
        # if self.psf_cube is None or not self.fix_psf_cube:  # slower
430
        psf_cube = self.spectrum.chromatic_psf.build_psf_cube(self.pixels, profile_params,
1✔
431
                                                              fwhmx_clip=3 * parameters.PSF_FWHM_CLIP,
432
                                                              fwhmy_clip=parameters.PSF_FWHM_CLIP, dtype="float32",
433
                                                              mask=self.psf_cube_masked)
434
        if A2 > 0 and self.tr_ratio_next_order is not None:
1✔
435
            dispersion_law_next_order = self.spectrum.compute_dispersion_in_spectrogram(self.lambdas, dx0, dy0, angle,
1✔
436
                                                                                        niter=5, with_adr=True,
437
                                                                                        order=self.spectrum.order + np.sign(self.spectrum.order))
438
            # Prepare order 2 profile params indexed by the wavelength associated to x
439
            profile_params_next_order = self.spectrum.chromatic_psf.from_poly_params_to_profile_params(poly_params_next_order,
1✔
440
                                                                                                   apply_bounds=True)
441
            # Second diffraction order amplitude is the first order amplitude multiplied by ratio 2/1
442
            # Ratio 2/1 is in flam/flam but no need to convert in ADU/ADU because lambda*dlambda is the same for both orders
443
            profile_params_next_order[:, 0] = self.tr_ratio_next_order(self.lambdas)
1✔
444
            profile_params_next_order[:, 1] = dispersion_law_next_order.real + self.spectrum.spectrogram_x0
1✔
445
            profile_params_next_order[:, 2] += dispersion_law_next_order.imag - self.bgd_width
1✔
446

447
            psf_cube_next_order = A2 * self.spectrum.chromatic_psf.build_psf_cube(self.pixels, profile_params_next_order,
1✔
448
                                                                                 fwhmx_clip=3 * parameters.PSF_FWHM_CLIP,
449
                                                                                 fwhmy_clip=parameters.PSF_FWHM_CLIP,
450
                                                                                 dtype="float32", mask=self.psf_cube_masked_next_order)
451
            psf_cube += psf_cube_next_order
1✔
452
            if self.tr_ratio_next_next_order is not None:
1✔
453
                dispersion_law_next_next_order = self.spectrum.compute_dispersion_in_spectrogram(self.lambdas, dx0, dy0, angle,
×
454
                                                                                                 niter=5, with_adr=True,
455
                                                                                                 order=self.spectrum.order + 2*np.sign(self.spectrum.order))
456

457
                profile_params_next_next_order = self.spectrum.chromatic_psf.from_poly_params_to_profile_params(poly_params_next_order, apply_bounds=True)
×
458
                profile_params_next_next_order[:, 0] *= self.spectrum.disperser.ratio_order_3over2(self.lambdas)
×
459
                profile_params_next_next_order[:, 1] = dispersion_law_next_next_order.real + self.spectrum.spectrogram_x0
×
460
                profile_params_next_next_order[:, 2] += dispersion_law_next_next_order.imag - self.bgd_width
×
461
                psf_cube_next_next_order = A2 * self.spectrum.chromatic_psf.build_psf_cube(self.pixels, profile_params_next_next_order,
×
462
                                                                                           fwhmx_clip=3 * parameters.PSF_FWHM_CLIP,
463
                                                                                           fwhmy_clip=parameters.PSF_FWHM_CLIP,
464
                                                                                           dtype="float32", mask=None)
465
                psf_cube += psf_cube_next_next_order
×
466

467
        M = psf_cube.reshape(len(profile_params), self.pixels[0].size).T  # flattening
1✔
468
        if self.sparse_indices is None:
1✔
469
            self.sparse_indices = np.where(M > 0)
1✔
470
        M = sparse.csr_matrix((M[self.sparse_indices].ravel(), self.sparse_indices), shape=M.shape, dtype="float32")
1✔
471
        # Algebra to compute amplitude parameters
472
        if self.amplitude_priors_method != "fixed":
1✔
473
            M_dot_W = M.T * self.sqrtW
1✔
474
            M_dot_W_dot_M = M_dot_W @ M_dot_W.T
1✔
475
            if self.amplitude_priors_method != "spectrum":
1✔
476
                # try:  # slower
477
                #     L = np.linalg.inv(np.linalg.cholesky(M_dot_W_dot_M))
478
                #     cov_matrix = L.T @ L
479
                # except np.linalg.LinAlgError:
480
                cov_matrix = np.linalg.inv(M_dot_W_dot_M)
×
481
                amplitude_params = cov_matrix @ (M.T @ W_dot_data)
×
482
                if self.amplitude_priors_method == "positive":
×
483
                    amplitude_params[amplitude_params < 0] = 0
×
484
                elif self.amplitude_priors_method == "smooth":
×
485
                    null_indices = np.where(amplitude_params < 0)[0]
×
486
                    for index in null_indices:
×
487
                        right = amplitude_params[index]
×
488
                        for i in range(index, min(index + 10, self.Nx)):
×
489
                            right = amplitude_params[i]
×
490
                            if i not in null_indices:
×
491
                                break
×
492
                        left = amplitude_params[index]
×
493
                        for i in range(index, max(0, index - 10), -1):
×
494
                            left = amplitude_params[i]
×
495
                            if i not in null_indices:
×
496
                                break
×
497
                        amplitude_params[index] = 0.5 * (right + left)
×
498
                elif self.amplitude_priors_method == "noprior":
×
499
                    pass
×
500
            else:
501
                M_dot_W_dot_M_plus_Q = M_dot_W_dot_M + self.reg * self.Q
1✔
502
                # try:  # slower
503
                #     L = np.linalg.inv(np.linalg.cholesky(M_dot_W_dot_M_plus_Q))
504
                #     cov_matrix = L.T @ L
505
                # except np.linalg.LinAlgError:
506
                cov_matrix = np.linalg.inv(M_dot_W_dot_M_plus_Q)
1✔
507
                amplitude_params = cov_matrix @ (M.T @ W_dot_data + self.reg * self.Q_dot_A0)
1✔
508
            self.M_dot_W_dot_M = M_dot_W_dot_M
1✔
509
            amplitude_params = np.asarray(amplitude_params).reshape(-1)
1✔
510
        else:
511
            amplitude_params = np.copy(self.amplitude_priors)
×
512
            err2 = np.copy(amplitude_params)
×
513
            err2[err2 <= 0] = np.min(np.abs(err2[err2 > 0]))
×
514
            cov_matrix = np.diag(err2)
×
515

516
        # Save results
517
        self.M = M
1✔
518
        self.psf_profile_params = np.copy(profile_params)
1✔
519
        self.psf_poly_params = np.copy(poly_params)
1✔
520
        self.amplitude_params = np.copy(amplitude_params)
1✔
521
        self.amplitude_params_err = np.array([np.sqrt(cov_matrix[x, x]) for x in range(self.Nx)])
1✔
522
        self.amplitude_cov_matrix = np.copy(cov_matrix)
1✔
523

524
        # Compute the model
525
        self.model = M @ amplitude_params
1✔
526
        self.model_err = np.zeros_like(self.model)
1✔
527

528
        return self.pixels, self.model, self.model_err
1✔
529

530
    # this use of fix_psf_cube gives slower results
531
    # def jacobian(self, params, epsilon, fixed_params=None, model_input=None):
532
    #     start = time.time()
533
    #     if model_input is not None:
534
    #         lambdas, model, model_err = model_input
535
    #     else:
536
    #         lambdas, model, model_err = self.simulate(*params)
537
    #     model = model.flatten()
538
    #     J = np.zeros((params.size, model.size))
539
    #     strategy = copy.copy(self.fix_psf_cube)
540
    #     strategy_order2 = copy.copy(self.fix_psf_cube_order2)
541
    #     for ip, p in enumerate(params):
542
    #         if fixed_params[ip]:
543
    #             continue
544
    #         # if ip in self.psf_params_index:
545
    #         #     self.fix_psf_cube = False
546
    #         # else:
547
    #         #     self.fix_psf_cube = True
548
    #         if ip in self.psf_params_index_order2:
549
    #             # reset the 1st order cube when starting 2nd order params
550
    #             if ip == self.psf_params_start_index_order2:
551
    #                 self.psf_cube = None
552
    #             self.fix_psf_cube_order2 = False
553
    #         else:
554
    #             self.fix_psf_cube_order2 = True
555
    #         tmp_p = np.copy(params)
556
    #         if tmp_p[ip] + epsilon[ip] < self.bounds[ip][0] or tmp_p[ip] + epsilon[ip] > self.bounds[ip][1]:
557
    #             epsilon[ip] = - epsilon[ip]
558
    #         tmp_p[ip] += epsilon[ip]
559
    #         tmp_lambdas, tmp_model, tmp_model_err = self.simulate(*tmp_p)
560
    #         J[ip] = (tmp_model.flatten() - model) / epsilon[ip]
561
    #     self.fix_psf_cube = strategy
562
    #     self.fix_psf_cube_order2 = strategy_order2
563
    #     self.my_logger.debug(f"\n\tJacobian time computation = {time.time() - start:.1f}s")
564
    #     return J
565

566
    def plot_spectrogram_comparison_simple(self, ax, title='', extent=None, dispersion=False):
1✔
567
        """Method to plot a spectrogram issued from data and compare it with simulations.
568

569
        Parameters
570
        ----------
571
        ax: Axes
572
            Axes instance of shape (4, 2).
573
        title: str, optional
574
            Title for the simulation plot (default: '').
575
        extent: array_like, optional
576
            Extent argument for imshow to crop plots (default: None).
577
        dispersion: bool, optional
578
            If True, plot a colored bar to see the associated wavelength color along the x axis (default: False).
579
        """
580
        cmap_bwr = copy.copy(cm.get_cmap('bwr'))
1✔
581
        cmap_bwr.set_bad(color='lightgrey')
1✔
582
        cmap_viridis = copy.copy(cm.get_cmap('viridis'))
1✔
583
        cmap_viridis.set_bad(color='lightgrey')
1✔
584

585
        data = np.copy(self.data_before_mask)
1✔
586
        if len(self.outliers) > 0 or len(self.mask) > 0:
1✔
587
            bad_indices = np.array(list(self.get_bad_indices()) + list(self.mask)).astype(int)
1✔
588
            data[bad_indices] = np.nan
1✔
589

590
        lambdas = self.spectrum.lambdas
1✔
591
        sub = np.where((lambdas > parameters.LAMBDA_MIN) & (lambdas < parameters.LAMBDA_MAX))[0]
1✔
592
        sub = np.where(sub < self.spectrum.spectrogram.shape[1])[0]
1✔
593
        data = (data + self.bgd_flat).reshape((self.Ny, self.Nx))
1✔
594
        err = self.err.reshape((self.Ny, self.Nx))
1✔
595
        model = (self.model + self.params.p[5] * self.bgd_flat).reshape((self.Ny, self.Nx))
1✔
596
        if extent is not None:
1✔
597
            sub = np.where((lambdas > extent[0]) & (lambdas < extent[1]))[0]
1✔
598
        if len(sub) > 0:
1✔
599
            norm = np.nanmax(data[:, sub])
1✔
600
            plot_image_simple(ax[0, 0], data=data[:, sub] / norm, title='Data', aspect='auto',
1✔
601
                              cax=ax[0, 1], vmin=0, vmax=1, units='1/max(data)', cmap=cmap_viridis)
602
            ax[0, 0].set_title('Data', fontsize=10, loc='center', color='white', y=0.8)
1✔
603
            plot_image_simple(ax[1, 0], data=model[:, sub] / norm, aspect='auto', cax=ax[1, 1], vmin=0, vmax=1,
1✔
604
                              units='1/max(data)', cmap=cmap_viridis)
605
            if dispersion:
1✔
606
                x = self.spectrum.chromatic_psf.table['Dx'][sub[5:-5]] + self.spectrum.spectrogram_x0 - sub[0]
1✔
607
                y = np.ones_like(x)
1✔
608
                ax[1, 0].scatter(x, y, cmap=from_lambda_to_colormap(self.lambdas[sub[5:-5]]), edgecolors='None',
1✔
609
                                 c=self.lambdas[sub[5:-5]],
610
                                 label='', marker='o', s=10)
611
                ax[1, 0].set_xlim(0, model[:, sub].shape[1])
1✔
612
                ax[1, 0].set_ylim(0, model[:, sub].shape[0])
1✔
613
            # p0 = ax.plot(lambdas, self.model(lambdas), label='model')
614
            # # ax.plot(self.lambdas, self.model_noconv, label='before conv')
615
            if title != '':
1✔
616
                ax[1, 0].set_title(title, fontsize=10, loc='center', color='white', y=0.8)
1✔
617
            residuals = (data - model)
1✔
618
            # residuals_err = self.spectrum.spectrogram_err / self.model
619
            norm = err
1✔
620
            residuals /= norm
1✔
621
            std = float(np.nanstd(residuals[:, sub]))
1✔
622
            plot_image_simple(ax[2, 0], data=residuals[:, sub], vmin=-5 * std, vmax=5 * std, title='(Data-Model)/Err',
1✔
623
                              aspect='auto', cax=ax[2, 1], units='', cmap=cmap_bwr)
624
            ax[2, 0].set_title('(Data-Model)/Err', fontsize=10, loc='center', color='black', y=0.8)
1✔
625
            ax[2, 0].text(0.05, 0.05,
1✔
626
                          f'mean={np.nanmean(residuals[:, sub]):.3f}\nstd={np.nanstd(residuals[:, sub]):.3f}',
627
                          horizontalalignment='left', verticalalignment='bottom',
628
                          color='black', transform=ax[2, 0].transAxes)
629
            ax[0, 0].set_xticks(ax[2, 0].get_xticks()[1:-1])
1✔
630
            ax[0, 1].get_yaxis().set_label_coords(3.5, 0.5)
1✔
631
            ax[1, 1].get_yaxis().set_label_coords(3.5, 0.5)
1✔
632
            ax[2, 1].get_yaxis().set_label_coords(3.5, 0.5)
1✔
633
            ax[3, 1].remove()
1✔
634
            ax[3, 0].plot(self.lambdas[sub], np.nansum(data, axis=0)[sub], label='Data')
1✔
635
            model[np.isnan(data)] = np.nan  # mask background values outside fitted region
1✔
636
            ax[3, 0].plot(self.lambdas[sub], np.nansum(model, axis=0)[sub], label='Model')
1✔
637
            ax[3, 0].set_ylabel('Cross spectrum')
1✔
638
            ax[3, 0].set_xlabel(r'$\lambda$ [nm]')
1✔
639
            ax[3, 0].legend(fontsize=7)
1✔
640
            ax[3, 0].grid(True)
1✔
641

642
    def plot_fit(self):
1✔
643
        """Plot the fit result.
644

645
        Examples
646
        --------
647

648
        >>> spec = Spectrum('tests/data/sim_20170530_134_spectrum.fits')
649
        >>> w = FullForwardModelFitWorkspace(spec, verbose=True, plot=True, live_fit=False)
650
        >>> lambdas, model, model_err = w.simulate(*w.params.p)
651
        >>> w.plot_fit()
652

653
        .. plot::
654
            :include-source:
655

656
            from spectractor.fit.fit_spectrogram import SpectrogramFitWorkspace
657
            file_name = 'tests/data/reduc_20170530_134_spectrum.fits'
658
            atmgrid_file_name = file_name.replace('spectrum', 'atmsim')
659
            fit_workspace = SpectrogramFitWorkspace(file_name, atmgrid_file_name=atmgrid_file_name, verbose=True)
660
            A1, A2, ozone, pwv, aerosols, D, shift_x, shift_y, angle, *psf = fit_workspace.p
661
            lambdas, model, model_err = fit_workspace.simulation.simulate(A1, A2, ozone, pwv, aerosols, D, shift_x,
662
                                                                          shift_y, angle, psf)
663
            fit_workspace.lambdas = lambdas
664
            fit_workspace.model = model
665
            fit_workspace.model_err = model_err
666
            fit_workspace.plot_fit()
667

668
        """
669
        if np.max(self.lambdas) < 730:
1✔
670
            gs_kw = dict(width_ratios=[1, 0.15], height_ratios=[1, 1, 1, 1])
×
671
            fig, ax = plt.subplots(nrows=4, ncols=2, figsize=(8, 10), gridspec_kw=gs_kw)
×
672
        elif 800 < np.max(self.lambdas) < 950:
1✔
673
            gs_kw = dict(width_ratios=[3, 0.15, 1, 0.15], height_ratios=[1, 1, 1, 1])
×
674
            fig, ax = plt.subplots(nrows=4, ncols=4, figsize=(14, 10), gridspec_kw=gs_kw)
×
675
        else:
676
            gs_kw = dict(width_ratios=[3, 0.15, 1, 0.15, 1, 0.15], height_ratios=[1, 1, 1, 1])
1✔
677
            fig, ax = plt.subplots(nrows=4, ncols=6, figsize=(18, 10), gridspec_kw=gs_kw)
1✔
678

679
        # A2, D2CCD, dx0, dy0, angle, B, rot, *poly_params = self.p
680
        # plt.suptitle(f'A2={A2:.3f}, D={D2CCD:.2f}mm, shift_x={dx0:.3f}pix, shift_y={dy0:.3f}pix, '
681
        #              f'angle={angle:.2f}pix, B={B:.3f}', y=1)
682
        # main plot
683
        self.plot_spectrogram_comparison_simple(ax[:, 0:2], title='Spectrogram model', dispersion=True)
1✔
684
        # zoom O2
685
        if np.max(self.lambdas) > 800:
1✔
686
            self.plot_spectrogram_comparison_simple(ax[:, 2:4], extent=[730, 800], title='Zoom $O_2$', dispersion=True)
1✔
687
        # zoom H2O
688
        if np.max(self.lambdas) > 950:
1✔
689
            self.plot_spectrogram_comparison_simple(ax[:, 4:6], extent=[870, min(1000, int(np.max(self.lambdas)))], title='Zoom $H_2 O$', dispersion=True)
1✔
690
        # for i in range(ax.shape[0]-1):  # clear middle colorbars
691
        #     for j in range(ax.shape[1]//2-1):
692
        #         plt.delaxes(ax[i, 2 * j + 1])
693
        for i in range(ax.shape[0]):  # clear middle y axis labels
1✔
694
            for j in range(1, ax.shape[1]//2):
1✔
695
                ax[i, 2 * j].set_ylabel("")
1✔
696
        fig.tight_layout()
1✔
697
        if parameters.LSST_SAVEFIGPATH:  # pragma: no cover
698
            figname = os.path.join(parameters.LSST_SAVEFIGPATH, f'ffm_bestfit.pdf')
699
            self.my_logger.info(f"\n\tSave figure {figname}.")
700
            fig.savefig(figname, dpi=100, bbox_inches='tight')
701
            gs_kw = dict(width_ratios=[3, 0.15], height_ratios=[1, 1, 1, 1])
702
            fig2, ax2 = plt.subplots(nrows=4, ncols=2, figsize=(10, 12), gridspec_kw=gs_kw)
703
            self.plot_spectrogram_comparison_simple(ax2, title='Spectrogram model', dispersion=True)
704
            # plt.delaxes(ax2[3, 1])
705
            fig2.tight_layout()
706
            figname = os.path.join(parameters.LSST_SAVEFIGPATH, f'ffm_bestfit_2.pdf')
707
            self.my_logger.info(f"\n\tSave figure {figname}.")
708
            fig2.savefig(figname, dpi=100, bbox_inches='tight')
709
        if self.live_fit:  # pragma: no cover
710
            plt.draw()
711
            plt.pause(1e-8)
712
            plt.close()
713
        else:
714
            if parameters.DISPLAY and self.verbose:
1✔
715
                plt.show()
×
716

717
    def adjust_spectrogram_position_parameters(self):
1✔
718
        # fit the spectrogram trace
719
        epsilon = 1e-4 * self.params.p
1✔
720
        epsilon[epsilon == 0] = 1e-4
1✔
721
        fixed_default = np.copy(self.params.fixed)
1✔
722
        self.params.fixed = [True] * len(self.params.p)
1✔
723
        # if fixed_default[3] is False and fixed_default[4] is False:
724
        self.params.fixed[3:5] = [False, False]  # shift_y and angle
1✔
725
        # else:
726
        #     for k, par in enumerate(self.input_labels):
727
        #         if "y_c" in par and "_2" not in par:
728
        #             self.fixed[k] = False
729
        self.sparse_indices = None
1✔
730
        run_minimisation(self, "newton", epsilon, xtol=1e-2, ftol=0.01)  # 1000 / self.data.size)
1✔
731
        self.params.fixed = fixed_default
1✔
732
        self.sparse_indices = None
1✔
733
        self.set_mask(params=self.params.p, fwhmx_clip=3*parameters.PSF_FWHM_CLIP, fwhmy_clip=parameters.PSF_FWHM_CLIP)
1✔
734
        # self.set_y_c()
735

736

737
def run_ffm_minimisation(w, method="newton", niter=2):
1✔
738
    """Interface function to fit spectrogram simulation parameters to data.
739

740
    Parameters
741
    ----------
742
    w: FullForwardModelFitWorkspace
743
        An instance of the SpectrogramFitWorkspace class.
744
    method: str, optional
745
        Fitting method (default: 'newton').
746
    niter: int, optional
747
        Number of FFM iterations to final result (default: 2).
748

749
    Returns
750
    -------
751
    spectrum: Spectrum
752
        The extracted spectrum.
753

754
    Examples
755
    --------
756

757
    >>> spec = Spectrum("./tests/data/sim_20170530_134_spectrum.fits")
758
    >>> parameters.VERBOSE = True
759
    >>> w = FullForwardModelFitWorkspace(spec, verbose=True, plot=True, live_fit=True, amplitude_priors_method="spectrum")
760
    >>> spec = run_ffm_minimisation(w, method="newton")  # doctest: +ELLIPSIS
761
    >>> if 'LBDAS_T' in spec.header: plot_comparison_truth(spec, w)
762

763
    .. doctest:
764
        :hide:
765

766
        >>> assert w.costs[-1] / w.data.size < 1.22  # reduced chisq
767
        >>> assert np.isclose(w.params.p[4], -1.56, rtol=0.05)  # angle
768
        >>> assert np.isclose(w.params.p[5], 1, rtol=0.05)  # B
769

770
    """
771
    my_logger = set_logger(__name__)
1✔
772
    my_logger.info(f"\n\tStart FFM with adjust_spectrogram_position_parameters.")
1✔
773
    w.adjust_spectrogram_position_parameters()
1✔
774

775
    if method != "newton":
1✔
776
        run_minimisation(w, method=method)
×
777
    else:
778
        costs = np.array([w.chisq(w.params.p)])
1✔
779
        if parameters.DISPLAY and (parameters.DEBUG or w.live_fit):
1✔
780
            w.plot_fit()
×
781
        start = time.time()
1✔
782
        my_logger.info(f"\n\tStart guess: {w.params.p}\n\twith {w.params.input_labels}")
1✔
783
        epsilon = 1e-4 * w.params.p
1✔
784
        epsilon[epsilon == 0] = 1e-4
1✔
785

786
        run_minimisation(w, method=method, xtol=1e-3, ftol=1e-2)  # 1000 / (w.data.size - len(w.mask)))
1✔
787
        if parameters.DEBUG and parameters.DISPLAY:
1✔
788
            w.plot_fit()
×
789

790
        weighted_mean_fwhm = np.average(w.spectrum.chromatic_psf.table['fwhm'], weights=w.spectrum.chromatic_psf.table['amplitude'])
1✔
791
        my_logger.info(f"\n\tMean FWHM: {weighted_mean_fwhm} pixels (weighted with spectrum amplitude)")
1✔
792
        if parameters.DEBUG:
1✔
793
            fig, ax = plt.subplots(1, 1, figsize=(7, 5), sharex="all")
1✔
794
            ax.plot(w.spectrum.lambdas, np.array(w.spectrum.chromatic_psf.table['fwhm']), label=f"weighted mean={weighted_mean_fwhm} pix")
1✔
795
            ax.set_xlabel(r"$\lambda$ [nm]")
1✔
796
            ax.set_ylabel("Transverse FWHM [pixels]")
1✔
797
            ax.set_ylim((0.8 * np.min(w.spectrum.chromatic_psf.table['fwhm']), 1.2 * np.max(w.spectrum.chromatic_psf.table['fwhm'])))  # [-10:])))
1✔
798
            ax.grid()
1✔
799
            ax.legend()
1✔
800
            if parameters.DISPLAY:
1✔
801
                plt.show()
×
802
            if parameters.LSST_SAVEFIGPATH:
1✔
803
                fig.savefig(os.path.join(parameters.LSST_SAVEFIGPATH, 'fwhm_2.pdf'))
×
804

805
        my_logger.info("\n\tStart regularization parameter only.")
1✔
806
        # Optimize the regularisation parameter only if it was not done before
807
        if w.amplitude_priors_method == "spectrum" and w.reg == parameters.PSF_FIT_REG_PARAM:  # pragma: no cover
808
            w_reg = RegFitWorkspace(w, opt_reg=parameters.PSF_FIT_REG_PARAM, verbose=True)
809
            w_reg.run_regularisation()
810
            w.opt_reg = w_reg.opt_reg
811
            w.reg = np.copy(w_reg.opt_reg)
812
            w.simulate(*w.params.p)
813
            if np.trace(w.amplitude_cov_matrix) < np.trace(w.amplitude_priors_cov_matrix):
814
                w.my_logger.warning(
815
                    f"\n\tTrace of final covariance matrix ({np.trace(w.amplitude_cov_matrix)}) is "
816
                    f"below the trace of the prior covariance matrix "
817
                    f"({np.trace(w.amplitude_priors_cov_matrix)}). This is probably due to a very "
818
                    f"high regularisation parameter in case of a bad fit. Therefore the final "
819
                    f"covariance matrix is multiplied by the ratio of the traces and "
820
                    f"the amplitude parameters are very close the amplitude priors.")
821
                r = np.trace(w.amplitude_priors_cov_matrix) / np.trace(w.amplitude_cov_matrix)
822
                w.amplitude_cov_matrix *= r
823
                w.amplitude_params_err = np.array(
824
                    [np.sqrt(w.amplitude_cov_matrix[x, x]) for x in range(w.Nx)])
825
            w.spectrum.header['PSF_REG'] = w.opt_reg
826
            w.spectrum.header['TRACE_R'] = np.trace(w_reg.resolution)
827

828
        if parameters.DEBUG and parameters.DISPLAY:
1✔
829
            w.plot_fit()
×
830

831
        my_logger.info(f"\n\tStart run_minimisation_sigma_clipping "
1✔
832
                       f"with sigma={parameters.SPECTRACTOR_DECONVOLUTION_SIGMA_CLIP}.")
833
        for i in range(niter):
1✔
834
            w.set_mask(params=w.params.p, fwhmx_clip=3*parameters.PSF_FWHM_CLIP, fwhmy_clip=parameters.PSF_FWHM_CLIP)
1✔
835
            run_minimisation_sigma_clipping(w, "newton", epsilon, xtol=1e-5,
1✔
836
                                            ftol=1e-3, niter_clip=3,  # ftol=100 / (w.data.size - len(w.mask))
837
                                            sigma_clip=parameters.SPECTRACTOR_DECONVOLUTION_SIGMA_CLIP, verbose=True)
838
            my_logger.info(f"\n\t  niter = {i} : Newton: total computation time: {time.time() - start}s")
1✔
839
            if parameters.DEBUG and parameters.DISPLAY:
1✔
840
                w.plot_fit()
×
841

842
            # recompute angle and dy0 if fixed while y_c parameters are free
843
            # if w.fixed[3] and w.fixed[4] and not np.any([w.fixed[k] for k, par in enumerate(w.input_labels) if "y_c" in par]):
844
            #     pval_leg = [w.p[k] for k, par in enumerate(w.input_labels) if "y_c" in par][
845
            #                :w.spectrum.chromatic_psf.degrees["y_c"] + 1]
846
            #     pval_poly = np.polynomial.legendre.leg2poly(pval_leg)
847
            #     new_dy0, new_angle = w.p[2], w.p[4]
848
            #     from numpy.polynomial import Polynomial as P
849
            #     p = P(pval_poly)
850
            #     pX = P([0, 0.5 * (w.spectrum.spectrogram_Nx)])
851
            #     pfinal = p(pX)
852
            #     pval_poly = pfinal.coef
853
            #     for k in range(pval_poly.size):
854
            #         if k == 0:
855
            #             new_dy0 += pval_poly[k]
856
            #         if k == 1:
857
            #             new_angle += np.arctan(pval_poly[k]) * 180 / np.pi
858

859
            w.spectrum.lambdas = np.copy(w.lambdas)
1✔
860
            w.spectrum.chromatic_psf.table['lambdas'] = np.copy(w.lambdas)
1✔
861
            w.spectrum.data = np.copy(w.amplitude_params)
1✔
862
            w.spectrum.err = np.copy(w.amplitude_params_err)
1✔
863
            w.spectrum.cov_matrix = np.copy(w.amplitude_cov_matrix)
1✔
864
            w.spectrum.chromatic_psf.fill_table_with_profile_params(w.psf_profile_params)
1✔
865
            w.spectrum.chromatic_psf.table["amplitude"] = np.copy(w.amplitude_params)
1✔
866
            w.spectrum.chromatic_psf.from_profile_params_to_shape_params(w.psf_profile_params)
1✔
867
            w.spectrum.chromatic_psf.poly_params = w.spectrum.chromatic_psf.from_table_to_poly_params()
1✔
868
            w.spectrum.spectrogram_fit = w.model
1✔
869
            w.spectrum.spectrogram_residuals = (w.data - w.spectrum.spectrogram_fit) / w.err
1✔
870
            w.spectrum.header['CHI2_FIT'] = w.costs[-1] / (w.data.size - len(w.mask))
1✔
871
            w.spectrum.header['PIXSHIFT'] = w.params.p[2]
1✔
872
            w.spectrum.header['D2CCD'] = w.params.p[1]
1✔
873
            w.spectrum.header['A2_FIT'] = w.params.p[0]
1✔
874
            w.spectrum.header["ROTANGLE"] = w.params.p[4]
1✔
875
            w.spectrum.header["AM_FIT"] = w.params.p[9]
1✔
876
            # Compute next order contamination
877
            if w.tr_ratio_next_order:
1✔
878
                w.spectrum.data_next_order = w.params.p[0] * w.amplitude_params * w.tr_ratio_next_order(w.lambdas)
1✔
879
                w.spectrum.err_next_order = np.abs(w.params.p[0] * w.amplitude_params_err * w.tr_ratio_next_order(w.lambdas))
1✔
880

881
            # Calibrate the spectrum
882
            calibrate_spectrum(w.spectrum, with_adr=True)
1✔
883
            w.params.p[1] = w.spectrum.disperser.D
1✔
884
            w.params.p[2] = w.spectrum.header['PIXSHIFT']
1✔
885
            w.spectrum.convert_from_flam_to_ADUrate()
1✔
886

887
        if w.filename != "":
1✔
888
            parameters.SAVE = True
×
889
            w.params.plot_correlation_matrix()
×
890
            write_fitparameter_json(w.params.json_filename, w.params,
×
891
                                    extra={"chi2": costs[-1] / (w.data.size - len(w.outliers) - len(w.mask)),
892
                                           "date-obs": w.spectrum.date_obs})
893
            w.plot_fit()
×
894
            parameters.SAVE = False
×
895

896
    # Save results
897
    # w.spectrum.convert_from_ADUrate_to_flam()
898
    # x, model, model_err = w.simulate(*w.p)
899

900
    # Propagate uncertainties
901
    # from spectractor.tools import plot_correlation_matrix_simple, compute_correlation_matrix
902
    # M = np.copy(w.M)
903
    # amplitude_params = np.copy(w.amplitude_params)
904
    # ipar = np.array(np.where(np.array(w.fixed).astype(int) == 0)[0])
905
    # w.amplitude_priors_method = "fixed"
906
    # jac = w.jacobian(w.p, epsilon=epsilon, fixed_params=w.fixed, model_input=[x, model, model_err])
907
    # jac = jac[ipar]
908
    # start = jac.shape[0]
909
    # J = np.hstack([jac.T, M])
910
    # H = (J.T * w.W) @ J
911
    # H[start:, start:] += w.reg*w.Q
912
    # full_cov_matrix = np.linalg.inv(H)
913
    # amplitude_params_err = np.array([np.sqrt(full_cov_matrix[start+x, start+x]) for x in range(w.Nx)])
914
    # plt.errorbar(w.lambdas, amplitude_params, yerr=amplitude_params_err)
915
    # plt.grid()
916
    # plt.show()
917
    # plt.figure()
918
    # plot_correlation_matrix_simple(ax=plt.gca(), rho=compute_correlation_matrix(full_cov_matrix))
919
    # plt.show()
920
    # w.amplitude_params = amplitude_params
921
    # w.amplitude_params_err = amplitude_params_err
922
    # w.amplitude_cov_matrix = full_cov_matrix[start:, start:]
923

924
    # Propagate parameters
925
    A2, D2CCD, dx0, dy0, angle, B, *poly_params = w.params.p
1✔
926
    w.spectrum.rotation_angle = angle
1✔
927
    w.spectrum.spectrogram_bgd *= B
1✔
928
    w.spectrum.spectrogram_bgd_rms *= B
1✔
929
    w.spectrum.spectrogram_x0 += dx0
1✔
930
    w.spectrum.spectrogram_y0 += dy0
1✔
931
    w.spectrum.x0[0] += dx0
1✔
932
    w.spectrum.x0[1] += dy0
1✔
933
    w.spectrum.header["TARGETX"] = w.spectrum.x0[0]
1✔
934
    w.spectrum.header["TARGETY"] = w.spectrum.x0[1]
1✔
935
    w.spectrum.header['MEANFWHM'] = np.mean(np.array(w.spectrum.chromatic_psf.table['fwhm']))
1✔
936

937
    # Convert to flam
938
    w.spectrum.convert_from_ADUrate_to_flam()
1✔
939

940
    # Compare with truth if available
941
    if 'LBDAS_T' in w.spectrum.header and parameters.DEBUG:
1✔
942
        plot_comparison_truth(w.spectrum, w)
1✔
943

944
    return w.spectrum
1✔
945

946

947
def SpectractorInit(file_name, target_label='', disperser_label="", config=''):
1✔
948
    """ Spectractor initialisation: load the config parameters and build the Image instance.
949

950
    Parameters
951
    ----------
952
    file_name: str
953
        Input file nam of the image to analyse.
954
    target_label: str, optional
955
        The name of the targeted object (default: "").
956
    disperser_label: str, optional
957
        The name of the disperser (default: "").
958
    config: str
959
        The config file name (default: "").
960

961
    Returns
962
    -------
963
    image: Image
964
        The prepared Image instance ready for spectrum extraction.
965

966
    Examples
967
    --------
968

969
    Extract the spectrogram and its characteristics from the image:
970

971
    .. doctest::
972

973
        >>> import os
974
        >>> from spectractor.logbook import LogBook
975
        >>> logbook = LogBook(logbook='./tests/data/ctiofulllogbook_jun2017_v5.csv')
976
        >>> file_names = ['./tests/data/reduc_20170530_134.fits']
977
        >>> for file_name in file_names:
978
        ...     tag = file_name.split('/')[-1]
979
        ...     disperser_label, target_label, xpos, ypos = logbook.search_for_image(tag)
980
        ...     if target_label is None or xpos is None or ypos is None:
981
        ...         continue
982
        ...     image = SpectractorInit(file_name, target_label=target_label,
983
        ...                             disperser_label=disperser_label, config='./config/ctio.ini')
984

985
    .. doctest::
986
        :hide:
987

988
        >>> assert image is not None
989

990
    """
991

992
    my_logger = set_logger(__name__)
1✔
993
    my_logger.info('\n\tSpectractor initialisation')
1✔
994
    # Load config file
995
    if config != "":
1✔
996
        load_config(config)
×
997
    if parameters.LSST_SAVEFIGPATH:  # pragma: no cover
998
        ensure_dir(parameters.LSST_SAVEFIGPATH)
999

1000
    # Load reduced image
1001
    image = Image(file_name, target_label=target_label, disperser_label=disperser_label)
1✔
1002
    return image
1✔
1003

1004

1005
def SpectractorRun(image, output_directory, guess=None):
1✔
1006
    """ Spectractor main function to extract a spectrum from an image
1007

1008
    Parameters
1009
    ----------
1010
    image: Image
1011
        Input Image instance.
1012
    output_directory: str
1013
        Output directory.
1014
    guess: [int,int], optional
1015
        [x0,y0] list of the guessed pixel positions of the target in the image (must be integers). Mandatory if
1016
        WCS solution is absent (default: None).
1017

1018
    Returns
1019
    -------
1020
    spectrum: Spectrum
1021
        The extracted spectrum object.
1022

1023
    Examples
1024
    --------
1025

1026
    Extract the spectrogram and its characteristics from the image:
1027

1028
    .. doctest::
1029

1030
        >>> import os
1031
        >>> from spectractor.logbook import LogBook
1032
        >>> logbook = LogBook(logbook='./tests/data/ctiofulllogbook_jun2017_v5.csv')
1033
        >>> file_names = ['./tests/data/reduc_20170530_134.fits']
1034
        >>> for file_name in file_names:
1035
        ...     tag = file_name.split('/')[-1]
1036
        ...     disperser_label, target_label, xpos, ypos = logbook.search_for_image(tag)
1037
        ...     if target_label is None or xpos is None or ypos is None:
1038
        ...         continue
1039
        ...     image = SpectractorInit(file_name, target_label=target_label,
1040
        ...                             disperser_label=disperser_label, config='./config/ctio.ini')
1041
        ...     spectrum = SpectractorRun(image, './tests/data/', guess=[xpos, ypos])
1042

1043
    .. doctest::
1044
        :hide:
1045

1046
        >>> assert spectrum is not None
1047
        >>> assert os.path.isfile('tests/data/reduc_20170530_134_spectrum.fits')
1048

1049
    """
1050

1051
    my_logger = set_logger(__name__)
1✔
1052
    my_logger.info('\n\tRun Spectractor')
1✔
1053

1054
    # Guess position of order 0
1055
    if guess is not None and image.target_guess is None:
1✔
1056
        image.target_guess = np.asarray(guess)
1✔
1057
    if image.target_guess is None:
1✔
1058
        from scipy.signal import medfilt2d
×
1059
        data = medfilt2d(image.data.T, kernel_size=9)
×
1060
        image.target_guess = np.unravel_index(np.argmax(data), data.shape)
×
1061
        my_logger.info(f"\n\tNo guess position of order 0 has been given. Assuming the spectrum to extract comes "
×
1062
                       f"from the brightest object, guess position is set as {image.target_guess}.")
1063
    if parameters.DEBUG:
1✔
1064
        image.plot_image(scale='symlog', title="before rebinning", target_pixcoords=image.target_guess, cmap='gray', vmax=1e3)
1✔
1065

1066
    # Use fast mode
1067
    if parameters.CCD_REBIN > 1:
1✔
1068
        my_logger.info('\n\t  ======================= REBIN =============================')
1✔
1069
        image.rebin()
1✔
1070
        if parameters.DEBUG:
1✔
1071
            image.plot_image(scale='symlog', title="after rebinning ", target_pixcoords=image.target_guess)
1✔
1072

1073
    # Set output path
1074
    ensure_dir(output_directory)
1✔
1075
    output_filename = os.path.basename(image.file_name)
1✔
1076
    output_filename = output_filename.replace('.fits', '_spectrum.fits')
1✔
1077
    output_filename = output_filename.replace('.fz', '_spectrum.fits')
1✔
1078
    output_filename = os.path.join(output_directory, output_filename)
1✔
1079
    # Find the exact target position in the raw cut image: several methods
1080
    my_logger.info(f'\n\tSearch for the target in the image with guess={image.target_guess}...')
1✔
1081
    find_target(image, image.target_guess, widths=(parameters.XWINDOW, parameters.YWINDOW))
1✔
1082
    # Rotate the image
1083
    turn_image(image)
1✔
1084
    # Find the exact target position in the rotated image: several methods
1085
    my_logger.info('\n\tSearch for the target in the rotated image...')
1✔
1086

1087
    find_target(image, image.target_guess, rotated=True, widths=(parameters.XWINDOW_ROT,
1✔
1088
                                                                 parameters.YWINDOW_ROT))
1089
    # Create Spectrum object
1090
    spectrum = Spectrum(image=image, order=parameters.SPEC_ORDER)
1✔
1091
    # First 1D spectrum extraction and background extraction
1092

1093
    my_logger.info('\n\t ======================== PSF1D Extraction ====================================')
1✔
1094
    w_psf1d, bgd_model_func = extract_spectrum_from_image(image, spectrum, signal_width=parameters.PIXWIDTH_SIGNAL,
1✔
1095
                                                          ws=(parameters.PIXDIST_BACKGROUND,
1096
                                                              parameters.PIXDIST_BACKGROUND
1097
                                                              + parameters.PIXWIDTH_BACKGROUND),
1098
                                                          right_edge=image.data.shape[1])
1099

1100
    # PSF2D deconvolution
1101
    if parameters.SPECTRACTOR_DECONVOLUTION_PSF2D:
1✔
1102
        my_logger.info('\n\t ========================== PSF2D DECONVOLUTION  ===============================')
1✔
1103
        run_spectrogram_deconvolution_psf2d(spectrum, bgd_model_func=bgd_model_func)
1✔
1104

1105
    # Calibrate the spectrum
1106
    my_logger.info(f'\n\tCalibrating order {spectrum.order:d} spectrum...')
1✔
1107
    with_adr = True
1✔
1108
    if parameters.OBS_OBJECT_TYPE != "STAR":
1✔
1109
        with_adr = False
×
1110
    calibrate_spectrum(spectrum, with_adr=with_adr)
1✔
1111
    spectrum.data_next_order = np.zeros_like(spectrum.lambdas)
1✔
1112
    spectrum.err_next_order = np.zeros_like(spectrum.lambdas)
1✔
1113

1114
    # Full forward model extraction: add transverse ADR and order 2 subtraction
1115
    if parameters.SPECTRACTOR_DECONVOLUTION_FFM:
1✔
1116
        my_logger.info('\n\t  ======================= FFM DECONVOLUTION =============================')
1✔
1117
        w = FullForwardModelFitWorkspace(spectrum, verbose=parameters.VERBOSE, plot=True, live_fit=False,
1✔
1118
                                         amplitude_priors_method="spectrum")
1119
        spectrum = run_ffm_minimisation(w, method="newton", niter=2)
1✔
1120

1121
    # Save the spectrum
1122
    my_logger.info('\n\t  ======================= SAVE SPECTRUM =============================')
1✔
1123
    spectrum.save_spectrum(output_filename, overwrite=True)
1✔
1124
    spectrum.lines.print_detected_lines(amplitude_units=spectrum.units)
1✔
1125

1126
    # Plot the spectrum
1127
    if parameters.VERBOSE and parameters.DISPLAY:
1✔
1128
        spectrum.plot_spectrum(xlim=None)
×
1129

1130
    return spectrum
1✔
1131

1132

1133
def Spectractor(file_name, output_directory, target_label='', guess=None, disperser_label="", config=''):
1✔
1134

1135
    """ Spectractor main function to extract a spectrum from a FITS file.
1136

1137
    Parameters
1138
    ----------
1139
    file_name: str
1140
        Input file nam of the image to analyse.
1141
    output_directory: str
1142
        Output directory.
1143
    target_label: str, optional
1144
        The name of the targeted object (default: "").
1145
    guess: [int,int], optional
1146
        [x0,y0] list of the guessed pixel positions of the target in the image (must be integers). Mandatory if
1147
        WCS solution is absent (default: None).
1148
    disperser_label: str, optional
1149
        The name of the disperser (default: "").
1150
    config: str
1151
        The config file name (default: "").
1152

1153
    Returns
1154
    -------
1155
    spectrum: Spectrum
1156
        The extracted spectrum object.
1157

1158
    Examples
1159
    --------
1160

1161
    Extract the spectrogram and its characteristics from the image:
1162

1163
    .. doctest::
1164

1165
        >>> import os
1166
        >>> from spectractor.logbook import LogBook
1167
        >>> logbook = LogBook(logbook='./tests/data/ctiofulllogbook_jun2017_v5.csv')
1168
        >>> file_names = ['./tests/data/reduc_20170530_134.fits']
1169
        >>> for file_name in file_names:
1170
        ...     tag = file_name.split('/')[-1]
1171
        ...     disperser_label, target_label, xpos, ypos = logbook.search_for_image(tag)
1172
        ...     if target_label is None or xpos is None or ypos is None:
1173
        ...         continue
1174
        ...     spectrum = Spectractor(file_name, './tests/data/', guess=[xpos, ypos], target_label=target_label,
1175
        ...                            disperser_label=disperser_label, config='./config/ctio.ini')
1176

1177
    .. doctest::
1178
        :hide:
1179

1180
        >>> assert spectrum is not None
1181
        >>> assert os.path.isfile('tests/data/reduc_20170530_134_spectrum.fits')
1182

1183
    """
1184
    image = SpectractorInit(file_name, target_label=target_label, disperser_label=disperser_label, config=config)
1✔
1185
    spectrum = SpectractorRun(image, guess=guess, output_directory=output_directory)
1✔
1186
    return spectrum
1✔
1187

1188

1189
def extract_spectrum_from_image(image, spectrum, signal_width=10, ws=(20, 30), right_edge=parameters.CCD_IMSIZE):
1✔
1190
    """Extract the 1D spectrum from the image.
1191

1192
    Method : remove a uniform background estimated from the rectangular lateral bands
1193

1194
    The spectrum amplitude is the sum of the pixels in the 2*w rectangular window
1195
    centered on the order 0 y position.
1196
    The up and down backgrounds are estimated as the median in rectangular regions
1197
    above and below the spectrum, in the ws-defined rectangular regions; stars are filtered
1198
    as nan values using an hessian analysis of the image to remove structures.
1199
    The subtracted background is the mean of the two up and down backgrounds.
1200
    Stars are filtered.
1201

1202
    Prerequisites: the target position must have been found before, and the
1203
        image turned to have an horizontal dispersion line
1204

1205
    Parameters
1206
    ----------
1207
    image: Image
1208
        Image object from which to extract the spectrum
1209
    spectrum: Spectrum
1210
        Spectrum object to store new wavelengths, data and error arrays
1211
    signal_width: int
1212
        Half width of central region where the spectrum is extracted and summed (default: 10)
1213
    ws: list
1214
        up/down region extension where the sky background is estimated with format [int, int] (default: [20,30])
1215
    right_edge: int
1216
        Right-hand pixel position above which no pixel should be used (default: parameters.CCD_IMSIZE)
1217
    """
1218

1219
    my_logger = set_logger(__name__)
1✔
1220
    if ws is None:
1✔
1221
        ws = [signal_width + 20, signal_width + 30]
×
1222

1223
    my_logger.info('\n\t  ======================= extract_spectrum_from_image =============================')
1✔
1224
    my_logger.info(
1✔
1225
        f'\n\tExtracting spectrum from image: spectrum with width 2*{signal_width:.0f} pixels '
1226
        f'and background from {ws[0]:.0f} to {ws[1]:.0f} pixels')
1227

1228
    # Make a data copy
1229
    data = np.copy(image.data_rotated)#[:, 0:right_edge]
1✔
1230
    err = np.copy(image.stat_errors_rotated)#[:, 0:right_edge]
1✔
1231

1232
    # Lateral bands to remove sky background
1233
    Ny, Nx = data.shape
1✔
1234
    y0 = int(image.target_pixcoords_rotated[1])
1✔
1235
    ymax = min(Ny, y0 + ws[1])
1✔
1236
    ymin = max(0, y0 - ws[1])
1✔
1237

1238
    # Roughly estimates the wavelengths and set start 0 nm before parameters.LAMBDA_MIN
1239
    # and end 0 nm after parameters.LAMBDA_MAX
1240
    if spectrum.order < 0:
1✔
1241
        distance = np.sign(spectrum.order) * (np.arange(Nx) - image.target_pixcoords_rotated[0])
×
1242
        lambdas = image.disperser.grating_pixel_to_lambda(distance, x0=image.target_pixcoords, order=spectrum.order)
×
1243
        lambda_min_index = int(np.argmin(np.abs(lambdas[::np.sign(spectrum.order)] - parameters.LAMBDA_MIN)))
×
1244
        lambda_max_index = int(np.argmin(np.abs(lambdas[::np.sign(spectrum.order)] - parameters.LAMBDA_MAX)))
×
1245
        xmin = max(0, int(distance[lambda_min_index]))
×
1246
        xmax = min(right_edge, int(distance[lambda_max_index]) + 1)  # +1 to  include edges
×
1247
    else:
1248
        lambdas = image.disperser.grating_pixel_to_lambda(np.arange(Nx) - image.target_pixcoords_rotated[0],
1✔
1249
                                                          x0=image.target_pixcoords,
1250
                                                          order=spectrum.order)
1251
        xmin = int(np.argmin(np.abs(lambdas - parameters.LAMBDA_MIN)))
1✔
1252
        xmax = int(np.argmin(np.abs(lambdas - parameters.LAMBDA_MAX)))
1✔
1253

1254
    # Create spectrogram
1255
    data = data[ymin:ymax, xmin:xmax]
1✔
1256
    err = err[ymin:ymax, xmin:xmax]
1✔
1257
    Ny, Nx = data.shape
1✔
1258
    my_logger.info(f'\n\tExtract spectrogram: crop rotated image [{xmin}:{xmax},{ymin}:{ymax}] (size ({Nx}, {Ny}))')
1✔
1259

1260
    # Position of the order 0 in the spectrogram coordinates
1261
    target_pixcoords_spectrogram = [image.target_pixcoords_rotated[0] - xmin, image.target_pixcoords_rotated[1] - ymin]
1✔
1262

1263
    # Extract the background on the rotated image
1264
    def bgd_model_func(x, y):
1✔
1265
        x, y = np.atleast_1d(x), np.atleast_1d(y)
×
1266
        return np.zeros((y.size, x.size))
×
1267
    if parameters.SPECTRACTOR_BACKGROUND_SUBTRACTION:
1✔
1268
        bgd_model_func, bgd_res, bgd_rms = extract_spectrogram_background_sextractor(data, err, ws=ws, mask_signal_region=True)
1✔
1269
        # while np.nanmean(bgd_res)/np.nanstd(bgd_res) < -0.2 and parameters.PIXWIDTH_BOXSIZE >= 5:
1270
        while (np.abs(np.nanmean(bgd_res)) > 0.5 or np.nanstd(bgd_res) > 1.3) and parameters.PIXWIDTH_BOXSIZE > 5:
1✔
1271
            parameters.PIXWIDTH_BOXSIZE = max(5, parameters.PIXWIDTH_BOXSIZE // 2)
1✔
1272
            my_logger.debug(f"\n\tPull distribution of background residuals differs too much from mean=0 and std=1. "
1✔
1273
                            f"\n\t\tmean={np.nanmean(bgd_res):.3g}; std={np.nanstd(bgd_res):.3g}"
1274
                            f"\n\tThese value should be smaller in absolute value than 0.5 and 1.3. "
1275
                            f"\n\tTo do so, parameters.PIXWIDTH_BOXSIZE is divided "
1276
                            f"by 2 from {parameters.PIXWIDTH_BOXSIZE * 2} -> {parameters.PIXWIDTH_BOXSIZE}.")
1277
            bgd_model_func, bgd_res, bgd_rms = extract_spectrogram_background_sextractor(data, err, ws=ws, mask_signal_region=True)
1✔
1278

1279
        # Propagate background uncertainties
1280
        err = np.sqrt(err * err + bgd_rms * bgd_rms)
1✔
1281

1282
    # Fit the transverse profile
1283
    my_logger.info('\n\t  ======================= Fit the transverse profile =============================')
1✔
1284

1285
    my_logger.info(f'\n\tStart PSF1D transverse fit...')
1✔
1286
    psf = load_PSF(psf_type=parameters.PSF_TYPE, target=image.target, clip=False)
1✔
1287
    s = ChromaticPSF(psf, Nx=Nx, Ny=Ny, x0=target_pixcoords_spectrogram[0], y0=target_pixcoords_spectrogram[1],
1✔
1288
                     deg=parameters.PSF_POLY_ORDER, saturation=image.saturation)
1289
    verbose = copy.copy(parameters.VERBOSE)
1✔
1290
    debug = copy.copy(parameters.DEBUG)
1✔
1291
    parameters.VERBOSE = False
1✔
1292
    parameters.DEBUG = False
1✔
1293
    s.fit_transverse_PSF1D_profile(data, err, signal_width, ws, pixel_step=parameters.PSF_PIXEL_STEP_TRANSVERSE_FIT,
1✔
1294
                                   sigma_clip=5, bgd_model_func=bgd_model_func, saturation=image.saturation,
1295
                                   live_fit=False)
1296
    parameters.VERBOSE = verbose
1✔
1297
    parameters.DEBUG = debug
1✔
1298

1299
    # Fill spectrum object
1300
    spectrum.pixels = np.arange(xmin, xmax, 1).astype(int)
1✔
1301
    spectrum.data = np.copy(s.table['amplitude'])
1✔
1302
    spectrum.err = np.copy(s.table['flux_err'])
1✔
1303
    my_logger.debug(f'\n\tTransverse fit table:\n{s.table}')
1✔
1304
    if parameters.DEBUG:
1✔
1305
        s.plot_summary()
1✔
1306

1307
    # Fit the data:
1308
    method = "noprior"
1✔
1309
    mode = "1D"
1✔
1310

1311
    my_logger.info('\n\t  ======================= ChromaticPSF polynomial fit  =============================')
1✔
1312

1313
    my_logger.info(f'\n\tStart ChromaticPSF polynomial fit with '
1✔
1314
                   f'mode={mode} and amplitude_priors_method={method}...')
1315
    w = s.fit_chromatic_psf(data, bgd_model_func=bgd_model_func, data_errors=err,
1✔
1316
                            amplitude_priors_method=method, mode=mode, verbose=parameters.VERBOSE)
1317

1318
    Dx_rot = spectrum.pixels.astype(float) - image.target_pixcoords_rotated[0]
1✔
1319
    s.table['Dx'] = np.copy(Dx_rot)
1✔
1320
    s.table['Dy'] = s.table['y_c'] - (image.target_pixcoords_rotated[1] - ymin)
1✔
1321
    s.table['Dy_disp_axis'] = 0
1✔
1322
    s.table['Dy_fwhm_inf'] = s.table['Dy'] - 0.5 * s.table['fwhm']
1✔
1323
    s.table['Dy_fwhm_sup'] = s.table['Dy'] + 0.5 * s.table['fwhm']
1✔
1324
    my_logger.debug(f"\n\tTransverse fit table before derotation:"
1✔
1325
                    f"\n{s.table[['amplitude', 'x_c', 'y_c', 'Dx', 'Dy', 'Dy_disp_axis']]}")
1326

1327
    # Rotate, crop and save the table
1328
    s.rotate_table(-image.rotation_angle)
1✔
1329
    extra_pixels = int(np.max(s.table['Dx']) + image.target_pixcoords[0] - right_edge + 1)  # spectrum pixels outside CCD in rotated image
1✔
1330
    new_Nx = len(s.table['Dx']) - extra_pixels
1✔
1331
    if extra_pixels > 0:
1✔
1332
        my_logger.info(f"\n\tCrop table of size {len(s.table)=} to {new_Nx=} first values "
×
1333
                       f"to remove data fitted outside the CCD region in the rotated image.")
1334
        s.crop_table(new_Nx=new_Nx)
×
1335
    spectrum.data = np.copy(w.amplitude_params[:new_Nx])
1✔
1336
    spectrum.err = np.copy(w.amplitude_params_err[:new_Nx])
1✔
1337
    spectrum.cov_matrix = np.copy(w.amplitude_cov_matrix[:new_Nx, :new_Nx])
1✔
1338
    spectrum.chromatic_psf = s
1✔
1339

1340
    # Extract the spectrogram edges
1341
    data = np.copy(image.data)[:, 0:right_edge]
1✔
1342
    err = np.copy(image.stat_errors)[:, 0:right_edge]
1✔
1343
    Ny, Nx = data.shape
1✔
1344
    x0 = int(image.target_pixcoords[0])
1✔
1345
    y0 = int(image.target_pixcoords[1])
1✔
1346
    ymax = min(Ny, y0 + int(s.table['Dy_disp_axis'].max()) + ws[1] + 1)  # +1 to  include edges
1✔
1347
    ymin = max(0, y0 + int(s.table['Dy_disp_axis'].min()) - ws[1])
1✔
1348
    distance = s.get_algebraic_distance_along_dispersion_axis()
1✔
1349
    lambdas = image.disperser.grating_pixel_to_lambda(distance, x0=image.target_pixcoords, order=spectrum.order)
1✔
1350
    lambda_min_index = int(np.argmin(np.abs(lambdas[::np.sign(spectrum.order)] - parameters.LAMBDA_MIN)))
1✔
1351
    lambda_max_index = int(np.argmin(np.abs(lambdas[::np.sign(spectrum.order)] - parameters.LAMBDA_MAX)))
1✔
1352
    xmin = max(0, int(s.table['Dx'][lambda_min_index] + x0))
1✔
1353
    xmax = min(right_edge, int(s.table['Dx'][lambda_max_index] + x0) + 1)  # +1 to  include edges
1✔
1354
    # Position of the order 0 in the spectrogram coordinates
1355
    target_pixcoords_spectrogram = [image.target_pixcoords[0] - xmin, image.target_pixcoords[1] - ymin]
1✔
1356
    s.y0 = target_pixcoords_spectrogram[1]
1✔
1357
    s.x0 = target_pixcoords_spectrogram[0]
1✔
1358

1359
    # Update y_c and x_c after rotation
1360
    s.table['y_c'] = s.table['Dy'] + target_pixcoords_spectrogram[1]
1✔
1361
    s.table['x_c'] = s.table['Dx'] + target_pixcoords_spectrogram[0]
1✔
1362
    my_logger.debug(f"\n\tTransverse fit table after derotation:"
1✔
1363
                    f"\n{s.table[['amplitude', 'x_c', 'y_c', 'Dx', 'Dy', 'Dy_disp_axis']]}")
1364

1365
    # Create spectrogram
1366
    data = data[ymin:ymax, xmin:xmax]
1✔
1367
    err = err[ymin:ymax, xmin:xmax]
1✔
1368
    Ny, Nx = data.shape
1✔
1369
    my_logger.info(f'\n\tExtract spectrogram: crop raw image [{xmin}:{xmax},{ymin}:{ymax}] (size ({Nx}, {Ny}))')
1✔
1370

1371
    # Extract the non rotated background
1372
    my_logger.info('\n\t  ======================= Extract the non rotated background  =============================')
1✔
1373
    if parameters.SPECTRACTOR_BACKGROUND_SUBTRACTION:
1✔
1374
        bgd_model_func, bgd_res, bgd_rms = extract_spectrogram_background_sextractor(data, err, ws=ws, Dy_disp_axis=s.table['y_c'])
1✔
1375
        bgd = bgd_model_func(np.arange(Nx), np.arange(Ny))
1✔
1376
        my_logger.info(f"\n\tBackground statistics: mean={np.nanmean(bgd):.3f} {image.units}, "
1✔
1377
                   f"RMS={np.nanmean(bgd_rms):.3f} {image.units}.")
1378

1379
        # Propagate background uncertainties
1380
        err = np.sqrt(err * err + bgd_rms * bgd_rms)
1✔
1381
        spectrum.spectrogram_bgd = bgd
1✔
1382
        spectrum.spectrogram_bgd_rms = bgd_rms
1✔
1383

1384
    # First guess for lambdas
1385

1386
    my_logger.info('\n\t  ======================= first guess for lambdas  =============================')
1✔
1387

1388
    first_guess_lambdas = image.disperser.grating_pixel_to_lambda(s.get_algebraic_distance_along_dispersion_axis(),
1✔
1389
                                                                  x0=image.target_pixcoords, order=spectrum.order)
1390
    s.table['lambdas'] = first_guess_lambdas
1✔
1391
    spectrum.lambdas = np.array(first_guess_lambdas)
1✔
1392

1393
    # Position of the order 0 in the spectrogram coordinates
1394
    my_logger.info(f'\n\tExtract spectrogram: crop image [{xmin}:{xmax},{ymin}:{ymax}] (size ({Nx}, {Ny}))'
1✔
1395
                   f'\n\tNew target position in spectrogram frame: {target_pixcoords_spectrogram}')
1396

1397
    # Save results
1398
    spectrum.spectrogram = data
1✔
1399
    spectrum.spectrogram_err = err
1✔
1400
    spectrum.spectrogram_x0 = target_pixcoords_spectrogram[0]
1✔
1401
    spectrum.spectrogram_y0 = target_pixcoords_spectrogram[1]
1✔
1402
    spectrum.spectrogram_xmin = xmin
1✔
1403
    spectrum.spectrogram_xmax = xmax
1✔
1404
    spectrum.spectrogram_ymin = ymin
1✔
1405
    spectrum.spectrogram_ymax = ymax
1✔
1406
    spectrum.spectrogram_Nx = Nx
1✔
1407
    spectrum.spectrogram_Ny = Ny
1✔
1408
    spectrum.spectrogram_deg = spectrum.chromatic_psf.deg
1✔
1409
    spectrum.spectrogram_saturation = spectrum.chromatic_psf.saturation
1✔
1410

1411
    # Plot FHWM(lambda)
1412
    if parameters.DEBUG:
1✔
1413
        fig, ax = plt.subplots(2, 1, figsize=(10, 8), sharex="all")
1✔
1414
        ax[0].plot(spectrum.lambdas, np.array(s.table['fwhm']))
1✔
1415
        ax[0].set_xlabel(r"$\lambda$ [nm]")
1✔
1416
        ax[0].set_ylabel("Transverse FWHM [pixels]")
1✔
1417
        ax[0].set_ylim((0.8 * np.min(s.table['fwhm']), 1.2 * np.max(s.table['fwhm'])))  # [-10:])))
1✔
1418
        ax[0].grid()
1✔
1419
        ax[1].plot(spectrum.lambdas, np.array(s.table['y_c']))
1✔
1420
        ax[1].set_xlabel(r"$\lambda$ [nm]")
1✔
1421
        ax[1].set_ylabel("Distance from mean dispersion axis [pixels]")
1✔
1422
        # ax[1].set_ylim((0.8*np.min(s.table['Dy']), 1.2*np.max(s.table['fwhm'][-10:])))
1423
        ax[1].grid()
1✔
1424
        if parameters.DISPLAY:
1✔
1425
            plt.show()
×
1426
        if parameters.LSST_SAVEFIGPATH:
1✔
1427
            fig.savefig(os.path.join(parameters.LSST_SAVEFIGPATH, 'fwhm.pdf'))
×
1428

1429
    # Summary plot
1430
    if parameters.DEBUG or parameters.LSST_SAVEFIGPATH:
1✔
1431
        gs_kw = dict(width_ratios=[3, 0.08], height_ratios=[1, 1])
1✔
1432
        fig, ax = plt.subplots(2, 2, sharex='none', figsize=(16, 6), gridspec_kw=gs_kw)
1✔
1433
        xx = np.arange(s.table['Dx'].size)
1✔
1434
        plot_image_simple(ax[1, 0], data=data, scale="symlog", title='', units=image.units, aspect='auto', cax=ax[1, 1])
1✔
1435
        ax[1, 0].plot(xx, target_pixcoords_spectrogram[1] + s.table['Dy_disp_axis'], label='Dispersion axis', color="r")
1✔
1436
        ax[1, 0].scatter(xx, target_pixcoords_spectrogram[1] + s.table['Dy'],
1✔
1437
                         c=s.table['lambdas'], edgecolors='None', cmap=from_lambda_to_colormap(s.table['lambdas']),
1438
                         label='Fitted spectrum centers', marker='o', s=10)
1439
        ax[1, 0].plot(xx, target_pixcoords_spectrogram[1] + s.table['Dy_fwhm_inf'], 'k-', label='Fitted FWHM')
1✔
1440
        ax[1, 0].plot(xx, target_pixcoords_spectrogram[1] + s.table['Dy_fwhm_sup'], 'k-', label='')
1✔
1441
        # ax[1, 0].set_ylim(0.5 * Ny - signal_width, 0.5 * Ny + signal_width)
1442
        ax[1, 0].set_xlim(0, xx.size)
1✔
1443
        ax[1, 0].legend(loc='best')
1✔
1444
        plot_spectrum_simple(ax[0, 0], spectrum.lambdas, spectrum.data, data_err=spectrum.err,
1✔
1445
                             units=image.units, label='Fitted spectrum')
1446
        ax[0, 0].plot(spectrum.lambdas, s.table['flux_sum'], 'k-', label='Cross spectrum')
1✔
1447
        ax[1, 0].set_xlabel(r"$\lambda$ [nm]")
1✔
1448
        ax[0, 0].legend(loc='best')
1✔
1449
        fig.tight_layout()
1✔
1450
        # fig.subplots_adjust(hspace=0)
1451
        pos0 = ax[0, 0].get_position()
1✔
1452
        pos1 = ax[1, 0].get_position()
1✔
1453
        ax[0, 0].set_position([pos1.x0, pos0.y0, pos1.width, pos0.height])
1✔
1454
        ax[0, 1].remove()
1✔
1455
        if parameters.DISPLAY:
1✔
1456
            plt.show()
×
1457
        if parameters.LSST_SAVEFIGPATH:
1✔
1458
            fig.savefig(os.path.join(parameters.LSST_SAVEFIGPATH, 'intermediate_spectrum.pdf'))
×
1459

1460
    return w, bgd_model_func
1✔
1461

1462

1463
def run_spectrogram_deconvolution_psf2d(spectrum, bgd_model_func):
1✔
1464
    """Get the spectrum from a 2D PSF deconvolution of the spectrogram.
1465

1466
    Parameters
1467
    ----------
1468
    spectrum: Spectrum
1469
    bgd_model_func: callable
1470

1471
    Returns
1472
    -------
1473
    w: ChromaticPSFFitWorkspace
1474

1475
    """
1476
    my_logger = set_logger(__name__)
1✔
1477
    s = spectrum.chromatic_psf
1✔
1478
    Ny, Nx = spectrum.spectrogram.shape
1✔
1479

1480
    # build 1D priors
1481
    Dx_rot = np.copy(s.table['Dx'])
1✔
1482
    amplitude_priors = np.copy(s.table['amplitude'])
1✔
1483
    amplitude_priors_err = np.copy(s.table['flux_err'])
1✔
1484
    psf_poly_priors = s.from_table_to_poly_params()[s.Nx:]
1✔
1485
    Dy_disp_axis = np.copy(s.table["Dy_disp_axis"])
1✔
1486

1487
    # initialize a new ChromaticPSF
1488
    s = ChromaticPSF(s.psf, Nx=Nx, Ny=Ny, x0=spectrum.spectrogram_x0, y0=spectrum.spectrogram_y0,
1✔
1489
                     deg=s.deg, saturation=s.saturation)
1490

1491
    # fill a first table with first guess
1492
    s.table['Dx'] = (np.arange(spectrum.spectrogram_xmin, spectrum.spectrogram_xmax, 1)
1✔
1493
                     - spectrum.x0[0])[:len(s.table['Dx'])]
1494
    s.table["amplitude"] = np.interp(s.table['Dx'], Dx_rot, amplitude_priors)
1✔
1495
    s.table["flux_sum"] = np.interp(s.table['Dx'], Dx_rot, amplitude_priors)
1✔
1496
    s.table["flux_err"] = np.interp(s.table['Dx'], Dx_rot, amplitude_priors_err)
1✔
1497
    s.table['Dy_disp_axis'] = np.interp(s.table['Dx'], Dx_rot, Dy_disp_axis)
1✔
1498
    s.poly_params = np.concatenate((s.table["amplitude"], psf_poly_priors))
1✔
1499
    s.cov_matrix = np.copy(spectrum.cov_matrix)
1✔
1500
    s.profile_params = s.from_poly_params_to_profile_params(s.poly_params, apply_bounds=True)
1✔
1501
    s.fill_table_with_profile_params(s.profile_params)
1✔
1502
    s.from_profile_params_to_shape_params(s.profile_params)
1✔
1503
    s.table['Dy'] = s.table['y_c'] - spectrum.spectrogram_y0
1✔
1504

1505
    # deconvolve and regularize with 1D priors
1506
    method = "psf1d"
1✔
1507
    mode = "2D"
1✔
1508
    my_logger.debug(f"\n\tTransverse fit table before PSF_2D fit:"
1✔
1509
                    f"\n{s.table[['amplitude', 'x_c', 'y_c', 'Dx', 'Dy', 'Dy_disp_axis']]}")
1510
    my_logger.info(f'\n\tStart ChromaticPSF polynomial fit with '
1✔
1511
                   f'mode={mode} and amplitude_priors_method={method}...')
1512
    data = spectrum.spectrogram
1✔
1513
    err = spectrum.spectrogram_err
1✔
1514
    w = s.fit_chromatic_psf(data, bgd_model_func=bgd_model_func, data_errors=err, live_fit=False,
1✔
1515
                            amplitude_priors_method=method, mode=mode, verbose=parameters.VERBOSE)
1516

1517
    # save results
1518
    spectrum.spectrogram_fit = s.build_spectrogram_image(s.poly_params, mode=mode)
1✔
1519
    spectrum.spectrogram_residuals = (data - spectrum.spectrogram_fit - bgd_model_func(np.arange(Nx),
1✔
1520
                                                                                       np.arange(Ny))) / err
1521
    lambdas = spectrum.disperser.grating_pixel_to_lambda(s.get_algebraic_distance_along_dispersion_axis(),
1✔
1522
                                                         x0=spectrum.x0, order=spectrum.order)
1523
    s.table['lambdas'] = lambdas
1✔
1524
    spectrum.lambdas = np.array(lambdas)
1✔
1525
    spectrum.data = np.copy(w.amplitude_params)
1✔
1526
    spectrum.err = np.copy(w.amplitude_params_err)
1✔
1527
    spectrum.cov_matrix = np.copy(w.amplitude_cov_matrix)
1✔
1528
    spectrum.pixels = np.copy(s.table['Dx'])
1✔
1529
    s.table['Dy'] = s.table['y_c'] - spectrum.spectrogram_y0
1✔
1530
    s.table['Dy_fwhm_inf'] = s.table['Dy'] - 0.5 * s.table['fwhm']
1✔
1531
    s.table['Dy_fwhm_sup'] = s.table['Dy'] + 0.5 * s.table['fwhm']
1✔
1532
    spectrum.chromatic_psf = s
1✔
1533
    spectrum.header['PSF_REG'] = s.opt_reg
1✔
1534
    spectrum.header['TRACE_R'] = w.trace_r
1✔
1535
    spectrum.header['MEANFWHM'] = np.mean(np.array(s.table['fwhm']))
1✔
1536

1537
    # Plot FHWM(lambda)
1538
    if parameters.DEBUG:
1✔
1539
        fig, ax = plt.subplots(2, 1, figsize=(10, 8), sharex="all")
1✔
1540
        ax[0].plot(spectrum.lambdas, np.array(s.table['fwhm']))
1✔
1541
        ax[0].set_xlabel(r"$\lambda$ [nm]")
1✔
1542
        ax[0].set_ylabel("Transverse FWHM [pixels]")
1✔
1543
        ax[0].set_ylim((0.8 * np.min(s.table['fwhm']), 1.2 * np.max(s.table['fwhm'])))  # [-10:])))
1✔
1544
        ax[0].grid()
1✔
1545
        ax[1].plot(spectrum.lambdas, np.array(s.table['y_c']))
1✔
1546
        ax[1].set_xlabel(r"$\lambda$ [nm]")
1✔
1547
        ax[1].set_ylabel("Distance from mean dispersion axis [pixels]")
1✔
1548
        # ax[1].set_ylim((0.8*np.min(s.table['Dy']), 1.2*np.max(s.table['fwhm'][-10:])))
1549
        ax[1].grid()
1✔
1550
        if parameters.DISPLAY:
1✔
1551
            plt.show()
×
1552
        if parameters.LSST_SAVEFIGPATH:
1✔
1553
            fig.savefig(os.path.join(parameters.LSST_SAVEFIGPATH, 'fwhm_2.pdf'))
×
1554

1555
    return w
1✔
1556

1557

1558
def plot_comparison_truth(spectrum, w):  # pragma: no cover
1559
    s = spectrum.chromatic_psf
1560
    lambdas_truth = np.fromstring(spectrum.header['LBDAS_T'][1:-1], sep=' ')
1561
    psf_poly_truth = np.fromstring(spectrum.header['PSF_P_T'][1:-1], sep=' ', dtype=float)
1562
    deg_truth = int(spectrum.header["PSF_DEG"])
1563
    psf_poly_truth[-1] = spectrum.spectrogram_saturation
1564
    amplitude_truth = np.fromstring(spectrum.header['AMPLIS_T'][1:-1], sep=' ', dtype=float)
1565
    amplitude_truth *= parameters.FLAM_TO_ADURATE * lambdas_truth * np.gradient(lambdas_truth) * parameters.CCD_REBIN
1566
    s0 = ChromaticPSF(s.psf, lambdas_truth.size, s.Ny, deg=deg_truth,
1567
                      saturation=spectrum.spectrogram_saturation)
1568
    s0.poly_params = np.asarray(list(amplitude_truth) + list(psf_poly_truth))
1569
    # s0.deg = (len(s0.poly_params[s0.Nx:]) - 1) // ((len(s0.psf.param_names) - 2) - 1) // 2
1570
    # s0.set_polynomial_degrees(s0.deg)
1571
    s0.profile_params = s0.from_poly_params_to_profile_params(s0.poly_params)
1572
    s0.from_profile_params_to_shape_params(s0.profile_params)
1573

1574
    gs_kw = dict(width_ratios=[2, 1], height_ratios=[2, 1])
1575
    fig, ax = plt.subplots(2, 2, figsize=(11, 5), sharex="all", gridspec_kw=gs_kw)
1576
    ax[0, 0].plot(lambdas_truth, amplitude_truth, label="truth")
1577
    amplitude_priors_err = [np.sqrt(w.amplitude_priors_cov_matrix[x, x]) for x in range(w.Nx)]
1578
    ax[0, 0].errorbar(spectrum.lambdas, w.amplitude_priors, yerr=amplitude_priors_err, label="prior")
1579
    ax[0, 0].errorbar(spectrum.lambdas, w.amplitude_params, yerr=w.amplitude_params_err, label="2D")
1580
    ax[0, 0].grid()
1581
    ax[0, 0].legend()
1582
    ax[0, 0].set_xlabel(r"$\lambda$ [nm]")
1583
    ax[0, 0].set_ylabel(f"Amplitude $A$ [ADU/s]")
1584
    ax[1, 0].plot(lambdas_truth, np.zeros_like(lambdas_truth), label="truth")
1585
    amplitude_truth_interp = np.interp(spectrum.lambdas, lambdas_truth, amplitude_truth)
1586
    res = (w.amplitude_priors - amplitude_truth_interp) / amplitude_priors_err
1587
    ax[1, 0].errorbar(spectrum.lambdas, res, yerr=np.ones_like(res),
1588
                      label=f"prior: mean={np.mean(res):.2f}, std={np.std(res):.2f}")
1589
    res = (w.amplitude_params - amplitude_truth_interp) / w.amplitude_params_err
1590
    ax[1, 0].errorbar(spectrum.lambdas, res, yerr=np.ones_like(res),
1591
                      label=f"2D: mean={np.mean(res):.2f}, std={np.std(res):.2f}")
1592
    ax[1, 0].grid()
1593
    ax[1, 0].legend()
1594
    ax[1, 0].set_xlabel(r"$\lambda$ [nm]")
1595
    ax[1, 0].set_ylim(-5, 5)
1596
    ax[1, 0].set_ylabel(r"$(A - A_{\rm truth})/\sigma_A$")
1597

1598
    fwhm_truth = np.interp(spectrum.lambdas, lambdas_truth, s0.table["fwhm"]) / parameters.CCD_REBIN
1599
    # fwhm_prior = np.interp(spectrum.lambdas, np.arange(w.amplitude_priors.size), w.fwhm_priors)
1600
    # fwhm_1d = np.interp(np.arange(len(s.table)), np.arange(w.fwhm_1d.size), w.fwhm_1d)
1601
    # fwhm_2d = np.interp(np.arange(len(s.table)), np.arange(s.Nx), fwhm_1d)
1602
    ax[0, 1].plot(lambdas_truth, s0.table["fwhm"], label="truth")
1603
    ax[0, 1].plot(spectrum.lambdas, w.fwhm_priors, label="prior")
1604
    ax[0, 1].plot(spectrum.lambdas, s.table["fwhm"], label="fit")
1605
    ax[0, 1].grid()
1606
    ax[0, 1].set_ylim(0, 10)
1607
    ax[0, 1].legend()
1608
    ax[0, 1].set_xlabel(r"$\lambda$ [nm]")
1609
    ax[1, 1].set_xlabel(r"$\lambda$ [nm]")
1610
    ax[0, 1].set_ylabel(f"FWHM [pix]")
1611
    ax[1, 1].set_ylabel(r"FWHM - FWHM$_{\rm truth}$ [pix]")
1612
    ax[1, 1].plot(lambdas_truth, np.zeros_like(lambdas_truth), label="truth")
1613
    ax[1, 1].plot(spectrum.lambdas, w.fwhm_priors - fwhm_truth, label="prior")
1614
    ax[1, 1].plot(spectrum.lambdas, s.table["fwhm"] - fwhm_truth, label="2D")
1615
    ax[1, 1].grid()
1616
    ax[1, 1].set_ylim(-0.5, 0.5)
1617
    ax[1, 1].legend()
1618
    plt.subplots_adjust(hspace=0)
1619
    fig.tight_layout()
1620
    if parameters.DISPLAY:
1621
        plt.show()
1622
    if parameters.LSST_SAVEFIGPATH:
1623
        fig.savefig(os.path.join(parameters.LSST_SAVEFIGPATH, 'deconvolution_truth.pdf'), transparent=True)
1624
    plt.show()
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

© 2025 Coveralls, Inc