Mesa 3.0.0a4__py3-none-any.whl → 3.0.0b0__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 -3
- mesa/agent.py +116 -85
- mesa/batchrunner.py +22 -23
- mesa/cookiecutter-mesa/hooks/post_gen_project.py +2 -0
- mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/__init__.py +1 -0
- mesa/datacollection.py +138 -27
- mesa/experimental/UserParam.py +17 -6
- mesa/experimental/__init__.py +2 -0
- mesa/experimental/cell_space/__init__.py +14 -1
- mesa/experimental/cell_space/cell.py +84 -23
- mesa/experimental/cell_space/cell_agent.py +117 -21
- mesa/experimental/cell_space/cell_collection.py +54 -17
- mesa/experimental/cell_space/discrete_space.py +79 -7
- mesa/experimental/cell_space/grid.py +19 -8
- mesa/experimental/cell_space/network.py +9 -7
- mesa/experimental/cell_space/voronoi.py +26 -33
- mesa/experimental/components/altair.py +10 -0
- mesa/experimental/components/matplotlib.py +18 -0
- mesa/experimental/devs/__init__.py +2 -0
- mesa/experimental/devs/eventlist.py +36 -15
- mesa/experimental/devs/examples/epstein_civil_violence.py +65 -29
- mesa/experimental/devs/examples/wolf_sheep.py +40 -35
- mesa/experimental/devs/simulator.py +55 -15
- mesa/experimental/solara_viz.py +10 -19
- mesa/main.py +6 -4
- mesa/model.py +51 -54
- mesa/space.py +145 -120
- mesa/time.py +57 -67
- mesa/visualization/UserParam.py +19 -6
- mesa/visualization/__init__.py +3 -2
- mesa/visualization/components/altair.py +4 -2
- mesa/visualization/components/matplotlib.py +176 -85
- mesa/visualization/solara_viz.py +167 -84
- mesa/visualization/utils.py +3 -1
- {mesa-3.0.0a4.dist-info → mesa-3.0.0b0.dist-info}/METADATA +55 -13
- mesa-3.0.0b0.dist-info/RECORD +45 -0
- mesa-3.0.0b0.dist-info/licenses/LICENSE +202 -0
- mesa-3.0.0a4.dist-info/licenses/LICENSE → mesa-3.0.0b0.dist-info/licenses/NOTICE +2 -2
- mesa-3.0.0a4.dist-info/RECORD +0 -44
- {mesa-3.0.0a4.dist-info → mesa-3.0.0b0.dist-info}/WHEEL +0 -0
- {mesa-3.0.0a4.dist-info → mesa-3.0.0b0.dist-info}/entry_points.txt +0 -0
|
@@ -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__(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,3 +1,5 @@
|
|
|
1
|
+
"""CellCollection class."""
|
|
2
|
+
|
|
1
3
|
from __future__ import annotations
|
|
2
4
|
|
|
3
5
|
import itertools
|
|
@@ -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))
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
"""DiscreteSpace base class."""
|
|
2
|
+
|
|
1
3
|
from __future__ import annotations
|
|
2
4
|
|
|
5
|
+
from collections.abc import Callable
|
|
3
6
|
from functools import cached_property
|
|
4
7
|
from random import Random
|
|
5
|
-
from typing import Generic, TypeVar
|
|
8
|
+
from typing import Any, Generic, TypeVar
|
|
6
9
|
|
|
7
10
|
from mesa.experimental.cell_space.cell import Cell
|
|
8
11
|
from mesa.experimental.cell_space.cell_collection import CellCollection
|
|
12
|
+
from mesa.space import PropertyLayer
|
|
9
13
|
|
|
10
14
|
T = TypeVar("T", bound=Cell)
|
|
11
15
|
|
|
@@ -19,7 +23,7 @@ class DiscreteSpace(Generic[T]):
|
|
|
19
23
|
random (Random): The random number generator
|
|
20
24
|
cell_klass (Type) : the type of cell class
|
|
21
25
|
empties (CellCollection) : collecction of all cells that are empty
|
|
22
|
-
|
|
26
|
+
property_layers (dict[str, PropertyLayer]): the property layers of the discrete space
|
|
23
27
|
"""
|
|
24
28
|
|
|
25
29
|
def __init__(
|
|
@@ -28,6 +32,13 @@ class DiscreteSpace(Generic[T]):
|
|
|
28
32
|
cell_klass: type[T] = Cell,
|
|
29
33
|
random: Random | None = None,
|
|
30
34
|
):
|
|
35
|
+
"""Instantiate a DiscreteSpace.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
capacity: capacity of cells
|
|
39
|
+
cell_klass: base class for all cells
|
|
40
|
+
random: random number generator
|
|
41
|
+
"""
|
|
31
42
|
super().__init__()
|
|
32
43
|
self.capacity = capacity
|
|
33
44
|
self._cells: dict[tuple[int, ...], T] = {}
|
|
@@ -38,27 +49,88 @@ class DiscreteSpace(Generic[T]):
|
|
|
38
49
|
|
|
39
50
|
self._empties: dict[tuple[int, ...], None] = {}
|
|
40
51
|
self._empties_initialized = False
|
|
52
|
+
self.property_layers: dict[str, PropertyLayer] = {}
|
|
41
53
|
|
|
42
54
|
@property
|
|
43
|
-
def cutoff_empties(self):
|
|
55
|
+
def cutoff_empties(self): # noqa
|
|
44
56
|
return 7.953 * len(self._cells) ** 0.384
|
|
45
57
|
|
|
46
58
|
def _connect_single_cell(self, cell: T): ...
|
|
47
59
|
|
|
48
60
|
@cached_property
|
|
49
61
|
def all_cells(self):
|
|
62
|
+
"""Return all cells in space."""
|
|
50
63
|
return CellCollection({cell: cell.agents for cell in self._cells.values()})
|
|
51
64
|
|
|
52
|
-
def __iter__(self):
|
|
65
|
+
def __iter__(self): # noqa
|
|
53
66
|
return iter(self._cells.values())
|
|
54
67
|
|
|
55
|
-
def __getitem__(self, key):
|
|
68
|
+
def __getitem__(self, key: tuple[int, ...]) -> T: # noqa: D105
|
|
56
69
|
return self._cells[key]
|
|
57
70
|
|
|
58
71
|
@property
|
|
59
|
-
def empties(self) -> CellCollection:
|
|
72
|
+
def empties(self) -> CellCollection[T]:
|
|
73
|
+
"""Return all empty in spaces."""
|
|
60
74
|
return self.all_cells.select(lambda cell: cell.is_empty)
|
|
61
75
|
|
|
62
76
|
def select_random_empty_cell(self) -> T:
|
|
63
|
-
"""
|
|
77
|
+
"""Select random empty cell."""
|
|
64
78
|
return self.random.choice(list(self.empties))
|
|
79
|
+
|
|
80
|
+
# PropertyLayer methods
|
|
81
|
+
def add_property_layer(
|
|
82
|
+
self, property_layer: PropertyLayer, add_to_cells: bool = True
|
|
83
|
+
):
|
|
84
|
+
"""Add a property layer to the grid.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
property_layer: the property layer to add
|
|
88
|
+
add_to_cells: whether to add the property layer to all cells (default: True)
|
|
89
|
+
"""
|
|
90
|
+
if property_layer.name in self.property_layers:
|
|
91
|
+
raise ValueError(f"Property layer {property_layer.name} already exists.")
|
|
92
|
+
self.property_layers[property_layer.name] = property_layer
|
|
93
|
+
if add_to_cells:
|
|
94
|
+
for cell in self._cells.values():
|
|
95
|
+
cell._mesa_property_layers[property_layer.name] = property_layer
|
|
96
|
+
|
|
97
|
+
def remove_property_layer(self, property_name: str, remove_from_cells: bool = True):
|
|
98
|
+
"""Remove a property layer from the grid.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
property_name: the name of the property layer to remove
|
|
102
|
+
remove_from_cells: whether to remove the property layer from all cells (default: True)
|
|
103
|
+
"""
|
|
104
|
+
del self.property_layers[property_name]
|
|
105
|
+
if remove_from_cells:
|
|
106
|
+
for cell in self._cells.values():
|
|
107
|
+
del cell._mesa_property_layers[property_name]
|
|
108
|
+
|
|
109
|
+
def set_property(
|
|
110
|
+
self, property_name: str, value, condition: Callable[[T], bool] | None = None
|
|
111
|
+
):
|
|
112
|
+
"""Set the value of a property for all cells in the grid.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
property_name: the name of the property to set
|
|
116
|
+
value: the value to set
|
|
117
|
+
condition: a function that takes a cell and returns a boolean
|
|
118
|
+
"""
|
|
119
|
+
self.property_layers[property_name].set_cells(value, condition)
|
|
120
|
+
|
|
121
|
+
def modify_properties(
|
|
122
|
+
self,
|
|
123
|
+
property_name: str,
|
|
124
|
+
operation: Callable,
|
|
125
|
+
value: Any = None,
|
|
126
|
+
condition: Callable[[T], bool] | None = None,
|
|
127
|
+
):
|
|
128
|
+
"""Modify the values of a specific property for all cells in the grid.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
property_name: the name of the property to modify
|
|
132
|
+
operation: the operation to perform
|
|
133
|
+
value: the value to use in the operation
|
|
134
|
+
condition: a function that takes a cell and returns a boolean (used to filter cells)
|
|
135
|
+
"""
|
|
136
|
+
self.property_layers[property_name].modify_cells(operation, value, condition)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Various Grid Spaces."""
|
|
2
|
+
|
|
1
3
|
from __future__ import annotations
|
|
2
4
|
|
|
3
5
|
from collections.abc import Sequence
|
|
@@ -10,8 +12,8 @@ from mesa.experimental.cell_space import Cell, DiscreteSpace
|
|
|
10
12
|
T = TypeVar("T", bound=Cell)
|
|
11
13
|
|
|
12
14
|
|
|
13
|
-
class Grid(DiscreteSpace, Generic[T]):
|
|
14
|
-
"""Base class for all grid classes
|
|
15
|
+
class Grid(DiscreteSpace[T], Generic[T]):
|
|
16
|
+
"""Base class for all grid classes.
|
|
15
17
|
|
|
16
18
|
Attributes:
|
|
17
19
|
dimensions (Sequence[int]): the dimensions of the grid
|
|
@@ -30,6 +32,15 @@ class Grid(DiscreteSpace, Generic[T]):
|
|
|
30
32
|
random: Random | None = None,
|
|
31
33
|
cell_klass: type[T] = Cell,
|
|
32
34
|
) -> None:
|
|
35
|
+
"""Initialise the grid class.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
dimensions: the dimensions of the space
|
|
39
|
+
torus: whether the space wraps
|
|
40
|
+
capacity: capacity of the grid cell
|
|
41
|
+
random: a random number generator
|
|
42
|
+
cell_klass: the base class to use for the cells
|
|
43
|
+
"""
|
|
33
44
|
super().__init__(capacity=capacity, random=random, cell_klass=cell_klass)
|
|
34
45
|
self.torus = torus
|
|
35
46
|
self.dimensions = dimensions
|
|
@@ -63,7 +74,7 @@ class Grid(DiscreteSpace, Generic[T]):
|
|
|
63
74
|
if self.capacity is not None and not isinstance(self.capacity, float | int):
|
|
64
75
|
raise ValueError("Capacity must be a number or None.")
|
|
65
76
|
|
|
66
|
-
def select_random_empty_cell(self) -> T:
|
|
77
|
+
def select_random_empty_cell(self) -> T: # noqa
|
|
67
78
|
# FIXME:: currently just a simple boolean to control behavior
|
|
68
79
|
# FIXME:: basically if grid is close to 99% full, creating empty list can be faster
|
|
69
80
|
# FIXME:: note however that the old results don't apply because in this implementation
|
|
@@ -89,7 +100,7 @@ class Grid(DiscreteSpace, Generic[T]):
|
|
|
89
100
|
if self.torus:
|
|
90
101
|
n_coord = tuple(nc % d for nc, d in zip(n_coord, self.dimensions))
|
|
91
102
|
if all(0 <= nc < d for nc, d in zip(n_coord, self.dimensions)):
|
|
92
|
-
cell.connect(self._cells[n_coord])
|
|
103
|
+
cell.connect(self._cells[n_coord], d_coord)
|
|
93
104
|
|
|
94
105
|
def _connect_single_cell_2d(self, cell: T, offsets: list[tuple[int, int]]) -> None:
|
|
95
106
|
i, j = cell.coordinate
|
|
@@ -100,7 +111,7 @@ class Grid(DiscreteSpace, Generic[T]):
|
|
|
100
111
|
if self.torus:
|
|
101
112
|
ni, nj = ni % height, nj % width
|
|
102
113
|
if 0 <= ni < height and 0 <= nj < width:
|
|
103
|
-
cell.connect(self._cells[ni, nj])
|
|
114
|
+
cell.connect(self._cells[ni, nj], (di, dj))
|
|
104
115
|
|
|
105
116
|
|
|
106
117
|
class OrthogonalMooreGrid(Grid[T]):
|
|
@@ -122,7 +133,6 @@ class OrthogonalMooreGrid(Grid[T]):
|
|
|
122
133
|
( 1, -1), ( 1, 0), ( 1, 1),
|
|
123
134
|
]
|
|
124
135
|
# fmt: on
|
|
125
|
-
height, width = self.dimensions
|
|
126
136
|
|
|
127
137
|
for cell in self.all_cells:
|
|
128
138
|
self._connect_single_cell_2d(cell, offsets)
|
|
@@ -154,13 +164,12 @@ class OrthogonalVonNeumannGrid(Grid[T]):
|
|
|
154
164
|
( 1, 0),
|
|
155
165
|
]
|
|
156
166
|
# fmt: on
|
|
157
|
-
height, width = self.dimensions
|
|
158
167
|
|
|
159
168
|
for cell in self.all_cells:
|
|
160
169
|
self._connect_single_cell_2d(cell, offsets)
|
|
161
170
|
|
|
162
171
|
def _connect_cells_nd(self) -> None:
|
|
163
|
-
offsets = []
|
|
172
|
+
offsets: list[tuple[int, ...]] = []
|
|
164
173
|
dimensions = len(self.dimensions)
|
|
165
174
|
for dim in range(dimensions):
|
|
166
175
|
for delta in [
|
|
@@ -176,6 +185,8 @@ class OrthogonalVonNeumannGrid(Grid[T]):
|
|
|
176
185
|
|
|
177
186
|
|
|
178
187
|
class HexGrid(Grid[T]):
|
|
188
|
+
"""A Grid with hexagonal tilling of the space."""
|
|
189
|
+
|
|
179
190
|
def _connect_cells_2d(self) -> None:
|
|
180
191
|
# fmt: off
|
|
181
192
|
even_offsets = [
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""A Network grid."""
|
|
2
|
+
|
|
1
3
|
from random import Random
|
|
2
4
|
from typing import Any
|
|
3
5
|
|
|
@@ -5,8 +7,8 @@ from mesa.experimental.cell_space.cell import Cell
|
|
|
5
7
|
from mesa.experimental.cell_space.discrete_space import DiscreteSpace
|
|
6
8
|
|
|
7
9
|
|
|
8
|
-
class Network(DiscreteSpace):
|
|
9
|
-
"""A networked discrete space"""
|
|
10
|
+
class Network(DiscreteSpace[Cell]):
|
|
11
|
+
"""A networked discrete space."""
|
|
10
12
|
|
|
11
13
|
def __init__(
|
|
12
14
|
self,
|
|
@@ -15,13 +17,13 @@ class Network(DiscreteSpace):
|
|
|
15
17
|
random: Random | None = None,
|
|
16
18
|
cell_klass: type[Cell] = Cell,
|
|
17
19
|
) -> None:
|
|
18
|
-
"""A Networked grid
|
|
20
|
+
"""A Networked grid.
|
|
19
21
|
|
|
20
22
|
Args:
|
|
21
23
|
G: a NetworkX Graph instance.
|
|
22
24
|
capacity (int) : the capacity of the cell
|
|
23
|
-
random (Random):
|
|
24
|
-
|
|
25
|
+
random (Random): a random number generator
|
|
26
|
+
cell_klass (type[Cell]): The base Cell class to use in the Network
|
|
25
27
|
|
|
26
28
|
"""
|
|
27
29
|
super().__init__(capacity=capacity, random=random, cell_klass=cell_klass)
|
|
@@ -35,6 +37,6 @@ class Network(DiscreteSpace):
|
|
|
35
37
|
for cell in self.all_cells:
|
|
36
38
|
self._connect_single_cell(cell)
|
|
37
39
|
|
|
38
|
-
def _connect_single_cell(self, cell):
|
|
40
|
+
def _connect_single_cell(self, cell: Cell):
|
|
39
41
|
for node_id in self.G.neighbors(cell.coordinate):
|
|
40
|
-
cell.connect(self._cells[node_id])
|
|
42
|
+
cell.connect(self._cells[node_id], node_id)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Support for Voronoi meshed grids."""
|
|
2
|
+
|
|
1
3
|
from collections.abc import Sequence
|
|
2
4
|
from itertools import combinations
|
|
3
5
|
from random import Random
|
|
@@ -9,16 +11,17 @@ from mesa.experimental.cell_space.discrete_space import DiscreteSpace
|
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
class Delaunay:
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
+
"""Class to compute a Delaunay triangulation in 2D.
|
|
15
|
+
|
|
14
16
|
ref: http://github.com/jmespadero/pyDelaunay2D
|
|
15
17
|
"""
|
|
16
18
|
|
|
17
19
|
def __init__(self, center: tuple = (0, 0), radius: int = 9999) -> None:
|
|
18
|
-
"""
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
"""Init and create a new frame to contain the triangulation.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
center: Optional position for the center of the frame. Default (0,0)
|
|
24
|
+
radius: Optional distance from corners to the center.
|
|
22
25
|
"""
|
|
23
26
|
center = np.asarray(center)
|
|
24
27
|
# Create coordinates for the corners of the frame
|
|
@@ -44,9 +47,7 @@ class Delaunay:
|
|
|
44
47
|
self.circles[t] = self._circumcenter(t)
|
|
45
48
|
|
|
46
49
|
def _circumcenter(self, triangle: list) -> tuple:
|
|
47
|
-
"""
|
|
48
|
-
Compute circumcenter and circumradius of a triangle in 2D.
|
|
49
|
-
"""
|
|
50
|
+
"""Compute circumcenter and circumradius of a triangle in 2D."""
|
|
50
51
|
points = np.asarray([self.coords[v] for v in triangle])
|
|
51
52
|
points2 = np.dot(points, points.T)
|
|
52
53
|
a = np.bmat([[2 * points2, [[1], [1], [1]]], [[[1, 1, 1, 0]]]])
|
|
@@ -60,16 +61,12 @@ class Delaunay:
|
|
|
60
61
|
return (center, radius)
|
|
61
62
|
|
|
62
63
|
def _in_circle(self, triangle: list, point: list) -> bool:
|
|
63
|
-
"""
|
|
64
|
-
Check if point p is inside of precomputed circumcircle of triangle.
|
|
65
|
-
"""
|
|
64
|
+
"""Check if point p is inside of precomputed circumcircle of triangle."""
|
|
66
65
|
center, radius = self.circles[triangle]
|
|
67
66
|
return np.sum(np.square(center - point)) <= radius
|
|
68
67
|
|
|
69
68
|
def add_point(self, point: Sequence) -> None:
|
|
70
|
-
"""
|
|
71
|
-
Add a point to the current DT, and refine it using Bowyer-Watson.
|
|
72
|
-
"""
|
|
69
|
+
"""Add a point to the current DT, and refine it using Bowyer-Watson."""
|
|
73
70
|
point_index = len(self.coords)
|
|
74
71
|
self.coords.append(np.asarray(point))
|
|
75
72
|
|
|
@@ -121,9 +118,7 @@ class Delaunay:
|
|
|
121
118
|
self.triangles[triangle][2] = new_triangles[(i - 1) % n] # previous
|
|
122
119
|
|
|
123
120
|
def export_triangles(self) -> list:
|
|
124
|
-
"""
|
|
125
|
-
Export the current list of Delaunay triangles
|
|
126
|
-
"""
|
|
121
|
+
"""Export the current list of Delaunay triangles."""
|
|
127
122
|
triangles_list = [
|
|
128
123
|
(a - 4, b - 4, c - 4)
|
|
129
124
|
for (a, b, c) in self.triangles
|
|
@@ -132,9 +127,7 @@ class Delaunay:
|
|
|
132
127
|
return triangles_list
|
|
133
128
|
|
|
134
129
|
def export_voronoi_regions(self):
|
|
135
|
-
"""
|
|
136
|
-
Export coordinates and regions of Voronoi diagram as indexed data.
|
|
137
|
-
"""
|
|
130
|
+
"""Export coordinates and regions of Voronoi diagram as indexed data."""
|
|
138
131
|
use_vertex = {i: [] for i in range(len(self.coords))}
|
|
139
132
|
vor_coors = []
|
|
140
133
|
index = {}
|
|
@@ -163,11 +156,13 @@ class Delaunay:
|
|
|
163
156
|
return vor_coors, regions
|
|
164
157
|
|
|
165
158
|
|
|
166
|
-
def round_float(x: float) -> int:
|
|
159
|
+
def round_float(x: float) -> int: # noqa
|
|
167
160
|
return int(x * 500)
|
|
168
161
|
|
|
169
162
|
|
|
170
163
|
class VoronoiGrid(DiscreteSpace):
|
|
164
|
+
"""Voronoi meshed GridSpace."""
|
|
165
|
+
|
|
171
166
|
triangulation: Delaunay
|
|
172
167
|
voronoi_coordinates: list
|
|
173
168
|
regions: list
|
|
@@ -181,8 +176,7 @@ class VoronoiGrid(DiscreteSpace):
|
|
|
181
176
|
capacity_function: callable = round_float,
|
|
182
177
|
cell_coloring_property: str | None = None,
|
|
183
178
|
) -> None:
|
|
184
|
-
"""
|
|
185
|
-
A Voronoi Tessellation Grid.
|
|
179
|
+
"""A Voronoi Tessellation Grid.
|
|
186
180
|
|
|
187
181
|
Given a set of points, this class creates a grid where a cell is centered in each point,
|
|
188
182
|
its neighbors are given by Voronoi Tessellation cells neighbors
|
|
@@ -192,7 +186,7 @@ class VoronoiGrid(DiscreteSpace):
|
|
|
192
186
|
centroids_coordinates: coordinates of centroids to build the tessellation space
|
|
193
187
|
capacity (int) : capacity of the cells in the discrete space
|
|
194
188
|
random (Random): random number generator
|
|
195
|
-
|
|
189
|
+
cell_klass (type[Cell]): type of cell class
|
|
196
190
|
capacity_function (Callable): function to compute (int) capacity according to (float) area
|
|
197
191
|
cell_coloring_property (str): voronoi visualization polygon fill property
|
|
198
192
|
"""
|
|
@@ -215,17 +209,15 @@ class VoronoiGrid(DiscreteSpace):
|
|
|
215
209
|
self._build_cell_polygons()
|
|
216
210
|
|
|
217
211
|
def _connect_cells(self) -> None:
|
|
218
|
-
"""
|
|
219
|
-
Connect cells to neighbors based on given centroids and using Delaunay Triangulation
|
|
220
|
-
"""
|
|
212
|
+
"""Connect cells to neighbors based on given centroids and using Delaunay Triangulation."""
|
|
221
213
|
self.triangulation = Delaunay()
|
|
222
214
|
for centroid in self.centroids_coordinates:
|
|
223
215
|
self.triangulation.add_point(centroid)
|
|
224
216
|
|
|
225
217
|
for point in self.triangulation.export_triangles():
|
|
226
218
|
for i, j in combinations(point, 2):
|
|
227
|
-
self._cells[i].connect(self._cells[j])
|
|
228
|
-
self._cells[j].connect(self._cells[i])
|
|
219
|
+
self._cells[i].connect(self._cells[j], (i, j))
|
|
220
|
+
self._cells[j].connect(self._cells[i], (j, i))
|
|
229
221
|
|
|
230
222
|
def _validate_parameters(self) -> None:
|
|
231
223
|
if self.capacity is not None and not isinstance(self.capacity, float | int):
|
|
@@ -241,9 +233,10 @@ class VoronoiGrid(DiscreteSpace):
|
|
|
241
233
|
|
|
242
234
|
def _get_voronoi_regions(self) -> tuple:
|
|
243
235
|
if self.voronoi_coordinates is None or self.regions is None:
|
|
244
|
-
|
|
245
|
-
self.
|
|
246
|
-
|
|
236
|
+
(
|
|
237
|
+
self.voronoi_coordinates,
|
|
238
|
+
self.regions,
|
|
239
|
+
) = self.triangulation.export_voronoi_regions()
|
|
247
240
|
return self.voronoi_coordinates, self.regions
|
|
248
241
|
|
|
249
242
|
@staticmethod
|