Mesa 2.2.3__py3-none-any.whl → 2.3.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.
- mesa/__init__.py +2 -1
- mesa/agent.py +36 -31
- mesa/datacollection.py +12 -8
- mesa/experimental/UserParam.py +56 -0
- mesa/experimental/__init__.py +5 -1
- mesa/experimental/cell_space/__init__.py +23 -0
- mesa/experimental/cell_space/cell.py +152 -0
- mesa/experimental/cell_space/cell_agent.py +37 -0
- mesa/experimental/cell_space/cell_collection.py +81 -0
- mesa/experimental/cell_space/discrete_space.py +64 -0
- mesa/experimental/cell_space/grid.py +204 -0
- mesa/experimental/cell_space/network.py +40 -0
- mesa/experimental/components/altair.py +72 -0
- mesa/experimental/components/matplotlib.py +6 -2
- mesa/experimental/devs/__init__.py +4 -0
- mesa/experimental/devs/eventlist.py +166 -0
- mesa/experimental/devs/examples/epstein_civil_violence.py +273 -0
- mesa/experimental/devs/examples/wolf_sheep.py +250 -0
- mesa/experimental/devs/simulator.py +293 -0
- mesa/experimental/jupyter_viz.py +122 -60
- mesa/main.py +8 -5
- mesa/model.py +15 -4
- mesa/space.py +32 -16
- mesa/time.py +13 -113
- {mesa-2.2.3.dist-info → mesa-2.3.0.dist-info}/METADATA +12 -9
- mesa-2.3.0.dist-info/RECORD +45 -0
- {mesa-2.2.3.dist-info → mesa-2.3.0.dist-info}/WHEEL +1 -1
- mesa-2.2.3.dist-info/RECORD +0 -31
- {mesa-2.2.3.dist-info → mesa-2.3.0.dist-info}/entry_points.txt +0 -0
- {mesa-2.2.3.dist-info → mesa-2.3.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from itertools import product
|
|
5
|
+
from random import Random
|
|
6
|
+
from typing import Generic, TypeVar
|
|
7
|
+
|
|
8
|
+
from mesa.experimental.cell_space import Cell, DiscreteSpace
|
|
9
|
+
|
|
10
|
+
T = TypeVar("T", bound=Cell)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Grid(DiscreteSpace, Generic[T]):
|
|
14
|
+
"""Base class for all grid classes
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
dimensions (Sequence[int]): the dimensions of the grid
|
|
18
|
+
torus (bool): whether the grid is a torus
|
|
19
|
+
capacity (int): the capacity of a grid cell
|
|
20
|
+
random (Random): the random number generator
|
|
21
|
+
_try_random (bool): whether to get empty cell be repeatedly trying random cell
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
dimensions: Sequence[int],
|
|
28
|
+
torus: bool = False,
|
|
29
|
+
capacity: float | None = None,
|
|
30
|
+
random: Random | None = None,
|
|
31
|
+
cell_klass: type[T] = Cell,
|
|
32
|
+
) -> None:
|
|
33
|
+
super().__init__(capacity=capacity, random=random, cell_klass=cell_klass)
|
|
34
|
+
self.torus = torus
|
|
35
|
+
self.dimensions = dimensions
|
|
36
|
+
self._try_random = True
|
|
37
|
+
self._ndims = len(dimensions)
|
|
38
|
+
self._validate_parameters()
|
|
39
|
+
|
|
40
|
+
coordinates = product(*(range(dim) for dim in self.dimensions))
|
|
41
|
+
|
|
42
|
+
self._cells = {
|
|
43
|
+
coord: cell_klass(coord, capacity, random=self.random)
|
|
44
|
+
for coord in coordinates
|
|
45
|
+
}
|
|
46
|
+
self._connect_cells()
|
|
47
|
+
|
|
48
|
+
def _connect_cells(self) -> None:
|
|
49
|
+
if self._ndims == 2:
|
|
50
|
+
self._connect_cells_2d()
|
|
51
|
+
else:
|
|
52
|
+
self._connect_cells_nd()
|
|
53
|
+
|
|
54
|
+
def _connect_cells_2d(self) -> None: ...
|
|
55
|
+
|
|
56
|
+
def _connect_cells_nd(self) -> None: ...
|
|
57
|
+
|
|
58
|
+
def _validate_parameters(self):
|
|
59
|
+
if not all(isinstance(dim, int) and dim > 0 for dim in self.dimensions):
|
|
60
|
+
raise ValueError("Dimensions must be a list of positive integers.")
|
|
61
|
+
if not isinstance(self.torus, bool):
|
|
62
|
+
raise ValueError("Torus must be a boolean.")
|
|
63
|
+
if self.capacity is not None and not isinstance(self.capacity, (float, int)):
|
|
64
|
+
raise ValueError("Capacity must be a number or None.")
|
|
65
|
+
|
|
66
|
+
def select_random_empty_cell(self) -> T:
|
|
67
|
+
# FIXME:: currently just a simple boolean to control behavior
|
|
68
|
+
# FIXME:: basically if grid is close to 99% full, creating empty list can be faster
|
|
69
|
+
# FIXME:: note however that the old results don't apply because in this implementation
|
|
70
|
+
# FIXME:: because empties list needs to be rebuild each time
|
|
71
|
+
# This method is based on Agents.jl's random_empty() implementation. See
|
|
72
|
+
# https://github.com/JuliaDynamics/Agents.jl/pull/541. For the discussion, see
|
|
73
|
+
# https://github.com/projectmesa/mesa/issues/1052 and
|
|
74
|
+
# https://github.com/projectmesa/mesa/pull/1565. The cutoff value provided
|
|
75
|
+
# is the break-even comparison with the time taken in the else branching point.
|
|
76
|
+
if self._try_random:
|
|
77
|
+
while True:
|
|
78
|
+
cell = self.all_cells.select_random_cell()
|
|
79
|
+
if cell.is_empty:
|
|
80
|
+
return cell
|
|
81
|
+
else:
|
|
82
|
+
return super().select_random_empty_cell()
|
|
83
|
+
|
|
84
|
+
def _connect_single_cell_nd(self, cell: T, offsets: list[tuple[int, ...]]) -> None:
|
|
85
|
+
coord = cell.coordinate
|
|
86
|
+
|
|
87
|
+
for d_coord in offsets:
|
|
88
|
+
n_coord = tuple(c + dc for c, dc in zip(coord, d_coord))
|
|
89
|
+
if self.torus:
|
|
90
|
+
n_coord = tuple(nc % d for nc, d in zip(n_coord, self.dimensions))
|
|
91
|
+
if all(0 <= nc < d for nc, d in zip(n_coord, self.dimensions)):
|
|
92
|
+
cell.connect(self._cells[n_coord])
|
|
93
|
+
|
|
94
|
+
def _connect_single_cell_2d(self, cell: T, offsets: list[tuple[int, int]]) -> None:
|
|
95
|
+
i, j = cell.coordinate
|
|
96
|
+
height, width = self.dimensions
|
|
97
|
+
|
|
98
|
+
for di, dj in offsets:
|
|
99
|
+
ni, nj = (i + di, j + dj)
|
|
100
|
+
if self.torus:
|
|
101
|
+
ni, nj = ni % height, nj % width
|
|
102
|
+
if 0 <= ni < height and 0 <= nj < width:
|
|
103
|
+
cell.connect(self._cells[ni, nj])
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class OrthogonalMooreGrid(Grid[T]):
|
|
107
|
+
"""Grid where cells are connected to their 8 neighbors.
|
|
108
|
+
|
|
109
|
+
Example for two dimensions:
|
|
110
|
+
directions = [
|
|
111
|
+
(-1, -1), (-1, 0), (-1, 1),
|
|
112
|
+
( 0, -1), ( 0, 1),
|
|
113
|
+
( 1, -1), ( 1, 0), ( 1, 1),
|
|
114
|
+
]
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
def _connect_cells_2d(self) -> None:
|
|
118
|
+
# fmt: off
|
|
119
|
+
offsets = [
|
|
120
|
+
(-1, -1), (-1, 0), (-1, 1),
|
|
121
|
+
( 0, -1), ( 0, 1),
|
|
122
|
+
( 1, -1), ( 1, 0), ( 1, 1),
|
|
123
|
+
]
|
|
124
|
+
# fmt: on
|
|
125
|
+
height, width = self.dimensions
|
|
126
|
+
|
|
127
|
+
for cell in self.all_cells:
|
|
128
|
+
self._connect_single_cell_2d(cell, offsets)
|
|
129
|
+
|
|
130
|
+
def _connect_cells_nd(self) -> None:
|
|
131
|
+
offsets = list(product([-1, 0, 1], repeat=len(self.dimensions)))
|
|
132
|
+
offsets.remove((0,) * len(self.dimensions)) # Remove the central cell
|
|
133
|
+
|
|
134
|
+
for cell in self.all_cells:
|
|
135
|
+
self._connect_single_cell_nd(cell, offsets)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class OrthogonalVonNeumannGrid(Grid[T]):
|
|
139
|
+
"""Grid where cells are connected to their 4 neighbors.
|
|
140
|
+
|
|
141
|
+
Example for two dimensions:
|
|
142
|
+
directions = [
|
|
143
|
+
(0, -1),
|
|
144
|
+
(-1, 0), ( 1, 0),
|
|
145
|
+
(0, 1),
|
|
146
|
+
]
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
def _connect_cells_2d(self) -> None:
|
|
150
|
+
# fmt: off
|
|
151
|
+
offsets = [
|
|
152
|
+
(-1, 0),
|
|
153
|
+
(0, -1), (0, 1),
|
|
154
|
+
( 1, 0),
|
|
155
|
+
]
|
|
156
|
+
# fmt: on
|
|
157
|
+
height, width = self.dimensions
|
|
158
|
+
|
|
159
|
+
for cell in self.all_cells:
|
|
160
|
+
self._connect_single_cell_2d(cell, offsets)
|
|
161
|
+
|
|
162
|
+
def _connect_cells_nd(self) -> None:
|
|
163
|
+
offsets = []
|
|
164
|
+
dimensions = len(self.dimensions)
|
|
165
|
+
for dim in range(dimensions):
|
|
166
|
+
for delta in [
|
|
167
|
+
-1,
|
|
168
|
+
1,
|
|
169
|
+
]: # Move one step in each direction for the current dimension
|
|
170
|
+
offset = [0] * dimensions
|
|
171
|
+
offset[dim] = delta
|
|
172
|
+
offsets.append(tuple(offset))
|
|
173
|
+
|
|
174
|
+
for cell in self.all_cells:
|
|
175
|
+
self._connect_single_cell_nd(cell, offsets)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class HexGrid(Grid[T]):
|
|
179
|
+
def _connect_cells_2d(self) -> None:
|
|
180
|
+
# fmt: off
|
|
181
|
+
even_offsets = [
|
|
182
|
+
(-1, -1), (-1, 0),
|
|
183
|
+
( 0, -1), ( 0, 1),
|
|
184
|
+
( 1, -1), ( 1, 0),
|
|
185
|
+
]
|
|
186
|
+
odd_offsets = [
|
|
187
|
+
(-1, 0), (-1, 1),
|
|
188
|
+
( 0, -1), ( 0, 1),
|
|
189
|
+
( 1, 0), ( 1, 1),
|
|
190
|
+
]
|
|
191
|
+
# fmt: on
|
|
192
|
+
|
|
193
|
+
for cell in self.all_cells:
|
|
194
|
+
i = cell.coordinate[0]
|
|
195
|
+
offsets = even_offsets if i % 2 == 0 else odd_offsets
|
|
196
|
+
self._connect_single_cell_2d(cell, offsets=offsets)
|
|
197
|
+
|
|
198
|
+
def _connect_cells_nd(self) -> None:
|
|
199
|
+
raise NotImplementedError("HexGrids are only defined for 2 dimensions")
|
|
200
|
+
|
|
201
|
+
def _validate_parameters(self):
|
|
202
|
+
super()._validate_parameters()
|
|
203
|
+
if len(self.dimensions) != 2:
|
|
204
|
+
raise ValueError("HexGrid must have exactly 2 dimensions.")
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from random import Random
|
|
2
|
+
from typing import Any, Optional
|
|
3
|
+
|
|
4
|
+
from mesa.experimental.cell_space.cell import Cell
|
|
5
|
+
from mesa.experimental.cell_space.discrete_space import DiscreteSpace
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Network(DiscreteSpace):
|
|
9
|
+
"""A networked discrete space"""
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
G: Any, # noqa: N803
|
|
14
|
+
capacity: Optional[int] = None,
|
|
15
|
+
random: Optional[Random] = None,
|
|
16
|
+
cell_klass: type[Cell] = Cell,
|
|
17
|
+
) -> None:
|
|
18
|
+
"""A Networked grid
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
G: a NetworkX Graph instance.
|
|
22
|
+
capacity (int) : the capacity of the cell
|
|
23
|
+
random (Random):
|
|
24
|
+
CellKlass (type[Cell]): The base Cell class to use in the Network
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
super().__init__(capacity=capacity, random=random, cell_klass=cell_klass)
|
|
28
|
+
self.G = G
|
|
29
|
+
|
|
30
|
+
for node_id in self.G.nodes:
|
|
31
|
+
self._cells[node_id] = self.cell_klass(
|
|
32
|
+
node_id, capacity, random=self.random
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
for cell in self.all_cells:
|
|
36
|
+
self._connect_single_cell(cell)
|
|
37
|
+
|
|
38
|
+
def _connect_single_cell(self, cell):
|
|
39
|
+
for node_id in self.G.neighbors(cell.coordinate):
|
|
40
|
+
cell.connect(self._cells[node_id])
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
import solara
|
|
5
|
+
|
|
6
|
+
with contextlib.suppress(ImportError):
|
|
7
|
+
import altair as alt
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@solara.component
|
|
11
|
+
def SpaceAltair(model, agent_portrayal, dependencies: Optional[list[any]] = None):
|
|
12
|
+
space = getattr(model, "grid", None)
|
|
13
|
+
if space is None:
|
|
14
|
+
# Sometimes the space is defined as model.space instead of model.grid
|
|
15
|
+
space = model.space
|
|
16
|
+
chart = _draw_grid(space, agent_portrayal)
|
|
17
|
+
solara.FigureAltair(chart)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _draw_grid(space, agent_portrayal):
|
|
21
|
+
def portray(g):
|
|
22
|
+
all_agent_data = []
|
|
23
|
+
for content, (x, y) in g.coord_iter():
|
|
24
|
+
if not content:
|
|
25
|
+
continue
|
|
26
|
+
if not hasattr(content, "__iter__"):
|
|
27
|
+
# Is a single grid
|
|
28
|
+
content = [content] # noqa: PLW2901
|
|
29
|
+
for agent in content:
|
|
30
|
+
# use all data from agent portrayal, and add x,y coordinates
|
|
31
|
+
agent_data = agent_portrayal(agent)
|
|
32
|
+
agent_data["x"] = x
|
|
33
|
+
agent_data["y"] = y
|
|
34
|
+
all_agent_data.append(agent_data)
|
|
35
|
+
return all_agent_data
|
|
36
|
+
|
|
37
|
+
all_agent_data = portray(space)
|
|
38
|
+
invalid_tooltips = ["color", "size", "x", "y"]
|
|
39
|
+
|
|
40
|
+
encoding_dict = {
|
|
41
|
+
# no x-axis label
|
|
42
|
+
"x": alt.X("x", axis=None, type="ordinal"),
|
|
43
|
+
# no y-axis label
|
|
44
|
+
"y": alt.Y("y", axis=None, type="ordinal"),
|
|
45
|
+
"tooltip": [
|
|
46
|
+
alt.Tooltip(key, type=alt.utils.infer_vegalite_type([value]))
|
|
47
|
+
for key, value in all_agent_data[0].items()
|
|
48
|
+
if key not in invalid_tooltips
|
|
49
|
+
],
|
|
50
|
+
}
|
|
51
|
+
has_color = "color" in all_agent_data[0]
|
|
52
|
+
if has_color:
|
|
53
|
+
encoding_dict["color"] = alt.Color("color", type="nominal")
|
|
54
|
+
has_size = "size" in all_agent_data[0]
|
|
55
|
+
if has_size:
|
|
56
|
+
encoding_dict["size"] = alt.Size("size", type="quantitative")
|
|
57
|
+
|
|
58
|
+
chart = (
|
|
59
|
+
alt.Chart(
|
|
60
|
+
alt.Data(values=all_agent_data), encoding=alt.Encoding(**encoding_dict)
|
|
61
|
+
)
|
|
62
|
+
.mark_point(filled=True)
|
|
63
|
+
.properties(width=280, height=280)
|
|
64
|
+
# .configure_view(strokeOpacity=0) # hide grid/chart lines
|
|
65
|
+
)
|
|
66
|
+
# This is the default value for the marker size, which auto-scales
|
|
67
|
+
# according to the grid area.
|
|
68
|
+
if not has_size:
|
|
69
|
+
length = min(space.width, space.height)
|
|
70
|
+
chart = chart.mark_point(size=30000 / length**2, filled=True)
|
|
71
|
+
|
|
72
|
+
return chart
|
|
@@ -48,6 +48,9 @@ def _draw_grid(space, space_ax, agent_portrayal):
|
|
|
48
48
|
if "color" in data:
|
|
49
49
|
c.append(data["color"])
|
|
50
50
|
out = {"x": x, "y": y}
|
|
51
|
+
# This is the default value for the marker size, which auto-scales
|
|
52
|
+
# according to the grid area.
|
|
53
|
+
out["s"] = (180 / min(g.width, g.height)) ** 2
|
|
51
54
|
if len(s) > 0:
|
|
52
55
|
out["s"] = s
|
|
53
56
|
if len(c) > 0:
|
|
@@ -112,7 +115,8 @@ def _draw_continuous_space(space, space_ax, agent_portrayal):
|
|
|
112
115
|
space_ax.scatter(**portray(space))
|
|
113
116
|
|
|
114
117
|
|
|
115
|
-
|
|
118
|
+
@solara.component
|
|
119
|
+
def PlotMatplotlib(model, measure, dependencies: Optional[list[any]] = None):
|
|
116
120
|
fig = Figure()
|
|
117
121
|
ax = fig.subplots()
|
|
118
122
|
df = model.datacollector.get_model_vars_dataframe()
|
|
@@ -129,4 +133,4 @@ def make_plot(model, measure):
|
|
|
129
133
|
fig.legend()
|
|
130
134
|
# Set integer x axis
|
|
131
135
|
ax.xaxis.set_major_locator(MaxNLocator(integer=True))
|
|
132
|
-
solara.FigureMatplotlib(fig)
|
|
136
|
+
solara.FigureMatplotlib(fig, dependencies=dependencies)
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import itertools
|
|
4
|
+
from enum import IntEnum
|
|
5
|
+
from heapq import heapify, heappop, heappush
|
|
6
|
+
from types import MethodType
|
|
7
|
+
from typing import Any, Callable
|
|
8
|
+
from weakref import WeakMethod, ref
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Priority(IntEnum):
|
|
12
|
+
LOW = 10
|
|
13
|
+
DEFAULT = 5
|
|
14
|
+
HIGH = 1
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SimulationEvent:
|
|
18
|
+
"""A simulation event
|
|
19
|
+
|
|
20
|
+
the callable is wrapped using weakref, so there is no need to explicitly cancel event if e.g., an agent
|
|
21
|
+
is removed from the simulation.
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
time (float): The simulation time of the event
|
|
25
|
+
fn (Callable): The function to execute for this event
|
|
26
|
+
priority (Priority): The priority of the event
|
|
27
|
+
unique_id (int) the unique identifier of the event
|
|
28
|
+
function_args (list[Any]): Argument for the function
|
|
29
|
+
function_kwargs (Dict[str, Any]): Keyword arguments for the function
|
|
30
|
+
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
_ids = itertools.count()
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def CANCELED(self) -> bool:
|
|
37
|
+
return self._canceled
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
time: int | float,
|
|
42
|
+
function: Callable,
|
|
43
|
+
priority: Priority = Priority.DEFAULT,
|
|
44
|
+
function_args: list[Any] | None = None,
|
|
45
|
+
function_kwargs: dict[str, Any] | None = None,
|
|
46
|
+
) -> None:
|
|
47
|
+
super().__init__()
|
|
48
|
+
if not callable(function):
|
|
49
|
+
raise Exception()
|
|
50
|
+
|
|
51
|
+
self.time = time
|
|
52
|
+
self.priority = priority.value
|
|
53
|
+
self._canceled = False
|
|
54
|
+
|
|
55
|
+
if isinstance(function, MethodType):
|
|
56
|
+
function = WeakMethod(function)
|
|
57
|
+
else:
|
|
58
|
+
function = ref(function)
|
|
59
|
+
|
|
60
|
+
self.fn = function
|
|
61
|
+
self.unique_id = next(self._ids)
|
|
62
|
+
self.function_args = function_args if function_args else []
|
|
63
|
+
self.function_kwargs = function_kwargs if function_kwargs else {}
|
|
64
|
+
|
|
65
|
+
def execute(self):
|
|
66
|
+
"""execute this event"""
|
|
67
|
+
if not self._canceled:
|
|
68
|
+
fn = self.fn()
|
|
69
|
+
if fn is not None:
|
|
70
|
+
fn(*self.function_args, **self.function_kwargs)
|
|
71
|
+
|
|
72
|
+
def cancel(self) -> None:
|
|
73
|
+
"""cancel this event"""
|
|
74
|
+
self._canceled = True
|
|
75
|
+
self.fn = None
|
|
76
|
+
self.function_args = []
|
|
77
|
+
self.function_kwargs = {}
|
|
78
|
+
|
|
79
|
+
def __lt__(self, other):
|
|
80
|
+
# Define a total ordering for events to be used by the heapq
|
|
81
|
+
return (self.time, self.priority, self.unique_id) < (
|
|
82
|
+
other.time,
|
|
83
|
+
other.priority,
|
|
84
|
+
other.unique_id,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class EventList:
|
|
89
|
+
"""An event list
|
|
90
|
+
|
|
91
|
+
This is a heap queue sorted list of events. Events are always removed from the left, so heapq is a performant and
|
|
92
|
+
appropriate data structure. Events are sorted based on their time stamp, their priority, and their unique_id
|
|
93
|
+
as a tie-breaker, guaranteeing a complete ordering.
|
|
94
|
+
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def __init__(self):
|
|
98
|
+
self._events: list[SimulationEvent] = []
|
|
99
|
+
heapify(self._events)
|
|
100
|
+
|
|
101
|
+
def add_event(self, event: SimulationEvent):
|
|
102
|
+
"""Add the event to the event list
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
event (SimulationEvent): The event to be added
|
|
106
|
+
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
heappush(self._events, event)
|
|
110
|
+
|
|
111
|
+
def peak_ahead(self, n: int = 1) -> list[SimulationEvent]:
|
|
112
|
+
"""Look at the first n non-canceled event in the event list
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
n (int): The number of events to look ahead
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
list[SimulationEvent]
|
|
119
|
+
|
|
120
|
+
Raises:
|
|
121
|
+
IndexError: If the eventlist is empty
|
|
122
|
+
|
|
123
|
+
Notes:
|
|
124
|
+
this method can return a list shorted then n if the number of non-canceled events on the event list
|
|
125
|
+
is less than n.
|
|
126
|
+
|
|
127
|
+
"""
|
|
128
|
+
# look n events ahead
|
|
129
|
+
if self.is_empty():
|
|
130
|
+
raise IndexError("event list is empty")
|
|
131
|
+
|
|
132
|
+
peek: list[SimulationEvent] = []
|
|
133
|
+
for event in self._events:
|
|
134
|
+
if not event.CANCELED:
|
|
135
|
+
peek.append(event)
|
|
136
|
+
if len(peek) >= n:
|
|
137
|
+
return peek
|
|
138
|
+
return peek
|
|
139
|
+
|
|
140
|
+
def pop_event(self) -> SimulationEvent:
|
|
141
|
+
"""pop the first element from the event list"""
|
|
142
|
+
while self._events:
|
|
143
|
+
event = heappop(self._events)
|
|
144
|
+
if not event.CANCELED:
|
|
145
|
+
return event
|
|
146
|
+
raise IndexError("Event list is empty")
|
|
147
|
+
|
|
148
|
+
def is_empty(self) -> bool:
|
|
149
|
+
return len(self) == 0
|
|
150
|
+
|
|
151
|
+
def __contains__(self, event: SimulationEvent) -> bool:
|
|
152
|
+
return event in self._events
|
|
153
|
+
|
|
154
|
+
def __len__(self) -> int:
|
|
155
|
+
return len(self._events)
|
|
156
|
+
|
|
157
|
+
def remove(self, event: SimulationEvent) -> None:
|
|
158
|
+
"""remove an event from the event list"""
|
|
159
|
+
# we cannot simply remove items from _eventlist because this breaks
|
|
160
|
+
# heap structure invariant. So, we use a form of lazy deletion.
|
|
161
|
+
# SimEvents have a CANCELED flag that we set to True, while popping and peak_ahead
|
|
162
|
+
# silently ignore canceled events
|
|
163
|
+
event.cancel()
|
|
164
|
+
|
|
165
|
+
def clear(self):
|
|
166
|
+
self._events.clear()
|