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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. changelog/232.misc.rst +1 -0
  2. dkist_processing_cryonirsp/__init__.py +1 -0
  3. dkist_processing_cryonirsp/codecs/fits.py +1 -0
  4. dkist_processing_cryonirsp/config.py +5 -1
  5. dkist_processing_cryonirsp/models/beam_boundaries.py +1 -0
  6. dkist_processing_cryonirsp/models/constants.py +31 -30
  7. dkist_processing_cryonirsp/models/exposure_conditions.py +6 -5
  8. dkist_processing_cryonirsp/models/fits_access.py +40 -0
  9. dkist_processing_cryonirsp/models/parameters.py +14 -26
  10. dkist_processing_cryonirsp/models/tags.py +1 -0
  11. dkist_processing_cryonirsp/models/task_name.py +1 -0
  12. dkist_processing_cryonirsp/parsers/check_for_gains.py +1 -0
  13. dkist_processing_cryonirsp/parsers/cryonirsp_l0_fits_access.py +40 -47
  14. dkist_processing_cryonirsp/parsers/cryonirsp_l1_fits_access.py +1 -0
  15. dkist_processing_cryonirsp/parsers/exposure_conditions.py +14 -13
  16. dkist_processing_cryonirsp/parsers/map_repeats.py +1 -0
  17. dkist_processing_cryonirsp/parsers/measurements.py +29 -16
  18. dkist_processing_cryonirsp/parsers/modstates.py +5 -1
  19. dkist_processing_cryonirsp/parsers/optical_density_filters.py +1 -0
  20. dkist_processing_cryonirsp/parsers/polarimetric_check.py +18 -7
  21. dkist_processing_cryonirsp/parsers/scan_step.py +12 -4
  22. dkist_processing_cryonirsp/parsers/time.py +7 -7
  23. dkist_processing_cryonirsp/parsers/wavelength.py +6 -1
  24. dkist_processing_cryonirsp/tasks/__init__.py +2 -1
  25. dkist_processing_cryonirsp/tasks/assemble_movie.py +1 -0
  26. dkist_processing_cryonirsp/tasks/bad_pixel_map.py +6 -5
  27. dkist_processing_cryonirsp/tasks/beam_boundaries_base.py +12 -11
  28. dkist_processing_cryonirsp/tasks/ci_beam_boundaries.py +1 -0
  29. dkist_processing_cryonirsp/tasks/ci_science.py +1 -0
  30. dkist_processing_cryonirsp/tasks/cryonirsp_base.py +2 -3
  31. dkist_processing_cryonirsp/tasks/dark.py +5 -4
  32. dkist_processing_cryonirsp/tasks/gain.py +7 -6
  33. dkist_processing_cryonirsp/tasks/instrument_polarization.py +17 -16
  34. dkist_processing_cryonirsp/tasks/l1_output_data.py +1 -0
  35. dkist_processing_cryonirsp/tasks/linearity_correction.py +1 -0
  36. dkist_processing_cryonirsp/tasks/make_movie_frames.py +3 -2
  37. dkist_processing_cryonirsp/tasks/mixin/corrections.py +1 -0
  38. dkist_processing_cryonirsp/tasks/mixin/shift_measurements.py +9 -2
  39. dkist_processing_cryonirsp/tasks/parse.py +70 -52
  40. dkist_processing_cryonirsp/tasks/quality_metrics.py +15 -14
  41. dkist_processing_cryonirsp/tasks/science_base.py +8 -6
  42. dkist_processing_cryonirsp/tasks/sp_beam_boundaries.py +2 -1
  43. dkist_processing_cryonirsp/tasks/sp_geometric.py +11 -10
  44. dkist_processing_cryonirsp/tasks/sp_science.py +1 -0
  45. dkist_processing_cryonirsp/tasks/sp_solar_gain.py +15 -12
  46. dkist_processing_cryonirsp/tasks/sp_wavelength_calibration.py +300 -0
  47. dkist_processing_cryonirsp/tasks/write_l1.py +59 -38
  48. dkist_processing_cryonirsp/tests/conftest.py +75 -53
  49. dkist_processing_cryonirsp/tests/header_models.py +62 -11
  50. dkist_processing_cryonirsp/tests/local_trial_workflows/l0_cals_only.py +26 -46
  51. dkist_processing_cryonirsp/tests/local_trial_workflows/l0_to_l1.py +26 -47
  52. dkist_processing_cryonirsp/tests/local_trial_workflows/linearize_only.py +3 -3
  53. dkist_processing_cryonirsp/tests/local_trial_workflows/local_trial_helpers.py +57 -26
  54. dkist_processing_cryonirsp/tests/test_assemble_movie.py +4 -5
  55. dkist_processing_cryonirsp/tests/test_assemble_qualilty.py +5 -1
  56. dkist_processing_cryonirsp/tests/test_bad_pixel_maps.py +4 -5
  57. dkist_processing_cryonirsp/tests/test_ci_beam_boundaries.py +4 -5
  58. dkist_processing_cryonirsp/tests/test_ci_science.py +4 -5
  59. dkist_processing_cryonirsp/tests/test_corrections.py +5 -6
  60. dkist_processing_cryonirsp/tests/test_cryo_base.py +4 -6
  61. dkist_processing_cryonirsp/tests/test_cryo_constants.py +7 -3
  62. dkist_processing_cryonirsp/tests/test_dark.py +7 -8
  63. dkist_processing_cryonirsp/tests/test_fits_access.py +44 -0
  64. dkist_processing_cryonirsp/tests/test_gain.py +7 -8
  65. dkist_processing_cryonirsp/tests/test_instrument_polarization.py +19 -10
  66. dkist_processing_cryonirsp/tests/test_linearity_correction.py +5 -4
  67. dkist_processing_cryonirsp/tests/test_make_movie_frames.py +2 -3
  68. dkist_processing_cryonirsp/tests/test_parameters.py +23 -28
  69. dkist_processing_cryonirsp/tests/test_parse.py +48 -12
  70. dkist_processing_cryonirsp/tests/test_quality.py +2 -3
  71. dkist_processing_cryonirsp/tests/test_sp_beam_boundaries.py +5 -5
  72. dkist_processing_cryonirsp/tests/test_sp_geometric.py +5 -6
  73. dkist_processing_cryonirsp/tests/test_sp_make_movie_frames.py +2 -3
  74. dkist_processing_cryonirsp/tests/test_sp_science.py +4 -5
  75. dkist_processing_cryonirsp/tests/test_sp_solar.py +6 -5
  76. dkist_processing_cryonirsp/tests/{test_sp_dispersion_axis_correction.py → test_sp_wavelength_calibration.py} +11 -29
  77. dkist_processing_cryonirsp/tests/test_trial_create_quality_report.py +1 -1
  78. dkist_processing_cryonirsp/tests/test_workflows.py +1 -0
  79. dkist_processing_cryonirsp/tests/test_write_l1.py +29 -31
  80. dkist_processing_cryonirsp/workflows/__init__.py +1 -0
  81. dkist_processing_cryonirsp/workflows/ci_l0_processing.py +9 -5
  82. dkist_processing_cryonirsp/workflows/sp_l0_processing.py +12 -8
  83. dkist_processing_cryonirsp/workflows/trial_workflows.py +12 -11
  84. dkist_processing_cryonirsp-1.14.9rc1.dist-info/METADATA +552 -0
  85. dkist_processing_cryonirsp-1.14.9rc1.dist-info/RECORD +115 -0
  86. {dkist_processing_cryonirsp-1.4.20.dist-info → dkist_processing_cryonirsp-1.14.9rc1.dist-info}/WHEEL +1 -1
  87. docs/ci_science_calibration.rst +10 -0
  88. docs/conf.py +1 -0
  89. docs/index.rst +1 -0
  90. docs/sp_science_calibration.rst +7 -0
  91. docs/wavelength_calibration.rst +62 -0
  92. dkist_processing_cryonirsp/tasks/sp_dispersion_axis_correction.py +0 -492
  93. dkist_processing_cryonirsp-1.4.20.dist-info/METADATA +0 -452
  94. dkist_processing_cryonirsp-1.4.20.dist-info/RECORD +0 -111
  95. {dkist_processing_cryonirsp-1.4.20.dist-info → dkist_processing_cryonirsp-1.14.9rc1.dist-info}/top_level.txt +0 -0
changelog/232.misc.rst ADDED
@@ -0,0 +1 @@
1
+ Update dkist-processing-common to add a new constant for the dark number of frames per FPA.
@@ -1,4 +1,5 @@
1
1
  """Init."""
2
+
2
3
  from importlib.metadata import PackageNotFoundError
3
4
  from importlib.metadata import version
4
5
 
@@ -1,4 +1,5 @@
1
1
  """Encoders and decoders for writing and reading Cryo-NIRSP FITS files."""
2
+
2
3
  from pathlib import Path
3
4
 
4
5
  import numpy as np
@@ -1,11 +1,15 @@
1
1
  """Configuration for the dkist-processing-cryonirsp package and the logging thereof."""
2
+
2
3
  from dkist_processing_common.config import DKISTProcessingCommonConfiguration
4
+ from pydantic import Field
3
5
 
4
6
 
5
7
  class DKISTProcessingCryoNIRSPConfigurations(DKISTProcessingCommonConfiguration):
6
8
  """Configurations custom to the dkist-processing-cryonirsp package."""
7
9
 
8
- pass # nothing custom yet
10
+ fts_atlas_data_dir: str | None = Field(
11
+ default=None, description="Common cached directory for a downloaded FTS Atlas."
12
+ )
9
13
 
10
14
 
11
15
  dkist_processing_cryonirsp_configurations = DKISTProcessingCryoNIRSPConfigurations()
@@ -1,4 +1,5 @@
1
1
  """Beam boundary class."""
2
+
2
3
  from dataclasses import dataclass
3
4
 
4
5
  import numpy as np
@@ -1,15 +1,16 @@
1
1
  """CryoNIRSP additions to common constants."""
2
- from enum import Enum
2
+
3
+ from enum import StrEnum
3
4
  from enum import unique
4
5
 
5
- from dkist_processing_common.models.constants import BudName
6
+ import astropy.units as u
6
7
  from dkist_processing_common.models.constants import ConstantsBase
7
8
 
8
9
  from dkist_processing_cryonirsp.models.exposure_conditions import ExposureConditions
9
10
 
10
11
 
11
12
  @unique
12
- class CryonirspBudName(Enum):
13
+ class CryonirspBudName(StrEnum):
13
14
  """Names to be used for CryoNIRSP buds."""
14
15
 
15
16
  arm_id = "ARM_ID"
@@ -18,8 +19,6 @@ class CryonirspBudName(Enum):
18
19
  num_map_scans = "NUM_MAP_SCANS"
19
20
  num_modstates = "NUM_MODSTATES"
20
21
  wavelength = "WAVELENGTH"
21
- wave_min = "WAVE_MIN"
22
- wave_max = "WAVE_MAX"
23
22
  grating_position_deg = "GRATING_POSITION_DEG"
24
23
  grating_littrow_angle_deg = "GRATING_LITTROW_ANGLE_DEG"
25
24
  grating_constant = "GRATING_CONSTANT"
@@ -50,10 +49,12 @@ class CryonirspBudName(Enum):
50
49
  roi_1_size_x = "ROI_1_SIZE_X"
51
50
  roi_1_size_y = "ROI_1_SIZE_Y"
52
51
  optical_density_filter_picky_bud = "OPTICAL_DENSITY_FILTER_PICKY_BUD"
53
- solar_gain_ip_start_time = "SOLAR_GAIN_IP_START_TIME"
52
+ solar_gain_start_time = "SOLAR_GAIN_START_TIME"
54
53
  gain_frame_type_list = "GAIN_FRAME_TYPE_LIST"
55
54
  lamp_gain_frame_type_list = "LAMP_GAIN_FRAME_TYPE_LIST"
56
55
  solar_gain_frame_type_list = "SOLAR_GAIN_FRAME_TYPE_LIST"
56
+ center_wavelength = "CENTER_WAVELENGTH"
57
+ slit_width = "SLIT_WIDTH"
57
58
 
58
59
 
59
60
  class CryonirspConstants(ConstantsBase):
@@ -88,19 +89,9 @@ class CryonirspConstants(ConstantsBase):
88
89
  return self._db_dict[CryonirspBudName.wavelength.value]
89
90
 
90
91
  @property
91
- def wave_min(self) -> float:
92
- """Wavelength minimum."""
93
- return self._db_dict[CryonirspBudName.wave_min.value]
94
-
95
- @property
96
- def wave_max(self) -> float:
97
- """Wavelength maximum."""
98
- return self._db_dict[CryonirspBudName.wave_max.value]
99
-
100
- @property
101
- def solar_gain_ip_start_time(self) -> str:
102
- """Solar gain IP start time."""
103
- return self._db_dict[CryonirspBudName.solar_gain_ip_start_time.value]
92
+ def solar_gain_start_time(self) -> str:
93
+ """Solar gain start time."""
94
+ return self._db_dict[CryonirspBudName.solar_gain_start_time.value]
104
95
 
105
96
  @property
106
97
  def grating_position_deg(self) -> float:
@@ -112,10 +103,20 @@ class CryonirspConstants(ConstantsBase):
112
103
  """Grating littrow angle (deg)."""
113
104
  return self._db_dict[CryonirspBudName.grating_littrow_angle_deg.value]
114
105
 
106
+ @property
107
+ def center_wavelength(self) -> float:
108
+ """Center wavelength of the selected filter (nm)."""
109
+ return self._db_dict[CryonirspBudName.center_wavelength.value]
110
+
111
+ @property
112
+ def slit_width(self) -> float:
113
+ """Physical width of the selected slit (um)."""
114
+ return self._db_dict[CryonirspBudName.slit_width.value]
115
+
115
116
  @property
116
117
  def grating_constant(self) -> float:
117
118
  """Grating constant."""
118
- return self._db_dict[CryonirspBudName.grating_constant.value]
119
+ return self._db_dict[CryonirspBudName.grating_constant.value] / u.mm
119
120
 
120
121
  @property
121
122
  def camera_readout_mode(self) -> str:
@@ -214,16 +215,6 @@ class CryonirspConstants(ConstantsBase):
214
215
  conditions = [ExposureConditions(*item) for item in raw_conditions]
215
216
  return conditions
216
217
 
217
- @property
218
- def num_modstates(self) -> int:
219
- """Find the number of modulation states."""
220
- return self._db_dict[CryonirspBudName.num_modstates.value]
221
-
222
- @property
223
- def num_cs_steps(self) -> int:
224
- """Find the number of calibration sequence steps."""
225
- return self._db_dict[BudName.num_cs_steps.value]
226
-
227
218
  @property
228
219
  def stokes_I_list(self) -> [str]:
229
220
  """List containing only the Stokes-I parameter."""
@@ -236,6 +227,16 @@ class CryonirspConstants(ConstantsBase):
236
227
  CryonirspBudName.modulator_spin_mode.value
237
228
  ] in ["Continuous", "Stepped"]
238
229
 
230
+ @property
231
+ def pac_init_set(self):
232
+ """Return the label for the initial set of parameter values used when fitting demodulation matrices."""
233
+ retarder_name = self.retarder_name
234
+ match retarder_name:
235
+ case "SiO2 OC":
236
+ return "OCCal_VIS"
237
+ case _:
238
+ raise ValueError(f"No init set known for {retarder_name = }")
239
+
239
240
  @property
240
241
  def axis_1_type(self) -> str:
241
242
  """Find the type of the first array axis."""
@@ -1,16 +1,17 @@
1
1
  """Support classes for exposure conditions."""
2
- from collections import namedtuple
2
+
3
3
  from enum import StrEnum
4
+ from typing import NamedTuple
4
5
 
5
6
  # Number of digits used to round the exposure when creating the ExposureConditions tuple in fits_access
6
7
  CRYO_EXP_TIME_ROUND_DIGITS: int = 3
7
8
 
8
- """Base class to hold a tuple of exposure time and filter name."""
9
- ExposureConditionsBase = namedtuple("ExposureConditions", ["exposure_time", "filter_name"])
10
9
 
10
+ class ExposureConditions(NamedTuple):
11
+ """Named tuple to hold exposure time and filter name."""
11
12
 
12
- class ExposureConditions(ExposureConditionsBase):
13
- """Define str to make tags look reasonable."""
13
+ exposure_time: float
14
+ filter_name: str
14
15
 
15
16
  def __str__(self):
16
17
  return f"{self.exposure_time}_{self.filter_name}"
@@ -0,0 +1,40 @@
1
+ """CryoNIRSP control of FITS key names and values."""
2
+
3
+ from enum import StrEnum
4
+ from enum import unique
5
+
6
+
7
+ @unique
8
+ class CryonirspMetadataKey(StrEnum):
9
+ """Controlled list of names for FITS metadata header keys."""
10
+
11
+ camera_readout_mode = "CNCAMMD"
12
+ curr_frame_in_ramp = "CNCNDR"
13
+ num_frames_in_ramp = "CNNNDR"
14
+ arm_id = "CNARMID"
15
+ roi_1_origin_x = "HWROI1OX"
16
+ roi_1_origin_y = "HWROI1OY"
17
+ roi_1_size_x = "HWROI1SX"
18
+ roi_1_size_y = "HWROI1SY"
19
+ filter_name = "CNFILTNP"
20
+ number_of_modulator_states = "CNMODNST"
21
+ modulator_state = "CNMODCST"
22
+ scan_step = "CNCURSCN"
23
+ num_scan_steps = "CNNUMSCN"
24
+ num_cn1_scan_steps = "CNP1DNSP"
25
+ num_cn2_scan_steps = "CNP2DNSP"
26
+ cn2_step_size = "CNP2DSS"
27
+ cn1_scan_step = "CNP1DCUR"
28
+ meas_num = "CNCMEAS"
29
+ num_meas = "CNNMEAS"
30
+ sub_repeat_num = "CNCSREP"
31
+ num_sub_repeats = "CNSUBREP"
32
+ modulator_spin_mode = "CNSPINMD"
33
+ axis_1_type = "CTYPE1"
34
+ axis_2_type = "CTYPE2"
35
+ axis_3_type = "CTYPE3"
36
+ grating_position_deg = "CNGRTPOS"
37
+ grating_littrow_angle_deg = "CNGRTLAT"
38
+ grating_constant = "CNGRTCON"
39
+ center_wavelength = "CNCENWAV"
40
+ slit_width = "CNSLITW"
@@ -1,4 +1,5 @@
1
1
  """CryoNIRSP calibration pipeline parameters."""
2
+
2
3
  from datetime import datetime
3
4
  from functools import cached_property
4
5
 
@@ -7,6 +8,7 @@ import numpy as np
7
8
  from dkist_processing_common.models.parameters import ParameterArmIdMixin
8
9
  from dkist_processing_common.models.parameters import ParameterBase
9
10
  from dkist_processing_common.models.parameters import ParameterWavelengthMixin
11
+ from solar_wavelength_calibration import DownloadConfig
10
12
 
11
13
  from dkist_processing_cryonirsp.models.exposure_conditions import AllowableOpticalDensityFilterNames
12
14
 
@@ -82,11 +84,6 @@ class CryonirspParameters(ParameterBase, ParameterWavelengthMixin, ParameterArmI
82
84
  """Name of set of fitting flags to use during PAC Calibration Unit parameter fits."""
83
85
  return self._find_most_recent_past_value("cryonirsp_polcal_pac_fit_mode")
84
86
 
85
- @property
86
- def polcal_pac_init_set(self):
87
- """Name of set of initial values for Calibration Unit parameter fit."""
88
- return self._find_most_recent_past_value("cryonirsp_polcal_pac_init_set")
89
-
90
87
  @property
91
88
  def beam_boundaries_smoothing_disk_size(self) -> int:
92
89
  """Return the size of the smoothing disk (in pixels) to be used in the beam boundaries computation."""
@@ -141,8 +138,8 @@ class CryonirspParameters(ParameterBase, ParameterWavelengthMixin, ParameterArmI
141
138
  @cached_property
142
139
  def linearization_thresholds(self) -> np.ndarray:
143
140
  """Name of parameter associated with the linearization thresholds."""
144
- param_dict = self._find_parameter_for_arm("cryonirsp_linearization_thresholds")
145
- value = self._load_param_value_from_numpy_save(param_dict)
141
+ param_obj = self._find_parameter_for_arm("cryonirsp_linearization_thresholds")
142
+ value = self._load_param_value_from_numpy_save(param_obj=param_obj)
146
143
  # float64 data can blow up the memory required for linearization - convert to float32
147
144
  if np.issubdtype(value.dtype, np.float64):
148
145
  value = value.astype(np.float32, casting="same_kind")
@@ -272,24 +269,15 @@ class CryonirspParameters(ParameterBase, ParameterWavelengthMixin, ParameterArmI
272
269
  """Return the CryoNIRSP pixel pitch."""
273
270
  return self._find_most_recent_past_value("cryonirsp_pixel_pitch_micron") * u.micron
274
271
 
275
- @cached_property
276
- def solar_atlas(self) -> np.ndarray:
277
- """Solar reference atlas.
278
-
279
- Contains two arrays:
280
- - wavelength in nanometers
281
- - transmission at given wavelength
282
- """
283
- param_dict = self._find_most_recent_past_value("cryonirsp_solar_atlas")
284
- return self._load_param_value_from_numpy_save(param_dict)
272
+ @property
273
+ def wavecal_atlas_download_config(self) -> DownloadConfig:
274
+ """Define the `~solar_wavelength_calibration.DownloadConfig` used to grab the Solar atlas used for wavelength calibration."""
275
+ config_dict = self._find_most_recent_past_value("cryonirsp_wavecal_atlas_download_config")
276
+ return DownloadConfig.model_validate(config_dict)
285
277
 
286
278
  @cached_property
287
- def telluric_atlas(self) -> np.ndarray:
288
- """Telluric reference atlas.
289
-
290
- Contains two arrays:
291
- - wavelength in nanometers
292
- - transmission at given wavelength
293
- """
294
- param_dict = self._find_most_recent_past_value("cryonirsp_telluric_atlas")
295
- return self._load_param_value_from_numpy_save(param_dict)
279
+ def wavecal_fraction_of_unweighted_edge_pixels(self) -> int:
280
+ """Return the fraction of edge pixels to weight to zero during the wavelength calibration."""
281
+ return self._find_most_recent_past_value(
282
+ "cryonirsp_wavecal_fraction_of_unweighted_edge_pixels"
283
+ )
@@ -1,4 +1,5 @@
1
1
  """CryoNIRSP tags."""
2
+
2
3
  from enum import Enum
3
4
 
4
5
  from dkist_processing_common.models.tags import StemName
@@ -1,4 +1,5 @@
1
1
  """List of intermediate task names."""
2
+
2
3
  from enum import Enum
3
4
 
4
5
 
@@ -1,4 +1,5 @@
1
1
  """Pickybud to check for lamp gain and solar gain frames."""
2
+
2
3
  from typing import Hashable
3
4
  from typing import Type
4
5
 
@@ -1,10 +1,12 @@
1
1
  """CryoNIRSP FITS access for L0 data."""
2
+
2
3
  import numpy as np
3
4
  from astropy.io import fits
4
5
  from dkist_processing_common.parsers.l0_fits_access import L0FitsAccess
5
6
 
6
7
  from dkist_processing_cryonirsp.models.exposure_conditions import CRYO_EXP_TIME_ROUND_DIGITS
7
8
  from dkist_processing_cryonirsp.models.exposure_conditions import ExposureConditions
9
+ from dkist_processing_cryonirsp.models.fits_access import CryonirspMetadataKey
8
10
 
9
11
 
10
12
  class CryonirspRampFitsAccess(L0FitsAccess):
@@ -33,16 +35,15 @@ class CryonirspRampFitsAccess(L0FitsAccess):
33
35
  ):
34
36
  super().__init__(hdu=hdu, name=name, auto_squeeze=auto_squeeze)
35
37
 
36
- self.camera_readout_mode = self.header["CNCAMMD"]
37
- self.curr_frame_in_ramp: int = self.header["CNCNDR"]
38
- self.num_frames_in_ramp: int = self.header["CNNNDR"]
39
- self.arm_id: str = self.header["CNARMID"]
40
- self.filter_name = self.header["CNFILTNP"].upper()
41
- self.roi_1_origin_x = self.header["HWROI1OX"]
42
- self.roi_1_origin_y = self.header["HWROI1OY"]
43
- self.roi_1_size_x = self.header["HWROI1SX"]
44
- self.roi_1_size_y = self.header["HWROI1SY"]
45
- self.obs_ip_start_time = self.header["DKIST011"]
38
+ self.camera_readout_mode = self.header[CryonirspMetadataKey.camera_readout_mode]
39
+ self.curr_frame_in_ramp: int = self.header[CryonirspMetadataKey.curr_frame_in_ramp]
40
+ self.num_frames_in_ramp: int = self.header[CryonirspMetadataKey.num_frames_in_ramp]
41
+ self.arm_id: str = self.header[CryonirspMetadataKey.arm_id]
42
+ self.filter_name = self.header[CryonirspMetadataKey.filter_name].upper()
43
+ self.roi_1_origin_x = self.header[CryonirspMetadataKey.roi_1_origin_x]
44
+ self.roi_1_origin_y = self.header[CryonirspMetadataKey.roi_1_origin_y]
45
+ self.roi_1_size_x = self.header[CryonirspMetadataKey.roi_1_size_x]
46
+ self.roi_1_size_y = self.header[CryonirspMetadataKey.roi_1_size_y]
46
47
 
47
48
 
48
49
  class CryonirspL0FitsAccess(L0FitsAccess):
@@ -71,46 +72,38 @@ class CryonirspL0FitsAccess(L0FitsAccess):
71
72
  ):
72
73
  super().__init__(hdu=hdu, name=name, auto_squeeze=auto_squeeze)
73
74
 
74
- self.arm_id: str = self.header["CNARMID"]
75
- self.number_of_modulator_states: int = self.header["CNMODNST"]
76
- self.modulator_state: int = self.header["CNMODCST"]
77
- self.scan_step: int = self.header["CNCURSCN"]
78
- self.num_scan_steps: int = self.header["CNNUMSCN"]
79
- self.num_cn1_scan_steps: int = self.header["CNP1DNSP"]
80
- self.num_cn2_scan_steps: int = self.header["CNP2DNSP"]
81
- self.cn2_step_size: float = self.header["CNP2DSS"]
82
- self.meas_num: int = self.header["CNCMEAS"]
83
- self.num_meas: int = self.header["CNNMEAS"]
84
- self.sub_repeat_num = self.header["CNCSREP"]
85
- self.num_sub_repeats: int = self.header["CNSUBREP"]
86
- self.modulator_spin_mode: str = self.header["CNSPINMD"]
87
- self.axis_1_type: str = self.header["CTYPE1"]
88
- self.axis_2_type: str = self.header["CTYPE2"]
89
- self.axis_3_type: str = self.header["CTYPE3"]
90
- self.wave_min: float = round(
91
- self.header["CRVAL1"] - (self.header["CRPIX1"] * self.header["CDELT1"]), 1
92
- )
93
- self.wave_max: float = round(
94
- self.header["CRVAL1"]
95
- + ((self.header["NAXIS1"] - self.header["CRPIX1"]) * self.header["CDELT1"]),
96
- 1,
97
- )
98
- self.grating_position_deg: float = self.header["CNGRTPOS"]
99
- self.grating_littrow_angle_deg: float = self.header["CNGRTLAT"]
100
- # grating_constant is in the L0 header in (mm)^(-1) when needed in (m)^(-1) hence multiply by 1000.
101
- self.grating_constant: float = self.header["CNGRTCON"] * 1000
102
- self.obs_ip_start_time = self.header["DKIST011"]
75
+ self.arm_id: str = self.header[CryonirspMetadataKey.arm_id]
76
+ self.number_of_modulator_states: int = self.header[
77
+ CryonirspMetadataKey.number_of_modulator_states
78
+ ]
79
+ self.modulator_state: int = self.header[CryonirspMetadataKey.modulator_state]
80
+ self.scan_step: int = self.header[CryonirspMetadataKey.scan_step]
81
+ self.num_scan_steps: int = self.header[CryonirspMetadataKey.num_scan_steps]
82
+ self.num_cn1_scan_steps: int = self.header[CryonirspMetadataKey.num_cn1_scan_steps]
83
+ self.num_cn2_scan_steps: int = self.header[CryonirspMetadataKey.num_cn2_scan_steps]
84
+ self.cn2_step_size: float = self.header[CryonirspMetadataKey.cn2_step_size]
85
+ self.meas_num: int = self.header[CryonirspMetadataKey.meas_num]
86
+ self.num_meas: int = self.header[CryonirspMetadataKey.num_meas]
87
+ self.sub_repeat_num = self.header[CryonirspMetadataKey.sub_repeat_num]
88
+ self.num_sub_repeats: int = self.header[CryonirspMetadataKey.num_sub_repeats]
89
+ self.modulator_spin_mode: str = self.header[CryonirspMetadataKey.modulator_spin_mode]
90
+ self.axis_1_type: str = self.header[CryonirspMetadataKey.axis_1_type]
91
+ self.axis_2_type: str = self.header[CryonirspMetadataKey.axis_2_type]
92
+ self.axis_3_type: str = self.header[CryonirspMetadataKey.axis_3_type]
93
+ self.grating_position_deg: float = self.header[CryonirspMetadataKey.grating_position_deg]
94
+ self.grating_littrow_angle_deg: float = self.header[
95
+ CryonirspMetadataKey.grating_littrow_angle_deg
96
+ ]
97
+ self.grating_constant: float = self.header[CryonirspMetadataKey.grating_constant]
98
+ self.filter_name = self.header[CryonirspMetadataKey.filter_name.value].upper()
103
99
  # The ExposureConditions are a combination of the exposure time and the OD filter name:
104
100
  self.exposure_conditions = ExposureConditions(
105
- round(self.fpa_exposure_time_ms, CRYO_EXP_TIME_ROUND_DIGITS),
106
- self.header["CNFILTNP"].upper(),
101
+ round(self.fpa_exposure_time_ms, CRYO_EXP_TIME_ROUND_DIGITS), self.filter_name
107
102
  )
108
- self.solar_gain_ip_start_time = self.header["DATE-OBS"]
109
-
110
- @property
111
- def cn1_scan_step(self):
112
- """Convert the inner loop step number from float to int."""
113
- return int(self.header["CNP1DCUR"])
103
+ self.center_wavelength = self.header[CryonirspMetadataKey.center_wavelength]
104
+ self.slit_width = self.header[CryonirspMetadataKey.slit_width]
105
+ # Convert the inner loop step number from float to int:
106
+ self.cn1_scan_step = int(self.header[CryonirspMetadataKey.cn1_scan_step])
114
107
 
115
108
 
116
109
  class CryonirspLinearizedFitsAccess(CryonirspL0FitsAccess):
@@ -1,4 +1,5 @@
1
1
  """CryoNIRSP FITS access for L1 data."""
2
+
2
3
  from astropy.io import fits
3
4
  from dkist_processing_common.parsers.l1_fits_access import L1FitsAccess
4
5
 
@@ -1,6 +1,7 @@
1
1
  """Buds to parse the combination of exposure time and filter name."""
2
- from collections import namedtuple
2
+
3
3
  from typing import Hashable
4
+ from typing import NamedTuple
4
5
  from typing import Type
5
6
 
6
7
  from dkist_processing_common.models.flower_pot import SpilledDirt
@@ -28,7 +29,6 @@ class CryonirspTaskExposureConditionsBud(Stem):
28
29
 
29
30
  def __init__(self, stem_name: str, ip_task_type: str):
30
31
  super().__init__(stem_name=stem_name)
31
- self.metadata_key = "exposure_conditions"
32
32
  self.ip_task_type = ip_task_type
33
33
 
34
34
  def setter(self, fits_obj: CryonirspL0FitsAccess):
@@ -42,7 +42,7 @@ class CryonirspTaskExposureConditionsBud(Stem):
42
42
  """
43
43
  ip_task_type = parse_header_ip_task_with_gains(fits_obj)
44
44
  if ip_task_type.lower() == self.ip_task_type.lower():
45
- return getattr(fits_obj, self.metadata_key)
45
+ return fits_obj.exposure_conditions
46
46
  return SpilledDirt
47
47
 
48
48
  def getter(self, key: Hashable) -> tuple[ExposureConditions, ...]:
@@ -56,7 +56,6 @@ class CryonirspConditionalTaskExposureConditionsBudBase(Stem):
56
56
 
57
57
  def __init__(self, stem_name: str, task_types_to_ignore: list[str]):
58
58
  super().__init__(stem_name=stem_name)
59
- self.metadata_key = "exposure_conditions"
60
59
  self.task_types_to_ignore = task_types_to_ignore
61
60
 
62
61
  def setter(self, fits_obj: CryonirspL0FitsAccess) -> ExposureConditions | Type[SpilledDirt]:
@@ -73,7 +72,7 @@ class CryonirspConditionalTaskExposureConditionsBudBase(Stem):
73
72
  """
74
73
  task_type = parse_header_ip_task_with_gains(fits_obj=fits_obj)
75
74
  if task_type.upper() not in self.task_types_to_ignore:
76
- return getattr(fits_obj, self.metadata_key)
75
+ return fits_obj.exposure_conditions
77
76
  return SpilledDirt
78
77
 
79
78
  def getter(self, key: Hashable) -> tuple[ExposureConditions, ...]:
@@ -103,7 +102,6 @@ class CryonirspSPConditionalTaskExposureConditionsBud(
103
102
  stem_name=CryonirspBudName.sp_non_dark_and_non_polcal_task_exposure_conditions_list.value,
104
103
  task_types_to_ignore=[TaskName.dark.value, TaskName.polcal.value],
105
104
  )
106
- self.metadata_key = "exposure_conditions"
107
105
 
108
106
 
109
107
  class CryonirspCIConditionalTaskExposureConditionsBud(
@@ -120,19 +118,22 @@ class CryonirspCIConditionalTaskExposureConditionsBud(
120
118
  TaskName.lamp_gain.value,
121
119
  ],
122
120
  )
123
- self.metadata_key = "exposure_conditions"
121
+
122
+
123
+ class DarkTaskTestAndExposureConditionsContainer(NamedTuple):
124
+ """Named tuple to hold whether the task is dark along with the associated exposure conditions."""
125
+
126
+ is_dark: bool
127
+ exposure_conditions: ExposureConditions
124
128
 
125
129
 
126
130
  class CryonirspPickyDarkExposureConditionsBudBase(Stem):
127
131
  """Parse exposure conditions tuples to ensure existence of dark frames with the required exposure conditions."""
128
132
 
129
- DarkTaskTestAndExposureConditions = namedtuple(
130
- "DarkTaskTestAndExposureConditions", ["is_dark", "exposure_conditions"]
131
- )
133
+ DarkTaskTestAndExposureConditions = DarkTaskTestAndExposureConditionsContainer
132
134
 
133
135
  def __init__(self, stem_name: str, task_types_to_ignore: list[str]):
134
136
  super().__init__(stem_name=stem_name)
135
- self.metadata_key = "exposure_conditions"
136
137
  self.task_types_to_ignore = task_types_to_ignore
137
138
 
138
139
  def setter(self, fits_obj: CryonirspL0FitsAccess) -> tuple | Type[SpilledDirt]:
@@ -150,11 +151,11 @@ class CryonirspPickyDarkExposureConditionsBudBase(Stem):
150
151
  task_type = parse_header_ip_task_with_gains(fits_obj=fits_obj)
151
152
  if task_type.upper() == TaskName.dark.value:
152
153
  return self.DarkTaskTestAndExposureConditions(
153
- is_dark=True, exposure_conditions=getattr(fits_obj, self.metadata_key)
154
+ is_dark=True, exposure_conditions=fits_obj.exposure_conditions
154
155
  )
155
156
  if task_type.upper() not in self.task_types_to_ignore:
156
157
  return self.DarkTaskTestAndExposureConditions(
157
- is_dark=False, exposure_conditions=getattr(fits_obj, self.metadata_key)
158
+ is_dark=False, exposure_conditions=fits_obj.exposure_conditions
158
159
  )
159
160
  # Ignored task types fall through
160
161
  return SpilledDirt
@@ -1,4 +1,5 @@
1
1
  """Stems for organizing files into separate dsps repeats."""
2
+
2
3
  from dkist_processing_cryonirsp.models.constants import CryonirspBudName
3
4
  from dkist_processing_cryonirsp.models.tags import CryonirspStemName
4
5
  from dkist_processing_cryonirsp.parsers.scan_step import MapScanStepStemBase
@@ -1,45 +1,58 @@
1
1
  """Copies of UniqueBud and SingleValueSingleKeyFlower from common that only activate if the frames are "observe" task."""
2
+
3
+ from typing import Hashable
2
4
  from typing import Type
3
5
 
4
6
  from dkist_processing_common.models.flower_pot import SpilledDirt
5
7
  from dkist_processing_common.parsers.single_value_single_key_flower import (
6
8
  SingleValueSingleKeyFlower,
7
9
  )
8
- from dkist_processing_common.parsers.unique_bud import UniqueBud
9
10
 
10
11
  from dkist_processing_cryonirsp.models.constants import CryonirspBudName
12
+ from dkist_processing_cryonirsp.models.fits_access import CryonirspMetadataKey
11
13
  from dkist_processing_cryonirsp.models.tags import CryonirspStemName
12
14
  from dkist_processing_cryonirsp.parsers.cryonirsp_l0_fits_access import CryonirspL0FitsAccess
15
+ from dkist_processing_cryonirsp.parsers.scan_step import NumberOfScanStepsBase
13
16
 
14
17
 
15
- class NumberOfMeasurementsBud(UniqueBud):
18
+ class NumberOfMeasurementsBud(NumberOfScanStepsBase):
16
19
  """Bud for finding the total number of measurements per scan step."""
17
20
 
18
21
  def __init__(self):
19
- self.metadata_key = "num_meas"
20
- super().__init__(
21
- constant_name=CryonirspBudName.num_meas.value, metadata_key=self.metadata_key
22
- )
22
+ super().__init__(stem_name=CryonirspBudName.num_meas.value)
23
23
 
24
- def setter(self, fits_obj: CryonirspL0FitsAccess) -> Type[SpilledDirt] | int:
24
+ def getter(self, key: Hashable) -> Hashable:
25
25
  """
26
- Setter for the bud.
26
+ Search all scan steps to find the maximum number of measurements in a step.
27
27
 
28
- Parameters
29
- ----------
30
- fits_obj:
31
- A single FitsAccess object
28
+ This maximum number should be the same for all measurements, with the possible exception of the last one in
29
+ an abort.
30
+
31
+ Abort possibilities:
32
+ * if a measurement is missing in the last scan step of a multi-scan, multi-map observation, it will be handled
33
+ as an incomplete scan step and truncated by the scan step abort handler.
34
+ * if a measurement is missing in a single-map, single-scan observation then the number of measurements will be
35
+ given by the last measurement value that had as many frames as the maximum number of frames of any
36
+ measurement.
37
+ This is a formality for intensity mode observations but is an important check for polarimetric data as the
38
+ abort may have happened in the middle of the modulator state sequence.
32
39
  """
33
- if fits_obj.ip_task_type != "observe":
34
- return SpilledDirt
35
- return getattr(fits_obj, self.metadata_key)
40
+ measurements_in_scan_steps = []
41
+ for meas_dict in self.scan_step_dict.values():
42
+ measurements_in_scan_steps.append(len(meas_dict))
43
+ return max(
44
+ measurements_in_scan_steps
45
+ ) # if there are incomplete measurements, they should be at the end and will be truncated by an incomplete scan step
36
46
 
37
47
 
38
48
  class MeasurementNumberFlower(SingleValueSingleKeyFlower):
39
49
  """Flower for a measurement number."""
40
50
 
41
51
  def __init__(self):
42
- super().__init__(tag_stem_name=CryonirspStemName.meas_num.value, metadata_key="meas_num")
52
+ super().__init__(
53
+ tag_stem_name=CryonirspStemName.meas_num.value,
54
+ metadata_key=CryonirspMetadataKey.meas_num,
55
+ )
43
56
 
44
57
  def setter(self, fits_obj: CryonirspL0FitsAccess) -> Type[SpilledDirt] | int:
45
58
  """
@@ -1,4 +1,5 @@
1
1
  """Copy of SingleValueSingleKeyFlower from common that only activates if the frames are "observe" task."""
2
+
2
3
  from typing import Type
3
4
 
4
5
  from dkist_processing_common.models.flower_pot import SpilledDirt
@@ -7,6 +8,7 @@ from dkist_processing_common.parsers.single_value_single_key_flower import (
7
8
  SingleValueSingleKeyFlower,
8
9
  )
9
10
 
11
+ from dkist_processing_cryonirsp.models.fits_access import CryonirspMetadataKey
10
12
  from dkist_processing_cryonirsp.parsers.cryonirsp_l0_fits_access import CryonirspL0FitsAccess
11
13
 
12
14
 
@@ -14,7 +16,9 @@ class ModstateNumberFlower(SingleValueSingleKeyFlower):
14
16
  """Flower for a modstate number."""
15
17
 
16
18
  def __init__(self):
17
- super().__init__(tag_stem_name=StemName.modstate.value, metadata_key="modulator_state")
19
+ super().__init__(
20
+ tag_stem_name=StemName.modstate.value, metadata_key=CryonirspMetadataKey.modulator_state
21
+ )
18
22
 
19
23
  def setter(self, fits_obj: CryonirspL0FitsAccess) -> Type[SpilledDirt] | int:
20
24
  """
@@ -1,4 +1,5 @@
1
1
  """PickyBud to implement early parsing of Optical Density Filter Names."""
2
+
2
3
  from typing import Hashable
3
4
 
4
5
  from dkist_processing_common.models.flower_pot import Stem