OpenFOAM 源码深度剖析 · 第二季-13 fvOptions / fvConstraints / corrections:方程里的"可插拔源项与约束"
想让动量方程多一个体积力、能量方程多一个热源、或把某区域的速度"钳"住?传统做法是改求解器源码。OpenFOAM 提供了一条更优雅的路:
fvOptions------把"源项、约束、修正"统统做成运行期可按字典选择的fv::option对象,挂到任意方程上而不改一行求解器代码。这是第一季第 13 章里fvOptions(...)那股"神秘力量"的完整真相。本章从类族结构、RTS 选型、optionList遍历,一直拆到fvMatrix的source_/diag_/upper_/lower_组装与setValues钳制,最后给出可直接粘贴的字典模板与 20+ 条真实报错排查。所有路径与行号均来自本仓库src/与applications/下的真实文件。
1. 定位:fvOptions 到底解决了什么问题
1.1 一个"不改源码"的物理插件
在标准求解器里,动量方程通常是:
cpp
fvVectorMatrix UEqn
(
fvm::ddt(U) + fvm::div(phi, U)
+ MRF.DDt(U)
+ turbulence->divDevReff(U)
==
fvOptions(U)
);
(applications/solvers/incompressible/pimpleFoam/UEqn.H 第 5--12 行)
注意 == fvOptions(U)。右端这一项不是常量、也不是场,而是一个把"选项里的源项"装配进方程的表达式 。用户只要在 constant/fvOptions 里加一条:
ini
momentumSource
{
type meanVelocityForce;
selectionMode cellZone;
cellZone inletCellZone;
fields (U);
Ubar (1 0 0);
}
(tutorials/incompressible/pimpleFoam/LES/periodicHill/steadyState/system/fvOptions 第 17--26 行)
求解器立即获得"在 inletCellZone 内施加维持平均速度 (1 0 0) 的体积力"的能力------没有重新编译任何求解器 。这就是 fvOptions 的核心价值:把物理源项抽象成"可字典配置、运行期选择"的插件。
1.2 三个调用点 = 三类模型
标准求解器在动量方程里固定出现三个调用点(pimpleFoam/UEqn.H 第 11、17、23 行):
| 调用点 | 行号(pimpleFoam/UEqn.H) | 作用 | 对应模型家族 |
|---|---|---|---|
fvOptions(U) |
11 | 在方程里插入源项 S=Su+Spx | sources/ |
fvOptions.constrain(UEqn) |
17 | 对矩阵施加约束(固定值/阻尼) | constraints/ |
fvOptions.correct(U) |
23 | 对场施加修正(限幅) | corrections/(及部分 sources) |
同一个 fvOptions 对象可以被挂在动量、能量、组分、湍流等多个方程上------因为它本身只是一份字典加一个 fv::option 列表。
1.3 目录布局(与第一季不同,注意路径修正)
本仓库中框架代码与模型代码分处两处,行号引用会区分使用:
bash
src/finiteVolume/cfdTools/general/fvOptions/ ← 框架(第一季 fvMatrix 同一层级)
fvOptions.H/.C fv::options = IOdictionary + fv::optionList
fvOption.H/.C/.I/.IO.C fv::option 基类
fvOptionList.H/.C fv::optionList 遍历/装配
fvOptionListTemplates.C source()/constrain()/correct() 模板实现
makeFvOption.H RTS 注册宏
src/fvOptions/ ← 具体模型
cellSetOption/ 选区中间基类
sources/general|derived|interRegion/ 源项
constraints/general|derived/ 约束
corrections/ 修正
interRegionOption/ 跨区域基类
doc/fvOptionsDoc.H doxygen 分组
(框架来自 src/finiteVolume/cfdTools/general/fvOptions/ 的实际 glob 结果;模型来自 src/fvOptions/ 的 glob 结果。)
src/fvOptions/doc/fvOptionsDoc.H(第 28--50 行)用 doxygen 定义了四个分组:grpFvOptions、grpFvOptionsSources、grpFvOptionsCorrections、grpFvOptionsConstraints------这正好对应"一个基类 + 三个功能家族"的设计。
2. 类族与继承关系
2.1 全局视图
rust
IOdictionary ─┐
├─► fv::options (constant/fvOptions 本体)
fv::optionList┘ └─ PtrList<fv::option>
fv::option (抽象基类, type 由 RTS 决定)
├─ cellSetOption (选区中间基类)
│ ├─ SemiImplicitSource<Type>
│ ├─ CodedSource<Type>
│ ├─ meanVelocityForce / patchMeanVelocityForce
│ ├─ explicitPorositySource / heatExchangerSource / ...
│ ├─ FixedValueConstraint<Type>
│ ├─ fixedTemperatureConstraint / velocityDampingConstraint
│ └─ limitVelocity / limitTemperature / limitTurbulenceViscosity
└─ interRegionOption (跨区域中间基类)
└─ interRegionHeatTransfer / interRegionExplicitPorositySource
(继承关系由各 .H 中的 public fv::cellSetOption / public fv::option 声明汇总而来,例如 SemiImplicitSource.H 第 167--171 行、cellSetOption.H 第 166--169 行、interRegionOption.H 第 88--91 行。)
2.2 fv::options:字典与列表的合体
fvOptions.H(第 70--74 行)声明了类:
cpp
class options
:
public IOdictionary,
public fv::optionList
{
逐行解释:
- 第 72 行
public IOdictionary:options本身就是一部字典,直接对应constant/fvOptions(或system/fvOptions)文件,从而支持多区域路径constant/<region>/fvOptions(文件头注释第 34--46 行明确列出)。 - 第 73 行
public fv::optionList:options同时是"选项列表"------列表里每一项是一个fv::option。因此对fvOptions(U)的调用,本质是optionList::operator()(见 §5--§7)。 ClassName("fvOptions")(第 85 行):类名即字典名fvOptions。
构造与注册在 fvOptions.C:
cpp
Foam::fv::options& Foam::fv::options::New(const fvMesh& mesh)
{
auto* ptr = mesh.thisDb().getObjectPtr<fv::options>(typeName);
if (!ptr)
{
DebugInFunction
<< "Constructing " << typeName
<< " for region " << mesh.name() << nl;
ptr = new fv::options(mesh);
regIOobject::store(ptr);
}
return *ptr;
}
(fvOptions.C 第 123--138 行)
逐行解释:
- 第 125 行:先在网格的注册表里找
fv::options对象;找到就复用。这保证了同一个网格只构造一份fvOptions(多求解器/多区域共享)。 - 第 133 行
new fv::options(mesh):真正构造,转入文件读取。 - 第 134 行
regIOobject::store(ptr):把对象注册进objectRegistry,后续getObjectPtr才能命中。
字典位置探测逻辑在 fvOptions.C 第 50--95 行的匿名函数 createIOobject:
cpp
IOobject io
(
baseName,
mesh.time().constant(),
mesh.thisDb(),
IOobjectOption::MUST_READ,
IOobjectOption::NO_WRITE,
IOobjectOption::REGISTER
);
if (io.typeHeaderOk<IOdictionary>(true))
{
io.readOpt(IOobjectOption::READ_MODIFIED);
}
else
{
// Check if the fvOptions file is in system
io.instance() = mesh.time().system();
...
}
(fvOptions.C 第 58--85 行)
逐行解释:
- 第 58--66 行:先在
constant/下探测fvOptions。 - 第 68--70 行:存在则设为
READ_MODIFIED(运行期可改,改变即重读)。 - 第 73--77 行:否则改探
system/目录------这解释了为什么本仓库里既能看到constant/fvOptions(如porousBlockage)也能看到system/fvOptions(如periodicHill)。 - 第 83 行:两处都没有则
NO_READ(允许空配置,求解器照常见 "No finite volume options present" 运行)。
2.3 fv::option 基类:字段与接口
fvOption.H 第 96--131 行给出 option 的受保护成员:
cpp
class option
{
protected:
//- Source name
const word name_;
//- Model type
const word modelType_;
//- Reference to the mesh database
const fvMesh& mesh_;
//- Top level source dictionary
dictionary dict_;
//- Dictionary containing source coefficients
dictionary coeffs_;
//- Field names to apply source to - populated by derived models
wordList fieldNames_;
//- Applied flag list - corresponds to each fieldNames_ entry
List<bool> applied_;
//- Source active flag
bool active_;
逐行解释:
- 第 103 行
name_:选项名(用户在fvOptions里给的键,如momentumSource)。 - 第 106 行
modelType_:RTS 类型名(如meanVelocityForce),来自字典的type。 - 第 112 行
dict_:整条选项字典(含type/active/log/Coeffs)。 - 第 115 行
coeffs_:<type>Coeffs子字典(模型参数,如Ubar、Su)。 - 第 118 行
fieldNames_:该选项作用的场名列表------由派生模型自己填 (例如meanVelocityForce.C第 94 行coeffs_.readEntry("fields", fieldNames_);FixedValueConstraint.C第 68--74 行从fieldValues的键收集)。 - 第 121 行
applied_:与fieldNames_等长的"已应用"标志列表,配合checkApplied()检测"定义了却没用到的源项"。
关键接口(fvOption.H):
- 第 140 行
TypeName("option")+ 第 145--157 行declareRunTimeSelectionTable(...):运行期选择表 ,键是type词,值是构造函数指针(参数为name, modelType, dict, mesh)。 - 第 214--219 行
static autoPtr<option> New(name, dict, mesh):选型入口。 - 第 268--377 行:
addSup的 15 个重载(标量/矢量/张量 × 无 rho / 带 rho / 带 alpha+rho)。 - 第 380--410 行:
constrain的 5 个重载(按张量类型)。 - 第 413--423 行:
correct的 8 个重载(vol/surface × scalar/vector/tensor)。
2.4 构造:option::option
cpp
Foam::fv::option::option
(
const word& name,
const word& modelType,
const dictionary& dict,
const fvMesh& mesh
)
:
name_(name),
modelType_(modelType),
mesh_(mesh),
dict_(dict),
coeffs_(dict.optionalSubDict(modelType + "Coeffs")),
fieldNames_(),
applied_(),
active_(dict_.getOrDefault("active", true)),
log(true)
{
Log << incrIndent
<< indent << "Source: " << name_ << endl
<< indent << "State: " << (active_ ? "active" : "inactive") << endl
<< decrIndent;
}
(fvOption.C 第 57--79 行)
逐行解释:
- 第 69 行:
coeffs_取自dict.optionalSubDict(modelType + "Coeffs")。关键点 :子字典名由type拼出。type meanVelocityForce;对应meanVelocityForceCoeffs;type scalarSemiImplicitSource;对应scalarSemiImplicitSourceCoeffs。写了不匹配的Coeffs名会被静默忽略(子字典可缺省)。 - 第 72 行:
active_默认true。active false;可临时关闭而不删字典。 - 第 75--78 行:构造时打印
Source: <name>与State: active/inactive,这就是运行日志里那行Source: momentumSource。
2.5 option::New:RTS 选型的核心
cpp
Foam::autoPtr<Foam::fv::option> Foam::fv::option::New
(
const word& name,
const dictionary& coeffs,
const fvMesh& mesh
)
{
const word modelType(coeffs.get<word>("type"));
Info<< indent
<< "Selecting finite volume options type " << modelType << endl;
mesh.time().libs().open
(
coeffs,
"libs",
dictionaryConstructorTablePtr_
);
auto* ctorPtr = dictionaryConstructorTable(modelType);
if (!ctorPtr)
{
FatalIOErrorInLookup
(
coeffs,
"fvOption",
modelType,
*dictionaryConstructorTablePtr_
) << exit(FatalIOError);
}
return autoPtr<fv::option>(ctorPtr(name, modelType, coeffs, mesh));
}
(fvOption.C 第 84--117 行)
逐行解释:
- 第 91 行:
type是必填;缺失会直接报错。 - 第 96--101 行
mesh.time().libs().open(coeffs, "libs", dictionaryConstructorTablePtr_):允许选项字典带libs ("libX.so");,运行期动态加载外部库再查表------这就是"第三方源项库免重编译"的入口。 - 第 103--114 行:查不到类型时抛
FatalIOErrorInLookup,错误信息会列出所有可用类型 (*dictionaryConstructorTablePtr_),这正是报错清单里 "Unknown fvOption type ... Valid ... types" 的来源。 - 第 116 行:调用构造函数指针,完成多态创建。
2.6 RTS 注册宏 makeFvOption
cpp
#define makeFvOption(Option, Type) \
\
defineTemplateTypeNameAndDebugWithName \
( \
Foam::fv::Option<Foam::Type>, \
#Type#Option, \
0 \
); \
\
Foam::fv::option::adddictionaryConstructorToTable \
<Foam::fv::Option<Foam::Type>> \
add##Option##Type##dictionary##ConstructorTooptionTable_
(makeFvOption.H 第 36--48 行)
逐行解释:
#Type#Option:把模板参数名与模型名拼成类型串。makeFvOption(SemiImplicitSource, scalar)得到scalarSemiImplicitSource------这正是字典里写的type。adddictionaryConstructorToTable:把Foam::fv::Option<Foam::Type>的构造函数指针插入option::dictionaryConstructorTable。
因此模板模型的注册方式极其统一(semiImplicitSources.C 第 33--37 行):
cpp
makeFvOption(SemiImplicitSource, scalar);
makeFvOption(SemiImplicitSource, vector);
makeFvOption(SemiImplicitSource, sphericalTensor);
makeFvOption(SemiImplicitSource, symmTensor);
makeFvOption(SemiImplicitSource, tensor);
于是字典中可用的类型名就是 scalarSemiImplicitSource / vectorSemiImplicitSource / sphericalTensorSemiImplicitSource / symmTensorSemiImplicitSource / tensorSemiImplicitSource。同类注册见 fixedValueConstraints.C(第 33--37 行)与 codedFvSources.C(第 33--37 行)。
非模板模型则直接用 addToRunTimeSelectionTable(option, xxx, dictionary),例如 meanVelocityForce.C(第 37--44 行):
cpp
namespace Foam
{
namespace fv
{
defineTypeNameAndDebug(meanVelocityForce, 0);
addToRunTimeSelectionTable(option, meanVelocityForce, dictionary);
}
}
至此,type meanVelocityForce; 能找到构造函数,type scalarSemiImplicitSource; 也能------两套注册机制并存。
3. fv::optionList:列表的读取、遍历与装配
3.1 类声明与保护数据
fvOptionList.H 第 70--110 行:
cpp
class optionList
:
public PtrList<fv::option>
{
protected:
//- Reference to the mesh database
const fvMesh& mesh_;
//- Time index to check that all defined sources have been applied
label checkTimeIndex_;
逐行解释:
- 第 72 行:
optionList本身是PtrList<fv::option>,即"指向多态 option 的指针数组"。这是整个插件系统的容器。 - 第 85 行
checkTimeIndex_:用于周期性检查"是否有源项定义了却从未被使用"的时间戳,见checkApplied()。
3.2 读取:reset 与 readOptions
cpp
void Foam::fv::optionList::reset(const dictionary& dict)
{
// Count number of active fvOptions
label count = 0;
for (const entry& dEntry : dict)
{
if (dEntry.isDict())
{
++count;
}
}
this->resize(count);
count = 0;
for (const entry& dEntry : dict)
{
if (dEntry.isDict())
{
const word& name = dEntry.keyword();
const dictionary& sourceDict = dEntry.dict();
this->set
(
count++,
option::New(name, sourceDict, mesh_)
);
}
}
}
(fvOptionList.C 第 102--131 行)
逐行解释:
- 第 104--111 行:先数一遍字典里是子字典的条目个数 (只有子字典才是一个选项;非字典条目如
libs (...)会被跳过------注意fvOptions顶层的libs也可存在)。 - 第 114 行
resize(count):一次性分配列表长度。 - 第 117--130 行:第二遍,对每个子字典取
name(键名)与sourceDict,调用option::New(name, sourceDict, mesh_)创建多态对象并set进列表。注意顺序:选项按字典中出现顺序创建。
子字典名解析在 fvOptionList.C 第 45--51 行:
cpp
const Foam::dictionary& Foam::fv::optionList::optionsDict
(
const dictionary& dict
)
{
return dict.optionalSubDict("options", keyType::LITERAL);
}
即:如果 fvOptions 顶层存在名为 options 的子字典,则所有选项都收在该子字典内;否则(多数情况)选项直接列在顶层。这让 fvOptions 既能写成"扁平的一堆子字典",也能写成 options { ... } 分组形式。
运行期重读在 fvOptionList.C 第 56--67 行:
cpp
bool Foam::fv::optionList::readOptions(const dictionary& dict)
{
checkTimeIndex_ = mesh_.time().timeIndex() + 2;
bool allOk = true;
for (fv::option& opt : *this)
{
bool ok = opt.read(dict.subDict(opt.name()));
allOk = (allOk && ok);
}
return allOk;
}
逐行解释:
- 第 58 行:把检查时刻推迟 2 个时间步(
timeIndex()+2),留出装配窗口------见 §3.3。 - 第 60--65 行:逐个选项按名字取子字典并调用其
read。fv::option::read(fvOptionIO.C第 55--64 行)只刷新active/log/coeffs_:
cpp
bool Foam::fv::option::read(const dictionary& dict)
{
dict.readIfPresent("active", active_);
log = dict.getOrDefault("log", true);
coeffs_ = dict.optionalSubDict(modelType_ + "Coeffs");
return true;
}
也就是说:运行期修改只更新 active 与 Coeffs,不会重建对象 (选区、字段名等由派生类在自身 read 里处理,如 cellSetOption::read 第 429--454 行)。
3.3 使用检查:checkApplied
cpp
void Foam::fv::optionList::checkApplied() const
{
if (mesh_.time().timeIndex() == checkTimeIndex_)
{
for (const fv::option& opt : *this)
{
opt.checkApplied();
}
}
}
(fvOptionList.C 第 70--79 行)
配合 option::checkApplied(fvOption.C 第 134--145 行):
cpp
void Foam::fv::option::checkApplied() const
{
forAll(applied_, i)
{
if (!applied_[i])
{
WarningInFunction
<< "Source " << name_ << " defined for field "
<< fieldNames_[i] << " but never used" << endl;
}
}
}
逐行解释:
- 只有在
timeIndex == checkTimeIndex_那一刻才做检查(不是每步),由source()/constrain()/correct()开头调用checkApplied()(见fvOptionListTemplates.C第 43、125、193、316 行)。 - 每个选项在真正被应用时会
setApplied(fieldi)(fvOptionI.H第 63--66 行把applied_[fieldi]=true),把对应位翻真。 - 若到检查时刻仍是
false,说明"字典里定义了该选项作用于某场,但该场从未调用过fvOptions"------打印Source xxx defined for field yyy but never used。这就是**"fvOptions 未生效"的最常见提示**(见 §28 报错清单)。
3.4 字段匹配与应用
appliesToField(fvOptionList.C 第 134--147 行)逐个选项询问 applyToField(fieldName);后者即 fieldNames_.find(fieldName)(fvOption.C 第 128--131 行),返回索引或 -1。这条"名字匹配"贯穿三个通道:只有 fieldNames_ 里含有当前场名的选项才会被施加。
4. 三个通道的模板实现总览
三个通道的入口都在 fvOptionListTemplates.C,都是"遍历列表 → 匹配场名 → 按 active 决定是否调用派生类虚函数"的同一模式。
4.1 source()------源项通道(无 rho 版本)
cpp
template<class Type>
Foam::tmp<Foam::fvMatrix<Type>> Foam::fv::optionList::source
(
GeometricField<Type, fvPatchField, volMesh>& field,
const word& fieldName,
const dimensionSet& ds
)
{
checkApplied();
auto tmtx = tmp<fvMatrix<Type>>::New(field, ds);
auto& mtx = tmtx.ref();
for (fv::option& source : *this)
{
const label fieldi = source.applyToField(fieldName);
if (fieldi != -1)
{
addProfiling(fvopt, "fvOption().", source.name());
source.setApplied(fieldi);
const bool ok = source.isActive();
if (debug)
{
if (ok) { Info<< "Apply"; }
else { Info<< "(Inactive)"; }
Info<< " source " << source.name()
<< " for field " << fieldName << endl;
}
if (ok)
{
source.addSup(mtx, fieldi);
}
}
}
return tmtx;
}
(fvOptionListTemplates.C 第 35--82 行)
逐行解释:
- 第 43 行
checkApplied():装配前做一次"未使用"检查(见 §3.3)。 - 第 45--46 行:新建一个空
fvMatrix,量纲ds。这个空矩阵就是"源项矩阵",返回后由求解器加到UEqn上。 - 第 50 行:
applyToField(fieldName)取场在fieldNames_中的索引。 - 第 52--53 行:
fieldi != -1才处理(名字匹配)。 - 第 54 行
addProfiling(...):为性能剖析器打标签,便于看哪个选项耗时。 - 第 56 行
setApplied(fieldi):无论 active 与否都标记已应用 ------避免"未使用"误报(只要求解器调用过fvOptions(U),即便该选项此刻 inactive 也不算未使用)。 - 第 58 行
isActive():决定是否真正施加。 - 第 74--77 行:
source.addSup(mtx, fieldi)------多态分发点,交给具体模型(如 SemiImplicitSource)改写矩阵。 - 第 81 行:返回这个(可能已被填过 Su/Sp 的)源项矩阵。
4.2 operator()------求解器实际调用的重载族
fvOptions(U)、fvOptions(rho,U)、fvOptions(alpha,rho,phi) 等最终都映射到上面的 source(),只是量纲与调用签名不同:
cpp
template<class Type>
Foam::tmp<Foam::fvMatrix<Type>> Foam::fv::optionList::operator()
(
GeometricField<Type, fvPatchField, volMesh>& field,
const word& fieldName
)
{
return source(field, fieldName, field.dimensions()/dimTime*dimVolume);
}
(fvOptionListTemplates.C 第 96--103 行)
逐行解释:单场调用 fvOptions(U) → 量纲 [U]/[t]·[V] = 源项在方程两侧的单位(每单元体积积分后与 fvm::ddt 一致)。
带 rho 的版本(第 117--169 行)量纲为:
cpp
const dimensionSet ds
(
rho.dimensions()*field.dimensions()/dimTime*dimVolume
);
带 alpha, rho 的相方程版本(第 184--238 行)量纲为:
cpp
const dimensionSet ds
(
alpha.dimensions()*rho.dimensions()*field.dimensions()
/dimTime*dimVolume
);
而重载族里还有若干"占位/退化"版本,用于统一求解器写法:
- 第 241--250 行
operator()(geometricOneField& alpha, geometricOneField& rho, field)→ 直接退化为无 rho 版(return this->operator()(field, field.name()))。 - 第 253--277 行
operator()(alpha, geometricOneField& rho, field)→ 现场造一个单位volScalarField one(第 261--274 行)再转调三参版。 - 第 280--289 行
operator()(geometricOneField& alpha, rho, field)→ 退化为带 rho 版。
这解释了为什么求解器里可以写 fvOptions(alpha, rho, Yi)(reactingFoam/YEqn.H 第 30 行是 fvOptions(rho, Yi);多相求解器里则是 fvOptions(alpha1, rho, U))------列表提供了一整套重载来适配不同方程形态。 另有一个易被忽视的三参重载:
cpp
template<class Type>
Foam::tmp<Foam::fvMatrix<Type>> Foam::fv::optionList::operator()
(
const volScalarField& rho,
GeometricField<Type, fvPatchField, volMesh>& field,
const word& fieldName
)
(fvOptionListTemplates.C 第 117--123 行)
reactingFoam/pEqn.H 第 36 行 fvOptions(psi, p, rho.name()) 就解析到这里:第一参传 psi(const volScalarField& psi = thermo.psi();,见 reactingFoam/createFieldRefs.H 第 1 行)充当"权重场",第二参是被作用的 p,第三参 fieldName = rho.name()。这意味着:定义在场 rho 上的源项(如 filter 教程里 massSource1 对 rho 的质量源)会经此调用进入压力方程 (reactingParcelFoam/filter/constant/fvOptions 第 56--61 行确实定义了 rho (1e-4 0))。
4.3 constrain()------约束通道
cpp
template<class Type>
void Foam::fv::optionList::constrain(fvMatrix<Type>& eqn)
{
checkApplied();
for (fv::option& source : *this)
{
const label fieldi = source.applyToField(eqn.psi().name());
if (fieldi != -1)
{
addProfiling(fvopt, "fvOption::constrain.", eqn.psi().name());
source.setApplied(fieldi);
const bool ok = source.isActive();
if (debug)
{
if (ok) { Info<< "Constrain"; }
else { Info<< "(Inactive constrain)"; }
Info<< " source " << source.name()
<< " for field " << eqn.psi().name() << endl;
}
if (ok)
{
source.constrain(eqn, fieldi);
}
}
}
}
(fvOptionListTemplates.C 第 313--350 行)
逐行解释:
- 第 320 行:用
eqn.psi().name()(矩阵对应场的名字)去匹配------fvOptions.constrain(UEqn)时psi()就是U。 - 第 346 行
source.constrain(eqn, fieldi):把约束交给派生类。具体实现见 §8 与 §18:典型是FixedValueConstraint::constrain(eqn.setValues(cells_, fieldValues_[fieldi]))。
4.4 correct()------修正通道
cpp
template<class Type, template<class> class PatchField, class GeoMesh>
void Foam::fv::optionList::correct
(
GeometricField<Type, PatchField, GeoMesh>& field
)
{
const word& fieldName = field.name();
for (fv::option& source : *this)
{
const label fieldi = source.applyToField(fieldName);
if (fieldi != -1)
{
addProfiling(fvopt, "fvOption::correct.", source.name());
source.setApplied(fieldi);
const bool ok = source.isActive();
...
if (ok)
{
source.correct(field);
}
}
}
}
(fvOptionListTemplates.C 第 353--393 行)
correct 是模板化的,PatchField 与 GeoMesh 双参数使它能同时作用于 volField 与 surfaceField------这正是 option::correct 同时声明了 volume 与 surface 两族重载(fvOption.H 第 415--423 行)的原因。求解器里 fvOptions.correct(U) / fvOptions.correct(he)(reactingFoam/EEqn.H 第 35 行)即此。
4.5 基类的空实现
fv::option 里 addSup / constrain / correct 的默认实现全为空 (fvOption.C 第 148--350 行,函数体均为 {})。只有"相方程"的一组 addSup 有实在动作:把 alpha*rho 折成一个带 rho 的调用:
cpp
void Foam::fv::option::addSup
(
const volScalarField& alpha,
const volScalarField& rho,
fvMatrix<scalar>& eqn,
const label fieldi
)
{
addSup(alpha*rho, eqn, fieldi);
}
(fvOption.C 第 233--242 行,其余四个张量版本同构,第 245--290 行)
逐行解释:派生模型若只重写了 addSup(rho, eqn, fieldi),就自动获得了 addSup(alpha, rho, eqn, fieldi) 的能力------相方程通过 alpha*rho 折成单 rho 权重,无需每个模型都实现相版本。
5. 源项如何进入 fvMatrix:fvm::Su / fvm::Sp / fvm::SuSp
fvMatrix 的离散是 Aϕ=b,其中 diag_ 存对角、source_ 存右端、upper_/lower_ 存面系数。源项 S=Su+Spϕ 的装配规则是:
- Su(显式)→ 进
source_; - Sp(隐式,且 Sp>0)→ 进
diag_; - Spϕ 若 Sp<0(会让对角变负、破坏对角占优)→ 转成显式进
source_。
5.1 fvm::Su
cpp
template<class Type>
Foam::tmp<Foam::fvMatrix<Type>>
Foam::fvm::Su
(
const dimensioned<Type>& su,
const GeometricField<Type, fvPatchField, volMesh>& fld
)
{
auto tmat = tmp<fvMatrix<Type>>::New
(
fld,
dimVol*su.dimensions()
);
auto& mat = tmat.ref();
const auto& domain = fld.mesh().V();
if (magSqr(su.value()) > VSMALL)
{
mat.source() -= domain*su.value();
}
return tmat;
}
(fvmSup.C 第 45--67 行)
逐行解释:
- 第 53--57 行:造一个量纲为
dimVol*su.dimensions()的矩阵。注意单位 :su是"单位体积源",乘dimVol后与体积积分后的场方程一致。 - 第 63 行:
mat.source() -= domain*su.value()。domain即每单元体积V。方程写成 Aϕ=b 时,右端项移到source_需变号,因此是-=。 - 第 61 行
magSqr(su.value()) > VSMALL:极小值当作零,避免无意义改动。
5.2 fvm::Sp
cpp
template<class Type>
Foam::tmp<Foam::fvMatrix<Type>>
Foam::fvm::Sp
(
const dimensionedScalar& sp,
const GeometricField<Type, fvPatchField, volMesh>& fld
)
{
auto tmat = tmp<fvMatrix<Type>>::New
(
fld,
dimVol*sp.dimensions()*fld.dimensions()
);
auto& mat = tmat.ref();
const auto& domain = fld.mesh().V();
if (mag(sp.value()) > ROOTVSMALL)
{
mat.diag() += domain*sp.value();
}
return tmat;
}
(fvmSup.C 第 134--156 行)
逐行解释:
- 第 142--146 行:源项量纲为
dimVol*sp*fld。 - 第 152 行:
mat.diag() += domain*sp.value()------这就是S_p进对角 。由于方程左端是A φ,把+S_p φ移到左端使对角增大:隐式源提高对角占优,改善收敛。 - 第 145 行量纲含
fld.dimensions(),说明Sp的单位是"每单位场量的源率",如[1/s]。
5.3 fvm::SuSp
cpp
if (susp.value() > ROOTVSMALL)
{
mat.diag() += domain*susp.value();
}
else if (susp.value() < -ROOTVSMALL)
{
mat.source() -= domain*susp.value()*fld.primitiveField();
}
(fvmSup.C 第 239--246 行)
以及场版本(第 268--270 行):
cpp
mat.diag() += domain*max(susp.field(), scalar(0));
mat.source() -= domain*min(susp.field(), scalar(0))*fld.primitiveField();
逐行解释:
- 标量版:
SuSp正 → 当Sp进对角;负 → 当Su进源项。 - 场版:用
max(x,0)与min(x,0)把逐单元的正负值分流 :正值单元进对角、负值单元进源项。这就是SemiImplicitSource用eqn += fvm::SuSp(tsp, psi)的底层(见 §13)------它自动判断符号,保证对角不为负。
机制链 A(源项进入 fvMatrix) : fvOptions(U) → operator()(field, fieldName) → source(field, fieldName, ds) → 遍历选项 → 匹配场名 → SemiImplicitSource::addSup → 组装 Su/Sp → eqn += SuValue(改 source_)/ eqn += fvm::SuSp(tsp, psi)(正进对角、负进源项)→ 返回源项矩阵 → 求解器 UEqn == fvOptions(U) 把它加到 A φ = b。
6. constrain 通道如何改写矩阵:setValues 与 setValuesFromList
约束和源项的本质区别:源项是"加一项",约束是"强行覆盖"。 覆盖由 fvMatrix::setValues → setValuesFromList 完成。
cpp
template<class Type>
void Foam::fvMatrix<Type>::setValues
(
const labelUList& cellLabels,
const Type& value
)
{
this->setValuesFromList(cellLabels, UniformList<Type>(value));
}
(fvMatrix.C 第 976--984 行;另有 UList 版第 987--995 行、UIndirectList 版第 998--1006 行。)
真正的逻辑在 setValuesFromList(fvMatrix.C 第 223--317 行):
cpp
const fvMesh& mesh = psi_.mesh();
const cellList& cells = mesh.cells();
const labelUList& own = mesh.owner();
const labelUList& nei = mesh.neighbour();
scalarField& Diag = diag();
Field<Type>& psi =
const_cast
<
GeometricField<Type, fvPatchField, volMesh>&
>(psi_).primitiveFieldRef();
// Following actions:
// - adjust local field psi
// - set local matrix to be diagonal (so adjust source)
// - cut connections to neighbours
// - make (on non-adjusted cells) contribution explicit
(fvMatrix.C 第 231--249 行)
逐行解释:
- 第 233--235 行:拿到网格单元表、
owner(面所属主单元)、neighbour(面邻接单元)。它们决定"要切断哪些面连接"。 - 第 237 行
Diag = diag():对角引用。 - 第 238--242 行:
psi是矩阵对应场的内部场引用 。约束会直接改场值 (const_cast说明这一操作突破了const,属于"求解后强制"语义)。
第一遍循环------切断被约束单元与邻居的连接:
cpp
if (symmetric() || asymmetric())
{
forAll(cellLabels, i)
{
const label celli = cellLabels[i];
const Type& value = values[i];
for (const label facei : cells[celli])
{
const label patchi = mesh.boundaryMesh().patchID(facei);
if (patchi == -1)
{
if (symmetric())
{
if (celli == own[facei])
{
source_[nei[facei]] -= upper()[facei]*value;
}
else
{
source_[own[facei]] -= upper()[facei]*value;
}
upper()[facei] = 0.0;
}
else
{
if (celli == own[facei])
{
source_[nei[facei]] -= lower()[facei]*value;
}
else
{
source_[own[facei]] -= upper()[facei]*value;
}
upper()[facei] = 0.0;
lower()[facei] = 0.0;
}
}
else
{
if (internalCoeffs_[patchi].size())
{
const label patchFacei =
mesh.boundaryMesh()[patchi].whichFace(facei);
internalCoeffs_[patchi][patchFacei] = Zero;
boundaryCoeffs_[patchi][patchFacei] = Zero;
}
}
}
}
}
(fvMatrix.C 第 251--305 行)
逐行解释(这是最容易被忽略的一段):
- 对称矩阵(
symmetric(),如纯扩散方程)只有一个上三角upper_:把该面另一侧单元source_减去upper*value,再把upper()[facei]=0------把该单元从方程网中"隔离"。 - 非对称矩阵(
asymmetric(),如含对流)同时有upper_/lower_:都清零,并相应调整对面单元的source_。 - 边界面的情况(第 292--302 行):把该 patch 的
internalCoeffs_与boundaryCoeffs_都置零,取消边界对该单元的隐式影响。 - 为什么先减
source_?因为在第二遍里会把被约束单元的对角与源项彻底重写,但邻居单元的源项仍保留着来自该面的耦合项,必须先扣掉,否则方程不守恒。
第二遍循环------重写被约束单元:
cpp
forAll(cellLabels, i)
{
const label celli = cellLabels[i];
const Type& value = values[i];
psi[celli] = value;
source_[celli] = value*Diag[celli];
}
(fvMatrix.C 第 309--316 行)
逐行解释:
- 第 314 行
psi[celli] = value:把场值直接设为约束值。这就是fixedValueConstraint一施加就"立即"把场钳住的原因。 - 第 315 行
source_[celli] = value*Diag[celli]:把该行改成Diag[celli]*φ = Diag[celli]*value,即该单元的解恒等于value,与其它单元解耦。因为对角未变,只改源项,等价于φ_cell = value。
机制链 B(约束改写矩阵行) : fvOptions.constrain(UEqn) → optionList::constrain → 匹配场名 → FixedValueConstraint::constrain(eqn, fieldi) → eqn.setValues(cells_, fieldValues_[fieldi]) → setValuesFromList → 第一遍切断邻居面连接(upper_/lower_ 清零 + 邻居 source_ 修正 + 边界系数清零)→ 第二遍设置 psi[celli]=value 与 source_[celli]=value*Diag[celli],使该单元恒等于约束值。
7. correct 通道:以 limitVelocity 为例
correct 直接改场值(不做矩阵组装),典型是限幅。
cpp
void Foam::fv::limitVelocity::correct(volVectorField& U)
{
const scalar maxSqrU = sqr(max_);
// Count nTotCells ourselves
// (maybe only applying on a subset)
label nCellsAbove(0);
const label nTotCells(returnReduce(cells_.size(), sumOp<label>()));
vectorField& Uif = U.primitiveFieldRef();
for (const label celli : cells_)
{
auto& Uval = Uif[celli];
const scalar magSqrUi = magSqr(Uval);
if (magSqrUi > maxSqrU)
{
Uval *= sqrt(maxSqrU/magSqrUi);
++nCellsAbove;
}
}
...
}
(limitVelocity.C 第 114--136 行)
逐行解释:
- 第 116 行:
maxSqrU = max_²,避免每单元开方比较。 - 第 120--121 行:统计被限单元数与总单元数(只在选区内或全场,取决于
useSubMesh())。 - 第 123--136 行:对每个选中单元,若
|U|² > max²,就按比例缩到上限 :U *= sqrt(max²/|U|²)。这是"保方向、缩模长"的限幅,不会改变流向。 - 第 143--164 行:若
!useSubMesh()(即selectionMode all),还要对边界场做同样限幅,但跳过fixesValue()的 patch(那些自有固定值的边界不该被覆盖)。 - 第 205--214 行:把统计写入
postProcessing/<name>/<time>/下的日志(继承writeFile)。
limitVelocity.H 的构造与读取(limitVelocity.C 第 64--111 行)显示它读取 max(必填),并默认 U 场名;fieldNames_ 只放一个名字(第 94 行)。所以 fvOptions.correct(U) 时匹配到 U 即在此限幅。
机制链 C(修正通道) : fvOptions.correct(U) → optionList::correct → 匹配场名 → limitVelocity::correct(U) → 遍历 cells_ → |U|²>max² 时按 sqrt(max²/|U|²) 缩放 → 统计与日志 → 边界(all 模式)同样处理。
8. cellSetOption:选区的中间基类
8.1 五种(实为七种)选择模式
cellSetOption.H 第 174--184 行定义枚举:
cpp
enum selectionModeType : char
{
smAll, //!< "all" cells
smCellSet, //!< "cellSet"
smCellZone, //!< "cellZone"
smPoints, //!< "points"
smMovingPoints, //!< "movingPoints"
smGeometric, //!< "geometric"
smCellType //!< "overset type cells"
};
名称映射见 cellSetOption.C 第 46--59 行:
cpp
const Foam::Enum
<
Foam::fv::cellSetOption::selectionModeType
>
Foam::fv::cellSetOption::selectionModeTypeNames_
({
{ selectionModeType::smAll, "all" },
{ selectionModeType::smGeometric, "geometric" },
{ selectionModeType::smPoints, "points" },
{ selectionModeType::smMovingPoints, "movingPoints" },
{ selectionModeType::smCellSet, "cellSet" },
{ selectionModeType::smCellZone, "cellZone" },
{ selectionModeType::smCellType, "cellType" }
});
逐行解释:
all:全场(等价于不选子集,useSubMesh()返回 false)。geometric:用topoSetCellSource的几何动作(box/sphere/cylinder...)直接选单元------无需预定义 cellSet。points:给定一组空间点,选包含这些点的单元(mesh.findCell)。movingPoints:点的位置随时间由Function1<vector>给出,每步重算。cellSet:按网格sets/<name>里的cellSet。cellZone:按cellZones,且支持多个 zone/zone-group。cellType:仅用于重叠网格(overset),选POROUS类型的单元。
8.2 构造与数据
cellSetOption.H 第 190--222 行列出数据:selectionMode_、updateSelection_、timeStart_、duration_、selectionNames_(wordRes,存 set/zone 名)、points_、movingPoints_、geometricSelection_、cells_(最终选中的单元标签)、V_(选中单元体积和)。
构造在 cellSetOption.C 第 353--378 行:
cpp
Foam::fv::cellSetOption::cellSetOption
(
const word& name,
const word& modelType,
const dictionary& dict,
const fvMesh& mesh
)
:
fv::option(name, modelType, dict, mesh),
selectionMode_(selectionModeTypeNames_.get("selectionMode", coeffs_)),
updateSelection_(false),
timeStart_(-1),
duration_(0),
selectionNames_(),
points_(),
movingPoints_(),
geometricSelection_(),
V_(0)
{
Info<< incrIndent;
read(dict);
setSelection(coeffs_);
setCellSelection();
setVol();
Info<< decrIndent;
}
逐行解释:
- 第 362 行:
selectionMode是必填 (get而非getOrDefault),从coeffs_读。 - 第 363--364 行:
updateSelection_默认 false,timeStart_默认 -1(表示"从头开始、无限期")。 - 第 373--376 行:依次
read(dict)→setSelection→setCellSelection→setVol。注意顺序:先解析字典里的选区描述(名字/点),再真正计算单元标签,最后算体积。
8.3 选区描述解析:setSelection
cpp
void Foam::fv::cellSetOption::setSelection(const dictionary& dict)
{
selectionNames_.clear();
switch (selectionMode_)
{
case smAll:
{
break;
}
case smGeometric:
{
geometricSelection_ = dict.subDict("selection");
break;
}
case smPoints:
{
dict.readEntry("points", points_);
break;
}
case smMovingPoints:
{
const dictionary& mpsDict = dict.subDict("movingPoints");
movingPoints_.resize_null(mpsDict.size());
label pointi = 0;
for (const entry& dEntry : mpsDict)
{
const word& key = dEntry.keyword();
movingPoints_.set
(
pointi,
Function1<point>::New
(
key,
mpsDict,
&mesh_
)
);
++pointi;
}
break;
}
case smCellSet:
{
selectionNames_.resize(1);
dict.readEntry("cellSet", selectionNames_.first());
break;
}
case smCellZone:
{
if
(
!dict.readIfPresent("cellZones", selectionNames_)
|| selectionNames_.empty()
)
{
selectionNames_.resize(1);
dict.readEntry("cellZone", selectionNames_.first());
}
break;
}
case smCellType:
{
break;
}
default:
{
FatalErrorInFunction
<< "Unknown selectionMode "
<< selectionModeTypeNames_[selectionMode_]
<< ". Valid selectionMode types : "
<< selectionModeTypeNames_
<< exit(FatalError);
}
}
}
(cellSetOption.C 第 64--142 行)
逐行解释:
smGeometric(第 74--78 行):把selection子字典整体存下,稍后交给cellBitSet::select。smPoints(第 79--83 行):读points为vectorList。smMovingPoints(第 84--108 行):movingPoints是个字典,键是点名、值是Function1<vector>;用Function1<point>::New(key, mpsDict, &mesh_)逐个构造。带&mesh_是为了支持coordinateSystem等机构。smCellSet(第 109--114 行):读单个cellSet名。smCellZone(第 115--127 行):优先读cellZones(复数,wordRes,支持多 zone/组),读不到再读单数cellZone。这是常见的报错来源:写了cellZone却用了cellZones,两者都能被接受,但语义不同。default(第 132--140 行):未知selectionMode直接FatalError,并列出合法值------见 §21 报错清单。
8.4 真正选单元:setCellSelection
cellSetOption.C 第 173--348 行是核心。逐模式:
cpp
case smAll:
{
Info<< indent << "- selecting all cells" << endl;
cells_ = identity(mesh_.nCells());
break;
}
case smGeometric:
{
Info<< indent << "- selecting cells geometrically" << endl;
bitSet selectedCells
(
// verbosity = true
cellBitSet::select(mesh_, geometricSelection_, true)
);
// From bitSet -> labels
cells_ = selectedCells.sortedToc();
break;
}
(cellSetOption.C 第 177--197 行)
逐行解释:
smAll:cells_ = identity(nCells()),即0,1,...,nCells-1全选。smGeometric:调用cellBitSet::select(mesh_, geometricSelection_, true)得到一个bitSet,再用sortedToc()转成升序标签列表 (toc= "table of contents",即被置位索引)。升序 很关键:后续UIndirectList与setValues都按升序稳定工作。
smPoints(第 198--224 行):
cpp
labelHashSet selectedCells;
for (const point& p : points_)
{
const label celli = mesh_.findCell(p);
const bool found = (celli >= 0);
if (found)
{
selectedCells.insert(celli);
}
if (!returnReduceOr(found))
{
WarningInFunction
<< "No owner cell found for point " << p << endl;
}
}
cells_ = selectedCells.sortedToc();
逐行解释:
- 每个点用
mesh.findCell(p)找宿主单元;returnReduceOr(found)在所有进程上"或"归约------若所有进程都没找到 ,才警告No owner cell found for point(并行下点可能恰好落在别的进程,属正常,故用归约避免误报)。 - 用
labelHashSet去重(多点可能落入同单元),再转升序标签。
smMovingPoints(第 225--268 行)与 points 类似,但多了时间求值与并行去重:
cpp
const scalar t = mesh_.time().timeOutputValue();
...
const point p(movingPoints_[i].value(t));
const label celli = mesh_.findCell(p);
const bool found = (celli >= 0);
// Ensure that only one processor inserts this cell
label proci = -1;
if (found)
{
proci = Pstream::myProcNo();
}
reduce(proci, maxOp<label>());
if (found && (proci == Pstream::myProcNo()))
{
selectedCells.insert(celli);
}
(cellSetOption.C 第 229--257 行)
逐行解释:
movingPoints_[i].value(t)取该点在时间t的位置。- 并行下同一点可能被多个进程
findCell命中;用reduce(maxOp)选出编号最大的进程 插入,保证全局只插一次(sortedToc后再统一)。
smCellSet(第 269--277 行):
cpp
Info<< indent
<< "- selecting cells using cellSet "
<< zoneName() << endl;
cells_ = cellSet(mesh_, zoneName()).sortedToc();
逐行解释:直接用 Foam::cellSet 读网格 sets/<name>,再转标签。zoneName() 即 selectionNames_.first()(cellSetOptionI.H 第 77--80 行)。
smCellZone(第 278--312 行):
cpp
Info<< indent
<< "- selecting cells using cellZones "
<< flatOutput(selectionNames_) << nl;
const auto& zones = mesh_.cellZones();
// Also handles groups, multiple zones etc ...
labelList zoneIDs = zones.indices(selectionNames_);
if (zoneIDs.empty())
{
FatalErrorInFunction
<< "No matching cellZones: "
<< flatOutput(selectionNames_) << nl
<< "Valid zones : "
<< flatOutput(zones.names()) << nl
<< "Valid groups: "
<< flatOutput(zones.groupNames())
<< nl
<< exit(FatalError);
}
if (zoneIDs.size() == 1)
{
cells_ = zones[zoneIDs.first()];
}
else
{
cells_ = zones.selection(zoneIDs).sortedToc();
}
逐行解释:
zones.indices(selectionNames_)把名字(可以是通配、可以是 zone-group)解析为一组 zone ID。- 解析为空 →
FatalError并列出所有合法 zone 名与 group 名 ------这是cellZone写错时最常见的报错。 - 只匹配一个 zone 时直接取该 zone 的单元列表;多个时用
selection(zoneIDs)合并再排序。
smCellType(第 313--327 行)仅用于重叠网格:
cpp
labelHashSet selectedCells;
const cellCellStencilObject& overlap = Stencil::New(mesh_);
const labelUList& cellTypes = overlap.cellTypes();
forAll(cellTypes, celli)
{
if (cellTypes[celli] == cellCellStencil::POROUS)
{
selectedCells.insert(celli);
}
cells_ = selectedCells.sortedToc();
}
逐行解释:从重叠网格模板对象 Stencil::New(mesh_) 取每个单元的 cellTypes,把类型为 POROUS 的单元选入。注意 :cells_ = selectedCells.sortedToc(); 被写在 if 内循环体里(原样如此),略冗余但不影响正确性。
8.5 选区结果校验与体积
setCellSelection 末尾(第 339--347 行):
cpp
if
(
!(smAll == selectionMode_ || smMovingPoints == selectionMode_)
&& returnReduceAnd(cells_.empty())
)
{
WarningInFunction
<< "No cells selected!" << endl;
}
逐行解释:除 all 与 movingPoints 外,若所有进程选到的单元都是空 ,警告 No cells selected!------即"作用区域选错/为空"的典型信号。
setVol(第 145--170 行):
cpp
scalar sumVol = 0;
for (const label celli : cells_)
{
sumVol += mesh_.V()[celli];
}
reduce(sumVol, sumOp<scalar>());
const scalar old(V_);
V_ = sumVol;
逐行解释:累加选中单元体积并全局求和,得到 V_(选区总体积)。V_ 被 SemiImplicitSource(volumeMode specific)、meanVelocityForce(平均速度归一化)等使用。第 160--168 行用格式化比较判断体积是否变化,变化才打印 "- selected N cell(s) with volume V"------避免每步刷屏。
9. 几何选区与 topoSet:cellBitSet::select
geometric 模式的实现(cellSetOption.C 第 191 行)落到 src/meshTools/topoSet/topoSets/cellBitSet.C 第 90--178 行:
cpp
Foam::bitSet Foam::cellBitSet::select
(
const polyMesh& mesh,
const dictionary& dict,
const bool verbosity
)
{
// Start with all cells unselected
cellBitSet result(mesh);
// Execute all actions
for (const entry& dEntry : dict)
{
if (!dEntry.isDict())
{
WarningInFunction
<< "Ignoring non-dictionary entry "
<< dEntry << endl;
continue;
}
const dictionary& dict = dEntry.dict();
const auto action = topoSetSource::combineNames.get("action", dict);
switch (action)
{
case topoSetSource::INVERT :
{
result.invert(mesh.nCells());
continue;
break;
}
case topoSetSource::IGNORE :
continue;
break;
default:
break;
}
auto source = topoSetCellSource::New
(
dict.get<word>("source"),
mesh,
dict.optionalSubDict("sourceInfo")
);
source->verbose(verbosity);
switch (action)
{
case topoSetSource::NEW : // ie, "use"
case topoSetSource::ADD :
case topoSetSource::SUBTRACT :
{
if (topoSetSource::NEW == action)
{
// "use": only use this selection (CLEAR + ADD)
result.reset();
}
source->applyToSet(action, result);
break;
}
case topoSetSource::SUBSET :
{
cellBitSet other(mesh);
source->applyToSet(topoSetSource::NEW, other);
result.subset(other);
break;
}
...
}
}
bitSet addr(std::move(result.addressing()));
return addr;
}
逐行解释:
- 第 98 行:从"全不选"的
cellBitSet起步。 - 第 101--110 行:遍历字典里的每个子字典条目;非子字典条目忽略。
- 第 113 行:每条目读
action(use/add/subtract/subset/invert/ignore)。 - 第 116--131 行:
INVERT(取反)与IGNORE(跳过)直接处理。 - 第 133--138 行:其它动作根据
source(box/sphere/cellSet/faceSet...)用topoSetCellSource::New构造一个选区源,sourceInfo是可选的补充字典。 - 第 143--156 行:
use(NEW)= 先reset()清空再applyToSet(即"只用这一条重定义");add累加;subtract扣除。 - 第 158--166 行:
subset(取交集)用临时cellBitSet other先算出"这条 source 的集合",再与原集合求交。 - 第 176--178 行:
addressing()返回bitSet的原始位寻址,构造bitSet addr返回。
cellSetOption 再把这个 bitSet 通过 sortedToc() 转成 labelList cells_(第 195 行)。这条链把"字典里的几何体描述"一路变成"单元标签列表",正是几何选区的完整机制。
10. 时序选择:timeStart / duration / updateSelection
10.1 时间窗判定:inTimeLimits
cpp
inline bool Foam::fv::cellSetOption::inTimeLimits(const scalar timeValue) const
{
return
(
(timeStart_ < 0)
||
(
(mesh_.time().value() >= timeStart_)
&& (mesh_.time().value() <= (timeStart_ + duration_))
)
);
}
(cellSetOptionI.H 第 43--54 行)
逐行解释:
timeStart_ < 0(默认 -1)→ 恒为真,表示"无起始限制、全程生效"。- 否则仅在
[timeStart, timeStart+duration]窗口内为真。注意 这里比的是mesh_.time().value()(真实物理时间),不是timeOutputValue()。
10.2 激活判定:isActive
cpp
bool Foam::fv::cellSetOption::isActive()
{
if (fv::option::isActive() && inTimeLimits(mesh_.time().value()))
{
// Update the cell set if the mesh is changing
if (mesh_.changing())
{
if (mesh_.topoChanging())
{
setCellSelection();
// Force printing of new set volume
V_ = -GREAT;
}
else if
(
selectionMode_ == smGeometric
|| selectionMode_ == smPoints
|| selectionMode_ == smCellType
|| selectionMode_ == smMovingPoints
)
{
// Geometric selection mode(s)
setCellSelection();
}
// Report new volume (if changed)
setVol();
}
else if
(
selectionMode_ == smMovingPoints
|| selectionMode_ == smGeometric
)
{
// Update the cell selection even if mesh is not moving
setCellSelection();
setVol();
}
return true;
}
return false;
}
(cellSetOption.C 第 383--426 行)
逐行解释(这是 selectionMode 判定逻辑的核心):
- 第 385 行:双重门 ------既要
active_为真(fv::option::isActive()),又要处于时间窗内。所以active false;或过了duration都会让选项"隐身"。 - 第 388--410 行:若网格在变(动网格):
topoChanging()(拓扑变化,如层铺/滑移导致单元增减)→ 重算选区并强制打印新体积(V_ = -GREAT令setVol认为体积变了)。- 否则若是几何/点/overset/动点类选区 → 也重算(因为网格位移会让"包含某点的单元"改变)。
- 第 411--420 行:网格不动,但
movingPoints/geometric类 → 每步仍需重算(因为点/几何体本身随时间动)。 - 因此:
cellSet/cellZone/all只在构造时算一次(省开销);points/geometric/movingPoints/cellType会在需要时重算。
10.3 运行期更新开关
cpp
// Do not read and set selections unless users request
updateSelection_ = coeffs_.getOrDefault("updateSelection", false);
if (updateSelection_)
{
setSelection(coeffs_);
setCellSelection();
setVol();
}
(cellSetOption.C 第 443--451 行)
逐行解释:updateSelection 默认 false。若置 true,则每次 read(改字典触发重读)都重算选区 。它对"运行期改几何参数后立即生效"有用;对静态选区则纯属浪费。而 timeStart+duration 的读取在第 436--441 行:只有写了 timeStart 才强制要求 duration (coeffs_.readEntry("duration", duration_))。
机制链 D(时序选择判定) : fvOptions(U) → optionList::source → source.isActive() → cellSetOption::isActive() → fv::option::isActive()(active 标志)且 inTimeLimits(time.value())(时间窗)→ 通过后再按 mesh_.changing()/topoChanging() 与 selectionMode_ 决定是否 setCellSelection() 重算选区 → 返回 true 才真正 addSup。
11. 区域寻址方式对比与细节
selectionMode |
必需条目 | 计算时机 | 是否每步重算 | 典型用途 |
|---|---|---|---|---|
all |
无(可省略) | 构造 | 否 | 全场源项/限幅 |
cellSet |
cellSet <word> |
构造 | 网格拓扑变时 | 预定义 sets/<name> |
cellZone |
cellZone/cellZones |
构造 | 网格拓扑变时 | 多孔/热源区域 |
points |
points ( (x y z) ... ) |
构造 | 网格动或每步(见 isActive) | 单点/多点注入 |
movingPoints |
movingPoints { ... } |
构造 | 是(每步) | 移动热源/随体探针 |
geometric |
selection { ... } |
构造 | 是(每步) | 盒子/球/圆柱,免预定义 |
cellType |
无(overset) | 构造 | 网格动时 | 重叠网格 POROUS 单元 |
要点:
zoneName()(cellSetOptionI.H第 77--80 行)返回selectionNames_.first(),是selectionNames_为空时的安全退回(wordRe::null)。cellSetOption.H第 321--323 行还把cellSetName()定义为zoneName()的别名(housekeeping)。useSubMesh()(cellSetOptionI.H第 57--60 行):selectionMode_ != smAll即为真。所有"是否作用于全场"的分支都靠它 :smAll时源项可直接eqn += SuValue(对整场),否则要构造一个只填选区的DimensionedField(见 §13)。V()(cellSetOptionI.H第 83--86 行):选区体积,被specific体积模式与meanVelocityForce使用。timeStart(val)/duration(val)(cellSetOptionI.H第 95--108 行)提供运行期修改,返回旧值;isSelectionUpdated()(cellSetOption.H第 292--295 行)返回updateSelection_。
12. semiImplicitSource:系数解析
12.1 数学定义与两种写法
SemiImplicitSource.H(第 39--48 行)给出:
scss
S(x) = S_u + S_p x
S(x) | net source for field 'x'
S_u | explicit source contribution
S_p | linearised implicit contribution
新版支持三种系数写法(SemiImplicitSource.H 第 62--104 行):
- 元组:
k (30.7 0);即(Su Sp); Function1:k { explicit table ((0 0) (5 30.7)); implicit none; };- 表达式
exprField:k { explicit { type exprField; expression "(mag(pos()) < 0.1) ? 1.5 : 0"; } }。
旧式(兼容)写作 injectionRateSuSp { k (30.7 0); } 或 { Su ...; Sp ...; }。关键在 read(SemiImplicitSource.C 第 527--569 行):
cpp
volumeMode_ = volumeModeTypeNames_.get("volumeMode", coeffs_);
// Set volume normalisation
if (volumeMode_ == vmAbsolute)
{
VDash_ = V_;
}
// Compatibility (2112 and earlier)
const dictionary* injectDict =
coeffs_.findDict("injectionRateSuSp", keyType::LITERAL);
if (injectDict)
{
setFieldCoeffs
(
*injectDict,
"Su", // Su = explicit
"Sp" // Sp = implicit
);
}
else
{
setFieldCoeffs
(
coeffs_.subDict("sources", keyType::LITERAL),
"explicit",
"implicit"
);
}
逐行解释:
- 第 534 行:
volumeMode必填 ,取值absolute或specific。 - 第 537--540 行:若
absolute,令VDash_ = V_(选区总体积)。这是量纲归一化的关键旋钮。 - 第 543--563 行:若找到
injectionRateSuSp就走旧关键字Su/Sp;否则读sources,用新关键字explicit/implicit。两套路里"显式"对应Su/explicit,均进右端;"隐式"对应Sp/implicit。
12.2 setFieldCoeffs:把字典变成 Su_/Sp_
cpp
template<class Type>
void Foam::fv::SemiImplicitSource<Type>::setFieldCoeffs
(
const dictionary& dict,
const word& keyExplicit,
const word& keyImplicit
)
{
label count = dict.size();
fieldNames_.resize_nocopy(count);
Su_.clear();
Sp_.clear();
Su_.reserve(count);
Sp_.reserve(count);
...
fv::option::resetApplied();
word modelType;
Tuple2<Type, scalar> sourceRates;
count = 0;
for (const entry& dEntry : dict)
{
const word& fieldName = dEntry.keyword();
bool ok = false;
if (dEntry.isDict())
{
const dictionary& subDict = dEntry.dict();
const entry* eptr;
if ((eptr = subDict.findEntry(keyExplicit, keyType::LITERAL)) != nullptr)
{
ok = true;
if
(
eptr->isDict()
&& eptr->dict().readEntry("type", modelType, keyType::LITERAL)
&& (modelType == "exprField")
)
{
const dictionary& exprDict = eptr->dict();
valueExprSu_.emplace_set(fieldName);
valueExprSu_[fieldName].readEntry("expression", exprDict);
driverSu_.set
(
fieldName,
new expressions::volumeExprDriver(mesh_, exprDict)
);
}
else
{
Su_.set
(
fieldName,
Function1<Type>::New(keyExplicit, subDict, &mesh_)
);
}
}
... // 同样的逻辑处理 keyImplicit -> Sp_
}
else
{
// Non-dictionary form
dEntry.readEntry(sourceRates);
ok = true;
Su_.set(fieldName, new Function1Types::Constant<Type>(keyExplicit, sourceRates.first()));
Sp_.set(fieldName, new Function1Types::Constant<scalar>(keyImplicit, sourceRates.second()));
}
if (!ok)
{
FatalIOErrorInFunction(dict)
<< "Require at least one of "
<< keyExplicit << '/' << keyImplicit << " entries for "
<< "field: " << fieldName << endl
<< exit(FatalIOError);
}
fieldNames_[count] = fieldName;
++count;
}
}
(SemiImplicitSource.C 第 52--209 行,节选核心分支)
逐行解释:
- 第 60 行:
count = dict.size(),即"要处理的场数"= 子字典条目数。 - 第 62 行:
fieldNames_.resize_nocopy(count)一次性设定场名列表。 - 第 64--77 行:清空并预留
Su_/Sp_(HashPtrTable<Function1<Type>>/<scalar>,即"场名 → 系数函数")以及表达式相关表。 - 第 79 行
resetApplied():重置"已应用"标志(fvOption.C第 48--52 行)。 - 第 88--168 行:字典形式的每个条目------先看有没有
keyExplicit(Su/explicit):- 若该条目本身是字典且
type == "exprField"→ 走表达式路径,创建volumeExprDriver与表达式字符串; - 否则用
Function1<Type>::New(keyExplicit, subDict, &mesh_)------支持常量、多项式、table、codedFunction 等所有 Function1 类型。 - 对
keyImplicit(Sp/implicit)同理,但类型是Function1<scalar>(因为 Sp 是标量系数)。
- 若该条目本身是字典且
- 第 169--195 行(
else分支):非字典形式 (如k (30.7 0);)------直接读成Tuple2<Type, scalar>,前项是Su、后项是Sp,各自包成Function1Types::Constant。这是最常见的简写。 - 第 197--204 行:若
Su与Sp都没给,抛FatalIOError:Require at least one of <keyExplicit>/<keyImplicit> entries for field: <name>------见 §21。 - 第 206--207 行:把场名写进
fieldNames_。
注意第 108、144 行的 readEntry("type", ...) 用 && 短路 :只有当 eptr->isDict() 且读到 type 且等于 exprField 时,才走表达式;否则即使写了 type 但不为 exprField,也退回 Function1::New。这意味着 exprField 必须配合 sources 字典形式(注释第 135--137 行明确说明)。
13. semiImplicitSource:addSup 与量纲推导链
13.1 量纲设定
cpp
const dimensionSet SuDims(eqn.dimensions()/dimVolume);
const dimensionSet SpDims(SuDims/psi.dimensions());
(SemiImplicitSource.C 第 265--266 行)
逐行解释:
eqn.dimensions()是方程整体的量纲(对动量方程为[kg·m/s²]之类,含体积积分)。SuDims = eqn.dimensions()/dimVolume→ 单位体积源率。SpDims = SuDims/psi.dimensions()→ Sp 的单位([1/s]量级)。二者正是 §5 里fvm::Su/fvm::Sp期望的量纲。
13.2 显式源 Su
cpp
{
const auto iter1 = valueExprSu_.cfind(fieldName);
const auto iter2 = Su_.cfind(fieldName);
tmp<DimensionedField<Type, volMesh>> tsu;
if (iter1.good())
{
... // 表达式路径:driver.parse(valueExpr) -> Field<Type>
const Field<Type>& exprFld = ptr->primitiveField();
tsu = DimensionedField<Type, volMesh>::New
(
name_ + fieldName + "Su",
IOobject::NO_REGISTER,
mesh_,
dimensioned<Type>(SuDims, Zero)
);
if (this->useSubMesh())
{
for (const label celli : cells_)
{
tsu.ref()[celli] = exprFld[celli]/VDash_;
}
}
else
{
tsu.ref().field() = exprFld;
if (!equal(VDash_, 1)) { tsu.ref().field() /= VDash_; }
}
}
else if (iter2.good() && iter2.val()->good())
{
const dimensioned<Type> SuValue
(
"Su",
SuDims,
iter2.val()->value(tmVal)/VDash_
);
if (mag(SuValue.value()) <= ROOTVSMALL)
{
// No-op
}
else if (this->useSubMesh())
{
tsu = DimensionedField<Type, volMesh>::New
(
name_ + fieldName + "Su",
IOobject::NO_REGISTER,
mesh_,
dimensioned<Type>(SuDims, Zero)
);
UIndirectList<Type>(tsu.ref(), cells_) = SuValue.value();
}
else
{
eqn += SuValue;
}
}
if (tsu.valid())
{
eqn += tsu;
}
}
(SemiImplicitSource.C 第 268--395 行,节选)
逐行解释:
- 第 269--271 行:查两张表------表达式表
valueExprSu_与函数表Su_。表达式优先。 - 表达式路径(第 334--359 行):
- 造一个量纲
SuDims、初值零的DimensionedField(第 336--342 行); useSubMesh()为真时只把选区内单元填上exprFld[celli]/VDash_(第 344--350 行),其余保持零------这就是"源项只在选区生效"的实现;smAll时整场赋值再按需除以VDash_(第 351--359 行)。
- 造一个量纲
- 函数路径(第 361--389 行):
- 第 362--367 行:把
Function1在时间tmVal的值除以VDash_,并赋予SuDims量纲。这就是volumeMode的归一化 :specific时VDash_=1,用户给的数直接是"每单位体积";absolute时VDash_=V_,用户给的是"总值",除以体积后变成"每单位体积"。 - 第 370--373 行:
|Su|极小 → 直接跳过(// No-op)。 - 第 374--384 行:选区模式------造零场,用
UIndirectList把SuValue写到cells_上(只在选区非零)。 - 第 386--388 行:
smAll模式------eqn += SuValue,直接把均匀源项加到矩阵(快路径)。
- 第 362--367 行:把
- 第 391--394 行:若构造了
tsu,统一eqn += tsu(转成矩阵后累加)。
13.3 隐式源 Sp
隐式分支与之对称,但最后一步不同:
cpp
if (iter1.good())
{
... // 表达式路径同 Su
tsp = DimensionedField<scalar, volMesh>::New
(
name_ + fieldName + "Sp", IOobject::NO_REGISTER, mesh_,
dimensioned<scalar>(SpDims, Zero)
);
if (this->useSubMesh())
{
for (const label celli : cells_)
{
tsp.ref()[celli] = exprFld[celli]/VDash_;
}
}
else { ... }
}
else if (iter2.good() && iter2.val()->good())
{
const dimensioned<scalar> SpValue
(
"Sp", SpDims, iter2.val()->value(tmVal)/VDash_
);
if (mag(SpValue.value()) <= ROOTVSMALL)
{
// No-op
}
else if (this->useSubMesh())
{
tsp = DimensionedField<scalar, volMesh>::New(...);
UIndirectList<scalar>(tsp.ref(), cells_) = SpValue.value();
}
else
{
eqn += fvm::SuSp(SpValue, psi);
}
}
if (tsp.valid())
{
eqn += fvm::SuSp(tsp, psi);
}
(SemiImplicitSource.C 第 398--523 行,节选)
逐行解释:
- 第 505 行:
smAll时eqn += fvm::SuSp(SpValue, psi)------SuSp会按符号把SpValue放进对角或源项(§5.3)。 - 第 519--522 行:选区时
eqn += fvm::SuSp(tsp, psi)------tsp是"只在选区非零的标量场",SuSp逐单元分流到对角/源项。这正是SemiImplicitSource最常见用法:Sp=0时退化为纯显式Su(进右端),Sp>0时增强对角。
机制链 E(semiImplicitSource 的 Su/Sp 线性化与量纲) : 字典 sources { k (30.7 0); } → setFieldCoeffs(非字典形式)读成 Tuple2<Type,scalar> → Su_=Constant(30.7), Sp_=Constant(0) → addSup 计算 SuDims=eqn.dimensions()/dimVolume、SpDims=SuDims/psi.dimensions() → SuValue=(Su/VDash_, SuDims) → 选区则 UIndirectList(tsu, cells_)=SuValue;全场则 eqn += SuValue → 返回的源项矩阵被加到 UEqn。volumeMode absolute 令 VDash_=V_(除体积);specific 令 VDash_=1。
14. codedSource:运行期编译的源项
codedSource 允许用户在字典里写 C++ 片段,运行期编译成 .so 并加载------兼具 fvOptions 的"免改求解器"与 coded 的"极灵活"。
14.1 钩子函数
CodedFvSource.H 第 36--62 行定义了四个钩子:
scss
codeCorrect(GeometricField<Type, fvPatchField, volMesh>& field)
codeAddSup(fvMatrix<Type>& eqn, const label fieldi)
codeAddSupRho(const volScalarField& rho, fvMatrix<Type>& eqn, const label fieldi)
codeConstrain(fvMatrix<Type>& eqn, const label fieldi)
外加常见 code 条目 codeInclude/codeOptions/codeLibs/localCode(第 68--74 行)。用户只需填其中至少一个 codeAddSup/codeAddSupRho(见 read 的强约束)。
14.2 读取与编译准备
cpp
template<class Type>
bool Foam::fv::CodedSource<Type>::read(const dictionary& dict)
{
codedBase::setCodeContext(coeffs_);
if (!fv::cellSetOption::read(dict))
{
return false;
}
coeffs_.readEntry("fields", fieldNames_);
fv::option::resetApplied();
dict.readCompat<word>("name", {{"redirectType", 1706}}, name_);
auto& ctx = codedBase::codeContext();
ctx.readEntry("codeCorrect", codeCorrect_);
const bool mandatory =
(
!ctx.findEntry("codeAddSup")
&& !ctx.findEntry("codeAddSupRho")
);
ctx.readEntry("codeAddSup", codeAddSup_, mandatory);
ctx.readEntry("codeAddSupRho", codeAddSupRho_, mandatory);
ctx.readEntry
(
coeffs_.lookupEntryCompat
(
"codeConstrain",
{{ "codeSetValue", 1812 }},
keyType::LITERAL
).keyword(),
codeConstrain_
);
return true;
}
(CodedFvSource.C 第 156--204 行)
逐行解释:
- 第 159 行:先把
coeffs_设为 coded 上下文(编译相关信息从coeffs_取)。 - 第 166 行:
fields必填------声明这个 coded 源作用于哪些场。 - 第 170 行:
name兼容旧键redirectType(1706 版起改名)。 - 第 177 行:读
codeCorrect(可选)。 - 第 182--189 行:
codeAddSup与codeAddSupRho至少填一个 ------mandatory在两者都没有时为 true,readEntry会因缺项报错。这解决了"可压/不可压写混"问题。 - 第 191--201 行:读
codeConstrain,兼容旧键codeSetValue。
14.3 编译模板与重定向
cpp
template<class Type>
void Foam::fv::CodedSource<Type>::prepare
(
dynamicCode& dynCode,
const dynamicCodeContext& context
) const
{
const word sourceType(pTraits<Type>::typeName);
dynCode.setFilterVariable("typeName", name_);
dynCode.setFilterVariable("TemplateType", sourceType);
dynCode.setFilterVariable("SourceType", sourceType + "Source");
dynCode.setFilterVariable("codeCorrect", codeCorrect_);
dynCode.setFilterVariable("codeConstrain", codeConstrain_);
if (codeAddSup_.empty())
{
dynCode.setFilterVariable("codeAddSup", "NotImplemented");
}
else
{
dynCode.setFilterVariable("codeAddSup", codeAddSup_);
}
if (codeAddSupRho_.empty())
{
dynCode.setFilterVariable("codeAddSupRho", "NotImplemented");
}
else
{
dynCode.setFilterVariable("codeAddSupRho", codeAddSupRho_);
}
dynCode.addCompileFile(codeTemplateC);
dynCode.addCopyFile(codeTemplateH);
...
dynCode.setMakeOptions
(
"EXE_INC = -g \\\n"
"-I$(LIB_SRC)/finiteVolume/lnInclude \\\n"
"-I$(LIB_SRC)/fvOptions/lnInclude \\\n"
"-I$(LIB_SRC)/meshTools/lnInclude \\\n"
"-I$(LIB_SRC)/sampling/lnInclude \\\n"
+ context.options()
+ "\n\nLIB_LIBS = \\\n"
" -lfiniteVolume \\\n"
" -lfvOptions \\\n"
" -lmeshTools \\\n"
" -lsampling \\\n"
+ context.libs()
);
}
(CodedFvSource.C 第 66--134 行,节选)
逐行解释:
- 第 76--78 行:把
typeName/TemplateType/SourceType滤变量注入模板------模板里用这些名字生成类名。 - 第 89--104 行:空钩子注入
NotImplemented,避免模板引用未定义符号;也让用户写错(可压写了不可压钩子)时有明确编译错误。 - 第 107--110 行:加入编译的 C 模板
codedFvOptionTemplate.C与复制的 H 模板(模板名见CodedFvSource.H第 181--185 行)。 - 第 119--133 行:设置 Make/options ,把
finiteVolume/fvOptions/meshTools/sampling的lnInclude与库链接进去,并追加用户codeOptions/codeLibs。这就是在字典里写 coded 源时"自动可编译"的原因,也是coded source 编译失败时报错信息里出现这些 include 路径的原因。
14.4 多态重定向
cpp
template<class Type>
Foam::fv::option& Foam::fv::CodedSource<Type>::redirectOption() const
{
if (!redirectOptionPtr_)
{
dictionary constructDict(dict_);
constructDict.set("type", name_);
constructDict.changeKeyword(modelType_ & "Coeffs", name_ & "Coeffs");
redirectOptionPtr_ = fv::option::New
(
name_,
constructDict,
mesh_
);
}
return *redirectOptionPtr_;
}
(CodedFvSource.C 第 207--224 行)
逐行解释:
- 编译出的类以选项名作为
type(第 213 行constructDict.set("type", name_)),因此再去fv::option::New就能选到刚编译的类。 - 第 214 行把
modelTypeCoeffs(如scalarCodedSourceCoeffs)改名为<name>Coeffs,与编译模板的约定一致。 - 之后
addSup/constrain/correct(第 227--288 行)都是updateLibrary(name_)(确保库已加载)后转调redirectOption().xxx(...)。coded 源因此对求解器完全透明。
15. meanVelocityForce:平均速度驱动的力平衡
meanVelocityForce 用于封闭循环域(如周期风道、封闭腔),通过施加体积力把区域平均速度维持到目标值。
15.1 构造与初始梯度
cpp
: fv::cellSetOption(sourceName, modelType, dict, mesh),
Ubar_(coeffs_.get<vector>("Ubar")),
gradP0_(0.0),
dGradP_(0.0),
flowDir_(Ubar_/mag(Ubar_)),
relaxation_(coeffs_.getOrDefault<scalar>("relaxation", 1)),
rAPtr_(nullptr)
{
coeffs_.readEntry("fields", fieldNames_);
if (fieldNames_.size() != 1)
{
FatalErrorInFunction
<< "settings are:" << fieldNames_ << exit(FatalError);
}
fv::option::resetApplied();
IFstream propsFile
(
mesh_.time().timePath()/"uniform"/(name_ + "Properties")
);
if (propsFile.good())
{
Info<< " Reading pressure gradient from file" << endl;
dictionary propsDict(propsFile);
propsDict.readEntry("gradient", gradP0_);
}
}
(meanVelocityForce.C 第 86--118 行)
逐行解释:
- 第 87 行:
Ubar必填(目标平均速度矢量)。 - 第 90 行:
flowDir_ = Ubar/|Ubar|,流向单位矢量。所有力都沿此方向。 - 第 91 行:
relaxation默认 1,控制梯度更新步长。 - 第 94--100 行:
fields必须恰好一个 (即 U),否则FatalError。 - 第 104--117 行:若
timePath/uniform/<name>Properties存在(上次运行写的),读入初始gradP0_------支持重启续算。
15.2 平均速度计算
cpp
Foam::scalar Foam::fv::meanVelocityForce::magUbarAve
(
const volVectorField& U
) const
{
scalar magUbarAve = 0.0;
const scalarField& cv = mesh_.V();
forAll(cells_, i)
{
label celli = cells_[i];
scalar volCell = cv[celli];
magUbarAve += (flowDir_ & U[celli])*volCell;
}
reduce(magUbarAve, sumOp<scalar>());
magUbarAve /= V_;
return magUbarAve;
}
(meanVelocityForce.C 第 123--143 行)
逐行解释:
- 第 132--136 行:沿流向投影每个单元速度
flowDir_·U[celli],乘体积加权求和。 - 第 138 行:跨进程归约。
- 第 140 行:除以选区体积
V_→ 体积加权平均流向速度(有正负号,表示净流向)。
15.3 力平衡松弛:correct
cpp
void Foam::fv::meanVelocityForce::correct(volVectorField& U)
{
const scalarField& rAU = rAPtr_();
scalar rAUave = 0.0;
const scalarField& cv = mesh_.V();
forAll(cells_, i)
{
label celli = cells_[i];
scalar volCell = cv[celli];
rAUave += rAU[celli]*volCell;
}
reduce(rAUave, sumOp<scalar>());
rAUave /= V_;
scalar magUbarAve = this->magUbarAve(U);
// Calculate the pressure gradient increment needed to adjust the average
// flow-rate to the desired value
dGradP_ = relaxation_*(mag(Ubar_) - magUbarAve)/rAUave;
// Apply correction to velocity field
forAll(cells_, i)
{
label celli = cells_[i];
U[celli] += flowDir_*rAU[celli]*dGradP_;
}
U.correctBoundaryConditions();
scalar gradP = gradP0_ + dGradP_;
Info<< "Pressure gradient source: uncorrected Ubar = " << magUbarAve
<< ", pressure gradient = " << gradP << endl;
writeProps(gradP);
}
(meanVelocityForce.C 第 146--187 行)
逐行解释(力平衡松弛的核心):
- 第 148 行
rAU = rAPtr_():1/A,即动量方程对角倒数(由constrain存下,见 §15.4)。它在物理上代表"单位压力梯度大概能改变多少速度"。 - 第 151--164 行:体积加权平均的
rAUave = ⟨1/A⟩。 - 第 166 行:当前平均速度
⟨U⟩。 - 第 170 行(关键公式) :
dGradP_ = relaxation_*(mag(Ubar_) - magUbarAve)/rAUave。- 分子
(目标 − 当前)是速度亏欠;除以"灵敏度"⟨1/A⟩得到为补足亏欠所需增加的压力梯度 ;再乘relaxation做欠松弛。 - 这是一次牛顿型校正 :因为
U ≈ U_old + (1/A)·Δ(∇p),反解出Δ(∇p)。
- 分子
- 第 173--177 行:把校正施加回速度场------注意它按单元加
flowDir_*rAU[celli]*dGradP_,即每个单元按其1/A权重接受一份额外速度增量。这保证了下一次线性求解前速度更接近目标。 - 第 179 行:修正边界条件。
- 第 181 行:
gradP = gradP0_ + dGradP_是累计梯度。 - 第 183--186 行:打印
Pressure gradient source: uncorrected Ubar = ..., pressure gradient = ...(即运行日志那行)。 - 第 186 行:
writeProps(gradP)把梯度写入uniform/<name>Properties(meanVelocityForce.C第 49--73 行),供重启使用。
15.4 源项与对角存 1/A
addSup(第 190--210 行):
cpp
auto tSu = volVectorField::Internal::New
(
name_ + fieldNames_[fieldi] + "Sup",
IOobject::NO_REGISTER,
mesh_,
dimensionedVector(eqn.dimensions()/dimVolume, Zero)
);
auto& Su = tSu.ref();
scalar gradP = gradP0_ + dGradP_;
UIndirectList<vector>(Su, cells_) = flowDir_*gradP;
eqn += tSu;
逐行解释:把当前累计梯度 flowDir_*gradP 作为体积力写入选区内(UIndirectList),再 eqn += tSu。这样动量方程每步都带着这个体积力,配合 correct 的增量修正形成闭环。(addSup(rho,...) 版本第 213--221 行直接转调本函数------它对可压不可压一视同仁。)
constrain(第 224--248 行):
cpp
void Foam::fv::meanVelocityForce::constrain
(
fvMatrix<vector>& eqn,
const label
)
{
if (!rAPtr_)
{
rAPtr_.reset
(
new volScalarField
(
mesh_.newIOobject(IOobject::scopedName(name_, "rA")),
1.0/eqn.A()
)
);
}
else
{
rAPtr_() = 1.0/eqn.A();
}
gradP0_ += dGradP_;
dGradP_ = 0.0;
}
逐行解释:
- 第 237/243 行:把
1/eqn.A()存到rAPtr_(首次新建,之后原地更新)。eqn.A()是对角场。注意它存在constrain而非correct里的原因:constrain在solve之前被调用,此时矩阵已组装完毕,A可用;随后correct在solve之后用它做速度修正。 - 第 246--247 行:把本步增量
dGradP_累进gradP0_并清零增量。这形成"每步一次增量校正、逐步逼近目标平均速度"的迭代。
机制链 F(meanVelocityForce 力平衡松弛) : constrain(UEqn) → 存 rA=1/A → addSup 用 flowDir*(gradP0+dGradP) 加体积力进 UEqn → solve → correct(U) → 计算 ⟨U⟩ 与 ⟨1/A⟩ → dGradP = relaxation*(|Ubar|-⟨U⟩)/⟨1/A⟩ → 按 flowDir*rA*dGradP 修正 U → 打印并写 Properties → 下一步。
16. interRegionOption:跨区域耦合
多区域算例(CHT 共轭传热等)里,"区域 A 从区域 B 得到热流"很常见。interRegionOption 封装"区域配对 + meshToMesh 映射"。
interRegionOption.H 第 92--103 行数据:master_(主/从标志)、nbrRegionName_(邻区名)、meshInterpPtr_(autoPtr<meshToMesh>)。
interRegionOption.C 第 44--94 行 setMapper:
cpp
void Foam::fv::interRegionOption::setMapper()
{
if (master_)
{
Info<< indent << "- selecting inter region mapping" << endl;
const auto& nbrMesh = mesh_.time().lookupObject<fvMesh>(nbrRegionName_);
if (mesh_.name() == nbrMesh.name())
{
FatalErrorInFunction
<< "Inter-region model selected, but local and "
<< "neighbour regions are the same: " << nl
<< " local region: " << mesh_.name() << nl
<< " secondary region: " << nbrMesh.name() << nl
<< exit(FatalError);
}
if (mesh_.bounds().overlaps(nbrMesh.bounds()))
{
meshInterpPtr_.reset
(
new meshToMesh
(
mesh_,
nbrMesh,
meshToMesh::interpolationMethodNames_.getOrDefault
(
"interpolationMethod",
coeffs_,
meshToMesh::interpolationMethod::imCellVolumeWeight
),
meshToMesh::procMapMethodNames_.getOrDefault
(
"procMapMethod",
coeffs_,
meshToMesh::procMapMethod::pmAABB
),
false // not interpolating patches
)
);
}
else
{
FatalErrorInFunction
<< "regions " << mesh_.name() << " and "
<< nbrMesh.name() << " do not intersect"
<< exit(FatalError);
}
}
}
逐行解释:
- 第 46 行:只有
master_区域负责建立映射(从区域复用,避免重复构造)。 - 第 50 行:从时间数据库按
nbrRegion名取邻区网格------邻区必须已存在于同一Time中(多区域求解器会同时持有全部区域网格)。 - 第 52--60 行:若本地与邻区同名,报错
local and neighbour regions are the same(常见配置错误)。 - 第 62--85 行:若两区域包围盒相交,构造
meshToMesh;插值方法默认imCellVolumeWeight(单元体积权重),进程映射默认pmAABB。都可以在coeffs_里覆盖。 - 第 86--92 行:包盒不相交则
regions ... do not intersect报错。
构造与读取见第 99--135 行:master 默认 true、nbrRegion 必填;active() 为真时才 setMapper()。派生类(如 interRegionHeatTransfer)在此基类上叠加"场映射 + 源项"。nbrRegionName()/meshInterp() 访问器(interRegionOption.H 第 145--148 行)供派生模型取用映射结果。
17. constraints:把场"钳"住
17.1 FixedValueConstraint<Type>
FixedValueConstraint.H 第 116--162 行声明;数据只有一个:
cpp
// Private Data
//- Field values
List<Type> fieldValues_;
(FixedValueConstraint.H 第 121--124 行)
逐行解释:fieldValues_ 与 fieldNames_ 一一对应------第 i 个场在选区内被钳到 fieldValues_[i]。
读取(FixedValueConstraint.C 第 53--80 行):
cpp
if (fv::cellSetOption::read(dict))
{
const dictionary& fieldValuesDict = coeffs_.subDict("fieldValues");
label count = fieldValuesDict.size();
fieldNames_.resize(count);
fieldValues_.resize(count);
fv::option::resetApplied();
count = 0;
for (const entry& dEntry : fieldValuesDict)
{
fieldNames_[count] = dEntry.keyword();
dEntry.readEntry(fieldValues_[count]);
++count;
}
return true;
}
逐行解释:
- 第 58 行:
fieldValues是必填 子字典(subDict在缺省时会报错)。 - 第 60--63 行:场数与值列表按子字典条目数分配。
- 第 68--73 行:子字典的键即场名 ,值是约束值 。因此
fieldValues { k 1; epsilon 150; }表示把k钳到 1、epsilon钳到 150。
约束施加(第 83--96 行):
cpp
template<class Type>
void Foam::fv::FixedValueConstraint<Type>::constrain
(
fvMatrix<Type>& eqn,
const label fieldi
)
{
DebugInfo
<< "FixedValueConstraint<"
<< pTraits<Type>::typeName
<< ">::constrain for source " << name_ << endl;
eqn.setValues(cells_, fieldValues_[fieldi]);
}
逐行解释:全部逻辑压缩成一句 eqn.setValues(cells_, fieldValues_[fieldi]) ------cells_ 是选中的单元标签,fieldValues_[fieldi] 是目标值。§6 已详析 setValuesFromList 如何切断面连接并重写对角/源项。
注意张量特化 :fixedValueConstraints.C 第 33--37 行用 makeFvOption(FixedValueConstraint, scalar/vector/sphericalTensor/symmTensor/tensor) 注册五个类型,于是字典里写 scalarFixedValueConstraint(钳标量如 k/epsilon/T)、vectorFixedValueConstraint(钳矢量如 U)。张量类型必须与场的类型匹配,否则 RTS 选到的类无法处理该场------这是"约束与求解器冲突"的根源之一。
17.2 fixedTemperatureConstraint
它是能量-温度 专用约束:在选区内把温度固定。fixedTemperatureConstraint.H 第 124--149 行数据:mode_(tmUniform/tmLookup)、Tuniform_(Function1<scalar>)、TName_(温度场名,默认 "T")。
构造(fixedTemperatureConstraint.C 第 64--107 行):
cpp
fv::cellSetOption(name, modelType, dict, mesh),
mode_(temperatureModeNames_.get("mode", coeffs_)),
Tuniform_(nullptr),
TName_("T")
{
switch (mode_)
{
case tmUniform:
{
Tuniform_.reset
(
Function1<scalar>::New("temperature", coeffs_, &mesh_)
);
break;
}
case tmLookup:
{
TName_ = coeffs_.getOrDefault<word>("T", "T");
break;
}
}
// Set the field name to that of the energy
// field from which the temperature is obtained
const auto& thermo = mesh_.lookupObject<basicThermo>(basicThermo::dictName);
fieldNames_.resize(1, thermo.he().name());
fv::option::resetApplied();
}
逐行解释(这是理解"约束挂在哪个方程"的关键):
- 第 73 行:
mode必填,uniform(用Function1给时间变化的定温)或lookup(从已有场T取温度)。 - 第 79--84 行:
uniform模式读temperature为Function1<scalar>(可写temperature constant 500;)。 - 第 88--90 行:
lookup模式读温度场名。 - 第 100--104 行:
fieldNames_被设成thermo.he().name(),即能量场名(he或e) ------因为能量方程的解变量是焓/内能,而温度是它的函数。用户表面上写"固定温度",实际被约束的是能量方程。
约束施加(第 112--144 行):
cpp
void Foam::fv::fixedTemperatureConstraint::constrain
(
fvMatrix<scalar>& eqn,
const label
)
{
const auto& thermo = mesh_.lookupObject<basicThermo>(basicThermo::dictName);
switch (mode_)
{
case tmUniform:
{
const scalar t = mesh_.time().value();
scalarField Tuni(cells_.size(), Tuniform_->value(t));
eqn.setValues(cells_, thermo.he(thermo.p(), Tuni, cells_));
break;
}
case tmLookup:
{
const auto& T = mesh().lookupObject<volScalarField>(TName_);
scalarField Tlkp(T, cells_);
eqn.setValues(cells_, thermo.he(thermo.p(), Tlkp, cells_));
break;
}
default: { }
}
}
逐行解释:
tmUniform:把标量Tuniform_->value(t)铺成与cells_等长的场,再用热力学关系thermo.he(p, T, cells)反算焓 ,最后setValues把能量方程钳到该焓值。这就是"约束温度必须经过 thermo 关系换算"的原因 ------直接钳he与钳T不是一回事。tmLookup:从T场按下标cells_取值,再经 thermo 换算。- 校验:
thermo.he(thermo.p(), T, cells_)需要thermo已注册------因此该约束只能在含热物性的可压/传热求解器里使用 (用到basicThermo),在纯不可压等温求解器里会因lookupObject失败而报错。
17.3 velocityDampingConstraint
它不是"硬钳",而是阻尼 :对超过上限的单元增大对角 (隐式阻尼),而不是直接设定值。velocityDampingConstraint.C 第 54--108 行:
cpp
void Foam::fv::velocityDampingConstraint::addDamping(fvMatrix<vector>& eqn)
{
// Note: we want to add deltaU/deltaT where deltaT is a local time scale:
// U/(cbrt of volume)
// Since directly manipulating the diagonal we multiply by volume.
const scalarField& vol = mesh_.V();
const volVectorField& U = eqn.psi();
scalarField& diag = eqn.diag();
label nDamped(0);
const label nTotCells(returnReduce(cells_.size(), sumOp<label>()));
for (const label celli : cells_)
{
const scalar magU = mag(U[celli]);
if (magU > UMax_)
{
const scalar scale = sqr(Foam::cbrt(vol[celli]));
diag[celli] += C_*scale*(magU-UMax_);
++nDamped;
}
}
...
}
逐行解释(与 FixedValueConstraint 的对比是本节重点):
- 第 76 行
scale = sqr(cbrt(vol)) = vol^(2/3):这是"单元长度尺度"的平方,代表局部时间尺度ΔT ~ L/U的倒数关系。注释解释:想加ΔU/ΔT,由于直接改对角要乘体积,最终写成C_·V^(2/3)·(magU−UMax_)。 - 第 78 行
diag[celli] += C_*scale*(magU-UMax_):只把超限部分作为额外"阻力"加到对角 ,超得越多加得越狠。C_默认 1(第 137 行),可读若C。 - 第 151 行
constrain(eqn, fieldi)直接调用addDamping(eqn)------因此它通过fvOptions.constrain(UEqn)生效,不修改场值、只增强对角 ,因此不会像fixedValueConstraint那样引入硬解耦。 - 读取(第 161--192 行):
UMax必填、C可选、场名U(或UNames列表)。
17.4 MapFieldConstraint<Type>
mapFieldConstraint 用另一个网格上的场 映射过来做约束(可跨区域/跨网格)。MapFieldConstraint.C 第 59--94 行 setSourceMesh 负责确定源网格:先在时间数据库找 polyMesh::defaultRegion 命名的 fvMesh,找不到就从磁盘加载并缓存 (第 78--92 行)。第 97--132 行 createInterpolation 根据 consistent_ 决定构造哪种 meshToMesh(一致映射或带 patchMap_/cuttingPatches_ 的不一致映射)。这类约束把"外部场值"引入当前区域,是多区域耦合的字典化手段。
17.5 约束家族对比
| 约束类型 | 头文件 | 作用对象 | 施加方式 | 是否需 thermo |
|---|---|---|---|---|
scalar/vectorFixedValueConstraint |
fixedValueConstraint/ |
任意场(按类型) | setValues 硬钳 |
否 |
fixedTemperatureConstraint |
fixedTemperatureConstraint/ |
能量场(he/e) |
setValues,值经 T→he 换算 |
是 |
velocityDampingConstraint |
velocityDampingConstraint/ |
U |
增大 diag(隐式阻尼) |
否 |
MapFieldConstraint<Type> |
mapFieldConstraint/ |
任意场 | 由邻网格映射取值后钳制 | 否 |
18. corrections:限幅修正
修正类在 fvOptions.correct(field) 阶段对场值后处理,不碰矩阵。三个内置模型:
| 修正类型 | 文件 | 作用 | 关键参数 |
|---|---|---|---|
limitVelocity |
corrections/limitVelocity/ |
U 模长上限 |
max(必填)、U(默认 U) |
limitTemperature |
corrections/limitTemperature/ |
T 上下限 |
min、max(均必填)、phase |
limitTurbulenceViscosity |
corrections/limitTurbulenceViscosity/ |
nut 上限 c·ν |
c(默认 1e5)、nut |
limitTemperature.H 第 107--167 行显示它同时继承 fv::cellSetOption 与 functionObjects::writeFile(便于写日志),数据是 Tmin_/Tmax_/phase_(第 115--123 行),correct(volScalarField& he) 在能量场上做限幅。limitTurbulenceViscosity.H 第 34--38 行给出其数学: νt,max=cν,默认 c=1e5------这是防止湍流粘度爆炸的常用手段。
limitVelocity 已详析(§7)。它们都遵循同一模式:读参数 → correct 阶段遍历 cells_(或全场)逐单元钳制 → 统计并写 postProcessing/ 日志。注意与约束的区别:修正改的是场、在 solve 之后;约束改的是矩阵、在 solve 之前。
19. 求解器如何接入 fvOptions
19.1 创建
src/finiteVolume/cfdTools/general/include/createFvOptions.H(第 1--6 行全文):
cpp
fv::options& fvOptions(fv::options::New(mesh));
if (!fvOptions.optionList::size())
{
Info << "No finite volume options present" << endl;
}
逐行解释:
- 第 1 行:
fv::options::New(mesh)从constant/fvOptions(或system/fvOptions)构造并注册,得到引用fvOptions。 - 第 3--6 行:若列表为空,打印
No finite volume options present------所以算例没定义fvOptions时求解器也能照常跑。 - 求解器只需
#include "fvOptions.H"(如pimpleFoam.C第 83 行)再#include "createFvOptions.H"(reactingFoam/createFields.H第 96 行)。
19.2 三个调用点的真实上下文
不可压缩(pimpleFoam/UEqn.H 第 5--23 行):
cpp
tmp<fvVectorMatrix> tUEqn
(
fvm::ddt(U) + fvm::div(phi, U)
+ MRF.DDt(U)
+ turbulence->divDevReff(U)
==
fvOptions(U)
);
fvVectorMatrix& UEqn = tUEqn.ref();
UEqn.relax();
fvOptions.constrain(UEqn);
if (pimple.momentumPredictor())
{
solve(UEqn == -fvc::grad(p));
fvOptions.correct(U);
}
逐行解释:fvOptions(U) 在第 11 行作为源项;第 17 行 constrain 在 relax 之后、solve 之前;第 23 行 correct(U) 在 solve 之后。顺序至关重要:约束必须在 solve 前(要改矩阵),修正必须在 solve 后(要改解)。 pisoFoam/UEqn.H(第 5--22 行)与 simpleFoam/UEqn.H(第 5--23 行)写法完全一致(piso 用 piso.momentumPredictor(),simple 用 simple.momentumPredictor())。
可压缩/燃烧(reactingFoam):
UEqn.H第 11 行fvOptions(rho, U)→ 切到带 rho 的量纲版;EEqn.H第 21 行fvOptions(rho, he)、第 31 行fvOptions.constrain(EEqn)、第 35 行fvOptions.correct(he);YEqn.H第 30/35/39 行对组分Yi同样三步;pEqn.H第 36/72 行fvOptions(psi, p, rho.name())把"定义在rho上的源项"引入压力方程(见 §4.2)。
chtMultiRegionFoam/fluid/UEqn.H 第 11/16/34 行同样是 fvOptions(rho, U) / constrain / correct------多区域求解器的每个流体区域各持一份 fvOptions (因为 fv::options::New(mesh) 按网格注册,不同区域网格各自一份)。
19.3 一张"调用点---模型---方程"映射表
| 求解器调用 | 匹配的场 | 触发的模型方法 | 何时调用 |
|---|---|---|---|
fvOptions(U) |
U |
SemiImplicitSource<U>::addSup 等 |
组装方程时 |
fvOptions(rho, U) |
U(带密度权重) |
同上,带 rho 重载 | 组装方程时 |
fvOptions(rho, he) |
he/e |
能量源 addSup |
组装方程时 |
fvOptions(rho, Yi) |
Yi |
组分源 addSup |
组装方程时 |
fvOptions(constrain, UEqn) |
eqn.psi().name() |
FixedValueConstraint::constrain 等 |
relax 后、solve 前 |
fvOptions.correct(U) |
U |
limitVelocity::correct 等 |
solve 后 |
20. 可直接粘贴的字典模板集
以下模板均以本仓库真实教程/注解为蓝本(文件路径已标注),可直接粘贴到 constant/fvOptions 或 system/fvOptions,按需增删。
20.1 模板 1:semiImplicitSource(标量源 + 时间窗 + 体源)
cpp
massSource1
{
type scalarSemiImplicitSource;
timeStart 0.2;
duration 2.0;
volumeMode absolute;
selectionMode points;
points
(
(2.75 0.5 0)
);
sources
{
rho (1e-4 0); // kg/s
H2O (1e-4 0); // kg/s
}
}
(源自 tutorials/lagrangian/reactingParcelFoam/filter/constant/fvOptions 第 41--61 行)
参数逐项说明:
| 条目 | 含义 | 约束 |
|---|---|---|
type scalarSemiImplicitSource |
标量半隐式源(作用于标量场) | 必填 |
timeStart / duration |
只在 [0.2, 2.2] 生效 |
二者同时出现 |
volumeMode absolute |
值是总量(除以选区体积归一化) | 必填(absolute/specific) |
selectionMode points |
选包含给定点的单元 | 必填 |
sources { rho (1e-4 0); } |
对 rho 场:Su=1e-4、Sp=0(纯显式) |
Su/Sp 至少一个 |
H2O (...) |
对组分场 H2O 加质量源 |
同上 |
调参要点:(Su Sp) 中 Sp>0 会同时增强对角(隐式),可提高稳定性;Sp=0 为纯显式,可能受步长限制;volumeMode 从 absolute 改 specific 时数值应除以原体积。
20.2 模板 2:meanVelocityForce(封闭域平均速度驱动)
cpp
momentumSource
{
type meanVelocityForce;
selectionMode cellZone;
cellZone inletCellZone;
fields (U);
Ubar (1 0 0);
relaxation 1.0;
}
(源自 tutorials/incompressible/pimpleFoam/LES/periodicHill/steadyState/system/fvOptions 第 17--26 行;relaxation 来自 meanVelocityForce.H 第 60 行)
参数逐项说明:
| 条目 | 含义 | 约束 |
|---|---|---|
selectionMode cellZone |
按 cellZone 选作用区 | 必填 |
cellZone inletCellZone |
目标区域名 | 区域必须存在 |
fields (U) |
源作用于 U(必须恰好 1 个) | 必填 |
Ubar (1 0 0) |
目标平均速度矢量(决定流向) | 必填 |
relaxation 1.0 |
梯度更新欠松弛因子 | 可选,默认 1 |
调参要点:relaxation<1 更稳但收敛慢;目标区应覆盖完整流道截面,否则平均速度定义失真。它还会在 uniform/<name>Properties 写梯度,删除该文件即可重置。
20.3 模板 3:scalarCodedSource(运行期编译的源项)
cpp
energySource
{
type scalarCodedSource;
scalarCodedSourceCoeffs
{
selectionMode all;
fields (h);
name sourceTime;
codeInclude
#{
#};
codeAddSup
#{
const Time& time = mesh().time();
const scalarField& V = mesh_.V();
scalarField& heSource = eqn.source();
heSource -= 0.1*sqr(time.value())*V;
#};
codeConstrain
#{
Pout<< "**codeConstrain**" << endl;
#};
}
}
(源自 CodedFvSource.H 第 78--112 行的注解示例)
参数逐行说明:type scalarCodedSource 触发 CodedSource<scalar>;fields (h) 声明作用场;codeAddSup 里 eqn.source() 取源项(注意 OpenFOAM 的符号约定:source() 在方程右端,加源往往写 -= );V 是单元体积,保证与体积积分一致;codeCorrect/codeConstrain 可选。至少给 codeAddSup 或 codeAddSupRho 之一 (§14.2)。调参要点:编译产物缓存于 dynamicCode/,删掉可强制重编;codeInclude/codeLibs 用于引入额外头文件与库。
20.4 模板 4:fixedValueConstraint(多孔区固定湍流量)
cpp
porosityTurbulence
{
type scalarFixedValueConstraint;
selectionMode cellZone;
cellZone porosity;
fieldValues
{
k 1;
epsilon 150;
}
}
(源自 tutorials/compressible/rhoSimpleFoam/angledDuctExplicitFixedCoeff/constant/fvOptions 第 57--68 行)
参数说明:type scalarFixedValueConstraint 选 FixedValueConstraint<scalar>;fieldValues { k 1; epsilon 150; } 表示把选区内 k 钳到 1、epsilon 钳到 150。关键 :fieldValues 的键必须与求解器里被 fvOptions.constrain 的场名一致(k/epsilon 由湍流模型方程调用)。遗漏某个场不会报错,但该场不会被约束。
20.5 模板 5:fixedTemperatureConstraint(固定温度区)
cpp
fixedTemperature
{
type fixedTemperatureConstraint;
selectionMode cellZone;
cellZone porosity;
mode uniform;
temperature 350;
}
(源自 tutorials/compressible/rhoSimpleFoam/angledDuctExplicitFixedCoeff/constant/fvOptions 第 46--54 行)
参数说明:mode uniform 用常量/Function1 给温度(此处 350 K);也可 mode lookup; T <Tname>; 从场取。注意它约束的是能量场 he,温度值经 thermo.he(p,T,cells) 反算 (§17.2)。要求求解器含 basicThermo。
20.6 模板 6:explicitPorositySource(Darcy-Forchheimer 多孔)
cpp
porosity1
{
type explicitPorositySource;
explicitPorositySourceCoeffs
{
selectionMode cellZone;
cellZone porousBlockage;
type DarcyForchheimer;
D 1000;
d ($D $D $D);
f (0 0 0);
coordinateSystem
{
origin (0 0 0);
rotation none;
}
}
}
(源自 tutorials/incompressible/pisoFoam/laminar/porousBlockage/constant/fvOptions 第 17--43 行)
参数说明:外层 type explicitPorositySource;内层 type DarcyForchheimer(多孔模型,由 porosityModel RTS 选);d 是线性阻力张量、f 是惯性(Forchheimer)系数张量;coordinateSystem 定义各向异性方向。多孔区必须是 cellZone (explicitPorositySource.H 第 107--108 行注解)。调参:d 越大阻力越强;f=(0 0 0) 退化为纯 Darcy。
20.7 模板 7:heatExchangerSource(用效能表换热)
cpp
heatExchanger
{
type heatExchangerSource;
model effectivenessTable;
faceZone exchangerInlet;
U U;
T T;
phi phi;
effectivenessTableCoeffs
{
values ((0 1) (0.5 0.8) (1 0.5));
}
}
(骨架源自 heatExchangerSource.H 第 52--74 行;model 取值 effectivenessTable/referenceTemperature 见第 88--92 行)
参数说明:model 选换热模型;faceZone 是换热器进口面区;U/T/phi 可选场名(默认同名)。effectivenessTable 模型用一张"换热量-流量"效能表插值。注意:该模型是可压 源(addSup 只实现了带 rho 版本,heatExchangerSource.H 第 165--182 行),叠加在非可压求解器上会 NotImplemented。
21. 常见报错 / 警告清单(20+ 条)
下表按"真实报错文本样式 → 成因 → 排查 → 解决"组织。文本样式取自源码中 FatalIOErrorInLookup/FatalErrorInFunction/WarningInFunction 的实际输出格式。
| # | 报错/警告样式 | 成因 | 排查 | 解决 |
|---|---|---|---|---|
| 1 | Unknown fvOption type <type>. Valid ... types: |
type 拼错或库未加载 |
比对错误信息列出的合法类型表 | 改正 type;若来自外部库加 libs ("libX.so"); |
| 2 | Selecting finite volume options type <type> 后无后续 |
选中类构造失败 | 看下一行报错 | 定位具体模型参数 |
| 3 | Wrong or no fvOptions file / 无报错但不生效 |
字典不在 constant/ 或 system/ |
确认文件路径与文件名 fvOptions |
放到 constant/fvOptions |
| 4 | No finite volume options present |
列表为空(正常信息) | 确认确实没定义 | 无需处理;定义即消失 |
| 5 | Source <name> defined for field <f> but never used |
场名与被求解器调用的场不符 | 查该场是否被 fvOptions(...) 调用 |
改场名或用正确的求解器调用 |
| 6 | No cells selected! |
选区为空(cellZone/几何没选到单元) | 检查 zone 名/几何范围 | 修正 cellZone/selection |
| 7 | No matching cellZones: <names> + Valid zones: |
cellZone 名不存在 |
看列出的 Valid zones |
用存在的 zone 名 |
| 8 | No owner cell found for point <p> |
points 模式点在域外 |
核对点坐标 | 移动点或改坐标系 |
| 9 | Unknown selectionMode <name>. Valid selectionMode types : |
selectionMode 拼错 |
看合法值列表 | 用 all/cellZone/cellSet/points/movingPoints/geometric |
| 10 | Require at least one of explicit/implicit entries for field: <f> |
sources 里该场没给 Su/Sp |
检查该条目 | 补 (Su Sp) 或 explicit/implicit |
| 11 | Require at least one of Su/Sp entries for field: <f>(旧式) |
injectionRateSuSp 缺项 |
同上 | 同上 |
| 12 | ... fvMatrix dimensions ... 维度不一致 |
Su/Sp 单位与方程不符 |
用 [0 -2 -1 0 0 0 0] 等显式量纲 |
补齐量纲或改 volumeMode |
| 13 | SemiImplicitSource 报表达式中 evaluated to <N> instead of <nCells> values |
exprField 返回大小与网格不符 |
检查表达式 | 用逐单元表达式或改为 Function1 |
| 14 | Expression for Su <f> evaluated to <type> but expected <Type> |
表达式类型与场类型不符 | 检查表达式返回类型 | 改表达式类型 |
| 15 | settings are: ( ... )(meanVelocityForce) |
fields 不是恰好 1 个 |
检查 fields |
只留 U |
| 16 | Inter-region model selected, but local and neighbour regions are the same |
nbrRegion 与当前区同名 |
检查多区域配置 | 改 nbrRegion |
| 17 | regions <a> and <b> do not intersect |
两区包盒不相交 | 检查区域几何/位置 | 修正网格或区域配对 |
| 18 | 别处报 keyword <x> is undefined in dictionary |
必填项缺失(如 selectionMode) |
按报错键名查 | 补该键 |
| 19 | coded 源编译报错含 -I$(LIB_SRC)/fvOptions/lnInclude 等 |
coded 片段语法/类型错误 | 看编译器第一条 error | 修正片段;清 dynamicCode/ 重编 |
| 20 | NotImplemented 出现在运行日志/编译 |
可压模型用在不合适的方程(或反之) | 看是 addSupRho 还是 addSup |
用匹配的 codeAddSup/Rho 或改求解器 |
| 21 | Constraint <name> ... setValues 后场仍不固定 |
约束施加在 solve 之后 |
检查 fvOptions.constrain 位置 |
确保在 solve 前调用 |
| 22 | 修改 fvOptions 无效 |
fvOptions 已在内存、未触发重读 |
检查是否 READ_MODIFIED |
改字典后触发重读;或重启 |
| 23 | unused option ... dimensionSet 维度警告 |
源项物理单位与场不符 | 用调试打印量纲 | 修正数值/量纲 |
| 24 | Unknown action <a>(几何选区) |
selection 里 action 写了非法值 |
检查 use/add/subtract/subset |
用合法动作 |
| 25 | Ignoring non-dictionary entry <e> |
selection 里混入了非子字典条目 |
检查 selection |
每条写成 name { action ...; source ...; } |
补充排查技巧:
- "fvOptions 未生效"三连查 :① 文件路径(
constant/vssystem/);②type是否拼对(看 "Selecting ... type" 行);③ 场名是否匹配(看 "Apply source ... for field ..." 调试行;开启debug或看checkApplied警告)。 - "约束与求解器冲突" :
fixedValueConstraint的场名若与求解器调用的场名不一致(如求解器对Yi调fvOptions.constrain,但约束写的是Y),约束静默失效;fixedTemperatureConstraint需basicThermo,在等温不可压求解器中会lookupObject失败。 - 源项量纲错误 :先用
volumeMode specific、给Sp=0的显式值,观察是否发散;再用infoSwitches的量纲检查(dimensionSet::checking)定位。
22. 对比与选型表
22.1 source 类型对比
| 类型 | 空间 | 是否隐式 | 典型用途 | 关键参数 |
|---|---|---|---|---|
scalar/vector...SemiImplicitSource |
cellSet 选区 | 可(Sp) |
通用标量/矢量源 | volumeMode、sources (Su Sp) |
scalar/vector...CodedSource |
cellSet 选区 | 取决于代码 | 自定义任意源 | codeAddSup 等 |
explicitPorositySource |
cellZone | 显式阻力 | 多孔介质 | DarcyForchheimer 的 d/f |
meanVelocityForce |
cellZone | 体积力+校正 | 封闭域驱动 | Ubar、relaxation |
heatExchangerSource |
cellZone+faceZone | 显式 | 换热器(可压) | model、faceZone |
buoyancyForce/buoyancyEnergy |
cellZone | 显式 | 浮力(动量/能量) | 重力、ρ |
interRegionHeatTransfer |
区域间 | 显式 | CHT 耦合 | nbrRegion、传热模型 |
22.2 隐式 vs 显式源项
| 维度 | 显式(Su 进右端) |
隐式(Sp 进对角) |
|---|---|---|
| 矩阵位置 | source_ |
diag_ |
| 稳定性 | 受步长限制,可能振荡 | 增强对角占优,稳定 |
| 精度 | 一步滞后 | 与解同步 |
| 符号 | 任意 | 仅正值安全(负值被 SuSp 转显式) |
| 物理 | 外源(加热、注质) | 汇/阻力(抽吸、阻尼) |
22.3 区域指定方式对比
| 方式 | 定义位置 | 灵活性 | 是否需要预建 | 常用求解器 |
|---|---|---|---|---|
all |
字典 | 最低 | 否 | 全场限幅 |
cellZone |
constant/polyMesh/cellZones |
中 | 是(setFields/topoSet) |
多孔、热源 |
cellSet |
constant/polyMesh/sets/<name> |
中 | 是 | 预定义区域 |
points |
字典 | 高(点级) | 否 | 点注入 |
geometric |
字典(selection) |
最高(box/sphere/...) | 否 | 临时区域 |
movingPoints |
字典(Function1) |
最高(时空) | 否 | 移动源 |
22.4 fvOptions vs coded vs functionObject
| 维度 | fvOptions |
coded(fvOptions 的 coded 源) |
functionObject |
|---|---|---|---|
| 是否改方程 | 是(源/约束/修正进矩阵) | 是 | 否(只观测/后处理) |
| 是否需要编译 | 否 | 是(运行期) | 否 |
| 灵活性 | 内置模型 | 任意 C++ | 任意但限于场操作 |
| 典型 | 多孔/热源/约束 | 特殊源项 | 场统计、场映射 |
| 求解器耦合 | fvOptions(U) 三调用点 |
同 | 独立于方程 |
22.5 fv::option 派生基类对比
| 基类 | 职责 | 派生示例 | 作用于区域 |
|---|---|---|---|
fv::option |
字典/RTS/空接口 | --- | 无(自定义) |
cellSetOption |
选区 + 时序 | SemiImplicitSource、FixedValueConstraint | cellZone/cellSet/points/geometric |
interRegionOption |
区域配对 + 映射 | interRegionHeatTransfer | 区域间 |
23. 一个完整的 fvOptions 算例
把本章所有要素拼成一个可运行算例(多孔 + 固定湍流量 + 固定温度 + 平均速度驱动 + 速度限幅),写入 constant/fvOptions:
cpp
/*--------------------------------*- C++ -*----------------------------------*\
| ========= | |
| \\ / F ield | OpenFOAM: The Open Source CFD Toolbox |
| \\ / O peration | Version: v2606 |
| \\ / A nd | Website: www.openfoam.com |
| \\/ M anipulation | |
\*---------------------------------------------------------------------------*/
FoamFile
{
version 2.0;
format ascii;
class dictionary;
object fvOptions;
}
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
// 1) 多孔区阻力(源项)
porosity
{
type explicitPorositySource;
explicitPorositySourceCoeffs
{
selectionMode cellZone;
cellZone porousZone;
type DarcyForchheimer;
d (5e7 -1000 -1000);
f (0 0 0);
coordinateSystem
{
origin (0 0 0);
e1 (1 0 0);
e2 (0 1 0);
}
}
}
// 2) 多孔区固定湍流量(约束)
porosityTurbulence
{
type scalarFixedValueConstraint;
selectionMode cellZone;
cellZone porousZone;
fieldValues
{
k 1;
epsilon 150;
}
}
// 3) 固定温度区(约束,仅可压/传热求解器)
fixedTemperature
{
type fixedTemperatureConstraint;
selectionMode cellZone;
cellZone porousZone;
mode uniform;
temperature 350;
}
// 4) 封闭域平均速度驱动(源项+校正)
momentumSource
{
type meanVelocityForce;
selectionMode cellZone;
cellZone inletCellZone;
fields (U);
Ubar (1 0 0);
relaxation 1.0;
}
// 5) 速度限幅(修正)
limitU
{
type limitVelocity;
max 100;
}
对应的求解器调用点(以 pimpleFoam/UEqn.H 为蓝本):
cpp
fvVectorMatrix UEqn
(
fvm::ddt(U) + fvm::div(phi, U)
+ MRF.DDt(U)
+ turbulence->divDevReff(U)
==
fvOptions(U) // porosity + momentumSource 作为动量源项
);
UEqn.relax();
fvOptions.constrain(UEqn); // 约束(如别处对 U 的固定值约束)
if (pimple.momentumPredictor())
{
solve(UEqn == -fvc::grad(p));
fvOptions.correct(U); // limitU 速度限幅,meanVelocityForce 的力平衡校正
}
能量/湍流方程的挂载(以 reactingFoam 为蓝本):fvOptions(rho, he) 引入能量源、fvOptions.constrain(EEqn) 施加 fixedTemperature(匹配 he);湍流方程里对 k/epsilon 调 fvOptions.constrain(...) 施加 porosityTurbulence。
运行日志会依次出现:
vbnet
Creating finite-volume options from "constant/fvOptions"
Selecting finite volume options type explicitPorositySource
Source: porosity
State: active
- selecting cells using cellZones porousZone
- selected 1234 cell(s) with volume 0.56
Selecting finite volume options type meanVelocityForce
Source: momentumSource
State: active
- selecting cells using cellZone inletCellZone
...
Pressure gradient source: uncorrected Ubar = 0.998, pressure gradient = 12.7
limitVelocity limitU Limited 0 (0%) of cells, with max limit 100
从字典到方程,零代码改动 ------这就是 fvOptions 的价值:一份 constant/fvOptions 同时驱动动量、能量、湍流方程上的源项、约束与修正。
24. 小结与全章要点
- 类族 :
fv::options : IOdictionary + fv::optionList(fvOptions.H第 70--74 行),读constant/fvOptions(或system/fvOptions,支持多区域);fv::option基类含name_/modelType_/coeffs_/fieldNames_/applied_/active_(fvOption.H第 96--131 行)。 - RTS 选型 :
type经option::New(fvOption.C第 84--117 行)查dictionaryConstructorTable,模板模型用makeFvOption(makeFvOption.H第 36--48 行)注册成scalarSemiImplicitSource一类名字;非模板用addToRunTimeSelectionTable。 - 列表遍历 :
fv::optionList由reset(fvOptionList.C第 102--131 行)按子字典创建,checkApplied(第 70--79 行)在timeIndex+2检查未使用源项。 - 三通道 :
source()/constrain()/correct()(fvOptionListTemplates.C第 35--82、313--350、353--393 行)遍历列表,按场名匹配、按isActive()决定、多态分发到派生类的addSup/constrain/correct。 - 接入
fvMatrix:Su进source_、Sp(正)进diag_、SuSp按符号分流(fvmSup.C第 45--67、134--156、239--270 行);约束经setValues→setValuesFromList(fvMatrix.C第 223--317 行)切断面连接并让该行恒等于约束值。 - 选区 :
cellSetOption提供all/cellSet/cellZone/points/movingPoints/geometric/cellType七种模式(cellSetOption.C第 46--59、64--142、173--348 行),几何模式经cellBitSet::select(cellBitSet.C第 90--178 行)落到单元标签;时序由isActive(cellSetOption.C第 383--426 行)+inTimeLimits(cellSetOptionI.H第 43--54 行)判定。 - sources :
SemiImplicitSource的Su/Sp由setFieldCoeffs(SemiImplicitSource.C第 52--209 行)解析、addSup(第 233--524 行)按SuDims/SpDims与VDash_装配;meanVelocityForce用dGradP=relaxation(|Ubar|−⟨U⟩)/⟨1/A⟩(第 170 行)逐步逼近目标。 - constraints/corrections :约束改矩阵(
FixedValueConstraint::constrain的setValues、fixedTemperatureConstraint经 thermo 换算、velocityDampingConstraint增对角);修正改场(limitVelocity/limitTemperature/limitTurbulenceViscosity)。 - 求解器接入 :
createFvOptions.H第 1--6 行创建;fvOptions(U)(源)、fvOptions.constrain(UEqn)(约束)、fvOptions.correct(U)(修正)三点固定出现。 - 与 coded 互补 :
fvOptions免编译、字典配置;coded 源(CodedSource<Type>)用运行期编译实现任意源项,对求解器完全透明。
它就是"方程里的插件系统" ------把源项、约束、修正统一成字典可配的 fv::option,通过三个调用点无缝接入任意方程,并可分区、分时、分场精确控制。