dkist-processing-trend 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. changelog/.gitempty +0 -0
  2. dkist_processing_trend/__init__.py +10 -0
  3. dkist_processing_trend/config.py +11 -0
  4. dkist_processing_trend/models/__init__.py +1 -0
  5. dkist_processing_trend/models/constants.py +143 -0
  6. dkist_processing_trend/models/fit_options.py +15 -0
  7. dkist_processing_trend/models/fits_access.py +65 -0
  8. dkist_processing_trend/models/instrument.py +35 -0
  9. dkist_processing_trend/models/instrument_options.py +35 -0
  10. dkist_processing_trend/models/parameters.py +134 -0
  11. dkist_processing_trend/models/tags.py +117 -0
  12. dkist_processing_trend/models/task_name.py +20 -0
  13. dkist_processing_trend/parsers/__init__.py +1 -0
  14. dkist_processing_trend/parsers/arm_id.py +103 -0
  15. dkist_processing_trend/parsers/instrument_unique_bud.py +40 -0
  16. dkist_processing_trend/parsers/time.py +27 -0
  17. dkist_processing_trend/parsers/trend_l0_fits_access.py +123 -0
  18. dkist_processing_trend/tasks/__init__.py +27 -0
  19. dkist_processing_trend/tasks/arm_task_factory.py +57 -0
  20. dkist_processing_trend/tasks/dark.py +56 -0
  21. dkist_processing_trend/tasks/gain.py +74 -0
  22. dkist_processing_trend/tasks/initialize_arm_tasks.py +50 -0
  23. dkist_processing_trend/tasks/parse.py +169 -0
  24. dkist_processing_trend/tasks/prepare_fit_data_base.py +257 -0
  25. dkist_processing_trend/tasks/run_pac_fitter.py +392 -0
  26. dkist_processing_trend/tasks/trend_base.py +97 -0
  27. dkist_processing_trend/tasks/trend_output_data.py +167 -0
  28. dkist_processing_trend/tasks/visp/__init__.py +6 -0
  29. dkist_processing_trend/tasks/visp/visp_dmpd.py +410 -0
  30. dkist_processing_trend/tasks/visp/visp_extract_beam.py +14 -0
  31. dkist_processing_trend/tasks/visp/visp_geometric.py +260 -0
  32. dkist_processing_trend/tasks/visp/visp_prep_fit_data.py +162 -0
  33. dkist_processing_trend/tasks/visp/visp_process_demod.py +236 -0
  34. dkist_processing_trend/tasks/write_trend.py +663 -0
  35. dkist_processing_trend/tests/__init__.py +1 -0
  36. dkist_processing_trend/tests/conftest.py +718 -0
  37. dkist_processing_trend/tests/local_trial_workflows/__init__.py +0 -0
  38. dkist_processing_trend/tests/local_trial_workflows/l0_to_trend_visp_polcal.py +294 -0
  39. dkist_processing_trend/tests/local_trial_workflows/local_trial_helpers.py +488 -0
  40. dkist_processing_trend/tests/test_arm_task_factory.py +82 -0
  41. dkist_processing_trend/tests/test_base_tasks.py +86 -0
  42. dkist_processing_trend/tests/test_constants.py +120 -0
  43. dkist_processing_trend/tests/test_dark.py +97 -0
  44. dkist_processing_trend/tests/test_gain.py +135 -0
  45. dkist_processing_trend/tests/test_parameters.py +149 -0
  46. dkist_processing_trend/tests/test_parse.py +276 -0
  47. dkist_processing_trend/tests/test_prep_fit_data_base.py +233 -0
  48. dkist_processing_trend/tests/test_publish_catalog_messages.py +45 -0
  49. dkist_processing_trend/tests/test_run_pac_fitter.py +371 -0
  50. dkist_processing_trend/tests/test_stems.py +75 -0
  51. dkist_processing_trend/tests/test_transfer_output_data.py +76 -0
  52. dkist_processing_trend/tests/test_trend_fits_access.py +173 -0
  53. dkist_processing_trend/tests/test_visp.py +874 -0
  54. dkist_processing_trend/tests/test_workflows.py +10 -0
  55. dkist_processing_trend/tests/test_write_trend.py +460 -0
  56. dkist_processing_trend/workflows/__init__.py +3 -0
  57. dkist_processing_trend/workflows/visp.py +58 -0
  58. dkist_processing_trend-0.1.0.dist-info/METADATA +549 -0
  59. dkist_processing_trend-0.1.0.dist-info/RECORD +66 -0
  60. dkist_processing_trend-0.1.0.dist-info/WHEEL +5 -0
  61. dkist_processing_trend-0.1.0.dist-info/top_level.txt +3 -0
  62. docs/conf.py +57 -0
  63. docs/index.rst +10 -0
  64. docs/l0_to_trend_visp_polcal.rst +4 -0
  65. docs/landing_page.rst +11 -0
  66. docs/requirements_table.rst +8 -0
@@ -0,0 +1,276 @@
1
+ import json
2
+
3
+ import astropy.units as u
4
+ import pytest
5
+ from astropy.time.core import Time
6
+ from astropy.time.core import TimeDelta
7
+ from dkist_processing_common._util.scratch import WorkflowFileSystem
8
+ from dkist_processing_common.models.constants import BudName
9
+
10
+ from dkist_processing_trend.models.constants import TrendBudName
11
+ from dkist_processing_trend.models.instrument import InstrumentName
12
+ from dkist_processing_trend.models.parameters import TrendParsingParameters
13
+ from dkist_processing_trend.models.tags import TrendTag
14
+ from dkist_processing_trend.tasks.parse import ParseL0TrendInputData
15
+ from dkist_processing_trend.tests.conftest import ARM_ANGLE_FOR_ARM
16
+ from dkist_processing_trend.tests.conftest import CRYO_OD_FILTER_FOR_ARM
17
+ from dkist_processing_trend.tests.conftest import CRYO_SLIT_WIDTH_FOR_ARM
18
+ from dkist_processing_trend.tests.conftest import GRATING_ANGLE_FOR_ARM
19
+ from dkist_processing_trend.tests.conftest import GRATING_CONSTANT_FOR_ARM
20
+ from dkist_processing_trend.tests.conftest import WAVELENGTH_FOR_ARM
21
+ from dkist_processing_trend.tests.conftest import TrendInputDatasetParameterValues
22
+ from dkist_processing_trend.tests.conftest import write_calibration_sequence_frames_to_task
23
+
24
+
25
+ @pytest.fixture
26
+ def input_parse_task(tmp_path, recipe_run_id, assign_input_dataset_doc_to_task):
27
+ with ParseL0TrendInputData(
28
+ recipe_run_id=recipe_run_id,
29
+ workflow_name="workflow_name",
30
+ workflow_version="workflow_version",
31
+ ) as task:
32
+ task.scratch = WorkflowFileSystem(scratch_base_path=tmp_path, recipe_run_id=recipe_run_id)
33
+ assign_input_dataset_doc_to_task(
34
+ task=task,
35
+ parameter_values=TrendInputDatasetParameterValues(),
36
+ )
37
+ task.parameters = TrendParsingParameters(scratch=task.scratch)
38
+
39
+ yield task
40
+ task._purge()
41
+
42
+
43
+ @pytest.mark.parametrize(
44
+ "instrument, arm_id_list, op_id_list, expected_op_id_constant, experiment_id_list, expected_experiment_id_constant, proposal_id_list, expected_proposal_id_constant, hls_list, expected_hls_version_constant",
45
+ [
46
+ pytest.param(
47
+ InstrumentName.visp,
48
+ [1, 2, 3],
49
+ ["OP1"],
50
+ ["OP1"],
51
+ ["EX1"],
52
+ ["EX1"],
53
+ ["PROP1"],
54
+ ["PROP1"],
55
+ ["hlsv1"],
56
+ ["hlsv1"],
57
+ id="visp",
58
+ ),
59
+ pytest.param(
60
+ InstrumentName.cryonirsp,
61
+ ["CI", "SP"],
62
+ ["OP1"],
63
+ ["OP1"],
64
+ ["EX1"],
65
+ ["EX1"],
66
+ ["PROP1"],
67
+ ["PROP1"],
68
+ ["hlsv1"],
69
+ ["hlsv1"],
70
+ id="cryonirsp",
71
+ ),
72
+ pytest.param(
73
+ InstrumentName.dlnirsp,
74
+ ["VIS", "JBand", "HBand"],
75
+ ["OP1"],
76
+ ["OP1"],
77
+ ["EX1"],
78
+ ["EX1"],
79
+ ["PROP1"],
80
+ ["PROP1"],
81
+ ["hlsv1"],
82
+ ["hlsv1"],
83
+ id="dlnirsp",
84
+ ),
85
+ pytest.param(
86
+ InstrumentName.visp,
87
+ [1, 2],
88
+ ["OP2", "OP1", "OP2"],
89
+ ["OP2", "OP1"],
90
+ ["EX6", "EX6", "EX7"],
91
+ ["EX6", "EX7"],
92
+ ["PROP1", "PROP1", "PROP1"],
93
+ ["PROP1"],
94
+ ["hlsv1", "hlsv2", "hlsv2"],
95
+ ["hlsv2", "hlsv1"],
96
+ id="multi_OP",
97
+ ),
98
+ ],
99
+ )
100
+ def test_parse_input_data(
101
+ input_parse_task,
102
+ instrument,
103
+ arm_id_list,
104
+ op_id_list,
105
+ expected_op_id_constant,
106
+ experiment_id_list,
107
+ expected_experiment_id_constant,
108
+ proposal_id_list,
109
+ expected_proposal_id_constant,
110
+ hls_list,
111
+ expected_hls_version_constant,
112
+ visp_grating_angle,
113
+ visp_arm_position,
114
+ visp_grating_constant,
115
+ cryonirsp_od_filter_name,
116
+ cryonirsp_grating_angle,
117
+ cryonirsp_grating_littrow_angle,
118
+ cryonirsp_grating_constant,
119
+ cryonirsp_slit_width,
120
+ dlnirsp_grating_angle,
121
+ dlnirsp_arm_position,
122
+ dlnirsp_grating_constant,
123
+ small_calibration_sequence,
124
+ small_num_gain_steps,
125
+ small_num_dark_steps,
126
+ small_num_cs_steps,
127
+ ):
128
+ """
129
+ Given: A set of INPUT frames and a Parse task
130
+ When: Parsing the frames
131
+ Then: The frames are tagged correctly and constants are populated correctly
132
+ """
133
+
134
+ task = input_parse_task
135
+
136
+ pol_status, pol_theta, ret_status, ret_theta, dark_status = small_calibration_sequence
137
+ num_modstates = 3
138
+ num_ops = len(op_id_list)
139
+ start_time = Time("2026-02-01T15:00:12")
140
+
141
+ for i, (op_id, ex_id, prop_id, hls_v) in enumerate(
142
+ zip(op_id_list, experiment_id_list, proposal_id_list, hls_list)
143
+ ):
144
+ polcal_op_start_time = (start_time + i * TimeDelta(1 * u.day)).fits
145
+ for arm_id in arm_id_list:
146
+ write_calibration_sequence_frames_to_task(
147
+ task=task,
148
+ instrument=instrument,
149
+ arm_id=arm_id,
150
+ wavelength=WAVELENGTH_FOR_ARM[arm_id],
151
+ num_modstates=num_modstates,
152
+ pol_status=pol_status,
153
+ pol_theta=pol_theta,
154
+ ret_status=ret_status,
155
+ ret_theta=ret_theta,
156
+ dark_status=dark_status,
157
+ array_shape=(2, 2),
158
+ input_tags=[TrendTag.input()],
159
+ op_start_time=polcal_op_start_time,
160
+ op_id=op_id,
161
+ experiment_id=ex_id,
162
+ proposal_id=prop_id,
163
+ hls_version=hls_v,
164
+ visp_grating_angle=GRATING_ANGLE_FOR_ARM[arm_id],
165
+ visp_arm_position=ARM_ANGLE_FOR_ARM[arm_id],
166
+ visp_grating_constant=GRATING_CONSTANT_FOR_ARM[arm_id],
167
+ cryonirsp_od_filter_name=CRYO_OD_FILTER_FOR_ARM[arm_id],
168
+ cryonirsp_grating_angle=GRATING_ANGLE_FOR_ARM[arm_id],
169
+ cryonirsp_grating_littrow_angle=ARM_ANGLE_FOR_ARM[arm_id],
170
+ cryonirsp_grating_constant=GRATING_CONSTANT_FOR_ARM[arm_id],
171
+ cryonirsp_slit_width=CRYO_SLIT_WIDTH_FOR_ARM[arm_id],
172
+ dlnirsp_grating_angle=GRATING_ANGLE_FOR_ARM[arm_id],
173
+ dlnirsp_arm_position=ARM_ANGLE_FOR_ARM[arm_id],
174
+ dlnirsp_grating_constant=GRATING_CONSTANT_FOR_ARM[arm_id],
175
+ )
176
+
177
+ task()
178
+
179
+ # Tags applied correctly
180
+ for arm_id in arm_id_list:
181
+ for step in range(small_num_cs_steps * num_ops):
182
+ for modstate in range(1, num_modstates + 1):
183
+ assert (
184
+ task.count(
185
+ tags=[
186
+ TrendTag.task_polcal(),
187
+ TrendTag.arm_id(arm_id),
188
+ TrendTag.cs_step(step),
189
+ TrendTag.modstate(modstate),
190
+ ]
191
+ )
192
+ == 1
193
+ )
194
+
195
+ for arm_id in arm_id_list:
196
+ for modstate in range(1, num_modstates + 1):
197
+ assert (
198
+ task.count(
199
+ tags=[
200
+ TrendTag.arm_id(arm_id),
201
+ TrendTag.modstate(modstate),
202
+ TrendTag.task_polcal_gain(),
203
+ ]
204
+ )
205
+ == small_num_gain_steps * num_ops
206
+ )
207
+ assert (
208
+ task.count(
209
+ tags=[
210
+ TrendTag.arm_id(arm_id),
211
+ TrendTag.modstate(modstate),
212
+ TrendTag.task_polcal_dark(),
213
+ ]
214
+ )
215
+ == small_num_dark_steps * num_ops
216
+ )
217
+
218
+ # Constants parsed correctly
219
+ # Wrap both side in `Time(...).fits` to ensure the same formatting of decimal places in string
220
+ assert (
221
+ Time(task.constants._db_dict[TrendBudName.earliest_ip_start_time]).fits == start_time.fits
222
+ )
223
+ assert task.constants._db_dict[TrendBudName.wavelength_for_arm] == {
224
+ json.dumps(arm): WAVELENGTH_FOR_ARM[arm] for arm in arm_id_list
225
+ }
226
+ assert task.constants._db_dict[BudName.instrument] == instrument
227
+ assert task.constants._db_dict[BudName.num_cs_steps] == small_num_cs_steps * num_ops
228
+ assert task.constants._db_dict[BudName.num_modstates] == num_modstates
229
+ assert sorted(task.constants._db_dict[TrendBudName.arm_id_list]) == sorted(arm_id_list)
230
+ assert (
231
+ task.constants._db_dict[TrendBudName.observing_program_id_list] == expected_op_id_constant
232
+ )
233
+ assert (
234
+ task.constants._db_dict[TrendBudName.experiment_id_list] == expected_experiment_id_constant
235
+ )
236
+ assert task.constants._db_dict[TrendBudName.proposal_id_list] == expected_proposal_id_constant
237
+ assert task.constants._db_dict[TrendBudName.hls_version_list] == expected_hls_version_constant
238
+
239
+ if instrument == InstrumentName.visp:
240
+ assert task.constants._db_dict[TrendBudName.visp_grating_angle_for_arm] == {
241
+ json.dumps(arm): GRATING_ANGLE_FOR_ARM[arm] for arm in arm_id_list
242
+ }
243
+ assert task.constants._db_dict[TrendBudName.visp_arm_position_for_arm] == {
244
+ json.dumps(arm): ARM_ANGLE_FOR_ARM[arm] for arm in arm_id_list
245
+ }
246
+ assert task.constants._db_dict[TrendBudName.visp_grating_constant_for_arm] == {
247
+ json.dumps(arm): GRATING_CONSTANT_FOR_ARM[arm] for arm in arm_id_list
248
+ }
249
+
250
+ if instrument == InstrumentName.cryonirsp:
251
+ assert task.constants._db_dict[TrendBudName.cryonirsp_od_filter_name_for_arm] == {
252
+ json.dumps(arm): CRYO_OD_FILTER_FOR_ARM[arm] for arm in arm_id_list
253
+ }
254
+ assert task.constants._db_dict[TrendBudName.cryonirsp_grating_angle_for_arm] == {
255
+ json.dumps(arm): GRATING_ANGLE_FOR_ARM[arm] for arm in arm_id_list
256
+ }
257
+ assert task.constants._db_dict[TrendBudName.cryonirsp_grating_littrow_angle_for_arm] == {
258
+ json.dumps(arm): ARM_ANGLE_FOR_ARM[arm] for arm in arm_id_list
259
+ }
260
+ assert task.constants._db_dict[TrendBudName.cryonirsp_grating_constant_for_arm] == {
261
+ json.dumps(arm): GRATING_CONSTANT_FOR_ARM[arm] for arm in arm_id_list
262
+ }
263
+ assert task.constants._db_dict[TrendBudName.cryonirsp_slit_width_for_arm] == {
264
+ json.dumps(arm): CRYO_SLIT_WIDTH_FOR_ARM[arm] for arm in arm_id_list
265
+ }
266
+
267
+ if instrument == InstrumentName.dlnirsp:
268
+ assert task.constants._db_dict[TrendBudName.dlnirsp_grating_angle_for_arm] == {
269
+ json.dumps(arm): GRATING_ANGLE_FOR_ARM[arm] for arm in arm_id_list
270
+ }
271
+ assert task.constants._db_dict[TrendBudName.dlnirsp_arm_position_for_arm] == {
272
+ json.dumps(arm): ARM_ANGLE_FOR_ARM[arm] for arm in arm_id_list
273
+ }
274
+ assert task.constants._db_dict[TrendBudName.dlnirsp_grating_constant_for_arm] == {
275
+ json.dumps(arm): GRATING_CONSTANT_FOR_ARM[arm] for arm in arm_id_list
276
+ }
@@ -0,0 +1,233 @@
1
+ import numpy as np
2
+ import pytest
3
+ from dkist_processing_common._util.scratch import WorkflowFileSystem
4
+ from dkist_processing_common.codecs.asdf import asdf_decoder
5
+ from dkist_processing_common.codecs.fits import fits_array_encoder
6
+
7
+ from dkist_processing_trend.models.fits_access import CryonirspMetadataKey
8
+ from dkist_processing_trend.models.instrument import InstrumentName
9
+ from dkist_processing_trend.models.instrument_options import CryonirspInstrumentOptions
10
+ from dkist_processing_trend.models.parameters import TrendParameters
11
+ from dkist_processing_trend.models.tags import TrendTag
12
+ from dkist_processing_trend.tasks.prepare_fit_data_base import PrepareFitDataBase
13
+ from dkist_processing_trend.tests.conftest import ModulatedCSStepHeaders
14
+ from dkist_processing_trend.tests.conftest import TrendInputDatasetParameterValues
15
+ from dkist_processing_trend.tests.conftest import TrendTestingConstantsDatabase
16
+ from dkist_processing_trend.tests.conftest import write_calibration_sequence_frames_to_task
17
+
18
+
19
+ class InstrumentPrepData(PrepareFitDataBase):
20
+
21
+ def extract_beam(self, array: np.ndarray, beam: int) -> np.ndarray:
22
+ return array[beam - 1 :: 2, :]
23
+
24
+ def apply_global_instrument_options(
25
+ self, array: np.ndarray, instrument_options: CryonirspInstrumentOptions
26
+ ) -> np.ndarray:
27
+ return np.mean(array)[None, None]
28
+
29
+ def apply_local_instrument_options(
30
+ self, array: np.ndarray, instrument_options: CryonirspInstrumentOptions
31
+ ) -> np.ndarray:
32
+ return np.mean(array, axis=instrument_options.num_spatial_px)
33
+
34
+
35
+ @pytest.fixture
36
+ def instrument_options_list() -> dict[str, str | int]:
37
+ # pydantic forces the structure of these dicts. See `apply_local_instrument_options` above for how these parameters
38
+ # are actually used.
39
+ return [
40
+ {"name": "axis0_option", "num_spatial_px": 0, "num_spectral_px": 1},
41
+ {"name": "axis1_option", "num_spatial_px": 1, "num_spectral_px": 1},
42
+ ]
43
+
44
+
45
+ @pytest.fixture
46
+ def num_modstates() -> int:
47
+ return 4
48
+
49
+
50
+ @pytest.fixture
51
+ def dark_signal() -> float:
52
+ return 6.28
53
+
54
+
55
+ @pytest.fixture
56
+ def gain_signal() -> float:
57
+ return 3.14
58
+
59
+
60
+ @pytest.fixture
61
+ def make_data_function(dark_signal, gain_signal):
62
+ def make_data(frame: ModulatedCSStepHeaders) -> np.ndarray:
63
+ shape = frame.array_shape[1:]
64
+ cs_step = frame.cs_step_num
65
+ modstate = frame.current_modstate
66
+
67
+ true_signal = cs_step * 1000 + modstate * 100
68
+ raw_signal = true_signal * gain_signal + dark_signal
69
+ return np.full(shape, raw_signal)
70
+
71
+ return make_data
72
+
73
+
74
+ @pytest.fixture
75
+ def instrument_fit_data_task(
76
+ recipe_run_id,
77
+ tmp_path,
78
+ link_constants_db,
79
+ assign_input_dataset_doc_to_task,
80
+ arm_id,
81
+ instrument_options_list,
82
+ small_num_cs_steps,
83
+ num_modstates,
84
+ ):
85
+ link_constants_db(
86
+ recipe_run_id,
87
+ TrendTestingConstantsDatabase(
88
+ INSTRUMENT=InstrumentName.cryonirsp,
89
+ ARM_ID_LIST=["CI", "SP"],
90
+ NUM_CS_STEPS=small_num_cs_steps,
91
+ NUM_MODSTATES=num_modstates,
92
+ ),
93
+ )
94
+ with InstrumentPrepData(
95
+ arm_id=arm_id, recipe_run_id=recipe_run_id, workflow_name="name", workflow_version="version"
96
+ ) as task:
97
+ task.scratch = WorkflowFileSystem(recipe_run_id=recipe_run_id, scratch_base_path=tmp_path)
98
+ assign_input_dataset_doc_to_task(
99
+ task,
100
+ TrendInputDatasetParameterValues(
101
+ trend_instrument_processing_options_cryonirsp=instrument_options_list
102
+ ),
103
+ )
104
+ task.parameters = TrendParameters(
105
+ scratch=task.scratch,
106
+ obs_ip_start_time=task.constants.earliest_ip_start_time,
107
+ instrument=task.constants.instrument,
108
+ )
109
+
110
+ yield task
111
+ task._purge()
112
+
113
+
114
+ @pytest.mark.parametrize("arm_id", [pytest.param("SP"), pytest.param("CI")])
115
+ def test_prepare_data_base(
116
+ instrument_fit_data_task,
117
+ instrument_options_list,
118
+ small_calibration_sequence,
119
+ small_num_cs_steps,
120
+ num_modstates,
121
+ arm_id,
122
+ make_data_function,
123
+ dark_signal,
124
+ gain_signal,
125
+ mocker,
126
+ fake_gql_client,
127
+ ):
128
+ """
129
+ Given: A dummy child class of `PrepareFitDataBase` and a correct set of INPUT and INTERMEDIATE data.
130
+ When: Running the task
131
+ Then: The expected outputs are produced and they have the expected structure and data values
132
+ """
133
+ mocker.patch(
134
+ "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=fake_gql_client
135
+ )
136
+ num_beams = 1 if arm_id == "CI" else 2
137
+ shape = (8, 9)
138
+ task = instrument_fit_data_task
139
+
140
+ pol_status, pol_theta, ret_status, ret_theta, dark_status = small_calibration_sequence
141
+
142
+ write_calibration_sequence_frames_to_task(
143
+ task=task,
144
+ instrument=InstrumentName.cryonirsp,
145
+ arm_id=arm_id,
146
+ wavelength=1074.7,
147
+ num_modstates=num_modstates,
148
+ pol_status=pol_status,
149
+ pol_theta=pol_theta,
150
+ ret_status=ret_status,
151
+ ret_theta=ret_theta,
152
+ dark_status=dark_status,
153
+ array_shape=shape,
154
+ tags=[TrendTag.arm_id(arm_id)],
155
+ data_func=make_data_function,
156
+ )
157
+
158
+ # INTERMEIDATE dark and gain frames
159
+ task.write(
160
+ data=np.full(shape, dark_signal),
161
+ tags=[
162
+ TrendTag.intermediate(),
163
+ TrendTag.frame(),
164
+ TrendTag.arm_id(arm_id),
165
+ TrendTag.task_dark(),
166
+ ],
167
+ encoder=fits_array_encoder,
168
+ )
169
+ task.write(
170
+ data=np.full(shape, gain_signal),
171
+ tags=[
172
+ TrendTag.intermediate(),
173
+ TrendTag.frame(),
174
+ TrendTag.arm_id(arm_id),
175
+ TrendTag.task_gain(),
176
+ ],
177
+ encoder=fits_array_encoder,
178
+ )
179
+
180
+ task()
181
+
182
+ for inst_opt in instrument_options_list:
183
+
184
+ # See `InstrumentPrepData.apply_local_instrument_options` above for why these values are what they are
185
+ if inst_opt["num_spatial_px"] == 0:
186
+ expected_local_shape = (9,)
187
+ elif inst_opt["num_spatial_px"] == 1:
188
+ expected_local_shape = (4,)
189
+
190
+ for beam in range(1, num_beams + 1):
191
+ global_tags = [
192
+ TrendTag.intermediate(),
193
+ TrendTag.arm_id(arm_id),
194
+ TrendTag.beam(beam),
195
+ TrendTag.instrument_processing_options(inst_opt["name"]),
196
+ TrendTag.task_global_pac_input(),
197
+ ]
198
+ local_tags = [
199
+ TrendTag.intermediate(),
200
+ TrendTag.arm_id(arm_id),
201
+ TrendTag.beam(beam),
202
+ TrendTag.instrument_processing_options(inst_opt["name"]),
203
+ TrendTag.task_local_pac_input(),
204
+ ]
205
+ assert task.count(global_tags) == 1
206
+ assert task.count(local_tags) == 1
207
+
208
+ global_dict = next(task.read(tags=global_tags, decoder=asdf_decoder))
209
+ local_dict = next(task.read(tags=local_tags, decoder=asdf_decoder))
210
+ for cs_step in range(small_num_cs_steps):
211
+ global_modstate_list = global_dict[cs_step]
212
+ local_modstate_list = local_dict[cs_step]
213
+ assert isinstance(global_modstate_list, list)
214
+ assert isinstance(local_modstate_list, list)
215
+ assert len(global_modstate_list) == len(local_modstate_list) == num_modstates
216
+ for m in range(num_modstates):
217
+ global_mod_dict = global_modstate_list[m]
218
+ global_header = global_mod_dict["header"]
219
+ assert global_header[CryonirspMetadataKey.modulator_state] == m + 1
220
+ global_data = global_mod_dict["data"]
221
+ assert global_data.shape == (1, 1)
222
+ np.testing.assert_array_equal(global_data, 1000 * cs_step + 100 * (m + 1))
223
+
224
+ local_mod_dict = local_modstate_list[m]
225
+ local_header = local_mod_dict["header"]
226
+ assert local_header[CryonirspMetadataKey.modulator_state] == m + 1
227
+ local_data = local_mod_dict["data"]
228
+ assert local_data.shape == expected_local_shape
229
+ np.testing.assert_array_equal(local_data, 1000 * cs_step + 100 * (m + 1))
230
+
231
+ # These asserts are checking that there aren't any extra CS steps beyond what we expect
232
+ assert small_num_cs_steps not in global_dict.keys()
233
+ assert small_num_cs_steps not in local_dict.keys()
@@ -0,0 +1,45 @@
1
+ from pathlib import Path
2
+
3
+ import pytest
4
+
5
+ from dkist_processing_trend.tasks.trend_output_data import PublishTrendCatalogMessages
6
+ from dkist_processing_trend.tests.conftest import TrendTestingConstantsDatabase
7
+
8
+
9
+ @pytest.fixture
10
+ def publish_catalog_and_quality_messages_task(
11
+ recipe_run_id, link_constants_db, mocker, fake_gql_client
12
+ ):
13
+ instrument = "DOPE"
14
+ full_date = "2024-01-02T12:34:56.2341234234"
15
+ dir_date = "20240102T123456"
16
+ link_constants_db(
17
+ recipe_run_id,
18
+ TrendTestingConstantsDatabase(INSTRUMENT=instrument, EARLIEST_IP_START_TIME=full_date),
19
+ )
20
+ mocker.patch(
21
+ "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=fake_gql_client
22
+ )
23
+ with PublishTrendCatalogMessages(
24
+ recipe_run_id=recipe_run_id,
25
+ workflow_name="workflow_name",
26
+ workflow_version="workflow_version",
27
+ ) as task:
28
+ yield task, instrument, dir_date
29
+ task.constants._purge()
30
+
31
+
32
+ def test_frame_messages(publish_catalog_and_quality_messages_task):
33
+ """
34
+ Given: a PublishCatalogAndQualityMessages task
35
+ When: creating frame messages
36
+ Then: the attributes are correctly populated
37
+ """
38
+ task, instrument, dir_date = publish_catalog_and_quality_messages_task
39
+ filenames = [f"test_frame_{i}.ext" for i in range(10)]
40
+ filepaths = [Path(f"a/b/c/{filename}") for filename in filenames]
41
+ frame_messages = task.frame_messages(paths=filepaths)
42
+ for message in frame_messages:
43
+ assert message.body.bucket == "data"
44
+ assert str(Path(message.body.objectName).parent) == f"trend/polcal/{dir_date}/{instrument}"
45
+ assert message.body.conversationId == str(task.recipe_run_id)