codex/euler-stability-example.py

euler-stability-example.py
"""Compare Euler steps on [0, 6]; save euler-stability-example.png in 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 euler_decay(step, end=6.0):
    count = round(end / step)
    assert np.isclose(count * step, end)
    times = step * np.arange(count + 1)
    values = np.empty(count + 1)
    values[0] = 1.0
    for n in range(count):
        values[n + 1] = values[n] + step * (-2 * values[n])
    np.testing.assert_allclose(values, (1 - 2 * step) ** np.arange(count + 1))
    return times, values


def main():
    plt.rcParams.update({"font.size": 11, "axes.titlesize": 13})
    fig, axes = plt.subplots(1, 2, figsize=(10.8, 4.7), layout="constrained")
    fig.patch.set_facecolor("white")
    dense_times = np.linspace(0, 6, 600)
    blue, orange = "#2166ac", "#d46514"
    for ax, step in zip(axes, [0.2, 1.2]):
        times, values = euler_decay(step)
        assert times[-1] <= 6 + 1e-12
        assert np.allclose(np.diff(times), step)
        ax.plot(dense_times, np.exp(-2 * dense_times), color=blue, lw=2.5,
                label=r"Exact $y(t)=e^{-2t}$")
        ax.plot(times, values, "o-", color=orange, lw=1.6, markersize=4,
                label=f"Euler, h = {step}")
        ax.axhline(0, color="0.35", lw=0.8)
        ax.set(xlim=(0, 6), xlabel="Time t", ylabel="y",
               title=f"h = {step}: factor 1 − 2h = {1 - 2 * step:.1f}")
        ax.set_facecolor("white")
        ax.grid(alpha=0.18)
        ax.spines[["top", "right"]].set_visible(False)
        ax.legend(loc="upper right" if step == 0.2 else "upper left",
                  fontsize=10, framealpha=1)

    axes[0].set_ylim(-0.07, 1.09)
    axes[0].annotate("At t = 1:\nEuler = 0.07776\nExact ≈ 0.135335", (1, 0.6**5),
                     xytext=(1.65, 0.43),
                     arrowprops={"arrowstyle": "->", "color": "0.3"})
    axes[0].text(4.0, 0.22, "Decays, but underestimates\nthe exact solution", ha="center")
    axes[1].set_ylim(-5.9, 4.55)
    axes[1].annotate("At t = 6:\nEuler = −5.37824\nExact ≈ 0.00000614421", (6, (-1.4)**5),
                     xytext=(2.0, -4.1),
                     arrowprops={"arrowstyle": "->", "color": "0.3"})
    axes[1].text(2.5, -0.9, "Alternating signs, growing amplitude", ha="center", fontsize=10)
    fig.suptitle(r"Euler applied to $y'=-2y$, $y(0)=1$: same interval $0\leq t\leq6$",
                 fontsize=14)
    fig.savefig(Path.cwd() / "euler-stability-example.png", dpi=110,
                facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()