Prisoner’s Dilemma¶
This example models a population of agents repeatedly choosing between two strategies: cooperate or defect, as in the classic Prisoner’s Dilemma game.
It demonstrates how revision dynamics can drive the population toward defection, even when mutual cooperation would be socially optimal.
1import numpy as np
2import matplotlib.pyplot as plt
3from popgames import (
4 SinglePopulationGame,
5 PayoffMechanism,
6 PoissonRevisionProcess,
7 Simulator,
8)
9from popgames.revision_protocol import Softmax
10
11T, R, P, S = 3.0, 2.0, 1.0, 0.0 # Prisoner's dilemma parameters
12 # s.t. T > R > P > S
13 # https://en.wikipedia.org/wiki/Prisoner%27s_dilemma
14 # (floats, so that the example also runs with the numba backend)
15def fitness_function(x):
16 return np.dot(
17 np.array([[R, S], [T, P]]),
18 x
19 )
20
21population_game = SinglePopulationGame(
22 num_strategies=2,
23 fitness_function=fitness_function,
24)
25
26payoff_mechanism = PayoffMechanism(
27 h_map=fitness_function,
28 n=2,
29)
30
31revision_process = PoissonRevisionProcess(
32 Poisson_clock_rate=1,
33 revision_protocol=Softmax(0.1),
34)
35
36sim = Simulator(
37 population_game=population_game,
38 payoff_mechanism=payoff_mechanism,
39 revision_processes=revision_process,
40 num_agents=1000
41)
42
43x0 = np.array([0.5, 0.5]).reshape(2, 1)
44sim.reset(x0=x0)
45out = sim.run(T_sim=30)
46
47plt.figure(figsize=(3, 3))
48plt.plot(out.t, out.x[0, :], label='Cooperators')
49plt.plot(out.t, out.x[1, :], label='Defectors')
50plt.xlabel('Time')
51plt.ylabel('Portion of agents choosing each strategy')
52plt.legend()
53plt.grid()
54plt.tight_layout()
55plt.show()
Running with the Numba backend¶
The same simulation can run with the optional Numba backend (requires
pip install "popgames[numba]"). Only the simulator changes:
sim = Simulator(
population_game=population_game,
payoff_mechanism=payoff_mechanism,
revision_processes=revision_process,
num_agents=1000,
backend="numba",
)
The fitness function is compiled automatically with numba.njit. This is why the payoff parameters are declared
as floats (T, R, P, S = 3.0, 2.0, 1.0, 0.0): with integers, np.array([[R, S], [T, P]]) would be an integer
matrix, and Numba does not support matrix products between integer and floating-point arrays. With the numpy
backend, both versions work. If the fitness function cannot be compiled, the simulator logs a warning and falls back
to the numpy backend.
With 1000 agents over 30 time units (about 30 000 revision events), this simulation is too small for the Numba
backend to pay off: compiling the fitness function takes longer than the whole simulation with the numpy backend.
The Numba backend is worth it for large populations or long horizons, e.g., num_agents=100_000.