dkist-processing-vbi 2.0.1rc2__tar.gz → 2.1.1__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 (103) hide show
  1. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/CHANGELOG.rst +49 -0
  2. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/PKG-INFO +64 -61
  3. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/models/filter.py +1 -1
  4. dkist_processing_vbi-2.1.1/dkist_processing_vbi/tasks/science.py +298 -0
  5. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/tests/test_science.py +10 -2
  6. dkist_processing_vbi-2.1.1/dkist_processing_vbi/workflows/l0_processing.py +98 -0
  7. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/workflows/summit_data_processing.py +2 -2
  8. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/workflows/trial_workflows.py +19 -19
  9. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi.egg-info/PKG-INFO +64 -61
  10. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi.egg-info/SOURCES.txt +3 -5
  11. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi.egg-info/requires.txt +63 -60
  12. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi.egg-info/scm_file_list.json +3 -5
  13. dkist_processing_vbi-2.1.1/dkist_processing_vbi.egg-info/scm_version.json +8 -0
  14. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/docs/index.rst +3 -2
  15. dkist_processing_vbi-2.1.1/docs/l0_to_l1_vbi.rst +21 -0
  16. dkist_processing_vbi-2.1.1/docs/l0_to_l1_vbi_bulk.rst +18 -0
  17. dkist_processing_vbi-2.1.1/docs/l0_to_l1_vbi_full-trial.rst +22 -0
  18. dkist_processing_vbi-2.1.1/docs/l0_to_l1_vbi_summit-calibrated-full-trial.rst +18 -0
  19. dkist_processing_vbi-2.1.1/docs/l0_to_l1_vbi_summit-calibrated.rst +19 -0
  20. dkist_processing_vbi-2.1.1/docs/landing_page.rst +31 -0
  21. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/pyproject.toml +65 -62
  22. dkist_processing_vbi-2.0.1rc2/changelog/223.misc.1.rst +0 -3
  23. dkist_processing_vbi-2.0.1rc2/changelog/223.misc.2.rst +0 -3
  24. dkist_processing_vbi-2.0.1rc2/changelog/223.misc.rst +0 -3
  25. dkist_processing_vbi-2.0.1rc2/dkist_processing_vbi/tasks/science.py +0 -195
  26. dkist_processing_vbi-2.0.1rc2/dkist_processing_vbi/workflows/l0_processing.py +0 -74
  27. dkist_processing_vbi-2.0.1rc2/dkist_processing_vbi.egg-info/scm_version.json +0 -8
  28. dkist_processing_vbi-2.0.1rc2/docs/l0_to_l1_vbi_no-speckle-full-trial.rst +0 -11
  29. dkist_processing_vbi-2.0.1rc2/docs/l0_to_l1_vbi_no-speckle.rst +0 -13
  30. dkist_processing_vbi-2.0.1rc2/docs/l0_to_l1_vbi_summit-calibrated-full-trial.rst +0 -11
  31. dkist_processing_vbi-2.0.1rc2/docs/l0_to_l1_vbi_summit-calibrated.rst +0 -10
  32. dkist_processing_vbi-2.0.1rc2/docs/landing_page.rst +0 -18
  33. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/.gitignore +0 -0
  34. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/.pre-commit-config.yaml +0 -0
  35. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/.readthedocs.yml +0 -0
  36. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/.rovodev/.review-agent.md +0 -0
  37. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/.snyk +0 -0
  38. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/README.rst +0 -0
  39. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/SCIENCE_CHANGELOG.rst +0 -0
  40. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/bitbucket-pipelines.yml +0 -0
  41. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/changelog/.gitempty +0 -0
  42. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/__init__.py +0 -0
  43. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/config.py +0 -0
  44. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/models/__init__.py +0 -0
  45. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/models/burst.py +0 -0
  46. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/models/constants.py +0 -0
  47. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/models/dataset_extras.py +0 -0
  48. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/models/fits_access.py +0 -0
  49. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/models/parameters.py +0 -0
  50. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/models/tags.py +0 -0
  51. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/models/task_name.py +0 -0
  52. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/parsers/__init__.py +0 -0
  53. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/parsers/burst.py +0 -0
  54. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/parsers/mosaic_repeats.py +0 -0
  55. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/parsers/spatial_step_pattern.py +0 -0
  56. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/parsers/time.py +0 -0
  57. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/parsers/vbi_l0_fits_access.py +0 -0
  58. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/parsers/vbi_l1_fits_access.py +0 -0
  59. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/tasks/__init__.py +0 -0
  60. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/tasks/assemble_movie.py +0 -0
  61. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/tasks/dark.py +0 -0
  62. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/tasks/gain.py +0 -0
  63. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/tasks/make_movie_frames.py +0 -0
  64. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/tasks/parse.py +0 -0
  65. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/tasks/process_summit_processed.py +0 -0
  66. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/tasks/quality_metrics.py +0 -0
  67. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/tasks/vbi_base.py +0 -0
  68. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/tasks/write_dataset_extras.py +0 -0
  69. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/tasks/write_l1.py +0 -0
  70. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/tests/__init__.py +0 -0
  71. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/tests/conftest.py +0 -0
  72. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/tests/local_trial_workflows/__init__.py +0 -0
  73. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/tests/local_trial_workflows/l0_to_l1.py +0 -0
  74. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/tests/test_assemble_movie.py +0 -0
  75. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/tests/test_dark.py +0 -0
  76. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/tests/test_dataset_extras.py +0 -0
  77. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/tests/test_fits_access.py +0 -0
  78. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/tests/test_gain.py +0 -0
  79. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/tests/test_make_movie_frames.py +0 -0
  80. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/tests/test_parse_l0.py +0 -0
  81. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/tests/test_parse_summit.py +0 -0
  82. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/tests/test_process_summit.py +0 -0
  83. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/tests/test_quality_metrics.py +0 -0
  84. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/tests/test_spatial_step_pattern.py +0 -0
  85. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/tests/test_vbi_base.py +0 -0
  86. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/tests/test_vbi_constants.py +0 -0
  87. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/tests/test_workflows.py +0 -0
  88. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/tests/test_write_l1.py +0 -0
  89. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi/workflows/__init__.py +0 -0
  90. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi.egg-info/dependency_links.txt +0 -0
  91. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/dkist_processing_vbi.egg-info/top_level.txt +0 -0
  92. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/docs/Makefile +0 -0
  93. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/docs/changelog.rst +0 -0
  94. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/docs/conf.py +0 -0
  95. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/docs/dataset_extras.rst +0 -0
  96. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/docs/make.bat +0 -0
  97. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/docs/requirements.txt +0 -0
  98. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/docs/requirements_table.rst +0 -0
  99. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/docs/scientific_changelog.rst +0 -0
  100. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/licenses/LICENSE.rst +0 -0
  101. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/science_towncrier.sh +0 -0
  102. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/setup.cfg +0 -0
  103. {dkist_processing_vbi-2.0.1rc2 → dkist_processing_vbi-2.1.1}/towncrier_science.toml +0 -0
@@ -1,3 +1,52 @@
1
+ v2.1.1 (2026-09-30)
2
+ ===================
3
+
4
+ Misc
5
+ ----
6
+
7
+ - Update dkist-processing-common to 14.5.2 to restore provenance recording for
8
+ manual parse tasks and trial dataset inventory, ASDF, and quality report tasks. (`#236 <https://bitbucket.org/dkistdc/dkist-processing-vbi/pull-requests/236>`__)
9
+
10
+
11
+ v2.1.0 (2026-09-28)
12
+ ===================
13
+
14
+ Features
15
+ --------
16
+
17
+ - Add the ``l0_to_l1_vbi_bulk`` workflow for bulk processing of L0 data, using the default resource queue for science calibration. (`#233 <https://bitbucket.org/dkistdc/dkist-processing-vbi/pull-requests/233>`__)
18
+ - Enable multi-core parallel science frame calibration in the default VBI pipeline. (`#233 <https://bitbucket.org/dkistdc/dkist-processing-vbi/pull-requests/233>`__)
19
+
20
+
21
+ Misc
22
+ ----
23
+
24
+ - Rename workflows ``l0_to_l1_vbi_no-speckle`` to ``l0_to_l1_vbi`` and ``l0_to_l1_vbi_no-speckle-full-trial`` to ``l0_to_l1_vbi_full-trial``. (`#233 <https://bitbucket.org/dkistdc/dkist-processing-vbi/pull-requests/233>`__)
25
+
26
+
27
+ v2.0.1 (2026-09-24)
28
+ ===================
29
+
30
+ Bugfixes
31
+ --------
32
+
33
+ - Fix the typo in the name for the "VBI-Red Fe XI" filter. (`#235 <https://bitbucket.org/dkistdc/dkist-processing-vbi/pull-requests/235>`__)
34
+
35
+
36
+ Misc
37
+ ----
38
+
39
+ - Use the "level0_spec214" file schema keyword argument in test Datasets.
40
+ With this keyword, subsequent translation of the resulting headers to
41
+ Spec 214 is not necessary. (`#223 <https://bitbucket.org/dkistdc/dkist-processing-vbi/pull-requests/223>`__)
42
+ - Use dkist-fits-specifications v4.29.0, which includes the Trend FITS specification.
43
+ Use dkist-header-validator v6.0.0, a significant refactor that allows multi-HDU objects to be validated.
44
+ Use dkist-processing-common v14.5.0, which accommodates the changes to the header validator. (`#223 <https://bitbucket.org/dkistdc/dkist-processing-vbi/pull-requests/223>`__)
45
+ - Validators no longer have validate-and-translate methods.
46
+ Where still necessary, replace validate-and-translate calls with a call to
47
+ validate and then a call to translate. (`#223 <https://bitbucket.org/dkistdc/dkist-processing-vbi/pull-requests/223>`__)
48
+
49
+
1
50
  v2.0.0 (2026-09-03)
2
51
  ===================
3
52
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dkist-processing-vbi
3
- Version: 2.0.1rc2
3
+ Version: 2.1.1
4
4
  Summary: Processing code for the VBI instrument on DKIST
5
5
  Author-email: NSO / AURA <dkistdc@nso.edu>
6
6
  License: BSD-3-Clause
@@ -13,10 +13,10 @@ Classifier: Programming Language :: Python :: 3
13
13
  Classifier: Programming Language :: Python :: 3.13
14
14
  Requires-Python: >=3.13
15
15
  Description-Content-Type: text/x-rst
16
- Requires-Dist: dkist-processing-common==14.5.0rc3
16
+ Requires-Dist: dkist-processing-common==14.5.2
17
17
  Requires-Dist: dkist-processing-math==2.2.3
18
- Requires-Dist: dkist-header-validator==6.0.0rc1
19
- Requires-Dist: dkist-fits-specifications==4.29.0rc7
18
+ Requires-Dist: dkist-header-validator==6.0.0
19
+ Requires-Dist: dkist-fits-specifications==4.29.0
20
20
  Requires-Dist: astropy==7.0.2
21
21
  Requires-Dist: numpy==2.2.5
22
22
  Requires-Dist: sunpy==6.1.1
@@ -55,19 +55,19 @@ Requires-Dist: sphinx-autoapi; extra == "docs"
55
55
  Requires-Dist: pytest; extra == "docs"
56
56
  Requires-Dist: dkist-sphinx-theme; extra == "docs"
57
57
  Provides-Extra: frozen
58
- Requires-Dist: Deprecated==1.3.1; extra == "frozen"
59
- Requires-Dist: ImageIO==2.37.4; extra == "frozen"
58
+ Requires-Dist: Deprecated==3.0.0; extra == "frozen"
59
+ Requires-Dist: ImageIO==2.38.0; extra == "frozen"
60
60
  Requires-Dist: Jinja2==3.1.6; extra == "frozen"
61
61
  Requires-Dist: Mako==1.4.3; extra == "frozen"
62
62
  Requires-Dist: MarkupSafe==3.0.3; extra == "frozen"
63
63
  Requires-Dist: PIMS==0.7; extra == "frozen"
64
64
  Requires-Dist: PeakUtils==1.3.5; extra == "frozen"
65
65
  Requires-Dist: PyAVM==0.9.9; extra == "frozen"
66
- Requires-Dist: PyJWT==2.15.0; extra == "frozen"
66
+ Requires-Dist: PyJWT==2.15.1; extra == "frozen"
67
67
  Requires-Dist: PyYAML==6.0.3; extra == "frozen"
68
68
  Requires-Dist: PyYAML-ft==8.0.0; extra == "frozen"
69
69
  Requires-Dist: Pygments==2.21.0; extra == "frozen"
70
- Requires-Dist: SQLAlchemy==2.0.54; extra == "frozen"
70
+ Requires-Dist: SQLAlchemy==2.1.1; extra == "frozen"
71
71
  Requires-Dist: Send2Trash==2.1.0; extra == "frozen"
72
72
  Requires-Dist: a2wsgi==1.10.10; extra == "frozen"
73
73
  Requires-Dist: aioftp==0.28.3; extra == "frozen"
@@ -83,14 +83,15 @@ Requires-Dist: annotated-types==0.8.0; extra == "frozen"
83
83
  Requires-Dist: anyio==4.15.1; extra == "frozen"
84
84
  Requires-Dist: apache-airflow==3.3.1; extra == "frozen"
85
85
  Requires-Dist: apache-airflow-core==3.3.1; extra == "frozen"
86
- Requires-Dist: apache-airflow-providers-celery==3.24.0; extra == "frozen"
87
- Requires-Dist: apache-airflow-providers-common-compat==1.19.0; extra == "frozen"
88
- Requires-Dist: apache-airflow-providers-common-io==1.9.0; extra == "frozen"
89
- Requires-Dist: apache-airflow-providers-common-sql==2.1.1; extra == "frozen"
90
- Requires-Dist: apache-airflow-providers-postgres==7.0.2; extra == "frozen"
91
- Requires-Dist: apache-airflow-providers-smtp==3.0.4; extra == "frozen"
92
- Requires-Dist: apache-airflow-providers-standard==1.19.0; extra == "frozen"
86
+ Requires-Dist: apache-airflow-providers-celery==3.24.1; extra == "frozen"
87
+ Requires-Dist: apache-airflow-providers-common-compat==1.20.0; extra == "frozen"
88
+ Requires-Dist: apache-airflow-providers-common-io==1.10.0; extra == "frozen"
89
+ Requires-Dist: apache-airflow-providers-common-sql==2.2.0; extra == "frozen"
90
+ Requires-Dist: apache-airflow-providers-postgres==7.1.0; extra == "frozen"
91
+ Requires-Dist: apache-airflow-providers-smtp==3.1.0; extra == "frozen"
92
+ Requires-Dist: apache-airflow-providers-standard==1.20.0; extra == "frozen"
93
93
  Requires-Dist: apache-airflow-task-sdk==1.3.1; extra == "frozen"
94
+ Requires-Dist: appnope==1.0.0; extra == "frozen"
94
95
  Requires-Dist: argcomplete==3.7.2; extra == "frozen"
95
96
  Requires-Dist: argon2-cffi==25.1.0; extra == "frozen"
96
97
  Requires-Dist: argon2-cffi-bindings==26.1.0; extra == "frozen"
@@ -100,7 +101,7 @@ Requires-Dist: asdf_standard==1.5.0; extra == "frozen"
100
101
  Requires-Dist: asgiref==3.12.1; extra == "frozen"
101
102
  Requires-Dist: asteval==1.0.10; extra == "frozen"
102
103
  Requires-Dist: astropy==7.0.2; extra == "frozen"
103
- Requires-Dist: astropy-iers-data==0.2026.9.21.0.56.25; extra == "frozen"
104
+ Requires-Dist: astropy-iers-data==0.2026.9.28.0.59.37; extra == "frozen"
104
105
  Requires-Dist: astropy_healpix==2.0.1; extra == "frozen"
105
106
  Requires-Dist: asttokens==3.0.2; extra == "frozen"
106
107
  Requires-Dist: async-lru==2.3.0; extra == "frozen"
@@ -110,14 +111,14 @@ Requires-Dist: babel==2.18.0; extra == "frozen"
110
111
  Requires-Dist: beautifulsoup4==4.15.0; extra == "frozen"
111
112
  Requires-Dist: billiard==4.3.0; extra == "frozen"
112
113
  Requires-Dist: bleach==6.4.0; extra == "frozen"
113
- Requires-Dist: boto3==1.43.101; extra == "frozen"
114
- Requires-Dist: botocore==1.43.101; extra == "frozen"
114
+ Requires-Dist: boto3==1.43.106; extra == "frozen"
115
+ Requires-Dist: botocore==1.43.106; extra == "frozen"
115
116
  Requires-Dist: cachetools==7.2.0; extra == "frozen"
116
117
  Requires-Dist: cadwyn==7.0.0; extra == "frozen"
117
118
  Requires-Dist: celery==5.6.3; extra == "frozen"
118
119
  Requires-Dist: certifi==2026.7.22; extra == "frozen"
119
120
  Requires-Dist: cffi==2.1.1; extra == "frozen"
120
- Requires-Dist: charset-normalizer==3.5.1; extra == "frozen"
121
+ Requires-Dist: charset-normalizer==3.5.2; extra == "frozen"
121
122
  Requires-Dist: click==8.5.0; extra == "frozen"
122
123
  Requires-Dist: click-didyoumean==0.3.1; extra == "frozen"
123
124
  Requires-Dist: click-plugins==1.1.1.2; extra == "frozen"
@@ -126,9 +127,9 @@ Requires-Dist: cloudpickle==3.1.2; extra == "frozen"
126
127
  Requires-Dist: colorlog==6.12.0; extra == "frozen"
127
128
  Requires-Dist: comm==0.2.3; extra == "frozen"
128
129
  Requires-Dist: contourpy==1.4.0; extra == "frozen"
129
- Requires-Dist: cron_descriptor==2.1.0; extra == "frozen"
130
+ Requires-Dist: cron_descriptor==2.1.1; extra == "frozen"
130
131
  Requires-Dist: croniter==6.2.4; extra == "frozen"
131
- Requires-Dist: cryptography==50.0.1; extra == "frozen"
132
+ Requires-Dist: cryptography==50.0.2; extra == "frozen"
132
133
  Requires-Dist: cycler==0.12.1; extra == "frozen"
133
134
  Requires-Dist: dask==2026.8.0; extra == "frozen"
134
135
  Requires-Dist: dask-image==2026.5.0; extra == "frozen"
@@ -136,15 +137,15 @@ Requires-Dist: debugpy==1.8.22; extra == "frozen"
136
137
  Requires-Dist: decorator==5.3.1; extra == "frozen"
137
138
  Requires-Dist: defusedxml==0.7.1; extra == "frozen"
138
139
  Requires-Dist: dill==0.4.1; extra == "frozen"
139
- Requires-Dist: dkist-header-validator==6.0.0rc1; extra == "frozen"
140
- Requires-Dist: dkist-processing-common==14.5.0rc3; extra == "frozen"
140
+ Requires-Dist: dkist-header-validator==6.0.0; extra == "frozen"
141
+ Requires-Dist: dkist-processing-common==14.5.2; extra == "frozen"
141
142
  Requires-Dist: dkist-processing-core==7.5.0; extra == "frozen"
142
143
  Requires-Dist: dkist-processing-math==2.2.3; extra == "frozen"
143
144
  Requires-Dist: dkist-processing-pac==3.1.2; extra == "frozen"
144
- Requires-Dist: dkist-processing-vbi==2.0.1rc2; extra == "frozen"
145
+ Requires-Dist: dkist-processing-vbi==2.1.1; extra == "frozen"
145
146
  Requires-Dist: dkist-service-configuration==4.3.0; extra == "frozen"
146
147
  Requires-Dist: dkist-spectral-lines==3.0.1; extra == "frozen"
147
- Requires-Dist: dkist_fits_specifications==4.29.0rc7; extra == "frozen"
148
+ Requires-Dist: dkist_fits_specifications==4.29.0; extra == "frozen"
148
149
  Requires-Dist: dnspython==2.8.0; extra == "frozen"
149
150
  Requires-Dist: donfig==0.8.1.post1; extra == "frozen"
150
151
  Requires-Dist: email-validator==2.3.0; extra == "frozen"
@@ -153,13 +154,13 @@ Requires-Dist: fastapi==0.136.3; extra == "frozen"
153
154
  Requires-Dist: fastapi-cli==0.0.32; extra == "frozen"
154
155
  Requires-Dist: fastjsonschema==2.22.2; extra == "frozen"
155
156
  Requires-Dist: flower==2.2.0; extra == "frozen"
156
- Requires-Dist: fonttools==4.66.0; extra == "frozen"
157
- Requires-Dist: fqdn==1.5.1; extra == "frozen"
157
+ Requires-Dist: fonttools==4.66.1; extra == "frozen"
158
+ Requires-Dist: fqdn==1.6.0; extra == "frozen"
158
159
  Requires-Dist: frozenlist==1.8.0; extra == "frozen"
159
160
  Requires-Dist: fsspec==2026.9.0; extra == "frozen"
160
- Requires-Dist: globus-sdk==4.9.0; extra == "frozen"
161
- Requires-Dist: google-crc32c==1.8.0; extra == "frozen"
162
- Requires-Dist: googleapis-common-protos==1.75.3; extra == "frozen"
161
+ Requires-Dist: globus-sdk==4.9.1; extra == "frozen"
162
+ Requires-Dist: google-crc32c==1.9.0; extra == "frozen"
163
+ Requires-Dist: googleapis-common-protos==1.75.5; extra == "frozen"
163
164
  Requires-Dist: gqlclient==2.0.0; extra == "frozen"
164
165
  Requires-Dist: greenback==1.3.0; extra == "frozen"
165
166
  Requires-Dist: greenlet==3.5.6; extra == "frozen"
@@ -172,7 +173,7 @@ Requires-Dist: humanize==4.16.0; extra == "frozen"
172
173
  Requires-Dist: idna==3.20; extra == "frozen"
173
174
  Requires-Dist: imageio-ffmpeg==0.6.0; extra == "frozen"
174
175
  Requires-Dist: importlib_metadata==9.0.1; extra == "frozen"
175
- Requires-Dist: ipykernel==7.3.0; extra == "frozen"
176
+ Requires-Dist: ipykernel==7.4.0; extra == "frozen"
176
177
  Requires-Dist: ipython==9.17.1; extra == "frozen"
177
178
  Requires-Dist: ipython_pygments_lexers==1.1.1; extra == "frozen"
178
179
  Requires-Dist: isoduration==20.11.0; extra == "frozen"
@@ -212,7 +213,7 @@ Requires-Dist: methodtools==0.4.7; extra == "frozen"
212
213
  Requires-Dist: mistune==3.3.4; extra == "frozen"
213
214
  Requires-Dist: more-itertools==11.1.0; extra == "frozen"
214
215
  Requires-Dist: moviepy==2.2.1; extra == "frozen"
215
- Requires-Dist: msgspec==0.21.1; extra == "frozen"
216
+ Requires-Dist: msgspec==0.22.0; extra == "frozen"
216
217
  Requires-Dist: multidict==6.9.1; extra == "frozen"
217
218
  Requires-Dist: natsort==8.4.0; extra == "frozen"
218
219
  Requires-Dist: nbclient==0.11.0; extra == "frozen"
@@ -225,30 +226,32 @@ Requires-Dist: notebook_shim==0.2.4; extra == "frozen"
225
226
  Requires-Dist: numcodecs==0.17.0; extra == "frozen"
226
227
  Requires-Dist: numpy==2.2.5; extra == "frozen"
227
228
  Requires-Dist: object-clerk==1.0.0; extra == "frozen"
228
- Requires-Dist: opentelemetry-api==1.44.0; extra == "frozen"
229
- Requires-Dist: opentelemetry-exporter-otlp==1.44.0; extra == "frozen"
230
- Requires-Dist: opentelemetry-exporter-otlp-proto-common==1.44.0; extra == "frozen"
231
- Requires-Dist: opentelemetry-exporter-otlp-proto-grpc==1.44.0; extra == "frozen"
232
- Requires-Dist: opentelemetry-exporter-otlp-proto-http==1.44.0; extra == "frozen"
233
- Requires-Dist: opentelemetry-instrumentation==0.65b0; extra == "frozen"
234
- Requires-Dist: opentelemetry-instrumentation-aiohttp-client==0.65b0; extra == "frozen"
235
- Requires-Dist: opentelemetry-instrumentation-asgi==0.65b0; extra == "frozen"
236
- Requires-Dist: opentelemetry-instrumentation-botocore==0.65b0; extra == "frozen"
237
- Requires-Dist: opentelemetry-instrumentation-celery==0.65b0; extra == "frozen"
238
- Requires-Dist: opentelemetry-instrumentation-dbapi==0.65b0; extra == "frozen"
239
- Requires-Dist: opentelemetry-instrumentation-fastapi==0.65b0; extra == "frozen"
240
- Requires-Dist: opentelemetry-instrumentation-pika==0.65b0; extra == "frozen"
241
- Requires-Dist: opentelemetry-instrumentation-psycopg2==0.65b0; extra == "frozen"
242
- Requires-Dist: opentelemetry-instrumentation-pymongo==0.65b0; extra == "frozen"
243
- Requires-Dist: opentelemetry-instrumentation-redis==0.65b0; extra == "frozen"
244
- Requires-Dist: opentelemetry-instrumentation-requests==0.65b0; extra == "frozen"
245
- Requires-Dist: opentelemetry-instrumentation-sqlalchemy==0.65b0; extra == "frozen"
246
- Requires-Dist: opentelemetry-instrumentation-system-metrics==0.65b0; extra == "frozen"
229
+ Requires-Dist: opentelemetry-api==1.45.0; extra == "frozen"
230
+ Requires-Dist: opentelemetry-exporter-http-transport==0.66b0; extra == "frozen"
231
+ Requires-Dist: opentelemetry-exporter-otlp==1.45.0; extra == "frozen"
232
+ Requires-Dist: opentelemetry-exporter-otlp-common==0.66b0; extra == "frozen"
233
+ Requires-Dist: opentelemetry-exporter-otlp-proto-common==1.45.0; extra == "frozen"
234
+ Requires-Dist: opentelemetry-exporter-otlp-proto-grpc==1.45.0; extra == "frozen"
235
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http==1.45.0; extra == "frozen"
236
+ Requires-Dist: opentelemetry-instrumentation==0.66b0; extra == "frozen"
237
+ Requires-Dist: opentelemetry-instrumentation-aiohttp-client==0.66b0; extra == "frozen"
238
+ Requires-Dist: opentelemetry-instrumentation-asgi==0.66b0; extra == "frozen"
239
+ Requires-Dist: opentelemetry-instrumentation-botocore==0.66b0; extra == "frozen"
240
+ Requires-Dist: opentelemetry-instrumentation-celery==0.66b0; extra == "frozen"
241
+ Requires-Dist: opentelemetry-instrumentation-dbapi==0.66b0; extra == "frozen"
242
+ Requires-Dist: opentelemetry-instrumentation-fastapi==0.66b0; extra == "frozen"
243
+ Requires-Dist: opentelemetry-instrumentation-pika==0.66b0; extra == "frozen"
244
+ Requires-Dist: opentelemetry-instrumentation-psycopg2==0.66b0; extra == "frozen"
245
+ Requires-Dist: opentelemetry-instrumentation-pymongo==0.66b0; extra == "frozen"
246
+ Requires-Dist: opentelemetry-instrumentation-redis==0.66b0; extra == "frozen"
247
+ Requires-Dist: opentelemetry-instrumentation-requests==0.66b0; extra == "frozen"
248
+ Requires-Dist: opentelemetry-instrumentation-sqlalchemy==0.66b0; extra == "frozen"
249
+ Requires-Dist: opentelemetry-instrumentation-system-metrics==0.66b0; extra == "frozen"
247
250
  Requires-Dist: opentelemetry-propagator-aws-xray==1.0.2; extra == "frozen"
248
- Requires-Dist: opentelemetry-proto==1.44.0; extra == "frozen"
249
- Requires-Dist: opentelemetry-sdk==1.44.0; extra == "frozen"
250
- Requires-Dist: opentelemetry-semantic-conventions==0.65b0; extra == "frozen"
251
- Requires-Dist: opentelemetry-util-http==0.65b0; extra == "frozen"
251
+ Requires-Dist: opentelemetry-proto==1.45.0; extra == "frozen"
252
+ Requires-Dist: opentelemetry-sdk==1.45.0; extra == "frozen"
253
+ Requires-Dist: opentelemetry-semantic-conventions==0.66b0; extra == "frozen"
254
+ Requires-Dist: opentelemetry-util-http==0.66b0; extra == "frozen"
252
255
  Requires-Dist: outcome==1.3.0.post0; extra == "frozen"
253
256
  Requires-Dist: packaging==26.3; extra == "frozen"
254
257
  Requires-Dist: pandas==3.0.6; extra == "frozen"
@@ -263,7 +266,7 @@ Requires-Dist: pexpect==4.9.0; extra == "frozen"
263
266
  Requires-Dist: pika==1.4.4; extra == "frozen"
264
267
  Requires-Dist: pillow==10.4.0; extra == "frozen"
265
268
  Requires-Dist: pip==26.2.1; extra == "frozen"
266
- Requires-Dist: platformdirs==4.11.12; extra == "frozen"
269
+ Requires-Dist: platformdirs==4.12.2; extra == "frozen"
267
270
  Requires-Dist: pluggy==1.6.0; extra == "frozen"
268
271
  Requires-Dist: pooch==1.9.0; extra == "frozen"
269
272
  Requires-Dist: proglog==0.1.12; extra == "frozen"
@@ -290,8 +293,8 @@ Requires-Dist: python-dateutil==2.9.0.post0; extra == "frozen"
290
293
  Requires-Dist: python-dotenv==1.2.3; extra == "frozen"
291
294
  Requires-Dist: python-json-logger==4.2.0; extra == "frozen"
292
295
  Requires-Dist: python-multipart==0.0.32; extra == "frozen"
293
- Requires-Dist: python-slugify==9.1.1; extra == "frozen"
294
- Requires-Dist: pytz==2026.3.post1; extra == "frozen"
296
+ Requires-Dist: python-slugify==9.1.2; extra == "frozen"
297
+ Requires-Dist: pytz==2026.4; extra == "frozen"
295
298
  Requires-Dist: pyzmq==27.2.0; extra == "frozen"
296
299
  Requires-Dist: redis==6.4.0; extra == "frozen"
297
300
  Requires-Dist: referencing==0.37.0; extra == "frozen"
@@ -314,7 +317,7 @@ Requires-Dist: six==1.17.0; extra == "frozen"
314
317
  Requires-Dist: slicerator==1.1.0; extra == "frozen"
315
318
  Requires-Dist: sniffio==1.3.1; extra == "frozen"
316
319
  Requires-Dist: solar-wavelength-calibration==2.0.3; extra == "frozen"
317
- Requires-Dist: soupsieve==2.9.2; extra == "frozen"
320
+ Requires-Dist: soupsieve==2.10; extra == "frozen"
318
321
  Requires-Dist: sqids==0.5.1; extra == "frozen"
319
322
  Requires-Dist: sqlparse==0.6.0; extra == "frozen"
320
323
  Requires-Dist: stack-data==0.6.3; extra == "frozen"
@@ -344,7 +347,7 @@ Requires-Dist: universal_pathlib==0.3.10; extra == "frozen"
344
347
  Requires-Dist: uri-template==1.3.0; extra == "frozen"
345
348
  Requires-Dist: urllib3==2.8.0; extra == "frozen"
346
349
  Requires-Dist: uuid6==2025.0.1; extra == "frozen"
347
- Requires-Dist: uvicorn==0.53.0; extra == "frozen"
350
+ Requires-Dist: uvicorn==0.54.0; extra == "frozen"
348
351
  Requires-Dist: uvloop==0.22.1; extra == "frozen"
349
352
  Requires-Dist: vine==5.1.0; extra == "frozen"
350
353
  Requires-Dist: voluptuous==0.16.0; extra == "frozen"
@@ -355,7 +358,7 @@ Requires-Dist: webencodings==0.6.1; extra == "frozen"
355
358
  Requires-Dist: websocket-client==1.9.2; extra == "frozen"
356
359
  Requires-Dist: websockets==17.1; extra == "frozen"
357
360
  Requires-Dist: wirerope==1.0.0; extra == "frozen"
358
- Requires-Dist: wrapt==2.4.1; extra == "frozen"
361
+ Requires-Dist: wrapt==2.5.0; extra == "frozen"
359
362
  Requires-Dist: yamale==6.1.0; extra == "frozen"
360
363
  Requires-Dist: yarl==1.25.1; extra == "frozen"
361
364
  Requires-Dist: zarr==3.4.0; extra == "frozen"
@@ -25,7 +25,7 @@ VBI_FILTERS = [
25
25
  Filter(name="VBI-Red H-alpha", min=656.258 * u.nm, max=656.306 * u.nm),
26
26
  Filter(name="VBI-Red Continuum", min=668.202 * u.nm, max=668.644 * u.nm),
27
27
  Filter(name="VBI-Red Ti O", min=705.545 * u.nm, max=706.133 * u.nm),
28
- Filter(name="VBI-Red Fe IX", min=789.168 * u.nm, max=789.204 * u.nm),
28
+ Filter(name="VBI-Red Fe XI", min=789.168 * u.nm, max=789.204 * u.nm),
29
29
  ]
30
30
 
31
31
 
@@ -0,0 +1,298 @@
1
+ """VBI science task."""
2
+
3
+ from concurrent.futures import FIRST_COMPLETED
4
+ from concurrent.futures import ProcessPoolExecutor
5
+ from concurrent.futures import wait
6
+ from functools import lru_cache
7
+ from functools import partial
8
+ from itertools import islice
9
+ from pathlib import Path
10
+
11
+ import numpy as np
12
+ from astropy.io import fits
13
+ from dkist_processing_common.codecs.fits import fits_access_decoder
14
+ from dkist_processing_common.codecs.fits import fits_array_decoder
15
+ from dkist_processing_common.codecs.fits import fits_hdulist_encoder
16
+ from dkist_processing_common.models.task_name import TaskName
17
+ from dkist_processing_common.tasks.mixin.quality import QualityMixin
18
+ from dkist_processing_math.arithmetic import divide_fits_access_by_array
19
+ from dkist_processing_math.arithmetic import subtract_array_from_fits_access
20
+ from dkist_service_configuration.logging import logger
21
+
22
+ from dkist_processing_vbi.config import dkist_processing_vbi_configurations
23
+ from dkist_processing_vbi.models.tags import VbiTag
24
+ from dkist_processing_vbi.parsers.vbi_l0_fits_access import VbiL0FitsAccess
25
+ from dkist_processing_vbi.tasks.vbi_base import VbiTaskBase
26
+
27
+ __all__ = ["ScienceCalibration"]
28
+
29
+
30
+ # One entry per calibration type (dark, gain, bad-pixel map).
31
+ @lru_cache(maxsize=3)
32
+ def _mmap_calibration_array(path: Path) -> np.ndarray:
33
+ """Memory-map and cache a calibration array in the current worker process."""
34
+ array = fits_array_decoder(
35
+ path,
36
+ auto_squeeze=False,
37
+ checksum=False,
38
+ memmap=True,
39
+ )
40
+ array.flags.writeable = False
41
+ return array
42
+
43
+
44
+ def _decode_calibration_file(path: Path) -> None:
45
+ """Decode a calibration FITS file with checksum checking and discard the array."""
46
+ array = fits_array_decoder(
47
+ path,
48
+ auto_squeeze=False,
49
+ checksum=True,
50
+ memmap=True,
51
+ )
52
+ del array
53
+
54
+
55
+ class ScienceCalibration(VbiTaskBase, QualityMixin):
56
+ """
57
+ Task class for running full science calibration on a set of observe images.
58
+
59
+ Parameters
60
+ ----------
61
+ recipe_run_id : int
62
+ id of the recipe run used to identify the workflow run this task is part of
63
+ workflow_name : str
64
+ name of the workflow to which this instance of the task belongs
65
+ workflow_version : str
66
+ version of the workflow to which this instance of the task belongs
67
+ """
68
+
69
+ record_provenance = True
70
+
71
+ @staticmethod
72
+ def correct_bad_pixels(
73
+ image: np.ndarray,
74
+ bad_pixel_map: np.ndarray,
75
+ neighborhood_radius: int,
76
+ ) -> np.ndarray:
77
+ """Replace bad pixels with a masked neighborhood median."""
78
+ bad_pixel_map = np.squeeze(bad_pixel_map).astype(bool)
79
+
80
+ corrected_image = image.copy()
81
+ bad_rows, bad_columns = np.where(bad_pixel_map)
82
+ source_plane = image[0]
83
+ corrected_plane = corrected_image[0]
84
+ masked_source_plane = np.ma.masked_array(
85
+ source_plane,
86
+ mask=bad_pixel_map,
87
+ )
88
+ for row, column in zip(bad_rows, bad_columns, strict=True):
89
+ row_slice = slice(
90
+ max(0, row - neighborhood_radius),
91
+ min(source_plane.shape[0], row + neighborhood_radius + 1),
92
+ )
93
+ column_slice = slice(
94
+ max(0, column - neighborhood_radius),
95
+ min(source_plane.shape[1], column + neighborhood_radius + 1),
96
+ )
97
+ corrected_plane[row, column] = np.ma.median(
98
+ masked_source_plane[row_slice, column_slice]
99
+ )
100
+
101
+ return corrected_image
102
+
103
+ @staticmethod
104
+ def calibrate_science_frame(
105
+ science_access: VbiL0FitsAccess,
106
+ dark_calibration_array: np.ndarray,
107
+ gain_calibration_array: np.ndarray,
108
+ bad_pixel_map: np.ndarray,
109
+ bad_pixel_neighborhood_radius: int,
110
+ ) -> VbiL0FitsAccess:
111
+ """Calibrate one decoded science frame in a worker process."""
112
+ science_access = next(
113
+ subtract_array_from_fits_access(
114
+ access_objs=science_access,
115
+ array_to_subtract=dark_calibration_array,
116
+ )
117
+ )
118
+ science_access = next(
119
+ divide_fits_access_by_array(
120
+ access_objs=science_access,
121
+ array_to_divide_by=gain_calibration_array,
122
+ )
123
+ )
124
+ science_access.data = ScienceCalibration.correct_bad_pixels(
125
+ image=science_access.data,
126
+ bad_pixel_map=bad_pixel_map,
127
+ neighborhood_radius=bad_pixel_neighborhood_radius,
128
+ )
129
+ return science_access
130
+
131
+ @staticmethod
132
+ def encode_calibrated_science_frame(
133
+ science_access: VbiL0FitsAccess,
134
+ mosaic: int,
135
+ num_mosaic_repeats: int,
136
+ ) -> bytes:
137
+ """Encode one calibrated science frame as compressed FITS bytes."""
138
+ # Run the header through an `ImageHDU` to strip out any `PrimaryHDU`-only keys. This is to get around an
139
+ # `astropy` bug (since 7.0) that produces bad headers if a `CompImageHDU` is populated with a header
140
+ # from a `PrimaryHDU`.
141
+ cleaned_header = fits.ImageHDU(header=science_access.header).header
142
+ processed_hdu_list = fits.HDUList(
143
+ [fits.PrimaryHDU(), fits.CompImageHDU(data=science_access.data, header=cleaned_header)]
144
+ )
145
+ processed_hdu_list[1].header["VBINMOSC"] = num_mosaic_repeats
146
+ processed_hdu_list[1].header["VBICMOSC"] = mosaic
147
+
148
+ return fits_hdulist_encoder(processed_hdu_list)
149
+
150
+ @staticmethod
151
+ def calibrate_and_encode_science_frame(
152
+ science_path: Path,
153
+ dark_calibration_path: Path,
154
+ gain_calibration_path: Path,
155
+ bad_pixel_map_path: Path,
156
+ bad_pixel_neighborhood_radius: int,
157
+ mosaic: int,
158
+ num_mosaic_repeats: int,
159
+ ) -> tuple[bytes, int]:
160
+ """Decode, calibrate, and FITS-encode one science frame, returning bytes and its exposure number."""
161
+ fits_obj = fits_access_decoder(
162
+ science_path,
163
+ fits_access_class=VbiL0FitsAccess,
164
+ auto_squeeze=False,
165
+ )
166
+ fits_obj = ScienceCalibration.calibrate_science_frame(
167
+ science_access=fits_obj,
168
+ dark_calibration_array=_mmap_calibration_array(dark_calibration_path),
169
+ gain_calibration_array=_mmap_calibration_array(gain_calibration_path),
170
+ bad_pixel_map=_mmap_calibration_array(bad_pixel_map_path),
171
+ bad_pixel_neighborhood_radius=bad_pixel_neighborhood_radius,
172
+ )
173
+ encoded_frame = ScienceCalibration.encode_calibrated_science_frame(
174
+ science_access=fits_obj,
175
+ mosaic=mosaic,
176
+ num_mosaic_repeats=num_mosaic_repeats,
177
+ )
178
+ return encoded_frame, fits_obj.current_mosaic_step_exp
179
+
180
+ def run(self) -> None:
181
+ """
182
+ Perform science calibrations.
183
+
184
+ - Do initial array corrections (dark, gain, bad pixels)
185
+ - Write out L1 science frames
186
+ - Record quality metrics
187
+
188
+ Returns
189
+ -------
190
+ None
191
+
192
+ """
193
+ logger.info(
194
+ f"Starting science with {self.constants.num_spatial_steps} steps and {self.constants.num_mosaic_repeats} mosaic repeats"
195
+ )
196
+ worker_count = dkist_processing_vbi_configurations.cpu_count
197
+ logger.info(f"Calibrating science frames with {worker_count} worker processes")
198
+ with (
199
+ self.telemetry_span(
200
+ f"Reducing science frames from {self.constants.num_spatial_steps} steps and {self.constants.num_mosaic_repeats} mosaic repeats",
201
+ ),
202
+ ProcessPoolExecutor(max_workers=worker_count) as executor,
203
+ ):
204
+ for step in range(1, self.constants.num_spatial_steps + 1):
205
+ logger.info(f"retrieving gain calibration for step {step}")
206
+ gain_tags = [
207
+ VbiTag.intermediate(),
208
+ VbiTag.task_gain_frame(spatial_step=step),
209
+ ]
210
+ gain_calibration_path = next(self.read(tags=gain_tags))
211
+ _decode_calibration_file(gain_calibration_path)
212
+
213
+ logger.info(f"retrieving bad pixel map for step {step}")
214
+ bad_pixel_map_tags = [
215
+ VbiTag.intermediate(),
216
+ VbiTag.bad_pixel_map(spatial_step=step),
217
+ ]
218
+ bad_pixel_map_path = next(self.read(tags=bad_pixel_map_tags))
219
+ _decode_calibration_file(bad_pixel_map_path)
220
+
221
+ for exp_time in self.constants.observe_exposure_times:
222
+ logger.info(f"retrieving dark calibration for step {step} and {exp_time = }")
223
+ dark_tags = [
224
+ VbiTag.intermediate(),
225
+ VbiTag.task_dark_frame(spatial_step=step, exposure_time=exp_time),
226
+ ]
227
+ dark_calibration_path = next(self.read(tags=dark_tags))
228
+ _decode_calibration_file(dark_calibration_path)
229
+
230
+ for mosaic in range(1, self.constants.num_mosaic_repeats + 1):
231
+ apm_str = f"step {step} and repeat number {mosaic}"
232
+ logger.info(f"collecting observe frames for {apm_str}")
233
+ science_paths = self.read(
234
+ tags=[
235
+ VbiTag.input(),
236
+ VbiTag.frame(),
237
+ VbiTag.task_observe(),
238
+ VbiTag.mosaic(mosaic),
239
+ VbiTag.spatial_step(step),
240
+ VbiTag.exposure_time(exp_time),
241
+ ],
242
+ )
243
+
244
+ calibrate_frame = partial(
245
+ self.calibrate_and_encode_science_frame,
246
+ dark_calibration_path=dark_calibration_path,
247
+ gain_calibration_path=gain_calibration_path,
248
+ bad_pixel_map_path=bad_pixel_map_path,
249
+ bad_pixel_neighborhood_radius=self.parameters.bad_pixel_neighborhood_radius,
250
+ mosaic=mosaic,
251
+ num_mosaic_repeats=self.constants.num_mosaic_repeats,
252
+ )
253
+ # Keep one extra frame per worker queued while results are written.
254
+ pending_frames = {
255
+ executor.submit(calibrate_frame, science_path)
256
+ for science_path in islice(science_paths, 2 * worker_count)
257
+ }
258
+ while pending_frames:
259
+ completed_frames, pending_frames = wait(
260
+ pending_frames, return_when=FIRST_COMPLETED
261
+ )
262
+ while completed_frames:
263
+ encoded_frame, exp_num = completed_frames.pop().result()
264
+ logger.info(f"Writing output for {apm_str} and {exp_num = }")
265
+ self.write_calibrated_fits_bytes(encoded_frame, mosaic, step)
266
+ del encoded_frame
267
+
268
+ science_path = next(science_paths, None)
269
+ if science_path is not None:
270
+ pending_frames.add(
271
+ executor.submit(calibrate_frame, science_path)
272
+ )
273
+
274
+ with self.telemetry_span("Computing and logging quality metrics"):
275
+ no_of_raw_obs_frames: int = self.count(
276
+ tags=[
277
+ VbiTag.input(),
278
+ VbiTag.frame(),
279
+ VbiTag.task_observe(),
280
+ ],
281
+ )
282
+ self.quality_write_intermediate_task_type_counts(
283
+ task_type=TaskName.observe.value, total_frames=no_of_raw_obs_frames
284
+ )
285
+
286
+ def write_calibrated_fits_bytes(self, encoded_frame: bytes, mosaic: int, step: int) -> None:
287
+ """Write encoded calibrated FITS bytes to scratch and tag them correctly."""
288
+ # It is an intentional decision to not tag with exposure time here
289
+ self.write(
290
+ data=encoded_frame,
291
+ tags=[
292
+ VbiTag.calibrated(),
293
+ VbiTag.frame(),
294
+ VbiTag.spatial_step(step),
295
+ VbiTag.mosaic(mosaic),
296
+ VbiTag.stokes("I"),
297
+ ],
298
+ )
@@ -143,15 +143,21 @@ def science_calibration_task(
143
143
  "abort_mosaic",
144
144
  [pytest.param(False, id="Full set"), pytest.param(True, id="Aborted last mosaic")],
145
145
  )
146
- def test_science_calibration(science_calibration_task, mocker, fake_gql_client):
146
+ @pytest.mark.parametrize("cpu_count", [1, 2], ids=["single-core", "multi-core"])
147
+ def test_science_calibration(science_calibration_task, mocker, fake_gql_client, cpu_count):
147
148
  """
148
149
  Given: a set of parsed input observe frames, dark and gain calibrations, and a ScienceCalibration task
149
- When: the task is run
150
+ When: the task is run with one or two worker processes
150
151
  Then: the science frames are processed, no exposures are averaged, and the array values are correct
151
152
  """
152
153
  mocker.patch(
153
154
  "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=fake_gql_client
154
155
  )
156
+ mocker.patch(
157
+ "dkist_processing_vbi.config.DKISTProcessingVBIConfigurations.cpu_count",
158
+ new_callable=mocker.PropertyMock,
159
+ return_value=cpu_count,
160
+ )
155
161
  task, num_steps, num_exp_per_step, expected_num_mosaic_repeats = science_calibration_task
156
162
  task()
157
163
 
@@ -213,6 +219,7 @@ def test_correct_bad_pixels(science_calibration_task):
213
219
  corrected_image = task.correct_bad_pixels(
214
220
  image=image,
215
221
  bad_pixel_map=bad_pixel_map,
222
+ neighborhood_radius=task.parameters.bad_pixel_neighborhood_radius,
216
223
  )
217
224
 
218
225
  neighborhood = image[0, :11, :11]
@@ -241,6 +248,7 @@ def test_correct_clustered_bad_pixels(science_calibration_task, abort_mosaic):
241
248
  corrected_image = task.correct_bad_pixels(
242
249
  image=image,
243
250
  bad_pixel_map=bad_pixel_map,
251
+ neighborhood_radius=task.parameters.bad_pixel_neighborhood_radius,
244
252
  )
245
253
 
246
254
  for row, column in ((5, 5), (5, 6)):