File size: 5,346 Bytes
3329027
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
"""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