Introductory tutorial

Welcome to the ECAgent Introductory tutorial. We will be taking a look at some fundamental ideas that make-up the ECAgent framework. To get things started, make sure that you are at least a little familiar with the core concepts of the Entity-Component-System (ECS) architectural pattern. If you are not comfortable with ECS, see the Wikipedia page for more information.

Getting Started:

To start off with, you should make sure you have ECAgent installed:

pip install ECAgent

Now that we have ECAgent installed, we can proceed to create our Agent-based Model (ABM). We will be recreating a simple model inspired by [Dragulescu2002].

Our model will follow this simple procedure:

  • Agents will start the simulation with 1 coin each.

  • At each iteration, each agent will give 1 coin to another, randomly selected, agent.

  • After N iterations, our model will print out the wealth distribution of our agent population.

Let’s get started!

The first thing we want to do is open up the Tutorial.py file, located in the /src/ directory. You should see a completely blank file.

Let’s kick things off by first importing the ECAgent package like so:

[1]:
from ECAgent.Core import *

This will ensure that we have all the necessary classes needed to make our wealth distribution model.

Now that we have that out of the way, lets talk about the five core classes of ECAgent. They are:

  • Model

  • Environment

  • System

  • Agent

  • Component

You will need to understand how all of these classes work in order to create ABMs in ECAgent. If you are familiar with ECS, you should recognize the System and Component classes and, if you are familiar with ABMs, you are most likely familiar with the Model, Environment and Agent classes.

Let’s start with the simplest class, the Component class. In ECAgent, and ECS in general, Components are plain-old-data (POD) types. This means that they only store information, in variables, and have little-to-no functionality. In our Model we would create a MoneyComponent like so:

[2]:
class MoneyComponent(Component):
    def __init__(self, agent : Agent, model : Model):
        super().__init__(agent, model)
        self.wealth = 1  # Wealth Property (Default value of 1)

As you can see, we inherit from the Component base class and define a constructor. The Component class takes two arguments: The agent that the component belongs to and the simulation model. We simply add those parameters to our MoneyComponent constructor so that we can pass along that information when we initialize our MoneyComponent objects. We also add a wealth parameter and set it to 1. This follows our first rule in that all agents will start with 1 coin.

You’ll notice that the MoneyComponent class is incredibly simple. This is intentional. In ECS, components should rarely be complex. It is ok to add some functionality to calculate composite or auxiliary properties but, as a general rule, you should keep your components as simple as possible.

Now lets move onto our next class, the Agent class. You probably noticed from the class definitions that we seem to be missing the ‘Entity’ portion of our Entity-Component-System. In ECAgent, the Agent class is the Entity class. It is responsible for keeping track of the components attached to itself. It may even add/remove components from itself should the situation arise. For our ABM, our MoneyAgent class should look something like this:

[3]:
class MoneyAgent(Agent):
    def __init__(self, id: str, model : Model):
        super().__init__(id, model)
        self.add_component(MoneyComponent(self, self.model))

Again, this isn’t a very complex class, but it illustrates all you need to know about the Agent class. Once again, our base class requires that we pass it our model. We just pass this off to our MoneyAgent constructor function because we will supply the model when we create the agents for the first time. You will also notice the id property. This is a very important property in that it uniquely identifies the agent in our model. If you do not supply each agent with a unique id, the final program will throw an Exception.

Now that we have the Agent(Entity) and Component classes out of the way, lets create our System class. In ECS, Systems are responsible for modifying the values of components. They can also trigger events that trigger other systems and so on and so forth. ECAgent does not have an event system by default. ECAgent executes System procedures through a SystemManager. The SystemManager is responsible for scheduling when events run and when they do not. It is possible to write your own SystemManager but that is beyond the scope of this tutorial. We will just use the default one.

As you’ll remember from our brief model description, at every iteration of our model, an agent, if possible, must give away 1 coin to another random agent. Knowing this, we can create our MoneySystem like so:

[4]:
class MoneySystem(System):
    def __init__(self, model : Model):
        super().__init__("MONEY", model)

    def execute(self):
        components = self.model.systems.get_components(MoneyComponent)

        for component in components:  # Iterate over all MoneyComponents
            if component.wealth == 0:
                pass
            else:
                # Get other agent
                other_agent = self.model.environment.get_random_agent()
                # Transfer Wealth
                other_agent.get_component(MoneyComponent).wealth += 1
                component.wealth -= 1

This will be the most complicated part of our model. As is the norm, our System base class requires a that we supply it with the model it belongs too. Like before, we will just pass this in when we create the system. You will also see one other value being passed in, "MONEY". This is the system’s id. Just like the Agents, systems also require unique identifiers. The id is used by the SystemManager and some other systems in ECAgent.

If you look at the docs, you’ll notice that the System base class initialization method has a number of optional parameters. These parameters control how frequently and in what order your systems should run. It is out of scope for this tutorial but just know that our MoneySystem will run once at every iteration of our model.

Custom System classes also require that you overload the execute() function. This function is called by the SystemManager everytime our model needs to compute at a given timestep. The execute function is where your ABM logic goes. In our case, our MoneySystem needs to iterate through each MoneyComponent (which an agent has) and give 1 coin to a random agent if possible. We can get a list of all MoneyComponent objects in our model using the SystemManager.get_components() method supplying it with the name of our MoneyComponent class as input. We then iterate through each component and, if the agent has money, we give 1 coin to another random agent. We can use the Environment.get_random_agent() method to get a random agent from our environment, and then we use the Agent.get_component() method to get that agent’s MoneyComponent.

For our simple model, using get_random_agent() and get_component() is fine because we will only ever have one type of agent in our model, and we know for certain that each agent will have a MoneyComponent. If you are working with multiple types of Agents with varying components, you should first make sure the agent has the component you are looking for. This can be done using the Agent.has_component() function which returns True if the agent has the desired component(s). You can also supply the Environment.get_random_agent() function with a filter that will automatically exclude any agents that don’t contain all the components specified by the filter. For more information on this, check out the docs.

Putting it all together:

Now that our custom system is done, we can start to put everything together.

Environments are a very important in ABMs. They are responsible for managing the space the agents occupy. The environment may be dimensionless, 2D, 3D, continuous or even discrete. ECAgent allows you to customize your environment in a similar fashion to how you can customize systems, agents and components. However, because this is an introductory tutorial, custom environments will not be covered (They are covered in this tutorial here(TODO)). This is ok for our simple model as we just need an environment that contains some kind of reference to all agents. By default, and if no Environment is supplied to the Model upon initialization, the model will create an empty Environment() object. This environment simply holds a list of the agents currently occupying it and is actually the base class for the complex environments that we will introduce in further tutorials.

As a result of this functionality, you will not see an Environment() object being instantiated explicitly, just be aware of the fact that it is.

Now we move onto our custom model class. It looks like this:

[5]:
class MoneyModel(Model):
    def __init__(self, num_agents : int):
        super().__init__(seed=44)

        self.systems.add_system(MoneySystem(self))  # Add Money System

        # Add Money Agents
        for i in range(0, num_agents):
            self.environment.add_agent(MoneyAgent('a' + str(i), self))

    def run(self):  # Method created to run our model
        while self.systems.timestep < 10:
            self.systems.execute_systems()  # Executes all systems

As you can see, we’ve created a MoneyModel class that inherits from the Model base class. The model base class has two optional parameters: The environment as mentioned above and the seed which have set to 44 in order for us to compare results. The seed is used to construct a pseudo-random number generator that we can use whenever we need to add a little stochasticity to our model. We’ve actually already used it indirectly when we called get_random_agent() in our MoneySystem. By setting the seed of the random number generator, we will get the same results every time. This is essential if you want to make your work reproducible. You can use the model’s random number generator just like you would use python’s random. It works in exactly the same way.

After we call the base constructor, we add our MoneySystem to the SystemManger using SystemManager.add_system(). This will automatically register our system with the system manager. Next we add the agents. You’ll notice that our MoneyModel can have a variable number of agents (num_agents). We simply create that many agents and store them in the environment using Environment.add_agent(). You’ll also notice that we are ensuring that each agent has a unique iq by using the value of i. This is a really simple yet effective method to ensure each agent is uniquely identifiable.

We then define a run() method that we can call from outside the model class. This method runs the model for 10 iterations. To do this we use the timestep property found in the SystemManager object.

Lastly, we call the SystemManager.execute_systems() method. This method is responsible for calling the execute() method we defined for our MoneySystem class. At the end of the execute_systems() method, the SystemManager will increase the timestep counter by 1.

That’s it!!! Our model is finally complete. All we now have to do is run it.

Running the Model:

To run our model, we can use the following bit of code:

[6]:
model = MoneyModel(10)  # Create a MoneyModel with 10 agents
model.run()  # Run model for 10 iterations
print ([x.wealth for x in model.systems.get_components(MoneyComponent)])  # Print Wealth Distribution
[3, 0, 0, 1, 1, 1, 0, 1, 1, 2]

This code just creates a MoneyModel object (with 10 agents), runs the model using model.run() and then prints out the wealth distribution of the agents using a bit of list comprehension.

You should get the following output:

[3, 0, 0, 1, 1, 1, 0, 1, 1, 2]

This is the wealth of all the agents by id. As you can tell Agent 0 is very lucky and seems to have come out on top.

If you did not get this result, make sure that you set the seed of the model to 44.

Conclusion:

You have successfully created your first ABM using ECAgent. As you can hopefully tell, ECAgent is incredibly flexible and can be extended easily. If you are still interested in learning more about ECAgent, take a look at some other, more complicated, tutorials and, if you have any questions, please feel free to email or message one of the devs, and they will be more than happy to assist you.