codex/singular-perturbation-boundary-layer-profiles.py

singular-perturbation-boundary-layer-profiles.py
"""Plot exact boundary-layer profiles; Python 3.14, NumPy 2.3, Matplotlib 3.10.

Run from _media: the opaque PNG is written to the current working directory.
"""

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def exact_profile(x, epsilon):
    """Normalize with expm1, including when epsilon is not asymptotically small."""
    return np.expm1(-np.asarray(x) / epsilon) / np.expm1(-1.0 / epsilon)


def main():
    plt.rcParams.update({"font.size": 12, "axes.spines.top": False,
                         "axes.spines.right": False, "figure.facecolor": "white",
                         "axes.facecolor": "white"})
    fig, ax = plt.subplots(figsize=(9, 5.4), layout="constrained")
    x = np.unique(np.concatenate((np.linspace(0, 1, 1801),
                                  np.geomspace(1e-6, 1, 1200))))
    colors = ("#0072B2", "#D55E00", "#009E73")
    for epsilon, color in zip((0.2, 0.05, 0.01), colors):
        ax.plot(x, exact_profile(x, epsilon), color=color, linewidth=2.6,
                label=rf"Exact, $\epsilon={epsilon:g}$")
        ax.scatter([epsilon], [exact_profile(epsilon, epsilon)],
                   color=color, s=42, zorder=4)
    ax.axhline(1, color="#333333", linestyle="--", linewidth=1.6,
               label=r"Outer approximation $y_0=1$")
    ax.scatter([0], [0], color="black", s=36, zorder=5)
    ax.annotate("All exact profiles satisfy y(0) = 0", xy=(0, 0),
                xytext=(0.26, 0.17), arrowprops={"arrowstyle": "->", "color": "#555555"},
                fontsize=11)
    ax.text(0.33, 0.39, r"Dots mark $x=\epsilon$: about 63% of the rise." + "\n"
            + r"Smaller $\epsilon$ compresses the transition toward $x=0$.",
            fontsize=11)
    ax.set(xlim=(-0.008, 1.01), ylim=(-0.035, 1.07), xlabel=r"Outer coordinate $x$",
           ylabel=r"Solution $y_\epsilon(x)$",
           title=r"A thin boundary layer repairs the left boundary condition")
    ax.grid(alpha=0.18)
    ax.legend(loc="center right", bbox_to_anchor=(0.99, 0.62), frameon=False,
              fontsize=11)
    fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), dpi=100,
                facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()