dkist-processing-trend 0.1.0__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.
- changelog/.gitempty +0 -0
- dkist_processing_trend/__init__.py +10 -0
- dkist_processing_trend/config.py +11 -0
- dkist_processing_trend/models/__init__.py +1 -0
- dkist_processing_trend/models/constants.py +143 -0
- dkist_processing_trend/models/fit_options.py +15 -0
- dkist_processing_trend/models/fits_access.py +65 -0
- dkist_processing_trend/models/instrument.py +35 -0
- dkist_processing_trend/models/instrument_options.py +35 -0
- dkist_processing_trend/models/parameters.py +134 -0
- dkist_processing_trend/models/tags.py +117 -0
- dkist_processing_trend/models/task_name.py +20 -0
- dkist_processing_trend/parsers/__init__.py +1 -0
- dkist_processing_trend/parsers/arm_id.py +103 -0
- dkist_processing_trend/parsers/instrument_unique_bud.py +40 -0
- dkist_processing_trend/parsers/time.py +27 -0
- dkist_processing_trend/parsers/trend_l0_fits_access.py +123 -0
- dkist_processing_trend/tasks/__init__.py +27 -0
- dkist_processing_trend/tasks/arm_task_factory.py +57 -0
- dkist_processing_trend/tasks/dark.py +56 -0
- dkist_processing_trend/tasks/gain.py +74 -0
- dkist_processing_trend/tasks/initialize_arm_tasks.py +50 -0
- dkist_processing_trend/tasks/parse.py +169 -0
- dkist_processing_trend/tasks/prepare_fit_data_base.py +257 -0
- dkist_processing_trend/tasks/run_pac_fitter.py +392 -0
- dkist_processing_trend/tasks/trend_base.py +97 -0
- dkist_processing_trend/tasks/trend_output_data.py +167 -0
- dkist_processing_trend/tasks/visp/__init__.py +6 -0
- dkist_processing_trend/tasks/visp/visp_dmpd.py +410 -0
- dkist_processing_trend/tasks/visp/visp_extract_beam.py +14 -0
- dkist_processing_trend/tasks/visp/visp_geometric.py +260 -0
- dkist_processing_trend/tasks/visp/visp_prep_fit_data.py +162 -0
- dkist_processing_trend/tasks/visp/visp_process_demod.py +236 -0
- dkist_processing_trend/tasks/write_trend.py +663 -0
- dkist_processing_trend/tests/__init__.py +1 -0
- dkist_processing_trend/tests/conftest.py +718 -0
- dkist_processing_trend/tests/local_trial_workflows/__init__.py +0 -0
- dkist_processing_trend/tests/local_trial_workflows/l0_to_trend_visp_polcal.py +294 -0
- dkist_processing_trend/tests/local_trial_workflows/local_trial_helpers.py +488 -0
- dkist_processing_trend/tests/test_arm_task_factory.py +82 -0
- dkist_processing_trend/tests/test_base_tasks.py +86 -0
- dkist_processing_trend/tests/test_constants.py +120 -0
- dkist_processing_trend/tests/test_dark.py +97 -0
- dkist_processing_trend/tests/test_gain.py +135 -0
- dkist_processing_trend/tests/test_parameters.py +149 -0
- dkist_processing_trend/tests/test_parse.py +276 -0
- dkist_processing_trend/tests/test_prep_fit_data_base.py +233 -0
- dkist_processing_trend/tests/test_publish_catalog_messages.py +45 -0
- dkist_processing_trend/tests/test_run_pac_fitter.py +371 -0
- dkist_processing_trend/tests/test_stems.py +75 -0
- dkist_processing_trend/tests/test_transfer_output_data.py +76 -0
- dkist_processing_trend/tests/test_trend_fits_access.py +173 -0
- dkist_processing_trend/tests/test_visp.py +874 -0
- dkist_processing_trend/tests/test_workflows.py +10 -0
- dkist_processing_trend/tests/test_write_trend.py +460 -0
- dkist_processing_trend/workflows/__init__.py +3 -0
- dkist_processing_trend/workflows/visp.py +58 -0
- dkist_processing_trend-0.1.0.dist-info/METADATA +549 -0
- dkist_processing_trend-0.1.0.dist-info/RECORD +66 -0
- dkist_processing_trend-0.1.0.dist-info/WHEEL +5 -0
- dkist_processing_trend-0.1.0.dist-info/top_level.txt +3 -0
- docs/conf.py +57 -0
- docs/index.rst +10 -0
- docs/l0_to_trend_visp_polcal.rst +4 -0
- docs/landing_page.rst +11 -0
- docs/requirements_table.rst +8 -0
|
@@ -0,0 +1,874 @@
|
|
|
1
|
+
from functools import partial
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pytest
|
|
5
|
+
import scipy.ndimage as spnd
|
|
6
|
+
from astropy.io import fits
|
|
7
|
+
from astropy.modeling.functional_models import Voigt1D
|
|
8
|
+
from dkist_processing_common._util.scratch import WorkflowFileSystem
|
|
9
|
+
from dkist_processing_common.codecs.asdf import asdf_decoder
|
|
10
|
+
from dkist_processing_common.codecs.asdf import asdf_encoder
|
|
11
|
+
from dkist_processing_common.codecs.fits import fits_array_decoder
|
|
12
|
+
from dkist_processing_common.codecs.fits import fits_array_encoder
|
|
13
|
+
|
|
14
|
+
from dkist_processing_trend.models.instrument import InstrumentName
|
|
15
|
+
from dkist_processing_trend.models.instrument_options import VispInstrumentOptions
|
|
16
|
+
from dkist_processing_trend.models.parameters import TrendParameters
|
|
17
|
+
from dkist_processing_trend.models.tags import TrendTag
|
|
18
|
+
from dkist_processing_trend.tasks.visp.visp_dmpd import VispCalibrationCollection
|
|
19
|
+
from dkist_processing_trend.tasks.visp.visp_dmpd import VispDemodulatedPolcalData
|
|
20
|
+
from dkist_processing_trend.tasks.visp.visp_extract_beam import extract_visp_beam
|
|
21
|
+
from dkist_processing_trend.tasks.visp.visp_geometric import VispGeometricCalibration
|
|
22
|
+
from dkist_processing_trend.tasks.visp.visp_prep_fit_data import VispPrepareFitData
|
|
23
|
+
from dkist_processing_trend.tasks.visp.visp_process_demod import VispProcessDemodulationMatrices
|
|
24
|
+
from dkist_processing_trend.tests.conftest import ModulatedCSStepHeaders
|
|
25
|
+
from dkist_processing_trend.tests.conftest import TrendInputDatasetParameterValues
|
|
26
|
+
from dkist_processing_trend.tests.conftest import TrendTestingConstantsDatabase
|
|
27
|
+
from dkist_processing_trend.tests.conftest import write_calibration_sequence_frames_to_task
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_extract_beam():
|
|
31
|
+
"""
|
|
32
|
+
Given: A task based on `VispTrendTaskBase` and an array
|
|
33
|
+
When: Extracting the ViSP beams from the array
|
|
34
|
+
Then: The expected array is returned
|
|
35
|
+
"""
|
|
36
|
+
small_beam_border = 10
|
|
37
|
+
array = np.empty((small_beam_border * 3, 4))
|
|
38
|
+
beam2_signal = np.arange(array.shape[0] - small_beam_border)
|
|
39
|
+
array[:small_beam_border, :] = 1.1111
|
|
40
|
+
array[small_beam_border:, :] = beam2_signal[::-1][:, None]
|
|
41
|
+
|
|
42
|
+
beam1 = extract_visp_beam(array=array, beam=1, beam_border=small_beam_border)
|
|
43
|
+
np.testing.assert_array_equal(beam1, 1.1111)
|
|
44
|
+
|
|
45
|
+
beam2 = extract_visp_beam(array=array, beam=2, beam_border=small_beam_border)
|
|
46
|
+
np.testing.assert_array_equal(np.mean(beam2, axis=1), beam2_signal)
|
|
47
|
+
np.testing.assert_array_equal(np.std(beam2, axis=1), 0.0)
|
|
48
|
+
|
|
49
|
+
with pytest.raises(
|
|
50
|
+
ValueError, match="ViSP only has beams 1 and 2. Can't extract data for beam '3'."
|
|
51
|
+
):
|
|
52
|
+
extract_visp_beam(array=array, beam=3, beam_border=small_beam_border)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@pytest.fixture
|
|
56
|
+
def geometric_task_beam_shape() -> tuple[int, int]:
|
|
57
|
+
return (700, 900)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@pytest.fixture
|
|
61
|
+
def tiny_calibration_sequence() -> tuple[list, list, list, list, list]:
|
|
62
|
+
# We want some clear steps and at least one non-clear step
|
|
63
|
+
pol_status = [
|
|
64
|
+
"clear",
|
|
65
|
+
"Sapphire Polarizer",
|
|
66
|
+
"clear",
|
|
67
|
+
]
|
|
68
|
+
pol_theta = [0.0, 120.0, 0.0]
|
|
69
|
+
ret_status = ["clear", "SiO2 SAR", "clear"]
|
|
70
|
+
ret_theta = [0.0, 0.0, 0.0]
|
|
71
|
+
dark_status = [
|
|
72
|
+
"FieldStop (5arcmin)",
|
|
73
|
+
"FieldStop (5arcmin)",
|
|
74
|
+
"FieldStop (5arcmin)",
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
return pol_status, pol_theta, ret_status, ret_theta, dark_status
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@pytest.fixture
|
|
81
|
+
def visp_geometric_task(
|
|
82
|
+
tmp_path,
|
|
83
|
+
recipe_run_id,
|
|
84
|
+
geometric_task_beam_shape,
|
|
85
|
+
link_constants_db,
|
|
86
|
+
assign_input_dataset_doc_to_task,
|
|
87
|
+
):
|
|
88
|
+
link_constants_db(recipe_run_id, TrendTestingConstantsDatabase(INSTRUMENT=InstrumentName.visp))
|
|
89
|
+
|
|
90
|
+
with VispGeometricCalibration(
|
|
91
|
+
arm_id=1, recipe_run_id=recipe_run_id, workflow_name="name", workflow_version="version"
|
|
92
|
+
) as task:
|
|
93
|
+
task.scratch = WorkflowFileSystem(recipe_run_id=recipe_run_id, scratch_base_path=tmp_path)
|
|
94
|
+
assign_input_dataset_doc_to_task(
|
|
95
|
+
task=task,
|
|
96
|
+
parameter_values=TrendInputDatasetParameterValues(
|
|
97
|
+
trend_visp_beam_border=geometric_task_beam_shape[0]
|
|
98
|
+
),
|
|
99
|
+
)
|
|
100
|
+
task.parameters = TrendParameters(
|
|
101
|
+
scratch=task.scratch,
|
|
102
|
+
obs_ip_start_time=task.constants.earliest_ip_start_time,
|
|
103
|
+
instrument=InstrumentName.visp,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
yield task
|
|
107
|
+
task._purge()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def make_polcal_data_for_geo_task(
|
|
111
|
+
frame: ModulatedCSStepHeaders,
|
|
112
|
+
offset: tuple[float, float],
|
|
113
|
+
angles_deg: tuple[float, float],
|
|
114
|
+
) -> np.ndarray:
|
|
115
|
+
full_shape = frame.array_shape[1:]
|
|
116
|
+
if (
|
|
117
|
+
frame.pol_status != "clear"
|
|
118
|
+
or frame.ret_status != "clear"
|
|
119
|
+
or frame.dark_status == "DarkShutter"
|
|
120
|
+
):
|
|
121
|
+
# Put trash in the non-clear frames so we'll crash hard if they're used
|
|
122
|
+
return np.full(full_shape, np.nan)
|
|
123
|
+
|
|
124
|
+
beam_shape = (full_shape[0] // 2, full_shape[1])
|
|
125
|
+
|
|
126
|
+
# Spectral line centers
|
|
127
|
+
line_centers = np.array([0.2, 0.4, 0.5, 0.8]) * beam_shape[0]
|
|
128
|
+
offset_line_centers = line_centers - offset[0]
|
|
129
|
+
|
|
130
|
+
# Hairline centers
|
|
131
|
+
hairlines = [50, beam_shape[1] - 120]
|
|
132
|
+
|
|
133
|
+
beam1_data = np.full(beam_shape, 100.0)
|
|
134
|
+
# Approximate absorption lines with a set of Voigt profiles
|
|
135
|
+
beam1_voigt_model = sum(
|
|
136
|
+
[
|
|
137
|
+
Voigt1D(x_0=c, amplitude_L=2 - 0.8 * abs(i - 2), fwhm_G=4, fwhm_L=2)
|
|
138
|
+
for i, c in enumerate(line_centers[1:], start=1)
|
|
139
|
+
],
|
|
140
|
+
Voigt1D(x_0=line_centers[0], amplitude_L=0.4),
|
|
141
|
+
)
|
|
142
|
+
beam1_voight = beam1_voigt_model(np.arange(beam_shape[0]))
|
|
143
|
+
# Normalize so the strongest line imparts a 50% reduction in flux
|
|
144
|
+
beam1_voight = beam1_voight / beam1_voight.max() * 2 + 1
|
|
145
|
+
beam1_data /= beam1_voight[:, None]
|
|
146
|
+
for h in hairlines:
|
|
147
|
+
# Add hairlines at 10% the continuum
|
|
148
|
+
beam1_data[:, h : h + 5] *= 0.1
|
|
149
|
+
|
|
150
|
+
beam1_data = spnd.rotate(beam1_data, angle=angles_deg[0], reshape=False, mode="nearest")
|
|
151
|
+
|
|
152
|
+
# Same for beam2, but with offsets for line centers and hairlines
|
|
153
|
+
beam2_data = np.full(beam_shape, 200.0)
|
|
154
|
+
beam2_voigt_model = sum(
|
|
155
|
+
[
|
|
156
|
+
Voigt1D(x_0=c, amplitude_L=2 - 0.8 * abs(i - 2), fwhm_G=4, fwhm_L=2)
|
|
157
|
+
for i, c in enumerate(offset_line_centers[1:], start=1)
|
|
158
|
+
],
|
|
159
|
+
Voigt1D(x_0=offset_line_centers[0], amplitude_L=0.4),
|
|
160
|
+
)
|
|
161
|
+
beam2_voight = beam2_voigt_model(np.arange(beam_shape[0]))
|
|
162
|
+
beam2_voight = beam2_voight / beam2_voight.max() * 2 + 1
|
|
163
|
+
beam2_data /= beam2_voight[:, None]
|
|
164
|
+
for h in hairlines:
|
|
165
|
+
b2_h = h - int(offset[1])
|
|
166
|
+
beam2_data[:, b2_h : b2_h + 5] *= 0.1
|
|
167
|
+
|
|
168
|
+
beam2_data = spnd.rotate(beam2_data, angle=angles_deg[1], reshape=False, mode="nearest")
|
|
169
|
+
|
|
170
|
+
full_frame_array = np.vstack([beam1_data, beam2_data[::-1, :]])
|
|
171
|
+
|
|
172
|
+
return full_frame_array
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_visp_geometric_task(
|
|
176
|
+
visp_geometric_task,
|
|
177
|
+
geometric_task_beam_shape,
|
|
178
|
+
tiny_calibration_sequence,
|
|
179
|
+
mocker,
|
|
180
|
+
fake_gql_client,
|
|
181
|
+
):
|
|
182
|
+
"""
|
|
183
|
+
Given: A VispGeometricCalibration task with INPUT polcal data that contains clear measurements with a known offset between the beams
|
|
184
|
+
When: Running the task
|
|
185
|
+
Then: The correct output is written and the computed shift matches what is expected
|
|
186
|
+
"""
|
|
187
|
+
mocker.patch(
|
|
188
|
+
"dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=fake_gql_client
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
task = visp_geometric_task
|
|
192
|
+
pol_status, pol_theta, ret_status, ret_theta, dark_status = tiny_calibration_sequence
|
|
193
|
+
full_shape = (geometric_task_beam_shape[0] * 2, geometric_task_beam_shape[1])
|
|
194
|
+
offset = (23.2, 6.28)
|
|
195
|
+
angles_deg = (0.005, -0.01)
|
|
196
|
+
data_func = partial(make_polcal_data_for_geo_task, offset=offset, angles_deg=angles_deg)
|
|
197
|
+
|
|
198
|
+
write_calibration_sequence_frames_to_task(
|
|
199
|
+
task=task,
|
|
200
|
+
instrument=InstrumentName.visp,
|
|
201
|
+
arm_id=1,
|
|
202
|
+
wavelength=633.6,
|
|
203
|
+
pol_status=pol_status,
|
|
204
|
+
pol_theta=pol_theta,
|
|
205
|
+
ret_status=ret_status,
|
|
206
|
+
ret_theta=ret_theta,
|
|
207
|
+
dark_status=dark_status,
|
|
208
|
+
array_shape=full_shape,
|
|
209
|
+
data_func=data_func,
|
|
210
|
+
tags=[TrendTag.input(), TrendTag.arm_id(1)],
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
task()
|
|
214
|
+
|
|
215
|
+
for b, beam in enumerate([1, 2]):
|
|
216
|
+
tags = [
|
|
217
|
+
TrendTag.intermediate(),
|
|
218
|
+
TrendTag.arm_id(1),
|
|
219
|
+
TrendTag.beam(beam),
|
|
220
|
+
TrendTag.task_visp_geometric_calibration(),
|
|
221
|
+
]
|
|
222
|
+
assert task.count(tags) == 1
|
|
223
|
+
geo_dict = next(task.read(tags=tags, decoder=asdf_decoder))
|
|
224
|
+
|
|
225
|
+
# Check that the shift is correct to within 1/2 pixel, which is acceptable for Trend stuff
|
|
226
|
+
computed_shift = geo_dict["shift"]
|
|
227
|
+
expected_offset = (0, 0) if beam == 1 else -1 * np.array(offset)
|
|
228
|
+
np.testing.assert_array_less(np.abs(computed_shift - expected_offset), 0.5)
|
|
229
|
+
|
|
230
|
+
# Check that the angles are correct to within 1%, which I mostly just made up based on real data
|
|
231
|
+
computed_angle = geo_dict["angle_rad"]
|
|
232
|
+
np.testing.assert_array_less(
|
|
233
|
+
np.abs(np.rad2deg(computed_angle) - angles_deg[b]) / angles_deg[b], 0.01
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@pytest.fixture
|
|
238
|
+
def single_visp_instrument_options_list() -> list[dict]:
|
|
239
|
+
return [
|
|
240
|
+
{
|
|
241
|
+
"name": "visp_options",
|
|
242
|
+
"num_spatial_px": 10,
|
|
243
|
+
"num_spectral_px": 1,
|
|
244
|
+
"spatial_smoothing_fit_order": 3,
|
|
245
|
+
}
|
|
246
|
+
]
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@pytest.fixture
|
|
250
|
+
def small_beam_border() -> int:
|
|
251
|
+
return 100
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@pytest.fixture
|
|
255
|
+
def visp_prep_fit_data_task(
|
|
256
|
+
recipe_run_id,
|
|
257
|
+
tmp_path,
|
|
258
|
+
link_constants_db,
|
|
259
|
+
assign_input_dataset_doc_to_task,
|
|
260
|
+
single_visp_instrument_options_list,
|
|
261
|
+
small_num_cs_steps,
|
|
262
|
+
small_beam_border,
|
|
263
|
+
):
|
|
264
|
+
link_constants_db(
|
|
265
|
+
recipe_run_id,
|
|
266
|
+
TrendTestingConstantsDatabase(
|
|
267
|
+
INSTRUMENT=InstrumentName.visp,
|
|
268
|
+
ARM_ID_LIST=[1, 3],
|
|
269
|
+
NUM_CS_STEPS=small_num_cs_steps,
|
|
270
|
+
),
|
|
271
|
+
)
|
|
272
|
+
with VispPrepareFitData(
|
|
273
|
+
arm_id=3, recipe_run_id=recipe_run_id, workflow_name="name", workflow_version="version"
|
|
274
|
+
) as task:
|
|
275
|
+
task.scratch = WorkflowFileSystem(recipe_run_id=recipe_run_id, scratch_base_path=tmp_path)
|
|
276
|
+
assign_input_dataset_doc_to_task(
|
|
277
|
+
task,
|
|
278
|
+
TrendInputDatasetParameterValues(
|
|
279
|
+
trend_instrument_processing_options_visp=single_visp_instrument_options_list,
|
|
280
|
+
trend_visp_beam_border=small_beam_border,
|
|
281
|
+
),
|
|
282
|
+
)
|
|
283
|
+
task.parameters = TrendParameters(
|
|
284
|
+
scratch=task.scratch,
|
|
285
|
+
obs_ip_start_time=task.constants.earliest_ip_start_time,
|
|
286
|
+
instrument=task.constants.instrument,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
yield task
|
|
290
|
+
task._purge()
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def test_visp_prepare_fit_data(
|
|
294
|
+
visp_prep_fit_data_task,
|
|
295
|
+
single_visp_instrument_options_list,
|
|
296
|
+
small_calibration_sequence,
|
|
297
|
+
small_beam_border,
|
|
298
|
+
mocker,
|
|
299
|
+
fake_gql_client,
|
|
300
|
+
):
|
|
301
|
+
"""
|
|
302
|
+
Given: A `VispPrepareFitData` task and INPUT and INTERMEDIATE frames needed for the task to run
|
|
303
|
+
When: Running the task
|
|
304
|
+
Then: The dang thing runs and produces the outputs we expect
|
|
305
|
+
|
|
306
|
+
NOTE: This is just a test of "glue" of the subclass. See other tests for checks that the visp-specific methods function
|
|
307
|
+
correctly.
|
|
308
|
+
"""
|
|
309
|
+
mocker.patch(
|
|
310
|
+
"dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=fake_gql_client
|
|
311
|
+
)
|
|
312
|
+
shape = (small_beam_border * 2, small_beam_border)
|
|
313
|
+
inst_opt = single_visp_instrument_options_list[0]
|
|
314
|
+
task = visp_prep_fit_data_task
|
|
315
|
+
|
|
316
|
+
pol_status, pol_theta, ret_status, ret_theta, dark_status = small_calibration_sequence
|
|
317
|
+
|
|
318
|
+
write_calibration_sequence_frames_to_task(
|
|
319
|
+
task=task,
|
|
320
|
+
instrument=InstrumentName.visp,
|
|
321
|
+
arm_id=3,
|
|
322
|
+
wavelength=360.2,
|
|
323
|
+
pol_status=pol_status,
|
|
324
|
+
pol_theta=pol_theta,
|
|
325
|
+
ret_status=ret_status,
|
|
326
|
+
ret_theta=ret_theta,
|
|
327
|
+
dark_status=dark_status,
|
|
328
|
+
array_shape=shape,
|
|
329
|
+
tags=[TrendTag.arm_id(3)],
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
# INTERMEIDATE dark and gain frames
|
|
333
|
+
task.write(
|
|
334
|
+
data=np.full(shape, 0.0),
|
|
335
|
+
tags=[
|
|
336
|
+
TrendTag.intermediate(),
|
|
337
|
+
TrendTag.frame(),
|
|
338
|
+
TrendTag.arm_id(3),
|
|
339
|
+
TrendTag.task_dark(),
|
|
340
|
+
],
|
|
341
|
+
encoder=fits_array_encoder,
|
|
342
|
+
)
|
|
343
|
+
task.write(
|
|
344
|
+
data=np.full(shape, 1.0),
|
|
345
|
+
tags=[
|
|
346
|
+
TrendTag.intermediate(),
|
|
347
|
+
TrendTag.frame(),
|
|
348
|
+
TrendTag.arm_id(3),
|
|
349
|
+
TrendTag.task_gain(),
|
|
350
|
+
],
|
|
351
|
+
encoder=fits_array_encoder,
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
task()
|
|
355
|
+
|
|
356
|
+
for beam in range(1, 2):
|
|
357
|
+
global_tags = [
|
|
358
|
+
TrendTag.intermediate(),
|
|
359
|
+
TrendTag.arm_id(3),
|
|
360
|
+
TrendTag.beam(beam),
|
|
361
|
+
TrendTag.instrument_processing_options(inst_opt["name"]),
|
|
362
|
+
TrendTag.task_global_pac_input(),
|
|
363
|
+
]
|
|
364
|
+
local_tags = [
|
|
365
|
+
TrendTag.intermediate(),
|
|
366
|
+
TrendTag.arm_id(3),
|
|
367
|
+
TrendTag.beam(beam),
|
|
368
|
+
TrendTag.instrument_processing_options(inst_opt["name"]),
|
|
369
|
+
TrendTag.task_local_pac_input(),
|
|
370
|
+
]
|
|
371
|
+
assert task.count(global_tags) == 1
|
|
372
|
+
assert task.count(local_tags) == 1
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def test_apply_visp_global_instrument_options(
|
|
376
|
+
visp_prep_fit_data_task, single_visp_instrument_options_list
|
|
377
|
+
):
|
|
378
|
+
"""
|
|
379
|
+
Given: A `VispPrepareFitData` task and an array
|
|
380
|
+
When: Converting the array into a "global" data point
|
|
381
|
+
Then: The correct output is returned
|
|
382
|
+
"""
|
|
383
|
+
task = visp_prep_fit_data_task
|
|
384
|
+
inst_opt_object = VispInstrumentOptions.model_validate(single_visp_instrument_options_list[0])
|
|
385
|
+
array = np.full((100, 200), 100.0)
|
|
386
|
+
expected_median = np.median(array)
|
|
387
|
+
|
|
388
|
+
# Add some hairlines as sharp features in the spectral (0th) dimension
|
|
389
|
+
array[30, :] = 9999.9
|
|
390
|
+
array[70, :] = 121212.1
|
|
391
|
+
|
|
392
|
+
# And NaN's just b/c
|
|
393
|
+
array[34, 102] = np.nan
|
|
394
|
+
|
|
395
|
+
global_data = task.apply_global_instrument_options(
|
|
396
|
+
array=array, instrument_options=inst_opt_object
|
|
397
|
+
)
|
|
398
|
+
assert global_data.shape == (1, 1)
|
|
399
|
+
np.testing.assert_array_equal(global_data, expected_median)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def test_apply_visp_local_instrument_options(
|
|
403
|
+
visp_prep_fit_data_task, single_visp_instrument_options_list
|
|
404
|
+
):
|
|
405
|
+
"""
|
|
406
|
+
Given: A `VispPrepareFitData` task and an array
|
|
407
|
+
When: Converting the array into a "local" data point
|
|
408
|
+
Then: The correct output is returned
|
|
409
|
+
"""
|
|
410
|
+
task = visp_prep_fit_data_task
|
|
411
|
+
inst_opt_object = VispInstrumentOptions.model_validate(single_visp_instrument_options_list[0])
|
|
412
|
+
array = np.full((100, 200), 100.0)
|
|
413
|
+
expected_median = np.median(array)
|
|
414
|
+
|
|
415
|
+
# Add high-frequency signal in the spectral dimension that will disappear with a median
|
|
416
|
+
# Signal of [-1, 0, 1, ...] will have a median of 0 => no change to overall median
|
|
417
|
+
array += (1 - np.arange(100) % 3)[:, None]
|
|
418
|
+
|
|
419
|
+
# Add some spikes in the spatial dimension that will get smoothed out
|
|
420
|
+
array[:, 130] *= 1.2
|
|
421
|
+
array[:, 180] *= 1.5
|
|
422
|
+
|
|
423
|
+
# Add some hairlines as sharp features in the spectral dimension
|
|
424
|
+
array[30, :] = 9999.9
|
|
425
|
+
array[70, :] = 121212.1
|
|
426
|
+
|
|
427
|
+
# And NaN's just b/c
|
|
428
|
+
array[34, 102] = np.nan
|
|
429
|
+
|
|
430
|
+
local_data = task.apply_local_instrument_options(
|
|
431
|
+
array=array, instrument_options=inst_opt_object
|
|
432
|
+
)
|
|
433
|
+
assert local_data.shape == (1, inst_opt_object.num_spatial_px)
|
|
434
|
+
np.testing.assert_array_equal(local_data, expected_median)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
@pytest.fixture
|
|
438
|
+
def multiple_visp_instrument_options_list() -> list[dict]:
|
|
439
|
+
return [
|
|
440
|
+
{
|
|
441
|
+
"name": "visp_option1",
|
|
442
|
+
"num_spatial_px": 10,
|
|
443
|
+
"num_spectral_px": 1,
|
|
444
|
+
"spatial_smoothing_fit_order": 3,
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
"name": "visp_option2",
|
|
448
|
+
"num_spatial_px": 5,
|
|
449
|
+
"num_spectral_px": 1,
|
|
450
|
+
"spatial_smoothing_fit_order": 2,
|
|
451
|
+
},
|
|
452
|
+
]
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
@pytest.fixture
|
|
456
|
+
def multiple_pac_fit_options_list() -> list[dict]:
|
|
457
|
+
return [
|
|
458
|
+
{
|
|
459
|
+
"name": "option1",
|
|
460
|
+
"remove_I_trend": True,
|
|
461
|
+
"fit_mode_name": "fit_mode1",
|
|
462
|
+
"init_set_name": "init_set1",
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
"name": "option2",
|
|
466
|
+
"remove_I_trend": False,
|
|
467
|
+
"fit_mode_name": "fit_mode1",
|
|
468
|
+
"init_set_name": "init_set2",
|
|
469
|
+
},
|
|
470
|
+
]
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
@pytest.fixture
|
|
474
|
+
def visp_process_demod_task(
|
|
475
|
+
tmp_path,
|
|
476
|
+
recipe_run_id,
|
|
477
|
+
link_constants_db,
|
|
478
|
+
assign_input_dataset_doc_to_task,
|
|
479
|
+
small_beam_border,
|
|
480
|
+
multiple_visp_instrument_options_list,
|
|
481
|
+
multiple_pac_fit_options_list,
|
|
482
|
+
):
|
|
483
|
+
|
|
484
|
+
link_constants_db(
|
|
485
|
+
recipe_run_id,
|
|
486
|
+
TrendTestingConstantsDatabase(
|
|
487
|
+
INSTRUMENT=InstrumentName.visp,
|
|
488
|
+
ARM_ID_LIST=[2],
|
|
489
|
+
),
|
|
490
|
+
)
|
|
491
|
+
with VispProcessDemodulationMatrices(
|
|
492
|
+
arm_id=2, recipe_run_id=recipe_run_id, workflow_name="name", workflow_version="version"
|
|
493
|
+
) as task:
|
|
494
|
+
task.scratch = WorkflowFileSystem(recipe_run_id=recipe_run_id, scratch_base_path=tmp_path)
|
|
495
|
+
assign_input_dataset_doc_to_task(
|
|
496
|
+
task,
|
|
497
|
+
TrendInputDatasetParameterValues(
|
|
498
|
+
trend_fit_options_list=multiple_pac_fit_options_list,
|
|
499
|
+
trend_instrument_processing_options_visp=multiple_visp_instrument_options_list,
|
|
500
|
+
trend_visp_beam_border=small_beam_border,
|
|
501
|
+
),
|
|
502
|
+
)
|
|
503
|
+
task.parameters = TrendParameters(
|
|
504
|
+
scratch=task.scratch,
|
|
505
|
+
obs_ip_start_time=task.constants.earliest_ip_start_time,
|
|
506
|
+
instrument=task.constants.instrument,
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
yield task
|
|
510
|
+
task._purge()
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def test_visp_process_demod_task(
|
|
514
|
+
visp_process_demod_task,
|
|
515
|
+
multiple_visp_instrument_options_list,
|
|
516
|
+
multiple_pac_fit_options_list,
|
|
517
|
+
small_beam_border,
|
|
518
|
+
mocker,
|
|
519
|
+
fake_gql_client,
|
|
520
|
+
):
|
|
521
|
+
mocker.patch(
|
|
522
|
+
"dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=fake_gql_client
|
|
523
|
+
)
|
|
524
|
+
"""
|
|
525
|
+
Given: A `VispProcessDemodulationMatrices` task and some best-fit demodulation matrices to mimic the output of running the PAC fitter
|
|
526
|
+
When: Running the task
|
|
527
|
+
Then: The task runs, the correct number of outputs is produced, and all processed demodulation matrices have the correct form
|
|
528
|
+
"""
|
|
529
|
+
|
|
530
|
+
task = visp_process_demod_task
|
|
531
|
+
|
|
532
|
+
full_beam_shape = (small_beam_border, 102)
|
|
533
|
+
full_frame_shape = (full_beam_shape[0] * 2, full_beam_shape[1])
|
|
534
|
+
num_mod = 3
|
|
535
|
+
task.write(
|
|
536
|
+
data=np.empty(full_frame_shape),
|
|
537
|
+
tags=[
|
|
538
|
+
TrendTag.intermediate(),
|
|
539
|
+
TrendTag.frame(),
|
|
540
|
+
TrendTag.arm_id(task.arm_id),
|
|
541
|
+
TrendTag.task_gain(),
|
|
542
|
+
],
|
|
543
|
+
encoder=fits_array_encoder,
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
for fit_options in multiple_pac_fit_options_list:
|
|
547
|
+
for instrument_options in multiple_visp_instrument_options_list:
|
|
548
|
+
num_spat = instrument_options["num_spatial_px"]
|
|
549
|
+
num_wave = instrument_options["num_spectral_px"]
|
|
550
|
+
for beam in [1, 2]:
|
|
551
|
+
# Add a linear signal so there's *something* for the fit to grab onto
|
|
552
|
+
# We divide by the linear signal instead of multiply because this matrix will be inverted for the actual fit.
|
|
553
|
+
raw_demod = (
|
|
554
|
+
np.ones((num_wave, num_spat, 4, num_mod))
|
|
555
|
+
/ (np.arange(1, num_spat + 1) * 1000)[None, :, None, None]
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
task.write(
|
|
559
|
+
data=raw_demod,
|
|
560
|
+
tags=[
|
|
561
|
+
TrendTag.intermediate(),
|
|
562
|
+
TrendTag.arm_id(task.arm_id),
|
|
563
|
+
TrendTag.beam(beam),
|
|
564
|
+
TrendTag.pac_fit_options(fit_options["name"]),
|
|
565
|
+
TrendTag.instrument_processing_options(instrument_options["name"]),
|
|
566
|
+
TrendTag.task_best_fit_demodulation_matrices(),
|
|
567
|
+
],
|
|
568
|
+
encoder=fits_array_encoder,
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
task()
|
|
572
|
+
|
|
573
|
+
for fit_options in multiple_pac_fit_options_list:
|
|
574
|
+
for instrument_options in multiple_visp_instrument_options_list:
|
|
575
|
+
for beam in [1, 2]:
|
|
576
|
+
tags = [
|
|
577
|
+
TrendTag.intermediate(),
|
|
578
|
+
TrendTag.arm_id(task.arm_id),
|
|
579
|
+
TrendTag.beam(beam),
|
|
580
|
+
TrendTag.pac_fit_options(fit_options["name"]),
|
|
581
|
+
TrendTag.instrument_processing_options(instrument_options["name"]),
|
|
582
|
+
TrendTag.task_processed_demodulation_matrices(),
|
|
583
|
+
]
|
|
584
|
+
assert task.count(tags) == 1
|
|
585
|
+
processed_demod = next(task.read(tags=tags, decoder=fits_array_decoder))
|
|
586
|
+
assert processed_demod.shape == full_beam_shape + (4, num_mod)
|
|
587
|
+
# Test that the upsampling in the spectral dimension didn't do anything weird
|
|
588
|
+
np.testing.assert_almost_equal(np.std(processed_demod, axis=0), 0.0)
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def test_smooth_demod_matrices(visp_process_demod_task):
|
|
592
|
+
"""
|
|
593
|
+
Given: A demodulation matrix with known form in the spatial direction
|
|
594
|
+
When: Smoothing that demodulation matri
|
|
595
|
+
Then: The result has the right shape and the correct polynomial form
|
|
596
|
+
"""
|
|
597
|
+
task = visp_process_demod_task
|
|
598
|
+
order = 5
|
|
599
|
+
coeffs = np.linspace(1, 37, order + 1)
|
|
600
|
+
full_num_spat = 200
|
|
601
|
+
binned_num_spat = 50
|
|
602
|
+
full_signal = np.poly1d(coeffs)(np.arange(full_num_spat))
|
|
603
|
+
idx = np.linspace(0, full_num_spat, binned_num_spat, endpoint=False, dtype=int)
|
|
604
|
+
binned_signal = full_signal[idx]
|
|
605
|
+
|
|
606
|
+
# Divide by the signal because we fit the polynomial in the inverse
|
|
607
|
+
binned_demod = 1.0 / binned_signal[None, :, None, None]
|
|
608
|
+
|
|
609
|
+
smoothed_demod = task.smooth_demod_matrices(binned_demod, order, full_num_spat)
|
|
610
|
+
|
|
611
|
+
assert smoothed_demod.shape == (1, full_num_spat, 1, 1)
|
|
612
|
+
|
|
613
|
+
# Like above, fit the inverse of the signal
|
|
614
|
+
fit_coeffs = np.polyfit(np.arange(full_num_spat), 1.0 / smoothed_demod[0, :, 0, 0], order)
|
|
615
|
+
np.testing.assert_allclose(coeffs, fit_coeffs, rtol=1e-5)
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
@pytest.fixture
|
|
619
|
+
def visp_dmpd_task(
|
|
620
|
+
tmp_path,
|
|
621
|
+
recipe_run_id,
|
|
622
|
+
link_constants_db,
|
|
623
|
+
assign_input_dataset_doc_to_task,
|
|
624
|
+
small_beam_border,
|
|
625
|
+
multiple_visp_instrument_options_list,
|
|
626
|
+
multiple_pac_fit_options_list,
|
|
627
|
+
):
|
|
628
|
+
|
|
629
|
+
link_constants_db(
|
|
630
|
+
recipe_run_id,
|
|
631
|
+
TrendTestingConstantsDatabase(
|
|
632
|
+
INSTRUMENT=InstrumentName.visp,
|
|
633
|
+
ARM_ID_LIST=[2],
|
|
634
|
+
NUM_MODSTATES=3,
|
|
635
|
+
),
|
|
636
|
+
)
|
|
637
|
+
with VispDemodulatedPolcalData(
|
|
638
|
+
arm_id=2, recipe_run_id=recipe_run_id, workflow_name="name", workflow_version="version"
|
|
639
|
+
) as task:
|
|
640
|
+
task.scratch = WorkflowFileSystem(recipe_run_id=recipe_run_id, scratch_base_path=tmp_path)
|
|
641
|
+
assign_input_dataset_doc_to_task(
|
|
642
|
+
task,
|
|
643
|
+
TrendInputDatasetParameterValues(
|
|
644
|
+
trend_fit_options_list=multiple_pac_fit_options_list,
|
|
645
|
+
trend_instrument_processing_options_visp=multiple_visp_instrument_options_list,
|
|
646
|
+
trend_visp_beam_border=small_beam_border,
|
|
647
|
+
),
|
|
648
|
+
)
|
|
649
|
+
task.parameters = TrendParameters(
|
|
650
|
+
scratch=task.scratch,
|
|
651
|
+
obs_ip_start_time=task.constants.earliest_ip_start_time,
|
|
652
|
+
instrument=task.constants.instrument,
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
yield task
|
|
656
|
+
task._purge()
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def test_visp_dmpd_task(
|
|
660
|
+
visp_dmpd_task,
|
|
661
|
+
small_calibration_sequence,
|
|
662
|
+
small_num_cs_steps,
|
|
663
|
+
small_beam_border,
|
|
664
|
+
multiple_pac_fit_options_list,
|
|
665
|
+
multiple_visp_instrument_options_list,
|
|
666
|
+
mocker,
|
|
667
|
+
fake_gql_client,
|
|
668
|
+
):
|
|
669
|
+
mocker.patch(
|
|
670
|
+
"dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=fake_gql_client
|
|
671
|
+
)
|
|
672
|
+
shape = (small_beam_border * 2, small_beam_border)
|
|
673
|
+
task = visp_dmpd_task
|
|
674
|
+
|
|
675
|
+
pol_status, pol_theta, ret_status, ret_theta, dark_status = small_calibration_sequence
|
|
676
|
+
num_mod = task.constants.num_modstates
|
|
677
|
+
write_calibration_sequence_frames_to_task(
|
|
678
|
+
task=task,
|
|
679
|
+
instrument=InstrumentName.visp,
|
|
680
|
+
num_modstates=num_mod,
|
|
681
|
+
arm_id=2,
|
|
682
|
+
wavelength=360.2,
|
|
683
|
+
pol_status=pol_status,
|
|
684
|
+
pol_theta=pol_theta,
|
|
685
|
+
ret_status=ret_status,
|
|
686
|
+
ret_theta=ret_theta,
|
|
687
|
+
dark_status=dark_status,
|
|
688
|
+
array_shape=shape,
|
|
689
|
+
tags=[TrendTag.input(), TrendTag.arm_id(2)],
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
# INTERMEIDATE dark, gain, geo, and processed demod frames
|
|
693
|
+
task.write(
|
|
694
|
+
data=np.full(shape, 0.0),
|
|
695
|
+
tags=[
|
|
696
|
+
TrendTag.intermediate(),
|
|
697
|
+
TrendTag.frame(),
|
|
698
|
+
TrendTag.arm_id(2),
|
|
699
|
+
TrendTag.task_dark(),
|
|
700
|
+
],
|
|
701
|
+
encoder=fits_array_encoder,
|
|
702
|
+
)
|
|
703
|
+
task.write(
|
|
704
|
+
data=np.full(shape, 1.0),
|
|
705
|
+
tags=[
|
|
706
|
+
TrendTag.intermediate(),
|
|
707
|
+
TrendTag.frame(),
|
|
708
|
+
TrendTag.arm_id(2),
|
|
709
|
+
TrendTag.task_gain(),
|
|
710
|
+
],
|
|
711
|
+
encoder=fits_array_encoder,
|
|
712
|
+
)
|
|
713
|
+
task.write(
|
|
714
|
+
data={"angle_rad": 0.004, "shift": np.array([0.0, 0.0])},
|
|
715
|
+
tags=[
|
|
716
|
+
TrendTag.intermediate(),
|
|
717
|
+
TrendTag.arm_id(2),
|
|
718
|
+
TrendTag.beam(1),
|
|
719
|
+
TrendTag.task_visp_geometric_calibration(),
|
|
720
|
+
],
|
|
721
|
+
encoder=asdf_encoder,
|
|
722
|
+
)
|
|
723
|
+
task.write(
|
|
724
|
+
data={"angle_rad": -0.004, "shift": np.array([5.0, 12.3])},
|
|
725
|
+
tags=[
|
|
726
|
+
TrendTag.intermediate(),
|
|
727
|
+
TrendTag.arm_id(2),
|
|
728
|
+
TrendTag.beam(2),
|
|
729
|
+
TrendTag.task_visp_geometric_calibration(),
|
|
730
|
+
],
|
|
731
|
+
encoder=asdf_encoder,
|
|
732
|
+
)
|
|
733
|
+
demod = np.random.random((shape[0] // 2, shape[1], 4, num_mod))
|
|
734
|
+
for fit_options in multiple_pac_fit_options_list:
|
|
735
|
+
for inst_options in multiple_visp_instrument_options_list:
|
|
736
|
+
task.write(
|
|
737
|
+
data=demod,
|
|
738
|
+
tags=[
|
|
739
|
+
TrendTag.intermediate(),
|
|
740
|
+
TrendTag.arm_id(2),
|
|
741
|
+
TrendTag.pac_fit_options(fit_options["name"]),
|
|
742
|
+
TrendTag.instrument_processing_options(inst_options["name"]),
|
|
743
|
+
TrendTag.beam(1),
|
|
744
|
+
TrendTag.task_processed_demodulation_matrices(),
|
|
745
|
+
],
|
|
746
|
+
encoder=fits_array_encoder,
|
|
747
|
+
)
|
|
748
|
+
task.write(
|
|
749
|
+
data=demod,
|
|
750
|
+
tags=[
|
|
751
|
+
TrendTag.intermediate(),
|
|
752
|
+
TrendTag.arm_id(2),
|
|
753
|
+
TrendTag.pac_fit_options(fit_options["name"]),
|
|
754
|
+
TrendTag.instrument_processing_options(inst_options["name"]),
|
|
755
|
+
TrendTag.beam(2),
|
|
756
|
+
TrendTag.task_processed_demodulation_matrices(),
|
|
757
|
+
],
|
|
758
|
+
encoder=fits_array_encoder,
|
|
759
|
+
)
|
|
760
|
+
|
|
761
|
+
task()
|
|
762
|
+
|
|
763
|
+
# -5 and -13 come from the shift values written in the geo intermediate above
|
|
764
|
+
expected_beam_shape = (shape[0] // 2 - 5, shape[1] - 13)
|
|
765
|
+
expected_full_shape = (1, small_num_cs_steps, 4, *expected_beam_shape)
|
|
766
|
+
for fit_options in multiple_pac_fit_options_list:
|
|
767
|
+
for inst_options in multiple_visp_instrument_options_list:
|
|
768
|
+
tags = [
|
|
769
|
+
TrendTag.intermediate(),
|
|
770
|
+
TrendTag.arm_id(2),
|
|
771
|
+
TrendTag.pac_fit_options(fit_options["name"]),
|
|
772
|
+
TrendTag.instrument_processing_options(inst_options["name"]),
|
|
773
|
+
]
|
|
774
|
+
beam1_tags = tags + [
|
|
775
|
+
TrendTag.beam(1),
|
|
776
|
+
TrendTag.task_single_beam_demodulated_polcal_data(),
|
|
777
|
+
]
|
|
778
|
+
beam2_tags = tags + [
|
|
779
|
+
TrendTag.beam(2),
|
|
780
|
+
TrendTag.task_single_beam_demodulated_polcal_data(),
|
|
781
|
+
]
|
|
782
|
+
combined_tags = tags + [TrendTag.task_demodulated_polcal_data()]
|
|
783
|
+
|
|
784
|
+
assert task.count(tags=beam1_tags) == 1
|
|
785
|
+
assert task.count(tags=beam2_tags) == 1
|
|
786
|
+
assert task.count(tags=combined_tags) == 1
|
|
787
|
+
|
|
788
|
+
beam1_dmdp = next(task.read(tags=beam1_tags, decoder=fits_array_decoder))
|
|
789
|
+
beam2_dmdp = next(task.read(tags=beam2_tags, decoder=fits_array_decoder))
|
|
790
|
+
combined_dmdp = next(task.read(tags=combined_tags, decoder=fits_array_decoder))
|
|
791
|
+
|
|
792
|
+
assert beam1_dmdp.shape == expected_full_shape
|
|
793
|
+
# Testing >f4 instead of np.float32 b/c PrimaryHDUs change endianess
|
|
794
|
+
assert beam1_dmdp.dtype == np.dtype(">f4")
|
|
795
|
+
assert beam2_dmdp.shape == expected_full_shape
|
|
796
|
+
assert beam2_dmdp.dtype == np.dtype(">f4")
|
|
797
|
+
assert combined_dmdp.shape == expected_full_shape
|
|
798
|
+
assert combined_dmdp.dtype == np.dtype(">f4")
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
@pytest.mark.parametrize(
|
|
802
|
+
"shifts, expected",
|
|
803
|
+
# Shifts have shape (num_beams, 2)
|
|
804
|
+
# So the inner-most lists below (e.g., [5.0, 6.0]) correspond to [x_shift, y_shit]
|
|
805
|
+
[
|
|
806
|
+
(
|
|
807
|
+
np.array(
|
|
808
|
+
[
|
|
809
|
+
[0.0, 0.0], # Beam 1
|
|
810
|
+
[11.0, 10.0], # Beam 2
|
|
811
|
+
]
|
|
812
|
+
),
|
|
813
|
+
[slice(0, -11, None), slice(0, -10, None)],
|
|
814
|
+
),
|
|
815
|
+
(
|
|
816
|
+
np.array(
|
|
817
|
+
[
|
|
818
|
+
[0.0, 0.0], # Beam 1
|
|
819
|
+
[-11.0, -10.0], # Beam 2
|
|
820
|
+
]
|
|
821
|
+
),
|
|
822
|
+
[slice(11, None, None), slice(10, None, None)],
|
|
823
|
+
),
|
|
824
|
+
(
|
|
825
|
+
np.array(
|
|
826
|
+
[ # This case would never happen, but is useful as a check of the logic
|
|
827
|
+
[10.0, -2.0], # Beam 1
|
|
828
|
+
[-11.0, 10.0], # Beam 2
|
|
829
|
+
]
|
|
830
|
+
),
|
|
831
|
+
[slice(11, -10, None), slice(2, -10, None)],
|
|
832
|
+
),
|
|
833
|
+
],
|
|
834
|
+
ids=["All positive", "All negative", "Positive and negative"],
|
|
835
|
+
)
|
|
836
|
+
def test_beam_overlap_slice(shifts, expected):
|
|
837
|
+
"""
|
|
838
|
+
Given: A `VispCalibrationCollection` object with populated shifts
|
|
839
|
+
When: Computing the overlapping beam slices
|
|
840
|
+
Then: The correct values are returned
|
|
841
|
+
"""
|
|
842
|
+
shift_dict = {i: v for i, v in enumerate(shifts)}
|
|
843
|
+
calibrations = VispCalibrationCollection(
|
|
844
|
+
dark=np.empty(2), gain=np.empty(2), angle=dict(), shift=shift_dict, demod_matrices=dict()
|
|
845
|
+
)
|
|
846
|
+
x_slice, y_slice = calibrations.beams_overlap_slice
|
|
847
|
+
|
|
848
|
+
assert x_slice == expected[0]
|
|
849
|
+
assert y_slice == expected[1]
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
def test_combine_beams(visp_dmpd_task):
|
|
853
|
+
"""
|
|
854
|
+
Given: A `VispDemodulatedPolcalData` task and set of calibrated array data
|
|
855
|
+
When: Combining the two beams
|
|
856
|
+
Then: The correct result is returned
|
|
857
|
+
"""
|
|
858
|
+
task = visp_dmpd_task
|
|
859
|
+
shape = (10, 10, 4)
|
|
860
|
+
beam1 = np.ones(shape) + np.arange(4)[None, None, :]
|
|
861
|
+
beam2 = np.ones(shape) + np.arange(4)[::-1][None, None, :]
|
|
862
|
+
result = task.combine_beams(
|
|
863
|
+
beam1_data=beam1,
|
|
864
|
+
beam2_data=beam2,
|
|
865
|
+
)
|
|
866
|
+
|
|
867
|
+
x = np.arange(1, 5)
|
|
868
|
+
expected_I = np.ones((10, 10)) * 2.5
|
|
869
|
+
expected_Q = np.ones((10, 10)) * (x[1] / x[0] + x[-2] / x[-1]) / 2.0 * 2.5
|
|
870
|
+
expected_U = np.ones((10, 10)) * (x[2] / x[0] + x[-3] / x[-1]) / 2.0 * 2.5
|
|
871
|
+
expected_V = np.ones((10, 10)) * (x[3] / x[0] + x[-4] / x[-1]) / 2.0 * 2.5
|
|
872
|
+
expected = np.dstack([expected_I, expected_Q, expected_U, expected_V])
|
|
873
|
+
|
|
874
|
+
np.testing.assert_array_equal(result, expected)
|