dkist-processing-visp 5.1.2rc2__py3-none-any.whl → 5.2.0rc1__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.
- changelog/234.bugfix.rst +2 -0
- changelog/234.doc.rst +1 -0
- changelog/234.feature.rst +1 -0
- changelog/234.science.rst +2 -0
- changelog/253.misc.rst +1 -0
- dkist_processing_visp/models/parameters.py +43 -1
- dkist_processing_visp/models/tags.py +11 -0
- dkist_processing_visp/models/task_name.py +2 -0
- dkist_processing_visp/tasks/geometric.py +1 -1
- dkist_processing_visp/tasks/science.py +88 -11
- dkist_processing_visp/tasks/solar.py +12 -205
- dkist_processing_visp/tasks/wavelength_calibration.py +430 -0
- dkist_processing_visp/tasks/write_l1.py +2 -0
- dkist_processing_visp/tests/conftest.py +11 -0
- dkist_processing_visp/tests/header_models.py +22 -6
- dkist_processing_visp/tests/local_trial_workflows/l0_cals_only.py +21 -0
- dkist_processing_visp/tests/local_trial_workflows/l0_polcals_as_science.py +21 -0
- dkist_processing_visp/tests/local_trial_workflows/l0_solar_gain_as_science.py +20 -0
- dkist_processing_visp/tests/local_trial_workflows/l0_to_l1.py +21 -0
- dkist_processing_visp/tests/local_trial_workflows/local_trial_helpers.py +27 -0
- dkist_processing_visp/tests/test_parameters.py +11 -5
- dkist_processing_visp/tests/test_science.py +60 -5
- dkist_processing_visp/tests/test_solar.py +0 -1
- dkist_processing_visp/tests/test_wavelength_calibration.py +297 -0
- dkist_processing_visp/tests/test_write_l1.py +0 -2
- dkist_processing_visp/workflows/l0_processing.py +4 -1
- dkist_processing_visp/workflows/trial_workflows.py +4 -1
- {dkist_processing_visp-5.1.2rc2.dist-info → dkist_processing_visp-5.2.0rc1.dist-info}/METADATA +33 -33
- {dkist_processing_visp-5.1.2rc2.dist-info → dkist_processing_visp-5.2.0rc1.dist-info}/RECORD +34 -27
- docs/gain_correction.rst +3 -0
- docs/index.rst +1 -0
- docs/wavelength_calibration.rst +64 -0
- changelog/251.feature.rst +0 -1
- {dkist_processing_visp-5.1.2rc2.dist-info → dkist_processing_visp-5.2.0rc1.dist-info}/WHEEL +0 -0
- {dkist_processing_visp-5.1.2rc2.dist-info → dkist_processing_visp-5.2.0rc1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
"""Visp wavelength calibration task."""
|
|
2
|
+
|
|
3
|
+
import astropy.units as u
|
|
4
|
+
import numpy as np
|
|
5
|
+
from astropy.time import Time
|
|
6
|
+
from astropy.units import Quantity
|
|
7
|
+
from astropy.wcs import WCS
|
|
8
|
+
from dkist_processing_common.codecs.fits import fits_array_decoder
|
|
9
|
+
from dkist_processing_common.codecs.json import json_encoder
|
|
10
|
+
from dkist_processing_common.models.dkist_location import location_of_dkist
|
|
11
|
+
from dkist_processing_common.tasks.mixin.quality import QualityMixin
|
|
12
|
+
from dkist_service_configuration.logging import logger
|
|
13
|
+
from solar_wavelength_calibration import Atlas
|
|
14
|
+
from solar_wavelength_calibration import WavelengthCalibrationFitter
|
|
15
|
+
from solar_wavelength_calibration.fitter.parameters import AngleBoundRange
|
|
16
|
+
from solar_wavelength_calibration.fitter.parameters import BoundsModel
|
|
17
|
+
from solar_wavelength_calibration.fitter.parameters import DispersionBoundRange
|
|
18
|
+
from solar_wavelength_calibration.fitter.parameters import FitFlagsModel
|
|
19
|
+
from solar_wavelength_calibration.fitter.parameters import LengthBoundRange
|
|
20
|
+
from solar_wavelength_calibration.fitter.parameters import UnitlessBoundRange
|
|
21
|
+
from solar_wavelength_calibration.fitter.parameters import WavelengthCalibrationParameters
|
|
22
|
+
from solar_wavelength_calibration.fitter.wavelength_fitter import WavelengthParameters
|
|
23
|
+
from solar_wavelength_calibration.fitter.wavelength_fitter import calculate_initial_crval_guess
|
|
24
|
+
from sunpy.coordinates import HeliocentricInertial
|
|
25
|
+
|
|
26
|
+
from dkist_processing_visp.models.tags import VispTag
|
|
27
|
+
from dkist_processing_visp.tasks.visp_base import VispTaskBase
|
|
28
|
+
|
|
29
|
+
__all__ = ["WavelengthCalibration"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class WavelengthCalibration(VispTaskBase, QualityMixin):
|
|
33
|
+
"""Task class for correcting the dispersion axis wavelength values.
|
|
34
|
+
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
recipe_run_id : int
|
|
38
|
+
id of the recipe run used to identify the workflow run this task is part of
|
|
39
|
+
workflow_name : str
|
|
40
|
+
name of the workflow to which this instance of the task belongs
|
|
41
|
+
workflow_version : str
|
|
42
|
+
version of the workflow to which this instance of the task belongs
|
|
43
|
+
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
record_provenance = True
|
|
47
|
+
|
|
48
|
+
def run(self) -> None:
|
|
49
|
+
"""
|
|
50
|
+
Run method for the task.
|
|
51
|
+
|
|
52
|
+
#. Gather 2D characteristic spectra for beam 1 generated by solar gain task.
|
|
53
|
+
#. Compute spatial median of characteristic spectra as initial spectrum.
|
|
54
|
+
#. Compute the theoretical dispersion and order.
|
|
55
|
+
#. Compute the incident light angle.
|
|
56
|
+
#. Generate the input wavelength vector from the spectrum and instrument parameters.
|
|
57
|
+
#. Estimate the Doppler velocity and set the resolving power.
|
|
58
|
+
#. Define fitting bounds and initialize model parameters.
|
|
59
|
+
#. Set up and run the wavelength calibration fit.
|
|
60
|
+
#. Save the resulting wavelength solution and quality metrics.
|
|
61
|
+
|
|
62
|
+
Returns
|
|
63
|
+
-------
|
|
64
|
+
None
|
|
65
|
+
"""
|
|
66
|
+
with self.telemetry_span("Compute input spectrum and wavelength"):
|
|
67
|
+
logger.info("Loading characteristic spectrum")
|
|
68
|
+
char_spectra = next(
|
|
69
|
+
self.read(
|
|
70
|
+
tags=[
|
|
71
|
+
VispTag.intermediate_frame(beam=1),
|
|
72
|
+
VispTag.task_characteristic_spectra(),
|
|
73
|
+
],
|
|
74
|
+
decoder=fits_array_decoder,
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
logger.info("Calculating input spectrum")
|
|
79
|
+
# average along spatial dimension
|
|
80
|
+
med_char_spectrum = np.nanmedian(char_spectra, axis=1)
|
|
81
|
+
input_spectrum, nan_chop_amount = self.chop_and_clean_NaNs(med_char_spectrum)
|
|
82
|
+
|
|
83
|
+
incident_light_angle = self.constants.incident_light_angle_deg
|
|
84
|
+
logger.info(f"{incident_light_angle = !s}")
|
|
85
|
+
|
|
86
|
+
wavelength = self.constants.wavelength * u.nm
|
|
87
|
+
reflected_light_angle = self.constants.reflected_light_angle_deg
|
|
88
|
+
order = compute_order(
|
|
89
|
+
central_wavelength=wavelength,
|
|
90
|
+
incident_light_angle=incident_light_angle,
|
|
91
|
+
reflected_light_angle=reflected_light_angle,
|
|
92
|
+
grating_constant=self.constants.grating_constant_inverse_mm,
|
|
93
|
+
)
|
|
94
|
+
logger.info(f"{order = }")
|
|
95
|
+
|
|
96
|
+
pixpitch = self.parameters.wavecal_pixel_pitch_micron_per_pix
|
|
97
|
+
dispersion = compute_initial_dispersion(
|
|
98
|
+
central_wavelength=wavelength,
|
|
99
|
+
incident_light_angle=incident_light_angle,
|
|
100
|
+
reflected_light_angle=reflected_light_angle,
|
|
101
|
+
lens_parameters=self.parameters.wavecal_camera_lens_parameters,
|
|
102
|
+
pixel_pitch=pixpitch,
|
|
103
|
+
)
|
|
104
|
+
logger.info(f"{dispersion = !s}")
|
|
105
|
+
|
|
106
|
+
with self.telemetry_span("Compute brute-force CRVAL initial guess"):
|
|
107
|
+
logger.info("Computing initial wavelength vector.")
|
|
108
|
+
input_wavelength_vector = compute_input_wavelength_vector(
|
|
109
|
+
central_wavelength=wavelength,
|
|
110
|
+
dispersion=dispersion,
|
|
111
|
+
grating_constant=self.constants.grating_constant_inverse_mm,
|
|
112
|
+
order=order,
|
|
113
|
+
incident_light_angle=incident_light_angle,
|
|
114
|
+
num_spec_px=input_spectrum.size,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
wavelength_range = input_wavelength_vector[-1] - input_wavelength_vector[0]
|
|
118
|
+
logger.info(f"{wavelength_range = !s}")
|
|
119
|
+
|
|
120
|
+
atlas = Atlas(config=self.parameters.wavecal_atlas_download_config)
|
|
121
|
+
crval_initial_guess = calculate_initial_crval_guess(
|
|
122
|
+
input_wavelength_vector=input_wavelength_vector,
|
|
123
|
+
input_spectrum=input_spectrum,
|
|
124
|
+
atlas=atlas,
|
|
125
|
+
negative_limit=-wavelength_range / 2,
|
|
126
|
+
positive_limit=wavelength_range / 2,
|
|
127
|
+
num_steps=550,
|
|
128
|
+
)
|
|
129
|
+
logger.info(f"{crval_initial_guess = !s}")
|
|
130
|
+
|
|
131
|
+
with self.telemetry_span("Set up wavelength fit"):
|
|
132
|
+
doppler_velocity = get_doppler_velocity(self.constants.solar_gain_ip_start_time)
|
|
133
|
+
logger.info(f"{doppler_velocity = !s}")
|
|
134
|
+
|
|
135
|
+
resolving_power = self.parameters.wavecal_init_resolving_power
|
|
136
|
+
logger.info(f"{resolving_power = }")
|
|
137
|
+
|
|
138
|
+
logger.info("Setting bounds")
|
|
139
|
+
wavelength_search_width = dispersion * self.parameters.wavecal_crval_bounds_px
|
|
140
|
+
bounds = BoundsModel(
|
|
141
|
+
crval=LengthBoundRange(
|
|
142
|
+
min=crval_initial_guess - wavelength_search_width,
|
|
143
|
+
max=crval_initial_guess + wavelength_search_width,
|
|
144
|
+
),
|
|
145
|
+
dispersion=DispersionBoundRange(
|
|
146
|
+
min=dispersion * (1 - self.parameters.wavecal_dispersion_bounds_fraction),
|
|
147
|
+
max=dispersion * (1 + self.parameters.wavecal_dispersion_bounds_fraction),
|
|
148
|
+
),
|
|
149
|
+
incident_light_angle=AngleBoundRange(
|
|
150
|
+
min=incident_light_angle
|
|
151
|
+
- self.parameters.wavecal_incident_light_angle_bounds_deg,
|
|
152
|
+
max=incident_light_angle
|
|
153
|
+
+ self.parameters.wavecal_incident_light_angle_bounds_deg,
|
|
154
|
+
),
|
|
155
|
+
resolving_power=UnitlessBoundRange(
|
|
156
|
+
min=resolving_power - (resolving_power * 0.1),
|
|
157
|
+
max=resolving_power + (resolving_power * 0.1),
|
|
158
|
+
),
|
|
159
|
+
opacity_factor=UnitlessBoundRange(min=0.0, max=10.0),
|
|
160
|
+
straylight_fraction=UnitlessBoundRange(min=0.0, max=0.4),
|
|
161
|
+
continuum_level=UnitlessBoundRange(min=0.5, max=2.0),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
fit_flags = FitFlagsModel(
|
|
165
|
+
crval=True,
|
|
166
|
+
dispersion=True,
|
|
167
|
+
incident_light_angle=True,
|
|
168
|
+
resolving_power=True,
|
|
169
|
+
opacity_factor=True,
|
|
170
|
+
straylight_fraction=True,
|
|
171
|
+
continuum_level=True,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
logger.info("Initializing parameters")
|
|
175
|
+
input_parameters = WavelengthCalibrationParameters(
|
|
176
|
+
crval=crval_initial_guess,
|
|
177
|
+
dispersion=dispersion,
|
|
178
|
+
incident_light_angle=incident_light_angle,
|
|
179
|
+
resolving_power=resolving_power,
|
|
180
|
+
opacity_factor=self.parameters.wavecal_init_opacity_factor,
|
|
181
|
+
straylight_fraction=self.parameters.wavecal_init_straylight_fraction,
|
|
182
|
+
grating_constant=self.constants.grating_constant_inverse_mm,
|
|
183
|
+
doppler_velocity=doppler_velocity,
|
|
184
|
+
order=order,
|
|
185
|
+
bounds=bounds,
|
|
186
|
+
fit_flags=fit_flags,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
fitter = WavelengthCalibrationFitter(
|
|
190
|
+
input_parameters=input_parameters,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
logger.info(f"Input parameters: {input_parameters.lmfit_parameters.pretty_repr()}")
|
|
194
|
+
|
|
195
|
+
with self.telemetry_span("Run wavelength solution fit"):
|
|
196
|
+
extra_kwargs = self.parameters.wavecal_fit_kwargs
|
|
197
|
+
logger.info(f"Calling fitter with extra kwargs: {extra_kwargs}")
|
|
198
|
+
fit_result = fitter(
|
|
199
|
+
input_spectrum=input_spectrum,
|
|
200
|
+
**extra_kwargs,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
with self.telemetry_span("Save wavelength solution and quality metrics"):
|
|
204
|
+
axis_number = next(
|
|
205
|
+
(i for i in range(1, 4) if getattr(self.constants, f"axis_{i}_type") == "AWAV"),
|
|
206
|
+
None,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
if axis_number is None:
|
|
210
|
+
raise ValueError("No axis equal to AWAV. This should never happen!")
|
|
211
|
+
|
|
212
|
+
solution_header = fit_result.wavelength_parameters.to_header(
|
|
213
|
+
axis_num=axis_number, add_alternate_keys=True
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
# update the fit value of CRPIX to account for the fact that we may have chopped some NaN pixels away from the start of the data array
|
|
217
|
+
solution_header[f"CRPIX{axis_number}"] += nan_chop_amount
|
|
218
|
+
solution_header[f"CRPIX{axis_number}A"] += nan_chop_amount
|
|
219
|
+
|
|
220
|
+
self.write(
|
|
221
|
+
data=solution_header,
|
|
222
|
+
tags=[VispTag.task_wavelength_calibration(), VispTag.intermediate()],
|
|
223
|
+
encoder=json_encoder,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
self.quality_store_wavecal_results(
|
|
227
|
+
input_wavelength=input_wavelength_vector,
|
|
228
|
+
input_spectrum=input_spectrum,
|
|
229
|
+
fit_result=fit_result,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
def chop_and_clean_NaNs(self, spectrum: np.ndarray) -> tuple[np.ndarray, int]:
|
|
233
|
+
"""
|
|
234
|
+
Chop contiguous regions of NaN from either end of an array.
|
|
235
|
+
|
|
236
|
+
Returns
|
|
237
|
+
-------
|
|
238
|
+
np.ndarray
|
|
239
|
+
The input array with NaN's removed
|
|
240
|
+
|
|
241
|
+
int
|
|
242
|
+
The number of pixels chopped from the start of the array. This is needed to correctly adjust CRPIX later.
|
|
243
|
+
"""
|
|
244
|
+
starting_non_nan_idx = 0
|
|
245
|
+
while np.isnan(spectrum[starting_non_nan_idx]):
|
|
246
|
+
starting_non_nan_idx += 1
|
|
247
|
+
|
|
248
|
+
ending_non_nan_idx = spectrum.size - 1
|
|
249
|
+
while np.isnan(spectrum[ending_non_nan_idx]):
|
|
250
|
+
ending_non_nan_idx -= 1
|
|
251
|
+
|
|
252
|
+
logger.info(
|
|
253
|
+
f"Chopping NaN values from end of spectrum with slice [{starting_non_nan_idx}:{ending_non_nan_idx + 1}]"
|
|
254
|
+
)
|
|
255
|
+
chopped_spectrum = spectrum[starting_non_nan_idx : ending_non_nan_idx + 1]
|
|
256
|
+
|
|
257
|
+
return chopped_spectrum, starting_non_nan_idx
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def get_doppler_velocity(solar_gain_ip_start_time) -> u.Quantity:
|
|
261
|
+
"""Find the speed at which DKIST is moving relative to the Sun's center.
|
|
262
|
+
|
|
263
|
+
Positive values refer to when DKIST is moving away from the sun.
|
|
264
|
+
"""
|
|
265
|
+
coord = location_of_dkist.get_gcrs(obstime=Time(solar_gain_ip_start_time))
|
|
266
|
+
heliocentric_coord = coord.transform_to(
|
|
267
|
+
HeliocentricInertial(obstime=Time(solar_gain_ip_start_time))
|
|
268
|
+
)
|
|
269
|
+
obs_vr_kms = heliocentric_coord.d_distance
|
|
270
|
+
return obs_vr_kms
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def compute_order(
|
|
274
|
+
central_wavelength: Quantity,
|
|
275
|
+
incident_light_angle: Quantity,
|
|
276
|
+
reflected_light_angle: Quantity,
|
|
277
|
+
grating_constant: Quantity,
|
|
278
|
+
) -> int:
|
|
279
|
+
r"""
|
|
280
|
+
Compute the spectral order from the spectrograph setup.
|
|
281
|
+
|
|
282
|
+
From the grating equation, the spectral order, :math:`m`:, is
|
|
283
|
+
|
|
284
|
+
.. math::
|
|
285
|
+
m = \frac{\sin\alpha + \sin\beta}{G \lambda}
|
|
286
|
+
|
|
287
|
+
where :math:`\alpha` and :math:`\beta` are the incident and reflected light angles, respectively, :math:`G` is the
|
|
288
|
+
grating constant (lines per mm), and :math:`\lambda` is the central wavelength. All of these values come from the
|
|
289
|
+
input headers.
|
|
290
|
+
|
|
291
|
+
Parameters
|
|
292
|
+
----------
|
|
293
|
+
central_wavelength
|
|
294
|
+
Wavelength of the center of the spectral window.
|
|
295
|
+
|
|
296
|
+
incident_light_angle
|
|
297
|
+
Angle of light incident to the spectrograph grating. Often called :math:`\alpha`.
|
|
298
|
+
|
|
299
|
+
reflected_light_angle
|
|
300
|
+
Angle of light reflected from spectrograph grating. Often called :math:`\beta`.
|
|
301
|
+
|
|
302
|
+
grating_constant
|
|
303
|
+
Grating constant of the spectrograph grating [lines per mm]
|
|
304
|
+
|
|
305
|
+
Returns
|
|
306
|
+
-------
|
|
307
|
+
spectral_order
|
|
308
|
+
The order of the given spectrograph configuration
|
|
309
|
+
"""
|
|
310
|
+
return int(
|
|
311
|
+
(np.sin(incident_light_angle) + np.sin(reflected_light_angle))
|
|
312
|
+
/ (grating_constant * central_wavelength)
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def compute_input_wavelength_vector(
|
|
317
|
+
*,
|
|
318
|
+
central_wavelength: Quantity,
|
|
319
|
+
dispersion: Quantity,
|
|
320
|
+
grating_constant: Quantity,
|
|
321
|
+
order: int,
|
|
322
|
+
incident_light_angle: Quantity,
|
|
323
|
+
num_spec_px: int,
|
|
324
|
+
) -> u.Quantity:
|
|
325
|
+
r"""
|
|
326
|
+
Compute a wavelength vector based on information about the spectrograph setup.
|
|
327
|
+
|
|
328
|
+
The parameterization of the grating equation is via `astropy.wcs.WCS`, which follows section 5 of
|
|
329
|
+
`Greisen et al (2006) <https://ui.adsabs.harvard.edu/abs/2006A%26A...446..747G/abstract>`_.
|
|
330
|
+
|
|
331
|
+
Parameters
|
|
332
|
+
----------
|
|
333
|
+
central_wavelength
|
|
334
|
+
Wavelength at the center of the spectral window. This function forces the value of the output vector to be
|
|
335
|
+
``wavelength`` at index ``num_spec // 2 + 1``.
|
|
336
|
+
|
|
337
|
+
dispersion
|
|
338
|
+
Spectrograph dispersion [nm / px]
|
|
339
|
+
|
|
340
|
+
grating_constant
|
|
341
|
+
Grating constant of the spectrograph grating [lines per mm]
|
|
342
|
+
|
|
343
|
+
order
|
|
344
|
+
Spectrograph order
|
|
345
|
+
|
|
346
|
+
incident_light_angle
|
|
347
|
+
Angle of light incident to the spectrograph grating. Often called :math:`\alpha`.
|
|
348
|
+
|
|
349
|
+
num_spec_px
|
|
350
|
+
The length of the output wavelength vector. Defines size and physical limits of the output.
|
|
351
|
+
|
|
352
|
+
Returns
|
|
353
|
+
-------
|
|
354
|
+
wave_vec
|
|
355
|
+
1D array of length ``num_spec`` containing the wavelength values described by the input WCS parameterization.
|
|
356
|
+
The units of this array will be nanometers.
|
|
357
|
+
"""
|
|
358
|
+
wavelength_parameters = WavelengthParameters(
|
|
359
|
+
crpix=num_spec_px // 2 + 1,
|
|
360
|
+
crval=central_wavelength.to_value(u.nm),
|
|
361
|
+
dispersion=dispersion.to_value(u.nm / u.pix),
|
|
362
|
+
grating_constant=grating_constant.to_value(1 / u.mm),
|
|
363
|
+
order=order,
|
|
364
|
+
incident_light_angle=incident_light_angle.to_value(u.deg),
|
|
365
|
+
cunit="nm",
|
|
366
|
+
)
|
|
367
|
+
header = wavelength_parameters.to_header(axis_num=1)
|
|
368
|
+
wcs = WCS(header)
|
|
369
|
+
input_wavelength_vector = wcs.spectral.pixel_to_world(np.arange(num_spec_px)).to(u.nm)
|
|
370
|
+
|
|
371
|
+
return input_wavelength_vector
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def compute_initial_dispersion(
|
|
375
|
+
central_wavelength: Quantity,
|
|
376
|
+
incident_light_angle: Quantity,
|
|
377
|
+
reflected_light_angle: Quantity,
|
|
378
|
+
lens_parameters: list[Quantity],
|
|
379
|
+
pixel_pitch: Quantity,
|
|
380
|
+
) -> Quantity:
|
|
381
|
+
r"""
|
|
382
|
+
Compute the dispersion (:math:`d\,\lambda/d\, px`) given the spectrograph setup.
|
|
383
|
+
|
|
384
|
+
The dispersion is given via
|
|
385
|
+
|
|
386
|
+
.. math::
|
|
387
|
+
d\,\lambda / d\, px = \frac{p \lambda_0 \cos\beta}{f (\sin\alpha + \sin\beta)}
|
|
388
|
+
|
|
389
|
+
where :math:`p` is the pixel pitch (microns per pix), :math:`\lambda_0` is the central wavelength, :math:`f` is the
|
|
390
|
+
camera focal length, and :math:`\alpha` and :math:`\beta` are the incident and reflected light angles, respectively.
|
|
391
|
+
:math:`\lambda_0`, :math:`\alpha`, and :math:`\beta` are taken from input headers, while :math:`f` and :math:`p` are
|
|
392
|
+
pipeline parameters.
|
|
393
|
+
|
|
394
|
+
Parameters
|
|
395
|
+
----------
|
|
396
|
+
central_wavelength
|
|
397
|
+
Wavelength of the center of the spectral window.
|
|
398
|
+
|
|
399
|
+
incident_light_angle
|
|
400
|
+
Angle of light incident to the spectrograph grating. Often called :math:`\alpha`.
|
|
401
|
+
|
|
402
|
+
reflected_light_angle
|
|
403
|
+
Angle of light reflected from spectrograph grating. Often called :math:`\beta`.
|
|
404
|
+
|
|
405
|
+
lens_parameters
|
|
406
|
+
Parameterization of lens focal length as zero, first, and second orders of wavelength. If the total focal
|
|
407
|
+
length of the lens is :math:`f = a_0 + a_1\lambda + a_2\lambda^2` then this list is :math:`[a_0, a_1, a_2]`.
|
|
408
|
+
|
|
409
|
+
pixel_pitch
|
|
410
|
+
The physical size of a single pixel
|
|
411
|
+
|
|
412
|
+
Returns
|
|
413
|
+
-------
|
|
414
|
+
dispersion
|
|
415
|
+
The computed dispersion in units of nm / px
|
|
416
|
+
"""
|
|
417
|
+
camera_focal_length = lens_parameters[0] + central_wavelength * (
|
|
418
|
+
lens_parameters[1] + central_wavelength * lens_parameters[2]
|
|
419
|
+
)
|
|
420
|
+
logger.info(f"{camera_focal_length = !s}")
|
|
421
|
+
|
|
422
|
+
linear_dispersion = (
|
|
423
|
+
camera_focal_length
|
|
424
|
+
* (np.sin(incident_light_angle) + np.sin(reflected_light_angle))
|
|
425
|
+
/ (np.cos(reflected_light_angle) * central_wavelength)
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
dispersion = pixel_pitch / linear_dispersion
|
|
429
|
+
|
|
430
|
+
return dispersion.to(u.nm / u.pix)
|
|
@@ -185,6 +185,7 @@ class VispWriteL1Frame(WriteL1Frame):
|
|
|
185
185
|
]
|
|
186
186
|
wavelength_axis = axis_types.index("AWAV") + 1 # FITS axis numbering is 1-based, not 0
|
|
187
187
|
wavelength_unit = header[f"CUNIT{wavelength_axis}"]
|
|
188
|
+
|
|
188
189
|
minimum = header[f"CRVAL{wavelength_axis}"] - (
|
|
189
190
|
header[f"CRPIX{wavelength_axis}"] * header[f"CDELT{wavelength_axis}"]
|
|
190
191
|
)
|
|
@@ -192,6 +193,7 @@ class VispWriteL1Frame(WriteL1Frame):
|
|
|
192
193
|
(header[f"NAXIS{wavelength_axis}"] - header[f"CRPIX{wavelength_axis}"])
|
|
193
194
|
* header[f"CDELT{wavelength_axis}"]
|
|
194
195
|
)
|
|
196
|
+
|
|
195
197
|
return WavelengthRange(
|
|
196
198
|
min=u.Quantity(minimum, unit=wavelength_unit),
|
|
197
199
|
max=u.Quantity(maximum, unit=wavelength_unit),
|
|
@@ -190,6 +190,17 @@ class VispInputDatasetParameterValues:
|
|
|
190
190
|
visp_wavecal_init_resolving_power: int = 150000
|
|
191
191
|
visp_wavecal_init_straylight_fraction: float = 0.2
|
|
192
192
|
visp_wavecal_init_opacity_factor: float = 5.0
|
|
193
|
+
visp_wavecal_crval_bounds_px: float = 7
|
|
194
|
+
visp_wavecal_dispersion_bounds_fraction: float = 0.02
|
|
195
|
+
visp_wavecal_incident_light_angle_bounds_deg: float = 0.1
|
|
196
|
+
visp_wavecal_fit_kwargs: dict[str, Any] = field(
|
|
197
|
+
default_factory=lambda: {
|
|
198
|
+
"method": "differential_evolution",
|
|
199
|
+
"init": "halton",
|
|
200
|
+
"popsize": 1,
|
|
201
|
+
"tol": 1e-10,
|
|
202
|
+
}
|
|
203
|
+
)
|
|
193
204
|
visp_polcal_spatial_median_filter_width_px: int = 10
|
|
194
205
|
visp_polcal_num_spatial_bins: int = 10
|
|
195
206
|
visp_polcal_demod_spatial_smooth_fit_order: int = 17
|
|
@@ -37,6 +37,7 @@ class VispHeaders(Spec122Dataset):
|
|
|
37
37
|
grating_constant: float = 316.0,
|
|
38
38
|
grating_angle: float = -69.9,
|
|
39
39
|
arm_position: float = -4.0,
|
|
40
|
+
swap_wcs_axes: bool = False,
|
|
40
41
|
**kwargs,
|
|
41
42
|
):
|
|
42
43
|
super().__init__(
|
|
@@ -46,6 +47,7 @@ class VispHeaders(Spec122Dataset):
|
|
|
46
47
|
instrument=instrument,
|
|
47
48
|
**kwargs,
|
|
48
49
|
)
|
|
50
|
+
self.swap_wcs_axes = swap_wcs_axes
|
|
49
51
|
self.add_constant_key("VISP_001", arm_id)
|
|
50
52
|
self.add_constant_key("WAVELNTH", 656.30)
|
|
51
53
|
self.add_constant_key("VISP_010", num_modstates_header_value)
|
|
@@ -56,11 +58,9 @@ class VispHeaders(Spec122Dataset):
|
|
|
56
58
|
self.add_constant_key("DKIST011", ip_start_time)
|
|
57
59
|
self.add_constant_key("DKIST012", ip_end_time)
|
|
58
60
|
self.add_constant_key("FILE_ID", uuid.uuid4().hex)
|
|
59
|
-
|
|
60
61
|
self.add_constant_key("VISP_002", arm_position)
|
|
61
62
|
self.add_constant_key("VISP_013", grating_constant)
|
|
62
63
|
self.add_constant_key("VISP_015", grating_angle)
|
|
63
|
-
|
|
64
64
|
self.num_modstates_header_value = num_modstates_header_value
|
|
65
65
|
self.add_constant_key("CAM__001", "camera_id")
|
|
66
66
|
self.add_constant_key("CAM__002", "camera_name")
|
|
@@ -81,13 +81,29 @@ class VispHeaders(Spec122Dataset):
|
|
|
81
81
|
def fits_wcs(self):
|
|
82
82
|
w = WCS(naxis=self.array_ndim)
|
|
83
83
|
w.wcs.crpix = self.array_shape[2] / 2, self.array_shape[1] / 2, 1
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
if self.swap_wcs_axes:
|
|
85
|
+
w.wcs.crval = 656.30, 0, 0
|
|
86
|
+
w.wcs.cdelt = 0.2, 1, 1
|
|
87
|
+
w.wcs.cunit = "nm", "arcsec", "arcsec"
|
|
88
|
+
w.wcs.ctype = "AWAV", "HPLT-TAN", "HPLN-TAN"
|
|
89
|
+
else:
|
|
90
|
+
w.wcs.crval = 0, 656.30, 0
|
|
91
|
+
w.wcs.cdelt = 1, 0.2, 1
|
|
92
|
+
w.wcs.cunit = "arcsec", "nm", "arcsec"
|
|
93
|
+
w.wcs.ctype = "HPLT-TAN", "AWAV", "HPLN-TAN"
|
|
88
94
|
w.wcs.pc = np.identity(self.array_ndim)
|
|
89
95
|
return w
|
|
90
96
|
|
|
97
|
+
@key_function(
|
|
98
|
+
"CRPIX<n>A",
|
|
99
|
+
"CRVAL<n>A",
|
|
100
|
+
"CDELT<n>A",
|
|
101
|
+
"CUNIT<n>A",
|
|
102
|
+
"CTYPE<n>A",
|
|
103
|
+
)
|
|
104
|
+
def alternate_wcs_keys(self, key: str):
|
|
105
|
+
return self.fits_wcs.to_header()[key.removesuffix("A")]
|
|
106
|
+
|
|
91
107
|
|
|
92
108
|
class VispHeadersInputDarkFrames(VispHeaders):
|
|
93
109
|
def __init__(
|
|
@@ -19,6 +19,7 @@ from dkist_processing_visp.tasks.geometric import GeometricCalibration
|
|
|
19
19
|
from dkist_processing_visp.tasks.instrument_polarization import InstrumentPolarizationCalibration
|
|
20
20
|
from dkist_processing_visp.tasks.lamp import LampCalibration
|
|
21
21
|
from dkist_processing_visp.tasks.solar import SolarCalibration
|
|
22
|
+
from dkist_processing_visp.tasks.wavelength_calibration import WavelengthCalibration
|
|
22
23
|
from dkist_processing_visp.tests.conftest import VispInputDatasetParameterValues
|
|
23
24
|
from dkist_processing_visp.tests.local_trial_workflows.local_trial_helpers import LoadBackgroundCal
|
|
24
25
|
from dkist_processing_visp.tests.local_trial_workflows.local_trial_helpers import LoadDarkCal
|
|
@@ -27,6 +28,9 @@ from dkist_processing_visp.tests.local_trial_workflows.local_trial_helpers impor
|
|
|
27
28
|
from dkist_processing_visp.tests.local_trial_workflows.local_trial_helpers import LoadInstPolCal
|
|
28
29
|
from dkist_processing_visp.tests.local_trial_workflows.local_trial_helpers import LoadLampCal
|
|
29
30
|
from dkist_processing_visp.tests.local_trial_workflows.local_trial_helpers import LoadSolarCal
|
|
31
|
+
from dkist_processing_visp.tests.local_trial_workflows.local_trial_helpers import (
|
|
32
|
+
LoadWavelengthCalibration,
|
|
33
|
+
)
|
|
30
34
|
from dkist_processing_visp.tests.local_trial_workflows.local_trial_helpers import (
|
|
31
35
|
ParseCalOnlyL0InputData,
|
|
32
36
|
)
|
|
@@ -37,6 +41,9 @@ from dkist_processing_visp.tests.local_trial_workflows.local_trial_helpers impor
|
|
|
37
41
|
from dkist_processing_visp.tests.local_trial_workflows.local_trial_helpers import SaveInstPolCal
|
|
38
42
|
from dkist_processing_visp.tests.local_trial_workflows.local_trial_helpers import SaveLampCal
|
|
39
43
|
from dkist_processing_visp.tests.local_trial_workflows.local_trial_helpers import SaveSolarCal
|
|
44
|
+
from dkist_processing_visp.tests.local_trial_workflows.local_trial_helpers import (
|
|
45
|
+
SaveWavelengthCalibration,
|
|
46
|
+
)
|
|
40
47
|
from dkist_processing_visp.tests.local_trial_workflows.local_trial_helpers import SetNumModstates
|
|
41
48
|
from dkist_processing_visp.tests.local_trial_workflows.local_trial_helpers import SetObserveExpTime
|
|
42
49
|
from dkist_processing_visp.tests.local_trial_workflows.local_trial_helpers import (
|
|
@@ -112,6 +119,7 @@ def main(
|
|
|
112
119
|
load_lamp: bool = False,
|
|
113
120
|
load_geometric: bool = False,
|
|
114
121
|
load_solar: bool = False,
|
|
122
|
+
load_wavelength_calibration: bool = False,
|
|
115
123
|
load_inst_pol: bool = False,
|
|
116
124
|
dummy_wavelength: float = 630.0,
|
|
117
125
|
):
|
|
@@ -172,6 +180,12 @@ def main(
|
|
|
172
180
|
manual_processing_run.run_task(task=SolarCalibration)
|
|
173
181
|
manual_processing_run.run_task(task=SaveSolarCal)
|
|
174
182
|
|
|
183
|
+
if load_wavelength_calibration:
|
|
184
|
+
manual_processing_run.run_task(task=LoadWavelengthCalibration)
|
|
185
|
+
else:
|
|
186
|
+
manual_processing_run.run_task(task=WavelengthCalibration)
|
|
187
|
+
manual_processing_run.run_task(task=SaveWavelengthCalibration)
|
|
188
|
+
|
|
175
189
|
if load_inst_pol:
|
|
176
190
|
manual_processing_run.run_task(task=LoadInstPolCal)
|
|
177
191
|
else:
|
|
@@ -241,6 +255,12 @@ if __name__ == "__main__":
|
|
|
241
255
|
help="Load solar calibration from previously saved run",
|
|
242
256
|
action="store_true",
|
|
243
257
|
)
|
|
258
|
+
parser.add_argument(
|
|
259
|
+
"-W",
|
|
260
|
+
"--load-wavelength-calibration",
|
|
261
|
+
help="Load wavelength calibration solution from previously saved run",
|
|
262
|
+
action="store_true",
|
|
263
|
+
)
|
|
244
264
|
parser.add_argument(
|
|
245
265
|
"-P",
|
|
246
266
|
"--load-inst-pol",
|
|
@@ -261,6 +281,7 @@ if __name__ == "__main__":
|
|
|
261
281
|
load_lamp=args.load_lamp,
|
|
262
282
|
load_geometric=args.load_geometric,
|
|
263
283
|
load_solar=args.load_solar,
|
|
284
|
+
load_wavelength_calibration=args.load_wavelength_calibration,
|
|
264
285
|
load_inst_pol=args.load_inst_pol,
|
|
265
286
|
)
|
|
266
287
|
)
|
|
@@ -32,6 +32,7 @@ from dkist_processing_visp.tasks.lamp import LampCalibration
|
|
|
32
32
|
from dkist_processing_visp.tasks.science import ScienceCalibration
|
|
33
33
|
from dkist_processing_visp.tasks.solar import SolarCalibration
|
|
34
34
|
from dkist_processing_visp.tasks.visp_base import VispTaskBase
|
|
35
|
+
from dkist_processing_visp.tasks.wavelength_calibration import WavelengthCalibration
|
|
35
36
|
from dkist_processing_visp.tasks.write_l1 import VispWriteL1Frame
|
|
36
37
|
from dkist_processing_visp.tests.conftest import VispInputDatasetParameterValues
|
|
37
38
|
from dkist_processing_visp.tests.local_trial_workflows.local_trial_helpers import LoadBackgroundCal
|
|
@@ -45,6 +46,9 @@ from dkist_processing_visp.tests.local_trial_workflows.local_trial_helpers impor
|
|
|
45
46
|
LoadPolcalAsScience,
|
|
46
47
|
)
|
|
47
48
|
from dkist_processing_visp.tests.local_trial_workflows.local_trial_helpers import LoadSolarCal
|
|
49
|
+
from dkist_processing_visp.tests.local_trial_workflows.local_trial_helpers import (
|
|
50
|
+
LoadWavelengthCalibration,
|
|
51
|
+
)
|
|
48
52
|
from dkist_processing_visp.tests.local_trial_workflows.local_trial_helpers import (
|
|
49
53
|
ParseCalOnlyL0InputData,
|
|
50
54
|
)
|
|
@@ -59,6 +63,9 @@ from dkist_processing_visp.tests.local_trial_workflows.local_trial_helpers impor
|
|
|
59
63
|
SavePolcalAsScience,
|
|
60
64
|
)
|
|
61
65
|
from dkist_processing_visp.tests.local_trial_workflows.local_trial_helpers import SaveSolarCal
|
|
66
|
+
from dkist_processing_visp.tests.local_trial_workflows.local_trial_helpers import (
|
|
67
|
+
SaveWavelengthCalibration,
|
|
68
|
+
)
|
|
62
69
|
from dkist_processing_visp.tests.local_trial_workflows.local_trial_helpers import SetAxesTypes
|
|
63
70
|
from dkist_processing_visp.tests.local_trial_workflows.local_trial_helpers import (
|
|
64
71
|
SetCadenceConstants,
|
|
@@ -224,6 +231,7 @@ def main(
|
|
|
224
231
|
load_lamp: bool = False,
|
|
225
232
|
load_geometric: bool = False,
|
|
226
233
|
load_solar: bool = False,
|
|
234
|
+
load_wavelength_calibration: bool = False,
|
|
227
235
|
load_inst_pol: bool = False,
|
|
228
236
|
load_polcal_as_science: bool = False,
|
|
229
237
|
load_calibrated_data: bool = False,
|
|
@@ -288,6 +296,12 @@ def main(
|
|
|
288
296
|
manual_processing_run.run_task(task=SolarCalibration)
|
|
289
297
|
manual_processing_run.run_task(task=SaveSolarCal)
|
|
290
298
|
|
|
299
|
+
if load_wavelength_calibration:
|
|
300
|
+
manual_processing_run.run_task(task=LoadWavelengthCalibration)
|
|
301
|
+
else:
|
|
302
|
+
manual_processing_run.run_task(task=WavelengthCalibration)
|
|
303
|
+
manual_processing_run.run_task(task=SaveWavelengthCalibration)
|
|
304
|
+
|
|
291
305
|
if load_inst_pol:
|
|
292
306
|
manual_processing_run.run_task(task=LoadInstPolCal)
|
|
293
307
|
else:
|
|
@@ -382,6 +396,12 @@ if __name__ == "__main__":
|
|
|
382
396
|
help="Load solar calibration from previously saved run",
|
|
383
397
|
action="store_true",
|
|
384
398
|
)
|
|
399
|
+
parser.add_argument(
|
|
400
|
+
"-W",
|
|
401
|
+
"--load-wavelength-calibration",
|
|
402
|
+
help="Load wavelength calibration solution from previously saved run",
|
|
403
|
+
action="store_true",
|
|
404
|
+
)
|
|
385
405
|
parser.add_argument(
|
|
386
406
|
"-P",
|
|
387
407
|
"--load-inst-pol",
|
|
@@ -413,6 +433,7 @@ if __name__ == "__main__":
|
|
|
413
433
|
load_lamp=args.load_lamp,
|
|
414
434
|
load_geometric=args.load_geometric,
|
|
415
435
|
load_solar=args.load_solar,
|
|
436
|
+
load_wavelength_calibration=args.load_wavelength_calibration,
|
|
416
437
|
load_inst_pol=args.load_inst_pol,
|
|
417
438
|
load_polcal_as_science=args.load_polcal_as_science,
|
|
418
439
|
load_calibrated_data=args.load_calibrated_data,
|