dkist-processing-cryonirsp 1.3.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of dkist-processing-cryonirsp might be problematic. Click here for more details.

Files changed (111) hide show
  1. changelog/.gitempty +0 -0
  2. dkist_processing_cryonirsp/__init__.py +11 -0
  3. dkist_processing_cryonirsp/config.py +12 -0
  4. dkist_processing_cryonirsp/models/__init__.py +1 -0
  5. dkist_processing_cryonirsp/models/constants.py +248 -0
  6. dkist_processing_cryonirsp/models/exposure_conditions.py +26 -0
  7. dkist_processing_cryonirsp/models/parameters.py +296 -0
  8. dkist_processing_cryonirsp/models/tags.py +168 -0
  9. dkist_processing_cryonirsp/models/task_name.py +14 -0
  10. dkist_processing_cryonirsp/parsers/__init__.py +1 -0
  11. dkist_processing_cryonirsp/parsers/cryonirsp_l0_fits_access.py +111 -0
  12. dkist_processing_cryonirsp/parsers/cryonirsp_l1_fits_access.py +30 -0
  13. dkist_processing_cryonirsp/parsers/exposure_conditions.py +163 -0
  14. dkist_processing_cryonirsp/parsers/map_repeats.py +40 -0
  15. dkist_processing_cryonirsp/parsers/measurements.py +55 -0
  16. dkist_processing_cryonirsp/parsers/modstates.py +31 -0
  17. dkist_processing_cryonirsp/parsers/optical_density_filters.py +40 -0
  18. dkist_processing_cryonirsp/parsers/polarimetric_check.py +120 -0
  19. dkist_processing_cryonirsp/parsers/scan_step.py +412 -0
  20. dkist_processing_cryonirsp/parsers/time.py +80 -0
  21. dkist_processing_cryonirsp/parsers/wavelength.py +26 -0
  22. dkist_processing_cryonirsp/tasks/__init__.py +19 -0
  23. dkist_processing_cryonirsp/tasks/assemble_movie.py +202 -0
  24. dkist_processing_cryonirsp/tasks/bad_pixel_map.py +96 -0
  25. dkist_processing_cryonirsp/tasks/beam_boundaries_base.py +279 -0
  26. dkist_processing_cryonirsp/tasks/ci_beam_boundaries.py +55 -0
  27. dkist_processing_cryonirsp/tasks/ci_science.py +169 -0
  28. dkist_processing_cryonirsp/tasks/cryonirsp_base.py +67 -0
  29. dkist_processing_cryonirsp/tasks/dark.py +98 -0
  30. dkist_processing_cryonirsp/tasks/gain.py +251 -0
  31. dkist_processing_cryonirsp/tasks/instrument_polarization.py +447 -0
  32. dkist_processing_cryonirsp/tasks/l1_output_data.py +44 -0
  33. dkist_processing_cryonirsp/tasks/linearity_correction.py +582 -0
  34. dkist_processing_cryonirsp/tasks/make_movie_frames.py +302 -0
  35. dkist_processing_cryonirsp/tasks/mixin/__init__.py +1 -0
  36. dkist_processing_cryonirsp/tasks/mixin/beam_access.py +52 -0
  37. dkist_processing_cryonirsp/tasks/mixin/corrections.py +177 -0
  38. dkist_processing_cryonirsp/tasks/mixin/intermediate_frame.py +193 -0
  39. dkist_processing_cryonirsp/tasks/mixin/linearized_frame.py +309 -0
  40. dkist_processing_cryonirsp/tasks/mixin/shift_measurements.py +297 -0
  41. dkist_processing_cryonirsp/tasks/parse.py +281 -0
  42. dkist_processing_cryonirsp/tasks/quality_metrics.py +271 -0
  43. dkist_processing_cryonirsp/tasks/science_base.py +511 -0
  44. dkist_processing_cryonirsp/tasks/sp_beam_boundaries.py +270 -0
  45. dkist_processing_cryonirsp/tasks/sp_dispersion_axis_correction.py +484 -0
  46. dkist_processing_cryonirsp/tasks/sp_geometric.py +585 -0
  47. dkist_processing_cryonirsp/tasks/sp_science.py +299 -0
  48. dkist_processing_cryonirsp/tasks/sp_solar_gain.py +475 -0
  49. dkist_processing_cryonirsp/tasks/trial_output_data.py +61 -0
  50. dkist_processing_cryonirsp/tasks/write_l1.py +1033 -0
  51. dkist_processing_cryonirsp/tests/__init__.py +1 -0
  52. dkist_processing_cryonirsp/tests/conftest.py +456 -0
  53. dkist_processing_cryonirsp/tests/header_models.py +592 -0
  54. dkist_processing_cryonirsp/tests/local_trial_workflows/__init__.py +0 -0
  55. dkist_processing_cryonirsp/tests/local_trial_workflows/l0_cals_only.py +541 -0
  56. dkist_processing_cryonirsp/tests/local_trial_workflows/l0_to_l1.py +615 -0
  57. dkist_processing_cryonirsp/tests/local_trial_workflows/linearize_only.py +96 -0
  58. dkist_processing_cryonirsp/tests/local_trial_workflows/local_trial_helpers.py +592 -0
  59. dkist_processing_cryonirsp/tests/test_assemble_movie.py +144 -0
  60. dkist_processing_cryonirsp/tests/test_assemble_qualilty.py +517 -0
  61. dkist_processing_cryonirsp/tests/test_bad_pixel_maps.py +115 -0
  62. dkist_processing_cryonirsp/tests/test_ci_beam_boundaries.py +106 -0
  63. dkist_processing_cryonirsp/tests/test_ci_science.py +355 -0
  64. dkist_processing_cryonirsp/tests/test_corrections.py +126 -0
  65. dkist_processing_cryonirsp/tests/test_cryo_base.py +202 -0
  66. dkist_processing_cryonirsp/tests/test_cryo_constants.py +76 -0
  67. dkist_processing_cryonirsp/tests/test_dark.py +287 -0
  68. dkist_processing_cryonirsp/tests/test_gain.py +278 -0
  69. dkist_processing_cryonirsp/tests/test_instrument_polarization.py +531 -0
  70. dkist_processing_cryonirsp/tests/test_linearity_correction.py +245 -0
  71. dkist_processing_cryonirsp/tests/test_make_movie_frames.py +111 -0
  72. dkist_processing_cryonirsp/tests/test_parameters.py +266 -0
  73. dkist_processing_cryonirsp/tests/test_parse.py +1439 -0
  74. dkist_processing_cryonirsp/tests/test_quality.py +203 -0
  75. dkist_processing_cryonirsp/tests/test_sp_beam_boundaries.py +112 -0
  76. dkist_processing_cryonirsp/tests/test_sp_dispersion_axis_correction.py +155 -0
  77. dkist_processing_cryonirsp/tests/test_sp_geometric.py +319 -0
  78. dkist_processing_cryonirsp/tests/test_sp_make_movie_frames.py +121 -0
  79. dkist_processing_cryonirsp/tests/test_sp_science.py +483 -0
  80. dkist_processing_cryonirsp/tests/test_sp_solar.py +198 -0
  81. dkist_processing_cryonirsp/tests/test_trial_create_quality_report.py +79 -0
  82. dkist_processing_cryonirsp/tests/test_trial_output_data.py +251 -0
  83. dkist_processing_cryonirsp/tests/test_workflows.py +9 -0
  84. dkist_processing_cryonirsp/tests/test_write_l1.py +436 -0
  85. dkist_processing_cryonirsp/workflows/__init__.py +2 -0
  86. dkist_processing_cryonirsp/workflows/ci_l0_processing.py +77 -0
  87. dkist_processing_cryonirsp/workflows/sp_l0_processing.py +84 -0
  88. dkist_processing_cryonirsp/workflows/trial_workflows.py +190 -0
  89. dkist_processing_cryonirsp-1.3.4.dist-info/METADATA +194 -0
  90. dkist_processing_cryonirsp-1.3.4.dist-info/RECORD +111 -0
  91. dkist_processing_cryonirsp-1.3.4.dist-info/WHEEL +5 -0
  92. dkist_processing_cryonirsp-1.3.4.dist-info/top_level.txt +4 -0
  93. docs/Makefile +134 -0
  94. docs/bad_pixel_calibration.rst +47 -0
  95. docs/beam_angle_calculation.rst +53 -0
  96. docs/beam_boundary_computation.rst +88 -0
  97. docs/changelog.rst +7 -0
  98. docs/ci_science_calibration.rst +33 -0
  99. docs/conf.py +52 -0
  100. docs/index.rst +21 -0
  101. docs/l0_to_l1_cryonirsp_ci-full-trial.rst +10 -0
  102. docs/l0_to_l1_cryonirsp_ci.rst +10 -0
  103. docs/l0_to_l1_cryonirsp_sp-full-trial.rst +10 -0
  104. docs/l0_to_l1_cryonirsp_sp.rst +10 -0
  105. docs/linearization.rst +43 -0
  106. docs/make.bat +170 -0
  107. docs/requirements.txt +1 -0
  108. docs/requirements_table.rst +8 -0
  109. docs/scientific_changelog.rst +10 -0
  110. docs/sp_science_calibration.rst +59 -0
  111. licenses/LICENSE.rst +11 -0
@@ -0,0 +1,483 @@
1
+ import json
2
+ import random
3
+ from datetime import datetime
4
+
5
+ import numpy as np
6
+ import pytest
7
+ from astropy.io import fits
8
+ from astropy.time import Time
9
+ from astropy.time import TimeDelta
10
+ from dkist_header_validator import spec122_validator
11
+ from dkist_processing_common._util.scratch import WorkflowFileSystem
12
+ from dkist_processing_common.codecs.fits import fits_hdulist_encoder
13
+ from dkist_processing_common.tests.conftest import FakeGQLClient
14
+
15
+ from dkist_processing_cryonirsp.models.exposure_conditions import AllowableOpticalDensityFilterNames
16
+ from dkist_processing_cryonirsp.models.exposure_conditions import ExposureConditions
17
+ from dkist_processing_cryonirsp.models.tags import CryonirspStemName
18
+ from dkist_processing_cryonirsp.models.tags import CryonirspTag
19
+ from dkist_processing_cryonirsp.parsers.cryonirsp_l0_fits_access import CryonirspL0FitsAccess
20
+ from dkist_processing_cryonirsp.tasks.sp_science import CalibrationCollection
21
+ from dkist_processing_cryonirsp.tasks.sp_science import SPScienceCalibration
22
+ from dkist_processing_cryonirsp.tests.conftest import cryonirsp_testing_parameters_factory
23
+ from dkist_processing_cryonirsp.tests.conftest import CryonirspConstantsDb
24
+ from dkist_processing_cryonirsp.tests.conftest import generate_fits_frame
25
+ from dkist_processing_cryonirsp.tests.header_models import CryonirspHeadersValidObserveFrames
26
+
27
+
28
+ @pytest.fixture(scope="function", params=["Full Stokes", "Stokes-I"])
29
+ def sp_science_calibration_task(
30
+ tmp_path,
31
+ recipe_run_id,
32
+ assign_input_dataset_doc_to_task,
33
+ init_cryonirsp_constants_db,
34
+ request,
35
+ ):
36
+ num_map_scans = 2
37
+ num_beams = 2
38
+ num_scan_steps = 2
39
+ exposure_time = 0.02 # From CryonirspHeadersValidObserveFrames fixture
40
+ exposure_conditions = ExposureConditions(
41
+ exposure_time, AllowableOpticalDensityFilterNames.OPEN.value
42
+ )
43
+ if request.param == "Full Stokes":
44
+ num_modstates = 2
45
+ else:
46
+ num_modstates = 1
47
+ array_shape = (1, 30, 60)
48
+ intermediate_shape = (30, 30)
49
+ dataset_shape = (num_beams * num_map_scans * num_scan_steps * num_modstates,) + array_shape[1:]
50
+
51
+ constants_db = CryonirspConstantsDb(
52
+ NUM_MODSTATES=num_modstates,
53
+ NUM_MAP_SCANS=num_map_scans,
54
+ NUM_SCAN_STEPS=num_scan_steps,
55
+ NUM_BEAMS=num_beams,
56
+ OBSERVE_EXPOSURE_CONDITIONS_LIST=(exposure_conditions,),
57
+ MODULATOR_SPIN_MODE="Continuous" if request.param == "Full Stokes" else "Off",
58
+ )
59
+ init_cryonirsp_constants_db(recipe_run_id, constants_db)
60
+ with SPScienceCalibration(
61
+ recipe_run_id=recipe_run_id, workflow_name="sp_science_calibration", workflow_version="VX.Y"
62
+ ) as task:
63
+ try: # This try... block is here to make sure the dbs get cleaned up if there's a failure in the fixture
64
+ all_zeros = np.zeros(intermediate_shape)
65
+ all_ones = np.ones(intermediate_shape)
66
+ task.scratch = WorkflowFileSystem(
67
+ scratch_base_path=tmp_path, recipe_run_id=recipe_run_id
68
+ )
69
+ param_class = cryonirsp_testing_parameters_factory(param_path=tmp_path)
70
+ assign_input_dataset_doc_to_task(task, param_class())
71
+ # Create fake bad pixel map
72
+ task.intermediate_frame_write_arrays(
73
+ arrays=np.zeros((30, 60)),
74
+ task_tag=CryonirspTag.task_bad_pixel_map(),
75
+ )
76
+ # Create fake demodulation matrices
77
+ demod_matrices = np.zeros((1, 1, 4, num_modstates))
78
+ for modstate in range(num_modstates):
79
+ demod_matrices[0, 0, :, modstate] = [1, 2, 3, 4]
80
+ for beam in range(num_beams):
81
+ demod_hdul = fits.HDUList([fits.PrimaryHDU(data=demod_matrices)])
82
+ task.write(
83
+ data=demod_hdul,
84
+ tags=[
85
+ CryonirspTag.intermediate(),
86
+ CryonirspTag.frame(),
87
+ CryonirspTag.task_demodulation_matrices(),
88
+ CryonirspTag.beam(beam + 1),
89
+ ],
90
+ encoder=fits_hdulist_encoder,
91
+ )
92
+
93
+ # Create fake geometric objects
94
+ angle = np.array([0.0])
95
+ offset = np.array([-10.2, 5.1])
96
+ spec_shift = np.zeros(intermediate_shape[0])
97
+ for beam in range(1, num_beams + 1):
98
+ task.intermediate_frame_write_arrays(
99
+ arrays=angle, beam=beam, task_tag=CryonirspTag.task_geometric_angle()
100
+ )
101
+ task.intermediate_frame_write_arrays(
102
+ arrays=spec_shift,
103
+ beam=beam,
104
+ task_tag=CryonirspTag.task_geometric_sepectral_shifts(),
105
+ )
106
+ for modstate in range(1, num_modstates + 1):
107
+ task.intermediate_frame_write_arrays(
108
+ arrays=offset
109
+ * (beam - 1), # Because we need the fiducial array to have (0, 0) offset
110
+ beam=beam,
111
+ modstate=modstate,
112
+ task_tag=CryonirspTag.task_geometric_offset(),
113
+ )
114
+
115
+ # Create fake dark intermediate arrays
116
+ for beam in range(1, num_beams + 1):
117
+ task.intermediate_frame_write_arrays(
118
+ all_zeros,
119
+ beam=beam,
120
+ task_tag=CryonirspTag.task_dark(),
121
+ exposure_conditions=exposure_conditions,
122
+ )
123
+
124
+ # And a beam border intermediate array
125
+ for beam in range(1, num_beams + 1):
126
+ task.intermediate_frame_write_arrays(
127
+ arrays=np.array([0, 30, ((beam - 1) * 30), (30 + (beam - 1) * 30)]),
128
+ task_tag=CryonirspTag.task_beam_boundaries(),
129
+ beam=beam,
130
+ )
131
+
132
+ # Create fake lamp and solar gain intermediate arrays
133
+ for beam in range(1, num_beams + 1):
134
+ for modstate in range(1, num_modstates + 1):
135
+ gain_hdul = fits.HDUList([fits.PrimaryHDU(data=all_ones)])
136
+ task.write(
137
+ data=gain_hdul,
138
+ tags=[
139
+ CryonirspTag.intermediate(),
140
+ CryonirspTag.frame(),
141
+ CryonirspTag.task_lamp_gain(),
142
+ CryonirspTag.beam(beam),
143
+ CryonirspTag.modstate(modstate),
144
+ ],
145
+ encoder=fits_hdulist_encoder,
146
+ )
147
+
148
+ task.write(
149
+ data=gain_hdul,
150
+ tags=[
151
+ CryonirspTag.intermediate(),
152
+ CryonirspTag.frame(),
153
+ CryonirspTag.task_solar_gain(),
154
+ CryonirspTag.beam(beam),
155
+ CryonirspTag.modstate(modstate),
156
+ ],
157
+ encoder=fits_hdulist_encoder,
158
+ )
159
+
160
+ # Create fake observe arrays
161
+ start_time = datetime.now()
162
+ for map_scan in range(1, num_map_scans + 1):
163
+ for scan_step in range(1, num_scan_steps + 1):
164
+ for modstate in range(1, num_modstates + 1):
165
+ ds = CryonirspHeadersValidObserveFrames(
166
+ dataset_shape=dataset_shape,
167
+ array_shape=array_shape,
168
+ time_delta=10,
169
+ scan_step=scan_step,
170
+ num_scan_steps=num_scan_steps,
171
+ num_map_scans=num_map_scans,
172
+ map_scan=map_scan,
173
+ num_modstates=num_modstates,
174
+ modstate=modstate,
175
+ start_time=start_time,
176
+ num_meas=1,
177
+ meas_num=1,
178
+ arm_id="SP",
179
+ )
180
+ header_generator = (
181
+ spec122_validator.validate_and_translate_to_214_l0(
182
+ d.header(), return_type=fits.HDUList
183
+ )[0].header
184
+ for d in ds
185
+ )
186
+
187
+ hdul = generate_fits_frame(
188
+ header_generator=header_generator, shape=array_shape
189
+ )
190
+ header = hdul[0].header
191
+ task.write(
192
+ data=hdul,
193
+ tags=[
194
+ CryonirspTag.task_observe(),
195
+ CryonirspTag.meas_num(1),
196
+ CryonirspTag.scan_step(scan_step),
197
+ CryonirspTag.map_scan(map_scan),
198
+ CryonirspTag.modstate(modstate),
199
+ CryonirspTag.linearized(),
200
+ CryonirspTag.frame(),
201
+ CryonirspTag.exposure_conditions(exposure_conditions),
202
+ ],
203
+ encoder=fits_hdulist_encoder,
204
+ )
205
+
206
+ yield task, request.param, offset, header, intermediate_shape
207
+ finally:
208
+ task._purge()
209
+
210
+
211
+ @pytest.fixture(scope="function")
212
+ def sp_science_calibration_task_no_data(
213
+ tmp_path, recipe_run_id, init_cryonirsp_constants_db, is_polarimetric
214
+ ):
215
+ constants_db = CryonirspConstantsDb(
216
+ NUM_MODSTATES=2 if is_polarimetric else 1,
217
+ MODULATOR_SPIN_MODE="Continuous" if is_polarimetric else "Off",
218
+ )
219
+ init_cryonirsp_constants_db(recipe_run_id, constants_db)
220
+ with SPScienceCalibration(
221
+ recipe_run_id=recipe_run_id, workflow_name="sp_science_calibration", workflow_version="VX.Y"
222
+ ) as task:
223
+ try:
224
+ yield task
225
+ except:
226
+ raise
227
+ finally:
228
+ task._purge()
229
+
230
+
231
+ @pytest.fixture(scope="session")
232
+ def sp_headers_with_dates() -> tuple[list[fits.Header], str, int, int]:
233
+ num_headers = 5
234
+ start_time = "1969-12-06T18:00:00"
235
+ exp_time = 12
236
+ time_delta = 10
237
+ ds = CryonirspHeadersValidObserveFrames(
238
+ dataset_shape=(num_headers, 4, 4),
239
+ array_shape=(1, 4, 4),
240
+ time_delta=time_delta,
241
+ num_map_scans=1,
242
+ map_scan=1,
243
+ num_scan_steps=1,
244
+ scan_step=1,
245
+ num_modstates=num_headers,
246
+ modstate=1,
247
+ start_time=datetime.fromisoformat(start_time),
248
+ num_meas=1,
249
+ meas_num=1,
250
+ arm_id="SP",
251
+ )
252
+ headers = [
253
+ spec122_validator.validate_and_translate_to_214_l0(d.header(), return_type=fits.HDUList)[
254
+ 0
255
+ ].header
256
+ for d in ds
257
+ ]
258
+ random.shuffle(headers) # Shuffle to make sure they're not already in time order
259
+ for h in headers:
260
+ h["XPOSURE"] = exp_time # Exposure time, in ms
261
+
262
+ return headers, start_time, exp_time, time_delta
263
+
264
+
265
+ @pytest.fixture(scope="session")
266
+ def sp_compressed_headers_with_dates(
267
+ sp_headers_with_dates,
268
+ ) -> tuple[list[fits.Header], str, int, int]:
269
+ headers, start_time, exp_time, time_delta = sp_headers_with_dates
270
+ comp_headers = [fits.hdu.compressed.CompImageHeader(h, h) for h in headers]
271
+ return comp_headers, start_time, exp_time, time_delta
272
+
273
+
274
+ @pytest.fixture(scope="function")
275
+ def sp_calibration_collection_with_geo_shifts(shifts) -> CalibrationCollection:
276
+ num_beams, num_mod, _ = shifts.shape
277
+ geo_shifts = {
278
+ str(b + 1): {f"m{m + 1}": shifts[b, m, :] for m in range(num_mod)} for b in range(num_beams)
279
+ }
280
+ return CalibrationCollection(
281
+ dark=dict(),
282
+ angle=dict(),
283
+ state_offset=geo_shifts,
284
+ spec_shift=dict(),
285
+ demod_matrices=None,
286
+ )
287
+
288
+
289
+ @pytest.fixture
290
+ def calibrated_array_and_header_dicts(
291
+ sp_headers_with_dates, is_polarimetric
292
+ ) -> tuple[dict[str, np.ndarray], dict[str, fits.Header], tuple[int, int]]:
293
+ headers = sp_headers_with_dates[0]
294
+ header = headers[0]
295
+
296
+ num_stokes_params = 4 if is_polarimetric else 1
297
+ array_shape = (10, 10)
298
+ shape = (*array_shape, num_stokes_params)
299
+
300
+ beam1 = np.ones(shape) + np.arange(num_stokes_params)[None, None, :]
301
+ beam2 = np.ones(shape) + np.arange(num_stokes_params)[None, None, :] + 10
302
+
303
+ array_dict = {CryonirspTag.beam(1): beam1, CryonirspTag.beam(2): beam2}
304
+ header_dict = {CryonirspTag.beam(1): header, CryonirspTag.beam(2): header}
305
+
306
+ return array_dict, header_dict, array_shape
307
+
308
+
309
+ def test_sp_science_calibration_task(sp_science_calibration_task, mocker):
310
+ """
311
+ Given: A SPScienceCalibration task
312
+ When: Calling the task instance
313
+ Then: There are the expected number of science frames with the correct tags applied and the headers have been correctly updated
314
+ """
315
+
316
+ mocker.patch(
317
+ "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=FakeGQLClient
318
+ )
319
+
320
+ # When
321
+ task, polarization_mode, offset, og_header, og_single_beam_shape = sp_science_calibration_task
322
+ task()
323
+
324
+ # 1 from re-dummification
325
+ expected_final_shape = (
326
+ 1,
327
+ og_single_beam_shape[0],
328
+ og_single_beam_shape[1],
329
+ )
330
+
331
+ # Then
332
+ tags = [
333
+ CryonirspTag.calibrated(),
334
+ CryonirspTag.frame(),
335
+ ]
336
+ files = list(task.read(tags=tags))
337
+ if polarization_mode == "Full Stokes":
338
+ # 2 raster steps * 2 map scans * 4 stokes params = 16 frames
339
+ assert len(files) == 16
340
+ elif polarization_mode == "Stokes-I":
341
+ # 2 raster steps * 2 map scans * 1 stokes param = 4 frames
342
+ assert len(files) == 4
343
+ for file in files:
344
+ hdul = fits.open(file)
345
+ assert len(hdul) == 1
346
+ hdu = hdul[0]
347
+ assert type(hdul[0]) is fits.PrimaryHDU
348
+ assert hdu.data.shape == expected_final_shape
349
+ assert "DATE-BEG" in hdu.header.keys()
350
+ assert "DATE-END" in hdu.header.keys()
351
+ if polarization_mode == "Full Stokes":
352
+ assert "POL_NOIS" in hdu.header.keys()
353
+ assert "POL_SENS" in hdu.header.keys()
354
+
355
+ # Check that scan step keys were updated
356
+ scan_step = [
357
+ int(t.split("_")[-1]) for t in task.tags(file) if CryonirspStemName.scan_step.value in t
358
+ ][0]
359
+
360
+ assert hdu.header["CNNUMSCN"] == 2
361
+ assert hdu.header["CNCURSCN"] == scan_step
362
+
363
+ # Check that WCS keys were updated
364
+ if offset[0] > 0:
365
+ assert hdu.header["CRPIX2"] == og_header["CRPIX2"]
366
+ if offset[1] > 0:
367
+ assert hdu.header["CRPIX1"] == og_header["CRPIX1"]
368
+
369
+ quality_files = task.read(tags=[CryonirspTag.quality("TASK_TYPES")])
370
+ for file in quality_files:
371
+ with file.open() as f:
372
+ data = json.load(f)
373
+ assert isinstance(data, dict)
374
+ assert data["total_frames"] == task.scratch.count_all(
375
+ tags=[CryonirspTag.linearized(), CryonirspTag.frame(), CryonirspTag.task_observe()]
376
+ )
377
+
378
+
379
+ def test_compute_sp_date_keys(sp_headers_with_dates, recipe_run_id, init_cryonirsp_constants_db):
380
+ """
381
+ Given: A set of SP headers with different DATE-OBS values
382
+ When: Computing the time over which the headers were taken
383
+ Then: A header with correct DATE-BEG, DATE-END, and DATE-AVG keys is returned
384
+ """
385
+ headers, start_time, exp_time, time_delta = sp_headers_with_dates
386
+ constants_db = CryonirspConstantsDb()
387
+ init_cryonirsp_constants_db(recipe_run_id, constants_db)
388
+ with SPScienceCalibration(
389
+ recipe_run_id=recipe_run_id, workflow_name="science_calibration", workflow_version="VX.Y"
390
+ ) as task:
391
+ final_header = task.compute_date_keys(headers)
392
+ final_header_from_single = task.compute_date_keys(headers[0])
393
+
394
+ date_end = (
395
+ Time(start_time)
396
+ + (len(headers) - 1) * TimeDelta(time_delta, format="sec")
397
+ + TimeDelta(exp_time / 1000.0, format="sec")
398
+ ).isot
399
+
400
+ assert final_header["DATE-BEG"] == start_time
401
+ assert final_header["DATE-END"] == date_end
402
+
403
+ date_end_from_single = (
404
+ Time(headers[0]["DATE-BEG"])
405
+ # + TimeDelta(time_delta, format="sec")
406
+ + TimeDelta(exp_time / 1000.0, format="sec")
407
+ ).isot
408
+
409
+ assert final_header_from_single["DATE-BEG"] == headers[0]["DATE-BEG"]
410
+ assert final_header_from_single["DATE-END"] == date_end_from_single
411
+
412
+
413
+ def test_compute_sp_date_keys_compressed_headers(
414
+ sp_compressed_headers_with_dates, recipe_run_id, init_cryonirsp_constants_db
415
+ ):
416
+ """
417
+ Given: A set of SP compressed headers with different DATE-OBS values
418
+ When: Computing the time over which the headers were taken
419
+ Then: A header with correct DATE-BEG, DATE-END, and DATE-AVG keys is returned
420
+ """
421
+ headers, start_time, exp_time, time_delta = sp_compressed_headers_with_dates
422
+ constants_db = CryonirspConstantsDb()
423
+ init_cryonirsp_constants_db(recipe_run_id, constants_db)
424
+ with SPScienceCalibration(
425
+ recipe_run_id=recipe_run_id, workflow_name="science_calibration", workflow_version="VX.Y"
426
+ ) as task:
427
+ final_header = task.compute_date_keys(headers)
428
+ final_header_from_single = task.compute_date_keys(headers[0])
429
+
430
+ date_end = (
431
+ Time(start_time)
432
+ + (len(headers) - 1) * TimeDelta(time_delta, format="sec")
433
+ + TimeDelta(exp_time / 1000.0, format="sec")
434
+ ).isot
435
+
436
+ assert final_header["DATE-BEG"] == start_time
437
+ assert final_header["DATE-END"] == date_end
438
+
439
+ date_end_from_single = (
440
+ Time(headers[0]["DATE-BEG"]) + TimeDelta(exp_time / 1000.0, format="sec")
441
+ ).isot
442
+
443
+ assert final_header_from_single["DATE-BEG"] == headers[0]["DATE-BEG"]
444
+ assert final_header_from_single["DATE-END"] == date_end_from_single
445
+
446
+
447
+ @pytest.mark.parametrize(
448
+ "is_polarimetric", [pytest.param(True, id="Full Stokes"), pytest.param(False, id="Stokes-I")]
449
+ )
450
+ def test_combine_beams_into_fits_access(
451
+ sp_science_calibration_task_no_data, calibrated_array_and_header_dicts, is_polarimetric
452
+ ):
453
+ """
454
+ Given: A SPScienceCalibrationTask with a set of calibrated beam arrays and headers
455
+ When: Combining the beams
456
+ Then: The correct result is returned
457
+ """
458
+ array_dict, header_dict, array_shape = calibrated_array_and_header_dicts
459
+ task = sp_science_calibration_task_no_data
460
+
461
+ combined_result = task.combine_beams_into_fits_access(
462
+ array_dict=array_dict, header_dict=header_dict
463
+ )
464
+
465
+ assert isinstance(combined_result, CryonirspL0FitsAccess)
466
+
467
+ data = combined_result.data
468
+ expected_num_stokes = 4 if is_polarimetric else 1
469
+
470
+ # See `calibrated_array_and_header_dicts` for where these numbers come from
471
+ b1 = np.arange(1, expected_num_stokes + 1)
472
+ b2 = np.arange(1, expected_num_stokes + 1) + 10
473
+ avg_I = (b1[0] + b2[0]) / 2.0
474
+ if is_polarimetric:
475
+ expected_I = np.ones(array_shape) * avg_I
476
+ expected_Q = np.ones(array_shape) * (b1[1] / b1[0] + b2[1] / b2[0]) / 2.0 * avg_I
477
+ expected_U = np.ones(array_shape) * (b1[2] / b1[0] + b2[2] / b2[0]) / 2.0 * avg_I
478
+ expected_V = np.ones(array_shape) * (b1[3] / b1[0] + b2[3] / b2[0]) / 2.0 * avg_I
479
+ expected = np.dstack([expected_I, expected_Q, expected_U, expected_V])
480
+ else:
481
+ expected = np.ones((*array_shape, expected_num_stokes)) * avg_I
482
+
483
+ np.testing.assert_array_equal(data, expected)
@@ -0,0 +1,198 @@
1
+ import json
2
+
3
+ import numpy as np
4
+ import pytest
5
+ from dkist_processing_common._util.scratch import WorkflowFileSystem
6
+ from dkist_processing_common.codecs.fits import fits_hdulist_encoder
7
+ from dkist_processing_common.tests.conftest import FakeGQLClient
8
+
9
+ from dkist_processing_cryonirsp.models.exposure_conditions import AllowableOpticalDensityFilterNames
10
+ from dkist_processing_cryonirsp.models.exposure_conditions import ExposureConditions
11
+ from dkist_processing_cryonirsp.models.tags import CryonirspTag
12
+ from dkist_processing_cryonirsp.tasks.sp_solar_gain import SPSolarGainCalibration
13
+ from dkist_processing_cryonirsp.tests.conftest import cryonirsp_testing_parameters_factory
14
+ from dkist_processing_cryonirsp.tests.conftest import CryonirspConstantsDb
15
+ from dkist_processing_cryonirsp.tests.conftest import generate_214_l0_fits_frame
16
+ from dkist_processing_cryonirsp.tests.header_models import CryonirspHeadersValidSPSolarGainFrames
17
+
18
+
19
+ @pytest.fixture(scope="function")
20
+ def solar_gain_calibration_task_that_completes(
21
+ tmp_path,
22
+ recipe_run_id,
23
+ assign_input_dataset_doc_to_task,
24
+ init_cryonirsp_constants_db,
25
+ fringe_correction,
26
+ ):
27
+ number_of_modstates = 1
28
+ # Be careful here!!! We have some files that are beam specific and others that contain both beams!!!
29
+ number_of_beams = 2
30
+ exposure_time = 20.0 # From CryonirspHeadersValidSolarGainFrames fixture
31
+ exposure_conditions = ExposureConditions(
32
+ exposure_time, AllowableOpticalDensityFilterNames.OPEN.value
33
+ )
34
+ intermediate_shape = (10, 10)
35
+ dataset_shape = (1, 10, 20)
36
+ array_shape = (1, 10, 20)
37
+ constants_db = CryonirspConstantsDb(
38
+ NUM_MODSTATES=number_of_modstates,
39
+ SOLAR_GAIN_EXPOSURE_CONDITIONS_LIST=(exposure_conditions,),
40
+ ARM_ID="SP",
41
+ )
42
+ init_cryonirsp_constants_db(recipe_run_id, constants_db)
43
+ with SPSolarGainCalibration(
44
+ recipe_run_id=recipe_run_id,
45
+ workflow_name="sp_solar_gain_calibration",
46
+ workflow_version="VX.Y", # check workflow name?
47
+ ) as task:
48
+ try: # This try... block is here to make sure the dbs get cleaned up if there's a failure in the fixture
49
+ task.scratch = WorkflowFileSystem(
50
+ scratch_base_path=tmp_path, recipe_run_id=recipe_run_id
51
+ )
52
+ param_class = cryonirsp_testing_parameters_factory(param_path=tmp_path)
53
+ assign_input_dataset_doc_to_task(
54
+ task, param_class(cryonirsp_fringe_correction_on=fringe_correction)
55
+ )
56
+ # Create fake bad pixel map
57
+ task.intermediate_frame_write_arrays(
58
+ arrays=np.zeros((10, 20)),
59
+ task_tag=CryonirspTag.task_bad_pixel_map(),
60
+ )
61
+ for beam in range(1, number_of_beams + 1):
62
+ # Create fake beam border intermediate arrays
63
+ task.intermediate_frame_write_arrays(
64
+ arrays=np.array([0, 10, ((beam - 1) * 10), 10 + ((beam - 1) * 10)]),
65
+ task_tag=CryonirspTag.task_beam_boundaries(),
66
+ beam=beam,
67
+ )
68
+
69
+ # DarkCal object
70
+ dark_cal = np.ones(intermediate_shape) * 3.0
71
+ task.intermediate_frame_write_arrays(
72
+ arrays=dark_cal,
73
+ beam=beam,
74
+ task_tag=CryonirspTag.task_dark(),
75
+ exposure_conditions=exposure_conditions,
76
+ )
77
+
78
+ # Geo angles and spec_shifts
79
+ task.intermediate_frame_write_arrays(
80
+ arrays=np.zeros(1),
81
+ beam=beam,
82
+ task_tag=CryonirspTag.task_geometric_angle(),
83
+ )
84
+ task.intermediate_frame_write_arrays(
85
+ arrays=np.zeros(intermediate_shape[0]),
86
+ beam=beam,
87
+ task_tag=CryonirspTag.task_geometric_sepectral_shifts(),
88
+ )
89
+
90
+ for modstate in range(1, number_of_modstates + 1):
91
+ # LampCal object
92
+ lamp_cal_ramp = np.arange(1, intermediate_shape[1] + 1) / 5
93
+ lamp_cal_ramp = np.flip(lamp_cal_ramp)
94
+ lamp_cal = np.ones(intermediate_shape) * lamp_cal_ramp[None, :]
95
+ lamp_cal /= np.nanmean(lamp_cal)
96
+ task.intermediate_frame_write_arrays(
97
+ arrays=lamp_cal,
98
+ beam=beam,
99
+ modstate=modstate,
100
+ task_tag=CryonirspTag.task_lamp_gain(),
101
+ )
102
+
103
+ # Geo offsets
104
+ task.intermediate_frame_write_arrays(
105
+ arrays=np.zeros(2),
106
+ beam=beam,
107
+ modstate=modstate,
108
+ task_tag=CryonirspTag.task_geometric_offset(),
109
+ )
110
+
111
+ # Raw gain input images contain both beams, so are not beam specific!!!
112
+ ds = CryonirspHeadersValidSPSolarGainFrames(
113
+ dataset_shape=dataset_shape,
114
+ array_shape=array_shape,
115
+ time_delta=10,
116
+ )
117
+ header = ds.header()
118
+ true_gain = np.ones(intermediate_shape)
119
+ true_solar_signal = (
120
+ np.arange(1, intermediate_shape[1] + 1) / 5
121
+ ) # creates a trend from 0.2 to 2
122
+ true_solar_single_beam = true_gain * true_solar_signal[None, :]
123
+ true_solar_gain = np.concatenate(
124
+ (true_solar_single_beam, true_solar_single_beam), axis=1
125
+ )
126
+ raw_dark = np.concatenate((dark_cal, dark_cal), axis=1)
127
+ raw_solar = true_solar_gain + raw_dark
128
+ solar_hdul = generate_214_l0_fits_frame(data=raw_solar, s122_header=header)
129
+ task.write(
130
+ data=solar_hdul,
131
+ tags=[
132
+ CryonirspTag.linearized(),
133
+ CryonirspTag.task_solar_gain(),
134
+ CryonirspTag.modstate(modstate),
135
+ CryonirspTag.frame(),
136
+ CryonirspTag.exposure_conditions(exposure_conditions),
137
+ ],
138
+ encoder=fits_hdulist_encoder,
139
+ )
140
+
141
+ yield task, true_solar_single_beam
142
+ finally:
143
+ task._purge()
144
+
145
+
146
+ @pytest.fixture(scope="function")
147
+ def solar_gain_calibration_task_with_no_data(tmp_path, recipe_run_id, init_cryonirsp_constants_db):
148
+ number_of_modstates = 1
149
+ constants_db = CryonirspConstantsDb(NUM_MODSTATES=number_of_modstates, ARM_ID="SP")
150
+ init_cryonirsp_constants_db(recipe_run_id, constants_db)
151
+ with SPSolarGainCalibration(
152
+ recipe_run_id=recipe_run_id, workflow_name="geometric_calibration", workflow_version="VX.Y"
153
+ ) as task:
154
+ task.scratch = WorkflowFileSystem(scratch_base_path=tmp_path, recipe_run_id=recipe_run_id)
155
+
156
+ yield task
157
+ task._purge()
158
+
159
+
160
+ @pytest.mark.parametrize("fringe_correction", [False, True])
161
+ def test_solar_gain_task(solar_gain_calibration_task_that_completes, mocker, fringe_correction):
162
+ """
163
+ Given: A set of raw solar gain images and necessary intermediate calibrations
164
+ When: Running the solargain task
165
+ Then: The task completes and the outputs are correct
166
+ """
167
+ mocker.patch(
168
+ "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=FakeGQLClient
169
+ )
170
+
171
+ task, true_solar_single_beam = solar_gain_calibration_task_that_completes
172
+ task()
173
+ for beam in range(1, task.constants.num_beams + 1):
174
+ for modstate in range(1, task.constants.num_modstates + 1):
175
+ solar_gain = task.intermediate_frame_load_solar_gain_array(beam=beam)
176
+ # If fringe correction is on, then just be happy we got a file...
177
+ if task.parameters.fringe_correction_on:
178
+ continue
179
+ # The processed image is flipped, so we must flip the original to test
180
+ expected = np.flip(true_solar_single_beam, axis=1)
181
+ np.testing.assert_allclose(expected, solar_gain)
182
+ # Test for the existence of the spectral corrected solar array
183
+ spectral_corrected_array = task.intermediate_frame_load_intermediate_arrays(
184
+ tags=[CryonirspTag.beam(beam), CryonirspTag.task("SPECTRAL_CORRECTED_SOLAR_ARRAY")]
185
+ )
186
+
187
+ quality_files = task.read(tags=[CryonirspTag.quality("TASK_TYPES")])
188
+ for file in quality_files:
189
+ with file.open() as f:
190
+ data = json.load(f)
191
+ assert isinstance(data, dict)
192
+ assert data["total_frames"] == task.scratch.count_all(
193
+ tags=[
194
+ CryonirspTag.linearized(),
195
+ CryonirspTag.frame(),
196
+ CryonirspTag.task_solar_gain(),
197
+ ]
198
+ )