Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: time limits and solution statuses #89

Draft
wants to merge 24 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
8cc4111
feat: define time limit
KnorpelSenf Feb 27, 2025
366aeb1
feat: update clarabel
KnorpelSenf Feb 27, 2025
810a6b0
feat: support time limits in cbc
KnorpelSenf Feb 27, 2025
6d4870a
feat: support time limits in highs
KnorpelSenf Feb 27, 2025
620a060
feat: update lp_solvers
KnorpelSenf Feb 27, 2025
0fac9cd
feat: update lp_solve
KnorpelSenf Feb 27, 2025
e0fbd27
feat: update microlp
KnorpelSenf Feb 27, 2025
3e4cd71
feat: support time limits in scip
KnorpelSenf Feb 27, 2025
8d7fdce
Merge branch 'main' into time-limits
KnorpelSenf Feb 28, 2025
8e2f7c3
style: dedupe import
KnorpelSenf Feb 28, 2025
ee45134
style: fix fmt
KnorpelSenf Feb 28, 2025
1d0c35d
fix: cplex build
KnorpelSenf Feb 28, 2025
eec47c8
test: solve actual problems for SCIP tests
KnorpelSenf Feb 28, 2025
c4bc8ac
style: rename SCIP test
KnorpelSenf Feb 28, 2025
69ab613
test: solve actual problems for SCIP tests
KnorpelSenf Feb 28, 2025
37d837a
style: align CBC test imports
KnorpelSenf Feb 28, 2025
6da5416
fix: set correct status for CBC gap limit
KnorpelSenf Feb 28, 2025
916713c
fix: handle CBC result status correctly
KnorpelSenf Feb 28, 2025
4c0648d
test: add SAT test case for CBC
KnorpelSenf Feb 28, 2025
eb263e6
style: inline var
KnorpelSenf Feb 28, 2025
d867e77
style: extract var count to const
KnorpelSenf Feb 28, 2025
66662fb
build: temporarily switch to highs with mip gap
KnorpelSenf Mar 1, 2025
e0e9be2
fix: set gap limit
KnorpelSenf Mar 1, 2025
7c983b2
build: switch to rust-or's highs repo
KnorpelSenf Mar 1, 2025
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
5 changes: 3 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,9 @@ pub use solvers::scip::scip;
pub use solvers::scip::scip as default_solver;

pub use solvers::{
solver_name, DualValues, ModelWithSOS1, ResolutionError, Solution, SolutionWithDual, Solver,
SolverModel, StaticSolver, WithInitialSolution, WithMipGap,
solver_name, DualValues, ModelWithSOS1, ResolutionError, Solution, SolutionStatus,
SolutionWithDual, Solver, SolverModel, StaticSolver, WithInitialSolution, WithMipGap,
WithTimeLimit,
};
pub use variable::{variable, ProblemVariables, Variable, VariableDefinition};

Expand Down
4 changes: 4 additions & 0 deletions src/solvers/clarabel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::variable::UnsolvedProblem;
use crate::{
constraint::ConstraintReference,
solvers::{ObjectiveDirection, ResolutionError, Solution, SolverModel},
SolutionStatus,
};
use crate::{Constraint, DualValues, SolutionWithDual, Variable};

Expand Down Expand Up @@ -163,6 +164,9 @@ impl ClarabelSolution {
}

impl Solution for ClarabelSolution {
fn status(&self) -> SolutionStatus {
SolutionStatus::Optimal
}
fn value(&self, variable: Variable) -> f64 {
self.solution.x[variable.index()]
}
Expand Down
50 changes: 47 additions & 3 deletions src/solvers/coin_cbc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ use std::convert::TryInto;

use coin_cbc::{raw::Status, Col, Model, Sense, Solution as CbcSolution};

use crate::solvers::{MipGapError, ModelWithSOS1, WithInitialSolution, WithMipGap};
use crate::solvers::{
MipGapError, ModelWithSOS1, SolutionStatus, WithInitialSolution, WithMipGap, WithTimeLimit,
};
use crate::variable::{UnsolvedProblem, VariableDefinition};
use crate::{
constraint::ConstraintReference,
Expand Down Expand Up @@ -137,7 +139,14 @@ impl SolverModel for CoinCbcProblem {
let solution = self.model.solve();
let raw = solution.raw();
match raw.status() {
Status::Stopped => Err(ResolutionError::Other("Stopped")),
Status::Stopped => {
if raw.is_seconds_limit_reached() {
let solution_vec = solution.raw().col_solution().into();
Ok(CoinCbcSolution{ status: SolutionStatus::TimeLimit, solution, solution_vec })
} else {
Err(ResolutionError::Other("Stopped"))
}
},
Status::Abandoned => Err(ResolutionError::Other("Abandoned")),
Status::UserEvent => Err(ResolutionError::Other("UserEvent")),
Status::Finished // The optimization finished, but may not have found a solution
Expand All @@ -150,6 +159,7 @@ impl SolverModel for CoinCbcProblem {
} else {
let solution_vec = solution.raw().col_solution().into();
Ok(CoinCbcSolution {
status: SolutionStatus::Optimal,
solution,
solution_vec,
})
Expand Down Expand Up @@ -191,6 +201,14 @@ impl WithInitialSolution for CoinCbcProblem {
}
}

impl WithTimeLimit for CoinCbcProblem {
fn with_time_limit<T: Into<f64>>(mut self, seconds: T) -> Self {
self.model
.set_parameter("sec", &(seconds.into().ceil() as usize).to_string());
self
}
}

/// Unfortunately, the current version of cbc silently ignores
/// sos constraints on continuous variables.
/// See <https://github.com/coin-or/Cbc/issues/376>
Expand All @@ -209,6 +227,7 @@ impl ModelWithSOS1 for CoinCbcProblem {

/// A coin-cbc problem solution
pub struct CoinCbcSolution {
status: SolutionStatus,
solution: CbcSolution,
solution_vec: Vec<f64>, // See: rust-or/good_lp#6
}
Expand All @@ -221,6 +240,9 @@ impl CoinCbcSolution {
}

impl Solution for CoinCbcSolution {
fn status(&self) -> SolutionStatus {
self.status
}
fn value(&self, variable: Variable) -> f64 {
// Our indices should always match those of cbc
self.solution_vec[variable.index()]
Expand All @@ -246,9 +268,31 @@ impl WithMipGap for CoinCbcProblem {

#[cfg(test)]
mod tests {
use crate::{variable, variables, Solution, SolverModel, WithInitialSolution};
use crate::{
solvers::{SolutionStatus, WithTimeLimit},
variable, variables, Expression, Solution, SolverModel, Variable, WithInitialSolution,
};
use float_eq::assert_float_eq;

#[test]
fn solve_problem_with_time_limit() {
let n = 10;
let mut vars = variables!();
let mut v = Vec::with_capacity(n);
for _ in 0..n {
v.push(vars.add(variable().binary()));
}
let pb = vars
.maximise(v.iter().map(|&v| 3.5 * v).sum::<Expression>())
.using(super::coin_cbc)
.with_time_limit(0.0);
let sol = pb.solve().unwrap();
assert!(matches!(sol.status(), SolutionStatus::TimeLimit));
for var in v {
assert_eq!(sol.value(var), 1.0);
}
}

#[test]
fn solve_problem_with_initial_solution() {
let limit = 3.0;
Expand Down
41 changes: 38 additions & 3 deletions src/solvers/highs.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
//! A solver that uses [highs](https://docs.rs/highs), a parallel C++ solver.

use crate::solvers::{
MipGapError, ObjectiveDirection, ResolutionError, Solution, SolutionWithDual, SolverModel,
WithMipGap,
MipGapError, ObjectiveDirection, ResolutionError, Solution, SolutionStatus, SolutionWithDual,
SolverModel, WithMipGap, WithTimeLimit,
};
use crate::{
constraint::ConstraintReference,
Expand Down Expand Up @@ -303,7 +303,12 @@ impl SolverModel for HighsProblem {
HighsModelStatus::Infeasible => Err(ResolutionError::Infeasible),
HighsModelStatus::Unbounded => Err(ResolutionError::Unbounded),
HighsModelStatus::UnboundedOrInfeasible => Err(ResolutionError::Infeasible),
HighsModelStatus::ReachedTimeLimit => Ok(HighsSolution {
status: SolutionStatus::TimeLimit,
solution: solved.get_solution(),
}),
_ok_status => Ok(HighsSolution {
status: SolutionStatus::Optimal,
solution: solved.get_solution(),
}),
}
Expand Down Expand Up @@ -341,9 +346,16 @@ impl WithInitialSolution for HighsProblem {
}
}

impl WithTimeLimit for HighsProblem {
fn with_time_limit<T: Into<f64>>(self, seconds: T) -> Self {
self.set_time_limit(seconds.into())
}
}

/// The solution to a highs problem
#[derive(Debug)]
pub struct HighsSolution {
status: SolutionStatus,
solution: highs::Solution,
}

Expand All @@ -355,6 +367,9 @@ impl HighsSolution {
}

impl Solution for HighsSolution {
fn status(&self) -> SolutionStatus {
self.status
}
fn value(&self, variable: Variable) -> f64 {
self.solution.columns()[variable.index()]
}
Expand Down Expand Up @@ -389,9 +404,29 @@ impl WithMipGap for HighsProblem {

#[cfg(test)]
mod tests {
use crate::{constraint, variable, variables, Solution, SolverModel, WithInitialSolution};
use crate::{
constraint,
solvers::{SolutionStatus, WithTimeLimit},
variable, variables, Solution, SolverModel, WithInitialSolution,
};

use super::highs;
#[test]
fn can_solve_with_time_limit() {
let mut vars = variables!();
let x = vars.add(variable().clamp(0, 2));
let y = vars.add(variable().clamp(1, 3));
let solution = vars
.maximise(x + y)
.using(highs)
.with((2 * x + y) << 4)
.with_time_limit(0)
.solve()
.unwrap();
assert!(matches!(solution.status(), SolutionStatus::TimeLimit));
assert_eq!((solution.value(x), solution.value(y)), (0., 1.))
}

#[test]
fn can_solve_with_inequality() {
let mut vars = variables!();
Expand Down
5 changes: 4 additions & 1 deletion src/solvers/lp_solvers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ pub use lp_solvers::solvers::*;
use lp_solvers::util::UniqueNameGenerator;

use crate::constraint::ConstraintReference;
use crate::solvers::{MipGapError, ObjectiveDirection};
use crate::solvers::{MipGapError, ObjectiveDirection, SolutionStatus};
use crate::variable::UnsolvedProblem;
use crate::{
Constraint, Expression, IntoAffineExpression, ResolutionError, Solution as GoodLpSolution,
Expand Down Expand Up @@ -153,6 +153,9 @@ pub struct LpSolution {
}

impl GoodLpSolution for LpSolution {
fn status(&self) -> SolutionStatus {
SolutionStatus::Optimal
}
fn value(&self, variable: Variable) -> f64 {
self.solution[variable.index()]
}
Expand Down
5 changes: 4 additions & 1 deletion src/solvers/lpsolve.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! lp_solve is a free ([LGPL](https://lpsolve.sourceforge.net/5.5/LGPL.htm)) linear (integer) programming solver written in C and based on the revised simplex method.
//! good_lp uses the [lpsolve crate](https://docs.rs/lpsolve/) to call lpsolve. You will need a C compiler, but you won't have to install any additional library.
use crate::solvers::{ObjectiveDirection, ResolutionError, Solution, SolverModel};
use crate::solvers::{ObjectiveDirection, ResolutionError, Solution, SolutionStatus, SolverModel};
use crate::variable::UnsolvedProblem;
use crate::{
affine_expression_trait::IntoAffineExpression, constraint::ConstraintReference, ModelWithSOS1,
Expand Down Expand Up @@ -154,6 +154,9 @@ impl LpSolveSolution {
}

impl Solution for LpSolveSolution {
fn status(&self) -> SolutionStatus {
SolutionStatus::Optimal
}
fn value(&self, variable: Variable) -> f64 {
self.solution[variable.index()]
}
Expand Down
5 changes: 4 additions & 1 deletion src/solvers/microlp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use microlp::Error;
use crate::variable::{UnsolvedProblem, VariableDefinition};
use crate::{
constraint::ConstraintReference,
solvers::{ObjectiveDirection, ResolutionError, Solution, SolverModel},
solvers::{ObjectiveDirection, ResolutionError, Solution, SolutionStatus, SolverModel},
};
use crate::{Constraint, Variable};

Expand Down Expand Up @@ -120,6 +120,9 @@ impl MicroLpSolution {
}

impl Solution for MicroLpSolution {
fn status(&self) -> SolutionStatus {
SolutionStatus::Optimal
}
fn value(&self, variable: Variable) -> f64 {
self.solution[self.variables[variable.index()]]
}
Expand Down
23 changes: 23 additions & 0 deletions src/solvers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,28 @@ pub trait WithInitialSolution {
fn with_initial_solution(self, solution: impl IntoIterator<Item = (Variable, f64)>) -> Self;
}

/// A solver than can stop the solving process after some time
pub trait WithTimeLimit {
/// Sets the time limit for the solver
fn with_time_limit<T: Into<f64>>(self, seconds: T) -> Self;
}

/// Information about the status of a solution, such as whether the solution is
/// optimal
#[derive(Clone, Copy, Debug)]
pub enum SolutionStatus {
/// The solution is optimal
Optimal,
/// The solution is not optimal and it was obtained because the time limit
/// was reached
TimeLimit,
}

/// A problem solution
pub trait Solution {
/// Returns `true` if this solution is optimal and `false` otherwise
fn status(&self) -> SolutionStatus;

/// Get the optimal value of a variable of the problem
fn value(&self, variable: Variable) -> f64;

Expand Down Expand Up @@ -260,6 +280,9 @@ pub trait Solution {
/// If a HashMap doesn't contain the value for a variable,
/// then [Solution::value] will panic if you try to access it.
impl<N: Into<f64> + Clone> Solution for HashMap<Variable, N> {
fn status(&self) -> SolutionStatus {
SolutionStatus::Optimal
}
fn value(&self, variable: Variable) -> f64 {
self[&variable].clone().into()
}
Expand Down
41 changes: 37 additions & 4 deletions src/solvers/scip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ use russcip::WithSolutions;
use crate::variable::{UnsolvedProblem, VariableDefinition};
use crate::{
constraint::ConstraintReference,
solvers::{ObjectiveDirection, ResolutionError, Solution, SolverModel},
CardinalityConstraintSolver, WithInitialSolution,
solvers::{ObjectiveDirection, ResolutionError, Solution, SolutionStatus, SolverModel},
CardinalityConstraintSolver, WithInitialSolution, WithTimeLimit,
};
use crate::{Constraint, Variable};

Expand Down Expand Up @@ -275,7 +275,13 @@ impl SolverModel for SCIPProblem {
let solved_model = self.model.solve();
let status = solved_model.status();
match status {
russcip::Status::TimeLimit => Ok(SCIPSolved {
status: SolutionStatus::TimeLimit,
solved_problem: solved_model,
id_for_var: self.id_for_var,
}),
russcip::status::Status::Optimal => Ok(SCIPSolved {
status: SolutionStatus::Optimal,
solved_problem: solved_model,
id_for_var: self.id_for_var,
}),
Expand Down Expand Up @@ -332,13 +338,23 @@ impl WithInitialSolution for SCIPProblem {
}
}

impl WithTimeLimit for SCIPProblem {
fn with_time_limit<T: Into<f64>>(self, seconds: T) -> Self {
self.set_time_limit(seconds.into() as usize)
}
}

/// A wrapper to a solved SCIP problem
pub struct SCIPSolved {
status: SolutionStatus,
solved_problem: Model<Solved>,
id_for_var: HashMap<Variable, russcip::Variable>,
}

impl Solution for SCIPSolved {
fn status(&self) -> SolutionStatus {
self.status
}
fn value(&self, var: Variable) -> f64 {
self.solved_problem
.best_sol()
Expand All @@ -350,12 +366,29 @@ impl Solution for SCIPSolved {
#[cfg(test)]
mod tests {
use crate::{
constraint, variable, variables, CardinalityConstraintSolver, Solution, SolverModel,
WithInitialSolution,
constraint, variable, variables, CardinalityConstraintSolver, Solution, SolutionStatus,
SolverModel, WithInitialSolution, WithTimeLimit,
};

use super::scip;

#[test]
fn can_solve_with_time_limit() {
let mut vars = variables!();
let x = vars.add(variable().clamp(0, 2));
let y = vars.add(variable().clamp(1, 3));
let solution = vars
.maximise(x + y)
.using(scip)
.with((2 * x + y) << 4)
.set_verbose(true) // TODO: remove
.with_time_limit(0)
.solve()
.unwrap();
assert!(matches!(solution.status(), SolutionStatus::TimeLimit));
assert_eq!((solution.value(x), solution.value(y)), (0., 1.))
}

#[test]
fn can_solve_with_inequality() {
let mut vars = variables!();
Expand Down
Loading