1. 从一张翼型到一整套生态:这个项目到底在解决什么问题
第一次看到“airfoil-cst-rans-mlp”这串字符,很多人会本能地皱眉——它像是把四个不相干的缩写硬塞进了一个文件名。但如果你做过翼型气动分析,就会明白这其实是一条完整的技术链路:airfoil是翼型,CST是翼型参数化方法,RANS是雷诺平均数值模拟,MLP是代理模型。四者串起来,描述的是一件事——用参数化方法描述翼型几何,用数值模拟生成气动数据,再用神经网络做快速预测。
而标题后半段“PINNeAPPle 生态:zoo API、浏览器 Demo 与模型卡片标准”,才是真正有意思的地方。它说明这个项目不是孤立的单个模型,而是被放进了一个更大的框架里:有统一的模型仓库接口(zoo API)、有让非专业用户直接上手的浏览器演示(Demo)、还有一套描述模型元信息的规范(模型卡片标准)。换句话说,它试图回答一个工程界长期头疼的问题——一个训练好的科学计算模型,怎么才能被别人真正用起来,而不是躺在某个人的硬盘里吃灰。
我接触过不少类似的项目,模型本身做得不错,但交付方式极其原始:一个压缩包、一份 README、几个不知道什么版本的依赖。接手的人光是配环境就要折腾两天,最后还不一定能跑通。PINNeAPPle 这套生态的思路,是把“模型”当成“产品”来对待,从接口、交互、元信息三个层面做标准化。这个思路值得所有做科学计算模型的人参考,不管你做的是翼型、湍流、传热还是结构。
这篇文章适合三类人看:一是做翼型或气动代理模型、想了解怎么把成果工程化的同行;二是正在搭自己的模型管理平台、需要参考接口设计的开发者;三是对参数化建模和神经网络代理模型感兴趣、想找个完整案例上手的学习者。我会把 CST 参数化的原理、RANS 数据的生成逻辑、MLP 代理模型的设计取舍讲清楚,再重点拆解 zoo API、浏览器 Demo 和模型卡片这三块生态组件到底怎么落地。
2. 整体设计思路:为什么要把模型拆成“四层”
2.1 从“一个模型”到“一个可复用的资产”
先说说为什么这个项目要拆成 CST、RANS、MLP 三个环节,而不是端到端一把梭。端到端听起来很美,输入几何直接输出气动系数,但实际做起来问题很多。翼型几何的表示方式千变万化,有人用坐标点,有人用样条,有人用 NACA 四位数字;气动系数的定义也不统一,升力系数是按弦长还是按单位展长,阻力系数有没有算摩擦修正,这些细节不统一,模型就没法复用。
拆成三层之后,每一层都有明确的输入输出契约。CST 层负责“几何到参数”的映射,RANS 层负责“参数到气动数据”的映射,MLP 层负责“参数到预测值”的快速映射。这样做的好处是,任何一层都可以单独替换。比如你换一个更精确的 RANS 求解器,只要输出格式不变,MLP 层完全不用动;你想换一种参数化方法,只要把 CST 系数换成新的参数向量,后面的流程照样跑。
提示:这种分层设计在科学计算领域叫“关注点分离”,但落到实操上,关键是每一层都要有明确的 schema。没有 schema 的分层,只是把一个大坑拆成三个小坑。
2.2 为什么选 CST 而不是贝塞尔或 B 样条
翼型参数化方法很多,CST(Class Shape Transformation)是我个人比较推荐的一种。它的核心思想是把翼型上下表面的 y 坐标表示成“类函数”乘以“形状函数”再叠加一个尾缘厚度项。类函数负责控制前缘的圆头和后缘的尖尾,形状函数用伯恩斯坦多项式做加权,尾缘项单独处理厚度。
选 CST 的理由有三条。第一,它的参数有明确的几何意义,前几个系数大致对应弯度、厚度分布,调参的时候心里有数。第二,它的参数维度可控,一般 8 到 12 个系数就能把常见翼型拟合到很高的精度,这对后面训练 MLP 很友好——输入维度太高,样本需求会爆炸。第三,CST 的数学形式光滑,求导方便,做梯度优化或者敏感性分析时不会出问题。
相比之下,贝塞尔曲线控制点多了之后容易产生局部振荡,B 样条虽然灵活但参数和几何的对应关系不直观。我试过用 20 个控制点的 B 样条拟合一个薄翼型,结果前缘附近总是差一点,调了半天才发现是节点向量选得不对。CST 在这方面的容错性要好得多。
2.3 MLP 代理模型的定位:不是替代 RANS,而是替代“等待”
有人会问,既然有 RANS 了,为什么还要 MLP?答案很简单:RANS 太慢。一个二维翼型的 RANS 计算,网格质量好的话几分钟到十几分钟,网格差的话半小时起步。做优化设计的时候,动辄要评估上千个翼型,这个时间成本没人受得了。
MLP 代理模型的作用是在“设计探索阶段”提供秒级的近似评估。它的精度肯定不如 RANS,但在筛选阶段够用——先用 MLP 把明显不好的方案筛掉,剩下的候选再用 RANS 精算。这种“粗筛加精算”的策略在工程上非常常见,关键是 MLP 的误差要可控,不能把好方案误杀。
这里有个容易被忽略的点:MLP 的训练数据必须来自同一套 RANS 设置。如果训练集用的是 SA 湍流模型,测试的时候却拿 k-omega SST 的结果去比,误差会大得离谱。所以项目里 RANS 层的配置必须固化下来,作为模型卡片的一部分记录下来。
2.4 生态三件套的分工逻辑
zoo API、浏览器 Demo、模型卡片标准,这三样东西分别解决“怎么调”“怎么试”“怎么信”的问题。
zoo API 解决的是程序化调用的问题。它定义了一套统一的接口,不管背后是翼型模型、湍流模型还是结构模型,调用方式都一样。这对做集成的人来说是福音,不用为每个模型写一套适配代码。
浏览器 Demo 解决的是“零门槛体验”的问题。不是每个人都愿意装 Python 环境、配依赖、写脚本。一个网页,输入几个参数,点一下出结果,这种体验能极大降低传播成本。我见过很多好模型就是因为没有 Demo,知道的人寥寥无几。
模型卡片标准解决的是“可信度”的问题。一个模型用了什么数据训练、在什么范围内有效、误差大概多少、有哪些已知的失效场景,这些信息如果不写清楚,用的人就是在盲猜。模型卡片就是模型的“说明书加体检报告”,让使用者能判断这个模型适不适合自己的场景。
3. 核心细节解析:CST 参数化与 RANS 数据生成的实操要点
3.1 CST 参数化的数学形式与系数选择
CST 方法把翼型上表面 y 坐标写成:
y = sqrt(x) * (1-x) * sum(A_i * B_i(x)) + x * delta_z其中 sqrt(x)*(1-x) 是类函数,负责前缘圆头和尾缘收敛;B_i(x) 是伯恩斯坦基函数;A_i 是待定系数;delta_z 是尾缘厚度。下表面同理,只是类函数和系数不同。
实操中,我一般用 6 到 8 阶伯恩斯坦多项式,也就是上下表面各 7 到 9 个系数。阶数太低拟合精度不够,阶数太高容易过拟合,而且系数之间的相关性变强,后面做敏感性分析会很麻烦。有个经验值:对于相对厚度 10% 到 18% 的常见翼型,7 阶足够把几何误差控制在弦长的 0.1% 以内。
系数的初值可以用最小二乘拟合已有翼型得到。具体做法是取一批已知翼型的坐标点,构造伯恩斯坦基函数矩阵,解一个线性最小二乘问题。这一步不难,但要注意坐标点的分布——前缘附近曲率大,点要密一些,后缘可以稀一些。如果均匀取点,前缘的拟合误差会明显偏大。
注意:CST 拟合出来的翼型,前缘半径不一定和原始翼型完全一致。如果后续 RANS 计算对前缘特别敏感(比如大迎角分离),建议把前缘半径作为一个约束加进拟合过程,而不是纯最小二乘。
3.2 RANS 计算域的设置与网格要求
RANS 这一层是整个链路里最耗时的部分,也是最容易出问题的部分。二维翼型的 RANS 计算,计算域一般取远场 20 到 50 倍弦长,入口给速度入口,出口给压力出口,翼型表面给无滑移壁面。
网格方面,关键是边界层网格。y+ 值要控制在 1 以下(如果用低雷诺数湍流模型),第一层网格高度大概在 1e-5 到 1e-6 倍弦长量级。我见过有人用 y+ 等于 30 的壁面函数网格去算转捩,结果当然不对。网格量方面,二维翼型一般 3 万到 8 万单元,具体看雷诺数和湍流模型。
湍流模型的选择要固定。项目里用的是哪种模型,模型卡片里必须写清楚。我建议用 k-omega SST,它在逆压梯度和分离流动上的表现比较均衡,工程上接受度高。如果用 SA 模型,要注意它对转捩的预测偏保守。
计算设置里还有一个容易踩的坑:收敛判据。残差降到 1e-5 不代表结果收敛了,气动系数可能还在漂。我的做法是同时监控升力系数和阻力系数的历史曲线,连续 200 步变化小于 1e-4 才算收敛。这个判据比单纯看残差靠谱得多。
3.3 数据集的采样策略:拉丁超立方还是 Sobol
训练 MLP 需要一批“CST 系数到气动系数”的样本。采样策略直接影响模型的泛化能力。常用的有拉丁超立方采样(LHS)和 Sobol 序列。LHS 的优点是样本在边缘分布上均匀,缺点是样本之间可能有相关性;Sobol 序列是低差异序列,空间填充性更好,适合高维问题。
我的经验是,参数维度在 10 维以下用 LHS 就够了,超过 10 维用 Sobol。样本量方面,一个粗略的估计是参数维度的 10 到 20 倍。比如 14 个 CST 系数(上下表面各 7 个),样本量至少 150 到 300。如果要做交叉验证,还得再多一些。
采样范围也要注意。CST 系数的取值范围不能随便定,要根据实际翼型库的统计分布来。范围太窄,模型泛化不出去;范围太宽,很多样本对应的几何根本不合理,RANS 算出来也是浪费。我一般取已有翼型系数的均值加减 2 到 3 倍标准差作为采样边界,超出边界的样本直接丢弃。
3.4 MLP 的网络结构与训练技巧
MLP 的结构不用太复杂。输入层维度等于 CST 系数个数,输出层维度等于要预测的气动系数个数(比如 Cl、Cd、Cm 三个),中间两到三个隐藏层,每层 64 到 128 个神经元,激活函数用 ReLU 或者 SiLU。
训练的时候有几个技巧。第一,输入输出都要做标准化,CST 系数的量级差异很大,不标准化的话训练很难收敛。第二,损失函数用 MSE 就行,但如果某些气动系数的重要性更高(比如阻力系数对优化更关键),可以加权。第三,用早停法防止过拟合,验证集误差连续若干轮不下降就停。
还有一个容易被忽略的点:物理约束。MLP 是纯数据驱动的,预测出来的气动系数可能违反一些基本物理规律,比如负阻力。可以在损失函数里加惩罚项,或者在输出层加一个软约束。这个做法不一定能完全消除违规,但能明显改善。
4. 生态组件落地:zoo API、浏览器 Demo 与模型卡片怎么实现
4.1 zoo API 的接口设计:统一但不僵化
zoo API 的核心目标是让调用者用同一套代码访问不同模型。设计上有几个关键决策。
第一,接口的粒度。我倾向于把接口分成“元信息查询”和“推理调用”两类。元信息查询返回模型的基本信息(名称、版本、输入输出 schema、适用范围),推理调用接收输入、返回输出。这样调用者可以先查元信息,再决定怎么调。
第二,输入输出的格式。用 JSON 是最稳妥的选择,跨语言、跨平台都没问题。输入是一个字典,键是参数名,值是数值或数组;输出也是字典,键是结果名。schema 里要写清楚每个字段的类型、维度、单位。
第三,错误处理。模型推理可能因为输入越界、数值不稳定等原因失败,接口要返回明确的错误码和错误信息,而不是抛一个看不懂的异常堆栈。
下面是一个简化的接口定义示例:
# 元信息查询 GET /api/models/airfoil-cst-rans-mlp # 返回 { "name": "airfoil-cst-rans-mlp", "version": "1.2.0", "input_schema": { "cst_upper": {"type": "array", "length": 7}, "cst_lower": {"type": "array", "length": 7}, "alpha": {"type": "float", "unit": "degree"} }, "output_schema": { "cl": {"type": "float"}, "cd": {"type": "float"}, "cm": {"type": "float"} }, "valid_range": {"alpha": [-4, 12]} } # 推理调用 POST /api/models/airfoil-cst-rans-mlp/predict # 请求体 {"cst_upper": [...], "cst_lower": [...], "alpha": 4.0}这个设计的好处是,调用者不需要知道模型内部是 MLP 还是别的什么,只要按 schema 传参就行。换模型的时候,只要新模型的 schema 兼容,代码不用改。
提示:schema 里一定要写 valid_range。我见过太多模型被用在训练范围之外,结果惨不忍睹,用的人还以为是模型本身不行。
4.2 浏览器 Demo 的技术选型与交互设计
浏览器 Demo 的目标是让不懂代码的人也能体验模型。技术上有两条路:一是把模型转成 ONNX 或者 TensorFlow.js 格式,直接在前端推理;二是前端只做界面,推理请求发到后端。
两条路各有优劣。前端推理的好处是响应快、不依赖网络、隐私好,缺点是模型大小受限,复杂的 MLP 还好,大模型就吃力了。后端推理的好处是模型可以任意大,缺点是依赖服务可用性。
对于翼型 MLP 这种小模型,我推荐前端推理。一个 14 输入、3 输出、两隐藏层的 MLP,参数量也就几万个,转成 ONNX 之后几百 KB,加载毫无压力。用 onnxruntime-web 就能跑,不需要 GPU。
交互设计上,关键是让用户“看得见几何”。输入 CST 系数之后,应该实时画出翼型形状,让用户知道自己在调什么。输出气动系数之后,可以画一个极曲线或者压力分布示意,比单纯给几个数字直观得多。
还有一个细节:预设。大部分用户不知道怎么调 CST 系数,给几个预设翼型(比如常见的低速翼型、层流翼型)让用户一键加载,体验会好很多。这个功能实现起来很简单,但能显著降低使用门槛。
4.3 模型卡片标准:写什么、怎么写
模型卡片这个概念最早来自机器学习公平性研究,后来被广泛用于模型文档。对于科学计算模型,我认为卡片里必须包含以下几块内容。
基本信息:模型名称、版本、作者、发布日期、许可证。这些是追溯用的,缺了就没法管理。
训练数据:数据来源、样本量、采样方法、RANS 求解器版本、湍流模型、网格量级。这些信息决定了模型的可信边界。如果训练数据用的是 SA 模型,卡片里没写,别人用 k-omega SST 的结果去验证,误差大是必然的。
适用范围:CST 系数的有效范围、迎角的有效范围、雷诺数的有效范围。超出范围的使用要明确警告。
性能指标:在测试集上的 MAE、RMSE、最大误差,最好分区域报告(比如线性段和失速段分开)。单一的整体误差指标参考价值有限。
已知失效场景:比如大迎角分离、跨声速激波、非常规翼型。这些是使用者最需要知道的信息,但往往最容易被省略。
复现信息:训练脚本、配置文件、随机种子。没有这些,别人没法复现你的结果,模型的可信度就打折扣。
我见过一些模型卡片写得像论文摘要,全是“本模型取得了良好效果”之类的空话。卡片的价值在于具体,在于把那些“不好看”的信息也写出来。一个诚实的模型卡片,比一个漂亮的模型卡片有用得多。
4.4 三者的协同:从调用到验证的闭环
zoo API、浏览器 Demo、模型卡片不是孤立的,它们应该形成一个闭环。
用户通过 Demo 体验模型,觉得有用,去看模型卡片了解适用范围和精度,然后通过 zoo API 集成到自己的流程里。使用过程中如果发现问题,可以反馈到卡片里,更新已知失效场景。这个闭环跑通了,模型才真正“活”起来。
实现上,Demo 和 API 应该共享同一份 schema 定义,避免两边不一致。模型卡片可以从 schema 和训练配置里自动生成一部分内容,减少手工维护的成本。这些工程细节看起来琐碎,但决定了生态能不能持续运转。
5. 常见问题与排查技巧实录
5.1 CST 拟合误差大,问题出在哪
最常见的三个原因。一是伯恩斯坦阶数不够,前缘附近拟合不足,加阶数或者在前缘加密采样点。二是尾缘厚度项没处理好,原始翼型有钝尾缘但模型假设尖尾缘,导致后缘偏差。三是坐标归一化没做对,弦长不是 1 或者前缘不在原点,拟合出来的系数没有可比性。
排查方法:把拟合出来的翼型和原始翼型叠在一起画图,看误差集中在哪个区域。前缘误差大就加阶数,后缘误差大就检查尾缘项,整体偏移就检查归一化。
5.2 RANS 算出来的阻力系数明显偏大
先检查网格。边界层第一层高度是否满足 y+ 要求,边界层内是否有足够的径向节点(一般 15 到 25 层)。再检查参考值设置,参考面积、参考长度、参考速度是否和系数定义一致。最后检查湍流模型,壁面函数网格配低雷诺数模型,或者反过来,都会导致阻力偏大。
还有一个隐蔽的原因:转捩。如果用的是全湍流模型,层流翼型的阻力会被高估很多。这种情况要么换转捩模型,要么在数据处理时把转捩影响考虑进去。
5.3 MLP 预测在训练集上很好,测试集上崩了
典型的过拟合。先看样本量够不够,参数维度的 10 倍是下限。再看网络是不是太大,隐藏层神经元减半试试。然后检查训练集和测试集的采样范围是否一致,如果测试集超出了训练集的采样边界,崩是正常的。
还有一个容易忽略的点:数据泄漏。如果训练集和测试集里有几何非常接近的翼型,测试误差会虚低。正确的做法是按翼型族划分训练测试集,而不是随机划分。
5.4 浏览器 Demo 加载慢或者卡顿
模型文件太大是最常见的原因。检查 ONNX 模型的体积,如果超过几 MB,考虑量化或者剪枝。另一个原因是前端没有做懒加载,模型和界面一起加载,首屏时间很长。把模型加载放到用户点击“开始计算”之后,体验会好很多。
如果是推理卡顿,检查是不是在主线程里跑推理。onnxruntime-web 支持 Web Worker,把推理放到 worker 里,界面就不会卡。
5.5 模型卡片写了没人看
这个问题很现实。我的经验是,卡片要分层。最上面放一页“速查表”,包含适用范围、精度指标、已知限制,用表格呈现,30 秒能看完。下面再放详细内容,给需要深入的人看。另外,卡片要放在用户容易找到的地方,比如 API 文档首页、Demo 的结果页,而不是藏在仓库的某个角落。
| 问题现象 | 可能原因 | 排查方向 |
|---|---|---|
| CST 拟合前缘偏差大 | 阶数不足或采样点分布不均 | 提高阶数,前缘加密采样 |
| 阻力系数整体偏大 | 网格 y+ 不达标或参考值错误 | 检查边界层网格和参考设置 |
| MLP 测试集误差大 | 过拟合或数据泄漏 | 减网络规模,按翼型族划分数据集 |
| Demo 首屏慢 | 模型文件过大或未懒加载 | 量化模型,延迟加载 |
| 模型卡片无人问津 | 信息层级不清或位置隐蔽 | 做速查表,放在显眼位置 |
6. 我在这个链路上踩过的几个坑
说几个具体的心得,都是文档里不会写的。
第一个坑是 CST 系数的顺序。不同文献里伯恩斯坦基函数的排列顺序可能不一样,有的从低阶到高阶,有的反过来。如果训练和推理用的顺序不一致,结果会完全错乱,而且很难发现,因为数值看起来都“像那么回事”。我的做法是在 schema 里明确写死顺序,并且在代码里加一个校验。
第二个坑是 RANS 的收敛判据。早期我只看残差,结果有些算例残差降下去了但气动系数还在振荡,训练出来的 MLP 在这些区域误差特别大。后来改成监控气动系数的历史曲线,问题才解决。这个改动看起来小,但对数据质量的影响很大。
第三个坑是模型卡片的版本管理。模型更新了,卡片没更新,用户按旧卡片的范围去用新模型,出了问题。后来我把卡片和模型版本绑定,模型发布的时候卡片必须同步更新,否则 CI 不通过。这个约束一开始觉得麻烦,后来发现省了很多沟通成本。
第四个坑是 Demo 的输入校验。用户输入的 CST 系数可能对应一个自交的几何,前端不校验的话,画出来的图很诡异,推理结果也没意义。加一个简单的几何合法性检查(比如上下表面不相交、厚度为正),能挡掉大部分无效输入。
这个生态的思路,我觉得最有价值的地方不是某个具体技术,而是“把模型当产品”的意识。模型训练出来只是开始,怎么让人用得方便、用得放心,才是真正花功夫的地方。zoo API 解决调用便利性,Demo 解决体验门槛,模型卡片解决信任问题,三件事缺一不可。后续如果要扩展,我建议往“模型对比”方向走——让用户能同时调多个模型,对比结果,这样生态的价值会更大。