Quadratic programming solvers in Python with a unified API
—
Helper functions for matrix conversions, problem transformations, error handling, debugging support, and sample problem generation. These utilities support advanced QP solving workflows and integration with different solver backends.
Functions for converting between different matrix formats and problem formulations to ensure compatibility with various solvers.
def combine_linear_box_inequalities(
G: Optional[Union[np.ndarray, spa.csc_matrix]],
h: Optional[np.ndarray],
lb: Optional[np.ndarray],
ub: Optional[np.ndarray],
n: int
) -> Tuple[Union[np.ndarray, spa.csc_matrix], np.ndarray]:
"""
Combine linear inequality and box constraints into a single system.
Converts box constraints lb <= x <= ub into linear inequalities and
combines them with existing G x <= h constraints.
Parameters:
- G: Linear inequality constraint matrix
- h: Linear inequality constraint vector
- lb: Lower bound constraints (can contain -np.inf)
- ub: Upper bound constraints (can contain +np.inf)
- n: Problem dimension
Returns:
Tuple of (combined_G, combined_h) representing all constraints as G_new x <= h_new
"""
def linear_from_box_inequalities(
lb: Optional[np.ndarray],
ub: Optional[np.ndarray],
n: int
) -> Tuple[Union[np.ndarray, spa.csc_matrix], np.ndarray]:
"""
Convert box constraints to linear inequality format.
Transforms lb <= x <= ub into -I x <= -lb and I x <= ub form.
Parameters:
- lb: Lower bound constraints (can contain -np.inf)
- ub: Upper bound constraints (can contain +np.inf)
- n: Problem dimension
Returns:
Tuple of (G, h) representing box constraints as linear inequalities
"""
def ensure_sparse_matrices(
P: Union[np.ndarray, spa.csc_matrix],
q: np.ndarray,
G: Optional[Union[np.ndarray, spa.csc_matrix]] = None,
h: Optional[np.ndarray] = None,
A: Optional[Union[np.ndarray, spa.csc_matrix]] = None,
b: Optional[np.ndarray] = None,
lb: Optional[np.ndarray] = None,
ub: Optional[np.ndarray] = None,
) -> Tuple[
spa.csc_matrix,
np.ndarray,
Optional[spa.csc_matrix],
Optional[np.ndarray],
Optional[spa.csc_matrix],
Optional[np.ndarray],
Optional[np.ndarray],
Optional[np.ndarray],
]:
"""
Convert all matrices to sparse CSC format for sparse solvers.
Parameters:
- P, q, G, h, A, b, lb, ub: Problem matrices and vectors
Returns:
Tuple with all matrices converted to scipy.sparse.csc_matrix format
"""
def socp_from_qp(problem: Problem) -> Dict[str, Any]:
"""
Convert a quadratic program to second-order cone program format.
Transforms QP into SOCP formulation for solvers like ECOS.
Parameters:
- problem: Quadratic program to convert
Returns:
Dictionary containing SOCP formulation matrices
"""
def split_dual_linear_box(
z_combined: np.ndarray,
n_linear: int,
n_box_lower: int,
n_box_upper: int
) -> Tuple[Optional[np.ndarray], Optional[np.ndarray]]:
"""
Split combined dual multipliers back into linear and box constraint parts.
Reverses the combination performed by combine_linear_box_inequalities.
Parameters:
- z_combined: Combined dual multiplier vector
- n_linear: Number of original linear constraints
- n_box_lower: Number of lower bound constraints
- n_box_upper: Number of upper bound constraints
Returns:
Tuple of (z_linear, z_box) dual multipliers
"""Usage example:
import numpy as np
import scipy.sparse as spa
from qpsolvers.conversions import (
combine_linear_box_inequalities,
linear_from_box_inequalities,
ensure_sparse_matrices
)
# Convert box constraints to linear inequalities
n = 3
lb = np.array([0.0, -np.inf, 1.0])
ub = np.array([np.inf, 2.0, 3.0])
G_box, h_box = linear_from_box_inequalities(lb, ub, n)
print(f"Box constraints as linear inequalities:")
print(f"G_box shape: {G_box.shape}")
print(f"h_box: {h_box}")
# Combine with existing linear constraints
G_linear = np.array([[1.0, 1.0, 1.0]])
h_linear = np.array([5.0])
G_combined, h_combined = combine_linear_box_inequalities(
G_linear, h_linear, lb, ub, n
)
print(f"Combined constraints shape: {G_combined.shape}")
# Convert matrices to sparse format
P = np.eye(n)
q = np.ones(n)
P_sparse, q_out, G_sparse, h_out, A_out, b_out, lb_out, ub_out = ensure_sparse_matrices(
P, q, G_combined, h_combined
)
print(f"P matrix format: {type(P_sparse)}")
print(f"G matrix format: {type(G_sparse)}")Utilities for debugging QP problems and visualizing matrix structures.
def print_matrix_vector(
A: Union[np.ndarray, spa.csc_matrix],
A_label: str,
b: np.ndarray,
b_label: str,
column_width: int = 24,
) -> None:
"""
Print a matrix and vector side by side for debugging.
Useful for visualizing constraint matrices and vectors together.
Parameters:
- A: Matrix to print (converted to dense if sparse)
- A_label: Label for the matrix
- b: Vector to print
- b_label: Label for the vector
- column_width: Width of output columns in characters
"""Usage example:
import numpy as np
from qpsolvers import print_matrix_vector
# Create example constraint
G = np.array([
[1.0, 2.0, -1.0],
[0.0, 1.0, 2.0],
[-1.0, 0.0, 1.0]
])
h = np.array([3.0, 2.0, 1.0])
print("Constraint visualization:")
print_matrix_vector(G, "G", h, "h")
# Output will show G and h matrices side by side:
# G = h =
# [[ 1. 2. -1.] [[3.]
# [ 0. 1. 2.] [2.]
# [-1. 0. 1.]] [1.]]Functions for creating test problems and benchmarks.
def get_sparse_least_squares(n: int) -> Tuple[
spa.csc_matrix, # R matrix
np.ndarray, # s vector
spa.csc_matrix, # G matrix
np.ndarray, # h vector
spa.csc_matrix, # A matrix
np.ndarray, # b vector
]:
"""
Generate a sparse least squares test problem.
Creates a problem of the form:
minimize ||R x - s||^2
subject to G x <= h, A x = b
Parameters:
- n: Problem dimension
Returns:
Tuple of (R, s, G, h, A, b) defining the least squares problem
"""
def get_qpsut01() -> Tuple[Problem, Solution]:
"""
Get QPSUT01 problem and its solution.
Returns:
Tuple of (problem, solution) pair for testing
"""
def get_qpsut02() -> Tuple[Problem, Solution]:
"""
Get QPSUT02 problem and its solution.
Returns:
Tuple of (problem, solution) pair for testing
"""
def get_qpsut03() -> Tuple[Problem, Solution]:
"""
Get QPSUT03 problem and its solution.
This problem has partial box bounds with -infinity on some lower
bounds and +infinity on some upper bounds.
Returns:
Tuple of (problem, solution) pair for testing
"""
def get_qpsut04() -> Tuple[Problem, Solution]:
"""
Get QPSUT04 problem and its solution.
Returns:
Tuple of (problem, solution) pair for testing
"""
def get_qpsut05() -> Tuple[Problem, Solution]:
"""
Get QPSUT05 problem and its solution.
Returns:
Tuple of (problem, solution) pair for testing
"""
def get_qptest() -> Tuple[Problem, Solution]:
"""
Get QPTEST problem from the Maros-Meszaros test set.
Returns:
Tuple of (problem, solution) pair for testing
"""
def get_qpgurdu() -> Tuple[Problem, Solution]:
"""
Get sample random problem with linear inequality constraints.
Returns:
Tuple of (problem, solution) pair for testing
"""
def get_qpgureq() -> Tuple[Problem, Solution]:
"""
Get sample random problem with equality constraints.
Returns:
Tuple of (problem, solution) pair for testing
"""
def get_qpgurabs() -> Tuple[Problem, Solution]:
"""
Get sample random problem with box constraints.
Returns:
Tuple of (problem, solution) pair for testing
"""Usage example:
from qpsolvers.problems import get_sparse_least_squares, get_qpsut01
from qpsolvers import solve_ls, solve_problem
# Generate sparse least squares test problem
n = 50
R, s, G, h, A, b, lb, ub = get_sparse_least_squares(n)
print(f"Generated {n}-dimensional sparse least squares problem")
print(f"R matrix shape: {R.shape}, sparsity: {R.nnz / (R.shape[0] * R.shape[1]):.3f}")
print(f"G matrix shape: {G.shape}")
print(f"A matrix shape: {A.shape}")
# Solve the generated problem
x = solve_ls(R, s, G, h, A, b, lb=lb, ub=ub, solver="osqp")
if x is not None:
print(f"Solution found with residual: {np.linalg.norm(R @ x - s):.6f}")
# Use a predefined test problem
problem, expected_solution = get_qpsut01()
solution = solve_problem(problem, solver="osqp")
if solution.found:
print(f"QPSUT01 solved successfully")
print(f"Expected x: {expected_solution.x}")
print(f"Computed x: {solution.x}")
print(f"Difference: {np.linalg.norm(solution.x - expected_solution.x):.6f}")Comprehensive exception hierarchy for error handling and debugging.
class QPError(Exception):
"""
Base class for all qpsolvers exceptions.
All qpsolvers-specific exceptions inherit from this class to avoid
abstraction leakage from underlying solver libraries.
"""
class NoSolverSelected(QPError):
"""
Exception raised when the solver parameter is not specified.
Raised by solve_qp when no solver is specified and no default is available.
"""
class ParamError(QPError):
"""
Exception raised when solver parameters are incorrect.
Indicates issues with solver-specific parameter values or combinations.
"""
class ProblemError(QPError):
"""
Exception raised when a quadratic program is malformed.
Indicates issues with problem formulation such as:
- Inconsistent matrix dimensions
- Invalid constraint specifications
- Non-convex problems (when detected)
"""
class SolverNotFound(QPError):
"""
Exception raised when a requested solver is not available.
Includes suggestions for installing the missing solver.
"""
class SolverError(QPError):
"""
Exception raised when a solver fails during execution.
Wraps solver-specific errors to maintain abstraction boundaries.
"""Usage example:
from qpsolvers import solve_qp, QPError, SolverNotFound, ProblemError
import numpy as np
def robust_solve(P, q, G=None, h=None, A=None, b=None, solvers=None):
"""Robust QP solving with comprehensive error handling."""
if solvers is None:
from qpsolvers import available_solvers
solvers = available_solvers
for solver in solvers:
try:
x = solve_qp(P, q, G, h, A, b, solver=solver, verbose=False)
if x is not None:
print(f"Successfully solved with {solver}")
return x
except SolverNotFound:
print(f"Solver {solver} not available, trying next...")
continue
except ProblemError as e:
print(f"Problem is malformed: {e}")
return None
except QPError as e:
print(f"Solver {solver} failed: {e}")
continue
print("No solver succeeded")
return None
# Example usage
P = np.eye(3)
q = np.ones(3)
result = robust_solve(P, q, solvers=["nonexistent_solver", "osqp", "cvxopt"])def validate_qp_solution(
x: np.ndarray,
P: Union[np.ndarray, spa.csc_matrix],
q: np.ndarray,
G: Optional[Union[np.ndarray, spa.csc_matrix]] = None,
h: Optional[np.ndarray] = None,
A: Optional[Union[np.ndarray, spa.csc_matrix]] = None,
b: Optional[np.ndarray] = None,
lb: Optional[np.ndarray] = None,
ub: Optional[np.ndarray] = None,
tolerance: float = 1e-6
) -> Dict[str, Any]:
"""
Validate a QP solution against all constraints.
Parameters:
- x: Candidate solution
- P, q, G, h, A, b, lb, ub: Problem definition
- tolerance: Tolerance for constraint violations
Returns:
Dictionary with validation results and constraint violations
"""
results = {
'feasible': True,
'violations': {},
'objective': 0.5 * x.T @ P @ x + q.T @ x
}
# Check linear inequalities
if G is not None and h is not None:
violations = (G @ x - h).clip(min=0)
max_violation = np.max(violations)
if max_violation > tolerance:
results['feasible'] = False
results['violations']['linear_inequality'] = max_violation
# Check linear equalities
if A is not None and b is not None:
violations = np.abs(A @ x - b)
max_violation = np.max(violations)
if max_violation > tolerance:
results['feasible'] = False
results['violations']['linear_equality'] = max_violation
# Check box constraints
if lb is not None:
violations = (lb - x).clip(min=0)
max_violation = np.max(violations)
if max_violation > tolerance:
results['feasible'] = False
results['violations']['lower_bound'] = max_violation
if ub is not None:
violations = (x - ub).clip(min=0)
max_violation = np.max(violations)
if max_violation > tolerance:
results['feasible'] = False
results['violations']['upper_bound'] = max_violation
return results
# Example usage
x_candidate = np.array([0.5, 0.3, 0.2])
validation = validate_qp_solution(x_candidate, P, q, G, h, A, b, lb, ub)
print(f"Solution feasible: {validation['feasible']}")
print(f"Objective value: {validation['objective']:.6f}")
if validation['violations']:
print(f"Constraint violations: {validation['violations']}")Install with Tessl CLI
npx tessl i tessl/pypi-qpsolvers