Skip to content
This repository has been archived by the owner on Aug 25, 2020. It is now read-only.

Data Extraction

Marvin Frick edited this page May 28, 2013 · 1 revision

Extracting Data from a Simulation

Next thing to show is how to extract data from a simulation. So far, only processors were used that have all their private storage, and each of them is executed once per simulation round. For data collection, however, a simulation task should be used, because then there is only one instance that has access to the whole simulation. When iterating through the nodes to collect the wanted information, the task has particularly access to the shawn::Node which in turn owns a collection of processors (at least one). To avoid that the task must access the processor directly, it is possible (for the processor) to add a so called tag (see XXX) to the corresponding node. This tag can in turn be easily read by the task.

Adding Tags to the Nodes

At first, the processor must add a tag to the node, and write data that can be collected by the task. Therefore the header of SimpleAppProcessor is extended by the following lines

   protected:
      void write_int_tag( shawn::Node& node,
                          const std::string& tag_name,
                          int value ) throw();

to declare a new method that writes an integer value to a given tag name that in turn is added to the given node.

The source file must include the integer tag which is a basic one and can be found in basic_tags.h:

#include "sys/taggings/basic_tags.h"

Then, the implementation of the declared method is added

// ----------------------------------------------------------------------
void
SimpleAppProcessor::
write_int_tag( shawn::Node& node, const std::string& tag_name, int value )
   throw()
{
   shawn::TagHandle tag = node.find_tag_w( tag_name );

   if ( tag.is_not_null() )
   {
      shawn::IntegerTag* intt = dynamic_cast<shawn::IntegerTag*>( tag.get() );
      if ( intt != NULL )
      {
         intt->set_value( value );
      }
   }
   else
   {
      shawn::IntegerTag *intt = new shawn::IntegerTag( tag_name, value );
      node.add_tag( intt );
   }
}

It first checks, if a tag of the given name already exists. In the consequence that we will potentially alter this value, we use find_tag_w that returns a writable handle (if there exists any).

If the given name has been found (and thus tag is not null), it is checked whether the found tag is an integer one. If so (intt is not null), the stored value is updated with the new one. Otherwise, if the tag is not an integer one, we can not write an integer to an unknown tag type, and thus exit without doing anything. Here you see the type-safety of the implemented tagging in Shawn.

If the given tagname could not be found, a new tag is created and attached to the node. The name for the tag as well as the value are given to the constructor. Then, the tag is added to the node.

Next, the method must be called from somewhere. For simplicity reasons, we write the actual number of known neighbors to the tag. Alter the method process_message as follows.

   if( owner() != msg->source() )
   {
      neighbours_.insert( &msg->source() );
      INFO( logger(), "Received message from '" 
                       << msg->source().label() 
                       << "'" );
   }

   // write actual number of known neighbors to tag
   write_int_tag( owner_w(), "neighbours", neighbours_.size() );

   return true;

The only new code is the line containing write_int_tag (including the corresponding commment), whereas the rest of the shown code is already known. When running the simulation again, each processor will add a tag which contains the actual number of neighbors to the corresponding node.

However, if a node has no neighbors, it would not receive any message and thus would not create any tag named neighbors. Therefore we add

   // write actual number of known neighbors to tag
   write_int_tag( owner_w(), "neighbours", 0 );

to method boot, so that the tag will be initialized in any case.

There is already an example task that is able to iterate through the nodes, and prints all present tags (including value, name of tag, and type). It is part of the examples module in apps (and must be enabled when calling ccmake ../src). It is called tagtest. Hence, when appending tagtest to the above generated configuration file, and running Shawn again with this task executed at last results in Figure XXX.

ext_shawn04.png

For each node, it prints the id and the corresponding tags. The appearence of the lines containing tags follows a certain rule:

  tag_name = value [tag_type]

For example, there is one tag attached to node 99 (the one we created). Its name is neighbours, its value is 10, and it is of type integer.

Data Collection with Simulation Task

Since there is one integer tag that contains the actual number of neighbors attached to each node, this data must be collected. The usual way is to implement a simulation task, and start it once after the simulation finished. The task runs through all nodes and reads the appropriate information.

First, the header collect_simple_app_task.h must be created that contains the following code.

#ifndef __SHAWN_LEGACYAPPS_COLLECT_SIMPLE_APP_TASK_H
#define __SHAWN_LEGACYAPPS_COLLECT_SIMPLE_APP_TASK_H
#include "_legacyapps_enable_cmake.h"
#ifdef ENABLE_SIMPLE_APP

#include "sys/simulation/simulation_controller.h"
#include "sys/simulation/simulation_environment.h"
#include "sys/simulation/simulation_task.h"
#include "sys/node.h"


namespace simple_app
{

   class CollectSimpleAppTask
      : public shawn::SimulationTask
   { 
   public:
      CollectSimpleAppTask();
      virtual ~CollectSimpleAppTask();

      virtual std::string name( void ) const throw();
      virtual std::string description( void ) const throw();

      virtual void run( shawn::SimulationController& sc )
         throw( std::runtime_error );

   protected:
      bool read_int_tag( const shawn::Node& node,
                         const std::string& tag_name,
                         int& value ) const throw();

   };
}


#endif
#endif

The task CollectSimpleAppTask inherits from shawn::SimulationTask, and thus must implement the methods name(), description(), and run(). As already described earlier, name() defines the command which starts the task when running Shawn. description() can be used to get the purpose of the task. run() is called when the task is started.

There is also the additional method read_int_tag that can read a given integer tag (identified by its name) from the given node, and stores the value. If successful, the method returns true. Otherwise, for example, if the tagname does not exist, it returns false.

The implementation of the task is shown next.

#include "legacyapps/simple_app/collect_simple_app_task.h"
#ifdef ENABLE_SIMPLE_APP

#include "sys/taggings/basic_tags.h"
#include "sys/world.h"

namespace simple_app
{

   CollectSimpleAppTask::
   CollectSimpleAppTask()
   {}
   // ----------------------------------------------------------------------
   CollectSimpleAppTask::
   ~CollectSimpleAppTask()
   {}
   // ----------------------------------------------------------------------
   std::string
   CollectSimpleAppTask::
   name( void )
      const throw()
   {
      return "collect_simple_app";
   }
   // ----------------------------------------------------------------------
   std::string
   CollectSimpleAppTask::
   description( void )
      const throw()
   {
      return "Collect information from SimpleAppProcessor";
   }
   // ----------------------------------------------------------------------
   void
   CollectSimpleAppTask::
   run( shawn::SimulationController& sc )
      throw( std::runtime_error )
   {
      require_world( sc );
      int neighbors = 0, count = 0;

      for( shawn::World::const_node_iterator
               it = sc.world().begin_nodes();
               it != sc.world().end_nodes();
               ++it )
      { 
         int tag_value;
         if ( read_int_tag( *it, "neighbors", tag_value ) )
         {
            neighbors += tag_value;
            count++;
         }
      }

      if ( count > 0 ) 
      {
         double connectivity = (double)neighbors / (double)count;
         INFO( logger(), "Connectivity: " << connectivity );
      }
      else
      {
         INFO( logger(), "No tags found." );
      }
   }
   // ----------------------------------------------------------------------
   bool
   CollectSimpleAppTask::
   read_int_tag( const shawn::Node& node, 
                 const std::string& tag_name, 
                 int& value )
      const throw()
   {
      shawn::ConstTagHandle tag = node.find_tag( tag_name );
      if ( tag.is_not_null() )
      {
         const shawn::IntegerTag* intt = 
            dynamic_cast<const shawn::IntegerTag*>( tag.get() );

         if (!intt)
            return false;

         value = intt->value();
         return true;
      }

      return false;
   }
}

#endif

The read_int_method gets the node from which it should read the tag, the name of the tag, and a reference to a variable in which it stores the read value. First, it tries to find the tag by its name. If not found, the method returns false. Otherwise, it checks whether the found tag is an integer one. If so, it assigns the value and returns true. If not, false is returned.

The run() method works as follows. First, it is checked if the world already exists by the method require_world. If not (for example, if the task is called in the first line of the configuration file), an exception is thrown, and the simulation is aborted. Then, it is iterated through all nodes in the world. For each node, it is tried to read the previously written tag (remember method write_tag in the processor). If successful, the read value is added to the sum of all neighbors, and a counter is incremented. These two values are used after the loop to calculate the average connectivity in the network. If not possible (no tags could be read), an appropriate message is printed.

The next step is to add the task to Shawn, so that it can be found if called in a configuration file. That means, the task must be added to the simulation task keeper that is part of the SimulationController. The usual location for doing so is the init method. Hence, simple_app_init.cpp is altered as follows.

#include "legacyapps/simple_app/simple_app_processor_factory.h"
#include "legacyapps/simple_app/collect_simple_app_task.h"
#include "sys/simulation/simulation_controller.h"
#include "sys/simulation/simulation_task_keeper.h"

extern "C" void init_simple_app( shawn::SimulationController& sc )
{
   simple_app::SimpleAppProcessorFactory::register_factory( sc );
   sc.simulation_task_keeper_w().add( new simple_app::CollectSimpleAppTask );
}

In the consequence that a new file has been added (the new task), that file must be made public to CMake. So either rerun ccmake, or alternatively edit (add and remove a space) and save module.cmake. Then recompile.

Before running the simulation again, change the configuration file as follows.

  prepare_world edge_model=simple comm_model=disk_graph range=2
  rect_world width=10 height=10 count=100 processors=simple_app
  send_round=8
  simulation max_iterations=10
  collect_simple_app
  connectivity

In the end of the task there were two tasks added. The newly implemented collect_ simple_app, and connectivity to compare the results. An example is shown in Figure XXX.

ext_shawn05.png

Both tasks show an average connectivity of 10.64. The former computed the value by exchanging messages (and thus the transmission model). The latter used the internal communication model for acquiring the information.