Mesa 3.0.0b2__py3-none-any.whl → 3.0.1__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 (47) hide show
  1. mesa/__init__.py +1 -1
  2. mesa/agent.py +15 -3
  3. mesa/batchrunner.py +26 -1
  4. mesa/examples/README.md +11 -11
  5. mesa/examples/__init__.py +2 -2
  6. mesa/examples/advanced/epstein_civil_violence/agents.py +44 -38
  7. mesa/examples/advanced/epstein_civil_violence/app.py +29 -28
  8. mesa/examples/advanced/epstein_civil_violence/model.py +33 -65
  9. mesa/examples/advanced/pd_grid/app.py +9 -5
  10. mesa/examples/advanced/pd_grid/model.py +1 -1
  11. mesa/examples/advanced/sugarscape_g1mt/app.py +5 -13
  12. mesa/examples/advanced/sugarscape_g1mt/model.py +3 -1
  13. mesa/examples/advanced/wolf_sheep/agents.py +53 -39
  14. mesa/examples/advanced/wolf_sheep/app.py +37 -19
  15. mesa/examples/advanced/wolf_sheep/model.py +68 -74
  16. mesa/examples/basic/boid_flockers/agents.py +49 -18
  17. mesa/examples/basic/boid_flockers/app.py +2 -2
  18. mesa/examples/basic/boid_flockers/model.py +55 -19
  19. mesa/examples/basic/boltzmann_wealth_model/agents.py +23 -5
  20. mesa/examples/basic/boltzmann_wealth_model/app.py +22 -13
  21. mesa/examples/basic/boltzmann_wealth_model/model.py +48 -13
  22. mesa/examples/basic/boltzmann_wealth_model/st_app.py +2 -2
  23. mesa/examples/basic/conways_game_of_life/app.py +15 -3
  24. mesa/examples/basic/schelling/agents.py +9 -5
  25. mesa/examples/basic/schelling/app.py +5 -5
  26. mesa/examples/basic/schelling/model.py +48 -26
  27. mesa/examples/basic/virus_on_network/app.py +25 -47
  28. mesa/experimental/cell_space/cell_collection.py +14 -2
  29. mesa/experimental/cell_space/discrete_space.py +16 -2
  30. mesa/experimental/devs/simulator.py +59 -14
  31. mesa/model.py +4 -4
  32. mesa/space.py +0 -30
  33. mesa/time.py +4 -4
  34. mesa/visualization/__init__.py +17 -6
  35. mesa/visualization/components/__init__.py +83 -0
  36. mesa/visualization/components/{altair.py → altair_components.py} +34 -2
  37. mesa/visualization/components/matplotlib_components.py +175 -0
  38. mesa/visualization/mpl_space_drawing.py +593 -0
  39. mesa/visualization/solara_viz.py +156 -67
  40. {mesa-3.0.0b2.dist-info → mesa-3.0.1.dist-info}/METADATA +6 -8
  41. {mesa-3.0.0b2.dist-info → mesa-3.0.1.dist-info}/RECORD +46 -44
  42. {mesa-3.0.0b2.dist-info → mesa-3.0.1.dist-info}/WHEEL +1 -1
  43. mesa/visualization/components/matplotlib.py +0 -386
  44. /mesa/visualization/{UserParam.py → user_param.py} +0 -0
  45. {mesa-3.0.0b2.dist-info → mesa-3.0.1.dist-info}/entry_points.txt +0 -0
  46. {mesa-3.0.0b2.dist-info → mesa-3.0.1.dist-info}/licenses/LICENSE +0 -0
  47. {mesa-3.0.0b2.dist-info → mesa-3.0.1.dist-info}/licenses/NOTICE +0 -0
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import itertools
6
+ import warnings
6
7
  from collections.abc import Callable, Iterable, Mapping
7
8
  from functools import cached_property
8
9
  from random import Random
@@ -23,6 +24,12 @@ class CellCollection(Generic[T]):
23
24
  agents (List[CellAgent]) : List of agents occupying the cells in this collection
24
25
  random (Random) : The random number generator
25
26
 
27
+ Notes:
28
+ A `UserWarning` is issued if `random=None`. You can resolve this warning by explicitly
29
+ passing a random number generator. In most cases, this will be the seeded random number
30
+ generator in the model. So, you would do `random=self.random` in a `Model` or `Agent` instance.
31
+
32
+
26
33
  """
27
34
 
28
35
  def __init__(
@@ -45,7 +52,12 @@ class CellCollection(Generic[T]):
45
52
  self._capacity: int = next(iter(self._cells.keys())).capacity
46
53
 
47
54
  if random is None:
48
- random = Random() # FIXME
55
+ warnings.warn(
56
+ "Random number generator not specified, this can make models non-reproducible. Please pass a random number generator explicitly",
57
+ UserWarning,
58
+ stacklevel=2,
59
+ )
60
+ random = Random()
49
61
  self.random = random
50
62
 
51
63
  def __iter__(self): # noqa
@@ -115,4 +127,4 @@ class CellCollection(Generic[T]):
115
127
  yield cell
116
128
  count += 1
117
129
 
118
- return CellCollection(cell_generator(filter_func, at_most))
130
+ return CellCollection(cell_generator(filter_func, at_most), random=self.random)
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import warnings
5
6
  from collections.abc import Callable
6
7
  from functools import cached_property
7
8
  from random import Random
@@ -25,6 +26,12 @@ class DiscreteSpace(Generic[T]):
25
26
  cell_klass (Type) : the type of cell class
26
27
  empties (CellCollection) : collection of all cells that are empty
27
28
  property_layers (dict[str, PropertyLayer]): the property layers of the discrete space
29
+
30
+ Notes:
31
+ A `UserWarning` is issued if `random=None`. You can resolve this warning by explicitly
32
+ passing a random number generator. In most cases, this will be the seeded random number
33
+ generator in the model. So, you would do `random=self.random` in a `Model` or `Agent` instance.
34
+
28
35
  """
29
36
 
30
37
  def __init__(
@@ -44,7 +51,12 @@ class DiscreteSpace(Generic[T]):
44
51
  self.capacity = capacity
45
52
  self._cells: dict[tuple[int, ...], T] = {}
46
53
  if random is None:
47
- random = Random() # FIXME should default to default rng from model
54
+ warnings.warn(
55
+ "Random number generator not specified, this can make models non-reproducible. Please pass a random number generator explicitly",
56
+ UserWarning,
57
+ stacklevel=2,
58
+ )
59
+ random = Random()
48
60
  self.random = random
49
61
  self.cell_klass = cell_klass
50
62
 
@@ -67,7 +79,9 @@ class DiscreteSpace(Generic[T]):
67
79
  @cached_property
68
80
  def all_cells(self):
69
81
  """Return all cells in space."""
70
- return CellCollection({cell: cell.agents for cell in self._cells.values()})
82
+ return CellCollection(
83
+ {cell: cell.agents for cell in self._cells.values()}, random=self.random
84
+ )
71
85
 
72
86
  def __iter__(self): # noqa
73
87
  return iter(self._cells.values())
@@ -57,8 +57,20 @@ class Simulator:
57
57
  Args:
58
58
  model (Model): The model to simulate
59
59
 
60
+ Raises:
61
+ Exception if simulator.time is not equal to simulator.starttime
62
+ Exception if event list is not empty
63
+
60
64
  """
61
- self.event_list.clear()
65
+ if self.time != self.start_time:
66
+ raise ValueError(
67
+ "trying to setup model, but current time is not equal to start_time, Has the simulator been reset or freshly initialized?"
68
+ )
69
+ if not self.event_list.is_empty():
70
+ raise ValueError(
71
+ "trying to setup model, but events have already been scheduled. Call simulator.setup before any scheduling"
72
+ )
73
+
62
74
  self.model = model
63
75
 
64
76
  def reset(self):
@@ -68,7 +80,20 @@ class Simulator:
68
80
  self.time = self.start_time
69
81
 
70
82
  def run_until(self, end_time: int | float) -> None:
71
- """Run the simulator until the end time."""
83
+ """Run the simulator until the end time.
84
+
85
+ Args:
86
+ end_time (int | float): The end time for stopping the simulator
87
+
88
+ Raises:
89
+ Exception if simulator.setup() has not yet been called
90
+
91
+ """
92
+ if self.model is None:
93
+ raise Exception(
94
+ "simulator has not been setup, call simulator.setup(model) first"
95
+ )
96
+
72
97
  while True:
73
98
  try:
74
99
  event = self.event_list.pop_event()
@@ -84,6 +109,26 @@ class Simulator:
84
109
  self._schedule_event(event) # reschedule event
85
110
  break
86
111
 
112
+ def run_next_event(self):
113
+ """Execute the next event.
114
+
115
+ Raises:
116
+ Exception if simulator.setup() has not yet been called
117
+
118
+ """
119
+ if self.model is None:
120
+ raise Exception(
121
+ "simulator has not been setup, call simulator.setup(model) first"
122
+ )
123
+
124
+ try:
125
+ event = self.event_list.pop_event()
126
+ except IndexError: # event list is empty
127
+ return
128
+ else:
129
+ self.time = event.time
130
+ event.execute()
131
+
87
132
  def run_for(self, time_delta: int | float):
88
133
  """Run the simulator for the specified time delta.
89
134
 
@@ -92,6 +137,7 @@ class Simulator:
92
137
  plus the time delta
93
138
 
94
139
  """
140
+ # fixme, raise initialization error or something like it if model.setup has not been called
95
141
  end_time = self.time + time_delta
96
142
  self.run_until(end_time)
97
143
 
@@ -228,7 +274,7 @@ class ABMSimulator(Simulator):
228
274
 
229
275
  """
230
276
  super().setup(model)
231
- self.schedule_event_now(self.model.step, priority=Priority.HIGH)
277
+ self.schedule_event_next_tick(self.model.step, priority=Priority.HIGH)
232
278
 
233
279
  def check_time_unit(self, time) -> bool:
234
280
  """Check whether the time is of the correct unit.
@@ -277,7 +323,15 @@ class ABMSimulator(Simulator):
277
323
  Args:
278
324
  end_time (float| int): The end_time delta. The simulator is until the specified end time
279
325
 
326
+ Raises:
327
+ Exception if simulator.setup() has not yet been called
328
+
280
329
  """
330
+ if self.model is None:
331
+ raise Exception(
332
+ "simulator has not been setup, call simulator.setup(model) first"
333
+ )
334
+
281
335
  while True:
282
336
  try:
283
337
  event = self.event_list.pop_event()
@@ -285,6 +339,8 @@ class ABMSimulator(Simulator):
285
339
  self.time = end_time
286
340
  break
287
341
 
342
+ # fixme: the alternative would be to wrap model.step with an annotation which
343
+ # handles this scheduling.
288
344
  if event.time <= end_time:
289
345
  self.time = event.time
290
346
  if event.fn() == self.model.step:
@@ -298,17 +354,6 @@ class ABMSimulator(Simulator):
298
354
  self._schedule_event(event)
299
355
  break
300
356
 
301
- def run_for(self, time_delta: int):
302
- """Run the simulator for the specified time delta.
303
-
304
- Args:
305
- time_delta (float| int): The time delta. The simulator is run from the current time to the current time
306
- plus the time delta
307
-
308
- """
309
- end_time = self.time + time_delta - 1
310
- self.run_until(end_time)
311
-
312
357
 
313
358
  class DEVSimulator(Simulator):
314
359
  """A simulator where the unit of time is a float.
mesa/model.py CHANGED
@@ -114,7 +114,7 @@ class Model:
114
114
 
115
115
  def next_id(self) -> int: # noqa: D102
116
116
  warnings.warn(
117
- "using model.next_id() is deprecated. Agents track their unique ID automatically",
117
+ "using model.next_id() is deprecated and will be removed in Mesa 3.1. Agents track their unique ID automatically",
118
118
  DeprecationWarning,
119
119
  stacklevel=2,
120
120
  )
@@ -146,8 +146,8 @@ class Model:
146
146
  def get_agents_of_type(self, agenttype: type[Agent]) -> AgentSet:
147
147
  """Deprecated: Retrieves an AgentSet containing all agents of the specified type."""
148
148
  warnings.warn(
149
- f"Model.get_agents_of_type() is deprecated, please replace get_agents_of_type({agenttype})"
150
- f"with the property agents_by_type[{agenttype}].",
149
+ f"Model.get_agents_of_type() is deprecated and will be removed in Mesa 3.1."
150
+ f"Please replace get_agents_of_type({agenttype}) with the property agents_by_type[{agenttype}].",
151
151
  DeprecationWarning,
152
152
  stacklevel=2,
153
153
  )
@@ -262,7 +262,7 @@ class Model:
262
262
 
263
263
  """
264
264
  warnings.warn(
265
- "initialize_data_collector() is deprecated. Please use the DataCollector class directly. "
265
+ "initialize_data_collector() is deprecated and will be removed in Mesa 3.1. Please use the DataCollector class directly. "
266
266
  "by using `self.datacollector = DataCollector(...)`.",
267
267
  DeprecationWarning,
268
268
  stacklevel=2,
mesa/space.py CHANGED
@@ -1287,36 +1287,6 @@ class HexMultiGrid(_HexGrid, MultiGrid):
1287
1287
  """
1288
1288
 
1289
1289
 
1290
- class HexGrid(HexSingleGrid):
1291
- """Hexagonal Grid: a Grid where neighbors are computed according to a hexagonal tiling of the grid.
1292
-
1293
- Functions according to odd-q rules.
1294
- See http://www.redblobgames.com/grids/hexagons/#coordinates for more.
1295
-
1296
- Properties:
1297
- width, height: The grid's width and height.
1298
- torus: Boolean which determines whether to treat the grid as a torus.
1299
- """
1300
-
1301
- def __init__(self, width: int, height: int, torus: bool) -> None:
1302
- """Initializes a HexGrid, deprecated.
1303
-
1304
- Args:
1305
- width: the width of the grid
1306
- height: the height of the grid
1307
- torus: whether the grid wraps
1308
- """
1309
- super().__init__(width, height, torus)
1310
- warn(
1311
- (
1312
- "HexGrid is being deprecated; use instead HexSingleGrid or HexMultiGrid "
1313
- "depending on your use case."
1314
- ),
1315
- DeprecationWarning,
1316
- stacklevel=2,
1317
- )
1318
-
1319
-
1320
1290
  class ContinuousSpace:
1321
1291
  """Continuous space where each agent can have an arbitrary position.
1322
1292
 
mesa/time.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """Mesa Time Module.
2
2
 
3
3
  .. warning::
4
- The time module and all its Schedulers are deprecated and will be removed in a future version.
4
+ The time module and all its Schedulers are deprecated and will be removed in Mesa 3.1.
5
5
  They can be replaced with AgentSet functionality. See the migration guide for details:
6
6
  https://mesa.readthedocs.io/latest/migration_guide.html#time-and-schedulers
7
7
 
@@ -63,7 +63,7 @@ class BaseScheduler:
63
63
 
64
64
  """
65
65
  warnings.warn(
66
- "The time module and all its Schedulers are deprecated and will be removed in a future version. "
66
+ "The time module and all its Schedulers are deprecated and will be removed in Mesa 3.1. "
67
67
  "They can be replaced with AgentSet functionality. See the migration guide for details. "
68
68
  "https://mesa.readthedocs.io/latest/migration_guide.html#time-and-schedulers",
69
69
  DeprecationWarning,
@@ -375,7 +375,7 @@ class RandomActivationByType(BaseScheduler):
375
375
 
376
376
 
377
377
  class DiscreteEventScheduler(BaseScheduler):
378
- """This class has been deprecated and replaced by the functionality provided by experimental.devs."""
378
+ """This class has been removed and replaced by the functionality provided by experimental.devs."""
379
379
 
380
380
  def __init__(self, model: Model, time_step: TimeT = 1) -> None:
381
381
  """Initialize DiscreteEventScheduler.
@@ -387,5 +387,5 @@ class DiscreteEventScheduler(BaseScheduler):
387
387
  """
388
388
  super().__init__(model)
389
389
  raise Exception(
390
- "DiscreteEventScheduler is deprecated in favor of the functionality provided by experimental.devs"
390
+ "DiscreteEventScheduler is removed in favor of the functionality provided by experimental.devs"
391
391
  )
@@ -1,15 +1,26 @@
1
- """Solara based visualization for Mesa models."""
1
+ """Solara based visualization for Mesa models.
2
2
 
3
- from .components.altair import make_space_altair
4
- from .components.matplotlib import make_plot_measure, make_space_matplotlib
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
5
15
  from .solara_viz import JupyterViz, SolaraViz
6
- from .UserParam import Slider
16
+ from .user_param import Slider
7
17
 
8
18
  __all__ = [
9
19
  "JupyterViz",
10
20
  "SolaraViz",
11
21
  "Slider",
12
22
  "make_space_altair",
13
- "make_space_matplotlib",
14
- "make_plot_measure",
23
+ "draw_space",
24
+ "make_plot_component",
25
+ "make_space_component",
15
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
+ )
@@ -1,6 +1,7 @@
1
1
  """Altair based solara components for visualization mesa spaces."""
2
2
 
3
3
  import contextlib
4
+ import warnings
4
5
 
5
6
  import solara
6
7
 
@@ -12,7 +13,33 @@ from mesa.space import ContinuousSpace, _Grid
12
13
  from mesa.visualization.utils import update_counter
13
14
 
14
15
 
15
- def make_space_altair(agent_portrayal=None): # noqa: D103
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
+ """
16
43
  if agent_portrayal is None:
17
44
 
18
45
  def agent_portrayal(a):
@@ -25,7 +52,12 @@ def make_space_altair(agent_portrayal=None): # noqa: D103
25
52
 
26
53
 
27
54
  @solara.component
28
- def SpaceAltair(model, agent_portrayal, dependencies: list[any] | None = None): # noqa: D103
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
+ """
29
61
  update_counter.get()
30
62
  space = getattr(model, "grid", None)
31
63
  if space is None:
@@ -0,0 +1,175 @@
1
+ """Matplotlib based solara components for visualization MESA spaces and plots."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import warnings
6
+ from collections.abc import Callable
7
+
8
+ import matplotlib.pyplot as plt
9
+ import solara
10
+ from matplotlib.figure import Figure
11
+
12
+ from mesa.visualization.mpl_space_drawing import draw_space
13
+ from mesa.visualization.utils import update_counter
14
+
15
+
16
+ def make_space_matplotlib(*args, **kwargs): # noqa: D103
17
+ warnings.warn(
18
+ "make_space_matplotlib has been renamed to make_mpl_space_component",
19
+ DeprecationWarning,
20
+ stacklevel=2,
21
+ )
22
+ return make_mpl_space_component(*args, **kwargs)
23
+
24
+
25
+ def make_mpl_space_component(
26
+ agent_portrayal: Callable | None = None,
27
+ propertylayer_portrayal: dict | None = None,
28
+ post_process: Callable | None = None,
29
+ **space_drawing_kwargs,
30
+ ) -> SpaceMatplotlib:
31
+ """Create a Matplotlib-based space visualization component.
32
+
33
+ Args:
34
+ agent_portrayal: Function to portray agents.
35
+ propertylayer_portrayal: Dictionary of PropertyLayer portrayal specifications
36
+ post_process : a callable that will be called with the Axes instance. Allows for fine tuning plots (e.g., control ticks)
37
+ space_drawing_kwargs : additional keyword arguments to be passed on to the underlying space drawer function. See
38
+ the functions for drawing the various spaces for further details.
39
+
40
+ ``agent_portrayal`` is called with an agent and should return a dict. Valid fields in this dict are "color",
41
+ "size", "marker", "zorder", alpha, linewidths, and edgecolors. Other field are ignored and will result in a user warning.
42
+
43
+ Returns:
44
+ function: A function that creates a SpaceMatplotlib component
45
+ """
46
+ if agent_portrayal is None:
47
+
48
+ def agent_portrayal(a):
49
+ return {}
50
+
51
+ def MakeSpaceMatplotlib(model):
52
+ return SpaceMatplotlib(
53
+ model,
54
+ agent_portrayal,
55
+ propertylayer_portrayal,
56
+ post_process=post_process,
57
+ **space_drawing_kwargs,
58
+ )
59
+
60
+ return MakeSpaceMatplotlib
61
+
62
+
63
+ @solara.component
64
+ def SpaceMatplotlib(
65
+ model,
66
+ agent_portrayal,
67
+ propertylayer_portrayal,
68
+ dependencies: list[any] | None = None,
69
+ post_process: Callable | None = None,
70
+ **space_drawing_kwargs,
71
+ ):
72
+ """Create a Matplotlib-based space visualization component."""
73
+ update_counter.get()
74
+
75
+ space = getattr(model, "grid", None)
76
+ if space is None:
77
+ space = getattr(model, "space", None)
78
+
79
+ fig = Figure()
80
+ ax = fig.add_subplot()
81
+
82
+ draw_space(
83
+ space,
84
+ agent_portrayal,
85
+ propertylayer_portrayal=propertylayer_portrayal,
86
+ ax=ax,
87
+ **space_drawing_kwargs,
88
+ )
89
+
90
+ if post_process is not None:
91
+ post_process(ax)
92
+
93
+ solara.FigureMatplotlib(
94
+ fig, format="png", bbox_inches="tight", dependencies=dependencies
95
+ )
96
+
97
+
98
+ def make_plot_measure(*args, **kwargs): # noqa: D103
99
+ warnings.warn(
100
+ "make_plot_measure has been renamed to make_plot_component",
101
+ DeprecationWarning,
102
+ stacklevel=2,
103
+ )
104
+ return make_mpl_plot_component(*args, **kwargs)
105
+
106
+
107
+ def make_mpl_plot_component(
108
+ measure: str | dict[str, str] | list[str] | tuple[str],
109
+ post_process: Callable | None = None,
110
+ save_format="png",
111
+ ):
112
+ """Create a plotting function for a specified measure.
113
+
114
+ Args:
115
+ measure (str | dict[str, str] | list[str] | tuple[str]): Measure(s) to plot.
116
+ post_process: a user-specified callable to do post-processing called with the Axes instance.
117
+ save_format: save format of figure in solara backend
118
+
119
+ Returns:
120
+ function: A function that creates a PlotMatplotlib component.
121
+ """
122
+
123
+ def MakePlotMatplotlib(model):
124
+ return PlotMatplotlib(
125
+ model, measure, post_process=post_process, save_format=save_format
126
+ )
127
+
128
+ return MakePlotMatplotlib
129
+
130
+
131
+ @solara.component
132
+ def PlotMatplotlib(
133
+ model,
134
+ measure,
135
+ dependencies: list[any] | None = None,
136
+ post_process: Callable | None = None,
137
+ save_format="png",
138
+ ):
139
+ """Create a Matplotlib-based plot for a measure or measures.
140
+
141
+ Args:
142
+ model (mesa.Model): The model instance.
143
+ measure (str | dict[str, str] | list[str] | tuple[str]): Measure(s) to plot.
144
+ dependencies (list[any] | None): Optional dependencies for the plot.
145
+ post_process: a user-specified callable to do post-processing called with the Axes instance.
146
+ save_format: format used for saving the figure.
147
+
148
+ Returns:
149
+ solara.FigureMatplotlib: A component for rendering the plot.
150
+ """
151
+ update_counter.get()
152
+ fig = Figure()
153
+ ax = fig.subplots()
154
+ df = model.datacollector.get_model_vars_dataframe()
155
+ if isinstance(measure, str):
156
+ ax.plot(df.loc[:, measure])
157
+ ax.set_ylabel(measure)
158
+ elif isinstance(measure, dict):
159
+ for m, color in measure.items():
160
+ ax.plot(df.loc[:, m], label=m, color=color)
161
+ ax.legend(loc="best")
162
+ elif isinstance(measure, list | tuple):
163
+ for m in measure:
164
+ ax.plot(df.loc[:, m], label=m)
165
+ ax.legend(loc="best")
166
+
167
+ if post_process is not None:
168
+ post_process(ax)
169
+
170
+ ax.set_xlabel("Step")
171
+ # Set integer x axis
172
+ ax.xaxis.set_major_locator(plt.MaxNLocator(integer=True))
173
+ solara.FigureMatplotlib(
174
+ fig, format=save_format, bbox_inches="tight", dependencies=dependencies
175
+ )