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,447 @@
1
+ """Cryo instrument polarization task."""
2
+ from abc import ABC
3
+ from abc import abstractmethod
4
+ from collections import defaultdict
5
+
6
+ import numpy as np
7
+ from astropy.io import fits
8
+ from dkist_processing_common.models.task_name import TaskName
9
+ from dkist_processing_math.arithmetic import divide_arrays_by_array
10
+ from dkist_processing_math.arithmetic import subtract_array_from_arrays
11
+ from dkist_processing_math.statistics import average_numpy_arrays
12
+ from dkist_processing_math.transform.binning import resize_arrays
13
+ from dkist_processing_pac.fitter.polcal_fitter import PolcalFitter
14
+ from dkist_processing_pac.input_data.drawer import Drawer
15
+ from dkist_processing_pac.input_data.dresser import Dresser
16
+ from dkist_service_configuration.logging import logger
17
+
18
+ from dkist_processing_cryonirsp.models.exposure_conditions import ExposureConditions
19
+ from dkist_processing_cryonirsp.models.tags import CryonirspTag
20
+ from dkist_processing_cryonirsp.parsers.cryonirsp_l0_fits_access import CryonirspL0FitsAccess
21
+ from dkist_processing_cryonirsp.tasks.cryonirsp_base import CryonirspTaskBase
22
+
23
+ __all__ = ["CIInstrumentPolarizationCalibration", "SPInstrumentPolarizationCalibration"]
24
+
25
+
26
+ def generate_polcal_quality_label(arm: str, beam: int) -> str:
27
+ """
28
+ Make a quality label given an arm and beam.
29
+
30
+ Defined here so we don't have to remember what our labels are in the L1 output data task.
31
+ """
32
+ return f"{arm} Beam {beam}"
33
+
34
+
35
+ class InstrumentPolarizationCalibrationBase(CryonirspTaskBase, ABC):
36
+ """
37
+ Base task class for instrument polarization for a CryoNIRSP calibration run.
38
+
39
+ Parameters
40
+ ----------
41
+ recipe_run_id : int
42
+ id of the recipe run used to identify the workflow run this task is part of
43
+ workflow_name : str
44
+ name of the workflow to which this instance of the task belongs
45
+ workflow_version : str
46
+ version of the workflow to which this instance of the task belongs
47
+
48
+ """
49
+
50
+ record_provenance = True
51
+
52
+ @abstractmethod
53
+ def record_polcal_quality_metrics(self, beam: int, polcal_fitter: PolcalFitter):
54
+ """Abstract method to be implemented in subclass."""
55
+ pass
56
+
57
+ def run(self) -> None:
58
+ """
59
+ For each beam.
60
+
61
+ - Reduce calibration sequence steps
62
+ - Fit reduced data to PAC parameters
63
+ - Compute and save demodulation matrices
64
+
65
+ Returns
66
+ -------
67
+ None
68
+
69
+ """
70
+ if not self.constants.correct_for_polarization:
71
+ return
72
+
73
+ target_exposure_conditions = self.constants.polcal_exposure_conditions_list
74
+ logger.info(f"{target_exposure_conditions = }")
75
+
76
+ self.generate_polcal_dark_calibration(target_exposure_conditions)
77
+ self.generate_polcal_gain_calibration(target_exposure_conditions)
78
+
79
+ logger.info(
80
+ f"Demodulation matrices will span FOV with shape {(self.parameters.polcal_num_spatial_bins, self.parameters.polcal_num_spatial_bins)}"
81
+ )
82
+ for beam in range(1, self.constants.num_beams + 1):
83
+ with self.apm_processing_step(f"Reducing CS steps for {beam = }"):
84
+ local_reduced_arrays, global_reduced_arrays = self.reduce_cs_steps(beam)
85
+
86
+ with self.apm_processing_step(f"Fit CU parameters for {beam = }"):
87
+ local_dresser = Dresser()
88
+ local_dresser.add_drawer(Drawer(local_reduced_arrays))
89
+ global_dresser = Dresser()
90
+ global_dresser.add_drawer(Drawer(global_reduced_arrays))
91
+ pac_fitter = PolcalFitter(
92
+ local_dresser=local_dresser,
93
+ global_dresser=global_dresser,
94
+ fit_mode=self.parameters.polcal_pac_fit_mode,
95
+ init_set=self.parameters.polcal_pac_init_set,
96
+ fit_TM=False,
97
+ )
98
+
99
+ with self.apm_processing_step(f"Resampling demodulation matrices for {beam = }"):
100
+ demod_matrices = pac_fitter.demodulation_matrices
101
+ # Reshaping the demodulation matrix to get rid of unit length dimensions
102
+ logger.info(f"Resampling demodulation matrices for {beam = }")
103
+ demod_matrices = self.reshape_demod_matrices(demod_matrices)
104
+ logger.info(
105
+ f"Shape of resampled demodulation matrices for {beam = }: {demod_matrices.shape}"
106
+ )
107
+
108
+ with self.apm_writing_step(f"Writing demodulation matrices for {beam = }"):
109
+ self.intermediate_frame_write_arrays(
110
+ demod_matrices,
111
+ beam=beam,
112
+ task_tag=CryonirspTag.task_demodulation_matrices(),
113
+ )
114
+
115
+ with self.apm_processing_step("Computing and recording polcal quality metrics"):
116
+ self.record_polcal_quality_metrics(beam, polcal_fitter=pac_fitter)
117
+
118
+ with self.apm_processing_step("Computing and logging quality metrics"):
119
+ no_of_raw_polcal_frames: int = self.scratch.count_all(
120
+ tags=[
121
+ CryonirspTag.linearized(),
122
+ CryonirspTag.frame(),
123
+ CryonirspTag.task_polcal(),
124
+ ],
125
+ )
126
+
127
+ self.quality_store_task_type_counts(
128
+ task_type=TaskName.polcal.value, total_frames=no_of_raw_polcal_frames
129
+ )
130
+
131
+ def reduce_cs_steps(
132
+ self, beam: int
133
+ ) -> tuple[dict[int, list[CryonirspL0FitsAccess]], dict[int, list[CryonirspL0FitsAccess]]]:
134
+ """
135
+ Reduce all of the data for the cal sequence steps for this beam.
136
+
137
+ Parameters
138
+ ----------
139
+ beam
140
+ The current beam being processed
141
+
142
+ Returns
143
+ -------
144
+ Dict
145
+ A Dict of calibrated and binned arrays for all the cs steps for this beam
146
+ """
147
+ local_reduced_array_dict = defaultdict(list)
148
+ global_reduced_array_dict = defaultdict(list)
149
+
150
+ for modstate in range(1, self.constants.num_modstates + 1):
151
+ for exposure_conditions in self.constants.polcal_exposure_conditions_list:
152
+ logger.info(f"Loading dark array for {exposure_conditions = } and {beam = }")
153
+ try:
154
+ dark_array = self.intermediate_frame_load_polcal_dark_array(
155
+ exposure_conditions=exposure_conditions,
156
+ beam=beam,
157
+ )
158
+ except StopIteration as e:
159
+ raise ValueError(
160
+ f"No matching dark array found for {exposure_conditions = }"
161
+ ) from e
162
+
163
+ logger.info(f"Loading gain array for {exposure_conditions = } and {beam = }")
164
+ try:
165
+ gain_array = self.intermediate_frame_load_polcal_gain_array(
166
+ exposure_conditions=exposure_conditions, beam=beam
167
+ )
168
+ except StopIteration as e:
169
+ raise ValueError(
170
+ f"No matching gain array found for {exposure_conditions = }"
171
+ ) from e
172
+
173
+ for cs_step in range(self.constants.num_cs_steps):
174
+ local_obj, global_obj = self.reduce_single_step(
175
+ beam,
176
+ dark_array,
177
+ gain_array,
178
+ modstate,
179
+ cs_step,
180
+ exposure_conditions,
181
+ )
182
+ local_reduced_array_dict[cs_step].append(local_obj)
183
+ global_reduced_array_dict[cs_step].append(global_obj)
184
+
185
+ return local_reduced_array_dict, global_reduced_array_dict
186
+
187
+ def reduce_single_step(
188
+ self,
189
+ beam: int,
190
+ dark_array: np.ndarray,
191
+ gain_array: np.ndarray,
192
+ modstate: int,
193
+ cs_step: int,
194
+ exposure_conditions: ExposureConditions,
195
+ ) -> tuple[CryonirspL0FitsAccess, CryonirspL0FitsAccess]:
196
+ """
197
+ Reduce a single calibration step for this beam, cs step and modulator state.
198
+
199
+ Parameters
200
+ ----------
201
+ beam : int
202
+ The current beam being processed
203
+ dark_array : np.ndarray
204
+ The dark array for the current beam
205
+ gain_array : np.ndarray
206
+ The gain array for the current beam
207
+ modstate : int
208
+ The current modulator state
209
+ cs_step : int
210
+ The current cal sequence step
211
+ exposure_conditions : ExposureConditions
212
+ The exposure conditions (exposure time, OD filter)
213
+
214
+ Returns
215
+ -------
216
+ The final reduced result for this single step
217
+ """
218
+ apm_str = f"{beam = }, {modstate = }, {cs_step = }, and {exposure_conditions = }"
219
+ logger.info(f"Reducing {apm_str}")
220
+
221
+ pol_cal_headers = (
222
+ obj.header
223
+ for obj in self.linearized_frame_polcal_fits_access_generator(
224
+ modstate=modstate,
225
+ cs_step=cs_step,
226
+ exposure_conditions=exposure_conditions,
227
+ beam=beam,
228
+ )
229
+ )
230
+ pol_cal_arrays = (
231
+ obj.data
232
+ for obj in self.linearized_frame_polcal_fits_access_generator(
233
+ modstate=modstate,
234
+ cs_step=cs_step,
235
+ exposure_conditions=exposure_conditions,
236
+ beam=beam,
237
+ )
238
+ )
239
+
240
+ avg_inst_pol_cal_header = next(pol_cal_headers)
241
+ avg_inst_pol_cal_array = average_numpy_arrays(pol_cal_arrays)
242
+
243
+ with self.apm_processing_step(f"Apply basic corrections for {apm_str}"):
244
+ dark_corrected_array = subtract_array_from_arrays(avg_inst_pol_cal_array, dark_array)
245
+ gain_corrected_array = next(divide_arrays_by_array(dark_corrected_array, gain_array))
246
+
247
+ with self.apm_processing_step(f"Extract macro pixels from {apm_str}"):
248
+ self.set_original_beam_size(gain_corrected_array)
249
+ output_shape = (
250
+ self.parameters.polcal_num_spatial_bins,
251
+ self.parameters.polcal_num_spectral_bins,
252
+ )
253
+ local_binned_array = next(resize_arrays(gain_corrected_array, output_shape))
254
+ global_binned_array = next(resize_arrays(gain_corrected_array, (1, 1)))
255
+
256
+ with self.apm_processing_step(f"Create reduced CryonirspL0FitsAccess for {apm_str}"):
257
+ local_result = CryonirspL0FitsAccess(
258
+ fits.ImageHDU(local_binned_array[:, :], avg_inst_pol_cal_header),
259
+ auto_squeeze=False,
260
+ )
261
+
262
+ global_result = CryonirspL0FitsAccess(
263
+ fits.ImageHDU(global_binned_array[None, :, :], avg_inst_pol_cal_header),
264
+ auto_squeeze=False,
265
+ )
266
+
267
+ return local_result, global_result
268
+
269
+ def reshape_demod_matrices(self, demod_matrices: np.ndarray) -> np.ndarray:
270
+ """Upsample demodulation matrices to match the full beam size.
271
+
272
+ Given an input set of demodulation matrices with shape (X', Y', 4, M) resample the output to shape
273
+ (X, Y, 4, M), where X' and Y' are the binned size of the beam FOV, X and Y are the full beam shape, M is the
274
+ number of modulator states.
275
+
276
+ If only a single demodulation matrix was made then it is returned as a single array with shape (4, M).
277
+
278
+ Parameters
279
+ ----------
280
+ demod_matrices
281
+ A set of demodulation matrices with shape (X', Y', 4, M)
282
+
283
+ Returns
284
+ -------
285
+ If X' and Y' > 1 then upsampled matrices that are the full beam size (X, Y, 4, M).
286
+ If X' == Y' == 1 then a single matric for the whole FOV with shape (4, M)
287
+ """
288
+ expected_dims = 4
289
+ if len(demod_matrices.shape) != expected_dims:
290
+ raise ValueError(
291
+ f"Expected demodulation matrices to have {expected_dims} dimensions. Got shape {demod_matrices.shape}"
292
+ )
293
+
294
+ data_shape = demod_matrices.shape[
295
+ :2
296
+ ] # The non-demodulation matrix part of the larger array
297
+ demod_shape = demod_matrices.shape[-2:] # The shape of a single demodulation matrix
298
+ logger.info(f"Demodulation FOV sampling shape: {data_shape}")
299
+ logger.info(f"Demodulation matrix shape: {demod_shape}")
300
+ if data_shape == (1, 1):
301
+ # A single modulation matrix can be used directly, so just return it after removing extraneous dimensions
302
+ logger.info(f"Single demodulation matrix detected")
303
+ return demod_matrices[0, 0, :, :]
304
+
305
+ target_shape = self.single_beam_shape + demod_shape
306
+ logger.info(f"Target full-frame demodulation shape: {target_shape}")
307
+ return self.resize_polcal_array(demod_matrices, target_shape)
308
+
309
+ def set_original_beam_size(self, array: np.ndarray) -> None:
310
+ """Record the shape of a single beam as a class property."""
311
+ self.single_beam_shape = array.shape
312
+
313
+ @staticmethod
314
+ def resize_polcal_array(array: np.ndarray, output_shape: tuple[int, ...]) -> np.ndarray:
315
+ return next(resize_arrays(array, output_shape))
316
+
317
+ def generate_polcal_dark_calibration(
318
+ self, target_exposure_conditions_list: [ExposureConditions]
319
+ ):
320
+ """Compute the polcal dark calibration."""
321
+ with self.apm_task_step(
322
+ f"Calculating dark frames for {len(target_exposure_conditions_list)} exp times"
323
+ ):
324
+ for exposure_conditions in target_exposure_conditions_list:
325
+ for beam in range(1, self.constants.num_beams + 1):
326
+ with self.apm_processing_step(
327
+ f"Calculating polcal dark array(s) for {exposure_conditions = } and {beam = }"
328
+ ):
329
+ linearized_dark_arrays = self.linearized_frame_polcal_dark_array_generator(
330
+ exposure_conditions=exposure_conditions,
331
+ beam=beam,
332
+ )
333
+ averaged_dark_array = average_numpy_arrays(linearized_dark_arrays)
334
+ with self.apm_writing_step(
335
+ f"Writing dark for {exposure_conditions = } and {beam = }"
336
+ ):
337
+ self.intermediate_frame_write_arrays(
338
+ averaged_dark_array,
339
+ task_tag=CryonirspTag.task_polcal_dark(),
340
+ exposure_conditions=exposure_conditions,
341
+ beam=beam,
342
+ )
343
+
344
+ def generate_polcal_gain_calibration(self, exposure_conditions_list: [ExposureConditions]):
345
+ """Compute the polcal gain calibration."""
346
+ with self.apm_task_step(
347
+ f"Generate gains for {len(exposure_conditions_list)} exposure conditions"
348
+ ):
349
+ for exposure_conditions in exposure_conditions_list:
350
+ for beam in range(1, self.constants.num_beams + 1):
351
+ logger.info(
352
+ f"Load polcal dark array for {exposure_conditions = } and {beam = }"
353
+ )
354
+ try:
355
+ dark_array = self.intermediate_frame_load_polcal_dark_array(
356
+ exposure_conditions=exposure_conditions,
357
+ beam=beam,
358
+ )
359
+ except StopIteration as e:
360
+ raise ValueError(
361
+ f"No matching polcal dark found for {exposure_conditions = } s and {beam = }"
362
+ ) from e
363
+ with self.apm_processing_step(
364
+ f"Calculating polcal gain array(s) for {exposure_conditions = } and {beam = }"
365
+ ):
366
+ linearized_gain_arrays = self.linearized_frame_polcal_gain_array_generator(
367
+ exposure_conditions=exposure_conditions,
368
+ beam=beam,
369
+ )
370
+ averaged_gain_array = average_numpy_arrays(linearized_gain_arrays)
371
+ dark_corrected_gain_array = next(
372
+ subtract_array_from_arrays(averaged_gain_array, dark_array)
373
+ )
374
+
375
+ bad_pixel_map = self.intermediate_frame_load_bad_pixel_map(beam=beam)
376
+ bad_pixel_corrected_array = self.corrections_correct_bad_pixels(
377
+ dark_corrected_gain_array, bad_pixel_map
378
+ )
379
+
380
+ normalized_gain_array = bad_pixel_corrected_array / np.mean(
381
+ bad_pixel_corrected_array
382
+ )
383
+
384
+ with self.apm_writing_step(
385
+ f"Writing gain array for exposure time {exposure_conditions} and {beam = }"
386
+ ):
387
+ self.intermediate_frame_write_arrays(
388
+ normalized_gain_array,
389
+ task_tag=CryonirspTag.task_polcal_gain(),
390
+ exposure_conditions=exposure_conditions,
391
+ beam=beam,
392
+ )
393
+
394
+
395
+ class CIInstrumentPolarizationCalibration(InstrumentPolarizationCalibrationBase):
396
+ """
397
+ Task class for instrument polarization for a CI CryoNIRSP calibration run.
398
+
399
+ Parameters
400
+ ----------
401
+ recipe_run_id : int
402
+ id of the recipe run used to identify the workflow run this task is part of
403
+ workflow_name : str
404
+ name of the workflow to which this instance of the task belongs
405
+ workflow_version : str
406
+ version of the workflow to which this instance of the task belongs
407
+
408
+ """
409
+
410
+ def record_polcal_quality_metrics(self, beam: int, polcal_fitter: PolcalFitter):
411
+ """Record various quality metrics from PolCal fits."""
412
+ self.quality_store_polcal_results(
413
+ polcal_fitter=polcal_fitter,
414
+ label=generate_polcal_quality_label(arm="CI", beam=beam),
415
+ bin_nums=[
416
+ self.parameters.polcal_num_spatial_bins,
417
+ self.parameters.polcal_num_spatial_bins,
418
+ ],
419
+ bin_labels=["spatial", "spatial"],
420
+ skip_recording_constant_pars=False,
421
+ )
422
+
423
+
424
+ class SPInstrumentPolarizationCalibration(InstrumentPolarizationCalibrationBase):
425
+ """Task class for instrument polarization for an SP CryoNIRSP calibration run."""
426
+
427
+ def record_polcal_quality_metrics(
428
+ self,
429
+ beam: int,
430
+ polcal_fitter: PolcalFitter,
431
+ ) -> None:
432
+ """Record various quality metrics from PolCal fits."""
433
+ self.quality_store_polcal_results(
434
+ polcal_fitter=polcal_fitter,
435
+ label=generate_polcal_quality_label(arm="SP", beam=beam),
436
+ bin_nums=[
437
+ self.parameters.polcal_num_spatial_bins,
438
+ self.parameters.polcal_num_spectral_bins,
439
+ ],
440
+ bin_labels=["spatial", "spectral"],
441
+ ## This is a bit of a hack and thus needs some explanation
442
+ # By using the ``skip_recording_constant_pars`` switch we DON'T record the "polcal constant parameters" metric
443
+ # for beam 2. This is because both beam 1 and beam 2 will have the same table. The way `*-common` is built
444
+ # it will look for all metrics for both beam 1 and beam 2 so if we did save that metric for beam 2 then the
445
+ # table would show up twice in the quality report. The following line avoids that.
446
+ skip_recording_constant_pars=beam != 1,
447
+ )
@@ -0,0 +1,44 @@
1
+ """Subclasses of AssembleQualityData that cause the correct polcal metrics to build."""
2
+ from typing import Type
3
+
4
+ from dkist_processing_common.models.constants import ConstantsBase
5
+ from dkist_processing_common.tasks import AssembleQualityData
6
+
7
+ __all__ = ["CIAssembleQualityData", "SPAssembleQualityData"]
8
+
9
+ from dkist_processing_cryonirsp.models.constants import CryonirspConstants
10
+ from dkist_processing_cryonirsp.tasks.instrument_polarization import generate_polcal_quality_label
11
+
12
+
13
+ class CIAssembleQualityData(AssembleQualityData):
14
+ """Subclass just so that the polcal_label_list can be populated."""
15
+
16
+ @property
17
+ def constants_model_class(self) -> Type[CryonirspConstants]:
18
+ """Grab the Cryo constants so we can have the number of beams."""
19
+ return CryonirspConstants
20
+
21
+ @property
22
+ def polcal_label_list(self) -> list[str]:
23
+ """Return label(s) for Cryo CI."""
24
+ return [
25
+ generate_polcal_quality_label(arm="CI", beam=beam)
26
+ for beam in range(1, self.constants.num_beams + 1)
27
+ ]
28
+
29
+
30
+ class SPAssembleQualityData(AssembleQualityData):
31
+ """Subclass just so that the polcal_label_list can be populated."""
32
+
33
+ @property
34
+ def constants_model_class(self) -> Type[CryonirspConstants]:
35
+ """Grab the Cryo constants so we can have the number of beams."""
36
+ return CryonirspConstants
37
+
38
+ @property
39
+ def polcal_label_list(self) -> list[str]:
40
+ """Return labels for beams 1 and 2."""
41
+ return [
42
+ generate_polcal_quality_label(arm="SP", beam=beam)
43
+ for beam in range(1, self.constants.num_beams + 1)
44
+ ]