Skip to content

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