# oximo-clarabel
[Clarabel](https://clarabel.org) backend for [oximo](https://github.com/oximo-rs/oximo).
Wraps the [`clarabel`](https://crates.io/crates/clarabel) crate: an open-source,
pure-Rust interior-point solver for convex conic programs. Supports `LP`, convex `QP`
(quadratic objective, linear constraints), and `SOCP` model kinds. It has no
integer support, so mixed-integer models are rejected.
## Usage
Enable the `clarabel` feature on the umbrella `oximo` crate:
```toml
[dependencies]
oximo = { version = "0.4", features = ["clarabel"] }
```
To use this crate directly:
```toml
[dependencies]
oximo-clarabel = "0.4"
oximo-core = "0.4"
oximo-solver = "0.4"
```
The optional `faer` feature adds the [faer](https://crates.io/crates/faer)
sparse LDL factorization as an alternative KKT backend.
## Quick example
```rust,no_run
use oximo_core::prelude::*;
use oximo_clarabel::{Clarabel, ClarabelOptions};
use oximo_solver::{Solver, TerminationStatus};
let m = Model::new("socp");
variable!(m, x);
variable!(m, y);
variable!(m, t >= 0.0);
m.fix(t, 1.0);
soc_constraint!(m, disk, [x, y] <= t); // ||(x, y)||_2 <= t
objective!(m, Min, x + y);
let result = Clarabel.solve(&m, &ClarabelOptions::default()).unwrap();
assert_eq!(result.termination, TerminationStatus::Optimal);
println!("obj = {}", result.objective().unwrap()); // -sqrt(2)
```
Run the bundled SOCP example:
```sh
cargo run -p oximo --example gradostat_multiperiod_socp --features clarabel
```
## Supported model kinds
| `LP` | Yes |
| `QP` (quadratic objective, linear constraints) | Yes, the objective Hessian must be positive semidefinite (Clarabel does not check and a non-convex `P` fails numerically) |
| `SOCP` | Yes |
| `MILP`/`MIQP`/`MISOCP` | No, Clarabel has no integer support (returned as `UnsupportedKind`) |
| `QCP`/`MIQCP` | No, convex-QCP-to-SOC reformulation is not implemented. Write the constraint in SOC form instead. |
A quadratic constraint that is already SOC-shaped (`sum p_i x_i^2 <= n t^2`,
`t >= 0`) is detected as `SOCP` and solved, a general convex quadratic
constraint is not.
## Persistent handle (repeated solves)
`Clarabel.solve` builds a fresh solver for each call. When you re-solve one model
many times (parameter sweeps, sensitivity studies, column generation, rolling
horizons, etc) build a resident handle with `Clarabel.persistent()` and call
`solve` on it instead. `ClarabelPersistent` is a plain `Solver`. When the next
model has the same dimensions, cone layout, and `P`/`A` sparsity pattern, only
the numeric data (`P`, `q`, `A`, `b`) is overwritten in place via Clarabel's
`update_data`, reusing the KKT symbolic factorization. Any structural change
(added/removed rows or columns, a changed sparsity pattern, a new cone, or a
flipped constraint sense that moves a row between cones) triggers a transparent
rebuild, so results always match a cold solve.
## Convex quadratic objectives (QP)
A quadratic objective (e.g. `x.powi(2)`, `x * y`) with linear constraints is
detected as `QP`. oximo derives the Hessian from the objective and Clarabel
minimizes `c'x + 0.5·x'Px`.
> **Convexity.** Clarabel solves only convex QPs. For minimization `P` must be
> positive semidefinite; for maximization, negative semidefinite. Clarabel does
> not check this, so an indefinite or incorrectly signed Hessian may fail
> numerically or return a non-optimal point.
## Options
`ClarabelOptions` is a typed builder. All methods return `Self` for chaining,
and every field is `Option`. `None` leaves Clarabel's own default in place.
### Universal options (via `UniversalOptionsExt`)
| `.time_limit(Duration)` | `time_limit` | none |
| `.threads(usize)` | `max_threads` (KKT solvers only, see note) | none |
| `.verbose(bool)` | `verbose` | none |
`.threads` maps to `max_threads`, which only affects multithreaded KKT solvers.
The default `qdldl` is single-threaded.
### Clarabel options (selected)
| `.direct_solve_method(ClarabelDirectSolve)` | `direct_solve_method` | `Auto` |
| `.max_iter(u32)` | `max_iter` | `200` |
| `.max_step_fraction(f64)` | `max_step_fraction` | `0.99` |
| `.tol_gap_abs(f64)` | `tol_gap_abs` | `1e-8` |
| `.tol_gap_rel(f64)` | `tol_gap_rel` | `1e-8` |
| `.tol_feas(f64)` | `tol_feas` | `1e-8` |
| `.tol_infeas_abs(f64)` | `tol_infeas_abs` | `1e-8` |
| `.tol_infeas_rel(f64)` | `tol_infeas_rel` | `1e-8` |
| `.tol_ktratio(f64)` | `tol_ktratio` | `1e-6` |
| `.equilibrate_enable(bool)` | `equilibrate_enable` | `true` |
| `.static_regularization_enable(bool)` | `static_regularization_enable` | `true` |
| `.dynamic_regularization_enable(bool)` | `dynamic_regularization_enable` | `true` |
| `.iterative_refinement_enable(bool)` | `iterative_refinement_enable` | `true` |
| `.presolve_enable(bool)` | `presolve_enable` | `true` |
| `.input_sparse_dropzeros(bool)` | `input_sparse_dropzeros` | `false` |
There is also a matching set of low-accuracy fallback tolerances
(`reduced_tol_*`) and further equilibration, line-search, and regularization
options. The full list is in [src/options.rs](src/options.rs).
```rust,ignore
use oximo_clarabel::{ClarabelOptions, ClarabelDirectSolve};
use oximo_solver::UniversalOptionsExt;
let opts = ClarabelOptions::default()
.max_iter(100)
.tol_gap_rel(1e-9)
.direct_solve_method(ClarabelDirectSolve::Qdldl)
.verbose(true);
```
## Result
`SolverResult` fields, populated whenever a usable point is available
(`primal_status` is `FeasiblePoint` or `OptimalPoint`):
- `solutions` - primal points (`Vec<SolutionPoint>`). This backend returns a single point holding the `primal` values keyed by `VarId` and the `objective` (adjusted for the objective sign and any constant term). Access via `result.objective()` / `result.value_of(var)`
- `dual` - constraint duals, keyed by `ConstraintId`, access via `result.dual_of(c)`. Reported for equality and single-sided/range linear constraints in the LP convention (`gradient = A' y` of the problem as posed)
- `soc_dual` - for each explicit `soc_constraint!`, its norm-form bound multiplier (`z0`), keyed by `SocConstraintId`, access via `result.soc_dual_of(soc)`
- `reduced_costs` - not populated by this backend (always empty)
- `termination` - why the solve stopped, mapped from Clarabel's `SolverStatus`: `Optimal` (`Solved`), `Infeasible` (`PrimalInfeasible`), `Unbounded` (`DualInfeasible`), `IterationLimit` (`MaxIterations`), `TimeLimit` (`MaxTime`), `Interrupted` (`AlmostSolved`), `NumericError` (`NumericalError` / `InsufficientProgress`), `NotSolved` (`Unsolved`)
- `primal_status` - whether a usable point came back (`NoSolution` / `FeasiblePoint` / `OptimalPoint`), `result.has_solution()` is the shortcut
- `best_bound` / `gap` - always `None` (conic solver, no MIP bound)
- `solve_time` - wall time measured around the Clarabel solve call
- `iterations` - interior-point iteration count
## License
MIT OR Apache-2.0