Mesa 3.1.0.dev0__py3-none-any.whl → 3.1.1__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 -3
- mesa/agent.py +48 -0
- mesa/batchrunner.py +14 -1
- mesa/datacollection.py +1 -6
- mesa/examples/__init__.py +2 -2
- mesa/examples/advanced/epstein_civil_violence/app.py +5 -0
- mesa/examples/advanced/pd_grid/agents.py +2 -1
- mesa/examples/advanced/pd_grid/app.py +5 -0
- mesa/examples/advanced/pd_grid/model.py +3 -5
- mesa/examples/advanced/sugarscape_g1mt/agents.py +12 -65
- mesa/examples/advanced/sugarscape_g1mt/app.py +24 -19
- mesa/examples/advanced/sugarscape_g1mt/model.py +45 -52
- mesa/examples/advanced/wolf_sheep/agents.py +3 -1
- mesa/examples/advanced/wolf_sheep/model.py +17 -16
- mesa/examples/basic/boid_flockers/app.py +5 -0
- mesa/examples/basic/boltzmann_wealth_model/app.py +8 -5
- mesa/examples/basic/boltzmann_wealth_model/st_app.py +1 -1
- mesa/examples/basic/conways_game_of_life/app.py +5 -0
- mesa/examples/basic/conways_game_of_life/st_app.py +2 -2
- mesa/examples/basic/schelling/agents.py +11 -5
- mesa/examples/basic/schelling/app.py +6 -1
- mesa/examples/basic/virus_on_network/app.py +5 -0
- mesa/experimental/__init__.py +17 -10
- mesa/experimental/cell_space/__init__.py +19 -7
- mesa/experimental/cell_space/cell.py +22 -37
- mesa/experimental/cell_space/cell_agent.py +12 -1
- mesa/experimental/cell_space/cell_collection.py +18 -3
- mesa/experimental/cell_space/discrete_space.py +15 -64
- mesa/experimental/cell_space/grid.py +74 -4
- mesa/experimental/cell_space/network.py +13 -1
- mesa/experimental/cell_space/property_layer.py +444 -0
- mesa/experimental/cell_space/voronoi.py +13 -1
- mesa/experimental/devs/__init__.py +20 -2
- mesa/experimental/devs/eventlist.py +19 -1
- mesa/experimental/devs/simulator.py +24 -8
- mesa/experimental/mesa_signals/__init__.py +23 -0
- mesa/experimental/mesa_signals/mesa_signal.py +485 -0
- mesa/experimental/mesa_signals/observable_collections.py +133 -0
- mesa/experimental/mesa_signals/signals_util.py +52 -0
- mesa/mesa_logging.py +190 -0
- mesa/model.py +17 -23
- mesa/visualization/__init__.py +2 -2
- mesa/visualization/mpl_space_drawing.py +8 -5
- mesa/visualization/solara_viz.py +49 -11
- {mesa-3.1.0.dev0.dist-info → mesa-3.1.1.dist-info}/METADATA +1 -1
- mesa-3.1.1.dist-info/RECORD +94 -0
- {mesa-3.1.0.dev0.dist-info → mesa-3.1.1.dist-info}/WHEEL +1 -1
- mesa/experimental/UserParam.py +0 -67
- mesa/experimental/components/altair.py +0 -81
- mesa/experimental/components/matplotlib.py +0 -242
- mesa/experimental/devs/examples/epstein_civil_violence.py +0 -305
- mesa/experimental/devs/examples/wolf_sheep.py +0 -250
- mesa/experimental/solara_viz.py +0 -453
- mesa-3.1.0.dev0.dist-info/RECORD +0 -94
- {mesa-3.1.0.dev0.dist-info → mesa-3.1.1.dist-info}/entry_points.txt +0 -0
- {mesa-3.1.0.dev0.dist-info → mesa-3.1.1.dist-info}/licenses/LICENSE +0 -0
- {mesa-3.1.0.dev0.dist-info → mesa-3.1.1.dist-info}/licenses/NOTICE +0 -0
mesa/experimental/UserParam.py
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
"""helper classes."""
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
class UserParam: # noqa: D101
|
|
5
|
-
_ERROR_MESSAGE = "Missing or malformed inputs for '{}' Option '{}'"
|
|
6
|
-
|
|
7
|
-
def maybe_raise_error(self, param_type, valid): # noqa: D102
|
|
8
|
-
if valid:
|
|
9
|
-
return
|
|
10
|
-
msg = self._ERROR_MESSAGE.format(param_type, self.label)
|
|
11
|
-
raise ValueError(msg)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class Slider(UserParam):
|
|
15
|
-
"""A number-based slider input with settable increment.
|
|
16
|
-
|
|
17
|
-
Example:
|
|
18
|
-
slider_option = Slider("My Slider", value=123, min=10, max=200, step=0.1)
|
|
19
|
-
|
|
20
|
-
Args:
|
|
21
|
-
label: The displayed label in the UI
|
|
22
|
-
value: The initial value of the slider
|
|
23
|
-
min: The minimum possible value of the slider
|
|
24
|
-
max: The maximum possible value of the slider
|
|
25
|
-
step: The step between min and max for a range of possible values
|
|
26
|
-
dtype: either int or float
|
|
27
|
-
"""
|
|
28
|
-
|
|
29
|
-
def __init__(
|
|
30
|
-
self,
|
|
31
|
-
label="",
|
|
32
|
-
value=None,
|
|
33
|
-
min=None,
|
|
34
|
-
max=None,
|
|
35
|
-
step=1,
|
|
36
|
-
dtype=None,
|
|
37
|
-
):
|
|
38
|
-
"""Slider class.
|
|
39
|
-
|
|
40
|
-
Args:
|
|
41
|
-
label: The displayed label in the UI
|
|
42
|
-
value: The initial value of the slider
|
|
43
|
-
min: The minimum possible value of the slider
|
|
44
|
-
max: The maximum possible value of the slider
|
|
45
|
-
step: The step between min and max for a range of possible values
|
|
46
|
-
dtype: either int or float
|
|
47
|
-
"""
|
|
48
|
-
self.label = label
|
|
49
|
-
self.value = value
|
|
50
|
-
self.min = min
|
|
51
|
-
self.max = max
|
|
52
|
-
self.step = step
|
|
53
|
-
|
|
54
|
-
# Validate option type to make sure values are supplied properly
|
|
55
|
-
valid = not (self.value is None or self.min is None or self.max is None)
|
|
56
|
-
self.maybe_raise_error("slider", valid)
|
|
57
|
-
|
|
58
|
-
if dtype is None:
|
|
59
|
-
self.is_float_slider = self._check_values_are_float(value, min, max, step)
|
|
60
|
-
else:
|
|
61
|
-
self.is_float_slider = dtype is float
|
|
62
|
-
|
|
63
|
-
def _check_values_are_float(self, value, min, max, step):
|
|
64
|
-
return any(isinstance(n, float) for n in (value, min, max, step))
|
|
65
|
-
|
|
66
|
-
def get(self, attr): # noqa: D102
|
|
67
|
-
return getattr(self, attr)
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
"""Altair components."""
|
|
2
|
-
|
|
3
|
-
import contextlib
|
|
4
|
-
|
|
5
|
-
import solara
|
|
6
|
-
|
|
7
|
-
with contextlib.suppress(ImportError):
|
|
8
|
-
import altair as alt
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@solara.component
|
|
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
|
-
"""
|
|
21
|
-
space = getattr(model, "grid", None)
|
|
22
|
-
if space is None:
|
|
23
|
-
# Sometimes the space is defined as model.space instead of model.grid
|
|
24
|
-
space = model.space
|
|
25
|
-
chart = _draw_grid(space, agent_portrayal)
|
|
26
|
-
solara.FigureAltair(chart)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def _draw_grid(space, agent_portrayal):
|
|
30
|
-
def portray(g):
|
|
31
|
-
all_agent_data = []
|
|
32
|
-
for content, (x, y) in g.coord_iter():
|
|
33
|
-
if not content:
|
|
34
|
-
continue
|
|
35
|
-
if not hasattr(content, "__iter__"):
|
|
36
|
-
# Is a single grid
|
|
37
|
-
content = [content] # noqa: PLW2901
|
|
38
|
-
for agent in content:
|
|
39
|
-
# use all data from agent portrayal, and add x,y coordinates
|
|
40
|
-
agent_data = agent_portrayal(agent)
|
|
41
|
-
agent_data["x"] = x
|
|
42
|
-
agent_data["y"] = y
|
|
43
|
-
all_agent_data.append(agent_data)
|
|
44
|
-
return all_agent_data
|
|
45
|
-
|
|
46
|
-
all_agent_data = portray(space)
|
|
47
|
-
invalid_tooltips = ["color", "size", "x", "y"]
|
|
48
|
-
|
|
49
|
-
encoding_dict = {
|
|
50
|
-
# no x-axis label
|
|
51
|
-
"x": alt.X("x", axis=None, type="ordinal"),
|
|
52
|
-
# no y-axis label
|
|
53
|
-
"y": alt.Y("y", axis=None, type="ordinal"),
|
|
54
|
-
"tooltip": [
|
|
55
|
-
alt.Tooltip(key, type=alt.utils.infer_vegalite_type([value]))
|
|
56
|
-
for key, value in all_agent_data[0].items()
|
|
57
|
-
if key not in invalid_tooltips
|
|
58
|
-
],
|
|
59
|
-
}
|
|
60
|
-
has_color = "color" in all_agent_data[0]
|
|
61
|
-
if has_color:
|
|
62
|
-
encoding_dict["color"] = alt.Color("color", type="nominal")
|
|
63
|
-
has_size = "size" in all_agent_data[0]
|
|
64
|
-
if has_size:
|
|
65
|
-
encoding_dict["size"] = alt.Size("size", type="quantitative")
|
|
66
|
-
|
|
67
|
-
chart = (
|
|
68
|
-
alt.Chart(
|
|
69
|
-
alt.Data(values=all_agent_data), encoding=alt.Encoding(**encoding_dict)
|
|
70
|
-
)
|
|
71
|
-
.mark_point(filled=True)
|
|
72
|
-
.properties(width=280, height=280)
|
|
73
|
-
# .configure_view(strokeOpacity=0) # hide grid/chart lines
|
|
74
|
-
)
|
|
75
|
-
# This is the default value for the marker size, which auto-scales
|
|
76
|
-
# according to the grid area.
|
|
77
|
-
if not has_size:
|
|
78
|
-
length = min(space.width, space.height)
|
|
79
|
-
chart = chart.mark_point(size=30000 / length**2, filled=True)
|
|
80
|
-
|
|
81
|
-
return chart
|
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
"""Support for using matplotlib to draw spaces."""
|
|
2
|
-
|
|
3
|
-
from collections import defaultdict
|
|
4
|
-
|
|
5
|
-
import networkx as nx
|
|
6
|
-
import solara
|
|
7
|
-
from matplotlib.figure import Figure
|
|
8
|
-
from matplotlib.ticker import MaxNLocator
|
|
9
|
-
|
|
10
|
-
import mesa
|
|
11
|
-
from mesa.experimental.cell_space import VoronoiGrid
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@solara.component
|
|
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
|
-
"""
|
|
24
|
-
space_fig = Figure()
|
|
25
|
-
space_ax = space_fig.subplots()
|
|
26
|
-
space = getattr(model, "grid", None)
|
|
27
|
-
if space is None:
|
|
28
|
-
# Sometimes the space is defined as model.space instead of model.grid
|
|
29
|
-
space = model.space
|
|
30
|
-
if isinstance(space, mesa.space.NetworkGrid):
|
|
31
|
-
_draw_network_grid(space, space_ax, agent_portrayal)
|
|
32
|
-
elif isinstance(space, mesa.space.ContinuousSpace):
|
|
33
|
-
_draw_continuous_space(space, space_ax, agent_portrayal)
|
|
34
|
-
elif isinstance(space, VoronoiGrid):
|
|
35
|
-
_draw_voronoi(space, space_ax, agent_portrayal)
|
|
36
|
-
else:
|
|
37
|
-
_draw_grid(space, space_ax, agent_portrayal)
|
|
38
|
-
solara.FigureMatplotlib(space_fig, format="png", dependencies=dependencies)
|
|
39
|
-
|
|
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
|
-
|
|
72
|
-
def _draw_grid(space, space_ax, agent_portrayal):
|
|
73
|
-
def portray(g):
|
|
74
|
-
x = []
|
|
75
|
-
y = []
|
|
76
|
-
s = [] # size
|
|
77
|
-
c = [] # color
|
|
78
|
-
m = [] # shape
|
|
79
|
-
for i in range(g.width):
|
|
80
|
-
for j in range(g.height):
|
|
81
|
-
content = g._grid[i][j]
|
|
82
|
-
if not content:
|
|
83
|
-
continue
|
|
84
|
-
if not hasattr(content, "__iter__"):
|
|
85
|
-
# Is a single grid
|
|
86
|
-
content = [content]
|
|
87
|
-
for agent in content:
|
|
88
|
-
data = agent_portrayal(agent)
|
|
89
|
-
x.append(i)
|
|
90
|
-
y.append(j)
|
|
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}
|
|
103
|
-
return out
|
|
104
|
-
|
|
105
|
-
space_ax.set_xlim(-1, space.width)
|
|
106
|
-
space_ax.set_ylim(-1, space.height)
|
|
107
|
-
_split_and_scatter(portray(space), space_ax)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def _draw_network_grid(space, space_ax, agent_portrayal):
|
|
111
|
-
graph = space.G
|
|
112
|
-
pos = nx.spring_layout(graph, seed=0)
|
|
113
|
-
nx.draw(
|
|
114
|
-
graph,
|
|
115
|
-
ax=space_ax,
|
|
116
|
-
pos=pos,
|
|
117
|
-
**agent_portrayal(graph),
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
def _draw_continuous_space(space, space_ax, agent_portrayal):
|
|
122
|
-
def portray(space):
|
|
123
|
-
x = []
|
|
124
|
-
y = []
|
|
125
|
-
s = [] # size
|
|
126
|
-
c = [] # color
|
|
127
|
-
m = [] # shape
|
|
128
|
-
for agent in space._agent_to_index:
|
|
129
|
-
data = agent_portrayal(agent)
|
|
130
|
-
_x, _y = agent.pos
|
|
131
|
-
x.append(_x)
|
|
132
|
-
y.append(_y)
|
|
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}
|
|
144
|
-
return out
|
|
145
|
-
|
|
146
|
-
# Determine border style based on space.torus
|
|
147
|
-
border_style = "solid" if not space.torus else (0, (5, 10))
|
|
148
|
-
|
|
149
|
-
# Set the border of the plot
|
|
150
|
-
for spine in space_ax.spines.values():
|
|
151
|
-
spine.set_linewidth(1.5)
|
|
152
|
-
spine.set_color("black")
|
|
153
|
-
spine.set_linestyle(border_style)
|
|
154
|
-
|
|
155
|
-
width = space.x_max - space.x_min
|
|
156
|
-
x_padding = width / 20
|
|
157
|
-
height = space.y_max - space.y_min
|
|
158
|
-
y_padding = height / 20
|
|
159
|
-
space_ax.set_xlim(space.x_min - x_padding, space.x_max + x_padding)
|
|
160
|
-
space_ax.set_ylim(space.y_min - y_padding, space.y_max + y_padding)
|
|
161
|
-
|
|
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)
|
|
204
|
-
space_ax.scatter(**portray(space))
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
@solara.component
|
|
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
|
-
"""
|
|
226
|
-
fig = Figure()
|
|
227
|
-
ax = fig.subplots()
|
|
228
|
-
df = model.datacollector.get_model_vars_dataframe()
|
|
229
|
-
if isinstance(measure, str):
|
|
230
|
-
ax.plot(df.loc[:, measure])
|
|
231
|
-
ax.set_ylabel(measure)
|
|
232
|
-
elif isinstance(measure, dict):
|
|
233
|
-
for m, color in measure.items():
|
|
234
|
-
ax.plot(df.loc[:, m], label=m, color=color)
|
|
235
|
-
fig.legend()
|
|
236
|
-
elif isinstance(measure, list | tuple):
|
|
237
|
-
for m in measure:
|
|
238
|
-
ax.plot(df.loc[:, m], label=m)
|
|
239
|
-
fig.legend()
|
|
240
|
-
# Set integer x axis
|
|
241
|
-
ax.xaxis.set_major_locator(MaxNLocator(integer=True))
|
|
242
|
-
solara.FigureMatplotlib(fig, dependencies=dependencies)
|
|
@@ -1,305 +0,0 @@
|
|
|
1
|
-
"""Epstein civil violence example using ABMSimulator."""
|
|
2
|
-
|
|
3
|
-
import enum
|
|
4
|
-
import math
|
|
5
|
-
|
|
6
|
-
from mesa import Agent, Model
|
|
7
|
-
from mesa.experimental.devs.simulator import ABMSimulator
|
|
8
|
-
from mesa.space import SingleGrid
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class EpsteinAgent(Agent):
|
|
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)
|
|
23
|
-
self.vision = vision
|
|
24
|
-
self.movement = movement
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
class AgentState(enum.IntEnum):
|
|
28
|
-
"""Agent states."""
|
|
29
|
-
|
|
30
|
-
QUIESCENT = enum.auto()
|
|
31
|
-
ARRESTED = enum.auto()
|
|
32
|
-
ACTIVE = enum.auto()
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class Citizen(EpsteinAgent):
|
|
36
|
-
"""A member of the general population, may or may not be in active rebellion.
|
|
37
|
-
|
|
38
|
-
Summary of rule: If grievance - risk > threshold, rebel.
|
|
39
|
-
|
|
40
|
-
Attributes:
|
|
41
|
-
unique_id: unique int
|
|
42
|
-
model :
|
|
43
|
-
hardship: Agent's 'perceived hardship (i.e., physical or economic
|
|
44
|
-
privation).' Exogenous, drawn from U(0,1).
|
|
45
|
-
regime_legitimacy: Agent's perception of regime legitimacy, equal
|
|
46
|
-
across agents. Exogenous.
|
|
47
|
-
risk_aversion: Exogenous, drawn from U(0,1).
|
|
48
|
-
threshold: if (grievance - (risk_aversion * arrest_probability)) >
|
|
49
|
-
threshold, go/remain Active
|
|
50
|
-
vision: number of cells in each direction (N, S, E and W) that agent
|
|
51
|
-
can inspect
|
|
52
|
-
condition: Can be "Quiescent" or "Active;" deterministic function of
|
|
53
|
-
greivance, perceived risk, and
|
|
54
|
-
grievance: deterministic function of hardship and regime_legitimacy;
|
|
55
|
-
how aggrieved is agent at the regime?
|
|
56
|
-
arrest_probability: agent's assessment of arrest probability, given
|
|
57
|
-
rebellion
|
|
58
|
-
"""
|
|
59
|
-
|
|
60
|
-
def __init__(
|
|
61
|
-
self,
|
|
62
|
-
model,
|
|
63
|
-
vision,
|
|
64
|
-
movement,
|
|
65
|
-
hardship,
|
|
66
|
-
regime_legitimacy,
|
|
67
|
-
risk_aversion,
|
|
68
|
-
threshold,
|
|
69
|
-
arrest_prob_constant,
|
|
70
|
-
):
|
|
71
|
-
"""Create a new Citizen.
|
|
72
|
-
|
|
73
|
-
Args:
|
|
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
|
|
78
|
-
hardship: Agent's 'perceived hardship (i.e., physical or economic
|
|
79
|
-
privation).' Exogenous, drawn from U(0,1).
|
|
80
|
-
regime_legitimacy: Agent's perception of regime legitimacy, equal
|
|
81
|
-
across agents. Exogenous.
|
|
82
|
-
risk_aversion: Exogenous, drawn from U(0,1).
|
|
83
|
-
threshold: if (grievance - (risk_aversion * arrest_probability)) >
|
|
84
|
-
threshold, go/remain Active
|
|
85
|
-
arrest_prob_constant : agent's assessment of arrest probability
|
|
86
|
-
|
|
87
|
-
"""
|
|
88
|
-
super().__init__(model, vision, movement)
|
|
89
|
-
self.hardship = hardship
|
|
90
|
-
self.regime_legitimacy = regime_legitimacy
|
|
91
|
-
self.risk_aversion = risk_aversion
|
|
92
|
-
self.threshold = threshold
|
|
93
|
-
self.condition = AgentState.QUIESCENT
|
|
94
|
-
self.grievance = self.hardship * (1 - self.regime_legitimacy)
|
|
95
|
-
self.arrest_probability = None
|
|
96
|
-
self.arrest_prob_constant = arrest_prob_constant
|
|
97
|
-
|
|
98
|
-
def step(self):
|
|
99
|
-
"""Decide whether to activate, then move if applicable."""
|
|
100
|
-
self.update_neighbors()
|
|
101
|
-
self.update_estimated_arrest_probability()
|
|
102
|
-
net_risk = self.risk_aversion * self.arrest_probability
|
|
103
|
-
if self.grievance - net_risk > self.threshold:
|
|
104
|
-
self.condition = AgentState.ACTIVE
|
|
105
|
-
else:
|
|
106
|
-
self.condition = AgentState.QUIESCENT
|
|
107
|
-
if self.movement and self.empty_neighbors:
|
|
108
|
-
new_pos = self.random.choice(self.empty_neighbors)
|
|
109
|
-
self.model.grid.move_agent(self, new_pos)
|
|
110
|
-
|
|
111
|
-
def update_neighbors(self):
|
|
112
|
-
"""Look around and see who my neighbors are."""
|
|
113
|
-
self.neighborhood = self.model.grid.get_neighborhood(
|
|
114
|
-
self.pos, moore=True, radius=self.vision
|
|
115
|
-
)
|
|
116
|
-
self.neighbors = self.model.grid.get_cell_list_contents(self.neighborhood)
|
|
117
|
-
self.empty_neighbors = [
|
|
118
|
-
c for c in self.neighborhood if self.model.grid.is_cell_empty(c)
|
|
119
|
-
]
|
|
120
|
-
|
|
121
|
-
def update_estimated_arrest_probability(self):
|
|
122
|
-
"""Based on the ratio of cops to actives in my neighborhood, estimate the p(Arrest | I go active)."""
|
|
123
|
-
cops_in_vision = len([c for c in self.neighbors if isinstance(c, Cop)])
|
|
124
|
-
actives_in_vision = 1.0 # citizen counts herself
|
|
125
|
-
for c in self.neighbors:
|
|
126
|
-
if isinstance(c, Citizen) and c.condition == AgentState.ACTIVE:
|
|
127
|
-
actives_in_vision += 1
|
|
128
|
-
self.arrest_probability = 1 - math.exp(
|
|
129
|
-
-1 * self.arrest_prob_constant * (cops_in_vision / actives_in_vision)
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
def sent_to_jail(self, value):
|
|
133
|
-
"""Sent agent to jail.
|
|
134
|
-
|
|
135
|
-
Args:
|
|
136
|
-
value: duration of jail sentence
|
|
137
|
-
|
|
138
|
-
"""
|
|
139
|
-
self.model.active_agents.remove(self)
|
|
140
|
-
self.condition = AgentState.ARRESTED
|
|
141
|
-
self.model.simulator.schedule_event_relative(self.release_from_jail, value)
|
|
142
|
-
|
|
143
|
-
def release_from_jail(self):
|
|
144
|
-
"""Release agent from jail."""
|
|
145
|
-
self.model.active_agents.add(self)
|
|
146
|
-
self.condition = AgentState.QUIESCENT
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
class Cop(EpsteinAgent):
|
|
150
|
-
"""A cop for life. No defection.
|
|
151
|
-
|
|
152
|
-
Summary of rule: Inspect local vision and arrest a random active agent.
|
|
153
|
-
|
|
154
|
-
Attributes:
|
|
155
|
-
unique_id: unique int
|
|
156
|
-
x, y: Grid coordinates
|
|
157
|
-
vision: number of cells in each direction (N, S, E and W) that cop is
|
|
158
|
-
able to inspect
|
|
159
|
-
"""
|
|
160
|
-
|
|
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)
|
|
171
|
-
self.max_jail_term = max_jail_term
|
|
172
|
-
|
|
173
|
-
def step(self):
|
|
174
|
-
"""Inspect local vision and arrest a random active agent. Move if applicable."""
|
|
175
|
-
self.update_neighbors()
|
|
176
|
-
active_neighbors = []
|
|
177
|
-
for agent in self.neighbors:
|
|
178
|
-
if isinstance(agent, Citizen) and agent.condition == "Active":
|
|
179
|
-
active_neighbors.append(agent)
|
|
180
|
-
if active_neighbors:
|
|
181
|
-
arrestee = self.random.choice(active_neighbors)
|
|
182
|
-
arrestee.sent_to_jail(self.random.randint(0, self.max_jail_term))
|
|
183
|
-
if self.movement and self.empty_neighbors:
|
|
184
|
-
new_pos = self.random.choice(self.empty_neighbors)
|
|
185
|
-
self.model.grid.move_agent(self, new_pos)
|
|
186
|
-
|
|
187
|
-
def update_neighbors(self):
|
|
188
|
-
"""Look around and see who my neighbors are."""
|
|
189
|
-
self.neighborhood = self.model.grid.get_neighborhood(
|
|
190
|
-
self.pos, moore=True, radius=self.vision
|
|
191
|
-
)
|
|
192
|
-
self.neighbors = self.model.grid.get_cell_list_contents(self.neighborhood)
|
|
193
|
-
self.empty_neighbors = [
|
|
194
|
-
c for c in self.neighborhood if self.model.grid.is_cell_empty(c)
|
|
195
|
-
]
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
class EpsteinCivilViolence(Model):
|
|
199
|
-
"""Model 1 from "Modeling civil violence: An agent-based computational approach," by Joshua Epstein.
|
|
200
|
-
|
|
201
|
-
http://www.pnas.org/content/99/suppl_3/7243.full
|
|
202
|
-
Attributes:
|
|
203
|
-
height: grid height
|
|
204
|
-
width: grid width
|
|
205
|
-
citizen_density: approximate % of cells occupied by citizens.
|
|
206
|
-
cop_density: approximate % of cells occupied by cops.
|
|
207
|
-
citizen_vision: number of cells in each direction (N, S, E and W) that
|
|
208
|
-
citizen can inspect
|
|
209
|
-
cop_vision: number of cells in each direction (N, S, E and W) that cop
|
|
210
|
-
can inspect
|
|
211
|
-
legitimacy: (L) citizens' perception of regime legitimacy, equal
|
|
212
|
-
across all citizens
|
|
213
|
-
max_jail_term: (J_max)
|
|
214
|
-
active_threshold: if (grievance - (risk_aversion * arrest_probability))
|
|
215
|
-
> threshold, citizen rebels
|
|
216
|
-
arrest_prob_constant: set to ensure agents make plausible arrest
|
|
217
|
-
probability estimates
|
|
218
|
-
movement: binary, whether agents try to move at step end
|
|
219
|
-
max_iters: model may not have a natural stopping point, so we set a
|
|
220
|
-
max.
|
|
221
|
-
"""
|
|
222
|
-
|
|
223
|
-
def __init__(
|
|
224
|
-
self,
|
|
225
|
-
width=40,
|
|
226
|
-
height=40,
|
|
227
|
-
citizen_density=0.7,
|
|
228
|
-
cop_density=0.074,
|
|
229
|
-
citizen_vision=7,
|
|
230
|
-
cop_vision=7,
|
|
231
|
-
legitimacy=0.8,
|
|
232
|
-
max_jail_term=1000,
|
|
233
|
-
active_threshold=0.1,
|
|
234
|
-
arrest_prob_constant=2.3,
|
|
235
|
-
movement=True,
|
|
236
|
-
max_iters=1000,
|
|
237
|
-
seed=None,
|
|
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
|
-
"""
|
|
256
|
-
super().__init__(seed)
|
|
257
|
-
if cop_density + citizen_density > 1:
|
|
258
|
-
raise ValueError("Cop density + citizen density must be less than 1")
|
|
259
|
-
|
|
260
|
-
self.width = width
|
|
261
|
-
self.height = height
|
|
262
|
-
self.citizen_density = citizen_density
|
|
263
|
-
self.cop_density = cop_density
|
|
264
|
-
|
|
265
|
-
self.max_iters = max_iters
|
|
266
|
-
|
|
267
|
-
self.grid = SingleGrid(self.width, self.height, torus=True)
|
|
268
|
-
|
|
269
|
-
for _, pos in self.grid.coord_iter():
|
|
270
|
-
if self.random.random() < self.cop_density:
|
|
271
|
-
agent = Cop(
|
|
272
|
-
self,
|
|
273
|
-
cop_vision,
|
|
274
|
-
movement,
|
|
275
|
-
max_jail_term,
|
|
276
|
-
)
|
|
277
|
-
elif self.random.random() < (self.cop_density + self.citizen_density):
|
|
278
|
-
agent = Citizen(
|
|
279
|
-
self,
|
|
280
|
-
citizen_vision,
|
|
281
|
-
movement,
|
|
282
|
-
hardship=self.random.random(),
|
|
283
|
-
regime_legitimacy=legitimacy,
|
|
284
|
-
risk_aversion=self.random.random(),
|
|
285
|
-
threshold=active_threshold,
|
|
286
|
-
arrest_prob_constant=arrest_prob_constant,
|
|
287
|
-
)
|
|
288
|
-
else:
|
|
289
|
-
continue
|
|
290
|
-
self.grid.place_agent(agent, pos)
|
|
291
|
-
|
|
292
|
-
self.active_agents = self.agents
|
|
293
|
-
|
|
294
|
-
def step(self):
|
|
295
|
-
"""Run one step of the model."""
|
|
296
|
-
self.active_agents.shuffle_do("step")
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
if __name__ == "__main__":
|
|
300
|
-
model = EpsteinCivilViolence(seed=15)
|
|
301
|
-
simulator = ABMSimulator()
|
|
302
|
-
|
|
303
|
-
simulator.setup(model)
|
|
304
|
-
|
|
305
|
-
simulator.run_for(time_delta=100)
|