| """Plotly visualizations for benchmark results.""" |
|
|
| from __future__ import annotations |
|
|
| from typing import Any |
|
|
| import plotly.graph_objects as go |
| from plotly.subplots import make_subplots |
|
|
| from solvbench.constants import SOLVERS |
| from solvbench.models import BenchmarkRun, SolverResult |
|
|
|
|
| def build_solver_comparison_chart(results: list[SolverResult], metric: str = "total_solving_time") -> go.Figure: |
| labels = [SOLVERS.get(r.solver_id, {}).get("label", r.solver_id) for r in results] |
| values = [getattr(r.metrics, metric, 0) for r in results] |
| colors = ["#6366f1", "#10b981", "#f59e0b", "#ef4444", "#8b5cf6", "#06b6d4"] |
| fig = go.Figure(go.Bar( |
| x=labels, y=values, |
| marker_color=colors[: len(labels)], |
| text=[f"{v:.3f}" for v in values], |
| textposition="outside", |
| )) |
| fig.update_layout( |
| title=f"Solver Comparison — {metric.replace('_', ' ').title()}", |
| yaxis_title=metric.replace("_", " ").title(), |
| template="plotly_white", |
| height=400, |
| ) |
| return fig |
|
|
|
|
| def build_radar_chart(results: list[SolverResult]) -> go.Figure: |
| categories = ["Quality", "Speed", "Gap", "Stability", "Scalability"] |
| fig = go.Figure() |
| colors = ["#6366f1", "#10b981", "#f59e0b", "#ef4444", "#8b5cf6", "#06b6d4"] |
| for i, r in enumerate(results): |
| if not r.metrics.feasible: |
| continue |
| speed = max(0, 1 - r.metrics.total_solving_time / 30) |
| gap_score = max(0, 1 - r.metrics.optimality_gap / 20) |
| values = [ |
| r.metrics.solution_quality, |
| speed, |
| gap_score, |
| r.metrics.stability_score, |
| r.metrics.scalability_score, |
| ] |
| label = SOLVERS.get(r.solver_id, {}).get("label", r.solver_id) |
| fig.add_trace(go.Scatterpolar( |
| r=values + [values[0]], |
| theta=categories + [categories[0]], |
| fill="toself", |
| name=label, |
| line_color=colors[i % len(colors)], |
| opacity=0.7, |
| )) |
| fig.update_layout( |
| polar=dict(radialaxis=dict(visible=True, range=[0, 1])), |
| title="Multi-Metric Solver Radar", |
| template="plotly_white", |
| height=450, |
| ) |
| return fig |
|
|
|
|
| def build_benchmark_heatmap(rows: list[dict[str, Any]]) -> go.Figure: |
| if not rows: |
| return go.Figure() |
| problems = sorted({r["problem_type"] for r in rows}) |
| solvers = sorted({r["solver_id"] for r in rows}) |
| z = [] |
| for p in problems: |
| row = [] |
| for s in solvers: |
| match = [r for r in rows if r["problem_type"] == p and r["solver_id"] == s] |
| row.append(match[0]["solve_time_sec"] if match else 0) |
| z.append(row) |
| fig = go.Figure(go.Heatmap( |
| z=z, x=[SOLVERS.get(s, {}).get("label", s) for s in solvers], |
| y=problems, colorscale="Viridis", |
| )) |
| fig.update_layout(title="Solve Time Heatmap (sec)", template="plotly_white", height=500) |
| return fig |
|
|
|
|
| def build_scalability_chart(rows: list[dict[str, Any]]) -> go.Figure: |
| fig = go.Figure() |
| colors = {"small": "#10b981", "medium": "#6366f1", "large": "#ef4444"} |
| for solver_id in sorted({r["solver_id"] for r in rows}): |
| for size in ("small", "medium", "large"): |
| subset = [r for r in rows if r["solver_id"] == solver_id and r.get("size") == size] |
| if not subset: |
| continue |
| avg_time = sum(r["solve_time_sec"] for r in subset) / len(subset) |
| label = SOLVERS.get(solver_id, {}).get("label", solver_id) |
| fig.add_trace(go.Bar( |
| name=f"{label} ({size})", |
| x=[size], y=[avg_time], |
| marker_color=colors.get(size, "#888"), |
| )) |
| fig.update_layout( |
| title="Scalability by Instance Size", |
| xaxis_title="Size", yaxis_title="Avg Solve Time (sec)", |
| barmode="group", template="plotly_white", height=400, |
| ) |
| return fig |
|
|
|
|
| def build_metamodel_chart(rankings: list[dict[str, Any]]) -> go.Figure: |
| labels = [r["solver_label"] for r in rankings] |
| scores = [r["score"] for r in rankings] |
| fig = go.Figure(go.Bar( |
| x=labels, y=scores, |
| marker_color=["#6366f1" if i == 0 else "#94a3b8" for i in range(len(labels))], |
| text=[f"{s:.3f}" for s in scores], |
| textposition="outside", |
| )) |
| fig.update_layout(title="Meta-Model Solver Rankings", template="plotly_white", height=380) |
| return fig |
|
|
|
|
| def build_gap_timeline(results: list[SolverResult]) -> go.Figure: |
| fig = make_subplots(specs=[[{"secondary_y": True}]]) |
| labels = [SOLVERS.get(r.solver_id, {}).get("label", r.solver_id) for r in results] |
| fig.add_trace(go.Bar( |
| x=labels, |
| y=[r.metrics.time_to_first_feasible for r in results], |
| name="Time to First Feasible", |
| marker_color="#10b981", |
| ), secondary_y=False) |
| fig.add_trace(go.Scatter( |
| x=labels, |
| y=[r.metrics.time_to_best for r in results], |
| name="Time to Best", |
| mode="lines+markers", |
| line=dict(color="#6366f1", width=3), |
| ), secondary_y=True) |
| fig.update_layout(title="Solution Progress Timeline", template="plotly_white", height=400) |
| fig.update_yaxes(title_text="First Feasible (sec)", secondary_y=False) |
| fig.update_yaxes(title_text="Best Solution (sec)", secondary_y=True) |
| return fig |
|
|