Kinetic opinion formation
The kinetic model of opinion formation over a discrete grid of opinions.
- Name:
kinetic_opinion - Field:
social/opinion_dynamics - Formalism: ordinary differential equations
- References: Bellomo et al. 2017; Toscani 2006; Deffuant et al. 2000
Factory
@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(),
)
Components
class DensityElement(Element):
"""Value of a density at one class, a member of a distribution over the classes.
A distribution is a group of densities on the sites of a lattice of classes, one member per class, and encounter,
proliferation, and destruction laws change it. A density has no laws of its own.
Args:
label:
The label of the density, unique among the parts of its system.
visual:
How the element is drawn, or `None` for `default_visual_`.
f:
The initial density, a nonnegative number.
Attributes:
default_visual_:
The visual of the element when it sets none.
Examples:
>>> from skcomplex.core.elements import DensityElement, GroupElement
>>> from skcomplex.core.spaces import LatticeSpace
>>> classes = LatticeSpace(5, boundary='fixed')
>>> group = GroupElement('distribution', element=DensityElement, space=classes, on='sites', values={'f': 0.2})
>>> group.check().prepare().size_
5
"""
def __init__(
self,
label: str,
visual: VisualSpec | None = None,
*,
f: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
) -> None:
super().__init__(label, visual=visual)
self.f = f
class OpinionEncountersInteraction(EncounterInteraction):
"""Conservative encounters between opinion levels, which persuade close opinions and repel distant ones.
Args:
label:
The label of the interaction.
candidate:
The path of the distribution over opinion levels whose members move.
field:
The path of the distribution over opinion levels they meet, usually the same distribution.
levels:
The number of opinion levels.
confidence:
The largest distance in levels at which encounters persuade.
eta:
The encounter rate.
persuasion:
The probability of moving toward a close opinion.
repulsion:
The probability of moving away from a distant opinion.
Examples:
>>> from skcomplex.models.social.opinion_dynamics import OpinionEncountersInteraction
>>> OpinionEncountersInteraction('encounters', candidate='opinions', field='opinions').confidence
2
"""
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
def _get_rate(self, s: SimpleNamespace) -> object:
"""Return the encounter rate."""
return s.eta
def _get_kernel(self, s: SimpleNamespace) -> object:
"""Return the table of the persuasion and repulsion of the opinion levels."""
return persuasion_table(int(self.levels), int(self.confidence), s.persuasion, s.repulsion)
Equations
opinions.f[i]: diff(f_opinions(t), t) == 0