Mesa 2.3.4__py3-none-any.whl → 3.0.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 +3 -5
- mesa/agent.py +393 -116
- mesa/batchrunner.py +58 -31
- mesa/datacollection.py +141 -30
- mesa/examples/README.md +37 -0
- mesa/examples/__init__.py +21 -0
- mesa/examples/advanced/epstein_civil_violence/Epstein Civil Violence.ipynb +116 -0
- mesa/examples/advanced/epstein_civil_violence/Readme.md +34 -0
- mesa/examples/advanced/epstein_civil_violence/__init__.py +0 -0
- mesa/examples/advanced/epstein_civil_violence/agents.py +164 -0
- mesa/examples/advanced/epstein_civil_violence/app.py +73 -0
- mesa/examples/advanced/epstein_civil_violence/model.py +114 -0
- mesa/examples/advanced/pd_grid/Readme.md +43 -0
- mesa/examples/advanced/pd_grid/__init__.py +0 -0
- mesa/examples/advanced/pd_grid/agents.py +50 -0
- mesa/examples/advanced/pd_grid/analysis.ipynb +228 -0
- mesa/examples/advanced/pd_grid/app.py +54 -0
- mesa/examples/advanced/pd_grid/model.py +71 -0
- mesa/examples/advanced/sugarscape_g1mt/Readme.md +64 -0
- mesa/examples/advanced/sugarscape_g1mt/__init__.py +0 -0
- mesa/examples/advanced/sugarscape_g1mt/agents.py +344 -0
- mesa/examples/advanced/sugarscape_g1mt/app.py +62 -0
- mesa/examples/advanced/sugarscape_g1mt/model.py +180 -0
- mesa/examples/advanced/sugarscape_g1mt/sugar-map.txt +50 -0
- mesa/examples/advanced/sugarscape_g1mt/tests.py +69 -0
- mesa/examples/advanced/wolf_sheep/Readme.md +57 -0
- mesa/examples/advanced/wolf_sheep/__init__.py +0 -0
- mesa/examples/advanced/wolf_sheep/agents.py +102 -0
- mesa/examples/advanced/wolf_sheep/app.py +84 -0
- mesa/examples/advanced/wolf_sheep/model.py +137 -0
- mesa/examples/basic/__init__.py +0 -0
- mesa/examples/basic/boid_flockers/Readme.md +22 -0
- mesa/examples/basic/boid_flockers/__init__.py +0 -0
- mesa/examples/basic/boid_flockers/agents.py +71 -0
- mesa/examples/basic/boid_flockers/app.py +58 -0
- mesa/examples/basic/boid_flockers/model.py +69 -0
- mesa/examples/basic/boltzmann_wealth_model/Readme.md +56 -0
- mesa/examples/basic/boltzmann_wealth_model/__init__.py +0 -0
- mesa/examples/basic/boltzmann_wealth_model/agents.py +31 -0
- mesa/examples/basic/boltzmann_wealth_model/app.py +74 -0
- mesa/examples/basic/boltzmann_wealth_model/model.py +43 -0
- mesa/examples/basic/boltzmann_wealth_model/st_app.py +115 -0
- mesa/examples/basic/conways_game_of_life/Readme.md +39 -0
- mesa/examples/basic/conways_game_of_life/__init__.py +0 -0
- mesa/examples/basic/conways_game_of_life/agents.py +47 -0
- mesa/examples/basic/conways_game_of_life/app.py +51 -0
- mesa/examples/basic/conways_game_of_life/model.py +31 -0
- mesa/examples/basic/conways_game_of_life/st_app.py +72 -0
- mesa/examples/basic/schelling/Readme.md +40 -0
- mesa/examples/basic/schelling/__init__.py +0 -0
- mesa/examples/basic/schelling/agents.py +26 -0
- mesa/examples/basic/schelling/analysis.ipynb +205 -0
- mesa/examples/basic/schelling/app.py +42 -0
- mesa/examples/basic/schelling/model.py +59 -0
- mesa/examples/basic/virus_on_network/Readme.md +61 -0
- mesa/examples/basic/virus_on_network/__init__.py +0 -0
- mesa/examples/basic/virus_on_network/agents.py +69 -0
- mesa/examples/basic/virus_on_network/app.py +114 -0
- mesa/examples/basic/virus_on_network/model.py +96 -0
- mesa/experimental/UserParam.py +18 -7
- mesa/experimental/__init__.py +10 -2
- mesa/experimental/cell_space/__init__.py +16 -1
- mesa/experimental/cell_space/cell.py +93 -23
- mesa/experimental/cell_space/cell_agent.py +117 -21
- mesa/experimental/cell_space/cell_collection.py +56 -19
- mesa/experimental/cell_space/discrete_space.py +92 -8
- mesa/experimental/cell_space/grid.py +33 -9
- mesa/experimental/cell_space/network.py +15 -10
- mesa/experimental/cell_space/voronoi.py +257 -0
- mesa/experimental/components/altair.py +11 -2
- mesa/experimental/components/matplotlib.py +132 -26
- mesa/experimental/devs/__init__.py +2 -0
- mesa/experimental/devs/eventlist.py +54 -15
- mesa/experimental/devs/examples/epstein_civil_violence.py +71 -39
- mesa/experimental/devs/examples/wolf_sheep.py +45 -45
- mesa/experimental/devs/simulator.py +57 -16
- mesa/experimental/{jupyter_viz.py → solara_viz.py} +151 -98
- mesa/model.py +212 -84
- mesa/space.py +217 -151
- mesa/time.py +63 -80
- mesa/visualization/__init__.py +25 -6
- mesa/visualization/components/__init__.py +83 -0
- mesa/visualization/components/altair_components.py +188 -0
- mesa/visualization/components/matplotlib_components.py +175 -0
- mesa/visualization/mpl_space_drawing.py +593 -0
- mesa/visualization/solara_viz.py +458 -0
- mesa/visualization/user_param.py +69 -0
- mesa/visualization/utils.py +9 -0
- {mesa-2.3.4.dist-info → mesa-3.0.0.dist-info}/METADATA +65 -19
- mesa-3.0.0.dist-info/RECORD +95 -0
- mesa-3.0.0.dist-info/licenses/LICENSE +202 -0
- mesa-2.3.4.dist-info/licenses/LICENSE → mesa-3.0.0.dist-info/licenses/NOTICE +2 -2
- mesa/cookiecutter-mesa/cookiecutter.json +0 -8
- mesa/cookiecutter-mesa/hooks/post_gen_project.py +0 -11
- mesa/cookiecutter-mesa/{{cookiecutter.snake}}/README.md +0 -4
- mesa/cookiecutter-mesa/{{cookiecutter.snake}}/run.pytemplate +0 -3
- mesa/cookiecutter-mesa/{{cookiecutter.snake}}/setup.pytemplate +0 -11
- mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/model.pytemplate +0 -60
- mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/server.pytemplate +0 -36
- mesa/flat/__init__.py +0 -6
- mesa/flat/visualization.py +0 -5
- mesa/main.py +0 -63
- mesa/visualization/ModularVisualization.py +0 -1
- mesa/visualization/TextVisualization.py +0 -1
- mesa/visualization/UserParam.py +0 -1
- mesa/visualization/modules.py +0 -1
- mesa-2.3.4.dist-info/RECORD +0 -45
- /mesa/{cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}} → examples/advanced}/__init__.py +0 -0
- {mesa-2.3.4.dist-info → mesa-3.0.0.dist-info}/WHEEL +0 -0
- {mesa-2.3.4.dist-info → mesa-3.0.0.dist-info}/entry_points.txt +0 -0
mesa/experimental/UserParam.py
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
"""helper classes."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class UserParam: # noqa: D101
|
|
2
5
|
_ERROR_MESSAGE = "Missing or malformed inputs for '{}' Option '{}'"
|
|
3
6
|
|
|
4
|
-
def maybe_raise_error(self, param_type, valid):
|
|
7
|
+
def maybe_raise_error(self, param_type, valid): # noqa: D102
|
|
5
8
|
if valid:
|
|
6
9
|
return
|
|
7
10
|
msg = self._ERROR_MESSAGE.format(param_type, self.label)
|
|
@@ -9,11 +12,9 @@ class UserParam:
|
|
|
9
12
|
|
|
10
13
|
|
|
11
14
|
class Slider(UserParam):
|
|
12
|
-
"""
|
|
13
|
-
A number-based slider input with settable increment.
|
|
15
|
+
"""A number-based slider input with settable increment.
|
|
14
16
|
|
|
15
17
|
Example:
|
|
16
|
-
|
|
17
18
|
slider_option = Slider("My Slider", value=123, min=10, max=200, step=0.1)
|
|
18
19
|
|
|
19
20
|
Args:
|
|
@@ -34,6 +35,16 @@ class Slider(UserParam):
|
|
|
34
35
|
step=1,
|
|
35
36
|
dtype=None,
|
|
36
37
|
):
|
|
38
|
+
"""Slider class.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
label: The displayed label in the UI
|
|
42
|
+
value: The initial value of the slider
|
|
43
|
+
min: The minimum possible value of the slider
|
|
44
|
+
max: The maximum possible value of the slider
|
|
45
|
+
step: The step between min and max for a range of possible values
|
|
46
|
+
dtype: either int or float
|
|
47
|
+
"""
|
|
37
48
|
self.label = label
|
|
38
49
|
self.value = value
|
|
39
50
|
self.min = min
|
|
@@ -47,10 +58,10 @@ class Slider(UserParam):
|
|
|
47
58
|
if dtype is None:
|
|
48
59
|
self.is_float_slider = self._check_values_are_float(value, min, max, step)
|
|
49
60
|
else:
|
|
50
|
-
self.is_float_slider = dtype
|
|
61
|
+
self.is_float_slider = dtype is float
|
|
51
62
|
|
|
52
63
|
def _check_values_are_float(self, value, min, max, step):
|
|
53
64
|
return any(isinstance(n, float) for n in (value, min, max, step))
|
|
54
65
|
|
|
55
|
-
def get(self, attr):
|
|
66
|
+
def get(self, attr): # noqa: D102
|
|
56
67
|
return getattr(self, attr)
|
mesa/experimental/__init__.py
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
"""Experimental init."""
|
|
2
|
+
|
|
2
3
|
from mesa.experimental import cell_space
|
|
3
4
|
|
|
5
|
+
try:
|
|
6
|
+
from .solara_viz import JupyterViz, Slider, SolaraViz, make_text
|
|
4
7
|
|
|
5
|
-
__all__ = ["
|
|
8
|
+
__all__ = ["cell_space", "JupyterViz", "Slider", "SolaraViz", "make_text"]
|
|
9
|
+
except ImportError:
|
|
10
|
+
print(
|
|
11
|
+
"Could not import SolaraViz. If you need it, install with 'pip install --pre mesa[viz]'"
|
|
12
|
+
)
|
|
13
|
+
__all__ = ["cell_space"]
|
|
@@ -1,5 +1,16 @@
|
|
|
1
|
+
"""Cell spaces.
|
|
2
|
+
|
|
3
|
+
Cell spaces offer an alternative API for discrete spaces. It is experimental and under development. The API is more
|
|
4
|
+
expressive that the default grids available in `mesa.space`.
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
|
|
1
8
|
from mesa.experimental.cell_space.cell import Cell
|
|
2
|
-
from mesa.experimental.cell_space.cell_agent import
|
|
9
|
+
from mesa.experimental.cell_space.cell_agent import (
|
|
10
|
+
CellAgent,
|
|
11
|
+
FixedAgent,
|
|
12
|
+
Grid2DMovingAgent,
|
|
13
|
+
)
|
|
3
14
|
from mesa.experimental.cell_space.cell_collection import CellCollection
|
|
4
15
|
from mesa.experimental.cell_space.discrete_space import DiscreteSpace
|
|
5
16
|
from mesa.experimental.cell_space.grid import (
|
|
@@ -9,15 +20,19 @@ from mesa.experimental.cell_space.grid import (
|
|
|
9
20
|
OrthogonalVonNeumannGrid,
|
|
10
21
|
)
|
|
11
22
|
from mesa.experimental.cell_space.network import Network
|
|
23
|
+
from mesa.experimental.cell_space.voronoi import VoronoiGrid
|
|
12
24
|
|
|
13
25
|
__all__ = [
|
|
14
26
|
"CellCollection",
|
|
15
27
|
"Cell",
|
|
16
28
|
"CellAgent",
|
|
29
|
+
"Grid2DMovingAgent",
|
|
30
|
+
"FixedAgent",
|
|
17
31
|
"DiscreteSpace",
|
|
18
32
|
"Grid",
|
|
19
33
|
"HexGrid",
|
|
20
34
|
"OrthogonalMooreGrid",
|
|
21
35
|
"OrthogonalVonNeumannGrid",
|
|
22
36
|
"Network",
|
|
37
|
+
"VoronoiGrid",
|
|
23
38
|
]
|
|
@@ -1,13 +1,20 @@
|
|
|
1
|
+
"""The Cell in a cell space."""
|
|
2
|
+
|
|
1
3
|
from __future__ import annotations
|
|
2
4
|
|
|
3
|
-
from
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from functools import cache, cached_property
|
|
4
7
|
from random import Random
|
|
5
|
-
from typing import TYPE_CHECKING
|
|
8
|
+
from typing import TYPE_CHECKING, Any
|
|
6
9
|
|
|
10
|
+
from mesa.experimental.cell_space.cell_agent import CellAgent
|
|
7
11
|
from mesa.experimental.cell_space.cell_collection import CellCollection
|
|
12
|
+
from mesa.space import PropertyLayer
|
|
8
13
|
|
|
9
14
|
if TYPE_CHECKING:
|
|
10
|
-
from mesa.
|
|
15
|
+
from mesa.agent import Agent
|
|
16
|
+
|
|
17
|
+
Coordinate = tuple[int, ...]
|
|
11
18
|
|
|
12
19
|
|
|
13
20
|
class Cell:
|
|
@@ -24,11 +31,13 @@ class Cell:
|
|
|
24
31
|
|
|
25
32
|
__slots__ = [
|
|
26
33
|
"coordinate",
|
|
27
|
-
"
|
|
34
|
+
"connections",
|
|
28
35
|
"agents",
|
|
29
36
|
"capacity",
|
|
30
37
|
"properties",
|
|
31
38
|
"random",
|
|
39
|
+
"_mesa_property_layers",
|
|
40
|
+
"__dict__",
|
|
32
41
|
]
|
|
33
42
|
|
|
34
43
|
# def __new__(cls,
|
|
@@ -42,34 +51,40 @@ class Cell:
|
|
|
42
51
|
|
|
43
52
|
def __init__(
|
|
44
53
|
self,
|
|
45
|
-
coordinate:
|
|
46
|
-
capacity:
|
|
54
|
+
coordinate: Coordinate,
|
|
55
|
+
capacity: int | None = None,
|
|
47
56
|
random: Random | None = None,
|
|
48
57
|
) -> None:
|
|
49
|
-
"""
|
|
58
|
+
"""Initialise the cell.
|
|
50
59
|
|
|
51
60
|
Args:
|
|
52
|
-
coordinate:
|
|
61
|
+
coordinate: coordinates of the cell
|
|
53
62
|
capacity (int) : the capacity of the cell. If None, the capacity is infinite
|
|
54
63
|
random (Random) : the random number generator to use
|
|
55
64
|
|
|
56
65
|
"""
|
|
57
66
|
super().__init__()
|
|
58
67
|
self.coordinate = coordinate
|
|
59
|
-
self.
|
|
60
|
-
self.agents
|
|
61
|
-
|
|
62
|
-
|
|
68
|
+
self.connections: dict[Coordinate, Cell] = {}
|
|
69
|
+
self.agents: list[
|
|
70
|
+
Agent
|
|
71
|
+
] = [] # TODO:: change to AgentSet or weakrefs? (neither is very performant, )
|
|
72
|
+
self.capacity: int | None = capacity
|
|
73
|
+
self.properties: dict[Coordinate, object] = {}
|
|
63
74
|
self.random = random
|
|
75
|
+
self._mesa_property_layers: dict[str, PropertyLayer] = {}
|
|
64
76
|
|
|
65
|
-
def connect(self, other: Cell) -> None:
|
|
77
|
+
def connect(self, other: Cell, key: Coordinate | None = None) -> None:
|
|
66
78
|
"""Connects this cell to another cell.
|
|
67
79
|
|
|
68
80
|
Args:
|
|
69
81
|
other (Cell): other cell to connect to
|
|
82
|
+
key (Tuple[int, ...]): key for the connection. Should resemble a relative coordinate
|
|
70
83
|
|
|
71
84
|
"""
|
|
72
|
-
|
|
85
|
+
if key is None:
|
|
86
|
+
key = other.coordinate
|
|
87
|
+
self.connections[key] = other
|
|
73
88
|
|
|
74
89
|
def disconnect(self, other: Cell) -> None:
|
|
75
90
|
"""Disconnects this cell from another cell.
|
|
@@ -78,7 +93,9 @@ class Cell:
|
|
|
78
93
|
other (Cell): other cell to remove from connections
|
|
79
94
|
|
|
80
95
|
"""
|
|
81
|
-
self.
|
|
96
|
+
keys_to_remove = [k for k, v in self.connections.items() if v == other]
|
|
97
|
+
for key in keys_to_remove:
|
|
98
|
+
del self.connections[key]
|
|
82
99
|
|
|
83
100
|
def add_agent(self, agent: CellAgent) -> None:
|
|
84
101
|
"""Adds an agent to the cell.
|
|
@@ -104,7 +121,6 @@ class Cell:
|
|
|
104
121
|
|
|
105
122
|
"""
|
|
106
123
|
self.agents.remove(agent)
|
|
107
|
-
agent.cell = None
|
|
108
124
|
|
|
109
125
|
@property
|
|
110
126
|
def is_empty(self) -> bool:
|
|
@@ -116,37 +132,91 @@ class Cell:
|
|
|
116
132
|
"""Returns a bool of the contents of a cell."""
|
|
117
133
|
return len(self.agents) == self.capacity
|
|
118
134
|
|
|
119
|
-
def __repr__(self):
|
|
135
|
+
def __repr__(self): # noqa
|
|
120
136
|
return f"Cell({self.coordinate}, {self.agents})"
|
|
121
137
|
|
|
138
|
+
@cached_property
|
|
139
|
+
def neighborhood(self) -> CellCollection[Cell]:
|
|
140
|
+
"""Returns the direct neighborhood of the cell.
|
|
141
|
+
|
|
142
|
+
This is equivalent to cell.get_neighborhood(radius=1)
|
|
143
|
+
|
|
144
|
+
"""
|
|
145
|
+
return self.get_neighborhood()
|
|
146
|
+
|
|
122
147
|
# FIXME: Revisit caching strategy on methods
|
|
123
148
|
@cache # noqa: B019
|
|
124
|
-
def
|
|
125
|
-
|
|
149
|
+
def get_neighborhood(
|
|
150
|
+
self, radius: int = 1, include_center: bool = False
|
|
151
|
+
) -> CellCollection[Cell]:
|
|
152
|
+
"""Returns a list of all neighboring cells for the given radius.
|
|
153
|
+
|
|
154
|
+
For getting the direct neighborhood (i.e., radius=1) you can also use
|
|
155
|
+
the `neighborhood` property.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
radius (int): the radius of the neighborhood
|
|
159
|
+
include_center (bool): include the center of the neighborhood
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
a list of all neighboring cells
|
|
163
|
+
|
|
164
|
+
"""
|
|
165
|
+
return CellCollection[Cell](
|
|
126
166
|
self._neighborhood(radius=radius, include_center=include_center),
|
|
127
167
|
random=self.random,
|
|
128
168
|
)
|
|
129
169
|
|
|
130
170
|
# FIXME: Revisit caching strategy on methods
|
|
131
171
|
@cache # noqa: B019
|
|
132
|
-
def _neighborhood(
|
|
172
|
+
def _neighborhood(
|
|
173
|
+
self, radius: int = 1, include_center: bool = False
|
|
174
|
+
) -> dict[Cell, list[Agent]]:
|
|
133
175
|
# if radius == 0:
|
|
134
176
|
# return {self: self.agents}
|
|
135
177
|
if radius < 1:
|
|
136
178
|
raise ValueError("radius must be larger than one")
|
|
137
179
|
if radius == 1:
|
|
138
|
-
neighborhood = {
|
|
180
|
+
neighborhood = {
|
|
181
|
+
neighbor: neighbor.agents for neighbor in self.connections.values()
|
|
182
|
+
}
|
|
139
183
|
if not include_center:
|
|
140
184
|
return neighborhood
|
|
141
185
|
else:
|
|
142
186
|
neighborhood[self] = self.agents
|
|
143
187
|
return neighborhood
|
|
144
188
|
else:
|
|
145
|
-
neighborhood = {}
|
|
146
|
-
for neighbor in self.
|
|
189
|
+
neighborhood: dict[Cell, list[Agent]] = {}
|
|
190
|
+
for neighbor in self.connections.values():
|
|
147
191
|
neighborhood.update(
|
|
148
192
|
neighbor._neighborhood(radius - 1, include_center=True)
|
|
149
193
|
)
|
|
150
194
|
if not include_center:
|
|
151
195
|
neighborhood.pop(self, None)
|
|
152
196
|
return neighborhood
|
|
197
|
+
|
|
198
|
+
# PropertyLayer methods
|
|
199
|
+
def get_property(self, property_name: str) -> Any:
|
|
200
|
+
"""Get the value of a property."""
|
|
201
|
+
return self._mesa_property_layers[property_name].data[self.coordinate]
|
|
202
|
+
|
|
203
|
+
def set_property(self, property_name: str, value: Any):
|
|
204
|
+
"""Set the value of a property."""
|
|
205
|
+
self._mesa_property_layers[property_name].set_cell(self.coordinate, value)
|
|
206
|
+
|
|
207
|
+
def modify_property(
|
|
208
|
+
self, property_name: str, operation: Callable, value: Any = None
|
|
209
|
+
):
|
|
210
|
+
"""Modify the value of a property."""
|
|
211
|
+
self._mesa_property_layers[property_name].modify_cell(
|
|
212
|
+
self.coordinate, operation, value
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
def __getstate__(self):
|
|
216
|
+
"""Return state of the Cell with connections set to empty."""
|
|
217
|
+
# fixme, once we shift to 3.11, replace this with super. __getstate__
|
|
218
|
+
state = (self.__dict__, {k: getattr(self, k) for k in self.__slots__})
|
|
219
|
+
state[1][
|
|
220
|
+
"connections"
|
|
221
|
+
] = {} # replace this with empty connections to avoid infinite recursion error in pickle/deepcopy
|
|
222
|
+
return state
|
|
@@ -1,37 +1,133 @@
|
|
|
1
|
+
"""An agent with movement methods for cell spaces."""
|
|
2
|
+
|
|
1
3
|
from __future__ import annotations
|
|
2
4
|
|
|
3
|
-
from typing import TYPE_CHECKING
|
|
5
|
+
from typing import TYPE_CHECKING, Protocol
|
|
4
6
|
|
|
5
|
-
from mesa import Agent
|
|
7
|
+
from mesa.agent import Agent
|
|
6
8
|
|
|
7
9
|
if TYPE_CHECKING:
|
|
8
|
-
from mesa.experimental.cell_space
|
|
10
|
+
from mesa.experimental.cell_space import Cell
|
|
9
11
|
|
|
10
12
|
|
|
11
|
-
class
|
|
12
|
-
"""
|
|
13
|
+
class HasCellProtocol(Protocol):
|
|
14
|
+
"""Protocol for discrete space cell holders."""
|
|
13
15
|
|
|
16
|
+
cell: Cell
|
|
14
17
|
|
|
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
18
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
Create a new agent.
|
|
19
|
+
class HasCell:
|
|
20
|
+
"""Descriptor for cell movement behavior."""
|
|
25
21
|
|
|
26
|
-
|
|
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
|
|
22
|
+
_mesa_cell: Cell | None = None
|
|
32
23
|
|
|
33
|
-
|
|
24
|
+
@property
|
|
25
|
+
def cell(self) -> Cell | None: # noqa: D102
|
|
26
|
+
return self._mesa_cell
|
|
27
|
+
|
|
28
|
+
@cell.setter
|
|
29
|
+
def cell(self, cell: Cell | None) -> None:
|
|
30
|
+
# remove from current cell
|
|
34
31
|
if self.cell is not None:
|
|
35
32
|
self.cell.remove_agent(self)
|
|
33
|
+
|
|
34
|
+
# update private attribute
|
|
35
|
+
self._mesa_cell = cell
|
|
36
|
+
|
|
37
|
+
# add to new cell
|
|
38
|
+
if cell is not None:
|
|
39
|
+
cell.add_agent(self)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class BasicMovement:
|
|
43
|
+
"""Mixin for moving agents in discrete space."""
|
|
44
|
+
|
|
45
|
+
def move_to(self: HasCellProtocol, cell: Cell) -> None:
|
|
46
|
+
"""Move to a new cell."""
|
|
36
47
|
self.cell = cell
|
|
48
|
+
|
|
49
|
+
def move_relative(self: HasCellProtocol, direction: tuple[int, ...]):
|
|
50
|
+
"""Move to a cell relative to the current cell.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
direction: The direction to move in.
|
|
54
|
+
"""
|
|
55
|
+
new_cell = self.cell.connections.get(direction)
|
|
56
|
+
if new_cell is not None:
|
|
57
|
+
self.cell = new_cell
|
|
58
|
+
else:
|
|
59
|
+
raise ValueError(f"No cell in direction {direction}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class FixedCell(HasCell):
|
|
63
|
+
"""Mixin for agents that are fixed to a cell."""
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def cell(self) -> Cell | None: # noqa: D102
|
|
67
|
+
return self._mesa_cell
|
|
68
|
+
|
|
69
|
+
@cell.setter
|
|
70
|
+
def cell(self, cell: Cell) -> None:
|
|
71
|
+
if self.cell is not None:
|
|
72
|
+
raise ValueError("Cannot move agent in FixedCell")
|
|
73
|
+
self._mesa_cell = cell
|
|
74
|
+
|
|
37
75
|
cell.add_agent(self)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class CellAgent(Agent, HasCell, BasicMovement):
|
|
79
|
+
"""Cell Agent is an extension of the Agent class and adds behavior for moving in discrete spaces.
|
|
80
|
+
|
|
81
|
+
Attributes:
|
|
82
|
+
cell (Cell): The cell the agent is currently in.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def remove(self):
|
|
86
|
+
"""Remove the agent from the model."""
|
|
87
|
+
super().remove()
|
|
88
|
+
self.cell = None # ensures that we are also removed from cell
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class FixedAgent(Agent, FixedCell):
|
|
92
|
+
"""A patch in a 2D grid."""
|
|
93
|
+
|
|
94
|
+
def remove(self):
|
|
95
|
+
"""Remove the agent from the model."""
|
|
96
|
+
super().remove()
|
|
97
|
+
|
|
98
|
+
# fixme we leave self._mesa_cell on the original value
|
|
99
|
+
# so you cannot hijack remove() to move patches
|
|
100
|
+
self.cell.remove_agent(self)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class Grid2DMovingAgent(CellAgent):
|
|
104
|
+
"""Mixin for moving agents in 2D grids."""
|
|
105
|
+
|
|
106
|
+
# fmt: off
|
|
107
|
+
DIRECTION_MAP = {
|
|
108
|
+
"n": (-1, 0), "north": (-1, 0), "up": (-1, 0),
|
|
109
|
+
"s": (1, 0), "south": (1, 0), "down": (1, 0),
|
|
110
|
+
"e": (0, 1), "east": (0, 1), "right": (0, 1),
|
|
111
|
+
"w": (0, -1), "west": (0, -1), "left": (0, -1),
|
|
112
|
+
"ne": (-1, 1), "northeast": (-1, 1), "upright": (-1, 1),
|
|
113
|
+
"nw": (-1, -1), "northwest": (-1, -1), "upleft": (-1, -1),
|
|
114
|
+
"se": (1, 1), "southeast": (1, 1), "downright": (1, 1),
|
|
115
|
+
"sw": (1, -1), "southwest": (1, -1), "downleft": (1, -1)
|
|
116
|
+
}
|
|
117
|
+
# fmt: on
|
|
118
|
+
|
|
119
|
+
def move(self, direction: str, distance: int = 1):
|
|
120
|
+
"""Move the agent in a cardinal direction.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
direction: The cardinal direction to move in.
|
|
124
|
+
distance: The distance to move.
|
|
125
|
+
"""
|
|
126
|
+
direction = direction.lower() # Convert direction to lowercase
|
|
127
|
+
|
|
128
|
+
if direction not in self.DIRECTION_MAP:
|
|
129
|
+
raise ValueError(f"Invalid direction: {direction}")
|
|
130
|
+
|
|
131
|
+
move_vector = self.DIRECTION_MAP[direction]
|
|
132
|
+
for _ in range(distance):
|
|
133
|
+
self.move_relative(move_vector)
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
"""CellCollection class."""
|
|
2
|
+
|
|
1
3
|
from __future__ import annotations
|
|
2
4
|
|
|
3
5
|
import itertools
|
|
4
|
-
from collections.abc import Iterable, Mapping
|
|
6
|
+
from collections.abc import Callable, Iterable, Mapping
|
|
5
7
|
from functools import cached_property
|
|
6
8
|
from random import Random
|
|
7
|
-
from typing import TYPE_CHECKING,
|
|
9
|
+
from typing import TYPE_CHECKING, Generic, TypeVar
|
|
8
10
|
|
|
9
11
|
if TYPE_CHECKING:
|
|
10
12
|
from mesa.experimental.cell_space.cell import Cell
|
|
@@ -14,7 +16,7 @@ T = TypeVar("T", bound="Cell")
|
|
|
14
16
|
|
|
15
17
|
|
|
16
18
|
class CellCollection(Generic[T]):
|
|
17
|
-
"""An immutable collection of cells
|
|
19
|
+
"""An immutable collection of cells.
|
|
18
20
|
|
|
19
21
|
Attributes:
|
|
20
22
|
cells (List[Cell]): The list of cells this collection represents
|
|
@@ -28,6 +30,12 @@ class CellCollection(Generic[T]):
|
|
|
28
30
|
cells: Mapping[T, list[CellAgent]] | Iterable[T],
|
|
29
31
|
random: Random | None = None,
|
|
30
32
|
) -> None:
|
|
33
|
+
"""Initialize a CellCollection.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
cells: cells to add to the collection
|
|
37
|
+
random: a seeded random number generator.
|
|
38
|
+
"""
|
|
31
39
|
if isinstance(cells, dict):
|
|
32
40
|
self._cells = cells
|
|
33
41
|
else:
|
|
@@ -40,42 +48,71 @@ class CellCollection(Generic[T]):
|
|
|
40
48
|
random = Random() # FIXME
|
|
41
49
|
self.random = random
|
|
42
50
|
|
|
43
|
-
def __iter__(self):
|
|
51
|
+
def __iter__(self): # noqa
|
|
44
52
|
return iter(self._cells)
|
|
45
53
|
|
|
46
|
-
def __getitem__(self, key: T) -> Iterable[CellAgent]:
|
|
54
|
+
def __getitem__(self, key: T) -> Iterable[CellAgent]: # noqa
|
|
47
55
|
return self._cells[key]
|
|
48
56
|
|
|
49
57
|
# @cached_property
|
|
50
|
-
def __len__(self) -> int:
|
|
58
|
+
def __len__(self) -> int: # noqa
|
|
51
59
|
return len(self._cells)
|
|
52
60
|
|
|
53
|
-
def __repr__(self):
|
|
61
|
+
def __repr__(self): # noqa
|
|
54
62
|
return f"CellCollection({self._cells})"
|
|
55
63
|
|
|
56
64
|
@cached_property
|
|
57
|
-
def cells(self) -> list[T]:
|
|
65
|
+
def cells(self) -> list[T]: # noqa
|
|
58
66
|
return list(self._cells.keys())
|
|
59
67
|
|
|
60
68
|
@property
|
|
61
|
-
def agents(self) -> Iterable[CellAgent]:
|
|
69
|
+
def agents(self) -> Iterable[CellAgent]: # noqa
|
|
62
70
|
return itertools.chain.from_iterable(self._cells.values())
|
|
63
71
|
|
|
64
72
|
def select_random_cell(self) -> T:
|
|
73
|
+
"""Select a random cell."""
|
|
65
74
|
return self.random.choice(self.cells)
|
|
66
75
|
|
|
67
76
|
def select_random_agent(self) -> CellAgent:
|
|
77
|
+
"""Select a random agent.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
CellAgent instance
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
"""
|
|
68
84
|
return self.random.choice(list(self.agents))
|
|
69
85
|
|
|
70
|
-
def select(
|
|
71
|
-
|
|
72
|
-
|
|
86
|
+
def select(
|
|
87
|
+
self,
|
|
88
|
+
filter_func: Callable[[T], bool] | None = None,
|
|
89
|
+
at_most: int | float = float("inf"),
|
|
90
|
+
):
|
|
91
|
+
"""Select cells based on filter function.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
filter_func: filter function
|
|
95
|
+
at_most: The maximum amount of cells to select. Defaults to infinity.
|
|
96
|
+
- If an integer, at most the first number of matching cells is selected.
|
|
97
|
+
- If a float between 0 and 1, at most that fraction of original number of cells
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
CellCollection
|
|
101
|
+
|
|
102
|
+
"""
|
|
103
|
+
if filter_func is None and at_most == float("inf"):
|
|
73
104
|
return self
|
|
74
105
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
106
|
+
if at_most <= 1.0 and isinstance(at_most, float):
|
|
107
|
+
at_most = int(len(self) * at_most) # Note that it rounds down (floor)
|
|
108
|
+
|
|
109
|
+
def cell_generator(filter_func, at_most):
|
|
110
|
+
count = 0
|
|
111
|
+
for cell in self:
|
|
112
|
+
if count >= at_most:
|
|
113
|
+
break
|
|
114
|
+
if not filter_func or filter_func(cell):
|
|
115
|
+
yield cell
|
|
116
|
+
count += 1
|
|
117
|
+
|
|
118
|
+
return CellCollection(cell_generator(filter_func, at_most))
|