Coverage for src/cvx/linalg/operators/__init__.py: 100%
7 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-03 18:56 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-03 18:56 +0000
1"""Symmetric linear operators for active-set and Krylov solvers.
3Active-set and Krylov solvers never need a symmetric matrix ``A`` as an explicit
4array. They touch it through only a handful of products: a full matrix-vector
5product ``A x``, an arbitrary sub-block product ``A[rows, cols] v``, and a direct
6solve ``A[free, free]^{-1} rhs`` against a principal ("free") sub-block. This
7subpackage captures exactly that contract as :class:`SymmetricOperator` -- together
8with :meth:`SymmetricOperator.rcond_free`, a conditioning check an active-set
9solver uses to detect a rank-deficient free block -- and provides four backends
10that implement it at very different cost:
12* :class:`DenseOperator` wraps an explicit ``n x n`` matrix.
13* :class:`GramOperator` represents ``A = M.T @ M`` from a factor ``M`` and never
14 forms the ``n x n`` Gram matrix.
15* :class:`FactorOperator` represents a diagonal-plus-low-rank
16 ``A = diag(d) + U @ Delta @ U.T`` and inverts free blocks by the Woodbury
17 identity.
18* :class:`IncrementalDenseOperator` is a :class:`DenseOperator` that maintains the
19 free-block inverse across single-index changes for active-set sweeps.
21The protocol and the shared index/conditioning helpers live in :mod:`~.base`;
22each backend lives in its own module (:mod:`~.dense`, :mod:`~.gram`,
23:mod:`~.factor`).
25Two solving strategies compose with the same protocol. A direct solver consumes
26:meth:`SymmetricOperator.solve_free`; a matrix-free conjugate-gradient solver
27consumes :meth:`SymmetricOperator.apply_free` (the free-block forward product)
28and does its own iterating. Because a solver reaches ``A`` only through the
29protocol, switching backend requires no change to the solver.
31The two products a solver needs beyond the free-block solve are the free-block
32forward product (:meth:`SymmetricOperator.apply_free`, i.e. ``block_matvec`` with
33``rows == cols``) and a cross product ``A[bound, free] x_free`` -- the reduced
34gradient at the bound indices -- obtained from :meth:`block_matvec` on the
35disjoint index sets. ``block_matvec`` is defined for arbitrary ``rows`` and
36``cols``, so both regimes and everything between are covered.
38Example:
39 >>> import numpy as np
40 >>> from cvx.linalg import GramOperator
41 >>> rng = np.random.default_rng(0)
42 >>> M = rng.standard_normal((20, 5))
43 >>> op = GramOperator(M)
44 >>> A = M.T @ M
45 >>> np.allclose(op.matvec(np.ones(5)), A @ np.ones(5))
46 True
47 >>> free = np.array([0, 2, 4])
48 >>> rhs = np.array([1.0, 2.0, 3.0])
49 >>> x = op.solve_free(free, rhs)
50 >>> np.allclose(A[np.ix_(free, free)] @ x, rhs)
51 True
52"""
54from .base import SymmetricOperator as SymmetricOperator
55from .composite import SumOperator as SumOperator
56from .dense import DenseOperator as DenseOperator
57from .dense import IncrementalDenseOperator as IncrementalDenseOperator
58from .factor import FactorOperator as FactorOperator
59from .gram import GramOperator as GramOperator
61__all__ = [
62 "DenseOperator",
63 "FactorOperator",
64 "GramOperator",
65 "IncrementalDenseOperator",
66 "SumOperator",
67 "SymmetricOperator",
68]