codexfinite-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()