How to Write an Ikaros Module

Jan Morén

new version of this article is available which is updated for version 1.2 of Ikaros.

To use the Ikaros simulator, we need to know how to write a new simulation module and add it to Ikaros. While this may look daunting at first, it is actually quite easy; most of the stuff needed to do is 'boilerplate' code that can largely be cut and pasted from other modules.

The steps needed are:

Example

Let's assume a module that receives an array of values from two different inputs, sums them, then sends the summed array to the output. We'll call it 'Sum'. We will require that the two input arrays are the same size and will set the output array to the same size as the input arrays.

The Sum.h file

Sum.h looks like this:


     1	//
     2	//    Sum.h  This file is a part of the IKAROS project
     3	//    Pointless example module summing its inputs.
     4	//
     5	//    Copyright (C) 2001-2007 Jan Morén
     6	//
     7	//    This program is free software; you can redistribute it and/or modify
     8	//    it under the terms of the GNU General Public License as published by
     9	//    the Free Software Foundation; either version 2 of the License, or
    10	//    (at your option) any later version.
    11	//    
    12	//    This program is distributed in the hope that it will be useful,
    13	//    but WITHOUT ANY WARRANTY; without even the implied warranty of
    14	//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    15	//    GNU General Public License for more details.
    16	//    
    17	//    You should have received a copy of the GNU General Public License
    18	//    along with this program; if not, write to the Free Software
    19	//    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
    20	//

    21	#ifndef SUM
    22  #define SUM
	
    23	#include "IKAROS.h"

    24	class Sum: public Module 
    25	{
       

    26	public:
    27	    
    28	    // This is all boilerplate code to declare the needed methods for
    29	    // initialization of the module. Just change 'Sum' to whatever name your
    30	    // module has.
       

    31	    Sum(char * name, Parameter * p);
    32	    virtual ~Sum();
    33	    
    34	    static Module * Create(char * name, Parameter * p);
       

    35	    void SetSizes();
    36	    
    37	    void Init();
    38	    void Tick();
       

    39	    // Declare some variables.
    40	    
    41	    int     theNoOfInputs1;
    42	    int     theNoOfInputs2;
    43	    int     theNoOfOutputs;
       

    44	    // the input and output vectors. The storage is declared and handled by
    45	    // Ikaros so you do not have to.
    46	    
    47	    float * input1;
    48	    float * input2;
    49	    float * output;
    50	};
    
    51
    52  #endif

So, what are we doing with this header? On line 9 we declare 'Sum' to be a subclass of Module; the module class is found in 'IKAROS.h' in the IKAROS subdirectory.

Line 31 declares the constructor; the module needs to register in and outputs there so that the simulator can figure out how wide the data paths will be between modules.

Line 34 is a convenience function for Ikaros; just have this in every module.

Line 35 declares the SetSizes function used by Ikaros to determine the input and output sizes at runtime.

37 and 38 declare the two member functions that are the point of the module. Init() is called as soon as all modules have been created, and is used by the module creator to find out the input and output sizes, allocate dynamic storage, and anything else that might be needed at startup.

The rest of the header is used for whatever needs to be declared for the module to function properly.

'Sum.h' is already in the Modules/Examples/Sum subdirectory.

The Sum.cc file

The Sum.cc file may look big and complex, but is actually very simple. We have here intentionally written things in a fairly verbose manner and with lots of commentary to make it easily understandable; a compact version of this would likely be under 30 lines of code. Now for the Sum.cc:


     1	//
     2	//    Sum.cc  This file is a part of the IKAROS project
     3	//    Pointless example module summing its inputs.
     4	//
     5	//    Copyright (C) 2001-2002 Jan Morén
     6	//
     7	//    This program is free software; you can redistribute it and/or modify
     8	//    it under the terms of the GNU General Public License as published by
     9	//    the Free Software Foundation; either version 2 of the License, or
    10	//    (at your option) any later version.
    11	//    
    12	//    This program is distributed in the hope that it will be useful,
    13	//    but WITHOUT ANY WARRANTY; without even the implied warranty of
    14	//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    15	//    GNU General Public License for more details.
    16	//    
    17	//    You should have received a copy of the GNU General Public License
    18	//    along with this program; if not, write to the Free Software
    19	//    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
    20	//
    21	// Created: 2001-12-01
    22	//
    23	// 2003-01-14 and 2007-01-01 Updated for the new simulator
       
    24	#include "Sum.h"
    25
       
    26	Module * Sum::Create(char * name, Parameter * p)
    27	{
    28	    return new Sum(name, p);
    29	}
       
    30	Sum::Sum(char * name, Parameter * p): Module(p)
    31	{ 

    32	    // AddInput here declares two inputs for the module. These are the same
    33	    // names used in the experiment configuration file.
    34	    
    35	    AddInput("INPUT1");
    36	    AddInput("INPUT2");

    37	    // Here we declare an output and tell the simulator that its size will be
    38	    // figured out later. The parameter is not strictly necessary as it is the
    39	    // default value, but it is a good idea to include it for clarity.
    40	    
    41	    AddOutput("OUTPUT", unknown_size);
    42	}
       
    43	// SetSizes is called during initialization of a network to
    44	// let modules set output sizes based on the sizes of their inputs
    45	//
    46	// It is called repeatedly until all sizes for all modules are fixed or until
    47	// Ikaros discovers it is not possible to set a consistent set of data sizes.
       
    48	void Sum::SetSizes()
    49	{
    50	    // find out the sizes of the inputs.

    51	    int in1 = GetInputSize("INPUT1");
    52	    int in2 = GetInputSize("INPUT2");
       
    53	    // now, we require that the input vectors are the same size. So we first
    54	    // check that the sizes are both set, and the compare them to see if
    55	    // they're equal. If not, we abort with an error message.

    56	    if(in1 != unknown_size && in2 != unknown_size){
    57	        if(in1!=in2)
    58	        {
    59	            Print("Sum: the input sizes must be equal: INPUT1 = %d\t INPUT2 = %d\n", in1, in2);
    60	            Notify(msg_fatal_error);
    61	            return;
    62	        }
    63	    
    64	    // Set the size of the output vector to be equal to the size of the
    65	    // input vectors.
    66	 
    67	    SetOutputSize("OUTPUT", in1);
    68	    }
    69	}
    70	 
    71	void Sum::Init()
    72	{
    73	    input1 = NULL;
    74	    input2 = NULL;
    75	    output = NULL;
    76	    
    77	    theNoOfInputs1 = GetInputSize("INPUT1");
    78	    theNoOfInputs2 = GetInputSize("INPUT2");
    79	    theNoOfOutputs = GetOutputSize("OUTPUT");
       
    80	    // Here we could add any number of consistency checks and do any memory
    81	    // allocations and other initializations that wer need to do.

    82	    // Set the variables that will be associated with the inputs and outputs
    83	    // during the simulation. Each time the Tick method is entered, the input1
    84	    // and input2 will point to the input vectors, and output will point to
    85	    // the output vector. 
    86	    
    87	    input1  = GetInputArray("INPUT1");
    88	    input2  = GetInputArray("INPUT2");
    89	    output  = GetOutputArray("OUTPUT");
    90	}

    91	Sum::~Sum()
    92	{
    93	}
       
    94	void Sum::Tick()
    95	{
    96	    int i;
    97	    
    98	    // Sum in action. input1 and input2 are pointers to the current input
    99	    // vectors and output points to the output vector that will be sent
   100	    // along to whatever modules it is connected to.
   101	    
   102	    for(i=0; i<theNoOfOutputs; i++) {
   103	     output[i] = input1[i] + input2[i];
   104	    }
   105	}
      
      

In lines 26 to 29 we define the creator function; this function will look the same in any module we care to make, so just go ahead and copy it - but don't forget to change the module name.

Line 30 is the start of the constructor function. Here we tell the simulator what inputs and outputs we will use. The parameter to the AddInput() function on lines 35 and 36 corresponds to the 'target' in the connection table of the xml file. The first parameter of the AddOutput() function is of the same kind, but in addition, this function needs to know the size of the data array. We could set the output size here if we already know what size it's going to be (as in the case where the output is a simple scalar); usually we set the size to the constant 'unknown_size' and figure out all output sizes in the SetSizes function instead.

On line 48, we define the SetSizes function. This function is called repeatedly during initialization by Ikaros in order to determine the sizes of all inputs and outputs. Here you determine the size of your outputs, and can make that size depend on the sizes of its input as we do here. Note that once an input or output size gets a value it is fixed; you can not write modules that depend on these values converging onto their correct value.

On lines 51 and 52 we ask for the input sizes. On line 56 we check if both have been defined (if not, they may be the next time the function is called). We then do a consistency check to see that both have been defined to the same size, and if not, we print out an error message and exit.

Line 67 simply sets our output size to be the same as whatever size the inputs are.

At Line 71, the Init function is defined. This function is called when all modules have been registered, and is a good place to put any and all initializations that may be needed. In this case, we zero out the input and output variables in lines 73-75, then find out what the input and output array sizes really are in lines 77-79 (again, referred through the connection target names in the xml file). In this simple module we do not actually use these for anything, but are commonly used here to allocate internal arrays.

We can set parameters in the xml file, by adding a tag and use the GetInputValue("tagname", defaultvalue) function to get it. If no such tag is defined in the xml file, the function returns the defaultvalue. There is a GetFloatValue() function for floats and a GetValue() function for arbitrary strings as well. In this way, your module can read any parameters it needs, and they can be defined in the experiments' xml file together with needed parameters for all other modules as well. Again, in this simple module we do not use this functionality.

In lines 87-89, we connect our array variables with the inputs and outputs of our module. Internally, all we are doing is getting pointers to arrays defined in the simulator kernel, of course.

Lines 91-93 defines our destructor function, called at the end of the simulation. If we had any dynamically allocated memory, opened files or anything like that, here is where to close and deallocate.

Finally, in lines 94-105, we have the meat of our module. Every time the Tick() function is called, our input arrays have been initialized with new values, so all we need to do is to loop through these arrays and sum them. A lot of work for little result, it may seem, but all of these preliminaries will be much the same size, no matter what the module is. The Tick() function will in a non-trivial module contain almost all the code.

Add Sum to Ikaros

Now, we have our module, but we're not yet ready to run it. We need to add the module to the UserModules.h located in the UserModules directory file, so that it gets initialized properly, and make sure our module is built with all other modules when we compile Ikaros.

First, add

#include "UserModules/Examples/Sum/Sum.h"

at the top, among the other module includes. Then add

k.AddClass("Sum", &Sum::Create, "Source/UserModules/Examples/Sum/");

in the same way as the other modules in the 'InitUserModules' function. (The version of this example included with Ikaros is in the Modules directory so it is included in Module.h rather than UserModules.h but UserModule is the directory to place you own modules.)

Create Sum.ikc

To let Ikaros know about the capabilities of your new module you need to create an .ikc file that describes the input, outputs and parameters of the module. The .ikc file for the module Sum is listed below:

      1   <?xml version="1.0"?>
      2		
      3   <group name="Sum" description="adds two inputs">
      4	
      
      5   <description type="text">
      6      Module that sums its two inputs element by element.
      7      Both inputs must have the same size.
      8   </description>
      9	
            
     10   <xample description="A simple example">
     11   <module
     12      class="Sum"
     13      name="Sum"
     14   />
     14   </example>
          
     15
     16   <input name="INPUT1" description="The first input" />
     17   <input name="INPUT2" description="The second input" />
     18
     19   <output name="OUTPUT" description="The output" />
     20     
     21   <module class="Sum" />
     22
          
     23   <author>
     24      <name>Jan Morén</name>
     25      <email>jan.moren@lucs.lu.se</email>
     26      <affiliation>Lund Univeristy Cognitive Science</affiliation>
     27      <homepage>http://www.lucs.lu.se/People/Jan.Moren</homepage>
     28   </author>
     27
     29   <files>
     30      <file>Sum.h</file>
     31      <file>Sum.cc</file>
     32      <file>Sum.ikc</file>
     33   </files>

     35   </group>

The ikc file for a module is used internally by Ikaros to know about the module, but it is also used to automatically generate documentation of a module. This means that you should give a comprehensie description of what the modules does in this file. The idea is that this is the only documentation that you need to produce for a module so the information in the ikc file should be sufficient to understand the module.

Line 2 sets the name of the module and gives a short description. A longer description of the module is given in lines 5-8. Here the description in plain text but it is also possible to use XHTML here. In this case the type attribute should have been set to 'xhtml' instead of 'text'.

Lines 10-14 gives an example of how the module can be used.

Lines 16-21 describes the inputs and outputs of the module. If the module has had parameter, these too whould have been described here. The <module> element at line 21 defines whhat code to run when the module is used. Since we want to run the code in Sum.cc, we set the class to 'Sum'.

The final lines 23-33 adds additional information about the module such as the name of the author.

Runtime

To actually test the module, we need two additional components: an ikc file describing the experiment and a file with input.

The experiment IKC file

To get our module to actually do something, it needs to be connected with other modules that will give it arrays to sum and that will take the result and display or save it in one way or another. Also, many modules have parameters that may need to be set (our Sum module has the 'outputsize' parameter, for example). Both these functions are done in an xml file that describes how our simulation should be set up; if we wanted to run a set of simulation with different parameters, we would have one xml file per variation.

Here is a depiction of how we will connect our 'Sum' module:

Diagram of the Sum module

Here is our ikc file (let's call it 'network.ikc'):

     1	<?xml version="1.0"?>
     2	<group>
     3	   
     4     <module
     5        class = "InputFile"
     6        name = "DATA"
     7        filename = "inputfile.txt"
     8        iterations = 1"
     6	    />
     7	 
     8	   <module
     9        class = "OutputFile"
    10        name = "OUT"
    11        filename = "outputfile.txt"
    12     >
    13	      <column name = "OUT"/>
    14	   </module>
    15	 
    16	   <module
    17         class = "Sum"
    18         name = "SUM"
    19     />>
    20	  
    21	   <connection sourcemodule = "DATA"   source = "A"
    22	        targetmodule = "SUM"    target = "INPUT1"/>
    23	 
    24	   <connection sourcemodule = "DATA"   source = "B"
    25	        targetmodule = "SUM"    target = "INPUT2"/>
    26	 
    27	   <connection sourcemodule = "SUM"    source = "OUTPUT"
    28	        targetmodule = "OUT"    target = "OUT"/>
    29	  
    30	</group>

The ikc file is composed of two main parts: the module definitions and the connections between them. Every module has its own entry with name and any parameters it needs.

To get our 'Sum' module rolling, we need the module itself, some kind of input (to get arrays to sum) and some kind of output (so we can see the result). We first add an 'InputFile' module, and call it 'DATA'. This module also needs to know what file to read ('inputfile.txt'). We have added an 'iterations' tag that tells it how many times to read the file; it defaults to 1 and is thus not really needed here.

Next, we add an output module, called 'OutputFile'. We name it 'OUT', and decide that it will write the output into 'outputfile.txt'. We add a 'column' tag with name 'OUT' which will be the target input for our output. We can add as many column tags as we wish, and also set various proprties on them such as being integer or a float; number of decimal places and so on - check the documentation for the module.

Last, we add our new module 'Sum' and call this instance 'SUM'. Had we defined any parameters to set, these would be set as tags here as well.

Note that there is nothing stopping us from adding more than one instance of a module; we could add another 'Sum' module, or multiple 'InputFile' modules, reading from different files. All that's required is that we do not name the instances the same.

Next, we need to describe how to connect these modules to one another. This is done with 'connection' elements.

A connection is described as:

   <connection
      sourcemodule = "name1"
      source = "output"
      targetmodule = "name2"
      target = "input"
   />

'name1' and 'name2' refers to the names we gave to the modules above. the 'output' and 'input' refers to the names of the in- and outputs we have given them in the code. For our 'Sum' module, we gave them the names 'INPUT1', 'INPUT2' and 'OUTPUT' (lines 35-41 in Sum.cc above).

So, the first connection above states that output 'A' from our input module 'DATA' should go to 'INPUT1' in our module 'SUM'. Similarily, the 'B' output should go to 'INPUT2'. Finally, the result in 'OUTPUT' should go to the output module 'OUT', to its sourse 'OUT'.

Input file

We need one last thing before we can test this, we need an input file. Here's an example:

A/2		B/2
1 1		1 1
2 1		1 2
3 1		1 3
4 1		1 4
5 5		5 5
0 0		0 0
0 0		0 0

This looks slightly strange, but is the standard format for the 'InputFile' module.

The first line declares the names of all outputs (that's where the 'A' and 'B' came from above) and how wide each output is. In this case, we want both outputs to be just two values wide. We could have 'A/3 B/3' and then have two three-element arrays to add, which would require six values per row. Our output will resize appropriately as we saw earlier.

We run this with:

    ikaros network.ikc

or, if you haven't set the path to your own bin/ directory:

    ~/bin/ikaros network.xml

If all goes well, the simulator spits out some information on our modules and how they're interconnected, and tells us our simulation started, then stopped.

Let's look at the file 'outputfile.txt':

T/1	OUT/2
0	0.0000	0.0000	
1	0.0000	0.0000	
2	2.0000	2.0000	
3	3.0000	3.0000	
4	4.0000	4.0000	
5	5.0000	5.0000	
6	10.0000	10.0000	

Now, this seems wrong, doesn't it? The first column is a time step (which the 'OutputFile' module generates for us). The next one should be our summed values, but 1+1 is definitely not 0, and neither is 1+2 (or 2+1). the third value is right for the first pair, though, and the fourth is right for the second. The results have 'slipped' down two steps. Also, the last values are missing from the output. There should be two arrays of zero at the end.

This is not really wrong, though. Ikaros runs in discrete time steps, and it takes a few steps for values to propagate through the system. This is what it looks like in time:

Time InputFile A and B Sum INPUT1 and INPUT2 OutputFile
01 1 1 10 0 0 00 00 0
12 1 1 21 1 1 12 20 0
23 1 1 32 1 1 23 32 2
34 1 1 43 1 1 34 43 3
85 5 5 54 1 1 45 54 4
80 0 0 05 5 5 510 105 5
80 0 0 00 0 0 00 010 10

After which the input file is empty, and the simulation stops. This is not an error; this just reflects the fact that calculations aren't instantaneous. To get the last values, we pad the input with a few rows of zeroes; this might interfere with a simulation being run over multiple iterations, though (In this case the extend attribute in InputFile can be used to add the extra zeros).

This has been a walkthrough of the implementation of a (very) simple module. For a complex module all the extra work will be in the module itself; the effort required to interface it with Ikaros is just about the same as in this example. To find out more, browse through the documentation and take a look at other predefined modules available as part of the distribution.