oximo is a Rust algebraic modeling library for linear, mixed-integer, and nonlinear optimization.
Generated on July 27, 2026
Installation
For most projects, installation is one command. oximo includes the Highs
solver by default, so you can build and solve linear, mixed-integer linear, and
quadratic models without installing a solver or obtaining a license.
This site is a guide for tutorials and worked examples. For the complete API
reference, see docs.rs/oximo.
oximo requires Rust 1.85 or later (edition 2024). Install Rust with
rustup if you do not already have it.
Create a project and add oximo:
cargo new my-oximo-model --edition 2024cd my-oximo-modelcargo add oximo
That is enough to use Highs and write models to MPS, LP, or NL files.
The bundled HiGHS build needs a C compiler:
On Windows, install the MSVC C++ build tools,
on macOS, run xcode-select --install,
on Linux, install your distribution's standard C/C++ build tools.
Next, follow the Quickstart to create and solve your first
model. If the project builds and the Quickstart prints an objective value, your
installation is ready.
If you need another backend, you can use the provided features flags.
The Solvers guide compares model-kind support, shows the different
requirements of each backend, and covers external solver setup.
It is the best place to choose an engine when you are unsure.
The default highs and io features are convenient for most users. If you want
to avoid compiling bundled HiGHS, turn them off explicitly and select the backend you want:
[dependencies]oximo = { version = "0.5", default-features = false, features = ["clarabel"] }
With no solver feature, you can still construct models. You will need a solver
feature before solving them.
pounce uses finite-difference derivatives for nonlinear expressions by
default. The nightly-only pounce-enzyme feature provides exact gradients,
Jacobians, and Hessians through Enzyme. Use it for
nonlinear POUNCE models when your toolchain can support it. It is highly
recommended to use it.
It requires a nightly Rust toolchain with the enzyme component,
RUSTFLAGS="-Zautodiff=Enable", and a fat-LTO profile. The oximo workspace
provides an enzyme profile:
This walkthrough builds a small linear program end-to-end: variables, constraints, objective, solve, read result. By the end you'll have run Highs on a real model and inspected the solution.
Model is the container that holds variables, constraints, and an objective. The name is used when exporting or printing it and as a label in solver logs.
variable!(m, x >= 0.0);variable!(m, 0.0 <= y <= 4.0);
The variable! macro registers a variable on m and binds a Rust
binding of the same name, so you can use x directly in later expressions. Write
the bounds the way you'd write them on paper: x >= 0.0, 0.0 <= y <= 4.0, or
just variable!(m, z) for a free variable. See Modeling for
integer, binary, and indexed variables.
SolverResult::objective() returns Option<f64>, None when
the model has no objective or no solution was found.
value_of(expr) gives a variable's value in the best solution.
For a quick human-readable dump of the whole solution, print the built-in report:
print!("{}", result.report(&m));
For status checking, duals, and reduced costs, see Results.
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}\).
// 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]);
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 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);
Pow, Sin, Cos, Exp, Log, Abs, and bilinear products are first-class,
so you can write nonlinear algebra in the same expressions as linear terms.
The model's kind is inferred from what you write.
// Rosenbrock NLPobjective!(m1, Min, (1.0 - x).powi(2) + 100.0 * (y - x.powi(2)).powi(2));// Quadratic constraint (model kind: QCP)constraint!(m2, disk, x.powi(2) + y.powi(2) <= 1.0);// Second-order cone ||(x, y)|| <= t (model kind: SOCP)soc_constraint!(m3, cone, [x, y] <= t);// Transcendental utility (MINLP when any variable is integer/binary)objective!(m4, Max, sum!(u[i] * (1.0 + w[i] * x[i]).log() for i in items));
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, 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.
Gurobi, MOSEK, BARON, and GAMS need software installed outside Cargo. Add the
corresponding feature only after that software is installed and licensed.
Enable mosek, install MOSEK 11.2, and configure a valid license. Set
MOSEK_BINDIR_112 to MOSEK's platform bin directory when the installation is
not in its default location. The backend links through the
mosek crate.
Note for Windows: the current mosek crate build script emits an unquoted
linker flag, so setting MOSEK_BINDIR_112 directly to a path containing
spaces can fail. Create a junction with a space-free path, then point the
environment variable at that junction. See mosek.rust#1.
Note: We currently support only MOSEK 11.2.
Support for other versions of MOSEK is planned.
[dependencies]oximo = { version = "0.5", features = ["mosek"] }
Enable baron or gams after placing the respective executable on PATH.
Both backends exchange files with an external program rather than linking the
solver into your process.
[dependencies]oximo = { version = "0.5", features = ["baron"] }
Clarabel is a pure-Rust conic interior-point solver. No install, no license. It handles continuous LP, QP (convex quadratic objectives), and SOCP models.
use oximo::prelude::*;use oximo::solvers::Clarabel;let result = Clarabel.solve(&m, &ClarabelOptions::default())?;
Pounce is a pure-Rust port of IPOPT, covering continuous LP/QP/QCP/NLP. Enable the pounce feature.
use oximo::prelude::*;use oximo::pounce::Pounce;let result = Pounce.solve(&m, &PounceOptions::default())?;
Purely linear/quadratic models solve with exact analytic derivatives. Models containing a nonlinear function fall back to finite differences and an L-BFGS Hessian unless you enable the nightly-only pounce-enzyme feature, which supplies exact gradients plus sparse Jacobians and Hessians.
For any nonlinear model, enabling pounce-enzyme is highly recommended. Exact derivatives are faster and far more accurate than the finite-difference fallback, which improves both convergence and robustness. It requires a nightly toolchain and a fat-LTO build (see Installation > Advanced: exact nonlinear derivatives).
Mosek supports LP, MILP, convex QP/MIQP, convex QCP/MIQCP, and
SOCP/MISOCP 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.
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.
Gams requires the gams Cargo feature plus a GAMS install on PATH. Useful when you want to route a model through GAMS-managed solvers (CPLEX, BARON, IPOPT, KNITRO, ...).
use oximo::prelude::*;use oximo::solvers::Gams;let result = Gams.solve(&m, &GamsOptions::default())?;
See GamsOptions and the per-solver option structs in oximo::gams (GamsCplexOptions, GamsBaronOptions, GamsIpoptOptions, ...) for tuning the underlying solver.
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_id); // 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.
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.
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. All three writers are 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.
use oximo::io::{NlFormat, WriteOptions, write_nl_with};let opts = WriteOptions::default().format(NlFormat::Ascii);write_nl_with(&m, "model.nl", &opts)?;
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.
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).
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.