From f5ca4fce15b26ca2490f453fe27b94902d7b82b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Heim?= Date: Sat, 30 May 2026 08:39:00 -0300 Subject: [PATCH 1/8] feat: Add Hessian upload API for quadratic objectives --- src/lib.rs | 105 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 10a176e..1d1d807 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -275,6 +275,27 @@ pub enum Sense { Minimise = OBJECTIVE_SENSE_MINIMIZE as isize, } +/// Storage layout of a quadratic objective Hessian passed to +/// [`Model::pass_hessian`]. +#[derive(Clone, Copy, Eq, PartialEq, Debug)] +pub enum HessianFormat { + /// Only the lower triangle of the symmetric Hessian is stored, in + /// compressed sparse column form. This is the usual way to give the + /// Hessian of `0.5 x' Q x`. + Triangular, + /// The full square Hessian is stored in compressed sparse column form. + Square, +} + +impl HessianFormat { + fn as_raw(self) -> HighsInt { + match self { + HessianFormat::Triangular => kHighsHessianFormatTriangular, + HessianFormat::Square => kHighsHessianFormatSquare, + } + } +} + impl Model { /// Return pointer to underlying HiGHS model pub fn as_ptr(&self) -> *const c_void { @@ -677,6 +698,90 @@ impl Model { }?; Ok(()) } + + /// Upload a quadratic objective Hessian `Q`, turning the model into a QP + /// with objective `c'x + 0.5 x' Q x` (where `c` is the linear objective + /// already set on the columns). + /// + /// `Q` is given in compressed sparse column form. For + /// [`HessianFormat::Triangular`] only the lower triangle is stored: + /// `start` has one entry per column (`start.len() == dim`), and `index` / + /// `value` list the row index and coefficient of each stored entry, + /// column by column. + /// + /// HiGHS solves **convex** QPs only: `Q` must be positive semidefinite. + /// HiGHS does not verify this and behaviour on an indefinite `Q` is + /// undefined, so check convexity before calling if the matrix is not PSD + /// by construction. + /// + /// # Panics + /// + /// If HiGHS returns an error status value, or the slice lengths are + /// inconsistent (`start.len() != dim` or `index.len() != value.len()`). + pub fn pass_hessian( + &mut self, + dim: usize, + format: HessianFormat, + start: &[usize], + index: &[usize], + value: &[f64], + ) { + self.try_pass_hessian(dim, format, start, index, value) + .unwrap_or_else(|e| panic!("HiGHS error: {e:?}")) + } + + /// Same as [`Model::pass_hessian`], but returns the error status value + /// instead of panicking. Returns [`HighsStatus::Error`] when the slice + /// lengths are inconsistent. An empty Hessian (`value.is_empty()`) is a + /// no-op and leaves the model linear. + /// + /// ``` + /// use highs::{RowProblem, Sense, HessianFormat, HighsModelStatus}; + /// // min x^2 + y^2 s.t. x + y = 1, x, y in [-10, 10] + /// let mut pb = RowProblem::new(); + /// let x = pb.add_column(0.0, -10.0..=10.0); + /// let y = pb.add_column(0.0, -10.0..=10.0); + /// pb.add_row(1.0..=1.0, [(x, 1.0), (y, 1.0)]); + /// let mut model = pb.optimise(Sense::Minimise); + /// // Hessian of 0.5 x'Qx with Q = diag(2, 2), stored lower-triangular CSC. + /// model + /// .try_pass_hessian(2, HessianFormat::Triangular, &[0, 1], &[0, 1], &[2.0, 2.0]) + /// .unwrap(); + /// let solved = model.solve(); + /// assert_eq!(solved.status(), HighsModelStatus::Optimal); + /// let cols = solved.get_solution().columns().to_vec(); + /// assert!((cols[0] - 0.5).abs() < 1e-6); + /// assert!((cols[1] - 0.5).abs() < 1e-6); + /// ``` + pub fn try_pass_hessian( + &mut self, + dim: usize, + format: HessianFormat, + start: &[usize], + index: &[usize], + value: &[f64], + ) -> Result<(), HighsStatus> { + if start.len() != dim || index.len() != value.len() { + return Err(HighsStatus::Error); + } + if value.is_empty() { + return Ok(()); + } + let start: Vec = start.iter().map(|&s| c(s)).collect(); + let index: Vec = index.iter().map(|&i| c(i)).collect(); + unsafe { + highs_call!(Highs_passHessian( + self.highs.mut_ptr(), + c(dim), + c(value.len()), + format.as_raw(), + start.as_ptr(), + index.as_ptr(), + value.as_ptr() + )) + } + .map(|_| ()) + } } impl From for Model { From 5828cf577ee549e84cd1647ed0c3c6121b968661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Heim?= Date: Sat, 30 May 2026 09:26:25 -0300 Subject: [PATCH 2/8] tests: Add tests for try_pass_hessian and QP --- src/lib.rs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 1d1d807..35cc908 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1202,4 +1202,33 @@ mod test { assert_eq!(status, highs_sys::STATUS_OK); assert_eq!(value, 2); } + + #[test] + fn test_pass_hessian_convex_qp() { + use crate::status::HighsModelStatus::Optimal; + // min x^2 + y^2 s.t. x + y = 1, x, y in [-10, 10]. Optimum at (0.5, 0.5). + let mut pb = RowProblem::new(); + let x = pb.add_column(0., -10.0..=10.0); + let y = pb.add_column(0., -10.0..=10.0); + pb.add_row(1.0..=1.0, [(x, 1.), (y, 1.)]); + let mut model = pb.optimise(Sense::Minimise); + model.make_quiet(); + // Q = diag(2, 2) for the 0.5 x'Qx convention, lower-triangular CSC. + model + .try_pass_hessian(2, HessianFormat::Triangular, &[0, 1], &[0, 1], &[2.0, 2.0]) + .unwrap(); + let solved = model.solve(); + assert_eq!(solved.status(), Optimal); + let cols = solved.get_solution().columns().to_vec(); + assert!((cols[0] - 0.5).abs() < 1e-6, "x = {}", cols[0]); + assert!((cols[1] - 0.5).abs() < 1e-6, "y = {}", cols[1]); + assert!((solved.objective_value() - 0.5).abs() < 1e-6); + } + + #[test] + fn test_pass_hessian_length_mismatch() { + let mut model = RowProblem::default().optimise(Sense::Minimise); + let err = model.try_pass_hessian(2, HessianFormat::Triangular, &[0], &[], &[]); + assert!(err.is_err()); + } } From 6612b742cb8def7e74ba4f892d28b3433717c316 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Heim?= Date: Sun, 31 May 2026 15:18:02 -0300 Subject: [PATCH 3/8] Refactor Hessian API --- src/lib.rs | 55 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 35cc908..c3b4cbe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -705,9 +705,9 @@ impl Model { /// /// `Q` is given in compressed sparse column form. For /// [`HessianFormat::Triangular`] only the lower triangle is stored: - /// `start` has one entry per column (`start.len() == dim`), and `index` / - /// `value` list the row index and coefficient of each stored entry, - /// column by column. + /// `start` has one entry per column, so its length is the dimension of + /// `Q`, `index` / `value` list the row index and coefficient of each + /// stored entry, column by column. /// /// HiGHS solves **convex** QPs only: `Q` must be positive semidefinite. /// HiGHS does not verify this and behaviour on an indefinite `Q` is @@ -716,24 +716,31 @@ impl Model { /// /// # Panics /// - /// If HiGHS returns an error status value, or the slice lengths are - /// inconsistent (`start.len() != dim` or `index.len() != value.len()`). + /// If HiGHS returns an error status value, the slice lengths are + /// inconsistent (`index.len() != value.len()`), or an index does not fit + /// in HiGHS' integer type. pub fn pass_hessian( &mut self, - dim: usize, format: HessianFormat, start: &[usize], index: &[usize], value: &[f64], ) { - self.try_pass_hessian(dim, format, start, index, value) - .unwrap_or_else(|e| panic!("HiGHS error: {e:?}")) + self.try_pass_hessian(format, start, index, value) + .unwrap_or_else(|e| { + panic!( + "pass_hessian failed (format={format:?}, dim={}, nnz={}): {e:?}", + start.len(), + value.len(), + ) + }) } /// Same as [`Model::pass_hessian`], but returns the error status value /// instead of panicking. Returns [`HighsStatus::Error`] when the slice - /// lengths are inconsistent. An empty Hessian (`value.is_empty()`) is a - /// no-op and leaves the model linear. + /// lengths are inconsistent or an index does not fit in HiGHS' integer + /// type. An empty Hessian (`value.is_empty()`) is a no-op and leaves the + /// model linear. /// /// ``` /// use highs::{RowProblem, Sense, HessianFormat, HighsModelStatus}; @@ -745,7 +752,7 @@ impl Model { /// let mut model = pb.optimise(Sense::Minimise); /// // Hessian of 0.5 x'Qx with Q = diag(2, 2), stored lower-triangular CSC. /// model - /// .try_pass_hessian(2, HessianFormat::Triangular, &[0, 1], &[0, 1], &[2.0, 2.0]) + /// .try_pass_hessian(HessianFormat::Triangular, &[0, 1], &[0, 1], &[2.0, 2.0]) /// .unwrap(); /// let solved = model.solve(); /// assert_eq!(solved.status(), HighsModelStatus::Optimal); @@ -755,25 +762,32 @@ impl Model { /// ``` pub fn try_pass_hessian( &mut self, - dim: usize, format: HessianFormat, start: &[usize], index: &[usize], value: &[f64], ) -> Result<(), HighsStatus> { - if start.len() != dim || index.len() != value.len() { + if index.len() != value.len() { return Err(HighsStatus::Error); } if value.is_empty() { return Ok(()); } - let start: Vec = start.iter().map(|&s| c(s)).collect(); - let index: Vec = index.iter().map(|&i| c(i)).collect(); + let dim: HighsInt = start.len().try_into().map_err(|_| HighsStatus::Error)?; + let nnz: HighsInt = value.len().try_into().map_err(|_| HighsStatus::Error)?; + let start: Vec = start + .iter() + .map(|&s| s.try_into().map_err(|_| HighsStatus::Error)) + .collect::>()?; + let index: Vec = index + .iter() + .map(|&i| i.try_into().map_err(|_| HighsStatus::Error)) + .collect::>()?; unsafe { highs_call!(Highs_passHessian( self.highs.mut_ptr(), - c(dim), - c(value.len()), + dim, + nnz, format.as_raw(), start.as_ptr(), index.as_ptr(), @@ -1215,7 +1229,7 @@ mod test { model.make_quiet(); // Q = diag(2, 2) for the 0.5 x'Qx convention, lower-triangular CSC. model - .try_pass_hessian(2, HessianFormat::Triangular, &[0, 1], &[0, 1], &[2.0, 2.0]) + .try_pass_hessian(HessianFormat::Triangular, &[0, 1], &[0, 1], &[2.0, 2.0]) .unwrap(); let solved = model.solve(); assert_eq!(solved.status(), Optimal); @@ -1228,7 +1242,10 @@ mod test { #[test] fn test_pass_hessian_length_mismatch() { let mut model = RowProblem::default().optimise(Sense::Minimise); - let err = model.try_pass_hessian(2, HessianFormat::Triangular, &[0], &[], &[]); + // `index` and `value` have different lengths. + let err = model.try_pass_hessian(HessianFormat::Triangular, &[0, 1], &[0], &[2.0, 2.0]); + assert!(err.is_err()); + } assert!(err.is_err()); } } From a05c81466badedf86b9a68b5be2333877364a0ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Heim?= Date: Sun, 31 May 2026 15:25:40 -0300 Subject: [PATCH 4/8] tests: Add test for index overflow --- src/lib.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index c3b4cbe..68899ec 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1246,6 +1246,16 @@ mod test { let err = model.try_pass_hessian(HessianFormat::Triangular, &[0, 1], &[0], &[2.0, 2.0]); assert!(err.is_err()); } + + #[test] + fn test_pass_hessian_index_overflow_is_error() { + let mut model = RowProblem::default().optimise(Sense::Minimise); + let err = model.try_pass_hessian( + HessianFormat::Triangular, + &[0, 1], + &[0, usize::MAX], + &[2.0, 2.0], + ); assert!(err.is_err()); } } From 14a8d267b23b027df19f5f1b4a1d3489b077ba62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Heim?= Date: Mon, 1 Jun 2026 16:48:12 -0300 Subject: [PATCH 5/8] feat: Accept generic iterables in `pass_hessian` --- src/lib.rs | 82 +++++++++++++++++++++++++++--------------------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 68899ec..605911c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -705,9 +705,11 @@ impl Model { /// /// `Q` is given in compressed sparse column form. For /// [`HessianFormat::Triangular`] only the lower triangle is stored: - /// `start` has one entry per column, so its length is the dimension of - /// `Q`, `index` / `value` list the row index and coefficient of each - /// stored entry, column by column. + /// `start` yields the offset into `entries` at which each column begins, + /// so its length is the dimension of `Q`. `entries` yields one + /// `(row index, coefficient)` pair per stored value, column by column. + /// Both can be anything iterable and the indices may be any integer type + /// that converts to HiGHS' integer type. /// /// HiGHS solves **convex** QPs only: `Q` must be positive semidefinite. /// HiGHS does not verify this and behaviour on an indefinite `Q` is @@ -716,31 +718,23 @@ impl Model { /// /// # Panics /// - /// If HiGHS returns an error status value, the slice lengths are - /// inconsistent (`index.len() != value.len()`), or an index does not fit - /// in HiGHS' integer type. - pub fn pass_hessian( - &mut self, - format: HessianFormat, - start: &[usize], - index: &[usize], - value: &[f64], - ) { - self.try_pass_hessian(format, start, index, value) - .unwrap_or_else(|e| { - panic!( - "pass_hessian failed (format={format:?}, dim={}, nnz={}): {e:?}", - start.len(), - value.len(), - ) - }) + /// If HiGHS returns an error status value, or an index does not fit in + /// HiGHS' integer type. + pub fn pass_hessian(&mut self, format: HessianFormat, start: S, entries: E) + where + S: IntoIterator, + S::Item: TryInto, + E: IntoIterator, + I: TryInto, + { + self.try_pass_hessian(format, start, entries) + .unwrap_or_else(|e| panic!("pass_hessian failed (format={format:?}): {e:?}")) } /// Same as [`Model::pass_hessian`], but returns the error status value - /// instead of panicking. Returns [`HighsStatus::Error`] when the slice - /// lengths are inconsistent or an index does not fit in HiGHS' integer - /// type. An empty Hessian (`value.is_empty()`) is a no-op and leaves the - /// model linear. + /// instead of panicking. Returns [`HighsStatus::Error`] when an index does + /// not fit in HiGHS' integer type. An empty Hessian (no `entries`) is a + /// no-op and leaves the model linear. /// /// ``` /// use highs::{RowProblem, Sense, HessianFormat, HighsModelStatus}; @@ -752,7 +746,7 @@ impl Model { /// let mut model = pb.optimise(Sense::Minimise); /// // Hessian of 0.5 x'Qx with Q = diag(2, 2), stored lower-triangular CSC. /// model - /// .try_pass_hessian(HessianFormat::Triangular, &[0, 1], &[0, 1], &[2.0, 2.0]) + /// .try_pass_hessian(HessianFormat::Triangular, [0, 1], [(0, 2.0), (1, 2.0)]) /// .unwrap(); /// let solved = model.solve(); /// assert_eq!(solved.status(), HighsModelStatus::Optimal); @@ -760,29 +754,35 @@ impl Model { /// assert!((cols[0] - 0.5).abs() < 1e-6); /// assert!((cols[1] - 0.5).abs() < 1e-6); /// ``` - pub fn try_pass_hessian( + pub fn try_pass_hessian( &mut self, format: HessianFormat, - start: &[usize], - index: &[usize], - value: &[f64], - ) -> Result<(), HighsStatus> { - if index.len() != value.len() { - return Err(HighsStatus::Error); + start: S, + entries: E, + ) -> Result<(), HighsStatus> + where + S: IntoIterator, + S::Item: TryInto, + E: IntoIterator, + I: TryInto, + { + let start: Vec = start + .into_iter() + .map(|s| s.try_into().map_err(|_| HighsStatus::Error)) + .collect::>()?; + let entries = entries.into_iter(); + let (lower, _) = entries.size_hint(); + let mut index: Vec = Vec::with_capacity(lower); + let mut value: Vec = Vec::with_capacity(lower); + for (i, v) in entries { + index.push(i.try_into().map_err(|_| HighsStatus::Error)?); + value.push(v); } if value.is_empty() { return Ok(()); } let dim: HighsInt = start.len().try_into().map_err(|_| HighsStatus::Error)?; let nnz: HighsInt = value.len().try_into().map_err(|_| HighsStatus::Error)?; - let start: Vec = start - .iter() - .map(|&s| s.try_into().map_err(|_| HighsStatus::Error)) - .collect::>()?; - let index: Vec = index - .iter() - .map(|&i| i.try_into().map_err(|_| HighsStatus::Error)) - .collect::>()?; unsafe { highs_call!(Highs_passHessian( self.highs.mut_ptr(), From 97c069471cbdbbd4040cd4cc73e217fd99c29b4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Heim?= Date: Mon, 1 Jun 2026 16:48:47 -0300 Subject: [PATCH 6/8] tests: Update `pass_hessian` tests --- src/lib.rs | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 605911c..0514343 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1229,7 +1229,7 @@ mod test { model.make_quiet(); // Q = diag(2, 2) for the 0.5 x'Qx convention, lower-triangular CSC. model - .try_pass_hessian(HessianFormat::Triangular, &[0, 1], &[0, 1], &[2.0, 2.0]) + .try_pass_hessian(HessianFormat::Triangular, [0, 1], [(0, 2.0), (1, 2.0)]) .unwrap(); let solved = model.solve(); assert_eq!(solved.status(), Optimal); @@ -1239,22 +1239,13 @@ mod test { assert!((solved.objective_value() - 0.5).abs() < 1e-6); } - #[test] - fn test_pass_hessian_length_mismatch() { - let mut model = RowProblem::default().optimise(Sense::Minimise); - // `index` and `value` have different lengths. - let err = model.try_pass_hessian(HessianFormat::Triangular, &[0, 1], &[0], &[2.0, 2.0]); - assert!(err.is_err()); - } - #[test] fn test_pass_hessian_index_overflow_is_error() { let mut model = RowProblem::default().optimise(Sense::Minimise); let err = model.try_pass_hessian( HessianFormat::Triangular, - &[0, 1], - &[0, usize::MAX], - &[2.0, 2.0], + [0usize, 1], + [(0usize, 2.0), (usize::MAX, 2.0)], ); assert!(err.is_err()); } From 35daa0dabc6afb3320976cb1f4bbd622bec46b42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Heim?= Date: Mon, 1 Jun 2026 16:49:24 -0300 Subject: [PATCH 7/8] tests: Add lazy iterators tests for `pass_hessian` --- src/lib.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 0514343..89afad8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1249,4 +1249,23 @@ mod test { ); assert!(err.is_err()); } + + #[test] + fn test_pass_hessian_accepts_lazy_iterators() { + use crate::status::HighsModelStatus::Optimal; + let mut pb = RowProblem::new(); + let x = pb.add_column(0., -10.0..=10.0); + let y = pb.add_column(0., -10.0..=10.0); + pb.add_row(1.0..=1.0, [(x, 1.), (y, 1.)]); + let mut model = pb.optimise(Sense::Minimise); + model.make_quiet(); + model + .try_pass_hessian(HessianFormat::Triangular, 0..2, (0..2).map(|i| (i, 2.0))) + .unwrap(); + let solved = model.solve(); + assert_eq!(solved.status(), Optimal); + let cols = solved.get_solution().columns().to_vec(); + assert!((cols[0] - 0.5).abs() < 1e-6, "x = {}", cols[0]); + assert!((cols[1] - 0.5).abs() < 1e-6, "y = {}", cols[1]); + } } From 2b0c51cd666fd3bb993c93cf861859e48fb3ec38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Heim?= Date: Wed, 3 Jun 2026 08:56:20 -0300 Subject: [PATCH 8/8] feat: Add `HessianError` and refactor `pass_hessian` API --- src/lib.rs | 167 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 119 insertions(+), 48 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 89afad8..3c9eea6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -296,6 +296,62 @@ impl HessianFormat { } } +/// Reason a Hessian could not be uploaded by [`Model::try_pass_hessian`]. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum HessianError { + /// The dimension of `Q` (its number of columns) does not fit in HiGHS' + /// integer type. + DimensionTooLarge { + /// The dimension that was requested. + dim: usize, + }, + /// The number of stored nonzero coefficients does not fit in HiGHS' + /// integer type. + TooManyNonZeros { + /// The number of nonzeros that was requested. + nnz: usize, + }, + /// A row index does not fit in HiGHS' integer type. + IndexTooLarge { + /// Position, among the stored coefficients, of the offending entry. + entry: usize, + }, + /// HiGHS rejected the Hessian and returned this status. + Highs(HighsStatus), +} + +impl std::fmt::Display for HessianError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let max = HighsInt::MAX; + match *self { + HessianError::DimensionTooLarge { dim } => write!( + f, + "the dimension of the quadratic objective matrix (Hessian Q) is too large: \ + got {dim} but HiGHS supports at most {max}" + ), + HessianError::TooManyNonZeros { nnz } => write!( + f, + "the Hessian Q has too many nonzero coefficients: \ + got {nnz} but HiGHS supports at most {max}" + ), + HessianError::IndexTooLarge { entry } => write!( + f, + "the row index of Hessian coefficient {entry} is too large \ + for HiGHS' integer type (at most {max})" + ), + HessianError::Highs(status) => write!(f, "HiGHS rejected the Hessian: {status:?}"), + } + } +} + +impl std::error::Error for HessianError {} + +impl From for HessianError { + fn from(status: HighsStatus) -> Self { + HessianError::Highs(status) + } +} + impl Model { /// Return pointer to underlying HiGHS model pub fn as_ptr(&self) -> *const c_void { @@ -703,38 +759,38 @@ impl Model { /// with objective `c'x + 0.5 x' Q x` (where `c` is the linear objective /// already set on the columns). /// - /// `Q` is given in compressed sparse column form. For - /// [`HessianFormat::Triangular`] only the lower triangle is stored: - /// `start` yields the offset into `entries` at which each column begins, - /// so its length is the dimension of `Q`. `entries` yields one - /// `(row index, coefficient)` pair per stored value, column by column. - /// Both can be anything iterable and the indices may be any integer type + /// `Q` is provided column by column: `columns` yields one item per column + /// of `Q` and each column yields its stored `(row index, coefficient)` pairs. + /// For [`HessianFormat::Triangular`] store only the lower triangle. + /// For [`HessianFormat::Square`] store the full matrix. + /// Both levels can be anything iterable and the indices may be any integer type /// that converts to HiGHS' integer type. /// - /// HiGHS solves **convex** QPs only: `Q` must be positive semidefinite. - /// HiGHS does not verify this and behaviour on an indefinite `Q` is - /// undefined, so check convexity before calling if the matrix is not PSD - /// by construction. + /// HiGHS solves **convex** QPs only: `Q` should be positive semidefinite. + /// HiGHS does not check this: + /// on an indefinite `Q` it may return a wrong or + /// non-optimal solution. + /// HiGHS, however, does test for negative diagonal values. + /// Verify convexity yourself if `Q` is not PSD by construction. /// /// # Panics /// - /// If HiGHS returns an error status value, or an index does not fit in - /// HiGHS' integer type. - pub fn pass_hessian(&mut self, format: HessianFormat, start: S, entries: E) + /// If HiGHS returns an error status value, or an index/size does not fit in + /// HiGHS' integer type. Use [`Model::try_pass_hessian`] to handle these as + /// a [`HessianError`] instead. + pub fn pass_hessian(&mut self, format: HessianFormat, columns: C) where - S: IntoIterator, - S::Item: TryInto, + C: IntoIterator, E: IntoIterator, I: TryInto, { - self.try_pass_hessian(format, start, entries) - .unwrap_or_else(|e| panic!("pass_hessian failed (format={format:?}): {e:?}")) + self.try_pass_hessian(format, columns) + .unwrap_or_else(|e| panic!("pass_hessian failed: {e}")) } - /// Same as [`Model::pass_hessian`], but returns the error status value - /// instead of panicking. Returns [`HighsStatus::Error`] when an index does - /// not fit in HiGHS' integer type. An empty Hessian (no `entries`) is a - /// no-op and leaves the model linear. + /// Same as [`Model::pass_hessian`], but returns a [`HessianError`] instead + /// of panicking. An empty Hessian (no coefficients) is a no-op and leaves + /// the model linear. /// /// ``` /// use highs::{RowProblem, Sense, HessianFormat, HighsModelStatus}; @@ -744,9 +800,9 @@ impl Model { /// let y = pb.add_column(0.0, -10.0..=10.0); /// pb.add_row(1.0..=1.0, [(x, 1.0), (y, 1.0)]); /// let mut model = pb.optimise(Sense::Minimise); - /// // Hessian of 0.5 x'Qx with Q = diag(2, 2), stored lower-triangular CSC. + /// // Q = diag(2, 2): one column per variable, each with its diagonal entry. /// model - /// .try_pass_hessian(HessianFormat::Triangular, [0, 1], [(0, 2.0), (1, 2.0)]) + /// .try_pass_hessian(HessianFormat::Triangular, [[(0, 2.0)], [(1, 2.0)]]) /// .unwrap(); /// let solved = model.solve(); /// assert_eq!(solved.status(), HighsModelStatus::Optimal); @@ -754,35 +810,47 @@ impl Model { /// assert!((cols[0] - 0.5).abs() < 1e-6); /// assert!((cols[1] - 0.5).abs() < 1e-6); /// ``` - pub fn try_pass_hessian( + pub fn try_pass_hessian( &mut self, format: HessianFormat, - start: S, - entries: E, - ) -> Result<(), HighsStatus> + columns: C, + ) -> Result<(), HessianError> where - S: IntoIterator, - S::Item: TryInto, + C: IntoIterator, E: IntoIterator, I: TryInto, { - let start: Vec = start - .into_iter() - .map(|s| s.try_into().map_err(|_| HighsStatus::Error)) - .collect::>()?; - let entries = entries.into_iter(); - let (lower, _) = entries.size_hint(); - let mut index: Vec = Vec::with_capacity(lower); - let mut value: Vec = Vec::with_capacity(lower); - for (i, v) in entries { - index.push(i.try_into().map_err(|_| HighsStatus::Error)?); - value.push(v); + // Build the compressed-sparse-column arrays from the per-column + // iterators. + let mut start: Vec = Vec::new(); + let mut index: Vec = Vec::new(); + let mut value: Vec = Vec::new(); + for column in columns { + let offset = index.len(); + start.push( + offset + .try_into() + .map_err(|_| HessianError::TooManyNonZeros { nnz: offset })?, + ); + for (i, v) in column { + let i: HighsInt = i + .try_into() + .map_err(|_| HessianError::IndexTooLarge { entry: index.len() })?; + index.push(i); + value.push(v); + } } if value.is_empty() { return Ok(()); } - let dim: HighsInt = start.len().try_into().map_err(|_| HighsStatus::Error)?; - let nnz: HighsInt = value.len().try_into().map_err(|_| HighsStatus::Error)?; + let dim: HighsInt = start + .len() + .try_into() + .map_err(|_| HessianError::DimensionTooLarge { dim: start.len() })?; + let nnz: HighsInt = value + .len() + .try_into() + .map_err(|_| HessianError::TooManyNonZeros { nnz: value.len() })?; unsafe { highs_call!(Highs_passHessian( self.highs.mut_ptr(), @@ -795,6 +863,7 @@ impl Model { )) } .map(|_| ()) + .map_err(HessianError::from) } } @@ -1227,9 +1296,9 @@ mod test { pb.add_row(1.0..=1.0, [(x, 1.), (y, 1.)]); let mut model = pb.optimise(Sense::Minimise); model.make_quiet(); - // Q = diag(2, 2) for the 0.5 x'Qx convention, lower-triangular CSC. + // Q = diag(2, 2) for the 0.5 x'Qx convention: one column per variable. model - .try_pass_hessian(HessianFormat::Triangular, [0, 1], [(0, 2.0), (1, 2.0)]) + .try_pass_hessian(HessianFormat::Triangular, [[(0, 2.0)], [(1, 2.0)]]) .unwrap(); let solved = model.solve(); assert_eq!(solved.status(), Optimal); @@ -1244,10 +1313,9 @@ mod test { let mut model = RowProblem::default().optimise(Sense::Minimise); let err = model.try_pass_hessian( HessianFormat::Triangular, - [0usize, 1], - [(0usize, 2.0), (usize::MAX, 2.0)], + [vec![(0usize, 2.0)], vec![(usize::MAX, 2.0)]], ); - assert!(err.is_err()); + assert!(matches!(err, Err(HessianError::IndexTooLarge { entry: 1 }))); } #[test] @@ -1260,7 +1328,10 @@ mod test { let mut model = pb.optimise(Sense::Minimise); model.make_quiet(); model - .try_pass_hessian(HessianFormat::Triangular, 0..2, (0..2).map(|i| (i, 2.0))) + .try_pass_hessian( + HessianFormat::Triangular, + (0..2).map(|j| std::iter::once((j, 2.0))), + ) .unwrap(); let solved = model.solve(); assert_eq!(solved.status(), Optimal);