mathematicskit.linalg#
LU decomposition with partial pivoting (scipy.linalg.lu); QR via
Householder reflections (scipy.linalg.qr) and a hand-rolled
Gram-Schmidt comparison; Cholesky decomposition (numpy.linalg.cholesky);
eigenvalue computation (numpy.linalg.eigh/eig) alongside
hand-rolled power/inverse iteration; SVD (numpy.linalg.svd);
conjugate gradient and GMRES (scipy.sparse.linalg.cg/gmres);
condition-number estimation (numpy.linalg.cond) and a
least-squares stability comparison (normal equations vs. QR vs.
numpy.linalg.lstsq).
mathematicskit.linalg: numerical linear algebra, built on numpy.linalg/scipy.linalg/scipy.sparse.linalg.
LU decomposition with partial pivoting (scipy.linalg.lu()); QR
decomposition via Householder reflections (scipy.linalg.qr()),
with a hand-rolled Gram-Schmidt variant kept only for the pedagogical
Householder-vs-Gram-Schmidt stability comparison; Cholesky decomposition
for SPD matrices (numpy.linalg.cholesky()); eigenvalue computation
(numpy.linalg.eigh()/numpy.linalg.eig()); power iteration and
inverse iteration for dominant/nearest eigenvalues (hand-rolled – the
iteration itself is the pedagogical subject, with no library equivalent);
SVD (numpy.linalg.svd()); conjugate gradient and GMRES
(scipy.sparse.linalg.cg()/gmres());
condition number (numpy.linalg.cond()) and least-squares
(numpy.linalg.lstsq()), with a comparison utility showing why
normal equations are less stable than QR for ill-conditioned systems.
Also: Cramer’s rule (numpy.linalg.slogdet()); characteristic
polynomials (numpy.poly()) and the Cayley-Hamilton theorem;
Jacobi, Gauss-Seidel, and SOR stationary iterations (hand-rolled – no
library equivalent); the Moore-Penrose pseudoinverse
(numpy.linalg.pinv()); Gershgorin discs; Lanczos extreme
eigenpairs (scipy.sparse.linalg.eigsh()); and Hessenberg/Schur
forms (scipy.linalg.hessenberg()/schur()).
mathematicskit’s systems/ classes/functions wrap these library calls in the
package’s own dataclass results, docstrings, and stability commentary –
they are thin, well-tested wrappers, not reimplementations of already-
correct numerical primitives.
- class mathematicskit.linalg.CholeskyResult(L)[source]#
Bases:
objectOutput of a Cholesky decomposition of an SPD matrix:
A = L L^T.- Parameters:
L (ndarray)
- class mathematicskit.linalg.ConjugateGradient(tol=1e-08, max_iter=1000)[source]#
Bases:
IterativeLinearSolverConjugate gradient method for symmetric positive-definite systems.
Minimizes the quadratic \(\phi(x) = \tfrac12 x^T A x - b^T x\) (equivalent to solving
A x = bfor SPDA) by generating search directions \(A\)-conjugate to all previous ones, which guarantees exact convergence in at mostniterations in exact arithmetic. Callsscipy.sparse.linalg.cg()directly. See Golub & Van Loan, Matrix Computations, 4th ed., Ch. 11.3, Algorithm 11.3.1.- Parameters:
tol (
float) – Relative residual-norm tolerance,||r_k|| < tol * ||b||(passed asrtoltoscipy.sparse.linalg.cg()).max_iter (
int) – Maximum number of iterations (defaults toDEFAULT_MAX_ITER;nis normally sufficient).
Examples
>>> import numpy as np >>> A = np.array([[4.0, 1.0], [1.0, 3.0]]) >>> b = np.array([1.0, 2.0]) >>> result = ConjugateGradient().solve(A, b) >>> np.allclose(A @ result.x, b, atol=1e-8) True
- class mathematicskit.linalg.EigenResult(eigenvalues, eigenvectors, iterations=0, converged=True, method='', extra=<factory>)[source]#
Bases:
objectOutput of an eigenvalue computation, whole-spectrum or single-pair.
The whole-spectrum solvers (
eigen_symmetric(),eigen_general()) fill eigenvalues/eigenvectors with the full spectrum in one shot and reportiterations=1, converged=True, since LAPACK does not expose its internal iteration count. The hand-rolled iterations (power_iteration(),inverse_iteration()) return a single eigenpair and a genuine iteration count and convergence flag.- Parameters:
- converged: bool = True#
Whether the convergence tolerance was met (always
Truefor the direct library solvers, which do not iterate visibly).- Type:
- eigenvalues: ndarray#
The whole spectrum, shape (n,), for the library-backed solvers; a single dominant (or shift-selected) eigenvalue, shape (1,), for power/inverse iteration. Real and ascending from
eigh, possibly complex and unordered fromeig.- Type:
ndarray
- eigenvectors: ndarray#
Shape (n, n) with eigenvectors as columns for the whole-spectrum solvers, so
eigenvectors[:, k]pairs witheigenvalues[k]; a single unit eigenvector of shape (n,) for power/inverse iteration.- Type:
ndarray
- class mathematicskit.linalg.GMRES(tol=1e-08, max_iter=1000)[source]#
Bases:
IterativeLinearSolverGMRES: Generalized Minimal RESidual method for general (non-symmetric) systems.
Builds an orthonormal basis of the Krylov subspace via the Arnoldi process and picks the iterate minimizing the residual norm over it. Unlike conjugate gradient, GMRES applies to any nonsingular
A(not just SPD). Callsscipy.sparse.linalg.gmres()directly withrestartcapped atn(so a single restart cycle already spans the full Krylov space, i.e. “full GMRES”, matching the textbook presentation). See Saad & Schultz (1986), GMRES: A Generalized Minimal Residual Algorithm for Solving Nonsymmetric Linear Systems, SIAM J. Sci. Stat. Comput. 7(3), and Golub & Van Loan, Matrix Computations, 4th ed., Ch. 9.3.3.- Parameters:
Examples
>>> import numpy as np >>> A = np.array([[4.0, 1.0, 0.0], [1.0, 3.0, 1.0], [0.0, 2.0, 5.0]]) >>> b = np.array([1.0, 2.0, 3.0]) >>> result = GMRES().solve(A, b) >>> np.allclose(A @ result.x, b, atol=1e-8) True
- class mathematicskit.linalg.GaussSeidel(tol=1e-08, max_iter=1000)[source]#
Bases:
SORGauss-Seidel iteration (Gauss 1823, Seidel 1874):
SORwithomega = 1.Each unknown is solved from its own equation using the newest values of the unknowns already updated in this sweep. Converges for strictly diagonally dominant or symmetric positive-definite
A.Examples
>>> import numpy as np >>> A = np.array([[4.0, 1.0], [1.0, 3.0]]) >>> b = np.array([1.0, 2.0]) >>> result = GaussSeidel(tol=1e-12).solve(A, b) >>> np.round(result.x, 8) array([0.09090909, 0.63636364])
- method = 'gauss_seidel'#
- class mathematicskit.linalg.GershgorinResult(centers, radii)[source]#
Bases:
objectThe Gershgorin discs of a square matrix.
Disc \(k\) is centered at \(a_{kk}\) with radius \(R_k = \sum_{j \ne k} |a_{kj}|\); every eigenvalue lies in the union of the discs.
- class mathematicskit.linalg.IterativeLinearSolver(tol=1e-08, max_iter=1000)[source]#
Bases:
ABCCommon base for Krylov-subspace iterative linear-system solvers.
- Parameters:
- class mathematicskit.linalg.IterativeSolveResult(x, residual_history, iterations=0, converged=True, method='')[source]#
Bases:
objectOutput of an iterative linear-system solver (CG, GMRES).
- residual_history: ndarray#
The relative residual norm
||b - A x_k|| / ||b||at each iterate, starting from the initial guess. Normalizing by||b||is what makes the history directly comparable to method’stol(itself a relative tolerance) and comparable across right-hand sides of different magnitudes.- Type:
ndarray, shape (iterations + 1,)
- class mathematicskit.linalg.JacobiIteration(tol=1e-08, max_iter=1000)[source]#
Bases:
SORJacobi iteration (Jacobi 1845): every unknown updated simultaneously from the previous iterate.
\(x_{k+1} = D^{-1}(b - (L + U) x_k)\). Simpler and fully parallel, but for consistently ordered matrices it needs about twice as many sweeps as Gauss-Seidel.
Examples
>>> import numpy as np >>> A = np.array([[4.0, 1.0], [1.0, 3.0]]) >>> b = np.array([1.0, 2.0]) >>> result = JacobiIteration(tol=1e-12).solve(A, b) >>> np.allclose(A @ result.x, b) True
- method = 'jacobi'#
- class mathematicskit.linalg.LUResult(L, U, P, num_row_swaps=0)[source]#
Bases:
objectOutput of an LU decomposition with partial pivoting:
P A = L U.
- class mathematicskit.linalg.LeastSquaresResult(coefficients, residual_norm=0.0, method='', condition_number=None)[source]#
Bases:
objectOutput of a least-squares solve (normal equations or QR-based).
- Parameters:
- class mathematicskit.linalg.QRResult(Q, R, method='')[source]#
Bases:
objectOutput of a QR decomposition:
A = Q R.
- class mathematicskit.linalg.SOR(omega=1.0, tol=1e-08, max_iter=1000)[source]#
Bases:
IterativeLinearSolverSuccessive over-relaxation (Frankel 1950, Young 1950).
One sweep updates the unknowns in order, blending the Gauss-Seidel value with the old one:
\[x_i \leftarrow (1 - \omega)\, x_i + \frac{\omega}{a_{ii}} \Big(b_i - \sum_{j < i} a_{ij} x_j^{\text{new}} - \sum_{j > i} a_{ij} x_j^{\text{old}}\Big).\]omega = 1is Gauss-Seidel;1 < omega < 2over-relaxes. For symmetric positive-definiteAit converges for every0 < omega < 2(Ostrowski-Reich).- Parameters:
Examples
>>> import numpy as np >>> A = np.array([[4.0, 1.0], [1.0, 3.0]]) >>> b = np.array([1.0, 2.0]) >>> result = SOR(omega=1.1, tol=1e-12).solve(A, b) >>> np.allclose(A @ result.x, b), result.method (True, 'sor')
- method = 'sor'#
- class mathematicskit.linalg.SVDResult(U, S, Vt)[source]#
Bases:
objectOutput of a singular value decomposition:
A = U diag(S) V^T.
- class mathematicskit.linalg.SchurResult(T, Z)[source]#
Bases:
objectOutput of a Schur decomposition:
A = Z T Z^H.- T: ndarray#
Schur form – upper triangular (complex output) or quasi-upper-triangular with 2x2 blocks for complex-conjugate eigenvalue pairs (real output). Its diagonal (or block) entries are the eigenvalues of
A.
- property eigenvalues: ndarray#
Eigenvalues read off
T(2x2 diagonal blocks resolved withnumpy.linalg.eigvals()).- Type:
ndarray, shape (n,)
- mathematicskit.linalg.characteristic_polynomial(a)[source]#
Coefficients of \(p_A(\lambda) = \det(\lambda I - A)\), highest degree first, via
numpy.poly().The polynomial is monic, the coefficient of \(\lambda^{n-1}\) is \(-\operatorname{tr} A\), and the constant term is \((-1)^n \det A\).
- Parameters:
a (
ndarray)- Return type:
- Returns:
ndarray, shape (n + 1,) – Real if
ais real (imaginary round-off from complex-conjugate eigenvalue pairs is discarded).
Examples
>>> import numpy as np >>> A = np.array([[1.0, 2.0], [3.0, 4.0]]) >>> np.round(characteristic_polynomial(A), 8) # l^2 - 5 l - 2 array([ 1., -5., -2.])
- mathematicskit.linalg.cholesky_decompose(a)[source]#
Factor a symmetric positive-definite
A = L L^Tvianumpy.linalg.cholesky().Roughly half the cost of a general LU factorization (exploits symmetry); the underlying LAPACK routine (
?potrf) raises when a diagonal pivot would require a square root of a negative number, which doubles as a certificate thatais not positive-definite. See Burden & Faires, Numerical Analysis, 10th ed., Ch. 6.6, Theorem 6.25.- Parameters:
a (
ndarray) – Symmetric positive-definite matrix.- Return type:
- Returns:
CholeskyResult
Examples
>>> import numpy as np >>> A = np.array([[4.0, 2.0], [2.0, 5.0]]) >>> result = cholesky_decompose(A) >>> np.allclose(result.L @ result.L.T, A) True
- mathematicskit.linalg.cholesky_solve(result, b)[source]#
Solve
A x = bgiven a precomputedCholeskyResult.Forward substitution with
L, then back substitution withL^T, viascipy.linalg.solve_triangular().- Parameters:
result (
CholeskyResult)b (
ndarray)
- Return type:
- Returns:
ndarray, shape (n,)
Examples
>>> import numpy as np >>> A = np.array([[4.0, 2.0], [2.0, 5.0]]) >>> b = np.array([2.0, 1.0]) >>> x = cholesky_solve(cholesky_decompose(A), b) >>> np.allclose(A @ x, b) True
- mathematicskit.linalg.condition_number_2norm(a)[source]#
2-norm condition number \(\kappa_2(A) = \sigma_{\max}/\sigma_{\min}\), via
numpy.linalg.cond().A large \(\kappa_2\) means small relative perturbations in
Aorbcan produce large relative changes in the solution ofA x = b– seeleast_squares_normal_equations()for why this matters concretely for least squares. See Trefethen & Bau, Numerical Linear Algebra, 1997, Lecture 12.Examples
>>> import numpy as np >>> round(condition_number_2norm(np.diag([10.0, 1.0])), 6) 10.0
- mathematicskit.linalg.cramer_solve(a, b)[source]#
Solve
A x = bby Cramer’s rule, \(x_i = \det(A_i) / \det(A)\).\(A_i\) is
Awith itsi-th column replaced byb.- Parameters:
- Return type:
- Returns:
ndarray, shape (n,)
- Raises:
numpy.linalg.LinAlgError – If
det(A)is (numerically) zero.
Examples
>>> import numpy as np >>> A = np.array([[2.0, 1.0], [1.0, 3.0]]) >>> b = np.array([3.0, 5.0]) >>> np.round(cramer_solve(A, b), 8) array([0.8, 1.4])
- mathematicskit.linalg.eigen_general(a)[source]#
Eigenvalues/eigenvectors of a general (possibly non-symmetric) matrix, via
numpy.linalg.eig().Eigenvalues/eigenvectors may be complex even for a real input matrix (e.g. a rotation matrix). See Golub & Van Loan, Matrix Computations, 4th ed., Ch. 7.
- Parameters:
a (
ndarray)- Return type:
- Returns:
EigenResult –
method="numpy_eig".
Examples
>>> import numpy as np >>> A = np.array([[0.0, -1.0], [1.0, 0.0]]) # 90-degree rotation >>> result = eigen_general(A) >>> np.round(np.sort(np.abs(result.eigenvalues.imag)), 8) array([1., 1.])
- mathematicskit.linalg.eigen_symmetric(a)[source]#
Eigenvalues/eigenvectors of a symmetric (Hermitian) matrix, via
numpy.linalg.eigh().eighexploits symmetry to guarantee real eigenvalues and an orthonormal eigenvector basis, returned in ascending order, at roughly half the cost of the general eigenvalue problem. See Golub & Van Loan, Matrix Computations, 4th ed., Ch. 8.- Parameters:
a (
ndarray) – Symmetric matrix.- Return type:
- Returns:
EigenResult –
eigenvaluesshape (n,) ascending,eigenvectorsshape (n, n) with columns the corresponding eigenvectors,method="numpy_eigh".
Examples
>>> import numpy as np >>> A = np.array([[2.0, 1.0], [1.0, 2.0]]) >>> result = eigen_symmetric(A) >>> np.round(result.eigenvalues, 8) array([1., 3.])
- mathematicskit.linalg.frobenius_norm(a)[source]#
Frobenius norm \(\|A\|_F = \sqrt{\sum_{ij} A_{ij}^2}\), via
numpy.linalg.norm().Examples
>>> import numpy as np >>> round(frobenius_norm(np.array([[3.0, 0.0], [0.0, 4.0]])), 6) 5.0
- mathematicskit.linalg.gershgorin_discs(a, by='rows')[source]#
The Gershgorin discs of a square matrix.
Disc \(k\) has center \(a_{kk}\) and radius \(R_k = \sum_{j \ne k} |a_{kj}|\) (
by="rows") or \(\sum_{j \ne k} |a_{jk}|\) (by="columns"). A consequence: a strictly diagonally dominant matrix (every \(|a_{kk}| > R_k\)) has no eigenvalue at 0 and is therefore nonsingular.- Parameters:
- Return type:
- Returns:
GershgorinResult
Examples
>>> import numpy as np >>> A = np.array([[10.0, 1.0, 0.0], [0.2, 8.0, 0.2], [1.0, 1.0, 2.0]]) >>> discs = gershgorin_discs(A) >>> discs.centers, discs.radii (array([10., 8., 2.]), array([1. , 0.4, 2. ])) >>> all(discs.contains(lam) for lam in np.linalg.eigvals(A)) True
- mathematicskit.linalg.gram_schmidt_qr(a, modified=True)[source]#
QR decomposition by (modified or classical) Gram-Schmidt.
Orthogonalizes
A’s columns one at a time against the previously-computedQcolumns. Modified Gram-Schmidt (modified=True, the default) subtracts each projection immediately as it’s computed rather than all at once from the original vector (classical Gram-Schmidt,modified=False) – algebraically equivalent in exact arithmetic, but modified Gram-Schmidt loses far less orthogonality in floating point, especially for near-collinear columns. See Trefethen & Bau, Numerical Linear Algebra, 1997, Lecture 8, andorthogonality_error()for a direct numerical comparison.- Parameters:
- Return type:
- Returns:
QRResult – With
method="gram_schmidt".
Examples
>>> import numpy as np >>> A = np.array([[1.0, -1.0], [1.0, 1.0], [0.0, 1.0]]) >>> result = gram_schmidt_qr(A) >>> np.allclose(result.Q @ result.R, A) True
- mathematicskit.linalg.hessenberg_reduce(a)[source]#
Upper-Hessenberg form
A = Q H Q^Tviascipy.linalg.hessenberg().His zero below its first subdiagonal. Reducing to it first (by Householder reflections) cuts each QR step from \(O(n^3)\) to \(O(n^2)\), and the QR iteration preserves the Hessenberg shape.- Parameters:
a (
ndarray)- Return type:
- Returns:
H (ndarray, shape (n, n))
Q (ndarray, shape (n, n)) – Orthogonal (unitary) similarity.
Examples
>>> import numpy as np >>> A = np.arange(16.0).reshape(4, 4) + np.eye(4) >>> H, Q = hessenberg_reduce(A) >>> bool(np.allclose(np.tril(H, -2), 0.0)), bool(np.allclose(Q @ H @ Q.T, A)) (True, True)
- mathematicskit.linalg.householder_qr(a)[source]#
QR decomposition by Householder reflections, via
scipy.linalg.qr().Each column below the diagonal is zeroed by an orthogonal reflector \(H_k = I - 2 v_k v_k^T / (v_k^T v_k)\) chosen to map that column’s trailing part onto a multiple of \(e_1\); the product of all the \(H_k\) is
Q.mode="economic"requests the thin factorization (Qshape (m, n) rather than (m, m)) to matchgram_schmidt_qr()’s shape. See Trefethen & Bau, Numerical Linear Algebra, 1997, Lecture 10.- Parameters:
a (
ndarray) – Full-column-rank matrix.- Return type:
- Returns:
QRResult – With
method="householder",Qshape (m, n),Rshape (n, n).
Examples
>>> import numpy as np >>> A = np.array([[1.0, -1.0], [1.0, 1.0], [0.0, 1.0]]) >>> result = householder_qr(A) >>> np.allclose(result.Q @ result.R, A) True >>> np.allclose(result.Q.T @ result.Q, np.eye(2), atol=1e-10) True
- mathematicskit.linalg.inverse_iteration(a, mu, tol=1e-12, max_iter=1000, v0=None)[source]#
Inverse iteration for the eigenvalue nearest a shift
mu.Power iteration applied to \((A - \mu I)^{-1}\): since that matrix’s dominant eigenvalue is \(1/(\lambda - \mu)\) for the \(\lambda\) closest to
mu, iterating it converges to the corresponding eigenvector, with the Rayleigh quotient of the originalAgiving that eigenvalue directly. The linear solve at each step usesnumpy.linalg.solve()rather than an explicit inverse; only the iteration itself is hand-rolled. See Burden & Faires, Numerical Analysis, 10th ed., Ch. 9.3, and Golub & Van Loan, Matrix Computations, 4th ed., Ch. 8.2.2.- Parameters:
- Return type:
- Returns:
EigenResult
Examples
>>> import numpy as np >>> A = np.array([[2.0, 0.0], [0.0, 5.0]]) >>> result = inverse_iteration(A, mu=4.5) >>> round(float(result.eigenvalues[0]), 6) 5.0
- mathematicskit.linalg.is_symmetric(a, atol=1e-10)[source]#
Check whether a matrix is (numerically) symmetric.
Examples
>>> import numpy as np >>> is_symmetric(np.array([[1.0, 2.0], [2.0, 1.0]])) True
- mathematicskit.linalg.is_symmetric_positive_definite(a)[source]#
Check symmetric positive-definiteness by attempting a Cholesky factorization.
Examples
>>> import numpy as np >>> is_symmetric_positive_definite(np.array([[4.0, 2.0], [2.0, 5.0]])) True >>> is_symmetric_positive_definite(np.array([[1.0, 2.0], [2.0, 1.0]])) False
- mathematicskit.linalg.jacobi_spectral_radius(a)[source]#
Spectral radius \(\rho(G_J)\) of the Jacobi iteration matrix \(G_J = I - D^{-1} A\).
Computed with
numpy.linalg.eigvals(). The Jacobi iteration converges from every starting vector iff this is below 1.Examples
>>> import numpy as np >>> n = 5 >>> A = 2 * np.eye(n) - np.eye(n, k=1) - np.eye(n, k=-1) >>> bool(np.isclose(jacobi_spectral_radius(A), np.cos(np.pi / (n + 1)))) True
- mathematicskit.linalg.lanczos_eigsh(a, k=6, which='LA', tol=0.0, sigma=None)[source]#
The
kextreme eigenpairs of a symmetric matrix, viascipy.sparse.linalg.eigsh()(Lanczos/ARPACK).Lanczos builds an orthonormal basis \(Q_m\) of the Krylov subspace \(\operatorname{span}\{v, Av, \dots, A^{m-1}v\}\) with a three-term recurrence, in which \(T_m = Q_m^T A Q_m\) is tridiagonal; the eigenvalues of \(T_m\) (Ritz values) converge first to the extreme eigenvalues of
A.- Parameters:
a (ndarray, sparse matrix, or LinearOperator, shape (n, n)) – Symmetric (Hermitian) matrix.
k (
int) – Number of eigenpairs,k < n.which (
str) – Largest/smallest algebraic, largest/smallest magnitude, or both ends of the spectrum.tol (
float) – Relative accuracy for the Ritz values;0means machine precision.sigma (
float|None) – Shift-invert mode: run Lanczos on \((A - \sigma I)^{-1}\) (factored once by sparse LU), which returns thekeigenvalues nearestsigmawhenwhich="LM". The standard route to interior or tightly clustered eigenvalues, which plain Lanczos resolves only slowly.
- Return type:
- Returns:
EigenResult –
eigenvaluesshape (k,) ascending,eigenvectorsshape (n, k),method="lanczos_arpack".
Examples
>>> import numpy as np >>> A = np.diag(np.arange(1.0, 21.0)) >>> np.round(lanczos_eigsh(A, k=3).eigenvalues, 8) array([18., 19., 20.]) >>> np.round(lanczos_eigsh(A, k=2, sigma=7.2, which="LM").eigenvalues, 8) array([7., 8.])
- mathematicskit.linalg.least_squares_lstsq(a, b)[source]#
Least squares via
numpy.linalg.lstsq()(SVD-based, via LAPACK?gelsd).The most robust of the three routes here – it handles rank-deficient
agracefully (returning the minimum-norm solution) where bothleast_squares_normal_equations()andleast_squares_qr()would fail or become ill-conditioned – because it never needsato have full column rank in the first place. See Trefethen & Bau, Numerical Linear Algebra, 1997, Lecture 11.- Parameters:
- Return type:
- Returns:
LeastSquaresResult –
method="lstsq".
Examples
>>> import numpy as np >>> A = np.array([[1.0, 1.0], [1.0, 2.0], [1.0, 3.0]]) >>> b = np.array([2.0, 3.0, 5.0]) >>> result = least_squares_lstsq(A, b) >>> np.round(result.coefficients, 4) array([0.3333, 1.5 ])
- mathematicskit.linalg.least_squares_normal_equations(a, b)[source]#
Least squares via the normal equations \(A^T A x = A^T b\).
A^T AandA^T bare formed explicitly and solved withnumpy.linalg.solve(). Simple, but \(\kappa_2(A^T A) = \kappa_2(A)^2\) – squaring the condition number ofAitself – so this can lose roughly twice as many digits of accuracy as the QR-based approach (least_squares_qr()) for an ill-conditionedA. See Trefethen & Bau, Numerical Linear Algebra, 1997, Lecture 11.- Parameters:
- Return type:
- Returns:
LeastSquaresResult
Examples
>>> import numpy as np >>> A = np.array([[1.0, 1.0], [1.0, 2.0], [1.0, 3.0]]) >>> b = np.array([2.0, 3.0, 5.0]) >>> result = least_squares_normal_equations(A, b) >>> np.round(result.coefficients, 4) array([0.3333, 1.5 ])
- mathematicskit.linalg.least_squares_qr(a, b)[source]#
Least squares via
A = Q R(scipy.linalg.qr()): solveR x = Q^T b.Avoids ever forming \(A^T A\), so the effective conditioning is \(\kappa_2(A)\) rather than its square – the standard remedy for
least_squares_normal_equations()’s instability. (For very ill-conditioned or rank-deficient problems,numpy.linalg.lstsq()– SVD-based – is even more robust; the QR route is used here specifically to make the comparison with the normal equations concrete.) See Trefethen & Bau, Numerical Linear Algebra, 1997, Lecture 11.- Parameters:
- Return type:
- Returns:
LeastSquaresResult
Examples
>>> import numpy as np >>> A = np.array([[1.0, 1.0], [1.0, 2.0], [1.0, 3.0]]) >>> b = np.array([2.0, 3.0, 5.0]) >>> result = least_squares_qr(A, b) >>> np.round(result.coefficients, 4) array([0.3333, 1.5 ])
- mathematicskit.linalg.lu_decompose(a)[source]#
Factor
P A = L Uby Gaussian elimination with partial pivoting.Thin wrapper around
scipy.linalg.lu(), which brings the largest-magnitude entry in each column to the pivot position by a row swap before eliminating below it – the standard remedy for the numerical instability of naive (unpivoted) Gaussian elimination.scipy.linalg.lu()returns a permutation matrixP0withA = P0 L U; this function returns its transpose so that, matching the textbook convention,result.P @ a == result.L @ result.U. See Burden & Faires, Numerical Analysis, 10th ed., Ch. 6.5 (“Matrix Factorizations”), and Trefethen & Bau, Numerical Linear Algebra, 1997, Lecture 21.Examples
>>> import numpy as np >>> A = np.array([[2.0, 1.0, 1.0], [4.0, 3.0, 3.0], [8.0, 7.0, 9.0]]) >>> result = lu_decompose(A) >>> np.allclose(result.P @ A, result.L @ result.U) True >>> # Singularity is judged from U's pivots, not from det(A), which >>> # underflows to 0.0 for large well-conditioned matrices. >>> np.allclose(lu_decompose(0.01 * np.eye(200)).U, 0.01 * np.eye(200)) True
- mathematicskit.linalg.lu_det(a)[source]#
Determinant via LU decomposition: \(\det A = (-1)^s \prod_i U_{ii}\).
Thin wrapper around
scipy.linalg.det(), which itself factorsavia LAPACK’s?getrfand takes the signed product ofU’s diagonal – \(O(n^3)\), versus the \(O(n!)\) cofactor expansion. See Burden & Faires, Numerical Analysis, 10th ed., Ch. 6.5.Examples
>>> import numpy as np >>> A = np.array([[1.0, 2.0], [3.0, 4.0]]) >>> round(lu_det(A), 10) -2.0
- mathematicskit.linalg.lu_solve(result, b)[source]#
Solve
A x = bgiven a precomputedLUResultforA.Solves
L y = P bthenU x = yby triangular substitution (scipy.linalg.solve_triangular(), \(O(n^2)\)) rather than refactoring at \(O(n^3)\) – the whole point of reusing a factorization across several right-hand sides.- Parameters:
result (
LUResult) – Fromlu_decompose().b (
ndarray)
- Return type:
- Returns:
ndarray, shape (n,)
Examples
>>> import numpy as np >>> A = np.array([[2.0, 1.0], [1.0, 3.0]]) >>> b = np.array([3.0, 5.0]) >>> x = lu_solve(lu_decompose(A), b) >>> np.allclose(A @ x, b) True
- mathematicskit.linalg.lu_solve_system(a, b)[source]#
Convenience: factor
aand solve in one call.Uses
scipy.linalg.lu_factor()/scipy.linalg.lu_solve()directly (LAPACK’s?getrf/?getrspair) rather than routing throughlu_decompose()’s explicitP/L/U, which is marginally faster since it skips reconstructing the dense factors.Examples
>>> import numpy as np >>> A = np.array([[3.0, 2.0, -1.0], [2.0, -2.0, 4.0], [-1.0, 0.5, -1.0]]) >>> x_true = np.array([1.0, -2.0, -2.0]) >>> b = A @ x_true >>> np.allclose(lu_solve_system(A, b), x_true, atol=1e-8) True
- mathematicskit.linalg.matrix_polynomial(coeffs, a)[source]#
Evaluate \(p(A) = c_0 A^d + c_1 A^{d-1} + \dots + c_d I\) by Horner’s rule.
With
coeffs = characteristic_polynomial(a)the result is the zero matrix, up to rounding – the Cayley-Hamilton theorem.- Parameters:
coeffs (
ndarray) – Polynomial coefficients, highest degree first (thenumpy.polyval()convention).a (
ndarray)
- Return type:
- Returns:
ndarray, shape (n, n)
Examples
>>> import numpy as np >>> A = np.array([[1.0, 2.0], [3.0, 4.0]]) >>> np.allclose(matrix_polynomial(characteristic_polynomial(A), A), 0.0) True
- mathematicskit.linalg.optimal_sor_omega(a)[source]#
Young’s optimal SOR relaxation factor \(\omega^* = 2 / (1 + \sqrt{1 - \rho(G_J)^2})\).
Exact for consistently ordered matrices whose Jacobi iteration matrix has real eigenvalues with \(\rho(G_J) < 1\) (e.g. the discrete Poisson matrices); at \(\omega^*\) the SOR spectral radius drops to \(\omega^* - 1\). See D. M. Young, Trans. AMS 76 (1954), 92-111.
Examples
>>> import numpy as np >>> A = 2 * np.eye(3) - np.eye(3, k=1) - np.eye(3, k=-1) # rho_J = cos(pi/4) >>> round(optimal_sor_omega(A), 6) # 2 / (1 + sqrt(1 - cos(pi/4)^2)) 1.171573
- mathematicskit.linalg.orthogonality_error(q)[source]#
Measure how far
Q’s columns are from exactly orthonormal.\(\max_{ij}\left|(Q^TQ - I)_{ij}\right|\): the largest entry of
Q’s Gram matrix minus the identity. (This is the entrywise max norm, not the induced \(\infty\)-norm, which would be the largest row sum.) Exactly zero for a perfectly orthonormalQ, and a direct numerical demonstration of which QR method loses orthogonality on ill-conditioned input: on a degree-12 Vandermonde matrix, classical Gram-Schmidt reaches order 1, modified Gram-Schmidt stays near 1e-10, and Householder near machine epsilon. See Trefethen & Bau, Numerical Linear Algebra, 1997, Lecture 9 (Figure 9.2’s classical-vs-modified comparison).Examples
>>> import numpy as np >>> orthogonality_error(np.eye(3)) == 0.0 True
- mathematicskit.linalg.penrose_residuals(a, x)[source]#
Frobenius-norm residuals of Penrose’s four equations for a candidate
X = A^+.\[AXA = A, \qquad XAX = X, \qquad (AX)^H = AX, \qquad (XA)^H = XA.\]Penrose (1955) proved exactly one
Xsatisfies all four.- Parameters:
- Return type:
- Returns:
ndarray, shape (4,) –
||AXA - A||_F,||XAX - X||_F,||(AX)^H - AX||_F,||(XA)^H - XA||_F.
Examples
>>> import numpy as np >>> A = np.array([[1.0, 2.0], [2.0, 4.0], [0.0, 1.0]]) >>> bool(np.all(penrose_residuals(A, pseudoinverse(A)) < 1e-12)) True
- mathematicskit.linalg.power_iteration(a, tol=1e-12, max_iter=1000, v0=None)[source]#
Power iteration for the dominant (largest-magnitude) eigenpair.
\(v_{k+1} = A v_k / \|A v_k\|\) converges to the eigenvector of the eigenvalue of largest magnitude (assuming it is unique and the initial vector has a nonzero component along it), with the Rayleigh quotient \(v_k^T A v_k\) converging to the eigenvalue itself. No library equivalent for the iteration itself – watching this converge is the point. See Burden & Faires, Numerical Analysis, 10th ed., Ch. 9.3.
- Parameters:
- Return type:
- Returns:
EigenResult –
eigenvaluesa length-1 array,eigenvectorsshape (n,).
Examples
>>> import numpy as np >>> A = np.array([[2.0, 0.0], [0.0, 5.0]]) >>> result = power_iteration(A) >>> round(float(result.eigenvalues[0]), 6) 5.0
- mathematicskit.linalg.pseudoinverse(a, rcond=None)[source]#
Moore-Penrose pseudoinverse \(A^+ = V \Sigma^+ U^T\), via
numpy.linalg.pinv().For any right-hand side
b, \(x = A^+ b\) is the minimum-norm least-squares solution ofA x = b– it exists and is unique even whenAis rectangular or rank-deficient.- Parameters:
- Return type:
- Returns:
ndarray, shape (n, m)
Examples
>>> import numpy as np >>> A = np.array([[1.0, 1.0], [1.0, 1.0]]) # rank 1 >>> pseudoinverse(A) array([[0.25, 0.25], [0.25, 0.25]])
- mathematicskit.linalg.random_spd_matrix(n, seed=0, condition_scale=1.0)[source]#
Generate a random symmetric positive-definite matrix.
Constructs \(A = M^T M + \epsilon I\) for a random \(M\), which is SPD for any nonsingular \(M\) (\(x^T A x = \|Mx\|^2 + \epsilon\|x\|^2 > 0\) for \(x \neq 0\)); condition_scale scales \(M\)’s entries to make the resulting matrix better- or worse-conditioned for stability demonstrations.
- Parameters:
- Return type:
- Returns:
ndarray, shape (n, n)
Examples
>>> import numpy as np >>> A = random_spd_matrix(4, seed=1) >>> np.allclose(A, A.T) True >>> bool(np.all(np.linalg.eigvalsh(A) > 0)) True
- mathematicskit.linalg.schur_decompose(a, output='real')[source]#
Schur decomposition
A = Z T Z^Hviascipy.linalg.schur().- Parameters:
- Return type:
- Returns:
SchurResult
Examples
>>> import numpy as np >>> A = np.array([[4.0, 1.0], [2.0, 3.0]]) # eigenvalues 2 and 5 >>> result = schur_decompose(A) >>> np.round(np.sort(np.diag(result.T)), 8) array([2., 5.]) >>> bool(np.allclose(result.Z @ result.T @ result.Z.T, A)) True
- mathematicskit.linalg.svd_decompose(a)[source]#
Compute the (thin) SVD
A = U diag(S) V^Tvianumpy.linalg.svd().full_matrices=Falserequests the thin/economy factorization (Ushape (m, k),Vtshape (k, n) withk = min(m, n)), which is what everymathematicskit.linalgcaller needs. Singular values are returned in descending order (numpy’s convention).Examples
>>> import numpy as np >>> A = np.array([[3.0, 0.0], [0.0, -2.0]]) >>> result = svd_decompose(A) >>> np.round(result.S, 6) array([3., 2.]) >>> np.allclose(result.U @ np.diag(result.S) @ result.Vt, A) True