codex/past-exam-of-the-mathematics-course-of-the-university-of-cambridge/2002/iii/paper-49-internal-wave.py

past-exam-of-the-mathematics-course-of-the-university-of-cambridge/2002/iii/paper-49-internal-wave.py
"""Plane internal-wave fields and phase/group-velocity geometry.

Write paper-49-internal-wave.png to the current working directory.
Tested with Python 3.14, NumPy 2.3.5 and Matplotlib 3.10.7.
"""

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def main():
    k, m, buoyancy_frequency = 1.0, 1.5, 1.0
    magnitude = np.hypot(k, m)
    omega = buoyancy_frequency * k / magnitude
    phase_velocity = omega * np.array([k, m]) / magnitude**2
    group_velocity = buoyancy_frequency * np.array([m * m, -k * m]) / magnitude**3
    assert np.isclose(phase_velocity @ group_velocity, 0.0)
    assert np.allclose(phase_velocity + group_velocity, [buoyancy_frequency / magnitude, 0])

    fig, axes = plt.subplots(1, 3, figsize=(12, 4.7), layout="constrained", facecolor="white")
    x = np.linspace(0, 2 * np.pi, 160)
    z = np.linspace(0, 2 * np.pi, 160)
    xx, zz = np.meshgrid(x, z)
    phase = k * xx + m * zz
    qx, qz = np.meshgrid(np.linspace(0.4, 5.9, 10), np.linspace(0.4, 5.9, 10))
    qphase = k * qx + m * qz

    for ax, scalar, arrow_phase, title, bar_title in [
        (axes[0], np.cos(phase), np.cos(qphase), "Buoyancy and displacement", r"$\sigma/A=\cos\phi$"),
        (axes[1], np.sin(phase), np.sin(qphase), "Pressure and velocity", r"$pK^2/(mA)=\sin\phi$"),
    ]:
        field = ax.pcolormesh(xx, zz, scalar, cmap="RdBu_r", vmin=-1, vmax=1, shading="auto")
        ax.contour(xx, zz, phase, levels=np.arange(0, 6 * np.pi, np.pi), colors="#666666", linewidths=0.55, alpha=0.45)
        ax.quiver(qx, qz, m * arrow_phase / k, -arrow_phase, angles="xy", scale_units="xy", scale=4.5, width=0.006, color="#222222")
        ax.set(xlabel=r"$x$", ylabel=r"$z$", title=title, xlim=(0, 2 * np.pi), ylim=(0, 2 * np.pi))
        ax.set_aspect("equal")
        colorbar = fig.colorbar(field, ax=ax, orientation="horizontal", shrink=0.88, pad=0.03, ticks=[-1, 0, 1])
        colorbar.set_label(bar_title)

    axes[0].text(0.03, 0.96, r"Arrows: $(\xi,\zeta)\propto(m/k,-1)\cos\phi$", transform=axes[0].transAxes, va="top", fontsize=8, bbox={"facecolor": "white", "alpha": 0.92, "edgecolor": "none"})
    axes[1].text(0.03, 0.96, r"Arrows: $(u,w)\propto(m/k,-1)\sin\phi$", transform=axes[1].transAxes, va="top", fontsize=8, bbox={"facecolor": "white", "alpha": 0.92, "edgecolor": "none"})
    for vector, label, color, origin in [
        (phase_velocity, r"$\mathbf{c}_p$", "#17633c", (0.7, 2.6)),
        (group_velocity, r"$\mathbf{c}_g$", "#61388d", (0.7, 2.6)),
    ]:
        end = np.array(origin) + 4 * vector
        axes[0].annotate("", xy=end, xytext=origin, arrowprops={"arrowstyle": "->", "color": color, "lw": 2.3})
        axes[0].text(*(end + [0.05, 0.03]), label, color=color, fontsize=12, bbox={"facecolor": "white", "alpha": 0.88, "edgecolor": "none"})

    ax = axes[2]
    tip = phase_velocity
    end = phase_velocity + group_velocity
    for start, finish, color in [(np.zeros(2), tip, "#17633c"), (tip, end, "#61388d")]:
        ax.annotate("", xy=finish, xytext=start, arrowprops={"arrowstyle": "->", "lw": 2.8, "color": color})
    ax.plot([0, end[0]], [0, 0], color="#333333", lw=1.5)
    back = -phase_velocity / np.linalg.norm(phase_velocity)
    onward = group_velocity / np.linalg.norm(group_velocity)
    corner = np.array([tip + 0.035 * back, tip + 0.035 * (back + onward), tip + 0.035 * onward])
    ax.plot(corner[:, 0], corner[:, 1], color="#333333", lw=1)
    ax.text(*(tip / 2 + [-0.08, 0.015]), r"$\mathbf{c}_p$", color="#17633c", fontsize=15)
    ax.text(*((tip + end) / 2 + [0.015, 0.035]), r"$\mathbf{c}_g$", color="#61388d", fontsize=15)
    ax.text(end[0] / 2, -0.055, r"$N/K$", ha="center", fontsize=13)
    ax.text(end[0] / 2, -0.18, r"$\mathbf{c}_p\cdot\mathbf{c}_g=0$" + "\n" + r"$\mathbf{c}_p+\mathbf{c}_g=(N/K,0)$", ha="center", va="center", fontsize=11)
    ax.set(title="Phase/group right triangle", xlim=(-0.12, 0.69), ylim=(-0.25, 0.40))
    ax.set_aspect("equal")
    ax.axis("off")
    fig.suptitle(r"Upward/rightward phase; downward/rightward energy ($k=1$, $m=1.5$, $N=1$, $t=0$)", fontsize=12)
    output = Path("paper-49-internal-wave.png")
    fig.savefig(output, dpi=150, facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()