别再被湍流模型搞晕了!用Python从零实现一个超简单的DNS求解器(附完整代码)
用Python从零实现极简DNS求解器让Navier-Stokes方程看得见摸得着当第一次听说直接数值模拟(DNS)时我盯着那组复杂的Navier-Stokes方程看了整整一个下午——那些偏微分符号像天书一样令人望而生畏。直到有一天我决定用Python把这些方程变成可以运行的代码才发现原来理解流体力学可以如此直观。本文将带你用不到200行Python代码构建一个能真实运行的二维方腔流DNS求解器让你亲手触摸流体运动的每一个细节。1. 准备工作理解极简版NS方程在开始编码前我们需要对Navier-Stokes方程做适当简化。考虑二维不可压缩流动控制方程可表示为# 连续性方程 ∂u/∂x ∂v/∂y 0 # x方向动量方程 ∂u/∂t u∂u/∂x v∂u/∂y -1/ρ ∂p/∂x ν(∂²u/∂x² ∂²u/∂y²) # y方向动量方程 ∂v/∂t u∂v/∂x v∂v/∂y -1/ρ ∂p/∂y ν(∂²v/∂x² ∂²v/∂y²)其中u,v分别是x,y方向速度分量p是压力ρ是流体密度(设为1)ν是运动粘度系数提示方腔流问题指在一个矩形区域内顶部壁面以恒定速度移动其余三面为静止壁面是验证CFD方法的经典案例。2. 数值方法有限差分法实现2.1 网格生成与变量排列我们采用交错网格(staggered grid)布置变量避免压力振荡问题import numpy as np # 参数设置 nx, ny 41, 41 # 网格数 dx 1.0 / (nx-1) dy 1.0 / (ny-1) # 初始化变量 (在交错网格位置) u np.zeros((ny, nx)) # x速度 (位于网格右面) v np.zeros((ny, nx)) # y速度 (位于网格上面) p np.zeros((ny, nx)) # 压力 (位于网格中心)2.2 压力泊松方程求解压力通过求解泊松方程获得我们使用松弛迭代法def solve_pressure(p, u, v, dx, dy, rho1.0, nit50): pn np.empty_like(p) for _ in range(nit): pn p.copy() p[1:-1,1:-1] ((dy**2*(pn[1:-1,2:]pn[1:-1,0:-2]) dx**2*(pn[2:,1:-1]pn[0:-2,1:-1])) - rho*dx**2*dy**2*((u[1:-1,2:]-u[1:-1,0:-2])/(2*dx) (v[2:,1:-1]-v[0:-2,1:-1])/(2*dy))) / (2*(dx**2dy**2)) # 边界条件 p[0,:] p[1,:] # dp/dy 0 at y0 p[-1,:] 0 # p0 at y1 p[:,0] p[:,1] # dp/dx 0 at x0 p[:,-1] p[:,-2] # dp/dx 0 at x1 return p2.3 时间推进算法采用分步法(time-splitting)进行时间积分def time_step(u, v, p, dx, dy, dt, nu0.1, rho1.0): # 临时速度场 un u.copy() vn v.copy() # 对流项计算 u[1:-1,1:-1] (un[1:-1,1:-1] - dt/dx*un[1:-1,1:-1]*(un[1:-1,1:-1]-un[1:-1,0:-2]) - dt/dy*vn[1:-1,1:-1]*(un[1:-1,1:-1]-un[0:-2,1:-1]) nu*(dt/dx**2*(un[1:-1,2:]-2*un[1:-1,1:-1]un[1:-1,0:-2]) dt/dy**2*(un[2:,1:-1]-2*un[1:-1,1:-1]un[0:-2,1:-1]))) v[1:-1,1:-1] (vn[1:-1,1:-1] - dt/dx*un[1:-1,1:-1]*(vn[1:-1,1:-1]-vn[1:-1,0:-2]) - dt/dy*vn[1:-1,1:-1]*(vn[1:-1,1:-1]-vn[0:-2,1:-1]) nu*(dt/dx**2*(vn[1:-1,2:]-2*vn[1:-1,1:-1]vn[1:-1,0:-2]) dt/dy**2*(vn[2:,1:-1]-2*vn[1:-1,1:-1]vn[0:-2,1:-1]))) # 压力修正 p solve_pressure(p, u, v, dx, dy, rho) # 速度修正 u[1:-1,1:-1] - dt/dx * (p[1:-1,2:]-p[1:-1,0:-2])/2 v[1:-1,1:-1] - dt/dy * (p[2:,1:-1]-p[0:-2,1:-1])/2 # 边界条件 u[0,:] 0 # 下壁面 u[-1,:] 1 # 上壁面移动 u[:,0] 0 # 左壁面 u[:,-1] 0 # 右壁面 v[0,:] 0 # 下壁面 v[-1,:] 0 # 上壁面 v[:,0] 0 # 左壁面 v[:,-1] 0 # 右壁面 return u, v, p3. 完整求解流程实现现在我们将所有部分组合起来形成完整的求解器import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D # 参数设置 nx, ny 41, 41 nt 500 # 时间步数 dt 0.001 nu 0.1 # 粘度系数 rho 1.0 dx 1.0/(nx-1) dy 1.0/(ny-1) # 初始化 u np.zeros((ny,nx)) v np.zeros((ny,nx)) p np.zeros((ny,nx)) # 主循环 for n in range(nt): u, v, p time_step(u, v, p, dx, dy, dt, nu, rho) # 每50步可视化 if n % 50 0: fig plt.figure(figsize(11,7)) plt.contourf(np.linspace(0,1,nx), np.linspace(0,1,ny), p, alpha0.5) plt.colorbar() plt.quiver(np.linspace(0,1,nx), np.linspace(0,1,ny), u, v) plt.xlabel(X) plt.ylabel(Y) plt.title(fTime Step {n}) plt.show()4. 结果分析与可视化运行上述代码后我们可以观察到方腔内逐渐形成的涡旋结构。为了更专业地分析结果添加流线可视化def plot_streamlines(u, v, p): x np.linspace(0, 1, nx) y np.linspace(0, 1, ny) X, Y np.meshgrid(x, y) plt.figure(figsize(11,7)) plt.streamplot(X, Y, u, v, density2, colork, linewidth1) plt.contourf(X, Y, p, alpha0.5) plt.colorbar() plt.xlabel(X) plt.ylabel(Y) plt.title(Streamlines and Pressure Contour) plt.show() plot_streamlines(u, v, p)典型输出应显示顶部移动壁面驱动的顺时针主涡左下角和右下角的次级涡压力在角落区域达到最大值5. 性能优化与扩展建议虽然我们的求解器已经可以工作但在实际应用中还需要考虑以下优化计算效率提升技巧使用Numba加速数值计算实现多核并行计算采用更高效的线性代数求解器物理模型扩展方向添加温度场模拟自然对流实现三维版本求解器引入自适应网格细化(AMR)技术# Numba加速示例 from numba import jit jit(nopythonTrue) def time_step_accelerated(u, v, p, dx, dy, dt, nu, rho): # 实现与之前相同的逻辑但会被编译为机器码 ...第一次运行这个求解器时我惊讶地发现原来那些抽象的数学符号真的能转化为肉眼可见的流体运动。当第一个涡旋在屏幕上出现时那种啊哈时刻的兴奋感至今难忘。建议你在修改参数(如粘度系数、网格分辨率)后重新运行观察流动模式的变化——这才是理解CFD最有趣的方式。