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

1"""Builders that assemble cvx-linalg symmetric operators from CLA / LASSO inputs. 

2 

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""" 

11 

12from __future__ import annotations 

13 

14import numpy as np 

15from cvx.linalg import DenseOperator, FactorOperator, GramOperator, IncrementalDenseOperator 

16from numpy.typing import NDArray 

17 

18 

19def _symmetric_matrix(matrix: NDArray[np.float64]) -> NDArray[np.float64]: 

20 """Return *matrix* as a float array after checking it is square and symmetric. 

21 

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 

33 

34 

35def dense_covariance(matrix: NDArray[np.float64]) -> DenseOperator: 

36 """Build a :class:`~cvx.linalg.DenseOperator` from an explicit symmetric covariance. 

37 

38 Args: 

39 matrix: A symmetric ``(n, n)`` covariance matrix. 

40 

41 Returns: 

42 A dense symmetric operator wrapping *matrix*. 

43 """ 

44 return DenseOperator(_symmetric_matrix(matrix)) 

45 

46 

47def incremental_dense_covariance(matrix: NDArray[np.float64]) -> IncrementalDenseOperator: 

48 """Build an :class:`~cvx.linalg.IncrementalDenseOperator` (maintained free-block inverse). 

49 

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. 

52 

53 Args: 

54 matrix: A symmetric ``(n, n)`` covariance matrix. 

55 

56 Returns: 

57 A dense symmetric operator that maintains the free-block inverse. 

58 """ 

59 return IncrementalDenseOperator(_symmetric_matrix(matrix)) 

60 

61 

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*. 

64 

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. 

69 

70 Args: 

71 returns: The ``(T, n)`` matrix of observations (``T >= 2``). 

72 ridge: A non-negative diagonal loading added to the covariance. 

73 

74 Returns: 

75 A matrix-free Gram operator for the (ridged) sample covariance. 

76 

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) 

88 

89 

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``. 

96 

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. 

102 

103 Returns: 

104 A diagonal-plus-low-rank operator with Woodbury free-block solves. 

105 

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) 

120 

121 

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