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