rpx_benchmark.reports

Result serialisation: JSON + markdown summary for benchmark runs.

  1"""Result serialisation: JSON + markdown summary for benchmark runs."""
  2
  3from __future__ import annotations
  4
  5import json
  6from dataclasses import asdict, is_dataclass
  7from pathlib import Path
  8from typing import Any, Dict
  9
 10from .deployment import DeploymentReadinessReport
 11from .evaluators import BenchmarkResult
 12
 13
 14def _to_jsonable(obj: Any) -> Any:
 15    if is_dataclass(obj):
 16        return _to_jsonable(asdict(obj))
 17    if isinstance(obj, dict):
 18        return {k: _to_jsonable(v) for k, v in obj.items()}
 19    if isinstance(obj, (list, tuple)):
 20        return [_to_jsonable(v) for v in obj]
 21    if hasattr(obj, "value"):  # Enum
 22        return obj.value
 23    return obj
 24
 25
 26def write_json(
 27    path: str | Path,
 28    *,
 29    task: str,
 30    model_name: str,
 31    split: str,
 32    repo_id: str,
 33    result: BenchmarkResult,
 34    dr_report: DeploymentReadinessReport | None = None,
 35    extra: Dict[str, Any] | None = None,
 36) -> Path:
 37    """Serialise a benchmark result + deployment report to JSON.
 38
 39    Parameters
 40    ----------
 41    path : str or Path
 42        Output file path. Parent directories are created if missing.
 43    task : str
 44        Task name string (e.g. ``"monocular_depth"``).
 45    model_name : str
 46        Display name of the model under test.
 47    split : str
 48        ESD difficulty split (``"easy"``, ``"medium"``, ``"hard"``).
 49    repo_id : str
 50        HuggingFace dataset repo id the samples came from.
 51    result : BenchmarkResult
 52        Per-sample + aggregated metric container returned by
 53        :class:`~rpx_benchmark.runner.BenchmarkRunner`.
 54    dr_report : DeploymentReadinessReport, optional
 55        Weighted Phase Score, STR, TS, efficiency metadata. Omitted
 56        when ``None``.
 57    extra : dict, optional
 58        Arbitrary free-form extra fields to embed under the ``extra``
 59        key of the payload. Useful for run-specific metadata a caller
 60        wants preserved alongside the standard keys.
 61
 62    Returns
 63    -------
 64    Path
 65        The resolved output path, for chaining.
 66
 67    Notes
 68    -----
 69    Dataclasses are converted via :func:`dataclasses.asdict`, enums
 70    are lowered to their ``.value``, and unknown objects pass through
 71    unchanged. The output is pretty-printed with indent=2 for
 72    diff-friendliness.
 73    """
 74    path = Path(path)
 75    path.parent.mkdir(parents=True, exist_ok=True)
 76    payload: Dict[str, Any] = {
 77        "task": task,
 78        "model": model_name,
 79        "split": split,
 80        "repo_id": repo_id,
 81        "num_samples": result.num_samples,
 82        "aggregated": result.aggregated,
 83        "per_sample": result.per_sample,
 84    }
 85    if dr_report is not None:
 86        # Split the report into the three top-level RPX axes (no
 87        # composite score; see benchmark/README.md for the policy).
 88        dr_json = _to_jsonable(dr_report)
 89        payload["robustness"] = {
 90            "weighted_phase_score": dr_json.get("weighted_phase_score"),
 91            "temporal_stability":   dr_json.get("temporal_stability"),
 92            "state_transition":     dr_json.get("state_transition"),
 93            "geometric_coherence":  dr_json.get("geometric_coherence"),
 94        }
 95        payload["compute_cost"] = {
 96            "params_m":              dr_json.get("params_m"),
 97            "flops_g":               dr_json.get("flops_g"),
 98            "macs_g":                dr_json.get("macs_g"),
 99            "actmem_gb_fp16":        dr_json.get("actmem_gb_fp16"),
100            "memory_traffic_gb":     dr_json.get("memory_traffic_gb"),
101            "arithmetic_intensity":  dr_json.get("arithmetic_intensity"),
102            "roofline":              dr_json.get("roofline"),
103            "latency_ms_per_sample": dr_json.get("latency_ms_per_sample"),
104            "peak_memory_mb":        dr_json.get("peak_memory_mb"),
105            "system_card":           dr_json.get("system_card"),
106            "operating_point":       dr_json.get("operating_point"),
107        }
108    if extra:
109        payload["extra"] = extra
110    with path.open("w", encoding="utf-8") as f:
111        json.dump(_to_jsonable(payload), f, indent=2)
112    return path
113
114
115def format_markdown_summary(
116    *,
117    task: str,
118    model_name: str,
119    split: str,
120    repo_id: str,
121    result: BenchmarkResult,
122    dr_report: DeploymentReadinessReport | None = None,
123) -> str:
124    """Render a benchmark result as a human-readable markdown report.
125
126    Parameters
127    ----------
128    task : str
129    model_name : str
130    split : str
131    repo_id : str
132    result : BenchmarkResult
133    dr_report : DeploymentReadinessReport, optional
134        When provided, the output includes the Weighted Phase Score
135        table, State-Transition Robustness deltas, Temporal Stability
136        score, and an Efficiency table (params, FLOPs, latency).
137
138    Returns
139    -------
140    str
141        A Markdown-formatted report. Matches the terminal UI tables
142        the CLI prints so on-disk reports and terminal output stay in
143        sync.
144
145    Examples
146    --------
147    >>> from rpx_benchmark.reports import format_markdown_summary  # doctest: +SKIP
148    >>> md = format_markdown_summary(                              # doctest: +SKIP
149    ...     task="monocular_depth", model_name="depth_pro",
150    ...     split="hard", repo_id="IRVLUTD/RPX",
151    ...     result=result, dr_report=report,
152    ... )
153    """
154    lines = [
155        f"# RPX benchmark — {task}",
156        "",
157        f"- **Model:** `{model_name}`",
158        f"- **Split:** `{split}`",
159        f"- **Repo:** `{repo_id}`",
160        f"- **Samples:** {result.num_samples}",
161        "",
162        "## Aggregated metrics",
163        "",
164        "| metric | value |",
165        "|---|---|",
166    ]
167    for k, v in result.aggregated.items():
168        lines.append(f"| {k} | {v:.4f} |")
169
170    if dr_report is not None and dr_report.weighted_phase_score is not None:
171        wps = dr_report.weighted_phase_score
172        lines += [
173            "",
174            "## Weighted Phase Score",
175            "",
176            "| phase | score |",
177            "|---|---|",
178            f"| clutter | {wps.s_clutter:.4f} |",
179            f"| interaction | {wps.s_interaction:.4f} |",
180            f"| clean | {wps.s_clean:.4f} |",
181            f"| **overall** | **{wps.s_overall:.4f}** |",
182            f"| Δ interaction (S_I − S_C) | {wps.delta_int:+.4f} |",
183            f"| Δ recovery    (S_L − S_I) | {wps.delta_rec:+.4f} |",
184        ]
185        if dr_report.state_transition is not None:
186            st = dr_report.state_transition
187            lines += [
188                "",
189                f"- **STR C→I (interaction drop):** {st.str_c_to_i:+.4f}",
190                f"- **STR I→L (recovery):**         {st.str_i_to_l:+.4f}",
191            ]
192        if dr_report.temporal_stability is not None:
193            lines += [
194                f"- **Temporal stability (TS):** {dr_report.temporal_stability.ts_score:.4f}",
195            ]
196
197    if dr_report is not None:
198        eff_rows = []
199        if dr_report.params_m is not None:
200            eff_rows.append(("params (M)", f"{dr_report.params_m:.2f}"))
201        if dr_report.flops_g is not None:
202            eff_rows.append(("FLOPs (G)", f"{dr_report.flops_g:.2f}"))
203        if dr_report.latency_ms_per_sample is not None:
204            eff_rows.append(("latency (ms/sample)", f"{dr_report.latency_ms_per_sample:.1f}"))
205        if dr_report.peak_memory_mb is not None:
206            eff_rows.append(("peak memory (MB)", f"{dr_report.peak_memory_mb:.1f}"))
207        if eff_rows:
208            lines += ["", "## Efficiency", "", "| metric | value |", "|---|---|"]
209            for k, v in eff_rows:
210                lines.append(f"| {k} | {v} |")
211
212    return "\n".join(lines) + "\n"
def write_json( path: str | pathlib.Path, *, task: str, model_name: str, split: str, repo_id: str, result: rpx_benchmark.metrics.registry.BenchmarkResult, dr_report: rpx_benchmark.deployment.DeploymentReadinessReport | None = None, extra: Optional[Dict[str, Any]] = None) -> pathlib.Path:
 27def write_json(
 28    path: str | Path,
 29    *,
 30    task: str,
 31    model_name: str,
 32    split: str,
 33    repo_id: str,
 34    result: BenchmarkResult,
 35    dr_report: DeploymentReadinessReport | None = None,
 36    extra: Dict[str, Any] | None = None,
 37) -> Path:
 38    """Serialise a benchmark result + deployment report to JSON.
 39
 40    Parameters
 41    ----------
 42    path : str or Path
 43        Output file path. Parent directories are created if missing.
 44    task : str
 45        Task name string (e.g. ``"monocular_depth"``).
 46    model_name : str
 47        Display name of the model under test.
 48    split : str
 49        ESD difficulty split (``"easy"``, ``"medium"``, ``"hard"``).
 50    repo_id : str
 51        HuggingFace dataset repo id the samples came from.
 52    result : BenchmarkResult
 53        Per-sample + aggregated metric container returned by
 54        :class:`~rpx_benchmark.runner.BenchmarkRunner`.
 55    dr_report : DeploymentReadinessReport, optional
 56        Weighted Phase Score, STR, TS, efficiency metadata. Omitted
 57        when ``None``.
 58    extra : dict, optional
 59        Arbitrary free-form extra fields to embed under the ``extra``
 60        key of the payload. Useful for run-specific metadata a caller
 61        wants preserved alongside the standard keys.
 62
 63    Returns
 64    -------
 65    Path
 66        The resolved output path, for chaining.
 67
 68    Notes
 69    -----
 70    Dataclasses are converted via :func:`dataclasses.asdict`, enums
 71    are lowered to their ``.value``, and unknown objects pass through
 72    unchanged. The output is pretty-printed with indent=2 for
 73    diff-friendliness.
 74    """
 75    path = Path(path)
 76    path.parent.mkdir(parents=True, exist_ok=True)
 77    payload: Dict[str, Any] = {
 78        "task": task,
 79        "model": model_name,
 80        "split": split,
 81        "repo_id": repo_id,
 82        "num_samples": result.num_samples,
 83        "aggregated": result.aggregated,
 84        "per_sample": result.per_sample,
 85    }
 86    if dr_report is not None:
 87        # Split the report into the three top-level RPX axes (no
 88        # composite score; see benchmark/README.md for the policy).
 89        dr_json = _to_jsonable(dr_report)
 90        payload["robustness"] = {
 91            "weighted_phase_score": dr_json.get("weighted_phase_score"),
 92            "temporal_stability":   dr_json.get("temporal_stability"),
 93            "state_transition":     dr_json.get("state_transition"),
 94            "geometric_coherence":  dr_json.get("geometric_coherence"),
 95        }
 96        payload["compute_cost"] = {
 97            "params_m":              dr_json.get("params_m"),
 98            "flops_g":               dr_json.get("flops_g"),
 99            "macs_g":                dr_json.get("macs_g"),
100            "actmem_gb_fp16":        dr_json.get("actmem_gb_fp16"),
101            "memory_traffic_gb":     dr_json.get("memory_traffic_gb"),
102            "arithmetic_intensity":  dr_json.get("arithmetic_intensity"),
103            "roofline":              dr_json.get("roofline"),
104            "latency_ms_per_sample": dr_json.get("latency_ms_per_sample"),
105            "peak_memory_mb":        dr_json.get("peak_memory_mb"),
106            "system_card":           dr_json.get("system_card"),
107            "operating_point":       dr_json.get("operating_point"),
108        }
109    if extra:
110        payload["extra"] = extra
111    with path.open("w", encoding="utf-8") as f:
112        json.dump(_to_jsonable(payload), f, indent=2)
113    return path

Serialise a benchmark result + deployment report to JSON.

Parameters

path : str or Path Output file path. Parent directories are created if missing. task : str Task name string (e.g. "monocular_depth"). model_name : str Display name of the model under test. split : str ESD difficulty split ("easy", "medium", "hard"). repo_id : str HuggingFace dataset repo id the samples came from. result : BenchmarkResult Per-sample + aggregated metric container returned by ~rpx_benchmark.runner.BenchmarkRunner. dr_report : DeploymentReadinessReport, optional Weighted Phase Score, STR, TS, efficiency metadata. Omitted when None. extra : dict, optional Arbitrary free-form extra fields to embed under the extra key of the payload. Useful for run-specific metadata a caller wants preserved alongside the standard keys.

Returns

Path The resolved output path, for chaining.

Notes

Dataclasses are converted via dataclasses.asdict(), enums are lowered to their .value, and unknown objects pass through unchanged. The output is pretty-printed with indent=2 for diff-friendliness.

def format_markdown_summary( *, task: str, model_name: str, split: str, repo_id: str, result: rpx_benchmark.metrics.registry.BenchmarkResult, dr_report: rpx_benchmark.deployment.DeploymentReadinessReport | None = None) -> str:
116def format_markdown_summary(
117    *,
118    task: str,
119    model_name: str,
120    split: str,
121    repo_id: str,
122    result: BenchmarkResult,
123    dr_report: DeploymentReadinessReport | None = None,
124) -> str:
125    """Render a benchmark result as a human-readable markdown report.
126
127    Parameters
128    ----------
129    task : str
130    model_name : str
131    split : str
132    repo_id : str
133    result : BenchmarkResult
134    dr_report : DeploymentReadinessReport, optional
135        When provided, the output includes the Weighted Phase Score
136        table, State-Transition Robustness deltas, Temporal Stability
137        score, and an Efficiency table (params, FLOPs, latency).
138
139    Returns
140    -------
141    str
142        A Markdown-formatted report. Matches the terminal UI tables
143        the CLI prints so on-disk reports and terminal output stay in
144        sync.
145
146    Examples
147    --------
148    >>> from rpx_benchmark.reports import format_markdown_summary  # doctest: +SKIP
149    >>> md = format_markdown_summary(                              # doctest: +SKIP
150    ...     task="monocular_depth", model_name="depth_pro",
151    ...     split="hard", repo_id="IRVLUTD/RPX",
152    ...     result=result, dr_report=report,
153    ... )
154    """
155    lines = [
156        f"# RPX benchmark — {task}",
157        "",
158        f"- **Model:** `{model_name}`",
159        f"- **Split:** `{split}`",
160        f"- **Repo:** `{repo_id}`",
161        f"- **Samples:** {result.num_samples}",
162        "",
163        "## Aggregated metrics",
164        "",
165        "| metric | value |",
166        "|---|---|",
167    ]
168    for k, v in result.aggregated.items():
169        lines.append(f"| {k} | {v:.4f} |")
170
171    if dr_report is not None and dr_report.weighted_phase_score is not None:
172        wps = dr_report.weighted_phase_score
173        lines += [
174            "",
175            "## Weighted Phase Score",
176            "",
177            "| phase | score |",
178            "|---|---|",
179            f"| clutter | {wps.s_clutter:.4f} |",
180            f"| interaction | {wps.s_interaction:.4f} |",
181            f"| clean | {wps.s_clean:.4f} |",
182            f"| **overall** | **{wps.s_overall:.4f}** |",
183            f"| Δ interaction (S_I − S_C) | {wps.delta_int:+.4f} |",
184            f"| Δ recovery    (S_L − S_I) | {wps.delta_rec:+.4f} |",
185        ]
186        if dr_report.state_transition is not None:
187            st = dr_report.state_transition
188            lines += [
189                "",
190                f"- **STR C→I (interaction drop):** {st.str_c_to_i:+.4f}",
191                f"- **STR I→L (recovery):**         {st.str_i_to_l:+.4f}",
192            ]
193        if dr_report.temporal_stability is not None:
194            lines += [
195                f"- **Temporal stability (TS):** {dr_report.temporal_stability.ts_score:.4f}",
196            ]
197
198    if dr_report is not None:
199        eff_rows = []
200        if dr_report.params_m is not None:
201            eff_rows.append(("params (M)", f"{dr_report.params_m:.2f}"))
202        if dr_report.flops_g is not None:
203            eff_rows.append(("FLOPs (G)", f"{dr_report.flops_g:.2f}"))
204        if dr_report.latency_ms_per_sample is not None:
205            eff_rows.append(("latency (ms/sample)", f"{dr_report.latency_ms_per_sample:.1f}"))
206        if dr_report.peak_memory_mb is not None:
207            eff_rows.append(("peak memory (MB)", f"{dr_report.peak_memory_mb:.1f}"))
208        if eff_rows:
209            lines += ["", "## Efficiency", "", "| metric | value |", "|---|---|"]
210            for k, v in eff_rows:
211                lines.append(f"| {k} | {v} |")
212
213    return "\n".join(lines) + "\n"

Render a benchmark result as a human-readable markdown report.

Parameters

task : str model_name : str split : str repo_id : str result : BenchmarkResult dr_report : DeploymentReadinessReport, optional When provided, the output includes the Weighted Phase Score table, State-Transition Robustness deltas, Temporal Stability score, and an Efficiency table (params, FLOPs, latency).

Returns

str A Markdown-formatted report. Matches the terminal UI tables the CLI prints so on-disk reports and terminal output stay in sync.

Examples

>>> from rpx_benchmark.reports import format_markdown_summary  # doctest: +SKIP
>>> md = format_markdown_summary(                              # doctest: +SKIP
...     task="monocular_depth", model_name="depth_pro",
...     split="hard", repo_id="IRVLUTD/RPX",
...     result=result, dr_report=report,
... )