Tridiagonal Solver Example — Thomas Algorithm

Demonstrates the Thomas algorithm via linearsystems.tridiag_solve() by solving the 1D Poisson equation \(-u'' = \pi^2 \sin(\pi x)\) on \([0,1]\) with Dirichlet boundary conditions, whose exact solution is \(u(x) = \sin(\pi x)\). The result is compared against a full-matrix solve using linearsystems.lslrg().

Example Code

"""IMSL Tridiagonal Solver example: 1D Poisson equation with Thomas algorithm.

Solves -u'' = f(x) on [0,1] with Dirichlet BCs u(0)=u(1)=0 and
f(x) = π²sin(πx), whose exact solution is u(x)=sin(πx).

Discretizing on an n-point interior grid gives a tridiagonal system. The
Thomas algorithm (tridiag_solve) is compared against a full-matrix solve
(lslrg) for accuracy and speed.

Outputs:
- Max error and solve times printed to stdout
- SVG plot of numerical vs analytical solution saved to
  test_output/example_imsl_tridiagonal.svg
"""

from __future__ import annotations

import time
from pathlib import Path
from typing import Dict

import matplotlib.pyplot as plt
import numpy as np

from linearsystems import tridiag_solve, lslrg


def run_demo_imsl_tridiagonal() -> Dict[str, object]:
    """Solve the 1D Poisson equation using tridiag_solve (Thomas algorithm).

    Discretises -u'' = π²sin(πx) on n=100 interior points, forms the
    tridiagonal system, solves with tridiag_solve(), and also with lslrg()
    for comparison. Reports max absolute error against sin(πx).

    Args:
        None

    Returns:
        Dict[str, object]: Result dict with keys ``max_error_tridiag``,
            ``max_error_full``, ``time_tridiag``, ``time_full``,
            and ``plot_path`` (str).
    """
    n = 100  # interior grid points
    h = 1.0 / (n + 1)
    x = np.linspace(h, 1.0 - h, n)       # interior nodes
    x_full = np.linspace(0, 1, n + 2)     # including BCs for plotting

    # Right-hand side: f(x) = π²sin(πx)
    f = np.pi**2 * np.sin(np.pi * x)

    # Exact solution
    u_exact = np.sin(np.pi * x)

    # Build tridiagonal system: (2/h²)u_i - (1/h²)u_{i-1} - (1/h²)u_{i+1} = f_i
    diag_main = np.full(n, 2.0 / h**2)
    diag_off = np.full(n - 1, -1.0 / h**2)

    # Thomas algorithm solve
    t0 = time.perf_counter()
    u_tridiag = tridiag_solve(diag_off, diag_main, diag_off, f)
    t_tridiag = time.perf_counter() - t0

    # Full-matrix solve (lslrg) for comparison
    a_full = np.diag(diag_main) + np.diag(diag_off, 1) + np.diag(diag_off, -1)
    t0 = time.perf_counter()
    result_full = lslrg(a_full, f)
    t_full = time.perf_counter() - t0
    u_full = result_full.x

    max_err_tridiag = float(np.max(np.abs(u_tridiag - u_exact)))
    max_err_full = float(np.max(np.abs(u_full - u_exact)))

    print("\nIMSL Tridiagonal Solver: 1D Poisson Equation (-u'' = π²sin(πx))")
    print("-" * 65)
    print(f"Grid points (interior): n = {n},  h = {h:.6f}")
    print(f"{'Method':<30} {'Max |error|':>14} {'Solve time (s)':>16}")
    print("-" * 65)
    print(f"{'tridiag_solve (Thomas)':<30} {max_err_tridiag:>14.4e} {t_tridiag:>16.6f}")
    print(f"{'lslrg (full matrix)':<30} {max_err_full:>14.4e} {t_full:>16.6f}")
    print("-" * 65)
    print(f"\nExpected O(h²) error ≈ {h**2:.4e}  (h²π²/12 estimate)")

    output_dir = Path("test_output")
    output_dir.mkdir(parents=True, exist_ok=True)
    plot_path = output_dir / "example_imsl_tridiagonal.svg"

    # Embed BCs (u=0) for plotting
    u_plot_tridiag = np.concatenate([[0.0], u_tridiag, [0.0]])
    u_plot_exact = np.concatenate([[0.0], u_exact, [0.0]])
    error_plot = np.concatenate([[0.0], u_tridiag - u_exact, [0.0]])

    fig, axes = plt.subplots(1, 2, figsize=(14, 5))

    # Left: solution comparison
    ax = axes[0]
    ax.plot(x_full, u_plot_exact, "k-", linewidth=2.5, label="Exact: sin(πx)")
    ax.plot(x_full, u_plot_tridiag, "b--", linewidth=1.5, label="tridiag_solve (Thomas)")
    ax.set_xlabel("x")
    ax.set_ylabel("u(x)")
    ax.set_title("1D Poisson: Numerical vs Analytical")
    ax.legend()
    ax.grid(True, alpha=0.3)

    # Right: pointwise error
    ax2 = axes[1]
    ax2.plot(x_full, error_plot, "r-", linewidth=1.5)
    ax2.axhline(0, color="k", linewidth=0.8, linestyle="--")
    ax2.set_xlabel("x")
    ax2.set_ylabel("u_numerical − u_exact")
    ax2.set_title(f"Pointwise Error  (max={max_err_tridiag:.2e})")
    ax2.grid(True, alpha=0.3)

    fig.tight_layout()
    fig.savefig(plot_path, format="svg")
    plt.close(fig)
    print(f"\nPlot saved to: {plot_path}")

    return {
        "max_error_tridiag": max_err_tridiag,
        "max_error_full": max_err_full,
        "time_tridiag": t_tridiag,
        "time_full": t_full,
        "plot_path": str(plot_path),
    }


if __name__ == "__main__":
    run_demo_imsl_tridiagonal()

Plot Output

Numerical vs analytical solution and pointwise error for 1D Poisson

Left: numerical solution (Thomas algorithm) overlaid on the exact \(\sin(\pi x)\) curve. Right: pointwise error showing the expected \(O(h^2)\) spatial convergence.

Console Output

IMSL Tridiagonal Solver: 1D Poisson Equation (-u'' = π²sin(πx))
-----------------------------------------------------------------
Grid points (interior): n = 100,  h = 0.009901
Method                            Max |error|   Solve time (s)
-----------------------------------------------------------------
tridiag_solve (Thomas)             8.0620e-05         0.000174
lslrg (full matrix)                8.0620e-05         0.026109
-----------------------------------------------------------------

Expected O(h²) error ≈ 9.8030e-05  (h²π²/12 estimate)

Plot saved to: test_output\example_imsl_tridiagonal.svg