把固定版本的 OpenFOAM 算例搬上 AWS Batch:哈希回传、两套网格与本地阻力对照

pai-design-workbench 把每个 OpenFOAM v2512 算例作为单独的 Batch 作业运行,回传后核对哈希与版本再做四项检查。一次保存的复核中,候选车身 Cd 本地 0.22945、Batch 0.22926。

流程图:标题下四个带编号 1 到 4 的实心圆从左到右用箭头相连,下方文字依次为:主机写出算例,打包 case.tar,上传 S3;每个算例一个 Batch 作业,16 vCPU / 32 GiB,不重试;按 result.json 重算三个文件的哈希,要求 v2512;level 3 与 level 4 两套网格做四项检查。底部是来源说明。
一个 Ahmed 型车身算例从主机打包、在 AWS Batch 上求解、回传核对到四项检查的步骤,依据 pai-design-workbench commit f225835 的 docs/AERO.md、native/cfd_review.py 与 native/cfd_job.py 绘制;失败或超期的作业只报告、不重提,图中未画出。

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 路径的步骤是:

  1. 主机用 cfd_case.py 写出算例(字典文件与 body.stl),打包成 case.tar 上传到 S3 的 jobs/<run>/<name>-l<level>/ 前缀下。打包时排除 run.sh。
  2. 提交一个 Batch 作业。Dockerfile.cfd 以同一个固定 digest 的 OpenCFD 镜像为基础,只加 Python 和 boto3;每个作业 16 vCPU、32 GiB,不重试。
  3. 作业端 cfd_job.py 要求前缀以 jobs/ 开头且不含 ..;解包前只接受普通文件和目录,拒绝绝对路径和含 .. 的路径,链接因不是普通文件而被拒绝;代码注释写着成员应位于 case/ 下,但代码并不检查这一点。解包使用 filter="data",之后必须有 case/system/controlDict。运行的 run.sh 取自镜像自身,不取自输入。
  4. 作业写出 coefficient.dat、run.json、log.checkMesh 和 result.json(schema pai-cfd-job-1,含每个文件的 SHA-256、OpenFOAM 版本、镜像标签、耗时和 CPU 数),上传时附 ChecksumSHA256。
  5. 主机每 10 秒查询一次作业状态,默认期限 7200 秒。作业失败或超期都只报告,不重新提交。成功后要求 status 为 measured、openfoam 为 v2512,再逐个下载三个文件,与 result.json 中的摘要比对,不一致即报错。
  6. 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 361,3100.25751141.961
参照 25°level 4219,6220.23406352.5269.1
候选 12.5°level 361,5730.2536615261.6
候选 12.5°level 4221,3540.22926262.1169.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 描述。

链接