Gmsh 源码深度解析-第 10 章 求解器框架、API 与语言绑定

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;
};

这里能读出三条设计信息:

  1. 纯虚 vs 带默认实现 :f/gradf/hessfuvw/getNumKeys/getKeys 是纯虚的,必须实现; fuvw/gradfuvw/hessf/thirdDevf 有空的默认实现,注释写着"should return to pure virtual once all is done"------说明这是一次渐进式的接口收紧,作者有意让旧空间先编译通过, 再逐步补齐。
  2. "物理"与"参考"两套求值 :f/gradf 返回物理坐标下的量与梯度(已做雅可比变换); fuvw/gradfuvw 返回参考坐标下的量(未变换)。后者在需要"先求和再统一变换"的场合更高效。
  3. 三阶导为高阶 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∇ξϕ \nabla_x \phi = J^{-T}\nabla_\xi \phi ∇xϕ=J−T∇ξϕ:

  1. ele->getGradShapeFunctions(u,v,w,gradsuvw) 拿参考坐标梯度 ∇ξϕi \nabla_\xi \phi_i ∇ξϕi;
  2. ele->getJacobian(u,v,w,jac) 拿雅可比 J=∂x/∂ξJ = \partial x/\partial \xi J=∂x/∂ξ;
  3. inv3x3(jac, invjac) 求 J−1J^{-1} J−1;
  4. 用 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\mathbf{u} 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\nabla(\mathbf{u}) = \sum_i \mathbf{u}_i \otimes \nabla\phi_i ∇(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\nabla(NF) = \nabla N\cdot F + N\nabla F ∇(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)v(x) v(x) 是各场 ff f、各基 函数 ii i 的系数 vfi v_{fi} vfi 与形函数 sif(x) s^f_i(x) sif(x) 的乘积之和 。一个 Dof 就是"第 ff f 个场在第 ii 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_k = \sum_i \alpha_i u_i + \beta 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=λMxKx = \lambda Mx 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 是整个组装器的核心,有四个重载:

  1. assemble(Dof R, Dof C, dataMat value)(第 393 行):单个矩阵元。
  2. assemble(vector<Dof> &R, vector<Dof> &C, fullMatrix<dataMat> &m)(第 419 行):局部 矩阵,行列自由度不同(非对称/混合元)。
  3. assemble(vector<Dof> &R, fullVector<dataMat> &m)(第 478 行):线性形式(右端项)。
  4. 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 u_k = \sum_i \alpha_i u_i 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),则 ∑j Kij uj=fi \sum_j K_{ij}u_j = f_i ∑jKijuj=fi 里的已知项 − Kij gj -K_{ij}g_j −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 行)按优先级链 查找:

  1. 别名 :若 associatedWith 里有映射,转到目标自由度;
  2. 影子 :ghostValue 里有值 → 返回;
  3. 未知量 :unknown 里有编号 → _current->getFromSolution(编号, val);
  4. 固定值 :fixed 里有 → 返回固定值;
  5. 仿射约束 :constraints 里有 → 计算 ∑αiui+β \sum \alpha_i u_i + \beta ∑α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]);
};

它能读出四条重要信息:

  1. SElement = MElement + 求解器附加数据 。注释 FIXME: the base SElement should not contain anything except the MElement 自嘲了当前设计的简约------未来更高级的求解器会派生 SElement 并加自己的容器。
  2. getVertex(i) 会解引用父单元 (_e->getParent())------与 ScalarLagrangeFunctionSpace 的父单元处理一致。
  3. 富集函数是"静态"的 :_enrichement_s/_enrichement_t 是 static 成员,即全局共享 。 这反映了早期设计的妥协:XFEM 类功能一次只能有一个活跃的富集函数。setShapeEnrichement 与 setTestEnrichement 分别设置"形函数富集"与"测试函数富集"。
  4. 提供"节点级"形函数接口 :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);
}

这就是"面力/边力/体力"的数值积分 : ∫Nkf dS \int N_k \mathbf{f}\,dS ∫NkfdS 用 Gauss 积分 ∑ip det⁡J⋅ wip ⋅Nk( ξip )⋅f( xip ) \sum_{ip}\det J\cdot w_{ip}\cdot N_k(\xi_{ip})\cdot\mathbf{f}(x_{ip}) ∑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)2(p-1) 2(p−1) 阶、"形函数×形函数" 2p2p 2p 阶,取 2p+12p+1 2p+1 是保守的过积分);② 积分点与权重 由 第 8 章的 e->getIntegrationPoints 给出;③ 雅可比 e->getJacobian(u,v,w,jac) 返回 det⁡J\det J detJ, weightDetJ = weight * detJ 是"参数体积元 → 物理体积元"换算;④ 材料系数 _k/_a 通过 setElement(e) 绑定单元后在积分点的物理坐标 p 处求值(为"域相关材料"准备的两段式); ⑤ 双线性型 mjk +=(K∇ϕj⋅∇ϕk+Aϕjϕk)wdet⁡J m_{jk} \mathrel{+}= (K\nabla\phi_j\cdot\nabla\phi_k + A\phi_j\phi_k)w\det J 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边界 f = -K u_{\text{边界}} f=−Ku边界,与 10.3.9 的消元一致)。_coordView 是一个 map<MVertex*, SPoint3>,把边界节点映射到它在参数 平面上的 (u,v)。

它服务于调和映射 :把曲面参数 (u,v)(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 d_x dx,接着 nn n 个 dy d_y dy,最后 nn n 个 dz d_z dz。这解释了 iCompR = iRow / n、ithLocalVertex = iRow % n 的除法取模。

elementMatrix 的实现(src/solver/elasticityTerm.cpp) 用一个 Voigt 记号下的 BB 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μ\lambda + 2\mu λ+2μ、 λ\lambda λ、 μ\mu μ 的组合。

然后是每个积分点上的 BB 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=∫BTHB dV\mathbf{K} = \int \mathbf{B}^T \mathbf{H} \mathbf{B}\, dV K=∫BTHBdV 的翻译 。 B 是应变-位移矩阵(6×3n),H 是弹性矩阵(6×6),BT = Bᵀ(3n×6);行对应六个应变分量 ( εxx ... γyz \varepsilon_{xx}\dots\gamma_{yz} εxx...γyz),列对应自由度。注意 B 与 BT 分开存 (本可直接用 B.transpose()),是为复用 fullMatrix::gemm 而不必每次转置。BTH.gemm(BT, H) 与 m.gemm(BTH, B, weight*detJ, 1.) 用第 8 章的稠密矩阵 fullMatrix::gemm 完成。

elementVector(第 127 行)是体力载荷 ∫Nkf dV \int N_k \mathbf{f}\,dV ∫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 N_k 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\int B^T H B ∫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\int \nabla u\cdot\nabla v ∫∇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>,算 BTHBB^THB BTHB)、 LoadTerm<T1>(线性载荷)、LagrangeMultiplierTerm<T1,double>(位移-乘子耦合,混合元/接触 核心项,对应 ∫(u⋅d)λ\int(\mathbf{u}\cdot\mathbf{d})\lambda ∫(u⋅d)λ)、LagMultTerm、LoadTermOnBorder<T1>。

IsotropicElasticTerm 就是 elasticityTerm 的"表达式版" :同样算 BTHBB^T H B 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 u_z = 0 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 \sum_i N_i u_i ∑iNiui(有限元插值)的完整实现 :取自由度 → 取解值 → 取形函数 → 加权求和。gradf 同理,求 ∑i∇Niui \sum_i \nabla N_i u_i ∑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=λMxKx = \lambda Mx 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( BTHBB^T H B 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\Delta u = 0,\ \Delta v = 0 Δu=0, Δv=0(即最小化 Dirichlet 能量 EΦ=12∫∥∇Φ∥2dAE\\Phi=\frac12\int\|\nabla\Phi\|^2 dA EΦ=21∫∥∇Φ∥2dA,其欧拉-拉格朗日 方程就是拉普拉斯方程)。离散后满足 Ku=fK\mathbf{u}=\mathbf{f} Ku=f, KK 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 的流程(概念上):

  1. 导入 GenApi :from GenApi import *;
  2. 定义 API 规格 :用 iint/idouble/istring/ostring/ivectorpair 等工厂构造参数列表, 再组装成函数、命名空间;
  3. 发射各语言文件 :对每个函数,用 arg 的字段拼出 C++/C/Python/Julia/Fortran 声明与实现;
  4. 写文件 到 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 源码中的任意一条调用链,并理解其背后的工程权衡。

相关推荐
鶴哥只手遮天1 小时前
OpenFOAM 源码深度剖析 · 第二季-02 燃烧与化学反应:`combustionModel`、化学求解器与 Arrhenius 反应率
算法·开源
鶴哥只手遮天1 小时前
Gmsh 源码深度解析-第 6 章 三维体网格算法
算法·开源
鶴哥只手遮天1 小时前
Gmsh 源码深度解析-第 3 章 脚本语言与解析器
算法·开源
鶴哥只手遮天1 小时前
OpenFOAM 源码深度剖析 · 第二季-16 工程与工具链:Windows 交叉编译、性能剖析、数据交换与测试
算法·开源
鶴哥只手遮天1 小时前
Gmsh 源码深度解析-第 5 章 二维曲面网格算法
算法·开源
鶴哥只手遮天1 小时前
Gmsh 源码深度解析-第 1 章 架构总览与数据模型
算法·开源
鶴哥只手遮天1 小时前
OpenFOAM 源码深度剖析 · 第二季-12 `coded` 系列:在字典里写 C++ 并在运行期编译
算法·开源
鶴哥只手遮天1 小时前
OpenFOAM 源码深度剖析 · 第二季-01 多相流与 VOF:`interFoam`、界面压缩与 MULES
算法·开源
鶴哥只手遮天1 小时前
OpenFOAM 源码深度剖析 · 第二季-14 `functionObjects`、求解器控制框架 与 表达式系统
算法·开源