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,475 @@
1
+ """Cryo SP solar gain task."""
2
+ import numpy as np
3
+ import scipy.ndimage as spnd
4
+ from dkist_processing_common.models.task_name import TaskName
5
+ from dkist_processing_math.arithmetic import divide_arrays_by_array
6
+ from dkist_processing_math.arithmetic import subtract_array_from_arrays
7
+ from dkist_processing_math.statistics import average_numpy_arrays
8
+ from dkist_service_configuration.logging import logger
9
+ from scipy import signal
10
+
11
+ from dkist_processing_cryonirsp.models.exposure_conditions import ExposureConditions
12
+ from dkist_processing_cryonirsp.models.tags import CryonirspTag
13
+ from dkist_processing_cryonirsp.models.task_name import CryonirspTaskName
14
+ from dkist_processing_cryonirsp.tasks.cryonirsp_base import CryonirspTaskBase
15
+
16
+ __all__ = ["SPSolarGainCalibration"]
17
+
18
+
19
+ class SPSolarGainCalibration(CryonirspTaskBase):
20
+ """Task class for generating Solar Gain images for each beam.
21
+
22
+ NB: This class does not extend GainCalibrationBase, because it is highly customized
23
+ and incorporates several correction steps as well as solar spectrum removal.
24
+
25
+ Parameters
26
+ ----------
27
+ recipe_run_id : int
28
+ id of the recipe run used to identify the workflow run this task is part of
29
+ workflow_name : str
30
+ name of the workflow to which this instance of the task belongs
31
+ workflow_version : str
32
+ version of the workflow to which this instance of the task belongs
33
+
34
+ """
35
+
36
+ record_provenance = True
37
+
38
+ def run(self):
39
+ """
40
+ For each beam.
41
+
42
+ - Do dark, lamp, and geometric corrections
43
+ - Compute the characteristic spectra
44
+ - Re-apply the spectral curvature to the characteristic spectra
45
+ - Re-apply angle and state offset distortions to the characteristic spectra
46
+ - Remove the distorted characteristic solar spectra from the original spectra
47
+ - Write master solar gain
48
+
49
+ Returns
50
+ -------
51
+ None
52
+
53
+ """
54
+ target_exposure_conditions = self.constants.solar_gain_exposure_conditions_list
55
+
56
+ with self.apm_step(f"Computing SP gain calibrations for {target_exposure_conditions=}"):
57
+ for exposure_conditions in target_exposure_conditions:
58
+ for beam in range(1, self.constants.num_beams + 1):
59
+ with self.apm_processing_step(
60
+ f"Perform initial corrections for {beam = } and {exposure_conditions = }"
61
+ ):
62
+ spectral_corrected_solar_array = self.do_initial_corrections(
63
+ beam=beam, exposure_conditions=exposure_conditions
64
+ )
65
+
66
+ with self.apm_processing_step(
67
+ f"Compute the characteristic spectrum for {beam = } and {exposure_conditions = }"
68
+ ):
69
+ char_spectrum = self.compute_char_spectrum(
70
+ array=spectral_corrected_solar_array, beam=beam
71
+ )
72
+
73
+ with self.apm_processing_step(
74
+ f"Re-apply the spectral and geometric distortions for {beam = } and {exposure_conditions = }"
75
+ ):
76
+ distorted_char_spectrum = self.distort_char_spectrum(char_spectrum)
77
+
78
+ with self.apm_processing_step(
79
+ f"Remove the solar spectrum for {beam = } and {exposure_conditions = }"
80
+ ):
81
+ # This is the final gain image, as we do not normalize
82
+ final_gain = self.remove_solar_signal(
83
+ char_solar_spectra=distorted_char_spectrum,
84
+ beam=beam,
85
+ exposure_conditions=exposure_conditions,
86
+ )
87
+
88
+ if self.parameters.fringe_correction_on:
89
+ with self.apm_processing_step(
90
+ f"Computing final solar gain based on fringe-corrected flux-scaled lamp gain for {beam = } and {exposure_conditions = }"
91
+ ):
92
+ # Compute a solar gain based on a fringe-corrected lamp gain
93
+ final_gain = self.compute_fringe_corrected_gain(
94
+ beam, exposure_conditions
95
+ )
96
+
97
+ with self.apm_writing_step(
98
+ f"Writing the final solar gain array for {beam = } and {exposure_conditions = }"
99
+ ):
100
+ self.write_solar_gain_calibration(
101
+ gain_array=final_gain,
102
+ beam=beam,
103
+ )
104
+
105
+ with self.apm_processing_step("Computing and logging quality metrics"):
106
+ no_of_raw_solar_frames: int = self.scratch.count_all(
107
+ tags=[
108
+ CryonirspTag.linearized(),
109
+ CryonirspTag.frame(),
110
+ CryonirspTag.task_solar_gain(),
111
+ ],
112
+ )
113
+ self.quality_store_task_type_counts(
114
+ task_type=TaskName.solar_gain.value, total_frames=no_of_raw_solar_frames
115
+ )
116
+
117
+ def do_initial_corrections(
118
+ self, beam: int, exposure_conditions: ExposureConditions
119
+ ) -> np.ndarray:
120
+ """
121
+ Perform dark, bad pixel, and lamp corrections on the input solar gain data.
122
+
123
+ Parameters
124
+ ----------
125
+ beam
126
+ The beam number
127
+
128
+ exposure_conditions
129
+ The exposure conditions
130
+
131
+ Returns
132
+ -------
133
+ A solar array with basic and geometric corrections
134
+ """
135
+ # Do the basic dark and bad pixel corrections
136
+ basic_corrected_solar_array = self.do_dark_and_bad_pixel_corrections(
137
+ beam, exposure_conditions
138
+ )
139
+ # Save as intermediate result for final gain computation
140
+ self.intermediate_frame_write_arrays(
141
+ arrays=basic_corrected_solar_array,
142
+ beam=beam,
143
+ task="SC_DARK_BP_CORRECTED_ONLY",
144
+ )
145
+ # Gain correct using the lamp gain. This removes internal optical effects.
146
+ lamp_array = self.intermediate_frame_load_lamp_gain_array(beam=beam)
147
+ lamp_corrected_solar_array = next(
148
+ divide_arrays_by_array(basic_corrected_solar_array, lamp_array)
149
+ )
150
+ # Do the rotation and spectral corrections
151
+ spectral_corrected_solar_array = self.do_geometric_corrections(
152
+ lamp_corrected_solar_array, beam
153
+ )
154
+ # Save as an intermediate result for science users
155
+ self.intermediate_frame_write_arrays(
156
+ arrays=spectral_corrected_solar_array,
157
+ beam=beam,
158
+ task=CryonirspTaskName.spectral_corrected_solar_array.value,
159
+ )
160
+ return spectral_corrected_solar_array
161
+
162
+ def do_dark_and_bad_pixel_corrections(
163
+ self, beam: int, exposure_conditions: ExposureConditions
164
+ ) -> np.ndarray:
165
+ """
166
+ Perform dark and bad pixel corrections on the input solar gain data.
167
+
168
+ Parameters
169
+ ----------
170
+ beam
171
+ The beam number
172
+
173
+ exposure_conditions
174
+ The exposure conditions
175
+
176
+ Returns
177
+ -------
178
+ A solar array with dark and bad pixel corrections
179
+ """
180
+ # Load the necessary files
181
+ dark_array = self.intermediate_frame_load_dark_array(
182
+ beam=beam, exposure_conditions=exposure_conditions
183
+ )
184
+ # Compute the avg solar array
185
+ linearized_solar_arrays = self.linearized_frame_gain_array_generator(
186
+ beam=beam, exposure_conditions=exposure_conditions, gain_type=TaskName.solar_gain.value
187
+ )
188
+ avg_solar_array = average_numpy_arrays(linearized_solar_arrays)
189
+ # Dark correct it
190
+ dark_corrected_solar_array = next(subtract_array_from_arrays(avg_solar_array, dark_array))
191
+ # Correct for bad pixels
192
+ bad_pixel_map = self.intermediate_frame_load_bad_pixel_map(beam=beam)
193
+ bad_pixel_corrected_solar_array = self.corrections_correct_bad_pixels(
194
+ dark_corrected_solar_array, bad_pixel_map
195
+ )
196
+ return bad_pixel_corrected_solar_array
197
+
198
+ def do_geometric_corrections(self, lamp_corrected_array: np.ndarray, beam: int) -> np.ndarray:
199
+ """
200
+ Perform geometric corrections on the input solar gain data.
201
+
202
+ Parameters
203
+ ----------
204
+ lamp_corrected_array
205
+ A solar array that has had dark, bad pixel and lamp gain corrections
206
+
207
+ beam
208
+ The beam number
209
+
210
+ Returns
211
+ -------
212
+ An array that has geometric and spectral corrections
213
+ """
214
+ # Get the parameters and save them to self for use later on...
215
+ self.angle = self.intermediate_frame_load_angle(beam=beam)
216
+ self.state_offset = self.intermediate_frame_load_state_offset(beam=beam)
217
+ self.spec_shift = self.intermediate_frame_load_spec_shift(beam=beam)
218
+ # Correct for rotation and state offset. This does not correct for spectral curvature!
219
+ geo_corrected_solar_array = next(
220
+ self.corrections_correct_geometry(lamp_corrected_array, self.state_offset, self.angle)
221
+ )
222
+ # Remove the spectral curvature
223
+ spectral_corrected_solar_array = next(
224
+ self.corrections_remove_spec_shifts(geo_corrected_solar_array, self.spec_shift)
225
+ )
226
+ return spectral_corrected_solar_array
227
+
228
+ def compute_char_spectrum(self, array: np.ndarray, beam: int) -> np.ndarray:
229
+ """
230
+ Estimate the characteristic solar spectrum from the corrected solar gain data.
231
+
232
+ Parameters
233
+ ----------
234
+ array
235
+ A corrected solar array image
236
+
237
+ Returns
238
+ -------
239
+ A 2D array with the estimate of the characteristic spectrum
240
+ """
241
+ # Normalize data row by row
242
+ pct = self.parameters.solar_characteristic_spatial_normalization_percentile
243
+ array_row_norm = array / np.nanpercentile(array, pct, axis=1)[:, None]
244
+ # Compute characteristic spectrum
245
+ char_spec_1d = np.nanmedian(array_row_norm, axis=0)
246
+ # Expand the 1D median along the columns (along the slit)
247
+ median_char_spec_2d = np.tile(char_spec_1d, (array_row_norm.shape[0], 1))
248
+ self.intermediate_frame_write_arrays(
249
+ arrays=char_spec_1d, task_tag=CryonirspTag.task_characteristic_spectra(), beam=beam
250
+ )
251
+ return median_char_spec_2d
252
+
253
+ def distort_char_spectrum(self, char_spec: np.ndarray) -> np.ndarray:
254
+ """
255
+ Re-apply the geometric distortions, that were previously removed, to the characteristic spectrum.
256
+
257
+ Parameters
258
+ ----------
259
+ char_spec
260
+ The characteristic spectrum
261
+
262
+ Returns
263
+ -------
264
+ The characteristic spectrum with spectral curvature distortion applied
265
+ """
266
+ # Re-distort the characteristic spectrum in the reverse order from the earlier correction
267
+ # 1. Add spectral curvature back
268
+ reshifted_spectrum = next(
269
+ self.corrections_remove_spec_shifts(arrays=char_spec, spec_shift=-self.spec_shift)
270
+ )
271
+ # 2. Add state offset and angular rotation back
272
+ distorted_spectrum = next(
273
+ self.corrections_distort_geometry(
274
+ reshifted_spectrum,
275
+ -self.state_offset,
276
+ -self.angle,
277
+ )
278
+ )
279
+ return distorted_spectrum
280
+
281
+ def geo_corrected_data(self, beam: int, exposure_conditions: ExposureConditions) -> np.ndarray:
282
+ """
283
+ Read the intermediate dark and bad-pixel corrected solar data saved previously.
284
+
285
+ Parameters
286
+ ----------
287
+ beam
288
+ The beam number
289
+
290
+ exposure_conditions
291
+ The exposure conditions
292
+
293
+ Returns
294
+ -------
295
+ A dark and bad pixel corrected solar array
296
+ """
297
+ array_generator = self.intermediate_frame_load_intermediate_arrays(
298
+ tags=[
299
+ CryonirspTag.task("SC_DARK_BP_CORRECTED_ONLY"),
300
+ CryonirspTag.beam(beam),
301
+ ]
302
+ )
303
+ return next(array_generator)
304
+
305
+ def remove_solar_signal(
306
+ self,
307
+ char_solar_spectra: np.ndarray,
308
+ beam: int,
309
+ exposure_conditions: ExposureConditions,
310
+ ) -> np.ndarray:
311
+ """
312
+ Remove the (distorted) characteristic solar spectra from the input solar data.
313
+
314
+ Parameters
315
+ ----------
316
+ char_solar_spectra
317
+ The characteristic spectrum
318
+
319
+ beam
320
+ The beam number
321
+
322
+ exposure_conditions
323
+ The exposure conditions
324
+
325
+ Returns
326
+ -------
327
+ A geometric and spectrally corrected array with the solar signal removed
328
+ """
329
+ logger.info(
330
+ f"Removing characteristic solar spectra from {beam=} and {exposure_conditions =}"
331
+ )
332
+ input_gain = self.geo_corrected_data(beam=beam, exposure_conditions=exposure_conditions)
333
+ array_with_solar_signal_removed = input_gain / char_solar_spectra
334
+ return array_with_solar_signal_removed
335
+
336
+ def write_solar_gain_calibration(self, gain_array: np.ndarray, beam: int) -> None:
337
+ """
338
+ Write the final gain array as a file.
339
+
340
+ Parameters
341
+ ----------
342
+ gain_array
343
+ The final gain array
344
+
345
+ beam
346
+ The beam number
347
+
348
+ Returns
349
+ -------
350
+ None
351
+ """
352
+ logger.info(f"Writing final SolarGain for {beam=}")
353
+ self.intermediate_frame_write_arrays(
354
+ arrays=gain_array,
355
+ beam=beam,
356
+ task_tag=CryonirspTag.task_solar_gain(),
357
+ )
358
+
359
+ def compute_fringe_corrected_gain(self, beam: int, exposure_conditions: float) -> np.ndarray:
360
+ """
361
+ Compute a solar gain based on a scaled and fringe-removed lamp gain.
362
+
363
+ Parameters
364
+ ----------
365
+ beam
366
+ The beam number
367
+
368
+ exposure_conditions
369
+ The exposure conditions
370
+
371
+ Returns
372
+ -------
373
+ A lamp gain array that has been scaled to the average solar flux and has been fringe-corrected
374
+ """
375
+ apm_str = f"{beam = } and {exposure_conditions = }"
376
+
377
+ with self.apm_processing_step(f"Perform initial corrections for {apm_str}"):
378
+ corrected_solar_array = self.do_dark_and_bad_pixel_corrections(
379
+ beam=beam, exposure_conditions=exposure_conditions
380
+ )
381
+
382
+ with self.apm_processing_step(f"Compute the flux-scaled lamp gain for {apm_str}"):
383
+ scaled_lamp_array = self.compute_flux_scaled_lamp_gain(corrected_solar_array, beam)
384
+
385
+ with self.apm_processing_step(f"Apply spectral filtering for {apm_str}"):
386
+ filtered_lamp_array = self.apply_spectral_and_spatial_filtering(scaled_lamp_array)
387
+
388
+ with self.apm_processing_step(f"Isolate and remove fringes for {apm_str}"):
389
+ final_gain_array = self.isolate_and_remove_fringes(
390
+ filtered_lamp_array, scaled_lamp_array
391
+ )
392
+
393
+ return final_gain_array
394
+
395
+ def compute_flux_scaled_lamp_gain(
396
+ self, corrected_solar_array: np.ndarray, beam: int
397
+ ) -> np.ndarray:
398
+ """
399
+ Scale the lamp gain image to match the flux of the average corrected solar array.
400
+
401
+ The average corrected solar array is gain corrected using the lamp gain image.
402
+ The flux ratio of the average corrected solar array relative to the lamp gain is computed
403
+ as the median of the lamp-gain-corrected solar array along the spectral axis.
404
+ The lamp gain is then scaled on a column by column basis by the flux ratio to yield
405
+ a gain image that is similar the average corrected solar array.
406
+
407
+ Parameters
408
+ ----------
409
+ corrected_solar_array
410
+ The dark and bad pixel corrected average solar array
411
+
412
+ beam
413
+ The beam number
414
+
415
+ Returns
416
+ -------
417
+ The scaled lamp array
418
+ """
419
+ lamp_array = self.intermediate_frame_load_lamp_gain_array(beam=beam)
420
+ lamp_corrected_solar_array = next(divide_arrays_by_array(corrected_solar_array, lamp_array))
421
+ flux_ratio = np.nanmedian(lamp_corrected_solar_array, axis=1)
422
+ scaled_lamp_array = lamp_array * flux_ratio[:, None]
423
+ return scaled_lamp_array
424
+
425
+ def apply_spectral_and_spatial_filtering(self, scaled_lamp_array: np.ndarray) -> np.ndarray:
426
+ """
427
+ Apply spectral and spatial filtering to the scaled lamp array.
428
+
429
+ Parameters
430
+ ----------
431
+ scaled_lamp_array
432
+ The input scaled lamp array
433
+
434
+ Returns
435
+ -------
436
+ The filtered lamp array
437
+ """
438
+ spectral_filter_size = self.parameters.fringe_correction_spectral_filter_size
439
+ spatial_filter_size = self.parameters.fringe_correction_spatial_filter_size
440
+ spectral_filtered_lamp_array = spnd.gaussian_filter(scaled_lamp_array, spectral_filter_size)
441
+ partial_filtered_array = scaled_lamp_array / spectral_filtered_lamp_array
442
+ spatial_filtered_lamp_gain = spnd.gaussian_filter(
443
+ partial_filtered_array, spatial_filter_size
444
+ )
445
+ return spatial_filtered_lamp_gain
446
+
447
+ def isolate_and_remove_fringes(
448
+ self, filtered_lamp_array: np.ndarray, scaled_lamp_array: np.ndarray
449
+ ) -> np.ndarray:
450
+ """
451
+ Use a low pass filter to estimate the fringes and then remove them from the scaled lamp array.
452
+
453
+ Parameters
454
+ ----------
455
+ filtered_lamp_array
456
+ The filtered lamp gain array
457
+
458
+ scaled_lamp_array
459
+ The scaled lamp gain array
460
+
461
+ Returns
462
+ -------
463
+ The scaled lamp array with fringes removed.
464
+ """
465
+ # The fringe cutoff is specified as a period so we invert it to get the lowpass cutoff frequency
466
+ cutoff_freq = 1.0 / self.parameters.fringe_correction_lowpass_cutoff_period
467
+ # Compute the Butterworth lowpass filter coefficients
468
+ numerator, denominator = signal.butter(2, cutoff_freq, btype="lowpass", fs=1)
469
+ # Apply the lowpass Butterworth filter and use Gustafsson's method to better preserve the array edges
470
+ low_pass_filtered_array = signal.filtfilt(
471
+ numerator, denominator, filtered_lamp_array, axis=1, method="gust"
472
+ )
473
+ fringe_estimate = filtered_lamp_array / low_pass_filtered_array
474
+ fringe_removed_array = scaled_lamp_array / fringe_estimate
475
+ return fringe_removed_array
@@ -0,0 +1,61 @@
1
+ """Tasks for transferring scratch data to custom location for post-pipeline analysis."""
2
+ from dkist_processing_common.models.task_name import TaskName
3
+ from dkist_processing_common.tasks.mixin.globus import GlobusTransferItem
4
+ from dkist_processing_common.tasks.trial_output_data import TransferTrialDataBase
5
+
6
+ __all__ = ["TransferCryoTrialData"]
7
+
8
+ from dkist_processing_cryonirsp.models.task_name import CryonirspTaskName
9
+
10
+
11
+ class TransferCryoTrialData(TransferTrialDataBase):
12
+ """Transfer DEBUG, Intermediate, and/or output data to the trial location."""
13
+
14
+ @property
15
+ def intermediate_task_names(self) -> list[str]:
16
+ """Grab all the Calibration products used to calibrate science data."""
17
+ return [
18
+ TaskName.dark.value,
19
+ TaskName.lamp_gain.value,
20
+ TaskName.geometric_angle.value,
21
+ TaskName.geometric_offsets.value,
22
+ TaskName.geometric_spectral_shifts.value,
23
+ TaskName.solar_gain.value,
24
+ TaskName.demodulation_matrices.value,
25
+ CryonirspTaskName.spectral_corrected_solar_array.value,
26
+ ]
27
+
28
+ def build_transfer_list(self) -> list[GlobusTransferItem]:
29
+ """
30
+ Build a list containing all files we want to transfer to the trial environment.
31
+
32
+ The classes of/specific files to transfer are defined in the switches that look at the recipe run configuration.
33
+ """
34
+ transfer_list = []
35
+
36
+ if self.debug_frame_switch:
37
+ transfer_list += self.build_debug_frame_transfer_list()
38
+
39
+ if self.intermediate_frame_switch:
40
+ transfer_list += self.build_intermediate_frame_transfer_list()
41
+
42
+ if self.output_frame_switch:
43
+ transfer_list += self.build_output_frame_transfer_list()
44
+ transfer_list += self.build_output_movie_transfer_list()
45
+
46
+ if self.specific_frame_tag_lists:
47
+ transfer_list += self.build_transfer_list_from_tag_lists(self.specific_frame_tag_lists)
48
+
49
+ if self.output_dataset_inventory_switch:
50
+ transfer_list += self.build_output_dataset_inventory_transfer_list()
51
+
52
+ if self.output_asdf_switch:
53
+ transfer_list += self.build_output_asdf_transfer_list()
54
+
55
+ if self.output_quality_report_switch:
56
+ transfer_list += self.build_output_quality_report_transfer_list()
57
+
58
+ if self.output_quality_data_switch:
59
+ transfer_list += self.build_output_quality_data_transfer_list()
60
+
61
+ return transfer_list