Source code for synthpriv.metrics.base

"""Metric result model and threshold evaluation."""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any

# Possible states of a metric.
PASSED = "passed"
FAILED = "failed"
REPORTED = "reported"
ERROR = "error"


[docs] @dataclass class MetricResult: """Result of a single metric.""" name: str description: str = "" value: Any = None threshold: Any = None direction: str = "lower_is_better" # "lower_is_better" | "higher_is_better" | "none" status: str = REPORTED message: str = "" details: dict[str, Any] = field(default_factory=dict) @property def passed(self) -> bool: return self.status == PASSED
[docs] def evaluate_status( value: float, threshold: float | None, direction: str, ) -> tuple[str, str]: """Decide PASSED/FAILED/REPORTED status by comparing against a threshold.""" if threshold is None or value is None: return REPORTED, "No threshold configured; value is only reported." if direction == "higher_is_better": ok = value >= threshold op = ">=" elif direction == "lower_is_better": ok = value <= threshold op = "<=" else: return REPORTED, "Direction 'none': no automatic comparison." status = PASSED if ok else FAILED return status, f"Value {value:.4f} vs threshold {threshold:.4f} ({op})"
[docs] def summarize(results: dict[str, MetricResult]) -> dict[str, int]: """Count statuses for the report summary.""" counts = {s: 0 for s in (PASSED, FAILED, REPORTED, ERROR)} for res in results.values(): if res.status in counts: counts[res.status] += 1 else: # pragma: no cover - defensive counts[REPORTED] += 1 return counts