《OpenFOAM 源码深度剖析》系列-第 7 章 网格与拓扑体系:`polyMesh`、`primitiveMesh` 与 patch

第 7 章 网格与拓扑体系:polyMesh、primitiveMesh 与 patch

有限体积法的空间基础是网格。OpenFOAM 的网格模型有一个鲜明特点:它用最少的原始数据(点、面、owner、neighbour)表达任意多面体网格,再按需"派生"出所有拓扑与几何量。 理解这套"原始数据 + 按需派生"的机制,是读懂 OpenFOAM 一切空间离散(梯度、散度、拉普拉斯)的前提。

7.1 类层次:网格的三层结构

scss 复制代码
objectRegistry                         (第 3 章的注册表)
    ▲
primitiveMesh                          "Cell-face mesh analysis engine"
    ▲
polyMesh                               (通用多面体网格, 含 IO 与边界)
    ▲
fvMesh                                 (有限体积网格, 含 Sf/Cf/C 等; 第 8 章)
  • polyMesh(src/OpenFOAM/meshes/polyMesh/polyMesh.H,第 81--85 行)同时继承 objectRegistry 与 primitiveMesh:
cpp 复制代码
class polyMesh
:
    public objectRegistry,
    public primitiveMesh

继承 objectRegistry 意味着网格本身是一个可注册对象 ,因此它能被注册到 Time 的注册表树里,也能承载自己的子注册表(U、p 等场都注册在 mesh 上)。继承 primitiveMesh 则获得了拓扑分析能力。

7.2 primitiveMesh:拓扑分析引擎

primitiveMesh(primitiveMesh.H,第 27--52 行)的定位是 Cell-face mesh analysis engine。它只保存"原始数据 + 按需计算的派生数据"。

7.2.1 原始数据

diff 复制代码
- 点数 nPoints_, 内部点数 nInternalPoints_
- 边数 nEdges_, 内部边的分级 nInternal0Edges_/nInternal1Edges_/nInternalEdges_
- 面数 nFaces_, 内部面数 nInternalFaces_
- 单元数 nCells_

7.2.2 派生数据(demand-driven addressing)

primitiveMesh.H 第 124--147 行列出了全部连通性(connectivity)表。它们全部是按需计算 的(用 std::unique_ptr 缓存):

表 含义 表 含义
ccPtr_ cell-cells(单元相邻) efPtr_ edge-faces
ecPtr_ edge-cells pfPtr_ point-faces
pcPtr_ point-cells cePtr_ cell-edges
cfPtr_ cell-faces fePtr_ face-edges
edgesPtr_ 边列表 pePtr_ point-edges
ppPtr_ point-points cellShapesPtr_ 单元形状

这些表由 primitiveMesh* 系列文件各自计算(源文件列表见第 34--50 行):primitiveMeshCellCells.C、primitiveMeshEdges.C、primitiveMeshCellPoints.C 等等。

访问方式是"惰性"的。以面中心为例(primitiveMeshFaceCentresAndAreas.C 第 77--86 行):

cpp 复制代码
const Foam::vectorField& Foam::primitiveMesh::faceCentres() const
{
    if (!faceCentresPtr_)
    {
        //calcFaceCentresAndAreas();
        const_cast<primitiveMesh&>(*this).updateGeom();
    }
    return *faceCentresPtr_;
}

第一次访问才计算,之后缓存命中------这就是 OpenFOAM 网格"省内存、省时间"的核心策略:不用拓扑量的算例(如纯几何操作)根本不会触发计算。

7.3 owner / neighbour:面与单元的邻接关系

7.3.1 数据结构

polyMesh 保存(polyMesh.H 第 121--137 行):

cpp 复制代码
// Points
pointIOField points_;
// Faces
faceCompactIOList faces_;
// Face owner
labelIOList owner_;
// Face neighbour
labelIOList neighbour_;
  • points_:所有顶点坐标;
  • faces_:每个面的顶点列表(面由顶点索引构成);
  • owner_[facei]:面的所有者单元;
  • neighbour_[facei]:面的邻居单元(仅内部面有)。

关键约定:内部面被排在前面 ,neighbour_ 只覆盖内部面(长度 = nInternalFaces);边界面的 neighbour 不存在,其归属由 patch 表示。这个"内部面在前、boundary face 在后"的排序是全部寻址逻辑的基石。

7.3.2 owner/neighbour 如何从 cell-faces 推导

polyMeshInitMesh.C 的 initMesh(cellList& c)(第 109--163 行)展示了这个推导:

cpp 复制代码
forAll(c, celli)
{
    const labelList& cellfaces = c[celli];
    forAll(cellfaces, facei)
    {
        if (!markedFaces[cellfaces[facei]])
        {
            // First visit: owner
            owner_[cellfaces[facei]] = celli;
            markedFaces[cellfaces[facei]] = true;
        }
        else
        {
            // Second visit: neighbour
            neighbour_[cellfaces[facei]] = celli;
            nInternalFaces++;
        }
    }
}
neighbour_.setSize(nInternalFaces);

逻辑极简:第一次遇到某个面的单元是 owner,第二次遇到的是 neighbour 。只有被两个单元共享的面才是内部面(nInternalFaces++);只被一个单元看到的面是边界面。

另一段(第 37--56 行)处理向后兼容 :老格式里 neighbour_ 常与 owner_ 等长、末尾用 -1 填充,这里检测到等长就扫描截断到第一个 -1。

7.4 边与点的拓扑:upper-triangular ordering

边的推导比面复杂,因为"面"是多边形(顶点任意),而"边"是两点之间的连线。primitiveMeshEdges.C 的 calcEdges(第 280 行起)用了一个重要技巧------把边按"上三角"(upper-triangular)顺序排列。

核心思想:对于每条边,规定编号较小的顶点为起点、较大的为终点。于是:

  • 遍历每个点的邻接点时,把"邻点编号 > 本点编号"的边归入"internal0Edges"区(正常内部边);对"邻点编号 < 本点编号"的记为 -1(已在别处登记)。
  • 用 SortableList<label> nbrPoints 对邻点做排序,从而把边映射到一个确定的新顺序(第 306--346 行)。
cpp 复制代码
SortableList<label> nbrPoints(primitiveMesh::edgesPerPoint_);
forAll(pe, pointi)
{
    const DynamicList<label>& pEdges = pe[pointi];
    ...
    forAll(pEdges, i)
    {
        const edge& e = es[pEdges[i]];
        label nbrPointi = e.otherVertex(pointi);
        nbrPoints[i] = (nbrPointi < pointi) ? -1 : nbrPointi;
    }
    nbrPoints.sort();

    if (nInternalPoints_ == -1)
    {
        // Sort all upper-triangular
        forAll(nbrPoints, i)
        {
            if (nbrPoints[i] != -1)
            {
                label edgeI = pEdges[nbrPoints.indices()[i]];
                oldToNew[edgeI] = internal0EdgeI++;
            }
        }
    }
    ...
}

为什么要做 upper-triangular ordering? 因为有限体积算法(尤其是矩阵寻址)需要"边/面索引具有一致的方向性",才能高效地在并行、映射、重编号时保持数据局部性。把边按 (min, max) 顶点编号排序,等于给边一个规范化方向 ,让 owner/neighbour 之外的拓扑对象也能被稳定寻址。

7.4.1 内部边的分级

primitiveMesh.H 记录了三级内部边计数:

  • nInternal0Edges_:两端都不是边界点的内部边;
  • nInternal1Edges_:至多一端是边界点的内部边;
  • nInternalEdges_:至多两端是边界点的内部边。

这种"分级"是为了让边界处理能快速定位到"涉及边界点"的边,避免每次遍历所有边。

7.5 几何量:散度定理的工程实现

7.5.1 面中心与面面积

primitiveMeshFaceCentresAndAreas.C 的 calcFaceCentresAndAreas()(第 41--72 行)委托给 primitiveMeshTools::makeFaceCentresAndAreas:

cpp 复制代码
faceCentresPtr_ = std::make_unique<vectorField>(nFaces());
auto& fCtrs = *faceCentresPtr_;
faceAreasPtr_ = std::make_unique<vectorField>(nFaces());
auto& fAreas = *faceAreasPtr_;
primitiveMeshTools::makeFaceCentresAndAreas(*this, points(), fCtrs, fAreas);

该文件头(第 31 行)说明:面中心用"面中心平均"或"面积加权平均",可应对轻微凹面。

面面积矢量 Sf 是有限体积法的核心量(它是带方向的面积,法向由顶点环绕顺序决定),其分解(三角形化后求和)也在此计算。

7.5.2 体心与体积:Gauss 散度定理

primitiveMeshCellCentresAndVols.C(第 38--78 行):

cpp 复制代码
cellCentresPtr_ = std::make_unique<vectorField>(nCells(), Zero);
cellVolumesPtr_ = std::make_unique<scalarField>(nCells(), Zero);
primitiveMeshTools::makeCellCentresAndVols
(
    *this, faceCentres(), faceAreas(), cellCtrs, cellVols
);

这是**散度定理(Gauss 定理)**的离散实现。对任意标量/向量场,∫_V ∇·f dV = ∮_S f·n dS。取 f = 1 得体积 V = (1/3)∮_S x·n dS;取 f = x 得体心 V·C = (1/2)∮_S (x·n) x dS(或等价形式)。OpenFOAM 正是在 primitiveMeshTools 里用"每个面的 Sf × 面中心"累加、再除以 3(体积)或 2(体心矩)得到这些量。

这解释了为什么 polyMesh 只需要点、面、owner、neighbour 四组数据------所有几何量都能由它们通过散度定理唯一确定。

7.6 patch:边界面的组织

7.6.1 polyPatch 与 polyBoundaryMesh

polyPatch(polyPatch.H,第 30--34 行):

cpp 复制代码
Description
    A patch is a list of labels that address the faces in the global face list.
    The patch can calculate its own edges based on the global faces.
    Patch also contains all addressing between the faces.

类定义(第 72--93 行):

cpp 复制代码
class polyPatch
:
    public patchIdentifier,
    public primitivePatch
{
    //- Start label of this patch in the polyMesh face list
    label start_;
    const polyBoundaryMesh& boundaryMesh_;
    mutable std::unique_ptr<labelList::subList> faceCellsPtr_;
    mutable std::unique_ptr<labelList> mePtr_;
    ...
};

要点:

  1. patch 只是一段"全局面列表"的连续区间 :start_ 是起始面标签,长度由 size() 给出。所以 patch 的"面"其实就是去掉前 start_ 后的一个 sublist。
  2. patch 自己是一个 primitivePatch:它能基于全局面独立计算自己的边与局部寻址。
  3. faceCellsPtr_:patch 面到单元的映射(demand-driven)。
  4. polyPatch 有自己的 RTS 表(第 142--155 行 declareRunTimeSelectionTable(autoPtr, polyPatch, word, ...)),因此 patch 类型也能从字典里按名字选择。

polyBoundaryMesh(polyBoundaryMesh.H,第 30--69 行):

cpp 复制代码
Description
    A polyBoundaryMesh is a polyPatch list with registered IO,
    a reference to the associated polyMesh,
    with additional search methods etc.

class polyBoundaryMesh
:
    public polyPatchList,      // = PtrList<polyPatch>
    public regIOobject
{
    const polyMesh& mesh_;
    mutable autoPtr<labelList> patchIDPtr_;      // 每个面的 patch id
    ...
};

它是一堆 patch 组成的列表 (PtrList<polyPatch>)+ IO 注册。patchIDPtr_ 提供"从全局面标签反查 patch id"的能力------这是把"面"与"patch"关联起来的关键索引。

7.6.2 patch 的派生家族

polyPatch 的派生子类覆盖了所有边界类型(在 src/OpenFOAM/meshes/polyMesh/polyPatches/ 下):

patch 类型 物理/几何含义
emptyPolyPatch 空面(2D/轴对称中"被移除"的方向)
wallPolyPatch 壁面
symmetryPolyPatch 对称面
symmetryPlanePolyPatch 对称平面
cyclicPolyPatch 周期性边界
processorPolyPatch 并行分解产生的处理器间界面(第 12 章)
coupledPolyPatch 耦合界面的基类(cyclic/processor 都派生于此)
wedgePolyPatch 楔形(轴对称 2D)

此外还有 genericPolyPatch、mappedPolyPatch、nonConformalCyclicPolyPatch 等。

7.7 网格状态与动态网格

polyMesh 定义了 readUpdateState(polyMesh.H 第 94--100 行):

cpp 复制代码
enum readUpdateState
{
    UNCHANGED,
    POINTS_MOVED,
    TOPO_CHANGE,
    TOPO_PATCH_CHANGE
};

这用于后处理/动网格:当从不同时间目录读入的网格发生变化时,区分"只是点移动了"、"拓扑变了"、"patch 变了"。相应地:

  • polyMeshUpdate.C 处理更新;
  • polyMeshClear.C 清理派生数据;
  • meshState 保存网格/求解器状态。

动态网格工具在 src/dynamicMesh/、src/dynamicFvMesh/、src/topoChangerFvMesh/ 中,提供 polyTopoChange(拓扑变更)、hexRef8(AMR 八叉树加密/粗化)、syncTools(同步)、meshTools(通用工具)等。fvMesh 会在网格变化后重算 Sf 等几何量(第 8 章)。

7.8 网格生成:blockMesh

网格从哪来?最基础的是块结构化网格生成器 blockMesh(applications/utilities/mesh/generation/blockMesh/)。它读 system/blockMeshDict,用"顶点 + 块(block)+ 边(edges)+ 边界(boundary)"描述一个或多个六面体块,再将其剖分为 polyMesh。src/Allwmake 第 100 行特意把 mesh/blockMesh 排在 dynamicMesh 之前编译,因为后者依赖它。

更复杂的网格来自 snappyHexMesh(基于 STL 几何的笛卡尔网格加密)、cfMesh(外部模块)等。

7.9 小结:为什么这样设计

OpenFOAM 的网格模型可以总结为三条设计原则:

  1. 最小原始数据:只存 points / faces / owner / neighbour,其它一切按需派生。省内存、省 IO。
  2. 规范化拓扑顺序:内部面前置、边按 upper-triangular 排序,让所有寻址逻辑有稳定的、可并行的方向性。
  3. 几何量由散度定理唯一确定:faceCentres/faceAreas/cellCentres/cellVolumes 全部可由原始数据算出,不需额外存储。

下一章把场(第 5 章)与网格(本章)结合起来:GeometricField 与边界条件体系。


本章要点回顾

  • 网格类层次:objectRegistry + primitiveMesh → polyMesh → fvMesh;primitiveMesh 是"拓扑分析引擎"。
  • 原始数据只有 points / faces / owner / neighbour ;连通性表(cell-cells、edge-faces、point-cells...)全部 demand-driven 惰性计算。
  • 内部面前置 、neighbour_ 只覆盖内部面;owner/neighbour 由 cell-faces "第一次=owner、第二次=neighbour" 推导。
  • 边采用 upper-triangular ordering(min/max 顶点编号规范化)以获得稳定方向性;内部边分三级计数。
  • 几何量由 Gauss 散度定理 计算:faceCentres/faceAreas 与 cellCentres/cellVolumes。
  • polyPatch 是全局面列表的一段连续区间 (start_ + size),自身也是一个 primitivePatch,并有自己的 RTS 表;polyBoundaryMesh = PtrList<polyPatch> + IO。
  • readUpdateState(UNCHANGED/POINTS_MOVED/TOPO_CHANGE/TOPO_PATCH_CHANGE)支撑动网格与后处理。
相关推荐
CHEEVEN_QY2 小时前
热仿真在液冷板设计中的应用价值与局限性客观评估
人工智能·算法
无忧.芙桃2 小时前
图的精髓:邻接矩阵与遍历全解析
c++·算法·面试·深度优先·广度优先
Nil2082 小时前
leetcode 1143最长公共子序列
算法·leetcode·职场和发展
脚踏实地,坚持不懈!2 小时前
Linux 内存回收机制:水位线、kswapd 与 Direct Reclaim
linux·jvm·算法
文慧的科技江湖2 小时前
2026年10月7日算力行业招投标早报:算力网进了“六张网”协调机制,调度软件仍没有标段 | 慧知开源智算调度平台
开源·算力·算力调度·智算·招投标
Logic1012 小时前
C语言/数据结构动态规划题解:字符串变回文的最少操作次数——最长回文子序列(LPS)
c语言·数据结构·算法·动态规划·时间复杂度·回文串·最长回文子序列
小南知更鸟2 小时前
浙大 nanoMuse 开源:不用抢 Meta Muse,跨设备个人 Agent 能动手、可自托管
开源
ting94520002 小时前
Kilo Code for JetBrains 深度技术剖析:原生开源编程智能体内部实现与工程实践
人工智能·架构·开源
guhunyihan3 小时前
开源无代码平台的技术逻辑,恐象 AI 的架构思路解析
大数据·人工智能·开源