Development of the simulator source code

General information about simulators architecture

Simulator methods sequence and framework interactions

As mentioned in the previous section, 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 time when the simulator will be called again.

This simulation time can be

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

openfluid::base::SchedulingRequest initializeRun()
{
  // do something here
  
  return DefaultDeltaT();
}

openfluid::base::SchedulingRequest runStep()
{
  // do something here
  
  return DefaultDeltaT();
}

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

openfluid::base::SchedulingRequest initializeRun()
{
  // do something here
  
  return DefaultDeltaT();
}

openfluid::base::SchedulingRequest runStep()
{
  double TmpValue = 0.0;

  // do something here

  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 calling, 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 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).

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 be applied. Parsing this graph in different ways is a common task in simulators. This graph can be browsed using predefined macros.

Sequential parsing

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


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


The source code below shows spatial graph parsing examples. 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.

openfluid::base::SchedulingRequest runStep()
{
  openfluid::core::Unit* SU;
  openfluid::core::Unit* UU;
  openfluid::core::Unit* UpSU;
  openfluid::core::UnitsPtrList_t* UpSUsList;
  openfluid::core::DoubleValue TmpValue;

  OPENFLUID_UNITS_ORDERED_LOOP("SU",SU)
  {
    UpSUsList = SU->getFromUnits("SU");

    OPENFLUID_UNITSLIST_LOOP(UpSUsList,UpSU)
    {
      // do something here
    }    
  }  
  
  OPENFLUID_ALLUNITS_ORDERED_LOOP(UU)
  {  
    // do something here
  }
  
  return DefaultDeltaT();
}

Parallel processing using multithreading

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

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


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

void computeA(openfluid::core::Unit* U)
{
 // compute something
 // can use/produce variables
}


void computeB(openfluid::core::Unit* U, 
              const double Coeff)
{
 // compute something else, with extra args
 // can use/produce variables
}


openfluid::base::SchedulingRequest runStep()
{

  APPLY_UNITS_ORDERED_LOOP_THREADED("SU",MySimulator::computeA);
  APPLY_UNITS_ORDERED_LOOP_THREADED("TU",MySimulator::computeB,2.5);
  
  APPLY_ALLUNITS_ORDERED_LOOP_THREADED(MySimulator::computeA);

  return DefaultDeltaT();
}

Please note:

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 can be statically defined through the input dataset. It can also be defined and modified dynamically during simulations, using primitives allowing 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 Spatial units graph.

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

Generators can help to automatically build a spatial domain graph, or to extend an existing one.
Currently, only one spatial domain generator is available:

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 informations about simulation time can be accessed from any part of the simulator (except from the constructor), using the following methods:

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


Example of code:

openfluid::base::SchedulingRequest runStep()
{
  long int Duration = OPENFLUID_GetSimulationDuration();

  long int CurrentIndex = OPENFLUID_GetCurrentTimeIndex();
  openfluid::core::DateTime CurrentDT = OPENFLUID_GetCurrentDate();  
  
  return DefaultDeltaT();      
}

Simulator parameters

Simulators parameters are passed through the model.fluidx file. They 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 used in the model.fluidx file (see Model section), or be filled from the OpenFLUID-Builder graphical interface.

Example of initParams method:

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

To be reused in other part of the simulator, the 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, available on OpenFLUID web site).

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::base::SchedulingRequest runStep()
{
  openfluid::core::Unit* SU;
  openfluid::core::DoubleValue AreaValue;

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

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::base::SchedulingRequest runStep()
{
  openfluid::core::DoubleValue TmpValue;
  openfluid::core::Unit* SU;

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

Events

A discrete event is defined by the 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 macro:

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:

openfluid::base::SchedulingRequest runStep()
{
  openfluid::core::Unit* TU;
  openfluid::core::EventCollection EvColl;
  openfluid::core::Event* Ev;
  std::list<openfluid::core::Event* > *EvList;
  openfluid::core::DateTime BTime, ETime;

  BTime = OPENFLUID_GetCurrentDate();
  ETime = OPENFLUID_GetCurrentDate() - 86400;

  OPENFLUID_UNITS_ORDERED_LOOP("TU",TU)
  {
    OPENFLUID_GetEvents(TU,BTime,ETime,EvColl);
    EvList = EvColl.getEventsList();

    OPENFLUID_EVENT_COLLECTION_LOOP(EvColl.getEventsList(),Ev)
    {
      if (Ev->isInfoEquals("molecule","glyphosate"))
      {
        // process the event
      }
    }

  }

  return DefaultDeltaT();
}

Internal state data

In order to keep the status of the simulation function 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 MySimulator : public openfluid::ware::PluggableSimulator
{
  private:

    openfluid::core::IDDoubleMap m_LastValue;

  public:
  
    // rest of the simulator class

}

Example of usage of the ID-map structures:

openfluid::base::SchedulingRequest runStep()
{
  int ID;
  double TmpValue;
  openfluid::core::Unit* SU;

  OPENFLUID_UNITS_ORDERED_LOOP("SU",SU)
  {
    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:

openfluid::base::SchedulingRequest initializeRun()
{
  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:

Logs, warnings, errors

Log messages from simulators

Simulators can log messages to both console and files using OpenFLUID_Logger :

The messages logged to file are put in a file named with the ID of the simulator suffixed by _ofware-sim.log, placed in the simulation output directory.

The OpenFLUID_Logger facility is the recommended way to log messages. Please avoid using std::cout or similar C++ facilities in production or released simulators.

Example:

openfluid::base::SchedulingRequest runStep()
{
  openfluid::core::Unit* TU;

  OPENFLUID_Logger.get() << 
    "This is a message to both file and console" << std::endl;
  OPENFLUID_Logger.getFile() << 
    "This is a message to file only" << std::endl;
  OPENFLUID_Logger.getStdout() << 
    "This is a message to console only" << std::endl;

  OPENFLUID_UNITS_ORDERED_LOOP("TestUnits",TU)
  {
    OPENFLUID_Logger.get() << "TestUnits " << TU->getID() << std::endl;
  }

  return DefaultDeltaT;
}

Raise warnings and errors

In order to trace errors and warnings during the run of a simulation, simulators can raise errors and warnings messages. These messages autmatically notice the OpenFLUID framework that something wrong or critical had happened. An error stops the simulation the next time the OpenFLUID framework has the control, a warning does not stop the simulation. Error and warnings are reported in the simulation report. (openfluid-messages.log file). They both can be dated with the number of the current time index when the warning or error occurs.

Errors and warnings can be raised using:

Example:

void checkConsistency()
{
  double TmpValue;
  openfluid::core::Unit* SU;
    
  OPENFLUID_UNITS_ORDERED_LOOP("SU",SU)
  {
    OPENFLUID_GetAttribute(SU,"MyAttr",TmpValue);
    
    if (TmpValue <= 0)
    {
      OPENFLUID_RaiseError("Wrong value for the MyProp distributed property on SU");
      return false;
    }    
  }
}

Debugging

Debugging macros add informations on standard output stream (usually displayed on screen). It allows 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.

The following macros are available for debugging:


Example:

openfluid::base::SchedulingRequest runStep()
{
  openfluid::core::Unit* TU;
  openfluid::core::DateTime BeginDate,EndDate;
  openfluid::core::EventsCollection EvColl;

  OFDBG_LOCATE;

  BeginDate = OPENFLUID_GetCurrentDate();
  EndDate = OPENFLUID_GetCurrentDate() + OPENFLUID_GetDefaultDeltaT() - 1;


  OPENFLUID_UNITS_ORDERED_LOOP("TU",TU)
  {
     OFDBG_UNIT_EXTENDED(TU);

     EvColl.clear();
     OPENFLUID_GetEvents(TU,BeginDate,EndDate,EvColl);
     OFDBG_EVENTCOLLECTION(EvColl);
  }

  return DefaultDeltaT();
}

Fortran 77/90 source code integration

The C++ - Fortran interface is defined in the openfluid/tools/FortranCPP.hpp file. In order to execute Fortran code from a simulator, this Fortran source code have to be wrapped into subroutines that will be called from the C++ code of the simulation function.
To help developers of simulators to achieve this wrapping operation, the FortranCPP.hpp file defines macros. These macros allows calls of Fortran77 and Fortran90 source code. You are invited to read the FortranCPP.hpp file to get more information about these macros.


Example of Fortran source code (FSubr.f90):

subroutine displayvector(Fsize,vect)

implicit none

integer Fsize,ifrom
real*8 vect(Fsize)

write(*,*) 'size',Fsize
write(*,*) (vect(i),i=1,Fsize)

return
end


Example of declaration block int the .cpp file (MySim.cpp):


Example of call of the fortran subroutine from the initializeRun method (MySim.cpp):

#include <openfluid/tools/FortranCPP.hpp>

openfluid::base::SchedulingRequest initializeRun()
{
  openfluid::core::VectorValue MyVect;
  
  MyVect = openfluid::core::VectorValue(15,9);
  int Size = MyVect.getSize();

  CALL_FSUBROUTINE(displayvector)(&Size,(MyVect.getData()));

  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).

Miscellaneous tools

These tools have been developped to help simulators developpers in their setup of data processing or numerical computation. They are available in the namespace openfluid::tools. In order to use these tools, 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 tools are available in the provided header files (.hpp), located in the include/openfluid/tools include directory.

See also:
openfluid::tools namespace
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Defines