资讯动态

OpenFOAM二次开发教程(03):第一个求解器——从 icoFoam 骨架到能编译的 myFirstFoam

发布时间:2026/9/24 17:55:17 来源:尧图企业网站定制
OpenFOAM二次开发教程03第一个求解器——从 icoFoam 骨架到能编译的 myFirstFoam版本与事实声明求解器骨架的#include结构setRootCase.H、createTime.H、createMesh.H、createFields.H、initContinuityErrs.H来自官方课程讲义N. HåkanChalmers/OS-CFD对icoFoam结构的说明讲义指出除createFields.H外其余 include 都位于$FOAM_SRC/finiteVolume/lnInclude。OpenFOAM 在近几个大版本中演进较快例如引入统一求解器foamRun、模块化组织求解器不同版本的文件路径与部分 API 名称可能调整本文示例以经典icoFoam骨架为主落笔前请以本机$FOAM_SRC、applications/solvers中的同名文件为准。wmake、Make/files、Make/options、EXE $(FOAM_USER_APPBIN)/...的用法来自官方文档与课程讲义。验证算例使用官方算例库中的 cavity顶盖驱动方腔系例属$FOAM_TUTORIALS内容。一句话结论一个 OpenFOAM 求解器的骨架只有六行魔法——setRootCase.H、createTime.H、createMesh.H、createFields.H、initContinuityErrs.H五个 include 加一个while (runTime.loop())时间循环把它写进myFirstFoam.C、在Make/files里写EXE $(FOAM_USER_APPBIN)/myFirstFoam、执行wmake你就完成了第一次对 OpenFOAM 本体的修改。〇、本篇要解决的认知问题Q1OpenFOAM 求解器为什么长得这么像那五个.Hinclude 各自干了什么Q2createFields.H为什么是唯一每个求解器都不同的文件它到底做了什么魔法Q3把自定义求解器编译出来Make/files与Make/options最少要写什么Q4怎么验证我编译出来的求解器确实在工作而不是编译过了但没跑对Q5统一求解器foamRun出现后自己写一个求解器还有意义吗一、机制解析1.1 为什么所有求解器长得一样骨架即公共前戏打开icoFoam的源码你会发现它开头只有几行 include然后直接进时间循环#includefvCFD.H// 有限体积求解器的总头文件intmain(intargc,char*argv[]){#includesetRootCase.H// 解析命令行确定算例路径#includecreateTime.H// 创建时间对象 runTime#includecreateMesh.H// 创建有限体积网格 fvMesh#includecreateFields.H// 创建本求解器所需的场唯一“定制”部分#includeinitContinuityErrs.H// 初始化连续性误差累计量while(runTime.loop())// 时间推进主循环{// 具体物理方程装配与求解}return0;}为什么这对你重要这套骨架把所有求解器必须做的事找算例、建时间、建网格、建场、初始化误差抽成了公共前戏。理解它你就理解了两件事——第一写新求解器不用从零开始复制一个最接近的官方求解器改中间部分即可第二大部分启动就报错的问题都出在这几步找不到算例、字典缺失、场文件缺失排查时按这个顺序走。人工拆解这五个 include 的职责include职责出错时的典型现象setRootCase.H解析命令行参数与-case路径确定算例根目录找不到算例目录、“case not found”createTime.H从controlDict读时间控制起始/终止/步长构造runTimecontrolDict缺少startTime等必需项createMesh.H构造fvMesh读constant/polyMesh找不到polyMesh提示先跑blockMeshcreateFields.H创建本求解器需要的场p、U…缺少0/场名文件、“cannot find file”initContinuityErrs.H初始化连续性误差累计变量后续每步累加一般不出错缺了会在后续用到时报未定义1.2 createFields.H唯一的求解器指纹骨架里唯一因求解器而异的就是createFields.H——它在icoFoam目录里负责这个求解器需要哪些场。以icoFoam不可压缩层流为例它需要压力p与速度UInfoReading field p\nendl;volScalarFieldp(IOobject(p,// 场名对应 0/p 文件runTime.timeName(),// 起始时间目录名mesh,// 所属网格IOobject::MUST_READ,// 必须从磁盘读入IOobject::AUTO_WRITE// 每个写出时刻自动写出),mesh);InfoReading field U\nendl;volVectorFieldU(IOobject(U,runTime.timeName(),mesh,IOobject::MUST_READ,IOobject::AUTO_WRITE),mesh);#includecreatePhi.H// 由 U 与网格构造面通量 phi三个必须点透的机制IOobject是场的三重身份的载体名字p、所属网格mesh、读写策略MUST_READ/AUTO_WRITE。这决定了场去磁盘的哪里读、要不要写回。第 05 篇会专门解剖。MUST_READ是硬约束它要求0/p必须存在缺了就报 “cannot find file”。这就是新建求解器后忘了准备场文件这类报错的根因。phi由createPhi.H派生面通量不是独立物理量而是U与网格面积的乘积所以放在独立的createPhi.H里而非手写。经验法则新建求解器时先决定我需要哪些场再决定哪些场要写回磁盘。写回策略AUTO_WRITE/NO_WRITE直接决定后处理能拿到什么很多后处理里找不到某个场的问题其实出在这里。1.3 从骨架到能编译三个文件一个可编译的自建求解器工程在磁盘上只需要三个文件myFirstFoam/ ├── myFirstFoam.C # 主程序骨架 时间循环 ├── createFields.H # 场定义求解器指纹 └── Make/ ├── files # 编译什么、产物放哪 └── options # 头文件路径、链接库Make/files的最小写法铁律 4产物落用户目录myFirstFoam.C EXE $(FOAM_USER_APPBIN)/myFirstFoamMake/options的最小写法求解器是可执行文件用EXE_INC/EXE_LIBSEXE_INC \ -I$(LIB_SRC)/finiteVolume/lnInclude EXE_LIBS \ -lfiniteVolume然后在myFirstFoam/目录执行wmake编译成功后可执行文件出现在$(FOAM_USER_APPBIN)/myFirstFoam可以直接用myFirstFoam -case 算例路径运行——因为 OpenFOAM 的应用都走同一套setRootCase.H所以你的求解器和官方的用法完全一致。这是骨架化设计的直接红利。1.4 为什么还要自己写求解器foamRun之后的定位现代 OpenFOAM 引入了统一求解器foamRun官方 API 文档对它的描述是Loads and executes an OpenFOAM solver module either specified by the optionalsolverentry in thecontrolDictor as a command-line argument且使用灵活的 PIMPLE 框架。这意味着很多标准物理不可压缩、可压缩、多相等已经模块化到可以用一个入口按需加载。那自己写求解器还有意义吗有而且意义更清晰了标准物理不用重写能用foamRun 模块解决的不要自己写第 01 篇决策表的选择逻辑非标准物理必须自己写当你要实现一个新的控制方程新输运方程、新耦合机制或要改动标准求解器的方程结构时仍是写求解器理解骨架是理解一切的前提即使用foamRun你也要靠骨架知识去读它的源码、判断它如何装配方程。最佳实践新求解器永远从最接近的官方求解器复制改造。要写层流不可压就从icoFoam抄要写带源项的标量输运就找个带标量输运的官方求解器抄。从零开始写 main 是在浪费生命——而且极容易漏掉某个初始化步骤导致诡异错误。1.5 自建求解器的命名、目录位置与长期维护编译出第一个求解器之后紧接着的问题就是把它放在哪里、叫什么名字。这三条约定能省掉未来大量麻烦① 名字要能自解释且不与官方冲突。官方求解器名如icoFoam、simpleFoam已经被占用在自己的环境里。你的自建求解器应使用唯一前缀例如团队/项目缩写 物理含义例如myScalarTransportFoam而不是scalarFoam。理由是当你的名字与某个官方或另一条发行线的求解器重名时PATH里谁先被找到是不确定的——这会表现为改了代码没生效这种极难排查的现象。② 源码放你自己的代码仓库不放 OpenFOAM 安装目录。把自建求解器写进$WM_PROJECT_DIR/applications/下的做法看似方便实则灾难升级或重装会覆盖/丢失你的代码而且你的改动与官方代码混在一起无法用版本控制区分。正确做法独立仓库 编译产物落$FOAM_USER_APPBIN铁律 4。③ 每个求解器目录都自带Make/与createFields.H。这三个文件.C、createFields.H、Make/{files,options}构成一个自包含单元——它不依赖任何外部路径就能编译。这是可交付的最小单位把这三个文件复制到任何一台装好 OpenFOAM 的机器上wmake就能重建它。最佳实践为每个自建求解器写一个极简README.md记录三件事——基于哪个官方求解器改造的、改了哪几处、用哪个算例验证过。第 10、20 篇会把这条纪律扩展到库与整个工具链它的价值是几个月后你自己还会感谢自己。1.6 可视化与后处理产物别忘了它们也会失控自建求解器通常会开启多个场的AUTO_WRITE再加上函数对象第 12 篇的产物磁盘占用会以你意想不到的速度增长。三条廉价但有效的控制手段只写你真正需要的场把中间量设为NO_WRITE、用writeInterval控制写出频率、用函数对象的时间窗timeStart/timeEnd限定监测区间。第 15 篇会从性能角度再谈这件事。二、完整代码与逐行剖析代码 2-1myFirstFoam.C最小可编译求解器/*---------------------------------------------------------------------------*\ myFirstFoam.C —— 最小可编译求解器骨架教学版 结构取自官方 icoFoam 的经典骨架本版本只做骨架演示不实现完整湍流物理。 \*---------------------------------------------------------------------------*/#includefvCFD.H// 有限体积求解器公共头文件fvMesh、volField、fvm/fvc 等// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //intmain(intargc,char*argv[]){// ---- 公共前戏这五行几乎所有 OpenFOAM 求解器都有 ----#includesetRootCase.H// 解析 -case 等命令行参数#includecreateTime.H// 由 system/controlDict 建立 runTime#includecreateMesh.H// 读 constant/polyMesh 建立 fvMesh#includecreateFields.H// 建立本求解器所需的场#includeinitContinuityErrs.H// 初始化连续性误差累计量// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //Info\nStarting time loop\nendl;// 时间推进主循环runTime.loop() 按 controlDict 的 startTime/endTime/deltaT 迭代while(runTime.loop()){InfoTime runTime.timeName()nlendl;// ---- 物理实现区本例留空这是你写方程的地方----// 例如求解输运方程、更新湍流量、计算目标统计量……runTime.write();// 按 writeControl 写出当前时刻的场InfoExecutionTime runTime.elapsedCpuTime() s ClockTime runTime.elapsedClockTime() snlendl;}InfoEnd\nendl;return0;}// ************************************************************************* //逐行剖析#include fvCFD.H是唯一需显式写的总头文件它聚合了有限体积离散fvm/fvc、场类型、网格类型等公共设施。没有它volScalarField之类都无从定义。五个#include xxx.H之所以能这么写不带路径是因为wmake通过Make/options里的-I.../lnInclude把库头文件目录加进了搜索路径。这也是为什么Make/options写错会导致找不到 fvCFD.H。runTime.loop()是时间循环的规范写法它内部处理当前时间是否超过endTime、是否到达写出时刻等逻辑。不要自己写for循环推进时间——那会绕过 OpenFOAM 的时间控制与写出机制。末尾打印ExecutionTime与ClockTime是 OpenFOAM 官方求解器的惯例CPU 时间与墙钟时间的差异是判断是否被 IO 或 MPI 阻塞的第一手线索第 15 篇性能优化会深入。物理区留空是故意的本篇目标是跑通编译与运行第 06、07 篇会往这个位置填fvMatrix方程。代码 2-2createFields.H本求解器的场定义/*---------------------------------------------------------------------------*\ createFields.H —— myFirstFoam 的场定义 放在求解器源码目录与 myFirstFoam.C 同级由后者 #include。 \*---------------------------------------------------------------------------*/InfoReading field p\nendl;volScalarFieldp(IOobject(p,// 场名对应算例 0/p 与 constant/ 下的边界类型定义runTime.timeName(),// 当前时间名起始一般为 0mesh,// 依附的网格对象IOobject::MUST_READ,// 必须从磁盘读缺文件直接报错避免“静默用默认值”IOobject::AUTO_WRITE// 到写出时刻自动写回磁盘),mesh);InfoReading field U\nendl;volVectorFieldU(IOobject(U,runTime.timeName(),mesh,IOobject::MUST_READ,IOobject::AUTO_WRITE),mesh);#includecreatePhi.H// 由 U 与网格构造面通量 phi不可压缩求解器的标准动作逐行剖析Info是 OpenFOAM 的日志流它同时受controlDict的日志级别控制是求解器说什么的规范通道比std::cout更符合 OpenFOAM 习惯。volScalarField/volVectorField是体心场类型值定义在网格单元中心。对应的还有surfaceScalarField面心场用于通量第 05 篇详述。IOobject的五个参数顺序固定为名字、时间名、所属对象注册表这里传mesh、读策略、写策略。写错顺序不会报错但语义全变是新手高危点——因为很多构造函数参数类型相近。MUST_READ而非READ_IF_PRESENT这是最佳实践。用READ_IF_PRESENT在缺文件时会静默用默认值把配置错误伪装成物理结果是无声错误的温床。#include createPhi.H放在createFields.H末尾因为phi的定义依赖U已存在顺序不可颠倒C 声明顺序敏感。代码 2-3编译与验证脚本POSIX Shell#!/bin/sh# build_and_test.sh —— 编译 myFirstFoam 并在官方 cavity 算例上冒烟测试# 用法sh build_and_test.shset-euSRC_DIR${1:-$PWD}# 求解器源码目录默认当前目录echo 1. 编译 cd$SRC_DIRwmake# 读 Make/files 与 Make/options增量编译test-x$FOAM_USER_APPBIN/myFirstFoam# 铁律 4产物必须落在用户目录echo[OK] 产物$FOAM_USER_APPBIN/myFirstFoamecho 2. 准备一个官方算例副本不改动原算例# 自行指定一个 cavity 系官方算例路径不同版本目录层级不同以本机 tutorials 为准TUT${CAVITY_TUT:-$FOAM_TUTORIALS/incompressible/icoFoam/cavity/cavity}WORK$PWD/_smoke_cavityrm-rf$WORKcp-r$TUT$WORK# 复制保护官方算例铁律 7 的“基准不被污染”cd$WORKecho 3. 生成网格并检查 blockMeshlog.blockMesh21checkMeshlog.checkMesh21grep-qMesh OKlog.checkMeshecho[OK] checkMesh: Mesh OK\||{echo[WARN] checkMesh 未报 Mesh OK请查看 log.checkMesh;}echo 4. 用自建求解器运行 myFirstFoamlog.myFirstFoam21echo[OK] 求解器已运行日志尾部tail-n8log.myFirstFoamecho 5. 结果目录 ls-1d[0-9]*2/dev/null|tail-n5|seds/^/ 时间目录: /逐行剖析set -eu出错即停-e 未定义变量即停-u。验证脚本最忌讳前面失败了后面还在跑最后给出误导性的成功结论。test -x $FOAM_USER_APPBIN/myFirstFoam用可执行位存在作为编译成功的客观判据比看日志没报 error更硬铁律 4 的自动化检查。cp -r $TUT $WORK永远复制官方算例不在原算例上动手。这既是保护基准铁律 7也是避免改了官方 tutorials 后忘了导致后续所有验证都基于被污染的算例。blockMeshcheckMesh是标准两口先建网格再验网格。checkMesh输出 “Mesh OK” 是网格质量通过的官方判据。把求解器输出重定向到log.myFirstFoam是 OpenFOAM 的惯例官方runApplication也这么做便于事后grep排查。最后列出时间目录有没有产生新的时间目录是求解器是否真的推进了时间的客观证据——比进程没崩强得多。三、常见报错与排查报错 3-1fatal error: fvCFD.H: No such file or directory。现象wmake编译报找不到总头文件。根因Make/options里没写-I$(LIB_SRC)/finiteVolume/lnInclude或EXE_INC变量名写错。解法检查Make/options是否为EXE_INC ...可执行文件用EXE_INC不是LIB_INC确认路径变量$(LIB_SRC)在该版本中有效以本机官方求解器的Make/options为准直接抄一份最稳妥。报错 3-2-- FOAM FATAL IO ERROR: cannot find file .../0/p。现象运行求解器时找不到场文件。根因求解器的createFields.H声明了MUST_READ的场但算例的0/目录里没有对应文件或你复制的算例来自另一个求解器场集合不同。解法对照createFields.H里声明的场名在0/下补齐可从同类官方算例复制p、U等并修改边界条件或改用READ_IF_PRESENT不推荐见 §二.2。报错 3-3-- FOAM FATAL ERROR: ... keyword ... undefined in dictionary ...。现象启动时某字典键缺失。根因求解器要求的字典项如fvSchemes中的某个离散格式、fvSolution中的求解器条目在你的算例里没有。解法打开报错给出的字典文件路径与行号按官方同类算例补齐相应条目不要凭空猜键名去官方算例或 Doxygen 里找铁律 1。报错 3-4编译通过但运行时报segmentation fault。现象求解器启动即崩无 FOAM FATAL 信息。根因常见于场初始化顺序错误例如createFields.H里在U之前使用了依赖U的量或误传了IOobject参数导致对象未正确构造。解法把createFields.H与官方同类求解器逐行对照特别检查#include createPhi.H之类依赖顺序的位置用gdb --args myFirstFoam -case 算例看崩在哪一行。报错 3-5-- FOAM FATAL IO ERROR: cannot find file .../constant/polyMesh/points。现象提示先跑blockMesh。根因尚未生成网格。解法在算例目录先执行blockMesh用ls constant/polyMesh确认有points、faces、owner、neighbour、boundary等文件。这是忘了建网格的经典报错。四、动手练习练习 1最小可编译把代码 2-1、2-2 与本篇的Make/files、Make/options抄到自建目录执行wmake。判定编译无 error$FOAM_USER_APPBIN/myFirstFoam存在且具备可执行权限。练习 2冒烟测试运行代码 2-3build_and_test.sh。判定脚本输出checkMesh: Mesh OKlog.myFirstFoam末尾出现End算例目录在运行后不产生新的时间目录因为本例未实现物理、也没有触发额外写出但求解器不崩溃、退出码为 0。练习 3给它一点物理在while循环的物理区加一行p p;或任意合法赋值重新wmake并运行。判定求解器输出中Time ...多行递增且当controlDict的写出时刻到达时生成相应时间目录说明runTime.write()生效。练习 4对照官方把你写的myFirstFoam.C与本机icoFoam的源码并排对比。判定能指出你的骨架与icoFoam的三个相同点五个 include、时间循环、写出调用与两个不同点物理区为空、createFields.H场集合不同并用 3 句话说明差异原因。练习 5思考题无标准答案假设你要实现带一个被动标量T的输运求解器列出你需要在createFields.H中新增的内容。验证要点(a) 是否新增volScalarField T并指明MUST_READ(b) 是否考虑T的输运需要速度场U与面通量phi© 是否考虑T的边界条件类型与p/U的差异第 07 篇会给出完整答案。五、小结与下一篇预告本篇完成了本系列最重要的一次跨越你不再只是 OpenFOAM 的使用者而是能编译出自己求解器的开发者。核心记忆点有三个——骨架五行setRootCase.H/createTime.H/createMesh.H/createFields.H/initContinuityErrs.H、唯一指纹createFields.H定义了求解器需要哪些场、三文件工程.CCreateFields.HMake/{files,options}以及一条纪律产物只落$FOAM_USER_APPBIN铁律 4。第 04 篇《算例即配置》将补上你理解 OpenFOAM 的最后一块拼图0/、constant/、system/三个目录各管什么controlDict/fvSchemes/fvSolution/blockMeshDict的职责如何划分以及如何用foamDictionary在脚本里安全地读写这些字典——那是把手改配置升级为自动改配置的第一步。本篇认知问题回显FAQQ1OpenFOAM 求解器的骨架包含哪些 include各自干什么A经典骨架有五个 includesetRootCase.H 解析命令行与 -case 路径createTime.H 依据 system/controlDict 建立时间对象 runTimecreateMesh.H 读 constant/polyMesh 建立 fvMeshcreateFields.H 建立本求解器所需的场唯一因求解器而异的部分initContinuityErrs.H 初始化连续性误差累计量。主程序随后进入 while (runTime.loop()) 时间循环。除 createFields.H 外其余 include 通常位于 $FOAM_SRC/finiteVolume/lnInclude。Q2createFields.H 为什么是每个求解器都不同的文件A因为它定义这个求解器需要哪些场。例如 icoFoam 用 volScalarField p 与 volVectorField U再用 createPhi.H 由 U 与网格构造面通量 phi。场的创建通过 IOobject 指定名字、时间名、所属网格与读写策略如 MUST_READ、AUTO_WRITE。它决定了求解器去 0/ 目录读哪些文件、又写回哪些文件因此是求解器的指纹。缺场的报错cannot find file ./0/p就源于此。Q3编译自定义求解器Make/files 与 Make/options 最少写什么AMake/files 写源文件名与产物位置最少两行myFirstFoam.C和EXE $(FOAM_USER_APPBIN)/myFirstFoam。Make/options 写头文件路径与链接库因为是可执行文件用EXE_INC -I$(LIB_SRC)/finiteVolume/lnInclude与EXE_LIBS -lfiniteVolume库则用 LIB_INC/LIB_LIBS。随后在该目录运行 wmake 即可产物出现在 FOAM_USER_APPBIN。若把 EXE 指向系统目录升级时会丢失并污染安装。Q4如何验证自建求解器确实在工作而不是编译过了但没跑对A用客观判据而非感觉。一是产物判据确认 $(FOAM_USER_APPBIN)/myFirstFoam 存在且可执行二是网格判据在复制的官方算例上跑 blockMesh 与 checkMeshcheckMesh 输出 “Mesh OK”三是运行判据求解器日志以 End 正常结束、退出码为 0四是推进判据日志中 Time 递增且到达写出时刻时目录中出现新的时间目录。全程必须在官方算例副本上验证且不改动原算例。Q5有了统一求解器 foamRun还需要自己写求解器吗A需要但定位更清晰。官方 API 文档描述 foamRun 会按 controlDict 的 solver 项或命令行参数加载求解器模块并用灵活的 PIMPLE 框架因此标准物理不可压缩、可压缩、多相等应优先用 foamRun 加模块解决不必自写。但当需要实现新的控制方程或改动方程结构非标准物理时仍要写求解器或求解器模块。此外掌握骨架是读懂 foamRun 源码与判断其方程装配方式的前提新求解器应从最接近的官方求解器复制改造而不是从零写 main。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价