This tutorial aims to facilitate the understanding of the EvoLudo code structure. The goal is to provide a solid basis to the reader to enable and encourage the creation of custom modules and extensions. The tutorials use VS Code for editing and debugging the code together with the Google Chrome browser for running, visualizing and debugging. With minor differences in the instructions, any other IDE can be used as well. However, setting up the development environment for debugging in the browser may require additional steps.
The TBT module is a good starting point for creating a new module because it is relatively simple and covers a wide range of capabilities. It is used to study pairwise interactions in games, as covered for example in chapter 4: Classical Games or chapter 5: Evolutionary Games. Every module implements interfaces to advertise its capabilities and to specify the GUI elements for visualizing the state the model’s state after loading this module.
The capabilities of the TBT module are advertised and specified through the following interfaces:
for individual performance based on payoffs,
for individual based simulations with pairwise interactions and discrete traits,
for handling differential equations with pairwise interactions,
for deterministic evolution modelled by ordinary differential equations and using the Euler method,
same as HasDE.EM but using the fifth-order Runge-Kutta method with variable steps size,
for deterministic evolution with a stochastic noise term modelled by stochastic differential equations and using the Euler-Maruyama method,
for deterministic evolution in spatial settings modelled by partial differential equations and again using the Euler method together with a reaction-diffusion term.
same as HasDE.PDERD but with a reaction-diffusion-advection term.
Moreover, the TBT module implements the following interfaces to request the GUI views to graphically visualize various aspects of the evolutionary process:
display the traits of all individuals in a 2D plot,
display the fitness of all individuals in a 2D plot,
display the traits of all individuals in a 3D plot,
display the fitness of all individuals in a 3D plot,
plot the mean frequency of each trait over time,
plot the mean fitness of each trait as well as the mean fitness of the populations over time,
plot the fitness distribution for each trait as a histogram,
plot the degree distribution of the population structure,
plot the histogram for the fixation probabilities for each node,
plot the histogram for the fixation and absorption times for each node,
plot the histogram for the sojourn time for the different trait counts in the stationary distribution.
The interface structure is summarized in Fig. 13.1.

This tutorial guides you through the process of creating a new module, TxT, from scratch. Eventually TxT will work identical to the existing TBT module. Note, this change of names is necessary because each module is a java class and class names must be unique.
New modules are created in the maven module EvoLudoCore. In order to get started, create a new java file org.evoludo.simulator.modules.TxT in the EvoLudoCore maven module. In VS Code this is done by right-clicking on the modules folder in EvoLudoCore/src/main/java/org/evoludo/simulator and selecting New Java File ¿ Class…. Enter the name of the new file as TxT. The new file should look like this:
Note, that the following code snippets in this tutorial contain little to no comments because the TBT class serves as a reference file. Besides, the motivation and purpose of each code snippet is explained in detail in the accompanying text.
The new TxT class must extend the Discrete class because we are dealing with a discrete set of traits, say and . This is done by changing the class definition from TxT to TxT extends Discrete. The Discrete class provides a number of useful methods and fields to handle discrete traits (and their interactions) as opposed to continuous traits. The Discrete class is located in the same package as the TxT class, so no import statement is needed.
However, extending Discrete triggers two errors in VS Code because (i) the Discrete class does not provide a default constructor and (ii) TxT does not implement the abstract method getTitle() of the Discrete class.
| . | . | Note: The Discrete class has no default constructor and therefore subclasses must provide their own constructor using one of the constructors of the super class. All single species modules use the constructor Discrete(EvoLudo engine). | . |
Both errors are easily addressed in VS Code by hovering over the error location and selecting Quick fix…, followed by Add constructor TxT(EvoLudo) as well as Add unimplemented methods. Now the content of the file looks as follows:
In the constructor there is nothing else we need to do, so simply delete the TODO comment.
| . | . | Note: If you follow the steps in this tutorial the line numbers in the code snippets should always match the line numbers in your file. | . |
The method getTitle() returns a short title of the module:
This information will be displayed in the list of available modules on the EvoLudo help screen. Before we can verify this we need to make EvoLudo aware of the new module.
In order to run the new module in EvoLudo, we first need to add it to the list of available modules. This is done in the EvoLudo class: open org.evoludo.simulator.EvoLudo in the EvoLudoCore maven module and located in the parent directory of our TxT class. The EvoLudo(boolean loadModules) constructor at the end of the file includes several addModule(...) calls to add all available modules. Add the following line to the list of modules:
together with the necessary import statement at the top:
Open the terminal in VS Code. Make sure the current working directory is the root directory of the EvoLudo project. Compile the project with the command ’mvn clean install’. This may take a few moments. Next, run ’mvn gwt:devmode’. This starts the GWT development web server.
Now switch to the Run and Debug view in VS Code and launch the GWT EvoLudo (localhost) target. If everything is properly configured, this opens a window in the Google Chrome browser and displays the message Compiling evoludoweb. This shouldn’t take long and then display the familiar EvoLudo GUI.
In the GUI click on Settings and then Help to see the list of available modules. The newly minted TxT module is listed as: TxT: 2x2 Games Tutorial. The first part, TxT, is the key to load the module and the second part the descriptive string returned by getTitle().
| . | . | Note: The key to load a module can be customized by overriding the method getKey(). The key defaults to the class name of the module, here TxT. | . |
The module is loaded by entering --module TxT in the settings field and then clicking Apply. Loading the module merely adds a line to the console: Module loaded: 2x2 Games Tutorial and reports an error that no model was found:
| . | . | Note: Similarly, the method getAuthors() can be overridden to claim the authorship of the module. By default, no authors are returned. | . |
More importantly, note that running ’mvn gwt:devmode’ actually set up a powerful development environment: after saving changes to the source code all that is needed to recompile and run the revised code is to reload the browser window! However, reloading the browser window also resets EvoLudo and, more specifically, the option string in the settings field.
Therefore, to make our lives easier by not having to adjust the settings all the time, it’s a good idea to edit the file EvoLudoGWT/src/main/webapp/TestEvoLudo.html and look for the string <div class="evoludo-simulation" > and change its data-clo attribute to --module TxT. This way the TxT module is loaded by default when the browser window is reloaded.
| . | . | Important: The development server that was started with ’mvn gwt:devmode’ serves pages out of the EvoLudoGWT/target/EvoLudoGWT-1.0-SNAPSHOT folder. Hence, changes to EvoLudoGWT/src/main/webapp/TestEvoLudo.html are ignored until the project is recompiled with ’mvn clean install’. Alternatively, the file EvoLudoGWT/target/EvoLudoGWT-1.0-SNAPSHOT/TestEvoLudo.html can be edited directly for immediate effect, but those changes are eventually overridden by ’mvn clean install’. | . |
Moreover, the development environment allows setting breakpoints in the Java code of VS Code and debug the JavaScript code running in the browser. How cool is that?!
| . | . | Important: Step-by-step debugging is possible by setting break points in VS Code. | . |
While debugging becomes more important later on and when adding your own customizations, it doesn’t hurt to give it a try now. For example, set a break point in VS Code at the return of the method getTitle() (line in the TxT class) by clicking the line number on the left side of the editor. The break point is marked by a . Clicking again on the line number removes the break point.
The method getTitle() is called when displaying the list of available options. Click on Help again. This will trigger a call to the method getTitle() and the execution of the code stops at the requested line. The Debug view in VS Code shows the current stack trace and all local variables.
Now you can explore the values of all variables and step through the code. Once you have convinced yourself of how seamlessly Google Chrome and VS Code work together, you can remove the breakpoint by clicking on the . In order to resume execution of the EvoLudo code, simply click on the Continue, F5 button in the debug view.
Many features and capabilities of EvoLudo modules are advertised by implementing certain interfaces. More specifically, with our TxT module we want to model pairwise interactions in games, which means individuals not only carry a trait but also a score (or fitness) that is determined through payoffs from interactions with other members of the population. In order to advertise that our module uses payoffs we need to implement the interface Payoffs:
Now VS Code alerts us that the interface Payoffs requires the implementation of two methods: getMinGameScore() and getMaxGameScore(). As before, just hover over the location of the error (marked by a red squiggly line) and select Quick fix… followed by Add unimplemented methods.
The two methods, getMinGameScore() and getMaxGameScore(), must return the minimum and maximum scores, respectively, that an individual can achieve in a single interaction. Those bounds on the scores are important to convert payoffs to probabilities and properly scale the range of views in the GUI. We will get back to those methods later. That is, once we have implemented the payoff matrix.
In order to model games we need to define the payoff matrix as well as the indices of the different traits. For readability, we define two constants for the trait indices. In TBT the two traits are called COOPERATE and DEFECT for historical reasons. Let’s call them A with index 0 and B with index 1. Next, we define a two-dimensional array payoffs, where payoffs[x][y] holds the payoff to an individual with trait x against another with trait y, where x and y refer to either A or B.
After adding the definition of constants and the payoff array to the beginning of our module, we get:
Note, to conserve memory, do not allocate the payoffs array. The gain in this case is minimal, of course, but it is still a good habit. Also, you are encouraged to add comments to explain the purpose of all variables and methods. Now, before implementing the two remaining, auto-generated method stubs, let’s make the newly created module available in EvoLudo.
The EvoLudo code has a highly modular structure with modules for different evolutionary setups, models for different kinds of analysis and modes for analyzing the evolutionary process (see section 11.2.1: Modules, Models and Modes). The new module TxT is loaded into the EvoLudo engine with the setting --module TxT, where TxT denotes the key to identify a module. The key defaults to the class name but can be customized by overriding the optional method getKey(). Whenever a module gets loaded, the method load() is called. If any initialization or memory allocations are required, this is the place to do so. In our case, this means allocating (and freeing) the memory for the payoff matrix. In addition, the names and colours for the different traits can be set, but this is optional. Before a new module is loaded the current one (if any) gets unloaded by calling the method unload() to free up resources.
| . | . | Important: The number of traits nTraits must be either set during loading by overriding the method load() (and include a call super.load()), or in a static block of the class. | . |
The Color class requires another import statement at the top:
| . | . | Note: The colors-array can hold nTraits or 2 * nTraits colours. The first nTraits colours are used to mark the traits of individuals in the GUI views and tooltips (or the output when exporting graphics). The second set of nTraits colours mark the traits of individuals that have changed since the previous update. If no (or insufficient numbers of) colours are set, the default colours of Color.BLUE, Color.RED, Color.GREEN, Color.YELLOW, Color.MAGENTA, Color.ORANGE, Color.PINK, Color.CYAN are used for up to eight traits. Further trait colours are generated at random. Newly changed traits are marked by a paler shade of the trait colour. | . |
| . | . | Note: The names-array holds the names for all nTraits traits. If no names are set, the default names are "Trait A", "Trait B" etc. This affects the axis labels and tooltips in the GUI as well as the output when exporting data. | . |
Similarly, when unloading the module (before potentially loading a different one), the method unload() is called. This is primarily an opportunity to release all resources that were allocated in load().
| . | . | Important: Always include a call to super.unload() to release all resources allocated by parent classes. In most cases the garbage collector is unable to identify unused resources because the shell of every module is initialized at launch time and remains in memory. | . |
The TxT module introduces only a single custom field payoffs to store the game matrix. Restricting access to fields of an object is not only a good habit but failure to do so would defeat the basic tenets of object-oriented programming. Therefore, we provide setter and getter methods to access the payoff matrix from outside the module:
| . | . | Note: Another good habit is to include Javadoc descriptions of all fields, constants and especially methods. | . |
Since we can always refer to the corresponding documentation in the original TBT module, we will not include the Javadoc descriptions in this tutorial.
For quick and dirty tests it is possible to hard code parameter settings. For example, we could fix the payoff matrix to Axelrod’s famous payoffs by replacing the line
with
at the end of the method load(). This is perfectly fine during code development and testing. However, in the long run a much better and more flexible approach is to add with very little extra effort a new command line option to set the payoff matrix.
All command line options are instances of the CLOption class. The class provides a number of constructors for different kinds of command line options.
The most common constructor is: CLOption(String name, String defaultArg, Category category, String description, CLODelegate delegate) and takes five arguments:
the name of the command line option (without the ’--’ prefix): "paymatrix".
the default value of the command line option (Axelrod’s famous payoffs in the prisoner’s dilemma): "3,0;5,1".
the command line option falls into the module category (for organizing options in the help screen): Category.Module.
a short description of the command line option (for the help screen): "--paymatrix <a,b;c,d> 2x2 payoff matrix".
the delegate to parse the command line option and configure the module accordingly.
As the last argument the CLOption(...) constructor expects an implementation of the CLODelegate interface.
The delegate must implement the CLODelegate interface and provide the method parse(String arg) to process the command line option and its arguments. The delegate defines three methods, but the implementation of only one is required:
(required) parse the arg string and set the parameters of the module accordingly. If parsing is successful, the payoff matrix is set and the method returns true. If parsing fails, for example if the matrix has wrong dimensions or non-numerical entries, the method must return false. This triggers a warning message with the name and argument of the option that caused trouble, which is logged (and shown in the GUI). Finally, if the option is missing, or parsing failed, the method parse(String) is called with the defaultArg provided in the CLOption constructor (see above).
(optional) return the description of the option. In most cases the String description argument to the CLOption(...) constructor is sufficient. Implementing the method getDescription() is only required if the description depends on other parameter settings. Most commonly this is the number of traits in a module, which may affect the formatting of the argument arg.
(optional) some options accept a selection of key values. For example, the options --module or --model. The getKeyPos() method returns the position of the key in the argument string for options that accept keys and additional arguments. By default, the key is expected in position 0 and may be followed by additional parameters. The option --init is an example. In contrast, --mutation, for example, expects the key in position 1.
In Java this CLODelegate can be easily implemented through anonymous inner classes. Instead of writing a custom class that implements the CLODelegate interface, we can simply pass the following code as the last argument to the above CLOption(...) constructor:
This creates an anonymous inner class (one without a name) that implements the CLODelegate interface and overrides the method parse(String arg). Assembling all pieces yields the following code snippet to define the command line option --paymatrix:
The code snippet above requires importing the following classes:
and has already been taken into account with the line numbers.
| . | . | Important: Make sure the default arguments are valid. Invalid default arguments may result in unexpected or undefined behaviour. | . |
Finally, register the new command line option with the option parser:
After dealing with the payoff matrix, we can finally implement the methods getMinGameScore() and getMaxGameScore(). The minimum score is the smallest entry in the payoff matrix and the maximum score is the largest entry. We can use the ArrayMath utility class to find the minimum and maximum values in the payoff matrix. Moreover, we can optionally override the method getMonoGameScore(int type) to return the payoffs in a monomorphic population of type (e.g., A or B), which corresponds to the diagonal entries in the matrix payoffs:
together with the necessary import statement
The minimum and maximum scores are essential for converting fitness to probabilities in individual based simulations, and to rates in differential equation models. In addition, the range of scores is used to set the scale of fitness in several data views as well as to scale the colour gradient for individual fitness in the population.
| . | . | Note: The implementation of getMonoGameScore(int) is optional. Monomorphic scores are used as visual references in the GUI. | . |
For example, in the fitness view, the monomorphic scores are shown as horizontal lines, or used to mark bins in the fitness histogram, and to colour individuals that obtain the monomorphic scores in the structure view.
Optionally, we may implement the methods getPayoff(int me, int you) to return the payoff of an individual with trait me against an individual with trait you and similarly getPayoff(double payoff, int me, int you) to set the payoff to an individual with trait me against one with trait you to payoff. Those methods are available in the TBT module even though they are presently unused.
In order to run individual based simulations (IBS) we need to implement the interface HasIBS.DPairs, where DPairs indicates has discrete traits and deals with interactions in pairs. Thus, import the interface and implement it in the class definition:
This requires implementing two additional methods:
to calculate the scores of a focal individual with trait me against all other traits given the counts of each trait in the array traitCount. The method returns the accumulated score of the focal individual. The payoffs that each trait scores against trait me are returned in the array traitScore.
to calculate the scores of each individual against all other individuals in the population. This only applies to unstructured (or well-mixed) populations where every individual may interact with every other. The state of the population is given by the counts of each trait in the array traitCount and the average scores of each trait are returned in the second array traitScore.
First, let VS Code create the stubs by hovering over the red squiggly line and selecting Quick fix… followed by Add unimplemented methods. After moving the methods to the middle of the class (after the getMonoPayoff(int) method), we have:
The field me in the method pairScores(int me, int[] traitCount, double[] traitScore) denotes the index of the focal individual’s trait. Thus, to determine the payoffs of other traits against me, we simply need to retrieve the corresponding entries in the payoff matrix payoffs. Similarly, the payoff to me is the accumulated score of all interactions against each trait with counts given in traitCount.
Implementing the mixedScores(...) method is similarly straight forward to calculate the average payoff in a well-mixed population, where every individual interacts with everyone else, with payoffs returned in traitScore.
| . | . | Important: Individuals do not interact with themselves. Hence, individuals with trait see one less individual with trait among their peers than individuals with trait , and vice versa. | . |
This completes the first stage of our new module, and we are ready to run our first simulation!
Reload the browser window to recompile the new code and load the TxT module. The error message ERROR: No model found! is gone and replaced by the more encouraging message:
Next is to visualize the state of the simulation.
Currently, the only information that our TxT module provides on the state of the individual based simulation is a console displaying the state of the module and model as well as to log errors, warnings and miscellaneous information about the state of the module. Adding more sophisticated views to visualize the state of the population is often as simple as implementing a particular interface.
In order to add a 2D visualization of the structure of the population as well as the traits of all individuals, TxT merely needs to implement the interface HasPop2D.Traits:
with
No further actions are required! Now try out the new view by reloading the browser window. The colours of the nodes indicates the trait (or strategy) of the individual. Alternatively, the colours could indicate the fitness of each individual. This is easily achieved by implementing another interface: HasPop2D.Fitness. After reloading the browser window there are now three views available: the Console, the view of the traits, Traits – 2D Structure and now the view of the fitness, Fitness – 2D Structure.
With little more additional effort views of the population structure in 3D can be added as well. Simply implement the interfaces HasPop3D.Traits and HasPop3D.Fitness in the TxT class. Exploring the network structures in 3D is visually interesting and a particularly pleasing extension for the nerdy folks out there that still have a pair of cyan-magenta 3D glasses lying around or, even better, a Google cardboard VR viewer for your phone. Check out the options in the context menu. Don’t be shy and give it a try!
This brings the number of visualizations available in the TxT module to a total of four views(plus the console), each triggered by implementing the corresponding interface:
with
The fitness and trait views, each in 2D and 3D, are the most important ones to visualize the structure of the population as well as its current state. In addition, the trait views also allow to interactively manipulate the current state (change traits through double-clicks or double taps). A detailed description of all the ways you can interact with the different kinds of views is given in section 11.1.2: Views, including the 2D Structure and 3D Structure views.
Consider a population of and types on a random graph with nodes and neighbours, on average. The two types engage in a donation game (c.f. section 4.7.4: Donation game) with cost-to-benefit ratio , which results in the rescaled payoff matrix
| (13.1) |
where denotes cooperation and defection. Plot the population structure in 2D. This is the first in a series of exercises to explore the -rule in structured populations (Ohtsuki et al., 2006).
| . | . | Note: Click Settings to open a text field where the command line options can be entered. Activate the new settings by clicking Apply. A brief description of all available options is available by clicking Help or consulting section 11.2: Parameters. | . |
Note, the details of your population structure and distribution of traits looks almost certainly different due to the stochastic element of the structure generation and trait distribution. In order to generate identical results, set the seed of the random number generator by adding --seed 0 to the command line options.
Traits – 2D Structure
While information on the current state and structure of the population is important, interesting and illuminating, it’s the changes over time that rank among the most relevant and informative aspects of the simulated evolutionary process. Luckily, all that is needed to visualize the mean frequency and the mean payoff of each trait over time is to implement the interfaces HasMean.Traits and HasMean.Fitness in the TxT class. This is done by adding the following lines to the class definition:
with
The two additional views, Traits – Mean and Fitness – Mean, are available in the GUI to visualize the mean frequency and the mean fitness of each trait as well as the mean fitness of the population over time.
Continuing with Exercise 13.1, draw the frequency of and types for Moran death-birth updating over time until one type goes extinct. Use a convex payoff-to-fitness mapping for the fitness of an individual with trait and payoff , where denotes the baseline fitness, and the selection strength.
Hint
Note, your trajectory looks almost certainly different due to the stochastic nature of the evolutionary process. In order to generate identical results, set the seed of the random number generator by adding --seed 0 to the command line options.
Traits – Mean
Solution
Set command line options to: --module TxT --popsize 200 --geometry R4 --popupdate dB --init frequency 2,8 --paymatrix 1,0;1.1,0.1 --fitnessmap convex 1,0.01 --view 3 and click Apply followed by Start.
.
.
Note: Alternatively, add the option --run to the command line to immediately launch the simulations. Instead of setting --view 3 you can also switch manually to the Traits – Mean view.
.
Note that the protection against exploitation provided by the spatial structure enables cooperators ( types) to reach fixation most of the time. However, because selection is fairly weak the process is fairly noisy with only a small bias in favour of cooperation based on the limited local interactions on the graph.
The mean fitness values are sufficient to describe the dynamics in unstructured (or well-mixed) populations. However, in structured populations the distribution of fitness provides additional insights into the evolutionary dynamics. To visualize the distribution of fitness values for each trait, a histogram of the respective fitness distributions is available when implementing the interface HasHistogram.Fitness:
with
In each histogram the marked bin corresponds to the fitness of a population that is monomorphic in the respective trait.
Continuing with Exercise 13.2, reset the simulation and draw the fitness distribution after ten generations.
Hint
Note, your distribution looks likely different due to the stochastic nature of the evolutionary process. In order to generate identical results, set the seed of the random number generator by adding --seed 0 to the command line options.
Histogram - Fitness
Solution
Set command line options to: --module TxT --popsize 200 --geometry R4 --popupdate dB --init frequency 2,8 --paymatrix 1,0;1.1,0.1 --fitnessmap convex 1,0.01 --view 7 and click Apply followed by Start. Quickly click Stop to halt the simulations again.
.
.
Note: Add the option --run on the command line to immediately launch the simulation. Alternatively, you can simply click Step ten times. Moreover, the simulations can be stopped automatically after ten generations by adding --generations 10 to the command line options. Finally, instead of setting --view 7 you can switch manually to the Histogram - Fitness view.
.
An interesting and relevant measure to characterize heterogeneous population structures is to consider the degree distribution of nodes in the population. The degree of a node is the number of its neighbours. In directed graphs the in-degree is the number of incoming edges and the out-degree is the number of outgoing edges. In undirected graphs the in- and out-degree are the same. Moreover, the structure of the interaction network may be different from the reproduction network.
The degree distribution depicts the proportion of nodes with a particular degree and visualizes the heterogeneity of the population structure. For example, in scale-free networks the degree distribution follows a power-law, which means that the probability of finding a node with a degree is proportional to , where is the power-law exponent. For random graphs the degree distribution is a binomial distribution, which is peaked around the average degree . Finally, for lattices (with periodic boundaries) or random regular graphs the degree distribution has a single peak at because all nodes have the same degree. In general, the degree distribution provides information about the heterogeneity of the population structure.
In order to visualize the degree distribution of the population structure, all that is needed is to implement the interface HasHistogram.Degree:
Extending on Exercise 13.1, plot the degree distribution of the population structure.
Hint
Note, your degree distribution looks likely slightly different because the generation of the network structure involves drawing links between randomly drawn nodes. In order to generate an identical structure, set the seed of the random number generator by adding --seed 0 to the command line options.
Histogram - Degree
Solution
Set command line options to: --module TxT --popsize 200 --geometry R4 --view 8 [--popupdate dB --init frequency 2,8 --paymatrix 1,0;1.1,0.1 --fitnessmap convex 1,0.01] and click Apply.
.
.
Note: The parameters in brackets are part of Exercise 13.1 and Exercise 13.2 but do not affect the degree distribution. Instead of setting --view 8 you can switch manually to the Histogram - Degree view.
.
The EvoLudo GUI supports two kinds of running modes: dynamics and statistics. The default is the dynamics mode, which simply reports the progress of the evolutionary process over time and is visualized in intervals set by the --timestep command line option. Statistics mode works slightly different in that samples are collected for statistical evaluations. At present two complementary sampling types are supported: fixation statistics and stationary distributions. For fixation statistics, each sample represents a run starting from a well-defined initial configuration that resulted in the fixation of one trait in the population. Naturally this requires that fixation is possible, that is, absorbing states must exist. Fixation is not possible, for example, in the presence of mutations. The initial configuration is typically a monomorphic resident population with a single mutant placed in a random location set by the --init command line option.
In contrast, stationary distributions are obtained by sampling the population at regular intervals, again set by the --timestep command line option, and recording the frequencies of each trait. The long-term average over the samples converges to the stationary distribution. This requires that no absorbing states exist, which can be easily achieved by adding small mutation rates (see --mutation option).
The statistical results are visualized as histograms that are continuously updated as more samples are collected. Neither statistics mode requires any particular consideration in the code of the module. Instead, all that is required is to implement the corresponding interfaces:
record the probability of fixation for each trait.
record the expected time until fixation for each trait plus the time to absorption, that is, the time until the population turns monomorphic, regardless of which trait fixated. For a detailed discussion about the connection between fixation probabilities as well as fixation and absorption times see sections 2.5 & 5.4.
record the probability to find the population in any particular state, specified by the number of mutants. This probability approximates the relative time spent in this particular state.
| . | . | Important: Whether and what kind of statistics modes are available in the GUI is not only determined by the interfaces implemented by a module but also by the command line options. For example, with mutations no absorbing states exist and hence fixation can never occur. Accordingly, only stationary distributions are available. Conversely, without mutations absorbing states exist and fixation is inevitable, eventually. Thus, fixation statistics are available provided that a compatible initial configuration is set. | . |
After this long introduction, the actual implementation of the statistics modes is as easy as adding another view to the GUI:
Building on Exercise 13.2, calculate the distribution of fixation probabilities for mutants arising in all nodes based on samples. Note that this requires changing the settings for the initial configuration to a single mutant in a resident population. Also, be aware that generating the samples may require a few minutes.
Hint
Note, your distribution looks likely different due to the stochastic nature of the evolutionary process. In order to generate identical results, set the seed of the random number generator by adding --seed 0 to the command line options.
Statistics - Fixation probabilities
Solution
Set command line options to: --module TxT --popsize 200 --geometry R4 --popupdate dB --init mutant 0,1 --paymatrix 1,0;1.1,0.1 --fitnessmap convex 1,0.01 --view 9 --samples 10000 and click Apply followed by Start.
.
.
Note: Compiling samples may take a few minutes. Instead of setting --view 9 you can switch manually to the Statistics - Fixation probabilities view.
.
While, overall, the results may not look in favour of cooperation, things need to be put into proper perspective. The average fixation probability of a single cooperator, , in a homogeneous defector population, , is approximately . In contrast, the fixation probability of a single trait under neutral selection is and hence for a population of size amounts to . Indeed, in comparison, the spatial structure promotes cooperation. Moreover, keep in mind that we are considering weak selection with small fitness differences and hence the results are expected to be close to the neutral case. Of course, to gain more confidence in the significance of this difference we would need to increase the sample size to or more. Nevertheless, the finding is in line with the -rule reported in Ohtsuki et al. (2006). More specifically, in our case, the cost-to-benefit ratio is and the (average) number of neighbours is and hence the rule applies.
| . | . | Note: The statistical data of the simulation runs can be exported in CSV format by choosing Export… and Statistics (csv) from the context menu of any statistics view. | . |
The histogram for the fixation probabilities shows significant variation, which has two independent sources: first, the location of the initial mutant on the regular graph and second the structure of the random graph itself. In heterogeneous graphs the fixation probability sensitively depends on the location of the initial mutant as well as the overall structure of the graph. In Exercise 13.5 the random graph was regenerated for each sample before placing the mutant uniformly at random. As a consequence the distribution of fixation probabilities across nodes should eventually converge to a constant for a sufficiently large number of samples.
Repeat the simulations in Exercise 13.5, but this time use a single graph for all samples. Now any systematic variability in the fixation probabilities is due to the structural features of the regular graph.
Hint
Note, your distribution looks likely different due to the stochastic nature of the evolutionary process. In order to generate identical results, set the seed of the random number generator by adding --seed 0 to the command line options.
Statistics - Fixation probabilities
Solution
Set command line options to: --module TxT --popsize 200 --geometry R4 --popupdate dB --init mutant 0,1 --paymatrix 1,0;1.1,0.1 --fitnessmap convex 1,0.01 --samples 10000 --statistics reset 0 --view 9 and click Apply followed by Start.
Run the simulations in Exercise 13.6 but make sure to include the --seed 0 option to guarantee reproducible results. Moreover, in order to reduce the noise in the evolutionary process, increase the selection strength to the maximum of . This is the maximum because the minimum payoff is zero. Note that the fixation probability sensitively depends on the location of the initial mutant. In particular, with the above settings it turns out that mutants arising in nodes with indices have all remarkable fixation probabilities of or higher, with a maximum of for node .
| . | . | Note: Hover with the mouse over the bars (or tap on a bar) to view a tooltip with more information | . |
Keep in mind that the statistical power remains limited because even for samples this leaves only an average of samples per node. Nevertheless, this should be enough to identify relevant patterns and mechanisms.
Now turn to the Traits – 2D Structure view and try to locate these nodes. Even more importantly, figure out why they are special and what their characteristics are. What are your observations? How could those features help to promote cooperation?
Hint
The random regular graph used for the fixation probability histogram in Exercise 13.6. Because of the --seed 0 option your graph should look identical. Can you identify which locations might be conducive to the fixation of cooperators and why?
Traits – 2D Structure
Solution
The key determining feature is that all nodes resulting in high fixation probabilities have (at least) one neighbour which is a leaf node (i.e. has no further neighbours). The leaf node provides a safeguard against accidental extinction of the mutant. At the same time, leaf nodes themselves are too isolated and the mutant unlikely to spread. The neighbours of leaf nodes strike a balance between protection from extinction through their leaf neighbours and opportunities for proliferating.
An alternate and complementing view on cooperation in structured populations is to consider the stationary distribution of the trait frequencies. Stationary distributions require an ergodic system, which means no absorbing states. In the presence of absorbing states the stationary distribution is either trivial or depends on the initial configuration. The Moran process is easily turned into an ergodic process by simply adding a small mutation rate, say . This translates into, on average, one mutation every five generations in a population of size . In addition, change the initial condition to start with a monomorphic defector population, types. Derive the stationary distribution after generations (samples).
Hint
Note, your distribution may look slightly different due to the stochastic nature of the evolutionary process, but the differences should be essentially imperceptible. In order to generate identical results, set the seed of the random number generator by adding --seed 0 to the command line options.
Statistics - Stationary distribution
Solution
Set the command line options to: --module TxT --popsize 200 --geometry R4 --popupdate dB --init mono 1 --mutation 0.001 --paymatrix 1,0;1.1,0.1 --fitnessmap convex 1,0.01 --timestop 10000 and click Apply followed by Start
.
.
Note: In order to collect the samples at maximum speed, move the slider to the far right or set --delay 0.
.
This section covers several, more advanced topics, which go beyond the basic functionalities of an EvoLudo module but serve to illustrate the flexibility and extensibility of the EvoLudo framework.
As illustrated at the very beginning of this book (see section 1.2: Gallery) some types of interactions spatially structured populations give rise to intriguing spatio-temporal patterns (Nowak and May, 1992). In particular, given the appropriate settings, games exhibit fascinating evolutionary kaleidoscopes, or patterns that are reminiscent of fractal structures or Persian carpets and even change dynamically over time (see e.g. Lab. 1.1).
In order to create evolutionary kaleidoscopes very specific settings for the simulations are required. Most importantly, the population must be structured, the updating of the population must be deterministic and requires very particular initial configurations that are symmetric. Structured populations ensure that different individuals experience distinct environments, that is, have sets of neighbours with different traits. The simplest case are square lattices with von Neumann, , or Moore, , neighbourhoods. Deterministic updates are achieved if:
the population is updated synchronously, that is, all individuals update their trait at the same time;
every individual interacts with all its neighbours;
every individual adopts the trait that performs best among all its neighbours, including itself.
For any symmetrical initial configuration the deterministic updating guarantees that the trait distribution in the population remains symmetrical at all times. The simplest case is given by a monomorphic population with a single placed at the centre.
Finally, kaleidoscopes only emerge for special types of interactions, that is particular payoff matrices. The most prominent example is the so-called weak prisoner’s dilemma (see section 6.1.1: Evolutionary Kaleidoscopes for more details) with a payoff matrix given by:
| (13.2) |
such that a player is indifferent between traits and against a co-player opting for . For the trait (or strategy) is weakly dominant and denotes the temptation to defect.
On square lattices with Moore neighbourhood evolutionary kaleidoscopes arise for , while those with von Neumann neighbourhood they emerge for (Nowak and May, 1992).
Given this long list of requirements it bears the question of why bother with evolutionary kaleidoscopes at all? The answer is simple: they may not carry any biological relevance, but they are fascinating and prime examples of the beauty in mathematics.
Assuming that the reader agrees with this assessment, let’s add the necessary code to allow for customized initial configurations. This requires a deeper access to the inner workings of individual based simulation models.
In order to get started, let us create our own implementation of an individual based simulation. Subclassing IBSDPopulation as an inner class of TxT will do the trick.
| . | . | Note: Using an inner class has the advantage that it maintains access to all fields and methods of the outer class TxT without the need to pass them as parameters. | . |
Add the following code to the end of the TxT class:
with
This triggers an error because the IBSDPopulation does not provide a default constructor, i.e. one without arguments. Thus, we need to explicitly implement one, which refers to the constructor IBSDPopulation(EvoLudo engine, Discrete module). Let VS Code create the constructor stub by hovering over the error, marked by a red squiggly line, and select Quick fix… followed by Add constructor ’IBSPop(EvoLudo engine, Discrete module)’ to obtain:
| . | . | Note: Because we are using an inner class, the constructor of IBSPop can be simplified to only take the TxT module as a parameter. The field engine is known by the outer class TxT and hence can be accessed directly using TxT.this.engine. this by itself refers to the current instance of IBSPop. Thus, in order to access the instance of the outer class we need to use TxT.this. | . |
Our simplified constructor now reads:
As the name of the super class suggests (reading from the back), it provides the framework to model a Population of individuals with Discrete traits through individual based simulations, IBS.
Now we are ready to link our newly minted subclass into the TxT module. The next step is to replace the default EvoLudo implementation for individual based simulation with our own. This is done by overriding the method createPopulation() in the TxT class and return our own implementation:
It is easy to check that our subclass gets indeed loaded by setting a break point on the constructor. After re-loading your browser window execution will stop at this line. Remove the break point again and click continue to proceed.
Before linking our newly minted subclass into the TxT module, let’s add a method to set the initial configuration of the population. For evolutionary kaleidoscopes some groundwork is already implemented, and we only need to implement the method initKaleidoscope(). This method should first check whether the population has an appropriate lattice structure and report a warning if not. On any lattice it then places a single mutant in the centre of a monomorphic population:
| . | . | Note: In principle, the method initKaleidoscope() can be used to provide any kind of special initial configuration – regardless of whether they are capable of producing evolutionary kaleidoscopes. | . |
The last step is to modify the --init command line option to include a key that triggers the initial configuration for kaleidoscopes. This is done by adjusting the command line options by overriding the adjustCLO(CLOParser) method in the TxT class:
with
This adds the key kaleidoscope to the --init command line option. Reload your browser window to recompile the module and click Help to verify the addition of the new key.
Create your first evolutionary kaleidoscope with EvoLudo on a square lattice with Moore neighbourhood.
Hint
Note, the snapshot of the kaleidoscope is taken after generations.
Traits – 2D Structure
Solution
Set the command line options to: --module TxT --popsize 101x101 --geometry m --popupdate sync --playerupdate best --interactions all --references all --init kaleidoscope --paymatrix 1,0;1.65,0 and click Apply.
On a square lattice every node has four nearest neighbours (to the north, east, south and west), the von Neumann neighbourhood, and also four second-nearest neighbours (on either side along the two diagonals). Combining first- and second-nearest neighbours results in the Moore neighbourhood, which includes all eight neighbours reachable by a chess-kings move, see Fig. 13.2.

Interestingly, however, the second neighbours form two disjoint sub-lattices, which can give rise to intriguing dynamical features. More specifically, in the spatial donation game where individuals interact with their four nearest neighbours but compete with, or imitate, their four second-nearest neighbours, spontaneous symmetry breaking may occur below a critical cost-to-benefit ratio . As a result, cooperation is more abundant in one sub-lattice than in the other. Rather surprisingly, this asymmetry increases with decreasing – even though conditions for cooperation become less challenging (smaller costs or increased benefits) – and eventually leads to complete asymmetry with the fixation of one trait in each sub-lattice. This is a neat example of a critical phase transition in evolutionary game theory (Szabó and Hauert, 2002, Hauert and Szabó, 2024, 2025).
The easy part is implementing the geometry with second neighbours. This is already accomplished in the Geometry class (see SQUARE_NEUMANN_2ND geometry). Similarly, a customized graphical visualization is available, which reveals the two sub-lattices. We will explore this further once the trickier part is dealt with for dealing with the two sub-lattices when reporting the state of the population. In principle, simulations using second neighbour lattices for interaction or competition structures work out of the box. However, without proper reporting it would be hard if not impossible to quantify the dynamics in each sub-lattice, in general, and the asymmetry between sub-lattices, in particular. For this reason the SQUARE_NEUMANN_2ND geometry is disabled by default.
The goal of this section is to enable the SQUARE_NEUMANN_2ND geometry and to report the state of each sub-lattice separately in terms of traits as well as fitness. First off, to enable the SQUARE_NEUMANN_2ND geometry, we simply need to add the corresponding key to the parser for the command line option --geometry for IBS models. If you skipped the previous section on evolutionary kaleidoscopes, you will need to add the following code:
with
Otherwise, simply insert the line
after clo.addKey(Init.Type.KALEIDOSCOPE) in the adjustCLO(CLOParser). This will make the SQUARE_NEUMANN_2ND geometry available in the GUI, which can be verified by reloading the browser window, clicking on Help and checking the list of available geometries.
Now, let us define different names for the traits and in each sub-lattice, say and , respectively. This requires two changes: First we need to extend the method getTraitName(int) in our TxT module for the SQUARE_NEUMANN_2ND geometry to return modified trait names for one sub-lattice and add two additional trait names (with indices and ) for the corresponding traits in the other sub-lattice:
| . | . | Note: HTML formatting is acceptable for most strings, including trait names. Exceptions are the keys and descriptions of the command line options, which must be plain text. | . |
Second, when reporting the trait names, the tooltips of the 2D and 3D lattice views need to take the sub-lattice into account on which the individual resides in the SQUARE_NEUMANN_2ND geometry. If you skipped the previous section on evolutionary kaleidoscopes, you will need to first create a custom IBS population as described in the section section 13.8.1: Customizing IBS populations above. The inner class TxT.IBSPop then needs to override the method getTraitNameAt(int):
Now is a good time to check our progress. Reload your browser window and click on Help to verify that the SQUARE_NEUMANN_2ND geometry is available, enter the settings --module TxT --geominter n --geomcomp n2 and click Apply to check the visualization of the geometry and the tooltips. In the 2D the nodes in one sub-lattice are represented by squares and those in the other by disks. In 3D the cubes representing the nodes of the sub-lattices are vertically displaced such that when looking at the structure at a low angle only one or the other sub-lattice is visible.
Next, let’s update the status bar to report the number of individuals in each sub-lattice. In preparation, we need to override the method getMeanTraits(double[]) and calculate the mean frequency of traits in each sub-lattice separately. Since this procedure requires additional CPU resources and the mean frequencies are also used by the Traits – Mean let us store the results in a local field, together with the time stamp of the last update. This allows us to only update the values if the time stamp is outdated. This is done by adding the following code to the TxT.IBSPop class:
Of course this also requires defining the local double array mean2nd to store the mean frequencies and the double tsMean to save the time stamp of the last calculation:
Their initialization is done by overriding the check() method, which is called after the initial loading of the module or after clicking Init, Reset, or Apply:
Now we are ready to show the information in the status bar. Override the method getStatus():
with
Verify that the status bar works by reloading your browser window. Make sure that the settings include --module TxT --geominter n --geomcomp n2. Click Step to update the status information.
With these changes in place we are almost ready to also visualize the frequencies of the traits in each sub-lattice over time in the Traits – Mean view. All that is needed is to advertise that our simulation, the TxT.IBSPop class, provides four mean values instead of the default two in case of the SQUARE_NEUMANN_2ND geometry:
As you can easily verify, the Traits – Mean view now indeed reports four mean values for the frequencies of and traits in each sub-lattice. However, you will also notice that now the colours of the lines are off. It would be nicer to use paler versions of the and colours to distinguish their frequencies in the two sub-lattices. In order to do this, override the method getMeanColor(int) in the TxT module and simply add some transparency to the existing trait colours:
with
Note, at this point our view Fitness – Mean is slightly messed up because, based on the return value of getNMean(), it now expects four mean values instead of two (plus the overall mean fitness). This is addressed by overriding the method for the fitness values, getMeanFitness(double[]). Similar to getMeanTraits(double[]) we calculate the mean fitness for and types in the two sub-lattices separately.
As before, this requires introducing two new fields, the time stamp for fitness calculations, tsFit, and the double array fit2nd to store the fitness,
as well as initializing them in the check() method:
with the additions highlighted in red.
Explore the spatial dynamics of cooperation in the two sub-lattices in a population of size . The population is updated asynchronously and individuals compete with a single, randomly selected neighbour among the four along the diagonals. The focal individual updates its trait following a probabilistic comparison of their payoff to that of the competitor according to the Fermi update with a noise term of . The payoffs are accumulated over interactions with all four nearest neighbours from donation games. All payoffs are ephemeral and solely used for the probabilistic comparison and discarded afterwards.
Fairly long relaxation times are required to reach the equilibrium configuration. Produce a snapshot of a typical equilibrium configuration for a cost-to-benefit ratio of after generations with symmetrical levels of cooperation in the two sub-lattices.
Hint
For the symmetrical setup, the equilibrium level of cooperation is around in each sub-lattice.
Traits – 2D Structure
Solution
Set the command line options to: --module TxT --timestep 1 --geominter n --geomcomp n2 --popsize 200x --popupdate async --interactions all --references random 1 --playerupdate thermal 0.5 --init frequency 1,1 --paymatrix 0.965,-0.035;1,0 --accuscores --resetscores ephemeral --timestop 10000 and click Apply. The same distribution as shown above can be replicated by adding the option --seed 0.
Explore the dynamics of cooperation in the two sub-lattices for the donation game with cost-to-benefit ratio . Use otherwise the same settings as in Exercise 13.10 to produce a snapshot of a typical equilibrium configuration with highly asymmetrical levels of cooperation in the two sub-lattices after generations.
Hint
For the asymmetrical setup, the equilibrium level of cooperation is around in one sub-lattice and around in the other. Thus, the trait frequencies are largely complementary between lattices.
Traits – 2D Structure
Solution
Set the command line options to: --module TxT --timestep 1 --geominter n --geomcomp n2 --popsize 200x --popupdate async --interactions all --references random 1 --playerupdate thermal 0.5 --init frequency 1,1 --paymatrix 0.995,-0.005;1,0 --accuscores --resetscores ephemeral --timestop 10000 and click Apply. The same distribution as shown above can be replicated by adding the option --seed 0.
The traditional way to investigate games is based on the replicator dynamics (Hofbauer and Sigmund, 1998). In order to do this in EvoLudo, we first need to implement the interface HasDE.DPairs, to advertise that we intend to offer a model based on differential equations (the HasDE interface) for discrete traits and interactions in pairs (the DPairs interface).
with
This advertises that our module can handle ordinary differential equations, which requires the implementation of two methods: getDependent() and avgScores(double[] density, double[] avgscores) in our TxT class. Again, you may let VS Code create the method stubs by hovering over the error, marked by a red squiggly line, and select Quick fix… followed by Add unimplemented methods to obtain (after moving the stubs to more appropriate locations):
and
The second method calculates the average payoffs from pairwise interactions and returns them in the array avgscores for each trait given their frequencies in the state array:
The state of the population is fully determined by the frequencies of the two traits, and . More specifically this means that none of the entries in the state array are negative, and they all must sum up to one. Thus, the state space corresponds to a simplex, in the present case, and the number of dynamical variables is reduced by one. This is where the first method getDependent() comes in and is used to identify the dependent trait. Let’s declare as the dependent trait, and the independent one:
| . | . | Note: The method getDependent() helps reduce numerical rounding errors by ensuring that frequencies always lie in and sum up to one, i.e. remain confined to the simplex. This results in computational redundancy that can be exploited to reduce numerical rounding errors and improve accuracy. | . |
In contrast, models based on densities have no dependent trait and thus numerical precautions are limited to enforcing non-negative values. In those cases the method getDependent() returns to indicate that there is no dependent trait.
Now we have finished the groundwork for implementing differential equations and are ready to specify the kinds of ordinary differential equation models that our model should support. EvoLudo supports two kinds of ODE models, again in the form of interfaces:
Simple integrator based on the Euler method.
More advanced fifth-order Runge-Kutta method with variable step size.
There is no reason not to support both methods, especially because all we need to do is to implement the corresponding interfaces:
| . | . | Note: The models are selected by adding the option --model EM or --model RK5 to the command line, respectively. In addition, the generic option --model ODE is also available, which defaults to RK5, if supported, and EM otherwise. | . |
Now we are almost done with the implementation of the ODE model. If you skipped section 13.8: Advanced topics you are good to go and may continue with the exercise. However, in case you did complete the advanced section, there is one important detail to consider:
| . | . | Attention: Two of the methods that were added to the TxT class in the advanced section 13.8.2: Adding square lattice geometry with second neighbours, require further considerations for ODE models. More specifically, getTraitName(int idx) and getMeanColors(), implicitly assume that the competition geometry exists, i.e. is not null. While this is the case for IBS models, it does not apply to ODE models (as well as stochastic, SDE, and partial differential equations, PDE, models, see below). | . |
In order to prevent access to an undefined field, we need to take suitable precautions by either checking that the field competition is not null, or even better, check the type of the current model:
and
These last considerations complete the implementation of the ODE model for games.
Plot the demise of cooperators ( types) in the donation game with a cost-to-benefit ratio of in a population with initially ’s using the ODE model and a time increment of generations.
Hint
Note, after generations all cooperators have disappeared and their frequencies converged to zero. Of course, in principle, extinction would take forever with infinite precision. However, given the deterministic nature of this model you should observe the exact same outcome.
Traits – Mean
Solution
Set the command line options to: --module TxT --model ODE --timestep 2 --init frequency 0.999,0.001 --paymatrix 1,0;1.1,0.1 and click Apply.
| . | . | Note: Not all the visualizations that we implemented before are available for the ODE model. In particular, when loading the ODE model with the options --model ODE, --model RK5, or --model EM only Traits – Mean, Fitness – Mean and Histogram – Fitness and, of course, the Console log are available. | . |
Stochastic differential equations, SDE’s, establish an insightful link between the stochastic processes in individual based simulations, IBS, and the deterministic dynamics of ordinary differential equations, ODE’s. For increasing population sizes the effects of demographic fluctuations gradually decrease and eventually disappear in the deterministic limit of infinite populations (see section 2.8: From finite to infinite populations & section 5.5: From finite to infinite populations). At the same time, running IBS models becomes increasingly expensive for increasing population sizes in terms of CPU time. For large but finite populations SDE’s provide an effective and numerically efficient alternative to IBS models. In particular, the performance of SDE’s remains unaffected by the population size and hence the computational cost is constant.
The implementation of SDE’s in EvoLudo is based on the Euler-Maruyama method, which is a simple and efficient numerical scheme to solve SDE’s. The Euler-Maruyama method is a generalization of the Euler method for ordinary differential equations to stochastic differential equations and simply adds a stochastic term arising from demographic fluctuations in finite populations.
In particular, more sophisticated ODE solvers, like Runge-Kutta methods (XXX), may, for example, adjust the increments of the independent variable to allow for bigger steps if changes are small. Such optimizations are often based on evaluating the rates of change at different points in time. While this is not a problem for a deterministic system, it is not possible for SDE’s because the stochastic term is not replicable and not known in advance.
In order to add the SDE model to our EvoLudo module, all we need to do is implement another interface, HasSDE.DPairs:
| . | . | Important: Implementing the interface HasDE.SDE also requires the implementation of either HasDE.DPairs or HasDE.DGroups for pairwise or group interactions, respectively. This requirement can only be enforced at run time without overcomplicating the interface structure of EvoLudo. | . |
Since we have already implemented HasDE.DPairs in the previous section (see section 13.9: Adding ODE model: ordinary differential equations), nothing else is required. However, in case you skipped the ODE section, you need to go back now before continuing and taking the SDE model for a test drive with the exercise.
| . | . | Note: In contrast to the ODE model, declaring a dependent trait is now essential. If no dependent trait is declared the demographic noise may result in invalid states outside the simplex! | . |
Investigate the dynamics of cooperation in the snowdrift game with a cost-to-benefit ratio of using the payoff matrix
| (13.3) |
Calculate the equilibrium frequency of types analytically.
Solution
Solve for to obtain or for .
Determine the stationary distribution after generations in a population of size with initially types using an SDE model and the default time increment of generation.
Hint
The stationary distribution is roughly a normal distribution with mean around types and types.
Statistics – Stationary distribution
Solution
Set the command line options to: --module TxT --view 4 --model SDE --timestep 1 --init frequency 0.5,0.5 --paymatrix 0.75,0.5;1,0 --mutation 0.001 --timestop 10000 and click Apply and Start.
For comparison, repeat the simulation with an IBS model and note the difference in speed.
Solution
Set the command line options to: --module TxT --view Statistics_-_Stationary_distribution --model IBS --timestep 1 --init frequency 0.5,0.5 --paymatrix 0.75,0.5;1,0 --mutation 0.001 --timestop 10000 and click Apply and Start.
Partial differential equations, PDE, provide an alternative to individual based simulations, IBS, for exploring the effect of spatial extensions on the evolutionary process. However, in contrast to the explicit modelling of population structures in IBS models, PDE models track the frequency distribution of traits in continuous space. The implementation of PDE models in EvoLudo is based on discretizing the spatial domain into a grid of discrete elements. Each element is updated following a reaction-diffusion process, or, in an evolutionary context maybe better termed a selection-diffusion process. Trait frequencies in each element change following a locally well-mixed dynamic in the selection step. During the diffusion step traits then diffuse between neighbouring elements depending on the differences in trait frequencies between adjacent elements as well as the diffusivity of each trait. Effectively this transforms the PDE into a system of coupled ordinary differential equations, where the coupling arises from the diffusion between the discrete elements.
EvoLudo supports two kinds of PDE models, again in the form of interfaces:
reaction-diffusion model.
reaction-diffusion-advection model.
Just as for ODE and SDE models, both interfaces additionally require implementing either the HasDE.DPairs or HasDE.DGroups interface for pairwise or group interactions, respectively, which we completed already (see section 13.9: Adding ODE model: ordinary differential equations). Thus, the PDE models are simply added by implementing the two interfaces:
| . | . | Note: The models are selected by adding the option --model PDERD or --model PDEADV to the command line, respectively. In addition, the generic option --model PDE is also available, which defaults to PDERD. | . |
Unfortunately, for games the results based on PDE models are a bit underwhelming. At first glance this may come as a surprise given that spatial structures are instrumental to enable cooperators to persist in the donation game (see e.g. Exercise 13.1 or Exercise 13.5) and can develop rich and complex dynamics as exemplified by evolutionary kaleidoscopes. Diffusion results in non-vanishing frequencies of both traits everywhere. As a consequence the spatial dimensions have only a transient impact on the dynamics. In the long term, the outcome is the same as in the well-mixed case. More specifically, in the donation game the spatial dimensions fail to protect cooperators from exploitation, because in continuous space diffusion undermines clustering of cooperators to offset losses against defectors. In analytical terms, the PDE for the donation game is
| (13.4) |
where is the frequency of cooperators at time and location . The corresponding fitness of cooperators is and denotes the average fitness in the population. The diffusion constant specifies the migration rate of cooperators and the diffusion operator. Finally, denotes the cost of cooperation. For the only steady state solution is , which is globally stable.
Returning to Exercise 13.1, draw the frequency distribution of and types using a PDE model for a homogeneous initial population of ’s with a single perturbation of only ’s in the centre element of the default elements. At what time does the model stop for the donation game with a cost-to-benefit ratio of and Fermi updating with noise ?
Hint
After generations the PDE integration stops because cooperators have effectively disappeared and their frequencies converged to zero. Of course, in principle, extinction takes forever with infinite precision. However, given the deterministic nature of this model you should observe the exact same outcome.
Traits – 2D Structure
.
.
Note: Even though the frequency distribution as shown does not appear to be homogeneous, it is actually easy to verify that the frequencies of ’s are zero everywhere by hovering over the elements in EvoLudo. It only appears as if the density of cooperators remains high in the corners because the colour gradient is automatically adjusted such that the lowest frequency of cooperators is red and the highest is blue. This helps to visually emphasize and amplify spatial patterns. However, in the present case this ‘feature’ backfires because the actual differences are negligibly small and controlled by the numerical precision (see option --accuracy).
.
Solution
Set command line options to: --module TxT --model PDE --timestep 1 --init perturbation 1.0,0.0 --paymatrix 1,0;1.1,0.1 --playerupdate thermal 0.1 and click Apply followed by Start.
.
.
Note: Alternatively, add the option --run to the command line to immediately launch the simulations.
.
One possibility to avert this outcome, but keep the continuous spatial dimensions, is to explicitly consider the population dynamics and independently model the cooperator and defector densities rather than just their frequencies. Even though this may seem like an innocent change, the fact that birth and death events need to be explicitly modelled to allow for variable population densities requires a fundamentally different approach and a new module. Maybe an idea to write your own EvoLudo module?
Without any further modifications the newly completed TxT module is now also available to be run from the command line as a classical Java application. For an introduction on running simulations from the command line see section 12.2.2: Command line. This avoids any security restrictions imposed on JavaScript running in a browser. Moreover, the command line allows running multiple simulations in parallel and automating the process of data collection and analysis.
For a test drive, first quit the Google Chrome browser window. This also ends the GWT EvoLudo (localhost) debug target in VS Code. Now switch to the terminal and execute ’mvn clean install’ in the top EvoLudo directory. This compiles the Java code and creates a self-contained, executable jar-archive in the ./EvoLudoJRE/target directory. It can be copied to any location and executed with ’java -jar EvoLudo.<version>.jar --module TxT --help’, which loads the TxT module and displays all the available options.
The most important, but rather technical, difference between running JavaScript simulations in the browser and a java application in the terminal, is that JavaScript is single threaded, whereas java provides a multithreaded environment. For example, in order to maintain a responsive user interface, JavaScript requires elaborate scheduling of short running tasks to prevent the browser from freezing. In contrast, in java the user interface and the simulations run in separate threads.
| . | . | Important: The semicolon, ’;’, used to separate rows of the payoff matrix, for example, can cause troubles on the command line because it is a special character and prematurely ends the option string, which results parsing failures. Either the ’;’ must be escaped with a backslash, ’\;’, or the entire option string needs to be protected by enclosing it in quotes, ". | . |
The option --data is only available when running simulations from the command line. It requests and collects specific data from simulations. For a list of all available data types see section 11.2.5: Simulation options. If present, the jar-archive runs in headless mode (without a GUI) and returns the requested data in a structured format. This is particularly useful for running extended simulations or batch processing for further data analysis.
Repeat Exercise 13.5 but now using the jar-archive and the --data option to obtain the fixation probabilities.
| . | . | Note: Don’t forget to remove the --view option. This triggers launching the java GUI instead of running the simulations on the command line. | . |
Compare the results to the histogram obtained in Exercise 13.5. When using the --seed 0 option in both cases the JavaScript and java results are identical!
Solution
Run the command: ’java -jar EvoLudo.<version>.jar --module TxT --popsize 200 --geometry R4 --popupdate dB --init mutant 0,1 --paymatrix 1,0;1.1,0.1 --fitnessmap convex 1,0.01 --samples 10000 --data statprob’.
Another option that is only available when running simulations from the command line is --export <filename>. Once the simulation finishes, it writes the final configuration to a file named <filename>. The exported configuration is stored as a PLIST file, which is a simple, human-readable, and machine-parsable format that can be easily imported into the EvoLudo GUI for visualization or used to restore the configuration using the --restore <filename> option and resume execution, see section 11.1.6: Export & Restore.
The extinction of cooperators on lattices exhibits a critical phase transition, see section LABEL:sect:XXX: LABEL:sect:XXX. This requires long relaxation times for the population to reach its dynamical equilibrium, where cooperators form isolated clusters to reduce exploitation by defectors. Consider the following string of options:
--module TxT --accuscores --geometry n --init frequency 8,2
--interactions all --paymatrix 1,0;1.037,0.037
--playerupdate thermal 0.4 --popsize 240x --popupdate async
--references random 1 --resetscores ephemeral --timestop 10000
--seed 0 --export dgr037K4.plist
What scenario does this describe?
Solution
The spatial donation game with a cost-to-benefit ratio of on a square lattice with von Neumann neighbourhood and asynchronous updating. Each individual accumulates its payoff from interactions with all four nearest neighbours and probabilistically compares its payoff to a randomly chosen neighbour according to the Fermi update with a noise term of . The payoffs are ephemeral and solely used for the probabilistic comparison and discarded afterwards. The simulation stops after generations and the final configuration is written to a file named dgr037K4.plist.
Use the EvoLudo.<version>.jar to run the simulation in Exercise 13.18 in java and generate a PLIST-file describing the relaxed configuration. Now visualize the configuration in the browser by simply dragging the generated PLIST-file onto the EvoLudo GUI in a browser window.
| . | . | Note: To resume the simulation in the browser, simply click Start. For comparison, it is also possible to click Reset and redo the simulations in the browser. If the option string includes --seed 0 the results are identical. | . |
Including --seed 0 results in the exact same spatial configuration as shown here.
Traits – 2D Structure
Relaxing the spatial configuration in Exercise 13.18 requires some time to finish, but that was only the preparation for the actual measurements, such as determining the size of fluctuations in equilibrium. Typically, this requires even longer execution times. All together this provides opportunities for something to go wrong – whether a power failure, a system crash, or simply killing the running process by mistake, for example installing a software update that requires rebooting the system. In an attempt to minimize the risk of data loss, all running simulations try to save their current state to a file named panic-t<time>-<date>.plist, where <time> is replaced by the current simulation time of the running model and <date> indicates the date and time when the crash happened.
| . | . | Important: The panic file is just a last resort and there is no guarantee that saving the configuration succeeds or restoring the configuration is possible. | . |
In order to test the panic recovery, run the simulation in Exercise 13.18 but change the name of the export file to dgr037K4a.plist. Kill the process after a few second, e.g. with Ctrl-C, and check if a file named panic-t<time>-<date>.plist has been created in the current working directory. Now resume the simulation by executing ’java -jar EvoLudo.<version>.jar --restore panic-t<time>-<date>.plist’. Let the simulation finish and compare the two final configurations dgr037K4.plist and dgr037K4a.plist. Apart from time stamps they are identical.
If neither the --data nor the --export options are present, the jar-archive launches the java GUI.
| . | . | Note: The java GUI is not as sophisticated as the JavaScript GUI. The java GUI is still maintained but no longer under active development. Consequently, it provides only a subset of the visualizations and interactive capabilities. | . |
For PDE models the ability to run multiple processes in parallel provides a significant speed boost. In the java implementation of PDE models both the reaction and diffusion steps are parallelized. This performance difference can be nicely illustrated by returning to Exercise 13.16. However, instead of the default elements, use a grid of elements. Open EvoLudo in the browser (as in Exercise 13.16). At the same time run the java application in the terminal with the exact same options. This launches another copy of EvoLudo but with a (more rudimentary) java GUI. Now click Step or Start in the browser as well as the java GUI and compare the impressive difference in speed of the two simulation environments.
This final section covers the steps for running custom simulations with the TxT module. In order to understand an evolutionary process in greater detail it is often necessary to run simulations for a range of parameters. In principle, this is possible by manually changing the parameter of interest in the EvoLudo GUI and recording the results. However, this is not only tedious and error-prone, but also suffers from the slower execution speed of JavaScript on top of it.
A more efficient way is to automate the process through a batch file, which executes EvoLudo.<version>.jar multiple times with appropriate parameters, see section 13.12: Simulations using the command line. This allows to run simulations in the background and even to run multiple simulations in parallel. The results can be written to one or several output files. This output can then be processed for visualization or further analysis. However, for more complex investigations, custom simulations may be required.
Our goal here is to create a custom simulation that calculates the fixation probability of a single cooperator in the donation game for a range of cost-to-benefit ratios , see Eq. (13.1). This requires running a series of simulations for different values of and collecting the fixation probabilities. In principle, this can be done by either method outlined above, but also serves as an illustrative example on how to build custom simulations. At the end we will have an executable jar-archive that can be run from the command line and, in addition to the parameters of TxT, provides further parameters to control the simulations. In the end this allows to validate the -rule in structured populations (Ohtsuki et al., 2006).
All custom simulations reside in the maven module EvoLudoSims folder. Several sample files can be found in the EvoLudoSims/src/main/java/org/evoludo/simulator/exec folder, including one for games, simTBT.java but let us start from scratch.
To get started, create a new file simDG.java in the EvoLudoSims/src/main/java/org/evoludo/simulator/exec folder with the following code:
| . | . | Note: Since the simDG class extends the TxT class, it simply represents another module that can be loaded by the EvoLudo engine and has access to all features of TxT. | . |
An executable jar-archive requires the implementation of an entry method: public static void main(String[] args), where args refers to the array of options provided on the command line. This method is called when the jar-archive is executed. The key tasks of the main(...) method are to instantiate the simDG class and start the EvoLudo engine. Custom simulations are handled by the method custom(Module module, String[] args) in the EvoLudo engine:
with
In contrast to re-creating the TBT-module there is no reference file available for simDG and hence a few comments are included that help explain the logic of the code. The custom(...) method completes a number of important tasks:
registers itself as a supplier of custom command line options (by overriding the collectCLO(CLOParser parser) method);
loads the module and processes all command line options, which includes loading a model;
resets the module, which also initializes the model and sets the seed of the random number generator (if requested);
registers simDG as a MilestoneListener to monitor the progress of the simulation through milestone events, see section 12.10.1: Milestone listeners;
| . | . | Important: Some simulations may require notifications of every update through ChangeListener events. However, this easily gets computationally expensive and risks slowing down the simulations. For this reason, the ChangeListener is not registered by default. It only gets registered if the module implements the ChangeListener interface, see section 12.10.2: Change listeners. | . |
returns control to simDG by calling the run() method of the parental Module class, which is inherited via the TxT and Discrete super classes.
By overriding the run() method we have full control over the simulation process. However, let us first finish the administrative part and build the custom simulation, albeit it’s just an empty shell that does nothing.
A number of executable jar-archives are built when running ’mvn clean install’ in the top EvoLudo directory and are located in the EvoLudoSims/target directory. The maven instructions to build the jar-archives are in EvoLudoSims/pom.xml. Open the file and look for the configuration of the maven-assembly-plugin. This includes a number of <executions>. Each one builds one executable jar-archive. Simply copy and paste one <execution> and adjust the <id>, <finalName>, and <mainClass> entries to build the simDG module. The relevant snippet of the pom.xml file is:
with the modifications highlighted. During development with VS Code, it is recommended to create a new launch configuration. The launch.json configuration file can be opened by selecting the Run & Debug tab and clicking on the settings button. The following lines add a new target for testing the DG custom simulations.
In order to set or change the command line options, simply edit the string "args" field in the target configuration. Now breakpoints can be set and when running the target, execution is interrupted at the first specified point with all the joys and sorrows of step-by-step debugging.
Alternatively, or in order to run the custom simulations in production, run ’mvn clean install’ in the top EvoLudo directory for a full build of the EvoLudo project, which includes EvoLudoSims. In order to restrict the build to the required maven modules, run
’mvn install -pl EvoLudoCore,EvoLudoJRE,EvoLudoSims’
to build the jar-archives. The executable simDG.<version>.jar-archive is located in the EvoLudoSims/target directory. Try it out by running ’java -jar EvoLudoSims/target/simDG.<version>.jar’. This loads the custom simulations and exits. Add --help to display all available options.
| . | . | Note: The --module option is not available for custom simulations and is ignored if provided. | . |
Now we are ready to start implementing the method run() and gaining full control over the simulation process. The skeletal setup is to request statistics mode, print the parameter settings, loop over the range of cost-to-benefit ratios and to collect statistical samples for each value of . Eventually we want to add a command line option to set the range of the cost-to-benefit ratio as well as the number of steps. However, for now let’s just add a double array cbRatio with [start, end, steps]:
and the structure of the run() method:
The custom simulation compiles and runs fine but, apart from printing the parameters, nothing much happens. Next, we set everything up for the statistics and print the results for each value of . To simplify printing output to the console (or any output file that is specified by --output or --append) we introduce the convenience variables out. For dealing with statistics, another field jrengine will be useful later. We declare the two fields at the beginning of the class:
and initialize them in the constructor:
For the statistics, all we need is an array to store the number of times that A or B types have fixed. With these additions we can provide feedback to the user:
with
The normalize(double[]) conveniently converts the number of fixations into probabilities. Compiling and running the custom module now returns the following output:
The NaN values are of no importance and simply a consequence of all elements of nFix being zero. However, at least we confirmed that our loop is working as expected. The key remaining piece is to implement the actual collection of statistical samples. The number of samples is set by the --samples option from the Model class. The method generateSample() in the class EvoLudoJRE generates and returns one statistics sample with the results stored in a FixationData object:
Now let’s see what we get with samples for each :
Oops! Recall that fixation statistics are only available for appropriate initial configurations. Let’s try again and add the --init mutant 0,1 to place a single A type in a homogeneous B population. Also, don’t forget to set the Moran death-birth update, --popupdate dB, together with the convex payoff to fitness mapping, --fitnessmap convex 1,0.01 with baseline fitness, , and weak selection, . The output now reads:
Looks rather promising – possibly with some minor variation due to the random initialization.
| . | . | Note: In order to obtain reproducible simulations, it is essential to set the seed of the random number generator using the --seed option. This ensures that the same sequence of random numbers is generated for each simulation. Every time the model is reset, the random number generator is reset as well, which not only results in identical initial configurations, but also re-generates the same network (if any) for the population structure. | . |
| . | . | Important: For statistical evaluations it is imperative to clear the seed after the first reset, which has already happened before returning control to simDG through calling the method run(). Otherwise, the same data point is generated over and over and over… | . |
If the option --seed has been set, clear it before starting the statistics:
| . | . | Note: Clear the seed only after printing the parameters to the console to ensure that the seed settings are correctly reported. | . |
Earlier, when using the command model.requestMode(Mode.STATISTICS_SAMPLE); we tacitly assumed that the model actually supports statistics mode. While this is true for IBS and SDE models it does not extend to ODE or PDE models. There are different approaches to handle this situation:
check the model type (for example with model.isIBS() or model instanceof IBS etc. at the beginning of the run() method and exit if not);
check the return value of model.requestMode(Mode.STATISTICS_SAMPLE); and exit if false is returned (indicates that the model does not support statistics mode);
modify the --model option to only allow compatible models;
restrict the available model types for simDG.
The last option is clearly the most elegant solution – and the easiest to implement:
The range of the cost-to-benefit ratio is at present hard-coded to with steps. Instead of setting these default values when initializing the field cbRatio it would be more convenient to adjust the settings with a command line option, for example, --ratio <s[,e[,n]]>, where the array of arguments follows the same convention as the cbRatio, only that the endpoint, <e>, and the number of steps, <n>, are optional:
with
The initial value of the field cbRatio is no longer used and can be safely removed. If --ratio is not provided on the command line, its default argument, "0,1,5", is still processed.
When running our custom simulations we do not want to allow the user to set the payoff matrix! We can easily prevent this by removing the cloPayoffs option. After all command line options have been collected, the method adjustCLO(CLOParser) is called to make any adjustments. This is the place to remove the --paymatrix option:
Now we are ready to verify the -rule.
| . | . | Note: When running statistical evaluations using structures that involve random elements, such as random (regular) graphs, the structure is regenerated for every new sample. Computationally this is a fairly expensive and unnecessary at the same time. In a structured population of size , there are possible configurations for the placement of the initial mutant. Thus, it is safe to use the same structure for at least samples. Even if the same initial configuration is used multiple times by chance, the bias is negligible as long as the evolutionary trajectory is stochastic, i.e. the initial condition does not uniquely determine the end state. The interval for resetting the structure can be controlled with the option --statistics. | . |
Calculate the fixation probabilities on random graphs with and , scanning the interval using steps and samples.
Solution
The resulting output is:
EvoLudo % java -jar EvoLudoSims/target/simDG.<version>.jar
--popsize 200 --popupdate dB --init mutant 0,1
--fitnessmap convex 1,0.01 --geometry R4 --ratio 0,0.5,10 --seed 0
--statistics reset 200 --samples 100000
# data:
# r rhoA rhoB
0.000 0.006020 0.993980
0.050 0.005990 0.994010
0.100 0.005730 0.994270
0.150 0.005390 0.994610
0.200 0.004950 0.995050
0.250 0.005150 0.994850
0.300 0.005000 0.995000
0.350 0.004380 0.995620
0.400 0.004520 0.995480
0.450 0.004240 0.995760
0.500 0.004090 0.995910
# runningtime: 0:01:58.90
With the --seed option the numbers should be identical. Without the --statistics option the results would show insignificant differences, but with a longer execution time. In this case seconds longer, which translates to a performance improvement of roughly when using --statistics. More importantly, overall the trend is clear, but the results remain noisy even at samples. According to the -rule the threshold for should be near .
Equip yourself with some patience and repeat the above calculations for samples, while also limiting the range to .
| . | . | Note: With samples, the statistical error is of the order such that we end up with merely two significant digits. In order to add another significant digit, samples are required, and the running time increase -fold. | . |
Long-running simulations that get accidentally interrupted attempt to dump their final state to a file named panic-t<t>-<date>.plist, where <t> is replaced by the evolutionary time at which the model stopped, and <date> by the actual time when the crash happened, see section 13.12: Simulations using the command line. Unfortunately, this is only of limited use for statistical evaluations. A better approach to reduce the risk of data loss, is to report the current fixation probability in regular intervals together with the number of samples collected thus far. Possibly add a leading # character to simplify post-processing of the output.