dkist-processing-cryonirsp 1.3.4__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.

Potentially problematic release.


This version of dkist-processing-cryonirsp might be problematic. Click here for more details.

Files changed (111) hide show
  1. changelog/.gitempty +0 -0
  2. dkist_processing_cryonirsp/__init__.py +11 -0
  3. dkist_processing_cryonirsp/config.py +12 -0
  4. dkist_processing_cryonirsp/models/__init__.py +1 -0
  5. dkist_processing_cryonirsp/models/constants.py +248 -0
  6. dkist_processing_cryonirsp/models/exposure_conditions.py +26 -0
  7. dkist_processing_cryonirsp/models/parameters.py +296 -0
  8. dkist_processing_cryonirsp/models/tags.py +168 -0
  9. dkist_processing_cryonirsp/models/task_name.py +14 -0
  10. dkist_processing_cryonirsp/parsers/__init__.py +1 -0
  11. dkist_processing_cryonirsp/parsers/cryonirsp_l0_fits_access.py +111 -0
  12. dkist_processing_cryonirsp/parsers/cryonirsp_l1_fits_access.py +30 -0
  13. dkist_processing_cryonirsp/parsers/exposure_conditions.py +163 -0
  14. dkist_processing_cryonirsp/parsers/map_repeats.py +40 -0
  15. dkist_processing_cryonirsp/parsers/measurements.py +55 -0
  16. dkist_processing_cryonirsp/parsers/modstates.py +31 -0
  17. dkist_processing_cryonirsp/parsers/optical_density_filters.py +40 -0
  18. dkist_processing_cryonirsp/parsers/polarimetric_check.py +120 -0
  19. dkist_processing_cryonirsp/parsers/scan_step.py +412 -0
  20. dkist_processing_cryonirsp/parsers/time.py +80 -0
  21. dkist_processing_cryonirsp/parsers/wavelength.py +26 -0
  22. dkist_processing_cryonirsp/tasks/__init__.py +19 -0
  23. dkist_processing_cryonirsp/tasks/assemble_movie.py +202 -0
  24. dkist_processing_cryonirsp/tasks/bad_pixel_map.py +96 -0
  25. dkist_processing_cryonirsp/tasks/beam_boundaries_base.py +279 -0
  26. dkist_processing_cryonirsp/tasks/ci_beam_boundaries.py +55 -0
  27. dkist_processing_cryonirsp/tasks/ci_science.py +169 -0
  28. dkist_processing_cryonirsp/tasks/cryonirsp_base.py +67 -0
  29. dkist_processing_cryonirsp/tasks/dark.py +98 -0
  30. dkist_processing_cryonirsp/tasks/gain.py +251 -0
  31. dkist_processing_cryonirsp/tasks/instrument_polarization.py +447 -0
  32. dkist_processing_cryonirsp/tasks/l1_output_data.py +44 -0
  33. dkist_processing_cryonirsp/tasks/linearity_correction.py +582 -0
  34. dkist_processing_cryonirsp/tasks/make_movie_frames.py +302 -0
  35. dkist_processing_cryonirsp/tasks/mixin/__init__.py +1 -0
  36. dkist_processing_cryonirsp/tasks/mixin/beam_access.py +52 -0
  37. dkist_processing_cryonirsp/tasks/mixin/corrections.py +177 -0
  38. dkist_processing_cryonirsp/tasks/mixin/intermediate_frame.py +193 -0
  39. dkist_processing_cryonirsp/tasks/mixin/linearized_frame.py +309 -0
  40. dkist_processing_cryonirsp/tasks/mixin/shift_measurements.py +297 -0
  41. dkist_processing_cryonirsp/tasks/parse.py +281 -0
  42. dkist_processing_cryonirsp/tasks/quality_metrics.py +271 -0
  43. dkist_processing_cryonirsp/tasks/science_base.py +511 -0
  44. dkist_processing_cryonirsp/tasks/sp_beam_boundaries.py +270 -0
  45. dkist_processing_cryonirsp/tasks/sp_dispersion_axis_correction.py +484 -0
  46. dkist_processing_cryonirsp/tasks/sp_geometric.py +585 -0
  47. dkist_processing_cryonirsp/tasks/sp_science.py +299 -0
  48. dkist_processing_cryonirsp/tasks/sp_solar_gain.py +475 -0
  49. dkist_processing_cryonirsp/tasks/trial_output_data.py +61 -0
  50. dkist_processing_cryonirsp/tasks/write_l1.py +1033 -0
  51. dkist_processing_cryonirsp/tests/__init__.py +1 -0
  52. dkist_processing_cryonirsp/tests/conftest.py +456 -0
  53. dkist_processing_cryonirsp/tests/header_models.py +592 -0
  54. dkist_processing_cryonirsp/tests/local_trial_workflows/__init__.py +0 -0
  55. dkist_processing_cryonirsp/tests/local_trial_workflows/l0_cals_only.py +541 -0
  56. dkist_processing_cryonirsp/tests/local_trial_workflows/l0_to_l1.py +615 -0
  57. dkist_processing_cryonirsp/tests/local_trial_workflows/linearize_only.py +96 -0
  58. dkist_processing_cryonirsp/tests/local_trial_workflows/local_trial_helpers.py +592 -0
  59. dkist_processing_cryonirsp/tests/test_assemble_movie.py +144 -0
  60. dkist_processing_cryonirsp/tests/test_assemble_qualilty.py +517 -0
  61. dkist_processing_cryonirsp/tests/test_bad_pixel_maps.py +115 -0
  62. dkist_processing_cryonirsp/tests/test_ci_beam_boundaries.py +106 -0
  63. dkist_processing_cryonirsp/tests/test_ci_science.py +355 -0
  64. dkist_processing_cryonirsp/tests/test_corrections.py +126 -0
  65. dkist_processing_cryonirsp/tests/test_cryo_base.py +202 -0
  66. dkist_processing_cryonirsp/tests/test_cryo_constants.py +76 -0
  67. dkist_processing_cryonirsp/tests/test_dark.py +287 -0
  68. dkist_processing_cryonirsp/tests/test_gain.py +278 -0
  69. dkist_processing_cryonirsp/tests/test_instrument_polarization.py +531 -0
  70. dkist_processing_cryonirsp/tests/test_linearity_correction.py +245 -0
  71. dkist_processing_cryonirsp/tests/test_make_movie_frames.py +111 -0
  72. dkist_processing_cryonirsp/tests/test_parameters.py +266 -0
  73. dkist_processing_cryonirsp/tests/test_parse.py +1439 -0
  74. dkist_processing_cryonirsp/tests/test_quality.py +203 -0
  75. dkist_processing_cryonirsp/tests/test_sp_beam_boundaries.py +112 -0
  76. dkist_processing_cryonirsp/tests/test_sp_dispersion_axis_correction.py +155 -0
  77. dkist_processing_cryonirsp/tests/test_sp_geometric.py +319 -0
  78. dkist_processing_cryonirsp/tests/test_sp_make_movie_frames.py +121 -0
  79. dkist_processing_cryonirsp/tests/test_sp_science.py +483 -0
  80. dkist_processing_cryonirsp/tests/test_sp_solar.py +198 -0
  81. dkist_processing_cryonirsp/tests/test_trial_create_quality_report.py +79 -0
  82. dkist_processing_cryonirsp/tests/test_trial_output_data.py +251 -0
  83. dkist_processing_cryonirsp/tests/test_workflows.py +9 -0
  84. dkist_processing_cryonirsp/tests/test_write_l1.py +436 -0
  85. dkist_processing_cryonirsp/workflows/__init__.py +2 -0
  86. dkist_processing_cryonirsp/workflows/ci_l0_processing.py +77 -0
  87. dkist_processing_cryonirsp/workflows/sp_l0_processing.py +84 -0
  88. dkist_processing_cryonirsp/workflows/trial_workflows.py +190 -0
  89. dkist_processing_cryonirsp-1.3.4.dist-info/METADATA +194 -0
  90. dkist_processing_cryonirsp-1.3.4.dist-info/RECORD +111 -0
  91. dkist_processing_cryonirsp-1.3.4.dist-info/WHEEL +5 -0
  92. dkist_processing_cryonirsp-1.3.4.dist-info/top_level.txt +4 -0
  93. docs/Makefile +134 -0
  94. docs/bad_pixel_calibration.rst +47 -0
  95. docs/beam_angle_calculation.rst +53 -0
  96. docs/beam_boundary_computation.rst +88 -0
  97. docs/changelog.rst +7 -0
  98. docs/ci_science_calibration.rst +33 -0
  99. docs/conf.py +52 -0
  100. docs/index.rst +21 -0
  101. docs/l0_to_l1_cryonirsp_ci-full-trial.rst +10 -0
  102. docs/l0_to_l1_cryonirsp_ci.rst +10 -0
  103. docs/l0_to_l1_cryonirsp_sp-full-trial.rst +10 -0
  104. docs/l0_to_l1_cryonirsp_sp.rst +10 -0
  105. docs/linearization.rst +43 -0
  106. docs/make.bat +170 -0
  107. docs/requirements.txt +1 -0
  108. docs/requirements_table.rst +8 -0
  109. docs/scientific_changelog.rst +10 -0
  110. docs/sp_science_calibration.rst +59 -0
  111. licenses/LICENSE.rst +11 -0
@@ -0,0 +1,168 @@
1
+ """CryoNIRSP tags."""
2
+ from enum import Enum
3
+
4
+ from dkist_processing_common.models.tags import StemName
5
+ from dkist_processing_common.models.tags import Tag
6
+
7
+ from dkist_processing_cryonirsp.models.exposure_conditions import ExposureConditions
8
+ from dkist_processing_cryonirsp.models.task_name import CryonirspTaskName
9
+
10
+
11
+ class CryonirspStemName(str, Enum):
12
+ """Controlled list of Tag Stems."""
13
+
14
+ linearized = "LINEARIZED"
15
+ beam = "BEAM"
16
+ scan_step = "SCAN_STEP"
17
+ curr_frame_in_ramp = "CURR_FRAME_IN_RAMP"
18
+ time_obs = "TIME_OBS"
19
+ meas_num = "MEAS_NUM"
20
+ map_scan = "MAP_SCAN"
21
+ exposure_conditions = "EXPOSURE_CONDITIONS"
22
+
23
+
24
+ class CryonirspTag(Tag):
25
+ """CryoNIRSP specific tag formatting."""
26
+
27
+ @classmethod
28
+ def beam(cls, beam_num: int) -> str:
29
+ """
30
+ Tags by beam number.
31
+
32
+ Parameters
33
+ ----------
34
+ beam_num
35
+ The beam number
36
+
37
+ Returns
38
+ -------
39
+ The formatted tag string
40
+ """
41
+ return cls.format_tag(CryonirspStemName.beam, beam_num)
42
+
43
+ @classmethod
44
+ def scan_step(cls, scan_step: int) -> str:
45
+ """
46
+ Tags by the current scan step number.
47
+
48
+ Parameters
49
+ ----------
50
+ scan_step
51
+ The current scan step number
52
+
53
+ Returns
54
+ -------
55
+ The formatted tag string
56
+ """
57
+ return cls.format_tag(CryonirspStemName.scan_step, scan_step)
58
+
59
+ @classmethod
60
+ def map_scan(cls, map_scan: int) -> str:
61
+ """
62
+ Tags by the current scan step number.
63
+
64
+ Parameters
65
+ ----------
66
+ map_scan
67
+ The current map_scan number
68
+
69
+ Returns
70
+ -------
71
+ The formatted tag string
72
+ """
73
+ return cls.format_tag(CryonirspStemName.map_scan, map_scan)
74
+
75
+ @classmethod
76
+ def linearized(cls) -> str:
77
+ """
78
+ Tags for linearized frames.
79
+
80
+ Returns
81
+ -------
82
+ The formatted tag string
83
+ """
84
+ return cls.format_tag(CryonirspStemName.linearized)
85
+
86
+ @classmethod
87
+ def curr_frame_in_ramp(cls, curr_frame_in_ramp: int) -> str:
88
+ """
89
+ Tags based on the current frame number in the ramp.
90
+
91
+ Parameters
92
+ ----------
93
+ curr_frame_in_ramp
94
+ The current frame number for this ramp
95
+
96
+ Returns
97
+ -------
98
+ The formatted tag string
99
+ """
100
+ return cls.format_tag(CryonirspStemName.curr_frame_in_ramp, curr_frame_in_ramp)
101
+
102
+ @classmethod
103
+ def time_obs(cls, time_obs: str) -> str:
104
+ """
105
+ Tags by the observe date.
106
+
107
+ Parameters
108
+ ----------
109
+ time_obs
110
+ The observe time
111
+
112
+ Returns
113
+ -------
114
+ The formatted tag string
115
+ """
116
+ return cls.format_tag(CryonirspStemName.time_obs, time_obs)
117
+
118
+ @classmethod
119
+ def meas_num(cls, meas_num: int) -> str:
120
+ """
121
+ Tags by the measurement number.
122
+
123
+ Parameters
124
+ ----------
125
+ meas_num
126
+ The current measurement number
127
+
128
+ Returns
129
+ -------
130
+ The formatted tag string
131
+ """
132
+ return cls.format_tag(CryonirspStemName.meas_num, meas_num)
133
+
134
+ @classmethod
135
+ def exposure_conditions(cls, exposure_conditions: ExposureConditions) -> str:
136
+ """
137
+ Tags by the measurement number.
138
+
139
+ Parameters
140
+ ----------
141
+ exposure_conditions
142
+ A tuple of (exposure time, filter name)
143
+
144
+ Returns
145
+ -------
146
+ The formatted tag string
147
+ """
148
+ return cls.format_tag(CryonirspStemName.exposure_conditions, exposure_conditions)
149
+
150
+ @classmethod
151
+ def task_beam_boundaries(cls) -> str:
152
+ """Tags intermediate beam boundary calibration objects."""
153
+ return cls.format_tag(StemName.task, CryonirspTaskName.beam_boundaries.value)
154
+
155
+ @classmethod
156
+ def task_bad_pixel_map(cls) -> str:
157
+ """Tags intermediate bad pixel map objects."""
158
+ return cls.format_tag(StemName.task, CryonirspTaskName.bad_pixel_map.value)
159
+
160
+ @classmethod
161
+ def task_spectral_fit(cls) -> str:
162
+ """Tags spectral fit solution."""
163
+ return cls.format_tag(StemName.task, CryonirspTaskName.spectral_fit.value)
164
+
165
+ @classmethod
166
+ def task_characteristic_spectra(cls) -> str:
167
+ """Tags 1D intermediate characteristic spectra."""
168
+ return cls.format_tag(StemName.task, CryonirspTaskName.solar_char_spec.value)
@@ -0,0 +1,14 @@
1
+ """List of intermediate task names."""
2
+ from enum import Enum
3
+
4
+
5
+ class CryonirspTaskName(str, Enum):
6
+ """Controlled list of CryoNirsp task tag names."""
7
+
8
+ beam_boundaries = "BEAM_BOUNDARIES"
9
+ bad_pixel_map = "BAD_PIXEL_MAP"
10
+ polcal_dark = "POLCAL_DARK"
11
+ polcal_gain = "POLCAL_GAIN"
12
+ spectral_corrected_solar_array = "SPECTRAL_CORRECTED_SOLAR_ARRAY"
13
+ spectral_fit = "SPECTRAL_FIT"
14
+ solar_char_spec = "SOLAR_CHAR_SPEC"
@@ -0,0 +1 @@
1
+ """Init."""
@@ -0,0 +1,111 @@
1
+ """CryoNIRSP FITS access for L0 data."""
2
+ from astropy.io import fits
3
+ from dkist_processing_common.parsers.l0_fits_access import L0FitsAccess
4
+
5
+ from dkist_processing_cryonirsp.models.exposure_conditions import CRYO_EXP_TIME_ROUND_DIGITS
6
+ from dkist_processing_cryonirsp.models.exposure_conditions import ExposureConditions
7
+
8
+
9
+ class CryonirspRampFitsAccess(L0FitsAccess):
10
+ """
11
+ Class to provide easy access to L0 headers for non-linearized (raw) CryoNIRSP data.
12
+
13
+ i.e. instead of <CryonirspL0FitsAccess>.header['key'] this class lets us use <CryonirspL0FitsAccess>.key instead
14
+
15
+ Parameters
16
+ ----------
17
+ hdu :
18
+ Fits L0 header object
19
+
20
+ name : str
21
+ The name of the file that was loaded into this FitsAccess object
22
+
23
+ auto_squeeze : bool
24
+ When set to True, dimensions of length 1 will be removed from the array
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ hdu: fits.ImageHDU | fits.PrimaryHDU | fits.CompImageHDU,
30
+ name: str | None = None,
31
+ auto_squeeze: bool = True,
32
+ ):
33
+ super().__init__(hdu=hdu, name=name, auto_squeeze=auto_squeeze)
34
+
35
+ self.camera_readout_mode = self.header["CNCAMMD"]
36
+ self.curr_frame_in_ramp: int = self.header["CNCNDR"]
37
+ self.num_frames_in_ramp: int = self.header["CNNNDR"]
38
+ self.arm_id: str = self.header["CNARMID"]
39
+ self.filter_name = self.header["CNFILTNP"].upper()
40
+ self.roi_1_origin_x = self.header["HWROI1OX"]
41
+ self.roi_1_origin_y = self.header["HWROI1OY"]
42
+ self.roi_1_size_x = self.header["HWROI1SX"]
43
+ self.roi_1_size_y = self.header["HWROI1SY"]
44
+ self.obs_ip_start_time = self.header["DKIST011"]
45
+
46
+
47
+ class CryonirspL0FitsAccess(L0FitsAccess):
48
+ """
49
+ Class to provide easy access to L0 headers for linearized (ready for processing) CryoNIRSP data.
50
+
51
+ i.e. instead of <CryonirspL0FitsAccess>.header['key'] this class lets us use <CryonirspL0FitsAccess>.key instead
52
+
53
+ Parameters
54
+ ----------
55
+ hdu :
56
+ Fits L0 header object
57
+
58
+ name : str
59
+ The name of the file that was loaded into this FitsAccess object
60
+
61
+ auto_squeeze : bool
62
+ When set to True, dimensions of length 1 will be removed from the array
63
+ """
64
+
65
+ def __init__(
66
+ self,
67
+ hdu: fits.ImageHDU | fits.PrimaryHDU | fits.CompImageHDU,
68
+ name: str | None = None,
69
+ auto_squeeze: bool = True,
70
+ ):
71
+ super().__init__(hdu=hdu, name=name, auto_squeeze=auto_squeeze)
72
+
73
+ self.arm_id: str = self.header["CNARMID"]
74
+ self.number_of_modulator_states: int = self.header["CNMODNST"]
75
+ self.modulator_state: int = self.header["CNMODCST"]
76
+ self.scan_step: int = self.header["CNCURSCN"]
77
+ self.num_scan_steps: int = self.header["CNNUMSCN"]
78
+ self.num_cn1_scan_steps: int = self.header["CNP1DNSP"]
79
+ self.num_cn2_scan_steps: int = self.header["CNP2DNSP"]
80
+ self.cn2_step_size: float = self.header["CNP2DSS"]
81
+ self.meas_num: int = self.header["CNCMEAS"]
82
+ self.num_meas: int = self.header["CNNMEAS"]
83
+ self.sub_repeat_num = self.header["CNCSREP"]
84
+ self.num_sub_repeats: int = self.header["CNSUBREP"]
85
+ self.modulator_spin_mode: str = self.header["CNSPINMD"]
86
+ self.axis_1_type: str = self.header["CTYPE1"]
87
+ self.axis_2_type: str = self.header["CTYPE2"]
88
+ self.axis_3_type: str = self.header["CTYPE3"]
89
+ self.wave_min: float = round(
90
+ self.header["CRVAL1"] - (self.header["CRPIX1"] * self.header["CDELT1"]), 1
91
+ )
92
+ self.wave_max: float = round(
93
+ self.header["CRVAL1"]
94
+ + ((self.header["NAXIS1"] - self.header["CRPIX1"]) * self.header["CDELT1"]),
95
+ 1,
96
+ )
97
+ self.grating_position_deg: float = self.header["CNGRTPOS"]
98
+ self.grating_littrow_angle_deg: float = self.header["CNGRTLAT"]
99
+ self.grating_constant: float = self.header["CNGRTCON"]
100
+ self.obs_ip_start_time = self.header["DKIST011"]
101
+ # The ExposureConditions are a combination of the exposure time and the OD filter name:
102
+ self.exposure_conditions = ExposureConditions(
103
+ round(self.fpa_exposure_time_ms, CRYO_EXP_TIME_ROUND_DIGITS),
104
+ self.header["CNFILTNP"].upper(),
105
+ )
106
+ self.solar_gain_ip_start_time = self.header["DATE-OBS"]
107
+
108
+ @property
109
+ def cn1_scan_step(self):
110
+ """Convert the inner loop step number from float to int."""
111
+ return int(self.header["CNP1DCUR"])
@@ -0,0 +1,30 @@
1
+ """CryoNIRSP FITS access for L1 data."""
2
+ from astropy.io import fits
3
+ from dkist_processing_common.parsers.l1_fits_access import L1FitsAccess
4
+
5
+
6
+ class CryonirspL1FitsAccess(L1FitsAccess):
7
+ """
8
+ Class to provide easy access to L1 headers.
9
+
10
+ i.e. instead of <CryonirspL1FitsAccess>.header['key'] this class lets us use <CryonirspL1FitsAccess>.key instead
11
+
12
+ Parameters
13
+ ----------
14
+ hdu :
15
+ Fits L1 header object
16
+
17
+ name : str
18
+ The name of the file that was loaded into this FitsAccess object
19
+
20
+ auto_squeeze : bool
21
+ When set to True, dimensions of length 1 will be removed from the array
22
+ """
23
+
24
+ def __init__(
25
+ self,
26
+ hdu: fits.ImageHDU | fits.PrimaryHDU | fits.CompImageHDU,
27
+ name: str | None = None,
28
+ auto_squeeze: bool | None = True,
29
+ ):
30
+ super().__init__(hdu=hdu, name=name, auto_squeeze=auto_squeeze)
@@ -0,0 +1,163 @@
1
+ """Buds to parse the combination of exposure time and filter name."""
2
+ from collections import namedtuple
3
+ from typing import Hashable
4
+ from typing import Type
5
+
6
+ from dkist_processing_common.models.flower_pot import SpilledDirt
7
+ from dkist_processing_common.models.flower_pot import Stem
8
+ from dkist_processing_common.models.flower_pot import Thorn
9
+ from dkist_processing_common.models.task_name import TaskName
10
+ from dkist_processing_common.parsers.task import parse_header_ip_task_with_gains
11
+
12
+ from dkist_processing_cryonirsp.models.constants import CryonirspBudName
13
+ from dkist_processing_cryonirsp.models.exposure_conditions import ExposureConditions
14
+ from dkist_processing_cryonirsp.parsers.cryonirsp_l0_fits_access import CryonirspL0FitsAccess
15
+
16
+ _DARK_AND_POLCAL_TASK_TYPES = (TaskName.dark.value, TaskName.polcal.value)
17
+
18
+
19
+ class CryonirspTaskExposureConditionsBud(Stem):
20
+ """
21
+ Bud to allow custom parsing of exposure conditions based on ip task type.
22
+
23
+ Parameters
24
+ ----------
25
+ stem_name : str
26
+ The name of the stem of the tag
27
+ ip_task_type : str
28
+ Instrument program task type
29
+ """
30
+
31
+ def __init__(self, stem_name: str, ip_task_type: str):
32
+ super().__init__(stem_name=stem_name)
33
+ self.metadata_key = "exposure_conditions"
34
+ self.ip_task_type = ip_task_type
35
+
36
+ def setter(self, fits_obj: CryonirspL0FitsAccess):
37
+ """
38
+ Set the value of the bud.
39
+
40
+ Parameters
41
+ ----------
42
+ fits_obj:
43
+ A single FitsAccess object
44
+ """
45
+ ip_task_type = parse_header_ip_task_with_gains(fits_obj)
46
+ if ip_task_type.lower() == self.ip_task_type.lower():
47
+ return getattr(fits_obj, self.metadata_key)
48
+ return SpilledDirt
49
+
50
+ def getter(self, key: Hashable) -> tuple[ExposureConditions, ...]:
51
+ """Return a list of the ExposureConditions for this ip task type."""
52
+ exposure_conditions_tuple = tuple(sorted(set(self.key_to_petal_dict.values())))
53
+ return exposure_conditions_tuple
54
+
55
+
56
+ class CryonirspNonDarkAndNonPolcalTaskExposureConditionsBud(Stem):
57
+ """For ip task types that are neither DARK nor POLCAL, produce a list of exposure conditions tuples."""
58
+
59
+ def __init__(self):
60
+ super().__init__(
61
+ stem_name=CryonirspBudName.non_dark_and_non_polcal_task_exposure_conditions_list.value
62
+ )
63
+ self.metadata_key = "exposure_conditions"
64
+
65
+ def setter(self, fits_obj: CryonirspL0FitsAccess) -> ExposureConditions | Type[SpilledDirt]:
66
+ """
67
+ Set the task exposure conditions tuple for this fits object.
68
+
69
+ Parameters
70
+ ----------
71
+ fits_obj
72
+ The input fits object
73
+ Returns
74
+ -------
75
+ The exposure time associated with this fits object
76
+ """
77
+ if fits_obj.ip_task_type.upper() not in _DARK_AND_POLCAL_TASK_TYPES:
78
+ return getattr(fits_obj, self.metadata_key)
79
+ return SpilledDirt
80
+
81
+ def getter(self, key: Hashable) -> tuple[ExposureConditions, ...]:
82
+ """
83
+ Get the list of exposure conditions tuples.
84
+
85
+ Parameters
86
+ ----------
87
+ key
88
+ The input key
89
+
90
+ Returns
91
+ -------
92
+ A tuple of (exposure time, filter name) tuples
93
+ """
94
+ exposure_conditions_tuple = tuple(sorted(set(self.key_to_petal_dict.values())))
95
+ return exposure_conditions_tuple
96
+
97
+
98
+ class CryonirspPickyDarkExposureConditionsBud(Stem):
99
+ """Parse exposure conditions tuples to ensure existence of dark frames with the required exposure conditions."""
100
+
101
+ DarkTaskTestAndExposureConditions = namedtuple(
102
+ "DarkTaskTestAndExposureConditions", ["is_dark", "exposure_conditions"]
103
+ )
104
+
105
+ def __init__(self):
106
+ super().__init__(stem_name=CryonirspBudName.picky_dark_exposure_conditions_list.value)
107
+ self.metadata_key = "exposure_conditions"
108
+
109
+ def setter(self, fits_obj: CryonirspL0FitsAccess) -> tuple | Type[SpilledDirt]:
110
+ """
111
+ Set the task exposure conditions tuple and whether it is a DARK task for this fits object.
112
+
113
+ Parameters
114
+ ----------
115
+ fits_obj
116
+ The input fits object
117
+ Returns
118
+ -------
119
+ A tuple of a boolean indicating if the task is a dark task, and the exposure conditions for this fits object
120
+ """
121
+ if fits_obj.ip_task_type.upper() == TaskName.dark.value:
122
+ return self.DarkTaskTestAndExposureConditions(
123
+ is_dark=True, exposure_conditions=getattr(fits_obj, self.metadata_key)
124
+ )
125
+ if fits_obj.ip_task_type.upper() not in _DARK_AND_POLCAL_TASK_TYPES:
126
+ return self.DarkTaskTestAndExposureConditions(
127
+ is_dark=False, exposure_conditions=getattr(fits_obj, self.metadata_key)
128
+ )
129
+ # Polcal falls through
130
+ return SpilledDirt
131
+
132
+ def getter(self, key: Hashable) -> Type[Thorn]:
133
+ """
134
+ Parse all exposure conditions and raise an error if any non-dark exposure condition is missing from the set of dark exposure conditions.
135
+
136
+ Parameters
137
+ ----------
138
+ key
139
+ The input key
140
+
141
+ Returns
142
+ -------
143
+ Thorn
144
+ """
145
+ dark_task_test_and_exposure_conditions_set = set(self.key_to_petal_dict.values())
146
+ dark_exposure_conditions_set = {
147
+ item.exposure_conditions
148
+ for item in dark_task_test_and_exposure_conditions_set
149
+ if item.is_dark
150
+ }
151
+ other_exposure_conditions_set = {
152
+ item.exposure_conditions
153
+ for item in dark_task_test_and_exposure_conditions_set
154
+ if not item.is_dark
155
+ }
156
+ other_exposure_conditions_missing_from_dark_exposure_conditions = (
157
+ other_exposure_conditions_set - dark_exposure_conditions_set
158
+ )
159
+ if other_exposure_conditions_missing_from_dark_exposure_conditions:
160
+ raise ValueError(
161
+ f"Exposure conditions required in the set of dark frames not found. Missing conditions = {other_exposure_conditions_missing_from_dark_exposure_conditions}"
162
+ )
163
+ return Thorn
@@ -0,0 +1,40 @@
1
+ """Stems for organizing files into separate dsps repeats."""
2
+ from dkist_processing_cryonirsp.models.constants import CryonirspBudName
3
+ from dkist_processing_cryonirsp.models.tags import CryonirspStemName
4
+ from dkist_processing_cryonirsp.parsers.scan_step import MapScanStepStemBase
5
+
6
+
7
+ class MapScanFlower(MapScanStepStemBase):
8
+ """Flower for computing and assigning map scan numbers."""
9
+
10
+ def __init__(self):
11
+ super().__init__(stem_name=CryonirspStemName.map_scan.value)
12
+
13
+ def getter(self, key: str) -> int:
14
+ """Compute the map scan number for a single frame.
15
+
16
+ The frame implies a SingleScanStep. That object is then compared to the sorted list of objects for a single
17
+ (raster_step, meas_num, modstate, sub_repeat) tuple. The location within that sorted list is the map scan number.
18
+ """
19
+ return self.get_map_scan_for_key(key)
20
+
21
+
22
+ class NumMapScansBud(MapScanStepStemBase):
23
+ """
24
+ Bud for determining the total number of dsps repeats.
25
+
26
+ Also checks that all scan steps have the same number of dsps repeats.
27
+ """
28
+
29
+ def __init__(self):
30
+ super().__init__(stem_name=CryonirspBudName.num_map_scans.value)
31
+
32
+ def getter(self, key: str) -> int:
33
+ """
34
+ Compute the total number of dsps repeats.
35
+
36
+ The number of map_scans for every scan step are calculated and if a map_scan is incomplete,
37
+ it will not be included.
38
+ Assumes the incomplete map_scan is always the last one due to summit abort or cancellation.
39
+ """
40
+ return self.number_of_complete_map_scans
@@ -0,0 +1,55 @@
1
+ """Copies of UniqueBud and SingleValueSingleKeyFlower from common that only activate if the frames are "observe" task."""
2
+ from typing import Type
3
+
4
+ from dkist_processing_common.models.flower_pot import SpilledDirt
5
+ from dkist_processing_common.parsers.single_value_single_key_flower import (
6
+ SingleValueSingleKeyFlower,
7
+ )
8
+ from dkist_processing_common.parsers.unique_bud import UniqueBud
9
+
10
+ from dkist_processing_cryonirsp.models.constants import CryonirspBudName
11
+ from dkist_processing_cryonirsp.models.tags import CryonirspStemName
12
+ from dkist_processing_cryonirsp.parsers.cryonirsp_l0_fits_access import CryonirspL0FitsAccess
13
+
14
+
15
+ class NumberOfMeasurementsBud(UniqueBud):
16
+ """Bud for finding the total number of measurements per scan step."""
17
+
18
+ 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
+ )
23
+
24
+ def setter(self, fits_obj: CryonirspL0FitsAccess) -> Type[SpilledDirt] | int:
25
+ """
26
+ Setter for the bud.
27
+
28
+ Parameters
29
+ ----------
30
+ fits_obj:
31
+ A single FitsAccess object
32
+ """
33
+ if fits_obj.ip_task_type != "observe":
34
+ return SpilledDirt
35
+ return getattr(fits_obj, self.metadata_key)
36
+
37
+
38
+ class MeasurementNumberFlower(SingleValueSingleKeyFlower):
39
+ """Flower for a measurement number."""
40
+
41
+ def __init__(self):
42
+ super().__init__(tag_stem_name=CryonirspStemName.meas_num.value, metadata_key="meas_num")
43
+
44
+ def setter(self, fits_obj: CryonirspL0FitsAccess) -> Type[SpilledDirt] | int:
45
+ """
46
+ Setter for a flower.
47
+
48
+ Parameters
49
+ ----------
50
+ fits_obj:
51
+ A single FitsAccess object
52
+ """
53
+ if fits_obj.ip_task_type != "observe":
54
+ return SpilledDirt
55
+ return super().setter(fits_obj)
@@ -0,0 +1,31 @@
1
+ """Copy of SingleValueSingleKeyFlower from common that only activates if the frames are "observe" task."""
2
+ from typing import Type
3
+
4
+ from dkist_processing_common.models.flower_pot import SpilledDirt
5
+ from dkist_processing_common.models.tags import StemName
6
+ from dkist_processing_common.parsers.single_value_single_key_flower import (
7
+ SingleValueSingleKeyFlower,
8
+ )
9
+
10
+ from dkist_processing_cryonirsp.parsers.cryonirsp_l0_fits_access import CryonirspL0FitsAccess
11
+
12
+
13
+ class ModstateNumberFlower(SingleValueSingleKeyFlower):
14
+ """Flower for a modstate number."""
15
+
16
+ def __init__(self):
17
+ super().__init__(tag_stem_name=StemName.modstate.value, metadata_key="modulator_state")
18
+
19
+ def setter(self, fits_obj: CryonirspL0FitsAccess) -> Type[SpilledDirt] | int:
20
+ """
21
+ Setter for a flower.
22
+
23
+ Parameters
24
+ ----------
25
+ fits_obj:
26
+ A single FitsAccess object
27
+ """
28
+ # Some intensity data incorrectly has modulator state = 0
29
+ if getattr(fits_obj, self.metadata_key) == 0:
30
+ return 1
31
+ return super().setter(fits_obj)
@@ -0,0 +1,40 @@
1
+ """PickyBud to implement early parsing of Optical Density Filter Names."""
2
+ from typing import Hashable
3
+
4
+ from dkist_processing_common.models.flower_pot import Stem
5
+ from dkist_processing_common.models.flower_pot import Thorn
6
+
7
+ from dkist_processing_cryonirsp.models.exposure_conditions import AllowableOpticalDensityFilterNames
8
+ from dkist_processing_cryonirsp.parsers.cryonirsp_l0_fits_access import CryonirspRampFitsAccess
9
+
10
+ ALLOWABLE_OPTICAL_DENSITY_FILTERS = {f for f in AllowableOpticalDensityFilterNames}
11
+
12
+
13
+ class OpticalDensityFiltersPickyBud(Stem):
14
+ """PickyBud to implement early parsing of Optical Density Filter Names."""
15
+
16
+ def __init__(self):
17
+ super().__init__(stem_name="OpticalDensityFilterNamePickyBud")
18
+
19
+ def setter(self, fits_obj: CryonirspRampFitsAccess):
20
+ """
21
+ Set the optical density filter name for this fits object.
22
+
23
+ Parameters
24
+ ----------
25
+ fits_obj
26
+ The input fits obj
27
+
28
+ Returns
29
+ -------
30
+ The optical density filter name associated with this fits object
31
+ """
32
+ return fits_obj.filter_name.upper()
33
+
34
+ def getter(self, key: Hashable) -> Thorn:
35
+ """Return a Thorn for valid names or raise an exception for bad names."""
36
+ filter_names = set(self.key_to_petal_dict.values())
37
+ bad_filter_names = filter_names.difference(ALLOWABLE_OPTICAL_DENSITY_FILTERS)
38
+ if bad_filter_names:
39
+ raise ValueError(f"Unknown Optical Density Filter Name(s): {bad_filter_names = }")
40
+ return Thorn