Mesa 3.0.0a4__py3-none-any.whl → 3.0.0b0__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 +116 -85
- mesa/batchrunner.py +22 -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 +17 -6
- mesa/experimental/__init__.py +2 -0
- mesa/experimental/cell_space/__init__.py +14 -1
- mesa/experimental/cell_space/cell.py +84 -23
- mesa/experimental/cell_space/cell_agent.py +117 -21
- mesa/experimental/cell_space/cell_collection.py +54 -17
- mesa/experimental/cell_space/discrete_space.py +79 -7
- 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 +10 -0
- mesa/experimental/components/matplotlib.py +18 -0
- mesa/experimental/devs/__init__.py +2 -0
- mesa/experimental/devs/eventlist.py +36 -15
- mesa/experimental/devs/examples/epstein_civil_violence.py +65 -29
- mesa/experimental/devs/examples/wolf_sheep.py +40 -35
- mesa/experimental/devs/simulator.py +55 -15
- mesa/experimental/solara_viz.py +10 -19
- mesa/main.py +6 -4
- mesa/model.py +51 -54
- mesa/space.py +145 -120
- mesa/time.py +57 -67
- mesa/visualization/UserParam.py +19 -6
- mesa/visualization/__init__.py +3 -2
- mesa/visualization/components/altair.py +4 -2
- mesa/visualization/components/matplotlib.py +176 -85
- mesa/visualization/solara_viz.py +167 -84
- mesa/visualization/utils.py +3 -1
- {mesa-3.0.0a4.dist-info → mesa-3.0.0b0.dist-info}/METADATA +55 -13
- mesa-3.0.0b0.dist-info/RECORD +45 -0
- mesa-3.0.0b0.dist-info/licenses/LICENSE +202 -0
- mesa-3.0.0a4.dist-info/licenses/LICENSE → mesa-3.0.0b0.dist-info/licenses/NOTICE +2 -2
- mesa-3.0.0a4.dist-info/RECORD +0 -44
- {mesa-3.0.0a4.dist-info → mesa-3.0.0b0.dist-info}/WHEEL +0 -0
- {mesa-3.0.0a4.dist-info → mesa-3.0.0b0.dist-info}/entry_points.txt +0 -0
|
@@ -1,114 +1,190 @@
|
|
|
1
|
-
|
|
1
|
+
"""Matplotlib based solara components for visualization MESA spaces and plots."""
|
|
2
2
|
|
|
3
|
+
import warnings
|
|
4
|
+
|
|
5
|
+
import matplotlib.pyplot as plt
|
|
3
6
|
import networkx as nx
|
|
7
|
+
import numpy as np
|
|
4
8
|
import solara
|
|
9
|
+
from matplotlib.cm import ScalarMappable
|
|
10
|
+
from matplotlib.colors import LinearSegmentedColormap, Normalize, to_rgba
|
|
5
11
|
from matplotlib.figure import Figure
|
|
6
|
-
from matplotlib.ticker import MaxNLocator
|
|
7
12
|
|
|
8
13
|
import mesa
|
|
9
14
|
from mesa.experimental.cell_space import VoronoiGrid
|
|
15
|
+
from mesa.space import PropertyLayer
|
|
10
16
|
from mesa.visualization.utils import update_counter
|
|
11
17
|
|
|
12
18
|
|
|
13
|
-
def make_space_matplotlib(agent_portrayal=None):
|
|
19
|
+
def make_space_matplotlib(agent_portrayal=None, propertylayer_portrayal=None):
|
|
20
|
+
"""Create a Matplotlib-based space visualization component.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
agent_portrayal (function): Function to portray agents
|
|
24
|
+
propertylayer_portrayal (dict): Dictionary of PropertyLayer portrayal specifications
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
function: A function that creates a SpaceMatplotlib component
|
|
28
|
+
"""
|
|
14
29
|
if agent_portrayal is None:
|
|
15
30
|
|
|
16
31
|
def agent_portrayal(a):
|
|
17
32
|
return {"id": a.unique_id}
|
|
18
33
|
|
|
19
34
|
def MakeSpaceMatplotlib(model):
|
|
20
|
-
return SpaceMatplotlib(model, agent_portrayal)
|
|
35
|
+
return SpaceMatplotlib(model, agent_portrayal, propertylayer_portrayal)
|
|
21
36
|
|
|
22
37
|
return MakeSpaceMatplotlib
|
|
23
38
|
|
|
24
39
|
|
|
25
40
|
@solara.component
|
|
26
|
-
def SpaceMatplotlib(
|
|
41
|
+
def SpaceMatplotlib(
|
|
42
|
+
model,
|
|
43
|
+
agent_portrayal,
|
|
44
|
+
propertylayer_portrayal,
|
|
45
|
+
dependencies: list[any] | None = None,
|
|
46
|
+
):
|
|
47
|
+
"""Create a Matplotlib-based space visualization component."""
|
|
27
48
|
update_counter.get()
|
|
28
49
|
space_fig = Figure()
|
|
29
50
|
space_ax = space_fig.subplots()
|
|
30
51
|
space = getattr(model, "grid", None)
|
|
31
52
|
if space is None:
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if isinstance(space, mesa.space.
|
|
35
|
-
|
|
53
|
+
space = getattr(model, "space", None)
|
|
54
|
+
|
|
55
|
+
if isinstance(space, mesa.space._Grid):
|
|
56
|
+
_draw_grid(space, space_ax, agent_portrayal, propertylayer_portrayal, model)
|
|
36
57
|
elif isinstance(space, mesa.space.ContinuousSpace):
|
|
37
|
-
_draw_continuous_space(space, space_ax, agent_portrayal)
|
|
58
|
+
_draw_continuous_space(space, space_ax, agent_portrayal, model)
|
|
59
|
+
elif isinstance(space, mesa.space.NetworkGrid):
|
|
60
|
+
_draw_network_grid(space, space_ax, agent_portrayal)
|
|
38
61
|
elif isinstance(space, VoronoiGrid):
|
|
39
62
|
_draw_voronoi(space, space_ax, agent_portrayal)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
solara.FigureMatplotlib(space_fig, format="png", dependencies=dependencies)
|
|
43
|
-
|
|
63
|
+
elif space is None and propertylayer_portrayal:
|
|
64
|
+
draw_property_layers(space_ax, space, propertylayer_portrayal, model)
|
|
44
65
|
|
|
45
|
-
|
|
46
|
-
def _split_and_scatter(portray_data, space_ax):
|
|
47
|
-
grouped_data = defaultdict(lambda: {"x": [], "y": [], "s": [], "c": []})
|
|
48
|
-
|
|
49
|
-
# Extract data from the dictionary
|
|
50
|
-
x = portray_data["x"]
|
|
51
|
-
y = portray_data["y"]
|
|
52
|
-
s = portray_data["s"]
|
|
53
|
-
c = portray_data["c"]
|
|
54
|
-
m = portray_data["m"]
|
|
55
|
-
|
|
56
|
-
if not (len(x) == len(y) == len(s) == len(c) == len(m)):
|
|
57
|
-
raise ValueError(
|
|
58
|
-
"Length mismatch in portrayal data lists: "
|
|
59
|
-
f"x: {len(x)}, y: {len(y)}, size: {len(s)}, "
|
|
60
|
-
f"color: {len(c)}, marker: {len(m)}"
|
|
61
|
-
)
|
|
66
|
+
solara.FigureMatplotlib(space_fig, format="png", dependencies=dependencies)
|
|
62
67
|
|
|
63
|
-
# Group the data by marker
|
|
64
|
-
for i in range(len(x)):
|
|
65
|
-
marker = m[i]
|
|
66
|
-
grouped_data[marker]["x"].append(x[i])
|
|
67
|
-
grouped_data[marker]["y"].append(y[i])
|
|
68
|
-
grouped_data[marker]["s"].append(s[i])
|
|
69
|
-
grouped_data[marker]["c"].append(c[i])
|
|
70
68
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
def draw_property_layers(ax, space, propertylayer_portrayal, model):
|
|
70
|
+
"""Draw PropertyLayers on the given axes.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
ax (matplotlib.axes.Axes): The axes to draw on.
|
|
74
|
+
space (mesa.space._Grid): The space containing the PropertyLayers.
|
|
75
|
+
propertylayer_portrayal (dict): Dictionary of PropertyLayer portrayal specifications.
|
|
76
|
+
model (mesa.Model): The model instance.
|
|
77
|
+
"""
|
|
78
|
+
for layer_name, portrayal in propertylayer_portrayal.items():
|
|
79
|
+
layer = getattr(model, layer_name, None)
|
|
80
|
+
if not isinstance(layer, PropertyLayer):
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
data = layer.data.astype(float) if layer.data.dtype == bool else layer.data
|
|
84
|
+
width, height = data.shape if space is None else (space.width, space.height)
|
|
85
|
+
|
|
86
|
+
if space and data.shape != (width, height):
|
|
87
|
+
warnings.warn(
|
|
88
|
+
f"Layer {layer_name} dimensions ({data.shape}) do not match space dimensions ({width}, {height}).",
|
|
89
|
+
UserWarning,
|
|
90
|
+
stacklevel=2,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Get portrayal properties, or use defaults
|
|
94
|
+
alpha = portrayal.get("alpha", 1)
|
|
95
|
+
vmin = portrayal.get("vmin", np.min(data))
|
|
96
|
+
vmax = portrayal.get("vmax", np.max(data))
|
|
97
|
+
colorbar = portrayal.get("colorbar", True)
|
|
98
|
+
|
|
99
|
+
# Draw the layer
|
|
100
|
+
if "color" in portrayal:
|
|
101
|
+
rgba_color = to_rgba(portrayal["color"])
|
|
102
|
+
normalized_data = (data - vmin) / (vmax - vmin)
|
|
103
|
+
rgba_data = np.full((*data.shape, 4), rgba_color)
|
|
104
|
+
rgba_data[..., 3] *= normalized_data * alpha
|
|
105
|
+
rgba_data = np.clip(rgba_data, 0, 1)
|
|
106
|
+
cmap = LinearSegmentedColormap.from_list(
|
|
107
|
+
layer_name, [(0, 0, 0, 0), (*rgba_color[:3], alpha)]
|
|
108
|
+
)
|
|
109
|
+
im = ax.imshow(
|
|
110
|
+
rgba_data.transpose(1, 0, 2),
|
|
111
|
+
extent=(0, width, 0, height),
|
|
112
|
+
origin="lower",
|
|
113
|
+
)
|
|
114
|
+
if colorbar:
|
|
115
|
+
norm = Normalize(vmin=vmin, vmax=vmax)
|
|
116
|
+
sm = ScalarMappable(norm=norm, cmap=cmap)
|
|
117
|
+
sm.set_array([])
|
|
118
|
+
ax.figure.colorbar(sm, ax=ax, orientation="vertical")
|
|
119
|
+
|
|
120
|
+
elif "colormap" in portrayal:
|
|
121
|
+
cmap = portrayal.get("colormap", "viridis")
|
|
122
|
+
if isinstance(cmap, list):
|
|
123
|
+
cmap = LinearSegmentedColormap.from_list(layer_name, cmap)
|
|
124
|
+
im = ax.imshow(
|
|
125
|
+
data.T,
|
|
126
|
+
cmap=cmap,
|
|
127
|
+
alpha=alpha,
|
|
128
|
+
vmin=vmin,
|
|
129
|
+
vmax=vmax,
|
|
130
|
+
extent=(0, width, 0, height),
|
|
131
|
+
origin="lower",
|
|
132
|
+
)
|
|
133
|
+
if colorbar:
|
|
134
|
+
plt.colorbar(im, ax=ax, label=layer_name)
|
|
135
|
+
else:
|
|
136
|
+
raise ValueError(
|
|
137
|
+
f"PropertyLayer {layer_name} portrayal must include 'color' or 'colormap'."
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _draw_grid(space, space_ax, agent_portrayal, propertylayer_portrayal, model):
|
|
142
|
+
if propertylayer_portrayal:
|
|
143
|
+
draw_property_layers(space_ax, space, propertylayer_portrayal, model)
|
|
144
|
+
|
|
145
|
+
agent_data = _get_agent_data(space, agent_portrayal)
|
|
146
|
+
|
|
147
|
+
space_ax.set_xlim(0, space.width)
|
|
148
|
+
space_ax.set_ylim(0, space.height)
|
|
149
|
+
_split_and_scatter(agent_data, space_ax)
|
|
150
|
+
|
|
151
|
+
# Draw grid lines
|
|
152
|
+
for x in range(space.width + 1):
|
|
153
|
+
space_ax.axvline(x, color="gray", linestyle=":")
|
|
154
|
+
for y in range(space.height + 1):
|
|
155
|
+
space_ax.axhline(y, color="gray", linestyle=":")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _get_agent_data(space, agent_portrayal):
|
|
159
|
+
"""Helper function to get agent data for visualization."""
|
|
160
|
+
x, y, s, c, m = [], [], [], [], []
|
|
161
|
+
for agents, pos in space.coord_iter():
|
|
162
|
+
if not agents:
|
|
163
|
+
continue
|
|
164
|
+
if not isinstance(agents, list):
|
|
165
|
+
agents = [agents] # noqa PLW2901
|
|
166
|
+
for agent in agents:
|
|
167
|
+
data = agent_portrayal(agent)
|
|
168
|
+
x.append(pos[0] + 0.5) # Center the agent in the cell
|
|
169
|
+
y.append(pos[1] + 0.5) # Center the agent in the cell
|
|
170
|
+
default_size = (180 / max(space.width, space.height)) ** 2
|
|
171
|
+
s.append(data.get("size", default_size))
|
|
172
|
+
c.append(data.get("color", "tab:blue"))
|
|
173
|
+
m.append(data.get("shape", "o"))
|
|
174
|
+
return {"x": x, "y": y, "s": s, "c": c, "m": m}
|
|
74
175
|
|
|
75
176
|
|
|
76
|
-
def
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
for
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
continue
|
|
88
|
-
if not hasattr(content, "__iter__"):
|
|
89
|
-
# Is a single grid
|
|
90
|
-
content = [content]
|
|
91
|
-
for agent in content:
|
|
92
|
-
data = agent_portrayal(agent)
|
|
93
|
-
x.append(i)
|
|
94
|
-
y.append(j)
|
|
95
|
-
|
|
96
|
-
# This is the default value for the marker size, which auto-scales
|
|
97
|
-
# according to the grid area.
|
|
98
|
-
default_size = (180 / max(g.width, g.height)) ** 2
|
|
99
|
-
# establishing a default prevents misalignment if some agents are not given size, color, etc.
|
|
100
|
-
size = data.get("size", default_size)
|
|
101
|
-
s.append(size)
|
|
102
|
-
color = data.get("color", "tab:blue")
|
|
103
|
-
c.append(color)
|
|
104
|
-
mark = data.get("shape", "o")
|
|
105
|
-
m.append(mark)
|
|
106
|
-
out = {"x": x, "y": y, "s": s, "c": c, "m": m}
|
|
107
|
-
return out
|
|
108
|
-
|
|
109
|
-
space_ax.set_xlim(-1, space.width)
|
|
110
|
-
space_ax.set_ylim(-1, space.height)
|
|
111
|
-
_split_and_scatter(portray(space), space_ax)
|
|
177
|
+
def _split_and_scatter(portray_data, space_ax):
|
|
178
|
+
"""Helper function to split and scatter agent data."""
|
|
179
|
+
for marker in set(portray_data["m"]):
|
|
180
|
+
mask = [m == marker for m in portray_data["m"]]
|
|
181
|
+
space_ax.scatter(
|
|
182
|
+
[x for x, show in zip(portray_data["x"], mask) if show],
|
|
183
|
+
[y for y, show in zip(portray_data["y"], mask) if show],
|
|
184
|
+
s=[s for s, show in zip(portray_data["s"], mask) if show],
|
|
185
|
+
c=[c for c, show in zip(portray_data["c"], mask) if show],
|
|
186
|
+
marker=marker,
|
|
187
|
+
)
|
|
112
188
|
|
|
113
189
|
|
|
114
190
|
def _draw_network_grid(space, space_ax, agent_portrayal):
|
|
@@ -122,7 +198,7 @@ def _draw_network_grid(space, space_ax, agent_portrayal):
|
|
|
122
198
|
)
|
|
123
199
|
|
|
124
200
|
|
|
125
|
-
def _draw_continuous_space(space, space_ax, agent_portrayal):
|
|
201
|
+
def _draw_continuous_space(space, space_ax, agent_portrayal, model):
|
|
126
202
|
def portray(space):
|
|
127
203
|
x = []
|
|
128
204
|
y = []
|
|
@@ -137,15 +213,13 @@ def _draw_continuous_space(space, space_ax, agent_portrayal):
|
|
|
137
213
|
|
|
138
214
|
# This is matplotlib's default marker size
|
|
139
215
|
default_size = 20
|
|
140
|
-
# establishing a default prevents misalignment if some agents are not given size, color, etc.
|
|
141
216
|
size = data.get("size", default_size)
|
|
142
217
|
s.append(size)
|
|
143
218
|
color = data.get("color", "tab:blue")
|
|
144
219
|
c.append(color)
|
|
145
220
|
mark = data.get("shape", "o")
|
|
146
221
|
m.append(mark)
|
|
147
|
-
|
|
148
|
-
return out
|
|
222
|
+
return {"x": x, "y": y, "s": s, "c": c, "m": m}
|
|
149
223
|
|
|
150
224
|
# Determine border style based on space.torus
|
|
151
225
|
border_style = "solid" if not space.torus else (0, (5, 10))
|
|
@@ -184,8 +258,6 @@ def _draw_voronoi(space, space_ax, agent_portrayal):
|
|
|
184
258
|
if "color" in data:
|
|
185
259
|
c.append(data["color"])
|
|
186
260
|
out = {"x": x, "y": y}
|
|
187
|
-
# This is the default value for the marker size, which auto-scales
|
|
188
|
-
# according to the grid area.
|
|
189
261
|
out["s"] = s
|
|
190
262
|
if len(c) > 0:
|
|
191
263
|
out["c"] = c
|
|
@@ -214,10 +286,19 @@ def _draw_voronoi(space, space_ax, agent_portrayal):
|
|
|
214
286
|
alpha=min(1, cell.properties[space.cell_coloring_property]),
|
|
215
287
|
c="red",
|
|
216
288
|
) # Plot filled polygon
|
|
217
|
-
space_ax.plot(*zip(*polygon), color="black") # Plot polygon edges in
|
|
289
|
+
space_ax.plot(*zip(*polygon), color="black") # Plot polygon edges in black
|
|
218
290
|
|
|
219
291
|
|
|
220
292
|
def make_plot_measure(measure: str | dict[str, str] | list[str] | tuple[str]):
|
|
293
|
+
"""Create a plotting function for a specified measure.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
measure (str | dict[str, str] | list[str] | tuple[str]): Measure(s) to plot.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
function: A function that creates a PlotMatplotlib component.
|
|
300
|
+
"""
|
|
301
|
+
|
|
221
302
|
def MakePlotMeasure(model):
|
|
222
303
|
return PlotMatplotlib(model, measure)
|
|
223
304
|
|
|
@@ -226,6 +307,16 @@ def make_plot_measure(measure: str | dict[str, str] | list[str] | tuple[str]):
|
|
|
226
307
|
|
|
227
308
|
@solara.component
|
|
228
309
|
def PlotMatplotlib(model, measure, dependencies: list[any] | None = None):
|
|
310
|
+
"""Create a Matplotlib-based plot for a measure or measures.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
model (mesa.Model): The model instance.
|
|
314
|
+
measure (str | dict[str, str] | list[str] | tuple[str]): Measure(s) to plot.
|
|
315
|
+
dependencies (list[any] | None): Optional dependencies for the plot.
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
solara.FigureMatplotlib: A component for rendering the plot.
|
|
319
|
+
"""
|
|
229
320
|
update_counter.get()
|
|
230
321
|
fig = Figure()
|
|
231
322
|
ax = fig.subplots()
|
|
@@ -242,5 +333,5 @@ def PlotMatplotlib(model, measure, dependencies: list[any] | None = None):
|
|
|
242
333
|
ax.plot(df.loc[:, m], label=m)
|
|
243
334
|
fig.legend()
|
|
244
335
|
# Set integer x axis
|
|
245
|
-
ax.xaxis.set_major_locator(MaxNLocator(integer=True))
|
|
336
|
+
ax.xaxis.set_major_locator(plt.MaxNLocator(integer=True))
|
|
246
337
|
solara.FigureMatplotlib(fig, dependencies=dependencies)
|