波动方程

单个脚本 examples/wave/wave.py 在单位正方形上以显式中心差分时间步进求解线性标量波动方程。它是 扩散 中热传导示例的经典双曲型对应。

强形式为

\[u_{tt} \;=\; c^2\, \Delta u \quad \text{in } \Omega, \qquad u = 0 \text{ on } \partial\Omega,\]

初始条件取自 WaveMultiFrequency 的多频傅里叶级数,初速度为零。脚本以 \(c = 2.0\)\(\Delta t = 10^{-3}\) 运行 100 步。

中心差分时间步进

标准的时间离散为

\[M\, U^{n+1} \;=\; 2\, M\, U^{n} - M\, U^{n-1} - \Delta t^2\, c^2\, A\, U^{n},\]

其中 \(M\) 是质量矩阵,\(A\) 是拉普拉斯刚度。这在时间上是显式的——每一步只需对一个固定的质量矩阵求解,因此“分解一次 / 多次回代”才是正确的模式。

第一步需要特殊处理,因为 \(U^{-1}\) 未知;利用泰勒展开 \(U^{-1} \approx U^{0} - \Delta t\, V^{0}\) 以及初始条件 \(V^{0} = 0\),得到

\[2\, M\, U^{1} \;=\; 2\, M\, U^{0} - \Delta t^2\, c^2\, A\, U^{0}.\]

TensorMesh 设置

两个极小的装配器(一个用于 \(M\),一个用于 \(A\))、一个用于齐次狄利克雷边界条件的 Condenser,以及一个 15 行的时间循环。注意那个对 SparseMatrix 做缩放、同时保持 SparseMatrix 类型的小辅助函数:

列表 8 examples/wave/wave.py
class AAssembler(ElementAssembler):
    def forward(self, gradu, gradv): return gradu @ gradv
class MAssembler(ElementAssembler):
    def forward(self, u, v):         return u * v

def scale_matrix(mat, s):
    return SparseMatrix(mat.edata * s, mat.row, mat.col, mat.shape)

mesh = Mesh.gen_rectangle(chara_length=0.01).to(device)
dataset = WaveMultiFrequency(K=16, c=c)

M = MAssembler.from_mesh(mesh, quadrature_order=2)()
A = AAssembler.from_mesh(mesh, quadrature_order=2)()
condenser = Condenser(mesh.boundary_mask)

u0 = dataset.initial_condition(mesh.points).to(device)
v0 = torch.zeros_like(u0)

# First step: 2 M U^1 = 2 M U^0 - dt^2 c^2 A U^0
cA = scale_matrix(A, c * c)
K  = scale_matrix(M, 2.0)
F  = -(dt*dt) * (cA @ u0) + 2.0 * (M @ u0) + (2.0 * dt) * (M @ v0)
K_, F_ = condenser(K, F)
U1 = condenser.recover(K_.solve(F_))
M_ = scale_matrix(K_, 0.5)            # K_ = 2 M_  →  M_ = K_/2

Us = [u0, U1]
for _ in range(n - 2):
    U_prev, U_curr = Us[-2], Us[-1]
    F  = 2.0*(M @ U_curr) - (M @ U_prev) - (dt*dt) * (cA @ U_curr)
    F_ = condenser.condense_rhs(F)
    U_ = M_.solve(F_)
    Us.append(condenser.recover(U_))

几点实用说明:

  • 稳定性。 中心差分是条件稳定的——\(\Delta t \lesssim h / c\)。脚本所取的参数(\(\Delta t = 10^{-3}\)\(h = 0.01\)\(c = 2\))能轻松满足该条件。

  • 分解一次。 M_(凝聚后的质量矩阵)从第一步的矩阵中恢复,并在每次循环迭代中重复使用;只有右端项发生变化。

  • 支持 GPU。 脚本在可用时选择 cudamesh.to(device) 一次调用即可将整个网格及缓冲区移动过去。快照仅在最后为可视化才移回 CPU。

For a higher-order time integrator (Newmark, Runge-Kutta) drop in ExplicitRungeKutta from tensormesh.ode — see 时间积分.

``wave.py`` 的输出单位正方形上多频驻波的有限元预测(左)与解析解(右)对比。在整个运行过程中,相位和振幅都紧跟解析参考解;只要满足 CFL 条件,中心差分就能保持能量守恒。

能量守恒

无阻尼波动方程守恒机械能

\[E \;=\; \underbrace{\tfrac12\, v^\top M\, v}_{\text{kinetic}} \;+\; \underbrace{\tfrac12\, u^\top (c^2 A)\, u}_{\text{potential}},\]

其中速度 \(v = u_t\) 取为所存储位移历史的中心差分。wave.py 在每一步都计算它并写出 wave_energy.png——这是对时间步进的直接检验,因为只要 CFL 条件成立,中心差分格式就能在 \(\mathcal{O}(\Delta t^2)\) 精度内守恒一个离散能量。

../_images/wave_energy.png

图 41 动能与势能反相交换,而总能量(红色)保持平直:100 步运行中的相对漂移约为 \(1.5\times10^{-3}\)。运动起始时为纯势能(初速度为零),随着驻波形成而趋于能量均分。

运行示例

cd examples/wave
python wave.py        # writes wave.mp4 (FEM vs analytical) and wave_energy.png

接下来

  • 时间积分——显式与隐式线性积分器类。

  • 稀疏求解器——后端选择,以及“分解一次 / 批量求解”的实用方法。

  • 机器学习数据集——同一个波动方程,在 100 个随机初始条件上批量求解。

  • 扩散——采用隐式 Euler 的抛物型对应。