Edit me

This chapter was written by Christian Psenica and reviewed by Ping He.

OpenFOAM v1812 NACA0012 Simulation Setup

Introduction

Background

OpenFOAM (Open-source Field Operation And Manipulation) is a free open-source finite-volume CFD solver. OpenFOAM is primarily written in C++ and comes with libraries to help facilitate numerical operations on field values. OpenFOAM also has a wide range of utilities for pre- and post-processing, such as mesh generation/quality checks and support for ParaView (for post-process visualization). There are three main branches of OpenFOAM: ESI OpenCFD, The OpenFOAM Foundation, and Extended Project. DAFoam only supports the ESI OpenCFD version, the OpenFOAM version discussed within this document.

Follow Along

This document contains basic information related to setting up an OpenFOAM simulation using the steady-state NACA0012 simulation. We will discuss every file within this case in an effort to clarify the various aspects of OpenFOAM.

To help with clarity, below is the file structure for the NACA0012 case. As a general overview: 0 contains boundary conditions and initial field values, constant handles flow properties (such as turbulence model and fluid modeling parameters), and system controls the numerical discretization, equation solution parameters, etc. This document serves as detailed documentation to these directories.

- 0.orig               // initial fields and boundary conditions
- constant             // transport/ turbulence property information and mesh files
- system               // flow discretization, simulation run parameters, solver settings etc.
- clean                // script to clean and reset case to default
- paraview.foam        // dummy file used by ParaView to load results
- run                  // script to execute simulation

These steps will be elaborated on in the coming sections with particular emphasis on the 0.orig, constant, and system directories. It is intended that the user reads this documentation carefully to gain a basic understanding of OpenFOAM. It is also recommended to follow along in the tutorial cases to recreate the simulation and post-process using ParaView (Don’t have ParaView? Download ParaView for free, here!).

1. Boundary and Initial Conditions - 0.orig

The 0.orig directory contains both the boundary conditions and the initial field values for the simulation. For clarity purposes it should be noted here that this simulation contains a wing boundary (wall), an inout boundary (far field domain), and two symmetry planes, one for either side of the airfoil. For this NACA0012 case, we can expand the 0.orig directory to see which field values have specified boundary conditions:

      
|-- 0.orig        // directory containing BCs
  |-- epsilon     // turbulent kinetic energy dissipation rate
  |-- k           // turbulent kinetic energy
  |-- nut         // turbulent kinematic viscosity
  |-- nuTilda     // modified turbulent viscosity
  |-- omega       // specific dissipation rate
  |-- p           // pressure
  |-- U           // velocity

As a note: we name this folder 0.orig. This is not a necessity; OpenFOAM needs this file to be named 0 at the start of the simulation. Hence in the run script we have the line cp -r 0.orig 0. This is simply for preservation and is common practice. To avoid confusion, as a motivated reader may notice that various sources use either 0 or 0.orig naming conventions, we leave the name of this folder as 0.orig and note the distinction here.

1.1 The dimensions and internalField

As a preface to the following boundary conditions discussed, we will clarify a couple entries found in all boundary condition files: dimensions and internalField.

When entering values for both boundary conditions and internalField, it is important to be mindful of the units used as they are not all constant. Though OpenFOAM’s default is SI units, the derived units are based on which solver is used. For example, if using the simpleFoam solver (the steady-state solver we will use for the NACA0012 case), pressure is expected to be the kinematic pressure which is measured in $m^2/s^2$. However, if using pimpleFoam (unsteady solver), then pressure is measured as thermodynamic pressure, measured in Pascals ($N/m^2 = kg/(m*s^2)$). The exact units used are specified by the dimensions key in 0.orig/. Using the same 0.orig/U from the NACA0012 case, we can see the dimensions used for velocity are dimensions [0 1 -1 0 0 0 0];. This is, as one would suspect, $m/s$. But looking at solely the dimensions key, this may not be so obvious. However it is in fact very simple:

dimensionsUsed  [Mass Meter Second Kelvin Mole Ampere Candela]   

dimensions      [0 1 -1 0 0 0 0]; 

The dimensions key is a list containing 7 numbers. Each (non-zero) number in dimensions denotes which unit is being used, corresponding to the entries in dimensionsUsed (this key is for an example, it is not an actual key used in OpenFOAM). A zero indicates that the unit is not being used. The value of the actual (non-zero) numbers gives the exponent on the unit. So an entry of [0 1 0 0 0 0 0] gives meter ($m$). However, [0 2 0 0 0 0 0] indicates $m*m = m^2$ and so on. A negative sign before the number indicates a negative exponent. Knowing this, it becomes far more clear to see which units are expected. As an example, the kinematic pressure, $m^2/s^2$, would be dimensions [0 2 -2 0 0 0 0].

Lastly, the internalField key is used as an initial condition to the problem and should be assigned with care. Field values such as pressure, temperature, velocity etc. can be easily assigned by the user and depend on what the user wants to simulate. The calculations for turbulent fields (such as nuTilda, k, epsilon etc.) are beyond the scope of this beginner user guide and we encourage the motivated reader to see the advanced user guide for a breakdown on these calculations.

1.2 Velocity (U)

For the NACA0012, the fluid flow is 10 m/s in magnitude at an angle of attack of ~5.14 degrees:

dimensions      [0 1 -1 0 0 0 0];          // dimensions for velocity

internalField   (9.9598 0.89575 0);        // velocity components at AOA = 5.139186 degreees

boundaryField
{
    wing                                   // patch name to apply BC to
    {
        type            fixedValue;        // type of BC, could use noSlip
        value           uniform (0 0 0);   // value at boundary
    }

    symmetry1
    {
        type            symmetry;          // symmetry type, nothing else to specify
    }

    symmetry2
    {
        type            symmetry;
    }

    inout
    {
        type            inletOutlet;       // handles reverse flow at outlets
        inletValue      $internalField;
        value           $internalField;
    }
}

The wing boundary takes on a fixedValue uniform (0 0 0); condition. This enforces a no slip boundary condition at the wall. This is also equivalent to using type noSlip; on the wing (either will work). The symmetry boundary condition will ensure an identical flow field over the two symmetry planes present (this is consistent for all field values specified in this section). The inletOutlet boundary condition automatically applies fixedValue when the flow enters the simulation domain with the values given by the inletValue key, and zeroGradient when the flow exits the domain. Here, the user can use $internalField to specify the far field domain conditions: prescribe the same as the internalField value defined above (i.e., uniform (9.9598 0.89575 0)). The value key for the inout patch prescribes an initial value for this patch, and this value will be automatically updated (depending on whether the flow enters or leaves the domain) when the simulation starts.

1.3 Pressure (p)

Since this is a steady-state simulation, the pressure is given as the kinematic pressure ($m^2/s^2$). For this solver we use a reference pressure of 0 (internalField uniform 0;).

dimensions      [0 2 -2 0 0 0 0];

internalField   uniform 0;

boundaryField
{
    wing
    {
        type            zeroGradient;
    }

    symmetry1
    {
        type            symmetry;
    }

    symmetry2
    {
        type            symmetry;
    }
    
    inout
    {
        type            fixedValue;
        value           $internalField;
    }
}

Wall boundaries (such as the wing) receive a zeroGradient boundary condition type. As mentioned previously, the symmetry planes always receive the symmetry type boundary condition (in the following sections, 1.4-1.8 we will omit discussion of this in an effort to reduce redundancy). The far field domain, inout, takes on a fixed value consistent with the internalField entry. In a typical setup, inlets (far field domain here) are fixedValue while all other physical domains will be zeroGradient.

1.4 Turbulent Kinetic Energy Dissipation Rate (epsilon)

The following sections (1.4-1.8) are all turbulence field values. It is not uncommon, though not all the time necessary, to use wall functions for these values. For now we will just state the use of wall functions and point the reader to the advanced user guide for in-depth details on wall functions and their applications within OpenFOAM. The discussion for these turbulent fields will also be brief as the types of boundary conditions specified largely remain unchanged aside from the wing boundary.

dimensions      [0 2 -3 0 0 0 0];

internalField   uniform 0.14;

boundaryField
{
    wing
    {
        type            epsilonWallFunction;
        value           $internalField;
    }

    symmetry1
    {
        type            symmetry;
    }

    symmetry2
    {
        type            symmetry;
    }
    
    inout
    {
        type            inletOutlet;
        inletValue      $internalField;
        value           $internalField;
    }
}

Here we apply a wall function to the wing boundary. As the name implies, wall functions are only applicable to wall boundaries and should not/ cannot be used for other types of boundaries. Additionally, we treat inout the same as for velocity (U): apply fixedValue when the flow enters the simulation domain, and zeroGradient when the flow exits the domain.

1.5 Turbulent Kinetic Energy (k)

As noted previously, the boundary conditions applied for these turbulent field values are mostly the same. The one change here is the type of wall function used, now being kqRWallFunction. There is no universal wall function applicable to all boundaries. In OpenFOAM we use different wall functions for different field values. Typically the name of the particular wall function being used indicates which field values it may be applied to. As we have the turbulent kinetic energy (k), we use the kqRWallFunction. As the name implies, this wall function may be used for the q and R field values as well (though these values will not be discussed in the basic user guide, but defined in the advanced user guide).

dimensions      [0 2 -2 0 0 0 0];

internalField   uniform 0.015;

boundaryField
{
    wing
    {
        type            kqRWallFunction;
        value           $internalField;
    }

    symmetry1
    {
        type            symmetry;
    }

    symmetry2
    {
        type            symmetry;
    }
    
    inout
    {
        type            inletOutlet;
        inletValue      $internalField;
        value           $internalField;
    }
}

1.6 Turbulent Kinematic Viscosity (nut)

For the turbulent viscosity, note the change of the wall function to nutUSpaldingWallFunction. Similar as before, this is a wall function specifically designed for the nut turbulent field.

dimensions      [0 2 -1 0 0 0 0];

internalField   uniform 4.5E-5;

boundaryField
{
    wing
    {
        type            nutUSpaldingWallFunction;
        value           $internalField;
    }

    symmetry1
    {
        type            symmetry;
    }

    symmetry2
    {
        type            symmetry;
    }
    
    inout
    {
        type            calculated;
        value           $internalField;
    }
}

For this field however, we switch the inout boundary condition type to calculated. This naturally raises two questions: what is a calculated boundary condition type, and why use this type versus inletOutlet such as the other turbulent fields?

Put simply, nut is a turbulent field which is not solved for directly, its value depends on (calculated from) other field values. Due to this, we do not want to assign a value to this field. We want OpenFOAM to solve for it throughout the simulation process. Therefore, we apply the calculated boundary condition type to tell OpenFOAM to solve for this field value rather than read in a user prescribed value on the boundary.

1.7 Modified Turbulent Viscosity (nuTilda)

The modified turbulent viscosity is not a calculated field, meaning OpenFOAM solved for it directly, hence the inout boundary uses inletOutlet again. This field is specific to the Spalart-Allmaras turbulence model (which we use for the NACA0012 case) and is, in some cases, referred to as the Spalart-Allmaras variable.

dimensions      [0 2 -1 0 0 0 0];

internalField   uniform 4.5e-05;

boundaryField
{
    wing
    {
        type            fixedValue;
        value           uniform 0.0;
    }

    symmetry1
    {
        type            symmetry;
    }

    symmetry2
    {
        type            symmetry;
    }
    
    inout
    {
        type            inletOutlet;
        inletValue      $internalField;
        value           $internalField;
    }
}

However, a wall function is not used for this field value on our wall boundary, wing. Rather we assign a zero value to the wall. for clarity’s sake we provide the definition for nuTilda, but leave the in-depth discussion for the advanced user guide: $\tilde{\nu} = \sqrt{\tfrac{3}{2}} (U I \ell)$, where $U$ = reference velocity magnitude, $I$ = turbulence intensity, and $\ell$ = turbulence length scale. Here we can see that nuTilda is based on the velocity which at the wall, is zero. Hence, nuTilda is zero at the wall as well.

1.8 Specific Dissipation Rate (omega)

The final value to discuss is the specific dissipation rate, omega. The treatment for omega is largely the same as the other turbulent field values. We use a wall function on the wing boundary and inletOutlet on the far field domain. The key difference here is the blended true; entry for the wall function. This delves deeper into wall modeling (a subject left for the advanced user guide), however to clarify for this guide: the blended key blends the physics of different viscous layers near the wall. Here we implement this blending, but the need for this blending is case dependent.

dimensions      [0 0 -1 0 0 0 0];

internalField   uniform 100;

boundaryField
{
    wing
    {
        type            omegaWallFunction;
        value           $internalField;
        blended         true;
    }

    symmetry1
    {
        type            symmetry;
    }

    symmetry2
    {
        type            symmetry;
    }
    
    inout
    {
        type            inletOutlet;
        inletValue      $internalField;
        value           $internalField;
    }
}

2. Transport, Turbulence, and Mesh Properties - constant

The constant directory contains information related to turbulence and transport properties (and in other cases, thermophysical properties). Additionally, the mesh used for the simulation resides in constant/polyMesh. The file structure for the NACA0012 case is shown below for clarity:

      
|-- constant                 // directory containing mesh, turbulence model, and transport properties
  |-- polyMesh               // directory containing the mesh
    |-- boundary             // list of boundaries and associated mesh cells
    |-- cellZones            // grouping of cells (user defined)
    |-- faces                // list of cell faces
    |-- faceZones            // grouping of cell faces (user defined)
    |-- neighbour            // list of neighbour cell labels
    |-- owner                // list of owner cell labels
    |-- points               // vectors describing mesh cell verticies 
    |-- pointZones           // grouping of point verticies (user defined)
  |-- transportProperties    // fluid transport parameters
  |-- turbulenceProperties   // turbulence parameters

2.1 Mesh Files (polyMesh)

The mesh is user generated and can be generated using various methods/ tools. This can be done using third party programs or using OpenFOAM’s built in tools (such as snappyHexMesh). For the NACA0012 case, the mesh is already generated and ready to use for the simulation.

For the discussion of the polyMesh directory, we will limit this section to showing the most pertinent file, boundary. This is the file most viewed in the polyMesh directory, and shows the various boundaries generated during the meshing process. The remaining files (cellZones, faces, faceZones, neighbour, owner, points, and pointZones) all contain organizational data on the mesh. Although it is possible to modify a few select entries within the boundary file without breaking the mesh (a topic left for the advanced user guide) it is highly recommended to never try and make manual adjustments to the remaining files. Although possible in theory to make modifications to these files successfully, it is very error prone and not an intended feature. If the physical mesh needs to be adjusted, the best practice is to always regenerate the mesh and have OpenFOAM modify these files for you. Below we can see the boundary file for our NACA0012 case:

      
4                                       // total number of boundaries in mesh
(
    symmetry1                           // name of boundary
    {
        type            symmetry;       // type of boundary
        inGroups        1(symmetry);
        nFaces          4032;
        startFace       7938;
    }
    symmetry2
    {
        type            symmetry;
        inGroups        1(symmetry);
        nFaces          4032;
        startFace       11970;
    }
    wing
    {
        type            wall;
        inGroups        1(wall);
        nFaces          126;
        startFace       16002;
    }
    inout
    {
        type            patch;
        nFaces          126;
        startFace       16128;
    }
)

From the boundary file we first notice the number 4. This number indicates the total number of boundaries defined in our mesh. As we saw from Sec.1, we indeed have four boundaries: two symmetry planes, the wing itself, and the far field domain. Each one of these boundaries is defined here. Within each boundary entry there is an accompanying dictionary providing extra information for OpenFOAM regarding the boundary. The type key tells OpenFOAM what type of boundary we have. As expected, the symmetry planes are type symmetry, the wing itself is a wall boundary, and the far field is a patch (allowing fluid to flow through). Similar boundary types will be grouped together in OpenFOAM for organization, hence the inGroups key. nFaces tells OpenFOAM how many cell faces are contained within the given boundary. startFace tells OpenFOAM which cell is the first cell making up a boundary. All mesh cells in OpenFOAM are numbered, and these two keys help OpenFOAM identify boundaries.

2.2 transportProperties

The transportProperties file contains fluid specific properties which help define what kind of fluid is being simulated (most common air but of course other fluids can be modeled such as water or various gases). In a general sense, transportProperties is not the only file containing fluid parameters. For example, thermophysicalProperties also contains fluid parameters but is not included in our NACA0012 case as we do not simulate thermodynamic properties for this simulation.

      
transportModel Newtonian;  // transport model being used

nu    1.5e-5;              // dynamic viscosity
Pr    0.7;                 // Prandtl number
Prt   1.0;                 // turbulent Prandtl number

The first entry tells OpenFOAM which transport model to use. The Newtonian transport model used in the NACA0012 case assumes a constant dynamic viscosity (nu). Hence the following entry, nu, where we give this constant value for OpenFOAM to use. The value used here, $1.5e-5$ is a typical value used for modeling air.

The following two entries are the Prandtl number and turbulent Prandtl number. The Prandtl number is a dimensionless quantity, a ratio of momentum diffusivity (kinematic viscosity, $\nu$) to thermal diffusivity ($\alpha$). The Prandtl number alone does not help describe fluid behavior for turbulent flow, hence the inclusion of the turbulent Prandtl number, Prt. Together, these quantities help identify if momentum diffusivity or thermal diffusivity dominates the fluid flow for both laminar and turbulent flow. Values for Pr Prt of 0.7 and 1.0, respectively are typical for modeling air.

2.3 turbulenceProperties

The turbulenceProperties file informs OpenFOAM on how us, the user, wishes to model turbulence. We can see how turbulence modeling is defined below in this file:

      
simulationType RAS;                     // type of simulation to run
RAS                                     // dictionary containing turbulence model parameters
{ 
    RASModel        SpalartAllmaras;    // specific turbulence model to use
    turbulence      on;                 // whether to model turbulence or only laminar flow
    printCoeffs     off;                // whether to print turbulence model coefficients
} 

For our NACA0012 case, we elect a Reynolds Averaged Simulation (RAS). There are other options available for the user which are outlined in the advanced user guide. Within the RAS dictionary we choose to use the SpalartAllmaras turbulence model. This is a very commonly used one-equation (solves a single transport equation) model using the ‘Spalart-Allmaras variable’, nuTilda. We elect this model typically due to it’s simplicity and low computational cost. An exhaustive discussion on turbulence models in OpenFOAM can be found in the advanced user guide.

The following entry, turbulence on; tells OpenFOAM to model turbulent flow, not just laminar flow. printCoeffs off; refers to the coefficients used in the Spalart-Allmaras turbulence model equation. These coefficients are beyond the scope of this user guide, and for the purposes of our NACA0012 case, we do not need to adjust these coefficients for this turbulence model hence we do not print these coefficients. For more advanced applications, these coefficients may need to be adjusted which would make printCoeffs on; a sensible entry.

3. Flow Discretization, Simulation Run Parameters, and Solver Settings - system

The system directory is home to most simulation parameters. Within this directory we specify things such as simulation run time, how to decompose the domain (only needed for running a simulation in parallel), as well as solver settings.

      
|-- system               // directory global simulation parameters
  |-- controlDict        // define solver, and simulation runtime parameters
  |-- decomposeParDict   // decompose the simulation domain to run in parallel
  |-- fvSchemes          // solution schemes
  |-- fvSolution         // iterative solution and algorithm parameters

3.1 Simulation Runtime Parameters (controlDict)

Below we can see the various inputs in controlDict. The inputs from the application key to runTimeModifiable are required by OpenFOAM to run the simulation. The following block of code, the functions dictionary, is optional.

Since we seek to run a steady-state case, we elect the simpleFoam solver for the application key. To specify how long we want the simulation to carry out (simulation time) we start the simulation at startTime 0; and tell OpenFOAM to start from this time (startFrom startTime;). We also need to tell OpenFOAM to stop at our specified end time. We use the stopAt endTime; entry for this and prescribe endTime 1000; (where this time is measured in seconds). The time step, deltaT is set to 1 second (this is fine and common practice for steady-state simulations, but for an unsteady simulation we would need a much smaller time step to maintain a stable simulation).

The writeControl key tells OpenFOAM how to measure when to write field values to the disk. Here we prescribe timeStep, effectively telling OpenFOAM we wish to write data at some increment based off the time step of the simulation. This is then a coupled entry with writeInterval; we only need the data at the end of the simulation so we set the writeInterval to 1000 seconds, matching our endTime. Combining writeControl and writeInterval, OpenFOAM understands this to be: “write the data every 1000 time steps”.

The purgeWrite key is an integer representing the limit on the number of time directories stored during a simulation. Here we disable this entry by passing a value of zero. If, for example, this entry were two, then only the latest two time directories will saved throughout the simulation (where old time directories are over written by new ones). For large cases, depending on the application, this can save a lot of storage. Additionally, we turn use writeCompression on; which tells OpenFOAM to store data in compressed files (gzip compression) rather than uncompressed (another method to save storage). We save our data in a human readable format, writeFormat ascii;, though we may also specify to store data as binary. When storing this data, we can choose how many decimal places to record. Here we choose 8: writePrecision 8;. This is the same where recording simulation time (timePrecision 8;). Lastly, we use timeFormat general; which instructs OpenFOAM to use general naming conventions for time directories.

      
application         simpleFoam;    // solver to use
startFrom           startTime;     // where to start simulation from
startTime           0;             // what time the simulation starts at (0 seconds)
stopAt              endTime;       // when to end simulation
endTime             1000;          // end time of simulation
deltaT              1;             // timeStep
writeControl        timeStep;      // when to check for writing data
writeInterval       1000;          // how often to write
purgeWrite          0;             // number of time directories to keep (0 to disable)
writeFormat         ascii;         // ascii or binary data format
writePrecision      8;             // precision in written data 
writeCompression    on;            // compress written data using gzip compression
timeFormat          general;       // format names of time directories
timePrecision       8;             // precision used for simulation time

functions
{
    aeroForces
    {
        type            forceCoeffs;        // type of function
        libs            ("libforces.so");   // library for function
        patches         (wing);             // patch to calculate forces on
        p               p;                  // pressure field to use
        U               U;                  // velocity field to use
        rho             rhoInf;             // density field to use
        rhoInf          1.225;              // SSL density 
        pRef            0;                  // reference pressure
        porosity        no;                 // model porosity effects?
        liftDir         (0 1 0);            // lift direction
        dragDir         (-1 0 0);           // drag direction
        pitchAxis       (0 0 1);            // pitch axis
        CofR            (0.025 0 0);        // rotation axis for momentum calculation
        magUInf         10;                 // magnitude of far field velocity
        lRef            0.1;                // reference length 
        Aref            0.1;                // reference area
        log             true;               // write to log file
        writeControl    timeStep;
        writeInterval   1000;            
    }
}

The next block of code is functions. In OpenFOAM, when the user wants to add some type of calculation, this is typically done within controlDict. As is typical with wing (airfoil for our NACA0012 simulation) cases, we want to simulate this wing in order to get aerodynamic data from it. One way we can do this is with the function aeroForces. The name aeroForces is specified by the user. For incompressible steady-state simulations, these entries can remain mostly unchanged from one case to another. However, there are a few pertinent entries to discuss. We must tell OpenFOAM which patch we want to calculate the aerodynamic forces for via patches (wing);. Additionally, we must prescribe the lift and drag direction in accordance with our global coordinate system. Here, we have lift in acting in the positive y-direction. Drag is in the positive x-direction (though multiplied by negative one to give the force acting on the wing). Our pitch axis is the z axis. As we are modeling an airfoil instead of a wing, we use Aref = lRef = 0.1 which is our chord length. For a wing case, Aref should be the planform area whereas lRef is the mean aerodynamic chord.

3.2 Decomposition of Domain (decomposeParDict)

Often times these CFD simulations last a very long time. To help speed up the process we can run the job in parallel, splitting up the simulation over multiple computer processors. This will greatly decrease the real-world run time of the simulation. To do this, we use decomposeParDict:

      
numberOfSubdomains  8;         // number of processors to split domain over
method              scotch;    // method for decomposition (splitting) domain

As can be seen by the above block, there are very few inputs to this file. Since the NACA0012 case is very light weight (coarse mesh in a steady simulation) we choose to only split (decompose) the domain into 8 distinct parts. These parts will be distributed over 8 processor cores when running. To see this, when running the case, you will find directories processor0 processor1 ... processor7 in your working directory. This is controlled by the numberOfSubdomains entry. For our NACA0012 case, and for most cases, we can choose a very simple and automatic decomposition algorithm, method scotch;. This method is very common to use as it is simple (requires no other entries), and handles everything automatically. There are other decomposition methods available, such as hierarchical, which will require a coeffs entry in decomposeParDict. This method is not needed for now, but for the motivated reader there are a few additional notes in decomposeParDict regarding this.

3.3 Numerical Schemes (fvSchemes)

fvSchemes delves into the particular numerical schemes used to solve the Navier-Stokes equations, particularly how to handle derivatives. An in-depth analysis of the parameters found in fvSchemes is covered in the advanced user guide. For this user guide, we will mostly focus on the basic ideas and define the various dictionaries within fvSchemes. Below is the fvSchemes for our NACA0012 simulation:

   
// time derivative scheme   
ddtSchemes 
{
    default                                       steadyState;
}

// gradient schemes
gradSchemes
{
    default                                       Gauss linear;
}

// divergence schemes
divSchemes
{
    default                                       none;
    div(phi,U)                                    bounded Gauss linearUpwindV grad(U);
    div(phi,T)                                    bounded Gauss upwind;
    div(phi,nuTilda)                              bounded Gauss upwind;
    div(phi,k)                                    bounded Gauss upwind;
    div(phi,omega)                                bounded Gauss upwind;
    div(phi,epsilon)                              bounded Gauss upwind;
    div((nuEff*dev2(T(grad(U)))))                 Gauss linear;
}

// point to point interpolation scheme
interpolationSchemes
{
    default                                       linear;
}

// laplacian scheme
laplacianSchemes
{
    default                                       Gauss linear corrected;
}

// component of gradient normal to surface
snGradSchemes
{
    default                                       corrected;
}

// wall disctance computation method
wallDist
{
    method                                        meshWave;
}

We first take note of ddtSchemes. As we are wanting a steady-state simulation, we elect steadyState for our time derivatives. In an unsteady case, it is common to use entries such as euler or backward. The steadyState entry will zero out time derivatives (will not compute them).

Following this, we specify our gradient schemes (how we interpolate values for the gradient terms). We specify using default Gauss linear;, which applies Gauss linear for all gradient terms. For some background, Gauss denotes the standard finite volume discretization using Gaussian integration which interpolates from cell centers to cell faces (at the center location on the face). Gauss linear is a second order accurate scheme.

The divSchemes dictionary is used to calculate divergence terms. It is generally not recommended to use just one exact type of divergence scheme as different terms are best solved using different methods. Hence, we elect no default option: default none;. The following six terms, div(phi,U) to div(phi,epsilon) represent the divergence schemes for convective terms. In this case, phi represent $\phi=\rho U$. For these terms, with the exception of div(phi,U), we use bounded Gauss upwind;, a first-order bounded accurate scheme which uses upwind differencing. For div(phi,U) we choose bounded Gauss linearUpwindV grad(U); which is second order accurate using linear upwind differencing. The V term denotes a specialized version of the scheme designed for vector fields and the grad(U) is used for correction. For these schemes, the bounded keyword helps prevent unphysical oscillations/overshoot in the solution. Lastly, we specify div((nuEff*dev2(T(grad(U))))) which is the viscous stress divergence term (the diffusive term in the momentum equation of the Navier-Stokes equations). For this term we apply Gauss linear; which uses central differencing (second order accurate).

The interpolationSchemes dictionary is used as a general setting for interpolating values, typically from cell centers to face centers. For this we use linear which is central differencing. The laplacianSchemes dictionary contains solution schemes for our Laplacian (divergence of gradient) terms. Here we apply a default setting of Gauss linear corrected. These terms tend to not be as tricky (in terms of stability) as our divergence terms (divSchemes). Hence, we are able to apply a default setting.

The next dictionary, snGradSchemes, pertains to surface normal gradients. This dictionary computes surface normal gradients which are required to compute a Laplacian term using Gaussian integration. As this calculation requires orthogonality at the wall to maintain second order accuracy, we must be careful with our default setting. If the mesh maintains high orthogonality then we can specify default orthogonal. This is fairly uncommon, most applications will not have a very high orthogonality at the wall, such as our NACA0012 mesh. To circumvent this, we use default corrected; which applies a correction for orthogonality to help maintain second order accuracy. The final entry is the wallDist dictionary. This dictionary handles calculating the distance to the nearest patch for all cells and boundaries. For this we use method meshWave;. This method works best for regular, undistorted meshes (low skewness, high orthogonality). For most cases, however, this method works well and is employed here (there is an optional correction term that can be added correctWalls true; which can correct the calculation for skewed/distorted meshes but this option is not needed for our NACA0012 case).

3.4 Iterative Solution and Algorithm Parameters (fvSolution)

In fvSolution, the equation solvers, their associated tolerances, and algorithms are defined. Below is the fvSolution for the NACA0012 case:

      
// dictionary for SIMPLE algorithm
SIMPLE
{
    consistent                         false;
    nNonOrthogonalCorrectors           0;
}

// potential flow orthogonality correction
potentialFlow
{
    nNonOrthogonalCorrectors           20;
}

// solver settings and tolerances
solvers
{
    "(p|p_rgh|G)"
    {
        
        solver                         GAMG;
        smoother                       GaussSeidel;
        relTol                         0.1;
        tolerance                      0;
    }

    Phi
    {
        $p;
        relTol                         0;
        tolerance                      1e-6;
    }

    "(U|T|e|h|nuTilda|k|omega|epsilon)"
    {
        solver                         smoothSolver;
        smoother                       GaussSeidel;
        relTol                         0.1;
        tolerance                      0;
        nSweeps                        1;
    }
}

// relaxation factors
relaxationFactors
{
    fields
    {
        "(p|p_rgh)"                         0.30;
    }

    equations
    {
        "(U|T|e|h|nuTilda|k|epsilon|omega)" 0.70;
    }
}

Similar to fvSchemes, most higher level details of this file are best suited and covered in the advanced user guide. Here we define the dictionaries and discuss the main ideas behind them.

The first dictionary, SIMPLE, where we define both consistent and nNonOrthogonalCorrectors. There are two variations of the SIMPLE algorithm in OpenFOAM: SIMPLE and SIMPLEC. The default algorithm, what we use for the NACA0012 case, is SIMPLE. We can specify SIMPLEC by setting consistent yes;. Additionally, we set nNonOrthogonalCorrectors 0;. This entry controls the extra amount of iterations performed in the SIMPLE algorithm to help account for non-orthogonal meshes. If the mesh is reasonably orthogonal (of good quality), then using zero correction is fine. However, in the following dictionary, potentialFlow, we set nNonOrthogonalCorrectors 20;. This dictionary relates to solving the potential flow equation (Laplace’s equation for velocity potential), $\nabla \cdot (\nabla \phi)$. This is a single linear solve meaning we rely on specifying iterations (nNonOrthogonalCorrectors) to converge the equation. A value of 20 is very typical for most general applications but this can be adjusted as needed.

The following dictionary is the solvers dictionary. In all sub-dictionaries, we specify a solver and smoother to begin (the Phi subdictionary uses the same settings as the "(p|p_rgh|G)" sub-dictionary, denoted by the use of $p). The details for solvers and smoothers are beyond the scope of this user guide (reference the advanced user guide). The quantities here of most importance for now are the tolerances. For all sub-dictionaries we specify a tolerance that we wish OpenFOAM to reach in the solution. A tolerance of 0 is very tight, and difficult to meet (taking machine precision to be zero). We can relax this requirement by specifying relTol. A relTol, which stands for relative tolerance, gives how much we want our baseline residual to drop. So a relative tolerance of 0.1 gives that when the residual has dropped by a factor of 10, the solution has sufficiently converged. It should be noted at this point that we define different solver settings for various field values (i.e. not all field values use the same solvers/settings, we use 3 total sub-dictionaries to define different solvers/settings). As was the case with fvSchemes; simply put, different field values benefit from particular solvers/settings (this topic again, covered in the advanced user guide).

Lastly we define relaxation factors within the relaxationFactors dictionary. These factors can help converge the solution much faster. We define two sub-dictionaries: fields and equations. The fields relaxation factors help stabilize the pressure calculations after solving the equation for that field whereas the equations relaxation factors are applied to actual computation of that field. The values used, 0.3 and 0.7, are fairly typical/standard and can work well for most cases meaning that often times, these do not have to be adjusted.

4. run, clean, and paraview.foam

This final section pertains to the last 3 files that we are yet to cover: run, clean, and paraview.foam. These files are simple and pertain to running the case and post-processing.

clean

The clean (bash) script is used to delete all generated files/directories after running a simulation. This is a bash script, which only uses the rm -rf command to delete these generated files/directories, and should be executed before re-running the simulation. Not running this executable before re-running can often times cause OpenFOAM to terminate the simulation right away.

paraview.foam

The paraview.foam file is a dummy file used by ParaView. It is an empty file that the user loads into ParaView to visualize the results. When the user loads this file into ParaView, ParaView will automatically extract all data from the case for post-processing.

run

The run (bash) script is used to actually begin the simulation:

      
source $WM_PROJECT_DIR/etc/bashrc                   # source OpenFOAM
cd "${0%/*}" || exit                                # run from this directory
. ${WM_PROJECT_DIR:?}/bin/tools/RunFunctions        # tutorial run functions
#------------------------------------------------------------------------------

# whether to run job in parallel or not
parallel=true

# copy initial field/BCs
cp -r 0.orig 0

# run simulation
if [ "$parallel" = true ]
then

    runApplication decomposePar
    runParallel $(getApplication)
    runApplication reconstructPar 
    rm -rf processor*

else

    runApplication $(getApplication)

fi

This script sources OpenFOAM and its run functions to begin. $WM_PROJECT_DIR is an environment variable that points to the OpenFOAM installation to help easily source OpenFOAM. Hence, we can source OpenFOAM by running source $WM_PROJECT_DIR/etc/bashrc. In a similar manner we also source OpenFOAM’s run functions (such as runApplication and runParallel seen later in the file) by running . ${WM_PROJECT_DIR:?}/bin/tools/RunFunctions. Running cd "${0%/*}" || exit tells OpenFOAM to run within the directory that the case resides, not necessarily the directory from which the run script is executed. This is not a needed line, but it offers some convenience to the user.

It is recommended to run this NACA0012 case in parallel as that will speed up the time it takes for the simulation to complete. However, here we have included this as an option to show the user how a case can be run in parallel or in serial. If parallel=true then the case runs in parallel. If false, then we run the case in serial. Hence the use of the if-else statement. For our parallel run, we first run runApplication decomposePar (only after copying our boundary conditions with cp -r 0.orig 0) which decomposes the domain according to decomposeParDict. After the decomposition, we run the simulation via runParallel $(getApplication) which looks into system/controlDict, gets the application (from Sec. 3.1, this is application simpleFoam;), and begins the simulation. Once the simulation is over, we reconstruct the domain (the opposite of decomposing the domain) via runApplication reconstructPar. This is not absolutely necessary to run, but it will save a lot of disk space, especially for larger simulations. Finally, we end with rm -rf processor* which deletes all of the processor directories. These directories contain our solution. However, they have been reconstructed (runApplication reconstructPar) and are therefore redundant and no longer needed.