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,585 @@
|
|
|
1
|
+
"""Cryo SP geometric task."""
|
|
2
|
+
import math
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import peakutils as pku
|
|
6
|
+
import scipy.ndimage as spnd
|
|
7
|
+
from dkist_processing_common.models.task_name import TaskName
|
|
8
|
+
from dkist_processing_math.arithmetic import divide_arrays_by_array
|
|
9
|
+
from dkist_processing_math.arithmetic import subtract_array_from_arrays
|
|
10
|
+
from dkist_processing_math.statistics import average_numpy_arrays
|
|
11
|
+
from dkist_service_configuration.logging import logger
|
|
12
|
+
from scipy.optimize import minimize
|
|
13
|
+
|
|
14
|
+
from dkist_processing_cryonirsp.models.tags import CryonirspTag
|
|
15
|
+
from dkist_processing_cryonirsp.tasks.cryonirsp_base import CryonirspTaskBase
|
|
16
|
+
from dkist_processing_cryonirsp.tasks.mixin.shift_measurements import ShiftMeasurementsMixin
|
|
17
|
+
from dkist_processing_cryonirsp.tasks.mixin.shift_measurements import SPATIAL
|
|
18
|
+
from dkist_processing_cryonirsp.tasks.mixin.shift_measurements import SPECTRAL
|
|
19
|
+
|
|
20
|
+
__all__ = ["SPGeometricCalibration"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SPGeometricCalibration(CryonirspTaskBase, ShiftMeasurementsMixin):
|
|
24
|
+
"""Task class for computing the spectral geometry. Geometry is represented by three quantities.
|
|
25
|
+
|
|
26
|
+
- angle - The angle (in radians) between slit hairlines and pixel axes. A one dimensional array with two elements- one for each beam.
|
|
27
|
+
|
|
28
|
+
- beam offset - The [x, y] shift of beam 2 relative to beam 1 (the reference beam). Two beam offset values are computed.
|
|
29
|
+
|
|
30
|
+
- spectral shift - The shift in the spectral dimension for each beam for every spatial position needed to "straighten" the spectra so a single wavelength is at the same pixel for all slit positions.Task class for computing the spectral geometry for a SP CryoNIRSP calibration run.
|
|
31
|
+
|
|
32
|
+
Parameters
|
|
33
|
+
----------
|
|
34
|
+
recipe_run_id : int
|
|
35
|
+
id of the recipe run used to identify the workflow run this task is part of
|
|
36
|
+
workflow_name : str
|
|
37
|
+
name of the workflow to which this instance of the task belongs
|
|
38
|
+
workflow_version : str
|
|
39
|
+
version of the workflow to which this instance of the task belongs
|
|
40
|
+
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
record_provenance = True
|
|
44
|
+
|
|
45
|
+
def run(self):
|
|
46
|
+
"""
|
|
47
|
+
Run method for the task.
|
|
48
|
+
|
|
49
|
+
For each beam.
|
|
50
|
+
|
|
51
|
+
- Gather dark corrected frames
|
|
52
|
+
- Calculate spectral tilt (angle)
|
|
53
|
+
- Remove spectral tilt
|
|
54
|
+
- Using the angle corrected array, find the beam offset
|
|
55
|
+
- Write beam offset
|
|
56
|
+
- Calculate the spectral skew and curvature (spectral shifts)
|
|
57
|
+
- Write the spectral skew and curvature
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
Returns
|
|
61
|
+
-------
|
|
62
|
+
None
|
|
63
|
+
|
|
64
|
+
"""
|
|
65
|
+
# The basic corrections are done outside the loop structure below as it makes these loops much
|
|
66
|
+
# simpler than they would be otherwise. See the comments in do_basic_corrections for more details.
|
|
67
|
+
with self.apm_processing_step("Basic corrections"):
|
|
68
|
+
self.do_basic_corrections()
|
|
69
|
+
|
|
70
|
+
for beam in range(1, self.constants.num_beams + 1):
|
|
71
|
+
with self.apm_task_step(f"Generating geometric calibrations for {beam = }"):
|
|
72
|
+
with self.apm_processing_step(f"Computing and writing angle for {beam = }"):
|
|
73
|
+
angle = self.compute_beam_angle(beam=beam)
|
|
74
|
+
self.write_angle(angle=angle, beam=beam)
|
|
75
|
+
|
|
76
|
+
with self.apm_processing_step(f"Removing angle from {beam = }"):
|
|
77
|
+
angle_corr_array = self.remove_beam_angle(angle=angle, beam=beam)
|
|
78
|
+
|
|
79
|
+
with self.apm_processing_step(f"Computing offset for {beam = }"):
|
|
80
|
+
beam_offset = self.compute_offset(
|
|
81
|
+
array=angle_corr_array,
|
|
82
|
+
beam=beam,
|
|
83
|
+
)
|
|
84
|
+
self.write_beam_offset(offset=beam_offset, beam=beam)
|
|
85
|
+
|
|
86
|
+
with self.apm_processing_step(f"Removing offset for {beam = }"):
|
|
87
|
+
self.remove_beam_offset(
|
|
88
|
+
array=angle_corr_array,
|
|
89
|
+
offset=beam_offset,
|
|
90
|
+
beam=beam,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
with self.apm_processing_step(f"Computing spectral shifts for {beam = }"):
|
|
94
|
+
spec_shifts = self.compute_spectral_shifts(beam=beam)
|
|
95
|
+
|
|
96
|
+
with self.apm_writing_step(f"Writing spectral shifts for {beam = }"):
|
|
97
|
+
self.write_spectral_shifts(shifts=spec_shifts, beam=beam)
|
|
98
|
+
|
|
99
|
+
with self.apm_processing_step("Computing and logging quality metrics"):
|
|
100
|
+
no_of_raw_geo_frames: int = self.scratch.count_all(
|
|
101
|
+
tags=[
|
|
102
|
+
CryonirspTag.linearized(),
|
|
103
|
+
CryonirspTag.frame(),
|
|
104
|
+
CryonirspTag.task_solar_gain(),
|
|
105
|
+
],
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
self.quality_store_task_type_counts(
|
|
109
|
+
task_type=TaskName.geometric.value, total_frames=no_of_raw_geo_frames
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def basic_gain_corrected_data(self, beam: int) -> np.ndarray:
|
|
113
|
+
"""
|
|
114
|
+
Get dark and lamp gain corrected data array for a single beam.
|
|
115
|
+
|
|
116
|
+
Parameters
|
|
117
|
+
----------
|
|
118
|
+
beam : int
|
|
119
|
+
The current beam being processed
|
|
120
|
+
|
|
121
|
+
Returns
|
|
122
|
+
-------
|
|
123
|
+
np.ndarray
|
|
124
|
+
Dark corrected data array
|
|
125
|
+
"""
|
|
126
|
+
array_generator = self.intermediate_frame_load_intermediate_arrays(
|
|
127
|
+
tags=[CryonirspTag.task("GC_BASIC_GAIN_CORRECTED"), CryonirspTag.beam(beam)]
|
|
128
|
+
)
|
|
129
|
+
return average_numpy_arrays(array_generator)
|
|
130
|
+
|
|
131
|
+
def basic_dark_bp_corrected_data(self, beam: int) -> np.ndarray:
|
|
132
|
+
"""
|
|
133
|
+
Get dark and bad pixel corrected data array for a single beam.
|
|
134
|
+
|
|
135
|
+
Parameters
|
|
136
|
+
----------
|
|
137
|
+
beam : int
|
|
138
|
+
The current beam being processed
|
|
139
|
+
|
|
140
|
+
Returns
|
|
141
|
+
-------
|
|
142
|
+
np.ndarray
|
|
143
|
+
Dark and bad pixel corrected data array
|
|
144
|
+
"""
|
|
145
|
+
array_generator = self.intermediate_frame_load_intermediate_arrays(
|
|
146
|
+
tags=[CryonirspTag.task("GC_BASIC_DARK_BP_CORRECTED"), CryonirspTag.beam(beam)]
|
|
147
|
+
)
|
|
148
|
+
return average_numpy_arrays(array_generator)
|
|
149
|
+
|
|
150
|
+
def offset_corrected_data(self, beam: int) -> np.ndarray:
|
|
151
|
+
"""
|
|
152
|
+
Array for a single beam that has been corrected for the x/y beam offset.
|
|
153
|
+
|
|
154
|
+
Parameters
|
|
155
|
+
----------
|
|
156
|
+
beam
|
|
157
|
+
The current beam being processed
|
|
158
|
+
|
|
159
|
+
Returns
|
|
160
|
+
-------
|
|
161
|
+
np.ndarray
|
|
162
|
+
Offset corrected data array
|
|
163
|
+
"""
|
|
164
|
+
array_generator = self.intermediate_frame_load_intermediate_arrays(
|
|
165
|
+
tags=[CryonirspTag.task("GC_OFFSET"), CryonirspTag.beam(beam)]
|
|
166
|
+
)
|
|
167
|
+
return average_numpy_arrays(array_generator)
|
|
168
|
+
|
|
169
|
+
def do_basic_corrections(self):
|
|
170
|
+
"""Apply dark, bad pixel and lamp gain corrections to all data that will be used for Geometric Calibration."""
|
|
171
|
+
# There is likely only a single exposure conditions tuple in the list, but we iterate over the list
|
|
172
|
+
# in case there are multiple exposure conditions tuples. We also need a specific exposure conditions tag
|
|
173
|
+
# to ensure we get the proper dark arrays to use in the correction.
|
|
174
|
+
for exposure_conditions in self.constants.solar_gain_exposure_conditions_list:
|
|
175
|
+
for beam in range(1, self.constants.num_beams + 1):
|
|
176
|
+
logger.info(f"Starting basic reductions for {exposure_conditions = } and {beam = }")
|
|
177
|
+
try:
|
|
178
|
+
dark_array = self.intermediate_frame_load_dark_array(
|
|
179
|
+
beam=beam, exposure_conditions=exposure_conditions
|
|
180
|
+
)
|
|
181
|
+
except StopIteration as e:
|
|
182
|
+
raise ValueError(f"No matching dark found for {exposure_conditions = }") from e
|
|
183
|
+
|
|
184
|
+
lamp_gain_array = self.intermediate_frame_load_lamp_gain_array(
|
|
185
|
+
beam=beam,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
input_solar_arrays = self.linearized_frame_gain_array_generator(
|
|
189
|
+
gain_type=TaskName.solar_gain.value,
|
|
190
|
+
beam=beam,
|
|
191
|
+
exposure_conditions=exposure_conditions,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
avg_solar_array = average_numpy_arrays(input_solar_arrays)
|
|
195
|
+
|
|
196
|
+
dark_corrected_solar_array = next(
|
|
197
|
+
subtract_array_from_arrays(arrays=avg_solar_array, array_to_subtract=dark_array)
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
bad_pixel_map = self.intermediate_frame_load_bad_pixel_map(beam=beam)
|
|
201
|
+
bad_pixel_corrected_array = self.corrections_correct_bad_pixels(
|
|
202
|
+
dark_corrected_solar_array, bad_pixel_map
|
|
203
|
+
)
|
|
204
|
+
logger.info(f"Writing bad pixel corrected data for {beam=}")
|
|
205
|
+
self.intermediate_frame_write_arrays(
|
|
206
|
+
arrays=bad_pixel_corrected_array,
|
|
207
|
+
beam=beam,
|
|
208
|
+
task="GC_BASIC_DARK_BP_CORRECTED",
|
|
209
|
+
)
|
|
210
|
+
gain_corrected_solar_array = next(
|
|
211
|
+
divide_arrays_by_array(bad_pixel_corrected_array, lamp_gain_array)
|
|
212
|
+
)
|
|
213
|
+
logger.info(f"Writing gain corrected data for {beam=}")
|
|
214
|
+
self.intermediate_frame_write_arrays(
|
|
215
|
+
arrays=gain_corrected_solar_array,
|
|
216
|
+
beam=beam,
|
|
217
|
+
task="GC_BASIC_GAIN_CORRECTED",
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
def compute_beam_angle(self, beam: int) -> float:
|
|
221
|
+
"""
|
|
222
|
+
Compute the angle between dispersion and pixel axes for a given beam.
|
|
223
|
+
|
|
224
|
+
The algorithm works as follows:
|
|
225
|
+
|
|
226
|
+
1. Load the corrected solar array for this beam
|
|
227
|
+
2. Compute a gradient array by shifting the array along the spatial axis (along the slit) and
|
|
228
|
+
calculating a normalized finite difference with the original array.
|
|
229
|
+
3. Compute 2D slices for two strips that are on either side of the spectral center.
|
|
230
|
+
4. Extract the spatial strips as arrays and compute the median values along their spectral axis.
|
|
231
|
+
5. Compute the relative shift of the right strip to the left strip (this is the shift along the spatial axis)
|
|
232
|
+
6. Compute the angular rotation of the beam relative to the array axes from the shift
|
|
233
|
+
and the separation of the strips along the spectral axis
|
|
234
|
+
|
|
235
|
+
Returns
|
|
236
|
+
-------
|
|
237
|
+
The beam rotation angle in radians
|
|
238
|
+
"""
|
|
239
|
+
# Step 1
|
|
240
|
+
# Do not use a gain corrected image here, as it will cancel out the slit structure
|
|
241
|
+
# that is used for the shift measurement computations
|
|
242
|
+
gain_array = self.intermediate_frame_load_lamp_gain_array(beam=beam)
|
|
243
|
+
|
|
244
|
+
full_spatial_size, full_spectral_size = gain_array.shape
|
|
245
|
+
|
|
246
|
+
# Get the params for the strips
|
|
247
|
+
spectral_offset = math.ceil(
|
|
248
|
+
full_spectral_size * self.parameters.geo_strip_spectral_offset_size_fraction
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# Steps 2-5:
|
|
252
|
+
shift = self.shift_measurements_compute_shift_along_axis(
|
|
253
|
+
axis=SPATIAL,
|
|
254
|
+
array_1=gain_array,
|
|
255
|
+
array_2=gain_array,
|
|
256
|
+
array_1_offset=(0, -spectral_offset),
|
|
257
|
+
array_2_offset=(0, spectral_offset),
|
|
258
|
+
upsample_factor=self.parameters.geo_upsample_factor,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
logger.info(f"Measured shift of beam {beam} = {shift}")
|
|
262
|
+
|
|
263
|
+
# Step 6
|
|
264
|
+
beam_angle = np.arctan(shift / (2 * spectral_offset))
|
|
265
|
+
|
|
266
|
+
logger.info(f"Measured angle for beam {beam} = {np.rad2deg(beam_angle):0.3f} deg")
|
|
267
|
+
|
|
268
|
+
return beam_angle
|
|
269
|
+
|
|
270
|
+
def remove_beam_angle(self, angle: float, beam: int) -> np.ndarray:
|
|
271
|
+
"""
|
|
272
|
+
De-rotate the beam array using the measured angle to align the slit with the array axes.
|
|
273
|
+
|
|
274
|
+
Parameters
|
|
275
|
+
----------
|
|
276
|
+
angle : float
|
|
277
|
+
The measured beam rotation angle (in radians)
|
|
278
|
+
beam : int
|
|
279
|
+
The current beam being processed
|
|
280
|
+
|
|
281
|
+
Returns
|
|
282
|
+
-------
|
|
283
|
+
np.ndarray
|
|
284
|
+
The corrected array
|
|
285
|
+
"""
|
|
286
|
+
rotated_array = self.basic_gain_corrected_data(beam=beam)
|
|
287
|
+
corrected_array = next(self.corrections_correct_geometry(rotated_array, angle=angle))
|
|
288
|
+
return corrected_array
|
|
289
|
+
|
|
290
|
+
def compute_offset(self, array: np.ndarray, beam: int) -> np.ndarray:
|
|
291
|
+
"""
|
|
292
|
+
Higher-level helper function to compute the (x, y) offset between beams.
|
|
293
|
+
|
|
294
|
+
Sets beam 1 as the reference beam or computes the offset of beam 2 relative to beam 1.
|
|
295
|
+
|
|
296
|
+
Parameters
|
|
297
|
+
----------
|
|
298
|
+
array : np.ndarray
|
|
299
|
+
Beam data
|
|
300
|
+
beam : int
|
|
301
|
+
The current beam being processed
|
|
302
|
+
|
|
303
|
+
Returns
|
|
304
|
+
-------
|
|
305
|
+
np.ndarray
|
|
306
|
+
(x, y) offset between beams
|
|
307
|
+
"""
|
|
308
|
+
if beam == 1:
|
|
309
|
+
self.reference_array = array
|
|
310
|
+
return np.zeros(2)
|
|
311
|
+
|
|
312
|
+
spatial_shift = self.shift_measurements_compute_shift_along_axis(
|
|
313
|
+
SPATIAL,
|
|
314
|
+
self.reference_array,
|
|
315
|
+
array,
|
|
316
|
+
upsample_factor=self.parameters.geo_upsample_factor,
|
|
317
|
+
)
|
|
318
|
+
spectral_shift = self.shift_measurements_compute_shift_along_axis(
|
|
319
|
+
SPECTRAL,
|
|
320
|
+
self.reference_array,
|
|
321
|
+
array,
|
|
322
|
+
upsample_factor=self.parameters.geo_upsample_factor,
|
|
323
|
+
)
|
|
324
|
+
shift = np.array([spatial_shift, spectral_shift])
|
|
325
|
+
logger.info(f"Offset for {beam = } is {np.array2string(shift, precision=3)}")
|
|
326
|
+
return shift
|
|
327
|
+
|
|
328
|
+
def remove_beam_offset(self, array: np.ndarray, offset: np.ndarray, beam: int) -> None:
|
|
329
|
+
"""
|
|
330
|
+
Shift an array by some offset (to make it in line with the reference array).
|
|
331
|
+
|
|
332
|
+
Parameters
|
|
333
|
+
----------
|
|
334
|
+
array : np.ndarray
|
|
335
|
+
Beam data
|
|
336
|
+
offset : np.ndarray
|
|
337
|
+
The beam offset for the current beam
|
|
338
|
+
beam : int
|
|
339
|
+
The current beam being processed
|
|
340
|
+
|
|
341
|
+
Returns
|
|
342
|
+
-------
|
|
343
|
+
None
|
|
344
|
+
|
|
345
|
+
"""
|
|
346
|
+
corrected_array = next(self.corrections_correct_geometry(array, shift=offset))
|
|
347
|
+
self.intermediate_frame_write_arrays(arrays=corrected_array, beam=beam, task="GC_OFFSET")
|
|
348
|
+
|
|
349
|
+
def compute_spectral_shifts(self, beam: int) -> np.ndarray:
|
|
350
|
+
"""
|
|
351
|
+
Compute the spectral 'curvature'.
|
|
352
|
+
|
|
353
|
+
I.e., the spectral shift at each slit position needed to have wavelength be constant across a single spatial
|
|
354
|
+
pixel. Generally, the algorithm is:
|
|
355
|
+
|
|
356
|
+
1. Identify the reference array spectrum as the center of the slit
|
|
357
|
+
2. For each slit position, make an initial guess of the shift via correlation
|
|
358
|
+
3. Take the initial guesses and use them in a chisq minimizer to refine the shifts
|
|
359
|
+
4. Interpolate over those shifts identified as too large
|
|
360
|
+
5. Remove the mean shift so the total shift amount is minimized
|
|
361
|
+
|
|
362
|
+
Parameters
|
|
363
|
+
----------
|
|
364
|
+
beam
|
|
365
|
+
The current beam being processed
|
|
366
|
+
|
|
367
|
+
Returns
|
|
368
|
+
-------
|
|
369
|
+
np.ndarray
|
|
370
|
+
Spectral shift for a single beam
|
|
371
|
+
"""
|
|
372
|
+
logger.info(f"Computing spectral shifts for beam {beam}")
|
|
373
|
+
beam_array = self.offset_corrected_data(beam=beam)
|
|
374
|
+
spatial_size = beam_array.shape[0]
|
|
375
|
+
|
|
376
|
+
if beam == 1:
|
|
377
|
+
# Use the same reference spectrum for both beams.
|
|
378
|
+
# We pick the spectrum from the center of the slit, with a buffer of 10 pixels on either side
|
|
379
|
+
middle_row = spatial_size // 2
|
|
380
|
+
self.ref_spec = np.nanmedian(beam_array[middle_row - 10 : middle_row + 10, :], axis=0)
|
|
381
|
+
|
|
382
|
+
beam_shifts = np.empty(spatial_size) * np.nan
|
|
383
|
+
for i in range(spatial_size):
|
|
384
|
+
target_spec = beam_array[i, :]
|
|
385
|
+
|
|
386
|
+
initial_guess = self.compute_initial_spec_shift_guess(
|
|
387
|
+
ref_spec=self.ref_spec, target_spec=target_spec, beam=beam, pos=i
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
shift = self.compute_single_spec_shift(
|
|
391
|
+
ref_spec=self.ref_spec,
|
|
392
|
+
target_spec=target_spec,
|
|
393
|
+
initial_guess=initial_guess,
|
|
394
|
+
beam=beam,
|
|
395
|
+
pos=i,
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
beam_shifts[i] = shift
|
|
399
|
+
|
|
400
|
+
# Subtract the average so we shift my a minimal amount
|
|
401
|
+
if beam == 1:
|
|
402
|
+
# Use the same mean shift for both beams to avoid any relative shifts between the two.
|
|
403
|
+
self.mean_shifts = np.nanmean(beam_shifts)
|
|
404
|
+
logger.info(f"Mean of spectral shifts = {self.mean_shifts}")
|
|
405
|
+
|
|
406
|
+
beam_shifts -= self.mean_shifts
|
|
407
|
+
self.intermediate_frame_write_arrays(
|
|
408
|
+
arrays=beam_shifts,
|
|
409
|
+
beam=beam,
|
|
410
|
+
task="GC_RAW_SPECTRAL_SHIFTS",
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
# Finally, fit the shifts and return the resulting polynomial. Any "bad" fits were set to NaN and will be
|
|
414
|
+
# interpolated over.
|
|
415
|
+
poly_fit_order = self.parameters.geo_poly_fit_order
|
|
416
|
+
nan_idx = np.isnan(beam_shifts)
|
|
417
|
+
poly = np.poly1d(
|
|
418
|
+
np.polyfit(np.arange(spatial_size)[~nan_idx], beam_shifts[~nan_idx], poly_fit_order)
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
return poly(np.arange(spatial_size))
|
|
422
|
+
|
|
423
|
+
def compute_initial_spec_shift_guess(
|
|
424
|
+
self, *, ref_spec: np.ndarray, target_spec: np.ndarray, beam: int, pos: int
|
|
425
|
+
) -> float:
|
|
426
|
+
"""
|
|
427
|
+
Make a rough guess for the offset between two spectra.
|
|
428
|
+
|
|
429
|
+
A basic correlation is performed and the location of the peak sets the initial guess. If more than one strong
|
|
430
|
+
peak is found then the peak locations are averaged together.
|
|
431
|
+
"""
|
|
432
|
+
corr = np.correlate(
|
|
433
|
+
target_spec - np.nanmean(target_spec),
|
|
434
|
+
ref_spec - np.nanmean(ref_spec),
|
|
435
|
+
mode="same",
|
|
436
|
+
)
|
|
437
|
+
# Truncate the correlation to contain only allowable shifts
|
|
438
|
+
max_shift = self.parameters.geo_max_shift
|
|
439
|
+
mid_position = corr.size // 2
|
|
440
|
+
start = mid_position - max_shift
|
|
441
|
+
stop = mid_position + max_shift + 1
|
|
442
|
+
truncated_corr = corr[start:stop]
|
|
443
|
+
|
|
444
|
+
# This min_dist ensures we only find a single peak in each correlation signal
|
|
445
|
+
pidx = pku.indexes(truncated_corr, min_dist=truncated_corr.size)
|
|
446
|
+
initial_guess = 1 * (pidx - truncated_corr.size // 2)
|
|
447
|
+
|
|
448
|
+
# These edge-cases are very rare, but do happen sometimes
|
|
449
|
+
if initial_guess.size == 0:
|
|
450
|
+
logger.info(
|
|
451
|
+
f"Spatial position {pos} in {beam=} doesn't have a correlation peak. Initial guess set to 0"
|
|
452
|
+
)
|
|
453
|
+
initial_guess = 0.0
|
|
454
|
+
|
|
455
|
+
elif initial_guess.size > 1:
|
|
456
|
+
logger.info(
|
|
457
|
+
f"Spatial position {pos} in {beam=} has more than one correlation peak ({initial_guess}). Initial guess set to mean ({np.nanmean(initial_guess)})"
|
|
458
|
+
)
|
|
459
|
+
initial_guess = np.nanmean(initial_guess)
|
|
460
|
+
|
|
461
|
+
return initial_guess
|
|
462
|
+
|
|
463
|
+
def compute_single_spec_shift(
|
|
464
|
+
self,
|
|
465
|
+
*,
|
|
466
|
+
ref_spec: np.ndarray,
|
|
467
|
+
target_spec: np.ndarray,
|
|
468
|
+
initial_guess: float,
|
|
469
|
+
beam: int,
|
|
470
|
+
pos: int,
|
|
471
|
+
) -> float:
|
|
472
|
+
"""
|
|
473
|
+
Refine the 1D offset between two spectra.
|
|
474
|
+
|
|
475
|
+
A 1-parameter minimization is performed where the goodness-of-fit parameter is simply the Chisq difference
|
|
476
|
+
between the reference spectrum and shifted target spectrum.
|
|
477
|
+
"""
|
|
478
|
+
shift = minimize(
|
|
479
|
+
self.shift_chisq,
|
|
480
|
+
np.atleast_1d(initial_guess),
|
|
481
|
+
args=(ref_spec, target_spec),
|
|
482
|
+
method="nelder-mead",
|
|
483
|
+
).x[0]
|
|
484
|
+
|
|
485
|
+
max_shift = self.parameters.geo_max_shift
|
|
486
|
+
if np.abs(shift) > max_shift:
|
|
487
|
+
# Didn't find a good peak
|
|
488
|
+
logger.info(
|
|
489
|
+
f"shift in {beam = } at spatial pixel {pos} out of range ({shift} > {max_shift})"
|
|
490
|
+
)
|
|
491
|
+
shift = np.nan
|
|
492
|
+
|
|
493
|
+
return shift
|
|
494
|
+
|
|
495
|
+
@staticmethod
|
|
496
|
+
def shift_chisq(par: np.ndarray, ref_spec: np.ndarray, spec: np.ndarray) -> float:
|
|
497
|
+
"""
|
|
498
|
+
Goodness of fit calculation for a simple shift. Uses simple chisq as goodness of fit.
|
|
499
|
+
|
|
500
|
+
Less robust than SPGainCalibration's `refine_shift`, but much faster.
|
|
501
|
+
|
|
502
|
+
Parameters
|
|
503
|
+
----------
|
|
504
|
+
par : np.ndarray
|
|
505
|
+
Spectral shift being optimized
|
|
506
|
+
|
|
507
|
+
ref_spec : np.ndarray
|
|
508
|
+
Reference spectra
|
|
509
|
+
|
|
510
|
+
spec : np.ndarray
|
|
511
|
+
Spectra being fitted
|
|
512
|
+
|
|
513
|
+
Returns
|
|
514
|
+
-------
|
|
515
|
+
float
|
|
516
|
+
Sum of chisquared fit
|
|
517
|
+
|
|
518
|
+
"""
|
|
519
|
+
shift = par[0]
|
|
520
|
+
shifted_spec = spnd.shift(spec, -shift, mode="constant", cval=np.nan)
|
|
521
|
+
chisq = np.nansum((ref_spec - shifted_spec) ** 2 / ref_spec)
|
|
522
|
+
return chisq
|
|
523
|
+
|
|
524
|
+
def write_angle(self, angle: float, beam: int) -> None:
|
|
525
|
+
"""
|
|
526
|
+
Write the angle component of the geometric calibration for a single beam.
|
|
527
|
+
|
|
528
|
+
Parameters
|
|
529
|
+
----------
|
|
530
|
+
angle : float
|
|
531
|
+
The beam angle (radians) for the current beam
|
|
532
|
+
|
|
533
|
+
beam : int
|
|
534
|
+
The current beam being processed
|
|
535
|
+
|
|
536
|
+
Returns
|
|
537
|
+
-------
|
|
538
|
+
None
|
|
539
|
+
"""
|
|
540
|
+
array = np.array([angle])
|
|
541
|
+
self.intermediate_frame_write_arrays(
|
|
542
|
+
arrays=array, beam=beam, task_tag=CryonirspTag.task_geometric_angle()
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
def write_beam_offset(self, offset: np.ndarray, beam: int) -> None:
|
|
546
|
+
"""
|
|
547
|
+
Write the beam offset component of the geometric calibration for a single beam.
|
|
548
|
+
|
|
549
|
+
Parameters
|
|
550
|
+
----------
|
|
551
|
+
offset : np.ndarray
|
|
552
|
+
The beam offset for the current beam
|
|
553
|
+
|
|
554
|
+
beam : int
|
|
555
|
+
The current beam being processed
|
|
556
|
+
|
|
557
|
+
Returns
|
|
558
|
+
-------
|
|
559
|
+
None
|
|
560
|
+
|
|
561
|
+
"""
|
|
562
|
+
self.intermediate_frame_write_arrays(
|
|
563
|
+
arrays=offset, beam=beam, task_tag=CryonirspTag.task_geometric_offset()
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
def write_spectral_shifts(self, shifts: np.ndarray, beam: int) -> None:
|
|
567
|
+
"""
|
|
568
|
+
Write the spectral shift component of the geometric calibration for a single beam.
|
|
569
|
+
|
|
570
|
+
Parameters
|
|
571
|
+
----------
|
|
572
|
+
shifts : np.ndarray
|
|
573
|
+
The spectral shifts for the current beam
|
|
574
|
+
|
|
575
|
+
beam : int
|
|
576
|
+
The current beam being processed
|
|
577
|
+
|
|
578
|
+
Returns
|
|
579
|
+
-------
|
|
580
|
+
None
|
|
581
|
+
|
|
582
|
+
"""
|
|
583
|
+
self.intermediate_frame_write_arrays(
|
|
584
|
+
arrays=shifts, beam=beam, task_tag=CryonirspTag.task_geometric_sepectral_shifts()
|
|
585
|
+
)
|