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.
- mesa/__init__.py +2 -1
- mesa/agent.py +35 -31
- mesa/datacollection.py +4 -1
- 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 +121 -59
- mesa/main.py +8 -5
- mesa/model.py +5 -4
- mesa/space.py +32 -16
- mesa/time.py +3 -110
- {mesa-2.2.4.dist-info → mesa-2.3.0.dist-info}/METADATA +12 -9
- mesa-2.3.0.dist-info/RECORD +45 -0
- {mesa-2.2.4.dist-info → mesa-2.3.0.dist-info}/WHEEL +1 -1
- mesa-2.2.4.dist-info/RECORD +0 -31
- {mesa-2.2.4.dist-info → mesa-2.3.0.dist-info}/entry_points.txt +0 -0
- {mesa-2.2.4.dist-info → mesa-2.3.0.dist-info}/licenses/LICENSE +0 -0
mesa/__init__.py
CHANGED
|
@@ -3,6 +3,7 @@ Mesa Agent-Based Modeling Framework
|
|
|
3
3
|
|
|
4
4
|
Core Objects: Model, and Agent.
|
|
5
5
|
"""
|
|
6
|
+
|
|
6
7
|
import datetime
|
|
7
8
|
|
|
8
9
|
import mesa.space as space
|
|
@@ -25,7 +26,7 @@ __all__ = [
|
|
|
25
26
|
]
|
|
26
27
|
|
|
27
28
|
__title__ = "mesa"
|
|
28
|
-
__version__ = "2.
|
|
29
|
+
__version__ = "2.3.0"
|
|
29
30
|
__license__ = "Apache 2.0"
|
|
30
31
|
_this_year = datetime.datetime.now(tz=datetime.timezone.utc).date().year
|
|
31
32
|
__copyright__ = f"Copyright {_this_year} Project Mesa Team"
|
mesa/agent.py
CHANGED
|
@@ -3,6 +3,7 @@ The agent class for Mesa framework.
|
|
|
3
3
|
|
|
4
4
|
Core Objects: Agent
|
|
5
5
|
"""
|
|
6
|
+
|
|
6
7
|
# Mypy; for the `|` operator purpose
|
|
7
8
|
# Remove this __future__ import once the oldest supported Python is 3.10
|
|
8
9
|
from __future__ import annotations
|
|
@@ -81,12 +82,6 @@ class Agent:
|
|
|
81
82
|
|
|
82
83
|
class AgentSet(MutableSet, Sequence):
|
|
83
84
|
"""
|
|
84
|
-
.. warning::
|
|
85
|
-
The AgentSet is experimental. It may be changed or removed in any and all future releases, including
|
|
86
|
-
patch releases.
|
|
87
|
-
We would love to hear what you think about this new feature. If you have any thoughts, share them with
|
|
88
|
-
us here: https://github.com/projectmesa/mesa/discussions/1919
|
|
89
|
-
|
|
90
85
|
A collection class that represents an ordered set of agents within an agent-based model (ABM). This class
|
|
91
86
|
extends both MutableSet and Sequence, providing set-like functionality with order preservation and
|
|
92
87
|
sequence operations.
|
|
@@ -115,17 +110,8 @@ class AgentSet(MutableSet, Sequence):
|
|
|
115
110
|
agents (Iterable[Agent]): An iterable of Agent objects to be included in the set.
|
|
116
111
|
model (Model): The ABM model instance to which this AgentSet belongs.
|
|
117
112
|
"""
|
|
118
|
-
self.model = model
|
|
119
|
-
|
|
120
|
-
if not self.__class__.agentset_experimental_warning_given:
|
|
121
|
-
self.__class__.agentset_experimental_warning_given = True
|
|
122
|
-
warnings.warn(
|
|
123
|
-
"The AgentSet is experimental. It may be changed or removed in any and all future releases, including patch releases.\n"
|
|
124
|
-
"We would love to hear what you think about this new feature. If you have any thoughts, share them with us here: https://github.com/projectmesa/mesa/discussions/1919",
|
|
125
|
-
FutureWarning,
|
|
126
|
-
stacklevel=2,
|
|
127
|
-
)
|
|
128
113
|
|
|
114
|
+
self.model = model
|
|
129
115
|
self._agents = weakref.WeakKeyDictionary({agent: None for agent in agents})
|
|
130
116
|
|
|
131
117
|
def __len__(self) -> int:
|
|
@@ -188,15 +174,21 @@ class AgentSet(MutableSet, Sequence):
|
|
|
188
174
|
|
|
189
175
|
Returns:
|
|
190
176
|
AgentSet: A shuffled AgentSet. Returns the current AgentSet if inplace is True.
|
|
191
|
-
"""
|
|
192
|
-
shuffled_agents = list(self)
|
|
193
|
-
self.random.shuffle(shuffled_agents)
|
|
194
177
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
)
|
|
178
|
+
Note:
|
|
179
|
+
Using inplace = True is more performant
|
|
180
|
+
|
|
181
|
+
"""
|
|
182
|
+
weakrefs = list(self._agents.keyrefs())
|
|
183
|
+
self.random.shuffle(weakrefs)
|
|
184
|
+
|
|
185
|
+
if inplace:
|
|
186
|
+
self._agents.data = {entry: None for entry in weakrefs}
|
|
187
|
+
return self
|
|
188
|
+
else:
|
|
189
|
+
return AgentSet(
|
|
190
|
+
(agent for ref in weakrefs if (agent := ref()) is not None), self.model
|
|
191
|
+
)
|
|
200
192
|
|
|
201
193
|
def sort(
|
|
202
194
|
self,
|
|
@@ -251,24 +243,36 @@ class AgentSet(MutableSet, Sequence):
|
|
|
251
243
|
"""
|
|
252
244
|
# we iterate over the actual weakref keys and check if weakref is alive before calling the method
|
|
253
245
|
res = [
|
|
254
|
-
getattr(
|
|
246
|
+
getattr(agent, method_name)(*args, **kwargs)
|
|
255
247
|
for agentref in self._agents.keyrefs()
|
|
256
|
-
if agentref()
|
|
248
|
+
if (agent := agentref()) is not None
|
|
257
249
|
]
|
|
258
250
|
|
|
259
251
|
return res if return_results else self
|
|
260
252
|
|
|
261
|
-
def get(self,
|
|
253
|
+
def get(self, attr_names: str | list[str]) -> list[Any]:
|
|
262
254
|
"""
|
|
263
|
-
Retrieve
|
|
255
|
+
Retrieve the specified attribute(s) from each agent in the AgentSet.
|
|
264
256
|
|
|
265
257
|
Args:
|
|
266
|
-
|
|
258
|
+
attr_names (str | list[str]): The name(s) of the attribute(s) to retrieve from each agent.
|
|
267
259
|
|
|
268
260
|
Returns:
|
|
269
|
-
list[Any]: A list
|
|
261
|
+
list[Any]: A list with the attribute value for each agent in the set if attr_names is a str
|
|
262
|
+
list[list[Any]]: A list with a list of attribute values for each agent in the set if attr_names is a list of str
|
|
263
|
+
|
|
264
|
+
Raises:
|
|
265
|
+
AttributeError if an agent does not have the specified attribute(s)
|
|
266
|
+
|
|
270
267
|
"""
|
|
271
|
-
|
|
268
|
+
|
|
269
|
+
if isinstance(attr_names, str):
|
|
270
|
+
return [getattr(agent, attr_names) for agent in self._agents]
|
|
271
|
+
else:
|
|
272
|
+
return [
|
|
273
|
+
[getattr(agent, attr_name) for attr_name in attr_names]
|
|
274
|
+
for agent in self._agents
|
|
275
|
+
]
|
|
272
276
|
|
|
273
277
|
def __getitem__(self, item: int | slice) -> Agent:
|
|
274
278
|
"""
|
mesa/datacollection.py
CHANGED
|
@@ -32,6 +32,7 @@ The default DataCollector here makes several assumptions:
|
|
|
32
32
|
* The model has an agent list called agents
|
|
33
33
|
* For collecting agent-level variables, agents must have a unique_id
|
|
34
34
|
"""
|
|
35
|
+
|
|
35
36
|
import contextlib
|
|
36
37
|
import itertools
|
|
37
38
|
import types
|
|
@@ -184,7 +185,9 @@ class DataCollector:
|
|
|
184
185
|
|
|
185
186
|
agent_records = map(
|
|
186
187
|
get_reports,
|
|
187
|
-
model.schedule.agents
|
|
188
|
+
model.schedule.agents
|
|
189
|
+
if hasattr(model, "schedule") and model.schedule is not None
|
|
190
|
+
else model.agents,
|
|
188
191
|
)
|
|
189
192
|
return agent_records
|
|
190
193
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
class UserParam:
|
|
2
|
+
_ERROR_MESSAGE = "Missing or malformed inputs for '{}' Option '{}'"
|
|
3
|
+
|
|
4
|
+
def maybe_raise_error(self, param_type, valid):
|
|
5
|
+
if valid:
|
|
6
|
+
return
|
|
7
|
+
msg = self._ERROR_MESSAGE.format(param_type, self.label)
|
|
8
|
+
raise ValueError(msg)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Slider(UserParam):
|
|
12
|
+
"""
|
|
13
|
+
A number-based slider input with settable increment.
|
|
14
|
+
|
|
15
|
+
Example:
|
|
16
|
+
|
|
17
|
+
slider_option = Slider("My Slider", value=123, min=10, max=200, step=0.1)
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
label: The displayed label in the UI
|
|
21
|
+
value: The initial value of the slider
|
|
22
|
+
min: The minimum possible value of the slider
|
|
23
|
+
max: The maximum possible value of the slider
|
|
24
|
+
step: The step between min and max for a range of possible values
|
|
25
|
+
dtype: either int or float
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
label="",
|
|
31
|
+
value=None,
|
|
32
|
+
min=None,
|
|
33
|
+
max=None,
|
|
34
|
+
step=1,
|
|
35
|
+
dtype=None,
|
|
36
|
+
):
|
|
37
|
+
self.label = label
|
|
38
|
+
self.value = value
|
|
39
|
+
self.min = min
|
|
40
|
+
self.max = max
|
|
41
|
+
self.step = step
|
|
42
|
+
|
|
43
|
+
# Validate option type to make sure values are supplied properly
|
|
44
|
+
valid = not (self.value is None or self.min is None or self.max is None)
|
|
45
|
+
self.maybe_raise_error("slider", valid)
|
|
46
|
+
|
|
47
|
+
if dtype is None:
|
|
48
|
+
self.is_float_slider = self._check_values_are_float(value, min, max, step)
|
|
49
|
+
else:
|
|
50
|
+
self.is_float_slider = dtype == float
|
|
51
|
+
|
|
52
|
+
def _check_values_are_float(self, value, min, max, step):
|
|
53
|
+
return any(isinstance(n, float) for n in (value, min, max, step))
|
|
54
|
+
|
|
55
|
+
def get(self, attr):
|
|
56
|
+
return getattr(self, attr)
|
mesa/experimental/__init__.py
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from mesa.experimental.cell_space.cell import Cell
|
|
2
|
+
from mesa.experimental.cell_space.cell_agent import CellAgent
|
|
3
|
+
from mesa.experimental.cell_space.cell_collection import CellCollection
|
|
4
|
+
from mesa.experimental.cell_space.discrete_space import DiscreteSpace
|
|
5
|
+
from mesa.experimental.cell_space.grid import (
|
|
6
|
+
Grid,
|
|
7
|
+
HexGrid,
|
|
8
|
+
OrthogonalMooreGrid,
|
|
9
|
+
OrthogonalVonNeumannGrid,
|
|
10
|
+
)
|
|
11
|
+
from mesa.experimental.cell_space.network import Network
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"CellCollection",
|
|
15
|
+
"Cell",
|
|
16
|
+
"CellAgent",
|
|
17
|
+
"DiscreteSpace",
|
|
18
|
+
"Grid",
|
|
19
|
+
"HexGrid",
|
|
20
|
+
"OrthogonalMooreGrid",
|
|
21
|
+
"OrthogonalVonNeumannGrid",
|
|
22
|
+
"Network",
|
|
23
|
+
]
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from functools import cache
|
|
4
|
+
from random import Random
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from mesa.experimental.cell_space.cell_collection import CellCollection
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from mesa.experimental.cell_space.cell_agent import CellAgent
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Cell:
|
|
14
|
+
"""The cell represents a position in a discrete space.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
coordinate (Tuple[int, int]) : the position of the cell in the discrete space
|
|
18
|
+
agents (List[Agent]): the agents occupying the cell
|
|
19
|
+
capacity (int): the maximum number of agents that can simultaneously occupy the cell
|
|
20
|
+
properties (dict[str, Any]): the properties of the cell
|
|
21
|
+
random (Random): the random number generator
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
__slots__ = [
|
|
26
|
+
"coordinate",
|
|
27
|
+
"_connections",
|
|
28
|
+
"agents",
|
|
29
|
+
"capacity",
|
|
30
|
+
"properties",
|
|
31
|
+
"random",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
# def __new__(cls,
|
|
35
|
+
# coordinate: tuple[int, ...],
|
|
36
|
+
# capacity: float | None = None,
|
|
37
|
+
# random: Random | None = None,):
|
|
38
|
+
# if capacity != 1:
|
|
39
|
+
# return object.__new__(cls)
|
|
40
|
+
# else:
|
|
41
|
+
# return object.__new__(SingleAgentCell)
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
coordinate: tuple[int, ...],
|
|
46
|
+
capacity: float | None = None,
|
|
47
|
+
random: Random | None = None,
|
|
48
|
+
) -> None:
|
|
49
|
+
""" "
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
coordinate:
|
|
53
|
+
capacity (int) : the capacity of the cell. If None, the capacity is infinite
|
|
54
|
+
random (Random) : the random number generator to use
|
|
55
|
+
|
|
56
|
+
"""
|
|
57
|
+
super().__init__()
|
|
58
|
+
self.coordinate = coordinate
|
|
59
|
+
self._connections: list[Cell] = [] # TODO: change to CellCollection?
|
|
60
|
+
self.agents = [] # TODO:: change to AgentSet or weakrefs? (neither is very performant, )
|
|
61
|
+
self.capacity = capacity
|
|
62
|
+
self.properties: dict[str, object] = {}
|
|
63
|
+
self.random = random
|
|
64
|
+
|
|
65
|
+
def connect(self, other: Cell) -> None:
|
|
66
|
+
"""Connects this cell to another cell.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
other (Cell): other cell to connect to
|
|
70
|
+
|
|
71
|
+
"""
|
|
72
|
+
self._connections.append(other)
|
|
73
|
+
|
|
74
|
+
def disconnect(self, other: Cell) -> None:
|
|
75
|
+
"""Disconnects this cell from another cell.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
other (Cell): other cell to remove from connections
|
|
79
|
+
|
|
80
|
+
"""
|
|
81
|
+
self._connections.remove(other)
|
|
82
|
+
|
|
83
|
+
def add_agent(self, agent: CellAgent) -> None:
|
|
84
|
+
"""Adds an agent to the cell.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
agent (CellAgent): agent to add to this Cell
|
|
88
|
+
|
|
89
|
+
"""
|
|
90
|
+
n = len(self.agents)
|
|
91
|
+
|
|
92
|
+
if self.capacity and n >= self.capacity:
|
|
93
|
+
raise Exception(
|
|
94
|
+
"ERROR: Cell is full"
|
|
95
|
+
) # FIXME we need MESA errors or a proper error
|
|
96
|
+
|
|
97
|
+
self.agents.append(agent)
|
|
98
|
+
|
|
99
|
+
def remove_agent(self, agent: CellAgent) -> None:
|
|
100
|
+
"""Removes an agent from the cell.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
agent (CellAgent): agent to remove from this cell
|
|
104
|
+
|
|
105
|
+
"""
|
|
106
|
+
self.agents.remove(agent)
|
|
107
|
+
agent.cell = None
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def is_empty(self) -> bool:
|
|
111
|
+
"""Returns a bool of the contents of a cell."""
|
|
112
|
+
return len(self.agents) == 0
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def is_full(self) -> bool:
|
|
116
|
+
"""Returns a bool of the contents of a cell."""
|
|
117
|
+
return len(self.agents) == self.capacity
|
|
118
|
+
|
|
119
|
+
def __repr__(self):
|
|
120
|
+
return f"Cell({self.coordinate}, {self.agents})"
|
|
121
|
+
|
|
122
|
+
# FIXME: Revisit caching strategy on methods
|
|
123
|
+
@cache # noqa: B019
|
|
124
|
+
def neighborhood(self, radius=1, include_center=False):
|
|
125
|
+
return CellCollection(
|
|
126
|
+
self._neighborhood(radius=radius, include_center=include_center),
|
|
127
|
+
random=self.random,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# FIXME: Revisit caching strategy on methods
|
|
131
|
+
@cache # noqa: B019
|
|
132
|
+
def _neighborhood(self, radius=1, include_center=False):
|
|
133
|
+
# if radius == 0:
|
|
134
|
+
# return {self: self.agents}
|
|
135
|
+
if radius < 1:
|
|
136
|
+
raise ValueError("radius must be larger than one")
|
|
137
|
+
if radius == 1:
|
|
138
|
+
neighborhood = {neighbor: neighbor.agents for neighbor in self._connections}
|
|
139
|
+
if not include_center:
|
|
140
|
+
return neighborhood
|
|
141
|
+
else:
|
|
142
|
+
neighborhood[self] = self.agents
|
|
143
|
+
return neighborhood
|
|
144
|
+
else:
|
|
145
|
+
neighborhood = {}
|
|
146
|
+
for neighbor in self._connections:
|
|
147
|
+
neighborhood.update(
|
|
148
|
+
neighbor._neighborhood(radius - 1, include_center=True)
|
|
149
|
+
)
|
|
150
|
+
if not include_center:
|
|
151
|
+
neighborhood.pop(self, None)
|
|
152
|
+
return neighborhood
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from mesa import Agent, Model
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from mesa.experimental.cell_space.cell import Cell
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CellAgent(Agent):
|
|
12
|
+
"""Cell Agent is an extension of the Agent class and adds behavior for moving in discrete spaces
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
Attributes:
|
|
16
|
+
unique_id (int): A unique identifier for this agent.
|
|
17
|
+
model (Model): The model instance to which the agent belongs
|
|
18
|
+
pos: (Position | None): The position of the agent in the space
|
|
19
|
+
cell: (Cell | None): the cell which the agent occupies
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, unique_id: int, model: Model) -> None:
|
|
23
|
+
"""
|
|
24
|
+
Create a new agent.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
unique_id (int): A unique identifier for this agent.
|
|
28
|
+
model (Model): The model instance in which the agent exists.
|
|
29
|
+
"""
|
|
30
|
+
super().__init__(unique_id, model)
|
|
31
|
+
self.cell: Cell | None = None
|
|
32
|
+
|
|
33
|
+
def move_to(self, cell) -> None:
|
|
34
|
+
if self.cell is not None:
|
|
35
|
+
self.cell.remove_agent(self)
|
|
36
|
+
self.cell = cell
|
|
37
|
+
cell.add_agent(self)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import itertools
|
|
4
|
+
from collections.abc import Iterable, Mapping
|
|
5
|
+
from functools import cached_property
|
|
6
|
+
from random import Random
|
|
7
|
+
from typing import TYPE_CHECKING, Callable, Generic, TypeVar
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from mesa.experimental.cell_space.cell import Cell
|
|
11
|
+
from mesa.experimental.cell_space.cell_agent import CellAgent
|
|
12
|
+
|
|
13
|
+
T = TypeVar("T", bound="Cell")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CellCollection(Generic[T]):
|
|
17
|
+
"""An immutable collection of cells
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
cells (List[Cell]): The list of cells this collection represents
|
|
21
|
+
agents (List[CellAgent]) : List of agents occupying the cells in this collection
|
|
22
|
+
random (Random) : The random number generator
|
|
23
|
+
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
cells: Mapping[T, list[CellAgent]] | Iterable[T],
|
|
29
|
+
random: Random | None = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
if isinstance(cells, dict):
|
|
32
|
+
self._cells = cells
|
|
33
|
+
else:
|
|
34
|
+
self._cells = {cell: cell.agents for cell in cells}
|
|
35
|
+
|
|
36
|
+
#
|
|
37
|
+
self._capacity: int = next(iter(self._cells.keys())).capacity
|
|
38
|
+
|
|
39
|
+
if random is None:
|
|
40
|
+
random = Random() # FIXME
|
|
41
|
+
self.random = random
|
|
42
|
+
|
|
43
|
+
def __iter__(self):
|
|
44
|
+
return iter(self._cells)
|
|
45
|
+
|
|
46
|
+
def __getitem__(self, key: T) -> Iterable[CellAgent]:
|
|
47
|
+
return self._cells[key]
|
|
48
|
+
|
|
49
|
+
# @cached_property
|
|
50
|
+
def __len__(self) -> int:
|
|
51
|
+
return len(self._cells)
|
|
52
|
+
|
|
53
|
+
def __repr__(self):
|
|
54
|
+
return f"CellCollection({self._cells})"
|
|
55
|
+
|
|
56
|
+
@cached_property
|
|
57
|
+
def cells(self) -> list[T]:
|
|
58
|
+
return list(self._cells.keys())
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def agents(self) -> Iterable[CellAgent]:
|
|
62
|
+
return itertools.chain.from_iterable(self._cells.values())
|
|
63
|
+
|
|
64
|
+
def select_random_cell(self) -> T:
|
|
65
|
+
return self.random.choice(self.cells)
|
|
66
|
+
|
|
67
|
+
def select_random_agent(self) -> CellAgent:
|
|
68
|
+
return self.random.choice(list(self.agents))
|
|
69
|
+
|
|
70
|
+
def select(self, filter_func: Callable[[T], bool] | None = None, n=0):
|
|
71
|
+
# FIXME: n is not considered
|
|
72
|
+
if filter_func is None and n == 0:
|
|
73
|
+
return self
|
|
74
|
+
|
|
75
|
+
return CellCollection(
|
|
76
|
+
{
|
|
77
|
+
cell: agents
|
|
78
|
+
for cell, agents in self._cells.items()
|
|
79
|
+
if filter_func is None or filter_func(cell)
|
|
80
|
+
}
|
|
81
|
+
)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from functools import cached_property
|
|
4
|
+
from random import Random
|
|
5
|
+
from typing import Generic, TypeVar
|
|
6
|
+
|
|
7
|
+
from mesa.experimental.cell_space.cell import Cell
|
|
8
|
+
from mesa.experimental.cell_space.cell_collection import CellCollection
|
|
9
|
+
|
|
10
|
+
T = TypeVar("T", bound=Cell)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DiscreteSpace(Generic[T]):
|
|
14
|
+
"""Base class for all discrete spaces.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
capacity (int): The capacity of the cells in the discrete space
|
|
18
|
+
all_cells (CellCollection): The cells composing the discrete space
|
|
19
|
+
random (Random): The random number generator
|
|
20
|
+
cell_klass (Type) : the type of cell class
|
|
21
|
+
empties (CellCollection) : collecction of all cells that are empty
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
capacity: int | None = None,
|
|
28
|
+
cell_klass: type[T] = Cell,
|
|
29
|
+
random: Random | None = None,
|
|
30
|
+
):
|
|
31
|
+
super().__init__()
|
|
32
|
+
self.capacity = capacity
|
|
33
|
+
self._cells: dict[tuple[int, ...], T] = {}
|
|
34
|
+
if random is None:
|
|
35
|
+
random = Random() # FIXME should default to default rng from model
|
|
36
|
+
self.random = random
|
|
37
|
+
self.cell_klass = cell_klass
|
|
38
|
+
|
|
39
|
+
self._empties: dict[tuple[int, ...], None] = {}
|
|
40
|
+
self._empties_initialized = False
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def cutoff_empties(self):
|
|
44
|
+
return 7.953 * len(self._cells) ** 0.384
|
|
45
|
+
|
|
46
|
+
def _connect_single_cell(self, cell: T): ...
|
|
47
|
+
|
|
48
|
+
@cached_property
|
|
49
|
+
def all_cells(self):
|
|
50
|
+
return CellCollection({cell: cell.agents for cell in self._cells.values()})
|
|
51
|
+
|
|
52
|
+
def __iter__(self):
|
|
53
|
+
return iter(self._cells.values())
|
|
54
|
+
|
|
55
|
+
def __getitem__(self, key):
|
|
56
|
+
return self._cells[key]
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def empties(self) -> CellCollection:
|
|
60
|
+
return self.all_cells.select(lambda cell: cell.is_empty)
|
|
61
|
+
|
|
62
|
+
def select_random_empty_cell(self) -> T:
|
|
63
|
+
"""select random empty cell"""
|
|
64
|
+
return self.random.choice(list(self.empties))
|