OpenFOAM 源码深度剖析 · 第二季-01 多相流与 VOF:`interFoam`、界面压缩与 MULES

OpenFOAM 源码深度剖析 · 第二季-01 多相流与 VOF:interFoam、界面压缩与 MULES

第二季开篇。第一季我们把 pimpleFoam、scalarTransportFoam 这类单相求解器的骨架拆到了线性求解器与矩阵装配层;本章正式进入 物理建模(physics modelling) 的深水区。多相流是 OpenFOAM 最具代表性的能力之一:interFoam 用 VOF(Volume of Fluid,流体体积法) 捕捉两相界面,靠 MULES (Multidimensional Universal Limiter with Explicit Solution)保证相分数有界,用 界面压缩(interface compression) 保持界面锐利,用 CSF(Continuum Surface Force) 模型把表面张力折算成体积力,并用 p_rgh 消除重力与压力梯度的量级相消。本章把这条链路从求解器主循环一路拆到限幅器的内层迭代。

本章全部路径、行号均来自在本仓库(WM_PROJECT_VERSION = v2606,C++17,master 分支)内的实际 Read。凡未亲自核对的内容一律不写;行号格式统一写作「文件(相对仓库根目录、第 a--b 行)」。

目录

  1. 本章定位与阅读地图
  2. 多相建模的基本框架与术语
  3. interFoam 主循环与 createFields.H 逐行拆解
  4. 相系统与物性:两相混合物族
  5. 相分数输运:alphaEqn.H 逐行剖析
  6. 界面压缩:cAlpha 的离散与量纲
  7. MULES:显式、限幅与有界的数学
  8. 表面张力与界面几何:interfaceProperties
  9. 动量、压力与连续性:p_rgh、ddtCorr
  10. 时间步与 Courant 数:显式相方程的配套机制
  11. 其它多相求解器:相变、可压缩与多相 VOF
  12. VOF 与其它界面方法的对照
  13. 常见报错 / 警告速查(20+ 条)
  14. 端到端调试流程与最佳实践
  15. 本章要点

1. 本章定位与阅读地图

1.1 为什么把 VOF 放在第二季开篇

第一季的分析对象是「单相、不可压、牛顿流体」这一最小闭环。它的求解器结构(UEqn 组装 → 压力方程 → 通量回代)在这一季依然是骨架,但多相流在三个维度上加了新东西:

  • 新方程 :除动量、压力外,多出一个 相分数输运方程 (alphaEqn.H)。它不是线性求解器的一行,而是一个「显式推进 + 限幅」的子系统。
  • 新数值工具 :MULES 限幅器(src/finiteVolume/fvMatrices/solvers/MULES/),用于在显式通量更新后把相分数强制约束到物理区间。
  • 新物理模型 :表面张力 (interfaceProperties + surfaceTensionModel)、重力势 (gh/ghf/p_rgh)、接触角 (alphaContactAngle*)。

这三类新增内容分别对应下面三块源码:

关注点 头文件 / 源文件 所在目录(相对仓库根)
求解器主循环 interFoam.C applications/solvers/multiphase/interFoam/
相分数方程 alphaEqn.H、alphaEqnSubCycle.H、createAlphaFluxes.H applications/solvers/multiphase/VoF/
相分数控制参数 alphaControls.H 求解器目录 / src/finiteVolume/cfdTools/general/include/
界面压缩与表面张力 interfaceProperties.H/.C src/transportModels/interfaceProperties/
表面张力模型 surfaceTensionModel、constant .../interfaceProperties/surfaceTensionModels/
压缩格式 interfaceCompression .../interfaceProperties/interfaceCompression/
两相物性 twoPhaseMixture、incompressibleTwoPhaseMixture、immiscibleIncompressibleTwoPhaseMixture src/transportModels/
限幅器 MULES.H、MULESTemplates.C、MULES.C、CMULES.H、CMULESTemplates.C src/finiteVolume/fvMatrices/solvers/MULES/
自适应时间步 alphaCourantNo.H、setDeltaT.H、CourantNo.H applications/solvers/multiphase/VoF/
示例字典 damBreak 用例 tutorials/multiphase/interFoam/laminar/damBreak/damBreak/

一个必须澄清的实现细节 :在 v2606 中,applications/solvers/multiphase/interFoam/ 目录下只有 interFoam.C、UEqn.H、pEqn.H、createFields.H、correctPhi.H、initCorrectPhi.H、alphaSuSp.H、rhofs.H 这些文件;相分数方程所用的 alphaEqn.H、alphaEqnSubCycle.H、createAlphaFluxes.H、setDeltaT.H、alphaCourantNo.H 全部位于共享目录 applications/solvers/multiphase/VoF/,通过求解器编译时的 include 路径被 #include。因此本章凡涉及相分数方程的行号,标注的都是 VoF/ 下的文件。这是很多网络资料里写错的地方,也是我们逐行核对后必须纠正的第一处。

1.2 阅读顺序建议

若你是第一次读多相源码,建议按 数据流 而不是按文件名读:

  1. 先读 createFields.H(第 3.8 节)------搞清 p_rgh、U、phi、mixture、rho、rhoPhi、gh、p 是谁造的、谁引用谁;
  2. 再读 alphaEqn.H(第 5 节)------搞清相分数如何被推进,alphaPhi10、alphaPhiUn、talphaPhi1Corr0 三个通量字段各自扮演什么角色;
  3. 再读 UEqn.H+pEqn.H(第 9 节)------搞清 rhoPhi 如何进入动量、phig 如何把界面力带进压力方程;
  4. 最后下潜到 MULES(第 7 节)与 interfaceProperties(第 8 节)------这时你已经知道这些函数被谁、在什么时机调用。

2. 多相建模的基本框架与术语

2.1 两相不互溶流的物理设定

interFoam 的官方描述(interFoam.C 第 33--37 行)是:

vbnet 复制代码
Solver for two incompressible, isothermal immiscible fluids using a VOF
(volume of fluid) phase-fraction based interface capturing approach,
with optional mesh motion and mesh topology changes including adaptive
re-meshing.

逐词拆解这段话,就是 interFoam 的能力边界:

  • two incompressible:只有两相(记作相 1 与相 2),且两相均不可压(密度为常数,只能在界面处变化);
  • isothermal:等温,不含能量方程;
  • immiscible :不互溶,没有相间质量传递(对比 interPhaseChangeFoam 才有相变);
  • VOF phase-fraction based:用相分数场隐式表征界面位置,而非显式追踪界面几何;
  • interface capturing:界面由数值格式「捕捉」,不与网格对齐;
  • optional mesh motion and topology changes :支持动网格与自适应重网格(dynamicFvMesh)。

2.2 相分数 α 与体积分数

VOF 的核心变量是一个标量场 相分数(phase fraction / volume fraction)α₁(x, t),定义为单元内第一相占据的体积分数:

  • α₁ = 1:该单元完全是相 1;
  • α₁ = 0:该单元完全是相 2;
  • 0 < α₁ < 1:界面穿过该单元(混合单元 / 界面单元)。

界面法向来自 α₁ 的梯度方向,曲率来自法向的散度:
n^= ∇α1 ∣∇α1∣ ,κ=−∇⋅n^=−∇⋅ ⁣ ( ∇α1 ∣∇α1∣ ) \hat n = \frac{\nabla \alpha_1}{|\nabla \alpha_1|}, \qquad \kappa = -\nabla\cdot\hat n = -\nabla\cdot\!\left(\frac{\nabla\alpha_1}{|\nabla\alpha_1|}\right) n^=∣∇α1∣∇α1,κ=−∇⋅n^=−∇⋅(∣∇α1∣∇α1)

表面张力则通过 CSF 模型 折算为单位体积力 fσ=σκ∇α1 f_\sigma = \sigma\kappa\nabla\alpha_1 fσ=σκ∇α1(第 8 节给出完整离散与量纲)。

2.3 三条必须同时成立的性质

VOF 的数值难点可以浓缩为三句「必须」:

  1. 输运要守恒:α₁ 的更新必须等价于「单元内两相体积的通量进出 + 源汇」的质量守恒离散表达;
  2. 有界性(boundedness):必须保证 0 ≤ α₁ ≤ 1,否则会出现非物理的「负相」或「超相」,并立即传染到混合物性(负密度、负粘度)导致求解崩溃;
  3. 界面锐利:数值扩散必须尽量小,否则界面被抹成十几个网格宽的「糊状带」,物性梯度被人为平滑,表面张力、浮力都失真。

interFoam 用「显式输运 + MULES 限幅 + 界面压缩通量」这一套组合同时满足三点:第 5 节讲相分数方程如何组装,第 6 节讲界面压缩,第 7 节讲 MULES 如何给出有界性。

2.4 求解器家族全景

applications/solvers/multiphase/ 下的求解器(由 Glob 得到)构成一条「物理能力递增」的谱系:

求解器 界面方法 相数 可压缩 相变 典型用途
interFoam VOF(代数目) 2 否 无 溃坝、波浪、液舱晃荡
interIsoFoam VOF(几何 isoAdvection) 2 否 无 高锐度界面、几何重构
interPhaseChangeFoam VOF + 相变源项 2 否 空化/蒸发 螺旋桨空化
interCondensatingEvaporatingFoam VOF + 能量 2 否 蒸发/冷凝 沸腾、冷凝
compressibleInterFoam VOF(可压缩) 2 是 无 水下爆炸、可压缩两相
compressibleMultiphaseInterFoam VOF(可压缩) N 是 无 多相可压缩
multiphaseInterFoam VOF(N 相) N 否 无 四相溃坝
twoPhaseEulerFoam 欧拉-欧拉(双流体) 2 可(近不可压) 有 鼓泡床、流化床
multiphaseEulerFoam 欧拉-欧拉 N 可 有 多相反应器
driftFluxFoam 漂移通量 2 是 无 悬浮液、泥浆
MPPICInterFoam VOF + 颗粒 2+颗粒 否 无 液滴夹带颗粒
cavitatingFoam 均质混合(barotropic) 1 是 空化 空化流动
potentialFreeSurfaceFoam 单相 + 自由液面 1 否 无 势流初值化

关键分野 :上表前一半是 界面捕捉(interface capturing) ,用相分数场表征界面;twoPhaseEulerFoam / multiphaseEulerFoam 是 欧拉-欧拉双流体,每相各有一套速度场与动量方程,界面以相间作用力(曳力、升力、湍流扩散)建模,而非几何界面。二者方程结构完全不同------这决定了「VOF 的代码经验」不能直接迁移到欧拉-欧拉。


3. interFoam 主循环与 createFields.H 逐行拆解

3.1 头文件与依赖(interFoam.C 第 41--54 行)

cpp 复制代码
#include "fvCFD.H"
#include "dynamicFvMesh.H"                       // 动网格
#include "CMULES.H"                              // MULES 限幅器(含 MULES.H)
#include "EulerDdtScheme.H"                      // 一阶欧拉时间格式
#include "localEulerDdtScheme.H"                 // 局部时间步(LTS / LES)
#include "CrankNicolsonDdtScheme.H"              // Crank-Nicolson 时间格式
#include "subCycle.H"                            // 子循环迭代器
#include "immiscibleIncompressibleTwoPhaseMixture.H"  // 两相混合物(VOF)
#include "incompressibleInterPhaseTransportModel.H"   // 不可压相间输运(湍流)
#include "turbulentTransportModel.H"             // 湍流模型
#include "pimpleControl.H"                       // PIMPLE 控制
#include "fvOptions.H"                           // 源项/约束框架
#include "CorrectPhi.H"                          // 通量一致性修正
#include "fvcSmooth.H"                           // 场平滑(界面/曲率)

逐行说明:

  • dynamicFvMesh.H 引入动网格基类,对应主循环里的 mesh.update();
  • CMULES.H 是 Corrected MULES 的入口(MULES::correct),它不是纯显式,而是「隐式有界解 + 显式限幅校正」;
  • 三个 ddt 格式头(Euler / localEuler / CrankNicolson)说明相分数方程支持一阶欧拉、局部时间步与 CN 二阶;这与 alphaEqn.H 里对 ddt 方案的运行时判断(第 5.2 节)一一对应;
  • subCycle.H 提供 subCycle<T> 迭代器类,用于 alphaEqnSubCycle.H(第 5.7 节);
  • immiscibleIncompressibleTwoPhaseMixture.H 是本章相系统的主角(第 4 节);
  • CorrectPhi.H 与 createFields.H 里的 Uf 配套,负责通量一致性(第 9 节)。

3.2 初始化段(interFoam.C 第 68--85 行)

cpp 复制代码
#include "postProcess.H"
#include "addCheckCaseOptions.H"
#include "setRootCaseLists.H"
#include "createTime.H"
#include "createDynamicFvMesh.H"
#include "initContinuityErrs.H"
#include "createDyMControls.H"
#include "createFields.H"
#include "createAlphaFluxes.H"        // ← 定义 alphaPhi10 / talphaPhi1Corr0 / alphaPhiUn
#include "initCorrectPhi.H"
#include "createUfIfPresent.H"

if (!LTS)
{
    #include "CourantNo.H"
    #include "setInitialDeltaT.H"
}

要点:

  • createAlphaFluxes.H 是 VoF 共享头,定义了相分数方程所需的三个通量字段(第 5.8 节),必须早于主循环前完成构造;
  • !LTS 分支在初始化时先做一次 Courant 数与初始时间步估计------这是显式相方程「一开始就要控 Co」的体现(第 10 节);
  • addCheckCaseOptions.H 提供 -dry-run、-dry-run-cleanup 等运行期检查选项,对调网格/边界尤其有用。

3.3 时间循环骨架(interFoam.C 第 90--178 行)

cpp 复制代码
while (runTime.run())
{
    #include "readDyMControls.H"

    if (LTS) { #include "setRDeltaT.H"; }
    else
    {
        #include "CourantNo.H";
        #include "alphaCourantNo.H";
        #include "setDeltaT.H";
    }

    ++runTime;

    // --- Pressure-velocity PIMPLE corrector loop
    while (pimple.loop())
    {
        if (pimple.firstIter() || moveMeshOuterCorrectors)
        {
            mesh.update();
            if (mesh.changing())
            {
                if (mesh.topoChanging()) { talphaPhi1Corr0.clear(); }

                gh  = (g & mesh.C())  - ghRef;
                ghf = (g & mesh.Cf()) - ghRef;

                MRF.update();

                if (correctPhi)
                {
                    phi = mesh.Sf() & Uf();
                    #include "correctPhi.H"
                    fvc::makeRelative(phi, U);
                    mixture.correct();
                }
                if (checkMeshCourantNo) { #include "meshCourantNo.H"; }
            }
        }

        #include "alphaControls.H"        // 读取 nAlphaCorr/nAlphaSubCycles/cAlpha/MULESCorr...
        #include "alphaEqnSubCycle.H"     // 相方程(可子循环)

        mixture.correct();                // 更新界面几何(K_/nHatf_)与物性

        if (pimple.frozenFlow()) { continue; }

        #include "UEqn.H"                 // 动量方程
        while (pimple.correct()) { #include "pEqn.H"; }

        if (pimple.turbCorr()) { turbulence->correct(); }
    }

    runTime.write();
    runTime.printExecutionTime(Info);
}

这段骨架里藏着六个关键设计决定,逐个展开:

  1. 相方程在动量/压力之前 (alphaEqnSubCycle.H 早于 UEqn.H):先推进界面、更新物性(mixture.correct() 重算 rho、mu、曲率 K_、面法向 nHatf_),再据新物性推进动量。这保证动量方程用的是「当前时刻」的密度与界面力。
  2. mixture.correct() 被调用两次 :一次在相方程后(第 154 行),确保 UEqn/pEqn 用新 α;一次在动网格重算通量后(第 141 行),确保界面几何随网格更新。它内部实际调用 interfaceProperties::correct() → calculateK()(第 8.3 节)。
  3. 动网格优先 :网格局部变化时先 mesh.update(),再更新重力势 gh/ghf(因为 gh = g·x - ghRef 依赖单元/面心位置 mesh.C()/mesh.Cf())。
  4. topoChanging() 时清 talphaPhi1Corr0 :拓扑变了(加/删单元),上一步缓存的「界面压缩通量历史项」失效,必须丢弃,否则会污染 ddtCorr(第 9.4 节)。
  5. frozenFlow() 提前 continue :当 PIMPLE 的 frozenFlow 选为运行期控制(例如等温稳态求平均)时,跳过动量与压力,只推进相分数。
  6. checkMeshCourantNo 与 meshCourantNo.H:动网格下的网格 Courant 数监控,避免网格运动本身过快。

3.4 主循环调用的五个文件角色速览

文件(相对 interFoam/) 何时调用 作用一句话
alphaControls.H 每外层迭代 从 fvSolution 的 alpha.water 子字典读取相分数控制参数
alphaEqnSubCycle.H(实际在 VoF/) 每外层迭代 封装「子循环 + 校正次数」的相分数推进
UEqn.H 每外层迭代 组装并求解动量方程(含界面力/浮力/动压)
pEqn.H 每压力校正 组装 p_rgh 泊松方程、回代 phi/U、还原 p
correctPhi.H 网格变化且 correctPhi 用 CorrectPhi 重建面通量,消除网格运动带来的不一致

3.5 createFields.H 逐段拆解(interFoam/createFields.H)

(a) p_rgh 的读入(第 3--15 行)

cpp 复制代码
Info<< "Reading field p_rgh\n" << endl;
volScalarField p_rgh
(
    IOobject
    (
        "p_rgh",
        runTime.timeName(),
        mesh,
        IOobject::MUST_READ,
        IOobject::AUTO_WRITE
    ),
    mesh
);

p_rgh 是「修正压力」p − ρg·h,MUST_READ 意味着算例必须在 0/ 目录下提供 p_rgh 初值文件。它与 U、alpha.water 共同构成 VOF 算例的三大必备初始场。

(b) U 与 phi(第 17--31 行)

cpp 复制代码
volVectorField U(..., IOobject::MUST_READ, IOobject::AUTO_WRITE ...);
#include "createPhi.H"

createPhi.H 从 U 插值造出 phi = Sf · Uf(面通量)。

(c) 两相混合物与相分数引用(第 34--41 行)

cpp 复制代码
Info<< "Reading transportProperties\n" << endl;
immiscibleIncompressibleTwoPhaseMixture mixture(U, phi);

volScalarField& alpha1(mixture.alpha1());
volScalarField& alpha2(mixture.alpha2());

const dimensionedScalar& rho1 = mixture.rho1();
const dimensionedScalar& rho2 = mixture.rho2();

混合物对象从 constant/transportProperties 读入相名、各相 nu/rho 与 sigma(第 4、8 节),并持有 alpha1/alpha2。注意 alpha1、alpha2 是引用 ,后续 alphaEqn.H 直接改它们。

(d) 密度与质量通量(第 44--71 行)

cpp 复制代码
// Need to store rho for ddt(rho, U)
volScalarField rho
(
    IOobject("rho", ...),
    alpha1*rho1 + alpha2*rho2
);
rho.oldTime();

// Mass flux
surfaceScalarField rhoPhi
(
    IOobject("rhoPhi", ..., IOobject::NO_READ, IOobject::NO_WRITE),
    fvc::interpolate(rho)*phi
);
  • rho = α₁ρ₁ + α₂ρ₂ 是体积加权混合密度(不是质量加权);
  • rhoPhi = interp(ρ)·phi 是质量通量,进入 UEqn 的对流项 fvm::div(rhoPhi, U);
  • rho.oldTime() 立即缓存旧值,因为 ddt(rho,U) 需要 ρ⁰。

(e) 湍流输运模型(第 73--84 行)

cpp 复制代码
typedef incompressibleInterPhaseTransportModel
    <immiscibleIncompressibleTwoPhaseMixture> transportModelType;
autoPtr<transportModelType> turbulence
(
    new transportModelType(rho, U, phi, rhoPhi, mixture)
);

多相湍流用专用的 incompressibleInterPhaseTransportModel,它把 mixture.surfaceTensionForce() 纳入湍流产生项(表面张力引起的界面湍流)。

(f) 重力势与绝对压力(第 86--113 行)

cpp 复制代码
#include "readGravitationalAcceleration.H"
#include "readhRef.H"
#include "gh.H"          // 由 g 与 hRef 造出 gh (单元) 与 ghf (面)

volScalarField p(..., p_rgh + rho*gh);

label pRefCell = 0;
scalar pRefValue = 0.0;
setRefCell(p, p_rgh, pimple.dict(), pRefCell, pRefValue);

gh = g·C − g·hRef 是重力势,ghf = g·Cf − g·hRef 是面重力势。p = p_rgh + ρgh 是绝对压力(回代公式,与 pEqn.H 最后一行一致)。setRefCell 从 fvSolution 的 PIMPLE 字典读参考单元/参考值,用于封闭压力方程(纯 Dirichlet 边界缺失时可固定一点)。

(g) 参考压力的一致化与 fluxRequired(第 115--127 行)

cpp 复制代码
if (p_rgh.needReference())
{
    p += dimensionedScalar("p", p.dimensions(),
                           pRefValue - getRefCellValue(p, pRefCell));
    p_rgh = p - rho*gh;
}

mesh.setFluxRequired(p_rgh.name());
mesh.setFluxRequired(alpha1.name());

needReference() 为真时(封闭域、无压力边界),把参考单元压力平移为 pRefValue,并同步 p_rgh。setFluxRequired 标记 p_rgh 与 alpha1 需要「通量」参与,使 pEqn/alphaEqn 能访问 phi 的守恒形式。


4. 相系统与物性:两相混合物族

4.1 twoPhaseMixture:相分数与相名的基类

src/transportModels/twoPhaseMixture/twoPhaseMixture/twoPhaseMixture.C 的构造函数(第 33--65 行):

cpp 复制代码
Foam::twoPhaseMixture::twoPhaseMixture
(
    const fvMesh& mesh,
    const dictionary& dict
)
:
    phase1Name_(dict.get<wordList>("phases")[0]),
    phase2Name_(dict.get<wordList>("phases")[1]),

    alpha1_
    (
        IOobject
        (
            IOobject::groupName("alpha", phase1Name_),
            mesh.time().timeName(),
            mesh,
            IOobject::MUST_READ,
            IOobject::AUTO_WRITE
        ),
        mesh
    ),

    alpha2_
    (
        IOobject(IOobject::groupName("alpha", phase2Name_), ...),
        1.0 - alpha1_
    )
{}

逐行解读:

  • dict.get<wordList>("phases")[0] / [1]:从 transportProperties 读相名列表,例如 phases (water air); 得到 phase1Name_ = "water"、phase2Name_ = "air"。相名不是硬编码的 1/2,而是任意词------这是 OpenFOAM 多相「族化」设计的体现。
  • IOobject::groupName("alpha", phase1Name_):把相名拼进字段名,得到 alpha.water 、alpha.air。这就是算例目录里字段名带组名的来源,也是 fvSolution 中求解器名 "alpha.water.*" 匹配的依据。
  • alpha1_ 是 MUST_READ(必须由算例提供初始 α 场),alpha2_ 用 1.0 - alpha1_ 构造(有界性由 α₁ 保证,α₂ 自动跟随)。
  • 只有 α₁ 被显式输运 ,α₂ 始终是 1 − α₁------这是两相 VOF 的最优近似,把「两相之和为 1」的代数约束内建进数据结构。

4.2 incompressibleTwoPhaseMixture:物性混合的主力

头文件(src/transportModels/incompressible/incompressibleTwoPhaseMixture/incompressibleTwoPhaseMixture.H 第 66--76 行)声明了四个物性成员:

cpp 复制代码
autoPtr<viscosityModel> nuModel1_;   // 相 1 粘度模型(New 由 RTS 选择)
autoPtr<viscosityModel> nuModel2_;
dimensionedScalar rho1_;
dimensionedScalar rho2_;
const volVectorField& U_;
const surfaceScalarField& phi_;
volScalarField nu_;

构造函数(.C 第 61--120 行)逐段:

cpp 复制代码
IOdictionary
(
    IOobject
    (
        "transportProperties",
        U.time().constant(),
        U.db(),
        IOobject::MUST_READ_IF_MODIFIED,
        IOobject::NO_WRITE
    )
),
twoPhaseMixture(U.mesh(), *this),

nuModel1_(viscosityModel::New("nu1", subDict(phase1Name_), U, phi)),
nuModel2_(viscosityModel::New("nu2", subDict(phase2Name_), U, phi)),

rho1_("rho", dimDensity, nuModel1_->viscosityProperties()),
rho2_("rho", dimDensity, nuModel2_->viscosityProperties()),
...
  • 继承链:incompressibleTwoPhaseMixture : public IOdictionary, public transportModel, public twoPhaseMixture。它同时是 一个 IOdictionary(自身就是 transportProperties 字典),又是一个 transportModel(提供 nu()),又是一个 twoPhaseMixture(提供 α)。
  • phase1Name_ 子字典经 viscosityModel::New 造出粘度模型(Newtonian、powerLaw、BirdCasson 等,由 transportModel 关键字选择);
  • rho1_/rho2_ 从各相子字典里读 rho,维度固定为 dimDensity(kg/m³)。

粘度混合 (.C 第 43--56 行的 calcNu):

cpp 复制代码
void Foam::incompressibleTwoPhaseMixture::calcNu()
{
    nuModel1_->correct();
    nuModel2_->correct();

    const volScalarField limitedAlpha1
    (
        "limitedAlpha1",
        clamp(alpha1_, zero_one{})          // ← 关键:把 α 夹到 [0,1] 再算物性
    );

    nu_ = mu()/(limitedAlpha1*rho1_ + (scalar(1) - limitedAlpha1)*rho2_);
}

clamp(alpha1_, zero_one{}) 是 OpenFOAM v2606 的现代写法(等价于 min(max(α,0),1))。它对所有物性计算都做有界化:即使 MULES 因某种原因留下了极小的越界,物性也不会出现负密度/负粘度。这是「数值鲁棒性的第二道防线」。

动态粘度的混合 (.C 第 125--140 行):

cpp 复制代码
Foam::tmp<Foam::volScalarField>
Foam::incompressibleTwoPhaseMixture::mu() const
{
    const volScalarField limitedAlpha1(clamp(alpha1_, zero_one{}));

    return volScalarField::New
    (
        "mu",
        IOobject::NO_REGISTER,
        limitedAlpha1*rho1_*nuModel1_->nu()
      + (scalar(1) - limitedAlpha1)*rho2_*nuModel2_->nu()
    );
}

这里 mu = α₁ρ₁ν₁ + (1−α₁)ρ₂ν₂(体积加权动态粘度)。面插值版本 muf()(第 151--166 行)先对 α 插值再夹紧:alpha1f = clamp(fvc::interpolate(alpha1_), zero_one{}),然后 muf = α₁f ρ₁ ν₁f + (1−α₁f) ρ₂ ν₂f。注意面插值前先夹紧------这是界面通量层面上的第三道有界化。

4.3 immiscibleIncompressibleTwoPhaseMixture:把界面属性也继承进来

src/transportModels/immiscibleIncompressibleTwoPhaseMixture/immiscibleIncompressibleTwoPhaseMixture.C(第 33--52 行):

cpp 复制代码
Foam::immiscibleIncompressibleTwoPhaseMixture::
immiscibleIncompressibleTwoPhaseMixture
(
    const volVectorField& U,
    const surfaceScalarField& phi
)
:
    incompressibleTwoPhaseMixture(U, phi),
    interfaceProperties(alpha1(), U, *this)
{}

bool Foam::immiscibleIncompressibleTwoPhaseMixture::read()
{
    return incompressibleTwoPhaseMixture::read()
        && interfaceProperties::read();
}

这短短五行是全章最重要的继承关系之一 :immiscibleIncompressibleTwoPhaseMixture 通过多重继承,同时具备

  • incompressibleTwoPhaseMixture(物性:ρ、μ、ν)+ twoPhaseMixture(α 场);
  • interfaceProperties(界面:曲率 K_、面法向 nHatf_、表面张力 surfaceTensionForce()、压缩系数 cAlpha)。

于是 interFoam 里一句 mixture.surfaceTensionForce() 就能拿到 CSF 界面力,mixture.cAlpha() 拿到压缩系数,mixture.nearInterface() 拿到界面标记------混合物对象是多相物理的「外观模式(facade)」 。read() 把两个基类的重读串起来:transportProperties 一变,物性与界面参数同步更新。

4.4 物性混合的数学与量纲核对

设相分数 α₁ ∈ 0,1,则混合物性定义为体积分数加权:
ρ(α1)=α1ρ1+(1−α1)ρ2,μ(α1)=α1ρ1ν1+(1−α1)ρ2ν2,ν=μρ \rho(\alpha_1) = \alpha_1\rho_1 + (1-\alpha_1)\rho_2,\qquad \mu(\alpha_1) = \alpha_1\rho_1\nu_1 + (1-\alpha_1)\rho_2\nu_2,\qquad \nu = \frac{\mu}{\rho} ρ(α1)=α1ρ1+(1−α1)ρ2,μ(α1)=α1ρ1ν1+(1−α1)ρ2ν2,ν=ρμ

量纲核对:

  • rho1_ 维度 dimDensity = kg/m³;
  • nuModel::nu() 维度 dimViscosity = m²/s(运动粘度);
  • mu = ρ·ν 维度 kg/(m·s),即 dimDensity*dimViscosity;
  • nu_ = mu/rho 回到 m²/s。三者自洽。

一个常见困惑 :为什么混合密度用体积加权而不是质量加权?因为 VOF 表征的是体积占比;单元内体积守恒是硬约束,对应的守恒量自然是体积加权密度。质量加权密度需要知道各相质量分数,而 VOF 直接给的是体积分数。

4.5 transportProperties 字典模板与逐项说明

以下是可直接粘贴的最小可用模板(结构对齐 tutorials/multiphase/interFoam/laminar/damBreak/damBreak/constant/transportProperties 第 17--33 行):

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      transportProperties;
}
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //

phases          (water air);

water
{
    transportModel  Newtonian;
    nu              1e-06;      // 水的运动粘度 [m^2/s]
    rho             1000;       // 水的密度 [kg/m^3]
}

air
{
    transportModel  Newtonian;
    nu              1.48e-05;   // 空气运动粘度 [m^2/s]
    rho             1;          // 空气密度 [kg/m^3]
}

sigma            0.07;          // 水-空气表面张力 [kg/s^2] = [N/m]

// ************************************************************************* //

逐项说明与调参表:

条目 含义 单位 / 维度 取值建议 注意
phases (water air) 相名列表,顺序决定 α₁、α₂ --- 与 0/ 下 alpha.water 一致 顺序反过来会改变 rho1/rho2
transportModel Newtonian 各相粘度模型类型(RTS) --- 水/空气均用 Newtonian 非牛顿用 powerLaw/CrossPowerLaw
nu 该相运动粘度 m²/s 水 1e-6,空气 1.48e-5 不是动力粘度 μ
rho 该相密度(常数) kg/m³ 水 1000,空气 1 决定浮力与 rhoPhi
sigma 两相表面张力系数 kg/s²(=N/m) 水-空气 0.072,水-油 0.02--0.05 也必须可写成子字典(见第 8.8 节)

易错点 :sigma 直接写在顶层时维度由 constantSurfaceTension 读取为 dimMass/sqr(dimTime)(第 8.7 节),数值上等于 N/m。若误填成动力量纲(如把 0.07 写成 70,或加了错误单位),surfaceTensionForce 会放大到把界面撕碎------见第 13 节 sigma 量纲错误条目。


5. 相分数输运:alphaEqn.H 逐行剖析

对应文件:applications/solvers/multiphase/VoF/alphaEqn.H(共 268 行)。它被 alphaEqnSubCycle.H 在子循环体内 #include,每调用一次就推进一个(子)时间步的相分数。

5.1 数学推导链 1:VOF 相分数输运方程与界面压缩项

Step 1:从体积守恒出发。 对任意控制体 V,相 1 的体积为 ∫α₁ dV。不可压、不互溶条件下相 1 体积守恒:
∂∂t ∫Vα1 dV+ ∮∂V α1 U dS=0 \frac{\partial}{\partial t}\int_V \alpha_1\,dV + \oint_{\partial V}\alpha_1\,\mathbf{U}\,d\mathbf{S} = 0 ∂t∂∫Vα1dV+∮∂Vα1UdS=0

逐项:左第一项是单元内相 1 体积的时间变化;第二项是相 1 被速度场 U 带出/带入单元的净通量。微分形式(不可压,∇·U = 0):
∂α1 ∂t +∇⋅(Uα1)=0 (5.1) \frac{\partial\alpha_1}{\partial t} + \nabla\cdot(\mathbf{U}\alpha_1) = 0 \tag{5.1} ∂t∂α1+∇⋅(Uα1)=0(5.1)

Step 2:截断误差制造界面抹平。 离散 (5.1) 时,数值格式引入的数值扩散使界面在若干网格内从 0 过渡到 1,「混合单元」变厚。物理上这会夸大界面面积、削弱表面张力与浮力。

Step 3:引入界面压缩。 加一个人工相对速度 U_c,只在界面法向、指向 alpha 增大侧,把界面「自我锐化」。对相 1 加对流项、对相 2 加相反项(以保持总量守恒),得
∂α1 ∂t +∇⋅(Uα1)+∇⋅ ⁣Uc α1(1−α1)=0 (5.2) \frac{\partial\alpha_1}{\partial t} + \nabla\cdot(\mathbf{U}\alpha_1) + \nabla\cdot\!\big\\mathbf{U}_c\\,\\alpha_1(1-\\alpha_1)\\big = 0 \tag{5.2} ∂t∂α1+∇⋅(Uα1)+∇⋅Ucα1(1−α1)=0(5.2)

因子 α₁(1−α₁) 保证压缩项只在混合单元(0<α<1)内非零,纯相区自动关闭。

Step 4:令 U_c 沿界面法向、大小正比于相分数梯度。
Uc=cα ∣U∣ n^,n^= ∇α1 ∣∇α1∣ \mathbf{U}c = c\alpha\,|\mathbf{U}|\,\hat n,\qquad \hat n = \frac{\nabla\alpha_1}{|\nabla\alpha_1|} Uc=cα∣U∣n^,n^=∣∇α1∣∇α1

代入 (5.2),压缩项变成
∇⋅ ⁣cα∣U∣n\^ α(1−α) \nabla\cdot\!\bigc_\\alpha\|\\mathbf{U}\|\\hat n\\,\\alpha(1-\\alpha)\\big ∇⋅cα∣U∣n\^α(1−α)

在 OpenFOAM 里,这个「 cα∣U∣ c_\alpha|\mathbf{U}| cα∣U∣」被实现为面尺度压缩通量 phic (第 5.3 节),而 n^ \hat n n^ 的界面投影交给 mixture.nHatf()(第 8.4 节)。 cα c_\alpha cα 就是 cAlpha。

Step 5:量纲核对。 压缩通量 phic 必须与 phi(体积通量,m³/s)同量纲,才能与对流项相加:
phic=cα⋅∣U∣=1⋅ms=m/s\\text{phic} = c_\\alpha\cdot\big\|\\mathbf{U}\|\\big = 1\cdot\frac{m}{s} = m/s phic=cα⋅∣U∣=1⋅sm=m/s

而离散方程里实际用的是「速度 × 面积」,因此 phic 在代码里定义为 cAlpha*mag(phi/mesh.magSf())(第 59 行),即「 cα c_\alpha cα × 面法向速度」,量纲 m/s;与 nHatf_(面法向矢量与 Sf 点积,量纲 m²)相乘后才变成 m³/s。这个量纲链在第 6 节会再核对一次。

5.2 ddt 方案与 ocCoeff(alphaEqn.H 第 6--56 行)

cpp 复制代码
// Set the off-centering coefficient according to ddt scheme
scalar ocCoeff = 0;
{
    tmp<fv::ddtScheme<scalar>> tddtAlpha
    (
        fv::ddtScheme<scalar>::New(mesh, mesh.ddtScheme("ddt(alpha)"))
    );
    const fv::ddtScheme<scalar>& ddtAlpha = tddtAlpha();

    if (isType<fv::EulerDdtScheme<scalar>>(ddtAlpha)
     || isType<fv::localEulerDdtScheme<scalar>>(ddtAlpha))
    {
        ocCoeff = 0;
    }
    else if (isType<fv::CrankNicolsonDdtScheme<scalar>>(ddtAlpha))
    {
        if (nAlphaSubCycles > 1)
        {
            FatalErrorInFunction
                << "Sub-cycling is not supported "
                   "with the CrankNicolson ddt scheme"
                << exit(FatalError);
        }
        if (alphaRestart || mesh.time().timeIndex() > mesh.time().startTimeIndex() + 1)
        {
            ocCoeff = refCast<const fv::CrankNicolsonDdtScheme<scalar>>(ddtAlpha).ocCoeff();
        }
    }
    else
    {
        FatalErrorInFunction
            << "Only Euler and CrankNicolson ddt schemes are supported"
            << exit(FatalError);
    }
}

// Set the time blending factor, 1 for Euler
scalar cnCoeff = 1.0/(1.0 + ocCoeff);

逐行要点:

  • 相分数方程只接受三种 ddt 格式 :Euler、localEuler(LTS)、CrankNicolson。其余格式立即 FatalError。原因:相分数的显式+MULES 组合对时间格式要求苛刻,高阶多步格式会破坏有界性证明。
  • ocCoeff 是 CN 的偏心系数(off-centering) ,从 fvSchemes 的 ddt(alpha) 子字典读 ocCoeff。ocCoeff = 0 是标准 CN(二阶但易振荡),ocCoeff = 1 退化为 Euler(一阶但稳定)。alphaEqn.H 只在非初始步且非 restart 时启用 CN 偏心。
  • cnCoeff = 1/(1+ocCoeff) 是时间混合系数。
  • CN 与子循环互斥 :nAlphaSubCycles > 1 且用 CN 时直接报错,因为子循环与 CN 的偏心加权在时间上无法自洽。

5.3 压缩通量 phic(alphaEqn.H 第 59--89 行)

cpp 复制代码
// Standard face-flux compression coefficient
surfaceScalarField phic(mixture.cAlpha()*mag(phi/mesh.magSf()));

// Add the optional isotropic compression contribution
if (icAlpha > 0)
{
    phic *= (1.0 - icAlpha);
    phic += (mixture.cAlpha()*icAlpha)*fvc::interpolate(mag(U));
}

// Add the optional shear compression contribution
if (scAlpha > 0)
{
    phic += scAlpha*mag(mesh.delta() & fvc::interpolate(symm(fvc::grad(U))));
}

surfaceScalarField::Boundary& phicBf = phic.boundaryFieldRef();

// Do not compress interface at non-coupled boundary faces
// (inlets, outlets etc.)
forAll(phic.boundaryField(), patchi)
{
    fvsPatchScalarField& phicp = phicBf[patchi];
    if (!phicp.coupled()) { phicp == 0; }
}

逐行解读:

  • 第 59 行 :phic = cAlpha * |phi|/|Sf|。phi/mesh.magSf() 是面法向速度(量纲 m/s),取模得到速度大小,再乘 cAlpha。默认 cAlpha = 1。
  • 第 62--66 行 :icAlpha > 0 时加入各向同性压缩 ------把标准压缩按 (1 − icAlpha) 缩放,再补一个 cAlpha·icAlpha·interp(|U|) 的全速度大小项。物理含义:不只在界面法向压缩,还在所有方向(各向同性)引入与速度大小成正比的压缩,用于强剪切/破碎界面的场合。
  • 第 69--73 行 :scAlpha > 0 时加入剪切压缩 ------scAlpha·|Δ·interp(symm(grad(U)))|,其中 mesh.delta() 是 owner→neighbour 位移矢量,symm(grad(U)) 是对称应变率。这一项按面法向的应变率投影施加压缩,抑制剪切层对界面的拉伸扩散。
  • 第 76--89 行 :边界处理 。phic.boundaryFieldRef() 遍历所有边界面,若该面不是 coupled (即 inlet/outlet/wall 等非连通面),则把 phicp 置零。意义:入口/出口处不应施加界面压缩(那里是物理流入流出,不是界面自锐化),否则会在边界生成伪界面。coupled 面(cyclic/processor)保留压缩以保证跨域一致。

5.4 MULESCorr 分支:隐式有界解 + 显式校正(alphaEqn.H 第 99--155 行)

cpp 复制代码
if (MULESCorr)
{
    #include "alphaSuSp.H"

    fvScalarMatrix alpha1Eqn
    (
        ( LTS ? fv::localEulerDdtScheme<scalar>(mesh).fvmDdt(alpha1)
              : fv::EulerDdtScheme<scalar>(mesh).fvmDdt(alpha1) )
      + fv::gaussConvectionScheme<scalar>
        (mesh, phiCN, upwind<scalar>(mesh, phiCN)).fvmDiv(phiCN, alpha1)
     ==
        Su + fvm::Sp(Sp + divU, alpha1)
    );

    alpha1Eqn.solve();

    Info<< "Phase-1 volume fraction = "
        << alpha1.weightedAverage(mesh.Vsc()).value()
        << "  Min(" << alpha1.name() << ") = " << min(alpha1).value()
        << "  Max(" << alpha1.name() << ") = " << max(alpha1).value()
        << endl;

    tmp<surfaceScalarField> talphaPhi1UD(alpha1Eqn.flux());
    alphaPhi10 = talphaPhi1UD();

    if (alphaApplyPrevCorr && talphaPhi1Corr0.valid())
    {
        Info<< "Applying the previous iteration compression flux" << endl;
        MULES::correct(geometricOneField(), alpha1, alphaPhi10,
                       talphaPhi1Corr0.ref(), oneField(), zeroField());
        alphaPhi10 += talphaPhi1Corr0();
    }

    // Cache the upwind-flux
    talphaPhi1Corr0 = talphaPhi1UD;

    alpha2 = 1.0 - alpha1;
    mixture.correct();
}

逐行解读:

  • 第 99 行 if (MULESCorr) :MULESCorr 由 alphaControls.H(=fvSolution)读入,默认 false;教程 damBreak 用 MULESCorr yes;(第 7.9 节)。
  • 第 103--120 行 :组装一个隐式相分数方程 。时间项用 Euler(或 LTS 局部);对流项用 gaussConvectionScheme + upwind(一阶迎风,严格有界) ,通量用 phiCN(CN 时为 cnCoeff*phi + (1−cnCoeff)*phi.oldTime(),见第 91--97 行;纯 Euler 时即 phi)。右端 Su + Sp·α 是源项(第 5.9 节的 alphaSuSp.H 在此默认全零)。
  • 为什么用隐式? 隐式一阶迎风天然有界(M-matrix 保证),能给出一个「安全但过扩散」的相分数。它作为 MULESCorr 的「底解」。
  • 第 130--131 行 :取出隐式解的通量 talphaPhi1UD = alpha1Eqn.flux(),赋给待修正通量 alphaPhi10。
  • 第 133--147 行 :alphaApplyPrevCorr 且存在上一步校正通量时,应用上一步缓存的压缩校正通量 talphaPhi1Corr0------通过 MULES::correct 再限制一次,然后累加到 alphaPhi10。这是「稳态/充分发展」下的效率优化(只需一次隐式解,后续靠缓存压缩通量维持锐度)。
  • 第 150 行 :把刚算出的迎风通量缓存 为 talphaPhi1Corr0,供下一时间步使用。
  • 第 152--154 行 :alpha2 = 1 − alpha1,并 mixture.correct() 立刻更新界面几何与物性。

5.5 校正循环与 alphaPhiUn(alphaEqn.H 第 158--226 行)

cpp 复制代码
for (int aCorr=0; aCorr<nAlphaCorr; aCorr++)
{
    #include "alphaSuSp.H"

    surfaceScalarField phir(phic*mixture.nHatf());

    alphaPhiUn =
        fvc::flux(phi, alpha1, alphaScheme)
      + fvc::flux(-fvc::flux(-phir, alpha2, alpharScheme), alpha1, alpharScheme);

    if (MULESCorr)
    {
        tmp<surfaceScalarField> talphaPhi1Corr(alphaPhiUn - alphaPhi10);
        volScalarField alpha10("alpha10", alpha1);

        MULES::correct(geometricOneField(), alpha1, alphaPhiUn,
                       talphaPhi1Corr.ref(), Sp, (-Sp*alpha1)(),
                       oneField(), zeroField());

        if (aCorr == 0) { alphaPhi10 += talphaPhi1Corr(); }
        else { alpha1 = 0.5*alpha1 + 0.5*alpha10; alphaPhi10 += 0.5*talphaPhi1Corr(); }
    }
    else
    {
        alphaPhi10 = alphaPhiUn;
        MULES::explicitSolve(geometricOneField(), alpha1, phiCN, alphaPhi10,
                             Sp, (Su + divU*min(alpha1(), scalar(1)))(),
                             oneField(), zeroField());
    }

    alpha2 = 1.0 - alpha1;
    mixture.correct();
}

逐行解读(这是全章最核心的一段):

  • 第 158 行 nAlphaCorr :相分数校正次数 (默认 1--2,教程 damBreak 用 2)。次数越多,压缩通量与限幅迭代越充分,界面越锐,但代价线性增加。
  • 第 162 行 phir = phic * mixture.nHatf() :把「压缩速度大小 phic」与「面法向矢量通量 nHatf_」相乘,得到沿界面法向的压缩体积通量 (量纲 m³/s)。nHatf_ 来自 interfaceProperties(第 8.4 节),是 nHatfv & Sf。
  • 第 164--176 行 alphaPhiUn :不受限的相分数通量 ,由两部分相加:
    1. fvc::flux(phi, alpha1, alphaScheme):相 1 对流通量,插值格式 div(phi,alpha)(教程用 Gauss vanLeer,TVD 有界);
    2. fvc::flux(-fvc::flux(-phir, alpha2, alpharScheme), alpha1, alpharScheme):压缩项 。内层 fvc::flux(-phir, alpha2, alpharScheme) 计算相 2 沿 −phir 的通量,外层再以 alpharScheme(教程 Gauss linear)对 alpha1 通量。双重嵌套实现 (5.2) 中的 ∇⋅Ucα1(1−α1) \nabla\cdot\\mathbf{U}_c\\alpha_1(1-\\alpha_1) ∇⋅Ucα1(1−α1)------即压缩通量在 α₂ 侧与 α₁ 侧各算一次,等价于 α(1−α) 因子。
  • MULESCorr 分支(第 178--205 行) :把「不受限通量 alphaPhiUn 与底解通量 alphaPhi10 之差」当作待限幅校正量 talphaPhi1Corr,交给 MULES::correct 限制;随后把限制后的校正量加回 alphaPhi10。第 0 次校正全量加入;后续校正欠松弛 (alpha1 = 0.5α1 + 0.5α10,校正量乘 0.5),防止过度校正导致振荡。
  • 显式分支(第 207--221 行) :alphaPhi10 = alphaPhiUn,然后 MULES::explicitSolve(...) 直接做「先限幅通量、再显式推进」。源项传的是 Su + divU*min(alpha1(),1)------这里的 min(α,1) 是对源项的额外有界化(避免相分数被源项顶过 1)。
  • 每个校正步末尾都执行 alpha2 = 1 − alpha1; mixture.correct();,保证下一步模拟用的物性与界面几何始终同步。

5.6 rhoPhi 的组装与 CN 收尾(alphaEqn.H 第 228--267 行)

cpp 复制代码
if (alphaApplyPrevCorr && MULESCorr)
{
    talphaPhi1Corr0 = alphaPhi10 - talphaPhi1Corr0;
    talphaPhi1Corr0.ref().rename("alphaPhi1Corr0");
}
else
{
    talphaPhi1Corr0.clear();
}

#include "rhofs.H"

if (word(mesh.ddtScheme("ddt(rho,U)")) == fv::EulerDdtScheme<vector>::typeName
 || word(mesh.ddtScheme("ddt(rho,U)")) == fv::localEulerDdtScheme<vector>::typeName)
{
    rhoPhi = alphaPhi10*(rho1f - rho2f) + phiCN*rho2f;
}
else
{
    if (ocCoeff > 0)
    {
        // Calculate the end-of-time-step alpha flux
        alphaPhi10 = (alphaPhi10 - (1.0 - cnCoeff)*alphaPhi10.oldTime())/cnCoeff;
    }
    // Calculate the end-of-time-step mass flux
    rhoPhi = alphaPhi10*(rho1f - rho2f) + phi*rho2f;
}

逐行解读:

  • 第 228--236 行 :只在「上一迭代校正」模式下把本次的通量与缓存相减得到增量校正通量,并重命名 alphaPhi1Corr0;否则 talphaPhi1Corr0.clear() 释放(对应 interFoam.C 里 topoChanging 时手动 clear())。
  • rhofs.H (interFoam/rhofs.H 第 1--2 行)极简,只有两行:const dimensionedScalar& rho1f(rho1); const dimensionedScalar& rho2f(rho2);------把相密度别名成 rho1f/rho2f(f 后缀表示「面」上的量,此处因为是常数所以等于体心值)。
  • rhoPhi = alphaPhi10*(rho1f − rho2f) + phiCN*rho2f :这是质量通量 的线性插值组装。推导:面质量通量应为 ρfϕf=α1ρ1+(1−α1)ρ2ϕ \rho_f\phi_f = \\alpha_1\\rho_1 + (1-\\alpha_1)\\rho_2\phi ρfϕf=α1ρ1+(1−α1)ρ2ϕ。若把相 1 的体积通量记作 alphaPhi10、总通量记作 phiCN,则相 2 的体积通量为 phiCN − alphaPhi10,于是

ρfϕf=ρ1 alphaPhi10+ρ2(phiCN−alphaPhi10)=alphaPhi10(ρ1−ρ2)+phiCNρ2 \rho_f\phi_f = \rho_1\,\text{alphaPhi10} + \rho_2(\text{phiCN} - \text{alphaPhi10}) = \text{alphaPhi10}(\rho_1-\rho_2) + \text{phiCN}\rho_2 ρfϕf=ρ1alphaPhi10+ρ2(phiCN−alphaPhi10)=alphaPhi10(ρ1−ρ2)+phiCNρ2

与代码逐字对应。这是 UEqn 里 fvm::div(rhoPhi,U) 的输入。

  • CN 分支(第 252--260 行) :若动量用 CN(ocCoeff > 0),需把相分数通量从「CN 中点」外推回「时间步末端」:alphaPhi10 = (alphaPhi10 − (1−cnCoeff)alphaPhi10.oldTime())/cnCoeff,再用 phi(末端)而非常中点 phiCN 组装质量通量。保证质量守恒与时间精度一致。

5.7 子循环:alphaEqnSubCycle.H(VoF/alphaEqnSubCycle.H 第 1--36 行)

cpp 复制代码
if (nAlphaSubCycles > 1)
{
    dimensionedScalar totalDeltaT = runTime.deltaT();
    surfaceScalarField rhoPhiSum
    (
        mesh.newIOobject("rhoPhiSum"), mesh,
        dimensionedScalar(rhoPhi.dimensions(), Zero)
    );

    tmp<volScalarField> trSubDeltaT;
    if (LTS) { trSubDeltaT = fv::localEulerDdt::localRSubDeltaT(mesh, nAlphaSubCycles); }

    for
    (
        subCycle<volScalarField> alphaSubCycle(alpha1, nAlphaSubCycles);
        !(++alphaSubCycle).end();
    )
    {
        #include "alphaEqn.H"
        rhoPhiSum += (runTime.deltaT()/totalDeltaT)*rhoPhi;
    }

    rhoPhi = rhoPhiSum;
}
else
{
    #include "alphaEqn.H"
}
rho == alpha1*rho1 + alpha2*rho2;

逐行解读:

  • 子循环动机 :相分数方程是显式的,其稳定 Courant 数受 maxAlphaCo 限制。当界面运动快(如溃坝冲击、高速液滴)时,若全局时间步由动量方程的 maxCo 决定而 maxAlphaCo 更小,则整个时间步会被相方程拖慢。解法:在一个动量时间步内把相方程推进 nAlphaSubCycles 个子步,每个子步 Δt_sub = Δt/nAlphaSubCycles,从而把相方程的 Courant 数降低到安全区。
  • subCycle<volScalarField> alphaSubCycle(alpha1, nAlphaSubCycles) :subCycle 类在构造时按 nAlphaSubCycles 划分 alpha1 的 oldTime,并在每次自增时设定子步 Δt;循环条件 !(++alphaSubCycle).end() 是「推进一步且未结束」的惯用法。
  • rhoPhiSum :累加每个子步的 rhoPhi。子步内 runTime.deltaT() 已经是子步 Δt,权重 (runTime.deltaT()/totalDeltaT) 把子步通量按时间占比合成整步通量,保证质量守恒。
  • rho == alpha1*rho1 + alpha2*rho2:子循环结束后统一重算密度(与相分数终值一致)。
  • LTS 分支 :localRSubDeltaT(mesh, nAlphaSubCycles) 给局部时间步法提供子循环倒数时间步。局部时间步(LTS)下每个单元的 rDeltaT 不同,子循环同样适用。

5.8 通量字段的诞生:createAlphaFluxes.H(VoF/createAlphaFluxes.H 第 1--41 行)

cpp 复制代码
IOobject alphaPhi10Header
(
    IOobject::groupName("alphaPhi0", alpha1.group()),
    runTime.timeName(), mesh,
    IOobject::READ_IF_PRESENT, IOobject::AUTO_WRITE
);

const bool alphaRestart = alphaPhi10Header.typeHeaderOk<surfaceScalarField>(true);

if (alphaRestart) { Info << "Restarting alpha" << endl; }

// MULES flux from previous time-step
surfaceScalarField alphaPhi10
(
    alphaPhi10Header,
    phi*fvc::interpolate(alpha1)
);

// MULES Correction
tmp<surfaceScalarField> talphaPhi1Corr0;

// MULES compressed flux is registered in case scalarTransport FO needs it.
surfaceScalarField alphaPhiUn
(
    IOobject("alphaPhiUn", runTime.timeName(), mesh,
             IOobject::NO_READ, IOobject::NO_WRITE),
    mesh,
    dimensionedScalar(phi.dimensions(), Zero)
);

三个字段各自的使命:

字段 含义 生命周期 用途
alphaPhi10 上一步的相 1 MULES 通量(含压缩) READ_IF_PRESENT + AUTO_WRITE 作为 MULESCorr 的「底解通量」,可重启续算
talphaPhi1Corr0 MULES 压缩校正通量(tmp) 每步缓存/清除 alphaApplyPrevCorr 时复用的历史校正
alphaPhiUn 不受限相分数通量 NO_READ/NO_WRITE 诊断/被 scalarTransport 函数对象引用
  • alphaPhi10Header 用 IOobject::groupName("alphaPhi0", alpha1.group())------组名来自 alpha1,得到 alphaPhi0.water 之类的通量文件名。
  • 初值 phi*fvc::interpolate(alpha1) 是「以插值相分数乘以总通量」的一阶猜测,重启时被读入值覆盖。
  • alphaPhiUn 被注册(即使 NO_WRITE)是为了让 scalarTransport function object 能访问当前相分数通量。

5.9 相分数控制参数:alphaControls.H(src/finiteVolume/cfdTools/general/include/alphaControls.H 第 1--27 行)

cpp 复制代码
const dictionary& alphaControls = mesh.solverDict(alpha1.name());

label nAlphaCorr(alphaControls.get<label>("nAlphaCorr"));
label nAlphaSubCycles(alphaControls.get<label>("nAlphaSubCycles"));
bool MULESCorr(alphaControls.getOrDefault("MULESCorr", false));

bool alphaApplyPrevCorr(alphaControls.getOrDefault("alphaApplyPrevCorr", false));

scalar icAlpha(alphaControls.getOrDefault<scalar>("icAlpha", 0));
scalar scAlpha(alphaControls.getOrDefault<scalar>("scAlpha", 0));
  • mesh.solverDict(alpha1.name()):在 fvSolution 的 solvers 里按字段名 alpha.water 查找子字典。因此这些参数必须写在 "alpha.water.*" 子字典里 (cAlpha 也在那里,见第 8.5 节的 interfaceProperties 构造)。
  • 逐参数见下表(与第 7 节 MULES 参数、第 6 节压缩参数合并成一张总表):
参数 默认 语义 调大/调小影响
nAlphaCorr 必填 相分数校正次数 大→界面更锐但更慢;小→快但界面钝
nAlphaSubCycles 必填 相方程子循环数 大→相 Co 降低、更稳;小→快但可能不有界
MULESCorr false 用隐式有界底解 + 显式限幅校正 yes→更稳、可配 α 稳态优化
alphaApplyPrevCorr false 复用上一步压缩校正通量 yes→稳态省时;瞬态慎用
cAlpha 必填(1) 界面压缩强度 大→界面锐但易振荡;小→稳但界面钝
icAlpha 0 各向同性压缩系数 >0→破碎/强剪切界面更稳
scAlpha 0 剪切压缩系数 >0→抑制剪切拉伸扩散

6. 界面压缩:cAlpha 的离散与量纲

6.1 推导链 B:界面压缩项的离散与量纲

Step 1:压缩项的目标形式。 从 (5.2) 出发,压缩项为
C=∇⋅ ⁣Uc α(1−α),Uc=cα ∣U∣ n^C = \nabla\cdot\!\big\\mathbf{U}_c\\,\\alpha(1-\\alpha)\\big,\qquad \mathbf{U}c = c\alpha\,|\mathbf{U}|\,\hat n C=∇⋅Ucα(1−α),Uc=cα∣U∣n^

Step 2:有限体积离散。 把 C 在单元上做散度定理,转为面通量之和 Ci= 1Vi ∑fϕfc C_i = \frac{1}{V_i}\sum_f \phi^c_f Ci=Vi1∑fϕfc,其中面压缩通量为
ϕfc= Uc,f ⋅Sf αf(1−αf)=cα∣U∣f ( n^f ⋅Sf) αf(1−αf) \phi^c_f = \mathbf{U}_{c,f}\cdot\mathbf{S}f\,\alpha_f(1-\alpha_f) = c\alpha|\mathbf{U}|_f\,(\hat n_f\cdot\mathbf{S}_f)\,\alpha_f(1-\alpha_f) ϕfc=Uc,f⋅Sfαf(1−αf)=cα∣U∣f(n^f⋅Sf)αf(1−αf)

OpenFOAM 的实现把上式拆成两因子相乘:
ϕfc= (cα∣U∣f) ⏟ phic    ( n^f ⋅Sf) ⏟ nHatf_   ×  (通量的 α 侧因子) \phi^c_f = \underbrace{\big(c_\alpha|\mathbf{U}|f\big)}{\text{phic}}\;\underbrace{(\hat n_f\cdot\mathbf{S}f)}{\texttt{nHatf\}}\;\times\;\text{(通量的 } \alpha \text{ 侧因子)} ϕfc=phic (cα∣U∣f)nHatf (n^f⋅Sf)×(通量的 α 侧因子)

即 phir = phic * mixture.nHatf(),再经 fvc::flux 对 α₂ 与 α₁ 双重施加,隐式携带 α(1−α)。

Step 3:phic 的量纲。 phi/|Sf| 量纲 = (m³/s)/m² = m/s(面法向速度),取模仍 m/s,乘 cAlpha(无量纲)后 phic 量纲 m/s。与 nHatf_(fvsPatchVectorField & Sf,量纲 m²)相乘得 m³/s,与 phi 同量纲,故可与 phi 的对流通量直接相加------这是 alphaPhiUn 中两部分能相加的前提。

Step 4:为何要限制在界面附近? 单看 n^⋅Sf \hat n\cdot S_f n^⋅Sf:在纯相区 α 均匀,∇α = 0,nHatfv = gradAlphaf/(|gradAlphaf| + deltaN_) 趋近 0/δN,故 nHatf_ 极小;再乘 α(1−α)(纯相区为 0),压缩项自动消失。这从数值上保证了「只在混合单元压缩」。

Step 5:deltaN_ 的作用。 前面的分母 |gradAlphaf| + deltaN_ 是为了避免 |∇α|→0 时的除零。deltaN_ = 1e-8/cbrt(average(V))(第 8.2 节),是一个与网格尺度相关、随网格加密而减小的稳定量。

6.2 压缩系数的两种实现路径

OpenFOAM 提供两条互斥的界面压缩实现,需明确区分:

  1. 代数(algebraic)压缩 :interFoam 走这条。压缩以「压缩通量 phir = phic*nHatf_」的形式进入 alphaPhiUn,由 cAlpha(fvSolution 里)控制强度。压缩项与对流项一起被 MULES 限幅------因此压缩不会破坏有界性,这是本实现的精髓。
  2. 几何(geometric)压缩 / 格式限制器 :div(phirb,alpha) 项采用 interfaceCompression 空间格式(见 6.4),或 interIsoFoam 用 isoAdvection 做真正的几何界面重构。

6.3 icAlpha 与 scAlpha:各向同性 / 剪切压缩

  • icAlpha (isotropic compression):把压缩从「仅界面法向」推广到「各向同性」。代码里先 phic *= (1 - icAlpha) 再 phic += cAlpha*icAlpha*interp(|U|),等价于把标准法向压缩权重降到 (1−icAlpha),补上 icAlpha 权重的全方向速度大小压缩。适用于界面严重破碎(气泡群、喷雾)时避免法向压缩不足。
  • scAlpha (shear compression):scAlpha*|delta & interp(symm(grad(U)))|,按单元对面法向的应变率投影施加压缩。用于抑制剪切流(如剪切层、旋涡)对界面的无谓拉伸。

二者默认皆 0,属进阶调参项;多数溃坝/波浪算例不需要。

6.4 interfaceCompression:四阶压缩限制器

src/transportModels/interfaceProperties/interfaceCompression/interfaceCompression.H 第 52--79 行定义的 interfaceCompressionLimiter:

cpp 复制代码
scalar limiter
(
    const scalar cdWeight, const scalar faceFlux,
    const scalar phiP, const scalar phiN, const vector&, const scalar
) const
{
    // Quadratic compression scheme
    //return min(max(4*min(phiP*(1 - phiP), phiN*(1 - phiN)), 0), 1);

    // Quartic compression scheme
    return clamp
    (
        1 - max(sqr(1 - 4*phiP*(1 - phiP)), sqr(1 - 4*phiN*(1 - phiN))),
        0, 1
    );
}

逐行解读:

  • 输入 phiP、phiN 是 owner/neighbour 侧的相分数。该限制器不使用 NVD/TVD 函数(注释明确写出),是专用压缩限制器。
  • 二次方案(注释里) :limiter = clamp(4*min(αP(1−αP), αN(1−αN)), 0, 1)。它在 α=0.5 处取最大 1(最需要压缩),在纯相区取 0。
  • 四次方案(启用) :1 − max((1−4αP(1−αP))², (1−4αN(1−αN))²),再 clamp 到 0,1。把二次的 1−limiter 误差平方,使限制器在界面附近更接近 1、在纯相区衰减更快(四次比二次更陡),从而界面更锐。
  • 用在 fvSchemes 的 div(phirb,alpha) 项,如 tutorials/multiphase/interFoam/laminar/waves/waveMakerFlap/system/fvSchemes 第 31 行 div(phirb,alpha) Gauss interfaceCompression;------这是与默认 Gauss linear 的关键区别。

6.5 推导链 F:界面锐化与有界性条件

VOF 的界面锐化愿望与有界性约束是一对矛盾,必须同时量化。

Step 1:有界性对 Courant 的约束。 显式对流格式的稳定性要求(1D,单一网格):
Co= ∣U∣ΔtΔx ≤1 \text{Co} = \frac{|\mathbf{U}|\Delta t}{\Delta x} \le 1 Co=Δx∣U∣Δt≤1

相分数方程多一个压缩速度,有效 Courant 数变为
Coα≈ (∣U∣+∣Uc∣) ΔtΔx = (1+cα)∣U∣Δt Δx =(1+cα) Co \text{Co}\alpha \approx \frac{(|\mathbf{U}| + |\mathbf{U}c|)\,\Delta t}{\Delta x} = \frac{(1 + c\alpha)|\mathbf{U}|\Delta t}{\Delta x} = (1+c\alpha)\,\text{Co} Coα≈Δx(∣U∣+∣Uc∣)Δt=Δx(1+cα)∣U∣Δt=(1+cα)Co

这解释了 maxAlphaCo 为何要小于 maxCo :默认 cAlpha = 1 时 Co_α ≈ 2·Co。若 maxCo = 1,则相方程实际承受的 Courant 数可达 2,超过显式稳定极限------这正是「maxAlphaCo 要小」的数学原因,也是 setDeltaT.H 取 min(maxCo/Co, maxAlphaCo/alphaCo) 的动机(第 10 节)。

Step 2:有界性判据。 MULES 要保证更新后 0 ≤ α ≤ 1,等价于「每个单元接受的净通量不能把 α 推过 0,1」。用离散更新式(第 7.2 节)
αin+1 = ρi0αi0 rΔT+Sui−∑f ϕα,f ρi rΔT−Spi \alpha_i^{n+1} = \frac{\rho_i^0\alpha_i^0\,r\Delta T + Su_i - \sum_f\phi_{\alpha,f}}{\rho_i\,r\Delta T - Sp_i} αin+1=ρirΔT−Spiρi0αi0rΔT+Sui−∑fϕα,f

要求 αin+1 ∈0,1 \alpha_i^{n+1}\in0,1 αin+1∈0,1,等价于对流入该单元的净通量 ∑f ϕα,f \sum_f\phi_{\alpha,f} ∑fϕα,f 加上一对对称的上/下界约束:
∑f ϕα,f ≤ρ0α0rΔT+Su−(ρrΔT−Sp)⋅1=:psiMaxni(上界) \sum_f\phi_{\alpha,f} \le \rho^0\alpha^0 r\Delta T + Su - (\rho r\Delta T - Sp)\cdot 1 =: \text{psiMaxn}i \quad(\text{上界}) f∑ϕα,f≤ρ0α0rΔT+Su−(ρrΔT−Sp)⋅1=:psiMaxni(上界)
∑f ϕα,f ≥ρ0α0rΔT+Su−(ρrΔT−Sp)⋅0=:−psiMinni(下界) \sum_f\phi
{\alpha,f} \ge \rho^0\alpha^0 r\Delta T + Su - (\rho r\Delta T - Sp)\cdot 0 =: -\text{psiMinn}_i \quad(\text{下界}) f∑ϕα,f≥ρ0α0rΔT+Su−(ρrΔT−Sp)⋅0=:−psiMinni(下界)

这正是 limiter 里两个数组 psiMaxn/psiMinn 的名字来源(第 7.4 节逐行给出)。extremumCoeff 则在这对上/下界上再加一个与 psiMax − psiMin(本例为 1)成正比的松弛裕度。

Step 3:锐化与有界的折衷。 提高 cAlpha 增大压缩、锐化界面,但同时把有效 Co_α 提高、逼近有界性边界。实践建议:

  • cAlpha = 1 是 OpenFOAM 默认与推荐值;
  • cAlpha > 1 需同时下调 maxAlphaCo(不然界面振荡、Min(alpha) 变负);
  • cAlpha < 1 界面变钝,但更稳。

6.6 fvSchemes 字典模板(含 div(phirb,alpha) 与压缩格式)

可直接粘贴(对齐 tutorials/multiphase/interFoam/laminar/damBreak/damBreak/system/fvSchemes):

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      fvSchemes;
}
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //

ddtSchemes
{
    default         Euler;              // 相分数也受此控制,可改 CrankNicolson
}

gradSchemes
{
    default         Gauss linear;       // 界面法向用 Gauss linear
}

divSchemes
{
    div(rhoPhi,U)   Gauss linearUpwind grad(U);   // 动量对流(有界化)
    div(phi,alpha)  Gauss vanLeer;                // 相分数对流(TVD 有界)
    div(phirb,alpha) Gauss linear;                // 压缩项(可换 interfaceCompression)
    div(((rho*nuEff)*dev2(T(grad(U))))) Gauss linear;   // 粘性扩散
}

laplacianSchemes
{
    default         Gauss linear corrected;        // 非正交修正
}

interpolationSchemes
{
    default         linear;             // 面插值
}

snGradSchemes
{
    default         corrected;          // 面法向梯度(界面力/压力)
}

// ************************************************************************* //

逐项说明与调参表:

关键项 作用 可选项与影响
ddtSchemes default Euler 所有时间导数的默认格式 Euler(稳、一阶)vs CrankNicolson 0.9(二阶、需关子循环)
div(phi,alpha) Gauss vanLeer 相分数对流格式 vanLeer(TVD、有界、锐)是最常用;linearUpwind 更锐但欠有界,靠 MULES 兜底
div(phirb,alpha) Gauss linear / interfaceCompression 相分数压缩格式 linear(默认)vs interfaceCompression(四阶限制器,更锐)
div(rhoPhi,U) Gauss linearUpwind grad(U) 动量对流 linearUpwind 二阶迎风;limitedLinearV 1 更稳
snGradSchemes default corrected 界面法向梯度 非正交网格必须 corrected,否则表面张力失真
缺失 div(phirb,alpha) 报 FOAM FATAL ERROR: cannot find div(phirb,alpha) 必须在 divSchemes 中显式给出

7. MULES:显式、限幅与有界的数学

对应文件(全部在 src/finiteVolume/fvMatrices/solvers/MULES/):MULES.H、MULESTemplates.C(783 行)、MULES.C、CMULES.H、CMULESTemplates.C。

7.1 MULES 要解决的核心矛盾

MULES 全称 Multidimensional Universal Limiter with Explicit Solution 。CMULES.H 的类注释(第 30--40 行)给出它的精确表述:

csharp 复制代码
CMULES: Multidimensional universal limiter for
explicit corrected implicit solution.

Solve a convective-only transport equation using an explicit universal
multi-dimensional limiter to correct an implicit conservative bounded
obtained using rigorously bounded schemes such as Euler-implicit in time
upwind in space.

翻译成一句话:给定一个「隐式、时间欧拉、空间迎风」得到的有界但过扩散的解,用一个显式多维通用限制器去修正它的通量。核心矛盾是:

显式格式无需解大型线性系统、且便于施加几何性约束,但本身没有界性保证 (可能算出 α<0 或 α>1)。MULES 的做法是:先算出不受限的界面通量,再迭代地限制这些通量,使更新后的 ψ 不超出给定上下界 ψ_min, ψ_max。

对 interFoam 而言 ρ = 1(geometricOneField)、ψ = α₁、ψ_min = 0、ψ_max = 1(由 oneField()/zeroField() 传入),于是 MULES 保证 0 ≤ α₁ ≤ 1。

7.2 显式欧拉离散:explicitSolve(MULESTemplates.C 第 39--81 行)逐行

cpp 复制代码
template<class RdeltaTType, class RhoType, class SpType, class SuType>
void Foam::MULES::explicitSolve
(
    const RdeltaTType& rDeltaT, const RhoType& rho, volScalarField& psi,
    const surfaceScalarField& phiPsi, const SpType& Sp, const SuType& Su
)
{
    Info<< "MULES: Solving for " << psi.name() << endl;

    const fvMesh& mesh = psi.mesh();

    scalarField& psiIf = psi;
    const scalarField& psi0 = psi.oldTime();

    psiIf = 0.0;
    fvc::surfaceIntegrate(psiIf, phiPsi);

    if (mesh.moving())
    {
        psiIf =
        (
            mesh.Vsc0()().field()*rho.oldTime().field()
           *psi0*rDeltaT/mesh.Vsc()().field()
          + Su.field()
          - psiIf
        )/(rho.field()*rDeltaT - Sp.field());
    }
    else
    {
        psiIf =
        (
            rho.oldTime().field()*psi0*rDeltaT
          + Su.field()
          - psiIf
        )/(rho.field()*rDeltaT - Sp.field());
    }

    psi.correctBoundaryConditions();
}

逐行解读:

  • scalarField& psiIf = psi; 取体心场引用;psi0 = psi.oldTime() 取旧值。
  • psiIf = 0.0; fvc::surfaceIntegrate(psiIf, phiPsi);:这句是关键------先把场清零,再用 surfaceIntegrate 把面通量 phiPsi 对每个单元做「净流出」求和。surfaceIntegrate 对内部面用 owner − neighbour 符号累加,对边界面按 Sf·U 的正负处理,得到的就是 −∑f ϕψ,f -\sum_f\phi_{\psi,f} −∑fϕψ,f(约定流出为正)。因此后面 psiIf 里的 - psiIf 实际上把「净流出」变成「净流入」加回。
  • 更新公式(非动网格):

ψin+1 = ρi0ψi0 rΔT+Sui−∑f ϕψ,f ρi rΔT−Spi \psi_i^{n+1} = \frac{\rho_i^0\psi_i^0\,r\Delta T + Su_i - \sum_f\phi_{\psi,f}}{\rho_i\,r\Delta T - Sp_i} ψin+1=ρirΔT−Spiρi0ψi0rΔT+Sui−∑fϕψ,f

这就是显式欧拉 离散,分母 ρrΔT − Sp 对应隐式源的 fvm::Sp(把 Sp·ψ 移到左端),分子 Su 对应显式源。

  • rDeltaT 是「1/Δt」;LTS/局部时间步下每单元的 rDeltaT 不同(由 localEulerDdt::localRDeltaT 提供)。
  • 动网格分支:多乘一个 Vsc0/Vsc(旧/新单元体积比)因子,把旧值折算到新体积,保证网格运动下的守恒。
  • 末尾 psi.correctBoundaryConditions():用最新体心值更新边界面值。

7.3 「先限幅、再推进」:带界的 explicitSolve(MULESTemplates.C 第 146--182 行)

cpp 复制代码
template<class RhoType, class SpType, class SuType, class PsiMaxType, class PsiMinType>
void Foam::MULES::explicitSolve
(
    const RhoType& rho, volScalarField& psi,
    const surfaceScalarField& phi, surfaceScalarField& phiPsi,
    const SpType& Sp, const SuType& Su,
    const PsiMaxType& psiMax, const PsiMinType& psiMin
)
{
    const fvMesh& mesh = psi.mesh();

    psi.correctBoundaryConditions();

    if (fv::localEulerDdt::enabled(mesh))
    {
        const volScalarField& rDeltaT = fv::localEulerDdt::localRDeltaT(mesh);
        limit(rDeltaT, rho, psi, phi, phiPsi, Sp, Su, psiMax, psiMin, false);
        explicitSolve(rDeltaT, rho, psi, phiPsi, Sp, Su);
    }
    else
    {
        const scalar rDeltaT = 1.0/mesh.time().deltaTValue();
        limit(rDeltaT, rho, psi, phi, phiPsi, Sp, Su, psiMax, psiMin, false);
        explicitSolve(rDeltaT, rho, psi, phiPsi, Sp, Su);
    }
}

顺序是全部要义 :limit(...) 先把 phiPsi(相分数通量)修正到「不会使 ψ 越界」的水平,returnCorr = false 表示直接覆盖 phiPsi(而不是返回校正量);随后 explicitSolve(...) 用受限通量做显式推进。这就是「MULES 保证有界」的机制入口------限幅发生在前,推进发生在后。

7.4 限幅器内核 limiter(MULESTemplates.C 第 194--568 行)逐段

limiter 是 MULES 的算法心脏。它不直接改 ψ,而是计算每个面的缩放乘子 λ ∈ 0,1 (变量 allLambda/lambda),再用 λ 缩放出界方向的通量。

(a) 读控制参数(第 213--242 行)

cpp 复制代码
const dictionary& MULEScontrols = mesh.solverDict(psi.name());

const label  nLimiterIter = MULEScontrols.getOrDefault<label>("nLimiterIter", 3);
const scalar smoothLimiter = MULEScontrols.getOrDefault<scalar>("smoothLimiter", 0);
const scalar extremaCoeff = MULEScontrols.getOrDefault<scalar>("extremaCoeff", 0);
const scalar boundaryExtremaCoeff =
    MULEScontrols.getOrDefault<scalar>("boundaryExtremaCoeff", extremaCoeff);

const scalar boundaryDeltaExtremaCoeff = max(boundaryExtremaCoeff - extremaCoeff, 0);

mesh.solverDict(psi.name()) 即 fvSolution 里以字段名(如 alpha.water)命名的子字典------所以 MULES 参数写在该字段的 solver 子字典里(第 7.9 节模板)。

(b) 构造 λ 面场(第 259--277 行)

cpp 复制代码
scalarField& lambdaIf = lambda;
surfaceScalarField::Boundary& lambdaBf = lambda.boundaryFieldRef();

lambda 是 slicedSurfaceScalarField,初值 allLambda(mesh.nFaces(), 1.0)(第 614 行)------所有面初始 λ = 1(完全不限制),后续迭代只会把 λ 往下压。

(c) 上/下界初始化:psiMaxn = psiMin, psiMinn = psiMax(第 279--284 行)

cpp 复制代码
psiMaxn = psiMin;
psiMinn = psiMax;

这个「反着初始化」是经典的求极值技巧:先置成极小值 psiMin,再用 max(...) 逐邻居扩张;对称地 psiMinn 先置成极大值 psiMax,再用 min(...) 收缩。

(d) 遍历内部面,计算邻居极值与通量分解(第 290--316 行)

cpp 复制代码
forAll(phiCorrIf, facei)
{
    const label own = owner[facei];
    const label nei = neighb[facei];

    psiMaxn[own] = max(psiMaxn[own], psiIf[nei]);
    psiMinn[own] = min(psiMinn[own], psiIf[nei]);
    psiMaxn[nei] = max(psiMaxn[nei], psiIf[own]);
    psiMinn[nei] = min(psiMinn[nei], psiIf[own]);

    sumPhiBD[own] += phiBDIf[facei];
    sumPhiBD[nei] -= phiBDIf[facei];

    const scalar phiCorrf = phiCorrIf[facei];
    if (phiCorrf > 0) { sumPhip[own] += phiCorrf; mSumPhim[nei] += phiCorrf; }
    else              { mSumPhim[own] -= phiCorrf; sumPhip[nei] -= phiCorrf; }
}
  • psiMaxn/psiMinn 记录每个单元及其所有邻居的 ψ 极值(用于 extremum 判据,防止产生新极值);
  • sumPhiBD 累加「迎风底解通量」phiBD(把总通量拆成有界部分 phiBD 与待限幅校正部分 phiCorr = phiPsi − phiBD,见第 7.5 节 / 第 596--612 行);
  • sumPhip[cell]/mSumPhim[cell]:把校正通量按流向 归到 owner/neighbour------sumPhip 代表「流入某单元的正贡献之和」、mSumPhim 代表「流出某单元的负贡献之和」的分解量。

(e) 边界面上的极值与通量(第 318--384 行)

分三类边界处理:

  • psiPf.coupled()(cyclic/processor):用 patchNeighbourField() 取对侧值,更新极值;
  • psiPf.fixesValue()(如固定值边界):用边界值 psiPf 更新极值;
  • 其余(inlet/outlet/wall):若有 boundaryDeltaExtremaCoeff > 0,在极值上追加 boundaryDeltaExtremaCoeff*(psiMax − psiMin) 的额外裕度。

然后把边界校正通量也按符号累加到 sumPhip/mSumPhim。

(f) 施加 extremum 与 smooth 裕度(第 386--395 行)

cpp 复制代码
psiMaxn = min(psiMaxn + extremaCoeff*(psiMax - psiMin), psiMax);
psiMinn = max(psiMinn - extremaCoeff*(psiMax - psiMin), psiMin);

if (smoothLimiter > SMALL)
{
    psiMaxn = min(smoothLimiter*psiIf + (1.0 - smoothLimiter)*psiMaxn, psiMax);
    psiMinn = max(smoothLimiter*psiIf + (1.0 - smoothLimiter)*psiMinn, psiMin);
}
  • extremaCoeff 给上下界加「允许裕度」(相对 psiMax − psiMin,即 1),避免过紧约束引起的通量截断噪声;
  • smoothLimiter 用当前场值 psiIf 与极值做加权混合,数值上平滑限幅器,减少限幅导致的界面抖动。

(g) 换算成「通量预算」psiMaxn/psiMinn(第 397--438 行)

非动网格分支:

cpp 复制代码
psiMaxn = V*((rho.field()*rDeltaT - Sp.field())*psiMaxn
             - Su.field() - (rho.oldTime().field()*rDeltaT)*psi0) + sumPhiBD;
psiMinn = V*(Su.field() - (rho.field()*rDeltaT - Sp.field())*psiMinn
             + (rho.oldTime().field()*rDeltaT)*psi0) - sumPhiBD;

这两行把「ψ 的上下界」通过显式更新公式换算成「校正通量之和的可用预算 」:psiMaxn 是校正通量可用的正向最大余量,psiMinn 是负向最大余量。动网格分支(第 397--418 行)另含 Vsc0 因子。

(h) 迭代计算 λ(第 440--567 行)

cpp 复制代码
scalarField sumlPhip(psiIf.size());
scalarField mSumlPhim(psiIf.size());

for (int j=0; j<nLimiterIter; j++)
{
    sumlPhip = 0; mSumlPhim = 0;

    forAll(lambdaIf, facei)
    {
        const label own = owner[facei];
        const label nei = neighb[facei];
        scalar lambdaPhiCorrf = lambdaIf[facei]*phiCorrIf[facei];

        if (lambdaPhiCorrf > 0) { sumlPhip[own] += lambdaPhiCorrf; mSumlPhim[nei] += lambdaPhiCorrf; }
        else                    { mSumlPhim[own] -= lambdaPhiCorrf; sumlPhip[nei] -= lambdaPhiCorrf; }
    }
    // ... 边界面同理累加 ...

    forAll(sumlPhip, celli)
    {
        sumlPhip[celli] = max(min((sumlPhip[celli] + psiMaxn[celli])
                                 /(mSumPhim[celli] + ROOTVSMALL), 1.0), 0.0);
        mSumlPhim[celli] = max(min((mSumlPhim[celli] + psiMinn[celli])
                                  /(sumPhip[celli] + ROOTVSMALL), 1.0), 0.0);
    }

    const scalarField& lambdam = sumlPhip;
    const scalarField& lambdap = mSumlPhim;

    forAll(lambdaIf, facei)
    {
        if (phiCorrIf[facei] > 0)
            lambdaIf[facei] = min(lambdaIf[facei], min(lambdap[owner[facei]], lambdam[neighb[facei]]));
        else
            lambdaIf[facei] = min(lambdaIf[facei], min(lambdam[owner[facei]], lambdap[neighb[facei]]));
    }
    // ... 边界面同理 ... 
    syncTools::syncFaceList(mesh, allLambda, minEqOp<scalar>());
}

这一段的数学在 7.5 节完整推导。要点:

  • 每次迭代用当前 λ 重新累计「已限制校正通量」sumlPhip/mSumlPhim;
  • 由「预算 / 需求」得到单元级乘子 lambdap(正方向)与 lambdam(负方向),clamp 到 0,1;
  • 一个面的 λ 取其两端单元乘子的较小者 (owner/neighbour 同时受限),并对每个面取 min 单调下压;
  • 迭代 nLimiterIter 次(默认 3);
  • 最后 syncTools::syncFaceList(..., minEqOp) 跨 processor 面做取小同步,保证并行一致(限幅不能因分区而不同)。

7.5 推导链 C:MULES 通量限幅算法

Step 0:记号。 把相分数通量拆成两部分:有界底解部分 phiBD(迎风,天然有界)与待限幅校正部分 phiCorr:
ϕα,f = ϕfBD +λf  ϕfcorr ,λf∈0,1 (7.1) \phi_{\alpha,f} = \phi^{BD}_f + \lambda_f\,\phi^{corr}_f,\qquad \lambda_f\in0,1 \tag{7.1} ϕα,f=ϕfBD+λfϕfcorr,λf∈0,1(7.1)

limit 里 phiBD = upwind<scalar>(mesh, phi).flux(psi)(第 596 行),phiCorr = phiPsi − phiBD(第 611--612 行)。λ 是每个面的缩放乘子。

Step 1:单元净通量。 单元 i 受到的校正净流入为
Ii(λ)=− ∑f∋i λf  ϕfcorr  sgni(f) I_i(\lambda) = -\sum_{f\ni i} \lambda_f\,\phi^{corr}_f\, \text{sgn}_i(f) Ii(λ)=−f∋i∑λfϕfcorrsgni(f)

其中 sgn 使流出为负。为方便,定义两个正累积量:
sumPhipi= ∑ f: λf ϕfcorr 流入 i ∣λf ϕfcorr ∣,mSumPhimi= ∑f: 流出 i ∣λf ϕfcorr ∣(以正号累计) \text{sumPhip}i = \sum{f:\ \lambda_f\phi^{corr}_f \text{流入 } i}|\lambda_f\phi^{corr}_f|,\quad \text{mSumPhim}i = \sum{f:\ \text{流出 } i}|\lambda_f\phi^{corr}_f| \quad(\text{以正号累计}) sumPhipi=f: λfϕfcorr流入 i∑∣λfϕfcorr∣,mSumPhimi=f: 流出 i∑∣λfϕfcorr∣(以正号累计)

对应变量 sumlPhip(流入正贡献)与 mSumlPhim(流出量,记为正值)。

Step 2:有界约束转成 λ 的约束。 由 6.5 节,单元 i 的「通量预算」是 psiMaxn_i(正向可用余量)与 psiMinn_i(负向可用余量,注意其符号约定)。要求
Ii(λ)≤psiMaxni,−Ii(λ)≤−psiMinni I_i(\lambda) \le \text{psiMaxn}_i,\qquad -I_i(\lambda)\le -\text{psiMinn}_i Ii(λ)≤psiMaxni,−Ii(λ)≤−psiMinni

(即 psiMinn_i 以负数形式给出负向预算)。由于 Ii I_i Ii 关于 λ 单调不减(λ 越大流入越多),存在唯一临界值:

Step 3:单元级乘子。
lambdapi=clamp ⁣ ( psiMaxni+sumlPhipi mSumPhimi+ϵ , 0, 1) \boxed{\ \text{lambdap}_i = \mathrm{clamp}\!\left(\frac{\text{psiMaxn}_i + \text{sumlPhip}_i}{\text{mSumPhim}_i + \epsilon},\,0,\,1\right)\ } lambdapi=clamp(mSumPhimi+ϵpsiMaxni+sumlPhipi,0,1)
lambdami=clamp ⁣ ( psiMinni+mSumlPhimi sumPhipi+ϵ , 0, 1) \boxed{\ \text{lambdam}_i = \mathrm{clamp}\!\left(\frac{\text{psiMinn}_i + \text{mSumlPhim}_i}{\text{sumPhip}_i + \epsilon},\,0,\,1\right)\ } lambdami=clamp(sumPhipi+ϵpsiMinni+mSumlPhimi,0,1)

这正是代码第 493--507 行:sumlPhip[celli] = max(min((sumlPhip+psiMaxn)/(mSumPhim+ROOTVSMALL),1),0);mSumlPhim 对称。ROOTVSMALL 就是公式里的 ε,防止除零。

Step 4:面级乘子。 一个面同时是 owner 与 neighbour 的边界,必须同时满足两端约束,故取较小者(第 513--530 行):
λf←min⁡ ⁣(λf, min⁡( lambdapowner , lambdamneighbour ))( ϕfcorr >0) \lambda_f \leftarrow \min\!\big(\lambda_f,\ \min(\text{lambdap}{owner},\ \text{lambdam}{neighbour})\big)\quad (\phi^{corr}f>0) λf←min(λf, min(lambdapowner, lambdamneighbour))(ϕfcorr>0)
λf←min⁡ ⁣(λf, min⁡( lambdamowner , lambdapneighbour ))( ϕfcorr <0) \lambda_f \leftarrow \min\!\big(\lambda_f,\ \min(\text{lambdam}
{owner},\ \text{lambdap}_{neighbour})\big)\quad (\phi^{corr}_f<0) λf←min(λf, min(lambdamowner, lambdapneighbour))(ϕfcorr<0)

为什么方向不同取法不同? 当 phiCorr > 0 时,owner 是流出侧、neighbour 是流入侧,所以约束分别是 lambdap_owner(owner 允许流出多少)与 lambdam_neighbour(neighbour 允许流入多少)。

Step 5:迭代必要性。 一个单元常有多个面,缩减一个面的 λ 会改变该单元其它面的流入总量,进而可能违反另一侧的约束。因此必须迭代 :每轮用新 λ 重算 sumlPhip/mSumPhim,再重算单元乘子并取小下压。nLimiterIter 次后,λ 通常已收敛到使全部单元在界内。默认 3 次对多数工况足够;界面破碎剧烈时可提到 5--10。

Step 6:extremum 判据。 除了 ψ_min, ψ_max 的绝对 界,MULES 还用 psiMaxn/psiMinn(来自邻居极值)施加局部极值约束------更新的 ψ 不应超过其「邻居 + 自身」的历史极值(加 ±extremaCoeff 裕度)。这防止界面推进中产生非物理的新极值(振荡)。

Step 7:写回。 limit 末尾(第 647--654 行):

cpp 复制代码
if (returnCorr) { phiCorr *= lambda; }
else            { phiPsi = phiBD + lambda*phiCorr; }
  • returnCorr = false(explicitSolve 走这条):直接覆盖 phiPsi = phiBD + λ·phiCorr,即受限通量;
  • returnCorr = true(MULES::limit 被显式调用、多相 solveAlphas 走这条):只把校正部分乘 λ,供调用者自行组装。

7.6 nLimiterIter / smoothLimiter / extremaCoeff 参数表

参数 默认 语义 调大影响 调小影响
nLimiterIter 3 λ 迭代次数 更贴界、更稳,慢 快,但可能残留越界
smoothLimiter 0 限幅器与场值加权平滑 更平滑、抑振,稍钝 更锐利、可能抖
extremaCoeff 0 极值裕度(相对 ψMax−ψMin=1) 更宽松、少截断 更紧、易通量截断噪声
boundaryExtremaCoeff =extremaCoeff 边界极值裕度 边界更宽松 边界更紧

7.7 CMULES:隐式底解 + 显式校正(CMULESTemplates.C)

CMULES 与 MULES 的差别在于它多一条隐式底解路径 。MULES::correct(CMULESTemplates.C 第 131--191 行)先 limitCorr(限幅校正通量)再 correct(应用校正):

cpp 复制代码
void Foam::MULES::correct
(
    const RhoType& rho, volScalarField& psi,
    const surfaceScalarField& phi, surfaceScalarField& phiCorr,
    const SpType& Sp, const SuType& Su,
    const PsiMaxType& psiMax, const PsiMinType& psiMin
)
{
    const fvMesh& mesh = psi.mesh();
    if (fv::localEulerDdt::enabled(mesh))
    {
        const volScalarField& rDeltaT = fv::localEulerDdt::localRDeltaT(mesh);
        limitCorr(rDeltaT, rho, psi, phi, phiCorr, Sp, Su, psiMax, psiMin);
        correct(rDeltaT, rho, psi, phiCorr, Sp, Su);
    }
    else
    {
        const scalar rDeltaT = 1.0/mesh.time().deltaTValue();
        limitCorr(rDeltaT, rho, psi, phi, phiCorr, Sp, Su, psiMax, psiMin);
        correct(rDeltaT, rho, psi, phiCorr, Sp, Su);
    }
}

correct(CMULESTemplates.C 第 38--76 行)的更新式与 explicitSolve 略有不同 ------注意这里用的是当前 ψ(而非 oldTime),因为校正是在隐式底解之上叠加:

cpp 复制代码
psi.primitiveFieldRef() =
(
    rho.field()*psi.primitiveField()*rDeltaT
  + Su.field() - psiIf
)/(rho.field()*rDeltaT - Sp.field());

limiterCorr(第 194--567 行)与 MULESTemplates.C 的 limiter 结构几乎逐行对应,差别只在换算「通量预算」(第 396--410 行)时用 rho*psi*rDeltaT(当前值)而非 rho.oldTime()*psi0*rDeltaT(旧值)。这正对应「校正(correction)」与「求解(solve)」的语义差:前者在已有解上做增量修正,后者从旧时间步重新推进。

7.8 limitSum 与多相守恒(MULES.C 第 34--119 行)

对于 N 相 VOF(multiphaseInterFoam),每相各自限幅可能破坏「相分数之和 = 1」。MULES::limitSum 对每个面强制通量之和守恒:

cpp 复制代码
void Foam::MULES::limitSum(UPtrList<scalarField>& phiPsiCorrs)
{
    const label nPhases = phiPsiCorrs.size();

    forAll(phiPsiCorrs[0], facei)
    {
        scalar sumPos = 0, sumNeg = 0;
        for (label phasei = 0; phasei < nPhases; ++phasei)
        {
            if (phiPsiCorrs[phasei][facei] > 0) sumPos += phiPsiCorrs[phasei][facei];
            else                                sumNeg += phiPsiCorrs[phasei][facei];
        }

        scalar sum = sumPos + sumNeg;

        if (sum > 0 && sumPos > VSMALL)
        {
            scalar lambda = -sumNeg/sumPos;
            for (label phasei = 0; phasei < nPhases; ++phasei)
                if (phiPsiCorrs[phasei][facei] > 0) phiPsiCorrs[phasei][facei] *= lambda;
        }
        else if (sum < 0 && sumNeg < -VSMALL)
        {
            scalar lambda = -sumPos/sumNeg;
            for (label phasei = 0; phasei < nPhases; ++phasei)
                if (phiPsiCorrs[phasei][facei] < 0) phiPsiCorrs[phasei][facei] *= lambda;
        }
    }
}

逐行解读:

  • 对每个面,把各相校正通量分为正贡献 sumPos 与负贡献 sumNeg;
  • 若面净通量 sum > 0,则缩放正值项 使 sumPos·λ + sumNeg = 0,即 λ = −sumNeg/sumPos;
  • 若 sum < 0,对称地缩放负值项,λ = −sumPos/sumNeg;
  • 结果:每个面的各相校正通量之和为 0,保证 Σα 守恒。这就是 multiphaseMixture::solveAlphas 里 MULES::limit(...) 后紧接 MULES::limitSum(alphaPhiCorrs) 的意义(第 11 节)。
  • 第二个重载(第 85--119 行)支持「固定相」:只对非固定相施加守恒修正 phiPsiCorrs[phasei] += λ*alphas[phasei],用于某些相被强制保持的场合。

7.9 fvSolution 的 MULES 字典模板与调参表

可直接粘贴(对齐 tutorials/multiphase/interFoam/laminar/damBreak/damBreak/system/fvSolution 第 19--32 行):

cpp 复制代码
solvers
{
    "alpha.water.*"
    {
        nAlphaCorr      2;          // 相分数校正次数
        nAlphaSubCycles 1;          // 相方程子循环数(>1 关 CN)
        cAlpha          1;          // 界面压缩系数

        MULESCorr       yes;        // 隐式底解 + 显式限幅校正
        nLimiterIter    5;          // MULES λ 迭代次数

        solver          smoothSolver;
        smoother        symGaussSeidel;
        tolerance       1e-8;
        relTol          0;          // MULESCorr 底解需高精度
    }

    "pcorr.*"
    {
        solver          PCG;
        preconditioner  DIC;
        tolerance       1e-5;
        relTol          0;
    }

    p_rgh
    {
        solver          PCG;
        preconditioner  DIC;
        tolerance       1e-07;
        relTol          0.05;
    }

    p_rghFinal
    {
        $p_rgh;
        relTol          0;
    }

    U
    {
        solver          smoothSolver;
        smoother        symGaussSeidel;
        tolerance       1e-06;
        relTol          0;
    }
}

PIMPLE
{
    momentumPredictor   no;
    nOuterCorrectors    1;
    nCorrectors         3;
    nNonOrthogonalCorrectors 0;
}

relaxationFactors
{
    equations
    {
        ".*" 1;
    }
}

逐项说明与调参表:

条目 作用 调参建议
"alpha.water.*" 相分数子字典名(正则匹配组名) 必须与 transportProperties 的相名一致(alpha.water/alpha.air)
nAlphaCorr 2 相分数校正次数 1(快)~3(锐);界面破碎可 3
nAlphaSubCycles 1 子循环数 >1 会降低 αCo(更稳),但禁 CN
cAlpha 1 压缩强度 1 默认;>1 需同步降 maxAlphaCo
MULESCorr yes 隐式底解 + 显式校正 推荐 yes;纯显式用 no
nLimiterIter 5 限幅 λ 迭代 3(快)~10(强限幅)
tolerance 1e-8, relTol 0 底解精度 relTol 必须 0,否则底解欠收敛影响有界
momentumPredictor no 动量预测开关 多相常设 no(相方程已有预测性)
nCorrectors 3 压力校正次数 2~3

7.10 MULES vs 其它限幅:对比表

方法 类型 有界性保证 适用变量 与 VOF 的关系
MULES 显式多维通量限幅 严格(迭代后 ψmin,ψmax) 相分数、标量 interFoam 主力
TVD/NVD 限制器 (vanLeer/limitedLinear) 空间格式插值限制 单步、单方向有界 任意对流标量 提供 div(phi,alpha) 的初始有界性
interfaceCompression 专用压缩限制器 无(需 MULES 兜底) 相分数压缩通量 增强锐度
upwind 一阶迎风 严格有界但过扩散 任意 CMULES 底解
通量校正输运 MULES::limitSum 多相一致性约束 Σα 守恒 N 相 multiphaseInterFoam 必需
几何重构 isoAdvection 几何界面切割 结构上有界 相分数 interIsoFoam 替代方案

结论 :MULES 不是「更好的 TVD」,而是「在空间格式提供初步有界的基础上,用通量层面的迭代限幅给出严格有界」。它与 TVD 是互补而非替代关系。


8. 表面张力与界面几何:interfaceProperties

对应文件:src/transportModels/interfaceProperties/interfaceProperties.H(167 行)、interfaceProperties.C(266 行)。

8.1 类结构与私有数据(interfaceProperties.H 第 30--91 行)

头文件的 Description(第 33--35 行)明确它做两件事:

bash 复制代码
Properties to aid interFoam:
-# Correct the alpha boundary condition for dynamic contact angle.
-# Calculate interface curvature.

私有数据(第 73--90 行):

cpp 复制代码
const dictionary& transportPropertiesDict_;
int    nAlphaSmoothCurvature_;
scalar cAlpha_;
autoPtr<surfaceTensionModel> sigmaPtr_;
const dimensionedScalar deltaN_;
const volScalarField& alpha1_;
const volVectorField& U_;
surfaceScalarField nHatf_;
volScalarField K_;

逐个成员:

成员 类型 语义
transportPropertiesDict_ const dictionary& 指向 transportProperties(读 sigma)
nAlphaSmoothCurvature_ int 曲率平滑迭代次数(Yamamoto & Komarov 2022)
cAlpha_ scalar 界面压缩系数(从 fvSolution 读)
sigmaPtr_ autoPtr<surfaceTensionModel> 表面张力模型(RTS 选择)
deltaN_ dimensionedScalar 界面法向归一化稳定量
alpha1_ / U_ 引用 相分数与速度(只读引用)
nHatf_ surfaceScalarField 面上的界面单位法向通量(nHatfv & Sf)
K_ volScalarField 界面曲率(维度 1/m)

8.2 构造函数(interfaceProperties.C 第 171--225 行)

cpp 复制代码
Foam::interfaceProperties::interfaceProperties
(
    const volScalarField& alpha1,
    const volVectorField& U,
    const IOdictionary& dict
)
:
    transportPropertiesDict_(dict),
    nAlphaSmoothCurvature_
    (
        alpha1.mesh().solverDict(alpha1.name()).
            getOrDefault<int>("nAlphaSmoothCurvature", 0)
    ),
    cAlpha_
    (
        alpha1.mesh().solverDict(alpha1.name()).get<scalar>("cAlpha")
    ),
    sigmaPtr_(surfaceTensionModel::New(dict, alpha1.mesh())),
    deltaN_("deltaN", 1e-8/cbrt(average(alpha1.mesh().V()))),
    alpha1_(alpha1), U_(U),
    nHatf_(IOobject("nHatf", ...), alpha1_.mesh(), dimensionedScalar(dimArea, Zero)),
    K_(IOobject("interfaceProperties:K", ...), alpha1_.mesh(),
       dimensionedScalar(dimless/dimLength, Zero))
{
    calculateK();
}

逐行解读:

  • cAlpha_ 用 get<scalar>("cAlpha") 强制读取 (无默认值)------若 fvSolution 的 alpha.water 子字典缺 cAlpha,构造即报错。教程 damBreak 提供了 cAlpha 1;。
  • nAlphaSmoothCurvature_ 用 getOrDefault<int>(..., 0),默认不平滑。
  • sigmaPtr_ = surfaceTensionModel::New(dict, mesh):从 transportProperties 的 sigma(或 sigma 子字典)经运行时选择表造出表面张力模型。
  • deltaN_ = 1e-8/cbrt(average(mesh.V())):网格尺度相关 稳定量。average(V) 是平均单元体积,cbrt 后近似平均网格间距 Δ;随网格加密 Δ 减小,δN 增大。它的量纲是 dimless/dimLength 吗?不------1e-8 无量纲,除以 Δ 得 1/m 量纲,因此 deltaN_ 量纲与 |∇α|(1/m)一致,可直接加入 |gradAlphaf| + deltaN_ 做正则化。
  • nHatf_ 维度 dimArea(m²);K_ 维度 dimless/dimLength(1/m)。
  • 构造末尾立即 calculateK()------初始化时就把曲率算好。

8.3 calculateK 逐行(interfaceProperties.C 第 108--166 行)

cpp 复制代码
void Foam::interfaceProperties::calculateK()
{
    const fvMesh& mesh = alpha1_.mesh();
    const surfaceVectorField& Sf = mesh.Sf();

    // Cell gradient of alpha
    tmp<volVectorField> tgradAlpha;
    if (nAlphaSmoothCurvature_ < 1)
    {
        tgradAlpha = fvc::grad(alpha1_, "nHat");
    }
    else
    {
        // Smooth interface curvature to reduce spurious currents
        auto talpha1L = tmp<volScalarField>::New(alpha1_);
        auto& alpha1L = talpha1L.ref();
        for (int i = 0; i < nAlphaSmoothCurvature_; ++i)
        {
            alpha1L = fvc::average(fvc::interpolate(alpha1L));
        }
        tgradAlpha = fvc::grad(talpha1L, "nHat");
    }

    // Interpolated face-gradient of alpha
    surfaceVectorField gradAlphaf(fvc::interpolate(tgradAlpha));

    // Face unit interface normal
    surfaceVectorField nHatfv(gradAlphaf/(mag(gradAlphaf) + deltaN_));
    correctContactAngle(nHatfv.boundaryFieldRef(), gradAlphaf.boundaryField());

    // Face unit interface normal flux
    nHatf_ = nHatfv & Sf;

    // Simple expression for curvature
    K_ = -fvc::div(nHatf_);
}

逐行解读:

  • 第 115--131 行(梯度计算) :nAlphaSmoothCurvature_ < 1 时直接 fvc::grad(alpha1_, "nHat")------用名为 nHat 的 gradScheme(在 fvSchemes 里可专门指定)。否则先对 alpha1 做 nAlphaSmoothCurvature_ 次「插值到面再平均回体心」的扩散平滑(alpha1L = average(interpolate(alpha1L))),再求梯度。注释写明目的:平滑界面曲率以减少伪流(spurious currents)------表面张力失衡会在界面处产生非物理速度。
  • 第 134 行 :gradAlphaf = interp(tgradAlpha) 把体心梯度插值到面。
  • 第 141 行 :nHatfv = gradAlphaf/(mag(gradAlphaf) + deltaN_)------面单位法向。分母加 δN 是正则化(避免纯相区 0/0)。
  • 第 147 行 :correctContactAngle(...) 修正壁面处的法向以体现接触角(第 8.4 节)。
  • 第 150 行 :nHatf_ = nHatfv & Sf------面法向通量(矢量点乘面积矢量 Sf,量纲 m²)。它是界面压缩的输入(第 5.5、6 节)。
  • 第 153 行 :K_ = -fvc::div(nHatf_),即曲率 κ = −∇·n̂ 的离散。注意:是巧妙的「面法向通量的散度」直接等于体心曲率 ,无需再除以体积(因为 nHatf_ 已乘了 Sf,fvc::div 内含除以 V)。
  • 第 157--165 行是被注释掉的「复杂曲率表达式」(含 nHat & grad(nHatfv) & nHat 的修正项),注释说明它「形式为零但数值非零」,属可选改进项。

8.4 correctContactAngle(interfaceProperties.C 第 55--105 行)

cpp 复制代码
forAll(boundary, patchi)
{
    if (isA<alphaContactAngleTwoPhaseFvPatchScalarField>(abf[patchi]))
    {
        alphaContactAngleTwoPhaseFvPatchScalarField& acap = ...;
        fvsPatchVectorField& nHatp = nHatb[patchi];
        const scalarField theta(degToRad() * acap.theta(U_.boundaryField()[patchi], nHatp));
        const vectorField nf(boundary[patchi].nf());

        // Reset nHatp to correspond to the contact angle
        const scalarField a12(nHatp & nf);
        const scalarField b1(cos(theta));

        scalarField b2(nHatp.size());
        forAll(b2, facei)
        {
            b2[facei] = cos(acos(a12[facei]) - theta[facei]);
        }

        const scalarField det(1.0 - a12*a12);
        scalarField a((b1 - a12*b2)/det);
        scalarField b((b2 - a12*b1)/det);

        nHatp = a*nf + b*nHatp;
        nHatp /= (mag(nHatp) + deltaN_.value());

        acap.gradient() = (nf & nHatp)*mag(gradAlphaf[patchi]);
        acap.evaluate();
    }
}

逐行解读:

  • 只处理类型为 alphaContactAngleTwoPhaseFvPatchScalarField 的边界------即声明了接触角的壁面。
  • theta = degToRad()*acap.theta(U.boundaryField()[patchi], nHatp):把面法向与壁面切向速度传给接触角模型,得到动态接触角 θ(弧度)。
  • a12 = nHatp·nf 是当前法向与壁面单位法向 nf 的余弦;b1 = cosθ 是目标接触角的余弦。
  • b2 = cos(acos(a12) − θ):把当前法向夹角旋转 θ。随后用 2×2 线性代数(det = 1−a12²,a = (b1−a12·b2)/det,b = (b2−a12·b1)/det)解出把 nHatp 与 nf 线性组合成目标法向的系数。
  • nHatp = a*nf + b*nHatp; nHatp /= (mag(nHatp)+δN):重设壁面法向(体现接触角),并归一化。
  • acap.gradient() = (nf & nHatp)*mag(gradAlphaf[patchi]):把接触角对 α 的边界梯度贡献写回边界场 gradient(),再 evaluate() 更新面值。这样曲率 K_ 在壁面附近也正确包含接触角效应。

8.5 sigmaK / surfaceTensionForce / nearInterface(interfaceProperties.C 第 230--248 行)

cpp 复制代码
tmp<volScalarField> interfaceProperties::sigmaK() const
{
    return sigmaPtr_->sigma()*K_;
}

tmp<surfaceScalarField> interfaceProperties::surfaceTensionForce() const
{
    return fvc::interpolate(sigmaK())*fvc::snGrad(alpha1_);
}

tmp<volScalarField> interfaceProperties::nearInterface() const
{
    return pos0(alpha1_ - 0.01)*pos0(0.99 - alpha1_);
}
  • sigmaK() = σ·κ(体心场,量纲 N/m · 1/m = N/m³ = Pa/m,即体积力密度);
  • surfaceTensionForce() = interp(σκ) · snGrad(α)------把 σκ 插值到面,再乘面法向梯度 fvc::snGrad(alpha1_)(量纲 1/m),得面尺度表面张力(Pa·? 实际是 Pa/m · m... 见 8.6)。它被 fvc::reconstruct 重构为体积力进入动量方程(第 9.1 节)。
  • nearInterface() = pos0(α−0.01)·pos0(0.99−α):pos0(x) 是阶跃(x>0 取 1,否则 0)。两项相乘即 0.01<α<0.99 时取 1,否则 0------标记界面单元 。用于 alphaCourantNo.H(第 10.1 节)只统计界面处的 Courant 数,以及限制表面张力作用范围。

8.6 推导链 D:CSF 表面张力源项与离散

Step 1:CSF 的基本思想(Brackbill et al. 1992)。 界面上的表面张力是把界面张力 σ 解释为界面处的法向应力差。用一组分布函数 δ_s 把「作用在界面上的力」抹到体积上:
fσ=σκ n^ δs \mathbf{f}_\sigma = \sigma\kappa\,\hat n\,\delta_s fσ=σκn^δs

Step 2:用相分数梯度代替 δ_s·n̂。 由于 δ_s·n̂ = δ_s·(∇α/|∇α|) 且 α 在界面上跃变,可证明
δs n^=∇α \delta_s\,\hat n = \nabla\alpha δsn^=∇α

(严格的分布意义下)。于是 CSF 力简化为
fσ=σκ ∇α (8.1) \boxed{\ \mathbf{f}_\sigma = \sigma\kappa\,\nabla\alpha\ } \tag{8.1} fσ=σκ∇α (8.1)

这正是「不需要显式界面几何」的由来------只要 α 场,就能算表面张力。

Step 3:曲率。 曲率由界面单位法向定义:
κ=−∇⋅n^=−∇⋅ ⁣ ( ∇α∣∇α∣ ) (8.2) \kappa = -\nabla\cdot\hat n = -\nabla\cdot\!\left(\frac{\nabla\alpha}{|\nabla\alpha|}\right) \tag{8.2} κ=−∇⋅n^=−∇⋅(∣∇α∣∇α)(8.2)

对应 K_ = -fvc::div(nHatf_)。

Step 4:面尺度的离散。 OpenFOAM 不在体心算 (8.1),而在面 上算,随后用 fvc::reconstruct 通量重构得到体心体积力。面尺度形式:
(fσ)f≈(σκ)f (∇α)f⋅... (\mathbf{f}_\sigma)_f \approx (\sigma\kappa)_f\,(\nabla\alpha)_f\cdot ... (fσ)f≈(σκ)f(∇α)f⋅...

代码里 surfaceTensionForce() 返回的是面标量 interp(σκ) * snGrad(α)。它配合 mesh.magSf() 与 fvc::reconstruct(...) 使用:在 UEqn.H 里,界面力作为 (mixture.surfaceTensionForce() − ghf*snGrad(rho) − snGrad(p_rgh))*mesh.magSf() 的一部分,被 fvc::reconstruct 转成体心矢量。

Step 5:量纲核对。

  • σ 量纲 = kg/s²(N/m);
  • κ 量纲 = 1/m;
  • σκ 量纲 = kg/s² / m = N/m³ = Pa/m;
  • snGrad(α) 量纲 = 1/m;
  • 但 snGrad 返回的是「面法向梯度」,在 OpenFOAM 中常与面积打包使用;乘 mesh.magSf()(m²)后,σκ·snGrad(α)·|Sf| 量纲 = (Pa/m)·(1/m)·m² = Pa·(dimensionless)...

严格推导:fvc::reconstruct(面标量场 * magSf) 的语义是「面通量 → 体心矢量」。若面标量 s_f 与 magSf 相乘视为面积通量,则重构量纲 = s_f 量纲 × (体积/面) = s_f × (m³/m²) = s_f × m。要让结果成为体积力密度(N/m³ = kg/(m²s²)),需 s_f 量纲为 kg/(m³s²)。而 σκ·snGrad(α) = (N/m³)·(1/m) --- 多出一个 1/m。这正是因为 snGrad 在面上面密度「每米」用统一的 Δ(面间距)表达,重构时又乘回 Δ,两者抵消。结论:surfaceTensionForce()*magSf() 经 reconstruct 后量纲正确为 N/m³ ------这也是 OpenFOAM 需要把 snGrad 与 magSf 成对使用的原因。

8.7 sigma 量纲与 surfaceTensionModel 运行时选择

src/transportModels/interfaceProperties/surfaceTensionModels/surfaceTensionModel/surfaceTensionModel.C 第 39 行:

cpp 复制代码
const Foam::dimensionSet Foam::surfaceTensionModel::dimSigma(1, 0, -2, 0, 0);

dimensionSet(mass, length, time, temperature, moles, ...),即 (1, 0, −2, 0, 0) = kg·m⁰·s⁻² = kg/s² = N/m 。这就是 sigma 的正确量纲。

constantSurfaceTension.C 第 46--54 行:

cpp 复制代码
Foam::surfaceTensionModels::constant::constant(const dictionary& dict, const fvMesh& mesh)
:
    surfaceTensionModel(mesh),
    sigma_("sigma", dimMass/sqr(dimTime), dict)
{}

dimMass/sqr(dimTime) 与 dimSigma 一致(kg/s²)。第 65--75 行的 sigma() 把它包成 volScalarField;readDict(第 78--94 行)支持两种写法:直接 sigma 0.07;,或子字典 sigma { sigma 0.07; }。

表面张力模型的后处理/运行时选择 (surfaceTensionModel.H 第 104--114 行):declareRunTimeSelectionTable(autoPtr, surfaceTensionModel, dictionary, ...),因此 transportProperties 里可写 sigma { type temperatureDependent; ... } 让 σ 随温度变化(见 temperatureDependentSurfaceTension)。

8.8 sigma 字典模板与说明

写法 A(常数,最常用) ------与 transportProperties 顶层:

cpp 复制代码
sigma           0.07;    // 水-空气 [kg/s^2] = [N/m]

写法 B(子字典 + 模型,可随温度变化):

cpp 复制代码
sigma
{
    type                temperatureDependent;

    // 线性插值表:温度 [K] 对应表面张力 [N/m]
    Tsigma
    (
        (300 0.072)
        (400 0.06)
        (500 0.05)
    );
}
写法 读取处 适用 注意
sigma 0.07; constant::readDict(findDict("sigma") 判定) 等温、常数 σ 最简,量纲 kg/s²
sigma { sigma 0.07; } 同上子字典分支 需要子字典结构 与写法 A 等价
sigma { type ...; } surfaceTensionModel::New + RTS 变 σ(温度相关) 需模型可用,且需温度场

8.9 表面张力模型对比表

模型 / 途径 类 是否需几何重构 σ 是否可变 计算成本 适用
CSF(体积力) interfaceProperties::surfaceTensionForce 否(用 ∇α) 是 低 interFoam 默认
常数 σ surfaceTensionModels::constant 否 否 极低 等温水-空气
温度相关 σ surfaceTensionModels::temperatureDependent 否 是 低 热毛细、Marangoni
几何法 CSF interIsoFoam + isoAdvection 是 是 高 需高精度界面
尖锐界面(Sharp) 需额外库 是 是 很高 研究级

9. 动量、压力与连续性:p_rgh、ddtCorr

9.1 动量方程 UEqn.H(interFoam/UEqn.H 第 1--33 行)逐行

cpp 复制代码
MRF.correctBoundaryVelocity(U);

fvVectorMatrix UEqn
(
    fvm::ddt(rho, U) + fvm::div(rhoPhi, U)
  + MRF.DDt(rho, U)
  + turbulence->divDevRhoReff(rho, U)
 ==
    fvOptions(rho, U)
);

UEqn.relax();
fvOptions.constrain(UEqn);

if (pimple.momentumPredictor())
{
    solve
    (
        UEqn
     ==
        fvc::reconstruct
        (
            (
                mixture.surfaceTensionForce()
              - ghf*fvc::snGrad(rho)
              - fvc::snGrad(p_rgh)
            ) * mesh.magSf()
        )
    );
    fvOptions.correct(U);
}

逐行解读:

  • MRF.correctBoundaryVelocity(U):若启用 MRF(旋转/滑移网格),修正旋转参考系边界速度。
  • fvm::ddt(rho,U) + fvm::div(rhoPhi,U):变系数动量对流 。密度 rho、质量通量 rhoPhi 都来自相分数(第 3、5 节)。多相动量方程之所以必须用 rhoPhi 而非 phi,正是密度在界面处跳变。
  • turbulence->divDevRhoReff(rho, U):可压缩版湍流应力散度(第一季讲过单相 divDevReff)。
  • fvc::reconstruct((...)*mesh.magSf()):把「面尺度」的三项力重构为体心向量。三项为:
    1. mixture.surfaceTensionForce():CSF 表面张力(第 8.5 节);
    2. -ghf*fvc::snGrad(rho):密度差浮力项(界面两侧 ρ 不同);
    3. -fvc::snGrad(p_rgh):修正压力梯度。
  • 为什么用 p_rgh 而非 p :若直接用 p,重力 −ρg 与压力梯度 −∇p 在静止流体中量级相近且需精确相消(hydrostatic balance),离散误差会被放大成虚假流动。改用 p_rgh = p − ρg·h 后方程只处理动压,显著改善数值行为。

9.2 推导链 E:p_rgh 修正与连续性方程

Step 1:动量方程的半离散形式。 不可压两相动量方程:
∂(ρU)∂t +∇⋅(ρUU)=−∇p+∇⋅τ+fσ−ρg (9.1) \frac{\partial (\rho\mathbf{U})}{\partial t} + \nabla\cdot(\rho\mathbf{U}\mathbf{U}) = -\nabla p + \nabla\cdot\boldsymbol\tau + \mathbf{f}_\sigma - \rho\mathbf{g} \tag{9.1} ∂t∂(ρU)+∇⋅(ρUU)=−∇p+∇⋅τ+fσ−ρg(9.1)

Step 2:定义修正压力。 令 p_rgh = p − ρ g·h(h 自参考点算起),则
∇p=∇ prgh +∇(ρgh)=∇ prgh +gh∇ρ+ρg∇h \nabla p = \nabla p_{rgh} + \nabla(\rho g h) = \nabla p_{rgh} + g h\nabla\rho + \rho g\nabla h ∇p=∇prgh+∇(ρgh)=∇prgh+gh∇ρ+ρg∇h

把 −∇p − ρg 合并,注意到 ρ g ∇h = ρ g,故重力被「减去」:
−∇p−ρg=−∇ prgh −gh∇ρ-\nabla p - \rho\mathbf{g} = -\nabla p_{rgh} - \mathbf{g}h\nabla\rho −∇p−ρg=−∇prgh−gh∇ρ

离散后 −ghf*fvc::snGrad(rho) 就是 −g h ∇ρ 的面尺度对应项。于是 (9.1) 写成只含 p_rgh:
∂(ρU)∂t +∇⋅(ρUU)=−∇ prgh −gh∇ρ+∇⋅τ+fσ (9.2) \frac{\partial (\rho\mathbf{U})}{\partial t} + \nabla\cdot(\rho\mathbf{U}\mathbf{U}) = -\nabla p_{rgh} - \mathbf{g}h\nabla\rho + \nabla\cdot\boldsymbol\tau + \mathbf{f}_\sigma \tag{9.2} ∂t∂(ρU)+∇⋅(ρUU)=−∇prgh−gh∇ρ+∇⋅τ+fσ(9.2)

Step 3:半离散化(H/A 分解)。 隐式离散 (9.2) 得 UEqn 的矩阵形式,对每个单元
U=HA−1A∇ prgh +...,A=diag(UEqn), H=非对角 + 源项 \mathbf{U} = \frac{\mathbf{H}}{A} - \frac{1}{A}\nabla p_{rgh} + \dots,\qquad A = \text{diag}(\text{UEqn}),\ \mathbf{H} = \text{非对角 + 源项} U=AH−A1∇prgh+...,A=diag(UEqn), H=非对角 + 源项

定义 rAU = 1/A、HbyA = rAU*UEqn.H()(第 9.3 节)。

Step 4:连续性方程。 不可压两相的质量守恒要求面通量散度为零(∇·U = 0):
∇⋅(U)=0⟹∑fϕf=0\nabla\cdot(\mathbf{U}) = 0 \quad\Longrightarrow\quad \sum_f \phi_f = 0 ∇⋅(U)=0⟹f∑ϕf=0

把 U = HbyA + rAU*(-∇p_rgh ...) 代入并做面通量离散,得压力泊松方程
∇⋅ ⁣(rAUf ∇ prgh )=∇⋅ ϕHbyA (9.3) \nabla\cdot\!\big(rAU_f\,\nabla p_{rgh}\big) = \nabla\cdot\phi_{HbyA} \tag{9.3} ∇⋅(rAUf∇prgh)=∇⋅ϕHbyA(9.3)

Step 5:dtCorr 的作用。 上述推导隐含「 ϕ=Sf⋅Uf \phi = S_f\cdot U_f ϕ=Sf⋅Uf 与体心速度一致」。但时间离散上一阶欧拉会使 phi.oldTime() 与 interp(U.oldTime()) 不完全一致(尤其动网格/变时间步)。加减一个校正量 ddtCorr 使预测通量更符合时间离散:
ϕHbyA =ϕ(HbyA)+rAUf ddtCorr(U, ϕ, Uf) (9.4) \phi_{HbyA} = \phi(\mathbf{HbyA}) + rAU_f\,\text{ddtCorr}(\mathbf{U},\,\phi,\,\mathbf{U}_f) \tag{9.4} ϕHbyA=ϕ(HbyA)+rAUfddtCorr(U,ϕ,Uf)(9.4)

这正是 pEqn.H 第 17 行 + MRF.zeroFilter(fvc::interpolate(rho*rAU())*fvc::ddtCorr(U, phi, Uf))。

9.3 压力方程 pEqn.H(interFoam/pEqn.H 第 1--89 行)逐段

cpp 复制代码
{
    if (correctPhi) { rAU.ref() = 1.0/UEqn.A(); }
    else            { rAU = 1.0/UEqn.A(); }

    surfaceScalarField rAUf("rAUf", fvc::interpolate(rAU()));
    volVectorField HbyA(constrainHbyA(rAU()*UEqn.H(), U, p_rgh));
    surfaceScalarField phiHbyA
    (
        "phiHbyA",
        fvc::flux(HbyA)
      + MRF.zeroFilter(fvc::interpolate(rho*rAU())*fvc::ddtCorr(U, phi, Uf))
    );
    MRF.makeRelative(phiHbyA);

    if (p_rgh.needReference())
    {
        fvc::makeRelative(phiHbyA, U);
        adjustPhi(phiHbyA, U, p_rgh);
        fvc::makeAbsolute(phiHbyA, U);
    }

    surfaceScalarField phig
    (
        ( mixture.surfaceTensionForce() - ghf*fvc::snGrad(rho) )*rAUf*mesh.magSf()
    );
    phiHbyA += phig;

    constrainPressure(p_rgh, U, phiHbyA, rAUf, MRF);

    while (pimple.correctNonOrthogonal())
    {
        fvScalarMatrix p_rghEqn
        (
            fvm::laplacian(rAUf, p_rgh) == fvc::div(phiHbyA)
        );
        p_rghEqn.setReference(pRefCell, getRefCellValue(p_rgh, pRefCell));
        p_rghEqn.solve(p_rgh.select(pimple.finalInnerIter()));

        if (pimple.finalNonOrthogonalIter())
        {
            phi = phiHbyA - p_rghEqn.flux();
            p_rgh.relax();
            U = HbyA + rAU()*fvc::reconstruct((phig - p_rghEqn.flux())/rAUf);
            U.correctBoundaryConditions();
            fvOptions.correct(U);
        }
    }

    #include "continuityErrs.H"
    fvc::correctUf(Uf, U, phi);
    fvc::makeRelative(phi, U);
    p == p_rgh + rho*gh;

    if (p_rgh.needReference())
    {
        p += dimensionedScalar("p", p.dimensions(),
                               pRefValue - getRefCellValue(p, pRefCell));
        p_rgh = p - rho*gh;
    }
    if (!correctPhi) { rAU.clear(); }
}

逐段解读:

  • rAU 与 correctPhi(第 2--9 行) :correctPhi 为真时 rAU 需保留(后续网格变化复用),用 rAU.ref() 原地赋值;否则临时量用完可清。
  • HbyA(第 12 行) :rAU()*UEqn.H() 是「无压力预测速度」,经 constrainHbyA(U, p_rgh) 约束边界(固定压力/固定速度边界上的相容)。
  • phiHbyA(第 13--18 行) :fvc::flux(HbyA) 把预测速度变成面通量,加上 ddtCorr 修正(第 9.2 Step 5)。
  • adjustPhi(第 21--26 行) :needReference()(封闭域)时,用 adjustPhi 调整进出口通量使全局连续误差可消除------若无法消除则报错(第 9.5 节)。
  • phig(第 28--36 行) :把「表面张力 + 浮力」折算成对预测通量的贡献(乘 rAUf*magSf),加到 phiHbyA。这就是界面力进入压力方程的通道。
  • constrainPressure(第 39 行):更新压力边界以保证通量相容。
  • 压力泊松(第 43--50 行) :fvm::laplacian(rAUf, p_rgh) == fvc::div(phiHbyA),即 (9.3)。setReference 固定参考压力。p_rgh.select(finalInnerIter()) 选择求解器(p_rgh vs p_rghFinal)。
  • 回代(第 52--61 行) :phi = phiHbyA − p_rghEqn.flux()(压力梯度修正通量);U = HbyA + rAU*reconstruct((phig − p_rghEqn.flux())/rAUf)(速度含界面力);correctBoundaryConditions。
  • continuityErrs.H(第 64 行):输出全局与局部连续性误差(诊断守恒)。
  • correctUf + makeRelative(第 67--70 行) :动网格下修正面速度 Uf 并把 phi 转回相对于网格运动的通量。
  • p = p_rgh + rho*gh(第 72 行) :还原绝对压力;needReference() 时再平移参考值。

9.4 ddtCorr 与 talphaPhi1Corr0(fvcDDt.C + EulerDdtScheme.C)

fvc::ddtCorr 的第三重载(src/finiteVolume/finiteVolume/fvc/fvcDDt.C 第 205--227 行):

cpp 复制代码
template<class Type>
tmp<...> ddtCorr
(
    const GeometricField<Type, fvPatchField, volMesh>& U,
    const GeometricField<...>& phi,
    const autoPtr<GeometricField<Type, fvsPatchField, surfaceMesh>>& Uf
)
{
    if (U.mesh().dynamic())
    {
        return ddtCorr(U, Uf());       // 动网格:用面速度 Uf
    }
    else
    {
        return ddtCorr(U, phi);        // 静网格:用通量 phi
    }
}
  • U.mesh().dynamic() 为真(动网格)时,ddtCorr 用 Uf(面速度),否则用 phi(通量)。这解释了为什么 interFoam 需要 createUfIfPresent.H。
  • ddtCorr(U, phi) 走 ddtScheme::fvcDdtPhiCorr(U, phi)。以欧拉格式为例(EulerDdtScheme.C 第 551--579 行):
cpp 复制代码
template<class Type>
tmp<...> EulerDdtScheme<Type>::fvcDdtPhiCorr
(
    const GeometricField<Type, fvPatchField, volMesh>& U,
    const fluxFieldType& phi
)
{
    dimensionedScalar rDeltaT = 1.0/mesh().time().deltaT();

    fluxFieldType phiCorr
    (
        phi.oldTime() - fvc::dotInterpolate(mesh().Sf(), U.oldTime())
    );

    return tmp<fluxFieldType>
    (
        new fluxFieldType
        (
            IOobject("ddtCorr(" + U.name() + ',' + phi.name() + ')', ...),
            this->fvcDdtPhiCoeff(U.oldTime(), phi.oldTime(), phiCorr)*rDeltaT*phiCorr
        )
    );
}

逐行:

  • phiCorr = phi.oldTime() − dotInterpolate(Sf, U.oldTime()):旧时刻面通量与旧时刻插值速度之差------这是时间离散带来的不一致量;
  • 返回 coeff * rDeltaT * phiCorr,即把该不一致按 1/Δt 折算成对当前通量的修正。fvcDdtPhiCoeff 限制修正只在合理的面(非边界/非纯梯度面)上生效。
  • 动网格分支 fvcDdtUfCorr(第 519--547 行)用 phiUf0 = mesh.Sf() & Uf.oldTime() 取代 phi.oldTime(),正因为动网格下 Uf 才是真正的面速度。

talphaPhi1Corr0 与 ddtCorr 的关系 :相分数方程缓存的 talphaPhi1Corr0(第 5.4 节)是相分数压缩校正通量 的历史项,供 alphaApplyPrevCorr 复用;而 ddtCorr 是动量方程的时间一致性修正。两者都涉及「上一时间步的通量」,但服务不同方程:

项 所在方程 修正对象 触发条件
ddtCorr(U, phi, Uf) pEqn.H(phiHbyA) 动量预测通量 每步(动网格用 Uf)
talphaPhi1Corr0 alphaEqn.H 相分数压缩通量 alphaApplyPrevCorr && MULESCorr
网格拓扑变化 interFoam.C 第 122 行 talphaPhi1Corr0.clear() mesh.topoChanging()

9.5 连续性误差与 adjustPhi

src/finiteVolume/cfdTools/general/adjustPhi/adjustPhi.C(第 107--117 行)在无法用调整出口流量消除连续性误差时抛出:

cpp 复制代码
FatalErrorInFunction
    << "Continuity error cannot be removed by adjusting the"
       " outflow.\nPlease check the velocity boundary conditions"
       " and/or run potentialFoam to initialise the outflow." << nl
    << "Total flux              : " << totalFlux << nl
    << "Specified mass inflow   : " << massIn << nl
    << "Specified mass outflow  : " << fixedMassOut << nl
    << "Adjustable mass outflow : " << adjustableMassOut << nl
    << exit(FatalError);

含义:当域内存在「固定速度入口 + 固定速度出口」时,若入口与出口总流量不匹配又无可调出口(如全部是 fixedValue 速度边界),则无解。对策:把其中一个出口改为 inletOutlet/pressureOutlet(可调),或先用 potentialFoam 初始化速度场。


10. 时间步与 Courant 数:显式相方程的配套机制

10.1 界面 Courant 数 alphaCourantNo.H(VoF/alphaCourantNo.H 第 34--57 行)

cpp 复制代码
scalar maxAlphaCo(runTime.controlDict().get<scalar>("maxAlphaCo"));

scalar alphaCoNum = 0.0;
scalar meanAlphaCoNum = 0.0;

if (mesh.nInternalFaces())
{
    scalarField sumPhi
    (
        mixture.nearInterface()().primitiveField()
       *fvc::surfaceSum(mag(phi))().primitiveField()
    );

    alphaCoNum = 0.5*gMax(sumPhi/mesh.V().field())*runTime.deltaTValue();

    meanAlphaCoNum = 0.5*(gSum(sumPhi)/gSum(mesh.V().field()))*runTime.deltaTValue();
}

Info<< "Interface Courant Number mean: " << meanAlphaCoNum
    << " max: " << alphaCoNum << endl;

逐行解读:

  • maxAlphaCo 从 controlDict 强制读取(无默认值)。
  • 第 46--48 行 :sumPhi = nearInterface() * surfaceSum(|phi|)------只在界面单元统计通量 (nearInterface() 为界面标记,第 8.5 节)。这就是「界面 Courant 数」与「全体 Courant 数」的区别:相分数只在界面处需要严格控 Co。
  • 第 50 行 :alphaCoNum = 0.5*gMax(sumPhi/V)*deltaT。sumPhi/V 是「单位体积的面通量之和」,乘 0.5 是经典 Courant 定义(每面通量平均分配),乘 Δt 得无量纲 Courant 数。
  • gMax/gSum 是并行归约(全局最大/和),保证多核一致。
  • 对照 CourantNo.H(全体单元)与 alphaCourantNo.H(仅界面单元),可见 interFoam 用两套 Courant 数分别约束动量与相分数。

10.2 自适应时间步 setDeltaT.H(VoF/setDeltaT.H 第 36--54 行)

cpp 复制代码
if (adjustTimeStep)
{
    scalar maxDeltaTFact =
        Foam::min(maxCo/(CoNum + SMALL), maxAlphaCo/(alphaCoNum + SMALL));

    const scalar deltaTFact =
        Foam::min(Foam::min(maxDeltaTFact, 1.0 + 0.1*maxDeltaTFact), 1.2);

    runTime.setDeltaT
    (
        Foam::min(deltaTFact*runTime.deltaTValue(), maxDeltaT)
    );

    Info<< "deltaT = " << runTime.deltaTValue() << endl;
}

逐行解读:

  • 步长因子取两套 Courant 的较小者 :maxDeltaTFact = min(maxCo/(CoNum+ε), maxAlphaCo/(alphaCoNum+ε))。这直接对应 6.5 节的结论------相方程的有效 Courant 更大,需更小的时间步。
  • 增长阻尼 1.0 + 0.1*maxDeltaTFact :即使允许,时间步增长也最多 10% (相对因子),并硬上限 1.2------防止「突然放大时间步」导致下一时刻振荡。缩减则是立即的(maxDeltaTFact 可为任意小)。
  • min(deltaTFact*Δt, maxDeltaT):受 controlDict 的 maxDeltaT 封顶。
  • SMALL 加在分母防止除零。

10.3 子循环与 Courant 的关系

若某步界面运动剧烈、alphaCoNum 过大,setDeltaT.H 会把全局 Δt 压得很小,代价是动量方程也随之变小步(浪费)。更高效的做法是保持动量 Δt 不变,只把相方程推进多个子步 (nAlphaSubCycles > 1,第 5.7 节):相方程看到的 Δt_sub = Δt/nAlphaSubCycles,其界面 Courant 数相应降为 αCo/nAlphaSubCycles。这解释了为何 alphaEqnSubCycle.H 与 alphaCourantNo.H 是一对------一个降低、一个监控。

10.4 controlDict 模板与调参表

可直接粘贴(对齐 tutorials/multiphase/interFoam/laminar/damBreak/damBreak/system/controlDict 第 17--53 行):

cpp 复制代码
application     interFoam;

startFrom       startTime;
startTime       0;
stopAt          endTime;
endTime         1;

deltaT          0.001;

writeControl    adjustable;
writeInterval   0.05;

purgeWrite      0;
writeFormat     ascii;
writePrecision  6;
writeCompression off;
timeFormat      general;
timePrecision   6;
runTimeModifiable yes;

adjustTimeStep  yes;

maxCo           1;      // 动量 Courant 上限
maxAlphaCo      1;      // 界面 Courant 上限
maxDeltaT       1;      // 时间步硬上限

// ************************************************************************* //
条目 语义 调参建议
deltaT 0.001 初始时间步 自适应时仅作起点
adjustTimeStep yes 开启自适应 显式相方程几乎必开
maxCo 1 动量 Co 上限 1 常用;LES 可 0.5
maxAlphaCo 1 界面 Co 上限 默认 1;cAlpha>1 时降到 0.5 更稳
maxDeltaT 1 硬上限 防止步长无界增长

11. 其它多相求解器:相变、可压缩与多相 VOF

11.1 interPhaseChangeFoam:引入相变源项

interPhaseChangeFoam.C 第 48--55 行的头文件与 interFoam 类似,但多了 phaseChangeTwoPhaseMixture;主循环(第 97--130 行)在相方程后调用 interface.correct()。核心差异在 alphaEqn.H(interPhaseChangeFoam/alphaEqn.H 第 7--11 行):

cpp 复制代码
Pair<tmp<volScalarField>> vDotAlphal = mixture->vDotAlphal();
const volScalarField& vDotcAlphal = vDotAlphal[0]();
const volScalarField& vDotvAlphal = vDotAlphal[1]();
const volScalarField vDotvmcAlphal(vDotvAlphal - vDotcAlphal);
  • vDotAlphal 返回一对源项:vDotcAlphal(冷凝率)与 vDotvAlphal(蒸发率),单位 1/s;
  • 相方程(第 17--30 行)右端加入 fvm::Sp(vDotvmcAlphal, alpha1) + vDotcAlphal------这就是相变对相分数的源;
  • MULES 的 Sp/Su 参数直接吸收相变项(第 76--79 行 vDotvmcAlphal 与 divU*(alpha10−alpha100) − vDotvmcAlphal*alpha10),保证相变存在时仍然有界------这是 MULES 的通用性体现(第 7 节)。
  • pEqn.H(第 26--37 行)在压力方程中加入 −(vDotvP−vDotcP)*(pSat − rho*gh) + fvm::Sp(vDotvP−vDotcP, p_rgh),把相变的体积源耦合进压力泊松------相变改变体积率,故压力方程需相应源项。

11.2 compressibleInterFoam:两相可压缩

主循环(compressibleInterFoam.C 第 94--140 行)与 interFoam 类似,但用 compressibleAlphaEqnSubCycle.H 且多了 TEqn.H(能量)。可压缩性体现在:

  • createFields.H 提供 psi1/psi2(两相可压缩性 ∂ρ/∂p)与 dgdt(相分数源,来自压力变化);
  • pEqn.H(compressibleInterFoam/pEqn.H 第 26--95 行)在标准泊松之外,组装两个可压缩修正 p_rghEqnComp1/p_rghEqnComp2,形如 pos(alpha1)*((fvc::ddt(alpha1,rho1)+fvc::div(alphaPhi1*rho1f))/rho1 − fvc::ddt(alpha1) − fvc::div(alphaPhi1) + (alpha1*psi1/rho1)*correction(fvm::ddt(p_rgh)))。它把「密度随压力变化」的项作为源项加入压力方程;
  • 求解后(第 116--123 行)p = max(p_rgh + (alpha1*rho1+alpha2*rho2)*gh, pMin),并用 dgdt 与 p_rghEqnComp1/Comp2 & p_rgh 计算相分数源;
  • mixture.thermo1().correctRho(psi1*(p_rgh − p_rgh_0))(第 135--136 行)------用 psip 更新密度,这是可压缩 VOF 与不可压 VOF 的根本区别。
  • compressibleAlphaEqnSubCycle.H(第 50--62 行)在相方程后额外算连续性误差 contErr = fvc::ddt(rho) + fvc::div(rhoPhi) − ...。

11.3 multiphaseInterFoam:N 相 VOF 与守恒修正

主循环(multiphaseInterFoam.C 第 134--135 行)调用 mixture.solve(); rho = mixture.rho();。核心在 multiphaseMixture.C:

  • solveAlphas(第 560--679 行) :对每个相构造 alphaPhiCorr,其中压缩项对「其它每一相」都算一次(第 596--608 行 for (phase& alpha2 : phases_) { if (&alpha2 == &alpha) continue; ... phir = phic*nHatf(alpha,alpha2); ... }),得到 N 相之间的两两界面压缩;
  • 每相 MULES::limit(..., true)(第 610--622 行) :returnCorr = true,只把校正部分乘 λ;
  • MULES::limitSum(alphaPhiCorrs)(第 627 行):强制各相校正通量之和为零(第 7.8 节),保证 Σα 守恒;
  • 漂移校正(第 671--676 行) :sumCorr = 1.0 − sumAlpha; for (phase& alpha : phases_) alpha += alpha*sumCorr;------即使 MULES 已守恒,仍用代数式把 Σα 拉回 1,避免长期漂移。
  • surfaceTensionForce(第 257--301 行) :对每一对相 (alpha1,alpha2) 从 sigmas_ 表查 sigma,累加 sigma*interp(K(alpha1,alpha2))*(interp(alpha2)*snGrad(alpha1) − interp(alpha1)*snGrad(alpha2))------N 相两两表面张力的对相互作用叠加。

11.4 interIsoFoam:几何法界面重构

interIsoFoam/alphaEqn.H(第 1--23 行)用 advector.advect(Sp, Su) 取代代数压缩:

cpp 复制代码
if (mesh.moving()) { U -= fvc::reconstruct(mesh.phi()); }
#include "alphaSuSp.H"
advector.advect(Sp, Su);
if (mesh.moving()) { U += fvc::reconstruct(mesh.phi()); }
#include "rhofs.H"
rhoPhi = advector.getRhoPhi(rho1f, rho2f);
alpha2 = 1.0 - alpha1;
mixture.correct();
  • advector 是 isoAdvection 对象:用几何切割 (iso-surface 在单元内的平面近似)计算相分数通量,再交给 MULES 型限制器(getRhoPhi)。
  • 动网格时先把 U 转成相对网格速度,再推进界面,之后转回绝对速度。
  • 相比代数 VOF(interFoam),几何法界面更锐、伪流更小,但实现复杂、对网格质量敏感。

11.5 多相求解器对比表

维度 interFoam interPhaseChangeFoam compressibleInterFoam multiphaseInterFoam interIsoFoam
相数 2 2 2 N 2
可压缩 否 否 是(psip) 否 否
相变 无 蒸发/冷凝 无 无 无
界面法 代数 CSF+MULES 代数 + 相变源 代数 + 可压缩 代数 N 相 + limitSum 几何 isoAdvection
压力方程 p_rgh 泊松 泊松 + 相变源 泊松 + 可压缩修正 p_rgh 泊松 p_rgh 泊松
守恒修正 --- --- contErr limitSum + 漂移校正 MULES
典型算例 溃坝、波浪 螺旋桨空化 水下爆炸 四相溃坝 高锐度界面

12. VOF 与其它界面方法的对照

方法 界面表征 锐度 拓扑变化 质量守恒 表面张力 OpenFOAM 实现
VOF(代数) 相分数场 + 压缩 中(可调 cAlpha) 自动 好(MULES) CSF interFoam
VOF(几何) 相分数 + 几何重构 高 自动 好 CSF/重构 interIsoFoam
Level-Set 符号距离函数 φ 高 自动 差(需再初始化) CSF 需外部库
Front-Tracking 显式界面网格 极高 困难 极好 直接 非内置
欧拉-欧拉 相分数 + 相速度 --- 自动 好 相间力 twoPhaseEulerFoam
均质混合 混合密度 + 状态方程 --- 无 好 不适用 cavitatingFoam

选择原则 :要锐利界面且允许几何重构成本 → interIsoFoam;要稳健、通用、支持动网格 → interFoam;要 N 相 → multiphaseInterFoam;要相变 → interPhaseChangeFoam;要可压缩 → compressibleInterFoam。


13. 常见报错 / 警告速查(22 条)

以下条目中,带引号的文本为源码/日志里的真实报错样式(部分为截取),括号内给出定位文件与成因。

1. FOAM FATAL ERROR: cannot find file "points" ------ 网格缺失。排查:constant/polyMesh/points 是否存在、blockMesh 是否运行过。解决:先 blockMesh/snappyHexMesh 生成网格。

2. FOAM FATAL ERROR: ... Keyword sigma is undefined in dictionary ... ------ transportProperties 缺 sigma。成因:surfaceTensionModel::New 需读 sigma。解决:加 sigma 0.07; 或子字典。

3. FOAM FATAL ERROR: Keyword cAlpha is undefined in dictionary .../fvSolution ------ alpha.water 子字典缺 cAlpha(interfaceProperties.C 第 186 行 get<scalar>("cAlpha") 强制读)。解决:加 cAlpha 1;。

4. FOAM FATAL ERROR: Only Euler and CrankNicolson ddt schemes are supported (VoF/alphaEqn.H 第 50 行)------ 相分数用了 backward/steadyState 等 ddtScheme。解决:ddtSchemes default Euler;。

5. FOAM FATAL ERROR: Sub-cycling is not supported with the CrankNicolson ddt scheme (第 31 行)------ 同时设 nAlphaSubCycles>1 与 CN。解决:二选一(关子循环或改 Euler)。

6. FOAM FATAL ERROR: cannot find div(phirb,alpha) ------ fvSchemes 缺压缩项。解决:div(phirb,alpha) Gauss linear;(或 interfaceCompression)。

7. Phase-1 volume fraction ... Min(alpha.water) = -0.0012 ------ 相分数越界(负值) 。成因:Courant 过大 / cAlpha 过大 / MULES 迭代不足 / 时间格式振荡。排查:看 Interface Courant Number max;提高 nLimiterIter、降 maxAlphaCo、降 cAlpha、增 nAlphaSubCycles。

8. Max(alpha.water) = 1.003 ------ 相分数越界(超 1)。成因/对策同上;还可能是 div(phi,alpha) 用了欠有界格式(如 linear)而未加 MULES 兜底。

9. MULES: Solving for alpha.water 后紧跟 Min = -0.05, Max = 1.05 持续恶化 ------ MULES 未收敛。成因:nLimiterIter 太小或 nAlphaCorr 太小。解决:nLimiterIter 5~10、nAlphaCorr 2~3。

10. deltaT = 1e-08 ...(时间步骤降且不回升) ------ 界面 Courant 失控。成因:某处界面速度极高 / 网格极小。排查:定位 alphaCoNum 最大单元(自定义 functionObject 取 nearInterface)。解决:增 nAlphaSubCycles、局部加密网格、降低 maxAlphaCo。

11. Interface Courant Number ... max: 6.5(远超 1) ------ 相方程显式稳定被破坏。解决:maxAlphaCo 0.5、nAlphaSubCycles 4、cAlpha 0.5。

12. Courant Number mean: 0 ... max: 3.2 ------ 动量 Courant 过大。成因:maxCo 太松或边界速度过大。解决:maxCo 0.5、减小入口速度、检查 adjustTimeStep yes。

13. Continuity error cannot be removed by adjusting the outflow (adjustPhi.C 第 109 行)------ 进出口流量不匹配。解决:把出口改 inletOutlet,或 potentialFoam 初始化。

14. FOAM FATAL ERROR: ... Sigma [0 0 -1 0 0] ... / 单元量纲不匹配 ------ sigma 填错量纲。成因:把动力粘度或力当表面张力。解决:sigma 必须是 kg/s²(N/m),水-空气取 0.07。

15. 界面「抖动/锯齿」并伴随伪速度 spurious currents ------ 表面张力离散失衡。成因:网格太粗(曲率误差大)、nAlphaSmoothCurvature=0。解决:nAlphaSmoothCurvature 2、加密界面网格、降低 cAlpha。

16. p_rgh 或 p 发散(Solving for p_rgh, Initial residual = 1, Final residual = 1e+20) ------ 压力方程不收敛。成因:边界不相容、nNonOrthogonalCorrectors 不足、rAUf 极端。解决:nCorrectors 3、nNonOrthogonalCorrectors 1、检查 fixedFluxPressure 边界。

17. 表面张力作用下界面「爆炸」成破碎小滴 ------ sigma 放大 / 界面法向噪声。成因:sigma 量纲或数值错误、界面法向未平滑。解决:核对 sigma、nAlphaSmoothCurvature、deltaN 随网格。

18. FOAM FATAL ERROR: ... keyword nAlphaCorr ... ------ fvSolution 缺 nAlphaCorr/nAlphaSubCycles(alphaControls.H 用 get 强制读)。解决:补齐两个关键字。

19. 网格依赖:粗网格上相分数守恒但界面过厚,细网格上界面锐但时间步骤降 ------ VOF 固有网格依赖。成因:界面宽度 ≈ 2--3 网格,曲率精度正比网格。解决:局部加密(refineMesh/AMR)、做网格无关性研究、接受「界面宽度随网格」。

20. AlphaPhi0.water 重启后相分数跳变 ------ alphaPhi10 从文件读入但物性/界面未同步。解决:保证 0/ 与 constant/ 一致的 restart 目录、不要混用不同网格的重启文件。

21. 动网格拓扑变化后界面「撕裂」 ------ talphaPhi1Corr0 未被清(interFoam.C 第 120--123 行条件)。成因:moveMeshOuterCorrectors 或 topoChanging 判定异常。解决:确认 mesh.update() 正确报告 topoChanging;必要时每步清 talphaPhi1Corr0。

22. 接触角算不出预期润湿(液滴贴壁角度错) ------ 壁面 alpha 边界未用 alphaContactAngle。成因:correctContactAngle 只在 alphaContactAngleTwoPhaseFvPatchScalarField 类型边界生效(第 60 行 isA<>)。解决:把 0/alpha.water 壁面边界改为相应接触角类型并给 theta0。

通用排查顺序 :先看 Interface Courant Number → 再看 Min/Max(alpha.) → 再看 deltaT 是否骤降 → 再看 continuity errors → 最后看 p_rgh 残差。这五项基本覆盖 90% 的多相发散。


14. 端到端调试流程与最佳实践

  1. 网格先行 :先用 blockMesh/snappyHexMesh 生成界面附近竖直方向足够密的网格(界面至少跨 2--3 层),checkMesh 通过后再上物理。
  2. 初值化 :用 setFields 设定 alpha.water;压力先用 potentialFoam 或静止静水(p_rgh 由 p = p_rgh + rho*gh 反推)。
  3. 先稳态后瞬态 :把 cAlpha 从 0.5 起步,确认不越界后再调到 1;maxAlphaCo 从 0.5 起步。
  4. 自检五项 (每 100 步):Interface Courant Number max、Min/Max(alpha)、deltaT 是否稳定、continuity errors、p_rgh 残差。
  5. 界面诊断可视化 :用 ParaView 显示 alpha.water = 0.5 等值面;用 isoSurface/cuttingPlane 检查界面厚度与伪流。
  6. 守恒验证 :alpha.water 的体积分应当守恒(溃坝/波浪算例可用 functionObject 统计)。
  7. 网格无关性:至少在 2--3 套网格上验证关键量(前沿位置、冲击压力)。
  8. 并行注意 :MULES 的 λ 用 syncTools::syncFaceList(minEqOp) 跨 processor 取小;adjustPhi、gMax/gSum 保证全局一致------不要在并行时省 processor 面的检查。

15. 本章要点

  • interFoam = VOF 相分数 + MULES 显式限幅 + CSF 表面张力 + p_rgh 重力处理 + 动网格/子循环。相方程文件实际位于 applications/solvers/multiphase/VoF/(alphaEqn.H/alphaEqnSubCycle.H/createAlphaFluxes.H/setDeltaT.H/alphaCourantNo.H),interFoam/ 只保留 interFoam.C/UEqn.H/pEqn.H/createFields.H/correctPhi.H/alphaSuSp.H/rhofs.H。
  • 相分数方程 alphaEqn.H(268 行)= ddt 方案判定(仅 Euler/CN)→ 压缩通量 phic(含 icAlpha/scAlpha、边界置零)→ MULESCorr 隐式底解或显式 MULES::explicitSolve → rhoPhi = alphaPhi10*(rho1f−rho2f) + phiCN*rho2f。
  • MULES::limit 先限幅通量、explicitSolve 再显式推进;核心是 λ 的迭代计算(MULESTemplates.C 第 194--568 行),nLimiterIter/smoothLimiter/extremaCoeff 可调,保证 0≤α≤1;MULES::limitSum(MULES.C)保证多相 Σα 守恒。
  • interfaceProperties 用 cAlpha(压缩)、deltaN_=1e-8/cbrt(average(V))(稳定化)、K_=-div(nHatf_)(曲率)、nHatf_(面法向)实现界面压缩与 CSF 力;sigmaK()=σK_、surfaceTensionForce()=interp(σκ)*snGrad(α)。
  • sigma 量纲为 dimSigma=(1,0,-2,0,0)=kg/s²,可常数或温度相关;constantSurfaceTension 从顶层或子字典读。
  • 动量 UEqn.H 右端 reconstruct(surfaceTensionForce − ghf*snGrad(rho) − snGrad(p_rgh)) 是界面力/浮力/动压的三合一;压力 pEqn.H 经 phig 把界面力带入泊松,ddtCorr(U,phi,Uf) 做时间一致性修正,动网格用 Uf。
  • 两套 Courant 数:CourantNo.H(全体)与 alphaCourantNo.H(仅界面,用 nearInterface() 标记);setDeltaT.H 取二者较小并阻尼增长。
  • 相系统族化:twoPhaseInter(VOF)、twoPhaseEuler(欧拉-欧拉)、multiphaseInter(多相 VOF)、reactingEuler(反应欧拉)。immiscibleIncompressibleTwoPhaseMixture 多重继承「物性 + 界面」。
  • 其它多相求解器:interPhaseChangeFoam(相变源)、compressibleInterFoam(psip 可压缩)、multiphaseInterFoam(N 相 + limitSum + 漂移校正)、interIsoFoam(几何重构)。

一句话总结 :interFoam 用 VOF 相分数与界面压缩维持锐利界面,用 MULES 通量限幅严格保证 α∈0,1,用 CSF 模型把表面张力折算为体积力,用 p_rgh 消除重力与压力梯度的相消,并在两套 Courant 数与子循环的约束下保持守恒与稳定。这条链路上每一环都对应源码里一个清晰的对象与函数,也是理解其余多相求解器的公共基础。

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