Mesa 3.0.0a3__py3-none-any.whl → 3.0.0a5__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 +2 -3
- mesa/agent.py +193 -75
- mesa/batchrunner.py +18 -23
- mesa/cookiecutter-mesa/hooks/post_gen_project.py +2 -0
- mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/__init__.py +1 -0
- mesa/datacollection.py +138 -27
- mesa/experimental/UserParam.py +67 -0
- mesa/experimental/__init__.py +5 -1
- mesa/experimental/cell_space/__init__.py +7 -0
- mesa/experimental/cell_space/cell.py +61 -20
- mesa/experimental/cell_space/cell_agent.py +12 -7
- mesa/experimental/cell_space/cell_collection.py +54 -17
- mesa/experimental/cell_space/discrete_space.py +16 -5
- mesa/experimental/cell_space/grid.py +19 -8
- mesa/experimental/cell_space/network.py +9 -7
- mesa/experimental/cell_space/voronoi.py +26 -33
- mesa/experimental/components/altair.py +81 -0
- mesa/experimental/components/matplotlib.py +242 -0
- mesa/experimental/devs/__init__.py +2 -0
- mesa/experimental/devs/eventlist.py +36 -15
- mesa/experimental/devs/examples/epstein_civil_violence.py +71 -39
- mesa/experimental/devs/examples/wolf_sheep.py +43 -44
- mesa/experimental/devs/simulator.py +55 -15
- mesa/experimental/solara_viz.py +453 -0
- mesa/main.py +6 -4
- mesa/model.py +64 -61
- mesa/space.py +154 -123
- mesa/time.py +57 -67
- mesa/visualization/UserParam.py +19 -6
- mesa/visualization/__init__.py +14 -2
- mesa/visualization/components/altair.py +18 -1
- mesa/visualization/components/matplotlib.py +26 -2
- mesa/visualization/solara_viz.py +231 -225
- mesa/visualization/utils.py +9 -0
- {mesa-3.0.0a3.dist-info → mesa-3.0.0a5.dist-info}/METADATA +2 -1
- mesa-3.0.0a5.dist-info/RECORD +44 -0
- mesa-3.0.0a3.dist-info/RECORD +0 -39
- {mesa-3.0.0a3.dist-info → mesa-3.0.0a5.dist-info}/WHEEL +0 -0
- {mesa-3.0.0a3.dist-info → mesa-3.0.0a5.dist-info}/entry_points.txt +0 -0
- {mesa-3.0.0a3.dist-info → mesa-3.0.0a5.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""DiscreteSpace base class."""
|
|
2
|
+
|
|
1
3
|
from __future__ import annotations
|
|
2
4
|
|
|
3
5
|
from functools import cached_property
|
|
@@ -28,6 +30,13 @@ class DiscreteSpace(Generic[T]):
|
|
|
28
30
|
cell_klass: type[T] = Cell,
|
|
29
31
|
random: Random | None = None,
|
|
30
32
|
):
|
|
33
|
+
"""Instantiate a DiscreteSpace.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
capacity: capacity of cells
|
|
37
|
+
cell_klass: base class for all cells
|
|
38
|
+
random: random number generator
|
|
39
|
+
"""
|
|
31
40
|
super().__init__()
|
|
32
41
|
self.capacity = capacity
|
|
33
42
|
self._cells: dict[tuple[int, ...], T] = {}
|
|
@@ -40,25 +49,27 @@ class DiscreteSpace(Generic[T]):
|
|
|
40
49
|
self._empties_initialized = False
|
|
41
50
|
|
|
42
51
|
@property
|
|
43
|
-
def cutoff_empties(self):
|
|
52
|
+
def cutoff_empties(self): # noqa
|
|
44
53
|
return 7.953 * len(self._cells) ** 0.384
|
|
45
54
|
|
|
46
55
|
def _connect_single_cell(self, cell: T): ...
|
|
47
56
|
|
|
48
57
|
@cached_property
|
|
49
58
|
def all_cells(self):
|
|
59
|
+
"""Return all cells in space."""
|
|
50
60
|
return CellCollection({cell: cell.agents for cell in self._cells.values()})
|
|
51
61
|
|
|
52
|
-
def __iter__(self):
|
|
62
|
+
def __iter__(self): # noqa
|
|
53
63
|
return iter(self._cells.values())
|
|
54
64
|
|
|
55
|
-
def __getitem__(self, key):
|
|
65
|
+
def __getitem__(self, key: tuple[int, ...]) -> T: # noqa: D105
|
|
56
66
|
return self._cells[key]
|
|
57
67
|
|
|
58
68
|
@property
|
|
59
|
-
def empties(self) -> CellCollection:
|
|
69
|
+
def empties(self) -> CellCollection[T]:
|
|
70
|
+
"""Return all empty in spaces."""
|
|
60
71
|
return self.all_cells.select(lambda cell: cell.is_empty)
|
|
61
72
|
|
|
62
73
|
def select_random_empty_cell(self) -> T:
|
|
63
|
-
"""
|
|
74
|
+
"""Select random empty cell."""
|
|
64
75
|
return self.random.choice(list(self.empties))
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Various Grid Spaces."""
|
|
2
|
+
|
|
1
3
|
from __future__ import annotations
|
|
2
4
|
|
|
3
5
|
from collections.abc import Sequence
|
|
@@ -10,8 +12,8 @@ from mesa.experimental.cell_space import Cell, DiscreteSpace
|
|
|
10
12
|
T = TypeVar("T", bound=Cell)
|
|
11
13
|
|
|
12
14
|
|
|
13
|
-
class Grid(DiscreteSpace, Generic[T]):
|
|
14
|
-
"""Base class for all grid classes
|
|
15
|
+
class Grid(DiscreteSpace[T], Generic[T]):
|
|
16
|
+
"""Base class for all grid classes.
|
|
15
17
|
|
|
16
18
|
Attributes:
|
|
17
19
|
dimensions (Sequence[int]): the dimensions of the grid
|
|
@@ -30,6 +32,15 @@ class Grid(DiscreteSpace, Generic[T]):
|
|
|
30
32
|
random: Random | None = None,
|
|
31
33
|
cell_klass: type[T] = Cell,
|
|
32
34
|
) -> None:
|
|
35
|
+
"""Initialise the grid class.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
dimensions: the dimensions of the space
|
|
39
|
+
torus: whether the space wraps
|
|
40
|
+
capacity: capacity of the grid cell
|
|
41
|
+
random: a random number generator
|
|
42
|
+
cell_klass: the base class to use for the cells
|
|
43
|
+
"""
|
|
33
44
|
super().__init__(capacity=capacity, random=random, cell_klass=cell_klass)
|
|
34
45
|
self.torus = torus
|
|
35
46
|
self.dimensions = dimensions
|
|
@@ -63,7 +74,7 @@ class Grid(DiscreteSpace, Generic[T]):
|
|
|
63
74
|
if self.capacity is not None and not isinstance(self.capacity, float | int):
|
|
64
75
|
raise ValueError("Capacity must be a number or None.")
|
|
65
76
|
|
|
66
|
-
def select_random_empty_cell(self) -> T:
|
|
77
|
+
def select_random_empty_cell(self) -> T: # noqa
|
|
67
78
|
# FIXME:: currently just a simple boolean to control behavior
|
|
68
79
|
# FIXME:: basically if grid is close to 99% full, creating empty list can be faster
|
|
69
80
|
# FIXME:: note however that the old results don't apply because in this implementation
|
|
@@ -89,7 +100,7 @@ class Grid(DiscreteSpace, Generic[T]):
|
|
|
89
100
|
if self.torus:
|
|
90
101
|
n_coord = tuple(nc % d for nc, d in zip(n_coord, self.dimensions))
|
|
91
102
|
if all(0 <= nc < d for nc, d in zip(n_coord, self.dimensions)):
|
|
92
|
-
cell.connect(self._cells[n_coord])
|
|
103
|
+
cell.connect(self._cells[n_coord], d_coord)
|
|
93
104
|
|
|
94
105
|
def _connect_single_cell_2d(self, cell: T, offsets: list[tuple[int, int]]) -> None:
|
|
95
106
|
i, j = cell.coordinate
|
|
@@ -100,7 +111,7 @@ class Grid(DiscreteSpace, Generic[T]):
|
|
|
100
111
|
if self.torus:
|
|
101
112
|
ni, nj = ni % height, nj % width
|
|
102
113
|
if 0 <= ni < height and 0 <= nj < width:
|
|
103
|
-
cell.connect(self._cells[ni, nj])
|
|
114
|
+
cell.connect(self._cells[ni, nj], (di, dj))
|
|
104
115
|
|
|
105
116
|
|
|
106
117
|
class OrthogonalMooreGrid(Grid[T]):
|
|
@@ -122,7 +133,6 @@ class OrthogonalMooreGrid(Grid[T]):
|
|
|
122
133
|
( 1, -1), ( 1, 0), ( 1, 1),
|
|
123
134
|
]
|
|
124
135
|
# fmt: on
|
|
125
|
-
height, width = self.dimensions
|
|
126
136
|
|
|
127
137
|
for cell in self.all_cells:
|
|
128
138
|
self._connect_single_cell_2d(cell, offsets)
|
|
@@ -154,13 +164,12 @@ class OrthogonalVonNeumannGrid(Grid[T]):
|
|
|
154
164
|
( 1, 0),
|
|
155
165
|
]
|
|
156
166
|
# fmt: on
|
|
157
|
-
height, width = self.dimensions
|
|
158
167
|
|
|
159
168
|
for cell in self.all_cells:
|
|
160
169
|
self._connect_single_cell_2d(cell, offsets)
|
|
161
170
|
|
|
162
171
|
def _connect_cells_nd(self) -> None:
|
|
163
|
-
offsets = []
|
|
172
|
+
offsets: list[tuple[int, ...]] = []
|
|
164
173
|
dimensions = len(self.dimensions)
|
|
165
174
|
for dim in range(dimensions):
|
|
166
175
|
for delta in [
|
|
@@ -176,6 +185,8 @@ class OrthogonalVonNeumannGrid(Grid[T]):
|
|
|
176
185
|
|
|
177
186
|
|
|
178
187
|
class HexGrid(Grid[T]):
|
|
188
|
+
"""A Grid with hexagonal tilling of the space."""
|
|
189
|
+
|
|
179
190
|
def _connect_cells_2d(self) -> None:
|
|
180
191
|
# fmt: off
|
|
181
192
|
even_offsets = [
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""A Network grid."""
|
|
2
|
+
|
|
1
3
|
from random import Random
|
|
2
4
|
from typing import Any
|
|
3
5
|
|
|
@@ -5,8 +7,8 @@ from mesa.experimental.cell_space.cell import Cell
|
|
|
5
7
|
from mesa.experimental.cell_space.discrete_space import DiscreteSpace
|
|
6
8
|
|
|
7
9
|
|
|
8
|
-
class Network(DiscreteSpace):
|
|
9
|
-
"""A networked discrete space"""
|
|
10
|
+
class Network(DiscreteSpace[Cell]):
|
|
11
|
+
"""A networked discrete space."""
|
|
10
12
|
|
|
11
13
|
def __init__(
|
|
12
14
|
self,
|
|
@@ -15,13 +17,13 @@ class Network(DiscreteSpace):
|
|
|
15
17
|
random: Random | None = None,
|
|
16
18
|
cell_klass: type[Cell] = Cell,
|
|
17
19
|
) -> None:
|
|
18
|
-
"""A Networked grid
|
|
20
|
+
"""A Networked grid.
|
|
19
21
|
|
|
20
22
|
Args:
|
|
21
23
|
G: a NetworkX Graph instance.
|
|
22
24
|
capacity (int) : the capacity of the cell
|
|
23
|
-
random (Random):
|
|
24
|
-
|
|
25
|
+
random (Random): a random number generator
|
|
26
|
+
cell_klass (type[Cell]): The base Cell class to use in the Network
|
|
25
27
|
|
|
26
28
|
"""
|
|
27
29
|
super().__init__(capacity=capacity, random=random, cell_klass=cell_klass)
|
|
@@ -35,6 +37,6 @@ class Network(DiscreteSpace):
|
|
|
35
37
|
for cell in self.all_cells:
|
|
36
38
|
self._connect_single_cell(cell)
|
|
37
39
|
|
|
38
|
-
def _connect_single_cell(self, cell):
|
|
40
|
+
def _connect_single_cell(self, cell: Cell):
|
|
39
41
|
for node_id in self.G.neighbors(cell.coordinate):
|
|
40
|
-
cell.connect(self._cells[node_id])
|
|
42
|
+
cell.connect(self._cells[node_id], node_id)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Support for Voronoi meshed grids."""
|
|
2
|
+
|
|
1
3
|
from collections.abc import Sequence
|
|
2
4
|
from itertools import combinations
|
|
3
5
|
from random import Random
|
|
@@ -9,16 +11,17 @@ from mesa.experimental.cell_space.discrete_space import DiscreteSpace
|
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
class Delaunay:
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
+
"""Class to compute a Delaunay triangulation in 2D.
|
|
15
|
+
|
|
14
16
|
ref: http://github.com/jmespadero/pyDelaunay2D
|
|
15
17
|
"""
|
|
16
18
|
|
|
17
19
|
def __init__(self, center: tuple = (0, 0), radius: int = 9999) -> None:
|
|
18
|
-
"""
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
"""Init and create a new frame to contain the triangulation.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
center: Optional position for the center of the frame. Default (0,0)
|
|
24
|
+
radius: Optional distance from corners to the center.
|
|
22
25
|
"""
|
|
23
26
|
center = np.asarray(center)
|
|
24
27
|
# Create coordinates for the corners of the frame
|
|
@@ -44,9 +47,7 @@ class Delaunay:
|
|
|
44
47
|
self.circles[t] = self._circumcenter(t)
|
|
45
48
|
|
|
46
49
|
def _circumcenter(self, triangle: list) -> tuple:
|
|
47
|
-
"""
|
|
48
|
-
Compute circumcenter and circumradius of a triangle in 2D.
|
|
49
|
-
"""
|
|
50
|
+
"""Compute circumcenter and circumradius of a triangle in 2D."""
|
|
50
51
|
points = np.asarray([self.coords[v] for v in triangle])
|
|
51
52
|
points2 = np.dot(points, points.T)
|
|
52
53
|
a = np.bmat([[2 * points2, [[1], [1], [1]]], [[[1, 1, 1, 0]]]])
|
|
@@ -60,16 +61,12 @@ class Delaunay:
|
|
|
60
61
|
return (center, radius)
|
|
61
62
|
|
|
62
63
|
def _in_circle(self, triangle: list, point: list) -> bool:
|
|
63
|
-
"""
|
|
64
|
-
Check if point p is inside of precomputed circumcircle of triangle.
|
|
65
|
-
"""
|
|
64
|
+
"""Check if point p is inside of precomputed circumcircle of triangle."""
|
|
66
65
|
center, radius = self.circles[triangle]
|
|
67
66
|
return np.sum(np.square(center - point)) <= radius
|
|
68
67
|
|
|
69
68
|
def add_point(self, point: Sequence) -> None:
|
|
70
|
-
"""
|
|
71
|
-
Add a point to the current DT, and refine it using Bowyer-Watson.
|
|
72
|
-
"""
|
|
69
|
+
"""Add a point to the current DT, and refine it using Bowyer-Watson."""
|
|
73
70
|
point_index = len(self.coords)
|
|
74
71
|
self.coords.append(np.asarray(point))
|
|
75
72
|
|
|
@@ -121,9 +118,7 @@ class Delaunay:
|
|
|
121
118
|
self.triangles[triangle][2] = new_triangles[(i - 1) % n] # previous
|
|
122
119
|
|
|
123
120
|
def export_triangles(self) -> list:
|
|
124
|
-
"""
|
|
125
|
-
Export the current list of Delaunay triangles
|
|
126
|
-
"""
|
|
121
|
+
"""Export the current list of Delaunay triangles."""
|
|
127
122
|
triangles_list = [
|
|
128
123
|
(a - 4, b - 4, c - 4)
|
|
129
124
|
for (a, b, c) in self.triangles
|
|
@@ -132,9 +127,7 @@ class Delaunay:
|
|
|
132
127
|
return triangles_list
|
|
133
128
|
|
|
134
129
|
def export_voronoi_regions(self):
|
|
135
|
-
"""
|
|
136
|
-
Export coordinates and regions of Voronoi diagram as indexed data.
|
|
137
|
-
"""
|
|
130
|
+
"""Export coordinates and regions of Voronoi diagram as indexed data."""
|
|
138
131
|
use_vertex = {i: [] for i in range(len(self.coords))}
|
|
139
132
|
vor_coors = []
|
|
140
133
|
index = {}
|
|
@@ -163,11 +156,13 @@ class Delaunay:
|
|
|
163
156
|
return vor_coors, regions
|
|
164
157
|
|
|
165
158
|
|
|
166
|
-
def round_float(x: float) -> int:
|
|
159
|
+
def round_float(x: float) -> int: # noqa
|
|
167
160
|
return int(x * 500)
|
|
168
161
|
|
|
169
162
|
|
|
170
163
|
class VoronoiGrid(DiscreteSpace):
|
|
164
|
+
"""Voronoi meshed GridSpace."""
|
|
165
|
+
|
|
171
166
|
triangulation: Delaunay
|
|
172
167
|
voronoi_coordinates: list
|
|
173
168
|
regions: list
|
|
@@ -181,8 +176,7 @@ class VoronoiGrid(DiscreteSpace):
|
|
|
181
176
|
capacity_function: callable = round_float,
|
|
182
177
|
cell_coloring_property: str | None = None,
|
|
183
178
|
) -> None:
|
|
184
|
-
"""
|
|
185
|
-
A Voronoi Tessellation Grid.
|
|
179
|
+
"""A Voronoi Tessellation Grid.
|
|
186
180
|
|
|
187
181
|
Given a set of points, this class creates a grid where a cell is centered in each point,
|
|
188
182
|
its neighbors are given by Voronoi Tessellation cells neighbors
|
|
@@ -192,7 +186,7 @@ class VoronoiGrid(DiscreteSpace):
|
|
|
192
186
|
centroids_coordinates: coordinates of centroids to build the tessellation space
|
|
193
187
|
capacity (int) : capacity of the cells in the discrete space
|
|
194
188
|
random (Random): random number generator
|
|
195
|
-
|
|
189
|
+
cell_klass (type[Cell]): type of cell class
|
|
196
190
|
capacity_function (Callable): function to compute (int) capacity according to (float) area
|
|
197
191
|
cell_coloring_property (str): voronoi visualization polygon fill property
|
|
198
192
|
"""
|
|
@@ -215,17 +209,15 @@ class VoronoiGrid(DiscreteSpace):
|
|
|
215
209
|
self._build_cell_polygons()
|
|
216
210
|
|
|
217
211
|
def _connect_cells(self) -> None:
|
|
218
|
-
"""
|
|
219
|
-
Connect cells to neighbors based on given centroids and using Delaunay Triangulation
|
|
220
|
-
"""
|
|
212
|
+
"""Connect cells to neighbors based on given centroids and using Delaunay Triangulation."""
|
|
221
213
|
self.triangulation = Delaunay()
|
|
222
214
|
for centroid in self.centroids_coordinates:
|
|
223
215
|
self.triangulation.add_point(centroid)
|
|
224
216
|
|
|
225
217
|
for point in self.triangulation.export_triangles():
|
|
226
218
|
for i, j in combinations(point, 2):
|
|
227
|
-
self._cells[i].connect(self._cells[j])
|
|
228
|
-
self._cells[j].connect(self._cells[i])
|
|
219
|
+
self._cells[i].connect(self._cells[j], (i, j))
|
|
220
|
+
self._cells[j].connect(self._cells[i], (j, i))
|
|
229
221
|
|
|
230
222
|
def _validate_parameters(self) -> None:
|
|
231
223
|
if self.capacity is not None and not isinstance(self.capacity, float | int):
|
|
@@ -241,9 +233,10 @@ class VoronoiGrid(DiscreteSpace):
|
|
|
241
233
|
|
|
242
234
|
def _get_voronoi_regions(self) -> tuple:
|
|
243
235
|
if self.voronoi_coordinates is None or self.regions is None:
|
|
244
|
-
|
|
245
|
-
self.
|
|
246
|
-
|
|
236
|
+
(
|
|
237
|
+
self.voronoi_coordinates,
|
|
238
|
+
self.regions,
|
|
239
|
+
) = self.triangulation.export_voronoi_regions()
|
|
247
240
|
return self.voronoi_coordinates, self.regions
|
|
248
241
|
|
|
249
242
|
@staticmethod
|
|
@@ -0,0 +1,81 @@
|
|
|
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
|
|
@@ -0,0 +1,242 @@
|
|
|
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)
|