• 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

97.06
/spectractor/fit/fit_spectrogram.py
1
import time
1✔
2
import os
1✔
3
import matplotlib.pyplot as plt
1✔
4
import matplotlib as mpl
1✔
5
import numpy as np
1✔
6
from scipy.signal import convolve2d
1✔
7
import copy
1✔
8

9
from spectractor import parameters
1✔
10
from spectractor.config import set_logger
1✔
11
from spectractor.tools import plot_image_simple, from_lambda_to_colormap
1✔
12
from spectractor.extractor.spectrum import Spectrum
1✔
13
from spectractor.simulation.simulator import SpectrogramModel
1✔
14
from spectractor.simulation.atmosphere import Atmosphere, AtmosphereGrid
1✔
15
from spectractor.fit.fitter import (FitWorkspace, FitParameters, run_minimisation, run_minimisation_sigma_clipping,
1✔
16
                                    write_fitparameter_json)
17

18
plot_counter = 0
1✔
19

20

21
class SpectrogramFitWorkspace(FitWorkspace):
1✔
22

23
    def __init__(self, spectrum, atmgrid_file_name="", fit_angstrom_exponent=False,
1✔
24
                 verbose=False, plot=False, live_fit=False, truth=None):
25
        """Class to fit a spectrogram extracted with Spectractor.
26

27
        First the spectrogram is cropped using the parameters.PIXWIDTH_SIGNAL parameter to increase speedness.
28
        The truth parameters are loaded from the file header if provided.
29
        If provided, the atmospheric grid is used for the atmospheric transmission simulations and interpolated
30
        with splines, otherwise Libradtran is called at each step (slower).
31

32
        Parameters
33
        ----------
34
        spectrum: Spectrum
35
            Spectrum object.
36
        atmgrid_file_name: str, optional
37
            Atmospheric grid file name (default: "").
38
        fit_angstrom_exponent: bool, optional
39
            If True, fit angstrom exponent (default: False).
40
        verbose: bool, optional
41
            Verbosity level (default: False).
42
        plot: bool, optional
43
            If True, many plots are produced (default: False).
44
        live_fit: bool, optional
45
            If True, many plots along the fitting procedure are produced to see convergence in live (default: False).
46
        truth: array_like, optional
47
            Array of truth parameters to compare with the best fit result (default: None).
48

49
        Examples
50
        --------
51
        >>> spec = Spectrum('tests/data/reduc_20170530_134_spectrum.fits')
52
        >>> atmgrid_filename = spec.filename.replace('spectrum', 'atmsim')
53
        >>> w = SpectrogramFitWorkspace(spec, atmgrid_file_name=atmgrid_filename, verbose=True, plot=True, live_fit=False)
54
        >>> lambdas, model, model_err = w.simulate(*w.params.p)
55
        >>> w.plot_fit()
56

57
        """
58
        self.spectrum = spectrum
1✔
59
        self.filename = spectrum.filename.replace("spectrum", "spectrogram")
1✔
60
        length = len(self.spectrum.chromatic_psf.table)
1✔
61
        psf_poly_params = self.spectrum.chromatic_psf.from_table_to_poly_params()
1✔
62
        self.spectrum.chromatic_psf.psf.apply_max_width_to_bounds(max_half_width=self.spectrum.spectrogram_Ny)
1✔
63
        self.saturation = self.spectrum.spectrogram_saturation
1✔
64
        p = np.array([1, 1, 0.05, -2, 400, 5, self.spectrum.header['D2CCD'], self.spectrum.header['PIXSHIFT'],
1✔
65
                      0, self.spectrum.rotation_angle, 1])
66
        self.fixed_psf_params = np.array([0, 1, 2, 3, 4, 5, 9])
1✔
67
        self.atm_params_indices = np.array([2, 3, 4, 5])
1✔
68
        self.psf_params_start_index = p.size
1✔
69
        self.psf_poly_params = psf_poly_params[length:]
1✔
70
        psf_poly_params_labels = np.copy(self.spectrum.chromatic_psf.poly_params_labels[length:])
1✔
71
        psf_poly_params_names = np.copy(self.spectrum.chromatic_psf.poly_params_names[length:])
1✔
72
        psf_poly_params_bounds = self.spectrum.chromatic_psf.set_bounds_for_minuit(data=None)
1✔
73
        p = np.concatenate([p, self.psf_poly_params, np.copy(self.psf_poly_params)])
1✔
74
        input_labels = ["A1", "A2", "VAOD", "angstrom_exp_log10", "ozone [db]", "PWV [mm]", r"D_CCD [mm]",
1✔
75
                        r"shift_x [pix]", r"shift_y [pix]", r"angle [deg]", "B"] + \
76
                       list(psf_poly_params_labels) + [label+"_2" for label in psf_poly_params_labels]
77
        axis_names = ["$A_1$", "$A_2$", "VAOD", r'$\log_{10}\"a$', "ozone [db]", "PWV [mm]", r"$D_{CCD}$ [mm]",
1✔
78
                      r"$\Delta_{\mathrm{x}}$ [pix]", r"$\Delta_{\mathrm{y}}$ [pix]", r"$\theta$ [deg]",
79
                      "$B$"] + list(psf_poly_params_names) + [label+"_2" for label in psf_poly_params_names]
80
        bounds = [[0, 2], [0, 2/parameters.GRATING_ORDER_2OVER1], [0, 0.1], [-5, 2], [100, 700], [0, 10],
1✔
81
                  [p[6] - 5 * parameters.DISTANCE2CCD_ERR, p[6] + 5 * parameters.DISTANCE2CCD_ERR], [-2, 2],
82
                  [-10, 10], [-90, 90], [0.8, 1.2]] + list(psf_poly_params_bounds) * 2
83
        fixed = [False] * p.size
1✔
84
        for k, par in enumerate(input_labels):
1✔
85
            if "x_c" in par or "saturation" in par:
1✔
86
                fixed[k] = True
1✔
87
        for k, par in enumerate(input_labels):
1✔
88
            if "y_c" in par:
1✔
89
                fixed[k] = False
1✔
90
                p[k] = 0
1✔
91
        # A2 is free only if spectrogram is a simulation or if the order 2/1 ratio is not known and flat
92
        fixed[1] = "A2_T" not in self.spectrum.header  # not self.spectrum.disperser.flat_ratio_order_2over1
1✔
93
        # fixed[5:7] = [True, True]  # DCCD, x0
94
        fixed[7] = True  # Delta x
1✔
95
        fixed[8] = True  # Delta y
1✔
96
        fixed[9] = True  # angle
1✔
97
        fixed[10] = True  # B
1✔
98
        if not fit_angstrom_exponent:
1✔
99
            fixed[3] = True  # angstrom exponent
1✔
100
        params = FitParameters(p, input_labels=input_labels, axis_names=axis_names, bounds=bounds, fixed=fixed,
1✔
101
                               truth=truth, filename=self.filename)
102
        FitWorkspace.__init__(self, params, verbose=verbose, plot=plot, live_fit=live_fit, file_name=self.filename)
1✔
103
        self.my_logger = set_logger(self.__class__.__name__)
1✔
104
        if atmgrid_file_name == "":
1✔
105
            self.atmosphere = Atmosphere(self.spectrum.airmass, self.spectrum.pressure, self.spectrum.temperature)
1✔
106
        else:
107
            self.use_grid = True
1✔
108
            self.atmosphere = AtmosphereGrid(spectrum_filename=spectrum.filename, atmgrid_filename=atmgrid_file_name)
1✔
109
            self.my_logger.info(f'\n\tUse atmospheric grid models from file {atmgrid_file_name}. ')
1✔
110
        if self.spectrum.spectrogram_Ny > 2 * parameters.PIXDIST_BACKGROUND:
1✔
111
            self.crop_spectrogram()
1✔
112
        self.lambdas = self.spectrum.lambdas
1✔
113
        self.Ny, self.Nx = self.spectrum.spectrogram.shape
1✔
114
        self.data = self.spectrum.spectrogram.flatten()
1✔
115
        self.err = self.spectrum.spectrogram_err.flatten()
1✔
116
        self.fit_angstrom_exponent = fit_angstrom_exponent
1✔
117

118
        if atmgrid_file_name != "":
1✔
119
            self.params.bounds[2] = (min(self.atmosphere.AER_Points), max(self.atmosphere.AER_Points))
1✔
120
            self.params.bounds[4] = (min(self.atmosphere.OZ_Points), max(self.atmosphere.OZ_Points))
1✔
121
            self.params.bounds[5] = (min(self.atmosphere.PWV_Points), max(self.atmosphere.PWV_Points))
1✔
122
            self.params.fixed[3] = True  # angstrom exponent
1✔
123

124
        self.simulation = SpectrogramModel(self.spectrum, atmosphere=self.atmosphere,
1✔
125
                                           with_background=True, fast_sim=False, with_adr=True)
126
        self.lambdas_truth = None
1✔
127
        self.amplitude_truth = None
1✔
128
        self.get_spectrogram_truth()
1✔
129

130
        # PSF cube computation
131
        self.psf_cube_masked = None
1✔
132
        self.psf_cube = None
1✔
133
        self.psf_cube_order2 = None
1✔
134
        self.fix_psf_cube = False
1✔
135
        self.fix_psf_cube_order2 = False
1✔
136

137
        # error matrix
138
        # here image uncertainties are assumed to be uncorrelated
139
        # (which is not exactly true in rotated images)
140
        self.W = 1. / (self.err * self.err)
1✔
141
        self.W = self.W.flatten()
1✔
142

143
        # flat data for fitworkspace
144
        self.data_before_mask = np.copy(self.data)
1✔
145
        self.W_before_mask = np.copy(self.W)
1✔
146
        # create mask
147
        self.set_mask()
1✔
148

149
    def crop_spectrogram(self):
1✔
150
        """Crop the spectrogram in the middle, keeping a vertical width of 2*parameters.PIXWIDTH_SIGNAL around
151
        the signal region.
152

153
        """
154
        bgd_width = parameters.PIXWIDTH_BACKGROUND + parameters.PIXDIST_BACKGROUND - parameters.PIXWIDTH_SIGNAL
1✔
155
        self.spectrum.spectrogram_ymax = self.spectrum.spectrogram_ymax - bgd_width
1✔
156
        self.spectrum.spectrogram_ymin += bgd_width
1✔
157
        self.spectrum.spectrogram_bgd = self.spectrum.spectrogram_bgd[bgd_width:-bgd_width, :]
1✔
158
        self.spectrum.spectrogram = self.spectrum.spectrogram[bgd_width:-bgd_width, :]
1✔
159
        self.spectrum.spectrogram_err = self.spectrum.spectrogram_err[bgd_width:-bgd_width, :]
1✔
160
        self.spectrum.spectrogram_y0 -= bgd_width
1✔
161
        self.spectrum.chromatic_psf.y0 -= bgd_width
1✔
162
        self.spectrum.spectrogram_Ny, self.spectrum.spectrogram_Nx = self.spectrum.spectrogram.shape
1✔
163
        self.spectrum.chromatic_psf.table["y_c"] -= bgd_width
1✔
164
        self.my_logger.debug(f'\n\tSize of the spectrogram region after cropping: '
1✔
165
                             f'({self.spectrum.spectrogram_Nx},{self.spectrum.spectrogram_Ny})')
166

167
    def set_mask(self, params=None):
1✔
168
        """
169

170
        Parameters
171
        ----------
172
        params
173

174
        Returns
175
        -------
176

177
        Examples
178
        --------
179
        >>> spec = Spectrum('tests/data/reduc_20170530_134_spectrum.fits')
180
        >>> w = SpectrogramFitWorkspace(spec, verbose=True)
181
        >>> _ = w.simulate(*w.params.p)
182
        >>> w.plot_fit()
183

184
        """
185
        self.my_logger.info("\n\tReset spectrogram mask with current parameters.")
1✔
186
        if params is None:
1✔
187
            params = self.params.p
1✔
188
        A1, A2, aerosols, angstrom_exponent, ozone, pwv, D, shift_x, shift_y, angle, B, *psf_poly_params = params
1✔
189
        psf_profile_params = self.spectrum.chromatic_psf.from_poly_params_to_profile_params(psf_poly_params,
1✔
190
                                                                                            apply_bounds=True)
191
        self.spectrum.chromatic_psf.from_profile_params_to_shape_params(psf_profile_params)
1✔
192
        Dx = np.arange(len(psf_profile_params[:, 0])) - self.spectrum.spectrogram_x0 - shift_x  # distance in (x,y) spectrogram frame for column x
1✔
193
        Dy_disp_axis = np.tan(angle * np.pi / 180) * Dx  # disp axis height in spectrogram frame for x
1✔
194
        psf_profile_params[:, 0] = 1
1✔
195
        psf_profile_params[:, 1] = Dx + self.spectrum.spectrogram_x0 + shift_x
1✔
196
        psf_profile_params[:, 2] = Dy_disp_axis + (self.spectrum.spectrogram_y0 + shift_y)  # - self.bgd_width
1✔
197
        psf_cube = self.spectrum.chromatic_psf.build_psf_cube(self.simulation.pixels, psf_profile_params,
1✔
198
                                                              fwhmx_clip=3 * parameters.PSF_FWHM_CLIP,
199
                                                              fwhmy_clip=parameters.PSF_FWHM_CLIP, dtype="float32")
200
        self.simulation.psf_cube_masked = psf_cube > 0
1✔
201
        flat_spectrogram = np.sum(self.simulation.psf_cube_masked.reshape(len(psf_profile_params), self.simulation.pixels[0].size),
1✔
202
                                  axis=0)
203
        mask = flat_spectrogram == 0  # < 1e-2 * np.max(flat_spectrogram)
1✔
204
        mask = mask.reshape(self.simulation.pixels[0].shape)
1✔
205
        kernel = np.ones((3, self.spectrum.spectrogram_Nx//10))  # enlarge a bit more the edges of the mask
1✔
206
        mask = convolve2d(mask, kernel, 'same').astype(bool)
1✔
207
        for k in range(self.simulation.psf_cube_masked.shape[0]):
1✔
208
            self.simulation.psf_cube_masked[k] *= ~mask
1✔
209
        mask = mask.reshape((self.simulation.pixels[0].size,))
1✔
210
        self.W = np.copy(self.W_before_mask)
1✔
211
        self.W[mask] = 0
1✔
212
        self.mask = list(np.where(mask)[0])
1✔
213

214
    def get_spectrogram_truth(self):
1✔
215
        """Load the truth parameters (if provided) from the file header.
216

217
        """
218
        if 'A1_T' in list(self.spectrum.header.keys()):
1✔
219
            A1_truth = self.spectrum.header['A1_T']
1✔
220
            A2_truth = self.spectrum.header['A2_T']
1✔
221
            ozone_truth = self.spectrum.header['OZONE_T']
1✔
222
            pwv_truth = self.spectrum.header['PWV_T']
1✔
223
            aerosols_truth = self.spectrum.header['VAOD_T']
1✔
224
            D_truth = self.spectrum.header['D2CCD_T']
1✔
225
            shiftx_truth = 0
1✔
226
            shifty_truth = 0
1✔
227
            rotation_angle = self.spectrum.header['ROT_T']
1✔
228
            B = 1
1✔
229
            poly_truth = np.fromstring(self.spectrum.header['PSF_P_T'][1:-1], sep=' ', dtype=float)
1✔
230
            self.truth = (A1_truth, A2_truth, aerosols_truth, ozone_truth, pwv_truth,
1✔
231
                          D_truth, shiftx_truth, shifty_truth, rotation_angle, B, *poly_truth)
232
            self.lambdas_truth = np.fromstring(self.spectrum.header['LBDAS_T'][1:-1], sep=' ', dtype=float)
1✔
233
            self.amplitude_truth = np.fromstring(self.spectrum.header['AMPLIS_T'][1:-1], sep=' ', dtype=float)
1✔
234
        else:
235
            self.truth = None
1✔
236

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

240
        Parameters
241
        ----------
242
        ax: Axes
243
            Axes instance of shape (4, 2).
244
        title: str, optional
245
            Title for the simulation plot (default: '').
246
        extent: array_like, optional
247
            Extent argument for imshow to crop plots (default: None).
248
        dispersion: bool, optional
249
            If True, plot a colored bar to see the associated wavelength color along the x axis (default: False).
250
        """
251
        cmap_bwr = copy.copy(mpl.colormaps["bwr"])
1✔
252
        cmap_bwr.set_bad(color='lightgrey')
1✔
253
        cmap_viridis = copy.copy(mpl.colormaps["viridis"])
1✔
254
        cmap_viridis.set_bad(color='lightgrey')
1✔
255

256
        data = np.copy(self.data_before_mask)
1✔
257
        if len(self.outliers) > 0 or len(self.mask) > 0:
1✔
258
            bad_indices = np.array(list(self.get_bad_indices()) + list(self.mask)).astype(int)
1✔
259
            data[bad_indices] = np.nan
1✔
260

261
        lambdas = self.spectrum.lambdas
1✔
262
        sub = np.where((lambdas > parameters.LAMBDA_MIN) & (lambdas < parameters.LAMBDA_MAX))[0]
1✔
263
        sub = np.where(sub < self.spectrum.spectrogram_Nx)[0]
1✔
264
        data = data.reshape((self.Ny, self.Nx))
1✔
265
        model = self.model.reshape((self.Ny, self.Nx))
1✔
266
        err = self.err.reshape((self.Ny, self.Nx))
1✔
267
        if extent is not None:
1✔
268
            sub = np.where((lambdas > extent[0]) & (lambdas < extent[1]))[0]
1✔
269
        if len(sub) > 0:
1✔
270
            norm = np.nanmax(data[:, sub])
1✔
271
            plot_image_simple(ax[0, 0], data=data[:, sub] / norm, title='Data', aspect='auto',
1✔
272
                              cax=ax[0, 1], vmin=0, vmax=1, units='1/max(data)', cmap=cmap_viridis)
273
            ax[0, 0].set_title('Data', fontsize=10, loc='center', color='white', y=0.8)
1✔
274
            plot_image_simple(ax[1, 0], data=model[:, sub] / norm, aspect='auto', cax=ax[1, 1], vmin=0, vmax=1,
1✔
275
                              units='1/max(data)', cmap=cmap_viridis)
276
            if dispersion:
1✔
277
                x = self.spectrum.chromatic_psf.table['Dx'][sub[5:-5]] + self.spectrum.spectrogram_x0 - sub[0]
1✔
278
                y = np.ones_like(x)
1✔
279
                ax[1, 0].scatter(x, y, cmap=from_lambda_to_colormap(self.lambdas[sub[5:-5]]), edgecolors='None',
1✔
280
                                 c=self.lambdas[sub[5:-5]],
281
                                 label='', marker='o', s=10)
282
                ax[1, 0].set_xlim(0, model[:, sub].shape[1])
1✔
283
                ax[1, 0].set_ylim(0, model[:, sub].shape[0])
1✔
284
            # p0 = ax.plot(lambdas, self.model(lambdas), label='model')
285
            # # ax.plot(self.lambdas, self.model_noconv, label='before conv')
286
            if title != '':
1✔
287
                ax[1, 0].set_title(title, fontsize=10, loc='center', color='white', y=0.8)
1✔
288
            residuals = (data - model)
1✔
289
            # residuals_err = self.spectrum.spectrogram_err / self.model
290
            norm = np.sqrt(err**2 + self.model_err.reshape((self.Ny, self.Nx))**2)
1✔
291
            residuals /= norm
1✔
292
            std = float(np.nanstd(residuals[:, sub]))
1✔
293
            plot_image_simple(ax[2, 0], data=residuals[:, sub], vmin=-5 * std, vmax=5 * std, title='(Data-Model)/Err',
1✔
294
                              aspect='auto', cax=ax[2, 1], units='', cmap=cmap_bwr)
295
            ax[2, 0].set_title('(Data-Model)/Err', fontsize=10, loc='center', color='black', y=0.8)
1✔
296
            ax[2, 0].text(0.05, 0.05, f'mean={np.nanmean(residuals[:, sub]):.3f}\nstd={np.nanstd(residuals[:, sub]):.3f}',
1✔
297
                          horizontalalignment='left', verticalalignment='bottom',
298
                          color='black', transform=ax[2, 0].transAxes)
299
            ax[0, 0].set_xticks(ax[2, 0].get_xticks()[1:-1])
1✔
300
            ax[0, 1].get_yaxis().set_label_coords(3.5, 0.5)
1✔
301
            ax[1, 1].get_yaxis().set_label_coords(3.5, 0.5)
1✔
302
            ax[2, 1].get_yaxis().set_label_coords(3.5, 0.5)
1✔
303
            ax[3, 1].remove()
1✔
304
            ax[3, 0].plot(self.lambdas[sub], np.nansum(data, axis=0)[sub], label='Data')
1✔
305
            ax[3, 0].plot(self.lambdas[sub], np.nansum(model, axis=0)[sub], label='Model')
1✔
306
            ax[3, 0].set_ylabel('Cross spectrum')
1✔
307
            ax[3, 0].set_xlabel(r'$\lambda$ [nm]')
1✔
308
            ax[3, 0].legend(fontsize=7)
1✔
309
            ax[3, 0].grid(True)
1✔
310

311
    def simulate(self, A1, A2, aerosols, angstrom_exponent_log10, ozone, pwv, D, shift_x, shift_y, angle, B, *psf_poly_params):
1✔
312
        """Interface method to simulate a spectrogram.
313

314
        Parameters
315
        ----------
316
        A1: float
317
            Main amplitude parameter.
318
        A2: float
319
            Relative amplitude of the order 2 spectrogram.
320
        aerosols: float
321
            Vertical Aerosols Optical Depth quantity for Libradtran (no units).
322
        angstrom_exponent_log10: float
323
            Logarithm base 10 of Angstrom exponent for aerosols.
324
        ozone: float
325
            Ozone parameter for Libradtran (in db).
326
        pwv: float
327
            Precipitable Water Vapor quantity for Libradtran (in mm).
328
        D: float
329
            Distance between the CCD and the disperser (in mm).
330
        shift_x: float
331
            Shift of the order 0 position along the X axis (in pixels).
332
        shift_y: float
333
            Shift of the order 0 position along the Y axis (in pixels).
334
        angle: float
335
            Angle of the dispersion axis with respect to the X axis (in degrees).
336
        B: float
337
            Amplitude of the simulated background.
338
        psf_poly_params: array_like
339
            PSF polynomial parameters formatted with the ChromaticPSF class.
340

341
        Returns
342
        -------
343
        lambdas: array_like
344
            Array of wavelengths (1D).
345
        model: array_like
346
            Flat 1D array of the spectrogram simulation.
347
        model_err: array_like
348
            Flat 1D array of the spectrogram simulation uncertainty.
349

350
        Examples
351
        --------
352

353
        >>> spec = Spectrum('tests/data/reduc_20170530_134_spectrum.fits')
354
        >>> w = SpectrogramFitWorkspace(spec, verbose=True)
355
        >>> lambdas, model, model_err = w.simulate(*w.params.p)
356
        >>> w.plot_fit()
357

358
        """
359
        global plot_counter
360
        if self.fit_angstrom_exponent:
1✔
361
            angstrom_exponent = 10 ** angstrom_exponent_log10
×
362
        else:
363
            angstrom_exponent = None
1✔
364
        lambdas, model, model_err = \
1✔
365
            self.simulation.simulate(A1, A2, aerosols, angstrom_exponent, ozone, pwv, D, shift_x, shift_y, angle, B, psf_poly_params)
366
        self.p = np.array([A1, A2, aerosols, angstrom_exponent, ozone, pwv, D, shift_x, shift_y, angle, B] + list(psf_poly_params))
1✔
367
        self.lambdas = lambdas
1✔
368
        self.model = model.flatten()
1✔
369
        self.model_err = model_err.flatten()
1✔
370
        if self.live_fit and (plot_counter % 30) == 0:  # pragma: no cover
371
            self.plot_fit()
372
        plot_counter += 1
1✔
373
        return self.lambdas, self.model, self.model_err
1✔
374

375
    def jacobian(self, params, epsilon, model_input=None):
1✔
376
        start = time.time()
1✔
377
        if model_input is not None:
1✔
378
            lambdas, model, model_err = model_input
1✔
379
        else:
380
            lambdas, model, model_err = self.simulate(*params)
×
381
        model = model.flatten()
1✔
382
        J = np.zeros((params.size, model.size))
1✔
383
        strategy = copy.copy(self.simulation.fix_psf_cube)
1✔
384
        atmosphere = copy.copy(self.simulation.atmosphere_sim)
1✔
385
        for ip, p in enumerate(params):
1✔
386
            if self.params.fixed[ip]:
1✔
387
                continue
1✔
388
            if ip in self.fixed_psf_params:
1✔
389
                self.simulation.fix_psf_cube = True
1✔
390
            else:
391
                self.simulation.fix_psf_cube = False
1✔
392
            if ip in self.atm_params_indices:
1✔
393
                self.simulation.fix_atm_sim = False
1✔
394
            else:
395
                self.simulation.fix_atm_sim = True
1✔
396
            tmp_p = np.copy(params)
1✔
397
            if tmp_p[ip] + epsilon[ip] < self.params.bounds[ip][0] or tmp_p[ip] + epsilon[ip] > self.params.bounds[ip][1]:
1✔
398
                epsilon[ip] = - epsilon[ip]
1✔
399
            tmp_p[ip] += epsilon[ip]
1✔
400
            tmp_lambdas, tmp_model, tmp_model_err = self.simulate(*tmp_p)
1✔
401
            if self.simulation.fix_atm_sim is False:
1✔
402
                self.simulation.atmosphere_sim = atmosphere
1✔
403
            J[ip] = (tmp_model.flatten() - model) / epsilon[ip]
1✔
404
        self.simulation.fix_psf_cube = strategy
1✔
405
        self.simulation.fix_atm_sim = False
1✔
406
        self.my_logger.debug(f"\n\tJacobian time computation = {time.time() - start:.1f}s")
1✔
407
        return J
1✔
408

409
    def plot_fit(self):
1✔
410
        """Plot the fit result.
411

412
        Examples
413
        --------
414

415
        >>> spec = Spectrum('tests/data/reduc_20170530_134_spectrum.fits')
416
        >>> w = SpectrogramFitWorkspace(spec, verbose=True)
417
        >>> lambdas, model, model_err = w.simulate(*w.params.p)
418
        >>> w.plot_fit()
419

420
        .. plot::
421
            :include-source:
422

423
            from spectractor.fit.fit_spectrogram import SpectrogramFitWorkspace
424
            file_name = 'tests/data/reduc_20170530_134_spectrum.fits'
425
            atmgrid_file_name = file_name.replace('spectrum', 'atmsim')
426
            fit_workspace = SpectrogramFitWorkspace(file_name, atmgrid_file_name=atmgrid_file_name, verbose=True)
427
            A1, A2, aerosols, ozone, pwv, D, shift_x, shift_y, angle, *psf = fit_workspace.p
428
            lambdas, model, model_err = fit_workspace.simulation.simulate(A1, A2, aerosols, ozone, pwv, D, shift_x,
429
                                                                          shift_y, angle, psf)
430
            fit_workspace.lambdas = lambdas
431
            fit_workspace.model = model
432
            fit_workspace.model_err = model_err
433
            fit_workspace.plot_fit()
434

435
        """
436
        gs_kw = dict(width_ratios=[3, 0.01, 1, 0.01, 1, 0.15], height_ratios=[1, 1, 1, 1])
1✔
437
        fig, ax = plt.subplots(nrows=4, ncols=6, figsize=(10, 8), gridspec_kw=gs_kw)
1✔
438

439
        # A1, A2, aerosols, ozone, pwv, D, shift_x, shift_y, shift_t, B,  *psf = self.p
440
        # plt.suptitle(f'A1={A1:.3f}, A2={A2:.3f}, PWV={pwv:.3f}, OZ={ozone:.3g}, VAOD={aerosols:.3f}, '
441
        #              f'D={D:.2f}mm, shift_y={shift_y:.2f}pix, B={B:.3f}', y=1)
442
        # main plot
443
        self.plot_spectrogram_comparison_simple(ax[:, 0:2], title='Spectrogram model', dispersion=True)
1✔
444
        # zoom O2
445
        self.plot_spectrogram_comparison_simple(ax[:, 2:4], extent=[730, 800], title='Zoom $O_2$', dispersion=True)
1✔
446
        # zoom H2O
447
        self.plot_spectrogram_comparison_simple(ax[:, 4:6], extent=[870, 1000], title='Zoom $H_2 O$', dispersion=True)
1✔
448
        for i in range(3):  # clear middle colorbars
1✔
449
            for j in range(2):
1✔
450
                plt.delaxes(ax[i, 2*j+1])
1✔
451
        for i in range(4):  # clear middle y axis labels
1✔
452
            for j in range(1, 3):
1✔
453
                ax[i, 2*j].set_ylabel("")
1✔
454
        fig.tight_layout()
1✔
455
        if self.live_fit:  # pragma: no cover
456
            plt.draw()
457
            plt.pause(1e-8)
458
            plt.close()
459
        else:
460
            if parameters.DISPLAY and self.verbose:
1✔
461
                plt.show()
×
462
        if parameters.PdfPages:
1✔
463
            parameters.PdfPages.savefig()
×
464
        if parameters.SAVE:
1✔
465
            figname = os.path.splitext(self.filename)[0] + "_bestfit.pdf"
×
466
            self.my_logger.info(f"\n\tSave figure {figname}.")
×
467
            fig.savefig(figname, dpi=100, bbox_inches='tight', transparent=True)
×
468

469

470
def lnprob_spectrogram(p):  # pragma: no cover
471
    """Logarithmic likelihood function to maximize in MCMC exploration.
472

473
    Parameters
474
    ----------
475
    p: array_like
476
        Array of SpectrogramFitWorkspace parameters.
477

478
    Returns
479
    -------
480
    lp: float
481
        Log of the likelihood function.
482

483
    """
484
    global fit_workspace
485
    lp = fit_workspace.lnprior(p)
486
    if not np.isfinite(lp):
487
        return -1e20
488
    return lp + fit_workspace.lnlike_spectrogram(p)
489

490

491
def run_spectrogram_minimisation(fit_workspace, method="newton"):
1✔
492
    """Interface function to fit spectrogram simulation parameters to data.
493

494
    Parameters
495
    ----------
496
    fit_workspace: SpectrogramFitWorkspace
497
        An instance of the SpectrogramFitWorkspace class.
498
    method: str, optional
499
        Fitting method (default: 'newton').
500

501
    Examples
502
    --------
503
    >>> spec = Spectrum('tests/data/reduc_20170530_134_spectrum.fits')
504
    >>> w = SpectrogramFitWorkspace(spec, verbose=True, atmgrid_file_name='tests/data/reduc_20170530_134_atmsim.fits')
505
    >>> parameters.VERBOSE = True
506
    >>> run_spectrogram_minimisation(w, method="newton")
507

508
    """
509
    my_logger = set_logger(__name__)
1✔
510
    guess = np.asarray(fit_workspace.params.p)
1✔
511
    fit_workspace.simulate(*guess)
1✔
512
    fit_workspace.plot_fit()
1✔
513
    if method != "newton":
1✔
514
        run_minimisation(fit_workspace, method=method)
×
515
    else:
516
        # costs = np.array([fit_workspace.chisq(guess)])
517
        # if parameters.DISPLAY and (parameters.DEBUG or fit_workspace.live_fit):
518
        #     fit_workspace.plot_fit()
519
        # params_table = np.array([guess])
520
        start = time.time()
1✔
521
        my_logger.info(f"\n\tStart guess: {guess}\n\twith {fit_workspace.params.input_labels}")
1✔
522
        epsilon = 1e-4 * guess
1✔
523
        epsilon[epsilon == 0] = 1e-4
1✔
524
        fixed = np.copy(fit_workspace.params.fixed)
1✔
525

526
        # fit_workspace.simulation.fast_sim = True
527
        # fit_workspace.simulation.fix_psf_cube = False
528
        # fit_workspace.fixed = np.copy(fixed)
529
        # fit_workspace.fixed[:fit_workspace.psf_params_start_index] = True
530
        # params_table, costs = run_gradient_descent(fit_workspace, guess, epsilon, params_table, costs,
531
        #                                            fix=fit_workspace.fixed, xtol=1e-3, ftol=1e-2, niter=10)
532

533
        # fit_workspace.simulation.fast_sim = True
534
        # fit_workspace.simulation.fix_psf_cube = False
535
        # fit_workspace.fixed = np.copy(fixed)
536
        # for ip, label in enumerate(fit_workspace.input_labels):
537
        #     if "y_c_0" in label:
538
        #         fit_workspace.fixed[ip] = False
539
        #     else:
540
        #         fit_workspace.fixed[ip] = True
541
        # run_minimisation(fit_workspace, method="newton", epsilon=epsilon, fix=fit_workspace.fixed,
542
        #                  xtol=1e-2, ftol=10 / fit_workspace.data.size, verbose=False)
543

544
        fit_workspace.simulation.fast_sim = False
1✔
545
        fit_workspace.simulation.fix_psf_cube = False
1✔
546
        fit_workspace.params.fixed = np.copy(fixed)
1✔
547
        # guess = fit_workspace.p
548
        # params_table, costs = run_gradient_descent(fit_workspace, guess, epsilon, params_table, costs,
549
        #                                            fix=fit_workspace.fixed, xtol=1e-6, ftol=1 / fit_workspace.data.size,
550
        #                                            niter=40)
551
        run_minimisation_sigma_clipping(fit_workspace, method="newton", epsilon=epsilon,  xtol=1e-6,
1✔
552
                                        ftol=1 / fit_workspace.data.size, sigma_clip=100, niter_clip=3, verbose=False)
553
        my_logger.info(f"\n\tNewton: total computation time: {time.time() - start}s")
1✔
554
        if fit_workspace.filename != "":
1✔
555
            fit_workspace.params.plot_correlation_matrix()
1✔
556
            write_fitparameter_json(fit_workspace.params.json_filename, fit_workspace.params,
1✔
557
                                    extra={"chi2": fit_workspace.costs[-1] / fit_workspace.data.size,
558
                                           "date-obs": fit_workspace.spectrum.date_obs})
559
            # save_gradient_descent(fit_workspace, costs, params_table)
560
            fit_workspace.plot_fit()
1✔
561

562

563
if __name__ == "__main__":
1✔
564
    import doctest
1✔
565

566
    doctest.testmod()
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc