Skip to content

Axelrod culture dissemination

Axelrod's culture dissemination model on a full grid with von Neumann neighbourhoods.

  • Name: axelrod
  • Field: social/opinion_dynamics
  • Formalism: agent-based model
  • References: Axelrod 1997; Castellano, Fortunato, and Loreto 2009

Factory

@register_model(
    field='social/opinion_dynamics',
    tags=('culture', 'agents', 'grid', 'social influence', 'phase transition'),
    references=('Axelrod 1997', 'Castellano, Fortunato, and Loreto 2009'),
    defaults={'until': 200},
    observables=_get_culture_observables,
)
def axelrod(
    width: int = 20,
    height: int = 20,
    *,
    features: int = 5,
    traits: int = 10,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return Axelrod's culture dissemination model on a full grid with von Neumann neighbourhoods.

    Every village starts with random traits. For few traits per feature the villages converge to one culture, and
    for many traits they freeze into many small cultural regions, an order-disorder transition at a critical `q`.

    Args:
        width:
            The number of columns.

        height:
            The number of rows.

        features:
            The number of cultural features `F`.

        traits:
            The number of traits `q` of every feature.

        random_state:
            The seed or generator of the initial cultures.

    Returns:
        system:
            The model, with the group `villages` and the influence `influence`. `culture_observables` gives the
            share of the largest cultural region and the number of cultures.

    Examples:
        >>> from skcomplex.models.social.opinion_dynamics import axelrod
        >>> axelrod(5, 4).elements[0].check().prepare().size_
        20
    """
    rng = np.random.default_rng(random_state)
    size = width * height
    digits = rng.integers(traits, size=(size, features))
    cultures = (digits * traits ** np.arange(features)).sum(axis=1).astype(float)
    locations = [(x, y) for x in range(width) for y in range(height)]
    villages = GroupElement(
        'villages',
        element=VillageElement,
        size=size,
        values={'culture': cultures, 'features': features, 'traits': traits},
        location=locations,
    )
    return System(
        elements=[villages],
        interactions=[CulturalInfluenceInteraction('influence', target='villages')],
        space=LatticeSpace((width, height), boundary='fixed', neighborhood='von_neumann', capacity=1),
    )

Components

class VillageElement(Element):
    """Village of Axelrod's model, with a culture of `F` features of `q` traits each, stored as one integer.

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

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

        culture:
            The culture code, the traits of the features as the digits of a number in base `q`.

        features:
            The number of features `F`.

        traits:
            The number of traits `q` of every feature.

    Examples:
        >>> from skcomplex.core.elements import GroupElement
        >>> from skcomplex.models.social.opinion_dynamics import VillageElement
        >>> GroupElement(
        ...     'villages', element=VillageElement, size=4, values={'culture': 0, 'features': 3, 'traits': 5}
        ... ).size
        4
    """

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'culture'}),
        *,
        culture: Annotated[float, StateSpec(domain=NN)] = 0,
        features: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 5.0,
        traits: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 10.0,
    ) -> None:
        super().__init__(label, visual=visual)
        self.culture = culture
        self.features = features
        self.traits = traits

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

class CulturalInfluenceInteraction(GroupingInteraction):
    """Cultural influence of Axelrod's model, where every village in turn may copy a trait of a random neighbour.

    Every step every village, in random order, picks a random neighbour and, with probability equal to the fraction of
    features they share, copies the trait of one feature on which they differ.

    Args:
        label:
            The label of the interaction.

        target:
            The path of the group of villages.

        phase:
            The phase of the step the influence runs in, or `None` for the default phase.

    Examples:
        >>> from skcomplex.models.social.opinion_dynamics import CulturalInfluenceInteraction
        >>> CulturalInfluenceInteraction('influence', target='villages').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:
        """Copy one differing trait of a random neighbour with probability equal to the cultural overlap.

        Args:
            members:
                The village, followed by its neighbours.

            model:
                The model view of the rule.
        """
        agent, *neighbors = [member for member in members if isinstance(member, VillageView)]
        if not neighbors:
            return
        neighbor = neighbors[int(model.rng.integers(len(neighbors)))]
        features, traits = int(agent.features), int(agent.traits)
        own = cultural_traits(agent.culture, features, traits)
        other = cultural_traits(neighbor.culture, features, traits)
        different = np.flatnonzero(own != other)
        if len(different) and model.rng.random() < 1 - len(different) / features:
            feature = int(different[int(model.rng.integers(len(different)))])
            agent.culture += float((int(other[feature]) - int(own[feature])) * traits**feature)