dkist-processing-common 11.7.1rc1__py3-none-any.whl → 11.8.0rc1__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 (29) hide show
  1. changelog/245.feature.1.rst +1 -0
  2. dkist_processing_common/models/constants.py +333 -21
  3. dkist_processing_common/models/fits_access.py +16 -25
  4. dkist_processing_common/parsers/average_bud.py +48 -0
  5. dkist_processing_common/parsers/experiment_id_bud.py +8 -4
  6. dkist_processing_common/parsers/id_bud.py +35 -19
  7. dkist_processing_common/parsers/l0_fits_access.py +3 -3
  8. dkist_processing_common/parsers/l1_fits_access.py +47 -21
  9. dkist_processing_common/parsers/near_bud.py +4 -4
  10. dkist_processing_common/parsers/observing_program_id_bud.py +24 -0
  11. dkist_processing_common/parsers/proposal_id_bud.py +11 -5
  12. dkist_processing_common/parsers/single_value_single_key_flower.py +0 -1
  13. dkist_processing_common/parsers/time.py +147 -27
  14. dkist_processing_common/tasks/mixin/quality/_metrics.py +6 -4
  15. dkist_processing_common/tasks/parse_l0_input_data.py +246 -1
  16. dkist_processing_common/tasks/trial_catalog.py +38 -0
  17. dkist_processing_common/tests/mock_metadata_store.py +39 -0
  18. dkist_processing_common/tests/test_fits_access.py +19 -44
  19. dkist_processing_common/tests/test_input_dataset.py +1 -37
  20. dkist_processing_common/tests/test_parse_l0_input_data.py +45 -5
  21. dkist_processing_common/tests/test_quality_mixin.py +3 -11
  22. dkist_processing_common/tests/test_stems.py +162 -10
  23. dkist_processing_common/tests/test_task_parsing.py +6 -6
  24. dkist_processing_common/tests/test_trial_catalog.py +72 -2
  25. {dkist_processing_common-11.7.1rc1.dist-info → dkist_processing_common-11.8.0rc1.dist-info}/METADATA +5 -5
  26. {dkist_processing_common-11.7.1rc1.dist-info → dkist_processing_common-11.8.0rc1.dist-info}/RECORD +28 -26
  27. changelog/271.misc.rst +0 -1
  28. {dkist_processing_common-11.7.1rc1.dist-info → dkist_processing_common-11.8.0rc1.dist-info}/WHEEL +0 -0
  29. {dkist_processing_common-11.7.1rc1.dist-info → dkist_processing_common-11.8.0rc1.dist-info}/top_level.txt +0 -0
@@ -34,16 +34,23 @@ from dkist_processing_common.models.flower_pot import Stem
34
34
  from dkist_processing_common.models.flower_pot import Thorn
35
35
  from dkist_processing_common.models.tags import Tag
36
36
  from dkist_processing_common.models.task_name import TaskName
37
+ from dkist_processing_common.parsers.average_bud import TaskAverageBud
37
38
  from dkist_processing_common.parsers.experiment_id_bud import ContributingExperimentIdsBud
38
39
  from dkist_processing_common.parsers.experiment_id_bud import ExperimentIdBud
40
+ from dkist_processing_common.parsers.observing_program_id_bud import (
41
+ TaskContributingObservingProgramExecutionIdsBud,
42
+ )
39
43
  from dkist_processing_common.parsers.proposal_id_bud import ContributingProposalIdsBud
40
44
  from dkist_processing_common.parsers.proposal_id_bud import ProposalIdBud
45
+ from dkist_processing_common.parsers.task import parse_header_ip_task_with_gains
41
46
  from dkist_processing_common.parsers.time import AverageCadenceBud
42
47
  from dkist_processing_common.parsers.time import MaximumCadenceBud
43
48
  from dkist_processing_common.parsers.time import MinimumCadenceBud
49
+ from dkist_processing_common.parsers.time import TaskDateBeginBud
44
50
  from dkist_processing_common.parsers.time import TaskExposureTimesBud
45
51
  from dkist_processing_common.parsers.time import TaskReadoutExpTimesBud
46
52
  from dkist_processing_common.parsers.time import VarianceCadenceBud
53
+ from dkist_processing_common.parsers.unique_bud import TaskUniqueBud
47
54
  from dkist_processing_common.parsers.unique_bud import UniqueBud
48
55
  from dkist_processing_common.tasks.base import WorkflowTaskBase
49
56
 
@@ -59,9 +66,247 @@ logger = logging.getLogger(__name__)
59
66
  S = TypeVar("S", bound=Stem)
60
67
 
61
68
 
69
+ def dataset_extra_bud_factory() -> list[S]:
70
+ """Provide constant buds for use in dataset extras."""
71
+ return [
72
+ UniqueBud(constant_name=BudName.camera_id, metadata_key=MetadataKey.camera_id),
73
+ UniqueBud(constant_name=BudName.camera_name, metadata_key=MetadataKey.camera_name),
74
+ UniqueBud(
75
+ constant_name=BudName.camera_bit_depth, metadata_key=MetadataKey.camera_bit_depth
76
+ ),
77
+ UniqueBud(
78
+ constant_name=BudName.hardware_binning_x, metadata_key=MetadataKey.hardware_binning_x
79
+ ),
80
+ UniqueBud(
81
+ constant_name=BudName.hardware_binning_y, metadata_key=MetadataKey.hardware_binning_x
82
+ ),
83
+ UniqueBud(
84
+ constant_name=BudName.software_binning_x, metadata_key=MetadataKey.software_binning_x
85
+ ),
86
+ UniqueBud(
87
+ constant_name=BudName.software_binning_y, metadata_key=MetadataKey.software_binning_y
88
+ ),
89
+ UniqueBud(
90
+ constant_name=BudName.hls_version,
91
+ metadata_key=MetadataKey.hls_version,
92
+ ),
93
+ TaskContributingObservingProgramExecutionIdsBud(
94
+ constant_name=BudName.dark_observing_program_execution_ids,
95
+ ip_task_types=TaskName.dark,
96
+ ),
97
+ TaskContributingObservingProgramExecutionIdsBud(
98
+ constant_name=BudName.solar_gain_observing_program_execution_ids,
99
+ ip_task_types=TaskName.solar_gain,
100
+ task_type_parsing_function=parse_header_ip_task_with_gains,
101
+ ),
102
+ TaskContributingObservingProgramExecutionIdsBud(
103
+ constant_name=BudName.polcal_observing_program_execution_ids,
104
+ ip_task_types=TaskName.polcal,
105
+ ),
106
+ TaskUniqueBud(
107
+ constant_name=BudName.solar_gain_num_raw_frames_per_fpa,
108
+ metadata_key=MetadataKey.num_raw_frames_per_fpa,
109
+ ip_task_types=TaskName.solar_gain,
110
+ task_type_parsing_function=parse_header_ip_task_with_gains,
111
+ ),
112
+ TaskUniqueBud(
113
+ constant_name=BudName.polcal_num_raw_frames_per_fpa,
114
+ metadata_key=MetadataKey.num_raw_frames_per_fpa,
115
+ ip_task_types=TaskName.polcal,
116
+ ),
117
+ TaskUniqueBud(
118
+ constant_name=BudName.solar_gain_telescope_tracking_mode,
119
+ metadata_key=MetadataKey.telescope_tracking_mode,
120
+ ip_task_types=TaskName.solar_gain,
121
+ task_type_parsing_function=parse_header_ip_task_with_gains,
122
+ ),
123
+ TaskUniqueBud(
124
+ constant_name=BudName.polcal_telescope_tracking_mode,
125
+ metadata_key=MetadataKey.telescope_tracking_mode,
126
+ ip_task_types=TaskName.polcal,
127
+ ),
128
+ TaskUniqueBud(
129
+ constant_name=BudName.solar_gain_coude_table_tracking_mode,
130
+ metadata_key=MetadataKey.coude_table_tracking_mode,
131
+ ip_task_types=TaskName.solar_gain,
132
+ task_type_parsing_function=parse_header_ip_task_with_gains,
133
+ ),
134
+ TaskUniqueBud(
135
+ constant_name=BudName.polcal_coude_table_tracking_mode,
136
+ metadata_key=MetadataKey.coude_table_tracking_mode,
137
+ ip_task_types=TaskName.polcal,
138
+ ),
139
+ TaskUniqueBud(
140
+ constant_name=BudName.solar_gain_telescope_scanning_mode,
141
+ metadata_key=MetadataKey.telescope_scanning_mode,
142
+ ip_task_types=TaskName.solar_gain,
143
+ task_type_parsing_function=parse_header_ip_task_with_gains,
144
+ ),
145
+ TaskUniqueBud(
146
+ constant_name=BudName.polcal_telescope_scanning_mode,
147
+ metadata_key=MetadataKey.telescope_scanning_mode,
148
+ ip_task_types=TaskName.polcal,
149
+ ),
150
+ TaskUniqueBud(
151
+ constant_name=BudName.dark_gos_level3_status,
152
+ metadata_key=MetadataKey.gos_level3_status,
153
+ ip_task_types=TaskName.dark,
154
+ ),
155
+ TaskUniqueBud(
156
+ constant_name=BudName.solar_gain_gos_level3_status,
157
+ metadata_key=MetadataKey.gos_level3_status,
158
+ ip_task_types=TaskName.solar_gain,
159
+ task_type_parsing_function=parse_header_ip_task_with_gains,
160
+ ),
161
+ TaskUniqueBud(
162
+ constant_name=BudName.dark_gos_level3_lamp_status,
163
+ metadata_key=MetadataKey.gos_level3_lamp_status,
164
+ ip_task_types=TaskName.dark,
165
+ ),
166
+ TaskUniqueBud(
167
+ constant_name=BudName.solar_gain_gos_level3_lamp_status,
168
+ metadata_key=MetadataKey.gos_level3_lamp_status,
169
+ ip_task_types=TaskName.solar_gain,
170
+ task_type_parsing_function=parse_header_ip_task_with_gains,
171
+ ),
172
+ TaskUniqueBud(
173
+ constant_name=BudName.dark_gos_polarizer_status,
174
+ metadata_key=MetadataKey.gos_polarizer_status,
175
+ ip_task_types=TaskName.dark,
176
+ ),
177
+ TaskUniqueBud(
178
+ constant_name=BudName.solar_gain_gos_polarizer_status,
179
+ metadata_key=MetadataKey.gos_polarizer_status,
180
+ ip_task_types=TaskName.solar_gain,
181
+ task_type_parsing_function=parse_header_ip_task_with_gains,
182
+ ),
183
+ TaskUniqueBud(
184
+ constant_name=BudName.dark_gos_polarizer_angle,
185
+ metadata_key=MetadataKey.gos_polarizer_angle,
186
+ ip_task_types=TaskName.dark,
187
+ ),
188
+ TaskUniqueBud(
189
+ constant_name=BudName.solar_gain_gos_polarizer_angle,
190
+ metadata_key=MetadataKey.gos_polarizer_angle,
191
+ ip_task_types=TaskName.solar_gain,
192
+ task_type_parsing_function=parse_header_ip_task_with_gains,
193
+ ),
194
+ TaskUniqueBud(
195
+ constant_name=BudName.dark_gos_retarder_status,
196
+ metadata_key=MetadataKey.gos_retarder_status,
197
+ ip_task_types=TaskName.dark,
198
+ ),
199
+ TaskUniqueBud(
200
+ constant_name=BudName.solar_gain_gos_retarder_status,
201
+ metadata_key=MetadataKey.gos_retarder_status,
202
+ ip_task_types=TaskName.solar_gain,
203
+ task_type_parsing_function=parse_header_ip_task_with_gains,
204
+ ),
205
+ TaskUniqueBud(
206
+ constant_name=BudName.dark_gos_retarder_angle,
207
+ metadata_key=MetadataKey.gos_retarder_angle,
208
+ ip_task_types=TaskName.dark,
209
+ ),
210
+ TaskUniqueBud(
211
+ constant_name=BudName.solar_gain_gos_retarder_angle,
212
+ metadata_key=MetadataKey.gos_retarder_angle,
213
+ ip_task_types=TaskName.solar_gain,
214
+ task_type_parsing_function=parse_header_ip_task_with_gains,
215
+ ),
216
+ TaskUniqueBud(
217
+ constant_name=BudName.dark_gos_level0_status,
218
+ metadata_key=MetadataKey.gos_level0_status,
219
+ ip_task_types=TaskName.dark,
220
+ ),
221
+ TaskUniqueBud(
222
+ constant_name=BudName.solar_gain_gos_level0_status,
223
+ metadata_key=MetadataKey.gos_level0_status,
224
+ ip_task_types=TaskName.solar_gain,
225
+ task_type_parsing_function=parse_header_ip_task_with_gains,
226
+ ),
227
+ TaskAverageBud(
228
+ constant_name=BudName.dark_average_light_level,
229
+ metadata_key=MetadataKey.light_level,
230
+ ip_task_types=TaskName.dark,
231
+ ),
232
+ TaskAverageBud(
233
+ constant_name=BudName.solar_gain_average_light_level,
234
+ metadata_key=MetadataKey.light_level,
235
+ ip_task_types=TaskName.solar_gain,
236
+ task_type_parsing_function=parse_header_ip_task_with_gains,
237
+ ),
238
+ TaskAverageBud(
239
+ constant_name=BudName.polcal_average_light_level,
240
+ metadata_key=MetadataKey.light_level,
241
+ ip_task_types=TaskName.polcal,
242
+ ),
243
+ TaskAverageBud(
244
+ constant_name=BudName.dark_average_telescope_elevation,
245
+ metadata_key=MetadataKey.elevation,
246
+ ip_task_types=TaskName.dark,
247
+ ),
248
+ TaskAverageBud(
249
+ constant_name=BudName.solar_gain_average_telescope_elevation,
250
+ metadata_key=MetadataKey.elevation,
251
+ ip_task_types=TaskName.solar_gain,
252
+ task_type_parsing_function=parse_header_ip_task_with_gains,
253
+ ),
254
+ TaskAverageBud(
255
+ constant_name=BudName.polcal_average_telescope_elevation,
256
+ metadata_key=MetadataKey.elevation,
257
+ ip_task_types=TaskName.polcal,
258
+ ),
259
+ TaskAverageBud(
260
+ constant_name=BudName.dark_average_coude_table_angle,
261
+ metadata_key=MetadataKey.table_angle,
262
+ ip_task_types=TaskName.dark,
263
+ ),
264
+ TaskAverageBud(
265
+ constant_name=BudName.solar_gain_average_coude_table_angle,
266
+ metadata_key=MetadataKey.table_angle,
267
+ ip_task_types=TaskName.solar_gain,
268
+ task_type_parsing_function=parse_header_ip_task_with_gains,
269
+ ),
270
+ TaskAverageBud(
271
+ constant_name=BudName.polcal_average_coude_table_angle,
272
+ metadata_key=MetadataKey.table_angle,
273
+ ip_task_types=TaskName.polcal,
274
+ ),
275
+ TaskAverageBud(
276
+ constant_name=BudName.dark_average_telescope_azimuth,
277
+ metadata_key=MetadataKey.azimuth,
278
+ ip_task_types=TaskName.dark,
279
+ ),
280
+ TaskAverageBud(
281
+ constant_name=BudName.solar_gain_average_telescope_azimuth,
282
+ metadata_key=MetadataKey.azimuth,
283
+ ip_task_types=TaskName.solar_gain,
284
+ task_type_parsing_function=parse_header_ip_task_with_gains,
285
+ ),
286
+ TaskAverageBud(
287
+ constant_name=BudName.polcal_average_telescope_azimuth,
288
+ metadata_key=MetadataKey.azimuth,
289
+ ip_task_types=TaskName.polcal,
290
+ ),
291
+ TaskDateBeginBud(
292
+ constant_name=BudName.dark_date_begin,
293
+ ip_task_types=TaskName.dark,
294
+ ),
295
+ TaskDateBeginBud(
296
+ constant_name=BudName.solar_gain_date_begin,
297
+ ip_task_types=TaskName.solar_gain,
298
+ task_type_parsing_function=parse_header_ip_task_with_gains,
299
+ ),
300
+ TaskDateBeginBud(
301
+ constant_name=BudName.polcal_date_begin,
302
+ ip_task_types=TaskName.polcal,
303
+ ),
304
+ ]
305
+
306
+
62
307
  def default_constant_bud_factory() -> list[S]:
63
308
  """Provide default constant buds for use in common parsing tasks."""
64
- return [
309
+ return dataset_extra_bud_factory() + [
65
310
  UniqueBud(constant_name=BudName.instrument, metadata_key=MetadataKey.instrument),
66
311
  ProposalIdBud(),
67
312
  ContributingProposalIdsBud(),
@@ -5,15 +5,18 @@ import logging
5
5
  from datetime import datetime
6
6
  from itertools import chain
7
7
  from pathlib import Path
8
+ from typing import Any
8
9
  from typing import Generator
9
10
  from uuid import uuid4
10
11
 
11
12
  from dkist_processing_common.codecs.asdf import asdf_fileobj_encoder
13
+ from dkist_processing_common.codecs.basemodel import basemodel_decoder
12
14
  from dkist_processing_common.codecs.fits import fits_access_decoder
13
15
  from dkist_processing_common.codecs.json import json_encoder
14
16
  from dkist_processing_common.codecs.path import path_decoder
15
17
  from dkist_processing_common.codecs.quality import quality_data_decoder
16
18
  from dkist_processing_common.models.fits_access import FitsAccessBase
19
+ from dkist_processing_common.models.input_dataset import InputDatasetPartDocumentList
17
20
  from dkist_processing_common.models.tags import Tag
18
21
  from dkist_processing_common.tasks.output_data_base import OutputDataBase
19
22
 
@@ -138,11 +141,15 @@ class CreateTrialAsdf(OutputDataBase):
138
141
 
139
142
  def run(self) -> None:
140
143
  """Generate an ASDF file simulating the ASDF file that would be produced when cataloging the dataset."""
144
+ with self.telemetry_span("Collate input dataset parameters"):
145
+ parameters = self.parse_input_dataset_parameters()
146
+
141
147
  with self.telemetry_span("Generate ASDF tree"):
142
148
  tree = asdf_tree_from_filenames(
143
149
  filenames=self.absolute_output_frame_paths,
144
150
  hdu=1, # compressed
145
151
  relative_to=self.scratch.workflow_base_path,
152
+ parameters=parameters,
146
153
  )
147
154
 
148
155
  trial_history = [
@@ -169,6 +176,37 @@ class CreateTrialAsdf(OutputDataBase):
169
176
  ),
170
177
  )
171
178
 
179
+ def parse_input_dataset_parameters(self) -> list[dict[str, Any]]:
180
+ """
181
+ Return the parameters associated with the dataset.
182
+
183
+ Returns
184
+ -------
185
+ list[dict[str, Any]]
186
+ A list of dictionaries, each containing a parameter name and its values.
187
+
188
+ Raises
189
+ ------
190
+ ValueError
191
+ If there is not exactly one ``InputDatasetPartDocumentList`` found.
192
+ """
193
+ part_docs_iter = self.read(
194
+ tags=Tag.input_dataset_parameters(),
195
+ decoder=basemodel_decoder,
196
+ model=InputDatasetPartDocumentList,
197
+ )
198
+ docs = list(part_docs_iter)
199
+
200
+ if not docs:
201
+ logger.warning("No parameter list decoded from files")
202
+ return []
203
+
204
+ if len(docs) > 1:
205
+ raise ValueError(f"Expected 1 parameter list, found {len(docs)}")
206
+
207
+ parameters = docs[0].model_dump(by_alias=True).get("doc_list", [])
208
+ return parameters
209
+
172
210
 
173
211
  class CreateTrialQualityReport(OutputDataBase):
174
212
  """
@@ -6,6 +6,7 @@ import json
6
6
  from abc import ABC
7
7
  from abc import abstractmethod
8
8
  from datetime import datetime
9
+ from datetime import timedelta
9
10
  from pathlib import Path
10
11
  from uuid import uuid4
11
12
 
@@ -234,3 +235,41 @@ def fake_gql_client():
234
235
  Convenience fixture for default mock GQL client. To customize, use fake_gql_client_factory.
235
236
  """
236
237
  return fake_gql_client_factory()
238
+
239
+
240
+ def input_dataset_parameters_part_factory(
241
+ parameter_count: int = 1,
242
+ parameter_value_count: int = 1,
243
+ has_date: bool = False,
244
+ has_file: bool = False,
245
+ ) -> list[dict]:
246
+ """Create a mock InputDatasetPartDocumentList with parameters."""
247
+ result = [
248
+ {
249
+ "parameterName": uuid4().hex[:6],
250
+ "parameterValues": [
251
+ {"parameterValueId": i, "parameterValue": json.dumps(uuid4().hex)}
252
+ for i in range(parameter_value_count)
253
+ ],
254
+ }
255
+ for _ in range(parameter_count)
256
+ ]
257
+ if has_date:
258
+ base = datetime(2018, 9, 14, 0, 0, 0) # This date is before any possible start dates
259
+ for parameter_index, data in enumerate(result):
260
+ for item in data["parameterValues"]:
261
+ dt = base + timedelta(days=parameter_index)
262
+ item["parameterValueStartDate"] = dt.isoformat()
263
+ if has_file:
264
+ for data in result:
265
+ param_list = data["parameterValues"]
266
+ for item in param_list:
267
+ item["parameterValue"] = json.dumps(
268
+ {
269
+ "__file__": {
270
+ "bucket": "data",
271
+ "objectKey": f"parameters/{data['parameterName']}/{uuid4().hex}.dat",
272
+ }
273
+ }
274
+ )
275
+ return result
@@ -4,7 +4,6 @@ import numpy as np
4
4
  import pytest
5
5
  from astropy.io import fits
6
6
 
7
- from dkist_processing_common.models.fits_access import NOT_FOUND_MESSAGE
8
7
  from dkist_processing_common.models.fits_access import FitsAccessBase
9
8
  from dkist_processing_common.models.fits_access import MetadataKey
10
9
  from dkist_processing_common.parsers.l0_fits_access import L0FitsAccess
@@ -99,30 +98,16 @@ def hdu_with_no_data(complete_common_header):
99
98
  @pytest.fixture()
100
99
  def hdu_with_incomplete_common_header(complete_common_header):
101
100
  """
102
- An HDU with data and a header missing one of the expected common by-frame keywords
101
+ An HDU with data and a header missing two of the expected common by-frame keywords
103
102
  """
104
103
  incomplete_header = complete_common_header
105
- incomplete_header.pop("ELEV_ANG")
106
- incomplete_header.pop("TAZIMUTH")
104
+ incomplete_header.pop(MetadataKey.elevation)
105
+ incomplete_header.pop(MetadataKey.azimuth)
107
106
  data = np.arange(9).reshape(3, 3)
108
107
  hdu = fits.PrimaryHDU(data, header=incomplete_header)
109
108
  return hdu
110
109
 
111
110
 
112
- class MetadataKeyWithOptionalKeys(StrEnum):
113
- optional1 = "ELEV_ANG"
114
- optional2 = "TAZIMUTH"
115
-
116
-
117
- class FitsAccessWithOptionalKeys(FitsAccessBase):
118
- def __init__(self, hdu, name):
119
- super().__init__(hdu, name)
120
- self._set_metadata_key_value(MetadataKeyWithOptionalKeys.optional1, optional=True)
121
- self._set_metadata_key_value(
122
- MetadataKeyWithOptionalKeys.optional2, optional=True, default="SO_RAD"
123
- )
124
-
125
-
126
111
  @pytest.fixture()
127
112
  def fits_file_path(tmp_path, complete_common_header):
128
113
  file_path = tmp_path / "foo.fits"
@@ -155,35 +140,36 @@ class MetadataKeyWithNaxisKeys(StrEnum):
155
140
  class FitsAccessWithNaxisKeys(FitsAccessBase):
156
141
  def __init__(self, hdu, name):
157
142
  super().__init__(hdu, name)
158
- self._set_metadata_key_value(MetadataKeyWithNaxisKeys.naxis)
159
- self._set_metadata_key_value(MetadataKeyWithNaxisKeys.naxis1)
160
- self._set_metadata_key_value(MetadataKeyWithNaxisKeys.naxis2)
143
+ self.naxis = self.header[MetadataKeyWithNaxisKeys.naxis]
144
+ self.naxis1 = self.header[MetadataKeyWithNaxisKeys.naxis1]
145
+ self.naxis2 = self.header[MetadataKeyWithNaxisKeys.naxis2]
161
146
 
162
147
 
163
148
  def test_metadata_keys_in_access_bases(hdu_with_no_data):
164
149
  """
165
150
  Given: a set of metadata key names in the MetadataKey sting enumeration
166
151
  When: the FITS access classes define a set of attributes/properties
167
- Then: the sets are the same
152
+ Then: the sets are the same and the values of the attributes/properties are correct
168
153
  """
169
154
  all_metadata_key_names = {mk.name for mk in MetadataKey}
170
155
  hdu = hdu_with_no_data
171
- l1_properties = {k for k, v in L1FitsAccess.__dict__.items() if isinstance(v, property)}
172
- l0_properties = {k for k, v in L0FitsAccess.__dict__.items() if isinstance(v, property)}
173
- all_fits_access_properties = l1_properties | l0_properties
174
- l1_dict_keys = {k for k, v in L1FitsAccess(hdu=hdu).__dict__.items()}
175
- l0_dict_keys = {k for k, v in L0FitsAccess(hdu=hdu).__dict__.items()}
176
- all_fits_access_dict_keys = l1_dict_keys | l0_dict_keys
177
- instance_dict_keys = {"_hdu", "name", "auto_squeeze"}
178
- keys_defined_in_fits_access = all_fits_access_dict_keys | all_fits_access_properties
179
- assert all_metadata_key_names == keys_defined_in_fits_access - instance_dict_keys
156
+ fits_obj = L0FitsAccess(hdu=hdu)
157
+ fits_access_defined_attributes = {
158
+ k for k, v in fits_obj.__dict__.items() if k not in ["_hdu", "name", "auto_squeeze"]
159
+ }
160
+ l0_access_properties = {k for k, v in L0FitsAccess.__dict__.items() if isinstance(v, property)}
161
+ l1_access_properties = {k for k, v in L1FitsAccess.__dict__.items() if isinstance(v, property)}
162
+ fits_access_properties = l0_access_properties | l1_access_properties
163
+ assert fits_access_defined_attributes | fits_access_properties == all_metadata_key_names
164
+ for key in fits_access_defined_attributes:
165
+ assert getattr(fits_obj, key) == fits_obj.header[MetadataKey[key]]
180
166
 
181
167
 
182
168
  def test_from_single_hdu(hdu_with_complete_common_header):
183
169
  """
184
170
  Given: an HDU with expected, common by-frame keywords
185
171
  When: loading the HDU with the L0FitsAccess class
186
- Then: all values for common keywords are exposed as properties on the fits_obj class
172
+ Then: values for common keywords are exposed as properties on the fits_obj class
187
173
  """
188
174
  fits_obj = L0FitsAccess(hdu_with_complete_common_header)
189
175
  assert fits_obj.elevation == 6.28
@@ -198,7 +184,7 @@ def test_l1_only_fits_access(hdu_with_complete_l1_only_header):
198
184
  """
199
185
  Given: an HDU with 214 L1-only headers
200
186
  When: loading the HDU with the L1FitsAccess class
201
- Then: no errors are raised and all values are exposed
187
+ Then: no errors are raised and values are exposed
202
188
  """
203
189
  fits_obj = L1FitsAccess(hdu_with_complete_l1_only_header)
204
190
  assert fits_obj.elevation == 6.28
@@ -255,17 +241,6 @@ def test_no_header_value(hdu_with_incomplete_common_header):
255
241
  _ = L0FitsAccess(hdu_with_incomplete_common_header)
256
242
 
257
243
 
258
- def test_default_header_values(hdu_with_incomplete_common_header):
259
- """
260
- Given: an HDU with a header with missing common by-frame keywords
261
- When: processing the HDU with a FITS access class that sets these keywords as optional
262
- Then: the correct default values are set
263
- """
264
- fits_obj = FitsAccessWithOptionalKeys(hdu_with_incomplete_common_header, name="foo")
265
- assert fits_obj.optional1 == MetadataKeyWithOptionalKeys.optional1 + NOT_FOUND_MESSAGE
266
- assert fits_obj.optional2 == "SO_RAD"
267
-
268
-
269
244
  def test_as_subclass(hdu_with_complete_common_header):
270
245
  """
271
246
  Given: an instrument-specific fits_obj class that subclasses L0FitsAccess
@@ -1,5 +1,4 @@
1
1
  import json
2
- from datetime import datetime
3
2
  from typing import Any
4
3
  from uuid import uuid4
5
4
 
@@ -8,6 +7,7 @@ import pytest
8
7
  from dkist_processing_common.codecs.basemodel import basemodel_decoder
9
8
  from dkist_processing_common.models.input_dataset import InputDatasetPartDocumentList
10
9
  from dkist_processing_common.models.tags import Tag
10
+ from dkist_processing_common.tests.mock_metadata_store import input_dataset_parameters_part_factory
11
11
 
12
12
 
13
13
  def input_dataset_frames_part_factory(bucket_count: int = 1) -> list[dict]:
@@ -25,42 +25,6 @@ def flatten_frame_parts(frame_parts: list[dict]) -> list[tuple[str, str]]:
25
25
  return result
26
26
 
27
27
 
28
- def input_dataset_parameters_part_factory(
29
- parameter_count: int = 1,
30
- parameter_value_count: int = 1,
31
- has_date: bool = False,
32
- has_file: bool = False,
33
- ) -> list[dict]:
34
- result = [
35
- {
36
- "parameterName": uuid4().hex[:6],
37
- "parameterValues": [
38
- {"parameterValueId": i, "parameterValue": json.dumps(uuid4().hex)}
39
- for i in range(parameter_value_count)
40
- ],
41
- }
42
- for _ in range(parameter_count)
43
- ]
44
- if has_date:
45
- for data in result:
46
- param_list = data["parameterValues"]
47
- for item in param_list:
48
- item["parameterValueStartDate"] = datetime(2022, 9, 14).isoformat()
49
- if has_file:
50
- for data in result:
51
- param_list = data["parameterValues"]
52
- for item in param_list:
53
- item["parameterValue"] = json.dumps(
54
- {
55
- "__file__": {
56
- "bucket": "data",
57
- "objectKey": f"parameters/{data['parameterName']}/{uuid4().hex}.dat",
58
- }
59
- }
60
- )
61
- return result
62
-
63
-
64
28
  @pytest.mark.parametrize(
65
29
  "input_dataset_parts",
66
30
  [
@@ -97,9 +97,9 @@ class ViSPMetadataKey(StrEnum):
97
97
  class ViSPFitsAccess(FitsAccessBase):
98
98
  def __init__(self, hdu, name, auto_squeeze=False):
99
99
  super().__init__(hdu, name, auto_squeeze=auto_squeeze)
100
- self._set_metadata_key_value(ViSPMetadataKey.num_mod)
101
- self._set_metadata_key_value(ViSPMetadataKey.modstate)
102
- self._set_metadata_key_value(ViSPMetadataKey.ip_task_type)
100
+ self.num_mod: int = self.header[ViSPMetadataKey.num_mod]
101
+ self.modstate: int = self.header[ViSPMetadataKey.modstate]
102
+ self.ip_task_type: str = self.header[ViSPMetadataKey.ip_task_type]
103
103
  self.name = name
104
104
 
105
105
 
@@ -260,18 +260,58 @@ def test_subclass_flowers(visp_parse_inputs_task, max_cs_step_time_sec):
260
260
  tag_pot, constant_pot = visp_parse_inputs_task.make_flower_pots()
261
261
 
262
262
  assert len(tag_pot.stems) == 1
263
- assert len(constant_pot.stems) == 12
263
+ assert len(constant_pot.stems) == 60
264
264
  all_flower_names = [StemName.modstate]
265
265
  assert sorted([f.stem_name for f in tag_pot.stems]) == sorted(all_flower_names)
266
266
  all_bud_names = [b.stem_name for b in default_constant_bud_factory()] + [BudName.num_modstates]
267
267
  assert sorted([f.stem_name for f in constant_pot.stems]) == sorted(all_bud_names)
268
268
 
269
269
 
270
+ def test_dataset_extra_bud_factory(visp_parse_inputs_task, max_cs_step_time_sec):
271
+ """
272
+ Given: ParseInputData child class with custom stems
273
+ When: Making the constant pot
274
+ Then: The multi-task dataset extra buds are created
275
+ """
276
+ _, constant_pot = visp_parse_inputs_task.make_flower_pots()
277
+ stem_names = [f.stem_name.value for f in constant_pot.stems]
278
+ bud_name_base = [
279
+ "DATE_BEGIN",
280
+ "OBSERVING_PROGRAM_EXECUTION_IDS",
281
+ "NUM_RAW_FRAMES_PER_FPA",
282
+ "TELESCOPE_TRACKING_MODE",
283
+ "COUDE_TABLE_TRACKING_MODE",
284
+ "TELESCOPE_SCANNING_MODE",
285
+ "AVERAGE_LIGHT_LEVEL",
286
+ "AVERAGE_TELESCOPE_ELEVATION",
287
+ "AVERAGE_COUDE_TABLE_ANGLE",
288
+ "AVERAGE_TELESCOPE_AZIMUTH",
289
+ "GOS_LEVEL3_STATUS",
290
+ "GOS_LEVEL3_LAMP_STATUS",
291
+ "GOS_POLARIZER_STATUS",
292
+ "GOS_POLARIZER_ANGLE",
293
+ "GOS_RETARDER_STATUS",
294
+ "GOS_RETARDER_ANGLE",
295
+ "GOS_LEVEL0_STATUS",
296
+ ]
297
+ for base in bud_name_base:
298
+ assert "SOLAR_GAIN_" + base in stem_names
299
+ if base not in [
300
+ "NUM_RAW_FRAMES_PER_FPA",
301
+ "TELESCOPE_TRACKING_MODE",
302
+ "COUDE_TABLE_TRACKING_MODE",
303
+ "TELESCOPE_SCANNING_MODE",
304
+ ]:
305
+ assert "DARK_" + base in stem_names
306
+ if "GOS" not in base:
307
+ assert "POLCAL_" + base in stem_names
308
+
309
+
270
310
  def test_constants_correct(parse_inputs_task):
271
311
  """
272
312
  Given: ParseInputData task with a populated constant FlowerPot
273
313
  When: Updating pipeline constants
274
- Then: Pipeline constants are correctly populated
314
+ Then: A pipeline constant is correctly populated
275
315
  """
276
316
  _, constant_pot = parse_inputs_task.make_flower_pots()
277
317
  parse_inputs_task.update_constants(constant_pot)
@@ -1214,20 +1214,12 @@ def wavecal_weights(wavecal_input_wavelength) -> np.ndarray:
1214
1214
 
1215
1215
 
1216
1216
  @pytest.fixture(scope="session")
1217
- def wavecal_fit_result(wavecal_input_wavelength, wavecal_input_spectrum) -> FitResult:
1217
+ def wavecal_fit_result(wavecal_input_wavelength) -> FitResult:
1218
1218
  wavelength_params = WavelengthParameters(
1219
1219
  crpix=1, crval=10.0, dispersion=1, grating_constant=1, order=1, incident_light_angle=0
1220
1220
  )
1221
-
1222
- residuals = np.random.random(wavecal_input_wavelength.size)
1223
- residuals[-1] = np.nan
1224
- minimizer_result = MinimizerResult(residual=residuals)
1225
- return FitResult(
1226
- wavelength_parameters=wavelength_params,
1227
- minimizer_result=minimizer_result,
1228
- input_wavelength_vector=wavecal_input_wavelength,
1229
- input_spectrum=wavecal_input_spectrum,
1230
- )
1221
+ minimizer_result = MinimizerResult(residual=np.random.random(wavecal_input_wavelength.size))
1222
+ return FitResult(wavelength_parameters=wavelength_params, minimizer_result=minimizer_result)
1231
1223
 
1232
1224
 
1233
1225
  @pytest.mark.parametrize(