Mesa 3.1.5__py3-none-any.whl → 3.2.0.dev0__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 (45) hide show
  1. mesa/__init__.py +3 -1
  2. mesa/agent.py +20 -5
  3. mesa/discrete_space/__init__.py +50 -0
  4. mesa/{experimental/cell_space → discrete_space}/cell.py +29 -10
  5. mesa/{experimental/cell_space → discrete_space}/cell_agent.py +1 -1
  6. mesa/{experimental/cell_space → discrete_space}/cell_collection.py +3 -3
  7. mesa/{experimental/cell_space → discrete_space}/discrete_space.py +65 -3
  8. mesa/{experimental/cell_space → discrete_space}/grid.py +2 -2
  9. mesa/{experimental/cell_space → discrete_space}/network.py +22 -2
  10. mesa/{experimental/cell_space → discrete_space}/property_layer.py +1 -10
  11. mesa/{experimental/cell_space → discrete_space}/voronoi.py +2 -2
  12. mesa/examples/advanced/epstein_civil_violence/agents.py +1 -1
  13. mesa/examples/advanced/epstein_civil_violence/model.py +1 -1
  14. mesa/examples/advanced/pd_grid/agents.py +1 -1
  15. mesa/examples/advanced/pd_grid/model.py +1 -1
  16. mesa/examples/advanced/sugarscape_g1mt/agents.py +1 -1
  17. mesa/examples/advanced/sugarscape_g1mt/model.py +2 -2
  18. mesa/examples/advanced/wolf_sheep/agents.py +1 -1
  19. mesa/examples/advanced/wolf_sheep/app.py +2 -1
  20. mesa/examples/advanced/wolf_sheep/model.py +1 -1
  21. mesa/examples/basic/boid_flockers/agents.py +1 -0
  22. mesa/examples/basic/boid_flockers/app.py +17 -2
  23. mesa/examples/basic/boid_flockers/model.py +12 -0
  24. mesa/examples/basic/boltzmann_wealth_model/agents.py +6 -11
  25. mesa/examples/basic/boltzmann_wealth_model/app.py +2 -2
  26. mesa/examples/basic/boltzmann_wealth_model/model.py +7 -11
  27. mesa/examples/basic/conways_game_of_life/agents.py +13 -5
  28. mesa/examples/basic/conways_game_of_life/model.py +10 -7
  29. mesa/examples/basic/schelling/agents.py +13 -8
  30. mesa/examples/basic/schelling/model.py +6 -9
  31. mesa/examples/basic/virus_on_network/agents.py +13 -17
  32. mesa/examples/basic/virus_on_network/model.py +20 -24
  33. mesa/experimental/__init__.py +2 -2
  34. mesa/experimental/cell_space/__init__.py +18 -8
  35. mesa/space.py +1 -12
  36. mesa/visualization/__init__.py +2 -0
  37. mesa/visualization/command_console.py +482 -0
  38. mesa/visualization/components/altair_components.py +276 -16
  39. mesa/visualization/mpl_space_drawing.py +17 -9
  40. mesa/visualization/solara_viz.py +150 -21
  41. {mesa-3.1.5.dist-info → mesa-3.2.0.dev0.dist-info}/METADATA +12 -8
  42. {mesa-3.1.5.dist-info → mesa-3.2.0.dev0.dist-info}/RECORD +45 -43
  43. {mesa-3.1.5.dist-info → mesa-3.2.0.dev0.dist-info}/WHEEL +0 -0
  44. {mesa-3.1.5.dist-info → mesa-3.2.0.dev0.dist-info}/licenses/LICENSE +0 -0
  45. {mesa-3.1.5.dist-info → mesa-3.2.0.dev0.dist-info}/licenses/NOTICE +0 -0
@@ -3,10 +3,14 @@
3
3
  import warnings
4
4
 
5
5
  import altair as alt
6
+ import numpy as np
7
+ import pandas as pd
6
8
  import solara
9
+ from matplotlib.colors import to_rgb
7
10
 
8
- from mesa.experimental.cell_space import DiscreteSpace, Grid
9
- from mesa.space import ContinuousSpace, _Grid
11
+ import mesa
12
+ from mesa.discrete_space import DiscreteSpace, Grid
13
+ from mesa.space import ContinuousSpace, PropertyLayer, _Grid
10
14
  from mesa.visualization.utils import update_counter
11
15
 
12
16
 
@@ -20,13 +24,16 @@ def make_space_altair(*args, **kwargs): # noqa: D103
20
24
 
21
25
 
22
26
  def make_altair_space(
23
- agent_portrayal, propertylayer_portrayal, post_process, **space_drawing_kwargs
27
+ agent_portrayal,
28
+ propertylayer_portrayal=None,
29
+ post_process=None,
30
+ **space_drawing_kwargs,
24
31
  ):
25
32
  """Create an Altair-based space visualization component.
26
33
 
27
34
  Args:
28
35
  agent_portrayal: Function to portray agents.
29
- propertylayer_portrayal: not yet implemented
36
+ propertylayer_portrayal: Dictionary of PropertyLayer portrayal specifications
30
37
  post_process :A user specified callable that will be called with the Chart instance from Altair. Allows for fine tuning plots (e.g., control ticks)
31
38
  space_drawing_kwargs : not yet implemented
32
39
 
@@ -43,14 +50,23 @@ def make_altair_space(
43
50
  return {"id": a.unique_id}
44
51
 
45
52
  def MakeSpaceAltair(model):
46
- return SpaceAltair(model, agent_portrayal, post_process=post_process)
53
+ return SpaceAltair(
54
+ model,
55
+ agent_portrayal,
56
+ propertylayer_portrayal=propertylayer_portrayal,
57
+ post_process=post_process,
58
+ )
47
59
 
48
60
  return MakeSpaceAltair
49
61
 
50
62
 
51
63
  @solara.component
52
64
  def SpaceAltair(
53
- model, agent_portrayal, dependencies: list[any] | None = None, post_process=None
65
+ model,
66
+ agent_portrayal,
67
+ propertylayer_portrayal=None,
68
+ dependencies: list[any] | None = None,
69
+ post_process=None,
54
70
  ):
55
71
  """Create an Altair-based space visualization component.
56
72
 
@@ -63,10 +79,11 @@ def SpaceAltair(
63
79
  # Sometimes the space is defined as model.space instead of model.grid
64
80
  space = model.space
65
81
 
66
- chart = _draw_grid(space, agent_portrayal)
82
+ chart = _draw_grid(space, agent_portrayal, propertylayer_portrayal)
67
83
  # Apply post-processing if provided
68
84
  if post_process is not None:
69
85
  chart = post_process(chart)
86
+
70
87
  solara.FigureAltair(chart)
71
88
 
72
89
 
@@ -138,7 +155,7 @@ def _get_agent_data_continuous_space(space: ContinuousSpace, agent_portrayal):
138
155
  return all_agent_data
139
156
 
140
157
 
141
- def _draw_grid(space, agent_portrayal):
158
+ def _draw_grid(space, agent_portrayal, propertylayer_portrayal):
142
159
  match space:
143
160
  case Grid():
144
161
  all_agent_data = _get_agent_data_new_discrete_space(space, agent_portrayal)
@@ -168,23 +185,266 @@ def _draw_grid(space, agent_portrayal):
168
185
  }
169
186
  has_color = "color" in all_agent_data[0]
170
187
  if has_color:
171
- encoding_dict["color"] = alt.Color("color", type="nominal")
188
+ unique_colors = list({agent["color"] for agent in all_agent_data})
189
+ encoding_dict["color"] = alt.Color(
190
+ "color:N",
191
+ scale=alt.Scale(domain=unique_colors, range=unique_colors),
192
+ )
172
193
  has_size = "size" in all_agent_data[0]
173
194
  if has_size:
174
195
  encoding_dict["size"] = alt.Size("size", type="quantitative")
175
196
 
176
- chart = (
197
+ agent_chart = (
177
198
  alt.Chart(
178
199
  alt.Data(values=all_agent_data), encoding=alt.Encoding(**encoding_dict)
179
200
  )
180
201
  .mark_point(filled=True)
181
- .properties(width=280, height=280)
182
- # .configure_view(strokeOpacity=0) # hide grid/chart lines
202
+ .properties(width=300, height=300)
183
203
  )
184
- # This is the default value for the marker size, which auto-scales
185
- # according to the grid area.
204
+ base_chart = None
205
+ cbar_chart = None
206
+
207
+ # This is the default value for the marker size, which auto-scales according to the grid area.
186
208
  if not has_size:
187
209
  length = min(space.width, space.height)
188
- chart = chart.mark_point(size=30000 / length**2, filled=True)
210
+ agent_chart = agent_chart.mark_point(size=30000 / length**2, filled=True)
211
+
212
+ if propertylayer_portrayal is not None:
213
+ chart_width = agent_chart.properties().width
214
+ chart_height = agent_chart.properties().height
215
+ base_chart, cbar_chart = chart_property_layers(
216
+ space=space,
217
+ propertylayer_portrayal=propertylayer_portrayal,
218
+ chart_width=chart_width,
219
+ chart_height=chart_height,
220
+ )
221
+
222
+ base_chart = alt.layer(base_chart, agent_chart)
223
+ else:
224
+ base_chart = agent_chart
225
+ if cbar_chart is not None:
226
+ base_chart = alt.vconcat(base_chart, cbar_chart).configure_view(stroke=None)
227
+ return base_chart
228
+
229
+
230
+ def chart_property_layers(space, propertylayer_portrayal, chart_width, chart_height):
231
+ """Creates Property Layers in the Altair Components.
232
+
233
+ Args:
234
+ space: the ContinuousSpace instance
235
+ propertylayer_portrayal:Dictionary of PropertyLayer portrayal specifications
236
+ chart_width: width of the agent chart to maintain consistency with the property charts
237
+ chart_height: height of the agent chart to maintain consistency with the property charts
238
+ agent_chart: the agent chart to layer with the property layers on the grid
239
+ Returns:
240
+ Altair Chart
241
+ """
242
+ try:
243
+ # old style spaces
244
+ property_layers = space.properties
245
+ except AttributeError:
246
+ # new style spaces
247
+ property_layers = space._mesa_property_layers
248
+ base = None
249
+ bar_chart = None
250
+ for layer_name, portrayal in propertylayer_portrayal.items():
251
+ layer = property_layers.get(layer_name, None)
252
+ if not isinstance(
253
+ layer,
254
+ PropertyLayer | mesa.discrete_space.property_layer.PropertyLayer,
255
+ ):
256
+ continue
189
257
 
190
- return chart
258
+ data = layer.data.astype(float) if layer.data.dtype == bool else layer.data
259
+
260
+ if (space.width, space.height) != data.shape:
261
+ warnings.warn(
262
+ f"Layer {layer_name} dimensions ({data.shape}) do not match space dimensions ({space.width}, {space.height}).",
263
+ UserWarning,
264
+ stacklevel=2,
265
+ )
266
+ alpha = portrayal.get("alpha", 1)
267
+ vmin = portrayal.get("vmin", np.min(data))
268
+ vmax = portrayal.get("vmax", np.max(data))
269
+ colorbar = portrayal.get("colorbar", True)
270
+
271
+ # Prepare data for Altair (convert 2D array to a long-form DataFrame)
272
+ df = pd.DataFrame(
273
+ {
274
+ "x": np.repeat(np.arange(data.shape[0]), data.shape[1]),
275
+ "y": np.tile(np.arange(data.shape[1]), data.shape[0]),
276
+ "value": data.flatten(),
277
+ }
278
+ )
279
+
280
+ if "color" in portrayal:
281
+ # Create a function to map values to RGBA colors with proper opacity scaling
282
+ def apply_rgba(val, vmin=vmin, vmax=vmax, alpha=alpha, portrayal=portrayal):
283
+ """Maps data values to RGBA colors with opacity based on value magnitude.
284
+
285
+ Args:
286
+ val: The data value to convert
287
+ vmin: The smallest value for which the color is displayed in the colorbar
288
+ vmax: The largest value for which the color is displayed in the colorbar
289
+ alpha: The opacity of the color
290
+ portrayal: The specifics of the current property layer in the iterative loop
291
+
292
+ Returns:
293
+ String representation of RGBA color
294
+ """
295
+ # Normalize value to range [0,1] and clamp
296
+ normalized = max(0, min((val - vmin) / (vmax - vmin), 1))
297
+
298
+ # Scale opacity by alpha parameter
299
+ opacity = normalized * alpha
300
+
301
+ # Convert color to RGB components
302
+ rgb_color = to_rgb(portrayal["color"])
303
+ r = int(rgb_color[0] * 255)
304
+ g = int(rgb_color[1] * 255)
305
+ b = int(rgb_color[2] * 255)
306
+
307
+ return f"rgba({r}, {g}, {b}, {opacity:.2f})"
308
+
309
+ # Apply color mapping to each value in the dataset
310
+ df["color"] = df["value"].apply(apply_rgba)
311
+
312
+ # Create chart for the property layer
313
+ chart = (
314
+ alt.Chart(df)
315
+ .mark_rect()
316
+ .encode(
317
+ x=alt.X("x:O", axis=None),
318
+ y=alt.Y("y:O", axis=None),
319
+ fill=alt.Fill("color:N", scale=None),
320
+ )
321
+ .properties(width=chart_width, height=chart_height, title=layer_name)
322
+ )
323
+ base = alt.layer(chart, base) if base is not None else chart
324
+
325
+ # Add colorbar if specified in portrayal
326
+ if colorbar:
327
+ # Extract RGB components from base color
328
+ rgb_color = to_rgb(portrayal["color"])
329
+ r_int = int(rgb_color[0] * 255)
330
+ g_int = int(rgb_color[1] * 255)
331
+ b_int = int(rgb_color[2] * 255)
332
+
333
+ # Define gradient endpoints
334
+ min_color = f"rgba({r_int},{g_int},{b_int},0)"
335
+ max_color = f"rgba({r_int},{g_int},{b_int},{alpha:.2f})"
336
+
337
+ # Define colorbar dimensions
338
+ colorbar_height = 20
339
+ colorbar_width = chart_width
340
+
341
+ # Create dataframe for gradient visualization
342
+ df_gradient = pd.DataFrame({"x": [0, 1], "y": [0, 1]})
343
+
344
+ # Create evenly distributed tick values
345
+ axis_values = np.linspace(vmin, vmax, 11)
346
+ tick_positions = np.linspace(0, colorbar_width, 11)
347
+
348
+ # Prepare data for axis and labels
349
+ axis_data = pd.DataFrame({"value": axis_values, "x": tick_positions})
350
+
351
+ # Create colorbar with linear gradient
352
+ colorbar_chart = (
353
+ alt.Chart(df_gradient)
354
+ .mark_rect(
355
+ x=0,
356
+ y=0,
357
+ width=colorbar_width,
358
+ height=colorbar_height,
359
+ color=alt.Gradient(
360
+ gradient="linear",
361
+ stops=[
362
+ alt.GradientStop(color=min_color, offset=0),
363
+ alt.GradientStop(color=max_color, offset=1),
364
+ ],
365
+ x1=0,
366
+ x2=1, # Horizontal gradient
367
+ y1=0,
368
+ y2=0, # Keep y constant
369
+ ),
370
+ )
371
+ .encode(
372
+ x=alt.value(chart_width / 2), # Center colorbar
373
+ y=alt.value(0),
374
+ )
375
+ .properties(width=colorbar_width, height=colorbar_height)
376
+ )
377
+
378
+ # Add tick marks to colorbar
379
+ axis_chart = (
380
+ alt.Chart(axis_data)
381
+ .mark_tick(thickness=2, size=8)
382
+ .encode(x=alt.X("x:Q", axis=None), y=alt.value(colorbar_height - 2))
383
+ )
384
+
385
+ # Add value labels below tick marks
386
+ text_labels = (
387
+ alt.Chart(axis_data)
388
+ .mark_text(baseline="top", fontSize=10, dy=0)
389
+ .encode(
390
+ x=alt.X("x:Q"),
391
+ text=alt.Text("value:Q", format=".1f"),
392
+ y=alt.value(colorbar_height + 10),
393
+ )
394
+ )
395
+
396
+ # Add title to colorbar
397
+ title = (
398
+ alt.Chart(pd.DataFrame([{"text": layer_name}]))
399
+ .mark_text(
400
+ fontSize=12,
401
+ fontWeight="bold",
402
+ baseline="bottom",
403
+ align="center",
404
+ )
405
+ .encode(
406
+ text="text:N",
407
+ x=alt.value(colorbar_width / 2),
408
+ y=alt.value(colorbar_height + 40),
409
+ )
410
+ )
411
+
412
+ # Combine all colorbar components
413
+ combined_colorbar = alt.layer(
414
+ colorbar_chart, axis_chart, text_labels, title
415
+ ).properties(width=colorbar_width, height=colorbar_height + 50)
416
+
417
+ bar_chart = (
418
+ alt.vconcat(bar_chart, combined_colorbar)
419
+ .resolve_scale(color="independent")
420
+ .configure_view(stroke=None)
421
+ if bar_chart is not None
422
+ else combined_colorbar
423
+ )
424
+
425
+ elif "colormap" in portrayal:
426
+ cmap = portrayal.get("colormap", "viridis")
427
+ cmap_scale = alt.Scale(scheme=cmap, domain=[vmin, vmax])
428
+
429
+ chart = (
430
+ alt.Chart(df)
431
+ .mark_rect(opacity=alpha)
432
+ .encode(
433
+ x=alt.X("x:O", axis=None),
434
+ y=alt.Y("y:O", axis=None),
435
+ color=alt.Color(
436
+ "value:Q",
437
+ scale=cmap_scale,
438
+ title=layer_name,
439
+ legend=alt.Legend(title=layer_name) if colorbar else None,
440
+ ),
441
+ )
442
+ .properties(width=chart_width, height=chart_height)
443
+ )
444
+ base = alt.layer(chart, base) if base is not None else chart
445
+
446
+ else:
447
+ raise ValueError(
448
+ f"PropertyLayer {layer_name} portrayal must include 'color' or 'colormap'."
449
+ )
450
+ return base, bar_chart
@@ -24,7 +24,7 @@ from matplotlib.colors import LinearSegmentedColormap, Normalize, to_rgba
24
24
  from matplotlib.patches import Polygon
25
25
 
26
26
  import mesa
27
- from mesa.experimental.cell_space import (
27
+ from mesa.discrete_space import (
28
28
  OrthogonalMooreGrid,
29
29
  OrthogonalVonNeumannGrid,
30
30
  VoronoiGrid,
@@ -40,8 +40,8 @@ from mesa.space import (
40
40
  )
41
41
 
42
42
  OrthogonalGrid = SingleGrid | MultiGrid | OrthogonalMooreGrid | OrthogonalVonNeumannGrid
43
- HexGrid = HexSingleGrid | HexMultiGrid | mesa.experimental.cell_space.HexGrid
44
- Network = NetworkGrid | mesa.experimental.cell_space.Network
43
+ HexGrid = HexSingleGrid | HexMultiGrid | mesa.discrete_space.HexGrid
44
+ Network = NetworkGrid | mesa.discrete_space.Network
45
45
 
46
46
 
47
47
  def collect_agent_data(
@@ -101,7 +101,15 @@ def collect_agent_data(
101
101
  stacklevel=2,
102
102
  )
103
103
 
104
- return {k: np.asarray(v) for k, v in arguments.items()}
104
+ data = {
105
+ k: (np.asarray(v, dtype=object) if k == "marker" else np.asarray(v))
106
+ for k, v in arguments.items()
107
+ }
108
+ # ensures that the tuples in marker dont get converted by numpy to an array resulting in a 2D array
109
+ arr = np.empty(len(arguments["marker"]), dtype=object)
110
+ arr[:] = arguments["marker"]
111
+ data["marker"] = arr
112
+ return data
105
113
 
106
114
 
107
115
  def draw_space(
@@ -133,7 +141,7 @@ def draw_space(
133
141
  # https://stackoverflow.com/questions/67524641/convert-multiple-isinstance-checks-to-structural-pattern-matching
134
142
  match space:
135
143
  # order matters here given the class structure of old-style grid spaces
136
- case HexSingleGrid() | HexMultiGrid() | mesa.experimental.cell_space.HexGrid():
144
+ case HexSingleGrid() | HexMultiGrid() | mesa.discrete_space.HexGrid():
137
145
  draw_hex_grid(space, agent_portrayal, ax=ax, **space_drawing_kwargs)
138
146
  case (
139
147
  mesa.space.SingleGrid()
@@ -142,7 +150,7 @@ def draw_space(
142
150
  | mesa.space.MultiGrid()
143
151
  ):
144
152
  draw_orthogonal_grid(space, agent_portrayal, ax=ax, **space_drawing_kwargs)
145
- case mesa.space.NetworkGrid() | mesa.experimental.cell_space.Network():
153
+ case mesa.space.NetworkGrid() | mesa.discrete_space.Network():
146
154
  draw_network(space, agent_portrayal, ax=ax, **space_drawing_kwargs)
147
155
  case (
148
156
  mesa.space.ContinuousSpace()
@@ -221,7 +229,7 @@ def draw_property_layers(
221
229
  layer = property_layers.get(layer_name, None)
222
230
  if not isinstance(
223
231
  layer,
224
- PropertyLayer | mesa.experimental.cell_space.property_layer.PropertyLayer,
232
+ PropertyLayer | mesa.discrete_space.property_layer.PropertyLayer,
225
233
  ):
226
234
  continue
227
235
 
@@ -638,8 +646,8 @@ def _scatter(ax: Axes, arguments, **kwargs):
638
646
  f"{entry} is specified in agent portrayal and via plotting kwargs, you can only use one or the other"
639
647
  )
640
648
 
641
- for mark in np.unique(marker):
642
- mark_mask = marker == mark
649
+ for mark in set(marker):
650
+ mark_mask = [m == mark for m in list(marker)]
643
651
  for z_order in np.unique(zorder):
644
652
  zorder_mask = z_order == zorder
645
653
  logical = mark_mask & zorder_mask