Skip to content

Cooperation

Cooperation: evolutionary games between neighbours, in hyperedges, in tournaments, and between groups.

ContributorElement(label, visual=None, *, strategy=1.0, payoff=0.0, synergy=4.5, noise=0.5)

Bases: Element

Player of the spatial public goods game, a cooperator at 1 and a defector at 0, with its payoff.

The play is a PublicGoodsInteraction of the lattice or a HyperedgeGoodsInteraction of a hypergraph, and the imitation an ImitationInteraction that reads the payoffs of the same step, which public_goods_game and hypergraph_public_goods add.

Parameters:

Name Type Description Default
label str

The label of the player or the lattice group.

required
visual VisualSpec | None

How the agent is drawn, or None for default_visual_.

None
strategy Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))]

The initial strategy, 1 or 0.

1.0
payoff Annotated[float, StateSpec()]

The payoff of the last play, the sum of the shares of its groups minus its contributions.

0.0
synergy Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The factor that multiplies the contributions of a group.

4.5
noise Annotated[float, ParameterSpec(domain=open(0, oo))]

The noise of the Fermi imitation.

0.5

Examples:

>>> from skcomplex.systems.social.cooperation import ContributorElement
>>> ContributorElement('player').synergy
4.5
Source code in src/skcomplex/systems/social/cooperation/_contributor.py
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    strategy: Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))] = 1.0,
    payoff: Annotated[float, StateSpec()] = 0.0,
    synergy: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 4.5,
    noise: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 0.5,
) -> None:
    super().__init__(label, visual=visual)
    self.strategy = strategy
    self.payoff = payoff
    self.synergy = synergy
    self.noise = noise

DilemmaElement(label, visual=None, *, strategy=1.0, payoff=0.0, triangles=1.0)

Bases: Element

Player of the social dilemmas on a simplicial complex, a cooperator at 1 and a defector at 0, with its payoff.

The play is a SimplicialDilemmaInteraction of the network, and the imitation an ImitationInteraction that reads the payoffs of the same step, which simplicial_social_dilemmas adds.

Parameters:

Name Type Description Default
label str

The label of the player or the group.

required
visual VisualSpec | None

How the player is drawn, or None for default_visual_.

None
strategy Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))]

The initial strategy, 1 or 0.

1.0
payoff Annotated[float, StateSpec()]

The payoff of the last play, the mean payoff of the player over the links of its triangles.

0.0
triangles Annotated[float, ParameterSpec(domain=open(0, oo))]

The number of triangles of the network that hold the player.

1.0

Examples:

>>> from skcomplex.systems.social.cooperation import DilemmaElement
>>> DilemmaElement('player').triangles
1.0
Source code in src/skcomplex/systems/social/cooperation/_hypergraph_games.py
56
57
58
59
60
61
62
63
64
65
66
67
68
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    strategy: Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))] = 1.0,
    payoff: Annotated[float, StateSpec()] = 0.0,
    triangles: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
) -> None:
    super().__init__(label, visual=visual)
    self.strategy = strategy
    self.payoff = payoff
    self.triangles = triangles

DonorElement(label, visual=VisualSpec('agents', {'color_by': 'strategy', 'color_range': (0.0, 1.0)}), *, strategy=1.0, payoff=0.0)

Bases: Element

Player of donation games on the edges of a graph, a cooperator at 1 and a defector at 0.

Parameters:

Name Type Description Default
label str

The label of the player or the group.

required
visual VisualSpec | None

How the agent is drawn, or None for default_visual_.

VisualSpec('agents', {'color_by': 'strategy', 'color_range': (0.0, 1.0)})
strategy Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))]

The strategy, 1 to cooperate and 0 to defect.

1.0
payoff Annotated[float, StateSpec()]

The payoff accumulated over the games with every neighbour in the last time step.

0.0

Examples:

>>> from skcomplex.systems.social.cooperation import DonorElement
>>> DonorElement('player').strategy
1.0
Source code in src/skcomplex/systems/social/cooperation/_game_transitions.py
60
61
62
63
64
65
66
67
68
69
70
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'strategy', 'color_range': (0.0, 1.0)}),
    *,
    strategy: Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))] = 1.0,
    payoff: Annotated[float, StateSpec()] = 0.0,
) -> None:
    super().__init__(label, visual=visual)
    self.strategy = strategy
    self.payoff = payoff

DonorView(agent, stepper)

Bases: AgentView

View of a player of the game transitions system, with its states as attributes.

Source code in src/skcomplex/core/engine/solvers/_mesa.py
559
560
561
def __init__(self, agent: mesa.Agent, stepper: 'MesaSolver') -> None:
    self._agent = agent
    self._stepper = stepper

GameTransitionsInteraction(label, *, target, first_benefit=3.5, second_benefit=2.6, cost=1.0, selection=0.1, after_cooperation=0.0, after_defection=1.0, transitions=True, update='death-birth')

Bases: Interaction

Evolutionary dynamics with game transitions, where the game on every edge follows the actions played on it.

Every edge of the graph carries game 1 or game 2, donation games where a cooperator pays c to give its partner b1 or b2, and every edge starts with game 1. In every time step every player plays the game of every edge with its neighbour and accumulates the payoff pi, of fitness 1 - delta + delta pi. Under death-birth updating a uniformly drawn player dies and its neighbours compete for the site in proportion to their fitness, and under pairwise comparison a uniformly drawn player copies a random neighbour with the probability 1 / (1 + exp(-delta (pi_j - pi_i))). The games then change: an edge goes to game 2 with the probability after_cooperation after mutual cooperation, with after_defection after one defection, and surely after mutual defection, and to game 1 otherwise. The newcomer of a site plays the games of its prior occupant. Every step of the system has one time step per player, and the share of edges with game 1 is reported as game_one.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
target Annotated[str, RoleSpec()]

The path of the group of players on the nodes of a network.

required
first_benefit Annotated[float, ParameterSpec()]

The benefit b1 of the more valuable game 1.

3.5
second_benefit Annotated[float, ParameterSpec()]

The benefit b2 of game 2.

2.6
cost Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The cost c of cooperating.

1.0
selection Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The intensity delta of selection.

0.1
after_cooperation Annotated[float, ParameterSpec(domain=closed(0, 1))]

The probability p of game 2 after mutual cooperation.

0.0
after_defection Annotated[float, ParameterSpec(domain=closed(0, 1))]

The probability q of game 2 after one cooperation and one defection.

1.0
transitions bool

Whether the games change, or every edge plays game 1 throughout.

True
update str

'death-birth' or 'pairwise'.

'death-birth'

Raises:

Type Description
ValidationError

The update is unknown, raised by prepare.

Examples:

>>> from skcomplex.systems.social.cooperation import GameTransitionsInteraction
>>> GameTransitionsInteraction('evolution', target='players').update
'death-birth'
Source code in src/skcomplex/systems/social/cooperation/_game_transitions.py
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
def __init__(
    self,
    label: str,
    *,
    target: Annotated[str, RoleSpec()],
    first_benefit: Annotated[float, ParameterSpec()] = 3.5,
    second_benefit: Annotated[float, ParameterSpec()] = 2.6,
    cost: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
    selection: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.1,
    after_cooperation: Annotated[float, ParameterSpec(domain=RealSet.closed(0, 1))] = 0.0,
    after_defection: Annotated[float, ParameterSpec(domain=RealSet.closed(0, 1))] = 1.0,
    transitions: bool = True,
    update: str = 'death-birth',
) -> None:
    super().__init__(label)
    self.target = target
    self.first_benefit = first_benefit
    self.second_benefit = second_benefit
    self.cost = cost
    self.selection = selection
    self.after_cooperation = after_cooperation
    self.after_defection = after_defection
    self.transitions = transitions
    self.update = update

evolve(system)

Run one time step per player: play the games, replace one player, and change the games.

Parameters:

Name Type Description Default
system SystemView

The system.

required
Source code in src/skcomplex/systems/social/cooperation/_game_transitions.py
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
def evolve(self, system: SystemView) -> None:
    """Run one time step per player: play the games, replace one player, and change the games.

    Args:
        system:
            The system.
    """
    players = system.agents(self.target, DonorView)
    storage = system.storage
    if 'first' not in storage:
        sources, targets = self._get_edges(players)
        storage['first'], storage['second'] = sources, targets
        storage['games'] = np.ones(sources.size)
        storage['neighbors'] = [
            np.concatenate([targets[sources == node], sources[targets == node]]) for node in range(len(players))
        ]
    first, second, games = storage['first'], storage['second'], storage['games']
    neighbors = storage['neighbors']
    if not isinstance(first, np.ndarray) or not isinstance(second, np.ndarray) or not isinstance(games, np.ndarray):
        return
    if not isinstance(neighbors, list):
        return
    rng = system.rng
    strategies = np.array([player.strategy for player in players])
    payoffs = np.zeros(len(players))
    for _ in range(len(players)):
        payoffs = self._get_payoffs(strategies, games, first, second)
        actions = strategies[first] + strategies[second]
        self._replace(strategies, payoffs, neighbors, rng)
        if self.transitions:
            second_game = np.where(actions == MUTUAL_COOPERATION, self.after_cooperation, self.after_defection)
            second_game = np.where(actions == 0, 1.0, second_game)
            games[:] = np.where(rng.random(games.size) < second_game, 2.0, 1.0)
    for player, strategy, payoff in zip(players, strategies.tolist(), payoffs.tolist(), strict=True):
        player.strategy = strategy
        player.payoff = payoff
    system.report('game_one', float(np.mean(games == 1)) if games.size else 1.0)

HyperedgeGoodsInteraction(label, *, target, phase=None)

Bases: CouplingInteraction

Play of the public goods game in every hyperedge around a player, which sets the payoffs.

It is a CouplingInteraction over the hyperedges of a network in the map form, a cooperator at 1 and a defector at 0. In every hyperedge of m members a cooperator pays 1, and the pot multiplied by synergy is shared by the m members, so the payoff of a player is the sum over its hyperedges of its share minus its contribution. The synergy is a parameter of the players.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
target Annotated[str, RoleSpec()]

The path of the players on the network.

required
phase str | None

The phase of the step the play runs in, or None for the default phase.

None

Examples:

>>> from skcomplex.systems.social.cooperation import HyperedgeGoodsInteraction
>>> HyperedgeGoodsInteraction('play', target='players').write
'payoff'
Source code in src/skcomplex/systems/social/cooperation/_hypergraph_games.py
 95
 96
 97
 98
 99
100
101
102
103
104
def __init__(self, label: str, *, target: Annotated[str, RoleSpec()], phase: str | None = None) -> None:
    super().__init__(
        label,
        target=target,
        mode='hyperedges',
        form='map',
        write='payoff',
        function=self._get_share,
        phase=phase,
    )

IteratedDilemmaInteraction(label, *, target)

Bases: LocalRuleInteraction

Play of the iterated prisoner's dilemma between neighbours, revision to the best strategy, and mutation.

It is a stochastic LocalRuleInteraction of the strategies over the cells within two von Neumann steps of a player. Every step each player plays the N-round prisoner's dilemma with its four von Neumann neighbours and sums its years in prison. A player of strategy n against one of strategy m < n gets m R + S + (N - m - 1) P, the other m R + T + (N - m - 1) P, and two players of strategy n get n R + (N - n) P. Every player then adopts the strategy of the lowest score among itself and its four neighbours, a random one of them on ties, and finally mutates to a uniformly drawn strategy with the probability mu.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
target Annotated[str, RoleSpec()]

The path of the players on the lattice.

required

Examples:

>>> from skcomplex.systems.social.cooperation import IteratedDilemmaInteraction
>>> IteratedDilemmaInteraction('play', target='players').state
'strategy'
Source code in src/skcomplex/systems/social/cooperation/_spatial_games.py
180
181
def __init__(self, label: str, *, target: Annotated[str, RoleSpec()]) -> None:
    super().__init__(label, target=target, state='strategy', rule=self._play, neighborhood=DIAMOND, stochastic=True)

years(own, other, *, rounds, reward, sucker, temptation, punishment) staticmethod

Return the years in prison of a player of strategy n against one of strategy m in an N-round game.

Parameters:

Name Type Description Default
own Array

The strategies n of the players.

required
other Array

The strategies m of their opponents.

required
rounds float | Array

The number N of rounds.

required
reward float | Array

The punishment R of mutual cooperation.

required
sucker float | Array

The punishment S of a betrayed cooperator.

required
temptation float | Array

The punishment T of a defector who betrays.

required
punishment float | Array

The punishment P of mutual defection.

required

Returns:

Name Type Description
years Array

The years in prison of every player.

Examples:

>>> import numpy as np
>>> from skcomplex.systems.social.cooperation import IteratedDilemmaInteraction
>>> years = IteratedDilemmaInteraction.years(
...     np.arange(11.0), np.full(11, 6.0), rounds=10, reward=0.5, sucker=1.5, temptation=0.0, punishment=1.0
... )
>>> int(np.argmin(years))
5
Source code in src/skcomplex/systems/social/cooperation/_spatial_games.py
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
@staticmethod
def years(
    own: Array,
    other: Array,
    *,
    rounds: float | Array,
    reward: float | Array,
    sucker: float | Array,
    temptation: float | Array,
    punishment: float | Array,
) -> Array:
    """Return the years in prison of a player of strategy `n` against one of strategy `m` in an `N`-round game.

    Args:
        own:
            The strategies `n` of the players.

        other:
            The strategies `m` of their opponents.

        rounds:
            The number `N` of rounds.

        reward:
            The punishment `R` of mutual cooperation.

        sucker:
            The punishment `S` of a betrayed cooperator.

        temptation:
            The punishment `T` of a defector who betrays.

        punishment:
            The punishment `P` of mutual defection.

    Returns:
        years:
            The years in prison of every player.

    Examples:
        >>> import numpy as np
        >>> from skcomplex.systems.social.cooperation import IteratedDilemmaInteraction
        >>> years = IteratedDilemmaInteraction.years(
        ...     np.arange(11.0), np.full(11, 6.0), rounds=10, reward=0.5, sucker=1.5, temptation=0.0, punishment=1.0
        ... )
        >>> int(np.argmin(years))
        5
    """
    first = np.minimum(own, other)
    betrayal = np.where(own < other, temptation, sucker)
    mixed = first * reward + betrayal + (rounds - first - 1) * punishment
    return np.where(own == other, own * reward + (rounds - own) * punishment, mixed)

IteratedPrisonerElement(label, visual=None, *, strategy=0.0, rounds=7, reward=0.72, sucker=1.5, temptation=0.0, punishment=1.0, mutation=0.01, extremes=0.0)

Bases: Element

Player of the lattice iterated prisoner's dilemma, who cooperates for a number of rounds and then defects.

The strategy n of a player is the number of rounds it intends to cooperate, until round n included or until its opponent defects, after which it defects for the rest of the game. The payoffs are years in prison, so lower is better. The play, the revision, and the mutation are an IteratedDilemmaInteraction of the lattice, which iterated_lattice_dilemma adds.

Parameters:

Name Type Description Default
label str

The label of the player or the lattice group.

required
visual VisualSpec | None

How the agent is drawn, or None for default_visual_.

None
strategy Annotated[float, StateSpec(domain=NN)]

The number n of intended cooperation rounds, from 0, always defect, to rounds, always cooperate.

0.0
rounds Annotated[int, ParameterSpec(domain=NN)]

The number N of rounds of a game.

7
reward Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The punishment R of mutual cooperation.

0.72
sucker Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The punishment S of a cooperator who is betrayed.

1.5
temptation Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The punishment T of a defector who betrays.

0.0
punishment Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The punishment P of mutual defection.

1.0
mutation Annotated[float, ParameterSpec(domain=closed(0, 1))]

The probability mu that a player mutates to a random strategy at the end of a step.

0.01
extremes Annotated[float, ParameterSpec(domain=closed(0, 1))]

1 to allow only the strategies 0 and N, and 0 to allow every strategy from 0 to N.

0.0

Examples:

>>> from skcomplex.systems.social.cooperation import IteratedPrisonerElement
>>> IteratedPrisonerElement('player').rounds
7
Source code in src/skcomplex/systems/social/cooperation/_spatial_games.py
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    strategy: Annotated[float, StateSpec(domain=NN)] = 0.0,
    rounds: Annotated[int, ParameterSpec(domain=NN)] = 7,
    reward: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.72,
    sucker: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.5,
    temptation: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
    punishment: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
    mutation: Annotated[float, ParameterSpec(domain=RealSet.closed(0, 1))] = 0.01,
    extremes: Annotated[float, ParameterSpec(domain=RealSet.closed(0, 1))] = 0.0,
) -> None:
    super().__init__(label, visual=visual)
    self.strategy = strategy
    self.rounds = rounds
    self.reward = reward
    self.sucker = sucker
    self.temptation = temptation
    self.punishment = punishment
    self.mutation = mutation
    self.extremes = extremes

PrisonerElement(label, visual=None, *, strategy=1.0, temptation=1.85, noise=0.1)

Bases: Element

Player of the spatial prisoner's dilemma, a cooperator at 1 and a defector at 0.

The play and the imitation are a PrisonersDilemmaInteraction of the lattice, which spatial_prisoners_dilemma adds.

Parameters:

Name Type Description Default
label str

The label of the player or the lattice group.

required
visual VisualSpec | None

How the agent is drawn, or None for default_visual_.

None
strategy Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))]

The initial strategy, 1 or 0.

1.0
temptation Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The payoff of a defector against a cooperator.

1.85
noise Annotated[float, ParameterSpec(domain=open(0, oo))]

The noise of the Fermi imitation.

0.1

Examples:

>>> from skcomplex.systems.social.cooperation import PrisonerElement
>>> PrisonerElement('player').temptation
1.85
Source code in src/skcomplex/systems/social/cooperation/_spatial_games.py
125
126
127
128
129
130
131
132
133
134
135
136
137
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    strategy: Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))] = 1.0,
    temptation: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.85,
    noise: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 0.1,
) -> None:
    super().__init__(label, visual=visual)
    self.strategy = strategy
    self.temptation = temptation
    self.noise = noise

PrisonersDilemmaInteraction(label, *, target, imitation='best')

Bases: LocalRuleInteraction

Play of the weak prisoner's dilemma between neighbours, and imitation of more successful neighbours.

It is a stochastic LocalRuleInteraction of the strategies over the 5 by 5 square around a player, a cooperator at 1 and a defector at 0. Every step each player plays the weak dilemma, with reward 1, temptation temptation, and 0 otherwise, against itself and its eight neighbours. It then copies the best player among them, or a random neighbour with the Fermi probability 1 / (1 + exp((P_own - P_other) / noise)). The temptation and the noise are parameters of the players.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
target Annotated[str, RoleSpec()]

The path of the players on the lattice.

required
imitation str

'best' for the best neighbour, or 'fermi' for the Fermi rule.

'best'

Raises:

Type Description
ValidationError

The imitation is unknown, raised by prepare.

Examples:

>>> from skcomplex.systems.social.cooperation import PrisonersDilemmaInteraction
>>> PrisonersDilemmaInteraction('play', target='players', imitation='fermi').imitation
'fermi'
Source code in src/skcomplex/systems/social/cooperation/_spatial_games.py
323
324
325
def __init__(self, label: str, *, target: Annotated[str, RoleSpec()], imitation: str = 'best') -> None:
    super().__init__(label, target=target, state='strategy', rule=self._play, neighborhood=SQUARE, stochastic=True)
    self.imitation = imitation

PublicGoodsInteraction(label, *, target, phase=None)

Bases: GroupingInteraction

Play of the public goods game in the groups of a player and its four neighbours, which sets the payoffs.

It is a symbolic GroupingInteraction of the von Neumann neighbourhoods of a lattice, a cooperator at 1 and a defector at 0. Every player joins five groups, the one centred on itself and those centred on its four neighbours. In each group a cooperator pays 1, and the pot multiplied by synergy is shared by the members, so the payoff of a player is the sum of its shares minus its contributions. The synergy is a parameter of the players.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
target Annotated[str, RoleSpec()]

The path of the players on the lattice.

required
phase str | None

The phase of the step the play runs in, or None for the default phase.

None

Examples:

>>> from skcomplex.systems.social.cooperation import PublicGoodsInteraction
>>> PublicGoodsInteraction('play', target='players').write
'payoff'
Source code in src/skcomplex/systems/social/cooperation/_spatial_games.py
419
420
421
422
423
424
425
426
427
428
def __init__(self, label: str, *, target: Annotated[str, RoleSpec()], phase: str | None = None) -> None:
    super().__init__(
        label,
        target=target,
        groups='neighborhood',
        form='symbolic',
        write='payoff',
        function=self._get_share,
        phase=phase,
    )

PunisherGroupElement(label, visual=None, *, punishers=0.0)

Bases: Element

Group of defectors and altruistic punishers of a fixed size, held by its number of punishers.

Parameters:

Name Type Description Default
label str

The label of the group or the population of groups.

required
visual VisualSpec | None

How the agent is drawn, or None for default_visual_.

None
punishers Annotated[float, StateSpec(domain=NN)]

The number of altruistic punishers of the group.

0.0

Examples:

>>> from skcomplex.systems.social.cooperation import PunisherGroupElement
>>> PunisherGroupElement('group', punishers=3.0).punishers
3.0
Source code in src/skcomplex/systems/social/cooperation/_two_level_birth_death.py
51
52
53
54
55
56
57
58
59
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    punishers: Annotated[float, StateSpec(domain=NN)] = 0.0,
) -> None:
    super().__init__(label, visual=visual)
    self.punishers = punishers

PunisherGroupView(agent, stepper)

Bases: AgentView

View of a group of the two-level birth-death process, with its states as attributes.

Source code in src/skcomplex/core/engine/solvers/_mesa.py
559
560
561
def __init__(self, agent: mesa.Agent, stepper: 'MesaSolver') -> None:
    self._agent = agent
    self._stepper = stepper

SimplicialDilemmaInteraction(label, *, target, sucker=-0.2, temptation=1.2, sucker_allied=0.5, temptation_allied=0.8, sucker_opposed=-0.2, temptation_opposed=1.2, phase=None)

Bases: CouplingInteraction

Play of two-player games along the links of the triangles of a simplicial complex, which sets the payoffs.

It is a CouplingInteraction over the hyperedges of a network in the map form, a cooperator at 1 and a defector at 0. Every game pays 1 for mutual cooperation, 0 for mutual defection, and the sucker's payoff S and the temptation T when the strategies differ. A triangle of three links is three hyperedges of two members, along which the players play game 1 of S1 and T1. A triangle filled as a 2-simplex is one hyperedge of three members, where a player plays each other member while the third looks on: game 2 of S2 and T2 when the third member shares the player's strategy, and game 3 of S3 and T3 otherwise. The payoff of a player is the sum over the two links of every triangle around it divided by twice its number of triangles, a parameter of the players.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
target Annotated[str, RoleSpec()]

The path of the players on the network.

required
sucker Annotated[float, ParameterSpec()]

The payoff S1 of a cooperator against a defector along a link.

-0.2
temptation Annotated[float, ParameterSpec()]

The payoff T1 of a defector against a cooperator along a link.

1.2
sucker_allied Annotated[float, ParameterSpec()]

The payoff S2 in a 2-simplex whose third member shares the player's strategy.

0.5
temptation_allied Annotated[float, ParameterSpec()]

The payoff T2 in a 2-simplex whose third member shares the player's strategy.

0.8
sucker_opposed Annotated[float, ParameterSpec()]

The payoff S3 in a 2-simplex whose third member holds the other strategy.

-0.2
temptation_opposed Annotated[float, ParameterSpec()]

The payoff T3 in a 2-simplex whose third member holds the other strategy.

1.2
phase str | None

The phase of the step the play runs in, or None for the default phase.

None

Examples:

>>> from skcomplex.systems.social.cooperation import SimplicialDilemmaInteraction
>>> SimplicialDilemmaInteraction('play', target='players', temptation_allied=0.8).temptation_allied
0.8
Source code in src/skcomplex/systems/social/cooperation/_hypergraph_games.py
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
def __init__(
    self,
    label: str,
    *,
    target: Annotated[str, RoleSpec()],
    sucker: Annotated[float, ParameterSpec()] = -0.2,
    temptation: Annotated[float, ParameterSpec()] = 1.2,
    sucker_allied: Annotated[float, ParameterSpec()] = 0.5,
    temptation_allied: Annotated[float, ParameterSpec()] = 0.8,
    sucker_opposed: Annotated[float, ParameterSpec()] = -0.2,
    temptation_opposed: Annotated[float, ParameterSpec()] = 1.2,
    phase: str | None = None,
) -> None:
    super().__init__(
        label,
        target=target,
        mode='hyperedges',
        form='map',
        write='payoff',
        function=self._get_games,
        combine=self._get_mean,
        phase=phase,
    )
    self.sucker = sucker
    self.temptation = temptation
    self.sucker_allied = sucker_allied
    self.temptation_allied = temptation_allied
    self.sucker_opposed = sucker_opposed
    self.temptation_opposed = temptation_opposed

StrategistElement(label, visual=None, *, genome=1.0, fitness=0.0)

Bases: Element

Agent of the prisoner's dilemma tournament, with a genome of responses and the fitness of its last games.

Parameters:

Name Type Description Default
label str

The label of the agent or group.

required
visual VisualSpec | None

How the agent is drawn, or None for default_visual_.

None
genome Annotated[ArrayLike, StateSpec(vector=True)]

The seven moves, 1 to cooperate and 0 to defect, for the opponent's last two moves: none, C, D, CC, CD, DC, and DD.

1.0
fitness Annotated[float, StateSpec(domain=closed_open(0, oo))]

The mean score per round of the last tournament.

0.0

Examples:

>>> from skcomplex.core.components.elements import GroupElement
>>> from skcomplex.systems.social.cooperation import StrategistElement
>>> group = GroupElement('agents', element=StrategistElement, size=10, values={'genome': [1.0] * 7})
>>> group.label, group.element.__name__, group.size
('agents', 'StrategistElement', 10)
Source code in src/skcomplex/systems/social/cooperation/_tournaments.py
55
56
57
58
59
60
61
62
63
64
65
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    genome: Annotated[npt.ArrayLike, StateSpec(vector=True)] = 1.0,
    fitness: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
) -> None:
    super().__init__(label, visual=visual)
    self.genome = genome
    self.fitness = fitness

StrategistView(agent, stepper)

Bases: AgentView

View of a strategist of the iterated prisoner's dilemma, with its states as attributes.

Source code in src/skcomplex/core/engine/solvers/_mesa.py
559
560
561
def __init__(self, agent: mesa.Agent, stepper: 'MesaSolver') -> None:
    self._agent = agent
    self._stepper = stepper

TournamentInteraction(label, *, target, rounds=6, phase=None)

Bases: GroupingInteraction

Tournament of strategies of the iterated prisoner's dilemma.

Every step the agents stand in a random ring, and every agent forms a group with its two neighbours in the ring, whom it plays. Its fitness is its mean score per round.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
target Annotated[str, RoleSpec()]

The path of the group of strategists.

required
rounds int

The number of rounds of every game.

6
phase str | None

The phase of the step the tournament runs in, or None for the default phase.

None

Examples:

>>> from skcomplex.systems.social.cooperation import TournamentInteraction
>>> TournamentInteraction('tournament', target='agents').rounds
6
Source code in src/skcomplex/systems/social/cooperation/_tournaments.py
152
153
154
155
156
157
158
159
160
161
def __init__(
    self,
    label: str,
    *,
    target: Annotated[str, RoleSpec()],
    rounds: int = 6,
    phase: str | None = None,
) -> None:
    super().__init__(label, target=target, groups='neighborhood', phase=phase)
    self.rounds = rounds

act_on(members, system)

Set the fitness of the first member to its mean score per round against the two others.

Parameters:

Name Type Description Default
members list[AgentViewProtocol]

The agent, followed by its two neighbours in the ring.

required
system SystemViewProtocol

The system view of the rule.

required
Source code in src/skcomplex/systems/social/cooperation/_tournaments.py
171
172
173
174
175
176
177
178
179
180
181
182
183
def act_on(self, members: list[AgentViewProtocol], system: SystemViewProtocol) -> None:
    """Set the fitness of the first member to its mean score per round against the two others.

    Args:
        members:
            The agent, followed by its two neighbours in the ring.

        system:
            The system view of the rule.
    """
    agent, *opponents = [member for member in members if isinstance(member, StrategistView)]
    total = sum(play(agent.genome, opponent.genome, self.rounds)[0] for opponent in opponents)
    agent.fitness = total / (PAIR * self.rounds)

TwoLevelBirthDeathInteraction(label, *, target, members=20, selection=0.1, conflict_rate=0.2, benefit=2.0, cost=1.0, punishment=0.5, interaction_cost=0.5, fixed_cost=0.0)

Bases: Interaction

Two-level birth-death process of Luo and coauthors with pairwise conflicts between groups.

Within a group of n members with i punishers, a punisher reproduces at the rate 1 + w pi_P(i / n) and a defector at 1 + w pi_D(i / n), and the offspring replaces a random member of the group, the parent included. Every group fights a randomly drawn other group at the rate Lambda, wins with the probability rho = 1/2 + (z - u) / 2 of the fractions of punishers, and the winner's copy replaces the loser. The events of every step of one unit of time are drawn exactly, by the Gillespie algorithm.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
target Annotated[str, RoleSpec()]

The path of the population of groups.

required
members Annotated[int, ParameterSpec(domain=NN)]

The number n of members of every group.

20
selection Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The intensity w of individual-level selection.

0.1
conflict_rate Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The rate Lambda at which a group fights another group.

0.2
benefit Annotated[float, ParameterSpec()]

The benefit b of the donation game.

2.0
cost Annotated[float, ParameterSpec()]

The cost c of cooperating.

1.0
punishment Annotated[float, ParameterSpec()]

The punishment p of a defector by every punisher it meets.

0.5
interaction_cost Annotated[float, ParameterSpec()]

The cost k a punisher pays per defector it meets.

0.5
fixed_cost Annotated[float, ParameterSpec()]

The fixed cost q of being able to punish.

0.0

Examples:

>>> from skcomplex.systems.social.cooperation import TwoLevelBirthDeathInteraction
>>> TwoLevelBirthDeathInteraction('competition', target='groups').members
20
Source code in src/skcomplex/systems/social/cooperation/_two_level_birth_death.py
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
def __init__(
    self,
    label: str,
    *,
    target: Annotated[str, RoleSpec()],
    members: Annotated[int, ParameterSpec(domain=NN)] = 20,
    selection: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.1,
    conflict_rate: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.2,
    benefit: Annotated[float, ParameterSpec()] = 2.0,
    cost: Annotated[float, ParameterSpec()] = 1.0,
    punishment: Annotated[float, ParameterSpec()] = 0.5,
    interaction_cost: Annotated[float, ParameterSpec()] = 0.5,
    fixed_cost: Annotated[float, ParameterSpec()] = 0.0,
) -> None:
    super().__init__(label)
    self.target = target
    self.members = members
    self.selection = selection
    self.conflict_rate = conflict_rate
    self.benefit = benefit
    self.cost = cost
    self.punishment = punishment
    self.interaction_cost = interaction_cost
    self.fixed_cost = fixed_cost

compete(system)

Draw the births, deaths, and conflicts of one unit of time.

Parameters:

Name Type Description Default
system SystemView

The system.

required
Source code in src/skcomplex/systems/social/cooperation/_two_level_birth_death.py
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
def compete(self, system: SystemView) -> None:
    """Draw the births, deaths, and conflicts of one unit of time.

    Args:
        system:
            The system.
    """
    groups = system.agents(self.target, PunisherGroupView)
    size = len(groups)
    if size < PAIR:
        return
    rng = system.rng
    members = float(self.members)
    punishers = np.array([group.punishers for group in groups])
    time = 0.0
    while True:
        punisher_births, defector_births = self._get_birth_rates(punishers)
        rates = np.concatenate([punisher_births, defector_births, np.full(size, float(self.conflict_rate))])
        total = rates.sum()
        time += rng.exponential(1 / total)
        if time > 1.0:
            break
        event = int(np.searchsorted(np.cumsum(rates), rng.random() * total, side='right'))
        group, kind = event % size, event // size
        if kind == 0:
            punishers[group] += rng.random() >= punishers[group] / members
        elif kind == 1:
            punishers[group] -= rng.random() < punishers[group] / members
        else:
            other = int(rng.integers(size - 1))
            other += other >= group
            victory = 0.5 + (punishers[group] - punishers[other]) / (2 * members)
            winner, loser = (group, other) if rng.random() < victory else (other, group)
            punishers[loser] = punishers[winner]
    for group_view, value in zip(groups, punishers.tolist(), strict=True):
        group_view.punishers = value

game_transitions(n_players=100, *, degree=4, first_benefit=3.5, second_benefit=2.6, cost=1.0, selection=0.1, after_cooperation=0.0, after_defection=1.0, transitions=True, update='death-birth', share=0.5, random_state=0)

Return evolutionary dynamics with game transitions on a random regular graph, after Su, McAvoy, Wang, and Nowak.

Every edge carries a donation game, game 1 of benefit b1 or game 2 of benefit b2 < b1, at the same cost c. Players accumulate the payoffs of the games with all their neighbours, one player is replaced every time step by death-birth updating or pairwise comparison, and the game of every edge changes with the actions played on it: with the default transitions mutual cooperation leads to game 1 and every other outcome to game 2 (arXiv 1905.10269 section 2 and section 3.1). Under weak selection and death-birth updating on a k-regular graph, cooperation is favoured if b1 / c > k - xi (b1 - b2) / c with xi = (k - 1) / 2, lowered from the rule b1 / c > k of a single game, and with the probabilities p and q of game 2 after mutual cooperation and after one defection, xi = (k - 1) q / 2 - (k + 1) p / 2 (Eq. 1 and Eq. 3). Under pairwise comparison cooperation is favoured if (b1 - b2) / (2 c) > 1 (Eq. 2), whatever b1, and never with a single game. The defaults b1 = 3.5, b2 = b1 - 0.9, and k = 4 lie in the range of Fig. 2 where transitions favour cooperation, since 3.5 > 4 - 1.5 * 0.9, and game 1 alone does not, since 3.5 < 4.

Parameters:

Name Type Description Default
n_players int

The number of players.

100
degree int

The number k of neighbours of every player.

4
first_benefit float

The benefit b1 of game 1.

3.5
second_benefit float

The benefit b2 of game 2.

2.6
cost float

The cost c of cooperating.

1.0
selection float

The intensity delta of selection.

0.1
after_cooperation float

The probability p of game 2 after mutual cooperation.

0.0
after_defection float

The probability q of game 2 after one cooperation and one defection.

1.0
transitions bool

Whether the games change, or every edge plays game 1 throughout.

True
update str

'death-birth' or 'pairwise'.

'death-birth'
share float

The share of cooperators at the start.

0.5
random_state int | Generator | None

The seed or generator of the graph and of the initial strategies.

0

Returns:

Name Type Description
system System

The system, with the group players on the nodes of the graph, and the reporter evolution.game_one.

Examples:

>>> from skcomplex.systems.social.cooperation import game_transitions
>>> game_transitions(20).elements[0].prepare().size_
20
Source code in src/skcomplex/systems/social/cooperation/_game_transitions.py
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
@register_system(
    field='social/cooperation',
    tags=('network', 'games', 'cooperation', 'game transitions', 'evolution'),
    references=('Su, McAvoy, Wang, and Nowak 2019',),
    defaults={'until': 100},
)
def game_transitions(
    n_players: int = 100,
    *,
    degree: int = 4,
    first_benefit: float = 3.5,
    second_benefit: float = 2.6,
    cost: float = 1.0,
    selection: float = 0.1,
    after_cooperation: float = 0.0,
    after_defection: float = 1.0,
    transitions: bool = True,
    update: str = 'death-birth',
    share: float = 0.5,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return evolutionary dynamics with game transitions on a random regular graph, after Su, McAvoy, Wang, and Nowak.

    Every edge carries a donation game, game 1 of benefit `b1` or game 2 of benefit `b2 < b1`, at the same cost `c`.
    Players accumulate the payoffs of the games with all their neighbours, one player is replaced every time step by
    death-birth updating or pairwise comparison, and the game of every edge changes with the actions played on it:
    with the default transitions mutual cooperation leads to game 1 and every other outcome to game 2 (arXiv 1905.10269
    section 2 and section 3.1). Under weak selection and death-birth updating on a `k`-regular graph, cooperation is
    favoured if `b1 / c > k - xi (b1 - b2) / c` with `xi = (k - 1) / 2`, lowered from the rule `b1 / c > k` of a
    single game, and with the probabilities `p` and `q` of game 2 after mutual cooperation and after one defection,
    `xi = (k - 1) q / 2 - (k + 1) p / 2` (Eq. 1 and Eq. 3). Under pairwise comparison cooperation is favoured if
    `(b1 - b2) / (2 c) > 1` (Eq. 2), whatever `b1`, and never with a single game. The defaults `b1 = 3.5`,
    `b2 = b1 - 0.9`, and `k = 4` lie in the range of Fig. 2 where transitions favour cooperation, since
    `3.5 > 4 - 1.5 * 0.9`, and game 1 alone does not, since `3.5 < 4`.

    Args:
        n_players:
            The number of players.

        degree:
            The number `k` of neighbours of every player.

        first_benefit:
            The benefit `b1` of game 1.

        second_benefit:
            The benefit `b2` of game 2.

        cost:
            The cost `c` of cooperating.

        selection:
            The intensity `delta` of selection.

        after_cooperation:
            The probability `p` of game 2 after mutual cooperation.

        after_defection:
            The probability `q` of game 2 after one cooperation and one defection.

        transitions:
            Whether the games change, or every edge plays game 1 throughout.

        update:
            `'death-birth'` or `'pairwise'`.

        share:
            The share of cooperators at the start.

        random_state:
            The seed or generator of the graph and of the initial strategies.

    Returns:
        system:
            The system, with the group `players` on the nodes of the graph, and the reporter `evolution.game_one`.

    Examples:
        >>> from skcomplex.systems.social.cooperation import game_transitions
        >>> game_transitions(20).elements[0].prepare().size_
        20
    """
    rng = np.random.default_rng(random_state)
    graph = nx.random_regular_graph(degree, n_players, seed=int(rng.integers(2**31)))
    players = GroupElement(
        'players',
        element=DonorElement,
        size=n_players,
        values={'strategy': (rng.random(n_players) < share).astype(float)},
    )
    evolution = GameTransitionsInteraction(
        'evolution',
        target='players',
        first_benefit=first_benefit,
        second_benefit=second_benefit,
        cost=cost,
        selection=selection,
        after_cooperation=after_cooperation,
        after_defection=after_defection,
        transitions=transitions,
        update=update,
    )
    return System(elements=[players], interactions=[evolution], space=NetworkSpace(graph))

hypergraph_public_goods(n_nodes=400, *, sizes=(3, 5), hyperdegree=2.0, synergy=4.0, noise=0.5, share=0.5, seed=0)

Return the public goods game on a hypergraph, played in every hyperedge, with Fermi imitation.

Every hyperedge is a group that plays the public goods game: a cooperator pays 1, and the pot multiplied by synergy is shared by the members. Every step has two phases. In the phase play every player takes the sum of its shares minus its contributions over the hyperedges around it, and in the phase imitate every player copies a random neighbour, a member of one of its hyperedges, with the Fermi probability 1 / (1 + exp((P_own - P_other) / K)) of the payoffs of the same step, at the noise K. In a well-mixed population defection wins in a group of m for synergy < m, so cooperation needs a synergy near the sizes of the hyperedges.

Parameters:

Name Type Description Default
n_nodes int

The number of players.

400
sizes Sequence[int]

The sizes of the hyperedges.

(3, 5)
hyperdegree float

The mean number of hyperedges of every size around a player, whose members are drawn at random.

2.0
synergy float

The factor that multiplies the contributions of a hyperedge.

4.0
noise float

The noise of the Fermi imitation.

0.5
share float

The share of cooperators at the start.

0.5
seed int

The seed of the hypergraph and the initial strategies.

0

Returns:

Name Type Description
system System

The system, with the strategies players.strategy and the payoffs players.payoff.

Examples:

>>> from skcomplex.systems.social.cooperation import hypergraph_public_goods
>>> hypergraph_public_goods(30).elements[0].prepare().size_
30
Source code in src/skcomplex/systems/social/cooperation/_hypergraph_games.py
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
@register_system(
    field='social/cooperation',
    tags=('hypergraph', 'games', 'cooperation', 'public goods'),
    references=('Alvarez-Rodriguez et al. 2021', 'Majhi et al. 2022'),
    defaults={'until': 100, 'dt': 1},
)
def hypergraph_public_goods(
    n_nodes: int = 400,
    *,
    sizes: Sequence[int] = (3, 5),
    hyperdegree: float = 2.0,
    synergy: float = 4.0,
    noise: float = 0.5,
    share: float = 0.5,
    seed: int = 0,
) -> System:
    """Return the public goods game on a hypergraph, played in every hyperedge, with Fermi imitation.

    Every hyperedge is a group that plays the public goods game: a cooperator pays 1, and the pot multiplied by
    `synergy` is shared by the members. Every step has two phases. In the phase `play` every player takes the sum of
    its shares minus its contributions over the hyperedges around it, and in the phase `imitate` every player copies a
    random neighbour, a member of one of its hyperedges, with the Fermi probability `1 / (1 + exp((P_own - P_other) /
    K))` of the payoffs of the same step, at the noise `K`. In a well-mixed population defection wins in a group of `m`
    for `synergy < m`, so cooperation needs a synergy near the sizes of the hyperedges.

    Args:
        n_nodes:
            The number of players.

        sizes:
            The sizes of the hyperedges.

        hyperdegree:
            The mean number of hyperedges of every size around a player, whose members are drawn at random.

        synergy:
            The factor that multiplies the contributions of a hyperedge.

        noise:
            The noise of the Fermi imitation.

        share:
            The share of cooperators at the start.

        seed:
            The seed of the hypergraph and the initial strategies.

    Returns:
        system:
            The system, with the strategies `players.strategy` and the payoffs `players.payoff`.

    Examples:
        >>> from skcomplex.systems.social.cooperation import hypergraph_public_goods
        >>> hypergraph_public_goods(30).elements[0].prepare().size_
        30
    """
    rng = np.random.default_rng(seed)
    hypergraph = _hypergraph(n_nodes, sizes, hyperdegree, rng)
    graph = xgi.to_graph(hypergraph)
    strategies = (rng.random(n_nodes) < share).astype(float)
    players = LatticeElement(
        'players',
        element=ContributorElement,
        space=NetworkSpace(graph, hyperedges=hypergraph),
        values={'strategy': strategies, 'synergy': synergy, 'noise': noise},
        visual=VisualSpec('sites', {'states': ['strategy'], 'low': 0.0, 'high': 1.0}),
    )
    imitation = ImitationInteraction(
        'imitation',
        target='players',
        state='strategy',
        rule='fermi',
        form='probability',
        intensity=1 / noise,
        phase='imitate',
    )
    return System(
        elements=[players],
        interactions=[HyperedgeGoodsInteraction('play', target='players', phase='play'), imitation],
        time=DiscreteTime(schedule=PhasesScheduleSpec(tuple(PhaseSpec(phase) for phase in PHASES))),
    )

iterated_lattice_dilemma(shape=(30, 30), *, rounds=7, reward=0.6, sucker=1.5, temptation=0.0, punishment=1.0, mutation=0.01, extremes=False, random_state=0)

Return Argun's evolutionary iterated prisoner's dilemma on a lattice, with strategies of n cooperation rounds.

The payoffs are years in prison, T < R < P < S, so lower is better. A strategy n cooperates until round n or until the opponent defects, and defects afterwards, so n = 0 always defects and n = N always cooperates. Against a strategy m the best reply is n = m - 1. Every step each player plays the N-round game with its four von Neumann neighbours, adopts the strategy of the lowest total among itself and its neighbours, random on ties, and mutates to a random strategy with the probability mu (Argun et al. 2021 sections 13.2 and 13.3). With every strategy allowed and S = 1.5, cooperative strategies prevail for R below about 0.5, all strategies coexist and cycle between about 0.5 and 0.75, and defecting strategies dominate above, so the default R = 0.6 lies in the middle of the cycling regime, where a finite lattice is not absorbed by defection. With only the two extreme strategies, extremes=True, cooperation dominates at R = 0.82, coexists with defection at R = 0.84, and vanishes at R = 0.86 (Exercise 13.3).

Parameters:

Name Type Description Default
shape Sequence[int]

The number of rows and columns of the periodic lattice.

(30, 30)
rounds int

The number N of rounds of a game.

7
reward float

The punishment R of mutual cooperation.

0.6
sucker float

The punishment S of a betrayed cooperator.

1.5
temptation float

The punishment T of a defector who betrays.

0.0
punishment float

The punishment P of mutual defection.

1.0
mutation float

The probability mu of a mutation to a random strategy per player and step.

0.01
extremes bool

Whether only the strategies 0, always defect, and N, always cooperate, are allowed.

False
random_state int | Generator | None

The seed or generator of the initial strategies.

0

Returns:

Name Type Description
system System

The system, with the strategies players.strategy.

Examples:

>>> from skcomplex.systems.social.cooperation import iterated_lattice_dilemma
>>> iterated_lattice_dilemma((10, 10)).elements[0].prepare().size_
100
Source code in src/skcomplex/systems/social/cooperation/_spatial_games.py
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
@register_system(
    field='social/cooperation',
    tags=('lattice', 'games', 'cooperation', 'iterated', 'mutation'),
    references=('Argun et al. 2021', 'Axelrod and Hamilton 1981'),
    defaults={'until': 100, 'dt': 1},
)
def iterated_lattice_dilemma(
    shape: Sequence[int] = (30, 30),
    *,
    rounds: int = 7,
    reward: float = 0.6,
    sucker: float = 1.5,
    temptation: float = 0.0,
    punishment: float = 1.0,
    mutation: float = 0.01,
    extremes: bool = False,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return Argun's evolutionary iterated prisoner's dilemma on a lattice, with strategies of `n` cooperation rounds.

    The payoffs are years in prison, `T < R < P < S`, so lower is better. A strategy `n` cooperates until round `n`
    or until the opponent defects, and defects afterwards, so `n = 0` always defects and `n = N` always cooperates.
    Against a strategy `m` the best reply is `n = m - 1`. Every step each player plays the `N`-round game with its four
    von Neumann neighbours, adopts the strategy of the lowest total among itself and its neighbours, random on ties,
    and mutates to a random strategy with the probability `mu` (Argun et al. 2021 sections 13.2 and 13.3). With every
    strategy allowed and `S = 1.5`, cooperative strategies prevail for `R` below about 0.5, all strategies coexist and
    cycle between about 0.5 and 0.75, and defecting strategies dominate above, so the default `R = 0.6` lies in the
    middle of the cycling regime, where a finite lattice is not absorbed by defection. With only the two extreme
    strategies,
    `extremes=True`, cooperation dominates at `R = 0.82`, coexists with defection at `R = 0.84`, and vanishes at
    `R = 0.86` (Exercise 13.3).

    Args:
        shape:
            The number of rows and columns of the periodic lattice.

        rounds:
            The number `N` of rounds of a game.

        reward:
            The punishment `R` of mutual cooperation.

        sucker:
            The punishment `S` of a betrayed cooperator.

        temptation:
            The punishment `T` of a defector who betrays.

        punishment:
            The punishment `P` of mutual defection.

        mutation:
            The probability `mu` of a mutation to a random strategy per player and step.

        extremes:
            Whether only the strategies 0, always defect, and `N`, always cooperate, are allowed.

        random_state:
            The seed or generator of the initial strategies.

    Returns:
        system:
            The system, with the strategies `players.strategy`.

    Examples:
        >>> from skcomplex.systems.social.cooperation import iterated_lattice_dilemma
        >>> iterated_lattice_dilemma((10, 10)).elements[0].prepare().size_
        100
    """
    rng = np.random.default_rng(random_state)
    if extremes:
        strategies = rounds * rng.integers(2, size=tuple(shape)).astype(float)
    else:
        strategies = rng.integers(rounds + 1, size=tuple(shape)).astype(float)
    players = LatticeElement(
        'players',
        element=IteratedPrisonerElement,
        space=LatticeSpace(tuple(shape), neighborhood='von_neumann'),
        values={
            'strategy': strategies,
            'rounds': rounds,
            'reward': reward,
            'sucker': sucker,
            'temptation': temptation,
            'punishment': punishment,
            'mutation': mutation,
            'extremes': float(extremes),
        },
    )
    return System(elements=[players], interactions=[IteratedDilemmaInteraction('play', target='players')])

nonlinear_pairwise_group_conflict(cells=100, *, victory='fermi', group_strength=2.0, benefit=2.0, cost=1.0, punishment=0.5, interaction_cost=0.1, fixed_cost=0.0, sensitivity=2.0, exponent=1.0)

Return the density of groups of defectors and altruistic punishers under pairwise group conflict.

The density f(z) of groups with a fraction z of punishers is advected by the replicator dynamics within groups, z (1 - z) (pi_P - pi_D), and groups win pairwise conflicts, copying themselves over the loser, at the rate group_strength f(z) int (rho(z, u) - rho(u, z)) f(u) du. The victory probability rho is not additively separable: the group Fermi rule (1 + tanh(s (G(z) - G(u)))) / 2 ('fermi'), the local rule normalized by the payoffs of both groups ('local'), or the Tullock contest function of the payoffs above their minimum with the exponent 1 / a ('tullock'), where G(z) is the average payoff of a group (Cooney 2024, arXiv 2405.18419 §3.1 and §6). The density starts uniform, and the integral against the two-point kernel rho(z, u) - rho(u, z) keeps its total at one.

Parameters:

Name Type Description Default
cells int

The number of cells of the axis of the fraction of punishers.

100
victory str

The victory probability, 'fermi', 'local', or 'tullock'.

'fermi'
group_strength float

The relative strength lambda of between-group conflict.

2.0
benefit float

The benefit b of the donation game.

2.0
cost float

The cost c of cooperating.

1.0
punishment float

The punishment p of a defector by every punisher it meets.

0.5
interaction_cost float

The cost k a punisher pays per defector it meets.

0.1
fixed_cost float

The fixed cost q of being able to punish.

0.0
sensitivity float

The sensitivity s of the Fermi rule.

2.0
exponent float

The exponent a of the Tullock contest function.

1.0

Returns:

Name Type Description
system System

The system, with the density groups.f.

Raises:

Type Description
ValidationError

The victory probability is unknown.

Examples:

>>> from skcomplex.systems.social.cooperation import nonlinear_pairwise_group_conflict
>>> nonlinear_pairwise_group_conflict(40).elements[0].prepare().size_
40
Source code in src/skcomplex/systems/social/cooperation/_multilevel_selection.py
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
@register_system(
    field='social/cooperation',
    tags=('field', 'multilevel selection', 'punishment', 'integral operator', 'pde'),
    references=REFERENCES,
    defaults={'until': 15.0, 'dt': 0.5, 'solver': RK4Solver(dt=0.01)},
)
def nonlinear_pairwise_group_conflict(
    cells: int = 100,
    *,
    victory: str = 'fermi',
    group_strength: float = 2.0,
    benefit: float = 2.0,
    cost: float = 1.0,
    punishment: float = 0.5,
    interaction_cost: float = 0.1,
    fixed_cost: float = 0.0,
    sensitivity: float = 2.0,
    exponent: float = 1.0,
) -> System:
    """Return the density of groups of defectors and altruistic punishers under pairwise group conflict.

    The density `f(z)` of groups with a fraction `z` of punishers is advected by the replicator dynamics within groups,
    `z (1 - z) (pi_P - pi_D)`, and groups win pairwise conflicts, copying themselves over the loser, at the rate
    `group_strength f(z) int (rho(z, u) - rho(u, z)) f(u) du`. The victory probability `rho` is not additively
    separable: the group Fermi rule `(1 + tanh(s (G(z) - G(u)))) / 2` (`'fermi'`), the local rule normalized by the
    payoffs of both groups (`'local'`), or the Tullock contest function of the payoffs above their minimum with the
    exponent `1 / a` (`'tullock'`), where `G(z)` is the average payoff of a group (Cooney 2024, arXiv 2405.18419 §3.1
    and §6). The density starts uniform, and the integral against the two-point kernel `rho(z, u) - rho(u, z)` keeps
    its total at one.

    Args:
        cells:
            The number of cells of the axis of the fraction of punishers.

        victory:
            The victory probability, `'fermi'`, `'local'`, or `'tullock'`.

        group_strength:
            The relative strength `lambda` of between-group conflict.

        benefit:
            The benefit `b` of the donation game.

        cost:
            The cost `c` of cooperating.

        punishment:
            The punishment `p` of a defector by every punisher it meets.

        interaction_cost:
            The cost `k` a punisher pays per defector it meets.

        fixed_cost:
            The fixed cost `q` of being able to punish.

        sensitivity:
            The sensitivity `s` of the Fermi rule.

        exponent:
            The exponent `a` of the Tullock contest function.

    Returns:
        system:
            The system, with the density `groups.f`.

    Raises:
        ValidationError:
            The victory probability is unknown.

    Examples:
        >>> from skcomplex.systems.social.cooperation import nonlinear_pairwise_group_conflict
        >>> nonlinear_pairwise_group_conflict(40).elements[0].prepare().size_
        40
    """
    _check_victory(victory, PAIRWISE_VICTORIES)
    payoffs = (benefit, cost, punishment, interaction_cost, fixed_cost)
    z, u = coordinate('z'), integration_variable('z')
    win = _get_pairwise_victory(z, u, payoffs, victory=victory, sensitivity=sensitivity, exponent=exponent)
    loss = _get_pairwise_victory(u, z, payoffs, victory=victory, sensitivity=sensitivity, exponent=exponent)
    gain = win - loss
    advantage = (punishment + interaction_cost) * z - (cost + fixed_cost + interaction_cost)
    space = PhaseSpace(LatticeSpace((1,)), [AxisSpec('z', cells, 0.0, 1.0)])
    groups = FieldElement(
        'groups',
        element=DensityElement,
        space=space,
        values={'f': np.ones((1, cells))},
        discretization='finite-volume',
    )
    within = AdvectionInteraction(
        'within',
        target='groups',
        state='f',
        drift=lambda _: [0, z * (1 - z) * advantage],
        limiter=None,
    )
    between = RateInteraction(
        'between',
        target='groups',
        state='f',
        rate=lambda s: group_strength * s.target.f * integral_transform(s.target.f, gain, ('z',)),
    )
    return System(elements=[groups], interactions=[within, between])

play(first, second, rounds=6)

Return the scores of two genomes in an iterated prisoner's dilemma.

A genome holds seven moves, 1 to cooperate and 0 to defect, for the opponent's last two moves: none, C, D, CC, CD, DC, and DD. A round pays 3 to both cooperators, 1 to both defectors, and 5 to a defector against a cooperator, who gets 0.

Parameters:

Name Type Description Default
first ArrayLike

The genome of the first player.

required
second ArrayLike

The genome of the second player.

required
rounds int

The number of rounds.

6

Returns:

Name Type Description
scores tuple[float, float]

The total scores of the two players.

Examples:

>>> from skcomplex.systems.social.cooperation import play
>>> play([1] * 7, [0] * 7)
(0.0, 30.0)
Source code in src/skcomplex/systems/social/cooperation/_tournaments.py
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
def play(first: npt.ArrayLike, second: npt.ArrayLike, rounds: int = 6) -> tuple[float, float]:
    """Return the scores of two genomes in an iterated prisoner's dilemma.

    A genome holds seven moves, 1 to cooperate and 0 to defect, for the opponent's last two moves: none, C, D, CC,
    CD, DC, and DD. A round pays 3 to both cooperators, 1 to both defectors, and 5 to a defector against a cooperator,
    who gets 0.

    Args:
        first:
            The genome of the first player.

        second:
            The genome of the second player.

        rounds:
            The number of rounds.

    Returns:
        scores:
            The total scores of the two players.

    Examples:
        >>> from skcomplex.systems.social.cooperation import play
        >>> play([1] * 7, [0] * 7)
        (0.0, 30.0)
    """
    genomes = (np.asarray(first), np.asarray(second))
    moves: tuple[list[int], list[int]] = ([], [])
    scores = [0.0, 0.0]
    for _ in range(rounds):
        move = (int(genomes[0][_gene(moves[1])]), int(genomes[1][_gene(moves[0])]))
        payoff = PAYOFFS[move]
        scores = [scores[0] + payoff[0], scores[1] + payoff[1]]
        moves[0].append(move[0])
        moves[1].append(move[1])
    return scores[0], scores[1]

prisoners_dilemma_tournament(n_agents=100, *, rounds=6, death=0.1, mutation=0.05, random_state=0)

Return the evolution of strategies of the iterated prisoner's dilemma from random genomes.

Parameters:

Name Type Description Default
n_agents int

The number of agents.

100
rounds int

The number of rounds of every game.

6
death float

The probability that an agent dies every step.

0.1
mutation float

The probability that an offspring has one gene flipped.

0.05
random_state int | Generator | None

The seed or generator of the initial genomes.

0

Returns:

Name Type Description
system System

The system, with the group agents, the tournament tournament, and the death-birth replacement selection.

Examples:

>>> from skcomplex.systems.social.cooperation import prisoners_dilemma_tournament
>>> prisoners_dilemma_tournament(10, random_state=0).elements[0].prepare().size_
10
Source code in src/skcomplex/systems/social/cooperation/_tournaments.py
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
@register_system(
    field='social/cooperation',
    tags=('agents', 'games', 'cooperation', 'evolution'),
    references=('Axelrod 1984', 'Downey 2018'),
    defaults={'until': 50},
)
def prisoners_dilemma_tournament(
    n_agents: int = 100,
    *,
    rounds: int = 6,
    death: float = 0.1,
    mutation: float = 0.05,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the evolution of strategies of the iterated prisoner's dilemma from random genomes.

    Args:
        n_agents:
            The number of agents.

        rounds:
            The number of rounds of every game.

        death:
            The probability that an agent dies every step.

        mutation:
            The probability that an offspring has one gene flipped.

        random_state:
            The seed or generator of the initial genomes.

    Returns:
        system:
            The system, with the group `agents`, the tournament `tournament`, and the death-birth replacement
            `selection`.

    Examples:
        >>> from skcomplex.systems.social.cooperation import prisoners_dilemma_tournament
        >>> prisoners_dilemma_tournament(10, random_state=0).elements[0].prepare().size_
        10
    """
    genomes = np.random.default_rng(random_state).integers(0, 2, (n_agents, GENES)).astype(float)
    visual = VisualSpec('histograms', {'color_by': 'fitness'})
    agents = GroupElement(
        'agents',
        element=StrategistElement,
        size=n_agents,
        values={'genome': genomes},
        visual=visual,
    )
    return System(
        elements=[agents],
        interactions=[
            TournamentInteraction('tournament', target='agents', rounds=rounds),
            EvolutionInteraction(
                'selection',
                target='agents',
                strategy='genome',
                selection='proportional',
                replacement='death-birth',
                mutation=mutation,
                death=death,
                change='one',
                parents='all',
            ),
        ],
    )

public_goods_game(shape=(50, 50), *, synergy=4.5, noise=0.5, share=0.5, random_state=0)

Return the spatial public goods game in groups of five neighbours, with Fermi imitation.

Every step has two phases. In the phase play every player takes the payoff of its five groups, and in the phase imitate every player copies a random neighbour with the Fermi probability 1 / (1 + exp((P_own - P_other) / K)) of the payoffs of the same step, at the noise K.

Parameters:

Name Type Description Default
shape Sequence[int]

The number of rows and columns of the periodic lattice.

(50, 50)
synergy float

The factor that multiplies the contributions of a group.

4.5
noise float

The noise of the Fermi imitation.

0.5
share float

The share of cooperators at the start.

0.5
random_state int | Generator | None

The seed or generator of the initial strategies.

0

Returns:

Name Type Description
system System

The system, with the strategies players.strategy and the payoffs players.payoff.

Examples:

>>> from skcomplex.systems.social.cooperation import public_goods_game
>>> public_goods_game((10, 10)).elements[0].prepare().size_
100
Source code in src/skcomplex/systems/social/cooperation/_spatial_games.py
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
@register_system(
    field='social/cooperation',
    tags=('lattice', 'games', 'cooperation', 'public goods'),
    references=('Szabó and Hauert 2002', 'Szabó and Fáth 2007'),
    defaults={'until': 50, 'dt': 1},
)
def public_goods_game(
    shape: Sequence[int] = (50, 50),
    *,
    synergy: float = 4.5,
    noise: float = 0.5,
    share: float = 0.5,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the spatial public goods game in groups of five neighbours, with Fermi imitation.

    Every step has two phases. In the phase `play` every player takes the payoff of its five groups, and in the phase
    `imitate` every player copies a random neighbour with the Fermi probability `1 / (1 + exp((P_own - P_other) / K))`
    of the payoffs of the same step, at the noise `K`.

    Args:
        shape:
            The number of rows and columns of the periodic lattice.

        synergy:
            The factor that multiplies the contributions of a group.

        noise:
            The noise of the Fermi imitation.

        share:
            The share of cooperators at the start.

        random_state:
            The seed or generator of the initial strategies.

    Returns:
        system:
            The system, with the strategies `players.strategy` and the payoffs `players.payoff`.

    Examples:
        >>> from skcomplex.systems.social.cooperation import public_goods_game
        >>> public_goods_game((10, 10)).elements[0].prepare().size_
        100
    """
    strategies = _random_strategies(shape, share, random_state)
    players = LatticeElement(
        'players',
        element=ContributorElement,
        space=LatticeSpace(tuple(shape), neighborhood='von_neumann'),
        values={'strategy': strategies, 'synergy': synergy, 'noise': noise},
    )
    imitation = ImitationInteraction(
        'imitation',
        target='players',
        state='strategy',
        rule='fermi',
        form='probability',
        intensity=1 / noise,
        phase='imitate',
    )
    time = DiscreteTime(schedule=PhasesScheduleSpec((PhaseSpec('play'), PhaseSpec('imitate'))))
    return System(
        elements=[players],
        interactions=[PublicGoodsInteraction('play', target='players', phase='play'), imitation],
        time=time,
    )

simplicial_social_dilemmas(n_nodes=500, *, simplices=0.5, sucker=-0.2, temptation=1.2, sucker_allied=0.5, temptation_allied=0.8, sucker_opposed=None, temptation_opposed=None, noise=0.1, share=0.5, seed=0)

Return the social dilemmas of Guo et al. on the links and the 2-simplices of a simplicial complex.

The network grows from a complete graph on five nodes, every new node joining both ends of a random link, which forms one new triangle. A share simplices of the triangles, drawn at random, are 2-simplices, and the others are three links. Players play game 1 along the links of a plain triangle, and in a 2-simplex game 2 against a member when the third member shares their strategy and game 3 otherwise, every game paying 1 for mutual cooperation, 0 for mutual defection, and S and T to a cooperator and a defector that meet. The payoff of a player is the mean over the two links of every triangle around it. Every step has two phases: in play every player takes its payoff, and in imitate every player copies a random neighbour with the Fermi probability 1 / (1 + exp((P_own - P_other) / K)) at the noise K.

The review of Majhi et al. writes the payoff as a mean over the links of a player of the mean over the triangles of each link, and picks game 2 when s_ki = s_kj. This system takes the mean over the links of the triangles, which counts a link once per triangle, and reads the condition as the third member sharing the strategy of the player.

Parameters:

Name Type Description Default
n_nodes int

The number of players.

500
simplices float

The share of the triangles that are 2-simplices.

0.5
sucker float

The sucker's payoff S1 of game 1.

-0.2
temptation float

The temptation T1 of game 1.

1.2
sucker_allied float

The sucker's payoff S2 of game 2.

0.5
temptation_allied float

The temptation T2 of game 2.

0.8
sucker_opposed float | None

The sucker's payoff S3 of game 3, or None for S1.

None
temptation_opposed float | None

The temptation T3 of game 3, or None for T1.

None
noise float

The noise of the Fermi imitation.

0.1
share float

The share of cooperators at the start.

0.5
seed int

The seed of the network, the 2-simplices, and the initial strategies.

0

Returns:

Name Type Description
system System

The system, with the strategies players.strategy and the payoffs players.payoff.

Examples:

>>> from skcomplex.systems.social.cooperation import simplicial_social_dilemmas
>>> simplicial_social_dilemmas(30).elements[0].prepare().size_
30
Source code in src/skcomplex/systems/social/cooperation/_hypergraph_games.py
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
@register_system(
    field='social/cooperation',
    tags=('simplicial complex', 'games', 'cooperation', 'social dilemmas'),
    references=('Guo et al. 2021', 'Majhi et al. 2022'),
    defaults={'until': 100, 'dt': 1},
)
def simplicial_social_dilemmas(
    n_nodes: int = 500,
    *,
    simplices: float = 0.5,
    sucker: float = -0.2,
    temptation: float = 1.2,
    sucker_allied: float = 0.5,
    temptation_allied: float = 0.8,
    sucker_opposed: float | None = None,
    temptation_opposed: float | None = None,
    noise: float = 0.1,
    share: float = 0.5,
    seed: int = 0,
) -> System:
    """Return the social dilemmas of Guo et al. on the links and the 2-simplices of a simplicial complex.

    The network grows from a complete graph on five nodes, every new node joining both ends of a random link, which
    forms one new triangle. A share `simplices` of the triangles, drawn at random, are 2-simplices, and the others are
    three links. Players play game 1 along the links of a plain triangle, and in a 2-simplex game 2 against a member
    when the third member shares their strategy and game 3 otherwise, every game paying 1 for mutual cooperation, 0
    for mutual defection, and `S` and `T` to a cooperator and a defector that meet. The payoff of a player is the mean
    over the two links of every triangle around it. Every step has two phases: in `play` every player takes its
    payoff, and in `imitate` every player copies a random neighbour with the Fermi probability `1 / (1 + exp((P_own -
    P_other) / K))` at the noise `K`.

    The review of Majhi et al. writes the payoff as a mean over the links of a player of the mean over the triangles of
    each link, and picks game 2 when `s_ki = s_kj`. This system takes the mean over the links of the triangles, which
    counts a link once per triangle, and reads the condition as the third member sharing the strategy of the player.

    Args:
        n_nodes:
            The number of players.

        simplices:
            The share of the triangles that are 2-simplices.

        sucker:
            The sucker's payoff `S1` of game 1.

        temptation:
            The temptation `T1` of game 1.

        sucker_allied:
            The sucker's payoff `S2` of game 2.

        temptation_allied:
            The temptation `T2` of game 2.

        sucker_opposed:
            The sucker's payoff `S3` of game 3, or `None` for `S1`.

        temptation_opposed:
            The temptation `T3` of game 3, or `None` for `T1`.

        noise:
            The noise of the Fermi imitation.

        share:
            The share of cooperators at the start.

        seed:
            The seed of the network, the 2-simplices, and the initial strategies.

    Returns:
        system:
            The system, with the strategies `players.strategy` and the payoffs `players.payoff`.

    Examples:
        >>> from skcomplex.systems.social.cooperation import simplicial_social_dilemmas
        >>> simplicial_social_dilemmas(30).elements[0].prepare().size_
        30
    """
    rng = np.random.default_rng(seed)
    graph, triangles = _triangle_attachment(n_nodes, rng)
    filled = rng.random(len(triangles)) < simplices
    hyperedges: list[tuple[int, ...]] = []
    for triangle, simplex in zip(triangles, filled, strict=True):
        hyperedges.extend([triangle] if simplex else itertools.combinations(triangle, PAIR))
    held = np.bincount(np.asarray(triangles).ravel(), minlength=n_nodes).astype(float)
    players = LatticeElement(
        'players',
        element=DilemmaElement,
        space=NetworkSpace(graph, hyperedges=hyperedges),
        values={'strategy': (rng.random(n_nodes) < share).astype(float), 'triangles': held},
        visual=VisualSpec('sites', {'states': ['strategy'], 'low': 0.0, 'high': 1.0}),
    )
    play = SimplicialDilemmaInteraction(
        'play',
        target='players',
        sucker=sucker,
        temptation=temptation,
        sucker_allied=sucker_allied,
        temptation_allied=temptation_allied,
        sucker_opposed=sucker if sucker_opposed is None else sucker_opposed,
        temptation_opposed=temptation if temptation_opposed is None else temptation_opposed,
        phase='play',
    )
    imitation = ImitationInteraction(
        'imitation',
        target='players',
        state='strategy',
        rule='fermi',
        form='probability',
        intensity=1 / noise,
        phase='imitate',
    )
    time = DiscreteTime(schedule=PhasesScheduleSpec(tuple(PhaseSpec(phase) for phase in PHASES)))
    return System(elements=[players], interactions=[play, imitation], time=time)

spatial_prisoners_dilemma(shape=(50, 50), *, temptation=1.85, imitation='best', noise=0.1, share=0.9, random_state=0)

Return the spatial prisoner's dilemma of Nowak and May, with best-neighbour or Fermi imitation.

Parameters:

Name Type Description Default
shape Sequence[int]

The number of rows and columns of the periodic lattice.

(50, 50)
temptation float

The payoff of a defector against a cooperator.

1.85
imitation str

'best' to copy the best neighbour, or 'fermi' to copy a random neighbour with the Fermi probability.

'best'
noise float

The noise of the Fermi imitation.

0.1
share float

The share of cooperators at the start.

0.9
random_state int | Generator | None

The seed or generator of the initial strategies.

0

Returns:

Name Type Description
system System

The system, with the strategies players.strategy.

Examples:

>>> from skcomplex.systems.social.cooperation import spatial_prisoners_dilemma
>>> spatial_prisoners_dilemma((10, 10)).elements[0].prepare().size_
100
Source code in src/skcomplex/systems/social/cooperation/_spatial_games.py
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
@register_system(
    field='social/cooperation',
    tags=('lattice', 'games', 'cooperation', 'evolution'),
    references=('Nowak and May 1992', 'Szabó and Fáth 2007', 'Argun et al. 2021', 'Gros 2015'),
    defaults={'until': 50, 'dt': 1},
)
def spatial_prisoners_dilemma(
    shape: Sequence[int] = (50, 50),
    *,
    temptation: float = 1.85,
    imitation: str = 'best',
    noise: float = 0.1,
    share: float = 0.9,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the spatial prisoner's dilemma of Nowak and May, with best-neighbour or Fermi imitation.

    Args:
        shape:
            The number of rows and columns of the periodic lattice.

        temptation:
            The payoff of a defector against a cooperator.

        imitation:
            `'best'` to copy the best neighbour, or `'fermi'` to copy a random neighbour with the Fermi probability.

        noise:
            The noise of the Fermi imitation.

        share:
            The share of cooperators at the start.

        random_state:
            The seed or generator of the initial strategies.

    Returns:
        system:
            The system, with the strategies `players.strategy`.

    Examples:
        >>> from skcomplex.systems.social.cooperation import spatial_prisoners_dilemma
        >>> spatial_prisoners_dilemma((10, 10)).elements[0].prepare().size_
        100
    """
    players = LatticeElement(
        'players',
        element=PrisonerElement,
        space=LatticeSpace(tuple(shape)),
        values={
            'strategy': _random_strategies(shape, share, random_state),
            'temptation': temptation,
            'noise': noise,
        },
    )
    return System(
        elements=[players],
        interactions=[PrisonersDilemmaInteraction('play', target='players', imitation=imitation)],
    )

trimorphic_multilevel_selection(cells=30, *, victory='defectors', group_strength=4.0, benefit=3.0, cost=1.0, punishment=0.5, interaction_cost=0.1, fixed_cost=0.0)

Return the density of groups of cooperators, defectors, and altruistic punishers on the 2-simplex.

The density f(x, y) of groups with fractions x of cooperators, y of defectors, and 1 - x - y of punishers lives on the 2-simplex. The replicator dynamics within groups advect it, and groups win pairwise conflicts at the rate group_strength f(x, y) int (rho(x, y; u, v) - rho(u, v; x, y)) f(u, v) du dv over the simplex. The victory probability is 1/2 + (v - y) / 2 of the fraction of non-defectors ('defectors'), or the difference of the average payoffs normalized by their largest difference on the simplex ('payoff') (Cooney 2024, arXiv 2405.18419 §3.2 and §7). The density starts uniform at two, so its integral is one, the cells along the slanted face are half cells, and no flux leaves the simplex.

Parameters:

Name Type Description Default
cells int

The number of cells of each axis of the simplex.

30
victory str

The victory probability, 'defectors' or 'payoff'.

'defectors'
group_strength float

The relative strength lambda of between-group conflict.

4.0
benefit float

The benefit b of the donation game.

3.0
cost float

The cost c of cooperating.

1.0
punishment float

The punishment p of a defector by every punisher it meets.

0.5
interaction_cost float

The cost k a punisher pays per defector it meets.

0.1
fixed_cost float

The fixed cost q of being able to punish.

0.0

Returns:

Name Type Description
system System

The system, with the density groups.f.

Raises:

Type Description
ValidationError

The victory probability is unknown.

Examples:

>>> from skcomplex.systems.social.cooperation import trimorphic_multilevel_selection
>>> trimorphic_multilevel_selection(10).elements[0].prepare().size_
100
Source code in src/skcomplex/systems/social/cooperation/_multilevel_selection.py
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
@register_system(
    field='social/cooperation',
    tags=('field', 'multilevel selection', 'punishment', 'simplex', 'integral operator', 'pde'),
    references=REFERENCES,
    defaults={'until': 30.0, 'dt': 1.0, 'solver': RK4Solver(dt=0.01)},
)
def trimorphic_multilevel_selection(
    cells: int = 30,
    *,
    victory: str = 'defectors',
    group_strength: float = 4.0,
    benefit: float = 3.0,
    cost: float = 1.0,
    punishment: float = 0.5,
    interaction_cost: float = 0.1,
    fixed_cost: float = 0.0,
) -> System:
    """Return the density of groups of cooperators, defectors, and altruistic punishers on the 2-simplex.

    The density `f(x, y)` of groups with fractions `x` of cooperators, `y` of defectors, and `1 - x - y` of punishers
    lives on the 2-simplex. The replicator dynamics within groups advect it, and groups win pairwise conflicts at the
    rate `group_strength f(x, y) int (rho(x, y; u, v) - rho(u, v; x, y)) f(u, v) du dv` over the simplex. The victory
    probability is `1/2 + (v - y) / 2` of the fraction of non-defectors (`'defectors'`), or the difference of the
    average payoffs normalized by their largest difference on the simplex (`'payoff'`) (Cooney 2024, arXiv 2405.18419
    §3.2 and §7). The density starts uniform at two, so its integral is one, the cells along the slanted face are half
    cells, and no flux leaves the simplex.

    Args:
        cells:
            The number of cells of each axis of the simplex.

        victory:
            The victory probability, `'defectors'` or `'payoff'`.

        group_strength:
            The relative strength `lambda` of between-group conflict.

        benefit:
            The benefit `b` of the donation game.

        cost:
            The cost `c` of cooperating.

        punishment:
            The punishment `p` of a defector by every punisher it meets.

        interaction_cost:
            The cost `k` a punisher pays per defector it meets.

        fixed_cost:
            The fixed cost `q` of being able to punish.

    Returns:
        system:
            The system, with the density `groups.f`.

    Raises:
        ValidationError:
            The victory probability is unknown.

    Examples:
        >>> from skcomplex.systems.social.cooperation import trimorphic_multilevel_selection
        >>> trimorphic_multilevel_selection(10).elements[0].prepare().size_
        100
    """
    _check_victory(victory, TRIMORPHIC_VICTORIES)
    payoffs = (benefit, cost, punishment, interaction_cost, fixed_cost)
    gain = _get_trimorphic_victory_gain(victory, payoffs)
    axes = [AxisSpec('x', cells, 0.0, 1.0), AxisSpec('y', cells, 0.0, 1.0)]
    space = SimplexSpace(LatticeSpace((1,)), axes)
    groups = FieldElement(
        'groups',
        element=DensityElement,
        space=space,
        values={'f': 2.0 * (space.prepare().volumes_ > 0)},
        discretization='finite-volume',
        visual=SIMPLEX_VISUAL,
    )
    drift = _get_simplex_drift(payoffs)
    within = AdvectionInteraction('within', target='groups', state='f', drift=lambda _: drift, limiter=None)
    between = RateInteraction(
        'between',
        target='groups',
        state='f',
        rate=lambda s: group_strength * s.target.f * integral_transform(s.target.f, gain, ('x', 'y')),
    )
    return System(elements=[groups], interactions=[within, between])

two_level_birth_death(groups=50, *, members=20, selection=0.1, conflict_rate=0.2, benefit=2.0, cost=1.0, punishment=0.5, interaction_cost=0.5, fixed_cost=0.0, random_state=0)

Return the finite two-level birth-death process of defectors and altruistic punishers in competing groups.

The population has m groups of n members. Within groups, punishers and defectors reproduce at the rates 1 + w pi_P(z) and 1 + w pi_D(z) of the fraction z of punishers, with pi_P = (b + k) z - (c + q + k) and pi_D = (b - p) z, and the offspring replaces a random member of the group. Every group fights a random other group at the rate Lambda and wins with the probability 1/2 + (z - u) / 2, and the winner's copy replaces the loser (Cooney 2024, arXiv 2405.18419 section 3.1 and appendix A.1). With many large groups, the density of the fractions of punishers follows two_level_replicator with lambda = Lambda / w, so between-group conflict sustains the punishers above lambda* = c + q - p and the defectors take over without it. The groups start with uniformly drawn numbers of punishers.

Parameters:

Name Type Description Default
groups int

The number m of groups.

50
members int

The number n of members of every group.

20
selection float

The intensity w of individual-level selection.

0.1
conflict_rate float

The rate Lambda at which a group fights another group.

0.2
benefit float

The benefit b of the donation game.

2.0
cost float

The cost c of cooperating.

1.0
punishment float

The punishment p of a defector by every punisher it meets.

0.5
interaction_cost float

The cost k a punisher pays per defector it meets.

0.5
fixed_cost float

The fixed cost q of being able to punish.

0.0
random_state int | Generator | None

The seed or generator of the initial groups.

0

Returns:

Name Type Description
system System

The system, with the group groups.

Examples:

>>> from skcomplex.systems.social.cooperation import two_level_birth_death
>>> two_level_birth_death(10).elements[0].prepare().size_
10
Source code in src/skcomplex/systems/social/cooperation/_two_level_birth_death.py
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
@register_system(
    field='social/cooperation',
    tags=('agents', 'multilevel selection', 'punishment', 'birth-death', 'gillespie'),
    references=('Cooney 2024', 'Luo 2014'),
    defaults={'until': 100},
)
def two_level_birth_death(
    groups: int = 50,
    *,
    members: int = 20,
    selection: float = 0.1,
    conflict_rate: float = 0.2,
    benefit: float = 2.0,
    cost: float = 1.0,
    punishment: float = 0.5,
    interaction_cost: float = 0.5,
    fixed_cost: float = 0.0,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the finite two-level birth-death process of defectors and altruistic punishers in competing groups.

    The population has `m` groups of `n` members. Within groups, punishers and defectors reproduce at the rates
    `1 + w pi_P(z)` and `1 + w pi_D(z)` of the fraction `z` of punishers, with `pi_P = (b + k) z - (c + q + k)` and
    `pi_D = (b - p) z`, and the offspring replaces a random member of the group. Every group fights a random other
    group at the rate `Lambda` and wins with the probability `1/2 + (z - u) / 2`, and the winner's copy replaces the
    loser (Cooney 2024, arXiv 2405.18419 section 3.1 and appendix A.1). With many large groups, the density of the
    fractions of punishers follows `two_level_replicator` with `lambda = Lambda / w`, so between-group conflict
    sustains the punishers above `lambda* = c + q - p` and the defectors take over without it. The groups start with
    uniformly drawn numbers of punishers.

    Args:
        groups:
            The number `m` of groups.

        members:
            The number `n` of members of every group.

        selection:
            The intensity `w` of individual-level selection.

        conflict_rate:
            The rate `Lambda` at which a group fights another group.

        benefit:
            The benefit `b` of the donation game.

        cost:
            The cost `c` of cooperating.

        punishment:
            The punishment `p` of a defector by every punisher it meets.

        interaction_cost:
            The cost `k` a punisher pays per defector it meets.

        fixed_cost:
            The fixed cost `q` of being able to punish.

        random_state:
            The seed or generator of the initial groups.

    Returns:
        system:
            The system, with the group `groups`.

    Examples:
        >>> from skcomplex.systems.social.cooperation import two_level_birth_death
        >>> two_level_birth_death(10).elements[0].prepare().size_
        10
    """
    rng = np.random.default_rng(random_state)
    population = GroupElement(
        'groups',
        element=PunisherGroupElement,
        size=groups,
        values={'punishers': rng.integers(members + 1, size=groups).astype(float)},
    )
    competition = TwoLevelBirthDeathInteraction(
        'competition',
        target='groups',
        members=members,
        selection=selection,
        conflict_rate=conflict_rate,
        benefit=benefit,
        cost=cost,
        punishment=punishment,
        interaction_cost=interaction_cost,
        fixed_cost=fixed_cost,
    )
    return System(elements=[population], interactions=[competition])

two_level_replicator(cells=100, *, victory='punishers', group_strength=2.0, benefit=2.0, cost=1.0, punishment=0.5, interaction_cost=0.5, fixed_cost=0.0)

Return the two-level replicator equation of defectors and altruistic punishers, additive pairwise group conflict.

The density f(z) of groups with a fraction z of punishers is advected by the replicator dynamics within groups, z (1 - z) (pi_P - pi_D), and groups replicate at the rate group_strength f(z) (G(z) - int G(u) f(u) du) (Cooney 2024, arXiv 2405.18419 Eq. 3.3). Pairwise group conflict with an additively separable victory probability rho(z, u) = 1/2 + (G(z) - G(u)) / 2 reduces to this equation (Eq. 3.11): G(z) = z for the probability of Boyd and coauthors based on the fraction of punishers ('punishers', Eq. 3.6 and section 5.1), or the average payoff of a group normalized by its range, G(z) / (G* - G_*) ('payoff', Eq. 3.8 and section 5.2). From a uniform density with G(z) = z and p < c + q, the groups concentrate at the all-defector state below the threshold lambda* = c + q - p, and above it the mean fraction of punishers tends to 1 - (c + q - p) / lambda (Eq. 5.4).

Parameters:

Name Type Description Default
cells int

The number of cells of the axis of the fraction of punishers.

100
victory str

The group-level victory probability, 'punishers' or 'payoff'.

'punishers'
group_strength float

The relative strength lambda of between-group competition.

2.0
benefit float

The benefit b of the donation game.

2.0
cost float

The cost c of cooperating.

1.0
punishment float

The punishment p of a defector by every punisher it meets.

0.5
interaction_cost float

The cost k a punisher pays per defector it meets.

0.5
fixed_cost float

The fixed cost q of being able to punish.

0.0

Returns:

Name Type Description
system System

The system, with the density groups.f.

Raises:

Type Description
ValidationError

The victory probability is unknown.

Examples:

>>> from skcomplex.systems.social.cooperation import two_level_replicator
>>> two_level_replicator(40).elements[0].prepare().size_
40
Source code in src/skcomplex/systems/social/cooperation/_multilevel_selection.py
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
@register_system(
    field='social/cooperation',
    tags=('field', 'multilevel selection', 'punishment', 'two-level replicator', 'pde'),
    references=REFERENCES,
    defaults={'until': 40.0, 'dt': 1.0, 'solver': RK4Solver(dt=0.01)},
)
def two_level_replicator(
    cells: int = 100,
    *,
    victory: str = 'punishers',
    group_strength: float = 2.0,
    benefit: float = 2.0,
    cost: float = 1.0,
    punishment: float = 0.5,
    interaction_cost: float = 0.5,
    fixed_cost: float = 0.0,
) -> System:
    """Return the two-level replicator equation of defectors and altruistic punishers, additive pairwise group conflict.

    The density `f(z)` of groups with a fraction `z` of punishers is advected by the replicator dynamics within groups,
    `z (1 - z) (pi_P - pi_D)`, and groups replicate at the rate `group_strength f(z) (G(z) - int G(u) f(u) du)`
    (Cooney 2024, arXiv 2405.18419 Eq. 3.3). Pairwise group conflict with an additively separable victory probability
    `rho(z, u) = 1/2 + (G(z) - G(u)) / 2` reduces to this equation (Eq. 3.11): `G(z) = z` for the probability of
    Boyd and coauthors based on the fraction of punishers (`'punishers'`, Eq. 3.6 and section 5.1), or the average
    payoff of a group normalized by its range, `G(z) / (G* - G_*)` (`'payoff'`, Eq. 3.8 and section 5.2). From a uniform
    density with `G(z) = z` and `p < c + q`, the groups concentrate at the all-defector state below the threshold
    `lambda* = c + q - p`, and above it the mean fraction of punishers tends to `1 - (c + q - p) / lambda` (Eq. 5.4).

    Args:
        cells:
            The number of cells of the axis of the fraction of punishers.

        victory:
            The group-level victory probability, `'punishers'` or `'payoff'`.

        group_strength:
            The relative strength `lambda` of between-group competition.

        benefit:
            The benefit `b` of the donation game.

        cost:
            The cost `c` of cooperating.

        punishment:
            The punishment `p` of a defector by every punisher it meets.

        interaction_cost:
            The cost `k` a punisher pays per defector it meets.

        fixed_cost:
            The fixed cost `q` of being able to punish.

    Returns:
        system:
            The system, with the density `groups.f`.

    Raises:
        ValidationError:
            The victory probability is unknown.

    Examples:
        >>> from skcomplex.systems.social.cooperation import two_level_replicator
        >>> two_level_replicator(40).elements[0].prepare().size_
        40
    """
    _check_victory(victory, ADDITIVE_VICTORIES)
    payoffs = (benefit, cost, punishment, interaction_cost, fixed_cost)
    z, u = coordinate('z'), integration_variable('z')
    gain = _get_group_rate(z, payoffs, victory) - _get_group_rate(u, payoffs, victory)
    advantage = (punishment + interaction_cost) * z - (cost + fixed_cost + interaction_cost)
    space = PhaseSpace(LatticeSpace((1,)), [AxisSpec('z', cells, 0.0, 1.0)])
    groups = FieldElement(
        'groups',
        element=DensityElement,
        space=space,
        values={'f': np.ones((1, cells))},
        discretization='finite-volume',
    )
    within = AdvectionInteraction(
        'within',
        target='groups',
        state='f',
        drift=lambda _: [0, z * (1 - z) * advantage],
        limiter=None,
    )
    between = RateInteraction(
        'between',
        target='groups',
        state='f',
        rate=lambda s: group_strength * s.target.f * integral_transform(s.target.f, gain, ('z',)),
    )
    return System(elements=[groups], interactions=[within, between])