Skip to content

feat: MPRGP solver + active-set comparison notebook - #63

Merged
tschm merged 4 commits into
mainfrom
MPRGP
Jul 20, 2026
Merged

feat: MPRGP solver + active-set comparison notebook#63
tschm merged 4 commits into
mainfrom
MPRGP

Conversation

@tschm

@tschm tschm commented Jul 20, 2026

Copy link
Copy Markdown
Member

Summary

Adds MPRGP (Modified Proportioning with Reduced Gradient Projections, Dostál & Schöberl 2005) as a second solver for the repo's core problem min_{x≥0} ½xᵀAx − bᵀx, plus a marimo notebook comparing it head-to-head with the existing active-set loop.

MPRGP is a matrix-free, factorisation-free projection method — a complement to ActiveSetSolver, not a replacement. It stays feasible throughout and interleaves three moves under the proportioning test ‖β‖² ≤ γ²·φ̃ᵀφ:

  • CG step — minimise within the current face while it stays feasible;
  • expansion step — walk to the nearest bound + one fixed-step projected gradient (adds constraints);
  • proportioning step — release constraints along the chopped gradient.

The fixed step ᾱ ∈ (0, 2/‖A‖] is the only tuning; ‖A‖ is estimated matrix-free by power iteration (safe default 1/‖A‖, since power iteration approaches λ_max from below). A enters only via SymmetricOperator.matvec, so the n×n matrix is never formed.

What's in it

  • src/nncg/mprgp.pyMPRGP solver + MPRGPConfig/MPRGPResult and pure gradient primitives (free / chopped / reduced-free gradient, max feasible step). 100% covered.
  • src/nncg/api.pysolve_nnqp_mprgp one-call wrapper mirroring solve_nnqp.
  • Exports from the package __init__.
  • tests/test_nncg/test_mprgp.py — 25 tests (recovery across κ/seeds, matrix-free Gram, agreement with ActiveSetSolver, warm start + projection, max_iter contract, three-move bookkeeping, config validation, adversarial termination, gradient primitives).
  • book/marimo/notebooks/04_mprgp_vs_active_set.py — comparison notebook (agreement, live slider comparison, conditioning sweep, move breakdown, accuracy-vs-tolerance, guidance), registered in the mkdocs nav.
  • CLAUDE.md — documented the new module.

Scope

  • Bound constraints only. The equality-augmented Bx = c variant needs an augmented-Lagrangian outer wrap (SMALBE/SMALSE) and is out of scope; use ActiveSetSolver.solve_eq for equalities.
  • First-order accuracy. Unlike the active-set loop (exact free-block solves), MPRGP's iterate error scales with κ(A). Tests assert this honestly: kkt_violation/‖b‖ ≈ tol at all conditioning, and reaching 1e-6 in x at κ=1e5 needs tol≈1e-11.

Is it faster?

On dense SPD problems, no — the notebook's conditioning sweep shows the active-set loop is as fast or faster, its lead widening with κ (MPRGP takes √κ-growing full-size steps vs a few reduced-block outer steps). MPRGP's edge is elsewhere: huge sparse/matrix-free operators, combinatorially hard active-set searches, and problems wanting a proven convergence-rate bound.

Testing

  • make test — 127 passed, 100% coverage.
  • make fmt / make typecheck (ty + mypy --strict) / make deptry — all clean.
  • Notebook validated headless (uv run) and via marimo export html --sandbox.

🤖 Generated with Claude Code

tschm and others added 2 commits July 20, 2026 10:46
Implement Dostál & Schöberl's MPRGP (Modified Proportioning with Reduced
Gradient Projections) as a standalone, matrix-free, factorisation-free solver
for the same min_{x>=0} 1/2 x'Ax - b'x the active-set loop targets.

- src/nncg/mprgp.py: MPRGP solver with MPRGPConfig/MPRGPResult and the pure
  gradient primitives (free/chopped/reduced-free gradient, max feasible step).
  Interleaves conjugate-gradient, expansion and proportioning steps under the
  proportioning test; A enters only via SymmetricOperator.matvec, and ||A|| for
  the fixed step bound alpha_bar in (0, 2/||A||] is estimated matrix-free by
  power iteration (default 1/||A||, safe against the underestimate).
- src/nncg/api.py: solve_nnqp_mprgp one-call wrapper mirroring solve_nnqp.
- Export MPRGP, MPRGPConfig, MPRGPResult, solve_nnqp_mprgp from the package.
- tests/test_nncg/test_mprgp.py: 25 tests (recovery across kappa/seeds,
  matrix-free Gram, agreement with ActiveSetSolver, warm start + projection,
  max_iter contract, three-move bookkeeping, config validation, adversarial
  termination, gradient primitives). 100% coverage on the new module.

Bound constraints only; the equality-augmented Bx=c variant needs a SMALBE/
SMALSE outer wrap and is out of scope here (use ActiveSetSolver.solve_eq).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A marimo notebook (04_mprgp_vs_active_set) that puts solve_nnqp and
solve_nnqp_mprgp head to head on the shared planted-optimum problems:

- confirms both reach the same certified minimiser;
- an interactive live comparison (n / kappa / support / seed sliders) timing
  both at matched accuracy;
- a conditioning sweep (wall-clock and matrix-vector products vs kappa) — the
  plot that answers "is MPRGP faster?": on dense SPD problems the active-set
  loop is as fast or faster, its lead widening with kappa;
- MPRGP's CG/expansion/proportioning move breakdown across kappa;
- MPRGP's first-order accuracy-vs-tolerance behaviour;
- guidance on when to reach for each solver.

Comparisons precompute alpha_bar (so MPRGP is not re-charged for its ||A||
estimate) and match accuracy explicitly, since MPRGP is first-order while the
active-set loop returns near-exact iterates. Registered in the mkdocs nav.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 20, 2026 07:34

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 adds a second bound-constrained solver to nncg: MPRGP (a matrix-free projection method) alongside the existing primal-dual active-set loop, plus tests and a documentation notebook comparing the two approaches.

Changes:

  • Introduces src/nncg/mprgp.py with MPRGP, MPRGPConfig, MPRGPResult, and supporting gradient/step primitives.
  • Adds the convenience API entry point solve_nnqp_mprgp, and exports the new solver from nncg.__init__.
  • Adds a comprehensive test suite for MPRGP and a marimo notebook (plus mkdocs nav entry) comparing MPRGP vs the active-set solver.

Reviewed changes

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

Show a summary per file
File Description
src/nncg/mprgp.py New MPRGP solver implementation, config/result types, and core iteration logic.
src/nncg/api.py Adds solve_nnqp_mprgp one-call wrapper and updates module docstring.
src/nncg/__init__.py Re-exports the new solver types and wrapper function.
tests/test_nncg/test_mprgp.py New tests covering solver behavior, contracts, and primitives.
book/marimo/notebooks/04_mprgp_vs_active_set.py New notebook comparing solvers and discussing tradeoffs.
mkdocs.yml Registers the new notebook in the docs navigation.
CLAUDE.md Documents the new module in the project layout overview.

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

Comment thread src/nncg/mprgp.py
Comment on lines +273 to +282
x = np.zeros_like(b) if x0 is None else np.maximum(np.asarray(x0, dtype=np.float64), 0.0)
g = matvec(x) - b # gradient A x - b
products = 1
p = _free_gradient(x, g) # initial conjugate direction

stop = tol * (float(np.linalg.norm(b)) or 1.0)
cg_steps = expansion_steps = proportioning_steps = iterations = 0
converged = False

while iterations < max_iter:
Comment thread src/nncg/mprgp.py
Comment on lines +155 to +159
def __post_init__(self) -> None:
"""Validate that the proportioning constant is strictly positive."""
if self.gamma <= 0.0:
msg = f"gamma must be strictly positive; got {self.gamma:.2e}"
raise ValueError(msg)
tschm and others added 2 commits July 20, 2026 16:49
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@tschm
tschm merged commit f31cd42 into main Jul 20, 2026
54 checks passed
@tschm
tschm deleted the MPRGP branch July 20, 2026 12:57
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.

2 participants