OpenFOAM 源码深度剖析 · 第二季-03 拉格朗日粒子与喷雾:Cloud/parcel 模板体系、运动积分与子模型群
与欧拉方法"把流体当连续场、在场网格上解 PDE"不同,拉格朗日方法 把离散相(液滴、颗粒、气泡、煤粉)当作一个个"粒子包(parcel) ",每个时间步逐一积分运动方程、在网格中追踪位置、并与所在连续相交换质量/动量/能量。OpenFOAM 把这套框架建立在两层模板 (
particle/Cloud与parcel/Cloud)之上,几乎所有物理过程都做成可插拔的 RTS 子模型 。这是全书"模板 + 运行时选择"思想在离散相上的又一次体现。
本章所有路径、行号与代码摘录均来自本仓库(OpenFOAM master,WM_PROJECT_VERSION=v2606)源码的实际读取结果 ;字典示例取自 tutorials/lagrangian/ 下的真实算例。
1. 导论:离散相为何"反过来"设计数据结构
1.1 两类物理,两种数据结构
连续相(GeometricField)用连续数组 + 表达式模板 ,追求零拷贝与向量化;离散相恰恰相反:粒子数量动态变化 (注入、逃逸、破碎、聚并)、位置随机 (任意单元穿面、跨 patch、跨进程)、数量巨大 而单体数据量小。因此 OpenFOAM 选择侵入式双向链表 (IDLList):每个粒子对象自身内嵌 链表的 prev/next,所有粒子直接挂在"云"对象上。增删是 O(1) 指针操作,遍历按链序,代价是失去随机访问与缓存局部性------对"频繁增删、顺序遍历"的粒子场景,这是正确取舍。
1.2 三层模板结构
src/lagrangian/ 严格分层(目录实测):
bash
src/lagrangian/
├── basic/ 基础层:particle、Cloud(模板)、indexedParticle、injectedParticle、
│ passiveParticle、IOPosition、InteractionLists、Make
├── intermediate/ 中间层(无热/化学):clouds/{baseClasses,derived,Templates}、
│ parcels/{Templates,derived,include}、submodels/{CloudFunctionObjects,
│ ForceTypes,Kinematic,Reacting,ReactingMultiphase,Thermodynamic,MPPIC,addOns}、
│ integrationScheme、phaseProperties、conversion
├── spray/ 喷雾层:parcels/Templates/SprayParcel、clouds、submodels/{AtomizationModel,
│ BreakupModel,StochasticCollision}
├── coalCombustion/ 煤燃烧;distributionModels/ 粒径分布;DSMC/ 稀薄气体;
│ molecularDynamics/ 分子动力学;solidParticle/ 固体颗粒;turbulence/ 粒子湍流扩散
分层逻辑 :basic 只回答"粒子是什么、如何在任意网格里定位与追踪、如何并行迁移";intermediate 叠加"运动/传热/传质/相变/反应"整套子模型;spray 再加"雾化/二次破碎/随机碰撞"。每上一层只追加子模型列表 ,底层追踪/链表/并行完全复用。
1.3 本章路线
- 基础层:
particle数据布局与四面体追踪(§2--§4);2. 中间层KinematicCloud/KinematicParcel(§5--§6);3. 受力与运动积分、Courant 子步进(§7--§8);4. 蒸发/传热/扩散/壁面(§9--§11);5. 注射与注射框架(§12);6. 喷雾雾化与破碎(§13);7. 碰撞聚并(§14);8. 欧拉-拉格朗日耦合(§15);9. 云函数对象、MPPIC、并行容错(§16--§18);10. 字典模板、报错排查、选型表(§19--§21)。
2. 基础层(一):particle 的数据布局与链表嵌入
2.1 类声明:一个粒子就是一个链表节点
particle.H(src/lagrangian/basic/particle/particle.H 第 76--91 行):
cpp
class particle
:
public IDLList<particle>::link
{
// Private Data
//- Size in bytes of the position data
static const std::size_t sizeofPosition;
//- Size in bytes of the fields
static const std::size_t sizeofFields;
//- The value of nBehind_ at which tracking is abandoned.
static const label maxNBehind_;
逐行解读:
public IDLList<particle>::link------全章题眼。particle不是"被容器装着的普通对象",而是链表节点本身 。于是Cloud<ParticleType> : public IDLList<ParticleType>直接继承 整条链表;云里的粒子集合不需任何额外容器 ;每个粒子能自己找到前驱后继。这是与Field(连续数组)最根本的分野。sizeofPosition/sizeofFields------粒子序列化 (并行传递、IO、重启)时的字节大小。位置数据与属性数据分开,使"只要位置"的后处理(IOPosition的 POSITIONS 模式)只需传位置部分。maxNBehind_------"粒子卡在某个面之后"的计数阈值,定义于particle.C第 43 行:const Foam::label Foam::particle::maxNBehind_ = 10;。
2.2 私有数据:重心坐标系的完整清单
particle.H(第 129--173 行):
cpp
const polyMesh& mesh_; // 网格引用
barycentric coordinates_; // 重心坐标(四分量)------不是笛卡尔坐标
label celli_; // 所在单元,-1 表示丢失
label tetFacei_; // 所属四面体的基面
label tetPti_; // 基面上的基点索引
label facei_; // 恰在面上则记录,否则 -1
scalar stepFraction_; // 本时间步已完成比例 ∈ [0,1]
scalar behind_; // 落后于"历史最远点"的距离
label nBehind_; // 连续"后退"次数,超阈值放弃追踪
label origProc_; // 出生进程
label origId_; // 出生编号
| 字段 | 含义 | 设计动机 |
|---|---|---|
coordinates_ |
重心坐标 barycentric |
不用笛卡尔坐标:穿面/换四面体时位置自然连续,无需浮点重算 |
celli_ |
所在单元,-1=丢失 |
丢失粒子由此识别 |
tetFacei_/tetPti_ |
基面 + 基点 | 唯一确定一个四面体 |
stepFraction_ |
本步已完成比例 | 一个时间步被切成多个追踪子步,用比例累积 |
nBehind_ |
连续后退次数 | 超 maxNBehind_ 放弃追踪,防死循环 |
origProc_/origId_ |
出生标识 | 迁移后仍可标识来源,用于统计与重启 |
为何用重心坐标 :若存笛卡尔 x,每穿面进入新四面体都要把 x 反算成新重心坐标(4×3 线性变换),反复做会累积舍入误差。直接维护重心坐标后,命中计算在重心空间里做一维线性插值 ,命中后 reflect()/rotate() 做纯符号操作(§3.4),精度得以保持。
2.3 trackingData:追踪状态传递
particle.H(第 95--112 行):
cpp
class trackingData
{
public:
//- Flag to switch processor
bool switchProcessor;
//- Flag to indicate whether to keep particle (false = delete)
bool keepParticle;
template<class TrackCloudType>
trackingData(const TrackCloudType& cloud) {}
};
trackingData 是贯穿一次追踪的上下文对象 :由云创建、传给 move()、再传给每个粒子的 move()/calc()/hitPatch()。两个布尔量是控制流与生命周期总开关 :switchProcessor 置真表示"粒子跨过 processor patch,交给邻居进程"(§18.1);keepParticle 为 false 表示"删除该粒子"(穿 inlet/outlet、被壁面逃逸吸收)。它是模板化的,派生层(如 KinematicParcel::trackingData)会往里面塞插值器与缓存(§6.4)。
particle.H 还定义了兼容结构 positionsCompat1706(第 115--126 行),用于读 OpenFOAM-1706 及更早的粒子位置文件------旧版存笛卡尔坐标,新版存重心坐标,读取时转换。
2.4 前向声明暴露的"粒子会遇到谁"
particle.H(第 56--66 行)前向声明一串 patch 类型:
cpp
class polyPatch; class cyclicPolyPatch; class cyclicAMIPolyPatch;
class cyclicACMIPolyPatch; class processorPolyPatch;
class symmetryPlanePolyPatch; class symmetryPolyPatch;
class wallPolyPatch; class wedgePolyPatch;
这几乎是"粒子离开计算域的所有出口清单 ":壁面、对称面、循环/AMI、processorPolyPatch(并行边界)、楔形(2D 轴对称)。追踪函数按 patch 类型走不同处理:循环/AMI 镜像或变换 ,处理器 patch 迁移 ,壁面交给交互模型。第 47 行的 #include "polyMeshTetDecomposition.H" 是追踪的核心几何依赖------把任意多面体单元切成四面体 ;barycentricTensor.H 则是重心/世界坐标线性变换的载体。
3. 基础层(二):网格中的四面体追踪
3.1 trackToFace:单元内逐四面体推进
particle.C(第 645--698 行,节选):
cpp
Foam::scalar Foam::particle::trackToFace
(
const vector& displacement,
const scalar fraction
)
{
scalar f = 1;
label tetTriI = onFace() ? 0 : -1;
facei_ = -1;
// Loop the tets in the current cell
while (nBehind_ < maxNBehind_)
{
f *= trackToTri(f*displacement, f*fraction, tetTriI);
if (tetTriI == -1) return 0; // 本四面体内走完
else if (tetTriI == 0) { facei_ = tetFacei_; return f; } // 撞到单元面
else changeTet(tetTriI); // 撞到四面体内部三角,换 tet
}
// 卡住:警告并把 stepFraction 推进到子步末
WarningInFunction << "Particle #" << origId_ << " got stuck at " << position() << endl;
stepFraction_ += f*fraction;
behind_ = 0; nBehind_ = 0;
return 0;
}
逐段解读:
f = 1是"剩余可走位移比例";每命中一个四面体内部三角形就把f乘上本次子步命中比例,累积"实际走了多远"。trackToTri返回tetTriI:-1=在当前四面体内走完(返回 0);0=撞到单元的面 (记录facei_,返回f);1/2/3=撞到四面体内部三角形 ,changeTet换到相邻四面体。nBehind_防死循环 :注释(particle.H第 160--166 行)解释------当单元被负体积四面体 (大变形网格)切分时,一条直线轨迹 可能在正/负空间重叠区里绕成闭环 :几何上每次都在前进,但"走到的最远点"(behind_)其实没增加,于是nBehind_累加;超过maxNBehind_=10就强制终止 ,把stepFraction_推进到子步末并复位计数。- 警告
got stuck at (x y z)来自这里,不是致命错误 ,但提示网格质量差或时间步过大(§20 有专条)。为避免同一粒子刷屏,用static stuckID/stuckProc记住上次报的是谁。
3.2 track:跨单元接力
particle.C(第 626--642 行):
cpp
Foam::scalar Foam::particle::track(const vector& displacement, const scalar fraction)
{
scalar f = trackToFace(displacement, fraction);
while (onInternalFace()) // 停在内部面 → 换单元继续
{
changeCell();
f *= trackToFace(f*displacement, f*fraction);
}
return f;
}
停在内部面 时 changeCell() 换到对面单元,再用剩余位移继续,直到停在边界面或走完。这就是"上一单元出来、进入下一单元"的接力。
3.3 四面体内的命中测试与数学推导①
trackToStationaryTri(第 701 行起)流程:
- 取当前四面体正向变换
T(stationaryTetReverseTransform(centre, detA, T),第 722 行),形如detA·y = (x − centre) & T; - 把位移
x1用T变成重心空间位移barycentric Tx1(x1 & T)(第 732 行); - 在重心空间里四面体的四个面是
y_i = 0的超平面,命中退化成四个一维线性方程; - 取最小正命中分数对应的三角形,记为
tetTriI。
数学推导 ①(重心空间线性命中) :设粒子重心坐标 y = (ya,yb,yc,yd)(Σy_i=1),位移映射到重心空间为 Δy = x1 & T / detA,射线 y(s)=y+s·Δy。四面体的一个面(如 a 对面的 bcd)对应 ya=0,命中条件:
ini
ya + s·Δya = 0 ⇒ s = −ya / Δya (当 Δya 与 ya 反号)
对四个面各算一次 s,取最小且 ∈(0,1] 者作为本四面体内的命中。这是 O(1) 纯算术 ,无需迭代------重心坐标法的威力所在。四个面都无命中则本次在四面体内结束(tetTriI=-1)。
3.4 拓扑变换:reflect/rotate/changeTet/changeCell
particle.C(第 147--169 行):
cpp
void Foam::particle::reflect()
{
std::swap(coordinates_.c(), coordinates_.d()); // 镜像:交换 c、d
}
void Foam::particle::rotate(const bool reverse)
{
if (!reverse) { scalar t=coordinates_.b(); coordinates_.b()=coordinates_.c();
coordinates_.c()=coordinates_.d(); coordinates_.d()=t; } // (b,c,d) 循环移位
else { scalar t=coordinates_.d(); coordinates_.d()=coordinates_.c();
coordinates_.c()=coordinates_.b(); coordinates_.b()=t; }
}
reflect():交换c、d------对应"两个相邻四面体共享一个基点、但另两个顶点顺序相反"(镜像)。rotate(reverse):把(b,c,d)循环移位------对应"基点不同但取向相同"(旋转)。
二者配合 changeTet(第 172--244 行)在单元内换四面体:tetTriI 为 1/2/3 时按 isOwner 与 tetPti_ 是否到端点,选 reflect + tetPti_ ± 1 或 changeFace(tetTriI)。changeCell(第 361--369 行)跨越内部面:
cpp
void Foam::particle::changeCell()
{
const label ownerCellI = mesh_.faceOwner()[tetFacei_];
const bool isOwner = celli_ == ownerCellI;
celli_ = isOwner ? mesh_.faceNeighbour()[tetFacei_] : ownerCellI;
reflect(); // 两侧三角形取向相反,故镜像
}
关键 :换单元后只需一次 reflect(),位置(重心坐标)不需显式重算------重心坐标在新四面体下语义自动成立。
3.5 changeFace
当四面体走到尽头但还没离开单元 ,需要找同单元内共享该边的相邻基面(changeFace,第 247--358 行)。它为每个候选面找与当前命中三角形共享的有向边 (第 293 行 newOwner == celli_ ? -1 : +1,有向 比较以避免 ACMI 重合面误判),再按 tetBasePtIs 把边索引标准化,最后 rotate 把共享边旋到正确位置、reflect 处理三角形反向。
4. 基础层(三):定位、IO 与自动映射
4.1 Cloud<ParticleType>:云的模板骨架
Cloud.H(第 64--118 行,节选):
cpp
template<class ParticleType>
class Cloud
:
public cloud, // 非模板基类(网格/并行公共能力 + 虚接口)
public IDLList<ParticleType> // 承载粒子的侵入式链表
{
const polyMesh& polyMesh_;
mutable DynamicList<label> labels_; // findTris 用
mutable autoPtr<bitSet> cellWallFacesPtr_; // 含壁面的单元(按需算)
mutable autoPtr<vectorField> globalPositionsPtr_;// 全局粒子位置(按需算)
protected:
cloud::geometryType geometryType_;
public:
friend class particle;
typedef ParticleType particleType;
typedef ParticleType parcelType; // Parcels are just particles
TypeName("Cloud");
static word cloudPropertiesName;
要点 :双继承 ------cloud(非模板,提供与网格/并行的公共能力与虚接口)+ IDLList<ParticleType>(链表)。geometryType_ (COORDINATES/POSITIONS)决定 IO 与后处理如何写位置。cellWallFacesPtr_/globalPositionsPtr_ 是按需计算 的缓存(mutable autoPtr),延续第一季 demand-driven 取向。注释 Parcels are just particles (第 117 行)点破本质:parcel 本身就是 particle ,一个 parcel 代表 nParticle() 个物理性质相同的真实粒子。
4.2 IOPosition:位置的两种写法
IOPosition.C(第 56--96 行节选):
cpp
bool Foam::IOPosition<CloudType>::writeData(Ostream& os) const
{
os << cloud_.size() << nl << token::BEGIN_LIST << nl;
switch (geometryType_)
{
case cloud::geometryType::COORDINATES:
for (const auto& p : cloud_) { p.writeCoordinates(os); os << nl; }
break;
case cloud::geometryType::POSITIONS:
for (const auto& p : cloud_) { p.writePosition(os); os << nl; }
break;
}
os << token::END_LIST << endl;
return os.good();
}
COORDINATES:写完整位置数据(重心坐标 + 拓扑索引),重启可精确复原所在四面体;POSITIONS:只写笛卡尔位置,文件小、可读性好(后处理用),重启时重新定位。
读入 readData(第 99--147 行)按模式构造 new particleType(mesh, is, false, newFormat);老格式走 positionsCompat1706 兼容路径。
4.3 自动映射与丢失粒子
Cloud::autoMap(Cloud.C 第 303--332 行):
cpp
void Foam::Cloud<ParticleType>::autoMap(const mapPolyMesh& mapper)
{
if (!globalPositionsPtr_) { FatalErrorInFunction << "Global positions are not available. ..."; }
cellWallFacesPtr_.clear();
(void)polyMesh_.tetBasePtIs(); // 强制所有进程构建,否则通信次序不匹配
(void)polyMesh_.oldCellCentres();
const vectorField& positions = globalPositionsPtr_();
label i = 0;
for (ParticleType& p : *this) { p.autoMap(positions[i], mapper); ++i; }
}
为何要先 storeGlobalPositions :拓扑变化后旧重心坐标失效,autoMap 需要每个粒子的笛卡尔位置 去新网格重新定位(注释第 356--360 行解释 mapPolyMesh 无旧网格副本)。(void)polyMesh_.tetBasePtIs() 是并行一致性措施:强制所有进程构建,否则"某些进程没粒子、某些有"会导致通信次序不匹配而僵死。
丢失粒子处理 deleteLostParticles(第 114--128 行):
cpp
for (ParticleType& p : *this)
if (p.cell() == -1)
{
WarningInFunction << "deleting lost particle at position " << p.position() << endl;
deleteParticle(p);
}
运行时 deleting lost particle at position ... 就来自此(§20 有专条)。增删是原子操作(第 101--111 行):addParticle 调 append;deleteParticle 用 delete(this->remove(&p))------remove 从链表摘节点,O(1) 增删正是选链表的目的。
5. 中间层(一):KinematicCloud 架构与主循环
5.1 类说明与结构
KinematicCloud.H(第 27--55 行)文档列出它管理的全部内容:云函数对象 、粒子受力 (浮力/阻力/压力梯度...)、子模型(扩散/注入/壁面交互/随机碰撞/表面膜)。类定义(第 112--136 行):
cpp
template<class CloudType>
class KinematicCloud
:
public CloudType, // 继承具体云(如 basicKinematicCloud)
public kinematicCloud // 非模板基类(对外统计接口)
{
public:
typedef typename CloudType::particleType parcelType;
typedef KinematicCloud<CloudType> kinematicCloudType;
typedef ParticleForceList<KinematicCloud<CloudType>> forceType; // 受力列表
typedef CloudFunctionObjectList<KinematicCloud<CloudType>> functionType; // 云函数对象列表
};
结构与 Cloud 同构:模板派生自 CloudType + 非模板基类 ,并组合两个模型列表 (ParticleForceList 与 CloudFunctionObjectList),二者都是"按名从字典构造"的 RTS 容器。于是"给算例加一个力模型/云函数对象"只需在字典加一行(§19)。
非模板基类 kinematicCloud(kinematicCloud.H 第 53--110 行)是一组纯虚接口 ,让上层在不知模板参数时操作云:nParcels、massInSystem、linearMomentumOfSystem、linearKineticEnergyOfSystem、Dij、Dmax、vDotSweep、theta、alpha、rhoEff------全是欧拉侧诊断/耦合量(§15)。
5.2 setModels():装配全部单例子模型
KinematicCloud.C(第 49--123 行,节选):
cpp
void Foam::KinematicCloud<CloudType>::setModels()
{
dispersionModel_.reset(DispersionModel<KinematicCloud<CloudType>>::New(subModelProperties_, *this).ptr());
patchInteractionModel_.reset(PatchInteractionModel<...>::New(...).ptr());
stochasticCollisionModel_.reset(StochasticCollisionModel<...>::New(...).ptr());
surfaceFilmModel_.reset(SurfaceFilmModel<...>::New(...).ptr());
packingModel_.reset(PackingModel<...>::New(...).ptr());
dampingModel_.reset(DampingModel<...>::New(...).ptr());
isotropyModel_.reset(IsotropyModel<...>::New(...).ptr());
UIntegrator_.reset(integrationScheme::New("U", solution_.integrationSchemes()).ptr());
}
这是一个云拥有的全部"单例式子模型 ":扩散、壁面交互、随机碰撞、表面膜、MPPIC 的堆积/阻尼/各向同性模型,以及速度积分格式 UIntegrator_。都在构造时按字典 subModels 段创建,此后复用同一实例。UIntegrator_ 从 integrationSchemes() 按 字段名 "U" 取------这解释字典:
ini
integrationSchemes { U Euler; T analytical; }
U/T 可用不同格式,因为它们各自 ODE 刚性不同。
5.3 solve():稳态与瞬态分岔
KinematicCloud.C(第 126--171 行,节选):
cpp
if (solution_.steadyState())
{
cloud.storeState(); // 存云副本 cloudCopyPtr_
cloud.preEvolve(td);
evolveCloud(cloud, td);
if (solution_.coupled()) cloud.relaxSources(cloud.cloudCopy()); // 与旧源项松弛
}
else
{
cloud.preEvolve(td);
evolveCloud(cloud, td);
if (solution_.coupled()) cloud.scaleSources(); // 直接缩放本步源项
}
cloud.info();
cloud.postEvolve(td);
if (solution_.steadyState()) cloud.restoreState();
- 稳态 :先
storeState()(存副本),演化后relaxSources()对新旧源项做亚松弛 (field = field0 + coeff*(field−field0),relax第 734--745 行),最后restoreState()。用于稳态迭代收敛。 - 瞬态 :
scaleSources()把源项乘松弛系数。 - 两者都
preEvolve(缓存场、构建 cell occupancy)与postEvolve(释放、写输出)。
5.4 evolveCloud():注入 → 运动 → 碰撞
KinematicCloud.C(第 219--263 行):
cpp
if (solution_.coupled()) cloud.resetSourceTerms(); // rhokTrans/UTrans/UCoeff 归零
if (solution_.transient())
{
label preInjectionSize = this->size();
this->surfaceFilm().inject(cloud); // 表面膜注入
if (preInjectionSize != this->size()) updateCellOccupancy(); // 尺寸变了重建
injectors_.inject(cloud, td); // 注射模型列表
cloud.motion(cloud, td); // 推进所有粒子
stochasticCollision().update(td, solution_.trackTime()); // 碰撞聚并
}
else
{
injectors_.injectSteadyState(cloud, td, solution_.trackTime());
td.part() = parcelType::trackingData::tpLinearTrack;
CloudType::move(cloud, td, solution_.trackTime());
}
瞬态路径四步 (喷雾/燃烧最常见):① 清源项;② 表面膜注入;③ 注射模型列表注入 (§12);④ motion 推进粒子 (§8);⑤ 随机碰撞(§14)。注意第 240--244 行:注入后若云尺寸变了要重建 cellOccupancy(每单元粒子指针列表),因为碰撞模型依赖它。
5.5 motion() 与 preEvolve/postEvolve
motion(第 831--849 行):td.part() = tpLinearTrack; CloudType::move(...),之后若 MPPIC 各向同性模型活跃则 updateAverages+calculate,最后 updateCellOccupancy。td.part() 是追踪分段枚举 (tpVelocityHalfStep/tpLinearTrack/tpRotationalTrack,§6.4)。
preEvolve(第 782--817 行节选):
cpp
label nGeometricD = mesh_.nGeometricD(); // 强制求网格维度(并行一致性)
this->dispersion().cacheFields(true); // 缓存 k/epsilon(必要时算 grad(k))
forces_.cacheFields(true); // 缓存力的插值器
pAmbient_ = constProps_.dict().getOrDefault<scalar>("pAmbient", pAmbient_);
if (dampingModel().active() || packingModel().active()) td.updateAverages(*this);
updateCellOccupancy();
functions_.preEvolve(td);
postEvolve(第 266--310 行)反向释放缓存、写 outputProperties_。
6. 中间层(二):KinematicParcel 与模板链
6.1 类说明与模板链
KinematicParcel.H(第 27--48 行)文档:带旋转运动(仅球体)的 parcel,与连续相单向/双向耦合;子模型包括 drag、turbulent dispersion、wall interactions 。它继承 ParcelType,形成模板链:
swift
particle
└── KinematicParcel<ParcelType> 运动/阻力/壁面
└── ThermoParcel<...> + 传热/相变
└── ReactingParcel<...> + 组分/反应
└── ReactingMultiphaseParcel<...> + 多相(挥发+焦炭)
└── HeterogeneousReactingParcel<...> + 异相反应
MPPICParcel<...> 稠密粒子(四向耦合)
每加一层多一套子模型列表 与若干状态量,底层追踪/链表/并行完全复用------CRTP 式静态继承,零虚函数开销、编译期特化。
6.2 constantProperties:常量与变量分离
KinematicParcel.H(第 101--160 行节选):
cpp
class constantProperties
{
protected:
const dictionary dict_;
private:
demandDrivenEntry<label> parcelTypeId_; // 类型 id(后处理标识)
demandDrivenEntry<scalar> rhoMin_; // 最小密度
demandDrivenEntry<scalar> rho0_; // 粒子密度(常量)
demandDrivenEntry<scalar> minParcelMass_; // 最小 parcel 质量
};
动机 :单 parcel 属性分两类------随 parcel 变化的 (位置、速度、d_、nParticle_...)每个各存一份(数量巨大);整个 cloud 共享的常量 (rho0/rhoMin/minParcelMass_/子模型参数)只存一份。demandDrivenEntry<T> 更进一步:用到才从字典读 (延迟解析)。构造(KinematicParcelI.H 第 61--72 行):
cpp
dict_(parentDict.subOrEmptyDict("constantProperties")),
parcelTypeId_(dict_, "parcelTypeId", -1),
rhoMin_(dict_, "rhoMin", 1e-15),
rho0_(dict_, "rho0", -1),
minParcelMass_(dict_, "minParcelMass", 1e-15)
这是"上万粒子共享一份常量参数"的内存优化。
6.3 parcel 的计算属性与无量纲数
KinematicParcelI.H(第 360--487 行节选):
cpp
inline Foam::scalar ...::mass() const { return rho_*volume(); }
inline Foam::scalar ...::volume(const scalar d) { return pi/6.0*pow3(d); }
inline Foam::scalar ...::areaS(const scalar d) { return pi*d*d; } // 表面积
inline Foam::scalar ...::areaP(const scalar d) { return 0.25*areaS(d); } // 投影面积
inline Foam::scalar ...::Re(const scalar rhoc, const vector& U, const vector& Uc,
const scalar d, const scalar muc)
{ return rhoc*mag(U - Uc)*d/max(muc, ROOTVSMALL); }
inline Foam::scalar ...::We(const scalar rhoc, const vector& U, const vector& Uc,
const scalar d, const scalar sigma)
{ return rhoc*magSqr(U - Uc)*d/max(sigma, ROOTVSMALL); }
inline Foam::scalar ...::Eo(const vector& g, const scalar rho, const scalar rhoc,
const vector& U, const scalar d, const scalar sigma)
{ const vector dir = U/max(mag(U), ROOTVSMALL);
return mag(g & dir)*mag(rho - rhoc)*sqr(d)/max(sigma, ROOTVSMALL); }
- 体积
V = π/6·d³、表面积As = πd²、投影面积Ap = As/4; - Re = ρc·|U−Uc|·d/μc (滑移驱动);We = ρc·|U−Uc|²·d/σ (破碎判据);Eo = |g·û|·|ρ−ρc|·d²/σ (气泡变形判据)。 三者是 drag/breakup/变形模型的核心自变量;分母加
ROOTVSMALL防除零。
6.4 KinematicParcel::trackingData 与插值
KinematicParcel.H(第 162--220 行节选):
cpp
class trackingData : public ParcelType::trackingData
{
public:
enum trackPart { tpVelocityHalfStep, tpLinearTrack, tpRotationalTrack };
private:
autoPtr<interpolation<scalar>> rhoInterp_; // 密度插值器
autoPtr<interpolation<vector>> UInterp_; // 速度插值器
autoPtr<interpolation<scalar>> muInterp_; // 黏度插值器
scalar rhoc_; vector Uc_; scalar muc_; // 缓存连续相场
autoPtr<AveragingMethod<scalar>> volumeAverage_; // MPPIC 平均场
... (radiusAverage_/rhoAverage_/uAverage_/uSqrAverage_)
};
在基类之上追加连续相插值器与缓存 :rhoInterp_/UInterp_/muInterp_ 从欧拉体心场插值,格式由字典 interpolationSchemes 定;rhoc_/Uc_/muc_ 缓存本步子步结果(避免重复插值);trackPart 把一次追踪分成"半步速度/线性追踪/旋转追踪"三段。
setCellValues(KinematicParcel.C 第 43--70 行):
cpp
void ...::setCellValues(TrackCloudType& cloud, trackingData& td)
{
tetIndices tetIs = this->currentTetIndices();
td.rhoc() = td.rhoInterp().interpolate(this->coordinates(), tetIs);
if (td.rhoc() < cloud.constProps().rhoMin()) // 密度限幅,防除零
{
if (debug) WarningInFunction << "Limiting observed density in cell " << this->cell()
<< " to " << cloud.constProps().rhoMin() << nl << endl;
td.rhoc() = cloud.constProps().rhoMin();
}
td.Uc() = td.UInterp().interpolate(this->coordinates(), tetIs);
td.muc() = td.muInterp().interpolate(this->coordinates(), tetIs);
}
三点 :① 插值用重心坐标 + 四面体索引 而非笛卡尔坐标(粒子位置就定义在重心坐标上,避免二次舍入);② 密度限幅到 rhoMin(运行时见 Limiting observed density in cell ...);③ 这就是"力从哪里来 "的源头------rhoc_/Uc_/muc_ 供阻力、传热模型用。
7. 受力模型与运动积分
7.1 ParticleForceList:PtrList + RTS
ParticleForceList.H(第 53--74 行节选):
cpp
template<class CloudType>
class ParticleForceList : public PtrList<ParticleForce<CloudType>>
{
CloudType& owner_;
const fvMesh& mesh_;
const dictionary dict_;
bool calcCoupled_; // 是否算耦合(与 Uc 相关)力
bool calcNonCoupled_; // 是否算非耦合力
};
构造(ParticleForceList.C 第 50--112 行节选)遍历字典每个 entry,按关键字从 RTS 表 New:
cpp
for (const entry& dEntry : dict)
{
const word& modelName = dEntry.keyword();
if (dEntry.isDict()) // 带参数:如 sphereDrag { type ...; }
this->set(count, ParticleForce<CloudType>::New(owner, mesh, dEntry.dict(), modelName));
else // 仅类型名
this->set(count, ParticleForce<CloudType>::New(owner, mesh, dict, modelName));
++count;
}
对应字典两种写法:
ini
particleForces
{
sphereDrag; // 无参数,只写名字
gravity;
PlessisMasliyahDrag { alphac alpha.air; } // 带参数,写成子字典
}
7.2 forceSuSp:Su/Sp 分解与半隐式
forceSuSpI.H(第 46--70 行):
cpp
inline Foam::forceSuSp::forceSuSp(const vector& Su, const scalar Sp)
{ first() = Su; second() = Sp; }
inline const Foam::vector& Foam::forceSuSp::Su() const { return first(); }
inline Foam::scalar Foam::forceSuSp::Sp() const { return second(); }
把合力写成 F = Su + Sp·Uc (Uc 是连续相速度):Su 是与粒子速度无关的显式部分(重力、压力梯度显式项),Sp 是与 Uc 成比例、可半隐式 处理的部分(尤其阻力 F_drag = Sp·(Uc−Up))。calcCoupled(第 147--169 行)遍历力并逐项相加;massEff(第 198--213 行):
cpp
scalar massEff = mass;
forAll(*this, i) massEff += this->operator[](i).massAdd(p, td, mass);
return massEff;
有效质量 = 粒子自身质量 + 各力的附加质量 (虚拟质量力贡献附加质量),故积分里除以 massEff(§7.5)。
7.3 阻力模型族与数学推导②
SphereDragForce.C(第 33--85 行):
cpp
Foam::scalar Foam::SphereDragForce<CloudType>::CdRe(const scalar Re) const
{
// (AOB:Eq. 35)
if (Re > 1000.0) return 0.424*Re;
return 24.0*(1.0 + (1.0/6.0)*pow(Re, 2.0/3.0));
}
...
Foam::forceSuSp Foam::SphereDragForce<CloudType>::calcCoupled(...) const
{
// (AOB:Eq. 34)
return forceSuSp(Zero, mass*0.75*muc*CdRe(Re)/(p.rho()*sqr(p.d())));
}
数学推导 ②(从 Stokes 阻力到 Sp) :球体阻力 F_d = ½·ρc·Cd·Ap·|Uc−Up|·(Uc−Up)(Ap=πd²/4)。令 CdRe=Cd·Re、Re=ρc|Uc−Up|d/μc:
scss
F_d = ½·ρc·(πd²/4)·(Cd·|Uc−Up|)·(Uc−Up)
= (π/8)·μc·CdRe·d·(Uc−Up)
而代码 Sp = mass·0.75·μc·CdRe/(ρ·d²),代入 mass=ρ·πd³/6:
scss
Sp = (ρπd³/6)·(3/4)·μc·CdRe/(ρd²) = (π/8)·d·μc·CdRe
与上面完全一致 ------Sp 即"单位 (Uc−Up) 对应的阻力系数",Su=0(阻力全进 Sp,这正是半隐式成立的条件)。CdRe 用 AOB 相关式:Re>1000 时 Cd=0.424;否则 CdRe=24(1+Re^{2/3}/6),低 Re 退化为 Stokes 24/Re,中 Re 用 Schiller-Naumann 型 Re^{2/3} 修正。
NonSphereDragForce.C(第 33--65 行)用球形度 phi 修正:
cpp
Foam::scalar ...::CdRe(const scalar Re) const
{ // (HL:Eq. 10-11)
return 24.0*(1.0 + a_*pow(Re, b_)) + Re*c_/(1 + d_/(Re + ROOTVSMALL));
}
...
a_(exp(2.3288 - 6.4581*phi_ + 2.4486*sqr(phi_))),
b_(0.0964 + 0.5565*phi_),
c_(exp(4.9050 - 13.8944*phi_ + 18.4222*sqr(phi_) - 10.2599*pow3(phi_))),
d_(exp(1.4681 + 12.2584*phi_ - 20.7322*sqr(phi_) + 15.8855*pow3(phi_)))
{ if (phi_ <= 0 || phi_ > 1) FatalErrorInFunction << "... (phi) must be ... (0,1]"; }
a_/b_/c_/d_ 是 Haider-Levenspiel 关联式系数(phi=1 退化为球阻力),构造时校验 phi∈(0,1],否则 FatalError。
阻力族全景 (Drag/ 实测):SphereDrag、NonSphereDrag、WenYuDrag、ErgunWenYuDrag、PlessisMasliyahDrag、DistortedSphereDrag、TomiyamaDrag。前两者用于稀疏相 ;WenYu/ErgunWenYu/PlessisMasliyah 是稠密相(含孔隙率/体积分数修正),用于鼓泡床/流化床/MPPIC;后两者分别考虑液滴变形与气泡。
注册宏一览(makeParcelForces.H 第 55--73 行节选):
cpp
#define makeParcelForces(CloudType) \
makeParticleForceModel(CloudType); \
makeParticleForceModelType(SphereDragForce, CloudType); \
makeParticleForceModelType(NonSphereDragForce, CloudType); \
... (WenYuDrag/ErgunWenYuDrag/PlessisMasliyahDrag/SaffmanMeiLift/...) \
makeParticleForceModelType(GravityForce, CloudType); \
... (PressureGradientForce/SRFForce/VirtualMassForce/InterfaceForce/...) \
makeParticleForceModelType(CoulombForce, CloudType);
每类子模型都有注册宏,把实现登记进 RTS 表------与第一季第 4 章机制完全同构。
7.4 其他力的 Su/Sp 分解
重力(含浮力) GravityForce.C(第 62--78 行):
cpp
value.Su() = mass*g_*(1.0 - td.rhoc()/p.rho());
return value;
Su = m·g·(1 − ρc/ρ)------重力减浮力(浮力 ρc·V·g = m·g·ρc/ρ),是 calcNonCoupled(与 Uc 无关),Sp=0。
压力梯度力 PressureGradientForce.C。先缓存连续相物质导数(第 72--104 行):resultPtr = new volVectorField(resultName, fvc::ddt(Uc) + (Uc & fvc::grad(Uc))),即 DUc/Dt。再在 calcCoupled(第 117--136 行)插值:
cpp
vector DUcDt = DUcDtInterp().interpolate(p.coordinates(), p.currentTetIndices());
value.Su() = mass*td.rhoc()/p.rho()*DUcDt;
Su = m·(ρc/ρ)·DUc/Dt(等价于 ρc·V·DUc/Dt)。注意它用 fvc::ddt/fvc::grad------欧拉侧算子被拉格朗日侧复用。
虚拟质量力 VirtualMassForce.C(第 73--102 行):
cpp
forceSuSp value = PressureGradientForce<CloudType>::calcCoupled(p, td, dt, mass, Re, muc);
value.Su() *= Cvm_;
...
return mass*td.rhoc()/p.rho()*Cvm_; // massAdd:计入 massEff 的附加质量
它继承 PressureGradientForce,把 Su 乘虚拟质量系数 Cvm------"虚拟质量力=压力梯度力×Cvm",模板链式继承复用的典型。
升力族 (Lift/):SaffmanMeiLift(剪切升力)、TomiyamaLift(气泡升力);另有 InterfaceForce、CoulombForce(静电)、ParamagneticForce(磁)、SRFForce(旋转参考系)、NonInertialFrameForce(非惯性系)。
7.5 calcVelocity:速度积分
KinematicParcel.C(第 182--255 行节选):
cpp
const forceSuSp Fcp = forces.calcCoupled(p, ttd, dt, mass, Re, mu);
const forceSuSp Fncp = forces.calcNonCoupled(p, ttd, dt, mass, Re, mu);
const scalar massEff = forces.massEff(p, ttd, mass);
// Shortcut splitting assuming no implicit non-coupled force
const vector acp = (Fcp.Sp()*td.Uc() + Fcp.Su())/massEff; // 耦合常数项加速度
const vector ancp = (Fncp.Su() + Su)/massEff; // 非耦合 + 外部源
const scalar bcp = Fcp.Sp()/massEff; // 耦合线性系数(阻力刚度)
const vector deltaU = cloud.UIntegrator().delta(U_, dt, acp + ancp, bcp);
const vector deltaUncp = ancp*dt;
const vector deltaUcp = deltaU - deltaUncp;
vector Unew = U_ + deltaU;
dUTrans -= massEff*deltaUcp; // 只有耦合部分算作对连续相的动量反馈
Spu = dt*Fcp.Sp();
meshTools::constrainDirection(mesh, mesh.solutionD(), Unew); // 降维修正
meshTools::constrainDirection(mesh, mesh.solutionD(), dUTrans);
return Unew;
逐段解读:
Fcp/Fncp分开 :只有"与Uc相关"的力(阻力、压力梯度、虚拟质量)进Fcp,其余(重力...)进Fncp;后者做显式、前者可半隐式。- 系数 :
acp耦合常数项;ancp非耦合力 + 外部显式源;bcp耦合线性系数(阻力刚度,即 §8 的 β)。 - 积分 :
UIntegrator().delta(U_, dt, acp+ancp, bcp)解线性 ODEdU/dt=(acp+ancp)−bcp·U。 - 拆
deltaUcp/deltaUncp:只有耦合部分算作对连续相的动量反馈(重力是外力、不应改变连续相动量)。 Spu = dt*Fcp.Sp():传给连续相动量方程的线性化系数 ,与dUTrans一起构成 §15 的UTrans/UCoeff。constrainDirection:2D/1D 下把第三/二方向分量归零------降维算例正确性保证。
8. 时间推进与 Courant 限制
8.1 积分格式接口与数学推导③
integrationSchemeTemplates.C(第 32--72 行):
cpp
template<class Type>
inline Type Foam::integrationScheme::explicitDelta(const Type& phi, const scalar dtEff,
const Type& Alpha, const scalar Beta)
{ return (Alpha - Beta*phi)*dtEff; }
template<class Type>
inline Type Foam::integrationScheme::delta(const Type& phi, const scalar dt,
const Type& Alpha, const scalar Beta) const
{ return explicitDelta(phi, dtEff(dt, Beta), Alpha, Beta); } // 用积分格式的 dtEff
数学推导 ③(为什么是 dtEff):要解线性 ODE
ini
dφ/dt = Alpha − Beta·φ
精确解 φ(t)=Alpha/Beta + (φ0−Alpha/Beta)·exp(−Beta·t),一步增量整理为 (Alpha−Beta·φ)·dtEff,其中 dtEff = (1−e^{−Beta·dt})/Beta(解析格式,精确) 。Euler (Euler.C 第 56--63 行)用一阶近似:
cpp
Foam::scalar Foam::integrationSchemes::Euler::dtEff(const scalar dt, const scalar Beta) const
{ return dt/(1 + Beta*dt); }
dt/(1+βdt) 是精确式的一阶有理近似(也等价于"对 β 隐式"),无条件稳定、βdt 大时趋近 1/β 不过冲 。analytical.C(第 56--79 行)用精确式,βdt 很小时退化为 dt:
cpp
Foam::scalar Foam::integrationSchemes::analytical::dtEff(const scalar dt, const scalar Beta) const
{ return mag(Beta*dt) > SMALL ? (1 - exp(-Beta*dt))/Beta : dt; }
Foam::scalar ...::sumDtEff(const scalar dt, const scalar Beta) const
{ return mag(Beta*dt) > SMALL ? dt/Beta - (1 - exp(-Beta*dt))/sqr(Beta) : sqr(dt); }
| 格式 | dtEff(dt,β) |
特点 | 适用 |
|---|---|---|---|
Euler(默认) |
dt/(1+βdt) |
一阶、显式化、无条件稳定 | 速度 U |
analytical |
(1−e^{−βdt})/β |
精确 | 温度 T(刚性大) |
8.2 move():Courant 子步进
KinematicParcel.C(第 305--397 行节选):
cpp
ttd.switchProcessor = false;
ttd.keepParticle = true;
const cloudSolution& solution = cloud.solution();
const scalarField& cellLengthScale = cloud.cellLengthScale();
const scalar maxCo = solution.maxCo();
while (ttd.keepParticle && !ttd.switchProcessor && p.stepFraction() < 1)
{
const point start = p.position();
const scalar sfrac = p.stepFraction();
const vector s = trackTime*U_; // 本时间步总位移
const scalar l = cellLengthScale[p.cell()]; // 本单元长度尺度
const vector d = p.deviationFromMeshCentre(); // 降维偏差修正
scalar f = 1 - p.stepFraction();
f = min(f, maxCo); // 时间 Courant
f = min(f, maxCo*l/max(SMALL*l, mag(s))); // 距离/空间 Courant
if (p.active()) p.trackToFace(f*s - d, f); // 在四面体空间追踪
else p.stepFraction() += f; // 粘壁:不动,仅推进时间
const scalar dt = (p.stepFraction() - sfrac)*trackTime;
if (dt > ROOTVSMALL) // 极小步跳过
{
p.setCellValues(cloud, ttd); // 重插值 rhoc/Uc/muc
p.calcDispersion(cloud, ttd, dt); // 湍流扩散
if (solution.cellValueSourceCorrection()) p.cellValueSourceCorrection(cloud, ttd, dt);
p.calcUCorrection(cloud, ttd, dt); // MPPIC 速度修正
p.calc(cloud, ttd, dt); // 受力 + 积分 + 累加源项
}
p.age() += dt;
if (p.active() && p.onFace()) ttd.keepParticle = cloud.functions().postFace(p, ttd);
ttd.keepParticle = cloud.functions().postMove(p, dt, start, ttd);
if (p.active() && p.onFace() && ttd.keepParticle) p.hitFace(s, cloud, ttd);
}
return ttd.keepParticle;
这是整个拉格朗日时间推进的心脏,逐段:
-
循环条件 :三重------没被删、没换进程、本步没走完。一个时间步被拆成多个子步 (受 Courant 限制),
stepFraction从 0 累加到 1。 -
位移
s = trackTime*U_:本步总位移(匀速假设)。 -
Courant 双
min:cppf = min(f, maxCo); // 时间 Courant f = min(f, maxCo*l/max(SMALL*l, mag(s))); // 空间 Courant保证"单位子步穿过的单元数 ≤ maxCo "。这就是字典
maxCo(喷雾默认 0.3、MPPIC 默认 1.0)的真实含义------它限制每子步穿过的单元数,而非时间步本身。越小越稳越慢;越大越快但可能一步穿多单元导致位置错误。 -
trackToFace(f*s - d, f):四面体空间里追踪到下一个面,只走f比例(§3.1)。 -
子步
dt:随后setCellValues→calcDispersion→cellValueSourceCorrection→calcUCorrection→calc(§15)。 -
钩子 :
postFace/postMove交云函数对象(§16);在面上则hitFace→patch 分派(§11)。 -
if(dt>ROOTVSMALL):极小步跳过物理更新,防除零。
8.3 calc:单步物理核心
KinematicParcel.C(第 129--179 行节选):
cpp
const scalar np0 = nParticle_; // 一个 parcel 代表的真实粒子数
const scalar mass0 = mass();
const scalar Re = this->Re(td);
vector Su = Zero; scalar Spu = 0.0; vector dUTrans = Zero;
this->U_ = calcVelocity(cloud, td, dt, Re, td.muc(), mass0, Su, dUTrans, Spu);
this->U_ += this->UCorrect_; // MPPIC 修正
if (cloud.solution().coupled())
{
cloud.UTrans()[this->cell()] += np0*dUTrans; // 动量传递
cloud.UCoeff()[this->cell()] += np0*Spu; // 动量隐式系数
}
要点 :np0 是粒子数乘子 ------所有对连续相的源项都乘它(因为"parcel 是粒子包");UCorrect_ 是 MPPIC 修正(§17);源项累加只在 coupled() 时做(单向耦合跳过,§15)。
9. 蒸发与传热
9.1 相变模型体系
相变放在 Reacting/PhaseChangeModel/:LiquidEvaporation(liquidEvaporation)、LiquidEvaporationBoil(liquidEvaporationBoil)。基类 PhaseChangeModel 定义 calculate(算质量传递)、dh(相变焓)、Tvap/TMax。
9.2 LiquidEvaporation 与数学推导④
LiquidEvaporation.C 的 calculate(第 134--214 行节选):
cpp
if ((liquids_.Tc(X) - T) < SMALL) // 已达临界温度
{ forAll(activeLiquids_, i) dMassPC[liqToLiqMap_[i]] = GREAT; return; }
const scalarField Xc(calcXc(celli)); // 连续相摩尔分数
forAll(activeLiquids_, i)
{
const label gid = liqToCarrierMap_[i], lid = liqToLiqMap_[i];
const scalar Dab = liquids_.properties()[lid].D(pc, Ts); // 蒸汽扩散系数
const scalar pSat = liquids_.properties()[lid].pv(pc, T); // 饱和压力
const scalar Sc = nu/(Dab + ROOTVSMALL); // Schmidt 数
const scalar Sh = this->Sh(Re, Sc); // Sherwood 数
const scalar kc = Sh*Dab/(d + ROOTVSMALL); // 传质系数
const scalar Cs = pSat/(RR*Ts); // 表面蒸汽浓度
const scalar Cinf = Xc[gid]*pc/(RR*Ts); // 远场蒸汽浓度
const scalar Ni = max(kc*(Cs - Cinf), 0.0); // 摩尔通量
dMassPC[lid] += Ni*pi*sqr(d)*liquids_.properties()[lid].W()*dt; // 质量传递
}
数学推导 ④(单液滴蒸发质量流率):
- 饱和压力
pSat = pv(pc,T)(液滴温度下的蒸汽压); - 表面浓度
Cs = pSat/(RR·Ts)(RR通用气体常数,Ts膜温),单位 kmol/m³; - 远场浓度
Cinf = Xc·pc/(RR·Ts)(Xc为连续相该组分摩尔分数); - Sherwood 数
Sh→ 传质系数kc = Sh·Dab/d(膜理论); - 摩尔通量
Ni = max(kc·(Cs−Cinf), 0)------max(·,0)保证只蒸发不凝结; - 质量流率
dm/dt = Ni·(πd²)·W(表面积 × 摩尔通量 × 摩尔质量),乘dt得本步dMassPC。
cpp
template<class CloudType>
Foam::scalar Foam::LiquidEvaporation<CloudType>::Sh(const scalar Re, const scalar Sc) const
{ return 2.0 + 0.6*Foam::sqrt(Re)*cbrt(Sc); }
Sherwood 数 Sh = 2 + 0.6·√Re·∛Sc------Sh=2 是纯扩散,后项是对流增强;Re 用相对速度 ,故运动液滴蒸发更快。calcXc(第 37--53 行)把质量分数换摩尔分数:Xc[i] = Y[i]/W(i) 再归一化 Xc/sum(Xc)。Tvap(X)=Tpt(X)(沸点)、TMax(p,X)=pvInvert(p,X)(饱和温度上限)、dh(潜热 hl 或"载气焓−液相焓")。
9.3 ReactingParcel::calcPhaseChange:限幅与传热耦合
ReactingParcel.C(第 61--180 行节选):
cpp
if (!phaseChange.active()) return;
scalarField X(composition.liquids().X(Y));
scalar Tvap = phaseChange.Tvap(X);
if (T < Tvap) return; // 未到蒸发温度,直接返回
const scalar TMax = phaseChange.TMax(td.pc(), X);
const scalar Tdash = min(T, TMax), Tsdash = min(Ts, TMax); // 温度限幅
phaseChange.calculate(dt, this->cell(), Re, Pr, d, nus, rho, Tdash, Tsdash,
td.pc(), td.Tc(), X, Ysol*mass, YPhase*Y*mass, dMassPC);
forAll(Y, i) // 质量限幅:蒸发量不超过可蒸发质量
if (dMassPC[i] > 0) dMassPC[i] = min(mass*YPhase*Y[i], dMassPC[i]);
forAll(dMassPC, i)
{
const label cid = composition.localToCarrierId(idPhase, i);
const scalar dh = phaseChange.dh(cid, i, td.pc(), Tdash);
Sh -= dMassPC[i]*dh/dt; // 蒸发吸热,从粒子焓里扣
}
// BirdCorrection:斯蒂芬流修正,计算 N(摩尔通量之和)、NCpW、Cs
逐段 :① 早期退出 (未配置/未到温度)性能友好;② 温度限幅 防溢出;③ 质量限幅是根因防护 ------否则一步蒸发超量会让 updateMassFraction 得负质量分数而崩溃;④ 焓源 Sh :Sh -= dMassPC[i]*dh/dt,蒸发带走能量;⑤ BirdCorrection (第 153--179 行)算 N(摩尔通量之和)、NCpW(ΣNi·Cp·W)、Cs,供 RanzMarshall::htc 用。
updateMassFraction(第 183--203 行):
cpp
scalar mass1 = mass0 - sum(dMass);
if (mass1 > ROOTVSMALL)
forAll(Y, i) Y[i] = (Y[i]*mass0 - dMass[i])/mass1;
return mass1;
新质量分数 = (旧总量 − 蒸发量)/新总质量,mass1>ROOTVSMALL 再次防除零/负质量。
9.4 传热:RanzMarshall 与 ThermoParcel::calcHeatTransfer
RanzMarshall.C(第 61--70 行):
cpp
template<class CloudType>
Foam::scalar Foam::RanzMarshall<CloudType>::Nu(const scalar Re, const scalar Pr) const
{ // (AOB:p. 18 below Eq. 42)
return a_ + b_*pow(Re, m_)*pow(Pr, n_);
}
默认 a_=2, b_=0.6, m_=1/2, n_=1/3(构造第 33--45 行 getOrDefault 取),即 Nu = 2 + 0.6·Re^{1/2}·Pr^{1/3} ------经典 Ranz-Marshall 关系式,与 Sh 同形(传热传质类比),系数可被字典覆盖。
ThermoParcel::calcHeatTransfer(ThermoParcel.C 第 251--310 行节选):
cpp
if (!cloud.heatTransfer().active()) return T_;
const scalar d=this->d(), rho=this->rho(), As=this->areaS(d), V=this->volume(d), m=rho*V;
scalar htc = cloud.heatTransfer().htc(d, Re, Pr, kappa, NCpW); // 传热系数 = Nu*kappa/d
const scalar bcp = htc*As/(m*Cp_); // 传热刚度 β(对流越快越大)
const scalar acp = bcp*td.Tc(); // 趋向连续相 Tc 的驱动力
scalar ancp = Sh;
if (cloud.radiation()) ancp += As*epsilon*(Gc/4.0 - sigma*pow4(T_)); // 辐射
ancp /= m*Cp_;
const scalar deltaT = cloud.TIntegrator().delta(T_, dt, acp + ancp, bcp);
const scalar deltaTncp = ancp*dt;
const scalar deltaTcp = deltaT - deltaTncp;
scalar Tnew = T_ + deltaT;
Tnew = clamp(Tnew, cloud.constProps().TMin(), cloud.constProps().TMax()); // 温度限幅
dhsTrans -= m*Cp_*deltaTcp;
Sph = dt*m*Cp_*bcp;
return Tnew;
与速度积分完全同构 :htc = Nu·kappa/d;bcp = htc·As/(m·Cp) 是传热刚度(单位 W/K ÷ J/K = 1/s);acp = bcp·Tc;ancp = Sh/(m·Cp)(显式焓源含辐射);TIntegrator_ 用 analytical(温度刚性大);clamp 防越界;dhsTrans/Sph 即显式/隐式焓源项。ThermoParcel 的源项累加(第 180--248 行):
cpp
this->T_ = calcHeatTransfer(cloud, td, dt, Re, Pr, kappas, NCpW, Sh, dhsTrans, Sph);
this->U_ = calcVelocity(cloud, td, dt, Re, mus, mass0, Su, dUTrans, Spu);
if (cloud.solution().coupled())
{
cloud.UTrans()[celli] += np0*dUTrans; cloud.UCoeff()[celli] += np0*Spu;
cloud.hsTrans()[celli] += np0*dhsTrans; cloud.hsCoeff()[celli] += np0*Sph;
}
hsTrans(显式焓)与 hsCoeff(隐式焓系数)与 UTrans/UCoeff 一一对应,构成 §15 能量耦合。cellValueSourceCorrection (第 83 行):td.Tc() += cloud.hsTrans()[celli]/(CpMean*massCell)------用本步已累积的焓反馈立即修正 连续相温度,这就是 cellValueSourceCorrection on 的物理含义。
10. 湍流扩散
10.1 StochasticDispersionRAS(随机游走)
StochasticDispersionRAS.C(第 65--120 行):
cpp
Random& rnd = this->owner().rndGen();
const scalar cps = 0.16432;
const scalar k = this->kPtr_->primitiveField()[celli];
const scalar epsilon = this->epsilonPtr_->primitiveField()[celli] + ROOTVSMALL;
const scalar UrelMag = mag(U - Uc - UTurb);
const scalar tTurbLoc = min(k/epsilon, cps*pow(k, 1.5)/epsilon/(UrelMag + SMALL));
if (dt < tTurbLoc) // 在涡内
{
tTurb += dt;
if (tTurb > tTurbLoc) // 换新涡
{
tTurb = 0;
const scalar sigma = sqrt(2*k/3.0);
const scalar theta = rnd.sample01<scalar>()*twoPi;
const scalar u = 2*rnd.sample01<scalar>() - 1;
const scalar a = sqrt(1 - sqr(u));
const vector dir(a*cos(theta), a*sin(theta), u);
UTurb = sigma*mag(rnd.GaussNormal<scalar>())*dir; // 各向同性高斯扰动
}
}
else { tTurb = GREAT; UTurb = Zero; }
return Uc + UTurb;
物理要点 :湍流拉格朗日时间尺度 tTurbLoc = min(k/ε, cps·k^{1.5}/(ε·|Urel|))------k/ε 是大涡时间尺度,cps·k^{1.5}/(ε|Urel|)(cps=0.16432)是穿越涡的时间尺度 (滑移太快用不上大涡),取小者决定涡多久换一次。换涡时抽各向同性高斯扰动 UTurb:方向在球面均匀(theta 均匀、u 均匀于 -1,1),幅值 σ=√(2k/3) 乘高斯随机数。返回 Uc+UTurb 即粒子感觉到的有效连续相速度,直接影响阻力。
10.2 GradientDispersionRAS(梯度扩散)
GradientDispersionRAS.C(第 94--157 行节选):
cpp
const vector& gradk = this->gradkPtr_->primitiveField()[celli];
...
const vector dir = -gradk/(mag(gradk) + SMALL); // 方向 = -grad(k)
scalar fac = 0.0;
// In 2D calculations the -grad(k) is always away from the axis of symmetry
// This creates a 'hole' in the spray and to prevent this we let fac be both negative/positive
if (this->owner().mesh().nSolutionD() == 2) fac = rnd.GaussNormal<scalar>();
else fac = mag(rnd.GaussNormal<scalar>());
UTurb = sigma*fac*dir;
方向固定为 -∇k(从高湍动能区向外扩散),幅值高斯随机。2D 修正 (注释):2D 下 -∇k 总指向远离对称轴,若只取正向会在喷雾里形成"中心空洞 ",故 2D 时 fac 可为正可负。这是"物理直觉 vs 数值缺陷"的绝佳案例。cacheFields(true)(第 72--91 行)算 gradkPtr_ = fvc::grad(k)(用欧拉侧算子)。
11. 壁面交互:PatchInteractionModel
11.1 家族
PatchInteractionModel/ 实现:NoInteraction、StandardWallInteraction(逃逸/粘附/反弹)、Rebound、RecycleInteraction、LocalInteraction(按 patch 组,含 BaiGosman)、MultiInteraction。粒子撞壁是工程最常出问题的环节。
11.2 StandardWallInteraction
构造时把 type 词转枚举,反弹读 e/mu(第 94--99 行):
cpp
case PatchInteractionModel<CloudType>::itRebound:
{ e_ = this->coeffDict().getOrDefault("e", 1.0);
mu_ = this->coeffDict().getOrDefault("mu", 0.0);
break; }
correct()(第 151--256 行节选,仅壁面生效):
cpp
if (isA<wallPolyPatch>(pp))
{
switch (interactionType_)
{
case ...::itNone: return false;
case ...::itEscape: // 逃逸:删除
{ keepParticle = false; p.active(false); U = Zero;
nEscape_[pp.index()][idx]++; massEscape_[pp.index()][idx] += p.nParticle()*p.mass();
break; }
case ...::itStick: // 粘附:保留但不动
{ keepParticle = true; p.active(false); U = Zero;
nStick_[pp.index()][idx]++; massStick_[pp.index()][idx] += p.nParticle()*p.mass();
break; }
case ...::itRebound: // 反弹
{
keepParticle = true; p.active(true);
vector nw, Up; this->owner().patchData(p, pp, nw, Up);
U -= Up; // 相对壁面速度
if (mag(Up) > 0 && mag(U) < this->Urmax()) // 伪静止告警
{ WarningInFunction << "Particle U the same as patch ... removed";
keepParticle = false; p.active(false); U = Zero; break; }
scalar Un = U & nw; vector Ut = U - Un*nw;
if (Un > 0) U -= (1.0 + e_)*Un*nw; // 法向反弹(恢复系数 e)
U -= mu_*Ut; // 切向摩擦
U += Up; // 回全局系
break;
}
}
return true;
}
return false;
逐分支 :itEscape 穿出域→删(keepParticle=false,计入 nEscape_/massEscape_);itStick 粘壁→keepParticle=true 但 active(false)(后续追踪不再移动,走 §8.2 的 stepFraction += f 分支);itRebound 先减 patch 速度 Up(移动壁/旋转壁),若 patch 动而相对速度极小则删粒子并 Warning (运行时见 Particle U the same as patch),否则法向按 (1+e)·Un·nw 反弹、切向按 mu 摩擦,再回到全局系。patchData(KinematicCloud.C 第 852 行起)取 patch 法向 nw,若壁面速度边界 fixesValue() 则用其作为壁面切向速度 Up(移动壁/盖驱动腔)。统计文件名(writeFileHeader,第 33--51 行)形如 patchName_nEscape_0、patchName_massStick_0------每个 patch × 每个注入器一组,便于定位"哪个喷嘴的粒子从哪个面跑掉/粘了多少"。
12. 注射模型:粒子的"出生地"
12.1 家族与字典入口
InjectionModel/ 实现:NoInjection、ManualInjection(手动位置表)、PatchInjection/PatchFlowRateInjection(按 patch 面)、CellZoneInjection(按 cellZone)、ConeInjection/ConeNozzleInjection(锥形/喷嘴)、FieldActivatedInjection(场激活)、InflationInjection、KinematicLookupTableInjection、InjectedParticleInjection/InjectedParticleDistributionInjection。字典里挂多个注入器即"多喷嘴":
bash
injectionModels
{
model1 { type coneNozzleInjection; SOI 0; massTotal 6.0e-6; ... }
model2 { ... }
}
InjectionModelList 把它们组织成列表,injectors_.inject(cloud, td) 逐个执行(§5.4)。
12.2 注射主循环:新增粒子的完整生命周期
InjectionModel.C(第 438--563 行节选):
cpp
const scalar time = this->owner().db().time().value();
label parcelsAdded = 0; scalar massAdded = 0.0;
label newParcels = 0; scalar newVolumeFraction = 0.0; scalar delayedVolume = 0;
if (prepareForNextTimeStep(time, newParcels, newVolumeFraction))
{
const scalar trackTime = this->owner().solution().trackTime();
const polyMesh& mesh = this->owner().mesh();
const scalar deltaT = max(0.0, min(trackTime, min(time - SOI_, timeEnd() - time0_)));
const scalar padTime = max(0.0, SOI_ - time0_); // 本步中途才开始的补齐
for (label parcelI = 0; parcelI < newParcels; parcelI++)
{
if (validInjection(parcelI))
{
scalar timeInj = time0_ + padTime + deltaT*parcelI/newParcels; // 伪注入时刻
label celli=-1, tetFacei=-1, tetPti=-1; vector pos = Zero;
setPositionAndCell(parcelI, newParcels, timeInj, pos, celli, tetFacei, tetPti);
if (celli > -1) // 定位成功才继续
{
const scalar dt = time - timeInj;
meshTools::constrainToMeshCentre(mesh, pos);
parcelType* pPtr = new parcelType(mesh, pos, celli);
cloud.setParcelThermoProperties(*pPtr, dt); // 热物性(云负责)
setProperties(parcelI, newParcels, timeInj, *pPtr); // 速度/粒径/组分(派生模型)
cloud.checkParcelProperties(*pPtr, dt, fullyDescribed());
meshTools::constrainDirection(mesh, mesh.solutionD(), pPtr->U());
pPtr->nParticle() = setNumberOfParticles(newParcels, newVolumeFraction, pPtr->d(), pPtr->rho());
if (pPtr->nParticle() >= minParticlesPerParcel_)
{
parcelsAdded++; massAdded += pPtr->nParticle()*pPtr->mass();
if (pPtr->move(cloud, td, dt)) // 新粒子走完"出生"后剩余时间
{ pPtr->typeId() = injectorID_; cloud.addParticle(pPtr); }
else delete pPtr;
}
else { delayedVolume += pPtr->nParticle()*pPtr->volume(); delete pPtr; } // 太少→丢弃记账
}
}
}
}
delayedVolume_ = returnReduce(delayedVolume, sumOp<scalar>());
postInjectCheck(parcelsAdded, massAdded);
逐段解读:
prepareForNextTimeStep:基类据派生模型的parcelsToInject/volumeToInject决定本步注几个 parcel、总体积,返回 false 则不注入。deltaT:本步里实际落在注射窗口内 的时长(把起止时刻两个钳位都处理);padTime处理"注射在本步中途才开始"。- 伪时间
timeInj:把newParcels均匀铺在窗口内,保证注入流时间连续(而非一帧全出)。 setPositionAndCell(派生模型实现)、setProperties(设速度/直径/组分)、setParcelThermoProperties/checkParcelProperties(云负责)------注入的核心落在派生模型的两个函数上。if (celli > -1):定位失败即丢弃("注射失败"常见原因之一,§20)。nParticle >= minParticlesPerParcel_:太少则丢弃并把体积记账(delayedVolume)------"注入质量不足/不守恒"的隐形来源(§20)。pPtr->move(cloud, td, dt):新粒子先走完"出生到本步末"的剩余时间,成功才addParticle入链,否则delete。
12.3 setPositionAndCell 的两类实现
面注入 patchInjectionBase.C:把 patch 每个面三角化 ,构造累积面积权重 triCumulativeMagSf_(第 122--152 行节选):
cpp
for (const auto& t : triFace_) { patchArea += t.mag(points); *iter++ = patchArea; } // 累积
sumTriMagSf_.resize_nocopy(numProc+1); sumTriMagSf_[0] = 0;
{ scalarList::subList slice(sumTriMagSf_, numProc, 1);
slice[myProci] = patchArea; Pstream::allGatherList(slice); } // 并行全局累积
for (label i = 1; i < sumTriMagSf_.size(); ++i) sumTriMagSf_[i] += sumTriMagSf_[i-1];
- 按面积加权抽样 :累积面积作分布,抽一个
U(0, patchArea)定位三角形------每三角形被抽中概率正比于面积,注入在面上均匀。 - 并行全局累积
sumTriMagSf_用Pstream::allGatherList汇总各进程 patch 面积再前缀和,保证并行下注入量的全局一致。
喷嘴注入 ConeNozzleInjection::setPositionAndCell(第 399--545 行,imDisc 分支):
cpp
case injectionMethod::imDisc:
{
scalar frac = Foam::sqrt(rndGen.globalSample01<scalar>()); // 面积均匀:对 r 抽 sqrt
scalar dr = outerDiameter_ - innerDiameter_;
scalar r = 0.5*(innerDiameter_ + frac*dr);
position = positionVsTime_->value(t) + r*normal_;
// 移动喷嘴:用位置差估算 Uinjector_
this->findCellAtPosition(cellOwner, tetFacei, tetPti, position);
break;
}
frac = sqrt(U(0,1)) 是关键------圆盘上半径 r 处的面积元正比于 r·dr,累积分布 ∝ r²,故半径对 sqrt 抽样;若线性抽 r,粒子会向圆心聚集 。normal_ = tanVec1_·cosβ + tanVec2_·sinβ(第 435--436 行)在垂直喷嘴轴的圆盘上均匀取方位角。thetaInner_/thetaOuter_(内/外半锥角)决定速度方向的锥形分布------这正是"coneNozzleInjection"名字由来。
parcelsToInject(第 367--380 行)与 volumeToInject(第 383--396 行):
cpp
Foam::label ...::parcelsToInject(const scalar time0, const scalar time1)
{ return ((time0 >= 0.0) && (time0 < duration_)) ? floor((time1 - time0)*parcelsPerSecond_) : 0; }
Foam::scalar ...::volumeToInject(const scalar time0, const scalar time1)
{ return ((time0 >= 0.0) && (time0 < duration_)) ? flowRateProfile_->integrate(time0, time1) : 0.0; }
parcelsPerSecond_ 是注入"频率",flowRateProfile_(字典 flowRateProfile table(...))积分出本段体积流量,duration_ 是时长(timeEnd()=SOI_+duration_,第 360--364 行)。
12.4 粒子数分配与数学推导⑤
setNumberOfParticles 据 parcelBasisType(number/mass)决定每个 parcel 的 nParticle。
数学推导 ⑤(number vs mass) :设本步要注入总体积 V_tot、"目标 parcel 数" N_p,对一个直径 d 的 parcel:
-
mass 基准 :每个 parcel 承担固定体积
V_p = V_tot/N_p,则ininParticle = V_p/V_drop = (V_tot/N_p)/(πd³/6)即装的真实粒子数随
d³反比变化(大液滴的 parcel 代表更少的真实粒子)。 -
number 基准 :把粒子均匀分成
N_p份,nParticle = N_total/N_p(与d无关)。
为什么重要 :nParticle 是所有源项的乘子(§8.3 np0)。若 mass/number 弄反,会导致总注入质量与预期差数量级(§20 有专条)。
12.5 分布模型
distributionModels/(distributionModel 基类 + normal/uniform/RosinRammler/massRosinRammler/multiNormal/general/binned/exponential/fixedValue)提供粒径/速度的概率分布;注入时 setProperties 调 sizeDistribution_->sample() 抽直径。字典(aachenBomb):
ini
sizeDistribution
{
type RosinRammler;
RosinRammlerDistribution { minValue 1e-06; maxValue 0.00015; lambda 0.00015; n 3; }
}
RosinRammler 是喷雾/煤粉最常用:lambda 是尺度参数、n 是形状参数(n 大则粒径集中)。
13. 喷雾:雾化与二次破碎
13.1 SprayParcel/SprayCloud
spray/ 在 ReactingParcel(已含蒸发/传热)之上追加两个子模型列表与状态量:
markdown
spray/
├── parcels/Templates/SprayParcel(状态 y, yDot, KHindex, tc ...)
├── clouds/Templates/SprayCloud(装配 AtomizationModel/BreakupModel)
└── submodels/{AtomizationModel/{NoAtomization,LISAAtomization,BlobsSheetAtomization},
BreakupModel/{NoBreakup,TAB,ETAB,SHF,ReitzKHRT,ReitzDiwakar,PilchErdman},
StochasticCollision/{ORourkeCollision,TrajectoryCollision}}
物理链:注入 → 雾化(一次破碎)→ 二次破碎 → 蒸发/相变 → 与连续相耦合。
13.2 ReitzKHRT 的 KH/RT 双机制与数学推导⑥
ReitzKHRT 同时考虑两种不稳定波:KH(Kelvin-Helmholtz) (液滴表面与气流的剪切 不稳定,剥离小液滴)与 RT(Rayleigh-Taylor) (加速度 导致重液-轻气界面不稳定,产生大破碎)。ReitzKHRT.C(第 80--259 行节选):
cpp
scalar weGas = 0.5*rhoc*sqr(Urmag)*d/sigma;
scalar weLiquid = 0.5*rho*sqr(Urmag)*d/sigma;
scalar reLiquid = rho*Urmag*r/mu; // Note: 用半径 r
scalar ohnesorge = sqrt(weLiquid)/(reLiquid + VSMALL); // Oh
scalar taylor = ohnesorge*sqrt(weGas); // Taylor 数
vector acceleration = Urel/tMom; // 阻力加速度
vector trajectory = U/mag(U);
scalar gt = (g + acceleration) & trajectory; // 有效加速度
scalar omegaKH = (0.34 + 0.38*pow(weGas,1.5))
/((1.0 + ohnesorge)*(1.0 + 1.4*pow(taylor,0.6))) * sqrt(sigma/(rho*pow3(r))); // KH 频率
scalar lambdaKH = 9.02*r*(1.0 + 0.45*sqrt(ohnesorge))*(1.0 + 0.4*pow(taylor,0.7))
/ pow(1.0 + 0.865*pow(weGas,1.67), 0.6); // KH 波长
scalar tauKH = 3.726*b1_*r/(omegaKH*lambdaKH); // KH 破碎时间
scalar dc = 2.0*b0_*lambdaKH; // 稳定直径
scalar helpVariable = mag(gt*(rho - rhoc));
scalar omegaRT = sqrt(2.0*pow(helpVariable,1.5)/(3.0*sqrt(3.0*sigma)*(rhoc + rho))); // RT 频率
scalar KRT = sqrt(helpVariable/(3.0*sigma + VSMALL)); // RT 波数
scalar lambdaRT = constant::mathematical::twoPi*cRT_/(KRT + VSMALL); // RT 波长
数学推导 ⑥(KH/RT 特征波):
- 气动韦伯数
WeGas = ½ρc·Urmag²·d/σ;液韦伯数WeLiq同理;Ohnesorge 数Oh = √WeLiq/ReLiq;Taylor 数Taylor = Oh·√WeGas。 - KH 最快增长频率 :
ωKH = (0.34 + 0.38·WeGas^1.5)/[(1+Oh)(1+1.4·Taylor^0.6)]·√(σ/(ρr³))(分子气动驱动、分母黏性/表面张力阻尼)。 - KH 波长
λKH = 9.02·r·(1+0.45√Oh)(1+0.4·Taylor^0.7)/(1+0.865·WeGas^1.67)^0.6;破碎时间τKH = 3.726·B1·r/(ωKH·λKH)(B1默认 40);稳定直径dc = 2·B0·λKH(B0默认 0.61)。 - RT 频率 :由有效加速度
gt=(g+Urel/tMom)·trajectory与密度差(ρ−ρc)决定ωRT=√(2·(gt·|ρ−ρc|)^1.5/(3√3·σ·(ρc+ρ)));波数KRT=√(gt·|ρ−ρc|/(3σ));波长λRT=2π·CRT/KRT(CRT默认 0.1)。
破碎判定(第 164--252 行节选):
cpp
if ((tc > 0) || (lambdaRT < d)) tc += dt; // 表面有 RT 波生长才开始计时
scalar tauRT = cTau_/(omegaRT + VSMALL);
if ((tc > tauRT) && (lambdaRT < d)) // RT 破碎:碎成 d/lambdaRT 个(质量守恒)
{ tc = -GREAT; scalar nDrops = d/lambdaRT; d = cbrt(d3/nDrops); }
else if (dc < d) // KH 破碎(渐进剥离)
{
if (weGas > weberLimit_) // Weber < 12 不破碎
{
scalar fraction = dt/tauKH;
d = (fraction*dc + d)/(1.0 + fraction); // 速率方程拉向 dc
// 剥离质量 ms 超阈值时,按三次方程求父滴直径、算子液滴数(严格守恒分裂)
}
}
- RT :仅
λRT<d才计tc;tc>τRT触发,碎成d/λRT个(每份直径d/nDrops^{1/3},质量守恒)。 - KH :
dc<d且WeGas>WeberLimit时按速率方程d=(fraction·dc+d)/(1+fraction)(fraction=dt/τKH)渐进剥离。 - 子液滴数守恒 (第 197--238 行):剥离质量超阈值(
ms/averageParcelMass > msLimit,默认 0.2)时,用三次方程x³−dc·x²+0+d²(dc−d)=0求新父滴直径,再nChildDrops=mc/dc³------严格的守恒分裂 (Vieta 型求根,判别式D3=qe3²+pe3³<0视为无实根)。KHindex<0.5分支(第 241--252 行)用lengthScale=min(λKH, 2π·Urmag/ωKH)处理"破碎后的大液滴"(Reitz 1987 p.322)。
其余破碎模型:TAB (弹簧-阻尼-质量振荡,WeCrit 触发)、ETAB (增强 TAB)、SHF (袋式/剥离分型)、ReitzDiwakar (袋式 Cbag/剥离 Cstrip/Cs,aachenBomb 默认)、PilchErdman(实验关联)。
13.3 LISAAtomization 与数学推导⑦
LISAAtomization(线性不稳定液膜破碎)模拟喷嘴附近液膜因表面不稳定而碎裂 。update(第 130--281 行节选):
cpp
scalar We = 0.5*rhoAv*sqr(Urel)*d/sigma;
scalar nu = mu/rho; scalar Q = rhoAv/rho;
vector diff = pos - injectionPos; scalar pWalk = mag(diff);
scalar h = diff & injectorDirection_;
scalar delta = sqrt(sqr(pWalk) - sqr(h));
scalar hSheet = volFlowRate/(constant::mathematical::pi*delta*Urel); // 液膜厚度
d = min(d, hSheet); // 初始直径不超膜厚
if (We > 27.0/16.0) // 长波分支
{
scalar kPos=0.0, kNeg=Q*sqr(Urel)*rho/sigma, derivPos=sqrt(Q*sqr(Urel));
scalar derivNeg = (8.0*sqr(nu)*pow3(kNeg) + Q*sqr(Urel)*kNeg - 3.0*sigma/2.0/rho*sqr(kNeg))
/ sqrt(4.0*sqr(nu)*pow4(kNeg) + Q*sqr(Urel)*sqr(kNeg) - sigma*pow3(kNeg)/rho)
- 4.0*nu*kNeg;
for (label i=0; i<40; i++) // 割线法求根
{ k = kPos - (derivPos/((derivNeg - derivPos)/(kNeg - kPos)));
... if (mag(k - kOld)/k < 1e-4) break; kOld = k; }
scalar omegaS = -2.0*nu*sqr(k)
+ sqrt(4.0*sqr(nu)*pow4(k) + Q*sqr(Urel)*sqr(k) - sigma*pow3(k)/rho);
tau = cTau_/omegaS; dL = sqrt(8.0*d/k); // 破碎时间、波长
}
数学推导 ⑦(LISA 色散关系) :求解液膜正弦扰动的色散方程 ω=ω(k)(k 波数)。增长最快波数满足 dω/dk=0:
css
dω/dk = (8ν²k³ + Q·Urel²·k − (3σ/2ρ)·k²) / √(4ν²k⁴ + Q·Urel²·k² − (σ/ρ)k³) − 4νk = 0
代码用割线法 在 [kPos,kNeg] 间迭代 40 次求根(收敛判据 |Δk|/k<1e-4)。得 k 后 ωS = −2νk² + √(4ν²k⁴ + Q·Urel²·k² − (σ/ρ)k³)。于是破碎时间 τ=cτ/ωS、长波波长 dL=√(8d/k)(We>27/16);We≤27/16 用短波分支 k=ρAv·Urel²/(2σ)、τ=(3cτ)^{2/3}·∛(J·σ/(Q²·Urel⁴·ρ))、dL=√(4d/k)。液膜厚度 hSheet=V̇/(π·δ·Urel) 先限制初始直径;随后 dD(SMD)加闪蒸修正 (1−χ·pRatio^{−0.135}),破碎长度 lBU=Cl·Urel·τ------pWalk>lBU 时改用 SMD 作新直径并 liquidCore=0。
要点 :LISA 是"沿喷嘴轴向前进、逐步判断是否越过破碎长度 "的模型,依赖 injectionPos/injectorDirection/volFlowRate,必须与喷嘴注入模型配套 。BlobsSheetAtomization 是另一类液膜/液柱破碎模型(blob 假设),适用于压力旋流喷嘴。
14. 随机碰撞与聚并:ORourkeCollision
14.1 同单元两两配对与数学推导⑧
ORourkeCollision.C 的 collide(第 38--121 行节选):
cpp
labelList occupancy(this->owner().mesh().nCells(), Zero);
for (const parcelType& p : this->owner()) occupancy[p.cell()]++;
CompactListList<parcelType*> pInCell(occupancy); // 紧凑存"每单元的 parcel 指针"
occupancy = 0;
for (parcelType& p : this->owner()) pInCell(p.cell(), occupancy[p.cell()]++) = &p;
for (label celli=0; celli<this->owner().mesh().nCells(); celli++)
{
UList<parcelType*> pInCelli(pInCell[celli]);
if (pInCelli.size() >= 2)
forAll(pInCelli, i)
for (label j=i+1; j<pInCelli.size(); j++)
{
parcelType& p1 = *pInCelli[i]; parcelType& p2 = *pInCelli[j];
scalar m1 = p1.nParticle()*p1.mass(); scalar m2 = p2.nParticle()*p2.mass();
bool massChanged = collideParcels(dt, p1, p2, m1, m2);
...
}
}
CompactListList<parcelType*>:紧凑存储每单元的 parcel 指针,避免逐单元分配------性能优化。- 两两配对 :同单元所有组合,O(N²) per cell------这正是碰撞模型只适合稀疏/
maxCo不大的原因。
collideParcels(第 124--166 行):
cpp
const scalar Vc = this->owner().mesh().V()[p1.cell()];
scalar magUrel = mag(p1.U() - p2.U());
scalar sumD = d1 + d2;
scalar nu0 = 0.25*constant::mathematical::pi*sqr(sumD)*magUrel*dt/Vc; // 预期碰撞数
scalar nMin = min(p1.nParticle(), p2.nParticle());
scalar nu = nMin*nu0;
scalar collProb = exp(-nu); // Poisson 不发生碰撞概率
scalar xx = this->owner().rndGen().template sample01<scalar>();
if (xx > collProb) // 碰撞发生
return (d1 > d2) ? collideSorted(dt, p1, p2, m1, m2)
: collideSorted(dt, p2, p1, m2, m1);
return false;
数学推导 ⑧(O'Rourke 碰撞概率) :O'Rourke(1981) 用泊松过程建模。单位体积、单位时间内的碰撞数期望:
scss
ν₀ = (π/4)·(d1+d2)²·|U1−U2| / V_cell · dt
(碰撞截面 (π/4)(d1+d2)² × 相对速度 × 时间 ÷ 单元体积)。两 parcel 各代表 n1/n2 个真实液滴,预期碰撞数 ν = min(n1,n2)·ν₀(取小者)。用 Poisson:不发生碰撞概率 e^{−ν} ,抽 xx~U(0,1),xx>e^{−ν} 则碰撞发生。
14.2 聚并判据
collideSorted(第 169--286 行节选):
cpp
scalar gamma = d1/max(ROOTVSMALL, d2);
scalar f = pow3(gamma) + 2.7*gamma - 2.4*sqr(gamma); // 尺寸比函数
scalar Tave = (T1*m1 + T2*m2)/mTot; // 质量平均温度
scalar sigmaAve = sigma1; if (mag(T2 - T1) > SMALL)
sigmaAve += (sigma2 - sigma1)*(Tave - T1)/(T2 - T1); // 插值表面张力
scalar rhoAve = mTot/(m1/rho1 + m2/rho2);
scalar dAve = sqrt(d1*d2);
scalar WeColl = 0.5*rhoAve*sqr(magURel)*dAve/max(ROOTVSMALL, sigmaAve); // Collision Weber
scalar coalesceProb = min(1.0, 2.4*f/max(ROOTVSMALL, WeColl)); // 聚并概率
scalar prob = this->owner().rndGen().template sample01<scalar>();
if (coalescence_ && prob < coalesceProb) { /* 聚并:按质量/动量/能量守恒合并 */ }
else { /* 弹开(grazing):按 gf 交换动量,按 nP1/nP2 加权 */ }
聚并判据 :定义 WeColl = ρAve·|Urel|²·dAve/σAve(dAve=√(d1d2))与 f=γ³+2.7γ−2.4γ²(γ=d1/d2,d1≥d2)。聚并概率 P_coalesce = min(1, 2.4·f/WeColl)------相对速度越大(WeColl 大)越易弹开;尺寸越接近(f 大)越易聚并(O'Rourke 实验拟合)。
- 聚并分支 :
dm = nP1·nProb·m2/nP2把p2部分质量并入p1,温度质量平均(Tave·mTot−m2·T2)/m1,速度按动量守恒,组分质量加权。 - 弹开分支(grazing) :不合并但交换动量 ,用
gf=(√prob−√P_coalesce)/(1−√P_coalesce)(gf→1完全擦过、gf→0接近对心)构造新速度,并按nP1/nP2加权(parcel 代表多粒子,弹开只影响一部分)。
质量守恒收尾 (collide 末尾第 111--120 行):
cpp
for (parcelType& p : this->owner())
if (p.nParticle()*p.mass() < this->owner().constProps().minParcelMass())
this->owner().deleteParticle(p);
聚并/破碎后若 parcel 总质量低于 minParcelMass (字典 constantProperties.minParcelMass,默认 1e-15)即删除,避免残余碎片 parcel。调大阈值提速但要小心守恒(§20)。TrajectoryCollision 是按轨迹判碰撞的另一实现,适合更大时间步/更稀喷雾。
15. 欧拉-拉格朗日耦合:源项与守恒
15.1 cloudSolution
cloudSolution.H(第 56--112 行节选):
cpp
const Switch active_; // 云是否参与计算
Switch transient_; // 瞬态/稳态
label calcFrequency_, logFrequency_;
scalar maxCo_; // 粒子 Courant(子步穿单元数上限)
label iter_; scalar trackTime_; scalar deltaTMax_;
Switch coupled_; // 是否生成源项(双向耦合)
Switch cellValueSourceCorrection_; // 子步内用最新源项修正连续相
scalar maxTrackTime_; Switch resetSourcesOnStartup_;
List<Tuple2<word, Tuple2<bool, scalar>>> schemes_; // 源项显式/半隐式与松弛
字典 solution 每项对号入座:active/coupled/transient/cellValueSourceCorrection/maxCo/sourceTerms.schemes/interpolationSchemes/integrationSchemes(§19)。
15.2 源项场与 resetSourceTerms
以 KinematicCloud 为例三耦合量:rhokTrans (本步质量交换累积)、UTrans (动量交换累积)、UCoeff (动量隐式系数,由 Spu 累积);ThermoCloud 再加 hsTrans/hsCoeff,ReactingCloud 再加 YTrans。每步开始清零(第 725--731 行):
cpp
void Foam::KinematicCloud<CloudType>::resetSourceTerms()
{ rhokTrans().field() = Zero; UTrans().field() = Zero; UCoeff().field() = Zero; }
15.3 SU() 与数学推导⑨
KinematicCloudI.H 的 SU(第 527--572 行节选):
cpp
dimensionSet dim(dimForce);
if (incompressible) dim.reset(dimForce/dimDensity);
if (solution_.coupled())
{
if (solution_.semiImplicit("U"))
{
volScalarField::Internal Vdt(mesh_.V()*this->db().time().deltaT());
if (incompressible) Vdt.dimensions() *= dimDensity;
return UTrans()/Vdt - fvm::Sp(UCoeff()/Vdt, U) + UCoeff()/Vdt*U;
}
else
{
auto tfvm = tmp<fvVectorMatrix>::New(U, dim);
tfvm.ref().source() = -UTrans()/(this->db().time().deltaT());
return tfvm;
}
}
return tmp<fvVectorMatrix>::New(U, dim);
数学推导 ⑨(源项装配与守恒) :UTrans 是一个时间步内的总动量交换 ([kg·m/s]),转成方程源项([N/m³])需除以 V·Δt:
ini
S_U = UTrans / (V·Δt)
- 显式分支 :直接
fvm.source() = −UTrans/(V·Δt)(粒子从连续相拿走的动量对连续相是汇,为负)。 - 半隐式分支 :
S_U = UTrans/(V·Δt) − Sp(UCoeff/(V·Δt), U) + UCoeff/(V·Δt)·U------fvm::Sp生成隐式部分(吸入fvMatrix对角),后两项补回显式项,使总源项仍等于显式源项但数值更稳 (UCoeff很大即阻力强时避免发散)。
为什么除以 V·Δt :fvMatrix 源项是"每体积每时间"的量纲,这是"粒子源项→欧拉方程"量纲换算的唯一正确方式(也是"源项量级不对"问题的根源,§20)。incompressible 时量纲重置为 force/density。
15.4 统计场
KinematicCloudI.H 的一组欧拉侧诊断/耦合场:
cpp
// 质量源项 Srhok(第 500--524 行):除以 Δt·V
return rhokTrans()/this->db().time().deltaT()/this->mesh().V();
// 体积分数 theta(第 602--626 行)
for (const parcelType& p : *this) theta[p.cell()] += p.nParticle()*p.volume();
theta.primitiveFieldRef() /= mesh_.V();
// 质量分数 alpha / 有效密度 rhoEff(第 629--676 行)
for (const parcelType& p : *this) alpha[p.cell()] += p.nParticle()*p.mass();
alpha /= (mesh_.V()*rho_);
for (const parcelType& p : *this) rhoEff[p.cell()] += p.nParticle()*p.mass();
rhoEff /= mesh_.V();
// 扫掠体积率 vDotSweep(第 575--599 行)
for (const parcelType& p : *this) vDotSweep[p.cell()] += p.nParticle()*p.areaP()*mag(p.U() - U_[p.cell()]);
vDotSweep.primitiveFieldRef() /= mesh_.V();
theta=粒子占体积分数;alpha=粒子质量/(单元体积·连续相密度);rhoEff=粒子"有效密度";vDotSweep=投影面积×相对速度(常用于碰撞/燃烧/结焦的体积率)。系统统计 (第 313--406 行):massInSystem(Σ nParticle·mass)、linearMomentumOfSystem、linearKineticEnergyOfSystem、Dij(Σ nParticle·d^i / Σ nParticle·d^j)、Dmax(reduce(d, maxOp<scalar>()) 跨进程归约 )------并行下这些量是全局的。
15.5 稳态松弛与瞬态缩放;双向 vs 单向
KinematicCloud.C(第 734--779 行节选):
cpp
void ...::relax(field, field0, name) const
{ const scalar coeff = solution_.relaxCoeff(name); field = field0 + coeff*(field - field0); } // 稳态亚松弛
void ...::relaxSources(cloudOldTime)
{ relax(rhokTrans_(), cloudOldTime.rhokTrans(), "rhok"); relax(UTrans_(), ...); relax(UCoeff_(), ...); }
void ...::scaleSources()
{ scale(rhokTrans_(), "rhok"); scale(UTrans_(), "U"); scale(UCoeff_(), "U"); } // 瞬态缩放
把松弛系数调小通常显著改善"云不收敛/源项振荡"(§20)。coupled true 既插值又反馈(双向耦合);coupled false 只插值不反馈(单向耦合,如 uncoupledKinematicParcelFoam)。辐射/电磁耦合(cloudAbsorptionEmission/cloudScatter)经 addOns/radiation 挂云,机制同理。
16. 云函数对象
16.1 钩子与干预
CloudFunctionObject.H(第 148--193 行节选):
cpp
virtual void preEvolve(const typename parcelType::trackingData& td); // 演化前
virtual void postEvolve(const typename parcelType::trackingData& td); // 演化后
virtual bool postMove(parcelType& p, const scalar dt, const point& position0, ...); // 每子步
virtual bool postPatch(const parcelType& p, const polyPatch& pp, ...); // 撞 patch
virtual bool postFace(const parcelType& p, ...); // 在面上
这些钩子正是 §8.2 move() 里 cloud.functions().postFace/postMove(...) 与 §5.6 functions_.preEvolve/postEvolve 的目标。postMove/postPatch/postFace 的 bool 直接决定 keepParticle ------所以云函数对象不仅能统计,还能干预粒子寿命 (ParticleTrap/RemoveParcels/FreezeParticles)。
16.2 注册宏与清单
CloudFunctionObject.H(第 202--227 行)的 makeCloudFunctionObjectType(SS, CloudType) 是"每个函数对象一行注册"------与力模型/子模型完全同构,故"给云加统计量"只需改注册宏列表。
实现清单:FaceInteraction、FacePostProcessing、FreezeParticles、HeatTransferCoeff、KinematicReynoldsNumber、KinematicWeberNumber、NusseltNumber、ParticleCollector、ParticleDose、ParticleErosion、ParticleHistogram、ParticlePostProcessing、ParticleTracks、ParticleTrap、ParticleZoneInfo、PatchCollisionDensity、PatchInteractionFields、ReactingWeberNumber、RemoveParcels、ThermoReynoldsNumber、VoidFraction。
字典(aachenBomb):
bash
cloudFunctions
{
WeberNumber1 { type WeberNumber; } ReynoldsNumber1 { type ReynoldsNumber; }
NusseltNumber1 { type NusseltNumber; } HeatTransferCoeff1 { type HeatTransferCoeff; }
}
ParticleTracks 写轨迹 (供流线展示),与 IOPosition 写当前快照 (供云图)互补;VoidFraction/PatchCollisionDensity 输出体分数、壁面碰撞密度场;conversion/vtk、conversion/ensight 供 paraFoam/foamToVTK。
17. 稠密粒子:MPPIC
高浓度(流化床/鼓泡床/稠密气力输送)下稀疏假设失效。逐个解碰撞(PairCollision 弹簧-滑块-阻尼)极慢,MPPIC 用"回旋/统计 "处理四向耦合:粒子仍逐个追踪,但粒子-粒子碰撞/堆积的统计效应 经网格尺度三个模型施加于速度------PackingModel (堆积应力/排开力)、DampingModel (高浓度耗散脉动)、IsotropyModel(脉动重分配为各向同性)。
KinematicParcel::calcUCorrection(KinematicParcel.C 第 94--113 行):
cpp
this->UCorrect_ = Zero;
this->UCorrect_ = cloud.dampingModel().velocityCorrection(p, dt);
this->UCorrect_ += cloud.packingModel().velocityCorrection(p, dt);
UCorrect_ 随后在 calc 里 U_ += UCorrect_(§8.3);isotropyModel 在 motion() 后统一 calculate()(§5.5)。MPPIC 需要网格尺度平均场 (volumeAverage_/radiusAverage_/rhoAverage_/uAverage_...,§6.4),字典 solution.averagingMethod dual 指定方法。td.part() 的 tpVelocityHalfStep(半步速度)在 MPPIC 里用于先估速度、再堆积/各向同性修正。字典(Goldschmidt,§19.6):packingModel explicit + HarrisCrighton 应力(alphaPacked 0.65、pSolid 10.0、beta 2.0、eps 1.0e-7),isotropyModel stochastic + timeScaleModel isotropic。
18. 并行与追踪容错
18.1 跨进程迁移完整流程
Cloud::move(Cloud.C 第 141--300 行)用"流出-接收循环 "解决"粒子运动不受网格分解边界约束"的难题。阶段一:初始化(第 150--170 行):
cpp
const polyBoundaryMesh& pbm = pMesh().boundaryMesh();
const globalMeshData& pData = polyMesh_.globalData();
const labelList& procPatches = pData.processorPatches();
const labelList& procPatchNeighbours = pData.processorPatchNeighbours();
const labelList& neighbourProcs = pData.topology().procNeighbours();
for (ParticleType& p : *this) p.reset(); // stepFraction 复位
globalPositionsPtr_.clear();
取本进程所有 processor patch 及邻居进程,复位 stepFraction。阶段二:反复"发送-接收"直到无粒子可传(第 183--299 行节选):
cpp
PstreamBuffers pBufs; pBufs.allowClearRecv(false);
PtrList<UOPstream> UOPstreamPtrs(Pstream::nProcs()); // 流对象缓存(性能)
while (true)
{
pBufs.clear();
for (ParticleType& p : *this)
{
const bool keepParticle = p.move(cloud, td, trackTime);
if (keepParticle)
{
if (td.switchProcessor)
{
const label patchi = p.patch();
const label toProci = refCast<const processorPolyPatch>(pbm[patchi]).neighbProcNo();
auto* osptr = UOPstreamPtrs.get(toProci);
if (!osptr) { osptr = new UOPstream(toProci, pBufs); UOPstreamPtrs.set(toProci, osptr); }
p.prepareForParallelTransfer();
(*osptr) << procPatchNeighbours[patchi] << p; // 序列化 (patchi, particle)
deleteParticle(p); // 本进程立即摘除
}
}
else deleteParticle(p);
}
if (!Pstream::parRun()) break;
pBufs.finishedNeighbourSends(neighbourProcs);
if (!returnReduceOr(pBufs.hasRecvData())) break; // 任一进程有数据就继续
for (const label proci : neighbourProcs) // 接收
if (pBufs.recvDataCount(proci))
{
UIPstream is(proci, pBufs);
while (!is.eof())
{
label patchi = pTraits<label>(is);
auto* newp = new ParticleType(polyMesh_, is);
patchi = procPatches[patchi];
(*newp).correctAfterParallelTransfer(patchi, td);
addParticle(newp);
}
}
}
要点 :① while(true) 因一次移动可能跨多个 processor patch;② 序列化时把"目标 patch 在邻居进程的对应 patch 号 "和粒子一起写,接收端据此修正拓扑;③ 发出去的粒子在本进程立即删除 ;④ UOPstreamPtrs 缓存流对象避免反复 new;⑤ returnReduceOr(...) 归约保证所有进程节奏一致(否则死锁);⑥ 接收端 correctAfterParallelTransfer。
correctAfterParallelTransfer(particle.C 第 1070--1116 行节选):
cpp
const coupledPolyPatch& ppp = refCast<const coupledPolyPatch>(mesh_.boundaryMesh()[patchi]);
if (!ppp.parallel()) // AMI/非对齐:变换属性
{ const tensor& T = (ppp.forwardT().size()==1 ? ppp.forwardT()[0] : ppp.forwardT()[facei_]);
transformProperties(T); }
else if (ppp.separated()) // 分离循环边界:位置平移
{ const vector& s = (ppp.separation().size()==1 ? ppp.separation()[0] : ppp.separation()[facei_]);
transformProperties(-s); }
celli_ = ppp.faceCells()[facei_]; // 对面单元
facei_ += ppp.start(); // 局部面号→全局
tetFacei_ = facei_;
tetPti_ = mesh_.faces()[tetFacei_].size() - 1 - tetPti_; // 两侧三角方向相反
reflect();
注释(第 1112--1115 行) :粒子的笛卡尔位置不需显式变换------发送端与接收端的三角形互为变换,重心坐标不变,位置自然正确(重心坐标理念的又一红利)。
18.2 追踪容错与并行一致性
| 保护 | 位置 | 作用 |
|---|---|---|
maxNBehind_ = 10 |
particle.C 43 |
追踪在翻转四面体里死循环时强制终止 |
maxTrackAttempts = 1 |
KinematicParcel.C 38 |
单个时间步内重试追踪的次数上限 |
deleting lost particle |
Cloud.C 114--128 |
定位失败(cell == -1)的粒子被删除 |
if (dt > ROOTVSMALL) |
KinematicParcel.C 364 |
极小步跳过物理更新 |
并行一致性陷阱 :tetBasePtIs()/oldCellCentres() 强制构建(否则无粒子进程通信次序不匹配→僵死);统计量必须 reduce(否则各进程只拿局部值);注入模型 sumTriMagSf_ 全局累积保证均匀性;pBufs.allowClearRecv(false) 控制接收缓冲生命周期。
19. 字典配置全解(模板)
19.1 通用 cloudProperties 骨架
(constant/<cloudName>Properties,对 kinematicCloud/reactingCloud/sprayCloud 通用。)
cpp
FoamFile { version 2.0; format ascii; class dictionary; object kinematicCloudProperties; }
solution
{
active true; // 云是否参与计算
coupled true; // 双向耦合;false=单向
transient yes; // yes=瞬态;no=稳态
cellValueSourceCorrection off; // 子步内用最新源项修正连续相
maxCo 1.0; // 粒子 Courant(子步穿单元数上限)
interpolationSchemes
{
rho cell; // 体心值插值
U cellPoint; // 单元-点插值
mu cell;
}
integrationSchemes
{
U Euler; // 速度积分格式
T analytical;// 温度积分格式(thermo 云)
}
sourceTerms { schemes { U semiImplicit 1; rho explicit 1; } }
}
constantProperties { rho0 2526; alphaMax 0.9; } // 粒子密度、最大堆积体积分数
subModels { /* 见 §19.3 */ }
cloudFunctions { /* 见 §16.2 */ }
19.2 喷雾 ${sprayCloudProperties} 模板(摘自 aachenBomb)
真实可运行配置(tutorials/lagrangian/sprayFoam/aachenBomb/constant/sprayCloudProperties):
cpp
solution
{
active true;
coupled true;
transient yes;
cellValueSourceCorrection on;
maxCo 0.3;
sourceTerms { schemes { rho explicit 1; U explicit 1; Yi explicit 1; h explicit 1; radiation explicit 1; } }
interpolationSchemes { rho cell; U cellPoint; thermo:mu cell; T cell; Cp cell; kappa cell; p cell; }
integrationSchemes { U Euler; T analytical; }
}
constantProperties { T0 320; rho0 1000; Cp0 4187; constantVolume false; }
subModels
{
particleForces { sphereDrag; }
injectionModels
{
model1
{
type coneNozzleInjection;
SOI 0;
massTotal 6.0e-6;
parcelBasisType mass;
injectionMethod disc;
flowType flowRateAndDischarge;
outerDiameter 1.9e-4;
innerDiameter 0;
duration 1.25e-3;
position (0 0.0995 0);
direction (0 -1 0);
parcelsPerSecond 20000000;
flowRateProfile table ( (0 0.1272) ... (0.00125 3.9213) (1000 0.0000) );
Cd constant 0.9;
thetaInner constant 0.0;
thetaOuter constant 10.0;
sizeDistribution
{
type RosinRammler;
RosinRammlerDistribution { minValue 1e-06; maxValue 0.00015; lambda 0.00015; n 3; }
}
}
}
dispersionModel none;
patchInteractionModel standardWallInteraction;
heatTransferModel RanzMarshall;
compositionModel singlePhaseMixture;
phaseChangeModel liquidEvaporationBoil;
surfaceFilmModel none;
atomizationModel none;
breakupModel ReitzDiwakar; // 或 ReitzKHRT;
stochasticCollisionModel none;
radiation off;
standardWallInteractionCoeffs { type rebound; }
RanzMarshallCoeffs { BirdCorrection true; }
singlePhaseMixtureCoeffs { phases ( liquid { C7H16 1; } ); }
liquidEvaporationBoilCoeffs { enthalpyTransfer enthalpyDifference; activeLiquids ( C7H16 ); }
ReitzDiwakarCoeffs { solveOscillationEq yes; Cbag 6; Cb 0.785; Cstrip 0.5; Cs 10; }
TABCoeffs { y0 0; yDot0 0; Cmu 10; Comega 8; WeCrit 12; }
}
cloudFunctions
{
WeberNumber1 { type WeberNumber; } ReynoldsNumber1 { type ReynoldsNumber; }
NusseltNumber1 { type NusseltNumber; } HeatTransferCoeff1 { type HeatTransferCoeff; }
}
参数逐项说明与调参表:
| 参数 | 含义 | 推荐/陷阱 |
|---|---|---|
SOI |
开始注射时刻 s | 与 flowRateProfile 表起点对齐 |
massTotal |
本次注射总质量 kg | 与 parcelBasisType 配套 |
parcelBasisType |
mass/number |
喷雾常用 mass;弄反差数量级 |
parcelsPerSecond |
每秒 parcel 数 | 太大→慢;太小→统计噪声大 |
duration |
注射时长 s | 决定喷嘴开启窗口 |
outerDiameter/innerDiameter |
喷孔外/内径 m | imDisc 的圆盘半径范围 |
direction |
喷射轴方向 | 常与重力反向 |
thetaInner/thetaOuter |
内/外半锥角 deg | 雾锥张角 |
flowType |
flowRateAndDischarge |
U=V̇/(A·Cd) 定速度 |
Cd |
流量系数 | 0.9 常见 |
RosinRammler vs massRosinRammler |
数量 vs 质量分布 | 有实验 SMD 时优先 mass |
maxCo |
粒子 Courant | 0.3(喷雾,稳)~1.0(MPPIC,快) |
atomizationModel |
none/LISAAtomization/BlobsSheetAtomization |
需与喷嘴注入配套 |
breakupModel |
ReitzDiwakar/ReitzKHRT/TAB/ETAB/SHF/PilchErdman |
aachenBomb 用 ReitzDiwakar |
heatTransferModel |
RanzMarshall/NoHeatTransfer |
喷雾必开 |
phaseChangeModel |
liquidEvaporation/liquidEvaporationBoil |
有沸腾用 Boil |
dispersionModel |
none/stochasticDispersionRAS/gradientDispersionRAS |
需 k/epsilon 场 |
19.3 subModels 分节对照(模型名 → 源码目录)
subModels 键 |
值(可选) | 源码目录 |
|---|---|---|
particleForces |
sphereDrag,NonSphereDrag,WenYuDrag,ErgunWenYuDrag,PlessisMasliyahDrag,gravity,pressureGradient,virtualMass,SaffmanMeiLift,TomiyamaLift,Coulomb,... |
Kinematic/ParticleForces/ |
dispersionModel |
none,stochasticDispersionRAS,gradientDispersionRAS |
Kinematic/DispersionModel/ + turbulence/ |
injectionModels |
patchInjection,coneInjection,coneNozzleInjection,cellZoneInjection,manualInjection,fieldActivatedInjection,lookupTableInjection,... |
Kinematic/InjectionModel/ |
patchInteractionModel |
none,standardWallInteraction,rebound,recycle,localInteraction,multiInteraction |
Kinematic/PatchInteractionModel/ |
heatTransferModel |
none,RanzMarshall |
Thermodynamic/HeatTransferModel/ |
compositionModel |
singlePhaseMixture,singleMixtureFraction |
Reacting/CompositionModel/ |
phaseChangeModel |
none,liquidEvaporation,liquidEvaporationBoil |
Reacting/PhaseChangeModel/ |
devolatilisationModel/surfaceReactionModel |
none,singleKineticRateDevolatilisation/COxidationKineticDiffusionLimitedRate,... |
ReactingMultiphase/ |
surfaceFilmModel |
none,SurfaceFilm |
Kinematic/SurfaceFilmModel/ |
stochasticCollisionModel |
none,ORourke,trajectory,pairCollision |
Kinematic/StochasticCollision + CollisionModel |
atomizationModel |
none,LISAAtomization,BlobsSheetAtomization |
spray/submodels/AtomizationModel/ |
breakupModel |
none,TAB,ETAB,SHF,ReitzKHRT,ReitzDiwakar,PilchErdman |
spray/submodels/BreakupModel/ |
packingModel/dampingModel/isotropyModel |
none/explicit,implicit/relaxation/stochastic |
MPPIC/ |
19.4 injectionModels 三种典型写法
(a)面注入(patchInjection,摘自 verticalChannel):
ini
model1
{
type patchInjection;
SOI 0.01;
massTotal 8;
parcelBasisType mass;
patch inletCentral;
duration 10000;
parcelsPerSecond 1e5;
U0 (40 0 0);
flowRateProfile constant 1;
sizeDistribution { type general; generalDistribution { distribution ( (10e-06 0.0025) ... (90e-06 3.4688) ); } }
}
(b)喷嘴注入(coneNozzleInjection) :见 §19.2(含 injectionMethod/flowType/锥角/流量 profile)。
(c)手动注入(manualInjection,摘自 Goldschmidt):
ini
model1
{
type manualInjection;
massTotal 0;
parcelBasisType fixed;
nParticle 1;
SOI 0;
positionsFile "kinematicCloudPositions";
U0 (0 0 0);
sizeDistribution { type fixedValue; fixedValueDistribution { value 0.0025; } }
}
positionsFile 指向 constant/kinematicCloudPositions,可精确放置填充床/初始粒子。另有查表注入 (lookupTableInjection → inputFile "parcelInjectionProperties")。
19.5 parcelInjectionProperties 表(摘自 filter)
scss
// (x y z) (u v w) d rho mDot T Cp (Y0..Y2) (Yg0..YgN) (Yl0..YlN) (Ys0..YsN)
(
(0.1 0.32 0.0) (0.5 0.25 0.0) 0.001 1000 0.2 300 4200 (0 1 0) () (1) ()
(0.1 0.44 0.0) (0.5 0.10 0.0) 0.001 1000 0.2 300 4200 (0 1 0) () (1) ()
(0.1 0.56 0.0) (0.5 -0.10 0.0) 0.001 1000 0.2 300 4200 (0 1 0) () (1) ()
(0.1 0.68 0.0) (0.5 -0.25 0.0) 0.001 1000 0.2 300 4200 (0 1 0) () (1) ()
);
每列 (第一行注释即权威说明):位置 (x y z)、速度 (u v w)、直径 d、密度 rho、质量流率 mDot、温度 T、比热 Cp、组分质量分数(气/液/固相)。让用户逐粒子精确控制初值。
19.6 MPPIC ${kinematicCloudProperties}(摘自 Goldschmidt)
cpp
solution
{
active true; coupled true; transient yes;
cellValueSourceCorrection off;
maxCo 1.0;
interpolationSchemes { rho.air cell; U.air cellPoint; mu.air cell; }
averagingMethod dual;
integrationSchemes { U Euler; }
sourceTerms { schemes { U semiImplicit 1; } }
}
constantProperties { rho0 2526; alphaMax 0.9; }
subModels
{
particleForces { PlessisMasliyahDrag { alphac alpha.air; } gravity; }
injectionModels { model1 { type manualInjection; ... nParticle 1; positionsFile "kinematicCloudPositions"; ... } }
dispersionModel none;
patchInteractionModel localInteraction;
localInteractionCoeffs
{
patches ( "(top|bottom|walls|frontAndBack)" { type rebound; e 0.97; mu 0.09; } );
}
surfaceFilmModel none;
packingModel explicit;
explicitCoeffs
{
particleStressModel { type HarrisCrighton; alphaPacked 0.65; pSolid 10.0; beta 2.0; eps 1.0e-7; }
correctionLimitingMethod { type absolute; e 0.9; }
}
dampingModel none; // relaxation;
isotropyModel stochastic;
stochasticCoeffs { timeScaleModel { type isotropic; alphaPacked 0.65; e 0.9; } }
stochasticCollisionModel none;
}
cloudFunctions {}
MPPIC 用带体积分数的阻力 (PlessisMasliyahDrag 配 alphac alpha.air,从 α 场取局部值),localInteraction 按 patch 组给反弹系数,packingModel explicit+HarrisCrighton 处理堆积应力(alphaPacked=堆积体分数,pSolid/beta/eps=应力参数)。
19.7 fvOptions 与云的耦合
云通过求解器代码 反馈源项(clouds.SU(U) 进 UEqn,clouds.Sh(...) 进能量方程,§15.3);fvOptions 用于补充 外部源项。示例(filter/constant/fvOptions):
scss
massSource1 { type scalarSemiImplicitSource; timeStart 0.2; duration 2.0; volumeMode absolute;
selectionMode points; points ( (2.75 0.5 0) ); sources { rho (1e-4 0); H2O (1e-4 0); } }
momentumSource1 { type vectorSemiImplicitSource; ... sources { U ((0 0.005 0) 0); } }
energySource1 { type scalarSemiImplicitSource; ... sources { h (10 0); } }
clouds.SU(U) 与 fvOptions 源项同处一个矩阵装配链 (都进 fvMatrix),只是位置与量纲不同------云源项已在内部除了 V·Δt。
20. 常见报错/警告条目(≥20 条)
| # | 报错/警告 | 成因 | 排查 | 解决 |
|---|---|---|---|---|
| 1 | deleting lost particle at position (x y z)(Cloud.C 114--128) |
定位失败(celli_==-1),常见于动网格变形/AMI/时间步过大 |
网格负体积、maxCo、AMI |
减小 maxCo;修网格;开 cellValueSourceCorrection;必要时 ParticleTrap |
| 2 | Particle #n got stuck at (x y z)(particle.C 684--687) |
翻转/退化四面体使 nBehind_ 超阈值,追踪死循环 |
checkMesh 看 non-orthogonality/skewness/负体积 |
提高网格质量;降 maxCo;减小 deltaT |
| 3 | Particle U the same as patch ... removed(StandardWallInteraction.C 216--218) |
移动壁上粒子相对速度过小,被判"骑壁" | 壁面速度边界 fixesValue 是否过大 |
调 e/mu;检查壁面运动物理性 |
| 4 | Evaporation model selected, but no active liquids defined(LiquidEvaporation.C 84--86) |
activeLiquids 为空 |
查 phaseChangeModel 与 activeLiquids |
列蒸发组分 activeLiquids ( H2O ); |
| 5 | Parcel reached critical conditions: evaporating all available mass(LiquidEvaporation.C 159--161) |
液滴温度超临界温度 | 环境温压、TMax |
合理 T0/压力;此为物理应全蒸发情形 |
| 6 | Requested patch X not found(patchInjectionBase.C 56--59) |
patchInjection.patch 名字错 |
对照 boundary patch 名 |
改正 patch 名(大小写敏感) |
| 7 | Unknown interaction result type ...(StandardWallInteraction.C 86--90) |
type 非 escape/stick/rebound/none |
查枚举拼写 | 用合法值 |
| 8 | Ratio of surface of sphere ... (phi) must be ... (0,1](NonSphereDragForce.C 60--64) |
NonSphereDrag.phi 超范围 |
查 phi |
phi∈(0,1](球=1) |
| 9 | Changing tet without changing cell should only happen when the track is on triangle 1, 2 or 3(particle.C 239--242) |
追踪内部一致性被破坏 | 复现条件、网格质量 | 修网格;查 tetBasePtIs |
| 10 | The search for an edge-connected face and tet-point failed(particle.C 328--330) |
单元拓扑异常(ACMI/退化单元) | 检查该单元 | 修网格;避免退化单元 |
| 11 | Global positions are not available. ... storeGlobalPositions has not been called(Cloud.C 307--310) |
动网格 autoMap 前未存全局位置 |
确认 dynamicMesh 流程 |
由云框架正确调用 |
| 12 | size of particle ... not equal to size of positions(位置文件读取) |
位置文件与云尺寸不符(手工改文件) | 检查 0/<cloud>Positions |
删损坏文件让其重建 |
| 13 | Unknown injection model type ...(InjectionModelNew.C) |
injectionModels.*.type 非已注册 |
对比 §19.3 表 | 用合法模型名 |
| 14 | 注入量为 0 / no parcels to inject |
parcelsPerSecond 太小、deltaT 太小、nParticle<minParticlesPerParcel |
打印 parcelsAdded/massAdded |
增 parcelsPerSecond;减 minParticlesPerParcel;查 duration/SOI |
| 15 | 注入总质量与 massTotal 差数量级 |
parcelBasisType 与 sizeDistribution 不匹配(§12.4) |
核对 mass/number、lambda |
用 mass 配质量分布 |
| 16 | Limiting observed density in cell ... to rhoMin(KinematicParcel.C 59--61) |
插值密度 < rhoMin(真空/初值未设) |
查 rho 初值/边界 |
设合理 rho 初值;调 rhoMin |
| 17 | 云不收敛/残差振荡 | 源项过强、耦合太紧、maxCo 过大、未开 cellValueSourceCorrection |
看 UTrans/UCoeff min/max(§15.3 debug) |
减小源项松弛(sourceTerms.schemes);开修正;减 maxCo |
| 18 | We/Re 极大导致破碎频繁触发 |
相对速度大、sigma 小、时间步大 |
查 WeberLimit、maxCo |
调 WeberLimit/B0/B1;减 maxCo |
| 19 | 并行下粒子数异常/统计不守恒 | 缺 reduce、tetBasePtIs 未统一构建 |
对比串/并行结果 | 用 reduce 统计;勿手工删云函数 |
| 20 | 并行僵死在 finishedNeighbourSends |
各进程注入量/粒子数不一致致通信次序不匹配 | -parallel 复现;看每进程云尺寸 |
注入全局一致(sumTriMagSf_);避免 #if 分支 |
| 21 | 粒子突然消失(minParcelMass 相关) |
聚并/破碎后质量低于 minParcelMass 被删(§14.2) |
查 constantProperties.minParcelMass |
调小阈值(注意性能/守恒) |
| 22 | 温度被 TMin/TMax 夹紧 |
传热/蒸发数值过冲 | 查 constantProperties.TMin/TMax |
合理边界;T 用 analytical |
| 23 | Cannot find file .../<cloudName>Properties |
云字典名与求解器期望不符 | 查求解器(kinematicCloudProperties 等) |
改名或改求解器 cloudPropertiesName |
| 24 | 粒子撞到未处理 patch 类型(empty/wedge) |
patch 类型不在交互处理链中 | 查边界类型 | constraint 类型由框架自动处理;检查自定义 patch |
排查通用手法 :① 开 debug------system/controlDict 加 DebugSwitches { Cloud 1; particle 1; KinematicCloud 1; }(打开 §6.4/§15.3 的 WarningInFunction/Pout);② 看云的 info()(每步打印粒子数/质量/Dij/Dmax/逃逸粘附统计);③ 对 UTrans/UCoeff min/max(§15.3 debug);④ 网格先行 (checkMesh,多数"卡住/丢失/发散"源于网格质量);⑤ Courant 先行 (maxCo 是粒子的"安全阀",§8.2,调小几乎总能缓解追不上/穿面)。
21. 对比/选型表(≥5 张)
表 1:粒子受力模型对比
| 模型 | 适用 | 关键修正 | 依赖字典 |
|---|---|---|---|
SphereDrag |
稀疏相球颗粒 | CdRe=24(1+Re^{2/3}/6)(Re≤1000) |
无 |
NonSphereDrag |
非球颗粒 | 球形度 phi 的 Haider-Levenspiel |
phi |
WenYuDrag |
稠密相(含 α) |
Wen-Yu 修正 | alphac |
ErgunWenYuDrag |
高浓度床层 | Ergun(高α)+ WenYu(低α) | alphac |
PlessisMasliyahDrag |
填充/流化床 | Plessis-Masliyah | alphac |
DistortedSphereDrag |
变形液滴 | 变形修正 | phi/变形量 |
TomiyamaDrag |
气泡 | Tomiyama 相关式 | 表面张力等 |
表 2:蒸发/相变模型对比
| 模型 | 物理 | 传热/传质类比 | 何时用 |
|---|---|---|---|
none |
无 | --- | 纯运动/传热 |
liquidEvaporation |
蒸发 | Sh=2+0.6Re^{1/2}Sc^{1/3} |
常温喷雾蒸发 |
liquidEvaporationBoil |
蒸发+沸腾 | 同 + 沸腾段 | 高温/闪蒸(aachenBomb) |
liquidEvaporation + BirdCorrection true |
含斯蒂芬流修正 | N/NCpW/Cs |
大量蒸发/高蒸气压 |
表 3:破碎模型对比
| 模型 | 机制 | 关键参数 | 适用 |
|---|---|---|---|
none(NoBreakup) |
不破碎 | --- | 稳定滴 |
TAB |
弹簧-阻尼-质量振荡 | WeCrit,Cmu,Comega,y0,yDot0 |
低 We、液滴振荡 |
ETAB |
TAB 增强(含黏性耗散) | 同 TAB | 更广 We |
SHF |
袋式/剥离分型 | Cbag,Cb,Cstrip,Cs |
由 ReitzDiwakar 派生 |
ReitzDiwakar |
袋式+剥离 | Cbag,Cb,Cstrip,Cs |
柴油喷雾(aachenBomb) |
ReitzKHRT |
KH(剪切)+RT(加速)双波 | B0,B1,Ctau,CRT,msLimit,WeberLimit |
高压柴油喷雾(最全) |
PilchErdman |
实验关联破碎 | --- | 需经验数据时 |
表 4:注射模型对比
| 模型 | 注入位置 | 典型用途 | 关键字典 |
|---|---|---|---|
manualInjection |
表文件 | 初始床层/精确布点 | positionsFile,nParticle |
patchInjection |
patch 面 | 入口面喷入 | patch,U0,sizeDistribution |
patchFlowRateInjection |
patch 面(按流量) | 指定质量流率 | patch,flowRate |
cellZoneInjection |
cellZone | 体注入 | cellZone,numberOfParcels |
coneInjection |
锥形 | 一般锥形喷雾 | thetaInner/Outer |
coneNozzleInjection |
喷孔/圆盘 | 真实喷嘴 | injectionMethod,flowType,Cd |
fieldActivatedInjection |
场激活区域 | 按场条件触发 | activateField |
lookupTableInjection |
表文件 | 逐粒子指定 | inputFile |
表 5:耦合方式对比
| 方式 | coupled |
反馈源项 | 插值 | 典型算例 |
|---|---|---|---|---|
| 单向(one-way) | false |
否 | 是 | uncoupledKinematicParcelFoam |
| 双向(two-way) | true |
是 | 是 | reactingParcelFoam/sprayFoam |
| 四向(four-way,MPPIC/碰撞) | true |
是 | 是 | MPPICFoam+packingModel/collision |
| 子步强耦合 | true+cellValueSourceCorrection on |
是(子步内) | 是 | 稠密/强换热 |
表 6:积分格式对比
| 格式 | 公式 | 特点 | 适用 |
|---|---|---|---|
Euler |
dt/(1+βdt) |
无条件稳定、显式化 | 速度 U |
analytical |
(1−e^{−βdt})/β |
精确 | 温度 T(刚性大) |
22. 完整调用链与小结
22.1 调用链
scss
时间步开始
→ clouds.evolve() (求解器对每个云调用)
→ KinematicCloud::solve(*this, td)
→ preEvolve(td) §5.6 缓存 dispersion/forces 场、构建 cellOccupancy
→ evolveCloud(cloud, td) §5.4
├─ resetSourceTerms() §15.2 rhokTrans/UTrans/UCoeff 归零
├─ surfaceFilm().inject(cloud) §5.4 表面膜注入
├─ injectors_.inject(cloud, td) §12 注射(setPositionAndCell/setProperties/nParticle→move)
├─ cloud.motion(cloud, td) §5.5 td.part()=tpLinearTrack
│ └─ Cloud::move(cloud, td, trackTime) §18.1
│ └─ 每 parcel: p.move(cloud, td, trackTime) §8.2
│ ├─ 子步 f = min(1-stepFraction, maxCo, maxCo·l/|s|)
│ ├─ p.trackToFace(f·s-d, f) §3 四面体追踪
│ ├─ setCellValues §6.4 插值 rhoc/Uc/muc
│ ├─ calcDispersion §10 湍流扩散
│ ├─ cellValueSourceCorrection §9.4 子步内修正
│ ├─ calcUCorrection §17 MPPIC
│ ├─ calc §8.3
│ │ ├─ calcHeatTransfer §9.4 传热
│ │ ├─ calcPhaseChange §9.3 蒸发
│ │ ├─ calcVelocity §7.5 受力+积分
│ │ └─ 累加 UTrans/UCoeff/hsTrans/YTrans §15
│ ├─ functions().postFace/postMove §16 云函数对象
│ └─ hitFace → hitPatch §11 壁面/边界交互
│ ├─ processor→switchProcessor §18.1
│ ├─ wall→patchInteraction §11.2
│ └─ cyclic/AMI→变换+反射
└─ stochasticCollision().update(...) §14 碰撞聚并
→ info() §20 打印粒子数/质量/Dij/Dmax
→ postEvolve(td) §5.6 释放缓存、写输出
→ 求解器把 clouds.SU(U)/Sh(...) 加入 UEqn/hEqn/YEqn §15.3
→ PIMPLE / 时间推进
每个括号里的模型名,都是 constant/<cloud>Properties 里可配置的一项------这就是 OpenFOAM 拉格朗日框架可用性与可扩展性的全部来源。
22.2 三层结构再强化
| 层 | 类 | 职责 |
|---|---|---|
| 基础 | particle(IDLList<particle>::link)、Cloud<ParticleType>(IDLList) |
重心坐标存储、四面体追踪、并行迁移、IO、自动映射 |
| 中间 | KinematicParcel(+模板链)、KinematicCloud(组合 ParticleForceList+CloudFunctionObjectList) |
运动/受力/传热/相变/反应(全子模型化) |
| 应用 | spray/、coalCombustion/、MPPIC、DSMC、molecularDynamics、solidParticle |
场景特化 |
22.3 设计哲学对照
- 连续相 (第一季)用连续数组 + 表达式模板,追求零拷贝、向量化、与网格对齐;
- 离散相 (本章)用侵入式链表 + 子模型列表,追求 O(1) 增删、可插拔、随机访问无关;
- 两者共享同一套运行时选择(RTS)+ 注册宏 + 字典装配 ,都以"模板派生自非模板基类"获得类型无关的统一操作能力;
- 重心坐标 + 四面体追踪 是离散相独有的几何基石,把"穿面"这件高频操作化为纯符号操作 (
reflect/rotate),并用maxNBehind_兜住数值坏情形; forceSuSp的 Su/Sp 分解 与dtEff的半隐式 是离散相独有的"线性化 + 稳定化"套路,与连续相fvm::Sp思想异曲同工;- 源项除以
V·Δt是连接两相量纲的唯一桥梁,也是"守恒"落地的最后一公里。
一句话收束:OpenFOAM 拉格朗日框架 = 一条侵入式链表(particle/Cloud)+ 一条模板链(Kinematic→Thermo→Reacting→Multiphase)+ 一群 RTS 子模型 + 一套重心坐标追踪算法;喷雾不过是在这条链上再挂两个破碎/雾化模型而已。
(本章完)