codex/wave-equation-characteristic-causal-cones.py

wave-equation-characteristic-causal-cones.py
"""Synthetic causal geometry; Python 3.14, matplotlib 3.10.7, numpy 2.3.5.

Run from _media; writes the matching PNG basename in the current directory.
"""

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def main():
    plt.rcParams.update({"font.size": 11, "axes.spines.top": False,
                         "axes.spines.right": False, "figure.facecolor": "white",
                         "axes.facecolor": "white"})
    fig, (back, forward) = plt.subplots(1, 2, figsize=(11.6, 5.7))
    x_star, t_star, c = 0.5, 1.5, 1.0
    t = np.linspace(0, t_star, 201)
    lo = x_star - c * (t_star - t)
    hi = x_star + c * (t_star - t)
    back.fill_betweenx(t, lo, hi, color="#dbeaf3")
    back.plot(lo, t, color="#1976a3", lw=2)
    back.plot(hi, t, color="#1976a3", lw=2)
    back.plot([-1, 2], [0, 0], color="#c77719", lw=5, solid_capstyle="round", zorder=4)
    back.scatter([-1, 2, x_star], [0, 0, t_star], color="#242424", zorder=5)
    back.annotate("Observation (0.5, 1.5)", (x_star, t_star), (-1.6, 1.83),
                  arrowprops={"arrowstyle": "->", "color": "#333333"})
    back.text(0.5, 0.46, "Backward cone", ha="center", color="#235f82")
    back.text(0.5, -0.31, "Initial-data interval [−1, 2]", ha="center", color="#995810")
    back.annotate("x − t = −1", (-0.65, 0.35), (-2.55, 0.8),
                  arrowprops={"arrowstyle": "->", "color": "#1976a3"}, color="#1976a3", fontsize=10)
    back.annotate("x + t = 2", (1.65, 0.35), (1.6, 1.05),
                  arrowprops={"arrowstyle": "->", "color": "#1976a3"}, color="#1976a3", fontsize=10)
    back.set_title("Which initial data can affect an observation?", fontsize=12, pad=12)
    back.set_xlim(-2.8, 3.3)
    back.set_ylim(-0.43, 2.15)
    back.set_xticks((-2, -1, 0, 0.5, 1, 2, 3))
    back.set_yticks((0, 0.5, 1, 1.5, 2))
    back.set_xlabel("Position x")
    back.set_ylabel("Time t")

    t = np.linspace(0, 2, 301)
    forward.fill_betweenx(t, -1 - t, 1 + t, color="#edf3e6", label="Possible influence")
    forward.fill_betweenx(t, -1 - t, 1 - t, color="#f1b6a8", alpha=0.9, label="Left-pulse support")
    forward.fill_betweenx(t, -1 + t, 1 + t, color="#a9cee3", alpha=0.8, label="Right-pulse support")
    forward.plot(-1 - t, t, color="#63834c", lw=2, linestyle="--")
    forward.plot(1 + t, t, color="#63834c", lw=2, linestyle="--")
    forward.plot([-1, 1], [0, 0], color="#c77719", lw=5, zorder=4)
    forward.axhline(1.5, color="#777777", lw=1, linestyle=":")
    forward.text(0, 1.82, "g = 0:\ncentral gap", ha="center", fontsize=10)
    forward.text(-2.7, 0.75, "u = 0", ha="center", color="#555555")
    forward.text(2.7, 0.75, "u = 0", ha="center", color="#555555")
    forward.annotate("−1 − t", (-2.65, 1.65), (-3.45, 2.09), color="#4f6a3c",
                     arrowprops={"arrowstyle": "->", "color": "#63834c"}, fontsize=10)
    forward.annotate("1 + t", (2.65, 1.65), (2.4, 2.09), color="#4f6a3c",
                     arrowprops={"arrowstyle": "->", "color": "#63834c"}, fontsize=10)
    forward.text(0, -0.31, "Initial support [−1, 1]", ha="center", color="#995810")
    forward.set_title("Where can a compact disturbance reach?", fontsize=12, pad=12)
    forward.set_xlim(-3.6, 3.6)
    forward.set_ylim(-0.43, 2.3)
    forward.set_xticks((-3, -2, -1, 0, 1, 2, 3))
    forward.set_yticks((0, 0.5, 1, 1.5, 2))
    forward.set_xlabel("Position x")
    forward.set_ylabel("Time t")
    for ax in (back, forward):
        ax.grid(alpha=0.16)
    handles, labels = forward.get_legend_handles_labels()
    fig.legend(handles, labels, loc="lower center", bbox_to_anchor=(0.5, 0.075),
               ncol=3, frameon=False, fontsize=10)
    fig.suptitle("Characteristic cones and finite propagation (c = 1)", fontsize=16, y=0.97)
    fig.text(0.5, 0.03, "Synthetic ideal-PDE geometry. Colored bands: f(x) = b(x), g(x) = 0; outer bounds allow other data.",
             ha="center", fontsize=10, color="#555555")
    fig.subplots_adjust(left=0.065, right=0.985, bottom=0.23, top=0.82, wspace=0.24)
    fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), dpi=110, facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()