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
//! Minimal comparison harness shared by examples and future integrations.
use std::{fmt::Write as _, time::Duration};
use crate::{QpProblem, Solution, SolveStatus, Solver, WarmStart};
/// Solver-neutral adapter for benchmark integrations.
///
/// Optional OSQP or Clarabel adapters can implement this trait without
/// changing the instance generator or report format.
pub trait ComparisonSolver {
/// Stable label used in reports.
fn name(&self) -> &'static str;
/// Solves one problem, optionally from a common primal warm start.
///
/// # Errors
///
/// Returns a human-readable adapter or solver error.
fn solve(
&mut self,
problem: &QpProblem,
warm_start: Option<&WarmStart>,
) -> Result<Solution, String>;
}
impl ComparisonSolver for Solver {
fn name(&self) -> &'static str {
"ledge"
}
fn solve(
&mut self,
problem: &QpProblem,
warm_start: Option<&WarmStart>,
) -> Result<Solution, String> {
Solver::solve(self, problem, warm_start).map_err(|error| error.to_string())
}
}
/// One measured solver/instance result.
#[derive(Clone, Debug, PartialEq)]
pub struct BenchmarkRecord {
/// Instance label.
pub instance: String,
/// Solver label.
pub solver: String,
/// Termination status.
pub status: SolveStatus,
/// Objective at the returned point.
pub objective: f64,
/// Primal KKT residual.
pub primal_residual: f64,
/// Dual KKT residual.
pub dual_residual: f64,
/// ADMM or adapter iteration count.
pub iterations: usize,
/// Measured solver-reported duration.
pub duration: Duration,
}
impl BenchmarkRecord {
/// Renders one Markdown table row.
#[must_use]
pub fn markdown_row(&self) -> String {
format!(
"| {} | {} | {:?} | {:.8e} | {:.3e} | {:.3e} | {} | {:.3} |",
self.instance,
self.solver,
self.status,
self.objective,
self.primal_residual,
self.dual_residual,
self.iterations,
self.duration.as_secs_f64() * 1_000.0
)
}
}
/// Executes the same generated instance through registered adapters.
#[derive(Default)]
pub struct BenchmarkRunner {
solvers: Vec<Box<dyn ComparisonSolver>>,
}
impl BenchmarkRunner {
/// Creates an empty runner.
#[must_use]
pub const fn new() -> Self {
Self {
solvers: Vec::new(),
}
}
/// Registers a solver adapter.
pub fn add_solver(&mut self, solver: Box<dyn ComparisonSolver>) {
self.solvers.push(solver);
}
/// Runs all adapters and returns records or labeled failures.
pub fn run(
&mut self,
instance_name: &str,
problem: &QpProblem,
warm_start: Option<&WarmStart>,
) -> Vec<Result<BenchmarkRecord, String>> {
self.solvers
.iter_mut()
.map(|solver| {
let solver_name = solver.name().to_owned();
solver
.solve(problem, warm_start)
.map(|solution| BenchmarkRecord {
instance: instance_name.to_owned(),
solver: solver_name.clone(),
status: solution.status,
objective: solution.objective,
primal_residual: solution.residuals.primal,
dual_residual: solution.residuals.dual,
iterations: solution.iterations,
duration: solution.solve_time,
})
.map_err(|error| {
let mut message = String::new();
let _ = write!(message, "{solver_name}: {error}");
message
})
})
.collect()
}
/// Markdown header matching [`BenchmarkRecord::markdown_row`].
#[must_use]
pub const fn markdown_header() -> &'static str {
"| instance | solver | status | objective | primal residual | dual residual | iterations | time (ms) |\n\
|---|---|---:|---:|---:|---:|---:|---:|"
}
}