dkist-processing-common 14.3.6__tar.gz → 14.4.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 (146) hide show
  1. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/CHANGELOG.rst +9 -0
  2. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/PKG-INFO +1 -1
  3. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/models/quality.py +145 -28
  4. dkist_processing_common-14.4.0/dkist_processing_common/tasks/mixin/quality/_base.py +218 -0
  5. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tasks/mixin/quality/_metrics.py +0 -141
  6. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tests/test_assemble_quality.py +89 -11
  7. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tests/test_quality_mixin.py +58 -104
  8. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common.egg-info/PKG-INFO +1 -1
  9. dkist_processing_common-14.4.0/dkist_processing_common.egg-info/scm_version.json +8 -0
  10. dkist_processing_common-14.3.6/dkist_processing_common/tasks/mixin/quality/_base.py +0 -84
  11. dkist_processing_common-14.3.6/dkist_processing_common.egg-info/scm_version.json +0 -8
  12. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/.gitignore +0 -0
  13. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/.pre-commit-config.yaml +0 -0
  14. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/.readthedocs.yml +0 -0
  15. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/.rovodev/.review-agent.md +0 -0
  16. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/.snyk +0 -0
  17. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/README.rst +0 -0
  18. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/bitbucket-pipelines.yml +0 -0
  19. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/changelog/.gitempty +0 -0
  20. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/__init__.py +0 -0
  21. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/_util/__init__.py +0 -0
  22. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/_util/constants.py +0 -0
  23. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/_util/graphql.py +0 -0
  24. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/_util/scratch.py +0 -0
  25. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/_util/tags.py +0 -0
  26. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/codecs/__init__.py +0 -0
  27. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/codecs/array.py +0 -0
  28. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/codecs/asdf.py +0 -0
  29. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/codecs/basemodel.py +0 -0
  30. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/codecs/bytes.py +0 -0
  31. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/codecs/fits.py +0 -0
  32. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/codecs/iobase.py +0 -0
  33. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/codecs/json.py +0 -0
  34. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/codecs/path.py +0 -0
  35. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/codecs/quality.py +0 -0
  36. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/codecs/str.py +0 -0
  37. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/config.py +0 -0
  38. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/fonts/Lato-Regular.ttf +0 -0
  39. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/fonts/__init__.py +0 -0
  40. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/manual.py +0 -0
  41. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/models/__init__.py +0 -0
  42. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/models/constants.py +0 -0
  43. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/models/dkist_location.py +0 -0
  44. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/models/extras.py +0 -0
  45. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/models/fits_access.py +0 -0
  46. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/models/flower_pot.py +0 -0
  47. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/models/fried_parameter.py +0 -0
  48. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/models/graphql.py +0 -0
  49. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/models/input_dataset.py +0 -0
  50. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/models/message.py +0 -0
  51. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/models/message_queue_binding.py +0 -0
  52. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/models/metric_code.py +0 -0
  53. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/models/parameters.py +0 -0
  54. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/models/tags.py +0 -0
  55. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/models/task_name.py +0 -0
  56. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/models/telemetry.py +0 -0
  57. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/models/wavelength.py +0 -0
  58. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/parsers/__init__.py +0 -0
  59. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/parsers/average_bud.py +0 -0
  60. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/parsers/cs_step.py +0 -0
  61. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/parsers/dsps_repeat.py +0 -0
  62. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/parsers/experiment_id_bud.py +0 -0
  63. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/parsers/id_bud.py +0 -0
  64. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/parsers/l0_fits_access.py +0 -0
  65. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/parsers/l1_fits_access.py +0 -0
  66. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/parsers/lookup_bud.py +0 -0
  67. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/parsers/near_bud.py +0 -0
  68. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/parsers/observing_program_id_bud.py +0 -0
  69. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/parsers/proposal_id_bud.py +0 -0
  70. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/parsers/quality.py +0 -0
  71. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/parsers/retarder.py +0 -0
  72. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/parsers/single_value_single_key_flower.py +0 -0
  73. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/parsers/task.py +0 -0
  74. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/parsers/time.py +0 -0
  75. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/parsers/unique_bud.py +0 -0
  76. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/parsers/wavelength.py +0 -0
  77. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tasks/__init__.py +0 -0
  78. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tasks/assemble_movie.py +0 -0
  79. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tasks/base.py +0 -0
  80. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tasks/l1_output_data.py +0 -0
  81. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tasks/mixin/__init__.py +0 -0
  82. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tasks/mixin/globus.py +0 -0
  83. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tasks/mixin/interservice_bus.py +0 -0
  84. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tasks/mixin/metadata_store.py +0 -0
  85. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tasks/mixin/object_store.py +0 -0
  86. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tasks/mixin/quality/__init__.py +0 -0
  87. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tasks/output_data_base.py +0 -0
  88. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tasks/parse_l0_input_data.py +0 -0
  89. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tasks/quality_metrics.py +0 -0
  90. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tasks/teardown.py +0 -0
  91. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tasks/transfer_input_data.py +0 -0
  92. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tasks/trial_catalog.py +0 -0
  93. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tasks/trial_output_data.py +0 -0
  94. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tasks/write_extra.py +0 -0
  95. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tasks/write_l1.py +0 -0
  96. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tasks/write_l1_base.py +0 -0
  97. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tests/__init__.py +0 -0
  98. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tests/conftest.py +0 -0
  99. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tests/mock_metadata_store.py +0 -0
  100. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tests/test_assemble_movie.py +0 -0
  101. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tests/test_base.py +0 -0
  102. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tests/test_codecs.py +0 -0
  103. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tests/test_constants.py +0 -0
  104. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tests/test_construct_dataset_extras.py +0 -0
  105. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tests/test_cs_step.py +0 -0
  106. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tests/test_dkist_location.py +0 -0
  107. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tests/test_fits_access.py +0 -0
  108. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tests/test_flower_pot.py +0 -0
  109. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tests/test_fried_parameter.py +0 -0
  110. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tests/test_input_dataset.py +0 -0
  111. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tests/test_interservice_bus.py +0 -0
  112. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tests/test_interservice_bus_mixin.py +0 -0
  113. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tests/test_manual_processing.py +0 -0
  114. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tests/test_output_data_base.py +0 -0
  115. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tests/test_parameters.py +0 -0
  116. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tests/test_parse_l0_input_data.py +0 -0
  117. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tests/test_publish_catalog_messages.py +0 -0
  118. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tests/test_quality.py +0 -0
  119. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tests/test_scratch.py +0 -0
  120. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tests/test_stems.py +0 -0
  121. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tests/test_submit_dataset_metadata.py +0 -0
  122. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tests/test_tags.py +0 -0
  123. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tests/test_task_name.py +0 -0
  124. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tests/test_task_parsing.py +0 -0
  125. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tests/test_teardown.py +0 -0
  126. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tests/test_transfer_input_data.py +0 -0
  127. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tests/test_transfer_l1_output_data.py +0 -0
  128. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tests/test_trial_catalog.py +0 -0
  129. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tests/test_trial_output_data.py +0 -0
  130. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tests/test_workflow_task_base.py +0 -0
  131. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common/tests/test_write_l1.py +0 -0
  132. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common.egg-info/SOURCES.txt +0 -0
  133. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common.egg-info/dependency_links.txt +0 -0
  134. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common.egg-info/requires.txt +0 -0
  135. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common.egg-info/scm_file_list.json +0 -0
  136. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/dkist_processing_common.egg-info/top_level.txt +0 -0
  137. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/docs/Makefile +0 -0
  138. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/docs/changelog.rst +0 -0
  139. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/docs/conf.py +0 -0
  140. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/docs/index.rst +0 -0
  141. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/docs/landing_page.rst +0 -0
  142. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/docs/make.bat +0 -0
  143. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/docs/requirements.txt +0 -0
  144. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/licenses/LICENSE.rst +0 -0
  145. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/pyproject.toml +0 -0
  146. {dkist_processing_common-14.3.6 → dkist_processing_common-14.4.0}/setup.cfg +0 -0
@@ -1,3 +1,12 @@
1
+ v14.4.0 (2026-08-25)
2
+ ====================
3
+
4
+ Misc
5
+ ----
6
+
7
+ - Convert wavelength calibration quality metric generation to the new storage-based framework. (`#344 <https://bitbucket.org/dkistdc/dkist-processing-common/pull-requests/344>`__)
8
+
9
+
1
10
  v14.3.6 (2026-08-14)
2
11
  ====================
3
12
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dkist-processing-common
3
- Version: 14.3.6
3
+ Version: 14.4.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
@@ -1,6 +1,7 @@
1
1
  """Support classes used to create a quality report."""
2
2
 
3
3
  from datetime import datetime
4
+ from numbers import Real
4
5
  from typing import Annotated
5
6
  from typing import Any
6
7
 
@@ -181,20 +182,50 @@ DatetimeIsoDict = Annotated[
181
182
  ]
182
183
 
183
184
 
184
- class TimeSeriesData(QualityModel):
185
+ def as_xy_dict(input_value: Any) -> dict[str, Any]:
185
186
  """
186
- Time Series Data.
187
+ If required, transform json input from list[list[Any]] to dict[Any, Any].
187
188
 
188
- The context, e.g. task_type or modstate, is maintained outside of this structure.
189
+ The nominal input dict is dict[str, tuple[Any, ...]].
190
+ If already a dict, pass back the dict as is.
191
+ If a list or tuple, attempt to extract x_values and y_values.
192
+ """
193
+ # it's already a dict, no translation
194
+ if isinstance(input_value, dict):
195
+ return input_value
196
+
197
+ # translate to a dict when appropriate
198
+ if (
199
+ isinstance(input_value, (list, tuple))
200
+ and len(input_value) == 2
201
+ and isinstance(input_value[0], (list, tuple))
202
+ and isinstance(input_value[1], (list, tuple))
203
+ ):
204
+ return {
205
+ "x_values": input_value[0],
206
+ "y_values": input_value[1],
207
+ }
208
+
209
+ # not a dict and not recognized as something that could be a dict
210
+ # pass back the input value for probable pydantic framework failure
211
+ return input_value
212
+
213
+
214
+ class SeriesData(QualityModel):
215
+ """
216
+ Series Data where the data type of the x-axis unspecified.
217
+
218
+ This class is intended to be used as a base class.
219
+ It is not intended for direct implementation primarily because the data type of the x-axis is unspecified.
189
220
  """
190
221
 
191
222
  # by default, a tuple accepts a list as input
192
223
  # the use of tuples is intentional because list mutations do not trigger validation
193
- # the impact is that TimeSeriesData should not be instantiated until
224
+ # the impact is that SeriesData should not be instantiated until
194
225
  # after the tuple input has been fully accumulated
195
226
  # when `mode="json"`, the output of a tuple results in a list
196
227
  # when `mode="python"`, the output of a tuple results in a tuple
197
- x_values: tuple[DatetimeIsoDict, ...]
228
+ x_values: tuple[Any, ...]
198
229
  # NOTE: FiniteFloat will fail fast for `nan` and `inf`
199
230
  y_values: tuple[FiniteFloat, ...]
200
231
 
@@ -204,14 +235,14 @@ class TimeSeriesData(QualityModel):
204
235
  axes_are_different_lengths = len(self.x_values) != len(self.y_values)
205
236
  if axes_are_different_lengths:
206
237
  raise ValueError(
207
- f"Cannot store TimeSeriesData with different length axes. "
238
+ f"Cannot store {self.__class__.__name__} with different length axes. "
208
239
  f"{len(self.x_values)=}, {len(self.y_values)=}"
209
240
  )
210
241
  # Can't have this AND default to empty tuple
211
242
  axes_are_zero_length = not self.x_values or not self.y_values
212
243
  if axes_are_zero_length:
213
244
  raise ValueError(
214
- f"Cannot store TimeSeriesData with 0 length axes. "
245
+ f"Cannot store {self.__class__.__name__} with 0 length axes. "
215
246
  f"{len(self.x_values)=}, {len(self.y_values)=}"
216
247
  )
217
248
  return self
@@ -222,30 +253,80 @@ class TimeSeriesData(QualityModel):
222
253
  data = handler(self)
223
254
  # change structure if mode="json" for QRM compatibility
224
255
  if info.mode == "json":
225
- # dump as list[list[Any]] with DatetimeIsoDict format for x_values
256
+ # dump as list[list[Any]] with the specified format for x_values
226
257
  return [list(data["x_values"]), list(data["y_values"])]
227
258
  return data
228
259
 
229
260
  @model_validator(mode="before")
230
261
  @classmethod
231
262
  def handle_json(cls, input_value: Any):
232
- """
233
- Transform json input from list[list[Any]] to dict[Any, Any].
263
+ """Transform json input from list[list[Any]] to dict[Any, Any]."""
264
+ return as_xy_dict(input_value)
234
265
 
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
266
 
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]}
267
+ class TimeSeriesData(SeriesData):
268
+ """
269
+ Time Series Data.
247
270
 
248
- return input_value
271
+ The context, e.g. task_type or modstate, is maintained outside of this structure.
272
+ """
273
+
274
+ # by default, a tuple accepts a list as input
275
+ # the use of tuples is intentional because list mutations do not trigger validation
276
+ # the impact is that TimeSeriesData should not be instantiated until
277
+ # after the tuple input has been fully accumulated
278
+ # when `mode="json"`, the output of a tuple results in a list
279
+ # when `mode="python"`, the output of a tuple results in a tuple
280
+ x_values: tuple[DatetimeIsoDict, ...]
281
+
282
+
283
+ class FloatSeriesData(SeriesData):
284
+ """
285
+ Float Series Data.
286
+
287
+ The context, e.g. task_type or modstate, is maintained outside of this structure.
288
+ """
289
+
290
+ # by default, a tuple accepts a list as input
291
+ # the use of tuples is intentional because list mutations do not trigger validation
292
+ # the impact is that FloatSeriesData should not be instantiated until
293
+ # after the tuple input has been fully accumulated
294
+ # when `mode="json"`, the output of a tuple results in a list
295
+ # when `mode="python"`, the output of a tuple results in a tuple
296
+ # NOTE: FiniteFloat will fail fast for `nan` and `inf`
297
+ x_values: tuple[FiniteFloat, ...]
298
+
299
+
300
+ def float_or_time_series_data(input_value: Any) -> Any:
301
+ """
302
+ Identify the input value as FloatSeriesData or TimeSeriesData.
303
+
304
+ This supports reading data from a json encoded file.
305
+ A float is valid for datetime input, which would lead to non-deterministic behavior.
306
+ If the x_values appear to be float values, then return a FloatSeriesData instance.
307
+ Otherwise, return a TimeSeriesData instance.
308
+ """
309
+ xy_dict = as_xy_dict(input_value)
310
+ if not isinstance(xy_dict, dict):
311
+ # pass back the input value for probable pydantic framework failure
312
+ return xy_dict
313
+
314
+ x_values = xy_dict.get("x_values", ())
315
+
316
+ first_x = next((x for x in x_values if x is not None), None)
317
+
318
+ if isinstance(first_x, Real):
319
+ # the decision was made based on the first x value
320
+ # now validate the data types of all x_values and y_values
321
+ return FloatSeriesData.model_validate(xy_dict)
322
+
323
+ return TimeSeriesData.model_validate(xy_dict)
324
+
325
+
326
+ FloatOrTimeSeriesData = Annotated[
327
+ FloatSeriesData | TimeSeriesData,
328
+ BeforeValidator(float_or_time_series_data),
329
+ ]
249
330
 
250
331
 
251
332
  class XYData(QualityModel):
@@ -255,7 +336,7 @@ class XYData(QualityModel):
255
336
  ylabel: str
256
337
  # the key is the name assigned to the series, e.g. within the legend
257
338
  # the key cannot be None, because `null` is not a valid json key
258
- series_data: dict[str, TimeSeriesData] = Field(default_factory=dict)
339
+ series_data: dict[str, FloatOrTimeSeriesData] = Field(default_factory=dict)
259
340
  # TODO FUTURE - convert `series_name` to `legend_title` to better align with matplotlib terminology
260
341
  series_name: str | None = None
261
342
  ylabel_horizontal: bool | None = None
@@ -299,10 +380,45 @@ class TableData(QualityModel):
299
380
 
300
381
 
301
382
  class MultiPlotData(QualityModel):
302
- """Multiple Plot Data."""
383
+ """
384
+ Support class to hold a multi-pane plot with plots stacked vertically.
303
385
 
304
- # TBD
305
- pass
386
+ This type of metric is really geared towards plots that share an X axis and have no gap between them. If you just
387
+ want two separate plots it's probably better to use a list of `XYData` objects.
388
+ """
389
+
390
+ top_to_bottom_plot_list: list[XYData]
391
+ match_x_axes: bool = True
392
+ no_gap: bool = True
393
+ top_to_bottom_height_ratios: list[float] | None = None
394
+
395
+ @field_validator("top_to_bottom_height_ratios")
396
+ @classmethod
397
+ def ensure_same_number_of_height_ratios_and_plots(
398
+ cls, height_ratios: list[float] | None, info: ValidationInfo
399
+ ) -> list[float]:
400
+ """
401
+ Make sure that the number of height ratios is the same as the number of plots.
402
+
403
+ Also populates default, same-size ratios if no ratios were given.
404
+ """
405
+ try:
406
+ plot_list = info.data["top_to_bottom_plot_list"]
407
+ except KeyError:
408
+ # The plot list didn't validate for some reason. We're about to error anyway.
409
+ return [1.0]
410
+
411
+ num_plots = len(plot_list)
412
+ if height_ratios is None:
413
+ return [1.0] * num_plots
414
+
415
+ if len(height_ratios) != num_plots:
416
+ raise ValueError(
417
+ f"The number of items in `top_to_bottom_height_ratios` list ({len(height_ratios)}) is not "
418
+ f"the same as the number of plots ({num_plots})"
419
+ )
420
+
421
+ return height_ratios
306
422
 
307
423
 
308
424
  class HistogramData(QualityModel):
@@ -357,10 +473,11 @@ class QualityMetric(QualityModel):
357
473
  facet: QualityFacet = None
358
474
  statement: list[str] | None = None
359
475
  plot_data: list[XYData] | None = None
360
- multi_plot_data: list[MultiPlotData] | None = None
476
+ # NOTE: specifically NOT a list
477
+ multi_plot_data: MultiPlotData | None = None
361
478
  histogram_data: list[HistogramData] | None = None
362
479
  table_data: list[TableData] | None = None
363
- modmad_data: list[ModMatData] | None = None
480
+ modmat_data: list[ModMatData] | None = None
364
481
  raincloud_data: list[RaincloudData] | None = None
365
482
  efficiency_data: list[EfficiencyData] | None = None
366
483
  warnings: list[str] | None = None
@@ -0,0 +1,218 @@
1
+ """Base QualityMixin class that contains machinery common to all metric types."""
2
+
3
+ from typing import Iterable
4
+
5
+ import astropy.units as u
6
+ import numpy as np
7
+ from solar_wavelength_calibration.fitter.wavelength_fitter import FitResult
8
+
9
+ from dkist_processing_common.codecs.basemodel import basemodel_encoder
10
+ from dkist_processing_common.codecs.json import json_encoder
11
+ from dkist_processing_common.codecs.quality import QualityValueEncoder
12
+ from dkist_processing_common.models.metric_code import MetricCode
13
+ from dkist_processing_common.models.quality import FloatSeriesData
14
+ from dkist_processing_common.models.quality import MultiPlotData
15
+ from dkist_processing_common.models.quality import QualityMetric
16
+ from dkist_processing_common.models.quality import XYData
17
+ from dkist_processing_common.models.tags import Tag
18
+ from dkist_processing_common.tasks.mixin.quality._metrics import _PolcalQualityMixin
19
+
20
+
21
+ class QualityMixin(
22
+ _PolcalQualityMixin,
23
+ ):
24
+ """Mixin class supporting the generation of the quality reports."""
25
+
26
+ def quality_assemble_data(self, polcal_label_list: list[str] | None = None) -> list[dict]:
27
+ """Assemble the quality data by checking for the existence of each metric."""
28
+ report = []
29
+
30
+ polcal_labels = polcal_label_list or []
31
+ report += self.quality_polcal_metrics(polcal_labels)
32
+
33
+ return report
34
+
35
+ def quality_polcal_metrics(self, label_list: list) -> list[dict]:
36
+ """Encapsulate polcal metric parsing."""
37
+ result = []
38
+ for metric_name, metric_func in self.quality_metrics_polcal.items():
39
+ for label in label_list:
40
+ if self._quality_metric_exists(metric_name=metric_name, task_type=label):
41
+ result.append(metric_func(label=label))
42
+
43
+ return result
44
+
45
+ @property
46
+ def quality_metrics_polcal(self) -> dict:
47
+ """Return a dict of polcal quality metrics."""
48
+ return {
49
+ "POLCAL_CONSTANT_PAR_VALS": self.quality_build_polcal_constant_parameter_values,
50
+ "POLCAL_GLOBAL_PAR_VALS": self.quality_build_polcal_global_parameter_values,
51
+ "POLCAL_LOCAL_PAR_VALS": self.quality_build_polcal_local_parameter_values,
52
+ "POLCAL_FIT_RESIDUALS": self.quality_build_polcal_fit_residuals,
53
+ "POLCAL_EFFICIENCY": self.quality_build_polcal_efficiency,
54
+ }
55
+
56
+ def _quality_metric_exists(self, metric_name: str, task_type: str = None) -> bool:
57
+ """Look for the existence of data on disk for a quality metric."""
58
+ tags = [Tag.quality(quality_metric=metric_name)]
59
+ if task_type:
60
+ tags.append(Tag.quality_task(quality_task_type=task_type))
61
+ try:
62
+ next(self.read(tags=tags))
63
+ return True
64
+ except StopIteration:
65
+ return False
66
+
67
+ @staticmethod
68
+ def _format_warnings(warnings: list[str] | None) -> list[str] | None:
69
+ """If warnings is an empty list, change its value to None."""
70
+ return warnings or None
71
+
72
+ def _record_values(self, values, tags: Iterable[str] | str):
73
+ """Serialize and store distributed quality report values for."""
74
+ self.write(
75
+ data=values, tags=tags, encoder=json_encoder, allow_nan=False, cls=QualityValueEncoder
76
+ )
77
+
78
+ def quality_metrics_wavecal(
79
+ self,
80
+ *,
81
+ input_wavelength: u.Quantity,
82
+ input_spectrum: np.ndarray,
83
+ fit_result: FitResult,
84
+ weights: None | np.ndarray = None,
85
+ ):
86
+ """
87
+ Store the results of a wavelength solution fit.
88
+
89
+ Namely, save the:
90
+
91
+ * Input spectrum and wavelength
92
+ * Best-fit combined atlas spectrum
93
+ * Best-fit wavelength vector
94
+ * Fit residuals
95
+
96
+ Note that the residuals are the *unweighed* residuals.
97
+ """
98
+ weight_data = np.ones(input_wavelength.size) if weights is None else weights
99
+ prepared_weights = fit_result.prepared_weights
100
+ residuals = fit_result.minimizer_result.residual / prepared_weights
101
+ residuals[~np.isfinite(residuals)] = 0.0
102
+ normalized_residuals = residuals / input_spectrum
103
+
104
+ best_fit_atlas = fit_result.best_fit_atlas
105
+ best_fit_wavelength = fit_result.best_fit_wavelength_vector
106
+
107
+ finite_idx = (
108
+ np.isfinite(input_wavelength)
109
+ * np.isfinite(input_spectrum)
110
+ * np.isfinite(best_fit_wavelength)
111
+ * np.isfinite(best_fit_atlas)
112
+ * np.isfinite(normalized_residuals)
113
+ * np.isfinite(weight_data)
114
+ )
115
+
116
+ input_wave_list = input_wavelength.to_value(u.nm)[finite_idx].tolist()
117
+ input_spectrum_list = input_spectrum[finite_idx].tolist()
118
+ best_fit_wave_list = best_fit_wavelength[finite_idx].tolist()
119
+ best_fit_atlas_list = best_fit_atlas[finite_idx].tolist()
120
+ residuals_list = normalized_residuals[finite_idx].tolist()
121
+ weights = None if weights is None else weight_data[finite_idx].tolist()
122
+
123
+ fit_series = {
124
+ "Best Fit Observations": FloatSeriesData(
125
+ x_values=tuple(best_fit_wave_list),
126
+ y_values=tuple(input_spectrum_list),
127
+ ),
128
+ "Input Spectrum": FloatSeriesData(
129
+ x_values=tuple(input_wave_list),
130
+ y_values=tuple(input_spectrum_list),
131
+ ),
132
+ "Best Fit Atlas": FloatSeriesData(
133
+ x_values=tuple(best_fit_wave_list),
134
+ y_values=tuple(best_fit_atlas_list),
135
+ ),
136
+ }
137
+
138
+ # Set the colors and zorder here manually because the JSON-ization of the quality data means we can't be sure of
139
+ # the order these will be plotted in and thus can't rely on the default color-cycler in `dkist-quality`.
140
+ fit_plot_kwargs = {
141
+ "Best Fit Observations": {
142
+ "ls": "-",
143
+ "lw": 4,
144
+ "alpha": 0.8,
145
+ "ms": 0,
146
+ "color": "#FAA61C",
147
+ "zorder": 2,
148
+ },
149
+ "Input Spectrum": {"ls": "-", "alpha": 0.4, "ms": 0, "color": "#1E317A", "zorder": 2.1},
150
+ "Best Fit Atlas": {"color": "k", "ls": "-", "ms": 0, "zorder": 2.2},
151
+ }
152
+
153
+ fit_plot = XYData(
154
+ xlabel="Wavelength [nm]",
155
+ ylabel="Signal",
156
+ series_data=fit_series,
157
+ plot_kwargs=fit_plot_kwargs,
158
+ sort_series=False,
159
+ )
160
+
161
+ residuals_series = {
162
+ "Residuals": FloatSeriesData(
163
+ x_values=tuple(best_fit_wave_list),
164
+ y_values=tuple(residuals_list),
165
+ )
166
+ }
167
+ residuals_plot_kwargs = {"Residuals": {"ls": "-", "color": "k", "ms": 0}}
168
+
169
+ y_min = float(np.nanpercentile(residuals_list, 2))
170
+ y_max = float(np.nanpercentile(residuals_list, 98))
171
+ y_range = y_max - y_min
172
+ y_min -= 0.1 * y_range
173
+ y_max += 0.1 * y_range
174
+
175
+ residuals_plot = XYData(
176
+ xlabel="Wavelength [nm]",
177
+ ylabel=r"$\frac{\mathrm{Obs - Atlas}}{\mathrm{Obs}}$",
178
+ series_data=residuals_series,
179
+ plot_kwargs=residuals_plot_kwargs,
180
+ ylim=(y_min, y_max),
181
+ )
182
+
183
+ plot_list = [fit_plot, residuals_plot]
184
+ height_ratios = [1.5, 1.0]
185
+ if weights is not None:
186
+ weight_series = {
187
+ "Weights": FloatSeriesData(
188
+ x_values=tuple(best_fit_wave_list),
189
+ y_values=tuple(weights),
190
+ )
191
+ }
192
+ weight_plot_kwargs = {"Weights": {"ls": "-", "color": "k", "ms": 0}}
193
+ weight_plot = XYData(
194
+ xlabel="Wavelength [nm]",
195
+ ylabel="Fit Weights",
196
+ series_data=weight_series,
197
+ plot_kwargs=weight_plot_kwargs,
198
+ )
199
+ plot_list.append(weight_plot)
200
+ height_ratios.append(1.0)
201
+
202
+ full_plot = MultiPlotData(
203
+ top_to_bottom_plot_list=plot_list,
204
+ match_x_axes=True,
205
+ no_gap=True,
206
+ top_to_bottom_height_ratios=height_ratios,
207
+ )
208
+
209
+ metric = QualityMetric(
210
+ name="Wavelength Calibration Results",
211
+ description="These plots show the wavelength solution computed based on fits to a Solar FTS atlas. "
212
+ "The top plot shows the input and best-fit spectra along with the best-fit atlas, which is "
213
+ "a combination of Solar and Telluric spectra. The bottom plot shows the fit residuals.",
214
+ metric_code=MetricCode.wavecal_fit,
215
+ multi_plot_data=full_plot,
216
+ )
217
+
218
+ self.write(metric, tags=Tag.quality("GENERIC"), encoder=basemodel_encoder)
@@ -678,144 +678,3 @@ class _PolcalQualityMixin:
678
678
  base_str += " bins."
679
679
 
680
680
  return base_str
681
-
682
-
683
- class _WavecalQualityMixin:
684
- """Mixin class supporting the recording and building of wavecal-related metrics."""
685
-
686
- def quality_store_wavecal_results(
687
- self,
688
- *,
689
- input_wavelength: u.Quantity,
690
- input_spectrum: np.ndarray,
691
- fit_result: FitResult,
692
- weights: None | np.ndarray = None,
693
- ):
694
- """
695
- Store the results of a wavelength solution fit.
696
-
697
- Namely, save the:
698
-
699
- * Input spectrum and wavelength
700
- * Best-fit combined atlas spectrum
701
- * Best-fit wavelength vector
702
- * Fit residuals
703
-
704
- Note that the residuals are the *unweighed* residuals.
705
- """
706
- weight_data = np.ones(input_wavelength.size) if weights is None else weights
707
- prepared_weights = fit_result.prepared_weights
708
- residuals = fit_result.minimizer_result.residual / prepared_weights
709
- residuals[~np.isfinite(residuals)] = 0.0
710
- normalized_residuals = residuals / input_spectrum
711
-
712
- best_fit_atlas = fit_result.best_fit_atlas
713
- best_fit_wavelength = fit_result.best_fit_wavelength_vector
714
-
715
- finite_idx = (
716
- np.isfinite(input_wavelength)
717
- * np.isfinite(input_spectrum)
718
- * np.isfinite(best_fit_wavelength)
719
- * np.isfinite(best_fit_atlas)
720
- * np.isfinite(normalized_residuals)
721
- * np.isfinite(weight_data)
722
- )
723
-
724
- data = {
725
- "input_wavelength_nm": input_wavelength.to_value(u.nm)[finite_idx].tolist(),
726
- "input_spectrum": input_spectrum[finite_idx].tolist(),
727
- "best_fit_wavelength_nm": best_fit_wavelength[finite_idx].tolist(),
728
- "best_fit_atlas": best_fit_atlas[finite_idx].tolist(),
729
- "normalized_residuals": normalized_residuals[finite_idx].tolist(),
730
- "weights": None if weights is None else weight_data[finite_idx].tolist(),
731
- }
732
-
733
- self._record_values(values=data, tags=[Tag.quality(MetricCode.wavecal_fit)])
734
-
735
- def quality_build_wavecal_results(self) -> dict:
736
- """Build a ReportMetric containing a multi-pane plot showing the fit spectra and residuals."""
737
- data = next(self.read(tags=[Tag.quality(MetricCode.wavecal_fit)], decoder=json_decoder))
738
-
739
- input_wave_list = data["input_wavelength_nm"]
740
- input_spectrum_list = data["input_spectrum"]
741
- best_fit_wave_list = data["best_fit_wavelength_nm"]
742
- best_fit_atlas_list = data["best_fit_atlas"]
743
- residuals_list = data["normalized_residuals"]
744
- weights = data["weights"]
745
-
746
- fit_series = {
747
- "Best Fit Observations": [best_fit_wave_list, input_spectrum_list],
748
- "Input Spectrum": [input_wave_list, input_spectrum_list],
749
- "Best Fit Atlas": [best_fit_wave_list, best_fit_atlas_list],
750
- }
751
-
752
- # Set the colors and zorder here manually because the JSON-ization of the quality data means we can't be sure of
753
- # the order these will be plotted in and thus can't rely on the default color-cycler in `dkist-quality`.
754
- fit_plot_kwargs = {
755
- "Best Fit Observations": {
756
- "ls": "-",
757
- "lw": 4,
758
- "alpha": 0.8,
759
- "ms": 0,
760
- "color": "#FAA61C",
761
- "zorder": 2,
762
- },
763
- "Input Spectrum": {"ls": "-", "alpha": 0.4, "ms": 0, "color": "#1E317A", "zorder": 2.1},
764
- "Best Fit Atlas": {"color": "k", "ls": "-", "ms": 0, "zorder": 2.2},
765
- }
766
-
767
- fit_plot = Plot2D(
768
- xlabel="Wavelength [nm]",
769
- ylabel="Signal",
770
- series_data=fit_series,
771
- plot_kwargs=fit_plot_kwargs,
772
- sort_series=False,
773
- )
774
-
775
- residuals_series = {"Residuals": [best_fit_wave_list, residuals_list]}
776
- residuals_plot_kwargs = {"Residuals": {"ls": "-", "color": "k", "ms": 0}}
777
-
778
- y_min = np.nanpercentile(residuals_list, 2)
779
- y_max = np.nanpercentile(residuals_list, 98)
780
- y_range = y_max - y_min
781
- y_min -= 0.1 * y_range
782
- y_max += 0.1 * y_range
783
- residuals_plot = Plot2D(
784
- xlabel="Wavelength [nm]",
785
- ylabel=r"$\frac{\mathrm{Obs - Atlas}}{\mathrm{Obs}}$",
786
- series_data=residuals_series,
787
- plot_kwargs=residuals_plot_kwargs,
788
- ylim=(y_min, y_max),
789
- )
790
-
791
- plot_list = [fit_plot, residuals_plot]
792
- height_ratios = [1.5, 1.0]
793
- if weights is not None:
794
- weight_series = {"Weights": [best_fit_wave_list, weights]}
795
- weight_plot_kwargs = {"Weights": {"ls": "-", "color": "k", "ms": 0}}
796
- weight_plot = Plot2D(
797
- xlabel="Wavelength [nm]",
798
- ylabel="Fit Weights",
799
- series_data=weight_series,
800
- plot_kwargs=weight_plot_kwargs,
801
- )
802
- plot_list.append(weight_plot)
803
- height_ratios.append(1.0)
804
-
805
- full_plot = VerticalMultiPanePlot2D(
806
- top_to_bottom_plot_list=plot_list,
807
- match_x_axes=True,
808
- no_gap=True,
809
- top_to_bottom_height_ratios=height_ratios,
810
- )
811
-
812
- metric = ReportMetric(
813
- name="Wavelength Calibration Results",
814
- description="These plots show the wavelength solution computed based on fits to a Solar FTS atlas. "
815
- "The top plot shows the input and best-fit spectra along with the best-fit atlas, which is "
816
- "a combination of Solar and Telluric spectra. The bottom plot shows the fit residuals.",
817
- metric_code=MetricCode.wavecal_fit,
818
- multi_plot_data=full_plot,
819
- )
820
-
821
- return metric.model_dump()