Metadata-Version: 2.5
Name: cvx-linalg
Version: 1.1.1
Summary: Linear algebra utilities for portfolio optimization
Project-URL: Homepage, https://github.com/Jebel-Quant/linalg
Project-URL: Repository, https://github.com/Jebel-Quant/linalg
Author-email: Thomas Schmelzer <thomas.schmelzer@gmail.com>
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Scientific/Engineering :: Mathematics
Requires-Python: >=3.11
Requires-Dist: numpy>=2.0.0
Provides-Extra: ewm
Requires-Dist: polars>=1.40.0; extra == 'ewm'
Provides-Extra: scipy
Requires-Dist: scipy>=1.11.0; extra == 'scipy'
Description-Content-Type: text/markdown

# cvx-linalg

[![PyPI version](https://badge.fury.io/py/cvx-linalg.svg)](https://badge.fury.io/py/cvx-linalg)
[![Python](https://img.shields.io/badge/python-3.11%2B-blue)](https://pypi.org/project/cvx-linalg/)
[![Downloads](https://static.pepy.tech/personalized-badge/cvx-linalg?period=month&units=international_system&left_color=black&right_color=orange&left_text=PyPI%20downloads%20per%20month)](https://pepy.tech/project/cvx-linalg)
[![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/Jebel-Quant/linalg/blob/main/LICENSE)
[![CodeFactor](https://www.codefactor.io/repository/github/jebel-quant/linalg/badge)](https://www.codefactor.io/repository/github/jebel-quant/linalg)
[![Rhiza](https://img.shields.io/badge/dynamic/yaml?url=https%3A%2F%2Fraw.githubusercontent.com%2FJebel-Quant%2Flinalg%2Fmain%2F.rhiza%2Ftemplate.yml&query=%24.ref&label=rhiza)](https://github.com/jebel-quant/rhiza)
[![Coverage](https://jebel-quant.github.io/linalg/coverage-badge.svg)](https://jebel-quant.github.io/linalg/reports/html-coverage/)

Linear algebra utilities for portfolio optimization, part of the [jebel-quant](https://github.com/jebel-quant) ecosystem.

## Installation

```bash
pip install cvx-linalg
```

The `ewm_covariance` function requires the optional [Polars](https://pola.rs) dependency:

```bash
pip install 'cvx-linalg[ewm]'
```

## Usage

The entire public API is re-exported at the top level, so a flat import is all
you ever need:

```pycon
>>> 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:

```pycon
>>> 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`](https://github.com/Jebel-Quant/linalg/tree/main/src/cvx/linalg/core) | Type aliases, exceptions/warnings, condition-number helpers, matrix validation |
| [`cvx.linalg.covariance`](https://github.com/Jebel-Quant/linalg/tree/main/src/cvx/linalg/covariance) | Covariance utilities: PCA, random covariance, correlation conversion, EWM covariance |
| [`cvx.linalg.decomposition`](https://github.com/Jebel-Quant/linalg/tree/main/src/cvx/linalg/decomposition) | Matrix decompositions: Cholesky, QR, SVD, eigenvalue routines, power iteration |
| [`cvx.linalg.kkt`](https://github.com/Jebel-Quant/linalg/tree/main/src/cvx/linalg/kkt) | Equality-constrained solves: bordered KKT systems, affine projections |
| [`cvx.linalg.norm`](https://github.com/Jebel-Quant/linalg/tree/main/src/cvx/linalg/norm) | NaN-aware vector and matrix norms, A-norms and their inverses |
| [`cvx.linalg.operators`](https://github.com/Jebel-Quant/linalg/tree/main/src/cvx/linalg/operators) | Symmetric linear operators (dense, Gram, factor, incremental) for active-set and Krylov solvers |
| [`cvx.linalg.solve`](https://github.com/Jebel-Quant/linalg/tree/main/src/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)`](https://github.com/Jebel-Quant/linalg/blob/main/src/cvx/linalg/core/valid.py)** — Return a boolean mask and valid submatrix by removing rows/columns with non-finite diagonal entries
- **[`cond(matrix, p=None)`](https://github.com/Jebel-Quant/linalg/blob/main/src/cvx/linalg/core/exceptions.py)** — Condition number of a matrix (NaN-aware); accepts the same `p` norm values as `numpy.linalg.cond`
- **[`check_and_warn_condition(matrix, threshold)`](https://github.com/Jebel-Quant/linalg/blob/main/src/cvx/linalg/core/exceptions.py)** — Emit `IllConditionedMatrixWarning` when the condition number exceeds the threshold; `threshold=None` skips the check (and its SVD) entirely
- **[`warn_ill_conditioned(cond_value, threshold)`](https://github.com/Jebel-Quant/linalg/blob/main/src/cvx/linalg/core/exceptions.py)** — Emit `IllConditionedMatrixWarning` for an already-computed condition number; `threshold=None` never warns
- **`DEFAULT_COND_THRESHOLD`** — Default condition-number threshold (`1e12`) used by the ill-conditioning checks

### Types

- **[`Matrix`](https://github.com/Jebel-Quant/linalg/blob/main/src/cvx/linalg/core/types.py)** — Type alias for a 2-D `numpy.ndarray` with `float64` dtype
- **[`Vector`](https://github.com/Jebel-Quant/linalg/blob/main/src/cvx/linalg/core/types.py)** — Type alias for a 1-D `numpy.ndarray` with `float64` dtype

The package ships a `py.typed` marker; all public signatures are precisely annotated and verified with [ty](https://github.com/astral-sh/ty) in CI.

### Exceptions & Warnings

All exceptions and warnings live in [`core/exceptions.py`](https://github.com/Jebel-Quant/linalg/blob/main/src/cvx/linalg/core/exceptions.py):

- **`DimensionMismatchError`** — Raised when vector length does not match matrix dimension
- **`IllConditionedMatrixWarning`** — Emitted when the condition number exceeds a configurable threshold
- **`InvalidComponentsError`** — Raised when `pca` is asked for fewer than 1 or more components than the data supports
- **`NegativeWarmupError`** — Raised when a negative warmup period is passed to `ewm_covariance`
- **`NonIntegerWarmupError`** — Raised when a non-integer warmup is passed to `ewm_covariance`
- **`NonSquareMatrixError`** — Raised when a square matrix is required but the input is not square
- **`NotAMatrixError`** — Raised when a 2-D matrix is required but the input has different dimensionality
- **`SingularMatrixError`** — Raised when a matrix is numerically singular

## Covariance (`cvx.linalg.covariance`)

- **[`cov_to_corr(cov, min_var=1e-14)`](https://github.com/Jebel-Quant/linalg/blob/main/src/cvx/linalg/covariance/cov_to_corr.py)** — Convert a covariance matrix to a correlation matrix
- **[`ewm_covariance(data, assets, index_col, window, is_halflife, warmup)`](https://github.com/Jebel-Quant/linalg/blob/main/src/cvx/linalg/covariance/ewm_cov.py)** — Exponentially weighted covariance matrices from a Polars DataFrame (requires the `ewm` extra)
- **[`pca(returns, n_components=10)`](https://github.com/Jebel-Quant/linalg/blob/main/src/cvx/linalg/covariance/pca.py)** — Principal Component Analysis via SVD
- **[`rand_cov(n, seed=None)`](https://github.com/Jebel-Quant/linalg/blob/main/src/cvx/linalg/covariance/rand_cov.py)** — Random positive semi-definite covariance matrix

## Decompositions (`cvx.linalg.decomposition`)

- **[`cholesky(cov)`](https://github.com/Jebel-Quant/linalg/blob/main/src/cvx/linalg/decomposition/cholesky.py)** — Upper triangular Cholesky factor R such that R.T @ R = cov
- **[`cholesky_solve(cov, rhs)`](https://github.com/Jebel-Quant/linalg/blob/main/src/cvx/linalg/decomposition/cholesky.py)** — Solve cov @ x = rhs via Cholesky decomposition (falls back to LU for non-positive-definite matrices)
- **[`is_positive_definite(matrix)`](https://github.com/Jebel-Quant/linalg/blob/main/src/cvx/linalg/decomposition/cholesky.py)** — Return True if the matrix is symmetric positive-definite
- **[`eigh(matrix)`](https://github.com/Jebel-Quant/linalg/blob/main/src/cvx/linalg/decomposition/eigh.py)** — Eigenvalues/eigenvectors of the valid symmetric/Hermitian submatrix in ascending eigenvalue order
- **[`eigvalsh(matrix)`](https://github.com/Jebel-Quant/linalg/blob/main/src/cvx/linalg/decomposition/eigh.py)** — Eigenvalues-only convenience wrapper around `eigh`
- **[`eigvals(matrix)`](https://github.com/Jebel-Quant/linalg/blob/main/src/cvx/linalg/decomposition/eigvals.py)** — Eigenvalues of a general square matrix (supports complex output for non-symmetric matrices)
- **[`power_iteration(matrix, n_iter=1000, tol=1e-9, seed=None)`](https://github.com/Jebel-Quant/linalg/blob/main/src/cvx/linalg/decomposition/power_iteration.py)** — Estimate the dominant (largest-magnitude) eigenpair of a symmetric matrix via power iteration
- **[`qr(matrix)`](https://github.com/Jebel-Quant/linalg/blob/main/src/cvx/linalg/decomposition/qr.py)** — Reduced QR decomposition, matching `np.linalg.qr(mode='reduced')`
- **[`svd(matrix)`](https://github.com/Jebel-Quant/linalg/blob/main/src/cvx/linalg/decomposition/svd.py)** — Raw compact singular value decomposition via `np.linalg.svd(full_matrices=False)`
- **[`svd_k(matrix, k)`](https://github.com/Jebel-Quant/linalg/blob/main/src/cvx/linalg/decomposition/svd.py)** — Exact truncated rank-`k` SVD (the best rank-`k` approximation; leading triplets of the compact SVD)

## Norms (`cvx.linalg.norm`)

- **[`norm(x, ord=None)`](https://github.com/Jebel-Quant/linalg/blob/main/src/cvx/linalg/norm/norm.py)** — Norm of a vector or matrix, ignoring non-finite entries; supports all `ord` values of `np.linalg.norm`
- **[`a_norm(vector, matrix=None)`](https://github.com/Jebel-Quant/linalg/blob/main/src/cvx/linalg/norm/norm.py)** — Euclidean norm or NaN-aware matrix norm
- **[`inv_a_norm(vector, matrix=None, cond_threshold=1e12)`](https://github.com/Jebel-Quant/linalg/blob/main/src/cvx/linalg/norm/norm.py)** — 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)`](https://github.com/Jebel-Quant/linalg/blob/main/src/cvx/linalg/solve/solve.py)** — Solve a linear system (vector or matrix rhs) restricted to valid rows/columns; NaN entries are returned for invalid positions
- **[`lstsq(matrix, rhs, cond_threshold=1e12)`](https://github.com/Jebel-Quant/linalg/blob/main/src/cvx/linalg/solve/lstsq.py)** — Solve a least-squares system with NaN-aware row filtering; returns `(x, residuals, rank, sv)` consistent with `numpy.linalg.lstsq`
- **[`inv(matrix, cond_threshold=1e12)`](https://github.com/Jebel-Quant/linalg/blob/main/src/cvx/linalg/solve/inv.py)** — Invert a matrix restricted to valid rows/columns; NaN rows/columns are returned for invalid positions
- **[`det(matrix, cond_threshold=1e12)`](https://github.com/Jebel-Quant/linalg/blob/main/src/cvx/linalg/solve/det.py)** — Determinant of a square matrix with NaN-aware submatrix handling; emits `IllConditionedMatrixWarning` when 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`](https://github.com/Jebel-Quant/linalg/blob/main/src/cvx/linalg/operators/base.py)
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`](https://github.com/Jebel-Quant/linalg/blob/main/src/cvx/linalg/operators/base.py)** — Protocol exposing a symmetric matrix through `matvec`, `block_matvec`, `solve_free`, `apply_free`, `rcond_free`, `restricted`, and `diag`
- **[`DenseOperator`](https://github.com/Jebel-Quant/linalg/blob/main/src/cvx/linalg/operators/dense.py)** — Backend wrapping an explicit dense `n x n` matrix
- **[`IncrementalDenseOperator`](https://github.com/Jebel-Quant/linalg/blob/main/src/cvx/linalg/operators/dense.py)** — `DenseOperator` maintaining the free-block inverse across single-index flips for active-set sweeps
- **[`GramOperator`](https://github.com/Jebel-Quant/linalg/blob/main/src/cvx/linalg/operators/gram.py)** — Matrix-free backend for `A = M.T @ M`, represented by its factor `M`; never forms the Gram matrix
- **[`FactorOperator`](https://github.com/Jebel-Quant/linalg/blob/main/src/cvx/linalg/operators/factor.py)** — Diagonal-plus-low-rank backend `A = diag(d) + U @ Delta @ U.T` with Woodbury free-block solves
- **[`SumOperator`](https://github.com/Jebel-Quant/linalg/blob/main/src/cvx/linalg/operators/composite.py)** — Weighted sum of symmetric operators (forward-only; feed `apply_free` to 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)`](https://github.com/Jebel-Quant/linalg/blob/main/src/cvx/linalg/kkt/bordered.py)** — 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 a `SymmetricOperator`
- **[`AffineProjection(c, d)`](https://github.com/Jebel-Quant/linalg/blob/main/src/cvx/linalg/kkt/projection.py)** — Euclidean projection onto the affine set `{x : C x = d}`, caching the Gram matrix `C C.T` for repeated use

## Stability policy

This package follows [semantic versioning](https://semver.org). 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 `DeprecationWarning` in the meantime (currently: the two-argument
  `cholesky(cov, rhs)` form — use `cholesky_solve` — slated for removal in 2.0).
- **Supported environments:** Python 3.11–3.14, NumPy ≥ 2.0. The optional
  `ewm` extra requires Polars ≥ 1.40.

Numerical conventions (NaN handling, condition-number warnings, dtype
contract) are documented in
[Numerical Behavior](https://jebel-quant.github.io/linalg/numerical-behavior/).
