From 1979e3b68bba9d566f8c66b29451620aa1780dd5 Mon Sep 17 00:00:00 2001 From: Christoph Jabs Date: Fri, 18 Oct 2024 12:14:25 +0300 Subject: [PATCH 1/6] feat: allow adding integral columns to `Model` --- src/lib.rs | 84 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 77 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 3176008..65b7009 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -425,25 +425,76 @@ impl Model { /// # Panics /// /// If HIGHS returns an error status value. - pub fn add_col( + pub fn add_col + Copy, B: RangeBounds>( &mut self, col_factor: f64, - bounds: impl RangeBounds, + bounds: B, row_factors: impl IntoIterator, ) -> Col { - self.try_add_column(col_factor, bounds, row_factors) + self.try_add_column_with_integrality(col_factor, bounds, row_factors, false) .unwrap_or_else(|e| panic!("HiGHS error: {e:?}")) } /// Tries to add a new variable to the highs model. /// /// Returns the added column index, or the error status value if HIGHS returned an error status. - pub fn try_add_column( + pub fn try_add_column + Copy, B: RangeBounds>( &mut self, col_factor: f64, - bounds: impl RangeBounds, + bounds: B, row_factors: impl IntoIterator, ) -> Result { + self.try_add_column_with_integrality(col_factor, bounds, row_factors, false) + } + + /// Same as [`Model::add_column`], but adds an _integer_ column + pub fn add_integer_column + Copy, B: RangeBounds>( + &mut self, + col_factor: f64, + bounds: B, + row_factors: impl IntoIterator, + ) -> Col { + self.try_add_column_with_integrality(col_factor, bounds, row_factors, true) + .unwrap_or_else(|e| panic!("HiGHS error: {e:?}")) + } + + /// Same as [`Model::add_column`], but adds an _integer_ column + pub fn try_add_integer_column + Copy, B: RangeBounds>( + &mut self, + col_factor: f64, + bounds: B, + row_factors: impl IntoIterator, + ) -> Result { + self.try_add_column_with_integrality(col_factor, bounds, row_factors, true) + } + + /// Same as [`Model::add_column`], but lets you define whether the new variable should be + /// integral or continuous. + #[inline] + pub fn add_column_with_integrality + Copy, B: RangeBounds>( + &mut self, + col_factor: f64, + bounds: B, + row_factors: impl IntoIterator, + is_integer: bool, + ) -> Col { + self.try_add_column_with_integrality(col_factor, bounds, row_factors, is_integer) + .unwrap_or_else(|e| panic!("HiGHS error: {e:?}")) + } + + /// Same as [`Model::try_add_column`] but lets you define whether the new variable should be + /// integral or continuous. + pub fn try_add_column_with_integrality( + &mut self, + col_factor: f64, + bounds: B, + row_factors: impl IntoIterator, + is_integer: bool, + ) -> Result + where + N: Into + Copy, + B: RangeBounds, + { let (rows, factors): (Vec<_>, Vec<_>) = row_factors.into_iter().unzip(); unsafe { highs_call!(Highs_addCol( @@ -454,8 +505,17 @@ impl Model { rows.len().try_into().unwrap(), rows.into_iter().map(|r| r.0).collect::>().as_ptr(), factors.as_ptr() - )) - }?; + ))?; + } + if is_integer { + unsafe { + highs_call!(Highs_changeColIntegrality( + self.highs.mut_ptr(), + (self.highs.num_cols()? - 1).try_into().unwrap(), + is_integer.into() + ))?; + } + } Ok(Col(self.highs.num_cols()? - 1)) } @@ -857,4 +917,14 @@ mod test { let solved = m.solve(); assert_eq!(solved.objective_value(), 0.0); } + + #[test] + fn test_adding_integer_column() { + let mut model = RowProblem::new().optimise(Sense::Minimise); + let a = model.add_integer_column(1., 0..1, []); + let b = model.add_integer_column(1., 0..1, []); + model.add_row(1.5.., [(a, 1.), (b, 1.)]); + let solved = model.solve(); + assert_eq!(solved.objective_value(), 2.0); + } } From bf19f33eb0d206ef99d44189c90f2da0453fe99c Mon Sep 17 00:00:00 2001 From: Christoph Jabs Date: Fri, 18 Oct 2024 12:58:55 +0300 Subject: [PATCH 2/6] feat: make `Model::add_row` similar to `Problem::add_row` --- src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 65b7009..1c972d4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -382,9 +382,9 @@ impl Model { /// # Panics /// /// If HIGHS returns an error status value. - pub fn add_row( + pub fn add_row + Copy, B: RangeBounds>( &mut self, - bounds: impl RangeBounds, + bounds: B, row_factors: impl IntoIterator, ) -> Row { self.try_add_row(bounds, row_factors) @@ -394,9 +394,9 @@ impl Model { /// Tries to add a new constraint to the highs model. /// /// Returns the added row index, or the error status value if HIGHS returned an error status. - pub fn try_add_row( + pub fn try_add_row + Copy, B: RangeBounds>( &mut self, - bounds: impl RangeBounds, + bounds: B, row_factors: impl IntoIterator, ) -> Result { let (cols, factors): (Vec<_>, Vec<_>) = row_factors.into_iter().unzip(); From b97167e97ed51af9e11a2bd09a40bf3a1348737f Mon Sep 17 00:00:00 2001 From: Christoph Jabs Date: Fri, 18 Oct 2024 12:15:12 +0300 Subject: [PATCH 3/6] feat: allow getting index of column --- src/matrix_row.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/matrix_row.rs b/src/matrix_row.rs index ad9f335..8a22f55 100644 --- a/src/matrix_row.rs +++ b/src/matrix_row.rs @@ -11,6 +11,13 @@ use crate::Problem; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Col(pub(crate) usize); +impl Col { + /// Gets the index of the column + pub fn index(self) -> usize { + self.0 + } +} + /// A complete optimization problem stored by row #[derive(Debug, Clone, PartialEq, Default)] pub struct RowMatrix { From d71288c16490d4ee87e4f10be79a4412b9b5e75f Mon Sep 17 00:00:00 2001 From: Christoph Jabs Date: Fri, 2 May 2025 10:49:34 +0300 Subject: [PATCH 4/6] feat: allow for changing column cost --- src/lib.rs | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 1c972d4..b7aac84 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -215,6 +215,11 @@ where pub fn new() -> Self { Self::default() } + + /// Updates the cost of a column + pub fn change_column_cost(&mut self, col: Col, cost: f64) { + self.colcost[col.index()] = cost + } } fn bound_value + Copy>(b: Bound<&N>) -> Option { @@ -520,6 +525,18 @@ impl Model { Ok(Col(self.highs.num_cols()? - 1)) } + /// Updates the cost of a column + pub fn change_column_cost(&mut self, col: Col, cost: f64) { + unsafe { + highs_call!(Highs_changeColCost( + self.highs.mut_ptr(), + col.index() as c_int, + cost + )) + .unwrap_or_else(|e| panic!("HiGHS error: {e:?}")); + } + } + /// Hot-starts at the initial guess. See HIGHS documentation for further details. /// /// # Panics @@ -927,4 +944,27 @@ mod test { let solved = model.solve(); assert_eq!(solved.objective_value(), 2.0); } + + #[test] + fn test_problem_change_column_cost() { + let mut problem = RowProblem::new(); + let x = problem.add_column(1., 1..); + let solved = problem.clone().optimise(Sense::Minimise).solve(); + assert_eq!(solved.objective_value(), 1.0); + problem.change_column_cost(x, 2.); + let solved = problem.optimise(Sense::Minimise).solve(); + assert_eq!(solved.objective_value(), 2.0); + } + + #[test] + fn test_model_change_column_cost() { + let mut problem = RowProblem::new(); + let x = problem.add_column(1., 1..); + let solved = problem.optimise(Sense::Minimise).solve(); + assert_eq!(solved.objective_value(), 1.0); + let mut model: crate::Model = solved.into(); + model.change_column_cost(x, 2.); + let solved = model.solve(); + assert_eq!(solved.objective_value(), 2.0); + } } From 0c6926aa6896bd23224432859d9f6be238f5fa1f Mon Sep 17 00:00:00 2001 From: Christoph Jabs Date: Thu, 12 Jun 2025 17:03:48 +0300 Subject: [PATCH 5/6] feat: allow for geting number of columns and rows from `Model` --- src/lib.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index b7aac84..41d6200 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -281,6 +281,16 @@ impl Model { assert_eq!(ret, STATUS_OK, "changeObjectiveSense failed"); } + /// Gets the number of columns in the model + pub fn num_cols(&self) -> usize { + self.highs.num_cols().expect("num cols does not fit usize") + } + + /// Gets the number of rows in the model + pub fn num_rows(&self) -> usize { + self.highs.num_rows().expect("num rows does not fit usize") + } + /// Create a Highs model to be optimized (but don't solve it yet). /// If the given problem is a [RowProblem], it will have to be converted to a [ColProblem] first, /// which takes an amount of time proportional to the size of the problem. @@ -967,4 +977,15 @@ mod test { let solved = model.solve(); assert_eq!(solved.objective_value(), 2.0); } + + #[test] + fn test_num_cols_and_rows() { + let mut problem = RowProblem::new(); + let x = problem.add_column(1., -1..); + let y = problem.add_column(1., 0..); + problem.add_row(..1, [(x, 1.), (y, 1.)]); + let model = problem.optimise(Sense::Minimise); + assert_eq!(model.num_cols(), 2); + assert_eq!(model.num_rows(), 1); + } } From f3152eeca649799e99c061b6d4946d44d9e9e40d Mon Sep 17 00:00:00 2001 From: Christoph Jabs Date: Tue, 17 Jun 2025 14:31:24 +0300 Subject: [PATCH 6/6] feat: allow for updating bounds of columns --- src/lib.rs | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 41d6200..0f7e8a7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -220,6 +220,18 @@ where pub fn change_column_cost(&mut self, col: Col, cost: f64) { self.colcost[col.index()] = cost } + + /// Updates the bounds of a column + pub fn change_column_bounds + Copy, B: RangeBounds>( + &mut self, + col: Col, + bounds: B, + ) { + let low = bound_value(bounds.start_bound()).unwrap_or(f64::NEG_INFINITY); + let high = bound_value(bounds.end_bound()).unwrap_or(f64::INFINITY); + self.collower[col.index()] = low; + self.colupper[col.index()] = high; + } } fn bound_value + Copy>(b: Bound<&N>) -> Option { @@ -547,6 +559,25 @@ impl Model { } } + /// Updates the bounds of a column + pub fn change_column_bounds + Copy, B: RangeBounds>( + &mut self, + col: Col, + bounds: B, + ) { + let low = bound_value(bounds.start_bound()).unwrap_or(f64::NEG_INFINITY); + let high = bound_value(bounds.end_bound()).unwrap_or(f64::INFINITY); + unsafe { + highs_call!(Highs_changeColBounds( + self.highs.mut_ptr(), + col.index() as c_int, + low, + high + )) + .unwrap_or_else(|e| panic!("HiGHS error: {e:?}")); + } + } + /// Hot-starts at the initial guess. See HIGHS documentation for further details. /// /// # Panics @@ -988,4 +1019,27 @@ mod test { assert_eq!(model.num_cols(), 2); assert_eq!(model.num_rows(), 1); } + + #[test] + fn test_problem_change_column_bounds() { + let mut problem = RowProblem::new(); + let x = problem.add_column(1., 0..); + let solved = problem.clone().optimise(Sense::Minimise).solve(); + assert_eq!(solved.objective_value(), 0.0); + problem.change_column_bounds(x, 1..); + let solved = problem.optimise(Sense::Minimise).solve(); + assert_eq!(solved.objective_value(), 1.0); + } + + #[test] + fn test_model_change_column_bounds() { + let mut problem = RowProblem::new(); + let x = problem.add_column(1., 0..); + let solved = problem.optimise(Sense::Minimise).solve(); + assert_eq!(solved.objective_value(), 0.0); + let mut model: crate::Model = solved.into(); + model.change_column_bounds(x, 1..); + let solved = model.solve(); + assert_eq!(solved.objective_value(), 1.0); + } }