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
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import math
|
|
2
|
+
|
|
3
|
+
import solara
|
|
4
|
+
|
|
5
|
+
from mesa.examples.basic.virus_on_network.model import (
|
|
6
|
+
State,
|
|
7
|
+
VirusOnNetwork,
|
|
8
|
+
number_infected,
|
|
9
|
+
)
|
|
10
|
+
from mesa.visualization import (
|
|
11
|
+
Slider,
|
|
12
|
+
SolaraViz,
|
|
13
|
+
make_plot_component,
|
|
14
|
+
make_space_component,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def agent_portrayal(agent):
|
|
19
|
+
node_color_dict = {
|
|
20
|
+
State.INFECTED: "tab:red",
|
|
21
|
+
State.SUSCEPTIBLE: "tab:green",
|
|
22
|
+
State.RESISTANT: "tab:gray",
|
|
23
|
+
}
|
|
24
|
+
return {"color": node_color_dict[agent.state], "size": 10}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_resistant_susceptible_ratio(model):
|
|
28
|
+
ratio = model.resistant_susceptible_ratio()
|
|
29
|
+
ratio_text = r"$\infty$" if ratio is math.inf else f"{ratio:.2f}"
|
|
30
|
+
infected_text = str(number_infected(model))
|
|
31
|
+
|
|
32
|
+
return solara.Markdown(
|
|
33
|
+
f"Resistant/Susceptible Ratio: {ratio_text}<br>Infected Remaining: {infected_text}"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
model_params = {
|
|
38
|
+
"num_nodes": Slider(
|
|
39
|
+
label="Number of agents",
|
|
40
|
+
value=10,
|
|
41
|
+
min=10,
|
|
42
|
+
max=100,
|
|
43
|
+
step=1,
|
|
44
|
+
),
|
|
45
|
+
"avg_node_degree": Slider(
|
|
46
|
+
label="Avg Node Degree",
|
|
47
|
+
value=3,
|
|
48
|
+
min=3,
|
|
49
|
+
max=8,
|
|
50
|
+
step=1,
|
|
51
|
+
),
|
|
52
|
+
"initial_outbreak_size": Slider(
|
|
53
|
+
label="Initial Outbreak Size",
|
|
54
|
+
value=1,
|
|
55
|
+
min=1,
|
|
56
|
+
max=10,
|
|
57
|
+
step=1,
|
|
58
|
+
),
|
|
59
|
+
"virus_spread_chance": Slider(
|
|
60
|
+
label="Virus Spread Chance",
|
|
61
|
+
value=0.4,
|
|
62
|
+
min=0.0,
|
|
63
|
+
max=1.0,
|
|
64
|
+
step=0.1,
|
|
65
|
+
),
|
|
66
|
+
"virus_check_frequency": Slider(
|
|
67
|
+
label="Virus Check Frequency",
|
|
68
|
+
value=0.4,
|
|
69
|
+
min=0.0,
|
|
70
|
+
max=1.0,
|
|
71
|
+
step=0.1,
|
|
72
|
+
),
|
|
73
|
+
"recovery_chance": Slider(
|
|
74
|
+
label="Recovery Chance",
|
|
75
|
+
value=0.3,
|
|
76
|
+
min=0.0,
|
|
77
|
+
max=1.0,
|
|
78
|
+
step=0.1,
|
|
79
|
+
),
|
|
80
|
+
"gain_resistance_chance": Slider(
|
|
81
|
+
label="Gain Resistance Chance",
|
|
82
|
+
value=0.5,
|
|
83
|
+
min=0.0,
|
|
84
|
+
max=1.0,
|
|
85
|
+
step=0.1,
|
|
86
|
+
),
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def post_process_lineplot(ax):
|
|
91
|
+
ax.set_ylim(ymin=0)
|
|
92
|
+
ax.set_ylabel("# people")
|
|
93
|
+
ax.legend(bbox_to_anchor=(1.05, 1.0), loc="upper left")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
SpacePlot = make_space_component(agent_portrayal)
|
|
97
|
+
StatePlot = make_plot_component(
|
|
98
|
+
{"Infected": "tab:red", "Susceptible": "tab:green", "Resistant": "tab:gray"},
|
|
99
|
+
post_process=post_process_lineplot,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
model1 = VirusOnNetwork()
|
|
103
|
+
|
|
104
|
+
page = SolaraViz(
|
|
105
|
+
model1,
|
|
106
|
+
[
|
|
107
|
+
SpacePlot,
|
|
108
|
+
StatePlot,
|
|
109
|
+
get_resistant_susceptible_ratio,
|
|
110
|
+
],
|
|
111
|
+
model_params=model_params,
|
|
112
|
+
name="Virus Model",
|
|
113
|
+
)
|
|
114
|
+
page # noqa
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import math
|
|
2
|
+
|
|
3
|
+
import networkx as nx
|
|
4
|
+
|
|
5
|
+
import mesa
|
|
6
|
+
from mesa import Model
|
|
7
|
+
from mesa.examples.basic.virus_on_network.agents import State, VirusAgent
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def number_state(model, state):
|
|
11
|
+
return sum(1 for a in model.grid.get_all_cell_contents() if a.state is state)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def number_infected(model):
|
|
15
|
+
return number_state(model, State.INFECTED)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def number_susceptible(model):
|
|
19
|
+
return number_state(model, State.SUSCEPTIBLE)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def number_resistant(model):
|
|
23
|
+
return number_state(model, State.RESISTANT)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class VirusOnNetwork(Model):
|
|
27
|
+
"""A virus model with some number of agents."""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
num_nodes=10,
|
|
32
|
+
avg_node_degree=3,
|
|
33
|
+
initial_outbreak_size=1,
|
|
34
|
+
virus_spread_chance=0.4,
|
|
35
|
+
virus_check_frequency=0.4,
|
|
36
|
+
recovery_chance=0.3,
|
|
37
|
+
gain_resistance_chance=0.5,
|
|
38
|
+
seed=None,
|
|
39
|
+
):
|
|
40
|
+
super().__init__(seed=seed)
|
|
41
|
+
self.num_nodes = num_nodes
|
|
42
|
+
prob = avg_node_degree / self.num_nodes
|
|
43
|
+
self.G = nx.erdos_renyi_graph(n=self.num_nodes, p=prob)
|
|
44
|
+
self.grid = mesa.space.NetworkGrid(self.G)
|
|
45
|
+
|
|
46
|
+
self.initial_outbreak_size = (
|
|
47
|
+
initial_outbreak_size if initial_outbreak_size <= num_nodes else num_nodes
|
|
48
|
+
)
|
|
49
|
+
self.virus_spread_chance = virus_spread_chance
|
|
50
|
+
self.virus_check_frequency = virus_check_frequency
|
|
51
|
+
self.recovery_chance = recovery_chance
|
|
52
|
+
self.gain_resistance_chance = gain_resistance_chance
|
|
53
|
+
|
|
54
|
+
self.datacollector = mesa.DataCollector(
|
|
55
|
+
{
|
|
56
|
+
"Infected": number_infected,
|
|
57
|
+
"Susceptible": number_susceptible,
|
|
58
|
+
"Resistant": number_resistant,
|
|
59
|
+
"R over S": self.resistant_susceptible_ratio,
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Create agents
|
|
64
|
+
for node in self.G.nodes():
|
|
65
|
+
a = VirusAgent(
|
|
66
|
+
self,
|
|
67
|
+
State.SUSCEPTIBLE,
|
|
68
|
+
self.virus_spread_chance,
|
|
69
|
+
self.virus_check_frequency,
|
|
70
|
+
self.recovery_chance,
|
|
71
|
+
self.gain_resistance_chance,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Add the agent to the node
|
|
75
|
+
self.grid.place_agent(a, node)
|
|
76
|
+
|
|
77
|
+
# Infect some nodes
|
|
78
|
+
infected_nodes = self.random.sample(list(self.G), self.initial_outbreak_size)
|
|
79
|
+
for a in self.grid.get_cell_list_contents(infected_nodes):
|
|
80
|
+
a.state = State.INFECTED
|
|
81
|
+
|
|
82
|
+
self.running = True
|
|
83
|
+
self.datacollector.collect(self)
|
|
84
|
+
|
|
85
|
+
def resistant_susceptible_ratio(self):
|
|
86
|
+
try:
|
|
87
|
+
return number_state(self, State.RESISTANT) / number_state(
|
|
88
|
+
self, State.SUSCEPTIBLE
|
|
89
|
+
)
|
|
90
|
+
except ZeroDivisionError:
|
|
91
|
+
return math.inf
|
|
92
|
+
|
|
93
|
+
def step(self):
|
|
94
|
+
self.agents.shuffle_do("step")
|
|
95
|
+
# collect data
|
|
96
|
+
self.datacollector.collect(self)
|
mesa/experimental/UserParam.py
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
"""helper classes."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class UserParam: # noqa: D101
|
|
2
5
|
_ERROR_MESSAGE = "Missing or malformed inputs for '{}' Option '{}'"
|
|
3
6
|
|
|
4
|
-
def maybe_raise_error(self, param_type, valid):
|
|
7
|
+
def maybe_raise_error(self, param_type, valid): # noqa: D102
|
|
5
8
|
if valid:
|
|
6
9
|
return
|
|
7
10
|
msg = self._ERROR_MESSAGE.format(param_type, self.label)
|
|
@@ -9,11 +12,9 @@ class UserParam:
|
|
|
9
12
|
|
|
10
13
|
|
|
11
14
|
class Slider(UserParam):
|
|
12
|
-
"""
|
|
13
|
-
A number-based slider input with settable increment.
|
|
15
|
+
"""A number-based slider input with settable increment.
|
|
14
16
|
|
|
15
17
|
Example:
|
|
16
|
-
|
|
17
18
|
slider_option = Slider("My Slider", value=123, min=10, max=200, step=0.1)
|
|
18
19
|
|
|
19
20
|
Args:
|
|
@@ -34,6 +35,16 @@ class Slider(UserParam):
|
|
|
34
35
|
step=1,
|
|
35
36
|
dtype=None,
|
|
36
37
|
):
|
|
38
|
+
"""Slider class.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
label: The displayed label in the UI
|
|
42
|
+
value: The initial value of the slider
|
|
43
|
+
min: The minimum possible value of the slider
|
|
44
|
+
max: The maximum possible value of the slider
|
|
45
|
+
step: The step between min and max for a range of possible values
|
|
46
|
+
dtype: either int or float
|
|
47
|
+
"""
|
|
37
48
|
self.label = label
|
|
38
49
|
self.value = value
|
|
39
50
|
self.min = min
|
|
@@ -47,10 +58,10 @@ class Slider(UserParam):
|
|
|
47
58
|
if dtype is None:
|
|
48
59
|
self.is_float_slider = self._check_values_are_float(value, min, max, step)
|
|
49
60
|
else:
|
|
50
|
-
self.is_float_slider = dtype
|
|
61
|
+
self.is_float_slider = dtype is float
|
|
51
62
|
|
|
52
63
|
def _check_values_are_float(self, value, min, max, step):
|
|
53
64
|
return any(isinstance(n, float) for n in (value, min, max, step))
|
|
54
65
|
|
|
55
|
-
def get(self, attr):
|
|
66
|
+
def get(self, attr): # noqa: D102
|
|
56
67
|
return getattr(self, attr)
|
mesa/experimental/__init__.py
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
"""Experimental init."""
|
|
2
|
+
|
|
2
3
|
from mesa.experimental import cell_space
|
|
3
4
|
|
|
5
|
+
try:
|
|
6
|
+
from .solara_viz import JupyterViz, Slider, SolaraViz, make_text
|
|
4
7
|
|
|
5
|
-
__all__ = ["
|
|
8
|
+
__all__ = ["cell_space", "JupyterViz", "Slider", "SolaraViz", "make_text"]
|
|
9
|
+
except ImportError:
|
|
10
|
+
print(
|
|
11
|
+
"Could not import SolaraViz. If you need it, install with 'pip install --pre mesa[viz]'"
|
|
12
|
+
)
|
|
13
|
+
__all__ = ["cell_space"]
|
|
@@ -1,5 +1,16 @@
|
|
|
1
|
+
"""Cell spaces.
|
|
2
|
+
|
|
3
|
+
Cell spaces offer an alternative API for discrete spaces. It is experimental and under development. The API is more
|
|
4
|
+
expressive that the default grids available in `mesa.space`.
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
|
|
1
8
|
from mesa.experimental.cell_space.cell import Cell
|
|
2
|
-
from mesa.experimental.cell_space.cell_agent import
|
|
9
|
+
from mesa.experimental.cell_space.cell_agent import (
|
|
10
|
+
CellAgent,
|
|
11
|
+
FixedAgent,
|
|
12
|
+
Grid2DMovingAgent,
|
|
13
|
+
)
|
|
3
14
|
from mesa.experimental.cell_space.cell_collection import CellCollection
|
|
4
15
|
from mesa.experimental.cell_space.discrete_space import DiscreteSpace
|
|
5
16
|
from mesa.experimental.cell_space.grid import (
|
|
@@ -9,15 +20,19 @@ from mesa.experimental.cell_space.grid import (
|
|
|
9
20
|
OrthogonalVonNeumannGrid,
|
|
10
21
|
)
|
|
11
22
|
from mesa.experimental.cell_space.network import Network
|
|
23
|
+
from mesa.experimental.cell_space.voronoi import VoronoiGrid
|
|
12
24
|
|
|
13
25
|
__all__ = [
|
|
14
26
|
"CellCollection",
|
|
15
27
|
"Cell",
|
|
16
28
|
"CellAgent",
|
|
29
|
+
"Grid2DMovingAgent",
|
|
30
|
+
"FixedAgent",
|
|
17
31
|
"DiscreteSpace",
|
|
18
32
|
"Grid",
|
|
19
33
|
"HexGrid",
|
|
20
34
|
"OrthogonalMooreGrid",
|
|
21
35
|
"OrthogonalVonNeumannGrid",
|
|
22
36
|
"Network",
|
|
37
|
+
"VoronoiGrid",
|
|
23
38
|
]
|
|
@@ -1,13 +1,20 @@
|
|
|
1
|
+
"""The Cell in a cell space."""
|
|
2
|
+
|
|
1
3
|
from __future__ import annotations
|
|
2
4
|
|
|
3
|
-
from
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from functools import cache, cached_property
|
|
4
7
|
from random import Random
|
|
5
|
-
from typing import TYPE_CHECKING
|
|
8
|
+
from typing import TYPE_CHECKING, Any
|
|
6
9
|
|
|
10
|
+
from mesa.experimental.cell_space.cell_agent import CellAgent
|
|
7
11
|
from mesa.experimental.cell_space.cell_collection import CellCollection
|
|
12
|
+
from mesa.space import PropertyLayer
|
|
8
13
|
|
|
9
14
|
if TYPE_CHECKING:
|
|
10
|
-
from mesa.
|
|
15
|
+
from mesa.agent import Agent
|
|
16
|
+
|
|
17
|
+
Coordinate = tuple[int, ...]
|
|
11
18
|
|
|
12
19
|
|
|
13
20
|
class Cell:
|
|
@@ -24,11 +31,13 @@ class Cell:
|
|
|
24
31
|
|
|
25
32
|
__slots__ = [
|
|
26
33
|
"coordinate",
|
|
27
|
-
"
|
|
34
|
+
"connections",
|
|
28
35
|
"agents",
|
|
29
36
|
"capacity",
|
|
30
37
|
"properties",
|
|
31
38
|
"random",
|
|
39
|
+
"_mesa_property_layers",
|
|
40
|
+
"__dict__",
|
|
32
41
|
]
|
|
33
42
|
|
|
34
43
|
# def __new__(cls,
|
|
@@ -42,34 +51,40 @@ class Cell:
|
|
|
42
51
|
|
|
43
52
|
def __init__(
|
|
44
53
|
self,
|
|
45
|
-
coordinate:
|
|
46
|
-
capacity:
|
|
54
|
+
coordinate: Coordinate,
|
|
55
|
+
capacity: int | None = None,
|
|
47
56
|
random: Random | None = None,
|
|
48
57
|
) -> None:
|
|
49
|
-
"""
|
|
58
|
+
"""Initialise the cell.
|
|
50
59
|
|
|
51
60
|
Args:
|
|
52
|
-
coordinate:
|
|
61
|
+
coordinate: coordinates of the cell
|
|
53
62
|
capacity (int) : the capacity of the cell. If None, the capacity is infinite
|
|
54
63
|
random (Random) : the random number generator to use
|
|
55
64
|
|
|
56
65
|
"""
|
|
57
66
|
super().__init__()
|
|
58
67
|
self.coordinate = coordinate
|
|
59
|
-
self.
|
|
60
|
-
self.agents
|
|
61
|
-
|
|
62
|
-
|
|
68
|
+
self.connections: dict[Coordinate, Cell] = {}
|
|
69
|
+
self.agents: list[
|
|
70
|
+
Agent
|
|
71
|
+
] = [] # TODO:: change to AgentSet or weakrefs? (neither is very performant, )
|
|
72
|
+
self.capacity: int | None = capacity
|
|
73
|
+
self.properties: dict[Coordinate, object] = {}
|
|
63
74
|
self.random = random
|
|
75
|
+
self._mesa_property_layers: dict[str, PropertyLayer] = {}
|
|
64
76
|
|
|
65
|
-
def connect(self, other: Cell) -> None:
|
|
77
|
+
def connect(self, other: Cell, key: Coordinate | None = None) -> None:
|
|
66
78
|
"""Connects this cell to another cell.
|
|
67
79
|
|
|
68
80
|
Args:
|
|
69
81
|
other (Cell): other cell to connect to
|
|
82
|
+
key (Tuple[int, ...]): key for the connection. Should resemble a relative coordinate
|
|
70
83
|
|
|
71
84
|
"""
|
|
72
|
-
|
|
85
|
+
if key is None:
|
|
86
|
+
key = other.coordinate
|
|
87
|
+
self.connections[key] = other
|
|
73
88
|
|
|
74
89
|
def disconnect(self, other: Cell) -> None:
|
|
75
90
|
"""Disconnects this cell from another cell.
|
|
@@ -78,7 +93,9 @@ class Cell:
|
|
|
78
93
|
other (Cell): other cell to remove from connections
|
|
79
94
|
|
|
80
95
|
"""
|
|
81
|
-
self.
|
|
96
|
+
keys_to_remove = [k for k, v in self.connections.items() if v == other]
|
|
97
|
+
for key in keys_to_remove:
|
|
98
|
+
del self.connections[key]
|
|
82
99
|
|
|
83
100
|
def add_agent(self, agent: CellAgent) -> None:
|
|
84
101
|
"""Adds an agent to the cell.
|
|
@@ -104,7 +121,6 @@ class Cell:
|
|
|
104
121
|
|
|
105
122
|
"""
|
|
106
123
|
self.agents.remove(agent)
|
|
107
|
-
agent.cell = None
|
|
108
124
|
|
|
109
125
|
@property
|
|
110
126
|
def is_empty(self) -> bool:
|
|
@@ -116,37 +132,91 @@ class Cell:
|
|
|
116
132
|
"""Returns a bool of the contents of a cell."""
|
|
117
133
|
return len(self.agents) == self.capacity
|
|
118
134
|
|
|
119
|
-
def __repr__(self):
|
|
135
|
+
def __repr__(self): # noqa
|
|
120
136
|
return f"Cell({self.coordinate}, {self.agents})"
|
|
121
137
|
|
|
138
|
+
@cached_property
|
|
139
|
+
def neighborhood(self) -> CellCollection[Cell]:
|
|
140
|
+
"""Returns the direct neighborhood of the cell.
|
|
141
|
+
|
|
142
|
+
This is equivalent to cell.get_neighborhood(radius=1)
|
|
143
|
+
|
|
144
|
+
"""
|
|
145
|
+
return self.get_neighborhood()
|
|
146
|
+
|
|
122
147
|
# FIXME: Revisit caching strategy on methods
|
|
123
148
|
@cache # noqa: B019
|
|
124
|
-
def
|
|
125
|
-
|
|
149
|
+
def get_neighborhood(
|
|
150
|
+
self, radius: int = 1, include_center: bool = False
|
|
151
|
+
) -> CellCollection[Cell]:
|
|
152
|
+
"""Returns a list of all neighboring cells for the given radius.
|
|
153
|
+
|
|
154
|
+
For getting the direct neighborhood (i.e., radius=1) you can also use
|
|
155
|
+
the `neighborhood` property.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
radius (int): the radius of the neighborhood
|
|
159
|
+
include_center (bool): include the center of the neighborhood
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
a list of all neighboring cells
|
|
163
|
+
|
|
164
|
+
"""
|
|
165
|
+
return CellCollection[Cell](
|
|
126
166
|
self._neighborhood(radius=radius, include_center=include_center),
|
|
127
167
|
random=self.random,
|
|
128
168
|
)
|
|
129
169
|
|
|
130
170
|
# FIXME: Revisit caching strategy on methods
|
|
131
171
|
@cache # noqa: B019
|
|
132
|
-
def _neighborhood(
|
|
172
|
+
def _neighborhood(
|
|
173
|
+
self, radius: int = 1, include_center: bool = False
|
|
174
|
+
) -> dict[Cell, list[Agent]]:
|
|
133
175
|
# if radius == 0:
|
|
134
176
|
# return {self: self.agents}
|
|
135
177
|
if radius < 1:
|
|
136
178
|
raise ValueError("radius must be larger than one")
|
|
137
179
|
if radius == 1:
|
|
138
|
-
neighborhood = {
|
|
180
|
+
neighborhood = {
|
|
181
|
+
neighbor: neighbor.agents for neighbor in self.connections.values()
|
|
182
|
+
}
|
|
139
183
|
if not include_center:
|
|
140
184
|
return neighborhood
|
|
141
185
|
else:
|
|
142
186
|
neighborhood[self] = self.agents
|
|
143
187
|
return neighborhood
|
|
144
188
|
else:
|
|
145
|
-
neighborhood = {}
|
|
146
|
-
for neighbor in self.
|
|
189
|
+
neighborhood: dict[Cell, list[Agent]] = {}
|
|
190
|
+
for neighbor in self.connections.values():
|
|
147
191
|
neighborhood.update(
|
|
148
192
|
neighbor._neighborhood(radius - 1, include_center=True)
|
|
149
193
|
)
|
|
150
194
|
if not include_center:
|
|
151
195
|
neighborhood.pop(self, None)
|
|
152
196
|
return neighborhood
|
|
197
|
+
|
|
198
|
+
# PropertyLayer methods
|
|
199
|
+
def get_property(self, property_name: str) -> Any:
|
|
200
|
+
"""Get the value of a property."""
|
|
201
|
+
return self._mesa_property_layers[property_name].data[self.coordinate]
|
|
202
|
+
|
|
203
|
+
def set_property(self, property_name: str, value: Any):
|
|
204
|
+
"""Set the value of a property."""
|
|
205
|
+
self._mesa_property_layers[property_name].set_cell(self.coordinate, value)
|
|
206
|
+
|
|
207
|
+
def modify_property(
|
|
208
|
+
self, property_name: str, operation: Callable, value: Any = None
|
|
209
|
+
):
|
|
210
|
+
"""Modify the value of a property."""
|
|
211
|
+
self._mesa_property_layers[property_name].modify_cell(
|
|
212
|
+
self.coordinate, operation, value
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
def __getstate__(self):
|
|
216
|
+
"""Return state of the Cell with connections set to empty."""
|
|
217
|
+
# fixme, once we shift to 3.11, replace this with super. __getstate__
|
|
218
|
+
state = (self.__dict__, {k: getattr(self, k) for k in self.__slots__})
|
|
219
|
+
state[1][
|
|
220
|
+
"connections"
|
|
221
|
+
] = {} # replace this with empty connections to avoid infinite recursion error in pickle/deepcopy
|
|
222
|
+
return state
|
|
@@ -1,37 +1,133 @@
|
|
|
1
|
+
"""An agent with movement methods for cell spaces."""
|
|
2
|
+
|
|
1
3
|
from __future__ import annotations
|
|
2
4
|
|
|
3
|
-
from typing import TYPE_CHECKING
|
|
5
|
+
from typing import TYPE_CHECKING, Protocol
|
|
4
6
|
|
|
5
|
-
from mesa import Agent
|
|
7
|
+
from mesa.agent import Agent
|
|
6
8
|
|
|
7
9
|
if TYPE_CHECKING:
|
|
8
|
-
from mesa.experimental.cell_space
|
|
10
|
+
from mesa.experimental.cell_space import Cell
|
|
9
11
|
|
|
10
12
|
|
|
11
|
-
class
|
|
12
|
-
"""
|
|
13
|
+
class HasCellProtocol(Protocol):
|
|
14
|
+
"""Protocol for discrete space cell holders."""
|
|
13
15
|
|
|
16
|
+
cell: Cell
|
|
14
17
|
|
|
15
|
-
Attributes:
|
|
16
|
-
unique_id (int): A unique identifier for this agent.
|
|
17
|
-
model (Model): The model instance to which the agent belongs
|
|
18
|
-
pos: (Position | None): The position of the agent in the space
|
|
19
|
-
cell: (Cell | None): the cell which the agent occupies
|
|
20
|
-
"""
|
|
21
18
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
Create a new agent.
|
|
19
|
+
class HasCell:
|
|
20
|
+
"""Descriptor for cell movement behavior."""
|
|
25
21
|
|
|
26
|
-
|
|
27
|
-
unique_id (int): A unique identifier for this agent.
|
|
28
|
-
model (Model): The model instance in which the agent exists.
|
|
29
|
-
"""
|
|
30
|
-
super().__init__(unique_id, model)
|
|
31
|
-
self.cell: Cell | None = None
|
|
22
|
+
_mesa_cell: Cell | None = None
|
|
32
23
|
|
|
33
|
-
|
|
24
|
+
@property
|
|
25
|
+
def cell(self) -> Cell | None: # noqa: D102
|
|
26
|
+
return self._mesa_cell
|
|
27
|
+
|
|
28
|
+
@cell.setter
|
|
29
|
+
def cell(self, cell: Cell | None) -> None:
|
|
30
|
+
# remove from current cell
|
|
34
31
|
if self.cell is not None:
|
|
35
32
|
self.cell.remove_agent(self)
|
|
33
|
+
|
|
34
|
+
# update private attribute
|
|
35
|
+
self._mesa_cell = cell
|
|
36
|
+
|
|
37
|
+
# add to new cell
|
|
38
|
+
if cell is not None:
|
|
39
|
+
cell.add_agent(self)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class BasicMovement:
|
|
43
|
+
"""Mixin for moving agents in discrete space."""
|
|
44
|
+
|
|
45
|
+
def move_to(self: HasCellProtocol, cell: Cell) -> None:
|
|
46
|
+
"""Move to a new cell."""
|
|
36
47
|
self.cell = cell
|
|
48
|
+
|
|
49
|
+
def move_relative(self: HasCellProtocol, direction: tuple[int, ...]):
|
|
50
|
+
"""Move to a cell relative to the current cell.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
direction: The direction to move in.
|
|
54
|
+
"""
|
|
55
|
+
new_cell = self.cell.connections.get(direction)
|
|
56
|
+
if new_cell is not None:
|
|
57
|
+
self.cell = new_cell
|
|
58
|
+
else:
|
|
59
|
+
raise ValueError(f"No cell in direction {direction}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class FixedCell(HasCell):
|
|
63
|
+
"""Mixin for agents that are fixed to a cell."""
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def cell(self) -> Cell | None: # noqa: D102
|
|
67
|
+
return self._mesa_cell
|
|
68
|
+
|
|
69
|
+
@cell.setter
|
|
70
|
+
def cell(self, cell: Cell) -> None:
|
|
71
|
+
if self.cell is not None:
|
|
72
|
+
raise ValueError("Cannot move agent in FixedCell")
|
|
73
|
+
self._mesa_cell = cell
|
|
74
|
+
|
|
37
75
|
cell.add_agent(self)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class CellAgent(Agent, HasCell, BasicMovement):
|
|
79
|
+
"""Cell Agent is an extension of the Agent class and adds behavior for moving in discrete spaces.
|
|
80
|
+
|
|
81
|
+
Attributes:
|
|
82
|
+
cell (Cell): The cell the agent is currently in.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def remove(self):
|
|
86
|
+
"""Remove the agent from the model."""
|
|
87
|
+
super().remove()
|
|
88
|
+
self.cell = None # ensures that we are also removed from cell
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class FixedAgent(Agent, FixedCell):
|
|
92
|
+
"""A patch in a 2D grid."""
|
|
93
|
+
|
|
94
|
+
def remove(self):
|
|
95
|
+
"""Remove the agent from the model."""
|
|
96
|
+
super().remove()
|
|
97
|
+
|
|
98
|
+
# fixme we leave self._mesa_cell on the original value
|
|
99
|
+
# so you cannot hijack remove() to move patches
|
|
100
|
+
self.cell.remove_agent(self)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class Grid2DMovingAgent(CellAgent):
|
|
104
|
+
"""Mixin for moving agents in 2D grids."""
|
|
105
|
+
|
|
106
|
+
# fmt: off
|
|
107
|
+
DIRECTION_MAP = {
|
|
108
|
+
"n": (-1, 0), "north": (-1, 0), "up": (-1, 0),
|
|
109
|
+
"s": (1, 0), "south": (1, 0), "down": (1, 0),
|
|
110
|
+
"e": (0, 1), "east": (0, 1), "right": (0, 1),
|
|
111
|
+
"w": (0, -1), "west": (0, -1), "left": (0, -1),
|
|
112
|
+
"ne": (-1, 1), "northeast": (-1, 1), "upright": (-1, 1),
|
|
113
|
+
"nw": (-1, -1), "northwest": (-1, -1), "upleft": (-1, -1),
|
|
114
|
+
"se": (1, 1), "southeast": (1, 1), "downright": (1, 1),
|
|
115
|
+
"sw": (1, -1), "southwest": (1, -1), "downleft": (1, -1)
|
|
116
|
+
}
|
|
117
|
+
# fmt: on
|
|
118
|
+
|
|
119
|
+
def move(self, direction: str, distance: int = 1):
|
|
120
|
+
"""Move the agent in a cardinal direction.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
direction: The cardinal direction to move in.
|
|
124
|
+
distance: The distance to move.
|
|
125
|
+
"""
|
|
126
|
+
direction = direction.lower() # Convert direction to lowercase
|
|
127
|
+
|
|
128
|
+
if direction not in self.DIRECTION_MAP:
|
|
129
|
+
raise ValueError(f"Invalid direction: {direction}")
|
|
130
|
+
|
|
131
|
+
move_vector = self.DIRECTION_MAP[direction]
|
|
132
|
+
for _ in range(distance):
|
|
133
|
+
self.move_relative(move_vector)
|