Skip to content
5 changes: 3 additions & 2 deletions config/quality_control.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ metrics:
enabled: true
required_resources:
- psf_models.standard

params:
statistic: reduced_chi_square
normalize_residuals: true
Expand All @@ -28,7 +27,9 @@ rejection:

mask_obscuration:
enabled: true
threshold: 0.25
policy:
threshold:
value: 0.25

goodness_of_fit:
enabled: false
Expand Down
31 changes: 31 additions & 0 deletions src/wf_psf/quality_control/context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Quality control context.

Encapsulates contextual information, such as datasets and resolved resources,
required by the quality control pipeline and its metrics.

:Authors:
Jennifer Pollack <jennifer.pollack@cea.fr>
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any


@dataclass
class QualityControlContext:
"""Context shared across the quality control pipeline.

Attributes
----------
dataset : Any
Dataset or data container supplied to the quality control pipeline.
Comment thread
roryclaydon1994 marked this conversation as resolved.

resources : dict[str, Any]
Ready-to-use resources required by enabled quality metrics, keyed by
resource identifier.
"""

dataset: Any
resources: dict[str, Any] = field(default_factory=dict)
158 changes: 142 additions & 16 deletions src/wf_psf/quality_control/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@

Defines the orchestration layer for dataset quality control.

The QualityControlPipeline coordinates quality metric evaluation,
application of rejection policies, reporting, and dataset filtering.
Individual quality metrics and rejection policies are provided through
their respective interfaces, allowing new methods to be added without
modifying the pipeline implementation.
The QualityControlPipeline coordinates quality metric evaluation and
sample rejection. Individual quality metrics and rejection policies are
provided through their respective interfaces, allowing new methods to
be added without modifying the pipeline implementation.

:Authors: Jennifer Pollack <jennifer.pollack@cea.fr>

Expand All @@ -15,6 +14,19 @@
from dataclasses import dataclass
import numpy as np

from wf_psf.quality_control.config import QualityControlConfigHandler
from wf_psf.quality_control.context import QualityControlContext
from wf_psf.quality_control.metrics.base import QualityMetric
from wf_psf.quality_control.metrics.registry import build_metrics_registry
from wf_psf.quality_control.rejection.base import RejectionPolicy
from wf_psf.quality_control.rejection.registry import build_rejection_policy_registry
from wf_psf.quality_control.resources import Resources

import logging

logger = logging.getLogger(__name__)


@dataclass
class QualityControlResult:
"""Results produced by the quality control pipeline.
Expand All @@ -24,7 +36,7 @@ class QualityControlResult:
metrics
Computed quality metrics indexed by metric name.

rejection_masks
validity_masks
Boolean validity masks produced by each rejection policy.

valid_mask
Expand All @@ -34,7 +46,7 @@ class QualityControlResult:

metrics: dict[str, np.ndarray]

rejection_masks: dict[str, np.ndarray]
validity_masks: dict[str, np.ndarray]

valid_mask: np.ndarray

Expand All @@ -50,15 +62,129 @@ class QualityControlPipeline:
of the pipeline execution.
"""

def __init__(
self,
metrics,
rejection_policies,
):
...
def __init__(self, qc_config_path):
self.config = QualityControlConfigHandler(qc_config_path).load()
self.metrics_registry = build_metrics_registry()
self.rejection_registry = build_rejection_policy_registry()

def _instantiate_metrics(self) -> dict[str, QualityMetric]:
Comment thread
roryclaydon1994 marked this conversation as resolved.
"""Instantiate enabled quality metric implementations from configuration.

Returns
-------
dict[str, QualityMetric]
Enabled quality metric implementations keyed by metric name.

Notes
-----
The quality control configuration is assumed to have been validated
before policy instantiation.
"""
metrics = {}

for name, metric_config in self.config.metrics.items():
if not metric_config.enabled:
logger.debug("Skipping metric %s: not enabled.", name)
continue
Comment thread
roryclaydon1994 marked this conversation as resolved.

metric_cls = self.metrics_registry.get(name)

metrics[name] = metric_cls()

logger.debug("Instantiated metrics: %s", list(metrics))

return metrics

def _instantiate_rejection_policies(self) -> dict[str, RejectionPolicy]:
"""Instantiate enabled rejection policy implementations.

Returns
-------
dict[str, RejectionPolicy]
Enabled rejection policy implementations keyed by metric name.

Notes
-----
The quality control configuration is assumed to have been validated
before policy instantiation.
"""
rejection_policies = {}

for metric_name, rejection_config in self.config.rejection.items():
if not rejection_config.enabled:
logger.debug("Skipping rejection policy %s: not enabled.", metric_name)
continue

policy_name, policy_params = next(iter(rejection_config.policy.items()))
policy_cls = self.rejection_registry.get(policy_name)

rejection_policies[metric_name] = policy_cls(**policy_params)

logger.debug("Instantiated rejection policies: %s", list(rejection_policies))

return rejection_policies

def _resolve_resources(self, provided_resources):
"""Resolve resources required by enabled quality metrics.

Parameters
----------
provided_resources : Mapping[str, Any] or None
Ready-to-use resources supplied by the pipeline caller, keyed by
resource identifier.

Returns
-------
dict[str, Any]
Resolved resources required by enabled quality metrics.
"""
resource_manager = Resources(self.config)
return resource_manager.resolve(provided_resources)

def run(self, dataset, provided_resources=None):
"""Run quality control pipeline.

Parameters
----------
dataset : Any
Dataset or data container supplied to the quality control pipeline.

provided_resources : Mapping[str, Any] or None
Ready-to-use resources supplied by the pipeline caller, keyed by resource identifier.

Notes
-----
The pipeline is expected to be invoked only when at least one quality metric is enabled in the quality control configuration.
"""
resolved_resources = self._resolve_resources(
provided_resources=provided_resources
)

context = QualityControlContext(dataset, resolved_resources)

metrics = self._instantiate_metrics()

metric_results = {
name: metric.compute(context) for name, metric in metrics.items()
}
Comment thread
roryclaydon1994 marked this conversation as resolved.

rejection_policies = self._instantiate_rejection_policies()

def run(self, dataset):
validity_masks = {
name: policy.apply(metric_results[name])
for name, policy in rejection_policies.items()
}

...
if validity_masks:
# True indicates a valid sample. A sample is valid only if it passes
# every enabled rejection policy.
valid_mask = np.logical_and.reduce(list(validity_masks.values()))
else:
metric_result = next(iter(metric_results.values()))
valid_mask = np.ones(metric_result.shape, dtype=bool)

return QualityControlResult(...)
return QualityControlResult(
metrics=metric_results,
validity_masks=validity_masks,
valid_mask=valid_mask,
)
11 changes: 8 additions & 3 deletions src/wf_psf/quality_control/rejection/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,12 @@ class RejectionPolicy(ABC):

@abstractmethod
def apply(self, metric: np.ndarray) -> np.ndarray:
"""Return a boolean mask identifying valid dataset samples.

The returned mask has one entry per dataset sample.
"""Apply the rejection policy to metric values.

Returns
-------
np.ndarray
Boolean validity mask with one entry per dataset sample. ``True``
indicates that the sample passes the rejection policy and should
be retained; ``False`` indicates that it should be rejected.
"""
15 changes: 14 additions & 1 deletion src/wf_psf/quality_control/rejection/threshold.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,23 @@


class ThresholdRejectionPolicy(RejectionPolicy):
"""Reject dataset samples based on configurable metric thresholds."""
"""Reject dataset samples based on configurable metric threshold.

Attributes
----------
name : str
Policy identifier used by the rejection policy registry.

value : float
Threshold applied by the rejection policy.

"""

name = "threshold"

def __init__(self, value: float):
self.value = value

def apply(self, metric: np.ndarray) -> np.ndarray:
"""Apply threshold-based rejection to metric values."""
raise NotImplementedError
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,6 @@ rejection:

mask_obscuration:
enabled: true
threshold: 0.25
policy:
threshold:
value: 0.25
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
metrics:

imaginary_metric:
enabled: true
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ metrics:
enabled: true
required_resources:
- psf_models.standard

params:
statistic: reduced_chi_square
normalize_residuals: true
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
resources:

psf_models:
standard:
inference_config: inference_standard.yaml
oversampled:
inference_config: inference_oversampled.yaml

metrics:

mask_obscuration:
enabled: true
params:
aperture: gaussian
sigma: 2.5

goodness_of_fit:
enabled: true
required_resources:
- psf_models.standard
params:
statistic: reduced_chi_square
normalize_residuals: true

rejection:

mask_obscuration:
enabled: true
policy:
threshold:
value: 3.0

goodness_of_fit:
enabled: true
policy:
threshold:
value: 3.0

reporting:

save_metrics: true
log_statistics: true
Loading
Loading