codexlagrange-interpolation-cardinals.py
lagrange-interpolation-cardinals.py
"""Plot three Lagrange cardinal polynomials and their interpolant; 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 cardinals(x):
x = np.asarray(x, dtype=float)
return np.stack(((x - 1) * (x - 2) / 2,
-x * (x - 2), x * (x - 1) / 2))
def main():
nodes = np.array([0.0, 1.0, 2.0])
values = np.array([1.0, 2.0, 5.0])
x = np.linspace(0, 2, 801)
basis = cardinals(x)
p = values @ basis
np.testing.assert_array_equal(cardinals(nodes), np.eye(3))
np.testing.assert_allclose(basis.sum(axis=0), 1, rtol=0, atol=5e-16)
np.testing.assert_allclose(p, x**2 + 1, rtol=0, atol=2e-15)
np.testing.assert_array_equal(values @ cardinals(nodes), values)
np.testing.assert_array_equal(cardinals(0.5), [3 / 8, 3 / 4, -1 / 8])
assert values @ cardinals(0.5) == 1.25
# The Newton coefficients are 1, 1, 1, from divided differences 1, 3, 1.
first = np.diff(values) / np.diff(nodes)
second = (first[1] - first[0]) / (nodes[2] - nodes[0])
np.testing.assert_array_equal(first, [1.0, 3.0])
assert second == 1.0
np.testing.assert_allclose(1 + x + x * (x - 1), p, rtol=0, atol=2e-15)
plt.rcParams.update({"font.size": 11, "axes.titlesize": 13})
fig, axes = plt.subplots(1, 2, figsize=(10.4, 4.6), layout="constrained")
fig.patch.set_facecolor("white")
colors = ["#2166ac", "#25834a", "#b43b35"]
ax = axes[0]
for i, color in enumerate(colors):
ax.plot(x, basis[i], color=color, lw=2.2, label=rf"$\ell_{i}(x)$")
ax.scatter(nodes, cardinals(nodes)[i], color=color, s=45, zorder=5,
marker=["o", "s", "^"][i])
ax.axhline(1, color="0.45", ls="--", lw=1.2,
label=r"$\ell_0+\ell_1+\ell_2=1$")
ax.axhline(0, color="0.6", lw=0.8)
ax.set(ylim=(-0.25, 1.38), ylabel="Cardinal value",
title="Each basis polynomial selects one node")
ax.legend(loc="upper center", ncols=2, fontsize=10, framealpha=1)
ax.annotate("Negative between nodes", (0.5, -0.125),
xytext=(0.95, -0.23), fontsize=9,
arrowprops={"arrowstyle": "->", "color": "0.4"})
ax = axes[1]
for i, color in enumerate(colors):
ax.plot(x, values[i] * basis[i], color=color, lw=1.5, ls="--",
label=rf"${int(values[i])}\ell_{i}(x)$")
ax.plot(x, p, color="#292929", lw=2.7, label=r"Sum: $p(x)=x^2+1$")
ax.scatter(nodes, values, color="black", s=55, zorder=6)
for a, b in zip(nodes, values):
ax.annotate(f"({int(a)}, {int(b)})", (a, b), xytext=(5, 8),
textcoords="offset points", fontsize=10)
ax.scatter([0.5], [1.25], color="#7952a3", s=45, zorder=6)
ax.annotate(r"$p(1/2)=5/4$", (0.5, 1.25), xytext=(0.68, 2.75),
fontsize=11, color="#7952a3",
arrowprops={"arrowstyle": "->", "color": "#7952a3"})
ax.set(ylim=(-0.85, 5.65), ylabel="Weighted contribution or sum",
title="Data values are the basis coefficients")
ax.legend(loc="upper left", fontsize=10, framealpha=1)
for ax in axes:
ax.set(xlim=(-0.06, 2.16), xticks=[0, 0.5, 1, 1.5, 2], xlabel="$x$")
ax.set_facecolor("white")
ax.grid(alpha=0.18)
ax.spines[["top", "right"]].set_visible(False)
fig.savefig(Path.cwd() / "lagrange-interpolation-cardinals.png", dpi=120,
facecolor="white", transparent=False)
plt.close(fig)
print("Cardinal identity, sum-to-one, nodal values, p(1/2)=5/4, and Newton form verified.")
if __name__ == "__main__":
main()