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