codexcapillary-waves-crossover.py
capillary-waves-crossover.py
"""Plot stipulated deep-water gravity-capillary theory, never measured data.
Run from _media:
MPLCONFIGDIR=/tmp/codex-wiki-matplotlib python ../capillary-waves-crossover.py
Tested: Python 3.14.4, matplotlib 3.10.7+dfsg1, numpy 2.3.5.
The matching opaque white PNG basename is written to the current directory.
"""
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.ticker import ScalarFormatter
import numpy as np
def main():
gamma, rho, g = 0.072, 1000.0, 9.81 # N/m, kg/m³, m/s²: stipulated.
ell = np.sqrt(gamma / (rho * g))
crossover_mm = 2 * np.pi * ell * 1000
wavelength_mm = np.geomspace(0.8, 100, 1200)
k = 2 * np.pi / (wavelength_mm / 1000)
omega = np.sqrt(g * k + gamma * k**3 / rho)
cp = omega / k
cg = (g + 3 * gamma * k**2 / rho) / (2 * omega)
ratio = gamma * k**2 / (rho * g)
cmin = (4 * g * gamma / rho)**0.25
plt.rcParams.update({"font.size": 11, "axes.spines.top": False,
"axes.spines.right": False})
fig, (speed, balance) = plt.subplots(1, 2, figsize=(10.5, 5.0))
fig.patch.set_facecolor("white")
fig.suptitle("Gravity and surface tension meet at one wavelength", fontsize=17,
weight="bold", y=0.97)
fig.text(0.5, 0.90,
"Ideal deep-water theory • γ = 0.072 N/m, ρ = 1000 kg/m³, g = 9.81 m/s² • no measured data",
ha="center", fontsize=10)
for ax in (speed, balance):
ax.set_facecolor("white")
ax.axvspan(0.8, crossover_mm, color="#eaf3fa")
ax.axvspan(crossover_mm, 100, color="#fff4e4")
ax.axvline(crossover_mm, color="#45505a", linestyle=":", linewidth=1.4)
ax.set_xscale("log")
ax.set_xlim(0.8, 100)
ax.set_xticks([1, 3, 10, 30, 100])
ax.xaxis.set_major_formatter(ScalarFormatter())
ax.set_xlabel("Wavelength λ (mm)")
ax.grid(alpha=0.18, which="major")
speed.plot(wavelength_mm, cp, color="#1765a8", linewidth=2.5,
label=r"Crests: $c_p = \omega/k$")
speed.plot(wavelength_mm, cg, color="#bf5b13", linewidth=2.5,
label=r"Packet: $c_g = d\omega/dk$")
speed.plot(crossover_mm, cmin, "o", color="#28333d", markersize=6)
speed.annotate(f"Phase-speed minimum\n{cmin:.3f} m/s; here " + r"$c_p = c_g$",
xy=(crossover_mm, cmin), xytext=(7, 0.56), fontsize=10,
arrowprops={"arrowstyle": "->", "color": "#45505a"})
speed.text(1.0, 0.075, r"Short waves: $c_g \to 3c_p/2$", fontsize=10, color="#8c420d")
speed.text(23, 0.075, "Long waves:\n" + r"$c_g \to c_p/2$", fontsize=10, color="#8c420d")
speed.set_ylim(0, 1.23)
speed.set_ylabel("Speed (m/s)")
speed.set_title("A crest and its packet have different speeds", fontsize=11, pad=12)
speed.legend(loc="upper right", frameon=False, fontsize=10)
balance.plot(wavelength_mm, ratio, color="#476849", linewidth=2.5)
balance.axhline(1, color="#45505a", linestyle="--", linewidth=1.1)
balance.plot(crossover_mm, 1, "o", color="#28333d", markersize=6)
balance.set_yscale("log")
balance.set_ylim(0.02, 800)
balance.set_ylabel(r"Restoring-term ratio $(\gamma k^3/\rho)/(gk)$")
balance.set_title(r"Equality uses $\lambda = 2\pi\ell_c$, rather than $\lambda = \ell_c$", fontsize=11, pad=12)
balance.text(1.2, 150, "Surface tension dominates", color="#1765a8", fontsize=10)
balance.text(23, 0.055, "Gravity dominates", color="#996014", fontsize=10)
balance.annotate("Equal terms at\n" + r"$\lambda = 2\pi\ell_c$" + f" = {crossover_mm:.2f} mm",
xy=(crossover_mm, 1), xytext=(8, 25), fontsize=10,
arrowprops={"arrowstyle": "->", "color": "#45505a"})
balance.axvline(ell * 1000, color="#7f8c96", linestyle="--", linewidth=1)
balance.text(ell * 1000 * 1.10, 0.13, r"$\ell_c$" + f" = {ell*1000:.2f} mm\n(length scale)",
fontsize=9, color="#52616b")
fig.subplots_adjust(left=0.075, right=0.98, bottom=0.13, top=0.78, wspace=0.31)
fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), dpi=100,
facecolor="white", transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()