Coverage for src/nncg/_equality.py: 100%
18 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-15 07:01 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-15 07:01 +0000
1"""Equality-augmented reduction: the Schur-complement saddle solve on a free set.
3Factored out of :meth:`nncg.solver.ActiveSetSolver.solve_eq` so the outer loop
4keeps only its orchestration. On a free set the saddle system for ``B x = c`` is
5solved by eliminating the multiplier ``lambda`` in R^p through the p-by-p Schur
6complement ``S = B_F A_F^{-1} B_F^T``: the ``p + 1`` right-hand sides share the
7operator ``A_F`` and are each one inner solve, then ``S lambda = c - B_F v0``
8fixes the multipliers in closed form.
9"""
11from __future__ import annotations
13from typing import TYPE_CHECKING
15import numpy as np
16from cvx.linalg import Matrix, SymmetricOperator, Vector, cholesky_solve
17from numpy.typing import NDArray
19if TYPE_CHECKING:
20 from .solver import InnerSolver
23def _saddle_solve(
24 inner: InnerSolver,
25 a: SymmetricOperator,
26 b: Vector,
27 b_eq: Matrix,
28 c_eq: Vector,
29 idx: NDArray[np.int_],
30 x0: Vector | None,
31) -> tuple[Vector, Vector, int]:
32 """Solve the equality-augmented saddle system on the free set ``idx``.
34 Runs ``p + 1`` inner solves through the shared free-block operator ``A_F``
35 (the ``v0`` column warm-started at ``x0``, the ``v1`` columns cold), forms the
36 SPD Schur complement ``S = B_F A_F^{-1} B_F^T`` and recovers the multipliers
37 from ``S lambda = c - B_F v0`` before back-substituting ``x_F = v0 + v1 lambda``.
39 Args:
40 inner: The inner solver driving each free-block solve.
41 a: The SPD operator ``A``.
42 b: The linear term ``b``.
43 b_eq: Equality matrix ``B`` of shape ``(p, n)``, full row rank on ``idx``.
44 c_eq: Equality right-hand side ``c`` of shape ``(p,)``.
45 idx: Integer positions of the free set ``F``.
46 x0: Warm inner guess for the ``v0`` column restricted to ``idx``, or ``None``.
48 Returns:
49 ``(x_F, lam, inner_iters)``: the free-block solution, the equality
50 multipliers, and the total inner iteration count across all columns.
51 """
52 p = b_eq.shape[0]
53 b_f = b_eq[:, idx]
54 v0, k0 = inner.solve(a, idx, b[idx], x0)
55 v1 = np.zeros((idx.size, p))
56 k_cols = 0
57 for j in range(p):
58 v1[:, j], kj = inner.solve(a, idx, b_f[j], None)
59 k_cols += kj
60 schur = b_f @ v1 # p-by-p Schur complement, SPD
61 lam = cholesky_solve(schur, c_eq - b_f @ v0)
62 xf = v0 + v1 @ lam # x_F = A_F^{-1}(b_F + B_F^T lambda)
63 return xf, lam, k0 + k_cols