Skip to content

Voter

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

  • Class: VoterSystem
  • Import: from skcomplex.systems.social.opinion_dynamics import VoterSystem
  • Field: social/opinion_dynamics
  • Topics: opinion, agents, network
  • References: Holley and Liggett 1975

Use this system

from skcomplex.core.engine.runtime import simulate
from skcomplex.systems.social.opinion_dynamics import VoterSystem

trajectory = simulate(VoterSystem())

Parameters

Parameter Default
graph None
share 0.5
random_state 0

Tags

Tag Values
Time discrete
Space network
Laws update
Randomness stochastic
Domains integer
Structure fixed

Class

class VoterSystem(BaseSystem):
    """A voter system with one voter per node of a network.

    The system has the group `voters`.

    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.

        label:
            The label of the system, which names it when it is nested in another one.

    Examples:
        >>> import networkx as nx
        >>> from skcomplex.systems.social.opinion_dynamics import VoterSystem
        >>> VoterSystem(nx.cycle_graph(8), random_state=0).prepare().system_.elements[0].prepare().size_
        8
    """

    field = 'social/opinion_dynamics'
    topics = ('opinion', 'agents', 'network')
    references = ('Holley and Liggett 1975',)
    until = 100

    def __init__(
        self,
        graph: object = None,
        *,
        share: float = 0.5,
        random_state: int | np.random.Generator | None = 0,
        label: str = 'system',
    ) -> None:
        super().__init__(label)
        self.graph = graph
        self.share = share
        self.random_state = random_state

    def _build_system(self) -> System:
        """Return the parts of the system built from its model parameters."""
        graph = self.graph
        rng = np.random.default_rng(self.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) < self.share).astype(float)
        return System(
            self.label,
            elements=[GroupElement('voters', element=VoterElement, size=size, values={'opinion': opinions})],
            interactions=[VoterImitationInteraction('imitation', target='voters')],
            space=NetworkSpace(graph),
        )

Components

Elements:

  • VoterElement, from Element

Interactions:

  • VoterImitationInteraction, from MapInteraction
class VoterElement(Element):
    """Agent of the voter system on a network.

    The imitation sets its opinion to that of a random neighbour.

    Args:
        label:
            The label of the agent or group.

        visual:
            How the agent is drawn, or `None` for `default_visual_`.

        opinion:
            The initial opinion, 0 or 1.

    Examples:
        >>> from skcomplex.core.components.elements import GroupElement
        >>> from skcomplex.systems.social.opinion_dynamics import VoterElement
        >>> group = GroupElement('voters', element=VoterElement, size=20, values={'opinion': 1.0})
        >>> group.label, group.element.__name__, group.size
        ('voters', 'VoterElement', 20)
    """

    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

    @classmethod
    def _get_view(cls) -> type[VoterView]:
        """Return the class of the views of the agents.

        Their attributes are the states and parameters.
        """
        return VoterView

class VoterImitationInteraction(MapInteraction):
    """Imitation of the voter system.

    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.

    Args:
        label:
            The label of the interaction.

        target:
            The path of the group of voters.

    Examples:
        >>> from skcomplex.systems.social.opinion_dynamics import VoterImitationInteraction
        >>> VoterImitationInteraction('imitation', target='voters').schedule
        'random-sequential'
    """

    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',
        )

    def _get_opinion(self, agent: VoterView, system: SystemViewProtocol) -> float:
        """Return the opinion of a random neighbouring voter.

        Without one, it is the voter's own.
        """
        neighbors = [item for item in agent.neighbors() if isinstance(item, VoterView)]
        if not neighbors:
            return float(agent.opinion)
        return float(neighbors[int(system.rng.integers(len(neighbors)))].opinion)