Skip to content

Bonabeau hierarchies

The Bonabeau model, where a hierarchy of strengths emerges above a critical density of agents.

  • Name: bonabeau_hierarchies
  • Field: social/organizations
  • Formalism: agent-based model
  • References: Bonabeau, Theraulaz, and Deneubourg 1995; Castellano, Fortunato, and Loreto 2009

Factory

@register_model(
    field='social/organizations',
    tags=('hierarchy', 'agents', 'grid', 'phase transition'),
    references=('Bonabeau, Theraulaz, and Deneubourg 1995', 'Castellano, Fortunato, and Loreto 2009'),
    defaults={'until': 50},
)
def bonabeau_hierarchies(
    width: int = 20,
    height: int = 20,
    *,
    density: float = 0.5,
    eta: float = 1.0,
    relaxation: float = 0.1,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the Bonabeau model, where a hierarchy of strengths emerges above a critical density of agents.

    Args:
        width:
            The number of columns of the torus.

        height:
            The number of rows of the torus.

        density:
            The share of cells with an agent.

        eta:
            The sensitivity of the outcome of a fight to the strength difference.

        relaxation:
            The fraction of its strength an agent loses every step.

        random_state:
            The seed or generator of the initial cells.

    Returns:
        system:
            The model, with the group `contenders`.

    Examples:
        >>> from skcomplex.models.social.organizations import bonabeau_hierarchies
        >>> bonabeau_hierarchies(10, 10, density=0.2).elements[0].check().prepare().size_
        20
    """
    size = int(width * height * density)
    cells = np.random.default_rng(random_state).permutation(width * height)[:size]
    contenders = GroupElement(
        'contenders',
        element=ContenderElement,
        size=size,
        values={'eta': eta, 'relaxation': relaxation},
        location=[(int(cell % width), int(cell // width)) for cell in cells],
    )
    return System(elements=[contenders], space=LatticeSpace((width, height), boundary='periodic', capacity=1))

Components

class ContenderElement(Element):
    """Agent of the Bonabeau model, who wanders on a grid and fights the agents it runs into.

    Every step an agent's strength relaxes by the factor `1 - relaxation`, and the agent tries to step into a random
    neighbouring cell. An attacker beats the occupant with probability `1 / (1 + exp(eta * (h_occupant - h_attacker)))`,
    the winner gains 1 and the loser loses 1, and a winning attacker swaps places with the occupant. Agents differ only
    by their strength, so the swap exchanges their strengths.

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

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

        strength:
            The initial strength `h`.

        eta:
            The sensitivity of the outcome of a fight to the strength difference.

        relaxation:
            The fraction of its strength an agent loses every step.

    Examples:
        >>> from skcomplex.models.social.organizations import ContenderElement
        >>> group = GroupElement('contenders', element=ContenderElement, size=10)
        >>> group.label, group.element.__name__, group.size
        ('contenders', 'ContenderElement', 10)
    """

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'strength'}),
        *,
        strength: Annotated[float, StateSpec()] = 0.0,
        eta: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
        relaxation: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.1,
    ) -> None:
        super().__init__(label, visual=visual)
        self.strength = strength
        self.eta = eta
        self.relaxation = relaxation

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

    def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
        """Return the wander and fight rule.

        Args:
            s:
                The namespace of the symbols the laws can use.

        Returns:
            laws:
                The laws of the component.
        """
        return [RuleLaw(self.wander)]

    def _fight(self, agent: ContenderView, occupant: ContenderView, model: ModelView) -> None:
        """Fight the occupant of a cell and take its place on a win."""
        wins = model.rng.random() < 1 / (1 + np.exp(np.clip(agent.eta * (occupant.strength - agent.strength), -50, 50)))
        if wins:
            agent.strength, occupant.strength = occupant.strength - 1, agent.strength + 1
        else:
            agent.strength, occupant.strength = agent.strength - 1, occupant.strength + 1

    def wander(self, agent: ContenderView, model: ModelView) -> None:
        """Relax the strength, then step into a random neighbouring cell or fight its occupant.

        Args:
            agent:
                The contender.

            model:
                The model.
        """
        agent.strength *= 1 - agent.relaxation
        cells = agent.neighbor_cells()
        cell = cells[int(model.rng.integers(len(cells)))]
        occupants = [
            other
            for other in agent.neighbors()
            if isinstance(other, ContenderView)
            and tuple(np.ravel(np.asarray(other.location))) == tuple(np.ravel(np.asarray(cell)))
        ]
        if occupants:
            self._fight(agent, occupants[0], model)
            return
        agent.move_to(cell)