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