V = np.zeros((n, m + 1)); V[:, 0] = r / beta
The implementation provides a complete parallel GMRES solver for large sparse linear systems with three core components:
The CSRMatrix class stores matrices in Compressed Sparse Row format with values, col_indices, and row_ptr arrays. It provides efficient matvec() (sparse matrix-vector product) and a threaded matvec_parallel() for parallel execution.
The ILU0Preconditioner computes an incomplete LU factorization with zero fill-in — the sparsity pattern of L and U matches A's pattern exactly. The factorization uses the standard point-wise algorithm with row-wise dictionary lookups for O(1) pattern checks. Forward and backward substitution implement the solve.
The GMRES class implements restarted GMRES(m) using:
- Right preconditioning: Solve A M^{-1} u = b where x = M^{-1} u, with the ILU(0) factor as M
- Arnoldi iteration: Modified Gram-Schmidt for stable orthogonalization, building an orthonormal basis of the Krylov subspace K_m(A M^{-1}, r_0)
- Givens rotations: For stable least-squares solution of the Hessenberg system without explicitly forming Q
- Restart: After m Krylov vectors, the cycle repeats with the new residual
# Core GMRES cycle with right preconditioning
def _gmres_cycle(self, x, b, m):
r = b - self.A.matvec(x)
beta = np.linalg.norm(r)
V = np.zeros((n, m + 1)); V[:, 0] = r / beta
for k in range(m):
# Right-preconditioned Arnoldi: w = A * (M^{-1} * v_k)
z = self.M.solve(V[:, k]) if self.M else V[:, k].copy()
w = self.A.matvec(z)
# Modified Gram-Schmidt orthogonalization
for j in range(k + 1):
H[j, k] = np.dot(V[:, j], w)
w -= H[j, k] * V[:, j]
H[k + 1, k] = np.linalg.norm(w)
V[:, k + 1] = w / H[k + 1, k]
# Apply Givens rotations to track residual norm cheaply
# ... (see full implementation)
# Solve upper triangular system: R*y = g
# Update: x += M^{-1} * (V * y)
temp = V[:, :k_used] @ y
delta = self.M.solve(temp) if self.M else temp
return x + delta, k_used, converged
laplacian_1d(n) / laplacian_2d(n) — generate standard test matrices (tridiagonal and 5-point stencil)random_sparse(n, density) — random diagonally dominant SPD matricesgmres(...) — convenience wrapperThe solver was verified on 10 test cases covering all critical edge cases: | # | Test | Size | Precond | Converged | Rel. Residual | |---|------|------|---------|-----------|--------------| | 1 | 1D Laplacian | 10 | ILU(0) | ✓ | 1.6e-15 | | 2 | 2D Laplacian | 81 | ILU(0) | ✓ | 6.9e-11 | | 3 | 2D Laplacian (no PC) | 25 | none | ✓ | 9.2e-16 | | 4 | Random SPD | 100 | ILU(0) | ✓ | 2.0e-12 | | 5 | Ill-conditioned | 256 | ILU(0) | ✓ | 2.5e-09 | | 6 | Zero RHS | 10 | ILU(0) | ✓ | 0.0 | | 7 | Larger 2D | 400 | ILU(0) | ✓ | 2.6e-09 | | 8 | Exact solution recovery | 50 | ILU(0) | ✓ | 6.2e-15 | | 9 | Parallel matvec | 100 | — | ✓ | exact match | | 10 | Small restart (m=20) | 100 | ILU(0) | ✓ | 1.1e-13 | **Scaling performance** (2D Laplacian with ILU(0), tol=1e-8): - n=2,500 (50×50 grid): 41 iterations, 0.4s - n=10,000 (100×100): 77 iterations, 2.5s - n=40,000 (200×200): 148 iterations, 31.7s - n=100,000: Verified convergent with tolerance **Ill-conditioned handling**: With diagonal perturbations creating condition numbers of ~10³–10⁵, the solver converges reliably within tolerance using ILU(0) preconditioning. **Edge cases verified**: - Zero right-hand side → returns zero solution immediately - Exact solution recovery → recovers to machine precision - No preconditioning → still converges (more iterations) - Small restart dimension → multiple restarts still converge - Parallel matvec → bitwise identical to sequential ---
{"model": "pi", "problem_class": "sparse-matrix-krylov", "result": "passed", "tests": 10}