Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<!--
A new scriv changelog fragment.

Uncomment the section that is right (remove the HTML comment wrapper).
For top level release notes, leave all the headers commented out.
-->

<!--
### Breaking changes

- A bullet item for the Breaking changes category.

-->

### New features

- Added pixel-mask quality metric evaluation with diagnostics for total and aperture-masked pixels and their corresponding fractions.

<!--
### Bug fixes

- A bullet item for the Bug fixes category.

-->
<!--
### Performance improvements

- A bullet item for the Performance improvements category.

-->
<!--
### Internal changes

- A bullet item for the Internal changes category.

-->
4 changes: 3 additions & 1 deletion config/quality_control.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ metrics:
params:
aperture:
type: circular
centre: stamp_centre
radius: 2.6
unit: sigma

Expand All @@ -21,20 +22,21 @@ metrics:
required_resources:
- psf_models.standard
params:
statistic: reduced_chi_square
normalize_residuals: true


rejection:

pixel_mask:
enabled: true
diagnostic: aperture_masked_fraction
policy:
threshold:
value: 0.25

goodness_of_fit:
enabled: false
diagnostic: reduced_chi_square
policy:
threshold:
value: 3.0
Expand Down
100 changes: 17 additions & 83 deletions src/wf_psf/quality_control/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,15 @@ class RejectionPolicyConfig:
enabled : bool
Whether rejection policy is enabled.

diagnostic : str | None
Diagnostic from the quality metric to use when applying a rejection policy.

policy : dict[str, Any]
Rejection policy configuration keyed by policy type.
"""

enabled: bool = False
diagnostic: str | None = None
policy: dict[str, Any] = field(default_factory=dict)


Expand Down Expand Up @@ -236,6 +240,14 @@ def parse_rejection_policy_config(
policies[metric_name] = RejectionPolicyConfig(enabled=False)
continue

diagnostic = cfg.get("diagnostic", None)

if not isinstance(diagnostic, str) or not diagnostic:
raise ValueError(
f"Rejection policy configuration for '{metric_name}' "
"must specify a non-empty `diagnostic`."
)

if "policy" not in cfg:
raise ValueError(
f"Rejection policy configuration for '{metric_name}' "
Expand Down Expand Up @@ -268,7 +280,7 @@ def parse_rejection_policy_config(
)

policies[metric_name] = RejectionPolicyConfig(
enabled=enabled, policy=dict(policy)
enabled=enabled, diagnostic=diagnostic, policy=dict(policy)
)

return policies
Expand Down Expand Up @@ -332,80 +344,6 @@ 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.

Parameters
----------
config : QualityControlConfig
Parsed quality control configuration.

Raises
------
ValueError
If any cross-section configuration dependency is invalid.
"""
validate_metric_resources(config)
validate_rejection_policy_metrics(config)


def validate_metric_resources(config: QualityControlConfig) -> None:
"""Validate that all metric resource requirements can be resolved.

Parameters
----------
config : QualityControlConfig
Parsed quality control configuration.

Raises
------
ValueError
If a required resource identifier is not available in the configured resources.
"""
for metric_name, metric in config.metrics.items():
for resource_id in metric.required_resources:
resources = config.resources.available

if (
resource_id.family not in resources
or resource_id.variant not in resources[resource_id.family]
):
raise ValueError(
f"Metric '{metric_name}' requires unknown resource '{resource_id}'."
)


def validate_rejection_policy_metrics(config: QualityControlConfig) -> None:
"""Validate rejection policies against configured quality metrics.

Parameters
----------
config : QualityControlConfig
Parsed quality control configuration.

Raises
------
ValueError
If an enabled rejection policy references an unknown or disabled
quality metric.
"""
for metric_name, rejection_policy in config.rejection.items():
if not rejection_policy.enabled:
continue

if metric_name not in config.metrics:
raise ValueError(
f"Rejection policy configured for unknown metric '{metric_name}'."
)

if not config.metrics[metric_name].enabled:
raise ValueError(
f"Rejection policy cannot be enabled because metric "
f"'{metric_name}' is disabled."
)


SECTION_PARSERS = {
"metrics": parse_metrics_config,
"rejection": parse_rejection_policy_config,
Expand Down Expand Up @@ -437,16 +375,16 @@ def load(self) -> QualityControlConfig:
Returns
-------
QualityControlConfig
Parsed and validated quality control configuration.
Parsed quality control configuration.

Raises
------
TypeError
If a configuration section has an invalid structure or type.

ValueError
If the parsed configuration contains inconsistent references
between metrics, resources, or rejection policies.
If a configuration section contains an invalid value or
configuration structure.
"""
qc_config = read_yaml(self.qc_config_path)
config = {}
Expand All @@ -455,8 +393,4 @@ def load(self) -> QualityControlConfig:
values = qc_config.get(section, {})
config[section] = parser(values)

qc = QualityControlConfig(**config)

validate_quality_control_config(qc)

return qc
return QualityControlConfig(**config)
8 changes: 6 additions & 2 deletions src/wf_psf/quality_control/metrics/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

from abc import ABC, abstractmethod
import numpy as np
from typing import Any
from typing import Any, ClassVar

from wf_psf.quality_control.context import QualityControlContext

Expand All @@ -27,6 +27,9 @@ class QualityMetric(ABC):
Unique identifier for the metric implementation. Used by
the MetricsRegistry to register and retrieve metric classes.

diagnostics : ClassVar[frozenset[str]]
Immutable set of diagnostic names exposed by the metric.

params : dict[str, Any]
Parameter set for configuring a specific metric.

Expand All @@ -37,7 +40,8 @@ class QualityMetric(ABC):

"""

name: str
name: ClassVar[str]
diagnostics: ClassVar[frozenset[str]]

def __init__(self, params: dict[str, Any]):
self.params = params
Expand Down
4 changes: 4 additions & 0 deletions src/wf_psf/quality_control/metrics/goodness_of_fit.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,16 @@

from .base import QualityMetric
import numpy as np
from typing import ClassVar


class GoodnessOfFitMetric(QualityMetric):
"""Compute a goodness-of-fit metric (e.g. reduced chi square) for each dataset sample."""

name = "goodness_of_fit"
diagnostics: ClassVar[frozenset[str]] = frozenset(
{"chi_square", "reduced_chi_square"}
)

def compute(self, context: QualityControlContext) -> dict[str, np.ndarray]:
"""Compute reduced chi-square values for each dataset sample."""
Expand Down
Loading
Loading