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.
- changelog/.gitempty +0 -0
- dkist_processing_cryonirsp/__init__.py +11 -0
- dkist_processing_cryonirsp/config.py +12 -0
- dkist_processing_cryonirsp/models/__init__.py +1 -0
- dkist_processing_cryonirsp/models/constants.py +248 -0
- dkist_processing_cryonirsp/models/exposure_conditions.py +26 -0
- dkist_processing_cryonirsp/models/parameters.py +296 -0
- dkist_processing_cryonirsp/models/tags.py +168 -0
- dkist_processing_cryonirsp/models/task_name.py +14 -0
- dkist_processing_cryonirsp/parsers/__init__.py +1 -0
- dkist_processing_cryonirsp/parsers/cryonirsp_l0_fits_access.py +111 -0
- dkist_processing_cryonirsp/parsers/cryonirsp_l1_fits_access.py +30 -0
- dkist_processing_cryonirsp/parsers/exposure_conditions.py +163 -0
- dkist_processing_cryonirsp/parsers/map_repeats.py +40 -0
- dkist_processing_cryonirsp/parsers/measurements.py +55 -0
- dkist_processing_cryonirsp/parsers/modstates.py +31 -0
- dkist_processing_cryonirsp/parsers/optical_density_filters.py +40 -0
- dkist_processing_cryonirsp/parsers/polarimetric_check.py +120 -0
- dkist_processing_cryonirsp/parsers/scan_step.py +412 -0
- dkist_processing_cryonirsp/parsers/time.py +80 -0
- dkist_processing_cryonirsp/parsers/wavelength.py +26 -0
- dkist_processing_cryonirsp/tasks/__init__.py +19 -0
- dkist_processing_cryonirsp/tasks/assemble_movie.py +202 -0
- dkist_processing_cryonirsp/tasks/bad_pixel_map.py +96 -0
- dkist_processing_cryonirsp/tasks/beam_boundaries_base.py +279 -0
- dkist_processing_cryonirsp/tasks/ci_beam_boundaries.py +55 -0
- dkist_processing_cryonirsp/tasks/ci_science.py +169 -0
- dkist_processing_cryonirsp/tasks/cryonirsp_base.py +67 -0
- dkist_processing_cryonirsp/tasks/dark.py +98 -0
- dkist_processing_cryonirsp/tasks/gain.py +251 -0
- dkist_processing_cryonirsp/tasks/instrument_polarization.py +447 -0
- dkist_processing_cryonirsp/tasks/l1_output_data.py +44 -0
- dkist_processing_cryonirsp/tasks/linearity_correction.py +582 -0
- dkist_processing_cryonirsp/tasks/make_movie_frames.py +302 -0
- dkist_processing_cryonirsp/tasks/mixin/__init__.py +1 -0
- dkist_processing_cryonirsp/tasks/mixin/beam_access.py +52 -0
- dkist_processing_cryonirsp/tasks/mixin/corrections.py +177 -0
- dkist_processing_cryonirsp/tasks/mixin/intermediate_frame.py +193 -0
- dkist_processing_cryonirsp/tasks/mixin/linearized_frame.py +309 -0
- dkist_processing_cryonirsp/tasks/mixin/shift_measurements.py +297 -0
- dkist_processing_cryonirsp/tasks/parse.py +281 -0
- dkist_processing_cryonirsp/tasks/quality_metrics.py +271 -0
- dkist_processing_cryonirsp/tasks/science_base.py +511 -0
- dkist_processing_cryonirsp/tasks/sp_beam_boundaries.py +270 -0
- dkist_processing_cryonirsp/tasks/sp_dispersion_axis_correction.py +484 -0
- dkist_processing_cryonirsp/tasks/sp_geometric.py +585 -0
- dkist_processing_cryonirsp/tasks/sp_science.py +299 -0
- dkist_processing_cryonirsp/tasks/sp_solar_gain.py +475 -0
- dkist_processing_cryonirsp/tasks/trial_output_data.py +61 -0
- dkist_processing_cryonirsp/tasks/write_l1.py +1033 -0
- dkist_processing_cryonirsp/tests/__init__.py +1 -0
- dkist_processing_cryonirsp/tests/conftest.py +456 -0
- dkist_processing_cryonirsp/tests/header_models.py +592 -0
- dkist_processing_cryonirsp/tests/local_trial_workflows/__init__.py +0 -0
- dkist_processing_cryonirsp/tests/local_trial_workflows/l0_cals_only.py +541 -0
- dkist_processing_cryonirsp/tests/local_trial_workflows/l0_to_l1.py +615 -0
- dkist_processing_cryonirsp/tests/local_trial_workflows/linearize_only.py +96 -0
- dkist_processing_cryonirsp/tests/local_trial_workflows/local_trial_helpers.py +592 -0
- dkist_processing_cryonirsp/tests/test_assemble_movie.py +144 -0
- dkist_processing_cryonirsp/tests/test_assemble_qualilty.py +517 -0
- dkist_processing_cryonirsp/tests/test_bad_pixel_maps.py +115 -0
- dkist_processing_cryonirsp/tests/test_ci_beam_boundaries.py +106 -0
- dkist_processing_cryonirsp/tests/test_ci_science.py +355 -0
- dkist_processing_cryonirsp/tests/test_corrections.py +126 -0
- dkist_processing_cryonirsp/tests/test_cryo_base.py +202 -0
- dkist_processing_cryonirsp/tests/test_cryo_constants.py +76 -0
- dkist_processing_cryonirsp/tests/test_dark.py +287 -0
- dkist_processing_cryonirsp/tests/test_gain.py +278 -0
- dkist_processing_cryonirsp/tests/test_instrument_polarization.py +531 -0
- dkist_processing_cryonirsp/tests/test_linearity_correction.py +245 -0
- dkist_processing_cryonirsp/tests/test_make_movie_frames.py +111 -0
- dkist_processing_cryonirsp/tests/test_parameters.py +266 -0
- dkist_processing_cryonirsp/tests/test_parse.py +1439 -0
- dkist_processing_cryonirsp/tests/test_quality.py +203 -0
- dkist_processing_cryonirsp/tests/test_sp_beam_boundaries.py +112 -0
- dkist_processing_cryonirsp/tests/test_sp_dispersion_axis_correction.py +155 -0
- dkist_processing_cryonirsp/tests/test_sp_geometric.py +319 -0
- dkist_processing_cryonirsp/tests/test_sp_make_movie_frames.py +121 -0
- dkist_processing_cryonirsp/tests/test_sp_science.py +483 -0
- dkist_processing_cryonirsp/tests/test_sp_solar.py +198 -0
- dkist_processing_cryonirsp/tests/test_trial_create_quality_report.py +79 -0
- dkist_processing_cryonirsp/tests/test_trial_output_data.py +251 -0
- dkist_processing_cryonirsp/tests/test_workflows.py +9 -0
- dkist_processing_cryonirsp/tests/test_write_l1.py +436 -0
- dkist_processing_cryonirsp/workflows/__init__.py +2 -0
- dkist_processing_cryonirsp/workflows/ci_l0_processing.py +77 -0
- dkist_processing_cryonirsp/workflows/sp_l0_processing.py +84 -0
- dkist_processing_cryonirsp/workflows/trial_workflows.py +190 -0
- dkist_processing_cryonirsp-1.3.4.dist-info/METADATA +194 -0
- dkist_processing_cryonirsp-1.3.4.dist-info/RECORD +111 -0
- dkist_processing_cryonirsp-1.3.4.dist-info/WHEEL +5 -0
- dkist_processing_cryonirsp-1.3.4.dist-info/top_level.txt +4 -0
- docs/Makefile +134 -0
- docs/bad_pixel_calibration.rst +47 -0
- docs/beam_angle_calculation.rst +53 -0
- docs/beam_boundary_computation.rst +88 -0
- docs/changelog.rst +7 -0
- docs/ci_science_calibration.rst +33 -0
- docs/conf.py +52 -0
- docs/index.rst +21 -0
- docs/l0_to_l1_cryonirsp_ci-full-trial.rst +10 -0
- docs/l0_to_l1_cryonirsp_ci.rst +10 -0
- docs/l0_to_l1_cryonirsp_sp-full-trial.rst +10 -0
- docs/l0_to_l1_cryonirsp_sp.rst +10 -0
- docs/linearization.rst +43 -0
- docs/make.bat +170 -0
- docs/requirements.txt +1 -0
- docs/requirements_table.rst +8 -0
- docs/scientific_changelog.rst +10 -0
- docs/sp_science_calibration.rst +59 -0
- licenses/LICENSE.rst +11 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""Cryonirsp make movie frames task."""
|
|
2
|
+
from abc import ABC
|
|
3
|
+
from abc import abstractmethod
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from astropy.io import fits
|
|
7
|
+
from astropy.visualization import ZScaleInterval
|
|
8
|
+
from dkist_processing_common.codecs.fits import fits_access_decoder
|
|
9
|
+
from dkist_processing_common.codecs.fits import fits_hdulist_encoder
|
|
10
|
+
from dkist_service_configuration.logging import logger
|
|
11
|
+
|
|
12
|
+
from dkist_processing_cryonirsp.models.tags import CryonirspTag
|
|
13
|
+
from dkist_processing_cryonirsp.parsers.cryonirsp_l1_fits_access import CryonirspL1FitsAccess
|
|
14
|
+
from dkist_processing_cryonirsp.tasks.cryonirsp_base import CryonirspTaskBase
|
|
15
|
+
|
|
16
|
+
__all__ = ["MakeCryonirspMovieFrames", "SPMakeCryonirspMovieFrames"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MakeCryonirspMovieFramesBase(CryonirspTaskBase, ABC):
|
|
20
|
+
"""Create CryoNIRSP movie frames common functionality for the Context Imager (CI) and Spectropolarimeter (SP)."""
|
|
21
|
+
|
|
22
|
+
def run(self):
|
|
23
|
+
"""Create movie frames using all stokes states if they exist, otherwise only use intensity."""
|
|
24
|
+
if self.constants.correct_for_polarization:
|
|
25
|
+
with self.apm_task_step("Make full stokes movie"):
|
|
26
|
+
self.make_full_stokes_movie_frames()
|
|
27
|
+
else:
|
|
28
|
+
with self.apm_task_step("Make intensity only movie"):
|
|
29
|
+
self.make_intensity_movie_frames()
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def make_full_stokes_movie_frames(self):
|
|
33
|
+
"""Make a movie that combines each of the stokes frames into a single movie frame."""
|
|
34
|
+
|
|
35
|
+
@abstractmethod
|
|
36
|
+
def make_intensity_movie_frames(self):
|
|
37
|
+
"""Make a movie out of stokes I frames."""
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def scale_for_rendering(data: np.ndarray):
|
|
41
|
+
"""Scale the calibrated frame data using a normalization function to facilitate display as a movie frame."""
|
|
42
|
+
zscale = ZScaleInterval()
|
|
43
|
+
return zscale(data)
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def grid_movie_frame(
|
|
47
|
+
top_left: np.ndarray,
|
|
48
|
+
top_right: np.ndarray,
|
|
49
|
+
bottom_left: np.ndarray,
|
|
50
|
+
bottom_right: np.ndarray,
|
|
51
|
+
) -> np.ndarray:
|
|
52
|
+
"""Combine multiple arrays into a 2x2 grid."""
|
|
53
|
+
result = np.concatenate(
|
|
54
|
+
(
|
|
55
|
+
np.concatenate((top_left, top_right), axis=1),
|
|
56
|
+
np.concatenate((bottom_left, bottom_right), axis=1),
|
|
57
|
+
),
|
|
58
|
+
axis=0,
|
|
59
|
+
)
|
|
60
|
+
return result
|
|
61
|
+
|
|
62
|
+
def get_movie_header(
|
|
63
|
+
self, map_scan: int, scan_step: int, meas_num: int, stokes_state: str
|
|
64
|
+
) -> fits.Header:
|
|
65
|
+
"""Create a header to use on a movie frame based on a calibrated frame."""
|
|
66
|
+
calibrated_frame = next(
|
|
67
|
+
self.read(
|
|
68
|
+
tags=[
|
|
69
|
+
CryonirspTag.frame(),
|
|
70
|
+
CryonirspTag.calibrated(),
|
|
71
|
+
CryonirspTag.stokes(stokes_state),
|
|
72
|
+
CryonirspTag.meas_num(meas_num),
|
|
73
|
+
CryonirspTag.map_scan(map_scan),
|
|
74
|
+
CryonirspTag.scan_step(scan_step),
|
|
75
|
+
],
|
|
76
|
+
decoder=fits_access_decoder,
|
|
77
|
+
fits_access_class=CryonirspL1FitsAccess,
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
header = calibrated_frame.header
|
|
81
|
+
return header
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# See note below on `SPMakeCryonirspMovieFrames`
|
|
85
|
+
class MakeCryonirspMovieFrames(MakeCryonirspMovieFramesBase):
|
|
86
|
+
"""Make CryoNIRSP movie frames for the Context Imager and tag with CryonirspTag.movie_frame()."""
|
|
87
|
+
|
|
88
|
+
def make_full_stokes_movie_frames(self):
|
|
89
|
+
"""Make a movie that combines each of the stokes frames into a single movie frame."""
|
|
90
|
+
self.make_intensity_movie_frames()
|
|
91
|
+
|
|
92
|
+
def make_intensity_movie_frames(self):
|
|
93
|
+
"""Make a movie out of stokes I frames."""
|
|
94
|
+
for map_scan in range(1, self.constants.num_map_scans + 1):
|
|
95
|
+
for scan_step in range(1, self.constants.num_scan_steps + 1):
|
|
96
|
+
logger.info(f"Generate Stokes-I movie frame for {map_scan=} and {scan_step=}")
|
|
97
|
+
movie_frame_hdu = self.make_intensity_frame(map_scan=map_scan, scan_step=scan_step)
|
|
98
|
+
logger.info(f"Writing Stokes-I movie frame for {map_scan=} and {scan_step=}")
|
|
99
|
+
self.write(
|
|
100
|
+
data=fits.HDUList([movie_frame_hdu]),
|
|
101
|
+
tags=[
|
|
102
|
+
CryonirspTag.map_scan(map_scan),
|
|
103
|
+
CryonirspTag.scan_step(scan_step),
|
|
104
|
+
CryonirspTag.movie_frame(),
|
|
105
|
+
],
|
|
106
|
+
encoder=fits_hdulist_encoder,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def make_full_stokes_frame(self, map_scan: int, scan_step: int) -> fits.PrimaryHDU:
|
|
110
|
+
"""Create a movie frame (data + header) with the stokes frames (IQUV) combined into top left, top right, bottom left, bottom right quadrants respectively."""
|
|
111
|
+
meas_num = 1 # Use only the first measurement if there are multiple measurements.
|
|
112
|
+
stokes_i = self.get_movie_frame(
|
|
113
|
+
map_scan=map_scan, scan_step=scan_step, meas_num=meas_num, stokes_state="I"
|
|
114
|
+
)
|
|
115
|
+
stokes_q = self.get_movie_frame(
|
|
116
|
+
map_scan=map_scan, scan_step=scan_step, meas_num=meas_num, stokes_state="Q"
|
|
117
|
+
)
|
|
118
|
+
stokes_u = self.get_movie_frame(
|
|
119
|
+
map_scan=map_scan, scan_step=scan_step, meas_num=meas_num, stokes_state="U"
|
|
120
|
+
)
|
|
121
|
+
stokes_v = self.get_movie_frame(
|
|
122
|
+
map_scan=map_scan, scan_step=scan_step, meas_num=meas_num, stokes_state="V"
|
|
123
|
+
)
|
|
124
|
+
movie_frame = self.grid_movie_frame(
|
|
125
|
+
top_left=stokes_i,
|
|
126
|
+
top_right=stokes_q,
|
|
127
|
+
bottom_left=stokes_u,
|
|
128
|
+
bottom_right=stokes_v,
|
|
129
|
+
)
|
|
130
|
+
movie_header = self.get_movie_header(
|
|
131
|
+
map_scan=map_scan, scan_step=scan_step, meas_num=meas_num, stokes_state="I"
|
|
132
|
+
)
|
|
133
|
+
return fits.PrimaryHDU(header=movie_header, data=movie_frame)
|
|
134
|
+
|
|
135
|
+
def make_intensity_frame(self, map_scan: int, scan_step: int) -> fits.PrimaryHDU:
|
|
136
|
+
"""Create a movie frame (data + header) with just the stokes I frames."""
|
|
137
|
+
meas_num = 1 # Use only the first measurement if there are multiple measurements.
|
|
138
|
+
stokes_i = self.get_movie_frame(
|
|
139
|
+
map_scan=map_scan, scan_step=scan_step, meas_num=meas_num, stokes_state="I"
|
|
140
|
+
)
|
|
141
|
+
movie_header = self.get_movie_header(
|
|
142
|
+
map_scan=map_scan, scan_step=scan_step, meas_num=meas_num, stokes_state="I"
|
|
143
|
+
)
|
|
144
|
+
return fits.PrimaryHDU(header=movie_header, data=stokes_i)
|
|
145
|
+
|
|
146
|
+
def get_movie_frame(
|
|
147
|
+
self, map_scan: int, scan_step: int, meas_num: int, stokes_state: str
|
|
148
|
+
) -> np.ndarray:
|
|
149
|
+
"""Retrieve the calibrated frame data for the first frame which matches the input parameters and transform it into a movie frame (i.e. normalize the values)."""
|
|
150
|
+
calibrated_frame = next(
|
|
151
|
+
self.read(
|
|
152
|
+
tags=[
|
|
153
|
+
CryonirspTag.frame(),
|
|
154
|
+
CryonirspTag.calibrated(),
|
|
155
|
+
CryonirspTag.stokes(stokes_state),
|
|
156
|
+
CryonirspTag.meas_num(meas_num),
|
|
157
|
+
CryonirspTag.map_scan(map_scan),
|
|
158
|
+
CryonirspTag.scan_step(scan_step),
|
|
159
|
+
],
|
|
160
|
+
decoder=fits_access_decoder,
|
|
161
|
+
fits_access_class=CryonirspL1FitsAccess,
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
movie_frame = self.scale_for_rendering(calibrated_frame.data)
|
|
165
|
+
return movie_frame
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# NOTE:
|
|
169
|
+
# This task isn't used right now because the SP movies need some better handling of the wavelength dimension.
|
|
170
|
+
# For the time being both arms use the `MakeCryonirspMovieFrames` task above.
|
|
171
|
+
# See PR #91 for more information.
|
|
172
|
+
class SPMakeCryonirspMovieFrames(MakeCryonirspMovieFramesBase):
|
|
173
|
+
"""Make CryoNIRSP movie frames for the Spectropolarimeter and tag with CryonirspTag.movie_frame()."""
|
|
174
|
+
|
|
175
|
+
def make_full_stokes_movie_frames(self):
|
|
176
|
+
"""Make a movie that combines each of the stokes frames into a single movie frame."""
|
|
177
|
+
for map_scan in range(1, self.constants.num_map_scans + 1):
|
|
178
|
+
logger.info(f"Generate full stokes movie frame for {map_scan=}")
|
|
179
|
+
movie_frame_hdu = self.make_full_stokes_frame(map_scan=map_scan)
|
|
180
|
+
|
|
181
|
+
logger.info(f"Writing full stokes movie frame for {map_scan=}")
|
|
182
|
+
self.write(
|
|
183
|
+
data=fits.HDUList([movie_frame_hdu]),
|
|
184
|
+
tags=[
|
|
185
|
+
CryonirspTag.map_scan(map_scan),
|
|
186
|
+
CryonirspTag.movie_frame(),
|
|
187
|
+
],
|
|
188
|
+
encoder=fits_hdulist_encoder,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
def make_intensity_movie_frames(self):
|
|
192
|
+
"""Make a movie out of stokes I frames."""
|
|
193
|
+
for map_scan in range(1, self.constants.num_map_scans + 1):
|
|
194
|
+
logger.info(f"Generate intensity movie frame for {map_scan=}")
|
|
195
|
+
movie_frame_hdu = self.make_intensity_frame(map_scan=map_scan)
|
|
196
|
+
|
|
197
|
+
logger.info(f"Writing intensity movie frame for {map_scan=}")
|
|
198
|
+
self.write(
|
|
199
|
+
data=fits.HDUList([movie_frame_hdu]),
|
|
200
|
+
tags=[
|
|
201
|
+
CryonirspTag.map_scan(map_scan),
|
|
202
|
+
CryonirspTag.movie_frame(),
|
|
203
|
+
],
|
|
204
|
+
encoder=fits_hdulist_encoder,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
def make_full_stokes_frame(self, map_scan: int) -> fits.PrimaryHDU:
|
|
208
|
+
"""Create a movie frame (data + header) with the stokes frames (IQUV) combined into top left, top right, bottom left, bottom right quadrants respectively."""
|
|
209
|
+
meas_num = 1 # Use only the first measurement if there are multiple measurements.
|
|
210
|
+
stokes_i = self.get_movie_frame(map_scan=map_scan, meas_num=meas_num, stokes_state="I")
|
|
211
|
+
stokes_q = self.get_movie_frame(map_scan=map_scan, meas_num=meas_num, stokes_state="Q")
|
|
212
|
+
stokes_u = self.get_movie_frame(map_scan=map_scan, meas_num=meas_num, stokes_state="U")
|
|
213
|
+
stokes_v = self.get_movie_frame(map_scan=map_scan, meas_num=meas_num, stokes_state="V")
|
|
214
|
+
movie_frame = self.grid_movie_frame(
|
|
215
|
+
top_left=stokes_i,
|
|
216
|
+
top_right=stokes_q,
|
|
217
|
+
bottom_left=stokes_u,
|
|
218
|
+
bottom_right=stokes_v,
|
|
219
|
+
)
|
|
220
|
+
# Here we make the full map have the same header as the first scan step.
|
|
221
|
+
movie_header = self.get_movie_header(
|
|
222
|
+
map_scan=map_scan, scan_step=1, meas_num=meas_num, stokes_state="I"
|
|
223
|
+
)
|
|
224
|
+
return fits.PrimaryHDU(header=movie_header, data=movie_frame)
|
|
225
|
+
|
|
226
|
+
def make_intensity_frame(self, map_scan: int) -> fits.PrimaryHDU:
|
|
227
|
+
"""Create a movie frame (data + header) with just the stokes I frames."""
|
|
228
|
+
meas_num = 1 # Use only the first measurement if there are multiple measurements.
|
|
229
|
+
stokes_i = self.get_movie_frame(map_scan=map_scan, meas_num=meas_num, stokes_state="I")
|
|
230
|
+
movie_header = self.get_movie_header(
|
|
231
|
+
map_scan=map_scan, scan_step=1, meas_num=meas_num, stokes_state="I"
|
|
232
|
+
)
|
|
233
|
+
return fits.PrimaryHDU(header=movie_header, data=stokes_i)
|
|
234
|
+
|
|
235
|
+
def get_movie_frame(self, map_scan: int, meas_num: int, stokes_state: str) -> np.ndarray:
|
|
236
|
+
"""Retrieve the calibrated frame data for the first frame which matches the input parameters and transform it into a movie frame (i.e. normalize the values)."""
|
|
237
|
+
if self.constants.num_scan_steps == 1:
|
|
238
|
+
calibrated_frame = self.get_spectral_calibrated_frame(
|
|
239
|
+
map_scan=map_scan, meas_num=meas_num, stokes_state=stokes_state
|
|
240
|
+
)
|
|
241
|
+
else:
|
|
242
|
+
calibrated_frame = self.get_integrated_calibrated_frame(
|
|
243
|
+
map_scan=map_scan, meas_num=meas_num, stokes_state=stokes_state
|
|
244
|
+
)
|
|
245
|
+
movie_frame = self.scale_for_rendering(calibrated_frame.data)
|
|
246
|
+
return movie_frame
|
|
247
|
+
|
|
248
|
+
def get_spectral_calibrated_frame(
|
|
249
|
+
self, map_scan: int, meas_num: int, stokes_state: str
|
|
250
|
+
) -> np.ndarray:
|
|
251
|
+
"""Retrieve a calibrated frame for a single scan step (no integration)."""
|
|
252
|
+
scan_step = 1 # There is only a single scan step in a spectral movie
|
|
253
|
+
calibrated_frame = next(
|
|
254
|
+
self.read(
|
|
255
|
+
tags=[
|
|
256
|
+
CryonirspTag.frame(),
|
|
257
|
+
CryonirspTag.calibrated(),
|
|
258
|
+
CryonirspTag.stokes(stokes_state),
|
|
259
|
+
CryonirspTag.meas_num(meas_num),
|
|
260
|
+
CryonirspTag.map_scan(map_scan),
|
|
261
|
+
CryonirspTag.scan_step(scan_step),
|
|
262
|
+
],
|
|
263
|
+
decoder=fits_access_decoder,
|
|
264
|
+
fits_access_class=CryonirspL1FitsAccess,
|
|
265
|
+
)
|
|
266
|
+
)
|
|
267
|
+
return calibrated_frame.data
|
|
268
|
+
|
|
269
|
+
def get_integrated_calibrated_frame(
|
|
270
|
+
self, map_scan: int, meas_num: int, stokes_state: str
|
|
271
|
+
) -> np.ndarray:
|
|
272
|
+
"""Retrieve a frame that has been integrated across scan steps."""
|
|
273
|
+
data = self.get_spectral_calibrated_frame(
|
|
274
|
+
map_scan=map_scan, meas_num=meas_num, stokes_state=stokes_state
|
|
275
|
+
)
|
|
276
|
+
integrated_data = self.integrate_spectral_frame(data)
|
|
277
|
+
integrated_arrays = [integrated_data]
|
|
278
|
+
for scan_step in range(2, self.constants.num_scan_steps + 1):
|
|
279
|
+
calibrated_frame = next(
|
|
280
|
+
self.read(
|
|
281
|
+
tags=[
|
|
282
|
+
CryonirspTag.frame(),
|
|
283
|
+
CryonirspTag.calibrated(),
|
|
284
|
+
CryonirspTag.stokes(stokes_state),
|
|
285
|
+
CryonirspTag.meas_num(meas_num),
|
|
286
|
+
CryonirspTag.map_scan(map_scan),
|
|
287
|
+
CryonirspTag.scan_step(scan_step),
|
|
288
|
+
],
|
|
289
|
+
decoder=fits_access_decoder,
|
|
290
|
+
fits_access_class=CryonirspL1FitsAccess,
|
|
291
|
+
)
|
|
292
|
+
)
|
|
293
|
+
integrated_data = self.integrate_spectral_frame(calibrated_frame.data)
|
|
294
|
+
integrated_arrays.append(integrated_data)
|
|
295
|
+
full_frame = np.vstack(integrated_arrays)
|
|
296
|
+
return full_frame
|
|
297
|
+
|
|
298
|
+
@staticmethod
|
|
299
|
+
def integrate_spectral_frame(data: np.ndarray) -> np.ndarray:
|
|
300
|
+
"""Integrate spectral frame."""
|
|
301
|
+
wavelength_integrated_data = np.sum(np.abs(data), axis=1)
|
|
302
|
+
return wavelength_integrated_data
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Init."""
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Mixin to support extracting the desired beam from an input image on-the-fly."""
|
|
2
|
+
from functools import cached_property
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BeamAccessMixin:
|
|
8
|
+
"""Mixin that supports extracting the desired beam from an input image on-the-fly."""
|
|
9
|
+
|
|
10
|
+
def beam_access_get_beam(self, array: np.ndarray, beam: int) -> np.ndarray:
|
|
11
|
+
"""
|
|
12
|
+
Extract a single beam array from a dual-beam array.
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
array
|
|
17
|
+
The input dual-beam array
|
|
18
|
+
beam
|
|
19
|
+
The desired beam to extract
|
|
20
|
+
|
|
21
|
+
Returns
|
|
22
|
+
-------
|
|
23
|
+
An ndarray containing the extracted beam
|
|
24
|
+
"""
|
|
25
|
+
boundaries = self.beam_boundaries[beam]
|
|
26
|
+
spatial_min, spatial_max, spectral_min, spectral_max = boundaries
|
|
27
|
+
|
|
28
|
+
if (
|
|
29
|
+
spatial_min < 0
|
|
30
|
+
or spatial_max > array.shape[0]
|
|
31
|
+
or spectral_min < 0
|
|
32
|
+
or spectral_max > array.shape[1]
|
|
33
|
+
):
|
|
34
|
+
raise IndexError(
|
|
35
|
+
f"beam_access_get_boundaries exceed array bounds: {boundaries = }, {array.shape = }."
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
return np.copy(array[spatial_min:spatial_max, spectral_min:spectral_max])
|
|
39
|
+
|
|
40
|
+
@cached_property
|
|
41
|
+
def beam_boundaries(self) -> dict[int, np.ndarray]:
|
|
42
|
+
"""
|
|
43
|
+
Load the beam boundaries from their respective files and return as a boundary dict.
|
|
44
|
+
|
|
45
|
+
Returns
|
|
46
|
+
-------
|
|
47
|
+
beam_boundary dict
|
|
48
|
+
"""
|
|
49
|
+
boundaries = dict()
|
|
50
|
+
for beam in range(1, self.constants.num_beams + 1):
|
|
51
|
+
boundaries[beam] = self.intermediate_frame_load_beam_boundaries(beam=beam)
|
|
52
|
+
return boundaries
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Helper for CRYO-Nirsp array corrections."""
|
|
2
|
+
from collections.abc import Generator
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import scipy.ndimage as spnd
|
|
7
|
+
from dkist_processing_math.transform import affine_transform_arrays
|
|
8
|
+
from dkist_processing_math.transform import rotate_arrays_about_point
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CorrectionsMixin:
|
|
12
|
+
"""Mixin to provide support for various array corrections used by the workflow tasks."""
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def corrections_correct_geometry(
|
|
16
|
+
arrays: Iterable[np.ndarray] | np.ndarray,
|
|
17
|
+
shift: np.ndarray = np.zeros(2),
|
|
18
|
+
angle: float = 0.0,
|
|
19
|
+
) -> Generator[np.ndarray, None, None]:
|
|
20
|
+
"""
|
|
21
|
+
Shift and then rotate data.
|
|
22
|
+
|
|
23
|
+
It applies the inverse of the given shift and angle.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
arrays
|
|
28
|
+
2D array(s) containing the data for the un-shifted beam
|
|
29
|
+
|
|
30
|
+
shift
|
|
31
|
+
The measured shift offset in the spectral dimension
|
|
32
|
+
between beams or between modulator states in a single beam.
|
|
33
|
+
|
|
34
|
+
angle
|
|
35
|
+
The angle between the slit and pixel axes.
|
|
36
|
+
|
|
37
|
+
Returns
|
|
38
|
+
-------
|
|
39
|
+
Generator
|
|
40
|
+
2D array(s) containing the data of the rotated and shifted beam
|
|
41
|
+
"""
|
|
42
|
+
arrays = [arrays] if isinstance(arrays, np.ndarray) else arrays
|
|
43
|
+
for array in arrays:
|
|
44
|
+
# Need to recast type to fix endianness issue caused by `fits`
|
|
45
|
+
array = array.astype(np.float64)
|
|
46
|
+
array[np.where(array == np.inf)] = np.max(array[np.isfinite(array)])
|
|
47
|
+
array[np.where(array == -np.inf)] = np.min(array[np.isfinite(array)])
|
|
48
|
+
array[np.isnan(array)] = np.nanmedian(array)
|
|
49
|
+
translated = affine_transform_arrays(array, translation=-shift, mode="edge", order=1)
|
|
50
|
+
# rotate_arrays_about_point rotates the wrong way, so no negative sign here
|
|
51
|
+
yield next(rotate_arrays_about_point(translated, angle=angle, mode="edge", order=1))
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def corrections_distort_geometry(
|
|
55
|
+
arrays: Iterable[np.ndarray] | np.ndarray,
|
|
56
|
+
shift: np.ndarray = np.zeros(2),
|
|
57
|
+
angle: float = 0.0,
|
|
58
|
+
) -> Generator[np.ndarray, None, None]:
|
|
59
|
+
"""
|
|
60
|
+
Rotate and then shift data.
|
|
61
|
+
|
|
62
|
+
It applies the inverse of the given shift and angle.
|
|
63
|
+
|
|
64
|
+
Parameters
|
|
65
|
+
----------
|
|
66
|
+
arrays
|
|
67
|
+
2D array(s) containing the data for the un-shifted beam
|
|
68
|
+
|
|
69
|
+
shift
|
|
70
|
+
The measured shift offset in the spectral dimension
|
|
71
|
+
between beams or between modulator states in a single beam.
|
|
72
|
+
|
|
73
|
+
angle
|
|
74
|
+
The angle between the slit and pixel axes.
|
|
75
|
+
|
|
76
|
+
Returns
|
|
77
|
+
-------
|
|
78
|
+
Generator
|
|
79
|
+
2D array(s) containing the data of the rotated and shifted beam
|
|
80
|
+
"""
|
|
81
|
+
arrays = [arrays] if isinstance(arrays, np.ndarray) else arrays
|
|
82
|
+
for array in arrays:
|
|
83
|
+
# Need to recast type to fix endianness issue caused by `fits`
|
|
84
|
+
array = array.astype(np.float64)
|
|
85
|
+
array[np.where(array == np.inf)] = np.max(array[np.isfinite(array)])
|
|
86
|
+
array[np.where(array == -np.inf)] = np.min(array[np.isfinite(array)])
|
|
87
|
+
array[np.isnan(array)] = np.nanmedian(array)
|
|
88
|
+
# rotate_arrays_about_point rotates the wrong way, so no negative sign here
|
|
89
|
+
rotated = rotate_arrays_about_point(array, angle=angle, mode="edge")
|
|
90
|
+
yield next(affine_transform_arrays(rotated, translation=-shift, mode="edge"))
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
def corrections_remove_spec_shifts(
|
|
94
|
+
arrays: Iterable[np.ndarray] | np.ndarray, spec_shift: np.ndarray
|
|
95
|
+
) -> Generator[np.ndarray, None, None]:
|
|
96
|
+
"""
|
|
97
|
+
Remove spectral curvature.
|
|
98
|
+
|
|
99
|
+
This is a pretty simple function that simply undoes the computed spectral shifts.
|
|
100
|
+
|
|
101
|
+
Parameters
|
|
102
|
+
----------
|
|
103
|
+
arrays
|
|
104
|
+
2D array(s) containing the data for the un-distorted beam
|
|
105
|
+
|
|
106
|
+
spec_shift : np.ndarray
|
|
107
|
+
Array with shape (X), where X is the number of pixels in the spatial dimension.
|
|
108
|
+
This dimension gives the spectral shift.
|
|
109
|
+
|
|
110
|
+
Returns
|
|
111
|
+
-------
|
|
112
|
+
Generator
|
|
113
|
+
2D array(s) containing the data of the corrected beam
|
|
114
|
+
|
|
115
|
+
"""
|
|
116
|
+
arrays = [arrays] if isinstance(arrays, np.ndarray) else arrays
|
|
117
|
+
for array in arrays:
|
|
118
|
+
numx = array.shape[0]
|
|
119
|
+
array_output = np.zeros(array.shape)
|
|
120
|
+
for j in range(numx):
|
|
121
|
+
array_output[j, :] = spnd.shift(array[j, :], -spec_shift[j], mode="nearest")
|
|
122
|
+
yield array_output
|
|
123
|
+
|
|
124
|
+
def corrections_correct_bad_pixels(
|
|
125
|
+
self,
|
|
126
|
+
array_to_fix: np.ndarray,
|
|
127
|
+
bad_pixel_map: np.ndarray,
|
|
128
|
+
) -> np.ndarray:
|
|
129
|
+
"""
|
|
130
|
+
Correct bad pixels in an array using a median filter.
|
|
131
|
+
|
|
132
|
+
Corrects only the bad pixels identified by the bad pixel map.
|
|
133
|
+
|
|
134
|
+
If enough pixels are found to suggest readout errors (> a given fraction of the array),
|
|
135
|
+
a global replacement value is used instead.
|
|
136
|
+
|
|
137
|
+
Parameters
|
|
138
|
+
----------
|
|
139
|
+
array_to_fix
|
|
140
|
+
The array to be corrected
|
|
141
|
+
|
|
142
|
+
bad_pixel_map
|
|
143
|
+
The bad_pixel_map to use for the correction
|
|
144
|
+
An array of zeros with bad pixel locations set to 1
|
|
145
|
+
|
|
146
|
+
Returns
|
|
147
|
+
-------
|
|
148
|
+
ndarray
|
|
149
|
+
The corrected array
|
|
150
|
+
"""
|
|
151
|
+
bad_pixel_fraction = np.sum(bad_pixel_map) / bad_pixel_map.size
|
|
152
|
+
if bad_pixel_fraction > self.parameters.corrections_bad_pixel_fraction_threshold:
|
|
153
|
+
global_median = np.nanmedian(array_to_fix)
|
|
154
|
+
array_to_fix[bad_pixel_map == 1] = global_median
|
|
155
|
+
return array_to_fix
|
|
156
|
+
|
|
157
|
+
kernel_size = self.parameters.corrections_bad_pixel_median_filter_size
|
|
158
|
+
half_kernel_size = kernel_size // 2
|
|
159
|
+
|
|
160
|
+
masked_array = np.ma.array(array_to_fix, mask=bad_pixel_map)
|
|
161
|
+
|
|
162
|
+
y_locs, x_locs = np.nonzero(bad_pixel_map)
|
|
163
|
+
num_y = bad_pixel_map.shape[0]
|
|
164
|
+
num_x = bad_pixel_map.shape[1]
|
|
165
|
+
for x, y in zip(x_locs, y_locs):
|
|
166
|
+
y_slice = slice(max(y - half_kernel_size, 0), min(y + half_kernel_size + 1, num_y))
|
|
167
|
+
x_slice = slice(max(x - half_kernel_size, 0), min(x + half_kernel_size + 1, num_x))
|
|
168
|
+
|
|
169
|
+
if self.constants.arm_id == "SP":
|
|
170
|
+
# Only compute median in spatial dimension so we don't blur things spectrally
|
|
171
|
+
med = np.ma.median(masked_array[y_slice, x])
|
|
172
|
+
else:
|
|
173
|
+
med = np.ma.median(masked_array[y_slice, x_slice])
|
|
174
|
+
|
|
175
|
+
masked_array[y, x] = med
|
|
176
|
+
|
|
177
|
+
return masked_array.data
|