dkist-processing-common 12.0.0rc7__py3-none-any.whl → 12.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.
@@ -31,15 +31,30 @@ def fits_hdulist_encoder(hdu_list: fits.HDUList) -> bytes:
31
31
  return iobase_encoder(file_obj)
32
32
 
33
33
 
34
- def fits_hdu_decoder(path: Path, hdu: int | None = None) -> fits.PrimaryHDU | fits.CompImageHDU:
34
+ def fits_hdu_decoder(
35
+ path: Path,
36
+ hdu: int | None = None,
37
+ checksum: bool = True,
38
+ disable_image_compression: bool = False,
39
+ ) -> fits.PrimaryHDU | fits.CompImageHDU:
35
40
  """Read a Path with `fits` to produce an `HDUList`."""
36
- hdu_list = fits.open(path, checksum=True)
41
+ hdu_list = fits.open(
42
+ path, checksum=checksum, disable_image_compression=disable_image_compression
43
+ )
37
44
  return _extract_hdu(hdu_list, hdu)
38
45
 
39
46
 
40
- def fits_array_decoder(path: Path, hdu: int | None = None, auto_squeeze: bool = True) -> np.ndarray:
47
+ def fits_array_decoder(
48
+ path: Path,
49
+ hdu: int | None = None,
50
+ auto_squeeze: bool = True,
51
+ checksum: bool = True,
52
+ disable_image_compression: bool = False,
53
+ ) -> np.ndarray:
41
54
  """Read a Path with `fits` and return the `.data` property."""
42
- hdu = fits_hdu_decoder(path, hdu=hdu)
55
+ hdu = fits_hdu_decoder(
56
+ path, hdu=hdu, checksum=checksum, disable_image_compression=disable_image_compression
57
+ )
43
58
  data = hdu.data
44
59
 
45
60
  # This conditional is explicitly to catch summit data with a dummy first axis for WCS
@@ -50,10 +65,16 @@ def fits_array_decoder(path: Path, hdu: int | None = None, auto_squeeze: bool =
50
65
 
51
66
 
52
67
  def fits_access_decoder(
53
- path: Path, fits_access_class: Type[FitsAccessBase], **fits_access_kwargs
68
+ path: Path,
69
+ fits_access_class: Type[FitsAccessBase],
70
+ checksum: bool = True,
71
+ disable_image_compression: bool = False,
72
+ **fits_access_kwargs,
54
73
  ) -> FitsAccessBase:
55
74
  """Read a Path with `fits` and ingest into a `FitsAccessBase`-type object."""
56
- hdu = fits_hdu_decoder(path)
75
+ hdu = fits_hdu_decoder(
76
+ path, checksum=checksum, disable_image_compression=disable_image_compression
77
+ )
57
78
  return fits_access_class(hdu=hdu, name=str(path), **fits_access_kwargs)
58
79
 
59
80
 
@@ -333,8 +333,8 @@ class ConstantsBase:
333
333
  def dark_num_raw_frames_per_fpa(self) -> dict[float, list]:
334
334
  """Return the dictionary of exposure times to number of raw frames per fpa."""
335
335
  raw_return = self._db_dict[BudName.dark_num_raw_frames_per_fpa]
336
- # convert nested lists to dictionary
337
- return {k: v for k, v in raw_return}
336
+ # convert JSON string keys back to float
337
+ return {float(k): v for k, v in raw_return.items()}
338
338
 
339
339
  @property
340
340
  def solar_gain_num_raw_frames_per_fpa(self) -> int:
@@ -1,10 +1,14 @@
1
1
  """
2
2
  Framework for grouping multiple keys and values with arbitrary logic.
3
3
 
4
- Defines:
5
- Stem -> ABC for groupings that depend on both the key and (maybe) value. Subgroups (Petals) are implied but not enforced.
4
+ The key components are:
5
+ **Stem:** ABC for groupings that depend on both the key and (maybe) value. Subgroups (Petals) are implied but not enforced.
6
6
 
7
- FlowerPot -> Container for Stem children (Flowers)
7
+ **ListStem:** ABC for groups that depend on value only. More limited, but faster than `Stem` for cases where the keys don't matter.
8
+
9
+ **SetStem:** ABC for groups that depend on value only and the values are well represented by a `set`. Even more limited, but faster than `Stem` for cases where the keys don't matter.
10
+
11
+ **FlowerPot:** Container for Stem children (Flowers)
8
12
  """
9
13
 
10
14
  from __future__ import annotations
@@ -17,7 +21,7 @@ from typing import Any
17
21
 
18
22
 
19
23
  class FlowerPot:
20
- """Base class to hold multiple sets (stems) of key, value pairs."""
24
+ """Base class to hold multiple sets (Stems) of key, value pairs."""
21
25
 
22
26
  def __init__(self):
23
27
  self.stems: list[Stem] = list()
@@ -55,9 +59,9 @@ class FlowerPot:
55
59
 
56
60
  class SpilledDirt:
57
61
  """
58
- A custom class for when a Flower wants the FlowerPot to skip that particular key/value.
62
+ A custom class for when a Stem wants the FlowerPot to skip that particular key/value.
59
63
 
60
- Exists because None, False, [], (), etc. etc. are all valid Flower return values
64
+ Exists because None, False, [], (), etc. etc. are all valid Stem return values
61
65
  """
62
66
 
63
67
 
@@ -90,7 +94,7 @@ class Petal:
90
94
 
91
95
  class Stem(ABC):
92
96
  """
93
- Base group for grouping keys via arbitrary logic on the total collection of keys and values.
97
+ Base class for grouping keys via arbitrary logic on the total collection of keys and values.
94
98
 
95
99
  Parameters
96
100
  ----------
@@ -133,6 +137,15 @@ class Stem(ABC):
133
137
 
134
138
  return self._petal_cache
135
139
 
140
+ @property
141
+ def can_be_picked(self) -> bool:
142
+ """
143
+ Return True if there are any values to be picked.
144
+
145
+ A `Stem` could have no values even after dirt is added if all of the results were `SpilledDirt`.
146
+ """
147
+ return len(self.petals) > 0
148
+
136
149
  def _generate_petal_list(self) -> None:
137
150
  """
138
151
  Generate a list of petals.
@@ -146,7 +159,7 @@ class Stem(ABC):
146
159
  changes, `key_to_petal_dict`, is unhashable.
147
160
  """
148
161
  petal_to_key_dict = defaultdict(list)
149
- for key, petal in self.key_to_petal_dict.items():
162
+ for key in self.key_to_petal_dict.keys():
150
163
  petal = self.getter(key)
151
164
  petal_to_key_dict[petal].append(key)
152
165
 
@@ -180,7 +193,7 @@ class Stem(ABC):
180
193
  @abstractmethod
181
194
  def getter(self, key: Hashable) -> Hashable:
182
195
  """
183
- Logic to apply to all ingested values when picking the Flower.
196
+ Logic to apply to all ingested values when picking the Stem.
184
197
 
185
198
  Implemented in derived class.
186
199
 
@@ -193,3 +206,211 @@ class Stem(ABC):
193
206
  The value
194
207
  """
195
208
  pass
209
+
210
+
211
+ class ListStem(ABC):
212
+ """
213
+ Base class for collecting and applying logic to values in a `list` with a `Stem`-like interface.
214
+
215
+ Unlike the full `Stem`, this class does NOT retain information about the keys and thus does no grouping of keys based
216
+ on values. The direct consequence of this is that the `.petals` property is undefined and will raise an ``AttributeError``
217
+ if accessed. This also means there is no need to invert the `key_to_petal_dict` (because it doesn't exist), which,
218
+ in turn, means there is no need to run the `getter` for every key. The result is that the `bud` property only needs
219
+ one call to `getter`. Thus, the calculation of a single value derived from all values (i.e., `bud`) is much faster
220
+ than using a full `Stem`.
221
+
222
+ Parameters
223
+ ----------
224
+ stem_name
225
+ The name to be associated with the stem
226
+ """
227
+
228
+ def __init__(self, stem_name: Any):
229
+ self.stem_name = stem_name
230
+ self.value_list: list = list()
231
+ self._need_to_compute_bud_value: bool = True
232
+
233
+ def update(self, key: Any, value: Any) -> None:
234
+ """
235
+ Ingest a single key/value pair. Note that the ``key`` is not used.
236
+
237
+ Parameters
238
+ ----------
239
+ key
240
+ The key (unused)
241
+
242
+ value
243
+ The value
244
+
245
+ Returns
246
+ -------
247
+ None
248
+ """
249
+ result = self.setter(value)
250
+ if result is not SpilledDirt:
251
+ self.value_list.append(result)
252
+ self._need_to_compute_bud_value = True
253
+
254
+ @property
255
+ def petals(self) -> None:
256
+ """Raise an error because `ListStem` does not retain key information and therefore cannot group keys."""
257
+ raise AttributeError(
258
+ f"{self.__class__.__name__} subclasses ListStem and therefore does not define the `petals` property"
259
+ )
260
+
261
+ @property
262
+ def can_be_picked(self) -> bool:
263
+ """
264
+ Return True if there are any values to be picked.
265
+
266
+ A `Stem` could have no values even after dirt is added if all of the results were `SpilledDirt`.
267
+ """
268
+ return len(self.value_list) > 0
269
+
270
+ @property
271
+ def bud(self) -> Petal:
272
+ """Return the result of `getter` packaged in a `Petal` object."""
273
+ if self._need_to_compute_bud_value:
274
+ self._value_cache = self.getter()
275
+ self._need_to_compute_bud_value = False
276
+
277
+ return Petal((self._value_cache, "LISTSTEM_NOT_USED"))
278
+
279
+ @abstractmethod
280
+ def setter(self, value: Any) -> Any:
281
+ """
282
+ Logic to apply to a single value pair on ingest.
283
+
284
+ Implemented in derived class.
285
+
286
+ Parameters
287
+ ----------
288
+ value
289
+ The value to be added
290
+
291
+ Returns
292
+ -------
293
+ Any
294
+ """
295
+ pass
296
+
297
+ @abstractmethod
298
+ def getter(self) -> Any:
299
+ """
300
+ Logic to apply to all ingested values when computing the `bud`.
301
+
302
+ Implemented in derived class.
303
+
304
+ Returns
305
+ -------
306
+ The value of the bud
307
+ """
308
+ pass
309
+
310
+
311
+ class SetStem(ABC):
312
+ """
313
+ Base class for collecting and applying logic to values in a `set` with a `Stem`-like interface.
314
+
315
+ Unlike the full `Stem`, this class does NOT retain information about the keys and thus does no grouping of keys based
316
+ on values. The direct consequence of this is that the `.petals` property is undefined and will raise an ``AttributeError``
317
+ if accessed. This also means there is no need to invert the `key_to_petal_dict` (because it doesn't exist), which,
318
+ in turn, means there is no need to run the `getter` for every key. The result is that the `bud` property only needs
319
+ one call to `getter`. Combined with the efficiency of storing values in a `set`, the calculation of a single value
320
+ derived from all values (i.e., `bud`) is much faster than using a full `Stem`.
321
+
322
+ .. Note::
323
+ The use of a `set` as the underlying storage mechanism means information regarding how many times a particular value
324
+ is present will be lost. It also means the return type of `setter` must be hashable. Both of these constraints can
325
+ be avoided by using `ListStem`, which still gets a significant speedup over `Stem` by dropping key information.
326
+
327
+
328
+ Parameters
329
+ ----------
330
+ stem_name
331
+ The name to be associated with the stem
332
+ """
333
+
334
+ def __init__(self, stem_name: Any):
335
+ self.stem_name = stem_name
336
+ self.value_set: set = set()
337
+ self._need_to_compute_bud_value: bool = True
338
+
339
+ def update(self, key: Any, value: Any) -> None:
340
+ """
341
+ Ingest a single key/value pair. Note that the ``key`` is not used.
342
+
343
+ Parameters
344
+ ----------
345
+ key
346
+ The key (unused)
347
+
348
+ value
349
+ The value
350
+
351
+ Returns
352
+ -------
353
+ None
354
+ """
355
+ result = self.setter(value)
356
+ if result is not SpilledDirt:
357
+ self.value_set.add(result)
358
+ self._need_to_compute_bud_value = True
359
+
360
+ @property
361
+ def petals(self) -> None:
362
+ """Raise an error because `SetStem` does not retain key information and therefore cannot group keys."""
363
+ raise AttributeError(
364
+ f"{self.__class__.__name__} subclasses SetStem and therefore does not define the `petals` property"
365
+ )
366
+
367
+ @property
368
+ def can_be_picked(self) -> bool:
369
+ """
370
+ Return True if there are any values to be picked.
371
+
372
+ A `Stem` could have no values even after dirt is added if all of the results were `SpilledDirt`.
373
+ """
374
+ return len(self.value_set) > 0
375
+
376
+ @property
377
+ def bud(self) -> Petal:
378
+ """Return the result of `getter` packaged in a `Petal` object."""
379
+ if self._need_to_compute_bud_value:
380
+ self._value_cache = self.getter()
381
+ self._need_to_compute_bud_value = False
382
+
383
+ return Petal((self._value_cache, "SETSTEM_NOT_USED"))
384
+
385
+ @abstractmethod
386
+ def setter(self, value: Any) -> Hashable:
387
+ """
388
+ Logic to apply to a single value pair on ingest.
389
+
390
+ Must return a Hashable object because the result will be stored in a `set`.
391
+
392
+ Implemented in derived class.
393
+
394
+ Parameters
395
+ ----------
396
+ value
397
+ The value to be added
398
+
399
+ Returns
400
+ -------
401
+ Any
402
+ """
403
+ pass
404
+
405
+ @abstractmethod
406
+ def getter(self) -> Any:
407
+ """
408
+ Logic to apply to all ingested values when computing the `bud`.
409
+
410
+ Implemented in derived class.
411
+
412
+ Returns
413
+ -------
414
+ The value of the bud
415
+ """
416
+ pass
@@ -30,8 +30,6 @@ class TaskAverageBud(TaskNearFloatBud):
30
30
  The function used to convert a header into an IP task type
31
31
  """
32
32
 
33
- key_to_petal_dict: dict[str, float]
34
-
35
33
  def __init__(
36
34
  self,
37
35
  constant_name: str,
@@ -7,6 +7,7 @@ from datetime import timezone
7
7
  from typing import Type
8
8
 
9
9
  from dkist_processing_common.models.constants import BudName
10
+ from dkist_processing_common.models.flower_pot import SetStem
10
11
  from dkist_processing_common.models.flower_pot import SpilledDirt
11
12
  from dkist_processing_common.models.flower_pot import Stem
12
13
  from dkist_processing_common.models.tags import StemName
@@ -103,6 +104,7 @@ class CSStepFlower(Stem):
103
104
  def __init__(self, max_cs_step_time_sec: float):
104
105
  super().__init__(stem_name=StemName.cs_step)
105
106
  self.max_cs_step_time_sec = max_cs_step_time_sec
107
+ self.CS_step_set = set()
106
108
 
107
109
  def setter(self, fits_obj: L0FitsAccess) -> CSStep | Type[SpilledDirt]:
108
110
  """
@@ -119,7 +121,10 @@ class CSStepFlower(Stem):
119
121
  """
120
122
  if fits_obj.ip_task_type != "polcal":
121
123
  return SpilledDirt
122
- 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
123
128
 
124
129
  def getter(self, key) -> str | float | int:
125
130
  """
@@ -133,11 +138,11 @@ class CSStepFlower(Stem):
133
138
  -------
134
139
  The cs step for the given key
135
140
  """
136
- unique_steps = sorted(list(set(self.key_to_petal_dict.values())))
141
+ unique_steps = sorted(self.CS_step_set)
137
142
  return unique_steps.index(self.key_to_petal_dict[key])
138
143
 
139
144
 
140
- class NumCSStepBud(Stem):
145
+ class NumCSStepBud(SetStem):
141
146
  """
142
147
  The total number of CS Steps present in a dataset.
143
148
 
@@ -168,17 +173,12 @@ class NumCSStepBud(Stem):
168
173
  return SpilledDirt
169
174
  return CSStep(fits_obj, max_cs_time_sec=self.max_cs_step_time_sec)
170
175
 
171
- def getter(self, key) -> int:
176
+ def getter(self) -> int:
172
177
  """
173
178
  Return the number of CS Steps present.
174
179
 
175
- Parameters
176
- ----------
177
- key
178
- The input key
179
180
  Returns
180
181
  -------
181
182
  The number of cs steps associated with the key
182
183
  """
183
- value_set = set(self.key_to_petal_dict.values())
184
- return len(value_set)
184
+ return len(self.value_set)
@@ -4,13 +4,13 @@ from enum import StrEnum
4
4
  from typing import Callable
5
5
  from typing import Type
6
6
 
7
+ from dkist_processing_common.models.flower_pot import SetStem
7
8
  from dkist_processing_common.models.flower_pot import SpilledDirt
8
- from dkist_processing_common.models.flower_pot import Stem
9
9
  from dkist_processing_common.parsers.l0_fits_access import L0FitsAccess
10
10
  from dkist_processing_common.parsers.task import passthrough_header_ip_task
11
11
 
12
12
 
13
- class ContributingIdsBud(Stem):
13
+ class ContributingIdsBud(SetStem):
14
14
  """Base class for contributing ID buds."""
15
15
 
16
16
  def __init__(self, constant_name: str, metadata_key: str | StrEnum):
@@ -33,20 +33,15 @@ class ContributingIdsBud(Stem):
33
33
  """
34
34
  return getattr(fits_obj, self.metadata_key)
35
35
 
36
- def getter(self, key) -> tuple[str, ...]:
36
+ def getter(self) -> tuple[str, ...]:
37
37
  """
38
38
  Get all ids seen for any type of frame.
39
39
 
40
- Parameters
41
- ----------
42
- key
43
- The input key
44
-
45
40
  Returns
46
41
  -------
47
42
  IDs from all types of frames
48
43
  """
49
- return tuple(set(self.key_to_petal_dict.values()))
44
+ return tuple(self.value_set)
50
45
 
51
46
 
52
47
  class TaskContributingIdsBud(ContributingIdsBud):
@@ -6,14 +6,14 @@ from typing import Any
6
6
  from typing import Callable
7
7
  from typing import DefaultDict
8
8
 
9
+ from dkist_processing_common.models.flower_pot import SetStem
9
10
  from dkist_processing_common.models.flower_pot import SpilledDirt
10
- from dkist_processing_common.models.flower_pot import Stem
11
11
  from dkist_processing_common.models.tags import EXP_TIME_ROUND_DIGITS
12
12
  from dkist_processing_common.parsers.l0_fits_access import L0FitsAccess
13
13
  from dkist_processing_common.parsers.task import passthrough_header_ip_task
14
14
 
15
15
 
16
- class TimeLookupBud(Stem):
16
+ class TimeLookupBud(SetStem):
17
17
  """
18
18
  Bud that reads two header keys from all files and creates a dictionary mapping a time KEY value to sets of a VALUE value.
19
19
 
@@ -64,20 +64,16 @@ class TimeLookupBud(Stem):
64
64
  self.mapping[rounded_key].add(value)
65
65
  return None
66
66
 
67
- def getter(self, key):
67
+ def getter(self):
68
68
  """
69
- Get the dictionary mapping created by the setter converted into hashable nested tuples.
69
+ Get the dictionary mapping created by the setter with values converted to JSON-able lists.
70
70
 
71
- Parameters
72
- ----------
73
- key
74
- The input key
75
71
  Returns
76
72
  -------
77
- The mapping dictionary converted into hashable nested tuples
73
+ The mapping dictionary with values converted to JSON-able lists
78
74
  """
79
- hashable_mapping = tuple((k, tuple(v)) for k, v in self.mapping.items())
80
- return hashable_mapping
75
+ mapping_lists = {k: list(v) for k, v in self.mapping.items()}
76
+ return mapping_lists
81
77
 
82
78
 
83
79
  class TaskTimeLookupBud(TimeLookupBud):
@@ -4,15 +4,15 @@ from enum import StrEnum
4
4
  from statistics import mean
5
5
  from typing import Callable
6
6
 
7
+ from dkist_processing_common.models.flower_pot import ListStem
7
8
  from dkist_processing_common.models.flower_pot import SpilledDirt
8
- from dkist_processing_common.models.flower_pot import Stem
9
9
  from dkist_processing_common.parsers.l0_fits_access import L0FitsAccess
10
10
  from dkist_processing_common.parsers.task import passthrough_header_ip_task
11
11
 
12
12
 
13
- class NearFloatBud(Stem):
13
+ class NearFloatBud(ListStem):
14
14
  """
15
- Pre-made flower that reads a single header key from all files and raises a ValueError if the values are not within a given tolerance.
15
+ Pre-made `ListStem` that reads a single header key from all files and raises a ValueError if the values are not within a given tolerance.
16
16
 
17
17
  This is intended for use with floats where the values may be slightly different, but should be the same.
18
18
 
@@ -54,26 +54,21 @@ class NearFloatBud(Stem):
54
54
  """
55
55
  return getattr(fits_obj, self.metadata_key)
56
56
 
57
- def getter(self, key):
57
+ def getter(self):
58
58
  """
59
59
  Get the value for this key and raise an error if the data spans more than the given tolerance.
60
60
 
61
- Parameters
62
- ----------
63
- key
64
- The input key
65
61
  Returns
66
62
  -------
67
63
  The mean value associated with this input key
68
64
  """
69
- value_list = list(self.key_to_petal_dict.values())
70
- biggest_value = max(value_list)
71
- smallest_value = min(value_list)
65
+ biggest_value = max(self.value_list)
66
+ smallest_value = min(self.value_list)
72
67
  if biggest_value - smallest_value > self.tolerance:
73
68
  raise ValueError(
74
69
  f"{self.stem_name} values are not close enough. Max: {biggest_value}, Min: {smallest_value}, Tolerance: {self.tolerance}"
75
70
  )
76
- return mean(value_list)
71
+ return mean(self.value_list)
77
72
 
78
73
 
79
74
  class TaskNearFloatBud(NearFloatBud):
@@ -2,7 +2,9 @@
2
2
 
3
3
  from dkist_processing_common.models.constants import BudName
4
4
  from dkist_processing_common.models.fits_access import MetadataKey
5
+ from dkist_processing_common.models.flower_pot import SpilledDirt
5
6
  from dkist_processing_common.models.task_name import TaskName
7
+ from dkist_processing_common.parsers.l0_fits_access import L0FitsAccess
6
8
  from dkist_processing_common.parsers.unique_bud import TaskUniqueBud
7
9
 
8
10
 
@@ -11,13 +13,9 @@ class RetarderNameBud(TaskUniqueBud):
11
13
  Bud for determining the name of the retarder used during a polcal Calibration Sequence (CS).
12
14
 
13
15
  This is *slightly* different than a simple `TaskUniqueBud` because we need to allow for CS steps when the retarder
14
- is out of the beam (i.g., "clear"). We do this by forcing the set of header values to be `{clear, RETARDER_NAME}`,
15
- where RETARDER_NAME is the value of this Bud.
16
+ is out of the beam (i.g., "clear"). We do this by returning `SpilledDirt` from the `setter` if the value is "clear".
16
17
  """
17
18
 
18
- # For type-hinting later
19
- key_to_petal_dict: dict[str, str]
20
-
21
19
  def __init__(self):
22
20
  super().__init__(
23
21
  constant_name=BudName.retarder_name,
@@ -25,12 +23,10 @@ class RetarderNameBud(TaskUniqueBud):
25
23
  ip_task_types=TaskName.polcal,
26
24
  )
27
25
 
28
- def getter(self, key) -> str:
29
- """Get the value for the retarder name and raise an Error if, ignoring "clear", that name is not unique."""
30
- value_set = set(self.key_to_petal_dict.values())
31
- value_set -= {"clear"}
32
- if len(value_set) > 1:
33
- raise ValueError(f"Multiple non-clear retarder names found. Names: {value_set}")
26
+ def setter(self, fits_obj: L0FitsAccess) -> type[SpilledDirt] | str:
27
+ """Drop the result if the retarder is out of the beam ("clear")."""
28
+ result = super().setter(fits_obj)
29
+ if result is not SpilledDirt and result.casefold() == "clear":
30
+ return SpilledDirt
34
31
 
35
- raw_retarder_name = value_set.pop()
36
- return raw_retarder_name
32
+ return result