做翼型优化的人,可能都有过这种体验:单点优化做多了,总会被一堆实际问题逼到墙角——一个翼型要同时兼顾高升力、低阻力、力矩可控,往往压下一个目标,另一个目标马上反弹。我之前在做某型低速无人机的翼型选型时,就为这种权衡关系折腾了很长时间,直到把一套基于非主导排序遗传算法(也就是NSGA-II,英文全称Nondominated Sorting Genetic Algorithm II,也有人叫它非支配排序遗传算法)的翼型形状优化流程跑通,配合Matlab代码实现和气动分析工具联调,才算真正把多目标取舍这件事理顺。这篇博文就是我那次项目从建模、编码、跑批到整理报告的全过程记录,偏工程实操向。想了解翼型优化怎么落地、或者打算把NSGA-II用于工程设计的朋友,应该能从里面找到可以直接照搬的思路。
1. 项目整体思路:翼型形状优化到底在解什么题
1.1 不再把多目标硬压成单目标
很多初做翼型优化的同学,习惯性先把多个目标加权成一个单目标,比如把升阻比和力矩系数乘以权重后取个总和。这在一些简单场景下有用,但放到实际工程里非常尴尬。原因是翼型性能目标之间普遍存在冲突:想要更大的最大升力系数,往往会伴随阻力上升;想要更厚的翼型保证结构强度,厚度增加带来的压差阻力又会让巡航效率变差。权重怎么给,本质上是个价值判断,而不是技术问题,你很难说清楚“升阻比权重0.7、力矩权重0.3”到底依据什么。
采用非主导排序遗传算法之后,思路就不一样了。它一次性保留一整套互不支配的解,叫做Pareto解集。简单解释一下支配关系:如果候选解A的所有目标都不比候选解B差,并且至少有一个目标严格优于B,就认为A支配了B。反过来,如果A和B各有胜负,那它们就是互不支配。所有互不支配的解构成Pareto前沿,这条前沿上的点,每一个都有资格成为最终方案,只不过侧重方向不同。
用买房来类比就很好懂:你既想面积大,又想总价低,这是两个冲突目标。把所有在售房源画在“面积-总价”坐标里,最左上角那条包络线上的房子就是Pareto前沿,剩下的房子基本可以直接忽略。你最后买哪套,主要看你自己更在意面积还是预算。翼型优化同理,算法替你找出一组“没有被任何其他方案完全碾压”的构型,具体挑哪个,由你在设计需求和工程约束下拍板。
1.2 为什么在翼型优化里选择NSGA-II
翼型优化可选的算法不少,常见的包括单目标遗传算法、粒子群算法、模拟退火,甚至更复杂的贝叶斯优化。我最后选了NSGA-II,主要是这几个原因:
第一,成熟度和稳定性。NSGA-II从提出到现在已经二十多年,数学性质和工程实现中的各种细节都被反复验证过,网上可以找到大量Matlab实现版本、教程和讨论。对于翼型优化这种气动评估本身就很费时的场景,我不想在优化器层面再引入算法不稳定、调参困难这些变量。
第二,它特别适合中等规模设计变量的问题。翼型形状优化如果采用Hicks-Henne扰动法,设计变量通常在10到30个量级,种群规模取40到100就能跑出比较像样的Pareto前沿。这个规模正好是NSGA-II的舒适区。相比之下,粒子群在低维连续问题上也很快,但对Pareto前沿的分布均匀性控制不如NSGA-II的拥挤度机制来得直接;单目标遗传算法则需要预设权重,又回到了“先定价值判断”的老问题。
第三,Matlab生态和并行计算很方便。Matlab代码可以直接循环调用气动求解器,也可以用parfor做种群并行评估。这一点在气动分析单次耗时约几秒钟、整个种群几百个个体时非常关键,直接决定你是等一晚还是等一周。
我当时项目里的总体架构是这样的:最外层是一个Matlab优化主循环,内部由参数化模块生成翼型坐标,然后调用气动求解器算性能,再把目标值返回给NSGA-II的评估函数。优化结束后,把Pareto前沿、最优翼型坐标、气动性能数据一起导到报告里。整条链路逻辑清楚,每一段都可以单独替换或者调试。
2. 翼型参数化与目标函数设计
2.1 用Hicks-Henne扰动函数定义优化变量
要做形状优化,第一步就是让设计变量能够连续、稳定地控制翼型外形。直接拿翼型表面每个离散点坐标当设计变量,变量数量太多,而且相邻点之间容易产生波浪形畸变,工程上基本不可行。我这次用的是经典的Hicks-Henne型函数扰动法。
思路是:先选定一个基准翼型,把优化变量定义为一组加在上表面和下表面的扰动系数,然后叠加到基准翼型的纵坐标上。
Hicks-Henne基函数的标准形式是:
[ b_i(x) = \sin^t(\pi x^{e_i}), \quad e_i = \frac{\ln(0.5)}{\ln(x_i)} ]
其中参数t控制函数的尖锐程度,常用t=3;(x_i)是第i个基函数的峰值位置,分布在翼型弦向的不同位置,比如从0.1到0.9等间距取10个点。这样上表面可写成:
[ y_u(x) = y_{u0}(x) + \sum_{i=1}^{n} a_i \cdot b_i(x) ]
下表面同理:
[ y_l(x) = y_{l0}(x) + \sum_{j=1}^{m} c_j \cdot b_j(x) ]
(y_{u0})和(y_{l0})是基准翼型上下表面纵坐标,(a_i)、(c_j)就是优化变量。通常上下表面各取8到12个基函数,设计变量总数控制在16到24个左右。扰动系数的上下界我一般取(\pm 0.03),这是弦长的3%。范围超过这个值,翼型很容易出现奇怪的凹凸或者局部鼓包,气动评估多半不收敛。
这种方法最大的好处是设计变量与几何变化之间的映射非常直观。看到某个基函数系数值变大,你基本能猜到哪个弦向位置的翼面“鼓”起来了,调试起来方便。如果你追求更光滑的外形,可以考虑CST参数化方法,用类函数/形状函数变换来表达整个翼型,表面连续性更好,设计变量也更有物理意义,但实现复杂度会高一些。我把两种方案做了个粗对比:
| 项目 | Hicks-Henne扰动法 | CST参数化 |
|---|---|---|
| 设计变量含义 | 表面叠加扰动的幅度 | 形状函数多项式系数 |
| 变量数量 | 10~24个 | 6~20个(上下表面合计) |
| 表面光滑性 | 较好,受基函数约束 | 很好,天然连续 |
| 几何直观性 | 强 | 一般 |
| 实现难度 | 低 | 中 |
| 和基准翼型结合度 | 容易,直接叠加 | 需要反解系数 |
对于Matlab教学和工程预研场景,Hicks-Henne更省事,实测结果也足够用于方案对比。如果你后面要做精密气动设计,再考虑切到CST。
2.2 目标函数、约束设置与气动评估方法
在我这个项目里,目标函数选择了最大升阻比和最小化力矩系数绝对值,同时把最大厚度比作为约束条件。具体解释一下选择理由:
- 升阻比(CL/CD)是巡航性能最直接的体现,目标当然是最大化;
- 俯仰力矩系数(|CM|)衡量翼型力矩特性,如果力矩过大,配平阻力会非常难看,我是把它作为第二个目标尽量最小化;
- 最大厚度比(t/c)则设下限,比如不低于基准翼型的92%,因为结构工程师那边需要翼型内部有足够的布置空间。
有的项目也把最大升力系数最大化作为一个目标,但最大升力系数的精确计算比较依赖气动求解器在近失速区的准确性,处理不当反而会拖累整个优化。我这次更关注巡航段,所以只把设计点性能作为优化目标,把厚度作为硬约束。如果你想加入大攻角性能,也可以在目标函数里通过加权或者第二个设计点的升阻比来体现,但那样Pareto前沿维度会提高,结果更难解读,建议慎重。
目标函数值都从气动分析来。我在项目里用的是XFOIL这类开源翼型分析工具,它基于面元法和粘性边界层积分,对低速/低亚声速翼型的设计评估足够用,单次计算也很快。Matlab调用它的方式有两种:一种是通过命令行批量调用XFOIL并读取输出文件,另一种是用Matlab自己写一个面元法求解器直接嵌入式计算。我当时选的是前者,因为XFOIL的转捩预测和阻力量算能力比普通教学用面元法更靠谱。
XFOIL的运行配置有几个关键参数要注意:马赫数、雷诺数和攻角范围。我用的设计点是马赫数0.3、雷诺数500万,攻角从0°到8°每隔2°算一个点,目标函数里用的是4°巡航攻角下的升阻比和力矩系数。运行时要保证XFOIL输出文件里有收敛结果,没有收敛的个体直接标记为无效解。
下面是一段简化版的目标函数调用示意,展示Matlab和XFOIL的配合方式:
function [CL, CD, CM] = evaluate_airfoil(coords, alpha, Ma, Re) % coords: 上下表面坐标组成的二维数组 % 调用XFOIL进行气动评估,这里省略系统调用和文件解析细节 % 返回升力系数CL、阻力系数CD、力矩系数CM fid = fopen('polar_file.txt', 'r'); % 解析XFOIL输出 data = load('polar_file.txt'); CL = interp1(data(:,1), data(:,2), alpha); CD = interp1(data(:,1), data(:,3), alpha); CM = interp1(data(:,1), data(:,4), alpha); end实际工程中,我会让XFOIL批量完成一个攻角列表的计算,然后插值得到目标攻角下的值。这里面最麻烦的是文件读写路径和进程调用,后面专门讲避坑。
3. NSGA-II核心机制与Matlab代码实现拆解
3.1 算法主流程:非支配排序与拥挤度在做什么
NSGA-II的主流程并不复杂,核心可以拆成四块:种群初始化、非支配排序、拥挤度距离计算、新一代种群生成。
种群初始化就是随机生成N个个体,每个个体是一组设计变量向量。在这之后,进入主循环。每一代里要做的事情是:先用当前父代种群通过锦标赛选择、交叉和变异生成子代种群,然后把父代和子代合在一起,形成一个2N规模的种群,对它做非支配排序和拥挤度计算,最后挑出N个最好的个体作为下一代。
非支配排序解决的是怎么评价“谁更优”的问题。算法先把所有个体按支配关系分层:第一层是当前种群中所有不被任何个体支配的解,第二层是去掉第一层后剩余个体中不被任何个体支配的解,以此类推。层数越靠前,说明这个解的综合性能越接近Pareto前沿。
拥挤度距离解决的是“同一个前沿里如何排名”的问题。一个解如果周围没有其他解,说明它处于一个相对孤立的区域,保留它可以让最终解集在Pareto前沿上分布得更均匀,避免所有解挤在一小段区域里。所以同一层内,拥挤度距离大的解优先保留。
通俗理解:非支配排序是决定哪些个体优先存活,拥挤度距离是决定同一个优势层级里谁更有资格留下来。二者合起来,NSGA-II才能在多目标空间里既往Pareto前沿方向前进,又能把前沿铺得比较均匀。
3.2 Matlab代码模块划分与关键片段
整个Matlab工程我建议按功能拆模块,而不是全部写在一个文件里。清晰的模块划分能让你在换参数化方法、换气动求解器时不至于动一发而牵全身。我的工程目录结构大致是这样:
|-- main_optimization.m |-- init_population.m |-- evaluate_population.m |-- non_dominated_sort.m |-- crowding_distance.m |-- tournament_selection.m |-- sbx_crossover.m |-- polynomial_mutation.m |-- hicks_henne_coords.m |-- plot_pareto_front.mmain_optimization.m负责读参数、初始化种群、执行主循环、输出结果。evaluate_population.m负责调用气动评估,是整个流程中最耗时也最容易出错的模块。non_dominated_sort.m和crowding_distance.m是NSGA-II最核心的两个函数。
非支配排序的Matlab实现思路很直接。假设种群规模为N,目标函数数量为M。先对每个个体p,遍历其他个体q,统计有多少个体支配p,以及p支配了哪些个体,然后构建分层:
function [rank, F] = non_dominated_sort(fitness) % fitness: N x M 的目标函数值矩阵,所有目标默认越小越好 N = size(fitness, 1); rank = zeros(N, 1); dominated_count = zeros(N, 1); dominate_set = cell(N, 1); F = {}; for p = 1:N dominate_set{p} = []; for q = 1:N if all(fitness(p,:) <= fitness(q,:)) && any(fitness(p,:) < fitness(q,:)) dominate_set{p} = [dominate_set{p}, q]; elseif all(fitness(q,:) <= fitness(p,:)) && any(fitness(q,:) < fitness(p,:)) dominated_count(p) = dominated_count(p) + 1; end end end front = []; for i = 1:N if dominated_count(i) == 0 rank(i) = 1; front = [front, i]; end end F{1} = front; k = 1; while ~isempty(F{k}) next_front = []; for i = F{k} for j = dominate_set{i} dominated_count(j) = dominated_count(j) - 1; if dominated_count(j) == 0 rank(j) = k + 1; next_front = [next_front, j]; end end end k = k + 1; F{k} = next_front; end end这段代码里有个隐形坑:如果不把目标统一成“越小越好”,比较逻辑会乱掉。因为NSGA-II比较用的是统一方向。我当时把所有目标都做了取反和取绝对值处理,比如最大化升阻比,就写成(1/(CL/CD))或者负的升阻比,这样排序逻辑就统一了。
拥挤度距离计算则是针对每个前沿单独进行。对每个目标变量,先把该前沿的个体按目标值排序,目标值最小和最大的个体,拥挤度设为无穷大,其他个体的距离则是相邻两个个体在该目标上的差除以该目标在整个前沿上的范围,最后把所有目标的距离累加。代码我就不整段贴了,网上Matlab版本大同小异,但注意要加上边界个体距离无穷大的处理,否则靠近前沿端点位置的解容易被误删。
3.3 参数设置建议与收敛性观察
NSGA-II需要设置的参数不算多,但每个参数都直接影响结果质量。我开始跑的时候抄的很多教程参数,后来发现针对翼型优化,有些参数要做针对性调整。
| 参数 | 常见取值 | 我的设置 | 调整原因 |
|---|---|---|---|
| 种群规模 | 40~100 | 60 | 太小前沿不均匀,太大XFOIL计算时间成倍增加 |
| 迭代代数 | 50~200 | 100 | 看收敛曲线,一般80代后HV指标趋于平稳 |
| 交叉概率 | 0.8~0.95 | 0.9 | 保持足够探索性 |
| 变异概率 | 1/n | 1/n | n是设计变量数,约0.05 |
| 交叉分布指数 | 10~20 | 15 | 越大子代越接近父代 |
| 变异分布指数 | 20 | 20 | 同上 |
收敛性我一般看两个指标:一个是Pareto前沿上个体数量和分布是否在后期基本稳定,另一个是超体积指标HV的变化趋势。超体积指标的意思是Pareto前沿与参考点之间的区域面积,HV越大说明前沿范围越广、越靠近理想区。你可以在Matlab里每10代计算一次HV,画一条曲线,如果曲线后半段基本走平,说明可以停了。
这里有个经验:翼型优化由于气动评估本身有数值噪声,Pareto前沿并不像很多教学题那样光滑。你会发现某些个体明明几何上变化很小,但气动数值却跳了一下,这在XFOIL这类工具里是正常现象,不用过分担心,也最好不要为了追求光滑前沿去缩小种群规模或减少代数,否则容易过拟合到数值噪声上。
4. 优化实际运行流程与结果解读
4.1 从初始化到Pareto前沿:一次完整运行记录
我把整个运行流程按实际操作顺序整理一下。首先在main_optimization.m里设置各项参数:设计变量个数取20,上下表面各10个Hicks-Henne基函数,基准翼型选某常规低速翼型,最大厚度比约12%。设计点取马赫0.3、雷诺500万、巡航攻角4°,目标函数为巡航升阻比最大化(做负值处理)和力矩系数绝对值最小化,约束是厚度比不低于11%。
然后是种群初始化。这个阶段千万别全随机。我试过全随机初始化,结果前20代几乎全是无效个体。有效的做法是以基准翼型为圆心,在扰动系数的边界范围内做小扰动随机采样,比如系数从-0.01到0.01之间随机,确保初始种群绝大多数个体几何正常,可以直接被气动评估接受。
进入主循环后,每一代的计算流程是固定的。种群评估是最大瓶颈。我用的机器开了六个并行worker,每个worker负责跑一部分个体的XFOIL计算。单个体一次评估大约要算5个攻角,耗时3到5秒,60个个体一代就是3到5分钟。迭代100代,大概需要6到8个小时。中途如果遇到系统卡顿或者XFOIL进程占用没释放,时间还会更久。
跑完后的直接输出是100代末代种群的非支配排序第一前沿,也就是Pareto前沿。我会再检查一下前沿上的解是否有明显密度异常,把那些几何形状发生畸变但目标值却异常优秀的“坏点”删掉。这类坏点通常是XFOIL在非物理几何上算出来的虚假收敛结果,务必警惕。
4.2 Pareto前沿怎么读,最终方案怎么选
Pareto前沿画出来后,横坐标我习惯放力矩系数绝对值,纵坐标放升阻比。你会在图上看到一条向左上方向延伸的带状点群。左上角的点升阻比高、力矩小,但一般厚度比会逼近约束边界;右下角的点力矩相对偏大,但升阻比也好不到哪去,通常是厚度余量较大或者外形过于保守。
从Pareto前沿上选解,我在这个项目里用的是两阶段法。第一阶段根据工程硬约束把所有不满足厚度要求的点剔除,第二阶段用一个偏好权重做二次排序。比如在设计讨论会上,结构同事提出希望翼型厚度不低于12.5%,巡航效率尽量高,同时力矩不能太差,那我就会在剩下的Pareto点里按三个指标做一次简单的归一化加权评分,选出一个折中解。
如果你不想引入权重,还有一个更直观的方法:直接画出几个典型解的翼型外形,让团队里的人看气动几何变化趋势。我每次汇报时都会把基准翼型、高升阻比翼型、大厚度翼型和折中翼型放在一张图上,大家一眼就能看出设计空间大概在哪。
4.3 优化前后翼型几何与性能变化
我那次项目里比较有代表性的三个解如下表。这里的数据是我自己项目中的某次运行结果,仅作演示参考,不同初始翼型和设计点得到的数值会不一样。
| 构型 | 升力系数CL | 阻力系数CD | 升阻比CL/CD | 力矩系数CM | 最大厚度比 |
|---|---|---|---|---|---|
| 基准翼型 | 0.852 | 0.0123 | 69.3 | -0.041 | 12.0% |
| 高升阻比方案 | 0.986 | 0.0108 | 91.3 | -0.052 | 11.4% |
| 大厚度方案 | 0.901 | 0.0136 | 66.3 | -0.033 | 12.8% |
| 折中方案 | 0.947 | 0.0115 | 82.3 | -0.043 | 12.2% |
看几何变化,高升阻比方案的前缘半径略微减小,上表面中部适度抬升,camber明显增加,这使得设计点附近升力效率提升。大厚度方案则是中后段厚度整体增大,力矩特性变好了,但摩擦阻力和压差阻力都有增加。折中方案表面看没有哪个指标最突出,但每个指标都在可接受范围,工程上往往最后就是这类方案被选中。
这里想多说一句:NSGA-II给出的Pareto前沿是一组候选方案,不是“最终给出一个最优翼型”的黑箱。如果你翻开一份报告,里面只有一张翼型图,那大概率是优化过程被“人工挑过”了。合格的多目标优化报告,应当附上Pareto前沿图和候选解的分布,让人能判断取舍是否合理。
5. 实操避坑指南与报告撰写要点
5.1 气动评估不收敛、个体无效怎么处理
我在整个过程中踩过的坑,量最大的就集中在这里。XFOIL或任何面元类求解器,对翼型几何的连续性都很敏感。Hicks-Henne扰动系数的取值如果偏大,翼型表面可能出现局部凸凹剧烈变化,XFOIL会迭代不收敛,或者给出一个不合理的高升力系数,这种解会直接污染Pareto前沿。
我的处理措施有三个层面。第一是代码层面:在evaluate_population.m里加一个几何有效性检查,凡是非物理翼型,比如上下表面交叉、前缘闭合失败、厚度比小于某个阈值,直接返回一个极大目标值,把这个个体判成无效。第二是求解器层面:跑XFOIL时设置收敛判据和时保护,如果该攻角下迭代超越上限,把这个点标记为失败,整条极曲线都算无效,而不是用一个中间值凑数。第三是优化层面:在锦标赛选择时,对无效个体的目标值设置非常大的惩罚值,让它们很难被选中参与交叉。
另外特别提醒一个坑:XFOIL的进程调用在Matlab里如果不用系统调用加延时控制,很容易出现上次进程还没结束、下次进程就启动,导致文件读写冲突。我当时用的方案是给每个并行worker设置独立的临时文件目录,并在每次调用后强制等待进程结束,再读取结果文件。这一个小改动就把失效个体率从10%以上降到了3%以下。
5.2 种群规模、设计变量上下界与敏感性分析
关于种群规模和迭代代数,我见过很多同学被“越大越好”的观念带偏。翼型优化的问题是单次气动评估代价高,你把种群从60加到200,Pareto前沿质量提升有限,但运行时间可能从一天变成五天。我一般是先跑一个20代的小规模草稿,看看目标函数空间的大致分布,再决定要不要把种群扩大。如果20代时已经出现大量无效解或者前沿上有明显空洞,问题多半在参数化范围或者评估模块,而不是迭代代数不够。
敏感性分析在报告里必不可少的。固定其他参数不变,把扰动系数的上下界从(\pm 0.01)调到(\pm 0.03),观察Pareto前沿的范围和分布变化。这个试验可以告诉你设计空间的边界是否合理。如果上下界放大之后,Pareto前沿显著扩宽但高升阻比方案都是几何畸变的坏点,说明边界处的解已经超出了气动数值工具的可靠范围。
5.3 报告怎么写才像一份工程交付物
既然项目标题里带了“附Matlab代码和报告”,最后说一说报告组织。一份合格的翼型优化报告,至少应该包含五个部分。第一是问题定义,用一段话说明设计点、目标函数、约束条件、变量范围,这部分要让不懂优化算法的人也能读懂。第二是优化方法说明,包括NSGA-II的参数表、流程图、每个模块代码的对应关系。第三是优化过程与收敛性分析,附上HV曲线或者前沿逐代变化图,证明你确实等到了算法收敛,而不是随便跑了多少代就停了。第四是优化结果展示,Pareto前沿图加几个代表性翼型的性能对比表。第五是结论与工程建议,明确告诉读者推荐方案是哪个、为什么、以及下一步该做什么延伸验证,比如风洞试验或者全机CFD校核。
代码交付的规范性也很重要。我在交付前给所有matlab脚本补了文件头注释,写明输入输出、依赖函数和用法示例。这样哪怕同事三个月后再来看这个工程,也能直接跑通main_optimization.m,而不是抓着我问“这个函数参数是什么意思”。
5.4 后续还能怎么扩展
如果后续你的项目还需要加强度、翼盒结构或者铰链力矩边界,可以继续在约束和目标函数里加东西。比如把结构重量作为第三个目标,用有限元模型算一下主要承载截面的应力;或者把低速与高速两个设计点同时纳入评估,要求翼型具备多工况适应能力。这类扩展会显著增加计算量,那时候可能需要用代理模型加速,先离线生成一部分翼型-气动数据,训练一个响应面模型,再把NSGA-II迭代中的大部分评估都放在代理模型上,最后只对前沿点做高精度校核。总的方向都是把这段流程当作一个可以持续替换模块的框架,而不仅仅是一次性脚本。
我个人在实际操作中的一点体会是:NSGA-II的代码并不难,难的是让几何生成模块和气动评估模块在几百上千次迭代里稳定运行。很多优化工程最后失败,不是算法跑不起来,而是评估环节的数值噪声和非物理解把整个优化方向带偏了。所以如果你现在准备复现这个项目,我的建议是先别急着调算法参数,花更多时间打磨几何生成和气动评估这两个底层模块,多用几张基准翼型把单次评估的稳定性验证扎实,再让NSGA-II去跑。这个顺序走正了,后面的优化结果质量会明显上一个大台阶。