当物理量不仅随时间变化,还随空间变化时,我们进入了偏微分方程(
PDE)的领域。热量在金属棒中传导、波在水面上扩散、电磁场在空间中传播——这些现象都需要
PDE 来描述。本章将介绍 PDE
的基本概念、分类,以及三大经典方程的求解方法。
从 ODE 到 PDE
为什么需要偏微分方程?
热传导方程的物理意义
在前面的章节中,我们研究的未知函数只依赖于一个自变量,如 或 。但现实世界中,很多物理量同时依赖于多个变量:
温度场 :依赖于空间位置和时间
波函数 :依赖于位置和时间
流体速度场 :三维空间加时间
当未知函数依赖于多个自变量时,微分方程中出现偏导数 ,这就是偏微分方程(
PDE) 。
基本概念
定义 :偏微分方程是含有未知函数及其偏导数的方程。
阶 :方程中出现的最高阶偏导数的阶数。
线性 vs 非线性 : -
线性:未知函数及其导数以线性方式出现 -
非线性:存在未知函数或导数的乘积、幂次等
例子 :
一维热方程(线性,二阶):
一维波动方程(线性,二阶):
Laplace 方程(线性,二阶):
Burgers 方程(非线性,二阶):
PDE 的分类
二阶线性 PDE 的标准形式
波动方程的解
二阶线性 PDE 的一般形式(两个自变量):
分类标准
类比于二次曲线 , PDE 根据判别式 分类:
判别式
类型
物理原型
特征
双曲型
波动方程
信息传播,有限速度
抛物型
热方程
扩散过程,不可逆
椭圆型
Laplace 方程
稳态,平衡
物理直觉
双曲型(波动) :信息以有限速度传播。如果你在湖中投一颗石子,涟漪会逐渐扩散,但远处暂时不受影响。
抛物型(扩散) :信息瞬间传播到无穷远(虽然影响很小)。热量会立即开始向周围扩散。
椭圆型(平衡) :没有时间演化,描述的是稳态。如静电场、稳态温度分布。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 import numpy as npimport matplotlib.pyplot as pltfrom mpl_toolkits.mplot3d import Axes3Dfrom matplotlib import cmfig, axes = plt.subplots(1 , 3 , figsize=(15 , 4 )) x = np.linspace(0 , 10 , 200 ) t_values = [0 , 1 , 2 , 3 ] for t in t_values: u = np.exp(-(x - 2 *t - 2 )**2 ) axes[0 ].plot(x, u, label=f't = {t} ' ) axes[0 ].set_xlabel('x' ) axes[0 ].set_ylabel('u' ) axes[0 ].set_title('双曲型:行波传播' ) axes[0 ].legend() axes[0 ].grid(True , alpha=0.3 ) for t in [0.01 , 0.1 , 0.5 , 1.0 ]: u = np.exp(-x**2 / (4 *t)) / np.sqrt(4 *np.pi*t) axes[1 ].plot(x - 5 , u, label=f't = {t} ' ) axes[1 ].set_xlabel('x' ) axes[1 ].set_ylabel('u' ) axes[1 ].set_title('抛物型:热扩散' ) axes[1 ].legend() axes[1 ].grid(True , alpha=0.3 ) axes[1 ].set_xlim([-5 , 5 ]) x = np.linspace(0 , 1 , 50 ) y = np.linspace(0 , 1 , 50 ) X, Y = np.meshgrid(x, y) U = np.sin(np.pi * X) * np.sinh(np.pi * Y) / np.sinh(np.pi) c = axes[2 ].contourf(X, Y, U, levels=20 , cmap='hot' ) plt.colorbar(c, ax=axes[2 ]) axes[2 ].set_xlabel('x' ) axes[2 ].set_ylabel('y' ) axes[2 ].set_title('椭圆型:稳态温度分布' ) plt.tight_layout() plt.savefig('pde_types.png' , dpi=150 , bbox_inches='tight' ) plt.show()
初边值问题
为什么需要边界条件?
拉普拉斯方程与调和函数
ODE 需要初始条件来确定解。 PDE
不仅需要初始条件 (对于演化型方程),还需要边界条件 (因为有空间域)。
边界条件的类型
设区域边界为 :
第一类( Dirichlet) :给定函数值
第二类( Neumann) :给定法向导数
第三类( Robin/混合) :函数值和导数的线性组合
适定性
一个 PDE 问题是适定 的( well-posed),如果: 1.
存在性 :解存在 2. 唯一性 :解唯一 3.
稳定性 :解连续依赖于数据
热方程
物理推导
分离变量法示意
考虑一维细杆的热传导。设
为 点在 时刻的温度。
Fourier 热传导定律 :热流量与温度梯度成正比
能量守恒 :
组合得到热方程 :
其中
是热扩散率 。
分离变量法
考虑问题:$
$
假设解的形式 : 代入方程:
分离变量:
(两边必须等于同一常数,因为左边只依赖 ,右边只依赖 )
得到两个 ODE:$
$
求解空间部分 ( Sturm-Liouville 问题):
边界条件
导致:
求解时间部分 :
一般解 :
确定系数 :由初始条件 Fourier 正弦级数系数: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 def heat_equation_analytical (f, L, alpha, x, t, n_terms=50 ): """ 热方程的解析解(分离变量法) u_t = alpha * u_xx u(0,t) = u(L,t) = 0 u(x,0) = f(x) """ from scipy.integrate import quad def integrand (xi, n ): return f(xi) * np.sin(n * np.pi * xi / L) u = np.zeros_like(x, dtype=float ) for n in range (1 , n_terms + 1 ): Bn, _ = quad(integrand, 0 , L, args=(n,)) Bn *= 2 / L lambda_n = (n * np.pi / L)**2 Xn = np.sin(n * np.pi * x / L) Tn = np.exp(-alpha * lambda_n * t) u += Bn * Xn * Tn return u L = 1 alpha = 0.01 def f_initial (x ): """三角形初始条件""" if x <= 0.5 : return 2 * x else : return 2 * (1 - x) f_vec = np.vectorize(f_initial) x = np.linspace(0 , L, 100 ) times = [0 , 0.1 , 0.5 , 1.0 , 2.0 , 5.0 ] plt.figure(figsize=(10 , 6 )) for t in times: if t == 0 : u = f_vec(x) else : u = heat_equation_analytical(f_initial, L, alpha, x, t) plt.plot(x, u, label=f't = {t} ' ) plt.xlabel('位置 x' ) plt.ylabel('温度 u' ) plt.title('热方程的解:三角形初始条件' ) plt.legend() plt.grid(True , alpha=0.3 ) plt.savefig('heat_analytical.png' , dpi=150 , bbox_inches='tight' ) plt.show()
有限差分法求解热方程
显式格式( FTCS) :
用向前差分近似时间导数,中心差分近似空间导数:
整理得:
其中 。
稳定性条件 : 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 def heat_ftcs (alpha, L, T, Nx, Nt, f, u_left=0 , u_right=0 ): """ 热方程的 FTCS 显式格式 """ dx = L / Nx dt = T / Nt r = alpha * dt / dx**2 print (f"网格: dx = {dx:.4 f} , dt = {dt:.4 f} " ) print (f"稳定性参数 r = {r:.4 f} (需要 r <= 0.5)" ) if r > 0.5 : print ("警告:可能不稳定!" ) x = np.linspace(0 , L, Nx + 1 ) u = f(x) u[0 ], u[-1 ] = u_left, u_right u_history = [u.copy()] t_history = [0 ] for n in range (Nt): u_new = u.copy() for j in range (1 , Nx): u_new[j] = u[j] + r * (u[j+1 ] - 2 *u[j] + u[j-1 ]) u_new[0 ], u_new[-1 ] = u_left, u_right u = u_new if (n + 1 ) % (Nt // 20 ) == 0 : u_history.append(u.copy()) t_history.append((n + 1 ) * dt) return x, u_history, t_history f_init = lambda x: np.sin(np.pi * x) L, T = 1 , 0.5 alpha = 0.1 Nx, Nt = 50 , 1000 x, u_history, t_history = heat_ftcs(alpha, L, T, Nx, Nt, f_init) fig, axes = plt.subplots(1 , 2 , figsize=(14 , 5 )) for i in range (0 , len (u_history), len (u_history)//5 ): axes[0 ].plot(x, u_history[i], label=f't = {t_history[i]:.3 f} ' ) x_fine = np.linspace(0 , L, 200 ) for t in [0 , T/4 , T/2 , 3 *T/4 , T]: u_exact = np.sin(np.pi * x_fine) * np.exp(-alpha * np.pi**2 * t) axes[0 ].plot(x_fine, u_exact, 'k--' , alpha=0.3 ) axes[0 ].set_xlabel('x' ) axes[0 ].set_ylabel('u' ) axes[0 ].set_title('FTCS 方法:热方程' ) axes[0 ].legend() axes[0 ].grid(True , alpha=0.3 ) t_grid = np.linspace(0 , T, len (u_history)) X, T_grid = np.meshgrid(x, t_grid) U = np.array(u_history) c = axes[1 ].contourf(X, T_grid, U, levels=20 , cmap='hot' ) plt.colorbar(c, ax=axes[1 ]) axes[1 ].set_xlabel('x' ) axes[1 ].set_ylabel('t' ) axes[1 ].set_title('温度的时空分布' ) plt.tight_layout() plt.savefig('heat_ftcs.png' , dpi=150 , bbox_inches='tight' ) plt.show()
隐式格式( Crank-Nicolson)
FTCS 的稳定性限制可能很严格。Crank-Nicolson
格式 是无条件稳定的:
这是一个三对角线性系统 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 def heat_crank_nicolson (alpha, L, T, Nx, Nt, f, u_left=0 , u_right=0 ): """ 热方程的 Crank-Nicolson 隐式格式 """ dx = L / Nx dt = T / Nt r = alpha * dt / (2 * dx**2 ) x = np.linspace(0 , L, Nx + 1 ) u = f(x) u[0 ], u[-1 ] = u_left, u_right n = Nx - 1 A_left = np.diag([1 + 2 *r] * n) A_left += np.diag([-r] * (n-1 ), 1 ) A_left += np.diag([-r] * (n-1 ), -1 ) A_right = np.diag([1 - 2 *r] * n) A_right += np.diag([r] * (n-1 ), 1 ) A_right += np.diag([r] * (n-1 ), -1 ) u_history = [u.copy()] t_history = [0 ] for step in range (Nt): b = A_right @ u[1 :-1 ] b[0 ] += r * (u_left + u_left) b[-1 ] += r * (u_right + u_right) u_interior = np.linalg.solve(A_left, b) u[1 :-1 ] = u_interior if (step + 1 ) % (Nt // 20 ) == 0 : u_history.append(u.copy()) t_history.append((step + 1 ) * dt) return x, u_history, t_history Nx, Nt = 50 , 50 x_ftcs, u_ftcs, t_ftcs = heat_ftcs(alpha, L, T, Nx, Nt, f_init) x_cn, u_cn, t_cn = heat_crank_nicolson(alpha, L, T, Nx, Nt, f_init) fig, axes = plt.subplots(1 , 2 , figsize=(14 , 5 )) for i in range (0 , min (len (u_ftcs), 5 )): axes[0 ].plot(x_ftcs, u_ftcs[i], label=f't = {t_ftcs[i]:.3 f} ' ) axes[0 ].set_xlabel('x' ) axes[0 ].set_ylabel('u' ) axes[0 ].set_title('FTCS 方法(可能不稳定)' ) axes[0 ].legend() axes[0 ].grid(True , alpha=0.3 ) axes[0 ].set_ylim([-2 , 2 ]) for i in range (0 , len (u_cn), len (u_cn)//5 ): axes[1 ].plot(x_cn, u_cn[i], label=f't = {t_cn[i]:.3 f} ' ) axes[1 ].set_xlabel('x' ) axes[1 ].set_ylabel('u' ) axes[1 ].set_title('Crank-Nicolson 方法(无条件稳定)' ) axes[1 ].legend() axes[1 ].grid(True , alpha=0.3 ) plt.tight_layout() plt.savefig('heat_stability.png' , dpi=150 , bbox_inches='tight' ) plt.show()
波动方程
物理推导
傅里叶级数展开
考虑绷紧的弦的小振动。设
为 点在 时刻的垂直位移。
由 Newton 第二定律:
得到波动方程 :
其中
是波速 。
d'Alembert 解
对于无界区域的初值问题:
解为:
这表明解由左行波 和右行波 的叠加组成。
分离变量法
对于有界区域 ,边界条件 :
系数由初始条件确定。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 def wave_equation_analytical (f, g, L, c, x, t, n_terms=50 ): """ 波动方程的解析解(分离变量法) u_tt = c ² u_xx u(0,t) = u(L,t) = 0 u(x,0) = f(x), u_t(x,0) = g(x) """ from scipy.integrate import quad u = np.zeros_like(x, dtype=float ) for n in range (1 , n_terms + 1 ): def f_integrand (xi ): return f(xi) * np.sin(n * np.pi * xi / L) def g_integrand (xi ): return g(xi) * np.sin(n * np.pi * xi / L) An, _ = quad(f_integrand, 0 , L) An *= 2 / L Bn, _ = quad(g_integrand, 0 , L) omega_n = n * np.pi * c / L Bn *= 2 / (L * omega_n) Xn = np.sin(n * np.pi * x / L) Tn = An * np.cos(omega_n * t) + Bn * np.sin(omega_n * t) u += Xn * Tn return u L = 1 c = 1 def f_pluck (x ): if x <= 0.3 : return x / 0.3 else : return (1 - x) / 0.7 f_pluck_vec = np.vectorize(f_pluck) g_zero = lambda x: 0 x = np.linspace(0 , L, 100 ) times = [0 , 0.25 , 0.5 , 0.75 , 1.0 , 1.25 , 1.5 , 1.75 ] fig, axes = plt.subplots(2 , 4 , figsize=(14 , 6 )) axes = axes.flatten() for i, t in enumerate (times): if t == 0 : u = f_pluck_vec(x) else : u = wave_equation_analytical(f_pluck, g_zero, L, c, x, t) axes[i].plot(x, u, 'b-' , linewidth=2 ) axes[i].axhline(y=0 , color='k' , linestyle='-' , alpha=0.3 ) axes[i].set_xlim([0 , L]) axes[i].set_ylim([-1.2 , 1.2 ]) axes[i].set_title(f't = {t:.2 f} ' ) axes[i].grid(True , alpha=0.3 ) plt.suptitle('波动方程:拨弦的振动' , fontsize=14 ) plt.tight_layout() plt.savefig('wave_analytical.png' , dpi=150 , bbox_inches='tight' ) plt.show()
有限差分法求解波动方程
显式格式 :
整理得:
其中 。
CFL 条件 : ( Courant-Friedrichs-Lewy)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 def wave_fd (c, L, T, Nx, Nt, f, g, u_left=0 , u_right=0 ): """ 波动方程的有限差分法 """ dx = L / Nx dt = T / Nt r = c * dt / dx print (f"CFL 数 r = {r:.4 f} (需要 r <= 1)" ) if r > 1 : print ("警告:不满足 CFL 条件,可能不稳定!" ) x = np.linspace(0 , L, Nx + 1 ) u_prev = f(x) u_curr = np.zeros(Nx + 1 ) for j in range (1 , Nx): u_curr[j] = u_prev[j] + dt * g(x[j]) + \ 0.5 * r**2 * (u_prev[j+1 ] - 2 *u_prev[j] + u_prev[j-1 ]) u_curr[0 ], u_curr[-1 ] = u_left, u_right u_history = [u_prev.copy(), u_curr.copy()] t_history = [0 , dt] for n in range (2 , Nt + 1 ): u_next = np.zeros(Nx + 1 ) for j in range (1 , Nx): u_next[j] = 2 *u_curr[j] - u_prev[j] + \ r**2 * (u_curr[j+1 ] - 2 *u_curr[j] + u_curr[j-1 ]) u_next[0 ], u_next[-1 ] = u_left, u_right u_prev = u_curr u_curr = u_next if n % (Nt // 20 ) == 0 : u_history.append(u_curr.copy()) t_history.append(n * dt) return x, u_history, t_history f_gauss = lambda x: np.exp(-100 *(x - 0.5 )**2 ) g_zero = lambda x: 0 L, T = 1 , 2 c = 1 Nx, Nt = 100 , 200 x, u_history, t_history = wave_fd(c, L, T, Nx, Nt, f_gauss, g_zero) fig, axes = plt.subplots(1 , 2 , figsize=(14 , 5 )) for i in range (0 , len (u_history), max (1 , len (u_history)//6 )): axes[0 ].plot(x, u_history[i], label=f't = {t_history[i]:.2 f} ' ) axes[0 ].set_xlabel('x' ) axes[0 ].set_ylabel('u' ) axes[0 ].set_title('波动方程:有限差分解' ) axes[0 ].legend() axes[0 ].grid(True , alpha=0.3 ) t_grid = np.array(t_history) X, T_grid = np.meshgrid(x, t_grid) U = np.array(u_history) c = axes[1 ].contourf(X, T_grid, U, levels=20 , cmap='seismic' ) plt.colorbar(c, ax=axes[1 ]) axes[1 ].set_xlabel('x' ) axes[1 ].set_ylabel('t' ) axes[1 ].set_title('位移的时空分布' ) plt.tight_layout() plt.savefig('wave_fd.png' , dpi=150 , bbox_inches='tight' ) plt.show()
Laplace 方程与 Poisson 方程
物理背景
Laplace 方程 :
描述稳态 、无源 的场: -
稳态温度分布(无内热源) - 静电势(无电荷区域) - 不可压缩流的速度势
Poisson 方程 :
当有源时使用(如有电荷、有热源)。
性质
极值原理 :调和函数(满足 Laplace
方程的函数)在区域内部不取极值,极值只在边界上达到。
均值性质 :圆心处的值等于圆周上的平均值。
有限差分法
用五点差分格式:
即:
这是一个大型稀疏线性系统 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 def laplace_2d (Nx, Ny, bc_left, bc_right, bc_bottom, bc_top ): """ 二维 Laplace 方程 ∇² u = 0 在单位正方形上,给定 Dirichlet 边界条件 """ hx = 1.0 / Nx hy = 1.0 / Ny n = (Nx - 1 ) * (Ny - 1 ) x = np.linspace(0 , 1 , Nx + 1 ) y = np.linspace(0 , 1 , Ny + 1 ) A = np.zeros((n, n)) b = np.zeros(n) def index (i, j ): """将 2D 索引转换为 1D""" return (j - 1 ) * (Nx - 1 ) + (i - 1 ) for j in range (1 , Ny): for i in range (1 , Nx): k = index(i, j) A[k, k] = -4 if i > 1 : A[k, index(i-1 , j)] = 1 else : b[k] -= bc_left(y[j]) if i < Nx - 1 : A[k, index(i+1 , j)] = 1 else : b[k] -= bc_right(y[j]) if j > 1 : A[k, index(i, j-1 )] = 1 else : b[k] -= bc_bottom(x[i]) if j < Ny - 1 : A[k, index(i, j+1 )] = 1 else : b[k] -= bc_top(x[i]) u_interior = np.linalg.solve(A, b) u = np.zeros((Ny + 1 , Nx + 1 )) for j in range (1 , Ny): for i in range (1 , Nx): u[j, i] = u_interior[index(i, j)] u[:, 0 ] = [bc_left(y[j]) for j in range (Ny + 1 )] u[:, -1 ] = [bc_right(y[j]) for j in range (Ny + 1 )] u[0 , :] = [bc_bottom(x[i]) for i in range (Nx + 1 )] u[-1 , :] = [bc_top(x[i]) for i in range (Nx + 1 )] return x, y, u bc_left = lambda y: 0 bc_right = lambda y: 0 bc_bottom = lambda x: 0 bc_top = lambda x: np.sin(np.pi * x) Nx, Ny = 50 , 50 x, y, u = laplace_2d(Nx, Ny, bc_left, bc_right, bc_bottom, bc_top) X, Y = np.meshgrid(x, y) u_exact = np.sin(np.pi * X) * np.sinh(np.pi * Y) / np.sinh(np.pi) fig = plt.figure(figsize=(14 , 5 )) ax1 = fig.add_subplot(131 ) c1 = ax1.contourf(X, Y, u, levels=20 , cmap='hot' ) plt.colorbar(c1, ax=ax1) ax1.set_xlabel('x' ) ax1.set_ylabel('y' ) ax1.set_title('数值解' ) ax1.set_aspect('equal' ) ax2 = fig.add_subplot(132 ) c2 = ax2.contourf(X, Y, u_exact, levels=20 , cmap='hot' ) plt.colorbar(c2, ax=ax2) ax2.set_xlabel('x' ) ax2.set_ylabel('y' ) ax2.set_title('精确解' ) ax2.set_aspect('equal' ) ax3 = fig.add_subplot(133 ) c3 = ax3.contourf(X, Y, abs (u - u_exact), levels=20 , cmap='viridis' ) plt.colorbar(c3, ax=ax3) ax3.set_xlabel('x' ) ax3.set_ylabel('y' ) ax3.set_title('绝对误差' ) ax3.set_aspect('equal' ) plt.tight_layout() plt.savefig('laplace_2d.png' , dpi=150 , bbox_inches='tight' ) plt.show() print (f"最大误差: {np.max (abs (u - u_exact)):.6 e} " )
迭代方法
对于大规模问题,直接求解太慢。常用迭代方法:
Jacobi 迭代 :
Gauss-Seidel 迭代 :
SOR(逐次超松弛) :
最优松弛因子约为 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 def laplace_sor (Nx, Ny, bc, f=None , omega=None , tol=1e-6 , max_iter=10000 ): """ SOR 方法求解 Laplace/Poisson 方程 bc: 字典 {'left': func, 'right': func, 'bottom': func, 'top': func} f: 源项函数( Poisson 方程),默认为 0( Laplace 方程) """ h = 1.0 / Nx if omega is None : omega = 2 / (1 + np.sin(np.pi * h)) x = np.linspace(0 , 1 , Nx + 1 ) y = np.linspace(0 , 1 , Ny + 1 ) u = np.zeros((Ny + 1 , Nx + 1 )) u[:, 0 ] = [bc['left' ](y[j]) for j in range (Ny + 1 )] u[:, -1 ] = [bc['right' ](y[j]) for j in range (Ny + 1 )] u[0 , :] = [bc['bottom' ](x[i]) for i in range (Nx + 1 )] u[-1 , :] = [bc['top' ](x[i]) for i in range (Nx + 1 )] residuals = [] for iteration in range (max_iter): max_change = 0 for j in range (1 , Ny): for i in range (1 , Nx): old_u = u[j, i] f_ij = f(x[i], y[j]) if f else 0 new_u = 0.25 * (u[j, i+1 ] + u[j, i-1 ] + u[j+1 , i] + u[j-1 , i] - h**2 * f_ij) u[j, i] = (1 - omega) * old_u + omega * new_u max_change = max (max_change, abs (u[j, i] - old_u)) residuals.append(max_change) if max_change < tol: print (f"SOR 收敛于第 {iteration + 1 } 次迭代" ) break return x, y, u, residuals bc = { 'left' : lambda y: 0 , 'right' : lambda y: 0 , 'bottom' : lambda x: 0 , 'top' : lambda x: np.sin(np.pi * x) } Nx, Ny = 50 , 50 x, y, u_sor, residuals = laplace_sor(Nx, Ny, bc) plt.figure(figsize=(10 , 5 )) plt.semilogy(residuals) plt.xlabel('迭代次数' ) plt.ylabel('最大变化量' ) plt.title('SOR 方法的收敛历史' ) plt.grid(True , alpha=0.3 ) plt.savefig('sor_convergence.png' , dpi=150 , bbox_inches='tight' ) plt.show()
数值方法的稳定性分析
Von Neumann 稳定性分析
基本思想:将误差展开为 Fourier 模式,分析每个模式的增长因子。
设误差 ,代入差分格式,求解 的条件。
例:热方程 FTCS 格式
代入 :
稳定性要求 :
当 时, 。
要使 ,需要 。
CFL 条件
对于波动方程, CFL
条件说:数值信息传播速度必须大于等于物理信息传播速度 。
物理波速:
数值信息传播速度: CFL 条件: ,即 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 c = 1 L, T = 1 , 0.5 Nx = 50 fig, axes = plt.subplots(1 , 3 , figsize=(15 , 4 )) Nt_stable = 100 r_stable = c * (T/Nt_stable) / (L/Nx) x1, u1, t1 = wave_fd(c, L, T, Nx, Nt_stable, f_gauss, g_zero) axes[0 ].plot(x1, u1[-1 ], 'b-' ) axes[0 ].set_title(f'稳定: r = {r_stable:.2 f} ' ) axes[0 ].set_ylim([-1.5 , 1.5 ]) axes[0 ].grid(True , alpha=0.3 ) Nt_boundary = 50 r_boundary = c * (T/Nt_boundary) / (L/Nx) x2, u2, t2 = wave_fd(c, L, T, Nx, Nt_boundary, f_gauss, g_zero) axes[1 ].plot(x2, u2[-1 ], 'g-' ) axes[1 ].set_title(f'边界稳定: r = {r_boundary:.2 f} ' ) axes[1 ].set_ylim([-1.5 , 1.5 ]) axes[1 ].grid(True , alpha=0.3 ) Nt_unstable = 30 r_unstable = c * (T/Nt_unstable) / (L/Nx) try : x3, u3, t3 = wave_fd(c, L, T, Nx, Nt_unstable, f_gauss, g_zero) u3_clipped = np.clip(u3[-1 ], -10 , 10 ) axes[2 ].plot(x3, u3_clipped, 'r-' ) except : pass axes[2 ].set_title(f'不稳定: r = {r_unstable:.2 f} ' ) axes[2 ].set_ylim([-1.5 , 1.5 ]) axes[2 ].grid(True , alpha=0.3 ) plt.suptitle('波动方程: CFL 条件的影响' ) plt.tight_layout() plt.savefig('cfl_condition.png' , dpi=150 , bbox_inches='tight' ) plt.show()
高级话题
谱方法
思想 :用全局基函数(如 Fourier 级数、 Chebyshev
多项式)展开解。
优点 :对于光滑解,可以达到指数收敛 (
spectral accuracy)。
缺点 :处理复杂边界困难;不适合不光滑解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 def heat_spectral (alpha, L, T, N, Nt, f ): """ 热方程的谱方法( Fourier) 周期边界条件 """ x = np.linspace(0 , L, N, endpoint=False ) dx = L / N dt = T / Nt u_hat = np.fft.fft(f(x)) k = np.fft.fftfreq(N, dx) * 2 * np.pi u_history = [f(x)] t_history = [0 ] for n in range (1 , Nt + 1 ): u_hat = u_hat * np.exp(-alpha * k**2 * dt) if n % (Nt // 20 ) == 0 : u = np.real(np.fft.ifft(u_hat)) u_history.append(u) t_history.append(n * dt) return x, u_history, t_history L, T = 2 *np.pi, 1 alpha = 0.1 f_smooth = lambda x: np.sin(x) + 0.5 *np.sin(3 *x) x_spec, u_spec, t_spec = heat_spectral(alpha, L, T, 32 , 100 , f_smooth) t_final = T u_exact = np.sin(x_spec)*np.exp(-alpha*t_final) + 0.5 *np.sin(3 *x_spec)*np.exp(-9 *alpha*t_final) plt.figure(figsize=(10 , 5 )) plt.plot(x_spec, u_spec[-1 ], 'b.-' , label='谱方法 (N=32)' ) plt.plot(x_spec, u_exact, 'r--' , label='精确解' ) plt.xlabel('x' ) plt.ylabel('u' ) plt.title('谱方法 vs 精确解' ) plt.legend() plt.grid(True , alpha=0.3 ) plt.savefig('spectral_method.png' , dpi=150 , bbox_inches='tight' ) plt.show() print (f"谱方法误差: {np.max (abs (u_spec[-1 ] - u_exact)):.2 e} " )
有限元方法简介
有限元方法( FEM) 是工程中最常用的 PDE
数值方法。
基本步骤 :
弱形式 :将 PDE 乘以测试函数并积分
离散化 :用有限维空间近似
组装 :构造刚度矩阵和载荷向量
求解 :解线性系统
优点 : - 处理复杂几何灵活 - 理论基础完善 -
可以处理非均匀网格
多重网格方法
问题 :迭代方法(如 Jacobi 、
Gauss-Seidel)对低频误差收敛慢。
多重网格思想 : 1. 在细网格上迭代几步(消除高频误差)
2. 将残差限制到粗网格 3. 在粗网格上求解(低频误差在粗网格上变成高频) 4.
将粗网格修正延拓回细网格 5. 重复
复杂度 : !(最优)
实际应用
热扩散:冷却鳍片
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 def cooling_fin (k, h_conv, T_base, T_ambient, L, A, P, Nx ): """ 冷却鳍片的稳态温度分布 -kA d ² T/dx ² + hP(T - T_ambient) = 0 T(0) = T_base, dT/dx(L) = 0 (绝热端) """ dx = L / Nx m2 = h_conv * P / (k * A) T = np.zeros(Nx + 1 ) A_mat = np.zeros((Nx + 1 , Nx + 1 )) b = np.zeros(Nx + 1 ) A_mat[0 , 0 ] = 1 b[0 ] = T_base for i in range (1 , Nx): A_mat[i, i-1 ] = 1 A_mat[i, i] = -2 - m2 * dx**2 A_mat[i, i+1 ] = 1 b[i] = -m2 * dx**2 * T_ambient A_mat[Nx, Nx] = 1 A_mat[Nx, Nx-1 ] = -1 b[Nx] = 0 T = np.linalg.solve(A_mat, b) x = np.linspace(0 , L, Nx + 1 ) m = np.sqrt(m2) T_exact = T_ambient + (T_base - T_ambient) * np.cosh(m*(L-x)) / np.cosh(m*L) return x, T, T_exact k = 200 h = 25 T_base = 100 T_ambient = 25 L = 0.1 width, thickness = 0.05 , 0.005 A = width * thickness P = 2 * (width + thickness) x, T_num, T_exact = cooling_fin(k, h, T_base, T_ambient, L, A, P, Nx=50 ) plt.figure(figsize=(10 , 5 )) plt.plot(x*100 , T_num, 'b.-' , label='数值解' ) plt.plot(x*100 , T_exact, 'r--' , label='精确解' ) plt.xlabel('位置 (cm)' ) plt.ylabel('温度 (° C)' ) plt.title('冷却鳍片的温度分布' ) plt.legend() plt.grid(True , alpha=0.3 ) plt.savefig('cooling_fin.png' , dpi=150 , bbox_inches='tight' ) plt.show()
二维稳态传热
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 def heat_2d_source (Nx, Ny, k, Q, T_boundary ): """ 二维稳态传热,带内热源 -k ∇² T = Q """ hx = 1.0 / Nx hy = 1.0 / Ny x = np.linspace(0 , 1 , Nx + 1 ) y = np.linspace(0 , 1 , Ny + 1 ) X, Y = np.meshgrid(x, y) Q_field = np.zeros((Ny + 1 , Nx + 1 )) center = (0.5 , 0.5 ) radius = 0.2 mask = (X - center[0 ])**2 + (Y - center[1 ])**2 < radius**2 Q_field[mask] = Q T = np.ones((Ny + 1 , Nx + 1 )) * T_boundary omega = 1.8 for _ in range (5000 ): T_old = T.copy() for j in range (1 , Ny): for i in range (1 , Nx): T_new = 0.25 * (T[j, i+1 ] + T[j, i-1 ] + T[j+1 , i] + T[j-1 , i] + hx**2 * Q_field[j, i] / k) T[j, i] = (1 - omega) * T[j, i] + omega * T_new if np.max (abs (T - T_old)) < 1e-6 : break return X, Y, T, Q_field k = 1 Q = 100 T_boundary = 0 X, Y, T, Q_field = heat_2d_source(50 , 50 , k, Q, T_boundary) fig, axes = plt.subplots(1 , 2 , figsize=(12 , 5 )) c1 = axes[0 ].contourf(X, Y, Q_field, levels=20 , cmap='Reds' ) plt.colorbar(c1, ax=axes[0 ]) axes[0 ].set_xlabel('x' ) axes[0 ].set_ylabel('y' ) axes[0 ].set_title('热源分布' ) axes[0 ].set_aspect('equal' ) c2 = axes[1 ].contourf(X, Y, T, levels=20 , cmap='hot' ) plt.colorbar(c2, ax=axes[1 ]) axes[1 ].set_xlabel('x' ) axes[1 ].set_ylabel('y' ) axes[1 ].set_title('稳态温度分布' ) axes[1 ].set_aspect('equal' ) plt.tight_layout() plt.savefig('heat_2d_source.png' , dpi=150 , bbox_inches='tight' ) plt.show()
波动方程:鼓膜振动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 def drum_vibration (Nx, Ny, c, T, Nt, f_initial ): """ 圆形鼓膜的振动(在正方形网格上近似) 使用掩模处理圆形边界 """ dx = 2.0 / Nx dy = 2.0 / Ny dt = T / Nt r = c * dt / dx x = np.linspace(-1 , 1 , Nx + 1 ) y = np.linspace(-1 , 1 , Ny + 1 ) X, Y = np.meshgrid(x, y) mask = X**2 + Y**2 <= 1 u_prev = f_initial(X, Y) * mask u_curr = u_prev.copy() u_history = [u_prev.copy()] for n in range (Nt): u_next = np.zeros_like(u_curr) for j in range (1 , Ny): for i in range (1 , Nx): if mask[j, i]: u_next[j, i] = (2 *u_curr[j, i] - u_prev[j, i] + r**2 * (u_curr[j, i+1 ] + u_curr[j, i-1 ] + u_curr[j+1 , i] + u_curr[j-1 , i] - 4 *u_curr[j, i])) u_next[~mask] = 0 u_prev = u_curr u_curr = u_next if n % (Nt // 8 ) == 0 : u_history.append(u_curr.copy()) return X, Y, u_history f_init = lambda X, Y: np.maximum(0 , 0.5 - 2 *np.sqrt(X**2 + Y**2 )) * (X**2 + Y**2 <= 1 ) c = 1 T = 3 X, Y, u_hist = drum_vibration(50 , 50 , c, T, 300 , f_init) fig, axes = plt.subplots(2 , 4 , figsize=(16 , 8 )) axes = axes.flatten() for i, u in enumerate (u_hist): ax = axes[i] c = ax.contourf(X, Y, u, levels=20 , cmap='seismic' , vmin=-0.3 , vmax=0.3 ) circle = plt.Circle((0 , 0 ), 1 , fill=False , color='k' , linewidth=2 ) ax.add_patch(circle) ax.set_aspect('equal' ) ax.set_title(f'帧 {i} ' ) ax.set_xlim([-1.2 , 1.2 ]) ax.set_ylim([-1.2 , 1.2 ]) plt.suptitle('圆形鼓膜的振动' , fontsize=14 ) plt.tight_layout() plt.savefig('drum_vibration.png' , dpi=150 , bbox_inches='tight' ) plt.show()
练习题
基础题
练习 1 :对方程 进行分类(双曲/抛物/椭圆)。
练习 2 :用分离变量法求解:$
$
练习 3 :实现热方程的
BTCS(向后时间中心空间)格式,验证它是无条件稳定的。
练习 4 :用有限差分法求解 Poisson 方程:
在单位正方形上,边界为 0 。精确解是什么?
练习 5 :验证 d'Alembert 公式对于 , , 。
中级题
练习 6 :实现波动方程的 Newmark-β方法,比较不同 , 参数的稳定性。
练习 7 :用
ADI(交替方向隐式)方法求解二维热方程。
练习 8 :对于不规则边界(如 L
形区域),实现有限差分法求解 Laplace 方程。
练习 9 :实现热方程的谱方法(用
FFT),比较与有限差分的精度。
练习 10 :研究波动方程在不同边界条件下的行为: -
固定端: - 自由端:
高级题
练习 11 :实现简单的多重网格方法( V-cycle)求解
Poisson 方程,验证 复杂度。
练习 12 :求解非线性热方程:
研究解的行为。
练习 13 :用有限差分法求解 Schr ö dinger 方程:
观察波包的演化。
练习 14 :实现 Burgers 方程的数值解:
观察激波的形成。
练习 15 :用边界元方法( BEM)求解外部 Laplace
问题,与有限差分比较。
总结
本章我们进入了偏微分方程的世界:
方程类型
物理模型
典型数值方法
稳定性条件
抛物型(热方程)
扩散、热传导
FTCS, CN
(FTCS)
双曲型(波动方程)
波传播、振动
中心差分
CFL:
椭圆型( Laplace)
稳态、平衡
迭代法、直接法
总是稳定
关键概念 :
分类 :判别式 决定方程类型
适定性 :存在性、唯一性、稳定性
稳定性分析 : Von Neumann 分析、 CFL 条件
数值方法 :有限差分、有限元、谱方法
实践建议 :
先从简单的显式格式开始
检查 CFL 条件
验证收敛阶数
对于刚性问题使用隐式方法
大规模问题考虑迭代方法或多重网格
这一章标志着我们从 ODE 到 PDE 的跨越。 PDE
是一个广阔的领域,我们只是触及了冰山一角。希望这个引论能为你进一步学习计算数学、流体力学、电磁学等领域打下基础!