OpenFOAM 源码深度剖析 · 第二季-14 functionObjects、求解器控制框架 与 表达式系统
本季最后一章"框架与基础设施",讲三块让 OpenFOAM "用起来顺手"的机制:functionObjects (可插拔后处理/运行期工具)、求解器控制框架 (PISO/SIMPLE/PIMPLE 的控制对象)、表达式系统 (在字典里写数学表达式)。它们共同构成 OpenFOAM 的"用户自动化"能力。本章结论均来自
v2606(master)源码逐行阅读,路径与行号以仓库d:\CODE\openfoam-master为准。
全章地图
functionObject基类:接口与生命周期。functionObjectList:调度中枢(read/execute/end的真实实现)。timeControl:execute/write 的"闸门"与时间换算。functionObjects::timeControl包装器。regionFunctionObject/stateFunctionObject/functionObjectProperties。- 常用 functionObjects 逐个剖析。
solutionControl家族与 SIMPLE/PIMPLE 状态机。- 表达式系统:从字符串到
exprValue/exprResult。 #eval/#calc/Function1Expression:表达式的三个落点。- 可直接粘贴的完整字典模板(含调参表)。
- 常见报错/警告速查(≥20 条)。
- 对比与选型表(≥5 张)。
- 三块协同与第二季收官。
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 行)建的IOobjectpath 是"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_;SIMPLEabsTolOnly、PIMPLE 字典 relTol;loop()/correct()/correctNonOrthogonal()是算法参数化身;PIMPLE 达标后多跑一轮。- 表达式:
exprDriver(swak4foam 血统)+exprValue(类型标签联合体)+exprTraits(编译期类型码)+fieldExpr(Ragel 词法 + Lemon 语法);#eval走表达式、#calc走运行期 C++ 编译;Function1Expression/exprField复用同一引擎。