做风力机叶片或者机翼的气动弹性分析时,我经常要面对一个不算特别复杂、但也非常容易翻车的需求:翼型本身在绕某一点做俯仰振荡,与此同时尾缘还要叠加一定幅度的柔性变形。前者是典型的刚体运动,对应Fluent动网格里的刚体区域加CG运动;后者是典型的边界变形,对应Deforming区域加网格光顺。两个拆开做,教程一抓一大把,都不难;一旦要求它们同时作用在同一个翼型上,很多人就开始犯嘀咕了——动网格区域到底怎么设,UDF怎么写,同一个边界能不能又刚体又变形,网格会不会在某个时刻直接被挤爆……
这篇文章就围绕“Fluent动网格实现翼型俯仰振荡同时尾缘变形”这个组合,把从方案选型、网格准备、UDF编写,到求解器设置、常见问题排错的完整过程梳理一遍。案例本身选的是最简单的NACA0012翼型,低速来流,刚体俯仰加尾缘二次型变形,但里面的方法论可以直接迁移到风机叶片、涡轮叶片、直升机旋翼这类工程问题上。适合已经会跑基本Fluent仿真、想进阶动网格的工程师,也适合正在做气动弹性课题、被“刚体+柔性变形叠加”卡住的学生。
1. 同样都是动网格,为什么这个案例要单独拿出来讲
1.1 先想清楚:翼型在“动”的到底是什么
很多新手上来就写UDF,结果连自己要模拟的运动都没拆清楚。翼型俯仰振荡加尾缘变形,听起来是“一个运动”,数学上其实是两个独立位移场的叠加。
第一部分是刚体俯仰。整个翼型绕固定的弹性轴做正弦转动,典型形式是:
θ(t) = θ₀ + A·sin(2πft)
其中θ₀是平均攻角,A是俯仰振幅,f是俯仰频率。这个运动的特点是:翼型表面上所有网格节点的相对位置不改变,整体绕旋转中心转一个角度。在Fluent里最经典的实现方式是DEFINE_CG_MOTION,直接给定瞬时角速度即可。
第二部分是尾缘变形。这个更微妙——它不是整体转动,而是翼型尾缘附近的一段边界,相对它自身的初始位置做一个连续、光滑的偏移。比如尾缘点在某个时刻下移0.03倍弦长,然后离尾缘越近变形越大,越往上游变形越小,到某个起点位置变形恰好为零。这种运动不能用CG_MOTION表达,因为它破坏了“刚性”——区域内节点之间的相对距离变了。
所以本质上是:刚体位移场 + 柔性变形场,按时间同步叠加。搞清楚这一点,后面所有技术选择都顺理成章。
1.2 刚体俯仰和尾缘变形的“叠加”难点
难点恰恰出在Fluent动网格的框架上。在一个动网格计算里,一个边界区域通常只允许一种运动属性:
- 设成Rigid Body,整个区域做刚性平移/旋转,你无法单独挑出尾缘那一小块让它再多动一点;
- 设成Deforming,所有节点的位置都由用户自己控制,刚体运动也得自己写到UDF里。
也就是说,你不能把同一个壁面既设成刚体又设成Deforming。那怎么办?
直觉的解决办法是把翼型一分为二:前段到尾缘上游设为Rigid Body做俯仰,尾缘单独设为Deforming做变形。中间切一个interface。这个思路我最早也试过,结果是:交界面两侧的网格密度必须高度匹配,否则插值误差会直接污染壁面压力场;而且边界层在interface处被硬生生切断,动网格光顺在这种位置经常出现负体积。真实算下来,稳定性很差。
后来我换了一种思路:既然Deforming模式允许我直接控制节点位置,那为什么不在UDF里同时写上“刚性旋转位移 + 尾缘变形位移”,让Fluent的光顺算法去处理网格内部更新?这个思路最终稳定跑通了,也是下面这篇文章所有内容的核心。整个方案的代价是UDF要自己写,但换来的是对流场物理的更可控、对运动叠加的完全接管。
2. 方案选型:放弃CG Motion,把整个翼型交给GRID_MOTION
2.1 Fluent动网格三种方法,哪些能用哪些不能用
Fluent动网格大体有三类手段:Smoothing、Remeshing、Overset。选型前建议先把适用范围圈清楚。
| 方法 | 适用场景 | 本案例可行性 |
|---|---|---|
| Smoothing(光顺) | 边界位移中小幅度,网格拓扑不变 | 核心手段,配合Diffusion光顺 |
| Remeshing(局部重构) | 大位移、大转动,局部网格拓扑重建 | 辅助手段,大俯仰角时需要 |
| Overset(重叠网格) | 多体大位移、相互穿越 | 可用但杀鸡用牛刀,交界面插值额外耗成本 |
本案例的运动量级如果控制在工程界很常见的范围内——俯仰振幅±5°、尾缘变形3%~5%弦长——那么Smoothing加局部Remeshing足够。Diffusion-based光顺比弹簧光顺的鲁棒性好得多,尤其适合边界做旋转运动的情况,因为它会把变形量“均匀扩散”到全场,而不是像弹簧一样在局部积累。
真正需要纠结的并不是这三种方法的取舍,而是边界区域运动属性怎么设。我最终使用的是Deforming区域加DEFINE_GRID_MOTION,让一个UDF同时控制刚体旋转和尾缘变形,内部网格交给Diffusion光顺吸收变形。
2.2 本案例的动网格区域设置
在Fluent里按照下面的方式设置动网格区域,思路很清晰:
- 打开Dynamic Mesh,启用Smoothing和Remeshing;
- 在Dynamic Mesh Zones中,将翼型壁面设置为Deforming,Motion UDF指定为后面要写的airfoil_pitch_deform;
- 远场边界设置为Stationary,保持固定;
- 计算域内部不需要额外指定动态区域,光顺算法会自动把壁面的位移扩散到整个网格。
这里有个容易误解的点:Deforming区域指定的虽然是翼型壁面,但Fluent在调用UDF得到壁面节点位移后,会通过Smoothing算法把位移逐步传递到内部网格节点。所以UDF里只需要管壁面节点的位置,内部网格怎么动是求解器的事。
Smoothing参数里,我习惯把Diffusion参数调到1.5~2.0。Diffusion参数越大,壁面附近的位移衰减越快,网格更倾向于在远场吸收变形,这对保护边界层质量非常关键。如果案例的俯仰角较大,再把Remeshing里Minimum Length Scale和Maximum Length Scale设成当地网格尺寸的0.5倍和2倍,目标偏斜度0.7左右,局部网格坏了就让Fluent自动重构。
2.3 计算域、网格与边界层准备要点
网格是整个动网格算例的地基。翼型几何本身可以用NACA0012标准型值点生成,弦长取1m。计算域推荐C型或O型拓扑,远场半径取20~30倍弦长,太小会污染气动力系数,太大浪费网格量。
近壁网格按y+≈1来准备。粗略估算第一层网格高度可以直接用平板边界层公式:
uτ = U·√(Cf/2),Cf ≈ 0.0576·Re_x^(-1/5),y₁ = y+·μ/(ρ·uτ)
以Re=3×10⁶、来流50m/s估算,第一层高度大约在10⁻⁵m量级。边界层内网格增长率建议1.1~1.2,尾缘变形区域(x/c从0.7到1.0)的弦向网格间距建议不大于0.01c,否则变形后网格会被拉伸得很难看。
如果你用的是Fluent Meshing而非ICEM,有个操作细节容易踩:新建的Group在网格显示里不出现,多半不是网格没建上,而是显示对象的勾选没对上。在Graphics面板里新建Scene,把要显示的Zone或Group加进去并勾选,刷新一下就会显示。直接在Mesh Display的默认设置里找新Group,经常找不到。
网格生成后先检查质量:skewness最好小于0.7,minimum orthogonal quality大于0.2。动网格案例的网格质量余量要比定常算例留得更足,因为变形过程会让网格质量持续下降。
3. UDF逐行拆解:俯仰加尾缘变形的核心逻辑
3.1 为什么刚体旋转要用角度增量而不是绝对角度
这是整个UDF里最容易写错的地方,也最需要理解清楚。
Fluent的DEFINE_GRID_MOTION在每一个时间步开始前被调用,它执行的是在当前网格位置基础上施加一个位移增量,而不是直接把节点挪到某个绝对位置。如果你写成“把节点坐标设置为旋转后的绝对坐标”,那么第一个时间步网格挪到位,第二个时间步又基于被挪过的位置再设置一次,位移会反复累积,几个步之后网格就废了。
正确做法是计算当前步的角度增量:
dθ = θ(t+Δt) - θ(t)
然后把这个增量对应的节点位移加到当前坐标上。写成代码就是:
dx = xr·(cos(dθ) - 1) - yr·sin(dθ)
dy = xr·sin(dθ) + yr·(cos(dθ) - 1)
其中(xr, yr)是节点相对旋转中心的相对坐标。这个方式无论dθ多大都能保持刚性旋转的精确性,不会产生小角度近似误差。
还有一个好处:Fluent在一个物理时间步内可能因为网格重构问题多次调用动网格函数,但传入的time和dtime是同一个时间步的,这样dθ每次算出来都是0,不会重复叠加位移。用绝对角度写法就会出大问题。
3.2 DEFINE_GRID_MOTION完整代码与注释
下面给出我实际使用的完整UDF,代码层面做了参数化处理,方便调到自己的工况。
#include "udf.h" #include "dynamesh_tools.h" #include "math.h" #ifndef M_PI #define M_PI 3.14159265358979323846 #endif /* 翼型参数 */ #define CHORD 1.0 /* 弦长 */ #define PITCH_CX 0.25 /* 俯仰旋转中心x坐标,取1/4弦点 */ #define PITCH_CY 0.0 /* 旋转中心y坐标 */ /* 俯仰运动参数 */ #define PITCH_AMP 5.0 /* 俯仰振幅,单位:度 */ #define PITCH_FREQ 2.0 /* 俯仰频率,单位:Hz */ /* 尾缘变形参数 */ #define TAIL_X0 0.7 /* 尾缘变形起始位置,x/c */ #define TAIL_AMP 0.03 /* 尾缘最大变形量,单位:m(0.03倍弦长) */ #define TAIL_FREQ 4.0 /* 尾缘变形频率,单位:Hz */ DEFINE_GRID_MOTION(airfoil_pitch_deform, domain, dt, time, dtime) { Thread *tf = DT_THREAD(dt); face_t f; Node *v; int n; real theta, theta_pdt, dtheta; real cos_d, sin_d; real x, y, xr, yr, dxr, dyr; real dx, dy; real xr_norm, shape, dy_deform; /* 标记当前线程为网格变形线程,这一步不能少 */ SET_DEFORMING_THREAD_FLAG(THREAD_T0(tf)); /* 计算本时间步的角度增量 */ theta = PITCH_AMP * M_PI / 180.0 * sin(2.0 * M_PI * PITCH_FREQ * time); theta_pdt = PITCH_AMP * M_PI / 180.0 * sin(2.0 * M_PI * PITCH_FREQ * (time + dtime)); dtheta = theta_pdt - theta; cos_d = cos(dtheta); sin_d = sin(dtheta); begin_f_loop(f, tf) { f_node_loop(f, tf, n) { v = F_NODE(f, tf, n); if (NODE_POS_NEED_UPDATE(v)) { NODE_POS_UPDATED(v); x = NODE_X(v); y = NODE_Y(v); /* 1. 刚体俯仰旋转:绕旋转中心的位移增量 */ xr = x - PITCH_CX; yr = y - PITCH_CY; dxr = xr * (cos_d - 1.0) - yr * sin_d; dyr = xr * sin_d + yr * (cos_d - 1.0); dx = dxr; dy = dyr; /* 2. 尾缘变形:在尾缘局部叠加y向变形 */ if (x > TAIL_X0) { xr_norm = (x - TAIL_X0) / (CHORD - TAIL_X0); /* 二次形状函数:起始位置变形为0,尾缘处变形最大 */ shape = xr_norm * xr_norm; dy_deform = TAIL_AMP * shape * sin(2.0 * M_PI * TAIL_FREQ * time); dy += dy_deform; } NODE_X(v) += dx; NODE_Y(v) += dy; } } } end_f_loop(f, tf) }代码本身并不长,但有几个关键点需要重点解释。
首先,SET_DEFORMING_THREAD_FLAG(THREAD_T0(tf))是必须的,它告诉Fluent当前线程的网格节点需要更新位置。如果不设置,UDF虽然会被调用,但节点位置可能完全不变化,问题是“函数执行了,网格纹丝不动”,很多新手在这个坑里耗很久。
其次,NODE_POS_NEED_UPDATE和NODE_POS_UPDATED是配套使用的防重复更新机制。在一个时间步里,同一个节点可能被多个面共享,如果不做这个判断,节点会在这个循环里被反复更新,位移被叠加多次。这是写好动网格UDF的基本功。
第三,f_node_loop(f, tf, n)里的n是节点在当前面上的局部编号,每次循环拿到的v是一个指向节点的指针。Fluent允许一个节点被多个面共享,但因为有了NODE_POS_NEED_UPDATE的判断,共享节点只被更新一次。
编译时选择Compiled UDF,不能用Interpreted模式,因为代码里用了dynamesh_tools.h。编译成功后,在Dynamic Mesh Zones的Deforming区域的Motion UDF下拉列表里选择airfoil_pitch_deform。
3.3 尾缘变形的形状函数与变形范围设定
代码里的shape = xr_norm * xr_norm,也就是从变形起始点x/c=0.7到尾缘x/c=1.0采用二次函数过渡。这样保证了在起始位置变形量及其斜率都为0,避免在x=0.7处出现几何突变——如果变形函数在起始点不光滑,那个位置附近会产生很大的网格畸变,很容易直接负体积。
如果想要更光滑的过渡,可以用Hermite型形状函数:
shape = xr_norm³ · (6·xr_norm² - 15·xr_norm + 10)
这个函数在起始点和终点的一阶导都是0,变形轮廓更接近结构模态里的悬臂梁一阶弯曲振型。我实际对比过,这个函数对网格质量的保护明显好于简单二次型,代价只是多一行代码。
变形方向这里用了全局y方向,是因为NACA0012上下表面本身关于x轴对称,尾缘垂直方向变形可以近似用y向表达。如果要做有弯度的翼型,或变形方向沿局部表面法向,需要更精细的处理:在face循环里通过F_AREA(f, tf)取面的面积矢量,归一化得到法向,再把变形位移沿法向施加。这会让UDF更复杂,但物理意义更准确。
还有一个经验:变形量和频率不要一开始就拉满。建议先用俯仰UDF单独跑通,再叠加尾缘变形。叠加时先给一半振幅,确认网格没问题再逐步加大。
4. 求解设置与收敛控制:先稳后动是铁律
4.1 定常初场:为什么要先“冻住”翼型算稳定
动网格计算最忌讳的就是从均匀流场直接启动。如果初始化后就直接开瞬态、动网格,第一个时间步翼型开始转动,尾缘开始变形,流场会感受到一个剧烈的“冲击”,壁面附近必然产生非物理的压力波,轻则前几个周期升力系数乱跳,重则直接发散。
我的标准流程是:
- 先用定常求解器,在平均攻角位置把翼型固定住,算一个稳态流场;
- 待残差降到1×10⁻⁴以下,升阻力系数不再明显变化,再切换为瞬态;
- 在瞬态计算开始的同时打开动网格,让网格运动在一个相对真实的流场上逐步启动。
如果定常计算本身就很难收敛,先解决网格和湍流模型的问题,不要指望动网格能帮你“兜底”。动网格只会放大初场的不稳定,不会修正它。
实际操作中,很多老工程师还会在切瞬态后先固定翼型再跑几百步,让定常流场在瞬态格式下进一步稳定,然后才真正启动动网格。这个方法尤其适合雷诺数较高、边界层敏感的算例。
4.2 时间步长、约化频率与每步迭代次数
本案例的参数我建议这样取:
| 项目 | 设定值 | 说明 |
|---|---|---|
| 来流速度 | 50 m/s | 低速不可压缩 |
| 弦长 | 1 m | 参考长度 |
| 俯仰频率 | 2 Hz | 周期T=0.5s |
| 约化频率k | πfc/U≈0.126 | 较低约化频率,对时间步长要求不算苛刻 |
| 时间步长 | 0.0005~0.001 s | T/500到T/1000 |
| 每步内迭代 | 20~40次 | 视残差和升力系数稳定情况 |
| 网格最大位移 | 小于最小网格尺寸的1/3 | 重要的稳定性判据 |
约化频率k是无量纲的振荡频率,公式是k=πfc/U。它直接决定了流动非定常性的强弱。k=0.126属于低频大幅振荡,流场能较快响应翼型运动;如果k接近0.5甚至更高,时间步长必须成倍缩小。
时间步长的选择除了满足每个周期的采样点数,还要考虑网格位移。一个时间步内,翼型表面节点移动的距离不能超过当地最小网格尺寸的三分之一,否则Diffusion光顺很容易产生负体积。用这个判据反推,往往比单纯按周期取步长更有效。
并行计算方面,Fluent Launcher启动时在Parallel选项卡里设置的Processor进程数,一般不超过机器物理核心数。动网格加UDF的计算,我建议先跑串行或4核以下排查UDF和网格问题,确认稳定后再上大规模并行,否则日志文件里全是网格畸变报错,排查效率极低。
4.3 湍流模型与离散格式选型依据
低速翼型绕流,压力基求解器是自然选择。湍流模型我在这个案例里推荐两段式策略:先用Spalart-Allmaras模型把计算框架跑通,得到初步结果后,再用SST k-ω模型做正式计算。
理由很直接:SA模型只有单一湍流输运方程,数值鲁棒性好,收敛难度低,特别适合在动网格调通阶段使用;但SA对强逆压梯度下的流动分离预测偏粗糙。翼型俯仰振荡往往伴随动态失速,尾缘附近的流动会出现周期性分离与再附,这时候SST k-ω对分离点和再附的捕捉准确得多,代价是收敛难度上升,对网格质量更敏感。
离散格式的设置我有明确偏好:压力用Second Order,动量用Second Order Upwind,湍流量也用Second Order Upwind。瞬态格式先用First Order Implicit起跑50~100步,等流场结构稳定后切换到Bounded Second Order Implicit。压力速度耦合推荐Coupled,虽然每个迭代步成本高一些,但整体时间步内收敛更快,动网格工况下比SIMPLE族算法更稳。
我不建议在正式计算时开Solution Steering的自动模式,它为了鲁棒性会主动降低离散格式迎风阶数,掩盖网格运动带来的真实数值行为,结果就是“算完看着没发散,但曲线一塌糊涂”。手动控制格式,每步监控残差和升力系数,才是最可靠的。
5. 实战踩坑记录:负体积、发散和曲线导出
5.1 负体积:网格到底在哪里被“挤爆”了
跑动网格的人对这条报错一定不陌生:Negative Cell Volume。第一次遇到时基本是凌晨两三点,盯着Console里的报错代码一脸茫然。
根据我的经验,负体积最常出现在两个位置:一是旋转中心附近的边界层网格,二是尾缘变形起始点x/c=0.7附近。前者的机理是网格在旋转过程中被“压扁”,后者则是变形函数曲率突变导致的拉伸过度。
排查套路要系统:
- 看Console里报出的单元ID,在Display面板用Cell ID显示方式把这几个网格高亮出来,先确认“爆”在哪;
- 回看报错时间是物理时间还是某一步——是所有周期都会爆,还是只在某个俯仰角附近爆;
- 如果是所有周期都爆,优先减小时间步长;如果只在大角度时刻爆,说明光顺算法来不及吸收位移,需要加大Diffusion参数或调整变形形状函数;
- 确认负体积网格集中在极薄的边界层区域时,考虑减少每个时间步内壁面节点位移,而不是盲目加密网格——网格越密,允许的位移反而越小。
Debug时最好先关闭Remeshing,把问题全部归因到Smoothing身上。如果关闭重构后网格能跑通,再开启Remeshing,并仔细设置最小和最大尺寸标尺。
用尾缘变形做调试时,我会故意把变形量设成0,先跑一个32核、几百步的纯俯仰算例,确认光顺参数没有大问题,再逐步加变形量。这样能把变量隔离,快速定位问题源头。
5.2 升力曲线毛刺与网格更新频率的关系
动网格计算完成后,把升力系数时程导出来,经常能看到曲线底部有密密麻麻的小锯齿。试几个案例之后你会明白,这通常不是物理现象,而是数值噪声。
毛刺的来源往往是每个时间步开始时的网格位置更新,导致壁面附近的压力场瞬间被扰动。如果每步内迭代次数太少、压力场尚未充分恢复就进入下一步,锯齿就会逐级累积。
我的对策顺序是:
- 增加每步内迭代次数到30~50次,观察毛刺是否收敛;
- 如果仍然有锯齿,把时间步长减半,代价是计算量翻倍;
- 调大Diffusion参数,让网格位移在空间上更平滑,减小局部网格体积突变率;
- 检查Smoothing里的Spring Constant或Diffusion参数设置,不要同时开多个高刚度设置。
我自己的经验总结是:升力曲线小幅锯齿可以接受,但如果锯齿幅值超过升力平均值的1%,说明数值噪声已经大到会污染高阶统计量,必须处理后再继续计算。
5.3 Report Definition曲线导出与后处理经验
很多人在Fluent里定义了Report Definition,计算完了却不知道怎么把曲线数据导出来。操作其实很简单,在树形菜单的Results → Report Definitions里,找到你定义的升力系数或阻力系数,右键选择Export,会弹出一个保存CSV或文本文件的对话框,里面就是每个时间步的物理时间与对应量值。
如果想要计算过程中自动保存,就在创建Report Definition时勾选Write to File,并设置输出文件路径。计算结束后直接拿到完整时程数据,不用再手动点Export。
如果要用Origin或matplotlib画图,我习惯把第一个参数列设为t/T(无量纲周期数),第二列设为Cl,第三列为Cd,第四列为Cm。对比文献数据时注意力矩参考点,很多文献的Cm参考点是1/4弦线,Fluent里默认参考点要自己核对。
需要导出壁面压力分布时,用File → Export → Solution Data,在Location里选翼型壁面,Variable里勾选Pressure和Wall Shear,可以导出各个工位的压力系数分布。后续对所有时刻的压力分布做积分,还能还原出升力系数时程,和Report Definition的直出结果互相校验。
最后再多说一句,关于这个算例我前后调了很多个版本,最大的体会是:动网格发散十有八九不是求解器不行,而是运动写得不干净,要么是刚体旋转用了绝对位移导致累计误差,要么是变形函数在节点处不光滑,要么是Deforming区域和Smoothing参数配合不合适。尤其是“刚体加柔性变形”这种叠加运动,坐标系、增量写法、起始范围这三点想清楚,整个案例就成功了一半。如果后面你遇到类似问题,可以先用一个只有俯仰的UDF把网格鲁棒性测利索,再渐进式地把尾缘变形加进来。动网格这个东西,慢就是快,急不来。