Development of the simulator source code

Table of Contents

General information about simulators architecture

Simulator methods sequence and framework interactions

As previously mentioned, a simulator is a C++ class which defines mandatory methods (see Mandatory methods to be defined). These methods are called by the OpenFLUID framework at the right time during the simulation, following the interactions sequence in the figure below.

openfluid_sequence.png

Among these methods, the initializeRun() and runStep() methods have a special behaviour: these two methods must return the simulation duration after which the simulator will be executed again.

This duration can returned using the following instructions :

Example for a fixed time step simulator, with a time step equal to the default DeltaT value given in the input dataset:

{
return DefaultDeltaT();
}
{
return DefaultDeltaT();
}

Example for a variable time step simulator, based on the internal computation of the simulator:

{
// do something here
return DefaultDeltaT();
}
{
double TmpValue = 0.0;
// do something here with TmpValue
if (TmpValue < 1.0)
{
return DefaultDeltaT();
}
else
{
return Duration(10);
}
}

For fully synchronized coupled simulators, all simulators must return the same duration for the next execution, usually DefaultDeltaT() .

OpenFLUID data types

Simulation data exchanged through the OpenFLUID framework should be typed with an OpenFLUID defined type. The available simple types are:

The available compound types are:

A specific type is available for storing non-existing or null values:

Simulation data are stored using these types :

Each data type can be converted to and from openfluid::core::StringValue (as far as the string format is correct). String representations of values are described in the String representation of values part.

Simulation variables can be typed or untyped. This is set at the declaration of these variables (see Simulation variables). In case of typed variables, each value of the variable must be of the type of the variable. In case of untyped variables, values for the variable can be of any type.

Handling the spatial domain

Parsing the spatial graph

The spatial graph represents the spatial domain where coupled simulators will operate. Parsing this graph in different ways is a common task in simulators. This graph can be browsed using predefined instructions.

Sequential parsing

Spatial units can be parsed following the process order by using the following OpenFLUID instructions:

To parse a specific list of of spatial units, you can use the instruction:

The source code below shows examples of spatial graph parsing. The first part of the source code shows how to browse all units of the SU units class, and how to browse the "From" units for each SU unit. The second part of the source code shows how to browse all units of the spatial domain.

{
{
UpSUsList = SU->fromSpatialUnits("SU");
OPENFLUID_UNITSLIST_LOOP(UpSUsList,UpSU)
{
// do something here
OPENFLUID_GetVariable(UpSU,"varA",TmpValue);
}
}
{
// do something here
OPENFLUID_GetVariable(UU,"varB",TmpValue);
}
return DefaultDeltaT();
}

Parallel processing using multithreading

A processing defined as a method of a simulator class can be applied in parallel to the spatial graph, following the process order, using the following methods:

The first argument of the method passed to the instruction must be a pointer to an as it represents the currently processed spatial unit.


In order to enable the parallel processing in the spatial graph, the following inclusion must be added at the top of the simulator source code:

The code below shows how to apply a method in parallel over the spatial graph:

{
// compute something
// can use/produce variables
OPENFLUID_GetVariable(U,"varA",TmpValue);
}
void computeB(openfluid::core::SpatialUnit* U,
const double Coeff)
{
// compute something else, with extra args
// can use/produce variables
OPENFLUID_GetVariable(U,"varA",TmpValue);
OPENFLUID_AppendVariable(U,"varB",TmpValue*Coeff);
}
{
APPLY_UNITS_ORDERED_LOOP_THREADED("SU",SnippetsSimulator::computeA);
APPLY_UNITS_ORDERED_LOOP_THREADED("TU",SnippetsSimulator::computeB,2.5);
APPLY_ALLUNITS_ORDERED_LOOP_THREADED(SnippetsSimulator::computeA);
return DefaultDeltaT();
}
Note
  • If a spatial loop is used inside another spatial loop, it is recommended to use multithreading in only one loop.
  • In case of concurrent data access, it is strongly recommended to use mutex locks for thread-safe data handling.
  • Concurrent parsing using multithreading should improve computing performance, reducing simulations durations. But in case of very short computing durations, the cost of multithreading management may counterbalance the performance improvements of concurrent computing.

Querying the spatial graph

The spatial domain graph can be queried during simulations in order to get informations about spatial units and connections.

The following methods are available:

Modifying the spatial graph

The spatial graph is usually statically defined through the input dataset. It can also be defined and modified dynamically during simulations, using primitives to create and delete spatial units, and also to add and remove connections between these spatial units. Although the creation, deletion and modification of connections are allowed at any stage of the simulation, the creation, deletion and modification of spatial units are currently allowed only during the data preparation stage (i.e. in the prepareData() method of the simulator). For consistent use of simulators which modify the spatial domain graph, please fill the signature with the correct directives. See the Spatial units graph part of the signature declaration.

Creating and deleting spatial units

In order to create and delete units, you can use the following methods:

Adding and removing spatial connections

Connections between spatial units can be of two types:

In order to add and remove connections, you can use the following methods, whenever during simulations:

Example:

void prepareData()
{
/*
TU.1 TU.2
| |
--> TU.22 <--
|
--> TU.18
|
TU.52 --> OU.5 <-- OU.13
|
--> OU.25
VU1 <-> VU2
with:
TU1, TU2, TU22, TU18 are children of VU1
TU52, OU5, OU13, OU25 are children of VU2
*/
OPENFLUID_AddUnit("VU",1,1);
OPENFLUID_AddUnit("VU",2,2);
OPENFLUID_AddUnit("TU",1,1);
OPENFLUID_AddUnit("TU",2,1);
OPENFLUID_AddUnit("TU",22,2);
OPENFLUID_AddUnit("TU",18,3);
OPENFLUID_AddUnit("TU",52,1);
OPENFLUID_AddUnit("OU",5,4);
OPENFLUID_AddUnit("OU",13,1);
OPENFLUID_AddUnit("OU",25,5);
OPENFLUID_AddFromToConnection("VU",1,"VU",2);
OPENFLUID_AddFromToConnection("VU",2,"VU",1);
OPENFLUID_AddFromToConnection("TU",1,"TU",22);
OPENFLUID_AddFromToConnection("TU",2,"TU",22);
OPENFLUID_AddFromToConnection("TU",22,"TU",18);
OPENFLUID_AddFromToConnection("TU",18,"OU",5);
OPENFLUID_AddFromToConnection("TU",52,"OU",5);
OPENFLUID_AddFromToConnection("OU",13,"OU",5);
OPENFLUID_AddFromToConnection("OU",5,"OU",25);
OPENFLUID_AddChildParentConnection("TU",1,"VU",1);
OPENFLUID_AddChildParentConnection("TU",2,"VU",1);
OPENFLUID_AddChildParentConnection("TU",22,"VU",1);
OPENFLUID_AddChildParentConnection("TU",18,"VU",1);
OPENFLUID_AddChildParentConnection("TU",52,"VU",2);
OPENFLUID_AddChildParentConnection("OU",5,"VU",2);
OPENFLUID_AddChildParentConnection("OU",13,"VU",2);
OPENFLUID_AddChildParentConnection("OU",25,"VU",2);
}

Generating spatial domain graphs automatically

A spatial domain graph can be automatically built or extended using a provided method to create a matrix-like graph:

Informations about simulation time

Simulators can access to informations about simulation time. There are constant time informations, such as simulation duration or begin and end date, and evolutive informations such as current time index.

Constant time informations can be accessed from any part of the simulator (except from the constructor), using the following methods:

Evolutive time informations can be accessed only from specific parts of the simulator, using the following methods:

Example of code:

{
openfluid::core::Duration_t Duration = OPENFLUID_GetSimulationDuration();
openfluid::core::TimeIndex_t CurrentIndex = OPENFLUID_GetCurrentTimeIndex();
openfluid::core::DateTime CurrentDT = OPENFLUID_GetCurrentDate();
std::cout << Duration << std::endl;
std::cout << CurrentIndex << std::endl;
std::cout << CurrentDT.getAsISOString() << std::endl;
return DefaultDeltaT();
}

Simulator parameters

Simulators parameters can be accessed in the source code from the initParams method of the simulator. Values of simulators parameters can be retreived using:

The requested parameter name must be the same as the one declared in the signature and used in the model.fluidx file (see Model section of the signature declaration).

Example of initParams method:

void initParams(const openfluid::ware::WareParams_t& Params)
{
m_MyParam = 0; //default value set to the class member
OPENFLUID_GetSimulatorParameter(Params,"myparam",m_MyParam);
}

To be used in other part of the simulator, the C++ variable storing a simulator parameter should be declared as class member. The types of parameters can be string, double, integer, boolean, vector of string, vector of double (see API documentation of OPENFLUID_GetSimulatorParameter method to get more informations about other available types ).

Spatial attributes

In order to access or update values of spatial attributes, or to test if a spatial attribute is present, you can use the following methods:

The methods to test if an attribute exists or to access to an attribute value are usable from any simulators part except from the initParams() part. The methods to update an attribute value are only usable from the prepareData() and checkConsistency() parts of the simulator. The names of the attributes must match the names in the input dataset (see Spatial domain section), or the name of an attribute created by a simulator.

Example of use:

{
{
OPENFLUID_GetAttribute(SU,"area",AreaValue);
// continue with source code using the value of the area attribute
}
return DefaultDeltaT();
}

Simulation variables

The values for the simulation variables are attached to the spatial units.

The available methods to access to simulation variables are:

The available methods to add or update a value of a simulation variable are:

The available methods to test if a simulation variable exists are:

These methods can be accessed only from the initializeRun(), runStep() and finalizeRun() parts of the simulator.

Example:

{
{
OPENFLUID_GetVariable(SU,"MyVar",TmpValue);
TmpValue = TmpValue * 2;
OPENFLUID_AppendVariable(SU,"MyVarX2",TmpValue);
}
return DefaultDeltaT();
}

Events

A discrete event is defined by the openfluid::core::Event class. It is made of a date and a set of key-value informations that can be accessed by methods proposed by the openfluid::core::Event class. A collection of discrete events can be contained in an openfluid::core::EventsCollection class.


A collection of events occuring during a period on a given spatial unit can be acessed using

This method returns an that can be processed.

The returned event collection can be parsed using the specific loop instruction:

At each loop iteration, the next event can be processed.

An event can be added on a specific spatial unit at a given date using:

Example of process of events occurring on the current time step:

{
BTime = OPENFLUID_GetCurrentDate();
ETime = OPENFLUID_GetCurrentDate() - 86400;
{
OPENFLUID_GetEvents(TU,BTime,ETime,EvColl);
{
if (Ev->isInfoEqual("molecule","glyphosate"))
{
// process the event
}
}
}
return DefaultDeltaT();
}

Internal state data

In order to preserve the internal state of the simulator between calls (from the run step to the next one for example), internal variables can be stored as class members. The class members are persistant during the whole life of the simulator. To store distributed values, data structures are available to associate a spatial unit ID to a storedvalue. These data structures exist for different types of data:

Example of declaration of ID-map structures in private members of the simulator class:

class SnippetsSimulator : public openfluid::ware::PluggableSimulator
{
private:
public:
// rest of the simulator class

Example of usage of the ID-map structures:

{
int ID;
double TmpValue;
{
ID = SU->getID();
TmpValue = TmpValue + m_LastValue[ID];
OPENFLUID_AppendVariable(SU,"MyVarPlus",TmpValue);
m_LastValue[ID] = TmpValue;
}
return DefaultDeltaT();
}

Runtime environment

The runtime environment of the simulator are informations about the context during execution of the simulation: input and output directories, temporary directory,... They are accessible from simulators using:

Example:

{
std::string InputDir;
OPENFLUID_GetRunEnvironment("dir.input",InputDir);
// the current input directory is now available through the InputDir local variable
return DefaultDeltaT();
}

The keys for requesting runtime environment information are:

Informations, warnings and errors

Informations and warnings from simulators

Simulators can emit informations and warnings to both console and files using various methods

Using these methods is the recommended way to log and display messages. Please avoid using std::cout or similar C++ facilities in production or released simulators.

Example:

{
{
OPENFLUID_LogInfo("TestUnits #" << TU->getID());
OPENFLUID_DisplayInfo("TestUnits #" << TU->getID());
OPENFLUID_LogWarning("This is a warning message for " << "TestUnits #" << TU->getID());
}
return DefaultDeltaT();
}

The messages logged to file are put in the openfluid-messages.log file placed in the simulation output directory. This file can be browsed using the OpenFLUID-Builder application (Outputs tab) or any text editor.

Errors from simulators

Simulators can raise errors to notify the OpenFLUID framework that something wrong or critical had happened. An error stops the simulation the next time the OpenFLUID framework has the control.

Errors can be raised using OPENFLUID_RaiseError

Example:

void checkConsistency()
{
double TmpValue;
{
OPENFLUID_GetAttribute(SU,"MyAttr",TmpValue);
if (TmpValue <= 0)
{
OPENFLUID_RaiseError("Wrong value for the MyProp attribute on SU");
}
}
}

Debugging

Debugging instructions allow developpers to trace various information during simulations. They are enabled only when debug is enabled at simulators builds. They are ignored for other build types.

In order to enable debug build mode, the option -DCMAKE_BUILD_TYPE=Debug must be added to the cmake command (e.g. cmake <srcpath> -DCMAKE_BUILD_TYPE=Debug).

Example of build configuration:

cmake .. -DCMAKE_BUILD_TYPE=Debug

This debug build mode is disabled using the release build mode, with the option -DCMAKE_BUILD_TYPE=Release.

Simulators can emit debug information to both console and files using various methods

Example:

{
OPENFLUID_LogDebug("Entering runStep at time index " << OPENFLUID_GetCurrentTimeIndex());
return DefaultDeltaT();
}

Additional instructions are available for debugging, see file debug.hpp:

Integrating Fortran code

The C++/Fortran interface is defined in the openfluid/tools/FortranCPP.hpp file. It allows to integrate Fortran 77/90 code into simulators.
In order to execute Fortran code from a simulator, the Fortran source code have to be wrapped into subroutines that are called from the C++ code of the simulator. To help developers of simulators to achieve this wrapping operation, the FortranCPP.hpp file defines dedicated instructions. You are invited to read the FortranCPP.hpp file to get more information about these instructions.

In order to enable the call of Fortran code, the following inclusion must be added at the top of the simulator source code:

Example of Fortran source code (e.g. FortranSubr.f90):

subroutine displayvector(Fsize,vect)
implicit none
integer fsize,i
real*8 vect(fsize)
write(*,*) 'size',fsize
do i=1,fsize
write(*,*) vect(i)
end do
return
end

Example of declaration block int the .cpp file (e.g. FortranSim.cpp):

EXTERN_FSUBROUTINE(displayvector)(FINT *Size, FREAL8 *Vect);

Example of call of the fortran subroutine from the initializeRun method (e.g. FortranSim.cpp):

{
int Size = MyVect.getSize();
CALL_FSUBROUTINE(displayvector)(&Size,(MyVect.data()));
return DefaultDeltaT();
}

The compilation and linking of Fortran source code is automatically done when adding fortran source files to the SIM_FORTRAN variable in the CMake.in.config file (See File CMake.in.config containing the build configuration).

Embedding R code

Note
The embedding of R code in simulators is currently an experimental feature.

Thanks to the RInside package, It is possible to embed R code in simulators written in C++. It also relies on the Rcpp package for handling data from and to the R environment.

In order to embed R code using RInside, the following inclusion must be added at the top of the simulator source code:

#include <RInside.h>

A unique RInside variable is used to run R code, it should be declared as a member of the simulator class (named m_R in this example).

class SnippetsSimulator : public openfluid::ware::PluggableSimulator
{
private:
RInside m_R;
public:
// rest of the simulator class

The R environment can be acessed through the RInside variable and R commands can be run using its parseEvalQ() method.

m_R["varA"] = 1.2;
m_R["varB"] = 5.3;
m_R.parseEvalQ("varC = max(varA,varB)");
// value of the R varC variable can be accessed from C++ through the m_R["varC"] variable

In this short example, simple variables and commands are used. It is possible to perform complex operations involving external R packages, or call R scripts by executing a source() R command through RInside. See the RInside package documentation to get more details and examples.

To help configuring the simulator which is using the RInside package, a CMake module is provided with OpenFLUID to setup the configuration variables when building the simulator. It should be used in the CMake.in.cmake file of the simulator.

FIND_PACKAGE(R REQUIRED RInside)
SET(SIM_INCLUDE_DIRS ${R_INCLUDE_DIRS} ${R_RCPP_INCLUDE_DIR} ${R_RINSIDE_INCLUDE_DIR})
SET(SIM_LIBRARY_DIRS ${R_LIBRARY_DIRS} ${R_RCPP_LIBRARY_DIR} ${R_RINSIDE_LIBRARY_DIR})
SET(SIM_LINK_LIBS ${R_LIBRARIES} ${R_RCPP_LIBRARIES} ${R_RINSIDE_LIBRARIES})

An adjustment of the CMake module path prefix may be required to find the provided R module for CMake

SET(CMAKE_MODULE_PATH "/prefix/lib/openfluidhelpers/cmake;${CMAKE_MODULE_PATH}")

where prefix depends on the OpenFLUID installation path and operating system (e.g. /usr,/usr/local, C:\OpenFLUID-x.x.x)

Warning
Due to limitations of the RInside package, embedding R code in simulators does not support threading.
Simulations including simulators with RInside must be run using Command line mode in OpenFLUID-Builder or using the openfluid command line.
Due to limitations of the RInside package, only one simulator embedding R code can be used in a coupled model.

Miscellaneous helpers

The OpenFLUID API provides miscellaneous functions and classes to help simulators developpers in their setup of data processing or numerical computation. They are available in various namespaces:

In order to use these helpers, the corresponding headers files must be included in the simulator source code.

As they are not detailed here in this manual, more informations about these helpers are available in the provided header files (.hpp), located in the corresponding include directories.