Mesa 3.0.0b0__py3-none-any.whl → 3.0.0b2__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 (85) hide show
  1. mesa/__init__.py +2 -1
  2. mesa/agent.py +37 -27
  3. mesa/examples/README.md +37 -0
  4. mesa/examples/__init__.py +21 -0
  5. mesa/examples/advanced/__init__.py +0 -0
  6. mesa/examples/advanced/epstein_civil_violence/Epstein Civil Violence.ipynb +116 -0
  7. mesa/examples/advanced/epstein_civil_violence/Readme.md +34 -0
  8. mesa/examples/advanced/epstein_civil_violence/__init__.py +0 -0
  9. mesa/examples/advanced/epstein_civil_violence/agents.py +158 -0
  10. mesa/examples/advanced/epstein_civil_violence/app.py +72 -0
  11. mesa/examples/advanced/epstein_civil_violence/model.py +146 -0
  12. mesa/examples/advanced/pd_grid/Readme.md +43 -0
  13. mesa/examples/advanced/pd_grid/__init__.py +0 -0
  14. mesa/examples/advanced/pd_grid/agents.py +50 -0
  15. mesa/examples/advanced/pd_grid/analysis.ipynb +228 -0
  16. mesa/examples/advanced/pd_grid/app.py +50 -0
  17. mesa/examples/advanced/pd_grid/model.py +71 -0
  18. mesa/examples/advanced/sugarscape_g1mt/Readme.md +64 -0
  19. mesa/examples/advanced/sugarscape_g1mt/__init__.py +0 -0
  20. mesa/examples/advanced/sugarscape_g1mt/agents.py +344 -0
  21. mesa/examples/advanced/sugarscape_g1mt/app.py +70 -0
  22. mesa/examples/advanced/sugarscape_g1mt/model.py +180 -0
  23. mesa/examples/advanced/sugarscape_g1mt/sugar-map.txt +50 -0
  24. mesa/examples/advanced/sugarscape_g1mt/tests.py +69 -0
  25. mesa/examples/advanced/wolf_sheep/Readme.md +57 -0
  26. mesa/examples/advanced/wolf_sheep/__init__.py +0 -0
  27. mesa/examples/advanced/wolf_sheep/agents.py +102 -0
  28. mesa/examples/advanced/wolf_sheep/app.py +77 -0
  29. mesa/examples/advanced/wolf_sheep/model.py +137 -0
  30. mesa/examples/basic/__init__.py +0 -0
  31. mesa/examples/basic/boid_flockers/Readme.md +22 -0
  32. mesa/examples/basic/boid_flockers/__init__.py +0 -0
  33. mesa/examples/basic/boid_flockers/agents.py +71 -0
  34. mesa/examples/basic/boid_flockers/app.py +58 -0
  35. mesa/examples/basic/boid_flockers/model.py +69 -0
  36. mesa/examples/basic/boltzmann_wealth_model/Readme.md +56 -0
  37. mesa/examples/basic/boltzmann_wealth_model/__init__.py +0 -0
  38. mesa/examples/basic/boltzmann_wealth_model/agents.py +31 -0
  39. mesa/examples/basic/boltzmann_wealth_model/app.py +65 -0
  40. mesa/examples/basic/boltzmann_wealth_model/model.py +43 -0
  41. mesa/examples/basic/boltzmann_wealth_model/st_app.py +115 -0
  42. mesa/examples/basic/conways_game_of_life/Readme.md +39 -0
  43. mesa/examples/basic/conways_game_of_life/__init__.py +0 -0
  44. mesa/examples/basic/conways_game_of_life/agents.py +47 -0
  45. mesa/examples/basic/conways_game_of_life/app.py +39 -0
  46. mesa/examples/basic/conways_game_of_life/model.py +31 -0
  47. mesa/examples/basic/conways_game_of_life/st_app.py +72 -0
  48. mesa/examples/basic/schelling/Readme.md +40 -0
  49. mesa/examples/basic/schelling/__init__.py +0 -0
  50. mesa/examples/basic/schelling/agents.py +26 -0
  51. mesa/examples/basic/schelling/analysis.ipynb +205 -0
  52. mesa/examples/basic/schelling/app.py +42 -0
  53. mesa/examples/basic/schelling/model.py +59 -0
  54. mesa/examples/basic/virus_on_network/Readme.md +61 -0
  55. mesa/examples/basic/virus_on_network/__init__.py +0 -0
  56. mesa/examples/basic/virus_on_network/agents.py +69 -0
  57. mesa/examples/basic/virus_on_network/app.py +136 -0
  58. mesa/examples/basic/virus_on_network/model.py +96 -0
  59. mesa/experimental/__init__.py +8 -2
  60. mesa/experimental/cell_space/cell.py +9 -0
  61. mesa/experimental/cell_space/discrete_space.py +13 -1
  62. mesa/experimental/cell_space/grid.py +13 -0
  63. mesa/experimental/cell_space/network.py +3 -0
  64. mesa/experimental/devs/eventlist.py +6 -0
  65. mesa/model.py +76 -12
  66. mesa/space.py +70 -5
  67. mesa/time.py +5 -3
  68. mesa/visualization/components/altair.py +87 -19
  69. mesa/visualization/components/matplotlib.py +65 -16
  70. mesa/visualization/solara_viz.py +13 -58
  71. {mesa-3.0.0b0.dist-info → mesa-3.0.0b2.dist-info}/METADATA +1 -3
  72. mesa-3.0.0b2.dist-info/RECORD +93 -0
  73. mesa/cookiecutter-mesa/cookiecutter.json +0 -8
  74. mesa/cookiecutter-mesa/hooks/post_gen_project.py +0 -13
  75. mesa/cookiecutter-mesa/{{cookiecutter.snake}}/README.md +0 -4
  76. mesa/cookiecutter-mesa/{{cookiecutter.snake}}/app.pytemplate +0 -27
  77. mesa/cookiecutter-mesa/{{cookiecutter.snake}}/setup.pytemplate +0 -11
  78. mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/__init__.py +0 -1
  79. mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/model.pytemplate +0 -60
  80. mesa/main.py +0 -65
  81. mesa-3.0.0b0.dist-info/RECORD +0 -45
  82. {mesa-3.0.0b0.dist-info → mesa-3.0.0b2.dist-info}/WHEEL +0 -0
  83. {mesa-3.0.0b0.dist-info → mesa-3.0.0b2.dist-info}/entry_points.txt +0 -0
  84. {mesa-3.0.0b0.dist-info → mesa-3.0.0b2.dist-info}/licenses/LICENSE +0 -0
  85. {mesa-3.0.0b0.dist-info → mesa-3.0.0b2.dist-info}/licenses/NOTICE +0 -0
@@ -7,6 +7,7 @@ from functools import cached_property
7
7
  from random import Random
8
8
  from typing import Any, Generic, TypeVar
9
9
 
10
+ from mesa.agent import AgentSet
10
11
  from mesa.experimental.cell_space.cell import Cell
11
12
  from mesa.experimental.cell_space.cell_collection import CellCollection
12
13
  from mesa.space import PropertyLayer
@@ -22,7 +23,7 @@ class DiscreteSpace(Generic[T]):
22
23
  all_cells (CellCollection): The cells composing the discrete space
23
24
  random (Random): The random number generator
24
25
  cell_klass (Type) : the type of cell class
25
- empties (CellCollection) : collecction of all cells that are empty
26
+ empties (CellCollection) : collection of all cells that are empty
26
27
  property_layers (dict[str, PropertyLayer]): the property layers of the discrete space
27
28
  """
28
29
 
@@ -55,6 +56,12 @@ class DiscreteSpace(Generic[T]):
55
56
  def cutoff_empties(self): # noqa
56
57
  return 7.953 * len(self._cells) ** 0.384
57
58
 
59
+ @property
60
+ def agents(self) -> AgentSet:
61
+ """Return an AgentSet with the agents in the space."""
62
+ return AgentSet(self.all_cells.agents, random=self.random)
63
+
64
+ def _connect_cells(self): ...
58
65
  def _connect_single_cell(self, cell: T): ...
59
66
 
60
67
  @cached_property
@@ -134,3 +141,8 @@ class DiscreteSpace(Generic[T]):
134
141
  condition: a function that takes a cell and returns a boolean (used to filter cells)
135
142
  """
136
143
  self.property_layers[property_name].modify_cells(operation, value, condition)
144
+
145
+ def __setstate__(self, state):
146
+ """Set the state of the discrete space and rebuild the connections."""
147
+ self.__dict__ = state
148
+ self._connect_cells()
@@ -22,8 +22,21 @@ class Grid(DiscreteSpace[T], Generic[T]):
22
22
  random (Random): the random number generator
23
23
  _try_random (bool): whether to get empty cell be repeatedly trying random cell
24
24
 
25
+ Notes:
26
+ width and height are accessible via properties, higher dimensions can be retrieved via dimensions
27
+
25
28
  """
26
29
 
30
+ @property
31
+ def width(self) -> int:
32
+ """Convenience access to the width of the grid."""
33
+ return self.dimensions[0]
34
+
35
+ @property
36
+ def height(self) -> int:
37
+ """Convenience access to the height of the grid."""
38
+ return self.dimensions[1]
39
+
27
40
  def __init__(
28
41
  self,
29
42
  dimensions: Sequence[int],
@@ -34,6 +34,9 @@ class Network(DiscreteSpace[Cell]):
34
34
  node_id, capacity, random=self.random
35
35
  )
36
36
 
37
+ self._connect_cells()
38
+
39
+ def _connect_cells(self) -> None:
37
40
  for cell in self.all_cells:
38
41
  self._connect_single_cell(cell)
39
42
 
@@ -33,6 +33,12 @@ class SimulationEvent:
33
33
  function_args (list[Any]): Argument for the function
34
34
  function_kwargs (Dict[str, Any]): Keyword arguments for the function
35
35
 
36
+
37
+ Notes:
38
+ simulation events use a weak reference to the callable. Therefore, you cannot pass a lambda function in fn.
39
+ A simulation event where the callable no longer exists (e.g., because the agent has been removed from the model)
40
+ will fail silently.
41
+
36
42
  """
37
43
 
38
44
  _ids = itertools.count()
mesa/model.py CHANGED
@@ -8,14 +8,21 @@ Core Objects: Model
8
8
  from __future__ import annotations
9
9
 
10
10
  import random
11
+ import sys
11
12
  import warnings
13
+ from collections.abc import Sequence
12
14
 
13
15
  # mypy
14
16
  from typing import Any
15
17
 
18
+ import numpy as np
19
+
16
20
  from mesa.agent import Agent, AgentSet
17
21
  from mesa.datacollection import DataCollector
18
22
 
23
+ SeedLike = int | np.integer | Sequence[int] | np.random.SeedSequence
24
+ RNGLike = np.random.Generator | np.random.BitGenerator
25
+
19
26
 
20
27
  class Model:
21
28
  """Base class for models in the Mesa ABM library.
@@ -28,7 +35,8 @@ class Model:
28
35
  running: A boolean indicating if the model should continue running.
29
36
  schedule: An object to manage the order and execution of agent steps.
30
37
  steps: the number of times `model.step()` has been called.
31
- random: a seeded random number generator.
38
+ random: a seeded python.random number generator.
39
+ rng : a seeded numpy.random.Generator
32
40
 
33
41
  Notes:
34
42
  Model.agents returns the AgentSet containing all agents registered with the model. Changing
@@ -37,7 +45,13 @@ class Model:
37
45
 
38
46
  """
39
47
 
40
- def __init__(self, *args: Any, seed: float | None = None, **kwargs: Any) -> None:
48
+ def __init__(
49
+ self,
50
+ *args: Any,
51
+ seed: float | None = None,
52
+ rng: RNGLike | SeedLike | None = None,
53
+ **kwargs: Any,
54
+ ) -> None:
41
55
  """Create a new model.
42
56
 
43
57
  Overload this method with the actual code to initialize the model. Always start with super().__init__()
@@ -46,25 +60,51 @@ class Model:
46
60
  Args:
47
61
  args: arguments to pass onto super
48
62
  seed: the seed for the random number generator
63
+ rng : Pseudorandom number generator state. When `rng` is None, a new `numpy.random.Generator` is created
64
+ using entropy from the operating system. Types other than `numpy.random.Generator` are passed to
65
+ `numpy.random.default_rng` to instantiate a `Generator`.
49
66
  kwargs: keyword arguments to pass onto super
67
+
68
+ Notes:
69
+ you have to pass either seed or rng, but not both.
70
+
50
71
  """
51
72
  super().__init__(*args, **kwargs)
52
73
  self.running = True
53
74
  self.steps: int = 0
54
75
 
55
- self._setup_agent_registration()
56
-
57
- self._seed = seed
58
- if self._seed is None:
59
- # We explicitly specify the seed here so that we know its value in
60
- # advance.
61
- self._seed = random.random()
62
- self.random = random.Random(self._seed)
76
+ if (seed is not None) and (rng is not None):
77
+ raise ValueError("you have to pass either rng or seed, not both")
78
+ elif seed is None:
79
+ self.rng: np.random.Generator = np.random.default_rng(rng)
80
+ self._rng = (
81
+ self.rng.bit_generator.state
82
+ ) # this allows for reproducing the rng
83
+
84
+ try:
85
+ self.random = random.Random(rng)
86
+ except TypeError:
87
+ seed = int(self.rng.integers(np.iinfo(np.int32).max))
88
+ self.random = random.Random(seed)
89
+ self._seed = seed # this allows for reproducing stdlib.random
90
+ elif rng is None:
91
+ self.random = random.Random(seed)
92
+ self._seed = seed # this allows for reproducing stdlib.random
93
+
94
+ try:
95
+ self.rng: np.random.Generator = np.random.default_rng(rng)
96
+ except TypeError:
97
+ rng = self.random.randint(0, sys.maxsize)
98
+ self.rng: np.random.Generator = np.random.default_rng(rng)
99
+ self._rng = self.rng.bit_generator.state
63
100
 
64
101
  # Wrap the user-defined step method
65
102
  self._user_step = self.step
66
103
  self.step = self._wrapped_step
67
104
 
105
+ # setup agent registration data structures
106
+ self._setup_agent_registration()
107
+
68
108
  def _wrapped_step(self, *args: Any, **kwargs: Any) -> None:
69
109
  """Automatically increments time and steps after calling the user's step method."""
70
110
  # Automatically increment time and step counters
@@ -119,7 +159,9 @@ class Model:
119
159
  self._agents_by_type: dict[
120
160
  type[Agent], AgentSet
121
161
  ] = {} # a dict with an agentset for each class of agents
122
- self._all_agents = AgentSet([], self) # an agenset with all agents
162
+ self._all_agents = AgentSet(
163
+ [], random=self.random
164
+ ) # an agenset with all agents
123
165
 
124
166
  def register_agent(self, agent):
125
167
  """Register the agent with the model.
@@ -153,7 +195,7 @@ class Model:
153
195
  [
154
196
  agent,
155
197
  ],
156
- self,
198
+ random=self.random,
157
199
  )
158
200
 
159
201
  self._all_agents.add(agent)
@@ -194,6 +236,15 @@ class Model:
194
236
  self.random.seed(seed)
195
237
  self._seed = seed
196
238
 
239
+ def reset_rng(self, rng: RNGLike | SeedLike | None = None) -> None:
240
+ """Reset the model random number generator.
241
+
242
+ Args:
243
+ rng: A new seed for the RNG; if None, reset using the current seed
244
+ """
245
+ self.rng = np.random.default_rng(rng)
246
+ self._rng = self.rng.bit_generator.state
247
+
197
248
  def initialize_data_collector(
198
249
  self,
199
250
  model_reporters=None,
@@ -225,3 +276,16 @@ class Model:
225
276
  )
226
277
  # Collect data for the first time during initialization.
227
278
  self.datacollector.collect(self)
279
+
280
+ def remove_all_agents(self):
281
+ """Remove all agents from the model.
282
+
283
+ Notes:
284
+ This method calls agent.remove for all agents in the model. If you need to remove agents from
285
+ e.g., a SingleGrid, you can either explicitly implement your own agent.remove method or clean this up
286
+ near where you are calling this method.
287
+
288
+ """
289
+ # we need to wrap keys in a list to avoid a RunTimeError: dictionary changed size during iteration
290
+ for agent in list(self._agents.keys()):
291
+ agent.remove()
mesa/space.py CHANGED
@@ -2,10 +2,21 @@
2
2
 
3
3
  Objects used to add a spatial component to a model.
4
4
 
5
- * Grid: base grid, which creates a rectangular grid.
6
- * SingleGrid: extension to Grid which strictly enforces one agent per cell.
7
- * MultiGrid: extension to Grid where each cell can contain a set of agents.
8
- * HexGrid: extension to Grid to handle hexagonal neighbors.
5
+ .. note::
6
+ All Grid classes (:class:`_Grid`, :class:`SingleGrid`, :class:`MultiGrid`,
7
+ :class:`HexGrid`, etc.) are now in maintenance-only mode. While these classes remain
8
+ fully supported, new development occurs in the experimental cell space module
9
+ (:mod:`mesa.experimental.cell_space`).
10
+
11
+ The :class:`PropertyLayer` and :class:`ContinuousSpace` classes remain fully supported
12
+ and actively developed.
13
+
14
+ Classes
15
+ -------
16
+ * PropertyLayer: A data layer that can be added to Grids to store cell properties
17
+ * SingleGrid: a Grid which strictly enforces one agent per cell.
18
+ * MultiGrid: a Grid where each cell can contain a set of agents.
19
+ * HexGrid: a Grid to handle hexagonal neighbors.
9
20
  * ContinuousSpace: a two-dimensional space where each agent has an arbitrary position of `float`'s.
10
21
  * NetworkGrid: a network where each node contains zero or more agents.
11
22
  """
@@ -32,7 +43,7 @@ import numpy as np
32
43
  import numpy.typing as npt
33
44
 
34
45
  # For Mypy
35
- from .agent import Agent
46
+ from .agent import Agent, AgentSet
36
47
 
37
48
  # for better performance, we calculate the tuple to use in the is_integer function
38
49
  _types_integer = (int, np.integer)
@@ -153,6 +164,26 @@ class _Grid:
153
164
  @overload
154
165
  def __getitem__(self, index: int | Sequence[Coordinate]) -> list[GridContent]: ...
155
166
 
167
+ @property
168
+ def agents(self) -> AgentSet:
169
+ """Return an AgentSet with the agents in the space."""
170
+ agents = []
171
+ for entry in self:
172
+ if not entry:
173
+ continue
174
+ if not isinstance(entry, list):
175
+ entry = [entry] # noqa PLW2901
176
+ for agent in entry:
177
+ agents.append(agent)
178
+
179
+ # getting the rng is a bit hacky because old style spaces don't have the rng
180
+ try:
181
+ rng = agents[0].random
182
+ except IndexError:
183
+ # there are no agents in the space
184
+ rng = None
185
+ return AgentSet(agents, random=rng)
186
+
156
187
  @overload
157
188
  def __getitem__(
158
189
  self, index: tuple[int | slice, int | slice]
@@ -1333,6 +1364,19 @@ class ContinuousSpace:
1333
1364
  self._index_to_agent: dict[int, Agent] = {}
1334
1365
  self._agent_to_index: dict[Agent, int | None] = {}
1335
1366
 
1367
+ @property
1368
+ def agents(self) -> AgentSet:
1369
+ """Return an AgentSet with the agents in the space."""
1370
+ agents = list(self._agent_to_index)
1371
+
1372
+ # getting the rng is a bit hacky because old style spaces don't have the rng
1373
+ try:
1374
+ rng = agents[0].random
1375
+ except IndexError:
1376
+ # there are no agents in the space
1377
+ rng = None
1378
+ return AgentSet(agents, random=rng)
1379
+
1336
1380
  def _build_agent_cache(self):
1337
1381
  """Cache agents positions to speed up neighbors calculations."""
1338
1382
  self._index_to_agent = {}
@@ -1506,6 +1550,27 @@ class NetworkGrid:
1506
1550
  for node_id in self.G.nodes:
1507
1551
  g.nodes[node_id]["agent"] = self.default_val()
1508
1552
 
1553
+ @property
1554
+ def agents(self) -> AgentSet:
1555
+ """Return an AgentSet with the agents in the space."""
1556
+ agents = []
1557
+ for node_id in self.G.nodes:
1558
+ entry = self.G.nodes[node_id]["agent"]
1559
+ if not entry:
1560
+ continue
1561
+ if not isinstance(entry, list):
1562
+ entry = [entry]
1563
+ for agent in entry:
1564
+ agents.append(agent)
1565
+
1566
+ # getting the rng is a bit hacky because old style spaces don't have the rng
1567
+ try:
1568
+ rng = agents[0].random
1569
+ except IndexError:
1570
+ # there are no agents in the space
1571
+ rng = None
1572
+ return AgentSet(agents, random=rng)
1573
+
1509
1574
  @staticmethod
1510
1575
  def default_val() -> list:
1511
1576
  """Default value for a new node."""
mesa/time.py CHANGED
@@ -77,7 +77,7 @@ class BaseScheduler:
77
77
  if agents is None:
78
78
  agents = []
79
79
 
80
- self._agents: AgentSet = AgentSet(agents, model)
80
+ self._agents: AgentSet = AgentSet(agents, model.random)
81
81
 
82
82
  self._remove_warning_given = False
83
83
  self._agents_key_warning_given = False
@@ -312,7 +312,9 @@ class RandomActivationByType(BaseScheduler):
312
312
  try:
313
313
  self._agents_by_type[type(agent)].add(agent)
314
314
  except KeyError:
315
- self._agents_by_type[type(agent)] = AgentSet([agent], self.model)
315
+ self._agents_by_type[type(agent)] = AgentSet(
316
+ [agent], self.model.random
317
+ )
316
318
 
317
319
  def add(self, agent: Agent) -> None:
318
320
  """Add an Agent object to the schedule.
@@ -325,7 +327,7 @@ class RandomActivationByType(BaseScheduler):
325
327
  try:
326
328
  self._agents_by_type[type(agent)].add(agent)
327
329
  except KeyError:
328
- self._agents_by_type[type(agent)] = AgentSet([agent], self.model)
330
+ self._agents_by_type[type(agent)] = AgentSet([agent], self.model.random)
329
331
 
330
332
  def remove(self, agent: Agent) -> None:
331
333
  """Remove all instances of a given agent from the schedule.
@@ -7,6 +7,8 @@ import solara
7
7
  with contextlib.suppress(ImportError):
8
8
  import altair as alt
9
9
 
10
+ from mesa.experimental.cell_space import DiscreteSpace, Grid
11
+ from mesa.space import ContinuousSpace, _Grid
10
12
  from mesa.visualization.utils import update_counter
11
13
 
12
14
 
@@ -29,35 +31,101 @@ def SpaceAltair(model, agent_portrayal, dependencies: list[any] | None = None):
29
31
  if space is None:
30
32
  # Sometimes the space is defined as model.space instead of model.grid
31
33
  space = model.space
34
+
32
35
  chart = _draw_grid(space, agent_portrayal)
33
36
  solara.FigureAltair(chart)
34
37
 
35
38
 
39
+ def _get_agent_data_old__discrete_space(space, agent_portrayal):
40
+ """Format agent portrayal data for old-style discrete spaces.
41
+
42
+ Args:
43
+ space: the mesa.space._Grid instance
44
+ agent_portrayal: the agent portrayal callable
45
+
46
+ Returns:
47
+ list of dicts
48
+
49
+ """
50
+ all_agent_data = []
51
+ for content, (x, y) in space.coord_iter():
52
+ if not content:
53
+ continue
54
+ if not hasattr(content, "__iter__"):
55
+ # Is a single grid
56
+ content = [content] # noqa: PLW2901
57
+ for agent in content:
58
+ # use all data from agent portrayal, and add x,y coordinates
59
+ agent_data = agent_portrayal(agent)
60
+ agent_data["x"] = x
61
+ agent_data["y"] = y
62
+ all_agent_data.append(agent_data)
63
+ return all_agent_data
64
+
65
+
66
+ def _get_agent_data_new_discrete_space(space: DiscreteSpace, agent_portrayal):
67
+ """Format agent portrayal data for new-style discrete spaces.
68
+
69
+ Args:
70
+ space: the mesa.experiment.cell_space.Grid instance
71
+ agent_portrayal: the agent portrayal callable
72
+
73
+ Returns:
74
+ list of dicts
75
+
76
+ """
77
+ all_agent_data = []
78
+
79
+ for cell in space.all_cells:
80
+ for agent in cell.agents:
81
+ agent_data = agent_portrayal(agent)
82
+ agent_data["x"] = cell.coordinate[0]
83
+ agent_data["y"] = cell.coordinate[1]
84
+ all_agent_data.append(agent_data)
85
+ return all_agent_data
86
+
87
+
88
+ def _get_agent_data_continuous_space(space: ContinuousSpace, agent_portrayal):
89
+ """Format agent portrayal data for continuous space.
90
+
91
+ Args:
92
+ space: the ContinuousSpace instance
93
+ agent_portrayal: the agent portrayal callable
94
+
95
+ Returns:
96
+ list of dicts
97
+ """
98
+ all_agent_data = []
99
+ for agent in space._agent_to_index:
100
+ agent_data = agent_portrayal(agent)
101
+ agent_data["x"] = agent.pos[0]
102
+ agent_data["y"] = agent.pos[1]
103
+ all_agent_data.append(agent_data)
104
+ return all_agent_data
105
+
106
+
36
107
  def _draw_grid(space, agent_portrayal):
37
- def portray(g):
38
- all_agent_data = []
39
- for content, (x, y) in g.coord_iter():
40
- if not content:
41
- continue
42
- if not hasattr(content, "__iter__"):
43
- # Is a single grid
44
- content = [content] # noqa: PLW2901
45
- for agent in content:
46
- # use all data from agent portrayal, and add x,y coordinates
47
- agent_data = agent_portrayal(agent)
48
- agent_data["x"] = x
49
- agent_data["y"] = y
50
- all_agent_data.append(agent_data)
51
- return all_agent_data
52
-
53
- all_agent_data = portray(space)
108
+ match space:
109
+ case Grid():
110
+ all_agent_data = _get_agent_data_new_discrete_space(space, agent_portrayal)
111
+ case _Grid():
112
+ all_agent_data = _get_agent_data_old__discrete_space(space, agent_portrayal)
113
+ case ContinuousSpace():
114
+ all_agent_data = _get_agent_data_continuous_space(space, agent_portrayal)
115
+ case _:
116
+ raise NotImplementedError(
117
+ f"visualizing {type(space)} is currently not supported through altair"
118
+ )
119
+
54
120
  invalid_tooltips = ["color", "size", "x", "y"]
55
121
 
122
+ x_y_type = "ordinal" if not isinstance(space, ContinuousSpace) else "nominal"
123
+
56
124
  encoding_dict = {
57
125
  # no x-axis label
58
- "x": alt.X("x", axis=None, type="ordinal"),
126
+ "x": alt.X("x", axis=None, type=x_y_type),
59
127
  # no y-axis label
60
- "y": alt.Y("y", axis=None, type="ordinal"),
128
+ "y": alt.Y("y", axis=None, type=x_y_type),
61
129
  "tooltip": [
62
130
  alt.Tooltip(key, type=alt.utils.infer_vegalite_type([value]))
63
131
  for key, value in all_agent_data[0].items()
@@ -11,7 +11,7 @@ from matplotlib.colors import LinearSegmentedColormap, Normalize, to_rgba
11
11
  from matplotlib.figure import Figure
12
12
 
13
13
  import mesa
14
- from mesa.experimental.cell_space import VoronoiGrid
14
+ from mesa.experimental.cell_space import Grid, VoronoiGrid
15
15
  from mesa.space import PropertyLayer
16
16
  from mesa.visualization.utils import update_counter
17
17
 
@@ -52,18 +52,26 @@ def SpaceMatplotlib(
52
52
  if space is None:
53
53
  space = getattr(model, "space", None)
54
54
 
55
- if isinstance(space, mesa.space._Grid):
56
- _draw_grid(space, space_ax, agent_portrayal, propertylayer_portrayal, model)
57
- elif isinstance(space, mesa.space.ContinuousSpace):
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)
61
- elif isinstance(space, VoronoiGrid):
62
- _draw_voronoi(space, space_ax, agent_portrayal)
63
- elif space is None and propertylayer_portrayal:
64
- draw_property_layers(space_ax, space, propertylayer_portrayal, model)
65
-
66
- solara.FigureMatplotlib(space_fig, format="png", dependencies=dependencies)
55
+ # https://stackoverflow.com/questions/67524641/convert-multiple-isinstance-checks-to-structural-pattern-matching
56
+ match space:
57
+ case mesa.space._Grid():
58
+ _draw_grid(space, space_ax, agent_portrayal, propertylayer_portrayal, model)
59
+ case mesa.space.ContinuousSpace():
60
+ _draw_continuous_space(space, space_ax, agent_portrayal, model)
61
+ case mesa.space.NetworkGrid():
62
+ _draw_network_grid(space, space_ax, agent_portrayal)
63
+ case VoronoiGrid():
64
+ _draw_voronoi(space, space_ax, agent_portrayal)
65
+ case Grid(): # matches OrthogonalMooreGrid, OrthogonalVonNeumannGrid, and Hexgrid
66
+ # fixme add a separate draw method for hexgrids in the future
67
+ _draw_discrete_space_grid(space, space_ax, agent_portrayal)
68
+ case None:
69
+ if propertylayer_portrayal:
70
+ draw_property_layers(space_ax, space, propertylayer_portrayal, model)
71
+
72
+ solara.FigureMatplotlib(
73
+ space_fig, format="png", bbox_inches="tight", dependencies=dependencies
74
+ )
67
75
 
68
76
 
69
77
  def draw_property_layers(ax, space, propertylayer_portrayal, model):
@@ -289,6 +297,44 @@ def _draw_voronoi(space, space_ax, agent_portrayal):
289
297
  space_ax.plot(*zip(*polygon), color="black") # Plot polygon edges in black
290
298
 
291
299
 
300
+ def _draw_discrete_space_grid(space: Grid, space_ax, agent_portrayal):
301
+ if space._ndims != 2:
302
+ raise ValueError("Space must be 2D")
303
+
304
+ def portray(g):
305
+ x = []
306
+ y = []
307
+ s = [] # size
308
+ c = [] # color
309
+
310
+ for cell in g.all_cells:
311
+ for agent in cell.agents:
312
+ data = agent_portrayal(agent)
313
+ x.append(cell.coordinate[0])
314
+ y.append(cell.coordinate[1])
315
+ if "size" in data:
316
+ s.append(data["size"])
317
+ if "color" in data:
318
+ c.append(data["color"])
319
+ out = {"x": x, "y": y}
320
+ out["s"] = s
321
+ if len(c) > 0:
322
+ out["c"] = c
323
+
324
+ return out
325
+
326
+ space_ax.set_xlim(0, space.width)
327
+ space_ax.set_ylim(0, space.height)
328
+
329
+ # Draw grid lines
330
+ for x in range(space.width + 1):
331
+ space_ax.axvline(x, color="gray", linestyle=":")
332
+ for y in range(space.height + 1):
333
+ space_ax.axhline(y, color="gray", linestyle=":")
334
+
335
+ space_ax.scatter(**portray(space))
336
+
337
+
292
338
  def make_plot_measure(measure: str | dict[str, str] | list[str] | tuple[str]):
293
339
  """Create a plotting function for a specified measure.
294
340
 
@@ -327,11 +373,14 @@ def PlotMatplotlib(model, measure, dependencies: list[any] | None = None):
327
373
  elif isinstance(measure, dict):
328
374
  for m, color in measure.items():
329
375
  ax.plot(df.loc[:, m], label=m, color=color)
330
- fig.legend()
376
+ ax.legend(loc="best")
331
377
  elif isinstance(measure, list | tuple):
332
378
  for m in measure:
333
379
  ax.plot(df.loc[:, m], label=m)
334
- fig.legend()
380
+ ax.legend(loc="best")
381
+ ax.set_xlabel("Step")
335
382
  # Set integer x axis
336
383
  ax.xaxis.set_major_locator(plt.MaxNLocator(integer=True))
337
- solara.FigureMatplotlib(fig, dependencies=dependencies)
384
+ solara.FigureMatplotlib(
385
+ fig, format="png", bbox_inches="tight", dependencies=dependencies
386
+ )