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,126 @@
1
+ from typing import Literal
2
+
3
+ import numpy as np
4
+ import pytest
5
+ from dkist_processing_common._util.scratch import WorkflowFileSystem
6
+ from dkist_processing_common.tasks import WorkflowTaskBase
7
+ from dkist_processing_common.tasks.mixin.input_dataset import InputDatasetMixin
8
+ from dkist_processing_common.tests.conftest import FakeGQLClient
9
+
10
+ from dkist_processing_cryonirsp.models.constants import CryonirspConstants
11
+ from dkist_processing_cryonirsp.tasks.mixin.corrections import CorrectionsMixin
12
+ from dkist_processing_cryonirsp.tests.conftest import cryonirsp_testing_parameters_factory
13
+ from dkist_processing_cryonirsp.tests.conftest import CryonirspConstantsDb
14
+
15
+ base_bad_pixel_map = np.zeros(shape=(10, 10))
16
+
17
+ normal_bad_pixel_map = base_bad_pixel_map.copy()
18
+ normal_bad_pixel_map[1, 6] = 1
19
+
20
+ column_error_bad_pixel_map = base_bad_pixel_map.copy()
21
+ column_error_bad_pixel_map[:, 6] = 1
22
+
23
+
24
+ class BadPixelMapTask(WorkflowTaskBase, CorrectionsMixin, InputDatasetMixin):
25
+ constants: CryonirspConstants
26
+
27
+ @property
28
+ def constants_model_class(self):
29
+ """Get CryoNIRSP pipeline constants."""
30
+ return CryonirspConstants
31
+
32
+ def run(self):
33
+ pass
34
+
35
+
36
+ @pytest.fixture(params=["CI", "SP"])
37
+ def bad_pixel_mask_task(
38
+ tmp_path,
39
+ recipe_run_id,
40
+ assign_input_dataset_doc_to_task,
41
+ mocker,
42
+ request,
43
+ init_cryonirsp_constants_db,
44
+ ):
45
+ mocker.patch(
46
+ "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=FakeGQLClient
47
+ )
48
+ constants_db = CryonirspConstantsDb(
49
+ ARM_ID=request.param,
50
+ )
51
+ init_cryonirsp_constants_db(recipe_run_id, constants_db)
52
+ with BadPixelMapTask(
53
+ recipe_run_id=recipe_run_id,
54
+ workflow_name="bad_pixel_mask",
55
+ workflow_version="VX.Y",
56
+ ) as task:
57
+ try: # This try... block is here to make sure the dbs get cleaned up if there's a failure in the fixture
58
+ task.scratch = WorkflowFileSystem(
59
+ scratch_base_path=tmp_path, recipe_run_id=recipe_run_id
60
+ )
61
+ param_class = cryonirsp_testing_parameters_factory(param_path=tmp_path)
62
+ assign_input_dataset_doc_to_task(task, param_class())
63
+ yield task
64
+ finally:
65
+ task._purge()
66
+
67
+
68
+ def contrive_bad_px_neighborhood(
69
+ array: np.ndarray, bad_px_location: tuple[int, int], kernel_size: int, desired_median: float
70
+ ) -> np.array:
71
+ """
72
+ Adjust array values so the neighborhood around a bad pixel results in a known value being used to replace that pixel.
73
+ """
74
+ # Sticking with the y, x convention used by `corrections_correct_bad_pixels`.
75
+ y, x = bad_px_location
76
+ half_kernel_size = kernel_size // 2
77
+ num_y, num_x = array.shape
78
+
79
+ y_slice = slice(max(y - half_kernel_size, 0), min(y + half_kernel_size + 1, num_y))
80
+ x_slice = slice(max(x - half_kernel_size, 0), min(x + half_kernel_size + 1, num_x))
81
+
82
+ array[y_slice, x_slice] = desired_median
83
+
84
+ return array
85
+
86
+
87
+ @pytest.mark.parametrize(
88
+ "bad_pixel_map, algorithm_type",
89
+ [
90
+ pytest.param(normal_bad_pixel_map, "normal", id="normal algorithm"),
91
+ pytest.param(column_error_bad_pixel_map, "fast", id="fast algorithm"),
92
+ ],
93
+ )
94
+ def test_corrections_correct_bad_pixels(bad_pixel_map, algorithm_type, bad_pixel_mask_task):
95
+ t = bad_pixel_mask_task
96
+ bad_pixel_x = 1
97
+ bad_pixel_y = 6
98
+
99
+ # Create a data array. Adding 10 ensures that 0 will be a valid sentinel value of bad-ness
100
+ rng = np.random.default_rng()
101
+ array_to_fix = rng.random((10, 10), dtype=float) * 100 + 10.0
102
+
103
+ # Assign a single bad pixel to check against
104
+ if algorithm_type == "normal":
105
+ expected_corrected_value = rng.random() * 100 + 10
106
+ array_to_fix = contrive_bad_px_neighborhood(
107
+ array=array_to_fix,
108
+ bad_px_location=(1, 6),
109
+ kernel_size=t.parameters.corrections_bad_pixel_median_filter_size,
110
+ desired_median=expected_corrected_value,
111
+ )
112
+ array_to_fix[bad_pixel_x, bad_pixel_y] = 0
113
+
114
+ corrected_array = t.corrections_correct_bad_pixels(
115
+ array_to_fix=array_to_fix, bad_pixel_map=bad_pixel_map
116
+ )
117
+ if algorithm_type == "fast":
118
+ for val in corrected_array[:, bad_pixel_y]:
119
+ assert val == np.nanmedian(array_to_fix)
120
+ assert corrected_array[bad_pixel_x, bad_pixel_y] == np.nanmedian(array_to_fix)
121
+
122
+ if algorithm_type == "normal":
123
+ x, y = np.meshgrid(np.arange(10), np.arange(10))
124
+ idx = np.where((x != bad_pixel_x) | (y != bad_pixel_y))
125
+ np.testing.assert_array_equal(corrected_array[idx], array_to_fix[idx])
126
+ assert corrected_array[bad_pixel_x, bad_pixel_y] == expected_corrected_value
@@ -0,0 +1,202 @@
1
+ import numpy as np
2
+ import pytest
3
+ from astropy.io import fits
4
+ from dkist_processing_common._util.scratch import WorkflowFileSystem
5
+ from dkist_processing_common.codecs.fits import fits_hdu_decoder
6
+
7
+ from dkist_processing_cryonirsp.models.tags import CryonirspTag
8
+ from dkist_processing_cryonirsp.models.task_name import CryonirspTaskName
9
+ from dkist_processing_cryonirsp.tasks.cryonirsp_base import CryonirspTaskBase
10
+ from dkist_processing_cryonirsp.tasks.mixin.intermediate_frame import (
11
+ IntermediateFrameMixin,
12
+ )
13
+ from dkist_processing_cryonirsp.tests.conftest import cryonirsp_testing_parameters_factory
14
+ from dkist_processing_cryonirsp.tests.conftest import CryonirspConstantsDb
15
+
16
+ NUM_BEAMS = 2
17
+ NUM_MODSTATES = 8
18
+ NUM_CS_STEPS = 6
19
+ NUM_SCAN_STEPS = 10
20
+ WAVE = 1082.0
21
+
22
+
23
+ @pytest.fixture(scope="function")
24
+ def cryo_science_task(
25
+ tmp_path, recipe_run_id, assign_input_dataset_doc_to_task, init_cryonirsp_constants_db
26
+ ):
27
+ class Task(CryonirspTaskBase, IntermediateFrameMixin):
28
+ def run(self):
29
+ ...
30
+
31
+ constants_db = CryonirspConstantsDb(
32
+ NUM_MODSTATES=NUM_MODSTATES,
33
+ NUM_CS_STEPS=NUM_CS_STEPS,
34
+ NUM_SCAN_STEPS=NUM_SCAN_STEPS,
35
+ WAVELENGTH=WAVE,
36
+ )
37
+ init_cryonirsp_constants_db(recipe_run_id, constants_db)
38
+ with Task(
39
+ recipe_run_id=recipe_run_id,
40
+ workflow_name="parse_cryonirsp_input_data",
41
+ workflow_version="VX.Y",
42
+ ) as task:
43
+ try:
44
+ task.scratch = WorkflowFileSystem(
45
+ scratch_base_path=tmp_path, recipe_run_id=recipe_run_id
46
+ )
47
+ param_class = cryonirsp_testing_parameters_factory(param_path=tmp_path)
48
+ assign_input_dataset_doc_to_task(task, param_class())
49
+ yield task
50
+ finally:
51
+ task._purge()
52
+
53
+
54
+ def test_write_intermediate_arrays(cryo_science_task):
55
+ """
56
+ Given: A CryonirspTaskBase task
57
+ When: Using the helper to write a single intermediate array
58
+ Then: The array is written and tagged correctly
59
+ """
60
+ data = np.random.random((10, 10))
61
+ head = fits.Header()
62
+ head["TEST"] = "foo"
63
+ cryo_science_task.intermediate_frame_write_arrays(
64
+ arrays=data, headers=head, beam=1, map_scan=2, scan_step=3, task="BAR"
65
+ )
66
+ loaded_list = list(
67
+ cryo_science_task.read(
68
+ tags=[
69
+ CryonirspTag.intermediate(),
70
+ CryonirspTag.frame(),
71
+ CryonirspTag.beam(1),
72
+ CryonirspTag.map_scan(2),
73
+ CryonirspTag.scan_step(3),
74
+ CryonirspTag.task("BAR"),
75
+ ],
76
+ decoder=fits_hdu_decoder,
77
+ )
78
+ )
79
+ assert len(loaded_list) == 1
80
+ hdu = loaded_list[0]
81
+ np.testing.assert_equal(hdu.data, data)
82
+ assert hdu.header["TEST"] == "foo"
83
+
84
+
85
+ def test_write_intermediate_arrays_task_tag(cryo_science_task):
86
+ """
87
+ Given: A CryonirspTaskBase task
88
+ When: Using the helper to write a single intermediate array with a formatted task tag input arg
89
+ Then: The array is written and tagged correctly
90
+ """
91
+ data = np.random.random((10, 10))
92
+ head = fits.Header()
93
+ head["TEST"] = "foo"
94
+
95
+ # bad_pixel_map chosen for no particular reason
96
+ cryo_science_task.intermediate_frame_write_arrays(
97
+ arrays=data, headers=head, task_tag=CryonirspTag.task_bad_pixel_map()
98
+ )
99
+ loaded_list = list(
100
+ cryo_science_task.read(
101
+ tags=[CryonirspTag.task_bad_pixel_map()],
102
+ decoder=fits_hdu_decoder,
103
+ )
104
+ )
105
+ assert len(loaded_list) == 1
106
+ hdu = loaded_list[0]
107
+ np.testing.assert_equal(hdu.data, data)
108
+ assert hdu.header["TEST"] == "foo"
109
+
110
+
111
+ def test_write_intermediate_arrays_task_collisions(cryo_science_task):
112
+ """
113
+ Given: A CryonirspTaskBase task
114
+ When: Using the helper but providing invalid `task` or `task_tag` inputs
115
+ Then: An error is raised
116
+ """
117
+ data = np.random.random((10, 10))
118
+
119
+ # Test both given
120
+ with pytest.raises(ValueError, match="Cannot specify"):
121
+ cryo_science_task.intermediate_frame_write_arrays(
122
+ arrays=data, task_tag=CryonirspTag.task_bad_pixel_map(), task="DARK"
123
+ )
124
+
125
+ # Test neither given
126
+ with pytest.raises(ValueError, match="Must specify"):
127
+ cryo_science_task.intermediate_frame_write_arrays(arrays=data)
128
+
129
+
130
+ def test_write_intermediate_arrays_none_header(cryo_science_task):
131
+ """
132
+ Given: A CryonirspTaskBase task
133
+ When: Using the helper to write a single intermediate array with no header
134
+ Then: The array is written and tagged correctly
135
+ """
136
+ data = np.random.random((10, 10))
137
+ cryo_science_task.intermediate_frame_write_arrays(
138
+ arrays=data, headers=None, beam=1, map_scan=2, scan_step=3, task="BAR"
139
+ )
140
+ loaded_list = list(
141
+ cryo_science_task.read(
142
+ tags=[
143
+ CryonirspTag.intermediate(),
144
+ CryonirspTag.frame(),
145
+ CryonirspTag.beam(1),
146
+ CryonirspTag.map_scan(2),
147
+ CryonirspTag.scan_step(3),
148
+ CryonirspTag.task("BAR"),
149
+ ],
150
+ decoder=fits_hdu_decoder,
151
+ )
152
+ )
153
+ assert len(loaded_list) == 1
154
+ hdu = loaded_list[0]
155
+ np.testing.assert_equal(hdu.data, data)
156
+
157
+
158
+ @pytest.fixture
159
+ def cryo_science_task_with_tagged_intermediates(
160
+ recipe_run_id, tmpdir_factory, init_cryonirsp_constants_db
161
+ ):
162
+ class Task(CryonirspTaskBase, IntermediateFrameMixin):
163
+ def run(self):
164
+ ...
165
+
166
+ init_cryonirsp_constants_db(recipe_run_id, CryonirspConstantsDb())
167
+ with Task(
168
+ recipe_run_id=recipe_run_id,
169
+ workflow_name="parse_cryo_input_data",
170
+ workflow_version="VX.Y",
171
+ ) as task:
172
+ try:
173
+ task.scratch = WorkflowFileSystem(scratch_base_path=tmpdir_factory.mktemp("data"))
174
+ tag_names = [["beam"], ["exposure_time", "task"], ["modstate"]]
175
+ tag_vals = [[1], [10.23, "dark"], [3]]
176
+ tag_fcns = [[getattr(CryonirspTag, n) for n in nl] for nl in tag_names]
177
+ tag_list = [[f(v) for f, v in zip(fl, vl)] for fl, vl in zip(tag_fcns, tag_vals)]
178
+ for i, tags in enumerate(tag_list):
179
+ hdul = fits.HDUList([fits.PrimaryHDU(data=np.ones((2, 2)) * i)])
180
+ fname = task.scratch.workflow_base_path / f"file{i}.fits"
181
+ hdul.writeto(fname)
182
+ task.tag(fname, tags + [CryonirspTag.intermediate(), CryonirspTag.frame()])
183
+
184
+ yield task, tag_names, tag_vals
185
+ finally:
186
+ task._purge()
187
+
188
+
189
+ def test_load_intermediate_arrays(cryo_science_task_with_tagged_intermediates):
190
+ """
191
+ Given: A task with tagged intermediate frames
192
+ When: Using intermediate frame loaders to grab the intermediate frames
193
+ Then: The correct arrays are returned
194
+ """
195
+ task, tag_names, tag_vals = cryo_science_task_with_tagged_intermediates
196
+ tag_list_list = [
197
+ [getattr(CryonirspTag, n)(v) for n, v in zip(nl, vl)] for nl, vl in zip(tag_names, tag_vals)
198
+ ]
199
+ for i, tags in enumerate(tag_list_list):
200
+ arrays = list(task.intermediate_frame_load_intermediate_arrays(tags=tags))
201
+ assert len(arrays) == 1
202
+ np.testing.assert_equal(arrays[0], np.ones((2, 2)) * i)
@@ -0,0 +1,76 @@
1
+ from dataclasses import asdict
2
+ from dataclasses import dataclass
3
+
4
+ import pytest
5
+
6
+ from dkist_processing_cryonirsp.models.exposure_conditions import AllowableOpticalDensityFilterNames
7
+ from dkist_processing_cryonirsp.models.exposure_conditions import ExposureConditions
8
+ from dkist_processing_cryonirsp.tasks.cryonirsp_base import CryonirspTaskBase
9
+
10
+
11
+ @pytest.fixture(scope="function")
12
+ def testing_constants(polarimetric):
13
+ @dataclass
14
+ class testing_constants:
15
+ obs_ip_start_time: str = "1999-12-31T23:59:59"
16
+ num_modstates: int = 10 if polarimetric else 1
17
+ num_beams: int = 2
18
+ num_cs_steps: int = 18
19
+ num_scan_steps: int = 1000
20
+ wavelength: float = 1082.0
21
+ lamp_gain_exposure_conditions_list: tuple[ExposureConditions, ...] = (
22
+ ExposureConditions(100.0, AllowableOpticalDensityFilterNames.OPEN.value),
23
+ )
24
+ solar_gain_exposure_conditions_list: tuple[ExposureConditions, ...] = (
25
+ ExposureConditions(1.0, AllowableOpticalDensityFilterNames.OPEN.value),
26
+ )
27
+ observe_exposure_conditions_list: tuple[ExposureConditions, ...] = (
28
+ ExposureConditions(0.01, AllowableOpticalDensityFilterNames.OPEN.value),
29
+ )
30
+ modulator_spin_mode: str = "Continuous" if polarimetric else "Off"
31
+ # We don't need all the common ones, but let's put one just to check
32
+ instrument: str = "CHECK_OUT_THIS_INSTRUMENT"
33
+ arm_id: str = "SP"
34
+
35
+ return testing_constants()
36
+
37
+
38
+ @pytest.fixture(scope="function")
39
+ def expected_constant_dict(testing_constants) -> dict:
40
+ lower_dict = asdict(testing_constants)
41
+ return {k.upper(): v for k, v in lower_dict.items()}
42
+
43
+
44
+ @pytest.fixture(scope="function")
45
+ def cryo_science_task_with_constants(
46
+ recipe_run_id, expected_constant_dict, init_cryonirsp_constants_db
47
+ ):
48
+ class Task(CryonirspTaskBase):
49
+ def run(self):
50
+ ...
51
+
52
+ init_cryonirsp_constants_db(recipe_run_id, expected_constant_dict)
53
+ task = Task(
54
+ recipe_run_id=recipe_run_id,
55
+ workflow_name="parse_cryo_input_data",
56
+ workflow_version="VX.Y",
57
+ )
58
+
59
+ yield task
60
+
61
+ task._purge()
62
+
63
+
64
+ @pytest.mark.parametrize(
65
+ "polarimetric",
66
+ [pytest.param(True, id="Polarimetric"), pytest.param(False, id="Spectrographic")],
67
+ )
68
+ def test_cryo_constants(cryo_science_task_with_constants, expected_constant_dict, polarimetric):
69
+ task = cryo_science_task_with_constants
70
+ for k, v in expected_constant_dict.items():
71
+ if k.lower() == "modulator_spin_mode":
72
+ continue
73
+ if type(v) is tuple:
74
+ v = list(v)
75
+ assert getattr(task.constants, k.lower()) == v
76
+ assert task.constants.correct_for_polarization == polarimetric
@@ -0,0 +1,287 @@
1
+ import json
2
+
3
+ import numpy as np
4
+ import pytest
5
+ from astropy.io import fits
6
+ from dkist_header_validator import spec122_validator
7
+ from dkist_processing_common._util.scratch import WorkflowFileSystem
8
+ from dkist_processing_common.codecs.fits import fits_hdulist_encoder
9
+ from dkist_processing_common.tests.conftest import FakeGQLClient
10
+
11
+ from dkist_processing_cryonirsp.models.exposure_conditions import AllowableOpticalDensityFilterNames
12
+ from dkist_processing_cryonirsp.models.exposure_conditions import ExposureConditions
13
+ from dkist_processing_cryonirsp.models.tags import CryonirspTag
14
+ from dkist_processing_cryonirsp.tasks.dark import DarkCalibration
15
+ from dkist_processing_cryonirsp.tests.conftest import cryonirsp_testing_parameters_factory
16
+ from dkist_processing_cryonirsp.tests.conftest import CryonirspConstantsDb
17
+ from dkist_processing_cryonirsp.tests.conftest import generate_fits_frame
18
+ from dkist_processing_cryonirsp.tests.header_models import CryonirspHeadersValidDarkFrames
19
+
20
+
21
+ @pytest.fixture(scope="function")
22
+ def sp_dark_calibration_task(
23
+ tmp_path, assign_input_dataset_doc_to_task, init_cryonirsp_constants_db, recipe_run_id
24
+ ):
25
+ # Make sure we test cases where either the exp time or filter are the same, but the other value is
26
+ # different
27
+ exposure_conditions = (
28
+ ExposureConditions(100.0, AllowableOpticalDensityFilterNames.OPEN.value),
29
+ ExposureConditions(1.0, AllowableOpticalDensityFilterNames.G278.value),
30
+ ExposureConditions(0.01, AllowableOpticalDensityFilterNames.NONE.value),
31
+ ExposureConditions(100.0, AllowableOpticalDensityFilterNames.G278.value),
32
+ )
33
+ unused_exposure_condition = ExposureConditions(
34
+ 200.0, AllowableOpticalDensityFilterNames.NONE.value
35
+ )
36
+ constants_db = CryonirspConstantsDb(
37
+ NON_DARK_AND_NON_POLCAL_TASK_EXPOSURE_CONDITIONS_LIST=exposure_conditions,
38
+ ARM_ID="SP",
39
+ )
40
+ init_cryonirsp_constants_db(recipe_run_id, constants_db)
41
+ with DarkCalibration(
42
+ recipe_run_id=recipe_run_id, workflow_name="dark_calibration", workflow_version="VX.Y"
43
+ ) as task:
44
+ illuminated_beam_shape = (6, 4)
45
+ num_beams = 2
46
+ num_exp_cond = len(exposure_conditions) + 1 # +1 for the unused condition
47
+ num_frames_per_condition = 3
48
+ array_shape = (1, 10, 20)
49
+ dataset_shape = (num_exp_cond * num_frames_per_condition, 20, 10)
50
+ try: # This try... block is here to make sure the dbs get cleaned up if there's a failure in the fixture
51
+ task.scratch = WorkflowFileSystem(
52
+ scratch_base_path=tmp_path, recipe_run_id=recipe_run_id
53
+ )
54
+ param_class = cryonirsp_testing_parameters_factory(param_path=tmp_path)
55
+ assign_input_dataset_doc_to_task(task, param_class())
56
+
57
+ # Create fake beam border intermediate arrays
58
+ for beam in range(1, num_beams + 1):
59
+ spectral_starting_pixel = 5 + ((beam - 1) * 10)
60
+ beam_boundaries = np.array(
61
+ [
62
+ 3,
63
+ 3 + illuminated_beam_shape[0],
64
+ spectral_starting_pixel,
65
+ spectral_starting_pixel + illuminated_beam_shape[1],
66
+ ]
67
+ )
68
+ task.intermediate_frame_write_arrays(
69
+ arrays=beam_boundaries,
70
+ task_tag=CryonirspTag.task_beam_boundaries(),
71
+ beam=beam,
72
+ )
73
+
74
+ ds = CryonirspHeadersValidDarkFrames(
75
+ dataset_shape=dataset_shape,
76
+ array_shape=array_shape,
77
+ time_delta=10,
78
+ exposure_time=1.0,
79
+ )
80
+ header_generator = (
81
+ spec122_validator.validate_and_translate_to_214_l0(
82
+ d.header(), return_type=fits.HDUList
83
+ )[0].header
84
+ for d in ds
85
+ )
86
+
87
+ for condition in exposure_conditions + (unused_exposure_condition,):
88
+ for _ in range(num_frames_per_condition):
89
+ hdul = generate_fits_frame(header_generator=header_generator, shape=array_shape)
90
+ hdul[0].data.fill(condition.exposure_time)
91
+ task.write(
92
+ data=hdul,
93
+ tags=[
94
+ CryonirspTag.linearized(),
95
+ CryonirspTag.frame(),
96
+ CryonirspTag.task_dark(),
97
+ CryonirspTag.exposure_conditions(condition),
98
+ ],
99
+ encoder=fits_hdulist_encoder,
100
+ )
101
+ yield task, num_beams, exposure_conditions, unused_exposure_condition, illuminated_beam_shape
102
+ finally:
103
+ task._purge()
104
+
105
+
106
+ @pytest.fixture(scope="function")
107
+ def ci_dark_calibration_task(
108
+ tmp_path, assign_input_dataset_doc_to_task, init_cryonirsp_constants_db, recipe_run_id
109
+ ):
110
+ # Make sure we test cases where either the exp time or filter are the same, but the other value is
111
+ # different
112
+ exposure_conditions = (
113
+ ExposureConditions(100.0, AllowableOpticalDensityFilterNames.OPEN.value),
114
+ ExposureConditions(1.0, AllowableOpticalDensityFilterNames.G278.value),
115
+ ExposureConditions(0.01, AllowableOpticalDensityFilterNames.NONE.value),
116
+ ExposureConditions(100.0, AllowableOpticalDensityFilterNames.G278.value),
117
+ )
118
+ unused_exposure_condition = ExposureConditions(
119
+ 200.0, AllowableOpticalDensityFilterNames.NONE.value
120
+ )
121
+
122
+ constants_db = CryonirspConstantsDb(
123
+ NON_DARK_AND_NON_POLCAL_TASK_EXPOSURE_CONDITIONS_LIST=exposure_conditions,
124
+ ARM_ID="CI",
125
+ )
126
+ init_cryonirsp_constants_db(recipe_run_id, constants_db)
127
+ with DarkCalibration(
128
+ recipe_run_id=recipe_run_id, workflow_name="dark_calibration", workflow_version="VX.Y"
129
+ ) as task:
130
+ num_exp_cond = len(exposure_conditions) + 1 # +1 for the unused condition
131
+ num_frames_per_condition = 3
132
+ array_shape = (1, 10, 10)
133
+ dataset_shape = (num_exp_cond * num_frames_per_condition, 20, 10)
134
+ try: # This try... block is here to make sure the dbs get cleaned up if there's a failure in the fixture
135
+ task.scratch = WorkflowFileSystem(
136
+ scratch_base_path=tmp_path, recipe_run_id=recipe_run_id
137
+ )
138
+ param_class = cryonirsp_testing_parameters_factory(param_path=tmp_path)
139
+ assign_input_dataset_doc_to_task(task, param_class())
140
+
141
+ # Need a beam boundary file
142
+ task.intermediate_frame_write_arrays(
143
+ arrays=np.array([0, 10, 0, 10]),
144
+ task_tag=CryonirspTag.task_beam_boundaries(),
145
+ beam=1,
146
+ )
147
+
148
+ ds = CryonirspHeadersValidDarkFrames(
149
+ dataset_shape=dataset_shape,
150
+ array_shape=array_shape,
151
+ time_delta=10,
152
+ exposure_time=1.0,
153
+ )
154
+ header_generator = (
155
+ spec122_validator.validate_and_translate_to_214_l0(
156
+ d.header(), return_type=fits.HDUList
157
+ )[0].header
158
+ for d in ds
159
+ )
160
+ for condition in exposure_conditions + (unused_exposure_condition,):
161
+ for _ in range(num_frames_per_condition):
162
+ hdul = generate_fits_frame(header_generator=header_generator, shape=array_shape)
163
+ hdul[0].data.fill(condition.exposure_time)
164
+ task.write(
165
+ data=hdul,
166
+ tags=[
167
+ CryonirspTag.linearized(),
168
+ CryonirspTag.frame(),
169
+ CryonirspTag.task_dark(),
170
+ CryonirspTag.exposure_conditions(condition),
171
+ ],
172
+ encoder=fits_hdulist_encoder,
173
+ )
174
+ yield task, exposure_conditions, unused_exposure_condition
175
+ finally:
176
+ task._purge()
177
+
178
+
179
+ def test_sp_dark_calibration_task(sp_dark_calibration_task, mocker):
180
+ """
181
+ Given: A DarkCalibration task with multiple task exposure times
182
+ When: Calling the task instance
183
+ Then: Only one average intermediate dark frame exists for each exposure time and unused times are not made
184
+ """
185
+ mocker.patch(
186
+ "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=FakeGQLClient
187
+ )
188
+ # When
189
+ (
190
+ task,
191
+ num_beams,
192
+ exp_conditions,
193
+ unused_condition,
194
+ illuminated_beam_shape,
195
+ ) = sp_dark_calibration_task
196
+ task()
197
+ # Then
198
+ for condition in exp_conditions:
199
+ for b in range(num_beams):
200
+ files = list(
201
+ task.read(
202
+ tags=[
203
+ CryonirspTag.task_dark(),
204
+ CryonirspTag.intermediate(),
205
+ CryonirspTag.frame(),
206
+ CryonirspTag.beam(b + 1),
207
+ CryonirspTag.exposure_conditions(condition),
208
+ ]
209
+ )
210
+ )
211
+ assert len(files) == 1
212
+ expected = np.ones(illuminated_beam_shape) * condition.exposure_time
213
+ hdul = fits.open(files[0])
214
+ np.testing.assert_equal(expected, hdul[0].data)
215
+ hdul.close()
216
+
217
+ unused_time_read = task.read(
218
+ tags=[
219
+ CryonirspTag.task_dark(),
220
+ CryonirspTag.intermediate(),
221
+ CryonirspTag.frame(),
222
+ CryonirspTag.exposure_conditions(unused_condition),
223
+ ]
224
+ )
225
+ assert len(list(unused_time_read)) == 0
226
+
227
+ quality_files = task.read(tags=[CryonirspTag.quality("TASK_TYPES")])
228
+ for file in quality_files:
229
+ with file.open() as f:
230
+ data = json.load(f)
231
+ assert isinstance(data, dict)
232
+ assert data["total_frames"] == task.scratch.count_all(
233
+ tags=[CryonirspTag.linearized(), CryonirspTag.frame(), CryonirspTag.task_dark()]
234
+ )
235
+ assert data["frames_not_used"] == 3
236
+
237
+
238
+ def test_ci_dark_calibration_task(ci_dark_calibration_task, mocker):
239
+ """
240
+ Given: A DarkCalibration task with multiple task exposure times
241
+ When: Calling the task instance
242
+ Then: Only one average intermediate dark frame exists for each exposure time and unused times are not made
243
+ """
244
+ mocker.patch(
245
+ "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=FakeGQLClient
246
+ )
247
+ # When
248
+ task, exp_conditions, unused_condition = ci_dark_calibration_task
249
+ task()
250
+ # Then
251
+ for condition in exp_conditions:
252
+ files = list(
253
+ task.read(
254
+ tags=[
255
+ CryonirspTag.task_dark(),
256
+ CryonirspTag.beam(1),
257
+ CryonirspTag.intermediate(),
258
+ CryonirspTag.frame(),
259
+ CryonirspTag.exposure_conditions(condition),
260
+ ]
261
+ )
262
+ )
263
+ assert len(files) == 1
264
+ expected = np.ones((10, 10)) * condition.exposure_time
265
+ hdul = fits.open(files[0])
266
+ np.testing.assert_equal(expected, hdul[0].data)
267
+ hdul.close()
268
+
269
+ unused_time_read = task.read(
270
+ tags=[
271
+ CryonirspTag.task_dark(),
272
+ CryonirspTag.intermediate(),
273
+ CryonirspTag.frame(),
274
+ CryonirspTag.exposure_conditions(unused_condition),
275
+ ]
276
+ )
277
+ assert len(list(unused_time_read)) == 0
278
+
279
+ quality_files = task.read(tags=[CryonirspTag.quality("TASK_TYPES")])
280
+ for file in quality_files:
281
+ with file.open() as f:
282
+ data = json.load(f)
283
+ assert isinstance(data, dict)
284
+ assert data["total_frames"] == task.scratch.count_all(
285
+ tags=[CryonirspTag.linearized(), CryonirspTag.frame(), CryonirspTag.task_dark()]
286
+ )
287
+ assert data["frames_not_used"] == 3