Mesa 2.2.4__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.

@@ -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
- def make_plot(model, measure):
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,4 @@
1
+ from .eventlist import Priority, SimulationEvent
2
+ from .simulator import ABMSimulator, DEVSimulator
3
+
4
+ __all__ = ["ABMSimulator", "DEVSimulator", "SimulationEvent", "Priority"]
@@ -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()