Mesa 2.4.0__py3-none-any.whl → 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of Mesa might be problematic. Click here for more details.
- mesa/__init__.py +3 -5
- mesa/agent.py +105 -92
- mesa/batchrunner.py +55 -31
- mesa/datacollection.py +10 -14
- mesa/examples/README.md +37 -0
- mesa/examples/__init__.py +21 -0
- mesa/examples/advanced/epstein_civil_violence/Epstein Civil Violence.ipynb +116 -0
- mesa/examples/advanced/epstein_civil_violence/Readme.md +34 -0
- mesa/examples/advanced/epstein_civil_violence/__init__.py +0 -0
- mesa/examples/advanced/epstein_civil_violence/agents.py +164 -0
- mesa/examples/advanced/epstein_civil_violence/app.py +73 -0
- mesa/examples/advanced/epstein_civil_violence/model.py +114 -0
- mesa/examples/advanced/pd_grid/Readme.md +43 -0
- mesa/examples/advanced/pd_grid/__init__.py +0 -0
- mesa/examples/advanced/pd_grid/agents.py +50 -0
- mesa/examples/advanced/pd_grid/analysis.ipynb +228 -0
- mesa/examples/advanced/pd_grid/app.py +54 -0
- mesa/examples/advanced/pd_grid/model.py +71 -0
- mesa/examples/advanced/sugarscape_g1mt/Readme.md +64 -0
- mesa/examples/advanced/sugarscape_g1mt/__init__.py +0 -0
- mesa/examples/advanced/sugarscape_g1mt/agents.py +344 -0
- mesa/examples/advanced/sugarscape_g1mt/app.py +62 -0
- mesa/examples/advanced/sugarscape_g1mt/model.py +180 -0
- mesa/examples/advanced/sugarscape_g1mt/sugar-map.txt +50 -0
- mesa/examples/advanced/sugarscape_g1mt/tests.py +69 -0
- mesa/examples/advanced/wolf_sheep/Readme.md +57 -0
- mesa/examples/advanced/wolf_sheep/__init__.py +0 -0
- mesa/examples/advanced/wolf_sheep/agents.py +102 -0
- mesa/examples/advanced/wolf_sheep/app.py +84 -0
- mesa/examples/advanced/wolf_sheep/model.py +137 -0
- mesa/examples/basic/__init__.py +0 -0
- mesa/examples/basic/boid_flockers/Readme.md +22 -0
- mesa/examples/basic/boid_flockers/__init__.py +0 -0
- mesa/examples/basic/boid_flockers/agents.py +71 -0
- mesa/examples/basic/boid_flockers/app.py +58 -0
- mesa/examples/basic/boid_flockers/model.py +69 -0
- mesa/examples/basic/boltzmann_wealth_model/Readme.md +56 -0
- mesa/examples/basic/boltzmann_wealth_model/__init__.py +0 -0
- mesa/examples/basic/boltzmann_wealth_model/agents.py +31 -0
- mesa/examples/basic/boltzmann_wealth_model/app.py +74 -0
- mesa/examples/basic/boltzmann_wealth_model/model.py +43 -0
- mesa/examples/basic/boltzmann_wealth_model/st_app.py +115 -0
- mesa/examples/basic/conways_game_of_life/Readme.md +39 -0
- mesa/examples/basic/conways_game_of_life/__init__.py +0 -0
- mesa/examples/basic/conways_game_of_life/agents.py +47 -0
- mesa/examples/basic/conways_game_of_life/app.py +51 -0
- mesa/examples/basic/conways_game_of_life/model.py +31 -0
- mesa/examples/basic/conways_game_of_life/st_app.py +72 -0
- mesa/examples/basic/schelling/Readme.md +40 -0
- mesa/examples/basic/schelling/__init__.py +0 -0
- mesa/examples/basic/schelling/agents.py +26 -0
- mesa/examples/basic/schelling/analysis.ipynb +205 -0
- mesa/examples/basic/schelling/app.py +42 -0
- mesa/examples/basic/schelling/model.py +59 -0
- mesa/examples/basic/virus_on_network/Readme.md +61 -0
- mesa/examples/basic/virus_on_network/__init__.py +0 -0
- mesa/examples/basic/virus_on_network/agents.py +69 -0
- mesa/examples/basic/virus_on_network/app.py +114 -0
- mesa/examples/basic/virus_on_network/model.py +96 -0
- mesa/experimental/UserParam.py +18 -7
- mesa/experimental/__init__.py +10 -2
- mesa/experimental/cell_space/__init__.py +16 -1
- mesa/experimental/cell_space/cell.py +93 -23
- mesa/experimental/cell_space/cell_agent.py +117 -21
- mesa/experimental/cell_space/cell_collection.py +56 -19
- mesa/experimental/cell_space/discrete_space.py +92 -8
- mesa/experimental/cell_space/grid.py +33 -9
- mesa/experimental/cell_space/network.py +15 -10
- mesa/experimental/cell_space/voronoi.py +257 -0
- mesa/experimental/components/altair.py +11 -2
- mesa/experimental/components/matplotlib.py +132 -26
- mesa/experimental/devs/__init__.py +2 -0
- mesa/experimental/devs/eventlist.py +54 -15
- mesa/experimental/devs/examples/epstein_civil_violence.py +69 -38
- mesa/experimental/devs/examples/wolf_sheep.py +42 -43
- mesa/experimental/devs/simulator.py +57 -16
- mesa/experimental/{jupyter_viz.py → solara_viz.py} +151 -99
- mesa/model.py +136 -78
- mesa/space.py +208 -148
- mesa/time.py +63 -80
- mesa/visualization/__init__.py +25 -6
- mesa/visualization/components/__init__.py +83 -0
- mesa/visualization/components/altair_components.py +188 -0
- mesa/visualization/components/matplotlib_components.py +175 -0
- mesa/visualization/mpl_space_drawing.py +593 -0
- mesa/visualization/solara_viz.py +458 -0
- mesa/visualization/user_param.py +69 -0
- mesa/visualization/utils.py +9 -0
- {mesa-2.4.0.dist-info → mesa-3.0.0.dist-info}/METADATA +62 -17
- mesa-3.0.0.dist-info/RECORD +95 -0
- mesa-3.0.0.dist-info/licenses/LICENSE +202 -0
- mesa-2.4.0.dist-info/licenses/LICENSE → mesa-3.0.0.dist-info/licenses/NOTICE +2 -2
- mesa/cookiecutter-mesa/cookiecutter.json +0 -8
- mesa/cookiecutter-mesa/hooks/post_gen_project.py +0 -11
- mesa/cookiecutter-mesa/{{cookiecutter.snake}}/README.md +0 -4
- mesa/cookiecutter-mesa/{{cookiecutter.snake}}/run.pytemplate +0 -3
- mesa/cookiecutter-mesa/{{cookiecutter.snake}}/setup.pytemplate +0 -11
- mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/model.pytemplate +0 -60
- mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/server.pytemplate +0 -36
- mesa/flat/__init__.py +0 -6
- mesa/flat/visualization.py +0 -5
- mesa/main.py +0 -63
- mesa/visualization/ModularVisualization.py +0 -1
- mesa/visualization/TextVisualization.py +0 -1
- mesa/visualization/UserParam.py +0 -1
- mesa/visualization/modules.py +0 -1
- mesa-2.4.0.dist-info/RECORD +0 -45
- /mesa/{cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}} → examples/advanced}/__init__.py +0 -0
- {mesa-2.4.0.dist-info → mesa-3.0.0.dist-info}/WHEEL +0 -0
- {mesa-2.4.0.dist-info → mesa-3.0.0.dist-info}/entry_points.txt +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
"""Altair components."""
|
|
2
|
+
|
|
1
3
|
import contextlib
|
|
2
|
-
from typing import Optional
|
|
3
4
|
|
|
4
5
|
import solara
|
|
5
6
|
|
|
@@ -8,7 +9,15 @@ with contextlib.suppress(ImportError):
|
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
@solara.component
|
|
11
|
-
def SpaceAltair(model, agent_portrayal, dependencies:
|
|
12
|
+
def SpaceAltair(model, agent_portrayal, dependencies: list[any] | None = None):
|
|
13
|
+
"""A component that renders a Space using Altair.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
model: a model instance
|
|
17
|
+
agent_portrayal: agent portray specification
|
|
18
|
+
dependencies: optional list of dependencies (currently not used)
|
|
19
|
+
|
|
20
|
+
"""
|
|
12
21
|
space = getattr(model, "grid", None)
|
|
13
22
|
if space is None:
|
|
14
23
|
# Sometimes the space is defined as model.space instead of model.grid
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
"""Support for using matplotlib to draw spaces."""
|
|
2
|
+
|
|
3
|
+
from collections import defaultdict
|
|
2
4
|
|
|
3
5
|
import networkx as nx
|
|
4
6
|
import solara
|
|
@@ -6,10 +8,19 @@ from matplotlib.figure import Figure
|
|
|
6
8
|
from matplotlib.ticker import MaxNLocator
|
|
7
9
|
|
|
8
10
|
import mesa
|
|
11
|
+
from mesa.experimental.cell_space import VoronoiGrid
|
|
9
12
|
|
|
10
13
|
|
|
11
14
|
@solara.component
|
|
12
|
-
def SpaceMatplotlib(model, agent_portrayal, dependencies:
|
|
15
|
+
def SpaceMatplotlib(model, agent_portrayal, dependencies: list[any] | None = None):
|
|
16
|
+
"""A component for rendering a space using Matplotlib.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
model: a model instance
|
|
20
|
+
agent_portrayal: a specification of how to portray an agent.
|
|
21
|
+
dependencies: list of dependencies.
|
|
22
|
+
|
|
23
|
+
"""
|
|
13
24
|
space_fig = Figure()
|
|
14
25
|
space_ax = space_fig.subplots()
|
|
15
26
|
space = getattr(model, "grid", None)
|
|
@@ -20,17 +31,51 @@ def SpaceMatplotlib(model, agent_portrayal, dependencies: Optional[list[any]] =
|
|
|
20
31
|
_draw_network_grid(space, space_ax, agent_portrayal)
|
|
21
32
|
elif isinstance(space, mesa.space.ContinuousSpace):
|
|
22
33
|
_draw_continuous_space(space, space_ax, agent_portrayal)
|
|
34
|
+
elif isinstance(space, VoronoiGrid):
|
|
35
|
+
_draw_voronoi(space, space_ax, agent_portrayal)
|
|
23
36
|
else:
|
|
24
37
|
_draw_grid(space, space_ax, agent_portrayal)
|
|
25
38
|
solara.FigureMatplotlib(space_fig, format="png", dependencies=dependencies)
|
|
26
39
|
|
|
27
40
|
|
|
41
|
+
# matplotlib scatter does not allow for multiple shapes in one call
|
|
42
|
+
def _split_and_scatter(portray_data, space_ax):
|
|
43
|
+
grouped_data = defaultdict(lambda: {"x": [], "y": [], "s": [], "c": []})
|
|
44
|
+
|
|
45
|
+
# Extract data from the dictionary
|
|
46
|
+
x = portray_data["x"]
|
|
47
|
+
y = portray_data["y"]
|
|
48
|
+
s = portray_data["s"]
|
|
49
|
+
c = portray_data["c"]
|
|
50
|
+
m = portray_data["m"]
|
|
51
|
+
|
|
52
|
+
if not (len(x) == len(y) == len(s) == len(c) == len(m)):
|
|
53
|
+
raise ValueError(
|
|
54
|
+
"Length mismatch in portrayal data lists: "
|
|
55
|
+
f"x: {len(x)}, y: {len(y)}, size: {len(s)}, "
|
|
56
|
+
f"color: {len(c)}, marker: {len(m)}"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Group the data by marker
|
|
60
|
+
for i in range(len(x)):
|
|
61
|
+
marker = m[i]
|
|
62
|
+
grouped_data[marker]["x"].append(x[i])
|
|
63
|
+
grouped_data[marker]["y"].append(y[i])
|
|
64
|
+
grouped_data[marker]["s"].append(s[i])
|
|
65
|
+
grouped_data[marker]["c"].append(c[i])
|
|
66
|
+
|
|
67
|
+
# Plot each group with the same marker
|
|
68
|
+
for marker, data in grouped_data.items():
|
|
69
|
+
space_ax.scatter(data["x"], data["y"], s=data["s"], c=data["c"], marker=marker)
|
|
70
|
+
|
|
71
|
+
|
|
28
72
|
def _draw_grid(space, space_ax, agent_portrayal):
|
|
29
73
|
def portray(g):
|
|
30
74
|
x = []
|
|
31
75
|
y = []
|
|
32
76
|
s = [] # size
|
|
33
77
|
c = [] # color
|
|
78
|
+
m = [] # shape
|
|
34
79
|
for i in range(g.width):
|
|
35
80
|
for j in range(g.height):
|
|
36
81
|
content = g._grid[i][j]
|
|
@@ -43,23 +88,23 @@ def _draw_grid(space, space_ax, agent_portrayal):
|
|
|
43
88
|
data = agent_portrayal(agent)
|
|
44
89
|
x.append(i)
|
|
45
90
|
y.append(j)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
91
|
+
|
|
92
|
+
# This is the default value for the marker size, which auto-scales
|
|
93
|
+
# according to the grid area.
|
|
94
|
+
default_size = (180 / max(g.width, g.height)) ** 2
|
|
95
|
+
# establishing a default prevents misalignment if some agents are not given size, color, etc.
|
|
96
|
+
size = data.get("size", default_size)
|
|
97
|
+
s.append(size)
|
|
98
|
+
color = data.get("color", "tab:blue")
|
|
99
|
+
c.append(color)
|
|
100
|
+
mark = data.get("shape", "o")
|
|
101
|
+
m.append(mark)
|
|
102
|
+
out = {"x": x, "y": y, "s": s, "c": c, "m": m}
|
|
58
103
|
return out
|
|
59
104
|
|
|
60
105
|
space_ax.set_xlim(-1, space.width)
|
|
61
106
|
space_ax.set_ylim(-1, space.height)
|
|
62
|
-
|
|
107
|
+
_split_and_scatter(portray(space), space_ax)
|
|
63
108
|
|
|
64
109
|
|
|
65
110
|
def _draw_network_grid(space, space_ax, agent_portrayal):
|
|
@@ -79,20 +124,23 @@ def _draw_continuous_space(space, space_ax, agent_portrayal):
|
|
|
79
124
|
y = []
|
|
80
125
|
s = [] # size
|
|
81
126
|
c = [] # color
|
|
127
|
+
m = [] # shape
|
|
82
128
|
for agent in space._agent_to_index:
|
|
83
129
|
data = agent_portrayal(agent)
|
|
84
130
|
_x, _y = agent.pos
|
|
85
131
|
x.append(_x)
|
|
86
132
|
y.append(_y)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
133
|
+
|
|
134
|
+
# This is matplotlib's default marker size
|
|
135
|
+
default_size = 20
|
|
136
|
+
# establishing a default prevents misalignment if some agents are not given size, color, etc.
|
|
137
|
+
size = data.get("size", default_size)
|
|
138
|
+
s.append(size)
|
|
139
|
+
color = data.get("color", "tab:blue")
|
|
140
|
+
c.append(color)
|
|
141
|
+
mark = data.get("shape", "o")
|
|
142
|
+
m.append(mark)
|
|
143
|
+
out = {"x": x, "y": y, "s": s, "c": c, "m": m}
|
|
96
144
|
return out
|
|
97
145
|
|
|
98
146
|
# Determine border style based on space.torus
|
|
@@ -112,11 +160,69 @@ def _draw_continuous_space(space, space_ax, agent_portrayal):
|
|
|
112
160
|
space_ax.set_ylim(space.y_min - y_padding, space.y_max + y_padding)
|
|
113
161
|
|
|
114
162
|
# Portray and scatter the agents in the space
|
|
163
|
+
_split_and_scatter(portray(space), space_ax)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _draw_voronoi(space, space_ax, agent_portrayal):
|
|
167
|
+
def portray(g):
|
|
168
|
+
x = []
|
|
169
|
+
y = []
|
|
170
|
+
s = [] # size
|
|
171
|
+
c = [] # color
|
|
172
|
+
|
|
173
|
+
for cell in g.all_cells:
|
|
174
|
+
for agent in cell.agents:
|
|
175
|
+
data = agent_portrayal(agent)
|
|
176
|
+
x.append(cell.coordinate[0])
|
|
177
|
+
y.append(cell.coordinate[1])
|
|
178
|
+
if "size" in data:
|
|
179
|
+
s.append(data["size"])
|
|
180
|
+
if "color" in data:
|
|
181
|
+
c.append(data["color"])
|
|
182
|
+
out = {"x": x, "y": y}
|
|
183
|
+
# This is the default value for the marker size, which auto-scales
|
|
184
|
+
# according to the grid area.
|
|
185
|
+
out["s"] = s
|
|
186
|
+
if len(c) > 0:
|
|
187
|
+
out["c"] = c
|
|
188
|
+
|
|
189
|
+
return out
|
|
190
|
+
|
|
191
|
+
x_list = [i[0] for i in space.centroids_coordinates]
|
|
192
|
+
y_list = [i[1] for i in space.centroids_coordinates]
|
|
193
|
+
x_max = max(x_list)
|
|
194
|
+
x_min = min(x_list)
|
|
195
|
+
y_max = max(y_list)
|
|
196
|
+
y_min = min(y_list)
|
|
197
|
+
|
|
198
|
+
width = x_max - x_min
|
|
199
|
+
x_padding = width / 20
|
|
200
|
+
height = y_max - y_min
|
|
201
|
+
y_padding = height / 20
|
|
202
|
+
space_ax.set_xlim(x_min - x_padding, x_max + x_padding)
|
|
203
|
+
space_ax.set_ylim(y_min - y_padding, y_max + y_padding)
|
|
115
204
|
space_ax.scatter(**portray(space))
|
|
116
205
|
|
|
206
|
+
for cell in space.all_cells:
|
|
207
|
+
polygon = cell.properties["polygon"]
|
|
208
|
+
space_ax.fill(
|
|
209
|
+
*zip(*polygon),
|
|
210
|
+
alpha=min(1, cell.properties[space.cell_coloring_property]),
|
|
211
|
+
c="red",
|
|
212
|
+
) # Plot filled polygon
|
|
213
|
+
space_ax.plot(*zip(*polygon), color="black") # Plot polygon edges in red
|
|
214
|
+
|
|
117
215
|
|
|
118
216
|
@solara.component
|
|
119
|
-
def PlotMatplotlib(model, measure, dependencies:
|
|
217
|
+
def PlotMatplotlib(model, measure, dependencies: list[any] | None = None):
|
|
218
|
+
"""A solara component for creating a matplotlib figure.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
model: Model instance
|
|
222
|
+
measure: measure to plot
|
|
223
|
+
dependencies: list of additional dependencies
|
|
224
|
+
|
|
225
|
+
"""
|
|
120
226
|
fig = Figure()
|
|
121
227
|
ax = fig.subplots()
|
|
122
228
|
df = model.datacollector.get_model_vars_dataframe()
|
|
@@ -127,7 +233,7 @@ def PlotMatplotlib(model, measure, dependencies: Optional[list[any]] = None):
|
|
|
127
233
|
for m, color in measure.items():
|
|
128
234
|
ax.plot(df.loc[:, m], label=m, color=color)
|
|
129
235
|
fig.legend()
|
|
130
|
-
elif isinstance(measure,
|
|
236
|
+
elif isinstance(measure, list | tuple):
|
|
131
237
|
for m in measure:
|
|
132
238
|
ax.plot(df.loc[:, m], label=m)
|
|
133
239
|
fig.legend()
|
|
@@ -1,23 +1,28 @@
|
|
|
1
|
+
"""Eventlist which is at the core of event scheduling."""
|
|
2
|
+
|
|
1
3
|
from __future__ import annotations
|
|
2
4
|
|
|
3
5
|
import itertools
|
|
6
|
+
from collections.abc import Callable
|
|
4
7
|
from enum import IntEnum
|
|
5
8
|
from heapq import heapify, heappop, heappush
|
|
6
9
|
from types import MethodType
|
|
7
|
-
from typing import Any
|
|
10
|
+
from typing import Any
|
|
8
11
|
from weakref import WeakMethod, ref
|
|
9
12
|
|
|
10
13
|
|
|
11
14
|
class Priority(IntEnum):
|
|
15
|
+
"""Enumeration of priority levels."""
|
|
16
|
+
|
|
12
17
|
LOW = 10
|
|
13
18
|
DEFAULT = 5
|
|
14
19
|
HIGH = 1
|
|
15
20
|
|
|
16
21
|
|
|
17
22
|
class SimulationEvent:
|
|
18
|
-
"""A simulation event
|
|
23
|
+
"""A simulation event.
|
|
19
24
|
|
|
20
|
-
|
|
25
|
+
The callable is wrapped using weakref, so there is no need to explicitly cancel event if e.g., an agent
|
|
21
26
|
is removed from the simulation.
|
|
22
27
|
|
|
23
28
|
Attributes:
|
|
@@ -28,12 +33,18 @@ class SimulationEvent:
|
|
|
28
33
|
function_args (list[Any]): Argument for the function
|
|
29
34
|
function_kwargs (Dict[str, Any]): Keyword arguments for the function
|
|
30
35
|
|
|
36
|
+
|
|
37
|
+
Notes:
|
|
38
|
+
simulation events use a weak reference to the callable. Therefore, you cannot pass a lambda function in fn.
|
|
39
|
+
A simulation event where the callable no longer exists (e.g., because the agent has been removed from the model)
|
|
40
|
+
will fail silently.
|
|
41
|
+
|
|
31
42
|
"""
|
|
32
43
|
|
|
33
44
|
_ids = itertools.count()
|
|
34
45
|
|
|
35
46
|
@property
|
|
36
|
-
def CANCELED(self) -> bool:
|
|
47
|
+
def CANCELED(self) -> bool: # noqa: D102
|
|
37
48
|
return self._canceled
|
|
38
49
|
|
|
39
50
|
def __init__(
|
|
@@ -44,6 +55,15 @@ class SimulationEvent:
|
|
|
44
55
|
function_args: list[Any] | None = None,
|
|
45
56
|
function_kwargs: dict[str, Any] | None = None,
|
|
46
57
|
) -> None:
|
|
58
|
+
"""Initialize a simulation event.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
time: the instant of time of the simulation event
|
|
62
|
+
function: the callable to invoke
|
|
63
|
+
priority: the priority of the event
|
|
64
|
+
function_args: arguments for callable
|
|
65
|
+
function_kwargs: keyword arguments for the callable
|
|
66
|
+
"""
|
|
47
67
|
super().__init__()
|
|
48
68
|
if not callable(function):
|
|
49
69
|
raise Exception()
|
|
@@ -63,20 +83,20 @@ class SimulationEvent:
|
|
|
63
83
|
self.function_kwargs = function_kwargs if function_kwargs else {}
|
|
64
84
|
|
|
65
85
|
def execute(self):
|
|
66
|
-
"""
|
|
86
|
+
"""Execute this event."""
|
|
67
87
|
if not self._canceled:
|
|
68
88
|
fn = self.fn()
|
|
69
89
|
if fn is not None:
|
|
70
90
|
fn(*self.function_args, **self.function_kwargs)
|
|
71
91
|
|
|
72
92
|
def cancel(self) -> None:
|
|
73
|
-
"""
|
|
93
|
+
"""Cancel this event."""
|
|
74
94
|
self._canceled = True
|
|
75
95
|
self.fn = None
|
|
76
96
|
self.function_args = []
|
|
77
97
|
self.function_kwargs = {}
|
|
78
98
|
|
|
79
|
-
def __lt__(self, other):
|
|
99
|
+
def __lt__(self, other): # noqa
|
|
80
100
|
# Define a total ordering for events to be used by the heapq
|
|
81
101
|
return (self.time, self.priority, self.unique_id) < (
|
|
82
102
|
other.time,
|
|
@@ -86,30 +106,31 @@ class SimulationEvent:
|
|
|
86
106
|
|
|
87
107
|
|
|
88
108
|
class EventList:
|
|
89
|
-
"""An event list
|
|
109
|
+
"""An event list.
|
|
90
110
|
|
|
91
111
|
This is a heap queue sorted list of events. Events are always removed from the left, so heapq is a performant and
|
|
92
112
|
appropriate data structure. Events are sorted based on their time stamp, their priority, and their unique_id
|
|
93
113
|
as a tie-breaker, guaranteeing a complete ordering.
|
|
94
114
|
|
|
115
|
+
|
|
95
116
|
"""
|
|
96
117
|
|
|
97
118
|
def __init__(self):
|
|
119
|
+
"""Initialize an event list."""
|
|
98
120
|
self._events: list[SimulationEvent] = []
|
|
99
121
|
heapify(self._events)
|
|
100
122
|
|
|
101
123
|
def add_event(self, event: SimulationEvent):
|
|
102
|
-
"""Add the event to the event list
|
|
124
|
+
"""Add the event to the event list.
|
|
103
125
|
|
|
104
126
|
Args:
|
|
105
127
|
event (SimulationEvent): The event to be added
|
|
106
128
|
|
|
107
129
|
"""
|
|
108
|
-
|
|
109
130
|
heappush(self._events, event)
|
|
110
131
|
|
|
111
132
|
def peak_ahead(self, n: int = 1) -> list[SimulationEvent]:
|
|
112
|
-
"""Look at the first n non-canceled event in the event list
|
|
133
|
+
"""Look at the first n non-canceled event in the event list.
|
|
113
134
|
|
|
114
135
|
Args:
|
|
115
136
|
n (int): The number of events to look ahead
|
|
@@ -138,7 +159,7 @@ class EventList:
|
|
|
138
159
|
return peek
|
|
139
160
|
|
|
140
161
|
def pop_event(self) -> SimulationEvent:
|
|
141
|
-
"""
|
|
162
|
+
"""Pop the first element from the event list."""
|
|
142
163
|
while self._events:
|
|
143
164
|
event = heappop(self._events)
|
|
144
165
|
if not event.CANCELED:
|
|
@@ -146,16 +167,33 @@ class EventList:
|
|
|
146
167
|
raise IndexError("Event list is empty")
|
|
147
168
|
|
|
148
169
|
def is_empty(self) -> bool:
|
|
170
|
+
"""Return whether the event list is empty."""
|
|
149
171
|
return len(self) == 0
|
|
150
172
|
|
|
151
|
-
def __contains__(self, event: SimulationEvent) -> bool:
|
|
173
|
+
def __contains__(self, event: SimulationEvent) -> bool: # noqa
|
|
152
174
|
return event in self._events
|
|
153
175
|
|
|
154
|
-
def __len__(self) -> int:
|
|
176
|
+
def __len__(self) -> int: # noqa
|
|
155
177
|
return len(self._events)
|
|
156
178
|
|
|
179
|
+
def __repr__(self) -> str:
|
|
180
|
+
"""Return a string representation of the event list."""
|
|
181
|
+
events_str = ", ".join(
|
|
182
|
+
[
|
|
183
|
+
f"Event(time={e.time}, priority={e.priority}, id={e.unique_id})"
|
|
184
|
+
for e in self._events
|
|
185
|
+
if not e.CANCELED
|
|
186
|
+
]
|
|
187
|
+
)
|
|
188
|
+
return f"EventList([{events_str}])"
|
|
189
|
+
|
|
157
190
|
def remove(self, event: SimulationEvent) -> None:
|
|
158
|
-
"""
|
|
191
|
+
"""Remove an event from the event list.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
event (SimulationEvent): The event to be removed
|
|
195
|
+
|
|
196
|
+
"""
|
|
159
197
|
# we cannot simply remove items from _eventlist because this breaks
|
|
160
198
|
# heap structure invariant. So, we use a form of lazy deletion.
|
|
161
199
|
# SimEvents have a CANCELED flag that we set to True, while popping and peak_ahead
|
|
@@ -163,4 +201,5 @@ class EventList:
|
|
|
163
201
|
event.cancel()
|
|
164
202
|
|
|
165
203
|
def clear(self):
|
|
204
|
+
"""Clear the event list."""
|
|
166
205
|
self._events.clear()
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Epstein civil violence example using ABMSimulator."""
|
|
2
|
+
|
|
1
3
|
import enum
|
|
2
4
|
import math
|
|
3
5
|
|
|
@@ -7,21 +9,32 @@ from mesa.space import SingleGrid
|
|
|
7
9
|
|
|
8
10
|
|
|
9
11
|
class EpsteinAgent(Agent):
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
"""Epstein Agent."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, model, vision, movement):
|
|
15
|
+
"""Initialize the agent.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
model: a model instance
|
|
19
|
+
vision: size of neighborhood
|
|
20
|
+
movement: boolean whether agent can move or not
|
|
21
|
+
"""
|
|
22
|
+
super().__init__(model)
|
|
12
23
|
self.vision = vision
|
|
13
24
|
self.movement = movement
|
|
14
25
|
|
|
15
26
|
|
|
16
27
|
class AgentState(enum.IntEnum):
|
|
28
|
+
"""Agent states."""
|
|
29
|
+
|
|
17
30
|
QUIESCENT = enum.auto()
|
|
18
31
|
ARRESTED = enum.auto()
|
|
19
32
|
ACTIVE = enum.auto()
|
|
20
33
|
|
|
21
34
|
|
|
22
35
|
class Citizen(EpsteinAgent):
|
|
23
|
-
"""
|
|
24
|
-
|
|
36
|
+
"""A member of the general population, may or may not be in active rebellion.
|
|
37
|
+
|
|
25
38
|
Summary of rule: If grievance - risk > threshold, rebel.
|
|
26
39
|
|
|
27
40
|
Attributes:
|
|
@@ -46,7 +59,6 @@ class Citizen(EpsteinAgent):
|
|
|
46
59
|
|
|
47
60
|
def __init__(
|
|
48
61
|
self,
|
|
49
|
-
unique_id,
|
|
50
62
|
model,
|
|
51
63
|
vision,
|
|
52
64
|
movement,
|
|
@@ -56,11 +68,13 @@ class Citizen(EpsteinAgent):
|
|
|
56
68
|
threshold,
|
|
57
69
|
arrest_prob_constant,
|
|
58
70
|
):
|
|
59
|
-
"""
|
|
60
|
-
|
|
71
|
+
"""Create a new Citizen.
|
|
72
|
+
|
|
61
73
|
Args:
|
|
62
|
-
unique_id: unique int
|
|
63
74
|
model : model instance
|
|
75
|
+
vision: number of cells in each direction (N, S, E and W) that
|
|
76
|
+
agent can inspect. Exogenous.
|
|
77
|
+
movement: whether agent can move or not
|
|
64
78
|
hardship: Agent's 'perceived hardship (i.e., physical or economic
|
|
65
79
|
privation).' Exogenous, drawn from U(0,1).
|
|
66
80
|
regime_legitimacy: Agent's perception of regime legitimacy, equal
|
|
@@ -68,10 +82,10 @@ class Citizen(EpsteinAgent):
|
|
|
68
82
|
risk_aversion: Exogenous, drawn from U(0,1).
|
|
69
83
|
threshold: if (grievance - (risk_aversion * arrest_probability)) >
|
|
70
84
|
threshold, go/remain Active
|
|
71
|
-
|
|
72
|
-
|
|
85
|
+
arrest_prob_constant : agent's assessment of arrest probability
|
|
86
|
+
|
|
73
87
|
"""
|
|
74
|
-
super().__init__(
|
|
88
|
+
super().__init__(model, vision, movement)
|
|
75
89
|
self.hardship = hardship
|
|
76
90
|
self.regime_legitimacy = regime_legitimacy
|
|
77
91
|
self.risk_aversion = risk_aversion
|
|
@@ -82,9 +96,7 @@ class Citizen(EpsteinAgent):
|
|
|
82
96
|
self.arrest_prob_constant = arrest_prob_constant
|
|
83
97
|
|
|
84
98
|
def step(self):
|
|
85
|
-
"""
|
|
86
|
-
Decide whether to activate, then move if applicable.
|
|
87
|
-
"""
|
|
99
|
+
"""Decide whether to activate, then move if applicable."""
|
|
88
100
|
self.update_neighbors()
|
|
89
101
|
self.update_estimated_arrest_probability()
|
|
90
102
|
net_risk = self.risk_aversion * self.arrest_probability
|
|
@@ -97,9 +109,7 @@ class Citizen(EpsteinAgent):
|
|
|
97
109
|
self.model.grid.move_agent(self, new_pos)
|
|
98
110
|
|
|
99
111
|
def update_neighbors(self):
|
|
100
|
-
"""
|
|
101
|
-
Look around and see who my neighbors are
|
|
102
|
-
"""
|
|
112
|
+
"""Look around and see who my neighbors are."""
|
|
103
113
|
self.neighborhood = self.model.grid.get_neighborhood(
|
|
104
114
|
self.pos, moore=True, radius=self.vision
|
|
105
115
|
)
|
|
@@ -109,10 +119,7 @@ class Citizen(EpsteinAgent):
|
|
|
109
119
|
]
|
|
110
120
|
|
|
111
121
|
def update_estimated_arrest_probability(self):
|
|
112
|
-
"""
|
|
113
|
-
Based on the ratio of cops to actives in my neighborhood, estimate the
|
|
114
|
-
p(Arrest | I go active).
|
|
115
|
-
"""
|
|
122
|
+
"""Based on the ratio of cops to actives in my neighborhood, estimate the p(Arrest | I go active)."""
|
|
116
123
|
cops_in_vision = len([c for c in self.neighbors if isinstance(c, Cop)])
|
|
117
124
|
actives_in_vision = 1.0 # citizen counts herself
|
|
118
125
|
for c in self.neighbors:
|
|
@@ -123,18 +130,25 @@ class Citizen(EpsteinAgent):
|
|
|
123
130
|
)
|
|
124
131
|
|
|
125
132
|
def sent_to_jail(self, value):
|
|
133
|
+
"""Sent agent to jail.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
value: duration of jail sentence
|
|
137
|
+
|
|
138
|
+
"""
|
|
126
139
|
self.model.active_agents.remove(self)
|
|
127
140
|
self.condition = AgentState.ARRESTED
|
|
128
141
|
self.model.simulator.schedule_event_relative(self.release_from_jail, value)
|
|
129
142
|
|
|
130
143
|
def release_from_jail(self):
|
|
144
|
+
"""Release agent from jail."""
|
|
131
145
|
self.model.active_agents.add(self)
|
|
132
146
|
self.condition = AgentState.QUIESCENT
|
|
133
147
|
|
|
134
148
|
|
|
135
149
|
class Cop(EpsteinAgent):
|
|
136
|
-
"""
|
|
137
|
-
|
|
150
|
+
"""A cop for life. No defection.
|
|
151
|
+
|
|
138
152
|
Summary of rule: Inspect local vision and arrest a random active agent.
|
|
139
153
|
|
|
140
154
|
Attributes:
|
|
@@ -144,15 +158,20 @@ class Cop(EpsteinAgent):
|
|
|
144
158
|
able to inspect
|
|
145
159
|
"""
|
|
146
160
|
|
|
147
|
-
def __init__(self,
|
|
148
|
-
|
|
161
|
+
def __init__(self, model, vision, movement, max_jail_term):
|
|
162
|
+
"""Initialize a Cop agent.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
model: a model instance
|
|
166
|
+
vision: size of neighborhood
|
|
167
|
+
movement: whether agent can move or not
|
|
168
|
+
max_jail_term: maximum jail sentence
|
|
169
|
+
"""
|
|
170
|
+
super().__init__(model, vision, movement)
|
|
149
171
|
self.max_jail_term = max_jail_term
|
|
150
172
|
|
|
151
173
|
def step(self):
|
|
152
|
-
"""
|
|
153
|
-
Inspect local vision and arrest a random active agent. Move if
|
|
154
|
-
applicable.
|
|
155
|
-
"""
|
|
174
|
+
"""Inspect local vision and arrest a random active agent. Move if applicable."""
|
|
156
175
|
self.update_neighbors()
|
|
157
176
|
active_neighbors = []
|
|
158
177
|
for agent in self.neighbors:
|
|
@@ -166,9 +185,7 @@ class Cop(EpsteinAgent):
|
|
|
166
185
|
self.model.grid.move_agent(self, new_pos)
|
|
167
186
|
|
|
168
187
|
def update_neighbors(self):
|
|
169
|
-
"""
|
|
170
|
-
Look around and see who my neighbors are.
|
|
171
|
-
"""
|
|
188
|
+
"""Look around and see who my neighbors are."""
|
|
172
189
|
self.neighborhood = self.model.grid.get_neighborhood(
|
|
173
190
|
self.pos, moore=True, radius=self.vision
|
|
174
191
|
)
|
|
@@ -179,9 +196,8 @@ class Cop(EpsteinAgent):
|
|
|
179
196
|
|
|
180
197
|
|
|
181
198
|
class EpsteinCivilViolence(Model):
|
|
182
|
-
"""
|
|
183
|
-
|
|
184
|
-
approach," by Joshua Epstein.
|
|
199
|
+
"""Model 1 from "Modeling civil violence: An agent-based computational approach," by Joshua Epstein.
|
|
200
|
+
|
|
185
201
|
http://www.pnas.org/content/99/suppl_3/7243.full
|
|
186
202
|
Attributes:
|
|
187
203
|
height: grid height
|
|
@@ -220,6 +236,23 @@ class EpsteinCivilViolence(Model):
|
|
|
220
236
|
max_iters=1000,
|
|
221
237
|
seed=None,
|
|
222
238
|
):
|
|
239
|
+
"""Initialize the Eppstein civil violence model.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
width: the width of the grid
|
|
243
|
+
height: the height of the grid
|
|
244
|
+
citizen_density: density of citizens
|
|
245
|
+
cop_density: density of cops
|
|
246
|
+
citizen_vision: size of citizen vision
|
|
247
|
+
cop_vision: size of cop vision
|
|
248
|
+
legitimacy: perceived legitimacy
|
|
249
|
+
max_jail_term: maximum jail term
|
|
250
|
+
active_threshold: threshold for citizen to become active
|
|
251
|
+
arrest_prob_constant: arrest probability
|
|
252
|
+
movement: allow agent movement or not
|
|
253
|
+
max_iters: number of iterations
|
|
254
|
+
seed: seed for random number generator
|
|
255
|
+
"""
|
|
223
256
|
super().__init__(seed)
|
|
224
257
|
if cop_density + citizen_density > 1:
|
|
225
258
|
raise ValueError("Cop density + citizen density must be less than 1")
|
|
@@ -236,7 +269,6 @@ class EpsteinCivilViolence(Model):
|
|
|
236
269
|
for _, pos in self.grid.coord_iter():
|
|
237
270
|
if self.random.random() < self.cop_density:
|
|
238
271
|
agent = Cop(
|
|
239
|
-
self.next_id(),
|
|
240
272
|
self,
|
|
241
273
|
cop_vision,
|
|
242
274
|
movement,
|
|
@@ -244,7 +276,6 @@ class EpsteinCivilViolence(Model):
|
|
|
244
276
|
)
|
|
245
277
|
elif self.random.random() < (self.cop_density + self.citizen_density):
|
|
246
278
|
agent = Citizen(
|
|
247
|
-
self.next_id(),
|
|
248
279
|
self,
|
|
249
280
|
citizen_vision,
|
|
250
281
|
movement,
|
|
@@ -271,4 +302,4 @@ if __name__ == "__main__":
|
|
|
271
302
|
|
|
272
303
|
simulator.setup(model)
|
|
273
304
|
|
|
274
|
-
simulator.
|
|
305
|
+
simulator.run_for(time_delta=100)
|