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)
- Look up each metric's
MetricSpec(direction, best, worst). 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.Take the geometric mean::
J = ( prod_k d_k ) ** (1/K)
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]
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).
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.
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.
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