Social impact theory
The social impact model on a full grid, with random persuasiveness and supportiveness.
- Name:
social_impact - Field:
social/opinion_dynamics - Formalism: agent-based model
- References: Nowak, Szamrej, and Latané 1990; Castellano, Fortunato, and Loreto 2009
Factory
@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),
)
Components
class InfluenceeElement(Element):
"""Agent of the social impact model, with an opinion of -1 or 1, a persuasiveness, and a supportiveness.
Args:
label:
The label of the agent or group.
visual:
How the agent is drawn, or `None` for `default_visual_`.
opinion:
The initial opinion, -1 or 1.
persuasiveness:
The strength with which the agent persuades agents of the other opinion.
supportiveness:
The strength with which the agent supports agents of its opinion.
exponent:
The exponent of the distance by which impact decays.
noise:
The half-width of the uniform random field added to the impact on every agent and step.
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)
"""
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
@classmethod
def _get_view(cls) -> type[InfluenceeView]:
"""Return the class of the views of the agents, whose attributes are the states and parameters."""
return InfluenceeView
class SocialImpactInteraction(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.
Args:
label:
The label of the interaction.
target:
The path of the group of agents.
Examples:
>>> from skcomplex.models.social.opinion_dynamics import SocialImpactInteraction
>>> SocialImpactInteraction('impact', target='people').check().form
'agent'
"""
def __init__(
self,
label: str,
*,
target: Annotated[str, RoleSpec()],
) -> None:
super().__init__(label, target=target, state='opinion', function=self._get_opinion, form='agent')
def _get_impact(self, agent: InfluenceeView, model: ModelViewProtocol) -> float:
"""Return the net pressure to change on an agent from every agent of the group, itself from distance 1."""
agents = [item for item in model.agents('target') if isinstance(item, InfluenceeView)]
locations = np.array([item.location for item in agents], dtype=float)
distances = np.linalg.norm(locations - np.asarray(agent.location, dtype=float), axis=-1)
distances[[item.id == agent.id for item in agents]] = 1.0
weights = distances**-agent.exponent
agreement = agent.opinion * np.array([item.opinion for item in agents], dtype=float)
persuasion = np.array([item.persuasiveness for item in agents], dtype=float)
support = np.array([item.supportiveness for item in agents], dtype=float)
return float((weights * persuasion * (1 - agreement)).sum() - (weights * support * (1 + agreement)).sum())
def _get_opinion(self, agent: InfluenceeView, model: ModelViewProtocol) -> float:
"""Return the opposite opinion of an agent under a positive net pressure to change, and its own otherwise."""
field = model.rng.uniform(-agent.noise, agent.noise)
return -agent.opinion if self._get_impact(agent, model) + field > 0 else float(agent.opinion)