dkist-processing-cryonirsp 1.3.4__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.

Potentially problematic release.


This version of dkist-processing-cryonirsp might be problematic. Click here for more details.

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