Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
30c275f
oximo-expr: Add ExprClass + classify helper
GermanHeim May 27, 2026
394d9e8
oximo-gams: Add NLP/MINLP support
GermanHeim May 27, 2026
87fab8d
oximo-gurobi: Support NLP/MINLP
GermanHeim May 28, 2026
6b28b26
oximo-gurobi: Update translate to support NLP/MINLP
GermanHeim May 28, 2026
b945e36
oximo-gurobi: Add NLP/MINLP tests
GermanHeim May 28, 2026
846661a
oximo-gurobi: Remove rayon crate
GermanHeim May 28, 2026
e87a3c3
oximo-gurobi: Update README
GermanHeim May 29, 2026
4b445f8
oximo-gams: Update README
GermanHeim May 29, 2026
610d161
oximo-gams: Correctly update comments
GermanHeim May 29, 2026
760d0a4
oximo-gams: Validate GAMS solver
GermanHeim May 29, 2026
4d08dd6
oximo-gams: Update comment
GermanHeim May 29, 2026
44a47d3
oximo-gams: Supported solver types
GermanHeim May 29, 2026
4c96605
oximo-gams: Add tests for solver support
GermanHeim May 30, 2026
acdc356
oximo-gams: Update README
GermanHeim May 30, 2026
ef6f2a3
oximo-expr: Add division operator (Expr/Expr) and constant folding
GermanHeim May 30, 2026
f1d1e4f
oximo-expr: Add Expr/Expr tests
GermanHeim May 30, 2026
2940515
oximo-core: Add Div to non-linear detection
GermanHeim May 30, 2026
36686d9
oximo-core: Add more tests for division detection
GermanHeim May 30, 2026
5718437
oximo-expr: Update README
GermanHeim May 30, 2026
ea1353e
oximo-gams: Support Expr/Expr
GermanHeim May 30, 2026
9cd4ad3
oximo-gurobi: Support Expr/Expr
GermanHeim May 30, 2026
6d909d4
oximo-gurobi: Add tests for Expr/Expr
GermanHeim May 30, 2026
030d8a0
oximo: Add process selection example
GermanHeim May 31, 2026
54cf448
oximo: Update README
GermanHeim May 31, 2026
78a1cd9
oximo-core: Update README
GermanHeim May 31, 2026
39db2ee
oximo-core: Update README
GermanHeim May 31, 2026
d841c17
oximo-expr: Fix tests
GermanHeim May 31, 2026
8fbe30c
oximo: Update example name
GermanHeim May 31, 2026
20e4b59
oximo-expr: Use `ExprClass` to determine model kind
GermanHeim May 31, 2026
0b11fef
oximo-core: Add and update classifier's tests
GermanHeim May 31, 2026
1e6efba
oximo-gams: Update classifier's test
GermanHeim May 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 24 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@
<img src="https://img.shields.io/github/actions/workflow/status/GermanHeim/oximo/ci.yml?branch=main&label=oximo%20CI&logo=github" alt="CI" />
</a>

oximo is a Rust algebraic modeling library for mathematical optimization. Build LP and MILP models with a clean builder API, then solve them with bundled or commercial solvers.

> Support for nonlinear programming (NLP) and mixed-integer nonlinear programming (MINLP) is planned.
oximo is a Rust algebraic modeling library for mathematical optimization. Build LP, MILP, QP/MIQP, NLP, and MINLP models with a clean builder API, then solve them with bundled or commercial solvers.

```rust,no_run
use oximo::prelude::*;
Expand All @@ -37,12 +35,12 @@ println!("y = {:?}", result.value_of(y)); // 4.0

## Features

| Feature | What it adds | Default |
|----------|---------------------------------------------------|---------|
| `highs` | HiGHS LP/MILP solver (bundled, no install) | yes |
| `io` | MPS and LP file writers | yes |
| `gurobi` | Gurobi LP/MILP solver (requires licensed install) | no |
| `gams` | GAMS solver bridge (requires GAMS on PATH) | no |
| Feature | What it adds | Default |
|----------|-----------------------------------------------------------------------|---------|
| `highs` | HiGHS - LP/MILP solver (bundled, no install) | yes |
| `io` | MPS and LP file writers | yes |
| `gurobi` | Gurobi - LP/MILP/QP/MIQP/NLP/MINLP solver (requires licensed install) | no |
| `gams` | GAMS bridge - LP/MILP/QP/MIQP/NLP/MINLP depending on solver | no |

```toml
[dependencies]
Expand Down Expand Up @@ -170,6 +168,23 @@ m.add_constraints_over("supply", &plants, |p: String| {
m.add_constraints_over("c", &set, |k: IndexKey| x[&k].le(1.0));
```

### Nonlinear expressions

`Pow`, `Sin`, `Cos`, `Exp`, `Log`, and bilinear products are first-class. The
model's kind (`LP`/`MILP`/`QP`/`MIQP`/`NLP`/`MINLP`) is inferred from the
expressions.

```rust,ignore
// Rosenbrock NLP
m.minimize((1.0 - x).powi(2) + 100.0 * (y - x.powi(2)).powi(2));

// Quadratic constraint
m.constraint("disk", (x.powi(2) + y.powi(2)).le(1.0));

// Transcendental utility (MINLP when any variable is integer/binary)
m.maximize(sum_over(&items, |i: usize| u[i] * (1.0 + w[i] * x[i]).log()));
```

## Solving

All backends implement the `Solver` trait:
Expand Down
2 changes: 0 additions & 2 deletions crates/oximo-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,6 @@ Inferred automatically from variables and expressions, cached and invalidated on
| `NLP` | All continuous, `Pow`/`Sin`/`Cos`/`Exp`/`Log` |
| `MINLP` | Any integer/binary + nonlinear |

For now, we only support linear constraints, so `QP` and `NLP` are not possible. But the API is designed to allow nonlinear constraints in the future without breaking changes.

## License

MIT OR Apache-2.0
52 changes: 22 additions & 30 deletions crates/oximo-core/src/model.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::cell::{Ref, RefCell};

use oximo_expr::{Expr, ExprArena, VarId};
use oximo_expr::{Expr, ExprArena, ExprClass, VarId, classify};
use rustc_hash::FxHashMap;
use smol_str::SmolStr;

Expand Down Expand Up @@ -269,38 +269,30 @@ impl Model {
}
let arena = self.arena.borrow();
let has_int = self.variables.borrow().iter().any(|v| v.domain.is_integer());
let nonlinear = self.constraints.borrow().iter().any(|c| has_nonlinear(&arena, c.lhs))
|| self.objective.borrow().as_ref().is_some_and(|o| has_nonlinear(&arena, o.expr));
let k = match (has_int, nonlinear) {
(false, false) => ModelKind::LP,
(true, false) => ModelKind::MILP,
(false, true) => ModelKind::NLP,
(true, true) => ModelKind::MINLP,
};
*self.cached_kind.borrow_mut() = Some(k);
k
}
}

fn has_nonlinear(arena: &ExprArena, id: oximo_expr::ExprId) -> bool {
use oximo_expr::ExprNode as N;
match arena.get(id) {
N::Const(_) | N::Var(_) | N::Param(_) | N::Linear { .. } => false,
N::Neg(inner) => has_nonlinear(arena, *inner),
N::Add(children) => children.iter().any(|c| has_nonlinear(arena, *c)),
N::Mul(children) => {
let mut nonconst = 0;
for c in children {
if !matches!(arena.get(*c), N::Const(_)) {
nonconst += 1;
}
if has_nonlinear(arena, *c) {
return true;
}
// Highest expression class across the objective and every constraint
// determines the model class
let mut class = ExprClass::Linear;
if let Some(o) = self.objective.borrow().as_ref() {
class = class.max(classify(&arena, o.expr));
}
for c in self.constraints.borrow().iter() {
if class == ExprClass::Nonlinear {
break;
}
nonconst >= 2
class = class.max(classify(&arena, c.lhs));
}
N::Pow(_, _) | N::Sin(_) | N::Cos(_) | N::Exp(_) | N::Log(_) => true,

let k = match (has_int, class) {
(false, ExprClass::Linear) => ModelKind::LP,
(true, ExprClass::Linear) => ModelKind::MILP,
(false, ExprClass::Quadratic) => ModelKind::QP,
(true, ExprClass::Quadratic) => ModelKind::MIQP,
(false, ExprClass::Nonlinear) => ModelKind::NLP,
(true, ExprClass::Nonlinear) => ModelKind::MINLP,
};
*self.cached_kind.borrow_mut() = Some(k);
k
}
}

Expand Down
41 changes: 40 additions & 1 deletion crates/oximo-core/tests/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,53 @@ fn classifies_milp() {
assert_eq!(m.kind(), ModelKind::MILP);
}

#[test]
fn classifies_qp() {
let m = Model::new("qp");
let x = m.var("x").lb(0.0).build();
m.minimize(x.powi(2));
assert_eq!(m.kind(), ModelKind::QP);
}

#[test]
fn classifies_miqp() {
let m = Model::new("miqp");
let x = m.var("x").lb(0.0).build();
let y = m.var("y").lb(0.0).ub(1.0).integer().build();
// Bilinear term keeps it quadratic, the integer var makes QP -> MIQP.
m.minimize(x * y);
assert_eq!(m.kind(), ModelKind::MIQP);
}

#[test]
fn quadratic_constraint_classifies_qp() {
let m = Model::new("qp_con");
let x = m.var("x").lb(0.0).build();
// Linear objective but a quadratic constraint still makes the model a QP.
m.constraint("c", x.powi(2).le(4.0));
m.minimize(x);
assert_eq!(m.kind(), ModelKind::QP);
}

#[test]
fn classifies_nlp() {
let m = Model::new("nlp");
let x = m.var("x").lb(0.0).build();
m.minimize(x.powi(2));
// Degree-3, so it falls through to the nonlinear path.
m.minimize(x.powi(3));
assert_eq!(m.kind(), ModelKind::NLP);
}

#[test]
fn classifies_minlp_with_division() {
let m = Model::new("minlp_div");
let x = m.var("x").lb(1.0).build();
let y = m.var("y").lb(0.0).ub(1.0).integer().build();
// x / y is nonlinear (non-constant denominator)
m.minimize(x / y);
assert_eq!(m.kind(), ModelKind::MINLP);
}

#[test]
fn variable_count_matches_register() {
let m = Model::new("vars");
Expand Down
5 changes: 4 additions & 1 deletion crates/oximo-expr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Add(Children) // generic n-ary add
Mul(Children) // generic n-ary mul
Neg(ExprId)
Pow(ExprId, ExprId)
Div(ExprId, ExprId) // numerator / denominator
Sin(ExprId) / Cos(ExprId) / Exp(ExprId) / Log(ExprId)
Linear { coeffs: Vec<(VarId, f64)>, constant: f64 } // LP fast-path
```
Expand All @@ -36,13 +37,14 @@ Linear { coeffs: Vec<(VarId, f64)>, constant: f64 } // LP fast-path

## Operator overloads

`Expr` implements `Add`, `Sub`, `Mul`, `Neg` against other `Expr` values and against `f64`. All operations that stay linear produce a `Linear` node. For example:
`Expr` implements `Add`, `Sub`, `Mul`, `Div`, `Neg` against other `Expr` values and against `f64`. All operations that stay linear produce a `Linear` node. For example:

```rust,ignore
// All of these produce a single Linear node, not an Add/Mul tree:
let e = 2.0 * x + 3.0 * y - 1.0;
let e = x + y;
let e = -x;
let e = x / 2.0; // constant denominator: stays linear (x*0.5)
```

## Nonlinear methods on `Expr`
Expand All @@ -55,6 +57,7 @@ expr.sin()
expr.cos()
expr.exp()
expr.log()
expr/expr
```

## Utilities
Expand Down
1 change: 1 addition & 0 deletions crates/oximo-expr/src/arena.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ pub enum ExprNode {
Mul(Children),
Neg(ExprId),
Pow(ExprId, ExprId),
Div(ExprId, ExprId),
Sin(ExprId),
Cos(ExprId),
Exp(ExprId),
Expand Down
Loading