OpenFOAM 源码深度剖析 · 第二季-03 拉格朗日粒子与喷雾:`Cloud`/`parcel` 模板体系、运动积分与子模型群

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 本章路线

  1. 基础层: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_;

逐行解读:

  1. public IDLList<particle>::link ------全章题眼。particle 不是"被容器装着的普通对象",而是链表节点本身 。于是 Cloud<ParticleType> : public IDLList<ParticleType> 直接继承 整条链表;云里的粒子集合不需任何额外容器 ;每个粒子能自己找到前驱后继。这是与 Field(连续数组)最根本的分野。
  2. sizeofPosition/sizeofFields ------粒子序列化 (并行传递、IO、重启)时的字节大小。位置数据与属性数据分开,使"只要位置"的后处理(IOPosition 的 POSITIONS 模式)只需传位置部分。
  3. 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;
}

逐段解读:

  1. f = 1 是"剩余可走位移比例";每命中一个四面体内部三角形就把 f 乘上本次子步命中比例,累积"实际走了多远"。
  2. trackToTri 返回 tetTriI :-1=在当前四面体内走完(返回 0);0=撞到单元的面 (记录 facei_,返回 f);1/2/3=撞到四面体内部三角形 ,changeTet 换到相邻四面体。
  3. nBehind_ 防死循环 :注释(particle.H 第 160--166 行)解释------当单元被负体积四面体 (大变形网格)切分时,一条直线轨迹 可能在正/负空间重叠区里绕成闭环 :几何上每次都在前进,但"走到的最远点"(behind_)其实没增加,于是 nBehind_ 累加;超过 maxNBehind_=10 就强制终止 ,把 stepFraction_ 推进到子步末并复位计数。
  4. 警告 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 行起)流程:

  1. 取当前四面体正向变换 T(stationaryTetReverseTransform(centre, detA, T),第 722 行),形如 detA·y = (x − centre) & T;
  2. 把位移 x1 用 T 变成重心空间位移 barycentric Tx1(x1 & T)(第 732 行);
  3. 在重心空间里四面体的四个面是 y_i = 0 的超平面,命中退化成四个一维线性方程;
  4. 取最小正命中分数对应的三角形,记为 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;

逐段解读:

  1. Fcp/Fncp 分开 :只有"与 Uc 相关"的力(阻力、压力梯度、虚拟质量)进 Fcp,其余(重力...)进 Fncp;后者做显式、前者可半隐式。
  2. 系数 :acp 耦合常数项;ancp 非耦合力 + 外部显式源;bcp 耦合线性系数(阻力刚度,即 §8 的 β)。
  3. 积分 :UIntegrator().delta(U_, dt, acp+ancp, bcp) 解线性 ODE dU/dt=(acp+ancp)−bcp·U。
  4. 拆 deltaUcp/deltaUncp :只有耦合部分算作对连续相的动量反馈(重力是外力、不应改变连续相动量)。
  5. Spu = dt*Fcp.Sp() :传给连续相动量方程的线性化系数 ,与 dUTrans 一起构成 §15 的 UTrans/UCoeff。
  6. 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;

这是整个拉格朗日时间推进的心脏,逐段:

  1. 循环条件 :三重------没被删、没换进程、本步没走完。一个时间步被拆成多个子步 (受 Courant 限制),stepFraction 从 0 累加到 1。

  2. 位移 s = trackTime*U_:本步总位移(匀速假设)。

  3. Courant 双 min :

    cpp 复制代码
    f = min(f, maxCo);                          // 时间 Courant
    f = min(f, maxCo*l/max(SMALL*l, mag(s)));   // 空间 Courant

    保证"单位子步穿过的单元数 ≤ maxCo "。这就是字典 maxCo(喷雾默认 0.3、MPPIC 默认 1.0)的真实含义------它限制每子步穿过的单元数,而非时间步本身。越小越稳越慢;越大越快但可能一步穿多单元导致位置错误。

  4. trackToFace(f*s - d, f) :四面体空间里追踪到下一个面,只走 f 比例(§3.1)。

  5. 子步 dt :随后 setCellValues→calcDispersion→cellValueSourceCorrection→calcUCorrection→calc(§15)。

  6. 钩子 :postFace/postMove 交云函数对象(§16);在面上则 hitFace→patch 分派(§11)。

  7. 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;  // 质量传递
    }

数学推导 ④(单液滴蒸发质量流率):

  1. 饱和压力 pSat = pv(pc,T)(液滴温度下的蒸汽压);
  2. 表面浓度 Cs = pSat/(RR·Ts)(RR 通用气体常数,Ts 膜温),单位 kmol/m³;
  3. 远场浓度 Cinf = Xc·pc/(RR·Ts)(Xc 为连续相该组分摩尔分数);
  4. Sherwood 数 Sh → 传质系数 kc = Sh·Dab/d(膜理论);
  5. 摩尔通量 Ni = max(kc·(Cs−Cinf), 0)------max(·,0) 保证只蒸发不凝结;
  6. 质量流率 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);

逐段解读:

  1. prepareForNextTimeStep :基类据派生模型的 parcelsToInject/volumeToInject 决定本步注几个 parcel、总体积,返回 false 则不注入。
  2. deltaT :本步里实际落在注射窗口内 的时长(把起止时刻两个钳位都处理);padTime 处理"注射在本步中途才开始"。
  3. 伪时间 timeInj :把 newParcels 均匀铺在窗口内,保证注入流时间连续(而非一帧全出)。
  4. setPositionAndCell (派生模型实现)、setProperties (设速度/直径/组分)、setParcelThermoProperties/checkParcelProperties (云负责)------注入的核心落在派生模型的两个函数上。
  5. if (celli > -1):定位失败即丢弃("注射失败"常见原因之一,§20)。
  6. nParticle >= minParticlesPerParcel_ :太少则丢弃并把体积记账(delayedVolume)------"注入质量不足/不守恒"的隐形来源(§20)。
  7. 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,则

    ini 复制代码
    nParticle = 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 子模型 + 一套重心坐标追踪算法;喷雾不过是在这条链上再挂两个破碎/雾化模型而已。


(本章完)

相关推荐
hetao17338371 小时前
校内场-提高组 ZYZSC-S-Round 2
c++·算法
小严不想当码农1 小时前
icpc 亚洲区域赛补题链接整理
算法
xiaok1 小时前
Compositor图编辑器
算法
CAD开发小慧1 小时前
CAE软件开发:Coreform为何用ACIS处理CAD几何?
算法·架构·计算机图形学
七牛云行业应用1 小时前
DeepSeek Harness 自定义 Skill 教程:从 SKILL.md 到可复用 Agent 工作流
算法·github
hetao17338371 小时前
2026-10-14 hetao1733837 的刷题记录
c++·算法
Vitaliy Kostrov(柯维远)1 小时前
谁来承担额外工作?CDN 参与者如何协商资源
nginx·缓存·系统架构·开源·cdn
分布式存储与RustFS1 小时前
指标、日志、链路进同一个桶:GreptimeDB 存算分离解读与 RustFS 接入配置
运维·云原生·开源·对象存储·分布式存储·s3·性能基准
罗斯8391 小时前
Datalog与安全分析
算法·安全·网络安全