OpenFOAM 源码深度剖析 · 第二季-14 `functionObjects`、求解器控制框架 与 表达式系统

OpenFOAM 源码深度剖析 · 第二季-14 functionObjects、求解器控制框架 与 表达式系统

本季最后一章"框架与基础设施",讲三块让 OpenFOAM "用起来顺手"的机制:functionObjects (可插拔后处理/运行期工具)、求解器控制框架 (PISO/SIMPLE/PIMPLE 的控制对象)、表达式系统 (在字典里写数学表达式)。它们共同构成 OpenFOAM 的"用户自动化"能力。本章结论均来自 v2606(master)源码逐行阅读,路径与行号以仓库 d:\CODE\openfoam-master 为准。

全章地图

  1. functionObject 基类:接口与生命周期。
  2. functionObjectList:调度中枢(read/execute/end 的真实实现)。
  3. timeControl:execute/write 的"闸门"与时间换算。
  4. functionObjects::timeControl 包装器。
  5. regionFunctionObject / stateFunctionObject / functionObjectProperties。
  6. 常用 functionObjects 逐个剖析。
  7. solutionControl 家族与 SIMPLE/PIMPLE 状态机。
  8. 表达式系统:从字符串到 exprValue/exprResult。
  9. #eval/#calc/Function1Expression:表达式的三个落点。
  10. 可直接粘贴的完整字典模板(含调参表)。
  11. 常见报错/警告速查(≥20 条)。
  12. 对比与选型表(≥5 张)。
  13. 三块协同与第二季收官。

1. functionObject 基类:接口与生命周期

1.1 定位与设计哲学

functionObject.H(src/OpenFOAM/db/functionObjects/functionObject/,第 28--47 行)用一段说明定义它:

vbnet 复制代码
Function objects produce additional user-requested data both during
runtime and postprocessing calculations, ... additional logging to the
screen, or generating text, image and field files.
Function objects eliminate the need to store all runtime generated data ...
In addition, the output of most function objects, e.g. output fields, are
stored on the mesh database so that it can be retrieved and used for other
applications (e.g. directly using \c wallShearStress function object output
in \c fieldAverage function object to produce \c wallShearStressMean field).

三个可被后文反复印证的要点:(1)面向工作流 ------"runtime and postprocessing",同一对象既能随求解器跑也能在 postProcess 阶段跑;(2)省资源 ------不必把所有运行期数据都写盘(这正是 timeControl 存在的理由);(3)可被复用 ------输出场存进网格/输出注册表,后面的 functionObject 能直接用(第 5 节的 storedObjects()/stateFunctionObject 是这条链的实现)。

1.2 两种执行方式

同一文件(第 49--54 行):

vbnet 复制代码
Function objects can be executed by using two methods:
  - \c functions sub-dictionary in the \c system/controlDict file
  - \c postProcess command-line utility
  • 运行期 :在 system/controlDict 的 functions {} 里声明,随求解器运行自动执行;
  • 后处理 :用 postProcess 工具对已有结果执行。第 171--198 行还给出 postProcess -func CourantNo(按名字调单个对象,不必写字典)与 simpleFoam -postProcess(求解器自带选项)。

1.3 controlDict 里的条目模板

functionObject.H(第 60--101 行):

ini 复制代码
    functions
    {
        <dictName1>
        {
            // Mandatory entries
            type    <functionObjectTypeName>;
            libs    (<libType>FunctionObjects);
            ...
            // Optional (inherited) entries
            region              region0;
            enabled             true;
            log                 true;
            timeStart           0;
            timeEnd             1000;
            executeControl      timeStep;
            executeInterval     1;
            writeControl        timeStep;
            writeInterval       1;
        }
        ...
    }

两点:type (第 106 行,必填)经 RTS 选具体对象(第一季第 4 章),libs (第 107 行)声明所在库;"何时执行/何时写"由 executeControl/executeInterval/writeControl/writeInterval(即第 3 节 timeControl)控制。<dictName> 的用途在第 153--155 行点明:"typically used as the name of the output directory"------默认输出目录 postProcessing/<dictName>/<time>/。

1.4 时间控制选项与 errors 策略(文档级协议)

第 128--151 行给出两张协议表。时间控制(第 128--139 行):

sql 复制代码
        none              | Trigger is disabled
        timeStep          | Trigger every 'Interval' time-steps
        writeTime         | Trigger every 'Interval' output times
        runTime           | Trigger every 'Interval' run time period
        adjustableRunTime | Currently identical to "runTime"
        clockTime         | Trigger every 'Interval' clock time period
        cpuTime           | Trigger every 'Interval' CPU time period
        onEnd             | Trigger on end of simulation run

errors 策略(第 141--151 行):控制 FatalError 在构造/execute/write 时如何被捕获(FatalIOError 不受影响):

perl 复制代码
        default | warn      | fatal
        warn    | warn      | warn
        ignore  | silent    | silent
        strict  | fatal     | fatal

errors 很实用 :批量运行时某个 functionObject 构造或执行失败可以只 warn 而不终止算例------第 2.2 节会看到它在 read()/execute() 里如何用 try/catch + FatalError.throwing(true) 落地。

1.5 类声明:数据成员与接口

functionObject.H(第 236--320 行)的类声明是"最小契约":

cpp 复制代码
class functionObject
{
        const word name_;             // 公开的 { 输出目录名 = functions 键名 }
        bool useNamePrefix_;         // 是否给本对象产生的场名加前缀
protected:
        word scopedName(const word& name) const;
public:
        bool log;                                 // 是否写日志进 Info
        static int debug;
        static bool postProcess;                  // 全局后处理模式开关
        static bool defaultUseNamePrefix;
        static word outputPrefix;                 // = "postProcessing"
        declareRunTimeSelectionTable
        (autoPtr, functionObject, dictionary,
         (const word& name, const Time& runTime, const dictionary& dict),
         (name, runTime, dict));

name_ 同时决定输出目录名;useNamePrefix_ 决定是否把产生的场名加前缀 <FOname>:(scopedName() 见 functionObject.C 第 51--59 行)。functionObject.C(第 34--46 行)定义四个静态量:

cpp 复制代码
    defineRunTimeSelectionTable(functionObject, dictionary);
bool Foam::functionObject::postProcess(false);           // 默认运行期
bool Foam::functionObject::defaultUseNamePrefix(false);
Foam::word Foam::functionObject::outputPrefix("postProcessing");

1.6 生命周期方法

两条强制 纯虚(functionObject.H 第 348、359 行):execute() 与 write()。其余为带默认实现的可选钩子:read()(343)、execute(const label subIndex)(354)、end()(363)、adjustTimeStep()(366)、filesModified()(369)、updateMesh()(373)、movePoints()(377)。functionObject.C 第 186--215 行确认默认实现几乎为空(execute(label)→true、end()→true、adjustTimeStep()→false、filesModified()→false、updateMesh/movePoints 空操作)。

"一个 functionObject 最少只需实现 execute() 和 write()" 由此而来。注意第 348、359 行的注释:postProcess 会强制忽略 executeControl/writeControl。

read()(functionObject.C 第 163--183 行):用 keyType::LITERAL 读 useNamePrefix(回退全局 defaultUseNamePrefix);log 只在非后处理模式下才从字典读 ,因为后处理时构造已把 log(postProcess) 设为 true(第 72 行)。

1.7 选择器 New():RTS 装配与错误处理

functionObject.C(第 78--138 行)是"字典 type → 对象"的完整答案:

cpp 复制代码
    const word functionType(dict.get<word>("type"));        // type 必填
    {
        const auto finder =
            dict.csearchCompat("libs", {{"functionObjectLibs", 1612}});
        if (finder.good())
            runTime.libs().open(dict, finder.ref().keyword(),
                                dictionaryConstructorTablePtr_);
    }
    if (!dictionaryConstructorTablePtr_)                    // 库未加载
        FatalErrorInFunction << "Cannot load function type "
            << functionType << " ... Table of functionObjects is empty"
            << exit(FatalError);
    auto* ctorPtr = dictionaryConstructorTable(functionType);
    if (!ctorPtr)                                           // 类型未注册
        FatalErrorInLookup("function", functionType, *dictionaryConstructorTablePtr_)
            << exit(FatalError);
    return autoPtr<functionObject>(ctorPtr(name, runTime, dict));
  • 第 85 行 dict.get<word>("type"),缺失即失败;
  • 第 93--105 行 csearchCompat("libs", {{"functionObjectLibs", 1612}}):先找 libs,回退旧名 functionObjectLibs(版本 1612 兼容名),找到就打开动态库并注册构造表;
  • 第 115--121 行表为空 → "Cannot load function type ... Table of functionObjects is empty"(libs 没写/库名错);
  • 第 123--135 行查不到构造器 → FatalErrorInLookup。注释明确:"FatalError (not FatalIOError) to ensure it can be caught as an exception and ignored "------为 errors 策略能捕获而刻意选的错误类型。

unavailableFunctionObject(第 386--409 行 / .C 第 220--259 行)是"本平台未提供该类型"的占位,carp() 打印 #### <type> not available #### 并抛可捕获的 FatalError。


2. functionObjectList:调度中枢

2.1 类结构与数据成员

functionObjectList.H(第 110--147 行)声明一个继承 PtrList<functionObject> 的调度器:

cpp 复制代码
class functionObjectList : private PtrList<functionObject>
{
    List<error::handlerTypes> errorHandling_;   // 每对象一个 errors 策略,与列表同序
    List<SHA1Digest> digests_;                  // 每对象字典摘要
    HashTable<label> indices_;                  // name → 列表下标
    HashTable<unsigned> warnings_;              // 告警计数(maxWarnings=10)
    const Time& time_;
    const dictionary& parentDict_;              // 含 "functions" 子字典
    mutable autoPtr<functionObjects::properties> propsDictPtr_;  // 状态字典
    mutable autoPtr<objectRegistry> objectsRegistryPtr_;         // 输出注册表
    bool execution_;
    bool updated_;

三个并行容器是理解 read() 的核心:errorHandling_(策略)、digests_(摘要,只有字典变了才重读/重建 )、indices_(配合 remove() 实现"先摘旧的、再放新的")。两个 mutable autoPtr:propsDictPtr_ 是名为 functionObjectProperties 的状态字典;objectsRegistryPtr_ 是名为 functionObjectObjects 的输出注册表(供 FO 复用)。

2.2 read():热重载与 timeControl 包装(机制链 1)

functionObjectList.C(第 939--1209 行)是最长函数。逐段拆解:

(a)惰性建状态字典并置 updated_(第 941--952 行)

cpp 复制代码
    if (!propsDictPtr_) createPropertiesDict();
    updated_ = execution_;
    if (!execution_) return true;      // 关闭时不读

updated_ = execution_:只要在运行就认为"本步已读过",这也解释了 execute() 里 if (!updated_) read(); 的语义(off() 会把二者一起置 false)。

(b)取 functions 字典并处理缺失/类型错(第 954--977 行)

cpp 复制代码
    const entry* entryPtr = parentDict_.findEntry("functions", keyType::LITERAL);
    if (!entryPtr)            // 没有 functions:整体清空
        { PtrList<functionObject>::clear(); errorHandling_.clear(); ... }
    else if (!entryPtr->isDict())   // 有但不是字典:FatalIOError(errors 策略不能吞)
        { ok = false;
          FatalIOErrorInFunction(parentDict_)
            << "'functions' entry is not a dictionary" << exit(FatalIOError); }

(c)顶层 libs/errors 与逐条目循环(第 979--1060 行) :先按 functionsDict.size() 预留 newPtrs/newDigs(最后 resize(nFunc) 收紧),errorHandling_ 先全填 DEFAULT 再覆写,打开顶层 libs,取顶层 errors 作 fallback。循环里非字典条目:

cpp 复制代码
            if (!dEntry.isDict())
            {
                if (key == "useNamePrefix")  // 顶层:写进全局 defaultUseNamePrefix
                    { Switch sw(dEntry.stream().front());
                      if (sw.good()) functionObject::defaultUseNamePrefix = sw;
                      else IOWarningInFunction(parentDict_)
                          << "Entry '" << key << "' is not a valid switch"; }
                else if (key != "errors" && key != "libs")
                    IOWarningInFunction(parentDict_)
                        << "Entry '" << key << "' is not a dictionary";
                continue;
            }

顶层只允许 useNamePrefix/errors/libs 三个非字典关键字 ,其余一律 IOWarning 忽略------这就是"把 executeControl 误写到 functions{} 顶层"会得到 Entry '...' is not a dictionary 警告的原因。

(d)per-object 的 enabled/errors/digest(第 1045--1063 行)

cpp 复制代码
            bool enabled = dict.getOrDefault("enabled", true);
            const error::handlerTypes errorHandling =
                getOrDefaultErrorHandling("errors", dict, errorHandlingFallback);
            errorHandling_[nFunc] = errorHandling;
            newDigs[nFunc] = dict.digest();          // SHA1 摘要
            label oldIndex = -1;
            autoPtr<functionObject> objPtr = remove(key, oldIndex);  // 摘出同名旧对象

(e)是否需要 timeControl 包装(第 1065 行)------ 机制链关键

cpp 复制代码
            const bool needsTimeControl =
                functionObjects::timeControl::entriesPresent(dict);

entriesPresent()(timeControlFunctionObject.C 第 484--501 行)判定:

cpp 复制代码
    if (Foam::timeControl::entriesPresent(dict, "write")
     || Foam::timeControl::entriesPresent(dict, "output")   // 旧名兼容
     || Foam::timeControl::entriesPresent(dict, "execute")
     || dict.found("timeStart") || dict.found("timeEnd")
     || dict.found("triggerStart") || dict.found("triggerEnd"))
        return true;
    return false;

而 timeControl::entriesPresent(dict, prefix)(timeControl.C 第 87--96 行)只是 return dict.found(prefix + "Control");。

结论(本章最重要的机制之一) :只要该对象字典出现 writeControl/outputControl/executeControl/timeStart/timeEnd/triggerStart/triggerEnd 任一键,就被 functionObjects::timeControl 包装;否则就是"裸 functionObject" 。裸对象每次调度都直接跑 execute()+write();包装对象各自按 executeControl/writeControl 判定。

(f)旧对象复用 vs 重建(第 1068--1106 行)

cpp 复制代码
            if (objPtr)
            {
                if (enabled && newDigs[nFunc] != digests_[oldIndex])
                {
                    const bool wasTimeControl = isA<functionObjects::timeControl>(*objPtr);
                    if (needsTimeControl != wasTimeControl)
                        objPtr.reset(nullptr);          // 裸↔包装切换:丢弃重建
                    else
                        enabled = objPtr->read(dict);   // 摘要变了且形态不变:就地重读
                }
                if (!enabled) { objPtr.reset(nullptr); continue; }
            }

三条判据:摘要相同 →完全不读;摘要变但形态没变 →objPtr->read(dict);摘要变且裸↔包装切换→丢弃旧对象落到"新建"。

(g)新建 + errors 分派(第 1108--1185 行)

cpp 复制代码
            if (enabled && !objPtr)
            {
                const bool oldThrowingError = FatalError.throwing(true);      // 临时改抛异常
                const bool oldThrowingIOerr = FatalIOError.throwing(true);
                try
                {
                    if (needsTimeControl)
                        objPtr.reset(new functionObjects::timeControl(key, time_, dict));
                    else
                        objPtr = functionObject::New(key, time_, dict);
                }
                catch (const Foam::error& err)
                {
                    objPtr.reset(nullptr);
                    switch (errorHandling)
                    {
                        case error::handlerTypes::IGNORE: break;      // 静默吞
                        case error::handlerTypes::STRICT: exitNow(err); break;
                        case error::handlerTypes::DEFAULT:
                            if (isA<Foam::IOerror>(err)) { exitNow(err); break; }
                            [[fallthrough]];
                        case error::handlerTypes::WARN:              // 打印原始信息后继续
                            err.write(Warning, false);
                            Info<< "--> loading function object '" << key << "'";
                            break;
                    }
                }

这一段把文档 errors 表格变成可执行逻辑:DEFAULT 时 IO 错误致命、其它 fallthrough 到 WARN(打印后继续);exitNow()(第 60--81 行)按 FOAM_ABORT→std::abort()、并行→UPstream::exit(1)、串行→std::exit(1) 退出。addProfiling(fo, "functionObject::", key, "::new") 让构造耗时能被 profiling 定位。

(h)并行一致性校验与收尾(第 1179--1206 行)

cpp 复制代码
                if (!returnReduceAnd(bool(objPtr)))   // 要求所有 rank 都有效
                    { objPtr.reset(nullptr); ok = false; }
            }
            if (objPtr)
                { newPtrs.set(nFunc, std::move(objPtr));
                  newIndices.insert(key, nFunc); ++nFunc; }
        }
        newPtrs.resize(nFunc); newDigs.resize(nFunc); errorHandling_.resize(nFunc);
        PtrList<functionObject>::transfer(newPtrs);   // 整体换入,自动析构未复用对象
        digests_.transfer(newDigs); indices_.transfer(newIndices); warnings_.clear();

机制链 1 完成:字典 → digest 比较 → 复用/重建/包装切换 → errors 分派 → 并行校验 → 列表换入。

2.3 execute():调度与异常隔离(机制链 2)

functionObjectList.C(第 615--792 行)是运行期唯一调度入口 (它同时调用每个对象的 execute() 与 write()):

(a)入口守卫(第 619--625 行)

cpp 复制代码
    if (execution_)
    {
        if (!updated_) read();
        auto errIter = errorHandling_.cbegin();
        for (functionObject& funcObj : functions())
        {
            const auto errorHandling = *errIter; ++errIter;   // 与列表同序

(b)WARN/IGNORE 分支(第 635--745 行)

cpp 复制代码
            if (errorHandling == error::handlerTypes::WARN
             || errorHandling == error::handlerTypes::IGNORE)
            {
                const bool oldThrowingError = FatalError.throwing(true);
                const bool oldThrowingIOerr = FatalIOError.throwing(true);
                bool hadError = false;
                try { addProfiling(fo, "functionObject::", objName, "::execute");
                      ok = funcObj.execute() && ok; }
                catch (const Foam::error& err)
                {
                    unsigned nWarnings; hadError = true;
                    if ((errorHandling == error::handlerTypes::WARN)
                     && (nWarnings = ++warnings_(objName)) <= maxWarnings)
                    {
                        err.write(Warning, false);
                        Info<< "--> execute() function object '" << objName << "'";
                        if (nWarnings == maxWarnings)
                            Info<< "... silencing further warnings";
                    }
                }
                if (hadError) { FatalError.throwing(oldThrowingError);
                                FatalIOError.throwing(oldThrowingIOerr); continue; }
                try { addProfiling(fo, "functionObject::", objName, ":write");
                      ok = funcObj.write() && ok; }
                catch (const Foam::error& err) { /* 同上,最多 maxWarnings 次 */ }

要点:ok = funcObj.execute() && ok 的书写顺序 保证 execute 一定被调用;warnings_(objName) 每个对象最多打印 maxWarnings = 10 次;hadError 时 continue 跳过 write() (避免用半个状态写作);throwing 保存-恢复(第 733--734 行)不改动全局错误行为。

(c)非 WARN/IGNORE 分支(第 747--772 行) :DEFAULT/STRICT 不捕获异常,出错直接退出(与文档"运行期 fatal"一致)。

(d)属性字典写盘(第 776--789 行)

cpp 复制代码
    if (time_.writeTime() && writeProperties)          // 写时刻
    {
        const auto oldPrecision = IOstream::precision_;
        IOstream::precision_ = 16;
        propsDictPtr_->writeObject
            (IOstreamOption(IOstreamOption::ASCII, time_.writeCompression()), true);
        IOstream::precision_ = oldPrecision;
    }

把 functionObjectProperties(trigger/results/fieldAverage 累计)以 16 位精度写盘------这是"状态能跨重启续算"的落盘点。

(e)write 是怎么被触发的? functionObjectList 没有 write() 成员;write() 是通过每个对象的 funcObj.write() 在 execute() 里调用的。对裸对象每次调度都调;对包装对象其内部再由 writeControl_.execute() 把关(见第 4 节)。

2.4 其它成员

  • end()(第 838--905 行) :结构类似 execute(),但不抑制告警(注释:"we want to know about rare issues"):除 IGNORE 外都打印一次 --> end() function object '<name>'。
  • start()(第 609--612 行) :return read(); ------ start 就是一次 read (这也是 functionObjectList::New() 末尾调 start() 的原因)。
  • execute(const label subIndex)(第 795--810 行) 与 execute(names, subIndex)(第 813--835 行) :后者用 wordRes::match(functionNames, funcObj.name()) 只执行名字匹配的子集(用于 solver 内按需触发)。
  • triggerIndex()(第 502--505 行) :propsDict().getTrigger(),第 4.2 节 active() 的输入。
  • propsDict()/storedObjects()(第 516--558 行):惰性创建状态字典/输出注册表。
  • findObjectID()(第 572--587 行) 、on()/off()/status()(第 590--606 行,off() 同时置 updated_=execution_=false) 、clear()(第 561--569 行)。
  • createPropertiesDict()(第 88--108 行)建的 IOobject path 是 "uniform"/word("functionObjects"),READ_IF_PRESENT+NO_WRITE+REGISTER,对应磁盘 <time>/uniform/functionObjects/functionObjectProperties。

2.5 调度链的发起者:Time::run()

Time.C(第 783--862 行)的 run() 给出了完整调度链:

cpp 复制代码
bool Foam::Time::run() const
{
    bool isRunning = value() < (endTime_ - 0.5*deltaT_);
    if (!subCycling_)
    {
        if (!isRunning && timeIndex_ != startTimeIndex_)   // 退出步
        {
            functionObjects_.execute();   // 末步再 execute(写最新 functionObjectProperties)
            functionObjects_.end();
        }
    }
    if (isRunning)
    {
        if (!subCycling_)
        {
            const_cast<Time&>(*this).readModifiedObjects();
            if (timeIndex_ == startTimeIndex_)
                functionObjects_.start();      // 首步 = read()
            else
                functionObjects_.execute();
            if (functionObjects_.filesModified())  // timeActivatedFileUpdate 等
                const_cast<Time&>(*this).readModifiedObjects();
        }
        isRunning = value() < (endTime_ - 0.5*deltaT_);   // 重算(FO 可能提前结束)
    }
    return isRunning;
}

完整调度链:

css 复制代码
Time::loop() → Time::run()
 ├── 首步 (timeIndex_==startTimeIndex_) → functionObjects_.start()  // 即 read()
 ├── 其余步 → functionObjects_.execute()
 │     └── 遍历每个 functionObject
 │           ├── funcObj.execute()   // 裸对象直跑;包装对象按 executeControl
 │           └── funcObj.write()     // 裸对象直跑;包装对象按 writeControl
 │     └── 写时刻写 functionObjectProperties
 ├── functionObjects_.filesModified() → 若真则 readModifiedObjects()
 └── 退出步 → functionObjects_.execute() + functionObjects_.end()

关键细节:isRunning 在调用 functionObjects 之后被重算 (第 849 行),因为 runTimeControl 之类可能在 execute 中 writeAndEnd() 提前结束算例。


3. timeControl:execute/write 的闸门

3.1 枚举与名字表

timeControl.H(src/OpenFOAM/db/functionObjects/timeControl/,第 66--79 行):

cpp 复制代码
    enum timeControls
    {
        ocNone = 0,           //!< No execution
        ocAlways,             //!< Always execute
        ocTimeStep,           //!< Execution coupled to time-step (default)
        ocWriteTime,          //!< Execution coupled to write-time
        ocRunTime,            //!< Use run-time for execution
        ocAdjustableRunTime,  //!< Currently identical to "runTime"
        ocClockTime,          //!< Use clock time for execution
        ocCpuTime,            //!< Use CPU time for execution
        ocOnStart,            //!< Execute on first step of run
        ocOnEnd               //!< Execute on end of run
    };

timeControl.C(第 34--52 行)的 controlNames_ 把字符串映射到枚举(注意 outputTime 是 writeTime 别名、adjustable/adjustableRunTime 互为别名):

cpp 复制代码
    { timeControl::ocNone, "none" },        { timeControl::ocAlways, "always" },
    { timeControl::ocTimeStep, "timeStep" },{ timeControl::ocWriteTime, "writeTime" },
    { timeControl::ocWriteTime, "outputTime" },  // 别名
    { timeControl::ocRunTime, "runTime" },
    { timeControl::ocAdjustableRunTime, "adjustable" },
    { timeControl::ocAdjustableRunTime, "adjustableRunTime" },
    { timeControl::ocClockTime, "clockTime" },  { timeControl::ocCpuTime, "cpuTime" },
    { timeControl::ocOnStart, "onStart" },      { timeControl::ocOnEnd, "onEnd" },

3.2 数据成员

timeControl.H(第 91--107 行):const Time& time_、const word prefix_("execute"/"write")、timeControls timeControl_、label intInterval_(整数步,<=1 表示每步 )、scalar interval_(浮点时间)、label executionIndex_(上次执行的索引)。prefix_ 拼成 executeControl/writeControl 等键名。

3.3 read():Control/Interval 与时间换算(机制链 3)

timeControl.C(第 110--168 行):

cpp 复制代码
void Foam::timeControl::read(const dictionary& dict)
{
    timeControl_ = ocTimeStep;   intInterval_ = 0;   interval_ = 0;   // 默认 timeStep
    word controlName(prefix_ + "Control");
    word intervalName(prefix_ + "Interval");
    if (prefix_ == "write" && dict.found("outputControl"))       // 旧名兼容
    {
        controlName = "outputControl";  intervalName = "outputInterval";
        IOWarningInFunction(dict)
            << "Found deprecated 'outputControl'" << nl
            << "    Use 'writeControl' with 'writeInterval'";
        error::warnAboutAge("keyword", 1606);
    }
    timeControl_ = controlNames_.getOrDefault(controlName, dict, timeControl_);
    switch (timeControl_)
    {
        case ocTimeStep: case ocWriteTime:
            intInterval_ = dict.getOrDefault<label>(intervalName, 0);
            interval_ = intInterval_;                 // 镜像成 scalar(供输出)
            break;
        case ocClockTime: case ocRunTime: case ocCpuTime: case ocAdjustableRunTime:
        {
            const scalar userTime = dict.get<scalar>(intervalName);
            interval_ = time_.userTimeToTime(userTime);   // 用户时间→运行时间
            break;
        }
        default: break;
    }
}

四个关键点:(1)默认 timeStep (第 113 行),不写 executeControl 时每步触发,intInterval_ 用 getOrDefault(..., 0);(2)write 前缀旧名兼容 (outputControl→writeControl,带 deprecation 警告 + error::warnAboutAge("keyword", 1606));(3)整数控制 (timeStep/writeTime)只需整数步;(4)时间控制 (clockTime/runTime/cpuTime/adjustableRunTime)用 time_.userTimeToTime(userTime) 把用户时间换算成运行时间------writeInterval 0.5 这类浮点数的真实落点。

3.4 execute():逐分支判据

timeControl.C(第 171--285 行):

cpp 复制代码
bool Foam::timeControl::execute()
{
    switch (timeControl_)
    {
        case ocNone:    return false;
        case ocAlways:  return true;
        case ocTimeStep:
            return ((intInterval_ <= 1) || !(time_.timeIndex() % intInterval_));
        case ocWriteTime:
            if (time_.writeTime())
            { ++executionIndex_;
              return ((intInterval_ <= 1) || !(executionIndex_ % intInterval_)); }
            break;
        case ocRunTime: case ocAdjustableRunTime:
        {
            label executionIndex = label
                (((time_.value() - time_.startTime().value())
                  + 0.5*time_.deltaTValue()) / interval_);
            if (executionIndex > executionIndex_)
                { executionIndex_ = executionIndex; return true; }
            break;
        }
        case ocCpuTime:
        {
            label executionIndex = label
                (returnReduce(time_.elapsedCpuTime(), maxOp<double>()) / interval_);
            if (executionIndex > executionIndex_)
                { executionIndex_ = executionIndex; return true; }
            break;
        }
        case ocClockTime:                    // 与 CpuTime 同构,用 elapsedClockTime()
        case ocOnStart:  return time_.timeIndex() == 1;
        case ocOnEnd:
        {
            scalar endTime = time_.endTime().value() - 0.5*time_.deltaTValue();
            return time_.value() > endTime;
        }
        default:
            FatalErrorInFunction << "Undefined time control: "
                << controlNames_[timeControl_] << nl << abort(FatalError);
    }
    return false;
}

逐分支语义(3.4 表):

分支 触发条件 说明
none 恒 false 触发禁用
always 恒 true 每次调度都触发(无字典构造默认态)
timeStep intInterval_<=1 或 timeIndex % intInterval_==0 按时间步整数取模
writeTime 先 time_.writeTime(),再 executionIndex_ % intInterval_ 只在写时刻
runTime/adjustableRunTime floor((t-t0+0.5Δt)/interval_) 严格递增 最近整数索引比较,避免重复触发
cpuTime floor(reduceMax(elapsedCpuTime())/interval_) 递增 并行取最大 CPU 时间
clockTime floor(reduceMax(elapsedClockTime())/interval_) 递增 并行取最大墙钟时间
onStart timeIndex()==1 仅首步
onEnd t > endTime-0.5Δt 仅末步

两个工程要点: (1)runTime 类用单调索引 判定,deltaT 变化或跨多区间也只触发一次(不补触发);(2)并行 cpuTime/clockTime 用 returnReduce(..., maxOp<double>()) 让所有 rank 同时触发,避免通信错位。

3.5 intInterval_ <= 1 的语义陷阱

代码反复出现 (intInterval_ <= 1) || !(timeIndex % intInterval_):用户写 executeInterval 0 或 1 都等价于"每步" ,真正"不执行"要用 executeControl none。timeControlI.H(第 55--62 行)always() 显式表达:

cpp 复制代码
inline bool Foam::timeControl::always() const
{
    return ((timeControls::ocAlways == timeControl_)
         || (timeControls::ocTimeStep == timeControl_ && intInterval_ <= 1));
}

timeControl(runTime, prefix) 无字典构造的默认态是 ocAlways(timeControl.C 第 57--69 行),clear()(第 101--107 行)也复位到 ocAlways。


4. functionObjects::timeControl 包装器

4.1 为什么需要包装器

只要字典出现 writeControl 等键,read() 就用 new functionObjects::timeControl(key, time_, dict) 建对象。这个类(src/OpenFOAM/db/functionObjects/timeControl/timeControlFunctionObject.*)不是 第 3 节的 timeControl(闸门),而是一个 functionObject,内部持有另一个 functionObject + 两个闸门:

cpp 复制代码
Foam::functionObjects::timeControl::timeControl
(
    const word& name, const Time& runTime, const dictionary& dict
)
:
    timeFunctionObject(name, runTime),
    dict_(dict),
    controlMode_(controlMode::TIME),
    timeStart_(-VGREAT),  timeEnd_(VGREAT),
    triggerStart_(labelMin), triggerEnd_(labelMax),
    nStepsToStartTimeChange_(labelMax),
    executeControl_(runTime, dict, "execute"),     // 前缀 "execute"
    writeControl_(runTime, dict, "write"),         // 前缀 "write"
    foPtr_(functionObject::New(name, runTime, dict_)),   // 内层才是用户真正的对象
    executeTimeIndex_(-1), deltaT0_(0), seriesDTCoeff_(GREAT)
{ readControls(); }

(timeControlFunctionObject.C 第 456--479 行)关键:executeControl_ 读 executeControl/executeInterval,writeControl_ 读 writeControl/writeInterval,foPtr_ 用同一 dict_ 构造内层对象。

4.2 active():时间窗口与 trigger 区间

timeControlFunctionObject.C(第 89--138 行):

cpp 复制代码
bool Foam::functionObjects::timeControl::active() const
{
    label triggeri = time_.functionObjects().triggerIndex();
    bool inTime =
        time_.value() >= (timeStart_ - 0.5*time_.deltaTValue())
     && time_.value() <= (timeEnd_ + 0.5*time_.deltaTValue());
    bool inTrigger = triggeri >= triggerStart_ && triggeri <= triggerEnd_;
    switch (controlMode_)
    {
        case controlMode::TIME:            return inTime;
        case controlMode::TRIGGER:         return inTrigger;
        case controlMode::TIME_OR_TRIGGER: return inTime || inTrigger;
        case controlMode::TIME_AND_TRIGGER:return inTime && inTrigger;
        default: FatalErrorInFunction << "Unhandled enumeration: "
            << controlModeNames_[controlMode_] << abort(FatalError);
    }
    return false;
}

四种 controlMode(第 44--51 行:time/trigger/timeOrTrigger/timeAndTrigger)用时间窗 (timeStart/timeEnd,在 readControls() 第 56--86 行经 userTimeToTime 换算)与 trigger 区间 (triggerStart/triggerEnd,构造默认 labelMin/labelMax)逻辑组合。trigger 全局值来自 time_.functionObjects().triggerIndex(),即第 5.3 节的 functionObjectProperties 里的 triggerIndex(由 runTimeControl 的 setTrigger 或 solver 修改)。

4.3 execute()/write()/end()

timeControlFunctionObject.C(第 504--557 行):

cpp 复制代码
bool Foam::functionObjects::timeControl::execute()
{
    deltaT0_ = time_.deltaTValue();                 // 存旧 deltaT 供 ramp 参考
    if (active() && (postProcess || executeControl_.execute()))
        { executeTimeIndex_ = time_.timeIndex();  foPtr_->execute(); }
    return true;
}

bool Foam::functionObjects::timeControl::write()
{
    if (active() && (postProcess || writeControl_.execute()))
    {
        if (executeTimeIndex_ != time_.timeIndex())   // 本步 execute 没跑过 → 先补一次
            { executeTimeIndex_ = time_.timeIndex();  foPtr_->execute(); }
        foPtr_->write();
    }
    return true;
}

bool Foam::functionObjects::timeControl::end()
{
    if (active() && (executeControl_.execute() || writeControl_.execute()))
        foPtr_->end();
    return true;
}

write() 里的"补齐 execute"逻辑 (第 536--540 行)解决"writeControl 与 executeControl 频率不同步时结果过期"的问题。postProcess || 的短路(第 509、533 行)对应文档注释:后处理模式强制忽略 executeControl/writeControl 。end() 条件更宽(execute 或 write 闸门开即调 end())。

4.4 adjustTimeStep():writeInterval 对齐与 ramp

timeControlFunctionObject.C(第 560--792 行)服务"自适应时间步 + 写时刻对齐 + 步长渐变"。只有当 writeControl_.control() == ocAdjustableRunTime 时走复杂分支,否则直接设回内层 wantedDT 并用 deltaTCoeff_ 裁剪。复杂分支:

cpp 复制代码
    const scalar presentTime = time_.value();
    foPtr_->adjustTimeStep();
    const scalar wantedDT = time_.deltaTValue();
    const label writeTimeIndex = writeControl_.executionIndex();
    const scalar writeInterval = writeControl_.interval();
    scalar timeToNextWrite = (writeTimeIndex+1)*writeInterval
                           - (presentTime - time_.startTime().value());
    if (timeToNextWrite <= 0.0) timeToNextWrite = writeInterval;
    // 目标 dT 未必整除 writeInterval
    scalar intervalError = remainder(writeInterval, wantedDT)/writeInterval;
    if (mag(intervalError) > ROOTSMALL || deltaTCoeff_ == GREAT)
    {
        scalar deltaT = time_.deltaTValue();
        scalar nSteps = timeToNextWrite/deltaT;
        label nStepsToNextWrite = max(1, round(nSteps));
        scalar newDeltaT = timeToNextWrite/nStepsToNextWrite;   // 让写时刻落在步边界
        scalar clipThreshold = (deltaTCoeff_ != GREAT ? deltaTCoeff_ : 2);  // 步长裁剪 [1/2,2]
        if (newDeltaT >= deltaT) deltaT = min(newDeltaT, clipThreshold*deltaT);
        else { clipThreshold = 1/clipThreshold; deltaT = max(newDeltaT, clipThreshold*deltaT); }
        const_cast<Time&>(time_).setDeltaT(deltaT, false);
    }

要点:(1) 先让内层对象(setTimeStep/maxCo 逻辑)算出 wantedDT;(2) 算到下一个写时刻的剩余时间 timeToNextWrite;(3) 若 wantedDT 不整除 writeInterval(或未启用 ramp),把步长重算成 timeToNextWrite/nStepsToNextWrite 使写时刻精确落在步边界;(4) 步长变化被裁剪到 [1/2, 2](向后兼容)或 deltaTCoeff_。当整除且有 deltaTCoeff_ 时进入"渐变"分支(第 644--757 行),用 Newton 迭代求几何级数膨胀比,使步长在若干步内平滑过渡------核心是 calcExpansion()(第 141--189 行,牛顿迭代解 (y-1)*ratio^n + 1 - y*ratio^(n-1) = 0,收敛判据 mag(f) < 1e-6)与 calcDeltaTCoeff()(第 192--451 行)。这解释了自适应时间步为何有时在写时刻"突然变小"(为对齐 writeInterval),以及 deltaTCoeff 如何限制步长变化幅度。

4.5 read()

timeControlFunctionObject.C(第 795--810 行):仅当字典真的变了 (dict != dict_)才重读------先重读两个闸门、再 readControls()(timeStart/timeEnd/trigger/controlMode/deltaTCoeff),最后转发内层 foPtr_->read(dict)。返回 false 表示"没变化",配合 2.2 节的 enabled = objPtr->read(dict) 实现"无变化不重建"。


5. regionFunctionObject / stateFunctionObject / functionObjectProperties

5.1 类继承链

arduino 复制代码
functionObject                              // 最小契约
 └── functionObjects::timeFunctionObject          // + time_、输出注册表
      └── functionObjects::stateFunctionObject    // + 状态字典读写
           └── functionObjects::regionFunctionObject   // + 区域/子区域注册表 + 场存取
                └── functionObjects::fvMeshFunctionObject(src/finiteVolume)
                     └── 具体对象(fieldMinMax/fieldAverage/forces/...)

timeFunctionObject.C(第 62--73 行)的 storedObjects() 从 Time 拿输出注册表:return const_cast<Time&>(time_).functionObjects().storedObjects();;clearOutputObjects()(第 46--57 行)用 obr.checkOut(objName) 移除对象。

5.2 stateFunctionObject:状态字典读写代理

stateFunctionObject.H(第 70--265 行)给 functionObject 加"读写状态/结果"能力。stateFunctionObject.C(第 40--54 行)显示状态字典就是 functionObjectList 的 propsDict():

cpp 复制代码
const Foam::functionObjects::properties&
Foam::functionObjects::stateFunctionObject::stateDict() const
{ return time_.functionObjects().propsDict(); }

一批方法都是转发,例如(第 71--95 行):propertyDict() → stateDict().getObjectDict(name())(用对象名作子字典名);getTrigger() → stateDict().getTrigger();setTrigger(t) → stateDict().setTrigger(t)。

5.3 functionObjectProperties:trigger 与 results

functionObjectProperties.C:

cpp 复制代码
void Foam::functionObjects::properties::clearTrigger() { remove("triggerIndex"); }
Foam::label Foam::functionObjects::properties::getTrigger() const
{
    label idx = labelMin;  readIfPresent("triggerIndex", idx);  return idx;
}
bool Foam::functionObjects::properties::setTrigger(const label triggeri)
{
    if (triggeri != getTrigger()) { set("triggerIndex", triggeri); return true; }
    return false;
}

(第 82--111 行)三点:triggerIndex 就是该字典顶层的一个 entry ;getTrigger() 未设置时返回 labelMin (与 4.2 节 triggerStart_(labelMin) 匹配,使 inTrigger 默认恒真);setTrigger() 只在值变化时返回 true。results 部分(第 150--332 行)存在 resultsName_ = SHA1("results").str() 子字典下,结构 results/<objectName>/<dataFormat>/<entry>。graphFunctionObject(6.6 节)就靠读这段 results 画曲线。

5.4 regionFunctionObject:区域、子区域与场存取

构造(regionFunctionObject.C 第 137--154 行):

cpp 复制代码
    stateFunctionObject(name, runTime),
    subRegistryName_(dict.getOrDefault<word>("subRegion", word::null)),
    obr_(runTime.lookupObject<objectRegistry>(
            dict.getOrDefault("region", polyMesh::defaultRegion))),
    obrPtr_(nullptr)
  • region (默认 polyMesh::defaultRegion,即 "region0"):从 Time 查该名字的 objectRegistry(多区域算例的某个 mesh region)。
  • subRegion(默认空):可选"替代注册表"名,用于访问子区域。

obr()(第 46--70 行)惰性解析 subRegion:

cpp 复制代码
    if (!obrPtr_ && !subRegistryName_.empty())
    {
        obrPtr_ = obr_.cfindObject<objectRegistry>(subRegistryName_, true);   // 区域 registry
        if (!obrPtr_)
            obrPtr_ = storedObjects().cfindObject<objectRegistry>(subRegistryName_);
        if (!obrPtr_)
            WarningInFunction << "Could not locate subRegion \""
                << subRegistryName_ << '"';
    }
    return (obrPtr_ ? *obrPtr_ : obr_);

这是第 1.1 节"输出可被复用"的实现 :先在区域 registry 找 subRegion,找不到就到 storedObjects()(即 functionObjectObjects)里找------所以一个 FO 能引用另一个 FO 输出 的子注册表。writeObject()(第 73--91 行)找到 regIOobject 后 ptr->write();clearObject()/clearObjects()(第 94--132 行)用 checkOut() 从注册表签出。模板成员(store()/storeInDb()/lookupObject()/cfindObject() 等,声明在第 99--165 行)是各具体 FO 存取场的统一入口。

5.5 输出文件路径:writeFile 基类

许多 functionObject(fieldMinMax/forces/probes...)还有第二个父类 functionObjects::writeFile(writeFile.H 第 27--61 行:"Base class for writing single files from the function objects.")。它的可选条目(第 34--55 行):

arduino 复制代码
    writePrecision    <int>;    // 小数位数(默认取 system 设置)
    writeToFile       <bool>;   // 是否产生文本文件输出,默认 true
    useUserTime       <bool>;   // 用"用户时间"(如角度)代替物理时间,默认 true
    updateHeader      <bool>;   // 网格变化时更新表头,默认 true

保护数据(第 88--127 行)包括 fileObr_(区域注册表)、prefix_(前缀)、fileName_、filePtr_(autoPtr<OFstream>)、writePrecision_、writeToFile_、updateHeader_、writtenHeader_、useUserTime_、startTime_、ext_(默认 .dat)。关键成员函数(第 129--157 行):initStream()(初始化输出流)、baseFileDir()(输出根目录,即 postProcessing/<FOname>/)、baseTimeDir()(当前时间目录)、filePath(fName)(拼完整路径)、newFile(fName)/newFileAtTime(name,t)/newFileAtStartTime(name)(按需开新文件)。updateHeader_ 的语义 (第 57--61 行注释):文件表头通常在网格点/拓扑变化时更新,若 functionObject 实际不受影响,可用 updateHeader false 关闭------这正是"网格移动时同一 FO 的 dat 表头要不要重写"这个常见问题的开关。

writeFileTemplates.C 里另有一批供具体 FO 调用的模板:writeHeader()/writeCommented()/writeTabbed()/writeCurrentTime()/writeTime()/writeBreak() 等------第 6.1 节 fieldMinMax::writeFileHeader() 与 fieldMinMax.txx::output() 用的正是它们。


6. 常用 functionObjects 逐个剖析

6.1 fieldMinMax:极值 + 位置 + 并行归约(机制链)

fieldMinMax.H(src/functionObjects/field/fieldMinMax/)。构造(.cxx 第 112--127 行)继承两个父类 fvMeshFunctionObject(区域/状态)与 writeFile("写单文件",见 writeFile.H 第 34--55 行:writePrecision/writeToFile/useUserTime/updateHeader),成员 internal_(false)、location_(true)、mode_(mdMag)、fieldSet_(mesh_)。read()(第 132--145 行):internal_=getOrDefault("internal",false)、location_=getOrDefault("location",true)、mode_=modeTypeNames_.getOrDefault("mode",dict,mdMag)、fieldSet_.read(dict)。

modeTypeNames_(第 45--53 行)只有 magnitude/component------mode 决定矢量/张量按模还是按分量取极值(标量场始终按分量)。它用编译期模板分发 实现(fieldMinMax.H 第 134--145 行):

cpp 复制代码
        template<class T, modeType Mode = modeType::mdCmpt>
        struct modeValueType { using type = T; };                 // component: 保持 T
        template<class T>
        struct modeValueType<T, modeType::mdMag> { using type = Foam::scalar; }; // mag: 退化为 scalar
        template<class T, modeType Mode = modeType::mdCmpt>
        using modeValueType_t = typename modeValueType<T, Mode>::type;

execute() 是空操作 (.cxx 第 148--151 行 return true;),计算全在 write()(第 154--190 行):

cpp 复制代码
    for (const word& fieldName : fieldSet_.selectionNames())
    {
        bool ok = obr_.contains(fieldName) &&
        (
            calcMinMaxFields<scalar>(fieldName, modeType::mdCmpt)
         || calcMinMaxFields<vector>(fieldName, mode_)
         || calcMinMaxFields<sphericalTensor>(fieldName, mode_)
         || calcMinMaxFields<symmTensor>(fieldName, mode_)
         || calcMinMaxFields<tensor>(fieldName, mode_)
        );
        if (ok) ++count;
    }

用 short-circuit || 做运行时类型分派 :依次尝试 scalar→vector→sphericalTensor→symmTensor→tensor,calcMinMaxFields<Type> 类型不符时返回 false(.txx 第 364--405 行),避免了 typeid/dynamic_cast。

极值求法与并行归约(fieldMinMax.txx 第 157--361 行)------机制链:

cpp 复制代码
    struct Extrema { label cellID_; point position_; value_type value_; };
    struct Limits  { Extrema min_; Extrema max_; };
    List<Limits> allLimits(numProc);
    auto& myData = allLimits[myProci];                // 各 rank 先求本地极值(第 228--261 行)
    ...                                               // 非 internal_ 时再遍历边界场(第 264--304 行)
    UPstream::mpiAllGather(allLimits.data_bytes(), sizeof(Limits));   // 第 314 行一次汇总
    // 再本地串行比较(第 318--343 行)得全局 minProci/maxProci

findMinMax_mag()(第 35--71 行)逐单元取 Foam::mag() 比较,返回 labelPair(minIdx,maxIdx)。output()(第 76--154 行)同时做三件事------写文件、打日志、写回状态:

cpp 复制代码
    word nameStr('(' + outputName + ')');
    this->setResult("min" + nameStr, minValue);
    this->setResult("min" + nameStr + "_cell", minCell);
    this->setResult("min" + nameStr + "_position", minPosition);
    this->setResult("min" + nameStr + "_processor", minProci);
    this->setResult("max" + nameStr, maxValue);            // ... 及 max 的 cell/position/processor

这就是 graphFunctionObject 能画 fieldMinMax 极值曲线的数据来源 (setResult 写进 5.3 节 results)。writeFileHeader()(第 58--107 行)按 location_ 与是否并行决定列(field/min/location(min)/processor/max/location(max))。

6.2 forces:气动力/力矩积分

forces.H:按 patch 列表积分压力与粘性力/力矩(第 33--37 行),输出 force.dat/moment.dat(第 56--66 行)。关键成员(第 197--265 行):

cpp 复制代码
    vector sumPatchForcesP_, sumPatchForcesV_;       // 压力/粘性力
    vector sumPatchMomentsP_, sumPatchMomentsV_;     // 压力/粘性矩
    vector sumInternalForces_, sumInternalMoments_;  // 内部(多孔)贡献
    autoPtr<OFstream> forceFilePtr_, momentFilePtr_;
    autoPtr<coordinateSystem> coordSysPtr_;          // 坐标系统
    labelList patchIDs_;
    scalar rhoRef_, pRef_;                           // 参考密度/压力
    word pName_, UName_, rhoName_, fDName_;
    bool directForceDensity_, porosity_, writeFields_;

把 P/V 分量拆开存 ,故既能输出合力矩、又能分压力/粘性贡献。压力量按 rho 或 rhoInf(不可压用 rhoInf,文档第 158 行),粘性用有效应力 devRhoReff(第 294--298 行)与 mu()(第 300--301 行)。坐标系统可来自 CofR、origin/e1/e3 或完整 coordinateSystem 字典(文档第 90--112 行,setCoordinateSystem() 第 271--276 行);writeControl/writeInterval 控制输出频率(文档第 161--162 行)。

6.3 probes / sets / surfaces:采样三种形态

probes (src/sampling/probes/probes.H,第 27--90 行):在指定内部网格位置采样场并写文件,条目 fields + probeLocations,继承 Probes<internalFieldProbe>(第 108--109 行)。探针模型可换(probeModel/patchFieldProbe/internalFieldProbe,src/sampling/probes/probeModels/)。注意 includeOutOfBounds(文档第 72、84 行)------越界点默认包含,给 -VGREAT。

**sets(sampledSets,sampledSets.H 第 27--104 行)**沿"点集"采样(cloud/uniform/face/patchSeed/polyLine/circle/midPoint...,src/sampling/sampledSet/):

ini 复制代码
    surfaces
    {
        type    sets;    libs    (sampling);
        writeControl    writeTime;    writeInterval   1;
        fields          (p U);
        interpolationScheme cellPoint;
        setFormat   vtk;
        sets { Uref { type cloud; axis xyz; points ((-0.0508 0.0508 0.01)); } }
    }

**surfaces(sampledSurfaces,sampledSurfaces.H 第 27--110 行)**在"面"上采样,支持 cuttingPlane/isoSurface/distanceSurface/patch/faceZone/meshedSurface 等(src/sampling/sampledSurface/):

bash 复制代码
        type    surfaces;    libs    (sampling);
        fields (p U);  sampleScheme cell;  interpolationScheme cell;  surfaceFormat vtk;
        surfaces { f0surf { type meshedSurface; surface f0surf.obj;
                            source cells; interpolate true; } }

两者都用 writeControl writeTime 控制采样/写盘,都声明 libs (sampling)------采样类 FO 重写 write() 而非 execute()(头文件原话 "The write() method is used to sample and write files")。

6.4 fieldAverage:时均/脉动均与状态持久化(机制链)

fieldAverage.H(第 33--84 行)定义 mean(算术平均)与 prime2Mean(脉动平方均),base 是"按时间还是按迭代",window 是可选平均窗;场名按"基场 + 平均类型"拼接:U → UMean/UPrime2Mean(第 53--57 行)。

read()(fieldAverage.C 第 294--388 行)读 restartOnRestart/restartOnOutput/periodicRestart/restartPeriod/restartTime,对 fields 每项建 fieldAverageItem;重复基场名被 WarningInFunction 忽略(第 326--331 行)。execute() → calcAverages()(第 119--189 行):

cpp 复制代码
    const label currentTimeIndex = obr().time().timeIndex();
    if (prevTimeIndex_ == currentTimeIndex) return;   // 同一步只累加一次
    else prevTimeIndex_ = currentTimeIndex;
    ...   // periodicRestart_/restartTime_ 决定是否 restart()
    forAll(faItems_, fieldi) faItems_[fieldi].evolve(obr());
    ...   // addMeanSqrToPrime2Mean / calculateMeanFields / calculatePrime2MeanFields

prevTimeIndex_ 保证同一时间步只累加一次 (避免同一步内多次 execute 重复计入)。write()(第 399--410 行)先 writeAverages() 再 writeAveragingProperties()(restartOnOutput_ 时再 restart())。平均的累计步数与总时长落进 functionObjectProperties ,重启时由 readAveragingProperties()(第 217--265 行)用 foundProperty/getDict/item.readState 读回------实现"续算时平均不中断"。

6.5 writeObjects:按需写注册表对象

writeObjects.H(第 27--89 行):为注册到数据库的对象指定不同写频率。只继承 functionObject(第 114--117 行),带 writeOption 枚举(第 122--130 行):NO_WRITE/AUTO_WRITE/ANY_WRITE/LOG。含义(文档第 76--82 行):autoWrite 只写"设了输出时刻写"的对象;noWrite 只写"默认不写"的对象;anyWrite 二者皆可;log 只报告不写。execute() 是空操作,只有 write() 干活------说明"纯输出型"对象可只实现 write()。

6.6 graphFunctionObject / runTimeControl:利用 state 的两个高级用法

graphFunctionObject (2024 新增,graphFunctionObject.H 第 26--130 行):累积 function object result 值并渲染成 SVG 图。它读取 5.3 节的 results:

ini 复制代码
    residualGraph
    {
        type            graphFunctionObject;
        libs            (utilityFunctionObjects);
        functions
        {
            line1 { object solverInfo1; entry Ux_initial; }
            line2 { object solverInfo1; entry p_initial; }
        }
    }

每 line 指定"对象名 + 结果条目名"(object/entry),另可给 colour/dashes/title(第 121--133 行)。它印证:solverInfo 写进 results 的量可被另一 FO 读取绘图。

runTimeControl (runTimeControl.H 第 27--88 行)"当用户条件满足时终止计算",带 conditions 字典与 satisfiedAction(第 121--126 行):ABORT(写并抛 FatalError)、END(写并干净退出)、SET_TRIGGER(触发另一条件)。SET_TRIGGER 会写 triggerIndex------正是 4.2 节 active() 里 inTrigger 的输入 ,形成"条件满足 → 触发另一组 FO"的链式控制;abort/end 调 Time::writeAndEnd(),这正是 Time::run() 在调用 FO 之后重算 isRunning(2.5 节)的原因。

6.7 exprField(fvExpressionField):表达式生成的场

fvExpressionField.H 的 TypeName 是 "exprField" (第 213 行),三种动作(第 133--142 行):opNone/opNew(默认,新建/覆盖)/opModify(修改既有场)。用法(文档第 37--69 行):

go 复制代码
    exprFieldFO
    {
        type exprField;    libs (fieldFunctionObjects);
        field pTotal;      action new;
        expression "p + 0.5*(rho*magSqr(U))";
    }

opModify 可用 fieldMask 表达式(第 67 行 "(mag(pos()) < 0.05) && (pos().y() > 0)")只掩膜局部,且头文件注释 "useNamePrefix entry is always ignored for the modify action"(第 98--99 行)。内部持有 expressions::volumeExprDriver driver_(第 182 行),把第 8 节表达式系统接进 functionObject 框架。


7. solutionControl 家族与 SIMPLE/PIMPLE 状态机

7.1 基类 solutionControl

solutionControl.H(src/finiteVolume/cfdTools/general/solutionControl/solutionControl/,第 46--120 行):

cpp 复制代码
class solutionControl : public regIOobject
{
public:
    struct fieldData { wordRe name; scalar absTol, relTol, initialResidual; };
protected:
    fvMesh& mesh_;
    List<fieldData> residualControl_;   // 每场的残差控制
    const word algorithmName_;          // "SIMPLE" / "PIMPLE"
    label nNonOrthCorr_;                // 非正交修正次数
    bool momentumPredictor_, transonic_, consistent_, frozenFlow_;
    label corr_, corrNonOrtho_;         // 当前外迭代/非正交迭代

要点:继承 regIOobject (第 51--52 行,注册在网格上,IOobject::NO_READ/NO_WRITE 见 .C 第 274--284 行);fieldData 含 wordRe name(支持正则 )、absTol/relTol/initialResidual;algorithmName_ 决定读 fvSolution 哪个子字典(dict(),.C 第 300--303 行:mesh_.solutionDict().subOrEmptyDict(algorithmName_))。

read(const bool absTolOnly)(.C 第 41--133 行):

cpp 复制代码
    const dictionary solutionDict(this->dict());
    nNonOrthCorr_ = solutionDict.getOrDefault<label>("nNonOrthogonalCorrectors", 0);
    momentumPredictor_ = solutionDict.getOrDefault("momentumPredictor", true);
    transonic_  = solutionDict.getOrDefault("transonic", false);
    consistent_ = solutionDict.getOrDefault("consistent", false);
    frozenFlow_ = solutionDict.getOrDefault("frozenFlow", false);
    const dictionary residualDict(solutionDict.subOrEmptyDict("residualControl"));
    DynamicList<fieldData> data(residualControl_);
    for (const entry& dEntry : residualDict)
    {
        const word& fName = dEntry.keyword();
        const label fieldi = applyToField(fName, false);   // 已存在则复用旧槽
        if (fieldi == -1)
        {
            fieldData fd;  fd.name = fName.c_str();
            if (absTolOnly)                    // SIMPLE:标量绝对容差
                { fd.absTol = residualDict.get<scalar>(fName);
                  fd.relTol = -1;  fd.initialResidual = -1; }
            else if (dEntry.isDict())          // PIMPLE:字典 tolerance+relTol
                { const dictionary& fieldDict = dEntry.dict();
                  fd.absTol = fieldDict.get<scalar>("tolerance");
                  fd.relTol = fieldDict.get<scalar>("relTol");
                  fd.initialResidual = 0.0; }
            else
                FatalErrorInFunction << "Residual data for " << dEntry.keyword()
                    << " must be specified as a dictionary" << exit(FatalError);
            data.append(fd);
        }
        else { /* 更新已有槽的容差 */ }
    }
    residualControl_.transfer(data);

逐点:(1) momentumPredictor 默认 true、其余三个开关默认 false;(2) absTolOnly==true(SIMPLE)每场直接给标量 absTol;(3) absTolOnly==false(PIMPLE)每场必须是字典,读 tolerance+relTol;(4) 非字典且非 absTolOnly → FatalErrorInFunction(见第 11 节);(5) 已存在的场复用旧槽只更新容差。

applyToField()(第 142--157 行) 用 residualControl_[i].name.match(fieldName, !useRegEx) 做正则/字面匹配,返回下标或 -1。

maxResidual()(第 216--267 行) 从 solver 性能字典取残差,短路分派 5 种类型:

cpp 复制代码
template<class Type>
bool Foam::solutionControl::maxTypeResidual
(
    const fvMesh& fvmesh, const entry& solverPerfDictEntry, Pair<scalar>& residuals
)
{
    typedef GeometricField<Type, fvPatchField, volMesh> fieldType;
    if (fvmesh.foundObject<fieldType>(solverPerfDictEntry.keyword()))
    {
        const Pair<SolverPerformance<Type>> sp(solverPerfDictEntry.stream());
        residuals.first() = cmptMax(sp.first().initialResidual());
        residuals.last()  = cmptMax(sp.last().initialResidual());
        return true;
    }
    return false;
}

Pair<scalar> 的 first/last 分别是初始残差与最终残差 ;矢量/张量场用 cmptMax 取分量最大。

7.2 simpleControl:稳态收敛判据

simpleControl.C(第 42--46 行)read() 用 absTolOnly=true。criteriaSatisfied()(第 49--88 行):

cpp 复制代码
bool Foam::simpleControl::criteriaSatisfied()
{
    if (residualControl_.empty()) return false;              // 无判据 → 永不收敛
    bool achieved = true, checked = false;
    const dictionary& solverDict = mesh_.data().solverPerformanceDict();
    for (const entry& solverPerfDictEntry : solverDict)
    {
        const word& fieldName = solverPerfDictEntry.keyword();
        const label fieldi = applyToField(fieldName);
        if (fieldi != -1)
        {
            Pair<scalar> residuals = maxResidual(solverPerfDictEntry);
            checked = true;
            const bool absCheck = (residuals.first() < residualControl_[fieldi].absTol);
            achieved = achieved && absCheck;
        }
    }
    return checked && achieved;
}

三点:(1) 无 residualControl → 恒 false,只能按 endTime 结束(构造期提示第 109--118 行 "no convergence criteria found. Calculations will run for steps.");(2) absCheck 用初始残差 residuals.first() 与 absTol 比;(3) 返回 checked && achieved(至少检查过一个场且全部通过)。

loop()(第 136--161 行):setFirstIterFlag(true,true);read();若 initialised_ && criteriaSatisfied() 则打印 "solution converged in iterations" 并 runTime.writeAndEnd();否则 initialised_=true; storePrevIterFields()。initialised_ 保证"第一步不判收敛"(无残差数据)。storePrevIterFields()(第 160--168 行)store 5 种类型场。

7.3 pimpleControl:PISO/PIMPLE 状态机(机制链)

pimpleControl.C 第 41--58 行 read():

cpp 复制代码
    solutionControl::read(false);
    const dictionary pimpleDict(dict());
    solveFlow_ = pimpleDict.getOrDefault("solveFlow", true);
    nCorrPIMPLE_ = pimpleDict.getOrDefault<label>("nOuterCorrectors", 1);
    nCorrPISO_ = pimpleDict.getOrDefault<label>("nCorrectors", 1);
    SIMPLErho_ = pimpleDict.getOrDefault("SIMPLErho", false);
    turbOnFinalIterOnly_ = pimpleDict.getOrDefault("turbOnFinalIterOnly", true);
    finalOnLastPimpleIterOnly_ = pimpleDict.getOrDefault("finalOnLastPimpleIterOnly", false);
    ddtCorr_ = pimpleDict.getOrDefault("ddtCorr", true);

默认值信息量很大 :nOuterCorrectors=1(即默认 PISO 模式)、nCorrectors=1、turbOnFinalIterOnly=true(默认只在最后外迭代算湍流)、ddtCorr=true、finalOnLastPimpleIterOnly=false。

criteriaSatisfied()(第 61--126 行):

cpp 复制代码
bool Foam::pimpleControl::criteriaSatisfied()
{
    if ((corr_ == 1) || residualControl_.empty() || finalIter()) return false;
    const bool storeIni = this->storeInitialResiduals();
    bool achieved = true, checked = false;
    const dictionary& solverDict = mesh_.data().solverPerformanceDict();
    for (const entry& solverPerfDictEntry : solverDict)
    {
        const word& fieldName = solverPerfDictEntry.keyword();
        const label fieldi = applyToField(fieldName);
        if (fieldi != -1)
        {
            Pair<scalar> residuals = maxResidual(solverPerfDictEntry);
            checked = true;
            scalar relative = 0.0;  bool relCheck = false;
            const bool absCheck = (residuals.last() < residualControl_[fieldi].absTol);
            if (storeIni)
                residualControl_[fieldi].initialResidual = residuals.first();
            else
            {
                const scalar iniRes = (residualControl_[fieldi].initialResidual + ROOTVSMALL);
                relative = residuals.last() / iniRes;
                relCheck = (relative < residualControl_[fieldi].relTol);
            }
            achieved = achieved && (absCheck || relCheck);
        }
    }
    return checked && achieved;
}

关键差异(相对 SIMPLE) :(1) 从第二个外迭代 才判(corr_==1 时"nothing has been calculated yet");(2) storeInitialResiduals() 为真时把当前初始残差存为基准,否则用 residuals.last()/initialResidual 得相对残差 ;(3) achieved = achieved && (absCheck || relCheck)------绝对或相对任一达标即可 ;(4) absCheck 用最终残差 residuals.last()(PIMPLE 关注迭代内是否收敛),与 SIMPLE 用 first() 相反。

storeInitialResiduals()(pimpleControlI.H 第 79--83 行):return (corr_ == 2) && (corrPISO_ == 0) && (corrNonOrtho_ == 0);------第 2 个外迭代的第 0 个 PISO 子迭代、第 0 个非正交迭代时记录基准初始残差。

loop()(第 200--265 行):

cpp 复制代码
bool Foam::pimpleControl::loop()
{
    read();  ++corr_;
    setFirstIterFlag();
    if (corr_ == nCorrPIMPLE_ + 1)          // 达到最大外迭代
    {
        if (!residualControl_.empty() && (nCorrPIMPLE_ != 1))
            Info<< algorithmName_ << ": not converged within "
                << nCorrPIMPLE_ << " iterations";
        corr_ = 0; mesh_.data().setFinalIteration(false); return false;
    }
    bool completed = false;
    if (converged_ || criteriaSatisfied())
    {
        if (converged_)                     // 第二次进来:真正退出
        {
            Info<< algorithmName_ << ": converged in " << corr_ - 1 << " iterations";
            mesh_.data().setFinalIteration(false); corr_ = 0; converged_ = false;
            completed = true;
        }
        else                                // 首次达标:多跑一轮
        {
            Info<< algorithmName_ << ": iteration " << corr_;
            storePrevIterFields(); mesh_.data().setFinalIteration(true);
            converged_ = true;
        }
    }
    else
    {
        if (finalIter()) mesh_.data().setFinalIteration(true);
        if (corr_ <= nCorrPIMPLE_)
            { Info<< algorithmName_ << ": iteration " << corr_;
              storePrevIterFields(); completed = false; }
    }
    return !completed;
}

状态流(可背) :corr_++ → 达 nCorrPIMPLE_+1 则打印 "not converged within N" 并复位退出;criteriaSatisfied() 首次真则打印 "iteration k"、converged_=true、setFinalIteration(true)、再跑一轮 ;下一轮 converged_==true 则打印 "converged in k-1 iterations" 真正退出。

finalIter()(pimpleControlI.H 第 92--95 行)= converged_ || (corr_ == nCorrPIMPLE_)。correct()(第 55--76 行):++corrPISO_,corrPISO_ <= nCorrPISO_ 返回 true,结束后归零。finalInnerIter()(第 98--112 行)= corrPISO_ == nCorrPISO_ && corrNonOrtho_ == nNonOrthCorr_+1,finalOnLastPimpleIterOnly_ 时再 && finalIter()。turbCorr()(第 121--143 行)返回 !turbOnFinalIterOnly_ || finalIter()。

correctNonOrthogonal()(solutionControlI.H 第 78--100 行):

cpp 复制代码
    ++corrNonOrtho_;
    if (corrNonOrtho_ <= nNonOrthCorr_ + 1) return true;   // 注意 n+1
    corrNonOrtho_ = 0;  setFirstIterFlag();  return false;

nNonOrthCorr_ + 1 :非正交修正次数 n 对应 n+1 次压力求解(多一次用于收敛判断/最终求解)------与第一季第 13 章 while (pimple.correctNonOrthogonal()) 一致。finalNonOrthogonalIter()(第 48--51 行)= corrNonOrtho_ == nNonOrthCorr_+1。setFirstIterFlag()(.C 第 180--203 行)在 force || (check && corr_<=1 && corrNonOrtho_==0) 时给 mesh data 打 firstIteration 标记,供 relaxation 等使用。

7.4 pisoControl

pisoControl(.../pisoControl/)是 pimpleControl 的 PISO 化封装(文档 "PISO controls are a sub-set of PIMPLE controls")。createControl.H 与各求解器按宏 PIMPLE_CONTROL/SIMPLE_CONTROL 决定用哪个类。第一季第 13 章 pimple.loop()/pimple.correct() 主循环的语义现在完全可解释。

7.5 算法参数到代码的映射总表

fvSolution 参数 代码落点 语义
nNonOrthogonalCorrectors n nNonOrthCorr_;循环 corrNonOrtho_ <= nNonOrthCorr_+1 压力方程非正交修正(实际 n+1 次求解)
nCorrectors n nCorrPISO_;correct() 每个外迭代压力修正次数
nOuterCorrectors n nCorrPIMPLE_;loop() PIMPLE 外迭代次数
momentumPredictor momentumPredictor_ 是否做动量预测
transonic/consistent/frozenFlow 同名开关 跨声速/SIMPLEC/冻结流动
solveFlow solveFlow_ 是否解流动
SIMPLErho SIMPLErho_ SIMPLE 模式密度更新
turbOnFinalIterOnly turbOnFinalIterOnly_;turbCorr() 只在最终外迭代算湍流
finalOnLastPimpleIterOnly finalOnLastPimpleIterOnly_ 最终求解只在最后外迭代
ddtCorr ddtCorr_ ddt 修正
residualControl residualControl_(List<fieldData>) 收敛判据

8. 表达式系统:从字符串到 exprValue/exprResult

8.1 exprDriver

exprDriver.H(src/OpenFOAM/expressions/exprDriver/,第 26--64 行):

perl 复制代码
    Base driver for parsing (field) values.
    Largely based on code and ideas from swak4foam
    Properties
        variables          | List of variables for expressions    | no | ()
        functions<scalar>  | Dictionary of scalar Function1       | no | {}
        functions<vector>  | Dictionary of vector Function1       | no | {}
        allowShadowing     | Allow variables to shadow field names| no | false

三点:源自 swak4foam ;variables 定义变量;functions<scalar>/functions<vector> 把 Function1 引入表达式;allowShadowing(默认 false)决定变量名能否遮蔽场名。搜索控制(第 102--110 行):SEARCH_REGISTRY(先查注册表)、SEARCH_FILES(查磁盘)、CACHE_READ_FIELDS(缓存读盘场)、DEFAULT_SEARCH = SEARCH_REGISTRY。

数据成员(第 139--194 行)中与解析相关者:exprResult result_(结果);List<exprString> variableStrings_/HashTable<exprResult> variables_(变量表);HashTable<refPtr<Function1<scalar>>> scalarFuncs_/<vector> vectorFuncs_;contextObjectTableType contextObjects_(外部注入的上下文场,如 pos()/C() 背后几何);scalar arg1Value_;bool allowShadowing_;searchControls searchCtrl_。

两个必需虚函数(第 537--561 行) :virtual unsigned parse(...) = 0; 与模板 evaluate<Type>()/evaluateUniform<Type>()。exprDriverI.H(第 85--110 行):

cpp 复制代码
template<class Type>
Foam::tmp<Foam::Field<Type>> Foam::expressions::exprDriver::evaluate
(const expressions::exprString& expr, bool wantPointData)
{ parse(expr);  return getResult<Type>(wantPointData); }

template<class Type>
inline Type Foam::expressions::exprDriver::evaluateUniform
(const expressions::exprString& expr, bool wantPointData)
{ parse(expr);  return result_.getUniform(1, true).cref<Type>().first(); }

关键语义 :evaluate<Type> 返回场 ,evaluateUniform<Type> 返回单值 (取结果场第一个元素);wantPointData 区分"单元/点数据"(对应 FieldAssociation,见 8.6)。

8.2 exprResult:多态的"值或场"

exprResult.H(src/OpenFOAM/expressions/exprResult/,第 76--100 行):

cpp 复制代码
class exprResult
{
    word valType_;            // 运行期类型标签
    bool isPointData_;        // 是否点数据
    bool noReset_, needsReset_;
    label size_;              // 场长度(<=1 表示单值)
    expressions::exprValue value_;   // 单值表示
    void *fieldPtr_;          // 已分配的裸场(如 scalarField)

exprResult 用"单值 exprValue + 裸指针 fieldPtr_ + size_"表达"值/场二象性" :size_ <= 1 是单值,否则是场。valType_ 是运行期类型标签(对应 valueTypeCode 的名字)。

8.3 exprValue 与 exprTraits:类型分派的基础设施

exprTraits.H(第 81--99 行)定义类型码:

cpp 复制代码
enum class valueTypeCode : unsigned char
{
    NONE = 0, INVALID,
    type_bool, type_label, type_scalar,          // Rank 0
    type_vector,                                 // Rank 1
    type_sphericalTensor, type_symmTensor, type_tensor   // Rank 2
};

exprTypeTraits(第 136--178 行)是类型到"码/秩/分量数"的编译期映射:

cpp 复制代码
template<class Type>
struct exprTypeTraits
{
    typedef Type value_type;
    static constexpr const char* const name = "";
    static constexpr expressions::valueTypeCode value = valueTypeCode::INVALID;  // 默认 INVALID
    static constexpr ::Foam::direction rank = 0;
    static constexpr ::Foam::direction nComponents = 0;
};
#define defineExprTypeTraits(Type, Name, Rank, NumCmpts) \
    template<> struct exprTypeTraits<Type> { \
        typedef Type value_type; \
        static constexpr const char* const name = #Name; \
        static constexpr expressions::valueTypeCode value = valueTypeCode::type_##Name; \
        static constexpr ::Foam::direction rank = Rank; \
        static constexpr ::Foam::direction nComponents = NumCmpts; };
defineExprTypeTraits(bool, bool, 0, 1);
defineExprTypeTraits(::Foam::label, label, 0, 1);
defineExprTypeTraits(::Foam::scalar, scalar, 0, 1);
defineExprTypeTraits(::Foam::vector, vector, 1, 3);
defineExprTypeTraits(::Foam::tensor, tensor, 2, 9);
defineExprTypeTraits(::Foam::sphericalTensor, sphericalTensor, 2, 1);
defineExprTypeTraits(::Foam::symmTensor, symmTensor, 2, 6);

未特化的默认 value = INVALID ------正是 exprValue::supportedType<Type>()(exprValueI.H 第 76--83 行)判据:return valueTypeCode::INVALID != exprTypeTraits<Type>::value;。

exprValue(exprValue.H 第 152--160 行)是带类型标签的联合体 :Detail::exprValueUnion data_; + valueTypeCode typeCode_ = NONE;。exprValueUnion(第 83--116 行)用宏 FOR_ALL_EXPR_VALUE_TYPES(第 53--61 行,顺序 scalar,vector,tensor,symmTensor,sphericalTensor,label,bool)为每个类型建一个联合体成员与特化的 get<Type>()/set<Type>();对未知类型走基类(第 107--115 行):

cpp 复制代码
        template<class Type> inline const Type* get() const noexcept { return nullptr; }
        template<class Type> inline void set(const Type& val) noexcept
        { notSpecialized("set<" + std::string(typeid(Type).name()) + '>'); }

类型分派机制 :exprTraits 编译期给每种类型一个码;exprValue 按码选联合体成员;运行期用 typeCode_ 判断"当前存的是哪种"。源码注释(第 30--33 行):"a 'fatter' representation ... avoids heap allocations ... lists and fields would box/unbox an entire field, not individual values"。

8.4 fieldExpr:token → AST → evaluate(机制链)

fieldExpr(src/OpenFOAM/expressions/fields/)实现原始场表达式 (非网格相关,按 size() 展开):fieldExprScanner.H/.cc(Ragel 生成的词法扫描器 )、fieldExprParser.H(Lemon 语法分析器接口,第 51--106 行):

cpp 复制代码
class parser
{
    static constexpr const char* const prompt_ = "fieldExpr:";
    void* lemon_;                     // Lemon parser(需求驱动)
public:
    void start(parseDriver& driver_);
    void stop();
    void parse(int tokenId);
    void parse(int tokenId, scanToken tok);
};

fieldExprDriver.H(第 26--58 行):parseDriver 同时继承 Ragel 扫描器与 exprDriver:

cpp 复制代码
class parseDriver
:
    public parsing::genericRagelLemonDriver,
    public expressions::exprDriver
{
protected:
    label size_;     // 场尺寸

fieldExprFwd.H(第 60--61 行)给出别名 typedef fieldExpr::parseDriver fieldExprDriver;------这就是 exprDriver/fieldExprDriver/parseDriver 三个名字的关系。

完整管线: 字符串 → exprString(字典变量展开)→ Ragel 词法器(scanToken 流)→ Lemon parser::parse(tokenId, tok)(需求驱动,语法归约时逐节点语义动作)→ 查 variables_/contextObjects_/scalarFuncs_ → 写 exprResult result_ → getResult<Type>()(场或单值)。注意 size_ :parseDriver(const label len = 1)(第 101--104 行)使同一语法既能算单值(len=1)也能算场(len=网格单元数)------这就是 #calc(单值)与 exprField(场)共用同一语言的原理。

8.5 exprString 与 expressionEntry:变量展开

exprString.H(第 26--39 行):"A variant of Foam::string with expansion of dictionary variables into a comma-separated form.",继承 string(第 60--63 行),保留字典上下文展开、剥离 C/C++ 注释(第 86--94 行)。

expressionEntry.H(第 30--66 行)解释为何要特殊展开:

scss 复制代码
    condition  "mag(pos() - $centre) < 0.5";     // 普通展开成 (0.21 0 0.01)
    condition  "mag(pos() - $[(vector)centre]) < 0.5";  // 特殊展开成 vector(0.21,0,0.01)

这就是表达式里 $[...] 特殊语法的由来 :普通 $var 展开成 (a b c),$[(vector)var] 展开成 vector(a,b,c)(带类型强转、逗号分隔)。expressionEntry 是 exprEntry/ 里一组子类(Vectors/Strings/Bool/Dimensioned),通过 RTS 选"条目 → 表达式串"转换(头 106--113 行)。

8.6 FieldAssociation

exprFieldAssociation.H(第 44--50 行):NO_DATA=0、POINT_DATA=1、FACE_DATA=2、VOLUME_DATA=4。wantPointData 参数与 POINT_DATA/VOLUME_DATA 对应 :结果可能落在点/面/体上。pos() 返回单元中心(体数据),points() 返回点坐标(点数据)------同一语法在 fvOptions、边界条件、体积/面/点域都能用的原因。


9. #eval / #calc / Function1Expression:表达式的三个落点

9.1 #eval(机制链收尾)

evalEntry(src/OpenFOAM/db/dictionary/functionEntries/evalEntry/evalEntry.C)是实现。evaluate()(第 61--178 行)核心:

cpp 复制代码
    expressions::exprString::inplaceExpand(s, parentDict, true);   // 字典变量展开
    stringOps::inplaceTrim(s);
    // 去除多余的尾部分号(第 111--133 行)
    if (s.empty())
        { InfoErr << "Empty #eval - line " << is.lineNumber()
                  << " in file " << parentDict.relativeName(); return tokenList(); }
    expressions::exprResult result;
    {
        expressions::fieldExprDriver driver(fieldWidth);   // 注意 fieldWidth
        driver.parse(s);
        result = std::move(driver.result());
    }
    if (!result.hasValue() || !result.size())
        { InfoErr << "Failed #eval - line " << is.lineNumber()
                  << " in file " << parentDict.relativeName(); return tokenList(); }
    OTstream toks;
    if (result.size() <= 1) result.writeValue(toks);
    else                    result.writeField(toks);
    return tokenList(std::move(toks.tokens()));

这条链路把第 8 节全部机制串起来 :先 exprString::inplaceExpand 做字典变量展开(含 $[...]),再 fieldExprDriver driver(fieldWidth)(fieldWidth 就是结果场宽度 ),driver.parse(s) 走 Ragel 词法 + Lemon 语法,result 是 exprResult,最后按 size() 决定 writeValue(单值)还是 writeField(场),把结果转成 token 插回字典。

evaluate(Istream&)(第 181--244 行)支持四种写法:#eval "expr"、#eval INT "expr"、#eval { expr }(读到大括号,剥注释)、#eval #{ expr #}(多行);非法输入抛 FatalIOErrorInFunction(...) << "Invalid input for #eval. Expecting a string or block to evaluate, but found"。

9.2 #calc

calcEntry(src/OpenFOAM/db/dictionary/functionEntries/calcEntry/calcEntry.C,第 64--98 行):

cpp 复制代码
Foam::string Foam::functionEntries::calcEntry::evaluate
(const dictionary& parentDict, Istream& is)
{
    dynamicCode::checkSecurity("functionEntries::calcEntry::evaluate(..)", parentDict);
    string s(is);
    dictionary codeSubDict;
    codeSubDict.add("code", "os << (" + s + ");");        // 包成 C++ 语句
    dictionary codeDict(parentDict, codeSubDict);
    OStringStream buf(is.format());
    buf.precision(16);
    streamingFunctionType function = getFunction(parentDict, codeDict);
    (*function)(buf, parentDict);
    return buf.str();
}

#eval vs #calc: #eval 用表达式系统(不需编译器 ),#calc 用运行期 C++ 编译(codeStream/dynamicCode,需编译器 ,且经 dynamicCode::checkSecurity 安全检查,精度 16)。#calc "$R*2" 里的 $R 必须先被字典普通展开。

9.3 Function1Expression

Function1Expression.H(src/OpenFOAM/expressions/Function1/,第 26--58 行)文档:

scss 复制代码
        uniformValue
        {
            type            expression;
            variables       ( "start = 0.5"  "stop  = 1" );
            expression      #{ mag(arg() > start && arg() < stop) * vector(1, 0, 0) #};
        }

并明确 "Very much like #eval but runtime" ------把同一个 fieldExprDriver 用作 Function1:arg() 即自变量(exprDriver::argValue(),exprDriverI.H 第 31--40 行)。makeFunction1Expression.C(第 34--38 行)为 5 种类型注册 expression:makeFunction1Type(Function1Expression, scalar), vector, sphericalTensor, symmTensor, tensor。

一个表达式引擎服务五种落点 :#eval(字典条目)、Function1Expression(时间/坐标函数)、fvExpressionField/exprField(场生成)、SemiImplicitSource 的 exprField 源项、volumeExprDriver/patchExprDriver(体积/面域)。SemiImplicitSource.C(第 106--122 行)即按 modelType == "exprField" 建 new expressions::volumeExprDriver(mesh_, exprDict)。


10. 可直接粘贴的完整字典模板

均按 v2606 语法书写,可直接粘贴进 system/controlDict 的 functions {} 或 system/fvSolution。

10.1 模板 A:完整的 functionObject(fieldMinMax)

c 复制代码
functions
{
    minMax
    {
        type        fieldMinMax;              // 必填(RTS 键)
        libs        (fieldFunctionObjects);   // 必填(库)
        mode        magnitude;                // magnitude | component
        fields      (U p);                    // 要检查的场(支持正则)
        location    true;                     // 是否输出极值位置
        internal    false;                    // 只算内部场
        writePrecision  10;                   // 继承 writeFile:精度
        useUserTime     true;
        updateHeader    true;
        enabled     true;                     // 继承 functionObject
        log         true;
    }
}
条目 类型 必填 默认 含义与调参
type word 是 - fieldMinMax
libs words 是 - fieldFunctionObjects
mode word 否 magnitude 矢量/张量按模或分量取极值(标量恒分量)
fields wordList 是 - 场名列表(volFieldSelection,支持正则)
location bool 否 true 输出极值位置(多 3 列);大数据量设 false 精简
internal bool 否 false 只算内部场(跳过边界),提速
writePrecision int 否 system 默认 文本精度
useUserTime bool 否 true 用"用户时间"(角度等)替代物理时间
updateHeader bool 否 true 网格变化时重写表头

输出:postProcessing/minMax/<time>/fieldMinMax.dat,并在 results 里写 min(U) 等(6.1 节)。

10.2 模板 B:带 timeControl 的 functionObject(probes)

ini 复制代码
functions
{
    probes1
    {
        type            probes;
        libs            (sampling);
        fields          (p U);
        probeLocations  ( (0.1 0.1 0.1) (0.5 0.5 0.5) );
        interpolationScheme  cellPoint;
        fixedLocations  true;
        includeOutOfBounds true;

        executeControl   timeStep;      // ===== timeControl 段 =====
        executeInterval  1;
        writeControl     runTime;
        writeInterval    0.5;
        timeStart        0;
        timeEnd          100;

        enabled          true;
        log              false;
    }
}
条目 默认 语义 建议
executeControl 缺失→裸对象 检测到该键即被 functionObjects::timeControl 包装(2.2/4.1 节) 省 CPU 设 writeTime
executeInterval 1 整数步;0/1 都表示每步(3.5 节) -
writeControl 缺失→裸对象 写闸门;runTime 用浮点 interval(3.3 节) 大算例 runTime+较大值
writeInterval 1 timeStep/writeTime 为整数步;runTime 类经 userTimeToTime 采样频率
timeStart/timeEnd 0/inf 活跃时间窗(active()) 避开启动瞬态
controlMode time time/trigger/timeOrTrigger/timeAndTrigger 与 runTimeControl 联动用 trigger

10.3 模板 C:表达式 functionObject(exprField + #eval)

ini 复制代码
functions
{
    exprField1
    {
        type            exprField;
        libs            (fieldFunctionObjects);
        field           pTotal;
        action          new;                      // none | new | modify
        expression      "p + 0.5*(rho*magSqr(U))";
        dimensions      [ Pa ];
        store           true;
        autowrite       false;
    }

    exprField2                                        // 表达式局部修改
    {
        type            exprField;
        libs            (fieldFunctionObjects);
        field           p;
        action          modify;
        fieldMask       "(mag(pos()) < 0.05) && (pos().y() > 0)";
        expression      "1.2*p";
    }

    evalEntry
    {
        type            exprField;                    // 也可在其它字典里用 #eval
        libs            (fieldFunctionObjects);
        field           cellSizeField;
        expression      "#eval { 2*pow(V, 1.0/3.0) }";
    }
}
条目 默认 语义
action new new 新建/覆盖;modify 按 fieldMask 改既有场;none 不操作
field - new 时输出场名;modify 时输入/输出场名
expression - 表达式串(pos()/mag()/V...),可 #eval {}
fieldMask 空 逻辑表达式,为真的单元才应用 modify
dimensions - new 时新场量纲(如 [ Pa ])
store true 结果是否留在输出注册表供复用(5.4 节)
autowrite false 新场是否加 AUTO_WRITE
readFields - 后处理模式预加载的场

useNamePrefix 对 modify 动作被忽略(头文件第 98--99 行)。

10.4 模板 D:SIMPLE 的 residualControl

ini 复制代码
SIMPLE
{
    nNonOrthogonalCorrectors 0;
    momentumPredictor   true;
    consistent          false;

    residualControl
    {
        p               1e-4;      // 绝对容差
        U               1e-5;
        "k.*"           1e-4;      // 支持正则
        omega           1e-4;
    }
}
条目 类型 含义
p/U/... scalar 绝对容差 absTol;判据 残差.first() < absTol(初始残差)
键名 wordRe 支持正则 (applyToField 用 wordRe::match)
无 residualControl - criteriaSatisfied() 恒 false,只按 endTime 结束

10.5 模板 E:PIMPLE 的 residualControl 与迭代控制

ini 复制代码
PIMPLE
{
    nOuterCorrectors 3;
    nCorrectors      2;
    nNonOrthogonalCorrectors 1;
    momentumPredictor        true;
    turbOnFinalIterOnly      true;
    finalOnLastPimpleIterOnly false;
    ddtCorr                  true;

    residualControl
    {
        "(U|k|omega)" { tolerance 1e-5; relTol 0.01; }   // 字典形式
        p             { tolerance 1e-6; relTol 0.05; }
    }
}

| 条目 | 默认 | 判据/语义 | 建议 |
|--------------------|----|---------------------------------------------------------|-----------|-------------------------|-----------|
| nOuterCorrectors | 1 | 外迭代;=1 即"Operating solver in PISO mode" | 强耦合设 2--5 |
| nCorrectors | 1 | 每外迭代压力修正 | 通常 1--2 |
| tolerance | - | 绝对 容差,判 residuals.last() < tol | - |
| relTol | - | 相对 容差,判 residuals.last()/initialResidual < relTol | - |
| 收敛条件 | - | `absCheck | | relCheck`(绝对或相对达标) | 二者配合,避免过严 |
| 首迭代 | - | corr_==1 或 finalIter() 时不判(7.3 节) | - |


11. 常见报错/警告速查(≥20 条)

【逐字】 为实际 Read 到的源码原文;【样式】 为同模块同类错误的典型形态(用于识别)。

# 报错/警告文本 出处(文件 行) 成因 排查 解决
1 【逐字】Cannot load function type <T> ... Table of functionObjects is empty functionObject.C 115--121 type 对但 libs 未写/库名错 看该行前是否打印 type;查 libs 补 libs (fieldFunctionObjects); 等
2 【逐字】Unknown function type <T>(FatalErrorInLookup("function", ...)) functionObject.C 129--134 type 拼错或该库未加载 对比可用类型名 改正 type 或加载含该类型的库
3 【逐字】#### <type> not available #### functionObject.C 229--247 该平台未编译提供该类型 确认版本支持 换类型/重编库
4 【逐字】'functions' entry is not a dictionary functionObjectList.C 973 functions 写成值/列表 检查是否 functions { } 改成字典
5 【逐字】Entry '<key>' is not a dictionary(IOWarning) functionObjectList.C 1037 functions{} 顶层出现非字典条目(如 executeControl 写顶层) 把条目移进 FO 子字典 每个 FO 一个子字典
6 【逐字】Entry 'useNamePrefix' is not a valid switch(IOWarning) functionObjectList.C 1030 顶层 useNamePrefix 值非法 用 true/false/on/off 改正
7 【逐字】The sub-dictionary '<key>' masks error handling for functions(Warning) functionObjectList.C 354 errors 写成了字典而非枚举词 改为 errors warn; 改类型
8 【逐字】<X> is not in enumeration: default warn ignore strict functionObjectList.C 365 errors 值非法 对照枚举 用四值之一
9 【逐字】Found deprecated 'outputControl' Use 'writeControl' with 'writeInterval' timeControl.C 132--136 用了旧名 outputControl - 改 writeControl/writeInterval
10 【逐字】Undefined time control: <X> timeControl.C 276--279 *Control 值不在枚举 对照 3.1 节 用 timeStep/writeTime...
11 【逐字】Cannot find functionObject file <name>(Warning) functionObjectList.C 277 postProcess -func <name> 找不到配置 检查名字/大小写 修正名字
12 【逐字】Invalid input (after trailing ';') for #eval evalEntry.C 128--131 #eval 串 ; 后内容非空白 去掉多余分号内容 修正表达式
13 【逐字】Empty #eval - line <n> in file <dict> evalEntry.C 142--145 #eval 求值为空 检查表达式/变量展开 补合法表达式
14 【逐字】Failed #eval - line <n> in file <dict> evalEntry.C 159--162 result 无值或尺寸为 0 检查表达式类型/尺寸 修正表达式或 fieldWidth
15 【逐字】Invalid field width: <n> evalEntry.C 72--75 #eval INT ... 的 INT < 1 看首个整数 用 ≥1 宽度
16 【逐字】Invalid input for #eval. Expecting a string or block to evaluate, but found ... evalEntry.C 231--235 #eval 后非字符串也非 {} 检查语法 用 "..." 或 { ... }
17 【逐字】Residual data for <f> must be specified as a dictionary solutionControl.C 86--89/109--112 PIMPLE 模式(absTolOnly=false)下用标量值 检查该场条目 改成 { tolerance ...; relTol ...; }
18 【逐字】<ALG>: not converged within <N> iterations pimpleControl.C 217--218 外迭代用尽仍不满足 residualControl 看残差日志 增 nOuterCorrectors 或放宽容差
19 【逐字】<ALG>: Operating solver in PISO mode pimpleControl.C 190 nOuterCorrectors=1 信息性 需 PIMPLE 时设 >1
20 【逐字】<ALG>: no convergence criteria found. Calculations will run for <T> steps. simpleControl.C 115--117 SIMPLE 未给 residualControl 信息性(按 endTime 结束) 补 residualControl
21 【逐字】<ALG>: no residual control data found. Calculations will employ <N> corrector loops pimpleControl.C 171--173 PIMPLE 有外迭代但无 residualControl 信息性 需早停则补 residualControl
22 【逐字】Could not locate subRegion "<name>"(Warning) regionFunctionObject.C 63--66 subRegion 注册表不存在 检查子区域/表面输出名 改正名字或先让上游 FO 生成
23 【逐字】Unknown min/max mode: <X> fieldMinMax.txx 397--399 mode 非法 对照 magnitude/component 改正
24 【逐字】Field <f> not found in database for averaging(Warning) fieldAverage.C 89--93 fieldAverage 场不在注册表 检查场名/是否被其它 FO 生成 改正场名或调整 FO 顺序
25 【逐字】Duplicate entry found: <f> (ignored)(Warning) fieldAverage.C 328--331 fields 同一基场出现两次 检查列表 去重
26 【样式】Cannot find file "<path>" ... for field "<f>" 字段查找路径 场文件不在 <time> 目录 检查时间目录与场名 补文件/改正名字
27 【样式】request for objectRegistry ... not found 注册表查找(lookupObject) FO 依赖的场尚未生成(FO 顺序) 检查 functions{} 中先后 把生产方放前面

排查三原则 :(1)先看报错前的上下文行 (type/libs 打印);(2)把 errors warn; 临时加到该 FO 上,让构造失败不终止算例,便于定位;(3)表达式类问题用 #eval 的 Failed/Empty 提示 + debug.driver/debug.parser 调试开关(exprDriver.H 第 44--50 行)。


12. 对比与选型表

12.1 表 1:functionObjects 分类对比

目录 代表类型 典型落点 execute/write 侧重
field/ fieldMinMax、fieldAverage、exprField、scalarTransport 场统计/生成 write() 写文件+setResult
forces/ forces、forceCoeffs、bladeForces 气动力/力矩 execute() 积分,write() 写 dat
sampling/ probes、sets、surfaces 点/线/面采样 write() 采样写盘
utilities/ writeObjects、solverInfo、graphFunctionObject、runTimeControl、codedFunctionObject 通用/控制 视用途
solvers/ scalarTransport、energyTransport 内置小求解器 execute() 求解
lagrangian/ cloudInfo、vtkCloud 粒子云 write() 输出
phaseSystems/ sizeDistribution、phaseForces 多相统计 write()
randomProcesses/ energySpectrum 随机/谱 write()
initialisation/ hydrostaticPressure 初始场 execute()

12.2 表 2:采样/后处理方式对比

方式 对象定义 采样几何 输出格式 适用
probes probeLocations 离散点 .dat(每点一列) 时间序列/监测点
sets sets{ type cloud/uniform/... } 点集(点/线/圆/面片) vtk/raw/gnuplot 沿路径/线采样
surfaces surfaces{ type cuttingPlane/isoSurface/... } 面 vtk/ensight/obj 切面/等值面
graphFunctionObject 读 results 无(曲线) SVG 收敛史/曲线图
fieldAverage fields{ U{...} } 无(全场) 场 UMean/UPrime2Mean 时均/脉动均

12.3 表 3:solutionControl 家族对比

类 文件 residualControl 形式 收敛判据 loop() 语义 用于
pisoControl pisoControl/ 同 PIMPLE(absTolOnly=false) 内迭代收敛 PIMPLE 子集(外迭代 1) 瞬态 PISO
simpleControl simpleControl/ 标量绝对容差(absTolOnly=true) 初始残差 < absTol 收敛即 writeAndEnd() 稳态 SIMPLE
pimpleControl pimpleControl/ 字典 tolerance+relTol `最终残差<tol 相对残差<relTol`

12.4 表 4:表达式 vs coded vs Function1 vs #calc

工具 表达力 需编译 落点 典型用途 门槛
Function1(常数/table/polynomial/ramp) 时间/坐标预设函数 否 边界值、源项幅值 随时间变化 低
表达式(#eval/expression) 算术+几何+函数+逻辑 否 字典条目、exprField、Function1Expression 空间分布、条件式 中
exprField FO 表达式生成/修改场 否 functions{}、SemiImplicitSource 派生场、局部修正 中
#calc 任意 C++ 表达式 是 字典条目 简单算术 中高
coded(S2-12) 任意 C++ 是 FO/边界条件/Function1 复杂逻辑/特例 高

12.5 表 5:*Control 选项选型

*Control 值 判据(3.4 节) interval 类型 典型场景
none 恒 false - 暂停某 FO
always 恒 true - 强制每步
timeStep timeIndex % interval 整数步 默认
writeTime 写时刻计数 % interval 整数写次数 每 N 个输出
runTime/adjustableRunTime floor((t-t0+0.5Δt)/interval) 递增 用户时间 按物理时间输出
cpuTime floor(reduceMax(cpu)/interval) 递增 CPU 秒 CPU 预算控制
clockTime floor(reduceMax(clock)/interval) 递增 墙钟秒 长时间跑批
onStart timeIndex()==1 - 只首步
onEnd t > endTime-0.5Δt - 只末步

12.6 表 6:errors 策略对比

值 构造期 运行期 适用
default warn fatal 常规(IO 错误致命,其它告警)
warn warn warn 批量/不确定环境,尽量不中断
ignore silent silent 完全忽略该 FO 失败
strict fatal fatal 出问题必须停机(CI/验证)

13. 三块协同与第二季收官

13.1 三块如何咬合

bash 复制代码
controlDict
 ├── functions { ... }            → functionObjectList
 │      ├── read:digest 比较 → 复用/重建;写 timeControl 键则被包装
 │      ├── execute(每步):裸对象 execute+write;包装对象按 executeControl/writeControl
 │      └── 写时刻:写 functionObjectProperties(trigger/results/fieldAverage 累计)
 │           ↑                           ↑
 │      表达式(exprDriver)         stateFunctionObject(trigger/results)
 │      exprField/#eval/函数1          graphFunctionObject/runTimeControl
 └── 求解器主循环
       └── pimple/simple/piso(solutionControl 家族)
             ├── loop()/correct()/correctNonOrthogonal()   ← fvSolution 的算法参数
             ├── residualControl 判据(SIMPLE 绝对 / PIMPLE 绝对或相对)
             └── 方程里可含 fvOptions(S2-13)与 表达式(exprField/Function1)

三条闭环:(1)调度闭环 ------Time::run() → functionObjects_.start()/execute()/end(),runTimeControl 可 writeAndEnd() 提前结束,Time::run() 随后重算 isRunning;(2)状态闭环 ------FO 的极值/平均/收敛量 → setResult/setProperty → functionObjectProperties → 重启时读回,另一个 FO(graphFunctionObject/fieldAverage)消费它;(3)表达式闭环 ------字典串 → exprString 展开 → fieldExprDriver 解析求值 → exprResult → 既是 #eval 的 token、也是 exprField 的场、也是 Function1Expression 的函数值。

13.2 关键设计哲学

  • 契约最小化 :functionObject 只有 execute()/write() 两条纯虚,其余皆可选钩子;writeObjects 连 execute() 都可空实现。
  • 配置即行为 :同一批 FO,加不加 *Control 键决定"裸对象"还是"被包装";writeInterval 的整数/浮点语义由 control 类型决定。
  • 运行期装配 :type + libs 经 RTS 装配;errors 决定失败是否可容忍。
  • 复用优先 :输出场进输出注册表,结果进 functionObjectProperties,故 FO 能层层复用、graphFunctionObject 能直接读别的 FO 的 results。

13.3 第二季"框架与基础设施"四章回顾

  • S2-12 coded:字典里的 C++,运行期编译加载。
  • S2-13 fvOptions :字典里的源项/约束/修正,免编译(其 exprField 源项直接调用本章表达式系统)。
  • S2-14 本章:字典里的后处理(functionObjects)、迭代控制(solutionControl)、数学(表达式)。
  • 共同精神:把"可能要改源码"的东西抽象成"字典里可配置的对象",由 RTS 在运行期装配------这正是 OpenFOAM 的核心设计哲学。

本章要点(速记)

  • functionObject 契约:name/log/execute/write/end/read/adjustTimeStep/filesModified/updateMesh/movePoints;postProcess 强制忽略 *Control。
  • functionObjectList::read():functions 字典 → SHA1 digest 比较 → 复用/重建/包装切换 → errors 分派 → returnReduceAnd 并行校验 → transfer()。
  • functionObjectList::execute():唯一调度点,同时调用每个对象的 execute() 与 write();WARN/IGNORE 用 FatalError.throwing(true) + try/catch 隔离;写时刻写 functionObjectProperties。
  • timeControl:*Control/*Interval;intInterval_<=1 表示每步;runTime/cpuTime/clockTime 用索引递增 + userTimeToTime。
  • functionObjects::timeControl:包装器,两闸门 + active()(time/trigger);write() 会补齐 execute();adjustTimeStep() 做 writeInterval 对齐与 ramp。
  • region/state/properties:obr() 解析 region/subRegion;stateDict() 即 propsDict();triggerIndex 是 FO 间联动公共量。
  • solutionControl:fieldData(wordRe+abs/rel/initial)+ residualControl_;SIMPLE absTolOnly、PIMPLE 字典 relTol;loop()/correct()/correctNonOrthogonal() 是算法参数化身;PIMPLE 达标后多跑一轮。
  • 表达式:exprDriver(swak4foam 血统)+ exprValue(类型标签联合体)+ exprTraits(编译期类型码)+ fieldExpr(Ragel 词法 + Lemon 语法);#eval 走表达式、#calc 走运行期 C++ 编译;Function1Expression/exprField 复用同一引擎。
相关推荐
鶴哥只手遮天1 小时前
OpenFOAM 源码深度剖析 · 第二季-13 `fvOptions` / `fvConstraints` / `corrections`
算法·开源
鶴哥只手遮天1 小时前
OpenFOAM 源码深度剖析 · 第二季-03 拉格朗日粒子与喷雾:`Cloud`/`parcel` 模板体系、运动积分与子模型群
算法·开源
hetao17338371 小时前
校内场-提高组 ZYZSC-S-Round 2
c++·算法
小严不想当码农1 小时前
icpc 亚洲区域赛补题链接整理
算法
xiaok1 小时前
Compositor图编辑器
算法
CAD开发小慧1 小时前
CAE软件开发:Coreform为何用ACIS处理CAD几何?
算法·架构·计算机图形学
七牛云行业应用1 小时前
DeepSeek Harness 自定义 Skill 教程:从 SKILL.md 到可复用 Agent 工作流
算法·github
hetao17338371 小时前
2026-10-14 hetao1733837 的刷题记录
c++·算法
Vitaliy Kostrov(柯维远)1 小时前
谁来承担额外工作?CDN 参与者如何协商资源
nginx·缓存·系统架构·开源·cdn