Spaces:
Sleeping
Sleeping
File size: 7,440 Bytes
3116157 ac7a28c e407ec0 3116157 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 | """``MetricRegistry`` — Sprint A14-S5.
Container mutable qui associe chaque ``MetricSpec`` à son callable
de calcul. **Instancié explicitement** par un service au démarrage
de l'application (cf. ``picarones/app/services/registry_service.py``
au S20) — pas de singleton global, pas de side-effect d'import,
pas de décorateur magique.
Différence avec ``picarones.evaluation.metric_registry``
--------------------------------------------------------
L'autre registre utilise un dict module-level ``_METRIC_REGISTRY``
rempli par un décorateur ``@register_metric`` appliqué au top-level
d'autres modules. Conséquence : un ``import picarones`` charge
~50 sous-modules pour amorcer le registre.
Ici, ``MetricRegistry`` est une classe instanciable :
.. code-block:: python
from picarones.domain import ArtifactType
from picarones.domain.evaluation_spec import MetricSpec
from picarones.evaluation.registry import MetricRegistry
reg = MetricRegistry()
reg.register(
MetricSpec(name="cer", input_types=(
ArtifactType.RAW_TEXT, ArtifactType.RAW_TEXT,
)),
compute_cer, # callable
)
selected = reg.select(
ArtifactType.RAW_TEXT, ArtifactType.RAW_TEXT,
)
Anti-sur-ingénierie
-------------------
Pas de gestion de versions de métrique, pas de namespace, pas de
recherche par tag. Si un caller a besoin de ces features, il les
implémentera quand le besoin sera concret (probablement S15+).
"""
from __future__ import annotations
from typing import Any, Callable
from picarones.domain.artifacts import ArtifactType
from picarones.domain.errors import PicaronesError
from picarones.domain.evaluation_spec import MetricSpec
class MetricRegistrationError(PicaronesError):
"""Tentative d'enregistrement invalide d'une métrique."""
class MetricNotFoundError(PicaronesError):
"""La métrique demandée n'est pas enregistrée."""
class MetricRegistry:
"""Container mutable de ``MetricSpec`` + callables.
Thread-safe en lecture après initialisation ; la séquence
d'enregistrement attendue est : un seul service, au démarrage,
enregistre toutes les métriques en une fois, puis l'instance
est figée par convention (lecture seule depuis les services
consommateurs).
Pas de mécanisme de freeze technique pour l'instant — si un
caller modifie le registre après le bootstrap, c'est de sa
responsabilité.
"""
def __init__(self) -> None:
self._specs: dict[str, MetricSpec] = {}
self._callables: dict[str, Callable[..., Any]] = {}
# ──────────────────────────────────────────────────────────────────
# Enregistrement
# ──────────────────────────────────────────────────────────────────
def register(self, spec: MetricSpec, func: Callable[..., Any]) -> None:
"""Enregistre une métrique.
Raises
------
MetricRegistrationError
Si une métrique du même nom est déjà enregistrée
(sauf re-enregistrement strict du même couple
``(spec, func)``, toléré pour les tests qui re-instancient).
"""
if not callable(func):
raise MetricRegistrationError(
f"register({spec.name!r}) : func n'est pas callable."
)
if spec.name in self._specs:
existing_spec = self._specs[spec.name]
existing_func = self._callables[spec.name]
if existing_spec == spec and existing_func is func:
return # idempotent
raise MetricRegistrationError(
f"Métrique {spec.name!r} déjà enregistrée avec une "
"autre spec ou un autre callable."
)
self._specs[spec.name] = spec
self._callables[spec.name] = func
# ──────────────────────────────────────────────────────────────────
# Lecture
# ──────────────────────────────────────────────────────────────────
def __contains__(self, name: str) -> bool:
return name in self._specs
def __len__(self) -> int:
return len(self._specs)
def names(self) -> list[str]:
"""Liste des noms enregistrés (ordre d'enregistrement)."""
return list(self._specs.keys())
def get_spec(self, name: str) -> MetricSpec:
if name not in self._specs:
raise MetricNotFoundError(
f"Métrique {name!r} non enregistrée. "
f"Disponibles : {sorted(self._specs)}."
)
return self._specs[name]
def get_callable(self, name: str) -> Callable[..., Any]:
if name not in self._callables:
raise MetricNotFoundError(
f"Callable de métrique {name!r} non enregistré."
)
return self._callables[name]
def select(
self,
reference_type: ArtifactType,
hypothesis_type: ArtifactType,
) -> list[MetricSpec]:
"""Métriques applicables à une jonction donnée (signature exacte)."""
target = (reference_type, hypothesis_type)
return [s for s in self._specs.values() if s.input_types == target]
# ──────────────────────────────────────────────────────────────────
# Calcul
# ──────────────────────────────────────────────────────────────────
def compute(
self,
name: str,
reference: Any,
hypothesis: Any,
) -> Any:
"""Calcule la métrique nommée sur la paire (référence, hypothèse).
Aucune capture d'exception : si la métrique lève, l'exception
remonte au caller (qui est typiquement un
``EvaluationViewExecutor`` qui décide quoi en faire dans son
``ProjectionReport``).
"""
func = self.get_callable(name)
return func(reference, hypothesis)
def compute_at_junction(
self,
reference: Any,
hypothesis: Any,
reference_type: ArtifactType,
hypothesis_type: ArtifactType,
) -> dict[str, Any]:
"""Calcule **toutes** les métriques applicables à la jonction.
Retourne ``{metric_name: value}``. Une métrique qui lève
est absente du dict (warning loggé au niveau caller via
l'EvaluationViewExecutor — ici on remonte l'exception pour
que les tests détectent les bugs).
"""
results: dict[str, Any] = {}
for spec in self.select(reference_type, hypothesis_type):
results[spec.name] = self.compute(spec.name, reference, hypothesis)
return results
__all__ = [
"MetricRegistry",
"MetricRegistrationError",
"MetricNotFoundError",
]
|