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,511 @@
|
|
|
1
|
+
"""Base frameworks for Cryonirsp science calibration."""
|
|
2
|
+
from abc import ABC
|
|
3
|
+
from abc import abstractmethod
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from astropy.io import fits
|
|
9
|
+
from astropy.time import Time
|
|
10
|
+
from astropy.time import TimeDelta
|
|
11
|
+
from dkist_processing_common.codecs.fits import fits_hdulist_encoder
|
|
12
|
+
from dkist_processing_common.models.task_name import TaskName
|
|
13
|
+
from dkist_processing_math.arithmetic import divide_arrays_by_array
|
|
14
|
+
from dkist_processing_math.arithmetic import subtract_array_from_arrays
|
|
15
|
+
from dkist_processing_math.statistics import average_numpy_arrays
|
|
16
|
+
from dkist_processing_pac.optics.telescope import Telescope
|
|
17
|
+
from dkist_service_configuration.logging import logger
|
|
18
|
+
|
|
19
|
+
from dkist_processing_cryonirsp.models.exposure_conditions import ExposureConditions
|
|
20
|
+
from dkist_processing_cryonirsp.models.tags import CryonirspTag
|
|
21
|
+
from dkist_processing_cryonirsp.parsers.cryonirsp_l0_fits_access import CryonirspL0FitsAccess
|
|
22
|
+
from dkist_processing_cryonirsp.tasks.cryonirsp_base import CryonirspTaskBase
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class CalibrationCollection:
|
|
27
|
+
"""Dataclass to hold all calibration objects and allow for easy, property-based access."""
|
|
28
|
+
|
|
29
|
+
dark: dict
|
|
30
|
+
solar_gain: dict
|
|
31
|
+
angle: dict | None
|
|
32
|
+
state_offset: dict | None
|
|
33
|
+
spec_shift: dict | None
|
|
34
|
+
demod_matrices: dict | None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ScienceCalibrationBase(CryonirspTaskBase, ABC):
|
|
38
|
+
"""
|
|
39
|
+
Task class for Cryonirsp science calibration of polarized and non-polarized data.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
recipe_run_id : int
|
|
44
|
+
id of the recipe run used to identify the workflow run this task is part of
|
|
45
|
+
workflow_name : str
|
|
46
|
+
name of the workflow to which this instance of the task belongs
|
|
47
|
+
workflow_version : str
|
|
48
|
+
version of the workflow to which this instance of the task belongs
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
record_provenance = True
|
|
52
|
+
|
|
53
|
+
def run(self):
|
|
54
|
+
"""
|
|
55
|
+
Run Cryonirsp science calibration.
|
|
56
|
+
|
|
57
|
+
- Collect all calibration objects
|
|
58
|
+
- Calibrate and write all frames
|
|
59
|
+
- Record quality metrics
|
|
60
|
+
|
|
61
|
+
Returns
|
|
62
|
+
-------
|
|
63
|
+
None
|
|
64
|
+
|
|
65
|
+
"""
|
|
66
|
+
with self.apm_task_step("Loading calibration objects"):
|
|
67
|
+
calibrations = self.collect_calibration_objects()
|
|
68
|
+
|
|
69
|
+
with self.apm_task_step(
|
|
70
|
+
f"Calibrating Science Frames for "
|
|
71
|
+
f"{self.constants.num_map_scans} map scans and "
|
|
72
|
+
f"{self.constants.num_scan_steps} scan steps"
|
|
73
|
+
):
|
|
74
|
+
self.calibrate_and_write_frames(calibrations=calibrations)
|
|
75
|
+
|
|
76
|
+
with self.apm_processing_step("Computing and logging quality metrics"):
|
|
77
|
+
no_of_raw_science_frames: int = self.scratch.count_all(
|
|
78
|
+
tags=[
|
|
79
|
+
CryonirspTag.linearized(),
|
|
80
|
+
CryonirspTag.frame(),
|
|
81
|
+
CryonirspTag.task_observe(),
|
|
82
|
+
],
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
self.quality_store_task_type_counts(
|
|
86
|
+
task_type=TaskName.observe.value, total_frames=no_of_raw_science_frames
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
@abstractmethod
|
|
90
|
+
def calibrate_and_write_frames(self, calibrations: CalibrationCollection):
|
|
91
|
+
"""Fully calibrate science data and tag the results with CALIBRATED."""
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
@abstractmethod
|
|
95
|
+
def collect_calibration_objects(self) -> CalibrationCollection:
|
|
96
|
+
"""
|
|
97
|
+
Abstract method to be implemented in subclass.
|
|
98
|
+
|
|
99
|
+
Collect *all* calibration for all modstates, and exposure times.
|
|
100
|
+
|
|
101
|
+
Doing this once here prevents lots of reads as we reduce the science data.
|
|
102
|
+
"""
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
def apply_basic_corrections(
|
|
106
|
+
self,
|
|
107
|
+
beam: int,
|
|
108
|
+
modstate: int,
|
|
109
|
+
meas_num: int,
|
|
110
|
+
scan_step: int,
|
|
111
|
+
map_scan: int,
|
|
112
|
+
exposure_conditions: ExposureConditions,
|
|
113
|
+
calibrations: CalibrationCollection,
|
|
114
|
+
) -> tuple[np.ndarray, fits.Header]:
|
|
115
|
+
"""
|
|
116
|
+
Apply basic corrections to a single frame.
|
|
117
|
+
|
|
118
|
+
Generally the algorithm is:
|
|
119
|
+
1. Dark correct the array
|
|
120
|
+
2. Solar Gain correct the array
|
|
121
|
+
|
|
122
|
+
Parameters
|
|
123
|
+
----------
|
|
124
|
+
modstate
|
|
125
|
+
The modulator state for this single step
|
|
126
|
+
scan_step
|
|
127
|
+
The slit step for this single step
|
|
128
|
+
map_scan
|
|
129
|
+
The current map scan
|
|
130
|
+
exposure_conditions
|
|
131
|
+
The exposure conditions for this single step
|
|
132
|
+
calibrations
|
|
133
|
+
Collection of all calibration objects
|
|
134
|
+
|
|
135
|
+
Returns
|
|
136
|
+
-------
|
|
137
|
+
Corrected array, header
|
|
138
|
+
"""
|
|
139
|
+
# Extract calibrations
|
|
140
|
+
dark_array = calibrations.dark[CryonirspTag.beam(beam)][
|
|
141
|
+
CryonirspTag.exposure_conditions(exposure_conditions)
|
|
142
|
+
]
|
|
143
|
+
gain_array = calibrations.solar_gain[CryonirspTag.beam(beam)]
|
|
144
|
+
|
|
145
|
+
# Grab the input observe frame(s)
|
|
146
|
+
observe_object_list = list(
|
|
147
|
+
self.linearized_frame_observe_fits_access_generator(
|
|
148
|
+
beam=beam,
|
|
149
|
+
map_scan=map_scan,
|
|
150
|
+
scan_step=scan_step,
|
|
151
|
+
meas_num=meas_num,
|
|
152
|
+
modstate=modstate,
|
|
153
|
+
exposure_conditions=exposure_conditions,
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
# There can be more than 1 frame if there are sub-repeats
|
|
157
|
+
observe_fits_access = sorted(
|
|
158
|
+
[item for item in observe_object_list],
|
|
159
|
+
key=lambda x: x.time_obs,
|
|
160
|
+
)
|
|
161
|
+
observe_arrays = [item.data for item in observe_fits_access]
|
|
162
|
+
observe_headers = [item.header for item in observe_fits_access]
|
|
163
|
+
|
|
164
|
+
# Average over sub-repeats, if there are any
|
|
165
|
+
avg_observe_array = average_numpy_arrays(observe_arrays)
|
|
166
|
+
# Get the header for this frame
|
|
167
|
+
observe_header = observe_headers[0]
|
|
168
|
+
|
|
169
|
+
# Dark correction
|
|
170
|
+
dark_corrected_array = next(subtract_array_from_arrays(avg_observe_array, dark_array))
|
|
171
|
+
|
|
172
|
+
# Bad pixel correction
|
|
173
|
+
bad_pixel_map = self.intermediate_frame_load_bad_pixel_map(beam=beam)
|
|
174
|
+
bad_pixel_corrected_array = self.corrections_correct_bad_pixels(
|
|
175
|
+
dark_corrected_array, bad_pixel_map
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Gain correction
|
|
179
|
+
gain_corrected_array = next(divide_arrays_by_array(bad_pixel_corrected_array, gain_array))
|
|
180
|
+
|
|
181
|
+
return gain_corrected_array, observe_header
|
|
182
|
+
|
|
183
|
+
def correct_and_demodulate(
|
|
184
|
+
self,
|
|
185
|
+
beam: int,
|
|
186
|
+
meas_num: int,
|
|
187
|
+
scan_step: int,
|
|
188
|
+
map_scan: int,
|
|
189
|
+
exposure_conditions: ExposureConditions,
|
|
190
|
+
calibrations: CalibrationCollection,
|
|
191
|
+
) -> tuple[np.ndarray, fits.Header]:
|
|
192
|
+
"""
|
|
193
|
+
Process and demodulate a single collection of modulation state data.
|
|
194
|
+
|
|
195
|
+
- Apply dark and gain corrections
|
|
196
|
+
- Demodulate
|
|
197
|
+
"""
|
|
198
|
+
# Create the 3D stack of corrected modulated arrays
|
|
199
|
+
array_shape = calibrations.dark[CryonirspTag.beam(1)][
|
|
200
|
+
CryonirspTag.exposure_conditions(exposure_conditions)
|
|
201
|
+
].shape
|
|
202
|
+
array_stack = np.zeros(array_shape + (self.constants.num_modstates,))
|
|
203
|
+
header_stack = []
|
|
204
|
+
|
|
205
|
+
with self.apm_processing_step(f"Correcting {self.constants.num_modstates} modstates"):
|
|
206
|
+
for modstate in range(1, self.constants.num_modstates + 1):
|
|
207
|
+
# Correct the arrays
|
|
208
|
+
corrected_array, corrected_header = self.apply_basic_corrections(
|
|
209
|
+
beam=beam,
|
|
210
|
+
modstate=modstate,
|
|
211
|
+
meas_num=meas_num,
|
|
212
|
+
scan_step=scan_step,
|
|
213
|
+
map_scan=map_scan,
|
|
214
|
+
exposure_conditions=exposure_conditions,
|
|
215
|
+
calibrations=calibrations,
|
|
216
|
+
)
|
|
217
|
+
# Add this result to the 3D stack
|
|
218
|
+
array_stack[:, :, modstate - 1] = corrected_array
|
|
219
|
+
header_stack.append(corrected_header)
|
|
220
|
+
|
|
221
|
+
with self.apm_processing_step("Applying instrument polarization correction"):
|
|
222
|
+
intermediate_array = self.polarization_correction(
|
|
223
|
+
array_stack, calibrations.demod_matrices[CryonirspTag.beam(beam)]
|
|
224
|
+
)
|
|
225
|
+
intermediate_header = self.compute_date_keys(header_stack)
|
|
226
|
+
|
|
227
|
+
return intermediate_array, intermediate_header
|
|
228
|
+
|
|
229
|
+
@staticmethod
|
|
230
|
+
def polarization_correction(array_stack: np.ndarray, demod_matrices: np.ndarray) -> np.ndarray:
|
|
231
|
+
"""
|
|
232
|
+
Apply a polarization correction to an array by multiplying the array stack by the demod matrices.
|
|
233
|
+
|
|
234
|
+
Parameters
|
|
235
|
+
----------
|
|
236
|
+
array_stack : np.ndarray
|
|
237
|
+
(x, y, M) stack of corrected arrays with M modulation states
|
|
238
|
+
|
|
239
|
+
demod_matrices : np.ndarray
|
|
240
|
+
(x, y, 4, M) stack of demodulation matrices with 4 stokes planes and M modulation states
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
Returns
|
|
244
|
+
-------
|
|
245
|
+
np.ndarray
|
|
246
|
+
(x, y, 4) ndarray with the planes being IQUV
|
|
247
|
+
"""
|
|
248
|
+
demodulated_array = np.sum(demod_matrices * array_stack[:, :, None, :], axis=3)
|
|
249
|
+
return demodulated_array
|
|
250
|
+
|
|
251
|
+
def telescope_polarization_correction(
|
|
252
|
+
self,
|
|
253
|
+
inst_demod_obj: CryonirspL0FitsAccess,
|
|
254
|
+
) -> CryonirspL0FitsAccess:
|
|
255
|
+
"""
|
|
256
|
+
Apply a telescope polarization correction.
|
|
257
|
+
|
|
258
|
+
Parameters
|
|
259
|
+
----------
|
|
260
|
+
inst_demod_obj
|
|
261
|
+
A demodulated, beam averaged frame
|
|
262
|
+
|
|
263
|
+
Returns
|
|
264
|
+
-------
|
|
265
|
+
FitsAccess object with telescope corrections applied
|
|
266
|
+
"""
|
|
267
|
+
tm = Telescope.from_fits_access(inst_demod_obj)
|
|
268
|
+
mueller_matrix = tm.generate_inverse_telescope_model(
|
|
269
|
+
M12=True, rotate_to_fixed_SDO_HINODE_polarized_frame=True, swap_UV_signs=True
|
|
270
|
+
)
|
|
271
|
+
inst_demod_obj.data = self.polarization_correction(inst_demod_obj.data, mueller_matrix)
|
|
272
|
+
return inst_demod_obj
|
|
273
|
+
|
|
274
|
+
def write_calibrated_object(
|
|
275
|
+
self,
|
|
276
|
+
calibrated_object: CryonirspL0FitsAccess,
|
|
277
|
+
map_scan: int,
|
|
278
|
+
scan_step: int,
|
|
279
|
+
meas_num: int,
|
|
280
|
+
) -> None:
|
|
281
|
+
"""
|
|
282
|
+
Write out calibrated science frames.
|
|
283
|
+
|
|
284
|
+
For polarized data write out calibrated science frames for all 4 Stokes parameters.
|
|
285
|
+
For non-polarized data write out calibrated science frames for Stokes I only.
|
|
286
|
+
|
|
287
|
+
Parameters
|
|
288
|
+
----------
|
|
289
|
+
calibrated_object
|
|
290
|
+
Corrected frames object
|
|
291
|
+
|
|
292
|
+
map_scan
|
|
293
|
+
The current map scan. Needed because it's not a header key
|
|
294
|
+
|
|
295
|
+
scan_step
|
|
296
|
+
The current scan step
|
|
297
|
+
|
|
298
|
+
meas_num
|
|
299
|
+
The current measurement number
|
|
300
|
+
"""
|
|
301
|
+
final_header = self.update_calibrated_header(calibrated_object.header, map_scan=map_scan)
|
|
302
|
+
if self.constants.correct_for_polarization:
|
|
303
|
+
stokes_I_data = calibrated_object.data[:, :, 0]
|
|
304
|
+
for s, stokes_param in enumerate(self.constants.stokes_params):
|
|
305
|
+
stokes_data = calibrated_object.data[:, :, s]
|
|
306
|
+
final_data = self.re_dummy_data(stokes_data)
|
|
307
|
+
pol_header = self.add_L1_pol_headers(final_header, stokes_data, stokes_I_data)
|
|
308
|
+
self.write_calibrated_array(
|
|
309
|
+
data=final_data,
|
|
310
|
+
header=pol_header,
|
|
311
|
+
stokes=stokes_param,
|
|
312
|
+
meas_num=meas_num,
|
|
313
|
+
scan_step=scan_step,
|
|
314
|
+
map_scan=map_scan,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
else:
|
|
318
|
+
final_data = self.re_dummy_data(calibrated_object.data[:, :, 0])
|
|
319
|
+
self.write_calibrated_array(
|
|
320
|
+
data=final_data,
|
|
321
|
+
header=final_header,
|
|
322
|
+
stokes="I",
|
|
323
|
+
meas_num=meas_num,
|
|
324
|
+
scan_step=scan_step,
|
|
325
|
+
map_scan=map_scan,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
@staticmethod
|
|
329
|
+
def wrap_array_and_header_in_fits_access(
|
|
330
|
+
array: np.ndarray, header: fits.Header
|
|
331
|
+
) -> CryonirspL0FitsAccess:
|
|
332
|
+
"""Wrap input array and header in a CryonirspL0FitsAccess object."""
|
|
333
|
+
hdu = fits.ImageHDU(data=array, header=header)
|
|
334
|
+
obj = CryonirspL0FitsAccess(hdu=hdu, auto_squeeze=False)
|
|
335
|
+
|
|
336
|
+
return obj
|
|
337
|
+
|
|
338
|
+
@staticmethod
|
|
339
|
+
def add_stokes_dimension_to_intensity_only_array(array: np.ndarray) -> np.ndarray:
|
|
340
|
+
"""
|
|
341
|
+
Add a length-1 dimension to the end of an array.
|
|
342
|
+
|
|
343
|
+
We do this so code that loops over the Stokes dimension still work with I-only data.
|
|
344
|
+
"""
|
|
345
|
+
return array[..., None]
|
|
346
|
+
|
|
347
|
+
@staticmethod
|
|
348
|
+
def compute_date_keys(headers: Iterable[fits.Header] | fits.Header) -> fits.Header:
|
|
349
|
+
"""
|
|
350
|
+
Generate correct DATE-??? header keys from a set of input headers.
|
|
351
|
+
|
|
352
|
+
Keys are computed thusly:
|
|
353
|
+
* DATE-BEG - The (Spec-0122) DATE-OBS of the earliest input header
|
|
354
|
+
* DATE-END - The (Spec-0122) DATE-OBS of the latest input header, plus the FPA exposure time
|
|
355
|
+
|
|
356
|
+
Returns
|
|
357
|
+
-------
|
|
358
|
+
fits.Header
|
|
359
|
+
A copy of the earliest header, but with correct DATE-??? keys
|
|
360
|
+
"""
|
|
361
|
+
if isinstance(headers, fits.Header) or isinstance(
|
|
362
|
+
headers, fits.hdu.compressed.CompImageHeader
|
|
363
|
+
):
|
|
364
|
+
headers = [headers]
|
|
365
|
+
|
|
366
|
+
sorted_obj_list = sorted(
|
|
367
|
+
[CryonirspL0FitsAccess.from_header(h) for h in headers], key=lambda x: Time(x.time_obs)
|
|
368
|
+
)
|
|
369
|
+
date_beg = sorted_obj_list[0].time_obs
|
|
370
|
+
exp_time = TimeDelta(sorted_obj_list[-1].fpa_exposure_time_ms / 1000.0, format="sec")
|
|
371
|
+
date_end = (Time(sorted_obj_list[-1].time_obs) + exp_time).isot
|
|
372
|
+
|
|
373
|
+
header = sorted_obj_list[0].header
|
|
374
|
+
header["DATE-BEG"] = date_beg
|
|
375
|
+
header["DATE-END"] = date_end
|
|
376
|
+
|
|
377
|
+
return header
|
|
378
|
+
|
|
379
|
+
def re_dummy_data(self, data: np.ndarray):
|
|
380
|
+
"""
|
|
381
|
+
Add the dummy dimension that we have been secretly squeezing out during processing.
|
|
382
|
+
|
|
383
|
+
The dummy dimension is required because its corresponding WCS axis contains important information.
|
|
384
|
+
|
|
385
|
+
Parameters
|
|
386
|
+
----------
|
|
387
|
+
data : np.ndarray
|
|
388
|
+
Corrected data
|
|
389
|
+
"""
|
|
390
|
+
logger.info(f"Adding dummy WCS dimension to array with shape {data.shape}")
|
|
391
|
+
return data[None, :, :]
|
|
392
|
+
|
|
393
|
+
def update_calibrated_header(self, header: fits.Header, map_scan: int) -> fits.Header:
|
|
394
|
+
"""
|
|
395
|
+
Update calibrated headers with any information gleaned during science calibration.
|
|
396
|
+
|
|
397
|
+
Right now all this does is put map scan values in the header.
|
|
398
|
+
|
|
399
|
+
Parameters
|
|
400
|
+
----------
|
|
401
|
+
header
|
|
402
|
+
The header to update
|
|
403
|
+
|
|
404
|
+
map_scan
|
|
405
|
+
Current map scan
|
|
406
|
+
|
|
407
|
+
Returns
|
|
408
|
+
-------
|
|
409
|
+
fits.Header
|
|
410
|
+
"""
|
|
411
|
+
# Correct the headers for the number of map_scans due to potential observation aborts
|
|
412
|
+
header["CNNMAPS"] = self.constants.num_map_scans
|
|
413
|
+
header["CNMAP"] = map_scan
|
|
414
|
+
|
|
415
|
+
return header
|
|
416
|
+
|
|
417
|
+
def add_L1_pol_headers(
|
|
418
|
+
self, input_header: fits.Header, stokes_data: np.ndarray, stokes_I_data: np.ndarray
|
|
419
|
+
) -> fits.Header:
|
|
420
|
+
"""Compute and add 214 header values specific to polarimetric datasets."""
|
|
421
|
+
# Probably not needed, but just to be safe
|
|
422
|
+
output_header = input_header.copy()
|
|
423
|
+
|
|
424
|
+
pol_noise = self.compute_polarimetric_noise(stokes_data, stokes_I_data)
|
|
425
|
+
pol_sensitivity = self.compute_polarimetric_sensitivity(stokes_I_data)
|
|
426
|
+
output_header["POL_NOIS"] = pol_noise
|
|
427
|
+
output_header["POL_SENS"] = pol_sensitivity
|
|
428
|
+
|
|
429
|
+
return output_header
|
|
430
|
+
|
|
431
|
+
def compute_polarimetric_noise(
|
|
432
|
+
self, stokes_data: np.ndarray, stokes_I_data: np.ndarray
|
|
433
|
+
) -> float:
|
|
434
|
+
r"""
|
|
435
|
+
Compute the polarimetric noise for a single frame.
|
|
436
|
+
|
|
437
|
+
The polarimetric noise, :math:`N`, is defined as
|
|
438
|
+
|
|
439
|
+
.. math::
|
|
440
|
+
|
|
441
|
+
N = stddev(\frac{F_i}{F_I})
|
|
442
|
+
|
|
443
|
+
where :math:`F_i` is a full array of values for Stokes parameter :math:`i` (I, Q, U, V), and :math:`F_I` is the
|
|
444
|
+
full frame of Stokes-I. The stddev is computed across the entire frame.
|
|
445
|
+
"""
|
|
446
|
+
return float(np.nanstd(stokes_data / stokes_I_data))
|
|
447
|
+
|
|
448
|
+
def compute_polarimetric_sensitivity(self, stokes_I_data: np.ndarray) -> float:
|
|
449
|
+
r"""
|
|
450
|
+
Compute the polarimetric sensitivity for a single frame.
|
|
451
|
+
|
|
452
|
+
The polarimetric sensitivity is the smallest signal that can be measured based on the values in the Stokes-I
|
|
453
|
+
frame. The sensitivity, :math:`S`, is computed as
|
|
454
|
+
|
|
455
|
+
.. math::
|
|
456
|
+
|
|
457
|
+
S = \frac{1}{\sqrt{\mathrm{max}(F_I)}}
|
|
458
|
+
|
|
459
|
+
where :math:`F_I` is the full frame of values for Stokes-I.
|
|
460
|
+
"""
|
|
461
|
+
return float(1.0 / np.sqrt(np.nanmax(stokes_I_data)))
|
|
462
|
+
|
|
463
|
+
def write_calibrated_array(
|
|
464
|
+
self,
|
|
465
|
+
data: np.ndarray,
|
|
466
|
+
header: fits.Header,
|
|
467
|
+
stokes: str,
|
|
468
|
+
meas_num: int,
|
|
469
|
+
scan_step: int,
|
|
470
|
+
map_scan: int,
|
|
471
|
+
) -> None:
|
|
472
|
+
"""
|
|
473
|
+
Write out calibrated array.
|
|
474
|
+
|
|
475
|
+
Parameters
|
|
476
|
+
----------
|
|
477
|
+
data : np.ndarray
|
|
478
|
+
calibrated data to write out
|
|
479
|
+
|
|
480
|
+
header : fits.Header
|
|
481
|
+
calibrated header to write out
|
|
482
|
+
|
|
483
|
+
stokes : str
|
|
484
|
+
Stokes parameter of this step. 'I', 'Q', 'U', or 'V'
|
|
485
|
+
|
|
486
|
+
meas_num: int
|
|
487
|
+
The current measurement number
|
|
488
|
+
|
|
489
|
+
scan_step : int
|
|
490
|
+
The slit step for this step
|
|
491
|
+
|
|
492
|
+
map_scan : int
|
|
493
|
+
The current map scan
|
|
494
|
+
"""
|
|
495
|
+
tags = [
|
|
496
|
+
CryonirspTag.calibrated(),
|
|
497
|
+
CryonirspTag.frame(),
|
|
498
|
+
CryonirspTag.stokes(stokes),
|
|
499
|
+
CryonirspTag.meas_num(meas_num),
|
|
500
|
+
CryonirspTag.scan_step(scan_step),
|
|
501
|
+
CryonirspTag.map_scan(map_scan),
|
|
502
|
+
]
|
|
503
|
+
hdul = fits.HDUList([fits.PrimaryHDU(header=header, data=data)])
|
|
504
|
+
self.write(
|
|
505
|
+
data=hdul,
|
|
506
|
+
tags=tags,
|
|
507
|
+
encoder=fits_hdulist_encoder,
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
filename = next(self.read(tags=tags))
|
|
511
|
+
logger.info(f"Wrote calibrated frame for {tags = } to {filename}")
|