dkist-processing-visp 5.8.1__tar.gz → 5.8.2__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 (125) hide show
  1. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/CHANGELOG.rst +9 -0
  2. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/PKG-INFO +25 -25
  3. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/models/constants.py +17 -0
  4. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/models/fits_access.py +1 -0
  5. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/parsers/visp_l0_fits_access.py +4 -0
  6. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tasks/assemble_movie.py +9 -2
  7. dkist_processing_visp-5.8.2/dkist_processing_visp/tasks/make_movie_frames.py +351 -0
  8. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tasks/parse.py +5 -0
  9. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tests/conftest.py +8 -0
  10. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tests/header_models.py +11 -0
  11. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tests/local_trial_workflows/local_trial_helpers.py +19 -7
  12. dkist_processing_visp-5.8.2/dkist_processing_visp/tests/test_assemble_movie.py +135 -0
  13. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tests/test_fits_access.py +4 -0
  14. dkist_processing_visp-5.8.2/dkist_processing_visp/tests/test_make_movie_frames.py +245 -0
  15. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tests/test_parse.py +4 -0
  16. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tests/test_visp_constants.py +43 -0
  17. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp.egg-info/PKG-INFO +25 -25
  18. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp.egg-info/requires.txt +24 -24
  19. dkist_processing_visp-5.8.2/dkist_processing_visp.egg-info/scm_version.json +8 -0
  20. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/pyproject.toml +26 -26
  21. dkist_processing_visp-5.8.1/dkist_processing_visp/tasks/make_movie_frames.py +0 -160
  22. dkist_processing_visp-5.8.1/dkist_processing_visp/tests/test_assemble_movie.py +0 -62
  23. dkist_processing_visp-5.8.1/dkist_processing_visp/tests/test_make_movie_frames.py +0 -61
  24. dkist_processing_visp-5.8.1/dkist_processing_visp.egg-info/scm_version.json +0 -8
  25. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/.gitignore +0 -0
  26. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/.pre-commit-config.yaml +0 -0
  27. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/.readthedocs.yml +0 -0
  28. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/.rovodev/.review-agent.md +0 -0
  29. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/.snyk +0 -0
  30. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/README.rst +0 -0
  31. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/SCIENCE_CHANGELOG.rst +0 -0
  32. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/bitbucket-pipelines.yml +0 -0
  33. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/changelog/.gitempty +0 -0
  34. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/__init__.py +0 -0
  35. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/config.py +0 -0
  36. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/fonts/Lato-Regular.ttf +0 -0
  37. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/models/__init__.py +0 -0
  38. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/models/dataset_extras.py +0 -0
  39. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/models/metric_code.py +0 -0
  40. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/models/parameters.py +0 -0
  41. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/models/tags.py +0 -0
  42. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/models/task_name.py +0 -0
  43. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/parsers/__init__.py +0 -0
  44. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/parsers/map_repeats.py +0 -0
  45. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/parsers/modulator_states.py +0 -0
  46. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/parsers/polarimeter_mode.py +0 -0
  47. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/parsers/raster_step.py +0 -0
  48. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/parsers/spectrograph_configuration.py +0 -0
  49. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/parsers/time.py +0 -0
  50. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/parsers/visp_l1_fits_access.py +0 -0
  51. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tasks/__init__.py +0 -0
  52. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tasks/background_light.py +0 -0
  53. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tasks/dark.py +0 -0
  54. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tasks/geometric.py +0 -0
  55. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tasks/instrument_polarization.py +0 -0
  56. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tasks/l1_output_data.py +0 -0
  57. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tasks/lamp.py +0 -0
  58. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tasks/mixin/__init__.py +0 -0
  59. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tasks/mixin/beam_access.py +0 -0
  60. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tasks/mixin/corrections.py +0 -0
  61. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tasks/mixin/downsample.py +0 -0
  62. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tasks/polcal_as_science.py +0 -0
  63. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tasks/quality_metrics.py +0 -0
  64. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tasks/science.py +0 -0
  65. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tasks/solar.py +0 -0
  66. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tasks/visp_base.py +0 -0
  67. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tasks/wavelength_calibration.py +0 -0
  68. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tasks/write_dataset_extras.py +0 -0
  69. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tasks/write_l1.py +0 -0
  70. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tests/README.rst +0 -0
  71. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tests/__init__.py +0 -0
  72. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tests/local_trial_workflows/__init__.py +0 -0
  73. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tests/local_trial_workflows/l0_cals_only.py +0 -0
  74. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tests/local_trial_workflows/l0_polcals_as_science.py +0 -0
  75. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tests/local_trial_workflows/l0_solar_gain_as_science.py +0 -0
  76. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tests/local_trial_workflows/l0_to_l1.py +0 -0
  77. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tests/test_assemble_quality.py +0 -0
  78. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tests/test_background_light.py +0 -0
  79. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tests/test_dark.py +0 -0
  80. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tests/test_downsample.py +0 -0
  81. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tests/test_geometric.py +0 -0
  82. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tests/test_instrument_polarization.py +0 -0
  83. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tests/test_lamp.py +0 -0
  84. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tests/test_map_repeats.py +0 -0
  85. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tests/test_parameters.py +0 -0
  86. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tests/test_polcal_as_science.py +0 -0
  87. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tests/test_quality.py +0 -0
  88. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tests/test_science.py +0 -0
  89. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tests/test_solar.py +0 -0
  90. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tests/test_trial_create_quality_report.py +0 -0
  91. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tests/test_visp_base.py +0 -0
  92. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tests/test_wavelength_calibration.py +0 -0
  93. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tests/test_workflows.py +0 -0
  94. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tests/test_write_dataset_extras.py +0 -0
  95. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/tests/test_write_l1.py +0 -0
  96. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/workflows/__init__.py +0 -0
  97. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/workflows/l0_processing.py +0 -0
  98. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/workflows/single_task_workflows.py +0 -0
  99. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp/workflows/trial_workflows.py +0 -0
  100. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp.egg-info/SOURCES.txt +0 -0
  101. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp.egg-info/dependency_links.txt +0 -0
  102. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp.egg-info/scm_file_list.json +0 -0
  103. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/dkist_processing_visp.egg-info/top_level.txt +0 -0
  104. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/docs/Makefile +0 -0
  105. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/docs/background_light.rst +0 -0
  106. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/docs/changelog.rst +0 -0
  107. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/docs/conf.py +0 -0
  108. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/docs/dataset_extras.rst +0 -0
  109. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/docs/gain_correction.rst +0 -0
  110. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/docs/geometric.rst +0 -0
  111. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/docs/index.rst +0 -0
  112. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/docs/l0_to_l1_visp.rst +0 -0
  113. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/docs/l0_to_l1_visp_full-trial.rst +0 -0
  114. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/docs/landing_page.rst +0 -0
  115. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/docs/make.bat +0 -0
  116. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/docs/polarization_calibration.rst +0 -0
  117. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/docs/requirements.txt +0 -0
  118. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/docs/requirements_table.rst +0 -0
  119. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/docs/science_calibration.rst +0 -0
  120. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/docs/scientific_changelog.rst +0 -0
  121. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/docs/wavelength_calibration.rst +0 -0
  122. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/licenses/LICENSE.rst +0 -0
  123. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/science_towncrier.sh +0 -0
  124. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/setup.cfg +0 -0
  125. {dkist_processing_visp-5.8.1 → dkist_processing_visp-5.8.2}/towncrier_science.toml +0 -0
@@ -1,3 +1,12 @@
1
+ v5.8.2 (2026-09-04)
2
+ ===================
3
+
4
+ Bugfixes
5
+ --------
6
+
7
+ - Update movie generation for "sit'n'stare" datasets to avoid out-of-memory failures. Movies for sit'n'stare observations (when the slit doesn't move) are now displayed with time on the "spatial" axis; each column is the wavelength-integrated signal of a single slit observation. Genuine spatial raster scans, of any polarization mode, are unaffected and continue to use the existing algorithm. (`#329 <https://bitbucket.org/dkistdc/dkist-processing-visp/pull-requests/329>`__)
8
+
9
+
1
10
  v5.8.1 (2026-08-28)
2
11
  ===================
3
12
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dkist-processing-visp
3
- Version: 5.8.1
3
+ Version: 5.8.2
4
4
  Summary: Science processing code for the ViSP instrument on DKIST
5
5
  Author-email: NSO / AURA <dkistdc@nso.edu>
6
6
  License: BSD-3-Clause
@@ -84,11 +84,11 @@ Requires-Dist: aiohttp==3.14.3; extra == "frozen"
84
84
  Requires-Dist: aiosignal==1.4.0; extra == "frozen"
85
85
  Requires-Dist: aiosmtplib==5.1.2; extra == "frozen"
86
86
  Requires-Dist: aiosqlite==0.22.1; extra == "frozen"
87
- Requires-Dist: alembic==1.19.1; extra == "frozen"
87
+ Requires-Dist: alembic==1.19.2; extra == "frozen"
88
88
  Requires-Dist: amqp==5.3.1; extra == "frozen"
89
89
  Requires-Dist: annotated-doc==0.0.5; extra == "frozen"
90
90
  Requires-Dist: annotated-types==0.8.0; extra == "frozen"
91
- Requires-Dist: anyio==4.14.2; extra == "frozen"
91
+ Requires-Dist: anyio==4.15.0; extra == "frozen"
92
92
  Requires-Dist: apache-airflow==3.3.1; extra == "frozen"
93
93
  Requires-Dist: apache-airflow-core==3.3.1; extra == "frozen"
94
94
  Requires-Dist: apache-airflow-providers-celery==3.23.1; extra == "frozen"
@@ -109,7 +109,7 @@ Requires-Dist: asdf_standard==1.5.0; extra == "frozen"
109
109
  Requires-Dist: asgiref==3.12.1; extra == "frozen"
110
110
  Requires-Dist: asteval==1.0.10; extra == "frozen"
111
111
  Requires-Dist: astropy==7.0.2; extra == "frozen"
112
- Requires-Dist: astropy-iers-data==0.2026.8.24.0.24.29; extra == "frozen"
112
+ Requires-Dist: astropy-iers-data==0.2026.8.31.0.57.9; extra == "frozen"
113
113
  Requires-Dist: asttokens==3.0.2; extra == "frozen"
114
114
  Requires-Dist: async-lru==2.3.0; extra == "frozen"
115
115
  Requires-Dist: asyncpg==0.31.0; extra == "frozen"
@@ -118,9 +118,9 @@ Requires-Dist: babel==2.18.0; extra == "frozen"
118
118
  Requires-Dist: beautifulsoup4==4.15.0; extra == "frozen"
119
119
  Requires-Dist: billiard==4.2.4; extra == "frozen"
120
120
  Requires-Dist: bleach==6.4.0; extra == "frozen"
121
- Requires-Dist: boto3==1.43.82; extra == "frozen"
122
- Requires-Dist: botocore==1.43.82; extra == "frozen"
123
- Requires-Dist: cachetools==7.1.7; extra == "frozen"
121
+ Requires-Dist: boto3==1.43.88; extra == "frozen"
122
+ Requires-Dist: botocore==1.43.88; extra == "frozen"
123
+ Requires-Dist: cachetools==7.1.8; extra == "frozen"
124
124
  Requires-Dist: cadwyn==7.0.0; extra == "frozen"
125
125
  Requires-Dist: celery==5.6.3; extra == "frozen"
126
126
  Requires-Dist: certifi==2026.7.22; extra == "frozen"
@@ -130,6 +130,7 @@ Requires-Dist: click==8.5.0; extra == "frozen"
130
130
  Requires-Dist: click-didyoumean==0.3.1; extra == "frozen"
131
131
  Requires-Dist: click-plugins==1.1.1.2; extra == "frozen"
132
132
  Requires-Dist: click-repl==0.3.0; extra == "frozen"
133
+ Requires-Dist: cloudpickle==3.1.2; extra == "frozen"
133
134
  Requires-Dist: colorlog==6.12.0; extra == "frozen"
134
135
  Requires-Dist: comm==0.2.3; extra == "frozen"
135
136
  Requires-Dist: contourpy==1.3.3; extra == "frozen"
@@ -146,7 +147,7 @@ Requires-Dist: dkist-processing-common==14.4.0; extra == "frozen"
146
147
  Requires-Dist: dkist-processing-core==7.5.0; extra == "frozen"
147
148
  Requires-Dist: dkist-processing-math==2.2.3; extra == "frozen"
148
149
  Requires-Dist: dkist-processing-pac==3.1.2; extra == "frozen"
149
- Requires-Dist: dkist-processing-visp==5.8.1; extra == "frozen"
150
+ Requires-Dist: dkist-processing-visp==5.8.2; extra == "frozen"
150
151
  Requires-Dist: dkist-service-configuration==4.3.0; extra == "frozen"
151
152
  Requires-Dist: dkist-spectral-lines==3.0.1; extra == "frozen"
152
153
  Requires-Dist: dkist_fits_specifications==4.28.1; extra == "frozen"
@@ -157,12 +158,12 @@ Requires-Dist: fastapi==0.136.3; extra == "frozen"
157
158
  Requires-Dist: fastapi-cli==0.0.32; extra == "frozen"
158
159
  Requires-Dist: fastjsonschema==2.22.2; extra == "frozen"
159
160
  Requires-Dist: flower==2.1.0; extra == "frozen"
160
- Requires-Dist: fonttools==4.63.0; extra == "frozen"
161
+ Requires-Dist: fonttools==4.64.0; extra == "frozen"
161
162
  Requires-Dist: fqdn==1.5.1; extra == "frozen"
162
163
  Requires-Dist: frozenlist==1.8.0; extra == "frozen"
163
164
  Requires-Dist: fsspec==2026.7.0; extra == "frozen"
164
165
  Requires-Dist: globus-sdk==4.9.0; extra == "frozen"
165
- Requires-Dist: googleapis-common-protos==1.75.2; extra == "frozen"
166
+ Requires-Dist: googleapis-common-protos==1.75.3; extra == "frozen"
166
167
  Requires-Dist: gqlclient==2.0.0; extra == "frozen"
167
168
  Requires-Dist: greenback==1.3.0; extra == "frozen"
168
169
  Requires-Dist: greenlet==3.5.5; extra == "frozen"
@@ -174,22 +175,22 @@ Requires-Dist: httpx==0.28.1; extra == "frozen"
174
175
  Requires-Dist: humanize==4.16.0; extra == "frozen"
175
176
  Requires-Dist: idna==3.19; extra == "frozen"
176
177
  Requires-Dist: imageio-ffmpeg==0.6.0; extra == "frozen"
177
- Requires-Dist: importlib_metadata==9.0.0; extra == "frozen"
178
+ Requires-Dist: importlib_metadata==9.0.1; extra == "frozen"
178
179
  Requires-Dist: ipykernel==7.3.0; extra == "frozen"
179
- Requires-Dist: ipython==9.17.0; extra == "frozen"
180
+ Requires-Dist: ipython==9.17.1; extra == "frozen"
180
181
  Requires-Dist: ipython_pygments_lexers==1.1.1; extra == "frozen"
181
182
  Requires-Dist: isoduration==20.11.0; extra == "frozen"
182
183
  Requires-Dist: itsdangerous==2.2.0; extra == "frozen"
183
184
  Requires-Dist: jedi==0.20.0; extra == "frozen"
184
185
  Requires-Dist: jmespath==1.1.0; extra == "frozen"
185
- Requires-Dist: joblib==1.5.3; extra == "frozen"
186
+ Requires-Dist: joblib==1.6.0; extra == "frozen"
186
187
  Requires-Dist: json5==0.15.0; extra == "frozen"
187
188
  Requires-Dist: jsonpointer==3.1.1; extra == "frozen"
188
189
  Requires-Dist: jsonschema==4.26.0; extra == "frozen"
189
190
  Requires-Dist: jsonschema-specifications==2025.9.1; extra == "frozen"
190
191
  Requires-Dist: jupyter-events==0.12.1; extra == "frozen"
191
192
  Requires-Dist: jupyter-lsp==2.3.1; extra == "frozen"
192
- Requires-Dist: jupyter_builder==1.2.2; extra == "frozen"
193
+ Requires-Dist: jupyter_builder==1.2.3; extra == "frozen"
193
194
  Requires-Dist: jupyter_client==8.10.0; extra == "frozen"
194
195
  Requires-Dist: jupyter_core==5.9.1; extra == "frozen"
195
196
  Requires-Dist: jupyter_server==2.21.0; extra == "frozen"
@@ -203,7 +204,7 @@ Requires-Dist: lark==1.3.1; extra == "frozen"
203
204
  Requires-Dist: lazy-loader==0.5; extra == "frozen"
204
205
  Requires-Dist: lazy-object-proxy==1.12.0; extra == "frozen"
205
206
  Requires-Dist: libcst==1.9.0; extra == "frozen"
206
- Requires-Dist: linkify-it-py==2.1.1; extra == "frozen"
207
+ Requires-Dist: linkify-it-py==2.2.0; extra == "frozen"
207
208
  Requires-Dist: lmfit==1.3.4; extra == "frozen"
208
209
  Requires-Dist: lockfile==0.12.2; extra == "frozen"
209
210
  Requires-Dist: loguru==0.7.3; extra == "frozen"
@@ -264,17 +265,17 @@ Requires-Dist: pexpect==4.9.0; extra == "frozen"
264
265
  Requires-Dist: pika==1.4.4; extra == "frozen"
265
266
  Requires-Dist: pillow==10.4.0; extra == "frozen"
266
267
  Requires-Dist: pip==26.2.1; extra == "frozen"
267
- Requires-Dist: platformdirs==4.11.5; extra == "frozen"
268
+ Requires-Dist: platformdirs==4.11.7; extra == "frozen"
268
269
  Requires-Dist: pluggy==1.6.0; extra == "frozen"
269
270
  Requires-Dist: pooch==1.9.0; extra == "frozen"
270
271
  Requires-Dist: proglog==0.1.12; extra == "frozen"
271
272
  Requires-Dist: prometheus_client==0.26.0; extra == "frozen"
272
273
  Requires-Dist: prompt_toolkit==3.0.53; extra == "frozen"
273
274
  Requires-Dist: propcache==0.5.2; extra == "frozen"
274
- Requires-Dist: protobuf==7.36.0; extra == "frozen"
275
+ Requires-Dist: protobuf==7.36.1; extra == "frozen"
275
276
  Requires-Dist: psutil==7.2.2; extra == "frozen"
276
- Requires-Dist: psycopg==3.3.4; extra == "frozen"
277
- Requires-Dist: psycopg-binary==3.3.4; extra == "frozen"
277
+ Requires-Dist: psycopg==3.3.5; extra == "frozen"
278
+ Requires-Dist: psycopg-binary==3.3.5; extra == "frozen"
278
279
  Requires-Dist: psycopg2-binary==2.9.12; extra == "frozen"
279
280
  Requires-Dist: ptyprocess==0.7.0; extra == "frozen"
280
281
  Requires-Dist: pure_eval==0.2.3; extra == "frozen"
@@ -284,7 +285,7 @@ Requires-Dist: pydantic-extra-types==2.11.1; extra == "frozen"
284
285
  Requires-Dist: pydantic-settings==2.15.0; extra == "frozen"
285
286
  Requires-Dist: pydantic_core==2.46.5; extra == "frozen"
286
287
  Requires-Dist: pyerfa==2.0.1.5; extra == "frozen"
287
- Requires-Dist: pygtrie==2.5.0; extra == "frozen"
288
+ Requires-Dist: pygtrie==2.6.1; extra == "frozen"
288
289
  Requires-Dist: pyparsing==3.3.2; extra == "frozen"
289
290
  Requires-Dist: python-daemon==3.1.2; extra == "frozen"
290
291
  Requires-Dist: python-dateutil==2.9.0.post0; extra == "frozen"
@@ -302,7 +303,7 @@ Requires-Dist: rfc3986-validator==0.1.1; extra == "frozen"
302
303
  Requires-Dist: rfc3987-syntax==1.1.0; extra == "frozen"
303
304
  Requires-Dist: rich==15.0.0; extra == "frozen"
304
305
  Requires-Dist: rich-argparse==1.8.0; extra == "frozen"
305
- Requires-Dist: rich-toolkit==0.20.3; extra == "frozen"
306
+ Requires-Dist: rich-toolkit==0.20.4; extra == "frozen"
306
307
  Requires-Dist: rpds-py==2026.6.3; extra == "frozen"
307
308
  Requires-Dist: s3transfer==0.19.2; extra == "frozen"
308
309
  Requires-Dist: scikit-image==0.25.2; extra == "frozen"
@@ -339,7 +340,6 @@ Requires-Dist: typing-inspection==0.4.4; extra == "frozen"
339
340
  Requires-Dist: typing_extensions==4.16.0; extra == "frozen"
340
341
  Requires-Dist: tzdata==2026.3; extra == "frozen"
341
342
  Requires-Dist: tzlocal==5.4.4; extra == "frozen"
342
- Requires-Dist: uc-micro-py==2.0.0; extra == "frozen"
343
343
  Requires-Dist: uncertainties==3.2.3; extra == "frozen"
344
344
  Requires-Dist: universal_pathlib==0.3.10; extra == "frozen"
345
345
  Requires-Dist: uri-template==1.3.0; extra == "frozen"
@@ -350,13 +350,13 @@ Requires-Dist: uvloop==0.22.1; extra == "frozen"
350
350
  Requires-Dist: vine==5.1.0; extra == "frozen"
351
351
  Requires-Dist: voluptuous==0.16.0; extra == "frozen"
352
352
  Requires-Dist: watchfiles==1.2.0; extra == "frozen"
353
- Requires-Dist: wcwidth==0.8.2; extra == "frozen"
353
+ Requires-Dist: wcwidth==0.8.3; extra == "frozen"
354
354
  Requires-Dist: webcolors==25.10.0; extra == "frozen"
355
355
  Requires-Dist: webencodings==0.6.1; extra == "frozen"
356
- Requires-Dist: websocket-client==1.9.0; extra == "frozen"
356
+ Requires-Dist: websocket-client==1.9.2; extra == "frozen"
357
357
  Requires-Dist: websockets==17.1; extra == "frozen"
358
358
  Requires-Dist: wirerope==1.0.0; extra == "frozen"
359
- Requires-Dist: wrapt==2.3.0; extra == "frozen"
359
+ Requires-Dist: wrapt==2.4.0; extra == "frozen"
360
360
  Requires-Dist: yamale==6.1.0; extra == "frozen"
361
361
  Requires-Dist: yarl==1.24.5; extra == "frozen"
362
362
  Requires-Dist: zipp==4.1.0; extra == "frozen"
@@ -34,6 +34,7 @@ class VispBudName(Enum):
34
34
  axis_3_type = "AXIS_3_TYPE"
35
35
  dark_readout_exp_time_picky_bud = "DARK_READOUT_EXP_TIME_PICKY_BUD"
36
36
  dark_readout_to_total_exp_times_lookup = "DARK_READOUT_TO_TOTAL_EXP_TIMES_LOOKUP"
37
+ slit_velocity_mm_per_s = "SLIT_VELOCITY_MM_PER_S"
37
38
 
38
39
 
39
40
  class VispConstants(ConstantsBase):
@@ -196,3 +197,19 @@ class VispConstants(ConstantsBase):
196
197
  raw_return = self._db_dict[VispBudName.dark_readout_to_total_exp_times_lookup]
197
198
  # convert JSON string keys back to float
198
199
  return {float(k): v for k, v in raw_return.items()}
200
+
201
+ @property
202
+ def slit_velocity_mm_per_s(self) -> float:
203
+ """Return observation slit velocity [mm/s]."""
204
+ return self._db_dict[VispBudName.slit_velocity_mm_per_s.value]
205
+
206
+ @property
207
+ def is_sit_n_stare(self) -> bool:
208
+ """
209
+ Return True if the data were acquired in the intensity-only, no-slit-motion "sit'n'stare" mode.
210
+
211
+ The data are sit'n'stare if they were acquired in intensity mode with a slit velocity of
212
+ exactly zero (the slit never moved).
213
+ """
214
+ is_slit_stationary = self.slit_velocity_mm_per_s == 0.0
215
+ return self.polarimeter_mode == "observe_intensity" and is_slit_stationary
@@ -15,6 +15,7 @@ class VispMetadataKey(StrEnum):
15
15
  grating_angle_deg = "VSPGRTAN"
16
16
  arm_position_deg = "VSPARMPS"
17
17
  grating_constant_inverse_mm = "VSPGRTCN"
18
+ slit_velocity_mm_per_s = "VSPSLTSV"
18
19
  axis_1_type = "CTYPE1"
19
20
  axis_2_type = "CTYPE2"
20
21
  axis_3_type = "CTYPE3"
@@ -54,6 +54,10 @@ class VispL0FitsAccess(L0FitsAccess):
54
54
  self.grating_constant_inverse_mm: float = self.header[
55
55
  VispMetadataKey.grating_constant_inverse_mm
56
56
  ]
57
+ # Only present for intensity-mode data
58
+ self.slit_velocity_mm_per_s: float = self.header.get(
59
+ VispMetadataKey.slit_velocity_mm_per_s, HEADER_KEY_NOT_FOUND
60
+ )
57
61
  self.axis_1_type: str = self.header[VispMetadataKey.axis_1_type]
58
62
  self.axis_2_type: str = self.header[VispMetadataKey.axis_2_type]
59
63
  self.axis_3_type: str = self.header[VispMetadataKey.axis_3_type]
@@ -44,9 +44,16 @@ class AssembleVispMovie(AssembleMovie):
44
44
  def num_images(self) -> int:
45
45
  """Total number of images in final movie.
46
46
 
47
- Overloaded from `dkist-processing-common` because DSPS repeat does not correspond to map scan in ViSP
47
+ Overridden from `dkist-processing-common` because DSPS repeat does not correspond to map scan in ViSP.
48
+
49
+ This counts the movie frames actually written by `MakeVispMovieFrames`, rather than trusting
50
+ `self.constants.num_map_scans`. For the map-scan movie-frame path those two numbers are
51
+ always the same (one movie frame per map scan), but for the sit'n'stare movie-frame path a
52
+ single map scan can be chunked into many movie frames, so `num_map_scans` (which correctly
53
+ continues to describe the real map-scan structure of the science data, and is used as such
54
+ elsewhere) doesn't equal the number of movie frames in that case.
48
55
  """
49
- return self.constants.num_map_scans
56
+ return self.count(tags=[VispTag.movie_frame()])
50
57
 
51
58
  def tags_for_image_n(self, n: int) -> list[str]:
52
59
  """Return tags that grab the n'th movie image.
@@ -0,0 +1,351 @@
1
+ """Visp make movie frames task."""
2
+
3
+ import numpy as np
4
+ from astropy.io import fits
5
+ from astropy.visualization import ZScaleInterval
6
+ from dkist_processing_common.codecs.fits import fits_access_decoder
7
+ from dkist_processing_common.codecs.fits import fits_array_encoder
8
+ from dkist_processing_common.models.fits_access import MetadataKey
9
+ from dkist_service_configuration.logging import logger
10
+
11
+ from dkist_processing_visp.models.tags import VispTag
12
+ from dkist_processing_visp.parsers.visp_l1_fits_access import VispL1FitsAccess
13
+ from dkist_processing_visp.tasks.assemble_movie import AssembleVispMovie
14
+ from dkist_processing_visp.tasks.visp_base import VispTaskBase
15
+
16
+ __all__ = ["MakeVispMovieFrames"]
17
+
18
+ MOVIE_FRAME_WIDTH = AssembleVispMovie.MOVIE_FRAME_SHAPE[0]
19
+
20
+
21
+ class MakeVispMovieFrames(VispTaskBase):
22
+ """
23
+ Make ViSP movie frames and tag with VispTag.movie_frame().
24
+
25
+ Frames are built via one of two code paths, chosen per-dataset in `run` based on
26
+ `self.constants.is_sit_n_stare`:
27
+
28
+ Map-scan path (`make_map_scan_movie_frames`): genuine spatially-scanning maps, of any
29
+ polarization mode, build one movie frame per map scan.
30
+
31
+ Sit'n'stare path (`make_sit_n_stare_movie_frames`): the single map scan is split into
32
+ fixed-size chunks of raster steps (see `generate_sit_n_stare_chunk_ranges`), and one movie
33
+ frame, capped at `MOVIE_FRAME_WIDTH`, is built per chunk. The final chunk is zero-padded up to
34
+ full width if it's undersized, guaranteeing that any dataset with at least one raster step
35
+ produces at least one movie frame. `self.constants.num_map_scans == 1` is required and checked
36
+ explicitly, since sit'n'stare is defined as a single, continuous, non-moving-slit observation
37
+ and this path only ever reads/writes `map_scan=1` frames.
38
+
39
+ Parameters
40
+ ----------
41
+ recipe_run_id : int
42
+ id of the recipe run used to identify the workflow run this task is part of
43
+ workflow_name : str
44
+ name of the workflow to which this instance of the task belongs
45
+ workflow_version : str
46
+ version of the workflow to which this instance of the task belongs
47
+
48
+
49
+ """
50
+
51
+ def run(self) -> None:
52
+ """
53
+ Decide which movie-frame code path applies, based on `self.constants.is_sit_n_stare`, and build/write the frames.
54
+
55
+ Returns
56
+ -------
57
+ None
58
+ """
59
+ if self.constants.is_sit_n_stare:
60
+ logger.info(
61
+ "Sit'n'stare mode detected. Building movie frames by chunking raster steps."
62
+ )
63
+ with self.telemetry_span("Making and writing sit'n'stare movie frames"):
64
+ self.make_sit_n_stare_movie_frames()
65
+ else:
66
+ logger.info("Building one movie frame per map scan.")
67
+ with self.telemetry_span("Making and writing movie frames"):
68
+ self.make_map_scan_movie_frames()
69
+
70
+ @staticmethod
71
+ def generate_sit_n_stare_chunk_ranges(
72
+ num_raster_steps: int, chunk_size: int = MOVIE_FRAME_WIDTH
73
+ ) -> list[range]:
74
+ """
75
+ Split a number of raster steps into consecutive, non-overlapping ranges of at most `chunk_size`.
76
+
77
+ Parameters
78
+ ----------
79
+ num_raster_steps
80
+ Total number of raster steps (time samples) in the single map scan. Can be arbitrarily
81
+ large.
82
+
83
+ chunk_size
84
+ The number of raster steps per full range/movie frame. Defaults to `MOVIE_FRAME_WIDTH`
85
+ so the produced frames need no resizing before being handed to `AssembleVispMovie`.
86
+
87
+ Returns
88
+ -------
89
+ A list of `range` objects, in ascending, non-overlapping, time order, together covering every raster step from `0` to `num_raster_steps`.
90
+ """
91
+ num_chunks = -(-num_raster_steps // chunk_size) # ceiling division, no float round-trip
92
+ return [
93
+ range(i * chunk_size, min((i + 1) * chunk_size, num_raster_steps))
94
+ for i in range(num_chunks)
95
+ ]
96
+
97
+ def make_sit_n_stare_movie_frames(self) -> None:
98
+ """
99
+ For the single map scan, build one movie frame per fixed-size range of raster steps.
100
+
101
+ Stokes handling isn't needed here because this mode is intensity-only by definition.
102
+
103
+ For each range of up to `MOVIE_FRAME_WIDTH` sequential raster steps:
104
+
105
+ #. Integrate each raster step in the range over wavelength into a single column of
106
+ pixels
107
+ #. Build a movie frame by lining the columns up side by side, zero-padding out to
108
+ `MOVIE_FRAME_WIDTH` if this is a final, undersized chunk - guaranteeing at least one
109
+ frame is written even for a whole dataset smaller than `MOVIE_FRAME_WIDTH`
110
+ #. Write the frame as a "MOVIE_FRAME", tagged sequentially (chunk order, 1-indexed) -
111
+ *not* by any real map scan number, since there is only one
112
+
113
+ Raises
114
+ ------
115
+ ValueError
116
+ If `self.constants.num_map_scans != 1`. Sit'n'stare is defined as a single,
117
+ continuous, non-moving-slit observation, so this code path only ever reads/writes
118
+ `map_scan=1` frames.
119
+ """
120
+ num_map_scans = self.constants.num_map_scans
121
+ if num_map_scans != 1:
122
+ raise ValueError(
123
+ f"Sit'n'stare data is expected to have exactly one map scan, but {num_map_scans = }."
124
+ )
125
+
126
+ num_raster_steps = self.constants.num_raster_steps
127
+ chunk_ranges = self.generate_sit_n_stare_chunk_ranges(
128
+ num_raster_steps, chunk_size=MOVIE_FRAME_WIDTH
129
+ )
130
+
131
+ for chunk_index, raster_step_chunk_range in enumerate(chunk_ranges, start=1):
132
+ logger.info(
133
+ f"Making and writing sit'n'stare movie frame for {chunk_index = }, "
134
+ f"{raster_step_chunk_range = }"
135
+ )
136
+ (
137
+ movie_frame_data,
138
+ time_obs,
139
+ calibrated_frame,
140
+ ) = self.generate_frame_from_raster_steps(
141
+ stokes_state="I", map_scan=1, raster_steps=raster_step_chunk_range
142
+ )
143
+ # `num_raster_steps > 1` excludes the single-raster-step-in-the-whole-dataset
144
+ # special case in `generate_frame_from_raster_steps`, which returns the raw
145
+ # spectral frame (not a per-raster-step column stack) and so isn't paddable the same
146
+ # way; that one frame *is* the whole movie already, regardless of its native width.
147
+ if num_raster_steps > 1 and len(raster_step_chunk_range) < MOVIE_FRAME_WIDTH:
148
+ pad_width = MOVIE_FRAME_WIDTH - len(raster_step_chunk_range)
149
+ logger.info(
150
+ f"{chunk_index = } has only {len(raster_step_chunk_range)} raster "
151
+ f"steps; padding with {pad_width} zero-filled columns to reach "
152
+ f"{MOVIE_FRAME_WIDTH = }."
153
+ )
154
+ movie_frame_data = np.pad(
155
+ movie_frame_data, ((0, 0), (0, pad_width)), mode="constant"
156
+ )
157
+ header = self.build_movie_frame_header(
158
+ calibrated_frame=calibrated_frame,
159
+ time_obs=time_obs,
160
+ )
161
+ self.write_movie_frame(
162
+ movie_frame_data=movie_frame_data, header=header, index=chunk_index
163
+ )
164
+
165
+ def make_map_scan_movie_frames(self) -> None:
166
+ """
167
+ For each map scan, build one movie frame from all its raster steps, for each stokes state.
168
+
169
+ For each map scan:
170
+
171
+ #. For each stokes state present in the data:
172
+
173
+ #. Integrate each raster step in the scan over wavelength into a single column of
174
+ pixels
175
+ #. Build a movie frame by lining the columns up side by side
176
+
177
+ #. Combine stokes frames (normalizing across intensities if polarimetric)
178
+ #. Write the frame as a "MOVIE_FRAME", tagged by map scan number
179
+ """
180
+ stokes_states = ["I", "Q", "U", "V"]
181
+ # Loop over the number of raster scans
182
+ for map_scan in range(1, self.constants.num_map_scans + 1):
183
+ logger.info(f"Making movie frame for {map_scan = }")
184
+ stokes_frames = dict()
185
+ time_obs = []
186
+
187
+ # Loop over the stokes states to add them to the frame array
188
+ for stokes_state in stokes_states:
189
+ num_stokes_frames = self.count(
190
+ tags=[
191
+ VispTag.frame(),
192
+ VispTag.calibrated(),
193
+ VispTag.stokes(stokes_state),
194
+ ]
195
+ )
196
+ if num_stokes_frames > 0:
197
+ (
198
+ stokes_frames[stokes_state],
199
+ step_time_obs,
200
+ calibrated_frame,
201
+ ) = self.generate_frame_from_raster_steps(
202
+ stokes_state=stokes_state,
203
+ map_scan=map_scan,
204
+ raster_steps=range(0, self.constants.num_raster_steps),
205
+ )
206
+ time_obs += step_time_obs
207
+
208
+ header = self.build_movie_frame_header(
209
+ calibrated_frame=calibrated_frame,
210
+ time_obs=time_obs,
211
+ )
212
+ movie_frame_data = self.combine_stokes_frames(
213
+ stokes_frames, self.constants.correct_for_polarization
214
+ )
215
+
216
+ logger.info(f"Writing movie frame for {map_scan = }")
217
+ self.write_movie_frame(movie_frame_data=movie_frame_data, header=header, index=map_scan)
218
+
219
+ def read_calibrated_frame(
220
+ self, *, stokes_state: str, map_scan: int, raster_step: int
221
+ ) -> VispL1FitsAccess:
222
+ """Read a single calibrated frame for the given stokes state, map scan, and raster step."""
223
+ return next(
224
+ self.read(
225
+ tags=[
226
+ VispTag.frame(),
227
+ VispTag.calibrated(),
228
+ VispTag.stokes(stokes_state),
229
+ VispTag.map_scan(map_scan),
230
+ VispTag.raster_step(raster_step),
231
+ ],
232
+ decoder=fits_access_decoder,
233
+ fits_access_class=VispL1FitsAccess,
234
+ )
235
+ )
236
+
237
+ def generate_frame_from_raster_steps(
238
+ self, *, stokes_state: str, map_scan: int, raster_steps: range
239
+ ) -> tuple[np.ndarray, list[str], VispL1FitsAccess]:
240
+ """
241
+ Read and wavelength-integrate each raster step into a column, lining columns up side by side.
242
+
243
+ This is the piece of logic shared by both movie-frame code paths; only the `raster_steps`
244
+ being looped over (and whether this is called once per stokes state or just once for "I")
245
+ differs between them.
246
+
247
+ Parameters
248
+ ----------
249
+ stokes_state
250
+ Which stokes state to read ("I", "Q", "U", or "V")
251
+
252
+ map_scan
253
+ The map scan number to read frames from. Always `1` for the sit'n'stare path, since
254
+ that mode is defined as a single map scan.
255
+
256
+ raster_steps
257
+ The raster steps (in time order) to line up side-by-side into the frame. This is the
258
+ full `range(0, num_raster_steps)` for the per-map-scan path, or a single
259
+ `generate_sit_n_stare_chunk_ranges` window for the sit'n'stare path.
260
+
261
+ Returns
262
+ -------
263
+ The assembled 2D frame data (shape `(spectral_size, len(raster_steps))`, unless there's only a single raster step in the *entire dataset*, in which case the raw spectral frame is returned instead)
264
+
265
+ The list of time_obs values seen, in raster-step order
266
+
267
+ The `FitsAccess` object for the last-read raster step, used for movie-frame metadata
268
+ """
269
+ time_obs = []
270
+ frame_data = None
271
+
272
+ for i, raster_step in enumerate(raster_steps):
273
+ calibrated_frame = self.read_calibrated_frame(
274
+ stokes_state=stokes_state, map_scan=map_scan, raster_step=raster_step
275
+ )
276
+ data = np.nan_to_num(calibrated_frame.data, nan=0)
277
+ time_obs.append(calibrated_frame.time_obs)
278
+
279
+ if self.constants.num_raster_steps == 1:
280
+ logger.info("Only a single raster step found. Making a spectral movie.")
281
+ frame_data = data
282
+ else:
283
+ wavelength_integrated_data = np.sum(np.abs(data), axis=0)
284
+ if frame_data is None:
285
+ frame_data = np.empty((wavelength_integrated_data.shape[0], len(raster_steps)))
286
+ frame_data[:, i] = wavelength_integrated_data
287
+
288
+ return frame_data, time_obs, calibrated_frame
289
+
290
+ def build_movie_frame_header(
291
+ self,
292
+ *,
293
+ calibrated_frame: VispL1FitsAccess,
294
+ time_obs: list[str],
295
+ ) -> fits.Header:
296
+ """Build the header for a movie frame, updating `time_obs` to the start time of the raster scan."""
297
+ # Use the most recently read header as the base header because we need to be able to read it
298
+ # with VispL1FitsAccess.
299
+ header = fits.Header(calibrated_frame.header)
300
+ # The timestamp of a movie frame will be the time of raster scan start
301
+ header[MetadataKey.time_obs] = time_obs[0]
302
+ return header
303
+
304
+ @staticmethod
305
+ def combine_stokes_frames(
306
+ stokes_frames: dict[str, np.ndarray], is_polarized: bool
307
+ ) -> np.ndarray:
308
+ """Combine per-stokes frame data into the final movie frame, normalizing if polarimetric."""
309
+ if is_polarized:
310
+ i_norm = ZScaleInterval()(stokes_frames["I"])
311
+ q_norm = ZScaleInterval()(stokes_frames["Q"])
312
+ u_norm = ZScaleInterval()(stokes_frames["U"])
313
+ v_norm = ZScaleInterval()(stokes_frames["V"])
314
+ return np.concatenate(
315
+ (
316
+ np.concatenate((i_norm, q_norm), axis=1),
317
+ np.concatenate((u_norm, v_norm), axis=1),
318
+ ),
319
+ axis=0,
320
+ )
321
+ return stokes_frames["I"]
322
+
323
+ def write_movie_frame(
324
+ self, *, movie_frame_data: np.ndarray, header: fits.Header, index: int
325
+ ) -> None:
326
+ """
327
+ Write a single movie frame to disk, tagged with VispTag.movie_frame().
328
+
329
+ Parameters
330
+ ----------
331
+ movie_frame_data
332
+ The 2D frame array to write
333
+
334
+ header
335
+ The header to write alongside the frame
336
+
337
+ index
338
+ The sequential index used to tag the frame (reusing the `map_scan` tag stem). For the
339
+ map-scan path this is the real map scan number. For the sit'n'stare path this is the
340
+ 1-indexed chunk number - *not* a real map scan number, since sit'n'stare data always
341
+ has exactly one.
342
+ """
343
+ self.write(
344
+ data=movie_frame_data,
345
+ tags=[
346
+ VispTag.map_scan(index),
347
+ VispTag.movie_frame(),
348
+ ],
349
+ encoder=fits_array_encoder,
350
+ header=header,
351
+ )
@@ -90,6 +90,11 @@ class ParseL0VispInputData(ParseL0InputDataBase):
90
90
  TotalRasterStepsBud(),
91
91
  NumCSStepBud(max_cs_step_time_sec=self.parameters.max_cs_step_time_sec),
92
92
  NumFramesPerCSStepBud(max_cs_step_time_sec=self.parameters.max_cs_step_time_sec),
93
+ TaskUniqueBud(
94
+ constant_name=VispBudName.slit_velocity_mm_per_s.value,
95
+ metadata_key=VispMetadataKey.slit_velocity_mm_per_s,
96
+ ip_task_types=TaskName.observe.value,
97
+ ),
93
98
  ObsIpStartTimeBud(),
94
99
  NumberModulatorStatesBud(),
95
100
  ObserveWavelengthBud(),
@@ -20,6 +20,7 @@ from dkist_header_validator.translator import translate_spec122_to_spec214_l0
20
20
  from dkist_processing_common.codecs.basemodel import basemodel_encoder
21
21
  from dkist_processing_common.codecs.fits import fits_array_encoder
22
22
  from dkist_processing_common.codecs.fits import fits_hdulist_encoder
23
+ from dkist_processing_common.models.fits_access import HEADER_KEY_NOT_FOUND
23
24
  from dkist_processing_common.models.input_dataset import InputDatasetPartDocumentList
24
25
  from dkist_processing_common.tasks import WorkflowTaskBase
25
26
 
@@ -53,6 +54,11 @@ def init_visp_constants_db():
53
54
  class VispConstantsDb:
54
55
  ARM_ID: int = 1
55
56
  POLARIMETER_MODE: str = "observe_polarimetric"
57
+ # Real polarimetric data never has this header key at all; every other default in this
58
+ # dataclass already models a non-sit'n'stare dataset, so this sentinel default is consistent
59
+ # with that: `VispConstants.is_sit_n_stare` is False regardless of this value whenever
60
+ # POLARIMETER_MODE isn't "observe_intensity".
61
+ SLIT_VELOCITY_MM_PER_S: float | str = HEADER_KEY_NOT_FOUND
56
62
  OBS_IP_START_TIME: str = "2022-11-28T13:54:00"
57
63
  NUM_MODSTATES: int = 10
58
64
  NUM_MAP_SCANS: int = 2
@@ -712,6 +718,7 @@ def write_calibrated_frames_to_task(
712
718
  wcs_axis_names: tuple[str, str] = ("HPLT-TAN", "AWAV"),
713
719
  num_maps: int = 1,
714
720
  num_steps: int = 1,
721
+ slit_velocity_mm_per_s: float = 1.5,
715
722
  ):
716
723
  array_shape = (1, *data_shape)
717
724
  dataset = VispHeadersValidCalibratedFrames(
@@ -721,6 +728,7 @@ def write_calibrated_frames_to_task(
721
728
  num_raster_steps=num_steps,
722
729
  polarimeter_mode=pol_mode,
723
730
  wcs_axis_names=wcs_axis_names,
731
+ slit_velocity_mm_per_s=slit_velocity_mm_per_s,
724
732
  )
725
733
 
726
734
  num_written_frames = write_frames_to_task(