API reference¶
The public API: the two solvers and the KKT certificate. The matrix-free
inner solvers in nncg.krylov are documented below as the package's core
contribution, but are driven by the solvers rather than imported directly.
The planted-optimum problem generators live outside the installed package,
in the repository's tests/problems.py.
Solver¶
nncg.solver
¶
Non-negative conjugate gradients: the active-set / block-principal-pivoting loop.
Solves the strictly convex non-negative quadratic program
min_{x >= 0} 1/2 x^T A x - b^T x, A symmetric positive definite,
and its equality-augmented variant with a general linear system B x = c,
by wrapping a matrix-free conjugate-gradient inner solver in a primal-dual
active-set outer loop. The working-set toggles are the principal pivots of the
linear complementarity problem LCP(A, -b); guarding the fast block-pivot path
with a least-index Bland fallback gives unconditional finite termination at
the unique global minimiser — no non-degeneracy assumption (Theorem 5.1 of the
accompanying paper). See https://github.com/Jebel-Quant/mean_variance_solvers.
The quadratic term enters as a :class:cvx.linalg.SymmetricOperator, accessed
only through block products: apply_free drives the CG inner solves,
matvec the reduced gradient, and solve_free the optional direct inner
solver. Wrap an explicit SPD array in DenseOperator; for the Gram case
A = M^T M + ridge I pass GramOperator(M, ridge) and the n x n
matrix is never formed.
ReducedGradient = Callable[['Vector', 'Vector | None'], 'Vector']
module-attribute
¶
Reduced gradient of the subproblem: (x, lam) -> s.
SubSolve = Callable[[NDArray[np.int_], 'Vector | None'], 'tuple[Vector, Vector | None, int]']
module-attribute
¶
Subproblem solve on a free set: (idx, x0) -> (x_F, lam, inner_iters).
Result
dataclass
¶
Outcome of an active-set solve.
Attributes:
| Name | Type | Description |
|---|---|---|
x |
Vector
|
The minimiser (or the final iterate if |
outer |
int
|
Number of outer active-set steps taken. |
inner |
int
|
Total inner (CG/PCG) iterations across all outer steps; each direct inner solve counts as one. |
fallback |
int
|
Number of least-index Bland fallback pivots taken. |
converged |
bool
|
True when the KKT exit was reached; False when an
|
free |
NDArray[bool_]
|
Boolean mask of the final free set. |
lam |
Vector | None
|
Multipliers of the equality constraints (equality-augmented solves only; None otherwise). |
traj |
list[tuple[int, ...]] | None
|
The sequence of visited free sets as index tuples when trajectory tracking was requested; None otherwise. |
Source code in src/nncg/solver.py
kkt_violation(a, b, x)
¶
Maximum violation of the KKT system of min_{x>=0} 1/2 x'Ax - b'x.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
a
|
SymmetricOperator
|
The SPD operator |
required |
b
|
Vector
|
The linear term |
required |
x
|
Vector
|
Candidate solution. |
required |
Returns:
| Type | Description |
|---|---|
float
|
|
float
|
gradient |
float
|
|
Source code in src/nncg/solver.py
solve_nnqp(a, b, tol=1e-08, cg_tol=1e-10, p_max=3, inner='cg', track=False, cg_maxit=100000, max_outer=None, warm=None)
¶
Minimise 1/2 x^T A x - b^T x over x >= 0 by the active-set loop.
Each free-block solve is matrix-free CG on v -> op.apply_free(F, v);
the reduced matrix is never materialised and A is never refactorised.
The batch block-pivot fast path is guarded by a least-index Bland
fallback, so termination at the unique global minimiser is unconditional
— no non-degeneracy assumption.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
a
|
SymmetricOperator
|
The SPD operator |
required |
b
|
Vector
|
The linear term |
required |
tol
|
float
|
Threshold of the primal and dual violator tests. |
1e-08
|
cg_tol
|
float
|
Relative residual tolerance of the inner solves. Keep it a
couple of orders below |
1e-10
|
p_max
|
int
|
Patience budget — non-improving batch steps tolerated before a fallback pivot. Any value gives finite termination. |
3
|
inner
|
str
|
|
'cg'
|
track
|
bool
|
Record the free-set trajectory in |
False
|
cg_maxit
|
int
|
Iteration cap per inner solve. |
100000
|
max_outer
|
int | None
|
Optional cap on outer steps; when hit, the current iterate
is returned with |
None
|
warm
|
tuple[NDArray[bool_], Vector] | None
|
Optional |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
A |
Result
|
class: |
Result
|
satisfied to |
Raises:
| Type | Description |
|---|---|
TypeError
|
When |
ValueError
|
When the operator dimension does not match |
NotImplementedError
|
When |
Source code in src/nncg/solver.py
266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 | |
solve_nnqp_eq(a, b, b_eq, c_eq, tol=1e-08, cg_tol=1e-10, p_max=3, warm=None)
¶
Solve min 1/2 x^T A x - b^T x subject to x >= 0 and B x = c.
On each free set the saddle system is solved by eliminating the multiplier
lambda in R^p through the p-by-p Schur complement
S = B_F A_F^{-1} B_F^T: the p + 1 right-hand sides share the
operator A_F and are each one matrix-free CG solve, then
S lambda = c - B_F v0 fixes the multipliers in closed form. The single
normalisation 1^T x = beta is the p = 1 case. B must have full
row rank on the visited free sets (automatic for p = 1).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
a
|
SymmetricOperator
|
The SPD operator |
required |
b
|
Vector
|
The linear term |
required |
b_eq
|
Matrix
|
Equality matrix |
required |
c_eq
|
Vector
|
Equality right-hand side |
required |
tol
|
float
|
Threshold of the primal and dual violator tests. |
1e-08
|
cg_tol
|
float
|
Relative residual tolerance of the inner CG solves. |
1e-10
|
p_max
|
int
|
Patience budget of the batch fast path. |
3
|
warm
|
tuple[NDArray[bool_], Vector] | None
|
Optional |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
A |
Result
|
class: |
Result
|
gradient underlying the dual test is |
Raises:
| Type | Description |
|---|---|
TypeError
|
When |
ValueError
|
When the operator dimension does not match |
Source code in src/nncg/solver.py
Inner solvers (internal)¶
nncg.krylov
¶
Conjugate-gradient inner solvers.
Plain CG and Jacobi-preconditioned CG for symmetric positive definite (SPD)
systems, accessed only through a mat-vec callable — the matrix is never
required explicitly. These are the inner solvers of the active-set loop in
:mod:nncg.solver; their convergence is governed by the spectral condition
number kappa at the O(sqrt(kappa)) Krylov rate.
cg(matvec, rhs, tol=1e-08, maxit=100000, x0=None)
¶
Solve an SPD system by conjugate gradients.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
matvec
|
MatVec
|
The action |
required |
rhs
|
Vector
|
Right-hand side |
required |
tol
|
float
|
Relative residual stopping tolerance |
1e-08
|
maxit
|
int
|
Iteration cap; the current iterate is returned when it is hit. |
100000
|
x0
|
Vector | None
|
Optional warm start. The initial residual is |
None
|
Returns:
| Type | Description |
|---|---|
tuple[Vector, int]
|
The approximate solution and the number of iterations taken. |
Source code in src/nncg/krylov.py
pcg(matvec, rhs, dinv, tol=1e-08, maxit=100000)
¶
Solve an SPD system by Jacobi-preconditioned conjugate gradients.
The preconditioner is M^{-1} = diag(dinv); for operators that are a
well-conditioned core under a bad diagonal scaling, PCG runs at the core's
condition number regardless of the scaling.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
matvec
|
MatVec
|
The action |
required |
rhs
|
Vector
|
Right-hand side |
required |
dinv
|
Vector
|
Elementwise inverse of the operator's diagonal. |
required |
tol
|
float
|
Relative residual stopping tolerance. |
1e-08
|
maxit
|
int
|
Iteration cap; the current iterate is returned when it is hit. |
100000
|
Returns:
| Type | Description |
|---|---|
tuple[Vector, int]
|
The approximate solution and the number of iterations taken. |