0
# Utilities and Conversions
1
2
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.
3
4
## Capabilities
5
6
### Matrix and Problem Conversions
7
8
Functions for converting between different matrix formats and problem formulations to ensure compatibility with various solvers.
9
10
```python { .api }
11
def combine_linear_box_inequalities(
12
G: Optional[Union[np.ndarray, spa.csc_matrix]],
13
h: Optional[np.ndarray],
14
lb: Optional[np.ndarray],
15
ub: Optional[np.ndarray],
16
n: int
17
) -> Tuple[Union[np.ndarray, spa.csc_matrix], np.ndarray]:
18
"""
19
Combine linear inequality and box constraints into a single system.
20
21
Converts box constraints lb <= x <= ub into linear inequalities and
22
combines them with existing G x <= h constraints.
23
24
Parameters:
25
- G: Linear inequality constraint matrix
26
- h: Linear inequality constraint vector
27
- lb: Lower bound constraints (can contain -np.inf)
28
- ub: Upper bound constraints (can contain +np.inf)
29
- n: Problem dimension
30
31
Returns:
32
Tuple of (combined_G, combined_h) representing all constraints as G_new x <= h_new
33
"""
34
35
def linear_from_box_inequalities(
36
lb: Optional[np.ndarray],
37
ub: Optional[np.ndarray],
38
n: int
39
) -> Tuple[Union[np.ndarray, spa.csc_matrix], np.ndarray]:
40
"""
41
Convert box constraints to linear inequality format.
42
43
Transforms lb <= x <= ub into -I x <= -lb and I x <= ub form.
44
45
Parameters:
46
- lb: Lower bound constraints (can contain -np.inf)
47
- ub: Upper bound constraints (can contain +np.inf)
48
- n: Problem dimension
49
50
Returns:
51
Tuple of (G, h) representing box constraints as linear inequalities
52
"""
53
54
def ensure_sparse_matrices(
55
P: Union[np.ndarray, spa.csc_matrix],
56
q: np.ndarray,
57
G: Optional[Union[np.ndarray, spa.csc_matrix]] = None,
58
h: Optional[np.ndarray] = None,
59
A: Optional[Union[np.ndarray, spa.csc_matrix]] = None,
60
b: Optional[np.ndarray] = None,
61
lb: Optional[np.ndarray] = None,
62
ub: Optional[np.ndarray] = None,
63
) -> Tuple[
64
spa.csc_matrix,
65
np.ndarray,
66
Optional[spa.csc_matrix],
67
Optional[np.ndarray],
68
Optional[spa.csc_matrix],
69
Optional[np.ndarray],
70
Optional[np.ndarray],
71
Optional[np.ndarray],
72
]:
73
"""
74
Convert all matrices to sparse CSC format for sparse solvers.
75
76
Parameters:
77
- P, q, G, h, A, b, lb, ub: Problem matrices and vectors
78
79
Returns:
80
Tuple with all matrices converted to scipy.sparse.csc_matrix format
81
"""
82
83
def socp_from_qp(problem: Problem) -> Dict[str, Any]:
84
"""
85
Convert a quadratic program to second-order cone program format.
86
87
Transforms QP into SOCP formulation for solvers like ECOS.
88
89
Parameters:
90
- problem: Quadratic program to convert
91
92
Returns:
93
Dictionary containing SOCP formulation matrices
94
"""
95
96
def split_dual_linear_box(
97
z_combined: np.ndarray,
98
n_linear: int,
99
n_box_lower: int,
100
n_box_upper: int
101
) -> Tuple[Optional[np.ndarray], Optional[np.ndarray]]:
102
"""
103
Split combined dual multipliers back into linear and box constraint parts.
104
105
Reverses the combination performed by combine_linear_box_inequalities.
106
107
Parameters:
108
- z_combined: Combined dual multiplier vector
109
- n_linear: Number of original linear constraints
110
- n_box_lower: Number of lower bound constraints
111
- n_box_upper: Number of upper bound constraints
112
113
Returns:
114
Tuple of (z_linear, z_box) dual multipliers
115
"""
116
```
117
118
Usage example:
119
120
```python
121
import numpy as np
122
import scipy.sparse as spa
123
from qpsolvers.conversions import (
124
combine_linear_box_inequalities,
125
linear_from_box_inequalities,
126
ensure_sparse_matrices
127
)
128
129
# Convert box constraints to linear inequalities
130
n = 3
131
lb = np.array([0.0, -np.inf, 1.0])
132
ub = np.array([np.inf, 2.0, 3.0])
133
134
G_box, h_box = linear_from_box_inequalities(lb, ub, n)
135
print(f"Box constraints as linear inequalities:")
136
print(f"G_box shape: {G_box.shape}")
137
print(f"h_box: {h_box}")
138
139
# Combine with existing linear constraints
140
G_linear = np.array([[1.0, 1.0, 1.0]])
141
h_linear = np.array([5.0])
142
143
G_combined, h_combined = combine_linear_box_inequalities(
144
G_linear, h_linear, lb, ub, n
145
)
146
print(f"Combined constraints shape: {G_combined.shape}")
147
148
# Convert matrices to sparse format
149
P = np.eye(n)
150
q = np.ones(n)
151
152
P_sparse, q_out, G_sparse, h_out, A_out, b_out, lb_out, ub_out = ensure_sparse_matrices(
153
P, q, G_combined, h_combined
154
)
155
print(f"P matrix format: {type(P_sparse)}")
156
print(f"G matrix format: {type(G_sparse)}")
157
```
158
159
### Debugging and Visualization
160
161
Utilities for debugging QP problems and visualizing matrix structures.
162
163
```python { .api }
164
def print_matrix_vector(
165
A: Union[np.ndarray, spa.csc_matrix],
166
A_label: str,
167
b: np.ndarray,
168
b_label: str,
169
column_width: int = 24,
170
) -> None:
171
"""
172
Print a matrix and vector side by side for debugging.
173
174
Useful for visualizing constraint matrices and vectors together.
175
176
Parameters:
177
- A: Matrix to print (converted to dense if sparse)
178
- A_label: Label for the matrix
179
- b: Vector to print
180
- b_label: Label for the vector
181
- column_width: Width of output columns in characters
182
"""
183
```
184
185
Usage example:
186
187
```python
188
import numpy as np
189
from qpsolvers import print_matrix_vector
190
191
# Create example constraint
192
G = np.array([
193
[1.0, 2.0, -1.0],
194
[0.0, 1.0, 2.0],
195
[-1.0, 0.0, 1.0]
196
])
197
h = np.array([3.0, 2.0, 1.0])
198
199
print("Constraint visualization:")
200
print_matrix_vector(G, "G", h, "h")
201
202
# Output will show G and h matrices side by side:
203
# G = h =
204
# [[ 1. 2. -1.] [[3.]
205
# [ 0. 1. 2.] [2.]
206
# [-1. 0. 1.]] [1.]]
207
```
208
209
### Sample Problem Generation
210
211
Functions for creating test problems and benchmarks.
212
213
```python { .api }
214
def get_sparse_least_squares(n: int) -> Tuple[
215
spa.csc_matrix, # R matrix
216
np.ndarray, # s vector
217
spa.csc_matrix, # G matrix
218
np.ndarray, # h vector
219
spa.csc_matrix, # A matrix
220
np.ndarray, # b vector
221
]:
222
"""
223
Generate a sparse least squares test problem.
224
225
Creates a problem of the form:
226
minimize ||R x - s||^2
227
subject to G x <= h, A x = b
228
229
Parameters:
230
- n: Problem dimension
231
232
Returns:
233
Tuple of (R, s, G, h, A, b) defining the least squares problem
234
"""
235
236
def get_qpsut01() -> Tuple[Problem, Solution]:
237
"""
238
Get QPSUT01 problem and its solution.
239
240
Returns:
241
Tuple of (problem, solution) pair for testing
242
"""
243
244
def get_qpsut02() -> Tuple[Problem, Solution]:
245
"""
246
Get QPSUT02 problem and its solution.
247
248
Returns:
249
Tuple of (problem, solution) pair for testing
250
"""
251
252
def get_qpsut03() -> Tuple[Problem, Solution]:
253
"""
254
Get QPSUT03 problem and its solution.
255
256
This problem has partial box bounds with -infinity on some lower
257
bounds and +infinity on some upper bounds.
258
259
Returns:
260
Tuple of (problem, solution) pair for testing
261
"""
262
263
def get_qpsut04() -> Tuple[Problem, Solution]:
264
"""
265
Get QPSUT04 problem and its solution.
266
267
Returns:
268
Tuple of (problem, solution) pair for testing
269
"""
270
271
def get_qpsut05() -> Tuple[Problem, Solution]:
272
"""
273
Get QPSUT05 problem and its solution.
274
275
Returns:
276
Tuple of (problem, solution) pair for testing
277
"""
278
279
def get_qptest() -> Tuple[Problem, Solution]:
280
"""
281
Get QPTEST problem from the Maros-Meszaros test set.
282
283
Returns:
284
Tuple of (problem, solution) pair for testing
285
"""
286
287
def get_qpgurdu() -> Tuple[Problem, Solution]:
288
"""
289
Get sample random problem with linear inequality constraints.
290
291
Returns:
292
Tuple of (problem, solution) pair for testing
293
"""
294
295
def get_qpgureq() -> Tuple[Problem, Solution]:
296
"""
297
Get sample random problem with equality constraints.
298
299
Returns:
300
Tuple of (problem, solution) pair for testing
301
"""
302
303
def get_qpgurabs() -> Tuple[Problem, Solution]:
304
"""
305
Get sample random problem with box constraints.
306
307
Returns:
308
Tuple of (problem, solution) pair for testing
309
"""
310
```
311
312
Usage example:
313
314
```python
315
from qpsolvers.problems import get_sparse_least_squares, get_qpsut01
316
from qpsolvers import solve_ls, solve_problem
317
318
# Generate sparse least squares test problem
319
n = 50
320
R, s, G, h, A, b, lb, ub = get_sparse_least_squares(n)
321
322
print(f"Generated {n}-dimensional sparse least squares problem")
323
print(f"R matrix shape: {R.shape}, sparsity: {R.nnz / (R.shape[0] * R.shape[1]):.3f}")
324
print(f"G matrix shape: {G.shape}")
325
print(f"A matrix shape: {A.shape}")
326
327
# Solve the generated problem
328
x = solve_ls(R, s, G, h, A, b, lb=lb, ub=ub, solver="osqp")
329
if x is not None:
330
print(f"Solution found with residual: {np.linalg.norm(R @ x - s):.6f}")
331
332
# Use a predefined test problem
333
problem, expected_solution = get_qpsut01()
334
solution = solve_problem(problem, solver="osqp")
335
336
if solution.found:
337
print(f"QPSUT01 solved successfully")
338
print(f"Expected x: {expected_solution.x}")
339
print(f"Computed x: {solution.x}")
340
print(f"Difference: {np.linalg.norm(solution.x - expected_solution.x):.6f}")
341
```
342
343
### Exception Handling
344
345
Comprehensive exception hierarchy for error handling and debugging.
346
347
```python { .api }
348
class QPError(Exception):
349
"""
350
Base class for all qpsolvers exceptions.
351
352
All qpsolvers-specific exceptions inherit from this class to avoid
353
abstraction leakage from underlying solver libraries.
354
"""
355
356
class NoSolverSelected(QPError):
357
"""
358
Exception raised when the solver parameter is not specified.
359
360
Raised by solve_qp when no solver is specified and no default is available.
361
"""
362
363
class ParamError(QPError):
364
"""
365
Exception raised when solver parameters are incorrect.
366
367
Indicates issues with solver-specific parameter values or combinations.
368
"""
369
370
class ProblemError(QPError):
371
"""
372
Exception raised when a quadratic program is malformed.
373
374
Indicates issues with problem formulation such as:
375
- Inconsistent matrix dimensions
376
- Invalid constraint specifications
377
- Non-convex problems (when detected)
378
"""
379
380
class SolverNotFound(QPError):
381
"""
382
Exception raised when a requested solver is not available.
383
384
Includes suggestions for installing the missing solver.
385
"""
386
387
class SolverError(QPError):
388
"""
389
Exception raised when a solver fails during execution.
390
391
Wraps solver-specific errors to maintain abstraction boundaries.
392
"""
393
```
394
395
Usage example:
396
397
```python
398
from qpsolvers import solve_qp, QPError, SolverNotFound, ProblemError
399
import numpy as np
400
401
def robust_solve(P, q, G=None, h=None, A=None, b=None, solvers=None):
402
"""Robust QP solving with comprehensive error handling."""
403
404
if solvers is None:
405
from qpsolvers import available_solvers
406
solvers = available_solvers
407
408
for solver in solvers:
409
try:
410
x = solve_qp(P, q, G, h, A, b, solver=solver, verbose=False)
411
if x is not None:
412
print(f"Successfully solved with {solver}")
413
return x
414
415
except SolverNotFound:
416
print(f"Solver {solver} not available, trying next...")
417
continue
418
419
except ProblemError as e:
420
print(f"Problem is malformed: {e}")
421
return None
422
423
except QPError as e:
424
print(f"Solver {solver} failed: {e}")
425
continue
426
427
print("No solver succeeded")
428
return None
429
430
# Example usage
431
P = np.eye(3)
432
q = np.ones(3)
433
result = robust_solve(P, q, solvers=["nonexistent_solver", "osqp", "cvxopt"])
434
```
435
436
### Advanced Utilities
437
438
```python
439
def validate_qp_solution(
440
x: np.ndarray,
441
P: Union[np.ndarray, spa.csc_matrix],
442
q: np.ndarray,
443
G: Optional[Union[np.ndarray, spa.csc_matrix]] = None,
444
h: Optional[np.ndarray] = None,
445
A: Optional[Union[np.ndarray, spa.csc_matrix]] = None,
446
b: Optional[np.ndarray] = None,
447
lb: Optional[np.ndarray] = None,
448
ub: Optional[np.ndarray] = None,
449
tolerance: float = 1e-6
450
) -> Dict[str, Any]:
451
"""
452
Validate a QP solution against all constraints.
453
454
Parameters:
455
- x: Candidate solution
456
- P, q, G, h, A, b, lb, ub: Problem definition
457
- tolerance: Tolerance for constraint violations
458
459
Returns:
460
Dictionary with validation results and constraint violations
461
"""
462
463
results = {
464
'feasible': True,
465
'violations': {},
466
'objective': 0.5 * x.T @ P @ x + q.T @ x
467
}
468
469
# Check linear inequalities
470
if G is not None and h is not None:
471
violations = (G @ x - h).clip(min=0)
472
max_violation = np.max(violations)
473
if max_violation > tolerance:
474
results['feasible'] = False
475
results['violations']['linear_inequality'] = max_violation
476
477
# Check linear equalities
478
if A is not None and b is not None:
479
violations = np.abs(A @ x - b)
480
max_violation = np.max(violations)
481
if max_violation > tolerance:
482
results['feasible'] = False
483
results['violations']['linear_equality'] = max_violation
484
485
# Check box constraints
486
if lb is not None:
487
violations = (lb - x).clip(min=0)
488
max_violation = np.max(violations)
489
if max_violation > tolerance:
490
results['feasible'] = False
491
results['violations']['lower_bound'] = max_violation
492
493
if ub is not None:
494
violations = (x - ub).clip(min=0)
495
max_violation = np.max(violations)
496
if max_violation > tolerance:
497
results['feasible'] = False
498
results['violations']['upper_bound'] = max_violation
499
500
return results
501
502
# Example usage
503
x_candidate = np.array([0.5, 0.3, 0.2])
504
validation = validate_qp_solution(x_candidate, P, q, G, h, A, b, lb, ub)
505
print(f"Solution feasible: {validation['feasible']}")
506
print(f"Objective value: {validation['objective']:.6f}")
507
if validation['violations']:
508
print(f"Constraint violations: {validation['violations']}")
509
```