Mesa 2.3.0.dev0__py3-none-any.whl → 2.3.0rc1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of Mesa might be problematic. Click here for more details.
- mesa/__init__.py +2 -1
- mesa/agent.py +2 -16
- mesa/datacollection.py +1 -0
- mesa/experimental/__init__.py +4 -0
- mesa/experimental/cell_space/__init__.py +23 -0
- mesa/experimental/cell_space/cell.py +152 -0
- mesa/experimental/cell_space/cell_agent.py +37 -0
- mesa/experimental/cell_space/cell_collection.py +81 -0
- mesa/experimental/cell_space/discrete_space.py +64 -0
- mesa/experimental/cell_space/grid.py +204 -0
- mesa/experimental/cell_space/network.py +40 -0
- mesa/experimental/components/altair.py +13 -1
- mesa/experimental/components/matplotlib.py +3 -0
- mesa/experimental/devs/__init__.py +4 -0
- mesa/experimental/devs/eventlist.py +166 -0
- mesa/experimental/devs/examples/epstein_civil_violence.py +273 -0
- mesa/experimental/devs/examples/wolf_sheep.py +250 -0
- mesa/experimental/devs/simulator.py +293 -0
- mesa/experimental/jupyter_viz.py +51 -31
- mesa/model.py +2 -4
- mesa/space.py +26 -13
- mesa/time.py +3 -110
- {mesa-2.3.0.dev0.dist-info → mesa-2.3.0rc1.dist-info}/METADATA +4 -3
- mesa-2.3.0rc1.dist-info/RECORD +45 -0
- {mesa-2.3.0.dev0.dist-info → mesa-2.3.0rc1.dist-info}/WHEEL +1 -1
- mesa-2.3.0.dev0.dist-info/RECORD +0 -33
- {mesa-2.3.0.dev0.dist-info → mesa-2.3.0rc1.dist-info}/entry_points.txt +0 -0
- {mesa-2.3.0.dev0.dist-info → mesa-2.3.0rc1.dist-info}/licenses/LICENSE +0 -0
mesa/__init__.py
CHANGED
|
@@ -3,6 +3,7 @@ Mesa Agent-Based Modeling Framework
|
|
|
3
3
|
|
|
4
4
|
Core Objects: Model, and Agent.
|
|
5
5
|
"""
|
|
6
|
+
|
|
6
7
|
import datetime
|
|
7
8
|
|
|
8
9
|
import mesa.space as space
|
|
@@ -25,7 +26,7 @@ __all__ = [
|
|
|
25
26
|
]
|
|
26
27
|
|
|
27
28
|
__title__ = "mesa"
|
|
28
|
-
__version__ = "2.3.0-
|
|
29
|
+
__version__ = "2.3.0-rc1"
|
|
29
30
|
__license__ = "Apache 2.0"
|
|
30
31
|
_this_year = datetime.datetime.now(tz=datetime.timezone.utc).date().year
|
|
31
32
|
__copyright__ = f"Copyright {_this_year} Project Mesa Team"
|
mesa/agent.py
CHANGED
|
@@ -3,6 +3,7 @@ The agent class for Mesa framework.
|
|
|
3
3
|
|
|
4
4
|
Core Objects: Agent
|
|
5
5
|
"""
|
|
6
|
+
|
|
6
7
|
# Mypy; for the `|` operator purpose
|
|
7
8
|
# Remove this __future__ import once the oldest supported Python is 3.10
|
|
8
9
|
from __future__ import annotations
|
|
@@ -81,12 +82,6 @@ class Agent:
|
|
|
81
82
|
|
|
82
83
|
class AgentSet(MutableSet, Sequence):
|
|
83
84
|
"""
|
|
84
|
-
.. warning::
|
|
85
|
-
The AgentSet is experimental. It may be changed or removed in any and all future releases, including
|
|
86
|
-
patch releases.
|
|
87
|
-
We would love to hear what you think about this new feature. If you have any thoughts, share them with
|
|
88
|
-
us here: https://github.com/projectmesa/mesa/discussions/1919
|
|
89
|
-
|
|
90
85
|
A collection class that represents an ordered set of agents within an agent-based model (ABM). This class
|
|
91
86
|
extends both MutableSet and Sequence, providing set-like functionality with order preservation and
|
|
92
87
|
sequence operations.
|
|
@@ -115,17 +110,8 @@ class AgentSet(MutableSet, Sequence):
|
|
|
115
110
|
agents (Iterable[Agent]): An iterable of Agent objects to be included in the set.
|
|
116
111
|
model (Model): The ABM model instance to which this AgentSet belongs.
|
|
117
112
|
"""
|
|
118
|
-
self.model = model
|
|
119
|
-
|
|
120
|
-
if not self.__class__.agentset_experimental_warning_given:
|
|
121
|
-
self.__class__.agentset_experimental_warning_given = True
|
|
122
|
-
warnings.warn(
|
|
123
|
-
"The AgentSet is experimental. It may be changed or removed in any and all future releases, including patch releases.\n"
|
|
124
|
-
"We would love to hear what you think about this new feature. If you have any thoughts, share them with us here: https://github.com/projectmesa/mesa/discussions/1919",
|
|
125
|
-
FutureWarning,
|
|
126
|
-
stacklevel=2,
|
|
127
|
-
)
|
|
128
113
|
|
|
114
|
+
self.model = model
|
|
129
115
|
self._agents = weakref.WeakKeyDictionary({agent: None for agent in agents})
|
|
130
116
|
|
|
131
117
|
def __len__(self) -> int:
|
mesa/datacollection.py
CHANGED
mesa/experimental/__init__.py
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from mesa.experimental.cell_space.cell import Cell
|
|
2
|
+
from mesa.experimental.cell_space.cell_agent import CellAgent
|
|
3
|
+
from mesa.experimental.cell_space.cell_collection import CellCollection
|
|
4
|
+
from mesa.experimental.cell_space.discrete_space import DiscreteSpace
|
|
5
|
+
from mesa.experimental.cell_space.grid import (
|
|
6
|
+
Grid,
|
|
7
|
+
HexGrid,
|
|
8
|
+
OrthogonalMooreGrid,
|
|
9
|
+
OrthogonalVonNeumannGrid,
|
|
10
|
+
)
|
|
11
|
+
from mesa.experimental.cell_space.network import Network
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"CellCollection",
|
|
15
|
+
"Cell",
|
|
16
|
+
"CellAgent",
|
|
17
|
+
"DiscreteSpace",
|
|
18
|
+
"Grid",
|
|
19
|
+
"HexGrid",
|
|
20
|
+
"OrthogonalMooreGrid",
|
|
21
|
+
"OrthogonalVonNeumannGrid",
|
|
22
|
+
"Network",
|
|
23
|
+
]
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from functools import cache
|
|
4
|
+
from random import Random
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from mesa.experimental.cell_space.cell_collection import CellCollection
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from mesa.experimental.cell_space.cell_agent import CellAgent
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Cell:
|
|
14
|
+
"""The cell represents a position in a discrete space.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
coordinate (Tuple[int, int]) : the position of the cell in the discrete space
|
|
18
|
+
agents (List[Agent]): the agents occupying the cell
|
|
19
|
+
capacity (int): the maximum number of agents that can simultaneously occupy the cell
|
|
20
|
+
properties (dict[str, Any]): the properties of the cell
|
|
21
|
+
random (Random): the random number generator
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
__slots__ = [
|
|
26
|
+
"coordinate",
|
|
27
|
+
"_connections",
|
|
28
|
+
"agents",
|
|
29
|
+
"capacity",
|
|
30
|
+
"properties",
|
|
31
|
+
"random",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
# def __new__(cls,
|
|
35
|
+
# coordinate: tuple[int, ...],
|
|
36
|
+
# capacity: float | None = None,
|
|
37
|
+
# random: Random | None = None,):
|
|
38
|
+
# if capacity != 1:
|
|
39
|
+
# return object.__new__(cls)
|
|
40
|
+
# else:
|
|
41
|
+
# return object.__new__(SingleAgentCell)
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
coordinate: tuple[int, ...],
|
|
46
|
+
capacity: float | None = None,
|
|
47
|
+
random: Random | None = None,
|
|
48
|
+
) -> None:
|
|
49
|
+
""" "
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
coordinate:
|
|
53
|
+
capacity (int) : the capacity of the cell. If None, the capacity is infinite
|
|
54
|
+
random (Random) : the random number generator to use
|
|
55
|
+
|
|
56
|
+
"""
|
|
57
|
+
super().__init__()
|
|
58
|
+
self.coordinate = coordinate
|
|
59
|
+
self._connections: list[Cell] = [] # TODO: change to CellCollection?
|
|
60
|
+
self.agents = [] # TODO:: change to AgentSet or weakrefs? (neither is very performant, )
|
|
61
|
+
self.capacity = capacity
|
|
62
|
+
self.properties: dict[str, object] = {}
|
|
63
|
+
self.random = random
|
|
64
|
+
|
|
65
|
+
def connect(self, other: Cell) -> None:
|
|
66
|
+
"""Connects this cell to another cell.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
other (Cell): other cell to connect to
|
|
70
|
+
|
|
71
|
+
"""
|
|
72
|
+
self._connections.append(other)
|
|
73
|
+
|
|
74
|
+
def disconnect(self, other: Cell) -> None:
|
|
75
|
+
"""Disconnects this cell from another cell.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
other (Cell): other cell to remove from connections
|
|
79
|
+
|
|
80
|
+
"""
|
|
81
|
+
self._connections.remove(other)
|
|
82
|
+
|
|
83
|
+
def add_agent(self, agent: CellAgent) -> None:
|
|
84
|
+
"""Adds an agent to the cell.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
agent (CellAgent): agent to add to this Cell
|
|
88
|
+
|
|
89
|
+
"""
|
|
90
|
+
n = len(self.agents)
|
|
91
|
+
|
|
92
|
+
if self.capacity and n >= self.capacity:
|
|
93
|
+
raise Exception(
|
|
94
|
+
"ERROR: Cell is full"
|
|
95
|
+
) # FIXME we need MESA errors or a proper error
|
|
96
|
+
|
|
97
|
+
self.agents.append(agent)
|
|
98
|
+
|
|
99
|
+
def remove_agent(self, agent: CellAgent) -> None:
|
|
100
|
+
"""Removes an agent from the cell.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
agent (CellAgent): agent to remove from this cell
|
|
104
|
+
|
|
105
|
+
"""
|
|
106
|
+
self.agents.remove(agent)
|
|
107
|
+
agent.cell = None
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def is_empty(self) -> bool:
|
|
111
|
+
"""Returns a bool of the contents of a cell."""
|
|
112
|
+
return len(self.agents) == 0
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def is_full(self) -> bool:
|
|
116
|
+
"""Returns a bool of the contents of a cell."""
|
|
117
|
+
return len(self.agents) == self.capacity
|
|
118
|
+
|
|
119
|
+
def __repr__(self):
|
|
120
|
+
return f"Cell({self.coordinate}, {self.agents})"
|
|
121
|
+
|
|
122
|
+
# FIXME: Revisit caching strategy on methods
|
|
123
|
+
@cache # noqa: B019
|
|
124
|
+
def neighborhood(self, radius=1, include_center=False):
|
|
125
|
+
return CellCollection(
|
|
126
|
+
self._neighborhood(radius=radius, include_center=include_center),
|
|
127
|
+
random=self.random,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# FIXME: Revisit caching strategy on methods
|
|
131
|
+
@cache # noqa: B019
|
|
132
|
+
def _neighborhood(self, radius=1, include_center=False):
|
|
133
|
+
# if radius == 0:
|
|
134
|
+
# return {self: self.agents}
|
|
135
|
+
if radius < 1:
|
|
136
|
+
raise ValueError("radius must be larger than one")
|
|
137
|
+
if radius == 1:
|
|
138
|
+
neighborhood = {neighbor: neighbor.agents for neighbor in self._connections}
|
|
139
|
+
if not include_center:
|
|
140
|
+
return neighborhood
|
|
141
|
+
else:
|
|
142
|
+
neighborhood[self] = self.agents
|
|
143
|
+
return neighborhood
|
|
144
|
+
else:
|
|
145
|
+
neighborhood = {}
|
|
146
|
+
for neighbor in self._connections:
|
|
147
|
+
neighborhood.update(
|
|
148
|
+
neighbor._neighborhood(radius - 1, include_center=True)
|
|
149
|
+
)
|
|
150
|
+
if not include_center:
|
|
151
|
+
neighborhood.pop(self, None)
|
|
152
|
+
return neighborhood
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from mesa import Agent, Model
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from mesa.experimental.cell_space.cell import Cell
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CellAgent(Agent):
|
|
12
|
+
"""Cell Agent is an extension of the Agent class and adds behavior for moving in discrete spaces
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
Attributes:
|
|
16
|
+
unique_id (int): A unique identifier for this agent.
|
|
17
|
+
model (Model): The model instance to which the agent belongs
|
|
18
|
+
pos: (Position | None): The position of the agent in the space
|
|
19
|
+
cell: (Cell | None): the cell which the agent occupies
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, unique_id: int, model: Model) -> None:
|
|
23
|
+
"""
|
|
24
|
+
Create a new agent.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
unique_id (int): A unique identifier for this agent.
|
|
28
|
+
model (Model): The model instance in which the agent exists.
|
|
29
|
+
"""
|
|
30
|
+
super().__init__(unique_id, model)
|
|
31
|
+
self.cell: Cell | None = None
|
|
32
|
+
|
|
33
|
+
def move_to(self, cell) -> None:
|
|
34
|
+
if self.cell is not None:
|
|
35
|
+
self.cell.remove_agent(self)
|
|
36
|
+
self.cell = cell
|
|
37
|
+
cell.add_agent(self)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import itertools
|
|
4
|
+
from collections.abc import Iterable, Mapping
|
|
5
|
+
from functools import cached_property
|
|
6
|
+
from random import Random
|
|
7
|
+
from typing import TYPE_CHECKING, Callable, Generic, TypeVar
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from mesa.experimental.cell_space.cell import Cell
|
|
11
|
+
from mesa.experimental.cell_space.cell_agent import CellAgent
|
|
12
|
+
|
|
13
|
+
T = TypeVar("T", bound="Cell")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CellCollection(Generic[T]):
|
|
17
|
+
"""An immutable collection of cells
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
cells (List[Cell]): The list of cells this collection represents
|
|
21
|
+
agents (List[CellAgent]) : List of agents occupying the cells in this collection
|
|
22
|
+
random (Random) : The random number generator
|
|
23
|
+
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
cells: Mapping[T, list[CellAgent]] | Iterable[T],
|
|
29
|
+
random: Random | None = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
if isinstance(cells, dict):
|
|
32
|
+
self._cells = cells
|
|
33
|
+
else:
|
|
34
|
+
self._cells = {cell: cell.agents for cell in cells}
|
|
35
|
+
|
|
36
|
+
#
|
|
37
|
+
self._capacity: int = next(iter(self._cells.keys())).capacity
|
|
38
|
+
|
|
39
|
+
if random is None:
|
|
40
|
+
random = Random() # FIXME
|
|
41
|
+
self.random = random
|
|
42
|
+
|
|
43
|
+
def __iter__(self):
|
|
44
|
+
return iter(self._cells)
|
|
45
|
+
|
|
46
|
+
def __getitem__(self, key: T) -> Iterable[CellAgent]:
|
|
47
|
+
return self._cells[key]
|
|
48
|
+
|
|
49
|
+
# @cached_property
|
|
50
|
+
def __len__(self) -> int:
|
|
51
|
+
return len(self._cells)
|
|
52
|
+
|
|
53
|
+
def __repr__(self):
|
|
54
|
+
return f"CellCollection({self._cells})"
|
|
55
|
+
|
|
56
|
+
@cached_property
|
|
57
|
+
def cells(self) -> list[T]:
|
|
58
|
+
return list(self._cells.keys())
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def agents(self) -> Iterable[CellAgent]:
|
|
62
|
+
return itertools.chain.from_iterable(self._cells.values())
|
|
63
|
+
|
|
64
|
+
def select_random_cell(self) -> T:
|
|
65
|
+
return self.random.choice(self.cells)
|
|
66
|
+
|
|
67
|
+
def select_random_agent(self) -> CellAgent:
|
|
68
|
+
return self.random.choice(list(self.agents))
|
|
69
|
+
|
|
70
|
+
def select(self, filter_func: Callable[[T], bool] | None = None, n=0):
|
|
71
|
+
# FIXME: n is not considered
|
|
72
|
+
if filter_func is None and n == 0:
|
|
73
|
+
return self
|
|
74
|
+
|
|
75
|
+
return CellCollection(
|
|
76
|
+
{
|
|
77
|
+
cell: agents
|
|
78
|
+
for cell, agents in self._cells.items()
|
|
79
|
+
if filter_func is None or filter_func(cell)
|
|
80
|
+
}
|
|
81
|
+
)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from functools import cached_property
|
|
4
|
+
from random import Random
|
|
5
|
+
from typing import Generic, TypeVar
|
|
6
|
+
|
|
7
|
+
from mesa.experimental.cell_space.cell import Cell
|
|
8
|
+
from mesa.experimental.cell_space.cell_collection import CellCollection
|
|
9
|
+
|
|
10
|
+
T = TypeVar("T", bound=Cell)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DiscreteSpace(Generic[T]):
|
|
14
|
+
"""Base class for all discrete spaces.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
capacity (int): The capacity of the cells in the discrete space
|
|
18
|
+
all_cells (CellCollection): The cells composing the discrete space
|
|
19
|
+
random (Random): The random number generator
|
|
20
|
+
cell_klass (Type) : the type of cell class
|
|
21
|
+
empties (CellCollection) : collecction of all cells that are empty
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
capacity: int | None = None,
|
|
28
|
+
cell_klass: type[T] = Cell,
|
|
29
|
+
random: Random | None = None,
|
|
30
|
+
):
|
|
31
|
+
super().__init__()
|
|
32
|
+
self.capacity = capacity
|
|
33
|
+
self._cells: dict[tuple[int, ...], T] = {}
|
|
34
|
+
if random is None:
|
|
35
|
+
random = Random() # FIXME should default to default rng from model
|
|
36
|
+
self.random = random
|
|
37
|
+
self.cell_klass = cell_klass
|
|
38
|
+
|
|
39
|
+
self._empties: dict[tuple[int, ...], None] = {}
|
|
40
|
+
self._empties_initialized = False
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def cutoff_empties(self):
|
|
44
|
+
return 7.953 * len(self._cells) ** 0.384
|
|
45
|
+
|
|
46
|
+
def _connect_single_cell(self, cell: T): ...
|
|
47
|
+
|
|
48
|
+
@cached_property
|
|
49
|
+
def all_cells(self):
|
|
50
|
+
return CellCollection({cell: cell.agents for cell in self._cells.values()})
|
|
51
|
+
|
|
52
|
+
def __iter__(self):
|
|
53
|
+
return iter(self._cells.values())
|
|
54
|
+
|
|
55
|
+
def __getitem__(self, key):
|
|
56
|
+
return self._cells[key]
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def empties(self) -> CellCollection:
|
|
60
|
+
return self.all_cells.select(lambda cell: cell.is_empty)
|
|
61
|
+
|
|
62
|
+
def select_random_empty_cell(self) -> T:
|
|
63
|
+
"""select random empty cell"""
|
|
64
|
+
return self.random.choice(list(self.empties))
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from itertools import product
|
|
5
|
+
from random import Random
|
|
6
|
+
from typing import Generic, TypeVar
|
|
7
|
+
|
|
8
|
+
from mesa.experimental.cell_space import Cell, DiscreteSpace
|
|
9
|
+
|
|
10
|
+
T = TypeVar("T", bound=Cell)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Grid(DiscreteSpace, Generic[T]):
|
|
14
|
+
"""Base class for all grid classes
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
dimensions (Sequence[int]): the dimensions of the grid
|
|
18
|
+
torus (bool): whether the grid is a torus
|
|
19
|
+
capacity (int): the capacity of a grid cell
|
|
20
|
+
random (Random): the random number generator
|
|
21
|
+
_try_random (bool): whether to get empty cell be repeatedly trying random cell
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
dimensions: Sequence[int],
|
|
28
|
+
torus: bool = False,
|
|
29
|
+
capacity: float | None = None,
|
|
30
|
+
random: Random | None = None,
|
|
31
|
+
cell_klass: type[T] = Cell,
|
|
32
|
+
) -> None:
|
|
33
|
+
super().__init__(capacity=capacity, random=random, cell_klass=cell_klass)
|
|
34
|
+
self.torus = torus
|
|
35
|
+
self.dimensions = dimensions
|
|
36
|
+
self._try_random = True
|
|
37
|
+
self._ndims = len(dimensions)
|
|
38
|
+
self._validate_parameters()
|
|
39
|
+
|
|
40
|
+
coordinates = product(*(range(dim) for dim in self.dimensions))
|
|
41
|
+
|
|
42
|
+
self._cells = {
|
|
43
|
+
coord: cell_klass(coord, capacity, random=self.random)
|
|
44
|
+
for coord in coordinates
|
|
45
|
+
}
|
|
46
|
+
self._connect_cells()
|
|
47
|
+
|
|
48
|
+
def _connect_cells(self) -> None:
|
|
49
|
+
if self._ndims == 2:
|
|
50
|
+
self._connect_cells_2d()
|
|
51
|
+
else:
|
|
52
|
+
self._connect_cells_nd()
|
|
53
|
+
|
|
54
|
+
def _connect_cells_2d(self) -> None: ...
|
|
55
|
+
|
|
56
|
+
def _connect_cells_nd(self) -> None: ...
|
|
57
|
+
|
|
58
|
+
def _validate_parameters(self):
|
|
59
|
+
if not all(isinstance(dim, int) and dim > 0 for dim in self.dimensions):
|
|
60
|
+
raise ValueError("Dimensions must be a list of positive integers.")
|
|
61
|
+
if not isinstance(self.torus, bool):
|
|
62
|
+
raise ValueError("Torus must be a boolean.")
|
|
63
|
+
if self.capacity is not None and not isinstance(self.capacity, (float, int)):
|
|
64
|
+
raise ValueError("Capacity must be a number or None.")
|
|
65
|
+
|
|
66
|
+
def select_random_empty_cell(self) -> T:
|
|
67
|
+
# FIXME:: currently just a simple boolean to control behavior
|
|
68
|
+
# FIXME:: basically if grid is close to 99% full, creating empty list can be faster
|
|
69
|
+
# FIXME:: note however that the old results don't apply because in this implementation
|
|
70
|
+
# FIXME:: because empties list needs to be rebuild each time
|
|
71
|
+
# This method is based on Agents.jl's random_empty() implementation. See
|
|
72
|
+
# https://github.com/JuliaDynamics/Agents.jl/pull/541. For the discussion, see
|
|
73
|
+
# https://github.com/projectmesa/mesa/issues/1052 and
|
|
74
|
+
# https://github.com/projectmesa/mesa/pull/1565. The cutoff value provided
|
|
75
|
+
# is the break-even comparison with the time taken in the else branching point.
|
|
76
|
+
if self._try_random:
|
|
77
|
+
while True:
|
|
78
|
+
cell = self.all_cells.select_random_cell()
|
|
79
|
+
if cell.is_empty:
|
|
80
|
+
return cell
|
|
81
|
+
else:
|
|
82
|
+
return super().select_random_empty_cell()
|
|
83
|
+
|
|
84
|
+
def _connect_single_cell_nd(self, cell: T, offsets: list[tuple[int, ...]]) -> None:
|
|
85
|
+
coord = cell.coordinate
|
|
86
|
+
|
|
87
|
+
for d_coord in offsets:
|
|
88
|
+
n_coord = tuple(c + dc for c, dc in zip(coord, d_coord))
|
|
89
|
+
if self.torus:
|
|
90
|
+
n_coord = tuple(nc % d for nc, d in zip(n_coord, self.dimensions))
|
|
91
|
+
if all(0 <= nc < d for nc, d in zip(n_coord, self.dimensions)):
|
|
92
|
+
cell.connect(self._cells[n_coord])
|
|
93
|
+
|
|
94
|
+
def _connect_single_cell_2d(self, cell: T, offsets: list[tuple[int, int]]) -> None:
|
|
95
|
+
i, j = cell.coordinate
|
|
96
|
+
height, width = self.dimensions
|
|
97
|
+
|
|
98
|
+
for di, dj in offsets:
|
|
99
|
+
ni, nj = (i + di, j + dj)
|
|
100
|
+
if self.torus:
|
|
101
|
+
ni, nj = ni % height, nj % width
|
|
102
|
+
if 0 <= ni < height and 0 <= nj < width:
|
|
103
|
+
cell.connect(self._cells[ni, nj])
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class OrthogonalMooreGrid(Grid[T]):
|
|
107
|
+
"""Grid where cells are connected to their 8 neighbors.
|
|
108
|
+
|
|
109
|
+
Example for two dimensions:
|
|
110
|
+
directions = [
|
|
111
|
+
(-1, -1), (-1, 0), (-1, 1),
|
|
112
|
+
( 0, -1), ( 0, 1),
|
|
113
|
+
( 1, -1), ( 1, 0), ( 1, 1),
|
|
114
|
+
]
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
def _connect_cells_2d(self) -> None:
|
|
118
|
+
# fmt: off
|
|
119
|
+
offsets = [
|
|
120
|
+
(-1, -1), (-1, 0), (-1, 1),
|
|
121
|
+
( 0, -1), ( 0, 1),
|
|
122
|
+
( 1, -1), ( 1, 0), ( 1, 1),
|
|
123
|
+
]
|
|
124
|
+
# fmt: on
|
|
125
|
+
height, width = self.dimensions
|
|
126
|
+
|
|
127
|
+
for cell in self.all_cells:
|
|
128
|
+
self._connect_single_cell_2d(cell, offsets)
|
|
129
|
+
|
|
130
|
+
def _connect_cells_nd(self) -> None:
|
|
131
|
+
offsets = list(product([-1, 0, 1], repeat=len(self.dimensions)))
|
|
132
|
+
offsets.remove((0,) * len(self.dimensions)) # Remove the central cell
|
|
133
|
+
|
|
134
|
+
for cell in self.all_cells:
|
|
135
|
+
self._connect_single_cell_nd(cell, offsets)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class OrthogonalVonNeumannGrid(Grid[T]):
|
|
139
|
+
"""Grid where cells are connected to their 4 neighbors.
|
|
140
|
+
|
|
141
|
+
Example for two dimensions:
|
|
142
|
+
directions = [
|
|
143
|
+
(0, -1),
|
|
144
|
+
(-1, 0), ( 1, 0),
|
|
145
|
+
(0, 1),
|
|
146
|
+
]
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
def _connect_cells_2d(self) -> None:
|
|
150
|
+
# fmt: off
|
|
151
|
+
offsets = [
|
|
152
|
+
(-1, 0),
|
|
153
|
+
(0, -1), (0, 1),
|
|
154
|
+
( 1, 0),
|
|
155
|
+
]
|
|
156
|
+
# fmt: on
|
|
157
|
+
height, width = self.dimensions
|
|
158
|
+
|
|
159
|
+
for cell in self.all_cells:
|
|
160
|
+
self._connect_single_cell_2d(cell, offsets)
|
|
161
|
+
|
|
162
|
+
def _connect_cells_nd(self) -> None:
|
|
163
|
+
offsets = []
|
|
164
|
+
dimensions = len(self.dimensions)
|
|
165
|
+
for dim in range(dimensions):
|
|
166
|
+
for delta in [
|
|
167
|
+
-1,
|
|
168
|
+
1,
|
|
169
|
+
]: # Move one step in each direction for the current dimension
|
|
170
|
+
offset = [0] * dimensions
|
|
171
|
+
offset[dim] = delta
|
|
172
|
+
offsets.append(tuple(offset))
|
|
173
|
+
|
|
174
|
+
for cell in self.all_cells:
|
|
175
|
+
self._connect_single_cell_nd(cell, offsets)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class HexGrid(Grid[T]):
|
|
179
|
+
def _connect_cells_2d(self) -> None:
|
|
180
|
+
# fmt: off
|
|
181
|
+
even_offsets = [
|
|
182
|
+
(-1, -1), (-1, 0),
|
|
183
|
+
( 0, -1), ( 0, 1),
|
|
184
|
+
( 1, -1), ( 1, 0),
|
|
185
|
+
]
|
|
186
|
+
odd_offsets = [
|
|
187
|
+
(-1, 0), (-1, 1),
|
|
188
|
+
( 0, -1), ( 0, 1),
|
|
189
|
+
( 1, 0), ( 1, 1),
|
|
190
|
+
]
|
|
191
|
+
# fmt: on
|
|
192
|
+
|
|
193
|
+
for cell in self.all_cells:
|
|
194
|
+
i = cell.coordinate[0]
|
|
195
|
+
offsets = even_offsets if i % 2 == 0 else odd_offsets
|
|
196
|
+
self._connect_single_cell_2d(cell, offsets=offsets)
|
|
197
|
+
|
|
198
|
+
def _connect_cells_nd(self) -> None:
|
|
199
|
+
raise NotImplementedError("HexGrids are only defined for 2 dimensions")
|
|
200
|
+
|
|
201
|
+
def _validate_parameters(self):
|
|
202
|
+
super()._validate_parameters()
|
|
203
|
+
if len(self.dimensions) != 2:
|
|
204
|
+
raise ValueError("HexGrid must have exactly 2 dimensions.")
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from random import Random
|
|
2
|
+
from typing import Any, Optional
|
|
3
|
+
|
|
4
|
+
from mesa.experimental.cell_space.cell import Cell
|
|
5
|
+
from mesa.experimental.cell_space.discrete_space import DiscreteSpace
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Network(DiscreteSpace):
|
|
9
|
+
"""A networked discrete space"""
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
G: Any, # noqa: N803
|
|
14
|
+
capacity: Optional[int] = None,
|
|
15
|
+
random: Optional[Random] = None,
|
|
16
|
+
cell_klass: type[Cell] = Cell,
|
|
17
|
+
) -> None:
|
|
18
|
+
"""A Networked grid
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
G: a NetworkX Graph instance.
|
|
22
|
+
capacity (int) : the capacity of the cell
|
|
23
|
+
random (Random):
|
|
24
|
+
CellKlass (type[Cell]): The base Cell class to use in the Network
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
super().__init__(capacity=capacity, random=random, cell_klass=cell_klass)
|
|
28
|
+
self.G = G
|
|
29
|
+
|
|
30
|
+
for node_id in self.G.nodes:
|
|
31
|
+
self._cells[node_id] = self.cell_klass(
|
|
32
|
+
node_id, capacity, random=self.random
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
for cell in self.all_cells:
|
|
36
|
+
self._connect_single_cell(cell)
|
|
37
|
+
|
|
38
|
+
def _connect_single_cell(self, cell):
|
|
39
|
+
for node_id in self.G.neighbors(cell.coordinate):
|
|
40
|
+
cell.connect(self._cells[node_id])
|