codex/wave-pulse-propagation.py

wave-pulse-propagation.py
"""Plot a smooth wave pulse and its causal interval; save PNG in the cwd.

Run from _media with MPLBACKEND=Agg MPLCONFIGDIR=/tmp.
Tested with Python 3.14.4, NumPy 2.3.5 and Matplotlib 3.10.7.
"""

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def bump(z):
    """Smooth compact bump, with support [-1, 1] and peak one."""
    z = np.asarray(z, dtype=float)
    out = np.zeros_like(z)
    inside = np.abs(z) < 1
    out[inside] = np.exp(1 - 1 / (1 - z[inside] ** 2))
    return out


def main():
    plt.rcParams.update({"font.size": 11, "axes.spines.top": False,
                         "axes.spines.right": False, "figure.facecolor": "white",
                         "axes.facecolor": "white", "savefig.facecolor": "white"})
    left_color, right_color, total_color = "#c75a11", "#176ca4", "#252525"
    fig = plt.figure(figsize=(10, 6.3), layout="constrained")
    grid = fig.add_gridspec(3, 2, width_ratios=(1.25, 1), wspace=0.16)
    fig.suptitle("A pulse released from rest: splitting and finite-speed causality",
                 fontsize=15)
    x = np.linspace(-3, 3, 1501)
    for row, t in enumerate((0.0, 0.5, 1.5)):
        ax = fig.add_subplot(grid[row, 0])
        left, right = 0.5 * bump(x + t), 0.5 * bump(x - t)
        ax.plot(x, left + right, color=total_color, lw=2.7, label="total")
        ax.plot(x, left, color=left_color, lw=2, ls="--", label="left-moving half")
        ax.plot(x, right, color=right_color, lw=2, ls=":", label="right-moving half")
        ax.set(xlim=(-3, 3), ylim=(-0.07, 1.12), ylabel=r"$u/A$")
        ax.text(0.025, 0.86, rf"$ct/a={t:g}$", transform=ax.transAxes)
        ax.set_yticks((0, 0.5, 1))
        ax.grid(alpha=0.18)
        if row == 0:
            ax.set_title(r"$f(x)=A\,b(x/a),\quad g(x)=0$", fontsize=12)
            ax.legend(loc="upper right", fontsize=8.5, framealpha=1)
        if row == 2:
            ax.set_xlabel(r"Position $x/a$")
            for center, direction, color in ((-t, -1, left_color), (t, 1, right_color)):
                ax.annotate("", xy=(center + direction * 0.7, 0.83),
                            xytext=(center, 0.83),
                            arrowprops={"arrowstyle": "->", "color": color, "lw": 2})
        else:
            ax.tick_params(labelbottom=False)

    ax = fig.add_subplot(grid[:, 1])
    x_star, t_star = 0.5, 1.5
    ax.fill((-1, 2, x_star), (0, 0, t_star), color="#e4edf4", zorder=0)
    ax.plot((-1, x_star), (0, t_star), color=left_color, lw=2)
    ax.plot((2, x_star), (0, t_star), color=right_color, lw=2)
    ax.plot((-1, 2), (0, 0), color="#252525", lw=5, solid_capstyle="butt")
    ax.scatter((x_star,), (t_star,), color="#252525", zorder=4)
    ax.text(x_star, t_star + 0.06, r"$(x_*,t_*)=(0.5,1.5)$", ha="center", fontsize=11)
    ax.text(x_star, 0.30, "Only initial data in\nthis interval can matter",
            ha="center", fontsize=11)
    ax.text(-0.42, 0.94, r"$x-t=-1$", rotation=55, color=left_color)
    ax.text(1.37, 0.88, r"$x+t=2$", rotation=-55, color=right_color)
    ax.annotate("initial interval [-1, 2]", xy=(0.5, 0), xytext=(0.5, -0.27),
                ha="center", arrowprops={"arrowstyle": "->", "color": "#555555"})
    ax.set(xlim=(-1.55, 2.55), ylim=(-0.34, 1.91), xlabel="Position x", ylabel="Time t")
    ax.set_title("Backward characteristics\n(normalized speed c = 1)", fontsize=12)
    ax.set_xticks((-1, 0, 0.5, 2))
    ax.set_yticks((0, 0.5, 1, 1.5))
    ax.grid(alpha=0.18)
    fig.savefig(Path.cwd() / "wave-pulse-propagation.png", dpi=140, transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()