rpx_benchmark.jedi

JEDI: Joint Empirical Desirability Index.

Implements the composite desirability score from paper §3.3. JEDI is a direction-aware geometric mean over per-metric individual desirabilities \( d_k \in [0, 1] \). Failing any one metric drives the composite toward zero unless the caller opts into an \( \varepsilon \)-floor.

Pipeline (paper Eqs. 4–6)

  1. Look up each metric's MetricSpec (direction, best, worst).
  2. Map raw score \( x_k \) to individual desirability::

    d_k = clip( (x_k - x_k^worst) / (x_k^best - x_k^worst), epsilon, 1 )
    

    The numerator and denominator share the same sign by construction (paper §3.3), so \( d_k \in [0, 1] \) without ad-hoc negation. The clip(·, eps, 1) guards out-of-bounds values.

  3. Take the geometric mean::

    J = ( prod_k d_k ) ** (1/K)

  4. For per-(scene, phase) aggregation, average \( J_{s,p} \) across the \( 3N \) cells to get \( J_{model} \).

Default \( \varepsilon = 0 \) keeps the strict "any failure tanks J" semantics the paper argues for; practitioners who want softer behaviour can pass a small positive epsilon (e.g. 0.01).

  1"""JEDI: Joint Empirical Desirability Index.
  2
  3Implements the composite desirability score from paper §3.3. JEDI is a
  4direction-aware geometric mean over per-metric individual desirabilities
  5:math:`d_k \\in [0, 1]`. Failing any one metric drives the composite toward
  6zero unless the caller opts into an :math:`\\varepsilon`-floor.
  7
  8Pipeline (paper Eqs. 4–6)
  9-------------------------
 10
 111. Look up each metric's :class:`MetricSpec` (direction, best, worst).
 122. Map raw score :math:`x_k` to individual desirability::
 13
 14       d_k = clip( (x_k - x_k^worst) / (x_k^best - x_k^worst), epsilon, 1 )
 15
 16   The numerator and denominator share the *same sign* by construction
 17   (paper §3.3), so :math:`d_k \\in [0, 1]` without ad-hoc negation. The
 18   ``clip(·, eps, 1)`` guards out-of-bounds values.
 19
 203. Take the geometric mean::
 21
 22       J = ( prod_k d_k ) ** (1/K)
 23
 244. For per-(scene, phase) aggregation, average :math:`J_{s,p}` across the
 25   :math:`3N` cells to get :math:`J_{model}`.
 26
 27Default :math:`\\varepsilon = 0` keeps the strict "any failure tanks J"
 28semantics the paper argues for; practitioners who want softer behaviour
 29can pass a small positive ``epsilon`` (e.g. 0.01).
 30"""
 31
 32from __future__ import annotations
 33
 34from dataclasses import dataclass
 35from typing import Dict, List, Mapping, Optional, Sequence
 36
 37import numpy as np
 38
 39from .exceptions import MetricError
 40from .logging_utils import get_logger
 41from .metrics.specs import MetricSpec, get_spec
 42
 43log = get_logger(__name__)
 44
 45
 46# --------------------------------------------------------------------------- #
 47# Per-cell JEDI
 48# --------------------------------------------------------------------------- #
 49
 50
 51@dataclass
 52class JEDIResult:
 53    """Outcome of a single (scene, phase) JEDI computation.
 54
 55    Attributes
 56    ----------
 57    score : float
 58        The composite J = ( ∏ d_k )^(1/K) ∈ [eps^1, 1].
 59    individual : Dict[str, float]
 60        Per-metric individual desirabilities d_k.
 61    out_of_bounds : List[str]
 62        Metric names whose raw value fell outside the registered
 63        (worst, best) window and were clipped. Useful for flagging
 64        models that exceed the frozen bounds (paper §3.3).
 65    epsilon : float
 66        The ε floor used; 0.0 by default (strict).
 67    """
 68
 69    score: float
 70    individual: Dict[str, float]
 71    out_of_bounds: List[str]
 72    epsilon: float = 0.0
 73
 74    def to_dict(self) -> Dict[str, object]:
 75        return {
 76            "jedi": self.score,
 77            "individual": dict(self.individual),
 78            "out_of_bounds": list(self.out_of_bounds),
 79            "epsilon": self.epsilon,
 80        }
 81
 82
 83def _desirability(raw: float, spec: MetricSpec, epsilon: float) -> tuple[float, bool]:
 84    """Compute one individual desirability d_k and the out-of-bounds flag.
 85
 86    Returns
 87    -------
 88    d_k : float
 89        Clipped into ``[max(epsilon, 0), 1]``.
 90    out_of_bounds : bool
 91        True iff the raw value fell outside ``(worst, best)`` before
 92        clipping. Helps surface models that exceed the frozen window.
 93    """
 94    span = spec.best - spec.worst
 95    # span is positive for higher-is-better, negative for lower-is-better;
 96    # numerator (raw - worst) carries the same sign by construction, so the
 97    # ratio is in [0, 1] when raw ∈ [worst, best] regardless of direction.
 98    ratio = (raw - spec.worst) / span
 99
100    floor = max(epsilon, 0.0)
101    clipped = float(np.clip(ratio, floor, 1.0))
102    oob = bool(ratio < 0.0 or ratio > 1.0)
103    return clipped, oob
104
105
106def compute_jedi(
107    raw_metrics: Mapping[str, float],
108    *,
109    epsilon: float = 0.0,
110    specs: Optional[Mapping[str, MetricSpec]] = None,
111) -> JEDIResult:
112    """Compute JEDI for one (scene, phase) cell.
113
114    Parameters
115    ----------
116    raw_metrics : Mapping[str, float]
117        ``{metric_name: raw_score}`` for the K metrics of one task on
118        one (scene, phase). Keys must have specs registered (either
119        via the built-in registry in :mod:`rpx_benchmark.metrics.specs`
120        or via explicit ``specs=`` override).
121    epsilon : float
122        Lower floor on individual desirabilities. Default 0.0 (strict
123        Derringer–Suich: a single failure zeros J). Pass e.g. 0.01 for
124        a softer penalty (paper §3.3, "Practitioners who prefer a softer
125        penalty …").
126    specs : Mapping[str, MetricSpec], optional
127        Override the global spec registry. When None, each name is
128        resolved via :func:`get_spec`.
129
130    Returns
131    -------
132    JEDIResult
133        The composite score, per-metric d_k breakdown, and any
134        out-of-bounds flags.
135
136    Raises
137    ------
138    MetricError
139        If ``raw_metrics`` is empty or any metric has no registered spec.
140    """
141    if not raw_metrics:
142        raise MetricError(
143            "compute_jedi requires at least one metric",
144            hint="Pass {metric_name: raw_score, ...} with K >= 1 entries.",
145        )
146    if epsilon < 0.0 or epsilon >= 1.0:
147        raise MetricError(
148            f"epsilon must be in [0, 1), got {epsilon}",
149            hint="Use epsilon=0 for strict, or a small positive value (e.g. 0.01).",
150        )
151
152    individual: Dict[str, float] = {}
153    oob: List[str] = []
154    for name, raw in raw_metrics.items():
155        if not np.isfinite(raw):
156            raise MetricError(
157                f"Metric {name!r} has non-finite raw value {raw!r}",
158                hint="Filter NaN/Inf upstream or impute before computing JEDI.",
159            )
160        spec = specs[name] if specs is not None and name in specs else get_spec(name)
161        d_k, was_oob = _desirability(float(raw), spec, epsilon)
162        individual[name] = d_k
163        if was_oob:
164            oob.append(name)
165
166    # Geometric mean. Numerically safer via exp(mean(log(·))) when no d_k=0.
167    values = np.array(list(individual.values()), dtype=np.float64)
168    if (values == 0.0).any():
169        score = 0.0
170    else:
171        score = float(np.exp(np.log(values).mean()))
172
173    return JEDIResult(
174        score=score,
175        individual=individual,
176        out_of_bounds=oob,
177        epsilon=epsilon,
178    )
179
180
181# --------------------------------------------------------------------------- #
182# Per-model aggregation across (scene, phase)
183# --------------------------------------------------------------------------- #
184
185
186@dataclass
187class JEDIModelResult:
188    """Per-model JEDI aggregated over all (scene, phase) cells.
189
190    Attributes
191    ----------
192    jedi : float
193        ``mean_{s,p} J_{s,p}`` — paper Eq. 6.
194    per_phase : Dict[str, float]
195        Per-phase means: ``{"clu": J_clu, "int": J_int, "cln": J_cln}``.
196        Phase names are passed through from input keys.
197    n_cells : int
198        Number of (scene, phase) cells contributing to the mean.
199    n_out_of_bounds : int
200        Total count of out-of-bounds metric flags across all cells.
201    """
202
203    jedi: float
204    per_phase: Dict[str, float]
205    n_cells: int
206    n_out_of_bounds: int
207
208    def to_dict(self) -> Dict[str, object]:
209        return {
210            "jedi": self.jedi,
211            "per_phase": dict(self.per_phase),
212            "n_cells": self.n_cells,
213            "n_out_of_bounds": self.n_out_of_bounds,
214        }
215
216
217def aggregate_jedi(
218    per_cell: Sequence[tuple[str, JEDIResult]],
219) -> JEDIModelResult:
220    """Aggregate per-cell JEDI scores into a per-model summary.
221
222    Parameters
223    ----------
224    per_cell : sequence of (phase_label, JEDIResult)
225        One tuple per (scene, phase) cell. ``phase_label`` is the
226        column key (e.g. ``"clu"`` / ``"int"`` / ``"cln"``); rows
227        within the same phase are averaged together.
228
229    Returns
230    -------
231    JEDIModelResult
232    """
233    if not per_cell:
234        raise MetricError(
235            "aggregate_jedi requires at least one per-cell result",
236            hint="Pass [(phase_label, JEDIResult), ...] with len >= 1.",
237        )
238
239    by_phase: Dict[str, List[float]] = {}
240    total_oob = 0
241    for phase_label, r in per_cell:
242        by_phase.setdefault(phase_label, []).append(r.score)
243        total_oob += len(r.out_of_bounds)
244
245    per_phase = {p: float(np.mean(vs)) for p, vs in by_phase.items()}
246    overall = float(np.mean([r.score for _, r in per_cell]))
247
248    return JEDIModelResult(
249        jedi=overall,
250        per_phase=per_phase,
251        n_cells=len(per_cell),
252        n_out_of_bounds=total_oob,
253    )
254
255
256__all__ = [
257    "JEDIResult",
258    "JEDIModelResult",
259    "compute_jedi",
260    "aggregate_jedi",
261]
@dataclass
class JEDIResult:
52@dataclass
53class JEDIResult:
54    """Outcome of a single (scene, phase) JEDI computation.
55
56    Attributes
57    ----------
58    score : float
59        The composite J = ( ∏ d_k )^(1/K) ∈ [eps^1, 1].
60    individual : Dict[str, float]
61        Per-metric individual desirabilities d_k.
62    out_of_bounds : List[str]
63        Metric names whose raw value fell outside the registered
64        (worst, best) window and were clipped. Useful for flagging
65        models that exceed the frozen bounds (paper §3.3).
66    epsilon : float
67        The ε floor used; 0.0 by default (strict).
68    """
69
70    score: float
71    individual: Dict[str, float]
72    out_of_bounds: List[str]
73    epsilon: float = 0.0
74
75    def to_dict(self) -> Dict[str, object]:
76        return {
77            "jedi": self.score,
78            "individual": dict(self.individual),
79            "out_of_bounds": list(self.out_of_bounds),
80            "epsilon": self.epsilon,
81        }

Outcome of a single (scene, phase) JEDI computation.

Attributes

score : float The composite J = ( ∏ d_k )^(1/K) ∈ [eps^1, 1]. individual : Dict[str, float] Per-metric individual desirabilities d_k. out_of_bounds : List[str] Metric names whose raw value fell outside the registered (worst, best) window and were clipped. Useful for flagging models that exceed the frozen bounds (paper §3.3). epsilon : float The ε floor used; 0.0 by default (strict).

JEDIResult( score: float, individual: Dict[str, float], out_of_bounds: List[str], epsilon: float = 0.0)
score: float
individual: Dict[str, float]
out_of_bounds: List[str]
epsilon: float = 0.0
def to_dict(self) -> Dict[str, object]:
75    def to_dict(self) -> Dict[str, object]:
76        return {
77            "jedi": self.score,
78            "individual": dict(self.individual),
79            "out_of_bounds": list(self.out_of_bounds),
80            "epsilon": self.epsilon,
81        }
@dataclass
class JEDIModelResult:
187@dataclass
188class JEDIModelResult:
189    """Per-model JEDI aggregated over all (scene, phase) cells.
190
191    Attributes
192    ----------
193    jedi : float
194        ``mean_{s,p} J_{s,p}`` — paper Eq. 6.
195    per_phase : Dict[str, float]
196        Per-phase means: ``{"clu": J_clu, "int": J_int, "cln": J_cln}``.
197        Phase names are passed through from input keys.
198    n_cells : int
199        Number of (scene, phase) cells contributing to the mean.
200    n_out_of_bounds : int
201        Total count of out-of-bounds metric flags across all cells.
202    """
203
204    jedi: float
205    per_phase: Dict[str, float]
206    n_cells: int
207    n_out_of_bounds: int
208
209    def to_dict(self) -> Dict[str, object]:
210        return {
211            "jedi": self.jedi,
212            "per_phase": dict(self.per_phase),
213            "n_cells": self.n_cells,
214            "n_out_of_bounds": self.n_out_of_bounds,
215        }

Per-model JEDI aggregated over all (scene, phase) cells.

Attributes

jedi : float mean_{s,p} J_{s,p} — paper Eq. 6. per_phase : Dict[str, float] Per-phase means: {"clu": J_clu, "int": J_int, "cln": J_cln}. Phase names are passed through from input keys. n_cells : int Number of (scene, phase) cells contributing to the mean. n_out_of_bounds : int Total count of out-of-bounds metric flags across all cells.

JEDIModelResult( jedi: float, per_phase: Dict[str, float], n_cells: int, n_out_of_bounds: int)
jedi: float
per_phase: Dict[str, float]
n_cells: int
n_out_of_bounds: int
def to_dict(self) -> Dict[str, object]:
209    def to_dict(self) -> Dict[str, object]:
210        return {
211            "jedi": self.jedi,
212            "per_phase": dict(self.per_phase),
213            "n_cells": self.n_cells,
214            "n_out_of_bounds": self.n_out_of_bounds,
215        }
def compute_jedi( raw_metrics: Mapping[str, float], *, epsilon: float = 0.0, specs: Optional[Mapping[str, rpx_benchmark.metrics.specs.MetricSpec]] = None) -> JEDIResult:
107def compute_jedi(
108    raw_metrics: Mapping[str, float],
109    *,
110    epsilon: float = 0.0,
111    specs: Optional[Mapping[str, MetricSpec]] = None,
112) -> JEDIResult:
113    """Compute JEDI for one (scene, phase) cell.
114
115    Parameters
116    ----------
117    raw_metrics : Mapping[str, float]
118        ``{metric_name: raw_score}`` for the K metrics of one task on
119        one (scene, phase). Keys must have specs registered (either
120        via the built-in registry in :mod:`rpx_benchmark.metrics.specs`
121        or via explicit ``specs=`` override).
122    epsilon : float
123        Lower floor on individual desirabilities. Default 0.0 (strict
124        Derringer–Suich: a single failure zeros J). Pass e.g. 0.01 for
125        a softer penalty (paper §3.3, "Practitioners who prefer a softer
126        penalty …").
127    specs : Mapping[str, MetricSpec], optional
128        Override the global spec registry. When None, each name is
129        resolved via :func:`get_spec`.
130
131    Returns
132    -------
133    JEDIResult
134        The composite score, per-metric d_k breakdown, and any
135        out-of-bounds flags.
136
137    Raises
138    ------
139    MetricError
140        If ``raw_metrics`` is empty or any metric has no registered spec.
141    """
142    if not raw_metrics:
143        raise MetricError(
144            "compute_jedi requires at least one metric",
145            hint="Pass {metric_name: raw_score, ...} with K >= 1 entries.",
146        )
147    if epsilon < 0.0 or epsilon >= 1.0:
148        raise MetricError(
149            f"epsilon must be in [0, 1), got {epsilon}",
150            hint="Use epsilon=0 for strict, or a small positive value (e.g. 0.01).",
151        )
152
153    individual: Dict[str, float] = {}
154    oob: List[str] = []
155    for name, raw in raw_metrics.items():
156        if not np.isfinite(raw):
157            raise MetricError(
158                f"Metric {name!r} has non-finite raw value {raw!r}",
159                hint="Filter NaN/Inf upstream or impute before computing JEDI.",
160            )
161        spec = specs[name] if specs is not None and name in specs else get_spec(name)
162        d_k, was_oob = _desirability(float(raw), spec, epsilon)
163        individual[name] = d_k
164        if was_oob:
165            oob.append(name)
166
167    # Geometric mean. Numerically safer via exp(mean(log(·))) when no d_k=0.
168    values = np.array(list(individual.values()), dtype=np.float64)
169    if (values == 0.0).any():
170        score = 0.0
171    else:
172        score = float(np.exp(np.log(values).mean()))
173
174    return JEDIResult(
175        score=score,
176        individual=individual,
177        out_of_bounds=oob,
178        epsilon=epsilon,
179    )

Compute JEDI for one (scene, phase) cell.

Parameters

raw_metrics : Mapping[str, float] {metric_name: raw_score} for the K metrics of one task on one (scene, phase). Keys must have specs registered (either via the built-in registry in rpx_benchmark.metrics.specs or via explicit specs= override). epsilon : float Lower floor on individual desirabilities. Default 0.0 (strict Derringer–Suich: a single failure zeros J). Pass e.g. 0.01 for a softer penalty (paper §3.3, "Practitioners who prefer a softer penalty …"). specs : Mapping[str, MetricSpec], optional Override the global spec registry. When None, each name is resolved via get_spec().

Returns

JEDIResult The composite score, per-metric d_k breakdown, and any out-of-bounds flags.

Raises

MetricError If raw_metrics is empty or any metric has no registered spec.

def aggregate_jedi( per_cell: Sequence[tuple[str, JEDIResult]]) -> JEDIModelResult:
218def aggregate_jedi(
219    per_cell: Sequence[tuple[str, JEDIResult]],
220) -> JEDIModelResult:
221    """Aggregate per-cell JEDI scores into a per-model summary.
222
223    Parameters
224    ----------
225    per_cell : sequence of (phase_label, JEDIResult)
226        One tuple per (scene, phase) cell. ``phase_label`` is the
227        column key (e.g. ``"clu"`` / ``"int"`` / ``"cln"``); rows
228        within the same phase are averaged together.
229
230    Returns
231    -------
232    JEDIModelResult
233    """
234    if not per_cell:
235        raise MetricError(
236            "aggregate_jedi requires at least one per-cell result",
237            hint="Pass [(phase_label, JEDIResult), ...] with len >= 1.",
238        )
239
240    by_phase: Dict[str, List[float]] = {}
241    total_oob = 0
242    for phase_label, r in per_cell:
243        by_phase.setdefault(phase_label, []).append(r.score)
244        total_oob += len(r.out_of_bounds)
245
246    per_phase = {p: float(np.mean(vs)) for p, vs in by_phase.items()}
247    overall = float(np.mean([r.score for _, r in per_cell]))
248
249    return JEDIModelResult(
250        jedi=overall,
251        per_phase=per_phase,
252        n_cells=len(per_cell),
253        n_out_of_bounds=total_oob,
254    )

Aggregate per-cell JEDI scores into a per-model summary.

Parameters

per_cell : sequence of (phase_label, JEDIResult) One tuple per (scene, phase) cell. phase_label is the column key (e.g. "clu" / "int" / "cln"); rows within the same phase are averaged together.

Returns

JEDIModelResult