Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/wf_psf/quality_control/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
95 changes: 95 additions & 0 deletions src/wf_psf/quality_control/resources.py
Original file line number Diff line number Diff line change
@@ -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 <jennifer.pollack@cea.fr>
"""

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
45 changes: 0 additions & 45 deletions src/wf_psf/tests/test_quality_control/config_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Expand Down
50 changes: 50 additions & 0 deletions src/wf_psf/tests/test_quality_control/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import pytest
from wf_psf.quality_control.config import (
QualityControlConfig,
QualityMetricConfig,
RejectionPolicyConfig,
ResourcesConfig,
)


@pytest.fixture
def qc_config_factory():
Comment thread
roryclaydon1994 marked this conversation as resolved.
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
105 changes: 105 additions & 0 deletions src/wf_psf/tests/test_quality_control/resources_test.py
Original file line number Diff line number Diff line change
@@ -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 <jennifer.pollack@cea.fr>

"""

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()
Loading