dkist-processing-common 13.0.2__tar.gz → 13.0.3rc2__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 (142) hide show
  1. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/CHANGELOG.rst +0 -9
  2. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/PKG-INFO +2 -2
  3. dkist_processing_common-13.0.3rc2/changelog/326.feature.rst +1 -0
  4. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/models/quality.py +1 -2
  5. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/parsers/quality.py +2 -2
  6. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tasks/mixin/quality/_base.py +0 -4
  7. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tasks/mixin/quality/_metrics.py +0 -161
  8. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tasks/quality_metrics.py +235 -110
  9. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tests/test_assemble_quality.py +56 -18
  10. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tests/test_quality.py +486 -64
  11. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tests/test_quality_mixin.py +4 -232
  12. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common.egg-info/PKG-INFO +2 -2
  13. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common.egg-info/SOURCES.txt +1 -0
  14. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common.egg-info/requires.txt +1 -1
  15. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/pyproject.toml +1 -1
  16. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/.gitignore +0 -0
  17. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/.pre-commit-config.yaml +0 -0
  18. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/.readthedocs.yml +0 -0
  19. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/.snyk +0 -0
  20. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/README.rst +0 -0
  21. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/bitbucket-pipelines.yml +0 -0
  22. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/changelog/.gitempty +0 -0
  23. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/__init__.py +0 -0
  24. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/_util/__init__.py +0 -0
  25. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/_util/constants.py +0 -0
  26. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/_util/graphql.py +0 -0
  27. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/_util/scratch.py +0 -0
  28. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/_util/tags.py +0 -0
  29. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/codecs/__init__.py +0 -0
  30. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/codecs/array.py +0 -0
  31. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/codecs/asdf.py +0 -0
  32. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/codecs/basemodel.py +0 -0
  33. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/codecs/bytes.py +0 -0
  34. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/codecs/fits.py +0 -0
  35. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/codecs/iobase.py +0 -0
  36. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/codecs/json.py +0 -0
  37. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/codecs/path.py +0 -0
  38. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/codecs/quality.py +0 -0
  39. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/codecs/str.py +0 -0
  40. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/config.py +0 -0
  41. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/fonts/Lato-Regular.ttf +0 -0
  42. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/fonts/__init__.py +0 -0
  43. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/manual.py +0 -0
  44. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/models/__init__.py +0 -0
  45. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/models/constants.py +0 -0
  46. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/models/dkist_location.py +0 -0
  47. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/models/extras.py +0 -0
  48. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/models/fits_access.py +0 -0
  49. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/models/flower_pot.py +0 -0
  50. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/models/fried_parameter.py +0 -0
  51. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/models/graphql.py +0 -0
  52. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/models/input_dataset.py +0 -0
  53. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/models/message.py +0 -0
  54. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/models/message_queue_binding.py +0 -0
  55. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/models/metric_code.py +0 -0
  56. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/models/parameters.py +0 -0
  57. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/models/tags.py +0 -0
  58. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/models/task_name.py +0 -0
  59. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/models/telemetry.py +0 -0
  60. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/models/wavelength.py +0 -0
  61. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/parsers/__init__.py +0 -0
  62. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/parsers/average_bud.py +0 -0
  63. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/parsers/cs_step.py +0 -0
  64. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/parsers/dsps_repeat.py +0 -0
  65. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/parsers/experiment_id_bud.py +0 -0
  66. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/parsers/id_bud.py +0 -0
  67. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/parsers/l0_fits_access.py +0 -0
  68. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/parsers/l1_fits_access.py +0 -0
  69. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/parsers/lookup_bud.py +0 -0
  70. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/parsers/near_bud.py +0 -0
  71. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/parsers/observing_program_id_bud.py +0 -0
  72. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/parsers/proposal_id_bud.py +0 -0
  73. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/parsers/retarder.py +0 -0
  74. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/parsers/single_value_single_key_flower.py +0 -0
  75. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/parsers/task.py +0 -0
  76. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/parsers/time.py +0 -0
  77. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/parsers/unique_bud.py +0 -0
  78. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/parsers/wavelength.py +0 -0
  79. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tasks/__init__.py +0 -0
  80. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tasks/assemble_movie.py +0 -0
  81. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tasks/base.py +0 -0
  82. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tasks/l1_output_data.py +0 -0
  83. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tasks/mixin/__init__.py +0 -0
  84. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tasks/mixin/globus.py +0 -0
  85. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tasks/mixin/interservice_bus.py +0 -0
  86. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tasks/mixin/metadata_store.py +0 -0
  87. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tasks/mixin/object_store.py +0 -0
  88. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tasks/mixin/quality/__init__.py +0 -0
  89. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tasks/output_data_base.py +0 -0
  90. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tasks/parse_l0_input_data.py +0 -0
  91. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tasks/teardown.py +0 -0
  92. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tasks/transfer_input_data.py +0 -0
  93. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tasks/trial_catalog.py +0 -0
  94. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tasks/trial_output_data.py +0 -0
  95. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tasks/write_extra.py +0 -0
  96. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tasks/write_l1.py +0 -0
  97. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tasks/write_l1_base.py +0 -0
  98. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tests/__init__.py +0 -0
  99. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tests/conftest.py +0 -0
  100. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tests/mock_metadata_store.py +0 -0
  101. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tests/test_assemble_movie.py +0 -0
  102. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tests/test_base.py +0 -0
  103. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tests/test_codecs.py +0 -0
  104. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tests/test_constants.py +0 -0
  105. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tests/test_construct_dataset_extras.py +0 -0
  106. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tests/test_cs_step.py +0 -0
  107. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tests/test_dkist_location.py +0 -0
  108. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tests/test_fits_access.py +0 -0
  109. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tests/test_flower_pot.py +0 -0
  110. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tests/test_fried_parameter.py +0 -0
  111. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tests/test_input_dataset.py +0 -0
  112. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tests/test_interservice_bus.py +0 -0
  113. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tests/test_interservice_bus_mixin.py +0 -0
  114. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tests/test_manual_processing.py +0 -0
  115. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tests/test_output_data_base.py +0 -0
  116. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tests/test_parameters.py +0 -0
  117. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tests/test_parse_l0_input_data.py +0 -0
  118. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tests/test_publish_catalog_messages.py +0 -0
  119. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tests/test_scratch.py +0 -0
  120. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tests/test_stems.py +0 -0
  121. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tests/test_submit_dataset_metadata.py +0 -0
  122. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tests/test_tags.py +0 -0
  123. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tests/test_task_name.py +0 -0
  124. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tests/test_task_parsing.py +0 -0
  125. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tests/test_teardown.py +0 -0
  126. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tests/test_transfer_input_data.py +0 -0
  127. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tests/test_transfer_l1_output_data.py +0 -0
  128. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tests/test_trial_catalog.py +0 -0
  129. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tests/test_trial_output_data.py +0 -0
  130. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tests/test_workflow_task_base.py +0 -0
  131. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common/tests/test_write_l1.py +0 -0
  132. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common.egg-info/dependency_links.txt +0 -0
  133. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/dkist_processing_common.egg-info/top_level.txt +0 -0
  134. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/docs/Makefile +0 -0
  135. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/docs/changelog.rst +0 -0
  136. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/docs/conf.py +0 -0
  137. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/docs/index.rst +0 -0
  138. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/docs/landing_page.rst +0 -0
  139. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/docs/make.bat +0 -0
  140. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/docs/requirements.txt +0 -0
  141. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/licenses/LICENSE.rst +0 -0
  142. {dkist_processing_common-13.0.2 → dkist_processing_common-13.0.3rc2}/setup.cfg +0 -0
@@ -1,12 +1,3 @@
1
- v13.0.2 (2026-04-08)
2
- ====================
3
-
4
- Misc
5
- ----
6
-
7
- - Upgrade `dkist-processing-core` to version 7.2.0 which includes an `apache-airflow` version upgrade to 3.2.0. (`#325 <https://bitbucket.org/dkistdc/dkist-processing-common/pull-requests/325>`__)
8
-
9
-
10
1
  v13.0.1 (2026-04-07)
11
2
  ====================
12
3
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dkist-processing-common
3
- Version: 13.0.2
3
+ Version: 13.0.3rc2
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
@@ -17,7 +17,7 @@ Requires-Dist: asdf<4.0.0,>=3.5.0
17
17
  Requires-Dist: astropy>=7.0.0
18
18
  Requires-Dist: dkist-fits-specifications<5.0,>=4.24.0
19
19
  Requires-Dist: dkist-header-validator<6.0,>=5.3.0
20
- Requires-Dist: dkist-processing-core==7.2.0
20
+ Requires-Dist: dkist-processing-core==7.1.0
21
21
  Requires-Dist: dkist-processing-pac<4.0,>=3.1
22
22
  Requires-Dist: dkist-service-configuration<5.0,>=4.3.0
23
23
  Requires-Dist: dkist-spectral-lines<4.0,>=3.0.0
@@ -0,0 +1 @@
1
+ Simplify the production of L1 quality metrics.
@@ -13,7 +13,6 @@ from pydantic import PlainSerializer
13
13
  from pydantic import field_validator
14
14
  from pydantic import model_serializer
15
15
  from pydantic import model_validator
16
- from pydantic.alias_generators import to_camel
17
16
  from pydantic_core.core_schema import SerializationInfo
18
17
  from pydantic_core.core_schema import ValidationInfo
19
18
 
@@ -276,7 +275,7 @@ class TableData(QualityModel):
276
275
  # Note: numeric values are represented as strings for table data
277
276
  rows: tuple[tuple[str, ...], ...]
278
277
  header_row: bool | None = True
279
- header_col: bool | None = False
278
+ header_column: bool | None = False
280
279
 
281
280
  @model_validator(mode="after")
282
281
  def tuple_validation(self):
@@ -31,5 +31,5 @@ class L1QualityFitsAccess(L1FitsAccess):
31
31
  self.date_begin: str = self.header["DATE-BEG"]
32
32
  self.light_level: float = self.header["LIGHTLVL"]
33
33
  self.health_status: str = self.header["DSHEALTH"]
34
- self.ao_status: int = self.header.get("AO_LOCK", None)
35
- self.num_out_of_bounds_ao_values: int = self.header.get("OOBSHIFT", None)
34
+ self.ao_status: int | None = self.header.get("AO_LOCK", None)
35
+ self.num_out_of_bounds_ao_values: int | None = self.header.get("OOBSHIFT", None)
@@ -66,14 +66,10 @@ class QualityMixin(
66
66
  def quality_metrics_no_task_dependence(self) -> dict:
67
67
  """Return a dict of the quality metrics with no task dependence."""
68
68
  return {
69
- "FRIED_PARAMETER": self.quality_build_fried_parameter,
70
- "LIGHT_LEVEL": self.quality_build_light_level,
71
69
  "NOISE": self.quality_build_noise,
72
70
  "SENSITIVITY": self.quality_build_sensitivity,
73
- "HEALTH_STATUS": self.quality_build_health_status,
74
71
  "TASK_TYPES": self.quality_build_task_type_counts,
75
72
  "HISTORICAL": self.quality_build_historical,
76
- "AO_STATUS": self.quality_build_ao_status,
77
73
  "RANGE": self.quality_build_range,
78
74
  "WAVECAL_FIT": self.quality_build_wavecal_results,
79
75
  }
@@ -25,7 +25,6 @@ from pandas import DataFrame
25
25
  from solar_wavelength_calibration.fitter.wavelength_fitter import FitResult
26
26
 
27
27
  from dkist_processing_common.codecs.json import json_decoder
28
- from dkist_processing_common.models.fried_parameter import r0_valid
29
28
  from dkist_processing_common.models.metric_code import MetricCode
30
29
  from dkist_processing_common.models.quality import EfficiencyHistograms
31
30
  from dkist_processing_common.models.quality import ModulationMatrixHistograms
@@ -41,24 +40,6 @@ logger = logging.getLogger(__name__)
41
40
 
42
41
 
43
42
  class _SimpleQualityMixin:
44
- @staticmethod
45
- def _create_statement_metric(
46
- name: str,
47
- description: str,
48
- metric_code: str,
49
- statement: str,
50
- warnings: str | None = None,
51
- facet: str | None = None,
52
- ) -> dict:
53
- metric = ReportMetric(
54
- name=name,
55
- description=description,
56
- metric_code=metric_code,
57
- facet=facet,
58
- statement=statement,
59
- warnings=warnings,
60
- )
61
- return metric.model_dump()
62
43
 
63
44
  def quality_store_range(self, name: str, warnings: list[str]):
64
45
  """
@@ -209,111 +190,6 @@ class _SimplePlotQualityMixin:
209
190
  )
210
191
  return warnings
211
192
 
212
- def quality_store_ao_status_and_fried_parameter(
213
- self, datetimes: list[str], values: list[list[bool | float]]
214
- ):
215
- """
216
- Collect and store datetime / value pairs for the boolean AO status and Fried parameter.
217
-
218
- Store all non-None AO lock status values, but only store Fried parameter values if AO lock status is True.
219
-
220
- Because of how L1Metric.has_metric works, empty lists will not be passed to this method.
221
- However, because of how L1Metric.store_metric works, one or both values can be None.
222
- """
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]
225
- if len(ao_not_none) != 0:
226
- self._record_values(values=ao_not_none, tags=Tag.quality(MetricCode.ao_status))
227
- fried_values = [value[1] for value in values]
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:
241
- self._record_2d_plot_values(
242
- x_values=datetimes_to_plot,
243
- y_values=fried_values_to_plot,
244
- tags=Tag.quality(MetricCode.fried_parameter),
245
- )
246
-
247
- def quality_build_ao_status(self) -> dict:
248
- """
249
- Build ao status schema from stored data.
250
-
251
- Because of how quality_task_independent_metrics in the QualityMixin works, this method is not called if no data is on disk.
252
- """
253
- ao_status = []
254
- # Loop over files that contain data for this metric
255
- for path in self.read(tags=Tag.quality(MetricCode.ao_status)):
256
- with path.open() as f:
257
- ao_status += json.load(f)
258
- percentage = round(100 * np.count_nonzero(ao_status) / len(ao_status), 1)
259
- return self._create_statement_metric(
260
- name="Adaptive Optics Status",
261
- description="This metric shows the percentage of frames in which the adaptive optics "
262
- "system was running and locked",
263
- metric_code=MetricCode.ao_status,
264
- statement=f"The adaptive optics system was running and locked for {percentage}% of the "
265
- f"observed frames",
266
- warnings=None,
267
- )
268
-
269
- def quality_build_fried_parameter(self) -> dict:
270
- """
271
- Build fried parameter schema from stored data.
272
-
273
- Because of how quality_task_independent_metrics in the QualityMixin works, this method is not called if no data is on disk.
274
- """
275
- # Merge all recorded quality values
276
- series_data = self._load_2d_plot_values(tags=Tag.quality(MetricCode.fried_parameter))
277
- values = list(series_data.values())[0][1]
278
- return self._create_2d_plot_with_datetime_metric(
279
- name="Fried Parameter",
280
- description="This metric quantifies the stability of the atmosphere during an "
281
- "observation and directly impacts the data quality through a phenomenon "
282
- "known as atmospheric seeing. One measurement is taken per L1 frame. "
283
- "Only measurements taken while the AO system is locked are valid.",
284
- metric_code=MetricCode.fried_parameter,
285
- xlabel="Time",
286
- ylabel="Fried Parameter (m)",
287
- ylim=(0.0, 0.2),
288
- series_data=series_data,
289
- statement=f"Average valid Fried Parameter measurements for L1 dataset: "
290
- f"{round(np.mean(values), 2)} ± {round(np.std(values), 2)} m",
291
- warnings=None,
292
- )
293
-
294
- def quality_store_light_level(self, datetimes: list[str], values: list[float]):
295
- """Collect and store datetime / value pairs for the light level."""
296
- self._record_2d_plot_values(
297
- x_values=datetimes, y_values=values, tags=Tag.quality(MetricCode.light_level)
298
- )
299
-
300
- def quality_build_light_level(self) -> dict:
301
- """Build light_level schema from stored data."""
302
- series_data = self._load_2d_plot_values(tags=Tag.quality(MetricCode.light_level))
303
- values = list(series_data.values())[0][1]
304
- return self._create_2d_plot_with_datetime_metric(
305
- name="Light Level",
306
- description="The telescope light level, as measured by the Telescope Acquisition Camera, at the start of "
307
- "data acquisition of each frame.",
308
- metric_code=MetricCode.light_level,
309
- xlabel="Time",
310
- ylabel="Light Level (adu)",
311
- series_data=series_data,
312
- statement=f"Average Light Level for L1 dataset: "
313
- f"{round(np.mean(values), 2)} ± {round(np.std(values), 2)} adu",
314
- warnings=None,
315
- )
316
-
317
193
  def quality_store_noise(self, datetimes: list[str], values: list[float], stokes: str = "I"):
318
194
  """Collect and store datetime / value pairs for the noise data."""
319
195
  self._record_2d_plot_values(
@@ -390,43 +266,6 @@ class _TableQualityMixin:
390
266
  )
391
267
  return metric.model_dump()
392
268
 
393
- def quality_store_health_status(self, values: list[str]):
394
- """
395
- Collect and store health status data.
396
-
397
- Parameters
398
- ----------
399
- values: statuses as listed in the headers
400
- """
401
- self._record_values(values=values, tags=Tag.quality(MetricCode.health_status))
402
-
403
- def quality_build_health_status(self) -> dict:
404
- """Build health status schema from stored data."""
405
- values = []
406
- for path in self.read(tags=Tag.quality(MetricCode.health_status)):
407
- with path.open() as f:
408
- data = json.load(f)
409
- values += data
410
- statuses, counts = np.unique(values, return_counts=True)
411
- statuses = [s.lower() for s in statuses]
412
- # JSON serialization does not work with numpy types
413
- counts = [int(c) for c in counts]
414
- warnings = []
415
- if any(s in statuses for s in ["bad", "ill", "unknown"]):
416
- warnings.append(
417
- "Data sourced from components with a health status of 'ill', 'bad', or 'unknown'."
418
- )
419
- table_data = [list(z) for z in zip(statuses, counts)]
420
- table_data.insert(0, ["Status", "Count"])
421
- return self._create_table_metric(
422
- name="Data Source Health",
423
- description="This metric contains the worst health status of the data source during "
424
- "data acquisition. One reading is taken per L1 frame.",
425
- metric_code=MetricCode.health_status,
426
- rows=table_data,
427
- warnings=self._format_warnings(warnings),
428
- )
429
-
430
269
  def quality_store_task_type_counts(
431
270
  self, task_type: str, total_frames: int, frames_not_used: int = 0
432
271
  ):
@@ -3,19 +3,20 @@
3
3
  import logging
4
4
  from collections import defaultdict
5
5
  from datetime import datetime
6
- from inspect import signature
7
6
  from itertools import chain
8
7
  from pathlib import Path
9
- from typing import Callable
10
8
  from typing import Generator
11
9
  from typing import Iterable
12
10
  from typing import Type
13
11
 
14
12
  import numpy as np
15
13
  from pydantic import FiniteFloat
14
+ from pydantic import model_validator
16
15
 
17
16
  from dkist_processing_common.codecs.basemodel import basemodel_encoder
17
+ from dkist_processing_common.codecs.fits import fits_access_decoder
18
18
  from dkist_processing_common.models.fits_access import FitsAccessBase
19
+ from dkist_processing_common.models.fried_parameter import r0_valid
19
20
  from dkist_processing_common.models.metric_code import MetricCode
20
21
  from dkist_processing_common.models.quality import QualityMetric
21
22
  from dkist_processing_common.models.quality import QualityModel
@@ -261,11 +262,6 @@ class QualityL0Metrics(WorkflowTaskBase, QualityMixin):
261
262
  metric_code=MetricCode.dataset_average,
262
263
  table_data=[table_data],
263
264
  )
264
- # NOTE 1: QualityValueEncoder
265
- # NOTE 1: a BaseModel float field automatically converts np.float32 to python float
266
- # NOTE 2: QualityDataEncoder
267
- # NOTE 2: a BaseModel datetime field automatically converts to iso 8601 str with mode="json"
268
- # NOTE 2: a BaseModel FiniteFloat field inherently rejects `nan` and `inf` values
269
265
  self.write(metric, tags=Tag.quality("GENERIC"), encoder=basemodel_encoder)
270
266
 
271
267
  def compute_and_write_dataset_rms(
@@ -291,11 +287,6 @@ class QualityL0Metrics(WorkflowTaskBase, QualityMixin):
291
287
  metric_code=MetricCode.dataset_rms,
292
288
  table_data=[table_data],
293
289
  )
294
- # NOTE 1: QualityValueEncoder
295
- # NOTE 1: a BaseModel float field automatically converts np.float32 to python float
296
- # NOTE 2: QualityDataEncoder
297
- # NOTE 2: a BaseModel datetime field automatically converts to iso 8601 str with mode="json"
298
- # NOTE 2: a BaseModel FiniteFloat field inherently rejects `nan` and `inf` values
299
290
  self.write(metric, tags=Tag.quality("GENERIC"), encoder=basemodel_encoder)
300
291
 
301
292
  def compute_and_write_frame_average(
@@ -341,12 +332,6 @@ class QualityL0Metrics(WorkflowTaskBase, QualityMixin):
341
332
  plot_data=[xy_data],
342
333
  warnings=warnings,
343
334
  )
344
- # NOTE 1: QualityValueEncoder
345
- # NOTE 1: a BaseModel float field automatically converts np.float32 to python float
346
- # NOTE 2: QualityDataEncoder
347
- # NOTE 2: a BaseModel datetime field automatically converts to iso 8601 str with mode="json"
348
- # NOTE 2: a BaseModel FiniteFloat field inherently rejects `nan` and `inf` values
349
- # write one file per task type
350
335
  self.write(metric, tags=Tag.quality("GENERIC"), encoder=basemodel_encoder)
351
336
 
352
337
  def compute_and_write_frame_rms(
@@ -392,112 +377,252 @@ class QualityL0Metrics(WorkflowTaskBase, QualityMixin):
392
377
  plot_data=[xy_data],
393
378
  warnings=warnings,
394
379
  )
395
- # NOTE 1: QualityValueEncoder
396
- # NOTE 1: a BaseModel float field automatically converts np.float32 to python float
397
- # NOTE 2: QualityDataEncoder
398
- # NOTE 2: a BaseModel datetime field automatically converts to iso 8601 str with mode="json"
399
- # NOTE 2: a BaseModel FiniteFloat field inherently rejects `nan` and `inf` values
400
- # write one file per task type
401
380
  self.write(metric, tags=Tag.quality("GENERIC"), encoder=basemodel_encoder)
402
381
 
403
382
 
404
- class L1Metric:
405
- """
406
- Class for collecting L1 quality metric data while frames are being opened before storing on disk.
407
-
408
- Parameters
409
- ----------
410
- storage_method
411
- The callable used to execute the storage
412
- value_source
413
- The source of the value being stored
414
- value_function
415
- The function to return the values
416
- """
383
+ class L1QualityData(QualityModel):
384
+ """Intermediate L1 Quality Data gleaned from collection of L1QualityFitsAccess."""
417
385
 
418
- def __init__(
419
- self,
420
- storage_method: Callable,
421
- value_source: list[str] | str,
422
- value_function: Callable | None = None,
423
- ):
424
- self.storage_method = storage_method
425
- self.value_source = value_source
426
- self.values = []
427
- self.datetimes = []
428
- self.value_function = value_function
429
-
430
- def append_value(self, frame: L1QualityFitsAccess) -> None:
431
- """
432
- Append datetime from the frame to the list of datetimes.
386
+ # by default, a tuple accepts a list as input
387
+ # the use of tuples is intentional because list mutations do not trigger validation
388
+ # the impact is that L1QualityData should not be instantiated until
389
+ # after the tuple input has been fully accumulated
390
+ # when `mode="json"`, the output of a tuple results in a list
391
+ # when `mode="python"`, the output of a tuple results in a tuple
392
+ datetimes: tuple[datetime, ...]
393
+ # NOTE: FiniteFloat will fail fast for `nan` and `inf`
394
+ light_level_values: tuple[FiniteFloat, ...]
395
+ health_status_values: tuple[str, ...]
396
+ # NOTE: FiniteFloat will fail fast for `nan` and `inf`
397
+ fried_parameter_values: tuple[FiniteFloat, ...]
398
+ ao_lock_values: tuple[bool | None, ...]
399
+ ao_oob_values: tuple[int | None, ...]
400
+
401
+ @model_validator(mode="after")
402
+ def tuple_validation(self):
403
+ """Validate tuple lengths."""
404
+ error_message = [f"{len(self.datetimes)=}"]
405
+ datetime_len = len(self.datetimes)
406
+ if len(self.light_level_values) != datetime_len:
407
+ error_message.append(f"{len(self.light_level_values)=}")
408
+ if len(self.health_status_values) != datetime_len:
409
+ error_message.append(f"{len(self.health_status_values)=}")
410
+ if len(self.fried_parameter_values) != datetime_len:
411
+ error_message.append(f"{len(self.fried_parameter_values)=}")
412
+ if len(self.ao_lock_values) != datetime_len:
413
+ error_message.append(f"{len(self.ao_lock_values)=}")
414
+ if len(self.ao_oob_values) != datetime_len:
415
+ error_message.append(f"{len(self.ao_oob_values)=}")
416
+ if len(error_message) > 1:
417
+ raise ValueError(
418
+ f"One or more dimensions have wrong length: {', '.join(error_message)}. "
419
+ )
420
+ return self
433
421
 
434
- If a value_function was provided, apply it to the given source attribute and append to
435
- self.values. Otherwise, append the attribute value itself to self.values. If multiple
436
- sources provided, append a list of the source attributes to self.values.
437
422
 
438
- Parameters
439
- ----------
440
- frame
441
- The input frame
423
+ class QualityL1Metrics(WorkflowTaskBase, QualityMixin):
424
+ """Task class supporting the generation of quality metrics for the L1 data."""
442
425
 
443
- Returns
444
- -------
445
- None
446
- """
447
- self.datetimes.append(frame.time_obs)
448
- if self.value_function:
449
- self.values.append(self.value_function(getattr(frame, self.value_source)))
426
+ def run(self) -> None:
427
+ """Run method for this task."""
428
+ with self.telemetry_span("Reading L1 frames"):
429
+ l1_quality_data = self.collect_l1_quality_data()
430
+
431
+ with self.telemetry_span("Writing L1 quality data"):
432
+ self.compute_and_write_l1_quality_metrics(l1_quality_data)
433
+
434
+ def collect_l1_quality_data(self) -> L1QualityData:
435
+ """Collect data used to compute L1 quality metrics."""
436
+ frames = self.read(
437
+ tags=[Tag.calibrated(), Tag.frame()],
438
+ decoder=fits_access_decoder,
439
+ fits_access_class=L1QualityFitsAccess,
440
+ )
441
+
442
+ # accumulate specified L1QualityFitsAccess values into lists, sorted by datetime
443
+ rows = sorted(
444
+ (
445
+ (
446
+ # DATE-BEG
447
+ datetime.fromisoformat(frame.time_obs),
448
+ # LIGHTLVL
449
+ frame.light_level,
450
+ # DSHEALTH
451
+ frame.health_status,
452
+ # ATMOS_R0
453
+ frame.fried_parameter,
454
+ # AO_LOCK
455
+ frame.ao_status,
456
+ # OOBSHIFT
457
+ frame.num_out_of_bounds_ao_values,
458
+ )
459
+ for frame in frames
460
+ ),
461
+ # DATE-BEG
462
+ key=lambda row: row[0],
463
+ )
464
+
465
+ return L1QualityData(
466
+ datetimes=tuple(row[0] for row in rows),
467
+ light_level_values=tuple(row[1] for row in rows),
468
+ health_status_values=tuple(row[2] for row in rows),
469
+ fried_parameter_values=tuple(row[3] for row in rows),
470
+ ao_lock_values=tuple(row[4] for row in rows),
471
+ ao_oob_values=tuple(row[5] for row in rows),
472
+ )
473
+
474
+ def compute_and_write_l1_quality_metrics(self, l1_quality_data: L1QualityData) -> None:
475
+ """Write L1 metrics to disk."""
476
+ self.compute_and_write_ao_status(l1_quality_data)
477
+ self.compute_and_write_fried_parameter(l1_quality_data)
478
+ self.compute_and_write_health_status(l1_quality_data)
479
+ self.compute_and_write_light_level(l1_quality_data)
480
+
481
+ def compute_and_write_ao_status(self, l1_quality_data: L1QualityData) -> None:
482
+ """Write AO_STATUS quality metric."""
483
+ ao_not_none = [ao for ao in l1_quality_data.ao_lock_values if ao is not None]
484
+
485
+ # don't write anything if there is no data
486
+ if not ao_not_none:
450
487
  return
451
- if isinstance(self.value_source, list):
452
- multiple_values = [getattr(frame, source, None) for source in self.value_source]
453
- self.values.append(multiple_values)
488
+
489
+ percentage = round(100 * np.count_nonzero(ao_not_none) / len(ao_not_none), 1)
490
+
491
+ metric = QualityMetric(
492
+ name="Adaptive Optics Status",
493
+ description="This metric shows the percentage of frames in which the adaptive optics "
494
+ "system was running and locked",
495
+ metric_code=MetricCode.ao_status,
496
+ statement=[
497
+ f"The adaptive optics system was running and locked for {percentage}% of the observed frames"
498
+ ],
499
+ )
500
+
501
+ self.write(metric, tags=Tag.quality("GENERIC"), encoder=basemodel_encoder)
502
+
503
+ def compute_and_write_fried_parameter(self, l1_quality_data: L1QualityData) -> None:
504
+ """Write FRIED_PARAMETER quality metric."""
505
+ datetimes = l1_quality_data.datetimes
506
+ fried_parameter_values = l1_quality_data.fried_parameter_values
507
+ ao_lock_values = l1_quality_data.ao_lock_values
508
+ ao_oob_values = l1_quality_data.ao_oob_values
509
+
510
+ fried_values_to_plot = []
511
+ datetimes_to_plot = []
512
+ # For each set of input data, check if the r0 is considered valid based on all data
513
+ for i in range(len(fried_parameter_values)):
514
+ if r0_valid(
515
+ r0=fried_parameter_values[i],
516
+ ao_lock=ao_lock_values[i],
517
+ num_out_of_bounds_ao_values=ao_oob_values[i],
518
+ ):
519
+ datetimes_to_plot.append(datetimes[i])
520
+ fried_values_to_plot.append(fried_parameter_values[i])
521
+
522
+ if len(fried_values_to_plot) == 0:
523
+ # don't write anything if there is no data
454
524
  return
455
- self.values.append(getattr(frame, self.value_source))
456
525
 
457
- @property
458
- def has_values(self):
459
- return len(self.values) > 0
460
-
461
- def store_metric(self):
462
- """Remove None values from a single-value values list (and also remove corresponding indices from datetimes) then send to the provided storage method."""
463
- # Get indices of non-None values and only use those.
464
- # Use multi-valued lists as is to be handled in the applicable storage_method.
465
- indices = [i for i, val in enumerate(self.values) if val is not None]
466
- d = [self.datetimes[i] for i in indices]
467
- v = [self.values[i] for i in indices]
468
- # Get signature of storage method and call with applicable args
469
- storage_method_sig = signature(self.storage_method)
470
- if storage_method_sig.parameters.get("datetimes", False):
471
- self.storage_method(datetimes=d, values=v)
526
+ time_series = TimeSeriesData(
527
+ x_values=tuple(datetimes_to_plot),
528
+ y_values=tuple(fried_values_to_plot),
529
+ )
530
+
531
+ series_data = {"": time_series}
532
+
533
+ xy_data = XYData(
534
+ xlabel="Time",
535
+ ylabel="Fried Parameter (m)",
536
+ ylim=(0.0, 0.2),
537
+ series_data=series_data,
538
+ )
539
+
540
+ statement = (
541
+ f"Average valid Fried Parameter measurements for L1 dataset: "
542
+ f"{round(np.mean(fried_values_to_plot), 2)} ± {round(np.std(fried_values_to_plot), 2)} m"
543
+ )
544
+
545
+ metric = QualityMetric(
546
+ name="Fried Parameter",
547
+ description="This metric quantifies the stability of the atmosphere during an "
548
+ "observation and directly impacts the data quality through a phenomenon "
549
+ "known as atmospheric seeing. One measurement is taken per L1 frame. "
550
+ "Only measurements taken while the AO system is locked are valid.",
551
+ metric_code=MetricCode.fried_parameter,
552
+ plot_data=[xy_data],
553
+ statement=[statement],
554
+ )
555
+
556
+ self.write(metric, tags=Tag.quality("GENERIC"), encoder=basemodel_encoder)
557
+
558
+ def compute_and_write_health_status(self, l1_quality_data: L1QualityData) -> None:
559
+ """Write HEALTH_STATUS quality metric."""
560
+ # don't need to check for None - l1_quality_data.health_status_values does not allow None
561
+ health_status_values = l1_quality_data.health_status_values
562
+
563
+ # don't write anything if there is no data
564
+ if not health_status_values:
472
565
  return
473
- self.storage_method(values=v)
474
566
 
567
+ statuses, counts = np.unique(health_status_values, return_counts=True)
568
+ # individual elements to lower case, with side effect of switch from numpy.ndarray to list
569
+ statuses = [s.lower() for s in statuses]
570
+ # individual elements from int to str, with side effect of switch from numpy.ndarray to list
571
+ counts = [str(c) for c in counts]
475
572
 
476
- class QualityL1Metrics(WorkflowTaskBase, QualityMixin):
477
- """Task class supporting the generation of quality metrics for the L0 data."""
573
+ header = ("Status", "Count")
574
+ rows = [header]
575
+ rows.extend(tuple(z) for z in zip(statuses, counts))
576
+ table_data = TableData(rows=tuple(rows))
478
577
 
479
- def run(self) -> None:
480
- """Run method for this task."""
481
- metrics = [
482
- L1Metric(storage_method=self.quality_store_light_level, value_source="light_level"),
483
- L1Metric(storage_method=self.quality_store_health_status, value_source="health_status"),
484
- L1Metric(
485
- storage_method=self.quality_store_ao_status_and_fried_parameter,
486
- value_source=["ao_status", "fried_parameter", "num_out_of_bounds_ao_values"],
487
- ),
488
- ]
578
+ metric = QualityMetric(
579
+ name="Data Source Health",
580
+ description="This metric contains the worst health status of the data source during "
581
+ "data acquisition. One reading is taken per L1 frame.",
582
+ metric_code=MetricCode.health_status,
583
+ table_data=[table_data],
584
+ )
489
585
 
490
- with self.telemetry_span("Reading L1 frames"):
491
- paths = list(self.read(tags=[Tag.calibrated(), Tag.frame()]))
586
+ if any(s in statuses for s in ["bad", "ill", "unknown"]):
587
+ metric.warnings = [
588
+ "Data sourced from components with a health status of 'ill', 'bad', or 'unknown'."
589
+ ]
492
590
 
493
- with self.telemetry_span("Calculating L1 quality metrics"):
494
- for metric in metrics:
495
- with self.telemetry_span(f"Calculating L1 metric {metric.value_source}"):
496
- for path in paths:
497
- frame = L1QualityFitsAccess.from_path(path)
498
- metric.append_value(frame=frame)
591
+ self.write(metric, tags=Tag.quality("GENERIC"), encoder=basemodel_encoder)
592
+
593
+ def compute_and_write_light_level(self, l1_quality_data: L1QualityData) -> None:
594
+ """Write LIGHT_LEVEL quality metric."""
595
+ # don't need to check for None - l1_quality_data.light_level_values does not allow None
596
+ datetimes = l1_quality_data.datetimes
597
+ light_level_values = l1_quality_data.light_level_values
598
+
599
+ # don't write anything if there is no data
600
+ if not light_level_values:
601
+ return
602
+
603
+ time_series = TimeSeriesData(
604
+ x_values=datetimes,
605
+ y_values=light_level_values,
606
+ )
607
+
608
+ series_data = {"": time_series}
609
+
610
+ xy_data = XYData(
611
+ xlabel="Time",
612
+ ylabel="Light Level (adu)",
613
+ series_data=series_data,
614
+ )
615
+
616
+ statement = f"Average Light Level for L1 dataset: "
617
+ f"{round(np.mean(light_level_values), 2)} ± {round(np.std(light_level_values), 2)} adu"
499
618
 
500
- with self.telemetry_span("Sending lists for storage"):
501
- for metric in metrics:
502
- if metric.has_values:
503
- metric.store_metric()
619
+ metric = QualityMetric(
620
+ name="Light Level",
621
+ description="The telescope light level, as measured by the Telescope Acquisition Camera, "
622
+ "at the start of data acquisition of each frame.",
623
+ metric_code=MetricCode.light_level,
624
+ plot_data=[xy_data],
625
+ statement=[statement],
626
+ )
627
+
628
+ self.write(metric, tags=Tag.quality("GENERIC"), encoder=basemodel_encoder)