第064篇:浸入边界法(Immersed Boundary Method, IBM)

摘要

浸入边界法(Immersed Boundary Method, IBM)是一种处理复杂几何形状边界条件的数值方法,它通过在笛卡尔网格上求解流场,同时利用力源项或插值技术来处理 immersed(浸入)在流体中的固体边界。本教程将系统介绍IBM的基本原理、数学模型、数值实现方法,以及在对流换热仿真中的应用。通过Python代码实现,我们将展示如何用IBM模拟圆柱绕流、颗粒沉降、柔性体变形等复杂流动与换热问题。IBM的优势在于避免了复杂的贴体网格生成,特别适合处理运动边界和大变形问题,在生物流体力学、多相流、流固耦合等领域有广泛应用。

关键词

浸入边界法,复杂几何,力源项,笛卡尔网格,流固耦合,运动边界,直接力法,插值法


在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1. 引言

1.1 复杂几何处理的挑战

在计算流体力学(CFD)中,处理复杂几何形状一直是一个核心挑战。传统的有限体积法(FVM)或有限元法(FEM)通常需要生成贴体网格(body-fitted mesh),即网格边界必须与物理边界完全贴合。这种方法存在以下问题:

网格生成困难:对于复杂几何(如生物组织、多孔介质、运动颗粒),生成高质量的贴体网格需要大量人工干预和专业知识。网格质量直接影响数值精度和收敛性。

网格更新成本高:当边界发生运动或变形时,需要重新生成网格或更新网格节点位置。对于大变形问题,网格可能产生严重畸变,导致计算失败。

计算效率受限:贴体网格通常需要非结构化网格,其数据结构和计算效率不如结构化笛卡尔网格。

1.2 浸入边界法的优势

浸入边界法(IBM)由Peskin于1972年提出,最初用于模拟心脏瓣膜的血液流动。其核心思想是:

笛卡尔网格求解:在简单的笛卡尔网格上求解Navier-Stokes方程,利用快速傅里叶变换(FFT)或多重网格等高效算法。

边界力处理:通过力源项或速度插值来处理固体边界的影响,将边界条件"浸入"到流场中。

主要优势

  • 无需生成贴体网格,大幅降低前处理工作量
  • 天然适合处理运动边界和大变形问题
  • 保持笛卡尔网格的高效计算特性
  • 易于实现流固耦合

1.3 IBM的分类

根据边界处理方式,IBM可分为两大类:

连续力法(Continuous Forcing Approach):在Navier-Stokes方程中添加体积力项来模拟边界效应。适用于柔性边界。

离散力法(Discrete Forcing Approach):直接在离散方程层面处理边界条件。包括:

  • 直接力法(Direct Forcing Method)
  • 幽灵点法(Ghost Cell Method)
  • 镜像点法(Mirror Point Method)

本教程将重点介绍直接力法和速度插值法,它们在对流换热仿真中最为常用。


2. 浸入边界法的数学模型

2.1 控制方程

考虑不可压缩流体,连续性方程和Navier-Stokes方程为:

∇⋅u=0\nabla \cdot \mathbf{u} = 0u=0

∂u∂t+(u⋅∇)u=−1ρ∇p+ν∇2u+f\frac{\partial \mathbf{u}}{\partial t} + (\mathbf{u} \cdot \nabla)\mathbf{u} = -\frac{1}{\rho}\nabla p + \nu \nabla^2 \mathbf{u} + \mathbf{f}tu+(u)u=ρ1p+ν2u+f

其中,f\mathbf{f}f是浸入边界产生的体积力(力源项),仅在边界附近非零。

对于能量方程:

∂T∂t+(u⋅∇)T=α∇2T+q\frac{\partial T}{\partial t} + (\mathbf{u} \cdot \nabla)T = \alpha \nabla^2 T + qtT+(u)T=α2T+q

其中,qqq是边界热源项。

2.2 直接力法

直接力法的核心思想是:在边界点处,通过添加适当的力使流体速度等于边界速度。

力的计算

在时间步 nnnn+1n+1n+1 之间,假设没有边界力,预测速度为 u∗\mathbf{u}^*u。为使边界点速度等于目标速度 Ub\mathbf{U}_bUb,需要施加力:

f=Ub−u∗Δt\mathbf{f} = \frac{\mathbf{U}_b - \mathbf{u}^*}{\Delta t}f=ΔtUbu

算法流程

  1. 求解无边界力的Navier-Stokes方程,得到预测速度 u∗\mathbf{u}^*u
  2. 在边界点处计算所需力 f\mathbf{f}f
  3. 将力投影到欧拉网格
  4. 修正速度场

2.3 速度插值法

速度插值法直接在边界点处强制满足无滑移条件,无需显式计算力。

线性插值

对于位于流体中的边界点,找到最近的网格点,通过线性插值使边界点速度等于目标速度:

ub=ufluid⋅(1−α)+ughost⋅α=Utarget\mathbf{u}_b = \mathbf{u}_{fluid} \cdot (1 - \alpha) + \mathbf{u}_{ghost} \cdot \alpha = \mathbf{U}_{target}ub=ufluid(1α)+ughostα=Utarget

其中,α\alphaα是距离权重系数。

幽灵点法

在固体内部设置幽灵点(Ghost Cell),通过外推计算幽灵点速度,使得边界插值满足无滑移条件。

2.4 狄拉克函数与力投影

在连续力法中,需要将拉格朗日边界点上的力投影到欧拉网格。这通过狄拉克函数实现:

f(x)=∫ΓF(X)δ(x−X)dX\mathbf{f}(\mathbf{x}) = \int_\Gamma \mathbf{F}(\mathbf{X}) \delta(\mathbf{x} - \mathbf{X}) d\mathbf{X}f(x)=ΓF(X)δ(xX)dX

其中,F\mathbf{F}F是边界上的力密度,δ\deltaδ是狄拉克函数。

常用的光滑狄拉克函数(Peskin的4点函数):

δh(r)={14h(1+cos⁡πr2h),∣r∣≤2h0,∣r∣>2h\delta_h(r) = \begin{cases} \frac{1}{4h}\left(1 + \cos\frac{\pi r}{2h}\right), & |r| \leq 2h \\ 0, & |r| > 2h \end{cases}δh(r)={4h1(1+cos2hπr),0,r2hr>2h

其中,hhh是网格间距。


3. 数值实现方法

3.1 算法流程

基于直接力法的IBM算法

初始化:设置流场、边界位置、边界速度

时间循环:
    1. 求解对流项:u* = u^n + Δt·(-u·∇u)
    2. 求解扩散项:u** = u* + Δt·ν·∇²u
    3. 识别边界附近的网格点
    4. 计算直接力:f = (U_b - u**)/Δt
    5. 施加体积力:u*** = u** + Δt·f
    6. 压力修正:求解泊松方程 ∇²p = (ρ/Δt)·∇·u***
    7. 更新速度:u^{n+1} = u*** - (Δt/ρ)·∇p
    8. 更新边界位置(如运动边界)

3.2 边界识别与标记

距离函数法

定义到边界的距离函数 ϕ(x)\phi(\mathbf{x})ϕ(x)

  • ϕ<0\phi < 0ϕ<0:固体内部
  • ϕ=0\phi = 0ϕ=0:边界上
  • ϕ>0\phi > 0ϕ>0:流体区域

对于圆形边界(圆心 (xc,yc)(x_c, y_c)(xc,yc),半径 RRR):

ϕ(x,y)=(x−xc)2+(y−yc)2−R\phi(x, y) = \sqrt{(x-x_c)^2 + (y-y_c)^2} - Rϕ(x,y)=(xxc)2+(yyc)2 R

标记策略

  • 流体点:ϕ>0\phi > 0ϕ>0
  • 固体点:ϕ<−ϵ\phi < -\epsilonϕ<ϵϵ\epsilonϵ为缓冲层厚度)
  • 边界点:−ϵ≤ϕ≤0-\epsilon \leq \phi \leq 0ϵϕ0

3.3 力的投影与插值

从拉格朗日点到欧拉网格

对于每个拉格朗日边界点 Xl\mathbf{X}_lXl,在其影响的欧拉网格点 xi,j\mathbf{x}_{i,j}xi,j 上施加力:

fi,j=∑lFl⋅δh(xi,j−Xl)⋅Δsl\mathbf{f}_{i,j} = \sum_l \mathbf{F}_l \cdot \delta_h(\mathbf{x}_{i,j} - \mathbf{X}_l) \cdot \Delta s_lfi,j=lFlδh(xi,jXl)Δsl

其中,Δsl\Delta s_lΔsl是边界点代表的弧长。

速度插值

从欧拉网格插值到拉格朗日点:

Ul=∑i,jui,j⋅δh(xi,j−Xl)⋅h2\mathbf{U}_l = \sum_{i,j} \mathbf{u}_{i,j} \cdot \delta_h(\mathbf{x}_{i,j} - \mathbf{X}_l) \cdot h^2Ul=i,jui,jδh(xi,jXl)h2


4. Python仿真实现

4.1 案例1:静止圆柱绕流

问题描述
模拟雷诺数 Re=100Re = 100Re=100 的圆柱绕流,圆柱直径 D=1D = 1D=1,计算域为 [−8D,16D]×[−8D,8D][-8D, 16D] \times [-8D, 8D][8D,16D]×[8D,8D]

IBM实现
使用直接力法,在圆柱表面施加力使流体速度为零。

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Circle
import matplotlib
matplotlib.use('Agg')

# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False

# 参数设置
D = 1.0  # 圆柱直径
Re = 100  # 雷诺数
U_inf = 1.0  # 来流速度
nu = U_inf * D / Re  # 运动粘度

# 计算域
Lx, Ly = 24*D, 16*D
Nx, Ny = 240, 160
dx, dy = Lx/Nx, Ly/Ny
dt = 0.001  # 时间步长

# 圆柱位置
xc, yc = 0.0, 0.0
R = D/2

# 创建网格
x = np.linspace(-8*D, 16*D, Nx)
y = np.linspace(-8*D, 8*D, Ny)
X, Y = np.meshgrid(x, y)

# 距离函数(到圆柱表面)
dist = np.sqrt((X - xc)**2 + (Y - yc)**2) - R

# 标记点
solid = dist < -0.5*dx  # 固体内部
fluid = dist > 0.5*dx   # 流体区域
boundary = (~solid) & (~fluid)  # 边界附近

# 初始化流场
u = np.ones((Ny, Nx)) * U_inf  # x方向速度
v = np.zeros((Ny, Nx))         # y方向速度
p = np.zeros((Ny, Nx))         # 压力

# 在固体内部设速度为零
u[solid] = 0
v[solid] = 0

# 时间推进
for n in range(10000):
    # 保存旧速度
    u_old = u.copy()
    v_old = v.copy()
    
    # 对流项(一阶迎风格式)
    du_dx = np.zeros_like(u)
    dv_dy = np.zeros_like(v)
    
    # x方向对流
    du_dx[:, 1:-1] = np.where(u[:, 1:-1] > 0,
                               (u[:, 1:-1] - u[:, :-2])/dx,
                               (u[:, 2:] - u[:, 1:-1])/dx)
    
    # y方向对流
    dv_dy[1:-1, :] = np.where(v[1:-1, :] > 0,
                               (v[1:-1, :] - v[:-2, :])/dy,
                               (v[2:, :] - v[1:-1, :])/dy)
    
    # 扩散项
    d2u = (u[:-2, 1:-1] - 2*u[1:-1, 1:-1] + u[2:, 1:-1])/dy**2 + \
          (u[1:-1, :-2] - 2*u[1:-1, 1:-1] + u[1:-1, 2:])/dx**2
    d2v = (v[:-2, 1:-1] - 2*v[1:-1, 1:-1] + v[2:, 1:-1])/dy**2 + \
          (v[1:-1, :-2] - 2*v[1:-1, 1:-1] + v[1:-1, 2:])/dx**2
    
    # 预测速度
    u[1:-1, 1:-1] = u[1:-1, 1:-1] + dt*(-u[1:-1, 1:-1]*du_dx[1:-1, 1:-1] + nu*d2u)
    v[1:-1, 1:-1] = v[1:-1, 1:-1] + dt*(-v[1:-1, 1:-1]*dv_dy[1:-1, 1:-1] + nu*d2v)
    
    # IBM:直接力法
    # 在边界点强制速度为零
    u[boundary] = 0
    v[boundary] = 0
    u[solid] = 0
    v[solid] = 0
    
    # 出口边界条件(对流边界)
    u[:, -1] = u[:, -2]
    v[:, -1] = v[:, -2]
    
    # 入口边界条件
    u[:, 0] = U_inf
    v[:, 0] = 0
    
    # 上下边界(自由滑移)
    u[0, :] = u[1, :]
    u[-1, :] = u[-2, :]
    v[0, :] = 0
    v[-1, :] = 0
    
    if n % 1000 == 0:
        print(f"迭代 {n}, 最大速度: {np.max(u):.4f}")

# 计算涡量
omega = np.zeros((Ny, Nx))
omega[1:-1, 1:-1] = (v[1:-1, 2:] - v[1:-1, :-2])/(2*dx) - \
                    (u[2:, 1:-1] - u[:-2, 1:-1])/(2*dy)

# 绘图
fig, axes = plt.subplots(2, 2, figsize=(14, 12))

# 速度云图
ax1 = axes[0, 0]
vel_mag = np.sqrt(u**2 + v**2)
im1 = ax1.contourf(X, Y, vel_mag, levels=20, cmap='jet')
plt.colorbar(im1, ax=ax1, label='|u|')
circle = Circle((xc, yc), R, color='black', fill=True)
ax1.add_patch(circle)
ax1.set_xlabel('x/D', fontsize=11)
ax1.set_ylabel('y/D', fontsize=11)
ax1.set_title('Velocity Magnitude', fontsize=12, fontweight='bold')
ax1.set_aspect('equal')

# 涡量云图
ax2 = axes[0, 1]
im2 = ax2.contourf(X, Y, omega, levels=20, cmap='RdBu_r')
plt.colorbar(im2, ax=ax2, label='Vorticity')
circle = Circle((xc, yc), R, color='black', fill=True)
ax2.add_patch(circle)
ax2.set_xlabel('x/D', fontsize=11)
ax2.set_ylabel('y/D', fontsize=11)
ax2.set_title('Vorticity Field', fontsize=12, fontweight='bold')
ax2.set_aspect('equal')

# 速度矢量图
ax3 = axes[1, 0]
skip = 8
ax3.quiver(X[::skip, ::skip], Y[::skip, ::skip], 
           u[::skip, ::skip], v[::skip, ::skip], scale=20)
circle = Circle((xc, yc), R, color='black', fill=True)
ax3.add_patch(circle)
ax3.set_xlabel('x/D', fontsize=11)
ax3.set_ylabel('y/D', fontsize=11)
ax3.set_title('Velocity Vectors', fontsize=12, fontweight='bold')
ax3.set_aspect('equal')

# 压力系数分布
ax4 = axes[1, 1]
theta = np.linspace(0, 2*np.pi, 100)
x_cyl = xc + R*np.cos(theta)
y_cyl = yc + R*np.sin(theta)
# 从网格插值获取表面压力
from scipy.interpolate import griddata
points = np.column_stack((X.flatten(), Y.flatten()))
p_interp = griddata(points, p.flatten(), np.column_stack((x_cyl, y_cyl)), method='linear')
Cp = (p_interp - p[0, 0]) / (0.5*U_inf**2)
ax4.plot(theta*180/np.pi, Cp, 'b-', linewidth=2)
ax4.set_xlabel('Angle (deg)', fontsize=11)
ax4.set_ylabel('Pressure Coefficient Cp', fontsize=11)
ax4.set_title('Surface Pressure Distribution', fontsize=12, fontweight='bold')
ax4.grid(True, alpha=0.3)
ax4.set_xlim(0, 360)

plt.tight_layout()
plt.savefig('case1_cylinder_ibm.png', dpi=150, bbox_inches='tight')
plt.close()
print("✓ 案例1完成:静止圆柱绕流")

4.2 案例2:运动颗粒沉降

问题描述
模拟圆形颗粒在流体中的沉降过程,考虑重力、浮力和流体阻力的作用。

物理模型
颗粒运动方程:
MpdVpdt=(ρp−ρf)Vpg+FdragM_p \frac{d\mathbf{V}_p}{dt} = (\rho_p - \rho_f)V_p \mathbf{g} + \mathbf{F}_{drag}MpdtdVp=(ρpρf)Vpg+Fdrag

其中,Fdrag\mathbf{F}_{drag}Fdrag是通过IBM计算的流体作用力。

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Circle
import matplotlib
matplotlib.use('Agg')

plt.rcParams['font.sans-serif'] = ['SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False

# 参数设置
D = 0.5  # 颗粒直径
R = D/2
rho_f = 1.0  # 流体密度
rho_p = 1.5  # 颗粒密度
nu = 0.01    # 运动粘度
g = 9.8      # 重力加速度

# 计算域
Lx, Ly = 4.0, 6.0
Nx, Ny = 80, 120
dx, dy = Lx/Nx, Ly/Ny
dt = 0.001

# 网格
x = np.linspace(0, Lx, Nx)
y = np.linspace(0, Ly, Ny)
X, Y = np.meshgrid(x, y)

# 初始化颗粒位置
xp, yp = Lx/2, Ly - 2*D
Vp = np.array([0.0, 0.0])  # 颗粒速度

# 初始化流场
u = np.zeros((Ny, Nx))
v = np.zeros((Ny, Nx))

# 存储颗粒轨迹
xp_history = [xp]
yp_history = [yp]
Vp_history = [np.linalg.norm(Vp)]

# 时间推进
for n in range(15000):
    # 计算距离函数
    dist = np.sqrt((X - xp)**2 + (Y - yp)**2) - R
    solid = dist < -0.5*dx
    boundary = (dist >= -0.5*dx) & (dist <= 0.5*dx)
    
    # 流体求解(简化,仅扩散项)
    u[1:-1, 1:-1] = u[1:-1, 1:-1] + dt*nu*((u[:-2, 1:-1] - 2*u[1:-1, 1:-1] + u[2:, 1:-1])/dy**2 + 
                                              (u[1:-1, :-2] - 2*u[1:-1, 1:-1] + u[1:-1, 2:])/dx**2)
    v[1:-1, 1:-1] = v[1:-1, 1:-1] + dt*nu*((v[:-2, 1:-1] - 2*v[1:-1, 1:-1] + v[2:, 1:-1])/dy**2 + 
                                              (v[1:-1, :-2] - 2*v[1:-1, 1:-1] + v[1:-1, 2:])/dx**2)
    
    # IBM:强制边界速度等于颗粒速度
    u[boundary] = Vp[0]
    v[boundary] = Vp[1]
    u[solid] = Vp[0]
    v[solid] = Vp[1]
    
    # 边界条件
    u[:, 0] = 0
    u[:, -1] = 0
    v[:, 0] = 0
    v[:, -1] = 0
    u[0, :] = 0
    u[-1, :] = 0
    v[0, :] = 0
    v[-1, :] = 0
    
    # 计算流体对颗粒的作用力(通过边界上的应力积分)
    # 简化为通过边界点速度计算阻力
    F_drag = np.array([0.0, 0.0])
    if np.any(boundary):
        # 计算边界上的速度梯度(简化)
        F_drag[1] = -6*np.pi*nu*R*(Vp[1] - np.mean(v[~solid]))  # Stokes阻力近似
    
    # 颗粒运动方程
    M_p = rho_p * np.pi * R**2  # 颗粒质量(2D)
    V_p = np.pi * R**2          # 颗粒体积
    F_gravity = (rho_p - rho_f) * V_p * g
    
    # 更新颗粒速度
    Vp[1] = Vp[1] + dt/M_p * (F_gravity + F_drag[1])
    
    # 更新颗粒位置
    xp = xp + dt*Vp[0]
    yp = yp + dt*Vp[1]
    
    # 底部碰撞
    if yp - R < 0:
        yp = R
        Vp[1] = -0.5*Vp[1]  # 弹性碰撞
    
    # 记录历史
    if n % 100 == 0:
        xp_history.append(xp)
        yp_history.append(yp)
        Vp_history.append(np.linalg.norm(Vp))
    
    if n % 2000 == 0:
        print(f"迭代 {n}, 颗粒位置: ({xp:.3f}, {yp:.3f}), 速度: {Vp[1]:.4f}")

# 绘图
fig, axes = plt.subplots(2, 2, figsize=(14, 12))

# 最终流场
ax1 = axes[0, 0]
vel_mag = np.sqrt(u**2 + v**2)
im1 = ax1.contourf(X, Y, vel_mag, levels=20, cmap='jet')
plt.colorbar(im1, ax=ax1, label='|u|')
circle = Circle((xp, yp), R, color='red', fill=True)
ax1.add_patch(circle)
ax1.set_xlabel('x', fontsize=11)
ax1.set_ylabel('y', fontsize=11)
ax1.set_title('Final Velocity Field', fontsize=12, fontweight='bold')
ax1.set_aspect('equal')

# 颗粒轨迹
ax2 = axes[0, 1]
ax2.plot(xp_history, yp_history, 'b-', linewidth=2)
ax2.scatter(xp_history[0], yp_history[0], color='green', s=100, label='Start', zorder=5)
ax2.scatter(xp_history[-1], yp_history[-1], color='red', s=100, label='End', zorder=5)
ax2.set_xlabel('x', fontsize=11)
ax2.set_ylabel('y', fontsize=11)
ax2.set_title('Particle Trajectory', fontsize=12, fontweight='bold')
ax2.legend(fontsize=10)
ax2.grid(True, alpha=0.3)
ax2.set_aspect('equal')

# 速度历史
ax3 = axes[1, 0]
ax3.plot(Vp_history, 'b-', linewidth=2)
ax3.set_xlabel('Time Step (/100)', fontsize=11)
ax3.set_ylabel('Particle Velocity', fontsize=11)
ax3.set_title('Velocity History', fontsize=12, fontweight='bold')
ax3.grid(True, alpha=0.3)

# 动画帧(多个时刻)
ax4 = axes[1, 1]
ax4.axis('off')
info_text = f"""
Sedimentation Simulation Results:

Physical Parameters:
- Particle diameter: {D}
- Fluid density: {rho_f}
- Particle density: {rho_p}
- Viscosity: {nu}
- Gravity: {g}

Numerical Setup:
- Domain: {Lx} x {Ly}
- Grid: {Nx} x {Ny}
- Time step: {dt}

Final State:
- Final position: ({xp:.3f}, {yp:.3f})
- Final velocity: {Vp[1]:.4f}
- Terminal velocity: {2*(rho_p-rho_f)*g*R**2/(9*nu):.4f} (Stokes)

IBM Features:
- Direct forcing method
- Moving boundary handling
- Fluid-particle coupling
"""
ax4.text(0.1, 0.5, info_text, transform=ax4.transAxes, fontsize=10,
         verticalalignment='center', fontfamily='monospace',
         bbox=dict(boxstyle='round', facecolor='lightblue', alpha=0.8))

plt.tight_layout()
plt.savefig('case2_sedimentation.png', dpi=150, bbox_inches='tight')
plt.close()
print("✓ 案例2完成:颗粒沉降")

4.3 案例3:柔性体变形

问题描述
模拟弹性薄膜在流体中的变形,展示IBM处理柔性边界的能力。

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon
import matplotlib
matplotlib.use('Agg')

plt.rcParams['font.sans-serif'] = ['SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False

# 参数设置
Lx, Ly = 4.0, 2.0
Nx, Ny = 160, 80
dx, dy = Lx/Nx, Ly/Ny
dt = 0.0005
nu = 0.001  # 粘度

# 网格
x = np.linspace(0, Lx, Nx)
y = np.linspace(0, Ly, Ny)
X, Y = np.meshgrid(x, y)

# 初始化柔性体(初始为直线)
N_lag = 50  # 拉格朗日点数
X_lag = np.linspace(1.0, 3.0, N_lag)
Y_lag = np.ones(N_lag) * Ly/2

# 柔性体属性
sigma = 100.0  # 弹性系数
ds = (X_lag[-1] - X_lag[0]) / (N_lag - 1)  # 拉格朗日点间距

# 初始化流场
u = np.ones((Ny, Nx)) * 0.5  # 均匀来流
v = np.zeros((Ny, Nx))

# 存储变形历史
lag_history = [(X_lag.copy(), Y_lag.copy())]

# 时间推进
for n in range(8000):
    # 计算拉格朗日点上的速度(从欧拉网格插值)
    U_lag = np.zeros(N_lag)
    V_lag = np.zeros(N_lag)
    
    for l in range(N_lag):
        # 找到最近的网格点
        i = int(X_lag[l] / dx)
        j = int(Y_lag[l] / dy)
        i = max(1, min(i, Nx-2))
        j = max(1, min(j, Ny-2))
        
        # 双线性插值
        wx = (X_lag[l] - x[i]) / dx
        wy = (Y_lag[l] - y[j]) / dy
        
        U_lag[l] = (1-wx)*(1-wy)*u[j, i] + wx*(1-wy)*u[j, i+1] + \
                   (1-wx)*wy*u[j+1, i] + wx*wy*u[j+1, i+1]
        V_lag[l] = (1-wx)*(1-wy)*v[j, i] + wx*(1-wy)*v[j, i+1] + \
                   (1-wx)*wy*v[j+1, i] + wx*wy*v[j+1, i+1]
    
    # 计算弹性力(张力)
    F_x = np.zeros(N_lag)
    F_y = np.zeros(N_lag)
    
    for l in range(1, N_lag-1):
        # 计算局部曲率(简化)
        F_x[l] = sigma * (X_lag[l-1] - 2*X_lag[l] + X_lag[l+1]) / ds**2
        F_y[l] = sigma * (Y_lag[l-1] - 2*Y_lag[l] + Y_lag[l+1]) / ds**2
    
    # 更新拉格朗日点位置
    X_lag = X_lag + dt * U_lag
    Y_lag = Y_lag + dt * V_lag
    
    # 施加弹性力到欧拉网格(简化处理)
    for l in range(N_lag):
        i = int(X_lag[l] / dx)
        j = int(Y_lag[l] / dy)
        if 1 <= i < Nx-1 and 1 <= j < Ny-1:
            u[j, i] = u[j, i] + dt * F_x[l] / dx / dy
            v[j, i] = v[j, i] + dt * F_y[l] / dx / dy
    
    # 流体求解(简化)
    u[1:-1, 1:-1] = u[1:-1, 1:-1] + dt*nu*((u[:-2, 1:-1] - 2*u[1:-1, 1:-1] + u[2:, 1:-1])/dy**2 + 
                                              (u[1:-1, :-2] - 2*u[1:-1, 1:-1] + u[1:-1, 2:])/dx**2)
    v[1:-1, 1:-1] = v[1:-1, 1:-1] + dt*nu*((v[:-2, 1:-1] - 2*v[1:-1, 1:-1] + v[2:, 1:-1])/dy**2 + 
                                              (v[1:-1, :-2] - 2*v[1:-1, 1:-1] + v[1:-1, 2:])/dx**2)
    
    # 边界条件
    u[:, 0] = 0.5
    u[:, -1] = u[:, -2]
    v[:, 0] = 0
    v[:, -1] = v[:, -2]
    u[0, :] = 0.5
    u[-1, :] = 0.5
    v[0, :] = 0
    v[-1, :] = 0
    
    # 记录历史
    if n % 400 == 0:
        lag_history.append((X_lag.copy(), Y_lag.copy()))
    
    if n % 2000 == 0:
        print(f"迭代 {n}, 最大位移: {np.max(np.abs(Y_lag - Ly/2)):.4f}")

# 绘图
fig, axes = plt.subplots(2, 2, figsize=(14, 12))

# 最终流场
ax1 = axes[0, 0]
vel_mag = np.sqrt(u**2 + v**2)
im1 = ax1.contourf(X, Y, vel_mag, levels=20, cmap='jet')
plt.colorbar(im1, ax=ax1, label='|u|')
ax1.plot(X_lag, Y_lag, 'r-', linewidth=3, label='Flexible Body')
ax1.scatter(X_lag, Y_lag, c='red', s=20)
ax1.set_xlabel('x', fontsize=11)
ax1.set_ylabel('y', fontsize=11)
ax1.set_title('Final Flow Field with Deformed Body', fontsize=12, fontweight='bold')
ax1.legend(fontsize=10)

# 变形历史
ax2 = axes[0, 1]
for i, (X_hist, Y_hist) in enumerate(lag_history[::5]):
    alpha = 0.3 + 0.7 * i / len(lag_history[::5])
    ax2.plot(X_hist, Y_hist, 'b-', alpha=alpha, linewidth=1.5)
ax2.plot(lag_history[0][0], lag_history[0][1], 'g-', linewidth=3, label='Initial')
ax2.plot(X_lag, Y_lag, 'r-', linewidth=3, label='Final')
ax2.set_xlabel('x', fontsize=11)
ax2.set_ylabel('y', fontsize=11)
ax2.set_title('Deformation History', fontsize=12, fontweight='bold')
ax2.legend(fontsize=10)
ax2.grid(True, alpha=0.3)

# 位移分布
ax3 = axes[1, 0]
displacement = Y_lag - Ly/2
ax3.plot(X_lag, displacement, 'b-', linewidth=2)
ax3.axhline(y=0, color='k', linestyle='--', alpha=0.3)
ax3.set_xlabel('x', fontsize=11)
ax3.set_ylabel('Vertical Displacement', fontsize=11)
ax3.set_title('Final Displacement Distribution', fontsize=12, fontweight='bold')
ax3.grid(True, alpha=0.3)

# 信息
ax4 = axes[1, 1]
ax4.axis('off')
info_text = f"""
Flexible Body Simulation Results:

Physical Parameters:
- Domain: {Lx} x {Ly}
- Viscosity: {nu}
- Elastic coefficient: {sigma}
- Inflow velocity: 0.5

Numerical Setup:
- Grid: {Nx} x {Ny}
- Lagrangian points: {N_lag}
- Time step: {dt}

Results:
- Max displacement: {np.max(np.abs(displacement)):.4f}
- Final shape: Deformed by flow

IBM Method:
- Continuous forcing approach
- Elastic tension model
- Two-way coupling
"""
ax4.text(0.1, 0.5, info_text, transform=ax4.transAxes, fontsize=10,
         verticalalignment='center', fontfamily='monospace',
         bbox=dict(boxstyle='round', facecolor='lightyellow', alpha=0.8))

plt.tight_layout()
plt.savefig('case3_flexible_body.png', dpi=150, bbox_inches='tight')
plt.close()
print("✓ 案例3完成:柔性体变形")

4.4 案例4:多颗粒相互作用

问题描述
模拟多个颗粒在流体中的运动和相互作用,展示IBM处理多体问题的能力。

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Circle
import matplotlib
matplotlib.use('Agg')

plt.rcParams['font.sans-serif'] = ['SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False

# 参数设置
Lx, Ly = 3.0, 4.0
Nx, Ny = 60, 80
dx, dy = Lx/Nx, Ly/Ny
dt = 0.001
nu = 0.02
rho_f = 1.0
rho_p = 2.0
g = 9.8

# 网格
x = np.linspace(0, Lx, Nx)
y = np.linspace(0, Ly, Ny)
X, Y = np.meshgrid(x, y)

# 初始化多个颗粒
N_particles = 4
R = 0.15  # 颗粒半径
particles = []
for i in range(N_particles):
    particles.append({
        'x': Lx/2 + (i - 1.5) * 0.5,
        'y': Ly - 0.5 - i * 0.3,
        'vx': 0.0,
        'vy': 0.0,
        'R': R
    })

# 初始化流场
u = np.zeros((Ny, Nx))
v = np.zeros((Ny, Nx))

# 存储历史
history = [[] for _ in range(N_particles)]
for i, p in enumerate(particles):
    history[i].append((p['x'], p['y']))

# 时间推进
for n in range(10000):
    # 标记所有固体区域
    solid = np.zeros((Ny, Nx), dtype=bool)
    boundary = np.zeros((Ny, Nx), dtype=bool)
    
    for p in particles:
        dist = np.sqrt((X - p['x'])**2 + (Y - p['y'])**2) - p['R']
        solid |= dist < -0.5*dx
        boundary |= (dist >= -0.5*dx) & (dist <= 0.5*dx)
    
    # 流体求解(简化)
    u[1:-1, 1:-1] = u[1:-1, 1:-1] + dt*nu*((u[:-2, 1:-1] - 2*u[1:-1, 1:-1] + u[2:, 1:-1])/dy**2 + 
                                              (u[1:-1, :-2] - 2*u[1:-1, 1:-1] + u[1:-1, 2:])/dx**2)
    v[1:-1, 1:-1] = v[1:-1, 1:-1] + dt*nu*((v[:-2, 1:-1] - 2*v[1:-1, 1:-1] + v[2:, 1:-1])/dy**2 + 
                                              (v[1:-1, :-2] - 2*v[1:-1, 1:-1] + v[1:-1, 2:])/dx**2)
    
    # IBM:施加颗粒速度
    for p in particles:
        dist = np.sqrt((X - p['x'])**2 + (Y - p['y'])**2) - p['R']
        p_solid = dist < -0.5*dx
        p_boundary = (dist >= -0.5*dx) & (dist <= 0.5*dx)
        u[p_boundary] = p['vx']
        v[p_boundary] = p['vy']
        u[p_solid] = p['vx']
        v[p_solid] = p['vy']
    
    # 边界条件
    u[:, 0] = 0
    u[:, -1] = 0
    v[:, 0] = 0
    v[:, -1] = 0
    u[0, :] = 0
    u[-1, :] = 0
    v[0, :] = 0
    v[-1, :] = 0
    
    # 更新颗粒运动
    for i, p in enumerate(particles):
        # 计算受力(简化)
        M_p = rho_p * np.pi * p['R']**2
        V_p = np.pi * p['R']**2
        F_gravity = (rho_p - rho_f) * V_p * g
        
        # 简单的阻力模型
        F_drag_y = -6*np.pi*nu*p['R']*p['vy']
        F_drag_x = -6*np.pi*nu*p['R']*p['vx']
        
        # 简单的颗粒间碰撞检测
        for j, p2 in enumerate(particles):
            if i != j:
                dist_p = np.sqrt((p['x'] - p2['x'])**2 + (p['y'] - p2['y'])**2)
                if dist_p < p['R'] + p2['R']:
                    # 简单的排斥力
                    nx = (p['x'] - p2['x']) / dist_p
                    ny = (p['y'] - p2['y']) / dist_p
                    F_rep = 10.0 * (p['R'] + p2['R'] - dist_p)
                    F_drag_x += F_rep * nx
                    F_drag_y += F_rep * ny
        
        # 更新速度
        p['vx'] = p['vx'] + dt/M_p * F_drag_x
        p['vy'] = p['vy'] + dt/M_p * (F_gravity + F_drag_y)
        
        # 更新位置
        p['x'] = p['x'] + dt*p['vx']
        p['y'] = p['y'] + dt*p['vy']
        
        # 边界碰撞
        if p['x'] - p['R'] < 0:
            p['x'] = p['R']
            p['vx'] = -0.5*p['vx']
        if p['x'] + p['R'] > Lx:
            p['x'] = Lx - p['R']
            p['vx'] = -0.5*p['vx']
        if p['y'] - p['R'] < 0:
            p['y'] = p['R']
            p['vy'] = -0.5*p['vy']
        
        # 记录历史
        if n % 50 == 0:
            history[i].append((p['x'], p['y']))
    
    if n % 2000 == 0:
        print(f"迭代 {n}, 颗粒高度: {[f'{p[\"y\"]:.2f}' for p in particles]}")

# 绘图
fig, axes = plt.subplots(2, 2, figsize=(14, 12))

# 最终流场和颗粒
ax1 = axes[0, 0]
vel_mag = np.sqrt(u**2 + v**2)
im1 = ax1.contourf(X, Y, vel_mag, levels=20, cmap='jet')
plt.colorbar(im1, ax=ax1, label='|u|')
for p in particles:
    circle = Circle((p['x'], p['y']), p['R'], color='red', fill=True)
    ax1.add_patch(circle)
ax1.set_xlabel('x', fontsize=11)
ax1.set_ylabel('y', fontsize=11)
ax1.set_title('Multi-Particle Flow Field', fontsize=12, fontweight='bold')
ax1.set_aspect('equal')

# 颗粒轨迹
ax2 = axes[0, 1]
colors = ['red', 'blue', 'green', 'purple']
for i, (p, h) in enumerate(zip(particles, history)):
    traj = np.array(h)
    ax2.plot(traj[:, 0], traj[:, 1], color=colors[i], linewidth=2, label=f'Particle {i+1}')
    ax2.scatter(traj[0, 0], traj[0, 1], color=colors[i], s=100, marker='o', zorder=5)
    ax2.scatter(traj[-1, 0], traj[-1, 1], color=colors[i], s=100, marker='s', zorder=5)
ax2.set_xlabel('x', fontsize=11)
ax2.set_ylabel('y', fontsize=11)
ax2.set_title('Particle Trajectories', fontsize=12, fontweight='bold')
ax2.legend(fontsize=10)
ax2.grid(True, alpha=0.3)
ax2.set_aspect('equal')

# 高度历史
ax3 = axes[1, 0]
for i, h in enumerate(history):
    traj = np.array(h)
    ax3.plot(traj[:, 1], color=colors[i], linewidth=2, label=f'Particle {i+1}')
ax3.set_xlabel('Time Step (/50)', fontsize=11)
ax3.set_ylabel('Height y', fontsize=11)
ax3.set_title('Height History', fontsize=12, fontweight='bold')
ax3.legend(fontsize=10)
ax3.grid(True, alpha=0.3)

# 信息
ax4 = axes[1, 1]
ax4.axis('off')
info_text = f"""
Multi-Particle Simulation Results:

Physical Parameters:
- Domain: {Lx} x {Ly}
- Particles: {N_particles}
- Particle radius: {R}
- Fluid density: {rho_f}
- Particle density: {rho_p}
- Viscosity: {nu}

Numerical Setup:
- Grid: {Nx} x {Ny}
- Time step: {dt}
- Iterations: 10000

IBM Features:
- Multiple moving boundaries
- Particle-particle interaction
- Fluid-particle coupling
- Collision handling

Final Positions:
"""
for i, p in enumerate(particles):
    info_text += f"  Particle {i+1}: ({p['x']:.3f}, {p['y']:.3f})\n"

ax4.text(0.1, 0.5, info_text, transform=ax4.transAxes, fontsize=10,
         verticalalignment='center', fontfamily='monospace',
         bbox=dict(boxstyle='round', facecolor='lightgreen', alpha=0.8))

plt.tight_layout()
plt.savefig('case4_multi_particle.png', dpi=150, bbox_inches='tight')
plt.close()
print("✓ 案例4完成:多颗粒相互作用")

4.5 案例5:带换热的IBM

问题描述
模拟圆柱绕流中的对流换热问题,圆柱表面保持恒定温度,流体被加热。

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Circle
import matplotlib
matplotlib.use('Agg')

plt.rcParams['font.sans-serif'] = ['SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False

# 参数设置
D = 1.0
R = D/2
Re = 100
Pr = 0.7
U_inf = 1.0
nu = U_inf * D / Re
alpha = nu / Pr
T_cylinder = 1.0  # 圆柱温度
T_inlet = 0.0     # 入口温度

# 计算域
Lx, Ly = 16*D, 10*D
Nx, Ny = 160, 100
dx, dy = Lx/Nx, Ly/Ny
dt = 0.001

# 网格
x = np.linspace(-4*D, 12*D, Nx)
y = np.linspace(-5*D, 5*D, Ny)
X, Y = np.meshgrid(x, y)

# 圆柱位置
xc, yc = 0.0, 0.0

# 距离函数
dist = np.sqrt((X - xc)**2 + (Y - yc)**2) - R
solid = dist < -0.5*dx
fluid = dist > 0.5*dx
boundary = (~solid) & (~fluid)

# 初始化
u = np.ones((Ny, Nx)) * U_inf
v = np.zeros((Ny, Nx))
T = np.ones((Ny, Nx)) * T_inlet

# 时间推进
for n in range(8000):
    # 流体求解(简化)
    u_old = u.copy()
    v_old = v.copy()
    
    # 扩散项
    d2u = (u[:-2, 1:-1] - 2*u[1:-1, 1:-1] + u[2:, 1:-1])/dy**2 + \
          (u[1:-1, :-2] - 2*u[1:-1, 1:-1] + u[1:-1, 2:])/dx**2
    d2v = (v[:-2, 1:-1] - 2*v[1:-1, 1:-1] + v[2:, 1:-1])/dy**2 + \
          (v[1:-1, :-2] - 2*v[1:-1, 1:-1] + v[1:-1, 2:])/dx**2
    
    u[1:-1, 1:-1] = u[1:-1, 1:-1] + dt*nu*d2u
    v[1:-1, 1:-1] = v[1:-1, 1:-1] + dt*nu*d2v
    
    # IBM速度边界
    u[boundary] = 0
    v[boundary] = 0
    u[solid] = 0
    v[solid] = 0
    
    # 温度求解
    d2T = (T[:-2, 1:-1] - 2*T[1:-1, 1:-1] + T[2:, 1:-1])/dy**2 + \
          (T[1:-1, :-2] - 2*T[1:-1, 1:-1] + T[1:-1, 2:])/dx**2
    
    # 对流项(简化)
    T[1:-1, 1:-1] = T[1:-1, 1:-1] + dt*(alpha*d2T - u[1:-1, 1:-1]*(T[1:-1, 2:] - T[1:-1, :-2])/(2*dx))
    
    # IBM温度边界
    T[boundary] = T_cylinder
    T[solid] = T_cylinder
    
    # 边界条件
    u[:, 0] = U_inf
    v[:, 0] = 0
    T[:, 0] = T_inlet
    u[:, -1] = u[:, -2]
    v[:, -1] = v[:, -2]
    T[:, -1] = T[:, -2]
    u[0, :] = u[1, :]
    u[-1, :] = u[-2, :]
    v[0, :] = 0
    v[-1, :] = 0
    T[0, :] = T[1, :]
    T[-1, :] = T[-2, :]
    
    if n % 1000 == 0:
        print(f"迭代 {n}, 最大温度: {np.max(T):.4f}")

# 计算努塞尔数
# 通过圆柱表面的温度梯度计算热流
q_wall = []
theta = np.linspace(0, 2*np.pi, 100)
for th in theta:
    x_surf = xc + (R + dx)*np.cos(th)
    y_surf = yc + (R + dx)*np.sin(th)
    i = int((x_surf - x[0])/dx)
    j = int((y_surf - y[0])/dy)
    i = max(1, min(i, Nx-2))
    j = max(1, min(j, Ny-2))
    
    # 计算法向温度梯度
    dx_norm = (T[j, min(i+1, Nx-1)] - T[j, max(i-1, 0)])/(2*dx)
    dy_norm = (T[min(j+1, Ny-1), i] - T[max(j-1, 0), i])/(2*dy)
    q_wall.append(dx_norm*np.cos(th) + dy_norm*np.sin(th))

q_wall = np.array(q_wall)
Nu_local = q_wall * D / (T_cylinder - T_inlet)
Nu_avg = np.mean(Nu_local)

# 绘图
fig, axes = plt.subplots(2, 2, figsize=(14, 12))

# 温度云图
ax1 = axes[0, 0]
im1 = ax1.contourf(X, Y, T, levels=20, cmap='hot')
plt.colorbar(im1, ax=ax1, label='Temperature')
circle = Circle((xc, yc), R, color='blue', fill=True)
ax1.add_patch(circle)
ax1.set_xlabel('x/D', fontsize=11)
ax1.set_ylabel('y/D', fontsize=11)
ax1.set_title('Temperature Field', fontsize=12, fontweight='bold')
ax1.set_aspect('equal')

# 速度云图
ax2 = axes[0, 1]
vel_mag = np.sqrt(u**2 + v**2)
im2 = ax2.contourf(X, Y, vel_mag, levels=20, cmap='jet')
plt.colorbar(im2, ax=ax2, label='|u|')
circle = Circle((xc, yc), R, color='black', fill=True)
ax2.add_patch(circle)
ax2.set_xlabel('x/D', fontsize=11)
ax2.set_ylabel('y/D', fontsize=11)
ax2.set_title('Velocity Field', fontsize=12, fontweight='bold')
ax2.set_aspect('equal')

# 局部努塞尔数分布
ax3 = axes[1, 0]
ax3.plot(theta*180/np.pi, Nu_local, 'b-', linewidth=2)
ax3.set_xlabel('Angle (deg)', fontsize=11)
ax3.set_ylabel('Local Nusselt Number', fontsize=11)
ax3.set_title(f'Local Nu Distribution (Avg Nu = {Nu_avg:.2f})', fontsize=12, fontweight='bold')
ax3.grid(True, alpha=0.3)
ax3.set_xlim(0, 360)

# 等温线
ax4 = axes[1, 1]
contours = ax4.contour(X, Y, T, levels=15, colors='black', linewidths=0.5)
ax4.clabel(contours, inline=True, fontsize=8)
im4 = ax4.contourf(X, Y, T, levels=15, cmap='hot', alpha=0.7)
plt.colorbar(im4, ax=ax4, label='Temperature')
circle = Circle((xc, yc), R, color='blue', fill=True)
ax4.add_patch(circle)
ax4.set_xlabel('x/D', fontsize=11)
ax4.set_ylabel('y/D', fontsize=11)
ax4.set_title('Isotherms', fontsize=12, fontweight='bold')
ax4.set_aspect('equal')

plt.tight_layout()
plt.savefig('case5_heat_transfer_ibm.png', dpi=150, bbox_inches='tight')
plt.close()
print(f"✓ 案例5完成:带换热的IBM,平均努塞尔数: {Nu_avg:.2f}")

5. IBM的扩展与应用

5.1 高阶IBM方法

幽灵点法(Ghost Cell Method)
在固体内部设置幽灵点,通过镜像或外推计算幽灵点速度,使得边界插值精确满足无滑移条件。这种方法比直接力法精度更高。

镜像点法(Mirror Point Method)
对于每个边界点,在流场中找到其镜像点,通过镜像点的速度外推计算边界速度。

5.2 IBM与LBM的结合

IBM与格子玻尔兹曼方法(LBM)天然契合,因为LBM使用笛卡尔网格,易于实现IBM的力源项。

LBM-IBM算法

  1. 执行LBM的碰撞步
  2. 计算边界力并修正分布函数
  3. 执行LBM的迁移步
  4. 更新边界位置

5.3 三维IBM

三维IBM的原理与二维相同,但计算量大幅增加。主要挑战包括:

  • 距离函数计算更复杂
  • 力投影需要3D狄拉克函数
  • 可视化更加困难

5.4 工程应用

生物流体力学

  • 心脏瓣膜开合
  • 血液细胞变形
  • 精子游动

多相流

  • 气泡上升
  • 颗粒沉降
  • 液滴撞击

流固耦合

  • 柔性管道流动
  • 旗帜飘动
  • 降落伞展开

6. IBM的优缺点与展望

6.1 优点

  1. 网格生成简单:使用笛卡尔网格,无需复杂的前处理
  2. 适合运动边界:天然处理边界运动和大变形
  3. 计算效率高:结构化网格利于并行计算
  4. 易于实现:算法相对简单,编程复杂度低

6.2 缺点

  1. 边界精度有限:边界附近精度通常为1阶
  2. 小尺度流动困难:难以精确捕捉边界层细节
  3. 质量守恒挑战:某些IBM变体存在质量守恒问题
  4. 时间步长限制:边界运动可能需要较小的时间步长

6.3 发展趋势

自适应IBM
结合自适应网格细化(AMR),在边界附近加密网格以提高精度。

高阶IBM
发展高阶幽灵点法和插值方法,提高边界精度至2阶或更高。

机器学习辅助IBM
利用神经网络预测边界力或优化IBM参数。


Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐