Skip to content

Cognitive consistency agents

Agents whose attitudes spread by conversation and settle in a belief network that seeks consistency.

  • Name: cognitive_consistency
  • Field: social/opinion_dynamics
  • Formalism: agent-based model
  • References: Lakkaraju and Speed 2019; Carmichael et al. 2019

Factory

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

Components

class ThinkerElement(Element):
    """Agent of the cognitive consistency model, with the valences of the concepts of its belief network.

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

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

        valences:
            The initial valences of the concepts F1, X, and F2, between -1 and 1.

        attitude:
            The attitude of the agent, the valence of the concept X once the belief network settles.

        decay:
            The fraction of every valence lost per update of the belief network.

    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)
    """

    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

    @classmethod
    def _get_view(cls) -> type[ThinkerView]:
        """Return the class of the views of the agents, whose attributes are the states and parameters."""
        return ThinkerView

class ConversationsInteraction(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.

    Args:
        label:
            The label of the interaction.

        target:
            The path of the group of thinkers.

        phase:
            The phase of the step the conversations run in, or `None` for the default phase.

    Examples:
        >>> from skcomplex.models.social.opinion_dynamics import ConversationsInteraction
        >>> ConversationsInteraction('talks', target='thinkers').groups
        'neighborhood'
    """

    def __init__(
        self,
        label: str,
        *,
        target: Annotated[str, RoleSpec()],
        phase: str | None = None,
    ) -> None:
        super().__init__(label, target=target, groups='neighborhood', phase=phase)

    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])