cvx-linalg
Linear algebra utilities for portfolio optimization, part of the jebel-quant ecosystem.
Installation
pip install cvx-linalg
The ewm_covariance function requires the optional Polars dependency:
pip install 'cvx-linalg[ewm]'
The optional SciPy extra makes cholesky_solve use
triangular solves on the Cholesky factor and lets the condition-number check
screen with a cheap LAPACK estimate instead of a full SVD (see
Solvers):
pip install 'cvx-linalg[scipy]'
Usage
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)
Everything is NaN-aware
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())
valid assets : [True, False, True]
>>> print("clean submatrix :", submatrix.tolist())
clean submatrix : [[4.0, 0.0], [0.0, 9.0]]
>>> print("cvx.linalg.solve :", solve(cov, rhs).tolist())
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.
Core (cvx.linalg.core)
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 threshold;threshold=Noneskips the check (and its SVD) entirelywarn_ill_conditioned(cond_value, threshold)— EmitIllConditionedMatrixWarningfor an already-computed condition number;threshold=Nonenever warnsDEFAULT_COND_THRESHOLD— Default condition-number threshold (1e12) used by the ill-conditioning checks
Types
Matrix— Type alias for a 2-Dnumpy.ndarraywithfloat64dtypeVector— Type alias for a 1-Dnumpy.ndarraywithfloat64dtypeSupportsMatvec— Structural protocol for a matrix-free operator: anything exposing a dimensionnandmatvec(x) -> A @ x(everySymmetricOperatorconforms)
The package ships a py.typed marker; all public signatures are precisely annotated and verified with ty and mypy --strict in CI.
Exceptions & Warnings
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
Covariance (cvx.linalg.covariance)
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
Decompositions (cvx.linalg.decomposition)
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(operator, *, n=None, n_iter=1000, tol=1e-9, seed=None)— Estimate the dominant (largest-magnitude) eigenpair of a symmetric operator; accepts a dense array, anySymmetricOperator, or a callablev -> A @ vplusn, so it runs matrix-freeqr(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)
Norms (cvx.linalg.norm)
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, cond_threshold=1e12)— Euclidean norm or inverse NaN-aware matrix norm
Solvers (cvx.linalg.solve)
solve, inv, det (and inv_a_norm) warn when the 2-norm condition number
of the valid submatrix exceeds cond_threshold. With the scipy extra
installed, a symmetric positive-definite submatrix is first screened by a LAPACK
estimate from its Cholesky factor (about the cost of one more solve), and the
full SVD runs only when that estimate cannot rule out a warning. Otherwise the
check costs a full SVD on every call, often far more than the solve itself.
Pass cond_threshold=None to skip the check entirely; cond_threshold=np.inf
only silences the warning. lstsq accepts
None too, but gets its condition number from singular values it already has.
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
Operators (cvx.linalg.operators)
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(matrix)— Backend wrapping an explicit densen x nmatrixIncrementalDenseOperator(matrix)—DenseOperatormaintaining the free-block inverse across single-index flips for active-set sweepsGramOperator(factor, ridge=0.0)— Matrix-free backend forA = M.T @ M + ridge * I, represented by its factorM; never forms the Gram matrix.GramOperator.regularized(factor, alpha, root)builds the shrinkage target(1 - alpha) M.T M + alpha R.T Ras a stacked-factor Gram operatorFactorOperator(diagonal, loadings, inner)— Diagonal-plus-low-rank backendA = diag(d) + U @ Delta @ U.Twith Woodbury free-block solvesSumOperator(terms)— Weighted sum of symmetric operators (forward-only; feedapply_freeto a Krylov solver)
Constrained solves (cvx.linalg.kkt)
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
Stability policy
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; the optionalscipyextra requires SciPy ≥ 1.11.
Numerical conventions (NaN handling, condition-number warnings, dtype contract) are documented in Numerical Behavior.
Metadata
Release files for cvx-linalg 1.1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| cvx_linalg-1.1.2.tar.gz | 43.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| cvx_linalg-1.1.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 101.2 kB
Release files / cvx_linalg-1.1.2.tar.gz
| Download URL | cvx_linalg-1.1.2.tar.gz |
|---|---|
| Size | 43.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4c1497147af11986a34bfeb5e2972052730952e167679ef0aea6d7b630ba7c56
|
|
BLAKE2b-256 checksum How to use checksums |
99859315f75dfb1f901f8c224c5d9bdff455c5c4a3a7ae4c5847126e29ddd93f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.
Transparency logRelease files / cvx_linalg-1.1.2-py3-none-any.whl
| Download URL | cvx_linalg-1.1.2-py3-none-any.whl |
|---|---|
| Size | 57.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a852a5a73566cd92fca8d36c0230c19e9e14cde30c6e60097293aef446785b49
|
|
BLAKE2b-256 checksum How to use checksums |
141c9be411ebfce2a73c09644cdc459456ac876ea3c1599b0ebe000ba7ba07ca
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.
Transparency log