在详细介绍如何在 OpenFOAM 中实现边界条件之前,本节将介绍从用户的角度定义边界条件的过程。

10.2.1 内部、边界和几何场

计算区域的边界和相应的边界patch可以在 constant/polyMesh/boundary 文件中找到。每个边界patch都是一组单元(有限体积)面。 OpenFOAM 用户很少检查边界文件;它可能有助于程序员理解 OpenFOAM 用于存储非结构化网格的连接性的数据结构。

OpenFOAM 将物理场作为文件存储在初始时间步目录 (0文件夹) 中。例如,考虑案例rising-bubble-2D的动态压力场p_rgh的配置文件的一部分:

internalField uniform 0;
boundaryField
{
    bottom
    {
        type zeroGradient;
    }

internalField 关键字与存储在网格中心的值相关(值为 0 的均匀场),bottom边界条件定义为 zeroGradient 类型(参见第 1 章的数值描述)。

OpenFOAM 操纵与非结构化网格的不同元素相关联的张量场。例如,速度 U 的 volVectorField 和压力场 p 的 volScalarField 与单元中心相关联,而 surfaceScalarField 存储与面中心相关联的体积通量场 phi。对任何求解器的简要介绍显示了在这些场上执行的许多不同操作。为了说明几何场上的一些常见操作,volScalarField p 作为示例的来源:

访问场值只需将特定的网格标签传递给场的访问运算符 [](const label&) 即可。在本例中,我们选择单元格 4538:

const label cellI{4538};
Info << p[cellI] << endl;

访问边界上的值稍微复杂一些,因为 volScalarField 不直接在网格面上存储任何值。边界值由在特定边界上定义的边界条件确定。可以使用以下代码来计算网格第一个边界块上 p 的最大值:

const label boundaryI{0};
Info << "max(p) = " << max(p.boundaryField()[boundaryI]) << endl;

提示:边界patch在边界网格中的存储顺序对于所有场都是相同的,并且由patch在 polyMesh/boundary 文件中列出的方式决定。这又由使用的网格生成器确定。

默认情况下,当使用访问运算符 operator[](const label&) 访问时,任何场都会从其 internalField 返回值。要访问边界字段的值,必须调用boundaryField() 成员函数(用于常量访问的boundaryFieldRef())。这将返回一个边界补丁列表,每个网格边界都有一个边界补丁。每个元素都是用户为此补丁选择的边界条件的抽象表示。根据物理场的类型,这种表示要么继承自 fvPatchField 要么继承自 pointPatchField,尽管第一种是最常用的。

提示:在使用OpenFOAM中的类时,查看Extended Code Guide有助于查找哪些成员函数可用以及它们的接口。

如上例所示,OpenFoam实现了所谓的geometrical场,它将值分成两组:内部值和边界值。 几何物理场是OpenFoam中的类模板,因为它们存储不同级别的张量,并将它们与不同的网格单元相关联。 例如,如果我们想象一个由线段组成的几何网格,并将其称为线网格,那么与线网格一起工作的几何场概念的相应模型将被命名为线场。 线场将张量值映射到每条线的中心(内部场值),以及线网格的两个边界端点(边界场值)。 实现几何场概念的类模板被命名为geometrical,它的实例化产生不同的几何场模型,映射到不同类型的网格。 在OpenFoam中,有不同的几何物理场模型:

  1. 第一类是众所周知的物理场,如volScalarFieldvolVectorField,它们将数据存储在网格中心。 在边界上,必须将边界条件应用于存储在面中心的模拟值。 满足vol*field的命名约定。
  2. 第二类是在面中心存储网格每个面的物理场。这不限于域的边界。其中一种场类型是surfaceScalarField,用于定义两个相邻单元之间的通量。此类的所有场均命名为surface*Field
  3. 第三类包含pointScalarFieldpointVectorField类型。 当谈到OpenFOAM物理场:将数据存储在网格点中的物理场时,这一类的物理场可能会被忽略。 网格中的每个点都有它自己的值,在边界上,必须为边界上的点定义边界条件,而不是为面中心定义边界条件。 在源代码中查找point*Field将显示该类别的所有物理场。

图10.1:9x5单元网格几何场的组成。

在此澄清之后,我们可以潜入物理场与边界条件之间的关系。 从设计角度来看,OpenFoam中的边界条件封装了映射到域边界的场值,以及负责基于内部值--边界条件--确定这些边界值的计算的成员函数。 GeometricField类模板提供了成员函数,简化了边界条件的制定和更新。 成员函数的一些功能包括计算边界物理场值和将存储在相邻内部单元中的值作为参数。 两者都是实现任何边界条件的关键要求。 关于哪些成员函数应该执行哪些任务的更多信息将在下一节10.2.2中提供。 边界物理场--因此边界条件与内场一起封装,形成几何场(见图10.1)。 三个几何场模型中的任何一个,都是具有适当模板参数的GeometricField的typedef。

// 程序清单41
template<class Type, template<class>  class PatchField, class GeoMesh> 
class GeometricField
:
public DimensionedField<Type, GeoMesh> 
{

可以在源代码中研究边界条件的结构。例如,考虑清单41所示的GeometricField类模板的声明部分。从清单41中可以明显看出,GeometricField是从DimensionedField派生出来的。这意味着在GeometricField类模板(几何字段模型)的任何实例化上执行的任何默认算术运算都将在不包括边界的情况下执行。这是有意义的,因为边界上的值应该仅由相应的边界条件确定。使用附加的赋值运算符GeometricField::operator ==,运算被扩展为也考虑边界场。边界上的值可以从实际边界条件的外部覆盖,直到GeometricField::correctBoundaryConditions()成员函数再次计算边界条件。

从类派生(在上面的代码片段中是:public)会导致继承其成员函数。 对于GeometricField,算术运算符继承自DimensionedField。 由于边界物理场是由GeometricField和GeometricBoundaryField属性组成的,所以维度对内部场值进行建模。 因此,来自维度的重载算术运算符排除边界场值。

注:运算符GeometricBoris::Operator==不是OpenFoam中的逻辑相等比较运算符,它扩展了GeometricalField的赋值,以包括边界场值。 它被添加到GeometricField界面中,以包括合成边界物理场上的算术运算。

边界场本身声明为嵌套类模板,GeometricField将其存储为私有属性。 边界场类模板被命名为GeometricBoundary,它的声明在GeometricField类模板中可以找到,其包含一个边界patch物理场列表,每个网格边界一个patch场。 虽然GeometricBoundaryField封装在GeometricField中,但提供了一个非常量访问,这导致GeometricField的客户端代码能够更改边界物理场的值。 乍一看,这打破了几何场对边界物理场的封装,然而好处超过了设计原则,因为这种方法产生了更大的使用灵活性。 例如,热力学模型可能以依赖于另一个几何场的方式改变场值。 这种情况下的变化是由几何场以外驱动的,这通常是这种情况,因为几何物理场是OpenFoam中的全局变量,由求解器操作,该求解器被实现为计算的过程序列。 对边界场数据成员的非常量访问如清单42所示。

//程序清单42
Boundary& boundaryFieldRef(const bool updateAccessTime = true);
//- Return const-reference to the boundary field
inline const Boundary& boundaryField() const;

与边界场相比,在GeometricField中内部场的处理方式不同。由于GeometricField派生自DimensionedField,因此不需要返回不同的对象。要访问内部场,将返回对 *this的引用,因为DimensionedField通过维度检查实现内部场及其算术运算。清单43阐明了几何字段如何提供对内部字段的访问。internalField()成员函数返回对GeometricField的非常数引用,该引用通过继承是DimensionedField。

// 程序清单43
// GeometricField.H
//- Return internal field
InternalField& internalField();
// GeometricField.C
template
<
    class Type,
    template<class>  class PatchField,
    class GeoMesh
> 
typename
Foam::GeometricField<Type, PatchField, GeoMesh> ::InternalField&
Foam::GeometricField<Type, PatchField, GeoMesh> ::internalField()
{
    this-> setUpToDate();
    storeOldTimes();
    return *this;
}

此时,您应该对OpenFoam中的模拟所涉及的物理场有了一个概述,以及为什么GeometricBoundaryField被封装在GeometricField中,并为GeometricField类模板的客户机代码所做的操作准备了非常量访问。 分析类继承和协作关系图虽然很有帮助,但在获得理解方面不如使用类本身有效,这将在下面几节中讨论。

10.2.2 边界条件

顾名思义,边界条件可为存储在边界场(GeometricBoundaryField)中的值添加功能。在面向对象设计(OOD)中,添加功能通常意味着扩展现有的类,这在边界条件的情况下也是如此。实际上,上述GeometricBoundaryField不仅封装存储在计算域边界的场值,每个物理场都用确定边界条件行为的虚拟成员函数来扩展。

边界条件是一个层次概念,在有限元法中,相似的边界条件被分成边界条件类别。 因此,为了使用户能够在运行时选择边界条件(RTS),它们被建模为类层次结构。顶级父抽象类fvPatchField定义每个边界条件必须符合的类界面。 OpenFOAM中的每个边界条件都是由fvPatchField或pointPatchField导出的。 后者多用于涉及网格运动或修改的应用。 两者都有一个名为internal_的常数私有属性,它是对GeometricField的内部场的引用,这在前一节中已经介绍过。 该属性提供对内部场值的访问,而不仅仅是对与边界网格贴片直接相邻的单元格的访问。 对于PointPatchField,interalField_属性声明为:

const DimensionedField<Type, pointMesh> & internalField_;

fvPatchField的内部场声明与fvPatchField相同,但是dimensionedField的第二个模板参数是volmesh而不是pointmesh:

const DimensionedField<Type, volMesh> & internalField_;

由于pointPatchField符合与fvPatchField相同的类接口,并且最常遇到体积场,因此本节将介绍fvPatchField。

在我们详细讨论fvPatchField的哪个成员函数是相关的之前,当涉及到新边界条件的实现时,我们尝试总结一下GeometricField和实际访问边界条件之间的联系。图10.2给出了这种关系的图示。几何场组成几何边界场,它继承自(FieldField),因此是(边界)场的集合。需要几何边界场的合成,因为内部场值的修改需要通过边界条件更新边界场值。此外,边界场不能被分离成与内部场不同的对象。内部场和边界场不仅在拓扑上通过网格彼此相连,当方程离散化以计算内部场值时,有限体积法需要边界场值。网格的细化导致单元面的分裂,因此内部场和边界场的长度在这种情况下也是间接连接的。因此,将内部场和边界场分开是毫无意义的。它将引入需要显式同步的全局变量,这将使应用程序级别上所有字段操作的语义严重复杂化。几何场在边界场的集合上循环,并且通过将更新委托给对应的边界条件来更新每个边界场。图10.2显示了PatchField模板参数,该参数在实例化时(体积网格的fvPatchField)是一个边界条件。

图10.2 类协作图为边界条件

当内部场被修改并且边界条件将被更新时,OpenFOAM应用程序调用GeometricField的correctBoundaryConditions()成员函数:在求解器中为场求解PDE或在预处理应用程序中显式计算内部值之后。GeometricField::updateBoundaryConditions()成员函数的实现如清单44所示。

// 程序清单44
// Correct the boundary conditions
template
<
    class Type,
    template<class>  class PatchField,
    class GeoMesh
> 
void Foam::GeometricField<Type, PatchField, GeoMesh> ::
correctBoundaryConditions()
{
    this-> setUpToDate();
    storeOldTimes();
    GeometricBoundaryField_.evaluate();
}

注:要理解GeometricField如何更新边界条件,必须理解清单44中的最后一行。

清单44中的最后一行调用GeometricBoundaryField的成员函数evaluate(),该函数依次执行各种任务。如果边界条件尚未初始化,则此成员函数调用fvPatchField的成员函数initEvaluate(),否则会调用成员函数evaluate()。由于OpenFOAM实现了zero halo layer并行,因此并行通信由GeometricBoundaryField::evaluate()处理,因为进程边界也作为边界条件实现。

geometricBoundaryField::Updatecoeffs()成员函数是从客户端代码触发特定FvPatchField功能的另一个主要成员函数。 与evaluate())相比,updatecoeffs()的实现更短,因为没有实现并行通信。 清单45显示了GeometricBoundaryField::UpdatEcoeffs的实现。

//程序清单45
template<class Type,template<class>  class PatchField,class GeoMesh> 
void Foam::GeometricField <Type,PatchField,GeoMesh> ::GeometricBoundaryField::updateCoeffs()
{
    if (debug)
    {
        Info<< "GeometricField<Type, PatchField, GeoMesh> ::"
        "GeometricBoundaryField::"
        "updateCoeffs()" << endl;
    }
    forAll(*this, patchi)
    {
        this-> operator[](patchi).updateCoeffs();
    }
}

清单45中的forAll循环遍历GeometricBoundaryField的所有patch,称为 *this。fvPatchField的成员函数updateCoeffs()使用运算符[]直接为域边界的每个单元调用。

updateCoeffs()和evaluate()成员函数表示fvPatchField公共接口的相关部分,可从每个求解器自动访问该部分。这两个成员函数之间的主要区别是evaluate()可以在单个时间步长中调用任意次数,但只能执行一次。另一方面,updateCoeffs()不检查边界条件是否更新,它将执行与调用次数相同的计算。两者都是由基类fvPatchField提供的通用类接口的一部分,可用于对直接或间接从fvPatchField导出的自定义边界条件进行编程。

在正式发布中只有少数边界条件是直接从fvPatchField导出的,如基本的fixedValueFvPatchField和zeroGradientFvPatchField。大多数导出的边界条件直接继承自基本边界条件。mixedFvPatchField是一个流行的基类,它提供了在用户定义的固定值和固定梯度边界条件之间进行混合的功能。mixedFvPatchField的类协作关系图如图10.3所示。它包含三个新的私有属性,如清单46所示,并且它不依赖于固定梯度和固定值边界条件的实现。相反,规定的固定梯度和固定值边界物理场存储为字段类型的私有属性。

图10.3 混合FvPatchField和inletOutletFvPatchField边界条件的类协作图

//程序清单46
//- Value field
Field<Type>  refValue_;
//- Normal gradient field
Field<Type>  refGrad_;
//- Fraction (0-1) of value used for boundary condition
scalarField valueFraction_;

因为这些属性是私有的,所以有公共成员函数提供对它们的常量和非常量访问。这使派生类能够间接使用属性,以便在固定值和零梯度边界条件之间实现不同的混合方式。直接从mixedFvPatchField导出的常用边界条件是inletOutletFvPatchField。它根据通量的方向在固定值和零梯度边界条件之间切换。如果通量指向域之外,则其充当零梯度边界条件(zeroGradientFvPatchField),否则其充当固定值边界条件(fixedValueFvPatchField)。这是在面对面的基础上并基于mixedFvPatchField边界条件的私有属性来确定的。场的梯度不是由用户在mixedFvPatchField中规定的-它由inletOutletFvPatchField构造函数设置为零值。

程序清单47中计算了mixedfvpatchfield<type> ::updatecoeffs()稍后使用的分数值,方法是为具有正(流出)容积流量的面赋值1,否则赋值0。 清单47显示了inletOutletFvPatchField的updatecoeffs())成员函数的实现。 这个成员函数只设置valueFraction()的值,边界场的计算被委托给父级mixedValueFvPatchField。 函数pos如清单48所示。 如果标量s的值大于或等于零,则返回1,否则返回0。 zeroGradientFvPatchField和fixedValueFvPatchField的实际赋值是通过调用mixedFvPatchField::updateCoeffs()完成的,因此不能重新实现。

//程序清单47
template<class Type> 
void Foam::inletOutletFvPatchField<Type> ::updateCoeffs()
{
    if (this-> updated())
    {
        return;
    }
    const Field<scalar> & phip =
        this-> patch().template lookupPatchField
        <
            surfaceScalarField,
            scalar
        > 
        (
            phiName_
        );
    this-> valueFraction() = 1.0 - pos(phip);
    mixedFvPatchField<Type> ::updateCoeffs();
}
//程序清单48
inline Scalar pos(const Scalar s)
{
    return (s > = 0)? 1: 0;
}

10.2.2.1 读取边界条件数据

在对可能具有新参数的自定义边界条件进行编程的过程中,需要从0目录中的场文件中读取相应的新参数名称和值。 因此,程序员必须在该文件中添加必要的参数及其值。 当边界条件以字典的形式从文件中读取时,字典将被读取并传递给边界条件构造函数。

某些边界条件可能使用dictionary类成员函数,该函数在提供默认值的同时查找数据。 在这种情况下,切换边界条件的类型和不提供适当的参数不会导致运行时错误。 Dictionary类上的操作在第5章已经讨论过,在下一节解释新边界条件的编程之前,应该很好地理解这些操作。