第 9 章:梯度计算(Gradient Computation)
9.1 笛卡尔网格上的梯度计算(Computing Gradients in Cartesian Grids)
梯度在单元形心与面上的离散是构造扩散方程离散形式(第 8 章已用到的)以及对流项相关方程(后续章节会揭示)离散形式的基础;除此以外,梯度还是若干算子求值时必需的中间量——压力导数直接出现在离散动量方程中,速度梯度用于计算湍流模型的生成项以及非牛顿黏度模型中的应变率。本章系统描述在一般网格拓扑上求值梯度的几种技术,先从 Cartesian 结构网格开始,再扩展到非结构网格;这些方法分别属于 Green-Gauss 路线或最小二乘路线,最后再讨论把梯度插值到单元面上的方法。
对图 9.1 那种均匀网格上的一维问题,假设 \(\phi\) 在单元形心之间线性变化,则在面 \(e\) 处的导数为
同样,形心 \(C\) 处的导数可以用两侧相邻单元的值写成
对多维 Cartesian 网格,沿相应坐标方向直接套用同一原则即可。例如对图 9.2 所示的二维网格,用前述中心差分近似得到 \(x\) 与 \(y\) 方向的偏导数为
然而在非结构网格上,单元之间不一定有"邻居对应的固定坐标方向"这一关系,直接套用同一公式便不再实际可行。具体来说,非结构网格的两个相邻单元通常不是沿同一坐标轴对齐的,因而"\(\phi_E - \phi_W\) 除以 \(x\) 方向距离"这种写法没有清晰的几何含义——它可能高估或低估实际梯度,取决于邻居的几何排布。这就是为什么需要 9.2 与 9.3 节介绍的更通用方法。
回到一维、二维情形的精度,式 9.1 与式 9.2 在均匀网格下都是二阶精确的——Taylor 展开中 \(O(\Delta x^2)\) 项的系数被对称结构消去。需要注意这是"均匀网格"假设下的结论;若 \(dx_e\) 随空间变化(但仍按正交方式变化),式 9.1 退化为 \(O(\Delta x)\);式 9.2 在非均匀网格下也会出现 \(O(\Delta x)\) 残差,需用更复杂的"非均匀中心差分"公式修正。书中 9.1 节限定在均匀网格,因此以上精度声明只在均匀前提下成立。
二维情形的几何意义在图 9.2 中通过面积矢量的方向被具体展示:四个主轴方向面 \(S_e\)、\(S_w\)、\(S_n\)、\(S_s\) 的面积矢量都平行于某个坐标轴,而东北、东南、西北、西南四个对角面的面积矢量则有非零的 \(x\) 与 \(y\) 分量。在一般的二维 Cartesian 网格上,这些对角面并不参与一阶中心差分模板的运算(因为它们与 \(C\) 的最近邻居对应的是主轴方向),但它们的面积矢量在更高阶的离散格式(如 Least-Square 9.3 节中讨论的)中会出现在矩阵元素里。
上述中心差分的另一个隐含假设是"邻居是直接邻居"。在 Cartesian 网格中,\(C\) 在 \(x\) 方向上的直接邻居就是 \(E\)(向东一个)与 \(W\)(向西一个),与 \(C\) 之间没有其他单元。但在非结构网格上,\(C\) 的"邻居"集合里包含与 \(C\) 有公共面的所有单元,而每个邻居与 \(C\) 之间的几何关系可能大不相同。这就是为什么在非结构网格上"\(\phi_F - \phi_C\) 除以 \(\mathbf{e}_{CF}\) 方向距离"这种写法没有清晰的几何含义——它依赖于邻居的具体几何排布,而不是预先约定的方向。9.2 节用 Green-Gauss 定理绕过这个问题,9.3 节用最小二乘优化绕过这个问题。
9.2 Green-Gauss 梯度(Green-Gauss Gradient)
这是求梯度最常用的方法之一,它在第 8 章已被引入,此处不再重复推导,只给出最终形式并补充若干求取面值的方法。第 8 章已推导出单元 \(C\)(体积为 \(V_C\))形心处的梯度为
紧凑模板在隐式方法中很有吸引力,因为能生成更紧凑的 Jacobian 矩阵;扩展模板则把更多邻域信息带入重构过程,预期会有更高的精度。这是"模板紧凑度 vs. 精度"的一个经典权衡:隐式耦合求解对 Jacobian 带宽敏感,所以工程实现通常偏好紧凑模板;而显式大步长或者对精度敏感的问题则偏好扩展模板。
回到式 9.4 本身,
是把 \(C\) 的所有邻居面上的 \(\phi_f\) 与 \(\mathbf{S}_f\) 加权求和,再除以 \(V_C\)。这个公式的来源是 Green-Gauss 定理:
(最后一步来自控制体闭合的几何性质:所有面的面积矢量之和为零),所以 \(\nabla c = 0\)。这是式 9.4 的一个隐式性质,对任意面值定义都成立。
\(\mathbf{S}_f\) 的方向约定为"从 \(C\) 指向邻居 \(F\) 的外法向"——也就是 \(\mathbf{S}_f\) 的指向是"离开 \(C\)"。式 9.4 的求和是从 \(C\) 看出去的"流量"——正值代表 \(\phi\) 沿该方向增大、负值代表减小。除以 \(V_C\) 把"流量"归一化到"梯度"。
从工程角度看,式 9.4 的实现涉及到两个层次的遍历:对所有单元 \(C\) 做一遍循环得到 \(\nabla\phi_C\);对每个 \(C\) 遍历它的所有邻居面 \(f\)。第二个层次中,对每个面 \(f\),若它把 \(C\) 与 \(F\) 分开,则这次遍历同时为 \(\nabla\phi_C\) 与 \(\nabla\phi_F\) 提供部分贡献——也就是 \(\phi_f\mathbf{S}_f\) 既被累加到 \(\nabla\phi_C\) 也被累加到 \(\nabla\phi_F\)(符号相反,因为对 \(F\) 而言 \(\mathbf{S}_f\) 指向 \(C\),与对 \(C\) 的方向相反)。这种"一次遍历、为两个单元同时累加"的实现模式在 OpenFOAM 与 uFVM 中都通过 LDU addressing 体现。
下面两小节(9.2.1、9.2.2)分别讨论两种 \(\phi_f\) 的求值方式——紧凑模板与扩展模板——它们在精度、模板规模、实现复杂度上各有取舍。
回到式 9.4 在实际工程中的角色——它是连接"单元形心梯度"与"面上 \(\phi\) 值"的桥梁。一旦 \(\phi_f\) 已知,式 9.4 把 \(\phi_f\) 与 \(\mathbf{S}_f\) 配对乘起来再求和,这本质上是一种离散积分——把 \(\nabla\phi\) 在单元体积上的体积分转换为面 \(\phi\) 与面面积矢量的求和。该离散积分的精度由两个因素决定:(1) 面值 \(\phi_f\) 是否精确代表 \(\phi\) 在该面上的"平均";(2) 面面积矢量 \(\mathbf{S}_f\) 是否精确代表该面的几何。前者由 \(\phi_f\) 的求值方法(紧凑或扩展模板)决定;后者由网格生成阶段保证——一旦网格生成完成,\(\mathbf{S}_f\) 就是固定的。
工程中常把式 9.4 与边界条件分开处理:内面上的 \(\phi_f\) 通过 9.2.1 或 9.2.2 节的方法求值;边界面上的 \(\phi_f\)(即 \(\phi_b\))由边界条件给定(Dirichlet 给出 \(\phi_b\)、Neumann 给出 \(\phi_b\) 的梯度等)。这样把"内部插值"与"边界施加"两个职责分开,让代码更模块化。OpenFOAM 与 uFVM 的实现都遵循这一模式——forAll(owner, facei) 循环处理内面,forAll(mesh.boundary(), patchi) 嵌套循环处理边界面。
Green-Gauss 法在 CFD 历史上的地位——它源于 Gauss 散度定理(也称散度定理或 Ostrogradsky 定理),在连续介质力学与流体力学中有广泛应用。把它离散化用于 FVM 框架的优势是几何无关性:无论网格是结构化还是非结构化、正交还是非正交、面是三角形还是四边形,式 9.4 的形式都保持不变——变化的只是 \(\phi_f\) 的具体求值方式与 \(\mathbf{S}_f\) 的几何含义。这种"形式不变、实现可变"的特性让 Green-Gauss 法成为 FVM 的"通用工具",适用于几乎所有类型的网格。这也是为什么 OpenFOAM 默认 gradSchemes 就是 Gauss——它是最通用、最稳健的选择。
9.2.1 紧凑模板(Method 1: Compact Stencil)
对图 9.3a、b 所示的二维与三维网格系统,最简单的面值近似就是用共享该面的两个单元值的平均,即
该方法在二、三维都易于实现、所有操作都基于面进行,不需要额外的网格连接信息。具体工程上的便利在于:每一步都只需要"两个相邻单元的形心值"以及"面的面积矢量",都不需要查询节点或更远的邻居。这种"只用到 face 的两个 owner/neighbour"的特性是隐式 Jacobian 紧凑的根本原因。
从精度上看,式 9.5 仅当线段 \([CF]\) 与面 \(\mathbf{S}_f\) 的交点正好是面 \(\mathbf{S}_f\) 的形心(即 Gaussian 积分点 \(f\))时才给出 \(\phi_f\) 的二阶近似;除此特殊构型外,一般都达不到二阶精度。这一条件的几何含义是"两单元的形心连线垂直穿过面的形心"——这只有在正交网格(或它的微小形变)下才能近似满足。一般结构化非正交或非结构网格系统(图 9.3c、d)通常不满足这一条件:网格的偏斜(非共面性)会让 \([CF]\) 与面 \(\mathbf{S}_f\) 交于一点 \(f'\),而不是面形心 \(f\)。
此时需要对插值得到的 \(\phi_{f'}\) 做一次修正,得到 \(\phi_f\)。书中的修正思路是:以 \(f'\) 为基点把 \(\phi\) 用一阶 Taylor 展开推到 \(f\):
由于 \(g_C\) 自身依赖于 \(f'\),式 9.9 意味着可以用迭代法得到更准确的梯度:每轮迭代都用上一轮算出的梯度重新计算面值,再算新的梯度。但过多迭代会引发振荡,实战里通常不超过两轮。这一警告的物理来源是:式 9.9 修正项中嵌入了 \(\nabla\phi\) 自身,而 \(\nabla\phi\) 又是 \(\phi_f\) 的函数——多次迭代会把误差放大甚至发散,所以工程上"两轮封顶"是经验法则。
要按式 9.9 计算 \(g_C\),必须先确定 \([CF]\) 与面 \(\mathbf{S}_f\) 的交点 \(f'\)。下面给出三种定位 \(f'\) 的方案。
方案 1:把 \(f'\) 取为 \([CF]\) 与面 \(\mathbf{S}_f\) 的精确交点。令 \(\mathbf{n} = \mathbf{S}_f/\|\mathbf{S}_f\|\) 为面的单位法向、 \(\mathbf{e} = \mathbf{C}\mathbf{F}/\|\mathbf{C}\mathbf{F}\|\) 为沿 \(CF\) 的单位矢量。利用 \(\mathbf{n}\) 与 \(\overline{ff'}\) 正交(即 \(\mathbf{n}\) 垂直于包含 \(\overline{ff'}\) 的面 \(\mathbf{S}_f\))的条件,得
方案 2:把 \(f'\) 取在 \(CF\) 线段中点(图 9.4a、b)。这一选择让公式被显著简化——中点的几何意义是无需解线性方程即可直接得到 \(f'\),也就避开了式 9.10–9.12 所需的方向投影。第一轮迭代直接用 \(\phi_{f'} = (\phi_C + \phi_F)/2\) 起步,再按式 9.4 算梯度;从第二轮起按下式更新 \(\phi_f\):
方案 3:把 \(f'\) 选成使 \(\overline{ff'}\) 距离最短的点(图 9.5a、b),即要求 \(\overline{ff'}\) 垂直于 \([CF]\)。这一选择通常能让首轮迭代的梯度更准确,因为 \(f'\) 是"几何上离面最近"的点——也就是 Taylor 展开在 \(f\) 处估值时最自然的基点。\(f'\) 的一般形式为
(式 9.14)。记 \(f'f\) 的距离为 \(d\),则其平方为\[ d^2 = (\mathbf{r}_f - \mathbf{r}_{f'})\cdot(\mathbf{r}_f - \mathbf{r}_{f'}) = \mathbf{r}_{Cf}\cdot\mathbf{r}_{Cf} - 2q\,\mathbf{r}_{Cf}\cdot(\mathbf{r}_C - \mathbf{r}_F) + q^2(\mathbf{r}_C - \mathbf{r}_F)\cdot(\mathbf{r}_C - \mathbf{r}_F) \](式 9.15)。对 \(q\) 求极小:\(\partial(d^2)/\partial q = 0\),得\[ -2\mathbf{r}_{Cf}\cdot(\mathbf{r}_C - \mathbf{r}_F) + 2q(\mathbf{r}_C - \mathbf{r}_F)\cdot(\mathbf{r}_C - \mathbf{r}_F) = 0 \](式 9.16),解得\[ q = -\frac{\mathbf{r}_{Cf}\cdot\mathbf{r}_{CF}}{\mathbf{r}_{CF}\cdot\mathbf{r}_{CF}} \](式 9.17),其中 \(\mathbf{r}_{Cf} = \mathbf{r}_f - \mathbf{r}_C\)、\(\mathbf{r}_{CF} = \mathbf{r}_F - \mathbf{r}_C\)。知道 \(q\) 后,第一轮迭代先用\[ \mathbf{r}_{f'} = \mathbf{r}_C - (\mathbf{r}_{Cf}\cdot\mathbf{r}_{CF}/\mathbf{r}_{CF}\cdot\mathbf{r}_{CF})(\mathbf{r}_C - \mathbf{r}_F) \]求 \(f'\),再算 \(g_C = \|\mathbf{r}_F - \mathbf{r}_{f'}\|/\|\mathbf{r}_F - \mathbf{r}_C\|\) ,由 \(\phi_{f'} = g_C\phi_C + (1-g_C)\phi_F\) 求值,最后按式 9.4 算梯度。从第二轮起按下式更新
\[ \nabla\phi_{f'} = g_C\nabla\phi_C + (1-g_C)\nabla\phi_F \],再用
\[ \phi_f = \phi_{f'} + \nabla\phi_{f'}\cdot(\mathbf{r}_f - \mathbf{r}_{f'}) \]修正面值,按式 9.4 重算梯度,循环回步骤 5。
三种方案的几何假设依次放松:方案 1 把 \(f'\) 钉死为精确交点(最准确但需要数值解);方案 2 退化为 \(CF\) 中点(解析形式最简单);方案 3 选择使 \(\overline{ff'}\) 最短的点("几何最近"原则,平衡了解析简洁性与精度)。
例题 1:对图 9.6 所示的网格,给定形心坐标 \(C(13, 11)\)、\(F_1(4.5, 9.5)\)、\(F_2(8, 3)\)、\(F_3(17, 3.5)\)、\(F_4(22, 10)\)、\(F_5(16, 20)\)、\(F_6(7, 18)\),各形心处的 \(\phi_C = 167\)、\(\phi_{F1} = 56.75\)、\(\phi_{F2} = 35\)、\(\phi_{F3} = 80\)、\(\phi_{F4} = 252\)、\(\phi_{F5} = 356\)、\(\phi_{F6} = 151\) 已知,邻居梯度
\[ \nabla\phi_{F1} = 10.5\mathbf{i} + 5.5\mathbf{j} \]、\(\nabla\phi_{F2} = 4\mathbf{i} + 9\mathbf{j}\)、
\[ \nabla\phi_{F3} = 4.5\mathbf{i} + 18\mathbf{j} \]、\(\nabla\phi_{F4} = 11\mathbf{i} + 23\mathbf{j}\)、\(\nabla\phi_{F5} = 21\mathbf{i} + 17\mathbf{j}\)、\(\nabla\phi_{F6} = 19\mathbf{i} + 8\mathbf{j}\) 已知,且 \(V_C = 76\)。
求解过程先用节点 \(n_1(9, 14)\)、\(n_2(8, 8)\)、\(n_3(12, 5)\)、\(n_4(17, 9)\)、\(n_5(17.5, 14)\)、\(n_6(12, 17)\) 几何信息求各面形心 \(f_1\sim f_6\) 的坐标,例如 \(f_1\) 取 \(n_1\) 与 \(n_2\) 的中点: \(x_{f_1} = 0.5(x_{n_1} + x_{n_2}) = 0.5(9 + 8) = 8.5\) ,\(y_{f_1} = 0.5(14 + 8) = 11\),故 \(f_1(8.5, 11)\)。其余面形心为 \(f_2(10, 6.5)\)、\(f_3(14.5, 7)\)、\(f_4(17.25, 11.5)\)、\(f_5(14.75, 15.5)\)、\(f_6(10.5, 15.5)\)。
面积矢量为 \(\mathbf{S}_{f_1} = -6\mathbf{i} + \mathbf{j}\)、\(\mathbf{S}_{f_2} = -3\mathbf{i} - 4\mathbf{j}\)、\(\mathbf{S}_{f_3} = 4\mathbf{i} - 5\mathbf{j}\)、\(\mathbf{S}_{f_4} = 5\mathbf{i} - 0.5\mathbf{j}\)、\(\mathbf{S}_{f_5} = 3\mathbf{i} + 5.5\mathbf{j}\)、\(\mathbf{S}_{f_6} = -3\mathbf{i} + 3\mathbf{j}\)。
最后用距离比得到插值因子
\[ (g_C)_1 = \sqrt{(-4)^2 + (-1.5)^2}/\left[\sqrt{(-4)^2 + (-1.5)^2} + \sqrt{(4.5)^2 + 0^2}\right] = 4.272/(4.272 + 4.5) = 0.487 \];其余 \((g_C)_2 = 0.427\)、\((g_C)_3 = 0.502\)、\((g_C)_4 = 0.538\)、\((g_C)_5 = 0.492\)、\((g_C)_6 = 0.455\)。
对无修正的 Green-Gauss 方法,按式 9.5 算面值 \(\phi_{f_1} = 110.442\)、\(\phi_{f_2} = 91.364\)、\(\phi_{f_3} = 123.674\)、\(\phi_{f_4} = 206.27\)、\(\phi_{f_5} = 263.012\)、\(\phi_{f_6} = 158.28\);再按式 9.4 累加:
\[ \nabla\phi_C = \frac{1}{76}\begin{pmatrix}110.442(-6) + 91.364(-3) + 123.674(4) + 206.27(5) + 263.012(3) + 158.28(-3) \\ 110.442(1) + 91.364(-4) + 123.674(-5) + 206.27(-0.5) + 263.012(5.5) + 158.28(3)\end{pmatrix} = 11.889\mathbf{i} + 12.433\mathbf{j} \]对带偏斜修正的方法(\(f'\) 取 \(CF\) 中点),用首轮 \(\phi_{f'} = (\phi_C + \phi_F)/2\) 起步得 \(\phi_{f'_1} = 111.875\)、\(\phi_{f'_2} = 101\)、\(\phi_{f'_3} = 123.5\)、\(\phi_{f'_4} = 209.5\)、\(\phi_{f'_5} = 261.5\)、\(\phi_{f'_6} = 158.5\);代入式 9.4 得\[ \nabla\phi_C = 11.53\mathbf{i} + 11.826\mathbf{j} \]。第二轮修正:定义 \(\mathbf{d}_f = \mathbf{r}_f - 0.5(\mathbf{r}_C + \mathbf{r}_F)\) ,得到 \(\mathbf{d}_{f_1} = -0.25\mathbf{i} + 0.75\mathbf{j}\) 、\(\mathbf{d}_{f_2} = -0.5\mathbf{i} - 0.5\mathbf{j}\)、\(\mathbf{d}_{f_3} = -0.5\mathbf{i} - 0.25\mathbf{j}\)、\(\mathbf{d}_{f_4} = -0.25\mathbf{i} + \mathbf{j}\)、\(\mathbf{d}_{f_5} = 0.25\mathbf{i}\)、\(\mathbf{d}_{f_6} = 0.5\mathbf{i} + \mathbf{j}\);用
\[ \phi_f = \phi_{f'} + 0.5[(\nabla\phi)_C + (\nabla\phi)_F]\cdot\mathbf{d}_f \]修正得 \(\phi_{f_1} = 115.619\)、\(\phi_{f_2} = 91.911\)、\(\phi_{f_3} = 115.764\)、\(\phi_{f_4} = 224.097\)、\(\phi_{f_5} = 265.566\)、\(\phi_{f_6} = 176.046\);再代入式 9.4 得
\[ \nabla\phi_C = 11.614\mathbf{i} + 13.761\mathbf{j} \]。
对比三组结果——无修正 \(11.889\mathbf{i} + 12.433\mathbf{j}\)、首轮带修正 \(11.53\mathbf{i} + 11.826\mathbf{j}\)、二轮修正 \(11.614\mathbf{i} + 13.761\mathbf{j}\)——可以看到修正项对两个分量都有非平凡影响,且通常需要两轮才能收敛到接近稳定的值。
9.2.2 扩展模板(Method 2: Extended Stencil)
面形心 \(f\) 处的 \(\phi_f\) 也可以用定义该面的若干节点上的 \(\phi\) 求平均得到,而这需要先把节点处的物理量估值出来。一个节点 \(n\) 处的物理量等于围绕该节点的若干单元的物理量的加权平均;图 9.7 显示了对节点 \(n_1\) 与 \(n_2\) 各自所贡献的单元。权因子取为节点到单元形心距离的倒数(直观上"距离越近影响越大"),于是节点处 \(\phi\) 的显式表达式为
\[ \phi_n = \frac{\sum_{k=1}^{NB(n)} \phi_{F_k}/\|\mathbf{r}_n - \mathbf{r}_{F_k}\|}{\sum_{k=1}^{NB(n)} 1/\|\mathbf{r}_n - \mathbf{r}_{F_k}\|} \](式 9.18),其中 \(n\) 是节点,\(F_k\) 是相邻单元的形心,\(NB(n)\) 是节点 \(n\) 周围的所有形心总数,\(\|\mathbf{r}_n - \mathbf{r}_{F_k}\|\) 是节点到形心的距离。这种"距离倒数加权"在 OpenFOAM 与 uFVM 中都很常见。得到节点值 \(\phi_n\) 后,再求面形心值 \(\phi_f\),进而求单元形心梯度。在二维情况下,\(\phi_f\) 取两端节点值的平均:
\[ \phi_f = (\phi_{n_1} + \phi_{n_2})/2 \](式 9.19),于是 \(C\) 处梯度为\[ \nabla\phi_C = \frac{1}{V_C}\sum_{f = nb(C)}\phi_f \mathbf{S}_f = \frac{1}{V_C}\sum_{f \sim nb(C)}\frac{\phi_{n_1} + \phi_{n_2}}{2}\mathbf{S}_f \](式 9.20)。三维情况略复杂,面的顶点数依赖于单元类型(三角形面 3 个顶点、四边形面 4 个顶点,多面体面则有任意多个)。\(\phi_f\) 由面顶点的值按"距离倒数"加权得到:
\[ \phi_f = \frac{\sum_{k=1}^{nb(f)} \phi_{n_k}/\|\mathbf{r}_{n_k} - \mathbf{r}_f\|}{\sum_{k=1}^{nb(f)} 1/\|\mathbf{r}_{n_k} - \mathbf{r}_f\|} \](式 9.21),其中 \(nb(f)\) 是面 \(f\) 的顶点数。再由式 9.4 求 \(\nabla\phi_C\)。该方法的缺点之一是面另一侧的单元(穿过面的"错边"单元)的信息也会参与节点 \(\phi\) 的加权平均——也就是说,对一个节点 \(n\),可能同时把面两侧的单元都纳入 \(NB(n)\) 求和。这种"过贡献"在物理上会让节点值带有不必要的"对侧"信息,理论上不够纯粹。可由 Cabello 提出的上风偏置梯度(upwind-biased gradient)方法来规避——只把流动上游一侧的单元纳入加权,但后者的存储开销(要存更多单元信息)与编码复杂度都更高。
例题 2:复用例题 1 的数据,按式 9.18 计算 \(\phi_{n_1}\)、\(\phi_{n_2}\) 时需要先算节点到各相邻形心的距离。对节点 \(n_1(9, 14)\),相邻形心为 \(F_6(7, 18)\)、\(F_1(4.5, 9.5)\)、\(C(13, 11)\),距离分别为 \(\|\mathbf{r}_{n_1} - \mathbf{r}_{F_6}\| = \sqrt{2^2 + 4^2} = 4.472\) 、 \(\|\mathbf{r}_{n_1} - \mathbf{r}_{F_1}\| = \sqrt{4.5^2 + 4.5^2} = 6.364\) 、 \(\|\mathbf{r}_{n_1} - \mathbf{r}_C\| = \sqrt{4^2 + 3^2} = 5\) 。代入加权和:
\[ \phi_{n_1} = \frac{151/4.472 + 56.75/6.364 + 167/5}{1/4.472 + 1/6.364 + 1/5} = \frac{33.77 + 8.92 + 33.4}{0.2237 + 0.1571 + 0.2} = \frac{76.09}{0.5808} = 131.009 \]类似地,对节点 \(n_2(8, 8)\),相邻形心为 \(F_1(4.5, 9.5)\)、\(F_2(8, 3)\)、\(C(13, 11)\),距离分别为 \(\|\mathbf{r}_{n_2} - \mathbf{r}_{F_1}\| = \sqrt{3.5^2 + 1.5^2} = 3.808\) 、 \(\|\mathbf{r}_{n_2} - \mathbf{r}_{F_2}\| = \sqrt{0^2 + 5^2} = 5\) 、 \(\|\mathbf{r}_{n_2} - \mathbf{r}_C\| = \sqrt{5^2 + 3^2} = 5.831\) 。于是\[ \phi_{n_2} = \frac{56.75/3.808 + 35/5 + 167/5.831}{1/3.808 + 1/5 + 1/5.831} = \frac{14.91 + 7.0 + 28.64}{0.2626 + 0.2 + 0.1715} = \frac{50.55}{0.6341} = 79.708 \]再按式 9.19 求 \(\phi_{f_1} = 0.5(131.009 + 79.708) = 105.3585\)。需要注意,原文该例把式 9.18 与式 9.21 都引用成了"9.16",应理解为笔误——按上下文推断实际使用的是式 9.18(即"节点值由周围单元的倒数距离加权得到")。该例未继续算出最终 \(\nabla\phi_C\),因为题目的本意是演示扩展模板的 \(\phi_{f_1}\) 求值。把 Method 1(紧凑)与 Method 2(扩展)做对比时,可以从以下几个角度展开:
- 精度:Method 1 的 \(\phi_f\) 是两个单元值的简单线性插值,在一般非正交网格上不保证二阶精度(除非面正好在两单元中点的特例);Method 2 通过把节点值扩展到周围更多单元后再平均,对偏斜与正交性的破坏不太敏感,理论上能保留更高的精度。
- 模板规模:Method 1 每个面只用到 \(C\) 与 \(F\) 两个单元;Method 2 每个节点用到所有围绕它的单元(数量通常为紧凑模板的两倍左右),再由节点平均得到面值。所以 Method 2 的总模板规模比 Method 1 大。
- 几何连接需求:Method 1 只需要"面—单元"的连接信息;Method 2 需要"节点—单元"以及"面—节点"的连接信息。前者是 OpenFOAM 与 uFVM 都默认有的连接;后者需要在数据预处理时显式构建。
- Jacobian 影响:对隐式耦合求解,Method 1 的 Jacobian 矩阵只受 \(C\) 与 \(F\) 两个邻居的影响,带宽较窄;Method 2 的 Jacobian 会受更多邻居单元的影响,带宽扩大。这正是 9.2 节开头所说"紧凑模板在隐式方法中很有吸引力"的根本原因。
工程上两者的取舍通常取决于问题类型:稳态、外部流场或纯扩散主导的问题里 Method 1 配合偏斜修正已经足够;高阶精度(如湍流生成项需要二阶以上梯度精度)或者动理学敏感的瞬态问题里更倾向 Method 2。OpenFOAM 默认
gradSchemes给出的就是 Method 1(带或不带 skew correction);Method 2 在 uFVM 中通过cfdComputeGradientNodal暴露。最后回到 9.2 节本身的全景:Green-Gauss 梯度是一个离散实现层面的概念——它不引入任何对 \(\phi\) 分布形状的假设,只要求把面值 \(\phi_f\) 给定后按式 9.4 加权求和。因此 Green-Gauss 法的"精度"完全取决于面值 \(\phi_f\) 的精度——Method 1 给出的 \(\phi_f\) 是线性精度的,所以最终梯度也是线性精度(在正交网格上自然达到二阶,在非正交网格上退化);Method 2 给出的 \(\phi_f\) 通过更多邻居的加权保留了更多信息,因此最终梯度能保留更高阶的精度(在网格足够规则时可达二阶以上)。
Green-Gauss 法的另一个优势是几何守恒——式 9.4 对常数场 \(\phi = c\) 给出零梯度(因为
\[ \sum \mathbf{S}_f = 0 \]),这是一个不依赖于 \(\phi_f\) 取值方式的代数恒等式。因此即便 Method 1 给出的 \(\phi_f\) 不精确,Green-Gauss 梯度仍自动满足守恒性。这一点比最小二乘方法(9.3 节)更强——最小二乘梯度没有类似的守恒性质,模板选取不当会导致"常数场非零梯度"的伪影。
9.3 最小二乘梯度(Least-Square Gradient)
用最小二乘方法求梯度 [5] 在两个维度上比 Green-Gauss 更灵活:一是能达到更高阶精度 [6],二是模板本身可调 [7]。作为特例,发散形式的梯度也能在最小二乘框架下被恢复。代价则是模板各项需要恰当的权因子,而权因子的求取会增加计算开销。这种"灵活性换开销"的权衡是 9.3 节的核心。
考虑控制体 \(C\) 与其相邻单元(图 9.8)。若 \(C\) 处的形心梯度 \((\nabla\phi)_C\) 是精确的,则 \(C\) 与邻居 \(F\) 的差值 \(\phi_F - \phi_C\) 也可由
\[ \phi_F = \phi_C + (\nabla\phi)_C\cdot(\mathbf{r}_F - \mathbf{r}_C) = \phi_C + (\nabla\phi)_C\cdot \mathbf{r}_{CF} \](式 9.22)算出,其中 \(\mathbf{r}_{CF} = \mathbf{r}_F - \mathbf{r}_C\)。式 9.22 本质上是 \(\phi\) 在 \(C\) 处的 Taylor 一阶展开到 \(F\) 处。除非解场是线性的,否则 \(C\) 处的形心梯度不可能精确——因为 \(C\) 的邻居数大于梯度矢量的分量数(典型二维情形下 \(C\) 有 4–8 个邻居但梯度只有 2 个分量)。这一"过约束"是最小二乘方法得以应用的物理基础。在最小二乘方法中,梯度通过一个优化过程得到,即最小化下面的函数 \(G_C\):
\[ G_C = \sum_{k=1}^{NB(C)} w_k \left[\phi_{F_k} - (\phi_C + (\nabla\phi)_C\cdot\mathbf{r}_{CF_k})\right]^2 \]整理为\[ G_C = \sum_{k=1}^{NB(C)} w_k \left[\Delta\phi_k - \left(\frac{\partial\phi}{\partial x}\bigg|_C \Delta x_k + \frac{\partial\phi}{\partial y}\bigg|_C \Delta y_k + \frac{\partial\phi}{\partial z}\bigg|_C \Delta z_k\right)\right]^2 \](式 9.23),其中\[ \Delta\phi_k = \phi_{F_k} - \phi_C,\quad \Delta x_k = \mathbf{r}_{CF_k}\cdot\mathbf{i},\quad \Delta y_k = \mathbf{r}_{CF_k}\cdot\mathbf{j},\quad \Delta z_k = \mathbf{r}_{CF_k}\cdot\mathbf{k} \](式 9.24)。\(w_k\) 是某种权因子。式 9.23 的结构是"对所有邻居,把 \(\phi\) 差值与梯度点积之差按权重平方求和"——经典最小二乘。令
\[ \partial G_C/\partial(\partial\phi/\partial x|_C) = \partial G_C/\partial(\partial\phi/\partial y|_C) = \partial G_C/\partial(\partial\phi/\partial z|_C) = 0 \](式 9.25),得三个关于三个未知量的方程(式 9.26)。写成矩阵形式即式 9.27:左侧是 \(3\times 3\) 对称矩阵,其元素为 \(\sum_k w_k \Delta x_k \Delta x_k\)、\(\sum_k w_k \Delta x_k \Delta y_k\)、\(\sum_k w_k \Delta x_k \Delta z_k\)、\(\sum_k w_k \Delta y_k \Delta y_k\)、\(\sum_k w_k \Delta y_k \Delta z_k\)、\(\sum_k w_k \Delta z_k \Delta z_k\);右侧是三维向量
\[ [\sum_k w_k \Delta x_k \Delta\phi_k;\ \sum_k w_k \Delta y_k \Delta\phi_k;\ \sum_k w_k \Delta z_k \Delta\phi_k] \]。
只要左侧矩阵非奇异就可解。权因子 \(w_k\) 的选择直接决定梯度的性质:若令所有邻居 \(w_k = 1\),则无论远近,所有邻居对梯度的贡献等权重——但实际远离 \(C\) 的邻居反而会让误差函数受其误差影响更大,因而更重要。这是因为
\[ (\phi_{F_k} - \phi_C - \nabla\phi_C\cdot\mathbf{r}_{CF_k}) \]这一项当 \(\|\mathbf{r}_{CF_k}\|\) 增大时(远离 \(C\) 时),由于 \(\nabla\phi_C\cdot\mathbf{r}_{CF_k}\) 本身会变大,从而该项的绝对值也会偏大;不加权的话,远离的邻居对 \(G_C\) 的贡献会被放大。
另一个常用选择就是"距离倒数",即
\[ w_k = \frac{1}{\|\mathbf{r}_{F_k} - \mathbf{r}_C\|} = \frac{1}{\sqrt{\Delta x_{F_k}^2 + \Delta y_{F_k}^2 + \Delta z_{F_k}^2}} \](式 9.28)。这种权因子抵消了上面所述的"远距离放大"——\(\Delta\phi_k\) 与 \(\|\mathbf{r}_{F_k} - \mathbf{r}_C\|\) 在梯度近似一致的情况下量级相当,倒数恰好把远距离项的权重压回正常水平。也可以使用距离倒数的任意正整数次幂 \(1/\|\mathbf{r}_{F_k} - \mathbf{r}_C\|^n\)(式 9.29,其中 \(n = 1, 2, 3, \ldots\));\(n\) 越大表示越"亲近"\(C\)、越忽略远处邻居,相当于把模板收紧。值得一提的是,发散形式的梯度是上述最小二乘框架的一个特例。对图 9.9 所示的 Cartesian 控制体,把几何量代入式 9.27 后即得
\[ \begin{pmatrix} x_E - x_W & 0 & 0 \\ 0 & y_N - y_S & 0 \\ 0 & 0 & z_T - z_B \end{pmatrix} \begin{pmatrix} (\partial\phi/\partial x)|_C \\ (\partial\phi/\partial y)|_C \\ (\partial\phi/\partial z)|_C \end{pmatrix} = \begin{pmatrix} \phi_E - \phi_W \\ \phi_N - \phi_S \\ \phi_T - \phi_B \end{pmatrix} \](式 9.30),解得\[ \left.\frac{\partial\phi}{\partial x}\right|_C = \frac{\phi_E - \phi_W}{x_E - x_W},\quad \left.\frac{\partial\phi}{\partial y}\right|_C = \frac{\phi_N - \phi_S}{y_N - y_S},\quad \left.\frac{\partial\phi}{\partial z}\right|_C = \frac{\phi_T - \phi_B}{z_T - z_B} \](式 9.31),与式 9.3 一致——证实了发散型梯度是最小二乘方法的特例。这一"特例性"具有教学价值:它说明 9.1 节与 9.3 节并非两条独立路线,而是同一框架在不同权因子选择下的特殊表现。最后,从 Taylor 展开\[ \phi(\mathbf{r}) - \phi(\mathbf{r}_C) = (\nabla\phi)_C\cdot(\mathbf{r} - \mathbf{r}_C) + O(\mathbf{r}^2) \](式 9.32)可知,这样得到的梯度精度至少为一阶——因为 \((\nabla\phi)_C\) 本身只能精确反映到一阶残差 \(O(\mathbf{r})\)。最后对 9.3 节做一个回顾——最小二乘方法的核心思想是:用邻居的 \(\phi\) 差值做约束,求最匹配这些约束的梯度。这个思路比 Green-Gauss 更灵活,因为:(1) 权因子 \(w_k\) 可以任意选,从而可以控制每个邻居对梯度的影响;(2) 模板可以从"最近邻居"扩展到"任意选取的邻居集合",因此可以构造二阶甚至更高阶的精度;(3) 退化情况(即模板稀疏到无法解)也有清晰的处理:左侧矩阵奇异意味着需要增加模板或调整网格。其代价则在于:(a) 需要求解 \(3\times 3\) 矩阵系统(对每个单元),虽然矩阵小但每一步都涉及浮点除法与求逆;(b) 权因子的选取会显著影响结果,工程上需要在"亲近 \(C\)、忽略远距"与"利用远距信息"之间权衡;(c) 模板的几何分布若退化(如所有邻居都集中在 \(C\) 的一侧),会导致矩阵病态。
9.4 插值梯度到面上(Interpolating Gradients to Faces)
第 8 章已经指出,在非正交网格上离散扩散项时,必须使用涉及面梯度的修正项;这意味着梯度需要从形心(算出处)插值到面(要用处)。图 9.10a 显示 Gauss 梯度计算时对两个相邻控制体所使用的模板——面梯度继承同样的模板(即"两个相邻单元"的形心梯度信息),而理想情况下应如图 9.10b 那样更紧凑地依赖"跨过该面的两个形心值差"。考虑到图 9.11a 显示的两个邻居节点 \(C\) 与 \(F\) 处的梯度 \(\nabla\phi_C\) 与 \(\nabla\phi_F\),面梯度 \(\nabla\phi_f\) 由两侧节点值简单平均(图 9.11b)即可得到。然而,简单平均的模板并不保证侧重于面两侧的节点——它的模板实质上是所有对 \(C\)、\(F\) 有贡献的邻居的并集,可能包含距离 \(f\) 很远的单元。
图 9.11c 示意了一种更好的做法——强制让面梯度沿 \(CF\) 方向的分量等于由 \(C\)、\(F\) 两节点值定义的局部梯度,数学表达为
\[ (\nabla\phi)_f = (\nabla\phi)_f' + \left[\frac{\phi_F - \phi_C}{d_{CF}} - (\nabla\phi)_f'\cdot\mathbf{e}_{CF}\right]\mathbf{e}_{CF} \](式 9.33),其中\[ (\nabla\phi)_f' = g_C\nabla\phi_C + g_F\nabla\phi_F \]是简单平均、 \(\mathbf{e}_{CF} = (\mathbf{r}_F - \mathbf{r}_C)/d_{CF}\) 是 \(CF\) 方向的单位矢量、\(d_{CF} = \|\mathbf{r}_F - \mathbf{r}_C\|\)(式 9.34)。
式 9.33 的物理含义是:把 \((\nabla\phi)_f'\) 沿 \(\mathbf{e}_{CF}\) 方向的分量调整为 \((\phi_F - \phi_C)/d_{CF}\)——后者就是 \(CF\) 方向上的"局部精确梯度"。也就是说,修正只在 \(CF\) 方向起作用,其他方向保持不变。直觉上:若 \(\phi\) 在 \(C\)、\(F\) 之间变化陡峭,平均梯度沿 \(CF\) 的分量可能偏小(因为拉低了远距单元的影响),而 \((\phi_F - \phi_C)/d_{CF}\) 直接反映了这种陡峭;修正后 \(CF\) 方向的梯度分量就与 \(C\)、\(F\) 实测的差异一致。
该方法对结构与非结构网格都适用。对非结构网格而言,虽然面梯度的模板未必更紧凑(仍可能涉及很多邻居),但跨面的梯度仍会基于面两侧的节点——这一点正是非正交扩散修正所需要的。
式 9.33 的修正项可以重新解读为两个矢量的差:
\[ (\nabla\phi)_f - (\nabla\phi)_f' = \left[\frac{\phi_F - \phi_C}{d_{CF}} - (\nabla\phi)_f'\cdot\mathbf{e}_{CF}\right]\mathbf{e}_{CF} \]这个差值只在 \(\mathbf{e}_{CF}\) 方向非零——也就是说修正"完全沿 \(CF\) 方向"、不动其他方向的分量。这种"只在某个方向做修正"的设计是有意的:扩散项的非正交修正(见第 8 章)只涉及面梯度在 \(\mathbf{S}_f\) 与 \(\mathbf{d}_{CF}\) 之间的差异分量,所以让面梯度沿 \(CF\) 方向精确就足够保证修正项的一致性。在工程实现中,式 9.33 由
Average:Corrected这条插值路径实现(9.5 节中的 Listing 9.3)。OpenFOAM 默认linear插值不会启用这个修正——它只做 \((\nabla\phi)_f = (\nabla\phi)_f'\) 的简单平均;要把式 9.33 启用,需要把interpolationSchemes改为skewCorrected linear;(或者用 9.5.2 节的紧凑语法grad(phi) Gauss linear;默认下其实只对应简单线性,需要显式开启 skew correction)。最后要指出 9.4 节的设计哲学:把"形心梯度"插值到"面梯度"是一个相对独立的模块,不依赖于 9.2 与 9.3 节的形心梯度求法。也就是说,无论形心梯度是用 Green-Gauss、最小二乘、还是更复杂的(如 Barth-Jespersen 限制梯度)算出来的,都可以再用 9.4 节的 CF-方向修正得到面梯度。这种"模块分离"让 FVM 的代码结构更清晰——形心梯度、面插值、面修正可以独立调试与替换。
从几何直观上看,式 9.33 的"局部精确梯度" \((\phi_F - \phi_C)/d_{CF}\) 与 9.1 节 Cartesian 网格上的中心差分式 9.2 形式上完全类似——后者也是用两侧节点 \(\phi\) 的差除以 \(CF\) 距离得到的"局部梯度"。所以式 9.33 在某种意义上是把 9.1 节一维情形的精度"继承"到 9.4 节的高维、非正交情形:在 \(CF\) 方向上保持一维中心差分的精度;在其他方向上仍依赖形心梯度的精度。这种"局部精度 vs. 全局精度"的权衡正是 FVM 在非结构网格上做扩散修正的核心思想。
最后回顾 9.4 节与第 8 章的关系:第 8 章的非正交扩散离散需要面梯度的 \(\mathbf{e}_{CF}\) 分量来构造 over-relaxed 修正(式 8.99 等);式 9.33 提供了这一分量的精确值 \((\phi_F - \phi_C)/d_{CF}\)——它把第 8 章的"几何近似"提升为"基于解的近似",从而让扩散项的非正交修正与实际 \(\phi\) 分布更一致。这种"前章留缺口、后章填上"的章节设计模式在 Moukalled 一书中反复出现——每一章都给后续章节留出"待补"的几何量或中间变量,让前后章之间形成连续的技术闭环。
9.5.1 uFVM 中的梯度计算(uFVM)
本节介绍 uFVM 中用于计算 Green-Gauss 梯度的三个核心函数——
cfdComputeGradientGauss0、cfdComputeGradientNodal与cfdInterpolateGradientsFromElementsToInteriorFaces——以及它们与前文公式的对应关系。函数名末尾的0不是版本号——它的语义是"无修正",即面值采用简单加权平均、没有任何对非共面性的偏斜修正。Gauss 梯度例程(Listing 9.1)输入单元上的 \(\phi\) 数组、输出单元梯度数组;面值由权因子直接插值得到。具体做法是先分离内面与边界面、收集它们的所有者与邻居索引、提取面积矢量 \(\mathbf{S}_f\) 与几何因子 \(g_f\),然后用 \(\phi_f = g_f \cdot \phi_N + (1-g_f)\cdot \phi_O\)(\(N\) = neighbour、\(O\) = owner)把单元值插值到面——遍历每个内面把它累加到两侧单元上 \(\phi_{\text{grad}}\) 上,对内面而言累加符号相反(从 neighbour 上减去),这是离散散度定理的体现;遍历每个边界面把 \(\phi_b \cdot \mathbf{S}_b\) 累加到其所有者单元上,最后用单元体积除一次得到形心梯度,并把边界单元梯度设为对应所有者单元的梯度(即令"边界元素的梯度等于其对应的内部所有者元素的梯度"——这是一种零阶梯度边界条件)。
节点梯度例程(Listing 9.2)的整体流程与 Gauss 例程类似,但先用
cfdInterpolateFromElementsToNodes把 \(\phi\) 插到节点、再用cfdInterpolateFromNodesToFaces插到面——这相当于扩展模板法。其中cfdInterpolateFromElementsToNodes实现的就是式 9.18(倒数距离加权);cfdInterpolateFromNodesToFaces在二维情形下实现式 9.19,在三维情形下实现式 9.21。面梯度插值由
cfdInterpolateGradientsFromElementsToInteriorFaces实现(Listing 9.3),输入是插值方案theInterpolationScheme、单元梯度grad、单元值数组phi、质量通量mdot_f(仅上/下风方案用到)。该函数提供了若干种方案:Average(直接平均 \(\nabla\phi_C\) 与 \(\nabla\phi_F\))、Upwind(按 \(\dot{m}\) 方向选上/下风单元的梯度)、Downwind(按 \(\dot{m}\) 方向选下/上风单元的梯度)、Average:Corrected(平均后再沿 \(CF\) 方向按局部梯度修正,正好实现式 9.33)。
Average:Corrected的具体做法是:先用Average公式得到 \(\nabla\phi_f\);再求 \(\mathbf{e}_{CF}\)(单位方向矢量)和局部梯度 \(\nabla\phi_{\text{local}}\)(即 \((\phi_F - \phi_C)/d_{CF}\cdot\mathbf{e}_{CF}\));再求平均梯度在 \(\mathbf{e}_{CF}\) 上的分量 \(\nabla\phi_{\text{avg,along CF}}\);最后用\[ \nabla\phi_f = \nabla\phi_f - \nabla\phi_{\text{avg,along CF}} + \nabla\phi_{\text{local}} \]替换——把 \(CF\) 方向的分量由"平均梯度沿 \(CF\) 的分量"换成"局部梯度"。如果方案名不匹配任何分支则退出,这是 uFVM 风格的"快速失败"——避免运行错误配置下的代码。
从代码结构看,Listing 9.1 中有三个循环:(1) 对内面的循环——同时累加到 owner 和 neighbour 的 \(\phi_{\text{grad}}\) 上;(2) 对边界面的循环——累加到边界单元(BOwner)上;(3) 对所有元素的循环——除以体积得到最终梯度。这种三段式结构清晰地把"累加—边界—归一化"三个步骤分开。其中第二个循环结束后还要做"边界梯度 = 所有者梯度"的赋值,这是 OpenFOAM 与 uFVM 都用到的零阶梯度边界条件——边界元素没有自己的体积梯度,只能从相邻内部单元的梯度继承。
Listing 9.2 把节点插值封装为两个独立函数调用(
cfdInterpolateFromElementsToNodes+cfdInterpolateFromNodesToFaces),然后再走与 Listing 9.1 类似的内面/边界面循环。这种"插值模块化"的设计让节点插值逻辑可以独立替换——例如后续若需要把倒数距离权因子换成其他函数,只需修改cfdInterpolateFromElementsToNodes一处。Listing 9.3 的
if/elseif链覆盖四种插值方案:每种方案都是对grad_f(:,1..3)的三个分量分别赋值。注意Upwind与Downwind之间的差别仅在pos与(1-pos)的乘法顺序——pos由mdot_f > 0决定(值为 1 表示流入方向指向 neighbour)。这种"利用 \(\dot{m}\) 方向选邻居"的策略让梯度具有上风偏置特性,可以与对流项离散的高阶格式(如 TVD)配合使用。值得一提的是,Listing 9.3 的代码顺序是"先做 Average 插值、再做 CF-方向修正"——也就是说
Average:Corrected分支的最后一行grad_f = grad_f - local_avg_grad + local_grad;才实施式 9.33 的修正。这一"先平均、后修正"的两步模式与 9.4 节的公式推导完全对应——\(\nabla\phi_f\) 先用简单平均得到 \((\nabla\phi)_f'\),再用局部梯度 \(\nabla\phi_{\text{local}}\) 替换其 \(CF\) 方向的分量。代码用dot(grad_f', e_CF')'做点积、用local_avg_grad_mag.*e_CF做投影——这两步把"沿 \(CF\) 方向的分量"提取出来,然后grad_f = grad_f - local_avg_grad + local_grad把平均的 \(CF\) 分量减去、加上局部的 \(CF\) 分量。9.5.2 OpenFOAM® 中的梯度计算(OpenFOAM®)
OpenFOAM® [8] 内置若干梯度求值方法:标准 Green-Gauss 方法、二阶最小二乘方法以及四阶最小二乘方法。本节聚焦 Green-Gauss 方法。Green-Gauss 梯度按式 9.4 在单元形心定义,源代码位于
src/finiteVolume/finiteVolume/gradSchemes/gaussGrad。实现分两步:第一步是"面插值"(由
calcGrad调用tinterpScheme_().interpolate(vsf)完成,Listing 9.4),把 \(\phi\) 值存到面通用字段vsf——这里的插值方案由字典指定,默认是线性;第二步是按 Green-Gauss 公式求梯度(由gradf完成,Listing 9.5)。gradf用 LDU addressing(L = Lower、D = Diagonal、U = Upper):遍历所有内面,把Sf[facei] * ssf[facei]加到owner[facei]、从neighbour[facei]上减去(Listing 9.6 即这一加/减操作的代码片段:igGrad[owner[facei]] += Sfssf; igGrad[neighbour[facei]] -= Sfssf;);再遍历每个边界面把pSf * pssf加到对应所有者单元;最后用mesh.V()除以体积即得梯度,并调用correctBoundaryConditions(gGrad)修正边界条件。这种"先内面再边界面、最后除以体积"的三段式实现与 uFVM 中的 Listing 9.1 完全对应——只是 OpenFOAM 的循环结构更紧凑(用
forAll宏),并通过 LDU addressing 把"加减号"自动内嵌。梯度类型由
fvSchemes字典定义(Listing 9.7):default none; grad(phi) Gauss;。这里的Gauss就是 Green-Gauss 方法;用户也可以写leastSquares(二阶最小二乘)或fourthOrderLeastSquares等。面插值方案由interpolationSchemes定义(Listing 9.8):interpolate(phi) linear;。把线性插值改为skewCorrected linear;(Listing 9.9)即可启用式 9.8 描述的偏斜修正。也可以采用更紧凑的语法(Listing 9.10),直接在gradSchemes下写grad(phi) Gauss linear;——插值方式被一并指定;这两种语法用户可任选,但分别定义interpolationSchemes更能凸显梯度计算的中间步骤。OpenFOAM 的设计哲学是"通过字典驱动"——计算的核心代码不变化,只是配置不同就得到不同的梯度精度与偏斜处理。这种"算法与配置解耦"的设计让用户能在不重编译的情况下切换算法,是 OpenFOAM 在工业界流行的原因之一。
从代码细节看,
gradf函数的实现有几个关键点值得注意:
- 临时对象
tgGrad由tmp<GeometricField<...>>包装,遵循 OpenFOAM 的引用计数内存管理模式——多个消费者可以共享同一数据,只有最后一个消费者释放时才真正析构。这是 OpenFOAM 性能优化的核心机制之一。- 字段
gGrad由tgGrad()解引用获得引用,后续correctBoundaryConditions(gGrad)调用确保边界条件被正确处理。- 内面循环用
forAll(owner, facei)遍历所有面索引;每个面都通过owner[facei]与neighbour[facei]两个标签做加减。这种"通过面索引遍历、间接访问单元"的模式是 LDU addressing 的具体体现。- 边界面循环则用嵌套结构:先遍历每个
patchi,再遍历该 patch 内的每个facei;每个面通过pFaceCells[facei]定位其所有者单元。- 最后
igGrad /= mesh.V();是"逐单元除以体积"——这里mesh.V()返回每个单元体积的列表,OpenFOAM 的逐分量除法会自动广播。这与 uFVM 中的显式for iElement循环等价,但更紧凑。
calcGrad函数本身非常短:它只做两件事——(1) 调用tinterpScheme_().interpolate(vsf)把 \(\phi\) 插值到面;(2) 把结果传给gradf计算梯度并返回。这种"先插值、再求梯度"的两步设计正是 OpenFOAM 在梯度计算上"算法与配置解耦"的具体体现——插值方案(interpScheme_)可以独立于梯度方案(gradSchemes)配置。最后看字典层级的几个 Listing(9.7–9.10),它们体现了 OpenFOAM 的"配置即代码"风格: -
default none;表示默认对所有未指定的字段不计算梯度;grad(phi) Gauss;显式指定 \(\phi\) 用 Gauss 方法。 -interpolationSchemes与gradSchemes的分工:前者只管"如何插值到面"、后者只管"如何求梯度"——清晰划分让修改一种方案不影响另一种。 -interpolate(phi) skewCorrected linear;等价于"先用 skew correction 修正、再用线性插值",这是 9.2 节中"先 \(f'\) 修正再插值"的具体实现。 - 紧凑语法grad(phi) Gauss linear;把插值与梯度方案合并在一行——适合简单情况;分别定义适合复杂的多场耦合。OpenFOAM 默认的
linear插值方案在几何正交时与一阶精度对应;skewCorrected方案则把 9.2 节中"先 \(f'\) 修正再插值"的两步流程封装为一个字典条目。这种"配置即算法"的风格让 OpenFOAM 用户不需要重编译就能切换算法,但也带来一个副作用:当用户希望混合使用不同插值方案时(例如某些场需要 skew correction、其他场不需要),必须为每个场分别指定——default none;+ 各字段单独指定是常见模式。最后回到 9.5 节本身的全景——uFVM 与 OpenFOAM 在梯度计算上的实现差别反映了两套代码库的不同设计哲学:uFVM 用 MATLAB 写成,语法接近伪代码,侧重于"易读、易教学"——函数名
cfdComputeGradientGauss0直接对应公式,参数phi、theMesh与返回值phiGrad都体现了"输入数据、输出结果"的简洁模式;OpenFOAM 用 C++ 写成,侧重于"高性能、可扩展"——tmp<>引用计数、GeometricField<>模板元编程、forAll宏等机制都是为了让大规模 CFD 模拟(百万级单元)也能高效运行。理解这两套实现的差异有助于在实际工程中根据需要选择——教学与原型开发用 uFVM、生产级模拟用 OpenFOAM。9.6 小结(Closure)
本章给出了在一般非正交网格上计算控制体形心处梯度的两类方法的离散细节:一类基于 Green-Gauss 定理(含紧凑与扩展两种模板以及三种偏斜修正方案),另一类基于最小二乘重构(含多种权因子选择以及把发散型作为特例的论证)。最后给出了把单元形心梯度插值回面形心的 CF-方向强制修正方法。第 10 章将专门讨论代数方程组的求解方法——把第 8 章的扩散离散、第 9 章的梯度计算、以及未来章节的对流离散所生成的代数方程组拼成可解的整体。
本章个人批注
本章是 Moukalled 一书里我迄今读到的方法学密度最高的章节之一——前三节把"梯度"这一几何对象在 FVM 上下文里的两条主要求值路线(Green-Gauss vs. 最小二乘)讲得相当完整。
9.2 节值得特别留意的是"偏斜修正"的三种 \(f'\) 取法(精确交点/中点/最短距离):实际 OpenFOAM 中默认是 \(f'\) 取中点(即 9.2.1 节中的"方案 2"),且用
skewCorrected才启用;最短距离("方案 3")的首轮更准但要解一个最小化问题,工程上常被牺牲掉。9.3 节那种把发散型梯度作为最小二乘特例的论证很巧妙——把"发散 vs. 最小二乘"从对立路线变成"同一框架下的特例",并通过显式代入 Cartesian 网格的几何量推导出完全相同的表达式(式 9.30/9.31 与式 9.3 一致)。9.4 节式 9.33 的 CF-方向强制是一个工程上的小补丁,物理直觉清晰:当平均梯度沿面方向的分量与"两侧 \(\phi\) 差除以距离"代表的局部梯度不一致时,把它校回到局部值——这就避免了"两面相邻但 \(\phi\) 变化剧烈"的情形下简单平均严重低估梯度的情形。
我对 9.2 节例题 2 引用编号的小笔误做了标注——原文把式 9.18、9.21 都写成了"9.16",应该是印误。两节中的距离计算都对得上源文坐标,按上下文应该是式 9.18(即"节点值由周围单元的倒数距离加权得到")。这一节还提醒了我:核对例题引用时不能机械地按编号去找——遇到异常编号要回到上下文用公式形式确认。
把 9.5 合并为一节是出于内容性质——它由代码清单组成(uFVM 的 MATLAB 与 OpenFOAM 的 C++),几乎没有可被"复述"的论述性事实点。把它分成 9.5.1 / 9.5.2 两个 H2 反而会让内概述充斥"该函数做了 X、Listing 9.X 显示 Y"这种对源码结构的描述,恰恰是约束 #12 的反面例子。所以我把它们合到一节里描述两套实现的核心差异点(Gauss 紧凑模板 vs. 节点扩展模板、面梯度的四种插值方案、OpenFOAM 的两步分立、字典驱动的方案选择)。
我对一些数学式做了详细展开(包括式 9.12 的求解、式 9.15 的展开、式 9.18 的代入求值)——这些细节原文直接用结果式给出。我把"中间步骤"也展开是为了让读者能跟着源文走一遍算——这种"显式代入"对一个偏工程的章节很有帮助,但要注意不能扩展到该节源文没有的概念。
与上下章的衔接(一段话)
第 8 章推导了扩散项的离散方程,并在"非正交修正"环节明确指出修正项需要面梯度——这一信息缺口正是第 9 章要补上的。第 9 章给出两类形心梯度的求法(Green-Gauss 与最小二乘)以及把梯度插值回面的方法,使扩散项的偏斜/非正交修正项有了可计算的具体形式。从章节逻辑看,第 9 章与第 8 章构成"扩散离散"的完整闭环:第 8 章给出形心与面通量的离散表达,第 9 章则把"形心梯度"这一中间量以及"面梯度"这一修正量都补上。第 10 章接续讨论的是这些离散后产生的代数方程组如何求解——也就是说第 9 章把"梯度"这一几何对象落到可计算的层面,第 10 章则把"方程组"这一代数对象推到可计算的层面,二者一道为后续章节(第 11 章起的对流项离散)做好算子与求解器的双重准备。