Banded and Sparse Solvers¶
Demonstrates banded matrix storage utilities and all sparse / iterative linear-system solvers on a 10-node 1D Poisson discretisation.
Functions covered: linearsystems.to_banded(),
linearsystems.from_banded(),
linearsystems.lslbd(),
linearsystems.sparse_solve(),
linearsystems.sparse_cg(),
linearsystems.sparse_gmres(),
linearsystems.lsiccg(),
linearsystems.lsisor().
Example Code¶
"""Banded and Sparse Solvers example.
Demonstrates:
- to_banded / from_banded: convert 10×10 tridiagonal (1D Poisson) to banded
storage and back, verify round-trip
- lslbd: solve from banded storage
- sparse_solve, sparse_cg, sparse_gmres: sparse matrix solvers
- lsiccg: CG with incomplete-Cholesky preconditioner
- lsisor: SOR iteration (omega=1.2)
Problem: -u'' = f on [0,1] with Dirichlet BCs, n=10 interior points.
Outputs:
- Solution and residuals printed to stdout
- SVG: banded storage visualisation + solution comparison + convergence
saved to test_output/example_imsl_banded_sparse.svg
"""
from __future__ import annotations
from pathlib import Path
from typing import Dict
import matplotlib.pyplot as plt
import numpy as np
import scipy.sparse as ssp
from linearsystems import (
to_banded,
from_banded,
lslbd,
sparse_solve,
sparse_cg,
sparse_gmres,
lsiccg,
lsisor,
)
def run_demo_imsl_banded_sparse() -> Dict[str, object]:
"""Run banded and sparse solvers demo on 1D Poisson system.
Builds a 10×10 tridiagonal matrix for the 1D Poisson equation, exercises
banded storage utilities and all sparse/iterative solvers, and compares
their solutions.
Args:
None
Returns:
Dict[str, object]: Keys: ``residuals`` (dict solver→float),
``round_trip_error`` (float), ``plot_path`` (str).
"""
n = 10
h = 1.0 / (n + 1)
x_nodes = np.linspace(h, 1.0 - h, n)
# Exact solution: u(x) = sin(π x), rhs: f(x) = π² sin(π x)
u_exact = np.sin(np.pi * x_nodes)
f = (np.pi ** 2) * np.sin(np.pi * x_nodes)
rhs = f * h ** 2 # scaled for the (2 -1 -1)/h² system → (2 -1 -1) form
# Build system A u = rhs where A = tridiag(-1, 2, -1) (h² absorbed into rhs)
main = 2.0 * np.ones(n)
off = -1.0 * np.ones(n - 1)
A_full = np.diag(main) + np.diag(off, 1) + np.diag(off, -1)
# ------------------------------------------------------------------
# 1. Banded storage round-trip
# ------------------------------------------------------------------
lower_bw = 1
upper_bw = 1
ab = to_banded(A_full, lower_bw, upper_bw)
A_reconstructed = from_banded(ab, lower_bw, upper_bw, n)
round_trip_error = float(np.linalg.norm(A_full - A_reconstructed))
print("\nBanded and Sparse Solvers (1D Poisson, n=10)")
print("=" * 60)
print(f"\n1. Banded storage round-trip error: {round_trip_error:.4e}")
print(f" ab shape: {ab.shape} (lower+upper+1=3 rows × n=10 cols)")
# ------------------------------------------------------------------
# 2. lslbd — solve from banded storage
# ------------------------------------------------------------------
result_bd = lslbd(ab, rhs, lower_bw, upper_bw)
res_bd = result_bd.residual
print(f"\n2. lslbd residual: {res_bd:.4e}")
# ------------------------------------------------------------------
# 3. Sparse solvers
# ------------------------------------------------------------------
A_sp = ssp.csr_matrix(A_full)
x_sp_direct = sparse_solve(A_sp, rhs)
res_sp_direct = float(np.linalg.norm(A_full @ x_sp_direct - rhs))
result_sp_cg = sparse_cg(A_sp, rhs)
res_sp_cg = result_sp_cg.residual
result_sp_gmres = sparse_gmres(A_sp, rhs)
res_sp_gmres = result_sp_gmres.residual
print(f"\n3. sparse_solve residual: {res_sp_direct:.4e}")
print(f" sparse_cg residual: {res_sp_cg:.4e} "
f"(iters={result_sp_cg.n_iter})")
print(f" sparse_gmres residual: {res_sp_gmres:.4e} "
f"(iters={result_sp_gmres.n_iter})")
# ------------------------------------------------------------------
# 4. lsiccg — IC-preconditioned CG
# ------------------------------------------------------------------
result_iccg = lsiccg(A_full, rhs)
res_iccg = result_iccg.residual
print(f"\n4. lsiccg residual: {res_iccg:.4e} "
f"(iters={result_iccg.n_iter})")
# ------------------------------------------------------------------
# 5. lsisor — SOR iteration
# ------------------------------------------------------------------
result_sor = lsisor(A_full, rhs, omega=1.2, tol=1e-10, maxiter=2000)
res_sor = result_sor.residual
print(f"\n5. lsisor residual: {res_sor:.4e} "
f"(iters={result_sor.n_iter}, success={result_sor.success})")
residuals = {
"lslbd": res_bd,
"sparse_solve": res_sp_direct,
"sparse_cg": res_sp_cg,
"sparse_gmres": res_sp_gmres,
"lsiccg": res_iccg,
"lsisor": res_sor,
}
# ------------------------------------------------------------------
# Plots
# ------------------------------------------------------------------
output_dir = Path("test_output")
output_dir.mkdir(parents=True, exist_ok=True)
plot_path = output_dir / "example_imsl_banded_sparse.svg"
fig, axes = plt.subplots(1, 3, figsize=(18, 6))
# (0) Banded storage visualisation
ax = axes[0]
im = ax.imshow(ab, cmap="viridis", aspect="auto")
ax.set_title("Banded storage ab\n(3 rows × 10 cols)")
ax.set_xlabel("Column j")
ax.set_ylabel("Band row")
ax.set_yticks([0, 1, 2])
ax.set_yticklabels(["super-diag", "main diag", "sub-diag"])
fig.colorbar(im, ax=ax, shrink=0.7)
# (1) Solution comparison
ax2 = axes[1]
ax2.plot(x_nodes, u_exact, "k-", linewidth=2.5, label="Exact sin(πx)")
ax2.plot(x_nodes, result_bd.x, "b--o", ms=5, linewidth=1.5, label="lslbd")
ax2.plot(x_nodes, x_sp_direct, "r:s", ms=4, linewidth=1.5, label="sparse_solve")
ax2.plot(x_nodes, result_sp_cg.x, "g-.^", ms=4, linewidth=1.2, label="sparse_cg")
ax2.set_xlabel("x")
ax2.set_ylabel("u(x)")
ax2.set_title("Solution comparison (1D Poisson, n=10)")
ax2.legend(fontsize=8)
ax2.grid(True, alpha=0.3)
# (2) Residuals bar chart
ax3 = axes[2]
labels = list(residuals.keys())
vals = [max(v, 1e-18) for v in residuals.values()]
colors = ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", "#8c564b"]
ax3.bar(range(len(labels)), vals, color=colors, alpha=0.85)
ax3.set_xticks(range(len(labels)))
ax3.set_xticklabels(labels, rotation=30, ha="right", fontsize=9)
ax3.set_yscale("log")
ax3.set_title("Solver residuals ‖Ax−b‖₂")
ax3.set_ylabel("Residual (log scale)")
ax3.grid(True, axis="y", alpha=0.3)
fig.suptitle("Banded and Sparse Solvers — 1D Poisson (n=10)", fontsize=13)
fig.tight_layout()
fig.savefig(plot_path, format="svg")
plt.close(fig)
print(f"\nPlot saved to: {plot_path}")
return {
"residuals": residuals,
"round_trip_error": round_trip_error,
"plot_path": str(plot_path),
}
if __name__ == "__main__":
run_demo_imsl_banded_sparse()
Plot Output¶
Left: banded storage visualisation (3 rows × 10 cols). Centre: solution comparison across all solvers vs exact sin(πx). Right: residual norms for all six solver methods.¶
Console Output¶
Banded and Sparse Solvers (1D Poisson, n=10)
============================================================
1. Banded storage round-trip error: 0.0000e+00
ab shape: (3, 10) (lower+upper+1=3 rows × n=10 cols)
2. lslbd residual: 5.4949e-16
3. sparse_solve residual: 5.1174e-16
sparse_cg residual: 1.0122e-15 (iters=1)
sparse_gmres residual: 1.4513e-15 (iters=1)
4. lsiccg residual: 5.9525e-16 (iters=1)
5. lsisor residual: 8.8765e-11 (iters=170, success=True)
Plot saved to: test_output\example_imsl_banded_sparse.svg