dkist-processing-common 12.6.2__tar.gz → 12.6.3__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-12.6.2 → dkist_processing_common-12.6.3}/CHANGELOG.rst +19 -0
  2. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/PKG-INFO +2 -2
  3. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/parsers/cs_step.py +69 -12
  4. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/parsers/l0_fits_access.py +23 -0
  5. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tasks/write_extra.py +7 -3
  6. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tests/conftest.py +65 -7
  7. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tests/test_construct_dataset_extras.py +13 -0
  8. dkist_processing_common-12.6.3/dkist_processing_common/tests/test_cs_step.py +111 -0
  9. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tests/test_fits_access.py +28 -0
  10. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tests/test_stems.py +25 -15
  11. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common.egg-info/PKG-INFO +2 -2
  12. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common.egg-info/requires.txt +1 -1
  13. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/pyproject.toml +1 -1
  14. dkist_processing_common-12.6.2/dkist_processing_common/tests/test_cs_step.py +0 -76
  15. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/.gitignore +0 -0
  16. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/.pre-commit-config.yaml +0 -0
  17. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/.readthedocs.yml +0 -0
  18. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/.snyk +0 -0
  19. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/README.rst +0 -0
  20. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/bitbucket-pipelines.yml +0 -0
  21. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/changelog/.gitempty +0 -0
  22. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/__init__.py +0 -0
  23. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/_util/__init__.py +0 -0
  24. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/_util/constants.py +0 -0
  25. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/_util/graphql.py +0 -0
  26. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/_util/scratch.py +0 -0
  27. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/_util/tags.py +0 -0
  28. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/codecs/__init__.py +0 -0
  29. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/codecs/array.py +0 -0
  30. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/codecs/asdf.py +0 -0
  31. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/codecs/basemodel.py +0 -0
  32. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/codecs/bytes.py +0 -0
  33. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/codecs/fits.py +0 -0
  34. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/codecs/iobase.py +0 -0
  35. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/codecs/json.py +0 -0
  36. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/codecs/path.py +0 -0
  37. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/codecs/quality.py +0 -0
  38. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/codecs/str.py +0 -0
  39. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/config.py +0 -0
  40. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/fonts/Lato-Regular.ttf +0 -0
  41. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/fonts/__init__.py +0 -0
  42. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/manual.py +0 -0
  43. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/models/__init__.py +0 -0
  44. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/models/constants.py +0 -0
  45. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/models/dkist_location.py +0 -0
  46. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/models/extras.py +0 -0
  47. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/models/fits_access.py +0 -0
  48. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/models/flower_pot.py +0 -0
  49. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/models/fried_parameter.py +0 -0
  50. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/models/graphql.py +0 -0
  51. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/models/input_dataset.py +0 -0
  52. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/models/message.py +0 -0
  53. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/models/message_queue_binding.py +0 -0
  54. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/models/metric_code.py +0 -0
  55. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/models/parameters.py +0 -0
  56. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/models/quality.py +0 -0
  57. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/models/tags.py +0 -0
  58. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/models/task_name.py +0 -0
  59. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/models/telemetry.py +0 -0
  60. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/models/wavelength.py +0 -0
  61. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/parsers/__init__.py +0 -0
  62. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/parsers/average_bud.py +0 -0
  63. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/parsers/dsps_repeat.py +0 -0
  64. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/parsers/experiment_id_bud.py +0 -0
  65. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/parsers/id_bud.py +0 -0
  66. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/parsers/l1_fits_access.py +0 -0
  67. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/parsers/lookup_bud.py +0 -0
  68. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/parsers/near_bud.py +0 -0
  69. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/parsers/observing_program_id_bud.py +0 -0
  70. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/parsers/proposal_id_bud.py +0 -0
  71. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/parsers/quality.py +0 -0
  72. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/parsers/retarder.py +0 -0
  73. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/parsers/single_value_single_key_flower.py +0 -0
  74. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/parsers/task.py +0 -0
  75. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/parsers/time.py +0 -0
  76. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/parsers/unique_bud.py +0 -0
  77. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/parsers/wavelength.py +0 -0
  78. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tasks/__init__.py +0 -0
  79. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tasks/assemble_movie.py +0 -0
  80. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tasks/base.py +0 -0
  81. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tasks/l1_output_data.py +0 -0
  82. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tasks/mixin/__init__.py +0 -0
  83. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tasks/mixin/globus.py +0 -0
  84. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tasks/mixin/interservice_bus.py +0 -0
  85. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tasks/mixin/metadata_store.py +0 -0
  86. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tasks/mixin/object_store.py +0 -0
  87. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tasks/mixin/quality/__init__.py +0 -0
  88. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tasks/mixin/quality/_base.py +0 -0
  89. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tasks/mixin/quality/_metrics.py +0 -0
  90. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tasks/output_data_base.py +0 -0
  91. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tasks/parse_l0_input_data.py +0 -0
  92. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tasks/quality_metrics.py +0 -0
  93. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tasks/teardown.py +0 -0
  94. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tasks/transfer_input_data.py +0 -0
  95. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tasks/trial_catalog.py +0 -0
  96. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tasks/trial_output_data.py +0 -0
  97. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tasks/write_l1.py +0 -0
  98. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tasks/write_l1_base.py +0 -0
  99. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tests/__init__.py +0 -0
  100. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tests/mock_metadata_store.py +0 -0
  101. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tests/test_assemble_movie.py +0 -0
  102. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tests/test_assemble_quality.py +0 -0
  103. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tests/test_base.py +0 -0
  104. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tests/test_codecs.py +0 -0
  105. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tests/test_constants.py +0 -0
  106. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tests/test_dkist_location.py +0 -0
  107. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tests/test_flower_pot.py +0 -0
  108. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tests/test_fried_parameter.py +0 -0
  109. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tests/test_input_dataset.py +0 -0
  110. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tests/test_interservice_bus.py +0 -0
  111. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tests/test_interservice_bus_mixin.py +0 -0
  112. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tests/test_manual_processing.py +0 -0
  113. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tests/test_output_data_base.py +0 -0
  114. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tests/test_parameters.py +0 -0
  115. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tests/test_parse_l0_input_data.py +0 -0
  116. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tests/test_publish_catalog_messages.py +0 -0
  117. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tests/test_quality.py +0 -0
  118. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tests/test_quality_mixin.py +0 -0
  119. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tests/test_scratch.py +0 -0
  120. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tests/test_submit_dataset_metadata.py +0 -0
  121. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tests/test_tags.py +0 -0
  122. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tests/test_task_name.py +0 -0
  123. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tests/test_task_parsing.py +0 -0
  124. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tests/test_teardown.py +0 -0
  125. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tests/test_transfer_input_data.py +0 -0
  126. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tests/test_transfer_l1_output_data.py +0 -0
  127. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tests/test_trial_catalog.py +0 -0
  128. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tests/test_trial_output_data.py +0 -0
  129. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tests/test_workflow_task_base.py +0 -0
  130. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common/tests/test_write_l1.py +0 -0
  131. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common.egg-info/SOURCES.txt +0 -0
  132. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common.egg-info/dependency_links.txt +0 -0
  133. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/dkist_processing_common.egg-info/top_level.txt +0 -0
  134. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/docs/Makefile +0 -0
  135. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/docs/changelog.rst +0 -0
  136. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/docs/conf.py +0 -0
  137. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/docs/index.rst +0 -0
  138. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/docs/landing_page.rst +0 -0
  139. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/docs/make.bat +0 -0
  140. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/docs/requirements.txt +0 -0
  141. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/licenses/LICENSE.rst +0 -0
  142. {dkist_processing_common-12.6.2 → dkist_processing_common-12.6.3}/setup.cfg +0 -0
@@ -1,3 +1,22 @@
1
+ v12.6.3 (2026-02-25)
2
+ ====================
3
+
4
+ Bugfixes
5
+ --------
6
+
7
+ - Fix `CSStepFrameFlower` and `NumFramesPerCSStepBud` to correctly handle the multiple modstates present in each CS step. (`#306 <https://bitbucket.org/dkistdc/dkist-processing-common/pull-requests/306>`__)
8
+ - Add correct HEADVERS, HEAD_URL, and CAL_URL keys to all dataset extras. (`#307 <https://bitbucket.org/dkistdc/dkist-processing-common/pull-requests/307>`__)
9
+
10
+
11
+ Misc
12
+ ----
13
+
14
+ - Add `modulator_state` as an expected property on `L0FitsAccess` to be clear about the expected interface in instrument-specific
15
+ subclasses. In the base `L0FitsAccess` class this property will raise a `NotImplementedError`. (`#306 <https://bitbucket.org/dkistdc/dkist-processing-common/pull-requests/306>`__)
16
+ - Add `CSStepModstate` object, which adds modulation state awareness to the `CSStep` object used in parsing. (`#306 <https://bitbucket.org/dkistdc/dkist-processing-common/pull-requests/306>`__)
17
+ - Move to `dkist-fits-specifications` v4.22.0, which includes HEADVERS in the schema for dataset extras. (`#307 <https://bitbucket.org/dkistdc/dkist-processing-common/pull-requests/307>`__)
18
+
19
+
1
20
  v12.6.2 (2026-02-19)
2
21
  ====================
3
22
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dkist-processing-common
3
- Version: 12.6.2
3
+ Version: 12.6.3
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
@@ -15,7 +15,7 @@ Requires-Python: >=3.13
15
15
  Description-Content-Type: text/x-rst
16
16
  Requires-Dist: asdf<4.0.0,>=3.5.0
17
17
  Requires-Dist: astropy>=7.0.0
18
- Requires-Dist: dkist-fits-specifications<5.0,>=4.21.0
18
+ Requires-Dist: dkist-fits-specifications<5.0,>=4.22.0
19
19
  Requires-Dist: dkist-header-validator<6.0,>=5.3.0
20
20
  Requires-Dist: dkist-processing-core==7.0.2
21
21
  Requires-Dist: dkist-processing-pac<4.0,>=3.1
@@ -7,6 +7,7 @@ from collections import Counter
7
7
  from collections import defaultdict
8
8
  from datetime import datetime
9
9
  from datetime import timezone
10
+ from enum import StrEnum
10
11
  from typing import Type
11
12
 
12
13
  from dkist_processing_common.models.constants import BudName
@@ -27,8 +28,8 @@ class CSStep:
27
28
  This class allows for an easy way to quickly compare fits access objects to determine if they come from the
28
29
  same Calibration Sequence (CS) step. Each step in a CS is defined by the configuration of the GOS, namely the
29
30
  status of the polarizer and retarder (in or out) and the angle of each. Because some CS schemes call for some
30
- GOS configurations to repeat a check is also made against the observation time for each object; the default in
31
- the parameter store defines a maximum difference in time where two exposures are considered different CS steps
31
+ GOS configurations to repeat, a check is also made against the observation time for each object; the value of
32
+ ``max_cs_time_sec`` defines a maximum difference in time where two exposures are considered different CS steps
32
33
  regardless of GOS configuration.
33
34
 
34
35
  This class can also be sorted. In this case, only the observation time is taken into account.
@@ -60,9 +61,10 @@ class CSStep:
60
61
  self.max_cs_time_sec = max_cs_time_sec
61
62
 
62
63
  self.fits_obj_repr = repr(fits_obj)
64
+ self.angle_round_ndigits = angle_round_ndigits
63
65
 
64
66
  def __repr__(self):
65
- return f"{self.__class__.__name__}({self.fits_obj_repr}, max_cs_time_sec={self.max_cs_time_sec!r})"
67
+ return f"{self.__class__.__name__}({self.fits_obj_repr}, max_cs_time_sec={self.max_cs_time_sec!r}, angle_round_ndigits={self.angle_round_ndigits})"
66
68
 
67
69
  def __str__(self):
68
70
  return "CS step with Pol = {}:{}, Ret = {}:{}, Dark = {}. Taken at {}".format(
@@ -76,8 +78,8 @@ class CSStep:
76
78
 
77
79
  def __eq__(self, other: object) -> bool:
78
80
  """Two steps are equal if they have the same GOS configuration and are taken within some package-defined time of each other."""
79
- if not isinstance(other, CSStep):
80
- raise TypeError(f"Cannot compare CSStep with type {type(other)}")
81
+ if not isinstance(other, type(self)):
82
+ raise TypeError(f"Cannot compare {type(self)} with {type(other)}")
81
83
 
82
84
  for item in ["pol_in", "pol_theta", "ret_in", "ret_theta", "dark_in"]:
83
85
  if getattr(self, item) != getattr(other, item):
@@ -98,6 +100,55 @@ class CSStep:
98
100
  return hash((self.pol_in, self.pol_theta, self.ret_in, self.ret_theta, self.dark_in))
99
101
 
100
102
 
103
+ class CSStepModstate(CSStep):
104
+ """
105
+ Subclass of `CSStep` that is also aware of the modulation state associated with a given fits object.
106
+
107
+ This is used for grouping files based on CS step and modstate, which is useful when determining the number of frames
108
+ in a given CS step.
109
+
110
+ The equality and hash functionality of `CSStep` is extended to modulation state, and sorting remains only based on
111
+ observe time.
112
+ """
113
+
114
+ def __init__(
115
+ self,
116
+ fits_obj: L0FitsAccess,
117
+ max_cs_time_sec: float,
118
+ angle_round_ndigits: int = 1,
119
+ ):
120
+ super().__init__(
121
+ fits_obj=fits_obj,
122
+ max_cs_time_sec=max_cs_time_sec,
123
+ angle_round_ndigits=angle_round_ndigits,
124
+ )
125
+ self.modstate = fits_obj.modulator_state
126
+
127
+ def __repr__(self):
128
+ return f"{self.__class__.__name__}({self.fits_obj_repr}, max_cs_time_sec={self.max_cs_time_sec!r}, angle_round_ndigits={self.angle_round_ndigits})"
129
+
130
+ def __str__(self):
131
+ return (
132
+ "CS step with Pol = {}:{}, Ret = {}:{}, Dark = {}. Modstate = {}. Taken at {}".format(
133
+ self.pol_in,
134
+ self.pol_theta,
135
+ self.ret_in,
136
+ self.ret_theta,
137
+ self.dark_in,
138
+ self.modstate,
139
+ self.obs_time.isoformat(),
140
+ )
141
+ )
142
+
143
+ def __eq__(self, other: object) -> bool:
144
+ return super().__eq__(other) and self.modstate == other.modstate
145
+
146
+ def __hash__(self) -> int:
147
+ return hash(
148
+ (self.pol_in, self.pol_theta, self.ret_in, self.ret_theta, self.dark_in, self.modstate)
149
+ )
150
+
151
+
101
152
  class CSStepFlower(Stem):
102
153
  """
103
154
  Identify which CS Step a header belongs to.
@@ -167,7 +218,7 @@ class CSStepFrameFlower(Stem):
167
218
  self.max_cs_step_time_sec = max_cs_step_time_sec
168
219
  self.CS_step_dict = defaultdict(list)
169
220
 
170
- def setter(self, fits_obj: L0FitsAccess) -> CSStep | Type[SpilledDirt]:
221
+ def setter(self, fits_obj: L0FitsAccess) -> CSStepModstate | Type[SpilledDirt]:
171
222
  """
172
223
  Set the CS Step for this fits object.
173
224
 
@@ -183,7 +234,10 @@ class CSStepFrameFlower(Stem):
183
234
  if fits_obj.ip_task_type != "polcal":
184
235
  return SpilledDirt
185
236
 
186
- cs_step = CSStep(fits_obj, max_cs_time_sec=self.max_cs_step_time_sec)
237
+ cs_step = CSStepModstate(
238
+ fits_obj,
239
+ max_cs_time_sec=self.max_cs_step_time_sec,
240
+ )
187
241
  self.CS_step_dict[cs_step].append(cs_step)
188
242
  return cs_step
189
243
 
@@ -204,8 +258,8 @@ class CSStepFrameFlower(Stem):
204
258
  """
205
259
  cs_step_for_key = self.key_to_petal_dict[key]
206
260
 
207
- # Use sorted's `key` here to guarantee we never hit `CSStep.__eq__`, which would consider all of these frames equal
208
- # because they come from the same CS Step.
261
+ # Use sorted's `key` here to guarantee we never hit `CSStepModstate.__eq__`, which would consider all of these
262
+ # frames equal because they come from the same CS Step.
209
263
  frames_in_cs_step_list = sorted(
210
264
  self.CS_step_dict[cs_step_for_key], key=lambda cs: cs.obs_time
211
265
  )
@@ -275,7 +329,7 @@ class NumFramesPerCSStepBud(ListStem):
275
329
  super().__init__(stem_name=BudName.num_frames_per_cs_step)
276
330
  self.max_cs_step_time_sec = max_cs_step_time_sec
277
331
 
278
- def setter(self, fits_obj: L0FitsAccess) -> CSStep | Type[SpilledDirt]:
332
+ def setter(self, fits_obj: L0FitsAccess) -> CSStepModstate | Type[SpilledDirt]:
279
333
  """
280
334
  Return the CS Step for this fits object.
281
335
 
@@ -290,13 +344,16 @@ class NumFramesPerCSStepBud(ListStem):
290
344
  """
291
345
  if fits_obj.ip_task_type != "polcal":
292
346
  return SpilledDirt
293
- return CSStep(fits_obj, max_cs_time_sec=self.max_cs_step_time_sec)
347
+ return CSStepModstate(
348
+ fits_obj,
349
+ max_cs_time_sec=self.max_cs_step_time_sec,
350
+ )
294
351
 
295
352
  def getter(self) -> int:
296
353
  """
297
354
  Return number of frames per CS step.
298
355
 
299
- We assume that each copy of the same `CSStep` object represents a single frame.
356
+ We assume that each copy of the same `CSStepModstate` object represents a single frame.
300
357
 
301
358
  Returns
302
359
  -------
@@ -6,6 +6,27 @@ from dkist_processing_common.models.fits_access import MetadataKey
6
6
  from dkist_processing_common.parsers.l1_fits_access import L1FitsAccess
7
7
 
8
8
 
9
+ class AbstractProperty:
10
+ """
11
+ A class for defining properties expected to exist in subclasses, but that don't exist on the current class.
12
+
13
+ Use like this
14
+
15
+ .. code::
16
+
17
+ class BaseClass:
18
+ required_property = AbstractProperty()
19
+
20
+ Then accessing `BaseClass().required_property` will result in a `NotImplementedError`.
21
+ """
22
+
23
+ # Inspired by https://stackoverflow.com/questions/56997102/python-notimplementederror-for-instance-attributes
24
+ def __get__(self, *args, **kwargs):
25
+ raise NotImplementedError(
26
+ "This property needs to be defined manually in an instrument subclass"
27
+ )
28
+
29
+
9
30
  class L0FitsAccess(L1FitsAccess):
10
31
  """
11
32
  Class defining a fits access object for L0 input data.
@@ -20,6 +41,8 @@ class L0FitsAccess(L1FitsAccess):
20
41
  A boolean indicating whether to 'squeeze' out dimensions of size 1
21
42
  """
22
43
 
44
+ modulator_state: int = AbstractProperty()
45
+
23
46
  def __init__(
24
47
  self,
25
48
  hdu: fits.ImageHDU | fits.PrimaryHDU | fits.CompImageHDU,
@@ -9,6 +9,7 @@ from typing import Literal
9
9
  import numpy as np
10
10
  from astropy.io import fits
11
11
  from astropy.time import Time
12
+ from dkist_fits_specifications import __version__ as spec_version
12
13
  from dkist_fits_specifications.utils.formatter import reformat_dataset_extra_header
13
14
  from dkist_header_validator.spec_validators import spec_extras_validator
14
15
 
@@ -150,6 +151,8 @@ class WriteL1DatasetExtras(WriteL1Base, ABC):
150
151
  start_datetime = datetime.fromisoformat(task_specific_date_begin)
151
152
  end_datetime = datetime.fromisoformat(header_data.end_time)
152
153
 
154
+ inst_name = self.constants.instrument.lower()
155
+ calvers = self.version_from_module_name()
153
156
  dataset_extra_header = {
154
157
  DatasetExtraHeaderSection.common: {
155
158
  "BUNIT": "count",
@@ -181,10 +184,11 @@ class WriteL1DatasetExtras(WriteL1Base, ABC):
181
184
  "RECIPEID": self.metadata_store_recipe_run.recipeInstance.recipeId,
182
185
  "RINSTID": self.metadata_store_recipe_run.recipeInstanceId,
183
186
  "FILENAME": header_data.filename,
184
- "HEAD_URL": "",
187
+ "HEADVERS": spec_version,
188
+ "HEAD_URL": f"{self.docs_base_url}/projects/data-products/en/v{spec_version}",
185
189
  "INFO_URL": self.docs_base_url,
186
- "CAL_URL": "",
187
- "CALVERS": self.version_from_module_name(),
190
+ "CAL_URL": f"{self.docs_base_url}/projects/{inst_name}/en/v{calvers}/{self.workflow_name}.html",
191
+ "CALVERS": calvers,
188
192
  "IDSPARID": (
189
193
  parameters.inputDatasetPartId
190
194
  if (parameters := self.metadata_store_input_dataset_parameters)
@@ -7,6 +7,7 @@ from collections import defaultdict
7
7
  from copy import deepcopy
8
8
  from datetime import datetime
9
9
  from datetime import timedelta
10
+ from enum import StrEnum
10
11
  from pathlib import Path
11
12
  from random import choice
12
13
  from random import randint
@@ -196,7 +197,9 @@ class CalibrationSequenceDataset(Spec122Dataset):
196
197
  instrument="visp",
197
198
  angle_max_random_perturbation: float = 0.049,
198
199
  num_frames_per_CS_step: int = 5,
200
+ num_modstates: int = 8,
199
201
  ):
202
+ self.num_modstates = num_modstates
200
203
  self.num_frames_per_CS_step = num_frames_per_CS_step
201
204
  self.angle_max_random_perturbation = angle_max_random_perturbation
202
205
  # Make up a Calibration sequence. Mostly random except for two clears and two darks at start and end, which
@@ -224,7 +227,10 @@ class CalibrationSequenceDataset(Spec122Dataset):
224
227
  ]
225
228
 
226
229
  self.num_steps = len(self.pol_theta)
227
- dataset_shape = (self.num_steps * self.num_frames_per_CS_step,) + array_shape[1:]
230
+ dataset_shape = (
231
+ self.num_steps * self.num_modstates,
232
+ self.num_frames_per_CS_step,
233
+ ) + array_shape[1:]
228
234
  super().__init__(
229
235
  dataset_shape,
230
236
  array_shape,
@@ -240,7 +246,11 @@ class CalibrationSequenceDataset(Spec122Dataset):
240
246
 
241
247
  @property
242
248
  def cs_step(self) -> int:
243
- return self.index // self.num_frames_per_CS_step
249
+ return self.index // (self.num_modstates * self.num_frames_per_CS_step)
250
+
251
+ @key_function("VISP_011")
252
+ def modstate(self, key: str) -> int:
253
+ return ((self.index // self.num_frames_per_CS_step) % self.num_modstates) + 1
244
254
 
245
255
  @key_function("PAC__004")
246
256
  def polarizer_status(self, key: str) -> str:
@@ -284,6 +294,17 @@ class NonPolCalDataset(Spec122Dataset):
284
294
  self.add_constant_key("PAC__008", "DarkShutter")
285
295
 
286
296
 
297
+ class ModstateFitsAccess(L0FitsAccess):
298
+ def __init__(
299
+ self,
300
+ hdu: fits.ImageHDU | fits.PrimaryHDU | fits.CompImageHDU,
301
+ name: str | None = None,
302
+ auto_squeeze: bool = True,
303
+ ):
304
+ super().__init__(hdu=hdu, name=name, auto_squeeze=auto_squeeze)
305
+ self.modulator_state: int = self.header["VSPSTNUM"]
306
+
307
+
287
308
  @pytest.fixture(scope="session")
288
309
  def cs_step_angle_round_ndigits() -> int:
289
310
  return 1
@@ -296,6 +317,11 @@ def angle_random_max_perturbation(cs_step_angle_round_ndigits) -> float:
296
317
  return 10**-cs_step_angle_round_ndigits / 2 - 10 ** -(cs_step_angle_round_ndigits + 2)
297
318
 
298
319
 
320
+ @pytest.fixture(scope="session")
321
+ def num_modstates() -> int:
322
+ return 4
323
+
324
+
299
325
  @pytest.fixture(scope="session")
300
326
  def num_frames_per_cs_step() -> int:
301
327
  return 3
@@ -303,13 +329,14 @@ def num_frames_per_cs_step() -> int:
303
329
 
304
330
  @pytest.fixture(scope="session")
305
331
  def grouped_cal_sequence_fits_objs(
306
- angle_random_max_perturbation, num_frames_per_cs_step
307
- ) -> dict[int, list[L0FitsAccess]]:
332
+ angle_random_max_perturbation, num_modstates, num_frames_per_cs_step
333
+ ) -> dict[int, list[ModstateFitsAccess]]:
308
334
  ds = CalibrationSequenceDataset(
309
335
  array_shape=(1, 2, 2),
310
336
  time_delta=2.0,
311
337
  angle_max_random_perturbation=angle_random_max_perturbation,
312
338
  num_frames_per_CS_step=num_frames_per_cs_step,
339
+ num_modstates=num_modstates,
313
340
  )
314
341
  header_list = [
315
342
  spec122_validator.validate_and_translate_to_214_l0(d.header(), return_type=fits.HDUList)[
@@ -319,9 +346,40 @@ def grouped_cal_sequence_fits_objs(
319
346
  ]
320
347
  expected_cs_dict = defaultdict(list)
321
348
  for i in range(ds.num_steps):
322
- for j in range(ds.num_frames_per_CS_step):
323
- expected_cs_dict[i].append(L0FitsAccess.from_header(header_list.pop(0)))
349
+ for m in range(ds.num_modstates):
350
+ for j in range(ds.num_frames_per_CS_step):
351
+ expected_cs_dict[i].append(ModstateFitsAccess.from_header(header_list.pop(0)))
352
+
353
+ # Make sure we used all the dataset frames
354
+ assert len(header_list) == 0
355
+ return expected_cs_dict
356
+
357
+
358
+ @pytest.fixture(scope="session")
359
+ def grouped_cal_sequence_modstate_fits_objs(
360
+ angle_random_max_perturbation, num_modstates, num_frames_per_cs_step
361
+ ) -> dict[int, dict[int, list[ModstateFitsAccess]]]:
362
+ ds = CalibrationSequenceDataset(
363
+ array_shape=(1, 2, 2),
364
+ time_delta=2.0,
365
+ angle_max_random_perturbation=angle_random_max_perturbation,
366
+ num_frames_per_CS_step=num_frames_per_cs_step,
367
+ num_modstates=num_modstates,
368
+ )
369
+ header_list = [
370
+ spec122_validator.validate_and_translate_to_214_l0(d.header(), return_type=fits.HDUList)[
371
+ 0
372
+ ].header
373
+ for d in ds
374
+ ]
375
+ expected_cs_dict = defaultdict(lambda: defaultdict(list))
376
+ for i in range(ds.num_steps):
377
+ for m in range(ds.num_modstates):
378
+ for j in range(ds.num_frames_per_CS_step):
379
+ expected_cs_dict[i][m].append(ModstateFitsAccess.from_header(header_list.pop(0)))
324
380
 
381
+ # Make sure we used all the dataset frames
382
+ assert len(header_list) == 0
325
383
  return expected_cs_dict
326
384
 
327
385
 
@@ -341,7 +399,7 @@ def non_polcal_fits_objs() -> list[L0FitsAccess]:
341
399
  @pytest.fixture(scope="session")
342
400
  def max_cs_step_time_sec() -> float:
343
401
  """Max CS step time in seconds"""
344
- return 15.0
402
+ return 30.0
345
403
 
346
404
 
347
405
  ####################################
@@ -6,6 +6,7 @@ from pathlib import Path
6
6
  import numpy as np
7
7
  import pytest
8
8
  from astropy.io import fits
9
+ from dkist_fits_specifications import __version__ as spec_version
9
10
 
10
11
  from dkist_processing_common._util.scratch import WorkflowFileSystem
11
12
  from dkist_processing_common.codecs.fits import fits_array_decoder
@@ -385,6 +386,18 @@ def test_construct_dataset_extras(
385
386
  "DEMODULATION MATRICES",
386
387
  "POLCAL AS SCIENCE",
387
388
  ]
389
+ assert header["INFO_URL"] == task.docs_base_url
390
+ assert header["HEADVERS"] == spec_version
391
+ assert (
392
+ header["HEAD_URL"]
393
+ == f"{task.docs_base_url}/projects/data-products/en/v{spec_version}"
394
+ )
395
+ calvers = task.version_from_module_name()
396
+ assert header["CALVERS"] == calvers
397
+ assert (
398
+ header["CAL_URL"]
399
+ == f"{task.docs_base_url}/projects/{task.constants.instrument.lower()}/en/v{calvers}/{task.workflow_name}.html"
400
+ )
388
401
  if header["IPTASK"] == "POLCAL":
389
402
  assert "POLANGLE" not in header
390
403
  else:
@@ -0,0 +1,111 @@
1
+ import random
2
+
3
+ import numpy as np
4
+ import pytest
5
+
6
+ from dkist_processing_common.parsers.cs_step import CSStep
7
+ from dkist_processing_common.parsers.cs_step import CSStepModstate
8
+
9
+
10
+ def test_equality_correct(
11
+ grouped_cal_sequence_fits_objs, max_cs_step_time_sec, cs_step_angle_round_ndigits
12
+ ):
13
+ """
14
+ Given: A set of PolCal headers
15
+ When: Converting them to CSStep objects and comparing them
16
+ Then: All headers belonging to the same step produce CSStep objects that are equal and are not equal to CSStep objects
17
+ from other steps
18
+ """
19
+ num_steps = len(grouped_cal_sequence_fits_objs)
20
+ for step in range(num_steps):
21
+ for i in range(num_steps):
22
+ objs_equal = CSStep(
23
+ grouped_cal_sequence_fits_objs[step][0], max_cs_step_time_sec
24
+ ) == CSStep(grouped_cal_sequence_fits_objs[i][0], max_cs_step_time_sec)
25
+ assert objs_equal == (step == i)
26
+
27
+
28
+ def test_csstepmodstate_equality_correct(
29
+ grouped_cal_sequence_modstate_fits_objs,
30
+ num_modstates,
31
+ max_cs_step_time_sec,
32
+ cs_step_angle_round_ndigits,
33
+ ):
34
+ """
35
+ Given: A set of PolCal headers
36
+ When: Converting them to CSStepModstate objects and comparing them
37
+ Then: All headers belonging to the same step and modstate produce CSStepModstate objects that are equal and that are
38
+ not equal to CSStepModstate objects from other steps and modstates
39
+ """
40
+ num_steps = len(grouped_cal_sequence_modstate_fits_objs)
41
+ for step, modstate_dict in grouped_cal_sequence_modstate_fits_objs.items():
42
+ for modstate in range(num_modstates):
43
+ for ss in range(num_steps):
44
+ for mm in range(num_modstates):
45
+ objs_equal = CSStepModstate(
46
+ grouped_cal_sequence_modstate_fits_objs[step][modstate][0],
47
+ max_cs_step_time_sec,
48
+ ) == CSStepModstate(
49
+ grouped_cal_sequence_modstate_fits_objs[ss][mm][0],
50
+ max_cs_step_time_sec,
51
+ )
52
+ assert objs_equal == (ss == step and mm == modstate)
53
+
54
+
55
+ @pytest.mark.parametrize(
56
+ "step_class",
57
+ [pytest.param(CSStep, id="CSStep"), pytest.param(CSStepModstate, id="CSStepModstate")],
58
+ )
59
+ def test_not_equal_non_CS_Step_type(
60
+ grouped_cal_sequence_fits_objs, step_class, max_cs_step_time_sec
61
+ ):
62
+ """
63
+ Given: A PolCal header and resulting CSStep[Modstate] object
64
+ When: Testing equality with a non CSStep object
65
+ Then: An error is raised
66
+ """
67
+ cs_step = step_class(grouped_cal_sequence_fits_objs[0][0], max_cs_step_time_sec)
68
+ with pytest.raises(
69
+ TypeError,
70
+ match=f"Cannot compare <class 'dkist_processing_common.parsers.cs_step.{step_class.__name__}'> with <class 'int'>",
71
+ ):
72
+ _ = cs_step == 1
73
+
74
+
75
+ @pytest.mark.parametrize(
76
+ "step_class",
77
+ [pytest.param(CSStep, id="CSStep"), pytest.param(CSStepModstate, id="CSStepModstate")],
78
+ )
79
+ def test_order_correct(grouped_cal_sequence_fits_objs, step_class, max_cs_step_time_sec):
80
+ """
81
+ Given: A set of PolCal headers
82
+ When: Converting them to CSStep[Modstate] objects and ordering them
83
+ Then: The step objects are correctly ordered by observe time
84
+ """
85
+ cs_step_list = [
86
+ step_class(header, max_cs_step_time_sec)
87
+ for header in sum(grouped_cal_sequence_fits_objs.values(), [])
88
+ ]
89
+ random.shuffle(cs_step_list) # Just to mix it up a bit
90
+ time_list = [c.obs_time for c in cs_step_list]
91
+ cs_sort_idx = np.argsort(cs_step_list)
92
+ time_sort_idx = np.argsort(time_list)
93
+ assert np.array_equal(cs_sort_idx, time_sort_idx)
94
+
95
+
96
+ @pytest.mark.parametrize(
97
+ "step_class",
98
+ [pytest.param(CSStep, id="CSStep"), pytest.param(CSStepModstate, id="CSStepModstate")],
99
+ )
100
+ def test_repr(grouped_cal_sequence_fits_objs, step_class):
101
+ """
102
+ Given: An `L0FitsAccess` object
103
+ When: Using that object to instantiate `CSStep[Modstate]`
104
+ Then: The repr of the instance is correct
105
+ """
106
+ fits_obj = grouped_cal_sequence_fits_objs[0][0]
107
+ cs_step_obj = step_class(fits_obj, max_cs_time_sec=10, angle_round_ndigits=5)
108
+ assert (
109
+ repr(cs_step_obj)
110
+ == f"{step_class.__name__}({fits_obj!r}, max_cs_time_sec=10, angle_round_ndigits=5)"
111
+ )
@@ -145,6 +145,17 @@ class FitsAccessWithNaxisKeys(FitsAccessBase):
145
145
  self.naxis2 = self.header[MetadataKeyWithNaxisKeys.naxis2]
146
146
 
147
147
 
148
+ class FitsAccessWithModstate(L0FitsAccess):
149
+ def __init__(
150
+ self,
151
+ hdu: fits.ImageHDU | fits.PrimaryHDU | fits.CompImageHDU,
152
+ name: str | None = None,
153
+ auto_squeeze: bool = True,
154
+ ):
155
+ super().__init__(hdu=hdu, name=name, auto_squeeze=auto_squeeze)
156
+ self.modulator_state = 5
157
+
158
+
148
159
  def test_metadata_keys_in_access_bases(hdu_with_no_data):
149
160
  """
150
161
  Given: a set of metadata key names in the MetadataKey sting enumeration
@@ -348,3 +359,20 @@ def test_from_header_naxis_preserved(header_type):
348
359
  assert fits_obj.naxis == 2
349
360
  assert fits_obj.naxis1 == 123
350
361
  assert fits_obj.naxis2 == 456
362
+
363
+
364
+ def test_l0fitsaccess_modulator_state(hdu_with_no_data):
365
+ """
366
+ Given: Instances of both the base `L0FitsAccess` class and a subclass of the same that defines the `modulator_state` property
367
+ When: Accessing the `modulator_state` property on both classes
368
+ Then: The base `L0FitsAccess` instance raises the appropriate error and the subclass returns the correct value
369
+ """
370
+ l0_obj = L0FitsAccess(hdu=hdu_with_no_data)
371
+ with pytest.raises(
372
+ NotImplementedError,
373
+ match="This property needs to be defined manually in an instrument subclass",
374
+ ):
375
+ _ = l0_obj.modulator_state
376
+
377
+ child_obj = FitsAccessWithModstate(hdu=hdu_with_no_data)
378
+ assert child_obj.modulator_state == 5
@@ -501,14 +501,15 @@ def test_cs_step_flower(grouped_cal_sequence_fits_objs, non_polcal_fits_objs, ma
501
501
 
502
502
  assert len(list(cs_step_flower.petals)) == len(list(grouped_cal_sequence_fits_objs.keys()))
503
503
  for step_petal in cs_step_flower.petals:
504
- assert sorted(step_petal.keys) == [
505
- f"step_{step_petal.value}_file_{i}" for i in range(len(step_petal.keys))
506
- ]
504
+ assert sorted(step_petal.keys) == sorted(
505
+ [f"step_{step_petal.value}_file_{i}" for i in range(len(step_petal.keys))]
506
+ )
507
507
 
508
508
 
509
509
  def test_cs_step_frame_flower(
510
- grouped_cal_sequence_fits_objs,
510
+ grouped_cal_sequence_modstate_fits_objs,
511
511
  non_polcal_fits_objs,
512
+ num_modstates,
512
513
  num_frames_per_cs_step,
513
514
  max_cs_step_time_sec,
514
515
  ):
@@ -517,24 +518,31 @@ def test_cs_step_frame_flower(
517
518
  When: Updating the CSStepFrameFlower with all headers
518
519
  Then: The flower correctly organizes the PolCal frames and ignores the non-PolCal frames
519
520
  """
520
- cs_step_frame_flower = CSStepFrameFlower(max_cs_step_time_sec=max_cs_step_time_sec)
521
+ cs_step_frame_flower = CSStepFrameFlower(
522
+ max_cs_step_time_sec=max_cs_step_time_sec,
523
+ )
521
524
  assert cs_step_frame_flower.stem_name == StemName.cs_step_frame
522
525
 
523
- num_cs_steps = len(grouped_cal_sequence_fits_objs)
524
- for step, objs in grouped_cal_sequence_fits_objs.items():
525
- file_names = [f"step_{step}_file_{i}" for i in range(len(objs))]
526
- # Reverse the lists here just to make sure we're not relying on pre-sorted inputs
527
- for o, k in zip(objs[::-1], file_names[::-1]):
528
- cs_step_frame_flower.update(k, o)
526
+ num_cs_steps = len(grouped_cal_sequence_modstate_fits_objs)
527
+ for step, modstate_dict in grouped_cal_sequence_modstate_fits_objs.items():
528
+ for m, objs in modstate_dict.items():
529
+ file_names = [f"step_{step}_mod_{m}_file_{i}" for i in range(len(objs))]
530
+ # Reverse the lists here just to make sure we're not relying on pre-sorted inputs
531
+ for o, k in zip(objs[::-1], file_names[::-1]):
532
+ cs_step_frame_flower.update(k, o)
529
533
 
530
534
  for o in non_polcal_fits_objs:
531
535
  cs_step_frame_flower.update("non_polcal", o)
532
536
 
533
537
  assert len(list(cs_step_frame_flower.petals)) == num_frames_per_cs_step
534
538
  for frame_petal in cs_step_frame_flower.petals:
535
- assert sorted(frame_petal.keys) == [
536
- f"step_{step}_file_{frame_petal.value}" for step in range(num_cs_steps)
537
- ]
539
+ assert sorted(frame_petal.keys) == sorted(
540
+ [
541
+ f"step_{step}_mod_{m}_file_{frame_petal.value}"
542
+ for step in range(num_cs_steps)
543
+ for m in range(num_modstates)
544
+ ]
545
+ )
538
546
 
539
547
 
540
548
  def test_num_cs_step_bud(
@@ -569,7 +577,9 @@ def test_num_frames_per_cs_step_bud(
569
577
  When: Updating the bud with the headers
570
578
  Then: The bud correctly parses the number of frames per CS step
571
579
  """
572
- num_frames_per_cs_bud_obj = NumFramesPerCSStepBud(max_cs_step_time_sec=max_cs_step_time_sec)
580
+ num_frames_per_cs_bud_obj = NumFramesPerCSStepBud(
581
+ max_cs_step_time_sec=max_cs_step_time_sec,
582
+ )
573
583
  num_cs_steps = len(grouped_cal_sequence_fits_objs)
574
584
  for i, (step, objs) in enumerate(grouped_cal_sequence_fits_objs.items()):
575
585
  if skip_frame and i == num_cs_steps - 1:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dkist-processing-common
3
- Version: 12.6.2
3
+ Version: 12.6.3
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
@@ -15,7 +15,7 @@ Requires-Python: >=3.13
15
15
  Description-Content-Type: text/x-rst
16
16
  Requires-Dist: asdf<4.0.0,>=3.5.0
17
17
  Requires-Dist: astropy>=7.0.0
18
- Requires-Dist: dkist-fits-specifications<5.0,>=4.21.0
18
+ Requires-Dist: dkist-fits-specifications<5.0,>=4.22.0
19
19
  Requires-Dist: dkist-header-validator<6.0,>=5.3.0
20
20
  Requires-Dist: dkist-processing-core==7.0.2
21
21
  Requires-Dist: dkist-processing-pac<4.0,>=3.1