有限差分法finite difference(FD)是求解微分方程的最为容易理解的方法,下面将针对几类常见的PDE来做一些具体的介绍。由于本人知识有限,关于误差分析和收敛性证明都不会介绍.
我们以一个一维PDE的求解来介绍有限差分算法,这里给出精确解 u ^ = x ( 1 − x ) exp ( x ) \hat{u} = x(1-x)\exp(x) u^=x(1−x)exp(x).
{
−
u
x
x
=
f
,
x
∈
(
0
,
1
)
,
f
=
exp
(
x
)
(
−
x
2
−
x
+
1
)
,
u
(
0
)
=
0
,
u
(
1
)
=
0.
\left\{
对区域 [ 0 , 1 ] [0,1] [0,1]剖分,均匀分成 N N N份,即 x 0 = 0 < x i < x N = 1 x_0 = 0 < x_i < x_N = 1 x0=0<xi<xN=1,其中 h x = 1 N h_x = \frac{1}{N} hx=N1,现在要求 u i = u ( x i ) , 1 ≤ i ≤ N − 1 u_i=u(x_i),1 \leq i \leq N - 1 ui=u(xi),1≤i≤N−1的取值.利用 u i + 1 − 2 u i + u i − 1 h x 2 , 1 ≤ i ≤ N − 1 \frac{u_{i+1} - 2u_i + u_{i-1}}{h_x^2},1 \leq i \leq N - 1 hx2ui+1−2ui+ui−1,1≤i≤N−1可以近似代替 u x x ( x i ) u_{xx}(x_i) uxx(xi),那么得到一个线性系统:
{
−
(
u
i
+
1
−
2
u
i
+
u
i
−
1
)
=
f
i
h
x
2
,
1
≤
i
≤
N
−
1
,
u
0
=
0
,
u
N
=
0
,
A
u
=
b
.
\left\{
这里的 b , u b,u b,u是一个 N + 1 N + 1 N+1维向量,其中 b 0 = 0 , b N = 0 b_0 = 0,b_N=0 b0=0,bN=0,其他部分 b i = f i h x 2 , 1 ≤ i ≤ N − 1 b_i = f_i h_x^2,1 \leq i \leq N - 1 bi=fihx2,1≤i≤N−1,这里的A是一个 ( N + 1 ) × ( N + 1 ) (N + 1)\times (N + 1) (N+1)×(N+1)的带状矩阵,满足 A 0 , 0 = A N , N = 1 A_{0,0}=A_{N,N} = 1 A0,0=AN,N=1,其余对角线元素 A i , i = 2 A_{i,i}=2 Ai,i=2,就是下面这个样子.
A
=
(
1
0
0
⋯
0
−
1
2
−
1
⋯
0
⋮
⋮
⋱
⋮
0
⋯
−
1
2
−
1
0
0
⋯
0
1
)
A=\left(
给定泊松方程如下,其中 Ω = [ 0 , 1 ] 2 , x = ( x , y ) \Omega = [0,1]^2,\mathbf{x} = (x,y) Ω=[0,1]2,x=(x,y):
{
−
Δ
u
=
f
,
x
∈
Ω
,
u
=
u
0
,
x
∈
∂
Ω
.
\left\{
引入剖分
M
,
N
,
h
x
=
1
M
,
h
y
=
1
N
M,N,h_x = \frac{1}{M},h_y = \frac{1}{N}
M,N,hx=M1,hy=N1,将区域离散化以后得到网格点
(
x
i
,
y
j
)
,
0
≤
i
≤
M
,
0
≤
j
≤
N
(x_i,y_j),0 \leq i \leq M,0 \leq j \leq N
(xi,yj),0≤i≤M,0≤j≤N,记
u
i
,
j
=
u
(
x
i
,
y
j
)
u_{i,j}=u(x_i,y_j)
ui,j=u(xi,yj),利用中心差分公式

{
u
x
x
(
x
i
,
y
j
)
=
u
i
+
1
,
j
−
2
u
i
,
j
+
u
i
−
1
,
j
h
x
2
+
O
(
h
x
2
)
,
1
≤
i
≤
M
−
1
,
u
y
y
(
x
i
,
y
j
)
=
u
i
,
j
+
1
−
2
u
i
,
j
+
u
i
,
j
−
1
h
y
2
+
O
(
h
y
2
)
,
1
≤
j
≤
N
−
1.
\left\{
{
−
u
i
+
1
,
j
−
2
u
i
,
j
+
u
i
−
1
,
j
h
x
2
−
u
i
,
j
+
1
−
2
u
i
,
j
+
u
i
,
j
−
1
h
y
2
=
f
i
,
j
,
1
≤
i
≤
M
−
1
,
1
≤
j
≤
N
−
1
,
u
i
,
j
=
u
0
(
x
i
,
y
j
)
,
(
x
i
,
y
j
)
∈
∂
Ω
.
\left\{
通过上面的公式可以形成一个线性方程组
A
x
=
b
,
A
∈
R
(
M
+
1
)
(
N
+
1
)
×
(
M
+
1
)
(
N
+
1
)
Ax = b,A \in R^{(M + 1)(N + 1)\times (M + 1)(N + 1)}
Ax=b,A∈R(M+1)(N+1)×(M+1)(N+1),不过由于在实际操作过程中
M
,
N
M,N
M,N很大,会导致
h
x
2
,
h
y
2
h_x^2,h_y^2
hx2,hy2太大,为了避免矩阵
A
A
A病态,我们可以在
{
−
h
y
h
x
u
i
−
1
,
j
+
(
2
h
x
h
y
+
2
h
x
h
y
)
u
i
,
j
−
h
y
h
x
u
i
+
1
,
j
−
h
x
h
y
u
i
,
j
−
1
−
h
x
h
y
u
i
,
j
+
1
=
f
i
,
j
h
x
h
y
,
u
i
,
j
=
u
0
(
x
i
,
y
j
)
,
(
x
i
,
y
j
)
∈
∂
Ω
.
\left\{
有了上述公式以后,下面我们开始在python中实现
A
,
b
A,b
A,b的构造,这里说明
x
=
(
u
0
,
0
,
u
0
,
1
,
…
,
u
0
,
N
,
…
,
u
M
,
0
,
u
M
,
1
,
…
,
u
M
,
N
)
x = (u_{0,0},u_{0,1},\ldots,u_{0,N},\ldots,u_{M,0},u_{M,1},\ldots,u_{M,N})
x=(u0,0,u0,1,…,u0,N,…,uM,0,uM,1,…,uM,N).

图片展示了五点差分法构造矩阵的全过程,
u
u
u在边界点的取值已知,所以当循环遇到边界点的索引,此时矩阵的对角元素设定为1.事实上,如果想缩小矩阵的规模,可以把已知量挪到右边,只针对位于网格点内部的函数值
u
i
,
j
u_{i,j}
ui,j求解,形成一个
(
M
−
1
)
(
N
−
1
)
×
(
M
−
1
)
(
N
−
1
)
(M - 1)(N-1)\times(M - 1)(N - 1)
(M−1)(N−1)×(M−1)(N−1)的矩阵,读者可以自己尝试.引入精确解
u
=
log
(
10
(
x
+
y
)
2
+
(
x
−
y
)
2
+
0.5
)
u = \log(10(x + y)^2 + (x - y)^2 + 0.5)
u=log(10(x+y)2+(x−y)2+0.5),然后形成对应的右端项和边界条件测试的结果如下所示:

代码参考添加链接描述
给定泊松方程如下,其中 Ω = [ 0 , 1 ] 2 , x = ( x , y ) \Omega = [0,1]^2,\mathbf{x} = (x,y) Ω=[0,1]2,x=(x,y):
{
−
Δ
u
+
u
=
f
,
x
∈
Ω
,
∂
u
∂
n
=
u
0
,
x
∈
∂
Ω
.
\left\{
针对Neumann边界条件,泊松方程的设定和上面的Dirichlet
{
−
h
y
h
x
u
i
−
1
,
j
+
(
2
h
x
h
y
+
2
h
x
h
y
+
h
x
h
y
)
u
i
,
j
−
h
y
h
x
u
i
+
1
,
j
−
h
x
h
y
u
i
,
j
−
1
−
h
x
h
y
u
i
,
j
+
1
=
f
i
,
j
h
x
h
y
,
∂
u
∂
n
(
x
i
,
y
j
)
=
u
0
(
x
i
,
y
j
)
,
(
x
i
,
y
j
)
∈
∂
Ω
.
\left\{
重点是在边界上的处理,方向导数放在四个边界上是有两个 u x , u y u_x,u_y ux,uy,因此这里需要逼近 u x , u y u_x,u_y ux,uy,可以使用一阶差分或者二阶差分和虚拟点来逼近,下面重点介绍虚拟点方法.
这里我们选取边界点 ( x i , y 0 ) , 1 ≤ i ≤ M − 1 (x_i,y_0),1 \leq i \leq M - 1 (xi,y0),1≤i≤M−1举例子,注意在4个角需要做特殊处理.则会有
∂ u ∂ n ( x i , y 0 ) = u i , 1 − u i , − 1 2 h y = u 0 ( x i , y 0 ) , 1 ≤ i ≤ M − 1. \frac{\partial u}{\partial n}(x_i,y_0) = \frac{u_{i,1} - u_{i,-1}}{2h_y} = u_0(x_i,y_0),1 \leq i \leq M - 1. ∂n∂u(xi,y0)=2hyui,1−ui,−1=u0(xi,y0),1≤i≤M−1.
然后代入方程中替换 u i , − 1 = u i , 1 − 2 u 0 ( x i , y 0 ) h y u_{i,-1} = u_{i,1} - 2u_0(x_i,y_0)h_y ui,−1=ui,1−2u0(xi,y0)hy即可,对应需要修改右端项 b b b的定义.代码参考添加链接描述.
引入方程定义:
{
u
(
x
,
t
)
−
Δ
u
(
x
,
t
)
=
f
(
x
,
t
)
,
(
x
,
t
)
∈
Ω
×
[
0
,
1
]
,
u
(
x
,
t
)
=
g
(
x
,
t
)
,
(
x
,
t
)
∈
∂
Ω
×
[
0
,
1
]
,
u
(
x
,
0
)
=
h
(
x
)
,
x
∈
Ω
.
\left\{
针对方程
δ
x
u
i
,
j
n
=
u
i
+
1
,
j
n
−
2
u
i
,
j
n
+
u
i
−
1
,
j
n
h
x
2
\delta_x u_{i,j}^n = \frac{u_{i + 1,j}^n - 2u_{i,j}^n + u_{i - 1,j}^n}{h_x^2}
δxui,jn=hx2ui+1,jn−2ui,jn+ui−1,jn,
δ
y
u
i
,
j
n
=
u
i
,
j
+
1
n
−
2
u
i
,
j
n
+
u
i
,
j
−
1
n
h
y
2
\delta_y u_{i,j}^n = \frac{u_{i,j + 1}^n - 2u_{i,j}^n + u_{i,j - 1}^n}{h_y^2}
δyui,jn=hy2ui,j+1n−2ui,jn+ui,j−1n \
Forward-Euler:
u i , j n + 1 − u i , j n τ − δ x u i , j n − δ y u i , j n = f i , j n + 1 . \frac{u_{i,j}^{n + 1} - u_{i,j}^{n}}{\tau} - \delta_x u_{i,j}^n - \delta_y u_{i,j}^n = f_{i,j}^{n + 1}. τui,jn+1−ui,jn−δxui,jn−δyui,jn=fi,jn+1.
Backward-Euler:
u i , j n + 1 − u i , j n τ − δ x u i , j n + 1 − δ y u i , j n + 1 = f i , j n + 1 . \frac{u_{i,j}^{n + 1} - u_{i,j}^{n}}{\tau} - \delta_x u_{i,j}^{n + 1} - \delta_y u_{i,j}^{n + 1} = f_{i,j}^{n + 1}. τui,jn+1−ui,jn−δxui,jn+1−δyui,jn+1=fi,jn+1.
BDF:
3 u i , j n + 1 − 4 u i , j n + u i , j n − 1 2 τ − δ x u i , j n + 1 − δ y u i , j n + 1 = f i , j n + 1 . \frac{3u_{i,j}^{n + 1} - 4u_{i,j}^{n} + u_{i,j}^{n - 1}}{2\tau} - \delta_x u_{i,j}^{n + 1} - \delta_y u_{i,j}^{n + 1} = f_{i,j}^{n + 1}. 2τ3ui,jn+1−4ui,jn+ui,jn−1−δxui,jn+1−δyui,jn+1=fi,jn+1.
CN:
u i , j n + 1 − u i , j n τ − 1 2 ( δ x u i , j n + 1 + δ y u i , j n + 1 + δ x u i , j n + δ y u i , j n ) = f i , j n + 1 . \frac{u_{i,j}^{n + 1} - u_{i,j}^{n}}{\tau} - \frac{1}{2}(\delta_x u_{i,j}^{n + 1} + \delta_y u_{i,j}^{n + 1} + \delta_x u_{i,j}^{n} + \delta_y u_{i,j}^{n}) = f_{i,j}^{n + 1}. τui,jn+1−ui,jn−21(δxui,jn+1+δyui,jn+1+δxui,jn+δyui,jn)=fi,jn+1.
有了这些格式以后,我们引入
U
n
=
(
u
0
,
0
n
,
…
,
u
0
,
N
n
,
…
,
u
M
,
0
n
,
…
,
u
M
,
N
n
)
U^n = (u_{0,0}^{n},\ldots,u_{0,N}^{n},\ldots,u_{M,0}^{n},\ldots,u_{M,N}^{n})
Un=(u0,0n,…,u0,Nn,…,uM,0n,…,uM,Nn),则上述4个格式可以理解为4个迭代的线性系统.
Forward-Euler:
U n + 1 = A U n + b . U^{n + 1} = A U^{n} + b. Un+1=AUn+b.
Backward-Euler:
A U n + 1 = B U n + b . AU^{n + 1} = B U^{n} + b. AUn+1=BUn+b.
BDF:
A U n + 1 = B U n + C U n − 1 + b . AU^{n + 1} = B U^{n} + C U^{n - 1} + b. AUn+1=BUn+CUn−1+b.
CN:
A U n + 1 = B U n + b . AU^{n + 1} = B U^{n} + b. AUn+1=BUn+b.
在代码的实现过程中,Forward-Euler不需要求解线性系统,但是这里需要对时间步长 τ \tau τ和空间步长 h x , h y h_x,h_y hx,hy做一个限制来满足稳定性条件,另外三个格式对于任意的时间空间步长都是稳定的,而上面提到的4个格式里面的矩阵需要自己根据离散格式来确定.这里仅仅针对BDF格式做一个具体的说明,其他的类似.
对于时间层,我们每隔 τ \tau τ时间做了一次计算, U n U^n Un表示当时间层到了第n层的时候那一层的解.将BDF格式整理如下:
(
3
+
4
τ
(
1
h
x
2
+
1
h
y
2
)
)
u
i
,
j
n
+
1
−
2
τ
h
y
2
(
u
i
,
j
+
1
n
+
u
i
,
j
−
1
n
)
−
2
τ
h
x
2
(
u
i
+
1
,
j
n
+
u
i
−
1
,
j
n
)
=
4
u
i
,
j
n
−
u
i
,
j
n
−
1
+
2
f
i
,
j
n
+
1
τ
.
具体代码参考下面图片

在BDF代码实现中,计算
U
2
U^2
U2的时候需要
U
1
,
U
0
U^1,U^0
U1,U0,其中
U
0
U^0
U0可以直接通过初始条件获得,
U
1
U^1
U1可以通过CN格式或者Backward-Euler格式获得.
这里我们取
u
=
(
x
2
+
y
2
)
exp
(
t
−
t
2
)
u=(x^2 + y^2)\exp(t-t^2)
u=(x2+y2)exp(t−t2)来做算例测试BDF格式,这里只计算最后一层
u
(
x
,
y
,
1
)
u(x,y,1)
u(x,y,1)的误差.

上面的FD把整个区域进行剖分,当剖分很细的时候,得到的线性系统维度很大,为了降低存储成本和计算成本,可以将区域划分为若干区域,在每个区域上求解,具体过程将以数学公式来说明.这种算法也称之为cauchy-schwarz算法.
首先需要把区域进行划分,先假设划分为两个区域 Ω = Ω 0 ⋃ Ω 1 \Omega=\Omega_0 \bigcup \Omega_1 Ω=Ω0⋃Ω1,其中 Ω 0 = [ 0 , 0.5 ] × [ 0 , 1 ] , ω 1 = [ 0.4 , 1 ] × [ 0 , 1 ] \Omega_0=[0,0.5]\times[0,1],\omega_1=[0.4,1]\times[0,1] Ω0=[0,0.5]×[0,1],ω1=[0.4,1]×[0,1],注意两个区域必须要有重叠overlap.做剖分的时候注意,为了避免麻烦,应该让边界线 { 0.4 } × [ 0 , 1 ] , { 0.5 } × [ 0 , 1 ] \{0.4\}\times [0,1],\{0.5\}\times [0,1] {0.4}×[0,1],{0.5}×[0,1]都位于网格线上,针对泊松方程引入两个子问题.
{
−
Δ
u
1
n
+
1
=
f
,
x
∈
Ω
0
,
u
1
n
+
1
=
u
0
,
x
∈
∂
Ω
0
∩
∂
Ω
,
u
1
n
+
1
=
u
2
n
,
x
∈
∂
Ω
0
∖
∂
Ω
.
\left\{
{
−
Δ
u
2
n
=
f
,
x
∈
Ω
1
,
u
2
n
=
u
0
,
x
∈
∂
Ω
1
∩
∂
Ω
,
u
2
n
=
u
1
n
+
1
,
x
∈
∂
Ω
1
∖
∂
Ω
.
\left\{
则cauchy-schwarz算法的定义是:

上面有两个子问题,两个线性系统的区别在于右端项,因此可以提前构建好矩阵
A
A
A,然后根据不同的右端项定义两个求解函数solve,具体过程可以参考下列图片:

如果将区域分成3个区域,比如说 Ω 0 = [ 0 , 0.5 ] × [ 0 , 1 ] , ω 1 = [ 0.4 , 0.8 ] × [ 0 , 1 ] , ω 1 = [ 0.7 , 1 ] × [ 0 , 1 ] \Omega_0=[0,0.5]\times[0,1],\omega_1=[0.4,0.8]\times[0,1],\omega_1=[0.7,1]\times[0,1] Ω0=[0,0.5]×[0,1],ω1=[0.4,0.8]×[0,1],ω1=[0.7,1]×[0,1],仍然要保证两两之间有overlap,过程与上面算法一样,代码可以参考添加链接描述
1:在上述方法中,最终的问题都被转化成求解一个线性系统
A
x
=
b
Ax=b
Ax=b,这里本人调用的是python库numpy自带的线性求解器
l
i
n
a
l
g
.
s
o
l
v
e
(
A
,
b
)
linalg.solve(A,b)
linalg.solve(A,b),如何设计G-S迭代方法求解,如何证明G-S迭代法对这类线性系统的收敛性.\par
2:线性系统
3:在介绍FD求解含时泊松方程时,向前欧拉格式不稳定,如何确定合适的
τ
,
h
x
,
h
y
\tau,h_x,h_y
τ,hx,hy使得该格式稳定.
4:在区域分解算法中,本人的划分都是针对一个维度,如果对区域做棋盘划分,这个算法该如何写,参考提示添加链接描述
FD方法的特点是只能求解离散点的取值,如果想要知道不在网格点的函数值,就需要重新做剖分或者做插值得到,基于此有限元方法finite element(FE)发展起来.FE可以被理解为一种插值方法,还是以二维区域为例,将区域 Ω \Omega Ω进行剖分得到 M × N M\times N M×N个小区域,在每个小区域上定义一个分片函数,然后根据网格点的取值进行插值,下面直接以数学公式来进行说明.
首先引入单元函数
ψ
(
x
,
y
)
{\psi}(x,y)
ψ(x,y),对区域进行剖分以后得到
(
x
i
,
y
j
)
(x_i,y_j)
(xi,yj),则会有
φ
i
,
j
=
ψ
(
x
−
x
i
h
x
,
y
−
y
j
h
y
)
\varphi_{i,j} = \psi(\frac{x - x_i}{h_x},\frac{y - y_j}{h_y})
φi,j=ψ(hxx−xi,hyy−yj),那么这样定义的基函数
φ
i
,
j
\varphi_{i,j}
φi,j就满足在第
(
i
,
j
)
(i,j)
(i,j)个网格点上取值为1,在其他网格点上取值为0.
ψ
(
x
,
y
)
=
{
(
1
−
x
)
∗
(
1
−
y
)
,
0
≤
x
≤
1
,
0
≤
y
≤
1
,
(
1
+
x
)
∗
(
1
−
y
)
,
−
1
≤
x
≤
0
,
0
≤
y
≤
1
,
(
1
+
x
)
∗
(
1
+
y
)
,
−
1
≤
x
≤
0
,
−
1
≤
y
≤
0
,
(
1
−
x
)
∗
(
1
+
y
)
,
0
≤
x
≤
1
,
−
1
≤
y
≤
0.
\operatorname{\psi}(x,y)=\left\{
于是我们的有限元近似解定义为,接下来的重点就是确定
u
i
,
j
u_{i,j}
ui,j的取值.有了上面FD的介绍,其实可以先通过FD求出对应的
u
i
,
j
u_{i,j}
ui,j,然后根据
u
h
=
∑
i
=
0
i
=
M
∑
j
=
0
j
=
N
u
i
,
j
φ
i
,
j
.
u_h = \sum_{i=0}^{i = M}\sum_{j=0}^{j = N}u_{i,j}\varphi_{i,j}.
uh=∑i=0i=M∑j=0j=Nui,jφi,j.
FE首先需要引入弱形式
{
a
(
u
,
v
)
=
∮
Ω
∇
u
⋅
∇
v
d
x
d
y
,
f
(
v
)
=
∮
Ω
f
v
d
x
d
y
.
\left\{
根据方程
重点:代入
u
h
,
v
=
φ
i
,
j
u_h,v = \varphi_{i, j}
uh,v=φi,j得到线性系统得到公式
{
a
(
u
h
,
φ
i
,
j
)
=
f
(
φ
i
,
j
)
,
1
≤
i
≤
M
−
1
,
1
≤
j
≤
N
−
1
,
u
h
(
x
i
,
y
j
)
=
u
0
(
x
i
,
y
j
)
,
(
x
i
,
y
j
)
∈
∂
Ω
.
\left\{
由于
u
h
u_h
uh和
φ
i
,
j
\varphi_{i, j}
φi,j的特殊性,区域
Ω
\Omega
Ω上的积分只有一小部分非0.
{
∫
Ω
(
∑
j
=
0
j
=
N
u
i
,
j
∇
φ
i
,
j
)
∇
φ
i
^
,
j
^
d
Ω
=
∫
Ω
f
φ
i
^
,
j
^
d
Ω
,
∑
m
=
0
M
N
−
1
∫
Ω
m
(
∑
i
=
0
i
=
M
∑
j
=
0
j
=
N
u
i
,
j
∇
φ
i
,
j
)
∇
φ
i
^
,
j
^
d
Ω
=
∑
m
=
0
M
N
−
1
∫
Ω
m
f
φ
i
^
,
j
^
d
Ω
.
\left\{
当
(
x
i
,
y
j
)
,
(
x
i
^
,
y
j
^
)
(x_i,y_j),(x_{\hat{i}},y_{\hat{j}})
(xi,yj),(xi^,yj^)同时位于
Ω
m
\Omega_m
Ωm的顶点,才有
∫
Ω
m
∇
φ
i
,
j
∇
φ
i
^
,
j
^
≠
0
\int_{\Omega_m}\nabla \varphi_{i,j} \nabla \varphi_{\hat{i},\hat{j}} \neq 0
∫Ωm∇φi,j∇φi^,j^=0.
∫
Ω
m
(
∑
i
=
0
i
=
M
∑
j
=
0
j
=
N
u
i
,
j
∇
φ
i
,
j
)
∇
φ
i
^
,
j
^
d
Ω
=
∫
Ω
m
∑
∣
i
−
i
^
∣
≤
1
,
∣
j
−
j
^
∣
≤
1
∇
φ
i
,
j
∇
φ
i
^
,
j
^
d
Ω
.
\int_{\Omega_m}(\sum_{i=0}^{i = M}\sum_{j=0}^{j = N}u_{i,j}\nabla \varphi_{i,j})\nabla \varphi_{\hat{i},\hat{j}} d\Omega = \int_{\Omega_m} \sum_{|i-\hat{i}|\leq 1,|j - \hat{j}|\leq 1} \nabla \varphi_{i,j} \nabla \varphi_{\hat{i},\hat{j}} d\Omega.
∫Ωm(∑i=0i=M∑j=0j=Nui,j∇φi,j)∇φi^,j^dΩ=∫Ωm∑∣i−i^∣≤1,∣j−j^∣≤1∇φi,j∇φi^,j^dΩ.
因此才有下面的公式成立.
a
i
,
j
=
∑
N
o
d
e
s
[
i
]
,
N
o
d
e
s
[
j
]
∈
T
m
∮
T
m
∇
ϕ
i
∇
ϕ
j
d
x
d
y
+
∑
N
o
d
e
s
[
i
]
,
N
o
d
e
s
[
j
]
∈
∂
Ω
m
∮
∂
Ω
m
β
ϕ
i
ϕ
j
d
s
a_{i,j}= \sum_{Nodes[i],Nodes[j] \in T_{m}} \oint_{T_{m}} \nabla \phi_{i}\nabla \phi_{j}dxdy + \sum_{Nodes[i],Nodes[j] \in \partial \Omega_{m}} \oint_{\partial \Omega_{m}} \beta \phi_{i}\phi_{j}ds
ai,j=∑Nodes[i],Nodes[j]∈Tm∮Tm∇ϕi∇ϕjdxdy+∑Nodes[i],Nodes[j]∈∂Ωm∮∂Ωmβϕiϕjds
其中Nodes$[i]
表示在整体排序中的第
表示在整体排序中的第
表示在整体排序中的第i
个网格点
,
个网格点,
个网格点,T_m
表示第
m
个单元,区域一共有
表示第m个单元,区域一共有
表示第m个单元,区域一共有M\times N$个单元.
类似地,我们可以给出右端项,也就是所谓的载荷向量的b的定义
b
i
=
∑
N
o
d
e
s
[
i
]
∈
T
m
∮
T
m
f
ϕ
i
d
x
d
y
+
∑
s
e
l
f
.
N
o
d
e
s
[
i
]
∈
∂
Ω
m
∮
∂
Ω
m
β
g
ϕ
i
d
s
b_{i}= \sum_{Nodes[i] \in T_{m}} \oint_{T_{m}} f\phi_{i}dxdy + \sum_{self.Nodes[i] \in \partial \Omega_{m}} \oint_{\partial \Omega_{m}} \beta g\phi_{i}ds
bi=∑Nodes[i]∈Tm∮Tmfϕidxdy+∑self.Nodes[i]∈∂Ωm∮∂Ωmβgϕids
在给出这些矩阵元素和右端项元素的定义计算以后,我们就必须要解决积分问题,我们需要对区域进行离散化处理,选取有限个点作为积分节点,根据数值逼近的原理,我们知道在区域
[
0
,
1
]
2
[0,1]^2
[0,1]2中,我们只要选取以下4个点
A
1
=
(
1
−
3
/
3
2
,
1
−
3
/
3
2
)
,
A
2
=
(
1
+
3
/
3
2
,
1
−
3
/
3
2
)
,
A
3
=
(
1
+
3
/
3
2
,
1
+
3
/
3
2
)
,
A
4
=
(
1
−
3
/
3
2
,
1
+
3
/
3
2
)
A_1 = (\frac{1 - \sqrt{3}/3}{2},\frac{1 - \sqrt{3}/3}{2}),A_2 = (\frac{1 + \sqrt{3}/3}{2},\frac{1 - \sqrt{3}/3}{2}),A_3 = (\frac{1 + \sqrt{3}/3}{2},\frac{1 + \sqrt{3}/3}{2}),A_4 = (\frac{1 - \sqrt{3}/3}{2},\frac{1 + \sqrt{3}/3}{2})
A1=(21−3/3,21−3/3),A2=(21+3/3,21−3/3),A3=(21+3/3,21+3/3),A4=(21−3/3,21+3/3)作为积分点,
∮
[
0
,
1
]
2
f
d
x
d
y
=
0.25
∗
∑
i
=
1
4
(
f
(
A
i
)
)
\oint_{[0,1]^2} f dxdy = 0.25*\sum_{i = 1}^{4}(f(A_i))
∮[0,1]2fdxdy=0.25∗∑i=14(f(Ai))
那么上述积分近似就有2阶代数精度,即对于
1
,
x
,
y
,
x
2
,
y
2
,
x
y
1,x,y,x^2,y^2,xy
1,x,y,x2,y2,xy这些函数的线性组合,上述积分等式恒成立。本人代码里面FENET里面定义了
s
e
l
f
.
g
p
_
p
o
s
=
[
1
−
3
/
3
2
,
1
−
3
/
3
2
]
self.gp\_{}pos = [\frac{1 - \sqrt{3}/3}{2},\frac{1 - \sqrt{3}/3}{2}]
self.gp_pos=[21−3/3,21−3/3]以便于在每个积分单元采取这样4个高斯积分节点。在这些定义完以后,本人定义了
I
n
t
_
b
a
s
i
c
_
b
a
s
i
c
Int\_{}basic\_{}basic
Int_basic_basic函数表示在某积分单元上两个基函数之间梯度的积分,定义了
I
n
t
_
F
_
b
a
s
i
c
Int\_{}F\_{}basic
Int_F_basic表示在某积分单元上f和某基函数的积分,定义
B
o
u
_
b
a
s
i
c
_
b
a
s
i
c
Bou\_{}basic\_{}basic
Bou_basic_basic表示在某Neumann边界线段单元上两个基函数的线积分,定义
B
o
u
_
N
e
u
_
b
a
s
i
c
Bou\_{}Neu\_{}basic
Bou_Neu_basic表示在Neumann边界上函数g和基函数在该线段单元上的线积分,代码参考添加链接描述

传统方法FD,FE最大的问题在于当坐标是高维向量的时候,无法对高维空间进行剖分,这个会造成维度灾难curse of dimensionality.而这些年深度学习的兴起给我们提供了一个新的方法来求解PDE,下图

一个通用的全连接网络NN表达式如下
{
f
i
(
s
)
=
ϕ
(
W
i
,
2
⋅
ϕ
(
W
i
,
1
s
+
b
i
,
1
)
+
b
i
,
2
)
,
z
=
f
n
∘
f
n
−
1
∘
…
∘
f
0
(
x
)
,
y
(
x
)
=
W
z
+
b
.
\left\{
正如上面提到的,利用深度学习求解PDE的核心在于选择合适的权重和偏置,我们以
θ
\theta
θ来表示神经网络NN中的权重和偏置,这些也被称为NN的参数,我们需要根据PDE来选择等价的优化问题,然后利用优化中的迭代算法来迭代更新参数
θ
\theta
θ,下面我们引入几个基本的算法来做介绍,测试函数都是
u
=
log
(
10
(
x
+
y
)
2
+
(
x
−
y
)
2
+
0.5
)
u=\log(10(x+y)^2 + (x - y)^2 + 0.5)
u=log(10(x+y)2+(x−y)2+0.5).
对于泊松方程,利用极小位能原理可以把PDE转化为等价的变分问题
{
min
u
∫
Ω
∣
∇
u
∣
2
−
2
f
u
d
Ω
,
subject to
u
=
u
0
,
x
∈
∂
Ω
.
\left\{
我们需要根据变分问题
第一:我们需要对区域
Ω
\Omega
Ω进行离散化,需要在内部采集样本点
X
=
{
x
i
}
i
=
0
n
I
−
1
X = \{\mathbf{x}_i\}_{i=0}^{n_I - 1}
X={xi}i=0nI−1,从上面公式中可以发现还需要准备好函数
f
(
X
)
f(X)
f(X)数据集,采样参考以下图片.

第二:我们需要在区域边界 ∂ Ω \partial \Omega ∂Ω进行采样,得到边界上的样本点 X = { x i } i = 0 n B − 1 X=\{\mathbf{x}_i\}_{i=0}^{n_B - 1} X={xi}i=0nB−1,以及 u 0 ( X ) u_0(X) u0(X).
第三:我们需要搭建全连接网络,pytorch自带的torch.nn.Linear来作为每一层的初始参数,搭建好一个输入节点为2,输出节点为1的Net.
第四:我们需要根据网络和区域内部边界样本点和
{
L
=
L
I
+
β
L
B
,
L
I
=
1
n
I
∑
i
=
0
n
I
−
1
∣
∇
u
(
θ
)
i
∣
2
−
2
f
i
u
(
θ
)
i
,
L
B
=
1
n
B
∑
i
=
0
n
B
−
1
(
u
(
θ
)
−
u
0
)
i
2
.
\left\{

第五:有了损失函数以后,我们需要对网络Net的参数做迭代优化,比如最简单的梯度下降法:
θ
k
+
1
=
θ
k
−
c
∇
L
(
θ
k
)
\theta^{k+1}=\theta^k - c \nabla L(\theta^k)
θk+1=θk−c∇L(θk)
对于大部分人来说,手动求出
∇
L
(
θ
k
)
\nabla L(\theta^k)
∇L(θk)极为困难,幸好pytorch自带这个功能,只需要调用L.backwad()函数就可以得到,并且调用pytorch自带的优化器比如说torch.optim.Adam(Net.parameters(),lr)就可以自动实现参数的更新,lr是步长,也称learning rate,参考论文,代码参考添加链接描述.

考虑该变分问题,可以发现这种方法有几个特点,首先这个问题只需要利用一阶梯度信息,其次这个问题不需要额外增加测试函数,正是这两点,使得Ritz方法非常适合求解高维PDE。但是另一方面,如果使用惩罚函数方法求解该优化问题,损失函数的最优值不见得是0,此时优化过程难以控制。因此如果可以寻找到一种方法,使得近似解自动满足边界条件,就可以避免惩罚项的出现。
为了进一步提高精度,有的论文修改了针对泊松方程
Galerkin
Galerkin的特点,只需要利用pytorch对模型求一阶梯度(可以大幅度降低计算时间和存储成本,相比于PINN而言,PINN需要额外存储几个梯度数组),但是必须要提前准备好测试函数数据,这个对数学功底要求很高。以及,Galerkin仍然不能用来求解高维PDE,原因在于高维空间中的测试函数
v
v
v 是定义在高维超立方体的函数,这个定义一定需要利用for循环定义,而且高维超立方体的采样不容易。
Galerkin原理是从另一个角度来对泊松方程进行降阶处理,具体参考公式
{
∫
Ω
∇
u
⋅
∇
v
−
f
v
d
Ω
=
0
,
subject to
u
=
u
0
,
x
∈
∂
Ω
,
v
∈
V
h
=
{
v
∣
v
∣
∂
Ω
=
0
}
.
\left\{
在代码的实现过程中,除了采集样本点
X
X
X和准备右端项
f
(
X
)
f(X)
f(X)以外,这里还需要额外准备好
v
(
X
)
,
∇
v
(
X
)
v(X),\nabla v(X)
v(X),∇v(X),在本人的代码实现中,
v
v
v取得是前面有限元提到的单元函数
φ
i
,
j
\varphi_{i,j}
φi,j,值得注意的是,此时我们需要选取尽可能多个测试函数
v
v
v带入方程
特别注意,Galerkin对数学功底要求很大的另一个原因在于推导弱形式,上面这个二维泊松方程的弱形式只需要利用二维积分的格林公式即可,但是我特意没用含时间的热方程举例子,原来就在于即使是那种简单例子,弱形式的推导就已经有难度了。下面罗列一个NS方程的例子.

上面这个NS方程的弱形式就连很多数学系本科的同学也很难推导(此时反而利用PINN会更加方便)。PINN和Galerkin都有一个特点,就是损失函数直接描述了PDE的求解情况,根据损失函数的定义我们知道,神经网络的训练就是尽可能让损失函数接近于0,和0越接近,说明模型训练的效果越好。
在论文中,以区域内点为中心构造了
φ
i
,
j
,
1
≤
M
−
1
,
1
≤
j
≤
N
−
1
\varphi_{i,j},1 \leq M - 1,1 \leq j \leq N - 1
φi,j,1≤M−1,1≤j≤N−1的
(
M
−
1
)
(
N
−
1
)
(M - 1)(N - 1)
(M−1)(N−1)个测试函数,然后代入方程
{
L
=
∑
i
=
1
M
−
1
∑
j
=
1
N
−
1
L
i
,
j
+
β
L
B
,
L
i
=
(
∫
Ω
∇
u
⋅
∇
φ
i
,
j
−
f
φ
i
,
j
d
Ω
)
2
,
L
B
=
1
n
B
∑
i
=
0
n
B
−
1
(
u
(
θ
)
−
u
0
)
i
2
.
\left\{
代码参考添加链接描述
\subsubsection{WAN}
对抗神经网络求解泊松方程的核心是利用两个网络,
N
u
N_u
Nu模拟精确解
u
u
u,
N
v
N_v
Nv模拟测试函数
v
v
v,在训练过程中需要不断调整网络
N
v
N_v
Nv的参数使得
v
v
v尽量填充整个
V
h
V_h
Vh空间.假设
θ
\theta
θ表示网络
N
u
N_u
Nu的参数,
η
\eta
η表示网络
N
v
N_v
Nv的参数,那么WAN的核心思想可以理解为求解下列优化问题
max
η
min
θ
L
.
\max_{\eta}\min_{\theta} L.
maxηminθL.
这里需要注意,为了使得
v
∈
V
h
v \in V_h
v∈Vh,我们需要在网络
N
v
N_v
Nv的基础上乘一个length factor,与此同时这两个网络需要交替训练,具体细节参考论文和代码添加链接描述
physics informed neural network最早出现于论文,核心在于下面这个二阶损失函数
{
L
=
L
I
+
β
L
B
,
L
I
=
1
n
I
∑
i
=
0
n
I
−
1
(
−
Δ
u
−
f
)
i
2
,
L
B
=
1
n
B
∑
i
=
0
n
B
−
1
(
u
(
θ
)
−
u
0
)
i
2
.
\left\{

PINN的优势在于损失函数的形成非常直观,如果使用高阶优化器可以得到非常好的效果。PINN的缺点在于不能求解高维PDE,原来在于自动求导的时候需要求二阶导数,想象一下,如果问题是一个100维的,求解一阶导数的时候只需要利用下面这一行代码就可以解决。
u_x, = torch.autograd.grad(u, inset.X, create_graph=True, retain_graph=True,
grad_outputs=torch.ones(inset.size,1))
但是,如果求解二阶导数,就必须要使用for循环,在形成损失函数的时候出现这个for循环将会导致优化过程特别缓慢,没有特别意义。
u_xx = torch.zeros(100,N)
for i in range(100):
u_xx[i:i + 1,:], = torch.autograd.grad(u_x[i:i + 1,:], inset.X, create_graph=True, retain_graph=True,
grad_outputs=torch.ones(inset.size,1))
仍然考虑泊松方程,不过这里的右端项
f
=
f
(
x
,
μ
)
,
μ
∈
P
f = f(\mathbf{x},\mathbf{\mu}),\mathbf{\mu} \in \mathcal{P}
f=f(x,μ),μ∈P,现在需要求解对于不同参数
μ
\mathbf{\mu}
μ的精确解,我们以
u
(
x
,
y
,
μ
)
=
exp
(
−
(
x
−
μ
1
)
2
+
(
y
−
μ
2
)
2
2
μ
3
2
)
u(x,y,\mathbf{\mu})=\exp(-\frac{(x - \mu_1)^2 + (y - \mu_2)^2}{2\mu_3^2})
u(x,y,μ)=exp(−2μ32(x−μ1)2+(y−μ2)2)
为例子,其中
μ
=
(
μ
1
,
μ
2
,
μ
3
)
∈
[
0
,
1
]
×
[
0
,
1
]
×
[
0.1
,
1
]
\mathbf{\mu}=(\mu_1,\mu_2,\mu_3) \in [0,1]\times[0,1]\times[0.1,1]
μ=(μ1,μ2,μ3)∈[0,1]×[0,1]×[0.1,1].对于这个问题,其实做法和上面的泊松方程处理类似,可以把
u
u
u理解为在一个5维空间的函数,也就是说在求解过程中,只需要在采样过程将原来的二维空间采样修改成现在的5维空间采样,把空间坐标和参数都当成输入传进网络就可以得到一个含参问题的解.引入网络Net接收
(
x
,
y
,
μ
1
,
μ
2
,
μ
3
)
(x,y,\mu_1,\mu_2,\mu_3)
(x,y,μ1,μ2,μ3)为输入,输出对应的解即可,网络搭建参考


为了更好理解,这里我们将针对一个典型的PINN求解含参PDE问题代码做一个细致的说明,代码参考添加链接描述
第一步:导入相关的库,torch是用于神经网络搭建和训练的库,安装参考官方说明添加链接描述,matplotlib用于可视化,用法比较复杂,本人也需要百度才能知道如何针对需求使用命令,time用于计时,numpy是python最主要的数学库,注意最后那个bfgs是本人课题组自己编写的一个优化器,读者可以注释掉,最后优化的时候用torch自带的LBFGS优化器代替.

第二步:定义代码的运行设备device,这里本人选择GPU也就是cuda,读者需要自己确定自己电脑或者服务器是否有GPU,其次定义精确解UU和右端项FF,UU里面的order表示精确解的阶数,比如说
[
0
,
1
]
[0,1]
[0,1]表示
u
y
u_y
uy,bounds表示求解区域和参数空间.

第三步:定义INSET,BDSET,TESET这三个类,分别表示内点集合,边界点集合,测试集集合,其中还会封装一些跟样本点有关的数据进去,比如说右端项.

第四步:搭建网络,这个Net类接受两个参数,layer表示网络的层数和每层的神经元个数,dtype表示网络的数据类型,我后面取的是double,根据我的经验,数据精度高得到的效果也会更好一些,当然了,这个也更消耗内存.

第五步:修改数据类型,上面的INSET那三个类里面的数据默认都是float,为了和Net数据类型匹配,这里需要把INSET那几个类里面的数据类型进行修改,比如说 x = x . t y p e ( d t y p e ) x = x.type(dtype) x=x.type(dtype)就可以把 x x x从float变成dtype一样的类型,然后为了后面训练的时候加速,我们一开始就提出要把代码送到GPU上训练,但是数据一开始默认在CPU上,因此我们还需要写一个函数把所有的数据搬运到GPU,为了训练完以后数据可以回到CPU,这里相应地也要定义一个函数把数据从GPU搬运回CPU.
第六步:定义损失函数和训练函数,损失函数之前讲过,这里不赘述,主要介绍一下训练函数的定义.训练需要优化器optim,我一般都取LBFGS或者我们课题组自己写的BFGS,如果使用Adam优化器,训练过程的核心就是里面注释掉的代码,但是BFGS或者LBFGS的核心就是closure这个函数.这里做一些解释,optim.zero
_
\_{}
_grad表示清除优化器里面存储的梯度,防止每一次的梯度进行叠加,loss.backward表示反向传播,这里会自动计算损失函数关于网络参数
W
,
b
W,b
W,b的梯度,optim.step是负责根据优化器来对参数
W
,
b
W,b
W,b做更新,三者缺一不可,等到这三个结束以后还需要重新计算一下loss,这里本人不是特别理解,但是本人的实验指出这一步很重要,如果注释掉,结果不对.

最后就是对上面定义的函数,网络,类进行实例化和调用,然后一切就结束了.

上述提到的几种方法各有特点,这里对这些方法做一个总结.

神经网络方法求解PDE的困难和弊端
1:神经网络方法求解PDE缺乏收敛性保证。通过上面的介绍,我们可以这么理解,神经网络求解PDE是把原始问题转化为一个优化问题,然后不断通过更新神经网络的参数来达到min loss function的做法,比如说PINN和Galerkin思想,神经网络形成的损失函数只要不断接近0,那么说明神经网络学习的越好。但是现在有一个问题,会不会出现一种情况,形成了一个损失函数,不管怎么更新网络的参数,损失函数都维持在一个较高的水平呢?或者另一种情况,神经网络需要花大量时间训练才能把损失函数降到一个较低的水平。下面我们看一个障碍流问题的例子。

上面这个就是计算区域,这个问题描述了流体从入口进入管道,管道内部有一个障碍,然后现在想预测流体在出口的流速。这个问题可以使用PINN的思想,先采集样本点,然后搭建网络,优化损失函数,下面先给出这个问题的一个参考解,这个解是通过OpenFoam求出来的。

本人训练过程就发现,这个问题的损失函数特别难下降,具体可以参考下面这个图片,虽然原理在那摆着,但是这个问题却难以求解。

本人总结了一句话:PINN或者是Galerkin这类神经网络方法,都只能求解简单的PDE,对于简单的PDE,往往可以得到优于传统数值方法的效果,但是训练时间绝对更长。对于复杂的PDE,比如说NS方程,尤其是带有复杂边界的问题,这类简单的NN方法无法得到多好的效果,但是另一方面,传统数值方法求解复杂的PDE也同样需要消耗大量的计算精力。
个人以为NN求解PDE的优势应该在于求解高维的简单的PDE,比如说高维的Dirichlet边界泊松方程,这种方程由于计算区域维度更高,所以传统数值方法很容易发生维度灾难,但是神经网络方法却可以通过蒙特卡洛等方法对计算区域进行采样,轻而易举求解。
NN求解PDE目前更多还是应用于科研领域,对于实际应用目前难以推广,一个非常大的原因就是NN求解PDE没有收敛性的理论保证,对于PDE问题,NN就像一个炼丹炉,谁也不知道这个问题到底会求解成什么样子。反而是传统数值方法(FD,FE),虽然求解PDE很消耗时间,计算复杂度也高,但是这类方法的理论收敛经过几十年的研究论证,已经可以做到在求解问题之前就推断出计算结果大概可以到什么程度,这一点对于应用非常重要。
