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]