This submodule provides efficient representations for low-rank matrices,
enabling memory-efficient storage and fast operations on matrices that
can be expressed as products of thin factors.
LowRankMatrix : Generic low-rank matrix base class
QuasiSVD : Quasi-SVD factorization (U @ S @ V.T) where S is dense
SVD : Singular Value Decomposition (U @ diag(s) @ V.T)
QR : QR factorization (Q @ R)
Authors: Benjamin Carrel and Rik Vorhaar, University of Geneva, 2022
As a subclass of scipy.sparse.linalg.LinearOperator, LowRankMatrix provides
efficient matrix-vector and matrix-matrix products without forming the full
dense matrix. This enables direct use with scipy’s iterative solvers and
other numerical algorithms.
Key LinearOperator features:
- Matrix-vector multiplication: A @ v or A.matvec(v)
- Adjoint multiplication: A.H @ v or A.rmatvec(v)
- Matrix-matrix multiplication: A @ B or A.matmat(B)
- Lazy composition: A + B, A @ B (when B is LinearOperator)
- Shape and dtype attributes: A.shape, A.dtype
Examples
Using with scipy iterative solvers:
>>> fromscipy.sparse.linalgimportgmres,LinearOperator>>> fromlow_rank_toolboximportSVD>>> importnumpyasnp>>>>>> # Create a well-conditioned low-rank matrix>>> U,_=np.linalg.qr(np.random.randn(1000,10))>>> s=np.logspace(0,-1,10)# Better conditioned>>> V,_=np.linalg.qr(np.random.randn(1000,10))>>> # Add diagonal regularization for better conditioning>>> A=SVD(U,s,V)>>> A_reg=A+0.1*LinearOperator((1000,1000),matvec=lambdax:x)>>>>>> # Solve Ax = b using GMRES (never forms the full matrix)>>> b=np.random.randn(1000)>>> x,info=gmres(A_reg,b,rtol=1e-6,atol=1e-6,maxiter=100)>>> assertinfo==0# Verify convergence
Lazy composition with other operators:
>>> fromscipy.sparseimportdiags>>> fromscipy.sparse.linalgimportaslinearoperator>>>>>> # Create a diagonal operator for better conditioning>>> D=diags([0.5foriinrange(1000)])>>> D_op=aslinearoperator(D)>>>>>> # Lazy sum - doesn't form full matrix>>> B=A+D_op# Returns _SumLinearOperator>>>>>> # Use in iterative solver>>> x2,info2=gmres(B,b,rtol=1e-6,atol=1e-6,maxiter=100)>>> assertinfo2==0# Verify convergence
Matrix-vector products (efficient, no dense formation):
>>> v=np.random.randn(1000)>>> y=A@v# Efficient: never forms full 1000x1000 matrix>>>>>> # Adjoint product>>> z=A.H@v# Hermitian transpose product
Custom preconditioners:
>>> # Use the regularized matrix from above>>> defprecondition(v):... # Simple diagonal preconditioner... returnv>>>>>> # Create LinearOperator from function>>> M=LinearOperator((1000,1000),matvec=precondition)>>>>>> # Use as preconditioner>>> x3,info3=gmres(A_reg,b,M=M,rtol=1e-6,atol=1e-6,maxiter=100)>>> assertinfo3==0# Verify convergence
Notes
All matrix-vector products are computed efficiently in O(rank) operations
Full matrix is never formed unless explicitly requested with .full()
Subclasses (SVD, QR, QuasiSVD) inherit all LinearOperator functionality
Compatible with all scipy.sparse.linalg iterative solvers (gmres, cg, bicgstab, etc.)
Initialize a low-rank matrix from a sequence of factor matrices.
Parameters:
*matrices (ndarray) – Sequence of matrices whose product forms the low-rank matrix.
At least two matrices must be provided, and their shapes must align
for matrix multiplication (i.e., matrices[i].shape[1] == matrices[i+1].shape[0]).
**extra_data (dict) – Additional data to store with the matrix (e.g., poles, residues).
Raises:
ValueError – If fewer than 2 matrices are provided or if matrix shapes do not align.
The ‘power_iteration’ method estimates the largest singular value
using power iteration. The smallest is estimated using the inverse
(via solving linear systems). This is approximate but avoids full SVD.
The ‘norm_ratio’ method uses matrix norms as bounds.
Subclasses should override this to perform format-specific conversions.
For example, SVD.from_low_rank() would compute the SVD of the input.
The base class implementation creates a generic LowRankMatrix.
Examples
>>> X_generic=LowRankMatrix(A,B)>>> X_svd=SVD.from_low_rank(X_generic)# Converts to SVD format
Access specific matrix entries without forming the full matrix.
Parameters:
indices (ndarray or list) – Index specification. For single element: [row_idx, col_idx].
For multiple elements (fancy indexing): [row_indices, col_indices].
This compares the total number of elements stored in the factorization
versus the full m×n matrix. For empty matrices (zero size), returns True.
Examples
>>> A=LowRankMatrix(np.random.randn(1000,10),np.random.randn(10,1000))>>> A.is_memory_efficient# True: 20,000 elements vs 1,000,000True>>> B=LowRankMatrix(np.random.randn(100,90),np.random.randn(90,100))>>> B.is_memory_efficient# False: 18,000 elements vs 10,000False
This is more efficient than computing the norm and squaring it,
as it avoids the square root operation. For complex matrices,
uses Hermitian transpose (X^H X) to ensure real result.
This is an upper bound on the true numerical rank. The actual rank
may be lower if the factors are rank-deficient. For the true numerical
rank, compute SVD of the full matrix.
For a product of matrices A₁A₂…Aₙ, this method uses the cyclic property
of the trace to minimize computational cost. The trace is computed as
tr(A₂…AₙA₁) by cyclically permuting to minimize the intermediate matrix sizes.
Efficient storage and operations for matrices represented as the product of an
orthogonal matrix Q and an upper triangular matrix R. Particularly well-suited
for solving linear systems and least squares problems.
CRITICAL: Q MUST have orthonormal columns (Q.H @ Q = I_r).
This is NOT verified at initialization for performance.
Invalid inputs produce incorrect results without warning.
Use .is_orthogonal() to verify orthonormality if needed
Use .is_upper_triangular() to verify R structure
R diagonal elements |R[i,i]| serve as importance indicators (but not sorted)
After operations, may return LowRankMatrix (orthogonality not preserved)
Transposed mode enables efficient representation of A.H without recomputation
>>> b=np.random.randn(100)>>> x=X.solve(b)>>> np.allclose(A@x,b)# True>>>>>> # Least squares for overdetermined systems>>> x_ls=X.lstsq(b)>>> residual=np.linalg.norm(A@x_ls-b)
Efficient operations:
>>> # Addition preserves QR structure (same transposed mode)>>> Y=QR.from_matrix(np.random.randn(100,80))>>> Z=X+Y# Returns QR (may auto-truncate to min(m,n))>>>>>> # Frobenius norm computed from R only>>> norm_fro=X.norm('fro')# O(rn) vs O(mnr) for full matrix
Transposed mode for efficient A.H representation:
>>> # Instead of computing QR(A.H), use transposed flag>>> X_t=QR.from_matrix(A,transposed=True)>>> np.allclose(X_t.full(),A.T)# True (for real A)>>>>>> # Conjugate transpose flips mode>>> X_h=X.H# Returns transposed QR>>> assertX_h._transposed!=X._transposed
Validation and diagnostics:
>>> X.is_orthogonal()# Verify Q orthonormality>>> X.is_upper_triangular()# Verify R structure>>> cond_approx=X.cond(exact=False)# Fast diagonal approximation>>> cond_exact=X.cond(exact=True)# Exact via SVD of R
Truncation for rank reduction:
>>> X_trunc=X.truncate(r=10)# Keep first 10 columns>>> X_trunc=X.truncate(atol=1e-10)# Remove small R[i,i]
Conversion between formats:
>>> fromlow_rank_toolbox.matricesimportSVD>>> X_svd=X.to_svd()# Convert to SVD (requires SVD of R)>>> Y=QR.from_svd(X_svd)# Convert back (Q=U, R=S@V.H)
Memory efficiency:
>>> X.compression_ratio()# < 1.0 means memory savings>>> X.memory_usage('MB')# Actual memory used
Q (ndarray) – Orthogonal matrix (m x r) with orthonormal columns.
ASSUMED to have orthonormal columns: Q.H @ Q = I_r.
Not verified at initialization for performance.
R (ndarray) – Upper triangular matrix (r x n)
transposed (bool, optional) – If True, represents X = R.H @ Q.H instead of X = Q @ R, by default False
For real matrices, this is equivalent to X = R.T @ Q.T
**extra_data – Additional data to store with the matrix
Notes
For the standard form (transposed=False): X = Q @ R
For the transposed form (transposed=True): X = R.H @ Q.H
The transposed form is useful when you have computed QR(A.H) and want
to represent A efficiently.
Compute condition number efficiently using R matrix.
For QR = Q @ R with orthogonal Q:
cond(Q @ R) = cond(R)
The condition number is preserved by orthogonal transformations.
Parameters:
p (int or str, optional) – Norm type. Default is 2 (spectral norm).
- 2: Spectral condition number (ratio of max/min singular values)
- ‘fro’: Frobenius norm condition number
- 1, -1, inf, -inf: Other matrix norms
exact (bool, optional) – If True, compute exact condition number using numpy.linalg.cond.
If False (default), use fast diagonal approximation for p=2.
For other norms, always uses exact computation.
ValueError – If R has zeros on diagonal (singular matrix)
Notes
Fast approximation (p=2, exact=False):
For upper triangular R, estimates the 2-norm condition number
from diagonal elements: cond_2(R) ≈ max(|diag(R)|) / min(|diag(R)|)
This is a LOWER BOUND for the true condition number. The exact value
requires SVD of R: cond_2(R) = σ_max(R) / σ_min(R)
The approximation is exact for diagonal matrices and reasonable for
well-conditioned matrices, but can significantly underestimate the
condition number for ill-conditioned non-diagonal triangular matrices.
Exact computation (exact=True or other norms):
Uses numpy.linalg.cond(R, p) which computes the exact condition number.
This requires SVD for p=2, which is O(r³).
Computational cost:
- p=2, exact=False: O(r) - diagonal extraction only
- p=2, exact=True: O(r³) - requires SVD of R
- Other norms: O(r²) to O(r³) depending on norm type
Caching:
Results are cached for repeated queries with the same (norm, exact) pair.
Singularity:
If any diagonal element is zero (within machine precision),
the approximation returns np.inf to indicate singular matrix.
Why use approximation?
For large ranks, computing exact condition number via SVD is expensive.
The diagonal approximation provides a quick lower bound that’s sufficient
for many applications (checking if matrix is well-conditioned).
Examples
>>> A=np.random.randn(100,80)>>> X=QR.from_matrix(A)>>>>>> # Fast approximation>>> cond_approx=X.cond(2)# O(r)>>>>>> # Exact computation>>> cond_exact=X.cond(2,exact=True)# O(r³)>>>>>> # For diagonal matrices, approximation is exact>>> A_diag=np.diag([1e10,1e5,1e0,1e-5,1e-10])>>> X_diag=QR.from_matrix(A_diag)>>> print(X_diag.cond(2))# 1e20 (exact for diagonal)>>>>>> # Other norms>>> cond_fro=X.cond('fro')# Always exact
mode (str, optional) – QR mode (‘economic’ or ‘complete’), by default ‘economic’
transposed (bool, optional) – If True, return the transposed form X = R.H @ Q.H, by default False
Standard form (False): X = Q @ R
Transposed form (True): X = R.H @ Q.H = matrix.H
**extra_data – Additional data to store
Returns:
QR decomposition object where full() reconstructs the input matrix
Interpretation: X = R.H @ Q.H = V @ S @ U.H = (U @ S @ V.H).H
Computational cost:
- Matrix multiplication S @ V.H: O(r²n) where r = rank
- No QR decomposition needed (already have orthogonal Q)
- Total: O(r²n)
Advantages over to_svd():
- Cheaper: O(r²n) vs O(r²(m+n))
- No loss of orthogonality (U is already orthogonal)
- Preserves singular value information in R structure
- No SVD computation needed
- Preserves singular value structure in R
When to use:
- When you have an SVD and need QR format
- When you want to exploit triangular structure of R
- For efficient solve operations (QR is faster than SVD)
Examples
>>> A=np.random.randn(100,80)>>> X_svd=SVD.from_matrix(A)>>> X_qr=QR.from_svd(X_svd)>>> print(X_qr.shape)# (100, 80)>>> print(X_qr.rank)# Same as X_svd.rank>>> np.allclose(X_svd.full(),X_qr.full())# True
>>> # Verify Q is orthogonal and R is upper triangular>>> print(X_qr.is_orthogonal())# True>>> print(X_qr.is_upper_triangular())# False (R = S @ V.H not triangular!)
The rank will be multiplied by the rank of the other matrix.
This operation may be inefficient for large ranks.
In that case a warning is raised, and one should consider
converting to dense with .todense() first.
Verifies that Q.H @ Q = I_k (orthonormal columns), where k is the number of columns in Q.
This is always true when QR is constructed via from_matrix() or qr(),
but may not hold if Q and R are provided manually.
Note that Q may have more columns than the reported rank after Hadamard products.
Computational cost: O(k²m) where k is the number of columns in Q.
Result is cached for the default tolerance.
Verifies that R[i,j] ≈ 0 for all i > j (strict lower triangular part is zero).
This is always true when QR is constructed via from_matrix() or qr(),
but may not hold if Q and R are provided manually.
For methods like solve() and lstsq(), having an upper triangular R enables
efficient back substitution. If R is not upper triangular, these methods
may produce incorrect results.
Computational cost: O(r²) where r = min(rank, n) is the number of rows in R.
Result is cached for the default tolerance.
b (ndarray) – Right-hand side, shape (m,) or (m, k)
rcond (float, optional) – Relative condition number threshold for rank determination.
Singular values smaller than rcond * largest_singular_value are treated as zero.
If None, machine precision is used.
Returns:
Least squares solution, shape (n,) or (n, k)
Return type:
ndarray
Notes
For overdetermined systems (m > n), finds x minimizing ||Ax - b||_2
For underdetermined systems (m < n), finds minimum norm solution
Uses QR decomposition:
- For X = Q @ R: x = R^+ @ Q.H @ b where R^+ is pseudoinverse of R
- Handles rank deficiency by zeroing small diagonal elements of R
Compute Moore-Penrose pseudoinverse using QR factorization.
For X = Q @ R, computes X^+ = R^+ @ Q.H
Parameters:
rcond (float, optional) – Cutoff for small singular values in R.
Singular values smaller than rcond * largest_singular_value are set to zero.
If None, machine precision is used.
Returns:
Pseudoinverse matrix, shape (n, m)
Return type:
ndarray
Notes
The pseudoinverse satisfies:
1. X @ X^+ @ X = X
2. X^+ @ X @ X^+ = X^+
3. (X @ X^+).H = X @ X^+
4. (X^+ @ X).H = X^+ @ X
Computational cost:
- SVD of R: O(r²n) where r = rank, n = number of columns
- Matrix multiplication Q @ U_R: O(mr²) where m = number of rows
- Total: O(r²(m + n))
Orthogonality:
- Input: Q has orthonormal columns, R is upper triangular
- Output: U = Q @ U_R has orthonormal columns (product of orthogonal matrices)
- Output: V = V_R has orthonormal columns (from SVD)
- Result is a valid SVD with orthonormal U and V
Transposed mode:
Both standard and transposed modes produce the same SVD representation.
This is because SVD naturally represents X, not X.H.
Examples
>>> A=np.random.randn(100,80)>>> X_qr=QR.from_matrix(A)>>> X_svd=X_qr.to_svd()>>> print(X_svd.shape)# (100, 80)>>> print(X_svd.rank)# Same as X_qr.rank>>> np.allclose(X_qr.full(),X_svd.full())# True
Truncate QR to lower rank by removing columns with small R diagonal elements.
The rank is prioritized over the tolerance.
The relative tolerance is prioritized over absolute tolerance.
NOTE: By default, the truncation is done with respect to the machine precision (DEFAULT_ATOL).
Parameters:
r (int, optional) – Target rank (number of columns to keep). If None, use tolerance.
rtol (float, optional) – Relative tolerance. Remove columns where |R[i,i]| < rtol * max(|R[i,i]|).
Default is DEFAULT_RTOL (None).
atol (float, optional) – Absolute tolerance. Remove columns where |R[i,i]| < atol.
Default is DEFAULT_ATOL (~2.22e-14).
inplace (bool, optional) – If True, modify in place. Default False.
For QR, truncation keeps the first r columns of Q and first r rows of R.
The diagonal elements of R, |R[i,i]|, serve as importance indicators.
Unlike SVD where singular values are sorted, R diagonal elements are NOT
necessarily in decreasing order. However, for numerically stable QR decompositions,
large diagonal elements indicate more important directions.
Algorithm:
1. If r is specified: keep first r columns/rows
2. If rtol is specified: keep columns where |R[i,i]| > rtol * max(|R[i,i]|)
3. If atol is specified: keep columns where |R[i,i]| > atol
Priority: r > rtol > atol
Examples
>>> X=QR.from_matrix(A)>>> X_trunc=X.truncate(r=10)# Keep only first 10 columns>>> X_trunc=X.truncate(rtol=1e-10)# Remove columns with small R[i,i]>>> X_trunc=X.truncate(atol=1e-12)# Absolute threshold
Quasi Singular Value Decomposition (Quasi-SVD) for low-rank matrices.
A generalization of the SVD where the middle matrix S is not necessarily diagonal.
This representation is useful for operations that preserve orthogonality of U and V
but may produce non-diagonal or even singular middle matrices.
where:
- U ∈ ℝ^(m×r) should have orthonormal columns (U^T @ U = I_r)
- V ∈ ℝ^(n×q) should have orthonormal columns (V^T @ V = I_q)
- S ∈ ℝ^(r×q) is a general matrix (not necessarily diagonal, may be singular)
Matrix multiplication between SVD and other.
The output is an SVD or a Matrix, depending on the type of other.
If two QuasiSVD are multiplied, the new rank is the minimum of the two ranks.
If side is ‘right’ or ‘usual’, compute self @ other.
If side is ‘left’ or ‘opposite’, compute other @ self.
Computational cost: O(r³) for matrix exponential of r×r matrix S
(vs O(n³) for general n×n matrix).
This method currently only supports Hermitian matrices where V.H == U.
For general matrices, the eigenvalue decomposition would be needed.
Examples
>>> S=np.array([[1.0,0.1],[0.1,0.5]])>>> U,_=np.linalg.qr(np.random.randn(100,2))>>> X=QuasiSVD(U,S,U)# Symmetric/Hermitian>>> # Note: Current implementation has a bug - references self.s which doesn't exist>>> # Use SVD.expm() for diagonal S matrices instead>>> # X_exp = X.expm() # Will raise AttributeError
The new rank is the multiplication of the two ranks, at maximum.
NOTE: if the expected rank is too large, dense matrices are used for the computation, but the output is still an SVD.
Result is cached after first computation.
Uses np.allclose with default tolerances.
Orthogonality is ASSUMED at initialization but not enforced.
This method verifies the assumption.
Result is cached after first computation.
Uses condition number test: cond(S) >= 1/ε where ε is machine precision.
S may become singular after operations like addition or subtraction.
For QuasiSVD to be symmetric, U and V must be identical (within tolerance).
This is a necessary condition when X = U @ S @ V.T.
Uses np.allclose for comparison (tolerates small numerical errors).
Matrix multiplication of several QuasiSVD matrices.
The rank of the output is the minimum of the ranks of the first and last inputs, at maximum.
The matrices are multiplied all at once, so this method is more efficient than multiplying them one by one.
Calculate matrix norm with caching and optimization for orthogonal case.
Parameters:
ord (str or int, default='fro') – Order of the norm. Supported values:
- ‘fro’: Frobenius norm (default)
- 2: Spectral norm (largest singular value)
- ‘nuc’: Nuclear norm (sum of singular values)
- Other values: computed via np.linalg.norm(self.full(), ord=ord)
Result is cached after first computation for each norm type.
For orthogonal U and V, common norms are computed efficiently from singular
values of S (O(r³)) instead of forming full matrix (O(mnr)).
- Frobenius: ||X||_F = ||S||_F
- Spectral (2-norm): ||X||_2 = σ_max(S)
- Nuclear: ||X||_* = Σ σ_i(S)
Check the numerical health of the QuasiSVD representation.
This method checks:
- Orthogonality of U and V
- Condition number of S
- Presence of near-zero singular values
- Memory efficiency
Parameters:
verbose (bool, optional) – If True, print a summary. Default is True.
Returns:
Dictionary with health metrics:
- ‘orthogonal_U’: bool, whether U is orthogonal
- ‘orthogonal_V’: bool, whether V is orthogonal
- ‘orthogonality_error_U’: float, ||U.H @ U - I||_F
- ‘orthogonality_error_V’: float, ||V.H @ V - I||_F
- ‘condition_number_S’: float, condition number of S
- ‘is_singular’: bool, whether S is numerically singular
- ‘min_singular_value’: float, smallest singular value of S
- ‘max_singular_value’: float, largest singular value of S
- ‘singular_value_ratio’: float, max/min singular values
- ‘compression_ratio’: float, storage efficiency
- ‘memory_efficient’: bool, whether low-rank is beneficial
- ‘recommendations’: list of str, suggested actions
Oblique projection onto the tangent space at self using interpolation (DEIM-like methods).
The formula is given by:
P_X Y = M_u Y_u - M_u Y_uv M_v^* + Y_v M_v^*
where M_u = U (P_U^* U)^{-1}, M_v = V (P_V^* V)^{-1},
Y_u = Y[p_u, :], Y_v = Y[:, p_v] and Y_uv = Y[p_u, p_v]
with p_u and p_v being the interpolation indices for U and V, respectively.
Here, self = U S Vh is the SVD of matrix self and Y is the input matrix to project.
Two usage modes:
Offline mode (provide pre-computed interpolatory matrices):
Provide Y_u, Y_v, Y_uv, M_u, M_v in kwargs.
Online mode (compute interpolation on-the-fly):
Provide Y in kwargs. Optionally provide cssp_method_u and/or cssp_method_v.
The indices and interpolatory matrices are computed using the specified CSSP methods.
If not provided, QDEIM is used by default.
Parameters:
mode (str, optional) – Either ‘online’ or ‘offline’. Default is ‘online’.
dense_output (bool, optional) – Whether to return a dense matrix. False by default.
>>> X=QuasiSVD.generate_random((100,80),5)>>> u=np.random.randn(100)>>> v=np.random.randn(80)>>> X_new=X.rank_one_update(u,v,alpha=0.5)>>> # X_new represents X + 0.5 * u @ v.T with rank at most 6
Re-orthogonalize U and V if numerical drift has occurred.
After many operations, U and V may lose orthogonality due to
numerical errors. This method restores orthogonality.
Parameters:
method (str, optional) – Method for re-orthogonalization:
- ‘qr’: QR decomposition (default, stable and efficient)
- ‘svd’: Full SVD (more expensive but more accurate)
inplace (bool, optional) – If True, modify the current object in-place. Otherwise, return a new object. Default is False.
Returns:
Re-orthogonalized QuasiSVD with same matrix values
Solution method:
- ‘lstsq’: Use pseudoinverse (default, works for any shape)
- ‘direct’: Use factored form assuming orthogonality (faster but requires
orthogonal U, V and invertible S)
Returns:
Solution x. Shape (n,) or (n, k).
Return type:
ndarray
Raises:
ValueError – If matrix is not square and method=’direct’.
If dimensions are incompatible.
Examples
>>> X=QuasiSVD.generate_random((100,100),100)# Full rank for unique solution>>> b=np.random.randn(100)>>> x=X.solve(b)>>> # Check: X @ x ≈ b>>> np.allclose(X.dot(x),b)True
>>> # For rank-deficient systems, use lstsq instead:>>> X_deficient=QuasiSVD.generate_random((100,100),20)>>> x_ls=X_deficient.lstsq(b)
The default method is ‘lstsq’ which uses the pseudoinverse and is robust
to non-orthogonal factors and rank deficiency. Use method=’direct’ only
when you are certain that U and V are orthogonal and S is invertible.
Classify the type of SVD representation based on S dimensions.
Returns:
One of:
- ‘full’: S has shape (m, n) - full SVD with all singular vectors
- ‘reduced’: S has shape (min(m,n), min(m,n)) - reduced/economic SVD
- ‘truncated’: S has shape (r, r) where r < min(m,n) - truncated SVD
- ‘unconventional’: S has shape (r, k) where r ≠ k - general quasi-SVD
atol (float, optional) – Absolute tolerance. Singular values < atol are removed.
Default is DEFAULT_ATOL (machine precision).
inplace (bool, optional) – If True, this parameter is ignored (QuasiSVD cannot be truncated in-place,
must convert to SVD). Kept for API compatibility. Default is False.
Singular Value Decomposition (SVD) for low-rank matrices.
The gold standard for low-rank matrix representation with guaranteed diagonal structure and non-negative singular values. Inherits from QuasiSVD but enforces diagonal middle matrix for optimal storage.
where:
- U ∈ ℝ^(m×r) has orthonormal columns (U^H @ U = I_r)
- s ∈ ℝ^r contains non-negative singular values in descending order (s[0] ≥ s[1] ≥ … ≥ s[r-1] ≥ 0)
- V ∈ ℝ^(n×r) has orthonormal columns (V^H @ V = I_r)
- S = diag(s) is an r×r matrix with singular values on the diagonal
>>> # Remove small singular values>>> X_trunc=X.truncate(r=5)# Keep top 5>>> X_trunc=X.truncate(rtol=1e-10)# Relative tolerance (based on max singular value)>>> X_trunc=X.truncate(atol=1e-12)# Absolute tolerance (raw threshold, independent of max singular value)>>>>>> # Get perpendicular component (residual)>>> X_perp=X.truncate_perpendicular(r=5)# Keep last (r-5) singular values
Random matrices with controlled spectrum:
>>> # Generate test matrices>>> s_decay=np.logspace(0,-10,20)# Exponential decay>>> X_test=SVD.generate_random((100,100),s_decay,is_symmetric=True)>>> # Perfect for testing algorithms!
Conversion between formats:
>>> fromlow_rank_toolbox.matricesimportQuasiSVD,LowRankMatrix>>>>>> # From QuasiSVD (non-diagonal S)>>> X_quasi=QuasiSVD(U,S_full,V)# S_full is general matrix>>> X_svd=SVD.from_quasiSVD(X_quasi)# Diagonalize S>>>>>> # From generic low-rank>>> X_lr=LowRankMatrix(A,B,C)>>> X_svd=SVD.from_low_rank(X_lr)# Compute SVD
Memory efficiency:
>>> X.compression_ratio()# < 1.0 means memory savings>>> X.memory_usage('MB')# Actual memory used>>> X.is_memory_efficient# True if saves memory
Create a low-rank matrix stored by its SVD: X = U @ diag(s) @ V.T
This is the primary constructor for the SVD class. It efficiently stores
singular values as a 1D vector rather than a full diagonal matrix.
Parameters:
U (ndarray) – Left singular vectors, shape (m, r).
ASSUMED to have orthonormal columns: U.T @ U = I_r.
Not verified at initialization for performance.
s (ndarray) – Singular values. Accepts two formats:
- 1D array (shape (r,)): Vector of singular values (RECOMMENDED)
- 2D array (shape (r, r) or (r, k)): Diagonal or nearly-diagonal matrix
V (ndarray) – Right singular vectors, shape (n, k).
ASSUMED to have orthonormal columns: V.T @ V = I_k.
Not verified at initialization for performance.
NOTE: Provide V, not V.T or V.H (conjugate transpose).
**extra_data (dict, optional) – Additional metadata to store with the matrix (e.g., poles, residues).
Internal flags:
- ‘_skip_diagonal_check’: Skip diagonal verification (used internally)
- ‘_skip_memory_check’: Skip memory efficiency check
If U, V, or s are not numpy arrays
- If s is not 1D or 2D
Notes
Storage: Singular values are stored internally as a diagonal matrix S (2D array).
The input parameter s can be provided in either 1D or 2D format for convenience.
Input format flexibility:
- If s is 1D: converted to diagonal matrix S for storage
- If s is 2D diagonal: stored directly as S
- If s is 2D rectangular (r ≠ k): stored as rectangular matrix with filled diagonal
Orthogonality assumption: U and V are ASSUMED to have orthonormal columns.
This is NOT verified at initialization for performance. Use .is_orthogonal() to verify if needed.
Parent class: Calls QuasiSVD.__init__ with a flag to suppress the “use SVD class” warning (since we ARE the SVD class).
For SVD X = U @ diag(s) @ V.H, if X is Hermitian (U == V for real, U == V.conj() for complex), then:
exp(X) = U @ diag(exp(s)) @ U.H
where exp(s) is the element-wise exponential of singular values.
This is MUCH faster than matrix exponentiation for general matrices:
- SVD.expm (symmetric): O(r) for computing exp(s)
- General expm: O(n³) using Padé approximation
Parameters:
inplace (bool, optional) – If True, modify the current object in-place. Default is False.
Calculate matrix norm, optimized for SVD representation.
For orthogonal U and V, common norms are computed directly from singular
values without forming the full matrix. This is MUCH faster: O(r) vs O(mnr).
Parameters:
ord (str or int, optional) – Order of the norm. Default is ‘fro’ (Frobenius).
Optimized norms (computed from singular values):
- ‘fro’: Frobenius norm = ||s||₂
- 2: Spectral norm (largest singular value) = max(s)
- ‘nuc’: Nuclear norm (sum of singular values) = sum(s)
Other norms fall back to full matrix computation.
LinAlgError – If matrix is singular and method=’direct’.
Notes
The ‘direct’ method is faster than QuasiSVD.solve() since S is diagonal:
- SVD.solve: O(mr + rk) where k is the number of RHS vectors
- QuasiSVD.solve: O(mr² + r²k) due to S inversion
For rank-deficient matrices, use method=’lstsq’ or call lstsq() directly.
Examples
>>> X=SVD.generate_random((100,100),np.ones(100))# Full rank>>> b=np.random.randn(100)>>> x=X.solve(b)>>> # Check: X @ x ≈ b>>> np.allclose(X@x,b)True
>>> # For rank-deficient systems, may have larger residual:>>> X_deficient=SVD.generate_random((100,100),np.ones(20))# Rank 20>>> x_deficient=X_deficient.solve(b,method='lstsq')>>> # Residual may be non-zero for rank-deficient case
Truncate the SVD.
The rank is prioritized over the tolerance.
The relative tolerance is prioritized over absolute tolerance.
NOTE: By default, the truncation is done with respect to the machine precision (DEFAULT_ATOL).
Perpendicular truncation of SVD. (keep the minimal rank)
Rank is prioritized over tolerance.
Relative tolerance is prioritized over absolute tolerance.
NOTE: By default, the truncation is done with respect to the machine precision (DEFAULT_ATOL).