Mesa 3.0.0a3__py3-none-any.whl → 3.0.0a5__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 (40) hide show
  1. mesa/__init__.py +2 -3
  2. mesa/agent.py +193 -75
  3. mesa/batchrunner.py +18 -23
  4. mesa/cookiecutter-mesa/hooks/post_gen_project.py +2 -0
  5. mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/__init__.py +1 -0
  6. mesa/datacollection.py +138 -27
  7. mesa/experimental/UserParam.py +67 -0
  8. mesa/experimental/__init__.py +5 -1
  9. mesa/experimental/cell_space/__init__.py +7 -0
  10. mesa/experimental/cell_space/cell.py +61 -20
  11. mesa/experimental/cell_space/cell_agent.py +12 -7
  12. mesa/experimental/cell_space/cell_collection.py +54 -17
  13. mesa/experimental/cell_space/discrete_space.py +16 -5
  14. mesa/experimental/cell_space/grid.py +19 -8
  15. mesa/experimental/cell_space/network.py +9 -7
  16. mesa/experimental/cell_space/voronoi.py +26 -33
  17. mesa/experimental/components/altair.py +81 -0
  18. mesa/experimental/components/matplotlib.py +242 -0
  19. mesa/experimental/devs/__init__.py +2 -0
  20. mesa/experimental/devs/eventlist.py +36 -15
  21. mesa/experimental/devs/examples/epstein_civil_violence.py +71 -39
  22. mesa/experimental/devs/examples/wolf_sheep.py +43 -44
  23. mesa/experimental/devs/simulator.py +55 -15
  24. mesa/experimental/solara_viz.py +453 -0
  25. mesa/main.py +6 -4
  26. mesa/model.py +64 -61
  27. mesa/space.py +154 -123
  28. mesa/time.py +57 -67
  29. mesa/visualization/UserParam.py +19 -6
  30. mesa/visualization/__init__.py +14 -2
  31. mesa/visualization/components/altair.py +18 -1
  32. mesa/visualization/components/matplotlib.py +26 -2
  33. mesa/visualization/solara_viz.py +231 -225
  34. mesa/visualization/utils.py +9 -0
  35. {mesa-3.0.0a3.dist-info → mesa-3.0.0a5.dist-info}/METADATA +2 -1
  36. mesa-3.0.0a5.dist-info/RECORD +44 -0
  37. mesa-3.0.0a3.dist-info/RECORD +0 -39
  38. {mesa-3.0.0a3.dist-info → mesa-3.0.0a5.dist-info}/WHEEL +0 -0
  39. {mesa-3.0.0a3.dist-info → mesa-3.0.0a5.dist-info}/entry_points.txt +0 -0
  40. {mesa-3.0.0a3.dist-info → mesa-3.0.0a5.dist-info}/licenses/LICENSE +0 -0
@@ -1,3 +1,5 @@
1
+ """DiscreteSpace base class."""
2
+
1
3
  from __future__ import annotations
2
4
 
3
5
  from functools import cached_property
@@ -28,6 +30,13 @@ class DiscreteSpace(Generic[T]):
28
30
  cell_klass: type[T] = Cell,
29
31
  random: Random | None = None,
30
32
  ):
33
+ """Instantiate a DiscreteSpace.
34
+
35
+ Args:
36
+ capacity: capacity of cells
37
+ cell_klass: base class for all cells
38
+ random: random number generator
39
+ """
31
40
  super().__init__()
32
41
  self.capacity = capacity
33
42
  self._cells: dict[tuple[int, ...], T] = {}
@@ -40,25 +49,27 @@ class DiscreteSpace(Generic[T]):
40
49
  self._empties_initialized = False
41
50
 
42
51
  @property
43
- def cutoff_empties(self):
52
+ def cutoff_empties(self): # noqa
44
53
  return 7.953 * len(self._cells) ** 0.384
45
54
 
46
55
  def _connect_single_cell(self, cell: T): ...
47
56
 
48
57
  @cached_property
49
58
  def all_cells(self):
59
+ """Return all cells in space."""
50
60
  return CellCollection({cell: cell.agents for cell in self._cells.values()})
51
61
 
52
- def __iter__(self):
62
+ def __iter__(self): # noqa
53
63
  return iter(self._cells.values())
54
64
 
55
- def __getitem__(self, key):
65
+ def __getitem__(self, key: tuple[int, ...]) -> T: # noqa: D105
56
66
  return self._cells[key]
57
67
 
58
68
  @property
59
- def empties(self) -> CellCollection:
69
+ def empties(self) -> CellCollection[T]:
70
+ """Return all empty in spaces."""
60
71
  return self.all_cells.select(lambda cell: cell.is_empty)
61
72
 
62
73
  def select_random_empty_cell(self) -> T:
63
- """select random empty cell"""
74
+ """Select random empty cell."""
64
75
  return self.random.choice(list(self.empties))
@@ -1,3 +1,5 @@
1
+ """Various Grid Spaces."""
2
+
1
3
  from __future__ import annotations
2
4
 
3
5
  from collections.abc import Sequence
@@ -10,8 +12,8 @@ from mesa.experimental.cell_space import Cell, DiscreteSpace
10
12
  T = TypeVar("T", bound=Cell)
11
13
 
12
14
 
13
- class Grid(DiscreteSpace, Generic[T]):
14
- """Base class for all grid classes
15
+ class Grid(DiscreteSpace[T], Generic[T]):
16
+ """Base class for all grid classes.
15
17
 
16
18
  Attributes:
17
19
  dimensions (Sequence[int]): the dimensions of the grid
@@ -30,6 +32,15 @@ class Grid(DiscreteSpace, Generic[T]):
30
32
  random: Random | None = None,
31
33
  cell_klass: type[T] = Cell,
32
34
  ) -> None:
35
+ """Initialise the grid class.
36
+
37
+ Args:
38
+ dimensions: the dimensions of the space
39
+ torus: whether the space wraps
40
+ capacity: capacity of the grid cell
41
+ random: a random number generator
42
+ cell_klass: the base class to use for the cells
43
+ """
33
44
  super().__init__(capacity=capacity, random=random, cell_klass=cell_klass)
34
45
  self.torus = torus
35
46
  self.dimensions = dimensions
@@ -63,7 +74,7 @@ class Grid(DiscreteSpace, Generic[T]):
63
74
  if self.capacity is not None and not isinstance(self.capacity, float | int):
64
75
  raise ValueError("Capacity must be a number or None.")
65
76
 
66
- def select_random_empty_cell(self) -> T:
77
+ def select_random_empty_cell(self) -> T: # noqa
67
78
  # FIXME:: currently just a simple boolean to control behavior
68
79
  # FIXME:: basically if grid is close to 99% full, creating empty list can be faster
69
80
  # FIXME:: note however that the old results don't apply because in this implementation
@@ -89,7 +100,7 @@ class Grid(DiscreteSpace, Generic[T]):
89
100
  if self.torus:
90
101
  n_coord = tuple(nc % d for nc, d in zip(n_coord, self.dimensions))
91
102
  if all(0 <= nc < d for nc, d in zip(n_coord, self.dimensions)):
92
- cell.connect(self._cells[n_coord])
103
+ cell.connect(self._cells[n_coord], d_coord)
93
104
 
94
105
  def _connect_single_cell_2d(self, cell: T, offsets: list[tuple[int, int]]) -> None:
95
106
  i, j = cell.coordinate
@@ -100,7 +111,7 @@ class Grid(DiscreteSpace, Generic[T]):
100
111
  if self.torus:
101
112
  ni, nj = ni % height, nj % width
102
113
  if 0 <= ni < height and 0 <= nj < width:
103
- cell.connect(self._cells[ni, nj])
114
+ cell.connect(self._cells[ni, nj], (di, dj))
104
115
 
105
116
 
106
117
  class OrthogonalMooreGrid(Grid[T]):
@@ -122,7 +133,6 @@ class OrthogonalMooreGrid(Grid[T]):
122
133
  ( 1, -1), ( 1, 0), ( 1, 1),
123
134
  ]
124
135
  # fmt: on
125
- height, width = self.dimensions
126
136
 
127
137
  for cell in self.all_cells:
128
138
  self._connect_single_cell_2d(cell, offsets)
@@ -154,13 +164,12 @@ class OrthogonalVonNeumannGrid(Grid[T]):
154
164
  ( 1, 0),
155
165
  ]
156
166
  # fmt: on
157
- height, width = self.dimensions
158
167
 
159
168
  for cell in self.all_cells:
160
169
  self._connect_single_cell_2d(cell, offsets)
161
170
 
162
171
  def _connect_cells_nd(self) -> None:
163
- offsets = []
172
+ offsets: list[tuple[int, ...]] = []
164
173
  dimensions = len(self.dimensions)
165
174
  for dim in range(dimensions):
166
175
  for delta in [
@@ -176,6 +185,8 @@ class OrthogonalVonNeumannGrid(Grid[T]):
176
185
 
177
186
 
178
187
  class HexGrid(Grid[T]):
188
+ """A Grid with hexagonal tilling of the space."""
189
+
179
190
  def _connect_cells_2d(self) -> None:
180
191
  # fmt: off
181
192
  even_offsets = [
@@ -1,3 +1,5 @@
1
+ """A Network grid."""
2
+
1
3
  from random import Random
2
4
  from typing import Any
3
5
 
@@ -5,8 +7,8 @@ from mesa.experimental.cell_space.cell import Cell
5
7
  from mesa.experimental.cell_space.discrete_space import DiscreteSpace
6
8
 
7
9
 
8
- class Network(DiscreteSpace):
9
- """A networked discrete space"""
10
+ class Network(DiscreteSpace[Cell]):
11
+ """A networked discrete space."""
10
12
 
11
13
  def __init__(
12
14
  self,
@@ -15,13 +17,13 @@ class Network(DiscreteSpace):
15
17
  random: Random | None = None,
16
18
  cell_klass: type[Cell] = Cell,
17
19
  ) -> None:
18
- """A Networked grid
20
+ """A Networked grid.
19
21
 
20
22
  Args:
21
23
  G: a NetworkX Graph instance.
22
24
  capacity (int) : the capacity of the cell
23
- random (Random):
24
- CellKlass (type[Cell]): The base Cell class to use in the Network
25
+ random (Random): a random number generator
26
+ cell_klass (type[Cell]): The base Cell class to use in the Network
25
27
 
26
28
  """
27
29
  super().__init__(capacity=capacity, random=random, cell_klass=cell_klass)
@@ -35,6 +37,6 @@ class Network(DiscreteSpace):
35
37
  for cell in self.all_cells:
36
38
  self._connect_single_cell(cell)
37
39
 
38
- def _connect_single_cell(self, cell):
40
+ def _connect_single_cell(self, cell: Cell):
39
41
  for node_id in self.G.neighbors(cell.coordinate):
40
- cell.connect(self._cells[node_id])
42
+ cell.connect(self._cells[node_id], node_id)
@@ -1,3 +1,5 @@
1
+ """Support for Voronoi meshed grids."""
2
+
1
3
  from collections.abc import Sequence
2
4
  from itertools import combinations
3
5
  from random import Random
@@ -9,16 +11,17 @@ from mesa.experimental.cell_space.discrete_space import DiscreteSpace
9
11
 
10
12
 
11
13
  class Delaunay:
12
- """
13
- Class to compute a Delaunay triangulation in 2D
14
+ """Class to compute a Delaunay triangulation in 2D.
15
+
14
16
  ref: http://github.com/jmespadero/pyDelaunay2D
15
17
  """
16
18
 
17
19
  def __init__(self, center: tuple = (0, 0), radius: int = 9999) -> None:
18
- """
19
- Init and create a new frame to contain the triangulation
20
- center: Optional position for the center of the frame. Default (0,0)
21
- radius: Optional distance from corners to the center.
20
+ """Init and create a new frame to contain the triangulation.
21
+
22
+ Args:
23
+ center: Optional position for the center of the frame. Default (0,0)
24
+ radius: Optional distance from corners to the center.
22
25
  """
23
26
  center = np.asarray(center)
24
27
  # Create coordinates for the corners of the frame
@@ -44,9 +47,7 @@ class Delaunay:
44
47
  self.circles[t] = self._circumcenter(t)
45
48
 
46
49
  def _circumcenter(self, triangle: list) -> tuple:
47
- """
48
- Compute circumcenter and circumradius of a triangle in 2D.
49
- """
50
+ """Compute circumcenter and circumradius of a triangle in 2D."""
50
51
  points = np.asarray([self.coords[v] for v in triangle])
51
52
  points2 = np.dot(points, points.T)
52
53
  a = np.bmat([[2 * points2, [[1], [1], [1]]], [[[1, 1, 1, 0]]]])
@@ -60,16 +61,12 @@ class Delaunay:
60
61
  return (center, radius)
61
62
 
62
63
  def _in_circle(self, triangle: list, point: list) -> bool:
63
- """
64
- Check if point p is inside of precomputed circumcircle of triangle.
65
- """
64
+ """Check if point p is inside of precomputed circumcircle of triangle."""
66
65
  center, radius = self.circles[triangle]
67
66
  return np.sum(np.square(center - point)) <= radius
68
67
 
69
68
  def add_point(self, point: Sequence) -> None:
70
- """
71
- Add a point to the current DT, and refine it using Bowyer-Watson.
72
- """
69
+ """Add a point to the current DT, and refine it using Bowyer-Watson."""
73
70
  point_index = len(self.coords)
74
71
  self.coords.append(np.asarray(point))
75
72
 
@@ -121,9 +118,7 @@ class Delaunay:
121
118
  self.triangles[triangle][2] = new_triangles[(i - 1) % n] # previous
122
119
 
123
120
  def export_triangles(self) -> list:
124
- """
125
- Export the current list of Delaunay triangles
126
- """
121
+ """Export the current list of Delaunay triangles."""
127
122
  triangles_list = [
128
123
  (a - 4, b - 4, c - 4)
129
124
  for (a, b, c) in self.triangles
@@ -132,9 +127,7 @@ class Delaunay:
132
127
  return triangles_list
133
128
 
134
129
  def export_voronoi_regions(self):
135
- """
136
- Export coordinates and regions of Voronoi diagram as indexed data.
137
- """
130
+ """Export coordinates and regions of Voronoi diagram as indexed data."""
138
131
  use_vertex = {i: [] for i in range(len(self.coords))}
139
132
  vor_coors = []
140
133
  index = {}
@@ -163,11 +156,13 @@ class Delaunay:
163
156
  return vor_coors, regions
164
157
 
165
158
 
166
- def round_float(x: float) -> int:
159
+ def round_float(x: float) -> int: # noqa
167
160
  return int(x * 500)
168
161
 
169
162
 
170
163
  class VoronoiGrid(DiscreteSpace):
164
+ """Voronoi meshed GridSpace."""
165
+
171
166
  triangulation: Delaunay
172
167
  voronoi_coordinates: list
173
168
  regions: list
@@ -181,8 +176,7 @@ class VoronoiGrid(DiscreteSpace):
181
176
  capacity_function: callable = round_float,
182
177
  cell_coloring_property: str | None = None,
183
178
  ) -> None:
184
- """
185
- A Voronoi Tessellation Grid.
179
+ """A Voronoi Tessellation Grid.
186
180
 
187
181
  Given a set of points, this class creates a grid where a cell is centered in each point,
188
182
  its neighbors are given by Voronoi Tessellation cells neighbors
@@ -192,7 +186,7 @@ class VoronoiGrid(DiscreteSpace):
192
186
  centroids_coordinates: coordinates of centroids to build the tessellation space
193
187
  capacity (int) : capacity of the cells in the discrete space
194
188
  random (Random): random number generator
195
- CellKlass (type[Cell]): type of cell class
189
+ cell_klass (type[Cell]): type of cell class
196
190
  capacity_function (Callable): function to compute (int) capacity according to (float) area
197
191
  cell_coloring_property (str): voronoi visualization polygon fill property
198
192
  """
@@ -215,17 +209,15 @@ class VoronoiGrid(DiscreteSpace):
215
209
  self._build_cell_polygons()
216
210
 
217
211
  def _connect_cells(self) -> None:
218
- """
219
- Connect cells to neighbors based on given centroids and using Delaunay Triangulation
220
- """
212
+ """Connect cells to neighbors based on given centroids and using Delaunay Triangulation."""
221
213
  self.triangulation = Delaunay()
222
214
  for centroid in self.centroids_coordinates:
223
215
  self.triangulation.add_point(centroid)
224
216
 
225
217
  for point in self.triangulation.export_triangles():
226
218
  for i, j in combinations(point, 2):
227
- self._cells[i].connect(self._cells[j])
228
- self._cells[j].connect(self._cells[i])
219
+ self._cells[i].connect(self._cells[j], (i, j))
220
+ self._cells[j].connect(self._cells[i], (j, i))
229
221
 
230
222
  def _validate_parameters(self) -> None:
231
223
  if self.capacity is not None and not isinstance(self.capacity, float | int):
@@ -241,9 +233,10 @@ class VoronoiGrid(DiscreteSpace):
241
233
 
242
234
  def _get_voronoi_regions(self) -> tuple:
243
235
  if self.voronoi_coordinates is None or self.regions is None:
244
- self.voronoi_coordinates, self.regions = (
245
- self.triangulation.export_voronoi_regions()
246
- )
236
+ (
237
+ self.voronoi_coordinates,
238
+ self.regions,
239
+ ) = self.triangulation.export_voronoi_regions()
247
240
  return self.voronoi_coordinates, self.regions
248
241
 
249
242
  @staticmethod
@@ -0,0 +1,81 @@
1
+ """Altair components."""
2
+
3
+ import contextlib
4
+
5
+ import solara
6
+
7
+ with contextlib.suppress(ImportError):
8
+ import altair as alt
9
+
10
+
11
+ @solara.component
12
+ def SpaceAltair(model, agent_portrayal, dependencies: list[any] | None = None):
13
+ """A component that renders a Space using Altair.
14
+
15
+ Args:
16
+ model: a model instance
17
+ agent_portrayal: agent portray specification
18
+ dependencies: optional list of dependencies (currently not used)
19
+
20
+ """
21
+ space = getattr(model, "grid", None)
22
+ if space is None:
23
+ # Sometimes the space is defined as model.space instead of model.grid
24
+ space = model.space
25
+ chart = _draw_grid(space, agent_portrayal)
26
+ solara.FigureAltair(chart)
27
+
28
+
29
+ def _draw_grid(space, agent_portrayal):
30
+ def portray(g):
31
+ all_agent_data = []
32
+ for content, (x, y) in g.coord_iter():
33
+ if not content:
34
+ continue
35
+ if not hasattr(content, "__iter__"):
36
+ # Is a single grid
37
+ content = [content] # noqa: PLW2901
38
+ for agent in content:
39
+ # use all data from agent portrayal, and add x,y coordinates
40
+ agent_data = agent_portrayal(agent)
41
+ agent_data["x"] = x
42
+ agent_data["y"] = y
43
+ all_agent_data.append(agent_data)
44
+ return all_agent_data
45
+
46
+ all_agent_data = portray(space)
47
+ invalid_tooltips = ["color", "size", "x", "y"]
48
+
49
+ encoding_dict = {
50
+ # no x-axis label
51
+ "x": alt.X("x", axis=None, type="ordinal"),
52
+ # no y-axis label
53
+ "y": alt.Y("y", axis=None, type="ordinal"),
54
+ "tooltip": [
55
+ alt.Tooltip(key, type=alt.utils.infer_vegalite_type([value]))
56
+ for key, value in all_agent_data[0].items()
57
+ if key not in invalid_tooltips
58
+ ],
59
+ }
60
+ has_color = "color" in all_agent_data[0]
61
+ if has_color:
62
+ encoding_dict["color"] = alt.Color("color", type="nominal")
63
+ has_size = "size" in all_agent_data[0]
64
+ if has_size:
65
+ encoding_dict["size"] = alt.Size("size", type="quantitative")
66
+
67
+ chart = (
68
+ alt.Chart(
69
+ alt.Data(values=all_agent_data), encoding=alt.Encoding(**encoding_dict)
70
+ )
71
+ .mark_point(filled=True)
72
+ .properties(width=280, height=280)
73
+ # .configure_view(strokeOpacity=0) # hide grid/chart lines
74
+ )
75
+ # This is the default value for the marker size, which auto-scales
76
+ # according to the grid area.
77
+ if not has_size:
78
+ length = min(space.width, space.height)
79
+ chart = chart.mark_point(size=30000 / length**2, filled=True)
80
+
81
+ return chart
@@ -0,0 +1,242 @@
1
+ """Support for using matplotlib to draw spaces."""
2
+
3
+ from collections import defaultdict
4
+
5
+ import networkx as nx
6
+ import solara
7
+ from matplotlib.figure import Figure
8
+ from matplotlib.ticker import MaxNLocator
9
+
10
+ import mesa
11
+ from mesa.experimental.cell_space import VoronoiGrid
12
+
13
+
14
+ @solara.component
15
+ def SpaceMatplotlib(model, agent_portrayal, dependencies: list[any] | None = None):
16
+ """A component for rendering a space using Matplotlib.
17
+
18
+ Args:
19
+ model: a model instance
20
+ agent_portrayal: a specification of how to portray an agent.
21
+ dependencies: list of dependencies.
22
+
23
+ """
24
+ space_fig = Figure()
25
+ space_ax = space_fig.subplots()
26
+ space = getattr(model, "grid", None)
27
+ if space is None:
28
+ # Sometimes the space is defined as model.space instead of model.grid
29
+ space = model.space
30
+ if isinstance(space, mesa.space.NetworkGrid):
31
+ _draw_network_grid(space, space_ax, agent_portrayal)
32
+ elif isinstance(space, mesa.space.ContinuousSpace):
33
+ _draw_continuous_space(space, space_ax, agent_portrayal)
34
+ elif isinstance(space, VoronoiGrid):
35
+ _draw_voronoi(space, space_ax, agent_portrayal)
36
+ else:
37
+ _draw_grid(space, space_ax, agent_portrayal)
38
+ solara.FigureMatplotlib(space_fig, format="png", dependencies=dependencies)
39
+
40
+
41
+ # matplotlib scatter does not allow for multiple shapes in one call
42
+ def _split_and_scatter(portray_data, space_ax):
43
+ grouped_data = defaultdict(lambda: {"x": [], "y": [], "s": [], "c": []})
44
+
45
+ # Extract data from the dictionary
46
+ x = portray_data["x"]
47
+ y = portray_data["y"]
48
+ s = portray_data["s"]
49
+ c = portray_data["c"]
50
+ m = portray_data["m"]
51
+
52
+ if not (len(x) == len(y) == len(s) == len(c) == len(m)):
53
+ raise ValueError(
54
+ "Length mismatch in portrayal data lists: "
55
+ f"x: {len(x)}, y: {len(y)}, size: {len(s)}, "
56
+ f"color: {len(c)}, marker: {len(m)}"
57
+ )
58
+
59
+ # Group the data by marker
60
+ for i in range(len(x)):
61
+ marker = m[i]
62
+ grouped_data[marker]["x"].append(x[i])
63
+ grouped_data[marker]["y"].append(y[i])
64
+ grouped_data[marker]["s"].append(s[i])
65
+ grouped_data[marker]["c"].append(c[i])
66
+
67
+ # Plot each group with the same marker
68
+ for marker, data in grouped_data.items():
69
+ space_ax.scatter(data["x"], data["y"], s=data["s"], c=data["c"], marker=marker)
70
+
71
+
72
+ def _draw_grid(space, space_ax, agent_portrayal):
73
+ def portray(g):
74
+ x = []
75
+ y = []
76
+ s = [] # size
77
+ c = [] # color
78
+ m = [] # shape
79
+ for i in range(g.width):
80
+ for j in range(g.height):
81
+ content = g._grid[i][j]
82
+ if not content:
83
+ continue
84
+ if not hasattr(content, "__iter__"):
85
+ # Is a single grid
86
+ content = [content]
87
+ for agent in content:
88
+ data = agent_portrayal(agent)
89
+ x.append(i)
90
+ y.append(j)
91
+
92
+ # This is the default value for the marker size, which auto-scales
93
+ # according to the grid area.
94
+ default_size = (180 / max(g.width, g.height)) ** 2
95
+ # establishing a default prevents misalignment if some agents are not given size, color, etc.
96
+ size = data.get("size", default_size)
97
+ s.append(size)
98
+ color = data.get("color", "tab:blue")
99
+ c.append(color)
100
+ mark = data.get("shape", "o")
101
+ m.append(mark)
102
+ out = {"x": x, "y": y, "s": s, "c": c, "m": m}
103
+ return out
104
+
105
+ space_ax.set_xlim(-1, space.width)
106
+ space_ax.set_ylim(-1, space.height)
107
+ _split_and_scatter(portray(space), space_ax)
108
+
109
+
110
+ def _draw_network_grid(space, space_ax, agent_portrayal):
111
+ graph = space.G
112
+ pos = nx.spring_layout(graph, seed=0)
113
+ nx.draw(
114
+ graph,
115
+ ax=space_ax,
116
+ pos=pos,
117
+ **agent_portrayal(graph),
118
+ )
119
+
120
+
121
+ def _draw_continuous_space(space, space_ax, agent_portrayal):
122
+ def portray(space):
123
+ x = []
124
+ y = []
125
+ s = [] # size
126
+ c = [] # color
127
+ m = [] # shape
128
+ for agent in space._agent_to_index:
129
+ data = agent_portrayal(agent)
130
+ _x, _y = agent.pos
131
+ x.append(_x)
132
+ y.append(_y)
133
+
134
+ # This is matplotlib's default marker size
135
+ default_size = 20
136
+ # establishing a default prevents misalignment if some agents are not given size, color, etc.
137
+ size = data.get("size", default_size)
138
+ s.append(size)
139
+ color = data.get("color", "tab:blue")
140
+ c.append(color)
141
+ mark = data.get("shape", "o")
142
+ m.append(mark)
143
+ out = {"x": x, "y": y, "s": s, "c": c, "m": m}
144
+ return out
145
+
146
+ # Determine border style based on space.torus
147
+ border_style = "solid" if not space.torus else (0, (5, 10))
148
+
149
+ # Set the border of the plot
150
+ for spine in space_ax.spines.values():
151
+ spine.set_linewidth(1.5)
152
+ spine.set_color("black")
153
+ spine.set_linestyle(border_style)
154
+
155
+ width = space.x_max - space.x_min
156
+ x_padding = width / 20
157
+ height = space.y_max - space.y_min
158
+ y_padding = height / 20
159
+ space_ax.set_xlim(space.x_min - x_padding, space.x_max + x_padding)
160
+ space_ax.set_ylim(space.y_min - y_padding, space.y_max + y_padding)
161
+
162
+ # Portray and scatter the agents in the space
163
+ _split_and_scatter(portray(space), space_ax)
164
+
165
+
166
+ def _draw_voronoi(space, space_ax, agent_portrayal):
167
+ def portray(g):
168
+ x = []
169
+ y = []
170
+ s = [] # size
171
+ c = [] # color
172
+
173
+ for cell in g.all_cells:
174
+ for agent in cell.agents:
175
+ data = agent_portrayal(agent)
176
+ x.append(cell.coordinate[0])
177
+ y.append(cell.coordinate[1])
178
+ if "size" in data:
179
+ s.append(data["size"])
180
+ if "color" in data:
181
+ c.append(data["color"])
182
+ out = {"x": x, "y": y}
183
+ # This is the default value for the marker size, which auto-scales
184
+ # according to the grid area.
185
+ out["s"] = s
186
+ if len(c) > 0:
187
+ out["c"] = c
188
+
189
+ return out
190
+
191
+ x_list = [i[0] for i in space.centroids_coordinates]
192
+ y_list = [i[1] for i in space.centroids_coordinates]
193
+ x_max = max(x_list)
194
+ x_min = min(x_list)
195
+ y_max = max(y_list)
196
+ y_min = min(y_list)
197
+
198
+ width = x_max - x_min
199
+ x_padding = width / 20
200
+ height = y_max - y_min
201
+ y_padding = height / 20
202
+ space_ax.set_xlim(x_min - x_padding, x_max + x_padding)
203
+ space_ax.set_ylim(y_min - y_padding, y_max + y_padding)
204
+ space_ax.scatter(**portray(space))
205
+
206
+ for cell in space.all_cells:
207
+ polygon = cell.properties["polygon"]
208
+ space_ax.fill(
209
+ *zip(*polygon),
210
+ alpha=min(1, cell.properties[space.cell_coloring_property]),
211
+ c="red",
212
+ ) # Plot filled polygon
213
+ space_ax.plot(*zip(*polygon), color="black") # Plot polygon edges in red
214
+
215
+
216
+ @solara.component
217
+ def PlotMatplotlib(model, measure, dependencies: list[any] | None = None):
218
+ """A solara component for creating a matplotlib figure.
219
+
220
+ Args:
221
+ model: Model instance
222
+ measure: measure to plot
223
+ dependencies: list of additional dependencies
224
+
225
+ """
226
+ fig = Figure()
227
+ ax = fig.subplots()
228
+ df = model.datacollector.get_model_vars_dataframe()
229
+ if isinstance(measure, str):
230
+ ax.plot(df.loc[:, measure])
231
+ ax.set_ylabel(measure)
232
+ elif isinstance(measure, dict):
233
+ for m, color in measure.items():
234
+ ax.plot(df.loc[:, m], label=m, color=color)
235
+ fig.legend()
236
+ elif isinstance(measure, list | tuple):
237
+ for m in measure:
238
+ ax.plot(df.loc[:, m], label=m)
239
+ fig.legend()
240
+ # Set integer x axis
241
+ ax.xaxis.set_major_locator(MaxNLocator(integer=True))
242
+ solara.FigureMatplotlib(fig, dependencies=dependencies)
@@ -1,3 +1,5 @@
1
+ """Support for event scheduling."""
2
+
1
3
  from .eventlist import Priority, SimulationEvent
2
4
  from .simulator import ABMSimulator, DEVSimulator
3
5