rpx_benchmark.metrics.specs

Per-metric specifications used by JEDI and Φ.

A MetricSpec carries the direction (higher-is-better vs lower-is- better), the per-metric bounds (best, worst), and whether those bounds are theoretical (e.g., AP ∈ [0,1]) or empirically frozen across a fixed model zoo. Specs are registered alongside the metric calculators that produce the numeric values; downstream code (JEDI in rpx_benchmark.jedi, Φ in rpx_benchmark.phi) consults the spec registry rather than hard-coding direction or bounds.

The design follows paper §3.3: direction-aware bounds avoid the "negate lower-is-better metrics" foot-gun, and the frozen-observed bounds policy makes JEDI reproducible across re-evaluations.

Usage

Calculators expose their specs via a specs() classmethod; the metric runtime imports the module, the @register_metric decorator runs, and specs are auto-registered. Direct lookup::

from rpx_benchmark.metrics import get_spec
spec = get_spec("absrel")
assert spec.direction == "lower"
  1"""Per-metric specifications used by JEDI and Φ.
  2
  3A :class:`MetricSpec` carries the *direction* (higher-is-better vs lower-is-
  4better), the per-metric *bounds* ``(best, worst)``, and whether those bounds
  5are theoretical (e.g., AP ∈ [0,1]) or empirically frozen across a fixed model
  6zoo. Specs are registered alongside the metric calculators that produce the
  7numeric values; downstream code (JEDI in :mod:`rpx_benchmark.jedi`, Φ in
  8:mod:`rpx_benchmark.phi`) consults the spec registry rather than hard-coding
  9direction or bounds.
 10
 11The design follows paper §3.3: direction-aware bounds avoid the "negate
 12lower-is-better metrics" foot-gun, and the frozen-observed bounds policy
 13makes JEDI reproducible across re-evaluations.
 14
 15Usage
 16-----
 17
 18Calculators expose their specs via a ``specs()`` classmethod; the metric
 19runtime imports the module, the ``@register_metric`` decorator runs, and
 20specs are auto-registered. Direct lookup::
 21
 22    from rpx_benchmark.metrics import get_spec
 23    spec = get_spec("absrel")
 24    assert spec.direction == "lower"
 25"""
 26
 27from __future__ import annotations
 28
 29from dataclasses import dataclass
 30from typing import Dict, Iterable, Literal
 31
 32from ..exceptions import MetricError
 33from ..logging_utils import get_logger
 34
 35log = get_logger(__name__)
 36
 37
 38Direction = Literal["higher", "lower"]
 39
 40
 41@dataclass(frozen=True)
 42class MetricSpec:
 43    """Static metadata about a single scalar metric.
 44
 45    Attributes
 46    ----------
 47    name : str
 48        Key under which the metric is emitted by the calculator
 49        (e.g. ``"absrel"``, ``"ap50"``). Must match the dict key that
 50        :meth:`MetricCalculator.compute` returns.
 51    direction : {"higher", "lower"}
 52        Whether higher raw values indicate better performance.
 53    best : float
 54        The optimal end of the metric scale. For higher-is-better
 55        metrics this is the maximum; for lower-is-better, the minimum.
 56    worst : float
 57        The non-optimal end. For higher-is-better, the minimum; for
 58        lower-is-better, the maximum.
 59    theoretical : bool
 60        True iff (best, worst) are derived from the metric's
 61        mathematical definition (e.g. AP ∈ [0, 1]). False if they are
 62        empirically frozen over a model zoo. The distinction is
 63        documented per paper §3.3 — theoretical bounds are stable
 64        forever; empirical bounds are part of a versioned release.
 65    description : str
 66        One-line human-readable summary; printed in the JEDI/Φ output
 67        cards for debugging.
 68    """
 69
 70    name: str
 71    direction: Direction
 72    best: float
 73    worst: float
 74    theoretical: bool
 75    description: str = ""
 76
 77    def __post_init__(self) -> None:
 78        if self.direction not in ("higher", "lower"):
 79            raise MetricError(
 80                f"MetricSpec direction must be 'higher' or 'lower', got {self.direction!r}",
 81                hint="See rpx_benchmark.metrics.specs.Direction.",
 82            )
 83        if self.direction == "higher" and self.best <= self.worst:
 84            raise MetricError(
 85                f"MetricSpec({self.name!r}): direction=higher requires best > worst, "
 86                f"got best={self.best}, worst={self.worst}",
 87            )
 88        if self.direction == "lower" and self.best >= self.worst:
 89            raise MetricError(
 90                f"MetricSpec({self.name!r}): direction=lower requires best < worst, "
 91                f"got best={self.best}, worst={self.worst}",
 92            )
 93
 94
 95# --------------------------------------------------------------------------- #
 96# Registry
 97# --------------------------------------------------------------------------- #
 98
 99_SPECS: Dict[str, MetricSpec] = {}
100
101
102def register_spec(spec: MetricSpec) -> MetricSpec:
103    """Register a :class:`MetricSpec` under its ``name``.
104
105    Idempotent: re-registering the same name with an *identical* spec is a
106    silent no-op; re-registering with a *different* spec raises
107    :class:`MetricError` (to catch accidental overwrites during module
108    reloads).
109    """
110    existing = _SPECS.get(spec.name)
111    if existing is not None and existing != spec:
112        raise MetricError(
113            f"MetricSpec for {spec.name!r} already registered with different values; "
114            f"existing={existing}, new={spec}",
115            hint="Either match the existing spec or unregister it first.",
116        )
117    _SPECS[spec.name] = spec
118    return spec
119
120
121def register_specs(specs: Iterable[MetricSpec]) -> None:
122    """Convenience wrapper to register a batch."""
123    for s in specs:
124        register_spec(s)
125
126
127def get_spec(name: str) -> MetricSpec:
128    """Look up the spec for a metric ``name``; raise if unknown."""
129    try:
130        return _SPECS[name]
131    except KeyError as e:
132        raise MetricError(
133            f"No MetricSpec registered for {name!r}",
134            hint=(
135                "Import the metric module that owns this name "
136                "(e.g. `import rpx_benchmark.metrics.depth`) so its specs "
137                "auto-register, or call register_spec(...) explicitly."
138            ),
139        ) from e
140
141
142def has_spec(name: str) -> bool:
143    """True iff a spec is registered under ``name``."""
144    return name in _SPECS
145
146
147def registered_specs() -> Dict[str, MetricSpec]:
148    """Snapshot of the current spec registry (defensive copy)."""
149    return dict(_SPECS)
150
151
152def clear_registry() -> None:
153    """Remove every registered spec. Primarily for tests."""
154    _SPECS.clear()
155
156
157# --------------------------------------------------------------------------- #
158# Built-in specs for every shipped task
159# --------------------------------------------------------------------------- #
160#
161# Bounds policy (paper §3.3):
162#   - theoretical=True  → derived from the metric's mathematical definition
163#                          (e.g. AP_50 ∈ [0, 1]).
164#   - theoretical=False → frozen observed bounds across the shipped model
165#                          zoo. The numbers below are the v1 freeze; rerunning
166#                          on a new model set requires an explicit re-release
167#                          (see §3.3 paragraph on out-of-bounds clipping).
168#
169# When a new model produces a score outside (worst, best), the individual
170# desirability d_k clips to [0, 1] and the toolkit flags the cell
171# `out-of-bounds`. Bounds are never silently re-fit.
172
173# Depth (Image Depth frame-level & Video Depth video) — robotics-first set.
174#
175# Organised into three blocks (see experiments/depth_benchmark/METRICS_FINAL.md):
176#   A. Standard accuracy   — citable, general-perception comparability.
177#   B. Robotics deployment — near-range, grasp-tolerance 3D, surface geometry.
178#   C. Temporal (video)    — per-clip stability; lower-is-better except TCC.
179#
180# Range-stratified variants (e.g. "absrel_near"/"_mid"/"_far") are NOT
181# registered individually: the summary layer resolves a suffixed name to its
182# base spec by stripping the trailing _near/_mid/_far token.
183#
184# Intentionally EXCLUDED from the final set (feasibility, see METRICS_FINAL.md):
185#   - relnormal : 1M-Sobol curvature consistency — highest cost-to-signal.
186#   - irmse     : inverse-depth RMSE — low added info over AbsRel/SILog on D435.
187# delta2/delta3 stay registered (the calculator still emits them) but are
188# reported as secondary; delta1 is the primary threshold metric.
189#
190# Bounds tagged "provisional v0" are placeholders to be FROZEN after the v1
191# model-zoo run (cell_log → frozen empirical bounds). Theoretical bounds
192# (fractions ∈ [0,1]) are stable forever.
193DEPTH_SPECS = (
194    # A. Standard accuracy — metric mode; relative models evaluated post-alignment
195    MetricSpec("absrel",    "lower",  best=0.0,  worst=0.5,  theoretical=False,
196               description="Absolute relative depth error |d̂−d|/d"),
197    MetricSpec("rmse",      "lower",  best=0.0,  worst=2.0,  theoretical=False,
198               description="Root-mean-square depth error (metres)"),
199    MetricSpec("rmselog",   "lower",  best=0.0,  worst=0.5,  theoretical=False,
200               description="RMSE in log-depth space (provisional v0)"),
201    MetricSpec("silog",     "lower",  best=0.0,  worst=25.0, theoretical=False,
202               description="Scale-invariant log error Var(log d̂−log d) (provisional v0)"),
203    MetricSpec("delta1",    "higher", best=1.0,  worst=0.0,  theoretical=True,
204               description="Fraction of pixels with max(d̂/d, d/d̂) < 1.25 (primary)"),
205    MetricSpec("delta2",    "higher", best=1.0,  worst=0.0,  theoretical=True,
206               description="Threshold accuracy < 1.25² (secondary)"),
207    MetricSpec("delta3",    "higher", best=1.0,  worst=0.0,  theoretical=True,
208               description="Threshold accuracy < 1.25³ (secondary)"),
209    # B. Robotics deployment — near-range, grasp-tolerance 3D, surface geometry
210    MetricSpec("chamfer_l1",    "lower",  best=0.0,  worst=0.5,  theoretical=False,
211               description="Camera-frame Chamfer-L1 of back-projected cloud, metres — pose-free (provisional v0)"),
212    MetricSpec("fscore_5cm",    "higher", best=1.0,  worst=0.0,  theoretical=True,
213               description="Grasp-reachability F-score @ 5cm, camera-frame. Also report @1cm/@2cm."),
214    MetricSpec("normal_mae",    "lower",  best=0.0,  worst=45.0, theoretical=False,
215               description="Mean angular error of depth-derived normals, degrees — grasp orientation (provisional v0)"),
216    MetricSpec("normal_acc1125","higher", best=1.0,  worst=0.0,  theoretical=True,
217               description="Fraction of surface normals within 11.25° of GT"),
218    MetricSpec("boundary_f1",   "higher", best=1.0,  worst=0.0,  theoretical=True,
219               description="Scale-invariant occluding-contour F1 from depth ratios — relative-only on holey GT"),
220    # C. Temporal (Video Depth only; per phase clip)
221    MetricSpec("opw",  "lower",  best=0.0, worst=0.1, theoretical=False,
222               description="Optical-flow warping consistency, RAFT flow. Not in RPX K-vector (external flow model dependency; RGB-D-only policy). Diagnostic only (provisional v0)"),
223    MetricSpec("tae",  "lower",  best=0.0, worst=0.1, theoretical=False,
224               description="Temporal alignment error via SE(3) reprojection. Not in RPX K-vector (needs T265 poses; RGB-D-only policy). Diagnostic only (provisional v0)"),
225    MetricSpec("tgm",  "lower",  best=0.0, worst=0.1, theoretical=False,
226               description="Temporal gradient matching vs GT depth (provisional v0)"),
227    MetricSpec("tcc",  "higher", best=1.0, worst=0.0, theoretical=True,
228               description="Temporal consistency (SSIM on depth-change maps)"),
229    MetricSpec("tgse", "lower",  best=0.0, worst=0.1, theoretical=False,
230               description="Temporal gradient squared error (L2, sign-preserving; VDPP, arXiv:2604.06665)"),
231    MetricSpec("tmc",  "higher", best=1.0, worst=0.0, theoretical=True,
232               description="Temporal motion consistency (SSIM on depth-flow maps; Zhang ICCV 2019)"),
233    # C.1 Range-stratified temporal (per depth bin)
234    MetricSpec("tae_near", "lower", best=0.0, worst=0.1, theoretical=False,
235               description="TAE at near range (0.3-1.0m, tabletop manipulation)"),
236    MetricSpec("tae_mid",  "lower", best=0.0, worst=0.1, theoretical=False,
237               description="TAE at mid range (1.0-2.5m, arm's reach)"),
238    MetricSpec("tae_far",  "lower", best=0.0, worst=0.1, theoretical=False,
239               description="TAE at far range (2.5-5.0m, room-scale)"),
240    # C.2 Range-stratified spatial accuracy (per depth bin, clip-averaged)
241    MetricSpec("absrel_near", "lower",  best=0.0, worst=1.0, theoretical=False,
242               description="AbsRel at near range (0.3-1.0m)"),
243    MetricSpec("absrel_mid",  "lower",  best=0.0, worst=1.0, theoretical=False,
244               description="AbsRel at mid range (1.0-2.5m)"),
245    MetricSpec("absrel_far",  "lower",  best=0.0, worst=1.0, theoretical=False,
246               description="AbsRel at far range (2.5-5.0m)"),
247    MetricSpec("rmse_near", "lower",  best=0.0, worst=1.0, theoretical=False,
248               description="RMSE at near range (0.3-1.0m)"),
249    MetricSpec("rmse_mid",  "lower",  best=0.0, worst=1.0, theoretical=False,
250               description="RMSE at mid range (1.0-2.5m)"),
251    MetricSpec("rmse_far",  "lower",  best=0.0, worst=1.0, theoretical=False,
252               description="RMSE at far range (2.5-5.0m)"),
253    MetricSpec("delta1_near", "higher", best=1.0, worst=0.0, theoretical=True,
254               description="delta1 at near range (0.3-1.0m)"),
255    MetricSpec("delta1_mid",  "higher", best=1.0, worst=0.0, theoretical=True,
256               description="delta1 at mid range (1.0-2.5m)"),
257    MetricSpec("delta1_far",  "higher", best=1.0, worst=0.0, theoretical=True,
258               description="delta1 at far range (2.5-5.0m)"),
259    # C.3 Range-stratified temporal (per depth bin, pose-free / flow-free)
260    MetricSpec("tgm_near", "lower", best=0.0, worst=0.1, theoretical=False,
261               description="TGM at near range (0.3-1.0m, tabletop manipulation)"),
262    MetricSpec("tgm_mid",  "lower", best=0.0, worst=0.1, theoretical=False,
263               description="TGM at mid range (1.0-2.5m, arm's reach)"),
264    MetricSpec("tgm_far",  "lower", best=0.0, worst=0.1, theoretical=False,
265               description="TGM at far range (2.5-5.0m, room-scale)"),
266    MetricSpec("tgse_near", "lower", best=0.0, worst=0.1, theoretical=False,
267               description="TGSE at near range (0.3-1.0m, tabletop manipulation)"),
268    MetricSpec("tgse_mid",  "lower", best=0.0, worst=0.1, theoretical=False,
269               description="TGSE at mid range (1.0-2.5m, arm's reach)"),
270    MetricSpec("tgse_far",  "lower", best=0.0, worst=0.1, theoretical=False,
271               description="TGSE at far range (2.5-5.0m, room-scale)"),
272)
273
274# Detection / Grounding (D2) — K=4
275DETECTION_SPECS = (
276    MetricSpec("ap50",    "higher", best=1.0, worst=0.0, theoretical=True,
277               description="AP at IoU≥0.50"),
278    MetricSpec("ap75",    "higher", best=1.0, worst=0.0, theoretical=True,
279               description="AP at IoU≥0.75"),
280    MetricSpec("map",     "higher", best=1.0, worst=0.0, theoretical=True,
281               description="mean AP averaged over IoU thresholds"),
282    MetricSpec("ar",      "higher", best=1.0, worst=0.0, theoretical=True,
283               description="Average recall"),
284)
285
286# Tracking (D3) — K=4
287TRACKING_SPECS = (
288    MetricSpec("mota",    "higher", best=1.0, worst=0.0, theoretical=True,
289               description="Multi-Object Tracking Accuracy"),
290    MetricSpec("idf1",    "higher", best=1.0, worst=0.0, theoretical=True,
291               description="ID F1 (identity matching F-score)"),
292    MetricSpec("hota",    "higher", best=1.0, worst=0.0, theoretical=True,
293               description="Higher-Order Tracking Accuracy"),
294    MetricSpec("id_sw",   "lower",  best=0.0, worst=100.0, theoretical=False,
295               description="Identity switches per scene (count)"),
296)
297
298# Scene QA (D4) and Spatial QA (D5) — K=2 each
299QA_SPECS = (
300    MetricSpec("fuzzy_acc",   "higher", best=1.0, worst=0.0, theoretical=True,
301               description="Normalised-similarity fuzzy match accuracy"),
302    MetricSpec("exact_match", "higher", best=1.0, worst=0.0, theoretical=True,
303               description="Strict string-equality accuracy"),
304)
305
306# Relative Camera Pose (D6) — K=4
307POSE_SPECS = (
308    MetricSpec("auc_5deg",   "higher", best=1.0, worst=0.0, theoretical=True,
309               description="AUC of rotation error CDF up to 5°"),
310    MetricSpec("auc_10deg",  "higher", best=1.0, worst=0.0, theoretical=True,
311               description="AUC of rotation error CDF up to 10°"),
312    MetricSpec("auc_20deg",  "higher", best=1.0, worst=0.0, theoretical=True,
313               description="AUC of rotation error CDF up to 20°"),
314    MetricSpec("metric_auc", "higher", best=1.0, worst=0.0, theoretical=True,
315               description="Joint rotation × metric-translation AUC"),
316)
317
318
319
320def _register_builtins() -> None:
321    """Populate the registry with every shipped task's specs at import time."""
322    for batch in (
323        DEPTH_SPECS,
324        DETECTION_SPECS,
325        TRACKING_SPECS,
326        QA_SPECS,
327        POSE_SPECS,
328    ):
329        register_specs(batch)
330
331
332_register_builtins()
333
334
335__all__ = [
336    "Direction",
337    "MetricSpec",
338    "register_spec",
339    "register_specs",
340    "get_spec",
341    "has_spec",
342    "registered_specs",
343    "clear_registry",
344    "DEPTH_SPECS",
345    "DETECTION_SPECS",
346    "TRACKING_SPECS",
347    "QA_SPECS",
348    "POSE_SPECS",
349]
Direction = typing.Literal['higher', 'lower']
@dataclass(frozen=True)
class MetricSpec:
42@dataclass(frozen=True)
43class MetricSpec:
44    """Static metadata about a single scalar metric.
45
46    Attributes
47    ----------
48    name : str
49        Key under which the metric is emitted by the calculator
50        (e.g. ``"absrel"``, ``"ap50"``). Must match the dict key that
51        :meth:`MetricCalculator.compute` returns.
52    direction : {"higher", "lower"}
53        Whether higher raw values indicate better performance.
54    best : float
55        The optimal end of the metric scale. For higher-is-better
56        metrics this is the maximum; for lower-is-better, the minimum.
57    worst : float
58        The non-optimal end. For higher-is-better, the minimum; for
59        lower-is-better, the maximum.
60    theoretical : bool
61        True iff (best, worst) are derived from the metric's
62        mathematical definition (e.g. AP ∈ [0, 1]). False if they are
63        empirically frozen over a model zoo. The distinction is
64        documented per paper §3.3 — theoretical bounds are stable
65        forever; empirical bounds are part of a versioned release.
66    description : str
67        One-line human-readable summary; printed in the JEDI/Φ output
68        cards for debugging.
69    """
70
71    name: str
72    direction: Direction
73    best: float
74    worst: float
75    theoretical: bool
76    description: str = ""
77
78    def __post_init__(self) -> None:
79        if self.direction not in ("higher", "lower"):
80            raise MetricError(
81                f"MetricSpec direction must be 'higher' or 'lower', got {self.direction!r}",
82                hint="See rpx_benchmark.metrics.specs.Direction.",
83            )
84        if self.direction == "higher" and self.best <= self.worst:
85            raise MetricError(
86                f"MetricSpec({self.name!r}): direction=higher requires best > worst, "
87                f"got best={self.best}, worst={self.worst}",
88            )
89        if self.direction == "lower" and self.best >= self.worst:
90            raise MetricError(
91                f"MetricSpec({self.name!r}): direction=lower requires best < worst, "
92                f"got best={self.best}, worst={self.worst}",
93            )

Static metadata about a single scalar metric.

Attributes

name : str Key under which the metric is emitted by the calculator (e.g. "absrel", "ap50"). Must match the dict key that MetricCalculator.compute() returns. direction : {"higher", "lower"} Whether higher raw values indicate better performance. best : float The optimal end of the metric scale. For higher-is-better metrics this is the maximum; for lower-is-better, the minimum. worst : float The non-optimal end. For higher-is-better, the minimum; for lower-is-better, the maximum. theoretical : bool True iff (best, worst) are derived from the metric's mathematical definition (e.g. AP ∈ [0, 1]). False if they are empirically frozen over a model zoo. The distinction is documented per paper §3.3 — theoretical bounds are stable forever; empirical bounds are part of a versioned release. description : str One-line human-readable summary; printed in the JEDI/Φ output cards for debugging.

MetricSpec( name: str, direction: Literal['higher', 'lower'], best: float, worst: float, theoretical: bool, description: str = '')
name: str
direction: Literal['higher', 'lower']
best: float
worst: float
theoretical: bool
description: str = ''
def register_spec( spec: MetricSpec) -> MetricSpec:
103def register_spec(spec: MetricSpec) -> MetricSpec:
104    """Register a :class:`MetricSpec` under its ``name``.
105
106    Idempotent: re-registering the same name with an *identical* spec is a
107    silent no-op; re-registering with a *different* spec raises
108    :class:`MetricError` (to catch accidental overwrites during module
109    reloads).
110    """
111    existing = _SPECS.get(spec.name)
112    if existing is not None and existing != spec:
113        raise MetricError(
114            f"MetricSpec for {spec.name!r} already registered with different values; "
115            f"existing={existing}, new={spec}",
116            hint="Either match the existing spec or unregister it first.",
117        )
118    _SPECS[spec.name] = spec
119    return spec

Register a MetricSpec under its name.

Idempotent: re-registering the same name with an identical spec is a silent no-op; re-registering with a different spec raises MetricError (to catch accidental overwrites during module reloads).

def register_specs(specs: Iterable[MetricSpec]) -> None:
122def register_specs(specs: Iterable[MetricSpec]) -> None:
123    """Convenience wrapper to register a batch."""
124    for s in specs:
125        register_spec(s)

Convenience wrapper to register a batch.

def get_spec(name: str) -> MetricSpec:
128def get_spec(name: str) -> MetricSpec:
129    """Look up the spec for a metric ``name``; raise if unknown."""
130    try:
131        return _SPECS[name]
132    except KeyError as e:
133        raise MetricError(
134            f"No MetricSpec registered for {name!r}",
135            hint=(
136                "Import the metric module that owns this name "
137                "(e.g. `import rpx_benchmark.metrics.depth`) so its specs "
138                "auto-register, or call register_spec(...) explicitly."
139            ),
140        ) from e

Look up the spec for a metric name; raise if unknown.

def has_spec(name: str) -> bool:
143def has_spec(name: str) -> bool:
144    """True iff a spec is registered under ``name``."""
145    return name in _SPECS

True iff a spec is registered under name.

def registered_specs() -> Dict[str, MetricSpec]:
148def registered_specs() -> Dict[str, MetricSpec]:
149    """Snapshot of the current spec registry (defensive copy)."""
150    return dict(_SPECS)

Snapshot of the current spec registry (defensive copy).

def clear_registry() -> None:
153def clear_registry() -> None:
154    """Remove every registered spec. Primarily for tests."""
155    _SPECS.clear()

Remove every registered spec. Primarily for tests.

DEPTH_SPECS = (MetricSpec(name='absrel', direction='lower', best=0.0, worst=0.5, theoretical=False, description='Absolute relative depth error |d̂−d|/d'), MetricSpec(name='rmse', direction='lower', best=0.0, worst=2.0, theoretical=False, description='Root-mean-square depth error (metres)'), MetricSpec(name='rmselog', direction='lower', best=0.0, worst=0.5, theoretical=False, description='RMSE in log-depth space (provisional v0)'), MetricSpec(name='silog', direction='lower', best=0.0, worst=25.0, theoretical=False, description='Scale-invariant log error Var(log d̂−log d) (provisional v0)'), MetricSpec(name='delta1', direction='higher', best=1.0, worst=0.0, theoretical=True, description='Fraction of pixels with max(d̂/d, d/d̂) < 1.25 (primary)'), MetricSpec(name='delta2', direction='higher', best=1.0, worst=0.0, theoretical=True, description='Threshold accuracy < 1.25² (secondary)'), MetricSpec(name='delta3', direction='higher', best=1.0, worst=0.0, theoretical=True, description='Threshold accuracy < 1.25³ (secondary)'), MetricSpec(name='chamfer_l1', direction='lower', best=0.0, worst=0.5, theoretical=False, description='Camera-frame Chamfer-L1 of back-projected cloud, metres — pose-free (provisional v0)'), MetricSpec(name='fscore_5cm', direction='higher', best=1.0, worst=0.0, theoretical=True, description='Grasp-reachability F-score @ 5cm, camera-frame. Also report @1cm/@2cm.'), MetricSpec(name='normal_mae', direction='lower', best=0.0, worst=45.0, theoretical=False, description='Mean angular error of depth-derived normals, degrees — grasp orientation (provisional v0)'), MetricSpec(name='normal_acc1125', direction='higher', best=1.0, worst=0.0, theoretical=True, description='Fraction of surface normals within 11.25° of GT'), MetricSpec(name='boundary_f1', direction='higher', best=1.0, worst=0.0, theoretical=True, description='Scale-invariant occluding-contour F1 from depth ratios — relative-only on holey GT'), MetricSpec(name='opw', direction='lower', best=0.0, worst=0.1, theoretical=False, description='Optical-flow warping consistency, RAFT flow. Not in RPX K-vector (external flow model dependency; RGB-D-only policy). Diagnostic only (provisional v0)'), MetricSpec(name='tae', direction='lower', best=0.0, worst=0.1, theoretical=False, description='Temporal alignment error via SE(3) reprojection. Not in RPX K-vector (needs T265 poses; RGB-D-only policy). Diagnostic only (provisional v0)'), MetricSpec(name='tgm', direction='lower', best=0.0, worst=0.1, theoretical=False, description='Temporal gradient matching vs GT depth (provisional v0)'), MetricSpec(name='tcc', direction='higher', best=1.0, worst=0.0, theoretical=True, description='Temporal consistency (SSIM on depth-change maps)'), MetricSpec(name='tgse', direction='lower', best=0.0, worst=0.1, theoretical=False, description='Temporal gradient squared error (L2, sign-preserving; VDPP, arXiv:2604.06665)'), MetricSpec(name='tmc', direction='higher', best=1.0, worst=0.0, theoretical=True, description='Temporal motion consistency (SSIM on depth-flow maps; Zhang ICCV 2019)'), MetricSpec(name='tae_near', direction='lower', best=0.0, worst=0.1, theoretical=False, description='TAE at near range (0.3-1.0m, tabletop manipulation)'), MetricSpec(name='tae_mid', direction='lower', best=0.0, worst=0.1, theoretical=False, description="TAE at mid range (1.0-2.5m, arm's reach)"), MetricSpec(name='tae_far', direction='lower', best=0.0, worst=0.1, theoretical=False, description='TAE at far range (2.5-5.0m, room-scale)'), MetricSpec(name='absrel_near', direction='lower', best=0.0, worst=1.0, theoretical=False, description='AbsRel at near range (0.3-1.0m)'), MetricSpec(name='absrel_mid', direction='lower', best=0.0, worst=1.0, theoretical=False, description='AbsRel at mid range (1.0-2.5m)'), MetricSpec(name='absrel_far', direction='lower', best=0.0, worst=1.0, theoretical=False, description='AbsRel at far range (2.5-5.0m)'), MetricSpec(name='rmse_near', direction='lower', best=0.0, worst=1.0, theoretical=False, description='RMSE at near range (0.3-1.0m)'), MetricSpec(name='rmse_mid', direction='lower', best=0.0, worst=1.0, theoretical=False, description='RMSE at mid range (1.0-2.5m)'), MetricSpec(name='rmse_far', direction='lower', best=0.0, worst=1.0, theoretical=False, description='RMSE at far range (2.5-5.0m)'), MetricSpec(name='delta1_near', direction='higher', best=1.0, worst=0.0, theoretical=True, description='delta1 at near range (0.3-1.0m)'), MetricSpec(name='delta1_mid', direction='higher', best=1.0, worst=0.0, theoretical=True, description='delta1 at mid range (1.0-2.5m)'), MetricSpec(name='delta1_far', direction='higher', best=1.0, worst=0.0, theoretical=True, description='delta1 at far range (2.5-5.0m)'), MetricSpec(name='tgm_near', direction='lower', best=0.0, worst=0.1, theoretical=False, description='TGM at near range (0.3-1.0m, tabletop manipulation)'), MetricSpec(name='tgm_mid', direction='lower', best=0.0, worst=0.1, theoretical=False, description="TGM at mid range (1.0-2.5m, arm's reach)"), MetricSpec(name='tgm_far', direction='lower', best=0.0, worst=0.1, theoretical=False, description='TGM at far range (2.5-5.0m, room-scale)'), MetricSpec(name='tgse_near', direction='lower', best=0.0, worst=0.1, theoretical=False, description='TGSE at near range (0.3-1.0m, tabletop manipulation)'), MetricSpec(name='tgse_mid', direction='lower', best=0.0, worst=0.1, theoretical=False, description="TGSE at mid range (1.0-2.5m, arm's reach)"), MetricSpec(name='tgse_far', direction='lower', best=0.0, worst=0.1, theoretical=False, description='TGSE at far range (2.5-5.0m, room-scale)'))
DETECTION_SPECS = (MetricSpec(name='ap50', direction='higher', best=1.0, worst=0.0, theoretical=True, description='AP at IoU≥0.50'), MetricSpec(name='ap75', direction='higher', best=1.0, worst=0.0, theoretical=True, description='AP at IoU≥0.75'), MetricSpec(name='map', direction='higher', best=1.0, worst=0.0, theoretical=True, description='mean AP averaged over IoU thresholds'), MetricSpec(name='ar', direction='higher', best=1.0, worst=0.0, theoretical=True, description='Average recall'))
TRACKING_SPECS = (MetricSpec(name='mota', direction='higher', best=1.0, worst=0.0, theoretical=True, description='Multi-Object Tracking Accuracy'), MetricSpec(name='idf1', direction='higher', best=1.0, worst=0.0, theoretical=True, description='ID F1 (identity matching F-score)'), MetricSpec(name='hota', direction='higher', best=1.0, worst=0.0, theoretical=True, description='Higher-Order Tracking Accuracy'), MetricSpec(name='id_sw', direction='lower', best=0.0, worst=100.0, theoretical=False, description='Identity switches per scene (count)'))
QA_SPECS = (MetricSpec(name='fuzzy_acc', direction='higher', best=1.0, worst=0.0, theoretical=True, description='Normalised-similarity fuzzy match accuracy'), MetricSpec(name='exact_match', direction='higher', best=1.0, worst=0.0, theoretical=True, description='Strict string-equality accuracy'))
POSE_SPECS = (MetricSpec(name='auc_5deg', direction='higher', best=1.0, worst=0.0, theoretical=True, description='AUC of rotation error CDF up to 5°'), MetricSpec(name='auc_10deg', direction='higher', best=1.0, worst=0.0, theoretical=True, description='AUC of rotation error CDF up to 10°'), MetricSpec(name='auc_20deg', direction='higher', best=1.0, worst=0.0, theoretical=True, description='AUC of rotation error CDF up to 20°'), MetricSpec(name='metric_auc', direction='higher', best=1.0, worst=0.0, theoretical=True, description='Joint rotation × metric-translation AUC'))