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.
- changelog/232.misc.rst +1 -0
- dkist_processing_cryonirsp/__init__.py +1 -0
- dkist_processing_cryonirsp/codecs/fits.py +1 -0
- dkist_processing_cryonirsp/config.py +5 -1
- dkist_processing_cryonirsp/models/beam_boundaries.py +1 -0
- dkist_processing_cryonirsp/models/constants.py +31 -30
- dkist_processing_cryonirsp/models/exposure_conditions.py +6 -5
- dkist_processing_cryonirsp/models/fits_access.py +40 -0
- dkist_processing_cryonirsp/models/parameters.py +14 -26
- dkist_processing_cryonirsp/models/tags.py +1 -0
- dkist_processing_cryonirsp/models/task_name.py +1 -0
- dkist_processing_cryonirsp/parsers/check_for_gains.py +1 -0
- dkist_processing_cryonirsp/parsers/cryonirsp_l0_fits_access.py +40 -47
- dkist_processing_cryonirsp/parsers/cryonirsp_l1_fits_access.py +1 -0
- dkist_processing_cryonirsp/parsers/exposure_conditions.py +14 -13
- dkist_processing_cryonirsp/parsers/map_repeats.py +1 -0
- dkist_processing_cryonirsp/parsers/measurements.py +29 -16
- dkist_processing_cryonirsp/parsers/modstates.py +5 -1
- dkist_processing_cryonirsp/parsers/optical_density_filters.py +1 -0
- dkist_processing_cryonirsp/parsers/polarimetric_check.py +18 -7
- dkist_processing_cryonirsp/parsers/scan_step.py +12 -4
- dkist_processing_cryonirsp/parsers/time.py +7 -7
- dkist_processing_cryonirsp/parsers/wavelength.py +6 -1
- dkist_processing_cryonirsp/tasks/__init__.py +2 -1
- dkist_processing_cryonirsp/tasks/assemble_movie.py +1 -0
- dkist_processing_cryonirsp/tasks/bad_pixel_map.py +6 -5
- dkist_processing_cryonirsp/tasks/beam_boundaries_base.py +12 -11
- dkist_processing_cryonirsp/tasks/ci_beam_boundaries.py +1 -0
- dkist_processing_cryonirsp/tasks/ci_science.py +1 -0
- dkist_processing_cryonirsp/tasks/cryonirsp_base.py +2 -3
- dkist_processing_cryonirsp/tasks/dark.py +5 -4
- dkist_processing_cryonirsp/tasks/gain.py +7 -6
- dkist_processing_cryonirsp/tasks/instrument_polarization.py +17 -16
- dkist_processing_cryonirsp/tasks/l1_output_data.py +1 -0
- dkist_processing_cryonirsp/tasks/linearity_correction.py +1 -0
- dkist_processing_cryonirsp/tasks/make_movie_frames.py +3 -2
- dkist_processing_cryonirsp/tasks/mixin/corrections.py +1 -0
- dkist_processing_cryonirsp/tasks/mixin/shift_measurements.py +9 -2
- dkist_processing_cryonirsp/tasks/parse.py +70 -52
- dkist_processing_cryonirsp/tasks/quality_metrics.py +15 -14
- dkist_processing_cryonirsp/tasks/science_base.py +8 -6
- dkist_processing_cryonirsp/tasks/sp_beam_boundaries.py +2 -1
- dkist_processing_cryonirsp/tasks/sp_geometric.py +11 -10
- dkist_processing_cryonirsp/tasks/sp_science.py +1 -0
- dkist_processing_cryonirsp/tasks/sp_solar_gain.py +15 -12
- dkist_processing_cryonirsp/tasks/sp_wavelength_calibration.py +300 -0
- dkist_processing_cryonirsp/tasks/write_l1.py +59 -38
- dkist_processing_cryonirsp/tests/conftest.py +75 -53
- dkist_processing_cryonirsp/tests/header_models.py +62 -11
- dkist_processing_cryonirsp/tests/local_trial_workflows/l0_cals_only.py +26 -46
- dkist_processing_cryonirsp/tests/local_trial_workflows/l0_to_l1.py +26 -47
- dkist_processing_cryonirsp/tests/local_trial_workflows/linearize_only.py +3 -3
- dkist_processing_cryonirsp/tests/local_trial_workflows/local_trial_helpers.py +57 -26
- dkist_processing_cryonirsp/tests/test_assemble_movie.py +4 -5
- dkist_processing_cryonirsp/tests/test_assemble_qualilty.py +5 -1
- dkist_processing_cryonirsp/tests/test_bad_pixel_maps.py +4 -5
- dkist_processing_cryonirsp/tests/test_ci_beam_boundaries.py +4 -5
- dkist_processing_cryonirsp/tests/test_ci_science.py +4 -5
- dkist_processing_cryonirsp/tests/test_corrections.py +5 -6
- dkist_processing_cryonirsp/tests/test_cryo_base.py +4 -6
- dkist_processing_cryonirsp/tests/test_cryo_constants.py +7 -3
- dkist_processing_cryonirsp/tests/test_dark.py +7 -8
- dkist_processing_cryonirsp/tests/test_fits_access.py +44 -0
- dkist_processing_cryonirsp/tests/test_gain.py +7 -8
- dkist_processing_cryonirsp/tests/test_instrument_polarization.py +19 -10
- dkist_processing_cryonirsp/tests/test_linearity_correction.py +5 -4
- dkist_processing_cryonirsp/tests/test_make_movie_frames.py +2 -3
- dkist_processing_cryonirsp/tests/test_parameters.py +23 -28
- dkist_processing_cryonirsp/tests/test_parse.py +48 -12
- dkist_processing_cryonirsp/tests/test_quality.py +2 -3
- dkist_processing_cryonirsp/tests/test_sp_beam_boundaries.py +5 -5
- dkist_processing_cryonirsp/tests/test_sp_geometric.py +5 -6
- dkist_processing_cryonirsp/tests/test_sp_make_movie_frames.py +2 -3
- dkist_processing_cryonirsp/tests/test_sp_science.py +4 -5
- dkist_processing_cryonirsp/tests/test_sp_solar.py +6 -5
- dkist_processing_cryonirsp/tests/{test_sp_dispersion_axis_correction.py → test_sp_wavelength_calibration.py} +11 -29
- dkist_processing_cryonirsp/tests/test_trial_create_quality_report.py +1 -1
- dkist_processing_cryonirsp/tests/test_workflows.py +1 -0
- dkist_processing_cryonirsp/tests/test_write_l1.py +29 -31
- dkist_processing_cryonirsp/workflows/__init__.py +1 -0
- dkist_processing_cryonirsp/workflows/ci_l0_processing.py +9 -5
- dkist_processing_cryonirsp/workflows/sp_l0_processing.py +12 -8
- dkist_processing_cryonirsp/workflows/trial_workflows.py +12 -11
- dkist_processing_cryonirsp-1.14.9rc1.dist-info/METADATA +552 -0
- dkist_processing_cryonirsp-1.14.9rc1.dist-info/RECORD +115 -0
- {dkist_processing_cryonirsp-1.4.20.dist-info → dkist_processing_cryonirsp-1.14.9rc1.dist-info}/WHEEL +1 -1
- docs/ci_science_calibration.rst +10 -0
- docs/conf.py +1 -0
- docs/index.rst +1 -0
- docs/sp_science_calibration.rst +7 -0
- docs/wavelength_calibration.rst +62 -0
- dkist_processing_cryonirsp/tasks/sp_dispersion_axis_correction.py +0 -492
- dkist_processing_cryonirsp-1.4.20.dist-info/METADATA +0 -452
- dkist_processing_cryonirsp-1.4.20.dist-info/RECORD +0 -111
- {dkist_processing_cryonirsp-1.4.20.dist-info → dkist_processing_cryonirsp-1.14.9rc1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"""Cryo SP wavelength calibration task. See :doc:`this page </wavelength_calibration>` for more information."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
|
|
5
|
+
import astropy.units as u
|
|
6
|
+
import numpy as np
|
|
7
|
+
from astropy.time import Time
|
|
8
|
+
from astropy.wcs import WCS
|
|
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_service_configuration.logging import logger
|
|
12
|
+
from solar_wavelength_calibration import Atlas
|
|
13
|
+
from solar_wavelength_calibration import WavelengthCalibrationFitter
|
|
14
|
+
from solar_wavelength_calibration.fitter.parameters import AngleBoundRange
|
|
15
|
+
from solar_wavelength_calibration.fitter.parameters import BoundsModel
|
|
16
|
+
from solar_wavelength_calibration.fitter.parameters import DispersionBoundRange
|
|
17
|
+
from solar_wavelength_calibration.fitter.parameters import LengthBoundRange
|
|
18
|
+
from solar_wavelength_calibration.fitter.parameters import UnitlessBoundRange
|
|
19
|
+
from solar_wavelength_calibration.fitter.parameters import WavelengthCalibrationParameters
|
|
20
|
+
from solar_wavelength_calibration.fitter.wavelength_fitter import WavelengthParameters
|
|
21
|
+
from solar_wavelength_calibration.fitter.wavelength_fitter import calculate_initial_crval_guess
|
|
22
|
+
from sunpy.coordinates import HeliocentricInertial
|
|
23
|
+
|
|
24
|
+
from dkist_processing_cryonirsp.codecs.fits import cryo_fits_array_decoder
|
|
25
|
+
from dkist_processing_cryonirsp.models.tags import CryonirspTag
|
|
26
|
+
from dkist_processing_cryonirsp.tasks.cryonirsp_base import CryonirspTaskBase
|
|
27
|
+
|
|
28
|
+
__all__ = ["SPWavelengthCalibration"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SPWavelengthCalibration(CryonirspTaskBase):
|
|
32
|
+
"""Task class for correcting the dispersion axis wavelength values.
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
recipe_run_id : int
|
|
37
|
+
id of the recipe run used to identify the workflow run this task is part of
|
|
38
|
+
workflow_name : str
|
|
39
|
+
name of the workflow to which this instance of the task belongs
|
|
40
|
+
workflow_version : str
|
|
41
|
+
version of the workflow to which this instance of the task belongs
|
|
42
|
+
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
record_provenance = True
|
|
46
|
+
|
|
47
|
+
def run(self) -> None:
|
|
48
|
+
"""
|
|
49
|
+
Run method for the task.
|
|
50
|
+
|
|
51
|
+
- Gather 1D characteristic spectrum. This will be the initial spectrum.
|
|
52
|
+
- Get a header from a solar gain frame for initial wavelength estimation.
|
|
53
|
+
- Compute the theoretical dispersion, order, and incident light angle.
|
|
54
|
+
- Compute the input wavelength vector from the spectrum and header.
|
|
55
|
+
- Get the Doppler velocity and resolving power.
|
|
56
|
+
- Define fitting bounds and initialize model parameters.
|
|
57
|
+
- Set and normalize spectral weights, zeroing edges.
|
|
58
|
+
- Fit the profile using WavelengthCalibrationFitter.
|
|
59
|
+
- Write fit results to disk.
|
|
60
|
+
|
|
61
|
+
Returns
|
|
62
|
+
-------
|
|
63
|
+
None
|
|
64
|
+
"""
|
|
65
|
+
with self.telemetry_span("Load input spectrum and wavelength"):
|
|
66
|
+
logger.info("Loading input spectrum")
|
|
67
|
+
input_spectrum = next(
|
|
68
|
+
self.read(
|
|
69
|
+
tags=[
|
|
70
|
+
CryonirspTag.intermediate_frame(beam=1),
|
|
71
|
+
CryonirspTag.task_characteristic_spectra(),
|
|
72
|
+
],
|
|
73
|
+
decoder=cryo_fits_array_decoder,
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
logger.info(
|
|
78
|
+
"Computing instrument specific dispersion, order, and incident light angle."
|
|
79
|
+
)
|
|
80
|
+
(
|
|
81
|
+
dispersion,
|
|
82
|
+
order,
|
|
83
|
+
incident_light_angle,
|
|
84
|
+
) = self.get_theoretical_dispersion_order_light_angle()
|
|
85
|
+
|
|
86
|
+
logger.info("Computing initial wavelength vector.")
|
|
87
|
+
input_wavelength_vector = self.compute_input_wavelength_vector(
|
|
88
|
+
spectrum=input_spectrum,
|
|
89
|
+
dispersion=dispersion,
|
|
90
|
+
order=order,
|
|
91
|
+
incident_light_angle=incident_light_angle,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Get the doppler velocity
|
|
95
|
+
doppler_velocity = self.get_doppler_velocity()
|
|
96
|
+
logger.info(f"{doppler_velocity = !s}")
|
|
97
|
+
|
|
98
|
+
# Get the resolving power
|
|
99
|
+
resolving_power = self.get_resolving_power()
|
|
100
|
+
logger.info(f"{resolving_power = }")
|
|
101
|
+
|
|
102
|
+
with self.telemetry_span("Compute brute-force CRVAL initial guess"):
|
|
103
|
+
atlas = Atlas(config=self.parameters.wavecal_atlas_download_config)
|
|
104
|
+
crval = calculate_initial_crval_guess(
|
|
105
|
+
input_wavelength_vector=input_wavelength_vector,
|
|
106
|
+
input_spectrum=input_spectrum,
|
|
107
|
+
atlas=atlas,
|
|
108
|
+
negative_limit=-2 * u.nm,
|
|
109
|
+
positive_limit=2 * u.nm,
|
|
110
|
+
num_steps=550,
|
|
111
|
+
)
|
|
112
|
+
logger.info(f"{crval = !s}")
|
|
113
|
+
|
|
114
|
+
with self.telemetry_span("Set up wavelength fit"):
|
|
115
|
+
logger.info("Setting bounds")
|
|
116
|
+
bounds = BoundsModel(
|
|
117
|
+
crval=LengthBoundRange(min=crval - (5 * u.nm), max=crval + (5 * u.nm)),
|
|
118
|
+
dispersion=DispersionBoundRange(
|
|
119
|
+
min=dispersion - (0.05 * u.nm / u.pix), max=dispersion + (0.05 * u.nm / u.pix)
|
|
120
|
+
),
|
|
121
|
+
incident_light_angle=AngleBoundRange(
|
|
122
|
+
min=incident_light_angle - (180 * u.deg),
|
|
123
|
+
max=incident_light_angle + (180 * u.deg),
|
|
124
|
+
),
|
|
125
|
+
resolving_power=UnitlessBoundRange(
|
|
126
|
+
min=resolving_power - (resolving_power * 0.1),
|
|
127
|
+
max=resolving_power + (resolving_power * 0.1),
|
|
128
|
+
),
|
|
129
|
+
opacity_factor=UnitlessBoundRange(min=0.0, max=10.0),
|
|
130
|
+
straylight_fraction=UnitlessBoundRange(min=0.0, max=0.4),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
logger.info("Initializing parameters")
|
|
134
|
+
input_parameters = WavelengthCalibrationParameters(
|
|
135
|
+
crval=crval,
|
|
136
|
+
dispersion=dispersion,
|
|
137
|
+
incident_light_angle=incident_light_angle,
|
|
138
|
+
resolving_power=resolving_power,
|
|
139
|
+
opacity_factor=5.0,
|
|
140
|
+
straylight_fraction=0.2,
|
|
141
|
+
grating_constant=self.constants.grating_constant,
|
|
142
|
+
doppler_velocity=doppler_velocity,
|
|
143
|
+
order=order,
|
|
144
|
+
bounds=bounds,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Define spectral weights to apply
|
|
148
|
+
weights = np.ones_like(input_spectrum)
|
|
149
|
+
# Set edge weights to zero to mitigate flat field artifacts (inner and outer 10% of array)
|
|
150
|
+
num_pixels = len(weights)
|
|
151
|
+
weights[: num_pixels // self.parameters.wavecal_fraction_of_unweighted_edge_pixels] = 0
|
|
152
|
+
weights[-num_pixels // self.parameters.wavecal_fraction_of_unweighted_edge_pixels :] = 0
|
|
153
|
+
|
|
154
|
+
fitter = WavelengthCalibrationFitter(
|
|
155
|
+
input_parameters=input_parameters,
|
|
156
|
+
atlas=atlas,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
logger.info(f"Input parameters: {input_parameters.lmfit_parameters.pretty_repr()}")
|
|
160
|
+
|
|
161
|
+
with self.telemetry_span("Run wavelength solution fit"):
|
|
162
|
+
fit_result = fitter(
|
|
163
|
+
input_spectrum=input_spectrum,
|
|
164
|
+
spectral_weights=weights,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
with self.telemetry_span("Save wavelength solution and quality metrics"):
|
|
168
|
+
self.write(
|
|
169
|
+
data=fit_result.wavelength_parameters.to_header(
|
|
170
|
+
axis_num=1, add_alternate_keys=True
|
|
171
|
+
),
|
|
172
|
+
tags=[CryonirspTag.task_spectral_fit(), CryonirspTag.intermediate()],
|
|
173
|
+
encoder=json_encoder,
|
|
174
|
+
)
|
|
175
|
+
self.quality_store_wavecal_results(
|
|
176
|
+
input_wavelength=input_wavelength_vector,
|
|
177
|
+
input_spectrum=input_spectrum,
|
|
178
|
+
fit_result=fit_result,
|
|
179
|
+
weights=weights,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
def get_theoretical_dispersion_order_light_angle(self) -> tuple[u.Quantity, int, u.Quantity]:
|
|
183
|
+
# TODO: Make this docstring correct (we use grating constant, not grating spacing) and show calculation of all values.
|
|
184
|
+
r"""
|
|
185
|
+
Compute theoretical dispersion, spectral order, and incident light angle.
|
|
186
|
+
|
|
187
|
+
The incident light angle, :math:`\alpha`, is computed as
|
|
188
|
+
|
|
189
|
+
.. math::
|
|
190
|
+
\alpha = \phi + \theta_L
|
|
191
|
+
|
|
192
|
+
where :math:`\phi`, the grating position, and :math:`\theta_L`, the Littrow angle, come from L0 headers.
|
|
193
|
+
|
|
194
|
+
From the grating equation, the spectral order, :math:`m`, is
|
|
195
|
+
|
|
196
|
+
.. math::
|
|
197
|
+
m = \frac{\sin\alpha + \sin\beta}{\sigma\lambda}
|
|
198
|
+
|
|
199
|
+
where :math:`\sigma` is the grating constant (lines per mm), :math:`\beta` is the diffracted light angle,
|
|
200
|
+
and :math:`\lambda` is the wavelength. The wavelength comes from L0 headers and :math:`\beta = \phi - \theta_L`.
|
|
201
|
+
|
|
202
|
+
Finally, the linear dispersion (nm / px) is
|
|
203
|
+
|
|
204
|
+
.. math::
|
|
205
|
+
\frac{d\lambda}{dl} = \frac{\mu \cos\beta}{m \sigma f}
|
|
206
|
+
|
|
207
|
+
where :math:`\mu` is the detector pixel pitch and :math:`f` is the camera focal length, both of which come from
|
|
208
|
+
L0 headers.
|
|
209
|
+
"""
|
|
210
|
+
wavelength = self.constants.wavelength * u.nanometer
|
|
211
|
+
grating_position_angle_phi = self.constants.grating_position_deg * u.deg
|
|
212
|
+
grating_littrow_angle_theta = self.constants.grating_littrow_angle_deg * u.deg
|
|
213
|
+
incident_light_angle = grating_position_angle_phi + grating_littrow_angle_theta
|
|
214
|
+
beta = grating_position_angle_phi - grating_littrow_angle_theta
|
|
215
|
+
order = int(
|
|
216
|
+
(np.sin(incident_light_angle) + np.sin(beta))
|
|
217
|
+
/ (wavelength * self.constants.grating_constant)
|
|
218
|
+
)
|
|
219
|
+
camera_mirror_focal_length = self.parameters.camera_mirror_focal_length_mm
|
|
220
|
+
pixpitch = self.parameters.pixel_pitch_micron
|
|
221
|
+
linear_disp = (
|
|
222
|
+
order * (self.constants.grating_constant / np.cos(beta)) * camera_mirror_focal_length
|
|
223
|
+
)
|
|
224
|
+
theoretical_dispersion = (pixpitch / linear_disp).to(u.nanometer) / u.pix
|
|
225
|
+
|
|
226
|
+
logger.info(f"{theoretical_dispersion = !s}")
|
|
227
|
+
logger.info(f"{order = }")
|
|
228
|
+
logger.info(f"{incident_light_angle = !s}")
|
|
229
|
+
|
|
230
|
+
return theoretical_dispersion, order, incident_light_angle
|
|
231
|
+
|
|
232
|
+
def compute_input_wavelength_vector(
|
|
233
|
+
self,
|
|
234
|
+
*,
|
|
235
|
+
spectrum: np.ndarray,
|
|
236
|
+
dispersion: u.Quantity,
|
|
237
|
+
order: int,
|
|
238
|
+
incident_light_angle: u.Quantity,
|
|
239
|
+
) -> u.Quantity:
|
|
240
|
+
"""Compute the expected wavelength vector based on the header information."""
|
|
241
|
+
num_wave_pix = spectrum.size
|
|
242
|
+
wavelength_parameters = WavelengthParameters(
|
|
243
|
+
crpix=num_wave_pix // 2 + 1,
|
|
244
|
+
crval=self.constants.wavelength,
|
|
245
|
+
dispersion=dispersion.to_value(u.nm / u.pix),
|
|
246
|
+
grating_constant=self.constants.grating_constant.to_value(1 / u.mm),
|
|
247
|
+
order=order,
|
|
248
|
+
incident_light_angle=incident_light_angle.to_value(u.deg),
|
|
249
|
+
cunit="nm",
|
|
250
|
+
)
|
|
251
|
+
header = wavelength_parameters.to_header(axis_num=1)
|
|
252
|
+
wcs = WCS(header)
|
|
253
|
+
input_wavelength_vector = wcs.spectral.pixel_to_world(np.arange(num_wave_pix)).to(u.nm)
|
|
254
|
+
|
|
255
|
+
return input_wavelength_vector
|
|
256
|
+
|
|
257
|
+
def get_doppler_velocity(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 = location_of_dkist.get_gcrs(obstime=Time(self.constants.solar_gain_start_time))
|
|
263
|
+
heliocentric_coord = coord.transform_to(
|
|
264
|
+
HeliocentricInertial(obstime=Time(self.constants.solar_gain_start_time))
|
|
265
|
+
)
|
|
266
|
+
obs_vr_kms = heliocentric_coord.d_distance
|
|
267
|
+
return obs_vr_kms
|
|
268
|
+
|
|
269
|
+
def get_resolving_power(self) -> int:
|
|
270
|
+
"""Find the resolving power for the slit and filter center wavelength used during observation."""
|
|
271
|
+
# Map of (center wavelength) → (slit width) → resolving power
|
|
272
|
+
resolving_power_map = {1080.0: {175: 39580, 52: 120671}, 1430.0: {175: 42943, 52: 133762}}
|
|
273
|
+
|
|
274
|
+
center_wavelength = self.constants.center_wavelength
|
|
275
|
+
slit_width = self.constants.slit_width
|
|
276
|
+
|
|
277
|
+
# Find the closest matching key within tolerance
|
|
278
|
+
matched_wavelength = next(
|
|
279
|
+
(
|
|
280
|
+
key
|
|
281
|
+
for key in resolving_power_map
|
|
282
|
+
if math.isclose(center_wavelength, key, abs_tol=10)
|
|
283
|
+
),
|
|
284
|
+
None,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
if matched_wavelength is None:
|
|
288
|
+
raise ValueError(
|
|
289
|
+
f"{center_wavelength} not a valid filter center wavelength. "
|
|
290
|
+
f"Should be within 10 nm of one of {', '.join(str(k) for k in resolving_power_map)} nm."
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
slit_dict = resolving_power_map[matched_wavelength]
|
|
294
|
+
if slit_width not in slit_dict:
|
|
295
|
+
raise ValueError(
|
|
296
|
+
f"{slit_width} not a valid slit width. "
|
|
297
|
+
f"Should be one of {', '.join(str(k) for k in slit_dict)} µm."
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
return slit_dict[slit_width]
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Cryonirsp write L1 task."""
|
|
2
|
+
|
|
2
3
|
from abc import ABC
|
|
3
4
|
from abc import abstractmethod
|
|
4
5
|
from functools import cached_property
|
|
@@ -11,21 +12,23 @@ from astropy.coordinates.builtin_frames.altaz import AltAz
|
|
|
11
12
|
from astropy.coordinates.sky_coordinate import SkyCoord
|
|
12
13
|
from astropy.io import fits
|
|
13
14
|
from astropy.time.core import Time
|
|
14
|
-
from dkist_processing_common.codecs.
|
|
15
|
+
from dkist_processing_common.codecs.json import json_decoder
|
|
16
|
+
from dkist_processing_common.models.dkist_location import location_of_dkist
|
|
17
|
+
from dkist_processing_common.models.fits_access import MetadataKey
|
|
15
18
|
from dkist_processing_common.models.wavelength import WavelengthRange
|
|
16
19
|
from dkist_processing_common.tasks import WriteL1Frame
|
|
17
|
-
from dkist_processing_common.tasks.mixin.input_dataset import InputDatasetMixin
|
|
18
20
|
from sunpy.coordinates import GeocentricEarthEquatorial
|
|
19
21
|
from sunpy.coordinates import Helioprojective
|
|
20
22
|
|
|
21
23
|
from dkist_processing_cryonirsp.models.constants import CryonirspConstants
|
|
24
|
+
from dkist_processing_cryonirsp.models.fits_access import CryonirspMetadataKey
|
|
22
25
|
from dkist_processing_cryonirsp.models.parameters import CryonirspParameters
|
|
23
26
|
from dkist_processing_cryonirsp.models.tags import CryonirspTag
|
|
24
27
|
|
|
25
28
|
__all__ = ["CIWriteL1Frame", "SPWriteL1Frame"]
|
|
26
29
|
|
|
27
30
|
|
|
28
|
-
class CryonirspWriteL1Frame(WriteL1Frame, ABC
|
|
31
|
+
class CryonirspWriteL1Frame(WriteL1Frame, ABC):
|
|
29
32
|
"""
|
|
30
33
|
Task class for writing out calibrated l1 CryoNIRSP frames.
|
|
31
34
|
|
|
@@ -51,7 +54,7 @@ class CryonirspWriteL1Frame(WriteL1Frame, ABC, InputDatasetMixin):
|
|
|
51
54
|
workflow_version=workflow_version,
|
|
52
55
|
)
|
|
53
56
|
self.parameters = CryonirspParameters(
|
|
54
|
-
self.
|
|
57
|
+
scratch=self.scratch,
|
|
55
58
|
obs_ip_start_time=self.constants.obs_ip_start_time,
|
|
56
59
|
wavelength=self.constants.wavelength,
|
|
57
60
|
arm_id=self.constants.arm_id,
|
|
@@ -92,13 +95,13 @@ class CryonirspWriteL1Frame(WriteL1Frame, ABC, InputDatasetMixin):
|
|
|
92
95
|
next_axis = self.add_map_scan_axis(header, axis_num=next_axis)
|
|
93
96
|
if self.constants.correct_for_polarization:
|
|
94
97
|
next_axis = self.add_stokes_axis(header, stokes=stokes, axis_num=next_axis)
|
|
95
|
-
self.add_wavelength_headers(header)
|
|
96
98
|
last_axis = next_axis - 1
|
|
97
99
|
self.add_common_headers(header, num_axes=last_axis)
|
|
98
100
|
self.flip_spectral_axis(header)
|
|
99
101
|
boresight_coordinates = self.get_boresight_coords(header)
|
|
100
102
|
self.correct_spatial_wcs_info(header, boresight_coordinates)
|
|
101
103
|
self.update_spectral_headers(header)
|
|
104
|
+
self.add_wavelength_headers(header)
|
|
102
105
|
|
|
103
106
|
return header
|
|
104
107
|
|
|
@@ -141,10 +144,9 @@ class CryonirspWriteL1Frame(WriteL1Frame, ABC, InputDatasetMixin):
|
|
|
141
144
|
header[f"DTYPE{axis_num}"] = "TEMPORAL"
|
|
142
145
|
header[f"DPNAME{axis_num}"] = "measurement number"
|
|
143
146
|
header[f"DWNAME{axis_num}"] = "time"
|
|
144
|
-
header[f"CNAME{axis_num}"] = "time"
|
|
145
147
|
header[f"DUNIT{axis_num}"] = "s"
|
|
146
148
|
# DINDEX and CNCMEAS are both one-based
|
|
147
|
-
header[f"DINDEX{axis_num}"] = header[
|
|
149
|
+
header[f"DINDEX{axis_num}"] = header[CryonirspMetadataKey.meas_num]
|
|
148
150
|
next_axis = axis_num + 1
|
|
149
151
|
return next_axis
|
|
150
152
|
|
|
@@ -159,7 +161,6 @@ class CryonirspWriteL1Frame(WriteL1Frame, ABC, InputDatasetMixin):
|
|
|
159
161
|
header[f"DTYPE{axis_num}"] = "TEMPORAL"
|
|
160
162
|
header[f"DPNAME{axis_num}"] = "map scan number"
|
|
161
163
|
header[f"DWNAME{axis_num}"] = "time"
|
|
162
|
-
header[f"CNAME{axis_num}"] = "time"
|
|
163
164
|
header[f"DUNIT{axis_num}"] = "s"
|
|
164
165
|
# Temporal position in dataset
|
|
165
166
|
# DINDEX and CNMAP are both one-based
|
|
@@ -175,7 +176,6 @@ class CryonirspWriteL1Frame(WriteL1Frame, ABC, InputDatasetMixin):
|
|
|
175
176
|
header[f"DTYPE{axis_num}"] = "STOKES"
|
|
176
177
|
header[f"DPNAME{axis_num}"] = "polarization state"
|
|
177
178
|
header[f"DWNAME{axis_num}"] = "polarization state"
|
|
178
|
-
header[f"CNAME{axis_num}"] = "polarization state"
|
|
179
179
|
header[f"DUNIT{axis_num}"] = ""
|
|
180
180
|
# Stokes position in dataset - stokes axis goes from 1-4
|
|
181
181
|
header[f"DINDEX{axis_num}"] = self.constants.stokes_params.index(stokes.upper()) + 1
|
|
@@ -204,6 +204,9 @@ class CryonirspWriteL1Frame(WriteL1Frame, ABC, InputDatasetMixin):
|
|
|
204
204
|
header["NBIN3"] = 1
|
|
205
205
|
header["NBIN"] = header["NBIN1"] * header["NBIN2"] * header["NBIN3"]
|
|
206
206
|
|
|
207
|
+
# Values don't have any units because they are relative to disk center
|
|
208
|
+
header["BUNIT"] = ("", "Values are relative to disk center. See calibration docs.")
|
|
209
|
+
|
|
207
210
|
def calculate_date_end(self, header: fits.Header) -> str:
|
|
208
211
|
"""
|
|
209
212
|
In CryoNIRSP, the instrument specific DATE-END keyword is calculated during science calibration.
|
|
@@ -277,16 +280,16 @@ class CryonirspWriteL1Frame(WriteL1Frame, ABC, InputDatasetMixin):
|
|
|
277
280
|
-------
|
|
278
281
|
None
|
|
279
282
|
"""
|
|
280
|
-
t0 = Time(header[
|
|
283
|
+
t0 = Time(header[MetadataKey.time_obs])
|
|
281
284
|
sky_coordinates = SkyCoord(
|
|
282
285
|
boresight_coordinates[0] * u.arcsec,
|
|
283
286
|
boresight_coordinates[1] * u.arcsec,
|
|
284
287
|
obstime=t0,
|
|
285
|
-
observer=
|
|
288
|
+
observer=location_of_dkist.get_itrs(t0),
|
|
286
289
|
frame="helioprojective",
|
|
287
290
|
)
|
|
288
291
|
|
|
289
|
-
frame_altaz = AltAz(obstime=t0, location=
|
|
292
|
+
frame_altaz = AltAz(obstime=t0, location=location_of_dkist)
|
|
290
293
|
|
|
291
294
|
# Find angle between zenith and solar north at the center boresight pointing
|
|
292
295
|
solar_orientation_angle = self.get_solar_orientation_angle(
|
|
@@ -348,7 +351,7 @@ class CryonirspWriteL1Frame(WriteL1Frame, ABC, InputDatasetMixin):
|
|
|
348
351
|
boresight_coordinates[0] * u.arcsec,
|
|
349
352
|
(boresight_coordinates[1] + 1) * u.arcsec,
|
|
350
353
|
obstime=t0,
|
|
351
|
-
observer=
|
|
354
|
+
observer=location_of_dkist.get_itrs(t0),
|
|
352
355
|
frame="helioprojective",
|
|
353
356
|
)
|
|
354
357
|
|
|
@@ -396,7 +399,7 @@ class CryonirspWriteL1Frame(WriteL1Frame, ABC, InputDatasetMixin):
|
|
|
396
399
|
"""Get CryoNIRSP slit orientation measured relative to solar north at time of observation."""
|
|
397
400
|
with Helioprojective.assume_spherical_screen(sky_coordinates.observer):
|
|
398
401
|
sc_alt = sky_coordinates.transform_to(frame_altaz)
|
|
399
|
-
coude_minus_azimuth_elevation = header[
|
|
402
|
+
coude_minus_azimuth_elevation = header[MetadataKey.table_angle] * u.deg - (
|
|
400
403
|
(sc_alt.az.deg - sc_alt.alt.deg) * u.deg
|
|
401
404
|
)
|
|
402
405
|
cryo_instrument_alignment_angle = self.parameters.cryo_instrument_alignment_angle
|
|
@@ -470,6 +473,32 @@ class CryonirspWriteL1Frame(WriteL1Frame, ABC, InputDatasetMixin):
|
|
|
470
473
|
"""Update spectral headers after spectral correction."""
|
|
471
474
|
pass
|
|
472
475
|
|
|
476
|
+
def add_timing_headers(self, header: fits.Header) -> fits.Header:
|
|
477
|
+
"""
|
|
478
|
+
Add timing headers to the FITS header.
|
|
479
|
+
|
|
480
|
+
This method adds or updates headers related to frame timings.
|
|
481
|
+
"""
|
|
482
|
+
# The source data is based on L0 data but L1 data takes L0 cadence * modstates * number of measurements to obtain.
|
|
483
|
+
# This causes the cadence to be num_modstates * num_measurements times longer.
|
|
484
|
+
# This causes the exposure time to be num_modstates times longer.
|
|
485
|
+
header["CADENCE"] = (
|
|
486
|
+
self.constants.average_cadence * self.constants.num_modstates * self.constants.num_meas
|
|
487
|
+
)
|
|
488
|
+
header["CADMIN"] = (
|
|
489
|
+
self.constants.minimum_cadence * self.constants.num_modstates * self.constants.num_meas
|
|
490
|
+
)
|
|
491
|
+
header["CADMAX"] = (
|
|
492
|
+
self.constants.maximum_cadence * self.constants.num_modstates * self.constants.num_meas
|
|
493
|
+
)
|
|
494
|
+
header["CADVAR"] = (
|
|
495
|
+
self.constants.variance_cadence * self.constants.num_modstates * self.constants.num_meas
|
|
496
|
+
)
|
|
497
|
+
header[MetadataKey.fpa_exposure_time_ms] = (
|
|
498
|
+
header[MetadataKey.fpa_exposure_time_ms] * self.constants.num_modstates
|
|
499
|
+
)
|
|
500
|
+
return header
|
|
501
|
+
|
|
473
502
|
|
|
474
503
|
class CIWriteL1Frame(CryonirspWriteL1Frame):
|
|
475
504
|
"""
|
|
@@ -518,10 +547,11 @@ class CIWriteL1Frame(CryonirspWriteL1Frame):
|
|
|
518
547
|
header[f"DTYPE{axis_num}"] = "TEMPORAL"
|
|
519
548
|
header[f"DPNAME{axis_num}"] = "scan step number"
|
|
520
549
|
header[f"DWNAME{axis_num}"] = "time"
|
|
521
|
-
|
|
550
|
+
if axis_num == 3:
|
|
551
|
+
header[f"CNAME{axis_num}"] = "time"
|
|
522
552
|
header[f"DUNIT{axis_num}"] = "s"
|
|
523
553
|
# DINDEX and CNCURSCN are both one-based
|
|
524
|
-
header[f"DINDEX{axis_num}"] = header[
|
|
554
|
+
header[f"DINDEX{axis_num}"] = header[CryonirspMetadataKey.scan_step]
|
|
525
555
|
next_axis = axis_num + 1
|
|
526
556
|
return next_axis
|
|
527
557
|
|
|
@@ -535,7 +565,7 @@ class CIWriteL1Frame(CryonirspWriteL1Frame):
|
|
|
535
565
|
|
|
536
566
|
Range is the wavelengths at the edges of the filter full width half maximum.
|
|
537
567
|
"""
|
|
538
|
-
filter_central_wavelength = header[
|
|
568
|
+
filter_central_wavelength = header[CryonirspMetadataKey.center_wavelength] * u.nm
|
|
539
569
|
filter_fwhm = header["CNFWHM"] * u.nm
|
|
540
570
|
return WavelengthRange(
|
|
541
571
|
min=filter_central_wavelength - (filter_fwhm / 2),
|
|
@@ -695,7 +725,7 @@ class CIWriteL1Frame(CryonirspWriteL1Frame):
|
|
|
695
725
|
observed_pix_x_rotated * u.arcsec,
|
|
696
726
|
observed_pix_y_rotated * u.arcsec,
|
|
697
727
|
obstime=t0,
|
|
698
|
-
observer=
|
|
728
|
+
observer=location_of_dkist.get_itrs(t0),
|
|
699
729
|
frame="helioprojective",
|
|
700
730
|
)
|
|
701
731
|
|
|
@@ -709,7 +739,7 @@ class CIWriteL1Frame(CryonirspWriteL1Frame):
|
|
|
709
739
|
x * u.arcsec,
|
|
710
740
|
y * u.arcsec,
|
|
711
741
|
obstime=t0,
|
|
712
|
-
observer=
|
|
742
|
+
observer=location_of_dkist.get_itrs(t0),
|
|
713
743
|
frame="helioprojective",
|
|
714
744
|
)
|
|
715
745
|
with Helioprojective.assume_spherical_screen(sky_coord.observer):
|
|
@@ -801,11 +831,12 @@ class SPWriteL1Frame(CryonirspWriteL1Frame):
|
|
|
801
831
|
header[f"DTYPE{axis_num}"] = "SPATIAL"
|
|
802
832
|
header[f"DPNAME{axis_num}"] = "scan step number"
|
|
803
833
|
header[f"DWNAME{axis_num}"] = "helioprojective longitude"
|
|
804
|
-
|
|
834
|
+
if axis_num == 3:
|
|
835
|
+
header[f"CNAME{axis_num}"] = "helioprojective longitude"
|
|
805
836
|
# NB: CUNIT axis number is hard coded here
|
|
806
837
|
header[f"DUNIT{axis_num}"] = header[f"CUNIT3"]
|
|
807
838
|
# DINDEX and CNCURSCN are both one-based
|
|
808
|
-
header[f"DINDEX{axis_num}"] = header[
|
|
839
|
+
header[f"DINDEX{axis_num}"] = header[CryonirspMetadataKey.scan_step]
|
|
809
840
|
next_axis = axis_num + 1
|
|
810
841
|
return next_axis
|
|
811
842
|
|
|
@@ -966,7 +997,7 @@ class SPWriteL1Frame(CryonirspWriteL1Frame):
|
|
|
966
997
|
observed_pix_x_rotated * u.arcsec,
|
|
967
998
|
observed_pix_y_rotated * u.arcsec,
|
|
968
999
|
obstime=t0,
|
|
969
|
-
observer=
|
|
1000
|
+
observer=location_of_dkist.get_itrs(t0),
|
|
970
1001
|
frame="helioprojective",
|
|
971
1002
|
)
|
|
972
1003
|
with Helioprojective.assume_spherical_screen(sky_coord.observer):
|
|
@@ -979,7 +1010,7 @@ class SPWriteL1Frame(CryonirspWriteL1Frame):
|
|
|
979
1010
|
x * u.arcsec,
|
|
980
1011
|
y * u.arcsec,
|
|
981
1012
|
obstime=t0,
|
|
982
|
-
observer=
|
|
1013
|
+
observer=location_of_dkist.get_itrs(t0),
|
|
983
1014
|
frame="helioprojective",
|
|
984
1015
|
)
|
|
985
1016
|
with Helioprojective.assume_spherical_screen(sky_coord.observer):
|
|
@@ -1018,24 +1049,14 @@ class SPWriteL1Frame(CryonirspWriteL1Frame):
|
|
|
1018
1049
|
|
|
1019
1050
|
@cached_property
|
|
1020
1051
|
def spectral_fit_results(self) -> dict:
|
|
1021
|
-
"""Get the spectral fit results."""
|
|
1022
|
-
|
|
1052
|
+
"""Get the spectral fit results from disk."""
|
|
1053
|
+
return next(
|
|
1023
1054
|
self.read(
|
|
1024
|
-
tags=[CryonirspTag.
|
|
1025
|
-
decoder=
|
|
1055
|
+
tags=[CryonirspTag.intermediate(), CryonirspTag.task_spectral_fit()],
|
|
1056
|
+
decoder=json_decoder,
|
|
1026
1057
|
)
|
|
1027
1058
|
)
|
|
1028
|
-
del fit_dict["asdf_library"]
|
|
1029
|
-
del fit_dict["history"]
|
|
1030
|
-
return fit_dict
|
|
1031
1059
|
|
|
1032
1060
|
def update_spectral_headers(self, header: fits.Header):
|
|
1033
1061
|
"""Update spectral headers after spectral correction."""
|
|
1034
|
-
|
|
1035
|
-
# update the headers
|
|
1036
|
-
header[key] = value
|
|
1037
|
-
|
|
1038
|
-
header["CTYPE1"] = "AWAV-GRA"
|
|
1039
|
-
header["CUNIT1"] = "nm"
|
|
1040
|
-
header["CTYPE1A"] = "AWAV-GRA"
|
|
1041
|
-
header["CUNIT1A"] = "nm"
|
|
1062
|
+
header.update(self.spectral_fit_results)
|