Coverage for src/cvxcla/operators/builders.py: 100%
41 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-11 09:58 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-11 09:58 +0000
1"""Builders that assemble cvx-linalg symmetric operators from CLA / LASSO inputs.
3These replace the former operator *classes*. The numerics -- matrix-vector and
4block products, free-block solves (Cholesky, Woodbury, maintained inverse), and
5the reciprocal-condition check -- now live in :mod:`cvx.linalg`. Here we only
6build the right operator from the domain inputs: a covariance matrix, a
7returns / data matrix, or a factor model. The backward-compatible names
8``DenseCovariance`` / ``IncrementalDenseCovariance`` / ``GramCovariance`` /
9``FactorCovariance`` are kept as aliases of these builders.
10"""
12from __future__ import annotations
14import numpy as np
15from cvx.linalg import DenseOperator, FactorOperator, GramOperator, IncrementalDenseOperator
16from numpy.typing import NDArray
19def _symmetric_matrix(matrix: NDArray[np.float64]) -> NDArray[np.float64]:
20 """Return *matrix* as a float array after checking it is square and symmetric.
22 Raises:
23 ValueError: If *matrix* is not square or not symmetric to tolerance.
24 """
25 matrix = np.asarray(matrix, dtype=np.float64)
26 if matrix.ndim != 2 or matrix.shape[0] != matrix.shape[1]:
27 msg = f"Covariance must be a square matrix, got shape {matrix.shape}"
28 raise ValueError(msg)
29 if not np.allclose(matrix, matrix.T):
30 msg = "Covariance must be symmetric"
31 raise ValueError(msg)
32 return matrix
35def dense_covariance(matrix: NDArray[np.float64]) -> DenseOperator:
36 """Build a :class:`~cvx.linalg.DenseOperator` from an explicit symmetric covariance.
38 Args:
39 matrix: A symmetric ``(n, n)`` covariance matrix.
41 Returns:
42 A dense symmetric operator wrapping *matrix*.
43 """
44 return DenseOperator(_symmetric_matrix(matrix))
47def incremental_dense_covariance(matrix: NDArray[np.float64]) -> IncrementalDenseOperator:
48 """Build an :class:`~cvx.linalg.IncrementalDenseOperator` (maintained free-block inverse).
50 A drop-in alternative to :func:`dense_covariance` for a loop that changes its
51 free set one index at a time; see the operator's own caveats on numerics.
53 Args:
54 matrix: A symmetric ``(n, n)`` covariance matrix.
56 Returns:
57 A dense symmetric operator that maintains the free-block inverse.
58 """
59 return IncrementalDenseOperator(_symmetric_matrix(matrix))
62def gram_covariance(returns: NDArray[np.float64], ridge: float = 0.0) -> GramOperator:
63 """Build a :class:`~cvx.linalg.GramOperator` for the sample covariance of *returns*.
65 The sample covariance ``X_c.T X_c / (T - 1)`` (``X_c`` the column-centered
66 ``(T, n)`` data) is realised by absorbing the scale into the factor:
67 ``M = sqrt(1 / (T - 1)) * X_c``, so the operator represents
68 ``M.T M + ridge * I`` and never forms the ``n x n`` covariance.
70 Args:
71 returns: The ``(T, n)`` matrix of observations (``T >= 2``).
72 ridge: A non-negative diagonal loading added to the covariance.
74 Returns:
75 A matrix-free Gram operator for the (ridged) sample covariance.
77 Raises:
78 ValueError: If *returns* is not a ``(T, n)`` matrix with ``T >= 2``.
79 """
80 returns = np.asarray(returns, dtype=np.float64)
81 if returns.ndim != 2 or returns.shape[0] < 2:
82 msg = f"returns must be a (T, n) matrix with T >= 2 observations, got shape {returns.shape}"
83 raise ValueError(msg)
84 t = returns.shape[0]
85 centered = returns - returns.mean(axis=0, keepdims=True)
86 factor = centered / np.sqrt(t - 1.0)
87 return GramOperator(factor, ridge=ridge)
90def factor_covariance(
91 d: NDArray[np.float64],
92 u: NDArray[np.float64],
93 delta: NDArray[np.float64],
94) -> FactorOperator:
95 """Build a :class:`~cvx.linalg.FactorOperator` for ``Sigma = diag(d) + U Delta U.T``.
97 Args:
98 d: Positive idiosyncratic variances of shape ``(n,)``.
99 u: Factor loadings of shape ``(n, k)``.
100 delta: Factor covariance, either ``(k,)`` eigenvalues (a diagonal ``Delta``)
101 or a symmetric positive-definite ``(k, k)`` matrix.
103 Returns:
104 A diagonal-plus-low-rank operator with Woodbury free-block solves.
106 Raises:
107 ValueError: If *delta* is neither a ``(k,)`` vector nor a ``(k, k)`` matrix.
108 """
109 d = np.asarray(d, dtype=np.float64)
110 u = np.asarray(u, dtype=np.float64)
111 delta = np.asarray(delta, dtype=np.float64)
112 if delta.ndim == 1:
113 inner = np.diag(delta)
114 elif delta.ndim == 2:
115 inner = delta
116 else:
117 msg = f"delta must be a (k,) vector or (k, k) matrix, got ndim {delta.ndim}"
118 raise ValueError(msg)
119 return FactorOperator(d, u, inner)
122# Backward-compatible names: the operator *classes* are gone, but the familiar
123# constructor-style names remain as builders returning cvx-linalg operators.
124DenseCovariance = dense_covariance
125IncrementalDenseCovariance = incremental_dense_covariance
126GramCovariance = gram_covariance
127FactorCovariance = factor_covariance