codex/past-exam-of-the-mathematics-course-of-the-university-of-cambridge/2010/iii/paper-71-mountain-frames.py

past-exam-of-the-mathematics-course-of-the-university-of-cambridge/2010/iii/paper-71-mountain-frames.py
"""Sketch mountain-wave phase and energy directions; output to caller CWD.

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

from pathlib import Path

import matplotlib

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


def arrow(ax, origin, delta, color, label, offset):
    end = np.asarray(origin) + delta
    ax.annotate("", end, origin,
                arrowprops={"arrowstyle": "-|>", "color": color, "lw": 2.5})
    ax.text(*(end + offset), label, color=color, fontsize=10,
            ha="center", va="center",
            bbox={"facecolor": "white", "edgecolor": "none", "alpha": 0.9})


def main():
    fig, axes = plt.subplots(1, 2, figsize=(11, 4.7), dpi=100)
    fig.set_facecolor("white")
    x, z = np.meshgrid(np.linspace(0, 4.4, 400), np.linspace(0, 3.2, 300))
    for ax in axes:
        ax.contour(x, z, x + 2 * z, levels=np.arange(-2, 13, 1),
                   colors="#a7b5c1", linewidths=1.2)
        ax.set(xlim=(0, 4.4), ylim=(0, 3.2), ylabel="Height z")
        ax.set_aspect("equal")
        ax.text(3.3, 0.85, "Wave crests", rotation=-27, color="#5c6974",
                bbox={"facecolor": "white", "edgecolor": "none", "alpha": 0.9})
    origin = np.array([2.05, 1.55])
    axes[0].plot(np.linspace(0, 4.4, 400),
                 0.08 + 0.05 * np.sin(2 * np.pi * np.linspace(0, 4.4, 400)),
                 color="black", lw=1.5)
    arrow(axes[0], origin, np.array([0.6, 1.2]), "#176a9e",
          "Energy: up and downwind", np.array([0.15, 0.22]))
    axes[0].scatter(*origin, s=20, color="black", zorder=5)
    axes[0].text(1.55, 1.3, "Phase pattern stationary", fontsize=10,
                 ha="center", bbox={"facecolor": "white", "edgecolor": "none"})
    axes[0].set(xlabel="Downwind coordinate x", title="Frame fixed to the hills")
    arrow(axes[1], origin, np.array([-1.2, 0.6]), "#176a9e",
          "Energy: upstream and up", np.array([0.15, 0.3]))
    arrow(axes[1], origin, np.array([-0.45, -0.9]), "#bc5138",
          "Phase: upstream and down", np.array([0.0, -0.28]))
    axes[1].scatter(*origin, s=20, color="black", zorder=5)
    axes[1].set(xlabel="Moving coordinate x′ = x − U₀t",
                title="Frame moving with the mean wind")
    fig.suptitle("Uniform mountain wave: m/k = 2; arrows show directions", fontsize=12)
    fig.tight_layout()
    fig.savefig(Path.cwd() / "paper-71-mountain-frames.png",
                facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()