Linear algebra utilities for portfolio optimization, part of the jebel-quant ecosystem.
pip install cvx-linalgThe ewm_covariance function requires the optional Polars dependency:
pip install 'cvx-linalg[ewm]'The entire public API is re-exported at the top level, so a flat import is all you ever need:
from cvx.linalg import (
a_norm,
cholesky,
cholesky_solve,
cov_to_corr,
inv,
inv_a_norm,
is_positive_definite,
lstsq,
pca,
rand_cov,
solve,
valid,
GramOperator,
bordered_solve,
)
from cvx.linalg.covariance.ewm_cov import ewm_covariance # requires the 'ewm' extra (polars)This is what sets cvx-linalg apart from numpy.linalg. Financial data arrives
with gaps: an asset that was not yet listed, a name that stopped trading, a
covariance estimate that never converged.
Hand such a matrix to np.linalg.solve and you get back nothing you can use.
LAPACK's pivoting smears the missing entry across the solution, and how far it
spreads depends on the BLAS your NumPy wheel was built against — the same call
returns [nan, nan, nan] under Accelerate on macOS and [nan, nan, 2.375] under
OpenBLAS on Linux. Neither is an answer you can act on.
Every function here instead restricts the computation to the valid sub-problem
and returns NaN only where the input was missing — the same result on every
platform:
import numpy as np
from cvx.linalg import solve, valid
# A covariance matrix in which the second asset has no usable data.
cov = np.array(
[
[4.0, 1.0, 0.0],
[1.0, np.nan, 1.0],
[0.0, 1.0, 9.0],
]
)
rhs = np.array([8.0, 1.0, 18.0])
mask, submatrix = valid(cov)
print("valid assets :", mask.tolist())
print("clean submatrix :", submatrix.tolist())
print("cvx.linalg.solve :", solve(cov, rhs).tolist())valid assets : [True, False, True]
clean submatrix : [[4.0, 0.0], [0.0, 9.0]]
cvx.linalg.solve : [2.0, nan, 2.0]
valid reports which rows and columns survive and hands back the clean
submatrix; solve uses that mask internally, solves the 2x2 system for the
assets that do have data, and marks the missing asset NaN rather than
destroying the other two answers.
Internally the package is organized into subpackages (which can also be
imported directly, e.g. from cvx.linalg.decomposition import qr):
| Subpackage | Contents |
|---|---|
cvx.linalg.core |
Type aliases, exceptions/warnings, condition-number helpers, matrix validation |
cvx.linalg.covariance |
Covariance utilities: PCA, random covariance, correlation conversion, EWM covariance |
cvx.linalg.decomposition |
Matrix decompositions: Cholesky, QR, SVD, eigenvalue routines, power iteration |
cvx.linalg.kkt |
Equality-constrained solves: bordered KKT systems, affine projections |
cvx.linalg.norm |
NaN-aware vector and matrix norms, A-norms and their inverses |
cvx.linalg.operators |
Symmetric linear operators (dense, Gram, factor, incremental) for active-set and Krylov solvers |
cvx.linalg.solve |
NaN-aware linear-system solvers: solve, lstsq, inv, det |
The one exception to the flat-import rule is ewm_covariance: it lives in
cvx.linalg.covariance.ewm_cov and is deliberately not re-exported because it
requires the optional polars dependency.
valid(matrix)— Return a boolean mask and valid submatrix by removing rows/columns with non-finite diagonal entriescond(matrix, p=None)— Condition number of a matrix (NaN-aware); accepts the samepnorm values asnumpy.linalg.condcheck_and_warn_condition(matrix, threshold)— EmitIllConditionedMatrixWarningwhen the condition number exceeds the thresholdwarn_ill_conditioned(cond_value, threshold)— EmitIllConditionedMatrixWarningfor an already-computed condition numberDEFAULT_COND_THRESHOLD— Default condition-number threshold (1e12) used by the ill-conditioning checks
Matrix— Type alias for a 2-Dnumpy.ndarraywithfloat64dtypeVector— Type alias for a 1-Dnumpy.ndarraywithfloat64dtype
The package ships a py.typed marker; all public signatures are precisely annotated and verified with ty in CI.
All exceptions and warnings live in core/exceptions.py:
DimensionMismatchError— Raised when vector length does not match matrix dimensionIllConditionedMatrixWarning— Emitted when the condition number exceeds a configurable thresholdInvalidComponentsError— Raised whenpcais asked for fewer than 1 or more components than the data supportsNegativeWarmupError— Raised when a negative warmup period is passed toewm_covarianceNonIntegerWarmupError— Raised when a non-integer warmup is passed toewm_covarianceNonSquareMatrixError— Raised when a square matrix is required but the input is not squareNotAMatrixError— Raised when a 2-D matrix is required but the input has different dimensionalitySingularMatrixError— Raised when a matrix is numerically singular
cov_to_corr(cov, min_var=1e-14)— Convert a covariance matrix to a correlation matrixewm_covariance(data, assets, index_col, window, is_halflife, warmup)— Exponentially weighted covariance matrices from a Polars DataFrame (requires theewmextra)pca(returns, n_components=10)— Principal Component Analysis via SVDrand_cov(n, seed=None)— Random positive semi-definite covariance matrix
cholesky(cov)— Upper triangular Cholesky factor R such that R.T @ R = covcholesky_solve(cov, rhs)— Solve cov @ x = rhs via Cholesky decomposition (falls back to LU for non-positive-definite matrices)is_positive_definite(matrix)— Return True if the matrix is symmetric positive-definiteeigh(matrix)— Eigenvalues/eigenvectors of the valid symmetric/Hermitian submatrix in ascending eigenvalue ordereigvalsh(matrix)— Eigenvalues-only convenience wrapper aroundeigheigvals(matrix)— Eigenvalues of a general square matrix (supports complex output for non-symmetric matrices)power_iteration(matrix, n_iter=1000, tol=1e-9, seed=None)— Estimate the dominant (largest-magnitude) eigenpair of a symmetric matrix via power iterationqr(matrix)— Reduced QR decomposition, matchingnp.linalg.qr(mode='reduced')svd(matrix)— Raw compact singular value decomposition vianp.linalg.svd(full_matrices=False)svd_k(matrix, k)— Exact truncated rank-kSVD (the best rank-kapproximation; leading triplets of the compact SVD)
norm(x, ord=None)— Norm of a vector or matrix, ignoring non-finite entries; supports allordvalues ofnp.linalg.norma_norm(vector, matrix=None)— Euclidean norm or NaN-aware matrix norminv_a_norm(vector, matrix=None)— Euclidean norm or inverse NaN-aware matrix norm
solve(matrix, rhs, cond_threshold=1e12)— Solve a linear system (vector or matrix rhs) restricted to valid rows/columns; NaN entries are returned for invalid positionslstsq(matrix, rhs, cond_threshold=1e12)— Solve a least-squares system with NaN-aware row filtering; returns(x, residuals, rank, sv)consistent withnumpy.linalg.lstsqinv(matrix, cond_threshold=1e12)— Invert a matrix restricted to valid rows/columns; NaN rows/columns are returned for invalid positionsdet(matrix, cond_threshold=1e12)— Determinant of a square matrix with NaN-aware submatrix handling; emitsIllConditionedMatrixWarningwhen near-singular
Active-set and Krylov solvers never need a symmetric matrix A as an explicit
array — they touch it through a handful of products: a full matrix-vector
product, sub-block products, and a direct solve against a principal ("free")
sub-block. The SymmetricOperator
protocol captures exactly that contract (including rcond_free for detecting
rank-deficient free blocks and restricted(free) for a pre-sliced free-block
view), and the backends implement it at very different cost:
SymmetricOperator— Protocol exposing a symmetric matrix throughmatvec,block_matvec,solve_free,apply_free,rcond_free,restricted, anddiagDenseOperator— Backend wrapping an explicit densen x nmatrixIncrementalDenseOperator—DenseOperatormaintaining the free-block inverse across single-index flips for active-set sweepsGramOperator— Matrix-free backend forA = M.T @ M, represented by its factorM; never forms the Gram matrixFactorOperator— Diagonal-plus-low-rank backendA = diag(d) + U @ Delta @ U.Twith Woodbury free-block solvesSumOperator— Weighted sum of symmetric operators (forward-only; feedapply_freeto a Krylov solver)
Building blocks for active-set and path-tracing solvers, built on top of the operator protocol:
bordered_solve(operator, free, c_free, rhs, d)— Range-space (Schur complement) solve of the bordered KKT system[[H_FF, C.T], [C, 0]] @ [x; nu] = [rhs; d], reaching the Hessian block only through aSymmetricOperatorAffineProjection(c, d)— Euclidean projection onto the affine set{x : C x = d}, caching the Gram matrixC C.Tfor repeated use
This package follows semantic versioning. The public API
is everything importable from cvx.linalg (plus cvx.linalg.covariance.ewm_cov):
- Breaking changes only occur in major releases.
- Deprecations are announced at least one minor release before removal and
emit a
DeprecationWarningin the meantime (currently: the two-argumentcholesky(cov, rhs)form — usecholesky_solve— slated for removal in 2.0). - Supported environments: Python 3.11–3.14, NumPy ≥ 2.0. The optional
ewmextra requires Polars ≥ 1.40.
Numerical conventions (NaN handling, condition-number warnings, dtype contract) are documented in Numerical Behavior.