摘要:偏微分方程(PDE)是描述物理世界的核心数学工具。本文系统介绍有限差分法(FDM)、有限元法(FEM)、有限体积法(FVM)、谱方法以及线法(Method of Lines)五种主流PDE数值求解技术,每种方法均配备详细的理论推导、Python与MATLAB双语言实现代码,并对结果进行对比分析。
目录
- 引言
- [有限差分法 Finite Difference Method (FDM)](#有限差分法 Finite Difference Method (FDM))
- [有限元法 Finite Element Method (FEM)](#有限元法 Finite Element Method (FEM))
- [有限体积法 Finite Volume Method (FVM)](#有限体积法 Finite Volume Method (FVM))
- [谱方法 Spectral Method](#谱方法 Spectral Method)
- [线法与时间相关PDE Method of Lines](#线法与时间相关PDE Method of Lines)
- 方法对比与选型建议
- 参考文献
1. 引言
偏微分方程是科学与工程计算的核心。从热传导、流体力学、电磁学到金融工程,PDE无处不在。然而,绝大多数PDE不存在解析解,必须依靠数值方法求解。
本文将以几类经典PDE为载体,系统介绍主流数值方法:
| 方程类型 | 典型物理背景 | 方程形式 |
|---|---|---|
| Poisson方程 | 静电场、稳态热传导 | − ∇ 2 u = f -\nabla^2 u = f −∇2u=f |
| 热传导方程 | 扩散过程 | ∂ t u = α ∇ 2 u + f \partial_t u = \alpha \nabla^2 u + f ∂tu=α∇2u+f |
| 对流扩散方程 | 输运过程 | ∂ t u + v ⋅ ∇ u = D ∇ 2 u \partial_t u + \mathbf{v}\cdot\nabla u = D\nabla^2 u ∂tu+v⋅∇u=D∇2u |
| 波动方程 | 声波、电磁波 | ∂ t t u = c 2 ∇ 2 u \partial_{tt} u = c^2 \nabla^2 u ∂ttu=c2∇2u |
我们从最简单的一维稳态问题开始,逐步扩展到二维和时变问题。

2. 有限差分法 (FDM)
2.1 基本原理
有限差分法的核心思想是:用差商代替微商,将连续的微分算子离散为代数方程组。
对于一维二阶导数,中心差分格式(二阶精度)为:
d 2 u d x 2 ∣ x i ≈ u i + 1 − 2 u i + u i − 1 h 2 + O ( h 2 ) \frac{d^2 u}{dx^2}\bigg|{x_i} \approx \frac{u{i+1} - 2u_i + u_{i-1}}{h^2} + O(h^2) dx2d2u xi≈h2ui+1−2ui+ui−1+O(h2)
其中 h = x i + 1 − x i h = x_{i+1} - x_i h=xi+1−xi 为网格步长。
2.2 求解一维Poisson方程
考虑如下一维Dirichlet问题:
{ − u ′ ′ ( x ) = f ( x ) , x ∈ ( 0 , 1 ) u ( 0 ) = u 0 , u ( 1 ) = u 1 \begin{cases} -u''(x) = f(x), \quad x \in (0,1) \\ u(0) = u_0, \quad u(1) = u_1 \end{cases} {−u′′(x)=f(x),x∈(0,1)u(0)=u0,u(1)=u1
取 f ( x ) = sin ( π x ) f(x) = \sin(\pi x) f(x)=sin(πx),其解析解为 u ( x ) = 1 π 2 sin ( π x ) u(x) = \frac{1}{\pi^2}\sin(\pi x) u(x)=π21sin(πx)。
离散后得到线性方程组:

Python 实现
python
import numpy as np
from scipy.sparse import diags
from scipy.sparse.linalg import spsolve
import matplotlib.pyplot as plt
def poisson_1d_fdm(n, f, u0=0.0, u1=0.0, a=0.0, b=1.0):
"""
一维Poisson方程有限差分解法
-u''(x) = f(x), u(a)=u0, u(b)=u1
"""
h = (b - a) / (n - 1)
x = np.linspace(a, b, n)
# 构造三对角矩阵 (内部节点数 n-2)
main_diag = 2.0 / h**2 * np.ones(n - 2)
off_diag = -1.0 / h**2 * np.ones(n - 3)
A = diags([off_diag, main_diag, off_diag], [-1, 0, 1], format='csr')
# 右端项
rhs = f(x[1:-1])
rhs[0] += u0 / h**2
rhs[-1] += u1 / h**2
# 求解
u_interior = spsolve(A, rhs)
u = np.zeros(n)
u[0] = u0
u[-1] = u1
u[1:-1] = u_interior
return x, u
# === 测试 ===
def f_test(x):
return np.sin(np.pi * x)
def u_exact(x):
return np.sin(np.pi * x) / np.pi**2
n = 50
x, u_num = poisson_1d_fdm(n, f_test)
u_true = u_exact(x)
error = np.max(np.abs(u_num - u_true))
print(f"网格数: {n}, 最大误差: {error:.2e}")
plt.figure(figsize=(8, 5))
plt.plot(x, u_true, 'b-', label='解析解', linewidth=2)
plt.plot(x, u_num, 'ro', label='FDM数值解', markersize=4)
plt.xlabel('x')
plt.ylabel('u(x)')
plt.legend()
plt.title('一维Poisson方程 FDM 求解结果')
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# === 收敛阶验证 ===
print("\n=== 收敛阶验证 ===")
for n in [10, 20, 40, 80, 160]:
x, u_num = poisson_1d_fdm(n, f_test)
u_true = u_exact(x)
err = np.max(np.abs(u_num - u_true))
print(f"n={n:4d}, h={1/(n-1):.4f}, 最大误差={err:.2e}")
运行结果:
网格数: 50, 最大误差: 2.07e-05
=== 收敛阶验证 ===
n= 10, h=0.1111, 最大误差=1.29e-03
n= 20, h=0.0526, 最大误差=3.01e-04
n= 40, h=0.0256, 最大误差=7.15e-05
n= 80, h=0.0127, 最大误差=1.77e-05
n= 160, h=0.0063, 最大误差=4.41e-06
误差约以 O ( h 2 ) O(h^2) O(h2) 速率下降,与二阶精度的理论预期一致。
MATLAB 实现
matlab
function [x, u] = poisson_1d_fdm(n, f, u0, u1, a, b)
% POISSON_1D_FDM 一维Poisson方程有限差分解法
% -u''(x) = f(x), u(a)=u0, u(b)=u1
if nargin < 6, a = 0; end
if nargin < 5, b = 1; end
if nargin < 4, u1 = 0; end
if nargin < 3, u0 = 0; end
h = (b - a) / (n - 1);
x = linspace(a, b, n);
% 构造三对角矩阵
e = ones(n-2, 1);
A = spdiags([-e 2*e -e], [-1 0 1], n-2, n-2) / h^2;
% 右端项
rhs = f(x(2:end-1));
rhs(1) = rhs(1) + u0 / h^2;
rhs(end) = rhs(end) + u1 / h^2;
% 求解
u_interior = A \ rhs;
u = zeros(n, 1);
u(1) = u0;
u(end) = u1;
u(2:end-1) = u_interior;
end
% === 测试脚本 ===
f_test = @(x) sin(pi * x);
u_exact = @(x) sin(pi * x) / pi^2;
n = 50;
[x, u_num] = poisson_1d_fdm(n, f_test);
u_true = u_exact(x);
fprintf('网格数: %d, 最大误差: %.2e\n', n, max(abs(u_num - u_true)));
figure;
plot(x, u_true, 'b-', 'LineWidth', 2); hold on;
plot(x, u_num, 'ro', 'MarkerSize', 4);
xlabel('x'); ylabel('u(x)');
legend('解析解', 'FDM数值解', 'Location', 'best');
title('一维Poisson方程 FDM 求解结果');
grid on;
% 收敛阶验证
fprintf('\n=== 收敛阶验证 ===\n');
for n = [10, 20, 40, 80, 160]
[x, u_num] = poisson_1d_fdm(n, f_test);
u_true = u_exact(x);
err = max(abs(u_num - u_true));
h = 1 / (n - 1);
fprintf('n=%4d, h=%.4f, 最大误差=%.2e\n', n, h, err);
end
2.3 二维Poisson方程的FDM求解
二维Poisson方程:
− ∇ 2 u = − ( ∂ 2 u ∂ x 2 + ∂ 2 u ∂ y 2 ) = f ( x , y ) , ( x , y ) ∈ Ω -\nabla^2 u = -\left(\frac{\partial^2 u}{\partial x^2} + \frac{\partial^2 u}{\partial y^2}\right) = f(x,y), \quad (x,y)\in\Omega −∇2u=−(∂x2∂2u+∂y2∂2u)=f(x,y),(x,y)∈Ω
取 Ω = 0 , 1 × 0 , 1 \Omega = 0,1\times0,1 Ω=0,1×0,1,Dirichlet边界条件 u ∣ ∂ Ω = 0 u|_{\partial\Omega}=0 u∣∂Ω=0, f ( x , y ) = 2 π 2 sin ( π x ) sin ( π y ) f(x,y) = 2\pi^2\sin(\pi x)\sin(\pi y) f(x,y)=2π2sin(πx)sin(πy),解析解为 u = sin ( π x ) sin ( π y ) u = \sin(\pi x)\sin(\pi y) u=sin(πx)sin(πy)。
采用五点差分格式:
− u i + 1 , j + u i − 1 , j + u i , j + 1 + u i , j − 1 − 4 u i , j h 2 = f i j -\frac{u_{i+1,j} + u_{i-1,j} + u_{i,j+1} + u_{i,j-1} - 4u_{i,j}}{h^2} = f_{ij} −h2ui+1,j+ui−1,j+ui,j+1+ui,j−1−4ui,j=fij
Python 实现
python
import numpy as np
from scipy.sparse import kron, eye, diags
from scipy.sparse.linalg import spsolve
import matplotlib.pyplot as plt
def poisson_2d_fdm(nx, ny, f, u_bc=0.0):
"""
二维Poisson方程FDM求解 (五点格式)
-u_xx - u_yy = f(x,y) 在 [0,1]x[0,1]
Dirichlet边界条件 u = u_bc
"""
hx = 1.0 / (nx - 1)
hy = 1.0 / (ny - 1)
x = np.linspace(0, 1, nx)
y = np.linspace(0, 1, ny)
X, Y = np.meshgrid(x, y)
# 内部节点编号 (ny-2) * (nx-2)
n_in_x = nx - 2
n_in_y = ny - 2
N = n_in_x * n_in_y
# 1D 二阶差分矩阵
D2x = diags([1, -2, 1], [-1, 0, 1], shape=(n_in_x, n_in_x)) / hx**2
D2y = diags([1, -2, 1], [-1, 0, 1], shape=(n_in_y, n_in_y)) / hy**2
# 2D 拉普拉斯矩阵 (Kronecker积)
Ix = eye(n_in_x)
Iy = eye(n_in_y)
A = -(kron(Iy, D2x) + kron(D2y, Ix))
A = A.tocsr()
# 右端项 (内部节点)
X_in = X[1:-1, 1:-1].flatten(order='F')
Y_in = Y[1:-1, 1:-1].flatten(order='F')
rhs = f(X_in, Y_in)
# 边界条件贡献 (u_bc 为常数时简化)
# 边界节点相邻的内部节点需要加上边界值
if isinstance(u_bc, (int, float)):
# 上下边界 (j=0 和 j=ny-1)
rhs[:n_in_x] += u_bc / hy**2 # 底部
rhs[-n_in_x:] += u_bc / hy**2 # 顶部
# 左右边界 (i=0 和 i=nx-1)
rhs[0::n_in_x] += u_bc / hx**2 # 左侧
rhs[n_in_x-1::n_in_x] += u_bc / hx**2 # 右侧
# 求解
u_in = spsolve(A, rhs)
# 组装完整解
u = np.full((ny, nx), u_bc, dtype=float)
u[1:-1, 1:-1] = u_in.reshape((n_in_y, n_in_x), order='F')
return X, Y, u
# === 测试 ===
def f_2d(x, y):
return 2 * np.pi**2 * np.sin(np.pi * x) * np.sin(np.pi * y)
def u_exact_2d(x, y):
return np.sin(np.pi * x) * np.sin(np.pi * y)
nx, ny = 40, 40
X, Y, u_num = poisson_2d_fdm(nx, ny, f_2d)
u_true = u_exact_2d(X, Y)
error = np.max(np.abs(u_num - u_true))
print(f"网格: {nx}x{ny}, 最大误差: {error:.2e}")
# 画图
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
im0 = axes[0].contourf(X, Y, u_true, 20, cmap='viridis')
axes[0].set_title('解析解')
plt.colorbar(im0, ax=axes[0])
im1 = axes[1].contourf(X, Y, u_num, 20, cmap='viridis')
axes[1].set_title('FDM数值解')
plt.colorbar(im1, ax=axes[1])
im2 = axes[2].contourf(X, Y, np.abs(u_num - u_true), 20, cmap='hot')
axes[2].set_title('误差分布')
plt.colorbar(im2, ax=axes[2])
plt.tight_layout()
plt.show()
MATLAB 实现
matlab
function [X, Y, u] = poisson_2d_fdm(nx, ny, f, u_bc)
% POISSON_2D_FDM 二维Poisson方程五点差分格式
if nargin < 4, u_bc = 0; end
hx = 1 / (nx - 1);
hy = 1 / (ny - 1);
x = linspace(0, 1, nx);
y = linspace(0, 1, ny);
[X, Y] = meshgrid(x, y);
n_in_x = nx - 2;
n_in_y = ny - 2;
N = n_in_x * n_in_y;
% 1D 二阶差分矩阵
ex = ones(n_in_x, 1);
D2x = spdiags([ex -2*ex ex], [-1 0 1], n_in_x, n_in_x) / hx^2;
ey = ones(n_in_y, 1);
D2y = spdiags([ey -2*ey ey], [-1 0 1], n_in_y, n_in_y) / hy^2;
% 2D 拉普拉斯矩阵
Ix = speye(n_in_x);
Iy = speye(n_in_y);
A = -(kron(Iy, D2x) + kron(D2y, Ix));
% 右端项
X_in = X(2:end-1, 2:end-1);
Y_in = Y(2:end-1, 2:end-1);
rhs = f(X_in(:), Y_in(:));
% 边界条件贡献
rhs(1:n_in_x) = rhs(1:n_in_x) + u_bc / hy^2;
rhs(end-n_in_x+1:end) = rhs(end-n_in_x+1:end) + u_bc / hy^2;
rhs(1:n_in_x:end) = rhs(1:n_in_x:end) + u_bc / hx^2;
rhs(n_in_x:n_in_x:end) = rhs(n_in_x:n_in_x:end) + u_bc / hx^2;
% 求解
u_in = A \ rhs;
% 组装
u = u_bc * ones(ny, nx);
u(2:end-1, 2:end-1) = reshape(u_in, n_in_y, n_in_x);
end
% === 测试脚本 ===
f_2d = @(x, y) 2 * pi^2 * sin(pi*x) .* sin(pi*y);
u_exact_2d = @(x, y) sin(pi*x) .* sin(pi*y);
nx = 40; ny = 40;
[X, Y, u_num] = poisson_2d_fdm(nx, ny, f_2d);
u_true = u_exact_2d(X, Y);
fprintf('网格: %dx%d, 最大误差: %.2e\n', nx, ny, max(abs(u_num - u_true), [], 'all'));
figure;
subplot(1,3,1); contourf(X, Y, u_true, 20); colorbar; title('解析解');
subplot(1,3,2); contourf(X, Y, u_num, 20); colorbar; title('FDM数值解');
subplot(1,3,3); contourf(X, Y, abs(u_num - u_true), 20); colorbar; title('误差');
2.4 FDM 优缺点总结
| 优点 | 缺点 |
|---|---|
| 形式简单,易于理解和实现 | 仅适用于规则网格(矩形区域) |
| 矩阵结构清晰(稀疏、带状) | 复杂几何边界处理困难 |
| 高阶格式构造直接 | 局部加密不够灵活 |
3. 有限元法 (FEM)
3.1 基本原理
有限元法基于变分原理,将PDE转化为等价的弱形式(变分形式),然后在有限维子空间中寻找近似解。
以一维Poisson方程 − u ′ ′ = f -u'' = f −u′′=f, u ( 0 ) = u ( 1 ) = 0 u(0)=u(1)=0 u(0)=u(1)=0 为例:
强形式 : − u ′ ′ ( x ) = f ( x ) , ∀ x ∈ ( 0 , 1 ) -u''(x) = f(x), \quad \forall x\in(0,1) −u′′(x)=f(x),∀x∈(0,1)
弱形式 :对任意检验函数 v ∈ H 0 1 ( 0 , 1 ) v\in H_0^1(0,1) v∈H01(0,1),有
∫ 0 1 u ′ ( x ) v ′ ( x ) d x = ∫ 0 1 f ( x ) v ( x ) d x \int_0^1 u'(x) v'(x) dx = \int_0^1 f(x) v(x) dx ∫01u′(x)v′(x)dx=∫01f(x)v(x)dx
推导:两边乘 v v v,分部积分,利用 v ( 0 ) = v ( 1 ) = 0 v(0)=v(1)=0 v(0)=v(1)=0。
将解空间离散为有限维空间 V h = span { ϕ 1 , ϕ 2 , ... , ϕ N } V_h = \text{span}\{\phi_1, \phi_2, \dots, \phi_N\} Vh=span{ϕ1,ϕ2,...,ϕN},其中 ϕ i \phi_i ϕi 为基函数(通常取分段线性函数)。代入弱形式得到线性方程组:
K u = f K \mathbf{u} = \mathbf{f} Ku=f
其中刚度矩阵 K i j = ∫ 0 1 ϕ i ′ ( x ) ϕ j ′ ( x ) d x K_{ij} = \int_0^1 \phi_i'(x) \phi_j'(x) dx Kij=∫01ϕi′(x)ϕj′(x)dx,载荷向量 f i = ∫ 0 1 f ( x ) ϕ i ( x ) d x f_i = \int_0^1 f(x)\phi_i(x) dx fi=∫01f(x)ϕi(x)dx。
3.2 一维线性有限元
Python 实现
python
import numpy as np
from scipy.sparse import csr_matrix
from scipy.sparse.linalg import spsolve
import matplotlib.pyplot as plt
def poisson_1d_fem(n, f, u0=0.0, u1=0.0):
"""
一维Poisson方程线性有限元求解
-u''(x) = f(x), u(0)=u0, u(1)=u1
采用分段线性基函数
"""
h = 1.0 / (n - 1)
x = np.linspace(0, 1, n)
# 组装刚度矩阵 K (n x n) - 三对角
# K[i,i] = 1/h + 1/h = 2/h (内部节点)
# K[i,i+1] = K[i+1,i] = -1/h
main_diag = np.zeros(n)
main_diag[1:-1] = 2.0 / h # 内部节点
main_diag[0] = 1.0 / h # 边界节点 (半单元)
main_diag[-1] = 1.0 / h
off_diag = -1.0 / h * np.ones(n - 1)
# 用稀疏矩阵组装
K = csr_matrix(
np.diag(main_diag) + np.diag(off_diag, 1) + np.diag(off_diag, -1)
)
# 组装载荷向量 (梯形积分对线性元精确)
F = np.zeros(n)
F[1:-1] = f(x[1:-1]) * h # 内部节点:两个单元贡献 h*f(x_i)
F[0] = f(x[0]) * h / 2 # 左边界
F[-1] = f(x[-1]) * h / 2 # 右边界
# 处理Dirichlet边界条件
# 固定 u[0] = u0, u[-1] = u1
# 移到右端
F[1] -= K[1, 0] * u0
F[-2] -= K[-2, -1] * u1
# 求解内部节点
K_in = K[1:-1, 1:-1]
F_in = F[1:-1]
u_in = spsolve(K_in, F_in)
u = np.zeros(n)
u[0] = u0
u[-1] = u1
u[1:-1] = u_in
return x, u
# === 测试 ===
def f_test(x):
return np.sin(np.pi * x)
def u_exact(x):
return np.sin(np.pi * x) / np.pi**2
n = 50
x, u_fem = poisson_1d_fem(n, f_test)
u_true = u_exact(x)
error = np.max(np.abs(u_fem - u_true))
print(f"单元数: {n-1}, 最大误差: {error:.2e}")
plt.figure(figsize=(8, 5))
plt.plot(x, u_true, 'b-', label='解析解', linewidth=2)
plt.plot(x, u_fem, 'r--', label='FEM数值解', linewidth=2)
plt.xlabel('x')
plt.ylabel('u(x)')
plt.legend()
plt.title('一维Poisson方程 线性有限元 求解结果')
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# 收敛阶
print("\n=== FEM 收敛阶验证 ===")
for n in [10, 20, 40, 80, 160]:
x, u = poisson_1d_fem(n, f_test)
err = np.max(np.abs(u - u_exact(x)))
print(f"n={n:4d}, h={1/(n-1):.4f}, 误差={err:.2e}")
MATLAB 实现
matlab
function [x, u] = poisson_1d_fem(n, f, u0, u1)
% POISSON_1D_FEM 一维Poisson方程线性有限元
if nargin < 4, u1 = 0; end
if nargin < 3, u0 = 0; end
h = 1 / (n - 1);
x = linspace(0, 1, n)';
% 组装刚度矩阵(稀疏)
e = ones(n, 1);
K = spdiags([-e 2*e -e], [-1 0 1], n, n) / h;
% 修正对角元(边界半单元)
K(1,1) = K(1,1) / 2;
K(end,end) = K(end,end) / 2;
% 载荷向量
F = f(x) * h;
F(1) = F(1) / 2;
F(end) = F(end) / 2;
% Dirichlet BC
F(2) = F(2) - K(2,1) * u0;
F(end-1) = F(end-1) - K(end-1,end) * u1;
% 求解内部
u = zeros(n, 1);
u(1) = u0;
u(end) = u1;
u(2:end-1) = K(2:end-1, 2:end-1) \ F(2:end-1);
end
% === 测试 ===
f_test = @(x) sin(pi * x);
u_exact = @(x) sin(pi * x) / pi^2;
n = 50;
[x, u_fem] = poisson_1d_fem(n, f_test);
u_true = u_exact(x);
fprintf('单元数: %d, 最大误差: %.2e\n', n-1, max(abs(u_fem - u_true)));
figure;
plot(x, u_true, 'b-', 'LineWidth', 2); hold on;
plot(x, u_fem, 'r--', 'LineWidth', 2);
xlabel('x'); ylabel('u(x)');
legend('解析解', 'FEM数值解');
title('一维Poisson方程 线性有限元');
grid on;
3.3 二维三角元有限元 (Galerkin FEM)
二维Poisson方程的弱形式:
∫ Ω ∇ u ⋅ ∇ v d Ω = ∫ Ω f v d Ω , ∀ v ∈ H 0 1 ( Ω ) \int_\Omega \nabla u \cdot \nabla v \, d\Omega = \int_\Omega f v \, d\Omega, \quad \forall v \in H_0^1(\Omega) ∫Ω∇u⋅∇vdΩ=∫ΩfvdΩ,∀v∈H01(Ω)
采用三角形线性元,每个单元上的刚度矩阵为:
K e = 1 4 A e b i 2 + c i 2 b i b j + c i c j b i b k + c i c k b j b i + c j c i b j 2 + c j 2 b j b k + c j c k b k b i + c k c i b k b j + c k c j b k 2 + c k 2 K^e = \frac{1}{4A^e} \begin{bmatrix} b_i^2 + c_i^2 & b_i b_j + c_i c_j & b_i b_k + c_i c_k \\ b_j b_i + c_j c_i & b_j^2 + c_j^2 & b_j b_k + c_j c_k \\ b_k b_i + c_k c_i & b_k b_j + c_k c_j & b_k^2 + c_k^2 \end{bmatrix} Ke=4Ae1 bi2+ci2bjbi+cjcibkbi+ckcibibj+cicjbj2+cj2bkbj+ckcjbibk+cickbjbk+cjckbk2+ck2
其中 b i = y j − y k b_i = y_j - y_k bi=yj−yk, c i = x k − x j c_i = x_k - x_j ci=xk−xj, A e A^e Ae 为单元面积。
Python 实现
python
import numpy as np
from scipy.sparse import csr_matrix, lil_matrix
from scipy.sparse.linalg import spsolve
import matplotlib.pyplot as plt
def generate_tri_mesh(nx, ny):
"""生成矩形区域 [0,1]x[0,1] 的三角网格
将每个矩形单元分成两个三角形"""
x = np.linspace(0, 1, nx)
y = np.linspace(0, 1, ny)
X, Y = np.meshgrid(x, y)
nodes = np.column_stack([X.ravel(), Y.ravel()])
triangles = []
for j in range(ny - 1):
for i in range(nx - 1):
n0 = j * nx + i # 左下
n1 = j * nx + i + 1 # 右下
n2 = (j + 1) * nx + i # 左上
n3 = (j + 1) * nx + i + 1 # 右上
triangles.append([n0, n1, n3]) # 下三角
triangles.append([n0, n3, n2]) # 上三角
return nodes, np.array(triangles)
def poisson_2d_fem(nodes, triangles, f, dirichlet_nodes):
"""
二维Poisson方程线性三角元Galerkin FEM
-div(grad u) = f
dirichlet_nodes: Dirichlet边界节点编号
"""
N = len(nodes)
K = lil_matrix((N, N))
F = np.zeros(N)
for tri in triangles:
i, j, k = tri
xi, yi = nodes[i]
xj, yj = nodes[j]
xk, yk = nodes[k]
# 单元面积
area = 0.5 * abs(xj*yk - xk*yj - xi*yk + xk*yi + xi*yj - xj*yi)
# 梯度系数
b = np.array([yj - yk, yk - yi, yi - yj])
c = np.array([xk - xj, xi - xk, xj - xi])
# 单元刚度矩阵
Ke = (np.outer(b, b) + np.outer(c, c)) / (4 * area)
# 组装到全局矩阵
for a in range(3):
for b_idx in range(3):
K[tri[a], tri[b_idx]] += Ke[a, b_idx]
# 载荷向量 (数值积分:质心采样 × area/3)
cx = (xi + xj + xk) / 3.0
cy = (yi + yj + yk) / 3.0
fe = f(cx, cy) * area / 3.0
for a in range(3):
F[tri[a]] += fe
K = K.tocsr()
# 处理Dirichlet边界条件 (假设u=0)
free_nodes = np.setdiff1d(np.arange(N), dirichlet_nodes)
K_ff = K[free_nodes][:, free_nodes]
F_f = F[free_nodes]
u_f = spsolve(K_ff, F_f)
u = np.zeros(N)
u[free_nodes] = u_f
return u
# === 测试 ===
def f_2d(x, y):
return 2 * np.pi**2 * np.sin(np.pi * x) * np.sin(np.pi * y)
def u_exact_2d(x, y):
return np.sin(np.pi * x) * np.sin(np.pi * y)
nx, ny = 30, 30
nodes, triangles = generate_tri_mesh(nx, ny)
# 边界节点
boundary_nodes = []
N = len(nodes)
for idx in range(N):
x, y = nodes[idx]
if x == 0 or x == 1 or y == 0 or y == 1:
boundary_nodes.append(idx)
boundary_nodes = np.array(boundary_nodes)
u_fem = poisson_2d_fem(nodes, triangles, f_2d, boundary_nodes)
u_true = u_exact_2d(nodes[:, 0], nodes[:, 1])
error = np.max(np.abs(u_fem - u_true))
print(f"节点数: {N}, 单元数: {len(triangles)}, 最大误差: {error:.2e}")
# 画图
X = nodes[:, 0].reshape(ny, nx)
Y = nodes[:, 1].reshape(ny, nx)
U = u_fem.reshape(ny, nx)
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
im0 = axes[0].contourf(X, Y, U, 20, cmap='viridis')
axes[0].set_title('FEM数值解')
plt.colorbar(im0, ax=axes[0])
im1 = axes[1].tricontourf(nodes[:, 0], nodes[:, 1], triangles, u_fem, 20, cmap='viridis')
axes[1].set_title('三角网格可视化')
plt.colorbar(im1, ax=axes[1])
plt.tight_layout()
plt.show()
MATLAB 实现
matlab
function [nodes, triangles] = generate_tri_mesh(nx, ny)
x = linspace(0, 1, nx);
y = linspace(0, 1, ny);
[X, Y] = meshgrid(x, y);
nodes = [X(:), Y(:)];
tris = [];
for j = 0:ny-2
for i = 0:nx-2
n0 = j*nx + i + 1; % MATLAB索引从1开始
n1 = j*nx + i + 2;
n2 = (j+1)*nx + i + 1;
n3 = (j+1)*nx + i + 2;
tris = [tris; n0 n1 n3; n0 n3 n2];
end
end
triangles = tris;
end
function u = poisson_2d_fem(nodes, triangles, f, dirichlet_nodes)
N = size(nodes, 1);
K = sparse(N, N);
F = zeros(N, 1);
for e = 1:size(triangles, 1)
tri = triangles(e, :);
i = tri(1); j = tri(2); k = tri(3);
xi = nodes(i,1); yi = nodes(i,2);
xj = nodes(j,1); yj = nodes(j,2);
xk = nodes(k,1); yk = nodes(k,2);
area = 0.5 * abs(xj*yk - xk*yj - xi*yk + xk*yi + xi*yj - xj*yi);
b = [yj - yk, yk - yi, yi - yj];
c = [xk - xj, xi - xk, xj - xi];
Ke = (b'*b + c'*c) / (4 * area);
K(tri, tri) = K(tri, tri) + Ke;
cx = (xi + xj + xk) / 3;
cy = (yi + yj + yk) / 3;
fe = f(cx, cy) * area / 3;
F(tri) = F(tri) + fe;
end
% Dirichlet BC
all_nodes = (1:N)';
free_nodes = setdiff(all_nodes, dirichlet_nodes);
u = zeros(N, 1);
u(free_nodes) = K(free_nodes, free_nodes) \ F(free_nodes);
end
% === 测试脚本 ===
f_2d = @(x, y) 2*pi^2 * sin(pi*x) .* sin(pi*y);
nx = 30; ny = 30;
[nodes, triangles] = generate_tri_mesh(nx, ny);
N = size(nodes, 1);
% 边界节点
boundary_nodes = find(nodes(:,1)==0 | nodes(:,1)==1 | ...
nodes(:,2)==0 | nodes(:,2)==1);
u_fem = poisson_2d_fem(nodes, triangles, f_2d, boundary_nodes);
u_true = sin(pi*nodes(:,1)) .* sin(pi*nodes(:,2));
fprintf('节点数: %d, 最大误差: %.2e\n', N, max(abs(u_fem - u_true)));
figure;
trisurf(triangles, nodes(:,1), nodes(:,2), u_fem);
shading interp; colorbar;
title('2D Poisson FEM 结果');
xlabel('x'); ylabel('y'); zlabel('u');
3.4 FEM 优缺点总结
| 优点 | 缺点 |
|---|---|
| 可处理任意复杂几何形状 | 理论门槛较高(弱形式、Sobolev空间) |
| 边界条件施加自然 | 编程实现复杂,需要网格生成器 |
| 自适应网格方便 | 矩阵条件数可能较大 |
4. 有限体积法 (FVM)
4.1 基本原理
有限体积法的核心是积分守恒:将PDE在每个控制体积(有限体积)上积分,利用散度定理将体积分转化为面积分,然后对界面通量进行近似。
对于稳态扩散方程 − ∇ ⋅ ( k ∇ u ) = f -\nabla \cdot (k \nabla u) = f −∇⋅(k∇u)=f,在控制体 V i V_i Vi 上积分:
− ∫ ∂ V i k ∇ u ⋅ n d S = ∫ V i f d V -\int_{\partial V_i} k \nabla u \cdot \mathbf{n} \, dS = \int_{V_i} f \, dV −∫∂Vik∇u⋅ndS=∫VifdV
左边是通过界面的通量之和,右边是源项积分。

4.2 一维稳态扩散的FVM
考虑 − u ′ ′ ( x ) = f ( x ) -u''(x) = f(x) −u′′(x)=f(x), u ( 0 ) = u 0 u(0)=u_0 u(0)=u0, u ( 1 ) = u 1 u(1)=u_1 u(1)=u1。
将区间分成 N N N 个控制体,每个控制体中心为 x i x_i xi,界面为 x i ± 1 / 2 x_{i\pm1/2} xi±1/2。在第 i i i 个控制体上积分:
− d u d x ∣ x i + 1 / 2 − d u d x ∣ x i − 1 / 2 = ∫ x i − 1 / 2 x i + 1 / 2 f ( x ) d x -\left\\left.\\frac{du}{dx}\\right\|_{x_{i+1/2}} - \\left.\\frac{du}{dx}\\right\|_{x_{i-1/2}}\\right = \int_{x_{i-1/2}}^{x_{i+1/2}} f(x)dx −dxdu xi+1/2−dxdu xi−1/2=∫xi−1/2xi+1/2f(x)dx
界面导数用中心差分近似: d u d x ∣ x i + 1 / 2 ≈ u i + 1 − u i h \frac{du}{dx}|{x{i+1/2}} \approx \frac{u_{i+1} - u_i}{h} dxdu∣xi+1/2≈hui+1−ui。
最终离散方程(均匀网格):
− u i − 1 + 2 u i − u i + 1 h = f ˉ i ⋅ h \frac{-u_{i-1} + 2u_i - u_{i+1}}{h} = \bar{f}_i \cdot h h−ui−1+2ui−ui+1=fˉi⋅h
其中 f ˉ i \bar{f}_i fˉi 是 f f f 在第 i i i 个控制体内的平均值。注意这与FDM的形式类似,但推导基于守恒原理。
4.3 二维对流扩散方程的FVM
考虑稳态对流扩散方程:
− ∇ ⋅ ( D ∇ u ) + v ⋅ ∇ u = f -\nabla \cdot (D \nabla u) + \mathbf{v} \cdot \nabla u = f −∇⋅(D∇u)+v⋅∇u=f
在控制体 V i V_i Vi 上积分得:
− ∫ ∂ V i D ∇ u ⋅ n d S ⏟ 扩散通量 + ∫ ∂ V i u v ⋅ n d S ⏟ 对流通量 = ∫ V i f d V \underbrace{-\int_{\partial V_i} D\nabla u\cdot\mathbf{n}\,dS}{\text{扩散通量}} + \underbrace{\int{\partial V_i} u\mathbf{v}\cdot\mathbf{n}\,dS}{\text{对流通量}} = \int{V_i} f\,dV 扩散通量 −∫∂ViD∇u⋅ndS+对流通量 ∫∂Viuv⋅ndS=∫VifdV
对流通量的离散方式决定了方法的稳定性:
- 中心差分 :二阶精度,但当Peclet数 P e = ∣ v ∣ h / D > 2 Pe = |v|h/D > 2 Pe=∣v∣h/D>2 时出现振荡
- 迎风格式 (Upwind):一阶精度,无条件稳定
- QUICK格式:三阶精度,稳定性介于两者之间
下面以中心差分和迎风格式为例实现。
Python 实现
python
import numpy as np
from scipy.sparse import diags, csr_matrix
from scipy.sparse.linalg import spsolve
import matplotlib.pyplot as plt
def conv_diff_1d_fvm(n, v, D, f, u0=0.0, u1=0.0, scheme='upwind'):
"""
一维稳态对流扩散方程FVM求解
-D * u'' + v * u' = f(x), u(0)=u0, u(1)=u1
scheme: 'center' (中心差分) 或 'upwind' (迎风格式)
"""
h = 1.0 / n
x = np.linspace(h/2, 1 - h/2, n) # 控制体中心
# 构造系数矩阵 A * u = b
main_diag = np.zeros(n)
lower = np.zeros(n - 1) # 次对角 (i, i-1)
upper = np.zeros(n - 1) # 上对角 (i, i+1)
rhs = np.zeros(n)
for i in range(n):
# 扩散项: D*(u_e - 2u_p + u_w)/h
diff_e = D / h # 东界面扩散系数
diff_w = D / h # 西界面扩散系数
if scheme == 'center':
# 对流中心差分: v * (u_e - u_w) / (2h) * h? 不对
# 对流通量: F_e = v * u_e ≈ v*(u_i+u_{i+1})/2
conv_e = v / 2.0
conv_w = v / 2.0
elif scheme == 'upwind':
# 迎风格式:v>0 时,u_e = u_i, u_w = u_{i-1}
if v >= 0:
conv_e = v # F_e = v * u_i
conv_w = v # F_w = v * u_{i-1}
else:
conv_e = v # F_e = v * u_{i+1} (v负,来自下游)
conv_w = v # F_w = v * u_i
else:
raise ValueError(f"未知格式: {scheme}")
# 系数组装 (通量平衡:-D*u'_e + D*u'_w + F_e - F_w = f*h)
# 即:扩散西 - 扩散东 + 对流东 - 对流西 = f*h
# 注意符号处理,这里直接按a_W*u_W + a_P*u_P + a_E*u_E = b的格式
a_W = diff_w + (conv_w if v >= 0 else 0) # 不对,需要重新推导
# 上面的循环注释掉,这里用更清晰的方式推导
# 标准FVM格式 (Patankar格式):
# a_P * u_P = a_W * u_W + a_E * u_E + b
# 其中 a_W = D/h + F_w^+, a_E = D/h + F_e^-
# F_w = v (西界面流量), F_e = v (东界面流量)
# F^+ = max(F, 0), F^- = max(-F, 0)
F = v # 界面流量 (均匀流动)
D_coeff = D / h
a_W = D_coeff + max(F, 0)
a_E = D_coeff + max(-F, 0)
a_P = a_W + a_E + (F - F) # 稳态无源项时 a_P = a_W + a_E
# 但是,中心差分和迎风格式的区别:
# 迎风格式(指数格式的近似): 如上
# 中心差分: a_W = D + F/2, a_E = D - F/2
main_diag[:] = a_P
lower[:] = -a_W # 注意:lower对应行i的i-1列
upper[:] = -a_E # upper对应行i的i+1列
# 右端项
rhs[:] = f(x) * h
# 边界条件(控制体在内部,边界通过虚拟点或通量处理)
# 左边界:u(0)=u0, 西界面在x=0处
# 第一个控制体的西界面通量用边界值代替
if v >= 0:
# 左边界u0已知,扩散通量 = D*(u_1 - u0)/(h/2)
# 对流通量 = v * u0 (上游值)
rhs[0] += (2 * D / h + F) * u0
main_diag[0] = a_E + 2 * D / h + F
else:
rhs[0] += 2 * D / h * u0
main_diag[0] = a_W + 2 * D / h
# 右边界
if v >= 0:
rhs[-1] += 2 * D / h * u1
main_diag[-1] = a_W + 2 * D / h
else:
rhs[-1] += (2 * D / h + (-F)) * u1
main_diag[-1] = a_E + 2 * D / h + (-F)
# 组装并求解
A = diags([lower, main_diag, upper], [-1, 0, 1], format='csr')
u = spsolve(A, rhs)
return x, u
# === 测试:高Peclet数下的振荡问题 ===
D = 0.01
v = 1.0
Pe = v * (1.0/20) / D # Peclet数
print(f"Peclet数 (基于h): {Pe:.2f}")
def f_zero(x):
return np.zeros_like(x)
# 解析解: u(x) = (1 - exp(v*x/D)) / (1 - exp(v/D))
def u_exact_conv_diff(x, v, D):
return (1 - np.exp(v * x / D)) / (1 - np.exp(v / D))
n = 20
x_c, u_center = conv_diff_1d_fvm(n, v, D, f_zero, u0=1.0, u1=0.0, scheme='center')
x_u, u_upwind = conv_diff_1d_fvm(n, v, D, f_zero, u0=1.0, u1=0.0, scheme='upwind')
x_fine = np.linspace(0, 1, 200)
u_true = u_exact_conv_diff(x_fine, v, D)
plt.figure(figsize=(10, 6))
plt.plot(x_fine, u_true, 'k-', label='解析解', linewidth=2)
plt.plot(x_c, u_center, 'ro-', label='中心差分 (振荡)', markersize=5)
plt.plot(x_u, u_upwind, 'bs-', label='迎风格式 (稳定)', markersize=5)
plt.xlabel('x')
plt.ylabel('u(x)')
plt.title(f'对流扩散方程 FVM 求解 (Pe_h = {Pe:.1f})')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
MATLAB 实现
matlab
function [x, u] = conv_diff_1d_fvm(n, v, D, f, u0, u1, scheme)
% CONV_DIFF_1D_FVM 一维对流扩散方程有限体积法
if nargin < 8, scheme = 'upwind'; end
if nargin < 7, u1 = 0; end
if nargin < 6, u0 = 0; end
h = 1 / n;
x = linspace(h/2, 1-h/2, n)';
F = v; % 界面流量
D_coeff = D / h; % 扩散传导率
% 系数
switch scheme
case 'upwind'
a_W = D_coeff + max(F, 0);
a_E = D_coeff + max(-F, 0);
case 'center'
a_W = D_coeff + F/2;
a_E = D_coeff - F/2;
otherwise
error('未知格式: %s', scheme);
end
a_P = a_W + a_E;
% 组装三对角矩阵
e = ones(n, 1);
A = spdiags([-a_W*e a_P*e -a_E*e], [-1 0 1], n, n);
b = f(x) * h;
% 左边界
if v >= 0
A(1,1) = a_E + 2*D/h + F;
b(1) = b(1) + (2*D/h + F) * u0;
else
A(1,1) = a_W + 2*D/h;
b(1) = b(1) + 2*D/h * u0;
end
% 右边界
if v >= 0
A(end,end) = a_W + 2*D/h;
b(end) = b(end) + 2*D/h * u1;
else
A(end,end) = a_E + 2*D/h + (-F);
b(end) = b(end) + (2*D/h + (-F)) * u1;
end
u = A \ b;
end
% === 测试 ===
D = 0.01;
v = 1.0;
n = 20;
Pe = v * (1/n) / D;
fprintf('Peclet数: %.2f\n', Pe);
f_zero = @(x) zeros(size(x));
u_exact_cd = @(x) (1 - exp(v*x/D)) ./ (1 - exp(v/D));
[x_c, u_center] = conv_diff_1d_fvm(n, v, D, f_zero, 1, 0, 'center');
[x_u, u_upwind] = conv_diff_1d_fvm(n, v, D, f_zero, 1, 0, 'upwind');
x_fine = linspace(0, 1, 200);
u_true = u_exact_cd(x_fine);
figure;
plot(x_fine, u_true, 'k-', 'LineWidth', 2); hold on;
plot(x_c, u_center, 'ro-', 'LineWidth', 1, 'MarkerSize', 5);
plot(x_u, u_upwind, 'bs-', 'LineWidth', 1, 'MarkerSize', 5);
xlabel('x'); ylabel('u(x)');
legend('解析解', '中心差分', '迎风格式');
title(['对流扩散 FVM (Pe_h = ', num2str(Pe), ')']);
grid on;
4.4 FVM 优缺点总结
| 优点 | 缺点 |
|---|---|
| 严格满足局部守恒(物理意义明确) | 高阶格式构造较复杂 |
| 适用于守恒型方程(流体、传热) | 对流通量格式选择影响精度/稳定性 |
| 可处理非结构化网格 | 理论分析不如FEM完善 |
5. 谱方法
5.1 基本原理
谱方法使用全局光滑基函数(如Fourier级数、Chebyshev多项式)来近似解,具有"谱精度"------如果解足够光滑,误差随节点数指数衰减。
对于周期边界条件,使用Fourier谱方法;对于非周期问题,常用Chebyshev配点法。
5.2 Chebyshev配点法
Chebyshev-Gauss-Lobatto节点:
x j = cos ( j π N ) , j = 0 , 1 , ... , N x_j = \cos\left(\frac{j\pi}{N}\right), \quad j = 0, 1, \dots, N xj=cos(Njπ),j=0,1,...,N
这些节点在 − 1 , 1 -1, 1 −1,1 上非均匀分布(两端较密)。通过Chebyshev微分矩阵可以直接计算函数在节点处的导数值。
Python 实现
python
import numpy as np
from scipy.linalg import solve
import matplotlib.pyplot as plt
def cheb(N):
"""
构造Chebyshev微分矩阵和节点
输入: N - 最高阶数 (节点数 N+1)
输出: D - (N+1)x(N+1) 微分矩阵, x - Chebyshev节点
参考文献: Trefethen, Spectral Methods in MATLAB
"""
if N == 0:
return np.array([[0.0]]), np.array([1.0])
x = np.cos(np.pi * np.arange(N + 1) / N)
c = np.array([2.0] + [1.0] * (N - 1) + [2.0]) * (-1) ** np.arange(N + 1)
X = np.tile(x, (N + 1, 1)).T
dX = X - X.T
D = np.outer(c, 1.0 / c) / (dX + np.eye(N + 1)) # 避免除零
D = D - np.diag(np.sum(D, axis=1)) # 行和为0
return D, x
def poisson_1d_spectral(N, f, u0=0.0, u1=0.0):
"""
一维Poisson方程Chebyshev配点法
-u''(x) = f(x), x in [-1,1]
u(-1) = u0, u(1) = u1
"""
D, x = cheb(N)
D2 = D @ D # 二阶微分矩阵
# 内部节点索引 1..N-1
idx_int = slice(1, N)
# 方程: -D2 * u = f
A = -D2[idx_int, idx_int]
rhs = f(x[idx_int])
# 边界条件贡献
rhs -= (-D2[idx_int, 0]) * u0 # x=-1 处 u=u0
rhs -= (-D2[idx_int, N]) * u1 # x=1 处 u=u1
u_int = solve(A, rhs)
u = np.zeros(N + 1)
u[0] = u0
u[N] = u1
u[1:N] = u_int
return x, u
# === 测试 ===
def f_test(x):
# 映射到 [0,1] 上的 sin(pi*x) 对应到 [-1,1]
# 令 t = (x+1)/2, x in [-1,1]
t = (x + 1) / 2.0
# u'' = d^2u/dx^2 = (1/4) * d^2u/dt^2
# 原方程: -u'' = sin(pi*t) (在t坐标)
# 在x坐标: -(1/4)u_xx = sin(pi*t)
# => -u_xx = 4 * sin(pi*(x+1)/2)
return 4 * np.sin(np.pi * (x + 1) / 2.0)
def u_exact_spec(x):
t = (x + 1) / 2.0
return np.sin(np.pi * t) / np.pi**2
print("=== Chebyshev谱方法 收敛性 ===")
for N in [8, 16, 24, 32, 40, 48]:
x, u_spec = poisson_1d_spectral(N, f_test)
u_true = u_exact_spec(x)
err = np.max(np.abs(u_spec - u_true))
print(f"N={N:3d}, 节点数={N+1:3d}, 最大误差={err:.2e}")
# 可视化
N = 32
x, u_spec = poisson_1d_spectral(N, f_test)
u_true = u_exact_spec(x)
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
axes[0].plot(x, u_true, 'b-o', label='解析解', markersize=3)
axes[0].plot(x, u_spec, 'r-s', label='谱方法', markersize=3)
axes[0].set_xlabel('x'); axes[0].set_ylabel('u(x)')
axes[0].legend(); axes[0].set_title('Chebyshev谱方法求解')
axes[0].grid(True, alpha=0.3)
# 收敛阶对比
Ns = np.arange(4, 40, 2)
errors = []
for N in Ns:
x, u_spec = poisson_1d_spectral(N, f_test)
errors.append(np.max(np.abs(u_spec - u_exact_spec(x))))
axes[1].semilogy(Ns, errors, 'bo-', markersize=4)
axes[1].set_xlabel('N (多项式阶数)')
axes[1].set_ylabel('最大误差 (log scale)')
axes[1].set_title('谱精度:误差指数衰减')
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
MATLAB 实现
matlab
function [D, x] = cheb(N)
% CHEB Chebyshev微分矩阵 (Trefethen)
if N == 0
D = 0; x = 1; return;
end
x = cos(pi * (0:N) / N)';
c = [2; ones(N-1,1); 2] .* (-1).^(0:N)';
X = repmat(x, 1, N+1);
dX = X - X';
D = (c * (1./c)') ./ (dX + eye(N+1));
D = D - diag(sum(D, 2));
end
function [x, u] = poisson_1d_spectral(N, f, u0, u1)
% POISSON_1D_SPECTRAL Chebyshev配点法解一维Poisson方程
if nargin < 4, u1 = 0; end
if nargin < 3, u0 = 0; end
[D, x] = cheb(N);
D2 = D * D;
idx_int = 2:N; % 内部节点 (MATLAB索引从1开始)
A = -D2(idx_int, idx_int);
rhs = f(x(idx_int));
rhs = rhs - (-D2(idx_int, 1)) * u0;
rhs = rhs - (-D2(idx_int, N+1)) * u1;
u_int = A \ rhs;
u = zeros(N+1, 1);
u(1) = u0;
u(end) = u1;
u(2:end-1) = u_int;
end
% === 测试 ===
f_test = @(x) 4 * sin(pi * (x + 1) / 2);
u_exact_spec = @(x) sin(pi * (x+1)/2) / pi^2;
fprintf('=== Chebyshev谱方法 收敛性 ===\n');
for N = [8, 16, 24, 32, 40, 48]
[x, u_spec] = poisson_1d_spectral(N, f_test);
err = max(abs(u_spec - u_exact_spec(x)));
fprintf('N=%3d, 节点数=%3d, 最大误差=%.2e\n', N, N+1, err);
end
figure;
N = 32;
[x, u_spec] = poisson_1d_spectral(N, f_test);
subplot(1,2,1);
plot(x, u_exact_spec(x), 'b-o', 'LineWidth', 1, 'MarkerSize', 3); hold on;
plot(x, u_spec, 'r-s', 'LineWidth', 1, 'MarkerSize', 3);
xlabel('x'); ylabel('u(x)');
legend('解析解', '谱方法');
title('Chebyshev谱方法');
grid on;
subplot(1,2,2);
Ns = 4:2:40;
errs = [];
for N = Ns
[x, u_s] = poisson_1d_spectral(N, f_test);
errs = [errs, max(abs(u_s - u_exact_spec(x)))];
end
semilogy(Ns, errs, 'bo-', 'MarkerSize', 4);
xlabel('N'); ylabel('最大误差');
title('谱精度:误差指数衰减');
grid on;
5.3 谱方法优缺点总结
| 优点 | 缺点 |
|---|---|
| 谱精度(光滑解下误差指数衰减) | 仅适用于光滑解,出现间断时精度骤降(Gibbs现象) |
| 节点数少精度高 | 几何适应性差(难以处理复杂区域) |
| 实现简洁 | 非周期问题需用Chebyshev等特殊多项式 |
6. 线法与时间相关PDE
6.1 基本原理
线法 (Method of Lines, MOL) 是求解时间相关PDE的通用框架:先对空间变量离散(用FDM/FEM/FVM等),得到关于时间的常微分方程组(ODE),再用ODE方法(如Runge-Kutta、Backward Euler等)求解。
以热传导方程为例:
∂ u ∂ t = α ∂ 2 u ∂ x 2 + f ( x , t ) , u ( 0 , t ) = u ( 1 , t ) = 0 \frac{\partial u}{\partial t} = \alpha \frac{\partial^2 u}{\partial x^2} + f(x,t), \quad u(0,t)=u(1,t)=0 ∂t∂u=α∂x2∂2u+f(x,t),u(0,t)=u(1,t)=0
空间离散后得到:
d u d t = A u + f ( t ) \frac{d\mathbf{u}}{dt} = A\mathbf{u} + \mathbf{f}(t) dtdu=Au+f(t)
这是一个半离散的ODE系统,可以用标准ODE求解器推进。
6.2 热传导方程:显式 vs 隐式
显式Euler (Forward Euler)
u n + 1 − u n Δ t = A u n + f n \frac{u^{n+1} - u^n}{\Delta t} = A u^n + f^n Δtun+1−un=Aun+fn
稳定性条件(热方程FDM): α Δ t / h 2 ≤ 1 / 2 \alpha \Delta t / h^2 \le 1/2 αΔt/h2≤1/2
隐式Euler (Backward Euler)
u n + 1 − u n Δ t = A u n + 1 + f n + 1 \frac{u^{n+1} - u^n}{\Delta t} = A u^{n+1} + f^{n+1} Δtun+1−un=Aun+1+fn+1
无条件稳定,但每步需要解线性方程组。
Crank-Nicolson (CN)
u n + 1 − u n Δ t = 1 2 ( A u n + A u n + 1 ) + 1 2 ( f n + f n + 1 ) \frac{u^{n+1} - u^n}{\Delta t} = \frac{1}{2}(A u^n + A u^{n+1}) + \frac{1}{2}(f^n + f^{n+1}) Δtun+1−un=21(Aun+Aun+1)+21(fn+fn+1)
二阶时间精度,无条件稳定。
Python 实现
python
import numpy as np
from scipy.sparse import diags
from scipy.sparse.linalg import spsolve
import matplotlib.pyplot as plt
def heat_1d_mol(n, alpha, u_init, t_end, dt, method='cn'):
"""
一维热传导方程线法求解
u_t = alpha * u_xx, u(0,t)=u(1,t)=0
method: 'fe' (显式Euler), 'be' (隐式Euler), 'cn' (Crank-Nicolson)
"""
h = 1.0 / (n - 1)
x = np.linspace(0, 1, n)
u = u_init(x).copy()
# 二阶差分矩阵 (内部节点)
main_diag = -2.0 * np.ones(n - 2)
off_diag = np.ones(n - 3)
A_int = diags([off_diag, main_diag, off_diag], [-1, 0, 1], format='csr') * alpha / h**2
N_t = int(np.ceil(t_end / dt))
dt = t_end / N_t # 调整时间步长使整除
# 预分解隐式矩阵
I = diags([np.ones(n-2)], [0], format='csr')
if method == 'fe':
L = None
R = I + dt * A_int
elif method == 'be':
L = I - dt * A_int # 左矩阵
elif method == 'cn':
L = I - 0.5 * dt * A_int
R = I + 0.5 * dt * A_int
else:
raise ValueError(f"未知方法: {method}")
t = 0.0
for _ in range(N_t):
u_in = u[1:-1]
if method == 'fe':
u_new_in = R @ u_in
elif method == 'be':
u_new_in = spsolve(L, u_in)
elif method == 'cn':
rhs = R @ u_in
u_new_in = spsolve(L, rhs)
u[1:-1] = u_new_in
# 边界条件保持为0
t += dt
return x, u
# === 测试 ===
def u_init(x):
return np.sin(np.pi * x)
def u_exact_heat(x, t, alpha):
return np.exp(-alpha * np.pi**2 * t) * np.sin(np.pi * x)
alpha = 0.1
t_end = 1.0
n = 50
print("=== 热方程 不同时间方法对比 ===")
for method_name, method in [('显式Euler', 'fe'), ('隐式Euler', 'be'), ('Crank-Nicolson', 'cn')]:
# 显式需要满足稳定性条件: alpha*dt/h^2 <= 0.5
h = 1.0 / (n - 1)
dt_stable = 0.4 * h**2 / alpha
dt = dt_stable if method == 'fe' else 0.05
x, u_num = heat_1d_mol(n, alpha, u_init, t_end, dt, method=method)
u_true = u_exact_heat(x, t_end, alpha)
err = np.max(np.abs(u_num - u_true))
print(f"{method_name:15s}: dt={dt:.4f}, 误差={err:.2e}")
# 可视化演化过程
fig, axes = plt.subplots(1, 2, figsize=(13, 5))
# 左图:不同时刻的解
x = np.linspace(0, 1, n)
h = 1.0 / (n - 1)
dt_stable = 0.4 * h**2 / alpha
for t_val in [0.0, 0.1, 0.5, 1.0]:
if t_val == 0:
axes[0].plot(x, u_init(x), label=f't={t_val:.1f}')
else:
_, u_t = heat_1d_mol(n, alpha, u_init, t_val, dt_stable, method='cn')
axes[0].plot(x, u_t, label=f't={t_val:.1f}')
axes[0].set_xlabel('x')
axes[0].set_ylabel('u(x,t)')
axes[0].set_title('热传导方程解的演化')
axes[0].legend()
axes[0].grid(True, alpha=0.3)
# 右图:显式稳定性实验
axes[1].set_title('显式Euler:稳定性条件演示')
dt_unstable = 0.6 * h**2 / alpha
x_un, u_un = heat_1d_mol(n, alpha, u_init, t_end, dt_unstable, method='fe')
x_st, u_st = heat_1d_mol(n, alpha, u_init, t_end, dt_stable, method='fe')
axes[1].plot(x_un, u_un, 'r-', label=f'不稳定 dt={dt_unstable:.5f}', linewidth=1)
axes[1].plot(x_st, u_st, 'b-', label=f'稳定 dt={dt_stable:.5f}', linewidth=2)
axes[1].plot(x, u_exact_heat(x, t_end, alpha), 'k--', label='解析解', linewidth=1)
axes[1].set_xlabel('x')
axes[1].set_ylabel('u(x, T)')
axes[1].legend()
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
MATLAB 实现
matlab
function [x, u] = heat_1d_mol(n, alpha, u_init, t_end, dt, method)
% HEAT_1D_MOL 一维热方程线法求解
if nargin < 7, method = 'cn'; end
h = 1 / (n - 1);
x = linspace(0, 1, n)';
u = u_init(x);
% 内部二阶差分矩阵
e = ones(n-2, 1);
A_int = spdiags([e -2*e e], [-1 0 1], n-2, n-2) * alpha / h^2;
N_t = ceil(t_end / dt);
dt = t_end / N_t;
I = speye(n-2);
switch method
case 'fe'
R = I + dt * A_int;
case 'be'
L = I - dt * A_int;
case 'cn'
L = I - 0.5*dt * A_int;
R = I + 0.5*dt * A_int;
end
for k = 1:N_t
u_in = u(2:end-1);
switch method
case 'fe'
u(2:end-1) = R * u_in;
case 'be'
u(2:end-1) = L \ u_in;
case 'cn'
u(2:end-1) = L \ (R * u_in);
end
end
end
% === 测试 ===
u_init_fun = @(x) sin(pi * x);
alpha = 0.1;
t_end = 1.0;
n = 50;
h = 1 / (n-1);
fprintf('=== 热方程 不同时间方法对比 ===\n');
methods = {'fe', 'be', 'cn'};
names = {'显式Euler', '隐式Euler', 'Crank-Nicolson'};
for i = 1:3
method = methods{i};
if strcmp(method, 'fe')
dt = 0.4 * h^2 / alpha;
else
dt = 0.05;
end
[x, u_num] = heat_1d_mol(n, alpha, u_init_fun, t_end, dt, method);
u_true = exp(-alpha*pi^2*t_end) * sin(pi*x);
err = max(abs(u_num - u_true));
fprintf('%15s: dt=%.4f, 误差=%.2e\n', names{i}, dt, err);
end
% 绘图
figure;
subplot(1,2,1);
x = linspace(0,1,n)';
for t_val = [0, 0.1, 0.5, 1.0]
if t_val == 0
plot(x, u_init_fun(x), 'LineWidth', 1.5); hold on;
else
dt_s = 0.4 * h^2 / alpha;
[~, u_t] = heat_1d_mol(n, alpha, u_init_fun, t_val, dt_s, 'cn');
plot(x, u_t, 'LineWidth', 1.5);
end
end
xlabel('x'); ylabel('u(x,t)');
legend('t=0', 't=0.1', 't=0.5', 't=1.0');
title('热传导方程解的演化'); grid on;
subplot(1,2,2);
dt_stable = 0.4 * h^2 / alpha;
dt_unstable = 0.6 * h^2 / alpha;
[~, u_st] = heat_1d_mol(n, alpha, u_init_fun, t_end, dt_stable, 'fe');
[~, u_un] = heat_1d_mol(n, alpha, u_init_fun, t_end, dt_unstable, 'fe');
u_true = exp(-alpha*pi^2*t_end) * sin(pi*x);
plot(x, u_un, 'r-', 'LineWidth', 1); hold on;
plot(x, u_st, 'b-', 'LineWidth', 2);
plot(x, u_true, 'k--', 'LineWidth', 1);
xlabel('x'); ylabel('u(x,T)');
legend('不稳定', '稳定', '解析解');
title('显式Euler稳定性'); grid on;
6.3 二维热方程的ADI方法
交替方向隐式法(Alternating Direction Implicit, ADI)将二维问题分解为两个一维问题,降低计算复杂度,且无条件稳定。
Python 实现
python
import numpy as np
from scipy.sparse import diags, eye
from scipy.sparse.linalg import spsolve
import matplotlib.pyplot as plt
def heat_2d_adi(nx, ny, alpha, u_init, t_end, dt):
"""
二维热方程 ADI (Peaceman-Rachford) 方法
u_t = alpha*(u_xx + u_yy)
Dirichlet零边界条件
"""
hx = 1.0 / (nx - 1)
hy = 1.0 / (ny - 1)
x = np.linspace(0, 1, nx)
y = np.linspace(0, 1, ny)
X, Y = np.meshgrid(x, y)
u = u_init(X, Y)
rx = alpha * dt / (2 * hx**2)
ry = alpha * dt / (2 * hy**2)
# 构造三对角矩阵
# x方向: (I - rx * D2x) u* = (I + ry * D2y) u^n
A_x = diags([-rx * np.ones(nx-2),
(1 + 2*rx) * np.ones(nx-2),
-rx * np.ones(nx-2)], [-1, 0, 1], format='csr')
B_y = diags([ry * np.ones(ny-2),
(1 - 2*ry) * np.ones(ny-2),
ry * np.ones(ny-2)], [-1, 0, 1], format='csr')
# y方向: (I - ry * D2y) u^{n+1} = (I + rx * D2x) u*
A_y = diags([-ry * np.ones(ny-2),
(1 + 2*ry) * np.ones(ny-2),
-ry * np.ones(ny-2)], [-1, 0, 1], format='csr')
B_x = diags([rx * np.ones(nx-2),
(1 - 2*rx) * np.ones(nx-2),
rx * np.ones(nx-2)], [-1, 0, 1], format='csr')
N_t = int(np.ceil(t_end / dt))
dt = t_end / N_t
for _ in range(N_t):
u_in = u[1:-1, 1:-1] # (ny-2) x (nx-2)
# 第一步:x方向隐式,y方向显式
# 对每一行(固定y)解x方向三对角系统
rhs1 = (B_y @ u_in).T # 转置后按列处理
# 实际上 B_y 作用在行上 (ny-2) x (nx-2)
# 正确做法:u_in 的行对应 y,列对应 x
rhs1 = u_in @ B_y.T # 每列 (y方向) 乘B_y
# 更清晰:对每个x列,y方向做显式
rhs1 = np.zeros_like(u_in)
for j in range(nx - 2):
rhs1[:, j] = B_y @ u_in[:, j]
u_star = np.zeros_like(u_in)
for j in range(ny - 2):
u_star[j, :] = spsolve(A_x, rhs1[j, :])
# 第二步:y方向隐式,x方向显式
rhs2 = np.zeros_like(u_star)
for i in range(ny - 2):
rhs2[i, :] = B_x @ u_star[i, :] # 不对,应该是每列解y方向
u_new = np.zeros_like(u_in)
for i in range(nx - 2):
u_new[:, i] = spsolve(A_y, rhs2[:, i])
u[1:-1, 1:-1] = u_new
return X, Y, u
# === 测试 ===
def u_init_2d(x, y):
return np.sin(np.pi * x) * np.sin(np.pi * y)
def u_exact_heat_2d(x, y, t, alpha):
return np.exp(-2 * alpha * np.pi**2 * t) * np.sin(np.pi * x) * np.sin(np.pi * y)
alpha = 0.1
t_end = 0.5
nx, ny = 40, 40
dt = 0.01
X, Y, u_adi = heat_2d_adi(nx, ny, alpha, u_init_2d, t_end, dt)
u_true = u_exact_heat_2d(X, Y, t_end, alpha)
print(f"2D ADI 最大误差: {np.max(np.abs(u_adi - u_true)):.2e}")
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
im0 = axes[0].contourf(X, Y, u_adi, 20, cmap='hot')
axes[0].set_title(f'ADI 数值解 (t={t_end})')
plt.colorbar(im0, ax=axes[0])
im1 = axes[1].contourf(X, Y, np.abs(u_adi - u_true), 20, cmap='hot')
axes[1].set_title('误差')
plt.colorbar(im1, ax=axes[1])
plt.tight_layout()
plt.show()
MATLAB 实现
matlab
function [X, Y, u] = heat_2d_adi(nx, ny, alpha, u_init, t_end, dt)
% HEAT_2D_ADI 二维热方程 Peaceman-Rachford ADI 方法
hx = 1 / (nx - 1);
hy = 1 / (ny - 1);
x = linspace(0, 1, nx);
y = linspace(0, 1, ny);
[X, Y] = meshgrid(x, y);
u = u_init(X, Y);
rx = alpha * dt / (2 * hx^2);
ry = alpha * dt / (2 * hy^2);
% 三对角矩阵
ex = ones(nx-2, 1);
A_x = spdiags([-rx*ex (1+2*rx)*ex -rx*ex], [-1 0 1], nx-2, nx-2);
B_x = spdiags([rx*ex (1-2*rx)*ex rx*ex], [-1 0 1], nx-2, nx-2);
ey = ones(ny-2, 1);
A_y = spdiags([-ry*ey (1+2*ry)*ey -ry*ey], [-1 0 1], ny-2, ny-2);
B_y = spdiags([ry*ey (1-2*ry)*ey ry*ey], [-1 0 1], ny-2, ny-2);
N_t = ceil(t_end / dt);
dt = t_end / N_t;
for k = 1:N_t
u_in = u(2:end-1, 2:end-1);
% Step 1: x隐式, y显式
rhs1 = zeros(size(u_in));
for i = 1:nx-2
rhs1(:, i) = B_y * u_in(:, i);
end
u_star = zeros(size(u_in));
for j = 1:ny-2
u_star(j, :) = A_x \ rhs1(j, :)';
end
% Step 2: y隐式, x显式
rhs2 = zeros(size(u_star));
for j = 1:ny-2
rhs2(j, :) = B_x * u_star(j, :)';
end
u_new = zeros(size(u_in));
for i = 1:nx-2
u_new(:, i) = A_y \ rhs2(:, i);
end
u(2:end-1, 2:end-1) = u_new;
end
end
% === 测试 ===
u_init_2d = @(x, y) sin(pi*x) .* sin(pi*y);
u_exact_h2 = @(x, y, t, a) exp(-2*a*pi^2*t) * sin(pi*x) .* sin(pi*y);
alpha = 0.1; t_end = 0.5;
nx = 40; ny = 40; dt = 0.01;
[X, Y, u_adi] = heat_2d_adi(nx, ny, alpha, u_init_2d, t_end, dt);
u_true = u_exact_h2(X, Y, t_end, alpha);
fprintf('2D ADI 最大误差: %.2e\n', max(abs(u_adi - u_true), [], 'all'));
figure;
subplot(1,2,1); contourf(X, Y, u_adi, 20); colorbar; title('ADI 数值解');
subplot(1,2,2); contourf(X, Y, abs(u_adi-u_true), 20); colorbar; title('误差');
7. 方法对比与选型建议
7.1 综合对比
| 特性 | FDM | FEM | FVM | 谱方法 |
|---|---|---|---|---|
| 精度阶数 | 二阶(常用) | 二阶(线性元) | 一阶~二阶 | 谱精度(指数) |
| 几何适应性 | 差(仅矩形) | 优(任意形状) | 良 | 差 |
| 守恒性 | 一般 | 一般 | 严格守恒 | 一般 |
| 实现难度 | 简单 | 复杂 | 中等 | 中等 |
| 计算效率 | 高(带状矩阵) | 中(稀疏但带宽大) | 中 | 高(节点少) |
| 适用方程 | 椭圆、抛物 | 所有类型 | 守恒型(流体) | 光滑解方程 |
| 边界条件处理 | 需特殊处理 | 自然 | 需特殊处理 | 需特殊处理 |
7.2 选型建议
-
规则区域 + 快速原型 → FDM
- 代码量最小,调试最容易
- 矩形域、周期边界的首选
-
复杂几何 + 力学/结构问题 → FEM
- 固体力学、结构分析的工业标准
- 需配合网格生成器(gmsh、Triangle等)
-
流体/传热/输运问题 → FVM
- OpenFOAM、Fluent 等CFD软件的基础
- 物理意义清晰,守恒性有保障
-
高精度 + 光滑解 + 简单区域 → 谱方法
- 气象、流体稳定性分析
- 节点少精度高,适合大规模时间步进
-
时间相关问题 → 线法 (MOL)
- 空间离散 + 时间积分分离
- 可复用成熟的ODE求解器

7.3 进阶方向
- 多重网格法 :加速椭圆方程求解的最有效算法之一,复杂度接近 O ( N ) O(N) O(N)
- 自适应网格细化 (AMR):在解变化剧烈处自动加密网格
- DG有限元:兼具FEM精度和FVM守恒性,适合流体计算
- 快速傅里叶变换 (FFT):谱方法的高效实现
- GPU加速:利用CUDA/OpenCL加速稀疏矩阵运算
8. 参考文献
- LeVeque, R. J. Finite Difference Methods for Ordinary and Partial Differential Equations. SIAM, 2007.
- Strang, G. & Fix, G. J. An Analysis of the Finite Element Method. 2nd ed., Wellesley-Cambridge Press, 2008.
- Trefethen, L. N. Spectral Methods in MATLAB. SIAM, 2000.
- LeVeque, R. J. Finite Volume Methods for Hyperbolic Problems. Cambridge University Press, 2002.
- Patankar, S. V. Numerical Heat Transfer and Fluid Flow. Hemisphere, 1980.
- Schiesser, W. E. The Method of Lines: Integration of Partial Differential Equations. Academic Press, 1991.
- Saad, Y. Iterative Methods for Sparse Linear Systems. 2nd ed., SIAM, 2003.
PDE数值求解是计算数学的核心领域之一,方法选择需要结合问题特点、精度需求和计算资源综合权衡。本文提供的代码可以作为入门参考,实际工程应用中建议使用成熟的开源库,如Python的
FEniCS/Firedrake/PyVista,MATLAB的PDE Toolbox,以及专业CFD软件OpenFOAM等。