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",
]