This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
cvx-quadprog is a pure NumPy/SciPy implementation of the Goldfarb/Idnani dual
active-set method for strictly convex QPs: minimise ½ xᵀGx − aᵀx subject to
Cᵀx ≥ b, with the leading meq constraints held as equalities. It is a
drop-in, MIT-licensed replacement for the GPL-2.0 quadprog package (which wraps
C descended from Turlach's Fortran). No compiler, no build step.
Three conventions inherited from the original and easy to trip over: the linear
term is subtracted, C is column-wise (n × m, one column per
constraint), and equalities are the leading meq columns — they cannot be
interleaved.
The Makefile is a thin shim over uvx rhiza-task@<pin>; make <anything> is
routed to the CLI, and RHIZA_TASK in the Makefile is the whole version
contract. make help lists every task.
make install # create the venv and sync dependencies (bootstraps uv if missing)
make doctor # check local prerequisites
make all # every gate CI runs: fmt deps test docs-coverage security license typecheck rhiza-test
make test # full suite, gated at 100% coverage of statements AND branches
make fmt # pre-commit hooks + ruff
make typecheck # ty and mypy, both --strict (typechecker = "both" in pyproject.toml)
make hypothesis-test # property-based tests only
make benchmark # performance benchmarks
make rhiza-test # repository conformance checks (pytest-rhiza), incl. README code-block executionSingle test or single file — go through uv run directly, not make:
uv run pytest tests/test_qr.py -v
uv run pytest tests/test_specification.py::test_name -v
uv run pytest -k "sweep and not stress"
uv run pytest -m stress # markers: stress, property, kaleido
uv run pytest -o log_cli=true --log-cli-level=DEBUG # live logs (off by default)pytest.ini (template-owned) is authoritative over [tool.pytest.ini_options];
there is deliberately no such table in pyproject.toml. It sets a 60s timeout
and turns one specific pytest deprecation into an error.
Versions live in [tool.bumpversion] and are rewritten across pyproject.toml,
uv.lock and CITATION.cff together — never edit one by hand. CHANGELOG.md is
generated by git-cliff from Conventional Commit subjects; do not hand-edit it.
src/cvx/quadprog/ — public surface is exactly three names (solve_qp,
Solution, Sweep), everything else is underscore-private.
_base.py—Solution,_WarmEntry,VSMALL. Depends on nothing, so the fast path can build aSolutionwithout importing the solver (which would cycle)._setup.py— validation and the Cholesky/triangular-inverse ofGthat the iteration is expressed in terms of._solve.py—solve_qp, plus the outer loop: pick the entering constraint, decide infeasibility._dispatchtries the fast path first when asked, then falls back to_solve_with_factors, which is also whatSweepcalls (it returns the factorisation(J, R)alongside the solution, and accepts awarmstate)._steps.py— one pass of the inner loop: the ratio test, full-vs-partial step, which constraint leaves._qr.py— orthogonal updates of the active-normals factorisation. Insertion is a single Householder reflection, deletion a Givens chase.Ris stored packed by column ((i, j) → j*(j+1)/2 + i) so the leadingnacttriangle is contiguous and BLAStpsvreads it in place — 7.5 µs against 77 µs for the dense strided equivalent atn = 700, once per iteration._structure.py— detects single-nonzero constraint columns (bounds), so those become indexing rather than reductions, and picks dense vs CSR for the slack product by size and density._pdas.py— thefast=Trueprimal-dual active-set path: guess the whole set, one dense KKT solve, repair from the signs. Not globally convergent — every candidate must pass_certified(sufficient here because the problem is strictly convex) or it is discarded and the exact walk runs._sweep.py—Sweepkeeps the factorisation across a family of QPs differing only ina; verifies the cached active set against KKT, repairs it when stale._threads.py— opt-inblas_threads=cap viathreadpoolctl, plus stdlib+NumPy-only probes of which BLAS is linked and how many threads it will start.
Two invariants worth knowing before touching the numerics:
fast=Truechanges two reported fields, which is why it is off by default:iterationscounts a different algorithm's working-set edits, andiactcomes out index-ordered rather than insertion-ordered.x,f,xuandlagrangianare unaffected. It also declines below 12 variables and wheneverfactorizedis set.- Infeasibility is only concluded above the rounding floor. The Householder reduction makes a degenerate iterate land ~8·eps outside a constraint where the reference leaves it ~4.68·eps inside; such a constraint is set aside rather than taken as proof of infeasibility. The margin is deliberately loose.
- Never copy from the GPL-2.0 reference. Solver code must be your own work
from the 1983 paper — not from the C, the R package, or Turlach's Fortran.
PROVENANCE.mdrecords what is deliberately shared (the API signature, theValueErrormessage strings verbatim, the packedRlayout) and why each is functional rather than expressive. The GPLquadprogis a dev-only dependency, imported solely bytests/test_against_c.pybehindpytest.importorskip. - Tests derive expectations independently.
tests/test_specification.pyuses closed forms, direct KKT saddle-point solves, or KKT certificates — which for a strictly convex QP prove optimality. A test that only checks agreement with the C reference measures the oracle.test_against_c.pyis for differential comparison, not for specifications, and it verifies KKT rather than an identical dual where the dual is non-unique (duplicated or dependent constraints). - Performance claims in
README.mdanddocs/paper/are measured, not illustrative — the n≈135 crossover, 11× at n=1600, the 10.2× packed solve. If a change moves any of them, re-runbenchmarks/ref_probe.pyand update both places; if it does not, say so in the PR. A timing without its BLAS and thread count is not reproducible. - Rhiza-managed files are synced, not owned. Everything under
files:in.rhiza/template.lock— most of.github/,ruff.toml,pytest.ini,.pre-commit-config.yaml,docs/index.md,tests/test_rhiza_packaging.py— is overwritten at the next/rhiza:update. This is why per-module# ruff: noqa: N803, N806comments sit in the source files instead of a[lint.per-file-ignores]block inruff.toml.README.md,CONTRIBUTING.md,PROVENANCE.md,CITATION.cff, and everything undersrc/,tests/anddocs/paper/are repo-owned. - Docstrings and docs are gated.
interrogateruns atfail-under = 100(make docs-coverage), andtests/test_docs.pyasserts thatcvx.quadprog.__all__and the::: cvx.quadprog.Xblocks indocs/api.mdare the same set in both directions. Adding an export means adding an API-reference block and a summary-table row.
- Uppercase
G,C,R,J,CA,Ymatch Goldfarb & Idnani (1983) and the reference signature; pep8-naming is waived per-file for exactly this reason. ValueErrormessage strings are reproduced verbatim from the reference, soTRY003is waived alongsideN803/N806.- Inputs are never mutated (the C routine overwrites
Ganda). - Line length 120. Tests are organised by behaviour, not mirroring module
names — the rhiza test-layout check is switched off in
[tool.check_test_layout]with the reasoning recorded there; per-module reach is guaranteed by the 100% statement-and-branch coverage gate instead. - BLAS wrappers (
dtpsv,dtrtri) are imported by name rather than throughget_blas_funcs: everything is float64 by the time it arrives, and naming them gives a sharper static type. - Branch off
main(protected, no direct pushes). Conventional Commits are enforced — the subject becomes the changelog entry verbatim. - Long explanatory comments in
pyproject.toml,_qr.py,_structure.pyand_threads.pyrecord why a constant or a layout is what it is, usually with the measurement behind it. Preserve that when editing nearby; if you change a threshold, change the recorded number too.