有限差分法求解波动方程
要通过数值方法求解偏微分方程,可以先将求解区域划分成细密的网格,然后在时间域上迭代更新。
1. 问题描述
在给定初值条件的情况下,通过数值方法近似求解波动方程:
\[\frac{\partial ^2u}{\partial t^2}=\Delta u=\frac{\partial ^2u}{\partial x^2}+\frac{\partial ^2u}{\partial y^2}+\frac{\partial ^2u}{\partial z^2}\]在下文中,我们将 $u(x,y,z,t)$ 简记为 $u(\mathbf{x},t)$.
1.1. 问题区域
我们先将问题区域限定在一个立方体中:
\[\Omega=\left[0 \leqslant x \leqslant L_{x}\right] \times\left[0 \leqslant y \leqslant L_{y}\right] \times\left[0 \leqslant z \leqslant L_{z}\right]\]1.2. 初值条件
首先是时间上的初值条件:
\[\begin{aligned} \left.u\right|_{t=0} &=\varphi(x, y, z) \\ \left.\frac{\partial u}{\partial t}\right|_{t=0} &=0 \end{aligned}\]然后是空间上的初值条件,可以有两种情况:第一类齐次边界条件,也就是在边界上都为0,
\[\begin{array}{ll} u(0, y, z, t)=0, & u\left(L_{x}, y, z, t\right)=0 \\ u(x, 0, z, t)=0, & u\left(x, L_{y}, z, t\right)=0 \\ u(x, y, 0, t)=0, & u\left(x, y, L_{z}, t\right)=0 \end{array}\]或周期性边界条件,
\[\begin{array}{ll} u(0, y, z, t)=u\left(L_{x}, y, z, t\right), & u_{x}(0, y, z, t)=u_{x}\left(L_{x}, y, z, t\right) \\ u(x, 0, z, t)=u\left(x, L_{y}, z, t\right), & u_{y}(x, 0, z, t)=u_{y}\left(x, L_{y}, z, t\right) \\ u(x, y, 0, t)=u\left(x, y, L_{z}, t\right), & u_{z}(x, y, 0, t)=u_{z}\left(x, y, L_{z}, t\right) \end{array}\]2. 数值求解
2.1. 差分法
所谓差分就是用数值的方法来近似计算梯度:
\[\begin{aligned} \frac{\partial f(x)}{\partial x}&= \lim_{\Delta x\rightarrow 0} \frac{f(x+\Delta x)-f(x)}{\Delta x}\\ &\approx \frac{f(x+\Delta x)-f(x)}{\Delta x} \\ \end{aligned}\] \[\begin{aligned} \dfrac{\partial ^{2}f}{\partial x^{2}} &= \lim_{\Delta x\rightarrow 0} \frac{f'(x+\Delta x)-f'(x)}{\Delta x}\\ &\approx \frac{\frac{f\left( x+\Delta x\right) -f\left( x\right) }{\Delta x}-\frac{f\left( x\right) -f\left( x-\Delta x\right) }{\Delta x}}{\Delta x}\\ &= \frac{f(x+\Delta x)+f(x-\Delta x)-2f(x)}{\Delta x^2}\\ \end{aligned}\]2.2. 网格划分
我们将立方体区域 $\Omega$ 划分为 $N\times N \times N$的立方体网格,每个网格的尺寸为 $h_x\times h_y \times h_z$. 我们以每个网格的顶点作为计算节点,如下图所示.
\[\begin{aligned} \bar{\omega}_{h} &=\left\{\left(x_{i}=i h_{x}, y_{j}=j h_{y}, z_{k}=k h_{z}\right), i, j, k=0,1, \ldots, N, h_{x} N=L_{x}, h_{y} N=L_{y}, h_{z} N=L_{z}\right\} \\ \omega_{\tau} &=\left\{t_{n}=n \tau, n=0,1, \ldots, K, \tau K=T\right\}. \end{aligned}\]上面的公式中, \(\bar{\omega}_{h}\) 表示网格区域的划分,\(\omega_{\tau}\) 表示时间上的划分,每个时间节点之间的跨度为 $\tau$.
2.3. 差分模拟
我们要通过数值近似模拟波动方程:
\[\frac{\partial ^2u}{\partial t^2}=\Delta u=\frac{\partial ^2u}{\partial x^2}+\frac{\partial ^2u}{\partial y^2}+\frac{\partial ^2u}{\partial z^2}\]首先将函数 $u(x,y,z,t)$ 在节点$(x_i,y_j,z_k,t_n)$处取值,记为:
\[u(x_i,y_j,z_k,t_n)=u_{ijk}^n\]这里的下标 $i,j,k$ 表示在空间上的$(x_i,y_j,z_k)$处取值,上标 $n$ 表示此时的时间为$t_n$.
2.3.1. 等式左侧的模拟
我们先对等式左侧进行差分模拟:
\[\frac{\partial ^2u(\mathbf{x},t)}{\partial t^2}\approx \frac{u(\mathbf{x},t+\Delta t)+u(\mathbf{x},t-\Delta t)-2u(\mathbf{x},t)}{\Delta t^2}\]体现在网格上就是:
\[\frac{\partial ^2u(\mathbf{x},t_n)}{\partial t^2}\approx \frac{u(\mathbf{x},t_{n+1})+u(\mathbf{x},t_{n-1})-2u(\mathbf{x},t_{n})}{\tau^2}\]简写为上下标的形式为:
\[\left. \dfrac{\partial ^2u(\mathbf{x},t)}{\partial t^2} \right |_{u_{i j k}^{n}} \approx \dfrac{u_{i j k}^{n+1}-2 u_{i j k}^{n}+u_{i j k}^{n-1}}{\tau^{2}}\]2.3.2. 等式右侧的模拟
接下来是对等式右侧$\Delta u=\frac{\partial ^2u}{\partial x^2}+\frac{\partial ^2u}{\partial y^2}+\frac{\partial ^2u}{\partial z^2}$的模拟,我们首先模拟 $\frac{\partial ^2u}{\partial x^2}$ :
\[\frac{\partial ^2u(x,y,z,t)}{\partial x^2} \approx \dfrac{u(x+\Delta x,y,z,t)+u(x-\Delta x,y,z,t)-2u(x,y,z,t)}{\Delta x^2}\]即:
\[\left. \dfrac{\partial ^2u}{\partial x^2} \right |_{u_{i j k}^{n}} \approx \dfrac{u_{i+1, j, k}^{n}-2 u_{i, j, k}^{n}+u_{i-1, j, k}^{n}}{h_x^{2}}\]类似的,我们可以求出$u$关于$y$和$z$的二阶导,整合后得到:
\[\Delta {u_{i j k}^{n}} \approx \dfrac{u_{i+1, j, k}^{n}-2 u_{i, j, k}^{n}+u_{i-1, j, k}^{n}}{h_x^{2}} + \dfrac{u_{i, j+1, k}^{n}-2 u_{i, j, k}^{n}+u_{i, j-1, k}^{n}}{h_y^{2}} + \dfrac{u_{i, j, k+1}^{n}-2 u_{i, j, k}^{n}+u_{i, j, k-1}^{n}}{h_z^{2}}\]2.4. 迭代求解
我们通过波动方程的离散形式:
\[\left. \dfrac{\partial ^2u(\mathbf{x},t)}{\partial t^2} \right |_{u_{i j k}^{n}}= \Delta {u_{i j k}^{n}}\]获得了方程组:
\[\dfrac{u_{i j k}^{n+1}-2 u_{i j k}^{n}+u_{i j k}^{n-1}}{\tau^{2}}=\dfrac{u_{i+1, j, k}^{n}-2 u_{i, j, k}^{n}+u_{i-1, j, k}^{n}}{h_x^{2}} + \dfrac{u_{i, j+1, k}^{n}-2 u_{i, j, k}^{n}+u_{i, j-1, k}^{n}}{h_y^{2}} + \dfrac{u_{i, j, k+1}^{n}-2 u_{i, j, k}^{n}+u_{i, j, k-1}^{n}}{h_z^{2}}\]接下来我们对该方程组进行恒定变形,得到:
\[\begin{aligned} u_{i j k}^{n+1}&=2\left( 1-\frac{\tau^{2}}{h_x^{2}}-\frac{\tau^{2}}{h_y^{2}}-\frac{\tau^{2}}{h_z^{2}} \right) u_{i j k}^{n}-u_{i j k}^{n-1} +\frac{\tau^{2}}{h_x^{2}}\left( u_{i+1, j, k}^{n}+u_{i-1, j, k}^{n} \right)\\ &\qquad +\frac{\tau^{2}}{h_y^{2}}\left( u_{i, j+1, k}^{n}+u_{i, j-1, k}^{n} \right) +\frac{\tau^{2}}{h_z^{2}}\left( u_{i, j, k+1}^{n}+u_{i, j, k-1}^{n} \right) \\ & = 2\left( 1-r_x-r_y-r_z \right) u_{i j k}^{n}-u_{i j k}^{n-1} +r_x\left( u_{i+1, j, k}^{n}+u_{i-1, j, k}^{n} \right)\\ &\qquad+r_y\left( u_{i, j+1, k}^{n}+u_{i, j-1, k}^{n} \right) +r_z\left( u_{i, j, k+1}^{n}+u_{i, j, k-1}^{n} \right) \end{aligned}\]这里将 $\frac{\tau^{2}}{h_x^{2}}$ 记为 $r_x$,使得看起来更简洁。那么我们的迭代策略也很明显了:
\[u_{i j k}^{n+1} =2\left( 1-r_x-r_y-r_z \right) u_{i j k}^{n}-u_{i j k}^{n-1} +r_x\left( u_{i+1, j, k}^{n}+u_{i-1, j, k}^{n} \right)+r_y\left( u_{i, j+1, k}^{n}+u_{i, j-1, k}^{n} \right) +r_z\left( u_{i, j, k+1}^{n}+u_{i, j, k-1}^{n} \right)\]2.5. 具体流程
-
通过 $t=0$ 时的初值条件计算 $u_{ijk}^0=\varphi(x_i,y_j,z_k)$.
-
要计算 $u_{ijk}^1$,如果通过上面的方程来计算的话,也就是$n=0$的情况,此时需要 $u^0$ 和 $u^{-1}$,但我们不知道$u^{-1}$的值,这时就需要进行一点修改,原方程为:
\[\frac{u^{1}-2u^0+u^{-1}}{\tau^2}=\Delta u^0\]我们可以假设 $u^{1}-u^0=u^0-u^{-1}$,这样就得到了:
\[\begin{aligned} u^1_{ijk}&=u^0_{ijk}+\frac{\tau^2}{2}\Delta u^0_{ijk}\\ &=u^0_{ijk}+\frac{\tau^2}{2}\Delta \varphi(x_i,y_j,z_k)\\ \end{aligned}\] -
接下来要计算$u_{ijk}^1,u_{ijk}^2,\ldots$,就可直接用迭代方程求解即可:
3. 实验
接下来用 Python 对二维情况进行模拟,这里使用二维情况是为了方便可视化,实际上二维和三维的区别不大。 这里我们对以下函数进行模拟:
\[u(x,y,t)=\sin \left(\frac{\pi}{L_{x}} x\right) \cdot \sin \left(\frac{\pi}{L_{y}} y\right) \cdot \cos \left(a_{t} \cdot t\right),\quad a_{t}=\pi \sqrt{\frac{1}{L_{x}^{2}}+\frac{1}{L_{y}^{2}}}\]那么我们的初值函数为:
\[\varphi(x,y)=\sin \left(\frac{\pi}{L_{x}} x\right) \cdot \sin \left(\frac{\pi}{L_{y}} y\right)\]第一步是计算 $u^0$ 的情况: \(u(x_i,y_j,t=0)=\varphi(x_i,y_j)\)
第二步是计算 $u^1$,使用公式:
\[u^1=u^0+\frac{\tau^2}{2}\Delta \varphi(x_i,y_j)\\\]这里初值函数的Laplace算子为:
\[\Delta \varphi(x,y)=-\left(\left(\frac{\pi}{L_{x}}\right)^2+\left(\frac{\pi}{L_{y}} \right)^2\right)\sin \left(\frac{\pi}{L_{x}} x\right) \cdot \sin \left(\frac{\pi}{L_{y}} y\right)\]第三步为迭代更新:
\[u_{i j}^{n+1} =2\left( 1-r_x-r_y \right) u_{i j }^{n}-u_{i j }^{n-1} +r_x\left( u_{i+1, j}^{n}+u_{i-1, j}^{n} \right) +r_y\left( u_{i, j+1}^{n}+u_{i, j-1}^{n} \right)\]这里我们有一点要注意:我们更新 $u^{n+1}_{ij}$ 时需要用到上下左右中 5 个顶点的值来计算差分,但边界上的 $u$ 值可以直接根据边界条件算出,不需要通过差分模拟计算,这也就避免了边界点处无法计算差分的问题。
在输入条件为 $L_x=L_y=2,T=4,N=50,K=500$ 的情况下,我们得到的模拟解大致是这样的:
我们也计算了误差随着迭代次数的变化情况,居然还能忽上忽下,属实有点神奇。
最后提醒一点,为了保证模拟结果收敛,我们要保证步长比小于1:
\[\frac{\tau}{\frac{1}{2}\sqrt{h_x^2+h_y^2}}\leq 1\]由于我们的 $r_x=r_y$ ,因此只需 $r_x\leq 1/4$ 即可。
4. Python 代码
import numpy as np
import matplotlib.pyplot as plt
import os
from PIL import Image
class WaveEquation2D:
"""
对二维情况下的波动方程进行模拟
使用第一类齐次边界条件
"""
def __init__(self, Lx, Ly, Lt, N, K):
"""
设定网格范围和划分个数
"""
self.U = np.zeros((K+1, N+1, N+1))
self.N = N
self.K = K
self.Lt = Lt
self.Lx = Lx
self.Ly = Ly
self.dx = Lx/N
self.dy = Ly/N
self.dt = T/K
self.rx = (self.dt/self.dx)**2
self.ry = (self.dt/self.dy)**2
self.X = np.arange(N+1) * self.dx
self.Y = np.arange(N+1) * self.dy
self.T = np.arange(K+1) * self.dt
def set_u0(self):
"""
定义 t=0 时刻的初值函数
"""
Fx = np.sin(np.pi * self.X / self.Lx)
Fy = np.sin(np.pi * self.Y / self.Ly)
self.U[0, :, :] = np.outer(Fx, Fy)
def set_u1(self):
"""
通过 u0 的 Laplace 算子计算 u1
"""
Fx = np.sin(np.pi * self.X / self.Lx)
Fy = np.sin(np.pi * self.Y / self.Ly)
DeltaU = -(np.pi/self.Lx)**2 * np.outer(Fx, Fy) - \
(np.pi/self.Ly)**2 * np.outer(Fx, Fy)
self.U[1, :, :] = self.U[0, :, :] + self.dt**2 / 2*DeltaU
def simulation(self):
"""
仿真模拟
"""
## 计算 u0
self.set_u0()
## 计算 u1
self.set_u1()
rx = self.rx
ry = self.ry
## 从 t=2 的时刻开始迭代更新
for n in range(1, self.K):
## 边界处的值由边界条件确定, 我们只更新内部
for i in range(1, self.N):
for j in range(1, self.N):
self.U[n+1, i, j] = 2*(1-rx-ry)*self.U[n, i, j]
self.U[n+1, i, j] -= self.U[n-1, i, j]
self.U[n+1, i, j] += rx * \
(self.U[n, i+1, j]+self.U[n, i-1, j])
self.U[n+1, i, j] += ry * \
(self.U[n, i, j+1]+self.U[n, i, j-1])
## 更新边界条件
self.U[n+1, [0, -1], :] = 0
self.U[n+1, :, [0, -1]] = 0
def animate(self):
"""
通过动画演示, 保存为 gif 动图
"""
if not os.path.exists("Figure"):
os.makedirs("Figure")
fig = plt.figure(2)
ax = fig.add_subplot(111, projection='3d')
X, Y = np.meshgrid(self.X, self.Y)
imgs = []
## 隔 4 次计算保存一次图
for n in range(0, self.K+1, 4):
t = self.T[n]
ax.plot_surface(
X, Y, self.U[n, :, :], cmap='rainbow', linewidth=2, antialiased=False)
ax.set_xlim3d(0, self.Lx)
ax.set_ylim3d(0, self.Ly)
ax.set_zlim3d(-1.5, 1.5)
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.set_zlabel("U")
ax.text(1, 1, 1, "T = {:.2f}s".format(t),
size=plt.rcParams["axes.titlesize"],
ha="center", transform=ax.transAxes, )
plt.savefig("Figure/"+str(n), dpi=300)
print("Save figure in Figure/"+str(n)+".png")
img = Image.open("Figure/"+str(n)+'.png')
imgs.append(img)
ax.cla()
img.save('wave.gif', save_all=True, append_images=imgs, duration=0.1)
def evaluate(self):
"""
评估与真实函数的误差
"""
Fx = np.sin(np.pi * self.X / self.Lx)
Fy = np.sin(np.pi * self.Y / self.Ly)
Fu = np.outer(Fx, Fy)
c = np.pi*np.sqrt(1/self.Lx**2+1/self.Ly**2)
Error = []
for n in range(self.K+1):
t = PDE.T[n]
Ut = Fu*np.cos(c*t)
err = np.linalg.norm(Ut-self.U[n, :, :])
Error.append(err)
## print(err)
self.Err = Error
plt.figure(1)
plt.plot(self.Err)
plt.savefig("Error.png", dpi=300)
if __name__ == "__main__":
Lx = 2
Ly = 2
T = 4
N = 50
K = 500
PDE = WaveEquation2D(Lx, Ly, T, N, K)
PDE.simulation()
print(PDE.rx)
print(PDE.U.shape)
PDE.evaluate()
PDE.animate()