Skip to content

Crowd dynamics and evacuation

The kinetic model of the evacuation of a building whose rooms form a network.

  • Name: evacuation
  • Field: social/crowds
  • Formalism: ordinary differential equations
  • References: Bellomo et al. 2017; Bellomo and Gibelli 2015; Agnelli, Colasuonno, and Knopoff 2015

Factory

@register_model(
    field='social/crowds',
    tags=('crowds', 'evacuation', 'kinetic', 'network', 'distribution'),
    references=EVACUATION_REFERENCES,
    defaults={'until': 40, 'dt': 0.5},
    observables=lambda system: evacuation_observables(*_sizes_of(system)),
)
def evacuation(
    branching: int = 2,
    depth: int = 2,
    speeds: int = 3,
    *,
    mobility: float = 0.5,
    crowding: float = 2.0,
    recovery: float = 0.5,
) -> System:
    """Return the kinetic model of the evacuation of a building whose rooms form a network.

    The crowd is a distribution over pairs of a room and a speed class, held by a site group on a two-dimensional
    lattice indexed by room and speed, with one last row of sites for the outside. The rooms form a balanced tree with
    the exit in the root room, and the walking encounters read its doors, so pedestrians walk to the next room on the
    way out at a rate proportional to their speed. Encounters within a room slow pedestrians down, and the exit signs
    speed them up again, so the crowd queues in the rooms near the exit and the evacuated fraction grows along an
    S-shaped curve.

    Args:
        branching:
            The number of rooms behind every door of the building.

        depth:
            The number of doors between the exit room and the rooms furthest from it.

        speeds:
            The number of speed classes.

        mobility:
            The rate at which pedestrians at the free speed leave a room.

        crowding:
            The rate of the encounters within a room that slow pedestrians down.

        recovery:
            The rate at which pedestrians speed up again.

    Returns:
        system:
            The model, with the crowd `crowd`, the exit signs `signs`, and the interactions `walking`, `crowding`, and
            `guidance`.
            Its standard observables are the evacuated fraction and the mean walking speed.

    Examples:
        >>> from skcomplex.models.social.crowds import evacuation
        >>> evacuation().elements[0].check().prepare().size_
        24
    """
    rooms = building_of(branching, depth).number_of_nodes()
    initial = np.zeros((rooms + 1, speeds))
    initial[:rooms, speeds // 2] = 1.0 / rooms
    crowd_space = LatticeSpace((rooms + 1, speeds), boundary='fixed')
    return System(
        elements=[
            GroupElement(
                'crowd',
                element=DensityElement,
                on='sites',
                space=crowd_space,
                values={'f': initial.ravel()},
            ),
            GroupElement('signs', element=DensityElement, size=1, values={'f': 1.0}),
        ],
        interactions=[
            WalkingInteraction(
                'walking',
                candidate='crowd',
                field='signs',
                branching=branching,
                depth=depth,
                speeds=speeds,
                mobility=mobility,
            ),
            CrowdingInteraction(
                'crowding',
                candidate='crowd',
                field='crowd',
                rooms=rooms,
                speeds=speeds,
                crowding=crowding,
            ),
            GuidanceInteraction(
                'guidance',
                candidate='crowd',
                field='signs',
                rooms=rooms,
                speeds=speeds,
                recovery=recovery,
            ),
        ],
        space=crowd_space,
        time=ContinuousTime(),
    )

Components

class DensityElement(Element):
    """Value of a density at one class, a member of a distribution over the classes.

    A distribution is a group of densities on the sites of a lattice of classes, one member per class, and encounter,
    proliferation, and destruction laws change it. A density has no laws of its own.

    Args:
        label:
            The label of the density, unique among the parts of its system.

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

        f:
            The initial density, a nonnegative number.

    Attributes:
        default_visual_:
            The visual of the element when it sets none.

    Examples:
        >>> from skcomplex.core.elements import DensityElement, GroupElement
        >>> from skcomplex.core.spaces import LatticeSpace
        >>> classes = LatticeSpace(5, boundary='fixed')
        >>> group = GroupElement('distribution', element=DensityElement, space=classes, on='sites', values={'f': 0.2})
        >>> group.check().prepare().size_
        5
    """

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = None,
        *,
        f: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
    ) -> None:
        super().__init__(label, visual=visual)
        self.f = f

class WalkingInteraction(EncounterInteraction):
    """Walking of pedestrians to the next room on the way out, an encounter with the exit signs.

    Args:
        label:
            The label of the interaction.

        candidate:
            The path of the crowd, a distribution over rooms and speed classes.

        field:
            The path of the group of one member whose fixed density is the guidance of the exit signs.

        branching:
            The number of rooms behind every door of the building, a balanced tree of rooms with the exit in room 0.

        depth:
            The number of doors between the exit room and the rooms furthest from it.

        speeds:
            The number of speed classes.

        mobility:
            The rate at which pedestrians at the free speed leave a room.

    Examples:
        >>> from skcomplex.models.social.crowds import WalkingInteraction
        >>> WalkingInteraction('walking', candidate='crowd', field='signs').mobility
        0.5
    """

    def __init__(
        self,
        label: str,
        *,
        candidate: Annotated[str, RoleSpec()],
        field: Annotated[str, RoleSpec()],
        branching: Annotated[int, ParameterSpec(domain=RealSet.open(0, oo))] = 2,
        depth: Annotated[int, ParameterSpec(domain=RealSet.open(0, oo))] = 2,
        speeds: Annotated[int, ParameterSpec(domain=RealSet.open(0, oo))] = 3,
        mobility: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.5,
    ) -> None:
        super().__init__(label, candidate=candidate, field=field, rate=self._get_rate, kernel=self._get_kernel)
        self.branching = branching
        self.depth = depth
        self.speeds = speeds
        self.mobility = mobility

    def _get_rate(self, s: SimpleNamespace) -> object:
        """Return the rate at which every speed class of every room leaves it."""
        building = building_of(int(self.branching), int(self.depth))
        return walking_table(building, 0, int(self.speeds))[0] * s.mobility

    def _get_kernel(self, s: SimpleNamespace) -> object:
        """Return the table that moves a walker to the next room on the way out."""
        building = building_of(int(self.branching), int(self.depth))
        return walking_table(building, 0, int(self.speeds))[1]

class CrowdingInteraction(EncounterInteraction):
    """Encounters that slow pedestrians down in crowded rooms.

    It is an `EncounterInteraction` of the crowd with itself. Two pedestrians in the same room meet at the rate
    `crowding`, and the candidate slows down one speed class. Pedestrians outside keep their speed.

    Args:
        label:
            The label of the interaction.

        candidate:
            The path of the crowd, a distribution over rooms and speed classes.

        field:
            The path of the crowd that the candidates meet, the crowd again.

        rooms:
            The number of rooms.

        speeds:
            The number of speed classes.

        crowding:
            The rate of the encounters within a room that slow pedestrians down.

    Examples:
        >>> from skcomplex.models.social.crowds import CrowdingInteraction
        >>> CrowdingInteraction('crowding', candidate='crowd', field='crowd').crowding
        2.0
    """

    def __init__(
        self,
        label: str,
        *,
        candidate: Annotated[str, RoleSpec()],
        field: Annotated[str, RoleSpec()],
        rooms: Annotated[int, ParameterSpec(domain=RealSet.open(0, oo))] = 7,
        speeds: Annotated[int, ParameterSpec(domain=RealSet.open(0, oo))] = 3,
        crowding: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 2.0,
    ) -> None:
        super().__init__(label, candidate=candidate, field=field, rate=self._get_rate, kernel=self._get_kernel)
        self.rooms = rooms
        self.speeds = speeds
        self.crowding = crowding

    def _get_rate(self, s: SimpleNamespace) -> object:
        """Return the rate of the slowing encounters of every pair of classes within a room."""
        return crowding_table(int(self.rooms), int(self.speeds), -1)[0] * s.crowding

    def _get_kernel(self, s: SimpleNamespace) -> object:
        """Return the table that moves a pedestrian down one speed class."""
        return crowding_table(int(self.rooms), int(self.speeds), -1)[1]

class GuidanceInteraction(EncounterInteraction):
    """Encounters with the exit signs that speed pedestrians up again.

    It is an `EncounterInteraction` of the crowd with the exit signs. A pedestrian meets the exit signs at the rate
    `recovery` and speeds up one class. Pedestrians outside keep their speed.

    Args:
        label:
            The label of the interaction.

        candidate:
            The path of the crowd, a distribution over rooms and speed classes.

        field:
            The path of the group of one member whose fixed density is the guidance of the exit signs.

        rooms:
            The number of rooms.

        speeds:
            The number of speed classes.

        recovery:
            The rate at which pedestrians speed up again.

    Examples:
        >>> from skcomplex.models.social.crowds import GuidanceInteraction
        >>> GuidanceInteraction('guidance', candidate='crowd', field='signs').recovery
        0.5
    """

    def __init__(
        self,
        label: str,
        *,
        candidate: Annotated[str, RoleSpec()],
        field: Annotated[str, RoleSpec()],
        rooms: Annotated[int, ParameterSpec(domain=RealSet.open(0, oo))] = 7,
        speeds: Annotated[int, ParameterSpec(domain=RealSet.open(0, oo))] = 3,
        recovery: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.5,
    ) -> None:
        super().__init__(label, candidate=candidate, field=field, rate=self._get_rate, kernel=self._get_kernel)
        self.rooms = rooms
        self.speeds = speeds
        self.recovery = recovery

    def _get_rate(self, s: SimpleNamespace) -> object:
        """Return the rate of the encounters of every class with the exit signs."""
        return crowding_table(int(self.rooms), int(self.speeds), 1)[0] * s.recovery

    def _get_kernel(self, s: SimpleNamespace) -> object:
        """Return the table that moves a pedestrian up one speed class."""
        return crowding_table(int(self.rooms), int(self.speeds), 1)[1]

Equations

crowd.f[i]: diff(f_crowd(t), t) == 0
signs.f[i]: diff(f_signs(t), t) == 0