Mesa 3.0.0a2__py3-none-any.whl → 3.0.0a4__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 +1 -1
- mesa/agent.py +293 -43
- mesa/batchrunner.py +7 -4
- mesa/datacollection.py +2 -2
- mesa/experimental/UserParam.py +56 -0
- mesa/experimental/__init__.py +3 -1
- mesa/experimental/cell_space/__init__.py +2 -0
- mesa/experimental/cell_space/cell_agent.py +2 -2
- mesa/experimental/cell_space/voronoi.py +264 -0
- mesa/experimental/components/altair.py +71 -0
- mesa/experimental/components/matplotlib.py +224 -0
- mesa/experimental/devs/examples/epstein_civil_violence.py +6 -10
- mesa/experimental/devs/examples/wolf_sheep.py +7 -12
- mesa/experimental/solara_viz.py +462 -0
- mesa/model.py +108 -38
- mesa/space.py +9 -3
- mesa/time.py +0 -7
- mesa/visualization/__init__.py +13 -2
- mesa/visualization/components/altair.py +15 -0
- mesa/visualization/components/matplotlib.py +75 -0
- mesa/visualization/solara_viz.py +121 -185
- mesa/visualization/utils.py +7 -0
- {mesa-3.0.0a2.dist-info → mesa-3.0.0a4.dist-info}/METADATA +8 -1
- mesa-3.0.0a4.dist-info/RECORD +44 -0
- mesa-3.0.0a2.dist-info/RECORD +0 -38
- {mesa-3.0.0a2.dist-info → mesa-3.0.0a4.dist-info}/WHEEL +0 -0
- {mesa-3.0.0a2.dist-info → mesa-3.0.0a4.dist-info}/entry_points.txt +0 -0
- {mesa-3.0.0a2.dist-info → mesa-3.0.0a4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from itertools import combinations
|
|
3
|
+
from random import Random
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from mesa.experimental.cell_space.cell import Cell
|
|
8
|
+
from mesa.experimental.cell_space.discrete_space import DiscreteSpace
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Delaunay:
|
|
12
|
+
"""
|
|
13
|
+
Class to compute a Delaunay triangulation in 2D
|
|
14
|
+
ref: http://github.com/jmespadero/pyDelaunay2D
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, center: tuple = (0, 0), radius: int = 9999) -> None:
|
|
18
|
+
"""
|
|
19
|
+
Init and create a new frame to contain the triangulation
|
|
20
|
+
center: Optional position for the center of the frame. Default (0,0)
|
|
21
|
+
radius: Optional distance from corners to the center.
|
|
22
|
+
"""
|
|
23
|
+
center = np.asarray(center)
|
|
24
|
+
# Create coordinates for the corners of the frame
|
|
25
|
+
self.coords = [
|
|
26
|
+
center + radius * np.array((-1, -1)),
|
|
27
|
+
center + radius * np.array((+1, -1)),
|
|
28
|
+
center + radius * np.array((+1, +1)),
|
|
29
|
+
center + radius * np.array((-1, +1)),
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
# Create two dicts to store triangle neighbours and circumcircles.
|
|
33
|
+
self.triangles = {}
|
|
34
|
+
self.circles = {}
|
|
35
|
+
|
|
36
|
+
# Create two CCW triangles for the frame
|
|
37
|
+
triangle1 = (0, 1, 3)
|
|
38
|
+
triangle2 = (2, 3, 1)
|
|
39
|
+
self.triangles[triangle1] = [triangle2, None, None]
|
|
40
|
+
self.triangles[triangle2] = [triangle1, None, None]
|
|
41
|
+
|
|
42
|
+
# Compute circumcenters and circumradius for each triangle
|
|
43
|
+
for t in self.triangles:
|
|
44
|
+
self.circles[t] = self._circumcenter(t)
|
|
45
|
+
|
|
46
|
+
def _circumcenter(self, triangle: list) -> tuple:
|
|
47
|
+
"""
|
|
48
|
+
Compute circumcenter and circumradius of a triangle in 2D.
|
|
49
|
+
"""
|
|
50
|
+
points = np.asarray([self.coords[v] for v in triangle])
|
|
51
|
+
points2 = np.dot(points, points.T)
|
|
52
|
+
a = np.bmat([[2 * points2, [[1], [1], [1]]], [[[1, 1, 1, 0]]]])
|
|
53
|
+
|
|
54
|
+
b = np.hstack((np.sum(points * points, axis=1), [1]))
|
|
55
|
+
x = np.linalg.solve(a, b)
|
|
56
|
+
bary_coords = x[:-1]
|
|
57
|
+
center = np.dot(bary_coords, points)
|
|
58
|
+
|
|
59
|
+
radius = np.sum(np.square(points[0] - center)) # squared distance
|
|
60
|
+
return (center, radius)
|
|
61
|
+
|
|
62
|
+
def _in_circle(self, triangle: list, point: list) -> bool:
|
|
63
|
+
"""
|
|
64
|
+
Check if point p is inside of precomputed circumcircle of triangle.
|
|
65
|
+
"""
|
|
66
|
+
center, radius = self.circles[triangle]
|
|
67
|
+
return np.sum(np.square(center - point)) <= radius
|
|
68
|
+
|
|
69
|
+
def add_point(self, point: Sequence) -> None:
|
|
70
|
+
"""
|
|
71
|
+
Add a point to the current DT, and refine it using Bowyer-Watson.
|
|
72
|
+
"""
|
|
73
|
+
point_index = len(self.coords)
|
|
74
|
+
self.coords.append(np.asarray(point))
|
|
75
|
+
|
|
76
|
+
bad_triangles = []
|
|
77
|
+
for triangle in self.triangles:
|
|
78
|
+
if self._in_circle(triangle, point):
|
|
79
|
+
bad_triangles.append(triangle)
|
|
80
|
+
|
|
81
|
+
boundary = []
|
|
82
|
+
triangle = bad_triangles[0]
|
|
83
|
+
edge = 0
|
|
84
|
+
|
|
85
|
+
while True:
|
|
86
|
+
opposite_triangle = self.triangles[triangle][edge]
|
|
87
|
+
if opposite_triangle not in bad_triangles:
|
|
88
|
+
boundary.append(
|
|
89
|
+
(
|
|
90
|
+
triangle[(edge + 1) % 3],
|
|
91
|
+
triangle[(edge - 1) % 3],
|
|
92
|
+
opposite_triangle,
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
edge = (edge + 1) % 3
|
|
96
|
+
if boundary[0][0] == boundary[-1][1]:
|
|
97
|
+
break
|
|
98
|
+
else:
|
|
99
|
+
edge = (self.triangles[opposite_triangle].index(triangle) + 1) % 3
|
|
100
|
+
triangle = opposite_triangle
|
|
101
|
+
|
|
102
|
+
for triangle in bad_triangles:
|
|
103
|
+
del self.triangles[triangle]
|
|
104
|
+
del self.circles[triangle]
|
|
105
|
+
|
|
106
|
+
new_triangles = []
|
|
107
|
+
for e0, e1, opposite_triangle in boundary:
|
|
108
|
+
triangle = (point_index, e0, e1)
|
|
109
|
+
self.circles[triangle] = self._circumcenter(triangle)
|
|
110
|
+
self.triangles[triangle] = [opposite_triangle, None, None]
|
|
111
|
+
if opposite_triangle:
|
|
112
|
+
for i, neighbor in enumerate(self.triangles[opposite_triangle]):
|
|
113
|
+
if neighbor and e1 in neighbor and e0 in neighbor:
|
|
114
|
+
self.triangles[opposite_triangle][i] = triangle
|
|
115
|
+
|
|
116
|
+
new_triangles.append(triangle)
|
|
117
|
+
|
|
118
|
+
n = len(new_triangles)
|
|
119
|
+
for i, triangle in enumerate(new_triangles):
|
|
120
|
+
self.triangles[triangle][1] = new_triangles[(i + 1) % n] # next
|
|
121
|
+
self.triangles[triangle][2] = new_triangles[(i - 1) % n] # previous
|
|
122
|
+
|
|
123
|
+
def export_triangles(self) -> list:
|
|
124
|
+
"""
|
|
125
|
+
Export the current list of Delaunay triangles
|
|
126
|
+
"""
|
|
127
|
+
triangles_list = [
|
|
128
|
+
(a - 4, b - 4, c - 4)
|
|
129
|
+
for (a, b, c) in self.triangles
|
|
130
|
+
if a > 3 and b > 3 and c > 3
|
|
131
|
+
]
|
|
132
|
+
return triangles_list
|
|
133
|
+
|
|
134
|
+
def export_voronoi_regions(self):
|
|
135
|
+
"""
|
|
136
|
+
Export coordinates and regions of Voronoi diagram as indexed data.
|
|
137
|
+
"""
|
|
138
|
+
use_vertex = {i: [] for i in range(len(self.coords))}
|
|
139
|
+
vor_coors = []
|
|
140
|
+
index = {}
|
|
141
|
+
for triangle_index, (a, b, c) in enumerate(sorted(self.triangles)):
|
|
142
|
+
vor_coors.append(self.circles[(a, b, c)][0])
|
|
143
|
+
use_vertex[a] += [(b, c, a)]
|
|
144
|
+
use_vertex[b] += [(c, a, b)]
|
|
145
|
+
use_vertex[c] += [(a, b, c)]
|
|
146
|
+
|
|
147
|
+
index[(a, b, c)] = triangle_index
|
|
148
|
+
index[(c, a, b)] = triangle_index
|
|
149
|
+
index[(b, c, a)] = triangle_index
|
|
150
|
+
|
|
151
|
+
regions = {}
|
|
152
|
+
for i in range(4, len(self.coords)):
|
|
153
|
+
vertex = use_vertex[i][0][0]
|
|
154
|
+
region = []
|
|
155
|
+
for _ in range(len(use_vertex[i])):
|
|
156
|
+
triangle = next(
|
|
157
|
+
triangle for triangle in use_vertex[i] if triangle[0] == vertex
|
|
158
|
+
)
|
|
159
|
+
region.append(index[triangle])
|
|
160
|
+
vertex = triangle[1]
|
|
161
|
+
regions[i - 4] = region
|
|
162
|
+
|
|
163
|
+
return vor_coors, regions
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def round_float(x: float) -> int:
|
|
167
|
+
return int(x * 500)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class VoronoiGrid(DiscreteSpace):
|
|
171
|
+
triangulation: Delaunay
|
|
172
|
+
voronoi_coordinates: list
|
|
173
|
+
regions: list
|
|
174
|
+
|
|
175
|
+
def __init__(
|
|
176
|
+
self,
|
|
177
|
+
centroids_coordinates: Sequence[Sequence[float]],
|
|
178
|
+
capacity: float | None = None,
|
|
179
|
+
random: Random | None = None,
|
|
180
|
+
cell_klass: type[Cell] = Cell,
|
|
181
|
+
capacity_function: callable = round_float,
|
|
182
|
+
cell_coloring_property: str | None = None,
|
|
183
|
+
) -> None:
|
|
184
|
+
"""
|
|
185
|
+
A Voronoi Tessellation Grid.
|
|
186
|
+
|
|
187
|
+
Given a set of points, this class creates a grid where a cell is centered in each point,
|
|
188
|
+
its neighbors are given by Voronoi Tessellation cells neighbors
|
|
189
|
+
and the capacity by the polygon area.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
centroids_coordinates: coordinates of centroids to build the tessellation space
|
|
193
|
+
capacity (int) : capacity of the cells in the discrete space
|
|
194
|
+
random (Random): random number generator
|
|
195
|
+
CellKlass (type[Cell]): type of cell class
|
|
196
|
+
capacity_function (Callable): function to compute (int) capacity according to (float) area
|
|
197
|
+
cell_coloring_property (str): voronoi visualization polygon fill property
|
|
198
|
+
"""
|
|
199
|
+
super().__init__(capacity=capacity, random=random, cell_klass=cell_klass)
|
|
200
|
+
self.centroids_coordinates = centroids_coordinates
|
|
201
|
+
self._validate_parameters()
|
|
202
|
+
|
|
203
|
+
self._cells = {
|
|
204
|
+
i: cell_klass(self.centroids_coordinates[i], capacity, random=self.random)
|
|
205
|
+
for i in range(len(self.centroids_coordinates))
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
self.regions = None
|
|
209
|
+
self.triangulation = None
|
|
210
|
+
self.voronoi_coordinates = None
|
|
211
|
+
self.capacity_function = capacity_function
|
|
212
|
+
self.cell_coloring_property = cell_coloring_property
|
|
213
|
+
|
|
214
|
+
self._connect_cells()
|
|
215
|
+
self._build_cell_polygons()
|
|
216
|
+
|
|
217
|
+
def _connect_cells(self) -> None:
|
|
218
|
+
"""
|
|
219
|
+
Connect cells to neighbors based on given centroids and using Delaunay Triangulation
|
|
220
|
+
"""
|
|
221
|
+
self.triangulation = Delaunay()
|
|
222
|
+
for centroid in self.centroids_coordinates:
|
|
223
|
+
self.triangulation.add_point(centroid)
|
|
224
|
+
|
|
225
|
+
for point in self.triangulation.export_triangles():
|
|
226
|
+
for i, j in combinations(point, 2):
|
|
227
|
+
self._cells[i].connect(self._cells[j])
|
|
228
|
+
self._cells[j].connect(self._cells[i])
|
|
229
|
+
|
|
230
|
+
def _validate_parameters(self) -> None:
|
|
231
|
+
if self.capacity is not None and not isinstance(self.capacity, float | int):
|
|
232
|
+
raise ValueError("Capacity must be a number or None.")
|
|
233
|
+
if not isinstance(self.centroids_coordinates, Sequence) or not isinstance(
|
|
234
|
+
self.centroids_coordinates[0], Sequence
|
|
235
|
+
):
|
|
236
|
+
raise ValueError("Centroids should be a list of lists")
|
|
237
|
+
dimension_1 = len(self.centroids_coordinates[0])
|
|
238
|
+
for coordinate in self.centroids_coordinates:
|
|
239
|
+
if dimension_1 != len(coordinate):
|
|
240
|
+
raise ValueError("Centroid coordinates should be a homogeneous array")
|
|
241
|
+
|
|
242
|
+
def _get_voronoi_regions(self) -> tuple:
|
|
243
|
+
if self.voronoi_coordinates is None or self.regions is None:
|
|
244
|
+
self.voronoi_coordinates, self.regions = (
|
|
245
|
+
self.triangulation.export_voronoi_regions()
|
|
246
|
+
)
|
|
247
|
+
return self.voronoi_coordinates, self.regions
|
|
248
|
+
|
|
249
|
+
@staticmethod
|
|
250
|
+
def _compute_polygon_area(polygon: list) -> float:
|
|
251
|
+
polygon = np.array(polygon)
|
|
252
|
+
x = polygon[:, 0]
|
|
253
|
+
y = polygon[:, 1]
|
|
254
|
+
return 0.5 * np.abs(np.dot(x, np.roll(y, 1)) - np.dot(y, np.roll(x, 1)))
|
|
255
|
+
|
|
256
|
+
def _build_cell_polygons(self):
|
|
257
|
+
coordinates, regions = self._get_voronoi_regions()
|
|
258
|
+
for region in regions:
|
|
259
|
+
polygon = [coordinates[i] for i in regions[region]]
|
|
260
|
+
self._cells[region].properties["polygon"] = polygon
|
|
261
|
+
polygon_area = self._compute_polygon_area(polygon)
|
|
262
|
+
self._cells[region].properties["area"] = polygon_area
|
|
263
|
+
self._cells[region].capacity = self.capacity_function(polygon_area)
|
|
264
|
+
self._cells[region].properties[self.cell_coloring_property] = 0
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
|
|
3
|
+
import solara
|
|
4
|
+
|
|
5
|
+
with contextlib.suppress(ImportError):
|
|
6
|
+
import altair as alt
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@solara.component
|
|
10
|
+
def SpaceAltair(model, agent_portrayal, dependencies: list[any] | None = None):
|
|
11
|
+
space = getattr(model, "grid", None)
|
|
12
|
+
if space is None:
|
|
13
|
+
# Sometimes the space is defined as model.space instead of model.grid
|
|
14
|
+
space = model.space
|
|
15
|
+
chart = _draw_grid(space, agent_portrayal)
|
|
16
|
+
solara.FigureAltair(chart)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _draw_grid(space, agent_portrayal):
|
|
20
|
+
def portray(g):
|
|
21
|
+
all_agent_data = []
|
|
22
|
+
for content, (x, y) in g.coord_iter():
|
|
23
|
+
if not content:
|
|
24
|
+
continue
|
|
25
|
+
if not hasattr(content, "__iter__"):
|
|
26
|
+
# Is a single grid
|
|
27
|
+
content = [content] # noqa: PLW2901
|
|
28
|
+
for agent in content:
|
|
29
|
+
# use all data from agent portrayal, and add x,y coordinates
|
|
30
|
+
agent_data = agent_portrayal(agent)
|
|
31
|
+
agent_data["x"] = x
|
|
32
|
+
agent_data["y"] = y
|
|
33
|
+
all_agent_data.append(agent_data)
|
|
34
|
+
return all_agent_data
|
|
35
|
+
|
|
36
|
+
all_agent_data = portray(space)
|
|
37
|
+
invalid_tooltips = ["color", "size", "x", "y"]
|
|
38
|
+
|
|
39
|
+
encoding_dict = {
|
|
40
|
+
# no x-axis label
|
|
41
|
+
"x": alt.X("x", axis=None, type="ordinal"),
|
|
42
|
+
# no y-axis label
|
|
43
|
+
"y": alt.Y("y", axis=None, type="ordinal"),
|
|
44
|
+
"tooltip": [
|
|
45
|
+
alt.Tooltip(key, type=alt.utils.infer_vegalite_type([value]))
|
|
46
|
+
for key, value in all_agent_data[0].items()
|
|
47
|
+
if key not in invalid_tooltips
|
|
48
|
+
],
|
|
49
|
+
}
|
|
50
|
+
has_color = "color" in all_agent_data[0]
|
|
51
|
+
if has_color:
|
|
52
|
+
encoding_dict["color"] = alt.Color("color", type="nominal")
|
|
53
|
+
has_size = "size" in all_agent_data[0]
|
|
54
|
+
if has_size:
|
|
55
|
+
encoding_dict["size"] = alt.Size("size", type="quantitative")
|
|
56
|
+
|
|
57
|
+
chart = (
|
|
58
|
+
alt.Chart(
|
|
59
|
+
alt.Data(values=all_agent_data), encoding=alt.Encoding(**encoding_dict)
|
|
60
|
+
)
|
|
61
|
+
.mark_point(filled=True)
|
|
62
|
+
.properties(width=280, height=280)
|
|
63
|
+
# .configure_view(strokeOpacity=0) # hide grid/chart lines
|
|
64
|
+
)
|
|
65
|
+
# This is the default value for the marker size, which auto-scales
|
|
66
|
+
# according to the grid area.
|
|
67
|
+
if not has_size:
|
|
68
|
+
length = min(space.width, space.height)
|
|
69
|
+
chart = chart.mark_point(size=30000 / length**2, filled=True)
|
|
70
|
+
|
|
71
|
+
return chart
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
|
|
3
|
+
import networkx as nx
|
|
4
|
+
import solara
|
|
5
|
+
from matplotlib.figure import Figure
|
|
6
|
+
from matplotlib.ticker import MaxNLocator
|
|
7
|
+
|
|
8
|
+
import mesa
|
|
9
|
+
from mesa.experimental.cell_space import VoronoiGrid
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@solara.component
|
|
13
|
+
def SpaceMatplotlib(model, agent_portrayal, dependencies: list[any] | None = None):
|
|
14
|
+
space_fig = Figure()
|
|
15
|
+
space_ax = space_fig.subplots()
|
|
16
|
+
space = getattr(model, "grid", None)
|
|
17
|
+
if space is None:
|
|
18
|
+
# Sometimes the space is defined as model.space instead of model.grid
|
|
19
|
+
space = model.space
|
|
20
|
+
if isinstance(space, mesa.space.NetworkGrid):
|
|
21
|
+
_draw_network_grid(space, space_ax, agent_portrayal)
|
|
22
|
+
elif isinstance(space, mesa.space.ContinuousSpace):
|
|
23
|
+
_draw_continuous_space(space, space_ax, agent_portrayal)
|
|
24
|
+
elif isinstance(space, VoronoiGrid):
|
|
25
|
+
_draw_voronoi(space, space_ax, agent_portrayal)
|
|
26
|
+
else:
|
|
27
|
+
_draw_grid(space, space_ax, agent_portrayal)
|
|
28
|
+
solara.FigureMatplotlib(space_fig, format="png", dependencies=dependencies)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# matplotlib scatter does not allow for multiple shapes in one call
|
|
32
|
+
def _split_and_scatter(portray_data, space_ax):
|
|
33
|
+
grouped_data = defaultdict(lambda: {"x": [], "y": [], "s": [], "c": []})
|
|
34
|
+
|
|
35
|
+
# Extract data from the dictionary
|
|
36
|
+
x = portray_data["x"]
|
|
37
|
+
y = portray_data["y"]
|
|
38
|
+
s = portray_data["s"]
|
|
39
|
+
c = portray_data["c"]
|
|
40
|
+
m = portray_data["m"]
|
|
41
|
+
|
|
42
|
+
if not (len(x) == len(y) == len(s) == len(c) == len(m)):
|
|
43
|
+
raise ValueError(
|
|
44
|
+
"Length mismatch in portrayal data lists: "
|
|
45
|
+
f"x: {len(x)}, y: {len(y)}, size: {len(s)}, "
|
|
46
|
+
f"color: {len(c)}, marker: {len(m)}"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Group the data by marker
|
|
50
|
+
for i in range(len(x)):
|
|
51
|
+
marker = m[i]
|
|
52
|
+
grouped_data[marker]["x"].append(x[i])
|
|
53
|
+
grouped_data[marker]["y"].append(y[i])
|
|
54
|
+
grouped_data[marker]["s"].append(s[i])
|
|
55
|
+
grouped_data[marker]["c"].append(c[i])
|
|
56
|
+
|
|
57
|
+
# Plot each group with the same marker
|
|
58
|
+
for marker, data in grouped_data.items():
|
|
59
|
+
space_ax.scatter(data["x"], data["y"], s=data["s"], c=data["c"], marker=marker)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _draw_grid(space, space_ax, agent_portrayal):
|
|
63
|
+
def portray(g):
|
|
64
|
+
x = []
|
|
65
|
+
y = []
|
|
66
|
+
s = [] # size
|
|
67
|
+
c = [] # color
|
|
68
|
+
m = [] # shape
|
|
69
|
+
for i in range(g.width):
|
|
70
|
+
for j in range(g.height):
|
|
71
|
+
content = g._grid[i][j]
|
|
72
|
+
if not content:
|
|
73
|
+
continue
|
|
74
|
+
if not hasattr(content, "__iter__"):
|
|
75
|
+
# Is a single grid
|
|
76
|
+
content = [content]
|
|
77
|
+
for agent in content:
|
|
78
|
+
data = agent_portrayal(agent)
|
|
79
|
+
x.append(i)
|
|
80
|
+
y.append(j)
|
|
81
|
+
|
|
82
|
+
# This is the default value for the marker size, which auto-scales
|
|
83
|
+
# according to the grid area.
|
|
84
|
+
default_size = (180 / max(g.width, g.height)) ** 2
|
|
85
|
+
# establishing a default prevents misalignment if some agents are not given size, color, etc.
|
|
86
|
+
size = data.get("size", default_size)
|
|
87
|
+
s.append(size)
|
|
88
|
+
color = data.get("color", "tab:blue")
|
|
89
|
+
c.append(color)
|
|
90
|
+
mark = data.get("shape", "o")
|
|
91
|
+
m.append(mark)
|
|
92
|
+
out = {"x": x, "y": y, "s": s, "c": c, "m": m}
|
|
93
|
+
return out
|
|
94
|
+
|
|
95
|
+
space_ax.set_xlim(-1, space.width)
|
|
96
|
+
space_ax.set_ylim(-1, space.height)
|
|
97
|
+
_split_and_scatter(portray(space), space_ax)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _draw_network_grid(space, space_ax, agent_portrayal):
|
|
101
|
+
graph = space.G
|
|
102
|
+
pos = nx.spring_layout(graph, seed=0)
|
|
103
|
+
nx.draw(
|
|
104
|
+
graph,
|
|
105
|
+
ax=space_ax,
|
|
106
|
+
pos=pos,
|
|
107
|
+
**agent_portrayal(graph),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _draw_continuous_space(space, space_ax, agent_portrayal):
|
|
112
|
+
def portray(space):
|
|
113
|
+
x = []
|
|
114
|
+
y = []
|
|
115
|
+
s = [] # size
|
|
116
|
+
c = [] # color
|
|
117
|
+
m = [] # shape
|
|
118
|
+
for agent in space._agent_to_index:
|
|
119
|
+
data = agent_portrayal(agent)
|
|
120
|
+
_x, _y = agent.pos
|
|
121
|
+
x.append(_x)
|
|
122
|
+
y.append(_y)
|
|
123
|
+
|
|
124
|
+
# This is matplotlib's default marker size
|
|
125
|
+
default_size = 20
|
|
126
|
+
# establishing a default prevents misalignment if some agents are not given size, color, etc.
|
|
127
|
+
size = data.get("size", default_size)
|
|
128
|
+
s.append(size)
|
|
129
|
+
color = data.get("color", "tab:blue")
|
|
130
|
+
c.append(color)
|
|
131
|
+
mark = data.get("shape", "o")
|
|
132
|
+
m.append(mark)
|
|
133
|
+
out = {"x": x, "y": y, "s": s, "c": c, "m": m}
|
|
134
|
+
return out
|
|
135
|
+
|
|
136
|
+
# Determine border style based on space.torus
|
|
137
|
+
border_style = "solid" if not space.torus else (0, (5, 10))
|
|
138
|
+
|
|
139
|
+
# Set the border of the plot
|
|
140
|
+
for spine in space_ax.spines.values():
|
|
141
|
+
spine.set_linewidth(1.5)
|
|
142
|
+
spine.set_color("black")
|
|
143
|
+
spine.set_linestyle(border_style)
|
|
144
|
+
|
|
145
|
+
width = space.x_max - space.x_min
|
|
146
|
+
x_padding = width / 20
|
|
147
|
+
height = space.y_max - space.y_min
|
|
148
|
+
y_padding = height / 20
|
|
149
|
+
space_ax.set_xlim(space.x_min - x_padding, space.x_max + x_padding)
|
|
150
|
+
space_ax.set_ylim(space.y_min - y_padding, space.y_max + y_padding)
|
|
151
|
+
|
|
152
|
+
# Portray and scatter the agents in the space
|
|
153
|
+
_split_and_scatter(portray(space), space_ax)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _draw_voronoi(space, space_ax, agent_portrayal):
|
|
157
|
+
def portray(g):
|
|
158
|
+
x = []
|
|
159
|
+
y = []
|
|
160
|
+
s = [] # size
|
|
161
|
+
c = [] # color
|
|
162
|
+
|
|
163
|
+
for cell in g.all_cells:
|
|
164
|
+
for agent in cell.agents:
|
|
165
|
+
data = agent_portrayal(agent)
|
|
166
|
+
x.append(cell.coordinate[0])
|
|
167
|
+
y.append(cell.coordinate[1])
|
|
168
|
+
if "size" in data:
|
|
169
|
+
s.append(data["size"])
|
|
170
|
+
if "color" in data:
|
|
171
|
+
c.append(data["color"])
|
|
172
|
+
out = {"x": x, "y": y}
|
|
173
|
+
# This is the default value for the marker size, which auto-scales
|
|
174
|
+
# according to the grid area.
|
|
175
|
+
out["s"] = s
|
|
176
|
+
if len(c) > 0:
|
|
177
|
+
out["c"] = c
|
|
178
|
+
|
|
179
|
+
return out
|
|
180
|
+
|
|
181
|
+
x_list = [i[0] for i in space.centroids_coordinates]
|
|
182
|
+
y_list = [i[1] for i in space.centroids_coordinates]
|
|
183
|
+
x_max = max(x_list)
|
|
184
|
+
x_min = min(x_list)
|
|
185
|
+
y_max = max(y_list)
|
|
186
|
+
y_min = min(y_list)
|
|
187
|
+
|
|
188
|
+
width = x_max - x_min
|
|
189
|
+
x_padding = width / 20
|
|
190
|
+
height = y_max - y_min
|
|
191
|
+
y_padding = height / 20
|
|
192
|
+
space_ax.set_xlim(x_min - x_padding, x_max + x_padding)
|
|
193
|
+
space_ax.set_ylim(y_min - y_padding, y_max + y_padding)
|
|
194
|
+
space_ax.scatter(**portray(space))
|
|
195
|
+
|
|
196
|
+
for cell in space.all_cells:
|
|
197
|
+
polygon = cell.properties["polygon"]
|
|
198
|
+
space_ax.fill(
|
|
199
|
+
*zip(*polygon),
|
|
200
|
+
alpha=min(1, cell.properties[space.cell_coloring_property]),
|
|
201
|
+
c="red",
|
|
202
|
+
) # Plot filled polygon
|
|
203
|
+
space_ax.plot(*zip(*polygon), color="black") # Plot polygon edges in red
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@solara.component
|
|
207
|
+
def PlotMatplotlib(model, measure, dependencies: list[any] | None = None):
|
|
208
|
+
fig = Figure()
|
|
209
|
+
ax = fig.subplots()
|
|
210
|
+
df = model.datacollector.get_model_vars_dataframe()
|
|
211
|
+
if isinstance(measure, str):
|
|
212
|
+
ax.plot(df.loc[:, measure])
|
|
213
|
+
ax.set_ylabel(measure)
|
|
214
|
+
elif isinstance(measure, dict):
|
|
215
|
+
for m, color in measure.items():
|
|
216
|
+
ax.plot(df.loc[:, m], label=m, color=color)
|
|
217
|
+
fig.legend()
|
|
218
|
+
elif isinstance(measure, list | tuple):
|
|
219
|
+
for m in measure:
|
|
220
|
+
ax.plot(df.loc[:, m], label=m)
|
|
221
|
+
fig.legend()
|
|
222
|
+
# Set integer x axis
|
|
223
|
+
ax.xaxis.set_major_locator(MaxNLocator(integer=True))
|
|
224
|
+
solara.FigureMatplotlib(fig, dependencies=dependencies)
|
|
@@ -7,8 +7,8 @@ from mesa.space import SingleGrid
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class EpsteinAgent(Agent):
|
|
10
|
-
def __init__(self,
|
|
11
|
-
super().__init__(
|
|
10
|
+
def __init__(self, model, vision, movement):
|
|
11
|
+
super().__init__(model)
|
|
12
12
|
self.vision = vision
|
|
13
13
|
self.movement = movement
|
|
14
14
|
|
|
@@ -46,7 +46,6 @@ class Citizen(EpsteinAgent):
|
|
|
46
46
|
|
|
47
47
|
def __init__(
|
|
48
48
|
self,
|
|
49
|
-
unique_id,
|
|
50
49
|
model,
|
|
51
50
|
vision,
|
|
52
51
|
movement,
|
|
@@ -59,7 +58,6 @@ class Citizen(EpsteinAgent):
|
|
|
59
58
|
"""
|
|
60
59
|
Create a new Citizen.
|
|
61
60
|
Args:
|
|
62
|
-
unique_id: unique int
|
|
63
61
|
model : model instance
|
|
64
62
|
hardship: Agent's 'perceived hardship (i.e., physical or economic
|
|
65
63
|
privation).' Exogenous, drawn from U(0,1).
|
|
@@ -71,7 +69,7 @@ class Citizen(EpsteinAgent):
|
|
|
71
69
|
vision: number of cells in each direction (N, S, E and W) that
|
|
72
70
|
agent can inspect. Exogenous.
|
|
73
71
|
"""
|
|
74
|
-
super().__init__(
|
|
72
|
+
super().__init__(model, vision, movement)
|
|
75
73
|
self.hardship = hardship
|
|
76
74
|
self.regime_legitimacy = regime_legitimacy
|
|
77
75
|
self.risk_aversion = risk_aversion
|
|
@@ -144,8 +142,8 @@ class Cop(EpsteinAgent):
|
|
|
144
142
|
able to inspect
|
|
145
143
|
"""
|
|
146
144
|
|
|
147
|
-
def __init__(self,
|
|
148
|
-
super().__init__(
|
|
145
|
+
def __init__(self, model, vision, movement, max_jail_term):
|
|
146
|
+
super().__init__(model, vision, movement)
|
|
149
147
|
self.max_jail_term = max_jail_term
|
|
150
148
|
|
|
151
149
|
def step(self):
|
|
@@ -236,7 +234,6 @@ class EpsteinCivilViolence(Model):
|
|
|
236
234
|
for _, pos in self.grid.coord_iter():
|
|
237
235
|
if self.random.random() < self.cop_density:
|
|
238
236
|
agent = Cop(
|
|
239
|
-
self.next_id(),
|
|
240
237
|
self,
|
|
241
238
|
cop_vision,
|
|
242
239
|
movement,
|
|
@@ -244,7 +241,6 @@ class EpsteinCivilViolence(Model):
|
|
|
244
241
|
)
|
|
245
242
|
elif self.random.random() < (self.cop_density + self.citizen_density):
|
|
246
243
|
agent = Citizen(
|
|
247
|
-
self.next_id(),
|
|
248
244
|
self,
|
|
249
245
|
citizen_vision,
|
|
250
246
|
movement,
|
|
@@ -270,4 +266,4 @@ if __name__ == "__main__":
|
|
|
270
266
|
|
|
271
267
|
simulator.setup(model)
|
|
272
268
|
|
|
273
|
-
simulator.
|
|
269
|
+
simulator.run_for(time_delta=100)
|
|
@@ -14,8 +14,8 @@ from mesa.experimental.devs.simulator import ABMSimulator
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class Animal(mesa.Agent):
|
|
17
|
-
def __init__(self,
|
|
18
|
-
super().__init__(
|
|
17
|
+
def __init__(self, model, moore, energy, p_reproduce, energy_from_food):
|
|
18
|
+
super().__init__(model)
|
|
19
19
|
self.energy = energy
|
|
20
20
|
self.p_reproduce = p_reproduce
|
|
21
21
|
self.energy_from_food = energy_from_food
|
|
@@ -30,7 +30,6 @@ class Animal(mesa.Agent):
|
|
|
30
30
|
def spawn_offspring(self):
|
|
31
31
|
self.energy /= 2
|
|
32
32
|
offspring = self.__class__(
|
|
33
|
-
self.model.next_id(),
|
|
34
33
|
self.model,
|
|
35
34
|
self.moore,
|
|
36
35
|
self.energy,
|
|
@@ -109,7 +108,7 @@ class GrassPatch(mesa.Agent):
|
|
|
109
108
|
function_args=[self, "fully_grown", True],
|
|
110
109
|
)
|
|
111
110
|
|
|
112
|
-
def __init__(self,
|
|
111
|
+
def __init__(self, model, fully_grown, countdown, grass_regrowth_time):
|
|
113
112
|
"""
|
|
114
113
|
Creates a new patch of grass
|
|
115
114
|
|
|
@@ -117,7 +116,7 @@ class GrassPatch(mesa.Agent):
|
|
|
117
116
|
grown: (boolean) Whether the patch of grass is fully grown or not
|
|
118
117
|
countdown: Time for the patch of grass to be fully grown again
|
|
119
118
|
"""
|
|
120
|
-
super().__init__(
|
|
119
|
+
super().__init__(model)
|
|
121
120
|
self._fully_grown = fully_grown
|
|
122
121
|
self.grass_regrowth_time = grass_regrowth_time
|
|
123
122
|
|
|
@@ -191,7 +190,6 @@ class WolfSheep(mesa.Model):
|
|
|
191
190
|
)
|
|
192
191
|
energy = self.random.randrange(2 * sheep_gain_from_food)
|
|
193
192
|
sheep = Sheep(
|
|
194
|
-
self.next_id(),
|
|
195
193
|
self,
|
|
196
194
|
moore,
|
|
197
195
|
energy,
|
|
@@ -208,7 +206,6 @@ class WolfSheep(mesa.Model):
|
|
|
208
206
|
)
|
|
209
207
|
energy = self.random.randrange(2 * wolf_gain_from_food)
|
|
210
208
|
wolf = Wolf(
|
|
211
|
-
self.next_id(),
|
|
212
209
|
self,
|
|
213
210
|
moore,
|
|
214
211
|
energy,
|
|
@@ -225,14 +222,12 @@ class WolfSheep(mesa.Model):
|
|
|
225
222
|
countdown = grass_regrowth_time
|
|
226
223
|
else:
|
|
227
224
|
countdown = self.random.randrange(grass_regrowth_time)
|
|
228
|
-
patch = GrassPatch(
|
|
229
|
-
self.next_id(), self, fully_grown, countdown, grass_regrowth_time
|
|
230
|
-
)
|
|
225
|
+
patch = GrassPatch(self, fully_grown, countdown, grass_regrowth_time)
|
|
231
226
|
self.grid.place_agent(patch, pos)
|
|
232
227
|
|
|
233
228
|
def step(self):
|
|
234
|
-
self.
|
|
235
|
-
self.
|
|
229
|
+
self.agents_by_type[Sheep].shuffle(inplace=True).do("step")
|
|
230
|
+
self.agents_by_type[Wolf].shuffle(inplace=True).do("step")
|
|
236
231
|
|
|
237
232
|
|
|
238
233
|
if __name__ == "__main__":
|