Skip to content

Opinion dynamics

Opinion dynamics: voting, coevolving networks, persuasion, bounded confidence, conventions, and diffusion.

AdopterElement(label, visual=VisualSpec('agents', {'color_by': 'adopted', 'color_range': (0.0, 1.0)}), *, adopted=0.0, threshold=50.0, reach=15.0)

Bases: Element

Agent of the threshold adoption system, who adopts an innovation for good.

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_.

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

Whether the agent starts as an adopter.

0.0
threshold Annotated[float, StateSpec(domain=closed_open(0, oo))]

The percentage of adopters in the agent's social circle above which it adopts in the influence mode.

50.0
reach Annotated[float, ParameterSpec(domain=open(0, oo))]

The distance within which other agents form an agent's social circle.

15.0

Examples:

>>> from skcomplex.core.components.elements import GroupElement
>>> from skcomplex.systems.social.opinion_dynamics import AdopterElement
>>> group = GroupElement('people', element=AdopterElement, size=10, values={'threshold': 50.0})
>>> group.label, group.element.__name__, group.size
('people', 'AdopterElement', 10)
Source code in src/skcomplex/systems/social/opinion_dynamics/_diffusion.py
127
128
129
130
131
132
133
134
135
136
137
138
139
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'adopted', 'color_range': (0.0, 1.0)}),
    *,
    adopted: Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))] = 0.0,
    threshold: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 50.0,
    reach: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 15.0,
) -> None:
    super().__init__(label, visual=visual)
    self.adopted = adopted
    self.threshold = threshold
    self.reach = reach

AdopterView(agent, stepper)

Bases: AgentView

View of an agent of the threshold adoption system, with its states and parameters 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

AdoptionInteraction(label, *, target, mode='infection')

Bases: TransitionInteraction

Synchronous adoption of the threshold system, where every agent adopts for good from its social circle.

In the infection mode an agent adopts once anyone in its social circle has adopted. In the influence mode it adopts once the percentage of adopters in its circle exceeds its threshold, or in society when its circle is empty. Every agent decides from the adoptions at the start of the step.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
target Annotated[str, RoleSpec()]

The path of the group of adopters.

required
mode str

'infection' or 'influence'.

'infection'

Raises:

Type Description
ValidationError

The mode is unknown.

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import AdoptionInteraction
>>> AdoptionInteraction('adoption', target='people', mode='influence').mode
'influence'
Source code in src/skcomplex/systems/social/opinion_dynamics/_diffusion.py
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
def __init__(
    self,
    label: str,
    *,
    target: Annotated[str, RoleSpec()],
    mode: str = 'infection',
) -> None:
    super().__init__(
        label,
        target=target,
        state='adopted',
        source=0.0,
        destination=1.0,
        condition=self._adopts,
        form='agent',
    )
    self.mode = mode

BelieverElement(label, visual=VisualSpec('agents', {'color_by': 'caring', 'color_range': (0.0, 3.0)}), *, belief=0.0, caring=0.0, influence=1.0, learning=1.0)

Bases: Element

Agent of the belief diffusion system, with the weights it gives to four levels of caring about an issue.

The levels are not caring at all, caring less, caring a bit, and caring passionately. An agent cares at the level of its largest weight. In influentials, a MapInteraction of the vector of weights adds every step learning times the influence of every neighbour to the weight of the level the neighbour cares at, a tie broken at random, and a second one sets the level of caring.

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_.

VisualSpec('agents', {'color_by': 'caring', 'color_range': (0.0, 3.0)})
belief Annotated[ArrayLike, StateSpec(vector=True)]

The initial weights of the four levels.

0.0
caring Annotated[float, StateSpec(domain=NN)]

The initial level of caring, the index of the largest weight.

0.0
influence Annotated[float, StateSpec(domain=open(0, oo))]

The weight of the agent's own level in the beliefs of its neighbours.

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

The increment per neighbour and step.

1.0

Examples:

>>> from skcomplex.core.components.elements import GroupElement
>>> from skcomplex.systems.social.opinion_dynamics import BelieverElement
>>> group = GroupElement('people', element=BelieverElement, size=4, values={'belief': [1.0, 0.0, 0.0, 0.0]})
>>> group.label, group.element.__name__, group.size
('people', 'BelieverElement', 4)
Source code in src/skcomplex/systems/social/opinion_dynamics/_diffusion.py
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'caring', 'color_range': (0.0, 3.0)}),
    *,
    belief: Annotated[npt.ArrayLike, StateSpec(vector=True)] = 0.0,
    caring: Annotated[float, StateSpec(domain=NN)] = 0.0,
    influence: Annotated[float, StateSpec(domain=RealSet.open(0, oo))] = 1.0,
    learning: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
) -> None:
    super().__init__(label, visual=visual)
    self.belief = belief
    self.caring = caring
    self.influence = influence
    self.learning = learning

BelieverView(agent, stepper)

Bases: AgentView

View of an agent of the influentials system, with its states and parameters 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

CitizenElement(label, visual=None, *, opinion=0.0)

Bases: Element

Agent of the Galam majority system, who holds a binary opinion.

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
opinion Annotated[float, StateSpec(domain=NN)]

The initial opinion, 0 or 1.

0.0

Examples:

>>> from skcomplex.core.components.elements import GroupElement
>>> from skcomplex.systems.social.opinion_dynamics import CitizenElement
>>> group = GroupElement('citizens', element=CitizenElement, size=10, values={'opinion': 1.0})
>>> group.label, group.element.__name__, group.size
('citizens', 'CitizenElement', 10)
Source code in src/skcomplex/systems/social/opinion_dynamics/_persuasion.py
 99
100
101
102
103
104
105
106
107
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    opinion: Annotated[float, StateSpec(domain=NN)] = 0.0,
) -> None:
    super().__init__(label, visual=visual)
    self.opinion = opinion

CitizenView(agent, stepper)

Bases: AgentView

View of an agent of the Galam majority system, with its states and parameters 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

ClubMemberElement(label, visual=OPINION_VISUAL, *, opinion=0.0, degree=0.0, beta=1.0, appeal=0.0, deciding=0.1, activity=0.5, rewiring=0.5)

Bases: Element

Agent that is in a club (spin 1) or out of it (spin 0), aligned by a heat bath with the agents it knows.

Every step an agent reconsiders with probability deciding and then joins with probability 1 / (1 + exp(-2 β h)), where the field h counts the acquaintances in the club minus those out of it, plus the appeal of the club. The interaction SeparationInteraction dissolves acquaintances between a member and an outsider and reforms them towards an agent of the same side.

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_.

OPINION_VISUAL
opinion Annotated[float, StateSpec(domain=closed_open(0, oo))]

1 for a member of the club and 0 for an outsider.

0.0
degree Annotated[float, StateSpec(domain=closed_open(0, oo))]

The number of acquaintances, read at the start of every step.

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

The inverse temperature of the heat bath.

1.0
appeal Annotated[float, ParameterSpec()]

The field of the club on every agent, positive for an attractive club.

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

The probability that an agent reconsiders its choice in a step.

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

The probability that an acquaintance is reconsidered in a step.

0.5
rewiring Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The probability that a reconsidered acquaintance across the club boundary dissolves and reforms.

0.5

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import ClubMemberElement
>>> ClubMemberElement('agents', beta=2.0).beta
2.0
Source code in src/skcomplex/systems/social/opinion_dynamics/_coevolution.py
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,
    visual: VisualSpec | None = OPINION_VISUAL,
    *,
    opinion: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
    degree: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
    beta: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
    appeal: Annotated[float, ParameterSpec()] = 0.0,
    deciding: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.1,
    activity: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.5,
    rewiring: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.5,
) -> None:
    super().__init__(label, visual=visual)
    self.opinion = opinion
    self.degree = degree
    self.beta = beta
    self.appeal = appeal
    self.deciding = deciding
    self.activity = activity
    self.rewiring = rewiring

CoevolvingVoterElement(label, visual=OPINION_VISUAL, *, opinion=0.0, degree=0.0, activity=0.5, rewiring=0.3)

Bases: Element

Voter of a coevolving network, with a binary opinion, its degree, and its number of discordant neighbours.

Every step a voter is active with probability activity and picks a random neighbour. When the neighbour disagrees, the voter copies its opinion with probability 1 - rewiring, and otherwise the interaction SeparationInteraction moves the edge to a random voter who agrees with it.

Parameters:

Name Type Description Default
label str

The label of the voter or the group.

required
visual VisualSpec | None

How the agent is drawn, or None for default_visual_.

OPINION_VISUAL
opinion Annotated[float, StateSpec(domain=closed_open(0, oo))]

The opinion, 0 or 1.

0.0
degree Annotated[float, StateSpec(domain=closed_open(0, oo))]

The number of neighbours, read at the start of every step.

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

The probability that a voter acts in a step.

0.5
rewiring Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The probability that an active voter with a discordant neighbour rewires instead of copying.

0.3

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import CoevolvingVoterElement
>>> CoevolvingVoterElement('voters', rewiring=0.3).rewiring
0.3
Source code in src/skcomplex/systems/social/opinion_dynamics/_coevolution.py
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = OPINION_VISUAL,
    *,
    opinion: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
    degree: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
    activity: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.5,
    rewiring: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.3,
) -> None:
    super().__init__(label, visual=visual)
    self.opinion = opinion
    self.degree = degree
    self.activity = activity
    self.rewiring = rewiring

ComplexAgentElement(label, visual=None, *, configuration=2.0, spin=0.0)

Bases: Element

Agent with many internal configurations, of which two show a characteristic with the spin 1 or -1.

Configuration 0 has the spin S = 1, configuration 1 the spin S = -1, and the configurations from 2 to H + 1 the spin 0. In standardized_agents, a MapInteraction sets the spin after every change of configuration.

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
configuration Annotated[float, StateSpec(domain=NN)]

The internal configuration, from 0 to H + 1.

2.0
spin Annotated[float, StateSpec(domain=ZZ)]

The spin of the configuration.

0.0

Examples:

>>> from skcomplex.core.components.elements import GroupElement
>>> from skcomplex.systems.social.opinion_dynamics import ComplexAgentElement
>>> GroupElement(
...     'agents', element=ComplexAgentElement, size=10, values={'configuration': 5.0, 'spin': 0.0}
... ).size
10
Source code in src/skcomplex/systems/social/opinion_dynamics/_standardization.py
58
59
60
61
62
63
64
65
66
67
68
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    configuration: Annotated[float, StateSpec(domain=NN)] = 2.0,
    spin: Annotated[float, StateSpec(domain=ZZ)] = 0.0,
) -> None:
    super().__init__(label, visual=visual)
    self.configuration = configuration
    self.spin = spin

ComplexAgentView(agent, stepper)

Bases: AgentView

View of an agent of the standardization system, with its states and parameters 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

ConfidenceCouplingInteraction(label, *, target, state='opinion')

Bases: CouplingInteraction

Hegselmann-Krause coupling, the move of every agent to the mean opinion of the neighbours within its confidence.

Every step every agent takes at once the mean opinion of its neighbours, itself included, whose opinions differ from its own by less than its parameter confidence. It is the map form of coupling with the normalized mean over the neighbours of the network weighted by the indicator of the confidence.

Parameters:

Name Type Description Default
label str

The label of the interaction, unique among the parts of its system.

required
target Annotated[str, RoleSpec()]

The path of the group of agents, on the nodes of a network.

required
state str

The name of the state that holds the opinions.

'opinion'

Examples:

>>> import networkx as nx
>>> import numpy as np
>>> from skcomplex.core.components.elements import LatticeElement
>>> from skcomplex.core.components.spaces import DiscreteTime, NetworkSpace
>>> from skcomplex.core.components.system import System
>>> from skcomplex.core.engine.runtime import Simulation
>>> from skcomplex.systems.social.opinion_dynamics import ConfidenceCouplingInteraction, ConfidentElement
>>> people = LatticeElement(
...     'people',
...     element=ConfidentElement,
...     space=NetworkSpace(nx.complete_graph(3)),
...     values={'opinion': [0.1, 0.2, 0.9], 'confidence': 0.2},
... )
>>> coupling = ConfidenceCouplingInteraction('compromise', target='people')
>>> system = System(elements=[people], interactions=[coupling], time=DiscreteTime())
>>> opinions = Simulation(system).prepare().run(until=1).trajectory_.values('people.opinion')
>>> np.round(opinions[-1], 6).tolist()
[0.15, 0.15, 0.9]
Source code in src/skcomplex/systems/social/opinion_dynamics/_bounded_confidence.py
157
158
159
160
161
162
163
164
165
166
167
168
169
def __init__(self, label: str, *, target: Annotated[str, RoleSpec()], state: str = 'opinion') -> None:
    super().__init__(
        label,
        target=target,
        read=state,
        write=state,
        function=self._get_opinion,
        reduce='normalized-mean',
        weights=self._get_trust,
        form='map',
        neighborhood=AdjacencyNeighborhood(center=True),
    )
    self.state = state

ConfidentElement(label, visual=VisualSpec('histograms', {'color_range': (0.0, 1.0), 'histogram_first': True}), *, opinion=0.5, confidence=0.2)

Bases: Element

Agent of the Hegselmann-Krause system, with an opinion and the confidence within which it trusts neighbours.

ConfidenceCouplingInteraction moves every agent each step to the mean opinion of the neighbours, itself included, whose opinions differ from its own by less than confidence.

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_.

VisualSpec('histograms', {'color_range': (0.0, 1.0), 'histogram_first': True})
opinion Annotated[float, StateSpec(domain=closed_open(0, oo))]

The opinion, between 0 and 1.

0.5
confidence Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The largest opinion difference of a neighbour an agent still listens to.

0.2

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import ConfidentElement
>>> ConfidentElement('people', confidence=0.1).confidence
0.1
Source code in src/skcomplex/systems/social/opinion_dynamics/_bounded_confidence.py
51
52
53
54
55
56
57
58
59
60
61
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = VisualSpec('histograms', {'color_range': (0.0, 1.0), 'histogram_first': True}),
    *,
    opinion: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.5,
    confidence: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.2,
) -> None:
    super().__init__(label, visual=visual)
    self.opinion = opinion
    self.confidence = confidence

ConnectionElement(label, visual=None, *, A=0.0)

Bases: Element

Edge of a network with a connection A, the phase an agent transports along it.

Parameters:

Name Type Description Default
label str

The label of the edge or the group on the edges.

required
visual VisualSpec | None

How the edge is drawn, or None for default_visual_.

None
A Annotated[float, ParameterSpec()]

The connection along the direction of the edge.

0.0

Attributes:

Name Type Description
default_visual_

The visual of the edge when it sets none.

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import ConnectionElement
>>> ConnectionElement('edge', A=0.5).A
0.5
Source code in src/skcomplex/systems/social/opinion_dynamics/_gauge_agents.py
127
128
129
130
131
132
133
134
135
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    A: Annotated[float, ParameterSpec()] = 0.0,
) -> None:
    super().__init__(label, visual=visual)
    self.A = A

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

Bases: GroupingInteraction

Conversations of the cognitive consistency system, one per agent and step, the agents speaking in random order.

A speaker talks to a random neighbour about F1 or F2. The hearer averages its valence of the topic with the speaker's and settles its belief network, and its attitude is its valence of X.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
target Annotated[str, RoleSpec()]

The path of the group of thinkers.

required
phase str | None

The phase of the step the conversations run in, or None for the default phase.

None

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import ConversationsInteraction
>>> ConversationsInteraction('talks', target='thinkers').groups
'neighborhood'
Source code in src/skcomplex/systems/social/opinion_dynamics/_cognition.py
152
153
154
155
156
157
158
159
def __init__(
    self,
    label: str,
    *,
    target: Annotated[str, RoleSpec()],
    phase: str | None = None,
) -> None:
    super().__init__(label, target=target, groups='neighborhood', phase=phase)

act_on(members, system)

Let the first member speak to a random neighbour, who averages the topic and settles its beliefs.

Parameters:

Name Type Description Default
members list[AgentViewProtocol]

The speaker, followed by its neighbours.

required
system SystemViewProtocol

The system view of the rule.

required
Source code in src/skcomplex/systems/social/opinion_dynamics/_cognition.py
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
def act_on(self, members: list[AgentViewProtocol], system: SystemViewProtocol) -> None:
    """Let the first member speak to a random neighbour, who averages the topic and settles its beliefs.

    Args:
        members:
            The speaker, followed by its neighbours.

        system:
            The system view of the rule.
    """
    speaker, *neighbors = [member for member in members if isinstance(member, ThinkerView)]
    if not neighbors:
        return
    hearer = neighbors[int(system.rng.integers(len(neighbors)))]
    topic = TOPICS[int(system.rng.integers(len(TOPICS)))]
    valences = np.array(hearer.valences, dtype=np.float64)
    valences[topic] = (valences[topic] + np.asarray(speaker.valences, dtype=np.float64)[topic]) / 2
    settled = settle(valences, hearer.decay)
    hearer.valences, hearer.attitude = settled, float(settled[1])

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

Bases: GroupingInteraction

Cultural influence of Axelrod's system, where every village in turn may copy a trait of a random neighbour.

Every step every village, in random order, picks a random neighbour and, with probability equal to the fraction of features they share, copies the trait of one feature on which they differ.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
target Annotated[str, RoleSpec()]

The path of the group of villages.

required
phase str | None

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

None

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import CulturalInfluenceInteraction
>>> CulturalInfluenceInteraction('influence', target='villages').groups
'neighborhood'
Source code in src/skcomplex/systems/social/opinion_dynamics/_axelrod.py
129
130
131
132
133
134
135
136
def __init__(
    self,
    label: str,
    *,
    target: Annotated[str, RoleSpec()],
    phase: str | None = None,
) -> None:
    super().__init__(label, target=target, groups='neighborhood', phase=phase)

act_on(members, system)

Copy one differing trait of a random neighbour with probability equal to the cultural overlap.

Parameters:

Name Type Description Default
members list[AgentViewProtocol]

The village, followed by its neighbours.

required
system SystemViewProtocol

The system view of the rule.

required
Source code in src/skcomplex/systems/social/opinion_dynamics/_axelrod.py
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
def act_on(self, members: list[AgentViewProtocol], system: SystemViewProtocol) -> None:
    """Copy one differing trait of a random neighbour with probability equal to the cultural overlap.

    Args:
        members:
            The village, followed by its neighbours.

        system:
            The system view of the rule.
    """
    agent, *neighbors = [member for member in members if isinstance(member, VillageView)]
    if not neighbors:
        return
    neighbor = neighbors[int(system.rng.integers(len(neighbors)))]
    features, traits = int(agent.features), int(agent.traits)
    own = cultural_traits(agent.culture, features, traits)
    other = cultural_traits(neighbor.culture, features, traits)
    different = np.flatnonzero(own != other)
    if len(different) and system.rng.random() < 1 - len(different) / features:
        feature = int(different[int(system.rng.integers(len(different)))])
        agent.culture += float((int(other[feature]) - int(own[feature])) * traits**feature)

DiscussantElement(label, visual=VisualSpec('agents', {'color_by': 'opinion', 'color_range': (0.0, 1.0)}), *, opinion=0.5, confidence=0.2, convergence=0.5)

Bases: Element

Agent of the Deffuant system, with a continuous opinion and a confidence bound.

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_.

VisualSpec('agents', {'color_by': 'opinion', 'color_range': (0.0, 1.0)})
opinion Annotated[float, StateSpec(domain=closed_open(0, oo))]

The initial opinion, between 0 and 1.

0.5
confidence Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The largest opinion difference at which two agents still compromise.

0.2
convergence Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The fraction of the opinion difference by which each agent of a compromising pair moves.

0.5

Examples:

>>> from skcomplex.core.components.elements import GroupElement
>>> from skcomplex.systems.social.opinion_dynamics import DiscussantElement
>>> group = GroupElement('people', element=DiscussantElement, size=10, values={'opinion': 0.5})
>>> group.label, group.element.__name__, group.size
('people', 'DiscussantElement', 10)
Source code in src/skcomplex/systems/social/opinion_dynamics/_bounded_confidence.py
 99
100
101
102
103
104
105
106
107
108
109
110
111
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'opinion', 'color_range': (0.0, 1.0)}),
    *,
    opinion: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.5,
    confidence: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.2,
    convergence: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.5,
) -> None:
    super().__init__(label, visual=visual)
    self.opinion = opinion
    self.confidence = confidence
    self.convergence = convergence

DiscussantView(agent, stepper)

Bases: AgentView

View of an agent of the Deffuant bounded confidence system, with its states and parameters 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

DiscussionsInteraction(label, *, target, size=3)

Bases: GroupingInteraction

Discussions of the Galam system, which split the citizens into random groups that adopt their majority opinion.

It is a symbolic GroupingInteraction of random partitions, drawn again at every step. A tie in a group of even size goes to opinion 0, the status quo.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
target Annotated[str, RoleSpec()]

The path of the group of citizens.

required
size int

The number of citizens in each discussion group, the last group holding the remainder.

3

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import DiscussionsInteraction
>>> DiscussionsInteraction('talks', target='citizens', size=4).write
'opinion'
Source code in src/skcomplex/systems/social/opinion_dynamics/_persuasion.py
204
205
206
207
208
209
210
211
212
213
def __init__(self, label: str, *, target: Annotated[str, RoleSpec()], size: int = 3) -> None:
    super().__init__(
        label,
        target=target,
        groups='partition',
        size=size,
        form='symbolic',
        write='opinion',
        function=self._get_majority,
    )

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

Bases: RandomPairsInteraction

Random binary encounters of the Deffuant system, as many per step as there are agents.

In every encounter two random agents whose opinions differ by less than the confidence bound move towards each other by the convergence fraction of their difference.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
target Annotated[str, RoleSpec()]

The path of the group of discussants.

required
phase str | None

The phase of the step the encounters run in, or None for the default phase.

None

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import EncountersInteraction
>>> EncountersInteraction('meetings', target='people').target
'people'
Source code in src/skcomplex/systems/social/opinion_dynamics/_random_pairs.py
38
39
40
41
42
43
44
45
def __init__(
    self,
    label: str,
    *,
    target: Annotated[str, RoleSpec()],
    phase: str | None = None,
) -> None:
    super().__init__(label, target=target, groups='pairs', phase=phase)

act_on(members, system)

Move the opinions of a pair towards each other when they differ by less than the confidence bound.

Parameters:

Name Type Description Default
members list[AgentViewProtocol]

The two discussants of the encounter.

required
system SystemViewProtocol

The system view of the rule.

required
Source code in src/skcomplex/systems/social/opinion_dynamics/_bounded_confidence.py
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
def act_on(self, members: list[AgentViewProtocol], system: SystemViewProtocol) -> None:
    """Move the opinions of a pair towards each other when they differ by less than the confidence bound.

    Args:
        members:
            The two discussants of the encounter.

        system:
            The system view of the rule.
    """
    first, second = [member for member in members if isinstance(member, DiscussantView)]
    difference = second.opinion - first.opinion
    if abs(difference) < first.confidence:
        first.opinion += first.convergence * difference
        second.opinion -= first.convergence * difference

InfluenceeElement(label, visual=VisualSpec('agents', {'color_by': 'opinion', 'color_range': (-1.0, 1.0)}), *, opinion=1.0, persuasiveness=1.0, supportiveness=1.0, exponent=2.0, noise=0.0)

Bases: Element

Agent of the social impact system, with an opinion of -1 or 1, a persuasiveness, and a supportiveness.

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_.

VisualSpec('agents', {'color_by': 'opinion', 'color_range': (-1.0, 1.0)})
opinion Annotated[float, StateSpec(domain=ZZ)]

The initial opinion, -1 or 1.

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

The strength with which the agent persuades agents of the other opinion.

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

The strength with which the agent supports agents of its opinion.

1.0
exponent Annotated[float, ParameterSpec(domain=open(0, oo))]

The exponent of the distance by which impact decays.

2.0
noise Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The half-width of the uniform random field added to the impact on every agent and step.

0.0

Examples:

>>> from skcomplex.core.components.elements import GroupElement
>>> from skcomplex.systems.social.opinion_dynamics import InfluenceeElement
>>> group = GroupElement('people', element=InfluenceeElement, size=4, values={'opinion': -1.0})
>>> group.label, group.element.__name__, group.size
('people', 'InfluenceeElement', 4)
Source code in src/skcomplex/systems/social/opinion_dynamics/_persuasion.py
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'opinion', 'color_range': (-1.0, 1.0)}),
    *,
    opinion: Annotated[float, StateSpec(domain=ZZ)] = 1.0,
    persuasiveness: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
    supportiveness: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
    exponent: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 2.0,
    noise: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
) -> None:
    super().__init__(label, visual=visual)
    self.opinion = opinion
    self.persuasiveness = persuasiveness
    self.supportiveness = supportiveness
    self.exponent = exponent
    self.noise = noise

InfluenceeView(agent, stepper)

Bases: AgentView

View of an agent of the social impact system, with its states and parameters 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

MajorityVoterElement(label, visual=None, *, opinion=0)

Bases: Element

Cell that takes the majority opinion of its Moore neighbourhood, itself included.

The majority rule is a LocalRuleInteraction of the lattice, which majority_rule adds.

Parameters:

Name Type Description Default
label str

The label of the cell or the lattice group.

required
visual VisualSpec | None

How the agent is drawn, or None for default_visual_.

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

The initial opinion, 0 or 1.

0

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import MajorityVoterElement
>>> MajorityVoterElement('cell', opinion=1).opinion
1
Source code in src/skcomplex/systems/social/opinion_dynamics/_voters.py
47
48
49
50
51
52
53
54
55
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    opinion: Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))] = 0,
) -> None:
    super().__init__(label, visual=visual)
    self.opinion = opinion

NamerElement(label, visual=None, *, words=0.0)

Bases: Element

Agent of the naming game, with an inventory of the words it knows for one object.

The inventory is an indicator vector over the lexicon, with one slot per agent for the word that agent invents when it first speaks with an empty inventory.

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
words Annotated[ArrayLike, StateSpec(vector=True)]

The initial inventory, 1 for every known word and 0 otherwise.

0.0

Examples:

>>> from skcomplex.core.components.elements import GroupElement
>>> import numpy as np
>>> from skcomplex.systems.social.opinion_dynamics import NamerElement
>>> group = GroupElement('namers', element=NamerElement, size=5, values={'words': np.zeros((5, 5))})
>>> group.label, group.element.__name__, group.size
('namers', 'NamerElement', 5)
Source code in src/skcomplex/systems/social/opinion_dynamics/_conventions.py
52
53
54
55
56
57
58
59
60
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    words: Annotated[npt.ArrayLike, StateSpec(vector=True)] = 0.0,
) -> None:
    super().__init__(label, visual=visual)
    self.words = words

NamerView(agent, stepper)

Bases: AgentView

View of an agent of the naming game, with its states and parameters 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

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

Bases: RandomPairsInteraction

Pairwise games of the minimal naming game, as many per step as there are agents.

A random speaker says a random word of its inventory to a random hearer. When the hearer knows it, both keep that word alone, and otherwise the hearer adds it. A speaker with an empty inventory first invents a word that no agent knows.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
target Annotated[str, RoleSpec()]

The path of the group of namers.

required
phase str | None

The phase of the step the games run in, or None for the default phase.

None

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import NamingGamesInteraction
>>> NamingGamesInteraction('games', target='namers').target
'namers'
Source code in src/skcomplex/systems/social/opinion_dynamics/_random_pairs.py
38
39
40
41
42
43
44
45
def __init__(
    self,
    label: str,
    *,
    target: Annotated[str, RoleSpec()],
    phase: str | None = None,
) -> None:
    super().__init__(label, target=target, groups='pairs', phase=phase)

act_on(members, system)

Play one naming game between the first member, the speaker, and the second, the hearer.

Parameters:

Name Type Description Default
members list[AgentViewProtocol]

The speaker and the hearer.

required
system SystemViewProtocol

The system view of the rule.

required
Source code in src/skcomplex/systems/social/opinion_dynamics/_conventions.py
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
def act_on(self, members: list[AgentViewProtocol], system: SystemViewProtocol) -> None:
    """Play one naming game between the first member, the speaker, and the second, the hearer.

    Args:
        members:
            The speaker and the hearer.

        system:
            The system view of the rule.
    """
    speaker, hearer = [member for member in members if isinstance(member, NamerView)]
    spoken = np.array(speaker.words, dtype=np.float64)
    heard = np.array(hearer.words, dtype=np.float64)
    if not spoken.any():
        self._invent(spoken, system)
    known = np.flatnonzero(spoken)
    name = known[int(system.rng.integers(len(known)))]
    if heard[name]:
        spoken[:] = heard[:] = 0.0
        spoken[name] = heard[name] = 1.0
    else:
        heard[name] = 1.0
    speaker.words, hearer.words = spoken, heard

OpinionElement(label, visual=None, *, x=0.0)

Bases: Element

Node of a consensus process with a continuous opinion.

Parameters:

Name Type Description Default
label str

The label of the node or the group.

required
visual VisualSpec | None

How the node is drawn, or None for default_visual_.

None
x Annotated[float, StateSpec()]

The initial opinion.

0.0

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import OpinionElement
>>> OpinionElement('node', x=0.5).x
0.5
Source code in src/skcomplex/systems/social/opinion_dynamics/_hypergraph_consensus.py
45
46
47
48
49
50
51
52
53
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    x: Annotated[float, StateSpec()] = 0.0,
) -> None:
    super().__init__(label, visual=visual)
    self.x = x

OpinionEncountersInteraction(label, *, candidate, field, levels=7, confidence=2, eta=1.0, persuasion=0.3, repulsion=0.1)

Bases: EncounterInteraction

Conservative encounters between opinion levels, which persuade close opinions and repel distant ones.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
candidate Annotated[str, RoleSpec()]

The path of the distribution over opinion levels whose members move.

required
field Annotated[str, RoleSpec()]

The path of the distribution over opinion levels they meet, usually the same distribution.

required
levels Annotated[int, ParameterSpec(domain=open(0, oo))]

The number of opinion levels.

7
confidence Annotated[int, ParameterSpec(domain=closed_open(0, oo))]

The largest distance in levels at which encounters persuade.

2
eta Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The encounter rate.

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

The probability of moving toward a close opinion.

0.3
repulsion Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The probability of moving away from a distant opinion.

0.1

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import OpinionEncountersInteraction
>>> OpinionEncountersInteraction('encounters', candidate='opinions', field='opinions').confidence
2
Source code in src/skcomplex/systems/social/opinion_dynamics/_kinetic_opinion.py
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
def __init__(
    self,
    label: str,
    *,
    candidate: Annotated[str, RoleSpec()],
    field: Annotated[str, RoleSpec()],
    levels: Annotated[int, ParameterSpec(domain=RealSet.open(0, oo))] = 7,
    confidence: Annotated[int, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 2,
    eta: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
    persuasion: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.3,
    repulsion: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.1,
) -> None:
    super().__init__(label, candidate=candidate, field=field, rate=self._get_rate, kernel=self._get_kernel)
    self.levels = levels
    self.confidence = confidence
    self.eta = eta
    self.persuasion = persuasion
    self.repulsion = repulsion

PersuaderElement(label, visual=VisualSpec('agents', {'color_by': 'opinion', 'color_range': (0.0, 1.0)}), *, opinion=0.0)

Bases: Element

Agent of the Sznajd system, who convinces the neighbours of an agreeing pair.

A step of an agent picks a random neighbour, and when both share an opinion every neighbour of either adopts it. On a ring this is the original one-dimensional rule, where the pair convinces the two agents beside it.

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_.

VisualSpec('agents', {'color_by': 'opinion', 'color_range': (0.0, 1.0)})
opinion Annotated[float, StateSpec(domain=NN)]

The initial opinion, 0 or 1.

0.0

Examples:

>>> from skcomplex.core.components.elements import GroupElement
>>> from skcomplex.systems.social.opinion_dynamics import PersuaderElement
>>> group = GroupElement('people', element=PersuaderElement, size=10, values={'opinion': 1.0})
>>> group.label, group.element.__name__, group.size
('people', 'PersuaderElement', 10)
Source code in src/skcomplex/systems/social/opinion_dynamics/_persuasion.py
56
57
58
59
60
61
62
63
64
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'opinion', 'color_range': (0.0, 1.0)}),
    *,
    opinion: Annotated[float, StateSpec(domain=NN)] = 0.0,
) -> None:
    super().__init__(label, visual=visual)
    self.opinion = opinion

PersuaderView(agent, stepper)

Bases: AgentView

View of an agent of the Sznajd system, with its states and parameters 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

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

Bases: GroupingInteraction

Persuasion of the Sznajd system, where an agreeing pair of neighbours convinces the neighbours of both.

Every step every agent, in random order, picks a random neighbour, and when both share an opinion every neighbour of either adopts it. On a ring this is the original one-dimensional rule, where the pair convinces the two agents beside it.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
target Annotated[str, RoleSpec()]

The path of the group of persuaders.

required
phase str | None

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

None

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import PersuasionInteraction
>>> PersuasionInteraction('persuasion', target='people').groups
'neighborhood'
Source code in src/skcomplex/systems/social/opinion_dynamics/_persuasion.py
295
296
297
298
299
300
301
302
def __init__(
    self,
    label: str,
    *,
    target: Annotated[str, RoleSpec()],
    phase: str | None = None,
) -> None:
    super().__init__(label, target=target, groups='neighborhood', phase=phase)

act_on(members, system)

Convince the neighbours of the first member and a random neighbour of it when the two agree.

Parameters:

Name Type Description Default
members list[AgentViewProtocol]

The persuader, followed by its neighbours.

required
system SystemViewProtocol

The system view of the rule.

required
Source code in src/skcomplex/systems/social/opinion_dynamics/_persuasion.py
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
def act_on(self, members: list[AgentViewProtocol], system: SystemViewProtocol) -> None:
    """Convince the neighbours of the first member and a random neighbour of it when the two agree.

    Args:
        members:
            The persuader, followed by its neighbours.

        system:
            The system view of the rule.
    """
    agent, *neighbors = [member for member in members if isinstance(member, PersuaderView)]
    if not neighbors:
        return
    partner = neighbors[int(system.rng.integers(len(neighbors)))]
    if partner.opinion != agent.opinion:
        return
    for other in [*neighbors, *partner.neighbors()]:
        if isinstance(other, PersuaderView):
            other.opinion = agent.opinion

PhaseAgentElement(label, visual=None, *, theta=0.0)

Bases: Element

Agent with a phase theta.

Parameters:

Name Type Description Default
label str

The label of the agent or the group on the nodes.

required
visual VisualSpec | None

How the agent is drawn, or None for default_visual_.

None
theta Annotated[float, StateSpec(period=2 * pi)]

The initial phase.

0.0

Attributes:

Name Type Description
default_visual_

The visual of the agent when it sets none.

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import PhaseAgentElement
>>> PhaseAgentElement('agent', theta=0.5).theta
0.5
Source code in src/skcomplex/systems/social/opinion_dynamics/_gauge_agents.py
 93
 94
 95
 96
 97
 98
 99
100
101
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    theta: Annotated[float, StateSpec(period=2 * pi)] = 0.0,
) -> None:
    super().__init__(label, visual=visual)
    self.theta = theta

RadicalElement(label, visual=VisualSpec('agents', {'color_by': 'opinion', 'color_range': (-3.0, 3.0)}), *, opinion=0.0, activity=0.1)

Bases: Element

Agent with an unbounded opinion, whose sign is its stance and whose size is its conviction, and an activity.

Parameters:

Name Type Description Default
label str

The label of the agent or the group.

required
visual VisualSpec | None

How the agent is drawn, or None for default_visual_.

VisualSpec('agents', {'color_by': 'opinion', 'color_range': (-3.0, 3.0)})
opinion Annotated[float, StateSpec()]

The opinion x.

0.0
activity Annotated[float, StateSpec(domain=closed(0, 1))]

The probability a that the agent contacts others in an integration step.

0.1

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import RadicalElement
>>> RadicalElement('agent', activity=0.1).activity
0.1
Source code in src/skcomplex/systems/social/opinion_dynamics/_echo_chambers.py
55
56
57
58
59
60
61
62
63
64
65
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'opinion', 'color_range': (-3.0, 3.0)}),
    *,
    opinion: Annotated[float, StateSpec()] = 0.0,
    activity: Annotated[float, StateSpec(domain=RealSet.closed(0, 1))] = 0.1,
) -> None:
    super().__init__(label, visual=visual)
    self.opinion = opinion
    self.activity = activity

RadicalView(agent, stepper)

Bases: AgentView

View of an agent of the echo chamber 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

RadicalizationInteraction(label, *, target, strength=3.0, controversy=3.0, homophily=3.0, contacts=10, reciprocity=0.5, dt=0.01, substeps=10)

Bases: Interaction

Opinion dynamics of Baumann and coauthors, dx_i/dt = -x_i + K sum_j A_ij(t) tanh(alpha x_j).

Every step of the system integrates the opinions with substeps Euler steps of length dt, and draws a new temporal network for every one of them. In a draw, every agent is active with the probability of its activity and contacts contacts distinct others, each chosen with the probability |x_i - x_j|^-beta normalized over the others. A contacted agent receives the influence of the agent that contacts it, which receives the influence back when the contact is reciprocated, with the probability reciprocity.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
target Annotated[str, RoleSpec()]

The path of the group of agents.

required
strength Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The social interaction strength K.

3.0
controversy Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The controversialness alpha of the issue, the steepness of the influence tanh(alpha x).

3.0
homophily Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The exponent beta of the homophily of the contacts.

3.0
contacts Annotated[int, ParameterSpec(domain=NN)]

The number m of distinct agents an active agent contacts.

10
reciprocity Annotated[float, ParameterSpec(domain=closed(0, 1))]

The probability r that a contact is reciprocated.

0.5
dt Annotated[float, ParameterSpec(domain=open(0, oo))]

The length of an Euler step.

0.01
substeps Annotated[int, ParameterSpec(domain=NN)]

The number of Euler steps per step of the system.

10

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import RadicalizationInteraction
>>> RadicalizationInteraction('influence', target='agents').contacts
10
Source code in src/skcomplex/systems/social/opinion_dynamics/_echo_chambers.py
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
def __init__(
    self,
    label: str,
    *,
    target: Annotated[str, RoleSpec()],
    strength: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 3.0,
    controversy: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 3.0,
    homophily: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 3.0,
    contacts: Annotated[int, ParameterSpec(domain=NN)] = 10,
    reciprocity: Annotated[float, ParameterSpec(domain=RealSet.closed(0, 1))] = 0.5,
    dt: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 0.01,
    substeps: Annotated[int, ParameterSpec(domain=NN)] = 10,
) -> None:
    super().__init__(label)
    self.target = target
    self.strength = strength
    self.controversy = controversy
    self.homophily = homophily
    self.contacts = contacts
    self.reciprocity = reciprocity
    self.dt = dt
    self.substeps = substeps

influence(system)

Integrate the opinions over the Euler steps of one step of the system.

Parameters:

Name Type Description Default
system SystemView

The system.

required
Source code in src/skcomplex/systems/social/opinion_dynamics/_echo_chambers.py
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
def influence(self, system: SystemView) -> None:
    """Integrate the opinions over the Euler steps of one step of the system.

    Args:
        system:
            The system.
    """
    agents = system.agents(self.target, RadicalView)
    if len(agents) < PAIR:
        return
    opinions = np.array([agent.opinion for agent in agents])
    activities = np.array([agent.activity for agent in agents])
    for _ in range(int(self.substeps)):
        inputs = self._get_inputs(opinions, activities, system.rng)
        opinions = opinions + self.dt * (-opinions + self.strength * inputs)
    for agent, opinion in zip(agents, opinions.tolist(), strict=True):
        agent.opinion = opinion

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

Bases: GroupingInteraction

Grouping that draws as many random ordered pairs of distinct agents per step as there are agents.

The pairs are drawn independently, so an agent can meet several partners in one step, and every pair acts in random order through act_on, which a subclass overrides. One unit of time holds one encounter per agent, as in the random sequential encounters of mean-field opinion and convention systems.

Parameters:

Name Type Description Default
label str

The label of the interaction, unique among the parts of its system.

required
target Annotated[str, RoleSpec()]

The path of the group whose agents meet in pairs.

required
phase str | None

The phase of the step the encounters run in, or None for the default phase.

None

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import RandomPairsInteraction
>>> RandomPairsInteraction('meetings', target='people').groups
'pairs'
Source code in src/skcomplex/systems/social/opinion_dynamics/_random_pairs.py
38
39
40
41
42
43
44
45
def __init__(
    self,
    label: str,
    *,
    target: Annotated[str, RoleSpec()],
    phase: str | None = None,
) -> None:
    super().__init__(label, target=target, groups='pairs', phase=phase)

ScheduleElement(label, visual=None, *, phase=0.0)

Bases: Element

Triangle of a temporal hypergraph, active in every other half of a period from its own phase.

Parameters:

Name Type Description Default
label str

The label of the triangle or the group.

required
visual VisualSpec | None

How the triangle is drawn, or None for default_visual_.

None
phase Annotated[float, ParameterSpec()]

The phase of the schedule, in radians.

0.0

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import ScheduleElement
>>> ScheduleElement('triangle', phase=1.0).phase
1.0
Source code in src/skcomplex/systems/social/opinion_dynamics/_hypergraph_consensus.py
75
76
77
78
79
80
81
82
83
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    phase: Annotated[float, ParameterSpec()] = 0.0,
) -> None:
    super().__init__(label, visual=visual)
    self.phase = phase

SeparationInteraction(label, *, voters, keep='source', opinion=1.0)

Bases: TopologyInteraction

Rewiring of discordant edges by one end, which keeps the edge and moves its other end to a like-minded node.

It is a TopologyInteraction that rewires the edges of the voters. The end keep acts on an edge with probability activity / degree per step, and when it holds the opinion opinion and the other end does not, it rewires the edge with probability rewiring, drawing a new end among the voters of the opinion opinion.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
voters Annotated[str, RoleSpec()]

The path of the voters on the nodes.

required
keep str

The end of an edge that acts and keeps the edge, 'source' or 'target'.

'source'
opinion float

The opinion of the end that acts, 0 or 1.

1.0

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import SeparationInteraction
>>> SeparationInteraction('separation', voters='voters', keep='target', opinion=0).action
'rewire'
Source code in src/skcomplex/systems/social/opinion_dynamics/_coevolution.py
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
def __init__(
    self,
    label: str,
    *,
    voters: Annotated[str, RoleSpec()],
    keep: str = 'source',
    opinion: float = 1.0,
) -> None:
    super().__init__(
        label,
        target=voters,
        action='rewire',
        probability=self._get_chance,
        toward=self._get_weight,
        keep=keep,
    )
    self.voters = voters
    self.keep = keep
    self.opinion = opinion

SocialImpactInteraction(label, *, target)

Bases: MapInteraction

Synchronous update of the social impact system, where every agent takes the opinion of the stronger impact.

The impact on agent i is I_i = sum_j t_j (1 - o_i o_j) / d_ij^a - sum_j s_j (1 + o_i o_j) / d_ij^a, with the persuasiveness t, the supportiveness s, and the distance d on the grid, where an agent supports itself from distance 1. The impact is the net pressure to change, whatever the opinion, so an agent flips its opinion when I_i + h_i > 0, with a uniform random field h_i. Every agent decides from the opinions at the start of the step.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
target Annotated[str, RoleSpec()]

The path of the group of agents.

required

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import SocialImpactInteraction
>>> SocialImpactInteraction('impact', target='people').form
'agent'
Source code in src/skcomplex/systems/social/opinion_dynamics/_persuasion.py
241
242
243
244
245
246
247
def __init__(
    self,
    label: str,
    *,
    target: Annotated[str, RoleSpec()],
) -> None:
    super().__init__(label, target=target, state='opinion', function=self._get_opinion, form='agent')

StandardizationInteraction(label, *, target, K=10.0, J=1.0, H=50, intensity=2.0, activity=1.0, phase=None)

Bases: ChoiceInteraction

Changes of configuration of random agents, accepted with the logistic probability of their gain in utility.

The utility of agent i is u_i = K / N sum_j |S_i| |S_j| + J / N sum_j S_i S_j over the other agents. An agent proposes a configuration drawn uniformly from the H + 2 configurations, its own included, and takes it with the probability 1 / (1 + exp(-beta (u_i' - u_i))) of the rule 'fermi', so the agents sample P ~ exp(beta E). The utilities read the counts of the configurations of the group, so every proposal sees the changes accepted before it. In standardized_agents, a phase of visits draws N agents with replacement every unit of time.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
target Annotated[str, RoleSpec()]

The path of the group of agents.

required
K Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The coupling of the characteristics |S_i|, which standardizes the agents.

10.0
J Annotated[float, ParameterSpec()]

The coupling of the spins S_i, which orders the standardized agents.

1.0
H int

The number of configurations without the characteristic.

50
intensity float

The intensity of choice beta, the inverse of the temperature.

2.0
activity float

The probability that a visited agent proposes a configuration.

1.0
phase str | None

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

None

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import StandardizationInteraction
>>> StandardizationInteraction('standardization', target='agents', H=100).options[-1]
101.0
Source code in src/skcomplex/systems/social/opinion_dynamics/_standardization.py
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
def __init__(
    self,
    label: str,
    *,
    target: Annotated[str, RoleSpec()],
    K: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 10.0,
    J: Annotated[float, ParameterSpec()] = 1.0,
    H: int = 50,
    intensity: float = 2.0,
    activity: float = 1.0,
    phase: str | None = None,
) -> None:
    super().__init__(
        label,
        target=target,
        state='configuration',
        options=tuple(float(option) for option in range(H + MARKED)),
        utility=self._get_utility,
        rule='fermi',
        intensity=intensity,
        schedule='member',
        activity=activity,
        form='agent',
        phase=phase,
        proposal='all',
    )
    self.K = K
    self.J = J
    self.H = H

ThinkerElement(label, visual=VisualSpec('agents', {'color_by': 'attitude', 'color_range': (-1.0, 1.0)}), *, valences=0.0, attitude=0.0, decay=0.05)

Bases: Element

Agent of the cognitive consistency system, with the valences of the concepts of its belief network.

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_.

VisualSpec('agents', {'color_by': 'attitude', 'color_range': (-1.0, 1.0)})
valences Annotated[ArrayLike, StateSpec(vector=True)]

The initial valences of the concepts F1, X, and F2, between -1 and 1.

0.0
attitude Annotated[float, StateSpec()]

The attitude of the agent, the valence of the concept X once the belief network settles.

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

The fraction of every valence lost per update of the belief network.

0.05

Examples:

>>> from skcomplex.core.components.elements import GroupElement
>>> from skcomplex.systems.social.opinion_dynamics import ThinkerElement
>>> group = GroupElement('thinkers', element=ThinkerElement, size=5, values={'valences': [1.0, 1.0, -1.0]})
>>> group.label, group.element.__name__, group.size
('thinkers', 'ThinkerElement', 5)
Source code in src/skcomplex/systems/social/opinion_dynamics/_cognition.py
60
61
62
63
64
65
66
67
68
69
70
71
72
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'attitude', 'color_range': (-1.0, 1.0)}),
    *,
    valences: Annotated[npt.ArrayLike, StateSpec(vector=True)] = 0.0,
    attitude: Annotated[float, StateSpec()] = 0.0,
    decay: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.05,
) -> None:
    super().__init__(label, visual=visual)
    self.valences = valences
    self.attitude = attitude
    self.decay = decay

ThinkerView(agent, stepper)

Bases: AgentView

View of an agent of the cognitive consistency system, with its states and parameters 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

ThreeBodyConsensusInteraction(label, *, target, triangles, reinforcement=0.0, period=1.0)

Bases: HyperedgeInteraction

Nonlinear three-body consensus of Neuhäuser, Mellor, and Lambiotte, on the triangles active at the time.

The focal node i of an active triangle ijk moves towards the two others at the rate s(|x_j - x_k|) (x_j + x_k - 2 x_i), with s(d) = exp(reinforcement * d), so a pair that agrees pulls harder for a negative reinforcement. A triangle is active when sin(2 pi t / period + phase) > 0, with the phase of the triangle member of the group on hyperedges.

Parameters:

Name Type Description Default
label str

The label of the interaction, unique among the parts of its system.

required
target Annotated[str, RoleSpec()]

The path of the group of nodes.

required
triangles Annotated[str, RoleSpec()]

The path of the group of the schedules of the triangles.

required
reinforcement Annotated[float, ParameterSpec()]

The exponent of the modulating function s.

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

The period of the schedules.

1.0

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import ThreeBodyConsensusInteraction
>>> ThreeBodyConsensusInteraction('consensus', target='nodes', triangles='triangles').size
3
Source code in src/skcomplex/systems/social/opinion_dynamics/_hypergraph_consensus.py
119
120
121
122
123
124
125
126
127
128
129
130
131
132
def __init__(
    self,
    label: str,
    *,
    target: Annotated[str, RoleSpec()],
    triangles: Annotated[str, RoleSpec()],
    reinforcement: Annotated[float, ParameterSpec()] = 0.0,
    period: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
) -> None:
    super().__init__(label, size=TRIANGLE)
    self.target = target
    self.triangles = triangles
    self.reinforcement = reinforcement
    self.period = period

TransportedCouplingInteraction(label, *, target, links, K=1.0)

Bases: CouplingInteraction

Relaxation of agent phases towards the phases of their neighbours, theta_i' += K sin(theta_j - theta_i - A_ij).

The connection A_ij lives on the edge of the network from i to j, and A_ji = -A_ij.

Parameters:

Name Type Description Default
label str

The label of the interaction, unique among the parts of its system.

required
target Annotated[str, RoleSpec()]

The path of the group of agents on the nodes.

required
links Annotated[str, RoleSpec()]

The path of the group on the edges, whose parameter A holds the connection.

required
K Annotated[float, ParameterSpec()]

The coupling strength.

1.0

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import TransportedCouplingInteraction
>>> TransportedCouplingInteraction('coupling', target='agents', links='links', K=0.5).K
0.5
Source code in src/skcomplex/systems/social/opinion_dynamics/_gauge_agents.py
51
52
53
54
55
56
57
58
59
60
61
def __init__(
    self,
    label: str,
    *,
    target: Annotated[str, RoleSpec()],
    links: Annotated[str, RoleSpec()],
    K: Annotated[float, ParameterSpec()] = 1.0,
) -> None:
    super().__init__(label, target=target, read='theta', write='theta')
    self.links = links
    self.K = K

UndecidedElement(label, visual=VisualSpec('agents', {'color_by': 'opinion', 'color_range': (-1.0, 1.0)}), *, opinion=0.0, sensitivity=2.0)

Bases: Element

Voter who is against, undecided, or for, at -1, 0, or 1, and redraws its opinion from its neighbours.

Parameters:

Name Type Description Default
label str

The label of the voter or the group.

required
visual VisualSpec | None

How the agent is drawn, or None for default_visual_.

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

The opinion, -1, 0, or 1.

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

The sensitivity beta of the opinion to the sum of the opinions of the neighbours.

2.0

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import UndecidedElement
>>> UndecidedElement('voter').sensitivity
2.0
Source code in src/skcomplex/systems/social/opinion_dynamics/_undecided.py
54
55
56
57
58
59
60
61
62
63
64
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'opinion', 'color_range': (-1.0, 1.0)}),
    *,
    opinion: Annotated[float, StateSpec(domain=FiniteEnumeratedSet([-1, 0, 1]))] = 0.0,
    sensitivity: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 2.0,
) -> None:
    super().__init__(label, visual=visual)
    self.opinion = opinion
    self.sensitivity = sensitivity

UndecidedFlowsElement(label, visual=None, *, against=0.0, undecided=100.0, support=0.0, omega=1.0, gamma=0.05, eta=0.02, drive=1.0)

Bases: Element

Population split into the numbers A against, B for, and C undecided.

Parameters:

Name Type Description Default
label str

The label of the element.

required
visual VisualSpec | None

How the element is drawn, or None for default_visual_.

None
against Annotated[object, StateSpec(domain=closed_open(0, oo), quantity=agents)]

The number A of agents against.

0.0
undecided Annotated[object, StateSpec(domain=closed_open(0, oo), quantity=agents)]

The number C of undecided agents.

100.0
support Annotated[object, StateSpec(domain=closed_open(0, oo), quantity=agents)]

The number B of agents for.

0.0
omega Annotated[object, ParameterSpec(domain=closed_open(0, oo))]

The rate omega at which an undecided agent joins either side.

1.0
gamma Annotated[object, ParameterSpec(domain=closed_open(0, oo))]

The rate gamma at which an agent against becomes undecided.

0.05
eta Annotated[object, ParameterSpec(domain=closed_open(0, oo))]

The rate eta at which an agent for becomes undecided.

0.02
drive Annotated[object, ParameterSpec(domain=FiniteEnumeratedSet([0, 1]))]

The exponent of C in the flows into the undecided group, 0 for constant rates and 1 for rates in proportion to the undecided.

1.0

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import UndecidedFlowsElement
>>> UndecidedFlowsElement('population').omega
1.0
Source code in src/skcomplex/systems/social/opinion_dynamics/_undecided.py
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    against: Annotated[object, StateSpec(domain=RealSet.closed_open(0, oo), quantity='agents')] = 0.0,
    undecided: Annotated[object, StateSpec(domain=RealSet.closed_open(0, oo), quantity='agents')] = 100.0,
    support: Annotated[object, StateSpec(domain=RealSet.closed_open(0, oo), quantity='agents')] = 0.0,
    omega: Annotated[object, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
    gamma: Annotated[object, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.05,
    eta: Annotated[object, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.02,
    drive: Annotated[object, ParameterSpec(domain=FiniteEnumeratedSet([0, 1]))] = 1.0,
) -> None:
    super().__init__(label, visual=visual)
    self.against = against
    self.undecided = undecided
    self.support = support
    self.omega = omega
    self.gamma = gamma
    self.eta = eta
    self.drive = drive

UndecidedView(agent, stepper)

Bases: AgentView

View of a voter of the three-state opinion system, with its states and parameters 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

VariantVoterElement(label, visual=VisualSpec('agents', {'color_by': 'opinion', 'color_range': (0.0, 1.0)}), *, opinion=0.0, contrarian=0.0, threshold=0.0)

Bases: Element

Agent of the voter system variants: an imitator, a threshold voter, or a contrarian.

With a threshold of 0 the voting copies a random neighbour into the agent, as in the voter system. With a positive threshold it gives the agent the opinion opposite to its own when the share of neighbours that hold it exceeds the threshold, so a threshold of 0.5 gives the majority rule. A contrarian adopts the opposite of what it would otherwise adopt.

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_.

VisualSpec('agents', {'color_by': 'opinion', 'color_range': (0.0, 1.0)})
opinion Annotated[float, StateSpec(domain=NN)]

The initial opinion, 0 or 1.

0.0
contrarian Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))]

Whether the agent is a contrarian, 1, or not, 0.

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

The share of disagreeing neighbours above which the agent changes its opinion, or 0 to copy a random neighbour.

0.0

Examples:

>>> from skcomplex.core.components.elements import GroupElement
>>> from skcomplex.systems.social.opinion_dynamics import VariantVoterElement
>>> group = GroupElement('voters', element=VariantVoterElement, size=10, values={'threshold': 0.5})
>>> group.label, group.element.__name__, group.size
('voters', 'VariantVoterElement', 10)
Source code in src/skcomplex/systems/social/opinion_dynamics/_voters.py
 99
100
101
102
103
104
105
106
107
108
109
110
111
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'opinion', 'color_range': (0.0, 1.0)}),
    *,
    opinion: Annotated[float, StateSpec(domain=NN)] = 0.0,
    contrarian: Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))] = 0.0,
    threshold: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
) -> None:
    super().__init__(label, visual=visual)
    self.opinion = opinion
    self.contrarian = contrarian
    self.threshold = threshold

VariantVoterView(agent, stepper)

Bases: AgentView

View of an agent of the voter system variants, with its states and parameters 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

VariantVotingInteraction(label, *, target)

Bases: MapInteraction

Voting of the voter system variants, where every voter in turn takes the opinion of its variant.

A voter of threshold 0 copies a random neighbour. A voter of positive threshold adopts the opinion opposite to its own when the share of neighbours that hold it exceeds the threshold. A contrarian adopts the opposite of what it would otherwise adopt. The voters update one after the other in a new random order every step, each reading the opinions the voters before it set, and a voter without a neighbouring voter keeps its opinion.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
target Annotated[str, RoleSpec()]

The path of the group of voters.

required

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import VariantVotingInteraction
>>> VariantVotingInteraction('voting', target='voters').schedule
'random-sequential'
Source code in src/skcomplex/systems/social/opinion_dynamics/_voters.py
183
184
185
186
187
188
189
190
191
def __init__(self, label: str, *, target: Annotated[str, RoleSpec()]) -> None:
    super().__init__(
        label,
        target=target,
        state='opinion',
        function=self._get_opinion,
        schedule='random-sequential',
        form='agent',
    )

VillageElement(label, visual=VisualSpec('agents', {'color_by': 'culture'}), *, culture=0, features=5.0, traits=10.0)

Bases: Element

Village of Axelrod's system, with a culture of F features of q traits each, stored as one integer.

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_.

VisualSpec('agents', {'color_by': 'culture'})
culture Annotated[float, StateSpec(domain=NN)]

The culture code, the traits of the features as the digits of a number in base q.

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

The number of features F.

5.0
traits Annotated[float, ParameterSpec(domain=open(0, oo))]

The number of traits q of every feature.

10.0

Examples:

>>> from skcomplex.core.components.elements import GroupElement
>>> from skcomplex.systems.social.opinion_dynamics import VillageElement
>>> GroupElement(
...     'villages', element=VillageElement, size=4, values={'culture': 0, 'features': 3, 'traits': 5}
... ).size
4
Source code in src/skcomplex/systems/social/opinion_dynamics/_axelrod.py
62
63
64
65
66
67
68
69
70
71
72
73
74
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'culture'}),
    *,
    culture: Annotated[float, StateSpec(domain=NN)] = 0,
    features: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 5.0,
    traits: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 10.0,
) -> None:
    super().__init__(label, visual=visual)
    self.culture = culture
    self.features = features
    self.traits = traits

VillageView(agent, stepper)

Bases: AgentView

View of a village of the Axelrod culture system, with its states and parameters 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

VoterElement(label, visual=VisualSpec('agents', {'color_by': 'opinion', 'color_range': (0.0, 1.0)}), *, opinion=0.0)

Bases: Element

Agent of the voter system, whose opinion the imitation sets to that of a random neighbour on a network.

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_.

VisualSpec('agents', {'color_by': 'opinion', 'color_range': (0.0, 1.0)})
opinion Annotated[float, StateSpec(domain=NN)]

The initial opinion, 0 or 1.

0.0

Examples:

>>> from skcomplex.core.components.elements import GroupElement
>>> from skcomplex.systems.social.opinion_dynamics import VoterElement
>>> group = GroupElement('voters', element=VoterElement, size=20, values={'opinion': 1.0})
>>> group.label, group.element.__name__, group.size
('voters', 'VoterElement', 20)
Source code in src/skcomplex/systems/social/opinion_dynamics/_voters.py
146
147
148
149
150
151
152
153
154
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'opinion', 'color_range': (0.0, 1.0)}),
    *,
    opinion: Annotated[float, StateSpec(domain=NN)] = 0.0,
) -> None:
    super().__init__(label, visual=visual)
    self.opinion = opinion

VoterImitationInteraction(label, *, target)

Bases: MapInteraction

Imitation of the voter system, where every voter in turn adopts the opinion of a random neighbour.

The voters update one after the other in a new random order every step, each reading the opinions the voters before it set. A voter without a neighbouring voter keeps its opinion.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
target Annotated[str, RoleSpec()]

The path of the group of voters.

required

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import VoterImitationInteraction
>>> VoterImitationInteraction('imitation', target='voters').schedule
'random-sequential'
Source code in src/skcomplex/systems/social/opinion_dynamics/_voters.py
233
234
235
236
237
238
239
240
241
def __init__(self, label: str, *, target: Annotated[str, RoleSpec()]) -> None:
    super().__init__(
        label,
        target=target,
        state='opinion',
        function=self._get_opinion,
        schedule='random-sequential',
        form='agent',
    )

VoterView(agent, stepper)

Bases: AgentView

View of an agent of the voter system, with its states and parameters 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

WillingMajorityInteraction(label, *, target, group_size=3)

Bases: Interaction

Majority rule with willingness, where a random agent adopts the majority opinion of a random group.

Every step has one update per agent. An update chooses an agent uniformly, then an odd number group_size of other agents uniformly, and the agent adopts the majority opinion of the group with the probability of its willingness.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
target Annotated[str, RoleSpec()]

The path of the group of voters.

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

The odd number of other agents an agent consults.

3

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import WillingMajorityInteraction
>>> WillingMajorityInteraction('majority', target='voters').group_size
3
Source code in src/skcomplex/systems/social/opinion_dynamics/_willing_majority.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
def __init__(
    self,
    label: str,
    *,
    target: Annotated[str, RoleSpec()],
    group_size: Annotated[int, ParameterSpec(domain=NN)] = 3,
) -> None:
    super().__init__(label)
    self.target = target
    self.group_size = group_size

follow(system)

Update as many random agents as there are agents, each by the majority of a random group.

Parameters:

Name Type Description Default
system SystemView

The system.

required
Source code in src/skcomplex/systems/social/opinion_dynamics/_willing_majority.py
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
def follow(self, system: SystemView) -> None:
    """Update as many random agents as there are agents, each by the majority of a random group.

    Args:
        system:
            The system.
    """
    voters = system.agents(self.target, WillingVoterView)
    size = len(voters)
    group_size = int(self.group_size)
    if size <= group_size:
        return
    opinions = np.array([voter.opinion for voter in voters])
    willingness = np.array([voter.willingness for voter in voters])
    rng = system.rng
    for chosen in rng.integers(size, size=size).tolist():
        others = rng.choice(size - 1, group_size, replace=False)
        others = others + (others >= chosen)
        if rng.random() < willingness[chosen]:
            opinions[chosen] = float(2 * opinions[others].sum() > group_size)
    for voter, opinion in zip(voters, opinions.tolist(), strict=True):
        voter.opinion = opinion

WillingVoterElement(label, visual=VisualSpec('agents', {'color_by': 'opinion', 'color_range': (0.0, 1.0)}), *, opinion=0.0, willingness=1.0)

Bases: Element

Voter with a binary opinion and a willingness to follow the majority, a zealot at willingness 0.

Parameters:

Name Type Description Default
label str

The label of the voter or the group.

required
visual VisualSpec | None

How the agent is drawn, or None for default_visual_.

VisualSpec('agents', {'color_by': 'opinion', 'color_range': (0.0, 1.0)})
opinion Annotated[float, StateSpec(domain=NN)]

The opinion, 0 for B or 1 for A.

0.0
willingness Annotated[float, StateSpec(domain=closed(0, 1))]

The probability that the voter adopts the majority opinion of the group it consults.

1.0

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import WillingVoterElement
>>> WillingVoterElement('voter').willingness
1.0
Source code in src/skcomplex/systems/social/opinion_dynamics/_willing_majority.py
51
52
53
54
55
56
57
58
59
60
61
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'opinion', 'color_range': (0.0, 1.0)}),
    *,
    opinion: Annotated[float, StateSpec(domain=NN)] = 0.0,
    willingness: Annotated[float, StateSpec(domain=RealSet.closed(0, 1))] = 1.0,
) -> None:
    super().__init__(label, visual=visual)
    self.opinion = opinion
    self.willingness = willingness

WillingVoterView(agent, stepper)

Bases: AgentView

View of a voter of the majority rule with willingness, 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

axelrod(width=20, height=20, *, features=5, traits=10, random_state=0)

Return Axelrod's culture dissemination system on a full grid with von Neumann neighbourhoods.

Every village starts with random traits. For few traits per feature the villages converge to one culture, and for many traits they freeze into many small cultural regions, an order-disorder transition at a critical q.

Parameters:

Name Type Description Default
width int

The number of columns.

20
height int

The number of rows.

20
features int

The number of cultural features F.

5
traits int

The number of traits q of every feature.

10
random_state int | Generator | None

The seed or generator of the initial cultures.

0

Returns:

Name Type Description
system System

The system, with the group villages and the influence influence. culture_observables gives the share of the largest cultural region and the number of cultures.

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import axelrod
>>> axelrod(5, 4).elements[0].prepare().size_
20
Source code in src/skcomplex/systems/social/opinion_dynamics/_axelrod.py
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
@register_system(
    field='social/opinion_dynamics',
    tags=('culture', 'agents', 'grid', 'social influence', 'phase transition'),
    references=('Axelrod 1997', 'Castellano, Fortunato, and Loreto 2009'),
    defaults={'until': 200},
    observables=_get_culture_observables,
)
def axelrod(
    width: int = 20,
    height: int = 20,
    *,
    features: int = 5,
    traits: int = 10,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return Axelrod's culture dissemination system on a full grid with von Neumann neighbourhoods.

    Every village starts with random traits. For few traits per feature the villages converge to one culture, and
    for many traits they freeze into many small cultural regions, an order-disorder transition at a critical `q`.

    Args:
        width:
            The number of columns.

        height:
            The number of rows.

        features:
            The number of cultural features `F`.

        traits:
            The number of traits `q` of every feature.

        random_state:
            The seed or generator of the initial cultures.

    Returns:
        system:
            The system, with the group `villages` and the influence `influence`. `culture_observables` gives the
            share of the largest cultural region and the number of cultures.

    Examples:
        >>> from skcomplex.systems.social.opinion_dynamics import axelrod
        >>> axelrod(5, 4).elements[0].prepare().size_
        20
    """
    rng = np.random.default_rng(random_state)
    size = width * height
    digits = rng.integers(traits, size=(size, features))
    cultures = (digits * traits ** np.arange(features)).sum(axis=1).astype(float)
    locations = [(x, y) for x in range(width) for y in range(height)]
    villages = GroupElement(
        'villages',
        element=VillageElement,
        size=size,
        values={'culture': cultures, 'features': features, 'traits': traits},
        location=locations,
    )
    return System(
        elements=[villages],
        interactions=[CulturalInfluenceInteraction('influence', target='villages')],
        space=LatticeSpace((width, height), boundary='fixed', neighborhood='von_neumann', capacity=1),
    )

coevolving_voter(n_voters=200, *, mean_degree=4.0, rewiring=0.3, activity=0.5, random_state=0)

Return the coevolving voter system, which fragments into consensual components when rewiring is fast.

Voters on an Erdős-Rényi graph copy discordant neighbours or rewire the discordant edges to like-minded voters. Below a critical rewiring probability, near 0.38 for mean degree 4, the network stays connected and discordant edges survive for long times, and above it the network splits into components of one opinion.

Parameters:

Name Type Description Default
n_voters int

The number of voters.

200
mean_degree float

The mean degree of the initial random graph.

4.0
rewiring float

The probability that an active voter with a discordant neighbour rewires instead of copying.

0.3
activity float

The probability that a voter acts in a step.

0.5
random_state int | Generator | None

The seed or generator of the graph and of the uniform random opinions.

0

Returns:

Name Type Description
system System

The system, with the groups voters and ties and the interaction separation.

Examples:

>>> from skcomplex.core.engine.runtime import Simulation
>>> from skcomplex.systems.social.opinion_dynamics import coevolving_voter
>>> trajectory = Simulation(coevolving_voter(30), random_state=0).prepare().run(until=5).trajectory_
>>> int(trajectory.network(-1).sources.size) == int(trajectory.network(0).sources.size)
True
Source code in src/skcomplex/systems/social/opinion_dynamics/_coevolution.py
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
@register_system(
    field='social/opinion_dynamics',
    tags=('opinion', 'voter', 'adaptive network', 'rewiring', 'fragmentation'),
    references=('Holme and Newman 2006', 'Vazquez, Eguíluz, and San Miguel 2008', 'Thurner et al. 2018'),
    defaults={'until': 200},
)
def coevolving_voter(
    n_voters: int = 200,
    *,
    mean_degree: float = 4.0,
    rewiring: float = 0.3,
    activity: float = 0.5,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the coevolving voter system, which fragments into consensual components when rewiring is fast.

    Voters on an Erdős-Rényi graph copy discordant neighbours or rewire the discordant edges to like-minded voters.
    Below a critical rewiring probability, near 0.38 for mean degree 4, the network stays connected and discordant
    edges survive for long times, and above it the network splits into components of one opinion.

    Args:
        n_voters:
            The number of voters.

        mean_degree:
            The mean degree of the initial random graph.

        rewiring:
            The probability that an active voter with a discordant neighbour rewires instead of copying.

        activity:
            The probability that a voter acts in a step.

        random_state:
            The seed or generator of the graph and of the uniform random opinions.

    Returns:
        system:
            The system, with the groups `voters` and `ties` and the interaction `separation`.

    Examples:
        >>> from skcomplex.core.engine.runtime import Simulation
        >>> from skcomplex.systems.social.opinion_dynamics import coevolving_voter
        >>> trajectory = Simulation(coevolving_voter(30), random_state=0).prepare().run(until=5).trajectory_
        >>> int(trajectory.network(-1).sources.size) == int(trajectory.network(0).sources.size)
        True
    """
    rng = np.random.default_rng(random_state)
    space = NetworkSpace(nx.gnp_random_graph(n_voters, mean_degree / (n_voters - 1), seed=rng))
    opinions = (rng.random(n_voters) < FIRST_OPINION_SHARE).astype(float)
    copying = MapInteraction('copying', target='voters', state='opinion', function=_copy_discordant)
    return _coevolving(CoevolvingVoterElement, space, opinions, copying, rewiring=rewiring, activity=activity)

cognitive_consistency(graph=None, *, positive=0.7, decay=0.05, random_state=0)

Return agents whose attitudes spread by conversation and settle in a belief network that seeks consistency.

Parameters:

Name Type Description Default
graph object

The NetworkX graph, or None for a complete graph of 40 agents.

None
positive float

The share of agents that start in the positive state, F1 = X = 1 and F2 = -1, the others starting in the negative state.

0.7
decay float

The fraction of every valence lost per update of the belief network.

0.05
random_state int | Generator | None

The seed or generator of the initial states.

0

Returns:

Name Type Description
system System

The system, with the group thinkers.

Examples:

>>> import networkx as nx
>>> from skcomplex.systems.social.opinion_dynamics import cognitive_consistency
>>> cognitive_consistency(nx.complete_graph(6), random_state=0).elements[0].prepare().size_
6
Source code in src/skcomplex/systems/social/opinion_dynamics/_cognition.py
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
@register_system(
    field='social/opinion_dynamics',
    tags=('opinion', 'agents', 'network', 'cognition'),
    references=('Lakkaraju and Speed 2019', 'Carmichael et al. 2019'),
    defaults={'until': 30},
)
def cognitive_consistency(
    graph: object = None,
    *,
    positive: float = 0.7,
    decay: float = 0.05,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return agents whose attitudes spread by conversation and settle in a belief network that seeks consistency.

    Args:
        graph:
            The NetworkX graph, or `None` for a complete graph of 40 agents.

        positive:
            The share of agents that start in the positive state, F1 = X = 1 and F2 = -1, the others starting in the
            negative state.

        decay:
            The fraction of every valence lost per update of the belief network.

        random_state:
            The seed or generator of the initial states.

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

    Examples:
        >>> import networkx as nx
        >>> from skcomplex.systems.social.opinion_dynamics import cognitive_consistency
        >>> cognitive_consistency(nx.complete_graph(6), random_state=0).elements[0].prepare().size_
        6
    """
    graph = nx.complete_graph(40) if graph is None else graph
    size = nx.number_of_nodes(graph)
    signs = np.where(np.random.default_rng(random_state).random(size) < positive, 1.0, -1.0)
    valences = signs[:, None] * np.array(POSITIVE)
    thinkers = GroupElement(
        'thinkers',
        element=ThinkerElement,
        size=size,
        values={'valences': valences, 'attitude': valences[:, 1], 'decay': decay},
    )
    return System(
        elements=[thinkers],
        interactions=[ConversationsInteraction('talks', target='thinkers')],
        space=NetworkSpace(graph),
    )

cultural_traits(culture, features, traits)

Return the traits of a culture vector stored as one integer, its digits in base q.

Parameters:

Name Type Description Default
culture float

The integer code of the culture vector.

required
features int

The number of features F.

required
traits int

The number of traits q of every feature.

required

Returns:

Name Type Description
traits IntArray

The trait of every feature, the first feature first.

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import cultural_traits
>>> cultural_traits(7, 3, 2).tolist()
[1, 1, 1]
Source code in src/skcomplex/systems/social/opinion_dynamics/_axelrod.py
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
def cultural_traits(culture: float, features: int, traits: int) -> IntArray:
    """Return the traits of a culture vector stored as one integer, its digits in base `q`.

    Args:
        culture:
            The integer code of the culture vector.

        features:
            The number of features `F`.

        traits:
            The number of traits `q` of every feature.

    Returns:
        traits:
            The trait of every feature, the first feature first.

    Examples:
        >>> from skcomplex.systems.social.opinion_dynamics import cultural_traits
        >>> cultural_traits(7, 3, 2).tolist()
        [1, 1, 1]
    """
    return (int(culture) // traits ** np.arange(features, dtype=np.int64)) % traits

culture_observables(width, height)

Return the share of the largest cultural region and the number of cultures of a full grid of villages.

Parameters:

Name Type Description Default
width int

The number of columns of the grid.

required
height int

The number of rows of the grid.

required

Returns:

Name Type Description
observables list[ObservableSpec]

The observables largest_region and cultures, each reducing the cultures of the group villages.

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import culture_observables
>>> [item.label for item in culture_observables(5, 4)]
['largest_region', 'cultures']
Source code in src/skcomplex/systems/social/opinion_dynamics/_axelrod.py
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
def culture_observables(width: int, height: int) -> list[ObservableSpec]:
    """Return the share of the largest cultural region and the number of cultures of a full grid of villages.

    Args:
        width:
            The number of columns of the grid.

        height:
            The number of rows of the grid.

    Returns:
        observables:
            The observables `largest_region` and `cultures`, each reducing the cultures of the group `villages`.

    Examples:
        >>> from skcomplex.systems.social.opinion_dynamics import culture_observables
        >>> [item.label for item in culture_observables(5, 4)]
        ['largest_region', 'cultures']
    """
    locations = [(x, y) for x in range(width) for y in range(height)]
    region = partial(largest_region, locations, width=width, height=height)
    return [
        ObservableSpec('largest_region', group='villages', variable='culture', reduce=region),
        ObservableSpec('cultures', group='villages', variable='culture', reduce=_count_cultures),
    ]

deffuant(n_agents=200, *, confidence=0.2, convergence=0.5, random_state=0)

Return the Deffuant bounded confidence system, whose opinions settle into about 1 / (2 * confidence) clusters.

Parameters:

Name Type Description Default
n_agents int

The number of agents.

200
confidence float

The largest opinion difference at which two agents compromise.

0.2
convergence float

The fraction of the difference by which each agent of a compromising pair moves.

0.5
random_state int | Generator | None

The seed or generator of the uniform initial opinions.

0

Returns:

Name Type Description
system System

The system, with the group people.

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import deffuant
>>> deffuant(20, random_state=0).elements[0].prepare().size_
20
Source code in src/skcomplex/systems/social/opinion_dynamics/_bounded_confidence.py
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
@register_system(
    field='social/opinion_dynamics',
    tags=('opinion', 'agents', 'bounded confidence', 'clusters'),
    references=('Deffuant et al. 2000', 'Castellano, Fortunato, and Loreto 2009', 'Gros 2015'),
    defaults={'until': 50},
)
def deffuant(
    n_agents: int = 200,
    *,
    confidence: float = 0.2,
    convergence: float = 0.5,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the Deffuant bounded confidence system, whose opinions settle into about `1 / (2 * confidence)` clusters.

    Args:
        n_agents:
            The number of agents.

        confidence:
            The largest opinion difference at which two agents compromise.

        convergence:
            The fraction of the difference by which each agent of a compromising pair moves.

        random_state:
            The seed or generator of the uniform initial opinions.

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

    Examples:
        >>> from skcomplex.systems.social.opinion_dynamics import deffuant
        >>> deffuant(20, random_state=0).elements[0].prepare().size_
        20
    """
    opinions = np.random.default_rng(random_state).random(n_agents)
    visual = VisualSpec('histograms', {'color_by': 'opinion', 'color_range': (0.0, 1.0), 'histogram_first': True})
    people = GroupElement(
        'people',
        element=DiscussantElement,
        size=n_agents,
        values={'opinion': opinions, 'confidence': confidence, 'convergence': convergence},
        visual=visual,
    )
    return System(elements=[people], interactions=[EncountersInteraction('meetings', target='people')])

echo_chamber_radicalization(n_agents=200, *, strength=3.0, controversy=3.0, homophily=3.0, contacts=10, reciprocity=0.5, exponent=2.1, least_activity=0.01, random_state=0)

Return the radicalization model of echo chambers of Baumann and coauthors on an activity-driven network.

Opinions follow dx_i/dt = -x_i + K sum_j A_ij(t) tanh(alpha x_j), where the controversialness alpha shapes the influence and A_ij(t) is an activity-driven temporal network redrawn at every integration step dt = 0.01: agent i is active with the probability a_i, drawn from F(a) ~ a^-gamma on [epsilon, 1], and contacts m others with the homophilic probability |x_i - x_j|^-beta / sum_j |x_i - x_j|^-beta, and a contact is reciprocated with the probability r (arXiv 1906.12325 Eqs. 1 and 2). Opinions start uniformly spaced on [-1, 1]. For small K alpha they reach the neutral consensus, above alpha_c = 1 / ((1 + r) K m <a>) they radicalize, all to one side without homophily (beta = 0), and into two opposite echo chambers with it. The defaults are the polarized case of Fig. 1(c), K = 3, alpha = 3, beta = 3, r = 0.5, m = 10, and gamma = 2.1, with fewer agents than the 1000 of the paper so a step is fast. Every step of the system is ten integration steps.

Parameters:

Name Type Description Default
n_agents int

The number of agents.

200
strength float

The social interaction strength K.

3.0
controversy float

The controversialness alpha.

3.0
homophily float

The exponent beta of the homophily of the contacts.

3.0
contacts int

The number m of distinct agents an active agent contacts.

10
reciprocity float

The probability r that a contact is reciprocated.

0.5
exponent float

The exponent gamma of the power law of the activities.

2.1
least_activity float

The least activity epsilon.

0.01
random_state int | Generator | None

The seed or generator of the activities.

0

Returns:

Name Type Description
system System

The system, with the group agents.

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import echo_chamber_radicalization
>>> echo_chamber_radicalization(50).elements[0].prepare().size_
50
Source code in src/skcomplex/systems/social/opinion_dynamics/_echo_chambers.py
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
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
@register_system(
    field='social/opinion_dynamics',
    tags=('opinion', 'agents', 'temporal network', 'polarization', 'echo chambers'),
    references=('Baumann, Lorenz-Spreen, Sokolov, and Starnini 2020',),
    defaults={'until': 100},
)
def echo_chamber_radicalization(
    n_agents: int = 200,
    *,
    strength: float = 3.0,
    controversy: float = 3.0,
    homophily: float = 3.0,
    contacts: int = 10,
    reciprocity: float = 0.5,
    exponent: float = 2.1,
    least_activity: float = 0.01,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the radicalization model of echo chambers of Baumann and coauthors on an activity-driven network.

    Opinions follow `dx_i/dt = -x_i + K sum_j A_ij(t) tanh(alpha x_j)`, where the controversialness `alpha` shapes
    the influence and `A_ij(t)` is an activity-driven temporal network redrawn at every integration step `dt = 0.01`:
    agent `i` is active with the probability `a_i`, drawn from `F(a) ~ a^-gamma` on `[epsilon, 1]`, and contacts `m`
    others with the homophilic probability `|x_i - x_j|^-beta / sum_j |x_i - x_j|^-beta`, and a contact is
    reciprocated with the probability `r` (arXiv 1906.12325 Eqs. 1 and 2). Opinions start uniformly spaced on
    `[-1, 1]`. For small `K alpha` they reach the neutral consensus, above `alpha_c = 1 / ((1 + r) K m <a>)` they
    radicalize, all to one side without homophily (`beta = 0`), and into two opposite echo chambers with it. The
    defaults are the polarized case of Fig. 1(c), `K = 3`, `alpha = 3`, `beta = 3`, `r = 0.5`, `m = 10`, and
    `gamma = 2.1`, with fewer agents than the 1000 of the paper so a step is fast. Every step of the system is ten
    integration steps.

    Args:
        n_agents:
            The number of agents.

        strength:
            The social interaction strength `K`.

        controversy:
            The controversialness `alpha`.

        homophily:
            The exponent `beta` of the homophily of the contacts.

        contacts:
            The number `m` of distinct agents an active agent contacts.

        reciprocity:
            The probability `r` that a contact is reciprocated.

        exponent:
            The exponent `gamma` of the power law of the activities.

        least_activity:
            The least activity `epsilon`.

        random_state:
            The seed or generator of the activities.

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

    Examples:
        >>> from skcomplex.systems.social.opinion_dynamics import echo_chamber_radicalization
        >>> echo_chamber_radicalization(50).elements[0].prepare().size_
        50
    """
    rng = np.random.default_rng(random_state)
    low, power = least_activity ** (1 - exponent), 1 - exponent
    activities = (low + rng.random(n_agents) * (1 - low)) ** (1 / power)
    agents = GroupElement(
        'agents',
        element=RadicalElement,
        size=n_agents,
        values={'opinion': np.linspace(-1.0, 1.0, n_agents), 'activity': activities},
    )
    influence = RadicalizationInteraction(
        'influence',
        target='agents',
        strength=strength,
        controversy=controversy,
        homophily=homophily,
        contacts=contacts,
        reciprocity=reciprocity,
    )
    return System(elements=[agents], interactions=[influence])

galam_majority(n_agents=300, *, group_size=3, share=0.4, random_state=0)

Return the Galam majority system, where random groups adopt their majority until one opinion wins.

Parameters:

Name Type Description Default
n_agents int

The number of agents.

300
group_size int

The number of agents in each discussion group.

3
share float

The share of agents with opinion 1.

0.4
random_state int | Generator | None

The seed or generator of the opinions.

0

Returns:

Name Type Description
system System

The system, with the group citizens.

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import galam_majority
>>> galam_majority(30, random_state=0).elements[0].prepare().size_
30
Source code in src/skcomplex/systems/social/opinion_dynamics/_persuasion.py
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
@register_system(
    field='social/opinion_dynamics',
    tags=('opinion', 'agents', 'majority', 'consensus'),
    references=('Galam 2002', 'Castellano, Fortunato, and Loreto 2009'),
    defaults={'until': 20},
)
def galam_majority(
    n_agents: int = 300,
    *,
    group_size: int = 3,
    share: float = 0.4,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the Galam majority system, where random groups adopt their majority until one opinion wins.

    Args:
        n_agents:
            The number of agents.

        group_size:
            The number of agents in each discussion group.

        share:
            The share of agents with opinion 1.

        random_state:
            The seed or generator of the opinions.

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

    Examples:
        >>> from skcomplex.systems.social.opinion_dynamics import galam_majority
        >>> galam_majority(30, random_state=0).elements[0].prepare().size_
        30
    """
    citizens = GroupElement(
        'citizens',
        element=CitizenElement,
        size=n_agents,
        values={'opinion': _shares(n_agents, share, random_state)},
    )
    return System(
        elements=[citizens],
        interactions=[DiscussionsInteraction('talks', target='citizens', size=group_size)],
    )

gauge_network_agents(n_agents=12, *, flux=0.0, K=1.0, spread=1.0, connections=None, graph=None, seed=0)

Return agent phases on the nodes of a network that relax towards the transported phases of their neighbours.

Agent i follows theta_i' = K sum_j sin(theta_j - theta_i - A_ij) over its neighbours. A connection whose oriented sum around a loop is not a multiple of 2 pi frustrates the loop: the transported differences cannot all vanish, so the agents keep a residual energy sum (1 - cos(theta_j - theta_i - A_ij)) and a twist around the loop.

Parameters:

Name Type Description Default
n_agents int

The number of agents on a ring, when no graph is given.

12
flux float

The oriented sum of the connection around the first face of the minimum cycle basis, spread evenly over the edges of that face.

0.0
K float

The coupling strength.

1.0
spread float

The largest absolute initial phase.

1.0
connections Sequence[float] | None

The connection of every edge, in place of flux, or None.

None
graph Graph | None

The network, or None for a ring of n_agents.

None
seed int

The seed of the initial phases.

0

Returns:

Name Type Description
system System

The system.

Raises:

Type Description
ValidationError

A flux is given on a network without faces.

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import gauge_network_agents
>>> [element.label for element in gauge_network_agents(4).elements]
['agents', 'links']
Source code in src/skcomplex/systems/social/opinion_dynamics/_gauge_agents.py
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
250
251
252
253
@register_system(
    field='social/opinion_dynamics',
    tags=('gauge', 'opinions', 'frustration', 'network', 'cell complex'),
    references=('Mihaylov and Cacciatori 2025',),
    defaults={'until': 100.0, 'dt': 0.5},
    observables=_agent_observables,
)
def gauge_network_agents(
    n_agents: int = 12,
    *,
    flux: float = 0.0,
    K: float = 1.0,
    spread: float = 1.0,
    connections: Sequence[float] | None = None,
    graph: nx.Graph | None = None,
    seed: int = 0,
) -> System:
    """Return agent phases on the nodes of a network that relax towards the transported phases of their neighbours.

    Agent `i` follows `theta_i' = K sum_j sin(theta_j - theta_i - A_ij)` over its neighbours. A connection whose
    oriented sum around a loop is not a multiple of `2 pi` frustrates the loop: the transported differences cannot all
    vanish, so the agents keep a residual energy `sum (1 - cos(theta_j - theta_i - A_ij))` and a twist around the loop.

    Args:
        n_agents:
            The number of agents on a ring, when no graph is given.

        flux:
            The oriented sum of the connection around the first face of the minimum cycle basis, spread evenly over the
            edges of that face.

        K:
            The coupling strength.

        spread:
            The largest absolute initial phase.

        connections:
            The connection of every edge, in place of `flux`, or `None`.

        graph:
            The network, or `None` for a ring of `n_agents`.

        seed:
            The seed of the initial phases.

    Returns:
        system:
            The system.

    Raises:
        ValidationError:
            A flux is given on a network without faces.

    Examples:
        >>> from skcomplex.systems.social.opinion_dynamics import gauge_network_agents
        >>> [element.label for element in gauge_network_agents(4).elements]
        ['agents', 'links']
    """
    network = nx.cycle_graph(n_agents) if graph is None else graph
    space = CellComplexSpace(NetworkSpace(network)).prepare()
    faces = space.incidences_[1]
    if flux and not faces.shape[0] and connections is None:
        error_msg = f'A flux of {flux} needs a loop, but the network has no faces. Give a network with a cycle.'
        raise ValidationError(error_msg)
    if connections is not None:
        connection = np.asarray(connections, dtype=float)
    elif flux:
        first = faces[[0], :].toarray().ravel()
        connection = flux * first / np.abs(first).sum()
    else:
        connection = np.zeros(faces.shape[1])
    phases = np.random.default_rng(seed).uniform(-spread, spread, space.size_)
    agents = GroupElement('agents', element=PhaseAgentElement, space=space, on='sites', values={'theta': phases})
    links = GroupElement(
        'links',
        element=ConnectionElement,
        space=space,
        on='edges',
        values={'A': connection},
    )
    coupling = TransportedCouplingInteraction('coupling', target='agents', links='links', K=K)
    return System(elements=[agents, links], interactions=[coupling])

hegselmann_krause(n_agents=200, *, confidence=0.15, graph=None, random_state=0)

Return the Hegselmann-Krause system, whose opinions freeze within a few steps into clusters about 2 ε apart.

Every step every agent moves at once to the mean opinion of its neighbours, itself included, whose opinions lie within the confidence ε of its own. On the complete graph the opinions settle into about 1 / (2 ε) clusters.

Parameters:

Name Type Description Default
n_agents int

The number of agents, the nodes of the complete graph when no graph is given.

200
confidence float

The confidence ε.

0.15
graph Graph | None

The network of the agents, or None for the complete graph.

None
random_state int | Generator | None

The seed or generator of the uniform initial opinions.

0

Returns:

Name Type Description
system System

The system, with the group people on the nodes of the network.

Examples:

>>> import numpy as np
>>> from skcomplex.core.engine.runtime import Simulation
>>> from skcomplex.systems.social.opinion_dynamics import hegselmann_krause
>>> trajectory = Simulation(
...     hegselmann_krause(50, confidence=0.3), random_state=0,
... ).prepare().run(until=20).trajectory_
>>> len(np.unique(np.round(trajectory.values('people.opinion')[-1], 6))) <= 3
True
Source code in src/skcomplex/systems/social/opinion_dynamics/_bounded_confidence.py
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
@register_system(
    field='social/opinion_dynamics',
    tags=('opinion', 'bounded confidence', 'clusters', 'network', 'synchronous'),
    references=('Hegselmann and Krause 2002', 'Castellano, Fortunato, and Loreto 2009'),
    defaults={'until': 30},
)
def hegselmann_krause(
    n_agents: int = 200,
    *,
    confidence: float = 0.15,
    graph: nx.Graph | None = None,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the Hegselmann-Krause system, whose opinions freeze within a few steps into clusters about `2 ε` apart.

    Every step every agent moves at once to the mean opinion of its neighbours, itself included, whose opinions lie
    within the confidence `ε` of its own. On the complete graph the opinions settle into about `1 / (2 ε)` clusters.

    Args:
        n_agents:
            The number of agents, the nodes of the complete graph when no graph is given.

        confidence:
            The confidence `ε`.

        graph:
            The network of the agents, or `None` for the complete graph.

        random_state:
            The seed or generator of the uniform initial opinions.

    Returns:
        system:
            The system, with the group `people` on the nodes of the network.

    Examples:
        >>> import numpy as np
        >>> from skcomplex.core.engine.runtime import Simulation
        >>> from skcomplex.systems.social.opinion_dynamics import hegselmann_krause
        >>> trajectory = Simulation(
        ...     hegselmann_krause(50, confidence=0.3), random_state=0,
        ... ).prepare().run(until=20).trajectory_
        >>> len(np.unique(np.round(trajectory.values('people.opinion')[-1], 6))) <= 3
        True
    """
    space = NetworkSpace(graph if graph is not None else nx.complete_graph(n_agents)).prepare()
    opinions = np.random.default_rng(random_state).random(space.size_)
    people = LatticeElement(
        'people',
        element=ConfidentElement,
        space=space,
        values={'opinion': opinions, 'confidence': confidence},
    )
    coupling = ConfidenceCouplingInteraction('compromise', target='people')
    return System(elements=[people], interactions=[coupling], time=DiscreteTime())

hypergraph_consensus(n_nodes=30, *, triangles=0.05, reinforcement=-1.0, period=2.0, seed=0)

Return the nonlinear three-body consensus system on a random hypergraph of triangles that switch on and off.

Every triple of nodes is a triangle with probability triangles, and every triangle is active in alternate half periods from a random phase. On an active triangle ijk, node i follows x_i' = s(|x_j - x_k|) (x_j + x_k - 2 x_i) with s(d) = exp(reinforcement * d). Without reinforcement the pulls of a triangle on its three members sum to zero, so the mean opinion stays fixed whatever the schedules. With reinforcement the pull depends on the member it acts on, and the mean opinion drifts towards the pairs that agree, for a negative reinforcement, while the opinions still reach a consensus on a connected hypergraph.

Parameters:

Name Type Description Default
n_nodes int

The number of nodes.

30
triangles float

The probability that a triple of nodes is a triangle.

0.05
reinforcement float

The exponent of the modulating function s.

-1.0
period float

The period of the schedules of the triangles.

2.0
seed int

The seed of the hypergraph, the phases, and the opinions.

0

Returns:

Name Type Description
system System

The system, with the opinions nodes.x, the mean opinion mean_opinion, and the mean squared opinion spread.

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import hypergraph_consensus
>>> system = hypergraph_consensus(8, triangles=0.5)
>>> system.elements[0].prepare().size_
8
Source code in src/skcomplex/systems/social/opinion_dynamics/_hypergraph_consensus.py
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
@register_system(
    field='social/opinion_dynamics',
    tags=('opinion', 'consensus', 'hypergraph', 'temporal network'),
    references=('Neuhäuser, Mellor, and Lambiotte 2020', 'Majhi et al. 2022'),
    defaults={'until': 10.0, 'dt': 0.1},
    observables=lambda system: [
        ObservableSpec('mean_opinion', group='nodes', variable='x'),
        ObservableSpec('spread', _spread, group='nodes'),
    ],
)
def hypergraph_consensus(
    n_nodes: int = 30,
    *,
    triangles: float = 0.05,
    reinforcement: float = -1.0,
    period: float = 2.0,
    seed: int = 0,
) -> System:
    """Return the nonlinear three-body consensus system on a random hypergraph of triangles that switch on and off.

    Every triple of nodes is a triangle with probability `triangles`, and every triangle is active in alternate half
    periods from a random phase. On an active triangle `ijk`, node `i` follows `x_i' = s(|x_j - x_k|) (x_j + x_k - 2
    x_i)` with `s(d) = exp(reinforcement * d)`. Without reinforcement the pulls of a triangle on its three members sum
    to zero, so the mean opinion stays fixed whatever the schedules. With reinforcement the pull depends on the member
    it acts on, and the mean opinion drifts towards the pairs that agree, for a negative reinforcement, while the
    opinions still reach a consensus on a connected hypergraph.

    Args:
        n_nodes:
            The number of nodes.

        triangles:
            The probability that a triple of nodes is a triangle.

        reinforcement:
            The exponent of the modulating function `s`.

        period:
            The period of the schedules of the triangles.

        seed:
            The seed of the hypergraph, the phases, and the opinions.

    Returns:
        system:
            The system, with the opinions `nodes.x`, the mean opinion `mean_opinion`, and the mean squared opinion
            `spread`.

    Examples:
        >>> from skcomplex.systems.social.opinion_dynamics import hypergraph_consensus
        >>> system = hypergraph_consensus(8, triangles=0.5)
        >>> system.elements[0].prepare().size_
        8
    """
    rng = np.random.default_rng(seed)
    triads = [triad for triad in itertools.combinations(range(n_nodes), TRIANGLE) if rng.random() < triangles]
    space = NetworkSpace(nx.complete_graph(n_nodes), hyperedges=triads)
    nodes = LatticeElement(
        'nodes',
        element=OpinionElement,
        space=space,
        values={'x': rng.uniform(-1.0, 1.0, n_nodes)},
        visual=VisualSpec('sites', {'states': ['x'], 'low': -1.0, 'high': 1.0}),
    )
    schedules = GroupElement(
        'triangles',
        element=ScheduleElement,
        space=space,
        on='hyperedges',
        values={'phase': rng.uniform(0.0, 2 * math.pi, len(triads))},
    )
    consensus = ThreeBodyConsensusInteraction(
        'consensus',
        target='nodes',
        triangles='triangles',
        reinforcement=reinforcement,
        period=period,
    )
    return System(elements=[nodes, schedules], interactions=[consensus])

influentials(width=20, height=20, *, n_influentials=10, influence=3.0, news_report=False, random_state=0)

Return the belief diffusion system with influentials who care passionately and weigh more with their neighbours.

Every cell of a torus holds one agent whose weights start uniform on 0 to 4. Every agent in turn, in a random order, learns from its neighbours and then sets its level of caring.

Parameters:

Name Type Description Default
width int

The number of columns.

20
height int

The number of rows.

20
n_influentials int

The number of influentials, chosen at random.

10
influence float

The influence of an influential, the others having influence 1.

3.0
news_report bool

Whether a news report adds one to every agent's weight of caring passionately at the start.

False
random_state int | Generator | None

The seed or generator of the initial beliefs and of the influentials.

0

Returns:

Name Type Description
system System

The system, with the group people.

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import influentials
>>> influentials(5, 4, n_influentials=2, random_state=0).elements[0].prepare().size_
20
Source code in src/skcomplex/systems/social/opinion_dynamics/_diffusion.py
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
@register_system(
    field='social/opinion_dynamics',
    tags=('opinion', 'agents', 'grid', 'influentials'),
    references=('Chesney 2021',),
    defaults={'until': 20},
)
def influentials(
    width: int = 20,
    height: int = 20,
    *,
    n_influentials: int = 10,
    influence: float = 3.0,
    news_report: bool = False,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the belief diffusion system with influentials who care passionately and weigh more with their neighbours.

    Every cell of a torus holds one agent whose weights start uniform on 0 to 4. Every agent in turn, in a random
    order, learns from its neighbours and then sets its level of caring.

    Args:
        width:
            The number of columns.

        height:
            The number of rows.

        n_influentials:
            The number of influentials, chosen at random.

        influence:
            The influence of an influential, the others having influence 1.

        news_report:
            Whether a news report adds one to every agent's weight of caring passionately at the start.

        random_state:
            The seed or generator of the initial beliefs and of the influentials.

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

    Examples:
        >>> from skcomplex.systems.social.opinion_dynamics import influentials
        >>> influentials(5, 4, n_influentials=2, random_state=0).elements[0].prepare().size_
        20
    """
    rng = np.random.default_rng(random_state)
    size = width * height
    belief = rng.integers(0, 5, (size, N_LEVELS)).astype(float)
    belief[:, PASSIONATE] += float(news_report)
    chosen = rng.choice(size, n_influentials, replace=False)
    belief[chosen, PASSIONATE] = belief[chosen].max(axis=1) + 1
    weights = np.ones(size)
    weights[chosen] = influence
    people = GroupElement(
        'people',
        element=BelieverElement,
        size=size,
        values={'belief': belief, 'caring': belief.argmax(axis=1).astype(float), 'influence': weights},
        location=[(column, row) for column in range(width) for row in range(height)],
    )
    learning = MapInteraction(
        'learning',
        target='people',
        state='belief',
        function=_learn,
        schedule='member',
        form='agent',
    )
    caring = MapInteraction('caring', target='people', state='caring', function=_care, schedule='member', form='agent')
    return System(
        elements=[people],
        interactions=[learning, caring],
        space=LatticeSpace((width, height), boundary='periodic', capacity=1),
    )

join_a_club(n_agents=200, *, mean_degree=6.0, beta=0.5, appeal=0.0, rewiring=0.5, random_state=0)

Return join-a-club spins, agents who join a club with their acquaintances while acquaintances follow the club.

Agents of an Erdős-Rényi graph are in or out of a club, a spin, and choose by a heat bath with the inverse temperature beta in the field of their acquaintances and of the appeal of the club. Acquaintances across the club boundary dissolve and reform among agents on the same side. Without coupling the choices stay random and many acquaintances cross the boundary, and with strong coupling the members and the outsiders separate into communities with few acquaintances between them.

Parameters:

Name Type Description Default
n_agents int

The number of agents.

200
mean_degree float

The mean number of acquaintances.

6.0
beta float

The inverse temperature of the heat bath.

0.5
appeal float

The field of the club on every agent.

0.0
rewiring float

The probability that a reconsidered acquaintance across the club boundary reforms on one side.

0.5
random_state int | Generator | None

The seed or generator of the graph and of the uniform random choices.

0

Returns:

Name Type Description
system System

The system, with the groups voters and ties and the interaction separation.

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import join_a_club
>>> join_a_club(30).elements[0].element.__name__
'ClubMemberElement'
Source code in src/skcomplex/systems/social/opinion_dynamics/_coevolution.py
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
361
362
363
364
365
@register_system(
    field='social/opinion_dynamics',
    tags=('opinion', 'spins', 'clubs', 'adaptive network', 'heat bath'),
    references=('Thurner et al. 2018', 'Biely, Hanel, and Thurner 2009'),
    defaults={'until': 150},
)
def join_a_club(
    n_agents: int = 200,
    *,
    mean_degree: float = 6.0,
    beta: float = 0.5,
    appeal: float = 0.0,
    rewiring: float = 0.5,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return join-a-club spins, agents who join a club with their acquaintances while acquaintances follow the club.

    Agents of an Erdős-Rényi graph are in or out of a club, a spin, and choose by a heat bath with the inverse
    temperature `beta` in the field of their acquaintances and of the appeal of the club. Acquaintances across the
    club boundary dissolve and reform among agents on the same side. Without coupling the choices stay random and
    many acquaintances cross the boundary, and with strong coupling the members and the outsiders separate into
    communities with few acquaintances between them.

    Args:
        n_agents:
            The number of agents.

        mean_degree:
            The mean number of acquaintances.

        beta:
            The inverse temperature of the heat bath.

        appeal:
            The field of the club on every agent.

        rewiring:
            The probability that a reconsidered acquaintance across the club boundary reforms on one side.

        random_state:
            The seed or generator of the graph and of the uniform random choices.

    Returns:
        system:
            The system, with the groups `voters` and `ties` and the interaction `separation`.

    Examples:
        >>> from skcomplex.systems.social.opinion_dynamics import join_a_club
        >>> join_a_club(30).elements[0].element.__name__
        'ClubMemberElement'
    """
    rng = np.random.default_rng(random_state)
    space = NetworkSpace(nx.gnp_random_graph(n_agents, mean_degree / (n_agents - 1), seed=rng))
    opinions = (rng.random(n_agents) < FIRST_OPINION_SHARE).astype(float)
    choice = MapInteraction('choice', target='voters', state='opinion', function=_choose_club)
    return _coevolving(ClubMemberElement, space, opinions, choice, beta=beta, appeal=appeal, rewiring=rewiring)

kinetic_opinion(levels=7, *, confidence=2, persuasion=0.3, repulsion=0.1, eta=1.0)

Return the kinetic system of opinion formation over a discrete grid of opinions.

The population is a distribution over opinion levels from -1 to 1, a site group on a lattice of the levels, which starts almost uniform with a slight lean toward positive opinions. Encounters between levels move an opinion one level toward a close opinion and one level away from a distant one, and keep the total density. With a wide confidence the opinions reach a consensus around the mean, and with a narrow confidence and repulsion they polarise into the two extremes.

Parameters:

Name Type Description Default
levels int

The number of opinion levels.

7
confidence int

The largest distance in levels at which encounters persuade.

2
persuasion float

The probability of moving toward a close opinion.

0.3
repulsion float

The probability of moving away from a distant opinion.

0.1
eta float

The encounter rate.

1.0

Returns:

Name Type Description
system System

The system, with the distribution opinions and the encounters encounters. Its standard observables are the mean opinion and the variance of the opinions.

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import kinetic_opinion
>>> kinetic_opinion(levels=5).elements[0].size
5
Source code in src/skcomplex/systems/social/opinion_dynamics/_kinetic_opinion.py
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
@register_system(
    field='social/opinion_dynamics',
    tags=('opinion', 'kinetic', 'distribution', 'polarization'),
    references=OPINION_REFERENCES,
    defaults={'until': 60, 'dt': 0.5},
    observables=lambda system: opinion_observables(_levels_of(system)),
)
def kinetic_opinion(
    levels: int = 7,
    *,
    confidence: int = 2,
    persuasion: float = 0.3,
    repulsion: float = 0.1,
    eta: float = 1.0,
) -> System:
    """Return the kinetic system of opinion formation over a discrete grid of opinions.

    The population is a distribution over opinion levels from -1 to 1, a site group on a lattice of the levels, which
    starts almost uniform with a slight lean toward positive opinions. Encounters between levels move an opinion one
    level toward a close opinion and one level away from a distant one, and keep the total density. With a wide
    confidence the opinions reach a consensus around the mean, and with a narrow confidence and repulsion they
    polarise into the two extremes.

    Args:
        levels:
            The number of opinion levels.

        confidence:
            The largest distance in levels at which encounters persuade.

        persuasion:
            The probability of moving toward a close opinion.

        repulsion:
            The probability of moving away from a distant opinion.

        eta:
            The encounter rate.

    Returns:
        system:
            The system, with the distribution `opinions` and the encounters `encounters`. Its standard observables are
            the mean opinion and the variance of the opinions.

    Examples:
        >>> from skcomplex.systems.social.opinion_dynamics import kinetic_opinion
        >>> kinetic_opinion(levels=5).elements[0].size
        5
    """
    initial = 1.0 + 0.1 * opinion_grid(levels)
    return System(
        elements=[
            GroupElement(
                'opinions',
                element=DensityElement,
                size=levels,
                on='sites',
                values={'f': initial / initial.sum()},
            ),
        ],
        interactions=[
            OpinionEncountersInteraction(
                'encounters',
                candidate='opinions',
                field='opinions',
                levels=levels,
                confidence=confidence,
                eta=eta,
                persuasion=persuasion,
                repulsion=repulsion,
            ),
        ],
        space=LatticeSpace(levels, boundary='fixed'),
        time=ContinuousTime(),
    )

largest_region(locations, cultures, width, height)

Return the fraction of the cells in the largest connected region of one culture.

Parameters:

Name Type Description Default
locations ArrayLike

The cell of every agent, one (x, y) row per agent.

required
cultures ArrayLike

The culture code of every agent.

required
width int

The number of columns of the grid.

required
height int

The number of rows of the grid.

required

Returns:

Name Type Description
fraction float

The size of the largest region of adjacent cells with the same culture over the number of cells.

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import largest_region
>>> largest_region([(0, 0), (1, 0), (0, 1), (1, 1)], [1, 1, 2, 3], 2, 2)
0.5
Source code in src/skcomplex/systems/social/opinion_dynamics/_axelrod.py
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
def largest_region(locations: npt.ArrayLike, cultures: npt.ArrayLike, width: int, height: int) -> float:
    """Return the fraction of the cells in the largest connected region of one culture.

    Args:
        locations:
            The cell of every agent, one `(x, y)` row per agent.

        cultures:
            The culture code of every agent.

        width:
            The number of columns of the grid.

        height:
            The number of rows of the grid.

    Returns:
        fraction:
            The size of the largest region of adjacent cells with the same culture over the number of cells.

    Examples:
        >>> from skcomplex.systems.social.opinion_dynamics import largest_region
        >>> largest_region([(0, 0), (1, 0), (0, 1), (1, 1)], [1, 1, 2, 3], 2, 2)
        0.5
    """
    cells = np.asarray(locations, dtype=np.int64).reshape(-1, 2)
    grid = np.full((width, height), -1.0)
    grid[cells[:, 0], cells[:, 1]] = np.asarray(cultures, dtype=float)
    largest = 0
    for culture in np.unique(grid[grid >= 0]):
        labels, count = ndimage.label(grid == culture)
        if count:
            largest = max(largest, int(np.bincount(labels.ravel())[1:].max()))
    return largest / (width * height)

majority_rule(shape=(100, 100), *, density=0.5, random_state=0)

Return voters that take the majority opinion of their neighbourhood, which coarsens into domains.

Every step every voter takes the opinion of the majority of its eight Moore neighbours and keeps its own when they tie, the majority rule of Argun et al. (2021, section 4.3).

Parameters:

Name Type Description Default
shape Sequence[int]

The number of rows and columns.

(100, 100)
density float

The probability that a voter starts with opinion 1.

0.5
random_state int | Generator | None

The seed or generator of the initial opinions.

0

Returns:

Name Type Description
system System

The system, with the opinions voters.opinion.

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import majority_rule
>>> majority_rule((10, 10)).elements[0].prepare().size_
100
Source code in src/skcomplex/systems/social/opinion_dynamics/_voters.py
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
@register_system(
    field='social/opinion_dynamics',
    tags=('lattice', 'opinion', 'voting'),
    references=('Argun et al. 2021', 'Gärtner and Zehmakan 2017'),
    defaults={'until': 50, 'dt': 1},
)
def majority_rule(
    shape: Sequence[int] = (100, 100),
    *,
    density: float = 0.5,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return voters that take the majority opinion of their neighbourhood, which coarsens into domains.

    Every step every voter takes the opinion of the majority of its eight Moore neighbours and keeps its own when they
    tie, the majority rule of Argun et al. (2021, section 4.3).

    Args:
        shape:
            The number of rows and columns.

        density:
            The probability that a voter starts with opinion 1.

        random_state:
            The seed or generator of the initial opinions.

    Returns:
        system:
            The system, with the opinions `voters.opinion`.

    Examples:
        >>> from skcomplex.systems.social.opinion_dynamics import majority_rule
        >>> majority_rule((10, 10)).elements[0].prepare().size_
        100
    """
    values = _random_cells(shape, density, random_state)
    return System(
        elements=[
            LatticeElement(
                'voters',
                element=MajorityVoterElement,
                space=LatticeSpace(tuple(shape)),
                values={'opinion': values},
            ),
        ],
        interactions=[
            LocalRuleInteraction(
                'majority',
                target='voters',
                state='opinion',
                rule=TotalisticSpec(_is_majority),
                neighborhood=MooreNeighborhood(center=True),
            ),
        ],
    )

mean_opinion(s, opinions)

Return the mean opinion of a distribution over opinion levels.

Parameters:

Name Type Description Default
s SimpleNamespace

The namespace of one member of the distribution.

required
opinions NDArray[float64]

The opinion of every level.

required

Returns:

Name Type Description
mean Expression

The normalized first moment.

Source code in src/skcomplex/systems/social/opinion_dynamics/_kinetic_opinion.py
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
def mean_opinion(s: SimpleNamespace, opinions: npt.NDArray[np.float64]) -> Expression:
    """Return the mean opinion of a distribution over opinion levels.

    Args:
        s:
            The namespace of one member of the distribution.

        opinions:
            The opinion of every level.

    Returns:
        mean:
            The normalized first moment.
    """
    return moment(s.f, 1, opinions, normalized=True)

naming_game(n_agents=50)

Return the minimal naming game, where agents with empty inventories agree on one word.

Parameters:

Name Type Description Default
n_agents int

The number of agents.

50

Returns:

Name Type Description
system System

The system, with the group namers.

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import naming_game
>>> naming_game(10).elements[0].prepare().size_
10
Source code in src/skcomplex/systems/social/opinion_dynamics/_conventions.py
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
@register_system(
    field='social/opinion_dynamics',
    tags=('opinion', 'agents', 'language', 'consensus'),
    references=('Baronchelli et al. 2006', 'Castellano, Fortunato, and Loreto 2009'),
    defaults={'until': 50},
)
def naming_game(n_agents: int = 50) -> System:
    """Return the minimal naming game, where agents with empty inventories agree on one word.

    Args:
        n_agents:
            The number of agents.

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

    Examples:
        >>> from skcomplex.systems.social.opinion_dynamics import naming_game
        >>> naming_game(10).elements[0].prepare().size_
        10
    """
    namers = GroupElement(
        'namers',
        element=NamerElement,
        size=n_agents,
        values={'words': np.zeros((n_agents, n_agents))},
    )
    return System(elements=[namers], interactions=[NamingGamesInteraction('games', target='namers')])

opinion_grid(levels)

Return the opinion of every level, evenly spaced from -1 to 1.

Parameters:

Name Type Description Default
levels int

The number of opinion levels.

required

Returns:

Name Type Description
opinions NDArray[float64]

The opinion of every level.

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import opinion_grid
>>> opinion_grid(5).tolist()
[-1.0, -0.5, 0.0, 0.5, 1.0]
Source code in src/skcomplex/systems/social/opinion_dynamics/_kinetic_opinion.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
def opinion_grid(levels: int) -> npt.NDArray[np.float64]:
    """Return the opinion of every level, evenly spaced from -1 to 1.

    Args:
        levels:
            The number of opinion levels.

    Returns:
        opinions:
            The opinion of every level.

    Examples:
        >>> from skcomplex.systems.social.opinion_dynamics import opinion_grid
        >>> opinion_grid(5).tolist()
        [-1.0, -0.5, 0.0, 0.5, 1.0]
    """
    return np.linspace(-1.0, 1.0, levels)

opinion_observables(levels)

Return the mean opinion and the variance of the opinions of a distribution opinions.

Parameters:

Name Type Description Default
levels int

The number of opinion levels.

required

Returns:

Name Type Description
observables list[ObservableSpec]

The observables, each reading the moments of the distribution.

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import opinion_observables
>>> [item.label for item in opinion_observables(7)]
['mean_opinion', 'opinion_variance']
Source code in src/skcomplex/systems/social/opinion_dynamics/_kinetic_opinion.py
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
def opinion_observables(levels: int) -> list[ObservableSpec]:
    """Return the mean opinion and the variance of the opinions of a distribution `opinions`.

    Args:
        levels:
            The number of opinion levels.

    Returns:
        observables:
            The observables, each reading the moments of the distribution.

    Examples:
        >>> from skcomplex.systems.social.opinion_dynamics import opinion_observables
        >>> [item.label for item in opinion_observables(7)]
        ['mean_opinion', 'opinion_variance']
    """
    opinions = opinion_grid(levels)
    return [
        ObservableSpec('mean_opinion', partial(mean_opinion, opinions=opinions), group='opinions'),
        ObservableSpec('opinion_variance', partial(opinion_variance, opinions=opinions), group='opinions'),
    ]

opinion_variance(s, opinions)

Return the variance of the opinions of a distribution, which grows as opinions polarise.

Parameters:

Name Type Description Default
s SimpleNamespace

The namespace of one member of the distribution.

required
opinions NDArray[float64]

The opinion of every level.

required

Returns:

Name Type Description
variance Expression

The central second moment.

Source code in src/skcomplex/systems/social/opinion_dynamics/_kinetic_opinion.py
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
def opinion_variance(s: SimpleNamespace, opinions: npt.NDArray[np.float64]) -> Expression:
    """Return the variance of the opinions of a distribution, which grows as opinions polarise.

    Args:
        s:
            The namespace of one member of the distribution.

        opinions:
            The opinion of every level.

    Returns:
        variance:
            The central second moment.
    """
    return moment(s.f, 2, opinions, central=True)

persuasion_table(levels, confidence, persuasion, repulsion)

Return the transition table of encounters between opinion levels, A[h, k, i].

A candidate at level h that meets a field particle at level k within confidence levels moves one level toward it with probability persuasion. One that meets a field particle further away moves one level away from it with probability repulsion, unless it holds an extreme opinion already.

Parameters:

Name Type Description Default
levels int

The number of opinion levels.

required
confidence int

The largest distance in levels at which encounters persuade.

required
persuasion object

The probability of moving toward a close opinion, a number or a parameter.

required
repulsion object

The probability of moving away from a distant opinion, a number or a parameter.

required

Returns:

Name Type Description
table NDArray[object_]

The transition probabilities, which sum to one over the outcomes.

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import persuasion_table
>>> persuasion_table(3, 1, 0.4, 0.1)[0, 1].astype(float).tolist()
[0.6, 0.4, 0.0]
Source code in src/skcomplex/systems/social/opinion_dynamics/_kinetic_opinion.py
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
def persuasion_table(levels: int, confidence: int, persuasion: object, repulsion: object) -> npt.NDArray[np.object_]:
    """Return the transition table of encounters between opinion levels, `A[h, k, i]`.

    A candidate at level `h` that meets a field particle at level `k` within `confidence` levels moves one level
    toward it with probability `persuasion`. One that meets a field particle further away moves one level away from it
    with probability `repulsion`, unless it holds an extreme opinion already.

    Args:
        levels:
            The number of opinion levels.

        confidence:
            The largest distance in levels at which encounters persuade.

        persuasion:
            The probability of moving toward a close opinion, a number or a parameter.

        repulsion:
            The probability of moving away from a distant opinion, a number or a parameter.

    Returns:
        table:
            The transition probabilities, which sum to one over the outcomes.

    Examples:
        >>> from skcomplex.systems.social.opinion_dynamics import persuasion_table
        >>> persuasion_table(3, 1, 0.4, 0.1)[0, 1].astype(float).tolist()
        [0.6, 0.4, 0.0]
    """
    table = np.zeros((levels, levels, levels), dtype=object)
    for candidate in range(levels):
        for field in range(levels):
            toward = int(np.sign(field - candidate))
            outcome, probability = (
                (candidate + toward, persuasion)
                if abs(field - candidate) <= confidence
                else (candidate - toward, repulsion)
            )
            if toward and 0 <= outcome < levels:
                table[candidate, field, outcome] += probability
            table[candidate, field, candidate] += 1 - table[candidate, field].sum()
    return table

settle(valences, decay=0.05)

Return the valences of a belief network updated until no valence changes by more than 0.01.

The network links the first concept to the second with weight 0.05 and the second to the third with weight -0.05. Every update decays a valence by decay and moves it towards 1 or -1 by its net input.

Parameters:

Name Type Description Default
valences ArrayLike

The valences of the three concepts, between -1 and 1.

required
decay float

The fraction of every valence lost per update.

0.05

Returns:

Name Type Description
valences Array

The settled valences.

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import settle
>>> settle([1.0, 1.0, -1.0]).round(2).tolist()
[0.48, 0.57, -0.48]
Source code in src/skcomplex/systems/social/opinion_dynamics/_cognition.py
 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
def settle(valences: npt.ArrayLike, decay: float = 0.05) -> Array:
    """Return the valences of a belief network updated until no valence changes by more than 0.01.

    The network links the first concept to the second with weight 0.05 and the second to the third with weight
    -0.05. Every update decays a valence by `decay` and moves it towards 1 or -1 by its net input.

    Args:
        valences:
            The valences of the three concepts, between -1 and 1.

        decay:
            The fraction of every valence lost per update.

    Returns:
        valences:
            The settled valences.

    Examples:
        >>> from skcomplex.systems.social.opinion_dynamics import settle
        >>> settle([1.0, 1.0, -1.0]).round(2).tolist()
        [0.48, 0.57, -0.48]
    """
    current = np.asarray(valences, dtype=float)
    for _ in range(MAX_SETTLING):
        following = _settle_once(current, decay)
        if np.abs(following - current).max() < SETTLED:
            return following
        current = following
    return current

social_impact(width=15, height=15, *, share=0.5, exponent=2.0, noise=0.0, random_state=0)

Return the social impact system on a full grid, with random persuasiveness and supportiveness.

Parameters:

Name Type Description Default
width int

The number of columns.

15
height int

The number of rows.

15
share float

The share of agents with opinion 1, the others holding -1.

0.5
exponent float

The exponent of the distance by which impact decays.

2.0
noise float

The half-width of the uniform random field.

0.0
random_state int | Generator | None

The seed or generator of the opinions and strengths.

0

Returns:

Name Type Description
system System

The system, with the group people.

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import social_impact
>>> social_impact(4, 5, random_state=0).elements[0].prepare().size_
20
Source code in src/skcomplex/systems/social/opinion_dynamics/_persuasion.py
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
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
@register_system(
    field='social/opinion_dynamics',
    tags=('opinion', 'agents', 'grid', 'clusters'),
    references=('Nowak, Szamrej, and Latané 1990', 'Castellano, Fortunato, and Loreto 2009'),
    defaults={'until': 20},
)
def social_impact(
    width: int = 15,
    height: int = 15,
    *,
    share: float = 0.5,
    exponent: float = 2.0,
    noise: float = 0.0,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the social impact system on a full grid, with random persuasiveness and supportiveness.

    Args:
        width:
            The number of columns.

        height:
            The number of rows.

        share:
            The share of agents with opinion 1, the others holding -1.

        exponent:
            The exponent of the distance by which impact decays.

        noise:
            The half-width of the uniform random field.

        random_state:
            The seed or generator of the opinions and strengths.

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

    Examples:
        >>> from skcomplex.systems.social.opinion_dynamics import social_impact
        >>> social_impact(4, 5, random_state=0).elements[0].prepare().size_
        20
    """
    rng = np.random.default_rng(random_state)
    size = width * height
    people = GroupElement(
        'people',
        element=InfluenceeElement,
        size=size,
        values={
            'opinion': 2 * _shares(size, share, rng) - 1,
            'persuasiveness': rng.random(size),
            'supportiveness': rng.random(size),
            'exponent': exponent,
            'noise': noise,
        },
        location=[(column, row) for column in range(width) for row in range(height)],
    )
    return System(
        elements=[people],
        interactions=[SocialImpactInteraction('impact', target='people')],
        space=LatticeSpace((width, height), boundary='fixed', capacity=1),
    )

standardized_agents(agents=100, *, K=10.0, J=1.0, T=0.5, H=50, standardized=1.0, random_state=0)

Return complex agents whose interactions make them standardized, all showing one characteristic, and ordered.

Below the temperature T_S = K / (2 ln(H / 2)) the most probable share of agents with the characteristic jumps from q ~ 0 to q ~ 1, a discontinuous transition, and below T_M = J the standardized agents order, with a magnetization m of either sign. Both states are long-lived on either side of T_S in large groups, so the initial share standardized selects the branch that the run follows.

Parameters:

Name Type Description Default
agents int

The number of agents N.

100
K float

The coupling of the characteristics |S_i|, which standardizes the agents.

10.0
J float

The coupling of the spins S_i, which orders the standardized agents.

1.0
T float

The temperature, the randomness of the choices.

0.5
H int

The number of configurations without the characteristic.

50
standardized float

The initial share of agents with the characteristic, each with a random spin. The others start in random configurations without it.

1.0
random_state int | Generator | None

The seed or generator of the initial configurations.

0

Returns:

Name Type Description
system System

The system, with the group agents, the share q of agents with the characteristic, and the magnetization m, the mean spin.

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import standardized_agents
>>> standardized_agents(10, standardized=0.0).elements[0].values['spin'].tolist()
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
Source code in src/skcomplex/systems/social/opinion_dynamics/_standardization.py
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
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
@register_system(
    field='social/opinion_dynamics',
    tags=('agents', 'standardization', 'phase transition', 'large deviations', 'mean field'),
    references=('Bertin 2021', 'Bertin and Jensen 2019'),
    defaults={'until': 1000},
    observables=lambda system: [
        ObservableSpec('q', group='agents', variable='spin', reduce=_standardization),
        ObservableSpec('m', group='agents', variable='spin', reduce='mean'),
    ],
)
def standardized_agents(
    agents: int = 100,
    *,
    K: float = 10.0,
    J: float = 1.0,
    T: float = 0.5,
    H: int = 50,
    standardized: float = 1.0,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return complex agents whose interactions make them standardized, all showing one characteristic, and ordered.

    Below the temperature `T_S = K / (2 ln(H / 2))` the most probable share of agents with the characteristic jumps
    from `q ~ 0` to `q ~ 1`, a discontinuous transition, and below `T_M = J` the standardized agents order, with a
    magnetization `m` of either sign. Both states are long-lived on either side of `T_S` in large groups, so the
    initial share `standardized` selects the branch that the run follows.

    Args:
        agents:
            The number of agents `N`.

        K:
            The coupling of the characteristics `|S_i|`, which standardizes the agents.

        J:
            The coupling of the spins `S_i`, which orders the standardized agents.

        T:
            The temperature, the randomness of the choices.

        H:
            The number of configurations without the characteristic.

        standardized:
            The initial share of agents with the characteristic, each with a random spin. The others start in random
            configurations without it.

        random_state:
            The seed or generator of the initial configurations.

    Returns:
        system:
            The system, with the group `agents`, the share `q` of agents with the characteristic, and the magnetization
            `m`, the mean spin.

    Examples:
        >>> from skcomplex.systems.social.opinion_dynamics import standardized_agents
        >>> standardized_agents(10, standardized=0.0).elements[0].values['spin'].tolist()
        [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
    """
    rng = np.random.default_rng(random_state)
    marked = rng.random(agents) < standardized
    configurations = np.where(marked, rng.integers(MARKED, size=agents), rng.integers(MARKED, H + MARKED, size=agents))
    configurations = configurations.astype(float)
    return System(
        elements=[
            GroupElement(
                'agents',
                element=ComplexAgentElement,
                size=agents,
                values={'configuration': configurations, 'spin': StandardizationInteraction._spins_of(configurations)},
            ),
        ],
        interactions=[
            StandardizationInteraction('standardization', target='agents', K=K, J=J, H=H, intensity=1 / T, phase=PHASE),
            MapInteraction(
                'spin',
                target='agents',
                state='spin',
                function=_spin_of,
                schedule='member',
                form='agent',
                phase=PHASE,
            ),
        ],
        time=DiscreteTime(schedule=PhasesScheduleSpec([PhaseSpec(PHASE, activation='visits', groups=('agents',))])),
    )

sznajd(graph=None, *, share=0.5, random_state=0)

Return the Sznajd system, where agreeing pairs of agents convince their neighbours.

Parameters:

Name Type Description Default
graph object

The NetworkX graph, or None for a ring of 50 agents.

None
share float

The share of agents with opinion 1.

0.5
random_state int | Generator | None

The seed or generator of the opinions.

0

Returns:

Name Type Description
system System

The system, with the group people and the persuasion persuasion.

Examples:

>>> import networkx as nx
>>> from skcomplex.systems.social.opinion_dynamics import sznajd
>>> sznajd(nx.cycle_graph(8), random_state=0).elements[0].prepare().size_
8
Source code in src/skcomplex/systems/social/opinion_dynamics/_persuasion.py
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
@register_system(
    field='social/opinion_dynamics',
    tags=('opinion', 'agents', 'network', 'consensus'),
    references=('Sznajd-Weron and Sznajd 2000', 'Castellano, Fortunato, and Loreto 2009'),
    defaults={'until': 100},
)
def sznajd(
    graph: object = None,
    *,
    share: float = 0.5,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the Sznajd system, where agreeing pairs of agents convince their neighbours.

    Args:
        graph:
            The NetworkX graph, or `None` for a ring of 50 agents.

        share:
            The share of agents with opinion 1.

        random_state:
            The seed or generator of the opinions.

    Returns:
        system:
            The system, with the group `people` and the persuasion `persuasion`.

    Examples:
        >>> import networkx as nx
        >>> from skcomplex.systems.social.opinion_dynamics import sznajd
        >>> sznajd(nx.cycle_graph(8), random_state=0).elements[0].prepare().size_
        8
    """
    graph = nx.cycle_graph(50) if graph is None else graph
    size = nx.number_of_nodes(graph)
    people = GroupElement(
        'people',
        element=PersuaderElement,
        size=size,
        values={'opinion': _shares(size, share, random_state)},
    )
    return System(
        elements=[people],
        interactions=[PersuasionInteraction('persuasion', target='people')],
        space=NetworkSpace(graph),
    )

three_state_opinion(n_agents=200, *, degree=4, sensitivity=2.0, random_state=0)

Return the three-state opinion system of agents against, undecided, and for, on a random regular network.

Every step the agents redraw their opinions one after the other in random order, each from the sum x of the opinions of its neighbours: against with the probability f(-beta x), for with f(beta x), and undecided otherwise, where f(t) = 1 / (2 (1 + exp(-t))) (Jensen 2023 section 11.5, Project 1). Since f(t) + f(-t) = 1 / 2, half of the draws are undecided. In mean field, with every agent seeing q <x>, the mean opinion solves <x> = tanh(beta q <x> / 2) / 2, which has a non-zero solution only above beta_c = 4 / q. On the sparse network, domains of both signs and the undecided half keep the order weaker than in mean field.

Parameters:

Name Type Description Default
n_agents int

The number of agents.

200
degree int

The number q of neighbours of every agent.

4
sensitivity float

The sensitivity beta of the opinion to the sum of the opinions of the neighbours.

2.0
random_state int | Generator | None

The seed or generator of the network and of the initial opinions.

0

Returns:

Name Type Description
system System

The system, with the group voters.

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import three_state_opinion
>>> three_state_opinion(20).elements[0].prepare().size_
20
Source code in src/skcomplex/systems/social/opinion_dynamics/_undecided.py
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
@register_system(
    field='social/opinion_dynamics',
    tags=('opinion', 'agents', 'network', 'undecided'),
    references=('Jensen 2023',),
    defaults={'until': 50},
)
def three_state_opinion(
    n_agents: int = 200,
    *,
    degree: int = 4,
    sensitivity: float = 2.0,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the three-state opinion system of agents against, undecided, and for, on a random regular network.

    Every step the agents redraw their opinions one after the other in random order, each from the sum `x` of the
    opinions of its neighbours: against with the probability `f(-beta x)`, for with `f(beta x)`, and undecided
    otherwise, where `f(t) = 1 / (2 (1 + exp(-t)))` (Jensen 2023 section 11.5, Project 1). Since
    `f(t) + f(-t) = 1 / 2`, half of the draws are undecided. In mean field, with every agent seeing `q <x>`, the mean
    opinion solves `<x> = tanh(beta q <x> / 2) / 2`, which has a non-zero solution only above `beta_c = 4 / q`. On the
    sparse network, domains of both signs and the undecided half keep the order weaker than in mean field.

    Args:
        n_agents:
            The number of agents.

        degree:
            The number `q` of neighbours of every agent.

        sensitivity:
            The sensitivity `beta` of the opinion to the sum of the opinions of the neighbours.

        random_state:
            The seed or generator of the network and of the initial opinions.

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

    Examples:
        >>> from skcomplex.systems.social.opinion_dynamics import three_state_opinion
        >>> three_state_opinion(20).elements[0].prepare().size_
        20
    """
    rng = np.random.default_rng(random_state)
    graph = nx.random_regular_graph(degree, n_agents, seed=int(rng.integers(2**31)))
    voters = GroupElement(
        'voters',
        element=UndecidedElement,
        size=n_agents,
        values={'opinion': rng.choice([-1.0, 0.0, 1.0], n_agents), 'sensitivity': sensitivity},
    )
    redraw = MapInteraction(
        'redraw',
        target='voters',
        state='opinion',
        function=_redraw,
        schedule='random-sequential',
        form='agent',
    )
    return System(elements=[voters], interactions=[redraw], space=NetworkSpace(graph))

threshold_adoption(n_agents=300, *, size=175.0, reach=15.0, mode='infection', thresholds='uniform', shifting=0.0, random_state=0)

Return the threshold adoption system in social circles, started by the 2.5% of agents with the lowest thresholds.

Parameters:

Name Type Description Default
n_agents int

The number of agents, placed at random on a square torus.

300
size float

The side of the torus.

175.0
reach float

The distance within which other agents form an agent's social circle.

15.0
mode str

'infection' to adopt after one adopter in the circle, or 'influence' to adopt above a threshold.

'infection'
thresholds str

'uniform' for thresholds uniform on 1 to 100 percent, or 'normal' for a normal distribution of mean 50 and standard deviation 50 clipped to 1 to 100.

'uniform'
shifting float

The share of agents that move one unit in a random direction every step.

0.0
random_state int | Generator | None

The seed or generator of the positions and thresholds.

0

Returns:

Name Type Description
system System

The system, with the group people.

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import threshold_adoption
>>> threshold_adoption(40, size=60.0, random_state=0).elements[0].prepare().size_
40
Source code in src/skcomplex/systems/social/opinion_dynamics/_diffusion.py
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
402
403
@register_system(
    field='social/opinion_dynamics',
    tags=('opinion', 'agents', 'innovation', 'threshold'),
    references=('Granovetter 1978', 'Hamill and Gilbert 2016'),
    defaults={'until': 25},
)
def threshold_adoption(
    n_agents: int = 300,
    *,
    size: float = 175.0,
    reach: float = 15.0,
    mode: str = 'infection',
    thresholds: str = 'uniform',
    shifting: float = 0.0,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the threshold adoption system in social circles, started by the 2.5% of agents with the lowest thresholds.

    Args:
        n_agents:
            The number of agents, placed at random on a square torus.

        size:
            The side of the torus.

        reach:
            The distance within which other agents form an agent's social circle.

        mode:
            `'infection'` to adopt after one adopter in the circle, or `'influence'` to adopt above a threshold.

        thresholds:
            `'uniform'` for thresholds uniform on 1 to 100 percent, or `'normal'` for a normal distribution of mean 50
            and standard deviation 50 clipped to 1 to 100.

        shifting:
            The share of agents that move one unit in a random direction every step.

        random_state:
            The seed or generator of the positions and thresholds.

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

    Examples:
        >>> from skcomplex.systems.social.opinion_dynamics import threshold_adoption
        >>> threshold_adoption(40, size=60.0, random_state=0).elements[0].prepare().size_
        40
    """
    rng = np.random.default_rng(random_state)
    drawn = rng.uniform(1, 100, n_agents) if thresholds == 'uniform' else np.clip(rng.normal(50, 50, n_agents), 1, 100)
    innovators = np.argsort(drawn)[: max(1, round(INNOVATORS * n_agents))]
    adopted = np.zeros(n_agents)
    adopted[innovators] = 1.0
    people = GroupElement(
        'people',
        element=AdopterElement,
        size=n_agents,
        values={'adopted': adopted, 'threshold': drawn, 'reach': reach},
        location=rng.uniform(0, size, (n_agents, 2)),
    )
    return System(
        elements=[people],
        interactions=[
            MovementInteraction(
                'shift',
                target='people',
                step=lambda agent, system: _shift(agent, system, size),
                probability=shifting,
            ),
            AdoptionInteraction('adoption', target='people', mode=mode),
        ],
        space=ContinuousSpace(extent=(size, size), boundary='periodic'),
    )

undecided_flows(n_agents=100.0, *, omega=1.0, gamma=0.05, eta=0.02, driven=True)

Return the mean-field flows between agents against, A, undecided, C, and for, B.

Undecided agents join either side at the rate omega, and agents against and for become undecided at the rates gamma and eta, with no direct flow between the sides, so N = A + B + C is conserved (Jensen 2023 section 11.5, Project 1). With constant rates, driven=False, dA/dt = omega C - gamma A, and the population settles at the attractive fixed point A : B : C = omega / gamma : omega / eta : 1. When the undecided drive the doubt, driven=True, the flows into the undecided grow with their number, dA/dt = omega C - gamma A C, and the dynamics change at N_c = omega (1 / gamma + 1 / eta): above it A = omega / gamma, B = omega / eta, and N - N_c agents never make up their mind, and below it every agent ends on one side.

Parameters:

Name Type Description Default
n_agents float

The number of agents, all undecided at the start.

100.0
omega float

The rate at which an undecided agent joins either side.

1.0
gamma float

The rate at which an agent against becomes undecided.

0.05
eta float

The rate at which an agent for becomes undecided.

0.02
driven bool

Whether the flows into the undecided are in proportion to their number.

True

Returns:

Name Type Description
system System

The system, with the numbers population.against, population.undecided, and population.support, and the flows population.joining_against, population.joining_support, population.doubting_against, and population.doubting_support. The observable of the system is population.critical_size.

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import undecided_flows
>>> undecided_flows(driven=False).elements[0].drive
0.0
Source code in src/skcomplex/systems/social/opinion_dynamics/_undecided.py
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
@register_system(
    field='social/opinion_dynamics',
    tags=('opinion', 'undecided', 'mean field', 'stocks and flows', 'ode'),
    references=('Jensen 2023',),
    defaults={'until': 20.0, 'dt': 0.1},
    observables=lambda system: [ObservableSpec('critical_size', _critical_size, element='population')],
)
def undecided_flows(
    n_agents: float = 100.0,
    *,
    omega: float = 1.0,
    gamma: float = 0.05,
    eta: float = 0.02,
    driven: bool = True,
) -> System:
    """Return the mean-field flows between agents against, `A`, undecided, `C`, and for, `B`.

    Undecided agents join either side at the rate `omega`, and agents against and for become undecided at the rates
    `gamma` and `eta`, with no direct flow between the sides, so `N = A + B + C` is conserved (Jensen 2023 section
    11.5, Project 1). With constant rates, `driven=False`, `dA/dt = omega C - gamma A`, and the population settles at
    the attractive fixed point `A : B : C = omega / gamma : omega / eta : 1`. When the undecided drive the doubt,
    `driven=True`, the flows into the undecided grow with their number, `dA/dt = omega C - gamma A C`, and the
    dynamics change at `N_c = omega (1 / gamma + 1 / eta)`: above it `A = omega / gamma`, `B = omega / eta`, and
    `N - N_c` agents never make up their mind, and below it every agent ends on one side.

    Args:
        n_agents:
            The number of agents, all undecided at the start.

        omega:
            The rate at which an undecided agent joins either side.

        gamma:
            The rate at which an agent against becomes undecided.

        eta:
            The rate at which an agent for becomes undecided.

        driven:
            Whether the flows into the undecided are in proportion to their number.

    Returns:
        system:
            The system, with the numbers `population.against`, `population.undecided`, and `population.support`, and
            the flows `population.joining_against`, `population.joining_support`, `population.doubting_against`, and
            `population.doubting_support`. The observable of the system is `population.critical_size`.

    Examples:
        >>> from skcomplex.systems.social.opinion_dynamics import undecided_flows
        >>> undecided_flows(driven=False).elements[0].drive
        0.0
    """
    population = UndecidedFlowsElement(
        'population',
        undecided=n_agents,
        omega=omega,
        gamma=gamma,
        eta=eta,
        drive=1.0 if driven else 0.0,
    )
    flows = TransferInteraction(
        'flows',
        source='population',
        target='population',
        state='undecided',
        into='against',
        amount=_joining_against,
        flow='joining_against',
        legs=[
            ('undecided', 'support', _joining_support, 'joining_support'),
            ('against', 'undecided', _doubting_against, 'doubting_against'),
            ('support', 'undecided', _doubting_support, 'doubting_support'),
        ],
    )
    return System(elements=[population], interactions=[flows])

voter_system(graph=None, *, share=0.5, random_state=0)

Return a voter system with one voter per node of a network.

Parameters:

Name Type Description Default
graph object

The NetworkX graph, or None for a ring of 50 nodes each linked to its four nearest neighbours.

None
share float

The share of voters with opinion 1.

0.5
random_state int | Generator | None

The seed or generator of the opinions.

0

Returns:

Name Type Description
system System

The system, with the group voters.

Examples:

>>> import networkx as nx
>>> from skcomplex.systems.social.opinion_dynamics import voter_system
>>> voter_system(nx.cycle_graph(8), random_state=0).elements[0].prepare().size_
8
Source code in src/skcomplex/systems/social/opinion_dynamics/_voters.py
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
@register_system(
    field='social/opinion_dynamics',
    tags=('opinion', 'agents', 'network'),
    references=('Holley and Liggett 1975',),
    defaults={'until': 100},
)
def voter_system(
    graph: object = None,
    *,
    share: float = 0.5,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return a voter system with one voter per node of a network.

    Args:
        graph:
            The NetworkX graph, or `None` for a ring of 50 nodes each linked to its four nearest neighbours.

        share:
            The share of voters with opinion 1.

        random_state:
            The seed or generator of the opinions.

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

    Examples:
        >>> import networkx as nx
        >>> from skcomplex.systems.social.opinion_dynamics import voter_system
        >>> voter_system(nx.cycle_graph(8), random_state=0).elements[0].prepare().size_
        8
    """
    rng = np.random.default_rng(random_state)
    graph = nx.watts_strogatz_graph(50, 4, 0.0) if graph is None else graph
    size = nx.number_of_nodes(graph)
    opinions = (rng.random(size) < share).astype(float)
    return System(
        elements=[GroupElement('voters', element=VoterElement, size=size, values={'opinion': opinions})],
        interactions=[VoterImitationInteraction('imitation', target='voters')],
        space=NetworkSpace(graph),
    )

voter_variants(graph=None, *, threshold=0.5, contrarians=0.1, share=0.5, random_state=0)

Return voter system variants on a network, with threshold voters and a share of contrarians.

Parameters:

Name Type Description Default
graph object

The NetworkX graph, or None for a ring of 50 nodes each linked to its four nearest neighbours.

None
threshold float

The share of disagreeing neighbours above which a voter changes its opinion, 0.5 for the majority rule, or 0 to copy a random neighbour.

0.5
contrarians float

The share of voters that are contrarians.

0.1
share float

The share of voters with opinion 1.

0.5
random_state int | Generator | None

The seed or generator of the opinions and of the contrarians.

0

Returns:

Name Type Description
system System

The system, with the group voters.

Examples:

>>> import networkx as nx
>>> from skcomplex.systems.social.opinion_dynamics import voter_variants
>>> voter_variants(nx.cycle_graph(8), random_state=0).elements[0].prepare().size_
8
Source code in src/skcomplex/systems/social/opinion_dynamics/_voters.py
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
@register_system(
    field='social/opinion_dynamics',
    tags=('opinion', 'agents', 'network', 'voting', 'contrarians'),
    references=('Thurner, Hanel, and Klimek 2018', 'Galam 2004'),
    defaults={'until': 100},
)
def voter_variants(
    graph: object = None,
    *,
    threshold: float = 0.5,
    contrarians: float = 0.1,
    share: float = 0.5,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return voter system variants on a network, with threshold voters and a share of contrarians.

    Args:
        graph:
            The NetworkX graph, or `None` for a ring of 50 nodes each linked to its four nearest neighbours.

        threshold:
            The share of disagreeing neighbours above which a voter changes its opinion, 0.5 for the majority rule, or
            0 to copy a random neighbour.

        contrarians:
            The share of voters that are contrarians.

        share:
            The share of voters with opinion 1.

        random_state:
            The seed or generator of the opinions and of the contrarians.

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

    Examples:
        >>> import networkx as nx
        >>> from skcomplex.systems.social.opinion_dynamics import voter_variants
        >>> voter_variants(nx.cycle_graph(8), random_state=0).elements[0].prepare().size_
        8
    """
    rng = np.random.default_rng(random_state)
    graph = nx.watts_strogatz_graph(50, 4, 0.0) if graph is None else graph
    size = nx.number_of_nodes(graph)
    voters = GroupElement(
        'voters',
        element=VariantVoterElement,
        size=size,
        values={
            'opinion': (rng.random(size) < share).astype(float),
            'contrarian': (rng.random(size) < contrarians).astype(float),
            'threshold': threshold,
        },
    )
    return System(
        elements=[voters],
        interactions=[VariantVotingInteraction('voting', target='voters')],
        space=NetworkSpace(graph),
    )

willing_majority(n_agents=200, *, group_size=3, zealots=0.0, zealot_opinion=None, willingness=1.0, random_state=0)

Return the majority rule with willingness, where zealots of willingness 0 never change their opinion.

Every agent holds opinion A or B, drawn with probability 1/2, and a willingness w_i. An update chooses an agent and an odd number n of other agents at random, and the agent adopts the majority opinion of the group with the probability w_i (Jensen 2023 section 11.5, Exercise 4). Every agent has w_i = willingness, except a fraction zealots of them with w_i = 0. With every agent willing, the population reaches consensus. Zealots of both opinions prevent it, while a small group of zealots of one opinion, zealot_opinion, wins over everyone.

Parameters:

Name Type Description Default
n_agents int

The number of agents.

200
group_size int

The odd number n of other agents an agent consults.

3
zealots float

The fraction f of agents with willingness 0.

0.0
zealot_opinion float | None

The opinion, 1 for A or 0 for B, every zealot starts with, or None for random opinions.

None
willingness float

The willingness of the other agents.

1.0
random_state int | Generator | None

The seed or generator of the opinions and of the zealots.

0

Returns:

Name Type Description
system System

The system, with the group voters.

Examples:

>>> from skcomplex.systems.social.opinion_dynamics import willing_majority
>>> willing_majority(30).elements[0].prepare().size_
30
Source code in src/skcomplex/systems/social/opinion_dynamics/_willing_majority.py
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
@register_system(
    field='social/opinion_dynamics',
    tags=('opinion', 'agents', 'majority', 'zealots', 'consensus'),
    references=('Jensen 2023',),
    defaults={'until': 50},
)
def willing_majority(
    n_agents: int = 200,
    *,
    group_size: int = 3,
    zealots: float = 0.0,
    zealot_opinion: float | None = None,
    willingness: float = 1.0,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the majority rule with willingness, where zealots of willingness 0 never change their opinion.

    Every agent holds opinion `A` or `B`, drawn with probability 1/2, and a willingness `w_i`. An update chooses an
    agent and an odd number `n` of other agents at random, and the agent adopts the majority opinion of the group with
    the probability `w_i` (Jensen 2023 section 11.5, Exercise 4). Every agent has `w_i = willingness`, except a
    fraction `zealots` of them with `w_i = 0`. With every agent willing, the population reaches consensus. Zealots of
    both opinions prevent it, while a small group of zealots of one opinion, `zealot_opinion`, wins over everyone.

    Args:
        n_agents:
            The number of agents.

        group_size:
            The odd number `n` of other agents an agent consults.

        zealots:
            The fraction `f` of agents with willingness 0.

        zealot_opinion:
            The opinion, 1 for `A` or 0 for `B`, every zealot starts with, or `None` for random opinions.

        willingness:
            The willingness of the other agents.

        random_state:
            The seed or generator of the opinions and of the zealots.

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

    Examples:
        >>> from skcomplex.systems.social.opinion_dynamics import willing_majority
        >>> willing_majority(30).elements[0].prepare().size_
        30
    """
    rng = np.random.default_rng(random_state)
    opinions = rng.integers(2, size=n_agents).astype(float)
    stubborn = rng.random(n_agents) < zealots
    if zealot_opinion is not None:
        opinions[stubborn] = zealot_opinion
    voters = GroupElement(
        'voters',
        element=WillingVoterElement,
        size=n_agents,
        values={'opinion': opinions, 'willingness': np.where(stubborn, 0.0, willingness)},
    )
    majority = WillingMajorityInteraction('majority', target='voters', group_size=group_size)
    return System(elements=[voters], interactions=[majority])