SciPy vs NumPy Linalg¶
SciPy's scipy.linalg extends NumPy's linear algebra capabilities.
Mental Model
NumPy provides the essentials -- determinants, eigenvalues, SVD, and basic solves. SciPy's linalg is a strict superset that adds specialized decompositions (LU, QR, Schur, Hessenberg), matrix functions (expm, logm), and LAPACK-level control over algorithm parameters. When in doubt, import from scipy.linalg -- it covers everything NumPy does and more.
Module Comparison¶
1. Basic Import¶
```python import numpy as np from scipy import linalg
def main(): A = np.array([[1, 2], [3, 4]])
# Both modules provide similar functions
det_np = np.linalg.det(A)
det_sp = linalg.det(A)
print(f"NumPy det: {det_np}")
print(f"SciPy det: {det_sp}")
if name == "main": main() ```
2. Key Differences¶
| Feature | numpy.linalg |
scipy.linalg |
|---|---|---|
| Scope | Core operations | Comprehensive |
| Decompositions | Basic (SVD, QR, Cholesky) | Extended (LU, Schur, Hessenberg) |
| Matrix functions | None | expm, logm, sqrtm |
| Special matrices | Limited | Extensive |
| LAPACK access | Indirect | Direct |
| Sparse support | No | Via scipy.sparse.linalg |
3. When to Use Which¶
```python import numpy as np from scipy import linalg
def main(): A = np.array([[4, 2], [2, 5]])
# NumPy: simple operations
eigvals_np = np.linalg.eigvals(A)
print(f"NumPy eigvals: {eigvals_np}")
# SciPy: more control and options
eigvals_sp = linalg.eigvals(A)
print(f"SciPy eigvals: {eigvals_sp}")
if name == "main": main() ```
Extended Functionality¶
1. Decompositions¶
```python import numpy as np from scipy import linalg
def main(): A = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 10]])
# LU decomposition (SciPy only)
P, L, U = linalg.lu(A)
print("P (permutation):")
print(P)
print()
print("L (lower):")
print(L)
print()
print("U (upper):")
print(U)
if name == "main": main() ```
2. Matrix Functions¶
```python import numpy as np from scipy import linalg
def main(): A = np.array([[1, 2], [0, 1]])
# Matrix exponential (SciPy only)
exp_A = linalg.expm(A)
print("A =")
print(A)
print()
print("exp(A) =")
print(exp_A)
if name == "main": main() ```
3. Special Solvers¶
```python import numpy as np from scipy import linalg
def main(): # Triangular system (SciPy provides specialized solver) U = np.array([[2, 1, 3], [0, 4, 2], [0, 0, 5]]) b = np.array([10, 14, 15])
# Specialized triangular solver
x = linalg.solve_triangular(U, b)
print(f"x = {x}")
print(f"Verify U @ x = {U @ x}")
if name == "main": main() ```
LAPACK and BLAS Access¶
1. Low-Level Routines¶
```python import numpy as np from scipy import linalg
def main(): # Access LAPACK routines directly A = np.array([[1, 2], [3, 4]], dtype=float)
# Get LAPACK function for LU factorization
getrf = linalg.get_lapack_funcs('getrf', (A,))
lu, piv, info = getrf(A)
print(f"LU result:\n{lu}")
print(f"Pivot indices: {piv}")
print(f"Info: {info}")
if name == "main": main() ```
2. BLAS Operations¶
```python import numpy as np from scipy import linalg
def main(): # Access BLAS for optimized operations x = np.array([1, 2, 3], dtype=float) y = np.array([4, 5, 6], dtype=float)
# Get BLAS dot product function
ddot = linalg.get_blas_funcs('dot', (x, y))
result = ddot(x, y)
print(f"BLAS dot product: {result}")
print(f"NumPy dot product: {np.dot(x, y)}")
if name == "main": main() ```
Recommendation¶
1. Use NumPy When¶
- Basic operations (det, inv, solve, eig)
- Minimizing dependencies
- Simple scripts
2. Use SciPy When¶
- Advanced decompositions (LU, Schur, Hessenberg)
- Matrix functions (expm, logm, sqrtm)
- Specialized solvers (triangular, banded)
- Maximum performance via LAPACK
3. General Advice¶
```python
Recommended import pattern¶
import numpy as np from scipy import linalg
Use scipy.linalg as default for comprehensive functionality¶
```
Exercises¶
Exercise 1.
Solve the upper triangular system \(Ux = b\) where \(U = \begin{pmatrix} 3 & 1 & 2 \\ 0 & 5 & 4 \\ 0 & 0 & 2 \end{pmatrix}\) and \(b = (10, 13, 4)^T\) using scipy.linalg.solve_triangular. Verify the solution by comparing with np.linalg.solve. The specialized solver should give the same result but is more efficient for triangular systems.
Solution to Exercise 1
import numpy as np
from scipy import linalg
U = np.array([[3, 1, 2],
[0, 5, 4],
[0, 0, 2]], dtype=float)
b = np.array([10, 13, 4], dtype=float)
x_tri = linalg.solve_triangular(U, b)
x_gen = np.linalg.solve(U, b)
print(f"Triangular solver: {x_tri}")
print(f"General solver: {x_gen}")
print(f"Match: {np.allclose(x_tri, x_gen)}")
Exercise 2.
Compare the eigenvalue computation of a \(100 \times 100\) symmetric matrix using both np.linalg.eigh and scipy.linalg.eigh (use np.random.seed(15) to create \(A + A^T\)). Verify that both return the same eigenvalues (up to \(10^{-10}\)) and measure the execution time of each.
Solution to Exercise 2
import numpy as np
from scipy import linalg
import time
np.random.seed(15)
B = np.random.randn(100, 100)
A = B + B.T
start = time.perf_counter()
vals_np = np.linalg.eigh(A)[0]
time_np = time.perf_counter() - start
start = time.perf_counter()
vals_sp = linalg.eigh(A)[0]
time_sp = time.perf_counter() - start
diff = np.max(np.abs(vals_np - vals_sp))
print(f"Max eigenvalue difference: {diff:.2e}")
print(f"NumPy time: {time_np:.4f} sec")
print(f"SciPy time: {time_sp:.4f} sec")
Exercise 3.
Demonstrate a function available only in SciPy: compute the matrix exponential expm and the Schur decomposition of a \(4 \times 4\) random matrix (use np.random.seed(20)). Print the Schur form \(T\) and verify that expm(A) can be computed as \(Z \cdot \text{expm}(T) \cdot Z^H\).
Solution to Exercise 3
import numpy as np
from scipy import linalg
np.random.seed(20)
A = np.random.randn(4, 4)
# Schur decomposition (SciPy only)
T, Z = linalg.schur(A, output='complex')
print(f"Schur form T:\n{T.round(4)}")
# Matrix exponential (SciPy only)
exp_A_direct = linalg.expm(A)
exp_A_schur = Z @ linalg.expm(T) @ Z.conj().T
error = np.linalg.norm(exp_A_direct - exp_A_schur.real)
print(f"\nexpm(A) via Schur matches direct: {error:.2e}")