关于negSumDiag()的一个问题
-
在看关于negSumDiag()的代码的时候遇到一个问题。diag上面的项是由该“行”上面的的off-diag的项相加后取负值得到。为什么下面的代码感觉像是把该“列”的数相加了呢?
template<class Type, class DType, class LUType> void Foam::LduMatrix<Type, DType, LUType>::negSumDiag() { const Field<LUType>& Lower = const_cast<const LduMatrix&>(*this).lower(); const Field<LUType>& Upper = const_cast<const LduMatrix&>(*this).upper(); Field<DType>& Diag = diag(); const unallocLabelList& l = lduAddr().lowerAddr(); const unallocLabelList& u = lduAddr().upperAddr(); for (label face=0; face<l.size(); face++) { Diag[l[face]] -= Lower[face]; Diag[u[face]] -= Upper[face]; } } 以这个网格为例
lowerAdd 为 (0 1 2 1 2 3 4 3 )
upperAdd 为 (2 2 3 6 4 5 6 5 )矩阵的形式为
那么Diag[0] 应该是由 Upper[1]得到。这个和上面的代码不符。如果矩阵是对称的,这个计算没有问题。而如果是 非对称的,是不是就有问题了?
谢谢大家!
-
为了进一步测试,建立一个3X3的网格,并创建一个fvMatrix。
fvVectorMatrix UEqn_lap ( fvm::laplacian(nu, U) );
网格编号如下:
|6|7|8| |3|4|5| |0|1|2|
输出 upper 和lower 的address,没有问题
Info<< "UEqn_lap upper add before sum " << UEqn_lap.lduAddr().upperAddr() << nl << endl; Info<< "UEqn_lap lower add before sum " << UEqn_lap.lduAddr().lowerAddr() << nl << endl; UEqn_lap upper add before sum 12 ( 1 3 2 4 5 4 6 5 7 8 7 8 ) UEqn_lap lower add before sum 12 ( 0 0 1 1 2 3 3 4 4 5 6 7 )
下面把upper lower 和diag重新赋值 并输出
UEqn_lap.upper() = 100; UEqn_lap.diag() = 0; UEqn_lap.lower() = 1; Info<< "UEqn_lap diag after change " << UEqn_lap.diag() << nl << endl; Info<< "UEqn_lap upper after change " << UEqn_lap.upper() << nl << endl; Info<< "UEqn_lap lower after change " << UEqn_lap.lower() << nl << endl;
输出为:
UEqn_lap diag after change 9{0} UEqn_lap upper after change 12{100} UEqn_lap lower after change 12{1}
调用
UEqn_lap.negSumDiag(); Info<< "UEqn_lap diag after negSumDiag " << UEqn_lap.diag() << nl << endl;
输出结果为:
UEqn_lap diag after negSumDiag 9(-2 -102 -101 -102 -202 -201 -101 -201 -200)
我特意把upper里面的数值设的很大,从输出的结果看,diag 是把lower里面的数加和了,也就是把列中的数加和了。
这个结果让我有点儿弄不明白。
-
@mengweilm425 这个问题cfd-online上也有人讨论过,不过没有结果。我个人觉得这确实是OpenFOAM的bug,不过由于fvm离散后的系数矩阵的特殊性质,列元素相加和行元素相加得到的结果是一样的,因此这个bug不影响计算。有机会细说。
-
-
这个问题困扰我了一天,但我解决了这个问题。
首先,我们应该明白OpenFOAM中并没有存A[][]这样的系数矩阵,存的是diag,lower和upper这些非零的系数。
其次,我们在看一些教程的时候,为了解释lduAddressing中的lower(), upper(), lowerAddr(), and upperAddr(),可能把lower解释成了
the coefficient multiplying in the algebraic equation for element neighbour,把upper解释成了
the coefficient multiplying in the algebraic equation for element owner.
甚至有些教程给出了下面的解释A[upperAddr[k]][lowerAddr[k]]=lower[k] A[lowerAddr[k]][upperAddr[k]]=upper[k]
如果这样理解的话,当然negSumDiag是对列求和的,这和我们认知里矩阵的一行代表一个方程相违背。有人强行解释对于扩散方程因为矩阵对称,虽有对行求和对列求和结果一致。
但事实上出现这样认知的原因在于我们对lower和upper的理解出现的错误。Hrvoje Jasak在帖子中也说明了这点
https://www.cfd-online.com/Forums/openfoam-programming-development/72456-matrix-storage-diagonal-coeffients-calculation.html经过对guassConvectionScheme的探究及对对流方程离散化的推导(原谅我尚未掌握这个论坛上使用公式排版的方法),正确的理解方式应该是:
- lower存的系数代表了以Neighbour为单元的代数方程中,Owner的物理量的贡献
- upper 存的系数代表了以Owner为单元的代数方程中,Neighbour的物理量的贡献
并且根据对流方程离散化的推导,我们其实可以知道diag的系数,并非为该行off-diagonal系数的和。而应该为
基于规则正交网格的推导可以参考
《The Finite Volume Method in Computational Fluid Dynamics An Advanced Introduction with OpenFOAM and Matlab》如果有时间,我会重新排版这条回复。有任何问题可以和我联系讨论。
-
上面这个式子是不是漏了
和 ? 是不是指的 Ax=b 中的 b?我的理解应该是
以上公式为计入边界条件后的表达式,
不为 0,且 的值被边界条件影响。negSumDiag 只对 diag, lower 和 upper 做了操作,并没有计入边界条件的影响,所以其过程并不对应以上公式。
-
扩散方程比较容易理解。这里仅以对流方程为例,不考虑边界条件,不考虑网格扭曲的影响。下面是我的理解。
仅考虑对流项
将对控制单元 的体积分转化为面积分, 为控制单元 的控制面, 为控制单元 的的邻居单元
定义
面心的物理量通过两侧体心物理量加权平均得到:
上面公式整理后为:
其中
在
gaussConvectionScheme<Type>::fvmDiv
中fvm.lower() = -weights.primitiveField()*faceFlux.primitiveField(); fvm.upper() = fvm.lower() + faceFlux.primitiveField(); fvm.negSumDiag();
可以看出,
lower
存的是 ,代表了以Neighbour为单元的代数方程中,Owner的物理量 的贡献 ,upper
存的是 ,代表了以Owner为单元的代数方程中,Neighbour的物理量 的贡献 。这里所说的Neighbour和Owner,其实是立足于面的,Owner是该面左侧的单元,Neighbour是该面右侧的单元,Owner编号小于Neighbour编号。至于
weights
的具体计算,则根据具体的插值格式进行计算,对于Upwind、FROMM、QUICK,TVD等不同格式,分别有不同的计算方法,这里暂不讨论。这里有个trick,
,为什么对于编号大于Owner的邻居单元,upper
存的是 ,而对有编号小于Owner的邻居单元,lower
存的是 呢?这是因为计算面心的系数 时在总是认为面左侧是Owner,右侧是Neighbour.最后是
fvm.negSumDiag()
求和计算对角项的系数 。这里我们并非按照公式 ,先对所有的cell循环,然后对每个cell的所有面循环,这样计算效率底下,非结构网格程序一般都是对所有的面进行循环,将其贡献分别计入两侧的控制体中。for (label face=0; face<l.size(); face++) { Diag[l[face]] -= Lower[face]; Diag[u[face]] -= Upper[face]; }
这里是对列求和,而非对行求和。如果是对行求和的话,得到的是
。而对角项的系数 .那么最后的问题就是为什么对列求和得到的是正确的
,这是巧合么?并不是巧合,而是必然。仔细想想这些系数所代表的含义,- lower存的系数代表了控制面右侧的单元(Neighbour)的代数方程中,控制面左侧单元(Owner)的物理量的贡献
- upper 存的系数代表了控制面左侧单元(Owner)的代数方程中,控制面右侧的单元(Neighbour)的物理量的贡献
另一方面在
中- 矩阵
中的第 行的非对角项系数,其实代表了与编号为 的单元相邻的单元物理量对 的单元控制方程的贡献。 - 矩阵
中的第 列的非对角项系数,其实代表了编号为 的单元对其相邻的单元控制方程的贡献。
对于对流项,单元
对单元 的影响不等于单元 对单元 的影响,但是,编号为 的单元对所有与其相邻的单元的贡献的总和,正应该是 的单元自身系数的相反数。也正是矩阵 中的第 列的非对角项系数之和的相反数。以上理解也只是我个人根据
guassConvectionScheme.C
文件的自我解读。如果哪里有理解错的地方,还请指正。 -
很精彩的推导!解开了我一直以来的疑惑。
不过有一点我不认同:
这里有个trick,
,为什么对于编号大于Owner的邻居单元,upper
存的是 ,而对有编号小于Owner的邻居单元,lower
存的是 呢?这是因为计算面心的系数 时在总是认为面左侧是Owner,右侧是Neighbour.这里的trick(正负号)应该是求flux的时候设置的,而不是求weight。
我换个说法理一下思路:
对于单元
,有 个面,也就有 个邻居单元,记为 ... 。每个面心的量通过面的 owner 和 neighbour 单元体心插值得到(注意这里的 owner、neighour 和上面的
、 完全不相关,为了避免歧义,上面的单元用 而不是用 表示)于是对流项的积分变为
上式中的
为质量通量,这个是由速度点乘面法向得到的。具体操作为fvc::flux(U)
,也就是mesh.Sf()
和U
的点乘。Sf()
是一个矢量场,它的方向很有讲究。它被规定为从 own 指向 nei。而高斯定理的 规定方向指向封闭单元外。这两个定义是由矛盾的。因此需要上面的trick来调整正负号。先看非对角元素。遍历 P 的所有面单元:
- 若该面单元的 own 为 P, nei 为 N,
Sf()
与 同方向
这种情况下,N 的系数在 upper,因此 upper 需要加上 。同时 P 的主对角元素加上 。 - 若该面单元的 nei 为 P,own 为 N,
Sf()
与 反方向
这种情况下,N 的系数在 lower,原本 lower 需要加上 ,调整负号后,最终加上的是 。同时 P 的主对角元素加上 。
以上过程对应以下两行代码:
fvm.lower() = -weights.primitiveField()*faceFlux.primitiveField(); fvm.upper() = fvm.lower() + faceFlux.primitiveField();
再看主对角元素。
对于邻居单元 N ,在计算其对应行的非对角系数时,所用插值公式和 P 相同,即还是
也就是说以上公式要用两次,分别是在 P 和 N 作为主对角元素的行上使用。
在循环到 N 与 P 共享的面单元时:
- 若该面单元的 own 为 P, nei 为 N,
Sf()
方向为 P->N , 方向为 N->P,二者反方向
这种情况下,P 的系数为 。 - 若该面单元的 nei 为 P,own 为 N,
Sf()
方向为 N->P, 方向也为 N->P,二者同方向
这种情况下,P 的系数为 。
注意以上两种情况,P 作为 N 的非对角系数和作为其本身的主对角系数正好是相反数,且处于同一列上。由于遍历一次面单元将所有网格单元(包括P和其所有邻居单元)的非对角元素都计算好了,为了避免重复计算,所以直接用该列的非对角元素加和并取相反数得到主对角元素的值。
即对应代码:
fvm.negSumDiag();
也就是说 negSumDiag 计算的确实是列,而不是行。我之前会由系数矩阵对称的错觉是因为采用的验证算例都是采用线性插值计算面心物理量,这使得插值系数
,正好造成系数矩阵反对称的假象。 - 若该面单元的 own 为 P, nei 为 N,
10/11