Gmsh 源码深度解析-第 10 章 求解器框架、API 与语言绑定
本章目标:讲清 Gmsh 内置有限元求解器(
src/solver/)的四层架构------FunctionSpace(有限元 空间)、Dof/dofManager(自由度分配与组装)、femTerm/terms(弱形式项)、linearSystem(后端无关的线性系统)------以及对外 API 的自动生成机制、C/Fortran/Python/Julia/JavaScript 五语言绑定的实现机理、Gmsh 程序主干(命令行、批处理/交互、单例上下文)与 Onelab 参数/求解器 集成。最后给出与前面各章的接口闭环与全系列结语。第 8 章建立了"形函数/基函数/数值积分"的数值内核;本章看这些内核如何被组织成一个能真正 求解偏微分方程的框架,以及 Gmsh 如何把这条能力以一份规格发射到五种语言。
10.1 求解器模块的定位与分层
10.1.1 一个被低估的模块
src/solver/ 在 Gmsh 里扮演"低调但关键"的角色。它提供一套内置有限元求解器 ,同时被一个 看似无关的功能复用:重参数化(parametrization) 。回忆第 1 章的 CMake 选项 (CMakeLists.txt 第 94 行):
cmake
opt(SOLVER "Enable built-in finite element solvers (required for reparametrization)" ${DEFAULT})
也就是说,当 Gmsh 需要把一张复杂曲面"展平"到参数平面(用于曲面网格、纹理映射、几何分析)时, 它把这当成一个偏微分方程求解问题 ,用 solver 模块来解。这体现了 Gmsh 的一种务实哲学: 能用同一套数值基础设施解决的,就复用它 。第 2 章的 GModelParametrize (src/geo/GModelParametrize.h)就是这条复用链的终端:它用 laplaceTerm 组装一张调和映射(harmonic map)的线性系统,解出曲面上每个节点的 (u,v)。
10.1.2 四层架构
求解器模块的类按层次排列,每层只依赖下一层:
text
第 4 层 具体求解器(elasticitySolver/thermicSolver/eigenSolver/frameSolver)+ 项表达式(terms.h)
第 3 层 弱形式项(femTerm<T> 及 helmholtzTerm/laplaceTerm/elasticityTerm ...)
第 2 层 自由度与组装(Dof / dofManager<T> / SElement)
第 1 层 有限元空间(FunctionSpace<T> + BasisFactory,复用第 8 章)
第 0 层 线性代数后端(linearSystem<scalar> + CSR/PETSc/MUMPS/Eigen/GMM)
核心思想是每一层都可独立替换或扩展:换一个线性后端(第 0 层)不必改有限元空间(第 1 层); 增加一个新弱形式项(第 3 层)可沿用现有组装器与后端。
10.1.3 与第 8 章的关系
第 8 章讲"给定 MElement,如何求形函数值/梯度/海森/积分点",那是纯数值函数 问题;本章的 第 1 层(FunctionSpace)在其上加拓扑与代数语义 :第 8 章的 MElement::getShapeFunctions(u,v,w,sf) 给"本单元第 i 个形函数在参考点的值",而 FunctionSpace<T>::f(...) 给"本单元各形函数的值"、getKeys(ele, keys) 给"第 i 个形函数对应 哪个全局 Dof"。一句话:第 8 章回答"形函数长什么样",本章回答"形函数属于哪个空间、对应哪个 自由度、如何组装成线性系统"。
10.1.4 文件清单
src/solver/ 下的文件按层归类如下(按实际存在列出):
| 层 | 文件 | 作用 |
|---|---|---|
| 有限元空间 | functionSpace.h |
FunctionSpace<T> 及一族实现 |
| 自由度 | dofManager.h/.cpp |
Dof、dofManager<T>、约束与组装 |
| 单元装配 | SElement.h/.cpp |
SElement(MElement 的求解器视图) |
| 弱形式(基类) | femTerm.h |
femTerm<T>、节点级边界条件 |
| 弱形式(实现) | helmholtzTerm.h、laplaceTerm.h、distanceTerm.h、elasticityTerm.h/.cpp |
具体项 |
| 术语表达式 | terms.h、terms.hpp、materialLaw.h、solverField.h、solverAlgorithms.h |
运算符风格组装 |
| 积分规则 | quadratureRules.h |
QuadratureBase/GaussQuadrature |
| 线性系统 | linearSystem.h 及各 linearSystem*.h/.cpp |
后端抽象与实现 |
| 稀疏结构 | sparsityPattern.h/.cpp |
非零元预估 |
| 具体求解器 | elasticitySolver.h/.cpp、thermicSolver.h、eigenSolver.h/.cpp、frameSolver.h |
应用级求解器 |
| 辅助 | filters.h、groupOfElements.h、STensor*.h |
筛选、分组、张量类型 |
注意几个张量类型 文件:STensor33.h、STensor43.h、STensor53.h、STensor63.h 定义了求解器 专用的三阶/四阶/五阶/六阶张量,它们是 TensorialTraits 得以"随 T 自动升级梯度类型"的物质 基础。第 8 章的 STensor3、SVector3 也在其中复用。
10.2 FunctionSpace:traits 驱动的通用有限元空间
10.2.1 TensorialTraits:让同一份代码服务标量/向量/张量
src/solver/functionSpace.h 的开头定义了一族 traits:
cpp
template <class T> struct TensorialTraits { // 主模板:值类型升级规则(标量语义)
typedef T ValType;
typedef T GradType[3];
typedef T HessType[3][3];
typedef T ThirdDevType[3][3][3];
};
template <> struct TensorialTraits<double> { // 标量:值 double,梯度升 SVector3
typedef double ValType; typedef SVector3 GradType;
typedef STensor3 HessType; typedef double TensProdType;
typedef STensor33 ThirdDevType;
};
template <> struct TensorialTraits<SVector3> { // 向量:梯度升 STensor3
typedef SVector3 ValType; typedef STensor3 GradType;
typedef STensor3 HessType; typedef STensor3 TensProdType;
typedef STensor3 ThirdDevType;
};
template <> struct TensorialTraits<STensor3> { // 张量:张量积升 STensor43
typedef STensor3 ValType;
typedef STensor43 TensProdType;
};
这是整个求解器模块的设计基石 :它让同一份 FunctionSpace<T> 代码,在 T = double (标量场,如温度)、T = SVector3(向量场,如位移)、T = STensor3(张量场,如应力)下都能 工作。关键在于梯度/海森/三阶导的类型随 T 自动升级:
- 标量(
double)的值是标量,梯度升为SVector3,海森升为STensor3,三阶导升为STensor33; - 向量(
SVector3)的值是向量,梯度升为二阶张量STensor3,海森仍是STensor3(向量海森 关于两个求导指标对称,故可用STensor3表达而不需要STensor33); - 张量(
STensor3)的值的"张量积"(用于弱形式里σ:ε这类双线性收缩)升为四阶张量STensor43。
C++ 模板的偏特化 机制在这里被用到极致:主模板给出"标量语义"的默认升级规则,三个偏特化 覆盖 double/SVector3/STensor3 三种"基础值类型"。任何新的值类型(比如复数、四元数)都可以 再加一个偏特化而无需改动上层代码。这就是"抽象先行、内核可插拔"原则在最底层的体现。
10.2.2 FunctionSpaceBase:与模板无关的公共接口
为了让"组装循环"能在不知道 T 的情况下遍历有限元空间,Gmsh 抽出一个非模板基类:
cpp
class FunctionSpaceBase {
public:
virtual ~FunctionSpaceBase() {}
virtual int getId(void) const = 0;
virtual int getNumKeys(MElement *ele) const = 0; // 自由度个数
virtual void getKeys(MElement *ele, std::vector<Dof> &keys) const = 0;
virtual void getKeysOnVertex(MElement *ele, MVertex *v,
const std::vector<int> &comp,
std::vector<Dof> &keys) const
{
Msg::Warning("this function is defined to get Dofs of vertex %d on element %d",
v->getNum(), ele->getNum());
}
virtual FunctionSpaceBase *clone(const int id) const { return nullptr; }
};
getId() 返回场号(field id),用于在组装时区分不同物理场;getNumKeys/getKeys 是 "单元形函数 ↔ 全局自由度"的映射 ;clone 用于复制空间并赋予新 id(多场问题里常见)。
getKeysOnVertex 的默认实现只是打印一个警告,这暴露了一个设计现实:真正需要"按顶点取 自由度"的空间(如某些高阶/混合元)才会覆盖它,其余空间并不需要。
10.2.3 FunctionSpace:对形函数求值的模板接口
模板类 FunctionSpace<T> 是核心抽象:
cpp
template <class T> class FunctionSpace : public FunctionSpaceBase {
protected:
int _iField; // field number (used to build dof keys)
public:
typedef typename TensorialTraits<T>::ValType ValType;
typedef typename TensorialTraits<T>::GradType GradType;
typedef typename TensorialTraits<T>::HessType HessType;
virtual int getId(void) const { return _iField; }
virtual void f(MElement *ele, double u, double v, double w,
std::vector<ValType> &vals) const = 0; // 物理坐标下的值
virtual void gradf(MElement *ele, double u, double v, double w,
std::vector<GradType> &grads) const = 0; // 物理梯度
virtual void hessfuvw(MElement *ele, double u, double v, double w,
std::vector<HessType> &hess) const = 0; // 参考域海森
virtual void fuvw(...) const {} // 参考坐标下的值(默认空)
virtual void gradfuvw(...) const {} // 参考梯度(默认空)
virtual void hessf(...) const {} // 物理海森(高阶 FEM)
virtual void thirdDevfuvw(...) const {}; // 三阶导(高阶 FEM)
virtual void thirdDevf(...) const {};
virtual int getNumKeys(MElement *ele) const = 0;
virtual void getKeys(MElement *ele, std::vector<Dof> &keys) const = 0;
};
这里能读出三条设计信息:
- 纯虚 vs 带默认实现 :
f/gradf/hessfuvw/getNumKeys/getKeys是纯虚的,必须实现;fuvw/gradfuvw/hessf/thirdDevf有空的默认实现,注释写着"should return to pure virtual once all is done"------说明这是一次渐进式的接口收紧,作者有意让旧空间先编译通过, 再逐步补齐。 - "物理"与"参考"两套求值 :
f/gradf返回物理坐标下的量与梯度(已做雅可比变换);fuvw/gradfuvw返回参考坐标下的量(未变换)。后者在需要"先求和再统一变换"的场合更高效。 - 三阶导为高阶 FEM 预留 :
thirdDev*服务于高阶(≥3 阶)单元的弱形式,其中会出现形函数的 三阶导数。默认不实现,因为大多数问题用不到。
命名里的 f、fuvw、gradf、gradfuvw 遵循同一个模式:后缀 uvw 表示"在参考坐标下 求值"。这个约定贯穿整个模块。
10.2.4 ScalarLagrangeFunctionSpace:标量 Lagrange 元
最常用的实现是标量 Lagrange 元。Gmsh 提供了两个版本,差别只在"是否处理父单元":
ScalarLagrangeFunctionSpaceOfElement(第 121 行):不做父单元解引用;ScalarLagrangeFunctionSpace(第 212 行):每个方法都有if(ele->getParent()) ele = ele->getParent();。
后者的存在是为了高阶/分层单元:高阶单元往往是"低阶父单元 + 附加自由度"的结构,求形函数时 应回到父单元。第 7 章的高阶曲边、第 8 章的层次基(hierarchical basis)都用这个机制。
先看 OfElement 版本的 f 与 gradf(第 135--164 行):
cpp
virtual void f(MElement *ele, double u, double v, double w,
std::vector<ValType> &vals) const
{
int ndofs = ele->getNumShapeFunctions();
int curpos = vals.size();
vals.resize(curpos + ndofs);
ele->getShapeFunctions(u, v, w, &(vals[curpos]));
}
virtual void gradf(MElement *ele, double u, double v, double w,
std::vector<GradType> &grads) const
{
int ndofs = ele->getNumShapeFunctions();
grads.reserve(grads.size() + ndofs);
double gradsuvw[256][3];
ele->getGradShapeFunctions(u, v, w, gradsuvw);
double jac[3][3];
double invjac[3][3];
ele->getJacobian(u, v, w, jac); // redondant : on fait cet appel a l'exterieur
inv3x3(jac, invjac);
for(int i = 0; i < ndofs; ++i) { // ∇_x φ = J^{-T} ∇_ξ φ
GradType g;
for(int a = 0; a < 3; ++a)
g(a) = invjac[a][0]*gradsuvw[i][0] + invjac[a][1]*gradsuvw[i][1] + invjac[a][2]*gradsuvw[i][2];
grads.push_back(g);
}
}
f 直接转调第 8 章的 MElement::getShapeFunctions ------这就是"数值内核复用"的直接证据。 gradf 则是有限元教材里的链式法则 ∇xϕ=J−T∇ξϕ:
ele->getGradShapeFunctions(u,v,w,gradsuvw)拿参考坐标梯度 ∇ξϕi;ele->getJacobian(u,v,w,jac)拿雅可比 J=∂x/∂ξ;inv3x3(jac, invjac)求 J−1;- 用
invjac把参考梯度变换为物理梯度。
第 3 步用 Numeric.h 的 inv3x3(3×3 显式求逆)。法语注释 // redondant : on fait cet appel a l'exterieur 提醒 getJacobian 被重复调用,应在外层缓存 jac/invjac。
hessfuvw(第 167 行)把 getHessShapeFunctions 的三维数组展平成 HessType(九个分量),用于高阶弱形式;gradfuvw(第 189 行)返回未变换的参考梯度。
getKeys(第 203 行)是"起点":
cpp
virtual void getKeys(MElement *ele, std::vector<Dof> &keys) const // appends ...
{
int ndofs = ele->getNumShapeFunctions();
keys.reserve(keys.size() + ndofs);
for(int i = 0; i < ndofs; ++i) getKeys(ele->getShapeFunctionNode(i), keys);
}
它遍历单元的每个形函数,取其形函数节点 (getShapeFunctionNode(i),注意区别于几何 顶点 getVertex(i)),对每个节点生成一个 Dof(节点号, 场号)。私有重载:
cpp
virtual void getKeys(MVertex *ver, std::vector<Dof> &keys) const
{
keys.push_back(Dof(ver->getNum(), _iField));
}
一句话概括 :一个 Lagrange 形函数 = 一个节点 = 一个自由度,Dof 的 entity 是节点编号, type 是场号。
10.2.5 ScalarToAnyFunctionSpace:从标量空间"长"出向量/张量空间
向量场(如位移 u)并不是另写一套代码,而是由标量空间按分量组合 得到。抽象是 ScalarToAnyFunctionSpace<T>(第 319 行):
cpp
template <class T> class ScalarToAnyFunctionSpace : public FunctionSpace<T> {
protected:
std::vector<T> multipliers; // 各分量的基向量(如 e_x, e_y, e_z)
std::vector<int> comp; // 各分量对应的分量号
FunctionSpace<double> *ScalarFS; // 底层标量空间
public:
template <class T2>
ScalarToAnyFunctionSpace(const T2 &SFS, const T &mult, int comp_)
: ScalarFS(new T2(SFS))
{
multipliers.push_back(mult); comp.push_back(comp_);
}
// 另有 2/3 分量构造
};
它的 f 把标量形函数值乘以基向量再拼接:ScalarFS->f(ele,u,v,w,valsd) 取标量值后 vals.push_back(multipliers[j] * valsd[i])(对每个分量 j、每个形函数 i)。gradf 则用 tensprod(multipliers[j], gradsd[i], val)------标量梯度(向量)× 基向量 → 二阶张量 ,正是 向量场梯度的定义 ∇(u)=∑iui⊗∇ϕi。
getKeys(第 425 行)体现了自由度 type 的打包技巧:
cpp
for(int j = 0; j < nbcomp; ++j) {
for(int i = 0; i < nk; ++i) {
int i1, i2;
Dof::getTwoIntsFromType(bufk[i].getType(), i1, i2);
keys.push_back(Dof(bufk[i].getEntity(),
Dof::createTypeWithTwoInts(comp[j], i1)));
}
}
即:把标量自由度的 type(场号)取出,与"分量号 comp[j]"重新打包。向量场的每个自由度由 (节点号, (分量号, 场号))唯一标识 。这就是 10.3.2 节"type 用 10000 进制打包两个整数" 的用途。
基于 ScalarToAnyFunctionSpace<SVector3> 的向量空间有两个版本:
VectorLagrangeFunctionSpaceOfElement(第 446 行);VectorLagrangeFunctionSpace(第 482 行,处理父单元)。
它们都提供 enum Along { VECTOR_X, VECTOR_Y, VECTOR_Z } 与三个基向量 BasisVectors[3]。构造函数重载支持"只取某些分量":VectorLagrangeFunctionSpace(id, VECTOR_X, VECTOR_Y) 表示"二维向量场"(x/y 两个分量),这正是 2D 弹性用到的(10.8.1 节)。
10.2.6 CompositeFunctionSpace:多场拼接
多物理问题(如弹性 + Lagrange 乘子、位移 + 压力)需要把多个空间拼成一个 。 CompositeFunctionSpace<T>(第 515 行)持有 std::vector<FunctionSpace<T>*> _spaces,各方法 把子空间的贡献顺序追加:
cpp
virtual void f(...) const {
for(iterFS it = _spaces.begin(); it != _spaces.end(); ++it)
(*it)->f(ele, u, v, w, vals); // 各子空间把结果 append 到同一个 vals
}
virtual int getNumKeys(MElement *ele) const {
int ndofs = 0;
for(...) ndofs += (*it)->getNumKeys(ele);
return ndofs;
}
virtual void getKeys(MElement *ele, std::vector<Dof> &keys) const {
for(...) (*it)->getKeys(ele, keys); // append
}
注释里那句 // appends ... 到处出现,正是这套"顺序追加"约定的自我说明。组装时,各子空间的 局部矩阵按块对角拼接------这就是混合元/多场问题的自然实现。
10.2.7 xFemFunctionSpace 与 FilteredFunctionSpace:高级扩展
xFemFunctionSpace<T>(第 592 行)实现扩展有限元(XFEM) :在基空间上加一个富集函数 simpleFunctionOnElement<double> *_funcEnrichment(如裂纹面附近的阶跃/奇异函数)。富集形函数 = 基形函数 × 富集函数;其梯度用乘积法则 ∇(NF)=∇N⋅F+N∇F (tensprod(valsd[i], gradfuncenrich, GradFunc) 外部积 + gradsd[i]*func)。富集自由度用 Dof::createTypeWithTwoInts(i1, i2 + 1) 打上"+1"标记(第 763 行),与普通自由度区分------这是 "同一节点、两组形函数(普通+富集)"的编码技巧 。FilteredFunctionSpace<T,F>(第 620 行) 持有一个谓词 F *_filter,在 getKeys/f/gradf 里只保留满足谓词的自由度与形函数值, 保证两者一一对应(用于"只对边界节点做富集"这类需求)。
10.2.8 与 BasisFactory / ElementType 的复用关系
本章的 FunctionSpace 完全建立在第 8 章的形函数接口之上 :f → getShapeFunctions; gradf → getGradShapeFunctions + getJacobian;hessfuvw → getHessShapeFunctions; getKeys → getNumShapeFunctions + getShapeFunctionNode。也就是说,它不是重新发明形函数, 而是给形函数接口加上"拓扑/代数"语义(节点↔Dof 映射、场号、分量、富集) 。两者还有一个重要 区别:第 8 章的基函数**按单元类型(ElementType/polynomialBasis)**给("几何形状+阶数+族别" 三元组的函数),本章的 FunctionSpace **按场(field)**给("物理量类型+分量"的函数);单元要 有形函数靠 ElementType,场要有自由度靠 FunctionSpace,两者通过 MElement 汇合(场的 形函数值取自单元的基函数,场自由度的 entity 取自单元的节点 )。在 FunctionSpace 之上, terms.h(第 10.6 节)进一步把 FunctionSpace<T> 用作 BilinearTerm<T1,T2> 的成员------形函数 空间的类型 T 直接决定了弱形式的类型。
10.3 Dof 与 dofManager:自由度分配与组装
10.3.1 Dof:极简的二元组
自由度 Dof(src/solver/dofManager.h 第 19 行)是整个框架的 "原子":
cpp
class Dof {
protected:
// v(x) = \sum_f \sum_i v_{fi} s^f_i(x)
long int _entity; // "i": node, edge, group, etc.
int _type; // "f": basis function type index, etc.
public:
Dof(long int entity, int type) : _entity(entity), _type(type) {}
inline long int getEntity() const { return _entity; }
inline int getType() const { return _type; }
inline static int createTypeWithTwoInts(int i1, int i2) { return i1 + 10000 * i2; }
inline static void getTwoIntsFromType(int t, int &i1, int &i2)
{ i1 = t % 10000; i2 = t / 10000; }
bool operator<(const Dof &other) const
{ if(_entity < other._entity) return true; if(_entity > other._entity) return false;
return _type < other._type; }
bool operator==(const Dof &other) const
{ return (_entity == other._entity && _type == other._type); }
};
类头注释 v(x) = \sum_f \sum_i v_{fi} s^f_i(x) 是全部语义的浓缩:解 v(x) 是各场 f、各基 函数 i 的系数 vfi 与形函数 sif(x) 的乘积之和 。一个 Dof 就是"第 f 个场在第 i 个实体上的系数"的索引。
_entity 的类型是 long int(不是 int),这是因为节点编号在大网格里可能超过 32 位 (第 4 章的重编号保证全局唯一)。_type 是 int,用来编码"场号 + 分量号"等元信息。
10.3.2 type 的 10000 进制打包
createTypeWithTwoInts(i1, i2) 用 i1 + 10000 * i2 把两个整数打包进一个 int,getTwoIntsFromType 用 % 和 / 解包。约定是:i1 是较小的量(分量号,0--2),i2 是较大的量(场号)。10000 的选择是"分量号最小取值空间"的工程估计------任何分量号只要 < 10000 就不会与场号混淆。
这套打包让"标量场/向量场的第几分量/高阶第几个形函数/富集与否"都能用一个 int 表达:
- 标量场:
type = 0 + 10000*field,等价于type = field(因为i1=0); - 向量场分量
c:type = c + 10000*field; - XFEM 富集:
type = i1 + 10000*(field + 1); elasticityMixedTerm里还会出现iComp = 3 + ...表示"压力/膨胀场分量"(10.5.7 节)。
10.3.3 dofTraits:让自由度可以是标量或矩阵
自由度"值"的类型不是固定的。dofTraits<T> 统一了"矩阵 × 向量 = 向量"的操作:
cpp
template <class T> struct dofTraits { // 自由度值类型:标量
typedef T VecType; typedef T MatType;
inline static void gemm(VecType &r, const MatType &m, const VecType &v,
double alpha, double beta)
{ r = beta * r + alpha * m * v; }
};
template <class T> struct dofTraits<fullMatrix<T> > { // 自由度值类型:矩阵
typedef fullMatrix<T> VecType; typedef fullMatrix<T> MatType;
inline static void gemm(VecType &r, const MatType &m, const VecType &v,
double alpha, double beta)
{ r.gemm(m, v, alpha, beta); }
};
关键 :对普通 T(double),VecType/MatType 都是 T,gemm 退化为标量乘加;对 fullMatrix<T>,VecType/MatType 都是 fullMatrix<T>,gemm 转调矩阵乘。 dofManager<T> 内部所有"值"的类型都是 dataVec = dofTraits<T>::VecType。
这意味着同一个 dofManager 既能管理"节点标量自由度",也能管理"节点张量自由度(局部刚度 块)"------后者在处理接触/内变量等"每个节点带一个小矩阵"的问题时有用。
代码里还注释掉了一个 dofTraits<fullVector<std::complex<double>>> 的偏特化(第 70--76 行), 提示复数自由度曾被考虑但未启用。
10.3.4 DofAffineConstraint:仿射约束
cpp
template <class T> class DofAffineConstraint {
public:
std::vector<std::pair<Dof, typename dofTraits<T>::MatType> > linear; // 参与线性组合的自由度及系数
typename dofTraits<T>::VecType shift; // 常数项
};
它表达"某自由度 = Σ 系数×其他自由度 + 常数",即 uk=∑iαiui+β。这类仿射约束在工程里极为常见:
- 周期边界 :
u_右 = u_左(系数 1、shift 0); - 从属自由度(悬挂节点) :把粗单元边中点自由度表示为两端点的平均(10.4 节注释里的
FillHangingNodes思路); - 接触/多点约束:从节点位移 = 主节点位移的线性组合;
- 刚体运动约束:某些自由度由若干主自由度决定。
dofManager 用独立约束(constraints) 与固定(fixed) 两种机制实现:
cpp
// general affine constraint on sub-blocks, treated by adding equations:
// Dof = \sum_i dataMat_i x Dof_i + dataVec
std::map<Dof, DofAffineConstraint<dataVec> > constraints;
// fixations on full blocks, treated by eliminating equations:
// DofVec = dataVec
std::map<Dof, dataVec> fixed;
区别在于:固定自由度是从方程组里"消元"(直接置值) ;仿射约束是"加一个方程" (约束行不动主方程,而是把约束自由度替换成主自由度的线性组合)。这两种处理在 assemble/assembleLinConst/insertInSparsityPatternLinConst 里各有专门分支。
10.3.5 dofManagerBase:与并行相关的非模板部分
dofManagerBase(第 86 行)把与 MPI 相关的字段 放进非模板基类,避免在头文件里 #include mpi.h:
cpp
class dofManagerBase {
protected:
std::map<Dof, int> unknown; // 未知自由度 → 全局编号
std::map<Dof, Dof> associatedWith; // 别名关系(一个 dof 指向另一个)
std::map<Dof, std::pair<int, int> > ghostByDof; // 影子自由度 → (procId, 全局号)
std::vector<std::vector<Dof> > ghostByProc, parentByProc;
int _localSize;
bool _parallelFinalized, _isParallel;
void _parallelFinalize();
dofManagerBase(bool isParallel) { ... }
};
四个映射各有分工:
| 字段 | 含义 |
|---|---|
unknown |
编号表:每个"待求解的未知量"对应一个 0..N-1 的全局(局部)编号 |
associatedWith |
别名 :Dof a 实际等同 Dof b(用于"一个自由度等价于另一个"的场合) |
ghostByDof |
影子:本进程上没有主人、但需要同步的自由度,记录其属主进程与全局号 |
ghostByProc/parentByProc |
按进程分组的影子/父自由度,供并行通信 |
注释坦率地写着 this is a first try, maybe not the final implementation------并行自由度管理是 Gmsh 求解器里最不成熟的角落之一 。这也解释了为什么大多数学术用途(单机、小规模)走的是 linearSystemFull/CSR,而不启用 PETSc 并行。
10.3.6 组装器 dofManager:四个映射 + 线性系统
dofManager<T>(第 113 行)在基类之上加了模板化的三个映射与对线性系统的引用:
cpp
std::map<Dof, DofAffineConstraint<dataVec> > constraints; // 仿射约束
std::map<Dof, dataVec> fixed; // 固定值
std::map<Dof, std::vector<dataVec> > initial; // 初值(未用)
linearSystem<dataMat> *_current;
std::map<const std::string, linearSystem<dataMat> *> _linearSystems; // 可多套系统 (A/B)
std::map<Dof, T> ghostValue;
_linearSystems 支持同时管理多套线性系统(如 A、B) ,通过 setCurrentMatrix(name) 切换。 这在以下场景有用:
- 块求解/预条件:用 B 系统算预条件子;
- 特征值问题 :需要 (K, M) 两个矩阵(第 10.8.6 节的广义特征值 Kx=λMx);
- 多步耦合:交替使用不同的矩阵结构。
10.3.7 numberDof:分配全局编号
编号的核心是 numberDof(Dof key)(第 213 行):
cpp
virtual inline void numberDof(Dof key)
{
if(associatedWith.find(key) != associatedWith.end()) return; // 别名:不单独编号
if(fixed.find(key) != fixed.end()) return; // 固定:从方程组消去
if(constraints.find(key) != constraints.end()) return; // 约束:由主自由度决定
if(ghostByDof.find(key) != ghostByDof.end()) return; // 影子:归属其他进程
auto it = unknown.find(key);
if(it == unknown.end()) {
std::size_t size = unknown.size();
unknown[key] = size; // 新自由度:编号 = 当前未知量个数
}
}
这段代码是"编号策略"的全部 :只有"既不是别名、又不是固定、又不是约束、又不是影子"的 自由度才获得一个 unknown 编号。编号是惰性的、第一次遇到时按出现顺序分配 ------Gmsh 不做 全局排序(那会增加一遍遍历),编号顺序完全由"组装遍历元素的顺序"决定。注释里的 size = unknown.size() 就是这个顺序。
正则 numberDof(const std::vector<Dof> &R) 对一组自由度逐个编号。numberVertex(v, iComp, iField) 是便捷重载(第 231 行):
cpp
inline void numberVertex(MVertex *v, int iComp, int iField)
{
numberDof(v->getNum(), Dof::createTypeWithTwoInts(iComp, iField));
}
这就是"向量场某分量的自由度"的编号方式。
10.3.8 fixDof / isFixed / isAnUnknown / isConstrained
cpp
virtual inline void fixDof(Dof key, const dataVec &value)
{
if(unknown.find(key) != unknown.end()) return; // 已编号则不再固定(先到先得)
fixed[key] = value;
}
inline void fixVertex(MVertex const *v, int iComp, int iField, const dataVec &value)
{
fixDof(v->getNum(), Dof::createTypeWithTwoInts(iComp, iField), value);
}
注意"先到先得" :fixDof 发现该自由度已进 unknown 就忽略 固定请求。这在 elasticitySolver::assemble 的注释里被明确点出:
we first do all fixations. the behavior of the dofManager is to give priority to fixations: when a dof is fixed, it cannot be numbered afterwards
也就是说:编号函数会跳过 fixed 的自由度,而 fixDof 遇到已编号的自由度会放弃 ------两者配合, 保证"只要先 fixDof 再 numberDof,固定自由度就一定不会被编号进未知量"。这就是"先做 Dirichlet,再做编号"的严格必要性(10.8.2 节)。
四个谓词则分别查询:
cpp
bool isFixed(Dof) { return fixed.find(key) != fixed.end(); }
bool isAnUnknown(Dof) { if(ghostValue.find(key)==end) return unknown.find(key)!=end; return false; }
bool isConstrained(Dof){ return constraints.find(key) != constraints.end(); }
10.3.9 assemble:四种重载与"固定/约束"的消元
assemble 是整个组装器的核心,有四个重载:
assemble(Dof R, Dof C, dataMat value)(第 393 行):单个矩阵元。assemble(vector<Dof> &R, vector<Dof> &C, fullMatrix<dataMat> &m)(第 419 行):局部 矩阵,行列自由度不同(非对称/混合元)。assemble(vector<Dof> &R, fullVector<dataMat> &m)(第 478 行):线性形式(右端项)。assemble(vector<Dof> &R, fullMatrix<dataMat> &m)(第 517 行):对称局部矩阵(行列同一 组自由度)。
第 3 个重载(线性形式)说明"约束如何消元" (第 478 行):若第 i 个自由度是未知量,直接 _current->addToRightHandSide(NR[i], m(i));若是仿射约束,则用 dofTraits<T>::gemm(tmp, constraint.linear[j].second, m(i), 1, 0) 把它的载荷按约束系数"转移"到主自由度上 ------这正是 uk=∑iαiui 的对称处理。
第 4 个重载(对称矩阵)是"固定自由度消元"的主场(第 517 行):
cpp
for(std::size_t i = 0; i < R.size(); i++) {
if(NR[i] != -1) {
for(std::size_t j = 0; j < R.size(); j++) {
if(NR[j] != -1) _current->addToMatrix(NR[i], NR[j], m(i, j)); // 常规:加进矩阵
else {
itFixed = fixed.find(R[j]);
if(itFixed != fixed.end()) {
dataVec tmp(itFixed->second);
dofTraits<T>::gemm(tmp, m(i, j), itFixed->second, -1, 0);
_current->addToRightHandSide(NR[i], tmp); // 移到右端项
}
else assembleLinConst(R[i], R[j], m(i, j)); // 约束
}
}
}
else { for(std::size_t j = 0; j < R.size(); j++) assembleLinConst(R[i], R[j], m(i, j)); }
}
这就是 Dirichlet 边界条件的标准消元 :若第 j 列自由度已固定(fixed[j] = g_j),则 ∑jKijuj=fi 里的已知项 −Kijgj 被移到右端项------"移项"就是那句 dofTraits<T>::gemm(tmp, m(i,j), itFixed->second, -1, 0) 的全部含义 。assembleLinConst (第 654 行)进一步处理"约束自由度出现在行/列"的情形:把 value 向主自由度"散射",并把 -value*shift 加到 RHS------这是仿射约束的通用消元实现 ,对所有 T 都成立。
10.3.10 getDofValue 与 getDofNumber
求解完成后,如何从 Dof 取值?getDofValue(Dof key, dataVec &val)(第 273 行)按优先级链 查找:
- 别名 :若
associatedWith里有映射,转到目标自由度; - 影子 :
ghostValue里有值 → 返回; - 未知量 :
unknown里有编号 →_current->getFromSolution(编号, val); - 固定值 :
fixed里有 → 返回固定值; - 仿射约束 :
constraints里有 → 计算 ∑αiui+β(递归调用getDofValue)。
这个优先级链定义了"一个自由度的值从哪里来" :未知量从线性系统的解向量取;固定量从 预设值取;约束量由主自由度线性组合算出。getDofValue(vector<Dof>&, vector<dataVec>&) 是批量 版本(第 235 行)。
getDofNumber(Dof ky)(第 706 行)返回全局编号:
cpp
Dof key = ky;
{ auto it = associatedWith.find(ky); if (it != associatedWith.end()) key = it->second; }
auto it = unknown.find(key);
if(it == unknown.end()) return -1;
else return it->second;
返回 -1 表示"不是未知量"(固定/约束/别名)。这是"查编号"的标准入口。
10.3.11 稀疏结构插入与惰性分配
组装前稀疏结构需要预估。sparsityDof(keys)(第 384 行)对一组自由度做笛卡尔积 (insertInSparsityPattern(keys[itR], keys[itC]))。insertInSparsityPattern(R, C) 里最关键 的一句是 if(!_current->isAllocated()) _current->allocate(sizeOfR());------第一次插入稀疏结构 时线性系统才被分配 ("惰性分配")。sizeOfR() 返回未知量个数(_isParallel ? _localSize : unknown.size()),sizeOfF() 返回固定自由度个数(在 elasticitySolver::assemble 末尾被打印为 nFixed)。
10.3.12 与网格实体的对应关系
Dof._entity 是网格实体的编号 ,指向哪类实体取决于 FunctionSpace: ScalarLagrangeFunctionSpace 与 VectorLagrangeFunctionSpace 都指向 MVertex::getNum() (标量/向量场的节点自由度);高阶/分层空间可能指向面/体内部自由度(需覆盖 getKeysOnVertex); 某些混合元用伪实体编号编码边/面/体自由度。当前实现 里 ScalarLagrangeFunctionSpace::getKeys 只对 getShapeFunctionNode(i) 生成 Dof(节点号, 场号) ------形函数节点 (可能是高阶的边/面内部节点)而非几何顶点,正是第 8 章"高阶单元有新节点"的 直接体现。注释 // "i": node, edge, group, etc. 表明 _entity 被设计成可编码任意实体, 只是当前最常见的是节点。
10.4 SElement:单元级装配的载体
src/solver/SElement.h 定义了一个**"求解器视图下的单元"**:
cpp
// A solver element.
// FIXME: this will change in the future (the base SElement should not
// contain anything except the MElement). More advanced solvers will
// derive and/or add containers for additional storage
class SElement {
private:
MElement *_e; // the underlying mesh element
static simpleFunction<double> *_enrichement_s, *_enrichement_t;
public:
SElement(MElement *e) : _e(e) {}
MElement *getMeshElement() const { return _e; }
static void setShapeEnrichement(simpleFunction<double> *f) { _enrichement_s = f; }
static void setTestEnrichement(simpleFunction<double> *f) { _enrichement_t = f; }
static const simpleFunction<double> *getShapeEnrichement();
static const simpleFunction<double> *getTestEnrichement();
int getNumNodalShapeFunctions() const;
inline MVertex *getVertex(int i) const
{ return _e->getParent() ? _e->getParent()->getVertex(i) : _e->getVertex(i); }
// 节点级形函数视图(给弱形式项用的 C 数组接口)
void nodalShapeFunctions(double u, double v, double w, double s[]);
void gradNodalShapeFunctions(double u, double v, double w, double invjac[3][3],
double grad[][3]);
void nodalTestFunctions(double u, double v, double w, double s[]);
void gradNodalTestFunctions(double u, double v, double w, double invjac[3][3],
double grad[][3]);
};
它能读出四条重要信息:
SElement=MElement+ 求解器附加数据 。注释FIXME: the base SElement should not contain anything except the MElement自嘲了当前设计的简约------未来更高级的求解器会派生SElement并加自己的容器。getVertex(i)会解引用父单元 (_e->getParent())------与ScalarLagrangeFunctionSpace的父单元处理一致。- 富集函数是"静态"的 :
_enrichement_s/_enrichement_t是static成员,即全局共享 。 这反映了早期设计的妥协:XFEM 类功能一次只能有一个活跃的富集函数。setShapeEnrichement与setTestEnrichement分别设置"形函数富集"与"测试函数富集"。 - 提供"节点级"形函数接口 :
nodalShapeFunctions/gradNodalShapeFunctions/nodalTestFunctions/gradNodalTestFunctions------这些是给弱形式项用的"节点形函数"视图 , 与FunctionSpace的接口互补(前者直接返回 C 数组,后者返回std::vector)。
SElement 的定位 :它是"弱形式项的求值上下文"。femTerm::elementMatrix(SElement*, fullMatrix&) 就是在 SElement 上求值的(10.5 节)。
10.5 femTerm:节点级弱形式项的基类
10.5.1 接口定义
src/solver/femTerm.h 的注释点明了定位:
a nodal finite element term : variables are always defined at nodes of the mesh
cpp
template <class T> class femTerm {
private:
typedef typename dofTraits<T>::VecType dataVec;
typedef typename dofTraits<T>::MatType dataMat;
protected:
GModel *_gm;
public:
femTerm(GModel *gm) : _gm(gm) {}
virtual ~femTerm() {}
virtual int sizeOfC(SElement *se) const = 0; // 单元矩阵的列数
virtual int sizeOfR(SElement *se) const = 0; // 单元矩阵的行数
virtual Dof getLocalDofR(SElement *se, int iRow) const = 0; // 行 → 局部自由度
virtual Dof getLocalDofC(SElement *se, int iCol) const // 默认:对称
{ return getLocalDofR(se, iCol); }
virtual void elementMatrix(SElement *se, fullMatrix<dataMat> &m) const = 0;
virtual void elementVector(SElement *se, fullVector<dataVec> &m) const
{ m.scale(0.0); }
};
五个纯虚/虚函数构成了弱形式项的全部契约:
sizeOfR/sizeOfC:单元矩阵的维度。对节点级 Lagrange 元,就是形函数个数;对弹性(向量场), 是 3×形函数个数(每节点 3 个分量)。getLocalDofR/getLocalDofC:把局部矩阵的行列索引映射到全局Dof。这是"局部→全局" 的关键。elementMatrix:在单元上组装局部刚度矩阵。这是每个弱形式项唯一必须实现的东西。elementVector:组装局部载荷向量。默认实现是全零(无载荷)。
10.5.2 addToMatrix:从单元到全局
femTerm 提供了三层 addToMatrix:
cpp
// ① 一组单元 × 一组单元的交集:对 L 逐个单元,仅当在 C 中也存在时才装配
void addToMatrix(dofManager<dataVec> &dm, groupOfElements &L, groupOfElements &C) const
{
for(auto it = L.begin(); it != L.end(); ++it) {
MElement *eL = *it;
if(&C == &L || C.find(eL)) { SElement se(eL); addToMatrix(dm, &se); }
}
}
// ② 单个单元:先求局部矩阵,再组装
void addToMatrix(dofManager<dataVec> &dm, SElement *se) const
{
fullMatrix<dataMat> localMatrix(sizeOfR(se), sizeOfC(se));
elementMatrix(se, localMatrix);
addToMatrix(dm, localMatrix, se);
}
// ③ 局部矩阵 → dofManager:构造 R/C 后按对称性选择路径
// if(!sym) dm.assemble(R, C, localMatrix); else dm.assemble(R, localMatrix);
③ 的"对称性判定"很巧妙 :逐行比较行自由度与列自由度是否相同 (getLocalDofR(se,j) == getLocalDofC(se,j)),全相同则走对称组装(只填一半),否则走通用路径。这省掉了显式的 isSymmetric() 接口,用运行时检查代替。
10.5.3 节点级边界条件
Dirichlet(dirichletNodalBC,第 105 行) :取物理组的所有网格顶点 (_gm->getMeshVerticesForPhysicalGroup(dim, physical, v)),对指定分量/场用函数 e 在其坐标处 求值并固定(dm.fixVertex(v[i], comp, field, e(v[i]->x(), v[i]->y(), v[i]->z())))。
**Neumann(neumannNodalBC,第 116 行)**在单元上做表面积分:
cpp
int integrationOrder = 2 * e->getPolynomialOrder();
e->getIntegrationPoints(integrationOrder, &npts, &GP);
for(int ip = 0; ip < npts; ip++) {
const double detJ = e->getJacobian(GP[ip].pt[0], GP[ip].pt[1], GP[ip].pt[2], jac);
SPoint3 p; e->pnt(GP[ip].pt[0], GP[ip].pt[1], GP[ip].pt[2], p);
e->getShapeFunctions(GP[ip].pt[0], GP[ip].pt[1], GP[ip].pt[2], sf);
const dataVec FCT = fct(p.x(), p.y(), p.z());
for(int k = 0; k < e->getNumShapeFunctions(); k++)
dm.assemble(e->getShapeFunctionNode(k), comp, field, detJ * GP[ip].weight * sf[k] * FCT);
}
这就是"面力/边力/体力"的数值积分 : ∫NkfdS 用 Gauss 积分 ∑ipdetJ⋅wip⋅Nk(ξip)⋅f(xip) 近似,积分阶取 2*阶数。 dirichletNodalBC 是节点级(直接固定),neumannNodalBC(physical,...) 重载遍历物理组的单元。 neumannNormalNodalBC(第 161 行)在三个方向逐一调用 neumannNodalBC------即"法向力"被当作 "每个方向上的同一标量力"处理,与"真正求法向再投影"不同,是一个尚未严格实现的接口。
10.5.4 helmholtzTerm:∇·k∇U − aU 的骨架
src/solver/helmholtzTerm.h 注释写的就是弱形式:
cpp
// \nabla \cdot k \nabla U - a U
template <class scalar> class helmholtzTerm : public femTerm<scalar> {
protected:
const simpleFunction<scalar> *_k, *_a;
const int _iFieldR;
int _iFieldC;
public:
helmholtzTerm(GModel *gm, int iFieldR, int iFieldC,
simpleFunction<scalar> *k, simpleFunction<scalar> *a) ...
virtual int sizeOfR(SElement *se) const
{ return se->getMeshElement()->getNumShapeFunctions(); } // 一节点一自由度
virtual int sizeOfC(SElement *se) const
{ return se->getMeshElement()->getNumShapeFunctions(); }
Dof getLocalDofR(SElement *se, int iRow) const
{ return Dof(se->getMeshElement()->getShapeFunctionNode(iRow)->getNum(),
Dof::createTypeWithTwoInts(0, _iFieldR)); }
Dof getLocalDofC(SElement *se, int iRow) const
{ return Dof(se->getMeshElement()->getShapeFunctionNode(iRow)->getNum(),
Dof::createTypeWithTwoInts(0, _iFieldC)); }
virtual void elementMatrix(SElement *se, fullMatrix<scalar> &m) const;
};
它的 elementMatrix(第 50 行)是最经典的有限元骨架,逐句拆解:
cpp
MElement *e = se->getMeshElement();
if(_k) _k->setElement(e);
if(_a) _a->setElement(e);
const int integrationOrder = 2 * e->getPolynomialOrder() + 1; // 积分阶
int npts; IntPt *GP;
e->getIntegrationPoints(integrationOrder, &npts, &GP);
const int nbSF = e->getNumShapeFunctions();
assert(nbSF < 100);
double jac[3][3], invjac[3][3];
double Grads[100][3], grads[100][3];
double sf[100];
m.setAll(0.);
for(int i = 0; i < npts; i++) {
const double u = GP[i].pt[0], v = GP[i].pt[1], w = GP[i].pt[2];
const double weightDetJ = GP[i].weight * e->getJacobian(u, v, w, jac);
SPoint3 p; e->pnt(u, v, w, p);
const scalar K = _k ? (*_k)(p.x(), p.y(), p.z()) : 0.0; // 材料系数
const scalar A = _a ? (*_a)(p.x(), p.y(), p.z()) : 0.0; // 质量系数
inv3x3(jac, invjac);
e->getGradShapeFunctions(u, v, w, grads); // 参考梯度
if(_a) e->getShapeFunctions(u, v, w, sf); // 形函数值
for(int j = 0; j < nbSF; j++) {
Grads[j][0] = invjac[0][0]*grads[j][0] + invjac[0][1]*grads[j][1] + invjac[0][2]*grads[j][2];
// Grads[j][1]、Grads[j][2] 同理(invjac 第二、三行 × grads),略
if(!_a) sf[j] = 0;
}
for(int j = 0; j < nbSF; j++) {
for(int k = 0; k <= j; k++) { // 只填下三角
m(j, k) += (K * (Grads[j][0]*Grads[k][0] + Grads[j][1]*Grads[k][1] + Grads[j][2]*Grads[k][2])
+ A * sf[j] * sf[k]) * weightDetJ;
}
}
}
for(int j = 0; j < nbSF; j++)
for(int k = 0; k < j; k++) m(k, j) = m(j, k); // 对称补全上三角
这段代码把有限元装配的每个环节都写在了明面上 :① 积分阶 2*polynomialOrder + 1("梯度× 梯度"约 2(p−1) 阶、"形函数×形函数" 2p 阶,取 2p+1 是保守的过积分);② 积分点与权重 由 第 8 章的 e->getIntegrationPoints 给出;③ 雅可比 e->getJacobian(u,v,w,jac) 返回 detJ, weightDetJ = weight * detJ 是"参数体积元 → 物理体积元"换算;④ 材料系数 _k/_a 通过 setElement(e) 绑定单元后在积分点的物理坐标 p 处求值(为"域相关材料"准备的两段式); ⑤ 双线性型 mjk+=(K∇ϕj⋅∇ϕk+Aϕjϕk)wdetJ, 且只算下三角 k<=j 再用 m(k,j)=m(j,k) 镜像补全 (省一半浮点运算);assert(nbSF < 100) 与定长数组 Grads[100][3] 说明作者刻意避免堆分配(性能优先的微观优化)。
10.5.5 laplaceTerm / massTerm:从 helmholtzTerm 派生
src/solver/laplaceTerm.h 里两个项都是 helmholtzTerm<double> 的派生:
cpp
class laplaceTerm : public helmholtzTerm<double> { // \nabla\cdot k\nabla U
const int _iField;
std::map<MVertex *, SPoint3> *_coordView;
public:
laplaceTerm(GModel *gm, int iField, simpleFunction<double> *k,
std::map<MVertex *, SPoint3> *coord = nullptr)
: helmholtzTerm<double>(gm, iField, iField, k, nullptr), ... {}
void elementVector(SElement *se, fullVector<double> &m) const { ... } // 重参数化用
};
class massTerm : public helmholtzTerm<double> { // a \nabla U
massTerm(GModel *gm, int iField, simpleFunction<double> *a)
: helmholtzTerm<double>(gm, iField, iField, nullptr, a) {}
};
laplaceTerm 传入 nullptr 作为质量系数、k 作为扩散系数 → 只保留刚度项;massTerm 反过来 (k = nullptr)→ 只保留质量项。这就是"一个骨架 + 参数化"复用。
laplaceTerm 的 elementVector(第 24 行)是重参数化的核心。它演示了"由单元矩阵生成右端 项":
cpp
void elementVector(SElement *se, fullVector<double> &m) const
{
MElement *e = se->getMeshElement();
int nbSF = e->getNumShapeFunctions();
fullMatrix<double> mat(nbSF, nbSF);
elementMatrix(se, mat); // 单元刚度矩阵
fullVector<double> val(nbSF); val.scale(0.);
for(int i = 0; i < nbSF; i++) { // 边界节点的已知 (u,v)
SPoint3 UV = _coordView->find(e->getShapeFunctionNode(i))->second;
val(i) = (_iField == 1) ? UV.x() : UV.y();
}
m.scale(0.);
for(int i = 0; i < nbSF; i++)
for(int j = 0; j < nbSF; j++) m(i) += -mat(i, j) * val(j); // f = -K u_边界
}
语义:用单元刚度矩阵作用于一组给定值,得到右端项 (注意负号: f=−Ku边界,与 10.3.9 的消元一致)。_coordView 是一个 map<MVertex*, SPoint3>,把边界节点映射到它在参数 平面上的 (u,v)。
它服务于调和映射 :把曲面参数 (u,v) 作为"温度"施加在边界上(_iField == 1 取 x 分量、 _iField == 2 取 y 分量),解拉普拉斯方程,得到的解就是内部节点的参数坐标。这正是 10.1.1 节"重参数化复用求解器"的细节------**重参数化 = 两个标量拉普拉斯问题(u 方向、v 方向)
- 边界条件**。
10.5.6 elasticityTerm:线弹性弱形式
src/solver/elasticityTerm.h 是向量场弱形式的代表。它的 辅助结构:
cpp
struct elasticityDataAtGaussPoint {
std::vector<fullMatrix<double> > gradSF; // 每个积分点的形函数梯度矩阵
std::vector<double> u, v, w, weight; // 每个积分点的坐标与权重
};
class elasticityTerm : public femTerm<double> {
protected:
double _e, _nu; // 杨氏模量、泊松比
int _iFieldR, _iFieldC;
SVector3 _volumeForce;
mutable std::map<int, elasticityDataAtGaussPoint> _data; // 按单元 MSH 类型缓存
void createData(MElement *) const;
public:
virtual int sizeOfR(SElement *se) const
{ return 3 * se->getMeshElement()->getNumShapeFunctions(); } // 3 dofs/节点
virtual int sizeOfC(SElement *se) const
{ return 3 * se->getMeshElement()->getNumShapeFunctions(); }
// order dofs in the local matrix :
// dx1, dx2 ... dxn, dy1, ..., dyn, dz1, ... dzn
Dof getLocalDofR(SElement *se, int iRow) const
{
MElement *e = se->getMeshElement();
int iCompR = iRow / (int)e->getNumShapeFunctions();
int ithLocalVertex = iRow % (int)e->getNumShapeFunctions();
return Dof(e->getShapeFunctionNode(ithLocalVertex)->getNum(),
Dof::createTypeWithTwoInts(iCompR, _iFieldR));
}
...
};
局部分量的排布是"先行后列"的分块 :前 n 个是 dx,接着 n 个 dy,最后 n 个 dz。这解释了 iCompR = iRow / n、ithLocalVertex = iRow % n 的除法取模。
elementMatrix 的实现(src/solver/elasticityTerm.cpp) 用一个 Voigt 记号下的 B 矩阵:
cpp
void elasticityTerm::createData(MElement *e) const
{
if(_data.find(e->getTypeForMSH()) != _data.end()) return; // 按 MSH 类型缓存
elasticityDataAtGaussPoint d;
int nbSF = (int)e->getNumShapeFunctions();
int integrationOrder = 2 * (e->getPolynomialOrder() - 1);
int npts; IntPt *GP; double gs[100][3];
e->getIntegrationPoints(integrationOrder, &npts, &GP);
for(int i = 0; i < npts; i++) {
fullMatrix<double> a(nbSF, 3); // 该积分点的参考域梯度
e->getGradShapeFunctions(GP[i].pt[0], GP[i].pt[1], GP[i].pt[2], gs);
for(int j = 0; j < nbSF; j++) { a(j,0)=gs[j][0]; a(j,1)=gs[j][1]; a(j,2)=gs[j][2]; }
d.gradSF.push_back(a);
d.u.push_back(GP[i].pt[0]); d.v.push_back(GP[i].pt[1]); d.w.push_back(GP[i].pt[2]);
d.weight.push_back(GP[i].weight);
}
_data[e->getTypeForMSH()] = d;
}
注意这里的缓存键是 e->getTypeForMSH()(单元类型),而不是单元本身 ------因为同一类型的所有 单元共享积分点与形函数梯度(在参考域上)。这是一个按类型缓存的优化。
elementMatrix 的材料矩阵 H(弹性张量):
cpp
double FACT = _e / (1 + _nu);
double C11 = FACT * (1 - _nu) / (1 - 2 * _nu);
double C12 = FACT * _nu / (1 - 2 * _nu);
double C44 = (C11 - C12) / 2;
const double C[6][6] = {{C11,C12,C12,0,0,0}, {C12,C11,C12,0,0,0},
{C12,C12,C11,0,0,0}, {0,0,0,C44,0,0},
{0,0,0,0,C44,0}, {0,0,0,0,0,C44}};
这是三维各向同性线弹性的 Voigt 形式弹性矩阵 (6×6)。C11/C12/C44 的公式对应 λ+2μ、 λ、 μ 的组合。
然后是每个积分点上的 B 矩阵装配:
cpp
for(int j = 0; j < nbSF; j++) { // 每列对应一个自由度:dx_n/dy_n/dz_n
BT(j, 0) = B(0, j) = Grads[j][0]; // ε_xx ← dx
BT(j + nbSF, 1) = B(1, j + nbSF) = Grads[j][1]; // ε_yy ← dy
BT(j + 2*nbSF, 2) = B(2, j + 2*nbSF) = Grads[j][2]; // ε_zz ← dz
BT(j, 3) = BT(j + nbSF, 3) = Grads[j][1]; // γ_xy
BT(j + nbSF, 4) = BT(j + 2*nbSF, 4) = Grads[j][2]; // γ_xz
BT(j, 5) = BT(j + 2*nbSF, 5) = Grads[j][0]; // γ_yz
}
BTH.gemm(BT, H); // B^T H
m.gemm(BTH, B, weight * detJ, 1.); // K += B^T H B * w * detJ
这是弹性有限元教材里 K=∫BTHBdV 的翻译 。 B 是应变-位移矩阵(6×3n),H 是弹性矩阵(6×6),BT = Bᵀ(3n×6);行对应六个应变分量 ( εxx...γyz),列对应自由度。注意 B 与 BT 分开存 (本可直接用 B.transpose()),是为复用 fullMatrix::gemm 而不必每次转置。BTH.gemm(BT, H) 与 m.gemm(BTH, B, weight*detJ, 1.) 用第 8 章的稠密矩阵 fullMatrix::gemm 完成。
elementVector(第 127 行)是体力载荷 ∫NkfdV:
cpp
m(j) += _volumeForce.x() * ff[j] * weight * detJ * .5;
m(j + nbSF) += _volumeForce.y() * ff[j] * weight * detJ * .5;
m(j + 2 * nbSF) += _volumeForce.z() * ff[j] * weight * detJ * .5;
注意那个 .5(0.5)因子 :它是"形函数求和 × 载荷"的对称分配约定(中心节点分一半), 在一阶单纯形 (顶点分权 1/3)与几何一致性 之间是一个简化处理------这是早期代码的痕迹, 严格应等于 Nk 的积分权重。
10.5.7 elasticityMixedTerm:三场混合弹性
elasticityMixedTerm(第 85 行)实现三场(位移 + 两个 Lagrange 乘子)混合弹性,块结构 (第 319 行注释):
text
3*_sizeN _sizeM _sizeM
KUU KUP KUG
m = KUP^t KPP KPG
KUG^T KPG^t KGG
KUU 是位移块( ∫BTHB)、KUP/KUG 是位移-乘子耦合块、KPP 是乘子块含稳定化项 (const double K = .0000002 * weight * detJ * pow(detJ, 2/_dimension),第二个 detJ 用于 稳定化)------混合元必须稳定化以避免棋盘格压力模式 。setPolynomialBasis(第 90 行)用 MElement::getFunctionSpace(order) 取回基对象并强制转换成 polynomialBasis * ,暴露了 第 8 章 polynomialBasis 与本章 FunctionSpace 两个同名概念冲突的隐患 。代码里还留着 // FIXME : PLANE STRESS !!!------平面应力被注释掉了,说明混合元当前只支持三维/平面应变。
10.6 terms.h:运算符风格的弱形式表达式
femTerm/helmholtzTerm/elasticityTerm 是**"单元级"的弱形式**(直接给单元矩阵)。terms.h 提供了一层更抽象的 "项表达式(term algebra)",四类项:
cpp
template <class T2 = double> class ScalarTermBase { // 返回标量
virtual void get(MElement *ele, int npts, IntPt *GP, T2 &val) const = 0;
virtual ScalarTermBase<T2> *clone() const = 0;
};
class BilinearTermBase { // 返回矩阵(左端项)
virtual void get(MElement *ele, int npts, IntPt *GP, fullMatrix<double> &m) const;
virtual BilinearTermBase *clone() const = 0;
};
template <class T2 = double> class LinearTermBase { // 返回向量(右端项)
virtual void get(MElement *ele, int npts, IntPt *GP, fullVector<T2> &v) const;
PlusTerm<T2> operator+(const LinearTermBase<T2> &other); // 线性项可以相加
};
| 类别 | 语义 | 例子 |
|---|---|---|
ScalarTermBase<T2> |
标量(每单元一个数) | ScalarTermConstant(常数 1) |
LinearTermBase<T2> |
向量(右端项) | LoadTerm、GradTerm |
BilinearTermBase |
矩阵(左端项) | LaplaceTerm、IsotropicElasticTerm |
PlusTerm |
两个线性项之和 | A + B |
这套设计的妙处 是可以用运算符 组合弱形式。terms.hpp 里 BilinearTermContract<T2> operator|(...) 用 | 表示"双线性收缩"(如 ∫∇u⋅∇v 写成 Grad(u) | Grad(v));BilinearTermContractWithLaw 则在 中间插入一个材料律 c,其类型是 ScalarTermBase<typename TensorialTraits<T2>::TensProdType>------材料律的类型是"值类型的张量 积" :T2 = SVector3 时 TensProdType = STensor3(二阶张量),上位到 T2 = STensor3 时 TensProdType = STensor43(四阶)。这正是 10.2.1 节 TensProdType 的用途。
具体项包括 IsotropicElasticTerm(BilinearTerm<SVector3,SVector3>,算 BTHB)、 LoadTerm<T1>(线性载荷)、LagrangeMultiplierTerm<T1,double>(位移-乘子耦合,混合元/接触 核心项,对应 ∫(u⋅d)λ)、LagMultTerm、LoadTermOnBorder<T1>。
IsotropicElasticTerm 就是 elasticityTerm 的"表达式版" :同样算 BTHB,但通过 FunctionSpace<SVector3> 接口(而非 SElement 的裸数组)。这解释了为什么 elasticitySolver::solve 里同时存在两套代码------同一弱形式的两种实现风格:前者"节点级裸 装配",后者"表达式风格"。
10.7 solverAlgorithms 与 quadratureRules
10.7.1 Assemble:把项、空间、积分规则组装起来
src/solver/solverAlgorithms.h 是**"装配的通用算法"**:
cpp
template <class Iterator, class Assembler>
void Assemble(BilinearTermBase &term, FunctionSpaceBase &space,
Iterator itbegin, Iterator itend, QuadratureBase &integrator,
Assembler &assembler) // symmetric
{
fullMatrix<typename Assembler::dataMat> localMatrix;
std::vector<Dof> R;
for(Iterator it = itbegin; it != itend; ++it) {
MElement *e = *it; R.clear();
IntPt *GP; int npts = integrator.getIntPoints(e, &GP);
term.get(e, npts, GP, localMatrix); // 求局部矩阵
space.getKeys(e, R); // 求局部自由度
assembler.assemble(R, localMatrix); // 全局组装
}
}
这三行就是"弱形式求解"的骨架 ,所有变体都是它的组合:非对称 版本(第 71 行)分别用 shapeFcts.getKeys(e,R) 与 testFcts.getKeys(e,C),并组装 assemble(R,C,localMatrix) 与 assemble(C,R,localMatrix.transpose())(两遍组装、转置一次 );带过滤器 版本(第 37/136 行)加 if(efilter(e));单单元 版本只处理一个 MElement;线性项 版本(第 118 行)用 fullVector 与 assemble(R, localVector);标量项 版本(第 169 行)累加到 val (算 能量/体积)------即 elasticitySolver::solve 里 Assemble(Elastic_Energy_Term, ..., energ) 调用的重载。
10.7.2 自由度固定与编号的通用算法
同一文件还提供了一批"装配无关"的通用算法:
cpp
template <class Assembler>
void NumberDofs(FunctionSpaceBase &space, Iterator itbegin, Iterator itend,
Assembler &assembler)
{
for(Iterator it = itbegin; it != itend; ++it) {
MElement *e = *it; std::vector<Dof> R;
space.getKeys(e, R);
for(int i = 0; i < R.size(); ++i) assembler.numberDof(R[i]);
}
}
template <class Assembler>
void FixNodalDofs(FunctionSpaceBase &space, Iterator itbegin, Iterator itend,
Assembler &assembler,
simpleFunction<typename Assembler::dataVec> &fct, FilterDof &filter)
{
for(Iterator it = itbegin; it != itend; ++it) {
MElement *e = *it; std::vector<MVertex *> tabV; std::vector<Dof> R;
space.getKeys(e, R);
for(int i = 0; i < e->getNumVertices(); ++i) tabV.push_back(e->getVertex(i));
for(auto itd = R.begin(); itd != R.end(); ++itd) {
Dof key = *itd;
if(filter(key))
for(int i = 0; i < (int)tabV.size(); ++i)
if((long int)tabV[i]->getNum() == key.getEntity()) {
assembler.fixDof(key, fct(tabV[i]->x(), tabV[i]->y(), tabV[i]->z())); break;
}
}
}
}
NumberDofs 与 FixNodalDofs 就是 elasticitySolver::assemble 里"编号"与"固定"两步的 实现 :前者遍历单元的局部自由度逐个 numberDof;后者对每个(过滤后的)自由度,找到它的 实体(节点),在该节点的物理坐标处求边界值函数 (filter 决定"只固定哪些分量",见下)。
10.7.3 FilterDof:按分量过滤
cpp
class FilterDofComponent : public FilterDof {
int comp;
public:
FilterDofComponent(int comp_) : comp(comp_) {}
virtual bool operator()(Dof key)
{
int type = key.getType();
int icomp, iphys;
Dof::getTwoIntsFromType(type, icomp, iphys);
if(icomp == comp) return true;
return false;
}
};
这就是"只固定 x 分量"的实现 ------解出 type 里的分量号 icomp,与目标分量比较。 elasticitySolver::assemble 里 FilterDofComponent filter(allDirichlet[i]._comp) 正是为 "沿某方向施加位移约束"服务(如"底面 uz=0")。
另有 FilterDofTrivial(全真)与 FilterDofSet(按自由度的集合过滤)。
10.7.4 quadratureRules:积分规则的封装
src/solver/quadratureRules.h 把第 8 章的积分点生成封装成 "规则对象":
cpp
class QuadratureBase { public: virtual int getIntPoints(MElement *e, IntPt **GP) = 0; };
class GaussQuadrature : public QuadratureBase { // 按 IntegCases 决定积分阶
enum IntegCases { Other, Val, Grad, ValVal, GradGrad };
virtual int getIntPoints(MElement *e, IntPt **GP)
{ /* switch(info): Other=order, Val=p+1, Grad=p, ValVal=2p, GradGrad=3(p-1)+1 */
e->getIntegrationPoints(integrationOrder, &npts, GP); return npts; }
};
五个 IntegCases 是"被积函数类型 → 积分阶"的查表:
| 情形 | 被积函数 | 阶 | 公式 |
|---|---|---|---|
Val |
形函数值(如 ∫f·N) |
p+1 |
值×形状 |
Grad |
单个梯度 | p |
一阶导 |
ValVal |
形函数×形函数(质量项) | 2p |
乘积 |
GradGrad |
梯度×梯度(刚度项) | 3(p-1)+1 |
两个一阶导的乘积 + 雅可比 |
Other |
显式指定 | order |
用户给 |
elasticitySolver 里 GaussQuadrature Integ_Bulk(GaussQuadrature::GradGrad) 就是取刚度项 积分规则 (Integ_Laplace、Integ_LagrangeMult、Integ_Boundary 同理)。QuadratureVoid (第 25 行)返回 0 个积分点,用于"刚体接触不需要高斯积分但接口需要 npts/IntPt"的场合。
10.7.5 SolverField:把"解"当作函数空间用
src/solver/solverField.h 的注释点明了它的角色:
cpp
template <class T>
class SolverField : public FunctionSpace<T>
// being able to use it instead of a real function space is interesting
// (nbkeys=1, explicit keys/dofs undefined ...)
它把一个 dofManager<double> 与一个 FunctionSpace<T> 组合,让"已经解出的场"表现得像一个 有限元空间,从而能被后续的弱形式/后处理直接使用:
cpp
virtual void f(MElement *ele, double u, double v, double w, ValType &val) const
{
std::vector<Dof> D;
std::vector<ValType> SFVals;
std::vector<double> DMVals;
fs->getKeys(ele, D); // 该单元的形函数自由度
dm->getDofValue(D, DMVals); // 这些自由度的解值
fs->f(ele, u, v, w, SFVals); // 形函数值
val = ValType();
for(std::size_t i = 0; i < D.size(); ++i) val += SFVals[i] * DMVals[i]; // 插值
}
这三行就是 ∑iNiui(有限元插值)的完整实现 :取自由度 → 取解值 → 取形函数 → 加权求和。gradf 同理,求 ∑i∇Niui(用于应变/应力)。
getNumKeys 恒返回 1(注释说"nbkeys=1"),getKeys 故意报错(不该被调用)------因为 SolverField 是**"求值用"的空间,不是"组装用"的空间**。
SolverField 是"求解结果 → 后处理/误差估计"的桥梁 :elasticitySolver::buildStressesView 等正是用 SolverField<SVector3> Field(pAssembler, LagSpace) 逐单元插值位移、梯度,再算应变/ 应力。
10.8 具体求解器
10.8.1 elasticitySolver:数据结构
src/solver/elasticitySolver.h 是线弹性求解器。四个结构体 描述问题:
cpp
struct elasticField { // 一套材料的作用域
int _tag; // dofManager 的场号
groupOfElements *g; // 该场的单元支持域
double _e, _nu; // 杨氏模量、泊松比 (should be somewhere else)
};
struct LagrangeMultiplierField { int _tag; groupOfElements *g; double _tau; SVector3 _d;
simpleFunction<double> *_f; };
struct BoundaryCondition {
int _tag;
enum location { UNDEF, ON_VERTEX, ON_EDGE, ON_FACE, ON_VOLUME };
location onWhat; // 作用在顶点还是单元上
groupOfElements *g;
};
struct dirichletBC : public BoundaryCondition { int _comp; simpleFunction<double> *_f; };
struct neumannBC : public BoundaryCondition { simpleFunction<SVector3> *_f; };
边界条件类型用 onWhat 枚举限定作用位置 (ON_VERTEX/ON_EDGE/ON_FACE/ON_VOLUME)------正好 复用第 1、2 章的几何维度概念。elasticField::_e/_nu 的注释 should be somewhere else 自嘲了 材料数据组织的简朴(材料本应是独立的"材料库",这里直接塞进场结构)。
主类:
cpp
class elasticitySolver {
public:
GModel *pModel;
int _dim, _tag;
dofManager<double> *pAssembler;
FunctionSpace<SVector3> *LagSpace; // 位移场(向量)
std::vector<FunctionSpace<double> *> LagrangeMultiplierSpaces; // 乘子场(标量)
std::vector<elasticField> elasticFields;
std::vector<LagrangeMultiplierField> LagrangeMultiplierFields;
std::vector<neumannBC> allNeumann;
std::vector<dirichletBC> allDirichlet;
...
};
注意 LagSpace 是 FunctionSpace<SVector3>*(位移),乘子空间是 FunctionSpace<double>* (标量)------这正是 10.2 节 traits 的直接应用。
构造函数按维数选空间(第 53 行):
cpp
elasticitySolver::elasticitySolver(GModel *model, int tag)
{
pModel = model;
_dim = pModel->getNumRegions() ? 3 : 2; // 有体 → 3D,否则 2D
_tag = tag;
pAssembler = nullptr;
if(_dim == 3) LagSpace = new VectorLagrangeFunctionSpace(_tag);
if(_dim == 2)
LagSpace = new VectorLagrangeFunctionSpace(
_tag, VectorLagrangeFunctionSpace::VECTOR_X,
VectorLagrangeFunctionSpace::VECTOR_Y); // 2D 只用 x/y 分量
}
维数从模型"有没有体"推断 ------3D 用三分量向量空间,2D 用两分量。这是 10.2.5 节 VectorLagrangeFunctionSpace(id, comp1, comp2) 构造的直接用途。
10.8.2 assemble:完整装配流程
elasticitySolver::assemble(src/solver/elasticitySolver.cpp 第 454 行)是本章最值得完整追踪的函数。它的顺序严格按"先固定、再编号、再组装":
cpp
void elasticitySolver::assemble(linearSystem<double> *lsys)
{
if(pAssembler) delete pAssembler;
pAssembler = new dofManager<double>(lsys);
// we first do all fixations. the behavior of the dofManager is to
// give priority to fixations : when a dof is fixed, it cannot be
// numbered afterwards
// ① Dirichlet conditions
for(std::size_t i = 0; i < allDirichlet.size(); i++) {
FilterDofComponent filter(allDirichlet[i]._comp);
FixNodalDofs(*LagSpace, allDirichlet[i].g->begin(), allDirichlet[i].g->end(),
*pAssembler, *allDirichlet[i]._f, filter);
}
// ② 乘子场编号
for(...LagrangeMultiplierFields...) { NumberDofs(LagrangeMultiplierSpaces[j], ...); }
// ③ 有材料的位移场编号(_e != 0)
for(...elasticFields...) if(elasticFields[i]._e != 0.) NumberDofs(*LagSpace, ...);
// ④ 空场(Void, _e == 0):位移全固定为 0 ------ "孔洞"的建模方式
for(...elasticFields...) if(elasticFields[i]._e == 0.) FixVoidNodalDofs(*LagSpace, ...);
// ⑤ Neumann:LoadTerm<SVector3> + Val 积分规则,逐物理组施加
GaussQuadrature Integ_Boundary(GaussQuadrature::Val);
for(...allNeumann...) { LoadTerm<SVector3> Lterm(*LagSpace, allNeumann[i]._f);
Assemble(Lterm, *LagSpace, ...); }
// ⑥ 乘子块:位移-乘子耦合(非对称,用双空间 Assemble)/ 乘子拉普拉斯 / 乘子载荷
GaussQuadrature Integ_LagrangeMult(GaussQuadrature::ValVal);
GaussQuadrature Integ_Laplace(GaussQuadrature::GradGrad);
for(...) {
LagrangeMultiplierTerm<SVector3> LagTerm(*LagSpace, *(LagrangeMultiplierSpaces[j]),
LagrangeMultiplierFields[i]._d);
Assemble(LagTerm, *LagSpace, *(LagrangeMultiplierSpaces[j]), ...);
LaplaceTerm<double, double> LapTerm(*(LagrangeMultiplierSpaces[j]),
-LagrangeMultiplierFields[i]._tau);
Assemble(LapTerm, ...);
LoadTermOnBorder<double> Lterm(*(LagrangeMultiplierSpaces[j]), ...);
Assemble(Lterm, ...);
}
// ⑦ 弹性本体积分:IsotropicElasticTerm + GradGrad 规则
GaussQuadrature Integ_Bulk(GaussQuadrature::GradGrad);
for(...elasticFields...) { IsotropicElasticTerm Eterm(*LagSpace, elasticFields[i]._e,
elasticFields[i]._nu);
Assemble(Eterm, *LagSpace, ...); }
printf("nDofs=%d\n", pAssembler->sizeOfR());
printf("nFixed=%d\n", pAssembler->sizeOfF());
}
逐条读出的信息 :① 先做 Dirichlet (因为 10.3.8 节的"先到先得"规则,必须先固定才能 编号);②③ 编号两个场 (LagrangeMultiplierSpaces[j]->getId() 与 LagrangeMultiplierFields[i]._tag 必须一致);③ 只编号"有材料"的场 ,④ 空场位移全固定 为 0 (孔洞/空腔的建模方式);⑤ Neumann 用 LoadTerm<SVector3> + Val 规则 ;⑥ 乘子 块用三种项 :LagrangeMultiplierTerm(位移-乘子耦合,唯一非对称的组装 ,故用 10.7.1 节的 双空间 Assemble(term, shapeFcts, testFcts, ...))、LaplaceTerm(乘子自项,-tau 罚)、 LoadTermOnBorder;⑦ 弹性块用 IsotropicElasticTerm + GradGrad 规则 。⑥ 是"混合元/ 接触问题在 Gmsh 里如何实现"的完整答案。
10.8.3 solve / postSolve
solve(第 113 行)选择后端并求能:
cpp
void elasticitySolver::solve()
{
std::string sysname = "A";
if(pAssembler && pAssembler->getLinearSystem(sysname))
delete pAssembler->getLinearSystem(sysname);
// 后端可插拔:PETSc > GMM > Full
#if defined(HAVE_PETSC)
linearSystemPETSc<double> *lsys = new linearSystemPETSc<double>;
#elif defined(HAVE_GMM)
linearSystemCSRGmm<double> *lsys = new linearSystemCSRGmm<double>;
#else
linearSystemFull<double> *lsys = new linearSystemFull<double>;
#endif
assemble(lsys);
lsys->systemSolve();
double energ = 0; // 用解值算弹性总能量
GaussQuadrature Integ_Bulk(GaussQuadrature::GradGrad);
for(std::size_t i = 0; i < elasticFields.size(); i++) {
SolverField<SVector3> Field(pAssembler, LagSpace);
IsotropicElasticTerm Eterm(Field, elasticFields[i]._e, elasticFields[i]._nu);
BilinearTermToScalarTerm Elastic_Energy_Term(Eterm);
Assemble(Elastic_Energy_Term, elasticFields[i].g->begin(),
elasticFields[i].g->end(), Integ_Bulk, energ);
}
}
三次编译期分支(PETSc > GMM > Full)就是"后端可插拔"的体现 :同一份求解代码在三种构建配置 下都能工作,只是精度/规模不同。注意 SolverField<SVector3> Field(pAssembler, LagSpace) 把"解"包装成空间 ,再喂给 IsotropicElasticTerm 算能量------这是 10.7.5 节 SolverField 的 第一个用途(用解值算能量)。
postSolve(第 145 行)几乎相同,只是重复算一次能量(历史上用于"求解后校验",现在看是 冗余代码)。
10.8.4 后处理视图
#if defined(HAVE_POST) 下有七个 build*View:
buildDisplacementView:逐节点位移(NodeData,分量 3);buildStrainView:逐单元应变(ElementData,9 分量张量);buildStressesView:逐单元应力(ElementData,9 分量);buildLagrangeMultiplierView:乘子场;buildElasticEnergyView:单元弹性能密度;buildVonMisesView:冯·米塞斯等效应力(这里其实复用了能量项的计算,注释里叫elastic view);buildVolumeView:单元体积与乘子边界长度;buildErrorView:与解析解比较的误差。
buildStressesView 的核心是用 SolverField 插值位移、interpolateGrad 求梯度、再组装应力:
cpp
double valx[256], valy[256], valz[256];
for(int k = 0; k < nbVertex; k++) {
MPoint p(e->getVertex(k)); Field.f(&p, 0, 0, 0, val[k]); // 顶点处求位移
valx[k] = val[k](0); valy[k] = val[k](1); valz[k] = val[k](2);
}
SPoint3 center = e->barycenterUVW(); // 形心处求梯度
e->interpolateGrad(valx, u, v, w, gradux); // 对 valy/valz 同理
double eps[6] = {gradux[0], graduy[1], graduz[2], 0.5*(gradux[1]+graduy[0]), ...}; // 应变
double A = E/(1.+nu), B = A*(nu/(1.-2*nu));
double sxx = A*eps[0] + B*(eps[0]+eps[1]+eps[2]); ... // 应力 = 本构关系
这段代码把"位移 → 应变(几何方程)→ 应力(本构方程)"的完整后处理链写清楚了 。注意它用 MPoint(第 2 章的"点实体")把节点包装成 MElement*(Field.f 的签名要 MElement*)------ MPoint 是"把 MVertex 适配成 MElement"的桥。
10.8.5 thermicSolver / eigenSolver / frameSolver
thermicSolver(src/solver/thermicSolver.h):热传导 (标量温度场)。它是elasticitySolver的标量版------把SVector3换成double,弹性项换成helmholtzTerm/laplaceTerm。eigenSolver(src/solver/eigenSolver.h):特征值问题 (模态/屈曲), Kx=λMx。它需要两套矩阵 (A, B) ------这正是 10.3.6 节dofManager支持_linearSystems多系统的原因。依赖 SLEPc(ENABLE_SLEPC)或内部实现。frameSolver(src/solver/frameSolver.h):框架/梁结构 求解。
groupOfElements(src/solver/groupOfElements.h)是求解时 的"单元支持域",用 std::set<MElement*>/std::set<MVertex*> 存单元与顶点,并多存一份 _parents(父单元)以支持高阶子单元。构造 groupOfElements(dim, physical) 按"维度+物理组" 取单元集合------正是第 1、2 章物理组概念在求解层的落地。它的 elementFilter 抽象 (operator()(MElement*))允许按谓词筛选单元。find 的注释 // FIXME : NOT VERY ELEGANT !!! 与实现("若父单元在集合里也算命中")暴露了高阶子单元与低阶父单元共存时的语义模糊点 。 groupOfLagMultElements 是它的派生,专门处理"父单元在源集合里的 Lagrange 乘子单元",用于 接触/水平集。
10.8.7 一个完整的求解实例
把本章串起来:Mesh.Column 结构 →(第 4--7 章)GRegion 的四面体 → elasticitySolver::assemble(10.8.2 节):① Dirichlet 固定底面 → dofManager.fixed; ② NumberDofs(位移场)→ dofManager.unknown;③ IsotropicElasticTerm( BTHB); ④ Neumann 顶面拉力 → LoadTerm;⑤ linearSystem.addToMatrix/addToRightHandSide → systemSolve()(10.9 节)→ 解向量 x → getDofValue/SolverField → 逐节点位移 (buildDisplacementView)与逐单元应力(buildStressesView)。这条链就是 Gmsh 内置求解器 "从网格到结果"的全部路径。
10.9 linearSystem:后端无关的线性系统
10.9.1 两层抽象
src/solver/linearSystem.h 的注释点明了定位:A class that encapsulates a linear system solver interface: building a sparse matrix, solving a linear system 。两层抽象各司其职:
linearSystemBase(生命周期与求解) :_parameters(map<string,string>命名参数,如 迭代容差、预处理器类型)、isAllocated/allocate/clear/zeroMatrix/zeroRightHandSide/zeroSolution/systemSolve、matMult(x = A*b)、setParameter/getParameter、insertInSparsityPattern、normInfRightHandSide/normInfSolution。linearSystem<scalar>(矩阵元素读写) :addToMatrix/getFromMatrix/addToRightHandSide(第三参ith是多重右端项 索引,用于多载荷工况)/getFromRightHandSide/getFromSolution/addToSolution。注意它被参数化为scalar------ 同一份dofManager<T>代码可用double或std::complex<double>作标量(复数用于声学/电磁 的频域求解)。
10.9.2 后端清单
| 后端 | 文件 | 特点 | 编译宏 |
|---|---|---|---|
| CSR(自研) | linearSystemCSR.h/.cpp |
内置 CSR 稀疏矩阵 + 迭代/直接解 | 默认 |
| CSR+GMM | linearSystemGmm.h |
CSR + GMM/MUMPS 直接解 | ENABLE_GMM |
| PETSc | linearSystemPETSc.h/.cpp |
大规模并行,丰富预条件/解法器 | ENABLE_PETSC |
| MUMPS | linearSystemMUMPS.h/.cpp |
稀疏直接解(多重前沿法) | ENABLE_MUMPS |
| Eigen | linearSystemEigen.h/.cpp |
头文件型模板库后端 | ENABLE_EIGEN |
| Full | linearSystemFull.h |
稠密矩阵(小规模/调试) | 默认 |
这几行每一个都在 src/solver/ 下成对存在(.h + .cpp,Gmm/Full 是头文件型)。linearSystemFull 的存在很重要 :它让"没有安装任何外部库"的构建也能求解 (尽管只适合小问题),这保证了 Gmsh 的内置求解器始终可用------与第 2 章"GEO 内核保底"是同 一种哲学。
10.9.3 CSR 后端的实现细节
linearSystemCSR<scalar>(src/solver/linearSystemCSR.h) 是最能代表"自研稀疏矩阵"的实现:
cpp
typedef struct { int nmax, size, incr, n, isorder; char *array; } CSRList_T;
void CSRList_Add(CSRList_T *liste, const void *data);
int CSRList_Nbr(CSRList_T *liste);
template <class scalar> class linearSystemCSR : public linearSystem<scalar> {
protected:
bool sorted, _entriesPreAllocated;
CSRList_T *_a, *_ai, *_ptr, *_jptr; // 值 / 列索引 / 临时指针 / 行指针
std::vector<scalar> *_b, *_x; // 右端 / 解
sparsityPattern _sparsity; // 仅用于预估,分配后即弃
public:
int getNNZ() { return CSRList_Nbr(_a); } int getNbUnk() { return _b->size(); }
virtual void insertInSparsityPattern(int i, int j) { _sparsity.insertEntry(i, j); }
virtual void addToMatrix(int il, int ic, const scalar &val)
{
if(!_entriesPreAllocated) preAllocateEntries();
INDEX_TYPE *jptr = (INDEX_TYPE *)_jptr->array, *ai = (INDEX_TYPE *)_ai->array;
scalar *a = (scalar *)_a->array;
INDEX_TYPE position = jptr[il];
if(sorted) { // 二分查找 + 线性收尾(余下 ≤20 个)
int p0 = jptr[il], p1 = jptr[il + 1];
while(p1 - p0 > 20) { position = (p0 + p1) / 2;
if(ai[position] > ic) p1 = position; else if(ai[position] < ic) p0 = position + 1;
else { a[position] += val; return; } }
for(position = p0; position < p1; position++) { if(ai[position] >= ic) { ... } }
}
...
}
};
关键信息三条 :① CSR 结构是 C 风格的 ------CSRList_T 用 char *array 存原始字节,与第 2 章 Tree_T/List_T 一脉相承,是历史遗留实现;② _sparsity 只用于预估 (注释 does not store the sparsity once allocated),这是"两遍装配":第一遍求稀疏结构并分配,第二遍 写入值;③ 排序优化 :sorted 为真时 addToMatrix 用二分查找 (先二分,再线性扫描余下 ≤20 个)定位列索引------这是稀疏矩阵组装性能的关键优化。
10.9.4 稀疏结构预估与编译期宏
src/solver/sparsityPattern.h 用一个按行的 std::set (std::map<int, std::set<int>> row2col)记录"这一行有哪些列有非零元",insertEntry(i,j) 插入 一项。流程是:dofManager::sparsityDof(keys) 对每个单元的局部自由度做笛卡尔积插入 → 第一次 插入时 insertInSparsityPattern 调用 _current->allocate(sizeOfR()) → preAllocateEntries() 按每行大小一次性分配 CSR 数组 。这就是"预估非零元 → 一次性分配 → 避免动态扩容"的标准 流程:代价是额外遍历一遍单元,收益是组装阶段无内存重分配。
后端的选择在 CMake(CMakeLists.txt 第 1077--1260 行)里是一串条件 (ENABLE_GMM/ENABLE_PETSC/ENABLE_SLEPC/ENABLE_MUMPS/ENABLE_GETDP),对应源码里的 编译期宏:
| 宏 | 含义 | 典型用法 |
|---|---|---|
HAVE_SOLVER |
是否启用内置求解器 | GmshConfig.h 里定义 |
HAVE_PETSC |
PETSc 可用 | elasticitySolver::solve 选 PETSc 后端 |
HAVE_GMM |
GMM 可用 | 降级选择 |
HAVE_MUMPS |
MUMPS 可用 | 稀疏直接解 |
HAVE_SLEPC |
SLEPc 可用 | eigenSolver |
HAVE_GETDP |
GetDP 集成 | 外部求解器 |
这是"Gmsh 求解能力随构建配置浮动"的缩影 :同一份源码在不同依赖组合下编译出不同求解能力, 上层用 #if 优雅降级。
10.9.5 dofManager 与 linearSystem 的交互
dofManager 对 linearSystem 的调用是唯一的接触点 :addToMatrix/addToRightHandSide (写)与 getFromSolution(读),外加 zeroMatrix/systemSolve。dofManager 是"符号层" (哪个自由度对应哪一行),linearSystem 是"数值层"(这一行如何存储与求解),两者只通过"全局 编号 int"通信------这就是"后端可插拔"的接口边界。
10.10 重参数化:求解器的一个非典型用途
10.10.1 为什么网格工具需要求解 PDE
第 2 章提到 GModelParametrize(src/geo/GModelParametrize.h) 用于把曲面"展平"。必要性在于:很多曲面没有好的参数化 (如 OCC 里裁剪过的 BSpline 面、带洞 的面),而网格生成、纹理映射、几何分析都需要 (u,v) 坐标。Gmsh 的解法是:把"求参数坐标"当成 解一个拉普拉斯方程 ------给定边界上的 (u,v),让内部节点的 (u,v) 满足 Δu=0, Δv=0(即最小化 Dirichlet 能量 EΦ=21∫∥∇Φ∥2dA,其欧拉-拉格朗日 方程就是拉普拉斯方程)。离散后满足 Ku=f, K 是 10.5.4 节的刚度矩阵。 这就是 10.5.5 节 laplaceTerm 的用途。
10.10.2 实现要点
- 两个标量问题 :
u、v方向各解一次,共用同一刚度矩阵(都是拉普拉斯)。这解释了laplaceTerm里_iField == 1取UV.x()、_iField == 2取UV.y()的分支。 - 边界条件 :
_coordView(map<MVertex*, SPoint3>)给出边界节点的已知(u,v),通常来自 第 2 章GFace::parFromPoint。 - 右端项 :
elementVector用-K u_边界生成(10.5.5 节),等价于把边界值移到右边。
这整套流程没有任何"特殊代码"------它只是"一个新的应用层调用",复用了全部基础设施 (这正是 10.1.1 节说的务实哲学)。重参数化的结果直接服务于第 5 章的曲面三角化:在参数平面上做 Delaunay/前沿推进,再映回三维------参数化质量差(畸变大),网格质量就差。
10.11 对外 API 与代码生成
10.11.1 一份规格,五种语言
第 1 章已述:整个 API 由 api/gen.py 与 api/GenApi.py 生成。GenApi.py 定义了一份语言无关的 API 规格 (函数、参数、类型、文档),gen.py 用它 发射出各语言的目标文件:
text
api/gmsh.h(C++,namespace gmsh) api/gmshc.h(C 头) api/gmshc.cpp(C 实现)
api/gmsh.h_cwrap(C 包装中间头) api/gmsh.py(ctypes) api/gmsh.jl(ccall)
api/gmsh.f90(iso_c_binding) api/gmsh.mjs(WASM/JS)
gmsh.h 与 gmshc.h 的头部都写着同一句话:Do not edit this file directly: it is automatically generated by 'api/gen.py'.------这是"单一真源(single source of truth)"的宣言。
10.11.2 arg 类:一个参数的五种表示
GenApi.py 的核心是 arg 类(第 16 行),它为每个参数同时维护 C++/C/Python/Julia/Fortran 五种表示:
python
class arg:
def __init__(self, name, value, python_value, julia_value, cpp_type, c_type, out):
self.name, self.value, self.out = name, value, out
self.cpp = cpp_type + " " + name + ((" = " + value) if value else "")
self.c = c_type + " " + name
self.c_arg = name; self.c_pre = ""; self.c_post = ""
self.cwrap_arg = name; self.cwrap_pre = ""; self.cwrap_post = ""
self.python_value = python_value if python_value is not None else value
self.python_arg = ""; self.python_return = ""; self.python_pre = ""
self.julia_value = julia_value if julia_value is not None else value
self.julia_arg = name; self.julia_ctype = ""
self.julia_pre = ""; self.julia_post = ""; self.julia_return = name
self.fortran_args = [name]; self.fortran_c_args = [name]
self.fortran_pre = self.fortran_post = self.fortran_call = self.fortran_local = None
self.texi_type = ""; self.texi = name + ((" = " + self.python_value) if self.python_value else "")
这个类是整个代码生成器的"基因"。每条字段都对应一种语言的"发言方式":
| 前缀 | 目标语言/产物 | 例子 |
|---|---|---|
cpp |
C++ 声明 | const std::string & name |
c |
C 声明 | const char * name |
c_arg/c_pre/c_post |
C 实现(包装 C++) | 类型转换胶水 |
cwrap_arg/cwrap_pre/cwrap_post |
C 头包装(gmsh.h_cwrap) |
中间层 |
python_* |
Python(ctypes) | c_char_p(name.encode()) |
julia_* |
Julia(ccall) | Ptr{Cchar} |
fortran_* |
Fortran(iso_c_binding) | character(len=*), intent(in) |
texi_* |
Texinfo 文档 | string |
注意 out 布尔与 python_return/julia_return :输出参数在 C++ 里是引用(int &),在 C 里 是指针(int *),在 Python 里被"提升"为返回值(api_x_.value)。这就是"一份规格、不同语言 的调用约定各异"的具体处理方式。
10.11.3 输入类型工厂
GenApi.py 为每种输入类型定义一个工厂函数,把"类型名 → 各语言表示"固定下来。例如布尔:
python
def ibool(name, value=None, ...):
a = arg(name, ..., "const bool", "const int", False)
a.python_arg = "c_int(bool(" + name + "))"; a.cwrap_arg = "(int)" + name
a.julia_ctype = "Cint"
a.fortran_types = ["logical, intent(in)"]
a.fortran_c_api = ["integer(c_int), value, intent(in)"]
a.fortran_call = f"{name}=merge(1_c_int, 0_c_int, {name})"
return a
def istring(name, value=None, ...):
a = arg(name, ..., "const std::string &", "const char *", False)
a.python_arg = "c_char_p(" + name + ".encode())"; a.cwrap_arg = name + ".c_str()"
a.julia_ctype = "Ptr{Cchar}"
a.fortran_types = ["character(len=*), intent(in)"]
a.fortran_c_api = ["character(len=1, kind=c_char), dimension(*), intent(in)"]
a.fortran_call = f"{name}=istring_({name})" # 需 istring_ 辅助函数
return a
这段代码浓缩了"bool 在五种语言里分别怎么表达" :C++ 用 bool、C 用 int(0/1)、Python 用 c_int(bool(x))、Julia 用 Cint、Fortran 用 logical。默认值(value)的表示也不同 : Fortran 用 optval_c_bool(.true./.false., x)。
字符串是"跨语言最麻烦的类型" :C++ 是 std::string,C 是 const char*,Python 要 .encode() 成字节,Fortran 要用 character 数组并写一个 istring_ 辅助函数(见上 istring 工厂的 fortran_call)。
10.11.4 复合类型:vector<vector> 的"指针+尺寸"模式
输出参数里最难的是嵌套容器。以 ivectorvectorint(vector<vector<int>>)为例:
python
def ivectorvectorint(name, ...):
a = arg(name, ..., "const std::vector<std::vector<int> > &",
"const int * const *", False)
api_name, api_name_n, api_name_nn = "api_"+name+"_", "api_"+name+"_n_", "api_"+name+"_nn_"
a.c = "const int * const * " + name + ", const size_t * " + name + "_n, const size_t " + name + "_nn"
a.c_pre = (" std::vector<std::vector<int> > " + api_name + "(" + name + "_nn);\n" +
" for(size_t i = 0; i < " + name + "_nn; ++i)\n" +
" " + api_name + "[i] = std::vector<int>(" + name + "[i], " + name + "[i] + " + name + "_n[i]);\n")
a.cwrap_pre = "size_t **" + api_name + "; ... vectorvector2ptrptr(" + name + ", &" + api_name + ", ...);\n"
...
"指针 + 尺寸"三元组模式是 Gmsh C API 处理可变长数组的统一约定:
name:数据指针(这里是const int* const*);name_n:每行长度数组(const size_t*);name_nn:行数(size_t)。
输出端 则由 C 实现(gmshc.cpp)里的辅助函数 vectorvector2ptrptr/vector2ptr/ vectorstring2charptrptr 负责把 C++ 容器转成 malloc 的 C 数组(10.11.6 节)。
10.11.5 C 包装:GMSH_API 与 extern "C"
gmshc.cpp(api/gmshc.cpp)是"把 C++ 功能暴露成 C"的胶水层。它的头部 定义了一组转换辅助:
cpp
GMSH_API void *gmshMalloc(size_t n) { return malloc(n); }
GMSH_API void gmshFree(void *p) { if(p) free(p); }
// 把 C++ 容器转成 malloc 的 C 数组
template<typename t>
void vector2ptr(const std::vector<t> &v, t **p, size_t *size)
{ if(p){ *p=(t*)gmshMalloc(sizeof(t)*v.size()); for(...) (*p)[i]=v[i]; } if(size) *size=v.size(); }
void vectorpair2intptr(const gmsh::vectorpair &v, int **p, size_t *size) { ... }
void vectorstring2charptrptr(const std::vector<std::string> &v, char ***p, size_t *size) { ... }
然后每个函数体都是一个统一的 try/catch 模式:
cpp
GMSH_API void gmshInitialize(int argc, char ** argv, const int readConfigFiles, const int run, int * ierr)
{
if(ierr) *ierr = 0;
try {
gmsh::initialize(argc, argv, readConfigFiles, run);
}
catch(...){
if(ierr) *ierr = 1;
}
}
这段代码是"C API 错误处理约定"的实现 :C++ 端抛异常 → C 端捕获 → 设置 *ierr = 1(并把 *ierr 初始化为 0)。C 没有异常,所以用返回码。
GMSH_API 宏在 gmsh.h 与 gmshc.h 里定义相同(第 37--47 行):
cpp
#if defined(GMSH_DLL)
#if defined(GMSH_DLL_EXPORT)
#define GMSH_API __declspec(dllexport)
#else
#define GMSH_API __declspec(dllimport)
#endif
#elif defined(__GNUC__)
#define GMSH_API __attribute__ ((visibility("default")))
#else
#define GMSH_API
#endif
这是"跨平台符号可见性"的标准做法 :Windows 用 __declspec、GCC/Clang 用 visibility("default")、其他平台空定义。
10.11.6 C API 的内存约定
gmshc.h 头部有一段关键注释:
c
/* All the functions in the Gmsh C API that return arrays allocate the
* necessary memory with gmshMalloc(). These arrays should be deallocated
* with gmshFree(). */
GMSH_API void gmshFree(void *p);
GMSH_API void *gmshMalloc(size_t n);
这是"谁分配、谁释放"的显式约定 :C API 返回的数组用 gmshMalloc 分配,调用方必须用 gmshFree 释放------不能混用 free (在 Windows DLL 场景里,跨 CRuntime 的 free 会导致崩溃)。 这解释了为什么 Gmsh 要把 malloc/free 也包一层。生成器在 cwrap_post 里对输出参数插入 Free(...) 调用,保证内部临时内存被清理。
10.11.7 Python / Julia / Fortran / JavaScript 绑定
Python (api/gmsh.py)用 ctypes 加载 libgmsh:字符串输入映射为 c_char_p(name.encode())、布尔映射为 c_int(bool(name));输出参数被转成返回值 (Python 无 "输出参数"概念),生成 api_x_ = c_int() → byref(api_x_) → return api_x_.value。 Julia (api/gmsh.jl)用 ccall,输出参数用 Ref{T}(julia_ctype = "Ptr{Cint}"、julia_return = api_name + "[]"),嵌套容器需先算每行长度。 Fortran (api/gmsh.f90)用 iso_c_binding,参数映射如 character(len=*), intent(in);Fortran 与 C 的字符串不兼容 (定长、无终止符),需 istring_ 辅助函数------这是所有 Fortran↔C 绑定的共同痛点。 JavaScript (api/gmsh.mjs)由 WASM 构建(Emscripten 编成 gmsh.js/gmsh.wasm + utils/wasm/ 胶水),让 Gmsh 能跑在浏览器里。
"一份规格、五语言发射"是 Gmsh 以极低边际成本支持多语言绑定的根本原因,也是它作为"可嵌入 库"被大量 CAE/前后处理软件集成的关键。
10.11.8 gmsh.h 的设计约定
从 api/gmsh.h 能读出几条贯穿全 API 的约定:① 纯函数式、只用标准库基本 类型 (头文件注释:By design, the Gmsh C++ API is purely functional, and only uses elementary types from the C++ standard library )------不暴露任何 Gmsh 内部类 (GModel/GEntity 等),只用 int/double/std::string/std::vector,这是"稳定 ABI/API" 的关键 ;② 命名空间分层 gmsh::{option, model, mesh, post, onelab, logger}(如 gmsh::model::occ::addBox);③ GMSH_API 导出宏 统一控制符号可见性;④ 字符串选项寻址 "Category.Option"/"Category[num].Option"(第 1 章选项系统);⑤ 维度+标签寻址 (dim,tag) 对封装成 std::vector<std::pair<int,int>>(gmsh::vectorpair);⑥ 错误约定 : C++/Python 抛异常(General.AbortOnError = 2),C/Fortran 用 int* ierr 返回码。
维度+标签寻址的类型定义 (gmsh.h 第 55--61 行):
cpp
// 实体由 (dim, tag) 两个整数表示;多实体打包为 (dim,tag) 对的向量
typedef std::vector<std::pair<int, int> > vectorpair;
这段注释是"API 如何寻址实体"的权威定义 ------(dim, tag) 对,多个实体打包成 vectorpair。
10.11.9 initialize / finalize 的语义
gmsh::initialize 的签名与语义(gmsh.h 第 66--78 行注释):
cpp
// gmsh::initialize
//
// Initialize the Gmsh API. This must be called before any call to the other
// functions in the API. If `argc' and `argv' (or just `argv' in Python or Julia)
// are provided, they will be handled in the same way as the command line
// arguments in the Gmsh app. If `readConfigFiles' is set, read system Gmsh
// configuration files (gmshrc and gmsh-options). If `run' is set, run in the
// same way as the Gmsh app, either interactively or in batch mode depending on
// the command line arguments. If `run' is not set, initializing the API sets the
// options "General.AbortOnError" to 2 and "General.Terminal" to 1.
GMSH_API void initialize(int argc = 0, char ** argv = 0,
const bool readConfigFiles = true,
const bool run = false);
run 参数是"库模式 vs 应用模式"的开关 :run = false(默认,库模式)设定 AbortOnError=2、Terminal=1(出错抛异常、消息打到终端,适合被嵌入);run = true(应用模式) 按命令行参数进入交互(GUI)或批处理,与直接运行 gmsh 等价。gmsh::finalize("删除所有模型与 视图",对应 10.12.4 节的 GmshFinalize)与 gmsh::isInitialized(返回 1/0)配套。
10.11.10 API 函数清单(按域)
gmsh.h 的函数按命名空间分组,主要域如下:
| 命名空间 | 代表函数 | 对应章节 |
|---|---|---|
gmsh(顶层) |
initialize/finalize/open/merge/write/clear |
第 1、10 章 |
gmsh::option |
setNumber/getNumber/setString/setColor |
第 1 章选项系统 |
gmsh::model |
add/remove/list/setCurrent/getEntities |
第 1、2 章数据模型 |
gmsh::model::mesh |
generate(dim)/partition/createOverlaps/refine |
第 4--7 章 |
gmsh::model::geo |
addPoint/addLine/addSurface/extrude |
第 2、3 章 GEO 内核 |
gmsh::model::occ |
addBox/booleanUnion/fillet/fragment |
第 2 章 OCC 内核 |
gmsh::view |
add/setData/setVisibility |
第 9 章后处理 |
gmsh::onelab |
set/get/run/setNumber/setChanged |
10.13 节 Onelab |
gmsh::logger |
write/start/stop |
第 1 章消息系统 |
这张表说明 API 覆盖了 Gmsh 的全部四个模块(几何/网格/求解/后处理) 。特别值得注意的是 gmsh::model::mesh::generate(dim)------它对应第 4 章的"0D→1D→2D→3D 逐层推进",dim 参数就是 "生成到哪一维"。
onelab 域的函数签名 (gmsh.h 第 4341--4416 行):
cpp
void set(const std::string &data, const std::string &format = "json");
void get(std::string &data, const std::string &name = "", const std::string &format = "json");
void setNumber(const std::string &name, const std::vector<double> &value);
void getNumber(const std::string &name, std::vector<double> &value);
int getChanged(const std::string &name);
void setChanged(const std::string &name, const int value);
void run(const std::string &name = "", const std::string &command = "");
注意 onelab 参数一律用 std::vector (即使只有一个值)------这是".number 参数可以有多个值(列表/ 循环)"的体现(10.13.3 节)。set/get 用 JSON 字符串 作交换格式,方便 Python/JS 端直接 json.loads/JSON.parse。
10.11.11 代码生成的工作流
gen.py 的流程(概念上):
- 导入
GenApi:from GenApi import *; - 定义 API 规格 :用
iint/idouble/istring/ostring/ivectorpair等工厂构造参数列表, 再组装成函数、命名空间; - 发射各语言文件 :对每个函数,用
arg的字段拼出 C++/C/Python/Julia/Fortran 声明与实现; - 写文件 到
api/gmsh.h、api/gmshc.h、api/gmshc.cpp、api/gmsh.py、api/gmsh.jl、api/gmsh.f90。
这套"规格 DSL + 多后端发射"是 Gmsh 工程上最优雅的部分之一 :新增一个 API 函数只需在 规格里写一行,六种语言的绑定同时 获得该函数。代价 是生成器本身有相当复杂度(GenApi.py 就有 130 KB+),且调试生成出的代码比手写代码困难。
10.12 Gmsh 程序主干与执行流
10.12.1 Main.cpp:最薄的一层
Gmsh 的程序入口极简(src/common/Main.cpp):
cpp
int main(int argc, char *argv[])
{
#if defined(HAVE_FLTK)
return GmshMainFLTK(argc, argv);
#else
return GmshMainBatch(argc, argv);
#endif
}
它只做一件事:根据是否编入 FLTK,分派到 GmshMainFLTK 或 GmshMainBatch 。Windows 下为了 处理非 ASCII 命令行参数,源码用 wmain + WideCharToMultiByte 转 UTF-8(第 9--38 行)------这是 第 2 章"Gmsh 全 UTF-8"约定的入口实现。
10.12.2 GmshMainBatch 与 GmshMainFLTK
src/common/GmshGlobal.cpp 里两个主函数:
cpp
int GmshMainBatch(int argc, char **argv) // 无 FLTK 构建的入口
{
if(argc < 2) { CTX::instance()->terminal = 1; PrintUsage(argv[0]); exit(0); }
new GModel(); GmshInitialize(argc, argv, true);
if(!Msg::GetGmshClient()) CTX::instance()->terminal = 1;
CTX::instance()->noPopup = 1;
GmshBatch(); GmshFinalize(); Msg::Exit(0);
return 1;
}
int GmshMainFLTK(int argc, char **argv) // 带 FLTK 构建的入口
{
new GModel();
if(argc == 2 && std::string(argv[1]) == "-doc") { // 自动生成选项文档
InitOptions(0); PluginManager::instance()->registerDefaultPlugins();
PrintOptionsDoc(); exit(0);
}
GmshInitialize(argc, argv, true);
if(CTX::instance()->batch) { // batch != 0:不进 GUI
if(!Msg::GetGmshClient()) CTX::instance()->terminal = 1;
GmshBatch(); GmshFinalize(); Msg::Exit(0);
}
return GmshFLTK(argc, argv); // 交互
}
核心信息 :① 两者都先 new GModel()(GmshInitialize 解析选项时需要模型);② GmshMainFLTK 是"批处理与交互的统一入口" ------即便编入 FLTK,只要 CTX::instance()->batch 非零(如 -3),也走 GmshBatch;③ -doc 分支让选项文档自动生成("文档即代码")。
10.12.3 GmshInitialize:初始化顺序
GmshInitialize(第 68 行)是初始化的权威定义:
cpp
int GmshInitialize(int argc, char **argv, bool readConfigFiles,
bool exitOnCommandLineError)
{
if(isInitialized) return 1; // 幂等保护
isInitialized = true;
GModel *dummy = nullptr;
if(GModel::list.empty()) dummy = new GModel(); // 选项解析需要一个模型
Msg::Initialize(argc, argv); // ① 消息系统
InitOptions(0); // ② 默认选项
GetOptions(readConfigFiles, exitOnCommandLineError); // ③ 配置文件 + 命令行
CheckResources(); // ④ 栈空间等资源检查
PluginManager::instance()->registerDefaultPlugins();
robustPredicates::exactinit(1.0, 1.0, 1.0); // ⑤ 第 8 章鲁棒谓词初始化
if(dummy) delete dummy;
return 1;
}
五步顺序很重要 :消息系统先行(否则后面的错误无处报告)→ 默认选项 → GetOptions 读 gmshrc/gmsh-options 与命令行(覆盖默认 )→ CheckResources(第 6 章递归算法依赖大栈) → robustPredicates::exactinit(第 8 章 Shewchuk 谓词的初始化)。isInitialized 的幂等 保护(第二次调用直接返回)避免重复初始化。
10.12.4 GmshBatch:批处理模式
GmshBatch(第 291 行)的流程:StartupMessage() → 打开当前模型 → 逐个处理 CTX::instance()->files(-new 建新模型、-merge/-open 切换、否则打开或合并)→ 按 CTX::instance()->batch 的值分派分支(-3 走 GmshRemote()、-2 走 checkMeshCoherence、 -1 写 .geo、>0 走 GModel::current()->mesh(batch),其中 4=adaptMesh、5=refineMesh、 6=二次重编号、7/8=classifySurfaces、9=computeSizeField)→ 若 batchAfterMesh 且 numPartitions>1 则 partitionMesh → CreateOutputFile(...) 输出 → onelabUtils::runClient()(启动求解器 )→ GoodbyeMessage()。
CTX::instance()->batch 的取值是一套完整的状态码 (Context.h 第 290 行注释给出):
| 值 | 含义 | 值 | 含义 |
|---|---|---|---|
-4 |
Lua 会话 | 1 |
生成 1D 网格 |
-3 |
服务器守护进程 | 2 |
生成 2D 网格 |
-2 |
检查网格一致性 | 3 |
生成 3D 网格 |
-1 |
只回写 .geo |
4 |
自适应网格(adaptMesh) |
0 |
完整图形界面 | 5 |
细化网格(refineMesh) |
这张表就是 Gmsh 命令行 -N 参数的语义 (gmsh -2 file.geo 生成二维网格);batch > 0 直接传给 GModel::mesh(dim) ,与 10.11.10 节的 gmsh::model::mesh::generate(dim) 对应。 批处理最后一步 onelabUtils::runClient() 是"求解器启动"的入口(10.13.7 节)。
10.12.5 GmshFLTK:交互模式
GmshFLTK(第 411 行)在 FLTK/GUI 下依次:FlGui::instance(argc, argv) 建 GUI → check() 立即 显示 → 打开/合并命令行文件 → 按 initialContext 打开初始模块(几何/网格/求解/后处理)→ 监听 外部求解器 (solver.listen 时创建 gmshLocalNetworkClient("Listen", ""))→ solver_cb(...) 启动请求的求解器并填充 onelab 树 → FlGui::instance()->run() 进入事件循环。"监听"是 Onelab 与 GUI 的接口:配成"监听模式"时 Gmsh 会等外部客户端连接。
10.12.6 三种驱动路径
Gmsh 的功能可由三种路径 驱动,最终都汇聚到同一套 GModel/GEntity:
text
① 脚本路径 .geo → Parser(第 3 章)→ GEO/OCC 内核 → GModel
② API 路径 用户代码 → gmsh.h/gmshc.h → src/common/gmsh.cpp → GModel
③ Onelab 路径 Gmsh + 外部客户端 → 参数/通信 → GModel + 外部求解器
三者共享同一份内核,这解释了为什么"用 API 建的模型"和"用脚本建的模型"行为一致。
10.12.7 上下文单例 CTX
CTX(src/common/Context.h)是全程序唯一的配置上下文:
cpp
class CTX {
private:
static CTX *_instance; static CTX *_create();
public:
static CTX *instance() { return _instance ? _instance : _create(); } // 惰性单例,须内联
int entityColorsStamp, entityVisibilityStamp;
void meshChanged(int ents = ENT_ALL) { meshOptionsChanged(ents); meshContentStamp++; }
void geomChanged(int ents = ENT_ALL) { for(...) geom.stamp[d]++; }
void entityColorsChanged() { entityColorsStamp++; }
...
contextGeometryOptions geom; contextMeshOptions mesh;
...(post/solver/print/color 等匿名结构)
};
CTX::instance() 是"全局单例 + 惰性创建" ,注释 called in tight loops in the drawing code: common path inline 说明它被高频调用,因此必须内联 。两组 stamp 是"失效广播"机制 (第 1 章已述):meshChanged()(网格内容变)、meshOptionsChanged()(仅画法变)、geomChanged()、 entityColorsChanged()/entityVisibilityChanged()。读者把"构建缓存时的 stamp"记下来,每次 比对当前 stamp,不同就重建------注释强调"每个读者都看到每个变化,且没有一个读者会把它清掉", 避免了"一个窗口重建后另一个窗口看不到变化"的经典 bug。
CTX 还持有几何/网格/后处理/求解器/打印/颜色的全部选项(contextGeometryOptions geom、 contextMeshOptions mesh、post、solver、print、color 等匿名结构)。它是选项系统的 内存落地 (第 1 章 Options.h 与 CTX 一一对应)。
solver 选项组 (Context.h 第 487--496 行)是 Onelab 的配置源:
cpp
struct {
int plugins, listen;
double timeout;
std::string socketName, pythonInterpreter, octaveInterpreter;
std::string name[NUM_SOLVERS], extension[NUM_SOLVERS];
std::string executable[NUM_SOLVERS], remoteLogin[NUM_SOLVERS];
int autoSaveDatabase, autoLoadDatabase;
int autoArchiveOutputFiles, autoMesh, autoMergeFile;
int autoShowViews, autoShowLastStep, autoCheck, showInvisibleParameters;
} solver;
NUM_SOLVERS(10)个求解器的 name/executable/remoteLogin 对应 GUI 里的"求解器槽位"; autoMesh/autoSaveDatabase/autoLoadDatabase 对应"求解前后自动网格化/存数据库/读数据库"的 行为(10.13.7 节 runClient 里用到)。
10.13 Onelab 集成
10.13.1 Onelab 的定位
Onelab 是 Gmsh 与外部参数/求解器系统的集成层(列日大学与鲁汶大学开发),核心思想是把"参数" 从"求解器"里解耦出来,让多个求解器共享一份参数数据库 。src/ 下没有独立的 onelab/ 目录; 实现分布在 src/common/onelab.h(参数/客户端/服务器类)、 onelabUtils.cpp(Gmsh 侧客户端逻辑)、 gmshLocalNetworkClient.h/.cpp、 GmshSocket.h(socket 协议)、 GmshRemote.cpp(服务器守护进程)与 libol1.h(libOL 客户端库)。
10.13.2 parameter:参数基类
onelab::parameter(onelab.h 第 48 行)是所有参数的基类:
cpp
class parameter {
private:
std::string _name; // '/' 分隔的层次路径
std::string _label; std::string _help;
std::map<std::string, int> _clients; // 客户端名 → changed 标志
int _changedValue; // 更新时 changed 应置为何值(默认 31)
bool _visible; bool _readOnly;
protected:
std::map<std::string, std::string> _attributes; // 附加属性
public:
parameter(const std::string &name = "", const std::string &label = "", const std::string &help = "") ...
void setChanged(int changed, const std::string &client = "");
void setVisible(bool visible); void setReadOnly(bool readOnly);
void addClient(const std::string &client, int changed);
virtual std::string getType() const = 0;
std::string getPath() const; std::string getShortName() const; int getChanged(...) const;
...
};
三个设计点 :① 参数有"层级路径" (/ 分隔,如 "Geometry/NbPoints"),getShortName 会去掉排序前缀 (开头的空白/数字/花括号)------这些前缀用于"强制参数树显示顺序";② _clients 是 map<客户端名, changed 标志> ,getChanged("") 返回所有客户端里最大的 changed ;③ _changedValue 默认 31 (defaultChangedValue()),注释说"1--31 可调制变更 程度,从而改变要执行的动作"------例如 changed > 2 才重新生成网格(10.13.7 节)。parameter 提供四种序列化:toChar/fromChar(内部二进制)、toFile/fromFile(数据库文件)、 toJSON/fromJSON(走 picojson.h)。
10.13.3 number 与 string
onelab::number(第 432 行)是数值参数:
cpp
class number : public parameter {
private:
std::vector<double> _values, _choices; // 值(可多个)/ 选项
double _min, _max, _step;
int _index; // 循环当前下标(-1 表示不在循环)
std::map<double, std::string> _valueLabels;
public:
number(const std::string &name = "", double value = 0., ...) ...
void setValue(double value); void setValues(const std::vector<double> &values);
void setMin(double min); void setMax(double max); void setStep(double step);
void setIndex(int index); void setChoices(const std::vector<double> &choices);
std::string getType() const { return "number"; }
};
数值参数支持"多值/范围/步长/选项/标签" ------这不是简单的标量,而是一个参数可以有多个值 (做参数扫描)、有 min/max/step(界面滑条)、有 choices(下拉选项) 。这解释了为什么 10.11.10 节的 API gmsh::onelab::setNumber 用 std::vector<double>。
onelab::string(第 678 行)是对应的字符串参数(同样支持多值/选项)。
parameterSpace(第 842 行)是参数空间(参数树) :它按名字管理 parameter*,支持设置/查询/ 筛选/序列化。注释里的"Subpaths can start with white space, numbers or braces (in that order) to force their relative ordering"就是 10.13.2 节排序前缀的出处。
10.13.4 client 与 server
onelab::server(第 1295 行)是单例参数服务器:
cpp
class server {
private:
static server *_server; // 唯一服务器(单例)
std::set<client *> _clients; // 已连接客户端
parameterSpace _parameterSpace; // 参数空间
public:
static server *instance(const std::string &address = "")
{ if(!_server) _server = new server(address); return _server; }
template <class T> bool set(const T &p, const std::string &client = "");
template <class T> bool get(std::vector<T> &ps, const std::string &name = "",
const std::string &client = "");
void registerClient(client *c) { _clients.insert(c); c->setId((int)_clients.size()); }
void setChanged(int changed, const std::string &client = "");
int getChanged(const std::string &client = "");
std::vector<std::string> toChar(const std::string &client = "");
bool toFile(FILE *fp, ...); bool toJSON(std::string &json, ...);
...
};
onelab::client(第 1201 行)是抽象客户端,接口分三组:
cpp
class client {
protected:
std::string _name; // 客户端名
int _id; // 用于创建唯一 socket
int _index; // 外部客户端列表里的下标
public:
virtual bool run() { return false; }
virtual bool isNetworkClient() { return false; }
virtual bool kill() { return false; }
// ① 消息
virtual void sendInfo(const std::string &msg) { std::cout << msg << std::endl; }
virtual void sendWarning(const std::string &msg) { std::cerr << msg << std::endl; }
virtual void sendError(const std::string &msg) { std::cerr << msg << std::endl; }
virtual void sendProgress(const std::string &msg){ std::cout << msg << std::endl; }
// ② 请求 Gmsh 做事
virtual void sendMergeFileRequest(const std::string &msg) {}
virtual void sendOpenProjectRequest(const std::string &msg) {}
virtual void sendParseStringRequest(const std::string &msg) {}
virtual void sendVertexArray(const std::string &msg) {}
// ③ 参数读写(纯虚)
virtual bool clear(const std::string &name) = 0;
virtual bool set(const number &p) = 0; virtual bool set(const string &p) = 0;
virtual bool get(std::vector<number> &ps, const std::string &name = "") = 0;
virtual bool get(std::vector<string> &ps, const std::string &name = "") = 0;
...
};
②组揭示了 Onelab 的核心能力 :外部客户端可以请求 Gmsh 打开项目、合并文件、解析字符串、 发送顶点数组 ------即 Gmsh 也能被外部求解器"驱动" ,这是一个双向的控制协议。
10.13.5 localClient / localNetworkClient / remoteNetworkClient
cpp
// A local client, which lives in the same memory space as the server.
class localClient : public client { ... };
// A local network client: runs in a separate process, communicates via socket.
class localNetworkClient : public localClient { ... };
// A remote network client.
class remoteNetworkClient : public client { ... };
三种客户端对应三种"距离" :localClient(同进程,直接 server::instance()->set(p, _name) 转发到单例)、localNetworkClient(同机不同进程,socket)、remoteNetworkClient(远程机器, socket + remoteLogin)。
10.13.6 gmshLocalNetworkClient:Gmsh 侧的客户端
src/common/gmshLocalNetworkClient.h 是 localNetworkClient 的派生:
cpp
class gmshLocalNetworkClient : public onelab::localNetworkClient {
private:
std::vector<gmshLocalNetworkClient *> _clients; // 子客户端(元模型会启动多个底层模型)
gmshLocalNetworkClient *_father; // 启动它的客户端(GMSH_CONNECT);顶层为 0
public:
gmshLocalNetworkClient(const std::string &name, const std::string &executable,
const std::string &remoteLogin = "", bool fullCmd = false) ...
void setFather(gmshLocalNetworkClient *father); void addClient(gmshLocalNetworkClient *);
int getNumConnectedClients(); // 统计 pid != -1 的客户端
bool receiveMessage(gmshLocalNetworkClient *master);
bool run(); bool kill();
};
_clients 与 _father 构成一棵客户端树 :一个 metamodel(元模型)可以启动多个子模型 (GMSH_CONNECT 创建子客户端)。这是 Onelab 支持"模型链/元模型"的机制------一个高级求解器 可以调用多个底层求解器,而 Gmsh 只是树中的一个节点。
10.13.7 runClient:Gmsh 侧的求解驱动
src/common/onelabUtils.cpp 的 runClient(第 441 行)是 "启动求解器"的入口,也是 10.12.4 节 GmshBatch 最后一步调用的:
cpp
void runClient(const std::string &name, const std::string &command)
{
if(name.size()) { // 启动指定客户端
onelab::remoteNetworkClient *c =
dynamic_cast<onelab::remoteNetworkClient *>(Msg::GetOnelabClient());
if(c) c->runSubClient(name, command);
else { gmshLocalNetworkClient client(name, command, "", true); client.run(); }
}
else { // 三要素取自 CTX 选项
int num = CTX::instance()->launchSolverAtStartup;
if(num == -1) return;
else if(num >= 0) { name = opt_solver_name(num, GMSH_GET, "");
exe = opt_solver_executable(num, GMSH_GET, "");
host = opt_solver_remote_login(num, GMSH_GET, ""); }
onelab::number n("0Metamodel/Batch", CTX::instance()->batch); n.setVisible(false);
onelab::server::instance()->set(n);
if(name.size()) { c = new gmshLocalNetworkClient(name, exe, host); o = c->getName() + "/Action"; }
runGmshClient("initialize", CTX::instance()->solver.autoMesh); // Gmsh 自注册
if(c) { o.setValue("initialize"); onelab::server::instance()->set(o); c->run(); }
if(CTX::instance()->solver.autoLoadDatabase) { ... loadDb(db); }
runGmshClient("check", CTX::instance()->solver.autoMesh);
if(c) { guessModelName(c); o.setValue("check"); onelab::server::instance()->set(o); c->run(); }
...
}
}
这段代码是 Onelab 工作流的完整编排 :求解器三要素来自 CTX 选项;0Metamodel/Batch 是隐藏 参数(前缀 0 是排序技巧);<client>/Action 是"动作协议" ------先后置为 "initialize"、 "check",每次置值并 c->run()("参数即命令");autoLoadDatabase/autoSaveDatabase 让 <model>.db 保存参数状态;runGmshClient 把"Gmsh 自己"也注册为客户端,从而能响应外部请求。 onelabUtils.cpp(第 401--433 行)还在参数变化(changed > 2)时自动重新打开项目、按需重新生成 网格、写出 .msh------即"参数一变,网格与求解自动重跑"。
10.13.8 socket 协议与 GmshRemote
GmshSocket.h:socket 通信底层协议(被onelab.h直接#include,是通信基元)。GmshRemote.cpp:服务器守护进程(batch == -3时运行),让 Gmsh 作"常驻服务器"等待 客户端连接。gmshLocalNetworkClient::receiveMessage:接收并处理来自 master 的消息。
通信的两种介质 :socket (网络,同机/远程进程)与文件 (.db/.json,持久化与脱机 交换)。parameter::charSep() 返回 '\0' ------参数序列化用 \0 作分隔符 ,故 sanitize() 把内容里的 \0 换成空格("用零字节做字段分隔、避免转义"的实用选择)。
10.13.9 libOL 与外部客户端库
src/common/libol1.h 是 libOL(Octree Localisation) 的 C 接口 (Copyright 2012-2018 Loïc Maréchal / INRIA),提供 LolNewOctree/LolGetNearest/ LolIntersectSurface/LolProjectVertex 等函数------这是 Onelab 生态里的"网格定位库":外部 求解器(如 GetDP)需要快速回答"某点落在哪个单元、最近的顶点是哪个"。它还同时提供 Fortran 77 API,印证了 Onelab 生态的"多语言、科学计算"定位。
10.13.10 典型工作流
典型多物理工作流:① 用户在 Gmsh GUI 打开几何 + .pro 求解器文件 → ② Gmsh 解析几何,把参数写入 onelab 参数树 → ③ 用户点"Run"(或 changed 触发)→ onelabUtils::runClient → ④ Gmsh 生成 网格、写 .msh → ⑤ 启动外部求解器进程(gmshLocalNetworkClient::run)→ ⑥ 求解器经 socket 读 参数、读 .msh(或 Gmsh 主动 sendVertexArray)→ ⑦ 求解器计算、写结果(.pos/.vtk)→ ⑧ 请求 Gmsh 合并结果(sendMergeFileRequest)→ ⑨ Gmsh 读入结果成为后处理视图(第 9 章)→ ⑩ 用户改参数 → 回到 ③(增量:changed 决定重跑哪些步骤)。
关键在"参数作为唯一的交互接口" :求解器不必知道 Gmsh 的 API,Gmsh 也不必知道求解器的实现 ------两者只共享一份参数数据库与一套通信协议("参数与求解器解耦"的全部含义)。
10.14 与前面各章的接口闭环
10.14.1 一条完整的调用链
把十章接起来:第 2 章 几何建模(Box(1) → BooleanDifference → OCCRegion/OCCFace)→ 第 3 章 脚本解析(Gmsh.y 语义动作调用 OCC_Internals::addBox/booleanDifference)→ 第 4 章 网格流水线(0D→1D→2D→3D,孔洞面成为 GFace、体为 GRegion)→ 第 5 章 二维算法 (每个 GFace 在参数平面三角化,用度量补偿畸变)→ 第 6 章 三维算法(GRegion 内四面体化)→ 第 7 章 质量与高级(局部优化、高阶曲边)→ 第 8 章 数值内核(MElement 按需生成形函数/积分 点)→ 第 9 章 后处理(PViewData* 承载解)→ 第 10 章 求解器(elasticitySolver::assemble → linearSystem::addToMatrix → systemSolve → SolverField 插值 → buildStressesView,成为 第 9 章的视图)。每一环都只依赖前一环建立的抽象。
10.14.2 每一章与本章的接口点
| 前面的章节 | 与本章的接口 |
|---|---|
| 第 1 章 数据模型 | GModel/GEntity 是求解器的"问题定义";CTX 是 gmsh::initialize 的实现基础 |
| 第 2 章 几何内核 | groupOfElements(dim, physical) 与 GModel::getMeshVerticesForPhysicalGroup 取自几何实体 |
| 第 3 章 脚本 | 第 10.12.6 节的"脚本路径";Onelab 的 sendParseStringRequest 能回灌脚本 |
| 第 4--7 章 网格 | MElement 是 SElement 的底层;getJacobian/getIntegrationPoints 是第 8 章接口 |
| 第 8 章 数值内核 | FunctionSpace 完全建立在 getShapeFunctions/getGradShapeFunctions 之上 |
| 第 9 章 后处理 | build*View 产出 PView;SolverField 是解与视图之间的桥梁 |
10.14.3 全系列总结:十章架构总览
| 章 | 主题 | 核心抽象 | 关键机制 |
|---|---|---|---|
| 1 | 数据模型与架构 | GModel/GEntity/CTX |
四维实体集合、MSH 编号、(dim,tag) 寻址、API 薄壳 |
| 2 | 几何内核 | GEntity 契约 |
双内核(GEO/OCC)、布尔/拉伸、拓扑重建、格式枢纽 |
| 3 | 脚本与解析 | flex+bison 文法 | 自动编号、表达式求值、循环回读、双内核分派 |
| 4 | 网格流水线 | 逐维推进 + 状态机 | OpenMP、重试、规范重编号、降维删除 |
| 5 | 二维算法 | 参数平面 + 度量 | Shephard-Beall BDS、两遍边界恢复、前沿推进 |
| 6 | 三维算法 | 平面数组内核 | 外接球心、边界恢复、点插入、HXT/MMG3D 回退 |
| 7 | 质量与高级技术 | gamma/eta/NCJ 度量 | 局部搜索、Winslow 解缠、边界层、高阶曲边、分区 |
| 8 | 数值内核 | 父类型+阶数+族别 | 按需生成形函数/积分点、五类基、鲁棒谓词 |
| 9 | 后处理与插件 | 视图 = 数据 + 选项 | 两类容器、std::function 回调、动态库 ABI |
| 10 | 求解器与 API | traits 驱动 + 后端无关 | FunctionSpace<T>、dofManager、弱形式、代码生成、Onelab |
十章合起来,就是一套完整的"科学计算平台"源码地图:几何→网格→数值→求解→后处理, 外加脚本、API、集成三条驱动路径。
10.14.4 贯穿全系列的设计原则
贯穿十章的五条原则:抽象先行、内核可插拔 (GEntity/Field/Plugin/linearSystem/ FunctionSpace<T> 都是"稳定接口 + 多实现");务实而非教条 (几何与网格不分离、求解器塞进 网格工具、循环用回读);性能优先的微观优化 (BDS 内联面数组、平面数组网格内核、定长栈数组、 CSR 二分查找、按类型缓存积分点);正确性的最后防线 (鲁棒谓词、规范重编号、异常隔离、失败 回退链、C API 的 try/catch);理论能力的工程化与单一真源(同调群、Smith 标准型、贝塞尔正性、 调和映射、Voigt 弹性,以及"选项文档与 API 自动生成")。
10.15 常见问答
Q1:为什么把求解器放进网格工具? Gmsh 需要它(重参数化、内置求解),且能复用已有基础设施 ------MElement 的形函数/积分点/雅可比都是现成的;代价是编译期依赖(ENABLE_SOLVER),收益是 "网格工具也能解 PDE"(10.1.1 节)。
Q2:为什么要代码生成,而不是手写五套 API? 手写五套会不一致、易漏、难维护 ;一份规格 (GenApi.py)发射五种语言,保证函数名、参数顺序、默认值一致,代价是生成器复杂(10.11.11 节)。
Q3:Onelab 如何解耦参数与求解器? 通过一份共享的参数数据库 :求解器只读写参数,Gmsh 只 提供参数与通信。<client>/Action 是"命令通道",changed 是"变更通知"(10.13.10 节)。
Q4:fixDof 与 numberDof 谁先谁后? 必须先 fixDof :numberDof 跳过已固定的自由度, 而 fixDof 遇到已编号的自由度会放弃("先到先得")(10.3.8 节)。
Q5:为什么 elasticitySolver::solve 有三段 #if? 因为后端可插拔:有 PETSc 用 PETSc、 有 GMM 用 GMM、都没有用稠密 Full(10.8.3 节)。
Q6:gmsh::initialize(run=true) 和直接运行 gmsh 有何区别? 功能上等价;区别在默认选项 (库模式设 AbortOnError=2/Terminal=1,应用模式按命令行)(10.11.9 节)。
10.16 术语中英对照速查
| 英文/代码 | 中文 | 说明 |
|---|---|---|
FunctionSpace<T> |
有限元空间 | 形函数 + 自由度映射,按值类型 T 参数化 |
TensorialTraits |
张量 traits | 让梯度类型随 T 自动升级 |
Dof / dofManager<T> |
自由度/管理器 | 编号、固定、约束、组装 |
DofAffineConstraint |
仿射约束 | u = Σαu + β |
femTerm<T> |
节点级弱形式项 | 提供单元矩阵/向量 |
helmholtzTerm / laplaceTerm / massTerm |
亥姆霍兹/拉普拉斯/质量项 | ∇·k∇U − aU 及其退化 |
elasticityTerm |
弹性项 | ∫BᵀHB(Voigt) |
linearSystem<scalar> |
线性系统 | 后端无关的矩阵/求解接口 |
CSR / PETSc / MUMPS / Eigen / GMM |
线性后端 | 自研稀疏 / 外部库 |
SolverField<T> |
求解场 | 把"解"当作空间用 |
groupOfElements / GaussQuadrature |
单元组/积分规则 | 求解的支持域与积分 |
gmsh::vectorpair |
(dim,tag) 对的向量 | API 实体寻址 |
GMSH_API / gmshMalloc / gmshFree |
导出宏 / 分配器 | 跨平台 ABI |
CTX |
全局上下文 | 单例 + 失效 stamp |
Onelab parameter / parameterSpace |
参数/参数空间 | 层级路径 + 客户端变更标志 |
Onelab client / server / changed |
客户端/服务器/变更标志 | 参数读写 + 双向通信 |
| 重参数化 | parametrization | 调和映射展平曲面 |
10.17 源码索引与延伸阅读
求解器基础设施 :functionSpace.h(FunctionSpace<T>/TensorialTraits/各种 Lagrange 空间)、 dofManager.h/.cpp(Dof/dofTraits/dofManager<T>)、SElement.h/.cpp、femTerm.h 及各 *Term.h、terms.h/.hpp、materialLaw.h、solverField.h、solverAlgorithms.h、 quadratureRules.h、groupOfElements.h、filters.h、sparsityPattern.h/.cpp。
线性系统后端 :linearSystem.h(抽象)、linearSystemCSR.h/.cpp、linearSystemGmm.h、 linearSystemPETSc.h/.cpp、linearSystemMUMPS.h/.cpp、linearSystemEigen.h/.cpp、 linearSystemFull.h。
具体求解器 :elasticitySolver.h/.cpp、thermicSolver.h、eigenSolver.h/.cpp、 frameSolver.h、solverAlgorithms.h/.cpp(含外部求解器 socket 机制)。
API 与代码生成 :api/gen.py、api/GenApi.py;api/gmsh.h、api/gmshc.h、api/gmshc.cpp、 api/gmsh.py、api/gmsh.jl、api/gmsh.f90、api/gmsh.mjs;src/common/gmsh.cpp。
程序主干与集成 :src/common/Main.cpp、GmshGlobal.cpp/.h、Context.h、Options.h、 onelab.h、onelabUtils.h/.cpp、gmshLocalNetworkClient.h/.cpp、GmshSocket.h、 GmshRemote.cpp、libol1.h;src/geo/GModelParametrize.h/.cpp(重参数化)。
延伸阅读 :Gmsh 参考手册的"Gmsh API"与"Onelab"章节;tutorials/(各语言)与 examples/api/;Onelab 项目主页(onelab.info)。
10.18 本章小结
求解器与 API 层的核心:① traits 驱动的通用有限元空间 (TensorialTraits 让同一份 FunctionSpace<T> 服务标量/向量/张量场,梯度类型随 T 自动升级);② 分层的装配架构 (Dof → dofManager → SElement → femTerm/terms → linearSystem);③ 弱形式的两套 风格 ("节点级裸装配"的 helmholtzTerm/elasticityTerm 与"表达式风格"的 terms.h);④ 后端无关 (linearSystem 两层抽象 + CSR/PETSc/MUMPS/Eigen/GMM/Full + 编译期宏降级);⑤ 重参数化是求解器的复用 (调和映射 = 两个标量拉普拉斯问题);⑥ 一份规格、五种语言 (GenApi.py 的 arg 类 + GMSH_API/gmshMalloc/gmshFree);⑦ 三入口一内核 (脚本、 API、Onelab 三条路径落到同一套 GModel/GEntity);⑧ Onelab 的参数解耦(参数服务器 + 三类客户端 + 双向消息协议)。
本章关键词/索引 :FunctionSpace、TensorialTraits、ScalarLagrangeFunctionSpace、 VectorLagrangeFunctionSpace、ScalarToAnyFunctionSpace、CompositeFunctionSpace、Dof、 dofTraits、dofManager、DofAffineConstraint、SElement、femTerm、helmholtzTerm、 laplaceTerm、massTerm、elasticityTerm、elasticityMixedTerm、terms.h、 IsotropicElasticTerm、LagrangeMultiplierTerm、solverAlgorithms、Assemble、FilterDof、 quadratureRules、GaussQuadrature、SolverField、elasticitySolver、thermicSolver、 eigenSolver、frameSolver、groupOfElements、linearSystem、linearSystemCSR、 linearSystemPETSc、sparsityPattern、GModelParametrize、GenApi.py、arg、gmsh.h、 gmshc.h、gmsh.py、gmsh.jl、gmsh.f90、vectorpair、GMSH_API、gmshMalloc、 gmshFree、Main.cpp、GmshBatch、GmshFLTK、GmshInitialize、CTX、parameter、 parameterSpace、client、server、gmshLocalNetworkClient、GmshSocket、changed、 runClient、libol1.h。
全系列结语
十章的旅程到此结束。回看这条主线:数据模型是根基 (第 1 章),几何层双内核 (第 2 章), 脚本层自制解析器 (第 3 章),网格流水线四阶段 (第 4 章),二维算法 (第 5 章), 三维算法 (第 6 章),质量与高级技术 (第 7 章),数值内核 (第 8 章), 后处理与插件 (第 9 章),最后是求解器与 API 绑定(第 10 章)。
十章合起来回答了一个问题 :一个能独立运行、又能对接工业 CAD、能跑五种语言、能内置求解 PDE 的网格工具,其源码是如何组织的?答案是五条设计原则(抽象先行、务实而非教条、性能优先、 正确性最后防线、理论能力工程化)在十层子系统里的反复体现。
希望这个系列能帮你在需要时,快速定位 Gmsh 源码中的任意一条调用链,并理解其背后的工程权衡。