Skip to content

Cooperation

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

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.models.social.cooperation import ContributorElement
>>> ContributorElement('player').synergy
4.5
Source code in src/skcomplex/models/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.models.social.cooperation import DilemmaElement
>>> DilemmaElement('player').triangles
1.0
Source code in src/skcomplex/models/social/cooperation/_dilemma.py
44
45
46
47
48
49
50
51
52
53
54
55
56
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

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.models.social.cooperation import HyperedgeGoodsInteraction
>>> HyperedgeGoodsInteraction('play', target='players').check().write
'payoff'
Source code in src/skcomplex/models/social/cooperation/_hyperedge_goods.py
40
41
42
43
44
45
46
47
48
49
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,
    )

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.models.social.cooperation import PrisonerElement
>>> PrisonerElement('player').temptation
1.85
Source code in src/skcomplex/models/social/cooperation/_prisoner.py
44
45
46
47
48
49
50
51
52
53
54
55
56
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 check.

Examples:

>>> from skcomplex.models.social.cooperation import PrisonersDilemmaInteraction
>>> PrisonersDilemmaInteraction('play', target='players', imitation='fermi').check().imitation
'fermi'
Source code in src/skcomplex/models/social/cooperation/_prisoners_dilemma.py
57
58
59
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.models.social.cooperation import PublicGoodsInteraction
>>> PublicGoodsInteraction('play', target='players').check().write
'payoff'
Source code in src/skcomplex/models/social/cooperation/_public_goods.py
40
41
42
43
44
45
46
47
48
49
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,
    )

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.models.social.cooperation import SimplicialDilemmaInteraction
>>> SimplicialDilemmaInteraction('play', target='players', temptation_allied=0.8).check().temptation_allied
0.8
Source code in src/skcomplex/models/social/cooperation/_simplicial_dilemma.py
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
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.elements import GroupElement
>>> from skcomplex.models.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/models/social/cooperation/_strategist.py
42
43
44
45
46
47
48
49
50
51
52
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/engine/solvers/_mesa.py
513
514
515
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.models.social.cooperation import TournamentInteraction
>>> TournamentInteraction('tournament', target='agents').rounds
6
Source code in src/skcomplex/models/social/cooperation/_tournament.py
 95
 96
 97
 98
 99
100
101
102
103
104
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, model)

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
model ModelViewProtocol

The model view of the rule.

required
Source code in src/skcomplex/models/social/cooperation/_tournament.py
114
115
116
117
118
119
120
121
122
123
124
125
126
def act_on(self, members: list[AgentViewProtocol], model: ModelViewProtocol) -> 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.

        model:
            The model 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)

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 model, with the strategies players.strategy and the payoffs players.payoff.

Examples:

>>> from skcomplex.models.social.cooperation import hypergraph_public_goods
>>> hypergraph_public_goods(30).elements[0].check().prepare().size_
30
Source code in src/skcomplex/models/social/cooperation/_hypergraph_games.py
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 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
125
126
127
128
129
130
131
132
133
@register_model(
    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 model, with the strategies `players.strategy` and the payoffs `players.payoff`.

    Examples:
        >>> from skcomplex.models.social.cooperation import hypergraph_public_goods
        >>> hypergraph_public_goods(30).elements[0].check().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))),
    )

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.models.social.cooperation import play
>>> play([1] * 7, [0] * 7)
(0.0, 30.0)
Source code in src/skcomplex/models/social/cooperation/_tournament.py
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
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.models.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 model, with the group agents, the tournament tournament, and the death-birth replacement selection.

Examples:

>>> from skcomplex.models.social.cooperation import prisoners_dilemma_tournament
>>> prisoners_dilemma_tournament(10, random_state=0).elements[0].check().prepare().size_
10
Source code in src/skcomplex/models/social/cooperation/_tournaments.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
@register_model(
    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 model, with the group `agents`, the tournament `tournament`, and the death-birth replacement
            `selection`.

    Examples:
        >>> from skcomplex.models.social.cooperation import prisoners_dilemma_tournament
        >>> prisoners_dilemma_tournament(10, random_state=0).elements[0].check().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 model, with the strategies players.strategy and the payoffs players.payoff.

Examples:

>>> from skcomplex.models.social.cooperation import public_goods_game
>>> public_goods_game((10, 10)).elements[0].check().prepare().size_
100
Source code in src/skcomplex/models/social/cooperation/_spatial_games.py
 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
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
@register_model(
    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 model, with the strategies `players.strategy` and the payoffs `players.payoff`.

    Examples:
        >>> from skcomplex.models.social.cooperation import public_goods_game
        >>> public_goods_game((10, 10)).elements[0].check().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 model 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 model, with the strategies players.strategy and the payoffs players.payoff.

Examples:

>>> from skcomplex.models.social.cooperation import simplicial_social_dilemmas
>>> simplicial_social_dilemmas(30).elements[0].check().prepare().size_
30
Source code in src/skcomplex/models/social/cooperation/_hypergraph_games.py
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
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
@register_model(
    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 model 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 model, with the strategies `players.strategy` and the payoffs `players.payoff`.

    Examples:
        >>> from skcomplex.models.social.cooperation import simplicial_social_dilemmas
        >>> simplicial_social_dilemmas(30).elements[0].check().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 model, with the strategies players.strategy.

Examples:

>>> from skcomplex.models.social.cooperation import spatial_prisoners_dilemma
>>> spatial_prisoners_dilemma((10, 10)).elements[0].check().prepare().size_
100
Source code in src/skcomplex/models/social/cooperation/_spatial_games.py
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
@register_model(
    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 model, with the strategies `players.strategy`.

    Examples:
        >>> from skcomplex.models.social.cooperation import spatial_prisoners_dilemma
        >>> spatial_prisoners_dilemma((10, 10)).elements[0].check().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)],
    )