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" 同时画出数据点。

自由落体与简谐振动:双子图对照 时间 t / s 高度 y / m 落地 t≈2.0 s 自由落体 y(t) 时间 t / s 位移 x / m 平衡位置 测量点 ±0.05 m 简谐振动 x(t) 振幅 A=1.0 m

四、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)并保持引用;若在函数内部创建后直接返回,动画可能在显示前被回收而“静止不动”。

把上述环节串起来,一个完整的可视化流程是:数值生成数组 → 建立画布与子图 → 绘制曲线并标注特征量 → 叠加误差棒表达不确定度 → 用动画展示演化 → 分别保存图像与数据。自由落体与简谐振动这两个模型虽简单,却覆盖了二维绘图、布局、标注、误差棒、动画与落盘的全部关键接口,后续更复杂的物理场可视化都可以在此基础上扩展。