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,299 @@
1
+ """Cryo SP science calibration task."""
2
+ from collections import defaultdict
3
+
4
+ import numpy as np
5
+ from astropy.io import fits
6
+ from dkist_processing_math.statistics import average_numpy_arrays
7
+ from dkist_service_configuration.logging import logger
8
+
9
+ from dkist_processing_cryonirsp.models.exposure_conditions import ExposureConditions
10
+ from dkist_processing_cryonirsp.models.tags import CryonirspTag
11
+ from dkist_processing_cryonirsp.parsers.cryonirsp_l0_fits_access import CryonirspL0FitsAccess
12
+ from dkist_processing_cryonirsp.tasks.science_base import CalibrationCollection
13
+ from dkist_processing_cryonirsp.tasks.science_base import ScienceCalibrationBase
14
+
15
+ __all__ = ["SPScienceCalibration"]
16
+
17
+
18
+ class SPScienceCalibration(ScienceCalibrationBase):
19
+ """Task class for SP Cryo science calibration of polarized and non-polarized data."""
20
+
21
+ def calibrate_and_write_frames(self, calibrations: CalibrationCollection):
22
+ """
23
+ Top-level method to collect frame groupings (map_scan, scan_step, etc.) and send them to be calibrated.
24
+
25
+ Then write the calibrated arrays.
26
+
27
+ This is also where the polarimetric/non-polarimetric split is made.
28
+ """
29
+ for exposure_conditions in self.constants.observe_exposure_conditions_list:
30
+ for map_scan in range(1, self.constants.num_map_scans + 1):
31
+ for scan_step in range(1, self.constants.num_scan_steps + 1):
32
+ for meas_num in range(1, self.constants.num_meas + 1):
33
+ if self.constants.correct_for_polarization:
34
+ calibrated_object = self.calibrate_polarimetric_beams(
35
+ exposure_conditions=exposure_conditions,
36
+ map_scan=map_scan,
37
+ scan_step=scan_step,
38
+ meas_num=meas_num,
39
+ calibrations=calibrations,
40
+ )
41
+ else:
42
+ calibrated_object = self.calibrate_intensity_only_beams(
43
+ exposure_conditions=exposure_conditions,
44
+ map_scan=map_scan,
45
+ scan_step=scan_step,
46
+ meas_num=meas_num,
47
+ calibrations=calibrations,
48
+ )
49
+ logging_str = f"{exposure_conditions = }, {map_scan = }, {scan_step = } and {meas_num = }"
50
+ logger.info(f"Writing calibrated array for {logging_str}")
51
+ self.write_calibrated_object(
52
+ calibrated_object,
53
+ map_scan=map_scan,
54
+ scan_step=scan_step,
55
+ meas_num=meas_num,
56
+ )
57
+
58
+ def calibrate_polarimetric_beams(
59
+ self,
60
+ *,
61
+ exposure_conditions: ExposureConditions,
62
+ map_scan: int,
63
+ scan_step: int,
64
+ meas_num: int,
65
+ calibrations: CalibrationCollection,
66
+ ) -> CryonirspL0FitsAccess:
67
+ """
68
+ Completely calibrate polarimetric science frames.
69
+
70
+ - Apply dark and gain corrections
71
+ - Demodulate
72
+ - Apply geometric correction
73
+ - Apply telescope correction
74
+ - Combine beams
75
+ """
76
+ beam_storage = dict()
77
+ header_storage = dict()
78
+ logging_str = f"{exposure_conditions = }, {map_scan = }, {scan_step = }, {meas_num = }"
79
+ for beam in range(1, self.constants.num_beams + 1):
80
+ logger.info(f"Processing polarimetric observe frames from {logging_str} and {beam = }")
81
+ intermediate_array, intermediate_header = self.correct_and_demodulate(
82
+ beam=beam,
83
+ meas_num=meas_num,
84
+ scan_step=scan_step,
85
+ map_scan=map_scan,
86
+ exposure_conditions=exposure_conditions,
87
+ calibrations=calibrations,
88
+ )
89
+
90
+ geo_corrected_array = self.apply_geometric_correction(
91
+ array=intermediate_array, beam=beam, calibrations=calibrations
92
+ )
93
+
94
+ beam_storage[CryonirspTag.beam(beam)] = geo_corrected_array
95
+ header_storage[CryonirspTag.beam(beam)] = intermediate_header
96
+
97
+ logger.info(f"Combining beams for {logging_str}")
98
+ combined = self.combine_beams_into_fits_access(beam_storage, header_storage)
99
+
100
+ logger.info(f"Correcting telescope polarization for {logging_str}")
101
+ calibrated = self.telescope_polarization_correction(combined)
102
+
103
+ return calibrated
104
+
105
+ def calibrate_intensity_only_beams(
106
+ self,
107
+ *,
108
+ exposure_conditions: ExposureConditions,
109
+ map_scan: int,
110
+ scan_step: int,
111
+ meas_num: int,
112
+ calibrations: CalibrationCollection,
113
+ ) -> CryonirspL0FitsAccess:
114
+ """
115
+ Completely calibrate non-polarimetric science frames.
116
+
117
+ - Apply all dark and gain corrections
118
+ - Apply geometric correction
119
+ - Combine beams
120
+ """
121
+ beam_storage = dict()
122
+ header_storage = dict()
123
+ for beam in range(1, self.constants.num_beams + 1):
124
+ logging_str = f"{exposure_conditions = }, {map_scan = }, {scan_step = }, {meas_num = }"
125
+ logger.info(f"Processing Stokes-I observe frames from {logging_str} and {beam = }")
126
+ intermediate_array, intermediate_header = self.apply_basic_corrections(
127
+ beam=beam,
128
+ modstate=1,
129
+ meas_num=meas_num,
130
+ scan_step=scan_step,
131
+ map_scan=map_scan,
132
+ exposure_conditions=exposure_conditions,
133
+ calibrations=calibrations,
134
+ )
135
+ intermediate_header = self.compute_date_keys(intermediate_header)
136
+
137
+ intermediate_array = self.add_stokes_dimension_to_intensity_only_array(
138
+ intermediate_array
139
+ )
140
+
141
+ geo_corrected_array = self.apply_geometric_correction(
142
+ array=intermediate_array, beam=beam, calibrations=calibrations
143
+ )
144
+
145
+ beam_storage[CryonirspTag.beam(beam)] = geo_corrected_array
146
+ header_storage[CryonirspTag.beam(beam)] = intermediate_header
147
+
148
+ logger.info(f"Combining beams for {logging_str}")
149
+ calibrated = self.combine_beams_into_fits_access(beam_storage, header_storage)
150
+
151
+ return calibrated
152
+
153
+ def apply_geometric_correction(
154
+ self, array: np.ndarray, beam: int, calibrations: CalibrationCollection
155
+ ) -> np.ndarray:
156
+ """
157
+ Apply rotation, x/y shift, and spectral shift corrections to an array.
158
+
159
+ The input array needs to have a final dimension that corresponds to Stokes parameters (even if it's only length
160
+ 1 for I-only).
161
+ """
162
+ corrected_array = np.zeros_like(array)
163
+ num_stokes = array.shape[-1]
164
+
165
+ for i in range(num_stokes):
166
+ geo_corrected_array = next(
167
+ self.corrections_correct_geometry(
168
+ array[:, :, i],
169
+ calibrations.state_offset[CryonirspTag.beam(beam)],
170
+ calibrations.angle[CryonirspTag.beam(beam)],
171
+ )
172
+ )
173
+
174
+ spectral_corrected_array = next(
175
+ self.corrections_remove_spec_shifts(
176
+ geo_corrected_array,
177
+ calibrations.spec_shift[CryonirspTag.beam(beam)],
178
+ )
179
+ )
180
+ # Insert the result into the fully corrected array stack
181
+ corrected_array[:, :, i] = spectral_corrected_array
182
+
183
+ return corrected_array
184
+
185
+ def combine_beams_into_fits_access(
186
+ self, array_dict: dict, header_dict: dict
187
+ ) -> CryonirspL0FitsAccess:
188
+ """
189
+ Average all beams together.
190
+
191
+ Also complain if the inputs are strange.
192
+ """
193
+ headers = list(header_dict.values())
194
+ if len(headers) == 0:
195
+ raise ValueError("No headers provided")
196
+ for h in headers[1:]:
197
+ if fits.HeaderDiff(headers[0], h):
198
+ raise ValueError("Headers are different! This should NEVER happen!")
199
+
200
+ if self.constants.correct_for_polarization:
201
+ avg_array = self.combine_polarimetric_beams(array_dict)
202
+ else:
203
+ avg_array = self.combine_spectrographic_beams(array_dict)
204
+
205
+ hdu = fits.ImageHDU(data=avg_array, header=headers[0])
206
+ obj = CryonirspL0FitsAccess(hdu=hdu, auto_squeeze=False)
207
+
208
+ return obj
209
+
210
+ def combine_polarimetric_beams(self, array_dict: dict[str, np.ndarray]) -> np.ndarray:
211
+ """
212
+ Combine polarimetric beams so that polarization states are normalized by the intensity state (Stokes I).
213
+
214
+ In other words:
215
+
216
+ avg_I = (beam1_I + beam2_I) / 2
217
+ avg_Q = (beam1_Q / beam1_I + beam2_Q / beam2_I) / 2. * avg_I
218
+
219
+ ...and the same for U and V
220
+ """
221
+ beam1_data = array_dict[CryonirspTag.beam(1)]
222
+ beam2_data = array_dict[CryonirspTag.beam(2)]
223
+
224
+ avg_data = np.zeros_like(beam1_data)
225
+ # Rely on the fact that the Stokes states are in order after demodulation
226
+ avg_I = (beam1_data[:, :, 0] + beam2_data[:, :, 0]) / 2.0
227
+ avg_data[:, :, 0] = avg_I
228
+
229
+ for stokes in range(1, 4):
230
+ beam1_norm = beam1_data[:, :, stokes] / beam1_data[:, :, 0]
231
+ beam2_norm = beam2_data[:, :, stokes] / beam2_data[:, :, 0]
232
+ avg_data[:, :, stokes] = avg_I * (beam1_norm + beam2_norm) / 2.0
233
+
234
+ return avg_data
235
+
236
+ def combine_spectrographic_beams(self, array_dict: dict[str, np.ndarray]) -> np.ndarray:
237
+ """Simply average the two beams together."""
238
+ array_list = []
239
+ for beam in range(1, self.constants.num_beams + 1):
240
+ array_list.append(array_dict[CryonirspTag.beam(beam)])
241
+
242
+ avg_array = average_numpy_arrays(array_list)
243
+ return avg_array
244
+
245
+ def collect_calibration_objects(self) -> CalibrationCollection:
246
+ """
247
+ Collect *all* calibration for all modstates, and exposure times.
248
+
249
+ Doing this once here prevents lots of reads as we reduce the science data.
250
+ """
251
+ dark_dict = defaultdict(dict)
252
+ solar_dict = dict()
253
+ angle_dict = dict()
254
+ state_offset_dict = dict()
255
+ spec_shift_dict = dict()
256
+ demod_dict = dict() if self.constants.correct_for_polarization else None
257
+
258
+ for beam in range(1, self.constants.num_beams + 1):
259
+ # Load the dark arrays
260
+ for exposure_conditions in self.constants.observe_exposure_conditions_list:
261
+ dark_array = self.intermediate_frame_load_dark_array(
262
+ beam=beam, exposure_conditions=exposure_conditions
263
+ )
264
+ dark_dict[CryonirspTag.beam(beam)][
265
+ CryonirspTag.exposure_conditions(exposure_conditions)
266
+ ] = dark_array
267
+
268
+ # Load the gain arrays
269
+ solar_dict[CryonirspTag.beam(beam)] = self.intermediate_frame_load_solar_gain_array(
270
+ beam=beam,
271
+ )
272
+
273
+ # Load the angle arrays
274
+ angle_dict[CryonirspTag.beam(beam)] = self.intermediate_frame_load_angle(beam=beam)
275
+
276
+ # Load the state offsets
277
+ state_offset_dict[CryonirspTag.beam(beam)] = self.intermediate_frame_load_state_offset(
278
+ beam=beam
279
+ )
280
+
281
+ # Load the spectral shifts
282
+ spec_shift_dict[CryonirspTag.beam(beam)] = self.intermediate_frame_load_spec_shift(
283
+ beam=beam
284
+ )
285
+
286
+ # Load the demod matrices
287
+ if self.constants.correct_for_polarization:
288
+ demod_dict[CryonirspTag.beam(beam)] = self.intermediate_frame_load_demod_matrices(
289
+ beam=beam
290
+ )
291
+
292
+ return CalibrationCollection(
293
+ dark=dark_dict,
294
+ solar_gain=solar_dict,
295
+ angle=angle_dict,
296
+ state_offset=state_offset_dict,
297
+ spec_shift=spec_shift_dict,
298
+ demod_matrices=demod_dict,
299
+ )