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.

Files changed (110) hide show
  1. mesa/__init__.py +3 -5
  2. mesa/agent.py +393 -116
  3. mesa/batchrunner.py +58 -31
  4. mesa/datacollection.py +141 -30
  5. mesa/examples/README.md +37 -0
  6. mesa/examples/__init__.py +21 -0
  7. mesa/examples/advanced/epstein_civil_violence/Epstein Civil Violence.ipynb +116 -0
  8. mesa/examples/advanced/epstein_civil_violence/Readme.md +34 -0
  9. mesa/examples/advanced/epstein_civil_violence/__init__.py +0 -0
  10. mesa/examples/advanced/epstein_civil_violence/agents.py +164 -0
  11. mesa/examples/advanced/epstein_civil_violence/app.py +73 -0
  12. mesa/examples/advanced/epstein_civil_violence/model.py +114 -0
  13. mesa/examples/advanced/pd_grid/Readme.md +43 -0
  14. mesa/examples/advanced/pd_grid/__init__.py +0 -0
  15. mesa/examples/advanced/pd_grid/agents.py +50 -0
  16. mesa/examples/advanced/pd_grid/analysis.ipynb +228 -0
  17. mesa/examples/advanced/pd_grid/app.py +54 -0
  18. mesa/examples/advanced/pd_grid/model.py +71 -0
  19. mesa/examples/advanced/sugarscape_g1mt/Readme.md +64 -0
  20. mesa/examples/advanced/sugarscape_g1mt/__init__.py +0 -0
  21. mesa/examples/advanced/sugarscape_g1mt/agents.py +344 -0
  22. mesa/examples/advanced/sugarscape_g1mt/app.py +62 -0
  23. mesa/examples/advanced/sugarscape_g1mt/model.py +180 -0
  24. mesa/examples/advanced/sugarscape_g1mt/sugar-map.txt +50 -0
  25. mesa/examples/advanced/sugarscape_g1mt/tests.py +69 -0
  26. mesa/examples/advanced/wolf_sheep/Readme.md +57 -0
  27. mesa/examples/advanced/wolf_sheep/__init__.py +0 -0
  28. mesa/examples/advanced/wolf_sheep/agents.py +102 -0
  29. mesa/examples/advanced/wolf_sheep/app.py +84 -0
  30. mesa/examples/advanced/wolf_sheep/model.py +137 -0
  31. mesa/examples/basic/__init__.py +0 -0
  32. mesa/examples/basic/boid_flockers/Readme.md +22 -0
  33. mesa/examples/basic/boid_flockers/__init__.py +0 -0
  34. mesa/examples/basic/boid_flockers/agents.py +71 -0
  35. mesa/examples/basic/boid_flockers/app.py +58 -0
  36. mesa/examples/basic/boid_flockers/model.py +69 -0
  37. mesa/examples/basic/boltzmann_wealth_model/Readme.md +56 -0
  38. mesa/examples/basic/boltzmann_wealth_model/__init__.py +0 -0
  39. mesa/examples/basic/boltzmann_wealth_model/agents.py +31 -0
  40. mesa/examples/basic/boltzmann_wealth_model/app.py +74 -0
  41. mesa/examples/basic/boltzmann_wealth_model/model.py +43 -0
  42. mesa/examples/basic/boltzmann_wealth_model/st_app.py +115 -0
  43. mesa/examples/basic/conways_game_of_life/Readme.md +39 -0
  44. mesa/examples/basic/conways_game_of_life/__init__.py +0 -0
  45. mesa/examples/basic/conways_game_of_life/agents.py +47 -0
  46. mesa/examples/basic/conways_game_of_life/app.py +51 -0
  47. mesa/examples/basic/conways_game_of_life/model.py +31 -0
  48. mesa/examples/basic/conways_game_of_life/st_app.py +72 -0
  49. mesa/examples/basic/schelling/Readme.md +40 -0
  50. mesa/examples/basic/schelling/__init__.py +0 -0
  51. mesa/examples/basic/schelling/agents.py +26 -0
  52. mesa/examples/basic/schelling/analysis.ipynb +205 -0
  53. mesa/examples/basic/schelling/app.py +42 -0
  54. mesa/examples/basic/schelling/model.py +59 -0
  55. mesa/examples/basic/virus_on_network/Readme.md +61 -0
  56. mesa/examples/basic/virus_on_network/__init__.py +0 -0
  57. mesa/examples/basic/virus_on_network/agents.py +69 -0
  58. mesa/examples/basic/virus_on_network/app.py +114 -0
  59. mesa/examples/basic/virus_on_network/model.py +96 -0
  60. mesa/experimental/UserParam.py +18 -7
  61. mesa/experimental/__init__.py +10 -2
  62. mesa/experimental/cell_space/__init__.py +16 -1
  63. mesa/experimental/cell_space/cell.py +93 -23
  64. mesa/experimental/cell_space/cell_agent.py +117 -21
  65. mesa/experimental/cell_space/cell_collection.py +56 -19
  66. mesa/experimental/cell_space/discrete_space.py +92 -8
  67. mesa/experimental/cell_space/grid.py +33 -9
  68. mesa/experimental/cell_space/network.py +15 -10
  69. mesa/experimental/cell_space/voronoi.py +257 -0
  70. mesa/experimental/components/altair.py +11 -2
  71. mesa/experimental/components/matplotlib.py +132 -26
  72. mesa/experimental/devs/__init__.py +2 -0
  73. mesa/experimental/devs/eventlist.py +54 -15
  74. mesa/experimental/devs/examples/epstein_civil_violence.py +71 -39
  75. mesa/experimental/devs/examples/wolf_sheep.py +45 -45
  76. mesa/experimental/devs/simulator.py +57 -16
  77. mesa/experimental/{jupyter_viz.py → solara_viz.py} +151 -98
  78. mesa/model.py +212 -84
  79. mesa/space.py +217 -151
  80. mesa/time.py +63 -80
  81. mesa/visualization/__init__.py +25 -6
  82. mesa/visualization/components/__init__.py +83 -0
  83. mesa/visualization/components/altair_components.py +188 -0
  84. mesa/visualization/components/matplotlib_components.py +175 -0
  85. mesa/visualization/mpl_space_drawing.py +593 -0
  86. mesa/visualization/solara_viz.py +458 -0
  87. mesa/visualization/user_param.py +69 -0
  88. mesa/visualization/utils.py +9 -0
  89. {mesa-2.3.4.dist-info → mesa-3.0.0.dist-info}/METADATA +65 -19
  90. mesa-3.0.0.dist-info/RECORD +95 -0
  91. mesa-3.0.0.dist-info/licenses/LICENSE +202 -0
  92. mesa-2.3.4.dist-info/licenses/LICENSE → mesa-3.0.0.dist-info/licenses/NOTICE +2 -2
  93. mesa/cookiecutter-mesa/cookiecutter.json +0 -8
  94. mesa/cookiecutter-mesa/hooks/post_gen_project.py +0 -11
  95. mesa/cookiecutter-mesa/{{cookiecutter.snake}}/README.md +0 -4
  96. mesa/cookiecutter-mesa/{{cookiecutter.snake}}/run.pytemplate +0 -3
  97. mesa/cookiecutter-mesa/{{cookiecutter.snake}}/setup.pytemplate +0 -11
  98. mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/model.pytemplate +0 -60
  99. mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/server.pytemplate +0 -36
  100. mesa/flat/__init__.py +0 -6
  101. mesa/flat/visualization.py +0 -5
  102. mesa/main.py +0 -63
  103. mesa/visualization/ModularVisualization.py +0 -1
  104. mesa/visualization/TextVisualization.py +0 -1
  105. mesa/visualization/UserParam.py +0 -1
  106. mesa/visualization/modules.py +0 -1
  107. mesa-2.3.4.dist-info/RECORD +0 -45
  108. /mesa/{cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}} → examples/advanced}/__init__.py +0 -0
  109. {mesa-2.3.4.dist-info → mesa-3.0.0.dist-info}/WHEEL +0 -0
  110. {mesa-2.3.4.dist-info → mesa-3.0.0.dist-info}/entry_points.txt +0 -0
@@ -1,7 +1,10 @@
1
- class UserParam:
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 == float
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)
@@ -1,5 +1,13 @@
1
- from .jupyter_viz import JupyterViz, make_text, Slider # noqa
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__ = ["JupyterViz", "make_text", "Slider", "cell_space"]
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 CellAgent
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 functools import cache
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.experimental.cell_space.cell_agent import CellAgent
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
- "_connections",
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: tuple[int, ...],
46
- capacity: float | None = None,
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._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] = {}
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
- self._connections.append(other)
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._connections.remove(other)
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 neighborhood(self, radius=1, include_center=False):
125
- return CellCollection(
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(self, radius=1, include_center=False):
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 = {neighbor: neighbor.agents for neighbor in self._connections}
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._connections:
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, Model
7
+ from mesa.agent import Agent
6
8
 
7
9
  if TYPE_CHECKING:
8
- from mesa.experimental.cell_space.cell import Cell
10
+ from mesa.experimental.cell_space import Cell
9
11
 
10
12
 
11
- class CellAgent(Agent):
12
- """Cell Agent is an extension of the Agent class and adds behavior for moving in discrete spaces
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
- def __init__(self, unique_id: int, model: Model) -> None:
23
- """
24
- Create a new agent.
19
+ class HasCell:
20
+ """Descriptor for cell movement behavior."""
25
21
 
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
22
+ _mesa_cell: Cell | None = None
32
23
 
33
- def move_to(self, cell) -> None:
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, Callable, Generic, TypeVar
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(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:
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
- 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
- )
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))