Repository navigation
QP_weeds
Source: src/QP.py · User guide: Quadratic Programs · Theory: Scenario Theory
solve_qp is the quadratic-objective version of solve_lp. The constraints, the slack handling, the support-list computation and the return values are the same. What changes is the objective, which gains Q. This page covers the whole interface and goes into detail only where the QP differs from the LP. The shared mechanics are on LP.py.
where x_ref. The robust, regularization and relaxation formulations come from setting tau, rho and include_slack exactly as for the LP (see the mapping table).
from src.QP import solve_qp
x_out, zeta_out, cost_out, N, complexity, constraints, degeneracy = solve_qp(
deltas, A_d, b_d, G, h, c, Q,
tau=0.0, x_ref=np.array([0.0]), rho=0.0, norm_type=2,
solver=None, include_slack=None,
)Q is the seventh positional argument, after c. Everything before and after it matches solve_lp.
| Name | Type / shape | Default | Meaning |
|---|---|---|---|
deltas |
numpy.ndarray, (N, q) or (N,)
|
required | The scenarios, one per row. Each row goes unchanged to A_d and b_d. An empty deltas raises ValueError. |
A_d |
callable, δ → ndarray (m_s, n)
|
required | Returns n is taken from A_d(deltas[0]).shape[1]. |
b_d |
callable, δ → ndarray (m_s, 1) or (m_s,)
|
required | Returns |
G |
numpy.ndarray, (p, n)
|
required | Hard-constraint matrix. Pass np.array([]) if there are none. |
h |
numpy.ndarray, (p, 1) or (p,)
|
required | Hard-constraint offset, reshaped to a column. If G or h has size 0, no hard constraint is added. |
c |
numpy.ndarray, (n, 1) or (n,)
|
required | Linear cost vector. |
Q |
numpy.ndarray, (n, n)
|
required | Quadratic cost matrix. It must be symmetric positive semidefinite, up to a small tolerance (see Validation of Q). np.zeros((n, n)) is allowed and reduces the problem to an LP. |
tau |
float |
0.0 |
Regularization weight |
x_ref |
scalar or array with 1 or n entries |
np.array([0.0]) |
Regularization centre (n, 1) (n entries). Any other size raises ValueError. |
rho |
float |
0.0 |
Slack penalty include_slack=None, slack exists exactly when rho != 0. |
norm_type |
any number p >= 1, "inf" / np.inf, "fro"
|
2 |
Order of the vector norm of x - x_ref, as in the LP. Used only when tau > 0. |
solver |
str or None
|
None |
CVXPY solver for the main solve and for all re-solves. See Solver compatibility. |
include_slack |
bool or None
|
None |
True forces slack, False forbids it, and None means "slack if rho != 0". Same semantics as solve_lp. |
| Name | Type / shape | Meaning |
|---|---|---|
x_out |
numpy.ndarray, (n, 1)
|
Optimal |
zeta_out |
numpy.ndarray, (N,)
|
Optimal slacks, or np.zeros(N) without slack. |
cost_out |
float |
prob.value, the full objective including |
N |
int |
len(deltas). |
complexity |
int |
|
constraints |
list of N cvxpy.constraints.Inequality
|
Scenario constraints in the order of deltas. Hard constraints are excluded. Their variable values are overwritten by later re-solves, as described in the LP caveat. |
degeneracy |
bool |
True when the lower risk bound is not certified. |
| Exception | When |
|---|---|
AssertionError |
Q is not symmetric within the tolerance: "Q must be symmetric". This check runs first. |
AssertionError |
Q has an eigenvalue below the negative tolerance: "Q must be positive semi-definite (eigenvalues: ...)". |
ValueError |
deltas is empty or None. |
ValueError |
x_ref has neither 1 nor n entries. |
Exception (from CVXPY) |
Q is not n × n: "Invalid dimensions for arguments to quad_form."
|
ValueError |
The status is not optimal or optimal_inaccurate: "QP did not solve to optimality. Status: ..."
|
cvxpy.error.SolverError |
The solver is missing or cannot handle the problem's cones. |
ValueError |
get_support cannot recover a support list. |
The pipeline is the same as solve_lp, with two additions.
Before anything else, the function runs Q = check_psd(Q) (in src/Miscellaneous.py):
Q = np.asarray(Q, dtype=float)
scale = max(1.0, float(np.abs(Q).max()))
assert np.allclose(Q, Q.T, rtol=0, atol=tol * scale), "Q must be symmetric" # tol = 1e-9
Q = (Q + Q.T) / 2
eigvals = np.linalg.eigvalsh(Q)
assert eigvals.min() >= -tol * scale, f"Q must be positive semi-definite (eigenvalues: {eigvals})"-
Both tests use a relative tolerance of
$10^{-9}$ times the largest absolute entry ofQ(or$10^{-9}$ when all entries are below 1). -
Rank-deficient matrices pass. Floating-point rounding often turns a zero eigenvalue into a tiny negative number, e.g. about
-6e-18forM @ M.TwithMof shape3 × 2. In a test with 20 such random matrices, all 20 were accepted. -
Symmetry up to rounding passes, and the symmetrised matrix
$\tfrac12 (Q + Q^\top)$ is what the solver uses. -
The eigenvalues come from
np.linalg.eigvalsh, the symmetric solver, applied after symmetrisation. - Q is not projected onto the PSD cone. A matrix whose most negative eigenvalue lies within the tolerance is passed on unchanged, and CVXPY is told to treat it as PSD (below). For matrices that are PSD up to rounding this is harmless.
objective = cp.Minimize((1/2) * cp.quad_form(x, cp.psd_wrap(Q))
+ c.T @ x + regularization_term(tau, x, x_ref, norm_type) + rho * cp.sum(zeta))cp.psd_wrap(Q) tells CVXPY to trust that Q is PSD, so CVXPY does not repeat its own eigenvalue check (check_psd already did it). As with the LP, the norm term is left out when tau == 0.
Variables, scenario constraints (with b(δ) reshaped to a column), hard constraints, the reshaping of x_ref, the status check and the call to get_support(constraints, non_risk_constraints, prob, objective, solver=solver) are line for line the same as in LP.py. This covers the dual screen at 1e-8, the value-based validation at relative tolerance 1e-6, greedy pruning, the degeneracy flag and the fallback path. The re-solves inside get_support reuse the same objective, so they are QPs too. All loops run in sequence, with no parallelism.
| Case | What CVXPY sends to the solver | Solvers that can accept it (CVXPY 1.7.2 solver metadata) |
|---|---|---|
tau == 0, or norm_type 1 or "inf"
|
quadratic objective plus linear constraints | conic solvers (CLARABEL, SCS, ECOS, CVXOPT, MOSEK) and QP solvers (OSQP, PIQP, PROXQP, DAQP, …) |
tau > 0, norm_type 2 or "fro"
|
quadratic objective plus a second-order cone | conic solvers only |
tau > 0, any other p
|
quadratic objective plus a power cone |
CLARABEL, SCS, MOSEK
|
Tested on the example below (CVXPY 1.7.2):
-
CLARABELgives$k = 1$ , non-degenerate, without regularization and withtau=0.5fornorm_type1, 2 and 3. -
SCSgives$k = 1$ without regularization. Withtau=0.5its solutions are not accurate enough for the support-list screen: the fallback runs and reportsdegeneracy=Truewith$k = 77$ (norm_type=1) or$k = 274$ (norm_type=2). -
OSQPgives$k = 1$ without regularization and withnorm_type=1, and rejectsnorm_type=2withtau > 0("Problem could not be reduced to a QP"). -
SCIPYis an LP solver and cannot solve QPs (SolverError);get_solversoffers it for LPs only.
This example finds the minimum-norm point that satisfies
import numpy as np
from src.QP import solve_qp
from src.Risk import quantify_risk
# Minimum-norm point that satisfies delta_i^T x >= 1 for every sampled delta_i:
# min 1/2 x^T Q x s.t. 1 - delta_i^T x <= 0
rng = np.random.default_rng(1)
deltas = rng.uniform(0.5, 1.5, size=(300, 2)) # N = 300 scenarios, q = 2
A_d = lambda d: -np.array([[d[0], d[1]]]) # (1, 2)
b_d = lambda d: np.array([[1.0]]) # (1, 1)
G = h = np.array([])
c = np.zeros((2, 1))
Q = np.eye(2) # symmetric PSD
x, zeta, cost, N, k, cons, degenerate = solve_qp(deltas, A_d, b_d, G, h, c, Q, solver="CLARABEL")
print("x =", x.ravel(), " cost =", round(cost, 6), " N =", N, " k =", k, " degenerate =", degenerate)
epsL, epsU = quantify_risk(k, N, 1e-6)
print(f"risk in [{epsL:.4f}, {epsU:.4f}] with confidence 1 - 1e-6")
# Same problem with regularisation towards x_ref = (1, 1) using the 1-norm
x, zeta, cost, N, k, cons, degenerate = solve_qp(deltas, A_d, b_d, G, h, c, Q,
tau=0.5, x_ref=np.array([1.0, 1.0]), norm_type=1,
solver="CLARABEL")
print("regularised x =", x.ravel(), " cost =", round(cost, 6), " k =", k, " degenerate =", degenerate)Output (CVXPY 1.7.2, CLARABEL):
x = [0.85388612 0.95576728] cost = 0.821306 N = 300 k = 1 degenerate = False
risk in [0.0000, 0.0662] with confidence 1 - 1e-6
regularised x = [0.88352631 0.92928663] cost = 0.91569 k = 1 degenerate = False
Here a single half-space, the one closest to the origin, defines the optimum, so cost includes
- LP.py: the shared pipeline, tolerances and pitfalls, in full.
-
Miscellaneous.py: how
complexityanddegeneracyare computed. -
Risk.py: turning
(complexity, N, beta)into risk bounds. - Quadratic Programs: the user-level guide.