codex/pde-transport-vs-diffusion.py

pde-transport-vs-diffusion.py
"""Plot exact periodic transport and heat profiles; output basename.png to cwd.

Tested with Python 3.14, NumPy 2.3.5 and Matplotlib 3.10.7.
"""

from pathlib import Path

import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np


def initial(x):
    return 1 + np.cos(x) + 0.4 * np.cos(3 * x)


def transport(x, t):
    return initial(x - t)


def heat(x, t):
    return 1 + np.exp(-t) * np.cos(x) + 0.4 * np.exp(-9 * t) * np.cos(3 * x)


def main():
    x = np.linspace(0, 2 * np.pi, 1201)
    times = (0, 0.25, 1)
    plt.rcParams.update({"font.size": 11, "axes.spines.top": False,
                         "axes.spines.right": False})
    fig, axes = plt.subplots(2, 3, figsize=(10.4, 5.6), sharex=True,
                             sharey=True, facecolor="white")
    for row, (solution, color, label) in enumerate([
        (transport, "#1266a0", "Transport"),
        (heat, "#bd5017", "Diffusion"),
    ]):
        for col, t in enumerate(times):
            ax = axes[row, col]
            ax.set_facecolor("white")
            ax.plot(x, initial(x), color="#999999", lw=1.2, ls="--",
                    label="Initial profile", zorder=1)
            ax.axhline(1, color="#bbbbbb", lw=0.8, ls=":")
            ax.plot(x, solution(x, t), color=color, lw=2.3,
                    label="Current profile", zorder=2)
            ax.set(xlim=(0, 2 * np.pi), ylim=(-0.55, 2.55),
                   xticks=[0, np.pi, 2 * np.pi],
                   xticklabels=["0", r"$\pi$", r"$2\pi$"],
                   yticks=[0, 1, 2])
            ax.grid(alpha=0.15)
            if row == 0:
                ax.set_title(f"t = {t:g}", pad=9)
            else:
                ax.set_xlabel("Position x (periodic)")
            if col == 0:
                ax.set_ylabel(f"{label}\nu(x, t)")
    axes[0, 1].text(0.5, 0.9, "Same shape, moving right", ha="center",
                    transform=axes[0, 1].transAxes, color="#1266a0",
                    fontsize=10, bbox={"facecolor": "white", "edgecolor": "none"})
    axes[1, 1].text(0.5, 0.9, "Short ripples fade first", ha="center",
                    transform=axes[1, 1].transAxes, color="#bd5017",
                    fontsize=10, bbox={"facecolor": "white", "edgecolor": "none"})
    axes[0, 0].legend(loc="upper center", fontsize=9, frameon=False)
    fig.suptitle("One initial profile, two different local rules", fontsize=16, y=0.98)
    fig.text(0.5, 0.905, r"$g(x)=1+\cos x+0.4\cos(3x)$; dimensionless coefficients = 1",
             ha="center", fontsize=12)
    fig.text(0.5, 0.025,
             r"Transport: $u_t+u_x=0$     |     Diffusion: $u_t=u_{xx}$     |     Mean = 1 in both",
             ha="center", fontsize=11)
    fig.subplots_adjust(left=0.075, right=0.985, bottom=0.14, top=0.81,
                        hspace=0.22, wspace=0.13)
    fig.savefig(Path.cwd() / Path(__file__).with_suffix(".png").name,
                dpi=125, facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()