Modeling With Agents: Working With Agents [Systems thinking & modelling series]
This is part 67 of a series of articles featuring the book Beyond Connecting the Dots, Modeling for Meaningful Results.
Working with agents is fundamentally different from working with primitives in a pure System Dynamics model. For instance, in a System Dynamics model if you refer to the value of a variable or stock, a single value is returned. With agents, however, when you refer to the value of a primitive you might get a separate value for each individual agent in your model.
If you have 100 agents and you refer to the primitive Height, you will get 100 different heights – one for each of the agents in the model. Similarly, in the case of our disease model, if you you request the value of the state Infected, you will get a different infected value for each of the agents in the model.
You will need to extend your modeling toolkit in order to be able to effectively manage agents and accomplish your goals in your model. The key building block of this extended toolkit is the vector1. In the following sections we will first introduce the general concept of vectors and then show how you can use them to interact with agents.
Working with Vectors
A vector is an ordered list of items. In Insight Maker vectors can be written using the ‘{’ followed by the ‘}’ sign. Imagine we had a small population of only four people. If we asked the model for the heights of those four people in meters the result might look something like the following2:
{2, 1.8, 1.9, 1.5}
This indicates that our population has four people with heights of 2, 1.8, 1.9 and 1.5 meters. Insight Maker has an extensive set of capabilities and functions for manipulating and summarizing vectors such as this. For instance, if we wanted to know the height of the tallest person in our population, we could use the Max() function:
Max({2, 1.8, 1.9, 1.5}) # = 2
If we wanted to know the height of the shortest person in the population we could use the Min() function:
Min({2, 1.8, 1.9, 1.5}) # = 1.5
Most vector functions can also be written using Object notation. Object notation takes the following form: Object.Function(). Object notation is often cleaner and clearer when working with objects. A vector is a type of object, and as we’ll see, an agent is also a type of object. We’ll primarily use object notation for the rest of this chapter. Here is how we would rewrite the max and min examples using object notation:
{2, 1.8, 1.9, 1.5}.Max() # = 2
{2, 1.8, 1.9, 1.5}.Min() # = 2
Let’s say we wanted to know the average height of the people in our population. We could either use the Mean() or Median() functions:
{2, 1.8, 1.9, 1.5}.Mean() # = 1.8
{2, 1.8, 1.9, 1.5}.Median() # = 1.85
We can also use basic mathematical operations on our vectors. For example, assume we needed to design a room such that the top of the room was at least half a meter above a person’s head. We could find the required room height for each person by adding 0.5 to the vector of heights:
{2, 1.8, 1.9, 1.5} + 0.5 # = {2.5, 2.3, 2.4, 2}
We can also add vectors together. For instance, let’s imagine that some of the agents had hats on, we have measured the height of these hats, and found the following vector of heights: {0.05, 0, 0.1, 0} (two of the people do not wear hats). We could find the height of the agents when they are wearing their hats using:
{2, 1.8, 1.9, 1.5} + {0.05, 0, 0.1, 0} # = {2.05, 1.8, 2, 1.5}
Another useful vector function is the Length() function. Assuming we did not know there were four agents, we could determine how many elements there were in the vector using this function:
{2, 1.8, 1.9, 1.5}.Length() # = 4
You can do a lot with these basic functions but there are also two very powerful vector functions we should mention: Map() and Filter(). Map applies some transformation to each element in a vector and returns a vector of the transformations. As an example, let’s say we wanted to test whether or not our agents were tall enough to ride an amusement park ride with a cutoff of 1.85 meters. We could get a vector containing whether or not each agent was tall enough using:
{2, 1.8, 1.9, 1.5}.Map(x >= 1.85) # = {true, false, true, false}
Here the function x >= 2 is applied to each element in the vector (with x representing the element value) and the results of this element-by-element evaluation of the function are returned.
Filter applies a function to each element in a vector. If the function evaluates to true, the element is included in the resulting vector; if the function evaluates to false, the element is not included in the results. For instance, if we just wanted the heights of the people who were tall enough to ride the ride, we could use:
({2, 1.8, 1.9, 1.5}.Filter(x >= 1.85) # = {2, 1.9}
Lastly, a couple of very useful functions are available to combine vectors. Union() combines two vectors, removing duplicated elements.
Union({1, 2 ,3}, {2, 3 ,4}) # = {1, 2, 3, 4}
Intersection() takes two vectors and returns a vector containing the elements that are in both of the vectors.
Intersection({1, 2 ,3}, {2, 3 ,4}) # = {2, 3}
Difference() takes two vectors and returns a vector containing the elements that are in either one of the vectors but not in both of the vectors.
Difference({1, 2 ,3}, {2, 3 ,4}) # = {1, 4}
There are many more vector functions available, but these are some of the key ones. They will prove invaluable when you work with vectors of agents.
Exercise 10-4 |
---|
Given a vector of heights 2, 1.8, 1.9, 1.5, write an equation to find the tallest height under 1.95 meters. |
Exercise 10-5 |
---|
Given a vector named a, write an equation to find the median of the squares of all the elements in a. |
Exercise 10-6 |
---|
Given a vector named a and a vector named b, write an equation to find the smallest element that is in both vectors. |
Exercise 10-7 |
---|
Given the vector named a, find the mean of the vector without using the Mean() function. |
Accessing Agents
Insight Maker includes a number of functions to access the individual agents within a population. The simplest of these is the FindAll() function. Given an agent population primitive that we’ll call Population, the FindAll function returns a vector containing all the agents within that agent population:
[Population].FindAll()
If your agent population currently contained 100 agents, FindAll would return a vector with 100 elements where the first element referred to the first agent, the second element referred to the second agent, and so on. It is important to note that these elements are agent references, not numbers. So you can use a function like Reverse() on the resulting vector, but you cannot directly use functions like Mean(), as the agent references are not numerical values.3 We will see how to access the values for agents next.
In addition to the FindAll function, other find functions return a subset of the agents in the model. For instance, the FindState() and FindNotState() functions return, respectively, agents that either have the given state active or not active. For instance, returning to our agent-based disease model, our agents had a state primitive called Infected that represented whether the agent was currently sick. We could get a vector of the agents in our population that were currently sick using the following:
[Population].FindState([Infected])
And we could obtain a vector of the agents that were not currently infected with:
[Population].FindNotState([Infected])
Find functions can also be chained together. For instance, if we added a Male state primitive to our agents to represent whether or not the agent was a man; we could obtain a vector of all currently infected men with something like the following:
[Population].FindState([Infected]).FindState([Male])
Nesting find statements is effectively using Boolean AND logic (like you might use on a search engine: “Infected AND Male”). To perform Boolean OR logic (e.g. “Infected OR Male”) and return all the agents that are either infected or a man (or both), you can use the Union function to merge two vectors:
Union([Population].FindState([Infected]), [Population].FindState([Male]))
If you wanted the agents that were either infected or men (but not both simultaneously), you could use:
Difference([Population].FindState([Infected]), [Population].FindState([Male]))
Exercise 10-8 |
---|
Write an equation using the disease example to return a vector of all female infected individuals. |
Exercise 10-9 |
---|
Write an equation using the disease example to return a vector of all female individuals, healthy individuals, or healthy females. |
Agent Values
Once you have a vector of agents, you can extract the values of the specific primitives in those agents using the Value() and SetValue() functions.
The Value function uses two arguments: a vector of agents and the primitive for which you want the value. It returns the value of that primitive in each of the agents. For instance, let’s say our agents have a primitive named Height. We could get a vector of the height of all the people in the model like so:
[Population].FindAll().Value([Height])
A vector of heights by itself is generally of not much use. Often we will want to summarize the vector of agents: converting the vector to a single number that represents some property of the population. For instance, we could determine the average height of individuals in the population. The following equation calculates the mean value of an agent’s height:
Mean([Population].FindAll().Value([Height]))
In addition to determining the value of a primitive in an agent, you can also manually set the agents’ primitive values using the SetValue function. It takes the same arguments as the Value function, in addition to the value to which you want to set primitives. For instance, we could use the following to set the height of all our agents to 2.1:
[Population].FindAll().SetValue([Height], 2.1)
Exercise 10-10 |
---|
Assume our disease model population had a height stock. Provide an equation to find the average difference in heights between males and females. |
An example showing how agents can interact with each other using the Find functions can be found in Chapter 10 of Beyond Connecting the Dots.
Next edition: Modeling With Agents: Agent Geography.
Article sources: Beyond Connecting the Dots, Insight Maker. Reproduced by permission.
Notes:
- In other programing languages and modeling environments vectors are sometimes called “Arrays” or “Lists”. ↩
- Using an equation like
Value(FindAll([Population]), [Height])
. We’ll see later how to construct equations like this. ↩ - The agents certainly contain many numerical values in their stocks, variables, or states; but an agent reference itself is not numerical so you cannot do things such as directly taking the average of the agents or sorting them. ↩