Non-negative conjugate gradients: bound-constrained SPD quadratics by a guarded active-set loop
Project description
📋 Overview
nncg solves the strictly convex non-negative quadratic program
$$\min_{x \geq 0}\ \tfrac{1}{2} x^\top A x - b^\top x, \qquad A \succ 0,$$
and its equality-augmented variant with a general linear system $Bx = c$, by wrapping matrix-free conjugate gradients in a primal-dual active-set loop. The working-set toggles are the principal pivots of the linear complementarity problem $\mathrm{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.
This is the reference implementation of the paper Non-Negative Conjugate Gradients (Schmelzer & Stoll), developed in Jebel-Quant/mean_variance_solvers. The paper's numerical study doubles as this package's test suite: planted-optimum recovery across condition numbers, the equality-augmented solve for $p \in {1, 3, 8}$, CG-vs-exact free-set trajectory agreement (the inexactness lemma), warm-started parameter sweeps, and the adversarial anti-correlated family on which the unguarded batch path provably cycles and the fallback terminates.
The quadratic term enters as a cvx.linalg.SymmetricOperator: wrap an
explicit SPD array in DenseOperator. When $A = M^\top M$ is a Gram matrix,
pass GramOperator(M, ridge) and the inner solves need only products with
$M$ — the $n \times n$ matrix is never formed and working memory is $O(n)$.
📦 Installation
pip install nncg
🚀 Quickstart
import numpy as np
from cvx.linalg import DenseOperator, GramOperator
from nncg import kkt_violation, solve_nnqp, solve_nnqp_eq
# a random SPD problem with condition number 1e4
rng = np.random.default_rng(0)
Q, _ = np.linalg.qr(rng.standard_normal((200, 200)))
A = (Q * np.geomspace(1.0, 1e4, 200)) @ Q.T
b = rng.standard_normal(200)
op = DenseOperator(A) # the solvers take a SymmetricOperator
res = solve_nnqp(op, b)
assert res.converged # stopped on the KKT certificate
assert kkt_violation(op, b, res.x) < 1e-6 # zero certifies the global minimiser
# equality-augmented: minimise subject to x >= 0 and B x = c
B = np.ones((1, 200)) # p = 1: the budget 1'x = 1
res_eq = solve_nnqp_eq(op, b, B, np.array([1.0]))
assert res_eq.lam.shape == (1,) # multiplier, via a p-by-p Schur solve
# warm-start a parametric sweep: support-stable steps take ONE outer step
res2 = solve_nnqp(op, b + 1e-4, warm=(res.free, res.x))
# Gram-structured: A = M'M + I only through products with M — never formed
M = np.random.default_rng(1).standard_normal((50, 200))
res_g = solve_nnqp(GramOperator(M, ridge=1.0), M.T @ np.ones(50))
assert res_g.converged
🔬 The algorithm in one paragraph
Fix a working set of free variables and solve the unconstrained reduced SPD
system by CG (matrix-free, $O(\sqrt{\kappa})$ Krylov rate). Push any free
variable that returns negative to its bound (primal step); release any bound
variable whose reduced gradient is negative (dual step); repeat. Batch
exchanges are fast but can cycle; a patience counter falls back to Murty's
least-index single pivot, which cannot — hence finite termination without any
non-degeneracy hypothesis, and the fallback is provably necessary: on
anti-correlated designs (the make_adversarial family in the test suite's
tests/problems.py) the unguarded batch path revisits a previously seen
working set and loops forever.
📖 Citation
If you use this package in academic work, please cite the paper:
@techreport{schmelzer2026nncg,
title = {Non-Negative Conjugate Gradients},
author = {Schmelzer, Thomas and Stoll, Martin},
year = {2026},
institution = {Jebel Quant Research and TU Chemnitz},
url = {https://github.com/Jebel-Quant/mean_variance_solvers},
}
⚖️ License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file nncg-0.2.0.tar.gz.
File metadata
- Download URL: nncg-0.2.0.tar.gz
- Upload date:
- Size: 170.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f0287890d197cfd22133e47cae607711d0f398b79588d087b62d57eeb46b4333
|
|
| MD5 |
bbd3b272ea0d053b410f95a2a105b475
|
|
| BLAKE2b-256 |
8d6c2835c22bb3fe486ef8eb24ad17724be165e1f058e99e246657e57a423202
|
Provenance
The following attestation bundles were made for nncg-0.2.0.tar.gz:
Publisher:
rhiza_release.yml on Jebel-Quant/nncg
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
nncg-0.2.0.tar.gz -
Subject digest:
f0287890d197cfd22133e47cae607711d0f398b79588d087b62d57eeb46b4333 - Sigstore transparency entry: 2057726678
- Sigstore integration time:
-
Permalink:
Jebel-Quant/nncg@5d3d022e430da28f62f962e1325e7d483723269e -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/Jebel-Quant
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
rhiza_release.yml@5d3d022e430da28f62f962e1325e7d483723269e -
Trigger Event:
push
-
Statement type:
File details
Details for the file nncg-0.2.0-py3-none-any.whl.
File metadata
- Download URL: nncg-0.2.0-py3-none-any.whl
- Upload date:
- Size: 11.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
81a0f45d8f1b7addb1f58cdcb04f60edb68f93cb050d607ecb270b9203d570ea
|
|
| MD5 |
ad111c34f5ea8293e1c7bc9e7454046b
|
|
| BLAKE2b-256 |
1aeea897a453122a7d59aae50c53b28f76e19c8d3f6b369fc02eafb13054e623
|
Provenance
The following attestation bundles were made for nncg-0.2.0-py3-none-any.whl:
Publisher:
rhiza_release.yml on Jebel-Quant/nncg
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
nncg-0.2.0-py3-none-any.whl -
Subject digest:
81a0f45d8f1b7addb1f58cdcb04f60edb68f93cb050d607ecb270b9203d570ea - Sigstore transparency entry: 2057727202
- Sigstore integration time:
-
Permalink:
Jebel-Quant/nncg@5d3d022e430da28f62f962e1325e7d483723269e -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/Jebel-Quant
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
rhiza_release.yml@5d3d022e430da28f62f962e1325e7d483723269e -
Trigger Event:
push
-
Statement type: