Semidefinite programming (SDP) support is available by choosing one BLAS/LAPACK provider:
Feature
Provider
Typical platform
clarabel-sdp-openblas
OpenBLAS
Linux; source builds need C and Fortran compilers
clarabel-sdp-mkl
Intel MKL
Windows or Linux
clarabel-sdp-accelerate
Apple Accelerate
macOS
The bare clarabel-sdp feature enables SDP when your application supplies BLAS/LAPACK linkage.
On Windows, the OpenBLAS feature uses a system installation.
The Solvers guide compares model-kind support and summarizes the
installation requirements of every backend. Enable a backend with its feature,
for example:
[dependencies]oximo = { version = "0.7", features = ["pounce"] }
With no solver feature, you can still construct models and export them through
the default io feature.
pounce uses finite-difference derivatives for nonlinear expressions by
default. The nightly-only pounce-enzyme feature provides exact gradients,
Jacobians, and Hessians through Enzyme.
It requires a nightly Rust toolchain with the enzyme component,
RUSTFLAGS="-Zautodiff=Enable", and a fat-LTO profile:
Model::new creates the model container. The variable! macro registers the
variables and their bounds, while constraint! adds relations written with
<=, >=, or ==. Finally, objective! declares the expression to maximize.
Every backend implements the same Solver trait, so switching from
HiGHS to another compatible backend changes only the solver type and options.
For a C-free continuous LP/QP/SOCP path, enable Clarabel instead:
cargo add oximo --features clarabel
See Modeling for indexed variables and nonlinear expressions,
Solvers for backend capabilities, and Results for
status, values, duals, and reduced costs.
Modeling
oximo's modeling layer lets you describe an optimization problem with idiomatic Rust.
You write models using declarative macros that mirror algebraic notation, set algebra for indices, operator-overloaded expressions, and rule-style constraint generation.
Everything on this page is re-exported from oximo::prelude.
Variables are the quantities a solver may choose. variable!
registers one on the model and binds a Rust variable of the same name, so the
symbol is immediately usable in later expressions. Bounds are written the way
you'd write them on paper.
use oximo::prelude::*;let m = Model::new("my_model");variable!(m, x >= 0.0); // continuous, x ≥ 0variable!(m, 0.0 <= y <= 10.0); // continuous, 0 ≤ y ≤ 10variable!(m, z); // free, unbounded by defaultvariable!(m, b, Bin); // binary {0, 1} (also Binary)variable!(m, n >= 0.0, Int); // general integer (also Integer)variable!(m, s <= 10.0, SemiCont(2.0)); // semicontinuous: 0 or in [2, 10]
Bounds, domain, warm start, and fixing can also be passed as keyword arguments after the name:
variable!(m, x, lb = 0.0, ub = 1.0); // same as `0.0 <= x <= 1.0`variable!(m, n, lb = 0.0, domain = Int); // keyword domainvariable!(m, w, lb = 0.0, ub = 10.0, Int); // mixed with a positional domain tokenvariable!(m, p, lb = 0.0, initial = 3.0); // warm start (scalar only)variable!(m, q, fix = 5.0); // fixed to 5.0 (scalar only)
A parameter is data that can change while the model structure stays fixed. It
stays symbolic in the model, so param! lets you build once and re-bind its
value between solves without rebuilding variables, constraints, or the
objective. This is useful for scenario sweeps.
param!(m, p1 = 0.0);variable!(m, x1 >= 0.0);objective!(m, Max, p1 * x1);for price in [1.0, 1.6, 2.0] { p1.set_param_value(price); let result = Highs.solve(&m, &HighsOptions::default())?; println!("{price} -> {:?}", result.objective());}
A parameter times a variable stays linear, so the model kind is unchanged by re-binding.
For parameter sweeps, it is recommended to use a Persistent solver if available. See Solvers for details.
A Set is the modeling-layer container for an ordered, finite index set
over integers, strings, or tuples. Use one to name the domain of an indexed
variable, a generated constraint, or a sum.
Most domains need no explicit Set, since an integer range is already
a domain (x[i in 0..5], sum!(… for i in 0..n)).
Reach for Set when keys are strings, tuples, sparse, or a
subset reused across statements.
The set! macro binds a named set. A plain right side is normalized to an owned set, while a pat in domain [if cond] comprehension builds and optionally filters one.
use oximo::prelude::*;let plants = Set::strings(["seattle", "san-diego"]);set!(items = 0..5); // range normalized to Set<usize>set!(routes = plants * plants); // Cartesian product// Comprehension: product domain + by-value `if`. These two are equivalent.set!(arcs = (p, q) in &plants * &plants if p != q); // single tuple patternset!(arcs = i in plants, j in plants if i != j); // multi-bind product// The typed filter is also a Set method (the receiver pins the key type):let diag = (&plants * &plants).filter_typed(|(p, q)| p == q);// Sparse / string leaf sets:let sparse = Set::from_ints([0, 2, 4, 8]);
Many optimization quantities vary by period, product, or route. The indexed
form, variable!(m, x[k in set]), registers one scalar per key and auto-names
it like x[seattle,nyc]. Bounds apply uniformly by default. A multi-index
family ranges over a Cartesian product.
let m = Model::new("transport");variable!(m, x[r in routes] >= 0.0); // one var per routevariable!(m, y[k in items] >= 0.0, Int); // integer familyvariable!(m, z[a in rows, b in cols], Bin); // multi-index (Cartesian product)// Scalar lookup: any type that converts to IndexKey works.let e1 = x[("seattle", "nyc")];let e2 = z[a, b];// Per-key bounds may reference the index.variable!(m, 0.0 <= w[(p, q) in routes] <= capacity_for(&p, &q));variable!(m, v[k in items], lb = 0.0, ub = cap[k]);// Filtered family: keep only matching keys (no trivial elements built).variable!(m, d[(i, j) in rc if i == j] >= 0.0);
x[key] returns the Expr for that element, ready to drop into a constraint or objective.
When an expression has one term per key, use sum! instead of building a Rust
loop. It produces one Expr that can go anywhere an expression is
accepted.
sum!(body for k in set) reads as \(\sum_{k \in \text{set}} \text{body}\).
The min! and max! macros use the same domains, Cartesian products, and
filters to build one flattened minimum or maximum expression. Their domains
must contain at least one selected key.
Empty sums are handled as the additive identity when the model is known. Sums
inside constraint!, objective!, soc_constraint!, and psd_constraint! inherit that macro's
model automatically, including nested sums and filtered domains:
// If maybe_empty has no selected keys, this contributes the model's zero.constraint!(m, balance, sum!(x[i] for i in maybe_empty) == 0.0);// Use the explicit model form for a standalone sum.let total = sum!(m, x[i] for i in maybe_empty);
The explicit sum!(model, ...) form also ensures that every selected term
belongs to that model. An unanchored standalone sum still needs at least one
term, because it has no model-owned expression context from which to construct
the empty result.
// Single sum: sum over i in items of weights[i] * x[i]constraint!(m, cap, sum!(weights[i] * x[i] for i in items) <= capacity);// Double sum, flat: sum over (p, q) in plants x marketslet total_cost = sum!(c[p, q] * x[p, q] for p in plants, q in markets);// Filtered sum.let active = sum!(x[i] for i in 0..n if online[i]);// Indexed extrema, with the same domain and filter syntax.let least_slack = min!(capacity[i] - x[i] for i in items);let peak_load = max!(load[t] for t in periods if online[t]);
Constraints define which combinations of variable values are allowed. Give each
one a stable name for readable solver output and diagnostics, then use
constraint! with a relation written as <=, >=, or ==.
A two-sided range becomes a single constraint.
constraint!(m, cap, 2.0 * x + 3.0 * y <= 100.0);constraint!(m, demand, x >= 5.0);constraint!(m, balance, x - y == 0.0);constraint!(m, band, 1.0 <= x + y <= 10.0); // two-sided range -> one constraint
A positive semidefinite (PSD) constraint requires every eigenvalue of a real
symmetric matrix to be nonnegative. Use symmetric_variable! to declare its
entries and psd_constraint! to require the matrix to be PSD. A symmetric
variable declaration alone creates free continuous variables and it does not
impose positivity or bounds.
symmetric_variable!(m, X[n]) returns a SymmetricMatrix<Expr<Affine>> with
n * (n + 1) / 2 independent variables. Mirrored entries refer to the same
variable. Use X[(i, j)] in Rust expressions, while modeling macros also accept
the X[i, j] notation. The side dimension must be positive.
For numeric matrix data, dense rows avoid triangle ordering:
let c = SymmetricMatrix::try_from_rows([[2.0, 1.0], [1.0, 2.0]])?;let identity = SymmetricMatrix::<f64>::identity(2);println!("C = {c}");println!("I = {identity}");
Matrices support addition, subtraction, negation, scalar multiplication,
trace(), and frobenius(&other). frobenius computes the full matrix inner
product, counting each stored off-diagonal product twice. Matrices combined by
arithmetic must have matching dimensions.
A PSD constraint can contain any symmetric matrix of constant or affine
entries, including symbolic parameters.
For example, minimizing t subject to t I - C being PSD gives the largest
eigenvalue of C. The eigenvalues of t I - C are t minus those of C, so
PSD membership requires t to be at least the largest eigenvalue.
Quadratic and nonlinear matrix entries are rejected. The current SDP support is
limited to real symmetric matrices.
PSD declarations support named, anonymous, computed-name, indexed, and filtered
forms. For a symmetric matrix variable X:
let identity = SymmetricMatrix::<f64>::identity(2);let shifts = [0.0, 1.0, 2.0];psd_constraint!(m, shifted[i in 0..shifts.len()], &X + shifts[i] * &identity);let extra = psd_constraint!(m, name = "extra positivity".to_owned(), &X);m.set_psd_active(extra, false)?;
A scalar declaration returns a PsdConstraintHandle. Retrieve a registered
handle with m.psd_constraint_handle("shifted[0]"). PSD constraints have their
own registry, separate from scalar and SOC constraints. Inactive PSD blocks
are excluded from solving and model-kind classification.
The indicator_constraint! macro applies an affine
relation only when a binary trigger has a selected value. The implication is
one-way: when the trigger has the other value, the relation is not enforced.
variable!(m, enabled, Binary);variable!(m, -10.0 <= x <= 20.0);indicator_constraint!(m, capacity, enabled == 1 => x <= 8.0);indicator_constraint!(m, shutdown, enabled == 0 => x == 0.0);indicator_constraint!(m, operating_band, enabled == 1 => -2.0 <= x <= 4.0);
The trigger must be a bare binary variable from the same model, its selected
value must be the literal 0 or 1, and the consequent must be affine. The
constraint does not imply the reverse direction: satisfying x <= 8.0 does
not force enabled to equal 1.
Indicator constraints support the same indexed-family and computed-name forms
as ordinary constraints:
variable!(m, enabled[t in periods], Binary);variable!(m, production[t in periods] >= 0.0);indicator_constraint!( m, capacity[t in periods if available[t]], enabled[t] == 1 => production[t] <= capacity_at(t),);let row_name = "shutdown_final";indicator_constraint!( m, name = row_name, enabled[last] == 0 => production[last] == 0.0,);
Gurobi, MOSEK, and SCIP consume indicators natively. GAMS supports them when an
explicit COPT, CPLEX, Gurobi, SCIP, or Xpress sub-solver is selected. Other
backends return SolverError::UnsupportedIndicator while active indicators
remain in the model. To use one of those backends, explicitly replace the
indicators with linear Big-M rows:
use oximo::prelude::*;use oximo::{HighsOptions, solvers::Highs};let m = Model::new("reformulated_indicators");variable!(m, produce, Binary);variable!(m, 0.0 <= quantity <= 10.0);indicator_constraint!(m, capacity, produce == 1 => quantity <= 7.0);indicator_constraint!(m, shutdown, produce == 0 => quantity == 0.0);objective!(m, Max, quantity + 2.0 * produce);let artifacts = m.reformulate_indicators(IndicatorReformulationOptions::default())?;let result = Highs.solve(&m, &HighsOptions::default())?;
Model::reformulate_indicators changes the model in place without cloning it
and returns artifacts containing the generated constraint IDs. To retain the
native-indicator model, use m.to_reformulated_indicator_model(options) to make
an independent transformed model. Both forms preserve source indicator IDs,
mark the sources inactive, and record generated constraint IDs in
indicator_reformulations(). They add no variables. For one indicator, use its
handle's reformulate(options) or to_reformulated_model(options) method.
Each finite side of an indicator gets a Big-M row. oximo derives M from the
body's affine coefficients and variable bounds, including the zero option for
semi-continuous and semi-integer variables. For an unbounded side, pass a
positive, finite fallback M to the in-place call instead:
let options = IndicatorReformulationOptions::default().with_fallback_big_m(1.0e6);m.reformulate_indicators(options)?;
SOS1 and SOS2 constraints are native special ordered sets. SOS1 allows at most
one nonzero member. SOS2 allows at most two adjacent nonzero members according
to their ordering weights.
Use the short form when member order itself defines the weights. It assigns
consecutive weights 1.0, 2.0, and so on:
sos_constraint!(m, one_choice, SOS1, [x, y, z]);sos_constraint!(m, adjacent_choice, SOS2, [x, y, z]);
Use explicit (variable, weight) pairs when the ordering values are meaningful
or nonuniform:
The indexed form creates one SOS constraint for every key in the binder. The
index domain must be written explicitly; the macro cannot infer it from an
IndexedVar:
variable!(m, x[i in 0..n] >= 0.0);variable!(m, y[i in 0..n] >= 0.0);variable!(m, z[i in 0..n] >= 0.0);sos_constraint!(m, choice[i in 0..n], SOS1, [x[i], y[i], z[i]]);
This registers choice[0], choice[1], and so on, with positional weights
1.0, 2.0, and 3.0 in each set. Explicit weights work in the same form:
For one SOS over a dynamically assembled collection, use the method API:
let members = vec![x, y, z];m.add_sos_constraint_auto_weights("one_choice", SosType::Sos1, members);
Use Model::add_sos_constraint instead when supplying explicit weights from a
runtime iterator.
Backends with native SOS support, such as Gurobi and SCIP, consume these constraints
directly. Other backends reject a model while it still contains active SOS
constraints. oximo never changes the formulation implicitly, but you can
explicitly replace every active SOS with a portable MILP formulation before
using a backend such as HiGHS:
use oximo::prelude::*;use oximo::{HighsOptions, solvers::Highs};let m = Model::new("reformulated_sos");variable!(m, 0.0 <= x <= 10.0);variable!(m, 0.0 <= y <= 10.0);variable!(m, 0.0 <= z <= 10.0);sos_constraint!(m, choice, SOS1, [x, y, z]);objective!(m, Max, x + 2.0 * y + 3.0 * z);// Uses each member's finite bounds as its Big-M values. The source SOS is// retained as inactive provenance, while binary variables and linear rows are// appended to `m`.let artifacts = m.reformulate_sos(SosReformulationOptions::default())?;let result = Highs.solve(&m, &HighsOptions::default())?;
Model::reformulate_sos modifies the model in place and returns the generated
variable and constraint IDs. To retain the native-SOS model—for example, to
solve it with Gurobi as well—produce an independent transformed model instead:
let reformulated = m.to_reformulated_sos_model(SosReformulationOptions::default())?;let result = Highs.solve(&reformulated, &HighsOptions::default())?;
A member needs finite bounds in every direction in which it can be nonzero. If
that is not available, pass a positive finite fallback to the in-place call
instead:
let options = SosReformulationOptions::default().with_fallback_big_m(1.0e6);m.reformulate_sos(options)?;
A fallback that is too small truncates the feasible region. Generated Big-M
rows embed the member bounds that existed during reformulation, so those bounds
cannot subsequently be changed on the transformed model. Set those bounds
before reformulating, or change them on a retained native source and make a
fresh reformulated model.
A Model has exactly one objective function, the quantity it minimizes or
maximizes among feasible solutions. Add it after defining the expressions it
uses, with the sense written next to the model.
objective!(m1, Min, 3.0 * x + 5.0 * y);objective!(m2, Max, x + 2.0 * y); // also Minimize/min, Maximize/max
If you have a feasibility problem, use the feas/Feasibility sense.
Use the indexed form of constraint! when the same rule applies
across a set. It emits one constraint per key, auto-named like
supply[seattle], without an explicit Rust loop. A trailing if filters the
keys, and name = expr gives a computed run-time name.
// Scalar set: one constraint per period.let periods = Set::range(0..T);constraint!(m, setup[t in periods], x[t] <= capacity * s[t]);// Tuple set + inner sum builds the LHS expression (key types inferred).constraint!(m, supply[p in plants], sum!(x[p, q] for q in markets) <= supply_of(&p));// Filtered family: only the keys passing the guard are built.constraint!(m, diag[(i, j) in arcs if i == j], x[i, j] <= 1.0);// Computed run-time name.constraint!(m, name = format!("bal_{p}"), inflow[p] - outflow[p] == 0.0);
Use model.affine_builder() to construct an affine expression by adding
terms incrementally, without creating intermediate addition nodes. The
builder accepts constant or affine terms (weighted sums of variables plus a
constant) from the same model.
use oximo::prelude::*;let m = Model::new("incremental terms");variable!(m, x[i in 0..3]);let mut builder = m.affine_builder();for (i, weight) in [2.0, 3.0, 4.0].into_iter().enumerate() { builder.add_term(weight, x[i]);}builder.add_constant(5.0);let lhs = builder.build();constraint!(m, capacity, lhs <= 100.0);
build() returns an affine expression, resets the pending terms and constant,
and retains scratch capacity for reuse in the next row. Numeric terms are
combined into a compact expression. Parameter-dependent terms stay symbolic.
Combining affine terms can change how floating-point additions are grouped.
Mathematically equivalent constructions may therefore produce different
coefficients, especially when large terms cancel.
For repeated contributions to a coefficient with large magnitude differences,
choose model.compensated_affine_builder(). Supplying the original weights
[1e16, 1.0, -1e16] for the same variable retains the coefficient 1, while
ordinary floating-point accumulation gives 0. Compensated summation is
explicit and uses additional arithmetic and scratch storage. It cannot recover
rounding already present in an input expression.
To determine what backend to use, consider what each backend solves (by model kind) and what it costs and offers (license, install, diagnostics). Each backend's Cargo feature flag is named in its own section below.
License: the underlying solver is commercial and needs a license at runtime. The oximo wrapper crates are all MIT OR Apache-2.0.
Direct interface: talks to the solver in-process. C API/FFI for HiGHS, SCIP, Gurobi, and MOSEK. Pure Rust for Clarabel and Pounce. BARON and GAMS instead exchange model/result files (.bar, .gms) with an external executable.
C compiler: whether a C/C++ compiler is required at build time.
IIS: computes an irreducible infeasible set to explain an infeasible model.
Warm start: persistent handle for incremental re-solves.
¶ GAMS's pool and duals come from the underlying sub-solver's GDX output (e.g. CPLEX solnpool).
‡ SCIP returns LP duals and reduced costs only when it can certify a complete original-model LP solution.
Clarabel's build requirements above describe the base clarabel feature.
SDP adds native BLAS/LAPACK dependencies. Compiler and installation requirements
depend on the selected provider.
Highs is enabled with the highs Cargo feature. No external solver
install is required, but a C/C++ compiler is needed at build time. Add it with
cargo add oximo --features highs.
use oximo::prelude::*;use oximo::solvers::Highs;use std::time::Duration;let result = Highs.solve(&m, &HighsOptions::default() .time_limit(Duration::from_secs(60)) .threads(4) .mip_gap(0.01) .verbose(true) .method(HighsMethod::Ipm))?;
Clarabel is a conic interior-point solver. The base clarabel
feature is pure Rust, requires no external solver installation or license,
and handles continuous LP, QP (convex quadratic objectives), and SOCP models.
use oximo::prelude::*;use oximo::solvers::Clarabel;let result = Clarabel.solve(&m, &ClarabelOptions::default())?;
Enable clarabel-sdp with BLAS/LAPACK linkage, or select a provider feature
such as clarabel-sdp-mkl; see Installation.
This adds continuous SDP with real symmetric affine PSD blocks, linear rows,
and SOC constraints. Objectives may be affine or convex quadratic when
minimizing (concave quadratic when maximizing). General quadratic constraints,
nonlinear expressions, and integer variables are unsupported on this path.
Clarabel.persistent() updates compatible numerical data between solves.
Changes to cone structure rebuild the solver. Chordal decomposition,
presolve, or dropped structural zeros can also prevent a native update, in
which case oximo rebuilds automatically.
Default stable path: LP/QP/QCP models use exact analytic derivatives,
including Jacobian rows and the constant Lagrangian Hessian. Nonlinear models
use compiled tapes with finite-difference nonlinear derivatives and a
limited-memory L-BFGS Hessian.
PounceSolverSelection::Auto is the default. It certifies convexity before
selecting a specialized engine:
LP and convex QP use the convex IPM.
SOCP with a convex objective uses the conic IPM, including explicit cones and
recognized quadratic SOC forms.
QCP, indefinite QP, and general NLP use the TNLP/builder path.
Inconclusive convexity checks fall back to NLP.
A numerical failure in an automatically selected LP or detected-SOCP route gets
one NLP attempt. The specialized convex engines currently have no
time-limit hook, with a time limit, Auto uses NLP.
PounceOptions provides dedicated setters and typed builders for POUNCE's
option reference:
use oximo::pounce::{MuStrategy, PounceOptions, PounceSolverSelection};let options = PounceOptions::default() .tol(1e-8) .mu_strategy(MuStrategy::Adaptive) .solver_selection(PounceSolverSelection::Auto) .presolve(true) .linear_solver("feral");// Escape hatch for an option not exposed by a dedicated setter.let options = options.set("acceptable_tol", 1e-5);
The backend manages print_level, max_cpu_time, warm_start_init_point, and
hessian_approximation. Configure those through the corresponding oximo options
or persistent handle.
Scip solves LP, MILP, QP, MIQP, QCP, MIQCP, SOCP, MISOCP, NLP,
and MINLP models through russcip. Enable scip to download bundled SCIP
binaries at build time:
[dependencies]oximo = { version = "0.7", features = ["scip"] }
use oximo::prelude::*;use oximo::solvers::Scip;use oximo::ScipOptions;let result = Scip::new().solve(&m, &ScipOptions::default() .mip_gap(0.01) .node_limit(10_000))?;
For an existing SCIP installation, use scip-system instead of scip. Set
SCIPOPTDIR to a SCIP development tree containing include/scip headers and
lib/libscip*, or let the build find SCIP via CONDA_PREFIX or standard system
paths. The system build generates bindings with libclang. The scip and
scip-system features cannot be combined.
SCIP supports native SOS1/SOS2 sets, affine indicator constraints,
semi-continuous and semi-integer variables, and explicit second-order cones.
Nonlinear expressions support arithmetic, division, constant powers, negation,
abs, sqrt, exp, log, sin, and cos. Nonlinear objectives are translated using
an auxiliary variable and an epigraph or hypograph constraint.
ScipOptions includes universal time, thread, and verbosity controls, typed
SCIP limits and tolerances, and .param(name, value) for other supported
parameters. .concurrent(true) enables SCIP's concurrent solver; the thread
option alone only sets the maximum thread count.
Scip::new().persistent() reuses compatible models across solves and accepts
append-only variables and constraints. solve_detailed also returns SCIP
statistics and values for columns generated by pricers. Scip::with_plugins
registers event handlers, separators, pricers, branching rules, heuristics,
constraint handlers, and node selectors. Custom Rust plugins cannot be used
with concurrent solving.
Gurobi uses gurobi-rs when the
gurobi Cargo feature is enabled. It requires a licensed Gurobi installation.
Set GUROBI_HOME to the installation directory before building and make sure a
Gurobi license is active.
Note: Only Gurobi v13 and later are supported.
[dependencies]oximo = { version = "0.7", features = ["gurobi"] }
use oximo::prelude::*;use oximo::solvers::Gurobi;use std::time::Duration;let result = Gurobi.solve(&m, &GurobiOptions::default() .time_limit(Duration::from_secs(120)) .mip_focus(1) .seed(101))?;
Mosek supports LP, MILP, convex QP/MIQP, convex QCP/MIQCP,
SOCP/MISOCP, and continuous SDP models. It requires the mosek Cargo feature, a licensed MOSEK
11.2 installation, and MOSEK_BINDIR_112 when MOSEK is outside its default
location. MOSEK validates the convexity of quadratic data. The backend links
through the mosek crate.
On Windows, setting MOSEK_BINDIR_112 directly to a path containing spaces can
fail because of an unquoted linker flag in the current mosek crate build
script. Create a junction with a space-free path and point the environment
variable at that junction; see mosek.rust#1.
Only MOSEK 11.2 is currently supported.
[dependencies]oximo = { version = "0.7", features = ["mosek"] }
use oximo::prelude::*;use oximo::solvers::Mosek;use std::time::Duration;let result = Mosek.solve(&m, &MosekOptions::default() .time_limit(Duration::from_secs(120)) .threads(4) .mio_tol_rel_gap(1e-4))?;
MosekOptions provides builders for every MOSEK 11.2 parameter. Universal
options such as time_limit, threads, and verbose are applied first.
MOSEK-specific parameter builders are then applied in call order.
The development backend supports real symmetric affine PSD blocks through the
existing mosek feature. The current SDP path requires an affine objective and
accepts linear and SOC constraints alongside PSD blocks. Quadratic objectives
with PSD blocks and mixed integer SDP are rejected; use supports_model(&m)
before selecting this backend.
Mosek.persistent() currently rebuilds SDP tasks on each solve, including
parameter-only changes. Primal and PSD dual matrices use the same
result accessors and conventions as Clarabel.
Baron is a global solver for LP/MILP/QP/MIQP/QCP/MIQCP/SOCP/MISOCP/
NLP/MINLP models. The Oximo adapter supports all of these model kinds and
translates explicit second-order cones to BARON's quadratic constraint
representation. It requires the baron feature, a licensed BARON installation
on PATH, and exchanges model and result files with the external executable.
[dependencies]oximo = { version = "0.7", features = ["baron"] }
use oximo::prelude::*;use oximo::solvers::Baron;let result = Baron::new().solve(&m, &BaronOptions::default())?;
Gams requires the gams Cargo feature plus a GAMS installation on
PATH. It exchanges model and result files with GAMS, which is useful when you
want to route a model through GAMS-managed solvers (CPLEX, BARON, IPOPT,
KNITRO, ...).
For a model containing indicators, select COPT, CPLEX, Gurobi, SCIP, or Xpress
explicitly in GamsOptions. The default solver selection is rejected because
indicator handling is sub-solver-specific.
[dependencies]oximo = { version = "0.7", features = ["gams"] }
use oximo::prelude::*;use oximo::solvers::Gams;let result = Gams::new().solve(&m, &GamsOptions::default())?;
See GamsOptions and the per-solver option structs in oximo::gams (GamsCplexOptions, GamsBaronOptions, GamsIpoptOptions, ...) for tuning the underlying solver.
A solver accepting NLP or MINLP does not imply that a backend can represent every
nonlinear expression. oximo validates the expressions before solving
and returns SolverError::UnsupportedNonlinearOperator when a
translation is unavailable.
Results: inspect solver status, values, duals, and solution pools
Printing & Debugging: print a model as algebra and track down what it actually says
I/O: write your model to MPS, LP or NL for use with external tools
Results
Every oximo backend returns the same SolverResult. Read it
the same way independently of the solver.
It is recommended to first check what stopped the solve and whether a usable point is available, then
inspect the values relevant to your application.
The quickest option is the built-in report, which renders a model-aware summary:
print!("{}", result.report(&m)?);
For programmatic access, SolverResult separates why the
solver stopped from whether a usable point came back. That split matters because,
for example, a run that hits a time limit can still carry a good incumbent.
let result = Highs.solve(&m, &HighsOptions::default())?;match result.termination { TerminationStatus::Optimal => { // `objective()` is Option, since a model may have no objective. if let Some(obj) = result.objective() { println!("optimal: {obj}"); } } TerminationStatus::Infeasible => println!("infeasible"), TerminationStatus::TimeLimit if result.has_solution() => { println!("time limit, best = {:?}", result.objective()); } _ => {}}let x_val = result.value_of(x)?; // Option<f64>let dual = result.dual_of(constraint_handle)?; // Option<f64>
TerminationStatus says why the solver stopped:
Optimal, LocallyOptimal, Feasible, Infeasible, Unbounded,
InfeasibleOrUnbounded, IterationLimit, TimeLimit, NodeLimit,
Interrupted, NumericError, NotSolved, or Other(String) for an unmapped
backend status.
PrimalStatus says what you actually got: NoSolution,
FeasiblePoint, or OptimalPoint. result.has_solution() is the shorthand.
Always check it before trusting a value.
Constraint declarations return handles that let you query their rows after a
solve. A scalar relation returns a model-bound ConstraintHandle.
A two-sided range returns RangeConstraintHandles,
which identifies either one interval row or its separate lower and upper rows.
The model identity prevents accidentally querying a result with a handle from a
different model. Use .id() on a handle only when a backend-facing raw
ConstraintId is required.
Capture an indexed declaration when you need to query its rows. The returned
IndexedConstraint maps each typed key to its registered
handle, so dual queries do not need to reconstruct generated names:
let cover: IndexedConstraint<usize> = constraint!(m, cover[i in 0..n_items], x[i] >= demand[i]);let handle = cover.get(0).expect("cover row exists");let dual = result.dual_of(handle)?;for (i, handle) in cover.iter() { println!("cover[{i}] dual = {:?}", result.dual_of(handle)?);}
get(key) also works for sparse, filtered, string, and tuple domains, and
iter() yields typed (key, ConstraintHandle) pairs in domain order. Handles
carry their model identity and remain usable after the model declaration block.
Indexed two-sided ranges return an IndexedRangeConstraint.
Each key maps to RangeConstraintHandles::Interval(handle) when the row stays a
native interval, or to RangeConstraintHandles::Split { lower, upper } when
symbolic bounds or a nonlinear body require two rows. Match the value and query
each handle with result.dual_of separately; a family can contain both forms.
Query matrix values and PSD duals separately. result.value_of_matrix(&matrix)
evaluates a symmetric matrix variable or expression at the best solution.
result.psd_dual_of(handle) retrieves the dual matrix associated with the
PsdConstraintHandle returned by psd_constraint!.
Display prints the full matrix as nested rows, including mirrored entries.
Both matrices contain ordinary, unscaled symmetric entries, with mirrored
indexing and upper-triangle column order. Use frobenius for the matrix inner product.
For a constraint F(x) PSD, the dual matrix Y is PSD within solver
tolerances for both minimization and maximization. Its Lagrangian contribution
is -<Y, F(x)>, with the objective taken as f(x) for minimization and -f(x)
for maximization. Compute complementarity using the evaluated constraint
matrix F(x) and its dual Y. At an optimal primal/dual pair, their inner
product is approximately zero.
psd_dual_of takes a PsdConstraintHandle, rather than a scalar constraint
handle. It returns None when no dual matrix is available, including for
inactive blocks; infeasibility certificates are not returned as PSD duals.
As with scalar results, model identity mismatches return an error.
value_of_matrix also accepts affine matrix expressions, such as t I - C,
and is available on individual SolutionPoints. It evaluates symbolic
parameters using their current values and returns None when there is no
solution or a required variable value is missing. Save the evaluated matrix before
rebinding parameters if you need the matrix for an earlier solve.
I/O: export a model for inspection in another tool
Printing & Debugging
Macros that generate families of constraints are convenient right up until the model doesn't say what you thought it said. Model implements Display, so the fastest way to check is to print it.
You can print the complete model as a readable algebra block to the console with println!("{m}");.
let m = Model::new("diet");variable!(m, x >= 0.0);variable!(m, y >= 0.0);constraint!(m, c1, x + 2.0 * y <= 14.0);constraint!(m, c2, 3.0 * x - y >= 0.0);objective!(m, Min, 3.0 * x + 4.0 * y);println!("{m}");
Model 'diet' (LP)min 3 x + 4 ys.t. c1: x + 2 y <= 14 c2: 3 x - y >= 0vars x >= 0 y >= 0
The header carries the inferred ModelKind, so a single print answers both "did my constraints come out right?" and "why is this backend refusing my model?". Ranges, cones, and domains all render in the algebraic form you wrote:
max x + y band: 1 <= x + t <= 4 disk: ||x, t|| <= x + 1 0 <= y <= 1, binary
Printing a 10,000-constraint model is not debugging. Display adapters render a single piece, which is what you want inside an assertion or a targeted dbg!:
Look an id up by name with constraint_id (or soc_constraint_id), then render it:
let c = m.constraint_id("c").unwrap();assert_eq!(m.display_constraint(c).to_string(), "c: -y + x * y <= 3");assert_eq!(m.display_expr(2.0 * x - y).to_string(), "2 x - y");
Because these return Display adapters rather than String, they're cheap to leave in test assertions. The rendering only happens when something formats them.
Indexed constraints are auto-named base[key], and that name is exactly what constraint_id expects. This is the usual way to confirm a rule expanded over the keys you intended:
constraint!(m, supply[p in plants], sum!(x[p, q] for q in markets) <= supply_of(&p));let c = m.constraint_id("supply[seattle]").unwrap();println!("{}", m.display_constraint(c));
To render a key the same way the auto-namer does, use display_index_key.
A printed model resolves parameters to whatever they're bound to right now, which makes it easy to confirm a scenario sweep is re-binding what you think:
param!(m, price = 4.0);objective!(m, Min, price * x);println!("{m}"); // min 4 x ... params: price = 4m.set_param(price, 7.5)?;println!("{m}"); // min 7.5 x ... params: price = 7.5
oximo infers the ModelKind from the expressions you write. You never declare it. A backend that rejects your model is usually telling you an expression is a different kind than you assumed, so you can pin it down in a test:
assert_eq!(m.kind(), ModelKind::LP);
Compare the result against the backend table to see which solvers accept it.
Active PSD blocks normally give a model the SDP kind, or MISDP when it has
integer variables or other discrete requirements. General quadratic constraints
and nonlinear expressions take precedence.
See SDP backend limits for objective restrictions.
Sometimes the backend accepts the model's kind and still returns infeasible. Each constraint is fine on its own, but together they can't all hold. Backends built on a solver with a native conflict refiner can express why by computing an irreducible infeasible subsystem (IIS), a minimal set of constraints and variable bounds that are jointly infeasible, where dropping any one member makes the rest feasible.
use oximo::solvers::Gurobi;let m = Model::new("iis");variable!(m, x >= 0.0);constraint!(m, floor, x >= 2.0);constraint!(m, ceil, x <= 1.0);objective!(m, Min, x);let iis = Gurobi.compute_iis(&m, &GurobiOptions::default())?;println!("{}", iis.report(&m));
The report uses the model's own names, so floor and ceil point straight back to the constraint! lines that conflict. The x >= 0 bound isn't part of the conflict, so it isn't listed. Reach the members programmatically through Iis's constraints, soc_constraints, and var_bounds fields (each bound tagged VarBoundKindLower/Upper). compute_iis returns a SolverError if the model turns out feasible, so it doubles as an "is this actually infeasible?" assertion.
I/O: export your model to MPS, LP, or NL for inspection in other tools
Modeling: back to variables, sets, and rule-style constraints
I/O
The oximo-io crate writes Models to the standard text formats MPS, LP, and NL, and can read all three formats back into a Model. All I/O is gated on the io Cargo feature, which is on by default.
Use this when you want to:
Hand a Model to a solver oximo doesn't bundle (COPT, SCIP, CPLEX, ...)
Feed a nonlinear model to an AMPL-compatible solver via NL
Reproduce a bug report against a third-party tool
Archive the exact problem instance for later inspection
MPS and LP describe linear and quadratic models. In general, you should use LP and reach for NL when the model has
nonlinear expressions that MPS and LP cannot represent.
Format
Pros
When to pick
MPS
Universal, column-oriented, fixed historical format
The NL writer is the most configurable of the three. write_nl_with and to_nl_string_with take a WriteOptions to select the NlFormat (binary or ASCII) and attach solver metadata: suffixes, defined variables, imported functions, and complementarity pairs.
write_nl_files emits the .nl alongside its companion .col/.row name files, which is what most AMPL-compatible solvers expect when you want readable names in the solution.
The NL writer emits supported nonlinear operations as exact prefix opcode
trees. The reader uses the same mapping for ASCII and little-endian binary NL:
Opcode
Operation
oximo support
o13
floor
Not represented by the expression model
o14
ceil
Not represented by the expression model
o15
abs
Read and written
o16
unary -
Read and written
o34
logical not
Logical expressions are not represented
o37
tanh
Read and written
o38
tan
Read and written
o39
sqrt
Read and written
o40
sinh
Read and written
o41
sin
Read and written
o42
log10
Read and written
o43
log
Read and written
o44
exp
Read and written
o45
cosh
Read and written
o46
cos
Read and written
o47
atanh
Read and written
o49
atan
Read and written
o50
asinh
Read and written
o51
asin
Read and written
o52
acosh
Read and written
o53
acos
Read and written
exp2(x) is written exactly as the power expression 2 ^ x (o5), because
NL has no separate exp2 opcode.
The oximo operations cbrt, expm1, log1p, and log2 also have no direct
NL opcode. Writing one returns
IoError::UnsupportedNonlinearOperator.
Reading floor, ceil, logical not, or another
well-formed but unrepresentable opcode returns IoError::UnsupportedNl.
All writers preserve the Variable and constraint names from your model, so exported files cross-reference cleanly with SolverResult lookups such as dual_of and reduced_costs (see Results).
use oximo::io::{read_mps, read_mps_file};use std::fs::File;let model = read_mps_file("model.mps")?;let model_from_stream = read_mps(File::open("model.mps")?)?;
The reader accepts the standard linear sections, range rows, integer markers,
binary and semi-variable bounds, SOS sections, INDICATORS records, and the
QUADOBJ, QMATRIX, QCMATRIX, and QSECTION quadratic extensions. An MPS
indicator references an ordinary affine row using IF row binary 0|1; ranged
indicator bodies are represented as two rows. MPS does not identify the
coefficient scaling used by quadratic constraints, so the default is the
Gurobi convention. Select CPLEX or MOSEK scaling explicitly when needed:
use oximo::io::{ MpsQuadraticFormat, MpsReadOptions, read_mps_file_with,};let options = MpsReadOptions { quadratic_format: MpsQuadraticFormat::Cplex,};let model = read_mps_file_with("cplex-model.mps", &options)?;
The NL reader imports models produced by oximo or compatible
AMPL-style tools. Use read_nl_file for a path or read_nl
for any byte stream:
use oximo::io::{read_nl, read_nl_file};use std::fs::File;let model = read_nl_file("model.nl")?;let model_from_stream = read_nl(File::open("model.nl")?)?;
Both ASCII and little-endian binary NL encodings are accepted. When a .row or
.col sidecar exists beside the file, it supplies the original row and column
names; otherwise deterministic names are generated. Interval rows and initial
values are preserved when they can be represented by the core model.
The reader rejects malformed input with IoError::InvalidNl and
well-formed NL sections that the core model cannot represent with
IoError::UnsupportedNl. Imported functions, defined variables,
logical/network constraints, complementarity sections, and unsupported expression
opcodes are intentionally rejected.
LP files can be imported from a byte stream or a path with read_lp
and read_lp_file:
use oximo::io::{read_lp, read_lp_file};use std::fs::File;let model = read_lp_file("model.lp")?;let model_from_stream = read_lp(File::open("model.lp")?)?;
The reader supports the CPLEX LP linear and quadratic subset represented by the
core model, including objectives, constraints, bounds, integer/binary and
semicontinuous domains, quadratic terms, SOS sections, and inline indicator
rows of the form binary = 0|1 -> affine relation. Ranged indicator bodies are
written as two indicator rows. Malformed input returns
IoError::InvalidLp with its source line and column. Unsupported LP
sections return IoError::UnsupportedLp.
NL has no native segment for the indicator representation used by oximo.
Writing a model with an active indicator returns IoError::UnsupportedNl.
The MPS, LP, and NL writers do not support active PSD constraints. Writing
such a model returns IoError::Conic, including when other expressions make
its model kind QCP or NLP. Inactive PSD blocks are skipped.
If part of this guide is confusing, incomplete, outdated, or hard to follow, say so. Getting stuck while trying to do something in oximo usually means the docs can be better, not that you missed something.
AI-assisted contributions are allowed. Contributors remain fully responsible for the quality, correctness, licensing, and usefulness of what they submit. The standards are the same either way.
Review and validate AI-assisted content before submitting it. Using an AI tool does not transfer responsibility for correctness, code quality, license compliance, security, or documentation accuracy.
Disclose it when a significant portion of a contribution is generated by, or copied verbatim from, an AI tool. Routine help (grammar, spelling, minor phrasing) needs no disclosure.
Note it in the pull request description, or use a commit trailer:
Assisted-by: generic LLM chatbot
This helps the project evaluate tooling practices and refine these guidelines over time.