codexkundt-tube-pressure-motion-and-spacing-fit.py
kundt-tube-pressure-motion-and-spacing-fit.py
"""Original closed-tube acoustic mode and explicitly synthetic position fit.
Run from the output directory. Produces the matching opaque white PNG.
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 fields(x, t, *, pressure=1.0, density=1.2, speed=343.0, frequency=1000.0):
omega = 2 * np.pi * frequency
k = omega / speed
p = pressure * np.cos(k * np.asarray(x)) * np.cos(omega * np.asarray(t))
u = pressure / (density * speed) * np.sin(k * np.asarray(x)) * np.sin(omega * np.asarray(t))
return p, u
def example():
j = np.arange(6)
residuals = np.array([1, -1, 0, 0, -1, 1]) * 0.001
positions = 0.024 + 0.1715 * j + residuals
design = np.column_stack((np.ones(j.size), j))
intercept, spacing = np.linalg.lstsq(design, positions, rcond=None)[0]
frequency = 1000.0
sigma_spacing = 0.001 / np.sqrt(np.sum((j - j.mean())**2))
sigma_speed = np.hypot(2 * frequency * sigma_spacing, 2 * spacing * 1.0)
return j, positions, intercept, spacing, sigma_spacing, sigma_speed
def main():
plt.rcParams.update({"font.size": 11, "axes.titlesize": 12})
fig, axes = plt.subplots(2, 2, figsize=(11, 7.2), facecolor="white")
ax = axes[0, 0]
s = np.linspace(0, 1.5, 700)
ax.plot(s, np.abs(np.cos(2 * np.pi * s)), color="#0072B2", lw=2,
label="pressure amplitude / P")
ax.plot(s, np.abs(np.sin(2 * np.pi * s)), color="#D55E00", lw=2,
label=r"velocity amplitude / $[P/(\rho_0c)]$")
ax.annotate("", xy=(0.25, 1.06), xytext=(0, 1.06),
arrowprops={"arrowstyle": "<->"})
ax.text(0.125, 1.1, r"$\lambda/4$", ha="center")
ax.set(xlim=(0, 1.5), ylim=(0, 1.25), xlabel=r"position, $x/\lambda$",
ylabel="normalized amplitude")
ax.set_title("A. Pressure and motion peak at different positions", loc="left")
ax.legend(fontsize=9, loc="lower right")
ax = axes[0, 1]
tau = np.linspace(0, 2, 700)
ax.plot(tau, np.cos(2 * np.pi * tau), color="#0072B2", lw=2, label="pressure")
ax.plot(tau, np.sin(2 * np.pi * tau), color="#D55E00", lw=2, label="velocity")
ax.set(xlim=(0, 2), ylim=(-1.2, 1.2), xlabel="time / wave period",
ylabel="signed trace / its local peak amplitude")
ax.set_title(r"B. At $kx=\pi/4$: a quarter-cycle time shift", loc="left")
ax.legend(fontsize=9, loc="lower right")
j, positions, a, d, sigma_d, sigma_c = example()
ax = axes[1, 0]
ax.errorbar(j, positions * 1000, yerr=np.ones(j.size), fmt="o",
capsize=4, color="#0072B2", label="stipulated positions")
ax.plot(j, (a + d * j) * 1000, color="#D55E00", lw=1.7, label="fitted line")
ax.set(xlabel="consecutive maximum index j", ylabel="ruler coordinate (mm)")
ax.set_title("C. Fit an origin and a spacing", loc="left")
ax.legend(fontsize=9, loc="upper left")
ax.text(0.97, 0.08,
f"d = {d * 1000:.1f} mm\nc = 2fd = {2 * 1000 * d:.1f} m/s",
transform=ax.transAxes, ha="right",
bbox={"facecolor": "white", "edgecolor": "#999999", "alpha": 0.95})
ax = axes[1, 1]
residual = (positions - a - d * j) * 1000
ax.axhline(0, color="#777777", lw=1)
ax.errorbar(j, residual, yerr=np.ones(j.size), fmt="o",
capsize=4, color="#0072B2")
ax.set(xlabel="consecutive maximum index j", ylabel="position residual (mm)",
ylim=(-2.6, 3.8))
ax.set_title("D. Residuals and stipulated independent errors", loc="left")
ax.text(0.5, 0.92,
f"σx = 1 mm, σf = 1 Hz\nσd = {sigma_d * 1000:.4f} mm, σc = {sigma_c:.4f} m/s",
transform=ax.transAxes, ha="center", va="top", fontsize=10)
for ax in axes.flat:
ax.grid(alpha=0.18)
fig.suptitle("Kundt tube: an original lossless mode and synthetic spacing measurement",
fontsize=14, y=0.985)
fig.tight_layout(rect=(0, 0, 1, 0.95), h_pad=2.0, w_pad=1.6)
fig.savefig(Path.cwd() / Path(__file__).with_suffix(".png").name,
dpi=140, facecolor="white")
plt.close(fig)
if __name__ == "__main__":
main()