1. Our first agent-based evolutionary model

1.3. Interactivity and efficiency

 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.

2. Motivation. Rock, paper, scissors

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.

Figure 1. Interface design

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.[1] 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:

other players

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

other players

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

There are two simple ways to measure execution speed in NetLogo. One is using primitives reset-timer and timer. For instance, to time how long it takes to have every agent carry out the operation:

other players

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:

extensions [profiler]

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 show-profiler-report follows:

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

6. Sample run

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.[2]

7. Exercises

You can use the following link to download the complete NetLogo model: nxn-imitate-if-better-noise-efficient.

https://unsplash.com/photos/-1x5HVtV7fk
Picture by Romain Peli

 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?


  1. This action is equivalent to pushing the speed slider to its rightmost position and can also be done via code using primitive no-display
  2. 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.

License

Icon for the Creative Commons Attribution 4.0 International License

Agent-Based Evolutionary Game Dynamics by Luis R. Izquierdo, Segismundo S. Izquierdo & William H. Sandholm is licensed under a Creative Commons Attribution 4.0 International License, except where otherwise noted.

Share This Book