FVM and OpenFoam
In the previous session, we have used openFoam to resolve hydrothermal convection in oceanic crust. In this lecture, we will have a deeper look into how openFoam solves the governing equations and how the numerical schemes work.
OpenFoam uses the Finite Volume Method (FVM), which is a popular numerical technique used in computational fluid dynamics (CFD) and other areas of computational physics to solve partial differential equations (PDEs) that describe physical phenomena. It’s particularly well-suited for problems involving fluid flow, heat transfer, and associated physical processes. Here’s an overview of how FVM works:
Basic Concept
Domain Discretization The computational domain (the area or volume where the physical problem occurs) is divided into small, discrete control volumes (cells). These control volumes cover the entire domain without overlapping.
Integral Form of PDEs FVM operates on the integral form of the governing PDEs, as opposed to the differential form used in methods like the Finite Difference Method (FDM). The equations are integrated over each control volume.
Flux Calculations The key aspect of FVM is the calculation of fluxes of the conserved quantities like mass, momentum, energy across the faces of each control volume. These fluxes are used to determine how these quantities change within each control volume over time.
Boundary Conditions The method also incorporates boundary conditions, which define how the field behaves at the boundaries of the computational domain.
The general workflow in FVM is the following: Represent the variables like velocity, pressure, temperature at discrete locations in each control volume - either at the cell centers or at the cell faces. The integral form of the governing PDEs is discretized for each control volume. This involves expressing the rate of change of a quantity in a control volume in terms of the fluxes through its faces. The fluxes through the control volume faces are approximated using values of the variables at the centers or faces of the cells. Various schemes like upwind and central difference can be used for this approximation. The discretized equations form a system of algebraic equations. These equations are solved iteratively to find the field values (like pressure, velocity, etc.) throughout the domain. For steady-state problems, iterations continue until the solution converges. For transient problems, the solution is marched forward in time, updating the variables at each time step. One key advantage of the FVM is that it ensures conservation of physical quantities locally (in each control volume) and globally (across the entire domain); it can further handle complex geometries and irregular meshes. In summary, FVM is a versatile and powerful tool for solving complex physical problems numerically.
Openfoam uses what is called a cell-centered finite volume method. This means that the variables are stored at the center of the cells. The fluxes are calculated at the cell faces. The fluxes are then used to update the variables at the cell centers.
Example problem: heat diffusion
Let’s look at an example problem that solves for temperature diffusion, using the laplacianFoam solver.
Geometry and governing equations
The governing equation is the heat diffusion equation, which is given by:
\(k\) is the thermal conductivity and \(D\) is the thermal diffusivity \(\frac{k}{\rho c_p}\) . We here assume that \(D\) is a constant value. See also Theory.
Below is a simple 2D setup for a heat diffusion test case. We will use it to learn about the details of how openFOAM’s FV method works.
What we need to solve ?
Temperature field \(T\) of the model region at time \(t\), therefore \(T\) is the only unknown variable of our problem. It is a scalar field at the center of each cell.
Domain discretization
Mesh structure and topology
We can use a simple blockMesh to discretize our domain. You can download a prepared case here: Regular box case
.
The mesh comprises cell centers, cell faces, and cell vertices. The cell centers are the points where the temperature field is stored. The cell faces are the boundaries of the cells. The cell vertices are the points that define the cell faces. Notice how the internal faces (the ones that are not boundary conditions) are numbered continuously starting from 0.
In addition, a connectivity is needed to describe the topology of the mesh. This connectivity describes which cells are connected to each other through the faces. In openFoam a highly efficent data structure is used to store the connectivity information. For each face, the owner cell and the neighbour cell are stored. The normal vector of face always points from the owner cell to the neighbour cell. The numbering of the vertices that make up the face is again done using the right-hand rule and the face normal always points from the cell with the lower index to the cell with the larger index. Boundary faces have only an owner cell.
You can find details on the mesh description in the OpenFoam manual. Have a look at it and check the structure of the files in constant/polymesh
.
Tip
Check your understanding of the openFoam mesh structure and topology by creating the blockMesh mesh for the case we have downloaded and visualizing it using paraview. To create the labels, use the GenerateIds filter in paraview.
Spatial discretization: The diffusion term
The equation discretization step is performed over each element of the computational domain to yield an algebraic relation that connects the value of a variable in an element to the values of the variable in the neighboring elements [Moukalled et al., 2016].
Goal
Transform partial differential equation (49) into a set of algebraic equations: \(\mathbf{A} T = \mathbf{b}\).
Integrate equation (49) over element \(C\) (the green cell in Fig. 40) that enables recovering its integral balance form as,
Note that the transient term \(\iiint\limits_{V_C} \frac{\partial T}{\partial t} dV\) will be processed in the later section. Let’s set it to zero for the moment and assume steady-state.
Transform the volume integral of the heat flux into a surface integral by applying the divergence theorem,
The equation (51) is actually a heat balance over cell \(C\). It is basically the integral form of the original partial differential equation and involves no approximation.
The integrant is the diffusive heat flux,
Transform the surface integral in equation (51) as a summation over the control volume faces (still no approximation),
Here \(f\) denotes the boundary face of cell \(V_C\).
Now, the key problem is how to calculate flux integral on a face.
Evaluating flux integral on the faces
It’s worth noting that there are only two kinds of cells in the discretized domain, one is internal cell, the other is boundary cell. All faces of a internal cell are internal faces. The remain cells are boundary cells which contain at least one boundary face.
For a specific internal cell \(C\), e.g. cell 12, shown in Fig. 40, the equation (53) can be expanded as,
Note that the surface normals \(\vec{S}\) always point out of the cell; for example, \(\vec{S}_{f_{23}}\) would have a positive and \(\vec{S}_{f_{21}}\) a negative sign.
4.1 Considering face \(f_{23}\), calculate the first term on the right hand side of the equation (54) (we have to introduce the first approximation at this step). Using a Gaussian quadrature the integral at the face \(f_{23}\), for example, becomes,
where \(S_f\) is the area of face \(f_{23}\), \(ip\) refers to a integration point and \(ip(f_{23})\) the number of integration points along surface \(f_{23}\), \(\omega_{ip}\) is the integral weights.
Important
Only the one integration point scheme, i.e. Fig. 42 (a), is implemented in OpenFOAM (at least before version 8).
Therefore the first keyword of Laplacian scheme in system/fvScheme
dictionary file of a case has only one option, it is Gauss
.
4.2 Choose integral scheme or integral points
To simply explain the calculation process/logic and to also be consistent with OpenFOAM, we here adopt an one integration point scheme with weight \(\omega = 1\), thus equation (55) becomes,
where
\(S_{f_{23}} = \Delta y_{f_{23}}\) is the area of face \(f_{23}\) by assuming \(\Delta z = 1\).
\(\vec{S}_{f_{23}} = S_{f_{23}} \vec{n}_{f_{23}}\) is the surface vector of face \(f_{23}\).
\(\vec{n}_{f_{23}} = \vec{i}\) is the normal vector of the face \(f_{23}\) directed out of the cell \(C\) (see Fig. 40).
4.3 Calculate gradient of \(T\) at face centroid, here introduce the second approximation, e.g. assuming linear variation of T and then the gradient term in the equation (56) can be approximated as,
where \(\delta x_{f_{23}}\) represents the distance between center of cells who share face \(f_{23}\), they are cell 12 and cell 13.
Now let’s look at the western face \(f_{21}\) . Using a single integration point, we can again write:
Notice how the surface normal now has the opposite sign!
We continue to spell out the flux across face \(f_{21}\) as:
\[\color{blue}{\left( D\frac{\partial T}{\partial x} \right)_{f_{21}} \Delta y_{f_{21}}} = D\frac{T_{C} - T_{11}}{x_{11} - x_C}\Delta y_{f_{21}} = -\frac{D \Delta y_{f_{21}}} {\delta x_{f_{21}}} (T_{11} - T_{C})\]
Do the same thing for the remain faces (\(f_5, f_{24}\)).
4.3 Construct coefficients of a specific cell \(C_{12}\)
Now we get all the coefficients, thus the equation (54) can be discretized and expressed as a matrix form,
with
For a specific boundary cell, e.g. cell 19 shown in Fig. 40, the equation (53) can be rewritten as,
The first three terms are normal fluxes across internal faces, so there is nothing special and we can just calculate them following steps for internal faces as before. The special thing is the boundary face \(f_{52}\), we can call it \(f_b\) for a general case. Now the problem is how to evaluate flux on the boundary face \(f_b\).
The fluxes on the interior faces are discretized as before, while the boundary flux is discretized with the aim of constructing a linearization with respect to the cell field \(T_C\), e.g. \(T_{C_{19}}\) of cell \(C_{19}\) shown in Fig. 40, thus
All right, now out goal is to determine the coefficients of \(a_{F_b}\) and \(c_{F_b}\) according to the boundary conditions. Generally there are two basic kinds of boundary conditions, they are Dirichlet boundary condition (also called the first type or fixed value boundary condition) and Von Neumann boundary condition (all called the second type or fixed flux boundary condition), respectively.
4.1 Fixed value boundary condition
Fixed value means the value of field \(T\) is given on the boundary face is give, i.e. \(\color{purple}{T_{f_b}}\) is given. There fore we can calculate flux on boundary face, \(f_{51}\) for example,
Here we still use one integration point scheme to explain the calculation process,
Now let’s calculate the gradient on the boundary face,
Substituting equation (64) back to equation (63), equation (62) and equation (61), we can get Laplacian discretization coefficients \(a_{f_{51}}\) and \(c_{f_{51}}\),
Notice how \(\color{purple}{T_{f_{51}}}\) is known (a boundary condition), so that the term \(-a_{F_{51}}\color{purple}{T_{f_{51}}}\) will end up on the right-hand side and not in the coefficient matrix.
4.2 Fixed flux boundary condition
Fixed flux means normal gradient to the boundary face of field \(T\) is given, i.e. \(\color{purple}{\vec{J}^{T,D}_{f_{b}} \cdot \vec{n}_{f_{b}} = q_{f_b}}\) is given. There fore we can calculate flux on boundary face, \(f_{51}\) for example,
Substituting equation (66) back to equation (61), we can get Laplacian discretization coefficients \(a_{f_{51}}\) and \(c_{f_{51}}\),
4.3 Construct coefficients of the boundary cell \(C_{19}\)
Do the same thing as 4.1 or 4.2 (depends on the type of boundary condition) for all boundary faces of a boundary cell \(C\) and calculate the coefficients of \(a_{F_b}\) and \(c_{F_b}\), then the equation (60) can be expressed as,
where \(B_{19} = b_{C_{19}} - \sum\limits_{f\sim nb(\text{boundary face})} c_{F_f}\).
Construct general matrix form of the equation (53)
Matrix form of the equation (53) can be written as,
here \(N\) is the number of cell, \(\mathbf{A}\) is a sparse matrix of coefficients, \(\mathbf{T}\) is cell-centered temperature field. The matrix is visualized as Fig. 43. It should be noted that the coefficients matrix of Laplacian term is a symmetric matrix and the boundary conditions only affect diagonal entry of the coefficients matrix. Further, only the fixed value boundary condition affects the matrix and the fixed flux boundary condition affects the RHS(Right hand of side) \(B\).
Temporal Discretization: The Transient Term
After spatial discretization, the equation (50) can be expressed as,
where \(V_C\) is the volume of the discretization cell and \(L(T_C^t)\) is the spatial discretization operator expressed at some reference time \(t\), which can be written as algebraic form (see also equation (70)),
where \(a_C\) is the diagonal coefficients of the matrix, \(a_F\) is the off-diagonal coefficients, and the \(c_F\) is the source coefficients as right hand side of matrix of system.
Tip
- For specific cell \(C\):
\(a_F\) has only contributed from internal faces.
\(a_C\) is the negative summation of \(a_F\), contributed from all faces, but equation of the \(a_F\) for a boundary faces (equation (65) and equation (67)) is a little bit different from internal face.
\(c_F\) only comes from boundary faces of cell \(C\) if it has boundary face, see also equation (65) and equation (67). \(c_F\) will contribute to RHS (\(\mathbf{B}\)) of the algebraic equation (71).
Let’s do the temporal discretization for the first term of equation (72),
To derive the full discretized equation, an interpolation profile expressing the face values at (\(t-\Delta t/2\)) and (\(t+\Delta t\)) in terms of the element values at (\(t\)), (\(t-\Delta t\)), etc., is needed.Independent of the profile used, the flux will be linearized based on old and new values as,
where the superscript \(^{o}\) refers to old values. With the format of the linearization, the coefficient \(FluxC\) will be added to diagonal element and the coefficient \(FluxC^o T_C^o + FluxV\) will be added to the source or RHS (\(B\)).
First order implicit Euler scheme
Therefore the coefficients will be
First order explicit Euler scheme
Note that now the new time is at \(t+\Delta t\) and that the spatial operator (\(L\)) of equation (77) has to be evaluated at time \(t\). Therefore, in order to find the value of \(T\) at time \(t+\Delta t\), we don’t need to solve a set of linear algebraic equations,
Building the matrix: Mesh connectivity and internal data storage
Remember the mesh structure discussed above. The mesh connectivity is stored in the polyMesh
folder, which containes the neighbour
and owner
files. The normal vector of the face always points from the owner cell to the neighbour cell. The numbering of the vertices that make up the face is again done using the right-hand rule and the face normal always points from the cell with the lower index to the cell with the larger index. Boundary faces have only an owner cell.
Is is straightforward to find the indices for setting up the flux balance for each cell, as we did in equation (58)?
No, because the owner/neighbor data structure does not provide the indices of all the cells that are connected to a given cell. Rather the owner/neighbor data structure is “face-centered” in that for each face, we know which cells are on each side. Hence, we can easily loop over all faces and compute the fluxes but we cannot easily loop over cells and compute the fluxes to/from the neighboring cells.
Tip
Check out how openfoam makes use of this face-centered logic to compute the average gradient of a field in the gaussGrad
function in src/finiteVolume/finiteVolume/gradSchemes/gaussGrad/gaussGrad.C
(see below). The forAll loop is on the faces and the owner and neighbour functions are used to get the indices of the cells that are connected to a given face.
1/*---------------------------------------------------------------------------*\
2 ========= |
3 \\ / F ield | OpenFOAM: The Open Source CFD Toolbox
4 \\ / O peration | Website: https://openfoam.org
5 \\ / A nd | Copyright (C) 2011-2018 OpenFOAM Foundation
6 \\/ M anipulation |
7-------------------------------------------------------------------------------
8License
9This file is part of OpenFOAM.
10
11OpenFOAM is free software: you can redistribute it and/or modify it
12under the terms of the GNU General Public License as published by
13the Free Software Foundation, either version 3 of the License, or
14(at your option) any later version.
15
16OpenFOAM is distributed in the hope that it will be useful, but WITHOUT
17ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
18FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
19for more details.
20
21You should have received a copy of the GNU General Public License
22along with OpenFOAM. If not, see <http://www.gnu.org/licenses/>.
23
24\*---------------------------------------------------------------------------*/
25
26#include "gaussGrad.H"
27#include "extrapolatedCalculatedFvPatchField.H"
28
29// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
30
31template<class Type>
32Foam::tmp
33<
34 Foam::GeometricField
35 <
36 typename Foam::outerProduct<Foam::vector, Type>::type,
37 Foam::fvPatchField,
38 Foam::volMesh
39 >
40>
41Foam::fv::gaussGrad<Type>::gradf
42(
43 const GeometricField<Type, fvsPatchField, surfaceMesh>& ssf,
44 const word& name
45)
46{
47 typedef typename outerProduct<vector, Type>::type GradType;
48
49 const fvMesh& mesh = ssf.mesh();
50
51 tmp<GeometricField<GradType, fvPatchField, volMesh>> tgGrad
52 (
53 new GeometricField<GradType, fvPatchField, volMesh>
54 (
55 IOobject
56 (
57 name,
58 ssf.instance(),
59 mesh,
60 IOobject::NO_READ,
61 IOobject::NO_WRITE
62 ),
63 mesh,
64 dimensioned<GradType>
65 (
66 "0",
67 ssf.dimensions()/dimLength,
68 Zero
69 ),
70 extrapolatedCalculatedFvPatchField<GradType>::typeName
71 )
72 );
73 GeometricField<GradType, fvPatchField, volMesh>& gGrad = tgGrad.ref();
74
75 const labelUList& owner = mesh.owner();
76 const labelUList& neighbour = mesh.neighbour();
77 const vectorField& Sf = mesh.Sf();
78
79 Field<GradType>& igGrad = gGrad;
80 const Field<Type>& issf = ssf;
81
82 forAll(owner, facei)
83 {
84 GradType Sfssf = Sf[facei]*issf[facei];
85
86 igGrad[owner[facei]] += Sfssf;
87 igGrad[neighbour[facei]] -= Sfssf;
88 }
89
90 forAll(mesh.boundary(), patchi)
91 {
92 const labelUList& pFaceCells =
93 mesh.boundary()[patchi].faceCells();
94
95 const vectorField& pSf = mesh.Sf().boundaryField()[patchi];
96
97 const fvsPatchField<Type>& pssf = ssf.boundaryField()[patchi];
98
99 forAll(mesh.boundary()[patchi], facei)
100 {
101 igGrad[pFaceCells[facei]] += pSf[facei]*pssf[facei];
102 }
103 }
104
105 igGrad /= mesh.V();
106
107 gGrad.correctBoundaryConditions();
108
109 return tgGrad;
110}
111
112
113template<class Type>
114Foam::tmp
115<
116 Foam::GeometricField
117 <
118 typename Foam::outerProduct<Foam::vector, Type>::type,
119 Foam::fvPatchField,
120 Foam::volMesh
121 >
122>
123Foam::fv::gaussGrad<Type>::calcGrad
124(
125 const GeometricField<Type, fvPatchField, volMesh>& vsf,
126 const word& name
127) const
128{
129 typedef typename outerProduct<vector, Type>::type GradType;
130
131 tmp<GeometricField<GradType, fvPatchField, volMesh>> tgGrad
132 (
133 gradf(tinterpScheme_().interpolate(vsf), name)
134 );
135 GeometricField<GradType, fvPatchField, volMesh>& gGrad = tgGrad.ref();
136
137 correctBoundaryConditions(vsf, gGrad);
138
139 return tgGrad;
140}
141
142
143template<class Type>
144void Foam::fv::gaussGrad<Type>::correctBoundaryConditions
145(
146 const GeometricField<Type, fvPatchField, volMesh>& vsf,
147 GeometricField
148 <
149 typename outerProduct<vector, Type>::type, fvPatchField, volMesh
150 >& gGrad
151)
152{
153 typename GeometricField
154 <
155 typename outerProduct<vector, Type>::type, fvPatchField, volMesh
156 >::Boundary& gGradbf = gGrad.boundaryFieldRef();
157
158 forAll(vsf.boundaryField(), patchi)
159 {
160 if (!vsf.boundaryField()[patchi].coupled())
161 {
162 const vectorField n
163 (
164 vsf.mesh().Sf().boundaryField()[patchi]
165 / vsf.mesh().magSf().boundaryField()[patchi]
166 );
167
168 gGradbf[patchi] += n *
169 (
170 vsf.boundaryField()[patchi].snGrad()
171 - (n & gGradbf[patchi])
172 );
173 }
174 }
175}
176
177
178// ************************************************************************* //
OpenFoam uses a matrix format that is tightly integrated with the “face-centered” and the owner/neighbor structure. It is called the LDU matrix format. This format is a way of storing sparse matrices. A sparse matrix is one in which most elements are zero, which is a common case in CFD (check the example above). The LDU format is specifically tailored to store and manipulate these types of matrices efficiently.
- Components of LDU:
L (Lower triangle): This part of the matrix stores the coefficients that are below the main diagonal.
D (Diagonal): This represents the main diagonal of the matrix. In many algorithms, the diagonal elements play a crucial role and are treated separately for reasons of numerical stability and efficiency.
U (Upper triangle): This stores the coefficients above the main diagonal.
The LDU format is memory-efficient because it only stores non-zero elements. This is particularly beneficial in CFD where matrices can be very large, but only a small fraction of the elements are non-zero. OpenFOAM uses this structure to perform matrix operations like multiplication, addition, and especially the solving of linear systems, which is crucial in the iterative methods used in CFD simulations.
The LDU format uses scalar fields representing the diagonal, upper, and lower coefficients. The diagonal coefficients are indexed using the cell index. The upper and lower coefficients use face indices. In order to get the indices to match, mapping arrays between the face and cell indices are provided. The lowerAddr()
, and upperAddr()
functions of the lduAdressing class provide the owner and neighbor cell indices for each face.
A nice summary of the LDU format can be found in the OpenFoam Wiki.
Summary
In summary, the LDU format is a way of storing sparse matrices. It is specifically tailored to store and manipulate these types of matrices efficiently. OpenFOAM uses this structure to perform matrix operations like multiplication, addition, and especially the solving of linear systems, which is crucial in the iterative methods used in CFD simulations. Most of this happens under the hood but if you ever want to perform an operation on all rows of an lduMatrix, you need to know abou lduAdressing.