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 model, 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.elements import GroupElement
>>> from skcomplex.models.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/models/social/opinion_dynamics/_adopter.py
44
45
46
47
48
49
50
51
52
53
54
55
56
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 model, with its states and parameters as attributes.

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

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

Bases: TransitionInteraction

Synchronous adoption of the threshold model, 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.models.social.opinion_dynamics import AdoptionInteraction
>>> AdoptionInteraction('adoption', target='people', mode='influence').check().mode
'influence'
Source code in src/skcomplex/models/social/opinion_dynamics/_adoption.py
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
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 model, 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.elements import GroupElement
>>> from skcomplex.models.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/models/social/opinion_dynamics/_believer.py
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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 model, with its states and parameters as attributes.

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

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

Bases: Element

Agent of the Galam majority model, 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.elements import GroupElement
>>> from skcomplex.models.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/models/social/opinion_dynamics/_citizen.py
36
37
38
39
40
41
42
43
44
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 model, with its states and parameters as attributes.

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

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.models.social.opinion_dynamics import ClubMemberElement
>>> ClubMemberElement('agents', beta=2.0).beta
2.0
Source code in src/skcomplex/models/social/opinion_dynamics/_club_member.py
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
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.models.social.opinion_dynamics import CoevolvingVoterElement
>>> CoevolvingVoterElement('voters', rewiring=0.3).rewiring
0.3
Source code in src/skcomplex/models/social/opinion_dynamics/_coevolving_voter.py
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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.elements import GroupElement
>>> from skcomplex.models.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/models/social/opinion_dynamics/_complex_agent.py
43
44
45
46
47
48
49
50
51
52
53
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 model, with its states and parameters as attributes.

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

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.elements import LatticeElement
>>> from skcomplex.core.spaces import DiscreteTime, NetworkSpace
>>> from skcomplex.core.system import System
>>> from skcomplex.engine.runtime import Simulation
>>> from skcomplex.models.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).check().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/models/social/opinion_dynamics/_confidence_coupling.py
55
56
57
58
59
60
61
62
63
64
65
66
67
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 model, 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.models.social.opinion_dynamics import ConfidentElement
>>> ConfidentElement('people', confidence=0.1).confidence
0.1
Source code in src/skcomplex/models/social/opinion_dynamics/_confident.py
40
41
42
43
44
45
46
47
48
49
50
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

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

Bases: GroupingInteraction

Conversations of the cognitive consistency model, 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.models.social.opinion_dynamics import ConversationsInteraction
>>> ConversationsInteraction('talks', target='thinkers').groups
'neighborhood'
Source code in src/skcomplex/models/social/opinion_dynamics/_conversations.py
89
90
91
92
93
94
95
96
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, model)

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

The model view of the rule.

required
Source code in src/skcomplex/models/social/opinion_dynamics/_conversations.py
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
def act_on(self, members: list[AgentViewProtocol], model: ModelViewProtocol) -> 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.

        model:
            The model view of the rule.
    """
    speaker, *neighbors = [member for member in members if isinstance(member, ThinkerView)]
    if not neighbors:
        return
    hearer = neighbors[int(model.rng.integers(len(neighbors)))]
    topic = TOPICS[int(model.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 model, 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.models.social.opinion_dynamics import CulturalInfluenceInteraction
>>> CulturalInfluenceInteraction('influence', target='villages').groups
'neighborhood'
Source code in src/skcomplex/models/social/opinion_dynamics/_cultural_influence.py
64
65
66
67
68
69
70
71
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, model)

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

The model view of the rule.

required
Source code in src/skcomplex/models/social/opinion_dynamics/_cultural_influence.py
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
def act_on(self, members: list[AgentViewProtocol], model: ModelViewProtocol) -> None:
    """Copy one differing trait of a random neighbour with probability equal to the cultural overlap.

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

        model:
            The model view of the rule.
    """
    agent, *neighbors = [member for member in members if isinstance(member, VillageView)]
    if not neighbors:
        return
    neighbor = neighbors[int(model.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 model.rng.random() < 1 - len(different) / features:
        feature = int(different[int(model.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 model, 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.elements import GroupElement
>>> from skcomplex.models.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/models/social/opinion_dynamics/_discussant.py
43
44
45
46
47
48
49
50
51
52
53
54
55
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 model, with its states and parameters as attributes.

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

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

Bases: GroupingInteraction

Discussions of the Galam model, 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.models.social.opinion_dynamics import DiscussionsInteraction
>>> DiscussionsInteraction('talks', target='citizens', size=4).check().write
'opinion'
Source code in src/skcomplex/models/social/opinion_dynamics/_discussions.py
38
39
40
41
42
43
44
45
46
47
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 model, 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.models.social.opinion_dynamics import EncountersInteraction
>>> EncountersInteraction('meetings', target='people').target
'people'
Source code in src/skcomplex/models/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, model)

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

The model view of the rule.

required
Source code in src/skcomplex/models/social/opinion_dynamics/_encounters.py
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
def act_on(self, members: list[AgentViewProtocol], model: ModelViewProtocol) -> 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.

        model:
            The model 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 model, 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.elements import GroupElement
>>> from skcomplex.models.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/models/social/opinion_dynamics/_influencee.py
49
50
51
52
53
54
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': (-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 model, with its states and parameters as attributes.

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

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.models.social.opinion_dynamics import MajorityVoterElement
>>> MajorityVoterElement('cell', opinion=1).opinion
1
Source code in src/skcomplex/models/social/opinion_dynamics/_majority_voter.py
35
36
37
38
39
40
41
42
43
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.elements import GroupElement
>>> import numpy as np
>>> from skcomplex.models.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/models/social/opinion_dynamics/_namer.py
40
41
42
43
44
45
46
47
48
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/engine/solvers/_mesa.py
513
514
515
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.models.social.opinion_dynamics import NamingGamesInteraction
>>> NamingGamesInteraction('games', target='namers').target
'namers'
Source code in src/skcomplex/models/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, model)

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

The model view of the rule.

required
Source code in src/skcomplex/models/social/opinion_dynamics/_naming_games.py
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
def act_on(self, members: list[AgentViewProtocol], model: ModelViewProtocol) -> None:
    """Play one naming game between the first member, the speaker, and the second, the hearer.

    Args:
        members:
            The speaker and the hearer.

        model:
            The model 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, model)
    known = np.flatnonzero(spoken)
    name = known[int(model.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.models.social.opinion_dynamics import OpinionElement
>>> OpinionElement('node', x=0.5).x
0.5
Source code in src/skcomplex/models/social/opinion_dynamics/_opinion.py
31
32
33
34
35
36
37
38
39
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.models.social.opinion_dynamics import OpinionEncountersInteraction
>>> OpinionEncountersInteraction('encounters', candidate='opinions', field='opinions').confidence
2
Source code in src/skcomplex/models/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 model, 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.elements import GroupElement
>>> from skcomplex.models.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/models/social/opinion_dynamics/_persuader.py
39
40
41
42
43
44
45
46
47
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 model, with its states and parameters as attributes.

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

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

Bases: GroupingInteraction

Persuasion of the Sznajd model, 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.models.social.opinion_dynamics import PersuasionInteraction
>>> PersuasionInteraction('persuasion', target='people').groups
'neighborhood'
Source code in src/skcomplex/models/social/opinion_dynamics/_persuasion.py
55
56
57
58
59
60
61
62
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, model)

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

The model view of the rule.

required
Source code in src/skcomplex/models/social/opinion_dynamics/_persuasion.py
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
def act_on(self, members: list[AgentViewProtocol], model: ModelViewProtocol) -> 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.

        model:
            The model view of the rule.
    """
    agent, *neighbors = [member for member in members if isinstance(member, PersuaderView)]
    if not neighbors:
        return
    partner = neighbors[int(model.rng.integers(len(neighbors)))]
    if partner.opinion != agent.opinion:
        return
    for other in [*neighbors, *partner.neighbors()]:
        if isinstance(other, PersuaderView):
            other.opinion = agent.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 models.

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.models.social.opinion_dynamics import RandomPairsInteraction
>>> RandomPairsInteraction('meetings', target='people').groups
'pairs'
Source code in src/skcomplex/models/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.models.social.opinion_dynamics import ScheduleElement
>>> ScheduleElement('triangle', phase=1.0).phase
1.0
Source code in src/skcomplex/models/social/opinion_dynamics/_schedule.py
31
32
33
34
35
36
37
38
39
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.models.social.opinion_dynamics import SeparationInteraction
>>> SeparationInteraction('separation', voters='voters', keep='target', opinion=0).check().action
'rewire'
Source code in src/skcomplex/models/social/opinion_dynamics/_separation.py
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
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 model, 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.models.social.opinion_dynamics import SocialImpactInteraction
>>> SocialImpactInteraction('impact', target='people').check().form
'agent'
Source code in src/skcomplex/models/social/opinion_dynamics/_social_impact.py
37
38
39
40
41
42
43
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.models.social.opinion_dynamics import StandardizationInteraction
>>> StandardizationInteraction('standardization', target='agents', H=100).check().options[-1]
101.0
Source code in src/skcomplex/models/social/opinion_dynamics/_standardization.py
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
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 model, 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.elements import GroupElement
>>> from skcomplex.models.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/models/social/opinion_dynamics/_thinker.py
44
45
46
47
48
49
50
51
52
53
54
55
56
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 model, with its states and parameters as attributes.

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

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.models.social.opinion_dynamics import ThreeBodyConsensusInteraction
>>> ThreeBodyConsensusInteraction('consensus', target='nodes', triangles='triangles').check().size
3
Source code in src/skcomplex/models/social/opinion_dynamics/_three_body_consensus.py
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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

TieElement(label, visual=None)

Bases: Element

Edge of a coevolving network, which holds no state of its own.

Parameters:

Name Type Description Default
label str

The label of the edge group.

required

Examples:

>>> from skcomplex.models.social.opinion_dynamics import TieElement
>>> TieElement('ties').label
'ties'
Source code in src/skcomplex/core/elements/_base.py
70
71
72
def __init__(self, label: str, visual: VisualSpec | None = None) -> None:
    super().__init__(label)
    self.visual = visual

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 model 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 model. 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.elements import GroupElement
>>> from skcomplex.models.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/models/social/opinion_dynamics/_variant_voter.py
50
51
52
53
54
55
56
57
58
59
60
61
62
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 model variants, with its states and parameters as attributes.

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

VariantVotingInteraction(label, *, target)

Bases: MapInteraction

Voting of the voter model 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.models.social.opinion_dynamics import VariantVotingInteraction
>>> VariantVotingInteraction('voting', target='voters').check().schedule
'random-sequential'
Source code in src/skcomplex/models/social/opinion_dynamics/_variant_voting.py
35
36
37
38
39
40
41
42
43
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 model, 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.elements import GroupElement
>>> from skcomplex.models.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/models/social/opinion_dynamics/_village.py
44
45
46
47
48
49
50
51
52
53
54
55
56
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 model, with its states and parameters as attributes.

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

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

Bases: Element

Agent of the voter model, 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.elements import GroupElement
>>> from skcomplex.models.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/models/social/opinion_dynamics/_voter.py
36
37
38
39
40
41
42
43
44
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 model, 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.models.social.opinion_dynamics import VoterImitationInteraction
>>> VoterImitationInteraction('imitation', target='voters').check().schedule
'random-sequential'
Source code in src/skcomplex/models/social/opinion_dynamics/_voter_imitation.py
33
34
35
36
37
38
39
40
41
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 model, with its states and parameters as attributes.

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

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

Return Axelrod's culture dissemination model 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 model, 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.models.social.opinion_dynamics import axelrod
>>> axelrod(5, 4).elements[0].check().prepare().size_
20
Source code in src/skcomplex/models/social/opinion_dynamics/_axelrod.py
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
@register_model(
    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 model 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 model, 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.models.social.opinion_dynamics import axelrod
        >>> axelrod(5, 4).elements[0].check().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 model, 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 model, with the groups voters and ties and the interaction separation.

Examples:

>>> from skcomplex.engine.runtime import Simulation
>>> from skcomplex.models.social.opinion_dynamics import coevolving_voter
>>> trajectory = Simulation(coevolving_voter(30), random_state=0).check().prepare().run(until=5).trajectory_
>>> int(trajectory.network(-1).sources.size) == int(trajectory.network(0).sources.size)
True
Source code in src/skcomplex/models/social/opinion_dynamics/_coevolution.py
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
@register_model(
    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 model, 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 model, with the groups `voters` and `ties` and the interaction `separation`.

    Examples:
        >>> from skcomplex.engine.runtime import Simulation
        >>> from skcomplex.models.social.opinion_dynamics import coevolving_voter
        >>> trajectory = Simulation(coevolving_voter(30), random_state=0).check().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 model, with the group thinkers.

Examples:

>>> import networkx as nx
>>> from skcomplex.models.social.opinion_dynamics import cognitive_consistency
>>> cognitive_consistency(nx.complete_graph(6), random_state=0).elements[0].check().prepare().size_
6
Source code in src/skcomplex/models/social/opinion_dynamics/_cognition.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
@register_model(
    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 model, with the group `thinkers`.

    Examples:
        >>> import networkx as nx
        >>> from skcomplex.models.social.opinion_dynamics import cognitive_consistency
        >>> cognitive_consistency(nx.complete_graph(6), random_state=0).elements[0].check().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.models.social.opinion_dynamics import cultural_traits
>>> cultural_traits(7, 3, 2).tolist()
[1, 1, 1]
Source code in src/skcomplex/models/social/opinion_dynamics/_cultural_influence.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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.models.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.models.social.opinion_dynamics import culture_observables
>>> [item.label for item in culture_observables(5, 4)]
['largest_region', 'cultures']
Source code in src/skcomplex/models/social/opinion_dynamics/_axelrod.py
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
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.models.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 model, 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 model, with the group people.

Examples:

>>> from skcomplex.models.social.opinion_dynamics import deffuant
>>> deffuant(20, random_state=0).elements[0].check().prepare().size_
20
Source code in src/skcomplex/models/social/opinion_dynamics/_bounded_confidence.py
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
@register_model(
    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 model, 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 model, with the group `people`.

    Examples:
        >>> from skcomplex.models.social.opinion_dynamics import deffuant
        >>> deffuant(20, random_state=0).elements[0].check().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')])

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

Return the Galam majority model, 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 model, with the group citizens.

Examples:

>>> from skcomplex.models.social.opinion_dynamics import galam_majority
>>> galam_majority(30, random_state=0).elements[0].check().prepare().size_
30
Source code in src/skcomplex/models/social/opinion_dynamics/_persuasion.py
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
@register_model(
    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 model, 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 model, with the group `citizens`.

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

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

Return the Hegselmann-Krause model, 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 model, with the group people on the nodes of the network.

Examples:

>>> import numpy as np
>>> from skcomplex.engine.runtime import Simulation
>>> from skcomplex.models.social.opinion_dynamics import hegselmann_krause
>>> trajectory = Simulation(
...     hegselmann_krause(50, confidence=0.3), random_state=0,
... ).check().prepare().run(until=20).trajectory_
>>> len(np.unique(np.round(trajectory.values('people.opinion')[-1], 6))) <= 3
True
Source code in src/skcomplex/models/social/opinion_dynamics/_bounded_confidence.py
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
@register_model(
    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 model, 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 model, with the group `people` on the nodes of the network.

    Examples:
        >>> import numpy as np
        >>> from skcomplex.engine.runtime import Simulation
        >>> from skcomplex.models.social.opinion_dynamics import hegselmann_krause
        >>> trajectory = Simulation(
        ...     hegselmann_krause(50, confidence=0.3), random_state=0,
        ... ).check().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)).check().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 model 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 model, with the opinions nodes.x, the mean opinion mean_opinion, and the mean squared opinion spread.

Examples:

>>> from skcomplex.models.social.opinion_dynamics import hypergraph_consensus
>>> system = hypergraph_consensus(8, triangles=0.5)
>>> system.elements[0].check().prepare().size_
8
Source code in src/skcomplex/models/social/opinion_dynamics/_hypergraph_consensus.py
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
@register_model(
    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 model 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 model, with the opinions `nodes.x`, the mean opinion `mean_opinion`, and the mean squared opinion
            `spread`.

    Examples:
        >>> from skcomplex.models.social.opinion_dynamics import hypergraph_consensus
        >>> system = hypergraph_consensus(8, triangles=0.5)
        >>> system.elements[0].check().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 model 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 model, with the group people.

Examples:

>>> from skcomplex.models.social.opinion_dynamics import influentials
>>> influentials(5, 4, n_influentials=2, random_state=0).elements[0].check().prepare().size_
20
Source code in src/skcomplex/models/social/opinion_dynamics/_diffusion.py
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
@register_model(
    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 model 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 model, with the group `people`.

    Examples:
        >>> from skcomplex.models.social.opinion_dynamics import influentials
        >>> influentials(5, 4, n_influentials=2, random_state=0).elements[0].check().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 model, with the groups voters and ties and the interaction separation.

Examples:

>>> from skcomplex.models.social.opinion_dynamics import join_a_club
>>> join_a_club(30).elements[0].element.__name__
'ClubMemberElement'
Source code in src/skcomplex/models/social/opinion_dynamics/_coevolution.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
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
@register_model(
    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 model, with the groups `voters` and `ties` and the interaction `separation`.

    Examples:
        >>> from skcomplex.models.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 model 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 model, with the distribution opinions and the encounters encounters. Its standard observables are the mean opinion and the variance of the opinions.

Examples:

>>> from skcomplex.models.social.opinion_dynamics import kinetic_opinion
>>> kinetic_opinion(levels=5).elements[0].size
5
Source code in src/skcomplex/models/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_model(
    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 model 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 model, with the distribution `opinions` and the encounters `encounters`. Its standard observables are
            the mean opinion and the variance of the opinions.

    Examples:
        >>> from skcomplex.models.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.models.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/models/social/opinion_dynamics/_axelrod.py
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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.models.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.

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 model, with the opinions voters.opinion.

Examples:

>>> from skcomplex.models.social.opinion_dynamics import majority_rule
>>> majority_rule((10, 10)).elements[0].check().prepare().size_
100
Source code in src/skcomplex/models/social/opinion_dynamics/_voters.py
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
@register_model(
    field='social/opinion_dynamics',
    tags=('lattice', 'opinion', 'voting'),
    references=('Charbonneau 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.

    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 model, with the opinions `voters.opinion`.

    Examples:
        >>> from skcomplex.models.social.opinion_dynamics import majority_rule
        >>> majority_rule((10, 10)).elements[0].check().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/models/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 model, with the group namers.

Examples:

>>> from skcomplex.models.social.opinion_dynamics import naming_game
>>> naming_game(10).elements[0].check().prepare().size_
10
Source code in src/skcomplex/models/social/opinion_dynamics/_conventions.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@register_model(
    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 model, with the group `namers`.

    Examples:
        >>> from skcomplex.models.social.opinion_dynamics import naming_game
        >>> naming_game(10).elements[0].check().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.models.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/models/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.models.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.models.social.opinion_dynamics import opinion_observables
>>> [item.label for item in opinion_observables(7)]
['mean_opinion', 'opinion_variance']
Source code in src/skcomplex/models/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.models.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/models/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.models.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/models/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.models.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.models.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/models/social/opinion_dynamics/_conversations.py
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
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.models.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 model 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 model, with the group people.

Examples:

>>> from skcomplex.models.social.opinion_dynamics import social_impact
>>> social_impact(4, 5, random_state=0).elements[0].check().prepare().size_
20
Source code in src/skcomplex/models/social/opinion_dynamics/_persuasion.py
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
@register_model(
    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 model 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 model, with the group `people`.

    Examples:
        >>> from skcomplex.models.social.opinion_dynamics import social_impact
        >>> social_impact(4, 5, random_state=0).elements[0].check().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 model, with the group agents, the share q of agents with the characteristic, and the magnetization m, the mean spin.

Examples:

>>> from skcomplex.models.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/models/social/opinion_dynamics/_standardization.py
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
@register_model(
    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 model, with the group `agents`, the share `q` of agents with the characteristic, and the magnetization
            `m`, the mean spin.

    Examples:
        >>> from skcomplex.models.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 model, 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 model, with the group people and the persuasion persuasion.

Examples:

>>> import networkx as nx
>>> from skcomplex.models.social.opinion_dynamics import sznajd
>>> sznajd(nx.cycle_graph(8), random_state=0).elements[0].check().prepare().size_
8
Source code in src/skcomplex/models/social/opinion_dynamics/_persuasion.py
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
@register_model(
    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 model, 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 model, with the group `people` and the persuasion `persuasion`.

    Examples:
        >>> import networkx as nx
        >>> from skcomplex.models.social.opinion_dynamics import sznajd
        >>> sznajd(nx.cycle_graph(8), random_state=0).elements[0].check().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),
    )

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 model 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 model, with the group people.

Examples:

>>> from skcomplex.models.social.opinion_dynamics import threshold_adoption
>>> threshold_adoption(40, size=60.0, random_state=0).elements[0].check().prepare().size_
40
Source code in src/skcomplex/models/social/opinion_dynamics/_diffusion.py
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
@register_model(
    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 model 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 model, with the group `people`.

    Examples:
        >>> from skcomplex.models.social.opinion_dynamics import threshold_adoption
        >>> threshold_adoption(40, size=60.0, random_state=0).elements[0].check().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, model: _shift(agent, model, size),
                probability=shifting,
            ),
            AdoptionInteraction('adoption', target='people', mode=mode),
        ],
        space=ContinuousSpace(extent=(size, size), boundary='periodic'),
    )

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

Return a voter model 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 model, with the group voters.

Examples:

>>> import networkx as nx
>>> from skcomplex.models.social.opinion_dynamics import voter_model
>>> voter_model(nx.cycle_graph(8), random_state=0).elements[0].check().prepare().size_
8
Source code in src/skcomplex/models/social/opinion_dynamics/_voters.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
@register_model(
    field='social/opinion_dynamics',
    tags=('opinion', 'agents', 'network'),
    references=('Holley and Liggett 1975',),
    defaults={'until': 100},
)
def voter_model(
    graph: object = None,
    *,
    share: float = 0.5,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return a voter model 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 model, with the group `voters`.

    Examples:
        >>> import networkx as nx
        >>> from skcomplex.models.social.opinion_dynamics import voter_model
        >>> voter_model(nx.cycle_graph(8), random_state=0).elements[0].check().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 model 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 model, with the group voters.

Examples:

>>> import networkx as nx
>>> from skcomplex.models.social.opinion_dynamics import voter_variants
>>> voter_variants(nx.cycle_graph(8), random_state=0).elements[0].check().prepare().size_
8
Source code in src/skcomplex/models/social/opinion_dynamics/_voters.py
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
@register_model(
    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 model 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 model, with the group `voters`.

    Examples:
        >>> import networkx as nx
        >>> from skcomplex.models.social.opinion_dynamics import voter_variants
        >>> voter_variants(nx.cycle_graph(8), random_state=0).elements[0].check().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),
    )