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
@@ -2,6 +2,7 @@
2
2
 
3
3
  These classes should not be directly mixed in to anything. They are pre-mixed into the top-level QualityMixin
4
4
  """
5
+
5
6
  import copy
6
7
  import json
7
8
  import logging
@@ -12,14 +13,19 @@ from typing import Any
12
13
  from typing import Iterable
13
14
  from typing import Literal
14
15
 
16
+ import astropy.units as u
15
17
  import numpy as np
18
+ from astropy.wcs import WCS
16
19
  from dkist_processing_pac.fitter.fitter_parameters import CU_PARAMS
17
20
  from dkist_processing_pac.fitter.fitter_parameters import GLOBAL_PARAMS
18
21
  from dkist_processing_pac.fitter.fitter_parameters import TELESCOPE_PARAMS
19
22
  from dkist_processing_pac.fitter.fitting_core import compare_I
20
23
  from dkist_processing_pac.fitter.polcal_fitter import PolcalFitter
21
24
  from pandas import DataFrame
25
+ from solar_wavelength_calibration.fitter.wavelength_fitter import FitResult
22
26
 
27
+ from dkist_processing_common.codecs.json import json_decoder
28
+ from dkist_processing_common.models.fried_parameter import r0_valid
23
29
  from dkist_processing_common.models.metric_code import MetricCode
24
30
  from dkist_processing_common.models.quality import EfficiencyHistograms
25
31
  from dkist_processing_common.models.quality import ModulationMatrixHistograms
@@ -28,6 +34,7 @@ from dkist_processing_common.models.quality import PlotHistogram
28
34
  from dkist_processing_common.models.quality import PlotRaincloud
29
35
  from dkist_processing_common.models.quality import ReportMetric
30
36
  from dkist_processing_common.models.quality import SimpleTable
37
+ from dkist_processing_common.models.quality import VerticalMultiPanePlot2D
31
38
  from dkist_processing_common.models.tags import Tag
32
39
 
33
40
  logger = logging.getLogger(__name__)
@@ -203,7 +210,7 @@ class _SimplePlotQualityMixin:
203
210
  return warnings
204
211
 
205
212
  def quality_store_ao_status_and_fried_parameter(
206
- self, datetimes: list[str], values: list[list[bool, float]]
213
+ self, datetimes: list[str], values: list[list[bool | float]]
207
214
  ):
208
215
  """
209
216
  Collect and store datetime / value pairs for the boolean AO status and Fried parameter.
@@ -213,23 +220,27 @@ class _SimplePlotQualityMixin:
213
220
  Because of how L1Metric.has_metric works, empty lists will not be passed to this method.
214
221
  However, because of how L1Metric.store_metric works, one or both values can be None.
215
222
  """
216
- ao_values = [value[0] for value in values]
217
- ao_not_none = [ao for ao in ao_values if ao is not None]
223
+ ao_lock_values = [value[0] for value in values]
224
+ ao_not_none = [ao for ao in ao_lock_values if ao is not None]
218
225
  if len(ao_not_none) != 0:
219
226
  self._record_values(values=ao_not_none, tags=Tag.quality(MetricCode.ao_status))
220
227
  fried_values = [value[1] for value in values]
221
- fried_values_not_none_ao_locked = [
222
- r0 for ao, r0 in zip(ao_values, fried_values) if (ao is True) and (r0 is not None)
223
- ]
224
- datetimes_fried_not_none_ao_locked = [
225
- dt
226
- for dt, ao, r0 in zip(datetimes, ao_values, fried_values)
227
- if (ao is True) and (r0 is not None)
228
- ]
229
- if len(fried_values_not_none_ao_locked) != 0:
228
+ ao_oob_values = [value[2] for value in values]
229
+ fried_values_to_plot = []
230
+ datetimes_to_plot = []
231
+ # For each set of input data, check if the r0 is considered valid based on all data
232
+ for i in range(len(fried_values)):
233
+ if r0_valid(
234
+ r0=fried_values[i],
235
+ ao_lock=ao_lock_values[i],
236
+ num_out_of_bounds_ao_values=ao_oob_values[i],
237
+ ):
238
+ fried_values_to_plot.append(fried_values[i])
239
+ datetimes_to_plot.append(datetimes[i])
240
+ if len(fried_values_to_plot) != 0:
230
241
  self._record_2d_plot_values(
231
- x_values=datetimes_fried_not_none_ao_locked,
232
- y_values=fried_values_not_none_ao_locked,
242
+ x_values=datetimes_to_plot,
243
+ y_values=fried_values_to_plot,
233
244
  tags=Tag.quality(MetricCode.fried_parameter),
234
245
  )
235
246
 
@@ -1319,3 +1330,144 @@ class _PolcalQualityMixin:
1319
1330
  base_str += " bins."
1320
1331
 
1321
1332
  return base_str
1333
+
1334
+
1335
+ class _WavecalQualityMixin:
1336
+ """Mixin class supporting the recording and building of wavecal-related metrics."""
1337
+
1338
+ def quality_store_wavecal_results(
1339
+ self,
1340
+ *,
1341
+ input_wavelength: u.Quantity,
1342
+ input_spectrum: np.ndarray,
1343
+ fit_result: FitResult,
1344
+ weights: None | np.ndarray = None,
1345
+ ):
1346
+ """
1347
+ Store the results of a wavelength solution fit.
1348
+
1349
+ Namely, save the:
1350
+
1351
+ * Input spectrum and wavelength
1352
+ * Best-fit combined atlas spectrum
1353
+ * Best-fit wavelength vector
1354
+ * Fit residuals
1355
+
1356
+ Note that the residuals are the *unweighed* residuals.
1357
+ """
1358
+ weight_data = np.ones(input_wavelength.size) if weights is None else weights
1359
+ prepared_weights = fit_result.prepared_weights
1360
+ residuals = fit_result.minimizer_result.residual / prepared_weights
1361
+ residuals[~np.isfinite(residuals)] = 0.0
1362
+ normalized_residuals = residuals / input_spectrum
1363
+
1364
+ best_fit_atlas = fit_result.best_fit_atlas
1365
+ best_fit_wavelength = fit_result.best_fit_wavelength_vector
1366
+
1367
+ finite_idx = (
1368
+ np.isfinite(input_wavelength)
1369
+ * np.isfinite(input_spectrum)
1370
+ * np.isfinite(best_fit_wavelength)
1371
+ * np.isfinite(best_fit_atlas)
1372
+ * np.isfinite(normalized_residuals)
1373
+ * np.isfinite(weight_data)
1374
+ )
1375
+
1376
+ data = {
1377
+ "input_wavelength_nm": input_wavelength.to_value(u.nm)[finite_idx].tolist(),
1378
+ "input_spectrum": input_spectrum[finite_idx].tolist(),
1379
+ "best_fit_wavelength_nm": best_fit_wavelength[finite_idx].tolist(),
1380
+ "best_fit_atlas": best_fit_atlas[finite_idx].tolist(),
1381
+ "normalized_residuals": normalized_residuals[finite_idx].tolist(),
1382
+ "weights": None if weights is None else weight_data[finite_idx].tolist(),
1383
+ }
1384
+
1385
+ self._record_values(values=data, tags=[Tag.quality(MetricCode.wavecal_fit)])
1386
+
1387
+ def quality_build_wavecal_results(self) -> dict:
1388
+ """Build a ReportMetric containing a multi-pane plot showing the fit spectra and residuals."""
1389
+ data = next(self.read(tags=[Tag.quality(MetricCode.wavecal_fit)], decoder=json_decoder))
1390
+
1391
+ input_wave_list = data["input_wavelength_nm"]
1392
+ input_spectrum_list = data["input_spectrum"]
1393
+ best_fit_wave_list = data["best_fit_wavelength_nm"]
1394
+ best_fit_atlas_list = data["best_fit_atlas"]
1395
+ residuals_list = data["normalized_residuals"]
1396
+ weights = data["weights"]
1397
+
1398
+ fit_series = {
1399
+ "Best Fit Observations": [best_fit_wave_list, input_spectrum_list],
1400
+ "Input Spectrum": [input_wave_list, input_spectrum_list],
1401
+ "Best Fit Atlas": [best_fit_wave_list, best_fit_atlas_list],
1402
+ }
1403
+
1404
+ # Set the colors and zorder here manually because the JSON-ization of the quality data means we can't be sure of
1405
+ # the order these will be plotted in and thus can't rely on the default color-cycler in `dkist-quality`.
1406
+ fit_plot_kwargs = {
1407
+ "Best Fit Observations": {
1408
+ "ls": "-",
1409
+ "lw": 4,
1410
+ "alpha": 0.8,
1411
+ "ms": 0,
1412
+ "color": "#FAA61C",
1413
+ "zorder": 2,
1414
+ },
1415
+ "Input Spectrum": {"ls": "-", "alpha": 0.4, "ms": 0, "color": "#1E317A", "zorder": 2.1},
1416
+ "Best Fit Atlas": {"color": "k", "ls": "-", "ms": 0, "zorder": 2.2},
1417
+ }
1418
+
1419
+ fit_plot = Plot2D(
1420
+ xlabel="Wavelength [nm]",
1421
+ ylabel="Signal",
1422
+ series_data=fit_series,
1423
+ plot_kwargs=fit_plot_kwargs,
1424
+ sort_series=False,
1425
+ )
1426
+
1427
+ residuals_series = {"Residuals": [best_fit_wave_list, residuals_list]}
1428
+ residuals_plot_kwargs = {"Residuals": {"ls": "-", "color": "k", "ms": 0}}
1429
+
1430
+ y_min = np.nanpercentile(residuals_list, 2)
1431
+ y_max = np.nanpercentile(residuals_list, 98)
1432
+ y_range = y_max - y_min
1433
+ y_min -= 0.1 * y_range
1434
+ y_max += 0.1 * y_range
1435
+ residuals_plot = Plot2D(
1436
+ xlabel="Wavelength [nm]",
1437
+ ylabel=r"$\frac{\mathrm{Obs - Atlas}}{\mathrm{Obs}}$",
1438
+ series_data=residuals_series,
1439
+ plot_kwargs=residuals_plot_kwargs,
1440
+ ylim=(y_min, y_max),
1441
+ )
1442
+
1443
+ plot_list = [fit_plot, residuals_plot]
1444
+ height_ratios = [1.5, 1.0]
1445
+ if weights is not None:
1446
+ weight_series = {"Weights": [best_fit_wave_list, weights]}
1447
+ weight_plot_kwargs = {"Weights": {"ls": "-", "color": "k", "ms": 0}}
1448
+ weight_plot = Plot2D(
1449
+ xlabel="Wavelength [nm]",
1450
+ ylabel="Fit Weights",
1451
+ series_data=weight_series,
1452
+ plot_kwargs=weight_plot_kwargs,
1453
+ )
1454
+ plot_list.append(weight_plot)
1455
+ height_ratios.append(1.0)
1456
+
1457
+ full_plot = VerticalMultiPanePlot2D(
1458
+ top_to_bottom_plot_list=plot_list,
1459
+ match_x_axes=True,
1460
+ no_gap=True,
1461
+ top_to_bottom_height_ratios=height_ratios,
1462
+ )
1463
+
1464
+ metric = ReportMetric(
1465
+ name="Wavelength Calibration Results",
1466
+ description="These plots show the wavelength solution computed based on fits to a Solar FTS atlas. "
1467
+ "The top plot shows the input and best-fit spectra along with the best-fit atlas, which is "
1468
+ "a combination of Solar and Telluric spectra. The bottom plot shows the fit residuals.",
1469
+ metric_code=MetricCode.wavecal_fit,
1470
+ multi_plot_data=full_plot,
1471
+ )
1472
+
1473
+ return metric.model_dump()
@@ -1,4 +1,5 @@
1
1
  """Base class that supports common output data methods and paths."""
2
+
2
3
  import logging
3
4
  from abc import ABC
4
5
  from abc import abstractmethod
@@ -19,7 +20,7 @@ class OutputDataBase(WorkflowTaskBase, ABC):
19
20
  @cached_property
20
21
  def destination_bucket(self) -> str:
21
22
  """Get the destination bucket."""
22
- return self.metadata_store_recipe_run_configuration().get("destination_bucket", "data")
23
+ return self.metadata_store_recipe_run.configuration.destination_bucket
23
24
 
24
25
  def format_object_key(self, path: Path) -> str:
25
26
  """
@@ -57,10 +58,10 @@ class TransferDataBase(OutputDataBase, ObjectStoreMixin, ABC):
57
58
 
58
59
  def run(self) -> None:
59
60
  """Transfer the data and cleanup any folders."""
60
- with self.apm_task_step("Transfer objects"):
61
+ with self.telemetry_span("Transfer objects"):
61
62
  self.transfer_objects()
62
63
 
63
- with self.apm_task_step("Remove folder objects"):
64
+ with self.telemetry_span("Remove folder objects"):
64
65
  self.remove_folder_objects()
65
66
 
66
67
  @abstractmethod
@@ -20,7 +20,7 @@ that makes the rest of the pipeline easy to write.
20
20
  In other words, we can find exactly the frame we need (tags) and, once we have it, we never need to look
21
21
  at a different frame to get information (constants).
22
22
  """
23
- import json
23
+
24
24
  import logging
25
25
  from abc import ABC
26
26
  from abc import abstractmethod
@@ -28,24 +28,33 @@ from typing import TypeVar
28
28
 
29
29
  from dkist_processing_common.codecs.fits import fits_access_decoder
30
30
  from dkist_processing_common.models.constants import BudName
31
+ from dkist_processing_common.models.fits_access import MetadataKey
31
32
  from dkist_processing_common.models.flower_pot import FlowerPot
32
33
  from dkist_processing_common.models.flower_pot import Stem
33
34
  from dkist_processing_common.models.flower_pot import Thorn
34
35
  from dkist_processing_common.models.tags import Tag
36
+ from dkist_processing_common.models.task_name import TaskName
37
+ from dkist_processing_common.parsers.average_bud import TaskAverageBud
35
38
  from dkist_processing_common.parsers.experiment_id_bud import ContributingExperimentIdsBud
36
39
  from dkist_processing_common.parsers.experiment_id_bud import ExperimentIdBud
40
+ from dkist_processing_common.parsers.lookup_bud import TaskTimeLookupBud
41
+ from dkist_processing_common.parsers.observing_program_id_bud import (
42
+ TaskContributingObservingProgramExecutionIdsBud,
43
+ )
37
44
  from dkist_processing_common.parsers.proposal_id_bud import ContributingProposalIdsBud
38
45
  from dkist_processing_common.parsers.proposal_id_bud import ProposalIdBud
46
+ from dkist_processing_common.parsers.task import parse_header_ip_task_with_gains
39
47
  from dkist_processing_common.parsers.time import AverageCadenceBud
40
48
  from dkist_processing_common.parsers.time import MaximumCadenceBud
41
49
  from dkist_processing_common.parsers.time import MinimumCadenceBud
50
+ from dkist_processing_common.parsers.time import TaskDateBeginBud
42
51
  from dkist_processing_common.parsers.time import TaskExposureTimesBud
43
52
  from dkist_processing_common.parsers.time import TaskReadoutExpTimesBud
44
53
  from dkist_processing_common.parsers.time import VarianceCadenceBud
54
+ from dkist_processing_common.parsers.unique_bud import TaskUniqueBud
45
55
  from dkist_processing_common.parsers.unique_bud import UniqueBud
46
56
  from dkist_processing_common.tasks.base import WorkflowTaskBase
47
57
 
48
-
49
58
  __all__ = [
50
59
  "ParseL0InputDataBase",
51
60
  "ParseDataBase",
@@ -58,10 +67,254 @@ logger = logging.getLogger(__name__)
58
67
  S = TypeVar("S", bound=Stem)
59
68
 
60
69
 
70
+ def dataset_extra_bud_factory() -> list[S]:
71
+ """Provide constant buds for use in dataset extras."""
72
+ return [
73
+ UniqueBud(constant_name=BudName.camera_id, metadata_key=MetadataKey.camera_id),
74
+ UniqueBud(constant_name=BudName.camera_name, metadata_key=MetadataKey.camera_name),
75
+ UniqueBud(
76
+ constant_name=BudName.camera_bit_depth, metadata_key=MetadataKey.camera_bit_depth
77
+ ),
78
+ UniqueBud(
79
+ constant_name=BudName.hardware_binning_x, metadata_key=MetadataKey.hardware_binning_x
80
+ ),
81
+ UniqueBud(
82
+ constant_name=BudName.hardware_binning_y, metadata_key=MetadataKey.hardware_binning_x
83
+ ),
84
+ UniqueBud(
85
+ constant_name=BudName.software_binning_x, metadata_key=MetadataKey.software_binning_x
86
+ ),
87
+ UniqueBud(
88
+ constant_name=BudName.software_binning_y, metadata_key=MetadataKey.software_binning_y
89
+ ),
90
+ UniqueBud(
91
+ constant_name=BudName.hls_version,
92
+ metadata_key=MetadataKey.hls_version,
93
+ ),
94
+ TaskContributingObservingProgramExecutionIdsBud(
95
+ constant_name=BudName.dark_observing_program_execution_ids,
96
+ ip_task_types=TaskName.dark,
97
+ ),
98
+ TaskContributingObservingProgramExecutionIdsBud(
99
+ constant_name=BudName.solar_gain_observing_program_execution_ids,
100
+ ip_task_types=TaskName.solar_gain,
101
+ task_type_parsing_function=parse_header_ip_task_with_gains,
102
+ ),
103
+ TaskContributingObservingProgramExecutionIdsBud(
104
+ constant_name=BudName.polcal_observing_program_execution_ids,
105
+ ip_task_types=TaskName.polcal,
106
+ ),
107
+ TaskTimeLookupBud(
108
+ constant_name=BudName.dark_num_raw_frames_per_fpa,
109
+ key_metadata_key=MetadataKey.sensor_readout_exposure_time_ms,
110
+ value_metadata_key=MetadataKey.num_raw_frames_per_fpa,
111
+ ip_task_types=TaskName.dark,
112
+ ),
113
+ TaskUniqueBud(
114
+ constant_name=BudName.solar_gain_num_raw_frames_per_fpa,
115
+ metadata_key=MetadataKey.num_raw_frames_per_fpa,
116
+ ip_task_types=TaskName.solar_gain,
117
+ task_type_parsing_function=parse_header_ip_task_with_gains,
118
+ ),
119
+ TaskUniqueBud(
120
+ constant_name=BudName.polcal_num_raw_frames_per_fpa,
121
+ metadata_key=MetadataKey.num_raw_frames_per_fpa,
122
+ ip_task_types=TaskName.polcal,
123
+ ),
124
+ TaskUniqueBud(
125
+ constant_name=BudName.solar_gain_telescope_tracking_mode,
126
+ metadata_key=MetadataKey.telescope_tracking_mode,
127
+ ip_task_types=TaskName.solar_gain,
128
+ task_type_parsing_function=parse_header_ip_task_with_gains,
129
+ ),
130
+ TaskUniqueBud(
131
+ constant_name=BudName.polcal_telescope_tracking_mode,
132
+ metadata_key=MetadataKey.telescope_tracking_mode,
133
+ ip_task_types=TaskName.polcal,
134
+ ),
135
+ TaskUniqueBud(
136
+ constant_name=BudName.solar_gain_coude_table_tracking_mode,
137
+ metadata_key=MetadataKey.coude_table_tracking_mode,
138
+ ip_task_types=TaskName.solar_gain,
139
+ task_type_parsing_function=parse_header_ip_task_with_gains,
140
+ ),
141
+ TaskUniqueBud(
142
+ constant_name=BudName.polcal_coude_table_tracking_mode,
143
+ metadata_key=MetadataKey.coude_table_tracking_mode,
144
+ ip_task_types=TaskName.polcal,
145
+ ),
146
+ TaskUniqueBud(
147
+ constant_name=BudName.solar_gain_telescope_scanning_mode,
148
+ metadata_key=MetadataKey.telescope_scanning_mode,
149
+ ip_task_types=TaskName.solar_gain,
150
+ task_type_parsing_function=parse_header_ip_task_with_gains,
151
+ ),
152
+ TaskUniqueBud(
153
+ constant_name=BudName.polcal_telescope_scanning_mode,
154
+ metadata_key=MetadataKey.telescope_scanning_mode,
155
+ ip_task_types=TaskName.polcal,
156
+ ),
157
+ TaskUniqueBud(
158
+ constant_name=BudName.dark_gos_level3_status,
159
+ metadata_key=MetadataKey.gos_level3_status,
160
+ ip_task_types=TaskName.dark,
161
+ ),
162
+ TaskUniqueBud(
163
+ constant_name=BudName.solar_gain_gos_level3_status,
164
+ metadata_key=MetadataKey.gos_level3_status,
165
+ ip_task_types=TaskName.solar_gain,
166
+ task_type_parsing_function=parse_header_ip_task_with_gains,
167
+ ),
168
+ TaskUniqueBud(
169
+ constant_name=BudName.dark_gos_level3_lamp_status,
170
+ metadata_key=MetadataKey.gos_level3_lamp_status,
171
+ ip_task_types=TaskName.dark,
172
+ ),
173
+ TaskUniqueBud(
174
+ constant_name=BudName.solar_gain_gos_level3_lamp_status,
175
+ metadata_key=MetadataKey.gos_level3_lamp_status,
176
+ ip_task_types=TaskName.solar_gain,
177
+ task_type_parsing_function=parse_header_ip_task_with_gains,
178
+ ),
179
+ TaskUniqueBud(
180
+ constant_name=BudName.dark_gos_polarizer_status,
181
+ metadata_key=MetadataKey.gos_polarizer_status,
182
+ ip_task_types=TaskName.dark,
183
+ ),
184
+ TaskUniqueBud(
185
+ constant_name=BudName.solar_gain_gos_polarizer_status,
186
+ metadata_key=MetadataKey.gos_polarizer_status,
187
+ ip_task_types=TaskName.solar_gain,
188
+ task_type_parsing_function=parse_header_ip_task_with_gains,
189
+ ),
190
+ TaskUniqueBud(
191
+ constant_name=BudName.dark_gos_polarizer_angle,
192
+ metadata_key=MetadataKey.gos_polarizer_angle,
193
+ ip_task_types=TaskName.dark,
194
+ ),
195
+ TaskUniqueBud(
196
+ constant_name=BudName.solar_gain_gos_polarizer_angle,
197
+ metadata_key=MetadataKey.gos_polarizer_angle,
198
+ ip_task_types=TaskName.solar_gain,
199
+ task_type_parsing_function=parse_header_ip_task_with_gains,
200
+ ),
201
+ TaskUniqueBud(
202
+ constant_name=BudName.dark_gos_retarder_status,
203
+ metadata_key=MetadataKey.gos_retarder_status,
204
+ ip_task_types=TaskName.dark,
205
+ ),
206
+ TaskUniqueBud(
207
+ constant_name=BudName.solar_gain_gos_retarder_status,
208
+ metadata_key=MetadataKey.gos_retarder_status,
209
+ ip_task_types=TaskName.solar_gain,
210
+ task_type_parsing_function=parse_header_ip_task_with_gains,
211
+ ),
212
+ TaskUniqueBud(
213
+ constant_name=BudName.dark_gos_retarder_angle,
214
+ metadata_key=MetadataKey.gos_retarder_angle,
215
+ ip_task_types=TaskName.dark,
216
+ ),
217
+ TaskUniqueBud(
218
+ constant_name=BudName.solar_gain_gos_retarder_angle,
219
+ metadata_key=MetadataKey.gos_retarder_angle,
220
+ ip_task_types=TaskName.solar_gain,
221
+ task_type_parsing_function=parse_header_ip_task_with_gains,
222
+ ),
223
+ TaskUniqueBud(
224
+ constant_name=BudName.dark_gos_level0_status,
225
+ metadata_key=MetadataKey.gos_level0_status,
226
+ ip_task_types=TaskName.dark,
227
+ ),
228
+ TaskUniqueBud(
229
+ constant_name=BudName.solar_gain_gos_level0_status,
230
+ metadata_key=MetadataKey.gos_level0_status,
231
+ ip_task_types=TaskName.solar_gain,
232
+ task_type_parsing_function=parse_header_ip_task_with_gains,
233
+ ),
234
+ TaskAverageBud(
235
+ constant_name=BudName.dark_average_light_level,
236
+ metadata_key=MetadataKey.light_level,
237
+ ip_task_types=TaskName.dark,
238
+ ),
239
+ TaskAverageBud(
240
+ constant_name=BudName.solar_gain_average_light_level,
241
+ metadata_key=MetadataKey.light_level,
242
+ ip_task_types=TaskName.solar_gain,
243
+ task_type_parsing_function=parse_header_ip_task_with_gains,
244
+ ),
245
+ TaskAverageBud(
246
+ constant_name=BudName.polcal_average_light_level,
247
+ metadata_key=MetadataKey.light_level,
248
+ ip_task_types=TaskName.polcal,
249
+ ),
250
+ TaskAverageBud(
251
+ constant_name=BudName.dark_average_telescope_elevation,
252
+ metadata_key=MetadataKey.elevation,
253
+ ip_task_types=TaskName.dark,
254
+ ),
255
+ TaskAverageBud(
256
+ constant_name=BudName.solar_gain_average_telescope_elevation,
257
+ metadata_key=MetadataKey.elevation,
258
+ ip_task_types=TaskName.solar_gain,
259
+ task_type_parsing_function=parse_header_ip_task_with_gains,
260
+ ),
261
+ TaskAverageBud(
262
+ constant_name=BudName.polcal_average_telescope_elevation,
263
+ metadata_key=MetadataKey.elevation,
264
+ ip_task_types=TaskName.polcal,
265
+ ),
266
+ TaskAverageBud(
267
+ constant_name=BudName.dark_average_coude_table_angle,
268
+ metadata_key=MetadataKey.table_angle,
269
+ ip_task_types=TaskName.dark,
270
+ ),
271
+ TaskAverageBud(
272
+ constant_name=BudName.solar_gain_average_coude_table_angle,
273
+ metadata_key=MetadataKey.table_angle,
274
+ ip_task_types=TaskName.solar_gain,
275
+ task_type_parsing_function=parse_header_ip_task_with_gains,
276
+ ),
277
+ TaskAverageBud(
278
+ constant_name=BudName.polcal_average_coude_table_angle,
279
+ metadata_key=MetadataKey.table_angle,
280
+ ip_task_types=TaskName.polcal,
281
+ ),
282
+ TaskAverageBud(
283
+ constant_name=BudName.dark_average_telescope_azimuth,
284
+ metadata_key=MetadataKey.azimuth,
285
+ ip_task_types=TaskName.dark,
286
+ ),
287
+ TaskAverageBud(
288
+ constant_name=BudName.solar_gain_average_telescope_azimuth,
289
+ metadata_key=MetadataKey.azimuth,
290
+ ip_task_types=TaskName.solar_gain,
291
+ task_type_parsing_function=parse_header_ip_task_with_gains,
292
+ ),
293
+ TaskAverageBud(
294
+ constant_name=BudName.polcal_average_telescope_azimuth,
295
+ metadata_key=MetadataKey.azimuth,
296
+ ip_task_types=TaskName.polcal,
297
+ ),
298
+ TaskDateBeginBud(
299
+ constant_name=BudName.dark_date_begin,
300
+ ip_task_types=TaskName.dark,
301
+ ),
302
+ TaskDateBeginBud(
303
+ constant_name=BudName.solar_gain_date_begin,
304
+ ip_task_types=TaskName.solar_gain,
305
+ task_type_parsing_function=parse_header_ip_task_with_gains,
306
+ ),
307
+ TaskDateBeginBud(
308
+ constant_name=BudName.polcal_date_begin,
309
+ ip_task_types=TaskName.polcal,
310
+ ),
311
+ ]
312
+
313
+
61
314
  def default_constant_bud_factory() -> list[S]:
62
315
  """Provide default constant buds for use in common parsing tasks."""
63
- return [
64
- UniqueBud(constant_name=BudName.instrument.value, metadata_key="instrument"),
316
+ return dataset_extra_bud_factory() + [
317
+ UniqueBud(constant_name=BudName.instrument, metadata_key=MetadataKey.instrument),
65
318
  ProposalIdBud(),
66
319
  ContributingProposalIdsBud(),
67
320
  ExperimentIdBud(),
@@ -70,8 +323,10 @@ def default_constant_bud_factory() -> list[S]:
70
323
  MaximumCadenceBud(),
71
324
  MinimumCadenceBud(),
72
325
  VarianceCadenceBud(),
73
- TaskExposureTimesBud(stem_name=BudName.dark_exposure_times.value, ip_task_type="dark"),
74
- TaskReadoutExpTimesBud(stem_name=BudName.dark_readout_exp_times.value, ip_task_type="dark"),
326
+ TaskExposureTimesBud(stem_name=BudName.dark_exposure_times, ip_task_types=TaskName.dark),
327
+ TaskReadoutExpTimesBud(
328
+ stem_name=BudName.dark_readout_exp_times, ip_task_types=TaskName.dark
329
+ ),
75
330
  ]
76
331
 
77
332
 
@@ -100,21 +355,25 @@ class ParseDataBase(WorkflowTaskBase, ABC):
100
355
 
101
356
  @property
102
357
  @abstractmethod
103
- def tags_for_input_frames(self) -> list[Tag]:
358
+ def tags_for_input_frames(self) -> list[str]:
104
359
  """Define the tags for the data that will be parsed."""
105
360
 
361
+ def pre_run(self) -> None:
362
+ """Execute pre-task setup."""
363
+ self.outer_loop_progress.total = self.scratch.count_all(tags=self.tags_for_input_frames)
364
+
106
365
  def run(self) -> None:
107
366
  """Run method for this task."""
108
- with self.apm_task_step("Check that input frames exist"):
367
+ with self.telemetry_span("Check that input frames exist"):
109
368
  self.check_input_frames()
110
369
 
111
- with self.apm_task_step("Ingest all input files"):
370
+ with self.telemetry_span("Ingest all input files"):
112
371
  tag_pot, constant_pot = self.make_flower_pots()
113
372
 
114
- with self.apm_task_step("Update constants"):
373
+ with self.telemetry_span("Update constants"):
115
374
  self.update_constants(constant_pot)
116
375
 
117
- with self.apm_task_step("Tag files"):
376
+ with self.telemetry_span("Tag files"):
118
377
  self.tag_petals(tag_pot)
119
378
 
120
379
  def make_flower_pots(self) -> tuple[FlowerPot, FlowerPot]:
@@ -125,6 +384,7 @@ class ParseDataBase(WorkflowTaskBase, ABC):
125
384
  constant_pot.stems += self.constant_buds
126
385
 
127
386
  for fits_obj in self.input_frames:
387
+ self.outer_loop_progress.increment()
128
388
  filepath = fits_obj.name
129
389
  tag_pot.add_dirt(filepath, fits_obj)
130
390
  constant_pot.add_dirt(filepath, fits_obj)
@@ -137,6 +397,8 @@ class ParseDataBase(WorkflowTaskBase, ABC):
137
397
  return self.read(
138
398
  tags=self.tags_for_input_frames,
139
399
  decoder=fits_access_decoder,
400
+ checksum=False,
401
+ disable_image_compression=True,
140
402
  fits_access_class=self.fits_parsing_class,
141
403
  )
142
404
 
@@ -158,9 +420,9 @@ class ParseDataBase(WorkflowTaskBase, ABC):
158
420
  None
159
421
  """
160
422
  for stem in constant_pot:
161
- with self.apm_processing_step(f"Setting value of constant {stem.stem_name}"):
162
- if len(stem.petals) == 0:
163
- # There are no petals so nothing to do
423
+ with self.telemetry_span(f"Setting value of constant {stem.stem_name}"):
424
+ if not stem.can_be_picked:
425
+ # Nothing to do
164
426
  continue
165
427
  if stem.bud.value is Thorn:
166
428
  # Must've been a picky bud that passed. We don't want to pick it because it has no value
@@ -183,7 +445,7 @@ class ParseDataBase(WorkflowTaskBase, ABC):
183
445
  None
184
446
  """
185
447
  for stem in tag_pot:
186
- with self.apm_processing_step(f"Applying {stem.stem_name} tag to files"):
448
+ with self.telemetry_span(f"Applying {stem.stem_name} tag to files"):
187
449
  for petal in stem.petals:
188
450
  tag = Tag.format_tag(stem.stem_name, petal.value)
189
451
  for path in petal.keys: