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 行)」。
目录
- 本章定位与阅读地图
- 多相建模的基本框架与术语
interFoam主循环与createFields.H逐行拆解- 相系统与物性:两相混合物族
- 相分数输运:
alphaEqn.H逐行剖析 - 界面压缩:
cAlpha的离散与量纲 - MULES:显式、限幅与有界的数学
- 表面张力与界面几何:
interfaceProperties - 动量、压力与连续性:
p_rgh、ddtCorr - 时间步与 Courant 数:显式相方程的配套机制
- 其它多相求解器:相变、可压缩与多相 VOF
- VOF 与其它界面方法的对照
- 常见报错 / 警告速查(20+ 条)
- 端到端调试流程与最佳实践
- 本章要点
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 阅读顺序建议
若你是第一次读多相源码,建议按 数据流 而不是按文件名读:
- 先读
createFields.H(第 3.8 节)------搞清p_rgh、U、phi、mixture、rho、rhoPhi、gh、p是谁造的、谁引用谁; - 再读
alphaEqn.H(第 5 节)------搞清相分数如何被推进,alphaPhi10、alphaPhiUn、talphaPhi1Corr0三个通量字段各自扮演什么角色; - 再读
UEqn.H+pEqn.H(第 9 节)------搞清rhoPhi如何进入动量、phig如何把界面力带进压力方程; - 最后下潜到
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)
表面张力则通过 CSF 模型 折算为单位体积力 fσ=σκ∇α1(第 8 节给出完整离散与量纲)。
2.3 三条必须同时成立的性质
VOF 的数值难点可以浓缩为三句「必须」:
- 输运要守恒:α₁ 的更新必须等价于「单元内两相体积的通量进出 + 源汇」的质量守恒离散表达;
- 有界性(boundedness):必须保证 0 ≤ α₁ ≤ 1,否则会出现非物理的「负相」或「超相」,并立即传染到混合物性(负密度、负粘度)导致求解崩溃;
- 界面锐利:数值扩散必须尽量小,否则界面被抹成十几个网格宽的「糊状带」,物性梯度被人为平滑,表面张力、浮力都失真。
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);
}
这段骨架里藏着六个关键设计决定,逐个展开:
- 相方程在动量/压力之前 (
alphaEqnSubCycle.H早于UEqn.H):先推进界面、更新物性(mixture.correct()重算rho、mu、曲率K_、面法向nHatf_),再据新物性推进动量。这保证动量方程用的是「当前时刻」的密度与界面力。 mixture.correct()被调用两次 :一次在相方程后(第 154 行),确保UEqn/pEqn用新 α;一次在动网格重算通量后(第 141 行),确保界面几何随网格更新。它内部实际调用interfaceProperties::correct()→calculateK()(第 8.3 节)。- 动网格优先 :网格局部变化时先
mesh.update(),再更新重力势gh/ghf(因为gh = g·x - ghRef依赖单元/面心位置mesh.C()/mesh.Cf())。 topoChanging()时清talphaPhi1Corr0:拓扑变了(加/删单元),上一步缓存的「界面压缩通量历史项」失效,必须丢弃,否则会污染ddtCorr(第 9.4 节)。frozenFlow()提前continue:当 PIMPLE 的frozenFlow选为运行期控制(例如等温稳态求平均)时,跳过动量与压力,只推进相分数。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,ν=ρμ
量纲核对:
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α1dV+∮∂Vα1UdS=0
逐项:左第一项是单元内相 1 体积的时间变化;第二项是相 1 被速度场 U 带出/带入单元的净通量。微分形式(不可压,∇·U = 0):
∂t∂α1+∇⋅(Uα1)=0(5.1)
Step 2:截断误差制造界面抹平。 离散 (5.1) 时,数值格式引入的数值扩散使界面在若干网格内从 0 过渡到 1,「混合单元」变厚。物理上这会夸大界面面积、削弱表面张力与浮力。
Step 3:引入界面压缩。 加一个人工相对速度 U_c,只在界面法向、指向 alpha 增大侧,把界面「自我锐化」。对相 1 加对流项、对相 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
代入 (5.2),压缩项变成
∇⋅cα∣U∣n\^α(1−α)
在 OpenFOAM 里,这个「 cα∣U∣」被实现为面尺度压缩通量 phic (第 5.3 节),而 n^ 的界面投影交给 mixture.nHatf()(第 8.4 节)。 cα 就是 cAlpha。
Step 5:量纲核对。 压缩通量 phic 必须与 phi(体积通量,m³/s)同量纲,才能与对流项相加:
phic=cα⋅∣U∣=1⋅sm=m/s
而离散方程里实际用的是「速度 × 面积」,因此 phic 在代码里定义为 cAlpha*mag(phi/mesh.magSf())(第 59 行),即「 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:不受限的相分数通量 ,由两部分相加:fvc::flux(phi, alpha1, alphaScheme):相 1 对流通量,插值格式div(phi,alpha)(教程用Gauss vanLeer,TVD 有界);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)------即压缩通量在 α₂ 侧与 α₁ 侧各算一次,等价于 α(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ϕ。若把相 1 的体积通量记作alphaPhi10、总通量记作phiCN,则相 2 的体积通量为phiCN − alphaPhi10,于是
ρ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)是为了让scalarTransportfunction 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^
Step 2:有限体积离散。 把 C 在单元上做散度定理,转为面通量之和 Ci=Vi1∑fϕfc,其中面压缩通量为
ϕfc=Uc,f⋅Sfαf(1−αf)=cα∣U∣f(n^f⋅Sf)αf(1−αf)
OpenFOAM 的实现把上式拆成两因子相乘:
ϕ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:在纯相区 α 均匀,∇α = 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 提供两条互斥的界面压缩实现,需明确区分:
- 代数(algebraic)压缩 :
interFoam走这条。压缩以「压缩通量phir = phic*nHatf_」的形式进入alphaPhiUn,由cAlpha(fvSolution里)控制强度。压缩项与对流项一起被 MULES 限幅------因此压缩不会破坏有界性,这是本实现的精髓。 - 几何(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=Δx∣U∣Δt≤1
相分数方程多一个压缩速度,有效 Courant 数变为
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=ρirΔT−Spiρi0αi0rΔT+Sui−∑fϕα,f
要求 αin+1∈0,1,等价于对流入该单元的净通量 ∑fϕα,f 加上一对对称的上/下界约束:
f∑ϕα,f≤ρ0α0rΔT+Su−(ρrΔT−Sp)⋅1=:psiMaxni(上界)
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(约定流出为正)。因此后面psiIf里的- psiIf实际上把「净流出」变成「净流入」加回。- 更新公式(非动网格):
ψ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)
limit 里 phiBD = upwind<scalar>(mesh, phi).flux(psi)(第 596 行),phiCorr = phiPsi − phiBD(第 611--612 行)。λ 是每个面的缩放乘子。
Step 1:单元净通量。 单元 i 受到的校正净流入为
Ii(λ)=−f∋i∑λfϕfcorrsgni(f)
其中 sgn 使流出为负。为方便,定义两个正累积量:
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
(即 psiMinn_i 以负数形式给出负向预算)。由于 Ii 关于 λ 单调不减(λ 越大流入越多),存在唯一临界值:
Step 3:单元级乘子。
lambdapi=clamp(mSumPhimi+ϵpsiMaxni+sumlPhipi,0,1)
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)
λ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
Step 2:用相分数梯度代替 δ_s·n̂。 由于 δ_s·n̂ = δ_s·(∇α/|∇α|) 且 α 在界面上跃变,可证明
δsn^=∇α
(严格的分布意义下)。于是 CSF 力简化为
fσ=σκ∇α (8.1)
这正是「不需要显式界面几何」的由来------只要 α 场,就能算表面张力。
Step 3:曲率。 曲率由界面单位法向定义:
κ=−∇⋅n^=−∇⋅(∣∇α∣∇α)(8.2)
对应 K_ = -fvc::div(nHatf_)。
Step 4:面尺度的离散。 OpenFOAM 不在体心算 (8.1),而在面 上算,随后用 fvc::reconstruct 通量重构得到体心体积力。面尺度形式:
(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()):把「面尺度」的三项力重构为体心向量。三项为:mixture.surfaceTensionForce():CSF 表面张力(第 8.5 节);-ghf*fvc::snGrad(rho):密度差浮力项(界面两侧 ρ 不同);-fvc::snGrad(p_rgh):修正压力梯度。
- 为什么用
p_rgh而非 p :若直接用 p,重力−ρg与压力梯度−∇p在静止流体中量级相近且需精确相消(hydrostatic balance),离散误差会被放大成虚假流动。改用p_rgh = p − ρg·h后方程只处理动压,显著改善数值行为。
9.2 推导链 E:p_rgh 修正与连续性方程
Step 1:动量方程的半离散形式。 不可压两相动量方程:
∂t∂(ρU)+∇⋅(ρUU)=−∇p+∇⋅τ+fσ−ρg(9.1)
Step 2:定义修正压力。 令 p_rgh = p − ρ g·h(h 自参考点算起),则
∇p=∇prgh+∇(ρgh)=∇prgh+gh∇ρ+ρg∇h
把 −∇p − ρg 合并,注意到 ρ g ∇h = ρ g,故重力被「减去」:
−∇p−ρg=−∇prgh−gh∇ρ
离散后 −ghf*fvc::snGrad(rho) 就是 −g h ∇ρ 的面尺度对应项。于是 (9.1) 写成只含 p_rgh:
∂t∂(ρU)+∇⋅(ρUU)=−∇prgh−gh∇ρ+∇⋅τ+fσ(9.2)
Step 3:半离散化(H/A 分解)。 隐式离散 (9.2) 得 UEqn 的矩阵形式,对每个单元
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
把 U = HbyA + rAU*(-∇p_rgh ...) 代入并做面通量离散,得压力泊松方程
∇⋅(rAUf∇prgh)=∇⋅ϕHbyA(9.3)
Step 5:dtCorr 的作用。 上述推导隐含「 ϕ=Sf⋅Uf 与体心速度一致」。但时间离散上一阶欧拉会使 phi.oldTime() 与 interp(U.oldTime()) 不完全一致(尤其动网格/变时间步)。加减一个校正量 ddtCorr 使预测通量更符合时间离散:
ϕ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_rghvsp_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. 端到端调试流程与最佳实践
- 网格先行 :先用
blockMesh/snappyHexMesh生成界面附近竖直方向足够密的网格(界面至少跨 2--3 层),checkMesh通过后再上物理。 - 初值化 :用
setFields设定alpha.water;压力先用potentialFoam或静止静水(p_rgh由p = p_rgh + rho*gh反推)。 - 先稳态后瞬态 :把
cAlpha从 0.5 起步,确认不越界后再调到 1;maxAlphaCo从 0.5 起步。 - 自检五项 (每 100 步):
Interface Courant Number max、Min/Max(alpha)、deltaT是否稳定、continuity errors、p_rgh残差。 - 界面诊断可视化 :用 ParaView 显示
alpha.water = 0.5等值面;用isoSurface/cuttingPlane检查界面厚度与伪流。 - 守恒验证 :
alpha.water的体积分应当守恒(溃坝/波浪算例可用 functionObject 统计)。 - 网格无关性:至少在 2--3 套网格上验证关键量(前沿位置、冲击压力)。
- 并行注意 :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 数与子循环的约束下保持守恒与稳定。这条链路上每一环都对应源码里一个清晰的对象与函数,也是理解其余多相求解器的公共基础。