oximo

oximo is a Rust algebraic modeling library for linear, mixed-integer, and nonlinear optimization.
Generated on July 27, 2026

Table of Contents

  1. Installation
  2. Quickstart
  3. Modeling
  4. Solvers
  5. Results
  6. Printing & Debugging
  7. I/O
  8. Contributing

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.

🔗Start here

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 2024
cd my-oximo-model
cargo 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.

🔗Choose another backend

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.

For example, to add the Clarabel backend:

cargo add oximo --features clarabel --no-default-features

Or, in Cargo.toml:

[dependencies]
oximo = { version = "0.5", features = ["clarabel"], default-features = false }

🔗Minimal builds

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.

🔗Advanced: exact nonlinear derivatives

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:

RUSTFLAGS="-Zautodiff=Enable" cargo +nightly build --profile enzyme --features pounce-enzyme

See the Enzyme installation instructions for configuring the toolchain.

🔗Feature reference

FeatureIncluded by defaultPurpose
highsyesBundled Highs solver for LP, MILP, and QP.
ioyesMPS, LP, and NL file writers.
baronnoBaron global-optimization backend.
clarabelnoPure-Rust Clarabel solver for LP, QP, and SOCP.
clarabel-faernoUse Clarabel's faer linear-algebra backend.
gurobinoGurobi backend.
gamsnoGams bridge.
pouncenoPure-Rust Pounce solver for continuous nonlinear models.
pounce-enzymenoExact Enzyme derivatives for POUNCE; requires nightly Rust.
moseknoMosek backend for MOSEK 11.2.

Quickstart

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.

🔗The problem

\[ \begin{aligned} \max \quad & 3x + 4y \\ \text{s.t.} \quad & x + 2y \le 14 \\ & 3x \ge y \\ & x \le y + 2 \\ & x \ge 0 \\ & 0 \le y \le 4 \end{aligned} \]

🔗The full program

use oximo::prelude::*;
use oximo::solvers::Highs;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let m = Model::new("transport");

    variable!(m, x >= 0.0);
    variable!(m, 0.0 <= y <= 4.0);

    constraint!(m, c1, x + 2.0 * y <= 14.0);
    constraint!(m, c2, 3.0 * x >= y);
    constraint!(m, c3, x <= y + 2.0);
    objective!(m, Max, 3.0 * x + 4.0 * y);

    let result = Highs.solve(&m, &HighsOptions::default())?;
    println!("obj = {:?}", result.objective()); // Some(34.0)
    println!("x   = {:?}", result.value_of(x)); // Some(6.0)
    println!("y   = {:?}", result.value_of(y)); // Some(4.0)
    Ok(())
}

Run it with cargo run and you should see the optimum: obj = Some(34.0).

🔗Step by step

🔗1. Create a model

let m = Model::new("transport");

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.

🔗2. Declare variables

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.

🔗3. Add constraints

constraint!(m, c1, x + 2.0 * y <= 14.0);

constraint! takes the model, a name, and a relation written with <=, >=, or ==. Expressions use standard Rust operators.

🔗4. Set an objective

objective!(m, Max, 3.0 * x + 4.0 * y);

objective! takes a sense (Max or Min, also Maximize/max, Minimize/min) and the expression. A Model has one objective.

If your problem is a feasibility problem, you can use Feas/Feasibility as the sense.

🔗5. Solve

let result = Highs.solve(&m, &HighsOptions::default())?;

Each backend implements the Solver trait, so switching engines is a one-line change. Highs.solve(&model, &options) returns a SolverResult.

🔗6. Read the result

println!("obj = {:?}", result.objective());
println!("x   = {:?}", result.value_of(x));

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.

For more examples, see the examples directory.

🔗Next steps

  • Modeling: variables, index sets, indexed variables, summation, rule-style constraints
  • Solvers: backend options and result inspection
  • Printing & Debugging: print a model as algebra when it doesn't say what you expected
  • I/O: export models to MPS, LP, or NL

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

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 ≥ 0
variable!(m, 0.0 <= y <= 10.0);         // continuous, 0 ≤ y ≤ 10
variable!(m, z);                        // free, unbounded by default
variable!(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 domain
variable!(m, w, lb = 0.0, ub = 10.0, Int); // mixed with a positional domain token
variable!(m, p, lb = 0.0, initial = 3.0);  // warm start (scalar only)
variable!(m, q, fix = 5.0);                // fixed to 5.0 (scalar only)
Domain tokenMeaning
(none)Continuous
BinBinary {0, 1} (alias Binary)
IntGeneral integer (alias Integer)
SemiCont(t)Zero, or continuous in [t, ub]
SemiInt(t)Zero, or integer in [t, ub]

🔗Parameters

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.

🔗Index sets

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 pattern
set!(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]);

🔗Indexed variables

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 route
variable!(m, y[k in items] >= 0.0, Int);    // integer family
variable!(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.

🔗Expressions

Exprs are built with standard Rust operators. Scalars are f64.

let lhs = x + 2.0 * y - 3.0 * z;
let rhs = 4.0 * x + 5.0;

Behind the scenes oximo uses arena-allocated expression trees (oximo-expr), so combining large Exprs stays cheap.

🔗Summing over sets

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 markets
let 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]);

For the plain dot-product case there is also dot.

🔗Constraints

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

For SOC constraints, use the soc_constraint! macro.

🔗Objectives

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.

🔗Rule-style constraints

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);

🔗Nonlinear expressions

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 NLP
objective!(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));

Check the inferred kind with Model::kind(), which returns a ModelKind. Backends reject kinds they don't support. See Printing & Debugging.

🔗Next steps

  • Solvers: pick a backend, set options, read the solution
  • Printing & Debugging: print a model as algebra and check what it actually says
  • I/O: export your model to MPS, LP, or NL

Solvers

oximo is solver-agnostic, and provides a variety of backends to solve your optimization models.

Every backend implements the same Solver trait, so swapping engines is a one-line change. The general pattern is:

let result = Backend.solve(&model, &BackendOptions::default())?;

result is a SolverResult, the same struct regardless of backend.

🔗Choosing a backend

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.

🔗Model kind support

Each row is what Solver::supports accepts for that backend, against every ModelKind:

BackendLPMILPQPMIQPQCPMIQCPSOCPMISOCPNLPMINLP
Highs✓✓✓———————
Clarabel✓—✓———✓———
Pounce✓—✓—✓———✓—
Gurobi✓✓✓✓✓✓✓✓✓✓
Mosek✓✓✓✓✓✓✓✓——
Baron✓✓✓✓✓✓✓✓✓✓
Gams✓✓✓✓✓✓✓✓✓✓

Gams accepts every kind at the oximo layer, but the actual coverage is the GAMS sub-solver's. Pick one that handles the kind you emit.

🔗Requirements & capabilities

Deployment cost and diagnostic/solve capability per backend.

✓ = yes/supported; — = no.

BackendLicenseSeparate installC compilerDirect interfaceIISWarm startSol. poolDualsParallel
Highs——✓✓—†✓—✓✓
Clarabel———✓—✓—✓✓
Pounce———✓—✓—✓—
Gurobi✓✓—✓✓✓✓✓✓
Mosek✓✓—✓—✓—✓✓
Baron✓✓——✓—✓✓✓
Gams✓✓————✓¶✓✓

Column meanings:

  • 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.
  • Sol. pool: returns multiple solutions, best-first.
  • Duals: shadow prices/reduced costs.
  • Parallel: solver can solve in parallel.

Footnotes:

  • † The highs crate does not support IIS yet.
  • ¶ GAMS's pool and duals come from the underlying sub-solver's GDX output (e.g. CPLEX solnpool).

A backend rejects model kinds it can't handle, so check Model::kind() if a solve returns SolverError::UnsupportedKind.

🔗External solver setup

Gurobi, MOSEK, BARON, and GAMS need software installed outside Cargo. Add the corresponding feature only after that software is installed and licensed.

🔗Gurobi

Enable gurobi, set GUROBI_HOME to the Gurobi installation, and make sure a Gurobi license is active. The backend links through the grb crate.

[dependencies]
oximo = { version = "0.5", features = ["gurobi"] }

🔗MOSEK

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"] }

🔗BARON and GAMS

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"] }

Use features = ["gams"] instead for GAMS.

🔗HiGHS

Highs is bundled by default via the highs Cargo feature. No external install required, but a C/C++ compiler is needed at build time.

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))?;

Common HighsOptions:

MethodEffect
.time_limit(d)Stop after d (std::time::Duration)
.threads(n)Cap parallelism
.mip_gap(g)Relative MIP optimality gap (e.g. 0.01 = 1%)
.verbose(b)Stream the solver log
.method(m)LP algorithm via HighsMethod (Simplex, Ipm, …)

🔗Clarabel

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

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).

🔗Gurobi

Gurobi requires the gurobi Cargo feature plus a licensed Gurobi installation reachable via GUROBI_HOME.

Note: Only Gurobi v12 and later are supported.

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))?;

Common GurobiOptions:

MethodEffect
.time_limit(d)Wall-clock limit
.mip_focus(n)Gurobi MIPFocus (1 = feasibility, 2 = optimality, 3 = bound)
.mip_gap(g)Relative MIP optimality gap
.seed(n)Random seed for reproducible runs

🔗MOSEK

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.

🔗BARON

Baron is a global solver for nonconvex LP/MILP/QP/MIQP/NLP/MINLP. Requires the baron feature and a licensed BARON install on PATH.

use oximo::prelude::*;
use oximo::solvers::Baron;

let result = Baron::new().solve(&m, &BaronOptions::default())?;

🔗GAMS

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.

🔗Next steps

  • 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.

🔗Reading results

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>

🔗Status

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.

🔗Fields and accessors

ItemTypeWhat information is available
terminationTerminationStatusWhy the solver stopped
primal_statusPrimalStatusWhether a usable point is present
objective()Option<f64>Objective of the best solution
value_of(expr)Option<f64>Primal value for a variable or indexed element
values_of(&var)iterator of (&IndexKey, f64)Every element of an indexed variable
dual_of(id)Option<f64>Shadow price (continuous models)
reduced_costsmap keyed by VarIdReduced costs (continuous models)
best_bound, gapOption<f64>Populated by branch-and-bound backends
solve_timeDurationWall-clock time in the backend
iterationsu64Iteration count when the backend reports one
raw_logOption<String>Backend log, when captured

🔗Indexed variables

values_of walks an indexed family without a manual key loop:

for (key, value) in result.values_of(&x) {
    println!("x[{}] = {value:.2}", display_index_key(key));
}

🔗Solution pools

Backends that can return multiple points expose them, best-first:

for i in 0..result.result_count() {
    let point = result.solution(i).unwrap();
    println!("objective {:?}", point.objective);
}

🔗Next steps

  • Solvers: choose a backend and set its options
  • Printing & Debugging: inspect the model that produced a result
  • 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.

🔗Printing

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 y
s.t.
  c1: x + 2 y <= 14
  c2: 3 x - y >= 0
vars
  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

🔗Debugging

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!:

AdapterRenders
display_expr(e)A bare Expr
display_constraint(id)One named constraint
display_objective()The objective
display_soc(id)One second-order cone

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.

🔗Rule-generated names

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.

🔗Parameters show their current binding

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 = 4

m.set_param(price, 7.5);
println!("{m}"); // min 7.5 x ... params: price = 7.5

🔗Checking the model kind

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.

🔗Diagnosing infeasibility

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.

Call compute_iis from the InfeasibilityDiagnosis trait, then render the result against the model with Iis::report.

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));
irreducible infeasible subsystem (2 members)

constraints (2)
  floor
  ceil

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 VarBoundKind Lower/Upper). compute_iis returns a SolverError if the model turns out feasible, so it doubles as an "is this actually infeasible?" assertion.

For more details on IIS support, see Solvers > Requirements & capabilities.

🔗When printing isn't enough

ErrorDebugging steps
Backend rejects the modelCheck Model::kind() against the backend table
Model is too big to readExport it with io::write_lp (if linear)
Solver returns nothing usableInspect termination and primal_status, not just objective()
Model reports infeasibleCompute an IIS (if supported by backend)
Need the backend's own logsSet .verbose(true) in the options, or read result.raw_log

After a solve, result.report(&m) pairs the model with its solution in one print-out.

🔗Next steps

  • 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
  • Inspect the model

🔗To a string

use oximo::io;

let mps = io::to_mps_string(&m)?;
let lp  = io::to_lp_string(&m)?;
let nl  = io::to_nl_string(&m)?;

Each returns a String you can log, hash, or feed into something else in memory.

🔗To a file

use oximo::io;

io::write_mps(&m, "model.mps")?;
io::write_lp(&m, "model.lp")?;
io::write_nl(&m, "model.nl")?;

All three accept anything that implements AsRef<Path>.

🔗Picking a format

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.

FormatProsWhen to pick
MPSUniversal, column-oriented, fixed historical formatMaximum solver compatibility, archival
LPHuman-readable, row-oriented, mirrors algebraic notationQuick inspection, sharing in bug reports
NLCompact, carries nonlinear structureNLP/MINLP models, AMPL-compatible solvers

🔗NL options

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.

🔗Names round-trip

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).

🔗Skipping the writers

If you don't need file export, opt out of the io feature to drop the dependency:

[dependencies]
oximo = { version = "0.5", default-features = false, features = ["highs"] }

Contributing

All contributions are welcome: bug reports, documentation fixes, tests, and code. Everything happens on GitHub.

🔗Reporting bugs

If you hit a bug, unexpected behavior, or an inconsistency, open an issue. Many problems are found by users first, and a report helps everyone.

A useful report includes:

  • A clear description of the problem
  • Steps to reproduce it
  • Expected and actual behavior
  • Relevant error messages or logs
  • A minimal reproducible example, when possible

🔗Improving documentation

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.

Worth sending:

  • Clarifications to existing guides
  • More examples
  • Tutorials
  • Better API explanations
  • Typo and formatting fixes

Open an issue or send a pull request directly.

🔗Contributing code

Bug fixes, performance work, new features, tests, and refactors are all fair game.

  1. Fork the repository
  2. Create a branch for your changes
  3. Make your modifications
  4. Add or update tests when appropriate
  5. Run cargo fmt and make sure cargo clippy passes
  6. Write a commit message following the naming convention
  7. Run the tests relevant to your change
  8. Open a pull request

Keep pull requests focused and reasonably scoped. Smaller ones are easier to review and land.

🔗Testing

Some tests need third-party tools, including commercial solvers. Default features don't depend on any of them.

You are not expected to run tests for crates unrelated to your change. Skip them with --exclude:

cargo test --workspace --exclude oximo-gurobi

A reviewer runs the full suite manually before merging. The GitHub CI workflow can't execute all tests because of solver licensing restrictions.

🔗Commit naming convention

oximo follows a commit naming convention that keeps the history readable and makes the scope of a change obvious at a glance.

ScopePrefix
A single crateoximo-<crate-name>:
Workspace configurationworkspace:
GitHub workflows or repo configgithub:
More than one backendbackends:

Examples:

oximo-core: Improve error handling
workspace: Update dependency versions
github: Add CI workflow for testing
backends: Improve mapping of terminations

🔗AI-assisted contributions

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.

🔗Accountability

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.

🔗Transparency

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.

🔗Licensing

By contributing to oximo you agree that your contributions are licensed under the same dual-license terms as the project:

  • MIT License
  • Apache License 2.0

Unless stated otherwise, all submitted contributions are assumed to be provided under both.

© 2026 oximo