Coverage for src/cvx/quadprog/_setup.py: 100%
34 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-09 18:50 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-09 18:50 +0000
1"""Everything that happens before the first iteration.
3Validation of the caller's arrays, and the factorisation of ``G`` that the
4iteration is carried out in terms of.
5"""
7# G, C, R and J are the names used in Goldfarb & Idnani (1983) and in the
8# reference implementation's public signature `solve_qp(G, a, C, b, meq)`.
9# Lowercasing them would obscure the correspondence to the paper, so the
10# pep8-naming rules are waived here, as they are in _solve.py.
11# ruff: noqa: N803, N806, TRY003
13import numpy as np
14import scipy.linalg
15from scipy.linalg.lapack import dtrtri
18def _validate(
19 G: np.ndarray, a: np.ndarray, C: np.ndarray, b: np.ndarray, meq: int, check_finite: bool = False
20) -> tuple[int, int]:
21 """Check that the problem data is dimensionally consistent.
23 Args:
24 G: ``(n, n)`` matrix of the quadratic term.
25 a: ``(n,)`` vector of the linear term.
26 C: ``(n, m)`` constraint matrix.
27 b: ``(m,)`` right-hand side of the constraints.
28 meq: Number of leading constraints treated as equalities.
29 check_finite: Whether to reject NaN and infinity in the inputs. Off by
30 default, matching the reference; see ``solve_qp``.
32 Returns:
33 The number of variables and the number of constraints.
35 Raises:
36 ValueError: If any shape disagrees, if ``meq`` is out of range, or if
37 ``check_finite`` is set and any input holds a non-finite value.
38 """
39 if G.ndim != 2 or G.shape[0] != G.shape[1]:
40 raise ValueError(f"G must be a square matrix. Received shape={G.shape}")
41 n = G.shape[0]
42 if a.shape != (n,):
43 raise ValueError(f"G and a must have the same dimension. Received G as {G.shape} and a as {a.shape}")
44 if C.ndim != 2 or C.shape[0] != n:
45 raise ValueError(f"G and C must have the same first dimension. Received G as {G.shape} and C as {C.shape}")
46 q = C.shape[1]
47 if b.shape != (q,):
48 raise ValueError(
49 f"The number of columns of C must match the length of b. Received C as {C.shape} and b as {b.shape}"
50 )
51 if not 0 <= meq <= q:
52 raise ValueError(f"meq must satisfy 0 <= meq <= {q}. Received {meq}")
53 if check_finite:
54 # Last, so a caller who passes both a wrong shape and a NaN still hears
55 # about the shape -- that is the error they can act on without reading
56 # their data.
57 _check_finite(G, a, C, b)
58 return n, q
61def _check_finite(G: np.ndarray, a: np.ndarray, C: np.ndarray, b: np.ndarray) -> None:
62 """Reject NaN and infinity in the problem data, naming the first offender.
64 Only reached when ``check_finite`` is set: the scan is :math:`O(n^2)` on
65 ``G``, which is why it is opt-in rather than unconditional.
67 Args:
68 G: ``(n, n)`` matrix of the quadratic term.
69 a: ``(n,)`` vector of the linear term.
70 C: ``(n, m)`` constraint matrix.
71 b: ``(m,)`` right-hand side of the constraints.
73 Raises:
74 ValueError: If any argument holds a non-finite value.
75 """
76 for name, array in (("G", G), ("a", a), ("C", C), ("b", b)):
77 if not np.isfinite(array).all():
78 raise ValueError(f"{name} contains a non-finite value (NaN or infinity)")
81def _factorize(G: np.ndarray, a: np.ndarray, factorized: bool) -> tuple[np.ndarray, np.ndarray]:
82 """Return the inverse Cholesky factor of ``G`` and the unconstrained minimum.
84 Args:
85 G: ``(n, n)`` positive definite matrix, or its inverse Cholesky factor
86 :math:`R^{-1}` when ``factorized`` is True.
87 a: ``(n,)`` vector of the linear term.
88 factorized: Whether ``G`` already holds :math:`R^{-1}`.
90 Returns:
91 ``J``, an upper triangular array with ``J J^T = G^-1``, and the
92 unconstrained minimiser ``G^-1 a``. ``J`` is a fresh writable array; the
93 caller updates it in place.
95 Raises:
96 ValueError: If ``G`` is not positive definite.
97 """
98 # Fortran order throughout: the updates in _qr work on column blocks of J,
99 # which are then contiguous and can go straight to BLAS.
100 if factorized:
101 J = np.asfortranarray(np.triu(G))
102 return J, J @ (J.T @ a)
104 # check_finite=False skips a full scan of each array on the way in, which
105 # matches the reference: it does not check either. A non-finite G is
106 # therefore not diagnosed here, and what happens next is a property of the
107 # LAPACK build rather than of this package -- Accelerate reports a failed
108 # potrf and raises below, OpenBLAS runs to completion and propagates NaNs
109 # into the result. Neither returns a finite wrong answer, which is the only
110 # guarantee callers can portably rely on.
111 try:
112 R = scipy.linalg.cholesky(G, lower=False, check_finite=False)
113 except scipy.linalg.LinAlgError as exc:
114 raise ValueError("matrix G is not positive definite") from exc
116 xv = scipy.linalg.cho_solve((R, False), a, check_finite=False)
117 J, info = dtrtri(R, lower=0)
118 if info != 0: # pragma: no cover
119 # Defensive: trtri fails only on an exactly zero diagonal entry, which
120 # a successful Cholesky has already ruled out.
121 raise ValueError("matrix G is not positive definite")
122 return np.asfortranarray(np.triu(J)), xv