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,144 @@
1
+ import numpy as np
2
+ import pytest
3
+ from dkist_processing_common._util.scratch import WorkflowFileSystem
4
+ from dkist_processing_common.codecs.fits import fits_hdulist_encoder
5
+ from dkist_processing_common.tests.conftest import FakeGQLClient
6
+
7
+ from dkist_processing_cryonirsp.models.constants import CryonirspBudName
8
+ from dkist_processing_cryonirsp.models.tags import CryonirspTag
9
+ from dkist_processing_cryonirsp.tasks.assemble_movie import AssembleCryonirspMovie
10
+ from dkist_processing_cryonirsp.tasks.assemble_movie import SPAssembleCryonirspMovie
11
+ from dkist_processing_cryonirsp.tests.conftest import CryonirspConstantsDb
12
+ from dkist_processing_cryonirsp.tests.conftest import generate_214_l1_fits_frame
13
+ from dkist_processing_cryonirsp.tests.header_models import Cryonirsp122ObserveFrames
14
+
15
+
16
+ @pytest.fixture(
17
+ scope="function", params=[pytest.param(True, id="shrink"), pytest.param(False, id="noshrink")]
18
+ )
19
+ def assemble_movie_task_with_tagged_movie_frames(
20
+ request,
21
+ tmp_path,
22
+ recipe_run_id,
23
+ init_cryonirsp_constants_db,
24
+ ):
25
+ num_map_scans = 10
26
+ num_scan_steps = 1
27
+ if request.param:
28
+ frame_shape = (1080 * 2, 1920 * 2) # Intentionally "backward" from normal
29
+ expected_shape = (1080, 1920)[::-1]
30
+ else:
31
+ frame_shape = (100, 235) # Weird aspect ratio
32
+ expected_shape = (100, 235)[::-1]
33
+ init_cryonirsp_constants_db(recipe_run_id, CryonirspConstantsDb(NUM_MAP_SCANS=num_map_scans))
34
+ with AssembleCryonirspMovie(
35
+ recipe_run_id=recipe_run_id,
36
+ workflow_name="ci_cryo_make_movie_frames",
37
+ workflow_version="VX.Y",
38
+ ) as task:
39
+ try: # This try... block is here to make sure the dbs get cleaned up if there's a failure in the fixture
40
+ task.scratch = WorkflowFileSystem(
41
+ scratch_base_path=tmp_path, recipe_run_id=recipe_run_id
42
+ )
43
+ task.testing_num_map_scans = num_map_scans
44
+ task.num_steps = num_scan_steps
45
+ task.num_exp_per_step = 1
46
+ ds = Cryonirsp122ObserveFrames(
47
+ array_shape=(1, *frame_shape),
48
+ num_steps=task.num_steps,
49
+ num_exp_per_step=task.num_exp_per_step,
50
+ num_map_scans=task.testing_num_map_scans,
51
+ )
52
+ header_generator = (d.header() for d in ds)
53
+ data = np.random.random((1, *frame_shape))
54
+ for d, header in enumerate(header_generator):
55
+ for scan_step in range(num_scan_steps + 1):
56
+ hdl = generate_214_l1_fits_frame(s122_header=header, data=data)
57
+ task.write(
58
+ data=hdl,
59
+ tags=[
60
+ CryonirspTag.movie_frame(),
61
+ CryonirspTag.map_scan(d + 1),
62
+ CryonirspTag.scan_step(scan_step),
63
+ ],
64
+ encoder=fits_hdulist_encoder,
65
+ )
66
+ yield task, frame_shape, expected_shape
67
+ finally:
68
+ task._purge()
69
+
70
+
71
+ def test_assemble_movie(assemble_movie_task_with_tagged_movie_frames, mocker):
72
+ mocker.patch(
73
+ "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=FakeGQLClient
74
+ )
75
+ task, _, _ = assemble_movie_task_with_tagged_movie_frames
76
+ task()
77
+ movie_file = list(task.read(tags=[CryonirspTag.movie()]))
78
+ assert len(movie_file) == 1
79
+ assert movie_file[0].exists()
80
+
81
+
82
+ def test_compute_frame_shape(assemble_movie_task_with_tagged_movie_frames):
83
+ """
84
+ Given: An AssembleCryonirspMovieFrames task and OUTPUT frames
85
+ When: Computing the size of the final movie
86
+ Then: The correct size is computed: the size of an L1 output frame
87
+ """
88
+ task, frame_shape, expected_shape = assemble_movie_task_with_tagged_movie_frames
89
+
90
+ # Task starts as polarimetric from fixture constants
91
+ assert task.compute_frame_shape() == tuple(expected_shape)
92
+
93
+ # Update constants to be non-polarimetric
94
+ del task.constants._db_dict[CryonirspBudName.num_modstates.value]
95
+ task.constants._db_dict[CryonirspBudName.num_modstates.value] = 1
96
+ assert task.compute_frame_shape() == tuple(expected_shape)
97
+
98
+
99
+ @pytest.fixture(scope="function")
100
+ def assemble_sp_task_with_tagged_movie_frames(tmp_path, recipe_run_id, init_cryonirsp_constants_db):
101
+ num_map_scans = 10
102
+ init_cryonirsp_constants_db(recipe_run_id, CryonirspConstantsDb(NUM_MAP_SCANS=num_map_scans))
103
+ with SPAssembleCryonirspMovie(
104
+ recipe_run_id=recipe_run_id,
105
+ workflow_name="sp_cryo_make_movie_frames",
106
+ workflow_version="VX.Y",
107
+ ) as task:
108
+ try: # This try... block is here to make sure the dbs get cleaned up if there's a failure in the fixture
109
+ task.scratch = WorkflowFileSystem(
110
+ scratch_base_path=tmp_path, recipe_run_id=recipe_run_id
111
+ )
112
+ task.testing_num_map_scans = num_map_scans
113
+ task.num_steps = 1 # do we need num_steps and num_exp_per_step for SP?
114
+ task.num_exp_per_step = 1
115
+ ds = Cryonirsp122ObserveFrames(
116
+ array_shape=(1, 100, 100),
117
+ num_steps=task.num_steps,
118
+ num_exp_per_step=task.num_exp_per_step,
119
+ num_map_scans=task.testing_num_map_scans,
120
+ )
121
+ header_generator = (d.header() for d in ds)
122
+ for d, header in enumerate(header_generator):
123
+ hdl = generate_214_l1_fits_frame(s122_header=header)
124
+ task.write(
125
+ data=hdl,
126
+ tags=[
127
+ CryonirspTag.movie_frame(),
128
+ CryonirspTag.map_scan(d + 1),
129
+ ],
130
+ encoder=fits_hdulist_encoder,
131
+ )
132
+ yield task
133
+ finally:
134
+ task._purge()
135
+
136
+
137
+ def test_assemble_sp_movie(assemble_sp_task_with_tagged_movie_frames, mocker):
138
+ mocker.patch(
139
+ "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=FakeGQLClient
140
+ )
141
+ assemble_sp_task_with_tagged_movie_frames()
142
+ movie_file = list(assemble_sp_task_with_tagged_movie_frames.read(tags=[CryonirspTag.movie()]))
143
+ assert len(movie_file) == 1
144
+ assert movie_file[0].exists()
@@ -0,0 +1,517 @@
1
+ import json
2
+ import re
3
+ from dataclasses import dataclass
4
+ from itertools import chain
5
+ from typing import Callable
6
+ from unittest.mock import MagicMock
7
+ from uuid import uuid4
8
+
9
+ import numpy as np
10
+ import pytest
11
+ from dkist_processing_common._util.scratch import WorkflowFileSystem
12
+ from dkist_processing_common.codecs.json import json_decoder
13
+ from dkist_processing_common.tasks import AssembleQualityData
14
+ from dkist_quality.report import ReportMetric
15
+ from pandas import DataFrame
16
+
17
+ from dkist_processing_cryonirsp.models.tags import CryonirspTag
18
+ from dkist_processing_cryonirsp.tasks.l1_output_data import CIAssembleQualityData
19
+ from dkist_processing_cryonirsp.tasks.l1_output_data import SPAssembleQualityData
20
+ from dkist_processing_cryonirsp.tests.conftest import CryonirspConstantsDb
21
+
22
+
23
+ @pytest.fixture(scope="function", params=["CI", "SP"])
24
+ def cryo_assemble_quality_data_task(
25
+ tmp_path, recipe_run_id, init_cryonirsp_constants_db, request
26
+ ) -> AssembleQualityData:
27
+ arm_id = request.param
28
+ init_cryonirsp_constants_db(
29
+ recipe_run_id=recipe_run_id, constants_obj=CryonirspConstantsDb(ARM_ID=arm_id)
30
+ )
31
+ if arm_id == "CI":
32
+ with CIAssembleQualityData(
33
+ recipe_run_id=recipe_run_id,
34
+ workflow_name="cryonirsp_submit_quality",
35
+ workflow_version="VX.Y",
36
+ ) as task:
37
+ yield task, arm_id
38
+ task._purge()
39
+ elif arm_id == "SP":
40
+ with SPAssembleQualityData(
41
+ recipe_run_id=recipe_run_id,
42
+ workflow_name="cryonirsp_submit_quality",
43
+ workflow_version="VX.Y",
44
+ ) as task:
45
+ yield task, arm_id
46
+ task._purge()
47
+ else:
48
+ raise RuntimeError(f"Invalid cryo-nirsp arm_id: {arm_id!r}")
49
+
50
+
51
+ @pytest.fixture
52
+ def dummy_quality_data() -> list[dict]:
53
+ return [{"dummy_key": "dummy_value"}]
54
+
55
+
56
+ @pytest.fixture
57
+ def quality_assemble_data_mock(mocker, dummy_quality_data) -> MagicMock:
58
+ yield mocker.patch(
59
+ "dkist_processing_common.tasks.mixin.quality.QualityMixin.quality_assemble_data",
60
+ return_value=dummy_quality_data,
61
+ autospec=True,
62
+ )
63
+
64
+
65
+ @dataclass
66
+ class Metric:
67
+ value: dict | list
68
+ tags: list[str]
69
+
70
+ @property
71
+ def value_bytes(self) -> bytes:
72
+ return json.dumps(self.value).encode()
73
+
74
+ @property
75
+ def file_name(self) -> str:
76
+ # always include the metric in the filename
77
+ metric = re.sub("[ _]", "-", self.tags[0])
78
+ # if a second tag is present, include it in the filename
79
+ second_tag = re.sub("[ _]", "-", self.tags[1]) if len(self.tags) > 1 else None
80
+ if second_tag:
81
+ return f"{metric}_{second_tag}_{uuid4().hex[:6]}.dat"
82
+ return f"{metric}_{uuid4().hex[:6]}.dat"
83
+
84
+
85
+ @pytest.fixture()
86
+ def dataframe_json() -> str:
87
+ """Random dataframe for raincloud_plot"""
88
+ nummod = 3
89
+ numstep = 10
90
+ numpoints = 100
91
+ points = np.random.randn(numpoints * numstep * nummod)
92
+ mods = np.hstack([np.arange(nummod) + 1 for i in range(numstep * numpoints)])
93
+ steps = np.hstack([np.arange(numstep) + 1 for i in range(nummod * numpoints)])
94
+ data = np.vstack((points, mods, steps)).T
95
+ return DataFrame(data=data, columns=["Flux residual", "Modstate", "CS Step"]).to_json()
96
+
97
+
98
+ @pytest.fixture()
99
+ def quality_metrics(dataframe_json) -> list[Metric]:
100
+ """
101
+ Quality metric data
102
+ """
103
+ metrics = [
104
+ Metric(
105
+ {
106
+ "x_values": ["2021-01-01T01:01:01", "2021-01-01T02:01:01"],
107
+ "y_values": [1, 2],
108
+ "series_name": "",
109
+ },
110
+ ["QUALITY_FRAME_AVERAGE", "QUALITY_TASK_DARK"],
111
+ ),
112
+ Metric(
113
+ {
114
+ "x_values": ["2021-01-01T01:01:01", "2021-01-01T02:01:01"],
115
+ "y_values": [3, 4],
116
+ "series_name": "",
117
+ },
118
+ ["QUALITY_FRAME_AVERAGE", "QUALITY_TASK_GAIN"],
119
+ ),
120
+ Metric(
121
+ {
122
+ "x_values": ["2021-01-01T01:01:01", "2021-01-01T02:01:01"],
123
+ "y_values": [5, 6],
124
+ "series_name": "",
125
+ },
126
+ ["QUALITY_FRAME_RMS", "QUALITY_TASK_DARK"],
127
+ ),
128
+ Metric(
129
+ {
130
+ "x_values": ["2021-01-01T01:01:01", "2021-01-01T02:01:01"],
131
+ "y_values": [7, 8],
132
+ "series_name": "",
133
+ },
134
+ ["QUALITY_FRAME_RMS", "QUALITY_TASK_GAIN"],
135
+ ),
136
+ Metric(
137
+ {"task_type": "gain", "frame_averages": [6, 7, 8, 9, 10]}, ["QUALITY_DATASET_AVERAGE"]
138
+ ),
139
+ Metric(
140
+ {"task_type": "dark", "frame_averages": [1, 2, 3, 4, 5]}, ["QUALITY_DATASET_AVERAGE"]
141
+ ),
142
+ Metric(
143
+ {"task_type": "dark", "frame_averages": [6, 7, 8, 9, 10]}, ["QUALITY_DATASET_AVERAGE"]
144
+ ),
145
+ Metric({"task_type": "dark", "frame_rms": [1, 2, 3, 4, 5]}, ["QUALITY_DATASET_RMS"]),
146
+ Metric({"task_type": "dark", "frame_rms": [6, 7, 8, 8, 10]}, ["QUALITY_DATASET_RMS"]),
147
+ Metric({"task_type": "gain", "frame_rms": [2, 4, 6, 8, 10]}, ["QUALITY_DATASET_RMS"]),
148
+ Metric(
149
+ {
150
+ "x_values": ["2021-01-01T01:01:01", "2021-01-01T02:01:01"],
151
+ "y_values": [1, 2],
152
+ "series_name": "",
153
+ },
154
+ ["QUALITY_FRIED_PARAMETER"],
155
+ ),
156
+ Metric(
157
+ {
158
+ "x_values": ["2021-01-01T01:01:01", "2021-01-01T02:01:01"],
159
+ "y_values": [3, 4],
160
+ "series_name": "",
161
+ },
162
+ ["QUALITY_LIGHT_LEVEL"],
163
+ ),
164
+ Metric(["Good", "Good", "Good", "Good", "Good", "Ill"], ["QUALITY_HEALTH_STATUS"]),
165
+ Metric([1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0], ["QUALITY_AO_STATUS"]),
166
+ Metric(
167
+ {
168
+ "x_values": ["2021-01-01T01:01:01", "2021-01-01T02:01:01"],
169
+ "y_values": [5, 6],
170
+ "series_name": "",
171
+ },
172
+ ["QUALITY_NOISE"],
173
+ ),
174
+ Metric(
175
+ {
176
+ "x_values": ["2021-01-01T01:01:01", "2021-01-01T02:01:01"],
177
+ "y_values": [1, 2],
178
+ "series_name": "I",
179
+ },
180
+ ["QUALITY_SENSITIVITY", "STOKES_I"],
181
+ ),
182
+ Metric(
183
+ {
184
+ "x_values": ["2021-01-01T01:01:01", "2021-01-01T02:01:01"],
185
+ "y_values": [3, 4],
186
+ "series_name": "Q",
187
+ },
188
+ ["QUALITY_SENSITIVITY", "STOKES_Q"],
189
+ ),
190
+ Metric(
191
+ {
192
+ "x_values": ["2021-01-01T01:01:01", "2021-01-01T02:01:01"],
193
+ "y_values": [5, 6],
194
+ "series_name": "U",
195
+ },
196
+ ["QUALITY_SENSITIVITY", "STOKES_U"],
197
+ ),
198
+ Metric(
199
+ {
200
+ "x_values": ["2021-01-01T01:01:01", "2021-01-01T02:01:01"],
201
+ "y_values": [7, 8],
202
+ "series_name": "V",
203
+ },
204
+ ["QUALITY_SENSITIVITY", "STOKES_V"],
205
+ ),
206
+ Metric(
207
+ {"task_type": "dark", "total_frames": 100, "frames_not_used": 7}, ["QUALITY_TASK_TYPES"]
208
+ ),
209
+ Metric(
210
+ {"task_type": "gain", "total_frames": 100, "frames_not_used": 0}, ["QUALITY_TASK_TYPES"]
211
+ ),
212
+ Metric(
213
+ {
214
+ "param_names": ["foo"],
215
+ "param_vary": [True],
216
+ "param_init_vals": [1],
217
+ "param_fit_vals": [2],
218
+ "param_diffs": [1],
219
+ "param_ratios": [1],
220
+ "warnings": ["A warning"],
221
+ },
222
+ ["QUALITY_POLCAL_GLOBAL_PAR_VALS", "QUALITY_TASK_CI BEAM 1"],
223
+ ),
224
+ Metric(
225
+ {
226
+ "label": "Beam foo",
227
+ "modmat_list": np.random.randn(8, 4, 100).tolist(),
228
+ "free_param_dict": {
229
+ "I_sys_CS00_step00": {"fit_values": [1, 2, 3.0], "init_value": 0.3},
230
+ "I_sys_CS00_step01": {"fit_values": [10, 20, 30.0], "init_value": 0.33},
231
+ "param_X": {"fit_values": [5, 6, 7.0], "init_value": 99},
232
+ },
233
+ "bin_strs": ["bin1", "bin2"],
234
+ "total_bins": 100,
235
+ "num_varied_I_sys": 2,
236
+ },
237
+ ["QUALITY_POLCAL_LOCAL_PAR_VALS", "QUALITY_TASK_CI BEAM 1"],
238
+ ),
239
+ Metric(
240
+ {
241
+ "bin_strs": ["bin1", "bin2"],
242
+ "total_bins": 100,
243
+ "red_chi_list": [1, 2, 3],
244
+ "residual_json": dataframe_json,
245
+ },
246
+ ["QUALITY_POLCAL_FIT_RESIDUALS", "QUALITY_TASK_CI BEAM 1"],
247
+ ),
248
+ Metric(
249
+ {
250
+ "bin_strs": ["bin1", "bin2"],
251
+ "total_bins": 100,
252
+ "efficiency_list": ((np.random.randn(4, 100) - 0.5) * 0.3).tolist(),
253
+ "warnings": ["A warning"],
254
+ },
255
+ ["QUALITY_POLCAL_EFFICIENCY", "QUALITY_TASK_CI BEAM 1"],
256
+ ),
257
+ Metric({"name": "metric 1", "warnings": ["warning 1"]}, ["QUALITY_RANGE"]),
258
+ Metric({"name": "metric 2", "warnings": ["warning 2"]}, ["QUALITY_RANGE"]),
259
+ Metric({"name": "metric 3", "warnings": ["warning 3"]}, ["QUALITY_RANGE"]),
260
+ Metric({"name": "hist 1", "value": 7, "warnings": None}, ["QUALITY_HISTORICAL"]),
261
+ Metric({"name": "hist 2", "value": "abc", "warnings": None}, ["QUALITY_HISTORICAL"]),
262
+ Metric(
263
+ {"name": "hist 3", "value": 9.35, "warnings": "warning for historical metric 3"},
264
+ ["QUALITY_HISTORICAL"],
265
+ ),
266
+ ]
267
+ return metrics
268
+
269
+
270
+ @pytest.fixture()
271
+ def plot_data_expected() -> Callable[[str], bool]:
272
+ """
273
+ Tightly coupled with quality_metrics fixture and resultant report metric name
274
+ """
275
+ # names where plot_data is expected to be populated
276
+ names = {
277
+ "Average Across Frame - DARK",
278
+ "Average Across Frame - GAIN",
279
+ "Root Mean Square (RMS) Across Frame - DARK",
280
+ "Root Mean Square (RMS) Across Frame - GAIN",
281
+ "Fried Parameter",
282
+ "Light Level",
283
+ "Noise Estimation",
284
+ "Sensitivity",
285
+ }
286
+
287
+ def expected(name: str) -> bool:
288
+ return name in names
289
+
290
+ return expected
291
+
292
+
293
+ @pytest.fixture()
294
+ def table_data_expected() -> Callable[[str], bool]:
295
+ """
296
+ Tightly coupled with quality_metrics fixture and resultant report metric name
297
+ """
298
+ # names where table_data is expected to be populated
299
+ names = {
300
+ "Average Across Dataset",
301
+ "Dataset RMS",
302
+ "Data Source Health",
303
+ "Frame Counts",
304
+ "PolCal Global Calibration Unit Fit - CI Beam 1",
305
+ "Historical Comparisons",
306
+ }
307
+
308
+ def expected(name: str) -> bool:
309
+ return name in names
310
+
311
+ return expected
312
+
313
+
314
+ @pytest.fixture()
315
+ def modmat_data_expected() -> Callable[[str], bool]:
316
+ """
317
+ Tightly coupled with quality_metrics fixture and resultant report metric name
318
+ """
319
+ # names where modmat_data is expected to be populated
320
+ names = {
321
+ "PolCal Local Bin Fits - CI Beam 1",
322
+ }
323
+
324
+ def expected(name: str) -> bool:
325
+ return name in names
326
+
327
+ return expected
328
+
329
+
330
+ @pytest.fixture()
331
+ def histogram_data_expected() -> Callable[[str], bool]:
332
+ """
333
+ Tightly coupled with quality_metrics fixture and resultant report metric name
334
+ """
335
+ # names where histogram_data is expected to be populated
336
+ names = {
337
+ "PolCal Local Bin Fits - CI Beam 1",
338
+ "PolCal Fit Residuals - CI Beam 1",
339
+ }
340
+
341
+ def expected(name: str) -> bool:
342
+ return name in names
343
+
344
+ return expected
345
+
346
+
347
+ @pytest.fixture()
348
+ def raincloud_data_expected() -> Callable[[str], bool]:
349
+ """
350
+ Tightly coupled with quality_metrics fixture and resultant report metric name
351
+ """
352
+ # names where raincloud_data is expected to be populated
353
+ names = {
354
+ "PolCal Fit Residuals - CI Beam 1",
355
+ }
356
+
357
+ def expected(name: str) -> bool:
358
+ return name in names
359
+
360
+ return expected
361
+
362
+
363
+ @pytest.fixture()
364
+ def efficiency_data_expected() -> Callable[[str], bool]:
365
+ """
366
+ Tightly coupled with quality_metrics fixture and resultant report metric name
367
+ """
368
+ # names where efficiency_data is expected to be populated
369
+ names = {
370
+ "PolCal Modulation Efficiency - CI Beam 1",
371
+ }
372
+
373
+ def expected(name: str) -> bool:
374
+ return name in names
375
+
376
+ return expected
377
+
378
+
379
+ @pytest.fixture()
380
+ def statement_expected() -> Callable[[str], bool]:
381
+ """
382
+ Tightly coupled with quality_metrics fixture and resultant report metric name
383
+ """
384
+ # names where statement is expected to be populated
385
+ names = {
386
+ "Fried Parameter",
387
+ "Light Level",
388
+ "Adaptive Optics Status",
389
+ }
390
+
391
+ def expected(name: str) -> bool:
392
+ return name in names
393
+
394
+ return expected
395
+
396
+
397
+ @pytest.fixture()
398
+ def warnings_expected() -> Callable[[str], bool]:
399
+ """
400
+ Tightly coupled with quality_metrics fixture and resultant report metric name
401
+ """
402
+ # names where warnings is expected to be populated
403
+ names = {
404
+ "Data Source Health",
405
+ "Frame Counts",
406
+ "PolCal Global Calibration Unit Fit - CI Beam 1",
407
+ "PolCal Modulation Efficiency - CI Beam 1",
408
+ "Range checks",
409
+ "Historical Comparisons",
410
+ }
411
+
412
+ def expected(name: str) -> bool:
413
+ return name in names
414
+
415
+ return expected
416
+
417
+
418
+ @pytest.fixture()
419
+ def scratch_with_quality_metrics(recipe_run_id, tmp_path, quality_metrics) -> WorkflowFileSystem:
420
+ """Scratch instance for a recipe run id with tagged quality metrics."""
421
+ scratch = WorkflowFileSystem(
422
+ recipe_run_id=recipe_run_id,
423
+ scratch_base_path=tmp_path,
424
+ )
425
+ for metric in quality_metrics:
426
+ scratch.write(metric.value_bytes, tags=metric.tags, relative_path=metric.file_name)
427
+ return scratch
428
+
429
+
430
+ @pytest.fixture
431
+ def assemble_quality_data_task(
432
+ tmp_path, recipe_run_id, scratch_with_quality_metrics, init_cryonirsp_constants_db
433
+ ):
434
+ constants_db = CryonirspConstantsDb(NUM_MODSTATES=2)
435
+ init_cryonirsp_constants_db(recipe_run_id, constants_db)
436
+ with CIAssembleQualityData(
437
+ recipe_run_id=recipe_run_id,
438
+ workflow_name="ci_assemble_quality",
439
+ workflow_version="ci_assemble_quality_version",
440
+ ) as task:
441
+ task.scratch = WorkflowFileSystem(
442
+ recipe_run_id=recipe_run_id,
443
+ scratch_base_path=tmp_path,
444
+ )
445
+ task.scratch = scratch_with_quality_metrics
446
+ yield task
447
+ task._purge()
448
+
449
+
450
+ def test_assemble_quality_data(
451
+ assemble_quality_data_task,
452
+ recipe_run_id,
453
+ plot_data_expected,
454
+ table_data_expected,
455
+ modmat_data_expected,
456
+ histogram_data_expected,
457
+ raincloud_data_expected,
458
+ efficiency_data_expected,
459
+ statement_expected,
460
+ warnings_expected,
461
+ ):
462
+ """
463
+ :Given: An instance of CIAssembleQualityData with tagged quality metrics
464
+ :When: CIAssembleQualityData is run
465
+ :Then: A json quality data file for the dataset gets saved and tagged
466
+ """
467
+ task = assemble_quality_data_task
468
+
469
+ # When
470
+ task()
471
+ # Then
472
+ # each quality_data file is a list - this will combine the elements of multiple lists into a single list
473
+ quality_data = list(
474
+ chain.from_iterable(task.read(tags=CryonirspTag.quality_data(), decoder=json_decoder))
475
+ )
476
+ # 19 with polcal
477
+ assert len(quality_data) == 19
478
+ for metric_data in quality_data:
479
+ rm: ReportMetric = ReportMetric.from_dict(metric_data)
480
+ assert isinstance(rm.name, str)
481
+ assert isinstance(rm.description, str)
482
+ if plot_data_expected(rm.name):
483
+ assert rm.plot_data
484
+ if table_data_expected(rm.name):
485
+ assert rm.table_data
486
+ if modmat_data_expected(rm.name):
487
+ assert rm.modmat_data
488
+ if histogram_data_expected(rm.name):
489
+ assert rm.histogram_data
490
+ if raincloud_data_expected(rm.name):
491
+ assert rm.raincloud_data
492
+ if efficiency_data_expected(rm.name):
493
+ assert rm.efficiency_data
494
+ if statement_expected(rm.name):
495
+ assert rm.statement
496
+ if warnings_expected(rm.name):
497
+ assert rm.warnings
498
+
499
+
500
+ def test_correct_polcal_label_list(cryo_assemble_quality_data_task, quality_assemble_data_mock):
501
+ """
502
+ Given: A CIAssembleQualityData task
503
+ When: Calling the task
504
+ Then: The correct polcal_label_list property is passed to .quality_assemble_data
505
+ """
506
+ task, arm_id = cryo_assemble_quality_data_task
507
+
508
+ task()
509
+
510
+ if arm_id == "SP":
511
+ quality_assemble_data_mock.assert_called_once_with(
512
+ task, polcal_label_list=["SP Beam 1", "SP Beam 2"]
513
+ )
514
+ elif arm_id == "CI":
515
+ quality_assemble_data_mock.assert_called_once_with(task, polcal_label_list=["CI Beam 1"])
516
+ else:
517
+ raise RuntimeError(f"Invalid cryo-nirsp arm_id: {arm_id!r}")