From 67ae8cc5d4905ad541fd22cfd44c801d43349023 Mon Sep 17 00:00:00 2001 From: KnorpelSenf Date: Fri, 13 Dec 2024 10:32:25 +0000 Subject: [PATCH 01/13] feat: define trait for initial solutions --- src/lib.rs | 2 +- src/solvers/mod.rs | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index defe34b..fab392f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -134,7 +134,7 @@ pub use solvers::scip::scip as default_solver; pub use solvers::{ solver_name, DualValues, ModelWithSOS1, ResolutionError, Solution, SolutionWithDual, Solver, - SolverModel, StaticSolver, WithMipGap, + SolverModel, StaticSolver, WithInitialSolution, WithMipGap, }; pub use variable::{variable, ProblemVariables, Variable, VariableDefinition}; diff --git a/src/solvers/mod.rs b/src/solvers/mod.rs index 0b2423a..355a3f8 100644 --- a/src/solvers/mod.rs +++ b/src/solvers/mod.rs @@ -199,6 +199,15 @@ pub trait SolverModel { fn name() -> &'static str; } +/// A solver that can take an initial solution to a problem before solving it +pub trait WithInitialSolution { + /// The type of solution to the problem + type Solution: Solution; + + /// Sets the initial solution to the problem + fn set_initial_solution(self, solution: &Self::Solution) -> Self; +} + /// A problem solution pub trait Solution { /// Get the optimal value of a variable of the problem From 630baf103152fa002386b3c34733ef69b688e912 Mon Sep 17 00:00:00 2001 From: KnorpelSenf Date: Fri, 13 Dec 2024 10:32:40 +0000 Subject: [PATCH 02/13] feat: add support for initial solutions to cbc --- src/solvers/coin_cbc.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/solvers/coin_cbc.rs b/src/solvers/coin_cbc.rs index 291714b..b7f624d 100644 --- a/src/solvers/coin_cbc.rs +++ b/src/solvers/coin_cbc.rs @@ -5,7 +5,7 @@ use std::convert::TryInto; use coin_cbc::{raw::Status, Col, Model, Sense, Solution as CbcSolution}; -use crate::solvers::{MipGapError, ModelWithSOS1, WithMipGap}; +use crate::solvers::{MipGapError, ModelWithSOS1, WithInitialSolution, WithMipGap}; use crate::variable::{UnsolvedProblem, VariableDefinition}; use crate::{ constraint::ConstraintReference, @@ -166,6 +166,15 @@ impl SolverModel for CoinCbcProblem { } } +impl WithInitialSolution for CoinCbcProblem { + type Solution = CoinCbcSolution; + + fn set_initial_solution(mut self, solution: &Self::Solution) -> Self { + self.model.set_initial_solution(&solution.solution); + self + } +} + /// Unfortunately, the current version of cbc silently ignores /// sos constraints on continuous variables. /// See From 5b7963cf3fca71738a8ea49f7b79a18fee2c9445 Mon Sep 17 00:00:00 2001 From: KnorpelSenf Date: Fri, 13 Dec 2024 10:35:48 +0000 Subject: [PATCH 03/13] test: cover setting initial solutions for cbc --- tests/solver_scaling.rs | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/tests/solver_scaling.rs b/tests/solver_scaling.rs index 878c0bd..c06cd45 100644 --- a/tests/solver_scaling.rs +++ b/tests/solver_scaling.rs @@ -1,5 +1,8 @@ use float_eq::assert_float_eq; -use good_lp::{constraint, default_solver, variable, variables, Expression, Solution, SolverModel}; +use good_lp::{ + constraint, default_solver, variable, variables, Expression, Solution, SolverModel, + WithInitialSolution, +}; #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::*; const BIG_NUM: usize = 1000; // <- Set this higher to test how good_lp and the solvers scale @@ -21,6 +24,41 @@ fn solve_large_problem() { } } +#[test] +fn solve_problem_with_initial_solution() { + // Solve problem once + let mut vars = variables!(); + let min = -((BIG_NUM / 2) as f64); + let max = (BIG_NUM / 2 - 1) as f64; + let v = vars.add_vector(variable().min(min).max(max), BIG_NUM); + let objective: Expression = v.iter().sum(); + let mut pb = vars.maximise(objective).using(default_solver); + for vs in v.windows(2) { + pb = pb.with(constraint!(vs[0] + 1 <= vs[1])); + } + let sol = pb.solve().unwrap(); + for (i, var) in v.iter().enumerate() { + assert_float_eq!(sol.value(*var), min + i as f64, abs <= 1e-8); + } + // Recreate problem and solve with initial solution + let mut vars = variables!(); + let min = -((BIG_NUM / 2) as f64); + let max = (BIG_NUM / 2 - 1) as f64; + let v = vars.add_vector(variable().min(min).max(max), BIG_NUM); + let objective: Expression = v.iter().sum(); + let mut pb = vars + .maximise(objective) + .using(default_solver) + .set_initial_solution(&sol); + for vs in v.windows(2) { + pb = pb.with(constraint!(vs[0] + 1 <= vs[1])); + } + let sol = pb.solve().expect("problem has no solution"); + for (i, var) in v.iter().enumerate() { + assert_float_eq!(sol.value(*var), min + i as f64, abs <= 1e-8); + } +} + #[test] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] fn add_10_000_constraints() { From edfe50f022d911d054096be8b5759592dc332b05 Mon Sep 17 00:00:00 2001 From: KnorpelSenf Date: Fri, 13 Dec 2024 12:05:53 +0100 Subject: [PATCH 04/13] refactor: rename set_initial_solution to with_initial_solution Co-authored-by: Ophir LOJKINE --- src/solvers/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/solvers/mod.rs b/src/solvers/mod.rs index 355a3f8..4b61854 100644 --- a/src/solvers/mod.rs +++ b/src/solvers/mod.rs @@ -205,7 +205,7 @@ pub trait WithInitialSolution { type Solution: Solution; /// Sets the initial solution to the problem - fn set_initial_solution(self, solution: &Self::Solution) -> Self; + fn with_initial_solution(self, solution: &Self::Solution) -> Self; } /// A problem solution From 431be97b0a372a4519fde3d64a6ebfd21f2555d0 Mon Sep 17 00:00:00 2001 From: KnorpelSenf Date: Fri, 13 Dec 2024 11:11:06 +0000 Subject: [PATCH 05/13] fix: adjust trait usage --- 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 b7f624d..179903b 100644 --- a/src/solvers/coin_cbc.rs +++ b/src/solvers/coin_cbc.rs @@ -169,7 +169,7 @@ impl SolverModel for CoinCbcProblem { impl WithInitialSolution for CoinCbcProblem { type Solution = CoinCbcSolution; - fn set_initial_solution(mut self, solution: &Self::Solution) -> Self { + fn with_initial_solution(mut self, solution: &Self::Solution) -> Self { self.model.set_initial_solution(&solution.solution); self } From d61f21304927146430fc3e0a963923eb25d6d8bf Mon Sep 17 00:00:00 2001 From: KnorpelSenf Date: Fri, 13 Dec 2024 11:13:38 +0000 Subject: [PATCH 06/13] test: adjust trait usage --- tests/solver_scaling.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/solver_scaling.rs b/tests/solver_scaling.rs index c06cd45..dbe9d9d 100644 --- a/tests/solver_scaling.rs +++ b/tests/solver_scaling.rs @@ -49,7 +49,7 @@ fn solve_problem_with_initial_solution() { let mut pb = vars .maximise(objective) .using(default_solver) - .set_initial_solution(&sol); + .with_initial_solution(&sol); for vs in v.windows(2) { pb = pb.with(constraint!(vs[0] + 1 <= vs[1])); } From 23ab507fd0d664df8af48d45438489f1d2f92d86 Mon Sep 17 00:00:00 2001 From: KnorpelSenf Date: Fri, 13 Dec 2024 11:21:24 +0000 Subject: [PATCH 07/13] test: simplify test for initial solution --- tests/solver_scaling.rs | 41 +++++++++++++++-------------------------- 1 file changed, 15 insertions(+), 26 deletions(-) diff --git a/tests/solver_scaling.rs b/tests/solver_scaling.rs index dbe9d9d..f04e6d6 100644 --- a/tests/solver_scaling.rs +++ b/tests/solver_scaling.rs @@ -26,37 +26,26 @@ fn solve_large_problem() { #[test] fn solve_problem_with_initial_solution() { + let limit = 3.0; // Solve problem once - let mut vars = variables!(); - let min = -((BIG_NUM / 2) as f64); - let max = (BIG_NUM / 2 - 1) as f64; - let v = vars.add_vector(variable().min(min).max(max), BIG_NUM); - let objective: Expression = v.iter().sum(); - let mut pb = vars.maximise(objective).using(default_solver); - for vs in v.windows(2) { - pb = pb.with(constraint!(vs[0] + 1 <= vs[1])); - } + variables! { + vars: + 0.0 <= v <= limit; + }; + let pb = vars.maximise(v).using(default_solver); let sol = pb.solve().unwrap(); - for (i, var) in v.iter().enumerate() { - assert_float_eq!(sol.value(*var), min + i as f64, abs <= 1e-8); - } + assert_float_eq!(sol.value(v), limit, abs <= 1e-8); // Recreate problem and solve with initial solution - let mut vars = variables!(); - let min = -((BIG_NUM / 2) as f64); - let max = (BIG_NUM / 2 - 1) as f64; - let v = vars.add_vector(variable().min(min).max(max), BIG_NUM); - let objective: Expression = v.iter().sum(); - let mut pb = vars - .maximise(objective) + variables! { + vars: + 0.0 <= v <= limit; + }; + let pb = vars + .maximise(v) .using(default_solver) .with_initial_solution(&sol); - for vs in v.windows(2) { - pb = pb.with(constraint!(vs[0] + 1 <= vs[1])); - } - let sol = pb.solve().expect("problem has no solution"); - for (i, var) in v.iter().enumerate() { - assert_float_eq!(sol.value(*var), min + i as f64, abs <= 1e-8); - } + let sol = pb.solve().unwrap(); + assert_float_eq!(sol.value(v), limit, abs <= 1e-8); } #[test] From 4c96c017ed1e90cabc5c95f3099650466b9b2676 Mon Sep 17 00:00:00 2001 From: KnorpelSenf Date: Fri, 13 Dec 2024 12:11:06 +0000 Subject: [PATCH 08/13] test: only test initial solution for cbc --- src/solvers/coin_cbc.rs | 30 ++++++++++++++++++++++++++++++ tests/solver_scaling.rs | 29 +---------------------------- 2 files changed, 31 insertions(+), 28 deletions(-) diff --git a/src/solvers/coin_cbc.rs b/src/solvers/coin_cbc.rs index 179903b..ccdd367 100644 --- a/src/solvers/coin_cbc.rs +++ b/src/solvers/coin_cbc.rs @@ -227,3 +227,33 @@ impl WithMipGap for CoinCbcProblem { } } } + +#[cfg(test)] +mod tests { + use crate::{variables, Solution, SolverModel, WithInitialSolution}; + use float_eq::assert_float_eq; + + #[test] + fn solve_problem_with_initial_solution() { + 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 + variables! { + vars: + 0.0 <= v <= limit; + }; + let pb = vars + .maximise(v) + .using(super::coin_cbc) + .with_initial_solution(&sol); + let sol = pb.solve().unwrap(); + assert_float_eq!(sol.value(v), limit, abs <= 1e-8); + } +} diff --git a/tests/solver_scaling.rs b/tests/solver_scaling.rs index f04e6d6..878c0bd 100644 --- a/tests/solver_scaling.rs +++ b/tests/solver_scaling.rs @@ -1,8 +1,5 @@ use float_eq::assert_float_eq; -use good_lp::{ - constraint, default_solver, variable, variables, Expression, Solution, SolverModel, - WithInitialSolution, -}; +use good_lp::{constraint, default_solver, variable, variables, Expression, Solution, SolverModel}; #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::*; const BIG_NUM: usize = 1000; // <- Set this higher to test how good_lp and the solvers scale @@ -24,30 +21,6 @@ fn solve_large_problem() { } } -#[test] -fn solve_problem_with_initial_solution() { - let limit = 3.0; - // Solve problem once - variables! { - vars: - 0.0 <= v <= limit; - }; - let pb = vars.maximise(v).using(default_solver); - let sol = pb.solve().unwrap(); - assert_float_eq!(sol.value(v), limit, abs <= 1e-8); - // Recreate problem and solve with initial solution - variables! { - vars: - 0.0 <= v <= limit; - }; - let pb = vars - .maximise(v) - .using(default_solver) - .with_initial_solution(&sol); - let sol = pb.solve().unwrap(); - assert_float_eq!(sol.value(v), limit, abs <= 1e-8); -} - #[test] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] fn add_10_000_constraints() { From f75d800b46fd93617b2e30a2627ccd2a07aef6e3 Mon Sep 17 00:00:00 2001 From: KnorpelSenf Date: Thu, 26 Dec 2024 10:56:31 +0100 Subject: [PATCH 09/13] feat: draft construction of initial solutions --- src/solvers/coin_cbc.rs | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/solvers/coin_cbc.rs b/src/solvers/coin_cbc.rs index ccdd367..e79cd15 100644 --- a/src/solvers/coin_cbc.rs +++ b/src/solvers/coin_cbc.rs @@ -2,6 +2,7 @@ //! This solver is activated using the default `coin_cbc` feature. //! You can disable it an enable another solver instead using cargo features. use std::convert::TryInto; +use std::iter::FromIterator; use coin_cbc::{raw::Status, Col, Model, Sense, Solution as CbcSolution}; @@ -175,6 +176,18 @@ impl WithInitialSolution for CoinCbcProblem { } } +impl FromIterator<(Variable, f64)> for CoinCbcSolution { + fn from_iter>(iter: T) -> Self { + CoinCbcSolution { + solution: CbcSolution { + raw: todo!(), + col_solution: todo!(), + }, + solution_vec: iter.into_iter().map(|(_, value)| value).collect(), + } + } +} + /// Unfortunately, the current version of cbc silently ignores /// sos constraints on continuous variables. /// See @@ -230,7 +243,11 @@ impl WithMipGap for CoinCbcProblem { #[cfg(test)] mod tests { - use crate::{variables, Solution, SolverModel, WithInitialSolution}; + use std::iter::FromIterator; + + use crate::{ + solvers::coin_cbc::CoinCbcSolution, variables, Solution, SolverModel, WithInitialSolution, + }; use float_eq::assert_float_eq; #[test] @@ -245,6 +262,7 @@ mod tests { let sol = pb.solve().unwrap(); assert_float_eq!(sol.value(v), limit, abs <= 1e-8); // Recreate problem and solve with initial solution + let initial_solution = CoinCbcSolution::from_iter(vec![(v, sol.value(v))]); variables! { vars: 0.0 <= v <= limit; @@ -252,7 +270,7 @@ mod tests { let pb = vars .maximise(v) .using(super::coin_cbc) - .with_initial_solution(&sol); + .with_initial_solution(&initial_solution); let sol = pb.solve().unwrap(); assert_float_eq!(sol.value(v), limit, abs <= 1e-8); } From c0cbadad790ab0a4000ba1a9c81b08104b61555b Mon Sep 17 00:00:00 2001 From: KnorpelSenf Date: Fri, 27 Dec 2024 23:25:02 +0000 Subject: [PATCH 10/13] feat: make it work on vectors --- src/solvers/coin_cbc.rs | 24 ++++++------------------ src/solvers/mod.rs | 5 +---- 2 files changed, 7 insertions(+), 22 deletions(-) diff --git a/src/solvers/coin_cbc.rs b/src/solvers/coin_cbc.rs index e79cd15..1c87be2 100644 --- a/src/solvers/coin_cbc.rs +++ b/src/solvers/coin_cbc.rs @@ -2,7 +2,6 @@ //! This solver is activated using the default `coin_cbc` feature. //! You can disable it an enable another solver instead using cargo features. use std::convert::TryInto; -use std::iter::FromIterator; use coin_cbc::{raw::Status, Col, Model, Sense, Solution as CbcSolution}; @@ -168,23 +167,12 @@ impl SolverModel for CoinCbcProblem { } impl WithInitialSolution for CoinCbcProblem { - type Solution = CoinCbcSolution; - - fn with_initial_solution(mut self, solution: &Self::Solution) -> Self { - self.model.set_initial_solution(&solution.solution); - self - } -} - -impl FromIterator<(Variable, f64)> for CoinCbcSolution { - fn from_iter>(iter: T) -> Self { - CoinCbcSolution { - solution: CbcSolution { - raw: todo!(), - col_solution: todo!(), - }, - solution_vec: iter.into_iter().map(|(_, value)| value).collect(), + fn with_initial_solution(mut self, solution: &Vec<(Variable, f64)>) -> Self { + for (var, val) in solution { + self.model + .set_col_initial_solution(self.columns[var.index()], *val); } + self } } @@ -262,7 +250,7 @@ mod tests { let sol = pb.solve().unwrap(); assert_float_eq!(sol.value(v), limit, abs <= 1e-8); // Recreate problem and solve with initial solution - let initial_solution = CoinCbcSolution::from_iter(vec![(v, sol.value(v))]); + let initial_solution = vec![(v, sol.value(v))]; variables! { vars: 0.0 <= v <= limit; diff --git a/src/solvers/mod.rs b/src/solvers/mod.rs index 4b61854..e9128e6 100644 --- a/src/solvers/mod.rs +++ b/src/solvers/mod.rs @@ -201,11 +201,8 @@ pub trait SolverModel { /// A solver that can take an initial solution to a problem before solving it pub trait WithInitialSolution { - /// The type of solution to the problem - type Solution: Solution; - /// Sets the initial solution to the problem - fn with_initial_solution(self, solution: &Self::Solution) -> Self; + fn with_initial_solution(self, solution: &Vec<(Variable, f64)>) -> Self; } /// A problem solution From 3bff1ca6de0a8d1290c30a35f9b1938092eceaff Mon Sep 17 00:00:00 2001 From: KnorpelSenf Date: Sun, 29 Dec 2024 14:44:51 +0000 Subject: [PATCH 11/13] feat: support any iterators not just vecs --- src/solvers/coin_cbc.rs | 6 +++--- src/solvers/mod.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/solvers/coin_cbc.rs b/src/solvers/coin_cbc.rs index 1c87be2..98058d2 100644 --- a/src/solvers/coin_cbc.rs +++ b/src/solvers/coin_cbc.rs @@ -167,10 +167,10 @@ impl SolverModel for CoinCbcProblem { } impl WithInitialSolution for CoinCbcProblem { - fn with_initial_solution(mut self, solution: &Vec<(Variable, f64)>) -> Self { + fn with_initial_solution(mut self, solution: impl IntoIterator) -> Self { for (var, val) in solution { self.model - .set_col_initial_solution(self.columns[var.index()], *val); + .set_col_initial_solution(self.columns[var.index()], val); } self } @@ -258,7 +258,7 @@ mod tests { let pb = vars .maximise(v) .using(super::coin_cbc) - .with_initial_solution(&initial_solution); + .with_initial_solution(initial_solution); let sol = pb.solve().unwrap(); assert_float_eq!(sol.value(v), limit, abs <= 1e-8); } diff --git a/src/solvers/mod.rs b/src/solvers/mod.rs index e9128e6..6414191 100644 --- a/src/solvers/mod.rs +++ b/src/solvers/mod.rs @@ -202,7 +202,7 @@ pub trait SolverModel { /// A solver that can take an initial solution to a problem before solving it pub trait WithInitialSolution { /// Sets the initial solution to the problem - fn with_initial_solution(self, solution: &Vec<(Variable, f64)>) -> Self; + fn with_initial_solution(self, solution: impl IntoIterator) -> Self; } /// A problem solution From 807178c64a30d881fffadd0afaa904aced9987f3 Mon Sep 17 00:00:00 2001 From: KnorpelSenf Date: Sun, 29 Dec 2024 14:47:52 +0000 Subject: [PATCH 12/13] style: fix fmt --- src/solvers/coin_cbc.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/solvers/coin_cbc.rs b/src/solvers/coin_cbc.rs index 98058d2..9b1a253 100644 --- a/src/solvers/coin_cbc.rs +++ b/src/solvers/coin_cbc.rs @@ -167,7 +167,10 @@ impl SolverModel for CoinCbcProblem { } impl WithInitialSolution for CoinCbcProblem { - fn with_initial_solution(mut self, solution: impl IntoIterator) -> Self { + fn with_initial_solution( + mut self, + solution: impl IntoIterator, + ) -> Self { for (var, val) in solution { self.model .set_col_initial_solution(self.columns[var.index()], val); From cb8d21e534fd584019a3c82107e7bec3b0175603 Mon Sep 17 00:00:00 2001 From: KnorpelSenf Date: Sun, 29 Dec 2024 14:54:17 +0000 Subject: [PATCH 13/13] test: drop unused imports from test --- src/solvers/coin_cbc.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/solvers/coin_cbc.rs b/src/solvers/coin_cbc.rs index 9b1a253..3c0eb21 100644 --- a/src/solvers/coin_cbc.rs +++ b/src/solvers/coin_cbc.rs @@ -234,11 +234,7 @@ impl WithMipGap for CoinCbcProblem { #[cfg(test)] mod tests { - use std::iter::FromIterator; - - use crate::{ - solvers::coin_cbc::CoinCbcSolution, variables, Solution, SolverModel, WithInitialSolution, - }; + use crate::{variables, Solution, SolverModel, WithInitialSolution}; use float_eq::assert_float_eq; #[test]