
pai-design-workbench 的“车身气动”通道不让模型判断一个车身设计好不好,而是交给 OpenFOAM 的 RANS 求解:同一个 Ahmed 型车身在两套网格上各算一遍,再用四项检查给出结论。配置了 AWS Batch 作业定义时,每个算例作为一个独立的 Batch 作业运行,结果回到主机后先核对哈希和求解器版本,再进入检查。本文依据固定在 commit f225835 的 docs/AERO.md、native/cfd_review.py、native/cfd_job.py 和一份保存的复核记录 docs/evidence/aero-batch.json,讲这条链路每一步交出什么、核对什么,以及记录里本地与 Batch 的阻力系数到底差多少。
文中的数字都来自项目自己的文档、代码和记录([O],项目自报),本站没有运行这些算例,也没有复现那次 Batch 复核。数字只在它们的网格、迭代次数和湍流模型条件下成立,这和课程 物理AI实验室 0.4:在虚拟世界练一万遍,给三个仿真数字补上实验条件 讲的是同一条规则;这里换成了一个可以逐个文件核对的气动算例。
这条通道算什么、怎么判
按 AERO.md,几何由 CadQuery 2.8 / OCCT 7.9 生成参数化的 Ahmed 型车身。Ahmed 车身是汽车钝体气动的标准基准(Ahmed、Ramm 与 Faltin,1984):1.044 × 0.389 × 0.288 m,鼻部半径 0.1 m,尾部斜面长 0.222 m,离地间隙 50 mm。每次复核都以 25° 斜角的车身作参照。
求解器是 OpenFOAM v2512,来自官方 OpenCFD 镜像 opencfd/openfoam-default:2512,并通过环境变量 PAI_OPENFOAM_IMAGE 按 digest 固定。网格依次经过 blockMesh、surfaceFeatureExtract、snappyHexMesh;流动用 simpleFoam、k-ω SST、壁面函数,来流 40 m/s,移动地面,半模加对称面。四项检查(pai-cfd-checks-1)如下:
| 检查 | 定义 | 默认阈值 |
|---|---|---|
drag-coefficient | 细网格(level 4)Cd,取 1200 次 SIMPLE 迭代中最后 400 次的均值 | Cd ≤ 0.24 |
grid-convergence | |Cd(level 4) − Cd(level 3)| / Cd(level 4);文档写明这是两级网格依赖性,不是 GCI | ≤ 12 % |
iterative-convergence | 平均 Cd 是否平稳:|最后 400 次均值 − 最后 200 次均值| / 均值,取两套网格中较差者;原始振荡带宽另存于 cfd.json | ≤ 1 % |
mesh-quality | 两套网格的 checkMesh 都报告 “Mesh OK” | 两套都通过 |
迭代检查是被一次拒绝改出来的。文档记录:level 4 上 12.5° 车身的稳态 RANS 呈准周期振荡,Cd 在 0.2255–0.2313 之间,周期约 300 次迭代,100 次迭代内带宽 2.6 %。第一次端到端运行用这个带宽做检查,拒绝了该车身;现在改为对 400 次迭代取均值,再判断这个均值是否平稳。检查结果以 EvalArc 的 JUnit 形式进入与其他通道相同的反馈、复检和发布流程(EvalArc 的介绍见 EvalArc:分数变高了,智能体哪里退步了)。
一个算例怎样上 Batch,再回到主机
cfd_review.py 支持两种 runner:本地 docker(--network none,以主机用户运行,只挂载算例目录)和 batch。Batch 路径的步骤是:
- 主机用
cfd_case.py写出算例(字典文件与body.stl),打包成case.tar上传到 S3 的jobs/<run>/<name>-l<level>/前缀下。打包时排除run.sh。 - 提交一个 Batch 作业。
Dockerfile.cfd以同一个固定 digest 的 OpenCFD 镜像为基础,只加 Python 和 boto3;每个作业 16 vCPU、32 GiB,不重试。 - 作业端
cfd_job.py要求前缀以jobs/开头且不含..;解包前只接受普通文件和目录,拒绝绝对路径和含..的路径,链接因不是普通文件而被拒绝;代码注释写着成员应位于case/下,但代码并不检查这一点。解包使用filter="data",之后必须有case/system/controlDict。运行的run.sh取自镜像自身,不取自输入。 - 作业写出
coefficient.dat、run.json、log.checkMesh和result.json(schemapai-cfd-job-1,含每个文件的 SHA-256、OpenFOAM 版本、镜像标签、耗时和 CPU 数),上传时附ChecksumSHA256。 - 主机每 10 秒查询一次作业状态,默认期限 7200 秒。作业失败或超期都只报告,不重新提交。成功后要求
status为measured、openfoam为v2512,再逐个下载三个文件,与result.json中的摘要比对,不一致即报错。 - Cd 由主机从
coefficient.dat计算,四项检查也在主机上判定;算例目录随后删除,只保留汇总证据,不保留约 100 MB 的流场。
要注意哈希比对的边界:主机从同一个 S3 前缀下载 result.json 和三个文件,所以比对一致只说明下载到的文件与下载到的 result.json 中的摘要相符。它能发现文件损坏,或文件被改动而 result.json 未随之更新;有权写入该前缀的人若同时替换两者,它发现不了。作业上传时附带的 ChecksumSHA256 保护的是上传过程中的完整性。比对也不能独立证明求解本身是对的。求解是否可信,仍要看网格、迭代两项检查和下文的局限。
保存的复核记录:两个车身、两套网格、四个作业
aero-batch.json 记录了一次在项目托管站点上完成的 12.5° 车身复核(checkedAt 2026-10-03T21:50:00Z,结论 accepted-aero-body,阻断项 0,耗时 9 分钟)。参照车身为 25°,候选为 12.5°,每个车身两套网格,共 4 个 Batch 作业,每个作业报告 16 个 CPU:
| 车身 | 网格 | 单元数 | Cd | 主机计时(秒) | 作业自报(秒) |
|---|---|---|---|---|---|
| 参照 25° | level 3 | 61,310 | 0.25751 | 141.9 | 61 |
| 参照 25° | level 4 | 219,622 | 0.23406 | 352.5 | 269.1 |
| 候选 12.5° | level 3 | 61,573 | 0.25366 | 152 | 61.6 |
| 候选 12.5° | level 4 | 221,354 | 0.22926 | 262.1 | 169.2 |
四项检查两个车身都标为通过:参照的网格变化 0.1002、迭代漂移 0,候选的网格变化 0.1064、迭代漂移 0.00012,两者 mesh-quality 都是 2 套网格通过。记录没有写明这次复核用的阈值;按文档默认值,10.0 % 和 10.6 % 的网格变化离 12 % 的上限不远。主机计时从提交前开始,到解析完结果为止,所以比作业自报多出的部分包括上传、排队、轮询和下载,记录没有逐项拆分。
本地与 Batch:阻力系数和耗时怎么读
记录的 localComparison 给出:参照车身本地与 Batch 的 Cd 都是 0.23406;候选车身本地 0.22945,Batch 0.22926。AERO.md 称两者“相等”,但候选的数字实际相差 0.00019,约为本地值的 0.08 %。记录没有解释这一差异的来源,本文也不推测。
耗时方面,记录写本地 8 核 94 分钟,Batch 9 分钟。这是一次保存的开发运行:本地是 8 核,Batch 是 4 个各 16 vCPU 的作业,硬件、核数和调度都不同,记录也没有给出费用。它不说明 Batch 在一般情况下快十倍,也不是成本比较。
在本地复现
AERO.md 给出的本地运行命令如下,在 commit f225835 的仓库根目录执行:
docker pull opencfd/openfoam-default:2512
echo "PAI_OPENFOAM_IMAGE=$(docker image inspect opencfd/openfoam-default:2512 --format '{{index .RepoDigests 0}}')" >> .state/demo.env
npm run test:aero # 35° body vs the Cd target → feedback → 12.5° body re-solved on two meshes → feedback closed
注释说明了这个测试的过程:35° 车身对照 Cd 目标,产生反馈,再在两套网格上重算 12.5° 车身,直到反馈关闭。第二行把本机拉到的镜像 digest 写进环境文件,所以固定的是你自己拉到的那一个 digest,不一定是记录中那次复核所用的 digest(记录以 sha256:33fb575a… 截断显示)。所给文档没有写克隆仓库、安装 npm 依赖和 CadQuery 环境的步骤;检出提交只固定了仓库里的文件,不固定这些依赖的版本。
通过 API 发起时是 POST /api/projects/:id/aero,请求体含 requestId、projectRevision 和 parameters(slantAngleDeg、noseRadius、length、height),requirements 可选。AI 工具 aero-body 只提出带类型的参数,仍需人确认。只有配置了 PAI_SOLVER_CFD_JOB_DEFINITION 时才走 Batch;所给材料不含作业队列、角色和网络的定义,本文不给部署步骤。若自建,作业角色应只授予所用存储桶 jobs/ 前缀的读写,遇到 AccessDenied 时核对前缀,而不是放宽权限。
局限 / 注意
- 网格依赖仍然明显:level 3 到 level 4 Cd 变化 9–10 %,文档自己说求绝对值还需要更细的网格。当前不加边界层(prism layers),文档把加层和更细网格列为在 AWS Batch 上的下一步。
- 阻力被低估:本地 level 4 下 25° 车身 Cd 0.234,实验值 0.285;level 3 下 30° 车身 0.263,实验值 0.378。文档把原因归于带壁面函数的稳态 RANS 的已知弱点,30° 的高阻状态依赖非定常的 C 柱涡和斜面上的局部分离。
- 排序只在低阻一端成立:12.5° 在实验和计算中都是最低阻,level 4 下 0.230 与实验值 0.230 一致。但同一张表里,level 4 计算给出 35° 车身 0.249 高于 25° 的 0.234,实验则是 25° 的 0.285 高于 35° 的 0.260,次序相反。用它比较其他几何范围前需要另行验证。
- 每条记录都带
physicalValidation: false。这是设计比较工具,不是风洞、道路或任何物理安全验证。 - 保存的 Batch 记录说明那次托管复核跑完了;它不是独立复现,本站也没有重新执行。
cfd_review.py开头的模块说明仍写着“最后 100 次迭代均值”和 (max − min) / mean 带宽,与同一文件的代码及AERO.md的 400/200 平稳性检查不一致;本文按代码和AERO.md描述。



