Skip to content

OpenFOAM Cases

In this chapter we will look at the file structure and organisation of OpenFOAM cases.

Normally, the name assigned to the case becomes the name of the directory in which all the case files and subdirectories are stored.

The case directories themselves can be located anywhere. But, if they are within a run subdirectory of the project directory, eg.$HOME/OpenFOAM/$[USER]-vXXXX/run, and since the $FOAM_RUN environment variable is set to this PATH by default; we can quickly move to that directory by executing a preset alias, run, at the command line.


File Structure of OpenFOAM cases

The most basic directory structure for an OpenFOAM case:

flowchart TD
    A(case) --> B(system);
    A --> C(constant);
    A --> D(time directories);
    B --> controlDict --> fvSchemes --> fvSolutions --> blockMeshDict;
    C --> xProperties;
    C --> E(polyMesh) --> boundary --> faces --> neighbour --> owner --> points;

A system directory

For setting parameters associated with the solution procedure itself. It contains at least the following 3 files: controlDict where run control parameters are set including start/end time, time step and parameters for data output; fvSchemes where discretisation schemes used in the solution may be selected at run-time; and, fvSolution where the equation solvers, tolerances and other algorithm controls are set for the run.

A constant directory

Contains a full description of the case mesh in a subdirectory polyMesh and files specifying physical properties for the application concerned, eg. transportProperties.

A time directory

Containing individual files of data for particular fields. The data can be: either, initial values and boundary conditions that the user must specify to define the problem; or, results written to a file by OpenFOAM. The name of each time directory is based on the simulated time at which the data is written.

The OpenFOAM fields must always be initialised, even when the solution does not strictly require it, as in steady-state problems.

It is sufficient to say now that since we usually start our simulations at time t = 0 , the initial conditions are usually stored in a directory named 0 or 0.000000e+00, depending on the name format specified. For example, in the cavity tutorial, the velocity field \(\textbf{U}\) and pressure field \(p\) are initialised from files .../0/U and .../0/p respectively.


Basic I/O file format

OpenFOAM needs to read a range of data structures such as strings, scalars, vectors, tensors/matricies, lists and fields. The input/output (I/O) format of files is designed to be extremely flexible to enable the modifying the I/O in OpenFOAM applications as easily as possible.

The I/O follows a simple set of rules that make the files extremely intuitive to understand.

General syntax rules

The format follows some of the general principles of OpenFOAM’s C++ source code.

  1. Files have a free form. Columns have no meaning assigned to them and continuation across lines needs no indication.
  2. Lines have no particular meaning, except that // makes OpenFOAM ignore any text that follows it until the end of the line.
  3. A comment over multiple lines is done my eclosing text between /* and */.

//, /* and */ are called Delimiters

Dictionaries

OpenFOAM uses dictionaries as the most common way of specifying data.

A Dictionary/Dict is a data structure that contains a set of data entries that can be retrieved by the I/O by specifying the keyword.

The keyword entries follow the general format:

<keyword>  <dataEntry1> ... <dataEntryN>;

Most entries, however, are single data entries:

<keyword>  <dataEntry>;

Most OpenFOAM data files are dictionaries themselves. The format for a dictionary, in this case, is to specify the dictionary name followed the entries enclosed in curly braces {} as follows:

<dictionaryName>
{
    ... keyword entries ...
}

Dictionaries provide the means for organising entries into logical categories and can be specified hierarchically so that any dictionary can itself contain one or more dictionary entries.

The data file header

All data files read and written by OpenFOAM begin with a dictionary named FoamFile containing a standard set of keyword entries:

Keyword Description Entry
version I/O format version 2.0
format Data format ascii/binary
location PATH to the file, in “…” (optional)
class OpenFOAM class constructed from the concerned data file typicaly dictionary or a field, eg. volVectorField
object Filename eg. controlDict

The class entry is the name of the C++ class in the OpenFOAM library that will be constructed from the data in the file. Without knowledge of the underlying code which calls the file to be read, and knowledge of the OpenFOAM classes, the user will probably be unable to surmise the class entry correctly. However, most data files with simple keyword entries are read into an internal dictionary class and therefore the class entry is dictionary in those cases.

The extract, below, from an fvSolution dictionary file, contains 2 dictionaries, solvers and PISO.

17
18solvers
19{
20    p
21    {
22        solver          PCG;
23        preconditioner  DIC;
24        tolerance       1e-06;
25        relTol          0.05;
26    }
27
28    pFinal
29    {
30        $p;
31        relTol          0;
32    }
33
34    U
35    {
36        solver          smoothSolver;
37        smoother        symGaussSeidel;
38        tolerance       1e-05;
39        relTol          0;
40    }
41}
42
43PISO
44{
45    nCorrectors     2;
46    nNonOrthogonalCorrectors 0;
47    pRefCell        0;
48    pRefValue       0;
49}
50
51
52// ************************************************************************* //

The solvers dictionary contains multiple data entries for the solver to be used and tolerances for each of the pressure and velocity equations, represented by the p and U keywords respectively. The PISO dictionary contains algorithm controls.

The PISO algorithm (Pressure Implicit with Splitting of Operators) is an efficient method to solve the Navier-Stokes equations in unsteady problems.

Lists

OpenFOAM applications contain lists, eg. a list of vertex coordinates for a mesh description. Lists are commonly found in I/O and have a format of their own in which the entries are contained within parentheses ( ).

There is also a choice of format preceeding the parentheses:

The keyword is followed immediately by parentheses;

<listName>
(
    ... entries ...
);

The keyword is followed by the number of elements <n> in the list;

<listName>
<n>
(
    ... entries ...
);

The keyword is followed by a class name identifier label <type>. Where, <type> states what the list contains;

<listName>
List<scalar>
<n>         //optional
(
    ... entries ...
);

<scalar> in List<scalar> is not a generic name but the actual text that should be entered.

This example is for a list of scalar elements, for a list of vector elements use List<vector>

The simple format is a convenient way of writing a list. The other formats allow the code to read the data faster since the size of the list can be allocated to memory in advance of reading the data.

The simple format is therefore preferred for short lists, where read time is minimal, and the other formats are preferred for long lists.

Scalars, Vectors and Tensors

A scalar is a single number represented as suck in a data file.

A vector is a VectorSpace of rank 1 and dimension 3. A vector is written;

(1.0 1.1 1.2)

Since the number of elements in a vector is always fixed to 3, the simple List format is used.

A tensor is a VectorSpace of rank 2 and dimension 3, therefore data entries are always fixed to 9 real numbers. The identity tensor can be written as;

(
    1 0 0
    0 1 0
    0 0 1
)

A Tensor is just a Matrix.

Since most studies will be in 3D, our tensor is 3x3. In the case that a study is 2D, our 3D identity tensor simply becomes;

(
    1 0 0
    0 1 0
    0 0 0
)

A VectorSpace is a set whose elements, vectors, can be added together and multiplied, scaled, by scalars. The dimension of a VectorSpace is the number of vectors in any basis for the space to be spanned, represents the ‘3’ in ‘3D’. The rank is the dimension of the column space.

Dimensional Units

In contiuum mechanics, properties are represented in some chosen units. Algebraic operations must be performed on these properties using consistent units of measurement; in particular, addition, subtraction and equality are only physically meaningful for properties of the same dimensional units. As a safeguard against implementing a meaningless operation, OpenFOAM attaches dimensions to field data and physical properties and performs dimension checking on any tensor operation.

The I/O format for a dimensionSet is 7 scalars delimited by square brackets;

[0 2 -1 0 0 0 0]

Where each of the values in the dimensionSet correspond to the power of each of the base units of measurements;

No. Property SI Unit
1 Mass kilogram (kg)
2 Length metre (m)
3 Time second (s)
4 Temperature Kelvin (K)
5 Quantity kilogram-mole (kgmol)
6 Current ampere (A)
7 Luminous Intensity candela (cd)

We can now see that [0 2 -1 0 0 0 0] corresponds to \(m^2 s^{-1}\) (kinematic viscosity, \(\nu\) ).

Dimensioned Types

Physical properties are typically specified with their associated dimensions. These entries have the format that the following example of a dimensionedScalar demonstrates:

nu          [0 2 -1 0 0 0 0]  1;

nu is the keyword; the next entry is the dimensionSet; the final entry is the scalar value

Fields

Much of the I/O data in OpenFOAM are tensor fields that are read from and written into the time directories. OpenFOAM writes field data using keyword entries;

Keyword Description Example
dimensions Dimensions of field [1 1 -2 0 0 0 0]
internalField Value of internal field uniform (1 0 0)
boundaryField Boundary Field see note below

The boundaryField is a dictionary containing a set of entries whose names correspond to each of the names of the boundary patches listed in the boundary file in the polyMesh directory. Each patch entry is itself a dictionary containing a list of keyword entries. The compulsory entry, type, describes the patch field condition specified for the field. The remaining entries correspond to the type of patch field condition selected and can typically include field data specifying initial conditions on patch faces (covered later)

The data begins with an entry for its dimensions. Following that, is the internalField, described in one of two ways;

A single value is assigned to all elements within the field;”

internalField uniform <entry>;

Each field element is assigned a unique value from a list;

internalField nonuniform <List>;

The Token Identifier form of List is recommended

An example set of field dictionary entries for velocity U are below:

17dimensions      [0 1 -1 0 0 0 0];
18
19internalField   uniform (0 0 0);
20
21boundaryField
22{
23    movingWall
24    {
25        type            fixedValue;
26        value           uniform (1 0 0);
27    }
28
29    fixedWalls
30    {
31        type            noSlip;
32    }
33
34    frontAndBack
35    {
36        type            empty;
37    }
38}
39
40// ************************************************************************* //

Directives and Macro Substitutions

There is additional file syntax that offers great flexibility for the setting up of OpenFOAM case files, namely directives and macro substitutions.

Directives are commands that can be contained within case files that begin with the hash (#) symbol.

Macro substitutions begin with the dollar ($) symbol.

At present there are 4 directive commands available:

Reads the file of name <fileName>

#include "<fileName>"
//or
#includeIfPresent "<fileName>"

Has the following options:

#default
//provide default value if entry is not already defined

#overwrite
//silently overwrites existing entries

#warn
//warn about duplicate entries

#error
//error if any duplicate entries occur

#merge
//merge sub-directories when possible (the default mode)

Removes any included keyword entry; takes either a word or regular expression

#remove <keywordEntry>

Followed by verbatim C++ code, compiles, loads and executes the code on-the-fly to generate the entry.

The #include and #inputMode Directives

For example, let us say we wish to set an initial value of pressure once to be used as the internal field and initial value at a boundary. We could create a file, initialConditions, which contains the following entries:

pressure 1e+05;
#inputMode merge
In order to use this pressure for both the internal and initial boundary fields, we would simply include the following macro substitutions in the pressure field file p:
#include "initialConditions"
internalField uniform $pressure;
boundaryField
{
    patch1
    {
        type fixedValue;
        value $internalField;
    }
}

This is a fairly trivial example that simply demonstrates how this functionality works. However, the functionality can be used in many, more powerful ways particularly as a means of generalising case data to suit our needs. For example, if we have a set of cases that require the same RAS turbulence model settings, a single file can be created with those settings which is simply included in the turbulenceProperties file of each case. Macro substitutions can extend well beyond a single value so that, for example, sets of boundary conditions can be predefined and called by a single macro. The extent to which such functionality can be used is almost endless.

The #codeStream Directive

The #codeStream directive takes C++ code which is compiled and executed to deliver the dictionary entry. The code and compilation instructions are specified through the following keywords:

  1. code: specifies the code, called with arguments OStream& os and const dictionary& dict which the user can use in the code, eg. to lookup keyword entries from within the current case dictionary (file).

  2. codeInclude (optional): specifies additional C++ #include statements to include OpenFOAM files.

  3. codeOptions (optional): specifies any extra compilation flags to be added to EXE_INC in Make/options.

  4. codeLibs (optional): specifies any extra compilation flags to be added to LIB_LIBS in Make/options.

Code, like any string, can be written across multiple lines by enclosing it within hash-bracket delimiters,#{ … #}. Anything in between these delimiters becomes a string with all newlines, quotes, etc. preserved.

An example of #codeStream is given below. The code in the controlDict file looks up dictionary entries and does a simple calculation for the write interval:

startTime       0;
endTime         100;
...
writeInterval   #codeStream
{
    code
    #{
        scalar start = readScalar(dict.lookup("startTime"));
        scalar end = readScalar(dict.lookup("endTime"));
        label nDumps = 5;
        os << ((end - start)/nDumps);
    #};
};