diff --git a/src/wf_psf/quality_control/config.py b/src/wf_psf/quality_control/config.py index b9e9e074..b0166250 100644 --- a/src/wf_psf/quality_control/config.py +++ b/src/wf_psf/quality_control/config.py @@ -276,6 +276,7 @@ def parse_resources_config( return ResourcesConfig(available=dict(config)) +# validators for internal consistency of config sections def validate_quality_control_config(config: QualityControlConfig) -> None: """Validate internal consistency of a quality control configuration. diff --git a/src/wf_psf/quality_control/resources.py b/src/wf_psf/quality_control/resources.py new file mode 100644 index 00000000..556d9375 --- /dev/null +++ b/src/wf_psf/quality_control/resources.py @@ -0,0 +1,95 @@ +"""Resource dependency handling for the quality control pipeline. + +Defines helpers for identifying resources required by enabled quality metrics +and resolving those requirements against resources supplied by the caller. + +:Authors: + Jennifer Pollack +""" + +from __future__ import annotations +from collections.abc import Mapping +from typing import Any +from wf_psf.quality_control.config import QualityControlConfig + +import logging + +logger = logging.getLogger(__name__) + + +class Resources: + """Manage resources required by quality control metrics. + + Assesses resource requirements and availability for a validated quality + control configuration. + """ + + def __init__(self, config: QualityControlConfig): + self.config = config + + def get_required(self) -> set[str]: + """Return resources required by enabled quality metrics. + + Returns + ------- + Unique resource identifiers required by enabled quality metrics. + + Notes + ----- + This configuration is assumed to have been validated for internal consistency between resource requirements and available resources. + + """ + return { + resource + for metric in self.config.metrics.values() + if metric.enabled + for resource in metric.required_resources + } + + def resolve( + self, + provided: Mapping[str, Any] | None = None, + ) -> dict[str, Any]: + """Resolve resources required by enabled quality metrics. + + Parameters + ---------- + provided : Mapping[str, Any] or None + Ready-to-use resources supplied by the pipeline caller. + + Returns + ------- + dict[str, Any] + Resources required by enabled quality metrics and supplied by the + caller. + + Raises + ------ + NotImplementedError + If required resources are not supplied by the caller. Preparation of + missing resources is not yet implemented. + """ + required = self.get_required() + provided = {} if provided is None else provided + + resolved = { + resource: provided[resource] + for resource in required + if resource in provided + } + missing = required - provided.keys() + unused = provided.keys() - required + + logger.debug( + "Resource resolution: resolved=%s, missing=%s, unused=%s", + sorted(resolved), + sorted(missing), + sorted(unused), + ) + + if missing: + raise NotImplementedError( + f"Required resources are not available: {sorted(missing)}" + ) + + return resolved diff --git a/src/wf_psf/tests/test_quality_control/config_test.py b/src/wf_psf/tests/test_quality_control/config_test.py index e06668c8..32dde632 100644 --- a/src/wf_psf/tests/test_quality_control/config_test.py +++ b/src/wf_psf/tests/test_quality_control/config_test.py @@ -29,49 +29,6 @@ def load_config(config_file: str) -> QualityControlConfig: return handler.load() -@pytest.fixture -def qc_config_factory(): - def factory( - *, - required_resources=None, - rejection_metric=None, - resources=None, - metrics=None, - rejection=None, - ): - metric_default = { - "goodness_of_fit": QualityMetricConfig( - enabled=True, - required_resources=required_resources or [], - ) - } - - resources_default = ResourcesConfig( - available={ - "psf_models": { - "standard": { - "inference_config": "inference_standard.yaml", - } - } - } - ) - - rejection_default = { - rejection_metric or "goodness_of_fit": RejectionPolicyConfig( - enabled=True, - threshold=0.25, - ) - } - - return QualityControlConfig( - metrics=metric_default if metrics is None else metrics, - resources=resources_default if resources is None else resources, - rejection=rejection_default if rejection is None else rejection, - ) - - return factory - - # Test for config loading and parsers def test_quality_control_config_loading(): config = load_config("valid/quality_control.yaml") @@ -257,8 +214,6 @@ def test_validate_rejection_policy_metrics_metric_not_enabled(qc_config_factory) # Integration tests - - def test_load_config_validates_configuration_pass(): with does_not_raise(): load_config("valid/quality_control.yaml") diff --git a/src/wf_psf/tests/test_quality_control/conftest.py b/src/wf_psf/tests/test_quality_control/conftest.py new file mode 100644 index 00000000..6d76caca --- /dev/null +++ b/src/wf_psf/tests/test_quality_control/conftest.py @@ -0,0 +1,50 @@ +import pytest +from wf_psf.quality_control.config import ( + QualityControlConfig, + QualityMetricConfig, + RejectionPolicyConfig, + ResourcesConfig, +) + + +@pytest.fixture +def qc_config_factory(): + def factory( + *, + required_resources=None, + rejection_metric=None, + resources=None, + metrics=None, + rejection=None, + ): + metric_default = { + "goodness_of_fit": QualityMetricConfig( + enabled=True, + required_resources=required_resources or [], + ) + } + + resources_default = ResourcesConfig( + available={ + "psf_models": { + "standard": { + "inference_config": "inference_standard.yaml", + } + } + } + ) + + rejection_default = { + rejection_metric or "goodness_of_fit": RejectionPolicyConfig( + enabled=True, + threshold=0.25, + ) + } + + return QualityControlConfig( + metrics=metric_default if metrics is None else metrics, + resources=resources_default if resources is None else resources, + rejection=rejection_default if rejection is None else rejection, + ) + + return factory diff --git a/src/wf_psf/tests/test_quality_control/resources_test.py b/src/wf_psf/tests/test_quality_control/resources_test.py new file mode 100644 index 00000000..f7d14a8f --- /dev/null +++ b/src/wf_psf/tests/test_quality_control/resources_test.py @@ -0,0 +1,105 @@ +"""UNIT TESTS FOR PACKAGE MODULE: Quality Control Resources + +This module contains unit tests for the quality control resources module. + +:Author: Jennifer Pollack + +""" + +import pytest +from wf_psf.quality_control.config import QualityMetricConfig +from wf_psf.quality_control.resources import Resources + + +def test_get_required(qc_config_factory): + config = qc_config_factory( + metrics={ + "mask_obscuration": QualityMetricConfig( + enabled=True, + required_resources=[], + ), + "goodness_of_fit": QualityMetricConfig( + enabled=True, + required_resources=["psf_models.standard"], + ), + "shapes": QualityMetricConfig( + enabled=False, + required_resources=["psf_models.oversampled"], + ), + } + ) + resources = Resources(config) + assert resources.get_required() == {"psf_models.standard"} + + +def test_get_required_combines_unique_resources(qc_config_factory): + config = qc_config_factory( + metrics={ + "metric_a": QualityMetricConfig( + enabled=True, + required_resources=["psf_models.standard"], + ), + "metric_b": QualityMetricConfig( + enabled=True, + required_resources=[ + "psf_models.standard", + "psf_models.oversampled", + ], + ), + } + ) + + resources = Resources(config) + assert resources.get_required() == { + "psf_models.standard", + "psf_models.oversampled", + } + + +# Resource resolution orchestration tests +@pytest.mark.parametrize( + ("required_resources", "provided", "expected_resolved"), + [ + ( + {"psf_models.standard"}, + {"psf_models.standard": [1, 1, 1, 1]}, + {"psf_models.standard": [1, 1, 1, 1]}, + ), + ( + {"psf_models.standard"}, + { + "psf_models.standard": [1, 1, 1, 1], + "psf_models.oversampled": [2, 2, 2, 2], + }, + {"psf_models.standard": [1, 1, 1, 1]}, + ), + ( + [], + {}, + {}, + ), + ], +) +def test_resolve_resources( + qc_config_factory, + required_resources, + provided, + expected_resolved, +): + config = qc_config_factory(required_resources=required_resources) + resources = Resources(config) + + assert resources.resolve(provided) == expected_resolved + + +def test_resolve_resources_missing(qc_config_factory): + config = qc_config_factory( + required_resources=["psf_models.standard"], + ) + resources = Resources(config) + + with pytest.raises( + NotImplementedError, + match="Required resources are not available", + ): + resources.resolve()