EGTTools is a modular toolbox for simulating and analyzing evolutionary dynamics in strategic environments. It combines analytical methods (replicator dynamics, fixation probabilities) and numerical simulations (Monte Carlo with parallel C++ backends) under a unified interface.
Testing & Continuous Integration
- Features
- Installation
- Platform Notes
- Advanced Configuration
- Build from Source
- Usage Examples
- Documentation
- Testing & CI
- Citation
- License
- Acknowledgements
- Caveats
- ✅ Replicator dynamics for 2-strategy and N-player games
- ✅ Stochastic dynamics using the pairwise comparison rule
- ✅ Numerical simulation of evolutionary processes in finite populations
- ✅ Monte Carlo estimation of fixation probabilities and strategy distributions
- ✅ OpenMP parallelization for large-scale simulations (Linux/macOS)
- ✅ Modular game and strategy framework, extensible in both Python and C++
- ✅ Visual tools for plotting gradients, stationary distributions, and simplex diagrams
- ✅ Support for Boost, Eigen, and BLAS integration (configurable)
- ✅ Cross-platform wheels (Linux, macOS, Windows; x86_64 and ARM64)
EGTTools is distributed via PyPI (pip wheels) and conda (Anaconda.org). Prebuilt packages are available for all major platforms:
| Platform | Architectures | Python Versions | OpenMP Supported |
|---|---|---|---|
| Linux (x86_64) | x86_64 | 3.10 – 3.13 | ✅ Yes |
| macOS (x86/arm) | x86_64, arm64 (M1–M4) | 3.10 – 3.13 | ✅ Yes |
| Windows | x86_64, arm64 | 3.10 – 3.13 | ❌ Not available |
pip install egttoolsThe conda package bundles all native dependencies (BLAS/LAPACK, OpenMP) and works out of the box with miniforge, mambaforge, and miniconda (which are all free for any use, including large organisations).
conda install -c socrats egttoolsor with mamba (faster solver):
mamba install -c socrats egttoolsTo avoid specifying -c socrats every time, add the channel once to your
conda configuration:
conda config --add channels socrats
conda config --set channel_priority strictAfter that, conda install egttools works without a channel flag.
Note: The conda package is hosted on the maintainer's personal Anaconda.org channel. It is not in the
conda-forgeordefaultschannels.
These notes apply when building from source; the prebuilt pip/conda packages already bundle everything below.
-
OpenMP is enabled by default (
USE_OPENMP=ON). -
System packages needed:
libomp-dev,libblas-dev,liblapack-dev,autoconf,automake,autoconf-archive(seebuild_tools/github/apt-packages.txt):sudo apt-get install -y libomp-dev libblas-dev liblapack-dev autoconf automake autoconf-archive
-
Boost and Eigen are fetched automatically via the bundled
vcpkgsubmodule — no manual installation needed. -
BLAS/LAPACK acceleration is auto-detected (system BLAS/LAPACK); there is no toggle to enable/disable it.
-
Supported on both
x86_64andarm64(M1–M4). -
The conda package is the easiest install on macOS — all native dependencies are resolved automatically. If you only need the Python package, prefer that over building from source.
-
When using pip in a conda environment, prefer miniforge for ABI compatibility:
conda install numpy scipy matplotlib networkx seaborn plotly pip install egttools --no-deps
-
When building from source, install
libomp(andopenblas/lapackfor acceleration) via Homebrew or conda, e.g.:brew install libomp openblas lapack autoconf automake autoconf-archive
CMake needs to find the
libompinstall; the most reliable way is to point it at the package directory explicitly viaEGTTOOLS_EXTRA_CMAKE_ARGSwhen building (see below), e.g. with a conda-installedllvm-openmp:export EGTTOOLS_EXTRA_CMAKE_ARGS="-DLIBOMP_DIR=/path/to/llvm-openmp/pkg"
-
BLAS/LAPACK acceleration uses Apple's
Accelerateframework automatically when available, falling back to OpenBLAS/LAPACK otherwise — no manual toggle needed.
- Windows wheels are available for both Intel and ARM architectures.
- OpenMP is currently not available on Windows.
- Simulations will fall back to single-threaded mode.
- No system OpenMP/BLAS packages are needed — these are provided by MSVC.
Boost and Eigen are fetched automatically via
vcpkg.
The C++ backend of EGTTools supports several build-time options (defined in
CMakeLists.txt) that can be toggled when building from source:
| Feature | CMake Option | Default | Description |
|---|---|---|---|
| OpenMP | -DUSE_OPENMP=ON|OFF |
ON | Enables parallel computation for simulations (Linux/macOS) |
| Skip vcpkg | -DSKIP_VCPKG=ON|OFF |
OFF | Skip the vcpkg toolchain and use system-provided Boost/Eigen instead |
| ARPACK eigensolver | -DEGTTOOLS_ENABLE_ARPACK=ON|OFF |
OFF | Opt-in native ARPACK eigensolver (requires arpack-ng) |
| PETSc/SLEPc (MPI) | -DEGTTOOLS_ENABLE_PETSC=ON|OFF |
OFF | Opt-in MPI-distributed eigensolver (numerical_mpi_ module) |
BLAS/LAPACK is auto-detected (Apple Accelerate on macOS, system BLAS/LAPACK on Linux/Windows) — there is no separate flag to toggle it.
You may want to skip vcpkg in CI environments or when using a distribution that provides all necessary dependencies
system-wide:
EGTTOOLS_EXTRA_CMAKE_ARGS="-DSKIP_VCPKG=ON" pip install .In this case, you are responsible for ensuring that compatible versions of Boost and Eigen are available in your system paths.
EGTTools builds via scikit-build, so
CMake configuration happens through pip/setup.py, not by invoking
cmake/make directly. To build from source with all dependencies managed
via the bundled vcpkg submodule, run:
git clone --recurse-submodules https://github.com/Socrats/EGTTools.git
cd EGTTools
pip install .For iterative development (rebuilding just the C++ extension in place):
python setup.py build_ext --inplaceTo pass extra CMake options (e.g. disabling OpenMP, skipping vcpkg, enabling
ARPACK), use the EGTTOOLS_EXTRA_CMAKE_ARGS environment variable — this is
the same mechanism the CI wheel builds use:
EGTTOOLS_EXTRA_CMAKE_ARGS="-DUSE_OPENMP=OFF" pip install .On macOS you will typically also need to point CMake at your libomp
installation this way (see the macOS platform notes above).
vcpkg is auto-detected from a vcpkg/ submodule checkout at the project
root; if you've bootstrapped vcpkg elsewhere, point VCPKG_PATH at that
project root (not the vcpkg subdirectory itself):
export VCPKG_PATH=/path/to/your/checkoutIf using conda, make sure to activate your environment first and ensure that Python, NumPy, and compiler toolchains
are compatible.
from egttools.analytical import PairwiseComparison
from egttools.games import Matrix2PlayerGameHolder
A = [[-0.5, 2], [0, 0]]
game = Matrix2PlayerGameHolder(2, A)
evolver = PairwiseComparison(100, game)
gradient = evolver.calculate_gradient_of_selection(beta=1.0, state=[10, 90])from egttools.numerical import PairwiseComparisonNumerical
from egttools.games import Matrix2PlayerGameHolder
A = [[-0.5, 2], [0, 0]]
game = Matrix2PlayerGameHolder(2, A)
numerical_evolver = PairwiseComparisonNumerical(game, population_size=100, cache=1_000_000)
fp = numerical_evolver.estimate_fixation_probability(
index_invading_strategy=1,
index_resident_strategy=0,
nb_runs=500,
nb_generations=5000,
beta=1.0
)The Analytical example is a jupyter notebook which analyses analytically the evolutionary dynamics in a (2-person, 2-actions, one-shot) Hawk-Dove game.
The Numerical example is a jupyter notebook which analyses through numerical simulations the evolutionary dynamics in a (2-person, 2-actions, one-shot) Hawk-Dove game.
The Invasion example is a jupyter notebook calculates the fixation probabilities and stationary distribution of a Normal Form Game with 5 strategies and then plots an invasion diagram.
The Plot 2 Simplex is a jupyter notebook that shows how to use EGTtools to plot the evolutionary dynamics in a 2 Simplex (a triangle), both for infinite and finite populations.
You can also check all these notebooks and a bit more on this tutorial repository
For example, assuming the following payoff matrix:
You can plot the gradient of selection in a finite population of (Z=100) individuals and assuming and intensity of
selection in the following way:
import numpy as np
from egttools.analytical import PairwiseComparison
from egttools.games import Matrix2PlayerGameHolder
beta = 1;
Z = 100;
nb_strategies = 2;
A = np.array([[-0.5, 2.], [0., 0.]])
pop_states = np.arange(0, Z + 1, 1)
game = Matrix2PlayerGameHolder(nb_strategies, payoff_matrix=A)
# Instantiate evolver and calculate gradient
evolver = PairwiseComparison(population_size=Z, game=game)
gradients = np.array([evolver.calculate_gradient_of_selection(beta, np.array([x, Z - x])) for x in range(Z + 1)])Afterward, you can plot the results with:
from egttools.plotting import plot_gradients
plot_gradients(gradients, figsize=(4, 4), fig_title="Hawk-Dove game stochastic dynamics",
marker_facecolor='white',
xlabel="frequency of hawks (k/Z)", marker="o", marker_size=20, marker_plot_freq=2)And you can plot the stationary distribution for a mutation
rate with:
import matplotlib.pyplot as plt
from egttools.utils import calculate_stationary_distribution
transitions = evolver.calculate_transition_matrix(beta, mu=1e-3)
stationary_with_mu = calculate_stationary_distribution(transitions.transpose())
fig, ax = plt.subplots(figsize=(5, 4))
fig.patch.set_facecolor('white')
lines = ax.plot(np.arange(0, Z + 1) / Z, stationary_with_mu)
plt.setp(lines, linewidth=2.0)
ax.set_ylabel('stationary distribution', size=16)
ax.set_xlabel('$k/Z$', size=16)
ax.set_xlim(0, 1)
plt.show()We can get the same results through numerical simulations. The error will depend on how many independent simulations you perform and for how long you let the simulation run. While a future implementation will offer an adaptive method to vary these parameters depending on the variations between the estimated distributions, for the moment it is important that you let the simulation run for enough generations after it has achieved a steady state. Here is a comparison between analytical and numerical results:
from egttools.numerical import PairwiseComparisonNumerical
from egttools.games import NormalFormGame
# Instantiate the game
game = NormalFormGame(1, A)
numerical_evolver = PairwiseComparisonNumerical(Z, game, 1000000)
# We do this for different betas
betas = np.logspace(-4, 1, 50)
stationary_points = []
# numerical simulations
for i in range(len(betas)):
stationary_points.append(numerical_evolver.stationary_distribution(30, int(1e6), int(1e3),
betas[i], 1e-3))
stationary_points = np.asarray(stationary_points)
# Now we estimate the probability of Cooperation for each possible state
state_frequencies = np.arange(0, Z + 1) / Z
coop_level = np.dot(state_frequencies, stationary_points.T)Lastly, we plot the results:
from sklearn.metrics import mean_squared_error
mse = mean_squared_error(1 - coop_level_analytical, coop_level)
# Finally, we plot and compare visually (and check how much error we get)
fig, ax = plt.subplots(figsize=(7, 5))
# ax.scatter(betas, coop_level, label="simulation")
ax.scatter(betas, coop_level_analytical, marker='x', label="analytical")
ax.scatter(betas, coop_level, marker='o', label="simulation")
ax.text(0.01, 0.535, 'MSE = {0:.3e}'.format(mse), style='italic',
bbox={'facecolor': 'red', 'alpha': 0.5, 'pad': 10})
ax.legend()
ax.set_xlabel(r'$\beta$', fontsize=15)
ax.set_ylabel('Cooperation level', fontsize=15)
ax.set_xscale('log')
plt.show()Finally, you may also visualize the result of independent simulations:
init_states = np.random.randint(0, Z + 1, size=10, dtype=np.uint64)
output = []
for i in range(10):
output.append(evolver.run(int(1e6), 1, 1e-3,
[init_states[i], Z - init_states[i]]))
# Plot each year's time series in its own facet
fig, ax = plt.subplots(figsize=(5, 4))
for run in output:
ax.plot(run[:, 0] / Z, color='gray', linewidth=.1, alpha=0.6)
ax.set_ylabel('k/Z')
ax.set_xlabel('generation')
ax.set_xscale('log')EGTtools can also be used to visualize the evolutionary dynamics in a 2 Simplex. In the example bellow, we use the
egttools.plotting.plot_replicator_dynamics_in_simplex which calculates the gradients on a simplex given an initial
payoff matrix and returns a egttools.plotting.Simplex2D object which can be used to plot the 2 Simplex.
import numpy as np
import matplotlib.pyplot as plt
from egttools.plotting import plot_replicator_dynamics_in_simplex
payoffs = np.array([[1, 0, 0],
[0, 2, 0],
[0, 0, 3]])
type_labels = ['A', 'B', 'C']
fig, ax = plt.subplots(figsize=(10, 8))
simplex, gradient_function, roots, roots_xy, stability = plot_replicator_dynamics_in_simplex(payoffs, ax=ax)
plot = (simplex.add_axis(ax=ax)
.draw_triangle()
.draw_gradients(zorder=0)
.add_colorbar()
.add_vertex_labels(type_labels)
.draw_stationary_points(roots_xy, stability)
.draw_trajectory_from_roots(gradient_function,
roots,
stability,
trajectory_length=15,
linewidth=1,
step=0.01,
color='k', draw_arrow=True,
arrowdirection='right',
arrowsize=30, zorder=4, arrowstyle='fancy')
.draw_scatter_shadow(gradient_function, 300, color='gray', marker='.', s=0.1, zorder=0)
)
ax.axis('off')
ax.set_aspect('equal')
plt.xlim((-.05, 1.05))
plt.ylim((-.02, simplex.top_corner + 0.05))
plt.show()The same can be done for finite populations, with the added possibility to plot the stationary distribution inside the triangle (see simplex plotting and simplified simplex plotting for a more in-depth example).
- 📘 API Reference (ReadTheDocs): https://egttools.readthedocs.io
- 🌍 Live Tutorial & Examples: https://efernandez.eu/EGTTools/
You can find a full description of available games, strategies, and simulation methods, along with Jupyter notebooks and real-world use cases.
EGTTools uses GitHub Actions for full CI/CD automation:
- 🧱
wheels.ymlbuilds wheels for all platforms (Linux, macOS, Windows; x86_64 and arm64) - 📘
docs.ymlbuilds documentation and deploys it to GitHub Pages and ReadTheDocs - ✅ Unit tests run with
pytestand are included in each CI matrix build - 🧪 Python stub files are auto-generated from
pybind11bindings for better typing support
To run tests locally:
pytest testsYou can also build and validate docs locally with:
cd docs
make htmlIf you use EGTtools in your publications, please cite it in the following way with bibtex:
@article{Fernandez2023,
author = {Fernández Domingos, Elias and Santos, Francisco C. and Lenaerts, Tom},
title = {EGTtools: Evolutionary game dynamics in Python},
journal = {iScience},
volume = {26},
number = {4},
pages = {106419},
year = {2023},
issn = {2589-0042},
doi = {https://doi.org/10.1016/j.isci.2023.106419}
}Or in text format:
Fernández Domingos, E., Santos, F. C. & Lenaerts, T. EGTtools: Evolutionary game dynamics in Python. iScience 26, 106419 (2023).
And to cite the current version of EGTtools you can use:
@misc{Fernandez2020,
author = {Fernández Domingos, Elias},
title = {EGTTools: Toolbox for Evolutionary Game Theory (0.1.12)},
year = {2022},
month = {Dec},
journal = {Zenodo},
doi = {10.5281/zenodo.7458631}
}Moreover, you may find our article at here.
EGTTools is released under the GPLv3 or later.
Developed and maintained by Elias Fernández.
- Great parts of this project have been possible thanks to the help of Yannick Jadoul author of Parselmouth and Eugenio Bargiacchi author of AIToolBox. They are both great programmers and scientists, so it is always a good idea to check out their work.
- EGTtools makes use of the amazing pybind11. library to provide a Python interface for optimized monte-carlo simulations written in C++.
- On Windows, OpenMP is currently not supported. All simulations will run single-threaded.
- On macOS, OpenMP is supported but performance may depend on the installed
libomp. If usingconda, make surellvm-openmpis available. - Wheels are only built for Python 3.10 – 3.12.
- Numerical simulations require large RAM allocations when using large population sizes or caching; ensure you configure the
cachesize accordingly. - Advanced users building from source should ensure Boost, Eigen, and BLAS/LAPACK libraries are compatible with their compiler toolchain.





