codex/finite-element-poisson-refinement.py

finite-element-poisson-refinement.py
"""Compare exact Poisson solution and P1 refinements; opaque PNG to cwd.

Tested with Python 3.14.4, NumPy 2.3.5, Matplotlib 3.10.7.
"""

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def solution(n):
    nodes = np.linspace(0, 1, n + 1)
    matrix = np.zeros((n + 1, n + 1))
    load = np.zeros(n + 1)
    for j, length in enumerate(np.diff(nodes)):
        matrix[j:j + 2, j:j + 2] += np.array([[1., -1.], [-1., 1.]]) / length
        load[j:j + 2] += length / 2
    values = np.zeros(n + 1)
    values[1:-1] = np.linalg.solve(matrix[1:-1, 1:-1], load[1:-1])
    np.testing.assert_allclose(values, nodes * (1 - nodes) / 2, atol=2e-15)
    return nodes, values


def main():
    x = np.linspace(0, 1, 1601)
    exact = x * (1 - x) / 2
    plt.rcParams.update({"font.size": 11, "axes.titlesize": 13})
    fig, axes = plt.subplots(1, 2, figsize=(10.6, 4.5), layout="constrained",
                             facecolor="white")
    axes[0].plot(x, exact, color="#292929", lw=2.3, label="Exact parabola")
    errors = []
    for n, color, marker in [(4, "#b43b35", "o"), (8, "#2166ac", "s")]:
        nodes, values = solution(n)
        h = 1 / n
        approx = np.interp(x, nodes, values)
        error = exact - approx
        np.testing.assert_allclose(error.max(), h**2 / 8, atol=2e-15)
        # Two-point Gauss quadrature integrates the squared derivative error exactly.
        midpoints = (nodes[:-1] + nodes[1:]) / 2
        gauss_x = midpoints[:, None] + h / (2 * np.sqrt(3)) * np.array([-1., 1.])
        discrete_slopes = np.diff(values) / h
        derivative_error = 0.5 - gauss_x - discrete_slopes[:, None]
        energy_squared = (h / 2 * derivative_error**2).sum()
        np.testing.assert_allclose(energy_squared, h**2 / 12, atol=2e-16)
        np.testing.assert_allclose(np.interp(nodes, x, error), 0, atol=2e-15)
        errors.append((error.max(), np.sqrt(energy_squared)))
        axes[0].plot(nodes, values, color=color, lw=2, marker=marker, markersize=4,
                     label=f"P1, {n} intervals ({n - 1} unknowns)")
        axes[1].plot(x, error, color=color, lw=2,
                     label=rf"$n={n}$: max $=1/{8 * n**2}$")
        print(f"n={n}: max error={error.max():.9f}, energy error={np.sqrt(energy_squared):.9f}")
    np.testing.assert_allclose(np.array(errors[0]) / np.array(errors[1]), [4, 2], atol=1e-12)
    assert np.interp(3 / 8, *solution(4)) == 7 / 64

    axes[0].set(ylabel="$u(x)$ or $u_h(x)$", ylim=(-0.006, 0.151),
                title="Exact at nodes, approximate between them")
    axes[0].legend(loc="lower center", fontsize=10, framealpha=1)
    axes[1].set(ylabel="$u(x)-u_h(x)$", ylim=(-0.0003, 0.0106),
                title="Half the spacing, one quarter the value error")
    axes[1].legend(loc="upper center", fontsize=10, framealpha=1)
    axes[1].ticklabel_format(axis="y", style="plain", useOffset=False)
    for ax in axes:
        ax.set(xlim=(-0.02, 1.02), xlabel="$x$", xticks=[0, 0.25, 0.5, 0.75, 1])
        ax.set_facecolor("white")
        ax.grid(alpha=0.18)
        ax.spines[["top", "right"]].set_visible(False)
    fig.savefig(Path.cwd() / "finite-element-poisson-refinement.png", dpi=120,
                facecolor="white", transparent=False)
    plt.close(fig)
    print("Nodal exactness, midpoint error, energy integral and refinement ratios verified.")


if __name__ == "__main__":
    main()