Python偏微分方程
- 1. 导入模块
- 2. 偏微分方程
- 3. 有限差分法
- 3.1 一维热传导
- 3.2 二维热传导
- 3.3 有源问题
- 3.4 稀疏矩阵性能
- 4. 有限元法
- 4.1 FEM求解步骤
偏微分方程(PDE)是多元微分方程,方程中的导数是偏导数。处理ODE和PDE所需的计算方法大不相同,后者对计算的要求更高。
数值求解PDE的大多数技术都基于将PDE问题中的每个因变量离散化的思想,从而将微分问题变换为代数形式。将PDE转化为代数问题的两种常用技术是有限差分法(FDM)和有限元法(FEM)。其中有限差分法是将问题中的导数近似为有限差分,而有限元法则是将未知函数写成简单基函数的线性组合,其中基函数可以较容易进行微分和积分。未知函数可以表示为基函数的一组系数。
求解PDE问题所需的计算资源一般都非常大,一部分原因是对空间进行离散化所需要点的数量与维数是指数关系。例如一个一维问题如果需要用100个点来表示,那么具有类似分辨率的二维问题将需要10000个点。由于离散空间中的每个点都对应一个未知变量,因此PDE问题需要非常大的方程组。与OED问题不同,不存在标准形式对任意PDE问题进行定义。
对于FDM和FEM,得到的代数方程组一般都非常大。在矩阵表示下,此类方程组一般都非常稀疏。基于存储和计算效率考虑,FDM和FEM都非常依赖于稀疏矩阵来表示代数线性方程组。
1. 导入模块
为了使用稀疏矩阵,我们将导入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
2. 偏微分方程
PDE中的未知函数是多元函数。在N维问题中,函数依赖于n个独立变量。一般的PDE可以写为:
其中表示自变量的所有一阶导数,表示所有二阶导数。这里的是已知函数,用于描述PDE的形式,是PDE问题的定义域。
为了简化符号,通常使用表示自变量x的一阶偏导数,表示二阶导数。
大部分PDE问题最多包含二阶导数,并且通常是在二维或者三维空间中求解问题。例如热量方程在二维笛卡尔坐标系中的形式是。这里函数用于描述时间时,点处的温度,是热传导系数。
为了完全确定PDE的解,需要定义PDE的边界条件。边界条件是沿着问题域的函数值(Dirichlet边界条件)或者外法线导数(Neumann边界条件)的组合。如果问题是时间依赖的,那么还需要初始值。
3. 有限差分法
有限差分法的基本思想是:利用离散空间中的有限差分公式来近似PDE中出现的导数。
例如,在将连续变量离散化成时,常导数的有限差分公式可以表示为:
前向差分公式
后向差分公式
中心差分公式
同样,也可以为高阶导数(例如二阶)构造有限差分公式:
使用有限差分公式替代ODE或者PDE中的导数,就可以将微分方程转换为代数方程。
3.1 一维热传导
为了具体说明有限差分法,我们首先考虑一维稳态热方程中的ODE问题,其中,边界条件是和。与常微分方程章节中讨论的ODE初始值问题不同,这是边界值问题。
我们将区间均匀离散成N+2个空间点(包含边界点),这样问题就转换为了找到这些点的函数值。将ODE问题写为有限差分的形式,得到方程:
其中间隔。
由于函数在两个边界点的值是已知的,因此存在N个位置变量,对应内部点的函数值。我们可以将内部点的方程组表示为矩阵形式 :
这里矩阵描述了方程和相邻点的耦合。边界值包含在向量中。
现在,我们可以直接求解线性方程组中的未知向量,从而获得离散点处函数的近似值。
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
使用SciPy的线性方程求解器求解方程组:
u = np.linalg.solve(A, d)
u
容易可知,该ODE问题的解析解为。我们现在对解进行可视化。
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)
3.2 二维热传导
沿着每个离散化的坐标使用有限差分公式,可以很容易将有限差分法扩展到更高维度。对于二维问题,可以用二维数组来表示未知的函数值。使用有限差分公式时,对于中的每个元素可以得到一个耦合方程组。为了将这些方程写为标准的矩阵-向量点乘的形式,可以将二维数组重排成向量,并构建有限差分方程对应的矩阵。
考虑二维热传导问题,PDE为拉普拉斯公式 ,源项为0,边界条件为:
有限差分形式为:
如果将x和y的区间分成N个内部点,那么。
为了将方程写成标准形式,可以重新排布矩阵,将其的行或者列叠加成大小为的向量。矩阵的大小是。由于有限差分公式中只有相邻点发生耦合,因此矩阵很稀疏。
N = 100
u0_t, u0_b = 5, -5
u0_l, u0_r = 3, -1
dx = 1. / (N+1)
二维问题中相邻的行和列都会发生耦合,因此构造矩阵会稍微复杂一些。一种相对直接的方式是首先定义矩阵,对应一个坐标轴上的一维公式。为了在每一行使用该公式,可以将大小为的对角矩阵与进行张量积计算。为了涵盖每一列上耦合方程的项,需要将与主对角线相隔个位置的对角线相加。
我们将使用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
矩阵的非零值有49600个,占总元素数目的0.496%,可见其非常稀疏。
从边界条件构建向量,可以先生成一个的零值数组,并将边界条件赋值给数组的边元素。随后,可以用reshape方法,将其重排成的向量。
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
生成数组和向量后,我们可以求解方程组,并使用reshape方法将转成的矩阵。
u = sp.linalg.spsolve(A, d).reshape(N, N)
为了可视化绘图,我们创建一个矩阵,将矩阵和边界条件组合到一起。
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)
3.3 有源问题
考虑PDE问题
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)
3.4 稀疏矩阵性能
正如上面展示,使用FDM方法得到的矩阵非常稀疏。我们下面对比稀疏矩阵和稠密矩阵求解方程所需的时间。
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更加灵活和方便。
4. 有限元法
简单来说,有限差分法是基于泰勒展开的近似,有限元法是基于变分法的近似。
有限元法FEM的基本思想是用有限的离散区域或单元的集合来代表PDE的定义域,并将未知函数近似为基函数的线性组合,而这些基函可以在每个单元(或一组相邻单元)上获得局部支持。
在数学上,这种近似解表示从函数空间中的精确解到有限子空间(问题域的离散化相关)的映射。如果是的合适子空间,可以预期能够很好近似。
为了在简化的函数空间中求解近似问题,可以将PDE从原始公式(称为强形式)重写为对应的变体形式(称为弱形式)。为了得到弱形式,我们将PDE乘以任意函数,并在整个问题域上积分。函数称为test函数,通常可以在不用于和的函数空间中定义该函数。
4.1 FEM求解步骤
通常而言,使用FEM求解PDE问题通常涉及以下步骤:
- 为问题域生成网格
- 将PDE写成弱形式
- 在FEM框架中对问题进行编码
- 求解得到的代数方程
- 后续处理和可视化
参考文献来自桑鸿乾老师的课件:科学计算和人工智能