3. 数据可视化与结果呈现
数值计算得到数组只是中间产物,真正让物理结论“看得见”的是可视化。本节以自由落体与简谐振动两个经典模型为载体,串起 Matplotlib 的二维绘图、子图布局、曲线标注、误差棒、FuncAnimation 动画,以及图像与数据的保存。先约定坐标系:取竖直向上为 y 正方向,重力加速度 g = 9.8 m/s²,自由落体初速度为零、初始高度 y₀ = 20 m;简谐振动取 x(t) = A·cos(ωt + φ),A = 1.0 m,ω = 2π/T,T = 2.0 s,φ = 0。全文所有公式与图示均遵循这一约定。
一、二维绘图的基本骨架
Matplotlib 的绘图逻辑是“先建画布与坐标轴,再往坐标轴上添加图元”。面向对象接口比 pyplot 状态机接口更清晰,尤其在子图与动画场景中更可控。下面用数值积分生成自由落体的高度序列,并绘制 y–t 曲线:
import numpy as np
import matplotlib.pyplot as plt
g, y0, v0 = 9.8, 20.0, 0.0
t = np.linspace(0, 2.0, 400)
y = y0 + v0 * t - 0.5 * g * t**2
y = np.clip(y, 0, None) # 落地后高度不再为负
fig, ax = plt.subplots(figsize=(6, 4))
ax.plot(t, y, color="#1f77b4", lw=2, label="自由落体 y(t)")
ax.set_xlabel("时间 t / s")
ax.set_ylabel("高度 y / m")
ax.set_title("自由落体高度随时间变化")
ax.grid(alpha=0.3)
ax.legend()
fig.savefig("free_fall.png", dpi=150, bbox_inches="tight")
plt.show()
这里 np.clip 处理了落地后的负高度,避免曲线穿到坐标轴下方造成误读;dpi=150 与 bbox_inches="tight" 保证导出图像清晰且不留多余白边。判断绘图是否成功,除了看图,还应检查 ax.get_xlim()、ax.get_ylim() 是否符合物理预期——例如 y 轴上限应略高于 20 m。
二、子图布局:把两个模型并置比较
自由落体是单调加速下落,简谐振动是周期性往复,二者放在同一画布的不同子图中对照,能直观看出“非周期”与“周期”的差别。用 plt.subplots(1, 2) 建立一行两列布局,并通过 sharex 共享时间轴:
A, T = 1.0, 2.0
omega = 2 * np.pi / T
x = A * np.cos(omega * t)
fig, axes = plt.subplots(1, 2, figsize=(10, 4), sharex=True)
axes[0].plot(t, y, color="#1f77b4")
axes[0].set_title("自由落体")
axes[0].set_ylabel("高度 y / m")
axes[1].plot(t, x, color="#d62728")
axes[1].set_title("简谐振动")
axes[1].set_ylabel("位移 x / m")
axes[1].set_xlabel("时间 t / s")
fig.tight_layout()
tight_layout() 自动调整子图间距,防止标签重叠。若子图数量更多,可改用 GridSpec 实现不等宽布局,例如把相图(x–v 平面)放在右侧较窄的列中。
三、曲线标注与误差棒
标注的作用是把读者的注意力引向关键物理量。对简谐振动,可标出振幅 A、周期 T 与平衡位置;对自由落体,可标出落地时刻。用 ax.annotate 配合箭头指向特征点,用 ax.axhline 画平衡位置参考线。误差棒则用于表达测量或数值结果的不确定度:假设在若干时刻测得位移,标准差为 0.05 m,可用 ax.errorbar 绘制:
t_meas = np.array([0.0, 0.5, 1.0, 1.5, 2.0])
x_meas = A * np.cos(omega * t_meas)
x_err = np.full_like(x_meas, 0.05)
axes[1].errorbar(t_meas, x_meas, yerr=x_err,
fmt="o", capsize=4, color="#2ca02c",
label="测量点 ± 0.05 m")
axes[1].axhline(0, color="gray", ls="--", lw=1)
axes[1].annotate("振幅 A", xy=(0, A), xytext=(0.6, 1.15),
arrowprops=dict(arrowstyle="->"))
axes[1].legend()
误差棒的长度应与物理不确定度一致,不能为了“好看”随意设定;capsize 让误差棒端帽清晰可辨,fmt="o" 同时画出数据点。
四、FuncAnimation 动画入门
动画适合展示随时间演化的过程,例如小球下落或振子在平衡位置附近往复。核心是定义一个更新函数 update(frame),每帧修改图元数据并返回图元列表。注意必须保留动画对象引用,否则会被垃圾回收导致动画不播放:
from matplotlib.animation import FuncAnimation
fig2, ax2 = plt.subplots(figsize=(5, 4))
ax2.set_xlim(-1.5, 1.5)
ax2.set_ylim(-1.5, 1.5)
point, = ax2.plot([], [], "o", ms=12, color="#d62728")
line, = ax2.plot([], [], "-", lw=1, color="#9aa7b8")
def update(frame):
tt = frame * 0.05
xx = A * np.cos(omega * tt)
point.set_data([xx], [0])
line.set_data([0, xx], [0, 0])
return point, line
ani = FuncAnimation(fig2, update, frames=80, interval=40, blit=True)
ani.save("shm.gif", writer="pillow", fps=25)
frames=80 与 interval=40 共同决定动画时长约 3.2 s;blit=True 只重绘变化图元,提升流畅度。保存 GIF 需要 Pillow,保存 MP4 则需 ffmpeg。判断动画是否正常,可先不保存、直接 plt.show() 观察一帧帧是否连续。
五、保存图像与数据
图像用 fig.savefig 保存,数据则建议用 np.savetxt 或 np.savez 落盘,便于后续复算与绘图分离。把时间、高度、位移三列写入 CSV,并附上表头:
data = np.column_stack([t, y, x])
np.savetxt("fall_shm.csv", data, delimiter=",",
header="t,y,x", comments="", fmt="%.6f")
np.savez("fall_shm.npz", t=t, y=y, x=x)
CSV 便于人工查看与 Excel 打开,NPZ 保留数组精度与键名,二者按用途选择。保存后应回读校验,例如 np.loadtxt 读回并比较首行数值,确认没有列错位或精度丢失。
dpi 与 bbox_inches,保证不同机器上结果可复现。ani)并保持引用;若在函数内部创建后直接返回,动画可能在显示前被回收而“静止不动”。把上述环节串起来,一个完整的可视化流程是:数值生成数组 → 建立画布与子图 → 绘制曲线并标注特征量 → 叠加误差棒表达不确定度 → 用动画展示演化 → 分别保存图像与数据。自由落体与简谐振动这两个模型虽简单,却覆盖了二维绘图、布局、标注、误差棒、动画与落盘的全部关键接口,后续更复杂的物理场可视化都可以在此基础上扩展。