pvpython
来运行这个脚本即可。本篇用一个例子来说明 python 脚本的写法,以及常用 Paraview 操作对应的语句。
这个脚本实现的功能如下:
需要说明的是,以下脚本绝大部分都不是手写的,而是用 trace 功能自动生成的。打开一个空白的 Paraview,选择 Tools->Satrt Trace,然后,再去进行操作,Paraview 会将你的操作转换成 pvpython 语句,完成以后再 Tools->Stop Strace,就自动生成了一个脚本。
下面先给出脚本的全部,然后在脚本中给出注释,最后再给出一些额外的说明。
1 | try: paraview.simple |
1 | active_objects.source.SMProxy.InvokeEvent('UserEvent', 'HideWidget') |
语句的作用是,不显示 slice 的时候产生的哪个边框,即相当于 Paraview 里点掉 slice 的 “Show Plane”。
1 | RenderView1.Background = (81.0/255.0, 87.0/255.0, 110.0/255.0) |
作用是设置 RenderView1 的背景颜色。如果是直接在 Paraview 里以宏的形式运行脚本,那不需要这个,但若是用 pvpython 来运行脚本则需要,否则背景是黑色的。另外,这里的 RGB 颜色是用 255 归一化过的。
1 | a3_U_PVLookupTable = GetLookupTableForArray( "U", 3, RGBPoints=[0.0, 0.0, 0.0, 1.0, 16.0, 1.0, 0.0, 0.0], VectorMode='Magnitude', NanColor=[0.498039, 0.498039, 0.498039], ColorSpace='HSV', ScalarRangeInitialized=1.0, LockScalarRange=1 ) |
至于 LookupTable 中,RGBPoints 的构建规则,细节不清楚。这种套用模板就好了,没必要自己手动写。
1 | RenderView1.CameraPosition = [0.11499807611107826, 0.00038802623748779297, 0.725254404116244] |
这些数据对不同算例是不一样的,需要根据特定情景来修改。没必要手动改,应该用 trace 功能来生成这些坐标值。
最后附上一些上面脚本中没有涉及到,但可能会用到的功能:
关于动画时间的操作
1 | animationScene1.GoToNext() ## 下一帧 |
Hide, Show, Render
Hide 和 Show 可以作用于 pipeline 里的每个条目,使其隐藏/可见,对应 paraview 关闭/点亮某个“眼睛”。Render 是当改变了某个显示属性后,要想显示出改变以后的效果需要调用的函数,相当于 Paraview 里点 Apply。
pvpython 本质上就是 python,所以,python 的语法都可以用。比如,下面是一个循环删除所有 pipeline 的方法
1 | for key in GetSources().keys(): |
Paraview 的 python shell(Tools->Python Shell),可以当作是一个 pvpython 的 IDE,里面可以方便地进行命令补全,并且每条语句的效果可以即时地显示在 Paraview 中,调试脚本的时候非常有用。
最后说明一下,上述脚本只适用于 Paraview-4.1,在 Paraview-5.0 以后,接口的名字有所变化,这里给出一个链接,感兴趣的读者可以在链接中找到本文中介绍的脚本以及另一个适用于 Paraview-5.0 的脚本。
参考:
]]>pvpython
来运行这个脚本即可。本篇用一个例子来说明 python 脚本的写法,以及常用 Paraview 操作对应的语句。]]>
这个功能作用是将两个窗口(views)链接起来,这样,当一个窗口的视角变动的时候,另一个也相应跟着变化。比如,建立 Link 之前,这两个视窗是独立转动的,
有时候,为了比较,希望两个视窗是同步转动的,这时只要建立一个 link 就可以了,在左边视窗右键点击空白处,选择 Link Camera,给这个Link 取个名字,然后左键点一下右边的视窗,就完成了链接
链接之后,二者就不再是独立的了。这种链接是双向的,改变其一,另一个也跟着改,而且,允许多个窗口链接在一起,如下图,左一和右上先建立了链接,然后再建立一个左一和右下的链接,这样就把三个都链接在一起了。
新版的(5.0以上)还有一个 Interactive view 的选项,
选择这个选项以后,
会在建立链接的那一个视窗中(主动视窗),出现一个小的窗口,这个小窗口会将被链接视窗(被动视窗)的相同位置的显示结果叠加到主动视窗中。
步骤如下:
然后点右键选择 Create Custom Filter
选择 SurfaceVector1,设置为 Input,并取名为 SurfaceVector,设置好以后,点 “+”添加这个 Input。
然后,Next
将 StreamTracerWithCustomSource1 和 MaskPoints1 设置为 output。
继续 Next,这一步可以指定一些将来可以再调整的参数,如果不设置,那么新建立的 filter 将使用当前的 StreamTracerWithCustomSource1 和 MaskPoints1 使用的参数,不可调整,这样显然会严重限制新 fliter 的实用性,至少,我们应该添加控制 MaskPoint 的点数的参数。添加的方法是,选定左边的 MaskPoints,然后在右边的 Property 下拉菜单中,选择一个参数,比如,Maximum Number of Points,然后点 “+”,将来可能需要重新调整什么参数,就在这一步选上它,
然后,点 Finish,就完成了 Custom Filter 的添加。
之后,你可以在 Filter 菜单中找到新添加的 SurfaceStreamLines ,将其用在某个 slice 上,就自动生成了这个 slice 的流线。
注意 pipe line 这边,能调的参数,正是在建立 Custom filter 的最后一步添加的那四个。
建立好的 Custom Filter,应该保存下来,用下图中的 export。要不然,下次打开Paraview,自定义的 filter 就没了。选择菜单的 Tools-> Manage Custom Filters
导出的文件可以共享给其他人。
如果对建立的 filter 不满意,需要删除重建,在上图中选择需要删除的,点 Remove 即可。
]]>以 pitzdaily 算例为例,步骤如下:
作一个截面(slice),这一步不需详述
对截面使用 Surface Vector filter,这个的作用是让速度矢量投影到平面上。
对得到的 SurfaceVector 使用 Mask Points filter,这个的作用是生成一系列参考点,将来画流线的时候,以这些参考点的位置来确定流线的位置和疏密。
On Ratio 参数控制取点的疏密,这里的设置,表示每2560个点中取一个;Maximum number of points 控制总点数的数目;Random Sampling,开启随机取点模式,如果是非随机模式,将按坐标从小到大取点。假设 On Ratio = 2560 情况下,一共有1000个点,但是 Maximum number of points 设置为 100,那么将只取坐标最小的前100个点,而如果开启了随机模式,则点的分布基本上是均匀充满这个流动区域的。Generate Vertices,选择是否要显示参考点,如果开启,则会显示一个点阵。
Filter 里选择 Stream Tracer with Custom Source,Input 和 Seed Source 分别按下图设置
就得到了如下的流线图,流线的疏密,可以通过Mask Points 的点数来控制,只是,遗憾的是点数的空间分布不好控制,比如,我想让中间部分稀疏一点,角落上密一点,不容易做到。另外,还需要注意左边 Pipeline,出现了三个 StreamTracerWithCustomSource,似乎这三个其实是一个,改变任意一个都会改变流线的属性。
]]>首先简要看一下涉及的数学背景。对于一阶的常微分方程,
$$
y’=f(x,y), \quad x\in[a,b] \\
y(a)=y_0
$$
常微分方程,如果存在解析解的话,其解应该是一个函数 $y=f(x)$。然而,大多数常微分方程是没有解析解的,只能数值求解。数值方法得到的,是一系列的 $x_0, x_1, \cdots x_n$ 对应的函数值 $y_0, y_1, \cdots y_n$。
常用的数值解法有:
除了以上,当然还有很多方法,比如预测校正等等,这里就不再逐一介绍了。
问题是,实际中遇到的还可能是高阶的常微分方程,比如,弹簧-谐振子系统可以用以下e二阶常微分方程描述
$$
\frac{d^2y}{dt^2} = -\frac{k}{m}y
$$
高阶常微分方程的初值问题,可以用以下通式来描述
$$
y^{n} = f(x,y,y’,\cdots y^{n-1})
$$
其初始条件为
$$
y(0) = a_0, \quad y’(0) = a_1, \quad \cdots, \quad y^{n-1}(0) = a_n
$$
对于这种高阶常微分方程,可以将其表述为一系列一阶常微分方程的组成的方程组来求解。下面以二阶常微分方程为例,介绍如何将高阶常微分方程的初值问题转化为一阶常微分方程组。
考察如下二阶常微分方程
$$
y’’ = f(x,y,y’), \quad x\in[a,b] \\
y(a) = a_0, \quad y’(a) = a_1
$$
若令 $z=y’$,则上述二阶常微分方程可以表示成如下方程组
$$
\left \{
\begin{align*}
y’ &= z \\
z’ &= f(x,y,z)
\end{align*}
\right. \\
y(a)=a_0,\quad z(a)=a_1
$$
这个方程组,就可以用前面介绍的一阶常微分方程的解法来求解了,比如,若用最简单的显式欧拉法,则
$$
\begin{align*}
y_{m+1} &= y_m + h z_m \\
z_{m+1} &= z_m +hf(x_m, y_m, z_m)
\end{align*}
$$
或者用显式 4 阶 Runge-Kutta 方法
$$
\begin{align*}
y_{m+1} & = y_m + \frac{h}{6}[K_1+2K_2+2K_3+K_4] \\
z_{m+1} & = z_m + \frac{h}{6}[M_1+2M_2+2M_3+K_4] \\
K_1 & = z_m,\quad M_1=f(x_m, y_m, z_m) \\
K_2 & = z_m + \frac{M_1}{2}, \quad M_2 = f(x_m+\frac{h}{2}, y_m+\frac{K_1}{2}, z_m+\frac{M_1}{2})\\
K_3 & = z_m + \frac{M_2}{2},\quad M_3 = f(x_m+\frac{h}{2}, y_m+\frac{K_2}{2}, z_m+\frac{M_2}{2})\\
K_4& = z_m + M_3,\quad M_4=f(x_m+h, y_m+K_3, z_m+M_3)
\end{align*}
$$
二阶以上的常微分方程,除了可以给出初值条件,还可以给出边值条件,比如
$$
y’’ = f(x,y,y’), \quad x\in[a,b] \\
y(a) = \alpha, \quad y(b) = \beta
$$
这种情况下,就无法直接将此方程转化为一阶常微分方程组了。但是,边值问题可以通过一定的方法转换成初值问题,以下给出一种:试射法。
在不知道 $y’(a)$ 的情况下,不妨假设 $y’(a)=\gamma_1$,这样,就得到了一个初值问题
$$
y’’ = f(x,y,y’), \quad x\in[a,b] \\
y(a) = \alpha, \quad y’(a) = \gamma_1
$$
解此初值问题,得到 $y(b)$ 的值 $\beta_1$,并与 $\beta$ 比较,如果误差足够小,则认为假设的 $y’(a)=\gamma_1$ 是合理的。否则,就对 $\gamma_1$ 进行修正,比如令 $\gamma_2 = \tfrac{\beta}{\beta_1}\gamma_1$,然后再以 $y’(a)=\gamma_2$ 为初值,继续求解初值问题。直到得到的初值问题的解$y(b)=\beta_k$ 与 $\beta$ 足够接近为止。
上述方程可以归纳为,将初值问题转化为如下边值问题
$$
y’’ = f(x,y,y’), \quad x\in[a,b] \\
y(a) = \alpha, \quad y’(a) = \gamma_k, k=1,2,\cdots
$$
若记问题 $y_k(x)$ 的解为 $y(x;\gamma_k)$,则 $\gamma_k$ 的理想值应该满足
$$
F(\gamma) = y(b;\gamma)-\beta = 0
$$
这个方程,可以用牛顿迭代法来求解:
$$
\gamma_{k+1} = \gamma_k-\frac{F(\gamma_k)}{F’(\gamma_k)}
$$
其中,$F(\gamma_k)=y(b;\gamma_k)-\beta=\beta_k-\beta$。那么 $F’(\gamma_k)$ 该如何得到呢?根据 $F(\gamma_k)$ 的定义,可以知道 $F’(\gamma_k) = \frac{\partial y(b;\gamma)}{\partial \gamma}\big|_{\gamma=\gamma_k}$,若定义 $W=\frac{\partial y(b;\gamma)}{\partial \gamma}$,则 $F’(\gamma_k)=W(b;\gamma_k)$。
将上述归纳形式的初值问题,对$\gamma$ 求偏导,得
$$
\frac{\partial y’’}{\partial \gamma} = \frac{\partial f(x,y(x;\gamma),y’(x,y’))}{\partial y} \frac{\partial y(x;\gamma)}{\partial \gamma} + \frac{\partial f(x,y(x;\gamma),y’(x,y’))}{\partial y’} \frac{\partial y’(x,y’)}{\partial \gamma}
$$
根据 $W$ 的定义,有
$$
W=\frac{\partial y(x;\gamma)}{\partial \gamma}, W’=\frac{\partial y’(x,y’)}{\partial \gamma}, W’’=\frac{\partial y’’}{\partial \gamma}
$$
于是,可以得到一个关于 $W$ 的二阶常微分方程
$$
W’’=\frac{\partial f(x,y,y’)}{\partial y} W + \frac{\partial f(x,y,y’)}{\partial y’}W’
$$
其定解条件为
$$
W(a)=\frac{\partial y(a;\gamma)}{\partial \gamma}=0, W’(a)=\frac{\partial y’(a;\gamma)}{\partial \gamma}=\frac{\partial \gamma}{\partial \gamma} = 1
$$
这样就构成了一个关于 $W$ 的二阶初值常微分方程。
总结一下,二阶常微分方程的边值问题
$$
y’’ = f(x,y,y’), \quad x\in[a,b] \\
y(a) = \alpha, \quad y(b) = \beta
$$
的求解步骤如下:
为了在OpenFOAM中求解一个任意阶常微分方程的初值问题,需要做如下准备。
考虑一个通用形式的常微分方程
$$
y^{n}=f(x,y,y’,\cdots,y^{n-1})
$$
定义
$$
\begin{align*}
y_1 &=y\\
y_2 &=y’\\
y_j &=y^{\,j-1}, \quad j=1,2,\cdots,n
\end{align*}
$$
如果是求解刚性问题的 ODE 求解器,还需要定义 jacobian 矩阵。
令
$$
\begin{align*}
f_1 &=y’=y_2\\
f_j &=y’_{j}=y^{\,j+1}, \quad j=1,2,\cdots,n
\end{align*}
$$
则 jacobian 矩阵
$$
\begin{equation*}
J =
\begin{bmatrix}
\frac{\partial f_1}{\partial y_1} & \frac{\partial f_1}{\partial y_2} &
\cdots &\frac{\partial f_1}{\partial y_n}\\
\frac{\partial f_2}{\partial y_1} & \frac{\partial f_2}{\partial y_2} &
\cdots &\frac{\partial f_2}{\partial y_n}\\
\vdots & \vdots & \ddots & \vdots \\
\frac{\partial f_n}{\partial y_1} & \cdots & \cdots
&\frac{\partial f_n}{\partial y_n}
\end{bmatrix}
\end{equation*}
$$
此外,还需要给出 $f_1, f_2,\cdots,f_n$ 对自变量 $x$ 的偏导数,$\frac{\partial f_1}{\partial x}, \frac{\partial f_2}{\partial x},\cdots,\frac{\partial f_n}{\partial x}$。
下面举一个例子来具体说明。以常微分方程
$$
y’’=2x+2, x\in[0,1] \\
y(0) = 0, y’(0)= 0
$$
为例,需要定义的量为
$$
\begin{align*}
f_1 &=y’=y_2 \\
f_2 &= y’_{2} = 2x+2
\end{align*}
$$
$$
\begin{equation*}
J=
\begin{bmatrix}
0 & 1 \\
0 & 0
\end{bmatrix}
\end{equation*}
$$
$$
\frac{\partial f_1}{\partial x} = 0, \frac{\partial f_2}{\partial x} = 2
$$
求解这个常微分方程的代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119/*********************************************************
Description
d2y/dx2 = ax + b, with a=2, b=2.
initial value: y(0) = 0; y'(0) = 0
analytical solution: y = 1/3*x^3 + x^2;
**********************************************************/
#include "argList.H"
#include "IOmanip.H"
#include "ODESystem.H"
#include "ODESolver.H"
using namespace Foam;
·// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
class myODE2
:
public ODESystem
{
const scalar a_; //parameter
const scalar b_; //parameter
public:
myODE2(const scalar& a, const scalar& b)
:ODESystem(),
a_(a),
b_(b)
{}
label nEqns() const // number of equations, equals to the order of ODE
{
return 2;
}
void derivatives
(
const scalar x,
const scalarField& y,
scalarField& dydx
) const
{
dydx[0] = y[1]; //f1
dydx[1] = a_*x + b_; //f2
}
void jacobian // optional
(
const scalar x,
const scalarField& y,
scalarField& dfdx,
scalarSquareMatrix& dfdy
) const
{
dfdx[0] = 0.0; //df1/dx
dfdx[1] = a_; //df2/dx
dfdy[0][0] = 0.0; //df1/dy1
dfdy[0][1] = 1.0; //df1/dy2
dfdy[1][0] = 0.0; //df2/dy1
dfdy[1][1] = 0.0; //df2/dy2
}
};
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
// Main program:
int main(int argc, char *argv[])
{
argList::validArgs.append("ODESolver");
argList args(argc, argv);
const scalar a = 2.0;
const scalar b = 2.0;
const label n = 100; //number of steps
const scalar endTime = 1.0; //upper bound of the interval
// Create the ODE system
myODE2 ode(a, b);
dictionary dict;
dict.add("solver", args[1]);
// Create the selected ODE system solver
autoPtr<ODESolver> odeSolver = ODESolver::New(ode, dict);
// Initialise the ODE system fields
scalar xStart = 0.0; // lower bound of the interval
scalar dx = endTime/n; //step value
scalarField yStart(ode.nEqns());
yStart[0] = 0.0; // initial value of y
yStart[1] = 0.0; // initial value of y'
scalar dxEst = 0.1;
scalar xEnd = 0.0;
scalarField dyStart(ode.nEqns()); // dyStart[0]=f1, dyStart[1]=f2 ...
for(label i =0; i<n; i++)
{
xEnd = xStart + dx;
ode.derivatives(xStart, yStart, dyStart);
odeSolver->solve(xStart, xEnd, yStart, dxEst);
xStart = xEnd;
Info << xStart << " " << yStart[0] << endl; // output (x,y) for each dx.
}
return 0;
}
编译之后,假设你的可执行程序名为 TestODE
,则运行1
TestODE RKCK45 > log
就得到了数值解。
将数值解与解析解画图如下
可见在这里简单例子中,数值解与解析解吻合非常好。
同时注意,这个例子,用显式欧拉方法无法得到收敛的解。
参考资料:
先来看 OpenFOAM-2.2.x 里的1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29class fixedFluxPressureFvPatchScalarField
:
public fixedGradientFvPatchScalarField
{
// Private data
//- Name of the predicted flux transporting the field
word phiHbyAName_;
//- Name of the flux transporting the field
word phiName_;
//- Name of the density field used to normalise the mass flux
// if neccessary
word rhoName_;
//- Name of the pressure diffusivity field
word DpName_;
//- Is the pressure adjoint, i.e. has the opposite sign
Switch adjoint_;
public:
//- Runtime type information
TypeName("fixedFluxPressure");
......
......
//- Update the coefficients associated with the patch field
virtual void updateCoeffs();
......
};
其中 updateCoeffs
函数定义如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37void Foam::fixedFluxPressureFvPatchScalarField::updateCoeffs()
{
if (updated())
{
return;
}
const surfaceScalarField& phiHbyA =
db().lookupObject<surfaceScalarField>(phiHbyAName_);
const surfaceScalarField& phi =
db().lookupObject<surfaceScalarField>(phiName_);
fvsPatchField<scalar> phiHbyAp =
patch().patchField<surfaceScalarField, scalar>(phiHbyA);
fvsPatchField<scalar> phip =
patch().patchField<surfaceScalarField, scalar>(phi);
const scalarField *DppPtr = NULL;
if (db().foundObject<volScalarField>(DpName_))
{
DppPtr =
&patch().lookupPatchField<volScalarField, scalar>(DpName_);
}
else if (db().foundObject<surfaceScalarField>(DpName_))
{
const surfaceScalarField& Dp =
db().lookupObject<surfaceScalarField>(DpName_);
DppPtr =
&patch().patchField<surfaceScalarField, scalar>(Dp);
}
if (adjoint_)
{
gradient() = (phip - phiHbyAp)/patch().magSf()/(*DppPtr);
}
else
{
gradient() = (phiHbyAp - phip)/patch().magSf()/(*DppPtr);
}
fixedGradientFvPatchScalarField::updateCoeffs();
}
代码的说明如下:
这个边界条件,继承自 fixedGradientFvPatchScalarField
,可见它是一个用于压力场的第二类边界条件。其用如下公式来计算边界上的压力梯度:
$$
\nabla(p) = \frac{\phi_{H/A} - \phi}{|S_f| D_p}
$$
在设置边界条件的时候,可以指定 phiHbyA
, phi
和 Dp
对应的场,如果不指定,则三个量对应的默认场名分别是 phiHbyA
, phi
和 Dp
,这时如果你的程序中找不到这三个场,那就将出错了。
这个边界条件可以这样理解:
首先来看单相流的情形,比如 icoFoam
,半离散化的动量方程为
$$
AU=H-\nabla p
$$
即
$$
\nabla p = H - AU
$$
在入口处,理论上当计算收敛后,压力梯度应当是0。但是在构建压力方程1
2
3
4fvScalarMatrix pEqn
(
fvm::laplacian(rAU, p) == fvc::div(phiHbyA)
);
的时候,速度 $U$ 还没有经过修正,即这里的 $U$ 是预测值,所以,迭代过程中入口压力梯度不一定为零。将入口压力设置为等于 $H/A-U$,有助于提高计算稳定性。
从代码角度看,对压力方程而言,需要设置的是 inlet 边界上的 $(\nabla p)_{\bot} \cdot rAU$,
$$
(\nabla p)_{\bot} \cdot rAU = (\nabla p)_{\bot}/A_f = \frac{1}{A_f}(\nabla p \cdot \vec{n} \cdot |S_f|) = (H/A) \cdot S_f-U\cdot S_f
$$
设置的边界条件应为
$$
\nabla p \cdot \vec{n} = \frac{(H/A) \cdot S_f-U\cdot S_f}{\big |S_f \big| \cdot rAU}
$$
但实际上,fixedFluxPressureFvPatchScalarField
主要是用于两相流中,以 twoPhaseEulerFoam
为例,
半离散的动量方程为:
$$
U_{a}=\frac{1}{a_{p,a}}H(U_a)-\frac{\nabla p}{a_{p,a}\rho_a}+\frac{\alpha_b}{ a_{p,a} \rho_a} K U_b +\frac{1}{a_{p,a}} g
$$
$$
U_{b}=\frac{1}{a_{p,b}}H(U_b)-\frac{\nabla p}{a_{p,b}\rho_b}+\frac{\alpha_a}{ a_{p,b} \rho_b} K U_a +\frac{1}{a_{p,b}} g
$$
对 $U_a$ 项两边同时乘以 $\alpha_a$ ,$U_b$ 项两边同时乘以 $\alpha_b$ ,然后合并起来,得到
$$
\begin{align*}
\Big(\frac{\alpha_a}{a_{p,a}\rho_a}+ \frac{\alpha_b}{a_{p,b}\rho_b}\Big)\nabla p & = \alpha_a \Big[\frac{1}{a_{p,a}}H(U_a)+\frac{\alpha_b}{ a_{p,a} \rho_a} K U_b +\frac{1}{a_{p,a}} g\Big] + \alpha_b \Big[ \frac{1}{a_{p,b}}H(U_b)+\frac{\alpha_a}{ a_{p,b} \rho_b} K U_a +\frac{1}{a_{p,b}} g\Big] \\
& - (\alpha_a U_a + \alpha_b U_b)
\end{align*}
$$
转换成界面通量形式,则
$$
D_p \nabla p \cdot S_f = \alpha_a \cdot phiHbyA1 + \alpha_b \cdot phiHbyA2 - (\alpha_a \cdot phi1 + \alpha_b \cdot phi2) = phiHbyA - phi
$$
于是得到压力的边界条件为
$$
\nabla p \cdot \vec{n} = \frac{phiHbyA - phi}{Dp \cdot \big|S_f\big|}
$$
这大概就是 fixedFluxPressureFvPatchScalarField
的物理含义吧。
在 OpenFOAM-2.3.x 以后,fixedFluxPressureFvPatchScalarField
做了修改,不需要指定 Dp
等这些量的值,而是在求解器中直接指定压力梯度。
比如 twoPhaseEulerFoam
中1
2
3
4
5
6
7
8
9
10
11
12
13
14setSnGrad<fixedFluxPressureFvPatchScalarField>
(
p.boundaryField(),
(
phiHbyA.boundaryField()
- mrfZones.relative
(
alpha1f.boundaryField()
*(mesh.Sf().boundaryField() & U1.boundaryField())
+ alpha2f.boundaryField()
*(mesh.Sf().boundaryField() & U2.boundaryField())
)
)/(mesh.magSf().boundaryField()*rAUf.boundaryField())
);
setSnGrad
这个函数的定义在 fixedFluxPressureFvPatchScalarField
中定义:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
template<class GradBC>
inline void setSnGrad
(
volScalarField::GeometricBoundaryField& bf,
const FieldField<fvsPatchField, scalar>& snGrad
)
{
forAll(bf, patchi)
{
if (isA<GradBC>(bf[patchi]))
{
refCast<GradBC>(bf[patchi]).updateCoeffs(snGrad[patchi]);// 调用带一个参数的 updateCoeffs 函数
}
}
}
// 实际调用的是这个函数,这个函数调用完以后,curTimeIndex_ 将等于当前时间步的标签。
void Foam::fixedFluxPressureFvPatchScalarField::updateCoeffs
(
const scalarField& snGradp
)
{
if (updated())
{
return;
}
curTimeIndex_ = this->db().time().timeIndex();
gradient() = snGradp;
fixedGradientFvPatchScalarField::updateCoeffs();
}
// 但是,很多其他地方仍然需要不带参数的 updateCoeffs 函数接口。
// 这个函数将不起实质作用,但是,如果在调用这个无参的 updateCoeffs 函数之前,没有先调用带一个参数的 updateCoeffs 函数,
// curTimeIndex_ 也没有更新到当前的时间步标签,那就报错,表明不适合使用这个边界条件。
void Foam::fixedFluxPressureFvPatchScalarField::updateCoeffs()
{
if (updated())
{
return;
}
if (curTimeIndex_ != this->db().time().timeIndex())
{
FatalErrorIn("fixedFluxPressureFvPatchScalarField::updateCoeffs()")
<< "updateCoeffs(const scalarField& snGradp) MUST be called before"
" updateCoeffs() or evaluate() to set the boundary gradient."
<< exit(FatalError);
}
}
因此,显然,从 2.3 开始,只有在求解器中显式地调用带一个参数的 updateCoeffs 函数来指定 p 的边界条件时,该求解器的算例才能使用 fixedFluxPressure
这个边界条件。
以自带的 dambreak 算例为例,首先,将分块方法设置为 simple,1
2
3
4
5
6
7
8
9numberOfSubdomains 4;
method simple;
simpleCoeffs
{
n ( 2 2 1 );
delta 0.001;
}
然后,运行1
decomposePar -cellDist
于是便在 0 下面得到一个 volScalarField
:cellDist
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41FoamFile
{
version 2.0;
format ascii;
class volScalarField;
location "0";
object cellDist;
}
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
dimensions [0 0 0 0 0 0 0];
internalField nonuniform List<scalar>
2268
(
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
0
0
0
0
...
);
boundaryField
{
......
}
注意,上面可以看到, cellDist
的值是 1 和 0 等,这个值,对应着将来该网格将被分配到的 processor 的id。
所以,如果将 cellDist
当成是一个标量场,然后用设置初始场的工具对其值进行初始化,将来就能将对应网格手动分配到 cellDist
的值对应的进程。
OpenFOAM 自带的设置初始场的工具是 setFields
, swak4Foam
中的 funkySetField
也是可以的。这里介绍 setFields
的用法。
使用 setFields
,需要编写 setFieldsDict
,示例如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32defaultFieldValues
(
volScalarFieldValue cellDist 0
);
regions
(
boxToCell
{
box (0 0 -1) (0.2 0.2 1);
fieldValues
(
volScalarFieldValue cellDist 1
);
}
boxToCell
{
box (0 0.2 -1) (0.2 0.6 1);
fieldValues
(
volScalarFieldValue cellDist 2
);
}
boxToCell
{
box (0.2 0.2 -1) (0.6 0.6 1);
fieldValues
(
volScalarFieldValue cellDist 3
);
}
);
这里,用的是最简单的 boxToCell
,即指定一个 box 中的网格的 cellDist
值。 setFields
还有很多种方式来设置初始值。这里再举一个例子,可以先用 topoSet
来将指定区域的网格先提取到 cellSet
,然后,对整个 cellSet
的网格的 cellDist
值进行指定,示例如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22defaultFieldValues ( volScalarFieldValue cellDist 0 );
regions
(
cellToCell
{
set cellSet1 ;
fieldValues
(
volScalarFieldValue cellDist 1
);
}
cellToCell1
{
set cellSet2 ;
fieldValues
(
volScalarFieldValue cellDist 2
);
}
);
topoSet
的用法这里不举例了,有很多花样,详细的信息可以参考 applications/utilities/mesh/manipulation/topoSet/topoSetDict
中的说明。
设置好 setFieldDict
以后,运行 setFields
,便对 cellDist
的值进行了修改,可视化如下
下一步,需要根据 cellDist
的值来创建一个 labelList
,因为手动分块的时候,需要的是一个 labelList
。
在constant下创建一个文件, cellDecomposition
,内容如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40FoamFile
{
version 2.0;
format ascii;
class labelList;
location "constant";
object cellDecomposition;
}
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
2268
(
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
0
0
0
0
0
0
0
1
1
......
)
注意文件头的写法。 ()
内的内容与 cellDist
文件中 ()
内的内容一样。
再下一步,就是修改 decomposeParDict
1
2
3
4
5
6
7
8numberOfSubdomains 4;
method manual;
manualCoeffs
{
dataFile "cellDecomposition";
}
然后再运行 decomposePar -force
,这样就得到了根据 cellDist
值来指定的分块方式,如下
注意看这里的进程边界,跟上图中 cellDist
的值的边界是一样的。
在 LES 模拟的程序中,常用的一种过滤方法是隐式过滤,即,用当地网格的尺度作为该处的过滤尺度。这种方法在各种类型的网格上实现起来都相对简单。只是,稍微复杂一点的几何构体,生成的网格的尺度总是不可能一样,这也就意味着过滤尺度是变化的。如果相邻网格的尺度变化很大,这也将引起相邻网格的过滤尺度相差很大,这时就会带来严重的 commutation error。
Commutation error 产生的根本原因是,当相邻网格过滤尺度不一样时,,$\overline{\tfrac{\partial \phi}{\partial x}} \neq \tfrac{\partial \overline{\phi}}{\partial x}$。下面举一个例子来说明:
假设有一个场量,解析值为
$$
\phi = 1-x^2
$$
其导数为
$$
\frac{\partial \phi}{\partial x} = -2x
$$
在一个如下图的网格中来进行过滤
第一次,我们对点 $P$ 和 $N_1$ 处进行过滤,过滤直径都是 $\Delta_1 = 1$,使用 top hat 过滤函数,即
$$
G(x,\Delta)=
\begin{cases}
\frac{1}{\Delta} & if(|x’\le\frac{\Delta}{2}|) \\
0 & otherwise
\end{cases}
$$
则
$$
\overline{\phi}_P = \oint G(x,x\prime;\Delta)\,f(x\prime) dx\prime = \int _{-\infty} ^{-1} 0\cdot \phi(x\prime) dx\prime + \int_{-1}^{0} \frac{1}{\Delta_1} \cdot \phi(x\prime)dx\prime + \int _{0} ^{\infty} 0\cdot \phi(x\prime) dx\prime = \frac{2}{3}
$$
类似地,
$$
\overline{\phi}_{N_1} = \int_0^1 \frac{1}{\Delta_1} \cdot \phi(x\prime) dx\prime = \frac{2}{3}
$$
另一方面,对梯度使用过滤,得
$$
\Bigg(\overline{\frac{\partial \phi}{\partial x}} \Bigg)_0= \int_{-0.5}^{0.5} \frac{1}{\Delta_1}\cdot(-2x)dx = 0
$$
而
$$
\frac{\partial \overline{\phi}}{\partial x} = \frac{\overline{\phi}_{N1}-\overline{\phi}_P}{x_{N1}-x_{P}} = 0
$$
这说明,当相邻网格的过滤尺度一致时,commutation error 为零。但是,当过滤尺度不一致时,考虑右边的网格中心为 $N_2$ 的情形,此时
$$
\overline{\phi}_{N2} = \int_{0}^{0.5} \frac{1}{\Delta_2} \cdot \phi dx = \frac{11}{12}
$$
$$
\frac{\partial \overline{\phi}}{\partial x} = \frac{\overline{\phi}_{N2}-\overline{\phi}_P}{x_{N2}-x_{P}} = \frac{1}{6} \neq 0
$$
这时就产生了 commutation error。
所以,网格尺度不均匀现象严重的区域就会出现显著的 commutation error。边界层是容易出现网格尺度不一致的区域,在壁面上,严格来说,过滤尺度必须是零,否则将破坏无滑移条件。这种情况还可以通过 DES 这样的方法来处理。但是,核心区也可能出现相邻网格不一致的情形。局部网格加密是一种实用的节省计算量的方法,这种方法只需要在重要区域进行加密,不必全局加密。但是局部加密就导致了相邻网格尺度不一致,由此就带来了显著的 commutation error。所以,使用局部加密时,要注意不要让相邻两种等级的网格的边界落在重要区域。
局部加密效应可以从下图看出
当从细网格向粗网格过渡时,会出现亚格子粘度的突降;当从粗网格过渡到细网格时,亚格子粘度会突然上升。有一种办法是对过滤尺度进行光滑处理:
$$
\Delta P=max(\Delta P, \Delta N/C_{\Delta s})
$$
$C_{\Delta s}$ 的取值约为 1.5。
用这种方法可让局部加密带来的问题得到部分缓解。亚格子粘度在粗细网格交接附近的过渡也更光滑。
误差总是无可避免的,降低误差的技术也有很多。从“计算性价比”(即为了得到一定准确度的结果所耗费的计算量) 的角度看,没有哪个结果就一定是严格地错误或者正确,只是“计算性价比”不同。有时候为了降低计算量,在一些区域即便产生了严重的误差,也是可以接受的。关键是要对误差差生的原因有清楚的认识,尽量不要让很大的误差出现在重要的区域。
本篇的图,文,公式,完全取材整理自 Eugene de Villiers 的博士论文 “The Potential of Large Eddy Simulation for the Modeling of Wall Bounded Flows”,特此声明。
]]>有了前面的基础,增加一个模型应该不在话下了,这里给出一个例子。
关键在调用 makeThermo
宏函数,来将各个子模型组合起来,形成一个新的热物理模型,并添加到合适的 hashTable 里。
这里只看看怎么来增加状态方程模型,transport 模型(描述黏度 随温度的变化),thermo 模型(描述 cp 随温度的变化),energy 模型。将 perfectGas
, constTransport
, hConstThermo
,以及 sensibleInternalEnergy
拷贝出来到一个目录下,并分别重命名为 my+原始模型名
的形式。同时,修改各个模型的typeName,比如, perfectGas
修改为 myperfectGas
, myperfectGas.H
的 typeName
修改为:1
2
3
4static word typeName()
{
return "myperfectGas<" + word(Specie::typeName_()) + '>';
}
注意,这里 typeName
一定要改,否则,将在运行算例的时候,出现 duplicate entry
的错误,根本原因在于,将模型添加到 hashTable 的时候,hashTable 的 key 是由 typeName
组合而成的,如果新模型使用了跟旧模型一样的 typeName
就可能会在 hashTable 出现两个一个一样的 key,即 duplicate entry
。
然后,将 src/thermophysicalModels/basic/rhoThermo
目录下的 rhoThermos.C
拷贝到新模型所在目录下,并修改为:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49#include "rhoThermo.H"
#include "makeThermo.H"
#include "specie.H"
#include "perfectGas.H"
#include "myperfectGas.H" // 新加的
#include "incompressiblePerfectGas.H"
#include "rhoConst.H"
#include "perfectFluid.H"
#include "PengRobinsonGas.H"
#include "adiabaticPerfectFluid.H"
#include "hConstThermo.H"
#include "myhConstThermo.H" // 新加的
#include "janafThermo.H"
#include "sensibleEnthalpy.H"
#include "sensibleInternalEnergy.H"
#include "mysensibleInternalEnergy.H" // 新加的
#include "thermo.H"
#include "constTransport.H"
#include "myconstTransport.H" // 新加的
#include "sutherlandTransport.H"
#include "icoPolynomial.H"
#include "hPolynomialThermo.H"
#include "polynomialTransport.H"
#include "heRhoThermo.H"
#include "pureMixture.H"
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
namespace Foam
{
makeThermo
(
rhoThermo,
heRhoThermo,
pureMixture,
myconstTransport,
mysensibleInternalEnergy,
myhConstThermo,
myperfectGas,
specie
);
} // End namespace Foam
注意,头文件里有四个是新加的。 makeThermo
宏只调用了一次,即这里只增加了一个模型。其他的组合当然也是可以的,比如像这样1
2
3
4
5
6
7
8
9
10
11makeThermo
(
rhoThermo,
heRhoThermo,
pureMixture,
constTransport,
sensibleInternalEnergy,
hConstThermo,
myperfectGas,
specie
);
灵活组合就好了。
最后,将 src/thermophysicalModels/basic
目录下的 Make
拷贝到新模型所在目录下。并将 files
和 options
如下:
files
1 | rhoThermos.C |
options
1 | EXE_INC = \ |
注意,这里也作了修改, EXE_INC
里增加了 -I$(LIB_SRC)/thermophysicalModels/basic/lnInclude \
; LIB_LIBS
里增加了两条: -lspecie
和 -lfluidThermophysicalModels
。
有了这些,就万事具备了,下面给出一个目录树:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18├── const
│ ├── myconstTransport.C
│ ├── myconstTransport.H
│ └── myconstTransportI.H
├── hConst
│ ├── myhConstThermo.C
│ ├── myhConstThermo.H
│ └── myhConstThermoI.H
├── Make
│ ├── files
│ └── options
├── perfectGas
│ ├── myperfectGas.C
│ ├── myperfectGas.H
│ └── myperfectGasI.H
├── rhoThermos.C
└── sensibleInternalEnergy
└── mysensibleInternalEnergy.H
运行 wmake libso
,就能编译得到一个新的库了。
那么怎么调用新增的模型呢?分两步:
libs ( "libMyTestfluidThermophysicalModels.so" );
constant/thermophysicalProperties
,改为如下1 | thermoType |
这样改好以后,新模型就会被调用了。当运行求解器的时候出现如下内容,1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16......
......
Reading thermophysical properties
Selecting thermodynamics package
{
type heRhoThermo;
mixture pureMixture;
transport myconst;
thermo myhConst;
equationOfState myperfectGas;
specie specie;
energy mysensibleInternalEnergy;
}
.......
.......
就表示新模型调用成功了。
最后提醒一下,这里的测试,只是将原有模型原封不动地拷贝出来了,只是改了 tpeName
。实际应用场景肯定会比这个复杂,这里只是给出一个最基本的流程来供大家参考。
twoPhaseEulerFoam
的 EEqn.H
为例。
1 | { |
对应的能量方程为(忽略fvOptions)
$$
\alpha \rho \frac{\partial (\mathrm{he})}{\partial t} + \alpha \rho U\cdot \nabla(\mathrm{he}) + \alpha \rho \frac{\partial (\mathrm{K})}{\partial t} + \alpha \rho U\cdot \nabla\mathrm{K} + \\
\begin{cases}
p\cdot\dfrac{\partial \alpha}{\partial t} + \nabla \cdot (\alpha U p) , & \mbox{ if } he.name == \mbox{“e”} \\
-\alpha \dfrac{\partial p}{\partial t}, & \mbox{ if } he.name == \mbox{“h”}
\end{cases} \\
-\nabla \cdot \big(\alpha \cdot \alpha_{eff} \nabla (\mathrm{he}) \big) - \gamma(T_2 - T_1) = 0
$$
代码里剩下的两项,1
2+ heatTransferCoeff*he1/Cpv1
- fvm::Sp(heatTransferCoeff/Cpv1, he1)
含义暂不明。这两项,其实是同一个公式,只是前者是显示处理,后者用了隐式源项,估计是为了数值稳定性的目的而构建的。
前面提过,对于如下设置,1
2
3
4
5
6
7
8
9
10thermoType
{
type heRhoThermo;
mixture pureMixture;
transport const;
thermo hConst;
equationOfState perfectGas;
specie specie;
energy sensibleInternalEnergy;
}
最终,thermo
指针指向的是 heRhoThermo
类的对象,所以,从 heRhoThermo
类的构造函数看起:1
2
3
4
5
6
7
8
9
10
11template<class BasicPsiThermo, class MixtureType>
Foam::heRhoThermo<BasicPsiThermo, MixtureType>::heRhoThermo
(
const fvMesh& mesh,
const word& phaseName
)
:
heThermo<BasicPsiThermo, MixtureType>(mesh, phaseName)
{
calculate(); // 构造函数调用 calculate 函数来初始化所有的热物理相关量
}
可见,构造函数里调用了 calculate
函数,前面提过,这个函数的作用是更新各个热物理相关量。
接下来一个一个来看里面涉及到的函数。
he
he
其实是 “h or e”,具体是焓,还是内能,取决于 energy
这一项的设置。 he
函数在 heThermo
类中定义,返回的是数据成员 he_
,所以这里需要看一下数据成员 he_
的初始化:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32template<class BasicThermo, class MixtureType>
Foam::heThermo<BasicThermo, MixtureType>::heThermo
(
const fvMesh& mesh,
const dictionary& dict,
const word& phaseName
)
:
BasicThermo(mesh, dict, phaseName),
MixtureType(*this, mesh),
he_
(
IOobject
(
BasicThermo::phasePropertyName
(
MixtureType::thermoType::heName()
),
mesh.time().timeName(),
mesh,
IOobject::NO_READ,
IOobject::NO_WRITE
),
mesh,
dimEnergy/dimMass,
this->heBoundaryTypes(),
this->heBoundaryBaseTypes()
)
{
init();
}
这里调用的 init
函数的内容为1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25template<class BasicThermo, class MixtureType>
void Foam::heThermo<BasicThermo, MixtureType>::init()
{
scalarField& heCells = he_.internalField();
const scalarField& pCells = this->p_.internalField();
const scalarField& TCells = this->T_.internalField();
forAll(heCells, celli)
{
heCells[celli] =
this->cellMixture(celli).HE(pCells[celli], TCells[celli]);
}
forAll(he_.boundaryField(), patchi)
{
he_.boundaryField()[patchi] == he
(
this->p_.boundaryField()[patchi],
this->T_.boundaryField()[patchi],
patchi
);
}
this->heBoundaryCorrection(he_);
}
这里调用了 HE
函数来初始化 he_
的内部场,并对调用另一个三个数的 he
函数其边界条件进行了修正:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19template<class BasicThermo, class MixtureType>
Foam::tmp<Foam::scalarField> Foam::heThermo<BasicThermo, MixtureType>::he
(
const scalarField& p,
const scalarField& T,
const label patchi
) const
{
tmp<scalarField> the(new scalarField(T.size()));
scalarField& he = the();
forAll(T, facei)
{
he[facei] =
this->patchFaceMixture(patchi, facei).HE(p[facei], T[facei]);
// 本质上还是调用了 HE 函数
}
return the;
}
再来看 HE
函数,这个函数看名字和参数,应该是根据压力和温度来计算能量的,其定义在 species::thermo
类:1
2
3
4
5
6template<class Thermo, template<class> class Type>
inline Foam::scalar
Foam::species::thermo<Thermo, Type>::HE(const scalar p, const scalar T) const
{
return Type<thermo<Thermo, Type> >::HE(*this, p, T);
}
这里,由于能量最终是什么形式,取决于 energy
关键字对应的类,所以,这里也是调用了定义在前面提到的 energy variable
类中的 HE
函数,以 sensibleInternalEnergy
为例:1
2
3
4
5
6
7
8
9scalar HE
(
const Thermo& thermo,
const scalar p,
const scalar T
) const
{
return thermo.Es(p, T);
}
可见,其返回的是 species::thermo
类的 Es
函数,1
2
3
4
5
6
7
8
9
10
11
12
13template<class Thermo, template<class> class Type>
inline Foam::scalar
Foam::species::thermo<Thermo, Type>::Es(const scalar p, const scalar T) const
{
return this->es(p, T)/this->W();
}
template<class Thermo, template<class> class Type>
inline Foam::scalar
Foam::species::thermo<Thermo, Type>::es(const scalar p, const scalar T) const
{
return this->hs(p, T) - p*this->W()/this->rho(p, T);
}
hs
函数定义在 thermo
类型的类中,以 hConstThermo
类为例:1
2
3
4
5
6
7
8template<class equationOfState>
inline Foam::scalar Foam::hConstThermo<equationOfState>::hs
(
const scalar p, const scalar T
) const
{
return Cp_*T;
}
hs
表示的是显焓,等于 Cp_*T
。 es
是内能,根据焓的定义,$H=U+pV$。代码中的 hs
和 es
都是 J/kMol
的量纲,所以,$es=hs-pV/n$ 。以理想气体状态方程为例,$pV=nRT$,或者写成 $pM=\rho RT$,得 $pV/n = RT = pM/\rho$ 。
注意,这里的 Cp_
,在字典文件里给的是 J/(kg.K)
量纲的,但是在构造函数中,将其转成了 J/(kmol.K)
的量纲:1
2
3
4
5
6
7
8
9
10template<class equationOfState>
Foam::hConstThermo<equationOfState>::hConstThermo(const dictionary& dict)
:
equationOfState(dict),
Cp_(readScalar(dict.subDict("thermodynamics").lookup("Cp"))),
Hf_(readScalar(dict.subDict("thermodynamics").lookup("Hf")))
{
Cp_ *= this->W();
Hf_ *= this->W();
}
所以,hs
, es
是 J/kmol
; Es
, HE
是 J/kg
。
这个函数定义在 heThermo
类中。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44template<class BasicThermo, class MixtureType>
Foam::tmp<Foam::volScalarField>
Foam::heThermo<BasicThermo, MixtureType>::Cpv() const
{
const fvMesh& mesh = this->T_.mesh();
tmp<volScalarField> tCpv
(
new volScalarField
(
IOobject
(
"Cpv",
mesh.time().timeName(),
mesh,
IOobject::NO_READ,
IOobject::NO_WRITE
),
mesh,
dimEnergy/dimMass/dimTemperature
)
);
volScalarField& cpv = tCpv();
forAll(this->T_, celli)
{
cpv[celli] =
this->cellMixture(celli).Cpv(this->p_[celli], this->T_[celli]);
}
forAll(this->T_.boundaryField(), patchi)
{
const fvPatchScalarField& pp = this->p_.boundaryField()[patchi];
const fvPatchScalarField& pT = this->T_.boundaryField()[patchi];
fvPatchScalarField& pCpv = cpv.boundaryField()[patchi];
forAll(pT, facei)
{
pCpv[facei] =
this->patchFaceMixture(patchi, facei).Cpv(pp[facei], pT[facei]);
}
}
return tCpv;
}
这个函数,创建了一个 tmp<volScalarField>
,然后调用定义在 species::thermo
类中的两参数 Cpv
函数来对场量进行初始化,这个函数的形式如下1
2
3
4
5
6
7
8
9
10
11
12
13template<class Thermo, template<class> class Type>
inline Foam::scalar
Foam::species::thermo<Thermo, Type>::Cpv(const scalar p, const scalar T) const
{
return this->cpv(p, T)/this->W();
}
template<class Thermo, template<class> class Type>
inline Foam::scalar
Foam::species::thermo<Thermo, Type>::cpv(const scalar p, const scalar T) const
{
return Type<thermo<Thermo, Type> >::cpv(*this, p, T);
}
cpv
函数返回的是定义在 energy variable
类中的三参数 cpv
函数,对于 sensibleInternalEnergy
,1
2
3
4
5
6
7
8
9scalar cpv
(
const Thermo& thermo,
const scalar p,
const scalar T
) const
{
return thermo.cv(p, T);
}
这里返回的是 species::thermo
类的 cv
函数,1
2
3
4
5
6template<class Thermo, template<class> class Type>
inline Foam::scalar
Foam::species::thermo<Thermo, Type>::cv(const scalar p, const scalar T) const
{
return this->cp(p, T) - this->cpMcv(p, T);
}
这里的 cp
函数定义在 thermo
类型的类中,以 hConst
为例1
2
3
4
5
6
7
8
9template<class equationOfState>
inline Foam::scalar Foam::hConstThermo<equationOfState>::cp
(
const scalar p,
const scalar T
) const
{
return Cp_; // 量纲是 J/(kmol.K)
}
cpMcv
的定义在状态方程类中,以 perfectGas
为例1
2
3
4
5template<class Specie>
inline Foam::scalar Foam::perfectGas<Specie>::cpMcv(scalar, scalar) const
{
return this->RR; // 量纲是 J/(kmol.K),所以值应该是 8314
}
这个函数需要一个参数,其定义在 heThermo
类中1
2
3
4
5
6
7
8
9
10
11template<class BasicThermo, class MixtureType>
Foam::tmp<Foam::volScalarField>
Foam::heThermo<BasicThermo, MixtureType>::alphaEff
(
const volScalarField& alphat
) const
{
tmp<Foam::volScalarField> alphaEff(this->CpByCpv()*(this->alpha_ + alphat));
alphaEff().rename("alphaEff");
return alphaEff;
}
这里, 无参数的 CpByCpv
函数定义在 species::thermo
类中,最终调用的是 energy varialble
类中的 CpByCpv
函数,如果是内能形式的,则返回 thermo.gamma(p, T)
,焓形式则返回 1
。 gamma
的定义在 species::thermo
1
2
3
4
5
6
7template<class Thermo, template<class> class Type>
inline Foam::scalar
Foam::species::thermo<Thermo, Type>::gamma(const scalar p, const scalar T) const
{
scalar cp = this->cp(p, T);
return cp/(cp - this->cpMcv(p, T));
}
alpha_
是层流能量扩散系数,定义在 basicThermo
类,并在该类的构造函数中初始化为零。在 heRhoThermo
类的 calculate
函数中,对其进行了更新1
2
3
4
5scalarField& alphaCells = this->alpha_.internalField();
alphaCells[celli] = mixture_.alphah(pCells[celli], TCells[celli]);
fvPatchScalarField& palpha = this->alpha_.boundaryField()[patchi];
palpha[facei] = mixture_.alphah(pp[facei], pT[facei]);
可见, alpha_
的值是通过 alphah
函数来计算更新的,这个函数的定义在 trasnport
模型里,以 constTransport
为例1
2
3
4
5
6
7
8
9template<class Thermo>
inline Foam::scalar Foam::constTransport<Thermo>::alphah
(
const scalar p,
const scalar T
) const
{
return mu(p, T)*rPr_;
}
返回粘度与普朗特数的比值。
至于 alphat
,则是函数 alphaEff
的参数,根据开头的代码可知, alphat
其实是 mut
。
只是,暂时不知道为什么有效热扩散系数 alphaEff = CpByCpv * (alpha + alphat)
。
构建起能量方程后,就该对其进行求解了。1
2
3
4
5
6
7
8
9
10
11
12
13fvOptions.constrain(he1Eqn);
he1Eqn.solve();
fvOptions.constrain(he2Eqn);
he2Eqn.solve();
thermo1.correct();
Info<< "min " << thermo1.T().name()
<< " " << min(thermo1.T()).value() << endl;
thermo2.correct();
Info<< "min " << thermo2.T().name()
<< " " << min(thermo2.T()).value() << endl;
这里, solve
函数值得细说,重点是看 correct()
函数,以及 T()
函数。
corretc()
函数指的是定义在 heRhoThermo
类中的 correct()
函数:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15template<class BasicPsiThermo, class MixtureType>
void Foam::heRhoThermo<BasicPsiThermo, MixtureType>::correct()
{
if (debug)
{
Info<< "entering heRhoThermo<MixtureType>::correct()" << endl;
}
calculate();
if (debug)
{
Info<< "exiting heRhoThermo<MixtureType>::correct()" << endl;
}
}
可见, correct()
函数,其实就是对 calculate
函数进行了一次调用而已。
看来最核心最关键的就在 calculate
函数中了。在仔细看这个函数之前,先把 T()
的定义看完。 T()
定义在 basicThermo
类中,其作用仅是返回同样定义在 basicThermo
类中定义的数据成员 T_
而已。
下面深入分析一下 heRhoThermo
类中的 calculate
函数,这里再将它列出来一次:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75template<class BasicPsiThermo, class MixtureType>
void Foam::heRhoThermo<BasicPsiThermo, MixtureType>::calculate()
{
const scalarField& hCells = this->he().internalField();
const scalarField& pCells = this->p_.internalField();
scalarField& TCells = this->T_.internalField();
scalarField& psiCells = this->psi_.internalField();
scalarField& rhoCells = this->rho_.internalField();
scalarField& muCells = this->mu_.internalField();
scalarField& alphaCells = this->alpha_.internalField();
forAll(TCells, celli)
{
const typename MixtureType::thermoType& mixture_ =
this->cellMixture(celli);
TCells[celli] = mixture_.THE
(
hCells[celli],
pCells[celli],
TCells[celli]
);
psiCells[celli] = mixture_.psi(pCells[celli], TCells[celli]);
rhoCells[celli] = mixture_.rho(pCells[celli], TCells[celli]);
muCells[celli] = mixture_.mu(pCells[celli], TCells[celli]);
alphaCells[celli] = mixture_.alphah(pCells[celli], TCells[celli]);
}
forAll(this->T_.boundaryField(), patchi)
{
fvPatchScalarField& pp = this->p_.boundaryField()[patchi];
fvPatchScalarField& pT = this->T_.boundaryField()[patchi];
fvPatchScalarField& ppsi = this->psi_.boundaryField()[patchi];
fvPatchScalarField& prho = this->rho_.boundaryField()[patchi];
fvPatchScalarField& ph = this->he().boundaryField()[patchi];
fvPatchScalarField& pmu = this->mu_.boundaryField()[patchi];
fvPatchScalarField& palpha = this->alpha_.boundaryField()[patchi];
if (pT.fixesValue())
{
forAll(pT, facei)
{
const typename MixtureType::thermoType& mixture_ =
this->patchFaceMixture(patchi, facei);
ph[facei] = mixture_.HE(pp[facei], pT[facei]);
ppsi[facei] = mixture_.psi(pp[facei], pT[facei]);
prho[facei] = mixture_.rho(pp[facei], pT[facei]);
pmu[facei] = mixture_.mu(pp[facei], pT[facei]);
palpha[facei] = mixture_.alphah(pp[facei], pT[facei]);
}
}
else
{
forAll(pT, facei)
{
const typename MixtureType::thermoType& mixture_ =
this->patchFaceMixture(patchi, facei);
pT[facei] = mixture_.THE(ph[facei], pp[facei], pT[facei]);
ppsi[facei] = mixture_.psi(pp[facei], pT[facei]);
prho[facei] = mixture_.rho(pp[facei], pT[facei]);
pmu[facei] = mixture_.mu(pp[facei], pT[facei]);
palpha[facei] = mixture_.alphah(pp[facei], pT[facei]);
}
}
}
}
这个函数是在对几个热物理相关的量来进行更新,先更新内部场,再更新边界值。一个一个来看:
he
he 前面讲了,这里需要注意的是其边界值的更新。由于 he
没有IO,其内部场量通过求解能量方程来更新,边界则需要根据情况特殊处理。两种情况,一种是设定了边界的温度值(pT.fixesValue()),这时需要更新边界上的 he
值: ph[facei] = mixture_.HE(pp[facei], pT[facei]);
否则,则边界上的 he
不需要特殊地更新。
psi
这个直接调用两参数的 psi
函数来更新,这个函数的定义在状态方程里,以 perfaceGas
为例,
1 | template<class Specie> |
psi
是压缩因子,返回 $\frac{1}{RT}$。
rho
函数,定义在状态方程类中,用于密度的更新,同样以 perfaceGas
为例,1 | template<class Specie> |
返回 $\frac{p}{RT}$。
mu
函数,其定义在 transport 类中,以 constTransport
为例,这个返回的是场量的层流粘度1 | template<class Thermo> |
alphah 上面说过了,不再重复。
最后,最复杂的就是温度的更新了
THE
函数,这个函数定义在 species::thermo
类中,1 | template<class Thermo, template<class> class Type> |
这里,调用的是 energy variable
类的 THE
函数,以 sensibleInternalEnergy
为例,1
2
3
4
5
6
7
8
9
10scalar THE
(
const Thermo& thermo,
const scalar e,
const scalar p,
const scalar T0
) const
{
return thermo.TEs(e, p, T0);
}
可见,对于 sensibleInternalEnergy
, THE
函数实际上返回的是 species::thermo
类的 TEs
函数。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18template<class Thermo, template<class> class Type>
inline Foam::scalar Foam::species::thermo<Thermo, Type>::TEs
(
const scalar es,
const scalar p,
const scalar T0
) const
{
return T
(
es,
p,
T0,
&thermo<Thermo, Type>::Es,
&thermo<Thermo, Type>::Cv,
&thermo<Thermo, Type>::limit
);
}
这里,终于来到了这个六参数的 T
函数:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56// 声明
inline scalar T
(
scalar f,
scalar p,
scalar T0,
scalar (thermo::*F)(const scalar, const scalar) const,
scalar (thermo::*dFdT)(const scalar, const scalar) const,
scalar (thermo::*limit)(const scalar) const
) const;
//实现
template<class Thermo, template<class> class Type>
inline Foam::scalar Foam::species::thermo<Thermo, Type>::T
(
scalar f,
scalar p,
scalar T0,
scalar (thermo<Thermo, Type>::*F)(const scalar, const scalar) const,
scalar (thermo<Thermo, Type>::*dFdT)(const scalar, const scalar)
const,
scalar (thermo<Thermo, Type>::*limit)(const scalar) const
) const
{
scalar Test = T0;
scalar Tnew = T0;
scalar Ttol = T0*tol_;
int iter = 0;
do
{
Test = Tnew;
Tnew =
(this->*limit)
(Test - ((this->*F)(p, Test) - f)/(this->*dFdT)(p, Test));
if (iter++ > maxIter_)
{
FatalErrorIn
(
"thermo<Thermo, Type>::T(scalar f, scalar T0, "
"scalar (thermo<Thermo, Type>::*F)"
"(const scalar) const, "
"scalar (thermo<Thermo, Type>::*dFdT)"
"(const scalar) const, "
"scalar (thermo<Thermo, Type>::*limit)"
"(const scalar) const"
") const"
) << "Maximum number of iterations exceeded"
<< abort(FatalError);
}
} while (mag(Tnew - Test) > Ttol);
return Tnew;
}
这个函数,前三个参数是普通的 scalar 类型变量,后三个参数,是函数指针,并且都指向当前类 species::thermo
的成员函数。以 TEs
为例,后三个参数分别代入的是 Es
, Cv
以及 limit
三个函数。 Es
和 Cv
前面都看过了, limit
定义在 thermo 类中,以 hConst
为例,1
2
3
4
5
6
7
8template<class EquationOfState>
inline Foam::scalar Foam::hConstThermo<EquationOfState>::limit
(
const scalar T
) const
{
return T;
}
直接返回温度 T
。事实上,除了 janaf
模型,其他的都是返回 T
。 janaf
模型中, 如果温度没有超出 [Tlow,Thigh],则会出来警告信息,并且,若 T < Tlow
则返回 Tlow
,而 T > Thigh
时,则返回 Thigh
。
下面仔细来分析六参数 T
函数的核心部分。经过摸索,发现这个其实是一个牛顿迭代的过程,目的是根据 Es
函数,从内能 es
来计算温度 T
,即求解 $E_s(p,T) - E_s = 0$ 。令 $F(T)= E_s(p,T) - E_s $,则牛顿迭代法的递推公式为
$$
T_{New} = T_{old} - \dfrac{F(T_{old})}{F\prime(T_{old})} = T_{old} - \dfrac{E_s(p, T_{old)} - E_s}{dE_s(p,T)/dT |_{T=T_{old}}}
$$
对于 sensibleInternalEnergy
,$dE_s(p,T)/dT = C_v(p,T)$
所以最终得到递推公式为
$$
T_{New} = T_{old} - \dfrac{E_s(p, T_{old)} - E_s}{C_v(p, T_{old})}
$$
这里设置了最大迭代次数为 100,超过将报那个涉及到能量的模拟中最容易见到的崩溃信息:”Maximum number of iterations exceeded” 。
当能量变量是焓时,则 $E_s$ 要换成 $H_s$, $C_v$ 要换成 $C_p$ 。
至此便分析完了一个具体的能量方程实例。
]]>twoPhaseEulerFoam
的 EEqn.H
为例。]]>
1 | thermoType |
这个设置对应的是下述类:1
2
3
4
5
6
7
8heRhoThermo
<
rhoThermo,
pureMixture
<
constTransport<species::thermo<hConstThermo<perfectGas<specie>>,sensibleInternalEnergy>>
>
>
接下来就能来看看具体的类的继承派生关系了。
经过前面的分析,最终指针 thermo_
指向的是 heRhoThermo
类的对象,所以先来看一下 heRhoThermo
类。1
2
3
4
5
6
7
8
9
10
11
12template<class BasicPsiThermo, class MixtureType>
class heRhoThermo
:
public heThermo<BasicPsiThermo, MixtureType>
{
// Private Member Functions
//- Calculate the thermo variables
void calculate();
//- Construct as copy (not implemented)
heRhoThermo(const heRhoThermo<BasicPsiThermo, MixtureType>&);
heRhoThermo
类很简单,它继承自 heThermo
类,并且和 heThermo
类用同样的模板参数。
接下来看看 heThermo
类:1
2
3
4
5
6
7
8
9
10
11
12template<class BasicThermo, class MixtureType>
class heThermo
:
public BasicThermo,
public MixtureType
{
protected:
// Protected data
//- Energy field
volScalarField he_;
这里,前面多次提到的“继承其模板参数代表的类”这种模式又出现了。有了前两篇分析的基础,我们已经知晓了 thermophysicalProperties
文件里的设置将对应着怎么的一个具体的模型,所有的模板参数都一目了然。知道了模板参数,就能将模板类实例化,再分析其继承派生关系就不是问题了。探索的中间过程这里不详述了,只列出文章开头的那个实例对应的继承派生关系:
注意几点:
constTransport
这个类是作为 pureMixture
类的模板参数的,从而将这两个部分联系起来。species::thermo
中, species
是命名空间, thermo
是类名,具体定义在 src/thermophysicalModels/specie/thermo/thermo/thermo.H
。sensibleInternalEnergy
类的模板参数中,某种程度上讲,包含了它自己!了解了这些,就能理解热物理类的框架了。下一部分将针对 twoPhaseEulerFoam
的热物理相关的模块,来梳理一下热物理模型的流程,主要是,能量方程的构建,如何从能量来得到温度,其他依赖于温度的量包括粘度、密度、压力等又是如何更新的,希望能对传热的模拟有些指导作用,尤其是当出问题的时候,能大概知道可能的原因。
1 | thermoType |
这个设置对应的是下述类:1
2
3
4
5
6
7
8heRhoThermo
<
rhoThermo,
pureMixture
<
constTransport<species::thermo<hConstThermo<perfectGas<specie>>,sensibleInternalEnergy>>
>
>
接下来就能来看看具体的类的继承派生关系了。
]]>根据以前的经验,编译和构建 hashTable 肯定是跟这个源文件有关:src/thermophysicalModels/basic/rhoThermo/rhoThermos
。
这个文件里,全部都是在调用宏函数。而且,从宏函数的参数来看,似乎就是个排列组合的游戏,把所有可用的组合都写了一遍。1
2
3
4
5
6
7
8
9
10
11
12makeThermo
(
rhoThermo,
heRhoThermo,
pureMixture,
constTransport,
sensibleInternalEnergy,
hConstThermo,
perfectGas,
specie
);
......
为了弄清这部分内容,需要先理解 makeThermo
这个宏函数的定义,见 src/thermophysicalModels/basic/fluidThermo/makeThermo.H
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71#define makeThermoTypedefs(BaseThermo,Cthermo,Mixture,Transport,Type,Thermo,EqnOfState,Specie)\
\
typedef \
Transport \
< \
species::thermo \
< \
Thermo \
< \
EqnOfState \
< \
Specie \
> \
>, \
Type \
> \
> Transport##Type##Thermo##EqnOfState##Specie; \
\
typedef \
Cthermo \
< \
BaseThermo, \
Mixture<Transport##Type##Thermo##EqnOfState##Specie> \
> Cthermo##Mixture##Transport##Type##Thermo##EqnOfState##Specie; \
\
defineTemplateTypeNameAndDebugWithName \
( \
Cthermo##Mixture##Transport##Type##Thermo##EqnOfState##Specie, \
( \
#Cthermo"<"#Mixture"<" \
+ Transport##Type##Thermo##EqnOfState##Specie::typeName() \
+ ">>" \
).c_str(), \
0 \
);
#define makeThermo(BaseThermo,Cthermo,Mixture,Transport,Type,Thermo,EqnOfState,Specie)\
\
makeThermoTypedefs \
( \
BaseThermo, \
Cthermo, \
Mixture, \
Transport, \
Type, \
Thermo, \
EqnOfState, \
Specie \
) \
\
addToRunTimeSelectionTable \
( \
basicThermo, \
Cthermo##Mixture##Transport##Type##Thermo##EqnOfState##Specie, \
fvMesh \
); \
\
addToRunTimeSelectionTable \
( \
fluidThermo, \
Cthermo##Mixture##Transport##Type##Thermo##EqnOfState##Specie, \
fvMesh \
); \
\
addToRunTimeSelectionTable \
( \
BaseThermo, \
Cthermo##Mixture##Transport##Type##Thermo##EqnOfState##Specie, \
fvMesh \
);
可见,在 makeThermo
这个宏函数里,先调用了 makeThermoTypedefs
宏函数,然后调用 addToRunTimeSelectionTable
函数。按照之前对 RTS 机制的理解,调用 addToRunTimeSelectionTable
函数的作用是往 hashTable 里插入元素,细节不需再赘述。这里主要来看看 makeThermoTypedefs
函数的功能,以上文列举的这个实例为例。先来看第一个 typedef:将实例中的参数代入后,1
2
3typedef \
constTransport<species::thermo<hConstThermo<perfectGas<specie>>,sensibleInternalEnergy>> \
constTransportsensibleEnthalpyhConstThermoperfectGasspecie;
第二个 typedef1
2
3
4
5
6typedef \
heRhoThermo \
< \
rhoThermo, \
pureMixture<constTransportsensibleEnthalpyhConstThermoperfectGasspecie> \
> heRhoThermopureMixtureconstTransportsensibleEnthalpyhConstThermoperfectGasspecie;
除了这两个 typedef,还调用了 defineTemplateTypeNameAndDebugWithName
宏函数,这个函数的定义在 src/OpenFOAM/db/typeInfo/className.H
:
1 | #define defineTemplateTypeNameAndDebugWithName(Type, Name, DebugSwitch) \ |
很显然,这个宏函数的作用是修改类对应的 typeName
和 debug 选项。在 OpenFOAM 中,很多类中都会调用 TypeName("typename")
,这里的 TypeName
也是一个宏函数,定义在 src/OpenFOAM/db/typeInfo/className.H
:
1 | #define TypeName(TypeNameString) \ |
可见, TypeName
这个宏函数,声明了一个类静态变量 typeName
,定义了一个函数 type
用于返回 typeName
的值,并定义了一个静态变量 debug
用于存储 debug 选项。这里与 RTS 机制有关的是 typeName
这个变量。
绕了半圈,回到 defineTemplateTypeNameAndDebugWithName
函数。了解了 typeName
这个变量的定义,很容易就能看出来, defineTemplateTypeNameAndDebugWithName
这个函数其实就是在对类的静态变量 typeName
进行赋值。根据上文的实例提供的参数,宏函数 defineTemplateTypeNameAndDebugWithName
可以理解为:对 heRhoThermopureMixtureconstTransportsensibleEnthalpyhConstThermoperfectGasspecie
对应的类的静态变量 typeName
进行赋值,赋值结果为:heRhoThermo<pureMixture< + constTransport<species::thermo<hConstThermo<perfectGas<specie>>,sensibleInternalEnergy>>::typeName() + >>
。这里调用了 constTransport
类的成员函数 typeName()
。
经过一番冗长的函数调用,得到的最终结果是:将 heRhoThermopureMixtureconstTransportsensibleEnthalpyhConstThermoperfectGasspecie
对应的类的静态变量 typeName
赋值为:heRhoThermo<pureMixture<const<hConst<perfectGas<specie>>,sensiblesensibleEnthalpy>>>
。
至此,经过一番宏函数的调用,得到了 addToRunTimeSelectionTable
宏函数的参数。前面 RTS 机制部分讲过,这个函数的作用就是对 hashTable 增加元素,以
1 | addToRunTimeSelectionTable \ |
为例,第一个参数,表示元素将增加到 BaseThermo
类(这里是 rhoThermo
)中声明的 hashTable,第二个参数,表示将要添加的类,添加成功以后,这个类的 typeName
将是 hashTable 的 key,而返回这个类的对象的一个函数,将是 hashTable 的 value。第三个参数对应着 hashTable 对象的名字,fvMesh 对应的 hashTable 对象名为 fvMeshConstructorTable
,这与在 rhoThermo
中声明的名字是对应的。
最后总结如下:
宏函数
1 | makeThermo |
调用以后,向 rhoThermo
类中声明的 hashTable 中增加了一组元素,其 key 为 heRhoThermo<pureMixture<const<hConst<perfectGas<specie>>,sensibleInternalEnergy>>>
,value 对应的函数返回的是类
1 | heRhoThermo |
的对象。
每调用一次 makeThermo
函数,就增加了一个新组元素,也即增加了一个可选的模型。不同的参数,其实对应的是不同的模板实例。
至此,就知道了在 twoPhaseEulerFoam
的 phaseModel
中定义的热物理类接口 thermo_
最终指向的是 heRhoThermo
类的对象。虽然代入的模板数很复杂,但整个架构仍然是基于 RTS 机制的。
接下来,要想理解能量方程,理解温度,粘度,压力等这些热物理相关的量是怎么计算更新的,就需要仔细看一下 heRhoThermo
类的继承派生关系了。
twoPhaseEulerFoam
为例。
twoPhaseEulerFoam
中的热物理类的接口在 phaseModel
类中声明:1
2
3
4//- Thermophysical properties
autoPtr<rhoThermo> thermo_;
thermo_(rhoThermo::New(fluid.mesh(), name_))
可见,接口是 rhoThermo
类的指针。
接着看 rhoThermo
类的 New
函数。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15//- Selector
static autoPtr<rhoThermo> New
(
const fvMesh&,
const word& phaseName=word::null
);
Foam::autoPtr<Foam::rhoThermo> Foam::rhoThermo::New
(
const fvMesh& mesh,
const word& phaseName
)
{
return basicThermo::New<rhoThermo>(mesh, phaseName);
}
这里调用的是 basicThermo
类的 New
函数。 这里先提一下继承关系,后面再细说:rhoThermo
类继承自 fluidThermo
, fluidThermo
类继承自 basicThermo
。
1 | //- Generic New for each of the related thermodynamics packages |
根据 RTS 机制的惯例, New
函数的功能是模型选择(selector
),即根据用户指定的关键字来选择对应的模型。 New
函数中先定义了一个 IOdictionary
类的对象, thermoDict
,这个对象对应的正是热物理类的配置文件 thermophysicalProperties
。New
函数里调用了 lookupThermo
函数,这个函数是关键:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105// basicThermoTemplates.C
template<class Thermo, class Table>
typename Table::iterator Foam::basicThermo::lookupThermo
(
const dictionary& thermoDict,
Table* tablePtr
)
{
word thermoTypeName;
if (thermoDict.isDict("thermoType"))
{
const dictionary& thermoTypeDict(thermoDict.subDict("thermoType"));
Info<< "Selecting thermodynamics package " << thermoTypeDict << endl;
const int nCmpt = 7;
const char* cmptNames[nCmpt] =
{
"type",
"mixture",
"transport",
"thermo",
"equationOfState",
"specie",
"energy"
};
// Construct the name of the thermo package from the components
thermoTypeName =
word(thermoTypeDict.lookup("type")) + '<'
+ word(thermoTypeDict.lookup("mixture")) + '<'
+ word(thermoTypeDict.lookup("transport")) + '<'
+ word(thermoTypeDict.lookup("thermo")) + '<'
+ word(thermoTypeDict.lookup("equationOfState")) + '<'
+ word(thermoTypeDict.lookup("specie")) + ">>,"
+ word(thermoTypeDict.lookup("energy")) + ">>>";
// Lookup the thermo package
typename Table::iterator cstrIter = tablePtr->find(thermoTypeName);
// Print error message if package not found in the table
if (cstrIter == tablePtr->end())
{
FatalErrorIn(Thermo::typeName + "::New")
<< "Unknown " << Thermo::typeName << " type " << nl
<< "thermoType" << thermoTypeDict << nl << nl
<< "Valid " << Thermo::typeName << " types are:" << nl << nl;
// Get the list of all the suitable thermo packages available
wordList validThermoTypeNames
(
tablePtr->sortedToc()
);
// Build a table of the thermo packages constituent parts
// Note: row-0 contains the names of constituent parts
List<wordList> validThermoTypeNameCmpts
(
validThermoTypeNames.size() + 1
);
validThermoTypeNameCmpts[0].setSize(nCmpt);
forAll(validThermoTypeNameCmpts[0], j)
{
validThermoTypeNameCmpts[0][j] = cmptNames[j];
}
// Split the thermo package names into their constituent parts
forAll(validThermoTypeNames, i)
{
validThermoTypeNameCmpts[i+1] =
Thermo::splitThermoName(validThermoTypeNames[i], nCmpt);
}
// Print the table of available packages
// in terms of their constituent parts
printTable(validThermoTypeNameCmpts, FatalError);
FatalError<< exit(FatalError);
}
return cstrIter;
}
else
{
thermoTypeName = word(thermoDict.lookup("thermoType"));
Info<< "Selecting thermodynamics package " << thermoTypeName << endl;
typename Table::iterator cstrIter = tablePtr->find(thermoTypeName);
if (cstrIter == tablePtr->end())
{
FatalErrorIn(Thermo::typeName + "::New")
<< "Unknown " << Thermo::typeName << " type "
<< thermoTypeName << nl << nl
<< "Valid " << Thermo::typeName << " types are:" << nl
<< tablePtr->sortedToc() << nl
<< exit(FatalError);
}
return cstrIter;
}
}
可见, loopupThermo
分两种情况处理,一种是 thermophysicalProperties
文件里有一个名为 thermoType
的 subdict
,例如1
2
3
4
5
6
7
8
9
10thermoType
{
type heRhoThermo;
mixture pureMixture;
transport const;
thermo hConst;
equationOfState perfectGas;
specie specie;
energy sensibleInternalEnergy;
}
这种情况下, subdict
里的 7 个关键字将逐一读入,最终将合并起来,得到一个字符串,并赋值给 thermoTypeName
以上面的那种情况为例,最终得到的thermoTypeName
为1
heRhoThermo<pureMixture<const<hConst<perfectGas<specie>>,sensibleInternalEnergy>>>
然后,根据这个关键词,从 hashTable 中找到对应的元素。如果找不到对应的,则报错,并输出所有可选的方案(由 splitThermoName
和 printTable
两个函数完成,细节这里暂且不表)。
另一种情况,直接从 thermophysicalProperties
读取 thermoType
对应的字符串并赋值给 thermoTypeName
,然后据此来从 hashTable 中找到对应的元素。
OpenFOAM 自带的算例中,thermophysicalProperties
文件绝大部分采用前一种方式,因为更直观。后一种方式我在 OpenFOAM-2.3.x 版中只找到一个例子:tutorials/mesh/foamyQuadMesh/OpenCFD/constant/thermophysicalProperties1
thermoType hePsiThermo<pureMixture<const<hConst<perfectGas<specie>>,sensibleEnthalpy>>>;
至此,大致就知道热物理类的接口定义是怎么回事了。但是,这个存储了可选模型的 hashTable 里有哪些内容,又是怎么构建起来的,还有待进一步深入探索。另外,从 thermoType
对应的字符串的样式,能猜到最终热物理类的接口 thermo_
指向的可能是类似 heRhoThermo
类的对象,而且这些类多半是模板类,并有着复杂的继承派生关系,这部分也还有待深入探索。
twoPhaseEulerFoam
为例。]]>
本篇介绍怎么在 OpenFOAM 中提取涡结构。
历史上曾用过的涡结构提取有以下几种:
压强的局部极小值
在形成涡的地方,通常伴随着压强的极小值。比如:
这种方法的缺点在于,缺乏客观的压力阈值来捕捉所有的涡结构,而且,压力出现极值的地方不见得就真的有涡。
流线
通过流线的封闭来显示涡的结构也是一种常见方法,比如
这种方法有一个最明显的缺点是,流线不满足伽利略不变性,即,如果换一个参考系,则可能显示出来的“涡结构”就完全不一样了。另外,这种方法也难以分辨两个很靠近的涡。
涡量的模
用涡量的模来显示涡结构是一种很常用的方法,类似这样
这种方法在自由剪切流中很有效,不过,对于壁面束缚流动则不太适用,原因是背景流动的剪切性导致的涡量模可以达到跟涡结构处的涡量的模差不多大小,这就使得涡结构难以从背景流动中分离出来了。并且,涡量的模的最大值通常发生在壁面上,而涡的核心显然不可能出现在壁面上。所以这种方法不适合用于提取边界层附近的涡结构。
OpenFOAM 中提供了两种方法来提取涡结构:Q 和 Lambda2。
可以用 $Q > 0$ 来作为涡结构存在的盘踞。
在 OpenFOAM 中,有一个程序用来计算 $Q$,名字就叫 Q
。在流场计算完毕以后,可以运行 Q
,然后在 paraview 中显示 Q
值大于 0 的等值面来显示涡的结构。只是,OpenFOAM 中 $Q$ 的计算用的是另一种方法:
1 | //Q.C |
代码里注释说这是另一种计算 $Q$ 的方法,与上面公式的计算方法差别很小。
另一种判据是 $\mathbf{W} \cdot \mathbf{W} + \mathbf{S} \cdot \mathbf{S}$ 的第二大特征值 $\lambda _ 2 < 0$。
在 OpenFOAM 中有一个程序用来计算 $\lambda _ 2$ :Lambda2
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23//Lambda2.C
const volTensorField gradU(fvc::grad(U));
volTensorField SSplusWW
(
(symm(gradU) & symm(gradU)) + (skew(gradU) & skew(gradU))
);
volScalarField Lambda2
(
IOobject
(
"Lambda2",
runTime.timeName(),
mesh,
IOobject::NO_READ,
IOobject::NO_WRITE
),
-eigenValues(SSplusWW)().component(vector::Y)
);
Info<< " Writing -Lambda2" << endl;
Lambda2.write();
注意,OpenFOAM 返回的是 $- \lambda _ 2$,所以,在计算了 Lambda2
后,需要通过 Lambda2
大于 0 的等值面来显示涡结构。本篇开头第一张图片,显示的是圆柱绕流的 Lambda2 = 500
等值面。
参考
Eugene de Villiers, The Potential of Large Eddy Simulation for the Modeling of Wall Bounded Flows, Ph.D Thesis, Imperial College of Science, 2005.
本篇介绍怎么在 OpenFOAM 中提取涡结构。
]]>divDevReff
函数来考虑雷诺应力项的作用。只是,细究起来,这个函数似乎有点小问题,本篇来探讨一下这些小问题。
OpenFOAM 中单相不可压缩求解器中,雷诺应力项调用的是湍流模型中的 divDevReff
函数。这个函数的返回值为
$$
\nabla \cdot(\nu_{eff}\nabla U)+\nabla \cdot\left [\nu_{eff}\nabla U^\mathrm{T}-\frac{1}{3} \nu_{eff} (\nabla \cdot U) \mathbf{I} \right ]
$$
这里有两个疑问,第一是为什么是 $\frac{1}{3}$ 而不是 $\frac{2}{3}$,对应到代码,即为什么用 dev
函数而不是 dev2
函数?第二个问题,根据涡粘度的 Boussinesq approximation,雷诺应力项
$$
\overline{u’_iu’_j}= -\nu_t \left( \frac{\partial \bar{u}_i}{\partial x_j} + \frac{\partial \bar{u}_j}{\partial x_i} \right) + \frac{2}{3}k \delta_{ij}
$$
中应该还包含湍动能 $k$,而 OpenFOAM 中的 divDefReff
函数是没有 $k$ 这一项的。
这个话题,在 cfd-online 上的一篇帖子里有深入的讨论。
对于第一个问题,我跟Holtzmann CFD 博客的博主 Tobias Holzmann 持同样观点,即$\frac{1}{3}$ 或 $\frac{2}{3}$ 不重要,因为对于不可压缩流动,连续方程为
$$
\nabla \cdot U = 0
$$
所以,收敛以后,$\frac{1}{3} \nu_{eff} (\nabla \cdot U)$ 这一项等于0. 但是在开始阶段,或者说还没有达到满足连续性的流场之前,这一项不为零。这里加上这一项是出于数值稳定性以及收敛速度的考虑,这一项不对收敛后的结果几乎没有影响。所以,$\frac{1}{3}$ 或 $\frac{2}{3}$ 不是很重要。
但是,可压缩湍流模型里必须是 $\frac{2}{3}$ ,因为这个 $\frac{2}{3}$ 是从 N-S 方程中严格推导而来的,而且,在可压缩的情形下,即使收敛以后,也有 $\nabla \cdot U \neq 0$ 。在 OpenFOAM=3.0 以后的版本里,不可压和可压缩湍流模型纳入到一个框架下了,两种情形下,都是用的 $\frac{2}{3}$ 这个系数。
对于第二个问题,有两种观点,一种认为 $k$ 的值相对很小,可以直接忽略不计。另一种观点认为,$k$ 被放到了压力项里,即,动量方程中的压力是雷诺时均压力与雷诺应力的各向同性分量(即 $\frac{2}{3}k$)之和:
$$
\bar{u}_j\frac{\partial\bar{u}_i}{\partial\bar{u}_j} =
-\frac{1}{\rho}\frac{\partial}{\partial x_i} \underbrace{\left[p + \frac{2}{3}k \right]}_{p^\prime}
+ \frac{\partial}{\partial x_j} \left[(\nu+\nu_{t}) \left( \frac{\partial \bar{u}_i}{\partial x_j} + \frac{\partial \bar{u}_j}{\partial x_i}\right) \right]
$$
这种观点可以在 Pope 2000 书第 88 页找到依据。在 “ The Finite Volume Method in Computational Fluid Dynamics: An Advanced Introduction with OpenFOAM® and Matlab®” 这本书的第 699 页,也提到 $k$ 是被放到压力项里去了,目的在于使动量方程中只含有 $\nu_t$ 这一个跟湍流有关的未知量。
不过,Tobias Holzmann 最后仍持前一种观点,即 $k$ 项被忽略了。理由是 OpenFOAM 中似乎找不到关于修改的压力场的代码,而且 OpenFOAM 那边也没见有人讨论说 OpenFOAM 中使用的是修改的压力场。
第二个问题目前还没有确切的结论,也尚不清楚这样处理对结果有多大影响。
divDevReff
函数来考虑雷诺应力项的作用。只是,细究起来,这个函数似乎有点小问题,本篇来探讨一下这些小问题。]]>
LIGGGHTS 中可以用 STL 格式的几何面来模拟复杂边界的问题。如果想用冻结粒子当作壁面,可以采用如下方法。1
2
3
4read_data test.dat
group Par_wall id <> 1 1000
fix fr Par_wall freeze
上述代码中,第一行是从外部文件中读取颗粒的信息;第二行是将ID在 1 到 1000 的粒子放到一个 group 里;第三行是将 Par_wall 这个 group 里的粒子冻结起来,具体的操作其实是将这些粒子的力归零,这样粒子将保持最初始的速度。如果将壁面粒子预先生成好,并将其初始速度设置为 0,便可以实现冻结粒子壁面了。
test.data 文件的数据格式如下,每一列数据的含义见注释:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26LAMMPS data file via write_data, version Version LIGGGHTS-PUBLIC 3.2.0, git commit 6de550fbf3b8451f51246aa3c76374012e935340 based on LAMMPS 23 Nov 2013, timestep = 0 ## 第一行随便是什么
5 atoms ## 颗粒数
1 atom types ## 颗粒的 type 数
## 模拟区域的大小
-5.0009999999999999e-01 5.0009999999999999e-01 xlo xhi
-2.0004000000000002e-01 2.0004000000000002e-01 ylo yhi
-2.0005500000000001e-01 3.4999999999999998e-01 zlo zhi
Atoms
#id type diameter density x y z i j k
1 1 2.9999999999999999e-02 2.5000000000000005e+03 -2.9626205235821884e-01 -1.7191257603378007e-01 -5.2585560979625336e-02 0 0 0
2 1 2.9999999999999999e-02 2.5000000000000005e+03 -3.1357080694177836e-01 -8.1292507237863978e-02 -3.0941241635135643e-02 0 0 0
3 1 2.9999999999999999e-02 2.5000000000000005e+03 -3.4986005571676082e-01 -4.6564797686740017e-02 -5.0161637377833301e-02 0 0 0
4 1 2.9999999999999999e-02 2.5000000000000005e+03 -3.2901105748658366e-01 1.1629149478965480e-01 -2.8537062345934828e-02 0 0 0
5 1 5.0000000000000003e-02 2.5000000000000000e+03 -3.9692279707164302e-01 1.5000972515153915e-01 -3.5647118241865984e-02 0 0 0
Velocities ## 如果粒子的初始速度为零,这一段可以删去。
#id vx vy vz omegax omegay omegaz
1 -1.5290519507823870e+00 1.0245516532619933e-01 -1.1594445288149451e+00 6.3791250045904881e+00 2.0674456758001139e+02 1.0276923966595568e+02
2 -2.1385398568904033e+00 -1.8858415304542153e-01 -9.4897293591801291e-01 1.2732686070189061e+01 1.9114652955524940e+02 -4.8862922016708987e+00
3 -2.1931823490540205e+00 1.2314081721772643e-01 -1.1305039942880526e+00 -7.7211996358126047e+00 1.8655504536271400e+02 -3.5674698533544941e+01
4 -2.3661710510727509e+00 6.5301832663338024e-03 -9.2367025774174294e-01 -5.7926985652143115e-01 1.7594397127744105e+02 6.1151183183219171e+00
5 -2.6032940321288258e+00 1.7791968545582579e-01 -1.0893683893889663e+00 -1.6450273309025711e+01 6.8599979334439681e+01 3.4617478295022179e+00
上述能实现静止的壁面,如果希望用粒子来实现运动壁面(比如旋转),则可以用 move 命令:1
2group rotateWall id <> 1001 2000 # 将 1001 <= id <= 2000 的粒子放到 group rotateWall 里
fix mov rotateWall move rotate -19.8 0 0 1 0 0 8
move 命令有不同的模式,这里用的是 rotate,用这个命令以后,rotateWall 这个 group 里的粒子,将按照指定的参数来进行旋转运动,而不再是根据其受力来更新速度和位置。参数的含义分别为:起始点坐标(x,y,z);旋转轴的指向(x,y,z);周期(转一圈的时间)。
最后再介绍几个小 tips:
1 | neigh_modify delay 0 exclude group Par_wall Par_wall |
这条命令将防止在 Par_wall 这个 group 里的粒子彼此之间建立碰撞对。
]]>这个类型的壁面函数,结构比较简单,计算的是每一个壁面边界面上的湍流粘度 $\nu_t$。nutWallFunction
是虚基类,其中定义了一个纯虚函数 calcNut
1
virtual tmp<scalarField> calcNut() const = 0;
并且在 updateCoeffs
函数中,将 calcNut
的返回值赋值给边界面1
2
3
4
5
6
7
8
9
10
11void nutWallFunctionFvPatchScalarField::updateCoeffs()
{
if (updated())
{
return;
}
operator==(calcNut());
fixedValueFvPatchScalarField::updateCoeffs();
}
这样,在具体的那些计算 $\nu_t$ 的壁面函数中,只需要看 calcNut
的返回值就可以了。
1 | tmp<scalarField> nutkWallFunctionFvPatchScalarField::calcNut() const |
这里,仍然是分情况处理yPlus < yPlusLam_
时,壁面上的 nut
设为0;yPlus > yPlusLam_
时
$$
\nu_t = \nu \cdot \left( \frac{\kappa y^+}{\ln(Ey^+)}-1 \right)
$$
这里实现的其实就是标准壁面函数。理论上讲,这里的计算只在粘性底层和对数区是有效的,所以,使用这个壁面条件的时候,要尽量壁面网格落在过渡区,否则可能会引入较大误差。
顺带提一下,这里还定义了一个 yPlus
函数,用来计算 $y^+$,这个函数在这里没有调用,不过在其他代码中需要 $y^+$ 的时候会调用这个函数。比如,计算 $y^+$ 的应用 yPlusRAS
就是调用这里的 yPlus
函数来计算 $y^+$。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16tmp<scalarField> nutkWallFunctionFvPatchScalarField::yPlus() const
{
const label patchi = patch().index();
const turbulenceModel& turbModel =
db().lookupObject<turbulenceModel>("turbulenceModel");
const scalarField& y = turbModel.y()[patchi];
const tmp<volScalarField> tk = turbModel.k();
const volScalarField& k = tk();
tmp<scalarField> kwc = k.boundaryField()[patchi].patchInternalField();
const tmp<volScalarField> tnu = turbModel.nu();
const volScalarField& nu = tnu();
const scalarField& nuw = nu.boundaryField()[patchi];
return pow025(Cmu_)*y*sqrt(kwc)/nuw;
}
1 | tmp<scalarField> nutUWallFunctionFvPatchScalarField::calcNut() const |
这个壁函数的 $y^+$ 的计算方式跟 nutkWallFunction
有点区别。经过摸索,这里 calcYPlus
函数中的那段 do ... while
循环的原理如下:
对数律可以表达如下:
$$
U^+ = \frac{U_p}{u_\tau}=\frac{1}{\kappa}\ln(Ey^+)
$$
其中 $U_p$ 等于壁面上的速度减去壁面所属网格中心的速度。
经过简单变形
$$
\frac{U_p}{ y u_\tau/\nu }\cdot (y/\nu)=\frac{U_p}{ y^+}\cdot (y/\nu)=\frac{1}{\kappa}\ln(Ey^+)
$$
整理得
$$
y^+ \ln(Ey^+) - \frac{\kappa y U_p}{\nu}=0
$$
这是一个 $y^+$ 的一元方程,可以通过牛顿迭代来求解
$$
y^+_{n+1} = y^+_{n} - \frac{f(y^+)}{f^{\prime}(y+)} = y^+_{n}-\frac{y_n^+ \ln(Ey_n^+) - \frac{\kappa y U_p}{\nu}}{1+\ln(Ey_n^+)} = \frac{y_n^+ + \frac{\kappa y U_p}{\nu}}{1+\ln(Ey_n^+)}
$$
上面代码里的 do ... while
循环,正是在做这个迭代求解,初始值选择的是 yPlusLam
,这个值在前面提过了。
求出 $y^+$ 以后,$\nu_t$ 计算如下
$$
\nu_t = \nu \cdot \left( \frac{\kappa y^+}{\ln(Ey^+)}-1 \right)
$$
与 nutkWallFunction
形式是一样的。
这个壁面函数,求壁面上的 $\nu_t$ 时使用的对数律方程,所以,理论上这个壁面函数应该只适用于第一层网格落在对数层的情形。
1 | tmp<scalarField> nutLowReWallFunctionFvPatchScalarField::calcNut() const |
$y^+$ 的计算也值得注意:1
2
3
4
5
6
7
8
9
10
11
12
13tmp<scalarField> nutLowReWallFunctionFvPatchScalarField::yPlus() const
{
const label patchi = patch().index();
const turbulenceModel& turbModel =
db().lookupObject<turbulenceModel>("turbulenceModel");
const scalarField& y = turbModel.y()[patchi];
const tmp<volScalarField> tnu = turbModel.nu();
const volScalarField& nu = tnu();
const scalarField& nuw = nu.boundaryField()[patchi];
const fvPatchVectorField& Uw = turbModel.U().boundaryField()[patchi];
return y*sqrt(nuw*mag(Uw.snGrad()))/nuw;
}
$$
y^+ = \frac{y\sqrt{\nu \cdot |\frac{U_w-U_c}{d}|}}{\nu}
$$
注意由于 $\nu_t = 0$ ,所以 $\frac{\tau_w}{\rho} = \nu \cdot |\frac{U_w-U_c}{d}|$,所以,$\sqrt{\nu \cdot |\frac{U_w-U_c}{d}|}=\sqrt{\frac{\tau_w}{\rho}}=u_\tau$ 。
1 | tmp<scalarField> nutUSpaldingWallFunctionFvPatchScalarField::calcNut() const |
calcUtau
函数,其实是在用牛顿法迭代求解 $y^+$,进而得到 $u_\tau$ 的值。calcNut
函数中
$$
\frac{u_\tau ^2}{|\frac{U_w-U_c}{d}|} - \nu = \frac{\tau_w}{|\frac{U_w-U_c}{d}|} -\nu = \nu_{eff} - \nu = \nu_t
$$
这个壁面函数使用的是从粘性底层连续变化到对数层的 $y^+ \text{-} u^+$ 关系式,所以,这个可以认为是网格无关的,即不管第一层网格落在哪个区,都是有效的。如果网格无法做到全部位于粘性层或者对数区,建议用这个壁面条件。
这个壁面函数,需要从外部读取一个 $U^+ \text{-}\,Re_y$ 数据表,通过从这个数据表插值来得到 $U^+$ 的值。其中 $Re_y=yU/\nu$ 。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25// 构造函数
nutUTabulatedWallFunctionFvPatchScalarField::
nutUTabulatedWallFunctionFvPatchScalarField
(
const fvPatch& p,
const DimensionedField<scalar, volMesh>& iF,
const dictionary& dict
)
:
nutWallFunctionFvPatchScalarField(p, iF, dict),
uPlusTableName_(dict.lookup("uPlusTable")),
uPlusTable_
(
IOobject
(
uPlusTableName_,
patch().boundaryMesh().mesh().time().constant(),
patch().boundaryMesh().mesh(),
IOobject::MUST_READ_IF_MODIFIED,
IOobject::NO_WRITE,
false
),
true
)
{}
$U^+$ 和 $\nu_t$ 分别由函数 calcUPlus
和 calcNut
来计算。
1 | tmp<scalarField> nutUTabulatedWallFunctionFvPatchScalarField::calcNut() const |
注意这里 calcUPlus
用的是 interpolateLog10
函数来插值,这个函数的定义为1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28template<class Type>
Type Foam::uniformInterpolationTable<Type>::interpolateLog10
(
scalar x
) const
{
if (log10_)
{
if (x > 0)
{
x = ::log10(x);
}
else if (bound_ && (x <= 0))
{
x = x0_;
}
else
{
FatalErrorIn
(
"uniformInterpolationTable<Type>::interpolateLog10(scalar x)"
) << "Table " << name() << nl
<< "Supplied value must be greater than 0 when in log10 mode"
<< nl << "x=" << x << nl << exit(FatalError);
}
}
return interpolate(x); // 这个是普通的线性插值函数
}
即计算 x
的对数(log10),在将计算结果用来进行线性插值。所以,用这个壁面函数的时候,要注意你所提供的数据表是普通线性坐标的还是对数坐标的。
基本上常见的处理壁面上的湍流粘度的方法就是以上几种了。OpenFOAM 中还提供了几个能处理粗糙壁面的壁面函数( nutURoughWallFunction
, nutkRoughWallFunction
),以及处理大气层边界的(nutkAtmRoughWallFunction
,需要跟 atmBoundaryLayerInletVelocity
这个入口边界配合使用 ),细节这里不再详述了,有需要时可以去看相关代码,代码结构是类似的,只是具体计算公式不一样。
本篇来看看 OpenFOAM 中的 epsilonWallFunction
,共有两个: epsilonWallFunction
和 epsilonLowReWallFunction
。
epsilonWallFunction
代码比前面的 kqRWallFunction
复杂多了,主要原因在于这里需要得到的是 epsilon
在临近网格的值,而且,需要考虑包含两个边界面的网格。这里先来梳理代码的脉络,然后再看具体的计算细节。
外部调用的主要是 updateCoeffs()
函数,所以,从这个函数看起。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38void epsilonWallFunctionFvPatchScalarField::updateCoeffs()
{
if (updated())
{
return;
}
const turbulenceModel& turbulence =
db().lookupObject<turbulenceModel>(turbulenceModel::typeName);
setMaster();
if (patch().index() == master_)
{
createAveragingWeights();
calculateTurbulenceFields(turbulence, G(true), epsilon(true));
}
const scalarField& G0 = this->G();
const scalarField& epsilon0 = this->epsilon();
typedef DimensionedField<scalar, volMesh> FieldType;
FieldType& G =
const_cast<FieldType&>
(
db().lookupObject<FieldType>(turbulence.GName())
);
//这里是获取内部场,所以,修改这里的引用 "epsilon",相当于修改 epsilon 的内部场值。
FieldType& epsilon = const_cast<FieldType&>(dimensionedInternalField());
forAll(*this, faceI)
{
label cellI = patch().faceCells()[faceI];
G[cellI] = G0[cellI];
epsilon[cellI] = epsilon0[cellI];
}
fvPatchField<scalar>::updateCoeffs();
}
一步一步来看。首先是调用了 setMaster()
函数,来看看这个函数以及相关的一个函数 epsilonPatch
的代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41void epsilonWallFunctionFvPatchScalarField::setMaster()
{
if (master_ != -1) // 如果当前处理的边界的 master_ != -1,说明它已被处理过,直接返回
{
return;
}
const volScalarField& epsilon =
static_cast<const volScalarField&>(this->dimensionedInternalField());
const volScalarField::GeometricBoundaryField& bf = epsilon.boundaryField();
label master = -1;
forAll(bf, patchI)
{
if (isA<epsilonWallFunctionFvPatchScalarField>(bf[patchI]))
{
epsilonWallFunctionFvPatchScalarField& epf = epsilonPatch(patchI);
if (master == -1) // 只有头一个被处理的边界满足这个条件
{
master = patchI;
}
epf.master() = master; // 这意味着所有边界的 master_ 数据成员都将赋值为头一个被处理的边界的编号,即第一个被处理的边界是master
}
}
}
epsilonWallFunctionFvPatchScalarField&
epsilonWallFunctionFvPatchScalarField::epsilonPatch(const label patchI)
{
const volScalarField& epsilon =
static_cast<const volScalarField&>(this->dimensionedInternalField());
const volScalarField::GeometricBoundaryField& bf = epsilon.boundaryField();
const epsilonWallFunctionFvPatchScalarField& epf =
refCast<const epsilonWallFunctionFvPatchScalarField>(bf[patchI]);
return const_cast<epsilonWallFunctionFvPatchScalarField&>(epf);
}
从上述代码可以看出, epsilonPatch
函数需要一个参数,这个参数的含义是某一个边界的序号,返回的是指向这个边界的一个 epsilonWallFunctionFvPatchScalarField
类型的引用。
在此基础上,再来看 setMaster
。先判断当前边界的数据成员 master_
是否不等于-1,如果成立则不做任何操作,直接返回;否则,先获取到 epsilon
的所有边界,存在变量 bf
中,然后,遍历 bf
,如果边界的类型是 epsilonWallFunctionFvPatchScalarField
,则判断临时变量 master
是否等于 -1
,等于则将边界的序号 patchI
赋值给 master
,并临时变量 master
的值赋给 patchI
对应边界的数据成员 master_
。 举个例子,假设有一个算例,有两个边界上使用了 epsilonWallFunctionFvPatchScalarField
类型的边界条件,两个边界的编号分别是 patchI = 0
和 patchI = 1
。则在上述循环过程中,当 patchI = 0
时, master == -1
肯定成立。于是,patchI = 0
对应边界的数据成员 master_
被赋值为0;而当遍历到 patchI = 1
时, 此时master = 0
,所以,结果是 patchI = 1
的边界的数据成员 master_
也被赋值为0。
继续向下看,如果 patch.index() == master_
,则调用两个函数。这个怎么理解呢?还以上面的那个简单例子来说明。注意,在外部调用边界条件的时候,也是会依次调用一个场的所有边界的边界条件的。在这里的简单例子中,有两个边界的类型是 epsilonWallFunctionFvPatchScalarField
,所以,我们假设调用 patchI = 0
对应的边界时,由于初始化时数据成员 master_
赋值为 -1
,所以,调用 patchI = 0
的边界时, setMaster
函数中的操作会进行。而根据上面的分析,调用 patchI = 0
的边界时, setMaster
函数同时也将 patchI = 1
边界的数据成员 master_
赋值为 0
了,所以,在外部调用 patchI = 1
的边界时, setMaster
函数将不作任何操作,直接返回。同样的,在外部调用 patchI = 0
的边界时,patch.index() == master_
条件是成立的,所以 createAveragingWeights()
和 calculateTurbulenceFields(turbulence, G(true), epsilon(true));
两个语句将会执行;而在外部调用 patchI = 1
边界时,由于 patch.index() == master_
不成立,这两个语句将不执行。
再继续往前看, const scalarField& G0 = this->G(); const scalarField& epsilon0 = this->epsilon();
,这里是将成员函数 G
和 epsilon
的返回值分别赋给变量 G0
和 epsilon0
。开看一下成员函数的定义1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25scalarField& epsilonWallFunctionFvPatchScalarField::G(bool init)
{
if (patch().index() == master_) // 只有头一个被处理的边界满足这个条件
{
if (init) // init 缺省值是 false
{
G_ = 0.0;
}
return G_;
}
return epsilonPatch(master_).G(); // 对于不是 master 的边界,返回master边界的数据成员 G_
}
scalarField& epsilonWallFunctionFvPatchScalarField::epsilon(bool init)
{
if (patch().index() == master_)
{
if (init)
{
epsilon_ = 0.0;
}
return epsilon_;
}
return epsilonPatch(master_).epsilon(init);
}
类似的,对于 patchI = 0
, patch().index() == master_
,所以返回值为 patchI = 0
边界的数据成员 G_
或 epsilon_
(init
的缺省值是 false
);而对于 patchI = 1
边界,返回的是 patchI = master_
对应边界的数据成员 G_
或 epsilon_
,而根据上面的分析, patchI= 1
的边界的数据成员 master_ = 0
,因此, patchI = 1
的边界的成员函数返回的是 patchI = 0
边界的相应的数据成员。
再往下的内容就很简单了,只是将得到的 G0
和 epsilon0
的值分别赋给当前边界的临近边界网格而已。
到此,代码的框架就基本清晰了,小结一下就是,如果对于某个算例,有多个边界上需要用到 epsilonWallFunctionFvPatchScalarField
类型的边界条件,则,编号更小的那个边界将会被设置成 master
。所有的相关计算都在调用 master
边界的时候进行,非 master
的边界,则只需要从 master
那里读取结果即可!
接下来看看外部调用 master
边界的时候,具体做了哪些计算,主要就是看 createAveragingWeights()
和 calculateTurbulenceFields(turbulence, G(true), epsilon(true));
这两条语句了。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136void epsilonWallFunctionFvPatchScalarField::createAveragingWeights()
{
const volScalarField& epsilon =
static_cast<const volScalarField&>(this->dimensionedInternalField());
const volScalarField::GeometricBoundaryField& bf = epsilon.boundaryField();
const fvMesh& mesh = epsilon.mesh();
if (initialised_ && !mesh.changing())
{
return;
}
volScalarField weights
(
IOobject
(
"weights",
mesh.time().timeName(),
mesh,
IOobject::NO_READ,
IOobject::NO_WRITE,
false // do not register
),
mesh,
dimensionedScalar("zero", dimless, 0.0)
);
DynamicList<label> epsilonPatches(bf.size());
//遍历所有边界,如果边界类型是 epsilonWallFunctionFvPatchScalarField 则将该边界放到 epsilonPatches 这个动态 list 中。
forAll(bf, patchI)
{
if (isA<epsilonWallFunctionFvPatchScalarField>(bf[patchI]))
{
epsilonPatches.append(patchI);
const labelUList& faceCells = bf[patchI].patch().faceCells();
forAll(faceCells, i)
{
label cellI = faceCells[i];
// weight 衡量的是网格cellI有多少个边界面使用了 epsilonWallFunctionFvPatchScalarField 类型的边界条件
weights[cellI]++;
}
}
}
cornerWeights_.setSize(bf.size());
// 遍历所有 epsilonWallFunctionFvPatchScalarField 类型的边界
forAll(epsilonPatches, i)
{
label patchI = epsilonPatches[i];
const fvPatchScalarField& wf = weights.boundaryField()[patchI];
//cornerWeights_存储的所有边界面的weight的倒数,边界面的weight等于其所属网格的weight。所以,如果有一个网格包含两个使用epsilonWallFunction的边界面,那么根据上面的计算,这个网格的weight将是 2,而这两个边界面的 cornerWeights_ 则都是 1/2。
cornerWeights_[patchI] = 1.0/wf.patchInternalField();
}
// 将数据成员 G_ 和 epsilon_ 初始化为0
G_.setSize(dimensionedInternalField().size(), 0.0);
epsilon_.setSize(dimensionedInternalField().size(), 0.0);
initialised_ = true;
}
void epsilonWallFunctionFvPatchScalarField::calculateTurbulenceFields
(
const turbulenceModel& turbulence,
scalarField& G0,
scalarField& epsilon0
)
{
// accumulate all of the G and epsilon contributions
//cornerWeights_ 是一个二维 list,这里是遍历这个list 的第一层
forAll(cornerWeights_, patchI)
{
if (!cornerWeights_[patchI].empty()) // 如果是empty,意味着这个对应的边界不是epsilonWallFunction类型,所以就不需要考虑
{
epsilonWallFunctionFvPatchScalarField& epf = epsilonPatch(patchI);
const List<scalar>& w = cornerWeights_[patchI];
// 非 empty 则调用 calculate 函数更新 G0 和 epsilon 的值
epf.calculate(turbulence, w, epf.patch(), G0, epsilon0);
}
}
// apply zero-gradient condition for epsilon
forAll(cornerWeights_, patchI)
{
if (!cornerWeights_[patchI].empty())
{
epsilonWallFunctionFvPatchScalarField& epf = epsilonPatch(patchI);
// 对 epsilon 使用 零梯度边界条件,即将上面计算得到的临近壁面网格的epsilon的值存储在壁面。
epf == scalarField(epsilon0, epf.patch().faceCells());
}
}
}
void epsilonWallFunctionFvPatchScalarField::calculate
(
const turbulenceModel& turbulence,
const List<scalar>& cornerWeights,
const fvPatch& patch,
scalarField& G,
scalarField& epsilon
)
{
const label patchI = patch.index();
const scalarField& y = turbulence.y()[patchI];
const scalar Cmu25 = pow025(Cmu_);
const scalar Cmu75 = pow(Cmu_, 0.75);
const tmp<volScalarField> tk = turbulence.k();
const volScalarField& k = tk();
const tmp<volScalarField> tnu = turbulence.nu();
const scalarField& nuw = tnu().boundaryField()[patchI];
const tmp<volScalarField> tnut = turbulence.nut();
const volScalarField& nut = tnut();
const scalarField& nutw = nut.boundaryField()[patchI];
const fvPatchVectorField& Uw = turbulence.U().boundaryField()[patchI];
const scalarField magGradUw(mag(Uw.snGrad()));
// Set epsilon and G
遍历参数 patch 对应的边界的每一个面
forAll(nutw, faceI)
{
label cellI = patch.faceCells()[faceI];
scalar w = cornerWeights[faceI];
epsilon[cellI] += w*Cmu75*pow(k[cellI], 1.5)/(kappa_*y[faceI]);
G[cellI] +=
w
*(nutw[faceI] + nuw[faceI])
*magGradUw[faceI]
*Cmu25*sqrt(k[cellI])
/(kappa_*y[faceI]);
}
}
calculate
函数中进行的是实际的计算过程,主要是更新了临近壁面网格的 epsilon
和 G
的值,计算公式如下:
$$
\varepsilon_c = \frac{1}{N} \sum_{f=i}^{N}\left( \frac{c_\mu^{3/4} k_C^{3/2}}{\kappa y_i}\right) \\
\text{相当于} \quad \quad \quad
\varepsilon ^+ = \frac{1}{\kappa y^+} \quad \quad \quad \quad \quad \quad \quad \quad \quad \quad \quad
$$
$$
G_c = \frac{1}{N} \sum_{f=i}^{N}\left( \frac{(\nu + \nu_t)\cdot |\tfrac{U_i-U_c}{d}|\cdot c_\mu^{1/4} k_C^{1/2}}{\kappa y_i}\right)
$$
这里的 Uw.snGrad()
是 fvPatchFields<Type>
类的成员函数:1
2
3
4
5template<class Type>
Foam::tmp<Foam::Field<Type> > Foam::fvPatchField<Type>::snGrad() const
{
return patch_.deltaCoeffs()*(*this - patchInternalField());
}
公式中下标 c
表示临近边界的网格, i
表示网格 c
包含的某个边界面元。y
和 d
都表示边界面元所属网格中心到该面元的垂直距离。
还有一个重要的函数, manipulateMatrix
1
2
3
4
5
6
7
8
9
10
11
12
13
14void epsilonWallFunctionFvPatchScalarField::manipulateMatrix
(
fvMatrix<scalar>& matrix
)
{
if (manipulatedMatrix())
{
return;
}
matrix.setValues(patch().faceCells(), patchInternalField());
fvPatchField<scalar>::manipulateMatrix(matrix);
}
这个函数的功能是修改 matrix 中的值,将当前 patch 每一个面所属网格的值更新到 matrix 中,参考这个帖子。
如果不是使用的低雷诺数湍流模型,则 $\varepsilon$ 应该使用这个边界条件。理论上,边界第一层网格应该设置在对数区。什么是低雷诺数湍流模型呢?这篇帖子的三楼有精彩的解释。
epsilonLowReWallFunction
继承自 epsilonWallFunction
,在此基础上,增加了一个成员函数 yPlusLam
,并重新定义了 calculate
函数1
2
3
4
5
6
7
8
9
10
11
12
13scalar epsilonLowReWallFunctionFvPatchScalarField::yPlusLam
(
const scalar kappa,
const scalar E
)
{
scalar ypl = 11.0;
for (int i=0; i<10; i++)
{
ypl = log(max(E*ypl, 1))/kappa;
}
return ypl;
}
这个跟 kLowReWallFunction
里是一样的,不再赘述。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48void epsilonLowReWallFunctionFvPatchScalarField::calculate
(
const turbulenceModel& turbulence,
const List<scalar>& cornerWeights,
const fvPatch& patch,
scalarField& G,
scalarField& epsilon
)
{
const label patchI = patch.index();
const scalarField& y = turbulence.y()[patchI];
const scalar Cmu25 = pow025(Cmu_);
const scalar Cmu75 = pow(Cmu_, 0.75);
const tmp<volScalarField> tk = turbulence.k();
const volScalarField& k = tk();
const tmp<volScalarField> tnu = turbulence.nu();
const scalarField& nuw = tnu().boundaryField()[patchI];
const tmp<volScalarField> tnut = turbulence.nut();
const volScalarField& nut = tnut();
const scalarField& nutw = nut.boundaryField()[patchI];
const fvPatchVectorField& Uw = turbulence.U().boundaryField()[patchI];
const scalarField magGradUw(mag(Uw.snGrad()));
// Set epsilon and G
forAll(nutw, faceI)
{
label cellI = patch.faceCells()[faceI];
scalar yPlus = Cmu25*sqrt(k[cellI])*y[faceI]/nuw[faceI];
scalar w = cornerWeights[faceI];
if (yPlus > yPlusLam_)
{
epsilon[cellI] = w*Cmu75*pow(k[cellI], 1.5)/(kappa_*y[faceI]);
}
else
{
epsilon[cellI] = w*2.0*k[cellI]*nuw[faceI]/sqr(y[faceI]);
}
G[cellI] =
w
*(nutw[faceI] + nuw[faceI])
*magGradUw[faceI]
*Cmu25*sqrt(k[cellI])
/(kappa_*y[faceI]);
}
}
这里需要根据 yPlus
和 yPlusLam_
的相对大小来选择不同的计算方式。只是,上面这段来自 OpenFOAM-2.3.1 的代码是有问题的!在OpenFOAM-3.0.1 中已经修复成如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 forAll(nutw, facei)
{
label celli = patch.faceCells()[facei];
scalar yPlus = Cmu25*sqrt(k[celli])*y[facei]/nuw[facei];
scalar w = cornerWeights[facei];
if (yPlus > yPlusLam_)
{
epsilon0[celli] += w*Cmu75*pow(k[celli], 1.5)/(kappa_*y[facei]);
G0[celli] +=
w
*(nutw[facei] + nuw[facei])
*magGradUw[facei]
*Cmu25*sqrt(k[celli])
/(kappa_*y[facei]);
}
else
{
epsilon0[celli] += w*2.0*k[celli]*nuw[facei]/sqr(y[facei]);
G0[celli] += G[celli];
}
}
}
yPlus > yPlusLam_
时,与 epsilonWallFunction
是一样的;yPlus < yPlusLam_
时
$$
\varepsilon_c = \frac{1}{N} \sum_{f=i}^{N}\left( \frac{2\cdot k_C \nu_i}{y^2_i}\right)
$$
这个公式等价于
$$
\varepsilon ^+ = 2\frac{k^+}{(y^+)^2}
$$
G
则取在湍流模型中定义的值,不作修改。 不过,这里 G0[celli] += G[celli]
意味着假设有一个网格有两个边界面,则这个网格的中计算得到的 G0
,将是在湍流模型中定义的该网格中的 G 值的 2 倍,即认为每一个边界面对都该网格内的湍动能生成有贡献。
这个边界是给低雷诺数的 $k-\varepsilon$ 模型以及 $v^2\text{-}f$ 模型使用的。用 OpenFOAM-3.0 以下版本的注意了,这些版本的 epsilonLowReWallFunction
有问题,一定不要忘了修正一下上面提到的那个bug !
OpenFOAM 中只提供了一个 omegaWallFunction
,这个壁面函数,属于一种自动壁面函数,能自动地根据 $y^+$ 的值来在粘性层和对数层切换,过渡层则采用粘性层和对数层混合的结果。omegaWallFunction
与 epsilonWallFunction
类似,也是需要计算 $\omega$ 和 $P_k$ 在临近边界网格里的值,因此也需要考虑一个网格包含两个以上边界面的情况。具体处理方法跟 epsilonWallFunction
是一样的 ,所以这里就不重复了,只看具体的计算 $\omega$ 和 $P_k$ 的公式1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38void omegaWallFunctionFvPatchScalarField::calculate
(
const turbulenceModel& turbulence,
const List<scalar>& cornerWeights,
const fvPatch& patch,
scalarField& G,
scalarField& omega
)
{
const label patchI = patch.index();
const scalarField& y = turbulence.y()[patchI];
const scalar Cmu25 = pow025(Cmu_);
const tmp<volScalarField> tk = turbulence.k();
const volScalarField& k = tk();
const tmp<volScalarField> tnu = turbulence.nu();
const scalarField& nuw = tnu().boundaryField()[patchI];
const tmp<volScalarField> tnut = turbulence.nut();
const volScalarField& nut = tnut();
const scalarField& nutw = nut.boundaryField()[patchI];
const fvPatchVectorField& Uw = turbulence.U().boundaryField()[patchI];
const scalarField magGradUw(mag(Uw.snGrad()));
// Set omega and G
forAll(nutw, faceI)
{
label cellI = patch.faceCells()[faceI];
scalar w = cornerWeights[faceI];
scalar omegaVis = 6.0*nuw[faceI]/(beta1_*sqr(y[faceI]));
scalar omegaLog = sqrt(k[cellI])/(Cmu25*kappa_*y[faceI]);
omega[cellI] += w*sqrt(sqr(omegaVis) + sqr(omegaLog));
G[cellI] +=
w
*(nutw[faceI] + nuw[faceI])
*magGradUw[faceI]
*Cmu25*sqrt(k[cellI])
/(kappa_*y[faceI]);
}
}
这里, omegaVis
和 omegaLog
分别指的是在假定第一层网格位于粘性底层和对数层时得到的 omega
的解析解
$$
\omega_{Vis} = \frac{6.0\nu}{\beta_1y^2} \
\omega_{Log} = \frac{k_C^{1/2}}{C_\mu^{1/4}\kappa y}
$$
然后,将 $\omega_{Vis}$ 和 $\omega_{Log}$ 用一个函数混合起来,就得到了
$$
\omega = \sqrt{\omega_{Vis}^2 + \omega_{Log}^2}
$$
只是,这里的湍动能生成项,却似乎并没有使用混合的方法,而是用的基于对数律的公式:
$$
G = \frac{(\nu + \nu_t)\cdot |\frac{U_c-U_w}{d}|\cdot C_\mu^{1/4}k_C^{1/2}}{\kappa y}
$$
$omega$ 方程是能直接积分到壁面,所以,如果使用基于 $\omega$ 的湍流模型,$\omega$ 变量直接使用这个边界条件就可以了。
]]>OpenFOAM 中提供了两种 $k$ 的壁面函数, kqRWallFunction
和 kLowReWallFunction
。
kqRWallFunction
其实就是 zeroGradient
,无需多言。除非使用 $v^2\text{-}f$ 模型,一般情况下 $k$ 应该使用这个边界条件。
kLowReWallFunction
这个壁面函数应该是可以用于低雷诺数模型的。该壁面函数继承自 fixedValue
:
1 | class kLowReWallFunctionFvPatchScalarField |
核心的函数是以下两个:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67scalar kLowReWallFunctionFvPatchScalarField::yPlusLam
(
const scalar kappa,
const scalar E
)
{
scalar ypl = 11.0;
for (int i=0; i<10; i++)
{
ypl = log(max(E*ypl, 1))/kappa;
}
return ypl;
}
void kLowReWallFunctionFvPatchScalarField::updateCoeffs()
{
if (updated())
{
return;
}
const label patchI = patch().index();
const turbulenceModel& turbulence =
db().lookupObject<turbulenceModel>("turbulenceModel");
const scalarField& y = turbulence.y()[patchI];
const tmp<volScalarField> tk = turbulence.k();
const volScalarField& k = tk();
const tmp<volScalarField> tnu = turbulence.nu();
const scalarField& nuw = tnu().boundaryField()[patchI];
const scalar Cmu25 = pow025(Cmu_);
scalarField& kw = *this; // 更新 kw 相当于更新壁面上的 k 值。
// Set k wall values
forAll(kw, faceI)
{
label faceCellI = patch().faceCells()[faceI];
scalar uTau = Cmu25*sqrt(k[faceCellI]);
scalar yPlus = uTau*y[faceI]/nuw[faceI];
if (yPlus > yPlusLam_)
{
scalar Ck = -0.416;
scalar Bk = 8.366;
kw[faceI] = Ck/kappa_*log(yPlus) + Bk;
}
else
{
scalar C = 11.0;
scalar Cf = (1.0/sqr(yPlus + C) + 2.0*yPlus/pow3(C) - 1.0/sqr(C));
kw[faceI] = 2400.0/sqr(Ceps2_)*Cf;
}
kw[faceI] *= sqr(uTau);
}
fixedValueFvPatchField<scalar>::updateCoeffs();
// TODO: perform averaging for cells sharing more than one boundary face
}
先在函数里计算 ypl
的值, updateCoeffs
函数里根据 yPlus
与这个 ypl
的值来相对大小而采取不同的方法来计算壁面上的 $k_w$。 ypl
的计算是一个迭代过程
$$
ypl = \frac{\log(\max(E*ypl,1.0))}{\kappa}
$$
初始值为 ypl = 11.0
,迭代10次,最终结果应该是 ypl = 11.5301073043272
。
$y^+$ 定义为:
$$
u_\tau = C_\mu^{1/4 }\sqrt{k_c} \
y^+ = \frac{u_\tau \cdot y}{\nu_w}
$$
壁面上的k计算方法如下:如果 $y^+ > ypl$,则
$$
k^+ _w = \frac{C_k}{\kappa}\ln(y^+) + B_k
$$
否则
$$
k^+ _w = \frac{2400}{C_{eps2}^2}\cdot \left[ \frac{1}{(y^+ + C)^2} + \frac{2y^+}{C^3} - \frac{1}{C^2}\right ]
$$
最终,壁面上的值为 $k_w=k^+ _w u_\tau ^2 =k^+ _w C_\mu^{1/2}k_c$ 。
以上公式中,下标 $c$ 表示壁面单元所述网格的值,下标 $w$ 表示当前壁面上的值。
这个壁面函数参考文献 “Kalitzin, G., Medic, G., Iaccarino, G., Durbin, P., 2005. Near-wall behavior of RANS turbulence models and implications for wall functions. J. Comput. Phys. 204, 265–291. doi:10.1016/j.jcp.2004.10.018”,是为 $v^2\text{-}f$ 模型设计的。
上面提到了 $v^2\text{-}f$ 模型,所以这里顺便来看看$v^2$ 和 $f$ 的壁面函数。这里参考的也是上面提到的那篇参考文献。
1 | forAll(v2, faceI) |
yPlus > yPlusLam_
时,
$$
v^2 = u_\tau^2 \cdot \left[ \frac{C_{v2}}{\kappa}\ln(y^+) + B_{v2} \right]
$$
与文献中的无量纲形式 $(\overline{v^2})^{^+} = \frac{C_{v2}}{\kappa}\ln(y^+) + B_{v2} $ 一致。
yPlus < yPlusLam\_
时,
$$
v^2 = u_\tau^2 \cdot C_{v2}(y^+)^2
$$
与无量纲形式 $(\overline{v^2})^{^+} = C_{v2}(y^+)^2$ 一致。
1 | forAll(f, faceI) |
yPlus > yPlusLam_
时,
$$
f = \frac{N \cdot v^2\cdot \varepsilon}{k^2 u_\tau^2}
$$
这似乎与文献中的无量纲形式
$$
f^+ = N \frac{(\overline{v^2})^{^+}}{(k^+)^2}\varepsilon^+
$$
不一致!是 bug 还是我推导出错了?存疑…
yPlus < yPlusLam_
时,文献给出的公式是
$$
f^+ = \frac{-4(6-N)(\overline{v^2})^{^+}}{\varepsilon^+ (y^+)^4}
$$
当 N=6
时,可以得到 $f^+ = 0$ 。
按理说,$v^2$ 和 $f$ 应该跟 $\varepsilon$ 和 $\omega$ 那样(见后文),计算第一层网格内的值,并且考虑一个网格有多个边界面的情形。OpenFOAM 目前计算的是每一个边界面元上的值,不知道这两种方式对结果有多大影响。
]]>