Skip to main content

Crate oximo_core

Crate oximo_core 

Source
Expand description

§oximo-core

Core modeling types for oximo: Model, Variable, Set, Constraint, Objective, Parameter, IndexedVar, Domain, and ModelKind.

Re-exports oximo-expr types (Expr, ExprArena, ExprId, ExprNode, UnaryOp, Children, ParamId, VarId) so downstream code does not need a separate oximo-expr import. End users typically depend on the umbrella oximo crate rather than this one directly.

§Usage

[dependencies]
oximo-core = "0.7.0"

Or via the umbrella crate (recommended for end users):

[dependencies]
oximo = "0.7.0"

§Quick example

use oximo_core::prelude::*;

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

// Scalar variables
variable!(m, x >= 0.0);
variable!(m, 0.0 <= y <= 10.0);

// Constraints (incl. a two-sided range, kept as one constraint)
constraint!(m, c1, x + 2.0 * y <= 14.0);
constraint!(m, c2, 3.0 * x - y >= 0.0);
constraint!(m, band, 1.0 <= x + y <= 12.0);

// Objective (or `objective!(m, Feasibility)` for a pure feasibility problem)
objective!(m, Max, 3.0 * x + 4.0 * y);

println!("kind = {:?}", m.kind()); // LP

§Modeling API

The modeling surface is a set of macros: variable!, constraint!, objective!, sum!, min!, max!, set!, and param!. Each expands to the underlying typed model operations, so there is no runtime cost and full compile-time type/borrow checking is preserved.

Model uses interior mutability, so a macro can take &m, register variables/constraints, and the variable!-introduced bindings (x, y, …) are locals you can use immediately.

let m = Model::new("my_model");
variable!(m, x >= 0.0);        // binds a local `x: Expr<'_>`
constraint!(m, cap, x <= 5.0); // uses x while holding &m

Names are unique per registry. Registering a duplicate variable or constraint name panics.

§Accessors

m.num_variables()   // usize
m.num_constraints() // algebraic + SOC + SOS + indicator constraints
m.variables()       // Ref<'_, Vec<Variable>>
m.constraints()     // ModelConstraints<'_>
m.constraints().algebraic()          // typed algebraic registry
m.constraints().second_order_cones() // typed explicit-SOC registry
m.constraints().special_ordered_sets() // typed SOS registry
m.constraints().indicators()         // typed indicator registry
m.arena()          // ExprArenaSnapshot<'_>
m.kind()           // ModelKind, cached, invalidated on change
m.try_objective()  // Result<Objective, Error>
m.variable_id("x") // Option<VarId>
m.constraint_id("cap")      // Option<ConstraintId> (algebraic)
m.soc_constraint_id("cone") // Option<SocConstraintId>
m.sos_constraint_id("choice") // Option<SosConstraintId>

ModelConstraints::iter() visits every declared constraint. Its algebraic() and second_order_cones() slices are convenient when both kinds are needed in one scope. Typed views keep backend passes homogeneous while preserving the same storage and typed IDs. SOC-shaped quadratic constraints declared with constraint! remain algebraic entries; only constraints declared with soc_constraint! or add_soc_constraint appear in second_order_cones().

§Fixing and unfixing variables

m.fix_var(var_id, 3.0);         // lb = ub = 3.0
m.unfix_var(var_id, 0.0, 10.0); // restore bounds

§Variables

§Scalar variables

variable!(m, x);                        // free (-inf, +inf)
variable!(m, x >= 0.0);                 // lower bound only
variable!(m, 0.0 <= x <= 10.0);         // both bounds
variable!(m, b, Bin);                   // binary {0, 1}  (also Binary)
variable!(m, 0.0 <= n <= 100.0, Int);   // general integer  (also Integer)
variable!(m, s <= 10.0, SemiCont(2.0)); // semicontinuous: 0 or in [2, 10]
variable!(m, t <= 5.0, SemiInt(1.0));   // semi-integer: 0 or integer in [1, 5]

// Keyword args:
variable!(m, u, lb = 0.0, ub = 10.0);    // same as `0.0 <= u <= 10.0`
variable!(m, v, lb = 0.0, domain = Int); // keyword domain (or a positional `Int`)
variable!(m, w, initial = 3.0);          // warm start  (scalar only)
variable!(m, p, fix = 5.0);              // fixed to 5.0 (scalar only)

§Indexed variables

Creates one scalar variable per key in a Set (or range), named base[key], and binds an IndexedVar.

let i = Set::range(0..5);
variable!(m, 0.0 <= x[k in i] <= 10.0);     // uniform bounds
variable!(m, y[k in i] >= 0.0, Int);        // integer family
variable!(m, z[a in rows, b in cols], Bin); // multi-index (Cartesian product)

// Access by key (panics on missing key):
let expr = x[2];  // single key (usize / "name" / (a, b))
let e2 = z[a, b]; // inside the macros: multi-index sugar == z[(&a, &b)]

// Bounds may reference the index -> lowered to per-key bounds:
variable!(m, lower[k] <= w[k in i] <= upper[k]);

// Filtered family: keep only matching keys (no trivial elements built).
variable!(m, d[(i, j) in rc if i == j] >= 0.0);

Large indexed variable, parameter, algebraic, range, SOC, and SOS families are prepared in parallel when the family is large enough, then registered serially in set iteration order.

§Domain

VariantDescription
Domain::RealAny real number (default)
Domain::IntegerAny integer
Domain::Binary0 or 1
Domain::SemiContinuous { threshold }0 or any value >= threshold
Domain::SemiInteger { threshold }0 or any integer >= threshold

§Sets

Set is an ordered finite index set. Three variants:

let i = Set::range(0..5);              // Range: i64 keys 0..5
let j = Set::strings(["a", "b", "c"]); // Strings
let k = Set::product(&i, &j);          // Tuples: (0,"a"), (0,"b"), ...
let k = &i * &j;                       // Same via Mul operator

// From sparse ints:
let s = Set::from_ints([0, 2, 4, 8]);

// Filter:
let evens = i.filter(|k| k.as_i64().unwrap() % 2 == 0);

§Constraints

==, <=, and >= are written directly, the macro intercepts the tokens, so these are real constraint operators.

constraint!(m, name, lhs <= rhs);                  // named, also >= and ==
constraint!(m, lhs >= rhs);                        // anonymous (auto-named _c0, _c1, ...)
constraint!(m, band, 1.0 <= e <= 3.0);             // two-sided range -> one constraint (expr bounds -> band_lo/band_hi)
constraint!(m, name = format!("c_{k}"), e == rhs); // computed run-time name

§Indexed family over a set

// One constraint per key, auto-named supply[seattle], ...
constraint!(m, supply[p in plants], sum!(x[p, q] for q in markets) <= cap[p]);

// Multi-index family (multi-index access sugar: x[i, j]).
constraint!(m, flow[i in 0..n, j in 0..m], x[i, j] >= 0.0);

// Filtered family: only keys passing the guard.
constraint!(m, diag[(i, j) in rc if i == j], x[i, j] <= 1.0);

Capture the macro result to query rows without reconstructing their names:

use oximo_core::prelude::*;

let m = Model::new("constraint_handles");
variable!(m, x[i in 0..4] >= 0.0);
let capacity: ConstraintHandle = constraint!(m, capacity, x[0] <= 10.0);
let cover: IndexedConstraint<usize> =
    constraint!(m, cover[i in 0..4 if i % 2 == 0], x[i] >= 1.0);
assert!(cover.get(1).is_none()); // filtered out
for (i, cid) in cover.iter() {
    assert_eq!(cover.get(i), Some(cid));
}

let bands: IndexedRangeConstraint<usize> =
    constraint!(m, bands[i in 0..4], 1.0 <= x[i] <= 4.0);
let RangeConstraintHandles::Interval(cid) = bands.get(0).unwrap() else {
    panic!("constant bounds and a linear body produce an interval row");
};

get(key) returns copied model-bound handles; iter() yields typed (K, handle) pairs in domain order for dense, sparse, string, tuple, and filtered domains.

Two-sided ranges return RangeConstraintHandles::Interval(handle) or RangeConstraintHandles::Split { lower, upper }. Call .id() on an individual handle when a backend-facing raw ConstraintId is required.

§Summation

sum!(body for k in domain) reads as sum_{k in domain} body. Nest with extra clauses and filter with a trailing if:

constraint!(m, cap, sum!(weights[i] * x[i] for i in items) <= capacity);
objective!(m, Min, sum!(c[i, j] * x[i, j] for i in rows, j in cols));
let evens = sum!(x[i] for i in items if i % 2 == 0); // filtered

Inside constraint!, soc_constraint!, and objective!, sums inherit the model’s expression context. Empty domains and filters produce the constant expression 0, including in indexed families and nested sums.

For a standalone sum, pass the model explicitly when the domain or filter can be empty:

let total = sum!(m, x[i] for i in possibly_empty_items);
let filtered = sum!(m, x[i] for i in items if predicate(i));
// Both produce the constant expression 0 when no term is selected.

§Special ordered sets

SOS1 and SOS2 constraints apply ordering weights to bare variables. SOS1 allows at most one nonzero member. SOS2 allows at most two adjacent nonzero members. Weights must be finite and unique within each set.

variable!(m, 0.0 <= choice_a <= 1.0);
variable!(m, 0.0 <= choice_b <= 1.0);
variable!(m, 0.0 <= choice_c <= 1.0);

let one_choice = sos_constraint!(m, one_choice, SOS1, [choice_a, choice_b, choice_c]);
sos_constraint!(m, adjacent_choice, SOS2, [choice_a, choice_b, choice_c]);
sos_constraint!(m, weighted, SOS2, [
    (choice_a, 10.0), (choice_b, 20.0), (choice_c, 30.0),
]);

The indexed form creates one set for every key in the binder. The binder is required because the macro cannot infer an index domain from an IndexedVar: choice[i in 0..n] produces choice[0], …, choice[n - 1]. To create one SOS over a dynamically assembled collection, use m.add_sos_constraint_auto_weights("choice", SosType::Sos1, members).

The single-constraint form returns a model-bound SosConstraintHandle. Backends without native SOS support return an error. Reformulate one constraint through its handle, or every active SOS in a model explicitly:

let transformed = one_choice.to_reformulated_model(SosReformulationOptions::default())?;
solver.solve(&transformed, &options)?;

let transformed = m.to_reformulated_sos_model(
    SosReformulationOptions::default().with_fallback_big_m(1.0e6),
)?;

// Reformulate every active SOS in place when the native form is no longer needed.
let artifacts = m.reformulate_sos(
    SosReformulationOptions::default().with_fallback_big_m(1.0e6),
)?;
solver.solve(&m, &options)?;

The to_reformulated_* methods produce an independent model. reformulate* methods modify the source model.

All forms preserve original IDs, append binary variables and linear rows, and retain the source SOS entries as inactive provenance. Finite variable bounds are used as member-specific Big-M values. An infinite bound is an error unless the caller explicitly supplies a positive finite fallback. A fallback that is too small can truncate the feasible region.

Prefer solving the original model with a backend that supports native SOS. Reformulate when the selected backend lacks that capability or when you intentionally need a MILP representation. In-place reformulation first validates every active set, then appends all generated artifacts without copying the source model.

Because the generated rows embed the current member bounds, those bounds cannot be changed on a reformulated model. Change bounds before reformulating, or produce a fresh reformulated copy after changing the source model.

§Indicator constraints

An indicator applies an affine row only when an existing binary variable has the selected value. The implication is one-way:

variable!(m, enabled, Binary);
variable!(m, x);
indicator_constraint!(m, capacity, enabled == 1 => x <= 10.0);
indicator_constraint!(m, shutdown, enabled == 0 => x == 0.0);
indicator_constraint!(m, band, enabled == 1 => -2.0 <= x <= 4.0);

The trigger must be a bare binary variable from the same model and the consequent must be affine. Native support is available in Gurobi and MOSEK; GAMS requires an explicitly selected COPT, CPLEX, Gurobi, SCIP, or Xpress subsolver.

Backends without native indicator support reject active indicators. Convert them explicitly with reformulate_indicators, or use to_reformulated_indicator_model when changing the source model isn’t intended.

m.reformulate_indicators(IndicatorReformulationOptions::default())?;
highs.solve(&m, &options)?;

§Second-order cone constraints

soc_constraint! registers ||terms||_2 <= bound. Every term and the bound must be affine. The model classifies as SOCP/MISOCP.

soc_constraint!(m, cone, [x, y] <= t);                       // named -> SocConstraintHandle
soc_constraint!(m, [x - y, 2.0 * y] <= t + 1.0);             // anonymous (auto-named _soc0, ...)
soc_constraint!(m, name = format!("c_{k}"), [x] <= t);       // computed run-time name
soc_constraint!(m, risk[i in assets], [s[i] * w[i]] <= cap); // family: risk[key] per key

The method form m.add_soc_constraint("cone", [x, y], t) is equivalent.

§Objectives

objective!(m, Min, cost_expr);
objective!(m, Max, revenue_expr);

Nonlinear expressions use Expr methods such as sqrt, exp2, log1p, atan2, min, and max. min!/max! provide the same indexed-domain syntax as sum! and preserve deterministic child order.

§Parameters

param!(m, rate = 0.05);     // binds a re-bindable `rate: Expr<'_>`
rate.set_param_value(0.07); // change between solves without rebuilding

§Indexed parameters

Mirror indexed variables: one re-bindable scalar parameter per key, bound as an IndexedParam. The right-hand side is evaluated per key and may reference the index.

let items = Set::range(0..3);
param!(m, cost[i in items] = base_cost[i]); // one parameter per key
param!(m, w[(i, j) in rc] = weight(i, j));  // multi-index
param!(m, c[p in plants] = price[p]);       // string-keyed (sparse)

let unit = cost[1];             // index for a param `Expr`
cost[1].set_param_value(9.0);   // re-bind one entry via its handle
m.set_param_idx(&cost, 1, 9.0)?; // ...or by key on the model
m.param_value_idx(&cost, 1);    // -> Some(9.0)

§Model kind

Inferred automatically from variables and expressions, cached and invalidated on change. The decision ladder runs top-down. Any integer/binary variable picks the MI* variant of the row that matches:

Kind (continuous/integer)Conditions
NLP/MINLPAny nonlinear expression (degree > 2, transcendentals, division)
QCP/MIQCPAny quadratic constraint not recognized as a second-order cone
SOCP/MISOCPSecond-order cones present (explicit or detected)
QP/MIQPQuadratic objective, linear constraints
LP/MILPEverything linear

§License

MIT OR Apache-2.0

Re-exports§

pub use constraint::Constraint;
pub use constraint::ConstraintExpr;
pub use constraint::ConstraintHandle;
pub use constraint::ConstraintId;
pub use constraint::IntoRhs;
pub use constraint::RangeConstraintHandles;
pub use constraint::RangeConstraintIds;
pub use constraint::Relate;
pub use constraint::Sense;
pub use display::ConstraintDisplay;
pub use display::ExprDisplay;
pub use display::IndicatorDisplay;
pub use display::ObjectiveDisplay;
pub use display::SocDisplay;
pub use display::SosDisplay;
pub use domain::Domain;
pub use error::Error;
pub use error::Result;
pub use indexed::IndexedConstraint;
pub use indexed::IndexedIndicatorConstraint;
pub use indexed::IndexedParam;
pub use indexed::IndexedRangeConstraint;
pub use indexed::IndexedRangeIndicatorConstraint;
pub use indexed::IndexedVar;
pub use indicator::IndicatorConstraint;
pub use indicator::IndicatorConstraintHandle;
pub use indicator::IndicatorConstraintId;
pub use indicator::RangeIndicatorConstraintHandles;
pub use indicator::RangeIndicatorConstraintIds;
pub use model::ConstraintRef;
pub use model::IndexedVarBuilder;
pub use model::Model;
pub use model::ModelConstraints;
pub use model::ModelKind;
pub use model::display_index_key;
pub use objective::Objective;
pub use objective::ObjectiveSense;
pub use param::Parameter;
pub use reformulation::IndicatorReformulationArtifacts;
pub use reformulation::IndicatorReformulationOptions;
pub use reformulation::ReformulatedModel;
pub use reformulation::ReformulationError;
pub use reformulation::SosReformulationArtifacts;
pub use reformulation::SosReformulationOptions;
pub use set::Axis;
pub use set::FromIndexKey;
pub use set::FromIndexKeyRef;
pub use set::IndexKey;
pub use set::IndexKeyRef;
pub use set::IndexTuple;
pub use set::KeyCat;
pub use set::ScalarKey;
pub use set::Set;
pub use set::SetIter;
pub use soc::SocConstraint;
pub use soc::SocConstraintHandle;
pub use soc::SocConstraintId;
pub use soc::SocForm;
pub use sos::SosConstraint;
pub use sos::SosConstraintHandle;
pub use sos::SosConstraintId;
pub use sos::SosMember;
pub use sos::SosType;
pub use sum::SumDomain;
pub use var::VarBuilder;
pub use var::Variable;
pub use var::var_name;

Modules§

constraint
display
Human-readable rendering of models, constraints, objectives, and expressions.
domain
error
indexed
indicator
model
objective
param
prelude
reformulation
Explicit, solver-independent model reformulations.
set
soc
sos
sum
var

Macros§

constraint
constraint!(model, [name|name[idx]], lhs <op> rhs), register a constraint, an auto-named anonymous constraint, or an indexed family of constraints.
indicator_constraint
Register a native binary-triggered affine constraint. indicator_constraint!(model, [name|name[idx]], binary == 0|1 => relation).
max
max!(body for pat in domain[, pat in domain ...][ if cond]), an indexed maximum.
min
min!(body for pat in domain[, pat in domain ...][ if cond]), an indexed minimum.
objective
objective!(model, Min|Max, expr), set the model objective and sense. objective!(model, Feasibility) (also feasibility/feas) declares a feasibility problem with no objective to optimize.
param
param!(model, name = value), declare a re-bindable scalar parameter and bind it to a local of the same name.
set
set!(name = domain), bind a local to an index Set. A plain right side (0..5, a * b) is normalized to an owned set (a top-level * is a borrowing Cartesian product). A pat in domain[ if cond] comprehension builds (and optionally filters) the set. See the crate docs.
soc_constraint
soc_constraint!(model, [name|name = expr|name[idx]], [terms] <= bound), register the second-order cone constraint ||terms||_2 <= bound (every term and the bound must be affine), an auto-named anonymous cone, or an indexed family of cones.
sos_constraint
sos_constraint!(model, [name|name[idx]], SOS1|SOS2, [var, ...]). Explicit weights can be supplied as [(var, weight), ...]. The short form assigns consecutive weights starting at one.
sum
sum!(body for pat in domain[, pat in domain ...]), algebraic summation, lowered to nested sum_over folds. To allow an empty domain (or an empty filter), anchor the sum to its model: sum!(model, body for pat in domain). Sums written inside constraint!, soc_constraint!, objective!, or an anchored sum inherit its expression context automatically. Selected terms must belong to that model. Standalone unanchored sums require a first term.
variable
variable!(model, spec), declare a decision variable (or an indexed family) and bind it to a local of the same name. See the crate docs for the grammar.

Structs§

Expr
Lightweight handle to a node in an ExprArenaCell.
ExprArena
ExprArenaCell
Parallel-safe owner used by crate::Expr handles.
ExprArenaSnapshot
Immutable copy-on-write snapshot of an ExprArenaCell.
ExprArenaWriteGuard
ExprId
ModelId
Stable identity shared by a model, its expression handles, and its results.
ModelMismatchError
An expression, indexed family, model, or result came from another model.
ParamId
VarId

Enums§

EvalError
ExprNode
Here we use a linear fast-path: sum(coeff * var) + constant. Built by the operator overloads when all children are linear, so LP/MILP construction never walks an Add(Mul(Const, Var), ...) tree.
UnaryOp
Unary scalar functions represented by ExprNode::Unary.

Functions§

describe_nonlinear_term
Render the first nonlinear summand of id as a short infix string, resolving each VarId to a display name via resolve. Returns None when id is fully affine (no nonlinear residual).
dot
Dot product of expressions with scalar coefficients: sum_{i} c_i * e_i.
render_expr
Render id as a canonical infix string, resolving each VarId to a display name via resolve. The linear part first, then any nonlinear residual summands. Signs are folded into the joins, and a value-less expression is rendered as 0.
render_linear_terms
Render a LinearTerms snapshot by:

Type Aliases§

Children