Parametrizing an OpenFOAM case

It is quite common to run simulations of models for different values of parameters. For example, running mesh studies to see what mesh size is necessary to get good solutions, or testing how the solution varies with physical properties (e.g. sensitivity analysis, emulation, etc.), or modifying the domain geometry to optimize some design.

In this post I describe a very simple way to parametrize an OpenFOAM (version 8) case folder using python (specifically template string). This allows to configure the simulation using the power of python.

UPDATE: (10.2023) In OpenFOAM v10 and later the notation for macros in dictionary files has changed. In the versions posterios to v8 you need to use ! instead of : (used herein) to refer to the highest level in the file; and / instead of . (used herein) for fields inside a file or dictionary. For a description of the new macrop expansions visit the official documentation

To achieve our goal we will go in steps:

  1. arrange the case information in a way suitable for programmatic parametrization

  2. convert case files into python string templates.

The methodology will be applied to boundary conditions of a model, but I am sure you will be able to apply the same idea to parametrize any aspect of an OpenFOAM case folder. It is so simple!

Arranging the case information

The official OpenFOAM documentation (and all tutorials I’ve seen) split the boundary conditions (BC from now on) into the field files. This is unavoidable because it is the way OpenFOAM works, but luckily the developers provide ways to include other files and perform macro expansions. With this tools we will be able to arrange the BC in such a way that they can be easily parametrized.

Consider the pitzDaily case folder, distributed together with OpenFOAM

pitzDaily/
├── 0
│   ├── epsilon
│   ├── f
│   ├── k
│   ├── nut
│   ├── nuTilda
│   ├── omega
│   ├── p
│   ├── U
│   └── v2
├── constant
│   ├── momentumTransport
│   └── transportProperties
└── system
    ├── blockMeshDict
    ├── controlDict
    ├── fvSchemes
    ├── fvSolution
    └── streamlines

Each field file, inside the folder 0, defines the initial value of the field and its boundary conditions on all boundary patches. That is the BC are split over the fields. This is evidently practical for parsing the information into the solver, but it is not very aligned with the logical way the mathematical model is setup. More commonly BC are defined over all fields, and they are associated with some physical entity. For example, we think of the BC imposed by a rigid wall and the effects it has on all the fields in the model. The splitting of the BC over all the field files make it very difficult i) to review and debug a given BC, ii) to edit and modify the BC, and iii) to understand a model written by somebody else.

As an example, take the pressure field (file p in folder 0); it contains:

FoamFile
{
    version     2.0;
    format      ascii;
    class       volScalarField;
    object      p;
}

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

internalField   uniform 0;

boundaryField
{
    inlet { type            zeroGradient; }
    outlet
    {
        type            fixedValue;
        value           uniform 0;
    }
    upperWall { type            zeroGradient; }
    lowerWall { type            zeroGradient; }
    frontAndBack { type            empty; }
}

The boundaryField dictionary must reference the boundary patches in the model, leading to yet another inefficiency in this way of arranging the information: the BC is repeated for all the boundary patches in the same physical class. In the file above you see that the same BC is imposed on the patches upperWall and lowerWall; because both are walls with the same behavior.

The idea here is to describe the BC corresponding to a physical entity, e.g. a wall, as a single unit that we can then reference in the field file. The end result of this re-arrangement in the pressure field file above will be the following

FoamFile
{
    version     2.0;
    format      ascii;
    class       volScalarField;
    object      p;
}

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

#include "../include/caseParameters"

internalField   $initialValue.p;

boundaryField
{
    inlet     { $:inletBC.p; }
    outlet    { $:outletBC.p; }
    upperWall { $:wallBC.p; }
    lowerWall { $:wallBC.p; }
    frontAndBack { type empty;}
}

You can see that all patches representing walls receive the same boundary condition: wallBC. The boundary patches are now associated with a class of physical boundary conditions. For example we could read upperWall { $:wallBC.p; }1 as

The patch named upperWall imposes a wall boundary condition, in this case on the field p.

Also, once parametrized like this, the file is purely logical, and needs no edition anymore. This is a logical description of the problem, which makes it easier to review/debug, edit, understand.

The information about the initial values and the BC are stored in the file caseParameters residing in a new folder include at the top level of the case folder:

pitzDaily/
├── 0
├── include
│   └── caseParameters
├── constant
└── system

In this example we use a single file called caseParameters which defines boundary conditions and initial values of all the fields. This might make sense in general because many BC parameters coincide with the initial value of the field. However one could use several files, e.g. one could split the data into the files initalValues, boundaryConditionParameters, boundaryConditions, containing the initial values, the parameters used in BC, and the definition of the BC, respectively. In the latter case, any scenario for which the type of boundary condition do not change (but the parameters do), boundaryConditions becomes an static file.

The main idea is that caseParameters (or boundaryConditions) centralizes all BC. For the pitzDaily example, the contents of the caseParameters are

FoamFile
{
    version     2.0;
    format      ascii;
    class       dictionary;
    object      boundaryConditions;
}

initialValue
{
    v2      0.25;
    U       (0 0 0);
    U_inlet (10 0 0);
    p       0;
    omega   440.15;
    nuTilda 0;
    nut     0;
    k       0.375;
    f       0;
    epsilon 14.855;
}

// Boundary conditions

inletBC
{
    v2
    {
            type    fixedValue;
            value   $:initialValue.v2;
    }
    U
    {
            type    fixedValue;
            value   uniform $:initialValue.U_inlet;
    }
    p     { type    zeroGradient; }
    omega
    {
            type   fixedValue;
            value  $:initialValue.omega;
    }
    nuTilda
    {
            type   fixedValue;
            value  uniform $:initialValue.nuTilda;
    }
    nut
    {
            type   calculated;
            value  uniform $:initialValue.nut;
    }
    k
    {
            type   fixedValue;
            value  uniform $:initialValue.k;
    }
    f     { type   zeroGradient; }
    epsilon
    {
        type   fixedValue;
        value  uniform $:initialValue.epsilon;
    }

}

outletBC
{
    v2 {      type    zeroGradient; }
    U  {      type    zeroGradient; }
    p
    {
              type   fixedValue;
              value  uniform $:initialValue.p;
    }
    omega   { type   zeroGradient; }
    nuTilda { type   zeroGradient; }
    nut
    {
              type   calculated;
              value  uniform $:initialValue.nut;
    }
    k       { type   zeroGradient; }
    f       { type   zeroGradient; }
    epsilon { type   zeroGradient; }

}

wallBC
{
    v2
    {
              type    v2WallFunction;
              value   $:initialValue.v2;
    }
    U       { type   noSlip; }
    p       { type   zeroGradient; }
    omega
    {
              type   omegaWallFunction;
              value  $:initialValue.omega;
    }
    nuTilda { type   zeroGradient; }
    nut
    {
              type   nutkWallFunction;
              value  uniform $:initialValue.nut;
    }
    k
    {
              type   kqRWallFunction;
              value  uniform $:initialValue.k;
    }
    f
    {
              type   fWallFunction;
              value  uniform $:initialValue.f;
    }
    epsilon
    {
              type   epsilonWallFunction;
              value  uniform $:initialValue.epsilon;
    }
}

This is file contains a semantic structure of the BC. The BC describe a physical class of entities and are detached from the specific boundary patch that instantiates that entity (that’s still in the field files). The dictionaries defining the BC specify their effect on all fields. I hope you agree that now it way easier to review and edit the BC.

Note also that the BC are fully parametrized with the values in the initialValue dictionary. Again, one could put the BC in a separate file that would never change.

Parametrizing with python string templates

Since the specification of the BC are now centralized in the caseParameters file we can make this file easy editable by a python program. A very simple way to do this is to convert this file into a python string template. The only issue to solve is that OpenFOAM’s macro expansion uses the same delimiters as the python string Templates, in a non-compatible way. Therefore we derive a new template using a different delimiter. Here is a definition of one using @ as delimiter:

class CaseTemplate(string.Template):
    """ String template with arbitrary delimiter. """
    delimiter = '@'

We parametrized the initialValue dictionary in caseParameters file, which now looks like this:

FoamFile
{
    version     2.0;
    format      ascii;
    class       dictionary;
    object      boundaryConditions;
}

initialValue
{
    v2      @v2;
    U       @U;
    U_inlet @U_inlet;
    p       @p;
    omega   @omega;
    nuTilda @nuTilda;
    nut     @nut;
    k       @k;
    f       @f;
    epsilon @epsilon;
}

The rest of the file remains the same.

For the sake of clarity, the file is now saved into caseParameters.tmpl to easily indicate that it is a template file (the actual extension is arbitrary, choose what you like).

The following program would then recreate the original parameter file (that can be copied over to the case folder):

import string
from pathlib import Path


class CaseTemplate(string.Template):
    """ String template with arbitrary delimiter. """
    delimiter = '@'


case_folder = Path('pitzDaily')
template_file = case_folder / 'include' / 'caseParameters.tmpl'
template = CaseTemplate(template_file.read_text())
content = template.substitute(
                              v2=0.25,
                              p= 0,
                              omega=440.15,
                              nuTilda=0,
                              nut=0,
                              k=0.375,
                              f=0,
                              epsilon=14.855,
                              U=str((0, 0, 0)).replace(',', ''),
                              U_inlet=str((10, 0, 0)).replace(',', ''),
          )

case_file = template_file.with_name('caseParameters')
case_file.write_text(content)

Note how the vector parameters (the velocity fields U) need special care to make their representation compatible with OpenFOAM. Of course, you can now write programs that generate simulations with any parameter values.

You can find implementation of these ideas in the WABEsense project’s repository. My intention is to distribute a package with this functionality when the ideas are mature.

Stay tuned!


  1. In OF v10, the colon : shoud be replaced with exclamation !, and the dot . with a slash /↩︎

comments powered by Disqus