第 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_;
...
};
要点:
- patch 只是一段"全局面列表"的连续区间 :
start_是起始面标签,长度由size()给出。所以 patch 的"面"其实就是去掉前start_后的一个 sublist。 - patch 自己是一个
primitivePatch:它能基于全局面独立计算自己的边与局部寻址。 faceCellsPtr_:patch 面到单元的映射(demand-driven)。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 的网格模型可以总结为三条设计原则:
- 最小原始数据:只存 points / faces / owner / neighbour,其它一切按需派生。省内存、省 IO。
- 规范化拓扑顺序:内部面前置、边按 upper-triangular 排序,让所有寻址逻辑有稳定的、可并行的方向性。
- 几何量由散度定理唯一确定: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)支撑动网格与后处理。