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: support setting initial values for individual variables #75

Open
wants to merge 32 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
4ba2ee2
feat: allow defining initial values for individual variables
KnorpelSenf Dec 30, 2024
fb9ab66
feat: support initial variable values for cbc
KnorpelSenf Dec 30, 2024
ccd790d
feat: support initial variable values for SCIP
KnorpelSenf Dec 30, 2024
3c06bc3
docs: fix typo in `variable.initial()` docs
KnorpelSenf Dec 30, 2024
b242224
fix: compile
KnorpelSenf Dec 30, 2024
cb1851b
fix: parse
KnorpelSenf Dec 30, 2024
814ddaa
perf: track and leverage initial solution size
KnorpelSenf Jan 7, 2025
6f9f1f4
docs: add doctest for initial_solution_len
KnorpelSenf Jan 8, 2025
33ae2db
perf: track and leverage initial solution size
KnorpelSenf Jan 12, 2025
00261df
build: switch over to highs bindings from git
KnorpelSenf Jan 15, 2025
d4447da
feat: support initial solutions for HiGHS
KnorpelSenf Jan 15, 2025
8304a48
test: add first tests for HiGHS solver
KnorpelSenf Jan 15, 2025
b7fd997
explain more in readme
lovasoa Dec 30, 2024
820d9f5
Docs: add readme details about restricting variables to have integer …
nik-sm Jan 2, 2025
3156ca5
chore: do not bundle scip on docs.rs (#81)
KnorpelSenf Jan 8, 2025
10d50f3
build: switch over to highs bindings from git
KnorpelSenf Jan 15, 2025
1bcdbd3
feat: support initial solutions for HiGHS
KnorpelSenf Jan 15, 2025
43d9687
test: add first tests for HiGHS solver
KnorpelSenf Jan 15, 2025
8e0cb62
build: update highs to 1.7.0
KnorpelSenf Jan 22, 2025
7daf028
Revert "build: update highs to 1.7.0"
KnorpelSenf Jan 22, 2025
78e1a4b
build: update highs and russcip
KnorpelSenf Jan 22, 2025
fd39501
Merge branch 'highs-initial-solutions' into initial-variable-values
KnorpelSenf Jan 22, 2025
f1dcedb
refactor: drop unused imports
KnorpelSenf Jan 22, 2025
376f960
style: fix lint for empty check
KnorpelSenf Jan 22, 2025
7ce4355
Revert "refactor: drop unused imports"
KnorpelSenf Jan 22, 2025
d1eac34
test: drop test for byte size
KnorpelSenf Jan 22, 2025
cb82317
Merge branch 'main' into initial-variable-values
KnorpelSenf Jan 22, 2025
dbe460b
refactor: prefer is_empty over >0
KnorpelSenf Jan 22, 2025
8bd3419
feat: add support for initial solutions with HiGHS
KnorpelSenf Jan 22, 2025
9085fd1
style: fix formatting
KnorpelSenf Jan 22, 2025
817deb1
test: cover initial variable values for HiGHS
KnorpelSenf Jan 22, 2025
5d9904c
test: add time_limit=0 to tests for hot starts
KnorpelSenf Jan 22, 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
49 changes: 40 additions & 9 deletions src/solvers/coin_cbc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::with_capacity(variables.initial_solution_len());
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);
Expand All @@ -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
},
)
Expand All @@ -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.is_empty() {
problem = problem.with_initial_solution(initial_solution);
}
problem
}

/// A coin-cbc model
Expand Down Expand Up @@ -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]
Expand All @@ -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);
}
}
1 change: 1 addition & 0 deletions src/solvers/cplex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
50 changes: 45 additions & 5 deletions src/solvers/highs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
..
},
Expand All @@ -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
Expand Down Expand Up @@ -408,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));
Expand All @@ -419,10 +429,40 @@ 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]
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();
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));
let solution = vars
.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.));
}

#[test]
Expand Down
39 changes: 38 additions & 1 deletion src/solvers/scip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::with_capacity(to_solve.variables.initial_solution_len());

for (
var,
&VariableDefinition {
min,
max,
initial,
is_integer,
ref name,
},
Expand All @@ -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.is_empty() {
problem = problem.with_initial_solution(initial_solution);
}
problem
}

/// A SCIP Model
Expand Down Expand Up @@ -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!();
Expand Down
46 changes: 45 additions & 1 deletion src/variable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand All @@ -128,6 +129,7 @@ impl VariableDefinition {
VariableDefinition {
min: f64::NEG_INFINITY,
max: f64::INFINITY,
initial: None,
name: String::new(),
is_integer: false,
}
Expand Down Expand Up @@ -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 initial solutions.
/// 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.
///
Expand Down Expand Up @@ -262,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,
KnorpelSenf marked this conversation as resolved.
Show resolved Hide resolved
}
}

/// Add a anonymous unbounded continuous variable to the problem
Expand All @@ -289,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)
}
Expand Down Expand Up @@ -390,6 +420,20 @@ impl ProblemVariables {
self.variables.is_empty()
}

/// 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
}

/// Display the given expression or constraint with the correct variable names
///
/// ```
Expand Down
16 changes: 0 additions & 16 deletions tests/variables.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Loading