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

cvxpy support hooks for #75 #157

Merged
merged 2 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
43 changes: 42 additions & 1 deletion python/clarabel/tests/test_data_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ def updating_test_data():

cones = [clarabel.NonnegativeConeT(2), clarabel.NonnegativeConeT(2)]
settings = clarabel.DefaultSettings()
settings.presolve_enable = False
return P, q, A, b, cones, settings


Expand Down Expand Up @@ -243,3 +242,45 @@ def test_update_b_tuple(updating_test_data):
solution2 = solver2.solve()

assert np.allclose(solution1.x, solution2.x)


def test_settings(updating_test_data):

P, q, A, b, cones, settings = updating_test_data

solver = clarabel.DefaultSolver(P, q, A, b, cones, settings)
solution = solver.solve()

assert solution.status == clarabel.SolverStatus.Solved

# extract settings, modify and reassign
settings = solver.get_settings()
settings.max_iter = 1
solver.update(settings=settings)
solution = solver.solve()

assert solution.status == clarabel.SolverStatus.MaxIterations


def test_presolved_update(updating_test_data):

# check that updates are rejected properly after presolving

P, q, A, b, cones, settings = updating_test_data

solver = clarabel.DefaultSolver(P, q, A, b, cones, settings)

# presolve enabled but nothing eliminated
assert solver.is_data_update_allowed()

# presolved disabled in settings
b[0] = 1e30
settings.presolve_enable = False
solver = clarabel.DefaultSolver(P, q, A, b, cones, settings)
assert solver.is_data_update_allowed()

# should be eliminated
b[0] = 1e30
settings.presolve_enable = True
solver = clarabel.DefaultSolver(P, q, A, b, cones, settings)
assert not solver.is_data_update_allowed()
36 changes: 25 additions & 11 deletions src/python/impl_default_py.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,10 +338,10 @@ impl PyDefaultSettings {
}
}

pub(crate) fn to_internal(&self) -> DefaultSettings<f64> {
pub(crate) fn to_internal(&self) -> Result<DefaultSettings<f64>, PyErr> {
// convert python settings -> Rust

DefaultSettings::<f64> {
let settings = DefaultSettings::<f64> {
max_iter: self.max_iter,
time_limit: self.time_limit,
verbose: self.verbose,
Expand Down Expand Up @@ -384,6 +384,12 @@ impl PyDefaultSettings {
chordal_decomposition_merge_method: self.chordal_decomposition_merge_method.clone(),
chordal_decomposition_compact: self.chordal_decomposition_compact,
chordal_decomposition_complete_dual: self.chordal_decomposition_complete_dual,
};

//manually validate settings from Python side
match settings.validate() {
Ok(_) => Ok(settings),
Err(e) => Err(PyException::new_err(format!("Invalid settings: {}", e))),
}
}
}
Expand Down Expand Up @@ -414,15 +420,7 @@ impl PyDefaultSolver {
settings: PyDefaultSettings,
) -> PyResult<Self> {
let cones = _py_to_native_cones(cones);
let settings = settings.to_internal();

//manually validate settings from Python side
match settings.validate() {
Ok(_) => (),
Err(e) => {
return Err(PyException::new_err(format!("Invalid settings: {}", e)));
}
}
let settings = settings.to_internal()?;

let solver = DefaultSolver::new(&P, &q, &A, &b, &cones, settings);
Ok(Self { inner: solver })
Expand Down Expand Up @@ -521,13 +519,29 @@ impl PyDefaultSolver {
return Err(PyException::new_err("Invalid b update data"));
}
},
"settings" => {
let settings: PyDefaultSettings = value.extract()?;
let settings = settings.to_internal()?;
self.inner.settings = settings;
}
_ => {
println!("unrecognized key: {}", key);
}
}
}
Ok(())
}

// return the currently configured settings of a solver. If settings
// are to be overridden, modify this object then pass back using kwargs
// update(settings=settings)
fn get_settings(&self) -> PyDefaultSettings {
PyDefaultSettings::new_from_internal(&self.inner.settings)
}

fn is_data_update_allowed(&self) -> bool {
self.inner.is_data_update_allowed()
}
}

enum PyMatrixUpdateData {
Expand Down
64 changes: 43 additions & 21 deletions src/solver/implementations/default/data_updating.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
#![allow(non_snake_case)]
use super::DefaultSolver;
use crate::algebra::*;
use crate::solver::DefaultSolver;
use core::iter::{zip, Zip};
use core::slice::Iter;
use thiserror::Error;

/// Error type returned by user data update utilities, e.g. [`check_format`](crate::algebra::CscMatrix::check_format) utility.
#[derive(Error, Debug)]
pub enum DataUpdateError {
#[error("Data updates are not allowed when presolve is enabled")]
PresolveEnabled,
#[error("Data updates are not allowed when presolver is active")]
PresolveIsActive,
#[cfg(feature = "sdp")]
#[error("Data updates are not allowed when chordal decomposition is active")]
ChordalDecompositionIsActive,
#[error("Data formatting error")]
BadFormat(#[from] SparseFormatError),
}

// Trait for updating P and A matrices from various data types
/// Trait for updating problem data matrices (`P` and `A`) from various data types
pub trait MatrixProblemDataUpdate<T: FloatT> {
fn update_matrix(
&self,
Expand All @@ -25,7 +28,7 @@ pub trait MatrixProblemDataUpdate<T: FloatT> {
) -> Result<(), SparseFormatError>;
}

// Trait for updating q and b vectors from various data types
/// Trait for updating problem data vectors (`q`` and `b`) from various data types
pub trait VectorProblemDataUpdate<T: FloatT> {
fn update_vector(
&self,
Expand All @@ -40,7 +43,17 @@ where
T: FloatT,
{
/// Overwrites internal problem data structures in a solver object with new data, avoiding new memory allocations.
/// See `update_P``, `update_q`, `update_A`, `update_b` for allowable inputs.
/// See `update_P`, `update_q`, `update_A`, `update_b` for allowable inputs.
///
/// <div class="warning">
///
/// data updating functions will return an error when either presolving or chordal
/// decomposition have modfied the original problem structure. In order to guarantee
/// that data updates will be accepted regardless of the original problem data, set
/// `presolve_enable = false` and `chordal_decomposition_enable = false` in the solver settings.
/// See also `is_data_update_allowed()`.
///
/// </div>
pub fn update_data<
DataP: MatrixProblemDataUpdate<T>,
Dataq: VectorProblemDataUpdate<T>,
Expand All @@ -63,19 +76,19 @@ where

/// Overwrites the `P` matrix data in an existing solver object. The input `P` can be
///
/// - a nonempty Vector, in which case the nonzero values of the original `P` are overwritten, preserving the sparsity pattern, or
/// - a nonempty `Vec`, in which case the nonzero values of the original `P` are overwritten, preserving the sparsity pattern, or
///
/// - a SparseMatrixCSC, in which case the input must match the sparsity pattern of the upper triangular part of the original `P`.
/// - a `CscMatrix`, in which case the input must match the sparsity pattern of the upper triangular part of the original `P`.
///
/// - an iterator zip(&index,&values), specifying a selective update of values.
/// - an iterator `zip(&index,&values)`, specifying a selective update of values.
///
/// - an empty vector, in which case no action is taken.
///
pub fn update_P<Data: MatrixProblemDataUpdate<T>>(
&mut self,
data: &Data,
) -> Result<(), DataUpdateError> {
self.check_presolve_disabled()?;
self.check_data_update_allowed()?;
let d = &self.data.equilibration.d;
let c = self.data.equilibration.c;
data.update_matrix(&mut self.data.P, d, d, Some(c))?;
Expand All @@ -86,19 +99,19 @@ where

/// Overwrites the `A` matrix data in an existing solver object. The input `A` can be
///
/// - a nonempty Vector, in which case the nonzero values of the original `A` are overwritten, preserving the sparsity pattern, or
/// - a nonempty `Vec`, in which case the nonzero values of the original `A` are overwritten, preserving the sparsity pattern, or
///
/// - a SparseMatrixCSC, in which case the input must match the sparsity pattern of the original `A`.
/// - a `CscMatrix`, in which case the input must match the sparsity pattern of the original `A`.
///
/// - an iterator zip(&index,&values), specifying a selective update of values.
/// - an iterator `zip(&index,&values)`, specifying a selective update of values.
///
/// - an empty vector, in which case no action is taken.
///
pub fn update_A<Data: MatrixProblemDataUpdate<T>>(
&mut self,
data: &Data,
) -> Result<(), DataUpdateError> {
self.check_presolve_disabled()?;
self.check_data_update_allowed()?;
let d = &self.data.equilibration.d;
let e = &self.data.equilibration.e;
data.update_matrix(&mut self.data.A, e, d, None)?;
Expand All @@ -112,7 +125,7 @@ where
&mut self,
data: &Data,
) -> Result<(), DataUpdateError> {
self.check_presolve_disabled()?;
self.check_data_update_allowed()?;
let d = &self.data.equilibration.d;
let c = self.data.equilibration.c;
data.update_vector(&mut self.data.q, d, Some(c))?;
Expand All @@ -128,7 +141,7 @@ where
&mut self,
data: &Data,
) -> Result<(), DataUpdateError> {
self.check_presolve_disabled()?;
self.check_data_update_allowed()?;
let e = &self.data.equilibration.e;
data.update_vector(&mut self.data.b, e, None)?;

Expand All @@ -138,12 +151,21 @@ where
Ok(())
}

fn check_presolve_disabled(&self) -> Result<(), DataUpdateError> {
if self.settings.presolve_enable {
Err(DataUpdateError::PresolveEnabled)
} else {
Ok(())
fn check_data_update_allowed(&self) -> Result<(), DataUpdateError> {
if self.data.is_presolved() {
return Err(DataUpdateError::PresolveIsActive);
}
#[cfg(feature = "sdp")]
if self.data.is_chordal_decomposed() {
return Err(DataUpdateError::ChordalDecompositionIsActive);
}
Ok(())
}

/// Returns `true` if problem structure has been modified by
/// presolving or chordal decomposition
pub fn is_data_update_allowed(&self) -> bool {
self.check_data_update_allowed().is_ok()
}
}

Expand Down
15 changes: 15 additions & 0 deletions src/solver/implementations/default/problemdata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,21 @@ where
pub(crate) fn clear_normb(&mut self) {
self.normb = None;
}

// data updating not supported following presolve
//reduction or chordal decomposition
pub(crate) fn is_presolved(&self) -> bool {
self.presolver.is_some()
}

#[allow(dead_code)]
pub(crate) fn is_chordal_decomposed(&self) -> bool {
#[cfg(feature = "sdp")]
if self.chordal_info.is_some() {
return true;
}
false
}
}

impl<T> ProblemData<T> for DefaultProblemData<T>
Expand Down
29 changes: 22 additions & 7 deletions tests/data_updating.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,26 +309,41 @@ fn test_update_noops() {
#[test]
fn test_fail_on_presolve_enable() {
// original problem
let (P, q, A, b, cones, mut settings) = updating_test_data();
let (P, q, A, mut b, cones, mut settings) = updating_test_data();
settings.presolve_enable = true;
let mut solver = DefaultSolver::new(&P, &q, &A, &b, &cones, settings);
solver.solve();
let solver = DefaultSolver::new(&P, &q, &A, &b, &cones, settings.clone());

// presolve enabled but nothing eliminated
assert!(solver.is_data_update_allowed());

// presolved disabled in settings
b[0] = 1e40;
settings.presolve_enable = false;
let solver = DefaultSolver::new(&P, &q, &A, &b, &cones, settings.clone());
assert!(solver.is_data_update_allowed());

// should be eliminated
b[0] = 1e40;
settings.presolve_enable = true;
let mut solver = DefaultSolver::new(&P, &q, &A, &b, &cones, settings.clone());
assert!(!solver.is_data_update_allowed());

// apply no-op updates to check that updates are rejected
// when presolve is active
assert!(matches!(
solver.update_P(&[]).err(),
Some(DataUpdateError::PresolveEnabled)
Some(DataUpdateError::PresolveIsActive)
));
assert!(matches!(
solver.update_A(&[]).err(),
Some(DataUpdateError::PresolveEnabled)
Some(DataUpdateError::PresolveIsActive)
));
assert!(matches!(
solver.update_b(&[]).err(),
Some(DataUpdateError::PresolveEnabled)
Some(DataUpdateError::PresolveIsActive)
));
assert!(matches!(
solver.update_q(&[]).err(),
Some(DataUpdateError::PresolveEnabled)
Some(DataUpdateError::PresolveIsActive)
));
}