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,202 @@
1
+ """CryoNIRSP-specific assemble movie task subclass."""
2
+ import numpy as np
3
+ from dkist_processing_common.codecs.fits import fits_array_decoder
4
+ from dkist_processing_common.tasks import AssembleMovie
5
+ from dkist_service_configuration.logging import logger
6
+ from PIL import ImageDraw
7
+ from PIL.ImageFont import FreeTypeFont
8
+
9
+ from dkist_processing_cryonirsp.models.constants import CryonirspConstants
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.parsers.cryonirsp_l1_fits_access import CryonirspL1FitsAccess
13
+
14
+ __all__ = ["AssembleCryonirspMovie", "SPAssembleCryonirspMovie"]
15
+
16
+
17
+ class AssembleCryonirspMovieBase(AssembleMovie):
18
+ """
19
+ Assemble all CryoNIRSP movie frames (tagged with CryonirspTag.movie_frame()) into an mp4 movie file.
20
+
21
+ Subclassed from the AssembleMovie task in dkist_processing_common to add CryoNIRSP specific text overlays.
22
+
23
+
24
+ Parameters
25
+ ----------
26
+ recipe_run_id : int
27
+ id of the recipe run used to identify the workflow run this task is part of
28
+ workflow_name : str
29
+ name of the workflow to which this instance of the task belongs
30
+ workflow_version : str
31
+ version of the workflow to which this instance of the task belongs
32
+ """
33
+
34
+ MPL_COLOR_MAP = "gray"
35
+
36
+ def compute_frame_shape(self) -> tuple[int, int]:
37
+ """Dynamically set the dimensions of the movie based on L1 file shape."""
38
+ movie_frame_arrays = self.read(
39
+ tags=[CryonirspTag.movie_frame()], decoder=fits_array_decoder
40
+ )
41
+ random_frame = next(movie_frame_arrays)
42
+ raw_L1_shape = random_frame.shape
43
+ flipped_shape = raw_L1_shape[::-1]
44
+
45
+ standard_HD_num_pix = 1920 * 1080
46
+ frame_num_pix = np.prod(flipped_shape)
47
+ scale_factor = (
48
+ np.sqrt(standard_HD_num_pix / frame_num_pix)
49
+ if frame_num_pix > standard_HD_num_pix
50
+ else 1.0
51
+ )
52
+ scaled_shape = tuple(int(i * scale_factor) for i in flipped_shape)
53
+
54
+ return scaled_shape
55
+
56
+ def pre_run(self) -> None:
57
+ """Set the movie frame shape prior to running."""
58
+ super().pre_run()
59
+ frame_shape = self.compute_frame_shape()
60
+ logger.info(f"Setting movie shape to {frame_shape}")
61
+ self.MOVIE_FRAME_SHAPE = frame_shape
62
+
63
+ @property
64
+ def constants_model_class(self):
65
+ """Get CryoNIRSP constants."""
66
+ return CryonirspConstants
67
+
68
+ @property
69
+ def fits_parsing_class(self):
70
+ """Cryonirsp specific subclass of L1FitsAccess to use for reading images."""
71
+ return CryonirspL1FitsAccess
72
+
73
+ def write_overlay(self, draw: ImageDraw, fits_obj: CryonirspL0FitsAccess) -> None:
74
+ """
75
+ Mark each image with its instrument, observed wavelength, and observation time.
76
+
77
+ Parameters
78
+ ----------
79
+ draw
80
+ A PIL.ImageDraw object
81
+
82
+ fits_obj
83
+ A single movie "image", i.e., a single array tagged with CryonirspTag.movie_frame
84
+ """
85
+ self.write_line(
86
+ draw=draw,
87
+ text=f"INSTRUMENT: {self.constants.instrument}",
88
+ line=3,
89
+ column="right",
90
+ font=self.font_36,
91
+ )
92
+ self.write_line(
93
+ draw=draw,
94
+ text=f"WAVELENGTH: {fits_obj.wavelength} nm",
95
+ line=2,
96
+ column="right",
97
+ font=self.font_36,
98
+ )
99
+ self.write_line(
100
+ draw=draw,
101
+ text=f"OBS TIME: {fits_obj.time_obs}",
102
+ line=1,
103
+ column="right",
104
+ font=self.font_36,
105
+ )
106
+
107
+ if self.constants.correct_for_polarization:
108
+ self.write_line(draw=draw, text="Stokes-I", line=4, column="right", font=self.font_36)
109
+
110
+ def get_middle_line(self, draw: ImageDraw, text: str, font: FreeTypeFont) -> int:
111
+ """
112
+ Get the line number for the middle of the frame.
113
+
114
+ We need to compute this in real time because the frame size is dynamically based on the L1 file shape.
115
+ """
116
+ _, _, _, text_height = draw.textbbox(xy=(0, 0), text=text, font=font)
117
+ # See `write_line` in `dkist-processing-common` for why this is the expression.
118
+ line = (self.MOVIE_FRAME_SHAPE[1] // 2) / (self.TEXT_MARGIN_PX + text_height)
119
+ return line
120
+
121
+
122
+ # See note below on `SPAssembleCryonirspMovie`
123
+ class AssembleCryonirspMovie(AssembleCryonirspMovieBase):
124
+ """
125
+ Assemble all CryoNIRSP CI movie frames (tagged with CryonirspTag.movie_frame()) into an mp4 movie file.
126
+
127
+ Subclassed from the AssembleMovie task in dkist_processing_common to add CryoNIRSP specific text overlays.
128
+
129
+
130
+ Parameters
131
+ ----------
132
+ recipe_run_id : int
133
+ id of the recipe run used to identify the workflow run this task is part of
134
+ workflow_name : str
135
+ name of the workflow to which this instance of the task belongs
136
+ workflow_version : str
137
+ version of the workflow to which this instance of the task belongs
138
+
139
+
140
+ """
141
+
142
+ @property
143
+ def num_images(self) -> int:
144
+ """Total number of images in final movie.
145
+
146
+ Overloaded from `dkist-processing-common` because DSPS repeat does not correspond to map scan in CryoNIRSP
147
+ """
148
+ return self.constants.num_map_scans * self.constants.num_scan_steps
149
+
150
+ def tags_for_image_n(self, n: int) -> list[str]:
151
+ """Return tags that grab the n'th movie image.
152
+
153
+ Overloaded from `dkist-processing-common` because DSPS repeat does not correspond to map scan in CryoNIRSP
154
+ """
155
+ map_scan_num = n // self.constants.num_scan_steps + 1
156
+ scan_step = n % self.constants.num_scan_steps + 1
157
+
158
+ tags = [
159
+ CryonirspTag.map_scan(map_scan_num),
160
+ CryonirspTag.scan_step(scan_step),
161
+ ]
162
+ logger.info(f"AssembleMovie.tags_for_image_n: {tags =}")
163
+ return tags
164
+
165
+
166
+ # NOTE:
167
+ # This task isn't used right now because the SP movies need some better handling of the wavelength dimension.
168
+ # For the time being both arms use the `MakeCryonirspMovieFrames` task above.
169
+ # See PR #91 for more information.
170
+ class SPAssembleCryonirspMovie(AssembleCryonirspMovieBase):
171
+ """
172
+ Assemble all CryoNIRSP SP movie frames (tagged with CryonirspTag.movie_frame()) into an mp4 movie file.
173
+
174
+ Subclassed from the AssembleMovie task in dkist_processing_common to add CryoNIRSP specific text overlays.
175
+
176
+
177
+ Parameters
178
+ ----------
179
+ recipe_run_id : int
180
+ id of the recipe run used to identify the workflow run this task is part of
181
+ workflow_name : str
182
+ name of the workflow to which this instance of the task belongs
183
+ workflow_version : str
184
+ version of the workflow to which this instance of the task belongs
185
+
186
+
187
+ """
188
+
189
+ @property
190
+ def num_images(self) -> int:
191
+ """Total number of images in final movie.
192
+
193
+ Overloaded from `dkist-processing-common` because DSPS repeat does not correspond to map scan in CryoNIRSP
194
+ """
195
+ return self.constants.num_map_scans
196
+
197
+ def tags_for_image_n(self, n: int) -> list[str]:
198
+ """Return tags that grab the n'th movie image.
199
+
200
+ Overloaded from `dkist-processing-common` because DSPS repeat does not correspond to map scan in CryoNIRSP
201
+ """
202
+ return [CryonirspTag.map_scan(n + 1)]
@@ -0,0 +1,96 @@
1
+ """Task class for bad pixel map computation."""
2
+ import numpy as np
3
+ import scipy.ndimage as spnd
4
+ from dkist_processing_math.statistics import average_numpy_arrays
5
+ from dkist_service_configuration.logging import logger
6
+
7
+ from dkist_processing_cryonirsp.models.tags import CryonirspTag
8
+ from dkist_processing_cryonirsp.models.task_name import CryonirspTaskName
9
+ from dkist_processing_cryonirsp.tasks.cryonirsp_base import CryonirspTaskBase
10
+
11
+ __all__ = ["BadPixelMapCalibration"]
12
+
13
+
14
+ class BadPixelMapCalibration(CryonirspTaskBase):
15
+ """
16
+ Task class for calculation of the bad pixel map for later use during calibration.
17
+
18
+ Parameters
19
+ ----------
20
+ recipe_run_id : int
21
+ id of the recipe run used to identify the workflow run this task is part of
22
+ workflow_name : str
23
+ name of the workflow to which this instance of the task belongs
24
+ workflow_version : str
25
+ version of the workflow to which this instance of the task belongs
26
+
27
+ """
28
+
29
+ record_provenance = True
30
+
31
+ def run(self):
32
+ """
33
+ Compute the bad pixel map by analyzing a set of solar gain images.
34
+
35
+ Steps:
36
+ 1. Compute the average gain image
37
+ 2. Smooth the array with a median filter
38
+ 3. Calculate the difference between the smoothed and input arrays
39
+ 4. Threshold the difference array on both ends to determine good and bad pixels
40
+ 5. Save the bad pixel map as a fits file
41
+
42
+ Returns
43
+ -------
44
+ None
45
+
46
+ """
47
+ with self.apm_task_step(f"Compute average uncorrected solar gain image"):
48
+ average_solar_gain_array = self.compute_average_gain_array()
49
+
50
+ with self.apm_task_step(f"Compute the bad pixel map"):
51
+ with self.apm_processing_step("Smooth array with median filter"):
52
+ filter_size = self.parameters.bad_pixel_map_median_filter_size
53
+ filtered_array = spnd.median_filter(
54
+ average_solar_gain_array,
55
+ size=filter_size,
56
+ mode="constant",
57
+ cval=np.nanmedian(average_solar_gain_array),
58
+ )
59
+
60
+ with self.apm_processing_step("Identify bad pixels"):
61
+ thresh = self.parameters.bad_pixel_map_threshold_factor
62
+
63
+ diff = filtered_array - average_solar_gain_array
64
+ bad_pixel_map = np.array((np.abs(diff) > thresh * diff.std()), dtype=int)
65
+
66
+ # Find and fix any residual zeros that slipped through the bad pixel map.
67
+ zeros = np.where(average_solar_gain_array == 0.0)
68
+ bad_pixel_map[zeros] = 1
69
+
70
+ with self.apm_writing_step("Writing bad pixel map"):
71
+ self.intermediate_frame_write_arrays(
72
+ bad_pixel_map, task=CryonirspTaskName.bad_pixel_map.value
73
+ )
74
+
75
+ def compute_average_gain_array(self) -> np.ndarray:
76
+ """
77
+ Compute an average of uncorrected solar gain arrays.
78
+
79
+ We are computing the overall illumination pattern for one (CI) or both (SP) beams
80
+ simultaneously, so no dark correction is required and no beam splitting is used at
81
+ this point. Solar gain images are used because of their higher flux levels and they
82
+ more accurately represent the illuminated beam seen in solar images.
83
+
84
+ Returns
85
+ -------
86
+ The average gain array
87
+ """
88
+ lin_corr_gain_arrays = self.linearized_frame_full_array_generator(
89
+ tags=[
90
+ CryonirspTag.task_solar_gain(),
91
+ CryonirspTag.linearized(),
92
+ CryonirspTag.frame(),
93
+ ]
94
+ )
95
+ averaged_gain_data = average_numpy_arrays(lin_corr_gain_arrays)
96
+ return averaged_gain_data
@@ -0,0 +1,279 @@
1
+ """CryoNIRSP compute beam boundary task."""
2
+ from abc import abstractmethod
3
+ from dataclasses import dataclass
4
+
5
+ import numpy as np
6
+ from dkist_processing_math.statistics import average_numpy_arrays
7
+ from dkist_service_configuration.logging import logger
8
+ from largestinteriorrectangle import lir
9
+ from skimage import filters
10
+ from skimage.exposure import rescale_intensity
11
+ from skimage.morphology import disk
12
+ from skimage.util import img_as_ubyte
13
+
14
+ from dkist_processing_cryonirsp.models.tags import CryonirspTag
15
+ from dkist_processing_cryonirsp.tasks.cryonirsp_base import CryonirspTaskBase
16
+
17
+ __all__ = ["BeamBoundariesCalibrationBase", "BeamBoundary"]
18
+
19
+
20
+ @dataclass
21
+ class BeamBoundary:
22
+ """Simple dataclass to hold boundary information for the illuminated portion of the beam array."""
23
+
24
+ y_min: int
25
+ y_max: int
26
+ x_min: int
27
+ x_max: int
28
+
29
+ @property
30
+ def y_slice(self):
31
+ """Return a slice object representing the illumination along the y-axis (numpy 0 axis)."""
32
+ return slice(self.y_min, self.y_max)
33
+
34
+ @property
35
+ def x_slice(self):
36
+ """Return a slice object representing the illumination along the x-axis (numpy 1 axis)."""
37
+ return slice(self.x_min, self.x_max)
38
+
39
+ @property
40
+ def beam_boundaries(self):
41
+ """Return a tuple containing the beam boundaries."""
42
+ return self.y_min, self.y_max, self.x_min, self.x_max
43
+
44
+ @property
45
+ def beam_boundaries_array(self):
46
+ """Return a tuple containing the beam boundaries."""
47
+ return np.array(self.beam_boundaries)
48
+
49
+
50
+ class BeamBoundariesCalibrationBase(CryonirspTaskBase):
51
+ """
52
+ Task class for calculation of the beam boundaries for later use during calibration.
53
+
54
+ Parameters
55
+ ----------
56
+ recipe_run_id : int
57
+ id of the recipe run used to identify the workflow run this task is part of
58
+ workflow_name : str
59
+ name of the workflow to which this instance of the task belongs
60
+ workflow_version : str
61
+ version of the workflow to which this instance of the task belongs
62
+
63
+ """
64
+
65
+ record_provenance = True
66
+
67
+ def run(self):
68
+ """
69
+ Compute the beam boundaries by analyzing a set of solar gain images.
70
+
71
+ Steps:
72
+ 1. Compute the average gain image
73
+ 2. Correct any bad pixels
74
+ 3. Smooth the image using a median filter
75
+ 4. Split the beam along the horizontal axis (SP only)
76
+ 5. Use a bimodal threshold filter to segment the image into illuminated and non-illuminated regions
77
+ 6. Compute the boundaries of the illuminated region
78
+ 7. Extract the illuminated portion of the beam images
79
+ 8. Find the horizontal shift between the two images
80
+ 9. Identify the boundaries of the overlap
81
+ 10. Save the boundaries as a fits file (json?)
82
+
83
+ Returns
84
+ -------
85
+ None
86
+
87
+ """
88
+ # Step 1:
89
+ with self.apm_processing_step(f"Compute average solar gain image"):
90
+ average_solar_gain_array = self.compute_average_gain_array()
91
+
92
+ # Step 2:
93
+ with self.apm_task_step(f"Retrieve bad pixel map"):
94
+ bad_pixel_map = self.intermediate_frame_load_full_bad_pixel_map()
95
+ corrected_solar_gain_array = self.corrections_correct_bad_pixels(
96
+ average_solar_gain_array, bad_pixel_map
97
+ )
98
+
99
+ # Step 3
100
+ with self.apm_processing_step(f"Smooth the array to get good segmentation"):
101
+ smoothed_solar_gain_array = self.smooth_gain_array(corrected_solar_gain_array)
102
+
103
+ # Step 4
104
+ with self.apm_processing_step(f"Split the beam horizontally"):
105
+ split_beams = self.split_beams(smoothed_solar_gain_array)
106
+
107
+ # Step 5
108
+ with self.apm_processing_step(
109
+ f"Segment the beams into illuminated and non-illuminated pixels"
110
+ ):
111
+ segmented_beams = self.segment_arrays(split_beams)
112
+
113
+ # Step 6:
114
+ with self.apm_processing_step(
115
+ f"Compute the inscribed rectangular extents of the illuminated portions of the sensor"
116
+ ):
117
+ illuminated_boundaries = self.compute_boundaries_of_beam_illumination_regions(
118
+ segmented_beams
119
+ )
120
+
121
+ # Steps 7 - 9:
122
+ with self.apm_processing_step(f"Compute the boundaries of the illuminated beams"):
123
+ split_beams_float = [split_beam.astype(float) for split_beam in split_beams]
124
+ boundaries = self.compute_final_beam_boundaries(
125
+ split_beams_float, illuminated_boundaries
126
+ )
127
+
128
+ # Step 10:
129
+ with self.apm_writing_step("Writing beam boundaries"):
130
+ for beam, bounds in enumerate(boundaries, start=1):
131
+ self.intermediate_frame_write_arrays(
132
+ bounds.beam_boundaries_array,
133
+ task_tag=CryonirspTag.task_beam_boundaries(),
134
+ beam=beam,
135
+ )
136
+
137
+ def compute_average_gain_array(self) -> np.ndarray:
138
+ """
139
+ Compute an average of uncorrected solar gain arrays.
140
+
141
+ We are computing the overall illumination pattern for both beams simultaneously,
142
+ so no dark correction is required and no beam splitting is used at this point.
143
+ Solar gain images are used because they have larger flux than the lamp gain images
144
+ and the lamp gain images do not have the same illumination pattern as the solar
145
+ gain images.
146
+
147
+ Returns
148
+ -------
149
+ The average gain array
150
+ """
151
+ lin_corr_gain_arrays = self.linearized_frame_full_array_generator(
152
+ tags=[
153
+ CryonirspTag.task_solar_gain(),
154
+ CryonirspTag.linearized(),
155
+ CryonirspTag.frame(),
156
+ ]
157
+ )
158
+ averaged_gain_data = average_numpy_arrays(lin_corr_gain_arrays)
159
+ return averaged_gain_data
160
+
161
+ def smooth_gain_array(self, array: np.ndarray) -> np.ndarray:
162
+ """
163
+ Smooth the input array with morphological filtering using a disk shape.
164
+
165
+ The array is smoothed to help eliminate artifacts in the image segmentation step.
166
+
167
+ Parameters
168
+ ----------
169
+ array
170
+ The input array to be smoothed
171
+
172
+ Returns
173
+ -------
174
+ The smoothed output array
175
+ """
176
+ # skimage.filters requires ubyte arrays and float->ubyte conversion only works with float in range [-1, 1]
177
+ norm_gain = img_as_ubyte(rescale_intensity(array, out_range=(0, 1.0)))
178
+
179
+ disk_size = self.parameters.beam_boundaries_smoothing_disk_size
180
+ norm_gain = filters.rank.median(norm_gain, disk(disk_size))
181
+ return norm_gain
182
+
183
+ @abstractmethod
184
+ def split_beams(self, input_array: np.ndarray) -> list[np.ndarray]:
185
+ """
186
+ Split the beams along the horizontal axis.
187
+
188
+ We use an abstract method so that the processing sequence is the same for both SP and CI
189
+ although the processing steps may be different.
190
+
191
+ Parameters
192
+ ----------
193
+ input_array
194
+ The array to be split
195
+
196
+ Returns
197
+ -------
198
+ [array, ...]
199
+ A list of arrays after the split
200
+ """
201
+ pass
202
+
203
+ @staticmethod
204
+ def segment_arrays(arrays: list[np.ndarray]) -> list[np.ndarray]:
205
+ """
206
+ Segment the arrays into illuminated (True) and non-illuminated (False) regions.
207
+
208
+ Parameters
209
+ ----------
210
+ arrays
211
+ The arrays to be segmented
212
+
213
+ Returns
214
+ -------
215
+ [array, ...]
216
+ The boolean segmented output arrays
217
+ """
218
+ segmented_arrays = []
219
+ for beam_num, beam_array in enumerate(arrays, start=1):
220
+ thresh = filters.threshold_minimum(beam_array)
221
+ logger.info(f"Segmentation threshold for beam {beam_num} = {thresh}")
222
+ segmented_arrays.append((beam_array > thresh).astype(bool))
223
+ return segmented_arrays
224
+
225
+ def compute_boundaries_of_beam_illumination_regions(
226
+ self, arrays: list[np.ndarray]
227
+ ) -> list[BeamBoundary]:
228
+ """
229
+ Compute the rectangular boundaries describing the illuminated regions of the beam images.
230
+
231
+ Parameters
232
+ ----------
233
+ arrays
234
+ A list of segmented boolean arrays over which the boundaries are to be computed
235
+
236
+ Returns
237
+ -------
238
+ [BeamBoundary, ...]
239
+ A list of the BeamBoundary objects representing the illuminated region(s) of the beam image(s)
240
+ """
241
+ boundaries = []
242
+ for beam, array in enumerate(arrays, start=1):
243
+ # Find the largest interior rectangle that inscribes the illuminated region for this beam
244
+ x_min, y_min, x_range, y_range = lir(array)
245
+ # Compute the new image bounds, the maximums are exclusive and can be used in slices
246
+ y_max = y_min + y_range
247
+ x_max = x_min + x_range
248
+
249
+ # Make sure all pixels are 1s
250
+ if not np.all(array[y_min:y_max, x_min:x_max]):
251
+ raise RuntimeError(
252
+ f"Unable to compute illuminated image boundaries for beam {beam}"
253
+ )
254
+
255
+ boundaries.append(BeamBoundary(y_min, y_max, x_min, x_max))
256
+
257
+ return boundaries
258
+
259
+ @abstractmethod
260
+ def compute_final_beam_boundaries(
261
+ self,
262
+ smoothed_solar_gain_arrays: list[np.ndarray],
263
+ illuminated_boundaries: list[BeamBoundary],
264
+ ) -> list[BeamBoundary]:
265
+ """
266
+ Compute the final beam boundaries to be used when accessing individual beam images from the input dual-beam arrays.
267
+
268
+ Parameters
269
+ ----------
270
+ smoothed_solar_gain_arrays
271
+ Smoothed solar gain arrays to be used for coarsely aligning the individual beam images.
272
+ illuminated_boundaries
273
+ A list of BeamBoundary objects representing the illuminated regions of each beam image.
274
+
275
+ Returns
276
+ -------
277
+ A list of BeamBoundary objects describing the final beam boundaries for each beam.
278
+ """
279
+ pass
@@ -0,0 +1,55 @@
1
+ """Cryonirsp CI beam boundaries task."""
2
+ import largestinteriorrectangle as lir
3
+ import numpy as np
4
+ from dkist_service_configuration.logging import logger
5
+
6
+ from dkist_processing_cryonirsp.tasks.beam_boundaries_base import BeamBoundariesCalibrationBase
7
+ from dkist_processing_cryonirsp.tasks.beam_boundaries_base import BeamBoundary
8
+
9
+ __all__ = ["CIBeamBoundariesCalibration"]
10
+
11
+
12
+ class CIBeamBoundariesCalibration(BeamBoundariesCalibrationBase):
13
+ """Task class for calculation of the CI beam boundaries for later use during calibration."""
14
+
15
+ def split_beams(self, array: np.ndarray) -> list[np.ndarray]:
16
+ """
17
+ Return the input array, as there is no beam split for CI.
18
+
19
+ This is just a pass-through method to facilitate using the same processing
20
+ steps in the base run method.
21
+
22
+ Parameters
23
+ ----------
24
+ array
25
+ The input array
26
+
27
+ Returns
28
+ -------
29
+ [array]
30
+ The input array embedded in a list
31
+ """
32
+ return [array]
33
+
34
+ def compute_final_beam_boundaries(
35
+ self, smoothed_solar_gain_array: np.ndarray, illuminated_boundaries: list[BeamBoundary]
36
+ ):
37
+ """
38
+ Compute the final beam boundaries to be used when accessing CI beam images from the input dual-beam arrays.
39
+
40
+ Note: For CI, the illuminated beam boundaries ARE the final beam boundaries.
41
+ This method is simply a pass-through that allows both SP and CI to use the processing sequence.
42
+
43
+ Parameters
44
+ ----------
45
+ smoothed_solar_gain_array
46
+ The smoothed solar gain array for a single CI beam.
47
+ illuminated_boundaries
48
+ A list with a single BeamBoundary object for the illuminated CI beam.
49
+
50
+ Returns
51
+ -------
52
+ [BeamBoundary]
53
+ The CI BeamBoundary object embedded in a list.
54
+ """
55
+ return illuminated_boundaries