codexstanding-string-normal-modes.py
standing-string-normal-modes.py
"""Generate standing-string-normal-modes.png in the current working directory.
Tested: Python 3.14.4, NumPy 2.3.5, Matplotlib 3.10.7.
Run from _media with MPLBACKEND=Agg MPLCONFIGDIR=/tmp.
The parameters describe an ideal model, not a measured musical instrument.
"""
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
def main():
plt.rcParams.update({"font.size": 11, "figure.facecolor": "white",
"axes.facecolor": "white", "savefig.facecolor": "white",
"axes.spines.top": False, "axes.spines.right": False})
length, tension, density = 1.0, 1.0, 1.0
speed = np.sqrt(tension / density)
position = np.linspace(0, length, 1201)
snapshots = [(0, "0", "#176ca4", "-"),
(1 / 8, "1/8", "#dc951c", "--"),
(1 / 4, "1/4", "#40845a", ":"),
(1 / 2, "1/2", "#bd446b", "-.")]
fig, axes = plt.subplots(3, 1, figsize=(8.4, 6.7), sharex=True)
fig.subplots_adjust(left=0.10, right=0.97, top=0.81, bottom=0.15, hspace=0.57)
fig.suptitle("First three normal modes of an ideal fixed string\n"
r"$L=1$ m, $\tau=1$ N, $\mu=1$ kg/m; $c=1$ m/s",
fontsize=13, y=0.98)
fig.text(0.5, 0.865, r"Snapshots use each mode's OWN period $T_n=1/f_n$; phase $\varphi=0$",
ha="center", fontsize=10)
for n, ax in enumerate(axes, start=1):
frequency = n * speed / (2 * length)
period = 1 / frequency
shape = np.sin(n * np.pi * position / length)
for fraction, label, color, style in snapshots:
factor = np.cos(2 * np.pi * fraction)
if fraction == 1 / 4:
factor = 0.0
ax.plot(position / length, shape * factor, color=color, ls=style,
lw=2.1, label=rf"$t/T_n={label}$")
nodes = np.arange(n + 1) / n
ax.scatter(nodes, np.zeros_like(nodes), color="#222222", s=30,
zorder=5, label="permanent nodes (ends included)")
ax.set(xlim=(-0.025, 1.025), ylim=(-1.25, 1.25), ylabel=r"$u_n/A$")
ax.set_yticks([-1, 0, 1])
period_label = ["2", "1", "2/3"][n - 1]
ax.set_title(rf"$n={n}$: $f_n={frequency:g}$ Hz, $T_n={period_label}$ s; "
f"{n - 1} interior node{'s' if n != 2 else ''}", loc="left", fontsize=11)
ax.grid(alpha=0.18)
axes[-1].set_xlabel(r"Position $x/L$")
axes[-1].set_xticks([0, 1 / 4, 1 / 2, 3 / 4, 1])
handles, labels = axes[0].get_legend_handles_labels()
fig.legend(handles, labels, loc="lower center", bbox_to_anchor=(0.5, 0.006),
ncol=3, fontsize=9, frameon=False)
fig.savefig(Path.cwd() / "standing-string-normal-modes.png", dpi=140,
facecolor="white", transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()