OpenFOAM 源码深度剖析 · 第二季-15 实战:手写一个求解器、库与边界条件
前十四节把 OpenFOAM 的"零件"一件件拆开讲清了:时间离散(S2-06)、非正交修正(S2-07)、对流格式(S2-05)、梯度格式(S2-04)、fvOptions(S2-13)、函数对象与控制框架(S2-14)......但读者真正的第一诉求始终没被正面回答------"我到底怎么亲手写出一个能跑起来的求解器/库/边界条件?" 本章就是回答它。
我们不从零发明,而是把仓库里最小的完整求解器
laplacianFoam当作"麻雀"做彻底解剖:它的每一个文件、每一行代码、每一条#include片段、每一处 Makefile 语法、每一次链接顺序,全部落到真实文件与真实行号。然后把它作为"可复制模板",向外扩展到两类最常见的自定义工作:自定义库(写模型/函数对象) 与 自定义边界条件(fvPatchField派生类) 。最后给出时间步自适应(CourantNo.H/setDeltaT.H/setRDeltaT.H)的完整机制链。本章所有路径、行号、代码摘录均来自本仓库(OpenFOAM master 分支,
WM_PROJECT_VERSION=v2606,C++17),已逐个 Read 核对,不确定的一律不写。
全章地图
本章按"解剖 → 机制 → 模板 → 排错"的顺序展开 18 节:
- 为什么从
laplacianFoam讲起:最小完整求解器的边界。 - 文件构成:一个求解器到底由几个文件组成(真实清单)。
- 头文件三行解剖:
fvCFD.H/fvOptions.H/simpleControl.H。 main()骨架逐行:argList→Time→fvMesh→ 控制对象。- 循环机制:
simple.loop()与correctNonOrthogonal()的状态机。 - 方程组装:
fvm::ddt−fvm::laplacian==fvOptions。 solve()与fvSolution:从fvMatrix到线性求解器。createFields.H逐行:IOobject五参数、默认值与字典回退。write.H逐行:writeTime()与"梯度过热"的写出片段。- 机制链 A :求解流程全链(
fvSchemes选格式 →fvm::laplacian出fvMatrix→solve→ 写场)。 - 机制链 B :
Make/files的EXE/LIB语法与 wmake 链接顺序、lnInclude。 - 机制链 C :自定义
fvPatchField的类骨架(构造/updateCoeffs/evaluate/clone/write)。 - 机制链 D :
CourantNo.H的Co计算与setDeltaT.H/setRDeltaT.H自适应时间步。 - 可直接复制的完整文件模板(Make/files、Make/options、createFields.H、主文件、边界条件 .H/.C、controlDict 段)。
- 对比与选型表(≥5 张)。
- 常见报错/警告速查(≥20 条:成因/排查/解决)。
- 从
laplacianFoam改造:scalarTransportFoam对照。 - 调试、验证与小结。
1. 为什么从 laplacianFoam 讲起
laplacianFoam 位于 applications/solvers/basic/laplacianFoam/。它的官方描述(laplacianFoam.C 第 33--53 行)就一句核心:
css
Laplace equation solver for a scalar quantity.
...
\ddt{T} = \div \left( D_T \grad T \right)
即求解标量场 T 的瞬态热传导方程(无对流、无源项,只有时间项与扩散项)。为什么拿它当"教学标本"?因为它满足三个条件:
- 它是最小的"完整求解器" :具备
Time/fvMesh/场/方程/循环/写出的全部骨架要素,一个都不缺。 - 它没有任何"专业包袱" :不像
icoFoam要处理压力-速度耦合、不像rhoCentralFoam要处理可压缩通量,laplacianFoam的物理就是一步扩散。 - 它被仓库自己当作"参考骨架" :搜索仓库里
fvm::laplacian(DT, T)的写法、IOobject的建场写法、Make/options的库列表,都是从这个文件抄出去的。
一句话定位 :
laplacianFoam是"最小可编译、可运行、可并行"的求解器,理解了它,你就理解了 OpenFOAM 求解器 90% 的骨架代码。剩下 10% 是各物理模型特有的方程与场。
1.1 "最小完整"到底最小在哪
我们把一个 OpenFOAM 求解器必须具备的要素列出来,并标注 laplacianFoam 的对应物:
| 必备要素 | 作用 | laplacianFoam 中的对应 |
文件/行 |
|---|---|---|---|
| 命令行解析 | 定位 case、处理 -parallel |
setRootCaseLists.H |
laplacianFoam.C 第 73 行 |
| 时间对象 | 读 controlDict、推进时间 |
createTime.H |
第 74 行 |
| 网格对象 | 读 constant/polyMesh |
createMesh.H |
第 75 行 |
| 迭代控制 | 判断是否收敛/退出 | simpleControl simple(mesh) |
第 77 行 |
| 场创建 | 建 T、DT |
createFields.H |
第 79 行 |
| 方程组装 | 离散成矩阵 | fvScalarMatrix TEqn(...) |
第 91--96 行 |
| 线性求解 | 调用求解器 | TEqn.solve() |
第 99 行 |
| 源项/约束 | fvOptions 三调用 |
constrain/solve/correct |
第 98--100 行 |
| 写出 | 落盘 | write.H |
第 103 行 |
| 计时 | 打印耗时 | printExecutionTime |
第 105 行 |
这张表就是"求解器骨架"的权威定义:任何求解器(包括你未来要写的)都必须覆盖这 10 项,区别只在于"场有几个、方程几项、控制对象是哪一种"。
1.2 学习路径:先克隆,再变异
对绝大多数读者而言,"从零敲一个求解器"是效率最低的路径。仓库自身的实践是:复制一个最接近的求解器,改方程、改场、改控制对象。因此本章的方法论是:
css
读透 laplacianFoam(本章第 2--9 节)
│
├─▶ 要瞬态对流扩散?→ 对照 scalarTransportFoam(第 17 节)
├─▶ 要自定义模型/库?→ 机制链 B + 模板(第 11、14 节)
├─▶ 要自定义边界?→ 机制链 C + 模板(第 12、14 节)
└─▶ 要自适应时间步?→ 机制链 D(第 13 节)
2. 文件构成:一个求解器到底由几个文件组成
很多人以为"写求解器=写一个大 .C 文件"。打开 laplacianFoam 目录会发现远不止如此。真实清单如下(applications/solvers/basic/laplacianFoam/):
| 文件 | 类型 | 作用 |
|---|---|---|
laplacianFoam.C |
主程序 | main() 骨架 + 方程 + 循环 |
createFields.H |
代码片段 | 建场(T、DT)+ fvOptions |
write.H |
代码片段 | 写盘逻辑(gradT、flux) |
Make/files |
构建描述 | 源文件清单 + 产物目标(EXE/LIB) |
Make/options |
构建描述 | include 路径(EXE_INC)+ 链接库(EXE_LIBS) |
注意:目录里没有 .H 头文件的"声明/实现分离"------createFields.H、write.H 不是"被编译的头文件",而是在 main() 里被 #include 展开的代码片段。这是 OpenFOAM 求解器特有的"文本级组合"风格,第 9、10 节会专门讲它的利弊。
2.1 两个 Make 文件的真实内容
laplacianFoam/Make/files(全 3 行):
ini
laplacianFoam.C
EXE = $(FOAM_APPBIN)/laplacianFoam
laplacianFoam/Make/options(全 8 行):
bash
EXE_INC = \
-I$(LIB_SRC)/finiteVolume/lnInclude \
-I$(LIB_SRC)/meshTools/lnInclude
EXE_LIBS = \
-lfiniteVolume \
-lfvOptions \
-lmeshTools
读法(细节见第 11 节):
Make/files第一行laplacianFoam.C是源文件清单 (只列.C,不列.H);EXE = ...是产物声明 (这里产出可执行文件到$FOAM_APPBIN)。Make/options里EXE_INC指向各库的lnInclude(lnInclude是 wmake 自动生成的"符号链接头文件目录");EXE_LIBS用-l声明要链接的库。
2.2 一个容易被忽略的事实:库列表不是"越多越好"
注意 Make/options 里出现了 -lmeshTools,可 laplacianFoam 的方程里并没有网格工具调用。为什么还留着?因为头文件 lnInclude 与链接库是两回事 :createMesh.H 等片段间接依赖 meshTools 提供的符号,链接器在最终链接阶段需要它们。少写一个库会直接触发"undefined reference"(第 16 节),这是新手第一大坑。
经验法则 :
Make/options的库列表,直接照抄同目录下一个相干求解器即可,不要凭想象删减。
3. 头文件三行解剖
laplacianFoam.C 第 57--59 行的三行 #include,是整个求解器的"入口依赖":
cpp
#include "fvCFD.H"
#include "fvOptions.H"
#include "simpleControl.H"
3.1 fvCFD.H:一键引入全家桶
fvCFD.H 的真实内容(src/finiteVolume/cfdTools/general/include/fvCFD.H,全 34 行,去掉许可证后核心是第 4--33 行):
cpp
#include "Time.H"
#include "fvMesh.H"
#include "fvc.H"
#include "fvMatrices.H"
#include "fvm.H"
#include "linear.H"
#include "uniformDimensionedFields.H"
#include "calculatedFvPatchFields.H"
#include "extrapolatedCalculatedFvPatchFields.H"
#include "fixedValueFvPatchFields.H"
#include "zeroGradientFvPatchFields.H"
#include "fixedFluxPressureFvPatchScalarField.H"
#include "constrainHbyA.H"
#include "constrainPressure.H"
#include "adjustPhi.H"
#include "findRefCell.H"
#include "IOMRFZoneList.H"
#include "constants.H"
#include "gravityMeshObject.H"
#include "columnFvMesh.H"
#include "OSspecific.H"
#include "argList.H"
#include "timeSelector.H"
#ifndef namespaceFoam
#define namespaceFoam
using namespace Foam;
#endif
逐行解读:
| 行 | include | 引入的东西 |
|---|---|---|
| 4 | Time.H |
时间对象 Time(runTime 的类型) |
| 5 | fvMesh.H |
有限体积网格 fvMesh(mesh 的类型) |
| 6 | fvc.H |
显式算子(fvc::grad/fvc::div/fvc::flux...) |
| 7 | fvMatrices.H |
矩阵族(fvMatrix/fvScalarMatrix/fvVectorMatrix) |
| 8 | fvm.H |
隐式算子(fvm::ddt/fvm::laplacian/fvm::div...) |
| 9 | linear.H |
插值/梯度格式基类(linear/corrected 等词汇) |
| 10 | uniformDimensionedFields.H |
均匀量纲场(如 g) |
| 11--14 | 边界条件头 | calculated/extrapolatedCalculated/fixedValue/zeroGradient 四个基础边界 |
| 15 | fixedFluxPressureFvPatchScalarField.H |
固定通量压力边界(压力-速度耦合用) |
| 16--17 | constrainHbyA.H/constrainPressure.H |
压力方程的 HbyA/压力约束辅助 |
| 18 | adjustPhi.H |
通量修正(保证连续性) |
| 19 | findRefCell.H |
找参考单元(纯 Neumann 压力场用) |
| 20 | IOMRFZoneList.H |
多重旋转参考系区(MRF) |
| 21 | constants.H |
pi、GREAT、SMALL、VGREAT 等常量 |
| 22 | gravityMeshObject.H |
重力场对象 |
| 24 | columnFvMesh.H |
一维简化网格(-dry-run 用,见第 4 节) |
| 26--28 | OSspecific.H/argList.H/timeSelector.H |
操作系统、命令行、时间选择器 |
| 31--33 | namespaceFoam 宏 |
若无 namespaceFoam 宏,则定义它并 using namespace Foam; |
结论 :fvCFD.H 不是"头文件",而是一份"有限体积应用的标准依赖清单" 。它的存在让求解器作者不必逐行 include 二十多个头,只写一行即可。代价是编译依赖变大 (改任何基础头都会触发重编),这在现代预编译头(-include/PCH)机制下可接受。
衍生点 :如果你的求解器用不到
fvOptions/simpleControl,第 58、59 行可以删;但fvCFD.H那行几乎不能删 (它是Time/fvMesh/fvm/fvc的唯一来源)。
3.2 fvOptions.H 与 simpleControl.H
fvOptions.H:fv::options家族(S2-13)。include后可用fvOptions(TEqn)/fvOptions.constrain(TEqn)/fvOptions.correct(T)。simpleControl.H:SIMPLE 稳态迭代控制对象(S2-14)。它定义了simpleControl类,并提供SIMPLECONTROL相关宏。
这两个头按需引入 :不用 fvOptions 就删第 58 行;稳态用 simple、瞬态用 pimpleControl/pisoControl(S2-14)则换第 59 行。
4. main() 骨架逐行
laplacianFoam.C 第 63--111 行是完整的 main():
cpp
int main(int argc, char *argv[])
{
argList::addNote
(
"Laplace equation solver for a scalar quantity."
);
#include "postProcess.H"
#include "addCheckCaseOptions.H"
#include "setRootCaseLists.H"
#include "createTime.H"
#include "createMesh.H"
simpleControl simple(mesh);
#include "createFields.H"
// * * * * * * * * * * * //
Info<< "\nCalculating temperature distribution\n" << endl;
while (simple.loop())
{
Info<< "Time = " << runTime.timeName() << nl << endl;
while (simple.correctNonOrthogonal())
{
fvScalarMatrix TEqn
(
fvm::ddt(T) - fvm::laplacian(DT, T)
==
fvOptions(T)
);
fvOptions.constrain(TEqn);
TEqn.solve();
fvOptions.correct(T);
}
#include "write.H"
runTime.printExecutionTime(Info);
}
Info<< "End\n" << endl;
return 0;
}
逐段解读:
4.1 第 65--68 行:argList::addNote
argList::addNote(...) 给命令行 -help 添加一行描述。它是静态方法 (在 main 开头、args 构造之前调用)。注意这里用的是 addNote,而 addCheckCaseOptions.H(第 72 行)内部调用的是 argList::addBoolOption 等选项注册 方法。二者都在 args 构造前执行------这是 OpenFOAM 命令行选项的"注册-解析"两阶段模式。
4.2 第 70 行:postProcess.H
cpp
#include "postProcess.H"
它让求解器支持"只做后处理 "模式(postProcess -func ...),会在 main 里插入一段判断:若检测到 -postProcess,就只执行函数对象(S2-14)然后退出,不进入主循环。这是"求解器兼作后处理工具"的标准开关,新写求解器建议保留。
4.3 第 72 行:addCheckCaseOptions.H
它向 args 注册 -dry-run(addDryRunOption)与 -dry-run-write(addBoolOption)等选项,真实内容(src/OpenFOAM/include/addCheckCaseOptions.H 第 21--29 行):
cpp
Foam::argList::addDryRunOption
(
"Check case set-up only using a single time step"
);
Foam::argList::addBoolOption
(
"dry-run-write",
"Check case set-up and write only using a single time step"
);
-dry-run 的语义在 createMesh.H 里落地(见下)。
4.4 第 73 行:setRootCaseLists.H
它展开为三段(src/OpenFOAM/include/setRootCaseLists.H 第 1--5 行):
cpp
// This is setRootCase, but with additional solver-related listing
#include "setRootCaseListOptions.H"
#include "setRootCase.H"
#include "setRootCaseListOutput.H"
其中 setRootCase.H 是最核心的(第 5--20 行):
cpp
Foam::argList args(argc, argv);
if (!args.checkRootCase())
{
Foam::FatalError.exit();
}
// Force dlOpen of FOAM_DLOPEN_LIBS (principally for Windows applications)
#include "foamDlOpenLibs.H"
逐行:
- 第 5 行
Foam::argList args(argc, argv);:构造命令行对象args,解析-case/-parallel/-roots等并定位算例根目录。 - 第 6--9 行:
checkRootCase()校验system/controlDict是否存在,失败则FatalError.exit()。 - 第 20 行
foamDlOpenLibs.H:在 Windows 上强制dlOpen一些库(与 S2-16 的.def导出呼应)。
关键 :从这一行之后,main 作用域里就有了 args,后续所有 #include 片段都能用它。这就是"片段式组合"的隐式依赖------片段之间靠局部变量名通信。
4.5 第 74 行:createTime.H
真实内容(src/OpenFOAM/include/createTime.H 全 3 行):
cpp
Foam::Info<< "Create time\n" << Foam::endl;
Foam::Time runTime(Foam::Time::controlDictName, args);
它构造 Time runTime(读 system/controlDict),并打印 "Create time"。注意 Time::controlDictName 就是常量 "controlDict"------这就是 Time 构造时去 system/ 目录找字典的名字来源。
4.6 第 75 行:createMesh.H(一个信息量极大的片段)
createMesh.H(src/OpenFOAM/include/createMesh.H,核心第 32--102 行)比想象中复杂------它同时处理正常模式与 -dry-run:
cpp
Foam::autoPtr<Foam::fvMesh> meshPtr(nullptr);
Foam::word regionName
(
args.getOrDefault<word>("region", Foam::polyMesh::defaultRegion)
);
if (args.dryRun() || args.found("dry-run-write"))
{
Foam::Info
<< "Operating in 'dry-run' mode: case will run for 1 time step. "
<< "All checks assumed OK on a clean exit" << Foam::endl;
Foam::FieldBase::allowConstructFromLargerSize = true;
// Create a simplified 1D mesh and attempt to re-create boundary conditions
meshPtr.reset
(
new Foam::simplifiedMeshes::columnFvMesh(runTime, regionName)
);
if (args.found("dry-run-write"))
{
runTime.stopAt(Foam::Time::saWriteNow);
meshPtr().setInstance(runTime.timeName());
}
else
{
runTime.stopAt(Foam::Time::saNoWriteNow);
}
Foam::functionObject::outputPrefix = "postProcessing-dry-run";
}
else
{
Foam::Info << "Create mesh";
...
meshPtr.reset
(
new Foam::fvMesh
(
Foam::IOobject
(
regionName,
runTime.timeName(),
runTime,
Foam::IOobject::MUST_READ
),
false
)
);
meshPtr().init(true); // initialise all (lower levels and current)
}
Foam::fvMesh& mesh = meshPtr();
逐段:
- 第 32 行:
meshPtr是autoPtr<fvMesh>(智能指针),先置空。 - 第 35--38 行:
regionName取命令行-region,默认defaultRegion(即"region0")。 - 第 40--73 行:
-dry-run分支 ------不读真实网格,而是构造一个 1D 简化网格columnFvMesh,只跑 1 步用以校验算例设置(边界类型、字典语法等)。saNoWriteNow/saWriteNow决定是否写盘。 - 第 74--100 行:正常分支 ------
new fvMesh(IOobject(regionName, runTime.timeName(), runTime, MUST_READ), false)。这里MUST_READ表示必须能读到constant/polyMesh。构造后meshPtr().init(true)完成全部初始化。 - 第 102 行:
Foam::fvMesh& mesh = meshPtr();------最终main作用域得到变量mesh。
理解要点 :
createMesh.H结束时,main里就有了mesh(类型fvMesh&)。这是"片段式组合"的第二个变量:runTime、mesh。
4.7 第 77 行:构造控制对象
cpp
simpleControl simple(mesh);
simpleControl(src/finiteVolume/cfdTools/general/solutionControl/simpleControl/simpleControl.H)构造签名(第 97--102 行)为:
cpp
simpleControl
(
fvMesh& mesh,
const word& dictName = "SIMPLE",
const bool verbose = true
);
即默认从 fvSolution 的 SIMPLE 子字典读控制参数。它继承自 solutionControl,提供 loop() 与 correctNonOrthogonal()(第 5 节详解)。这一行决定了求解器是"稳态 SIMPLE 型" ;瞬态则换 pimpleControl/pisoControl。
4.8 第 79 行:createFields.H
见第 8 节。
4.9 第 85--106 行:主循环
外层 while (simple.loop())(第 85 行)是稳态外迭代 ;内层 while (simple.correctNonOrthogonal())(第 89 行)是非正交修正循环 。二者语义在第 5 节展开。循环体里做三件事:组装并求解 TEqn、写出、计时。
4.10 第 108--110 行:收尾
Info<< "End\n" 与 return 0;------标准退出。
5. 循环机制:simple.loop() 与 correctNonOrthogonal()
上面 main 里最"玄"的两行是 while (simple.loop()) 与 while (simple.correctNonOrthogonal())。它们都不是简单的计数器,而是状态机。
5.1 simpleControl::loop()(逐行解读)
simpleControl.C(src/finiteVolume/cfdTools/general/solutionControl/simpleControl/simpleControl.C)第 136--161 行:
cpp
bool Foam::simpleControl::loop()
{
solutionControl::setFirstIterFlag(true, true);
read();
Time& runTime = const_cast<Time&>(mesh_.time());
if (initialised_ && criteriaSatisfied())
{
Info<< nl
<< algorithmName_
<< " solution converged in "
<< runTime.timeName() << " iterations" << nl << endl;
// Set to finalise calculation
runTime.writeAndEnd();
}
else
{
initialised_ = true;
storePrevIterFields();
}
return runTime.loop();
}
逐行:
- 第 138 行
setFirstIterFlag(true, true):复位"首次迭代"标志(用于松弛等"仅首步特殊处理"的逻辑)。 - 第 140 行
read():从fvSolution的SIMPLE子字典重新读控制参数(runTimeModifiable true时可在运行中改)。read()实现见simpleControl.C第 42--47 行,转调solutionControl::read(true)。 - 第 142 行:取
Time&(mesh_.time()的常量转换)。 - 第 144--153 行:收敛判定 。若已初始化且
criteriaSatisfied()为真(残差达标),打印"converged in X iterations",并runTime.writeAndEnd()结束计算。 - 第 154--158 行:否则置
initialised_ = true且storePrevIterFields()(保存上一迭代场,供松弛/残差用)。 - 第 160 行
return runTime.loop();:关键 ------它最终回到Time::loop(),由它决定"是否推进、"是否继续。simpleControl::loop()只是"包了一层收敛判定"。
这就是为什么稳态求解器也用
while (...loop()):loop()内部真正决定继续的是runTime.loop()(与stopAt/endTime挂钩),而 SIMPLE 收敛只是提前结束的一种方式。
5.2 correctNonOrthogonal()(逐行解读)
它在 solutionControlI.H(src/finiteVolume/cfdTools/general/solutionControl/solutionControl/solutionControlI.H)第 77--99 行:
cpp
inline bool Foam::solutionControl::correctNonOrthogonal()
{
setFirstIterFlag();
++corrNonOrtho_;
if (debug)
{
Info<< algorithmName_ << " correctNonOrthogonal: corrNonOrtho = "
<< corrNonOrtho_ << endl;
}
if (corrNonOrtho_ <= nNonOrthCorr_ + 1)
{
return true;
}
corrNonOrtho_ = 0;
setFirstIterFlag();
return false;
}
逐行:
- 第 79 行:
setFirstIterFlag()(默认参数)------每轮非正交修正开始时按语义复位。 - 第 81 行:计数器
corrNonOrtho_自增。 - 第 83--87 行:
debug时打印当前修正轮次。 - 第 89--92 行:只要
corrNonOrtho_ <= nNonOrthCorr_ + 1,返回true(继续循环)。 - 第 94--98 行:否则归零、复位标志、返回
false(结束)。
为什么是 nNonOrthCorr_ + 1 次? 因为第一次是"主装配 ",其后 nNonOrthCorr_ 次才是"非正交修正 "。nNonOrthCorr 来自 fvSolution 的 SIMPLE 子字典(默认 0)。仓库算例 tutorials/basic/laplacianFoam/flange/system/fvSolution 里写的是:
ini
SIMPLE
{
nNonOrthogonalCorrectors 2;
}
即该算例每步最多执行 3 次(1 主 + 2 修正)。修正的物理意义 (S2-07):非正交网格上,隐式拉普拉斯的隐式部分只含正交分量,非正交分量被"显式处理"进源项;每多修正一次,就把上一步显式修正后的 T 重新代入,使非正交误差收敛。
5.3 机制链 A(前半):稳态循环状态机
把上面两段串起来,稳态求解器的执行序列是:
css
main: while (simple.loop()) ← 外层:收敛判定 + runTime.loop()
│
├─ read() 每步读 SIMPLE 控制
├─ criteriaSatisfied()? 收敛? 是→writeAndEnd
│
└─ while (simple.correctNonOrthogonal()) ← 内层:1+nNonOrthCorr 次
fvScalarMatrix TEqn(...) 装配
fvOptions.constrain(TEqn) 约束
TEqn.solve() 线性求解
fvOptions.correct(T) 修正
#include "write.H" 写盘
这张流程就是"稳态求解器模板":换方程、换控制对象,骨架不变。
6. 方程组装:fvm::ddt − fvm::laplacian == fvOptions
laplacianFoam.C 第 91--96 行是整个求解器的"物理核心":
cpp
fvScalarMatrix TEqn
(
fvm::ddt(T) - fvm::laplacian(DT, T)
==
fvOptions(T)
);
它表达方程:
ini
∂T/∂t − ∇·(DT ∇T) = S (S 来自 fvOptions;无源时 S=0)
三个算子的分工:
fvm::ddt(T):时间导数,隐式,进入矩阵(S2-06)。fvm::前缀=有限体积隐式离散(返回fvMatrix)。fvm::laplacian(DT, T):拉普拉斯项,隐式。注意第一个参数DT是扩散系数场 (dict里读的),第二个才是待解场T。== fvOptions(T):右端源项(S2-13)。fvOptions(T)返回一个fvMatrix(源项被装配进矩阵的 source)。
注意 fvScalarMatrix 不是"普通矩阵" :它是 fvMatrix<scalar> 的类型别名,除了对角/上/下/源项,还携带边界条件的 internalCoeffs/boundaryCoeffs(第 12 节详述),所以"边界能进矩阵"。
6.1 fvm::laplacian 的重载树(逐行解读)
fvm::laplacian 在 src/finiteVolume/finiteVolume/fvm/fvmLaplacian.C 里是一整套重载家族(第 45--333 行,超过 20 个重载)。我们挑最关键的"主重载"(第 208--222 行):
cpp
template<class Type, class GType>
tmp<fvMatrix<Type>>
laplacian
(
const GeometricField<GType, fvPatchField, volMesh>& gamma,
const GeometricField<Type, fvPatchField, volMesh>& vf,
const word& name
)
{
return fv::laplacianScheme<Type, GType>::New
(
vf.mesh(),
vf.mesh().laplacianScheme(name)
).ref().fvmLaplacian(gamma, vf);
}
逐行:
- 模板参数
Type:待解场的类型(scalar);GType:扩散系数类型(scalar)。 - 第 217--221 行:选择器模式 ------
fv::laplacianScheme<Type, GType>::New(mesh, schemeData)按fvSchemes里laplacianSchemes子字典的名字 构造具体格式对象,再调用它的fvmLaplacian(gamma, vf)。 vf.mesh().laplacianScheme(name):从fvMesh取该名字对应的 scheme 数据(单词,如"Gauss linear corrected")。name默认由无name重载生成(第 248--253 行):"laplacian(" + gamma.name() + ',' + vf.name() + ')',即"laplacian(DT,T)"------这正是fvSchemes里那条laplacian(DT,T) Gauss linear corrected;的键名来源。
链路 :
fvm::laplacian(DT, T)→fvSchemes.laplacianSchemes[laplacian(DT,T)]取格式词 →New造格式对象 →fvmLaplacian装配fvMatrix。
6.2 laplacianScheme::New:运行期选择器(逐行解读)
laplacianScheme.C(src/finiteVolume/finiteVolume/laplacianSchemes/laplacianScheme/laplacianScheme.C)第 44--80 行:
cpp
template<class Type, class GType>
tmp<laplacianScheme<Type, GType>> laplacianScheme<Type, GType>::New
(
const fvMesh& mesh,
Istream& schemeData
)
{
if (fv::debug)
{
InfoInFunction << "Constructing laplacianScheme<Type, GType>" << endl;
}
if (schemeData.eof())
{
FatalIOErrorInFunction(schemeData)
<< "Laplacian scheme not specified" << endl << endl
<< "Valid laplacian schemes are :" << endl
<< IstreamConstructorTablePtr_->sortedToc()
<< exit(FatalIOError);
}
const word schemeName(schemeData);
auto* ctorPtr = IstreamConstructorTable(schemeName);
if (!ctorPtr)
{
FatalIOErrorInLookup
(
schemeData,
"laplacian",
schemeName,
*IstreamConstructorTablePtr_
) << exit(FatalIOError);
}
return ctorPtr(mesh, schemeData);
}
逐行:
- 第 55--62 行:若 scheme 数据流为空(
fvSchemes没写laplacian(DT,T)),报"Laplacian scheme not specified"并列出所有合法格式名。 - 第 64 行:读出第一个词
schemeName(如Gauss)。 - 第 66 行:
IstreamConstructorTable(schemeName)在 RTS 表里查构造器。 - 第 68--77 行:找不到则
FatalIOErrorInLookup(这是第 16 节"边界/scheme 未注册"类报错的同源机制)。 - 第 79 行:
ctorPtr(mesh, schemeData)调用gaussLaplacianScheme::New(因为Gauss注册到gaussLaplacianScheme)。
这个模式是全 OpenFOAM 格式选择的统一范式 :
×Scheme::New(mesh, word) → RTS 表 → 构造器。理解它,fvSchemes报错就再也难不倒你。
6.3 gaussLaplacianScheme::fvmLaplacian:装配矩阵(逐行解读)
gaussLaplacianScheme.C(第 157--201 行)是真正把拉普拉斯离散进矩阵的地方:
cpp
template<class Type, class GType>
tmp<fvMatrix<Type>>
gaussLaplacianScheme<Type, GType>::fvmLaplacian
(
const GeometricField<GType, fvsPatchField, surfaceMesh>& gamma,
const GeometricField<Type, fvPatchField, volMesh>& vf
)
{
const fvMesh& mesh = this->mesh();
const surfaceVectorField Sn(mesh.Sf()/mesh.magSf());
const surfaceVectorField SfGamma(mesh.Sf() & gamma);
const GeometricField<scalar, fvsPatchField, surfaceMesh> SfGammaSn
(
SfGamma & Sn
);
const surfaceVectorField SfGammaCorr(SfGamma - SfGammaSn*Sn);
tmp<fvMatrix<Type>> tfvm = fvmLaplacianUncorrected
(
SfGammaSn,
this->tsnGradScheme_().deltaCoeffs(vf),
vf
);
fvMatrix<Type>& fvm = tfvm.ref();
tmp<GeometricField<Type, fvsPatchField, surfaceMesh>> tfaceFluxCorrection
= gammaSnGradCorr(SfGammaCorr, vf);
if (this->tsnGradScheme_().corrected())
{
tfaceFluxCorrection.ref() +=
SfGammaSn*this->tsnGradScheme_().correction(vf);
}
fvm.source() -= mesh.V()*fvc::div(tfaceFluxCorrection())().primitiveField();
if (mesh.fluxRequired(vf.name()))
{
fvm.faceFluxCorrectionPtr(tfaceFluxCorrection.ptr());
}
return tfvm;
}
逐行:
- 第 165 行:
mesh。 - 第 167 行
Sn = Sf/magSf:面单位法向量。 - 第 169--174 行:把扩散系数投影到面并分解为"正交分量
SfGammaSn"与"非正交分量SfGammaCorr = SfGamma − SfGammaSn·Sn"。这就是非正交修正的代数根源(S2-07)。 - 第 176--181 行:调用
fvmLaplacianUncorrected(SfGammaSn, deltaCoeffs, vf)装配正交隐式矩阵 (upper=面系数、negSumDiag生成对角、遍历边界写internalCoeffs/boundaryCoeffs)。 - 第 184--185 行:
gammaSnGradCorr(SfGammaCorr, vf)计算非正交显式修正通量。 - 第 187--191 行:若
snGradScheme是corrected(即fvSchemes里snGradSchemes写corrected或laplacian写... corrected),追加corrected()修正项。 - 第 193 行:
fvm.source() -= V * div(tfaceFluxCorrection)------把非正交修正放进源项(显式)。 - 第 195--198 行:若该场需要通量(
fluxRequired),保存faceFluxCorrectionPtr供后续flux()用。 - 第 200 行:返回矩阵。
关键理解 :非正交修正不在隐式矩阵里 ,而是作为显式源项 加入。这就是为什么需要
correctNonOrthogonal()外层修正循环------每轮用更新后的T重算这个显式源项,逐步逼近。转到corrected后,非正交误差被迭代消掉;若用uncorrected,则误差残留、精度下降。
6.4 fvmLaplacianUncorrected:边界如何进矩阵(逐行解读)
gaussLaplacianScheme.C 第 46--90 行:
cpp
template<class Type, class GType>
tmp<fvMatrix<Type>>
gaussLaplacianScheme<Type, GType>::fvmLaplacianUncorrected
(
const surfaceScalarField& gammaMagSf,
const surfaceScalarField& deltaCoeffs,
const GeometricField<Type, fvPatchField, volMesh>& vf
)
{
tmp<fvMatrix<Type>> tfvm
(
new fvMatrix<Type>
(
vf,
deltaCoeffs.dimensions()*gammaMagSf.dimensions()*vf.dimensions()
)
);
fvMatrix<Type>& fvm = tfvm.ref();
fvm.upper() = deltaCoeffs.primitiveField()*gammaMagSf.primitiveField();
fvm.negSumDiag();
forAll(vf.boundaryField(), patchi)
{
const fvPatchField<Type>& pvf = vf.boundaryField()[patchi];
const fvsPatchScalarField& pGamma = gammaMagSf.boundaryField()[patchi];
const fvsPatchScalarField& pDeltaCoeffs =
deltaCoeffs.boundaryField()[patchi];
if (pvf.coupled())
{
fvm.internalCoeffs()[patchi] =
pGamma*pvf.gradientInternalCoeffs(pDeltaCoeffs);
fvm.boundaryCoeffs()[patchi] =
-pGamma*pvf.gradientBoundaryCoeffs(pDeltaCoeffs);
}
else
{
fvm.internalCoeffs()[patchi] = pGamma*pvf.gradientInternalCoeffs();
fvm.boundaryCoeffs()[patchi] = -pGamma*pvf.gradientBoundaryCoeffs();
}
}
return tfvm;
}
逐行:
- 第 55--63 行:构造
fvMatrix<Type>,量纲 =deltaCoeffs.dim × gammaMagSf.dim × vf.dim。 - 第 65 行:
fvm.upper() = deltaCoeffs × gammaMagSf------面法向梯度系数 × 面扩散系数,这是拉普拉斯的面系数。 - 第 66 行:
negSumDiag()------由面系数生成对角(矩阵对角 = 负的各面系数之和),这是有限体积矩阵的标准构造。 - 第 68 行:
forAll(vf.boundaryField(), patchi)遍历所有边界 patch。 - 第 75--81 行:耦合边界 (
coupled(),如 cyclic/processor)用带权版本gradientInternalCoeffs(pDeltaCoeffs);非耦合边界用无参版本。 - 第 78--80 行:把边界的
gradientInternalCoeffs/gradientBoundaryCoeffs乘面系数后写入fvm.internalCoeffs()[patchi]/boundaryCoeffs()[patchi]。
这一段的含义极其重要 :每个边界条件通过
gradientInternalCoeffs()/gradientBoundaryCoeffs()决定它对矩阵的贡献 。所以自定义边界条件若想"影响方程",必须正确实现这两个(以及value*两个)方法------见第 12 节机制链 C。
6.5 机制链 A 的"方程侧"小结
arduino
方程源码 fvm::laplacian(DT, T)
└─ fvm::laplacian(重载, fvmLaplacian.C 208-222)
└─ fv::laplacianScheme::New(mesh, fvSchemes["laplacian(DT,T)"])(laplacianScheme.C 44-80)
└─ gaussLaplacianScheme::fvmLaplacian(gaussLaplacianScheme.C 157-201)
├─ 正交部分 → fvmLaplacianUncorrected(46-90) 建 upper/diag/边界coeffs
└─ 非正交部分 → 显式修正进 source
7. solve() 与 fvSolution:从 fvMatrix 到线性求解器
方程装配完,TEqn.solve()(laplacianFoam.C 第 99 行)把它送进线性求解。
7.1 fvMatrix::solve 的公开入口与"seg/coupled"分派(逐行解读)
fvMatrixSolve.C(src/finiteVolume/fvMatrices/fvMatrix/fvMatrixSolve.C)第 61--107 行:
cpp
template<class Type>
Foam::SolverPerformance<Type> Foam::fvMatrix<Type>::solveSegregatedOrCoupled
(
const dictionary& solverControls
)
{
word regionName;
if (psi_.mesh().name() != polyMesh::defaultRegion)
{
regionName = psi_.mesh().name() + "::";
}
addProfiling(solve, "fvMatrix::solve.", regionName, psi_.name());
if (debug) { ... }
// Do not solve if maxIter == 0
if (solverControls.getOrDefault<label>("maxIter", -1) == 0)
{
return SolverPerformance<Type>();
}
word type(solverControls.getOrDefault<word>("type", "segregated"));
if (type == "segregated")
{
return solveSegregated(solverControls);
}
else if (type == "coupled")
{
return solveCoupled(solverControls);
}
else
{
FatalIOErrorInFunction(solverControls)
<< "Unknown type " << type
<< "; currently supported solver types are segregated and coupled"
<< exit(FatalIOError);
return SolverPerformance<Type>();
}
}
逐行:
- 第 66--70 行:若网格不是默认区域(多区域算例),
regionName加前缀(用于 profiling 标签)。 - 第 71 行:
addProfiling(solve, ...)------登记 profiling 计时节点(S2-16)。 - 第 82--86 行:
maxIter == 0直接返回空性能(跳过求解,用于"只装配不求解"的场景)。 - 第 88 行:从
fvSolution的该场求解器字典读type,默认"segregated"。 - 第 90--106 行:
segregated(分离式,逐分量)→solveSegregated;coupled(耦合式)→solveCoupled;否则FatalIOError。
7.2 线性求解器调用(逐行解读)
fvMatrixSolve.C 第 216--245 行(solveSegregated 的结尾):
cpp
SolverPerformance<Type> solverPerf;
// Solver call
solverPerf = lduMatrix::solver::New
(
psi.name() + pTraits<Type>::componentNames[cmpt],
*this,
bouCoeffsCmpt,
intCoeffsCmpt,
interfaces,
solverControls
)->solve(psiCmpt, sourceCmpt, cmpt);
if (logLevel)
{
solverPerf.print(Info.masterStream(this->mesh().comm()));
}
solverPerfVec.replace(cmpt, solverPerf);
solverPerfVec.solverName() = solverPerf.solverName();
psi.primitiveFieldRef().replace(cmpt, psiCmpt);
diag() = saveDiag;
}
psi.correctBoundaryConditions();
psi.mesh().data().setSolverPerformance(psi.name(), solverPerfVec);
return solverPerfVec;
逐行:
- 第 219--227 行:
lduMatrix::solver::New(...)按fvSolution的solver(如PCG/PBiCGStab/smoothSolver)构造线性求解器对象 ,再->solve(...)求解该分量。名字取psi.name() + 分量名(如T、Ux)。 - 第 229--232 行:
logLevel时打印求解性能(Info.masterStream(comm)------只在 master 进程输出,S2-16 并行调试技巧)。 - 第 234--237 行:收集该分量的性能,回填到分量场。
- 第 241 行:
psi.correctBoundaryConditions()------求解完修正边界。 - 第 243 行:把性能写回
mesh.data()(供后续函数对象读取)。
7.3 solverDict():字典从哪来(逐行解读)
fvMatrix.C(src/finiteVolume/fvMatrices/fvMatrix/fvMatrix.C)第 1518--1535 行:
cpp
template<class Type>
const Foam::dictionary& Foam::fvMatrix<Type>::solverDict
(
const word& name
) const
{
return psi_.mesh().solverDict(name);
}
template<class Type>
const Foam::dictionary& Foam::fvMatrix<Type>::solverDict() const
{
return psi_.mesh().solverDict
(
psi_.select(psi_.mesh().data().isFinalIteration())
);
}
逐行:
- 第 1519--1525 行:带名字版本------直接向
fvMesh要该名字的求解器字典。 - 第 1529--1535 行:无参版本------用
psi_.select(isFinalIteration())选字典名。select的语义 :若当前是"最后迭代"(isFinalIteration()),则可能选到"TFinal"(最终迭代用更严的容差),否则选"T"。
配套的 fvSolution 片段来自仓库算例(tutorials/basic/laplacianFoam/flange/system/fvSolution):
ini
solvers
{
T
{
solver PCG;
preconditioner DIC;
tolerance 1e-06;
relTol 0;
}
}
这就是
TEqn.solve()背后的一切 :solve()→solveSegregatedOrCoupled()→ 读fvSolution["T"]→lduMatrix::solver::New(按PCG)→solve。改线性求解器其实只改这 4 行字典,不用改 C++。
7.4 自由函数 solve() 与 fvMatrix 的关系
fvMatrix.C 第 1965--1979 行还提供自由函数 solve:
cpp
template<class Type>
Foam::SolverPerformance<Type> Foam::solve(fvMatrix<Type>& mat)
{
return mat.solve();
}
template<class Type>
Foam::SolverPerformance<Type> Foam::solve(const tmp<fvMatrix<Type>>& tmat)
{
SolverPerformance<Type> solverPerf(tmat.constCast().solve());
tmat.clear();
return solverPerf;
}
这两行说明:solve(TEqn) 与 TEqn.solve() 等价 。使用 tmp<fvMatrix> 版本时会 clear() 释放临时矩阵,避免内存泄漏。
8. createFields.H 逐行
laplacianFoam/createFields.H 全文 49 行。它是"如何创建场"的教科书。
8.1 建 T:MUST_READ 型场(逐行解读)
第 1--14 行:
cpp
Info<< "Reading field T\n" << endl;
volScalarField T
(
IOobject
(
"T",
runTime.timeName(),
mesh,
IOobject::MUST_READ,
IOobject::AUTO_WRITE
),
mesh
);
逐行:
- 第 1 行:打印"Reading field T"。
- 第 3 行:
volScalarField T------体标量场。 - 第 5--12 行:
IOobject的五个参数 :"T":对象名(决定读写文件名T)。runTime.timeName():时间目录(如"0"、"0.1")。mesh:注册表(场挂到网格对象注册表)。IOobject::MUST_READ:读取策略------必须存在 ,读不到即报FatalIOError。IOobject::AUTO_WRITE:写出策略------自动写盘 (runTime.write()时写)。
- 第 13 行:
mesh------用网格构造场(初始化internalField大小与boundaryField)。
对应算例
tutorials/basic/laplacianFoam/flange/0.orig/T的真实头:class volScalarField; object T;加internalField uniform 273;与boundaryField{...}。名字、类型、目录三者必须与IOobject一致。
8.2 建 DT:READ_IF_PRESENT + 默认值(逐行解读)
第 17--31 行:
cpp
Info<< "Reading diffusivity DT\n" << endl;
volScalarField DT
(
IOobject
(
"DT",
runTime.timeName(),
mesh,
IOobject::READ_IF_PRESENT,
IOobject::AUTO_WRITE
),
mesh,
dimensionedScalar(dimViscosity, Zero)
);
逐行:
- 第 26 行:
READ_IF_PRESENT------有就读,没有不报错。 - 第 30 行:第三个构造参数
dimensionedScalar(dimViscosity, Zero)------默认值 :量纲=运动粘度(m²/s),值=0。若DT文件不存在,就用这个默认值。
8.3 字典回退:headerOk()(逐行解读)
第 33--47 行:
cpp
if (!DT.headerOk())
{
IOdictionary transportProperties
(
IOobject
(
"transportProperties",
runTime.constant(),
mesh,
IOobject::MUST_READ_IF_MODIFIED,
IOobject::NO_WRITE
)
);
DT = dimensionedScalar("DT", dimViscosity, transportProperties);
}
逐行:
- 第 33 行:
headerOk()判断"DT文件头是否成功读入"(即文件是否存在)。注意!------不存在时。 - 第 35--45 行:读
constant/transportProperties(runTime.constant()指constant/目录)。MUST_READ_IF_MODIFIED=必须能读且监视修改(运行中改了会重载);NO_WRITE=不写回。 - 第 46 行:
DT = dimensionedScalar("DT", dimViscosity, transportProperties)------从字典里按名取标量 ,赋给DT。
这就是"两种配置方式" :DT 既可以写成 0/DT 独立场文件,也可以只写在 constant/transportProperties 里。仓库算例 flange/constant/transportProperties 只写 DT 4e-05;,因此走的是回退路径 (0/ 里没有 DT)。
8.4 createFvOptions.H(逐行解读)
第 49 行:
cpp
#include "createFvOptions.H"
createFvOptions.H(src/finiteVolume/cfdTools/general/include/createFvOptions.H,全 6 行):
cpp
fv::options& fvOptions(fv::options::New(mesh));
if (!fvOptions.optionList::size())
{
Info << "No finite volume options present" << endl;
}
逐行:
- 第 1 行:
fv::options::New(mesh)从注册表构造/获取fvOptions(S2-13),引用绑定到fv::options& fvOptions。 - 第 3--6 行:若选项列表为空,打印"No finite volume options present"(提示
constant/fvOptions缺失或为空)。
要点 :这一行让
main里有了变量fvOptions,供第 91--100 行的fvOptions(T)/constrain/correct使用。这也是"片段式组合"的第三个变量。
8.5 createFields.H 模板价值总结
| 模式 | 用法 | 何时用 |
|---|---|---|
MUST_READ + AUTO_WRITE |
必读必写 | 待解的主场(T) |
READ_IF_PRESENT + 默认值 |
可选 | 物性/系数(DT) |
headerOk() + 字典回退 |
双配置 | 物性可放 0/ 或 constant/ |
createFvOptions.H |
源项框架 | 需要 fvOptions 时 |
9. write.H 逐行
laplacianFoam/write.H 全文 58 行,它不是一个"写盘命令",而是一个"写盘时额外输出梯度/通量"的片段。
9.1 外层判断(逐行解读)
第 1--2 行:
cpp
if (runTime.writeTime())
{
第 57 行:
cpp
runTime.write();
}
逐行:
- 第 1 行
runTime.writeTime():"现在是不是该写盘的时间点" (按writeControl/writeInterval判定)。注意它不等于runTime.write()(后者执行写入动作)。 - 第 57 行
runTime.write():真正把AUTO_WRITE的场与注册表里登记的对象写到当前时间目录。
与很多求解器的差异 :不少求解器直接写
runTime.write()(无外层判断);而laplacianFoam用writeTime()判断,是为了只在写盘时计算gradT/flux(避免每步都算梯度)。这是"输出量按需计算"的优化。
9.2 gradT 与三个分量(逐行解读)
第 3 行:
cpp
volVectorField gradT(fvc::grad(T));
第 5--42 行分别构造 gradTx/gradTy/gradTz(三个体标量场),以第 5--16 行 gradTx 为例:
cpp
volScalarField gradTx
(
IOobject
(
"gradTx",
runTime.timeName(),
mesh,
IOobject::NO_READ,
IOobject::AUTO_WRITE
),
gradT.component(vector::X)
);
逐行:
- 第 3 行:
fvc::grad(T)计算T的梯度(体向量场)。 - 第 5--16 行:把向量场的 X 分量抽成标量场
gradTx。注意IOobject::NO_READ(不读,纯计算输出)+AUTO_WRITE。 gradTy/gradTz同理取vector::Y/vector::Z。
为什么要拆成三个分量? 因为 OpenFOAM 里"体向量场"的写出是
volVectorField,但很多后处理(如 ParaView 的等值面/切片)更希望看到标量分量。这是输出友好的实用技巧,可直接复制到你的求解器。
9.3 flux(逐行解读)
第 44--55 行:
cpp
volVectorField DTgradT
(
IOobject
(
"flux",
runTime.timeName(),
mesh,
IOobject::NO_READ,
IOobject::AUTO_WRITE
),
DT*gradT
);
runTime.write();
逐行:
- 第 44 行:变量名叫
DTgradT,但写出对象名是"flux"(IOobject第一个参数)。 - 第 54 行:初值
DT*gradT------扩散通量−DT∇T(这里省了负号,展示的是DT∇T)。 - 第 57 行:
runTime.write()。
命名陷阱 :变量名与写出名可以不同。
DTgradT只是局部变量名,磁盘上写出来的文件叫flux。新手常因"以为变量名=文件名"而找不到输出。
10. 机制链 A:求解流程全链(fvSchemes → fvMatrix → solve → 写场)
第 5--9 节已把链路"点"讲透,这一节把它连成一条完整、可复述的推导链。这条链是本章第一条机制链,也是理解任何求解器的总纲。
10.1 链的起点:fvMesh 就是 fvSchemes
一个常被忽略的事实:fvMesh 类多继承 自 fvSchemes。见 fvMesh.H(src/finiteVolume/fvMesh/fvMesh.H)第 87 行:
cpp
public fvSchemes,
而 fvSchemes 又继承自 schemesLookup(src/OpenFOAM/matrices/schemes/schemesLookup.H 第 80--83 行):
cpp
class schemesLookup
:
public IOdictionary
因此 mesh.laplacianScheme(name) 实际来自 schemesLookup(第 251--252 行):
cpp
//- Get laplacian scheme for given name, or default
ITstream& laplacianScheme(const word& name) const;
schemesLookup.H 头注释(第 33--45 行)把所有 scheme 条目列成一张表:
| 名称 | 含义 | 类型 | 必需 | 默认 |
|---|---|---|---|---|
ddtSchemes |
时间导数 | dict | no | none |
d2dt2Schemes |
二阶时间导数(有限面积) | dict | no | none |
interpolationSchemes |
插值 | dict | no | linear |
divSchemes |
散度(对流) | dict | yes | - |
gradSchemes |
梯度 | dict | yes | - |
lnGradSchemes |
线法向梯度(有限面积) | dict | no | corrected |
snGradSchemes |
面法向梯度 | dict | no | corrected |
laplacianSchemes |
拉普拉斯 | dict | yes | - |
fluxRequired |
是否需要面通量 | dict | no | - |
结论 :mesh 对象本身就是一份"已解析的 fvSchemes 字典",fvm/fvc 算子通过 mesh.×Scheme(name) 取格式词。
10.2 完整链(六步)
以 laplacianFoam 的 fvm::laplacian(DT, T) 为例,端到端链条是:
scss
① 用户写方程
fvm::ddt(T) - fvm::laplacian(DT, T) == fvOptions(T)
│
▼
② fvm::laplacian(DT, T) [fvmLaplacian.C 240-254 → 208-222]
名字 name = "laplacian(DT,T)"
│
▼
③ mesh.laplacianScheme("laplacian(DT,T)") [schemesLookup.H 251-252]
从 fvSchemes 的 laplacianSchemes 子字典取出 ITstream
例:tutorials/.../flange/system/fvSchemes:
laplacianSchemes { default none; laplacian(DT,T) Gauss linear corrected; }
→ 取出 "Gauss linear corrected"
│
▼
④ fv::laplacianScheme<scalar,scalar>::New(mesh, stream) [laplacianScheme.C 44-80]
读第一个词 "Gauss" → RTS 表 → gaussLaplacianScheme::New(mesh, stream)
读后续词 "linear" → 插值格式;"corrected" → snGradScheme
│
▼
⑤ gaussLaplacianScheme::fvmLaplacian(gamma, vf) [gaussLaplacianScheme.C 157-201]
├─ 正交隐式部分 → fvmLaplacianUncorrected(46-90)(写 upper/negSumDiag/边界coeffs)
└─ 非正交部分 → 显式修正进 source(fvm.source() -= V*div(修正通量))
│
▼
⑥ TEqn.solve() [fvMatrixSolve.C 61-107 → 216-245]
读 fvSolution["T"] → lduMatrix::solver::New(PCG...) → solve
→ psi.correctBoundaryConditions()
│
▼
⑦ runTime.write() [write.H 57] 写盘(含 gradT/flux)
六步价值 :任何"求解器报错在 scheme 上"的问题,都能用这张图定位------是 fvSchemes 缺条目(③),还是格式名写错(④),还是边界 coeffs 异常(⑤)。
10.3 这条链的"配置驱动"本质
注意链条的 ③④⑤ 全部由字典内容 驱动,C++ 源码固定不动。这正是 OpenFOAM "配置驱动"哲学的核心:同一份 .C 源码,换 fvSchemes 就换离散格式,换 fvSolution 就换线性求解器。写求解器时,你只需要保证"场/方程/循环"三条骨架正确,数值细节全在字典里。
11. 机制链 B:Make/files 的 EXE/LIB 语法与 wmake 链接顺序
第 2 节只展示了 Make/files/Make/options 的"长相"。这一节讲它们是怎么被 wmake 消化的 ,以及链接顺序为何重要。
11.1 Make/files/Make/options 的预处理(逐行解读)
wmake 用 cpp 把 Make/files、Make/options 预处理成内部文件。规则在 wmake/makefiles/files(wmake/makefiles/files 第 71--84 行):
makefile
# Make/files
# ~~~~~~~~~~
$(SFILES): $(MAKE_DIR)/files
$(call PREFILTER_MAKE_OPTIONS,$<,${FILES})
# Extracted macro definitions.
@sed -n -e '/=/p' $(FILES) > $(VARS)
@echo "SOURCE = \\" > $(SFILES)
# Remove macros, comments, blank lines, trailing space. Add backslash continuation
@sed -e '/=/d; /^#/d; /^[ \t]*$$/d' -e 's@[ \t]*$$@ \\@' $(FILES) >> $(SFILES)
@rm -f $(FILES)
# Ensure final line is followed by newline. Extra comment for safety.
@echo >> $(SFILES)
@echo "# sources" >> $(SFILES)
逐行:
- 第 73--74 行:把
Make/files经cpp预处理为${FILES}(PREFILTER_MAKE_OPTIONS定义在第 52--54 行:$(CPP) -Ulinux -Uunix $(GFLAGS) $1 | sed -e 's@ *@ @g' > $2------即"用 cpp 展开宏,但取消linux/unix宏定义以免误替换路径,并压缩多余空格")。 - 第 76 行:
sed -n -e '/=/p'------把含=的行(宏定义,如EXE = ...)抽到$(VARS)。 - 第 77 行:初始化
SOURCE = \(源文件列表头)。 - 第 79 行:
sed -e '/=/d; /^#/d; ...'------删掉含=的行、删掉注释行、删掉空行,并给非空行补行尾反斜杠 ,全部追加到$(SFILES)。 - 第 80 行:删除中间文件
${FILES}。
含义 :Make/files 里只要含 = 的行就是宏定义(被 $(VARS) 收集),不含 = 的非空行就是源文件名(被 SOURCE 收集)。这解释了为什么:
laplacianFoam.C被当作源文件;EXE = $(FOAM_APPBIN)/laplacianFoam被当作宏定义。
Make/options 走同一套预处理(第 61--68 行),但额外在 Windows 上调用 wmakeWindowsDlOpenLibs(第 63--65 行)自动补 dlOpen 库。
11.2 四个变量:EXE_INC/EXE_LIBS/LIB_LIBS/LIBO_LIBS
wmake/makefiles/general 头注释(第 27--33 行)明确定义:
bash
# EXE_INC : defines/includes etc for executable or library targets
# EXE_LIBS : linking for executables
# LIB_LIBS : linking for libraries (.so)
# LIBO_LIBS : linking for object libraries (.o)
对应第 82--85 行的默认空值:
makefile
EXE_INC =
EXE_LIBS =
LIB_LIBS =
LIBO_LIBS =
选型规则:
| 你要产出 | 用哪个 LIB 变量 | 场景 |
|---|---|---|
| 可执行文件(求解器/工具) | EXE_LIBS |
求解器、utilities |
共享库 .so |
LIB_LIBS |
自定义模型/边界/函数对象库 |
目标文件库 .o |
LIBO_LIBS |
特殊模块/单对象链接 |
新手最常犯的错 :在"库"目录的 Make/options 里写 EXE_LIBS 而不是 LIB_LIBS------wmake 不会报错,但该库不会被链接 ,导致加载时报 undefined symbol。
11.3 产物目标的四种形态
general 第 138--144 行定义默认目标:
makefile
# Library (default which is to be overridden)
LIB = libNULL
# Project executable (default which is to be overridden)
EXE = $(WM_PROJECT).out
# Standalone executable (default which is to be overridden)
SEXE = a.out
Make/files 里可通过赋值覆盖它们:
Make/files 写法 |
产物 | 链接规则(general 行号) |
|---|---|---|
EXE = $(FOAM_APPBIN)/xxx |
可执行(进 $FOAM_APPBIN) |
第 199--204 行 |
EXE = $(FOAM_USER_APPBIN)/xxx |
可执行(进用户目录) | 同上 |
LIB = $(FOAM_LIBBIN)/libxxx |
共享库 .so |
第 231--243 行 |
LIB = $(FOAM_USER_LIBBIN)/libxxx |
用户共享库 .so |
同上 |
LIB = .../libxxx + 目标 lib |
静态库 .a |
第 248--257 行 |
EXE = ... + 目标 exe(SEXE) |
独立可执行(不链接 OpenFOAM 库) | 第 209--213 行 |
11.4 链接顺序(逐行解读)
可执行文件的链接规则在 general 第 199--204 行:
makefile
$(EXE)$(EXT_EXE): $(OBJECTS)
@$(WM_SCRIPTS)/makeTargetDir $(EXE)
$(call QUIET_MESSAGE,link,$(EXE)$(EXT_EXE))
$E $(LINKEXE) $(FOAM_EXTRA_LDFLAGS) $(OBJECTS) -L$(LIB_PLATFORMS) \
$(EXE_LIBS) $(PROJECT_LIBS) $(SYS_LIBS) \
$(LINK_LIBS) $(GLIBS) -o $(EXE)$(EXT_EXE)
逐项链接顺序:
$(LINKEXE):链接器命令(如g++ -o/ld)。$(FOAM_EXTRA_LDFLAGS):用户额外链接标志(环境变量FOAM_EXTRA_LDFLAGS,S2-16)。$(OBJECTS):本目标编译出的所有.o------必须排在最前,否则符号解析不到。-L$(LIB_PLATFORMS):库搜索路径(platforms/$(WM_OPTIONS)/lib)。$(EXE_LIBS):来自Make/options的-l列表。$(PROJECT_LIBS):-l$(WM_PROJECT) -ldl(即-lOpenFOAM -ldl,第 92 行定义)。$(SYS_LIBS):系统库(OpenMP 等,第 69--79 行)。$(LINK_LIBS)/$(GLIBS):全局库。
链接顺序的铁律 :静态库/目标文件在前,被依赖的库在后 (Unix 链接器单遍扫描,后面的库才能解析前面 .o 的未定义符号)。这就是为什么 EXE_LIBS 里 -lfiniteVolume 要能提供 mesh/场相关符号,而 PROJECT_LIBS(-lOpenFOAM)提供最底层符号。
11.5 共享库的链接规则(逐行解读)
general 第 231--243 行(非 Windows 分支):
makefile
$(LIB)$(EXT_SO): $(OBJECTS)
@$(WM_SCRIPTS)/makeTargetDir $(LIB)
$(call QUIET_MESSAGE,link,$(LIB)$(EXT_SO))
$E $(LINKLIBSO) $(FOAM_EXTRA_LDFLAGS) $(OBJECTS) -L$(LIB_PLATFORMS) \
$(LIB_LIBS) $(GLIB_LIBS) -o $(LIB)$(EXT_SO)
注意与可执行文件的差别:共享库**不链接 PROJECT_LIBS(-lOpenFOAM)**于某些分支------因为共享库运行时由加载它的可执行文件提供符号(Windows 分支第 235--236 行则会显式链接 PROJECT_LIBS)。
11.6 lnInclude:为什么 include 路径都指向它
Make/options 里所有 -I.../lnInclude 的 lnInclude 是 wmake 自动生成的符号链接目录 。生成规则在 general 第 285--287 行:
makefile
lnInclude: $(MAKE_DIR)/files $(MAKE_DIR)/options
@wmakeLnInclude -force .
wmakeLnInclude -force . 会把当前库目录下所有 .H/.txx/.cxx 在 lnInclude/ 里建符号链接 ,使 #include "xxx.H" 无需写相对路径。因此:
-I$(LIB_SRC)/finiteVolume/lnInclude让编译器能在finiteVolume库的lnInclude找fvCFD.H/fvMesh.H/fvm.H。- 若
lnInclude缺失(未跑过wmakeLnInclude),编译会报"xxx.H: No such file or directory"(第 16 节)。
11.7 机制链 B 汇总
scss
Make/files ──cpp+sed──▶ SOURCE(源文件) + VARS(EXE=/LIB= 宏)
Make/options ─cpp+sed──▶ EXE_INC/EXE_LIBS/LIB_LIBS/LIBO_LIBS
│
┌─────────────────────┴──────────────────────┐
▼ ▼
EXE 目标: objects + EXE_LIBS + PROJECT_LIBS LIB 目标: objects + LIB_LIBS
└────────────► -L$(LIB_PLATFORMS) ◄────────┘
lnInclude ◄── wmakeLnInclude -force .
12. 机制链 C:自定义 fvPatchField 的类骨架
第 2 节提到"自定义边界需继承 fvPatchField"。这一节给出完整的类骨架与语义顺序,这是本章第三条机制链。
12.1 继承层次与四个"必须"
一个 fv 边界条件类,通常继承 fvPatchField<Type> 或更具体的基类(fixedValue/fixedGradient/calculated)。基类 fvPatchField.H(src/finiteVolume/fields/fvPatchFields/fvPatchField/fvPatchField.H)把"可重写点"定义清楚:
- 第 675 行
virtual void updateCoeffs();------"更新系数"(设置updated_=true)。 - 第 692--696 行
virtual void evaluate(commsType);------"求值"(设置updated_=false)。 - 第 717--724 行
valueInternalCoeffs(...)(默认NotImplemented)。 - 第 728 行起
valueBoundaryCoeffs(...)(默认NotImplemented)。
基类的默认实现极简(fvPatchField.txx 第 341--371 行):
cpp
template<class Type>
void Foam::fvPatchField<Type>::updateCoeffs()
{
fvPatchFieldBase::setUpdated(true);
}
template<class Type>
void Foam::fvPatchField<Type>::evaluate(const Pstream::commsTypes)
{
if (!updated())
{
updateCoeffs();
}
fvPatchFieldBase::setUpdated(false);
fvPatchFieldBase::setManipulated(false);
}
逐行:
updateCoeffs()默认只把updated_置真(setUpdated(true))。evaluate()默认逻辑:若未更新则先updateCoeffs(),随后把updated_置假、manipulated_置假。
核心语义 :
updateCoeffs()负责"算",evaluate()负责"用并复位"。二者通过updated_标志协作------同一次求值周期内updateCoeffs只跑一次,避免重复计算。
12.2 以 fixedValueFvPatchField 为模板的骨架
fixedValueFvPatchField.H(src/finiteVolume/fields/fvPatchFields/basic/fixedValue/)第 71--231 行的类声明是派生边界的标准骨架:
cpp
template<class Type>
class fixedValueFvPatchField
:
public fvPatchField<Type>
{
typedef fixedValueFvPatchField<Type> this_bctype;
typedef fvPatchField<Type> parent_bctype;
public:
//- Runtime type information
TypeName("fixedValue");
// Constructors
fixedValueFvPatchField(const fvPatch&, const DimensionedField<Type, volMesh>&);
fixedValueFvPatchField(const fvPatch&, const DimensionedField<Type, volMesh>&, const Type& value);
fixedValueFvPatchField(const fvPatch&, const DimensionedField<Type, volMesh>&, const dictionary&, IOobjectOption::readOption requireValue = IOobjectOption::MUST_READ);
fixedValueFvPatchField(const this_bctype&, const fvPatch&, const DimensionedField<Type, volMesh>&, const fvPatchFieldMapper&);
fixedValueFvPatchField(const this_bctype&, const DimensionedField<Type, volMesh>&);
//- Clone with an internal field reference
virtual tmp<fvPatchField<Type>> clone(const DimensionedField<Type, volMesh>& iF) const
{
return fvPatchField<Type>::Clone(*this, iF);
}
// Member Functions
virtual bool fixesValue() const { return true; }
virtual bool assignable() const { return false; }
virtual void write(Ostream&) const;
// Evaluation functions
virtual tmp<Field<Type>> valueInternalCoeffs(const tmp<scalarField>&) const;
virtual tmp<Field<Type>> valueBoundaryCoeffs(const tmp<scalarField>&) const;
virtual tmp<Field<Type>> gradientInternalCoeffs() const;
virtual tmp<Field<Type>> gradientBoundaryCoeffs() const;
};
骨架的语义顺序(这是本章机制链 C 的关键):
| 顺序 | 成员 | 语义 | 是否必须 |
|---|---|---|---|
| 1 | typedef this_bctype/parent_bctype |
自引用与父类型别名(简化后续书写) | 推荐 |
| 2 | TypeName("...") |
RTS 类型名(字典里 type xxx; 的键) |
必须 |
| 3 | 各构造函数 |
从 patch/值/字典/mapper 构造 | 必须(至少字典版) |
| 4 | clone(iF) |
复制到新内部场(用于重建/网格变化) | 必须 |
| 5 | fixesValue() |
是否固定值(影响方程装配语义) | 视类型 |
| 6 | assignable() |
是否允许赋值修改 | 视类型 |
| 7 | updateCoeffs() |
算边界值/系数 | 必须(值类) |
| 8 | evaluate() |
求值并复位 updated_ |
值类常由基类复用 |
| 9 | write(os) |
写出 value/自定义项 |
必须 |
| 10 | 四个 *Coeffs() |
边界进矩阵的贡献 | 影响方程时必须 |
12.3 四个 coeffs 的真实实现(逐行解读)
fixedValueFvPatchField.txx(fixedValueFvPatchField.txx 第 118--162 行):
cpp
template<class Type>
Foam::tmp<Foam::Field<Type>>
Foam::fixedValueFvPatchField<Type>::valueInternalCoeffs(const tmp<scalarField>&) const
{
// No contribution from internal values
return tmp<Field<Type>>::New(this->size(), Foam::zero{});
}
template<class Type>
Foam::tmp<Foam::Field<Type>>
Foam::fixedValueFvPatchField<Type>::valueBoundaryCoeffs(const tmp<scalarField>&) const
{
return *this;
}
template<class Type>
Foam::tmp<Foam::Field<Type>>
Foam::fixedValueFvPatchField<Type>::gradientInternalCoeffs() const
{
return -pTraits<Type>::one*this->patch().deltaCoeffs();
}
template<class Type>
Foam::tmp<Foam::Field<Type>>
Foam::fixedValueFvPatchField<Type>::gradientBoundaryCoeffs() const
{
return this->patch().deltaCoeffs()*(*this);
}
template<class Type>
void Foam::fixedValueFvPatchField<Type>::write(Ostream& os) const
{
this->parent_bctype::write(os);
fvPatchField<Type>::writeValueEntry(os);
}
逐行:
valueInternalCoeffs:返回 0("内部值对本边界无贡献")------固定值不随内部值变化。valueBoundaryCoeffs:返回*this(即边界值本身)------固定值边界的取值就是它自己。gradientInternalCoeffs:返回−deltaCoeffs------梯度对内部值的系数(deltaCoeffs = 1/Δ,面到单元中心距离的倒数)。gradientBoundaryCoeffs:返回deltaCoeffs * (*this)------梯度对边界值的系数。write:先调父类write,再writeValueEntry写出value项。
这是"边界如何进矩阵"的教科书答案 :拉普拉斯的边界系数 =
pGamma × gradientInternalCoeffs()(见第 6.4 节)。fixedValue用−Δ⁻¹与Δ⁻¹·value分别贡献对角与源,这正是"第一类边界"的有限体积离散。
12.4 对照:fixedGradient 的 coeffs(逐行解读)
fixedGradientFvPatchField.txx 第 169--221 行给出"第二类边界(给定梯度)"的对照:
cpp
template<class Type>
void Foam::fixedGradientFvPatchField<Type>::evaluate(const Pstream::commsTypes)
{
if (!this->updated())
{
this->updateCoeffs();
}
Field<Type>::operator=
(
this->patchInternalField() + gradient_/this->patch().deltaCoeffs()
);
fvPatchField<Type>::evaluate();
}
template<class Type>
Foam::tmp<Foam::Field<Type>>
Foam::fixedGradientFvPatchField<Type>::valueInternalCoeffs(const tmp<scalarField>&) const
{
return tmp<Field<Type>>::New(this->size(), pTraits<Type>::one);
}
template<class Type>
Foam::tmp<Foam::Field<Type>>
Foam::fixedGradientFvPatchField<Type>::valueBoundaryCoeffs(const tmp<scalarField>&) const
{
return gradient()/this->patch().deltaCoeffs();
}
template<class Type>
Foam::tmp<Foam::Field<Type>>
Foam::fixedGradientFvPatchField<Type>::gradientInternalCoeffs() const
{
return tmp<Field<Type>>::New(this->size(), Zero);
}
template<class Type>
Foam::tmp<Foam::Field<Type>>
Foam::fixedGradientFvPatchField<Type>::gradientBoundaryCoeffs() const
{
return gradient();
}
逐行:
evaluate():边界值 = 内部场外推 + gradient_/deltaCoeffs(即由给定梯度反推面值)。valueInternalCoeffs:返回 1(面值 = 1×内部值 + 边界项)。valueBoundaryCoeffs:gradient/deltaCoeffs。gradientInternalCoeffs:返回 0(梯度对内部值无依赖------梯度是给定的)。gradientBoundaryCoeffs:直接返回gradient()。
与 fixedValue 的对照表:
| 方法 | fixedValue |
fixedGradient |
|---|---|---|
valueInternalCoeffs |
0 | 1 |
valueBoundaryCoeffs |
*this |
gradient/deltaCoeffs |
gradientInternalCoeffs |
−deltaCoeffs |
0 |
gradientBoundaryCoeffs |
deltaCoeffs*(*this) |
gradient |
| 物理含义 | 给定值(第一类) | 给定梯度(第二类) |
规律 :fixedValue 把"值"当已知(gradient 依赖内部值);fixedGradient 把"梯度"当已知(value 依赖内部值)。二者的 coeffs 恰好互补。
12.5 一个真实的时变边界:updateCoeffs + write(逐行解读)
timeVaryingMappedFixedValueFvPatchField.txx(derived/timeVaryingMappedFixedValue/ 第 151--186 行)演示了"值类 + 时变"的写法:
cpp
template<class Type>
void Foam::timeVaryingMappedFixedValueFvPatchField<Type>::updateCoeffs()
{
if (this->updated())
{
return;
}
const scalar t = this->db().time().timeOutputValue();
fvPatchField<Type>::operator==(uniformValue_->value(t));
if (debug)
{
auto limits = gMinMax(*this);
auto avg = gAverage(*this);
Pout<< "updateCoeffs : set fixedValue to min:" << limits.min()
<< " max:" << limits.max()
<< " avg:" << avg << endl;
}
this->parent_bctype::updateCoeffs();
}
template<class Type>
void Foam::timeVaryingMappedFixedValueFvPatchField<Type>::write(Ostream& os) const
{
fvPatchField<Type>::write(os);
uniformValue_->writeData(os);
fvPatchField<Type>::writeValueEntry(os);
}
逐行:
- 第 154--157 行:
if (this->updated()) return;------防重复计算的"守门"。这是所有updateCoeffs的标准开头(对照第 12.1 节evaluate的语义)。 - 第 160 行
this->db().time().timeOutputValue():取当前"输出时间"(用于时变函数求值)。 - 第 161 行
fvPatchField<Type>::operator==(...):给边界赋值 (operator==在边界语境下是"设置 patch 值")。 - 第 163--171 行:
debug时打印 min/max/avg(gMinMax/gAverage是并行全局归约)。 - 第 173 行:调父类
updateCoeffs()------务必调用,否则updated_不会被置真。 write:先fvPatchField<Type>::write(os)写基础项,再写自定义数据、再写value项。
调用
parent_bctype::updateCoeffs()是新手最常见的遗漏 :漏了它,updated_永远不变,evaluate会每次重算,或updated()判断错乱。
12.6 注册宏:makePatchTypeField(逐行解读)
自定义类写完后必须注册到 RTS 表。宏定义在 fvPatchFieldMacros.H(src/finiteVolume/fields/fvPatchFields/fvPatchField/fvPatchFieldMacros.H)第 41--101 行:
cpp
#undef addToPatchFieldRunTimeSelection
#define addToPatchFieldRunTimeSelection(PatchTypeField, typePatchTypeField) \
addToRunTimeSelectionTable(PatchTypeField, typePatchTypeField, patch); \
addToRunTimeSelectionTable(PatchTypeField, typePatchTypeField, patchMapper); \
addToRunTimeSelectionTable(PatchTypeField, typePatchTypeField, dictionary);
#define makePatchTypeField(PatchTypeField, typePatchTypeField) \
defineTypeNameAndDebug(typePatchTypeField, 0); \
addToPatchFieldRunTimeSelection(PatchTypeField, typePatchTypeField);
逐行:
addToPatchFieldRunTimeSelection一次注册三张表 :patch、patchMapper、dictionary------三类构造来源。makePatchTypeField=defineTypeNameAndDebug(TypeName/debug)+ 三表注册。
三张表的用途:
| 表 | 构造来源 | 何时用 |
|---|---|---|
patch |
从 fvPatch+内部场 |
程序内部构造 |
patchMapper |
从旧 patchField + mapper | 网格变化/映射 |
dictionary |
从字典 | 从 0/T 的 boundaryField 读(最常用) |
模板化边界 则用 makePatchFields(bcType)(第 138--144 行)一次性为 scalar/vector/sphericalTensor/symmTensor/tensor 五种类型注册,或 makePatchTypeFieldTypedefs(bcType)(第 160--166 行)只建 typedef。真实对照:timeVaryingMappedFixedValueFvPatchFields.cxx 第 34--38 行:
cpp
namespace Foam
{
makePatchTypeFieldTypedefs(timeVaryingMappedFixedValue);
makePatchFields(timeVaryingMappedFixedValue);
}
12.7 机制链 C 汇总(按依赖顺序)
scss
① 继承合适基类(fixedValue/fixedGradient/fvPatchField<Type>)
② TypeName("myBC") RTS 键
③ 构造函数族(patch/值/字典/mapper/copy-with-iF)
④ clone(iF) 重建
⑤ updateCoeffs() [开头 if(updated()) return; 结尾调父类] 算
⑥ evaluate() [若继承值类,多半可复用;否则自实现] 用
⑦ 四个 *Coeffs() [影响方程则必须] 进矩阵
⑧ write(os) [写 value/自定义项] 落盘
⑨ makePatchTypeField(基类, myBC) 或 makePatchFields(myBC) 注册
⑩ Make/files: LIB=...; Make/options: LIB_LIBS=-lfiniteVolume 构建
⑪ 算例边界: type myBC; ... 使用
13. 机制链 D:CourantNo.H 的 Co 计算与自适应时间步
第 4 节旧稿只给了公式。这一节给出完整机制链 :从 CourantNo.H 的 Co,到 setDeltaT.H 的步长调整,到 Time::adjustDeltaT() 的写盘对齐,再到 setRDeltaT.H 的局域时间步(LTS)。
13.1 CourantNo.H(逐行解读)
src/finiteVolume/cfdTools/incompressible/CourantNo.H 第 34--50 行:
cpp
scalar CoNum = 0.0;
scalar meanCoNum = 0.0;
{
scalarField sumPhi
(
fvc::surfaceSum(mag(phi))().primitiveField()
);
CoNum = 0.5*gMax(sumPhi/mesh.V().field())*runTime.deltaTValue();
meanCoNum =
0.5*(gSum(sumPhi)/gSum(mesh.V().field()))*runTime.deltaTValue();
}
Info<< "Courant Number mean: " << meanCoNum
<< " max: " << CoNum << endl;
逐行:
- 第 34--35 行:初始化
CoNum/meanCoNum。 - 第 38--41 行:
fvc::surfaceSum(mag(phi))对每个单元求"四周面通量模之和Σ_f|φ_f|",取primitiveField()成scalarField sumPhi。 - 第 43 行:
CoNum = 0.5 × gMax(sumPhi/V) × Δt------最大库朗数 。gMax是并行全局最大(S2-16)。 - 第 45--46 行:
meanCoNum = 0.5 × (ΣsumPhi/ΣV) × Δt------平均库朗数 (gSum为并行全局求和)。注意平均用的是"总量比"(ΣsumPhi/ΣV),不是各单元库朗数的均值。 - 第 49--50 行:打印。
为什么是 0.5? 因为 Σ_f|φ_f| 是两面之和,而单元到面的"有效流速"约为其一半;系数 0.5 把这个和折算成单侧通量。这是 OpenFOAM 约定的库朗数定义。
13.2 setDeltaT.H(逐行解读)
src/finiteVolume/cfdTools/general/include/setDeltaT.H 第 36--53 行:
cpp
if (adjustTimeStep)
{
scalar maxDeltaTFact = maxCo/(CoNum + SMALL);
const scalar deltaTFact =
Foam::min(Foam::min(maxDeltaTFact, 1.0 + 0.1*maxDeltaTFact), 1.2);
runTime.setDeltaT
(
Foam::min
(
deltaTFact*runTime.deltaTValue(),
maxDeltaT
)
);
Info<< "deltaT = " << runTime.deltaTValue() << endl;
}
逐行:
- 第 36 行:只有
controlDict的adjustTimeStep为真才调整。 - 第 38 行:
maxDeltaTFact = maxCo/(CoNum + SMALL)------目标步长放大系数 (SMALL防止除零)。 - 第 40--41 行:
deltaTFact = min( min(maxDeltaTFact, 1+0.1·maxDeltaTFact), 1.2 )------限制增幅 :- 与
1+0.1·maxDeltaTFact取小:即使maxDeltaTFact很大,也只允许"最多 +10%"的温和放大; - 与
1.2取小:单步最多放大 1.2 倍。
- 与
- 第 43--50 行:
setDeltaT(min(deltaTFact·Δt, maxDeltaT))------新步长,并封顶maxDeltaT。 - 缩减是立即的 (
deltaTFact < 1时直接取maxDeltaTFact),放大是阻尼的(受 1+0.1·fact 与 1.2 双重限制)------正是文件头注释(第 29--32 行)所说 "Reduction of time-step is immediate, but increase is damped to avoid unstable oscillations."
配套读取控制量的 readTimeControls.H(第 35--42 行):
cpp
adjustTimeStep =
runTime.controlDict().getOrDefault("adjustTimeStep", false);
maxCo =
runTime.controlDict().getOrDefault<scalar>("maxCo", 1);
maxDeltaT =
runTime.controlDict().getOrDefault<scalar>("maxDeltaT", GREAT);
即三个量都有默认值:adjustTimeStep=false、maxCo=1、maxDeltaT=GREAT(无上限)。
13.3 Time::adjustDeltaT():写盘对齐(逐行解读)
setDeltaT.H 改的是"物理步长",而 Time::adjustDeltaT()(src/OpenFOAM/db/Time/Time.C 第 103--144 行)负责把步长对齐到"下次写盘时间":
cpp
void Foam::Time::adjustDeltaT()
{
bool adjustTime = false;
scalar timeToNextWrite = VGREAT;
if (writeControl_ == wcAdjustableRunTime)
{
adjustTime = true;
timeToNextWrite = max
(
0.0,
(writeTimeIndex_ + 1)*writeInterval_ - (value() - startTime_)
);
}
if (adjustTime)
{
scalar nSteps = timeToNextWrite/deltaT_;
// For tiny deltaT the label can overflow!
if (nSteps < scalar(labelMax))
{
label nStepsToNextWrite = max(1, round(nSteps));
scalar newDeltaT = timeToNextWrite/nStepsToNextWrite;
// Control the increase of the time step to within a factor of 2
// and the decrease within a factor of 5.
if (newDeltaT >= deltaT_)
{
deltaT_ = min(newDeltaT, 2.0*deltaT_);
}
else
{
deltaT_ = max(newDeltaT, 0.2*deltaT_);
}
}
}
functionObjects_.adjustTimeStep();
}
逐行:
- 第 105--106 行:
adjustTime标志与timeToNextWrite(初值VGREAT)。 - 第 108--116 行:当
writeControl == wcAdjustableRunTime(controlDict写writeControl adjustableRunTime;)时,计算"距下次写盘还剩多少时间 ":(writeTimeIndex_+1)×writeInterval − (当前时间−起始时间)。 - 第 118--141 行:若需调整,令
nSteps = timeToNextWrite/Δt,取整得"到下次写盘的步数"nStepsToNextWrite,再反算newDeltaT = timeToNextWrite/nStepsToNextWrite。- 第 132--135 行:若新步长变大,增幅限 2 倍 (
min(newDeltaT, 2Δ))。 - 第 137--139 行:若变小,降幅限 5 倍 (
max(newDeltaT, 0.2Δ))。
- 第 132--135 行:若新步长变大,增幅限 2 倍 (
- 第 143 行:
functionObjects_.adjustTimeStep()------把调整机会交给函数对象(S2-14)。
为什么要有这一步? 若只用 setDeltaT.H,Δt 可能不能整除写盘间隔,导致"写盘时间点被跳过"。adjustDeltaT() 保证恰好在下一次写盘时刻落地 ------这是 adjustableRunTime 与 runTime 的关键区别(第 15 节对比表)。
13.4 setRDeltaT.H:局域时间步(LTS)(逐行解读)
上面是"全局单一步长 "的自适应。OpenFOAM 还提供"每个单元各自的步长 "------局域时间步(LTS),在 setRDeltaT.H 与 createRDeltaT.H 实现。applications/solvers/incompressible/pimpleFoam/setRDeltaT.H 第 1--79 行:
cpp
{
volScalarField& rDeltaT = trDeltaT.ref();
const dictionary& pimpleDict = pimple.dict();
scalar maxCo(pimpleDict.getOrDefault<scalar>("maxCo", 0.8));
scalar rDeltaTSmoothingCoeff(pimpleDict.getOrDefault<scalar>("rDeltaTSmoothingCoeff", 0.02));
scalar rDeltaTDampingCoeff(pimpleDict.getOrDefault<scalar>("rDeltaTDampingCoeff", 1.0));
scalar maxDeltaT(pimpleDict.getOrDefault<scalar>("maxDeltaT", GREAT));
volScalarField rDeltaT0("rDeltaT0", rDeltaT);
// Set the reciprocal time-step from the local Courant number
rDeltaT.ref() = max
(
1/dimensionedScalar("maxDeltaT", dimTime, maxDeltaT),
fvc::surfaceSum(mag(phi))()()
/((2*maxCo)*mesh.V())
);
// Update the boundary values of the reciprocal time-step
rDeltaT.correctBoundaryConditions();
{
auto limits = gMinMax(rDeltaT.primitiveField());
limits.reset(1/(limits.max()+VSMALL), 1/(limits.min()+VSMALL));
Info<< "Flow time scale min/max = "
<< limits.min() << ", " << limits.max() << endl;
}
if (rDeltaTSmoothingCoeff < 1.0)
{
fvc::smooth(rDeltaT, rDeltaTSmoothingCoeff);
}
{
auto limits = gMinMax(rDeltaT.primitiveField());
limits.reset(1/(limits.max()+VSMALL), 1/(limits.min()+VSMALL));
Info<< "Smoothed flow time scale min/max = " << limits.min() << ", " << limits.max() << endl;
}
if
(
rDeltaTDampingCoeff < 1.0
&& runTime.timeIndex() > runTime.startTimeIndex() + 1
)
{
rDeltaT = rDeltaT0*max(rDeltaT/rDeltaT0, scalar(1) - rDeltaTDampingCoeff);
auto limits = gMinMax(rDeltaT.primitiveField());
limits.reset(1/(limits.max()+VSMALL), 1/(limits.min()+VSMALL));
Info<< "Damped flow time scale min/max = " << limits.min() << ", " << limits.max() << endl;
}
}
逐行:
- 第 2 行:取
rDeltaT(倒时间步1/Δt)。LTS 的核心就是"存倒时间步"而非时间步。 - 第 4--24 行:从
pimple子字典(fvSolution的PIMPLE)读maxCo(默认 0.8)、rDeltaTSmoothingCoeff(默认 0.02)、rDeltaTDampingCoeff(默认 1.0)、maxDeltaT(默认 GREAT)。 - 第 26 行:保存旧
rDeltaT0(供阻尼用)。 - 第 29--34 行:核心公式 ------
rDeltaT = max(1/maxDeltaT, Σ|φ|/(2·maxCo·V))。即由"局域库朗数 "反推每个单元的1/Δt:Σ|φ|/(2·maxCo·V) = 1/Δt_loc,与第 13.1 节Co = 0.5·(Σ|φ|/V)·Δt完全同源(移项即得)。 - 第 37 行:
correctBoundaryConditions()更新边界值。 - 第 40--45 行:归一化打印"流动时间尺度 min/max"(取倒数,因为存的是
rDeltaT)。 - 第 47--50 行:若
rDeltaTSmoothingCoeff < 1,用fvc::smooth平滑rDeltaT(避免相邻单元时间步突变)。 - 第 63--78 行:若
rDeltaTDampingCoeff < 1且已跑够两步,阻尼rDeltaT的增长:rDeltaT = rDeltaT0·max(rDeltaT/rDeltaT0, 1−damp)------只允许"以旧值的分数增长",防止振荡。
13.5 createRDeltaT.H 与 localEulerDdt(逐行解读)
src/finiteVolume/cfdTools/general/include/createRDeltaT.H 第 1--26 行:
cpp
bool LTS = fv::localEulerDdt::enabled(mesh);
tmp<volScalarField> trDeltaT;
if (LTS)
{
Info<< "Using LTS" << endl;
trDeltaT = tmp<volScalarField>
(
new volScalarField
(
IOobject(fv::localEulerDdt::rDeltaTName, runTime.timeName(), mesh,
IOobject::READ_IF_PRESENT, IOobject::AUTO_WRITE),
mesh,
dimensionedScalar("one", dimless/dimTime, 1),
fvPatchFieldBase::extrapolatedCalculatedType()
)
);
}
逐行:
-
第 1 行:
fv::localEulerDdt::enabled(mesh)判断是否启用 LTS------其实现(localEulerDdt.C第 39--44 行)是"fvSchemes的默认 ddt 是否等于localEuler":cppbool Foam::fv::localEulerDdt::enabled(const fvMesh& mesh) { return word(mesh.ddtScheme("default")) == fv::localEulerDdtScheme<scalar>::typeName; } -
第 5--11 行:仅当 LTS 为真才创建
rDeltaT场,名字来自fv::localEulerDdt::rDeltaTName(localEulerDdt.C第 33 行定义为"rDeltaT"),量纲dimless/dimTime(即1/s)。
配套的 localEulerDdt 名字 (localEulerDdt.C 第 33--35 行):
cpp
Foam::word Foam::fv::localEulerDdt::rDeltaTName("rDeltaT");
Foam::word Foam::fv::localEulerDdt::rDeltaTfName("rDeltaTf");
Foam::word Foam::fv::localEulerDdt::rSubDeltaTName("rSubDeltaT");
即 LTS 需要三张倒时间步场:体心 rDeltaT、面心 rDeltaTf、子循环 rSubDeltaT。
13.6 机制链 D 汇总
scss
全局自适应(runTime):
controlDict: adjustTimeStep true; maxCo; maxDeltaT
→ readTimeControls.H 读三量
→ CourantNo.H 算 CoNum = 0.5·gMax(Σ|φ|/V)·Δt
→ setDeltaT.H Δt ← min(damped(maxCo/Co)·Δt, maxDeltaT)
→ Time::adjustDeltaT() 对齐 writeControl adjustableRunTime 的写盘点
局域自适应(LTS):
fvSchemes: ddtSchemes { default localEuler; }
→ createRDeltaT.H 建 rDeltaT 场
→ setRDeltaT.H rDeltaT ← max(1/maxDeltaT, Σ|φ|/(2·maxCo·V)) → smooth → damp
14. 可直接复制的完整文件模板
前面讲的是"为什么",这一节给"怎么抄"。每个模板都配"逐项说明表",改哪几行即可变成你自己的东西。
14.1 模板一:库的 Make/files
ini
myBCFvPatchScalarField.C
myBCFvPatchVectorField.C
LIB = $(FOAM_USER_LIBBIN)/libmyBC
逐项说明表:
| 行 | 内容 | 说明 | 常见改法 |
|---|---|---|---|
| 1 | myBCFvPatchScalarField.C |
源文件(不含 =,被收入 SOURCE) |
换成你的 .C |
| 2 | myBCFvPatchVectorField.C |
第二个源文件 | 有几个写几个 |
| 3 | LIB = $(FOAM_USER_LIBBIN)/libmyBC |
产物:用户共享库 libmyBC.so |
换库名;若要装到主目录用 $(FOAM_LIBBIN) |
为什么用
$(FOAM_USER_LIBBIN)? 因为你没有系统目录写权限(或不想污染系统)。用户库目录随你的环境变量定义,运行时用libs ("libmyBC.so")加载。
14.2 模板二:库的 Make/options
ini
EXE_INC = \
-I$(LIB_SRC)/finiteVolume/lnInclude
LIB_LIBS = \
-lfiniteVolume
逐项说明表:
| 项 | 内容 | 说明 | 常见改法 |
|---|---|---|---|
EXE_INC |
-I$(LIB_SRC)/finiteVolume/lnInclude |
找 fvPatchField.H/volFields.H 等 |
用网格工具再加 -I$(LIB_SRC)/meshTools/lnInclude |
LIB_LIBS |
-lfiniteVolume |
库目标 用 LIB_LIBS(不是 EXE_LIBS!) |
用 meshTools/fvOptions 再加对应 -l |
两个"最易错"点 :(1) 库目录必须用
LIB_LIBS;(2)EXE_INC名字虽含 "EXE",但库里也要用它 (它同时服务可执行与库目标,见general第 30 行注释 "for executable or library targets")。
14.3 模板三:createFields.H
cpp
Info<< "Reading field T\n" << endl;
volScalarField T
(
IOobject
(
"T",
runTime.timeName(),
mesh,
IOobject::MUST_READ,
IOobject::AUTO_WRITE
),
mesh
);
Info<< "Reading transportProperties\n" << endl;
IOdictionary transportProperties
(
IOobject
(
"transportProperties",
runTime.constant(),
mesh,
IOobject::MUST_READ_IF_MODIFIED,
IOobject::NO_WRITE
)
);
Info<< "Reading diffusivity DT\n" << endl;
dimensionedScalar DT("DT", dimViscosity, transportProperties);
#include "createFvOptions.H"
逐项说明表:
| 字段 | 三项配置 | 取值 | 说明 |
|---|---|---|---|
T |
名字/目录/注册表 | "T" / runTime.timeName() / mesh |
主待解场,MUST_READ+AUTO_WRITE |
transportProperties |
名字/目录/注册表 | "transportProperties" / runTime.constant() / mesh |
物性字典,MUST_READ_IF_MODIFIED |
DT |
构造方式 | dimensionedScalar("DT", 量纲, 字典) |
从字典取,避免写 0/DT |
createFvOptions.H |
--- | --- | 引入 fvOptions |
与
laplacianFoam的差异 :真实laplacianFoam/createFields.H用READ_IF_PRESENT+headerOk()回退(第 8 节);本模板是"直接读字典 "的简化版,等价于scalarTransportFoam/createFields.H第 33--50 行的写法。二者取其一即可。
14.4 模板四:求解器主文件 .C(瞬态通用骨架)
cpp
#include "fvCFD.H"
#include "fvOptions.H"
#include "simpleControl.H" // 或 pimpleControl.H
int main(int argc, char *argv[])
{
argList::addNote
(
"My scalar transport solver."
);
#include "postProcess.H"
#include "addCheckCaseOptions.H"
#include "setRootCaseLists.H"
#include "createTime.H"
#include "createMesh.H"
simpleControl simple(mesh); // 稳态;瞬态换 pimpleControl
#include "createFields.H"
Info<< "\nCalculating my field\n" << endl;
while (simple.loop()) // 瞬态换 while (runTime.loop())
{
Info<< "Time = " << runTime.timeName() << nl << endl;
while (simple.correctNonOrthogonal())
{
fvScalarMatrix TEqn
(
fvm::ddt(T) - fvm::laplacian(DT, T)
==
fvOptions(T)
);
fvOptions.constrain(TEqn);
TEqn.solve();
fvOptions.correct(T);
}
runTime.write();
runTime.printExecutionTime(Info);
}
Info<< "End\n" << endl;
return 0;
}
逐项说明表:
| 片段 | 作用 | 稳态 | 瞬态 |
|---|---|---|---|
#include "fvCFD.H" |
引入 Time/fvMesh/fvm/fvc |
保留 | 保留 |
simpleControl simple(mesh) |
控制对象 | simpleControl |
pimpleControl/pisoControl |
| 外层循环 | 时间/外迭代 | while (simple.loop()) |
while (runTime.loop()) |
while (simple.correctNonOrthogonal()) |
非正交修正 | 保留 | 保留 |
TEqn 方程 |
离散 | fvm::ddt - fvm::laplacian |
加 + fvm::div(phi, T) |
constrain/solve/correct |
源项三调用 | 保留 | 保留 |
| 写盘 | 落盘 | #include "write.H" 或 runTime.write() |
同 |
printExecutionTime |
计时 | 保留 | 保留 |
瞬态化的三处改动 (详见第 17 节):①
simpleControl→pimpleControl/runTime;②simple.loop()→runTime.loop(),并加#include "CourantNo.H"/setDeltaT.H/++runTime;③ 方程加对流项fvm::div(phi, T)并#include "createPhi.H"。
14.5 模板五:自定义边界条件 .H 与 .C
头文件 mySineFvPatchScalarField.H (继承 fixedValue,实现时变正弦):
cpp
#ifndef mySineFvPatchScalarField_H
#define mySineFvPatchScalarField_H
#include "fixedValueFvPatchFields.H"
namespace Foam
{
class mySineFvPatchScalarField
:
public fixedValueFvPatchScalarField
{
// Private Data
scalar amplitude_;
scalar frequency_;
public:
TypeName("mySine");
// Constructors
mySineFvPatchScalarField
(
const fvPatch&,
const DimensionedField<scalar, volMesh>&
);
mySineFvPatchScalarField
(
const fvPatch&,
const DimensionedField<scalar, volMesh>&,
const dictionary&
);
mySineFvPatchScalarField
(
const mySineFvPatchScalarField&,
const fvPatch&,
const DimensionedField<scalar, volMesh>&,
const fvPatchFieldMapper&
);
mySineFvPatchScalarField
(
const mySineFvPatchScalarField&,
const DimensionedField<scalar, volMesh>&
);
virtual tmp<fvPatchScalarField> clone
(
const DimensionedField<scalar, volMesh>& iF
) const
{
return fvPatchScalarField::Clone(*this, iF);
}
// Member Functions
virtual void updateCoeffs();
virtual void write(Ostream&) const;
};
} // End namespace Foam
#endif
源文件 mySineFvPatchScalarField.C:
cpp
#include "mySineFvPatchScalarField.H"
#include "addToRunTimeSelectionTable.H"
#include "volFields.H"
namespace Foam
{
// Constructors
mySineFvPatchScalarField::mySineFvPatchScalarField
(
const fvPatch& p,
const DimensionedField<scalar, volMesh>& iF
)
:
fixedValueFvPatchScalarField(p, iF),
amplitude_(0),
frequency_(1)
{}
mySineFvPatchScalarField::mySineFvPatchScalarField
(
const fvPatch& p,
const DimensionedField<scalar, volMesh>& iF,
const dictionary& dict
)
:
fixedValueFvPatchScalarField(p, iF, dict, IOobjectOption::NO_READ),
amplitude_(dict.get<scalar>("amplitude")),
frequency_(dict.get<scalar>("frequency"))
{
evaluate();
}
mySineFvPatchScalarField::mySineFvPatchScalarField
(
const mySineFvPatchScalarField& ptf,
const fvPatch& p,
const DimensionedField<scalar, volMesh>& iF,
const fvPatchFieldMapper& mapper
)
:
fixedValueFvPatchScalarField(ptf, p, iF, mapper),
amplitude_(ptf.amplitude_),
frequency_(ptf.frequency_)
{}
mySineFvPatchScalarField::mySineFvPatchScalarField
(
const mySineFvPatchScalarField& ptf,
const DimensionedField<scalar, volMesh>& iF
)
:
fixedValueFvPatchScalarField(ptf, iF),
amplitude_(ptf.amplitude_),
frequency_(ptf.frequency_)
{}
// Member Functions
void mySineFvPatchScalarField::updateCoeffs()
{
if (updated())
{
return;
}
const scalar t = db().time().timeOutputValue();
operator==(amplitude_*sin(2*constant::mathematical::pi*frequency_*t));
fixedValueFvPatchScalarField::updateCoeffs();
}
void mySineFvPatchScalarField::write(Ostream& os) const
{
fixedValueFvPatchScalarField::write(os);
os.writeEntry("amplitude", amplitude_);
os.writeEntry("frequency", frequency_);
}
// Register
makePatchTypeField(fvPatchScalarField, mySineFvPatchScalarField);
} // End namespace Foam
逐项说明表:
| 成员 | 关键写法 | 说明 |
|---|---|---|
| 继承 | public fixedValueFvPatchScalarField |
复用"固定值"语义,只需重写 updateCoeffs |
TypeName("mySine") |
字典键 | type mySine; |
| 字典构造 | 先 parent(p,iF,dict,NO_READ) 再读 amplitude_/frequency_,末尾 evaluate() |
从 0/T 的 patch 段读参数 |
clone |
fvPatchScalarField::Clone(*this, iF) |
按基类工厂复制 |
updateCoeffs |
if (updated()) return; ... operator==(值) ... 调父类 |
防重复 + 赋值 + 置 updated_ |
write |
先父类再写自定义项 | 保证能回写 |
| 注册 | makePatchTypeField(fvPatchScalarField, mySineFvPatchScalarField); |
三表注册(第 12.6 节) |
对应算例写法 (0/T):
ini
boundaryField
{
myPatch
{
type mySine;
amplitude 10;
frequency 0.5;
value uniform 0; // 初始值(MUST_READ 语义需要)
}
}
value uniform 0;不能省 :fixedValue家族的字典构造默认MUST_READ(见fixedValueFvPatchField.H第 108 行),需要初始值;否则可能报缺value。
14.6 模板六:controlDict 的自适应时间步段
ini
application mySolver;
startFrom startTime;
startTime 0;
stopAt endTime;
endTime 10;
deltaT 1e-05;
writeControl adjustableRunTime; // 关键:可调 + 写盘对齐
writeInterval 0.1;
adjustTimeStep yes; // 开启自适应
maxCo 0.5; // 目标最大库朗数
maxDeltaT 1e-03; // 步长上限
逐项说明表:
| 项 | 取值 | 说明 | 依赖 |
|---|---|---|---|
writeControl |
adjustableRunTime |
允许 Time::adjustDeltaT 对齐写盘点(第 13.3 节) |
自适应时建议用 |
adjustTimeStep |
yes/no |
readTimeControls.H 读它(默认 false) |
CourantNo 需 phi |
maxCo |
0.5--1 | 目标库朗数(默认 1) | setDeltaT.H |
maxDeltaT |
物理上限 | 步长封顶(默认 GREAT) |
setDeltaT.H |
不加
maxDeltaT的后果 :在"流速趋于零"的区域,Co≈0使maxDeltaTFact巨大,步长被 1.2 倍限幅放大直到无意义的大------maxDeltaT就是防这个的。
15. 对比与选型表
15.1 表一:自编译库 vs coded
| 维度 | 自编译库(LIB) |
coded(运行期编译,S2-12) |
|---|---|---|
| 编译时机 | wmake 一次,之后长期复用 |
运行期 用 wmake 现编(#codeStream/coded 类型) |
| 产物 | libxxx.so 进 $FOAM_USER_LIBBIN |
临时 .so/对象,缓存在算例 dynamicCode/ |
| 调试 | 有调试符号,可断点 | 编译日志要看 dynamicCode 目录 |
| 性能 | 与主程序同 | 有首次编译开销,运行同 |
| 适用 | 长期模型/边界/库、要复用 | 一次性、试验、不想维护 Make 文件 |
| 依赖 | 需写 Make/files+Make/options |
源码内联在字典里 |
| 加载 | libs ("libxxx.so") |
自动 |
选型 :要复用/要性能/多人用 → 自编译库;一次性试验/快速验证 → coded。二者可并存。
15.2 表二:时间步自适应策略对比
| 策略 | 机制文件 | 步长粒度 | 关键字典 | 适用 |
|---|---|---|---|---|
| 固定步长 | --- | 全局固定 | deltaT |
稳态、简单瞬态 |
| 全局自适应 | CourantNo.H+setDeltaT.H |
全局单一 | adjustTimeStep/maxCo/maxDeltaT |
瞬态 CFD 主流 |
| 全局+写盘对齐 | Time::adjustDeltaT() |
全局、对齐写盘 | writeControl adjustableRunTime |
需精确写盘点 |
| 局域时间步 LTS | createRDeltaT.H+setRDeltaT.H |
每单元各自 | ddtSchemes localEuler、maxCo/rDeltaTSmoothingCoeff/rDeltaTDampingCoeff |
稳态问题加速 |
15.3 表三:编译目标 EXE/LIB/LIBO/SEXE 对比
| 目标 | Make/files 写法 |
产物 | 链接变量 | 场景 |
|---|---|---|---|---|
EXE |
EXE = $(FOAM_APPBIN)/x |
可执行(链接 OpenFOAM 库) | EXE_LIBS |
求解器、utilities |
LIB(.so) |
LIB = $(FOAM_USER_LIBBIN)/libx |
共享库 | LIB_LIBS |
模型/边界/函数对象库 |
LIB(.a) |
同上 + make lib |
静态库 | LIB_LIBS |
特殊静态链接 |
LIBO(.o) |
同上 + make libo |
单对象库 | LIBO_LIBS |
模块/单对象 |
SEXE |
EXE = ... + make exe |
独立可执行(不链接 OpenFOAM) | EXE_LIBS |
独立小工具 |
15.4 表四:常见边界条件类型对比(fv 标量)
| 类型 | 基类 | fixesValue() |
coeffs 特征 | 物理 |
|---|---|---|---|---|
fixedValue |
fvPatchField<Type> |
true | gradientInternalCoeffs=−Δ⁻¹ |
第一类(给定值) |
fixedGradient |
fvPatchField<Type> |
false | gradientInternalCoeffs=0 |
第二类(给定梯度) |
zeroGradient |
fvPatchField<Type> |
false | 梯度为 0 | 绝热/对称 |
calculated |
fvPatchField<Type> |
false | 由内部场外推 | 从其他场求值 |
fixedFluxPressure |
fvPatchField<scalar> |
--- | 给定通量压力 | 压力-速度耦合 |
自定义 mySine |
fixedValue |
true | 继承 fixedValue |
时变正弦 |
15.5 表五:求解器骨架对比(稳态 vs 瞬态)
| 维度 | laplacianFoam(稳态 SIMPLE 型) |
scalarTransportFoam(瞬态对流扩散) |
|---|---|---|
| 控制对象 | simpleControl simple(mesh) |
simpleControl simple(mesh) |
| 外层循环 | while (simple.loop()) |
while (simple.loop()) |
phi |
无 | #include "createPhi.H" |
| 库朗数 | 无 | #include "CourantNo.H" |
| ddt 格式 | Euler(配 steadyState 由 fvSchemes 决定) |
Euler/backward |
| 方程 | fvm::ddt(T) - fvm::laplacian(DT,T) |
fvm::ddt(T) + fvm::div(phi,T) - fvm::laplacian(DT,T) |
relax |
无 | TEqn.relax() |
| 场 | T、DT |
T、U、phi、DT |
真实对照见第 17 节(
scalarTransportFoam.C源码)。
15.6 表六:Make/options 变量选择对照
| 目标类型 | include 用 | 链接用 | 反例 |
|---|---|---|---|
可执行 EXE |
EXE_INC |
EXE_LIBS |
写 LIB_LIBS → 不链接 |
共享库 LIB |
EXE_INC |
LIB_LIBS |
写 EXE_LIBS → 不链接 |
对象库 LIBO |
EXE_INC |
LIBO_LIBS |
写 LIB_LIBS → 不链接 |
16. 常见报错/警告速查(≥20 条)
下列"报错样式"为编译/运行期典型文本样式 (编译器为 gcc/llvm-mingw,
$FOAM_USER_*为占位)。定位时请以你机器上的实际输出为准,但成因与对策是通用的。
16.1 编译/链接类(1--10)
1. undefined reference to ...(未定义引用)
- 样式 :
/usr/bin/ld: laplacianFoam.o: in function 'main': laplacianFoam.C:(.text+0x1a): undefined reference to 'Foam::fvMesh::fvMesh(...)' - 成因 :
Make/options的EXE_LIBS缺库(如漏-lfiniteVolume)。 - 排查 :把报错里的符号名反查是哪一库(
fvMesh→finiteVolume);对照同目录求解器的Make/options。 - 解决 :补
-lxxx;注意顺序(被依赖库在后)。
2. 库链接顺序错误导致的 undefined reference
- 样式 :即使库都写了,仍
undefined reference to 'Foam::...' - 成因 :
-lA -lB中 B 依赖 A 却没放 A 之后(单遍扫描)。 - 解决 :调整
-l顺序,让"被依赖者靠后";或重复列出。
3. xxx.H: No such file or directory(找不到头文件)
- 样式 :
laplacianFoam.C:57:10: fatal error: fvCFD.H: No such file or directory - 成因 :
EXE_INC缺-I.../finiteVolume/lnInclude,或该库从未跑过wmakeLnInclude导致lnInclude/不存在。 - 解决 :补
-I;在库目录跑wmakeLnInclude -force .(或wmake libso前先wmakeLnInclude)。
4. wmake 找不到 Make/files
- 样式 :
wmake: no Make/files found in this directory - 成因 :不在含
Make/的目录里跑wmake;或文件名拼错(应是Make/files不是makefiles)。 - 解决 :
cd到应用/库目录再wmake;核对Make/files、Make/options大小写。
5. No rule to make target 'Make/options'
- 样式 :
make: *** No rule to make target 'Make/options'. Stop. - 成因 :
Make/options缺失,或Make/目录为空。 - 解决:确保两个 Make 文件都存在(哪怕是空 options 也要有文件)。
6. 库目录误用 EXE_LIBS
- 样式 :库能编出,但加载时报
undefined symbol: ...(不是编译期错) - 成因 :库的
Make/options写了EXE_LIBS,wmake 不把它算进.so链接。 - 解决 :改成
LIB_LIBS(第 11.2、15.6 节)。
7. recompile with -fPIC / 位置无关代码错误
- 样式 :
relocation R_X86_64_PC32 against symbol ... can not be used when making a shared object; recompile with -fPIC - 成因 :目标文件未用
-fPIC编译而试图进.so。 - 解决 :确认走的是 wmake 的库规则(自动带
-fPIC);不要手工混用系统编译的对象。
8. 重复符号 multiple definition of ...
- 样式 :
multiple definition of 'Foam::myClass::myClass()' - 成因 :同一个源文件被列了两次,或
.H里写了非 inline 定义后被多处 include。 - 解决 :
Make/files去重;把定义移进.C/.txx或加inline。
9. TypeName/declareTypeName 未定义
- 样式 :
error: 'TypeName' was not declared in this scope - 成因 :缺
#include "xxx"(如未 includefvPatchField.H/runTimeSelectionTables.H)。 - 解决 :补对应头;模板类注意
.txx是否被包含。
10. .txx/.cxx 未参与编译导致 undefined
- 样式 :
undefined reference to 'Foam::myClass<Foam::scalar>::updateCoeffs()' - 成因 :模板实现放在
.txx,但Make/files只列了.C,且类头用#ifdef NoRepository才 include.txx------而你的类没有让编译器实例化。 - 解决 :把实现放
.C并在其中显式 include.H由编译器实例化;或按 OpenFOAM 惯例在.C里 include.txx(如fixedValueFvPatchFields.cxx的模式)。
16.2 运行期类(11--20)
11. 边界类型未注册
- 样式 :
--> FOAM FATAL IO ERROR: Unknown patchFieldType mySine for patch ...(并列出合法类型表) - 成因 :库未加载,或类未
makePatchTypeField注册。 - 解决 :
controlDict里libs ("libmyBC.so");确认注册宏在.C里且编译进库。
12. 库未加载
- 样式 :
--> FOAM Warning: Could not load library libmyBC.so或Cannot open shared object file - 成因 :
libs(...)名字写错;库不在$FOAM_USER_LIBBIN/LD_LIBRARY_PATH。 - 解决 :核对库名(含
lib前缀与.so);确认装到了用户库目录。
13. 场读不到(Cannot find file)
- 样式 :
--> FOAM FATAL IO ERROR: Cannot find file "0/T" ... - 成因 :
IOobject名字/时间目录与我们以为的不一致;0/缺场文件。 - 解决 :核对
IOobject("T", runTime.timeName(), ...)与磁盘文件名一致(第 8 节三处一致)。
14. Required entry 'value' : missing for patch ...
- 样式 :
Required entry 'value' : missing for patch myPatch in dictionary ... - 成因 :
fixedValue家族从字典构造要求value(MUST_READ)。 - 解决 :边界段补
value uniform ...;(第 14.5 节)。
15. Laplacian scheme not specified / Unknown laplacian scheme
- 样式 :
--> FOAM FATAL IO ERROR: Laplacian scheme not specified(并列出合法格式名) - 成因 :
fvSchemes的laplacianSchemes缺laplacian(DT,T)条目,且default为none(见laplacianScheme.C第 55--62 行)。 - 解决 :补
laplacian(DT,T) Gauss linear corrected;。
16. divScheme/gradScheme 缺失
- 样式 :
--> FOAM FATAL IO ERROR: div scheme not specified/Unknown grad scheme - 成因 :瞬态方程含
fvm::div(phi,T)/fvc::grad(T)但divSchemes/gradSchemes未配。 - 解决 :补
div(phi,T) Gauss linearUpwind grad(T);、grad(T) Gauss linear;。
17. 库朗数爆炸(Co 极大/Nan)
- 样式 :
Courant Number mean: inf max: inf或后续bounding .../nan - 成因 :
phi单位/量纲错;deltaT太大;对流格式不稳。 - 解决 :启用
adjustTimeStep+maxCo(第 13.2 节);检查phi由createPhi.H正确生成;换更稳的对流格式。
18. solve() 返回发散(DILUPCG: Solving for T, Initial residual = 1, Final residual = 1e+300)
- 样式 :线性求解器迭代到
Final residual = ...极大且不降 - 成因:矩阵病态(边界 coeffs 错、量纲错、非正交修正不足)。
- 解决 :增大
nNonOrthogonalCorrectors;加relax;检查边界实现(*Coeffs是否合理)。
19. 并行缺 phi/并行不一致
- 样式 :
-->> FOAM FATAL ERROR: ... field phi not found或decomposePar后各 CPU 结果对不上 - 成因 :并行求解器未生成/未写
phi(createPhi.H顺序问题),或fluxRequired未设。 - 解决 :确认
#include "createPhi.H"在方程前;fvSchemes加fluxRequired { default no; p yes; }(按需);用decomposePar/reconstructPar流程。
20. lduMatrix 相关 Unknown solver type xxx
- 样式 :
--> FOAM FATAL IO ERROR: Unknown solver type: mySolver(并列出合法求解器名) - 成因 :
fvSolution的solver名拼错(应如PCG/PBiCGStab/smoothSolver)。 - 解决 :核对求解器名单;
smoothSolver需配smoother。
16.3 补充条目(21--26)
21. Cannot open include file 'lnInclude/xxx.H'
- 成因 :
lnInclude未生成(同 3)。解决 :wmakeLnInclude -force .。
22. foamDlOpenLibs 相关(Windows)
- 样式 :Windows 下运行报
... entry point not found/DLL 缺失。 - 成因 :Windows 库导出符号未生成(S2-16 的
.def/libo机制)。 - 解决 :按 S2-16 的 Windows 构建流程处理导出;确认
Make/options被wmakeWindowsDlOpenLibs补全。
23. dimensionSet 量纲不匹配
- 样式 :
--> FOAM FATAL ERROR: dimensions ... not equal to ... - 成因 :
DT量纲(dimViscosity)与方程量纲不符;自定义边界返回的量纲错。 - 解决 :核对
dimensionedScalar量纲;fvMatrix构造时量纲乘积要一致(第 6.4 节)。
24. field T not found in objectRegistry
- 成因 :
createFields.H里IOobject注册名与后续引用名不一致,或构造顺序错(先引用后建)。 - 解决 :确保
createFields.H在方程之前 include(第 4.7 节顺序)。
25. readScalar/get<scalar> 失败
- 样式 :
--> FOAM FATAL IO ERROR: wrong token type ... expected scalar - 成因 :自定义边界参数(
amplitude/frequency)在字典里类型错(写成了词而非数)。 - 解决 :核对
dict.get<scalar>("amplitude")对应字典项确为数值。
26. write 时 gradTx 等场冲突
- 样式 :
--> FOAM FATAL ERROR: object gradTx ... already exists - 成因 :每步重复构造同名
IOobject且注册进同一注册表。 - 解决 :像
laplacianFoam/write.H那样在writeTime()内构造,或用NO_READ+局部构造并确保生命周期结束即注销。
17. 从 laplacianFoam 改造:scalarTransportFoam 对照
前面多次提到瞬态化,仓库里的成品是 applications/solvers/basic/scalarTransportFoam/。它的主循环(scalarTransportFoam.C 第 82--108 行)与 laplacianFoam 的差异恰好演示了"渐变异构"的标准路径:
cpp
Info<< "\nCalculating scalar transport\n" << endl;
#include "CourantNo.H"
while (simple.loop())
{
Info<< "Time = " << runTime.timeName() << nl << endl;
while (simple.correctNonOrthogonal())
{
fvScalarMatrix TEqn
(
fvm::ddt(T)
+ fvm::div(phi, T)
- fvm::laplacian(DT, T)
==
fvOptions(T)
);
TEqn.relax();
fvOptions.constrain(TEqn);
TEqn.solve();
fvOptions.correct(T);
}
runTime.write();
}
Info<< "End\n" << endl;
return 0;
与 laplacianFoam 的逐一差异:
| 位置 | laplacianFoam |
scalarTransportFoam |
变化含义 |
|---|---|---|---|
| 循环前 | 无 | #include "CourantNo.H" |
瞬态需监控库朗数 |
| 方程 | fvm::ddt - fvm::laplacian |
fvm::ddt + fvm::div(phi,T) - fvm::laplacian |
新增对流项 |
relax |
无 | TEqn.relax() |
对流项可能需欠松弛 |
| 循环末 | #include "write.H" |
runTime.write() |
简单写出(不额外算梯度) |
| 控制对象 | simpleControl |
simpleControl |
都稳态式外迭代 |
createFields.H |
T,DT |
T,U,phi,DT |
多了速度与通量 |
其 createFields.H(scalarTransportFoam/createFields.H 第 17--52 行)的差异:
cpp
Info<< "Reading field U\n" << endl;
volVectorField U
(
IOobject("U", runTime.timeName(), mesh, IOobject::MUST_READ, IOobject::AUTO_WRITE),
mesh
);
Info<< "Reading transportProperties\n" << endl;
IOdictionary transportProperties
(
IOobject("transportProperties", runTime.constant(), mesh,
IOobject::MUST_READ_IF_MODIFIED, IOobject::NO_WRITE)
);
Info<< "Reading diffusivity DT\n" << endl;
dimensionedScalar DT("DT", dimViscosity, transportProperties);
#include "createPhi.H"
#include "createFvOptions.H"
关键:
-
新增
volVectorField U(MUST_READ)。 -
#include "createPhi.H"必须在方程前 ------phi是fvm::div(phi, T)的输入。 -
createPhi.H(src/finiteVolume/cfdTools/incompressible/createPhi.H第 38--49 行)内容:cppsurfaceScalarField phi ( IOobject("phi", runTime.timeName(), mesh, IOobject::READ_IF_PRESENT, IOobject::AUTO_WRITE), fvc::flux(U) );即
phi = fvc::flux(U)(由U插值到面)。
它的 Make/options (scalarTransportFoam/Make/options 第 1--10 行)比 laplacianFoam 多了 sampling:
bash
EXE_INC = \
-I$(LIB_SRC)/finiteVolume/lnInclude \
-I$(LIB_SRC)/meshTools/lnInclude \
-I$(LIB_SRC)/sampling/lnInclude
EXE_LIBS = \
-lfiniteVolume \
-lfvOptions \
-lmeshTools \
-lsampling
这正是"克隆-变异"的效率 :整个新求解器只是在
laplacianFoam基础上加一个场(U)、加一个片段(createPhi.H)、加一项(fvm::div)、加两行库。这比"从零写"快一个数量级。
18. 调试、验证与小结
18.1 调试手段
| 手段 | 用途 | 出处 |
|---|---|---|
Info<< ... |
标准信息输出 | 随处可见 |
FatalErrorInFunction << ... << exit(FatalError) |
致命错误并终止 | 如 fvMatrixSolve.C 第 100 行 |
FatalIOErrorInFunction(dict) << ... << exit(FatalIOError) |
字典相关致命错误 | 如 laplacianScheme.C 第 57--61 行 |
Pout / DebugInfo |
受 debug 开关控制的调试输出 |
如 timeVaryingMapped 第 163--171 行 |
Info.masterStream(comm) |
只 master 进程输出 | 如 fvMatrixSolve.C 第 231 行 |
-dry-run / -dry-run-write |
只校验算例设置、跑 1 步 | addCheckCaseOptions.H + createMesh.H |
-debug |
打开相关类调试开关 | 标准命令行 |
checkMesh |
网格质量检查 | utilities |
调试原则 :先用极小算例 (如 tutorials/basic/laplacianFoam/flange)+ -dry-run 跑通"设置正确性",再上真实网格;用 -debug 配合 Pout/DebugInfo 定位。
18.2 "最小知识集"回顾
bash
骨架: main() { args → runTime → mesh → 控制对象 → createFields
→ while(loop){ 装方程 → constrain/solve/correct } → write }
场: IOobject(name, timeName, mesh, MUST_READ/READ_IF_PRESENT, AUTO_WRITE) + 默认值 + headerOk 回退
方程: fvm::ddt/div/laplacian + fvc::grad/div/flux → fvMatrix → solve
配置: system/{controlDict,fvSchemes,fvSolution} + constant/ + 0/
构建: Make/files(EXE=/LIB=) + Make/options(EXE_INC/EXE_LIBS/LIB_LIBS) + wmake + lnInclude
扩展: RTS 三宏注册(makePatchTypeField/makePatchFields) + $FOAM_USER_LIBBIN + libs(...)
时间: CourantNo.H → setDeltaT.H → Time::adjustDeltaT;LTS: createRDeltaT.H/createRDeltaT + setRDeltaT.H
18.3 本章要点
- 最小完整求解器 =
laplacianFoam:10 项必备要素(第 1.1 节表)一个不少。 - 文件构成 :
.C+createFields.H+write.H+Make/files+Make/options;.H片段是"代码片段"而非头文件。 - 头文件三行 :
fvCFD.H(全家桶)+fvOptions.H+simpleControl.H。 - 骨架 :
argList→Time→fvMesh→ 控制对象 →createFields.H→while(loop)→#include "write.H"→printExecutionTime。 - 循环 :
simple.loop()收敛判定 +runTime.loop();correctNonOrthogonal()执行1+nNonOrthCorr次。 - 机制链 A :
fvm::laplacian→laplacianScheme::New→gaussLaplacianScheme::fvmLaplacian(正交进矩阵、非正交进 source)→solve→ 写场。 - 机制链 B :
Make/files用=区分宏与源文件;LIB_LIBSvsEXE_LIBS;链接顺序"被依赖库靠后";lnInclude由wmakeLnInclude生成。 - 机制链 C :边界类骨架 = 继承 +
TypeName+ 构造族 +clone+updateCoeffs/evaluate+ 四个*Coeffs+write+makePatchTypeField注册。 - 机制链 D :
Co = 0.5·gMax(Σ|φ|/V)·Δt→setDeltaT.H(减立即、增阻尼)→Time::adjustDeltaT()(写盘对齐);LTS 用rDeltaT = max(1/maxDeltaT, Σ|φ|/(2·maxCo·V))。 - 排错 :
undefined reference→库/顺序;找不到头→lnInclude;未知边界→未注册/未加载;场读不到→三处一致。
OpenFOAM 的"可编程性"最终就落在这一章 :你既能在库层面加模型/边界(RTS),也能在应用层面写求解器(main + 方程),还能在配置层面用 fvSchemes/fvSolution/fvOptions/coded 微调------三条路随时切换。而这三条路的共同底座,就是本章解剖的这套"最小完整"骨架。