High-Level Programming of Openfoam Applications, and A First Glance at C++
High-Level Programming of Openfoam Applications, and A First Glance at C++
Learning outcomes
• You will get a suggested way of working with your own developments of applications.
• You will understand the vary basic parts of a C++/OpenFOAM code.
• You will understand the wmake compilation procedure for applications, and how it is related
to compilation with the g++ compiler and make.
• You will step-by-step from scratch implement and understand the purpose of the most gen-
eral high-level parts of OpenFOAM solvers.
The instructions are inspired by the codes found in $FOAM_APP/test and of course in $FOAM_APP.
See also the Programmers guide, at:
https://sourceforge.net/projects/openfoamplus/files/v1806/ProgrammersGuide.pdf
(linked to from https://www.openfoam.com/documentation/)
rm Test-onlyMainFunction $FOAM_USER_APPBIN/Test-onlyMainFunction
onlyMainFunction.C
EXE = $(FOAM_USER_APPBIN)/Test-onlyMainFunction
touch Make/options
• Compile:
wmake
Test-onlyMainFunction
11 12 13
A tensor T = 21 22 23 is defined line-by-line in OpenFOAM:
31 32 33
tensor T( 11, 12, 13, 21, 22, 23, 31, 32, 33);
However, we can’t just put this line in the main() function definition, since C++ does not know
what a tensor is. You can try if you like!
We need to declare the tensor class and link to its definition, which is in the shared object file
of the library to which the tensor class belongs.
The Foam:: at the final line is because the compiler complained that the function is ambiguous,
i.e. it exists in more than one namespace. Therefore we explicitely say that we want to use the
function that belongs to namespace Foam. It has to be done without using namespace Foam.
dimensionedTensor sigma
(
"sigma",
dimensionSet( 1, -1, -2, 0, 0, 0, 0),
tensor( 1e6, 0, 0, 0, 1e6, 0, 0, 0, 1e6)
);
Info<< "Sigma: " << sigma << endl;
You see that the object sigma belongs to the dimensionedTensor class that
contains both the name, the dimensions and values.
• See $FOAM_SRC/OpenFOAM/dimensionedTypes/dimensionedTensor
#include "fvCFD.H"
• OpenFOAM uses the finite volume method (fvm) to discretize the PDEs, and there are
many classes in OpenFOAM that are related to fvm.
• OpenFOAM provides the header file fvCFD.H that only includes other header files re-
lated to fvm, including the tensor classes we have discussed. It can therefore be used to
reduce the number of header files. It in fact also ends with using namespace Foam.
• Exchange all the lines before the main() function with:
#include "fvCFD.H"
EXE_INC = \
-I$(LIB_SRC)/finiteVolume/lnInclude \
-I$(LIB_SRC)/meshTools/lnInclude
We will now investigate a polyMesh, an fvMesh, and a geometricField, but for that we need
a base code...
Base code
• For the base code we are inspired both by $FOAM_APP/test/mesh/Test-mesh.C and
$FOAM_SOLVERS/incompressible/icoFoam/icoFoam.C
• We need a code that starts with (explanations coming later):
<header files>
int main(int argc, char *argv[])
{
#include "setRootCase.H"
#include "createTime.H"
where the <header files> can preferrably be fvCFD.H.
• We use a special functionality to generate a template application:
cd $WM_PROJECT_USER_DIR/applications/myTests
foamNewApp meshAndField
cd meshAndField
wmake
meshAndField
It complains that it can’t find a system/controlDict. We will see why soon.
Base code
The base code (meshAndField.C) contains a commented header, and:
#include "fvCFD.H"
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
Info<< nl;
runTime.printExecutionTime(Info);
return 0;
}
Base code
• int argc, char *argv[]
The arguments to the main function are the number of flags and the flags supplied when
running the code.
• setRootCase.H (find it with find $FOAM_SRC -name setRootCase.H)
Not a header file, just a piece of code:
Foam::argList args(argc, argv);
if (!args.checkRootCase())
{
Foam::FatalError.exit();
}
This constructs the object args, and uses it to check that we are running the code in a case
directory. It simply checks if there is an appropriate system/controlDict(!)
• createTime.H (find it with find $FOAM_SRC -name createTime.H)
Not a header file, just a piece of code:
Foam::Info<< "Create time\n" << Foam::endl;
Foam::Time runTime(Foam::Time::controlDictName, args);
This writes some text and constructs the runTime object of the class Time.
Base case
We need a case to run our code (here the cavity case with only four cells):
cp -r $FOAM_TUTORIALS/incompressible/icoFoam/cavity/cavity $FOAM_RUN/cavityFourCells
sed -i s/"20 20 1"/"2 2 1"/g $FOAM_RUN/cavityFourCells/system/blockMeshDict
blockMesh -case $FOAM_RUN/cavityFourCells
meshAndField -case $FOAM_RUN/cavityFourCells
Examine a polyMesh
Add after the line with #include "createTime.H" (from test/mesh):
Info<< "Create mesh" << endl;
polyMesh mesh
(
IOobject
(
fvMesh::defaultRegion,
runTime.timeName(),
runTime,
IOobject::MUST_READ
)
);
Examine a polyMesh
• polyMesh mesh ( ... )
An object named mesh is constructed from the polyMesh class. There is only one argument
to the constructor, which is an IOobject. The IOobject itself takes four arguments that are
not obvious. We can make the educated guess that it reads the mesh from
constant/polyMesh. At some point we have to stop figuring out exactly how things are
done. Now we are fine relying on how OpenFOAM reads from files.
• cellCentres()
gives the center of all cells and boundary faces.
• cellVolumes()
gives the volume of all the cells.
• faceCentres()
gives the center of all the faces.
However, a polyMesh only has the very basic information of the mesh...
Examine an fvMesh
An fvMesh builds on top of a polyMesh, and has additional attributes.
• Let us examine an fvMesh:
Start by changing the mesh class from polyMesh to fvMesh in meshAndField.C
sed -i s/'polyMesh mesh'/'fvMesh mesh'/g meshAndField.C
Examine an fvMesh
The construction of the fvMesh is done the same way for all solvers that use a static mesh.
Instead of replicating the call for the fvMesh constructor in all the solvers they just have after
#include "createTime.H" the line (see e.g. icoFoam.C)
#include "createMesh.H"
This is as well not a real header file, but just inserts the code (find $FOAM_SRC -name createMesh.H)
Foam::Info
<< "Create mesh for time = "
<< runTime.timeName() << Foam::nl << Foam::endl;
Foam::fvMesh mesh
(
Foam::IOobject
(
Foam::fvMesh::defaultRegion,
runTime.timeName(),
runTime,
Foam::IOobject::MUST_READ
)
);
Clean up your code by doing this, and check that it still works!
Examine a volScalarField
Now we are at a point where we can start implementing a particular PDE solver. Depend-
ing on the problem at hand we want to solve equations for different fields. I.e. the fields to
be constructed depends on the solver. That is why all the solvers include a local file named
createFields.H. See e.g. the original icoFoam directory:
createFields.H icoFoam.C Make
Inside the main(...) function of icoFoam.C we find:
#include "createFields.H"
This refers to the local file in the same directory, which is found by the compiler thanks to the
compiler flag -I.
Examine a volScalarField
We are here inspired by icoFoam/createFields.H, and construct a volScalarField (which
is typedef of a geometricField, and therefore relates to a mesh).
Add in createFields.H:
volScalarField p
(
IOobject
(
"p",
runTime.timeName(),
mesh,
IOobject::MUST_READ,
IOobject::AUTO_WRITE
),
mesh
);
Info<< p << endl;
Info<< p.boundaryField()[0] << endl;
Compile, run and have a look at the output! Note that the above Info statement is in
createFields.H, which is in the beginning of the execution.
Although we are not interested in all the details here we will still have a quick look...
Examine a volScalarField
volScalarField p( IOobject ( ... ) , mesh );
The object p is constructed as a volScalarField, and the constructor takes two arguments.
• IOobject ("p",runTime.timeName(),mesh,IOobject::MUST_READ,IOobject::AUTO_WRITE)
– The first argument is an internal name p, so that we at the end can ask our object for its
name (i.e. the internal name should be the same as the object name).
– The second argument uses the runTime object to determine which time directory to read
from (according to settings in system/controlDict).
– The third argument states that the volScalarField should be related to the fvMesh
corresponding to our object mesh.
– The fourth argument states that it must be read.
– The fifth argument states that the field should be written in the coming time directories
according to the settings in system/controlDict.
• mesh
If the volScalarField is read from a file, as in this case, you just specify mesh here. I
haven’t been interested in checking the details of this, but I guess it is used to specify the
size and structure of the field. If the volScalarField is NOT read from a file the second
argument can be an existing volScalarField that corresponds to the same mesh.
This is the point where we were before. Now we need to specify and discretize the equation...
A familiar example
A call for solving the equation
~
∂ρU ~ − ∇ · µ∇U
+ ∇ · φU ~ = −∇p
∂t
The convecting velocity is treated using the flux φ, and there is a viscosity mu defined somewhere
else. We will get back to that later.
runTime++ increases the time by the value of deltaT specified in controlDict, so that we
do not overwrite the T file in the startTime directory.
runTime.write() tells the code to write out all the fields that are specified with
IOobject::AUTO_WRITE, which is the case for our T field.
This means that in our case we must make sure that the fields are written at time
startTime+deltaT
Have a look at how the kinematic viscosity is read from a dictionary in createFields.H of
the laplacianFoam solver, and copy-paste from the next two slides into our createFields.H
file.
Now, spend some time to clean up the case for this specific solver, not to fool any
future user with settings that are not affecting the solver! Tell me when you’re done!
dimensionedScalar sp
(
"sp",
pow(dimTime,-1),
transportProperties
);
Add to constant/transportProperties:
su 0.02;
sp 0.03;
Run the case and investigate the result.
Later we can have a look at the code to figure out how the source terms are treated exactly.
Now we simply see that su is a dimensionedScalar. It means that it must be expanded and
treated as a field covering the entire computational domain. It will be added to the source term,
b, of the linear system Ax=b. The sp contribution is implemented using the fvm namespace,
which tells us that it will contribute to the coefficient matrix, A, rather than the source term,
b.
We see that the Initial residual jumps up a lot from the previous Final residual, and
is decreasing every time we solve the equation.
The reason is that with this way of writing the source term is given explicitely, and the tem-
perature field of the source term is considered constant each time we solve the equation. We
therefore need to iterate to get the correct solution. This is not efficient, and should be avoided
if possible.
Change fvc to fvm, and you see that the linear solver will only iterate the first time, i.e. we
reach the correct solution directly. Then the sp part of the source term is treated implicitely, as
it should.
See:
$FOAM_SRC/fvOptions/sources
We are not covering that now.
laplacianSchemes
{
default none;
laplacian(DT,T) Gauss linear corrected;
}
Boundary conditions:
Part of class volScalarField object T, read from file:
boundaryField{
patch1{ type zeroGradient;}
patch2{ type fixedValue; value uniform 273;}}