Skip to content

Crowds

Crowds: pedestrians driven by social forces, evacuations, and criminality against security forces.

CriminalContagionInteraction(label, *, candidate, field, classes=5, eta=1.0, attraction=0.3, redemption=0.1)

Bases: EncounterInteraction

Conservative encounters between citizens, in which crime attracts and honesty redeems.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
candidate Annotated[str, RoleSpec()]

The path of the distribution of citizens whose members move.

required
field Annotated[str, RoleSpec()]

The path of the distribution of citizens they meet, usually the same distribution.

required
classes Annotated[int, ParameterSpec(domain=open(0, oo))]

The number of criminality classes.

5
eta Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The encounter rate.

1.0
attraction Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The probability that a citizen who meets a more criminal one moves one class up.

0.3
redemption Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The probability that a citizen who meets a more honest one moves one class down.

0.1

Examples:

>>> from skcomplex.models.social.crowds import CriminalContagionInteraction
>>> CriminalContagionInteraction('contagion', candidate='citizens', field='citizens').attraction
0.3
Source code in src/skcomplex/models/social/crowds/_criminal_contagion.py
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
def __init__(
    self,
    label: str,
    *,
    candidate: Annotated[str, RoleSpec()],
    field: Annotated[str, RoleSpec()],
    classes: Annotated[int, ParameterSpec(domain=RealSet.open(0, oo))] = 5,
    eta: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
    attraction: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.3,
    redemption: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.1,
) -> None:
    super().__init__(label, candidate=candidate, field=field, rate=self._get_rate, kernel=self._get_kernel)
    self.classes = classes
    self.eta = eta
    self.attraction = attraction
    self.redemption = redemption

CrowdingInteraction(label, *, candidate, field, rooms=7, speeds=3, crowding=2.0)

Bases: 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.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
candidate Annotated[str, RoleSpec()]

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

required
field Annotated[str, RoleSpec()]

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

required
rooms Annotated[int, ParameterSpec(domain=open(0, oo))]

The number of rooms.

7
speeds Annotated[int, ParameterSpec(domain=open(0, oo))]

The number of speed classes.

3
crowding Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

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

2.0

Examples:

>>> from skcomplex.models.social.crowds import CrowdingInteraction
>>> CrowdingInteraction('crowding', candidate='crowd', field='crowd').crowding
2.0
Source code in src/skcomplex/models/social/crowds/_crowding.py
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
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

DeterrenceInteraction(label, *, candidate, field, classes=5, forces=3, deterrence=2.0)

Bases: EncounterInteraction

Encounters of citizens with security forces, which move criminal citizens toward honesty.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
candidate Annotated[str, RoleSpec()]

The path of the distribution of citizens.

required
field Annotated[str, RoleSpec()]

The path of the distribution of the security forces over efficacy.

required
classes Annotated[int, ParameterSpec(domain=open(0, oo))]

The number of criminality classes.

5
forces Annotated[int, ParameterSpec(domain=open(0, oo))]

The number of efficacy classes of the security forces.

3
deterrence Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The rate at which the most effective agents deter a criminal citizen.

2.0

Examples:

>>> from skcomplex.models.social.crowds import DeterrenceInteraction
>>> DeterrenceInteraction('deterrence', candidate='citizens', field='security').deterrence
2.0
Source code in src/skcomplex/models/social/crowds/_deterrence.py
80
81
82
83
84
85
86
87
88
89
90
91
92
93
def __init__(
    self,
    label: str,
    *,
    candidate: Annotated[str, RoleSpec()],
    field: Annotated[str, RoleSpec()],
    classes: Annotated[int, ParameterSpec(domain=RealSet.open(0, oo))] = 5,
    forces: Annotated[int, ParameterSpec(domain=RealSet.open(0, oo))] = 3,
    deterrence: 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.classes = classes
    self.forces = forces
    self.deterrence = deterrence

GuidanceInteraction(label, *, candidate, field, rooms=7, speeds=3, recovery=0.5)

Bases: 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.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
candidate Annotated[str, RoleSpec()]

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

required
field Annotated[str, RoleSpec()]

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

required
rooms Annotated[int, ParameterSpec(domain=open(0, oo))]

The number of rooms.

7
speeds Annotated[int, ParameterSpec(domain=open(0, oo))]

The number of speed classes.

3
recovery Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The rate at which pedestrians speed up again.

0.5

Examples:

>>> from skcomplex.models.social.crowds import GuidanceInteraction
>>> GuidanceInteraction('guidance', candidate='crowd', field='signs').recovery
0.5
Source code in src/skcomplex/models/social/crowds/_guidance.py
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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

MotivationInteraction(label, *, target, speed=1.34, direction=0.0, tau=0.5)

Bases: RelaxationInteraction

Driving force of the social force model, which relaxes a walker's velocity to its desired velocity.

The force per unit mass is (speed * direction - x_d1) / tau, where the velocity x_d1 is the first derivative of the position x.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
target Annotated[str, RoleSpec()]

The path of the walker or group of walkers.

required
speed Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The desired speed.

1.34
direction Annotated[ArrayLike, ParameterSpec(vector=True)]

The unit vector of the desired direction.

0.0
tau Annotated[float, ParameterSpec(domain=open(0, oo))]

The relaxation time.

0.5

Examples:

>>> from skcomplex.models.social.crowds import MotivationInteraction
>>> MotivationInteraction('drive', target='walkers', direction=[1.0, 0.0]).speed
1.34
Source code in src/skcomplex/models/social/crowds/_motivation.py
47
48
49
50
51
52
53
54
55
56
57
58
59
def __init__(
    self,
    label: str,
    *,
    target: Annotated[str, RoleSpec()],
    speed: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.34,
    direction: Annotated[npt.ArrayLike, ParameterSpec(vector=True)] = 0.0,
    tau: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 0.5,
) -> None:
    super().__init__(label, target=target, state='x_d1')
    self.speed = speed
    self.direction = direction
    self.tau = tau

RecruitmentInteraction(label, *, target, citizens, classes=5, recruitment=0.5, retirement=0.1)

Bases: RateInteraction

Recruitment of security agents in proportion to the mean criminality, a macro-scale external action.

It is a RateInteraction of the density f of the security forces that reads the citizens. The security forces grow at the rate recruitment m share_k in efficacy class k, where m is the mean criminality of the citizens, a moment of their distribution, and retire at the rate retirement.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
target Annotated[str, RoleSpec()]

The path of the distribution of the security forces over efficacy.

required
citizens Annotated[str, RoleSpec()]

The path of the distribution of citizens.

required
classes Annotated[int, ParameterSpec(domain=open(0, oo))]

The number of criminality classes.

5
recruitment Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The recruitment rate per unit of mean criminality.

0.5
retirement Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The retirement rate.

0.1

Examples:

>>> from skcomplex.models.social.crowds import RecruitmentInteraction
>>> RecruitmentInteraction('recruitment', target='security', citizens='citizens').retirement
0.1
Source code in src/skcomplex/models/social/crowds/_recruitment.py
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
def __init__(
    self,
    label: str,
    *,
    target: Annotated[str, RoleSpec()],
    citizens: Annotated[str, RoleSpec()],
    classes: Annotated[int, ParameterSpec(domain=RealSet.open(0, oo))] = 5,
    recruitment: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.5,
    retirement: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.1,
) -> None:
    super().__init__(label, target=target, state='f', rate=self._get_rate)
    self.citizens = citizens
    self.classes = classes
    self.recruitment = recruitment
    self.retirement = retirement

SecurityAgentsElement(label, visual=None, *, f=0.0, share=1.0)

Bases: Element

Density of security agents at one efficacy class, with the share of new recruits that join the class.

Parameters:

Name Type Description Default
label str

The label of the distribution.

required
visual VisualSpec | None

How the agent is drawn, or None for default_visual_.

None
f Annotated[float, StateSpec(domain=closed_open(0, oo))]

The initial density.

0.0
share Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The share of new recruits that join the efficacy class.

1.0

Examples:

>>> from skcomplex.core.elements import GroupElement
>>> from skcomplex.models.social.crowds import SecurityAgentsElement
>>> GroupElement('security', element=SecurityAgentsElement, size=3, values={'share': [0.5, 0.3, 0.2]}).size
3
Source code in src/skcomplex/models/social/crowds/_security_agents.py
38
39
40
41
42
43
44
45
46
47
48
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    f: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
    share: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
) -> None:
    super().__init__(label, visual=visual)
    self.f = f
    self.share = share

SocialRepulsionInteraction(label, first, second, cutoff=1.0, position='x', *, period=None, strength=2.0, falloff=0.3, radius=0.3)

Bases: PairCouplingInteraction

Repulsion between two walkers, the force strength * exp((2 * radius - r) / falloff) of the social force model.

It derives from the pair energy strength * falloff * exp((2 * radius - r) / falloff).

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
first str

The path of the group or walker of the first members.

required
second str

The path of the group or walker of the second members.

required
cutoff float

The distance beyond which walkers do not interact.

1.0
position str

The name of the vector state that holds the positions of the walkers.

'x'
period float | None

The side of a periodic box, for minimum image distances, or None for an open space.

None
strength Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The strength of the repulsion.

2.0
falloff Annotated[float, ParameterSpec(domain=open(0, oo))]

The distance over which the repulsion falls off.

0.3
radius Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The radius of a walker.

0.3

Examples:

>>> from skcomplex.models.social.crowds import SocialRepulsionInteraction
>>> SocialRepulsionInteraction('repulsion', first='walkers', second='walkers').strength
2.0
Source code in src/skcomplex/models/social/crowds/_social_repulsion.py
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
def __init__(
    self,
    label: str,
    first: str,
    second: str,
    cutoff: float = 1.0,
    position: str = 'x',
    *,
    period: float | None = None,
    strength: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 2.0,
    falloff: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 0.3,
    radius: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.3,
) -> None:
    super().__init__(
        label,
        first,
        second,
        cutoff=cutoff,
        position=position,
        potential=self._get_pair_energy,
        period=period,
    )
    self.strength = strength
    self.falloff = falloff
    self.radius = radius

WalkingInteraction(label, *, candidate, field, branching=2, depth=2, speeds=3, mobility=0.5)

Bases: EncounterInteraction

Walking of pedestrians to the next room on the way out, an encounter with the exit signs.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
candidate Annotated[str, RoleSpec()]

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

required
field Annotated[str, RoleSpec()]

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

required
branching Annotated[int, ParameterSpec(domain=open(0, oo))]

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

2
depth Annotated[int, ParameterSpec(domain=open(0, oo))]

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

2
speeds Annotated[int, ParameterSpec(domain=open(0, oo))]

The number of speed classes.

3
mobility Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

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

0.5

Examples:

>>> from skcomplex.models.social.crowds import WalkingInteraction
>>> WalkingInteraction('walking', candidate='crowd', field='signs').mobility
0.5
Source code in src/skcomplex/models/social/crowds/_walking.py
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
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

WallsInteraction(label, *, target, width=10.0, strength=10.0, falloff=0.2, radius=0.3)

Bases: GradientInteraction

Repulsion of walkers by two walls of a corridor at y = 0 and y = width.

It is a GradientInteraction of the position x down the potential strength falloff (exp((radius - y) / falloff) + exp((radius - width + y) / falloff)), so the force per unit mass from a wall at distance d is strength * exp((radius - d) / falloff), pointing away from the wall.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
target Annotated[str, RoleSpec()]

The path of the walker or group of walkers.

required
width Annotated[float, ParameterSpec(domain=open(0, oo))]

The distance between the walls.

10.0
strength Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The strength of the repulsion.

10.0
falloff Annotated[float, ParameterSpec(domain=open(0, oo))]

The distance over which the repulsion falls off.

0.2
radius Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The radius of a walker.

0.3

Examples:

>>> from skcomplex.models.social.crowds import WallsInteraction
>>> WallsInteraction('walls', target='walkers', width=5.0).width
5.0
Source code in src/skcomplex/models/social/crowds/_walls.py
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
def __init__(
    self,
    label: str,
    *,
    target: Annotated[str, RoleSpec()],
    width: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 10.0,
    strength: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 10.0,
    falloff: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 0.2,
    radius: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.3,
) -> None:
    super().__init__(label, target=target, potential=self._get_potential)
    self.width = width
    self.strength = strength
    self.falloff = falloff
    self.radius = radius

building_of(branching, depth)

Return a building whose rooms form a balanced tree, with the exit door in room 0, the root.

Parameters:

Name Type Description Default
branching int

The number of rooms behind every door of a room further from the exit.

required
depth int

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

required

Returns:

Name Type Description
building Graph

The network of rooms, labelled 0 to rooms - 1, whose edges are the doors.

Examples:

>>> from skcomplex.models.social.crowds import building_of
>>> building_of(2, 2).number_of_nodes()
7
Source code in src/skcomplex/models/social/crowds/_walking.py
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
def building_of(branching: int, depth: int) -> nx.Graph:
    """Return a building whose rooms form a balanced tree, with the exit door in room 0, the root.

    Args:
        branching:
            The number of rooms behind every door of a room further from the exit.

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

    Returns:
        building:
            The network of rooms, labelled `0` to `rooms - 1`, whose edges are the doors.

    Examples:
        >>> from skcomplex.models.social.crowds import building_of
        >>> building_of(2, 2).number_of_nodes()
        7
    """
    return nx.balanced_tree(branching, depth)

contagion_table(classes, attraction, redemption)

Return the transition table of encounters between citizens, A[h, k, i].

A citizen of class h who meets a more criminal citizen moves one class up with probability attraction, and one who meets a more honest citizen moves one class down with probability redemption.

Parameters:

Name Type Description Default
classes int

The number of criminality classes.

required
attraction object

The probability that crime attracts, a number or a parameter.

required
redemption object

The probability that honesty redeems, a number or a parameter.

required

Returns:

Name Type Description
table NDArray[object_]

The transition probabilities, which sum to one over the outcomes.

Examples:

>>> from skcomplex.models.social.crowds import contagion_table
>>> contagion_table(3, 0.3, 0.1)[0, 2].astype(float).tolist()
[0.7, 0.3, 0.0]
Source code in src/skcomplex/models/social/crowds/_criminal_contagion.py
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
def contagion_table(classes: int, attraction: object, redemption: object) -> npt.NDArray[np.object_]:
    """Return the transition table of encounters between citizens, `A[h, k, i]`.

    A citizen of class `h` who meets a more criminal citizen moves one class up with probability `attraction`, and one
    who meets a more honest citizen moves one class down with probability `redemption`.

    Args:
        classes:
            The number of criminality classes.

        attraction:
            The probability that crime attracts, a number or a parameter.

        redemption:
            The probability that honesty redeems, a number or a parameter.

    Returns:
        table:
            The transition probabilities, which sum to one over the outcomes.

    Examples:
        >>> from skcomplex.models.social.crowds import contagion_table
        >>> contagion_table(3, 0.3, 0.1)[0, 2].astype(float).tolist()
        [0.7, 0.3, 0.0]
    """
    table = np.zeros((classes, classes, classes), dtype=object)
    for candidate in range(classes):
        for field in range(classes):
            if field > candidate:
                table[candidate, field, candidate + 1] += attraction
            elif field < candidate:
                table[candidate, field, candidate - 1] += redemption
            table[candidate, field, candidate] += 1 - table[candidate, field].sum()
    return table

criminal_fraction(s, criminality)

Return the fraction of citizens whose criminality is at least one half.

Parameters:

Name Type Description Default
s SimpleNamespace

The namespace of one member of the distribution.

required
criminality NDArray[float64]

The criminality of every class.

required

Returns:

Name Type Description
fraction Expression

The moment of the indicator of the criminal classes, divided by the density.

Source code in src/skcomplex/models/social/crowds/_criminality.py
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
def criminal_fraction(s: SimpleNamespace, criminality: npt.NDArray[np.float64]) -> Expression:
    """Return the fraction of citizens whose criminality is at least one half.

    Args:
        s:
            The namespace of one member of the distribution.

        criminality:
            The criminality of every class.

    Returns:
        fraction:
            The moment of the indicator of the criminal classes, divided by the density.
    """
    return moment(s.f, 1, (criminality >= CRIMINAL_THRESHOLD).astype(float), normalized=True)

criminality(classes=5, forces=3, *, criminals=0.2, security=0.05, attraction=0.3, redemption=0.1, deterrence=2.0, recruitment=0.5, retirement=0.1)

Return the kinetic model of criminality in a society and the security forces that act against it.

Citizens are a distribution over criminality classes and security agents a distribution over efficacy classes, held by site groups on lattices of the classes. Encounters between citizens spread crime when it attracts more than honesty redeems, encounters with security agents move criminal citizens toward honesty, a micro-scale action, and the security forces grow in proportion to the mean criminality, a macro-scale action that reads a moment of the citizens. The security forces grow while crime is high, crime recedes, and the forces retire, so crime and security oscillate toward a balance where some crime persists.

Parameters:

Name Type Description Default
classes int

The number of criminality classes.

5
forces int

The number of efficacy classes of the security forces.

3
criminals float

The initial fraction of citizens in the most criminal class, the others being honest.

0.2
security float

The initial density of the security forces, spread evenly over their efficacy classes.

0.05
attraction float

The probability that a citizen who meets a more criminal one moves one class up.

0.3
redemption float

The probability that a citizen who meets a more honest one moves one class down.

0.1
deterrence float

The rate at which the most effective agents deter a criminal citizen.

2.0
recruitment float

The recruitment rate of security agents per unit of mean criminality.

0.5
retirement float

The retirement rate of security agents.

0.1

Returns:

Name Type Description
system System

The model, with the distributions citizens and security and the interactions contagion, deterrence, and recruitment. Its standard observables are the mean criminality, the criminal fraction, and the density of the security forces.

Examples:

>>> from skcomplex.models.social.crowds import criminality
>>> [item.label for item in criminality().interactions]
['contagion', 'deterrence', 'recruitment']
Source code in src/skcomplex/models/social/crowds/_criminality.py
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
@register_model(
    field='social/crowds',
    tags=('criminality', 'security', 'kinetic', 'distribution'),
    references=CRIMINALITY_REFERENCES,
    defaults={'until': 60, 'dt': 0.5},
    observables=lambda system: criminality_observables(*_sizes_of(system)),
)
def criminality(
    classes: int = 5,
    forces: int = 3,
    *,
    criminals: float = 0.2,
    security: float = 0.05,
    attraction: float = 0.3,
    redemption: float = 0.1,
    deterrence: float = 2.0,
    recruitment: float = 0.5,
    retirement: float = 0.1,
) -> System:
    """Return the kinetic model of criminality in a society and the security forces that act against it.

    Citizens are a distribution over criminality classes and security agents a distribution over efficacy classes,
    held by site groups on lattices of the classes. Encounters between citizens spread crime when it attracts more than
    honesty redeems, encounters with security agents move criminal citizens toward honesty, a micro-scale action, and
    the security forces grow in proportion to the mean criminality, a macro-scale action that reads a moment of the
    citizens. The security forces grow while crime is high, crime recedes, and the forces retire, so crime and
    security oscillate toward a balance where some crime persists.

    Args:
        classes:
            The number of criminality classes.

        forces:
            The number of efficacy classes of the security forces.

        criminals:
            The initial fraction of citizens in the most criminal class, the others being honest.

        security:
            The initial density of the security forces, spread evenly over their efficacy classes.

        attraction:
            The probability that a citizen who meets a more criminal one moves one class up.

        redemption:
            The probability that a citizen who meets a more honest one moves one class down.

        deterrence:
            The rate at which the most effective agents deter a criminal citizen.

        recruitment:
            The recruitment rate of security agents per unit of mean criminality.

        retirement:
            The retirement rate of security agents.

    Returns:
        system:
            The model, with the distributions `citizens` and `security` and the interactions `contagion`,
            `deterrence`, and `recruitment`. Its standard observables are the mean criminality, the criminal fraction,
            and the density of the security forces.

    Examples:
        >>> from skcomplex.models.social.crowds import criminality
        >>> [item.label for item in criminality().interactions]
        ['contagion', 'deterrence', 'recruitment']
    """
    citizens = np.zeros(classes)
    citizens[0], citizens[-1] = 1.0 - criminals, criminals
    return System(
        elements=[
            GroupElement('citizens', element=DensityElement, size=classes, on='sites', values={'f': citizens}),
            GroupElement(
                'security',
                element=SecurityAgentsElement,
                size=forces,
                on='sites',
                space=LatticeSpace(forces, boundary='fixed'),
                values={'f': np.full(forces, security / forces), 'share': np.full(forces, 1.0 / forces)},
            ),
        ],
        interactions=[
            CriminalContagionInteraction(
                'contagion',
                candidate='citizens',
                field='citizens',
                classes=classes,
                attraction=attraction,
                redemption=redemption,
            ),
            DeterrenceInteraction(
                'deterrence',
                candidate='citizens',
                field='security',
                classes=classes,
                forces=forces,
                deterrence=deterrence,
            ),
            RecruitmentInteraction(
                'recruitment',
                target='security',
                citizens='citizens',
                classes=classes,
                recruitment=recruitment,
                retirement=retirement,
            ),
        ],
        space=LatticeSpace(classes, boundary='fixed'),
        time=ContinuousTime(),
    )

criminality_grid(classes)

Return the criminality of every class of citizens, from 0 for honest to 1 for criminal.

Parameters:

Name Type Description Default
classes int

The number of criminality classes.

required

Returns:

Name Type Description
criminality NDArray[float64]

The criminality of every class.

Examples:

>>> from skcomplex.models.social.crowds import criminality_grid
>>> criminality_grid(3).tolist()
[0.0, 0.5, 1.0]
Source code in src/skcomplex/models/social/crowds/_recruitment.py
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def criminality_grid(classes: int) -> npt.NDArray[np.float64]:
    """Return the criminality of every class of citizens, from 0 for honest to 1 for criminal.

    Args:
        classes:
            The number of criminality classes.

    Returns:
        criminality:
            The criminality of every class.

    Examples:
        >>> from skcomplex.models.social.crowds import criminality_grid
        >>> criminality_grid(3).tolist()
        [0.0, 0.5, 1.0]
    """
    return np.linspace(0.0, 1.0, classes)

criminality_observables(classes, forces)

Return the mean criminality, the criminal fraction, and the density of the security forces of the model.

Parameters:

Name Type Description Default
classes int

The number of criminality classes.

required
forces int

The number of efficacy classes of the security forces.

required

Returns:

Name Type Description
observables list[ObservableSpec]

The observables, each reading the moments of a distribution.

Examples:

>>> from skcomplex.models.social.crowds import criminality_observables
>>> [item.label for item in criminality_observables(5, 3)]
['mean_criminality', 'criminal_fraction', 'security_forces']
Source code in src/skcomplex/models/social/crowds/_criminality.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
def criminality_observables(classes: int, forces: int) -> list[ObservableSpec]:
    """Return the mean criminality, the criminal fraction, and the density of the security forces of the model.

    Args:
        classes:
            The number of criminality classes.

        forces:
            The number of efficacy classes of the security forces.

    Returns:
        observables:
            The observables, each reading the moments of a distribution.

    Examples:
        >>> from skcomplex.models.social.crowds import criminality_observables
        >>> [item.label for item in criminality_observables(5, 3)]
        ['mean_criminality', 'criminal_fraction', 'security_forces']
    """
    criminality = criminality_grid(classes)
    return [
        ObservableSpec('mean_criminality', partial(mean_criminality, criminality=criminality), group='citizens'),
        ObservableSpec('criminal_fraction', partial(criminal_fraction, criminality=criminality), group='citizens'),
        ObservableSpec('security_forces', partial(security_density, forces=forces), group='security'),
    ]

crowding_table(rooms, speeds, step)

Return the rate pattern and the transition table of encounters that change the speed class by one step.

With step = -1, two pedestrians in the same room meet and the candidate slows down one class, so crowded rooms walk slower. With step = 1, a pedestrian meets the exit signs and speeds up one class.

Parameters:

Name Type Description Default
rooms int

The number of rooms.

required
speeds int

The number of speed classes.

required
step int

-1 to slow down in encounters within a room, 1 to speed up in encounters with the exit signs.

required

Returns:

Name Type Description
pattern NDArray[float64]

The rate pattern, 1 for the encounters that act.

table ...

The transition table.

Examples:

>>> from skcomplex.models.social.crowds import crowding_table
>>> crowding_table(1, 2, 1)[0].ravel().tolist()
[1.0, 0.0, 0.0, 0.0]
Source code in src/skcomplex/models/social/crowds/_crowding.py
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
def crowding_table(rooms: int, speeds: int, step: int) -> tuple[npt.NDArray[np.float64], ...]:
    """Return the rate pattern and the transition table of encounters that change the speed class by one step.

    With `step = -1`, two pedestrians in the same room meet and the candidate slows down one class, so crowded rooms
    walk slower. With `step = 1`, a pedestrian meets the exit signs and speeds up one class.

    Args:
        rooms:
            The number of rooms.

        speeds:
            The number of speed classes.

        step:
            `-1` to slow down in encounters within a room, `1` to speed up in encounters with the exit signs.

    Returns:
        pattern:
            The rate pattern, 1 for the encounters that act.

        table:
            The transition table.

    Examples:
        >>> from skcomplex.models.social.crowds import crowding_table
        >>> crowding_table(1, 2, 1)[0].ravel().tolist()
        [1.0, 0.0, 0.0, 0.0]
    """
    sites = (rooms + 1) * speeds
    fields = sites if step < 0 else 1
    pattern = np.zeros((sites, fields))
    table = np.zeros((sites, fields, sites))
    for site in range(sites):
        room, speed = divmod(site, speeds)
        outcome = min(max(speed + step, 0), speeds - 1)
        table[site, :, room * speeds + outcome] = 1.0
        if room == rooms or outcome == speed:
            continue
        if step > 0:
            pattern[site, 0] = 1.0
        else:
            pattern[site, room * speeds : (room + 1) * speeds] = 1.0
    return pattern, table

deterrence_table(classes, forces)

Return the rate pattern and the transition table of the encounters of citizens with security forces.

A citizen of a criminal class h > 0 who meets a security agent of efficacy v_k moves one class down at a rate proportional to v_k. Honest citizens are left alone.

Parameters:

Name Type Description Default
classes int

The number of criminality classes.

required
forces int

The number of efficacy classes of the security forces.

required

Returns:

Name Type Description
pattern NDArray[float64]

The rate pattern eta[h, k], the efficacy v_k for criminal classes and 0 for honest ones.

table NDArray[float64]

The transition table A[h, k, i].

Examples:

>>> from skcomplex.models.social.crowds import deterrence_table
>>> deterrence_table(2, 2)[0].tolist()
[[0.0, 0.0], [0.5, 1.0]]
Source code in src/skcomplex/models/social/crowds/_deterrence.py
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
def deterrence_table(classes: int, forces: int) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]:
    """Return the rate pattern and the transition table of the encounters of citizens with security forces.

    A citizen of a criminal class `h > 0` who meets a security agent of efficacy `v_k` moves one class down at a rate
    proportional to `v_k`. Honest citizens are left alone.

    Args:
        classes:
            The number of criminality classes.

        forces:
            The number of efficacy classes of the security forces.

    Returns:
        pattern:
            The rate pattern `eta[h, k]`, the efficacy `v_k` for criminal classes and 0 for honest ones.

        table:
            The transition table `A[h, k, i]`.

    Examples:
        >>> from skcomplex.models.social.crowds import deterrence_table
        >>> deterrence_table(2, 2)[0].tolist()
        [[0.0, 0.0], [0.5, 1.0]]
    """
    efficacy = np.linspace(1.0 / forces, 1.0, forces)
    pattern = np.zeros((classes, forces))
    pattern[1:] = efficacy
    table = np.zeros((classes, forces, classes))
    for candidate in range(classes):
        table[candidate, :, max(candidate - 1, 0)] = 1.0
    return pattern, table

evacuated_fraction(s, rooms, speeds)

Return the fraction of the crowd that has left the building.

Parameters:

Name Type Description Default
s SimpleNamespace

The namespace of one site of the crowd.

required
rooms int

The number of rooms.

required
speeds int

The number of speed classes.

required

Returns:

Name Type Description
fraction Expression

The moment of the indicator of the outside sites, divided by the density.

Source code in src/skcomplex/models/social/crowds/_evacuation.py
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def evacuated_fraction(s: SimpleNamespace, rooms: int, speeds: int) -> Expression:
    """Return the fraction of the crowd that has left the building.

    Args:
        s:
            The namespace of one site of the crowd.

        rooms:
            The number of rooms.

        speeds:
            The number of speed classes.

    Returns:
        fraction:
            The moment of the indicator of the outside sites, divided by the density.
    """
    outside = (np.arange((rooms + 1) * speeds) >= rooms * speeds).astype(float)
    return moment(s.f, 1, outside, normalized=True)

evacuation(branching=2, depth=2, speeds=3, *, mobility=0.5, crowding=2.0, recovery=0.5)

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.

Parameters:

Name Type Description Default
branching int

The number of rooms behind every door of the building.

2
depth int

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

2
speeds int

The number of speed classes.

3
mobility float

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

0.5
crowding float

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

2.0
recovery float

The rate at which pedestrians speed up again.

0.5

Returns:

Name Type Description
system 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
Source code in src/skcomplex/models/social/crowds/_evacuation.py
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
@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(),
    )

evacuation_observables(rooms, speeds)

Return the evacuated fraction and the mean walking speed of a crowd crowd.

Parameters:

Name Type Description Default
rooms int

The number of rooms.

required
speeds int

The number of speed classes.

required

Returns:

Name Type Description
observables list[ObservableSpec]

The observables, each reading the moments of the crowd.

Examples:

>>> from skcomplex.models.social.crowds import evacuation_observables
>>> [item.label for item in evacuation_observables(7, 3)]
['evacuated', 'mean_speed']
Source code in src/skcomplex/models/social/crowds/_evacuation.py
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
def evacuation_observables(rooms: int, speeds: int) -> list[ObservableSpec]:
    """Return the evacuated fraction and the mean walking speed of a crowd `crowd`.

    Args:
        rooms:
            The number of rooms.

        speeds:
            The number of speed classes.

    Returns:
        observables:
            The observables, each reading the moments of the crowd.

    Examples:
        >>> from skcomplex.models.social.crowds import evacuation_observables
        >>> [item.label for item in evacuation_observables(7, 3)]
        ['evacuated', 'mean_speed']
    """
    return [
        ObservableSpec('evacuated', partial(evacuated_fraction, rooms=rooms, speeds=speeds), group='crowd'),
        ObservableSpec('mean_speed', partial(mean_speed, rooms=rooms, speeds=speeds), group='crowd'),
    ]

mean_criminality(s, criminality)

Return the mean criminality of a distribution of citizens.

Parameters:

Name Type Description Default
s SimpleNamespace

The namespace of one member of the distribution.

required
criminality NDArray[float64]

The criminality of every class.

required

Returns:

Name Type Description
mean Expression

The normalized first moment.

Source code in src/skcomplex/models/social/crowds/_criminality.py
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
def mean_criminality(s: SimpleNamespace, criminality: npt.NDArray[np.float64]) -> Expression:
    """Return the mean criminality of a distribution of citizens.

    Args:
        s:
            The namespace of one member of the distribution.

        criminality:
            The criminality of every class.

    Returns:
        mean:
            The normalized first moment.
    """
    return moment(s.f, 1, criminality, normalized=True)

mean_speed(s, rooms, speeds)

Return the mean walking speed of the crowd, as a fraction of the free speed.

Parameters:

Name Type Description Default
s SimpleNamespace

The namespace of one site of the crowd.

required
rooms int

The number of rooms.

required
speeds int

The number of speed classes.

required

Returns:

Name Type Description
speed Expression

The normalized first moment of the speeds of the sites.

Source code in src/skcomplex/models/social/crowds/_evacuation.py
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
def mean_speed(s: SimpleNamespace, rooms: int, speeds: int) -> Expression:
    """Return the mean walking speed of the crowd, as a fraction of the free speed.

    Args:
        s:
            The namespace of one site of the crowd.

        rooms:
            The number of rooms.

        speeds:
            The number of speed classes.

    Returns:
        speed:
            The normalized first moment of the speeds of the sites.
    """
    return moment(s.f, 1, np.tile(walking_speeds(speeds), rooms + 1), normalized=True)

next_rooms(building, exit_room)

Return the next room on the shortest path to the exit room from every room, and the outside for the exit room.

Parameters:

Name Type Description Default
building Graph

The network of rooms, labelled 0 to rooms - 1, whose edges are the doors.

required
exit_room int

The room whose door leads outside.

required

Returns:

Name Type Description
next_rooms list[int]

The next room of every room, with rooms standing for the outside.

Examples:

>>> import networkx as nx
>>> from skcomplex.models.social.crowds import next_rooms
>>> next_rooms(nx.path_graph(3), 0)
[3, 0, 1]
Source code in src/skcomplex/models/social/crowds/_walking.py
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
def next_rooms(building: nx.Graph, exit_room: int) -> list[int]:
    """Return the next room on the shortest path to the exit room from every room, and the outside for the exit room.

    Args:
        building:
            The network of rooms, labelled `0` to `rooms - 1`, whose edges are the doors.

        exit_room:
            The room whose door leads outside.

    Returns:
        next_rooms:
            The next room of every room, with `rooms` standing for the outside.

    Examples:
        >>> import networkx as nx
        >>> from skcomplex.models.social.crowds import next_rooms
        >>> next_rooms(nx.path_graph(3), 0)
        [3, 0, 1]
    """
    paths = nx.shortest_path(building, target=exit_room)
    rooms = building.number_of_nodes()
    return [rooms if room == exit_room else int(paths[room][1]) for room in range(rooms)]

security_density(s, forces)

Return the density of the security forces.

Parameters:

Name Type Description Default
s SimpleNamespace

The namespace of one member of the distribution of the security forces.

required
forces int

The number of efficacy classes.

required

Returns:

Name Type Description
density Expression

The order-0 moment.

Source code in src/skcomplex/models/social/crowds/_criminality.py
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
def security_density(s: SimpleNamespace, forces: int) -> Expression:
    """Return the density of the security forces.

    Args:
        s:
            The namespace of one member of the distribution of the security forces.

        forces:
            The number of efficacy classes.

    Returns:
        density:
            The order-0 moment.
    """
    return moment(s.f, 0, np.arange(forces, dtype=float))

social_force(n_walkers=20, *, length=10.0, speed=1.34, random_state=0)

Return two crowds walking in opposite directions along a periodic corridor, which form lanes.

Parameters:

Name Type Description Default
n_walkers int

The number of walkers in each direction.

20
length float

The length and the width of the corridor, periodic along its length.

10.0
speed float

The desired speed.

1.34
random_state int | Generator | None

The seed or generator of the initial positions.

0

Returns:

Name Type Description
system System

The model, with the groups rightward and leftward.

Examples:

>>> from skcomplex.models.social.crowds import social_force
>>> [group.size for group in social_force(5).elements]
[5, 5]
Source code in src/skcomplex/models/social/crowds/_pedestrians.py
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
@register_model(
    field='social/crowds',
    tags=('pedestrians', 'particles', 'lanes', 'self-organization'),
    references=('Helbing and Molnár 1995', 'Helbing 2001'),
    defaults={'until': 20.0, 'dt': 0.2, 'solver': RK4Solver(dt=0.05)},
)
def social_force(
    n_walkers: int = 20,
    *,
    length: float = 10.0,
    speed: float = 1.34,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return two crowds walking in opposite directions along a periodic corridor, which form lanes.

    Args:
        n_walkers:
            The number of walkers in each direction.

        length:
            The length and the width of the corridor, periodic along its length.

        speed:
            The desired speed.

        random_state:
            The seed or generator of the initial positions.

    Returns:
        system:
            The model, with the groups `rightward` and `leftward`.

    Examples:
        >>> from skcomplex.models.social.crowds import social_force
        >>> [group.size for group in social_force(5).elements]
        [5, 5]
    """
    rng = np.random.default_rng(random_state)
    cutoff = 2.0
    walkers = [
        GroupElement(
            label,
            element=PointElement,
            size=n_walkers,
            values={'x': rng.uniform([0.0, 0.5], [length, length - 0.5], (n_walkers, 2))},
        )
        for label in ('rightward', 'leftward')
    ]
    interactions: list[BaseInteraction] = []
    for label, sign in (('rightward', 1.0), ('leftward', -1.0)):
        interactions += [
            MotivationInteraction(f'{label}_drive', target=label, speed=speed, direction=[sign, 0.0]),
            WallsInteraction(f'{label}_walls', target=label, width=length),
        ]
    pairs = (('rightward', 'rightward'), ('leftward', 'leftward'), ('rightward', 'leftward'))
    interactions += [
        SocialRepulsionInteraction(f'{first}_{second}', first=first, second=second, cutoff=cutoff, period=length)
        for first, second in pairs
    ]
    return System(
        elements=walkers,
        interactions=interactions,
        space=ContinuousSpace(extent=(length, length), boundary='periodic'),
    )

walking_speeds(speeds)

Return the walking speed of every speed class, evenly spaced from a quarter of the free speed to the free speed.

Parameters:

Name Type Description Default
speeds int

The number of speed classes.

required

Returns:

Name Type Description
speeds NDArray[float64]

The walking speed of every class, as a fraction of the free speed.

Examples:

>>> from skcomplex.models.social.crowds import walking_speeds
>>> walking_speeds(4).tolist()
[0.25, 0.5, 0.75, 1.0]
Source code in src/skcomplex/models/social/crowds/_walking.py
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
def walking_speeds(speeds: int) -> npt.NDArray[np.float64]:
    """Return the walking speed of every speed class, evenly spaced from a quarter of the free speed to the free speed.

    Args:
        speeds:
            The number of speed classes.

    Returns:
        speeds:
            The walking speed of every class, as a fraction of the free speed.

    Examples:
        >>> from skcomplex.models.social.crowds import walking_speeds
        >>> walking_speeds(4).tolist()
        [0.25, 0.5, 0.75, 1.0]
    """
    return np.linspace(0.25, 1.0, speeds)

walking_table(building, exit_room, speeds)

Return the rate pattern and the transition table of walking to the next room, an encounter with exit signs.

The sites of the crowd are indexed by room and speed class in row-major order, with one last row of sites for the outside. A pedestrian of speed v in a room walks to the next room on the way to the exit at the rate v.

Parameters:

Name Type Description Default
building Graph

The network of rooms, labelled 0 to rooms - 1.

required
exit_room int

The room whose door leads outside.

required
speeds int

The number of speed classes.

required

Returns:

Name Type Description
pattern NDArray[float64]

The rate pattern eta[h, 0], the walking speed of the site, 0 outside.

table ...

The transition table A[h, 0, i].

Examples:

>>> import networkx as nx
>>> from skcomplex.models.social.crowds import walking_table
>>> pattern, table = walking_table(nx.path_graph(2), 0, 2)
>>> pattern.ravel().tolist()
[0.25, 1.0, 0.25, 1.0, 0.0, 0.0]
Source code in src/skcomplex/models/social/crowds/_walking.py
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
def walking_table(building: nx.Graph, exit_room: int, speeds: int) -> tuple[npt.NDArray[np.float64], ...]:
    """Return the rate pattern and the transition table of walking to the next room, an encounter with exit signs.

    The sites of the crowd are indexed by room and speed class in row-major order, with one last row of sites for the
    outside. A pedestrian of speed `v` in a room walks to the next room on the way to the exit at the rate `v`.

    Args:
        building:
            The network of rooms, labelled `0` to `rooms - 1`.

        exit_room:
            The room whose door leads outside.

        speeds:
            The number of speed classes.

    Returns:
        pattern:
            The rate pattern `eta[h, 0]`, the walking speed of the site, 0 outside.

        table:
            The transition table `A[h, 0, i]`.

    Examples:
        >>> import networkx as nx
        >>> from skcomplex.models.social.crowds import walking_table
        >>> pattern, table = walking_table(nx.path_graph(2), 0, 2)
        >>> pattern.ravel().tolist()
        [0.25, 1.0, 0.25, 1.0, 0.0, 0.0]
    """
    following = next_rooms(building, exit_room)
    velocity = walking_speeds(speeds)
    sites = (len(following) + 1) * speeds
    pattern = np.zeros((sites, 1))
    table = np.zeros((sites, 1, sites))
    for site in range(sites):
        room, speed = divmod(site, speeds)
        if room == len(following):
            table[site, 0, site] = 1.0
            continue
        pattern[site, 0] = velocity[speed]
        table[site, 0, following[room] * speeds + speed] = 1.0
    return pattern, table