Mesa 2.3.4__py3-none-any.whl → 3.0.0__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 (110) hide show
  1. mesa/__init__.py +3 -5
  2. mesa/agent.py +393 -116
  3. mesa/batchrunner.py +58 -31
  4. mesa/datacollection.py +141 -30
  5. mesa/examples/README.md +37 -0
  6. mesa/examples/__init__.py +21 -0
  7. mesa/examples/advanced/epstein_civil_violence/Epstein Civil Violence.ipynb +116 -0
  8. mesa/examples/advanced/epstein_civil_violence/Readme.md +34 -0
  9. mesa/examples/advanced/epstein_civil_violence/__init__.py +0 -0
  10. mesa/examples/advanced/epstein_civil_violence/agents.py +164 -0
  11. mesa/examples/advanced/epstein_civil_violence/app.py +73 -0
  12. mesa/examples/advanced/epstein_civil_violence/model.py +114 -0
  13. mesa/examples/advanced/pd_grid/Readme.md +43 -0
  14. mesa/examples/advanced/pd_grid/__init__.py +0 -0
  15. mesa/examples/advanced/pd_grid/agents.py +50 -0
  16. mesa/examples/advanced/pd_grid/analysis.ipynb +228 -0
  17. mesa/examples/advanced/pd_grid/app.py +54 -0
  18. mesa/examples/advanced/pd_grid/model.py +71 -0
  19. mesa/examples/advanced/sugarscape_g1mt/Readme.md +64 -0
  20. mesa/examples/advanced/sugarscape_g1mt/__init__.py +0 -0
  21. mesa/examples/advanced/sugarscape_g1mt/agents.py +344 -0
  22. mesa/examples/advanced/sugarscape_g1mt/app.py +62 -0
  23. mesa/examples/advanced/sugarscape_g1mt/model.py +180 -0
  24. mesa/examples/advanced/sugarscape_g1mt/sugar-map.txt +50 -0
  25. mesa/examples/advanced/sugarscape_g1mt/tests.py +69 -0
  26. mesa/examples/advanced/wolf_sheep/Readme.md +57 -0
  27. mesa/examples/advanced/wolf_sheep/__init__.py +0 -0
  28. mesa/examples/advanced/wolf_sheep/agents.py +102 -0
  29. mesa/examples/advanced/wolf_sheep/app.py +84 -0
  30. mesa/examples/advanced/wolf_sheep/model.py +137 -0
  31. mesa/examples/basic/__init__.py +0 -0
  32. mesa/examples/basic/boid_flockers/Readme.md +22 -0
  33. mesa/examples/basic/boid_flockers/__init__.py +0 -0
  34. mesa/examples/basic/boid_flockers/agents.py +71 -0
  35. mesa/examples/basic/boid_flockers/app.py +58 -0
  36. mesa/examples/basic/boid_flockers/model.py +69 -0
  37. mesa/examples/basic/boltzmann_wealth_model/Readme.md +56 -0
  38. mesa/examples/basic/boltzmann_wealth_model/__init__.py +0 -0
  39. mesa/examples/basic/boltzmann_wealth_model/agents.py +31 -0
  40. mesa/examples/basic/boltzmann_wealth_model/app.py +74 -0
  41. mesa/examples/basic/boltzmann_wealth_model/model.py +43 -0
  42. mesa/examples/basic/boltzmann_wealth_model/st_app.py +115 -0
  43. mesa/examples/basic/conways_game_of_life/Readme.md +39 -0
  44. mesa/examples/basic/conways_game_of_life/__init__.py +0 -0
  45. mesa/examples/basic/conways_game_of_life/agents.py +47 -0
  46. mesa/examples/basic/conways_game_of_life/app.py +51 -0
  47. mesa/examples/basic/conways_game_of_life/model.py +31 -0
  48. mesa/examples/basic/conways_game_of_life/st_app.py +72 -0
  49. mesa/examples/basic/schelling/Readme.md +40 -0
  50. mesa/examples/basic/schelling/__init__.py +0 -0
  51. mesa/examples/basic/schelling/agents.py +26 -0
  52. mesa/examples/basic/schelling/analysis.ipynb +205 -0
  53. mesa/examples/basic/schelling/app.py +42 -0
  54. mesa/examples/basic/schelling/model.py +59 -0
  55. mesa/examples/basic/virus_on_network/Readme.md +61 -0
  56. mesa/examples/basic/virus_on_network/__init__.py +0 -0
  57. mesa/examples/basic/virus_on_network/agents.py +69 -0
  58. mesa/examples/basic/virus_on_network/app.py +114 -0
  59. mesa/examples/basic/virus_on_network/model.py +96 -0
  60. mesa/experimental/UserParam.py +18 -7
  61. mesa/experimental/__init__.py +10 -2
  62. mesa/experimental/cell_space/__init__.py +16 -1
  63. mesa/experimental/cell_space/cell.py +93 -23
  64. mesa/experimental/cell_space/cell_agent.py +117 -21
  65. mesa/experimental/cell_space/cell_collection.py +56 -19
  66. mesa/experimental/cell_space/discrete_space.py +92 -8
  67. mesa/experimental/cell_space/grid.py +33 -9
  68. mesa/experimental/cell_space/network.py +15 -10
  69. mesa/experimental/cell_space/voronoi.py +257 -0
  70. mesa/experimental/components/altair.py +11 -2
  71. mesa/experimental/components/matplotlib.py +132 -26
  72. mesa/experimental/devs/__init__.py +2 -0
  73. mesa/experimental/devs/eventlist.py +54 -15
  74. mesa/experimental/devs/examples/epstein_civil_violence.py +71 -39
  75. mesa/experimental/devs/examples/wolf_sheep.py +45 -45
  76. mesa/experimental/devs/simulator.py +57 -16
  77. mesa/experimental/{jupyter_viz.py → solara_viz.py} +151 -98
  78. mesa/model.py +212 -84
  79. mesa/space.py +217 -151
  80. mesa/time.py +63 -80
  81. mesa/visualization/__init__.py +25 -6
  82. mesa/visualization/components/__init__.py +83 -0
  83. mesa/visualization/components/altair_components.py +188 -0
  84. mesa/visualization/components/matplotlib_components.py +175 -0
  85. mesa/visualization/mpl_space_drawing.py +593 -0
  86. mesa/visualization/solara_viz.py +458 -0
  87. mesa/visualization/user_param.py +69 -0
  88. mesa/visualization/utils.py +9 -0
  89. {mesa-2.3.4.dist-info → mesa-3.0.0.dist-info}/METADATA +65 -19
  90. mesa-3.0.0.dist-info/RECORD +95 -0
  91. mesa-3.0.0.dist-info/licenses/LICENSE +202 -0
  92. mesa-2.3.4.dist-info/licenses/LICENSE → mesa-3.0.0.dist-info/licenses/NOTICE +2 -2
  93. mesa/cookiecutter-mesa/cookiecutter.json +0 -8
  94. mesa/cookiecutter-mesa/hooks/post_gen_project.py +0 -11
  95. mesa/cookiecutter-mesa/{{cookiecutter.snake}}/README.md +0 -4
  96. mesa/cookiecutter-mesa/{{cookiecutter.snake}}/run.pytemplate +0 -3
  97. mesa/cookiecutter-mesa/{{cookiecutter.snake}}/setup.pytemplate +0 -11
  98. mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/model.pytemplate +0 -60
  99. mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/server.pytemplate +0 -36
  100. mesa/flat/__init__.py +0 -6
  101. mesa/flat/visualization.py +0 -5
  102. mesa/main.py +0 -63
  103. mesa/visualization/ModularVisualization.py +0 -1
  104. mesa/visualization/TextVisualization.py +0 -1
  105. mesa/visualization/UserParam.py +0 -1
  106. mesa/visualization/modules.py +0 -1
  107. mesa-2.3.4.dist-info/RECORD +0 -45
  108. /mesa/{cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}} → examples/advanced}/__init__.py +0 -0
  109. {mesa-2.3.4.dist-info → mesa-3.0.0.dist-info}/WHEEL +0 -0
  110. {mesa-2.3.4.dist-info → mesa-3.0.0.dist-info}/entry_points.txt +0 -0
mesa/time.py CHANGED
@@ -1,6 +1,9 @@
1
- """
2
- Mesa Time Module
3
- ================
1
+ """Mesa Time Module.
2
+
3
+ .. warning::
4
+ The time module and all its Schedulers are deprecated and will be removed in a future version.
5
+ They can be replaced with AgentSet functionality. See the migration guide for details:
6
+ https://mesa.readthedocs.io/latest/migration_guide.html#time-and-schedulers
4
7
 
5
8
  Objects for handling the time component of a model. In particular, this module
6
9
  contains Schedulers, which handle agent activation. A Scheduler is an object
@@ -30,34 +33,25 @@ from collections import defaultdict
30
33
  from collections.abc import Iterable
31
34
 
32
35
  # mypy
33
- from typing import Union
34
-
35
36
  from mesa.agent import Agent, AgentSet
36
37
  from mesa.model import Model
37
38
 
38
39
  # BaseScheduler has a self.time of int, while
39
40
  # StagedActivation has a self.time of float
40
- TimeT = Union[float, int]
41
+ TimeT = float | int
41
42
 
42
43
 
43
44
  class BaseScheduler:
44
- """
45
- A simple scheduler that activates agents one at a time, in the order they were added.
45
+ """A simple scheduler that activates agents one at a time, in the order they were added.
46
46
 
47
47
  This scheduler is designed to replicate the behavior of the scheduler in MASON, a multi-agent simulation toolkit.
48
48
  It assumes that each agent added has a `step` method which takes no arguments and executes the agent's actions.
49
49
 
50
50
  Attributes:
51
- - model (Model): The model instance associated with the scheduler.
52
- - steps (int): The number of steps the scheduler has taken.
53
- - time (TimeT): The current time in the simulation. Can be an integer or a float.
54
-
55
- Methods:
56
- - add: Adds an agent to the scheduler.
57
- - remove: Removes an agent from the scheduler.
58
- - step: Executes a step, which involves activating each agent once.
59
- - get_agent_count: Returns the number of agents in the scheduler.
60
- - agents (property): Returns a list of all agent instances.
51
+ model (Model): The model instance associated with the scheduler.
52
+ steps (int): The number of steps the scheduler has taken.
53
+ time (TimeT): The current time in the simulation. Can be an integer or a float.
54
+
61
55
  """
62
56
 
63
57
  def __init__(self, model: Model, agents: Iterable[Agent] | None = None) -> None:
@@ -68,16 +62,22 @@ class BaseScheduler:
68
62
  agents (Iterable[Agent], None, optional): An iterable of agents who are controlled by the schedule
69
63
 
70
64
  """
65
+ warnings.warn(
66
+ "The time module and all its Schedulers are deprecated and will be removed in a future version. "
67
+ "They can be replaced with AgentSet functionality. See the migration guide for details. "
68
+ "https://mesa.readthedocs.io/latest/migration_guide.html#time-and-schedulers",
69
+ DeprecationWarning,
70
+ stacklevel=2,
71
+ )
72
+
71
73
  self.model = model
72
74
  self.steps = 0
73
75
  self.time: TimeT = 0
74
- self._original_step = self.step
75
- self.step = self._wrapped_step
76
76
 
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
@@ -86,10 +86,8 @@ class BaseScheduler:
86
86
  """Add an Agent object to the schedule.
87
87
 
88
88
  Args:
89
- agent: An Agent to be added to the schedule. NOTE: The agent must
90
- have a step() method.
89
+ agent (Agent): An Agent to be added to the schedule.
91
90
  """
92
-
93
91
  if agent not in self._agents:
94
92
  self._agents.add(agent)
95
93
  else:
@@ -98,12 +96,13 @@ class BaseScheduler:
98
96
  def remove(self, agent: Agent) -> None:
99
97
  """Remove all instances of a given agent from the schedule.
100
98
 
99
+ Args:
100
+ agent: An `Agent` instance.
101
+
101
102
  Note:
102
103
  It is only necessary to explicitly remove agents from the schedule if
103
104
  the agent is not removed from the model.
104
105
 
105
- Args:
106
- agent: An agent object.
107
106
  """
108
107
  self._agents.remove(agent)
109
108
 
@@ -115,21 +114,18 @@ class BaseScheduler:
115
114
  self.steps += 1
116
115
  self.time += 1
117
116
 
118
- def _wrapped_step(self):
119
- """Wrapper for the step method to include time and step updating."""
120
- self._original_step()
121
- self.model._advance_time()
122
-
123
117
  def get_agent_count(self) -> int:
124
118
  """Returns the current number of agents in the queue."""
125
119
  return len(self._agents)
126
120
 
127
121
  @property
128
122
  def agents(self) -> AgentSet:
123
+ """Return agents in the scheduler."""
129
124
  # a bit dirty, but returns a copy of the internal agent set
130
125
  return self._agents.select()
131
126
 
132
127
  def get_agent_keys(self, shuffle: bool = False) -> list[int]:
128
+ """Deprecated."""
133
129
  # To be able to remove and/or add agents during stepping
134
130
  # it's necessary to cast the keys view to a list.
135
131
 
@@ -147,14 +143,21 @@ class BaseScheduler:
147
143
  return agent_keys
148
144
 
149
145
  def do_each(self, method, shuffle=False):
146
+ """Perform `method` on each agent.
147
+
148
+ Args:
149
+ method: method to call
150
+ shuffle: shuffle the agents or not prior to calling method
151
+
152
+
153
+ """
150
154
  if shuffle:
151
155
  self._agents.shuffle(inplace=True)
152
156
  self._agents.do(method)
153
157
 
154
158
 
155
159
  class RandomActivation(BaseScheduler):
156
- """
157
- A scheduler that activates each agent once per step, in a random order, with the order reshuffled each step.
160
+ """A scheduler that activates each agent once per step, in a random order, with the order reshuffled each step.
158
161
 
159
162
  This scheduler is equivalent to the NetLogo 'ask agents...' behavior and is a common default for ABMs.
160
163
  It assumes that all agents have a `step` method.
@@ -164,23 +167,17 @@ class RandomActivation(BaseScheduler):
164
167
 
165
168
  Inherits all attributes and methods from BaseScheduler.
166
169
 
167
- Methods:
168
- - step: Executes a step, activating each agent in a random order.
169
170
  """
170
171
 
171
172
  def step(self) -> None:
172
- """Executes the step of all agents, one at a time, in
173
- random order.
174
-
175
- """
173
+ """Executes the step of all agents, one at a time, in random order."""
176
174
  self.do_each("step", shuffle=True)
177
175
  self.steps += 1
178
176
  self.time += 1
179
177
 
180
178
 
181
179
  class SimultaneousActivation(BaseScheduler):
182
- """
183
- A scheduler that simulates the simultaneous activation of all agents.
180
+ """A scheduler that simulates the simultaneous activation of all agents.
184
181
 
185
182
  This scheduler is unique in that it requires agents to have both `step` and `advance` methods.
186
183
  - The `step` method is for activating the agent and staging any changes without applying them immediately.
@@ -191,8 +188,6 @@ class SimultaneousActivation(BaseScheduler):
191
188
 
192
189
  Inherits all attributes and methods from BaseScheduler.
193
190
 
194
- Methods:
195
- - step: Executes a step for all agents, first calling `step` then `advance` on each.
196
191
  """
197
192
 
198
193
  def step(self) -> None:
@@ -207,9 +202,9 @@ class SimultaneousActivation(BaseScheduler):
207
202
 
208
203
 
209
204
  class StagedActivation(BaseScheduler):
210
- """
211
- A scheduler allowing agent activation to be divided into several stages, with all agents executing one stage
212
- before moving on to the next. This class is a generalization of SimultaneousActivation.
205
+ """A scheduler allowing agent activation to be divided into several stages.
206
+
207
+ All agents executing one stage before moving on to the next. This class is a generalization of SimultaneousActivation.
213
208
 
214
209
  This scheduler is useful for complex models where actions need to be broken down into distinct phases
215
210
  for each agent in each time step. Agents must implement methods for each defined stage.
@@ -224,8 +219,6 @@ class StagedActivation(BaseScheduler):
224
219
  - shuffle (bool): Determines whether to shuffle the order of agents each step.
225
220
  - shuffle_between_stages (bool): Determines whether to shuffle agents between each stage.
226
221
 
227
- Methods:
228
- - step: Executes all the stages for all agents in the defined order.
229
222
  """
230
223
 
231
224
  def __init__(
@@ -270,8 +263,7 @@ class StagedActivation(BaseScheduler):
270
263
 
271
264
 
272
265
  class RandomActivationByType(BaseScheduler):
273
- """
274
- A scheduler that activates each type of agent once per step, in random order, with the order reshuffled every step.
266
+ """A scheduler that activates each type of agent once per step, in random order, with the order reshuffled every step.
275
267
 
276
268
  This scheduler is useful for models with multiple types of agents, ensuring that each type is treated
277
269
  equitably in terms of activation order. The randomness in activation order helps in reducing biases
@@ -287,14 +279,10 @@ class RandomActivationByType(BaseScheduler):
287
279
  Attributes:
288
280
  - agents_by_type (defaultdict): A dictionary mapping agent types to dictionaries of agents.
289
281
 
290
- Methods:
291
- - step: Executes the step of each agent type in a random order.
292
- - step_type: Activates all agents of a given type.
293
- - get_type_count: Returns the count of agents of a specific type.
294
282
  """
295
283
 
296
284
  @property
297
- def agents_by_type(self):
285
+ def agents_by_type(self): # noqa: D102
298
286
  warnings.warn(
299
287
  "Because of the shift to using AgentSet, in the future this attribute will return a dict with"
300
288
  "type as key as AgentSet as value. Future behavior is available via RandomActivationByType._agents_by_type",
@@ -309,14 +297,13 @@ class RandomActivationByType(BaseScheduler):
309
297
  return agentsbytype
310
298
 
311
299
  def __init__(self, model: Model, agents: Iterable[Agent] | None = None) -> None:
312
- super().__init__(model, agents)
313
- """
300
+ """Initialize RandomActivationByType instance.
314
301
 
315
302
  Args:
316
303
  model (Model): The model to which the schedule belongs
317
304
  agents (Iterable[Agent], None, optional): An iterable of agents who are controlled by the schedule
318
305
  """
319
-
306
+ super().__init__(model, agents)
320
307
  # can't be a defaultdict because we need to pass model to AgentSet
321
308
  self._agents_by_type: [type, AgentSet] = {}
322
309
 
@@ -325,11 +312,12 @@ class RandomActivationByType(BaseScheduler):
325
312
  try:
326
313
  self._agents_by_type[type(agent)].add(agent)
327
314
  except KeyError:
328
- self._agents_by_type[type(agent)] = AgentSet([agent], self.model)
315
+ self._agents_by_type[type(agent)] = AgentSet(
316
+ [agent], self.model.random
317
+ )
329
318
 
330
319
  def add(self, agent: Agent) -> None:
331
- """
332
- Add an Agent object to the schedule
320
+ """Add an Agent object to the schedule.
333
321
 
334
322
  Args:
335
323
  agent: An Agent to be added to the schedule.
@@ -339,24 +327,24 @@ class RandomActivationByType(BaseScheduler):
339
327
  try:
340
328
  self._agents_by_type[type(agent)].add(agent)
341
329
  except KeyError:
342
- self._agents_by_type[type(agent)] = AgentSet([agent], self.model)
330
+ self._agents_by_type[type(agent)] = AgentSet([agent], self.model.random)
343
331
 
344
332
  def remove(self, agent: Agent) -> None:
345
- """
346
- Remove all instances of a given agent from the schedule.
333
+ """Remove all instances of a given agent from the schedule.
334
+
335
+ Args:
336
+ agent: An Agent to be removed from the schedule.
337
+
347
338
  """
348
339
  super().remove(agent)
349
340
  self._agents_by_type[type(agent)].remove(agent)
350
341
 
351
342
  def step(self, shuffle_types: bool = True, shuffle_agents: bool = True) -> None:
352
- """
353
- Executes the step of each agent type, one at a time, in random order.
343
+ """Executes the step of each agent type, one at a time, in random order.
354
344
 
355
345
  Args:
356
- shuffle_types: If True, the order of execution of each types is
357
- shuffled.
358
- shuffle_agents: If True, the order of execution of each agents in a
359
- type group is shuffled.
346
+ shuffle_types: If True, the order of execution of each types is shuffled.
347
+ shuffle_agents: If True, the order of execution of each agents in a type group is shuffled.
360
348
  """
361
349
  # To be able to remove and/or add agents during stepping
362
350
  # it's necessary to cast the keys view to a list.
@@ -369,12 +357,11 @@ class RandomActivationByType(BaseScheduler):
369
357
  self.time += 1
370
358
 
371
359
  def step_type(self, agenttype: type[Agent], shuffle_agents: bool = True) -> None:
372
- """
373
- Shuffle order and run all agents of a given type.
374
- This method is equivalent to the NetLogo 'ask [breed]...'.
360
+ """Shuffle order and run all agents of a given type.
375
361
 
376
362
  Args:
377
363
  agenttype: Class object of the type to run.
364
+ shuffle_agents: If True, shuffle agents
378
365
  """
379
366
  agents = self._agents_by_type[agenttype]
380
367
 
@@ -383,19 +370,15 @@ class RandomActivationByType(BaseScheduler):
383
370
  agents.do("step")
384
371
 
385
372
  def get_type_count(self, agenttype: type[Agent]) -> int:
386
- """
387
- Returns the current number of agents of certain type in the queue.
388
- """
373
+ """Returns the current number of agents of certain type in the queue."""
389
374
  return len(self._agents_by_type[agenttype])
390
375
 
391
376
 
392
377
  class DiscreteEventScheduler(BaseScheduler):
393
- """
394
- This class has been deprecated and replaced by the functionality provided by experimental.devs
395
- """
378
+ """This class has been deprecated and replaced by the functionality provided by experimental.devs."""
396
379
 
397
380
  def __init__(self, model: Model, time_step: TimeT = 1) -> None:
398
- """
381
+ """Initialize DiscreteEventScheduler.
399
382
 
400
383
  Args:
401
384
  model (Model): The model to which the schedule belongs
@@ -1,7 +1,26 @@
1
- import contextlib
1
+ """Solara based visualization for Mesa models.
2
2
 
3
- with contextlib.suppress(ImportError):
4
- from mesa_viz_tornado.ModularVisualization import * # noqa
5
- from mesa_viz_tornado.modules import * # noqa
6
- from mesa_viz_tornado.UserParam import * # noqa
7
- from mesa_viz_tornado.TextVisualization import * # noqa
3
+ .. note::
4
+ SolaraViz is experimental and still in active development for Mesa 3.0. While we attempt to minimize them, there might be API breaking changes between Mesa 3.0 and 3.1.
5
+
6
+ There won't be breaking changes between Mesa 3.0.x patch releases.
7
+ """
8
+
9
+ from mesa.visualization.mpl_space_drawing import (
10
+ draw_space,
11
+ )
12
+
13
+ from .components import make_plot_component, make_space_component
14
+ from .components.altair_components import make_space_altair
15
+ from .solara_viz import JupyterViz, SolaraViz
16
+ from .user_param import Slider
17
+
18
+ __all__ = [
19
+ "JupyterViz",
20
+ "SolaraViz",
21
+ "Slider",
22
+ "make_space_altair",
23
+ "draw_space",
24
+ "make_plot_component",
25
+ "make_space_component",
26
+ ]
@@ -0,0 +1,83 @@
1
+ """custom solara components."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+
7
+ from .altair_components import SpaceAltair, make_altair_space
8
+ from .matplotlib_components import (
9
+ SpaceMatplotlib,
10
+ make_mpl_plot_component,
11
+ make_mpl_space_component,
12
+ )
13
+
14
+
15
+ def make_space_component(
16
+ agent_portrayal: Callable | None = None,
17
+ propertylayer_portrayal: dict | None = None,
18
+ post_process: Callable | None = None,
19
+ backend: str = "matplotlib",
20
+ **space_drawing_kwargs,
21
+ ) -> SpaceMatplotlib | SpaceAltair:
22
+ """Create a Matplotlib-based space visualization component.
23
+
24
+ Args:
25
+ agent_portrayal: Function to portray agents.
26
+ propertylayer_portrayal: Dictionary of PropertyLayer portrayal specifications
27
+ post_process : a callable that will be called with the Axes instance. Allows for fine-tuning plots (e.g., control ticks)
28
+ backend: the backend to use {"matplotlib", "altair"}
29
+ space_drawing_kwargs : additional keyword arguments to be passed on to the underlying backend specific space drawer function. See
30
+ the functions for drawing the various spaces for the appropriate backend further details.
31
+
32
+
33
+ Returns:
34
+ function: A function that creates a space component
35
+ """
36
+ if backend == "matplotlib":
37
+ return make_mpl_space_component(
38
+ agent_portrayal,
39
+ propertylayer_portrayal,
40
+ post_process,
41
+ **space_drawing_kwargs,
42
+ )
43
+ elif backend == "altair":
44
+ return make_altair_space(
45
+ agent_portrayal,
46
+ propertylayer_portrayal,
47
+ post_process,
48
+ **space_drawing_kwargs,
49
+ )
50
+ else:
51
+ raise ValueError(
52
+ f"unknown backend {backend}, must be one of matplotlib, altair"
53
+ )
54
+
55
+
56
+ def make_plot_component(
57
+ measure: str | dict[str, str] | list[str] | tuple[str],
58
+ post_process: Callable | None = None,
59
+ backend: str = "matplotlib",
60
+ **plot_drawing_kwargs,
61
+ ):
62
+ """Create a plotting function for a specified measure using the specified backend.
63
+
64
+ Args:
65
+ measure (str | dict[str, str] | list[str] | tuple[str]): Measure(s) to plot.
66
+ post_process: a user-specified callable to do post-processing called with the Axes instance.
67
+ backend: the backend to use {"matplotlib", "altair"}
68
+ plot_drawing_kwargs: additional keyword arguments to pass onto the backend specific function for making a plotting component
69
+
70
+ Notes:
71
+ altair plotting backend is not yet implemented and planned for mesa 3.1.
72
+
73
+ Returns:
74
+ function: A function that creates a plot component
75
+ """
76
+ if backend == "matplotlib":
77
+ return make_mpl_plot_component(measure, post_process, **plot_drawing_kwargs)
78
+ elif backend == "altair":
79
+ raise NotImplementedError("altair line plots are not yet implemented")
80
+ else:
81
+ raise ValueError(
82
+ f"unknown backend {backend}, must be one of matplotlib, altair"
83
+ )
@@ -0,0 +1,188 @@
1
+ """Altair based solara components for visualization mesa spaces."""
2
+
3
+ import contextlib
4
+ import warnings
5
+
6
+ import solara
7
+
8
+ with contextlib.suppress(ImportError):
9
+ import altair as alt
10
+
11
+ from mesa.experimental.cell_space import DiscreteSpace, Grid
12
+ from mesa.space import ContinuousSpace, _Grid
13
+ from mesa.visualization.utils import update_counter
14
+
15
+
16
+ def make_space_altair(*args, **kwargs): # noqa: D103
17
+ warnings.warn(
18
+ "make_space_altair has been renamed to make_altair_space",
19
+ DeprecationWarning,
20
+ stacklevel=2,
21
+ )
22
+ return make_altair_space(*args, **kwargs)
23
+
24
+
25
+ def make_altair_space(
26
+ agent_portrayal, propertylayer_portrayal, post_process, **space_drawing_kwargs
27
+ ):
28
+ """Create an Altair-based space visualization component.
29
+
30
+ Args:
31
+ agent_portrayal: Function to portray agents.
32
+ propertylayer_portrayal: not yet implemented
33
+ post_process :not yet implemented
34
+ space_drawing_kwargs : not yet implemented
35
+
36
+ ``agent_portrayal`` is called with an agent and should return a dict. Valid fields in this dict are "color",
37
+ "size", "marker", and "zorder". Other field are ignored and will result in a user warning.
38
+
39
+
40
+ Returns:
41
+ function: A function that creates a SpaceMatplotlib component
42
+ """
43
+ if agent_portrayal is None:
44
+
45
+ def agent_portrayal(a):
46
+ return {"id": a.unique_id}
47
+
48
+ def MakeSpaceAltair(model):
49
+ return SpaceAltair(model, agent_portrayal)
50
+
51
+ return MakeSpaceAltair
52
+
53
+
54
+ @solara.component
55
+ def SpaceAltair(model, agent_portrayal, dependencies: list[any] | None = None):
56
+ """Create an Altair-based space visualization component.
57
+
58
+ Returns:
59
+ a solara FigureAltair instance
60
+ """
61
+ update_counter.get()
62
+ space = getattr(model, "grid", None)
63
+ if space is None:
64
+ # Sometimes the space is defined as model.space instead of model.grid
65
+ space = model.space
66
+
67
+ chart = _draw_grid(space, agent_portrayal)
68
+ solara.FigureAltair(chart)
69
+
70
+
71
+ def _get_agent_data_old__discrete_space(space, agent_portrayal):
72
+ """Format agent portrayal data for old-style discrete spaces.
73
+
74
+ Args:
75
+ space: the mesa.space._Grid instance
76
+ agent_portrayal: the agent portrayal callable
77
+
78
+ Returns:
79
+ list of dicts
80
+
81
+ """
82
+ all_agent_data = []
83
+ for content, (x, y) in space.coord_iter():
84
+ if not content:
85
+ continue
86
+ if not hasattr(content, "__iter__"):
87
+ # Is a single grid
88
+ content = [content] # noqa: PLW2901
89
+ for agent in content:
90
+ # use all data from agent portrayal, and add x,y coordinates
91
+ agent_data = agent_portrayal(agent)
92
+ agent_data["x"] = x
93
+ agent_data["y"] = y
94
+ all_agent_data.append(agent_data)
95
+ return all_agent_data
96
+
97
+
98
+ def _get_agent_data_new_discrete_space(space: DiscreteSpace, agent_portrayal):
99
+ """Format agent portrayal data for new-style discrete spaces.
100
+
101
+ Args:
102
+ space: the mesa.experiment.cell_space.Grid instance
103
+ agent_portrayal: the agent portrayal callable
104
+
105
+ Returns:
106
+ list of dicts
107
+
108
+ """
109
+ all_agent_data = []
110
+
111
+ for cell in space.all_cells:
112
+ for agent in cell.agents:
113
+ agent_data = agent_portrayal(agent)
114
+ agent_data["x"] = cell.coordinate[0]
115
+ agent_data["y"] = cell.coordinate[1]
116
+ all_agent_data.append(agent_data)
117
+ return all_agent_data
118
+
119
+
120
+ def _get_agent_data_continuous_space(space: ContinuousSpace, agent_portrayal):
121
+ """Format agent portrayal data for continuous space.
122
+
123
+ Args:
124
+ space: the ContinuousSpace instance
125
+ agent_portrayal: the agent portrayal callable
126
+
127
+ Returns:
128
+ list of dicts
129
+ """
130
+ all_agent_data = []
131
+ for agent in space._agent_to_index:
132
+ agent_data = agent_portrayal(agent)
133
+ agent_data["x"] = agent.pos[0]
134
+ agent_data["y"] = agent.pos[1]
135
+ all_agent_data.append(agent_data)
136
+ return all_agent_data
137
+
138
+
139
+ def _draw_grid(space, agent_portrayal):
140
+ match space:
141
+ case Grid():
142
+ all_agent_data = _get_agent_data_new_discrete_space(space, agent_portrayal)
143
+ case _Grid():
144
+ all_agent_data = _get_agent_data_old__discrete_space(space, agent_portrayal)
145
+ case ContinuousSpace():
146
+ all_agent_data = _get_agent_data_continuous_space(space, agent_portrayal)
147
+ case _:
148
+ raise NotImplementedError(
149
+ f"visualizing {type(space)} is currently not supported through altair"
150
+ )
151
+
152
+ invalid_tooltips = ["color", "size", "x", "y"]
153
+
154
+ x_y_type = "ordinal" if not isinstance(space, ContinuousSpace) else "nominal"
155
+
156
+ encoding_dict = {
157
+ # no x-axis label
158
+ "x": alt.X("x", axis=None, type=x_y_type),
159
+ # no y-axis label
160
+ "y": alt.Y("y", axis=None, type=x_y_type),
161
+ "tooltip": [
162
+ alt.Tooltip(key, type=alt.utils.infer_vegalite_type([value]))
163
+ for key, value in all_agent_data[0].items()
164
+ if key not in invalid_tooltips
165
+ ],
166
+ }
167
+ has_color = "color" in all_agent_data[0]
168
+ if has_color:
169
+ encoding_dict["color"] = alt.Color("color", type="nominal")
170
+ has_size = "size" in all_agent_data[0]
171
+ if has_size:
172
+ encoding_dict["size"] = alt.Size("size", type="quantitative")
173
+
174
+ chart = (
175
+ alt.Chart(
176
+ alt.Data(values=all_agent_data), encoding=alt.Encoding(**encoding_dict)
177
+ )
178
+ .mark_point(filled=True)
179
+ .properties(width=280, height=280)
180
+ # .configure_view(strokeOpacity=0) # hide grid/chart lines
181
+ )
182
+ # This is the default value for the marker size, which auto-scales
183
+ # according to the grid area.
184
+ if not has_size:
185
+ length = min(space.width, space.height)
186
+ chart = chart.mark_point(size=30000 / length**2, filled=True)
187
+
188
+ return chart