codex/standing-wave-nodes-mode-spectrum.py

standing-wave-nodes-mode-spectrum.py
"""Original spatial amplitude and discrete mode spectrum of an ideal string.

Run from _media; tested with Python 3.14, matplotlib 3.10.7, numpy 2.3.5.
"""
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0.0, 1.0, 1201)
nodes = np.arange(4) / 3
antinodes = (2 * np.arange(3) + 1) / 6
n = np.arange(1, 7)
f = n * 10
fig, (spatial, spectrum) = plt.subplots(2, 1, figsize=(9.6, 6.4), facecolor='white')
spatial.plot(x, np.abs(np.sin(3 * np.pi * x)), color='#245cb3', lw=2.8,
             label='Oscillation amplitude / maximum amplitude')
spatial.scatter(nodes, np.zeros(4), color='#222', s=45, zorder=4, label='Nodes')
spatial.scatter(antinodes, np.ones(3), color='#ca6d16', s=45, zorder=4, label='Antinodes')
for xp in nodes:
    spatial.axvline(xp, color='#d9dde3', ls=':', lw=1)
spatial.annotate('', xy=(0, 0.28), xytext=(1/3, 0.28),
                 arrowprops={'arrowstyle': '<->', 'color': '#333', 'lw': 1.4})
spatial.text(1/6, 0.36, r'Node spacing $=\lambda/2=1/3$ m', ha='center', fontsize=10,
             bbox={'facecolor': 'white', 'edgecolor': 'none', 'pad': 2})
spatial.set_title(r'Third mode: three loops, four nodes, $\lambda=2/3$ m', loc='left', fontsize=13)
spatial.set_xlim(-0.02, 1.02)
spatial.set_ylim(-0.10, 1.18)
spatial.set_xticks(np.arange(7)/6, ['0', '1/6', '1/3', '1/2', '2/3', '5/6', '1'])
spatial.set_xlabel(r'Position $x$ (m)', fontsize=11)
spatial.set_ylabel(r'Amplitude $/(2a)$', fontsize=11)
spatial.legend(loc='upper center', bbox_to_anchor=(0.5, -0.23), ncol=3, frameon=False, fontsize=9)
colors = ['#879bb9' if i != 3 else '#245cb3' for i in n]
spectrum.vlines(f, 0, 1, colors=colors, linewidth=2.5)
spectrum.scatter(f, np.ones(6), c=colors, s=55, zorder=3)
for ni, fi in zip(n, f, strict=True):
    spectrum.text(fi, 1.12, f'n = {ni}', ha='center', fontsize=10)
spectrum.set_title(r'Allowed fixed-end frequencies: $f_n=nc/(2L)=10n$ Hz', loc='left', fontsize=13)
spectrum.set_xlim(0, 70)
spectrum.set_ylim(0, 1.4)
spectrum.set_xticks(np.arange(0, 71, 10))
spectrum.set_yticks([])
spectrum.set_xlabel(r'Frequency $f$ (Hz)', fontsize=11)
spectrum.text(0.98, 0.24, 'Line heights only mark allowed frequencies;\nthey do not represent measured amplitudes.',
              transform=spectrum.transAxes, ha='right', fontsize=10,
              bbox={'facecolor': 'white', 'edgecolor': 'none', 'pad': 2})
for ax in (spatial, spectrum):
    ax.set_facecolor('white')
    ax.spines[['top', 'right']].set_visible(False)
spectrum.spines['left'].set_visible(False)
fig.suptitle('Spatial nodes and the discrete mode spectrum', fontsize=17, y=0.99)
fig.text(0.5, 0.94, r'Constructed uniform fixed-end string: $L=1$ m, $c=20$ m/s', ha='center', fontsize=11)
fig.subplots_adjust(left=0.095, right=0.975, top=0.86, bottom=0.10, hspace=0.76)
fig.savefig(Path.cwd() / (Path(__file__).stem + '.png'), dpi=100, facecolor='white', transparent=False)
plt.close(fig)