dkist-processing-common 10.5.4__py3-none-any.whl → 12.1.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 (122) hide show
  1. changelog/280.misc.rst +1 -0
  2. changelog/282.feature.2.rst +2 -0
  3. changelog/282.feature.rst +2 -0
  4. changelog/284.feature.rst +1 -0
  5. changelog/285.feature.rst +2 -0
  6. changelog/285.misc.rst +2 -0
  7. changelog/286.feature.rst +2 -0
  8. changelog/287.misc.rst +1 -0
  9. dkist_processing_common/__init__.py +1 -0
  10. dkist_processing_common/_util/constants.py +1 -0
  11. dkist_processing_common/_util/graphql.py +1 -0
  12. dkist_processing_common/_util/scratch.py +9 -9
  13. dkist_processing_common/_util/tags.py +1 -0
  14. dkist_processing_common/codecs/array.py +20 -0
  15. dkist_processing_common/codecs/asdf.py +9 -3
  16. dkist_processing_common/codecs/basemodel.py +22 -0
  17. dkist_processing_common/codecs/bytes.py +1 -0
  18. dkist_processing_common/codecs/fits.py +37 -9
  19. dkist_processing_common/codecs/iobase.py +1 -0
  20. dkist_processing_common/codecs/json.py +1 -0
  21. dkist_processing_common/codecs/path.py +1 -0
  22. dkist_processing_common/codecs/quality.py +1 -1
  23. dkist_processing_common/codecs/str.py +1 -0
  24. dkist_processing_common/config.py +64 -25
  25. dkist_processing_common/manual.py +6 -8
  26. dkist_processing_common/models/constants.py +373 -37
  27. dkist_processing_common/models/dkist_location.py +27 -0
  28. dkist_processing_common/models/fits_access.py +48 -0
  29. dkist_processing_common/models/flower_pot.py +231 -9
  30. dkist_processing_common/models/fried_parameter.py +41 -0
  31. dkist_processing_common/models/graphql.py +66 -75
  32. dkist_processing_common/models/input_dataset.py +117 -0
  33. dkist_processing_common/models/message.py +1 -1
  34. dkist_processing_common/models/message_queue_binding.py +1 -1
  35. dkist_processing_common/models/metric_code.py +2 -0
  36. dkist_processing_common/models/parameters.py +65 -28
  37. dkist_processing_common/models/quality.py +50 -5
  38. dkist_processing_common/models/tags.py +23 -21
  39. dkist_processing_common/models/task_name.py +3 -2
  40. dkist_processing_common/models/telemetry.py +28 -0
  41. dkist_processing_common/models/wavelength.py +3 -1
  42. dkist_processing_common/parsers/average_bud.py +46 -0
  43. dkist_processing_common/parsers/cs_step.py +13 -12
  44. dkist_processing_common/parsers/dsps_repeat.py +6 -4
  45. dkist_processing_common/parsers/experiment_id_bud.py +12 -4
  46. dkist_processing_common/parsers/id_bud.py +42 -27
  47. dkist_processing_common/parsers/l0_fits_access.py +5 -3
  48. dkist_processing_common/parsers/l1_fits_access.py +51 -23
  49. dkist_processing_common/parsers/lookup_bud.py +125 -0
  50. dkist_processing_common/parsers/near_bud.py +21 -20
  51. dkist_processing_common/parsers/observing_program_id_bud.py +24 -0
  52. dkist_processing_common/parsers/proposal_id_bud.py +13 -5
  53. dkist_processing_common/parsers/quality.py +2 -0
  54. dkist_processing_common/parsers/retarder.py +32 -0
  55. dkist_processing_common/parsers/single_value_single_key_flower.py +6 -1
  56. dkist_processing_common/parsers/task.py +8 -6
  57. dkist_processing_common/parsers/time.py +178 -72
  58. dkist_processing_common/parsers/unique_bud.py +21 -22
  59. dkist_processing_common/parsers/wavelength.py +5 -3
  60. dkist_processing_common/tasks/__init__.py +3 -2
  61. dkist_processing_common/tasks/assemble_movie.py +4 -3
  62. dkist_processing_common/tasks/base.py +59 -60
  63. dkist_processing_common/tasks/l1_output_data.py +54 -53
  64. dkist_processing_common/tasks/mixin/globus.py +24 -27
  65. dkist_processing_common/tasks/mixin/interservice_bus.py +1 -0
  66. dkist_processing_common/tasks/mixin/metadata_store.py +108 -243
  67. dkist_processing_common/tasks/mixin/object_store.py +22 -0
  68. dkist_processing_common/tasks/mixin/quality/__init__.py +1 -0
  69. dkist_processing_common/tasks/mixin/quality/_base.py +8 -1
  70. dkist_processing_common/tasks/mixin/quality/_metrics.py +166 -14
  71. dkist_processing_common/tasks/output_data_base.py +4 -3
  72. dkist_processing_common/tasks/parse_l0_input_data.py +277 -15
  73. dkist_processing_common/tasks/quality_metrics.py +9 -9
  74. dkist_processing_common/tasks/teardown.py +7 -7
  75. dkist_processing_common/tasks/transfer_input_data.py +67 -69
  76. dkist_processing_common/tasks/trial_catalog.py +77 -17
  77. dkist_processing_common/tasks/trial_output_data.py +16 -17
  78. dkist_processing_common/tasks/write_l1.py +102 -72
  79. dkist_processing_common/tests/conftest.py +32 -173
  80. dkist_processing_common/tests/mock_metadata_store.py +271 -0
  81. dkist_processing_common/tests/test_assemble_movie.py +4 -4
  82. dkist_processing_common/tests/test_assemble_quality.py +32 -4
  83. dkist_processing_common/tests/test_base.py +5 -19
  84. dkist_processing_common/tests/test_codecs.py +103 -12
  85. dkist_processing_common/tests/test_constants.py +15 -0
  86. dkist_processing_common/tests/test_dkist_location.py +15 -0
  87. dkist_processing_common/tests/test_fits_access.py +56 -19
  88. dkist_processing_common/tests/test_flower_pot.py +147 -5
  89. dkist_processing_common/tests/test_fried_parameter.py +27 -0
  90. dkist_processing_common/tests/test_input_dataset.py +78 -361
  91. dkist_processing_common/tests/test_interservice_bus.py +1 -0
  92. dkist_processing_common/tests/test_interservice_bus_mixin.py +1 -1
  93. dkist_processing_common/tests/test_manual_processing.py +33 -0
  94. dkist_processing_common/tests/test_output_data_base.py +5 -7
  95. dkist_processing_common/tests/test_parameters.py +71 -22
  96. dkist_processing_common/tests/test_parse_l0_input_data.py +115 -32
  97. dkist_processing_common/tests/test_publish_catalog_messages.py +2 -24
  98. dkist_processing_common/tests/test_quality.py +1 -0
  99. dkist_processing_common/tests/test_quality_mixin.py +255 -23
  100. dkist_processing_common/tests/test_scratch.py +2 -1
  101. dkist_processing_common/tests/test_stems.py +511 -168
  102. dkist_processing_common/tests/test_submit_dataset_metadata.py +3 -7
  103. dkist_processing_common/tests/test_tags.py +1 -0
  104. dkist_processing_common/tests/test_task_name.py +1 -1
  105. dkist_processing_common/tests/test_task_parsing.py +17 -7
  106. dkist_processing_common/tests/test_teardown.py +28 -24
  107. dkist_processing_common/tests/test_transfer_input_data.py +270 -125
  108. dkist_processing_common/tests/test_transfer_l1_output_data.py +2 -3
  109. dkist_processing_common/tests/test_trial_catalog.py +83 -8
  110. dkist_processing_common/tests/test_trial_output_data.py +46 -73
  111. dkist_processing_common/tests/test_workflow_task_base.py +8 -10
  112. dkist_processing_common/tests/test_write_l1.py +298 -76
  113. dkist_processing_common-12.1.0rc1.dist-info/METADATA +265 -0
  114. dkist_processing_common-12.1.0rc1.dist-info/RECORD +134 -0
  115. {dkist_processing_common-10.5.4.dist-info → dkist_processing_common-12.1.0rc1.dist-info}/WHEEL +1 -1
  116. docs/conf.py +1 -0
  117. docs/index.rst +1 -1
  118. docs/landing_page.rst +13 -0
  119. dkist_processing_common/tasks/mixin/input_dataset.py +0 -166
  120. dkist_processing_common-10.5.4.dist-info/METADATA +0 -175
  121. dkist_processing_common-10.5.4.dist-info/RECORD +0 -112
  122. {dkist_processing_common-10.5.4.dist-info → dkist_processing_common-12.1.0rc1.dist-info}/top_level.txt +0 -0
@@ -1,11 +1,11 @@
1
1
  """Data structures for messages placed on the interservice bus."""
2
+
2
3
  from typing import Type
3
4
 
4
5
  from pydantic import Field
5
6
  from talus import MessageBodyBase
6
7
  from talus import PublishMessageBase
7
8
 
8
-
9
9
  ########################
10
10
  # Message Body Schemas #
11
11
  ########################
@@ -1,4 +1,5 @@
1
1
  """Binding between a queue and a message to be published."""
2
+
2
3
  from talus import Binding
3
4
  from talus import Queue
4
5
 
@@ -7,7 +8,6 @@ from dkist_processing_common.models.message import CatalogFrameMessage
7
8
  from dkist_processing_common.models.message import CatalogObjectMessage
8
9
  from dkist_processing_common.models.message import CreateQualityReportMessage
9
10
 
10
-
11
11
  catalog_frame_queue = Queue(
12
12
  name="catalog.frame.q", arguments=common_configurations.isb_queue_arguments
13
13
  )
@@ -1,4 +1,5 @@
1
1
  """Controlled list of quality metric codes."""
2
+
2
3
  from enum import StrEnum
3
4
 
4
5
 
@@ -23,3 +24,4 @@ class MetricCode(StrEnum):
23
24
  range = "RANGE"
24
25
  sensitivity = "SENSITIVITY"
25
26
  task_types = "TASK_TYPES"
27
+ wavecal_fit = "WAVECAL_FIT"
@@ -1,14 +1,23 @@
1
1
  """Base class for parameter-parsing object."""
2
+
2
3
  import logging
4
+ from contextlib import contextmanager
3
5
  from datetime import datetime
6
+ from pathlib import Path
4
7
  from typing import Any
8
+ from typing import Callable
5
9
  from typing import Literal
6
10
 
7
11
  import numpy as np
8
12
  import scipy.interpolate as spi
9
- from astropy.io import fits
10
13
 
11
- from dkist_processing_common.tasks.mixin.input_dataset import InputDatasetParameterValue
14
+ from dkist_processing_common._util.scratch import WorkflowFileSystem
15
+ from dkist_processing_common.codecs.array import array_decoder
16
+ from dkist_processing_common.codecs.basemodel import basemodel_decoder
17
+ from dkist_processing_common.codecs.fits import fits_array_decoder
18
+ from dkist_processing_common.models.input_dataset import InputDatasetFilePointer
19
+ from dkist_processing_common.models.input_dataset import InputDatasetPartDocumentList
20
+ from dkist_processing_common.models.tags import Tag
12
21
 
13
22
  logger = logging.getLogger(__name__)
14
23
 
@@ -24,9 +33,9 @@ class ParameterBase:
24
33
 
25
34
  To use in an instrument pipeline a subclass is required. Here's a simple, but complete example::
26
35
 
27
- class InstParameters(ParameterBase)
28
- def __init__(self, input_dataset_parameters, some_other_parameter):
29
- super().__init__(input_dataset_parameters)
36
+ class InstParameters(ParameterBase):
37
+ def __init__(self, scratch, some_other_parameters):
38
+ super().__init__(scratch=scratch)
30
39
  self._thing = self._some_function(some_other_parameters)
31
40
 
32
41
  @property
@@ -34,7 +43,7 @@ class ParameterBase:
34
43
  return self._find_most_recent_past_value("some_parameter_name")
35
44
 
36
45
  @property
37
- def complicate_parameter(self):
46
+ def complicated_parameter(self):
38
47
  return self._some_complicated_parsing_function("complicated_parameter_name", another_argument)
39
48
 
40
49
 
@@ -55,15 +64,16 @@ class ParameterBase:
55
64
  workflow_version=workflow_version,
56
65
  )
57
66
 
58
- self.parameters = InstParameters(self.input_dataset_parameters) #<------ This is the important line
67
+ self.parameters = InstParameters(scratch=self.scratch) #<------ This is the important line
59
68
 
60
- Note that the first argument to the ConstantsSubclass with *always* be self.input_dataset_parameters, but
61
- additional argument can be passed if the subclass requires them.
69
+ ParameterBase needs the task scratch in order to read the parameters document written at input dataset
70
+ transfer. Note that the first argument to the ConstantsSubclass will *always* be scratch, but additional
71
+ arguments can be passed if the subclass requires them.
62
72
 
63
73
  Parameters
64
74
  ----------
65
- input_dataset_parameters
66
- The input parameters
75
+ scratch
76
+ The task scratch WorkflowFileSystem instance
67
77
 
68
78
  obs_ip_start_time
69
79
  A string containing the start date of the Observe IP task type frames. Must be in isoformat.
@@ -74,25 +84,53 @@ class ParameterBase:
74
84
 
75
85
  def __init__(
76
86
  self,
77
- input_dataset_parameters: dict[str, list[InputDatasetParameterValue]],
87
+ scratch: WorkflowFileSystem,
78
88
  obs_ip_start_time: str | None = None,
79
89
  **kwargs,
80
90
  ):
91
+ self.scratch = scratch
92
+ input_dataset_parameter_model = self._get_parameters_doc_from_file()
93
+ input_dataset_parameters = {}
94
+ if input_dataset_parameter_model is not None:
95
+ input_dataset_parameters = {
96
+ p.parameter_name: p.parameter_values for p in input_dataset_parameter_model.doc_list
97
+ }
81
98
  self.input_dataset_parameters = input_dataset_parameters
99
+
82
100
  if obs_ip_start_time is not None:
83
101
  # Specifically `not None` because we want to error normally on badly formatted strings (including "").
84
102
  self._obs_ip_start_datetime = datetime.fromisoformat(obs_ip_start_time)
85
103
  else:
86
104
  logger.info(
87
105
  "WARNING: "
88
- "The task containing this parameters object did not provide an obs ip start time. "
89
- "This really only makes sense for Parsing tasks."
106
+ "The task containing this parameters object did not provide an obs ip start time, "
107
+ "which really only makes sense for Parsing tasks."
90
108
  )
91
109
 
92
110
  for parent_class in self.__class__.__bases__:
93
111
  if hasattr(parent_class, "is_param_mixin"):
94
112
  parent_class.__init__(self, **kwargs)
95
113
 
114
+ def _read_parameter_file(
115
+ self, tag: str, decoder: Callable[[Path], Any], **decoder_kwargs
116
+ ) -> Any:
117
+ """Read any file in the task scratch instance."""
118
+ paths = list(self.scratch.find_all(tags=tag))
119
+ if len(paths) == 0:
120
+ logger.info(f"WARNING: There is no parameter file for {tag = }")
121
+ if len(paths) == 1:
122
+ return decoder(paths[0], **decoder_kwargs)
123
+ if len(paths) > 1:
124
+ raise ValueError(f"There is more than one parameter file for {tag = }: {paths}")
125
+
126
+ def _get_parameters_doc_from_file(self) -> InputDatasetPartDocumentList:
127
+ """Get parameters doc saved at the TransferL0Data task."""
128
+ tag = Tag.input_dataset_parameters()
129
+ parameters_from_file = self._read_parameter_file(
130
+ tag=tag, decoder=basemodel_decoder, model=InputDatasetPartDocumentList
131
+ )
132
+ return parameters_from_file
133
+
96
134
  def _find_most_recent_past_value(
97
135
  self,
98
136
  parameter_name: str,
@@ -113,20 +151,19 @@ class ParameterBase:
113
151
  )
114
152
  return result
115
153
 
116
- @staticmethod
117
- def _load_param_value_from_fits(param_dict: dict, hdu: int = 0) -> np.ndarray:
118
- """Load a numpy array from a parameter pointing to a FITS file."""
119
- file_path = param_dict["param_path"]
120
-
121
- hdul = fits.open(file_path)
122
- return hdul[hdu].data
123
-
124
- @staticmethod
125
- def _load_param_value_from_numpy_save(param_dict: dict) -> np.ndarray:
126
- """Return the data associated with a parameter file saved in numpy format."""
127
- file_path = param_dict["param_path"]
128
- result = np.load(file_path)
129
- return result
154
+ def _load_param_value_from_fits(
155
+ self, param_obj: InputDatasetFilePointer, hdu: int = 0
156
+ ) -> np.ndarray:
157
+ """Return the data associated with a tagged parameter file saved in FITS format."""
158
+ tag = param_obj.file_pointer.tag
159
+ param_value = self._read_parameter_file(tag=tag, decoder=fits_array_decoder, hdu=hdu)
160
+ return param_value
161
+
162
+ def _load_param_value_from_numpy_save(self, param_obj: InputDatasetFilePointer) -> np.ndarray:
163
+ """Return the data associated with a tagged parameter file saved in numpy format."""
164
+ tag = param_obj.file_pointer.tag
165
+ param_value = self._read_parameter_file(tag=tag, decoder=array_decoder)
166
+ return param_value
130
167
 
131
168
 
132
169
  class _ParamMixinBase:
@@ -1,7 +1,11 @@
1
1
  """Support classes used to create a quality report."""
2
+
2
3
  from typing import Any
3
4
 
4
5
  from pydantic import BaseModel
6
+ from pydantic import Field
7
+ from pydantic import field_validator
8
+ from pydantic_core.core_schema import ValidationInfo
5
9
 
6
10
 
7
11
  class Plot2D(BaseModel):
@@ -13,6 +17,50 @@ class Plot2D(BaseModel):
13
17
  series_name: str | None = None
14
18
  ylabel_horizontal: bool = False
15
19
  ylim: tuple[float, float] | None = None
20
+ plot_kwargs: dict[str, dict[str, Any]] = Field(default_factory=dict)
21
+ sort_series: bool = True
22
+
23
+
24
+ class VerticalMultiPanePlot2D(BaseModel):
25
+ """
26
+ Support class to hold a multi-pane plot with plots stacked vertically.
27
+
28
+ This type of metric is really geared towards plots that share an X axis and have no gap between them. If you just
29
+ want two separate plots it's probably better to use a list of `Plot2D` objects.
30
+ """
31
+
32
+ top_to_bottom_plot_list: list[Plot2D]
33
+ match_x_axes: bool = True
34
+ no_gap: bool = True
35
+ top_to_bottom_height_ratios: list[float] | None = None
36
+
37
+ @field_validator("top_to_bottom_height_ratios")
38
+ @classmethod
39
+ def ensure_same_number_of_height_ratios_and_plots(
40
+ cls, height_ratios: list[float] | None, info: ValidationInfo
41
+ ) -> list[float]:
42
+ """
43
+ Make sure that the number of height ratios is the same as the number of plots.
44
+
45
+ Also populates default, same-size ratios if no ratios were given.
46
+ """
47
+ try:
48
+ plot_list = info.data["top_to_bottom_plot_list"]
49
+ except KeyError:
50
+ # The plot list didn't validate for some reason. We're about to error anyway.
51
+ return [1.0]
52
+
53
+ num_plots = len(plot_list)
54
+ if height_ratios is None:
55
+ return [1.0] * num_plots
56
+
57
+ if len(height_ratios) != num_plots:
58
+ raise ValueError(
59
+ f"The number of items in `top_to_bottom_height_ratios` list ({len(height_ratios)}) is not "
60
+ f"the same as the number of plots ({num_plots})"
61
+ )
62
+
63
+ return height_ratios
16
64
 
17
65
 
18
66
  class SimpleTable(BaseModel):
@@ -57,11 +105,7 @@ class PlotRaincloud(BaseModel):
57
105
 
58
106
 
59
107
  class ReportMetric(BaseModel):
60
- """
61
- A Quality Report is made up of a list of metrics with the schema defined by this class.
62
-
63
- Additionally, this class can produce a Flowable or List of Flowables to be render the metric in the PDF Report
64
- """
108
+ """A Quality Report is made up of a list of metrics with the schema defined by this class."""
65
109
 
66
110
  name: str
67
111
  description: str
@@ -69,6 +113,7 @@ class ReportMetric(BaseModel):
69
113
  facet: str | None = None
70
114
  statement: str | list[str] | None = None
71
115
  plot_data: Plot2D | list[Plot2D] | None = None
116
+ multi_plot_data: VerticalMultiPanePlot2D | None = None
72
117
  histogram_data: PlotHistogram | list[PlotHistogram] | None = None
73
118
  table_data: SimpleTable | list[SimpleTable] | None = None
74
119
  modmat_data: ModulationMatrixHistograms | None = None
@@ -1,5 +1,7 @@
1
1
  """Components of the Tag model. Stem + Optional Suffix = Tag."""
2
+
2
3
  from enum import Enum
4
+ from enum import StrEnum
3
5
 
4
6
  from dkist_processing_common.models.task_name import TaskName
5
7
 
@@ -7,7 +9,7 @@ from dkist_processing_common.models.task_name import TaskName
7
9
  EXP_TIME_ROUND_DIGITS: int = 6
8
10
 
9
11
 
10
- class StemName(str, Enum):
12
+ class StemName(StrEnum):
11
13
  """Controlled list of Tag Stems."""
12
14
 
13
15
  output = "OUTPUT"
@@ -42,7 +44,7 @@ class Tag:
42
44
  """Controlled methods for creating tags from stems + optional suffixes."""
43
45
 
44
46
  @staticmethod
45
- def format_tag(stem: StemName | str, *parts):
47
+ def format_tag(stem: StemName | str, *parts) -> str:
46
48
  """
47
49
  Create a formatted tag sting given the input parts.
48
50
 
@@ -63,7 +65,7 @@ class Tag:
63
65
 
64
66
  # Static Tags
65
67
  @classmethod
66
- def movie_frame(cls):
68
+ def movie_frame(cls) -> str:
67
69
  """
68
70
  Return a movie frame tag.
69
71
 
@@ -74,7 +76,7 @@ class Tag:
74
76
  return cls.format_tag(StemName.movie_frame)
75
77
 
76
78
  @classmethod
77
- def input(cls):
79
+ def input(cls) -> str:
78
80
  """
79
81
  Return an input tag.
80
82
 
@@ -96,7 +98,7 @@ class Tag:
96
98
  return cls.format_tag(StemName.calibrated)
97
99
 
98
100
  @classmethod
99
- def output(cls):
101
+ def output(cls) -> str:
100
102
  """
101
103
  Return an output tag.
102
104
 
@@ -107,7 +109,7 @@ class Tag:
107
109
  return cls.format_tag(StemName.output)
108
110
 
109
111
  @classmethod
110
- def frame(cls):
112
+ def frame(cls) -> str:
111
113
  """
112
114
  Return a frame tag.
113
115
 
@@ -118,7 +120,7 @@ class Tag:
118
120
  return cls.format_tag(StemName.frame)
119
121
 
120
122
  @classmethod
121
- def intermediate(cls):
123
+ def intermediate(cls) -> str:
122
124
  """
123
125
  Return an intermediate tag.
124
126
 
@@ -129,7 +131,7 @@ class Tag:
129
131
  return cls.format_tag(StemName.intermediate)
130
132
 
131
133
  @classmethod
132
- def input_dataset_observe_frames(cls):
134
+ def input_dataset_observe_frames(cls) -> str:
133
135
  """
134
136
  Return an input dataset observe frames tag.
135
137
 
@@ -140,7 +142,7 @@ class Tag:
140
142
  return cls.format_tag(StemName.input_dataset, "observe_frames")
141
143
 
142
144
  @classmethod
143
- def input_dataset_calibration_frames(cls):
145
+ def input_dataset_calibration_frames(cls) -> str:
144
146
  """
145
147
  Return an input dataset calibration frames tag.
146
148
 
@@ -151,7 +153,7 @@ class Tag:
151
153
  return cls.format_tag(StemName.input_dataset, "calibration_frames")
152
154
 
153
155
  @classmethod
154
- def input_dataset_parameters(cls):
156
+ def input_dataset_parameters(cls) -> str:
155
157
  """
156
158
  Return an input dataset parameters tag.
157
159
 
@@ -162,7 +164,7 @@ class Tag:
162
164
  return cls.format_tag(StemName.input_dataset, "parameters")
163
165
 
164
166
  @classmethod
165
- def movie(cls):
167
+ def movie(cls) -> str:
166
168
  """
167
169
  Return a movie tag.
168
170
 
@@ -230,7 +232,7 @@ class Tag:
230
232
 
231
233
  @classmethod
232
234
  def task_demodulation_matrices(cls) -> str:
233
- """Tags intermediate demodulation matric calibration objects."""
235
+ """Tags intermediate demodulation matrix calibration objects."""
234
236
  return cls.task(TaskName.demodulation_matrices.value)
235
237
 
236
238
  @classmethod
@@ -254,13 +256,13 @@ class Tag:
254
256
  return cls.task(TaskName.geometric_offsets.value)
255
257
 
256
258
  @classmethod
257
- def task_geometric_sepectral_shifts(cls) -> str:
259
+ def task_geometric_spectral_shifts(cls) -> str:
258
260
  """Tags intermediate geometric spectral shift calibration objects."""
259
261
  return cls.task(TaskName.geometric_spectral_shifts.value)
260
262
 
261
263
  # Dynamic Tags
262
264
  @classmethod
263
- def task(cls, ip_task_type: str):
265
+ def task(cls, ip_task_type: str) -> str:
264
266
  """
265
267
  Return a task tag for the given task type.
266
268
 
@@ -275,7 +277,7 @@ class Tag:
275
277
  return cls.format_tag(StemName.task, ip_task_type)
276
278
 
277
279
  @classmethod
278
- def cs_step(cls, n: int):
280
+ def cs_step(cls, n: int) -> str:
279
281
  """
280
282
  Return a cs step tag for the given cs_step number.
281
283
 
@@ -291,7 +293,7 @@ class Tag:
291
293
  return cls.format_tag(StemName.cs_step, n)
292
294
 
293
295
  @classmethod
294
- def modstate(cls, n: int):
296
+ def modstate(cls, n: int) -> str:
295
297
  """
296
298
  Return a modstate tag for the given modstate number.
297
299
 
@@ -323,7 +325,7 @@ class Tag:
323
325
  return cls.format_tag(StemName.stokes, stokes_state)
324
326
 
325
327
  @classmethod
326
- def dsps_repeat(cls, dsps_repeat_number: int):
328
+ def dsps_repeat(cls, dsps_repeat_number: int) -> str:
327
329
  """
328
330
  Return a dsps repeat tag for the given dsps_repeat number.
329
331
 
@@ -428,18 +430,18 @@ class Tag:
428
430
  return cls.format_tag(StemName.workflow_task, class_name)
429
431
 
430
432
  @classmethod
431
- def dataset_inventory(cls):
433
+ def dataset_inventory(cls) -> str:
432
434
  """
433
- Return an dataset_inventory tag.
435
+ Return a dataset_inventory tag.
434
436
 
435
437
  Returns
436
438
  -------
437
- An dataset_inventory tag
439
+ A dataset_inventory tag
438
440
  """
439
441
  return cls.format_tag(StemName.dataset_inventory)
440
442
 
441
443
  @classmethod
442
- def asdf(cls):
444
+ def asdf(cls) -> str:
443
445
  """
444
446
  Return an asdf tag.
445
447
 
@@ -1,8 +1,9 @@
1
1
  """Controlled list of common IP task tag names."""
2
- from enum import Enum
3
2
 
3
+ from enum import StrEnum
4
4
 
5
- class TaskName(str, Enum):
5
+
6
+ class TaskName(StrEnum):
6
7
  """Controlled list of task tag names."""
7
8
 
8
9
  observe = "OBSERVE"
@@ -0,0 +1,28 @@
1
+ """Models to support telemetry data."""
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class ObservableProgress(BaseModel, validate_assignment=True):
7
+ """Container for tracking progress for a metering instrument e.g. task progress."""
8
+
9
+ current: int = 0
10
+ total: int = 0
11
+
12
+ def increment(self, step: int = 1) -> None:
13
+ """Increment the current progress by the given step."""
14
+ self.current += step
15
+
16
+ @property
17
+ def percent_complete(self) -> float:
18
+ """Return the percent complete as a float between 0 and 100."""
19
+ if self.total > 0:
20
+ return (self.current / self.total) * 100
21
+ return 0.0
22
+
23
+ def set_complete(self):
24
+ """Set the current progress to the total."""
25
+ if self.total == 0:
26
+ self.total = self.current = 1
27
+ else:
28
+ self.current = self.total
@@ -1,10 +1,11 @@
1
1
  """Support classes for manipulating wavelengths."""
2
+
2
3
  import astropy.units as u
3
4
  from pydantic import BaseModel
4
5
  from pydantic import ConfigDict
6
+ from pydantic import ValidationInfo
5
7
  from pydantic import field_validator
6
8
  from pydantic import model_validator
7
- from pydantic import ValidationInfo
8
9
 
9
10
 
10
11
  class WavelengthRange(BaseModel):
@@ -26,3 +27,4 @@ class WavelengthRange(BaseModel):
26
27
  """Validate that the max wavelength is greater than the min wavelength."""
27
28
  if self.min > self.max:
28
29
  raise ValueError("min is greater than max. Values may be reversed.")
30
+ return self
@@ -0,0 +1,46 @@
1
+ """Pre-made flower that reads a single header key from all files with specific task types and returns the average."""
2
+
3
+ from enum import StrEnum
4
+ from statistics import mean
5
+ from typing import Callable
6
+ from typing import Hashable
7
+
8
+ import numpy as np
9
+
10
+ from dkist_processing_common.parsers.near_bud import TaskNearFloatBud
11
+ from dkist_processing_common.parsers.task import passthrough_header_ip_task
12
+
13
+
14
+ class TaskAverageBud(TaskNearFloatBud):
15
+ """
16
+ Pre-made bud that returns the average of a single header key from all files with specific task types.
17
+
18
+ Parameters
19
+ ----------
20
+ constant_name
21
+ The name for the constant to be defined
22
+
23
+ metadata_key
24
+ The metadata key associated with the constant
25
+
26
+ ip_task_types
27
+ Only consider objects whose parsed header IP task type matches a string in this list
28
+
29
+ task_type_parsing_function
30
+ The function used to convert a header into an IP task type
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ constant_name: str,
36
+ metadata_key: str | StrEnum,
37
+ ip_task_types: str | list[str],
38
+ task_type_parsing_function: Callable = passthrough_header_ip_task,
39
+ ):
40
+ super().__init__(
41
+ constant_name=constant_name,
42
+ metadata_key=metadata_key,
43
+ ip_task_types=ip_task_types,
44
+ tolerance=np.inf,
45
+ task_type_parsing_function=task_type_parsing_function,
46
+ )
@@ -1,4 +1,5 @@
1
1
  """Classes supporting Calibration Sequence steps."""
2
+
2
3
  from __future__ import annotations
3
4
 
4
5
  from datetime import datetime
@@ -6,6 +7,7 @@ from datetime import timezone
6
7
  from typing import Type
7
8
 
8
9
  from dkist_processing_common.models.constants import BudName
10
+ from dkist_processing_common.models.flower_pot import SetStem
9
11
  from dkist_processing_common.models.flower_pot import SpilledDirt
10
12
  from dkist_processing_common.models.flower_pot import Stem
11
13
  from dkist_processing_common.models.tags import StemName
@@ -100,8 +102,9 @@ class CSStepFlower(Stem):
100
102
  """
101
103
 
102
104
  def __init__(self, max_cs_step_time_sec: float):
103
- super().__init__(stem_name=StemName.cs_step.value)
105
+ super().__init__(stem_name=StemName.cs_step)
104
106
  self.max_cs_step_time_sec = max_cs_step_time_sec
107
+ self.CS_step_set = set()
105
108
 
106
109
  def setter(self, fits_obj: L0FitsAccess) -> CSStep | Type[SpilledDirt]:
107
110
  """
@@ -118,7 +121,10 @@ class CSStepFlower(Stem):
118
121
  """
119
122
  if fits_obj.ip_task_type != "polcal":
120
123
  return SpilledDirt
121
- return CSStep(fits_obj, max_cs_time_sec=self.max_cs_step_time_sec)
124
+
125
+ cs_step = CSStep(fits_obj, max_cs_time_sec=self.max_cs_step_time_sec)
126
+ self.CS_step_set.add(cs_step)
127
+ return cs_step
122
128
 
123
129
  def getter(self, key) -> str | float | int:
124
130
  """
@@ -132,11 +138,11 @@ class CSStepFlower(Stem):
132
138
  -------
133
139
  The cs step for the given key
134
140
  """
135
- unique_steps = sorted(list(set(self.key_to_petal_dict.values())))
141
+ unique_steps = sorted(self.CS_step_set)
136
142
  return unique_steps.index(self.key_to_petal_dict[key])
137
143
 
138
144
 
139
- class NumCSStepBud(Stem):
145
+ class NumCSStepBud(SetStem):
140
146
  """
141
147
  The total number of CS Steps present in a dataset.
142
148
 
@@ -147,7 +153,7 @@ class NumCSStepBud(Stem):
147
153
  """
148
154
 
149
155
  def __init__(self, max_cs_step_time_sec: float):
150
- super().__init__(stem_name=BudName.num_cs_steps.value)
156
+ super().__init__(stem_name=BudName.num_cs_steps)
151
157
  self.max_cs_step_time_sec = max_cs_step_time_sec
152
158
 
153
159
  def setter(self, fits_obj: L0FitsAccess) -> CSStep | Type[SpilledDirt]:
@@ -167,17 +173,12 @@ class NumCSStepBud(Stem):
167
173
  return SpilledDirt
168
174
  return CSStep(fits_obj, max_cs_time_sec=self.max_cs_step_time_sec)
169
175
 
170
- def getter(self, key) -> int:
176
+ def getter(self) -> int:
171
177
  """
172
178
  Return the number of CS Steps present.
173
179
 
174
- Parameters
175
- ----------
176
- key
177
- The input key
178
180
  Returns
179
181
  -------
180
182
  The number of cs steps associated with the key
181
183
  """
182
- value_set = set(self.key_to_petal_dict.values())
183
- return len(value_set)
184
+ return len(self.value_set)
@@ -1,5 +1,7 @@
1
1
  """Classes supporting the Data Set Parameters Set (DSPS) Repeat parameter."""
2
+
2
3
  from dkist_processing_common.models.constants import BudName
4
+ from dkist_processing_common.models.fits_access import MetadataKey
3
5
  from dkist_processing_common.models.flower_pot import SpilledDirt
4
6
  from dkist_processing_common.models.tags import StemName
5
7
  from dkist_processing_common.models.task_name import TaskName
@@ -15,9 +17,9 @@ class TotalDspsRepeatsBud(TaskUniqueBud):
15
17
 
16
18
  def __init__(self):
17
19
  super().__init__(
18
- constant_name=BudName.num_dsps_repeats.value,
19
- metadata_key="num_dsps_repeats",
20
- ip_task_type=TaskName.observe.value,
20
+ constant_name=BudName.num_dsps_repeats,
21
+ metadata_key=MetadataKey.num_dsps_repeats,
22
+ ip_task_types=TaskName.observe,
21
23
  )
22
24
 
23
25
 
@@ -26,7 +28,7 @@ class DspsRepeatNumberFlower(SingleValueSingleKeyFlower):
26
28
 
27
29
  def __init__(self):
28
30
  super().__init__(
29
- tag_stem_name=StemName.dsps_repeat.value, metadata_key="current_dsps_repeat"
31
+ tag_stem_name=StemName.dsps_repeat, metadata_key=MetadataKey.current_dsps_repeat
30
32
  )
31
33
 
32
34
  def setter(self, fits_obj: L0FitsAccess):