use std::time::Instant;
use clarabel::solver::{DefaultSolver, IPSolver};
use oximo_core::{Model, ModelKind};
use oximo_solver::{Solver, SolverError, SolverResult};
use crate::translate::{Problem, build_problem, build_settings, read_result};
use crate::{ClarabelOptions, NAME};
struct State {
solver: DefaultSolver<f64>,
problem: Problem,
}
#[derive(Default)]
pub struct ClarabelPersistent {
state: Option<State>,
}
impl std::fmt::Debug for ClarabelPersistent {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ClarabelPersistent").field("resident", &self.state.is_some()).finish()
}
}
impl ClarabelPersistent {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn reset(&mut self) {
self.state = None;
}
fn solve_resident(
&mut self,
model: &Model,
opts: &ClarabelOptions,
) -> Result<SolverResult, SolverError> {
let new = build_problem(model)?;
let settings = build_settings(opts);
let mut updated = false;
if let Some(state) = self.state.as_mut() {
if state.problem.same_structure(&new)
&& state.solver.update_settings(settings.clone()).is_ok()
&& state.solver.update_data(&new.p_mat, &new.q, &new.a_mat, &new.b).is_ok()
{
updated = true;
}
}
if updated {
self.state.as_mut().expect("resident on fast path").problem = new;
} else {
let solver =
DefaultSolver::new(&new.p_mat, &new.q, &new.a_mat, &new.b, &new.cones, settings)
.map_err(|e| SolverError::Backend(format!("Clarabel setup: {e:?}")))?;
self.state = Some(State { solver, problem: new });
}
let state = self.state.as_mut().expect("state present before solve");
let started = Instant::now();
state.solver.solve();
let elapsed = started.elapsed();
Ok(read_result(&state.solver, &state.problem.meta, elapsed))
}
}
impl Solver for ClarabelPersistent {
type Options = ClarabelOptions;
fn name(&self) -> &str {
NAME
}
fn supports(&self, kind: ModelKind) -> bool {
crate::supported(kind)
}
fn solve(
&mut self,
model: &Model,
opts: &ClarabelOptions,
) -> Result<SolverResult, SolverError> {
match self.solve_resident(model, opts) {
Ok(result) => Ok(result),
Err(e) => {
self.state = None;
Err(e)
}
}
}
}
#[cfg(test)]
mod tests {
use oximo_core::prelude::*;
use oximo_solver::{PersistentSolver, Solver, SolverError, TerminationStatus};
use crate::{Clarabel, ClarabelOptions};
fn close(a: f64, b: f64) -> bool {
(a - b).abs() <= 1e-5 * a.abs().max(b.abs()).max(1.0)
}
#[test]
fn persistent_matches_cold_on_objective_sweep() {
let m = Model::new("pricing");
param!(m, p1 = 0.0);
variable!(m, x1 >= 0.0);
variable!(m, x2 >= 0.0);
constraint!(m, labor, 2.0 * x1 + x2 <= 100.0);
constraint!(m, material, x1 + 3.0 * x2 <= 90.0);
objective!(m, Max, p1 * x1 + 5.0 * x2);
let mut solver = Clarabel.persistent();
for price in [1.0, 1.6, 2.0, 5.0, 11.0] {
p1.set_param_value(price);
let s = solver.solve(&m, &ClarabelOptions::default()).unwrap();
let c = Clarabel.solve(&m, &ClarabelOptions::default()).unwrap();
assert_eq!(s.termination, TerminationStatus::Optimal, "price {price}");
assert!(close(s.objective().unwrap(), c.objective().unwrap()), "price {price}");
assert!(close(s.value_of(x1).unwrap(), c.value_of(x1).unwrap()), "price {price}");
}
}
#[test]
fn persistent_matches_cold_on_rhs_sweep() {
let m = Model::new("capacity");
param!(m, cap = 100.0);
variable!(m, x1 >= 0.0);
variable!(m, x2 >= 0.0);
constraint!(m, labor, 2.0 * x1 + x2 <= cap);
constraint!(m, material, x1 + 3.0 * x2 <= 90.0);
objective!(m, Max, 3.0 * x1 + 5.0 * x2);
let mut solver = Clarabel.persistent();
for c in [100.0, 60.0, 140.0] {
cap.set_param_value(c);
let s = solver.solve(&m, &ClarabelOptions::default()).unwrap();
let cold = Clarabel.solve(&m, &ClarabelOptions::default()).unwrap();
assert_eq!(s.termination, TerminationStatus::Optimal, "cap {c}");
assert!(close(s.objective().unwrap(), cold.objective().unwrap()), "cap {c}");
}
}
#[test]
fn persistent_matches_cold_on_matrix_coeff_sweep() {
let m = Model::new("coeff");
param!(m, a = 2.0);
variable!(m, x1 >= 0.0);
variable!(m, x2 >= 0.0);
constraint!(m, labor, a * x1 + x2 <= 100.0);
objective!(m, Max, 3.0 * x1 + 5.0 * x2);
let mut solver = Clarabel.persistent();
for av in [2.0, 1.0, 4.0] {
a.set_param_value(av);
let s = solver.solve(&m, &ClarabelOptions::default()).unwrap();
let cold = Clarabel.solve(&m, &ClarabelOptions::default()).unwrap();
assert_eq!(s.termination, TerminationStatus::Optimal, "a {av}");
assert!(close(s.objective().unwrap(), cold.objective().unwrap()), "a {av}");
}
}
#[test]
fn persistent_rebuilds_on_structural_change() {
let m = Model::new("feas");
variable!(m, 0.0 <= x <= 10.0);
variable!(m, 0.0 <= y <= 10.0);
constraint!(m, c, x + y == 5.0);
objective!(m, Min, x + 2.0 * y);
let mut solver = Clarabel.persistent();
let r = solver.solve(&m, &ClarabelOptions::default()).unwrap();
assert!(r.has_solution(), "termination = {:?}", r.termination);
m.fix(x, 2.0);
let r2 = solver.solve(&m, &ClarabelOptions::default()).unwrap();
let cold = Clarabel.solve(&m, &ClarabelOptions::default()).unwrap();
assert!(close(r2.value_of(x).unwrap(), 2.0));
assert!(close(r2.value_of(y).unwrap(), 3.0));
assert!(close(r2.objective().unwrap(), cold.objective().unwrap()));
}
#[test]
fn persistent_socp_objective_sweep() {
let m = Model::new("socp");
param!(m, wt = 1.0);
variable!(m, x);
variable!(m, y);
variable!(m, t >= 0.0);
m.fix(t, 1.0);
m.add_soc_constraint("disk", [x, y], t); objective!(m, Min, wt * x + y);
let mut solver = Clarabel.persistent();
for wv in [1.0, 2.0, 0.5] {
wt.set_param_value(wv);
let warm = solver.solve(&m, &ClarabelOptions::default()).unwrap();
let cold = Clarabel.solve(&m, &ClarabelOptions::default()).unwrap();
assert_eq!(warm.termination, TerminationStatus::Optimal, "wt {wv}");
assert!(close(warm.objective().unwrap(), cold.objective().unwrap()), "wt {wv}");
}
}
#[test]
fn persistent_reset_then_solve_ok() {
let m = Model::new("pricing");
param!(m, p1 = 0.0);
variable!(m, x1 >= 0.0);
variable!(m, x2 >= 0.0);
constraint!(m, labor, 2.0 * x1 + x2 <= 100.0);
objective!(m, Max, p1 * x1 + 5.0 * x2);
let mut solver = Clarabel.persistent();
p1.set_param_value(2.0);
let first = solver.solve(&m, &ClarabelOptions::default()).unwrap();
assert_eq!(first.termination, TerminationStatus::Optimal);
solver.reset();
let after = solver.solve(&m, &ClarabelOptions::default()).unwrap();
assert_eq!(after.termination, TerminationStatus::Optimal);
assert!(close(first.objective().unwrap(), after.objective().unwrap()));
}
#[test]
fn persistent_unsupported_kind_errors_and_clears() {
let m = Model::new("milp");
variable!(m, 0.0 <= x <= 5.0, Int);
objective!(m, Min, x);
let mut solver = Clarabel.persistent();
let err = solver.solve(&m, &ClarabelOptions::default()).unwrap_err();
assert!(matches!(err, SolverError::UnsupportedKind(ModelKind::MILP)));
}
}