dkist-processing-cryonirsp 1.8.3__py3-none-any.whl → 1.10.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/167.feature.rst +1 -0
- dkist_processing_cryonirsp/config.py +4 -1
- dkist_processing_cryonirsp/models/constants.py +14 -1
- dkist_processing_cryonirsp/models/parameters.py +11 -19
- dkist_processing_cryonirsp/parsers/cryonirsp_l0_fits_access.py +3 -2
- dkist_processing_cryonirsp/tasks/__init__.py +1 -1
- dkist_processing_cryonirsp/tasks/parse.py +15 -3
- dkist_processing_cryonirsp/tasks/sp_wavelength_calibration.py +300 -0
- dkist_processing_cryonirsp/tasks/write_l1.py +6 -16
- dkist_processing_cryonirsp/tests/conftest.py +13 -20
- dkist_processing_cryonirsp/tests/header_models.py +12 -0
- dkist_processing_cryonirsp/tests/local_trial_workflows/l0_cals_only.py +17 -17
- dkist_processing_cryonirsp/tests/local_trial_workflows/l0_to_l1.py +17 -19
- dkist_processing_cryonirsp/tests/local_trial_workflows/local_trial_helpers.py +3 -3
- dkist_processing_cryonirsp/tests/test_cryo_constants.py +4 -1
- dkist_processing_cryonirsp/tests/test_parameters.py +4 -0
- dkist_processing_cryonirsp/tests/test_parse.py +28 -2
- dkist_processing_cryonirsp/tests/{test_sp_dispersion_axis_correction.py → test_sp_wavelength_calibration.py} +6 -23
- dkist_processing_cryonirsp/tests/test_write_l1.py +15 -15
- dkist_processing_cryonirsp/workflows/ci_l0_processing.py +4 -1
- dkist_processing_cryonirsp/workflows/sp_l0_processing.py +7 -4
- dkist_processing_cryonirsp/workflows/trial_workflows.py +3 -3
- {dkist_processing_cryonirsp-1.8.3.dist-info → dkist_processing_cryonirsp-1.10.0rc1.dist-info}/METADATA +22 -21
- {dkist_processing_cryonirsp-1.8.3.dist-info → dkist_processing_cryonirsp-1.10.0rc1.dist-info}/RECORD +28 -26
- docs/index.rst +1 -0
- docs/wavelength_calibration.rst +62 -0
- dkist_processing_cryonirsp/tasks/sp_dispersion_axis_correction.py +0 -465
- {dkist_processing_cryonirsp-1.8.3.dist-info → dkist_processing_cryonirsp-1.10.0rc1.dist-info}/WHEEL +0 -0
- {dkist_processing_cryonirsp-1.8.3.dist-info → dkist_processing_cryonirsp-1.10.0rc1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
This change pulls the dispersion axis correction out of the Cryonirsp pipeline and uses the generalized solar-wavelength-calibration library.
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
"""Configuration for the dkist-processing-cryonirsp package and the logging thereof."""
|
|
2
2
|
from dkist_processing_common.config import DKISTProcessingCommonConfiguration
|
|
3
|
+
from pydantic import Field
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
class DKISTProcessingCryoNIRSPConfigurations(DKISTProcessingCommonConfiguration):
|
|
6
7
|
"""Configurations custom to the dkist-processing-cryonirsp package."""
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
fts_atlas_data_dir: str | None = Field(
|
|
10
|
+
default=None, description="Common cached directory for a downloaded FTS Atlas."
|
|
11
|
+
)
|
|
9
12
|
|
|
10
13
|
|
|
11
14
|
dkist_processing_cryonirsp_configurations = DKISTProcessingCryoNIRSPConfigurations()
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
from enum import Enum
|
|
3
3
|
from enum import unique
|
|
4
4
|
|
|
5
|
+
import astropy.units as u
|
|
5
6
|
from dkist_processing_common.models.constants import BudName
|
|
6
7
|
from dkist_processing_common.models.constants import ConstantsBase
|
|
7
8
|
|
|
@@ -54,6 +55,8 @@ class CryonirspBudName(Enum):
|
|
|
54
55
|
gain_frame_type_list = "GAIN_FRAME_TYPE_LIST"
|
|
55
56
|
lamp_gain_frame_type_list = "LAMP_GAIN_FRAME_TYPE_LIST"
|
|
56
57
|
solar_gain_frame_type_list = "SOLAR_GAIN_FRAME_TYPE_LIST"
|
|
58
|
+
center_wavelength = "CENTER_WAVELENGTH"
|
|
59
|
+
slit_width = "SLIT_WIDTH"
|
|
57
60
|
|
|
58
61
|
|
|
59
62
|
class CryonirspConstants(ConstantsBase):
|
|
@@ -112,10 +115,20 @@ class CryonirspConstants(ConstantsBase):
|
|
|
112
115
|
"""Grating littrow angle (deg)."""
|
|
113
116
|
return self._db_dict[CryonirspBudName.grating_littrow_angle_deg.value]
|
|
114
117
|
|
|
118
|
+
@property
|
|
119
|
+
def center_wavelength(self) -> float:
|
|
120
|
+
"""Center wavelength of the selected filter (nm)."""
|
|
121
|
+
return self._db_dict[CryonirspBudName.center_wavelength.value]
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def slit_width(self) -> float:
|
|
125
|
+
"""Physical width of the selected slit (um)."""
|
|
126
|
+
return self._db_dict[CryonirspBudName.slit_width.value]
|
|
127
|
+
|
|
115
128
|
@property
|
|
116
129
|
def grating_constant(self) -> float:
|
|
117
130
|
"""Grating constant."""
|
|
118
|
-
return self._db_dict[CryonirspBudName.grating_constant.value]
|
|
131
|
+
return self._db_dict[CryonirspBudName.grating_constant.value] / u.mm
|
|
119
132
|
|
|
120
133
|
@property
|
|
121
134
|
def camera_readout_mode(self) -> str:
|
|
@@ -7,6 +7,7 @@ import numpy as np
|
|
|
7
7
|
from dkist_processing_common.models.parameters import ParameterArmIdMixin
|
|
8
8
|
from dkist_processing_common.models.parameters import ParameterBase
|
|
9
9
|
from dkist_processing_common.models.parameters import ParameterWavelengthMixin
|
|
10
|
+
from solar_wavelength_calibration import DownloadConfig
|
|
10
11
|
|
|
11
12
|
from dkist_processing_cryonirsp.models.exposure_conditions import AllowableOpticalDensityFilterNames
|
|
12
13
|
|
|
@@ -267,24 +268,15 @@ class CryonirspParameters(ParameterBase, ParameterWavelengthMixin, ParameterArmI
|
|
|
267
268
|
"""Return the CryoNIRSP pixel pitch."""
|
|
268
269
|
return self._find_most_recent_past_value("cryonirsp_pixel_pitch_micron") * u.micron
|
|
269
270
|
|
|
270
|
-
@
|
|
271
|
-
def
|
|
272
|
-
"""Solar
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
- wavelength in nanometers
|
|
276
|
-
- transmission at given wavelength
|
|
277
|
-
"""
|
|
278
|
-
param_obj = self._find_most_recent_past_value("cryonirsp_solar_atlas")
|
|
279
|
-
return self._load_param_value_from_numpy_save(param_obj=param_obj)
|
|
271
|
+
@property
|
|
272
|
+
def wavecal_atlas_download_config(self) -> DownloadConfig:
|
|
273
|
+
"""Define the `~solar_wavelength_calibration.DownloadConfig` used to grab the Solar atlas used for wavelength calibration."""
|
|
274
|
+
config_dict = self._find_most_recent_past_value("cryonirsp_wavecal_atlas_download_config")
|
|
275
|
+
return DownloadConfig.model_validate(config_dict)
|
|
280
276
|
|
|
281
277
|
@cached_property
|
|
282
|
-
def
|
|
283
|
-
"""
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
- transmission at given wavelength
|
|
288
|
-
"""
|
|
289
|
-
param_obj = self._find_most_recent_past_value("cryonirsp_telluric_atlas")
|
|
290
|
-
return self._load_param_value_from_numpy_save(param_obj=param_obj)
|
|
278
|
+
def wavecal_fraction_of_unweighted_edge_pixels(self) -> int:
|
|
279
|
+
"""Return the fraction of edge pixels to weight to zero during the wavelength calibration."""
|
|
280
|
+
return self._find_most_recent_past_value(
|
|
281
|
+
"cryonirsp_wavecal_fraction_of_unweighted_edge_pixels"
|
|
282
|
+
)
|
|
@@ -97,8 +97,7 @@ class CryonirspL0FitsAccess(L0FitsAccess):
|
|
|
97
97
|
)
|
|
98
98
|
self.grating_position_deg: float = self.header["CNGRTPOS"]
|
|
99
99
|
self.grating_littrow_angle_deg: float = self.header["CNGRTLAT"]
|
|
100
|
-
|
|
101
|
-
self.grating_constant: float = self.header["CNGRTCON"] * 1000
|
|
100
|
+
self.grating_constant: float = self.header["CNGRTCON"]
|
|
102
101
|
self.obs_ip_start_time = self.header["DKIST011"]
|
|
103
102
|
# The ExposureConditions are a combination of the exposure time and the OD filter name:
|
|
104
103
|
self.exposure_conditions = ExposureConditions(
|
|
@@ -106,6 +105,8 @@ class CryonirspL0FitsAccess(L0FitsAccess):
|
|
|
106
105
|
self.header["CNFILTNP"].upper(),
|
|
107
106
|
)
|
|
108
107
|
self.solar_gain_ip_start_time = self.header["DATE-OBS"]
|
|
108
|
+
self.center_wavelength = self.header["CNCENWAV"]
|
|
109
|
+
self.slit_width = self.header["CNSLITW"]
|
|
109
110
|
|
|
110
111
|
@property
|
|
111
112
|
def cn1_scan_step(self):
|
|
@@ -11,8 +11,8 @@ from dkist_processing_cryonirsp.tasks.make_movie_frames import *
|
|
|
11
11
|
from dkist_processing_cryonirsp.tasks.parse import *
|
|
12
12
|
from dkist_processing_cryonirsp.tasks.quality_metrics import *
|
|
13
13
|
from dkist_processing_cryonirsp.tasks.sp_beam_boundaries import *
|
|
14
|
-
from dkist_processing_cryonirsp.tasks.sp_dispersion_axis_correction import *
|
|
15
14
|
from dkist_processing_cryonirsp.tasks.sp_geometric import *
|
|
16
15
|
from dkist_processing_cryonirsp.tasks.sp_science import *
|
|
17
16
|
from dkist_processing_cryonirsp.tasks.sp_solar_gain import *
|
|
17
|
+
from dkist_processing_cryonirsp.tasks.sp_wavelength_calibration import *
|
|
18
18
|
from dkist_processing_cryonirsp.tasks.write_l1 import *
|
|
@@ -247,21 +247,21 @@ class ParseL0CryonirspSPLinearizedData(ParseL0CryonirspLinearizedData):
|
|
|
247
247
|
TaskNearFloatBud(
|
|
248
248
|
constant_name=CryonirspBudName.grating_position_deg.value,
|
|
249
249
|
metadata_key="grating_position_deg",
|
|
250
|
-
ip_task_types=TaskName.solar_gain.value,
|
|
250
|
+
ip_task_types=[TaskName.observe.value, TaskName.solar_gain.value],
|
|
251
251
|
task_type_parsing_function=parse_header_ip_task_with_gains,
|
|
252
252
|
tolerance=0.01,
|
|
253
253
|
),
|
|
254
254
|
TaskNearFloatBud(
|
|
255
255
|
constant_name=CryonirspBudName.grating_littrow_angle_deg.value,
|
|
256
256
|
metadata_key="grating_littrow_angle_deg",
|
|
257
|
-
ip_task_types=TaskName.solar_gain.value,
|
|
257
|
+
ip_task_types=[TaskName.observe.value, TaskName.solar_gain.value],
|
|
258
258
|
task_type_parsing_function=parse_header_ip_task_with_gains,
|
|
259
259
|
tolerance=0.01,
|
|
260
260
|
),
|
|
261
261
|
TaskUniqueBud(
|
|
262
262
|
constant_name=CryonirspBudName.grating_constant.value,
|
|
263
263
|
metadata_key="grating_constant",
|
|
264
|
-
ip_task_types=TaskName.solar_gain.value,
|
|
264
|
+
ip_task_types=[TaskName.observe.value, TaskName.solar_gain.value],
|
|
265
265
|
task_type_parsing_function=parse_header_ip_task_with_gains,
|
|
266
266
|
),
|
|
267
267
|
TaskUniqueBud(
|
|
@@ -276,6 +276,18 @@ class ParseL0CryonirspSPLinearizedData(ParseL0CryonirspLinearizedData):
|
|
|
276
276
|
ip_task_types=TaskName.solar_gain.value,
|
|
277
277
|
task_type_parsing_function=parse_header_ip_task_with_gains,
|
|
278
278
|
),
|
|
279
|
+
TaskUniqueBud(
|
|
280
|
+
constant_name=CryonirspBudName.center_wavelength.value,
|
|
281
|
+
metadata_key="center_wavelength",
|
|
282
|
+
ip_task_types=[TaskName.observe.value, TaskName.solar_gain.value],
|
|
283
|
+
task_type_parsing_function=parse_header_ip_task_with_gains,
|
|
284
|
+
),
|
|
285
|
+
TaskUniqueBud(
|
|
286
|
+
constant_name=CryonirspBudName.slit_width.value,
|
|
287
|
+
metadata_key="slit_width",
|
|
288
|
+
ip_task_types=[TaskName.observe.value, TaskName.solar_gain.value],
|
|
289
|
+
task_type_parsing_function=parse_header_ip_task_with_gains,
|
|
290
|
+
),
|
|
279
291
|
CheckLampGainFramesPickyBud(),
|
|
280
292
|
CryonirspTaskExposureConditionsBud(
|
|
281
293
|
stem_name=CryonirspBudName.lamp_gain_exposure_conditions_list.value,
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"""Cryo SP wavelength calibration task. See :doc:`this page </wavelength_calibration>` for more information."""
|
|
2
|
+
import math
|
|
3
|
+
|
|
4
|
+
import astropy.units as u
|
|
5
|
+
import numpy as np
|
|
6
|
+
from astropy.time import Time
|
|
7
|
+
from astropy.wcs import WCS
|
|
8
|
+
from dkist_processing_common.codecs.json import json_encoder
|
|
9
|
+
from dkist_processing_common.models.dkist_location import location_of_dkist
|
|
10
|
+
from dkist_service_configuration.logging import logger
|
|
11
|
+
from solar_wavelength_calibration import Atlas
|
|
12
|
+
from solar_wavelength_calibration import WavelengthCalibrationFitter
|
|
13
|
+
from solar_wavelength_calibration.fitter.parameters import AngleBoundRange
|
|
14
|
+
from solar_wavelength_calibration.fitter.parameters import BoundsModel
|
|
15
|
+
from solar_wavelength_calibration.fitter.parameters import DispersionBoundRange
|
|
16
|
+
from solar_wavelength_calibration.fitter.parameters import LengthBoundRange
|
|
17
|
+
from solar_wavelength_calibration.fitter.parameters import UnitlessBoundRange
|
|
18
|
+
from solar_wavelength_calibration.fitter.parameters import WavelengthCalibrationParameters
|
|
19
|
+
from solar_wavelength_calibration.fitter.wavelength_fitter import calculate_initial_crval_guess
|
|
20
|
+
from solar_wavelength_calibration.fitter.wavelength_fitter import WavelengthParameters
|
|
21
|
+
from sunpy.coordinates import HeliocentricInertial
|
|
22
|
+
|
|
23
|
+
from dkist_processing_cryonirsp.codecs.fits import cryo_fits_array_decoder
|
|
24
|
+
from dkist_processing_cryonirsp.models.tags import CryonirspTag
|
|
25
|
+
from dkist_processing_cryonirsp.tasks.cryonirsp_base import CryonirspTaskBase
|
|
26
|
+
|
|
27
|
+
__all__ = ["SPWavelengthCalibration"]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SPWavelengthCalibration(CryonirspTaskBase):
|
|
31
|
+
"""Task class for correcting the dispersion axis wavelength values.
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
recipe_run_id : int
|
|
36
|
+
id of the recipe run used to identify the workflow run this task is part of
|
|
37
|
+
workflow_name : str
|
|
38
|
+
name of the workflow to which this instance of the task belongs
|
|
39
|
+
workflow_version : str
|
|
40
|
+
version of the workflow to which this instance of the task belongs
|
|
41
|
+
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
record_provenance = True
|
|
45
|
+
|
|
46
|
+
def run(self) -> None:
|
|
47
|
+
"""
|
|
48
|
+
Run method for the task.
|
|
49
|
+
|
|
50
|
+
- Gather 1D characteristic spectrum. This will be the initial spectrum.
|
|
51
|
+
- Get a header from a solar gain frame for initial wavelength estimation.
|
|
52
|
+
- Compute the theoretical dispersion, order, and incident light angle.
|
|
53
|
+
- Compute the input wavelength vector from the spectrum and header.
|
|
54
|
+
- Get the Doppler velocity and resolving power.
|
|
55
|
+
- Define fitting bounds and initialize model parameters.
|
|
56
|
+
- Set and normalize spectral weights, zeroing edges.
|
|
57
|
+
- Fit the profile using WavelengthCalibrationFitter.
|
|
58
|
+
- Write fit results to disk.
|
|
59
|
+
|
|
60
|
+
Returns
|
|
61
|
+
-------
|
|
62
|
+
None
|
|
63
|
+
"""
|
|
64
|
+
with self.apm_processing_step("Load input spectrum and wavelength"):
|
|
65
|
+
logger.info("Loading input spectrum")
|
|
66
|
+
input_spectrum = next(
|
|
67
|
+
self.read(
|
|
68
|
+
tags=[
|
|
69
|
+
CryonirspTag.intermediate_frame(beam=1),
|
|
70
|
+
CryonirspTag.task_characteristic_spectra(),
|
|
71
|
+
],
|
|
72
|
+
decoder=cryo_fits_array_decoder,
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
logger.info(
|
|
77
|
+
"Computing instrument specific dispersion, order, and incident light angle."
|
|
78
|
+
)
|
|
79
|
+
(
|
|
80
|
+
dispersion,
|
|
81
|
+
order,
|
|
82
|
+
incident_light_angle,
|
|
83
|
+
) = self.get_theoretical_dispersion_order_light_angle()
|
|
84
|
+
|
|
85
|
+
logger.info("Computing initial wavelength vector.")
|
|
86
|
+
input_wavelength_vector = self.compute_input_wavelength_vector(
|
|
87
|
+
spectrum=input_spectrum,
|
|
88
|
+
dispersion=dispersion,
|
|
89
|
+
order=order,
|
|
90
|
+
incident_light_angle=incident_light_angle,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Get the doppler velocity
|
|
94
|
+
doppler_velocity = self.get_doppler_velocity()
|
|
95
|
+
logger.info(f"{doppler_velocity = !s}")
|
|
96
|
+
|
|
97
|
+
# Get the resolving power
|
|
98
|
+
resolving_power = self.get_resolving_power()
|
|
99
|
+
logger.info(f"{resolving_power = }")
|
|
100
|
+
|
|
101
|
+
with self.apm_processing_step("Compute brute-force CRVAL initial guess"):
|
|
102
|
+
atlas = Atlas(config=self.parameters.wavecal_atlas_download_config)
|
|
103
|
+
crval = calculate_initial_crval_guess(
|
|
104
|
+
input_wavelength_vector=input_wavelength_vector,
|
|
105
|
+
input_spectrum=input_spectrum,
|
|
106
|
+
atlas=atlas,
|
|
107
|
+
negative_limit=-2 * u.nm,
|
|
108
|
+
positive_limit=2 * u.nm,
|
|
109
|
+
num_steps=550,
|
|
110
|
+
)
|
|
111
|
+
logger.info(f"{crval = !s}")
|
|
112
|
+
|
|
113
|
+
with self.apm_task_step("Set up wavelength fit"):
|
|
114
|
+
logger.info("Setting bounds")
|
|
115
|
+
bounds = BoundsModel(
|
|
116
|
+
crval=LengthBoundRange(min=crval - (5 * u.nm), max=crval + (5 * u.nm)),
|
|
117
|
+
dispersion=DispersionBoundRange(
|
|
118
|
+
min=dispersion - (0.05 * u.nm / u.pix), max=dispersion + (0.05 * u.nm / u.pix)
|
|
119
|
+
),
|
|
120
|
+
incident_light_angle=AngleBoundRange(
|
|
121
|
+
min=incident_light_angle - (180 * u.deg),
|
|
122
|
+
max=incident_light_angle + (180 * u.deg),
|
|
123
|
+
),
|
|
124
|
+
resolving_power=UnitlessBoundRange(
|
|
125
|
+
min=resolving_power - (resolving_power * 0.1),
|
|
126
|
+
max=resolving_power + (resolving_power * 0.1),
|
|
127
|
+
),
|
|
128
|
+
opacity_factor=UnitlessBoundRange(min=0.0, max=10.0),
|
|
129
|
+
straylight_fraction=UnitlessBoundRange(min=0.0, max=0.4),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
logger.info("Initializing parameters")
|
|
133
|
+
input_parameters = WavelengthCalibrationParameters(
|
|
134
|
+
crval=crval,
|
|
135
|
+
dispersion=dispersion,
|
|
136
|
+
incident_light_angle=incident_light_angle,
|
|
137
|
+
resolving_power=resolving_power,
|
|
138
|
+
opacity_factor=5.0,
|
|
139
|
+
straylight_fraction=0.2,
|
|
140
|
+
grating_constant=self.constants.grating_constant,
|
|
141
|
+
doppler_velocity=doppler_velocity,
|
|
142
|
+
order=order,
|
|
143
|
+
bounds=bounds,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Define spectral weights to apply
|
|
147
|
+
weights = np.ones_like(input_spectrum)
|
|
148
|
+
# Set edge weights to zero to mitigate flat field artifacts (inner and outer 10% of array)
|
|
149
|
+
num_pixels = len(weights)
|
|
150
|
+
weights[: num_pixels // self.parameters.wavecal_fraction_of_unweighted_edge_pixels] = 0
|
|
151
|
+
weights[-num_pixels // self.parameters.wavecal_fraction_of_unweighted_edge_pixels :] = 0
|
|
152
|
+
|
|
153
|
+
fitter = WavelengthCalibrationFitter(
|
|
154
|
+
input_parameters=input_parameters,
|
|
155
|
+
atlas=atlas,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
logger.info(f"Input parameters: {input_parameters.lmfit_parameters.pretty_repr()}")
|
|
159
|
+
|
|
160
|
+
with self.apm_processing_step("Run wavelength solution fit"):
|
|
161
|
+
fit_result = fitter(
|
|
162
|
+
input_wavelength_vector=input_wavelength_vector,
|
|
163
|
+
input_spectrum=input_spectrum,
|
|
164
|
+
spectral_weights=weights,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
with self.apm_writing_step("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_ip_start_time))
|
|
263
|
+
heliocentric_coord = coord.transform_to(
|
|
264
|
+
HeliocentricInertial(obstime=Time(self.constants.solar_gain_ip_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]
|
|
@@ -11,7 +11,7 @@ from astropy.coordinates.builtin_frames.altaz import AltAz
|
|
|
11
11
|
from astropy.coordinates.sky_coordinate import SkyCoord
|
|
12
12
|
from astropy.io import fits
|
|
13
13
|
from astropy.time.core import Time
|
|
14
|
-
from dkist_processing_common.codecs.
|
|
14
|
+
from dkist_processing_common.codecs.json import json_decoder
|
|
15
15
|
from dkist_processing_common.models.dkist_location import location_of_dkist
|
|
16
16
|
from dkist_processing_common.models.wavelength import WavelengthRange
|
|
17
17
|
from dkist_processing_common.tasks import WriteL1Frame
|
|
@@ -1020,24 +1020,14 @@ class SPWriteL1Frame(CryonirspWriteL1Frame):
|
|
|
1020
1020
|
|
|
1021
1021
|
@cached_property
|
|
1022
1022
|
def spectral_fit_results(self) -> dict:
|
|
1023
|
-
"""Get the spectral fit results."""
|
|
1024
|
-
|
|
1023
|
+
"""Get the spectral fit results from disk."""
|
|
1024
|
+
return next(
|
|
1025
1025
|
self.read(
|
|
1026
|
-
tags=[CryonirspTag.
|
|
1027
|
-
decoder=
|
|
1026
|
+
tags=[CryonirspTag.intermediate(), CryonirspTag.task_spectral_fit()],
|
|
1027
|
+
decoder=json_decoder,
|
|
1028
1028
|
)
|
|
1029
1029
|
)
|
|
1030
|
-
del fit_dict["asdf_library"]
|
|
1031
|
-
del fit_dict["history"]
|
|
1032
|
-
return fit_dict
|
|
1033
1030
|
|
|
1034
1031
|
def update_spectral_headers(self, header: fits.Header):
|
|
1035
1032
|
"""Update spectral headers after spectral correction."""
|
|
1036
|
-
|
|
1037
|
-
# update the headers
|
|
1038
|
-
header[key] = value
|
|
1039
|
-
|
|
1040
|
-
header["CTYPE1"] = "AWAV-GRA"
|
|
1041
|
-
header["CUNIT1"] = "nm"
|
|
1042
|
-
header["CTYPE1A"] = "AWAV-GRA"
|
|
1043
|
-
header["CUNIT1A"] = "nm"
|
|
1033
|
+
header.update(self.spectral_fit_results)
|
|
@@ -166,7 +166,7 @@ class CryonirspConstantsDb:
|
|
|
166
166
|
"EXPERID3",
|
|
167
167
|
)
|
|
168
168
|
# These are SP defaults...
|
|
169
|
-
AXIS_1_TYPE: str = "AWAV"
|
|
169
|
+
AXIS_1_TYPE: str = "AWAV-GRA"
|
|
170
170
|
AXIS_2_TYPE: str = "HPLT-TAN"
|
|
171
171
|
AXIS_3_TYPE: str = "HPLN-TAN"
|
|
172
172
|
ROI_1_ORIGIN_X: int = 0
|
|
@@ -177,6 +177,8 @@ class CryonirspConstantsDb:
|
|
|
177
177
|
GRATING_LITTROW_ANGLE_DEG: float = -5.5
|
|
178
178
|
GRATING_CONSTANT: float = 31.6
|
|
179
179
|
SOLAR_GAIN_IP_START_TIME: str = "2021-01-01T00:00:00"
|
|
180
|
+
CENTER_WAVELENGTH: float = 1074.9
|
|
181
|
+
SLIT_WIDTH: int = 175
|
|
180
182
|
|
|
181
183
|
|
|
182
184
|
@pytest.fixture()
|
|
@@ -287,8 +289,6 @@ class FileParameter(InputDatasetFilePointer):
|
|
|
287
289
|
# and in CryonirspTestingParameters.
|
|
288
290
|
LINEARIZATION_THRESHOLDS_CI = "cryonirsp_linearization_thresholds_ci.npy"
|
|
289
291
|
LINEARIZATION_THRESHOLDS_SP = "cryonirsp_linearization_thresholds_sp.npy"
|
|
290
|
-
SOLAR_ATLAS = "cryonirsp_solar_atlas.npy"
|
|
291
|
-
TELLURIC_ATLAS = "cryonirsp_telluric_atlas.npy"
|
|
292
292
|
|
|
293
293
|
|
|
294
294
|
def _create_parameter_files(task: WorkflowTaskBase) -> None:
|
|
@@ -301,13 +301,6 @@ def _create_parameter_files(task: WorkflowTaskBase) -> None:
|
|
|
301
301
|
data=thresh, tags=CryonirspTag.parameter(LINEARIZATION_THRESHOLDS_SP), encoder=array_encoder
|
|
302
302
|
)
|
|
303
303
|
|
|
304
|
-
# solar and telluric atlases
|
|
305
|
-
atlas_wavelengths = range(300, 16600)
|
|
306
|
-
atlas_transmission = np.random.rand(16300)
|
|
307
|
-
atlas = [atlas_wavelengths, atlas_transmission]
|
|
308
|
-
task.write(data=atlas, tags=CryonirspTag.parameter(SOLAR_ATLAS), encoder=array_encoder)
|
|
309
|
-
task.write(data=atlas, tags=CryonirspTag.parameter(TELLURIC_ATLAS), encoder=array_encoder)
|
|
310
|
-
|
|
311
304
|
|
|
312
305
|
TestingParameters = TypeVar("TestingParameters", bound="CryonirspTestingParameters")
|
|
313
306
|
|
|
@@ -380,18 +373,18 @@ def cryonirsp_testing_parameters_factory(
|
|
|
380
373
|
cryonirsp_linearization_optical_density_filter_attenuation_g408: WavelengthParameter = (
|
|
381
374
|
WavelengthParameter(values=(-4.26, -4.26, -4.26, -4.26))
|
|
382
375
|
)
|
|
383
|
-
cryonirsp_solar_atlas: FileParameter = field(
|
|
384
|
-
default_factory=lambda: FileParameter(
|
|
385
|
-
object_key=SOLAR_ATLAS,
|
|
386
|
-
)
|
|
387
|
-
)
|
|
388
|
-
cryonirsp_telluric_atlas: FileParameter = field(
|
|
389
|
-
default_factory=lambda: FileParameter(
|
|
390
|
-
object_key=TELLURIC_ATLAS,
|
|
391
|
-
)
|
|
392
|
-
)
|
|
393
376
|
cryonirsp_camera_mirror_focal_length_mm: float = 932.0
|
|
394
377
|
cryonirsp_pixel_pitch_micron: float = 18.0
|
|
378
|
+
cryonirsp_wavecal_atlas_download_config: dict[str, str] = field(
|
|
379
|
+
default_factory=lambda: {
|
|
380
|
+
"base_url": "doi:10.5281/zenodo.14646787/",
|
|
381
|
+
"telluric_reference_atlas_file_name": "telluric_reference_atlas.npy",
|
|
382
|
+
"telluric_reference_atlas_hash_id": "md5:8db5e12508b293bca3495d81a0747447",
|
|
383
|
+
"solar_reference_atlas_file_name": "solar_reference_atlas.npy",
|
|
384
|
+
"solar_reference_atlas_hash_id": "md5:84ab4c50689ef235fe5ed4f7ee905ca0",
|
|
385
|
+
}
|
|
386
|
+
)
|
|
387
|
+
cryonirsp_wavecal_fraction_of_unweighted_edge_pixels: int = 10
|
|
395
388
|
|
|
396
389
|
return CryonirspTestingParameters
|
|
397
390
|
|
|
@@ -374,6 +374,8 @@ class SimpleModulatedHeaders(CryonirspHeaders):
|
|
|
374
374
|
),
|
|
375
375
|
start_date: str = "2023-01-01T01:23:45",
|
|
376
376
|
modstate_length_sec: float = 0.5,
|
|
377
|
+
center_wavelength: float = 1080.0,
|
|
378
|
+
slit_width: float = 52.0,
|
|
377
379
|
):
|
|
378
380
|
dataset_shape = (1, *array_shape)
|
|
379
381
|
super().__init__(
|
|
@@ -395,9 +397,11 @@ class SimpleModulatedHeaders(CryonirspHeaders):
|
|
|
395
397
|
self.add_constant_key("CRSP_042", modstate)
|
|
396
398
|
self.add_constant_key("CAM__004", exposure_condition.exposure_time)
|
|
397
399
|
self.add_constant_key("CRSP_048", exposure_condition.filter_name)
|
|
400
|
+
self.add_constant_key("CRSP_053", center_wavelength)
|
|
398
401
|
self.add_constant_key("CRSP_074", grating_angle_deg)
|
|
399
402
|
self.add_constant_key("CRSP_079", grating_littrow_angle)
|
|
400
403
|
self.add_constant_key("CRSP_077", grating_constant)
|
|
404
|
+
self.add_constant_key("CRSP_082", slit_width)
|
|
401
405
|
self.add_constant_key("CRVAL1", CRVAL1)
|
|
402
406
|
self.add_constant_key("CRPIX1", CRPIX1)
|
|
403
407
|
self.add_constant_key("CDELT1", CDELT1)
|
|
@@ -445,6 +449,8 @@ class ModulatedSolarGainHeaders(SimpleModulatedHeaders):
|
|
|
445
449
|
modstate_length_sec: float = 0.5,
|
|
446
450
|
num_modstates: int = 1,
|
|
447
451
|
modstate: int = 1,
|
|
452
|
+
center_wavelength: float = 1080.0,
|
|
453
|
+
slit_width: float = 52.0,
|
|
448
454
|
):
|
|
449
455
|
super().__init__(
|
|
450
456
|
num_modstates=num_modstates,
|
|
@@ -454,6 +460,8 @@ class ModulatedSolarGainHeaders(SimpleModulatedHeaders):
|
|
|
454
460
|
exposure_condition=exposure_condition,
|
|
455
461
|
start_date=start_date,
|
|
456
462
|
modstate_length_sec=modstate_length_sec,
|
|
463
|
+
center_wavelength=center_wavelength,
|
|
464
|
+
slit_width=slit_width,
|
|
457
465
|
)
|
|
458
466
|
|
|
459
467
|
self.add_constant_key("PAC__002", "clear")
|
|
@@ -565,6 +573,8 @@ class ModulatedObserveHeaders(SimpleModulatedHeaders):
|
|
|
565
573
|
start_date: str = "2023-01-01T01:23:45",
|
|
566
574
|
modstate_length_sec: float = 0.5,
|
|
567
575
|
meas_num: int = 1,
|
|
576
|
+
center_wavelength: float = 1080.0,
|
|
577
|
+
slit_width: float = 52.0,
|
|
568
578
|
):
|
|
569
579
|
super().__init__(
|
|
570
580
|
num_modstates=num_modstates,
|
|
@@ -574,6 +584,8 @@ class ModulatedObserveHeaders(SimpleModulatedHeaders):
|
|
|
574
584
|
exposure_condition=exposure_condition,
|
|
575
585
|
start_date=start_date,
|
|
576
586
|
modstate_length_sec=modstate_length_sec,
|
|
587
|
+
center_wavelength=center_wavelength,
|
|
588
|
+
slit_width=slit_width,
|
|
577
589
|
)
|
|
578
590
|
|
|
579
591
|
self.num_map_scans = num_map_scans
|