A General Approach For Running Python Co
A General Approach For Running Python Co
Abstract. As the overlap between traditional computational mechanics and machine learning grows, there is
an increasing demand for straight-forward approaches to interface Python-based procedures with C++-based
OpenFOAM. This article introduces one such general methodology, allowing the execution of Python code
directly within an OpenFOAM solver without the need for Python code translation. The proposed approach
is based on the lightweight library pybind11, where OpenFOAM data is transferred to an embedded Python
interpreter for manipulation, and results are returned as needed. Following a review of related approaches,
the article describes the approach, with a particular focus on data transfer between Python and OpenFOAM,
executing Python scripts and functions, and practical details about the implementation in OpenFOAM. Three
complementary test cases are presented to highlight the functionality and demonstrate the effect of different
data transfer approaches: a Python-based velocity profile boundary condition; a Python-based solver for
prototyping; and a machine learning mechanical constitutive law class for solids4foam which performs field
calculations.
1. Introduction
Appearing in 1985, C++ was motivated by the limitations of the structured programming paradigm:
code reusability, code sharing, and code extensibility [1]. Built on code abstraction and pioneering the
use of object-oriented programming (OOP), C++ quickly became the number one choice for the creation
of large, complex, and extendable software. In fact, before C++, OOP was mostly unknown in industry
as its techniques were believed to be excessively expensive for real-world applications and too complex for
non-expert programmers [2]. OpenFOAM was one of the first large computational mechanics softwares to
embrace C++ and OOP, tracing its origin to its predecessor ‘FOAM’ with its roots in the late 80’s. Using
OOP techniques, the authors of FOAM represented computational entities using mathematical language,
Over the subsequent three decades, there has been an explosion in the availability of computational re-
sources, in particular via parallel computing, as well as C++ software to exploit it. However, as acknowledged
1
2 SIMON RODRIGUEZ1,∗ AND PHILIP CARDIFF1
by Bjarne Stroustrup when creating C++, code elegance was not its primary goal: “Even I knew how to
design a prettier language than C++” [2]. The aim for C++ was utility, not abstract beauty. In contrast,
Python, publicly released in 1991, aimed to achieve a higher level of abstraction which emphasised readability
and usability, while achieving it with simpler syntax and fewer rules. As a result, when compared to lower-
level languages such as C++, Python’s learning curve is less steep and requires considerably less effort for
prototyping code and performing auxiliary tasks such as handling files; however, these benefits are typically
Over time, Python has become one of the most widely used programming languages, with the ‘Popularity
of Programming Languages Index’ [5] placing it as the most popular programming language in the world.
This momentum has resonated with the OpenFOAM community, which has tried to merge the advantages
• PyFoam: A widely used library for manipulating OpenFOAM cases and controlling OpenFOAM runs
[6];
• PythonFlu: a Python-based OpenFOAM wrapper which aims to make the calculation environment
more interactive and automated [7]; however, in recent years this project has become inactive;
• Owls: Python tools for data analysis and plotting of OpenFOAM cases [9].
In general, these projects have used Python to improve the user experience or develop pre/post processing
OpenFOAM utilities. On the other hand, swak4Foam [10], which is a collection of parsers for expressions
on OpenFOAM-types, was designed with the goal of minimising the use of C++ in OpenFOAM. With this
premise, one of its functionalities offers a function object that can execute Python code using an integrated
Python interpreter. In addition, Maulik et al. [11] and Weiner et al. [12, 13] have aimed to provide capabilities
to use data analysis tools from Python within OpenFOAM; the former constructed OpenFOAM applications
that have bindings to data libraries in Python and the latter transfers data and neural network models from
Python to OpenFOAM via the PyTorch library. More recently, Anderluh and Jasak [14] have proposed to
Inspired by swak4Foam, the current article proposes a general approach for integrating an embedded
Python interpreter in OpenFOAM via the pybind11 library [15], with a particular emphasis on its use with
(1) The ability to rapidly prototype numerical algorithms using Python, while taking advantage of the
OpenFOAM framework;
RUNNING PYTHON CODES IN OPENFOAM USING PYBIND11 3
(2) The capacity to exploit the vast ecosystem of scientific programming tools that have been developed
for Python, particularly those in the area of machine learning and data science.
Related to the second point, the recent machine learning revolution has lead to the nascent area of hy-
learning-acceleration of solvers [16, 17]; machine learning-constitutive laws in solid mechanics [18, 19, 20];
machine learning-turbulence models in fluid dynamics [21]; and targeting both acceleration and accuracy
gains [22]. In these approaches, the first challenge involves the development of a machine learning procedure
- typically based on a neural network - capable of solving the required task; this is commonly done with
Python, or similar languages such as R, Matlab or Julia. The next immediate challenge consists of cou-
pling the machine learning model with a computational mechanics solver, whether OpenFOAM or another;
currently, there is no standard way of doing this in OpenFOAM. Possible approaches include:
(1) Using the TensorFlow C++ API where OpenFOAM is linked at compile time to TensorFlow [23];
(2) Using the Python C API combined with the NumPy package C API [11];
(3) Translating the Python-based neural network to C++ code via specialised conversion tools, such as
frugally-deep [24].
These solutions all require the Python code and/or models to be translated or converted to allow their
use with C++ in OpenFOAM. The primary conceptual disadvantage with this is that it conflicts with the
main motivation of many programmers who want to use Python: coding simplicity; for example, this is
particularly evident when attempting to compile and link the TensorFlow C++ API with OpenFOAM,
where both must be built from source with consistent settings. These challenges can limit the adoption of
Python as a development tool for applications beyond traditional machine learning for which Python might be
well-suited; individual components of OpenFOAM solvers that are particularly suitable are field calculations,
Motivated by this, the current article presents a general approach to execute Python code from within
OpenFOAM without the need to convert the Python code to C++. This approach uses an embedded Python
interpreter based on the lightweight library pybind11 [15]. The approach, which is similar to the approaches
adopted in swak4Foam [10] and by Maulik et al. [11], enables the use of Python classes/functions/libraries
The remainder of this article is structured as follows: Section 2 presents an introduction to the required
pybind11 support including a general description on pybind11 and how to include it in OpenFOAM; this is
followed by a description of the interaction between OpenFOAM and Python. In Section 3, three different test
cases are used to illustrate how the proposed approach can be used in OpenFOAM. The first case demonstrates
4 SIMON RODRIGUEZ1,∗ AND PHILIP CARDIFF1
how to implement a general velocity profile boundary condition via Python. The second case shows how the
Python interpreter can be used to quickly test “proof of concept” Python numerical implementations via
an OpenFOAM solver. The final case gives an example of how to perform field calculations via analytical
calculations in Python as well as via TensorFlow/Keras machine learning models in Python; in this case,
an elastic mechanical constitutive law for a solid mechanics simulation is chosen for demonstration, but the
approach is equally applicable to any other field calculation. Section 4 summarises the main findings and
This section gives a brief description of pybind11, followed by steps on how to use it with OpenFOAM.
After explaining the high level interaction between OpenFOAM and Python via pybind11, the core technical
2.1. The pybind11 library. pybind11 is a lightweight header-only library that is primarily used to allow
C++ code to be called from Python [15]; however, as used in this article, pybind11 also offers the possibility
Installing pybind11 with Conda Conda is an open source package and environment management
system, which can be used for managing Python environments. Using Conda, pybind11 can be installed in
Including pybind11 in OpenFOAM Once pybind11 is installed on the system, it can be used in an
• Declare two variables, PYTHON INC and PYTHON LIB DIR, at the top of the options file to store the
location of the required pybind11 header files and the Python libraries:
✞
1 PYTHON INC DIR := $(shell python3 −m pybind11 −−includes)
where the shell commands here use the python3 command to lookup the location of the include
• Include PYTHON INC DIR in the EXE INC field, for example:
✞
1 EXE INC = \
• Include PYTHON LIB DIR and the Python C dynamic library in the EXE LIBS field (or in the LIB LIBS
3 −lpython3.8
✝ ✆
where in this case, Python version 3.8 is used, but the programmer should change this to whichever version
Including the required header files The header files that will be required in the OpenFOAM/Python
communication depend on the specific interaction scheme used by the programmer; however, in general the
2 #include <pybind11/eval.h>
3 #include <pybind11/stl.h>
4 #include <pybind11/numpy.h>
✝ ✆
• embed.h: Enables the functionality required to embed the Python interpreter in a C++ program;
• eval.h: Allows Python code to be executed/evaluated from C++ in the embedded Python inter-
preter;
• stl.h: Tools for converting data from C++ standard template library containers, such as std::vector,
• numpy.h: NumPy is the workhorse of scientific computation in Python, and although it is not strictly
required, it is often convenient and efficient to perform Python computations via NumPy. This
header allows to use the py::array data type, which is a C++ wrapper for NumPy arrays. When
a py::array is sent to the Python interpreter, the wrapper is removed and the data is exposed as a
NumPy array.
6 SIMON RODRIGUEZ1,∗ AND PHILIP CARDIFF1
Assuming pybind11 was installed correctly, these header files should all be available within the PYTHON-
Note on setRootCase.H OpenFOAM applications typically include a header file named setRootCase.H
which is used to read and check command-line arguments; however, the default arguments which are used
when constructing the Foam::argList object within setRootCase.H can cause issues with certain Python
modules, such as TensorFlow, as noted in online forums [25]. This can be solved by modifying the default
3 if (!args.checkRootCase())
4 {
5 Foam::FatalError.exit();
6 }
✝ ✆
2.2. General workflow for using pybind11 with OpenFOAM. The central idea revolves around using
pybind11 to create a Python interpreter that exists alongside the OpenFOAM application. Then, pybind11
allows data to be transferred to and from the Python interpreter, as well as the ability to execute Python
The entire process is conceptually depicted in Figure 1. OpenFOAM execution begins as usual, for example
with common tasks involving the creation and initialisation of fields and other auxiliary data structures
(Step 1). At a convenient point, a Python interpreter is created and held in the background (Step 2). The
OpenFOAM execution continues and, when required, it sends data to the Python interpreter (Step 3). An
OpenFOAM command is then called to execute a piece of Python code (Step 4) which invokes the Python
interpreter (Step 5); while the Python interpreter is executing, the OpenFOAM code waits. Finally, the data
is sent back to OpenFOAM, where it can be used in the rest of OpenFOAM workflow (Step 6). Details of
the different steps shown in Figure 1 are given in the subsequent sub-sections.
2.3. Initialising the Python interpreter. To improve readability, it is assumed that all subsequent Open-
To initialise, embed and expose the Python interpreter within OpenFOAM, the following line is used:
✞
1 py::initialize interpreter();
✝ ✆
RUNNING PYTHON CODES IN OPENFOAM USING PYBIND11 7
Figure 1. Overview of the interaction between OpenFOAM and Python, via pybind11.
This can be considered a higher level version of the Py Initialise() method from the Python C API and its
location in the code must ensure that it is run only once. After the interpreter is created, it can be interacted
with at any time. The line can be placed anywhere convenient within the OpenFOAM code, for example, in
the createFields.H header file of a solver, in the constructor of a class, or whenever it is needed.
2.4. Transferring data between OpenFOAM and Python. To facilitate data transfer between Open-
FOAM and Python, a Python scope object must be created and stored in OpenFOAM:
✞
1 py::object scope = py::module ::import(" __main__ ").attr(" __dict__ ");
✝ ✆
A dissection this line requires analysing both main and dict in a Python context. According to
the official Python documentation [26], main is the name of the environment where top-level code is run.
It is top-level because it imports all other modules that the program needs. dict , on the other hand,
is a dictionary or other mapping object used to store an object’s (writable) attributes [27]. Therefore, this
line imports the dictionary that contains all the variables defined in the Python interpreter and assigns it to
8 SIMON RODRIGUEZ1,∗ AND PHILIP CARDIFF1
the object scope on the C++ side; this C++ scope object can be used to declare variables in the Python
interpreter from C++ or to retrieve variables from Python to the C++ side.
Passing by copy Using this scope object, it is possible to copy the variables from OpenFOAM to Python;
for example, the OpenFOAM scalar variable a can be copied to a new variable in the Python scope, x, as
follows:
✞
1 const scalar a = 2;
2 scope["x"] = a;
✝ ✆
Similarly, the scope object can be used to copy variables from the Python scope to OpenFOAM:
✞
1 const scalar b = scope["y"];
✝ ✆
where it is assumed here that a scalar variable “y” exists in the Python scope before this line is executed.
The examples above show how primitive data types (bool, integer, float, double, etc) can be transferred
between OpenFOAM and Python; this approach can be easily applied to OpenFOAM data containers, such
as List and Field, by passing each element of these containers one-by-one to the Python interpreter to be
processed. The following example code demonstrates how this approach could be used to perform cell-by-cell
3 forAll(p, cellI)
4 {
6 scope["p"] = pI[cellI];
9 // ...
10
12 // For example:
14 }
✝ ✆
Although the approach above works, it may be prohibitively slow for larger fields; the reason for this
is that compiler optimisations on the OpenFOAM side are inhibited, and, similarly, efficient NumPy array
RUNNING PYTHON CODES IN OPENFOAM USING PYBIND11 9
operators are not utilised on the Python side. An alternative approach is to pass the entire field to the
Python interpreter, process the entire field in Python, and transfer the result field back to OpenFOAM. To
As stated in the pybind11 documentation, “all major Python types are available as thin C++ wrapper
classes” [28], including object, bool, int, float, str, bytes, tuple, list and others. In general, to
convert C++ types to Python, the method py::cast() is used [15]. For example, if a std::vector<double>
named inputC has been declared and initialised in OpenFOAM/C++, it is possible to create a C++ object
inputPy of type py::array which can be directly transferred to the Python interpreter as a NumPy array.
where the std::vector is copied to a C++ py::array which can be directly transferred to the Python
scope. The same conversions are allowed when the arrays are multi-dimensional, for example, 2-D ar-
rays; the only difference in that case is that the x variable must be declared as a vector of vectors, i.e.
In the examples above, std::vector data containers are used on the C++ side; however, as Field and
List containers are the standard in OpenFOAM, we explain here how to convert between these container
types. Essentially, the std::vector containers are used as a intermediate step when transferring field data
The implication of passing OpenFOAM field data on an element-by-element basis or as an entire field is
Passing by reference Using the approach described above, a copy of the data is created when it is
transferred between C++ and Python; that is to say, when a C++ variable is transferred to the Python
interpreter scope, the C++ data type is copied to a Python data type. As an alternative, it is possible to
exchange data between C++/OpenFOAM and Python by reference rather than as a copy. The approach
is based on the built-in NumPy support for ctypes, which “provides C compatible data types” in Python
10 SIMON RODRIGUEZ1,∗ AND PHILIP CARDIFF1
Figure 3. Reverse (backward) conversion. Passing an NumPy array from the Python
interpreter implies transferring it to a py::array in the C++ side, which is later cast to a
std::vector via pybind11 and finally converted back to OpenFOAM field
[29]. The idea here is to introduce minor changes in the original Python code to allow Python to interact
directly with the underlying data in the OpenFOAM C++ code, thus removing the need for data copying
steps. Every OpenFOAM field stores its data in the form of a C array, and a pointer to the first element of
this array can be retrieved using the data (or cdata) functions provided by the underlying UList base class.
Using pybind11, this pointer address is converted to an unsigned integer and sent to the Python interpreter
along with the size/length of the array. From the Python side, this integer address and size are used to create
a NumPy array, which provides a reference to the C array data, without copying data. This can be achieved
in Python as:
✞
1 strain pointer = ctypes.cast(strain address, ctypes.POINTER(ctypes.c double))
where strain address is the integer address from C++/OpenFOAM and SIZE is the size/length of the array.
Since the arrays created in the Python side are directly manipulating the underlying data of the OpenFOAM
fields, all the modifications done in the Python side are immediately propagated to the OpenFOAM side,
eliminating the need for copying the data. Clearly, care must be taken when using this approach as memory
errors may occur if the address changes on the C++ side without informing Python.
2.5. Executing Python code from OpenFOAM. The command py::eval file(string, scope) where
string is the name of a Python file/script, loads all the entities defined in the Python file, which can then
be consumed using the pybind11 py::exec command. For example, assume the following trivial Python
2 return 2.0*number
✝ ✆
RUNNING PYTHON CODES IN OPENFOAM USING PYBIND11 11
This function receives a number as an argument and returns the number × 2. The following C++ code
4 scope["x"] = a;
7 // Transfer the value of the Python variable ‘x’ to the OpenFOAM scalar ‘b’
• Declares a variable x in the Python interpreter and assigns it the value of OpenFOAM variable a;
• Calls the Python function double value with the Python variable x as an argument, and assigns the
Table 1 shows the variables that in the OpenFOAM/C++ and Python scopes, and how their values change
Table 1. Evolution of the variables values as each line in the C++ code is executed.
3. Test Cases
This section presents three complementary test cases that demonstrate the methodology proposed in
Section 2:
(1) Python velocity profile boundary condition: An OpenFOAM wrapper boundary condition is
presented, which uses a Python script - supplied at run-time - to define a temporally-spatially varying
velocity profile;
(2) Python heat transfer prototyping solver: A wrapper OpenFOAM heat transfer solver is pre-
sented, which invokes a run-time Python script to solve the governing equations at each time step; this
12 SIMON RODRIGUEZ1,∗ AND PHILIP CARDIFF1
case shows how new solvers could be more quickly prototyped using a combination of OpenFOAM
and Python;
(3) Field calculations using Python A wrapper solids4foam [30] mechanical constitutive law is pre-
sented to demonstrate how a volSymmTensorField stress field can be calculated using Python as
a function of a volSymmTensorField strain field. Analytical expressions are compared with Ten-
sorFlow Keras [31] neural networks, with an emphasis placed on efficiency. It is expected that the
These cases and the code to run them are publicly available at https://bitbucket.org/ScimonUCD/pybind11foam/
src/master/.
3.1. Python velocity profile boundary condition. This case demonstrates how the general methodology
presented in Section 2 can be used to create a Python-based boundary condition in OpenFOAM. In this case,
a velocity profile boundary condition is created, where a Python script calculates the patch velocities as a
function of spatial coordinates and time. The boundary condition, named pythonVelocity, is a Dirichlet
condition and hence derives from the fixedValueFvPatchField boundary condition class. The central
concepts of the pythonVelocity boundary condition - related to the private data and updateCoeffs function
Boundary condition private data Two private data objects are stored in the boundary condition class:
• The name of the Python script (fileName pythonScript ) which is read at run-time;
• The Python interpreter scope object (py::object scope ) used for transferring data from Open-
FOAM to Python, evaluating Python functions, and transferring data back from Python to Open-
FOAM.
Boundary condition updateCoeffs function For each finite volume boundary condition in OpenFOAM
(those derived from fvPatchField), the updateCoeffs function is called as a preliminary step to the linear
solver call. Consequently, the updateCoeffs function is an appropriate location to update patch velocities
as a function of spatial position and time. As per Section 2, there are four essential operations that need to
• Within the boundary condition constructor, initialise the Python interpreter and load the Python
file;
• Within updateCoeffs, transfer the list of face-centre coordinates and time value to Python;
• Within updateCoeffs, call the Python script to calculate the list of new face velocities;
• Within updateCoeffs, transfer the list of new face velocities back to OpenFOAM.
In the boundary condition constructor, the Python interpreter is initialised and the Python script is read:
RUNNING PYTHON CODES IN OPENFOAM USING PYBIND11 13
✞
1 // Initialise the Python interpreter
2 py::initialize interpreter();
Next, in the updateCoeffs function, the patch face-centre coordinate vectors and current time are trans-
3 std::vector<std::vector<scalar>> inputC(C.size());
4 forAll(C, faceI)
5 {
6 inputC[faceI] = std::vector<scalar>(3);
8 {
9 inputC[faceI][compI] = C[faceI][compI];
10 }
11 }
12
15
16 // Transfer the C++ NumPy array to a NumPy array in the Python scope
The run-time Python script is then called to calculate the new velocities as a function of the face-centre
The output velocities list exists in the Python scope. Note that the only requirement of the run-time
loadable Python script is that it contains the definition of a function called calculate which accepts two
14 SIMON RODRIGUEZ1,∗ AND PHILIP CARDIFF1
arguments, one a NumPy array of vectors and the other a double, and returns a NumPy array of vectors. A
suitable example Python function, included in the accompanying setInletVelocity.py file, is:
✞
1 import numpy as np
4 # Initialise result
7 x = face centres[:, 0]
8 result[:, 0] = np.sin(np.pi*time)*np.sin(40*np.pi*x)
10 return result
✝ ✆
where the expression in this case varies the X component of velocity as a function of the X component of the
The calculated velocities are then retrieved from the Python scope and converted into an OpenFOAM
field:
✞
1 // Convert the Python velocities to a C++ NumPy array
6 outputPy.cast<std::vector<std::vector<scalar>>>();
9 vectorField velocities(patch().size());
10 forAll(velocities, faceI)
11 {
13 {
14 velocities[faceI][compI] = outputC[faceI][compI];
15 }
RUNNING PYTHON CODES IN OPENFOAM USING PYBIND11 15
16 }
✝ ✆
2 fixedValueFvPatchVectorField::updateCoeffs();
✝ ✆
Verification of the proposed boundary condition The proposed pythonVelocity boundary condition
is verified on the classic cavity tutorial case [32], where it is used to set the velocity on the upper moving
wall patch. The only modifications made to the tutorial were to change the velocity boundary condition for
2 {
3 type pythonVelocity;
6 }
✝ ✆
Running the case produces the velocity profile on the movingWall patch at 0.5 s as shown in Figure 4,
where the expected analytical expression is given for comparison. The corresponding velocity streamlines
3.2. Python heat transfer prototyping solver. This case demonstrates one of Python’s main advantages
over C++: fast prototyping. To do this, as an example, the classic laplacianFoam solver is modified such
that a run-time selectable Python script is passed mesh, material and time information and is expected to
calculate the temperature field. In this case, a simple explicit finite difference method is implemented in
Python, however, the approach could be modified to accommodate the prototyping any particular solution
As in the first test case, there are three main components added to the OpenFOAM code: data transfer
from OpenFOAM to Python; execution of the Python code; transfer of the result data back from Python to
OpenFOAM.
Data transfer from OpenFOAM to Python Following the approach in Section 2, OpenFOAM data
is mapped, where relevant, to a corresponding Python NumpPy array data. In this case, two pieces of data
Figure 5. Velocity streamlines in m s−1 (left) and pressure field in Pa m3 kg−1 (right) for
the cavity case, where the velocity on the upper wall is prescribed via a Python script.
• A parameter gamma, which encompasses diffusivity, mesh spacing and pseudo-time-step data:
✞
1 scope["gamma"] =
2 scalar
3 (
RUNNING PYTHON CODES IN OPENFOAM USING PYBIND11 17
4 DT*runTime.deltaT()*Foam::pow(max(mesh.deltaCoeffs()), 2)
5 ).value();
✝ ✆
• The temperature field, including boundary condition values, which are assumed here to be fixedValue-
✞
1 // Set boundary cell values to be equal to the boundary values as we
4 // difference code
5 scalarField& TI = T.primitiveFieldRef();
6 forAll(T.boundaryFieldRef(), patchI)
7 {
9 mesh.boundary()[patchI].faceCells();
10 forAll(faceCells, faceI)
11 {
13 TI[cellID] = T.boundaryFieldRef()[patchI][faceI];
14 }
15 }
16
18 std::vector<scalar> inputC(TI.size());
19 forAll(TI, cellI)
20 {
21 inputC[cellI] = TI[cellI];
22 }
23
26
28 scope["T"] = inputPy;
✝ ✆
18 SIMON RODRIGUEZ1,∗ AND PHILIP CARDIFF1
Perform Python calculations As in the first test case, the Python script is selected at run-time,
however, in this case it is expected that the provided Python script contains a calculate function, which
takes two arguments - the temperature field T with boundary conditions, and the gamma variable - and returns
the new temperature field. The function is called in the OpenFOAM solver as:
✞
1 py::exec("T = calculate (T, gamma )\n", scope);
✝ ✆
The Python function in this case employs a simple steady-state explicit finite difference approach to solve
4 N = T.shape[0]
5 Nx = np.sqrt(N).astype(int)
6 Ny = Nx
11 T[i*Ny + j] = \
12 gamma*(T[i*Ny + j + 1] + T[i*Ny + j − 1] \
15
16 return T
✝ ✆
Data transfer from Python to OpenFOAM The temperature field results are then extracted from
6 outputPy.cast<std::vector<scalar>>();
7
RUNNING PYTHON CODES IN OPENFOAM USING PYBIND11 19
9 forAll(TI, cellI)
10 {
11 TI[cellI] = outputC[cellI];
12 }
✝ ✆
Results The proposed prototyping solver is demonstrated on a steady-state heat conduction problem with
a 2-D square geometry (0.1 × 0.1 m) and 20 × 20 cells - the geometry and mesh from the cavity tutorial.
The diffusivity is 4 × 10−5 m2 s−1 . The left, bottom, and right boundaries are set to 273 K, while the top
where calculateT.py is the Python explicit finite difference code shown above.
The final converged temperature field is shown in Figure 6(a). For comparison, the temperature along a
vertical line from the centre of the bottom patch to the centre of the top patch are compared with the results
as calculated by laplacianFoam (Figure 6(b)). As expected the predictions are similar, with the differences
primarily attributed to the Python finite difference code enforcing boundary conditions in the cell-centres of
3.3. Field calculations using Python. The purpose of this final test case is to demonstrate how to perform
field calculations using the embedded Python interpreter; specifically, this case shows how this can be done
using machine learning models implemented in TensorFlow/Keras, scikit-learn, and Python in general. A
20 SIMON RODRIGUEZ1,∗ AND PHILIP CARDIFF1
solid mechanics problem is chosen, where the stress tensor in each cell is calculated as a function of the
displacement gradient via a run-time selectable constitutive mechanical law. This approach could be readily
applied to similar field calculations, such as the calculation of turbulent quantities, thermo-physical property
fields, or even for discrete differential operators. The case (Figure 7) consists of a plate, with a circular hole
in the centre, loaded by uniform tension tx = 1 MPa on the right boundary, while symmetry conditions
are applied to the left and bottom boundaries. Two quadrilateral meshes were considered: a coarse mesh
with 1 000 cells (Figure 7), and a finer mesh with 400 000 cells. All the simulations were solved for 1 time
step, small strains were assumed, and the inertia and gravity terms were neglected. A Hookean linear elastic
material is assumed with a Young’s modulus of E = 200 GPa and a Poisson’s ratio of ν = 0.3.
Figure 7. Geometry of the spatial computational domain for the flat plate with a circular
hole (a = 0.5 m, b = 2 m, E = 200 GPa, ν = 0.3, tx = 1MPa).
At each outer iteration within a time-step, the stress tensor σ in each cell is calculated as a function of
where the strain tensor ǫ is the symmetric component of the displacement gradient ∇d:
1h T
i
ǫ= ∇d + (∇d) (3)
2
For demonstration purposes, the calculation of the stress tensor in each cell (Equation 2) is performed
(1) solids4foam reference case: this simulation uses the native solids4foam/OpenFOAM implemen-
(2) Analytical expression in Python (Analytical-Python): the strain tensor is passed to the Python
interpreter; the stress is then calculated in Python as per Equation 2 and returned to OpenFOAM;
(3) Neural network base case (NN-Base): as per approach 2, the strain is passed to Python, but
in this case the stress is called using an artificial neural network created in Keras, which has been
trained on data from Equation 2. The prediction method uses the built-in .predict Keras function.
Approaches 4 to 7 use the same trained neural network as in approach 3, however, alternative neural
(4) Neural network functional model (NN-Functional): the neural network is constructed using the
Keras Functional API, as opposed to the Sequential model used in approach 3 [31].
(5) Neural network with eager mode disabled (NN-Eager-Disabled): the so-called eager execution
(6) Neural network using NumPy (NN-NumPy): the Keras neural network is fully translated to
pure NumPy array calculations, breaking the dependency with Keras and TensorFlow.
(7) Neural network using NumPy-Numba [34] (NN-NumPy-Numba): Numba is an open source
just-in-time compiler that translates a subset of Python and NumPy code into fast machine code
Each of these seven approaches are implemented in their own python file.py script, loaded at run-time.
The hole-in-a-plate case is solved twice using each of the seven approaches above: in the first solution, the
strain tensor is passed to the Python interpreter on a cell-by-cell basis, whereas in the second solution, the
entire OpenFOAM strain field (all cells) is transferred in one go to the Python interpreter, as discussed in
Section 2.4.
Performance comparison The predicted von Mises stress distribution for the reference solids4foam
case is shown in Figure 8(a), while the predicted xx component of the stress tensor σ xx along the vertical
line x = 0 is shown for all seven approaches in Figure 8(b). At this scale, the prediction from all approaches
closely agree. Table 2 lists the L∞ norm and average L2 norm of the differences with the reference case for
the displacement magnitude and von Mises stress fields; the maximum errors in the von Mises stress (< 800
MPa) and displacement magnitude (< 3e-9 m) are small in comparison to the maximum von Mises stress (2.7
MPa) and maximum displacement magnitudes (13 µm) in the reference solution. In addition, the predictions
22 SIMON RODRIGUEZ1,∗ AND PHILIP CARDIFF1
are the same regardless of whether the data is transferred on a cell-by-cell basis or by the entire field, and as
xx along x = 0 m
3.0
2.5
(in MPa)
2.0
xx
1.5
1.0
Table 2. L∞ norm and mean L2 norm between the predictions from approaches 2-6 with
the reference case, where the maximum stress in the reference case is 2.7 MPa and the
maximum displacement magnitude is 13 µm.
As expected the predictions are insensitive to the approach, however, there is a wide variation in the
timings between the implementations. Table 3 shows the execution times for all seven implementations on
the coarse (1 000 cells) and fine (400 000 cells) meshes, where the ‘Ratio’ is given for timing relative to the
reference case.
The first insight to be gained is that passing the data as entire fields is significantly faster than on a
cell-by-cell basis; for the analytical Python method (approach 2), the entire field approach is almost three
times faster than the cell-by-cell approach, for both coarse and fine meshes. The difference is even greater
RUNNING PYTHON CODES IN OPENFOAM USING PYBIND11 23
Table 3. Execution time for all the evaluated cases in s (unless stated otherwise), where
timings are rounded to the nearest second. The ‘Ratio’ gives the ratio of the time for the given
approach to the time for the reference case. *The cell-by-cell and entire field designations
do not apply to the reference solids4foam calculations.
for the neural network approaches, with the entire field approach being orders of magnitude faster than the
cell-by-cell approach.
Among the Python-based implementations, the analytical expression (approach 2) is the most efficient,
introducing an overhead as small as 8% when the data is passed as an entire field. This matches the expected
behaviour, considering that the analytical expression involves less floating point operations than any of the
examined neural networks. Comparing the neural network approaches, the neural network base case, the
functional model, and the disabled eager methods (approaches 3-5) all perform similarly on the fine mesh,
adding approximately an 18% overhead relative to the reference solids4foam case. Similar trends are seen
for the coarse mesh, although the overhead is significantly greater (>600%); from this, we can conclude that
there is a fixed overhead associated with loading a neural network (and even just the Python interpreter),
Examining the final two approaches, which are the non-Keras-based neural networks (NumPy and NumPy-
Numba-based), they show a minor advantage for the coarse mesh but actually slow down the solution for the
finer mesh.
Passing fields by reference As a final attempt to reduce the overhead of using the Python interpreter,
we will examine the effect of transferring data by reference (i.e. by address) rather than by copying the data,
as described in Section 2.4. Table 4 compares the results for the pass-by-copy approach presented in the
previous section and the pass-by-reference approach, where results are only shown for the fine mesh using
the entire field approach. The pass-by-reference approach can be seen to produce a significant speedup in
the timings. The analytical Python method (approach 2) exhibits essentially the same execution time as the
reference solids4foam case. Similarly, the base neural network approach only shows an overhead of less than
4%, while the slowest Keras-based neural network (functional API method) only increases the execution time
24 SIMON RODRIGUEZ1,∗ AND PHILIP CARDIFF1
by 7.3%. As in the pass-by-copy approach, the NumPy and NumPy-Numba neural networks methods show
Table 4. Timing results for the pass-by-reference approaches. *The cell-by-cell and entire
field designations do not apply to the native solids4foam calculations.
4. Conclusion
This article introduces a general approach for running Python code in OpenFOAM. This is achieved with
the pybind11 header library, which is used to create an instance of the Python interpreter and for exchanging
data between OpenFOAM and Python. The three examples demonstrate the feasibility and efficiency of
the presented approach, where the use of machine learning models is demonstrated in the final case. When
dealing with the calculation of fields, passing data cell-by-cell is compared with passing data via the entire
field. In addition, when passing the entire field, passing-by-copy is compared with pass-by-reference. The
• The most computationally inexpensive approach is to pass data between OpenFOAM and Python
as entire fields by reference, with an overhead of less than a few percent for the best performing
feed-forward neural networks with 266 weights, on a mesh with 400 000 cells;
• Passing data between OpenFOAM and Python by copy on a cell-by-cell basis can be prohibitively
expensive for larger fields, potentially increasing the run-time by multiple orders of magnitude;
• The use of pure NumPy neural networks or Numba-enhanced NumPy neural networks show minimal
benefits over the native TensorFlow/Keras Python implementations, and are in fact slower in most
cases.
Although not been demonstrated in article, the presented approach runs in parallel via the standard Open-
FOAM MPI approach without any changes; in that case, each processor has its own local copy of the Python
interpreter. It is hoped that the methods presented in this article will expand the universe of Python-based
RUNNING PYTHON CODES IN OPENFOAM USING PYBIND11 25
solutions applicable to OpenFOAM, especially those coming from the rapidly evolving machine learning field.
Future work will explore more complex problems related to elasto-plasticity, requiring machine learning mod-
5. Acknowledgements
Financial support is gratefully acknowledged from the Irish Research Council through the Laureate pro-
Additionally, the authors want to acknowledge project affiliates, Bekaert, through the Bekaert University
Technology Centre (UTC) at UCD (www.ucd.ie/bekaert), and I-Form, funded by Science Foundation Ireland
(SFI) Grant Number 16/RC/3872, co-funded under European Regional Development Fund and by I-Form
industry partners.
The authors wish to acknowledge the DJEI/DES/SFI/HEA Irish Centre for High-End Computing (ICHEC)
Appendix A. Python scripts for the linear Hookean laws implemented in section 3.3
The run-time loaded python file.py for each of the six Python methods (approaches 2 to 7) are given
15 return stress
✝ ✆
where lame 1 and lame 2 are the Lamé’s constant and shear modulus, respectively.
Neural network base case (NN-Base) In this case, the mapping between stress and strain from
equation (2) is provided by a neural network previously trained on Keras. This neural network has two
hidden layers with 20 and 6 feed-forward nodes, respectively. The activation function of the first hidden layer
is ReLu and the other is linear. The entire stress calculation process for a material point is summarised in
figure 9. At the training phase, both the strain and stress tensors in the training set are scaled into the range
[0, 1] using a linear scaler from the Scikit-Learn [36] Python library and fed to the neural network which
calculates the corresponding scaled stress tensor which is scaled back to its original range.
Therefore, in order to use the resulting neural network, the same scalers need to be available in Python. The
strategy used here was to serialise them once the training phase was completed, place them in the OpenFOAM
working directory and load (or deserialise) them in the embedded Python interpreter in OpenFOAM through
the python file.py. This is done using the Joblib Python library as shown in the next code:
✞
1 from joblib import load
RUNNING PYTHON CODES IN OPENFOAM USING PYBIND11 27
Figure 9. Linear elastic Hookean law based on a neural network. The components of the
strain tensor (ǫ) are scaled to the range [0, 1] (ǭ) and fed to a neural network. It calculates
the corresponding scaled stress tensor (σ̄) which is scaled back to its natural range (σ).
where x scaler.joblib and y scaler.joblib are the strain and stress scalers, respectively.
As in the previous case, the strain-stress mapping is provided by a predict function in the Python code,
given by:
✞
1 def predict(x new):
7 return prediction
✝ ✆
In this case, the built-in Keras .predict method is used to perform the inferences from the neural network.
Neural network functional model (NN-Functional) When the predictions are performed on a cell-
by-cell basis, the .predict method is located within a for loop that iterates over the cells, which is discouraged
[37]. For this reason, the previous case was revisited to modify the predict function. Instead of using Keras
.predict, another neural network is built with the same layers of the previous model using the Functional
API from Keras and the inferences are performed by a direct call to the model, as suggested by [37] and
7 return prediction
✝ ✆
Neural network with eager mode disabled (NN-Eager-Disabled) TensorFlow’s eager execution
is an imperative programming environment that evaluates operations immediately, without building graphs:
operations return concrete values instead of constructing a computational graph to run later [33]. The
Then, another neural network is created and loaded with the parameters used in the previous networks
7 model.load weights("DNN.h5")
✝ ✆
where DNN.h5 is the file of the neural network used in the base case. The prediction step to calculate the
Neural network using NumPy (NN-NumPy) The same neural network presented in the previous
cases was converted to NumPy code using the Python library keras-konverter [38]. First, the parameters of
2 w, b = wb[’wb’]
And the predictions from the neural network are given by:
RUNNING PYTHON CODES IN OPENFOAM USING PYBIND11 29
✞
1 def neural prediction(x, w0, w1, b0, b1):
2 l0 = x.dot(w0) + b0
3 l0 = np.maximum(0, l0)
4 l1 = l0.dot(w1) + b1
5 return l1
✝ ✆
2 x = x scaler.transform(x)
6 return prediction
✝ ✆
Neural network using NumPy-Numba (NN-NumPy-Numba) Finally, this case aimed to speed
execution up by using Numba on the previous NumPy based implementation. The main difference with
the previous case is that Numba is imported and a @jit(nopython = True) decorator is added to the
4 @jit(nopython = True)
6 l0 = x.dot(w0) + b0
7 l0 = np.maximum(0, l0)
8 l1 = l0.dot(w1) + b1
9 return l1
✝ ✆
References
[1] A. Sharma, Object-oriented Programming with C++. Pearson Education India, 2014.
[2] B. Stroustrup, Programming: principles and practice using C++. Pearson Education, 2014.
[3] H. G. Weller, G. Tabor, H. Jasak, and C. Fureby, “A tensorial approach to computational continuum mechanics using
object orientated techniques,” Computers in Physics, vol. 12, pp. 620–631, 1998.
30 SIMON RODRIGUEZ1,∗ AND PHILIP CARDIFF1
[4] W. McKinney, Python for data analysis: data wrangling with pandas, NumPy, and IPython, 2nd ed. Sebastopol, California:
[11] R. Maulik, D. Fytanidis, B. Lusch, V. Vishwanath, and S. Patel, “Pythonfoam: In-situ data analyses with openfoam and
[12] A. Weiner, D. Hillenbrand, H. Marschall, and D. Bothe, “Data-Driven Subgrid-Scale Modeling for Convection-Dominated
Concentration Boundary Layers,” Chemical Engineering & Technology, vol. 42, no. 7, pp. 1349–1356, Jul. 2019. [Online].
Available: https://onlinelibrary.wiley.com/doi/10.1002/ceat.201900044
[13] A. Weiner, “Running PyTorch models in OpenFOAM – basic setup and examples.” [Online]. Available:
https://ml-cfd.com/2020/12/29/running-pytorch-models-in-openfoam-basic-setup-and-examples/
[14] Anderluh and Jasak, “Like OpenFOAM, but Python,” in 16th OpenFOAM Workshop, University College Dublin (Online),
[15] W. Jakob, J. Rhinelander, and D. Moldovan, “pybind11 – seamless operability between c++11 and python,” 2017,
https://github.com/pybind/pybind11.
[16] D. Kochkov, J. A. Smith, A. Alieva, Q. Wang, M. P. Brenner, and S. Hoyer, “Machine learning–accelerated computational
fluid dynamics,” Proceedings of the National Academy of Sciences, vol. 118, no. 21, 2021.
[17] O. Obiols-Sales, A. Vishnu, N. Malaya, and A. Chandramowliswharan, “CFDNet: a deep learning-based accelerator for
fluid simulations,” in Proceedings of the 34th ACM International Conference on Supercomputing. Barcelona Spain: ACM,
[18] D. Pandya, B. Dennis, and R. Russell, “A computational fluid dynamics based artificial neural network
model to predict solid particle erosion,” Wear, vol. 378-379, pp. 198–210, May 2017. [Online]. Available:
https://linkinghub.elsevier.com/retrieve/pii/S0043164816303702
[19] D. W. Abueidda, S. Koric, N. A. Sobh, and H. Sehitoglu, “Deep learning for plasticity and thermo-
viscoplasticity,” International Journal of Plasticity, vol. 136, p. 102852, Jan. 2021. [Online]. Available:
https://linkinghub.elsevier.com/retrieve/pii/S0749641920302096
[20] C. Wang, L.-y. Xu, and J.-s. Fan, “A general deep learning framework for history-dependent response prediction based on
UA-Seq2Seq model,” Computer Methods in Applied Mechanics and Engineering, vol. 372, p. 113357, Dec. 2020. [Online].
Available: https://linkinghub.elsevier.com/retrieve/pii/S0045782520305429
[21] J. Ling, A. Kurzawski, and J. Templeton, “Reynolds averaged turbulence modelling using deep neural networks with
embedded invariance,” Journal of Fluid Mechanics, vol. 807, pp. 155–166, 2016.
[22] X. Morales, J. Mill, K. A. Juhl, A. Olivares, G. Jimenez-Perez, R. R. Paulsen, and O. Camara, “Deep Learning Surrogate
of Computational Fluid Dynamics for Thrombus Formation Risk in the Left Atrial Appendage,” in Statistical Atlases
and Computational Models of the Heart. Multi-Sequence CMR Segmentation, CRT-EPiggy and LV Full Quantification
Challenges, M. Pop, M. Sermesant, O. Camara, X. Zhuang, S. Li, A. Young, T. Mansi, and A. Suinesiaputra, Eds.
RUNNING PYTHON CODES IN OPENFOAM USING PYBIND11 31
Cham: Springer International Publishing, 2020, vol. 12009, pp. 157–166, series Title: Lecture Notes in Computer Science.
[23] R. Maulik, H. Sharma, S. Patel, B. Lusch, and E. Jennings, “Deploying deep learning in openfoam with tensorflow,” in
[25] “[QUESTION]. Crash when importing NumPy manually Issue #2485 - pybind/pybind11.” [Online]. Available:
https://github.com/pybind/pybind11/issues/2485
[26] “ main — Top-level code environment. Python 3.10.0 documentation.” [Online]. Available: https://docs.python.org/3/
pycpp/object.html
[29] “ctypes — A foreign function library for Python. Python 3.10.0 documentation.” [Online]. Available: https:
//docs.python.org/3/library/ctypes.html
[30] P. Cardiff, A. Karač, P. De Jaeger, H. Jasak, J. Nagy, A. Ivanković, and Ž. Tuković, “An open-source finite volume toolbox
for solid mechanics and fluid-solid interaction simulations,” arXiv preprint arXiv:1808.10736, 2018.
[32] “CFD.Direct.com. OpenFOAM v9 User Guide: 2.1 lid-driven cavity flow.” [Online]. Available: https://cfd.direct/
openfoam/user-guide/v9-cavity/#x5-40002.1
[34] S. K. Lam, A. Pitrou, and S. Seibert, “Numba: A llvm-based python jit compiler,” in Proceedings of the Second Workshop
[36] F. Pedregosa, G. Varoquaux, A. Gramfort, V. Michel, B. Thirion, O. Grisel, M. Blondel, P. Prettenhofer, R. Weiss,
V. Dubourg, J. Vanderplas, A. Passos, D. Cournapeau, M. Brucher, M. Perrot, and E. Duchesnay, “Scikit-learn: Machine
learning in Python,” Journal of Machine Learning Research, vol. 12, pp. 2825–2830, 2011.
[37] “[QUESTION]. model.predict is much slower on tf 2.1+ · issue #40261.” [Online]. Available: https://github.com/
tensorflow/tensorflow/issues/40261