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()
Prisoner's Dilemma trajectory

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.