1use std::time::Instant;
2
3use clarabel::solver::{DefaultSolver, IPSolver};
4use oximo_core::{Model, ModelKind};
5use oximo_solver::{Solver, SolverError, SolverResult};
6
7use crate::translate::{Problem, build_problem, build_settings, read_result};
8use crate::{ClarabelOptions, NAME};
9
10struct State {
15 solver: DefaultSolver<f64>,
16 problem: Problem,
17}
18
19#[derive(Default)]
44pub struct ClarabelPersistent {
45 state: Option<State>,
46}
47
48impl std::fmt::Debug for ClarabelPersistent {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 f.debug_struct("ClarabelPersistent").field("resident", &self.state.is_some()).finish()
51 }
52}
53
54impl ClarabelPersistent {
55 #[must_use]
57 pub fn new() -> Self {
58 Self::default()
59 }
60
61 pub fn reset(&mut self) {
65 self.state = None;
66 }
67
68 fn solve_resident(
71 &mut self,
72 model: &Model,
73 opts: &ClarabelOptions,
74 ) -> Result<SolverResult, SolverError> {
75 let new = build_problem(model)?;
76 let settings = build_settings(opts);
77
78 let mut updated = false;
82 if let Some(state) = self.state.as_mut() {
83 if state.problem.same_structure(&new)
84 && state.solver.update_settings(settings.clone()).is_ok()
85 && state.solver.update_data(&new.p_mat, &new.q, &new.a_mat, &new.b).is_ok()
86 {
87 updated = true;
88 }
89 }
90
91 if updated {
92 self.state.as_mut().expect("resident on fast path").problem = new;
94 } else {
95 let solver =
96 DefaultSolver::new(&new.p_mat, &new.q, &new.a_mat, &new.b, &new.cones, settings)
97 .map_err(|e| SolverError::Backend(format!("Clarabel setup: {e:?}")))?;
98 self.state = Some(State { solver, problem: new });
99 }
100
101 let state = self.state.as_mut().expect("state present before solve");
102 let started = Instant::now();
103 state.solver.solve();
104 let elapsed = started.elapsed();
105 Ok(read_result(&state.solver, &state.problem.meta, elapsed))
106 }
107}
108
109impl Solver for ClarabelPersistent {
110 type Options = ClarabelOptions;
111
112 fn name(&self) -> &str {
113 NAME
114 }
115
116 fn supports(&self, kind: ModelKind) -> bool {
117 crate::supported(kind)
118 }
119
120 fn solve(
121 &mut self,
122 model: &Model,
123 opts: &ClarabelOptions,
124 ) -> Result<SolverResult, SolverError> {
125 match self.solve_resident(model, opts) {
126 Ok(result) => Ok(result),
127 Err(e) => {
128 self.state = None;
129 Err(e)
130 }
131 }
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use oximo_core::prelude::*;
138 use oximo_solver::{PersistentSolver, Solver, SolverError, TerminationStatus};
139
140 use crate::{Clarabel, ClarabelOptions};
141
142 fn close(a: f64, b: f64) -> bool {
145 (a - b).abs() <= 1e-5 * a.abs().max(b.abs()).max(1.0)
146 }
147
148 #[test]
152 fn persistent_matches_cold_on_objective_sweep() {
153 let m = Model::new("pricing");
154 param!(m, p1 = 0.0);
155 variable!(m, x1 >= 0.0);
156 variable!(m, x2 >= 0.0);
157 constraint!(m, labor, 2.0 * x1 + x2 <= 100.0);
158 constraint!(m, material, x1 + 3.0 * x2 <= 90.0);
159 objective!(m, Max, p1 * x1 + 5.0 * x2);
160
161 let mut solver = Clarabel.persistent();
162 for price in [1.0, 1.6, 2.0, 5.0, 11.0] {
163 p1.set_param_value(price);
164 let s = solver.solve(&m, &ClarabelOptions::default()).unwrap();
165 let c = Clarabel.solve(&m, &ClarabelOptions::default()).unwrap();
166 assert_eq!(s.termination, TerminationStatus::Optimal, "price {price}");
167 assert!(close(s.objective().unwrap(), c.objective().unwrap()), "price {price}");
168 assert!(close(s.value_of(x1).unwrap(), c.value_of(x1).unwrap()), "price {price}");
169 }
170 }
171
172 #[test]
176 fn persistent_matches_cold_on_rhs_sweep() {
177 let m = Model::new("capacity");
178 param!(m, cap = 100.0);
179 variable!(m, x1 >= 0.0);
180 variable!(m, x2 >= 0.0);
181 constraint!(m, labor, 2.0 * x1 + x2 <= cap);
182 constraint!(m, material, x1 + 3.0 * x2 <= 90.0);
183 objective!(m, Max, 3.0 * x1 + 5.0 * x2);
184
185 let mut solver = Clarabel.persistent();
186 for c in [100.0, 60.0, 140.0] {
187 cap.set_param_value(c);
188 let s = solver.solve(&m, &ClarabelOptions::default()).unwrap();
189 let cold = Clarabel.solve(&m, &ClarabelOptions::default()).unwrap();
190 assert_eq!(s.termination, TerminationStatus::Optimal, "cap {c}");
191 assert!(close(s.objective().unwrap(), cold.objective().unwrap()), "cap {c}");
192 }
193 }
194
195 #[test]
198 fn persistent_matches_cold_on_matrix_coeff_sweep() {
199 let m = Model::new("coeff");
200 param!(m, a = 2.0);
201 variable!(m, x1 >= 0.0);
202 variable!(m, x2 >= 0.0);
203 constraint!(m, labor, a * x1 + x2 <= 100.0);
204 objective!(m, Max, 3.0 * x1 + 5.0 * x2);
205
206 let mut solver = Clarabel.persistent();
207 for av in [2.0, 1.0, 4.0] {
208 a.set_param_value(av);
209 let s = solver.solve(&m, &ClarabelOptions::default()).unwrap();
210 let cold = Clarabel.solve(&m, &ClarabelOptions::default()).unwrap();
211 assert_eq!(s.termination, TerminationStatus::Optimal, "a {av}");
212 assert!(close(s.objective().unwrap(), cold.objective().unwrap()), "a {av}");
213 }
214 }
215
216 #[test]
220 fn persistent_rebuilds_on_structural_change() {
221 let m = Model::new("feas");
222 variable!(m, 0.0 <= x <= 10.0);
223 variable!(m, 0.0 <= y <= 10.0);
224 constraint!(m, c, x + y == 5.0);
225 objective!(m, Min, x + 2.0 * y);
226
227 let mut solver = Clarabel.persistent();
228 let r = solver.solve(&m, &ClarabelOptions::default()).unwrap();
229 assert!(r.has_solution(), "termination = {:?}", r.termination);
230
231 m.fix(x, 2.0);
232 let r2 = solver.solve(&m, &ClarabelOptions::default()).unwrap();
233 let cold = Clarabel.solve(&m, &ClarabelOptions::default()).unwrap();
234 assert!(close(r2.value_of(x).unwrap(), 2.0));
235 assert!(close(r2.value_of(y).unwrap(), 3.0));
236 assert!(close(r2.objective().unwrap(), cold.objective().unwrap()));
237 }
238
239 #[test]
242 fn persistent_socp_objective_sweep() {
243 let m = Model::new("socp");
244 param!(m, wt = 1.0);
245 variable!(m, x);
246 variable!(m, y);
247 variable!(m, t >= 0.0);
248 m.fix(t, 1.0);
249 m.add_soc_constraint("disk", [x, y], t); objective!(m, Min, wt * x + y);
251
252 let mut solver = Clarabel.persistent();
253 for wv in [1.0, 2.0, 0.5] {
254 wt.set_param_value(wv);
255 let warm = solver.solve(&m, &ClarabelOptions::default()).unwrap();
256 let cold = Clarabel.solve(&m, &ClarabelOptions::default()).unwrap();
257 assert_eq!(warm.termination, TerminationStatus::Optimal, "wt {wv}");
258 assert!(close(warm.objective().unwrap(), cold.objective().unwrap()), "wt {wv}");
259 }
260 }
261
262 #[test]
263 fn persistent_reset_then_solve_ok() {
264 let m = Model::new("pricing");
265 param!(m, p1 = 0.0);
266 variable!(m, x1 >= 0.0);
267 variable!(m, x2 >= 0.0);
268 constraint!(m, labor, 2.0 * x1 + x2 <= 100.0);
269 objective!(m, Max, p1 * x1 + 5.0 * x2);
270
271 let mut solver = Clarabel.persistent();
272 p1.set_param_value(2.0);
273 let first = solver.solve(&m, &ClarabelOptions::default()).unwrap();
274 assert_eq!(first.termination, TerminationStatus::Optimal);
275
276 solver.reset();
277 let after = solver.solve(&m, &ClarabelOptions::default()).unwrap();
278 assert_eq!(after.termination, TerminationStatus::Optimal);
279 assert!(close(first.objective().unwrap(), after.objective().unwrap()));
280 }
281
282 #[test]
283 fn persistent_unsupported_kind_errors_and_clears() {
284 let m = Model::new("milp");
285 variable!(m, 0.0 <= x <= 5.0, Int);
286 objective!(m, Min, x);
287 let mut solver = Clarabel.persistent();
288 let err = solver.solve(&m, &ClarabelOptions::default()).unwrap_err();
289 assert!(matches!(err, SolverError::UnsupportedKind(ModelKind::MILP)));
290 }
291}