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