偏微分方程数值解:从理论到实现

摘要:偏微分方程(PDE)是描述物理世界的核心数学工具。本文系统介绍有限差分法(FDM)、有限元法(FEM)、有限体积法(FVM)、谱方法以及线法(Method of Lines)五种主流PDE数值求解技术,每种方法均配备详细的理论推导、Python与MATLAB双语言实现代码,并对结果进行对比分析。


目录

  1. 引言
  2. [有限差分法 Finite Difference Method (FDM)](#有限差分法 Finite Difference Method (FDM))
  3. [有限元法 Finite Element Method (FEM)](#有限元法 Finite Element Method (FEM))
  4. [有限体积法 Finite Volume Method (FVM)](#有限体积法 Finite Volume Method (FVM))
  5. [谱方法 Spectral Method](#谱方法 Spectral Method)
  6. [线法与时间相关PDE Method of Lines](#线法与时间相关PDE Method of Lines)
  7. 方法对比与选型建议
  8. 参考文献

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 选型建议

  1. 规则区域 + 快速原型 → FDM

    • 代码量最小,调试最容易
    • 矩形域、周期边界的首选
  2. 复杂几何 + 力学/结构问题 → FEM

    • 固体力学、结构分析的工业标准
    • 需配合网格生成器(gmsh、Triangle等)
  3. 流体/传热/输运问题 → FVM

    • OpenFOAM、Fluent 等CFD软件的基础
    • 物理意义清晰,守恒性有保障
  4. 高精度 + 光滑解 + 简单区域 → 谱方法

    • 气象、流体稳定性分析
    • 节点少精度高,适合大规模时间步进
  5. 时间相关问题 → 线法 (MOL)

    • 空间离散 + 时间积分分离
    • 可复用成熟的ODE求解器

7.3 进阶方向

  • 多重网格法 :加速椭圆方程求解的最有效算法之一,复杂度接近 O ( N ) O(N) O(N)
  • 自适应网格细化 (AMR):在解变化剧烈处自动加密网格
  • DG有限元:兼具FEM精度和FVM守恒性,适合流体计算
  • 快速傅里叶变换 (FFT):谱方法的高效实现
  • GPU加速:利用CUDA/OpenCL加速稀疏矩阵运算

8. 参考文献

  1. LeVeque, R. J. Finite Difference Methods for Ordinary and Partial Differential Equations. SIAM, 2007.
  2. Strang, G. & Fix, G. J. An Analysis of the Finite Element Method. 2nd ed., Wellesley-Cambridge Press, 2008.
  3. Trefethen, L. N. Spectral Methods in MATLAB. SIAM, 2000.
  4. LeVeque, R. J. Finite Volume Methods for Hyperbolic Problems. Cambridge University Press, 2002.
  5. Patankar, S. V. Numerical Heat Transfer and Fluid Flow. Hemisphere, 1980.
  6. Schiesser, W. E. The Method of Lines: Integration of Partial Differential Equations. Academic Press, 1991.
  7. Saad, Y. Iterative Methods for Sparse Linear Systems. 2nd ed., SIAM, 2003.

PDE数值求解是计算数学的核心领域之一,方法选择需要结合问题特点、精度需求和计算资源综合权衡。本文提供的代码可以作为入门参考,实际工程应用中建议使用成熟的开源库,如Python的FEniCS/Firedrake/PyVista,MATLAB的PDE Toolbox,以及专业CFD软件OpenFOAM等。

相关推荐
麦烤楽鸡翅10 个月前
简单迭代法求单根的近似值
java·c++·python·数据分析·c·数值分析
2401_841495641 年前
【数值分析】插值法实验
python·数学·算法·可视化·数值分析·数学原理·插值法
李铁柱的1 年前
数值分析——与范数相关的概念(向量的数量积、欧式范数、范数的连续性与等价性、矩阵范数及其相容性、从属范数、谱半径)
学习笔记·数值分析·矩阵范数·序列范数·从属范数·谱半径
你知道网上冲浪吗1 年前
【原创理论】Stochastic Coupled Dyadic System (SCDS):一个用于两性关系动力学建模的随机耦合系统框架
python·算法·数学建模·数值分析
MPCTHU1 年前
数值分析知识重构
数值分析
明月看潮生1 年前
青少年编程与数学 02-015 大学数学知识点 07课题、数值分析
机器学习·青少年编程·数值分析·编程与数学
F_D_Z2 年前
误差与误差限
数值分析·误差·误差限
计算小屋2 年前
MATLAB 共轭梯度法求解线性方程组(附代码)
开发语言·matlab·数值分析·共轭梯度法
爱吃白饭2 年前
数值分析复习:Richardson外推和Romberg算法
笔记·数值分析