dkist-processing-cryonirsp 1.4.20__py3-none-any.whl → 1.14.9rc1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. changelog/232.misc.rst +1 -0
  2. dkist_processing_cryonirsp/__init__.py +1 -0
  3. dkist_processing_cryonirsp/codecs/fits.py +1 -0
  4. dkist_processing_cryonirsp/config.py +5 -1
  5. dkist_processing_cryonirsp/models/beam_boundaries.py +1 -0
  6. dkist_processing_cryonirsp/models/constants.py +31 -30
  7. dkist_processing_cryonirsp/models/exposure_conditions.py +6 -5
  8. dkist_processing_cryonirsp/models/fits_access.py +40 -0
  9. dkist_processing_cryonirsp/models/parameters.py +14 -26
  10. dkist_processing_cryonirsp/models/tags.py +1 -0
  11. dkist_processing_cryonirsp/models/task_name.py +1 -0
  12. dkist_processing_cryonirsp/parsers/check_for_gains.py +1 -0
  13. dkist_processing_cryonirsp/parsers/cryonirsp_l0_fits_access.py +40 -47
  14. dkist_processing_cryonirsp/parsers/cryonirsp_l1_fits_access.py +1 -0
  15. dkist_processing_cryonirsp/parsers/exposure_conditions.py +14 -13
  16. dkist_processing_cryonirsp/parsers/map_repeats.py +1 -0
  17. dkist_processing_cryonirsp/parsers/measurements.py +29 -16
  18. dkist_processing_cryonirsp/parsers/modstates.py +5 -1
  19. dkist_processing_cryonirsp/parsers/optical_density_filters.py +1 -0
  20. dkist_processing_cryonirsp/parsers/polarimetric_check.py +18 -7
  21. dkist_processing_cryonirsp/parsers/scan_step.py +12 -4
  22. dkist_processing_cryonirsp/parsers/time.py +7 -7
  23. dkist_processing_cryonirsp/parsers/wavelength.py +6 -1
  24. dkist_processing_cryonirsp/tasks/__init__.py +2 -1
  25. dkist_processing_cryonirsp/tasks/assemble_movie.py +1 -0
  26. dkist_processing_cryonirsp/tasks/bad_pixel_map.py +6 -5
  27. dkist_processing_cryonirsp/tasks/beam_boundaries_base.py +12 -11
  28. dkist_processing_cryonirsp/tasks/ci_beam_boundaries.py +1 -0
  29. dkist_processing_cryonirsp/tasks/ci_science.py +1 -0
  30. dkist_processing_cryonirsp/tasks/cryonirsp_base.py +2 -3
  31. dkist_processing_cryonirsp/tasks/dark.py +5 -4
  32. dkist_processing_cryonirsp/tasks/gain.py +7 -6
  33. dkist_processing_cryonirsp/tasks/instrument_polarization.py +17 -16
  34. dkist_processing_cryonirsp/tasks/l1_output_data.py +1 -0
  35. dkist_processing_cryonirsp/tasks/linearity_correction.py +1 -0
  36. dkist_processing_cryonirsp/tasks/make_movie_frames.py +3 -2
  37. dkist_processing_cryonirsp/tasks/mixin/corrections.py +1 -0
  38. dkist_processing_cryonirsp/tasks/mixin/shift_measurements.py +9 -2
  39. dkist_processing_cryonirsp/tasks/parse.py +70 -52
  40. dkist_processing_cryonirsp/tasks/quality_metrics.py +15 -14
  41. dkist_processing_cryonirsp/tasks/science_base.py +8 -6
  42. dkist_processing_cryonirsp/tasks/sp_beam_boundaries.py +2 -1
  43. dkist_processing_cryonirsp/tasks/sp_geometric.py +11 -10
  44. dkist_processing_cryonirsp/tasks/sp_science.py +1 -0
  45. dkist_processing_cryonirsp/tasks/sp_solar_gain.py +15 -12
  46. dkist_processing_cryonirsp/tasks/sp_wavelength_calibration.py +300 -0
  47. dkist_processing_cryonirsp/tasks/write_l1.py +59 -38
  48. dkist_processing_cryonirsp/tests/conftest.py +75 -53
  49. dkist_processing_cryonirsp/tests/header_models.py +62 -11
  50. dkist_processing_cryonirsp/tests/local_trial_workflows/l0_cals_only.py +26 -46
  51. dkist_processing_cryonirsp/tests/local_trial_workflows/l0_to_l1.py +26 -47
  52. dkist_processing_cryonirsp/tests/local_trial_workflows/linearize_only.py +3 -3
  53. dkist_processing_cryonirsp/tests/local_trial_workflows/local_trial_helpers.py +57 -26
  54. dkist_processing_cryonirsp/tests/test_assemble_movie.py +4 -5
  55. dkist_processing_cryonirsp/tests/test_assemble_qualilty.py +5 -1
  56. dkist_processing_cryonirsp/tests/test_bad_pixel_maps.py +4 -5
  57. dkist_processing_cryonirsp/tests/test_ci_beam_boundaries.py +4 -5
  58. dkist_processing_cryonirsp/tests/test_ci_science.py +4 -5
  59. dkist_processing_cryonirsp/tests/test_corrections.py +5 -6
  60. dkist_processing_cryonirsp/tests/test_cryo_base.py +4 -6
  61. dkist_processing_cryonirsp/tests/test_cryo_constants.py +7 -3
  62. dkist_processing_cryonirsp/tests/test_dark.py +7 -8
  63. dkist_processing_cryonirsp/tests/test_fits_access.py +44 -0
  64. dkist_processing_cryonirsp/tests/test_gain.py +7 -8
  65. dkist_processing_cryonirsp/tests/test_instrument_polarization.py +19 -10
  66. dkist_processing_cryonirsp/tests/test_linearity_correction.py +5 -4
  67. dkist_processing_cryonirsp/tests/test_make_movie_frames.py +2 -3
  68. dkist_processing_cryonirsp/tests/test_parameters.py +23 -28
  69. dkist_processing_cryonirsp/tests/test_parse.py +48 -12
  70. dkist_processing_cryonirsp/tests/test_quality.py +2 -3
  71. dkist_processing_cryonirsp/tests/test_sp_beam_boundaries.py +5 -5
  72. dkist_processing_cryonirsp/tests/test_sp_geometric.py +5 -6
  73. dkist_processing_cryonirsp/tests/test_sp_make_movie_frames.py +2 -3
  74. dkist_processing_cryonirsp/tests/test_sp_science.py +4 -5
  75. dkist_processing_cryonirsp/tests/test_sp_solar.py +6 -5
  76. dkist_processing_cryonirsp/tests/{test_sp_dispersion_axis_correction.py → test_sp_wavelength_calibration.py} +11 -29
  77. dkist_processing_cryonirsp/tests/test_trial_create_quality_report.py +1 -1
  78. dkist_processing_cryonirsp/tests/test_workflows.py +1 -0
  79. dkist_processing_cryonirsp/tests/test_write_l1.py +29 -31
  80. dkist_processing_cryonirsp/workflows/__init__.py +1 -0
  81. dkist_processing_cryonirsp/workflows/ci_l0_processing.py +9 -5
  82. dkist_processing_cryonirsp/workflows/sp_l0_processing.py +12 -8
  83. dkist_processing_cryonirsp/workflows/trial_workflows.py +12 -11
  84. dkist_processing_cryonirsp-1.14.9rc1.dist-info/METADATA +552 -0
  85. dkist_processing_cryonirsp-1.14.9rc1.dist-info/RECORD +115 -0
  86. {dkist_processing_cryonirsp-1.4.20.dist-info → dkist_processing_cryonirsp-1.14.9rc1.dist-info}/WHEEL +1 -1
  87. docs/ci_science_calibration.rst +10 -0
  88. docs/conf.py +1 -0
  89. docs/index.rst +1 -0
  90. docs/sp_science_calibration.rst +7 -0
  91. docs/wavelength_calibration.rst +62 -0
  92. dkist_processing_cryonirsp/tasks/sp_dispersion_axis_correction.py +0 -492
  93. dkist_processing_cryonirsp-1.4.20.dist-info/METADATA +0 -452
  94. dkist_processing_cryonirsp-1.4.20.dist-info/RECORD +0 -111
  95. {dkist_processing_cryonirsp-1.4.20.dist-info → dkist_processing_cryonirsp-1.14.9rc1.dist-info}/top_level.txt +0 -0
@@ -1,492 +0,0 @@
1
- """Cryonirsp SP dispersion axis calibration task."""
2
- import warnings
3
-
4
- import numpy as np
5
- from astropy import constants as const
6
- from astropy import units as u
7
- from astropy.coordinates import EarthLocation
8
- from astropy.coordinates.spectral_coordinate import SpectralCoord
9
- from astropy.io import fits
10
- from astropy.time import Time
11
- from astropy.wcs import WCS
12
- from dkist_processing_common.codecs.asdf import asdf_encoder
13
- from dkist_processing_common.tasks.mixin.input_dataset import InputDatasetMixin
14
- from dkist_service_configuration.logging import logger
15
- from scipy.ndimage import gaussian_filter1d
16
- from scipy.optimize import differential_evolution
17
- from scipy.optimize import OptimizeResult
18
- from sunpy.coordinates import HeliocentricInertial
19
-
20
- from dkist_processing_cryonirsp.codecs.fits import cryo_fits_access_decoder
21
- from dkist_processing_cryonirsp.codecs.fits import cryo_fits_array_decoder
22
- from dkist_processing_cryonirsp.models.tags import CryonirspTag
23
- from dkist_processing_cryonirsp.parsers.cryonirsp_l0_fits_access import CryonirspL0FitsAccess
24
- from dkist_processing_cryonirsp.tasks.cryonirsp_base import CryonirspTaskBase
25
-
26
- __all__ = ["SPDispersionAxisCorrection"]
27
-
28
-
29
- class SPDispersionAxisCorrection(CryonirspTaskBase, InputDatasetMixin):
30
- """Task class for correcting the dispersion axis wavelength values.
31
-
32
- Parameters
33
- ----------
34
- recipe_run_id : int
35
- id of the recipe run used to identify the workflow run this task is part of
36
- workflow_name : str
37
- name of the workflow to which this instance of the task belongs
38
- workflow_version : str
39
- version of the workflow to which this instance of the task belongs
40
-
41
- """
42
-
43
- record_provenance = True
44
-
45
- def run(self):
46
- """
47
- Run method for the task.
48
-
49
- For each beam
50
- - Gather 1D characteristic spectrum.
51
- - Compute the theoretical dispersion.
52
- - Load the telluric and non-telluric FTS atlases and grab only the portion of the atlases that pertain to the current observation.
53
- - Shift the preliminary wavelength vector (from the data) so that it generally aligns with the FTS atlas data.
54
- - Account for the speed at which DKIST was moving relative to the sun's center at the time of observation.
55
- - Define fitting bounds and fit the profile using scipy.optimize.differential_evolution.
56
- - Write results to disk.
57
-
58
-
59
- Returns
60
- -------
61
- None
62
- """
63
- spectrum = self.load_1d_char_spectrum()
64
-
65
- solar_header = self.load_header()
66
-
67
- dispersion, order, alpha = self.get_theoretical_dispersion()
68
-
69
- expected_wavelength_vector = self.compute_expected_wavelength_vector(spectrum, solar_header)
70
-
71
- (
72
- telluric_atlas_wave,
73
- telluric_atlas_trans,
74
- solar_atlas_wave_air,
75
- solar_atlas_trans_flipped,
76
- ) = self.load_and_resample_fts_atlas(expected_wavelength_vector)
77
-
78
- (fts_wave, fts_solar, fts_telluric) = self.initial_alignment(
79
- spectrum,
80
- expected_wavelength_vector,
81
- solar_atlas_wave_air,
82
- solar_atlas_trans_flipped,
83
- telluric_atlas_wave,
84
- telluric_atlas_trans,
85
- )
86
-
87
- doppler_shift = self.get_doppler_shift()
88
-
89
- fit_result = self.fit_dispersion_axis_to_FTS(
90
- fts_wave,
91
- fts_telluric,
92
- fts_solar,
93
- dispersion,
94
- alpha,
95
- doppler_shift,
96
- spectrum,
97
- order,
98
- self.constants.grating_constant,
99
- )
100
-
101
- self.write_fit_results(spectrum, order, fit_result)
102
-
103
- def write_fit_results(
104
- self, spectrum: np.ndarray, order: int, fit_result: OptimizeResult
105
- ) -> None:
106
- """Save the fit results to disk to later be used to update the l1 headers."""
107
- updated_headers = {
108
- "CRPIX1": np.size(spectrum) // 2 + 1,
109
- "CRVAL1": fit_result.x[0],
110
- "CDELT1": fit_result.x[1],
111
- "PV1_0": self.constants.grating_constant,
112
- "PV1_1": order,
113
- "PV1_2": fit_result.x[2],
114
- "CRPIX1A": np.size(spectrum) // 2 + 1,
115
- "CRVAL1A": fit_result.x[0],
116
- "CDELT1A": fit_result.x[1],
117
- "PV1_0A": self.constants.grating_constant,
118
- "PV1_1A": order,
119
- "PV1_2A": fit_result.x[2],
120
- }
121
-
122
- self.write(
123
- data=updated_headers,
124
- tags=[CryonirspTag.task_spectral_fit(), CryonirspTag.intermediate()],
125
- encoder=asdf_encoder,
126
- )
127
-
128
- def load_1d_char_spectrum(self) -> np.ndarray:
129
- """Load intermediate 1d characteristic measured_spectra for beam 1."""
130
- # Only fitting with the left beam.
131
- beam = 1
132
- array_generator = self.read(
133
- tags=[CryonirspTag.intermediate_frame(beam=beam), CryonirspTag.task("SOLAR_CHAR_SPEC")],
134
- decoder=cryo_fits_array_decoder,
135
- )
136
-
137
- return next(array_generator)
138
-
139
- def load_header(self) -> fits.header.Header:
140
- """Grab a header from a random solar gain frame to be used to find a rough initial wavelength estimate."""
141
- # Only fitting with the left beam.
142
- beam = 1
143
- solar_tags = [CryonirspTag.linearized_frame(), CryonirspTag.task("SOLAR_GAIN")]
144
- solar_obj = next(
145
- self.read(
146
- tags=solar_tags,
147
- decoder=cryo_fits_access_decoder,
148
- fits_access_class=CryonirspL0FitsAccess,
149
- )
150
- )
151
- solar_header = solar_obj.header
152
- return solar_header
153
-
154
- def get_theoretical_dispersion(self) -> tuple[u.Quantity, int, float]:
155
- """Compute theoretical dispersion value using the following grating equation.
156
-
157
- m = d/lambda * (sin(alpha) + sin(beta))
158
-
159
- where
160
- m = order
161
- d = grating spacing
162
- lambda = wavelength
163
- alpha = incident angle
164
- beta = diffraction angle
165
-
166
- """
167
- wavelength = self.constants.wavelength * u.nanometer
168
- grating_position_angle_phi = np.deg2rad(self.constants.grating_position_deg)
169
- grating_littrow_angle_theta = np.deg2rad(self.constants.grating_littrow_angle_deg)
170
- alpha = grating_position_angle_phi + grating_littrow_angle_theta
171
- beta = grating_position_angle_phi - grating_littrow_angle_theta
172
- grating_spacing_distance = (1.0 / self.constants.grating_constant) * u.m
173
- order = int(grating_spacing_distance / wavelength * (np.sin(alpha) + np.sin(beta)))
174
- camera_mirror_focal_length = self.parameters.camera_mirror_focal_length_mm
175
- pixpitch = self.parameters.pixel_pitch_micron
176
- linear_disp = order / (grating_spacing_distance * np.cos(beta)) * camera_mirror_focal_length
177
- theoretical_dispersion = (pixpitch / linear_disp).to(u.nanometer)
178
-
179
- return theoretical_dispersion, order, alpha
180
-
181
- def compute_expected_wavelength_vector(
182
- self, spectrum: np.ndarray, header: fits.header.Header
183
- ) -> u.Quantity:
184
- """Compute the expected wavelength vector based on the header information.""" ""
185
- # resample atlases
186
- number_of_wave_pix = np.size(spectrum)
187
- header["CRPIX1"] = number_of_wave_pix // 2 + 1
188
- with warnings.catch_warnings():
189
- warnings.simplefilter("ignore") ## TO ELIMINATE datafix warnings
190
- wcs = WCS(header)
191
- # get wavelength based on header info (not fully accurate)
192
- expected_wavelength_vector = wcs.spectral.pixel_to_world(
193
- np.arange(number_of_wave_pix)
194
- ).to(u.nm)
195
-
196
- return expected_wavelength_vector
197
-
198
- def load_and_resample_fts_atlas(
199
- self, expected_wavelength_vector: u.Quantity
200
- ) -> tuple[u.Quantity, np.ndarray, u.Quantity, np.ndarray]:
201
- """Load telluric and non-telluric FTS atlas data, resample both atlases to be on the same, linear wavelength grid, select the portion of atlas that pertains to bandpass used."""
202
- solar_atlas_wavelength, solar_atlas_transmission = self.parameters.solar_atlas
203
- solar_atlas_wavelength = solar_atlas_wavelength * u.nm
204
- telluric_atlas_wavelength, telluric_atlas_transmission = self.parameters.telluric_atlas
205
- telluric_atlas_wavelength = telluric_atlas_wavelength * u.nm
206
-
207
- expected_wavelength_range = (
208
- expected_wavelength_vector.max() - expected_wavelength_vector.min()
209
- )
210
- min_wavelength = expected_wavelength_vector.min() - 0.25 * expected_wavelength_range
211
- max_wavelength = expected_wavelength_vector.max() + 0.25 * expected_wavelength_range
212
-
213
- cropped_telluric_mask = (telluric_atlas_wavelength > min_wavelength) * (
214
- telluric_atlas_wavelength < max_wavelength
215
- )
216
- telluric_atlas_wavelength = telluric_atlas_wavelength[cropped_telluric_mask]
217
- telluric_atlas_transmission = telluric_atlas_transmission[cropped_telluric_mask]
218
-
219
- cropped_solar_mask = (solar_atlas_wavelength > min_wavelength) * (
220
- solar_atlas_wavelength < max_wavelength
221
- )
222
- solar_atlas_wavelength = solar_atlas_wavelength[cropped_solar_mask]
223
- solar_atlas_transmission = solar_atlas_transmission[cropped_solar_mask]
224
-
225
- return (
226
- telluric_atlas_wavelength,
227
- telluric_atlas_transmission,
228
- solar_atlas_wavelength,
229
- solar_atlas_transmission,
230
- )
231
-
232
- def location_of_dkist(self) -> EarthLocation:
233
- """
234
- Return hard-coded EarthLocation of the DKIST.
235
-
236
- Cartesian geocentric coordinates of DKIST on Earth as retrieved from https://github.com/astropy/astropy-data/blob/gh-pages/coordinates/sites.json#L755
237
- """
238
- _dkist_site_info = {
239
- "aliases": ["DKIST", "ATST"],
240
- "name": "Daniel K. Inouye Solar Telescope",
241
- "elevation": 3067,
242
- "elevation_unit": "meter",
243
- "latitude": 20.7067,
244
- "latitude_unit": "degree",
245
- "longitude": 203.7436,
246
- "longitude_unit": "degree",
247
- "timezone": "US/Hawaii",
248
- "source": "DKIST website: https://www.nso.edu/telescopes/dki-solar-telescope/",
249
- }
250
- location_of_dkist = EarthLocation.from_geodetic(
251
- _dkist_site_info["longitude"] * u.Unit(_dkist_site_info["longitude_unit"]),
252
- _dkist_site_info["latitude"] * u.Unit(_dkist_site_info["latitude_unit"]),
253
- _dkist_site_info["elevation"] * u.Unit(_dkist_site_info["elevation_unit"]),
254
- )
255
- return location_of_dkist
256
-
257
- def get_doppler_shift(self) -> u.Quantity:
258
- """Find the speed at which DKIST is moving relative to the Sun's center.
259
-
260
- Positive values refer to when DKIST is moving away from the sun.
261
- """
262
- coord = self.location_of_dkist().get_gcrs(
263
- obstime=Time(self.constants.solar_gain_ip_start_time)
264
- )
265
- heliocentric_coord = coord.transform_to(
266
- HeliocentricInertial(obstime=Time(self.constants.solar_gain_ip_start_time))
267
- )
268
- obs_vr_kms = heliocentric_coord.d_distance
269
- return obs_vr_kms
270
-
271
- def initial_alignment(
272
- self,
273
- spectrum: np.ndarray,
274
- expected_wavelength_vector: SpectralCoord,
275
- solar_atlas_wave_air: u.Quantity,
276
- solar_atlas_trans_flipped: np.ndarray,
277
- telluric_atlas_wave: u.Quantity,
278
- telluric_atlas_trans: np.ndarray,
279
- ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
280
- """Determine a shift of the preliminary wavelength vector so that it generally aligns with the data itself."""
281
- shifts = np.linspace(-2, 2, 550) * u.nm
282
- merit = np.zeros(len(shifts))
283
- for n, shift in enumerate(shifts):
284
- preliminary_wavelength = expected_wavelength_vector + shift
285
- fts_solar = np.interp(
286
- preliminary_wavelength, solar_atlas_wave_air, solar_atlas_trans_flipped
287
- )
288
- fts_telluric = np.interp(
289
- preliminary_wavelength, telluric_atlas_wave, telluric_atlas_trans
290
- )
291
- # calculate a merit value to be minimized
292
- merit[n] = np.std(spectrum - fts_solar * fts_telluric)
293
-
294
- # get minimum
295
- shift = shifts[np.argmin(merit)]
296
-
297
- # recalculate spectral axis and atlas spectrum for the best shift value
298
- fts_wave = expected_wavelength_vector + shift
299
- fts_solar = np.interp(fts_wave, solar_atlas_wave_air, solar_atlas_trans_flipped)
300
- fts_telluric = np.interp(fts_wave, telluric_atlas_wave, telluric_atlas_trans)
301
-
302
- return fts_wave.value, fts_solar, fts_telluric
303
-
304
- @staticmethod
305
- def fitness(
306
- parameters: np.ndarray,
307
- spectrum: np.ndarray,
308
- fts_wave: np.ndarray,
309
- fts_telluric: np.ndarray,
310
- fts_solar: np.ndarray,
311
- order: int,
312
- grating_constant: float,
313
- doppler_shift: float,
314
- ) -> float:
315
- """
316
- Model function for profile fitting.
317
-
318
- Parameters
319
- ----------
320
- crval1
321
- Wavelength at crpix1
322
-
323
- cdelt1
324
- Spectral dispersion at crpix1
325
-
326
- incident_light_angle
327
- Incident angle in degrees
328
-
329
- resolving_power
330
- Resolving power -- used to estimate the line spread function (may not be correct off limb)
331
-
332
- opacity
333
- Opacity scaling applied to telluric absorption
334
-
335
- stray_light_frac
336
- Inferred straylight fraction in the spectrograph --> This scales the lines non-linearly.
337
-
338
- continuum_amplitude
339
- Amplitude of the scattered light continuum
340
- """
341
- (
342
- crval1,
343
- cdelt1,
344
- incident_light_angle,
345
- resolving_power,
346
- opacity,
347
- stray_light_frac,
348
- continuum_amplitude,
349
- ) = parameters
350
-
351
- # calculate the spectral axis
352
- # Representations of spectral coordinates in FITS
353
- # https://ui.adsabs.harvard.edu/abs/2006A%26A...446..747G/abstract
354
- # https://specreduce.readthedocs.io/en/latest/api/specreduce.utils.synth_data.make_2d_arc_image.html
355
-
356
- number_of_wave_pix = np.size(spectrum)
357
-
358
- non_linear_header = {
359
- "CTYPE1": "AWAV-GRA", # Grating dispersion function with air wavelengths
360
- "CUNIT1": "nm", # Dispersion units
361
- "CRPIX1": number_of_wave_pix // 2 + 1, # Reference pixel [pix]
362
- "PV1_0": grating_constant, # Grating density
363
- "PV1_1": order, # Diffraction order
364
- "CRVAL1": crval1, # Reference value [nm] (<<<< TO BE OPTMIZED <<<<<<)
365
- "CDELT1": cdelt1, # Linear dispersion [nm/pix] (<<<< TO BE OPTMIZED <<<<<<)
366
- "PV1_2": incident_light_angle, # Incident angle [deg] (<<<< TO BE OPTMIZED <<<<<<)
367
- }
368
-
369
- non_linear_wcs = WCS(non_linear_header)
370
- wavelength_vector = (
371
- (non_linear_wcs.spectral.pixel_to_world(np.arange(number_of_wave_pix))).to(u.nm).value
372
- )
373
-
374
- # Gaussian convolution of the FTS atlas
375
- fwhm_wavelength = np.divide(crval1, resolving_power)
376
- sigma_wavelength = fwhm_wavelength / (2.0 * np.sqrt(2.0 * np.log(2)))
377
- kern_pix = sigma_wavelength / np.abs(cdelt1)
378
-
379
- # interpolate the telluric spectral atlas onto the new wavelength axis and scale by the opacity value that is being optimized
380
- fts_atmosphere_interp = np.interp(wavelength_vector, fts_wave, fts_telluric)
381
- fts_telluric_interp = np.exp(opacity * np.log(fts_atmosphere_interp))
382
-
383
- # interpolate the solar spectral atlas onto the new wavelength axis and apply a shift according to the Doppler shift due to orbital motions
384
- fts_solar_interp = np.interp(
385
- wavelength_vector,
386
- fts_wave + doppler_shift / (const.c.to("km/s")).value * crval1,
387
- fts_solar,
388
- )
389
-
390
- # apply telluric absorption spectrum to solar spectrum
391
- fts_modulated = fts_telluric_interp * fts_solar_interp
392
- # add flat value of straylight contamination
393
- fts_modulated_with_straylight = (fts_modulated + stray_light_frac) / (
394
- 1.0 + stray_light_frac
395
- )
396
- # scale for total intensity of the continuum
397
- fit_amplitude = fts_modulated_with_straylight * continuum_amplitude
398
-
399
- # convolution for spectrograph line spread function
400
- fit_amplitude = gaussian_filter1d(fit_amplitude, kern_pix)
401
-
402
- # chisquare calculation for fit metric
403
- res_amplitude = np.sum((spectrum - fit_amplitude) ** 2)
404
-
405
- return res_amplitude
406
-
407
- def fit_dispersion_axis_to_FTS(
408
- self,
409
- fts_wave: np.ndarray,
410
- fts_telluric: np.ndarray,
411
- fts_solar: np.ndarray,
412
- dispersion: u.Quantity,
413
- alpha: float,
414
- doppler_shift: u.Quantity,
415
- spectrum: np.ndarray,
416
- order: int,
417
- grating_constant: float,
418
- ) -> OptimizeResult:
419
- """Define the bounds and send the fitting model on its way."""
420
- parameter_names = (
421
- "crval1 (wavelength at crpix1)",
422
- "cdelt1 (spectral dispersion at crpix1)",
423
- "incident_light_angle",
424
- "resolving_power",
425
- "opacity",
426
- "stray_light_frac",
427
- "continuum_amplitude",
428
- )
429
- crpix1_updated = np.size(spectrum) // 2 + 1
430
- crval1 = fts_wave[crpix1_updated] # initial guess
431
- bounds = [
432
- # [nm[ +\- 0.5 nm range used for finding CRVAL1
433
- (crval1 - 0.5, crval1 + 0.5),
434
- # [nm/pix] 5% bounds on the dispersion at CRPIX1
435
- (
436
- dispersion.value - 0.05 * dispersion.value,
437
- dispersion.value + 0.05 * dispersion.value,
438
- ),
439
- # [radian] Incident angle range is +/- 5 degree from value in header
440
- (np.rad2deg(alpha) - 5, np.rad2deg(alpha) + 5),
441
- # resolving power range
442
- (20000, 125000),
443
- # opacity factor bounds
444
- (0.0, 10),
445
- # straylight fraction
446
- (0.0, 0.5),
447
- # continuum intensity correction
448
- (0.8 * np.nanpercentile(spectrum, 75), 1.2 * np.nanpercentile(spectrum, 75)),
449
- ]
450
-
451
- for repeat_fit in range(5): # repeat just in case the fitting gets stuck in a local minimum
452
- fit_result = differential_evolution(
453
- SPDispersionAxisCorrection.fitness,
454
- args=(
455
- spectrum,
456
- fts_wave,
457
- fts_telluric,
458
- fts_solar,
459
- order,
460
- grating_constant,
461
- doppler_shift.value,
462
- ),
463
- popsize=2,
464
- maxiter=300,
465
- bounds=bounds,
466
- disp=True,
467
- polish=True,
468
- tol=1.0e-9,
469
- )
470
- if fit_result.fun < 0.03:
471
- logger.info(" Convergence good based on fit func value")
472
- break
473
-
474
- logger.info("Fitted Values:")
475
- logger.info(" ")
476
- for p in range(len(parameter_names)):
477
- logger.info(
478
- f"Parameter: {parameter_names[p]}, Fit Result: {fit_result.x[p]}, Bounds: {bounds[p]}"
479
- )
480
-
481
- fit_amplitude = SPDispersionAxisCorrection.fitness(
482
- fit_result.x,
483
- spectrum,
484
- fts_wave,
485
- fts_telluric,
486
- fts_solar,
487
- order,
488
- grating_constant,
489
- doppler_shift.value,
490
- )
491
-
492
- return fit_result