本篇简要分析不可压缩的 SpalartAllmaras 模型的代码。主要内容包括模型输运方程的代码说明,以及一些使用方面的细节。
湍流模型代码实例
这一部分分析几个 OpenFOAM 里自带的湍流模型, 并探讨修改或者添加新湍流模型的方法。
1 SpalartAllmaras 模型
1.1. 模型分析
SpalartAllmaras 模型是一方程模型,它只需要求解一个输运方程。OpenFOAM 中的 SpalartAllmaras 模型是在原始论文[1] 的基础上,引入了 Ashford [2] 对这个模型的修正。下面来看这个模型在 OpenFOAM 中的实现,代码位于 src/turbulenceModels/incompressible/RAS/SpalartAllmaras
首先是头文件, SpalartAllmaras.H
头文件开始,声明了一个类:SpalartAllmaras1
2
3class SpalartAllmaras
:
public RASModel
说明这个类是公有继承 RASModel
类的。
接下来是一些数据成员以及成员函数的声明,这里无需赘述,直接来看函数的具体定义吧。
函数的具体定义,在SpalartAllmaras.C
里(注意到 OpenFOAM 的代码几乎都是这样讲声明和定义分开,这样做的目的,是为了防止重复声明的问题,更详细的说明,见我的另一篇博文)。注意,湍流模型的定义,最终是为了在求解器中进行调用的,所以,先来回顾一下湍流模型贡献给求解器中动量方程中那一项:1
turbulence->divDevReff(U)
这一项,不同求解器可能会有不同,但大致的形式是这样的。
很显然,这个 divDevReff
应该是湍流模型类的成员函数,实际也的确如此, SpalartAllmaras.C
有这个函数的定义:1
2
3
4
5
6
7
8
9
10tmp<fvVectorMatrix> SpalartAllmaras::divDevReff(volVectorField& U) const
{
const volScalarField nuEff_(nuEff());
return
(
- fvm::laplacian(nuEff_, U)
- fvc::div(nuEff_*dev(T(fvc::grad(U))))
);
}
这个函数,写成公式是如下形式
$$
-\nabla \cdot (\nu_{eff}\nabla U)-\nabla \cdot [\nu_{eff}dev(\nabla ^TU)]
$$
OpenFOAM 中的 dev
运算符定义为 $dev(T)=T-\frac{1}{3}tr(T)I$ , tr
为张量的迹,即主对角元素之和所以。对于张量 $\nabla ^TU$ ,$tr(\nabla ^TU) =\nabla \cdot U$,因此上式可以写为【注一】
$$
-\nabla \cdot (\nu_{eff}\nabla U)-\nabla \cdot [\nu_{eff}(\nabla^TU-\frac{1}{3}(\nabla \cdot U))]
$$
上述 divDevReff
函数中,变量 nuEff_
d的值是函数 nuEff()
的返回值,但是,在 SpalartAllmaras.C
中却未见 nuEff()
函数的定义。这里就要记住了,C++ 的类继承的一个特性是派生类会从基类继承其中定义的成员函数。所以看到本类中没定义的函数时,don’t panic,往基类找就是了。翻看基类 RASModel
的代码,可以很容易从 RASModel.C
中找出来 nuEff()
的定义:1
2
3
4
5
6
7virtual tmp<volScalarField> nuEff() const
{
return tmp<volScalarField>
(
new volScalarField("nuEff", nut() + nu())
);
}
函数 nut()
的定义可以从 SpalartAllmaras.H
中找到,可是 nu()
呢?虽然从名字可以猜到它返回的是分子粘度(可见变量命名的重要性啊),可是,它的定义在哪里? RASModel.C
也没有啊…
答案是,继续向上找,RASModel 类没有,那就去更上一层的基类,turbulenceModel。果然,在 turbulenceModel.C
中能找到其定义:1
2
3
4inline tmp<volScalarField> nu() const
{
return transportModel_.nu();
}
transportModel_
又是什么鬼?这个不是本篇博文的内容,该回到正题了,这里应该关注的是湍流粘度 nut_
。
湍流模型不可能仅仅是给求解器贡献那一项,湍流粘度应该也随着求解器的运行而更新。是这样的,求解器中的1
turbulence->correct();
这句代码负责湍流粘度的更新。这里又涉及到一个湍流类的成员函数: corretc
。这个函数是湍流模型类的核心,因为湍流模型输运方程的求解,湍流粘度的更新都在这个函数里面。下面来看 SpalartAllmaras
模型的 correct
函数: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
45void SpalartAllmaras::correct()
{
RASModel::correct();
if (!turbulence_)
{
// Re-calculate viscosity
nut_ = nuTilda_*fv1(this->chi());
nut_.correctBoundaryConditions();
return;
}
if (mesh_.changing())
{
d_.correct();
}
const volScalarField chi(this->chi());
const volScalarField fv1(this->fv1(chi));
const volScalarField Stilda
(
fv3(chi, fv1)*::sqrt(2.0)*mag(skew(fvc::grad(U_)))
+ fv2(chi, fv1)*nuTilda_/sqr(kappa_*d_)
);
tmp<fvScalarMatrix> nuTildaEqn
(
fvm::ddt(nuTilda_)
+ fvm::div(phi_, nuTilda_)
- fvm::laplacian(DnuTildaEff(), nuTilda_)
- Cb2_/sigmaNut_*magSqr(fvc::grad(nuTilda_))
==
Cb1_*Stilda*nuTilda_
- fvm::Sp(Cw1_*fw(Stilda)*nuTilda_/sqr(d_), nuTilda_)
);
nuTildaEqn().relax();
solve(nuTildaEqn);
bound(nuTilda_, dimensionedScalar("0", nuTilda_.dimensions(), 0.0));
nuTilda_.correctBoundaryConditions();
// Re-calculate viscosity
nut_.internalField() = fv1*nuTilda_.internalField();
nut_.correctBoundaryConditions();
}
先看核心的输运方程, nuTildaEqn
,这个部分的每一行写成公式,分别如下:
$$
\frac{\partial \tilde{\nu}}{\partial t} \\
\nabla \cdot (U \tilde{\nu}) \\
-\nabla \cdot [D_{\tilde{\nu}_{eff}} \nabla \tilde{\nu}] \\
-\frac{C_{b2}}{\sigma_{\nu_t}} \cdot |\nabla \tilde{\nu}|^2 \\
C_{b1}\cdot \tilde{S} \cdot \tilde{\nu} \\
-\frac{C_{w1}\cdot f_w\cdot \tilde{\nu}}{d^2} \cdot \tilde{\nu}\\
$$
最后两项都包含了需要求解的量 $\tilde{\nu}$, 但是处理方式却不一样,最后一项,其实是将原本的 $\tilde{\nu} ^2$ 项进行了线性化,并用 fvm::Sp
操作符进行隐式处理。而倒数第二项,则是作的显式处理。差别在于,倒数第二项仅仅是用上一步得到的 $\tilde{\nu}$ 代入,然后这一项被加到 $Ax=b$ 中的 $b$ 部分。而隐式处理则会对系数矩阵 $A$ 有贡献。
输运方程中涉及到的变量和函数,全部都在 SpalartAllmaras
类中有定义,这里就不再单独分析了,仅将涉及到的公式列出如下
$$
D_{\tilde{\nu}_{eff}}=\frac{\nu+\tilde{\nu}}{\sigma_{\nu_t}}
$$
$$
f_w=g\cdot \left [ \frac{1+C_{w3}^6}{g^6+C_{w3}^6} \right]^{1/6},\quad g=r+C_{w2}\cdot(r^6-r) \\
r=min\left[ \frac{\tilde{\nu}}{max(\tilde{S}, SMALL)\cdot \kappa^2 d^2},10 \right]
$$
$$
\tilde{S}=f_{v3}\cdot\sqrt{2\Omega_{ij}:\Omega_{ij}}+\frac{f_{v2}\tilde{\nu}}{\kappa^2 d^2},\quad \Omega_{ij}=\frac{1}{2}(\nabla U -\nabla ^T U)
$$
$f_{v2}$ 和 $f_{v3}$ 有点特殊,因为涉及到了 Ashford 对原始模型的修正。根据作者的论文,修正的目的是防止 $\tilde{S}$ 出现负值。
如果不引入 Ashford 修正,则 $f_{v3}=1$;引入以后,则
$$
f_{v3}=\frac{(1+\chi \cdot f_{v1})}{c_{v2}}\cdot \frac{3+\frac{\chi}{c_{v2}} + (\frac{\chi}{c_{v2}})^2 }{(1+\frac{\chi}{c_{v2}})^3}
$$
对于 $f_{v2}$,如果不引入 Ashford 修正,则
$$
f_{v2}=\frac{1}{(1+\frac{\chi}{c_{v2}})^3}
$$
若引入,则
$$
f_{v2}=1-\frac{\chi}{1+\chi f_{v1}}
$$
此外,
$$
f_{v1}=\frac{\chi ^3}{\chi ^3 + c_{v1}^3}, \quad \chi=\frac{\tilde{\nu}}{\nu}, \quad \nu_t = \tilde{\nu}
f_{v1}$$
$d$ 是与壁面的距离,有一个专门的类 wallDist
来计算它。
其他的没有提到的变量,则都是模型常数了。具体的值都在 SpalartAllmaras
类的构造函数中定义了,这里就不再赘述了。
1.2. 一些细节
turbulence_
这个变量correct
函数中,只有当turbulence_
为true
时,下面求解输运方程的部分代码才会执行。这个变量又是从RASModel
类继承过来的,其初始化的语句为turbulence_(lookup("turbulence"))
,这意味着,这个变量的值是从RASProperties
文件中读取的(详见文末的RASProperties
示例)。- 模型常数,除了使用默认值,还可以自己指定(注:除非很有把握,一般不要随便修改模型常数)。具体方法是,在
RASProperties
文件里,建立一个名为SpalartAllmarasCoeffs
的 subDict,详见文末的示例。 bound
函数
这个函数的定义在头文件bound.H
里,其功能是限制 $\tilde{\nu}$ 的最小值。不过这种限制其实没有多少物理意义,纯粹是数值技巧。其中用到了fvc::average
函数(见我的另一篇博文)。read
成员函数
这个成员函数,初看之下似乎跟模型常数的初始化有关,实则不然。模型常数的初始化在类的构造函数部分完成,coeffDict_
这个变量在基类RASModel
的构造函数中先用type()+Coeffs
(这里是SpalartAllmarasCoeffs
)这个 subDict 初始化了,然后,在SpalartAllmaras
类的构造函数中,每一个模型常数都是先去coeffDict_
中查找是否用用户自定义的值,如果没有,则用模型的默认值,并且将这个默认值添加到coeffDict_
这个字典中。- 其他成员函数
SpalartAllmaras
显然不需要用到湍动能k
以及湍动能耗散epsilon
,但是在SpalartAllmaras.C
中确有这两个函数的定义,而且返回值是0。这是因为,在基类turbulenceModel
中,k()
,epsilon()
函数被声明为纯虚函数,如果不在SpalartAllmaras
中重新定义这两个函数,则SpalartAllmaras
也将是包括纯虚函数的抽象类了。在 C++ 中,抽象类是无法建立对象的。devReff
,R
,divDevRhoReff
这几个成员函数,是湍流模型提供给其他模型调用的函数,比如,在计算表面应力的forces
类中就需要调用湍流模型的devReff
函数。
附: RASProperties 文件示例
1 | /*--------------------------------*- C++ -*----------------------------------*\ |
【注一】:N-S 方程中,这一项应该是
$$
-\nabla \cdot (\nu_{eff}\nabla U)-\nabla \cdot [\nu_{eff}(\nabla^TU-\frac{2}{3}(\nabla \cdot U))]
$$
而且,在可压缩的湍流模型中, divDevReff
函数用的是 dev2
函数,所以其定义的形式就跟 N-S 方程一致了。但不知道为什么不可压缩的湍流模型为什么要用 dev
函数。虽然对于不可压缩的情形,由于 $\nabla \cdot U=0$,用 dev
或者 dev2
应该关系不大,但这种做法仍然很费解。在OpenFOAM-3.0版本中,可压和不可压用的都是 dev2
函数了。
参考:
[1] P. Spalart and S. Allmaras. “A one-equation turbulence model for aerodynamic flows”. Technical Report AIAA-92-0439. American Institute of Aeronautics and Astronautics. 1992.
[2] “An Unstructured Grid Generation and Adaptive Solution Technique for High Reynolds Number Compressible Flows”, G.A. Ashford, Ph.D. thesis, University of Michigan, 1996.