偏微分方程(PDE)是多元微分方程,方程中的导数是偏导数。处理ODE和PDE所需的计算方法大不相同,后者对计算的要求更高。
数值求解PDE的大多数技术都基于将PDE问题中的每个因变量离散化的思想,从而将微分问题变换为代数形式。将PDE转化为代数问题的两种常用技术是有限差分法(FDM)和有限元法(FEM)。其中有限差分法是将问题中的导数近似为有限差分,而有限元法则是将未知函数写成简单基函数的线性组合,其中基函数可以较容易进行微分和积分。未知函数可以表示为基函数的一组系数。
求解PDE问题所需的计算资源一般都非常大,一部分原因是对空间进行离散化所需要点的数量与维数是指数关系。例如一个一维问题如果需要用100个点来表示,那么具有类似分辨率的二维问题将需要10000个点。由于离散空间中的每个点都对应一个未知变量,因此PDE问题需要非常大的方程组。与OED问题不同,不存在标准形式对任意PDE问题进行定义。
对于FDM和FEM,得到的代数方程组一般都非常大。在矩阵表示下,此类方程组一般都非常稀疏。基于存储和计算效率考虑,FDM和FEM都非常依赖于稀疏矩阵来表示代数线性方程组。
为了使用稀疏矩阵,我们将导入SciPy的sparse模块,以及sparse模块的linalg线性代数子模块。
Python的PDE求解器只能由专门用于PDE问题的外部库和框架提供。我们将在后续章节中使用FEniCSx框架进行演示。
import matplotlib.pyplot as plt
import matplotlib as mpl
import mpl_toolkits.mplot3d
import numpy as np
import scipy.sparse as sp
import scipy.sparse.linalg
import scipy.linalg as la
%reload_ext version_information
%version_information numpy, matplotlib, scipy
PDE中的未知函数是多元函数。在N维问题中,函数 u u u依赖于n个独立变量。一般的PDE可以写为:
F ( x 1 , x 2 , ⋯ , x n , ∂ u ∂ x i , ∂ 2 u ∂ x i ∂ x j ) = 0 , x ∈ Ω F(x_1, x_2, \cdots, x_n, {\partial u \over \partial x_i}, {\partial ^{2}u \over \partial x_i\,\partial x_j} )=0, x \in \Omega F(x1,x2,⋯,xn,∂xi∂u,∂xi∂xj∂2u)=0,x∈Ω
其中 ∂ u ∂ x i {\partial u \over \partial x_i} ∂xi∂u表示自变量的所有一阶导数, ∂ 2 u ∂ x i ∂ x j {\partial ^{2}u \over \partial x_i\,\partial x_j} ∂xi∂xj∂2u表示所有二阶导数。这里的 F F F是已知函数,用于描述PDE的形式, Ω \Omega Ω是PDE问题的定义域。
为了简化符号,通常使用 u x = ∂ u ∂ x i u_x = {\partial u \over \partial x_i} ux=∂xi∂u表示自变量x的一阶偏导数, u x y = ∂ 2 u ∂ x ∂ y u_{xy} = {\partial ^{2}u \over \partial x\,\partial y} uxy=∂x∂y∂2u表示二阶导数。
大部分PDE问题最多包含二阶导数,并且通常是在二维或者三维空间中求解问题。例如热量方程在二维笛卡尔坐标系中的形式是 u t = a ( u x x + u y y ) u_{t}= a(u_{xx} + u_{yy}) ut=a(uxx+uyy)。这里函数 u ( x , y , t ) u(x, y, t) u(x,y,t)用于描述时间 t t t时,点 ( x , y ) (x, y) (x,y)处的温度, a a a是热传导系数。
为了完全确定PDE的解,需要定义PDE的边界条件。边界条件是沿着问题域 Ω \Omega Ω的函数值(Dirichlet边界条件)或者外法线导数(Neumann边界条件)的组合。如果问题是时间依赖的,那么还需要初始值。
有限差分法的基本思想是:利用离散空间中的有限差分公式来近似PDE中出现的导数。
例如,在将连续变量 x x x离散化成 { x n } \left\{ x_n \right\} {xn}时,常导数 d u d x \frac{du}{dx} dxdu的有限差分公式可以表示为:
前向差分公式 d u d x ≈ u ( x n + 1 ) − u ( x n ) x n + 1 − x n \frac{du}{dx} \approx \frac{u(x_{n+1}) - u(x_{n})}{x_{n+1} - x_{n}} dxdu≈xn+1−xnu(xn+1)−u(xn)
后向差分公式 d u d x ≈ u ( x n ) − u ( x n − 1 ) x n − x n − 1 \frac{du}{dx} \approx \frac{u(x_{n}) - u(x_{n-1})}{x_{n} - x_{n-1}} dxdu≈xn−xn−1u(xn)−u(xn−1)
中心差分公式 d u d x ≈ u ( x n + 1 ) − u ( x n − 1 ) x n + 1 − x n − 1 \frac{du}{dx} \approx \frac{u(x_{n+1}) - u(x_{n-1})}{x_{n+1} - x_{n-1}} dxdu≈xn+1−xn−1u(xn+1)−u(xn−1)
同样,也可以为高阶导数(例如二阶)构造有限差分公式:
d 2 u d x 2 ≈ u ( x n + 1 ) − 2 u ( x n ) + u ( x n − 1 ) ( x n + 1 − x n − 1 ) 2 \frac{d^2u}{dx^2} \approx \frac{u(x_{n+1}) - 2 u(x_{n}) + u(x_{n-1})}{(x_{n+1} - x_{n-1})^2} dx2d2u≈(xn+1−xn−1)2u(xn+1)−2u(xn)+u(xn−1)
使用有限差分公式替代ODE或者PDE中的导数,就可以将微分方程转换为代数方程。
为了具体说明有限差分法,我们首先考虑一维稳态热方程中的ODE问题 u x x = − 5 u_{xx}= -5 uxx=−5,其中 x ∈ [ 0 , 1 ] x \in [0, 1] x∈[0,1],边界条件是 u ( x = 0 ) = 1 u(x=0)=1 u(x=0)=1和 u ( x = 1 ) = 2 u(x=1)=2 u(x=1)=2。与常微分方程章节中讨论的ODE初始值问题不同,这是边界值问题。
我们将区间 [ 0 , 1 ] [0, 1] [0,1]均匀离散成N+2个空间点(包含边界点),这样问题就转换为了找到这些点的函数值 u ( x n ) = u n u(x_n)= u_n u(xn)=un。将ODE问题写为有限差分的形式,得到方程:
( u n − 1 − 2 u n + u n + 1 ) / Δ x 2 = − 5 (u_{n-1} - 2u_{n} + u_{n+1}) / {\Delta x}^2 = -5 (un−1−2un+un+1)/Δx2=−5
其中间隔 Δ x = 1 / ( N + 1 ) \Delta x = 1/(N+1) Δx=1/(N+1)。
由于函数在两个边界点的值是已知的,因此存在N个位置变量,对应内部点的函数值。我们可以将内部点的方程组表示为矩阵形式 A u = b Au=b Au=b:
1 Δ x 2 [ − 2 1 0 … 0 1 − 2 1 … 0 0 1 − 2 … 0 … … … … … 0 0 0 … − 2 ] [ u 1 u 2 u 3 … u n ] = [ − 5 − u 0 Δ x 2 − 5 − 5 … − 5 − u N + 1 Δ x 2 ] \frac{1}{{\Delta x}^2} \begin{bmatrix}\begin{array}{ccccccc}-2 & 1 & 0 & \ldots & 0 \\ 1 & -2 & 1 & \ldots & 0 \\ 0 & 1 & -2 & \ldots & 0 \\ \ldots & \ldots & \ldots & \ldots & \ldots \\ 0 & 0 & 0 & \ldots & -2 \end{array}\end{bmatrix} \begin{bmatrix}\begin{array}{c} u_1 \\ u_2 \\ u_3 \\ \ldots \\ u_n \end{array}\end{bmatrix} =\begin{bmatrix}\begin{array}{c} -5-\frac{u_0}{{\Delta x}^2} \\ -5 \\ -5 \\ \ldots \\ -5-\frac{u_{N+1}}{{\Delta x}^2} \end{array}\end{bmatrix} Δx21⎣⎢⎢⎢⎢⎡−210…01−21…001−2…0……………000…−2⎦⎥⎥⎥⎥⎤⎣⎢⎢⎢⎢⎡u1u2u3…un⎦⎥⎥⎥⎥⎤=⎣⎢⎢⎢⎢⎡−5−Δx2u0−5−5…−5−Δx2uN+1⎦⎥⎥⎥⎥⎤
这里矩阵 A A A描述了方程 u n u_n un和相邻点的耦合。边界值包含在向量 b b b中。
现在,我们可以直接求解线性方程组 A u = b Au=b Au=b中的未知向量 u u u,从而获得离散点 { x n } \left\{ x_n \right\} {xn}处函数的近似值。
N = 5
u0, u1 = 1., 2.
dx = 1.0 / (N + 1)
使用NumPy的eye函数,构造一个二维对角矩阵,同时使用参数k给定的偏移量生成上下对角线。
A = (np.eye(N, k=-1) - 2 * np.eye(N) + np.eye(N, k=1)) / dx**2
A
为向量 准备一个数组,该数组对应微分方程中的源项(热源)-5以及边界条件。
d = -5 * np.ones(N)
d[0] -= u0 / dx**2
d[N-1] -= u1 / dx**2
d
u = np.linalg.solve(A, d)
u
容易可知,该ODE问题的解析解为 u ( x ) = − 2.5 x 2 + 3.5 x + 1 u(x) = -2.5 x^2 + 3.5x + 1 u(x)=−2.5x2+3.5x+1。我们现在对解进行可视化。
f = lambda x: -2.5*x**2 + 3.5*x + 1
x = np.linspace(0, 1, N+2)
U = np.hstack([[u0], u, [u1]])
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(x, f(x))
ax.plot(x[1:-1], u, 'ks')
ax.set_xlim(0, 1)
ax.set_xlabel(r"$x$", fontsize=18)
ax.set_ylabel(r"$u(x)$", fontsize=18)
沿着每个离散化的坐标使用有限差分公式,可以很容易将有限差分法扩展到更高维度。对于二维问题,可以用二维数组 u u u来表示未知的函数值。使用有限差分公式时,对于 u u u中的每个元素可以得到一个耦合方程组。为了将这些方程写为标准的矩阵-向量点乘的形式,可以将二维数组 u u u重排成向量,并构建有限差分方程对应的矩阵 A A A。
考虑二维热传导问题,PDE为拉普拉斯公式 u x x + u y y = 0 u_{xx} + u_{yy} = 0 uxx+uyy=0,源项为0,边界条件为:
u ( x = 0 ) = 3 u(x=0) = 3 u(x=0)=3
u ( x = 1 ) = − 1 u(x=1) = -1 u(x=1)=−1
u ( y = 0 ) = − 5 u(y = 0) = -5 u(y=0)=−5
u ( y = 1 ) = 5 u(y = 1) = 5 u(y=1)=5
有限差分形式为:
u x x [ m , n ] = ( u [ m − 1 , n ] − 2 u [ m , n ] + u [ m + 1 , n ] ) / Δ x 2 u_{xx}[m, n] = (u[m-1, n] - 2u[m,n] + u[m+1,n])/{\Delta x}^2 uxx[m,n]=(u[m−1,n]−2u[m,n]+u[m+1,n])/Δx2
u y y [ m , n ] = ( u [ m , n − 1 ] − 2 u [ m , n ] + u [ m , n + 1 ] ) / Δ y 2 u_{yy}[m, n] = (u[m, n-1] - 2u[m,n] + u[m,n+1])/{\Delta y}^2 uyy[m,n]=(u[m,n−1]−2u[m,n]+u[m,n+1])/Δy2
如果将x和y的区间分成N个内部点,那么 Δ x = Δ y = 1 N + 1 \Delta x = \Delta y = \frac{1}{N+1} Δx=Δy=N+11。
为了将方程写成标准形式 A u = b Au=b Au=b,可以重新排布矩阵 u u u,将其的行或者列叠加成大小为 N 2 × 1 N^2 \times 1 N2×1的向量。矩阵 A A A的大小是 N 2 × N 2 N^2 \times N^2 N2×N2。由于有限差分公式中只有相邻点发生耦合,因此矩阵 A A A很稀疏。
N = 100
u0_t, u0_b = 5, -5
u0_l, u0_r = 3, -1
dx = 1. / (N+1)
二维问题中相邻的行和列都会发生耦合,因此构造矩阵 A A A会稍微复杂一些。一种相对直接的方式是首先定义矩阵 A l d A_ld Ald,对应一个坐标轴上的一维公式。为了在每一行使用该公式,可以将大小为 N × N N \times N N×N的对角矩阵与 A l d A_ld Ald进行张量积计算。为了涵盖每一列上耦合方程的项,需要将与主对角线相隔 N N N个位置的对角线相加。
我们将使用scipy.sparse
模块中的eye
和kron
函数构造矩阵A。
A_1d = (sp.eye(N, k=-1) + sp.eye(N, k=1) - 4 * sp.eye(N))/dx**2
A = sp.kron(sp.eye(N), A_1d) + (sp.eye(N**2, k=-N) + sp.eye(N**2, k=N))/dx**2
A
矩阵 A A A的非零值有49600个,占总元素数目的0.496%,可见其非常稀疏。
从边界条件构建向量 b b b,可以先生成一个 N × N N \times N N×N的零值数组,并将边界条件赋值给数组的边元素。随后,可以用reshape方法,将其重排成 N 2 × 1 N^2 \times 1 N2×1的向量。
d = np.zeros((N, N))
d[0, :] += -u0_b
d[-1, :] += -u0_t
d[:, 0] += -u0_l
d[:, -1] += -u0_r
d = d.reshape(N**2) / dx**2
生成数组 A A A和向量 b b b后,我们可以求解方程组,并使用reshape方法将 u u u转成 N × N N \times N N×N的矩阵。
u = sp.linalg.spsolve(A, d).reshape(N, N)
为了可视化绘图,我们创建一个矩阵 U U U,将矩阵 u u u和边界条件组合到一起。
U = np.vstack([np.ones((1, N+2)) * u0_b,
np.hstack([np.ones((N, 1)) * u0_l, u, np.ones((N, 1)) * u0_r]),
np.ones((1, N+2)) * u0_t])
x = np.linspace(0, 1, N+2)
X, Y = np.meshgrid(x, x)
fig = plt.figure(figsize=(10, 5))
cmap = mpl.cm.get_cmap('RdBu_r')
ax1 = fig.add_subplot(1, 2, 1)
c = ax1.pcolor(X, Y, U, vmin=-5, vmax=5, cmap=cmap)
ax1.set_xlabel(r"$x_1$", fontsize=18)
ax1.set_ylabel(r"$x_2$", fontsize=18)
ax2 = fig.add_subplot(1, 2, 2, projection='3d')
p = ax2.plot_surface(X, Y, U, vmin=-5, vmax=5, rstride=3, cstride=3, cmap=cmap)
ax2.set_xlabel(r"$x_1$", fontsize=18)
ax2.set_ylabel(r"$x_2$", fontsize=18)
cb = plt.colorbar(p, ax=ax2)
cb.set_label(r"$u(x_1, x_2)$", fontsize=18)
考虑PDE问题 u x x + u y y = 1 u_{xx} + u_{yy} = 1 uxx+uyy=1
d = - np.ones((N, N))
d = d.reshape(N**2)
u = sp.linalg.spsolve(A, d).reshape(N, N)
U = np.vstack([np.zeros((1, N+2)),
np.hstack([np.zeros((N, 1)), u, np.zeros((N, 1))]),
np.zeros((1, N+2))])
x = np.linspace(0, 1, N+2)
X, Y = np.meshgrid(x, x)
fig, ax = plt.subplots(1, 1, figsize=(8, 6), subplot_kw={'projection': '3d'})
p = ax.plot_surface(X, Y, U, rstride=4, cstride=4, linewidth=0, cmap=mpl.cm.get_cmap("Reds"))
cb = fig.colorbar(p, shrink=0.5)
ax.set_xlabel(r"$x_1$", fontsize=18)
ax.set_ylabel(r"$x_2$", fontsize=18)
cb.set_label(r"$u(x_1, x_2)$", fontsize=18)
正如上面展示,使用FDM方法得到的矩阵 A A A非常稀疏。我们下面对比稀疏矩阵和稠密矩阵求解方程 A u = b Au=b Au=b所需的时间。
A_dense = A.todense()
%time sp.linalg.spsolve(A, d)
%time np.linalg.solve(A_dense, d)
%time la.solve(A_dense, d)
从上面示例可知,有限差分法是一种强大且简单的求解ODE边界值问题以及简单形状PDE问题的方法。但是,这种方法并不适用更复杂的问题(计算量过大),以及不均匀坐标网格的问题。对于此类问题,有限元法FEM更加灵活和方便。
简单来说,有限差分法是基于泰勒展开的近似,有限元法是基于变分法的近似。
有限元法FEM的基本思想是用有限的离散区域或单元的集合来代表PDE的定义域,并将未知函数近似为基函数的线性组合,而这些基函可以在每个单元(或一组相邻单元)上获得局部支持。
在数学上,这种近似解 u h u_h uh表示从函数空间 V V V中的精确解 u u u到有限子空间(问题域的离散化相关) V h V_h Vh的映射。如果 V h V_h Vh是 V V V的合适子空间,可以预期 u h u_h uh能够很好近似 u u u。
为了在简化的函数空间 V h V_h Vh中求解近似问题,可以将PDE从原始公式(称为强形式)重写为对应的变体形式(称为弱形式)。为了得到弱形式,我们将PDE乘以任意函数 v v v,并在整个问题域上积分。函数 v v v称为test函数,通常可以在不用于 V V V和 V h V_h Vh的函数空间 V ^ \hat{V} V^中定义该函数。
通常而言,使用FEM求解PDE问题通常涉及以下步骤:
参考文献来自桑鸿乾老师的课件:科学计算和人工智能