OpenFOAM 源码深度剖析 · 第二季-15 实战:手写一个求解器、库与边界条件

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 节:

  1. 为什么从 laplacianFoam 讲起:最小完整求解器的边界。
  2. 文件构成:一个求解器到底由几个文件组成(真实清单)。
  3. 头文件三行解剖:fvCFD.H/fvOptions.H/simpleControl.H。
  4. main() 骨架逐行:argList → Time → fvMesh → 控制对象。
  5. 循环机制:simple.loop() 与 correctNonOrthogonal() 的状态机。
  6. 方程组装:fvm::ddt − fvm::laplacian == fvOptions。
  7. solve() 与 fvSolution:从 fvMatrix 到线性求解器。
  8. createFields.H 逐行:IOobject 五参数、默认值与字典回退。
  9. write.H 逐行:writeTime() 与"梯度过热"的写出片段。
  10. 机制链 A :求解流程全链(fvSchemes 选格式 → fvm::laplacian 出 fvMatrix → solve → 写场)。
  11. 机制链 B :Make/files 的 EXE/LIB 语法与 wmake 链接顺序、lnInclude。
  12. 机制链 C :自定义 fvPatchField 的类骨架(构造/updateCoeffs/evaluate/clone/write)。
  13. 机制链 D :CourantNo.H 的 Co 计算与 setDeltaT.H/setRDeltaT.H 自适应时间步。
  14. 可直接复制的完整文件模板(Make/files、Make/options、createFields.H、主文件、边界条件 .H/.C、controlDict 段)。
  15. 对比与选型表(≥5 张)。
  16. 常见报错/警告速查(≥20 条:成因/排查/解决)。
  17. 从 laplacianFoam 改造:scalarTransportFoam 对照。
  18. 调试、验证与小结。

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 的瞬态热传导方程(无对流、无源项,只有时间项与扩散项)。为什么拿它当"教学标本"?因为它满足三个条件:

  1. 它是最小的"完整求解器" :具备 Time/fvMesh/场/方程/循环/写出的全部骨架要素,一个都不缺。
  2. 它没有任何"专业包袱" :不像 icoFoam 要处理压力-速度耦合、不像 rhoCentralFoam 要处理可压缩通量,laplacianFoam 的物理就是一步扩散。
  3. 它被仓库自己当作"参考骨架" :搜索仓库里 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 的五个参数 :
    1. "T":对象名(决定读写文件名 T)。
    2. runTime.timeName():时间目录(如 "0"、"0.1")。
    3. mesh:注册表(场挂到网格对象注册表)。
    4. IOobject::MUST_READ:读取策略------必须存在 ,读不到即报 FatalIOError。
    5. 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)

逐项链接顺序:

  1. $(LINKEXE):链接器命令(如 g++ -o/ld)。
  2. $(FOAM_EXTRA_LDFLAGS):用户额外链接标志(环境变量 FOAM_EXTRA_LDFLAGS,S2-16)。
  3. $(OBJECTS):本目标编译出的所有 .o------必须排在最前,否则符号解析不到。
  4. -L$(LIB_PLATFORMS):库搜索路径(platforms/$(WM_OPTIONS)/lib)。
  5. $(EXE_LIBS):来自 Make/options 的 -l 列表。
  6. $(PROJECT_LIBS):-l$(WM_PROJECT) -ldl(即 -lOpenFOAM -ldl,第 92 行定义)。
  7. $(SYS_LIBS):系统库(OpenMP 等,第 69--79 行)。
  8. $(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Δ))。
  • 第 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 ":

    cpp 复制代码
    bool 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"(如未 include fvPatchField.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 行)内容:

    cpp 复制代码
    surfaceScalarField 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_LIBS vs EXE_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 微调------三条路随时切换。而这三条路的共同底座,就是本章解剖的这套"最小完整"骨架。

相关推荐
鶴哥只手遮天1 小时前
Gmsh 源码深度解析-第 8 章 数值内核与高阶有限元
算法·开源
鶴哥只手遮天1 小时前
Gmsh 源码深度解析-第 4 章 网格生成总控与尺寸场
算法·开源
鶴哥只手遮天1 小时前
Gmsh 源码深度解析-第 10 章 求解器框架、API 与语言绑定
算法·开源
鶴哥只手遮天1 小时前
OpenFOAM 源码深度剖析 · 第二季-02 燃烧与化学反应:`combustionModel`、化学求解器与 Arrhenius 反应率
算法·开源
鶴哥只手遮天1 小时前
Gmsh 源码深度解析-第 6 章 三维体网格算法
算法·开源
鶴哥只手遮天1 小时前
Gmsh 源码深度解析-第 3 章 脚本语言与解析器
算法·开源
鶴哥只手遮天1 小时前
OpenFOAM 源码深度剖析 · 第二季-16 工程与工具链:Windows 交叉编译、性能剖析、数据交换与测试
算法·开源
鶴哥只手遮天1 小时前
Gmsh 源码深度解析-第 5 章 二维曲面网格算法
算法·开源
鶴哥只手遮天1 小时前
Gmsh 源码深度解析-第 1 章 架构总览与数据模型
算法·开源