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,169 @@
1
+ """Cryonirsp CI science calibration task."""
2
+ from collections import defaultdict
3
+
4
+ from dkist_service_configuration.logging import logger
5
+
6
+ from dkist_processing_cryonirsp.models.exposure_conditions import ExposureConditions
7
+ from dkist_processing_cryonirsp.models.tags import CryonirspTag
8
+ from dkist_processing_cryonirsp.parsers.cryonirsp_l0_fits_access import CryonirspL0FitsAccess
9
+ from dkist_processing_cryonirsp.tasks.science_base import CalibrationCollection
10
+ from dkist_processing_cryonirsp.tasks.science_base import ScienceCalibrationBase
11
+
12
+ __all__ = ["CIScienceCalibration"]
13
+
14
+
15
+ class CIScienceCalibration(ScienceCalibrationBase):
16
+ """Task class for Cryonirsp CI science calibration of polarized and non-polarized data."""
17
+
18
+ CI_BEAM = 1
19
+
20
+ def calibrate_and_write_frames(self, calibrations: CalibrationCollection):
21
+ """
22
+ Completely calibrate all science frames.
23
+
24
+ - Apply dark and gain corrections
25
+ - Demodulate if needed
26
+ - Apply telescope correction, if needed
27
+ - Write calibrated arrays
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_frames(
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_frames(
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
+
50
+ logging_str = f"{exposure_conditions = }, {map_scan = }, {scan_step = } and {meas_num = }"
51
+ logger.info(f"Writing calibrated arrays for {logging_str}")
52
+ self.write_calibrated_object(
53
+ calibrated_object,
54
+ map_scan=map_scan,
55
+ scan_step=scan_step,
56
+ meas_num=meas_num,
57
+ )
58
+
59
+ def calibrate_polarimetric_frames(
60
+ self,
61
+ *,
62
+ exposure_conditions: ExposureConditions,
63
+ map_scan: int,
64
+ scan_step: int,
65
+ meas_num: int,
66
+ calibrations: CalibrationCollection,
67
+ ) -> CryonirspL0FitsAccess:
68
+ """
69
+ Completely calibrate polarimetric science frames.
70
+
71
+ - Apply dark and gain corrections
72
+ - Demodulate
73
+ - Apply telescope correction
74
+ """
75
+ logging_str = f"{exposure_conditions = }, {map_scan = }, {scan_step = } and {meas_num = }"
76
+ logger.info(f"Processing polarimetric observe frames from {logging_str}")
77
+
78
+ intermediate_array, intermediate_header = self.correct_and_demodulate(
79
+ beam=self.CI_BEAM,
80
+ meas_num=meas_num,
81
+ scan_step=scan_step,
82
+ map_scan=map_scan,
83
+ exposure_conditions=exposure_conditions,
84
+ calibrations=calibrations,
85
+ )
86
+
87
+ intermediate_object = self.wrap_array_and_header_in_fits_access(
88
+ intermediate_array, intermediate_header
89
+ )
90
+
91
+ logger.info(f"Correcting telescope polarization for {logging_str}")
92
+ calibrated_object = self.telescope_polarization_correction(intermediate_object)
93
+
94
+ return calibrated_object
95
+
96
+ def calibrate_intensity_only_frames(
97
+ self,
98
+ *,
99
+ exposure_conditions: ExposureConditions,
100
+ map_scan: int,
101
+ scan_step: int,
102
+ meas_num: int,
103
+ calibrations: CalibrationCollection,
104
+ ) -> CryonirspL0FitsAccess:
105
+ """
106
+ Completely calibrate non-polarimetric science frames.
107
+
108
+ - Apply dark and gain corrections
109
+ """
110
+ logging_str = f"{exposure_conditions = }, {map_scan = }, {scan_step = } and {meas_num = }"
111
+ logger.info(f"Processing Stokes-I observe frames from {logging_str}")
112
+ intermediate_array, intermediate_header = self.apply_basic_corrections(
113
+ beam=self.CI_BEAM,
114
+ modstate=1,
115
+ meas_num=meas_num,
116
+ scan_step=scan_step,
117
+ map_scan=map_scan,
118
+ exposure_conditions=exposure_conditions,
119
+ calibrations=calibrations,
120
+ )
121
+ intermediate_header = self.compute_date_keys(intermediate_header)
122
+
123
+ intermediate_array = self.add_stokes_dimension_to_intensity_only_array(intermediate_array)
124
+
125
+ calibrated_object = self.wrap_array_and_header_in_fits_access(
126
+ intermediate_array, intermediate_header
127
+ )
128
+
129
+ return calibrated_object
130
+
131
+ def collect_calibration_objects(self) -> CalibrationCollection:
132
+ """
133
+ Collect *all* calibration for all modstates, and exposure times.
134
+
135
+ Doing this once here prevents lots of reads as we reduce the science data.
136
+ """
137
+ dark_dict = defaultdict(dict)
138
+ solar_dict = dict()
139
+ demod_dict = dict() if self.constants.correct_for_polarization else None
140
+
141
+ beam = self.CI_BEAM
142
+ # Load the dark arrays
143
+ for exposure_conditions in self.constants.observe_exposure_conditions_list:
144
+ dark_array = self.intermediate_frame_load_dark_array(
145
+ beam=beam, exposure_conditions=exposure_conditions
146
+ )
147
+ dark_dict[CryonirspTag.beam(beam)][
148
+ CryonirspTag.exposure_conditions(exposure_conditions)
149
+ ] = dark_array
150
+
151
+ # Load the gain arrays
152
+ solar_dict[CryonirspTag.beam(beam)] = self.intermediate_frame_load_solar_gain_array(
153
+ beam=beam,
154
+ )
155
+
156
+ # Load the demod matrices
157
+ if self.constants.correct_for_polarization:
158
+ demod_dict[CryonirspTag.beam(beam)] = self.intermediate_frame_load_demod_matrices(
159
+ beam=beam
160
+ )
161
+
162
+ return CalibrationCollection(
163
+ dark=dark_dict,
164
+ angle=None,
165
+ spec_shift=None,
166
+ state_offset=None,
167
+ solar_gain=solar_dict,
168
+ demod_matrices=demod_dict,
169
+ )
@@ -0,0 +1,67 @@
1
+ """CryoNIRSP base class."""
2
+ from abc import ABC
3
+
4
+ from dkist_processing_common.tasks import WorkflowTaskBase
5
+ from dkist_processing_common.tasks.mixin.input_dataset import InputDatasetMixin
6
+ from dkist_processing_common.tasks.mixin.quality import QualityMixin
7
+
8
+ from dkist_processing_cryonirsp.models.constants import CryonirspConstants
9
+ from dkist_processing_cryonirsp.models.parameters import CryonirspParameters
10
+ from dkist_processing_cryonirsp.tasks.mixin.beam_access import BeamAccessMixin
11
+ from dkist_processing_cryonirsp.tasks.mixin.corrections import CorrectionsMixin
12
+ from dkist_processing_cryonirsp.tasks.mixin.intermediate_frame import (
13
+ IntermediateFrameMixin,
14
+ )
15
+ from dkist_processing_cryonirsp.tasks.mixin.linearized_frame import (
16
+ LinearizedFrameMixin,
17
+ )
18
+
19
+
20
+ class CryonirspTaskBase(
21
+ WorkflowTaskBase,
22
+ InputDatasetMixin,
23
+ BeamAccessMixin,
24
+ LinearizedFrameMixin,
25
+ IntermediateFrameMixin,
26
+ CorrectionsMixin,
27
+ QualityMixin,
28
+ ABC,
29
+ ):
30
+ """
31
+ Task class for base CryoNIRSP tasks that occur after linearization.
32
+
33
+ Parameters
34
+ ----------
35
+ recipe_run_id : int
36
+ id of the recipe run used to identify the workflow run this task is part of
37
+ workflow_name : str
38
+ name of the workflow to which this instance of the task belongs
39
+ workflow_version : str
40
+ version of the workflow to which this instance of the task belongs
41
+ """
42
+
43
+ # So tab completion shows all the Cryonirsp constants
44
+ constants: CryonirspConstants
45
+
46
+ @property
47
+ def constants_model_class(self):
48
+ """Get CryoNIRSP pipeline constants."""
49
+ return CryonirspConstants
50
+
51
+ def __init__(
52
+ self,
53
+ recipe_run_id: int,
54
+ workflow_name: str,
55
+ workflow_version: str,
56
+ ):
57
+ super().__init__(
58
+ recipe_run_id=recipe_run_id,
59
+ workflow_name=workflow_name,
60
+ workflow_version=workflow_version,
61
+ )
62
+ self.parameters = CryonirspParameters(
63
+ self.input_dataset_parameters,
64
+ obs_ip_start_time=self.constants.obs_ip_start_time,
65
+ wavelength=self.constants.wavelength,
66
+ arm_id=self.constants.arm_id,
67
+ )
@@ -0,0 +1,98 @@
1
+ """CryoNIRSP dark calibration task."""
2
+ from dkist_processing_common.models.task_name import TaskName
3
+ from dkist_processing_math.statistics import average_numpy_arrays
4
+ from dkist_service_configuration.logging import logger
5
+
6
+ from dkist_processing_cryonirsp.models.tags import CryonirspTag
7
+ from dkist_processing_cryonirsp.tasks.cryonirsp_base import CryonirspTaskBase
8
+
9
+ __all__ = ["DarkCalibration"]
10
+
11
+
12
+ class DarkCalibration(CryonirspTaskBase):
13
+ """Task class for calculation of the averaged dark frame for a CryoNIRSP calibration run.
14
+
15
+ Parameters
16
+ ----------
17
+ recipe_run_id : int
18
+ id of the recipe run used to identify the workflow run this task is part of
19
+ workflow_name : str
20
+ name of the workflow to which this instance of the task belongs
21
+ workflow_version : str
22
+ version of the workflow to which this instance of the task belongs
23
+
24
+ """
25
+
26
+ record_provenance = True
27
+
28
+ def run(self):
29
+ """
30
+ Run method for the task.
31
+
32
+ - Gather input dark frames
33
+ - Calculate average dark
34
+ - Write average dark
35
+ - Record quality metrics
36
+
37
+ Returns
38
+ -------
39
+ None
40
+
41
+ """
42
+ target_exposure_conditions = (
43
+ self.constants.non_dark_and_non_polcal_task_exposure_conditions_list
44
+ )
45
+
46
+ logger.info(f"{target_exposure_conditions = }")
47
+ with self.apm_task_step(
48
+ f"Calculating dark frames for {self.constants.num_beams} beams and {len(target_exposure_conditions)} exp times"
49
+ ):
50
+ total_dark_frames_used = 0
51
+ for exposure_conditions in target_exposure_conditions:
52
+ for beam in range(1, self.constants.num_beams + 1):
53
+ logger.info(
54
+ f"Gathering input dark frames for {exposure_conditions = } and {beam = }"
55
+ )
56
+ dark_tags = [
57
+ CryonirspTag.linearized(),
58
+ CryonirspTag.frame(),
59
+ CryonirspTag.task_dark(),
60
+ CryonirspTag.exposure_conditions(exposure_conditions),
61
+ ]
62
+ current_exp_dark_count = self.scratch.count_all(tags=dark_tags)
63
+ total_dark_frames_used += current_exp_dark_count
64
+ linearized_dark_arrays = self.linearized_frame_dark_array_generator(
65
+ exposure_conditions=exposure_conditions, beam=beam
66
+ )
67
+
68
+ with self.apm_processing_step(
69
+ f"Calculating dark for {exposure_conditions = } and {beam = }"
70
+ ):
71
+ averaged_dark_array = average_numpy_arrays(linearized_dark_arrays)
72
+
73
+ with self.apm_writing_step(
74
+ f"Writing dark for {exposure_conditions = } {beam = }"
75
+ ):
76
+ self.intermediate_frame_write_arrays(
77
+ averaged_dark_array,
78
+ beam=beam,
79
+ task_tag=CryonirspTag.task_dark(),
80
+ exposure_conditions=exposure_conditions,
81
+ )
82
+
83
+ with self.apm_processing_step("Computing and logging quality metrics"):
84
+ no_of_raw_dark_frames: int = self.scratch.count_all(
85
+ tags=[
86
+ CryonirspTag.linearized(),
87
+ CryonirspTag.frame(),
88
+ CryonirspTag.task_dark(),
89
+ ],
90
+ )
91
+ unused_count = int(
92
+ no_of_raw_dark_frames - (total_dark_frames_used // self.constants.num_beams)
93
+ )
94
+ self.quality_store_task_type_counts(
95
+ task_type=TaskName.dark.value,
96
+ total_frames=no_of_raw_dark_frames,
97
+ frames_not_used=unused_count,
98
+ )
@@ -0,0 +1,251 @@
1
+ """Cryo gain task."""
2
+ from abc import abstractmethod
3
+ from typing import Callable
4
+
5
+ import numpy as np
6
+ from dkist_processing_common.models.task_name import TaskName
7
+ from dkist_processing_math.arithmetic import subtract_array_from_arrays
8
+ from dkist_processing_math.statistics import average_numpy_arrays
9
+ from dkist_service_configuration.logging import logger
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.tasks.cryonirsp_base import CryonirspTaskBase
14
+
15
+ __all__ = ["LampGainCalibration", "CISolarGainCalibration"]
16
+
17
+
18
+ class GainCalibrationBase(CryonirspTaskBase):
19
+ """
20
+ Base task class for calculation of average lamp or solar gains for CI and average lamp gains for SP.
21
+
22
+ Parameters
23
+ ----------
24
+ recipe_run_id : int
25
+ id of the recipe run used to identify the workflow run this task is part of
26
+ workflow_name : str
27
+ name of the workflow to which this instance of the task belongs
28
+ workflow_version : str
29
+ version of the workflow to which this instance of the task belongs
30
+
31
+ """
32
+
33
+ @property
34
+ @abstractmethod
35
+ def gain_type(self) -> str:
36
+ """Return the gain type, SOLAR_GAIN or LAMP_GAIN."""
37
+ pass
38
+
39
+ @property
40
+ @abstractmethod
41
+ def exposure_conditions(self) -> [ExposureConditions]:
42
+ """Return the exposure conditions list."""
43
+ pass
44
+
45
+ @property
46
+ @abstractmethod
47
+ def gain_array_generator(self) -> Callable:
48
+ """Return the gain array generator to use based on the gain type."""
49
+ pass
50
+
51
+ @property
52
+ @abstractmethod
53
+ def normalize_gain_switch(self) -> bool:
54
+ """If True then the final gain image is normalized to have a mean of 1."""
55
+ pass
56
+
57
+ record_provenance = True
58
+
59
+ def run(self):
60
+ """
61
+ Execute the task.
62
+
63
+ For each exposure time and beam:
64
+ - Gather input gain and averaged dark arrays
65
+ - Calculate average array
66
+ - Normalize average array
67
+ - Write average gain array
68
+ - Record quality metrics
69
+
70
+ Returns
71
+ -------
72
+ None
73
+
74
+ """
75
+ target_exposure_conditions = self.exposure_conditions
76
+
77
+ logger.info(f"{target_exposure_conditions = }")
78
+ with self.apm_task_step(
79
+ f"Generate {self.gain_type} for {len(target_exposure_conditions)} exposure times"
80
+ ):
81
+ for exposure_conditions in target_exposure_conditions:
82
+ # NB: By using num_beams = 1 for CI, this method works for both CI and SP
83
+ for beam in range(1, self.constants.num_beams + 1):
84
+ apm_str = f"{beam = } and {exposure_conditions = }"
85
+ with self.apm_processing_step(f"Remove dark signal for {apm_str}"):
86
+ dark_array = self.intermediate_frame_load_dark_array(
87
+ beam=beam, exposure_conditions=exposure_conditions
88
+ )
89
+
90
+ avg_gain_array = self.compute_average_gain_array(
91
+ beam=beam, exposure_conditions=exposure_conditions
92
+ )
93
+
94
+ dark_corrected_gain_array = next(
95
+ subtract_array_from_arrays(avg_gain_array, dark_array)
96
+ )
97
+
98
+ with self.apm_processing_step(f"Correct bad pixels for {apm_str}"):
99
+ bad_pixel_map = self.intermediate_frame_load_bad_pixel_map(beam=beam)
100
+ bad_pixel_corrected_array = self.corrections_correct_bad_pixels(
101
+ dark_corrected_gain_array, bad_pixel_map
102
+ )
103
+
104
+ if self.normalize_gain_switch:
105
+ with self.apm_processing_step(f"Normalize final gain for {apm_str}"):
106
+ normalized_gain_array = self.normalize_gain(bad_pixel_corrected_array)
107
+ else:
108
+ normalized_gain_array = bad_pixel_corrected_array
109
+
110
+ with self.apm_writing_step(
111
+ f"Writing gain array for {beam = } and {exposure_conditions = }"
112
+ ):
113
+ self.intermediate_frame_write_arrays(
114
+ normalized_gain_array,
115
+ beam=beam,
116
+ task=self.gain_type,
117
+ )
118
+
119
+ with self.apm_processing_step("Computing and logging quality metrics"):
120
+ no_of_raw_gain_frames: int = self.scratch.count_all(
121
+ tags=[
122
+ CryonirspTag.linearized(),
123
+ CryonirspTag.frame(),
124
+ CryonirspTag.task(self.gain_type),
125
+ ],
126
+ )
127
+
128
+ self.quality_store_task_type_counts(
129
+ task_type=self.gain_type, total_frames=no_of_raw_gain_frames
130
+ )
131
+
132
+ def compute_average_gain_array(
133
+ self,
134
+ beam: int,
135
+ exposure_conditions: ExposureConditions,
136
+ ) -> np.ndarray:
137
+ """
138
+ Compute average gain array for a given exposure conditions and beam.
139
+
140
+ Parameters
141
+ ----------
142
+ beam : int
143
+ The number of the beam
144
+
145
+ exposure_conditions : float
146
+ Exposure time
147
+
148
+ Returns
149
+ -------
150
+ np.ndarray
151
+ """
152
+ linearized_gain_arrays = self.gain_array_generator(
153
+ beam=beam, exposure_conditions=exposure_conditions
154
+ )
155
+ averaged_gain_data = average_numpy_arrays(linearized_gain_arrays)
156
+ return averaged_gain_data
157
+
158
+ @staticmethod
159
+ def normalize_gain(gain_array: np.ndarray) -> np.ndarray:
160
+ """
161
+ Normalize gain to a mean of 1.
162
+
163
+ Find any residual pixels that are zero valued and set them to 1.
164
+
165
+ Parameters
166
+ ----------
167
+ gain_array : np.ndarray
168
+ Dark corrected gain array
169
+
170
+ Returns
171
+ -------
172
+ np.ndarray
173
+ Normalized dark-corrected gain array
174
+
175
+ """
176
+ avg = np.nanmean(gain_array)
177
+ gain_array /= avg
178
+
179
+ return gain_array
180
+
181
+
182
+ class LampGainCalibration(GainCalibrationBase):
183
+ """
184
+ Task class for calculation of an average lamp gain frame for a CryoNIRSP CI or SP calibration run.
185
+
186
+ Parameters
187
+ ----------
188
+ recipe_run_id : int
189
+ id of the recipe run used to identify the workflow run this task is part of
190
+ workflow_name : str
191
+ name of the workflow to which this instance of the task belongs
192
+ workflow_version : str
193
+ version of the workflow to which this instance of the task belongs
194
+
195
+ """
196
+
197
+ @property
198
+ def gain_type(self) -> str:
199
+ """Return the gain type, SOLAR_GAIN or LAMP_GAIN."""
200
+ return TaskName.lamp_gain.value
201
+
202
+ @property
203
+ def exposure_conditions(self) -> [ExposureConditions]:
204
+ """Return the exposure conditions list."""
205
+ return self.constants.lamp_gain_exposure_conditions_list
206
+
207
+ @property
208
+ def gain_array_generator(self) -> Callable:
209
+ """Return the gain array generator to use based on the gain type."""
210
+ return self.linearized_frame_lamp_gain_array_generator
211
+
212
+ @property
213
+ def normalize_gain_switch(self) -> True:
214
+ """Lamp gains should be normalized."""
215
+ return True
216
+
217
+
218
+ class CISolarGainCalibration(GainCalibrationBase):
219
+ """
220
+ Task class for calculation of an average solar gain frame for a CryoNIRSP CI calibration run.
221
+
222
+ Parameters
223
+ ----------
224
+ recipe_run_id : int
225
+ id of the recipe run used to identify the workflow run this task is part of
226
+ workflow_name : str
227
+ name of the workflow to which this instance of the task belongs
228
+ workflow_version : str
229
+ version of the workflow to which this instance of the task belongs
230
+
231
+ """
232
+
233
+ @property
234
+ def gain_type(self) -> str:
235
+ """Return the gain type, SOLAR_GAIN or LAMP_GAIN."""
236
+ return TaskName.solar_gain.value
237
+
238
+ @property
239
+ def exposure_conditions(self) -> [ExposureConditions]:
240
+ """Return the exposure conditions list."""
241
+ return self.constants.solar_gain_exposure_conditions_list
242
+
243
+ @property
244
+ def gain_array_generator(self) -> Callable:
245
+ """Return the gain array generator to use based on the gain type."""
246
+ return self.linearized_frame_solar_gain_array_generator
247
+
248
+ @property
249
+ def normalize_gain_switch(self) -> False:
250
+ """We don't want to normalize and Solar Gain images."""
251
+ return False