Coverage for src/cvxcla/operators/_core.py: 100%
14 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-09-29 05:24 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-09-29 05:24 +0000
1"""Operator protocol alias and the parametric-path helpers built on cvx-linalg.
3The parametric active-set path tracer reaches its Hessian through the cvx-linalg
4symmetric-operator protocol (``matvec`` / ``block_matvec`` / ``solve_free`` /
5``rcond_free``). :data:`QuadraticForm` is that contract; :data:`CovarianceOperator`
6is a backward-compatible alias for the portfolio (covariance) setting. The
7concrete backends live in :mod:`cvx.linalg`; :mod:`cvxcla.operators.builders`
8assembles them from CLA / LASSO inputs.
10The generic linear algebra -- the bordered KKT (Schur complement) solve and the
11affine projection -- also lives in :mod:`cvx.linalg`. What remains here is the
12thin homotopy-specific glue: adapting the loop's boolean masks to the operators'
13integer-index API, and packing the constant / ``lambda``-slope pair of a
14parametric segment into the shared multi-RHS solve.
15"""
17from __future__ import annotations
19import numpy as np
20from cvx.linalg import SymmetricOperator
21from cvx.linalg import bordered_solve as _bordered_solve
22from numpy.typing import NDArray
24# The Hessian contract for a parametric active-set path. In the CLA it is the
25# covariance ``Sigma``; in a LASSO / LARS path it is the Gram matrix ``X.T @ X``.
26QuadraticForm = SymmetricOperator
27CovarianceOperator = SymmetricOperator
29# The singularity threshold for ``rcond_free``. A genuinely rank-deficient free
30# block has a reciprocal condition number at round-off level (~1e-16); a
31# well-posed or merely near-degenerate block sits many orders above it (>= ~1e-4
32# across the degeneracy sweep in experiments/degeneracy_boundary.py). The 1e-12
33# cut sits in the wide gap between the two and is the conventional
34# numerical-singularity scale.
35_RCOND_FLOOR = 1e-12 # pragma: no mutate
38def cross(operator: SymmetricOperator, free: NDArray[np.bool_], x: NDArray[np.float64]) -> NDArray[np.float64]:
39 """Free-to-blocked cross product ``H[free][:, ~free] @ x[~free]`` from a boolean mask.
41 Adapts the boolean-mask indexing the path tracers use to the integer-index
42 :meth:`~cvx.linalg.SymmetricOperator.block_matvec` of a symmetric operator.
44 Args:
45 operator: The symmetric operator (Hessian) backend.
46 free: Boolean mask of shape ``(n,)`` selecting the free coordinates.
47 x: Full-length vector of shape ``(n,)``; only ``x[~free]`` enters the product.
49 Returns:
50 Vector of shape ``(n_free,)``.
51 """
52 result: NDArray[np.float64] = operator.block_matvec(np.flatnonzero(free), np.flatnonzero(~free), x[~free])
53 return result
56def bordered_solve(
57 quad: SymmetricOperator,
58 free: NDArray[np.bool_],
59 c_free: NDArray[np.float64],
60 rhs_const: NDArray[np.float64],
61 rhs_slope: NDArray[np.float64],
62 d_const: NDArray[np.float64],
63 d_slope: NDArray[np.float64],
64) -> tuple[
65 NDArray[np.float64],
66 NDArray[np.float64],
67 NDArray[np.float64],
68 NDArray[np.float64],
69]:
70 """Solve the bordered KKT system for a parametric segment's constant and slope parts.
72 A thin adapter over :func:`cvx.linalg.bordered_solve`: it converts the loop's
73 boolean *free* mask to integer indices and packs the constant and ``lambda``-slope
74 right-hand sides as the two columns of one multi-RHS solve, so ``H_FF`` and the
75 Schur complement are factorised once. Returns
76 ``(x_const, x_slope, nu_const, nu_slope)`` (multipliers empty when there are no
77 constraint rows).
78 """
79 x, nu = _bordered_solve(
80 quad,
81 np.flatnonzero(free),
82 c_free,
83 np.column_stack([rhs_const, rhs_slope]),
84 np.column_stack([d_const, d_slope]),
85 )
86 return x[:, 0], x[:, 1], nu[:, 0], nu[:, 1]