Source code for CADETProcess.metric_space.metric

"""Metric declarations for MetricSpace.

``Metric`` describes a single named output of an evaluation backend: its
identity, declared shape, and labels.  It carries no direction, bounds, or
normalization; those are annotations owned by ``MetricSpace``.  Shapes are
declared, never inferred from the first evaluation: inferred shapes cause
hard-to-diagnose failures when batch size, feed composition, or failed
evaluations change the returned shape.
"""

from __future__ import annotations

import math
from typing import Any, Optional

import numpy as np

__all__ = ["Metric"]


[docs] class Metric: """Declaration of a named output. Parameters ---------- name : str Unique name identifying this metric within a ``MetricSpace``. n_metrics : int, optional Number of scalar entries. Derived from *dims*/*coords* when named dimensions are declared; defaults to 1 (scalar). dims : tuple of str, optional Named dimensions, e.g. ``("component",)``. Requires *coords*. coords : dict, optional Coordinate values per dimension, e.g. ``{"component": ["A", "B"]}``. Every entry in *dims* must have coordinates. labels : list of str, optional One label per scalar entry. Defaults to *name* for scalars, to ``{name}_{coord}`` for one-dimensional named metrics, and to ``{name}_{i}`` otherwise. """ def __init__( self, name: str, n_metrics: Optional[int] = None, dims: Optional[tuple[str, ...]] = None, coords: Optional[dict[str, list[Any]]] = None, labels: Optional[list[str]] = None, ) -> None: if not isinstance(name, str) or not name: raise ValueError("Metric name must be a non-empty string.") self.name = name if dims is not None: if coords is None: raise ValueError(f"Metric {name!r}: dims requires coords.") dims = tuple(dims) missing = [d for d in dims if d not in coords] if missing: raise ValueError( f"Metric {name!r}: missing coordinates for dimensions {missing}." ) self.dims: Optional[tuple[str, ...]] = dims self.coords: Optional[dict[str, list[Any]]] = { d: list(coords[d]) for d in dims } shape = tuple(len(self.coords[d]) for d in dims) derived = math.prod(shape) if n_metrics is not None and n_metrics != derived: raise ValueError( f"Metric {name!r}: n_metrics={n_metrics} contradicts shape " f"{shape} declared via dims/coords." ) self._shape = shape self.n_metrics = derived else: if coords is not None: raise ValueError(f"Metric {name!r}: coords requires dims.") self.dims = None self.coords = None n = 1 if n_metrics is None else int(n_metrics) if n < 1: raise ValueError(f"Metric {name!r}: n_metrics must be >= 1.") self.n_metrics = n self._shape = () if n == 1 else (n,) self.labels = labels @property def shape(self) -> tuple[int, ...]: """Declared value shape; ``()`` for scalar metrics.""" return self._shape @property def labels(self) -> list[str]: """One label per scalar entry.""" if self._labels is not None: return list(self._labels) if self.n_metrics == 1: return [self.name] if self.dims is not None and len(self.dims) == 1: return [f"{self.name}_{c}" for c in self.coords[self.dims[0]]] return [f"{self.name}_{i}" for i in range(self.n_metrics)] @labels.setter def labels(self, value: Optional[list[str]]) -> None: if value is not None and len(value) != self.n_metrics: raise ValueError( f"Metric {self.name!r}: expected {self.n_metrics} labels, " f"got {len(value)}." ) self._labels = list(value) if value is not None else None
[docs] def validate(self, value: Any) -> np.ndarray: """Validate *value* against the declared shape. Returns the value as ``np.ndarray`` with the declared shape. A scalar metric additionally accepts a length-1 vector, which is canonicalized to a 0-d array. Raises ------ ValueError If the value's shape does not match the declaration. """ arr = np.asarray(value) if arr.shape == self._shape: return arr if self._shape == () and arr.shape == (1,): return arr.reshape(()) raise ValueError( f"Metric {self.name!r}: expected shape {self._shape}, got {arr.shape}." )
def __repr__(self) -> str: """Return a readable representation.""" if self.dims is not None: return f"Metric(name={self.name!r}, dims={self.dims!r})" if self.n_metrics != 1: return f"Metric(name={self.name!r}, n_metrics={self.n_metrics})" return f"Metric(name={self.name!r})"