dkist-processing-common 12.9.0__tar.gz → 12.10.0__tar.gz

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 (144) hide show
  1. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/CHANGELOG.rst +11 -0
  2. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/PKG-INFO +1 -1
  3. dkist_processing_common-12.10.0/dkist_processing_common/models/quality.py +351 -0
  4. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tasks/l1_output_data.py +19 -2
  5. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tasks/mixin/quality/_base.py +0 -20
  6. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tasks/mixin/quality/_metrics.py +2 -161
  7. dkist_processing_common-12.10.0/dkist_processing_common/tasks/quality_metrics.py +503 -0
  8. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tests/test_assemble_quality.py +158 -49
  9. dkist_processing_common-12.10.0/dkist_processing_common/tests/test_quality.py +706 -0
  10. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tests/test_quality_mixin.py +97 -266
  11. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common.egg-info/PKG-INFO +1 -1
  12. dkist_processing_common-12.9.0/dkist_processing_common/models/quality.py +0 -122
  13. dkist_processing_common-12.9.0/dkist_processing_common/tasks/quality_metrics.py +0 -316
  14. dkist_processing_common-12.9.0/dkist_processing_common/tests/test_quality.py +0 -292
  15. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/.gitignore +0 -0
  16. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/.pre-commit-config.yaml +0 -0
  17. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/.readthedocs.yml +0 -0
  18. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/.snyk +0 -0
  19. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/README.rst +0 -0
  20. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/bitbucket-pipelines.yml +0 -0
  21. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/changelog/.gitempty +0 -0
  22. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/__init__.py +0 -0
  23. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/_util/__init__.py +0 -0
  24. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/_util/constants.py +0 -0
  25. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/_util/graphql.py +0 -0
  26. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/_util/scratch.py +0 -0
  27. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/_util/tags.py +0 -0
  28. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/codecs/__init__.py +0 -0
  29. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/codecs/array.py +0 -0
  30. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/codecs/asdf.py +0 -0
  31. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/codecs/basemodel.py +0 -0
  32. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/codecs/bytes.py +0 -0
  33. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/codecs/fits.py +0 -0
  34. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/codecs/iobase.py +0 -0
  35. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/codecs/json.py +0 -0
  36. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/codecs/path.py +0 -0
  37. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/codecs/quality.py +0 -0
  38. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/codecs/str.py +0 -0
  39. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/config.py +0 -0
  40. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/fonts/Lato-Regular.ttf +0 -0
  41. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/fonts/__init__.py +0 -0
  42. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/manual.py +0 -0
  43. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/models/__init__.py +0 -0
  44. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/models/constants.py +0 -0
  45. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/models/dkist_location.py +0 -0
  46. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/models/extras.py +0 -0
  47. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/models/fits_access.py +0 -0
  48. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/models/flower_pot.py +0 -0
  49. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/models/fried_parameter.py +0 -0
  50. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/models/graphql.py +0 -0
  51. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/models/input_dataset.py +0 -0
  52. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/models/message.py +0 -0
  53. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/models/message_queue_binding.py +0 -0
  54. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/models/metric_code.py +0 -0
  55. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/models/parameters.py +0 -0
  56. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/models/tags.py +0 -0
  57. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/models/task_name.py +0 -0
  58. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/models/telemetry.py +0 -0
  59. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/models/wavelength.py +0 -0
  60. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/parsers/__init__.py +0 -0
  61. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/parsers/average_bud.py +0 -0
  62. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/parsers/cs_step.py +0 -0
  63. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/parsers/dsps_repeat.py +0 -0
  64. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/parsers/experiment_id_bud.py +0 -0
  65. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/parsers/id_bud.py +0 -0
  66. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/parsers/l0_fits_access.py +0 -0
  67. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/parsers/l1_fits_access.py +0 -0
  68. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/parsers/lookup_bud.py +0 -0
  69. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/parsers/near_bud.py +0 -0
  70. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/parsers/observing_program_id_bud.py +0 -0
  71. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/parsers/proposal_id_bud.py +0 -0
  72. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/parsers/quality.py +0 -0
  73. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/parsers/retarder.py +0 -0
  74. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/parsers/single_value_single_key_flower.py +0 -0
  75. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/parsers/task.py +0 -0
  76. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/parsers/time.py +0 -0
  77. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/parsers/unique_bud.py +0 -0
  78. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/parsers/wavelength.py +0 -0
  79. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tasks/__init__.py +0 -0
  80. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tasks/assemble_movie.py +0 -0
  81. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tasks/base.py +0 -0
  82. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tasks/mixin/__init__.py +0 -0
  83. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tasks/mixin/globus.py +0 -0
  84. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tasks/mixin/interservice_bus.py +0 -0
  85. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tasks/mixin/metadata_store.py +0 -0
  86. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tasks/mixin/object_store.py +0 -0
  87. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tasks/mixin/quality/__init__.py +0 -0
  88. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tasks/output_data_base.py +0 -0
  89. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tasks/parse_l0_input_data.py +0 -0
  90. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tasks/teardown.py +0 -0
  91. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tasks/transfer_input_data.py +0 -0
  92. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tasks/trial_catalog.py +0 -0
  93. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tasks/trial_output_data.py +0 -0
  94. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tasks/write_extra.py +0 -0
  95. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tasks/write_l1.py +0 -0
  96. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tasks/write_l1_base.py +0 -0
  97. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tests/__init__.py +0 -0
  98. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tests/conftest.py +0 -0
  99. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tests/mock_metadata_store.py +0 -0
  100. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tests/test_assemble_movie.py +0 -0
  101. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tests/test_base.py +0 -0
  102. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tests/test_codecs.py +0 -0
  103. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tests/test_constants.py +0 -0
  104. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tests/test_construct_dataset_extras.py +0 -0
  105. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tests/test_cs_step.py +0 -0
  106. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tests/test_dkist_location.py +0 -0
  107. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tests/test_fits_access.py +0 -0
  108. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tests/test_flower_pot.py +0 -0
  109. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tests/test_fried_parameter.py +0 -0
  110. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tests/test_input_dataset.py +0 -0
  111. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tests/test_interservice_bus.py +0 -0
  112. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tests/test_interservice_bus_mixin.py +0 -0
  113. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tests/test_manual_processing.py +0 -0
  114. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tests/test_output_data_base.py +0 -0
  115. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tests/test_parameters.py +0 -0
  116. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tests/test_parse_l0_input_data.py +0 -0
  117. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tests/test_publish_catalog_messages.py +0 -0
  118. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tests/test_scratch.py +0 -0
  119. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tests/test_stems.py +0 -0
  120. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tests/test_submit_dataset_metadata.py +0 -0
  121. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tests/test_tags.py +0 -0
  122. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tests/test_task_name.py +0 -0
  123. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tests/test_task_parsing.py +0 -0
  124. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tests/test_teardown.py +0 -0
  125. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tests/test_transfer_input_data.py +0 -0
  126. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tests/test_transfer_l1_output_data.py +0 -0
  127. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tests/test_trial_catalog.py +0 -0
  128. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tests/test_trial_output_data.py +0 -0
  129. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tests/test_workflow_task_base.py +0 -0
  130. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common/tests/test_write_l1.py +0 -0
  131. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common.egg-info/SOURCES.txt +0 -0
  132. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common.egg-info/dependency_links.txt +0 -0
  133. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common.egg-info/requires.txt +0 -0
  134. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/dkist_processing_common.egg-info/top_level.txt +0 -0
  135. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/docs/Makefile +0 -0
  136. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/docs/changelog.rst +0 -0
  137. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/docs/conf.py +0 -0
  138. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/docs/index.rst +0 -0
  139. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/docs/landing_page.rst +0 -0
  140. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/docs/make.bat +0 -0
  141. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/docs/requirements.txt +0 -0
  142. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/licenses/LICENSE.rst +0 -0
  143. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/pyproject.toml +0 -0
  144. {dkist_processing_common-12.9.0 → dkist_processing_common-12.10.0}/setup.cfg +0 -0
@@ -1,3 +1,14 @@
1
+ v12.10.0 (2026-04-02)
2
+ =====================
3
+
4
+ Features
5
+ --------
6
+
7
+ - Added capabilities to simplify the way that quality metrics get produced.
8
+ The central feature is the `QualityMetric` `BaseModel` which can be used to fully define any quality metric. (`#313 <https://bitbucket.org/dkistdc/dkist-processing-common/pull-requests/313>`__)
9
+ - Simplify the production of L0 quality metrics. (`#313 <https://bitbucket.org/dkistdc/dkist-processing-common/pull-requests/313>`__)
10
+
11
+
1
12
  v12.9.0 (2026-04-01)
2
13
  ====================
3
14
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dkist-processing-common
3
- Version: 12.9.0
3
+ Version: 12.10.0
4
4
  Summary: Common task classes used by the DKIST science data processing pipelines
5
5
  Author-email: NSO / AURA <dkistdc@nso.edu>
6
6
  License: BSD-3-Clause
@@ -0,0 +1,351 @@
1
+ """Support classes used to create a quality report."""
2
+
3
+ from datetime import datetime
4
+ from typing import Annotated
5
+ from typing import Any
6
+
7
+ from pydantic import BaseModel
8
+ from pydantic import BeforeValidator
9
+ from pydantic import ConfigDict
10
+ from pydantic import Field
11
+ from pydantic import FiniteFloat
12
+ from pydantic import PlainSerializer
13
+ from pydantic import field_validator
14
+ from pydantic import model_serializer
15
+ from pydantic import model_validator
16
+ from pydantic.alias_generators import to_camel
17
+ from pydantic_core.core_schema import SerializationInfo
18
+ from pydantic_core.core_schema import ValidationInfo
19
+
20
+ ###########################################################
21
+ # Old Quality Models #
22
+ # #
23
+ # These will gradually be replaced by #
24
+ # New Quality Models (below) #
25
+ ###########################################################
26
+
27
+
28
+ class Plot2D(BaseModel):
29
+ """Support class use to hold the data for creating a 2D plot in the quality report."""
30
+
31
+ xlabel: str
32
+ ylabel: str
33
+ series_data: dict[str, list[list[Any]]]
34
+ series_name: str | None = None
35
+ ylabel_horizontal: bool = False
36
+ ylim: tuple[float, float] | None = None
37
+ plot_kwargs: dict[str, dict[str, Any]] = Field(default_factory=dict)
38
+ sort_series: bool = True
39
+
40
+
41
+ class VerticalMultiPanePlot2D(BaseModel):
42
+ """
43
+ Support class to hold a multi-pane plot with plots stacked vertically.
44
+
45
+ This type of metric is really geared towards plots that share an X axis and have no gap between them. If you just
46
+ want two separate plots it's probably better to use a list of `Plot2D` objects.
47
+ """
48
+
49
+ top_to_bottom_plot_list: list[Plot2D]
50
+ match_x_axes: bool = True
51
+ no_gap: bool = True
52
+ top_to_bottom_height_ratios: list[float] | None = None
53
+
54
+ @field_validator("top_to_bottom_height_ratios")
55
+ @classmethod
56
+ def ensure_same_number_of_height_ratios_and_plots(
57
+ cls, height_ratios: list[float] | None, info: ValidationInfo
58
+ ) -> list[float]:
59
+ """
60
+ Make sure that the number of height ratios is the same as the number of plots.
61
+
62
+ Also populates default, same-size ratios if no ratios were given.
63
+ """
64
+ try:
65
+ plot_list = info.data["top_to_bottom_plot_list"]
66
+ except KeyError:
67
+ # The plot list didn't validate for some reason. We're about to error anyway.
68
+ return [1.0]
69
+
70
+ num_plots = len(plot_list)
71
+ if height_ratios is None:
72
+ return [1.0] * num_plots
73
+
74
+ if len(height_ratios) != num_plots:
75
+ raise ValueError(
76
+ f"The number of items in `top_to_bottom_height_ratios` list ({len(height_ratios)}) is not "
77
+ f"the same as the number of plots ({num_plots})"
78
+ )
79
+
80
+ return height_ratios
81
+
82
+
83
+ class SimpleTable(BaseModel):
84
+ """Support class to hold a simple table to be inserted into the quality report."""
85
+
86
+ rows: list[list[Any]]
87
+ header_row: bool = True
88
+ header_column: bool = False
89
+
90
+
91
+ class ModulationMatrixHistograms(BaseModel):
92
+ """Support class for holding the big ol' grid of histograms that represent the modulation matrix fits."""
93
+
94
+ modmat_list: list[list[list[float]]]
95
+
96
+
97
+ class EfficiencyHistograms(BaseModel):
98
+ """Support class for holding 4 histograms that correspond to efficiencies of the 4 stokes components."""
99
+
100
+ efficiency_list: list[list[float]]
101
+
102
+
103
+ class PlotHistogram(BaseModel):
104
+ """Support class to hold 1D data for plotting a histogram."""
105
+
106
+ xlabel: str
107
+ series_data: dict[str, list[float]]
108
+ series_name: str | None = None
109
+ vertical_lines: dict[str, float] | None
110
+
111
+
112
+ class PlotRaincloud(BaseModel):
113
+ """Support class to hold data series for fancy-ass violin plots."""
114
+
115
+ xlabel: str
116
+ ylabel: str
117
+ categorical_column_name: str
118
+ distribution_column_name: str
119
+ dataframe_json: str
120
+ hue_column_name: str | None
121
+ ylabel_horizontal: bool | None
122
+
123
+
124
+ class ReportMetric(BaseModel):
125
+ """A Quality Report is made up of a list of metrics with the schema defined by this class."""
126
+
127
+ name: str
128
+ description: str
129
+ metric_code: str
130
+ facet: str | None = None
131
+ statement: str | list[str] | None = None
132
+ plot_data: Plot2D | list[Plot2D] | None = None
133
+ multi_plot_data: VerticalMultiPanePlot2D | None = None
134
+ histogram_data: PlotHistogram | list[PlotHistogram] | None = None
135
+ table_data: SimpleTable | list[SimpleTable] | None = None
136
+ modmat_data: ModulationMatrixHistograms | None = None
137
+ efficiency_data: EfficiencyHistograms | None = None
138
+ raincloud_data: PlotRaincloud | None = None
139
+ warnings: list[str] | None = None
140
+
141
+
142
+ ###########################################################
143
+ # New Quality Models #
144
+ # #
145
+ # These define the interface with dkist-quality #
146
+ # These will eventually get moved to dkist-quality #
147
+ ###########################################################
148
+
149
+
150
+ class QualityModel(BaseModel):
151
+ """BaseModel for quality data."""
152
+
153
+ model_config = ConfigDict(
154
+ # only accept snake_case field names
155
+ validate_by_name=True,
156
+ # re-validate the field if its value gets changed after creation
157
+ validate_assignment=True,
158
+ )
159
+
160
+
161
+ def datetime_or_iso_dict_input(v: Any) -> Any:
162
+ """
163
+ Extend pydantic `datetime` capability.
164
+
165
+ Adds the ability to accept an 'iso dict' of the {"iso_date": <value>} form
166
+ """
167
+ # If appropriate, extract value from {"iso_date": <value>} before passing to the normal datetime handler
168
+ if isinstance(v, dict) and "iso_date" in v:
169
+ return v["iso_date"]
170
+ return v
171
+
172
+
173
+ # NOTE: This is a temporary data type that can be eliminated once all metric conversion is complete
174
+ DatetimeIsoDict = Annotated[
175
+ # this is a datetime within the pydantic BaseModel
176
+ datetime,
177
+ # accept 'iso dict' input in addition to normal datetime inputs
178
+ BeforeValidator(datetime_or_iso_dict_input),
179
+ # always output the datetime as an 'iso dict'
180
+ PlainSerializer(lambda dt: {"iso_date": dt.isoformat()}, return_type=dict[str, str]),
181
+ ]
182
+
183
+
184
+ class TimeSeriesData(QualityModel):
185
+ """
186
+ Time Series Data.
187
+
188
+ The context, e.g. task_type or modstate, is maintained outside of this structure.
189
+ """
190
+
191
+ # by default, a tuple accepts a list as input
192
+ # the use of tuples is intentional because list mutations do not trigger validation
193
+ # the impact is that TimeSeriesData should not be instantiated until
194
+ # after the tuple input has been fully accumulated
195
+ # when `mode="json"`, the output of a tuple results in a list
196
+ # when `mode="python"`, the output of a tuple results in a tuple
197
+ x_values: tuple[DatetimeIsoDict, ...]
198
+ # NOTE: FiniteFloat will fail fast for `nan` and `inf`
199
+ y_values: tuple[FiniteFloat, ...]
200
+
201
+ @model_validator(mode="after")
202
+ def tuple_validation(self):
203
+ """Validate tuple lengths."""
204
+ axes_are_different_lengths = len(self.x_values) != len(self.y_values)
205
+ if axes_are_different_lengths:
206
+ raise ValueError(
207
+ f"Cannot store TimeSeriesData with different length axes. "
208
+ f"{len(self.x_values)=}, {len(self.y_values)=}"
209
+ )
210
+ # Can't have this AND default to empty tuple
211
+ axes_are_zero_length = not self.x_values or not self.y_values
212
+ if axes_are_zero_length:
213
+ raise ValueError(
214
+ f"Cannot store TimeSeriesData with 0 length axes. "
215
+ f"{len(self.x_values)=}, {len(self.y_values)=}"
216
+ )
217
+ return self
218
+
219
+ @model_serializer(mode="wrap")
220
+ def custom_dump(self, handler, info: SerializationInfo):
221
+ """Serialize output for QRM compatibility."""
222
+ data = handler(self)
223
+ # change structure if mode="json" for QRM compatibility
224
+ if info.mode == "json":
225
+ # dump as list[list[Any]] with DatetimeIsoDict format for x_values
226
+ return [list(data["x_values"]), list(data["y_values"])]
227
+ return data
228
+
229
+ @model_validator(mode="before")
230
+ @classmethod
231
+ def handle_json(cls, input_value: Any):
232
+ """
233
+ Transform json input from list[list[Any]] to dict[Any, Any].
234
+
235
+ the dict is expected to be dict[str, tuple[Any, ...]], which gets validated by normal BaseModel configurations.
236
+ """
237
+ if isinstance(input_value, dict):
238
+ return input_value
239
+
240
+ if (
241
+ isinstance(input_value, (list, tuple))
242
+ and len(input_value) == 2
243
+ and isinstance(input_value[0], (list, tuple))
244
+ and isinstance(input_value[1], (list, tuple))
245
+ ):
246
+ return {"x_values": input_value[0], "y_values": input_value[1]}
247
+
248
+ return input_value
249
+
250
+
251
+ class XYData(QualityModel):
252
+ """One or more time series for a single x-y plot."""
253
+
254
+ xlabel: str
255
+ ylabel: str
256
+ # the key is the name assigned to the series, e.g. within the legend
257
+ # the key cannot be None, because `null` is not a valid json key
258
+ series_data: dict[str, TimeSeriesData] = Field(default_factory=dict)
259
+ # TODO FUTURE - convert `series_name` to `legend_title` to better align with matplotlib terminology
260
+ series_name: str | None = None
261
+ ylabel_horizontal: bool | None = None
262
+ ylim: tuple[float | None, float | None] | None = None
263
+ plot_kwargs: dict[str, dict[str, Any]] = Field(default_factory=dict)
264
+ sort_series: bool | None = True
265
+
266
+
267
+ class TableData(QualityModel):
268
+ """Tabular Data."""
269
+
270
+ # by default, a tuple accepts a list as input
271
+ # the use of tuples is intentional because list mutations do not trigger validation
272
+ # the impact is that TableData should not be instantiated until
273
+ # after the tuple input has been fully accumulated
274
+ # when `mode="json"`, the output of a tuple results in a list
275
+ # when `mode="python"`, the output of a tuple results in a tuple
276
+ # Note: numeric values are represented as strings for table data
277
+ rows: tuple[tuple[str, ...], ...]
278
+ header_row: bool | None = True
279
+ header_col: bool | None = False
280
+
281
+ @model_validator(mode="after")
282
+ def tuple_validation(self):
283
+ """Validate tuple lengths."""
284
+ row_count = len(self.rows)
285
+ if row_count == 0:
286
+ raise ValueError(f"Invalid table. No rows.")
287
+ header_column_count = len(self.rows[0])
288
+ if header_column_count == 0:
289
+ raise ValueError(f"Invalid table. No columns.")
290
+ row_column_counts = [len(row) for row in self.rows[1:]]
291
+ column_count_mismatch = any(
292
+ col_count != header_column_count for col_count in row_column_counts
293
+ )
294
+ if column_count_mismatch:
295
+ raise ValueError(
296
+ f"Mismatch in column count. {header_column_count=}, {row_column_counts=}"
297
+ )
298
+ return self
299
+
300
+
301
+ class MultiPlotData(QualityModel):
302
+ """Multiple Plot Data."""
303
+
304
+ # TBD
305
+ pass
306
+
307
+
308
+ class HistogramData(QualityModel):
309
+ """Histogram Data."""
310
+
311
+ # TBD
312
+ pass
313
+
314
+
315
+ class ModMatData(QualityModel):
316
+ """Modulation Matrix Data."""
317
+
318
+ # TBD
319
+ pass
320
+
321
+
322
+ class RaincloudData(QualityModel):
323
+ """Raincloud Data."""
324
+
325
+ # TBD
326
+ pass
327
+
328
+
329
+ class EfficiencyData(QualityModel):
330
+ """Efficiency Data."""
331
+
332
+ # TBD
333
+ pass
334
+
335
+
336
+ class QualityMetric(QualityModel):
337
+ """Quality Metric."""
338
+
339
+ name: str
340
+ description: str
341
+ metric_code: str
342
+ facet: str | None = None
343
+ statement: list[str] | None = None
344
+ plot_data: list[XYData] | None = None
345
+ multi_plot_data: list[MultiPlotData] | None = None
346
+ histogram_data: list[HistogramData] | None = None
347
+ table_data: list[TableData] | None = None
348
+ modmad_data: list[ModMatData] | None = None
349
+ raincloud_data: list[RaincloudData] | None = None
350
+ efficiency_data: list[EfficiencyData] | None = None
351
+ warnings: list[str] | None = None
@@ -5,11 +5,13 @@ from abc import ABC
5
5
  from pathlib import Path
6
6
  from typing import Iterable
7
7
 
8
+ from dkist_processing_common.codecs.basemodel import basemodel_decoder
8
9
  from dkist_processing_common.codecs.quality import quality_data_encoder
9
10
  from dkist_processing_common.models.message import CatalogFrameMessage
10
11
  from dkist_processing_common.models.message import CatalogFrameMessageBody
11
12
  from dkist_processing_common.models.message import CatalogObjectMessage
12
13
  from dkist_processing_common.models.message import CatalogObjectMessageBody
14
+ from dkist_processing_common.models.quality import QualityMetric
13
15
  from dkist_processing_common.models.tags import Tag
14
16
  from dkist_processing_common.tasks.mixin.globus import GlobusMixin
15
17
  from dkist_processing_common.tasks.mixin.interservice_bus import InterserviceBusMixin
@@ -146,8 +148,13 @@ class AssembleQualityData(L1OutputDataBase, QualityMixin):
146
148
 
147
149
  def run(self):
148
150
  """Run method for the task."""
149
- with self.telemetry_span("Assembling quality data"):
150
- quality_data = self.quality_assemble_data(polcal_label_list=self.polcal_label_list)
151
+ # this is the new way, which will eventually supplant the old `quality_assemble_data`
152
+ with self.telemetry_span("Assembling generic quality data"):
153
+ quality_data = self.assemble_quality_data()
154
+
155
+ # this is the old way, which is being replaced by the new `assemble_quality_data`
156
+ with self.telemetry_span("Assembling quality data by Metric"):
157
+ quality_data += self.quality_assemble_data(polcal_label_list=self.polcal_label_list)
151
158
 
152
159
  with self.telemetry_span(
153
160
  f"Saving quality data with {len(quality_data)} metrics to the file system"
@@ -159,6 +166,16 @@ class AssembleQualityData(L1OutputDataBase, QualityMixin):
159
166
  relative_path=f"{self.constants.dataset_id}_quality_data.json",
160
167
  )
161
168
 
169
+ def assemble_quality_data(self) -> list[dict]:
170
+ """Assemble Quality Data."""
171
+ metrics = []
172
+ tags = [Tag.quality("GENERIC")]
173
+ for metric in self.read(tags=tags, decoder=basemodel_decoder, model=QualityMetric):
174
+ # `metrics` needs to be list[dict] for compatibility with existing `quality_assemble_data`
175
+ # once all metric conversion is complete, `metrics` should be list[QualityMetric] instead
176
+ metrics.append(metric.model_dump(mode="json"))
177
+ return metrics
178
+
162
179
 
163
180
  class SubmitDatasetMetadata(L1OutputDataBase):
164
181
  """
@@ -28,7 +28,6 @@ class QualityMixin(
28
28
  """Assemble the quality data by checking for the existence of each metric."""
29
29
  report = []
30
30
  report += self.quality_task_independent_metrics()
31
- report += self.quality_task_dependent_metrics()
32
31
 
33
32
  polcal_labels = polcal_label_list or []
34
33
  report += self.quality_polcal_metrics(polcal_labels)
@@ -43,15 +42,6 @@ class QualityMixin(
43
42
  result.append(metric_func())
44
43
  return result
45
44
 
46
- def quality_task_dependent_metrics(self) -> list[dict]:
47
- """Encapsulate task dependent metric parsing."""
48
- result = []
49
- for metric_name, metric_func in self.quality_metrics_task_dependence.items():
50
- for task_type in self.quality_task_types:
51
- if self._quality_metric_exists(metric_name=metric_name, task_type=task_type):
52
- result.append(metric_func(task_type=task_type))
53
- return result
54
-
55
45
  def quality_polcal_metrics(self, label_list: list) -> list[dict]:
56
46
  """Encapsulate polcal metric parsing."""
57
47
  result = []
@@ -82,22 +72,12 @@ class QualityMixin(
82
72
  "SENSITIVITY": self.quality_build_sensitivity,
83
73
  "HEALTH_STATUS": self.quality_build_health_status,
84
74
  "TASK_TYPES": self.quality_build_task_type_counts,
85
- "DATASET_AVERAGE": self.quality_build_dataset_average,
86
- "DATASET_RMS": self.quality_build_dataset_rms,
87
75
  "HISTORICAL": self.quality_build_historical,
88
76
  "AO_STATUS": self.quality_build_ao_status,
89
77
  "RANGE": self.quality_build_range,
90
78
  "WAVECAL_FIT": self.quality_build_wavecal_results,
91
79
  }
92
80
 
93
- @property
94
- def quality_metrics_task_dependence(self) -> dict:
95
- """Return a dict of quality metrics which are dependent on the task."""
96
- return {
97
- "FRAME_AVERAGE": self.quality_build_frame_average,
98
- "FRAME_RMS": self.quality_build_frame_rms,
99
- }
100
-
101
81
  @property
102
82
  def quality_metrics_polcal(self) -> dict:
103
83
  """Return a dict of polcal quality metrics."""
@@ -12,10 +12,10 @@ from functools import partial
12
12
  from typing import Any
13
13
  from typing import Iterable
14
14
  from typing import Literal
15
+ from typing import Sequence
15
16
 
16
17
  import astropy.units as u
17
18
  import numpy as np
18
- from astropy.wcs import WCS
19
19
  from dkist_processing_pac.fitter.fitter_parameters import CU_PARAMS
20
20
  from dkist_processing_pac.fitter.fitter_parameters import GLOBAL_PARAMS
21
21
  from dkist_processing_pac.fitter.fitter_parameters import TELESCOPE_PARAMS
@@ -186,7 +186,7 @@ class _SimplePlotQualityMixin:
186
186
  return all_plot_data
187
187
 
188
188
  @staticmethod
189
- def _find_iqr_outliers(datetimes: list[str], values: list[float]) -> list[str]:
189
+ def _find_iqr_outliers(datetimes: Sequence[datetime], values: Sequence[float]) -> list[str]:
190
190
  """
191
191
  Given a list of values, find values that fall more than (1.5 * iqr) outside the quartiles of the data.
192
192
 
@@ -314,90 +314,6 @@ class _SimplePlotQualityMixin:
314
314
  warnings=None,
315
315
  )
316
316
 
317
- def quality_store_frame_average(
318
- self,
319
- datetimes: list[str],
320
- values: list[float],
321
- task_type: str,
322
- modstate: int | None = None,
323
- ):
324
- """Collect and store datetime / value pairs for the individual frame averages."""
325
- tags = [Tag.quality(MetricCode.frame_average)]
326
- if modstate:
327
- tags.append(Tag.modstate(modstate))
328
- self._record_2d_plot_values(
329
- x_values=datetimes,
330
- y_values=values,
331
- tags=tags,
332
- series_name=modstate or 1,
333
- task_type=task_type,
334
- )
335
-
336
- def quality_build_frame_average(self, task_type: str) -> dict:
337
- """Build frame average schema from stored data."""
338
- # This will load data for all modstates, if present
339
- series_data = self._load_2d_plot_values(
340
- tags=Tag.quality(MetricCode.frame_average), task_type=task_type
341
- )
342
-
343
- # Build metric dict
344
- if len(series_data) > 0:
345
- datetimes, values = list(series_data.values())[0]
346
- warnings = self._find_iqr_outliers(datetimes=datetimes, values=values)
347
- return self._create_2d_plot_with_datetime_metric(
348
- name=f"Average Across Frame - {task_type.upper()}",
349
- description=f"Average intensity value across frames of task type {task_type}. One measurement is taken per frame in each task type.",
350
- metric_code=MetricCode.frame_average,
351
- facet=task_type.upper(),
352
- xlabel="Time",
353
- ylabel="Average Value (adu / sec)",
354
- series_data=series_data,
355
- series_name="Modstate",
356
- warnings=self._format_warnings(warnings),
357
- )
358
-
359
- def quality_store_frame_rms(
360
- self,
361
- datetimes: list[str],
362
- values: list[float],
363
- task_type: str,
364
- modstate: int | None = None,
365
- ):
366
- """Collect and store datetime / value pairs for the individual frame rms."""
367
- tags = [Tag.quality(MetricCode.frame_rms)]
368
- if modstate:
369
- tags.append(Tag.modstate(modstate))
370
- self._record_2d_plot_values(
371
- x_values=datetimes,
372
- y_values=values,
373
- tags=tags,
374
- series_name=modstate or 1,
375
- task_type=task_type,
376
- )
377
-
378
- def quality_build_frame_rms(self, task_type: str) -> dict:
379
- """Build frame rms schema from stored data."""
380
- # This will load data for all modstates, if present
381
- series_data = self._load_2d_plot_values(
382
- tags=Tag.quality(MetricCode.frame_rms), task_type=task_type
383
- )
384
-
385
- # Build metric dict
386
- if len(series_data) > 0:
387
- datetimes, values = list(series_data.values())[0]
388
- warnings = self._find_iqr_outliers(datetimes=datetimes, values=values)
389
- return self._create_2d_plot_with_datetime_metric(
390
- name=f"Root Mean Square (RMS) Across Frame - {task_type.upper()}",
391
- description=f"RMS value across frames of task type {task_type}. One measurement is taken per frame in each task type.",
392
- metric_code=MetricCode.frame_rms,
393
- facet=task_type.upper(),
394
- xlabel="Time",
395
- ylabel="RMS (adu / sec)",
396
- series_data=series_data,
397
- series_name="Modstate",
398
- warnings=self._format_warnings(warnings),
399
- )
400
-
401
317
  def quality_store_noise(self, datetimes: list[str], values: list[float], stokes: str = "I"):
402
318
  """Collect and store datetime / value pairs for the noise data."""
403
319
  self._record_2d_plot_values(
@@ -565,81 +481,6 @@ class _TableQualityMixin:
565
481
  warnings=self._format_warnings(warnings),
566
482
  )
567
483
 
568
- def quality_store_dataset_average(self, task_type: str, frame_averages: list[float]):
569
- """
570
- Collect and store dataset average.
571
-
572
- Parameters
573
- ----------
574
- task_type: task type as listed in the headers
575
- frame_averages: average value of all pixels in each frame of the given task type
576
- """
577
- data = {"task_type": task_type, "frame_averages": frame_averages}
578
- self._record_values(values=data, tags=Tag.quality(MetricCode.dataset_average))
579
-
580
- def quality_build_dataset_average(self) -> dict:
581
- """Build dataset average schema from stored data."""
582
- dataset_averages = defaultdict(list)
583
- # Loop over files that contain data for this metric
584
- for path in self.read(tags=Tag.quality(MetricCode.dataset_average)):
585
- with path.open() as f:
586
- data = json.load(f)
587
- # Add counts for the task type to its already existing counts
588
- dataset_averages[data["task_type"]] += data["frame_averages"]
589
-
590
- # Now, build metric from the counts dict
591
- table_data = [[i[0], round(np.mean(i[1]), 2)] for i in dataset_averages.items()]
592
- # Add header row
593
- table_data.insert(0, ["Task Type", "Dataset Average (adu / sec)"])
594
- return self._create_table_metric(
595
- name="Average Across Dataset",
596
- description="This metric is the calculated mean intensity value across data from an "
597
- "instrument program task type used in the creation of an entire L1 "
598
- "dataset.",
599
- metric_code=MetricCode.dataset_average,
600
- rows=table_data,
601
- warnings=None,
602
- )
603
-
604
- def quality_store_dataset_rms(self, task_type: str, frame_rms: list[float]):
605
- """
606
- Collect and store dataset average.
607
-
608
- Parameters
609
- ----------
610
- task_type: task type as listed in the headers
611
- frame_rms: rms value of all pixels in each frame of the given task type
612
- """
613
- data = {"task_type": task_type, "frame_rms": frame_rms}
614
- self._record_values(values=data, tags=Tag.quality(MetricCode.dataset_rms))
615
-
616
- def quality_build_dataset_rms(self) -> dict:
617
- """Build dataset rms schema from stored data."""
618
- dataset_rms = {}
619
- # Loop over files that contain data for this metric
620
- for path in self.read(tags=Tag.quality(MetricCode.dataset_rms)):
621
- with path.open() as f:
622
- data = json.load(f)
623
- # If the task type isn't in the dict, add it with counts set to zero
624
- if not data["task_type"] in dataset_rms.keys():
625
- dataset_rms[data["task_type"]] = []
626
- # Add counts for the task type to its already existing counts
627
- dataset_rms[data["task_type"]] += data["frame_rms"]
628
-
629
- # Now, build metric from the counts dict
630
- table_data = [[i[0], round(np.mean(i[1]), 2)] for i in dataset_rms.items()]
631
- # Add header row
632
- table_data.insert(0, ["Task Type", "Dataset RMS (adu / sec)"])
633
- return self._create_table_metric(
634
- name="Dataset RMS",
635
- description="This metric is the calculated root mean square intensity value across data"
636
- " from an instrument program task type used in the creation of an entire "
637
- "L1 dataset.",
638
- metric_code=MetricCode.dataset_rms,
639
- rows=table_data,
640
- warnings=None,
641
- )
642
-
643
484
  def quality_store_historical(self, name: str, value: Any, warning: str | None = None):
644
485
  """
645
486
  Insert historical data into the schema used to record quality info.