good_lp/solvers/
clarabel.rs1use crate::affine_expression_trait::IntoAffineExpression;
4use crate::expression::LinearExpression;
5use crate::variable::UnsolvedProblem;
6use crate::{
7 constraint::ConstraintReference,
8 solvers::{ObjectiveDirection, ResolutionError, Solution, SolverModel},
9 SolutionStatus,
10};
11use crate::{Constraint, DualValues, SolutionWithDual, Variable};
12
13use clarabel::algebra::CscMatrix;
14use clarabel::solver::implementations::default::DefaultSettingsBuilder;
15use clarabel::solver::SupportedConeT::{self, *};
16use clarabel::solver::{DefaultSolution, SolverStatus};
17use clarabel::solver::{DefaultSolver, IPSolver};
18
19pub fn clarabel(to_solve: UnsolvedProblem) -> ClarabelProblem {
22 let UnsolvedProblem {
23 objective,
24 direction,
25 variables,
26 } = to_solve;
27 let coef = if direction == ObjectiveDirection::Maximisation {
28 -1.
29 } else {
30 1.
31 };
32 let mut objective_vector = vec![0.; variables.len()];
33 for (var, obj) in objective.linear_coefficients() {
34 objective_vector[var.index()] = obj * coef;
35 }
36 let constraints_matrix_builder = CscMatrixBuilder::new(variables.len());
37 let mut settings = DefaultSettingsBuilder::default();
38 settings.verbose(false).tol_feas(1e-9);
39 let mut p = ClarabelProblem {
40 objective: objective_vector,
41 constraints_matrix_builder,
42 constraint_values: Vec::new(),
43 variables: variables.len(),
44 settings,
45 cones: Vec::new(),
46 };
47 for (var, def) in variables.iter_variables_with_def() {
49 if def.is_integer {
50 panic!("Clarabel doesn't support integer variables")
51 }
52 if def.min != f64::NEG_INFINITY {
53 p.add_constraint(var >> def.min);
54 }
55 if def.max != f64::INFINITY {
56 p.add_constraint(var << def.max);
57 }
58 }
59 p
60}
61
62pub struct ClarabelProblem {
64 constraints_matrix_builder: CscMatrixBuilder,
65 constraint_values: Vec<f64>,
66 objective: Vec<f64>,
67 variables: usize,
68 settings: DefaultSettingsBuilder<f64>,
69 cones: Vec<SupportedConeT<f64>>,
70}
71
72impl ClarabelProblem {
73 pub fn settings(&mut self) -> &mut DefaultSettingsBuilder<f64> {
75 &mut self.settings
76 }
77
78 pub fn into_solver(self) -> DefaultSolver<f64> {
81 let settings = self.settings.build().expect("Invalid clarabel settings");
82 let quadratic_objective = &CscMatrix::zeros((self.variables, self.variables));
83 let objective = &self.objective;
84 let constraints = &self.constraints_matrix_builder.build();
85 let constraint_values = &self.constraint_values;
86 let cones = &self.cones;
87 DefaultSolver::new(
88 quadratic_objective,
89 objective,
90 constraints,
91 constraint_values,
92 cones,
93 settings,
94 ).expect("Invalid clarabel problem. This is likely a bug in good_lp. Problems should always have coherent dimensions.")
95 }
96}
97
98impl SolverModel for ClarabelProblem {
99 type Solution = ClarabelSolution;
100 type Error = ResolutionError;
101
102 fn solve(self) -> Result<Self::Solution, Self::Error> {
103 let mut solver = self.into_solver();
104 solver.solve();
105 match solver.solution.status {
106 SolverStatus::PrimalInfeasible | SolverStatus::AlmostPrimalInfeasible => {
107 Err(ResolutionError::Infeasible)
108 }
109 SolverStatus::Solved
110 | SolverStatus::AlmostSolved
111 | SolverStatus::AlmostDualInfeasible
112 | SolverStatus::DualInfeasible => Ok(ClarabelSolution {
113 solution: solver.solution,
114 }),
115 SolverStatus::Unsolved => Err(ResolutionError::Other("Unsolved")),
116 SolverStatus::MaxIterations => Err(ResolutionError::Other("Max iterations reached")),
117 SolverStatus::MaxTime => Err(ResolutionError::Other("Time limit reached")),
118 SolverStatus::NumericalError => Err(ResolutionError::Other("Numerical error")),
119 SolverStatus::InsufficientProgress => Err(ResolutionError::Other("No progress")),
120 SolverStatus::CallbackTerminated => Err(ResolutionError::Other("Callback terminated")),
121 }
122 }
123
124 fn add_constraint(&mut self, constraint: Constraint) -> ConstraintReference {
125 self.constraints_matrix_builder
126 .add_row(constraint.expression.linear);
127 let index = self.constraint_values.len();
128 self.constraint_values.push(-constraint.expression.constant);
129 let next_cone = if constraint.is_equality {
132 ZeroConeT(1)
133 } else {
134 NonnegativeConeT(1)
135 };
136 let prev_cone = self.cones.last_mut();
137 match (prev_cone, next_cone) {
138 (Some(ZeroConeT(a)), ZeroConeT(b)) => *a += b,
139 (Some(NonnegativeConeT(a)), NonnegativeConeT(b)) => *a += b,
140 (_, next_cone) => self.cones.push(next_cone),
141 };
142 ConstraintReference { index }
143 }
144
145 fn name() -> &'static str {
146 "Clarabel"
147 }
148}
149
150pub struct ClarabelSolution {
152 solution: DefaultSolution<f64>,
153}
154
155impl ClarabelSolution {
156 pub fn into_inner(self) -> DefaultSolution<f64> {
158 self.solution
159 }
160
161 pub fn inner(&self) -> &DefaultSolution<f64> {
163 &self.solution
164 }
165}
166
167impl Solution for ClarabelSolution {
168 fn status(&self) -> SolutionStatus {
169 SolutionStatus::Optimal
170 }
171 fn value(&self, variable: Variable) -> f64 {
172 self.solution.x[variable.index()]
173 }
174}
175
176impl<'a> SolutionWithDual<'a> for ClarabelSolution {
177 type Dual = &'a ClarabelSolution;
178
179 fn compute_dual(&'a mut self) -> Self::Dual {
180 self
181 }
182}
183
184impl DualValues for &ClarabelSolution {
185 fn dual(&self, constraint: ConstraintReference) -> f64 {
186 self.solution.z[constraint.index]
187 }
188}
189
190struct CscMatrixBuilder {
191 rowval: Vec<Vec<usize>>,
193 nzval: Vec<Vec<f64>>,
195 n_rows: usize,
196 n_cols: usize,
197}
198
199impl CscMatrixBuilder {
200 fn new(n_cols: usize) -> Self {
201 Self {
202 rowval: vec![Vec::new(); n_cols],
203 nzval: vec![Vec::new(); n_cols],
204 n_rows: 0,
205 n_cols,
206 }
207 }
208 fn add_row(&mut self, row: LinearExpression) {
209 for (var, value) in row.linear_coefficients() {
210 self.rowval[var.index()].push(self.n_rows);
211 self.nzval[var.index()].push(value);
212 }
213 self.n_rows += 1;
214 }
215 fn build(self) -> clarabel::algebra::CscMatrix {
216 let mut colptr = Vec::with_capacity(self.n_cols + 1);
217 colptr.push(0);
218 for col in &self.rowval {
219 colptr.push(colptr.last().unwrap() + col.len());
220 }
221 clarabel::algebra::CscMatrix::new(
222 self.n_rows,
223 self.n_cols,
224 colptr,
225 fast_flatten_vecs(self.rowval),
226 fast_flatten_vecs(self.nzval),
227 )
228 }
229}
230
231fn fast_flatten_vecs<T: Copy>(vecs: Vec<Vec<T>>) -> Vec<T> {
232 let size: usize = vecs.iter().map(|v| v.len()).sum();
236 let mut iter = vecs.into_iter();
237 let mut result = if let Some(v) = iter.next() {
238 v
239 } else {
240 return Vec::new();
241 };
242 result.reserve_exact(size - result.len());
243 for v in iter {
244 result.extend_from_slice(&v);
245 }
246 result
247}
248
249#[cfg(test)]
250mod tests {
251
252 use super::*;
253 use crate::variables;
254
255 #[test]
256 fn test_csc_matrix_builder() {
257 variables! {vars:
258 x;
259 y;
260 z;
261 }
262 let mut builder = CscMatrixBuilder::new(3);
263 builder.add_row((y + 2 * z).linear);
264 builder.add_row((3 * x + 4 * y + 5 * z).linear);
265 let matrix = builder.build();
266 assert_eq!(matrix.m, 2); assert_eq!(matrix.n, 3); assert_eq!(matrix.get_entry((0, 0)), None); assert_eq!(matrix.get_entry((0, 1)), Some(1.));
274 assert_eq!(matrix.get_entry((0, 2)), Some(2.));
275 assert_eq!(matrix.get_entry((1, 0)), Some(3.));
276 assert_eq!(matrix.get_entry((1, 1)), Some(4.));
277 assert_eq!(matrix.get_entry((1, 2)), Some(5.));
278 }
279}