From 4ba2ee23a90c3e720c348f1d32e684cfac5f14bc Mon Sep 17 00:00:00 2001 From: KnorpelSenf <shtrog@gmail.com> Date: Mon, 30 Dec 2024 12:45:10 +0000 Subject: [PATCH 01/29] feat: allow defining initial values for individual variables --- src/variable.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/variable.rs b/src/variable.rs index 8626eef..e8cd482 100644 --- a/src/variable.rs +++ b/src/variable.rs @@ -118,6 +118,7 @@ impl FormatWithVars for Variable { pub struct VariableDefinition { pub(crate) min: f64, pub(crate) max: f64, + pub(crate) initial: Option<f64>, pub(crate) name: String, pub(crate) is_integer: bool, } @@ -128,6 +129,7 @@ impl VariableDefinition { VariableDefinition { min: f64::NEG_INFINITY, max: f64::INFINITY, + initial: None, name: String::new(), is_integer: false, } @@ -177,6 +179,27 @@ impl VariableDefinition { self } + /// Set the initial value of the variable. This may help the solver to find a solution significantly faster. + /// + /// **Warning**: not all solvers support integer variables. + /// Refer to the documentation of the solver you are using. + /// + /// ``` + /// # use good_lp::{ProblemVariables, variable, default_solver, SolverModel, Solution}; + /// let mut problem = ProblemVariables::new(); + /// let x = problem.add(variable().max(3).initial(3)); + /// let y = problem.add(variable().max(5).initial(5)); + /// if cfg!(not(any(feature="clarabel"))) { + /// let solution = problem.maximise(x + y).using(default_solver).solve().unwrap(); + /// assert_eq!(solution.value(x), 3.); + /// assert_eq!(solution.value(y), 5.); + /// } + /// ``` + pub fn initial<N: Into<f64>>(mut self, value: N) -> Self { + self.initial = Some(value.into()); + self + } + /// Set the name of the variable. This is useful in particular when displaying the problem /// for debugging purposes. /// From fb9ab662c87d10e77d92e346e97ca2633739a025 Mon Sep 17 00:00:00 2001 From: KnorpelSenf <shtrog@gmail.com> Date: Mon, 30 Dec 2024 12:45:27 +0000 Subject: [PATCH 02/29] feat: support initial variable values for cbc --- src/solvers/coin_cbc.rs | 49 +++++++++++++++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/src/solvers/coin_cbc.rs b/src/solvers/coin_cbc.rs index 3c0eb21..87e88d2 100644 --- a/src/solvers/coin_cbc.rs +++ b/src/solvers/coin_cbc.rs @@ -23,15 +23,20 @@ pub fn coin_cbc(to_solve: UnsolvedProblem) -> CoinCbcProblem { variables, } = to_solve; let mut model = Model::default(); + let mut initial_solution = vec![]; let columns: Vec<Col> = variables - .into_iter() + .iter_variables_with_def() .map( - |VariableDefinition { - min, - max, - is_integer, - .. - }| { + |( + var, + &VariableDefinition { + min, + max, + initial, + is_integer, + .. + }, + )| { let col = model.add_col(); // Variables are created with a default min of 0 model.set_col_lower(col, min); @@ -41,6 +46,9 @@ pub fn coin_cbc(to_solve: UnsolvedProblem) -> CoinCbcProblem { if is_integer { model.set_integer(col); } + if let Some(val) = initial { + initial_solution.push((var, val)); + }; col }, ) @@ -52,12 +60,16 @@ pub fn coin_cbc(to_solve: UnsolvedProblem) -> CoinCbcProblem { ObjectiveDirection::Maximisation => Sense::Maximize, ObjectiveDirection::Minimisation => Sense::Minimize, }); - CoinCbcProblem { + let mut problem = CoinCbcProblem { model, columns, has_sos: false, mip_gap: None, + }; + if initial_solution.len() > 0 { + problem = problem.with_initial_solution(initial_solution); } + problem } /// A coin-cbc model @@ -234,7 +246,7 @@ impl WithMipGap for CoinCbcProblem { #[cfg(test)] mod tests { - use crate::{variables, Solution, SolverModel, WithInitialSolution}; + use crate::{variable, variables, Solution, SolverModel, WithInitialSolution}; use float_eq::assert_float_eq; #[test] @@ -261,4 +273,23 @@ mod tests { let sol = pb.solve().unwrap(); assert_float_eq!(sol.value(v), limit, abs <= 1e-8); } + + #[test] + fn solve_problem_with_initial_variable_values() { + let limit = 3.0; + // Solve problem once + variables! { + vars: + 0.0 <= v <= limit; + }; + let pb = vars.maximise(v).using(super::coin_cbc); + let sol = pb.solve().unwrap(); + assert_float_eq!(sol.value(v), limit, abs <= 1e-8); + // Recreate problem and solve with initial solution + let mut vars = variables!(); + let v = vars.add(variable().min(0).max(limit).initial(2)); + let pb = vars.maximise(v).using(super::coin_cbc); + let sol = pb.solve().unwrap(); + assert_float_eq!(sol.value(v), limit, abs <= 1e-8); + } } From ccd790d8f003a7344f98fc6ad4e417f151bfea95 Mon Sep 17 00:00:00 2001 From: KnorpelSenf <shtrog@gmail.com> Date: Mon, 30 Dec 2024 12:45:35 +0000 Subject: [PATCH 03/29] feat: support initial variable values for SCIP --- src/solvers/scip.rs | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/solvers/scip.rs b/src/solvers/scip.rs index 0fbe91c..06da49e 100644 --- a/src/solvers/scip.rs +++ b/src/solvers/scip.rs @@ -33,12 +33,14 @@ pub fn scip(to_solve: UnsolvedProblem) -> SCIPProblem { ObjectiveDirection::Minimisation => ObjSense::Minimize, }); let mut var_map = HashMap::new(); + let mut initial_solution = vec![]; for ( var, &VariableDefinition { min, max, + initial, is_integer, ref name, }, @@ -56,12 +58,19 @@ pub fn scip(to_solve: UnsolvedProblem) -> SCIPProblem { }; let id = model.add_var(min, max, coeff, name.as_str(), var_type); var_map.insert(var, id); + if let Some(val) = initial { + initial_solution.push((var, val)); + }; } - SCIPProblem { + let mut problem = SCIPProblem { model, id_for_var: var_map, + }; + if initial_solution.len() > 0 { + problem = problem.with_initial_solution(initial_solution); } + problem } /// A SCIP Model @@ -234,6 +243,34 @@ mod tests { assert_eq!((solution.value(x), solution.value(y)), (0.5, 3.)) } + #[test] + fn solve_problem_with_initial_variable_values() { + // Solve problem initially + 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) + .solve() + .unwrap(); + // Recreate same problem with initial values slightly off + let initial_x = solution.value(x) - 0.1; + let initial_y = solution.value(x) - 1.0; + let mut vars = variables!(); + let x = vars.add(variable().clamp(0, 2).initial(initial_x)); + let y = vars.add(variable().clamp(1, 3).initial(initial_y)); + let solution = vars + .maximise(x + y) + .using(scip) + .with((2 * x + y) << 4) + .solve() + .unwrap(); + + assert_eq!((solution.value(x), solution.value(y)), (0.5, 3.)) + } + #[test] fn can_solve_with_equality() { let mut vars = variables!(); From 3c06bc3475d5c5beabdf146a419019fdc47bb145 Mon Sep 17 00:00:00 2001 From: KnorpelSenf <shtrog@gmail.com> Date: Mon, 30 Dec 2024 13:52:28 +0100 Subject: [PATCH 04/29] docs: fix typo in `variable.initial()` docs --- src/variable.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/variable.rs b/src/variable.rs index e8cd482..61ff43c 100644 --- a/src/variable.rs +++ b/src/variable.rs @@ -181,7 +181,7 @@ impl VariableDefinition { /// Set the initial value of the variable. This may help the solver to find a solution significantly faster. /// - /// **Warning**: not all solvers support integer variables. + /// **Warning**: not all solvers support initial solutions. /// Refer to the documentation of the solver you are using. /// /// ``` From b2422246d7adbec5697a25250d7d12a3277cfa15 Mon Sep 17 00:00:00 2001 From: KnorpelSenf <shtrog@gmail.com> Date: Mon, 30 Dec 2024 14:05:09 +0100 Subject: [PATCH 05/29] fix: compile --- src/solvers/cplex.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/solvers/cplex.rs b/src/solvers/cplex.rs index c3f29a1..2570178 100644 --- a/src/solvers/cplex.rs +++ b/src/solvers/cplex.rs @@ -40,6 +40,7 @@ pub fn cplex_with_env(to_solve: UnsolvedProblem, cplex_env: Environment) -> CPLE max, is_integer, ref name, + .., }, )| { let coeff = *to_solve From cb1851b9c7b20fcb773a18df18131a822cdd9b4e Mon Sep 17 00:00:00 2001 From: KnorpelSenf <shtrog@gmail.com> Date: Mon, 30 Dec 2024 13:12:41 +0000 Subject: [PATCH 06/29] fix: parse --- src/solvers/cplex.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/solvers/cplex.rs b/src/solvers/cplex.rs index 2570178..4442bbc 100644 --- a/src/solvers/cplex.rs +++ b/src/solvers/cplex.rs @@ -40,7 +40,7 @@ pub fn cplex_with_env(to_solve: UnsolvedProblem, cplex_env: Environment) -> CPLE max, is_integer, ref name, - .., + .. }, )| { let coeff = *to_solve From 814ddaa4f94f0f687870622320a859a8d16a3668 Mon Sep 17 00:00:00 2001 From: KnorpelSenf <shtrog@gmail.com> Date: Tue, 7 Jan 2025 10:48:37 +0000 Subject: [PATCH 07/29] perf: track and leverage initial solution size Previously, when building up an initial solution from the problem variables would cause repeated reallocation. We support partial initial solutions so we may have 0-n values in the initial solution (with n being the number of total variables). We do not want to allocate memory for all variables because there might be 0 initial values, and we also do not want to do allocate 0 memory because then we have to repeatedly reallocate memory as the initial solution grows. These changes introduce a counter that is incrememted whenever a variable with an initial value is added to the problem. That way, we can allocate the perfect number of bytes upfront in all cases. --- src/solvers/coin_cbc.rs | 2 +- src/solvers/scip.rs | 2 +- src/variable.rs | 14 +++++++++++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/solvers/coin_cbc.rs b/src/solvers/coin_cbc.rs index 87e88d2..83794da 100644 --- a/src/solvers/coin_cbc.rs +++ b/src/solvers/coin_cbc.rs @@ -23,7 +23,7 @@ pub fn coin_cbc(to_solve: UnsolvedProblem) -> CoinCbcProblem { variables, } = to_solve; let mut model = Model::default(); - let mut initial_solution = vec![]; + let mut initial_solution = Vec::with_capacity(variables.initial_solution_len()); let columns: Vec<Col> = variables .iter_variables_with_def() .map( diff --git a/src/solvers/scip.rs b/src/solvers/scip.rs index 06da49e..7d2f875 100644 --- a/src/solvers/scip.rs +++ b/src/solvers/scip.rs @@ -33,7 +33,7 @@ pub fn scip(to_solve: UnsolvedProblem) -> SCIPProblem { ObjectiveDirection::Minimisation => ObjSense::Minimize, }); let mut var_map = HashMap::new(); - let mut initial_solution = vec![]; + let mut initial_solution = Vec::with_capacity(to_solve.variables.initial_solution_len()); for ( var, diff --git a/src/variable.rs b/src/variable.rs index 61ff43c..bf48314 100644 --- a/src/variable.rs +++ b/src/variable.rs @@ -285,12 +285,16 @@ pub fn variable() -> VariableDefinition { #[derive(Default)] pub struct ProblemVariables { variables: Vec<VariableDefinition>, + initial_count: usize, } impl ProblemVariables { /// Create an empty list of variables pub fn new() -> Self { - ProblemVariables { variables: vec![] } + ProblemVariables { + variables: vec![], + initial_count: 0, + } } /// Add a anonymous unbounded continuous variable to the problem @@ -312,6 +316,9 @@ impl ProblemVariables { /// ``` pub fn add(&mut self, var_def: VariableDefinition) -> Variable { let index = self.variables.len(); + if var_def.initial.is_some() { + self.initial_count += 1; + } self.variables.push(var_def); Variable::at(index) } @@ -413,6 +420,11 @@ impl ProblemVariables { self.variables.is_empty() } + /// Returns the number of variables with initial solution values + pub fn initial_solution_len(&self) -> usize { + self.initial_count + } + /// Display the given expression or constraint with the correct variable names /// /// ``` From 6f9f1f4bd69290198c22f6d7d4502c1f9d6bda96 Mon Sep 17 00:00:00 2001 From: KnorpelSenf <shtrog@gmail.com> Date: Wed, 8 Jan 2025 07:58:35 +0000 Subject: [PATCH 08/29] docs: add doctest for initial_solution_len --- src/variable.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/variable.rs b/src/variable.rs index bf48314..fe1b2b0 100644 --- a/src/variable.rs +++ b/src/variable.rs @@ -421,6 +421,15 @@ impl ProblemVariables { } /// Returns the number of variables with initial solution values + /// + /// ``` + /// use good_lp::{variable, variables}; + /// let mut vars = variables!(); + /// vars.add(variable()); + /// vars.add(variable().initial(5)); + /// vars.add(variable()); + /// assert_eq!(vars.initial_solution_len(), 1); + /// ``` pub fn initial_solution_len(&self) -> usize { self.initial_count } From 00261dff165c515f03fbb3fbc9c88db08e7dff4e Mon Sep 17 00:00:00 2001 From: KnorpelSenf <shtrog@gmail.com> Date: Wed, 15 Jan 2025 14:03:15 +0000 Subject: [PATCH 09/29] build: switch over to highs bindings from git --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 20e4d4d..907d960 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ minilp = ["microlp"] # minilp is not maintained anymore, we use the microlp fork coin_cbc = { version = "0.1", optional = true, default-features = false } microlp = { version = "0.2.6", optional = true } lpsolve = { version = "0.1", optional = true } -highs = { version = "1.5.0", optional = true } +highs = { git = "https://github.com/rust-or/highs.git", optional = true } russcip = { version = "0.4.1", optional = true } lp-solvers = { version = "1.0.0", features = ["cplex"], optional = true } cplex-rs = { version = "0.1", optional = true } From d4447da707e482a21531f96128ae686c2de05753 Mon Sep 17 00:00:00 2001 From: KnorpelSenf <shtrog@gmail.com> Date: Wed, 15 Jan 2025 14:03:36 +0000 Subject: [PATCH 10/29] feat: support initial solutions for HiGHS --- src/solvers/highs.rs | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/solvers/highs.rs b/src/solvers/highs.rs index 9e9d322..5dc98cd 100644 --- a/src/solvers/highs.rs +++ b/src/solvers/highs.rs @@ -9,9 +9,10 @@ use crate::{ solvers::DualValues, variable::{UnsolvedProblem, VariableDefinition}, }; -use crate::{Constraint, IntoAffineExpression, Variable}; +use crate::{Constraint, IntoAffineExpression, Variable, WithInitialSolution}; use highs::HighsModelStatus; use std::collections::HashMap; +use std::iter::FromIterator; /// The [highs](https://docs.rs/highs) solver, /// to be used with [UnsolvedProblem::using]. @@ -48,6 +49,7 @@ pub fn highs(to_solve: UnsolvedProblem) -> HighsProblem { sense, highs_problem, columns, + initial_solution: None, verbose: false, options: Default::default(), } @@ -170,6 +172,7 @@ pub struct HighsProblem { sense: highs::Sense, highs_problem: highs::RowProblem, columns: Vec<highs::Col>, + initial_solution: Option<Vec<(Variable, f64)>>, verbose: bool, options: HashMap<String, HighsOptionValue>, } @@ -250,6 +253,15 @@ impl SolverModel for HighsProblem { fn solve(mut self) -> Result<Self::Solution, Self::Error> { let verbose = self.verbose; let options = std::mem::take(&mut self.options); + let initial_solution = self.initial_solution.as_ref().map(|pairs| { + pairs + .iter() + .fold(vec![0.0; self.columns.len()], |mut sol, (var, val)| { + sol[var.index()] = *val; + sol + }) + }); + let mut model = self.into_inner(); if verbose { model.set_option(&b"output_flag"[..], true); @@ -265,6 +277,10 @@ impl SolverModel for HighsProblem { } } + if initial_solution.is_some() { + model.set_solution(initial_solution.as_deref(), None, None, None); + } + let solved = model.solve(); match solved.status() { HighsModelStatus::NotSet => Err(ResolutionError::Other("NotSet")), @@ -305,6 +321,16 @@ impl SolverModel for HighsProblem { } } +impl WithInitialSolution for HighsProblem { + fn with_initial_solution( + mut self, + solution: impl IntoIterator<Item = (Variable, f64)>, + ) -> Self { + self.initial_solution = Some(Vec::from_iter(solution)); + self + } +} + /// The solution to a highs problem #[derive(Debug)] pub struct HighsSolution { From 8304a48c4c238b86f1ff66052a211372652e289f Mon Sep 17 00:00:00 2001 From: KnorpelSenf <shtrog@gmail.com> Date: Wed, 15 Jan 2025 14:13:48 +0000 Subject: [PATCH 11/29] test: add first tests for HiGHS solver --- src/solvers/highs.rs | 64 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/src/solvers/highs.rs b/src/solvers/highs.rs index 5dc98cd..bb0cf84 100644 --- a/src/solvers/highs.rs +++ b/src/solvers/highs.rs @@ -376,3 +376,67 @@ impl WithMipGap for HighsProblem { self.set_mip_rel_gap(mip_gap) } } + +#[cfg(test)] +mod tests { + use crate::{constraint, variable, variables, Solution, SolverModel, WithInitialSolution}; + + use super::highs; + #[test] + fn can_solve_with_inequality() { + 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) + .solve() + .unwrap(); + assert_eq!((solution.value(x), solution.value(y)), (0.5, 3.)) + } + + #[test] + fn can_solve_with_initial_solution() { + // Solve problem initially + 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) + .solve() + .unwrap(); + // Recreate same problem with initial values slightly off + let initial_x = solution.value(x) - 0.1; + let initial_y = solution.value(x) - 1.0; + 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_initial_solution([(x, initial_x), (y, initial_y)]) + .solve() + .unwrap(); + + assert_eq!((solution.value(x), solution.value(y)), (0.5, 3.)) + } + + #[test] + fn can_solve_with_equality() { + let mut vars = variables!(); + let x = vars.add(variable().clamp(0, 2).integer()); + let y = vars.add(variable().clamp(1, 3).integer()); + let solution = vars + .maximise(x + y) + .using(highs) + .with(constraint!(2 * x + y == 4)) + .with(constraint!(x + 2 * y <= 5)) + .solve() + .unwrap(); + assert_eq!((solution.value(x), solution.value(y)), (1., 2.)); + } +} From b7fd99760d3eb06009e863a383b49908afc122c0 Mon Sep 17 00:00:00 2001 From: lovasoa <contact@ophir.dev> Date: Mon, 30 Dec 2024 13:53:47 +0100 Subject: [PATCH 12/29] explain more in readme --- README.md | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index c4ad93d..d691a8b 100644 --- a/README.md +++ b/README.md @@ -67,23 +67,25 @@ You can find a resource allocation problem example in This library offers an abstraction over multiple solvers. By default, it uses [cbc][cbc], but you can also activate other solvers using cargo features. -| solver feature name | integer variables | no C compiler\* | no additional libs\*\* | fast | WASM | -| ---------------------- | ----------------- | --------------- | ---------------------- | ---- | ---------- | -| [`coin_cbc`][cbc] | ✅ | ✅ | ❌ | ✅ | ❌ | -| [`highs`][highs] | ✅ | ❌ | ✅\+ | ✅ | ❌ | -| [`lpsolve`][lpsolve] | ✅ | ❌ | ✅ | ❌ | ❌ | -| [`microlp`][microlp] | ✅ | ✅ | ✅ | ❌ | ✅ | -| [`lp-solvers`][lps] | ✅ | ✅ | ✅ | ❌ | ❌ | -| [`scip`][scip] | ✅ | ✅ | ✅\+\+ | ✅ | ❌ | -| [`cplex-rs`][cplex] | ✅ | ❌ | ✅\+\+\+ | ✅ | ❌ | -| [`clarabel`][clarabel] | ❌ | ✅ | ✅ | ✅ | ✅\+\+\+\+ | - -- \* no C compiler: builds with only cargo, without requiring you to install a C compiler -- \*\* no additional libs: works without additional libraries at runtime, all the dependencies are statically linked -- \+ highs itself is statically linked and does not require manual installation. However, on some systems, you may have to [install dependencies of highs itself](https://github.com/rust-or/good_lp/issues/29). -- \+\+ using the precompiled binary is possible by enabling the optional `scip_bundled` feature -- \+\+\+ the cplex_rs crate links statically to a local installation of the IBM ILOG CPLEX Optimizer. -- \+\+\+\+ to use clarabel for WASM targets, set the `clarabel-wasm` feature flag +| solver feature name | integer variables | no C compiler\* | no additional libs\* | fast\* | WASM\* | +| ---------------------- | ----------------- | --------------- | ---------------------- | ---- | ---- | +| [`coin_cbc`][cbc] | ✅ | ✅ | ❌ | ✅ | ❌ | +| [`highs`][highs] | ✅ | ❌ | ✅¹ | ✅ | ❌ | +| [`lpsolve`][lpsolve] | ✅ | ❌ | ✅ | ❌ | ❌ | +| [`microlp`][microlp] | ✅ | ✅ | ✅ | ❌ | ✅ | +| [`lp-solvers`][lps] | ✅ | ✅ | ✅ | ❌ | ❌ | +| [`scip`][scip] | ✅ | ✅ | ✅² | ✅ | ❌ | +| [`cplex-rs`][cplex] | ✅ | ❌ | ✅³ | ✅ | ❌ | +| [`clarabel`][clarabel] | ❌ | ✅ | ✅ | ✅ | ✅⁴ | + +- \* *no C compiler*: builds with only cargo, without requiring you to install a C compiler +- \* *no additional libs*: works without additional libraries at runtime, all the dependencies are statically linked +- \* *fast*: the solver does good on large problems according to published benchmarks ([*caveats*](https://github.com/rust-or/good_lp/issues/68)) +- \* *WASM*: the solver can compile to WASM targets and run in web browsers +- ¹ highs itself is statically linked and does not require manual installation. However, on some systems, you may have to [install dependencies of highs itself](https://github.com/rust-or/good_lp/issues/29). +- ² using the precompiled binary is possible by enabling the optional `scip_bundled` feature +- ³ the [cplex_rs crate](https://crates.io/crates/cplex-rs) links statically to a local installation of the proprietary [IBM ILOG CPLEX Optimizer](https://www.ibm.com/products/ilog-cplex-optimization-studio/cplex-optimizer). +- ⁴ to use clarabel for WASM targets, set the `clarabel-wasm` feature flag To use an alternative solver, put the following in your `Cargo.toml`: From 820d9f591a8e8cd4508fef197bb175f7b0828371 Mon Sep 17 00:00:00 2001 From: Niklas <niklas.sm+github@gmail.com> Date: Thu, 2 Jan 2025 18:05:18 -0500 Subject: [PATCH 13/29] Docs: add readme details about restricting variables to have integer values (#78) * More details in README: - add more detail on constraining variables to integer solutions - show the `(integer)` qualifier syntax inside the `variables!` macro - update documentation links to use `latest` Add test in `variables.rs` showing `(integer)` qualifier * - conditional config for test - integer variables not suppored by "clarabel" solver --------- Co-authored-by: Niklas Smedemark-Margulies <niklas.smedemark-margulies@analog.com> --- README.md | 14 ++++++++------ tests/variables.rs | 23 ++++++++++++++++++++++- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index d691a8b..4b4f622 100644 --- a/README.md +++ b/README.md @@ -212,18 +212,18 @@ If you want to use it with WASM targets, you must include the `clarabel-wasm` fe ## Variable types -`good_lp` internally represents all [variable](https://docs.rs/good_lp/1.4.0/good_lp/variable/struct.Variable.html) values and coefficients as `f64`. -It lets you express constraints using either `f64` or `i32` (in the latter case, the integer will be losslessly converted to a floating point number). -The solution's [values are `f64`](https://docs.rs/good_lp/1.4.0/good_lp/solvers/trait.Solution.html#tymethod.value) as well. +`good_lp` internally represents all [variable](https://docs.rs/good_lp/latest/good_lp/variable/struct.Variable.html) values and coefficients as `f64`. +It lets you express constraints on the range of possible values using either `f64` or `i32` (in the latter case, the integer will be losslessly converted to a floating point number). +The solution's [values are `f64`](https://docs.rs/good_lp/latest/good_lp/solvers/trait.Solution.html#tymethod.value) as well. For instance: ```rust -// Correct use of f64 and i32 for Variable struct and constraints +// Correct use of f64 and i32 to specify feasible ranges for Variables variables! { problem: a <= 10.0; - 2 <= b <= 4; + 2 <= b (integer) <= 4; // Variables can be restricted using qualifiers like (integer) }; let model = problem .maximise(b) @@ -233,7 +233,9 @@ For instance: ``` Here, `a` and `b` are `Variable` instances that can take either continuous (floating-point) or [integer values](https://docs.rs/good_lp/latest/good_lp/variable/struct.VariableDefinition.html#method.integer). -Constraints can be expressed using either `f64` or `i32`, as shown in the example (but replacing for example `4.0` with a `usize` variable would fail, because an usize cannot be converted to an f64 losslessly). +Constraints on possible values can be expressed using either `f64` or `i32`, as shown in the example (but replacing for example `4.0` with a `usize` variable would fail, because an usize cannot be converted to an f64 losslessly). +The [`variables!` macro](https://docs.rs/good_lp/latest/good_lp/macro.variables.html) also allows constraining variables to integer values using qualifiers like `2 <= b (integer) <= 4` above. + Solution values will always be `f64`, regardless of whether the variables were defined with `f64` or `i32`. So, even if you use integer variables, the solution object will store the integer variable values as `f64`. diff --git a/tests/variables.rs b/tests/variables.rs index 1f5f633..bac617c 100644 --- a/tests/variables.rs +++ b/tests/variables.rs @@ -1,4 +1,5 @@ -use good_lp::{variables, Expression}; +use good_lp::{constraint, default_solver, variables, Expression, Solution, SolverModel}; + #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::*; @@ -57,3 +58,23 @@ fn debug_format() { expr_str ) } + +#[test] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] +#[cfg(not(feature = "clarabel"))] +fn variables_macro_integer() { + variables! { + vars: + a <= 1; + 2 <= b (integer) <= 4; + } + let solution = vars + .maximise(10 * (a - b / 5) - b) + .using(default_solver) + .with(constraint!(a + 2 <= b)) + .with(constraint!(1 + a >= 4 - b)) + .solve() + .expect("solve"); + assert!((solution.value(a) - 1.).abs() < 1e-5); + assert!((solution.value(b) - 3.).abs() < 1e-5); +} From 3156ca5cc33b6c628f977cc5aa2775cfeceadf58 Mon Sep 17 00:00:00 2001 From: KnorpelSenf <shtrog@gmail.com> Date: Wed, 8 Jan 2025 17:33:22 +0100 Subject: [PATCH 14/29] chore: do not bundle scip on docs.rs (#81) * chore: do not bundle scip on docs.rs * docs: add comment on docs.rs features --- Cargo.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index fbbb63d..20e4d4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,10 @@ harness = false [package.metadata.docs.rs] # Display the documentation for all solvers on docs.rs all-features = false -features = [ "all_default_solvers" ] +# Use almost the same as all_default_solvers. Similarly, cplex-rs is not +# included because it is incompatible with lpsolve. Additionally, +# russcip/bundled is not included because network access is blocked on docs.rs. +features = ["coin_cbc", "microlp", "lpsolve", "highs", "russcip", "lp-solvers", "clarabel"] default-target = "x86_64-unknown-linux-gnu" targets = ["x86_64-unknown-linux-gnu"] rustdoc-args = ["--cfg", "docsrs"] From 10d50f346cf70a7218f7c446cb0c75e54854385d Mon Sep 17 00:00:00 2001 From: KnorpelSenf <shtrog@gmail.com> Date: Wed, 15 Jan 2025 14:03:15 +0000 Subject: [PATCH 15/29] build: switch over to highs bindings from git --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 20e4d4d..907d960 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ minilp = ["microlp"] # minilp is not maintained anymore, we use the microlp fork coin_cbc = { version = "0.1", optional = true, default-features = false } microlp = { version = "0.2.6", optional = true } lpsolve = { version = "0.1", optional = true } -highs = { version = "1.5.0", optional = true } +highs = { git = "https://github.com/rust-or/highs.git", optional = true } russcip = { version = "0.4.1", optional = true } lp-solvers = { version = "1.0.0", features = ["cplex"], optional = true } cplex-rs = { version = "0.1", optional = true } From 1bcdbd34522a120509a82811e4290e11858b12bb Mon Sep 17 00:00:00 2001 From: KnorpelSenf <shtrog@gmail.com> Date: Wed, 15 Jan 2025 14:03:36 +0000 Subject: [PATCH 16/29] feat: support initial solutions for HiGHS --- src/solvers/highs.rs | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/solvers/highs.rs b/src/solvers/highs.rs index 9e9d322..5dc98cd 100644 --- a/src/solvers/highs.rs +++ b/src/solvers/highs.rs @@ -9,9 +9,10 @@ use crate::{ solvers::DualValues, variable::{UnsolvedProblem, VariableDefinition}, }; -use crate::{Constraint, IntoAffineExpression, Variable}; +use crate::{Constraint, IntoAffineExpression, Variable, WithInitialSolution}; use highs::HighsModelStatus; use std::collections::HashMap; +use std::iter::FromIterator; /// The [highs](https://docs.rs/highs) solver, /// to be used with [UnsolvedProblem::using]. @@ -48,6 +49,7 @@ pub fn highs(to_solve: UnsolvedProblem) -> HighsProblem { sense, highs_problem, columns, + initial_solution: None, verbose: false, options: Default::default(), } @@ -170,6 +172,7 @@ pub struct HighsProblem { sense: highs::Sense, highs_problem: highs::RowProblem, columns: Vec<highs::Col>, + initial_solution: Option<Vec<(Variable, f64)>>, verbose: bool, options: HashMap<String, HighsOptionValue>, } @@ -250,6 +253,15 @@ impl SolverModel for HighsProblem { fn solve(mut self) -> Result<Self::Solution, Self::Error> { let verbose = self.verbose; let options = std::mem::take(&mut self.options); + let initial_solution = self.initial_solution.as_ref().map(|pairs| { + pairs + .iter() + .fold(vec![0.0; self.columns.len()], |mut sol, (var, val)| { + sol[var.index()] = *val; + sol + }) + }); + let mut model = self.into_inner(); if verbose { model.set_option(&b"output_flag"[..], true); @@ -265,6 +277,10 @@ impl SolverModel for HighsProblem { } } + if initial_solution.is_some() { + model.set_solution(initial_solution.as_deref(), None, None, None); + } + let solved = model.solve(); match solved.status() { HighsModelStatus::NotSet => Err(ResolutionError::Other("NotSet")), @@ -305,6 +321,16 @@ impl SolverModel for HighsProblem { } } +impl WithInitialSolution for HighsProblem { + fn with_initial_solution( + mut self, + solution: impl IntoIterator<Item = (Variable, f64)>, + ) -> Self { + self.initial_solution = Some(Vec::from_iter(solution)); + self + } +} + /// The solution to a highs problem #[derive(Debug)] pub struct HighsSolution { From 43d96877ca35f6493d87d22d00f28c76857857b1 Mon Sep 17 00:00:00 2001 From: KnorpelSenf <shtrog@gmail.com> Date: Wed, 15 Jan 2025 14:13:48 +0000 Subject: [PATCH 17/29] test: add first tests for HiGHS solver --- src/solvers/highs.rs | 64 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/src/solvers/highs.rs b/src/solvers/highs.rs index 5dc98cd..bb0cf84 100644 --- a/src/solvers/highs.rs +++ b/src/solvers/highs.rs @@ -376,3 +376,67 @@ impl WithMipGap for HighsProblem { self.set_mip_rel_gap(mip_gap) } } + +#[cfg(test)] +mod tests { + use crate::{constraint, variable, variables, Solution, SolverModel, WithInitialSolution}; + + use super::highs; + #[test] + fn can_solve_with_inequality() { + 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) + .solve() + .unwrap(); + assert_eq!((solution.value(x), solution.value(y)), (0.5, 3.)) + } + + #[test] + fn can_solve_with_initial_solution() { + // Solve problem initially + 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) + .solve() + .unwrap(); + // Recreate same problem with initial values slightly off + let initial_x = solution.value(x) - 0.1; + let initial_y = solution.value(x) - 1.0; + 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_initial_solution([(x, initial_x), (y, initial_y)]) + .solve() + .unwrap(); + + assert_eq!((solution.value(x), solution.value(y)), (0.5, 3.)) + } + + #[test] + fn can_solve_with_equality() { + let mut vars = variables!(); + let x = vars.add(variable().clamp(0, 2).integer()); + let y = vars.add(variable().clamp(1, 3).integer()); + let solution = vars + .maximise(x + y) + .using(highs) + .with(constraint!(2 * x + y == 4)) + .with(constraint!(x + 2 * y <= 5)) + .solve() + .unwrap(); + assert_eq!((solution.value(x), solution.value(y)), (1., 2.)); + } +} From 8e0cb624443716d6368146bbc532e633523ad2fb Mon Sep 17 00:00:00 2001 From: KnorpelSenf <shtrog@gmail.com> Date: Wed, 22 Jan 2025 08:53:07 +0000 Subject: [PATCH 18/29] build: update highs to 1.7.0 --- Cargo.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 907d960..c4fef22 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,15 +16,16 @@ default = ["coin_cbc", "singlethread-cbc"] singlethread-cbc = ["coin_cbc?/singlethread-cbc"] scip = ["russcip"] scip_bundled = ["russcip?/bundled"] -all_default_solvers = ["coin_cbc", "microlp", "lpsolve", "highs", "russcip", "russcip/bundled", "lp-solvers", "clarabel"] # cplex-rs is not included because it is incompatible with lpsolve +all_default_solvers = ["coin_cbc", "microlp", "lpsolve", "russcip", "russcip/bundled", "lp-solvers", "clarabel"] # cplex-rs is not included because it is incompatible with lpsolve clarabel-wasm = ["clarabel/wasm"] minilp = ["microlp"] # minilp is not maintained anymore, we use the microlp fork instead +highs = ["dep:highs"] [dependencies] coin_cbc = { version = "0.1", optional = true, default-features = false } microlp = { version = "0.2.6", optional = true } lpsolve = { version = "0.1", optional = true } -highs = { git = "https://github.com/rust-or/highs.git", optional = true } +highs = { version = "1.7.0", optional = true } russcip = { version = "0.4.1", optional = true } lp-solvers = { version = "1.0.0", features = ["cplex"], optional = true } cplex-rs = { version = "0.1", optional = true } From 7daf0286f2f582152d6ba2dfadb3b074c9bc54a2 Mon Sep 17 00:00:00 2001 From: KnorpelSenf <shtrog@gmail.com> Date: Wed, 22 Jan 2025 08:54:36 +0000 Subject: [PATCH 19/29] Revert "build: update highs to 1.7.0" This reverts commit 8e0cb624443716d6368146bbc532e633523ad2fb. --- Cargo.toml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c4fef22..907d960 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,16 +16,15 @@ default = ["coin_cbc", "singlethread-cbc"] singlethread-cbc = ["coin_cbc?/singlethread-cbc"] scip = ["russcip"] scip_bundled = ["russcip?/bundled"] -all_default_solvers = ["coin_cbc", "microlp", "lpsolve", "russcip", "russcip/bundled", "lp-solvers", "clarabel"] # cplex-rs is not included because it is incompatible with lpsolve +all_default_solvers = ["coin_cbc", "microlp", "lpsolve", "highs", "russcip", "russcip/bundled", "lp-solvers", "clarabel"] # cplex-rs is not included because it is incompatible with lpsolve clarabel-wasm = ["clarabel/wasm"] minilp = ["microlp"] # minilp is not maintained anymore, we use the microlp fork instead -highs = ["dep:highs"] [dependencies] coin_cbc = { version = "0.1", optional = true, default-features = false } microlp = { version = "0.2.6", optional = true } lpsolve = { version = "0.1", optional = true } -highs = { version = "1.7.0", optional = true } +highs = { git = "https://github.com/rust-or/highs.git", optional = true } russcip = { version = "0.4.1", optional = true } lp-solvers = { version = "1.0.0", features = ["cplex"], optional = true } cplex-rs = { version = "0.1", optional = true } From 78e1a4b93e76122a472e89858f347b37d0b100b4 Mon Sep 17 00:00:00 2001 From: KnorpelSenf <shtrog@gmail.com> Date: Wed, 22 Jan 2025 09:00:40 +0000 Subject: [PATCH 20/29] build: update highs and russcip --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 907d960..4ea822a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,8 +24,8 @@ minilp = ["microlp"] # minilp is not maintained anymore, we use the microlp fork coin_cbc = { version = "0.1", optional = true, default-features = false } microlp = { version = "0.2.6", optional = true } lpsolve = { version = "0.1", optional = true } -highs = { git = "https://github.com/rust-or/highs.git", optional = true } -russcip = { version = "0.4.1", optional = true } +highs = { version = "1.7.0", optional = true } +russcip = { version = "0.5.1", optional = true } lp-solvers = { version = "1.0.0", features = ["cplex"], optional = true } cplex-rs = { version = "0.1", optional = true } clarabel = { version = "0.9.0", optional = true, features = [] } From f1dcedbc68e370c5d5501c8584bcd799540de6a4 Mon Sep 17 00:00:00 2001 From: KnorpelSenf <shtrog@gmail.com> Date: Wed, 22 Jan 2025 09:11:38 +0000 Subject: [PATCH 21/29] refactor: drop unused imports --- tests/variables.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/variables.rs b/tests/variables.rs index bac617c..e58e446 100644 --- a/tests/variables.rs +++ b/tests/variables.rs @@ -1,4 +1,4 @@ -use good_lp::{constraint, default_solver, variables, Expression, Solution, SolverModel}; +use good_lp::{variables, Expression}; #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::*; From 376f960aeb1ac3c156318ada06399f7610fce24c Mon Sep 17 00:00:00 2001 From: KnorpelSenf <shtrog@gmail.com> Date: Wed, 22 Jan 2025 09:11:48 +0000 Subject: [PATCH 22/29] style: fix lint for empty check --- src/solvers/coin_cbc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/solvers/coin_cbc.rs b/src/solvers/coin_cbc.rs index 83794da..4321f87 100644 --- a/src/solvers/coin_cbc.rs +++ b/src/solvers/coin_cbc.rs @@ -66,7 +66,7 @@ pub fn coin_cbc(to_solve: UnsolvedProblem) -> CoinCbcProblem { has_sos: false, mip_gap: None, }; - if initial_solution.len() > 0 { + if !initial_solution.is_empty() { problem = problem.with_initial_solution(initial_solution); } problem From 7ce4355ef49a3736b46eba5606dc389bb126a937 Mon Sep 17 00:00:00 2001 From: KnorpelSenf <shtrog@gmail.com> Date: Wed, 22 Jan 2025 09:13:07 +0000 Subject: [PATCH 23/29] Revert "refactor: drop unused imports" This reverts commit f1dcedbc68e370c5d5501c8584bcd799540de6a4. --- tests/variables.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/variables.rs b/tests/variables.rs index e58e446..bac617c 100644 --- a/tests/variables.rs +++ b/tests/variables.rs @@ -1,4 +1,4 @@ -use good_lp::{variables, Expression}; +use good_lp::{constraint, default_solver, variables, Expression, Solution, SolverModel}; #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::*; From d1eac341ff81a475ee453ef25a450d86c78ef64f Mon Sep 17 00:00:00 2001 From: KnorpelSenf <shtrog@gmail.com> Date: Wed, 22 Jan 2025 09:22:43 +0000 Subject: [PATCH 24/29] test: drop test for byte size --- tests/variables.rs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/tests/variables.rs b/tests/variables.rs index bac617c..f38e992 100644 --- a/tests/variables.rs +++ b/tests/variables.rs @@ -27,22 +27,6 @@ fn large_sum() { assert_eq!(sum_right, sum_reverse) } -#[test] -#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] -fn complete() { - let mut var1 = variables!(); - let mut var2 = variables!(); - assert_eq!( - // variables iss the size of an empty vector - std::mem::size_of_val(&Vec::<u8>::new()), - std::mem::size_of_val(&var1) - ); - let a = var1.add_variable(); - let b = var2.add_variable(); - let _sum_a = a + a; - let _diff_b = b - b + b; -} - #[test] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] fn debug_format() { From dbe460b408e706381885677d54fd79eadbf2049c Mon Sep 17 00:00:00 2001 From: KnorpelSenf <shtrog@gmail.com> Date: Wed, 22 Jan 2025 09:28:07 +0000 Subject: [PATCH 25/29] refactor: prefer is_empty over >0 --- src/solvers/scip.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/solvers/scip.rs b/src/solvers/scip.rs index 7d2f875..f0b787d 100644 --- a/src/solvers/scip.rs +++ b/src/solvers/scip.rs @@ -67,7 +67,7 @@ pub fn scip(to_solve: UnsolvedProblem) -> SCIPProblem { model, id_for_var: var_map, }; - if initial_solution.len() > 0 { + if !initial_solution.is_empty() { problem = problem.with_initial_solution(initial_solution); } problem From 8bd341976794ac0b01b3bf334c0c5972230013df Mon Sep 17 00:00:00 2001 From: KnorpelSenf <shtrog@gmail.com> Date: Wed, 22 Jan 2025 09:28:23 +0000 Subject: [PATCH 26/29] feat: add support for initial solutions with HiGHS --- src/solvers/highs.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/solvers/highs.rs b/src/solvers/highs.rs index bb0cf84..29d0c86 100644 --- a/src/solvers/highs.rs +++ b/src/solvers/highs.rs @@ -26,11 +26,14 @@ pub fn highs(to_solve: UnsolvedProblem) -> HighsProblem { ObjectiveDirection::Minimisation => highs::Sense::Minimise, }; let mut columns = Vec::with_capacity(to_solve.variables.len()); + let mut initial_solution = Vec::with_capacity(to_solve.variables.initial_solution_len()); + for ( var, &VariableDefinition { min, max, + initial, is_integer, .. }, @@ -44,15 +47,22 @@ pub fn highs(to_solve: UnsolvedProblem) -> HighsProblem { .unwrap_or(&0.); let col = highs_problem.add_column_with_integrality(col_factor, min..max, is_integer); columns.push(col); + if let Some(val) = initial { + initial_solution.push((var, val)); + } } - HighsProblem { + let mut problem = HighsProblem { sense, highs_problem, columns, initial_solution: None, verbose: false, options: Default::default(), + }; + if !initial_solution.is_empty() { + problem = problem.with_initial_solution(initial_solution); } + problem } /// Presolve option From 9085fd151eb4e9efe82efdba59ac9d2355788851 Mon Sep 17 00:00:00 2001 From: KnorpelSenf <shtrog@gmail.com> Date: Wed, 22 Jan 2025 09:29:52 +0000 Subject: [PATCH 27/29] style: fix formatting --- src/solvers/highs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/solvers/highs.rs b/src/solvers/highs.rs index 29d0c86..4bd78ce 100644 --- a/src/solvers/highs.rs +++ b/src/solvers/highs.rs @@ -27,7 +27,7 @@ pub fn highs(to_solve: UnsolvedProblem) -> HighsProblem { }; let mut columns = Vec::with_capacity(to_solve.variables.len()); let mut initial_solution = Vec::with_capacity(to_solve.variables.initial_solution_len()); - + for ( var, &VariableDefinition { From 817deb10043f2dd23b9e7193dd55770365c76988 Mon Sep 17 00:00:00 2001 From: KnorpelSenf <shtrog@gmail.com> Date: Wed, 22 Jan 2025 09:42:47 +0000 Subject: [PATCH 28/29] test: cover initial variable values for HiGHS --- src/solvers/highs.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/solvers/highs.rs b/src/solvers/highs.rs index 4bd78ce..29b7ca8 100644 --- a/src/solvers/highs.rs +++ b/src/solvers/highs.rs @@ -435,6 +435,34 @@ mod tests { assert_eq!((solution.value(x), solution.value(y)), (0.5, 3.)) } + #[test] + fn can_solve_with_initial_variable_values() { + // Solve problem initially + 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) + .solve() + .unwrap(); + // Recreate same problem with initial values slightly off + let initial_x = solution.value(x) - 0.1; + let initial_y = solution.value(x) - 1.0; + let mut vars = variables!(); + let x = vars.add(variable().clamp(0, 2).initial(initial_x)); + let y = vars.add(variable().clamp(1, 3).initial(initial_y)); + let solution = vars + .maximise(x + y) + .using(highs) + .with((2 * x + y) << 4) + .solve() + .unwrap(); + + assert_eq!((solution.value(x), solution.value(y)), (0.5, 3.)) + } + #[test] fn can_solve_with_equality() { let mut vars = variables!(); From 5d9904ce75c4f03bd0e815ce93d1918dd94027d3 Mon Sep 17 00:00:00 2001 From: KnorpelSenf <shtrog@gmail.com> Date: Wed, 22 Jan 2025 13:53:59 +0100 Subject: [PATCH 29/29] test: add time_limit=0 to tests for hot starts --- src/solvers/highs.rs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/solvers/highs.rs b/src/solvers/highs.rs index 29b7ca8..3e5d98e 100644 --- a/src/solvers/highs.rs +++ b/src/solvers/highs.rs @@ -418,9 +418,9 @@ mod tests { .with((2 * x + y) << 4) .solve() .unwrap(); - // Recreate same problem with initial values slightly off - let initial_x = solution.value(x) - 0.1; - let initial_y = solution.value(x) - 1.0; + let initial_x = solution.value(x); + let initial_y = solution.value(y); + // Recreate same problem with initial values let mut vars = variables!(); let x = vars.add(variable().clamp(0, 2)); let y = vars.add(variable().clamp(1, 3)); @@ -429,10 +429,11 @@ mod tests { .using(highs) .with((2 * x + y) << 4) .with_initial_solution([(x, initial_x), (y, initial_y)]) + .set_time_limit(0.0) .solve() .unwrap(); - assert_eq!((solution.value(x), solution.value(y)), (0.5, 3.)) + assert_eq!((solution.value(x), solution.value(y)), (0.5, 3.)); } #[test] @@ -447,9 +448,9 @@ mod tests { .with((2 * x + y) << 4) .solve() .unwrap(); - // Recreate same problem with initial values slightly off - let initial_x = solution.value(x) - 0.1; - let initial_y = solution.value(x) - 1.0; + let initial_x = solution.value(x); + let initial_y = solution.value(y); + // Recreate same problem with initial values let mut vars = variables!(); let x = vars.add(variable().clamp(0, 2).initial(initial_x)); let y = vars.add(variable().clamp(1, 3).initial(initial_y)); @@ -457,10 +458,11 @@ mod tests { .maximise(x + y) .using(highs) .with((2 * x + y) << 4) + .set_time_limit(0.0) .solve() .unwrap(); - assert_eq!((solution.value(x), solution.value(y)), (0.5, 3.)) + assert_eq!((solution.value(x), solution.value(y)), (0.5, 3.)); } #[test]