Skip to content

Direct Solvers

Direct methods for solving sparse linear systems.

Mental Model

Direct sparse solvers factor the matrix exactly (like LU), then solve by substitution. They give you the answer in a predictable amount of time with no convergence worries, but the factorization can create "fill-in" -- new nonzeros that weren't in the original matrix. For moderate-sized sparse systems, direct solvers are the reliable default; for very large systems, consider iterative methods instead.

spsolve

1. Basic Usage

```python import numpy as np from scipy import sparse from scipy.sparse import linalg as splinalg

def main(): A = sparse.csr_matrix([[4, 1, 0], [1, 4, 1], [0, 1, 4]]) b = np.array([1, 2, 1])

x = splinalg.spsolve(A, b)

print(f"Solution: x = {x}")
print(f"Residual: {np.linalg.norm(A @ x - b):.2e}")

if name == "main": main() ```

splu - Sparse LU

1. Factor Once, Solve Multiple

```python import numpy as np from scipy import sparse from scipy.sparse import linalg as splinalg

def main(): n = 1000 A = sparse.diags([-1, 2, -1], [-1, 0, 1], shape=(n, n), format='csc')

# Factor once
lu = splinalg.splu(A)

# Solve multiple systems
for i in range(3):
    b = np.random.randn(n)
    x = lu.solve(b)
    print(f"System {i}: residual = {np.linalg.norm(A @ x - b):.2e}")

if name == "main": main() ```

2. Fill-in Management

```python import numpy as np from scipy import sparse from scipy.sparse import linalg as splinalg

def main(): n = 100 A = sparse.diags([-1, 2, -1], [-1, 0, 1], shape=(n, n), format='csc')

lu = splinalg.splu(A)

print(f"Original nnz: {A.nnz}")
print(f"L nnz: {lu.L.nnz}")
print(f"U nnz: {lu.U.nnz}")

if name == "main": main() ```

spilu - Incomplete LU

1. Preconditioner

```python import numpy as np from scipy import sparse from scipy.sparse import linalg as splinalg

def main(): n = 100 A = sparse.diags([-1, 2, -1], [-1, 0, 1], shape=(n, n), format='csc')

# Incomplete LU (for preconditioning)
ilu = splinalg.spilu(A)

print(f"Original nnz: {A.nnz}")
print(f"ILU L nnz: {ilu.L.nnz}")
print(f"ILU U nnz: {ilu.U.nnz}")

if name == "main": main() ```

Performance

1. Sparse vs Dense

```python import numpy as np from scipy import sparse, linalg from scipy.sparse import linalg as splinalg import time

def main(): n = 2000 A_sparse = sparse.diags([-1, 2, -1], [-1, 0, 1], shape=(n, n), format='csc') A_dense = A_sparse.toarray() b = np.random.randn(n)

# Dense solve
start = time.perf_counter()
x_dense = linalg.solve(A_dense, b)
dense_time = time.perf_counter() - start

# Sparse solve
start = time.perf_counter()
x_sparse = splinalg.spsolve(A_sparse, b)
sparse_time = time.perf_counter() - start

print(f"Dense solve:  {dense_time:.4f} sec")
print(f"Sparse solve: {sparse_time:.4f} sec")
print(f"Speedup: {dense_time/sparse_time:.1f}x")

if name == "main": main() ```

Summary

Function Description
spsolve(A, b) Direct sparse solve
splu(A) Sparse LU factorization
spilu(A) Incomplete LU (preconditioner)

Use CSC format for direct solvers.


Exercises

Exercise 1. Create a \(500 \times 500\) sparse tridiagonal SPD matrix (2 on diagonal, -1 on off-diagonals) and a random right-hand side vector. Solve the system using spsolve and verify the residual norm is below \(10^{-10}\).

Solution to Exercise 1
import numpy as np
from scipy import sparse
from scipy.sparse import linalg as splinalg

n = 500
A = sparse.diags([-1, 2, -1], [-1, 0, 1], shape=(n, n), format='csr')
b = np.random.randn(n)

x = splinalg.spsolve(A, b)
residual = np.linalg.norm(A @ x - b)
print(f"Residual norm: {residual:.2e}")
assert residual < 1e-10

Exercise 2. Use splu to factor a \(1000 \times 1000\) sparse tridiagonal matrix (in CSC format). Solve the system for 10 different random right-hand sides using the pre-computed factorization. Print the average residual norm across all 10 solves.

Solution to Exercise 2
import numpy as np
from scipy import sparse
from scipy.sparse import linalg as splinalg

n = 1000
A = sparse.diags([-1, 2, -1], [-1, 0, 1], shape=(n, n), format='csc')
lu = splinalg.splu(A)

residuals = []
for _ in range(10):
    b = np.random.randn(n)
    x = lu.solve(b)
    residuals.append(np.linalg.norm(A @ x - b))

avg_residual = np.mean(residuals)
print(f"Average residual norm: {avg_residual:.2e}")

Exercise 3. Compare spsolve and splu + lu.solve for a \(2000 \times 2000\) sparse tridiagonal system. Factor the matrix once with splu, then solve for 5 right-hand sides with both approaches. Measure and print the total time for each approach (including the factorization time for splu).

Solution to Exercise 3
import numpy as np
from scipy import sparse
from scipy.sparse import linalg as splinalg
import time

n = 2000
A = sparse.diags([-1, 2, -1], [-1, 0, 1], shape=(n, n), format='csc')
bs = [np.random.randn(n) for _ in range(5)]

# spsolve approach
start = time.perf_counter()
for b in bs:
    x = splinalg.spsolve(A, b)
time_spsolve = time.perf_counter() - start

# splu approach
start = time.perf_counter()
lu = splinalg.splu(A)
for b in bs:
    x = lu.solve(b)
time_splu = time.perf_counter() - start

print(f"spsolve (5 solves):     {time_spsolve:.4f} sec")
print(f"splu + solve (5 solves): {time_splu:.4f} sec")