Skip to content

Commit

Permalink
Replaces minilp with microlp (#65)
Browse files Browse the repository at this point in the history
* add clarabel wasm feature flag

* Replace minilp with microlp
  • Loading branch information
Specy authored Nov 3, 2024
1 parent 32687d6 commit e03b02a
Show file tree
Hide file tree
Showing 8 changed files with 75 additions and 74 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ jobs:
- name: Run tests with all default solvers (no cplex)
# test on a single thread. See: https://github.com/KardinalAI/coin_cbc/issues/9
run: cargo test --features all_default_solvers -- --test-threads=1
- name: Run tests with minilp
run: cargo test --no-default-features --features minilp
- name: Run tests with microlp
run: cargo test --no-default-features --features microlp
- name: Run tests with lpsolve
run: cargo test --no-default-features --features lpsolve
- name: Run tests with highs
Expand All @@ -64,6 +64,6 @@ jobs:
run: cargo test --no-default-features --features clarabel
- name: Run tests with Clarabel on WASM
run: wasm-pack test --node --no-default-features --features "clarabel,clarabel-wasm"
- name: Run tests with minilp on WASM
run: wasm-pack test --node --no-default-features --features "minilp"
- name: Run tests with microlp on WASM
run: wasm-pack test --node --no-default-features --features microlp
- run: cargo bench
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Adding a new solver should not take more than a few hundred lines of code, tests

- add the solver as an optional dependency in `Cargo.toml`
- add a file named after your solver in the [solvers](./src/solvers) folder
- you can copy minilp, our smallest solver interface, as a starting point.
- you can copy microlp, our smallest solver interface, as a starting point.
- create a struct to store linear problems in a way that will make it cheap to dynamically add new constraints to the problem,
and easy to pass the problem to the solver once it has been fully constructed.
This generally means constructing vectors to which you can push values for each new constraint.
Expand Down
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ categories = ["mathematics", "algorithms", "science", "api-bindings", "data-stru
default = ["coin_cbc", "singlethread-cbc"]
singlethread-cbc = ["coin_cbc?/singlethread-cbc"]
scip = ["russcip"]
all_default_solvers = ["coin_cbc", "minilp", "lpsolve", "highs", "russcip", "lp-solvers", "clarabel"] # cplex-rs is not included because it is incompatible with lpsolve
all_default_solvers = ["coin_cbc", "microlp", "lpsolve", "highs", "russcip", "lp-solvers", "clarabel"] # cplex-rs is not included because it is incompatible with lpsolve
clarabel-wasm = ["clarabel/wasm"]

[dependencies]
coin_cbc = { version = "0.1", optional = true, default-features = false }
minilp = { version = "0.2", optional = true }
microlp = { version = "0.2.4", optional = true }
lpsolve = { version = "0.1", optional = true }
highs = { version = "1.5.0", optional = true }
russcip = { version = "0.3.4", optional = true }
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ However, it is currently very slow with large problems.

You can also directly use the underlying solver libraries, such as
[coin_cbc](https://docs.rs/coin_cbc/) or
[minilp](https://crates.io/crates/minilp)
[microlp](https://crates.io/crates/microlp)
if you don't need a way to express your objective function and
constraints using an idiomatic rust syntax.

Expand All @@ -72,7 +72,7 @@ you can also activate other solvers using cargo features.
| [`coin_cbc`][cbc] ||||||
| [`highs`][highs] |||\+ |||
| [`lpsolve`][lpsolve] ||||||
| [`minilp`][minilp] ||||||
| [`microlp`][microlp] ||||||
| [`lp-solvers`][lps] ||||||
| [`scip`][scip] ||||||
| [`cplex-rs`][cplex] |||\+\+ |||
Expand Down Expand Up @@ -116,13 +116,13 @@ option. Otherwise, using Cbc from multiple threads would be unsafe.

[cbc]: https://www.coin-or.org/Cbc/

### [minilp](https://docs.rs/minilp)
### [microlp](https://docs.rs/microlp)

minilp is a pure rust solver, which means it works out of the box without installing anything else.
Microlp is a fork of [minilp](https://docs.rs/minilp), a pure rust solver, which means it works out of the box without installing anything else.

[minilp]: https://docs.rs/minilp
[microlp]: https://docs.rs/microlp

Minilp is written in pure rust, so you can use it without having to install a C compiler on your machine,
Microlp is written in pure rust, so you can use it without having to install a C compiler on your machine,
or having to install any external library, but it is slower than other solvers.

It performs very poorly when compiled in debug mode, so be sure to compile your code
Expand Down
28 changes: 14 additions & 14 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ pub use constraint::Constraint;
pub use expression::Expression;
#[cfg(not(any(
feature = "coin_cbc",
feature = "minilp",
feature = "microlp",
feature = "lpsolve",
feature = "highs",
feature = "scip",
Expand All @@ -90,7 +90,7 @@ pub use solvers::coin_cbc::coin_cbc;
pub use solvers::coin_cbc::coin_cbc as default_solver;
#[cfg(not(any(
feature = "coin_cbc",
feature = "minilp",
feature = "microlp",
feature = "lpsolve",
feature = "highs"
)))]
Expand All @@ -99,7 +99,7 @@ pub use solvers::cplex::cplex as default_solver;
#[cfg(feature = "highs")]
#[cfg_attr(docsrs, doc(cfg(feature = "highs")))]
pub use solvers::highs::highs;
#[cfg(not(any(feature = "coin_cbc", feature = "minilp", feature = "lpsolve")))]
#[cfg(not(any(feature = "coin_cbc", feature = "microlp", feature = "lpsolve")))]
#[cfg(feature = "highs")]
/// When the "highs" cargo feature is present, highs is used as the default solver
pub use solvers::highs::highs as default_solver;
Expand All @@ -109,23 +109,23 @@ pub use solvers::lp_solvers::LpSolver;
#[cfg(feature = "lpsolve")]
#[cfg_attr(docsrs, doc(cfg(feature = "lpsolve")))]
pub use solvers::lpsolve::lp_solve;
#[cfg(not(any(feature = "coin_cbc", feature = "minilp")))]
#[cfg(not(any(feature = "coin_cbc", feature = "microlp")))]
#[cfg(feature = "lpsolve")]
/// When the "lpsolve" cargo feature is present, lpsolve is used as the default solver
pub use solvers::lpsolve::lp_solve as default_solver;
#[cfg(feature = "minilp")]
#[cfg_attr(docsrs, doc(cfg(feature = "minilp")))]
pub use solvers::minilp::minilp;
#[cfg(feature = "microlp")]
#[cfg_attr(docsrs, doc(cfg(feature = "microlp")))]
pub use solvers::microlp::microlp;
#[cfg(not(feature = "coin_cbc"))]
#[cfg(feature = "minilp")]
/// When the "coin_cbc" cargo feature is absent, minilp is used as the default solver
pub use solvers::minilp::minilp as default_solver;
#[cfg(feature = "microlp")]
/// When the "coin_cbc" cargo feature is absent, microlp is used as the default solver
pub use solvers::microlp::microlp as default_solver;
#[cfg(feature = "scip")]
#[cfg_attr(docsrs, doc(cfg(feature = "highs")))]
pub use solvers::scip::scip;
#[cfg(not(any(
feature = "coin_cbc",
feature = "minilp",
feature = "microlp",
feature = "lpsolve",
feature = "highs"
)))]
Expand All @@ -140,7 +140,7 @@ pub use variable::{variable, ProblemVariables, Variable, VariableDefinition};

#[cfg(not(any(
feature = "coin_cbc",
feature = "minilp",
feature = "microlp",
feature = "lpsolve",
feature = "highs",
feature = "scip",
Expand All @@ -154,7 +154,7 @@ pub const default_solver: LpSolver<

#[cfg(not(any(
feature = "coin_cbc",
feature = "minilp",
feature = "microlp",
feature = "lpsolve",
feature = "highs",
feature = "lp-solvers",
Expand All @@ -167,7 +167,7 @@ compile_error!(
You need to activate at least one solver feature flag in good_lp. \
You can do by adding the following to your Cargo.toml :
[dependencies]
good_lp = { version = \"*\", features = [\"minilp\"] }
good_lp = { version = \"*\", features = [\"microlp\"] }
"
);

Expand Down
83 changes: 42 additions & 41 deletions src/solvers/minilp.rs → src/solvers/microlp.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
//! A solver that uses [minilp](https://docs.rs/minilp), a pure rust solver.
//! A solver that uses [microlp](https://docs.rs/microlp), a pure rust solver.
use std::panic::catch_unwind;

use minilp::Error;
use microlp::Error;

use crate::variable::{UnsolvedProblem, VariableDefinition};
use crate::{
Expand All @@ -11,20 +11,20 @@ use crate::{
};
use crate::{Constraint, Variable};

/// The [minilp](https://docs.rs/minilp) solver,
/// The [microlp](https://docs.rs/microlp) solver,
/// to be used with [UnsolvedProblem::using].
pub fn minilp(to_solve: UnsolvedProblem) -> MiniLpProblem {
pub fn microlp(to_solve: UnsolvedProblem) -> MicroLpProblem {
let UnsolvedProblem {
objective,
direction,
variables,
} = to_solve;
let mut problem = minilp::Problem::new(match direction {
ObjectiveDirection::Maximisation => minilp::OptimizationDirection::Maximize,
ObjectiveDirection::Minimisation => minilp::OptimizationDirection::Minimize,
let mut problem = microlp::Problem::new(match direction {
ObjectiveDirection::Maximisation => microlp::OptimizationDirection::Maximize,
ObjectiveDirection::Minimisation => microlp::OptimizationDirection::Minimize,
});
let mut integers: Vec<minilp::Variable> = vec![];
let variables: Vec<minilp::Variable> = variables
let mut integers: Vec<microlp::Variable> = vec![];
let variables: Vec<microlp::Variable> = variables
.iter_variables_with_def()
.map(
|(
Expand All @@ -45,41 +45,41 @@ pub fn minilp(to_solve: UnsolvedProblem) -> MiniLpProblem {
},
)
.collect();
MiniLpProblem {
MicroLpProblem {
problem,
variables,
integers,
n_constraints: 0,
}
}

/// A minilp model
pub struct MiniLpProblem {
problem: minilp::Problem,
variables: Vec<minilp::Variable>,
integers: Vec<minilp::Variable>,
/// A microlp model
pub struct MicroLpProblem {
problem: microlp::Problem,
variables: Vec<microlp::Variable>,
integers: Vec<microlp::Variable>,
n_constraints: usize,
}

impl MiniLpProblem {
/// Get the inner minilp model
pub fn as_inner(&self) -> &minilp::Problem {
impl MicroLpProblem {
/// Get the inner microlp model
pub fn as_inner(&self) -> &microlp::Problem {
&self.problem
}
}

impl SolverModel for MiniLpProblem {
type Solution = MiniLpSolution;
impl SolverModel for MicroLpProblem {
type Solution = MicroLpSolution;
type Error = ResolutionError;

fn solve(self) -> Result<Self::Solution, Self::Error> {
let mut solution = self.problem.solve()?;
for int_var in self.integers {
solution = catch_unwind(|| solution.add_gomory_cut(int_var)).map_err(|_| {
ResolutionError::Other("minilp does not support integer variables")
ResolutionError::Other("microlp does not support integer variables")
})??;
}
Ok(MiniLpSolution {
Ok(MicroLpSolution {
solution,
variables: self.variables,
})
Expand All @@ -88,11 +88,11 @@ impl SolverModel for MiniLpProblem {
fn add_constraint(&mut self, constraint: Constraint) -> ConstraintReference {
let index = self.n_constraints;
let op = match constraint.is_equality {
true => minilp::ComparisonOp::Eq,
false => minilp::ComparisonOp::Le,
true => microlp::ComparisonOp::Eq,
false => microlp::ComparisonOp::Le,
};
let constant = -constraint.expression.constant;
let mut linear_expr = minilp::LinearExpr::empty();
let mut linear_expr = microlp::LinearExpr::empty();
for (var, coefficient) in constraint.expression.linear.coefficients {
linear_expr.add(self.variables[var.index()], coefficient);
}
Expand All @@ -102,33 +102,34 @@ impl SolverModel for MiniLpProblem {
}

fn name() -> &'static str {
"Minilp"
"Microlp"
}
}

impl From<minilp::Error> for ResolutionError {
fn from(minilp_error: Error) -> Self {
match minilp_error {
minilp::Error::Unbounded => Self::Unbounded,
minilp::Error::Infeasible => Self::Infeasible,
impl From<microlp::Error> for ResolutionError {
fn from(microlp_error: Error) -> Self {
match microlp_error {
microlp::Error::Unbounded => Self::Unbounded,
microlp::Error::Infeasible => Self::Infeasible,
microlp::Error::InternalError(s) => Self::Str(s),
}
}
}

/// The solution to a minilp problem
pub struct MiniLpSolution {
solution: minilp::Solution,
variables: Vec<minilp::Variable>,
/// The solution to a microlp problem
pub struct MicroLpSolution {
solution: microlp::Solution,
variables: Vec<microlp::Variable>,
}

impl MiniLpSolution {
/// Returns the MiniLP solution object. You can use it to dynamically add new constraints
pub fn into_inner(self) -> minilp::Solution {
impl MicroLpSolution {
/// Returns the MicroLP solution object. You can use it to dynamically add new constraints
pub fn into_inner(self) -> microlp::Solution {
self.solution
}
}

impl Solution for MiniLpSolution {
impl Solution for MicroLpSolution {
fn value(&self, variable: Variable) -> f64 {
self.solution[self.variables[variable.index()]]
}
Expand All @@ -138,7 +139,7 @@ impl Solution for MiniLpSolution {
mod tests {
use crate::{variable, variables, Solution, SolverModel};

use super::minilp;
use super::microlp;

#[test]
fn can_solve_easy() {
Expand All @@ -147,7 +148,7 @@ mod tests {
let y = vars.add(variable().clamp(1, 3));
let solution = vars
.maximise(x + y)
.using(minilp)
.using(microlp)
.with((2 * x + y) << 4)
.solve()
.unwrap();
Expand Down
8 changes: 4 additions & 4 deletions src/solvers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ pub mod cplex;
#[cfg_attr(docsrs, doc(cfg(feature = "coin_cbc")))]
pub mod coin_cbc;

#[cfg(feature = "minilp")]
#[cfg_attr(docsrs, doc(cfg(feature = "minilp")))]
pub mod minilp;
#[cfg(feature = "microlp")]
#[cfg_attr(docsrs, doc(cfg(feature = "microlp")))]
pub mod microlp;

#[cfg(feature = "lpsolve")]
#[cfg_attr(docsrs, doc(cfg(feature = "lpsolve")))]
Expand Down Expand Up @@ -259,7 +259,7 @@ pub trait DualValues {
/// # // These solvers do not support dual values
/// # #[cfg(not(any(
/// # feature = "coin_cbc",
/// # feature = "minilp",
/// # feature = "microlp",
/// # feature = "lpsolve",
/// # feature = "lp-solvers",
/// # feature = "scip",
Expand Down
4 changes: 2 additions & 2 deletions src/variable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ impl VariableDefinition {
/// # use good_lp::{ProblemVariables, variable, default_solver, SolverModel, Solution};
/// let mut problem = ProblemVariables::new();
/// let x = problem.add(variable().integer().min(0).max(2.5));
/// # if cfg!(not(any(feature = "minilp", feature="clarabel"))) {
/// # if cfg!(not(any(feature = "microlp", feature="clarabel"))) {
/// let solution = problem.maximise(x).using(default_solver).solve().unwrap();
/// // x is bound to [0; 2.5], but the solution is x=2 because x needs to be an integer
/// assert_eq!(solution.value(x), 2.);
Expand All @@ -164,7 +164,7 @@ impl VariableDefinition {
/// let mut problem = ProblemVariables::new();
/// let x = problem.add(variable().binary());
/// let y = problem.add(variable().binary());
/// if cfg!(not(any(feature = "minilp", feature="clarabel"))) {
/// if cfg!(not(any(feature = "microlp", feature="clarabel"))) {
/// let solution = problem.maximise(x + y).using(default_solver).solve().unwrap();
/// assert_eq!(solution.value(x), 1.);
/// assert_eq!(solution.value(y), 1.);
Expand Down

0 comments on commit e03b02a

Please sign in to comment.