Coverage for src/nncg/certificate.py: 100%
14 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-15 07:01 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-15 07:01 +0000
1"""The KKT certificate for the non-negative quadratic program and its shared precondition.
3:func:`kkt_violation` scores how far a candidate is from the unique global
4minimiser of ``min_{x>=0} 1/2 x'Ax - b'x`` — zero certifies optimality — and is
5the load-bearing check the paper's numerical study reports against.
6:func:`_require_operator` is the one operator/right-hand-side precondition shared
7by the certificate and both :class:`nncg.solver.ActiveSetSolver` entry points.
8"""
10from __future__ import annotations
12import numpy as np
13from cvx.linalg import SymmetricOperator, Vector
15_NEEDS_OPERATOR = (
16 "the quadratic term must be a cvx.linalg.SymmetricOperator: wrap a dense SPD "
17 "array in DenseOperator(a), or pass GramOperator(M, ridge) for A = M'M + ridge*I"
18)
21def _require_operator(a: SymmetricOperator, b: Vector) -> None:
22 """Validate that ``a`` is a symmetric operator whose dimension matches ``b``.
24 Args:
25 a: The quadratic term, expected to be a :class:`cvx.linalg.SymmetricOperator`.
26 b: The linear term ``b``.
28 Raises:
29 TypeError: When ``a`` is not a :class:`cvx.linalg.SymmetricOperator`.
30 ValueError: When the operator dimension does not match ``len(b)``.
31 """
32 if not isinstance(a, SymmetricOperator):
33 raise TypeError(_NEEDS_OPERATOR)
34 if a.n != len(b):
35 msg = f"operator dimension {a.n} does not match len(b) = {len(b)}"
36 raise ValueError(msg)
39def kkt_violation(a: SymmetricOperator, b: Vector, x: Vector) -> float:
40 """Maximum violation of the KKT system of ``min_{x>=0} 1/2 x'Ax - b'x``.
42 Args:
43 a: The SPD operator ``A`` (a :class:`cvx.linalg.SymmetricOperator`).
44 b: The linear term ``b``.
45 x: Candidate solution.
47 Returns:
48 ``max`` of the negativity violations of ``x`` and of the reduced
49 gradient ``s = A x - b``, and of the complementarity products
50 ``|x_i s_i|``. Zero certifies the unique global minimiser.
52 Examples:
53 Note that ``a`` must be an operator — a bare array raises ``TypeError``:
55 >>> import numpy as np
56 >>> from cvx.linalg import DenseOperator
57 >>> a = DenseOperator(np.array([[2.0, 0.0], [0.0, 2.0]]))
58 >>> b = np.array([2.0, -2.0])
60 The minimiser certifies at zero, while the origin does not:
62 >>> round(kkt_violation(a, b, np.array([1.0, 0.0])), 12)
63 0.0
64 >>> kkt_violation(a, b, np.zeros(2)) > 0
65 True
66 """
67 _require_operator(a, b)
68 s = a.matvec(x) - b
69 return float(
70 max(
71 np.max(-x, initial=0.0),
72 np.max(-s, initial=0.0),
73 np.max(np.abs(x * s), initial=0.0),
74 )
75 )