Skip to content

docs: state the stability policy, and take the free complexity win - #23

Merged
tschm merged 1 commit into
mainfrom
quality_findings_21_22
Aug 7, 2026
Merged

docs: state the stability policy, and take the free complexity win#23
tschm merged 1 commit into
mainfrom
quality_findings_21_22

Conversation

@tschm

@tschm tschm commented Aug 7, 2026

Copy link
Copy Markdown
Member

Closes #21. Addresses #22 — see the note at the end on how that one should
close.

#21 — a stated stability policy

The surface has been stable since 0.1.x and the README promises a drop-in
replacement, but 0.x carries no semver obligation, so a reader could not tell
whether that stability was a commitment or an accident.

A Stability section now says which it is:

Covered Not covered
the solve_qp signature — names, order, defaults anything under _solve / _qr reached directly
Solution field names and order, so 6-way unpacking keeps working the internal Q/R sign conventions
the two ValueError strings reproduced verbatim from the reference whether a problem takes the unit-column fast path
the input conventions (linear term subtracted, C column-wise, >=) bit-exact results, and whether a non-finite G raises or propagates NaNs

Shipping 1.0.0 was the other option the issue offered. That is a release
decision rather than a documentation one, so it is left to you — the section
closes the finding either way, and would need only a wording change if 1.0
follows.

#22 — measured, then took the free half

The complexity was measured rather than argued about, by deleting each
candidate from a scratch copy and re-running radon:

Variant solve_qp
before D (23)
extract the argument defaulting C (19)
…and stub out the inner loop as well A (4)

That third row is the finding: the inner loop carries 15 of the 23. It is
therefore the only route to a B — and it is also the hot loop, so lifting it out
means threading xv, uv, obj, iact, nact, J, R, u, slack, iter_partial in and
returning five, or hiding them behind an object whose attribute lookups land in
the innermost iteration. The README's own benchmarks show per-iteration dispatch
dominating below n ≈ 160, which is exactly where this package is already
slower than the C reference.

The argument defaulting was the opposite case — separable, runs once per call,
no hot-path cost. It moves to _default_constraints, which takes solve_qp
from D (23) to C (19) and the package average from 4.47 to 4.25.

The "free at runtime" claim was verified, not assumed. There is no
benchmarks/ folder, so make benchmark is a no-op — timed directly instead,
best-of-five on box-constrained problems:

n before after
10 0.0236 ms 0.0234 ms
25 0.0290 ms 0.0288 ms
50 0.0631 ms 0.0629 ms

Within noise, in the direction of faster. (Not comparable to the README's table
— different problem setup — but both columns ran the same script minutes apart.)

How #22 should close

solve_qp is C (19), not B, so the issue's first acceptance criterion is not
met and this PR does not auto-close it. Its second criterion is
wontfix with the decision recorded, which is now the case: the comment
above solve_qp carries the measurement, the reason the inner loop is the wrong
thing to extract, and the trigger for revisiting (the function gaining a new
responsibility — a second pivoting rule, an alternative factorisation).

Also

Corrects the layout table, which still described _qr.py as "Givens QR
insert/delete" after insertion moved to a Householder reflection.

Verification

Gate Result
make fmt PASS — 21 hooks
make typecheck PASS — ty + mypy --strict
make test PASS — 982 passed, 100% statement and branch
make rhiza-test PASS — 39 passed, 1 skipped
check_test_layout.py PASS
radon cc src -a A (4.25), was A (4.47)

The extraction is behaviour-preserving: same 982 tests, still 100% branch
coverage, including the C-without-b and b-without-C error paths that
moved into _default_constraints.

Closes the two remaining quality findings.

#21 -- the API has been stable since 0.1.x and the README promises a drop-in
replacement, but 0.x carries no semver obligation, so a reader could not tell
whether that stability was a commitment or an accident. A Stability section now
splits it explicitly: the solve_qp signature, the Solution field order and the
two verbatim ValueError strings are covered; the underscored modules, the
internal Q/R sign conventions, whether a problem takes the unit fast path, and
bit-exact results are not. Shipping 1.0.0 remains the alternative and is a
release decision, not a documentation one.

#22 -- measured where the complexity actually lives by deleting each candidate
from a scratch copy and re-running radon. The inner loop carries 15 of the 23:
stubbing it out drops solve_qp to A (4), which makes it the only route to a B
and also the one place indirection is not free, since it is the hot loop. The
argument defaulting was the opposite case -- separable, runs once per call, no
hot-path cost -- so it moves to _default_constraints, taking solve_qp from
D (23) to C (19) and the package average from 4.47 to 4.25.

Verified the extraction is free rather than assuming it. There is no benchmarks
folder, so `make benchmark` is a no-op; timed directly instead, best-of-five on
box-constrained problems: n=10 0.0236 -> 0.0234 ms, n=25 0.0290 -> 0.0288,
n=50 0.0631 -> 0.0629. Within noise in the direction of faster.

Also corrects the layout table, which still described _qr.py as "Givens QR
insert/delete" after insertion moved to a Householder reflection.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 7, 2026 08:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR documents the project’s pre-1.0 stability commitments for the public solve_qp surface (closing #21), and reduces solve_qp’s measured cyclomatic complexity by extracting argument defaulting into a helper without impacting runtime behavior (partial progress on #22). It also corrects the README’s layout description for _qr.py.

Changes:

  • Add a README Stability section that explicitly states what API/behavior is and isn’t covered by compatibility guarantees while 0.x.
  • Extract solve_qp’s C/b defaulting and coercion into _default_constraints, reducing solve_qp’s complexity from D (23) to C (19).
  • Update the README layout table to reflect Householder insertion + Givens deletion in _qr.py.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
src/cvx/quadprog/_solve.py Refactors constraint defaulting into _default_constraints and updates the recorded complexity rationale comment.
README.md Adds a Stability policy section and fixes the _qr.py layout description.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@tschm
tschm merged commit 98d26be into main Aug 7, 2026
60 checks passed
@tschm
tschm deleted the quality_findings_21_22 branch August 7, 2026 08:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

State the pre-1.0 stability policy for the solve_qp surface

2 participants