deepbox/linalg
Decompositions
LU, Cholesky, QR, SVD, eigendecompositions, Schur, Hessenberg and polar factorizations of 2-D matrices.
- Factorizations
Overview
- Every function takes 2-D tensors, reads them as float64 and returns
float64tensors on the default device, whatever the input dtype. NaN or infinite input throwsDataValidationError, and string or complex input throwsDTypeError. svd(a, fullMatrices = true)returns[U, s, Vt]with the singular values as a vector.qrreturns[Q, R]in reduced mode by default.lureturns[P, L, U]withP A = L U.choleskyreturns the lower factor, or the upper one with{ upper: true }, and throws for matrices that are not symmetric positive definite.hessenbergreturns[H, Q]andpolarreturns[U, P].eigsupports real eigenvalues only. A non-symmetric matrix with complex eigenvalues throwsInvalidParameterError. It returns[values, vectors]with unit-norm eigenvectors as columns. Symmetric input takes a faster path and gives ascending eigenvalues, andeighandeigvalshare the dedicated functions when you know the matrix is symmetric.
CholeskyOptions
typeOptions for cholesky.
- upper: if true, return the upper triangular factor U with A = Uᵀ * U instead of the lower triangular factor L with A = L * Lᵀ (default: false).
readonly upper?: boolean
EigOptions
typeOptions for eig and eigvals.
- maxIter: maximum number of QR sweeps spent on a single eigenvalue before a ConvergenceError is thrown (default: 300). A trailing 2×2 block is solved in closed form and needs no sweeps. - tol: relative threshold below which a sub-diagonal entry is treated as zero (default: machine epsilon, about 2.2e-16). Larger values deflate earlier at the cost of accuracy.
readonly maxIter?: numberreadonly tol?: number
cholesky
function cholesky(a: Tensor, options?: CholeskyOptions): Tensor;Cholesky decomposition.
Details
Factorizes symmetric positive definite matrix A into L * L^T where L is lower triangular.
Algorithm: Cholesky-Banachiewicz algorithm Time Complexity: O(N³/3) - approximately half the cost of LU decomposition Space Complexity: O(N²) for output matrix
Mathematical Background: For a symmetric positive definite matrix A, the Cholesky decomposition is: A = L * L^T where L is lower triangular with positive diagonal elements.
Numerical Stability: - Only works for positive definite matrices (all eigenvalues > 0) - More stable than LU for this class of matrices - No pivoting required due to positive definiteness
Parameters:
Parameters
aTensor- Symmetric positive definite matrix of shape (N, N)
optionsoptionalCholeskyOptions- Optional settings (see CholeskyOptions)
Returns
Tensor
Throws
ShapeErrorIf input is not 2D square matrixDTypeErrorIf input has string or complex dtypeDataValidationErrorIf matrix is not symmetricDataValidationErrorIf matrix is not positive definiteDataValidationErrorIf input contains non-finite values (NaN, Infinity)
import { cholesky } from 'deepbox/linalg';import { tensor } from 'deepbox/ndarray'; // Positive definite matrixconst A = tensor([[4, 12, -16], [12, 37, -43], [-16, -43, 98]]);const L = cholesky(A); // Verify: A ≈ L @ L^Teig
function eig(a: Tensor, options?: EigOptions): [ Tensor, Tensor ];Compute eigenvalues and eigenvectors of a square matrix.
Details
Solves A * v = λ * v where λ are eigenvalues and v are eigenvectors.
Algorithm: - Symmetric matrices: Householder tridiagonalization + implicit QL (same as eigh). Eigenvalues are returned in ascending order. - General matrices: balancing, Hessenberg reduction, Francis double-shift QR to the real Schur form, then back-substitution for the eigenvectors. Eigenvalues are returned in the order they appear on the diagonal of the Schur form.
Limitations: - Only real eigenvalues are supported. Non-symmetric matrices whose spectrum includes complex eigenvalues cause an InvalidParameterError to be thrown. - A conjugate pair whose imaginary part is at rounding level (at most 10 * N * eps * max|A| after balancing, as for the repeated zero eigenvalue of a low-rank matrix) is reported as a repeated real eigenvalue. - For symmetric/Hermitian matrices, use eigh() for better performance - Eigenvectors of a defective matrix (fewer independent eigenvectors than eigenvalues) are nearly parallel, as with numpy.linalg.eig.
Parameters:
Parameters
aTensor- Square matrix of shape (N, N)
optionsoptionalEigOptions- Optional configuration overrides (see EigOptions)
Returns
[ Tensor, Tensor ]
Throws
ShapeErrorIf input is not square matrixDTypeErrorIf input has string or complex dtypeDataValidationErrorIf input contains non-finite values (NaN, Infinity)InvalidParameterErrorIf matrix has complex eigenvalues, or an option is invalidConvergenceErrorIf the QR iteration does not converge withinmaxItersweeps
import { eig } from 'deepbox/linalg';import { tensor } from 'deepbox/ndarray'; const A = tensor([[1, 2], [2, 1]]);const [eigenvalues, eigenvectors] = eig(A); // Verify: A @ eigenvectors[:,i] ≈ eigenvalues[i] * eigenvectors[:,i]eigh
function eigh(a: Tensor): [ Tensor, Tensor ];Compute eigenvalues and eigenvectors of a symmetric/Hermitian matrix.
More efficient than eig() for symmetric matrices.
Parameters:
Parameters
aTensor- Symmetric matrix of shape (N, N) Returns: [eigenvalues, eigenvectors] - Eigenvalues are real and in ascending order - Eigenvectors are orthonormal columns
Returns
[ Tensor, Tensor ]
Throws
ShapeErrorIf input is not square matrixDTypeErrorIf input has string or complex dtypeDataValidationErrorIf input is not symmetricDataValidationErrorIf input contains non-finite values (NaN, Infinity)ConvergenceErrorIf the QL iteration does not converge
import { eigh } from 'deepbox/linalg';import { tensor } from 'deepbox/ndarray'; const A = tensor([[1, 2], [2, 1]]); // Symmetricconst [eigenvalues, eigenvectors] = eigh(A);eigvals
function eigvals(a: Tensor, options?: EigOptions): Tensor;Compute eigenvalues only (faster than eig).
Skips the eigenvector computation. Eigenvalues of symmetric input are returned in ascending order; for general input they follow the order of the real Schur form.
Parameters:
Parameters
aTensor- Square matrix of shape (N, N)
optionsoptionalEigOptions- Optional configuration overrides (see EigOptions) Returns: eigenvalues - Array of real eigenvalues Limitations: - Matrices with complex eigenvalues are not supported and will throw InvalidParameterError
Returns
Tensor
Throws
ShapeErrorIf input is not square matrixDTypeErrorIf input has string or complex dtypeDataValidationErrorIf input contains non-finite values (NaN, Infinity)InvalidParameterErrorIf matrix has complex eigenvalues, or an option is invalidConvergenceErrorIf the QR iteration does not converge withinmaxItersweeps
import { eigvals } from 'deepbox/linalg';import { tensor } from 'deepbox/ndarray'; const A = tensor([[1, 2], [2, 1]]);const eigenvalues = eigvals(A);console.log(eigenvalues); // [-1, 3]eigvalsh
function eigvalsh(a: Tensor): Tensor;Compute eigenvalues only of symmetric matrix (faster than eigh).
Parameters:
Parameters
aTensor- Symmetric square matrix of shape (N, N) Returns: eigenvalues - Array of real eigenvalues in ascending order
Returns
Tensor
Throws
ShapeErrorIf input is not square matrixDTypeErrorIf input has string or complex dtypeDataValidationErrorIf input is not symmetricDataValidationErrorIf input contains non-finite values (NaN, Infinity)ConvergenceErrorIf the QL iteration does not converge
import { eigvalsh } from 'deepbox/linalg';import { tensor } from 'deepbox/ndarray'; const A = tensor([[1, 2], [2, 1]]);const eigenvalues = eigvalsh(A);console.log(eigenvalues); // [-1, 3]hessenberg
function hessenberg(a: Tensor): [ Tensor, Tensor ];Upper Hessenberg decomposition of a square matrix.
Decomposes A into A = Q * H * Qᵀ where: - Q is an orthogonal matrix - H is an upper Hessenberg matrix (zero below the first sub-diagonal)
Uses Householder reflections. Unlike scipy.linalg.hessenberg, both factors are always returned.
Time Complexity: O(N³)
Parameters
aTensor- Input square matrix of shape (N, N)
Returns
[ Tensor, Tensor ][H, Q] where H is upper Hessenberg and Q is orthogonal
Throws
ShapeErrorIf input is not a square 2D matrixDTypeErrorIf input has string or complex dtypeDataValidationErrorIf input contains non-finite values (NaN, Infinity)
import { hessenberg } from 'deepbox/linalg';import { tensor } from 'deepbox/ndarray'; const A = tensor([[1, 2, 3], [4, 5, 6], [7, 8, 9]]);const [H, Q] = hessenberg(A);// Q * H * Q^T ≈ A// H is upper Hessenberg (zero below first sub-diagonal)lu
function lu(a: Tensor): [ Tensor, Tensor, Tensor ];LU decomposition with partial pivoting.
Details
Factorizes matrix A into P * A = L * U where: - P is a permutation matrix - L is lower triangular with unit diagonal - U is upper triangular
Algorithm: Gaussian elimination with partial pivoting
Convention: P is the row permutation applied to A (P @ A = L @ U). This is the transpose of the p returned by scipy.linalg.lu, which satisfies A = p @ l @ u.
Parameters:
Parameters
aTensor- Input matrix of shape (M, N) Returns: [P, L, U] - P: Permutation matrix of shape (M, M) - L: Lower triangular matrix of shape (M, K) where K = min(M, N) - U: Upper triangular matrix of shape (K, N) Requirements: - Input must be 2D matrix Properties: - P @ A = L @ U - L has unit diagonal (L[i,i] = 1) - Partial pivoting ensures numerical stability
Returns
[ Tensor, Tensor, Tensor ]
Throws
ShapeErrorIf input is not 2DDTypeErrorIf input has string or complex dtypeDataValidationErrorIf input contains non-finite values (NaN, Infinity) Rank-deficient matrices (with zero pivots) are accepted. The factorization still produces valid P, L, U factors.
import { lu } from 'deepbox/linalg';import { tensor } from 'deepbox/ndarray'; const A = tensor([[2, 1, 1], [4, 3, 3], [8, 7, 9]]);const [P, L, U] = lu(A); // Verify: P @ A ≈ L @ Upolar
function polar(a: Tensor, side?: "right" | "left"): [ Tensor, Tensor ];Polar decomposition of a matrix.
Details
Decomposes A into A = U * P where: - U has orthonormal columns (a unitary matrix when A is square, M >= N) or orthonormal rows (when M < N) - P is a symmetric positive semi-definite matrix (right polar factor)
Or A = S * U where: - S is a symmetric positive semi-definite matrix (left polar factor) - U is as above
Uses the SVD-based algorithm: given the reduced SVD A = W Σ Vᵀ, U = W Vᵀ (orthogonal factor) P = V Σ Vᵀ (right polar factor) S = W Σ Wᵀ (left polar factor)
For a rank-deficient A the orthogonal factor U is not unique; the one returned follows from the singular vectors that belong to the zero singular values.
Time Complexity: O(M * N * min(M,N)) dominated by SVD
Parameters
aTensor- Input matrix of shape (M, N)
sideoptional"right" | "left"- 'right' (default) returns [U, P] where A = U*P, 'left' returns [S, U] where A = S*U
Returns
[ Tensor, Tensor ][U, P] of shapes (M, N) and (N, N), or [S, U] of shapes (M, M) and (M, N)
Throws
ShapeErrorIf input is not 2DDTypeErrorIf input has string or complex dtypeDataValidationErrorIf input contains non-finite values (NaN, Infinity)InvalidParameterErrorIf side is not 'right' or 'left'ConvergenceErrorIf the underlying SVD does not converge
import { polar } from 'deepbox/linalg';import { tensor } from 'deepbox/ndarray'; const A = tensor([[1, 2], [3, 4]]);const [U, P] = polar(A);// U is unitary: U * U^T ≈ I// P is positive semi-definite// U * P ≈ Aqr
function qr(a: Tensor, mode?: "reduced" | "complete"): [ Tensor, Tensor ];QR decomposition.
Details
Factorizes matrix A into Q * R where Q is orthogonal and R is upper triangular.
Algorithm: Householder reflections, with the sign convention of LAPACK (and therefore NumPy): the reflection for column k maps it to -sign(x₀) * ‖x‖ * e₁, and a column that is already zero below the diagonal is left alone. As a result the diagonal of R is not necessarily positive.
Parameters:
Parameters
aTensor- Input matrix of shape (M, N)
modeoptional"reduced" | "complete"- Decomposition mode: - 'reduced' (default): Q has shape (M, K), R has shape (K, N) where K = min(M, N) - 'complete': Q has shape (M, M), R has shape (M, N) Returns: [Q, R] - Q: Orthogonal matrix (Q^T * Q = I) - R: Upper triangular matrix Requirements: - Input must be 2D matrix Properties: - A = Q @ R - Q is orthogonal: Q^T @ Q = I - R is upper triangular - In 'reduced' mode only the first K columns of Q are formed, so the cost is O(M * N * K) rather than O(M²) memory for tall inputs.
Returns
[ Tensor, Tensor ]
Throws
ShapeErrorIf input is not 2DDTypeErrorIf input has string or complex dtypeDataValidationErrorIf input contains non-finite values (NaN, Infinity)InvalidParameterErrorIf mode is not 'reduced' or 'complete'
import { qr } from 'deepbox/linalg';import { tensor } from 'deepbox/ndarray'; const A = tensor([[12, -51, 4], [6, 167, -68], [-4, 24, -41]]);const [Q, R] = qr(A); // Verify: A ≈ Q @ R// Verify: Q is orthogonal (Q^T @ Q ≈ I)schur
function schur(a: Tensor): [ Tensor, Tensor ];Real Schur decomposition of a square matrix.
Details
Decomposes A into A = Q * T * Qᵀ where: - Q is an orthogonal matrix (Schur vectors) - T is a quasi-upper triangular matrix (Schur form) - Real eigenvalues appear on the diagonal - Complex conjugate pairs appear in 2×2 blocks on the diagonal; such a block has equal diagonal entries and off-diagonal entries of opposite sign
Reduces A to Hessenberg form with Householder reflections, then runs the QR algorithm with implicit double shifts (Francis iteration) and deflation. The result has the same structure as scipy.linalg.schur(a, output="real"); the Schur vectors are unique only up to sign.
Time Complexity: O(N³)
Parameters
aTensor- Input square matrix of shape (N, N)
Returns
[ Tensor, Tensor ][T, Q] where T is the Schur form and Q contains the Schur vectors
Throws
ShapeErrorIf input is not a square 2D matrixDTypeErrorIf input has string or complex dtypeDataValidationErrorIf input contains non-finite values (NaN, Infinity)ConvergenceErrorIf the QR iteration does not converge
import { schur } from 'deepbox/linalg';import { tensor } from 'deepbox/ndarray'; const A = tensor([[1, 2], [3, 4]]);const [T, Q] = schur(A);// Q * T * Q^T ≈ A// T is quasi-upper triangularsvd
function svd(a: Tensor, fullMatrices?: boolean): [ Tensor, Tensor, Tensor ];Singular Value Decomposition.
Details
Factorizes matrix A into three matrices: A = U * Σ * V^T
Algorithm: Golub–Reinsch. Householder reflections reduce A to bidiagonal form, then implicit-shift QR iteration diagonalizes the bidiagonal matrix. Wide matrices (M < N) are handled by decomposing Aᵀ and swapping the factors.
Numerical Stability: Works on A directly and never forms AᵀA, so small singular values keep their relative accuracy far better than a normal-equations approach. Matrices with extreme entry magnitudes are prescaled by a power of two (an exact operation). When fullMatrices is true, the extra columns of U (or rows of Vᵀ) are an orthonormal completion and are not unique.
Parameters:
Parameters
aTensor- Input matrix of shape (M, N)
fullMatricesoptionalboolean- If true (default), U has shape (M, M) and Vt has shape (N, N). If false, U has shape (M, K) and Vt has shape (K, N) where K = min(M, N) Returns: [U, s, Vt] - U: Left singular vectors of shape (M, M) or (M, K) - s: Singular values of shape (K,) in descending order - Vt: Right singular vectors transposed, of shape (N, N) or (K, N) Requirements: - Input must be 2D matrix - Works with any M x N matrix, including rank-deficient and empty ones Properties: - A = U @ diag(s) @ Vt (with the first K columns of U and rows of Vt) - U and Vt have orthonormal columns and rows respectively - Singular values are non-negative and sorted in descending order - Singular vectors are unique only up to a common sign flip per pair
Returns
[ Tensor, Tensor, Tensor ]
Throws
ShapeErrorIf input is not 2DDTypeErrorIf input has string or complex dtypeDataValidationErrorIf input contains non-finite values (NaN, Infinity)ConvergenceErrorIf the QR iteration does not converge
import { svd } from 'deepbox/linalg';import { tensor } from 'deepbox/ndarray'; const A = tensor([[1, 2], [3, 4], [5, 6]]);const [U, s, Vt] = svd(A); console.log(U.shape); // [3, 3] if fullMatrices=true, [3, 2] if falseconsole.log(s.shape); // [2]console.log(Vt.shape); // [2, 2] // Reconstruction: A ≈ U[:, :2] @ diag(s) @ Vtsvdvals
function svdvals(a: Tensor): Tensor;Singular values of a only (no U/V).
Skips the accumulation of the singular vectors, so it is considerably cheaper than svd. Entries of extreme magnitude are prescaled by a power of two, as in svd.
Parameters
aTensor- Input matrix of shape (M, N)
Returns
TensorSingular values of shape (min(M, N),) in descending order
Throws
ShapeErrorIf input is not 2DDTypeErrorIf input has string or complex dtypeDataValidationErrorIf input contains non-finite values (NaN, Infinity)ConvergenceErrorIf the QR iteration does not converge
import { svdvals } from 'deepbox/linalg';import { tensor } from 'deepbox/ndarray'; const s = svdvals(tensor([[3, 0], [0, -2]]));// [3, 2]import { cholesky, eig, hessenberg, lu, polar, qr, schur, svd } from "deepbox/linalg";import { tensor } from "deepbox/ndarray"; const a = tensor([ [4, 1], [1, 3],]); const [q, r] = qr(a);const [u, s, vt] = svd(a);const [values, vectors] = eig(a);const [p, l, upper] = lu(a); console.log(q.shape, r.shape, u.shape, vt.shape);console.log(s.toString(), values.toString(), vectors.shape);console.log(p.shape, l.shape, upper.shape);console.log(cholesky(a).toString());console.log(schur(a)[0].shape, hessenberg(a)[0].shape, polar(a)[0].shape);Found a mistake? Open an issue.