1. Our first agent-based evolutionary model
CODE 1. Goal
Our goal in this section is to improve the interactivity and the efficiency of our model.
By interactivity we mean the possibility of changing the value of parameters at runtime, with immediate effect on the dynamics of the model. This feature is very convenient for exploratory work. In this section, we will implement the necessary functionality to let the user change the number of agents in the population at runtime.
By efficiency we mean implementing the model in such a way that it can be executed using as little time and memory as possible. In this section, we will modify the code of our model slightly to make it run significantly faster.
Sometimes there is a trade-off between interactivity and efficiency. As a matter of fact, making the model more interactive generally implies some loss of efficiency. Nonetheless, oftentimes we can find ways of implementing a model more efficiently without compromising its interactivity.
It is also important to be aware that –most often– there is also a trade-off between efficiency and code readability. The changes required to make our model run faster will frequently make our code somewhat less readable too. Uri Wilensky –the creator of NetLogo– and William Rand do not recommend making such comprises:
However, it is important that your code be readable, so others can understand it. In the end, computer time is cheap compared to human time. Therefore, it should be noted that, whenever there is a possibility of trade-off, clarity of code should be preferred over efficiency. Wilensky and Rand (2015, pp 219–20)
Our personal opinion is that this decision is best made case by case, taking into account the objectives and constraints of the whole modelling exercise in the specific context at hand. Our hope is that, after reading this book, you will be prepared to make these decisions by yourself in any specific situation you may encounter.
The dynamics of many evolutionary models strongly depend on the number of agents in the population. Can you guess how the population size affects the dynamics of the “imitate if better” revision protocol with noise in the Rock-Paper-Scissors game? In this section we will implement the possibility of changing the population size at runtime, a feature that will greatly facilitate the exploration of this question.
3. Description of the model
We will not make any modification on the formal model our program implements. Thus, we refer to the previous section to read the description of the model. The only paragraph we add (about the program itself) is the following:
The number of players in the simulation can be changed at runtime with immediate effect on the dynamics of the model, using parameter n-of-players:
- If n-of-players is reduced, the necessary number of (randomly selected) players are killed.
- If n-of-players is increased, the necessary number of (randomly selected) players are cloned.
Thus, the proportions of agents playing each strategy remain the same on average (although the actual effect of this change is stochastic).
CODE 4. Interactivity
Note that we can already modify the value of parameters prob-revision and noise at runtime, with immediate effect on the dynamics of the model. This is so because the values of these variables are used directly in the code. Parameter prob-revision is used only in procedure to go, in the following line:
if (random-float 1 < prob-revision) [update-strategy]
And parameter noise is used only in procedure to update-strategy, in the following line:
ifelse (random-float 1 < noise)
Whenever NetLogo reads the two lines of code above, it uses the current values of the two parameters. Because of this, we can modify the parameters’ values on the fly and immediately see how that change affects the dynamics of the model.
By contrast, changing the value of parameter n-of-players-for-each-strategy at runtime will have no effect whatsoever. This is so because parameter n-of-players-for-each-strategy is only used in procedure to setup-players, which is executed at the beginning of the simulation –triggered by procedure to setup– and never again.
To enable the user to modify the population size at runtime, we should create a slider for the new parameter n-of-players. Before doing so, we have to remove the declaration of the global variable n-of-players in the Code tab, since the creation of the slider implies the definition of the variable as global.
globals [ payoff-matrix n-of-strategies ;; n-of-players <== We remove this line ]
After creating the slider for parameter n-of-players, we could also remove the monitor showing n-of-players from the interface, since it is no longer needed. Another option (see figure 1 below) is to use that same monitor to display the value of the ticks that have gone by since the beginning of the simulation. To do this, we just have to write the primitive ticks (instead of n-of-players) in the “Reporter” box of the monitor.
The next step is to implement a separate procedure to check whether the value of parameter n-of-players differs from the current number of players in the simulation and, if it does, act accordingly. We find it natural to name this new procedure to update-n-of-players, and one possible implementation would be the following:
to update-n-of-players let diff (n-of-players - count players) if diff != 0 [ ifelse diff > 0 [ repeat diff [ ask one-of players [hatch-players 1] ] ] [ ask n-of (- diff) players [die] ] ] end
Note the use of primitives hatch-players and die to clone and kill agents respectively. The difference between primitives hatch-players and create-players is important. Hatching is an action that only individual agents (i.e. “turtles” and breeds of “turtles”, in NetLogo parlance) can execute. By contrast, only the observer can run create-turtles and create-<breeds> primitives.
Finally, we should include the call to the new procedure at the beginning of to go.
to go update-n-of-players ;; <== New line ask players [play] ask players [ if (random-float 1 < prob-revision) [update-strategy] ] tick update-graph end
And with this, we’re ready to go! Give it a try, and enjoy the good progress you are making!
CODE 5. Efficiency
Naturally, to make a model run faster, one can always untick the “view updates” box on the Interface tab. This is a must in models that do not make use of the view, like the ones we are programming in this chapter, since it implies a significant speed-up at no cost. But beyond this simple piece of advice, in general, how can we know whether our model can run faster? A good first step is to try to identify inefficiencies in our code. These inefficiencies often take one of two possible forms:
- Computations that we conduct but we do not use at all.
- Computations that we conduct several times despite knowing that their outputs will not change.
Let us see an example of each of these inefficiencies in our current code.
4.1. Example of computations that we conduct but do not use
Can you identify computations that we perform in the current implementation but are not actually needed (i.e. the model would behave in the same way without carrying them out)?
Note that in this model we make all agents play in every tick, but we only use the payoffs obtained by the revising agents and by the agents they observe. Thus, we can make the model run faster by asking only revising and observed agents to play. One way of implementing this efficiency improvement would be to modify the code of procedures to go and to update-strategy as follows:
to go update-n-of-players ;; ask players [play] <== We remove this line ask players [ if (random-float 1 < prob-revision) [update-strategy] ] tick end to update-strategy let observed-player one-of other players play ;; <== New line ask observed-player [play] ;; <== New line if ([payoff] of observed-player) > payoff [ set strategy ([strategy] of observed-player) ] end
These changes will make simulations with low prob-revision run much faster.
4.2. Example of computations that we conduct several times when once would do
Let us now focus on the second type of inefficiency pointed out above. Can you identify a computation that we repeatedly conduct in every tick, even though its result does not change?
Note that we undertake the computation:
several times in every tick, but we could conduct it just once for each agent in each simulation. To be sure, we conduct that operation every time an agent computes her payoff in to play:
let mate one-of other players
And also every time an agent revises her strategy in to update-strategy:
let observed-agent one-of other players
This computation may not sound very expensive, but if the number of agents is large, it may well be (see exercise 3 below). To make the model run faster, we could create an individually-owned variable named e.g. other-players, as follows
players-own [ strategy payoff other-players ]
And then we should set the new individually-owned variable other-players to the appropriate value only once at the beginning of each simulation (at the end of procedure to setup-agents).
ask players [ set other-players other players ]
Since we may change the number of players at runtime, we should also include the line above in the block of code where we clone or kill agents in procedure to update-n-of-agents, i.e.
to update-n-of-players let diff (n-of-players - count players) if diff != 0 [ ifelse diff > 0 [ repeat diff [ ask one-of players [hatch-players 1] ] ] [ ask n-of (- diff) players [die] ] ask players [set other-players other players] ] end
Once we have done that, in the two lines of code where we had the code
we should write other-players instead. These changes will make simulations with many players run faster.
4.3. Measuring execution speed of different parts of the code
we could write the following reporter:
to-report time-other-players reset-timer ask players [let temporary-var other players] report timer end
A second –more advanced– way of measuring execution speed involves the Profiler Extension, which comes bundled with NetLogo. This extension allows us to see how many times each procedure in our model is called during a run and how long each call takes. The extension is simple to use and well documented here. To use it in our model, we should include the extension at the beginning of our code, as follows:
Then we could execute the following procedure, borrowed from the Profiler Extension documentation page.
to show-profiler-report setup ;; set up the model profiler:start ;; start profiling repeat 1000 [ go ] ;; run something you want to measure profiler:stop ;; stop profiling print profiler:report ;; print the results profiler:reset ;; clear the data end
The profiler report includes the inclusive time and the exclusive time for each procedure. Inclusive time is the time the simulation spends running the procedure, i.e. since the procedure is entered until it finishes. Exclusive time is the time passed since the procedure is entered until it finishes, but does not include any time spent in other user-defined procedures which it calls. An example of the output printed by
BEGIN PROFILING DUMP Sorted by Exclusive Time Name Calls Incl T(ms) Excl T(ms) Excl/calls PLAY 119130 2804.330 2804.330 0.024 UPDATE-STRATEGY 60131 4441.429 1637.099 0.027 UPDATE-GRAPH 1000 231.718 231.718 0.232 GO 1000 4823.320 147.693 0.148 UPDATE-N-OF-PLAYERS 1000 2.480 2.480 0.002 Sorted by Inclusive Time GO 1000 4823.320 147.693 0.148 UPDATE-STRATEGY 60131 4441.429 1637.099 0.027 PLAY 119130 2804.330 2804.330 0.024 UPDATE-GRAPH 1000 231.718 231.718 0.232 UPDATE-N-OF-PLAYERS 1000 2.480 2.480 0.002 Sorted by Number of Calls PLAY 119130 2804.330 2804.330 0.024 UPDATE-STRATEGY 60131 4441.429 1637.099 0.027 GO 1000 4823.320 147.693 0.148 UPDATE-GRAPH 1000 231.718 231.718 0.232 UPDATE-N-OF-PLAYERS 1000 2.480 2.480 0.002 END PROFILING DUMP
In the example above we can see –among other things– that:
- Simulations spend most of the time executing procedure to play (2804.330 ms) and procedure to update-strategy (1637.099 ms).
- The procedure that is called the greatest number of times is to play, which is called 119130 times. This makes sense, since there were 600 agents in this simulation, prob-revision was 0.1, a revision requires a play by the agent and by the opponent he observes, and we ran the model 1000 ticks (600 × 0.1 × 2 × 1000 = 120000).
- Our implementation to allow the user to modify the number of agents at runtime hardly takes any computing time (just 2.480 ms).
4.4. Other tips to improve the efficiency of NetLogo code
Railsback et al. (2017) give several guidelines to identify slow parts of NetLogo code and make them run faster, providing specific examples for agent-based models written in NetLogo.
CODE 6. Complete code in the Code tab
globals [ payoff-matrix n-of-strategies ] breed [players player] players-own [ strategy payoff other-players ] to setup clear-all setup-payoffs setup-players setup-graph reset-ticks update-graph end to setup-payoffs set payoff-matrix read-from-string payoffs set n-of-strategies length payoff-matrix end to setup-players let initial-distribution read-from-string n-of-players-for-each-strategy if length initial-distribution != length payoff-matrix [ user-message (word "The number of items in\n" "n-of-players-for-each-strategy (i.e. " length initial-distribution "):\n" n-of-players-for-each-strategy "\nshould be equal to the number of rows\n" "in the payoff matrix (i.e. " length payoff-matrix "):\n" payoffs ) ] let i 0 foreach initial-distribution [ j -> create-players j [ set payoff 0 set strategy i ] set i (i + 1) ] set n-of-players count players ask players [set other-players other players] end to setup-graph set-current-plot "Strategy Distribution" foreach (range n-of-strategies) [ i -> create-temporary-plot-pen (word i) set-plot-pen-mode 1 set-plot-pen-color 25 + 40 * i ] end to go update-n-of-players ask players [ if (random-float 1 < prob-revision) [update-strategy] ] tick update-graph end to play let mate one-of other-players set payoff item ([strategy] of mate) (item strategy payoff-matrix) end to update-strategy ifelse (random-float 1 < noise) [ set strategy (random n-of-strategies) ] [ let observed-player one-of other-players play ask observed-player [play] if ([payoff] of observed-player) > payoff [ set strategy ([strategy] of observed-player) ] ] end to update-graph let strategy-numbers (range n-of-strategies) let strategy-frequencies map [n -> count players with [strategy = n] / n-of-players] strategy-numbers set-current-plot "Strategy Distribution" let bar 1 foreach strategy-numbers [ n -> set-current-plot-pen (word n) plotxy ticks bar set bar (bar - (item n strategy-frequencies)) ] set-plot-y-range 0 1 end to update-n-of-players let diff (n-of-players - count players) if diff != 0 [ ifelse diff > 0 [ repeat diff [ ask one-of players [hatch-players 1] ] ] [ ask n-of (- diff) players [die] ] ask players [set other-players other players] ] end
Now that we can change the population size at runtime, we can easily explore the question posed above: How does population size affect the dynamics of the “imitate if better” revision protocol with noise in the Rock-Paper-Scissors game? To do that, let us use the same setting as in the previous sections (i.e. payoffs = [[0 -1 1][1 0 -1][-1 1 0]] and prob-revision = 0.1), start with a small population of 60 agents (n-of-players-for-each-strategy = [20 20 20]), and then, increase n-of-players up to 2000 at runtime. The following video shows a representative run with these settings, where we increased the population size from 60 to 2000 at tick 4000.
As you can see, when the number of agents is small, the population consistently follows cycles of large amplitude among the three strategies. The cycles are so wide that sometimes one or even two strategies go extinct for a while. In stark contrast, when the population is large, the cycles get much smaller and the population tends to linger around the state where each strategy is used by approximately a third of the population.
You can use the following link to download the complete NetLogo model: nxn-imitate-if-better-noise-efficient.
CODE Exercise 1. In this section we have improved both the interactivity and the efficiency of our model. Can you quantify how much faster the current version of the code runs compared to the previous one? For the sake of concreteness, use 1000-tick simulations with 600 agents and prob-revision 0.1.
CODE Exercise 2. In this section we have reduced the number of times procedure to play is called (as long as prob-revision is less than 0.5). To illustrate this, compare the number of times this procedure is called in a 1000-tick simulation with 600 agents and prob-revision 0.1, before and after our efficiency improvement. Can you compute the number of times procedure to play is called in the general case?
CODE Exercise 3. In this section we have reduced the number of times the computation
other players is conducted by creating an individually-owned variable (named other-players). To compare these two approaches, write a short NetLogo program where 10000 agents conduct this operation.
CODE Exercise 4. In this section we have reduced the number of times procedure to play is called (as long as prob-revision is less than 0.5). However, it is still possible that some players will execute procedure to play more than once in the same tick, specially if prob-revision is high. Can you think of a way to reduce the number of calls to procedure to play even further?
- This action is equivalent to pushing the speed slider to its rightmost position and can also be done via code using primitive no-display ↵
- The state where all strategies are equally represented is a globally asymptotically stable state of the mean dynamics of this model (which provides a good approximation for models with large populations). See solution to Exercise 1.2.2. ↵