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.

Files changed (41) hide show
  1. mesa/__init__.py +2 -3
  2. mesa/agent.py +116 -85
  3. mesa/batchrunner.py +22 -23
  4. mesa/cookiecutter-mesa/hooks/post_gen_project.py +2 -0
  5. mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/__init__.py +1 -0
  6. mesa/datacollection.py +138 -27
  7. mesa/experimental/UserParam.py +17 -6
  8. mesa/experimental/__init__.py +2 -0
  9. mesa/experimental/cell_space/__init__.py +14 -1
  10. mesa/experimental/cell_space/cell.py +84 -23
  11. mesa/experimental/cell_space/cell_agent.py +117 -21
  12. mesa/experimental/cell_space/cell_collection.py +54 -17
  13. mesa/experimental/cell_space/discrete_space.py +79 -7
  14. mesa/experimental/cell_space/grid.py +19 -8
  15. mesa/experimental/cell_space/network.py +9 -7
  16. mesa/experimental/cell_space/voronoi.py +26 -33
  17. mesa/experimental/components/altair.py +10 -0
  18. mesa/experimental/components/matplotlib.py +18 -0
  19. mesa/experimental/devs/__init__.py +2 -0
  20. mesa/experimental/devs/eventlist.py +36 -15
  21. mesa/experimental/devs/examples/epstein_civil_violence.py +65 -29
  22. mesa/experimental/devs/examples/wolf_sheep.py +40 -35
  23. mesa/experimental/devs/simulator.py +55 -15
  24. mesa/experimental/solara_viz.py +10 -19
  25. mesa/main.py +6 -4
  26. mesa/model.py +51 -54
  27. mesa/space.py +145 -120
  28. mesa/time.py +57 -67
  29. mesa/visualization/UserParam.py +19 -6
  30. mesa/visualization/__init__.py +3 -2
  31. mesa/visualization/components/altair.py +4 -2
  32. mesa/visualization/components/matplotlib.py +176 -85
  33. mesa/visualization/solara_viz.py +167 -84
  34. mesa/visualization/utils.py +3 -1
  35. {mesa-3.0.0a4.dist-info → mesa-3.0.0b0.dist-info}/METADATA +55 -13
  36. mesa-3.0.0b0.dist-info/RECORD +45 -0
  37. mesa-3.0.0b0.dist-info/licenses/LICENSE +202 -0
  38. mesa-3.0.0a4.dist-info/licenses/LICENSE → mesa-3.0.0b0.dist-info/licenses/NOTICE +2 -2
  39. mesa-3.0.0a4.dist-info/RECORD +0 -44
  40. {mesa-3.0.0a4.dist-info → mesa-3.0.0b0.dist-info}/WHEEL +0 -0
  41. {mesa-3.0.0a4.dist-info → mesa-3.0.0b0.dist-info}/entry_points.txt +0 -0
@@ -1,114 +1,190 @@
1
- from collections import defaultdict
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(model, agent_portrayal, dependencies: list[any] | None = None):
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
- # Sometimes the space is defined as model.space instead of model.grid
33
- space = model.space
34
- if isinstance(space, mesa.space.NetworkGrid):
35
- _draw_network_grid(space, space_ax, agent_portrayal)
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
- else:
41
- _draw_grid(space, space_ax, agent_portrayal)
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
- # matplotlib scatter does not allow for multiple shapes in one call
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
- # Plot each group with the same marker
72
- for marker, data in grouped_data.items():
73
- space_ax.scatter(data["x"], data["y"], s=data["s"], c=data["c"], marker=marker)
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 _draw_grid(space, space_ax, agent_portrayal):
77
- def portray(g):
78
- x = []
79
- y = []
80
- s = [] # size
81
- c = [] # color
82
- m = [] # shape
83
- for i in range(g.width):
84
- for j in range(g.height):
85
- content = g._grid[i][j]
86
- if not content:
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
- out = {"x": x, "y": y, "s": s, "c": c, "m": m}
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 red
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)