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"
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.
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,
... )