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,488 @@
1
+ import json
2
+ import os
3
+ import shutil
4
+ from dataclasses import asdict
5
+ from pathlib import Path
6
+ from random import randint
7
+
8
+ import asdf
9
+ from astropy.io import fits
10
+ from dkist_header_validator import spec122_validator
11
+ from dkist_processing_common.codecs.basemodel import basemodel_encoder
12
+ from dkist_processing_common.models.input_dataset import InputDatasetPartDocumentList
13
+ from dkist_processing_common.models.task_name import TaskName
14
+ from dkist_processing_common.tasks.base import WorkflowTaskBase
15
+ from dkist_service_configuration.logging import logger
16
+
17
+ from dkist_processing_trend.models.constants import TrendBudName
18
+ from dkist_processing_trend.models.tags import TrendTag
19
+ from dkist_processing_trend.models.task_name import TrendTaskName
20
+ from dkist_processing_trend.tests.conftest import TrendInputDatasetParameterValues
21
+
22
+
23
+ def translate_task(instrument: str, suffix: str):
24
+ class Translate122To214L0(WorkflowTaskBase):
25
+ def run(self) -> None:
26
+ raw_dir = (
27
+ Path(self.scratch.scratch_base_path)
28
+ / f"{instrument.upper()}{self.recipe_run_id:03n}"
29
+ )
30
+ if not self.scratch.workflow_base_path.exists():
31
+ self.scratch.workflow_base_path.mkdir(parents=True, exist_ok=False)
32
+
33
+ if not raw_dir.exists():
34
+ raise FileNotFoundError(
35
+ f"Expected to find a raw {instrument.upper()}{self.recipe_run_id:03n} folder in {self.scratch.scratch_base_path}"
36
+ )
37
+
38
+ for file in raw_dir.glob(f"*.{suffix}"):
39
+ translated_file_name = self.scratch.workflow_base_path / os.path.basename(file)
40
+ logger.info(f"Translating {file} -> {translated_file_name}")
41
+ with fits.open(file, memmap=False) as hdl:
42
+ # Handle both compressed and uncompressed files...
43
+ hdu = hdl[1] if len(hdl) > 1 else hdl[0]
44
+ header = spec122_validator.validate_and_translate_to_214_l0(
45
+ hdu.header, return_type=fits.HDUList
46
+ )[0].header
47
+
48
+ comp_hdu = fits.CompImageHDU(header=header, data=hdu.data)
49
+ comp_hdl = fits.HDUList([fits.PrimaryHDU(), comp_hdu])
50
+ comp_hdl.writeto(translated_file_name, overwrite=True)
51
+
52
+ comp_hdl.close()
53
+ del comp_hdl
54
+
55
+ return Translate122To214L0
56
+
57
+
58
+ def tag_inputs_task(suffix: str):
59
+ class TagInputs(WorkflowTaskBase):
60
+ def run(self) -> None:
61
+ logger.info(f"Looking in {self.scratch.workflow_base_path.absolute()}")
62
+ input_file_list = list(self.scratch.workflow_base_path.glob(f"*.{suffix}"))
63
+ if len(input_file_list) == 0:
64
+ raise FileNotFoundError(
65
+ f"Did not find any files matching '*.{suffix}' in {self.scratch.workflow_base_path}"
66
+ )
67
+ for file in input_file_list:
68
+ logger.info(f"Found {file}")
69
+ self.tag(path=file, tags=[TrendTag.input(), TrendTag.frame()])
70
+
71
+ return TagInputs
72
+
73
+
74
+ def input_dataset_parameter_task(parameter_data: TrendInputDatasetParameterValues):
75
+
76
+ class CreateInputDatasetParameterDocument(WorkflowTaskBase):
77
+ def run(self) -> None:
78
+ relative_path = "input_dataset_parameters.json"
79
+ self.write(
80
+ data=InputDatasetPartDocumentList(
81
+ doc_list=self.input_dataset_document_simple_parameters_part
82
+ ),
83
+ relative_path=relative_path,
84
+ tags=TrendTag.input_dataset_parameters(),
85
+ encoder=basemodel_encoder,
86
+ overwrite=True,
87
+ )
88
+ logger.info(f"Wrote input dataset parameter doc to {relative_path}")
89
+
90
+ @property
91
+ def input_dataset_document_simple_parameters_part(self):
92
+ parameters_list = []
93
+ value_id = randint(1000, 2000)
94
+ for pn, pv in asdict(parameter_data).items():
95
+ values = [
96
+ {
97
+ "parameterValueId": value_id,
98
+ "parameterValue": json.dumps(pv),
99
+ "parameterValueStartDate": "1946-11-20",
100
+ }
101
+ ]
102
+ parameter = {"parameterName": pn, "parameterValues": values}
103
+ parameters_list.append(parameter)
104
+
105
+ return parameters_list
106
+
107
+ return CreateInputDatasetParameterDocument
108
+
109
+
110
+ def save_parsing_task(
111
+ tag_list: list[str], save_file: str, save_file_tags: bool = True, save_constants: bool = True
112
+ ):
113
+ class SaveParsing(WorkflowTaskBase):
114
+ """Save the result of parsing (constants and tags) to an asdf file."""
115
+
116
+ @property
117
+ def relative_save_file(self) -> str:
118
+ return save_file
119
+
120
+ def run(self):
121
+ if save_file_tags:
122
+ file_tag_dict = self.get_input_tags()
123
+ else:
124
+ logger.info("Skipping saving of file tags")
125
+ file_tag_dict = dict()
126
+ if save_constants:
127
+ constant_dict = self.get_constants()
128
+ else:
129
+ logger.info("Skipping saving of constants")
130
+ constant_dict = dict()
131
+
132
+ full_save_file = self.scratch.workflow_base_path / self.relative_save_file
133
+ tree = {"file_tag_dict": file_tag_dict, "constants_dict": constant_dict}
134
+ af = asdf.AsdfFile(tree)
135
+ af.write_to(full_save_file)
136
+ logger.info(f"Saved input tags to {full_save_file}")
137
+
138
+ def get_input_tags(self) -> dict[str, list[str]]:
139
+ file_tag_dict = dict()
140
+ path_list = self.read(tags=tag_list)
141
+ for p in path_list:
142
+ tags = self.tags(p)
143
+ file_tag_dict[str(p)] = tags
144
+
145
+ return file_tag_dict
146
+
147
+ def get_constants(self) -> dict[str, str | float | list]:
148
+ constants_dict = dict()
149
+ for c in self.constants._db_dict.keys():
150
+ constants_dict[c] = self.constants._db_dict[c]
151
+
152
+ return constants_dict
153
+
154
+ return SaveParsing
155
+
156
+
157
+ def load_parsing_task(save_file: str):
158
+ class LoadParsing(WorkflowTaskBase):
159
+ """Load tags and constants into the database."""
160
+
161
+ @property
162
+ def relative_save_file(self) -> str:
163
+ return save_file
164
+
165
+ def run(self):
166
+ full_save_file = self.scratch.workflow_base_path / self.relative_save_file
167
+ with asdf.open(full_save_file) as af:
168
+ file_tag_dict = af.tree["file_tag_dict"]
169
+ self.tag_input_files(file_tag_dict)
170
+
171
+ constants_dict = af.tree["constants_dict"]
172
+ self.populate_constants(constants_dict)
173
+
174
+ logger.info(f"Loaded tags and constants from {full_save_file}")
175
+
176
+ def tag_input_files(self, file_tag_dict: dict[str, list[str]]):
177
+ for f, t in file_tag_dict.items():
178
+ if not os.path.exists(f):
179
+ pass
180
+ # raise FileNotFoundError(f"Expected to find {f}, but it doesn't exist.")
181
+ else:
182
+ self.tag(path=f, tags=t)
183
+
184
+ def populate_constants(self, constants_dict: dict[str, str | int | float]) -> None:
185
+ # First we purge all constants because a previous load might have polluted the DB
186
+ self.constants._purge()
187
+ for c, v in constants_dict.items():
188
+ logger.info(f"Setting value of {c} to {v}")
189
+ self.constants._update({c: v})
190
+
191
+ return LoadParsing
192
+
193
+
194
+ def get_arm_id_list(workflow_base_path: Path, parsing_file_name: str | Path) -> list[str | int]:
195
+ with asdf.open(workflow_base_path / parsing_file_name) as af:
196
+ constants = af.tree["constants_dict"]
197
+ arm_id_list = constants[TrendBudName.arm_id_list]
198
+
199
+ return arm_id_list
200
+
201
+
202
+ class SaveTaskTags(WorkflowTaskBase):
203
+ """Base task for saving all INTERMEDIATE files with the same 'TASK'"""
204
+
205
+ @property
206
+ def task_str(self) -> str:
207
+ return "TASK"
208
+
209
+ @property
210
+ def arm_id(self) -> str | int | None:
211
+ return None
212
+
213
+ @property
214
+ def relative_save_file(self) -> str:
215
+ return "default_sav.asdf"
216
+
217
+ @property
218
+ def tag_lists_to_save(self) -> list[list[str]] | list[str]:
219
+ intermediate_tags = [TrendTag.task(self.task_str), TrendTag.intermediate()]
220
+ if self.arm_id is not None:
221
+ intermediate_tags.append(TrendTag.arm_id(self.arm_id))
222
+ return [intermediate_tags]
223
+
224
+ def run(self):
225
+ file_tag_dict = dict()
226
+ tag_list_list = self.tag_lists_to_save
227
+ if isinstance(tag_list_list[0], str):
228
+ tag_list_list = [tag_list_list]
229
+
230
+ for tags_to_save in tag_list_list:
231
+ path_list = self.read(tags=tags_to_save)
232
+ save_dir = self.scratch.workflow_base_path / Path(self.relative_save_file).stem
233
+ save_dir.mkdir(exist_ok=True)
234
+ for p in path_list:
235
+ copied_path = shutil.copy(str(p), save_dir)
236
+ tags = self.tags(p)
237
+ file_tag_dict[copied_path] = tags
238
+
239
+ full_save_file = self.scratch.workflow_base_path / self.relative_save_file
240
+ tree = {"file_tag_dict": file_tag_dict}
241
+ af = asdf.AsdfFile(tree)
242
+ af.write_to(full_save_file)
243
+ logger.info(f"Saved {self.task_str} to {full_save_file}")
244
+
245
+
246
+ class LoadTaskTags(WorkflowTaskBase):
247
+ """Base task for loading file/tag associations from a previously saved set."""
248
+
249
+ @property
250
+ def relative_save_file(self) -> str:
251
+ return "default_sav.asdf"
252
+
253
+ def run(self):
254
+ full_save_file = self.scratch.workflow_base_path / self.relative_save_file
255
+ with asdf.open(full_save_file) as af:
256
+ for f, t in af.tree["file_tag_dict"].items():
257
+ self.tag(path=f, tags=t)
258
+ logger.info(f"Loaded database entries from {full_save_file}")
259
+
260
+
261
+ def save_dark_cal_task(arm_id: str | int):
262
+ class SaveDarkCal(SaveTaskTags):
263
+ @property
264
+ def task_str(self) -> str:
265
+ return TaskName.dark.value
266
+
267
+ @property
268
+ def arm_id(self):
269
+ return arm_id
270
+
271
+ @property
272
+ def relative_save_file(self) -> str:
273
+ return f"dark_cal_{arm_id}.asdf"
274
+
275
+ return SaveDarkCal
276
+
277
+
278
+ def load_dark_cal_task(arm_id: str | int):
279
+ class LoadDarkCal(LoadTaskTags):
280
+ @property
281
+ def relative_save_file(self) -> str:
282
+ return f"dark_cal_{arm_id}.asdf"
283
+
284
+ return LoadDarkCal
285
+
286
+
287
+ def save_gain_cal_task(arm_id: str | int):
288
+ class SaveGainCal(SaveTaskTags):
289
+ @property
290
+ def task_str(self) -> str:
291
+ return TaskName.gain.value
292
+
293
+ @property
294
+ def arm_id(self):
295
+ return arm_id
296
+
297
+ @property
298
+ def relative_save_file(self) -> str:
299
+ return f"gain_cal_{arm_id}.asdf"
300
+
301
+ return SaveGainCal
302
+
303
+
304
+ def load_gain_cal_task(arm_id: str | int):
305
+ class LoadGainCal(LoadTaskTags):
306
+ @property
307
+ def relative_save_file(self) -> str:
308
+ return f"gain_cal_{arm_id}.asdf"
309
+
310
+ return LoadGainCal
311
+
312
+
313
+ def save_prepped_cal_task(arm_id: str | int):
314
+ class SavePreppedData(SaveTaskTags):
315
+ @property
316
+ def tag_lists_to_save(self):
317
+ return [
318
+ [
319
+ TrendTag.intermediate(),
320
+ TrendTag.arm_id(self.arm_id),
321
+ TrendTag.task_local_pac_input(),
322
+ ],
323
+ [
324
+ TrendTag.intermediate(),
325
+ TrendTag.arm_id(self.arm_id),
326
+ TrendTag.task_global_pac_input(),
327
+ ],
328
+ ]
329
+
330
+ @property
331
+ def arm_id(self):
332
+ return arm_id
333
+
334
+ @property
335
+ def relative_save_file(self) -> str:
336
+ return f"prepped_cal_{arm_id}.asdf"
337
+
338
+ return SavePreppedData
339
+
340
+
341
+ def load_prepped_cal_task(arm_id: str | int):
342
+ class LoadPreppedData(LoadTaskTags):
343
+ @property
344
+ def relative_save_file(self) -> str:
345
+ return f"prepped_cal_{arm_id}.asdf"
346
+
347
+ return LoadPreppedData
348
+
349
+
350
+ def save_pac_fit_task(arm_id: str | int):
351
+ class SavePacFit(SaveTaskTags):
352
+ @property
353
+ def tag_lists_to_save(self):
354
+ return [
355
+ [
356
+ TrendTag.intermediate(),
357
+ TrendTag.arm_id(self.arm_id),
358
+ TrendTag.task_calibration_sequence(),
359
+ ],
360
+ [
361
+ TrendTag.intermediate(),
362
+ TrendTag.arm_id(self.arm_id),
363
+ TrendTag.task_best_fit_parameters(),
364
+ ],
365
+ [
366
+ TrendTag.intermediate(),
367
+ TrendTag.arm_id(self.arm_id),
368
+ TrendTag.task_best_fit_demodulation_matrices(),
369
+ ],
370
+ [
371
+ TrendTag.intermediate(),
372
+ TrendTag.arm_id(self.arm_id),
373
+ TrendTag.task_best_fit_flux(),
374
+ ],
375
+ [
376
+ TrendTag.intermediate(),
377
+ TrendTag.arm_id(self.arm_id),
378
+ TrendTag.task_fit_residuals(),
379
+ ],
380
+ ]
381
+
382
+ @property
383
+ def arm_id(self):
384
+ return arm_id
385
+
386
+ @property
387
+ def relative_save_file(self) -> str:
388
+ return f"pac_fit_{arm_id}.asdf"
389
+
390
+ return SavePacFit
391
+
392
+
393
+ def load_pac_fit_task(arm_id: str | int):
394
+ class LoadPacFit(LoadTaskTags):
395
+ @property
396
+ def relative_save_file(self) -> str:
397
+ return f"pac_fit_{arm_id}.asdf"
398
+
399
+ return LoadPacFit
400
+
401
+
402
+ def save_processed_demod_cal_task(arm_id: str | int):
403
+ class SaveProcessedDemodCal(SaveTaskTags):
404
+ @property
405
+ def task_str(self) -> str:
406
+ return TrendTaskName.processed_demodulation_matrices.value
407
+
408
+ @property
409
+ def arm_id(self):
410
+ return arm_id
411
+
412
+ @property
413
+ def relative_save_file(self) -> str:
414
+ return f"processed_demod_cal_{arm_id}.asdf"
415
+
416
+ return SaveProcessedDemodCal
417
+
418
+
419
+ def load_processed_demod_cal_task(arm_id: str | int):
420
+ class LoadGainCal(LoadTaskTags):
421
+ @property
422
+ def relative_save_file(self) -> str:
423
+ return f"processed_demod_cal_{arm_id}.asdf"
424
+
425
+ return LoadGainCal
426
+
427
+
428
+ def save_dmpd_task(arm_id: str | int):
429
+ class SaveDmpd(SaveTaskTags):
430
+ @property
431
+ def tag_lists_to_save(self):
432
+ return [
433
+ [
434
+ TrendTag.intermediate(),
435
+ TrendTag.arm_id(self.arm_id),
436
+ TrendTag.task_single_beam_demodulated_polcal_data(),
437
+ ],
438
+ [
439
+ TrendTag.intermediate(),
440
+ TrendTag.arm_id(self.arm_id),
441
+ TrendTag.task_demodulated_polcal_data(),
442
+ ],
443
+ ]
444
+
445
+ @property
446
+ def arm_id(self):
447
+ return arm_id
448
+
449
+ @property
450
+ def relative_save_file(self) -> str:
451
+ return f"dmpd_{arm_id}.asdf"
452
+
453
+ return SaveDmpd
454
+
455
+
456
+ def load_dmpd_task(arm_id: str | int):
457
+ class LoadDmpd(LoadTaskTags):
458
+ @property
459
+ def relative_save_file(self) -> str:
460
+ return f"dmpd_{arm_id}.asdf"
461
+
462
+ return LoadDmpd
463
+
464
+
465
+ def save_visp_geo_cal_task(arm_id: str | int):
466
+ class SaveVispGeoCal(SaveTaskTags):
467
+ @property
468
+ def task_str(self) -> str:
469
+ return TrendTaskName.visp_geometric_calibration.value
470
+
471
+ @property
472
+ def arm_id(self):
473
+ return arm_id
474
+
475
+ @property
476
+ def relative_save_file(self) -> str:
477
+ return f"visp_geo_cal_{arm_id}.asdf"
478
+
479
+ return SaveVispGeoCal
480
+
481
+
482
+ def load_visp_geo_cal_task(arm_id: str | int):
483
+ class LoadVispGeoCal(LoadTaskTags):
484
+ @property
485
+ def relative_save_file(self) -> str:
486
+ return f"visp_geo_cal_{arm_id}.asdf"
487
+
488
+ return LoadVispGeoCal
@@ -0,0 +1,82 @@
1
+ from subprocess import Popen
2
+
3
+ import pytest
4
+ from dkist_processing_core import TaskBase
5
+
6
+ from dkist_processing_trend.tasks.arm_task_factory import initialize_arm_task
7
+ from dkist_processing_trend.tasks.arm_task_factory import retreive_arm_task
8
+ from dkist_processing_trend.tasks.trend_base import TrendArmTaskBase
9
+ from dkist_processing_trend.tests.conftest import TrendTestingConstantsDatabase
10
+
11
+
12
+ class DummyArmTask(TrendArmTaskBase):
13
+ """A real cool class"""
14
+
15
+ def run(self):
16
+ return 5
17
+
18
+
19
+ @pytest.mark.parametrize("arm_id", [pytest.param("foo", id="str"), pytest.param(3, id="int")])
20
+ def test_arm_task_factory(arm_id, recipe_run_id, link_constants_db):
21
+ """
22
+ Given: A Task class that subclasses `TrendArmTaskBase`
23
+ When: Initializing a new version of that task with a pre-defined arm_id and then retreiving the new class
24
+ Then: The new class is a `TaskBase`, has the correct arm_id, retains the docstring of its parent, and is importable
25
+ from the parent's module.
26
+ """
27
+ link_constants_db(
28
+ recipe_run_id,
29
+ TrendTestingConstantsDatabase(),
30
+ )
31
+ initialize_arm_task(DummyArmTask, arm_id=arm_id)
32
+ arm_task_class = retreive_arm_task(DummyArmTask, arm_id=arm_id)
33
+ assert issubclass(arm_task_class, DummyArmTask)
34
+ assert issubclass(arm_task_class, TaskBase)
35
+ assert arm_task_class.__doc__ == "A real cool class"
36
+ assert arm_task_class.__name__ == f"DummyArmTask{str(arm_id).capitalize()}"
37
+ assert arm_task_class.__qualname__ == f"DummyArmTask{str(arm_id).capitalize()}"
38
+ assert arm_task_class.__module__ == DummyArmTask.__module__
39
+
40
+ with arm_task_class(
41
+ recipe_run_id=recipe_run_id, workflow_version="version", workflow_name="name"
42
+ ) as task:
43
+ assert task.arm_id == arm_id
44
+ assert task.run() == 5
45
+
46
+
47
+ def test_uninitialized_arm_task_raises():
48
+ """
49
+ Given: A Task class that subclasses `TrendArmTaskBase`
50
+ When: Trying to retrieve an "arm version" of that class, when the particular arm hasn't been initialized
51
+ Then: The correct error is raise
52
+ """
53
+ initialize_arm_task(DummyArmTask, arm_id="foo")
54
+ with pytest.raises(AttributeError, match="Can't find DummyArmTaskBar"):
55
+ retreive_arm_task(DummyArmTask, arm_id="bar")
56
+
57
+
58
+ @pytest.mark.parametrize(
59
+ "module_path, task_name",
60
+ [
61
+ pytest.param(
62
+ "tasks.visp.visp_process_demod", "VispProcessDemodulationMatrices1", id="visp"
63
+ ),
64
+ pytest.param("tasks.dark", "DarkCalibrationSp", id="cryo"),
65
+ pytest.param("tasks.run_pac_fitter", "RunPacFitterHband", id="dlnirsp"),
66
+ ],
67
+ )
68
+ def test_real_arm_factory_tasks_are_importable(module_path, task_name):
69
+ """
70
+ Given: A python environment with `dkist_processing_trend` installed
71
+ When: Trying to import a task class that was defined with `initialize_arm_task` from its parent's module
72
+ Then: The import succeeds
73
+ """
74
+ p = Popen(
75
+ args=[
76
+ "python",
77
+ "-c",
78
+ f"from dkist_processing_trend.{module_path} import {task_name}",
79
+ ]
80
+ )
81
+ p.wait()
82
+ assert p.returncode == 0
@@ -0,0 +1,86 @@
1
+ import random
2
+
3
+ import pytest
4
+ from dkist_processing_common.codecs.str import str_decoder
5
+ from dkist_processing_common.codecs.str import str_encoder
6
+
7
+ from dkist_processing_trend.models.constants import TrendConstants
8
+ from dkist_processing_trend.models.tags import TrendTag
9
+ from dkist_processing_trend.tasks.trend_base import TrendArmTaskBase
10
+ from dkist_processing_trend.tasks.trend_base import TrendTaskBase
11
+ from dkist_processing_trend.tests.conftest import TrendTestingConstantsDatabase
12
+
13
+
14
+ @pytest.fixture
15
+ def trend_science_task(recipe_run_id, link_constants_db):
16
+ class ScienceTask(TrendTaskBase):
17
+ def run(self): ...
18
+
19
+ link_constants_db(recipe_run_id, TrendTestingConstantsDatabase())
20
+ with ScienceTask(
21
+ recipe_run_id=recipe_run_id, workflow_name="name", workflow_version="version"
22
+ ) as task:
23
+ yield task
24
+ task._purge
25
+
26
+
27
+ class ArmScienceTask(TrendArmTaskBase):
28
+ def run(self):
29
+ self.write(data="Ran .run!", tags=[TrendTag.task("ALL_DONE")], encoder=str_encoder)
30
+
31
+
32
+ @pytest.fixture
33
+ def make_trend_arm_science_task(recipe_run_id, link_constants_db):
34
+
35
+ def make_task(arm_id):
36
+ link_constants_db(recipe_run_id, TrendTestingConstantsDatabase())
37
+ with ArmScienceTask(
38
+ arm_id=arm_id,
39
+ recipe_run_id=recipe_run_id,
40
+ workflow_name="name",
41
+ workflow_version="version",
42
+ ) as task:
43
+ yield task
44
+ task._purge
45
+
46
+ return make_task
47
+
48
+
49
+ def test_trend_base_task(trend_science_task):
50
+ """
51
+ Given: A task class that is a subclass of `TrendTaskBase`
52
+ When: Instantiating the class
53
+ Then: The class' `.constants` property is the correct type
54
+ """
55
+ task = trend_science_task
56
+ assert isinstance(task.constants, TrendConstants)
57
+ assert task.constants.arm_id_list == [1, 2, 3]
58
+
59
+
60
+ @pytest.mark.parametrize(
61
+ "dataset_has_arm_data",
62
+ [pytest.param(False, id="arm_w_no_data"), pytest.param(True, id="arm_w_data")],
63
+ )
64
+ def test_trend_arm_base_task(make_trend_arm_science_task, dataset_has_arm_data):
65
+ """
66
+ Given: A task class that is a subclass of `TrendArmTaskBase`
67
+ When: Instantiating that class
68
+ Then: The `arm_id` argument is correctly set as the `.arm_id` property
69
+ """
70
+ constants_arm_id_list = TrendTestingConstantsDatabase().ARM_ID_LIST
71
+ if dataset_has_arm_data:
72
+ arm_id = random.choice(constants_arm_id_list)
73
+ else:
74
+ arm_id = "LUHILHDLLVUHFSHDUIAHDL"
75
+ assert arm_id not in constants_arm_id_list
76
+
77
+ task = next(make_trend_arm_science_task(arm_id=arm_id))
78
+ assert task.arm_id == arm_id
79
+
80
+ task()
81
+
82
+ if dataset_has_arm_data:
83
+ assert task.count(tags=[TrendTag.task("ALL_DONE")]) == 1
84
+ assert next(task.read(tags=[TrendTag.task("ALL_DONE")], decoder=str_decoder)) == "Ran .run!"
85
+ else:
86
+ assert task.count(tags=[TrendTag.task("ALL_DONE")]) == 0