Designing your first agent#

In this tutorial, we will go over how to make a POLARIS simulation from scratch.

Coding an agent#

Begin by opening the project produced at Getting Started

Under the “apps” filter - go to the Test_Application and open Test.cpp, you should see the following

#include "Core\Core.h"

using namespace polaris;

struct MasterType
{
};

int main()
{
}

here MasterType is a struct which is used as a dictionary of all Polaris Components used in the application at compile time

Defining The Agent#

So, what we intend to accomplish is to create 2 simple agents which say their name and the current iteration.

The first step is to define the agent like so: Each POLARIS object should use three files: Prototype, Implementation and Methods. Create a file for each:

  • Agent_Prototype.h

  • Agent_Implementation.h

  • Agent_Methods.h

In Agent_Prorotype.h:

#pragma once
using namespace polaris;

prototype struct Agent
{
	proto_func(void, Setup, (string& /*name*/)(int /*id*/));
	proto_func_templated((TargetType), void, Template_Example, (TargetType /*stuff*/));
	accessor(name, NONE, NONE);
	accessor(id, NONE, NONE);
};

There are a few macros that are used here. They are used so that you don’t need to type as much and it makes it a bit easier to read.

  • prototype - this defines the template signature required for the prototype structure as part of the CRTP/Facade pattern we use.

    This expands to:

  template<typename ComponentType>
  • proto_func - this defines the member function which calls the appropriate method in the associated implementation. Note the use of parenthesis in the parameter list.

    In this case it expands to:

  void Setup(string& arg1, int arg2)	{ return static_cast<ComponentType*>(this)->Setup_impl(arg1, arg2)}
  • proto_func_templated - similar to proto_func this is used for methods that are templated. Again, note the use of parenthesis in the list of template parameters

    In this case it expands to:

  template<typename TargetType> void Template_Example(TargetType arg1)	{ return static_cast<ComponentType*>(this)->template Template_Example_impl<TargetType>(arg1);	}
  • accessor - this is used to define the methods to set/get member variable

In Agent_Implementation.h:

#pragma once
#include <string>
#include "Agent_Prototype.h"

implementation struct Agent_Implementation : public Polaris_Component<MasterType, INHERIT(Agent_Implementation), Execution_Object>, public Agent<IMPLTYPE(Agent_Implementation)>
{
	impl_func(Agent_Implementation, void, Setup, (std::string& /*name*/)(int /*id*/));
	impl_func_templated(Agent_Implementation, (TargetType), void, Template_Example, (TargetType /*stuff*/));

	static void Talk(Agent_Implementation* _this, Event_Response& response);
	t_data(std::string, name);
	t_data(int, id);
};

#include "Agent_Methods.h"

There are several macros used here to simplify coding and make it easier to read.

  • implementation - this macro defines the template signature required for the implementation portion of the CRTP?Facade pattern

  • INHERIT - Polaris objects know their full identity at compile time, this is tracked in the type “InheritanceList” and communicated to the Polaris_Component

  • IMPLTYPE - This macro defines the template signature of the Polars object

  • impl_func - this is similar to the proto_func described above, except for the implementation methods. Note the use of parenthesis as well as the naming of the object

    In this case it would expand to:

  	void Setup_impl(std::string& arg1, int arg2)
	{
		return this->Setup_(arg1, arg2);
	}
  	void Setup_(std::string& arg1, int arg2)
	{
		return this->_Setup(arg1, arg2);
		// if a derived class also implements this method
		// the above line would be:
		// this_component_impl()->Setup_impl(arg1, arg2);
		// and follow that method chain
	}
    void _Setup(std::string& arg1, int arg2)
  • impl_func_templated - this is used to define the implementation method associated with the proto_func_templated macro

    In this case it would expand to:

  	template<typename TargetType> void Template_Example_impl(TargetType arg1)
	{
		return this->template Template_Example_<TargetType>(arg1);
	}
  	template<typename TargetType> void Template_Example_(TargetType arg1)
	{
		return this->template _Template_Example<TargetType>(arg1);
		// if a derived class also implements this method
		// the above line would be:
		// this_component_impl()->template Template_Example_impl<TargetType>(arg1, arg2);
		// and follow that method chain
	}
    template<typename TragetType> void _Setup(std::string& arg1, int arg2)
  • t_data - this is used to declare object data members as well as the implementation methods to get and set values

Adding Behavior#

In Agent_Methods.h:

#pragma once

// Note the underscore in the method name, defined by impl_func()
implement void IMPLTYPE(Agent_Implementation)::_Setup(std::string& name, int id)
{
	_name = name;
	_id = id;

	this->Load_Event(&Talk, 0, 0);
}

//Note the underscore in the name of the method, defined by impl_func_templated()
implement template<typename TargetType>
void IMPLTYPE(Agent_Implementation)::_Template_Example(TargetType stuff)
{
	cout << _name << " Whatever: " << stuff << endl;
}

//Note - no underscore , it is not defined by impl_func()
implement /*static*/ void IMPLTYPE(Agent_Implementation)::Talk(Agent_Implementation* _this, Event_Response& response)
{
	cout << "My name is: " << _this->_name << " (" << _this->_id << "); the current iteration is: " << iteration() << endl;

	response.next._iteration = iteration() + 1;
	response.next._sub_iteration = 0;
}

Here we have a setup method, a a method to show how templated methods work and an event function. All events use the same signature - they return void and take as parameters pointer to the class and a reference to the specialized structure Event_Response.

The macro implement is just shorthand expanding to:

template<typename MasterType, typename InheritanceList, typename DerivedType>

The setup function loads the event into the execution engine and specifies the first iteration and sub-iteration it will be executed. Polaris commonly uses iteration to denote a clock ticking and sub-iteration to differentiate the different events which may occur during that clock tick. In this case the agent will go on the first iteration and the first sub-iteration (0,0).

Our event function does what we said we wanted it to do, the person will say their name and what the current iteration is. Notice that since this is a static function we don’t have an implicit “this”, however POLARIS makes sure to supply that information in the _this variable. The iteration is one of several global variables available, accessible through getter functions.

The primary way for an agent to schedule themselves for the future is through the use of the Event_Response structure. This structure is set to go next at iteration = iteration()+1 and sub_iteration = sub_iteration().

Initializing the Agent#

This is standard good practice for C++, but it bears repeating here. When an object is created, the memory block it is placed in is not guaranteed to be empty, so we need to set the variables to safe values.

In Agent_Implementation.h:

#pragma once
#include <string>
#include "Agent_Prototype.h"

implementation struct Agent_Implementation : public Polaris_Component<MasterType, INHERIT(Agent_Implementation), Execution_Object>, public Agent<IMPLTYPE(Agent_Implementation)>
{
        Agent_Implementation(); // Default constructor

	impl_func(Agent_Implementation, void, Setup, (std::string& /*name*/)(int /*id*/));
	impl_func_templated(Agent_Implementation, (TargetType), void, Template_Example, (TargetType /*stuff*/));

	static void Talk(Agent_Implementation* _this, Event_Response& response);
	t_data(std::string, name);
	t_data(int, id);
};

#include "Agent_Methods.h"

And in Agent_Methods.h:

implement void IMPLTYPE(Agent_Implementation)::Agent_Implementation()
{
	_name = "ERROR: DEFAULT NAME";
	_id = -1;
}

In this case, we set the variables to values that let us know the “Setup” function was never hit.

Set Up the Simulation#

Now that we have set up our agent, it is time to set up the simulation.

#include "Core\Core.h"

using namespace polaris;

struct MasterType
{
};

int main()
{
	// Simulation configuration is used to set the run parameters for the program
	Simulation_Configuration cfg;
	// use this to set up a single-threaded run, specifying the number of iterations and threads to use
	cfg.Single_Threaded_Setup(10);
	//Similarly can configure for multithreaded application
	// 10 itertion with 2 threads
	//cfg.Multi_Threaded_Setup(10,2);
	 
	// initialize the run environment
	INITIALIZE_SIMULATION(cfg);
    
    // You will add code here to create and initialize agents

	START(); // Starts the simulation

	TERMINATE_SIMULATION(); // cleans up 
}

Setting up the simulation is fairly straightforward; before the simulation can be set up, Core needs to know a few details. The main relevant ones are how many threads to use and how many iterations to perform. Here we will use a default single threaded configuration which will run for 10 iterations. The configuration information is passed along through the use of the INITIALIZE_SIMULATION() macro.

Creating Agents#

Next, we should make our two agents, Bob and Sally.

#include "Core\Core.h"
#include "Agent_Implementation.h"

using namespace polaris;

struct MasterType
{
	typedef Agent_Implementation<MasterType> agent_type;
};

int main()
{
	Simulation_Configuration cfg;
	cfg.Single_Threaded_Setup(10);
	INITIALIZE_SIMULATION(cfg);

	// Allocate the agent
	MasterType::agent_type* bob = Allocate< MasterType::agent_type >();

    // initialize the agent with the name "Bob"
	bob->Setup(string("Bob", 1));

	// Allocate another agent
	MasterType::agent_type* sally = Allocate< MasterType::agent_type >();
	
    // initialize the agent with the name "Sally"
	sally->Setup(string("Sally", 2));

	START();

	TERMINATE_SIMULATION();
}

The first thing to point out is that we have started making use of MasterType. Many POLARIS objects are very interested in what types things are, to keep them all straight and to make sure agent_type means the same thing throughout the simulation we log them in the MasterType and make reference to them.

POLARIS objects should be allocated using the POLARIS-specific global Allocate function. The Allocate function mainly wants to know what type of thing to allocate (in this case MT::agent_type). Notice that Allocate can also take a universally unique id (uuid) for this object when it is allocated - Bob is 1 and Sally is 2. This enrolls the agent into a directory which allows it to be accessible from anywhere in the code through the use of its’ uuid. Also note that allocate only takes one argument - constructors which are not default constructors are not supported for POLARIS objects at this time.

Run The Simulation#

Finally, we can compile our simulation code and run it.

A couple of other projects need to be built before this one can. Fortunately, this is easy to do. First, right click on the IO library and click build; Next, right click on the Antares library and click build. Now, you should be able to right click on the Test_Application project and click build.

The output executable is located in the directory described in Building the Application section.

This application does not have any parameters. Execute the application and if everything went as planned we can see the desired output:

Bob Whatever: 12
Sally Whatever: 23
My name is: Bob (1); the current iteration is: 0
My name is: Sally (2); the current iteration is: 0
My name is: Bob (1); the current iteration is: 1
My name is: Sally (2); the current iteration is: 1
My name is: Bob (1); the current iteration is: 2
My name is: Sally (2); the current iteration is: 2
My name is: Bob (1); the current iteration is: 3
My name is: Sally (2); the current iteration is: 3
My name is: Bob (1); the current iteration is: 4
My name is: Sally (2); the current iteration is: 4
My name is: Bob (1); the current iteration is: 5
My name is: Sally (2); the current iteration is: 5
My name is: Bob (1); the current iteration is: 6
My name is: Sally (2); the current iteration is: 6
My name is: Bob (1); the current iteration is: 7
My name is: Sally (2); the current iteration is: 7
My name is: Bob (1); the current iteration is: 8
My name is: Sally (2); the current iteration is: 8
My name is: Bob (1); the current iteration is: 9
My name is: Sally (2); the current iteration is: 9

Next#

Advanced Implementation Prototype Design

Visualizing Your Agent