Skip to main content

ledge_core/
benchmark.rs

1//! Minimal comparison harness shared by examples and future integrations.
2
3use std::{fmt::Write as _, time::Duration};
4
5use crate::{QpProblem, Solution, SolveStatus, Solver, WarmStart};
6
7/// Solver-neutral adapter for benchmark integrations.
8///
9/// Optional OSQP or Clarabel adapters can implement this trait without
10/// changing the instance generator or report format.
11pub trait ComparisonSolver {
12    /// Stable label used in reports.
13    fn name(&self) -> &'static str;
14
15    /// Solves one problem, optionally from a common primal warm start.
16    ///
17    /// # Errors
18    ///
19    /// Returns a human-readable adapter or solver error.
20    fn solve(
21        &mut self,
22        problem: &QpProblem,
23        warm_start: Option<&WarmStart>,
24    ) -> Result<Solution, String>;
25}
26
27impl ComparisonSolver for Solver {
28    fn name(&self) -> &'static str {
29        "ledge"
30    }
31
32    fn solve(
33        &mut self,
34        problem: &QpProblem,
35        warm_start: Option<&WarmStart>,
36    ) -> Result<Solution, String> {
37        Solver::solve(self, problem, warm_start).map_err(|error| error.to_string())
38    }
39}
40
41/// One measured solver/instance result.
42#[derive(Clone, Debug, PartialEq)]
43pub struct BenchmarkRecord {
44    /// Instance label.
45    pub instance: String,
46    /// Solver label.
47    pub solver: String,
48    /// Termination status.
49    pub status: SolveStatus,
50    /// Objective at the returned point.
51    pub objective: f64,
52    /// Primal KKT residual.
53    pub primal_residual: f64,
54    /// Dual KKT residual.
55    pub dual_residual: f64,
56    /// ADMM or adapter iteration count.
57    pub iterations: usize,
58    /// Measured solver-reported duration.
59    pub duration: Duration,
60}
61
62impl BenchmarkRecord {
63    /// Renders one Markdown table row.
64    #[must_use]
65    pub fn markdown_row(&self) -> String {
66        format!(
67            "| {} | {} | {:?} | {:.8e} | {:.3e} | {:.3e} | {} | {:.3} |",
68            self.instance,
69            self.solver,
70            self.status,
71            self.objective,
72            self.primal_residual,
73            self.dual_residual,
74            self.iterations,
75            self.duration.as_secs_f64() * 1_000.0
76        )
77    }
78}
79
80/// Executes the same generated instance through registered adapters.
81#[derive(Default)]
82pub struct BenchmarkRunner {
83    solvers: Vec<Box<dyn ComparisonSolver>>,
84}
85
86impl BenchmarkRunner {
87    /// Creates an empty runner.
88    #[must_use]
89    pub const fn new() -> Self {
90        Self {
91            solvers: Vec::new(),
92        }
93    }
94
95    /// Registers a solver adapter.
96    pub fn add_solver(&mut self, solver: Box<dyn ComparisonSolver>) {
97        self.solvers.push(solver);
98    }
99
100    /// Runs all adapters and returns records or labeled failures.
101    pub fn run(
102        &mut self,
103        instance_name: &str,
104        problem: &QpProblem,
105        warm_start: Option<&WarmStart>,
106    ) -> Vec<Result<BenchmarkRecord, String>> {
107        self.solvers
108            .iter_mut()
109            .map(|solver| {
110                let solver_name = solver.name().to_owned();
111                solver
112                    .solve(problem, warm_start)
113                    .map(|solution| BenchmarkRecord {
114                        instance: instance_name.to_owned(),
115                        solver: solver_name.clone(),
116                        status: solution.status,
117                        objective: solution.objective,
118                        primal_residual: solution.residuals.primal,
119                        dual_residual: solution.residuals.dual,
120                        iterations: solution.iterations,
121                        duration: solution.solve_time,
122                    })
123                    .map_err(|error| {
124                        let mut message = String::new();
125                        let _ = write!(message, "{solver_name}: {error}");
126                        message
127                    })
128            })
129            .collect()
130    }
131
132    /// Markdown header matching [`BenchmarkRecord::markdown_row`].
133    #[must_use]
134    pub const fn markdown_header() -> &'static str {
135        "| instance | solver | status | objective | primal residual | dual residual | iterations | time (ms) |\n\
136         |---|---|---:|---:|---:|---:|---:|---:|"
137    }
138}