Mesa 3.0.0a4__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.
- mesa/__init__.py +2 -3
- mesa/agent.py +106 -61
- mesa/batchrunner.py +15 -23
- mesa/cookiecutter-mesa/hooks/post_gen_project.py +2 -0
- mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/__init__.py +1 -0
- mesa/datacollection.py +138 -27
- mesa/experimental/UserParam.py +17 -6
- mesa/experimental/__init__.py +2 -0
- mesa/experimental/cell_space/__init__.py +7 -0
- mesa/experimental/cell_space/cell.py +61 -20
- mesa/experimental/cell_space/cell_agent.py +10 -5
- mesa/experimental/cell_space/cell_collection.py +54 -17
- mesa/experimental/cell_space/discrete_space.py +16 -5
- mesa/experimental/cell_space/grid.py +19 -8
- mesa/experimental/cell_space/network.py +9 -7
- mesa/experimental/cell_space/voronoi.py +26 -33
- mesa/experimental/components/altair.py +10 -0
- mesa/experimental/components/matplotlib.py +18 -0
- mesa/experimental/devs/__init__.py +2 -0
- mesa/experimental/devs/eventlist.py +36 -15
- mesa/experimental/devs/examples/epstein_civil_violence.py +65 -29
- mesa/experimental/devs/examples/wolf_sheep.py +38 -34
- mesa/experimental/devs/simulator.py +55 -15
- mesa/experimental/solara_viz.py +10 -19
- mesa/main.py +6 -4
- mesa/model.py +43 -45
- mesa/space.py +145 -120
- mesa/time.py +57 -67
- mesa/visualization/UserParam.py +19 -6
- mesa/visualization/__init__.py +3 -2
- mesa/visualization/components/altair.py +4 -2
- mesa/visualization/components/matplotlib.py +6 -4
- mesa/visualization/solara_viz.py +157 -83
- mesa/visualization/utils.py +3 -1
- {mesa-3.0.0a4.dist-info → mesa-3.0.0a5.dist-info}/METADATA +1 -1
- mesa-3.0.0a5.dist-info/RECORD +44 -0
- mesa-3.0.0a4.dist-info/RECORD +0 -44
- {mesa-3.0.0a4.dist-info → mesa-3.0.0a5.dist-info}/WHEEL +0 -0
- {mesa-3.0.0a4.dist-info → mesa-3.0.0a5.dist-info}/entry_points.txt +0 -0
- {mesa-3.0.0a4.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
|
-
"""
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
245
|
-
self.
|
|
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
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Altair components."""
|
|
2
|
+
|
|
1
3
|
import contextlib
|
|
2
4
|
|
|
3
5
|
import solara
|
|
@@ -8,6 +10,14 @@ with contextlib.suppress(ImportError):
|
|
|
8
10
|
|
|
9
11
|
@solara.component
|
|
10
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
|
+
"""
|
|
11
21
|
space = getattr(model, "grid", None)
|
|
12
22
|
if space is None:
|
|
13
23
|
# Sometimes the space is defined as model.space instead of model.grid
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Support for using matplotlib to draw spaces."""
|
|
2
|
+
|
|
1
3
|
from collections import defaultdict
|
|
2
4
|
|
|
3
5
|
import networkx as nx
|
|
@@ -11,6 +13,14 @@ from mesa.experimental.cell_space import VoronoiGrid
|
|
|
11
13
|
|
|
12
14
|
@solara.component
|
|
13
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
|
+
"""
|
|
14
24
|
space_fig = Figure()
|
|
15
25
|
space_ax = space_fig.subplots()
|
|
16
26
|
space = getattr(model, "grid", None)
|
|
@@ -205,6 +215,14 @@ def _draw_voronoi(space, space_ax, agent_portrayal):
|
|
|
205
215
|
|
|
206
216
|
@solara.component
|
|
207
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
|
+
"""
|
|
208
226
|
fig = Figure()
|
|
209
227
|
ax = fig.subplots()
|
|
210
228
|
df = model.datacollector.get_model_vars_dataframe()
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Eventlist which is at the core of event scheduling."""
|
|
2
|
+
|
|
1
3
|
from __future__ import annotations
|
|
2
4
|
|
|
3
5
|
import itertools
|
|
@@ -10,15 +12,17 @@ from weakref import WeakMethod, ref
|
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
class Priority(IntEnum):
|
|
15
|
+
"""Enumeration of priority levels."""
|
|
16
|
+
|
|
13
17
|
LOW = 10
|
|
14
18
|
DEFAULT = 5
|
|
15
19
|
HIGH = 1
|
|
16
20
|
|
|
17
21
|
|
|
18
22
|
class SimulationEvent:
|
|
19
|
-
"""A simulation event
|
|
23
|
+
"""A simulation event.
|
|
20
24
|
|
|
21
|
-
|
|
25
|
+
The callable is wrapped using weakref, so there is no need to explicitly cancel event if e.g., an agent
|
|
22
26
|
is removed from the simulation.
|
|
23
27
|
|
|
24
28
|
Attributes:
|
|
@@ -34,7 +38,7 @@ class SimulationEvent:
|
|
|
34
38
|
_ids = itertools.count()
|
|
35
39
|
|
|
36
40
|
@property
|
|
37
|
-
def CANCELED(self) -> bool:
|
|
41
|
+
def CANCELED(self) -> bool: # noqa: D102
|
|
38
42
|
return self._canceled
|
|
39
43
|
|
|
40
44
|
def __init__(
|
|
@@ -45,6 +49,15 @@ class SimulationEvent:
|
|
|
45
49
|
function_args: list[Any] | None = None,
|
|
46
50
|
function_kwargs: dict[str, Any] | None = None,
|
|
47
51
|
) -> None:
|
|
52
|
+
"""Initialize a simulation event.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
time: the instant of time of the simulation event
|
|
56
|
+
function: the callable to invoke
|
|
57
|
+
priority: the priority of the event
|
|
58
|
+
function_args: arguments for callable
|
|
59
|
+
function_kwargs: keyword arguments for the callable
|
|
60
|
+
"""
|
|
48
61
|
super().__init__()
|
|
49
62
|
if not callable(function):
|
|
50
63
|
raise Exception()
|
|
@@ -64,20 +77,20 @@ class SimulationEvent:
|
|
|
64
77
|
self.function_kwargs = function_kwargs if function_kwargs else {}
|
|
65
78
|
|
|
66
79
|
def execute(self):
|
|
67
|
-
"""
|
|
80
|
+
"""Execute this event."""
|
|
68
81
|
if not self._canceled:
|
|
69
82
|
fn = self.fn()
|
|
70
83
|
if fn is not None:
|
|
71
84
|
fn(*self.function_args, **self.function_kwargs)
|
|
72
85
|
|
|
73
86
|
def cancel(self) -> None:
|
|
74
|
-
"""
|
|
87
|
+
"""Cancel this event."""
|
|
75
88
|
self._canceled = True
|
|
76
89
|
self.fn = None
|
|
77
90
|
self.function_args = []
|
|
78
91
|
self.function_kwargs = {}
|
|
79
92
|
|
|
80
|
-
def __lt__(self, other):
|
|
93
|
+
def __lt__(self, other): # noqa
|
|
81
94
|
# Define a total ordering for events to be used by the heapq
|
|
82
95
|
return (self.time, self.priority, self.unique_id) < (
|
|
83
96
|
other.time,
|
|
@@ -87,30 +100,31 @@ class SimulationEvent:
|
|
|
87
100
|
|
|
88
101
|
|
|
89
102
|
class EventList:
|
|
90
|
-
"""An event list
|
|
103
|
+
"""An event list.
|
|
91
104
|
|
|
92
105
|
This is a heap queue sorted list of events. Events are always removed from the left, so heapq is a performant and
|
|
93
106
|
appropriate data structure. Events are sorted based on their time stamp, their priority, and their unique_id
|
|
94
107
|
as a tie-breaker, guaranteeing a complete ordering.
|
|
95
108
|
|
|
109
|
+
|
|
96
110
|
"""
|
|
97
111
|
|
|
98
112
|
def __init__(self):
|
|
113
|
+
"""Initialize an event list."""
|
|
99
114
|
self._events: list[SimulationEvent] = []
|
|
100
115
|
heapify(self._events)
|
|
101
116
|
|
|
102
117
|
def add_event(self, event: SimulationEvent):
|
|
103
|
-
"""Add the event to the event list
|
|
118
|
+
"""Add the event to the event list.
|
|
104
119
|
|
|
105
120
|
Args:
|
|
106
121
|
event (SimulationEvent): The event to be added
|
|
107
122
|
|
|
108
123
|
"""
|
|
109
|
-
|
|
110
124
|
heappush(self._events, event)
|
|
111
125
|
|
|
112
126
|
def peak_ahead(self, n: int = 1) -> list[SimulationEvent]:
|
|
113
|
-
"""Look at the first n non-canceled event in the event list
|
|
127
|
+
"""Look at the first n non-canceled event in the event list.
|
|
114
128
|
|
|
115
129
|
Args:
|
|
116
130
|
n (int): The number of events to look ahead
|
|
@@ -139,7 +153,7 @@ class EventList:
|
|
|
139
153
|
return peek
|
|
140
154
|
|
|
141
155
|
def pop_event(self) -> SimulationEvent:
|
|
142
|
-
"""
|
|
156
|
+
"""Pop the first element from the event list."""
|
|
143
157
|
while self._events:
|
|
144
158
|
event = heappop(self._events)
|
|
145
159
|
if not event.CANCELED:
|
|
@@ -147,16 +161,17 @@ class EventList:
|
|
|
147
161
|
raise IndexError("Event list is empty")
|
|
148
162
|
|
|
149
163
|
def is_empty(self) -> bool:
|
|
164
|
+
"""Return whether the event list is empty."""
|
|
150
165
|
return len(self) == 0
|
|
151
166
|
|
|
152
|
-
def __contains__(self, event: SimulationEvent) -> bool:
|
|
167
|
+
def __contains__(self, event: SimulationEvent) -> bool: # noqa
|
|
153
168
|
return event in self._events
|
|
154
169
|
|
|
155
|
-
def __len__(self) -> int:
|
|
170
|
+
def __len__(self) -> int: # noqa
|
|
156
171
|
return len(self._events)
|
|
157
172
|
|
|
158
173
|
def __repr__(self) -> str:
|
|
159
|
-
"""Return a string representation of the event list"""
|
|
174
|
+
"""Return a string representation of the event list."""
|
|
160
175
|
events_str = ", ".join(
|
|
161
176
|
[
|
|
162
177
|
f"Event(time={e.time}, priority={e.priority}, id={e.unique_id})"
|
|
@@ -167,7 +182,12 @@ class EventList:
|
|
|
167
182
|
return f"EventList([{events_str}])"
|
|
168
183
|
|
|
169
184
|
def remove(self, event: SimulationEvent) -> None:
|
|
170
|
-
"""
|
|
185
|
+
"""Remove an event from the event list.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
event (SimulationEvent): The event to be removed
|
|
189
|
+
|
|
190
|
+
"""
|
|
171
191
|
# we cannot simply remove items from _eventlist because this breaks
|
|
172
192
|
# heap structure invariant. So, we use a form of lazy deletion.
|
|
173
193
|
# SimEvents have a CANCELED flag that we set to True, while popping and peak_ahead
|
|
@@ -175,4 +195,5 @@ class EventList:
|
|
|
175
195
|
event.cancel()
|
|
176
196
|
|
|
177
197
|
def clear(self):
|
|
198
|
+
"""Clear the event list."""
|
|
178
199
|
self._events.clear()
|