OpenFOAM 源码深度剖析 · 第二季-13 `fvOptions` / `fvConstraints` / `corrections`

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 S=S_u+S_p x 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ϕ=bA\phi = b Aϕ=b,其中 diag_ 存对角、source_ 存右端、upper_/lower_ 存面系数。源项 S=Su+Spϕ S=S_u+S_p\phi S=Su+Spϕ 的装配规则是:

  • Su S_u Su(显式)→ 进 source_;
  • Sp S_p Sp(隐式,且 Sp>0 S_p>0 Sp>0)→ 进 diag_;
  • Spϕ S_p\phi Spϕ 若 Sp<0 S_p<0 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ϕ=bA\phi = b 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 行):

  1. 元组:k (30.7 0); 即 (Su Sp);
  2. Function1:k { explicit table ((0 0) (5 30.7)); implicit none; };
  3. 表达式 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 S_p 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 S_p 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,直接把均匀源项加到矩阵(快路径)。
  • 第 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 ν \nu_{t,\max}=c\,\nu ν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/ vs system/);② 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. 小结与全章要点

  1. 类族 :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 行)。
  2. RTS 选型 :type 经 option::New(fvOption.C 第 84--117 行)查 dictionaryConstructorTable,模板模型用 makeFvOption(makeFvOption.H 第 36--48 行)注册成 scalarSemiImplicitSource 一类名字;非模板用 addToRunTimeSelectionTable。
  3. 列表遍历 :fv::optionList 由 reset(fvOptionList.C 第 102--131 行)按子字典创建,checkApplied(第 70--79 行)在 timeIndex+2 检查未使用源项。
  4. 三通道 :source()/constrain()/correct()(fvOptionListTemplates.C 第 35--82、313--350、353--393 行)遍历列表,按场名匹配、按 isActive() 决定、多态分发到派生类的 addSup/constrain/correct。
  5. 接入 fvMatrix :Su 进 source_、Sp(正)进 diag_、SuSp 按符号分流(fvmSup.C 第 45--67、134--156、239--270 行);约束经 setValues → setValuesFromList(fvMatrix.C 第 223--317 行)切断面连接并让该行恒等于约束值。
  6. 选区 :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 行)判定。
  7. sources :SemiImplicitSource 的 Su/Sp 由 setFieldCoeffs(SemiImplicitSource.C 第 52--209 行)解析、addSup(第 233--524 行)按 SuDims/SpDims 与 VDash_ 装配;meanVelocityForce 用 dGradP=relaxation(|Ubar|−⟨U⟩)/⟨1/A⟩(第 170 行)逐步逼近目标。
  8. constraints/corrections :约束改矩阵(FixedValueConstraint::constrain 的 setValues、fixedTemperatureConstraint 经 thermo 换算、velocityDampingConstraint 增对角);修正改场(limitVelocity/limitTemperature/limitTurbulenceViscosity)。
  9. 求解器接入 :createFvOptions.H 第 1--6 行创建;fvOptions(U)(源)、fvOptions.constrain(UEqn)(约束)、fvOptions.correct(U)(修正)三点固定出现。
  10. 与 coded 互补 :fvOptions 免编译、字典配置;coded 源(CodedSource<Type>)用运行期编译实现任意源项,对求解器完全透明。

它就是"方程里的插件系统" ------把源项、约束、修正统一成字典可配的 fv::option,通过三个调用点无缝接入任意方程,并可分区、分时、分场精确控制。

相关推荐
鶴哥只手遮天42 分钟前
OpenFOAM 源码深度剖析 · 第二季-14 `functionObjects`、求解器控制框架 与 表达式系统
算法·开源
鶴哥只手遮天44 分钟前
OpenFOAM 源码深度剖析 · 第二季-03 拉格朗日粒子与喷雾:`Cloud`/`parcel` 模板体系、运动积分与子模型群
算法·开源
hetao17338371 小时前
校内场-提高组 ZYZSC-S-Round 2
c++·算法
小严不想当码农1 小时前
icpc 亚洲区域赛补题链接整理
算法
xiaok1 小时前
Compositor图编辑器
算法
CAD开发小慧1 小时前
CAE软件开发:Coreform为何用ACIS处理CAD几何?
算法·架构·计算机图形学
七牛云行业应用1 小时前
DeepSeek Harness 自定义 Skill 教程:从 SKILL.md 到可复用 Agent 工作流
算法·github
hetao17338371 小时前
2026-10-14 hetao1733837 的刷题记录
c++·算法
Vitaliy Kostrov(柯维远)1 小时前
谁来承担额外工作?CDN 参与者如何协商资源
nginx·缓存·系统架构·开源·cdn