dkist-processing-cryonirsp 1.10.0rc1__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 (92) 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 +1 -0
  5. dkist_processing_cryonirsp/models/beam_boundaries.py +1 -0
  6. dkist_processing_cryonirsp/models/constants.py +8 -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 +1 -0
  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 -48
  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 +1 -0
  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 +9 -10
  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 +1 -0
  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 +16 -15
  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 +1 -0
  39. dkist_processing_cryonirsp/tasks/parse.py +66 -61
  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 +9 -9
  47. dkist_processing_cryonirsp/tasks/write_l1.py +36 -7
  48. dkist_processing_cryonirsp/tests/conftest.py +6 -7
  49. dkist_processing_cryonirsp/tests/header_models.py +40 -3
  50. dkist_processing_cryonirsp/tests/local_trial_workflows/l0_cals_only.py +11 -31
  51. dkist_processing_cryonirsp/tests/local_trial_workflows/l0_to_l1.py +11 -30
  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 +3 -2
  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 +3 -4
  57. dkist_processing_cryonirsp/tests/test_ci_beam_boundaries.py +3 -4
  58. dkist_processing_cryonirsp/tests/test_ci_science.py +3 -4
  59. dkist_processing_cryonirsp/tests/test_corrections.py +3 -3
  60. dkist_processing_cryonirsp/tests/test_cryo_base.py +3 -5
  61. dkist_processing_cryonirsp/tests/test_cryo_constants.py +1 -2
  62. dkist_processing_cryonirsp/tests/test_dark.py +5 -6
  63. dkist_processing_cryonirsp/tests/test_fits_access.py +44 -0
  64. dkist_processing_cryonirsp/tests/test_gain.py +5 -6
  65. dkist_processing_cryonirsp/tests/test_instrument_polarization.py +9 -6
  66. dkist_processing_cryonirsp/tests/test_linearity_correction.py +4 -3
  67. dkist_processing_cryonirsp/tests/test_make_movie_frames.py +2 -3
  68. dkist_processing_cryonirsp/tests/test_parameters.py +3 -4
  69. dkist_processing_cryonirsp/tests/test_parse.py +14 -8
  70. dkist_processing_cryonirsp/tests/test_quality.py +2 -3
  71. dkist_processing_cryonirsp/tests/test_sp_beam_boundaries.py +4 -4
  72. dkist_processing_cryonirsp/tests/test_sp_geometric.py +3 -4
  73. dkist_processing_cryonirsp/tests/test_sp_make_movie_frames.py +2 -3
  74. dkist_processing_cryonirsp/tests/test_sp_science.py +3 -4
  75. dkist_processing_cryonirsp/tests/test_sp_solar.py +5 -4
  76. dkist_processing_cryonirsp/tests/test_sp_wavelength_calibration.py +4 -5
  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 +12 -16
  80. dkist_processing_cryonirsp/workflows/__init__.py +1 -0
  81. dkist_processing_cryonirsp/workflows/ci_l0_processing.py +6 -5
  82. dkist_processing_cryonirsp/workflows/sp_l0_processing.py +6 -5
  83. dkist_processing_cryonirsp/workflows/trial_workflows.py +9 -8
  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. docs/conf.py +1 -0
  87. docs/wavelength_calibration.rst +1 -1
  88. changelog/167.feature.rst +0 -1
  89. dkist_processing_cryonirsp-1.10.0rc1.dist-info/METADATA +0 -458
  90. dkist_processing_cryonirsp-1.10.0rc1.dist-info/RECORD +0 -113
  91. {dkist_processing_cryonirsp-1.10.0rc1.dist-info → dkist_processing_cryonirsp-1.14.9rc1.dist-info}/WHEEL +0 -0
  92. {dkist_processing_cryonirsp-1.10.0rc1.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,4 +1,5 @@
1
1
  """Configuration for the dkist-processing-cryonirsp package and the logging thereof."""
2
+
2
3
  from dkist_processing_common.config import DKISTProcessingCommonConfiguration
3
4
  from pydantic import Field
4
5
 
@@ -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,16 +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
6
  import astropy.units as u
6
- from dkist_processing_common.models.constants import BudName
7
7
  from dkist_processing_common.models.constants import ConstantsBase
8
8
 
9
9
  from dkist_processing_cryonirsp.models.exposure_conditions import ExposureConditions
10
10
 
11
11
 
12
12
  @unique
13
- class CryonirspBudName(Enum):
13
+ class CryonirspBudName(StrEnum):
14
14
  """Names to be used for CryoNIRSP buds."""
15
15
 
16
16
  arm_id = "ARM_ID"
@@ -19,8 +19,6 @@ class CryonirspBudName(Enum):
19
19
  num_map_scans = "NUM_MAP_SCANS"
20
20
  num_modstates = "NUM_MODSTATES"
21
21
  wavelength = "WAVELENGTH"
22
- wave_min = "WAVE_MIN"
23
- wave_max = "WAVE_MAX"
24
22
  grating_position_deg = "GRATING_POSITION_DEG"
25
23
  grating_littrow_angle_deg = "GRATING_LITTROW_ANGLE_DEG"
26
24
  grating_constant = "GRATING_CONSTANT"
@@ -51,7 +49,7 @@ class CryonirspBudName(Enum):
51
49
  roi_1_size_x = "ROI_1_SIZE_X"
52
50
  roi_1_size_y = "ROI_1_SIZE_Y"
53
51
  optical_density_filter_picky_bud = "OPTICAL_DENSITY_FILTER_PICKY_BUD"
54
- solar_gain_ip_start_time = "SOLAR_GAIN_IP_START_TIME"
52
+ solar_gain_start_time = "SOLAR_GAIN_START_TIME"
55
53
  gain_frame_type_list = "GAIN_FRAME_TYPE_LIST"
56
54
  lamp_gain_frame_type_list = "LAMP_GAIN_FRAME_TYPE_LIST"
57
55
  solar_gain_frame_type_list = "SOLAR_GAIN_FRAME_TYPE_LIST"
@@ -91,19 +89,9 @@ class CryonirspConstants(ConstantsBase):
91
89
  return self._db_dict[CryonirspBudName.wavelength.value]
92
90
 
93
91
  @property
94
- def wave_min(self) -> float:
95
- """Wavelength minimum."""
96
- return self._db_dict[CryonirspBudName.wave_min.value]
97
-
98
- @property
99
- def wave_max(self) -> float:
100
- """Wavelength maximum."""
101
- return self._db_dict[CryonirspBudName.wave_max.value]
102
-
103
- @property
104
- def solar_gain_ip_start_time(self) -> str:
105
- """Solar gain IP start time."""
106
- 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]
107
95
 
108
96
  @property
109
97
  def grating_position_deg(self) -> float:
@@ -227,16 +215,6 @@ class CryonirspConstants(ConstantsBase):
227
215
  conditions = [ExposureConditions(*item) for item in raw_conditions]
228
216
  return conditions
229
217
 
230
- @property
231
- def num_modstates(self) -> int:
232
- """Find the number of modulation states."""
233
- return self._db_dict[CryonirspBudName.num_modstates.value]
234
-
235
- @property
236
- def num_cs_steps(self) -> int:
237
- """Find the number of calibration sequence steps."""
238
- return self._db_dict[BudName.num_cs_steps.value]
239
-
240
218
  @property
241
219
  def stokes_I_list(self) -> [str]:
242
220
  """List containing only the Stokes-I parameter."""
@@ -252,7 +230,7 @@ class CryonirspConstants(ConstantsBase):
252
230
  @property
253
231
  def pac_init_set(self):
254
232
  """Return the label for the initial set of parameter values used when fitting demodulation matrices."""
255
- retarder_name = self._db_dict[BudName.retarder_name.value]
233
+ retarder_name = self.retarder_name
256
234
  match retarder_name:
257
235
  case "SiO2 OC":
258
236
  return "OCCal_VIS"
@@ -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
 
@@ -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,47 +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
- self.grating_constant: float = self.header["CNGRTCON"]
101
- 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()
102
99
  # The ExposureConditions are a combination of the exposure time and the OD filter name:
103
100
  self.exposure_conditions = ExposureConditions(
104
- round(self.fpa_exposure_time_ms, CRYO_EXP_TIME_ROUND_DIGITS),
105
- self.header["CNFILTNP"].upper(),
101
+ round(self.fpa_exposure_time_ms, CRYO_EXP_TIME_ROUND_DIGITS), self.filter_name
106
102
  )
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"]
110
-
111
- @property
112
- def cn1_scan_step(self):
113
- """Convert the inner loop step number from float to int."""
114
- 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])
115
107
 
116
108
 
117
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
@@ -1,5 +1,7 @@
1
1
  """Copy of UniqueBud from common that only activates if the frames are polarimetric "observe" or "polcal" task, or non-polarimetric "observe" task."""
2
- from collections import namedtuple
2
+
3
+ from enum import StrEnum
4
+ from typing import NamedTuple
3
5
  from typing import Type
4
6
 
5
7
  from dkist_processing_common.models.flower_pot import SpilledDirt
@@ -7,21 +9,30 @@ from dkist_processing_common.models.flower_pot import Stem
7
9
  from dkist_processing_common.models.flower_pot import Thorn
8
10
  from dkist_processing_common.models.task_name import TaskName
9
11
 
10
- from dkist_processing_cryonirsp.models.constants import CryonirspBudName
12
+ from dkist_processing_cryonirsp.models.fits_access import CryonirspMetadataKey
11
13
  from dkist_processing_cryonirsp.parsers.cryonirsp_l0_fits_access import CryonirspL0FitsAccess
12
14
 
13
15
 
16
+ class PolarimetricValueDataContainer(NamedTuple):
17
+ """Named tuple to hold polarimetric metadata about a task."""
18
+
19
+ task: str
20
+ num_modstates: int
21
+ spin_mode: str
22
+ bud_value: str | int | float | bool
23
+
24
+
14
25
  class PolarimetricCheckingUniqueBud(Stem):
15
26
  """Bud for checking if frames are polarimetric."""
16
27
 
17
- PolarimetricValueData = namedtuple(
18
- "PolarimetricValueData", ["task", "num_modstates", "spin_mode", "bud_value"]
19
- )
28
+ PolarimetricValueData = PolarimetricValueDataContainer
20
29
  observe_task_name = TaskName.observe.value.casefold()
21
30
  polcal_task_name = TaskName.polcal.value.casefold()
22
31
 
23
- def __init__(self, constant_name: str, metadata_key: str):
32
+ def __init__(self, constant_name: str, metadata_key: str | StrEnum):
24
33
  super().__init__(stem_name=constant_name)
34
+ if isinstance(metadata_key, StrEnum):
35
+ metadata_key = metadata_key.name
25
36
  self.metadata_key = metadata_key
26
37
 
27
38
  @property
@@ -71,7 +82,7 @@ class PolarimetricCheckingUniqueBud(Stem):
71
82
  # Some intensity mode data has the number of modulator states set to 0
72
83
  num_modstates = fits_obj.number_of_modulator_states or 1
73
84
 
74
- if self.metadata_key == "number_of_modulator_states":
85
+ if self.metadata_key == CryonirspMetadataKey.number_of_modulator_states.name:
75
86
  bud_value = num_modstates
76
87
  else:
77
88
  bud_value = getattr(fits_obj, self.metadata_key)
@@ -3,6 +3,7 @@ Machinery for sorting files based on scan step.
3
3
 
4
4
  Also includes base classes that can be used to sort files based on map scan.
5
5
  """
6
+
6
7
  ######################
7
8
  # HOW THIS ALL WORKS #
8
9
  ######################
@@ -312,11 +313,11 @@ class MapScanStepStemBase(Stem, ABC):
312
313
  return step_list.index(scan_step_obj) + 1 # Here we decide that map scan indices start at 1
313
314
 
314
315
 
315
- class NumberOfScanStepsBud(MapScanStepStemBase):
316
- """Bud for finding the total number of scan steps."""
316
+ class NumberOfScanStepsBase(MapScanStepStemBase, ABC):
317
+ """Base class for managing scan steps."""
317
318
 
318
- def __init__(self):
319
- super().__init__(stem_name=CryonirspBudName.num_scan_steps.value)
319
+ def __init__(self, stem_name: CryonirspBudName):
320
+ super().__init__(stem_name=stem_name)
320
321
 
321
322
  @cached_property
322
323
  def map_scan_to_obj_dict(self) -> dict[int, list[SingleScanStep]]:
@@ -361,6 +362,13 @@ class NumberOfScanStepsBud(MapScanStepStemBase):
361
362
 
362
363
  return len(completed_steps)
363
364
 
365
+
366
+ class NumberOfScanStepsBud(NumberOfScanStepsBase):
367
+ """Bud for finding the total number of scan steps."""
368
+
369
+ def __init__(self):
370
+ super().__init__(stem_name=CryonirspBudName.num_scan_steps.value)
371
+
364
372
  def getter(self, key):
365
373
  """
366
374
  Compute the number of complete scan steps.