Mesa 3.0.0a5__py3-none-any.whl → 3.0.0b1__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.
- examples/README.md +37 -0
- examples/__init__.py +0 -0
- examples/advanced/__init__.py +0 -0
- examples/advanced/epstein_civil_violence/Epstein Civil Violence.ipynb +116 -0
- examples/advanced/epstein_civil_violence/Readme.md +33 -0
- examples/advanced/epstein_civil_violence/epstein_civil_violence/__init__.py +0 -0
- examples/advanced/epstein_civil_violence/epstein_civil_violence/agent.py +158 -0
- examples/advanced/epstein_civil_violence/epstein_civil_violence/model.py +146 -0
- examples/advanced/epstein_civil_violence/epstein_civil_violence/portrayal.py +33 -0
- examples/advanced/epstein_civil_violence/epstein_civil_violence/server.py +81 -0
- examples/advanced/epstein_civil_violence/requirements.txt +3 -0
- examples/advanced/epstein_civil_violence/run.py +3 -0
- examples/advanced/pd_grid/analysis.ipynb +228 -0
- examples/advanced/pd_grid/pd_grid/__init__.py +0 -0
- examples/advanced/pd_grid/pd_grid/agent.py +50 -0
- examples/advanced/pd_grid/pd_grid/model.py +72 -0
- examples/advanced/pd_grid/pd_grid/portrayal.py +19 -0
- examples/advanced/pd_grid/pd_grid/server.py +21 -0
- examples/advanced/pd_grid/readme.md +42 -0
- examples/advanced/pd_grid/requirements.txt +3 -0
- examples/advanced/pd_grid/run.py +3 -0
- examples/advanced/sugarscape_g1mt/Readme.md +87 -0
- examples/advanced/sugarscape_g1mt/app.py +61 -0
- examples/advanced/sugarscape_g1mt/requirements.txt +6 -0
- examples/advanced/sugarscape_g1mt/run.py +105 -0
- examples/advanced/sugarscape_g1mt/sugarscape_g1mt/__init__.py +0 -0
- examples/advanced/sugarscape_g1mt/sugarscape_g1mt/model.py +180 -0
- examples/advanced/sugarscape_g1mt/sugarscape_g1mt/resource_agents.py +26 -0
- examples/advanced/sugarscape_g1mt/sugarscape_g1mt/server.py +61 -0
- examples/advanced/sugarscape_g1mt/sugarscape_g1mt/sugar-map.txt +50 -0
- examples/advanced/sugarscape_g1mt/sugarscape_g1mt/trader_agents.py +321 -0
- examples/advanced/sugarscape_g1mt/tests.py +72 -0
- examples/advanced/wolf_sheep/Readme.md +57 -0
- examples/advanced/wolf_sheep/__init__.py +0 -0
- examples/advanced/wolf_sheep/requirements.txt +1 -0
- examples/advanced/wolf_sheep/run.py +3 -0
- examples/advanced/wolf_sheep/wolf_sheep/__init__.py +0 -0
- examples/advanced/wolf_sheep/wolf_sheep/agents.py +102 -0
- examples/advanced/wolf_sheep/wolf_sheep/model.py +136 -0
- examples/advanced/wolf_sheep/wolf_sheep/resources/sheep.png +0 -0
- examples/advanced/wolf_sheep/wolf_sheep/resources/wolf.png +0 -0
- examples/advanced/wolf_sheep/wolf_sheep/server.py +78 -0
- examples/basic/__init__.py +13 -0
- examples/basic/boid_flockers/Readme.md +43 -0
- examples/basic/boid_flockers/agents.py +71 -0
- examples/basic/boid_flockers/app.py +59 -0
- examples/basic/boid_flockers/model.py +70 -0
- examples/basic/boltzmann_wealth_model/Readme.md +60 -0
- examples/basic/boltzmann_wealth_model/agents.py +31 -0
- examples/basic/boltzmann_wealth_model/app.py +66 -0
- examples/basic/boltzmann_wealth_model/model.py +44 -0
- examples/basic/boltzmann_wealth_model/st_app.py +115 -0
- examples/basic/conways_game_of_life/Readme.md +35 -0
- examples/basic/conways_game_of_life/agents.py +47 -0
- examples/basic/conways_game_of_life/model.py +32 -0
- examples/basic/conways_game_of_life/portrayal.py +18 -0
- examples/basic/conways_game_of_life/requirements.txt +1 -0
- examples/basic/conways_game_of_life/server.py +11 -0
- examples/basic/conways_game_of_life/st_app.py +71 -0
- examples/basic/schelling/README.md +47 -0
- examples/basic/schelling/agents.py +26 -0
- examples/basic/schelling/analysis.ipynb +205 -0
- examples/basic/schelling/app.py +43 -0
- examples/basic/schelling/model.py +60 -0
- examples/basic/virus_on_network/README.md +61 -0
- examples/basic/virus_on_network/agents.py +69 -0
- examples/basic/virus_on_network/app.py +133 -0
- examples/basic/virus_on_network/model.py +99 -0
- mesa/__init__.py +4 -1
- mesa/agent.py +24 -43
- mesa/batchrunner.py +7 -0
- mesa/examples.py +3 -0
- mesa/experimental/__init__.py +8 -2
- mesa/experimental/cell_space/__init__.py +7 -1
- mesa/experimental/cell_space/cell.py +35 -6
- mesa/experimental/cell_space/cell_agent.py +114 -23
- mesa/experimental/cell_space/discrete_space.py +70 -3
- mesa/experimental/cell_space/grid.py +13 -0
- mesa/experimental/cell_space/network.py +3 -0
- mesa/experimental/devs/examples/wolf_sheep.py +2 -1
- mesa/model.py +71 -21
- mesa/time.py +7 -5
- mesa/visualization/components/matplotlib.py +184 -90
- mesa/visualization/solara_viz.py +25 -61
- {mesa-3.0.0a5.dist-info → mesa-3.0.0b1.dist-info}/METADATA +55 -13
- mesa-3.0.0b1.dist-info/RECORD +114 -0
- mesa-3.0.0b1.dist-info/licenses/LICENSE +202 -0
- mesa-3.0.0a5.dist-info/licenses/LICENSE → mesa-3.0.0b1.dist-info/licenses/NOTICE +2 -2
- mesa-3.0.0a5.dist-info/RECORD +0 -44
- {mesa-3.0.0a5.dist-info → mesa-3.0.0b1.dist-info}/WHEEL +0 -0
- {mesa-3.0.0a5.dist-info → mesa-3.0.0b1.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
from mesa import Agent
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class State(Enum):
|
|
7
|
+
SUSCEPTIBLE = 0
|
|
8
|
+
INFECTED = 1
|
|
9
|
+
RESISTANT = 2
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class VirusAgent(Agent):
|
|
13
|
+
"""Individual Agent definition and its properties/interaction methods."""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
model,
|
|
18
|
+
initial_state,
|
|
19
|
+
virus_spread_chance,
|
|
20
|
+
virus_check_frequency,
|
|
21
|
+
recovery_chance,
|
|
22
|
+
gain_resistance_chance,
|
|
23
|
+
):
|
|
24
|
+
super().__init__(model)
|
|
25
|
+
|
|
26
|
+
self.state = initial_state
|
|
27
|
+
|
|
28
|
+
self.virus_spread_chance = virus_spread_chance
|
|
29
|
+
self.virus_check_frequency = virus_check_frequency
|
|
30
|
+
self.recovery_chance = recovery_chance
|
|
31
|
+
self.gain_resistance_chance = gain_resistance_chance
|
|
32
|
+
|
|
33
|
+
def try_to_infect_neighbors(self):
|
|
34
|
+
neighbors_nodes = self.model.grid.get_neighborhood(
|
|
35
|
+
self.pos, include_center=False
|
|
36
|
+
)
|
|
37
|
+
susceptible_neighbors = [
|
|
38
|
+
agent
|
|
39
|
+
for agent in self.model.grid.get_cell_list_contents(neighbors_nodes)
|
|
40
|
+
if agent.state is State.SUSCEPTIBLE
|
|
41
|
+
]
|
|
42
|
+
for a in susceptible_neighbors:
|
|
43
|
+
if self.random.random() < self.virus_spread_chance:
|
|
44
|
+
a.state = State.INFECTED
|
|
45
|
+
|
|
46
|
+
def try_gain_resistance(self):
|
|
47
|
+
if self.random.random() < self.gain_resistance_chance:
|
|
48
|
+
self.state = State.RESISTANT
|
|
49
|
+
|
|
50
|
+
def try_remove_infection(self):
|
|
51
|
+
# Try to remove
|
|
52
|
+
if self.random.random() < self.recovery_chance:
|
|
53
|
+
# Success
|
|
54
|
+
self.state = State.SUSCEPTIBLE
|
|
55
|
+
self.try_gain_resistance()
|
|
56
|
+
else:
|
|
57
|
+
# Failed
|
|
58
|
+
self.state = State.INFECTED
|
|
59
|
+
|
|
60
|
+
def try_check_situation(self):
|
|
61
|
+
if (self.random.random() < self.virus_check_frequency) and (
|
|
62
|
+
self.state is State.INFECTED
|
|
63
|
+
):
|
|
64
|
+
self.try_remove_infection()
|
|
65
|
+
|
|
66
|
+
def step(self):
|
|
67
|
+
if self.state is State.INFECTED:
|
|
68
|
+
self.try_to_infect_neighbors()
|
|
69
|
+
self.try_check_situation()
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import math
|
|
2
|
+
|
|
3
|
+
import solara
|
|
4
|
+
from matplotlib.figure import Figure
|
|
5
|
+
from matplotlib.ticker import MaxNLocator
|
|
6
|
+
|
|
7
|
+
from mesa.visualization import Slider, SolaraViz, make_space_matplotlib
|
|
8
|
+
|
|
9
|
+
from .model import State, VirusOnNetwork, number_infected
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def agent_portrayal(graph):
|
|
13
|
+
def get_agent(node):
|
|
14
|
+
return graph.nodes[node]["agent"][0]
|
|
15
|
+
|
|
16
|
+
edge_width = []
|
|
17
|
+
edge_color = []
|
|
18
|
+
for u, v in graph.edges():
|
|
19
|
+
agent1 = get_agent(u)
|
|
20
|
+
agent2 = get_agent(v)
|
|
21
|
+
w = 2
|
|
22
|
+
ec = "#e8e8e8"
|
|
23
|
+
if State.RESISTANT in (agent1.state, agent2.state):
|
|
24
|
+
w = 3
|
|
25
|
+
ec = "black"
|
|
26
|
+
edge_width.append(w)
|
|
27
|
+
edge_color.append(ec)
|
|
28
|
+
node_color_dict = {
|
|
29
|
+
State.INFECTED: "tab:red",
|
|
30
|
+
State.SUSCEPTIBLE: "tab:green",
|
|
31
|
+
State.RESISTANT: "tab:gray",
|
|
32
|
+
}
|
|
33
|
+
node_color = [node_color_dict[get_agent(node).state] for node in graph.nodes()]
|
|
34
|
+
return {
|
|
35
|
+
"width": edge_width,
|
|
36
|
+
"edge_color": edge_color,
|
|
37
|
+
"node_color": node_color,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_resistant_susceptible_ratio(model):
|
|
42
|
+
ratio = model.resistant_susceptible_ratio()
|
|
43
|
+
ratio_text = r"$\infty$" if ratio is math.inf else f"{ratio:.2f}"
|
|
44
|
+
infected_text = str(number_infected(model))
|
|
45
|
+
|
|
46
|
+
return f"Resistant/Susceptible Ratio: {ratio_text}<br>Infected Remaining: {infected_text}"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def make_plot(model):
|
|
50
|
+
# This is for the case when we want to plot multiple measures in 1 figure.
|
|
51
|
+
fig = Figure()
|
|
52
|
+
ax = fig.subplots()
|
|
53
|
+
measures = ["Infected", "Susceptible", "Resistant"]
|
|
54
|
+
colors = ["tab:red", "tab:green", "tab:gray"]
|
|
55
|
+
for i, m in enumerate(measures):
|
|
56
|
+
color = colors[i]
|
|
57
|
+
df = model.datacollector.get_model_vars_dataframe()
|
|
58
|
+
ax.plot(df.loc[:, m], label=m, color=color)
|
|
59
|
+
fig.legend()
|
|
60
|
+
# Set integer x axis
|
|
61
|
+
ax.xaxis.set_major_locator(MaxNLocator(integer=True))
|
|
62
|
+
ax.set_xlabel("Step")
|
|
63
|
+
ax.set_ylabel("Number of Agents")
|
|
64
|
+
return solara.FigureMatplotlib(fig)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
model_params = {
|
|
68
|
+
"num_nodes": Slider(
|
|
69
|
+
label="Number of agents",
|
|
70
|
+
value=10,
|
|
71
|
+
min=10,
|
|
72
|
+
max=100,
|
|
73
|
+
step=1,
|
|
74
|
+
),
|
|
75
|
+
"avg_node_degree": Slider(
|
|
76
|
+
label="Avg Node Degree",
|
|
77
|
+
value=3,
|
|
78
|
+
min=3,
|
|
79
|
+
max=8,
|
|
80
|
+
step=1,
|
|
81
|
+
),
|
|
82
|
+
"initial_outbreak_size": Slider(
|
|
83
|
+
label="Initial Outbreak Size",
|
|
84
|
+
value=1,
|
|
85
|
+
min=1,
|
|
86
|
+
max=10,
|
|
87
|
+
step=1,
|
|
88
|
+
),
|
|
89
|
+
"virus_spread_chance": Slider(
|
|
90
|
+
label="Virus Spread Chance",
|
|
91
|
+
value=0.4,
|
|
92
|
+
min=0.0,
|
|
93
|
+
max=1.0,
|
|
94
|
+
step=0.1,
|
|
95
|
+
),
|
|
96
|
+
"virus_check_frequency": Slider(
|
|
97
|
+
label="Virus Check Frequency",
|
|
98
|
+
value=0.4,
|
|
99
|
+
min=0.0,
|
|
100
|
+
max=1.0,
|
|
101
|
+
step=0.1,
|
|
102
|
+
),
|
|
103
|
+
"recovery_chance": Slider(
|
|
104
|
+
label="Recovery Chance",
|
|
105
|
+
value=0.3,
|
|
106
|
+
min=0.0,
|
|
107
|
+
max=1.0,
|
|
108
|
+
step=0.1,
|
|
109
|
+
),
|
|
110
|
+
"gain_resistance_chance": Slider(
|
|
111
|
+
label="Gain Resistance Chance",
|
|
112
|
+
value=0.5,
|
|
113
|
+
min=0.0,
|
|
114
|
+
max=1.0,
|
|
115
|
+
step=0.1,
|
|
116
|
+
),
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
SpacePlot = make_space_matplotlib(agent_portrayal)
|
|
120
|
+
|
|
121
|
+
model1 = VirusOnNetwork()
|
|
122
|
+
|
|
123
|
+
page = SolaraViz(
|
|
124
|
+
model1,
|
|
125
|
+
[
|
|
126
|
+
SpacePlot,
|
|
127
|
+
make_plot,
|
|
128
|
+
# get_resistant_susceptible_ratio, # TODO: Fix and uncomment
|
|
129
|
+
],
|
|
130
|
+
model_params=model_params,
|
|
131
|
+
name="Virus Model",
|
|
132
|
+
)
|
|
133
|
+
page # noqa
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import math
|
|
2
|
+
|
|
3
|
+
import networkx as nx
|
|
4
|
+
|
|
5
|
+
import mesa
|
|
6
|
+
from mesa import Model
|
|
7
|
+
|
|
8
|
+
from .agents import State, VirusAgent
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def number_state(model, state):
|
|
12
|
+
return sum(1 for a in model.grid.get_all_cell_contents() if a.state is state)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def number_infected(model):
|
|
16
|
+
return number_state(model, State.INFECTED)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def number_susceptible(model):
|
|
20
|
+
return number_state(model, State.SUSCEPTIBLE)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def number_resistant(model):
|
|
24
|
+
return number_state(model, State.RESISTANT)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class VirusOnNetwork(Model):
|
|
28
|
+
"""A virus model with some number of agents."""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
num_nodes=10,
|
|
33
|
+
avg_node_degree=3,
|
|
34
|
+
initial_outbreak_size=1,
|
|
35
|
+
virus_spread_chance=0.4,
|
|
36
|
+
virus_check_frequency=0.4,
|
|
37
|
+
recovery_chance=0.3,
|
|
38
|
+
gain_resistance_chance=0.5,
|
|
39
|
+
):
|
|
40
|
+
super().__init__()
|
|
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
|
+
}
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Create agents
|
|
63
|
+
for node in self.G.nodes():
|
|
64
|
+
a = VirusAgent(
|
|
65
|
+
self,
|
|
66
|
+
State.SUSCEPTIBLE,
|
|
67
|
+
self.virus_spread_chance,
|
|
68
|
+
self.virus_check_frequency,
|
|
69
|
+
self.recovery_chance,
|
|
70
|
+
self.gain_resistance_chance,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Add the agent to the node
|
|
74
|
+
self.grid.place_agent(a, node)
|
|
75
|
+
|
|
76
|
+
# Infect some nodes
|
|
77
|
+
infected_nodes = self.random.sample(list(self.G), self.initial_outbreak_size)
|
|
78
|
+
for a in self.grid.get_cell_list_contents(infected_nodes):
|
|
79
|
+
a.state = State.INFECTED
|
|
80
|
+
|
|
81
|
+
self.running = True
|
|
82
|
+
self.datacollector.collect(self)
|
|
83
|
+
|
|
84
|
+
def resistant_susceptible_ratio(self):
|
|
85
|
+
try:
|
|
86
|
+
return number_state(self, State.RESISTANT) / number_state(
|
|
87
|
+
self, State.SUSCEPTIBLE
|
|
88
|
+
)
|
|
89
|
+
except ZeroDivisionError:
|
|
90
|
+
return math.inf
|
|
91
|
+
|
|
92
|
+
def step(self):
|
|
93
|
+
self.agents.shuffle_do("step")
|
|
94
|
+
# collect data
|
|
95
|
+
self.datacollector.collect(self)
|
|
96
|
+
|
|
97
|
+
def run_model(self, n):
|
|
98
|
+
for _ in range(n):
|
|
99
|
+
self.step()
|
mesa/__init__.py
CHANGED
|
@@ -5,6 +5,8 @@ Core Objects: Model, and Agent.
|
|
|
5
5
|
|
|
6
6
|
import datetime
|
|
7
7
|
|
|
8
|
+
import mesa.examples as examples
|
|
9
|
+
import mesa.experimental as experimental
|
|
8
10
|
import mesa.space as space
|
|
9
11
|
import mesa.time as time
|
|
10
12
|
from mesa.agent import Agent
|
|
@@ -20,10 +22,11 @@ __all__ = [
|
|
|
20
22
|
"DataCollector",
|
|
21
23
|
"batch_run",
|
|
22
24
|
"experimental",
|
|
25
|
+
"examples",
|
|
23
26
|
]
|
|
24
27
|
|
|
25
28
|
__title__ = "mesa"
|
|
26
|
-
__version__ = "3.0.
|
|
29
|
+
__version__ = "3.0.0b1"
|
|
27
30
|
__license__ = "Apache 2.0"
|
|
28
31
|
_this_year = datetime.datetime.now(tz=datetime.timezone.utc).date().year
|
|
29
32
|
__copyright__ = f"Copyright {_this_year} Project Mesa Team"
|
mesa/agent.py
CHANGED
|
@@ -46,38 +46,24 @@ class Agent:
|
|
|
46
46
|
# so, unique_id is unique relative to a model, and counting starts from 1
|
|
47
47
|
_ids = defaultdict(functools.partial(itertools.count, 1))
|
|
48
48
|
|
|
49
|
-
def __init__(self, *args, **kwargs) -> None:
|
|
49
|
+
def __init__(self, model: Model, *args, **kwargs) -> None:
|
|
50
50
|
"""Create a new agent.
|
|
51
51
|
|
|
52
52
|
Args:
|
|
53
53
|
model (Model): The model instance in which the agent exists.
|
|
54
|
-
args:
|
|
55
|
-
kwargs:
|
|
54
|
+
args: passed on to super
|
|
55
|
+
kwargs: passed on to super
|
|
56
|
+
|
|
57
|
+
Notes:
|
|
58
|
+
to make proper use of python's super, in each class remove the arguments and
|
|
59
|
+
keyword arguments you need and pass on the rest to super
|
|
60
|
+
|
|
56
61
|
"""
|
|
57
|
-
|
|
58
|
-
match args:
|
|
59
|
-
# Case 1: Only the model is provided. The new correct behavior.
|
|
60
|
-
case [model]:
|
|
61
|
-
self.model = model
|
|
62
|
-
self.unique_id = next(self._ids[model])
|
|
63
|
-
# Case 2: Both unique_id and model are provided, deprecated
|
|
64
|
-
case [_, model]:
|
|
65
|
-
warnings.warn(
|
|
66
|
-
"unique ids are assigned automatically to Agents in Mesa 3. The use of custom unique_id is "
|
|
67
|
-
"deprecated. Only input a model when calling `super()__init__(model)`. The unique_id inputted is not used.",
|
|
68
|
-
DeprecationWarning,
|
|
69
|
-
stacklevel=2,
|
|
70
|
-
)
|
|
71
|
-
self.model = model
|
|
72
|
-
self.unique_id = next(self._ids[model])
|
|
73
|
-
# Case 3: Anything else, raise an error
|
|
74
|
-
case _:
|
|
75
|
-
raise ValueError(
|
|
76
|
-
"Invalid arguments provided to initialize the Agent. Only input a model: `super()__init__(model)`."
|
|
77
|
-
)
|
|
62
|
+
super().__init__(*args, **kwargs)
|
|
78
63
|
|
|
64
|
+
self.model: Model = model
|
|
65
|
+
self.unique_id: int = next(self._ids[model])
|
|
79
66
|
self.pos: Position | None = None
|
|
80
|
-
|
|
81
67
|
self.model.register_agent(self)
|
|
82
68
|
|
|
83
69
|
def remove(self) -> None:
|
|
@@ -113,14 +99,18 @@ class AgentSet(MutableSet, Sequence):
|
|
|
113
99
|
which means that agents not referenced elsewhere in the program may be automatically removed from the AgentSet.
|
|
114
100
|
"""
|
|
115
101
|
|
|
116
|
-
def __init__(self, agents: Iterable[Agent],
|
|
102
|
+
def __init__(self, agents: Iterable[Agent], random: Random | None = None):
|
|
117
103
|
"""Initializes the AgentSet with a collection of agents and a reference to the model.
|
|
118
104
|
|
|
119
105
|
Args:
|
|
120
106
|
agents (Iterable[Agent]): An iterable of Agent objects to be included in the set.
|
|
121
|
-
|
|
107
|
+
random (Random): the random number generator
|
|
122
108
|
"""
|
|
123
|
-
|
|
109
|
+
if random is None:
|
|
110
|
+
random = (
|
|
111
|
+
Random()
|
|
112
|
+
) # FIXME see issue 1981, how to get the central rng from model
|
|
113
|
+
self.random = random
|
|
124
114
|
self._agents = weakref.WeakKeyDictionary({agent: None for agent in agents})
|
|
125
115
|
|
|
126
116
|
def __len__(self) -> int:
|
|
@@ -191,7 +181,7 @@ class AgentSet(MutableSet, Sequence):
|
|
|
191
181
|
|
|
192
182
|
agents = agent_generator(filter_func, agent_type, at_most)
|
|
193
183
|
|
|
194
|
-
return AgentSet(agents, self.
|
|
184
|
+
return AgentSet(agents, self.random) if not inplace else self._update(agents)
|
|
195
185
|
|
|
196
186
|
def shuffle(self, inplace: bool = False) -> AgentSet:
|
|
197
187
|
"""Randomly shuffle the order of agents in the AgentSet.
|
|
@@ -214,7 +204,7 @@ class AgentSet(MutableSet, Sequence):
|
|
|
214
204
|
return self
|
|
215
205
|
else:
|
|
216
206
|
return AgentSet(
|
|
217
|
-
(agent for ref in weakrefs if (agent := ref()) is not None), self.
|
|
207
|
+
(agent for ref in weakrefs if (agent := ref()) is not None), self.random
|
|
218
208
|
)
|
|
219
209
|
|
|
220
210
|
def sort(
|
|
@@ -239,7 +229,7 @@ class AgentSet(MutableSet, Sequence):
|
|
|
239
229
|
sorted_agents = sorted(self._agents.keys(), key=key, reverse=not ascending)
|
|
240
230
|
|
|
241
231
|
return (
|
|
242
|
-
AgentSet(sorted_agents, self.
|
|
232
|
+
AgentSet(sorted_agents, self.random)
|
|
243
233
|
if not inplace
|
|
244
234
|
else self._update(sorted_agents)
|
|
245
235
|
)
|
|
@@ -491,7 +481,7 @@ class AgentSet(MutableSet, Sequence):
|
|
|
491
481
|
Returns:
|
|
492
482
|
dict: A dictionary representing the state of the AgentSet.
|
|
493
483
|
"""
|
|
494
|
-
return {"agents": list(self._agents.keys()), "
|
|
484
|
+
return {"agents": list(self._agents.keys()), "random": self.random}
|
|
495
485
|
|
|
496
486
|
def __setstate__(self, state):
|
|
497
487
|
"""Set the state of the AgentSet during deserialization.
|
|
@@ -499,18 +489,9 @@ class AgentSet(MutableSet, Sequence):
|
|
|
499
489
|
Args:
|
|
500
490
|
state (dict): A dictionary representing the state to restore.
|
|
501
491
|
"""
|
|
502
|
-
self.
|
|
492
|
+
self.random = state["random"]
|
|
503
493
|
self._update(state["agents"])
|
|
504
494
|
|
|
505
|
-
@property
|
|
506
|
-
def random(self) -> Random:
|
|
507
|
-
"""Provide access to the model's random number generator.
|
|
508
|
-
|
|
509
|
-
Returns:
|
|
510
|
-
Random: The random number generator associated with the model.
|
|
511
|
-
"""
|
|
512
|
-
return self.model.random
|
|
513
|
-
|
|
514
495
|
def groupby(self, by: Callable | str, result_type: str = "agentset") -> GroupBy:
|
|
515
496
|
"""Group agents by the specified attribute or return from the callable.
|
|
516
497
|
|
|
@@ -543,7 +524,7 @@ class AgentSet(MutableSet, Sequence):
|
|
|
543
524
|
|
|
544
525
|
if result_type == "agentset":
|
|
545
526
|
return GroupBy(
|
|
546
|
-
{k: AgentSet(v,
|
|
527
|
+
{k: AgentSet(v, random=self.random) for k, v in groups.items()}
|
|
547
528
|
)
|
|
548
529
|
else:
|
|
549
530
|
return GroupBy(groups)
|
mesa/batchrunner.py
CHANGED
|
@@ -38,6 +38,9 @@ def batch_run(
|
|
|
38
38
|
Returns:
|
|
39
39
|
List[Dict[str, Any]]
|
|
40
40
|
|
|
41
|
+
Notes:
|
|
42
|
+
batch_run assumes the model has a `datacollector` attribute that has a DataCollector object initialized.
|
|
43
|
+
|
|
41
44
|
"""
|
|
42
45
|
runs_list = []
|
|
43
46
|
run_id = 0
|
|
@@ -173,6 +176,10 @@ def _collect_data(
|
|
|
173
176
|
step: int,
|
|
174
177
|
) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
|
175
178
|
"""Collect model and agent data from a model using mesas datacollector."""
|
|
179
|
+
if not hasattr(model, "datacollector"):
|
|
180
|
+
raise AttributeError(
|
|
181
|
+
"The model does not have a datacollector attribute. Please add a DataCollector to your model."
|
|
182
|
+
)
|
|
176
183
|
dc = model.datacollector
|
|
177
184
|
|
|
178
185
|
model_data = {param: values[step] for param, values in dc.model_vars.items()}
|
mesa/examples.py
ADDED
mesa/experimental/__init__.py
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
from mesa.experimental import cell_space
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
try:
|
|
6
|
+
from .solara_viz import JupyterViz, Slider, SolaraViz, make_text
|
|
6
7
|
|
|
7
|
-
__all__ = ["cell_space", "JupyterViz", "
|
|
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"]
|
|
@@ -6,7 +6,11 @@ expressive that the default grids available in `mesa.space`.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from mesa.experimental.cell_space.cell import Cell
|
|
9
|
-
from mesa.experimental.cell_space.cell_agent import
|
|
9
|
+
from mesa.experimental.cell_space.cell_agent import (
|
|
10
|
+
CellAgent,
|
|
11
|
+
FixedAgent,
|
|
12
|
+
Grid2DMovingAgent,
|
|
13
|
+
)
|
|
10
14
|
from mesa.experimental.cell_space.cell_collection import CellCollection
|
|
11
15
|
from mesa.experimental.cell_space.discrete_space import DiscreteSpace
|
|
12
16
|
from mesa.experimental.cell_space.grid import (
|
|
@@ -22,6 +26,8 @@ __all__ = [
|
|
|
22
26
|
"CellCollection",
|
|
23
27
|
"Cell",
|
|
24
28
|
"CellAgent",
|
|
29
|
+
"Grid2DMovingAgent",
|
|
30
|
+
"FixedAgent",
|
|
25
31
|
"DiscreteSpace",
|
|
26
32
|
"Grid",
|
|
27
33
|
"HexGrid",
|
|
@@ -2,15 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from collections.abc import Callable
|
|
5
6
|
from functools import cache, cached_property
|
|
6
7
|
from random import Random
|
|
7
|
-
from typing import TYPE_CHECKING
|
|
8
|
+
from typing import TYPE_CHECKING, Any
|
|
8
9
|
|
|
10
|
+
from mesa.experimental.cell_space.cell_agent import CellAgent
|
|
9
11
|
from mesa.experimental.cell_space.cell_collection import CellCollection
|
|
12
|
+
from mesa.space import PropertyLayer
|
|
10
13
|
|
|
11
14
|
if TYPE_CHECKING:
|
|
12
15
|
from mesa.agent import Agent
|
|
13
|
-
from mesa.experimental.cell_space.cell_agent import CellAgent
|
|
14
16
|
|
|
15
17
|
Coordinate = tuple[int, ...]
|
|
16
18
|
|
|
@@ -34,6 +36,7 @@ class Cell:
|
|
|
34
36
|
"capacity",
|
|
35
37
|
"properties",
|
|
36
38
|
"random",
|
|
39
|
+
"_mesa_property_layers",
|
|
37
40
|
"__dict__",
|
|
38
41
|
]
|
|
39
42
|
|
|
@@ -66,9 +69,10 @@ class Cell:
|
|
|
66
69
|
self.agents: list[
|
|
67
70
|
Agent
|
|
68
71
|
] = [] # TODO:: change to AgentSet or weakrefs? (neither is very performant, )
|
|
69
|
-
self.capacity: int = capacity
|
|
72
|
+
self.capacity: int | None = capacity
|
|
70
73
|
self.properties: dict[Coordinate, object] = {}
|
|
71
74
|
self.random = random
|
|
75
|
+
self._mesa_property_layers: dict[str, PropertyLayer] = {}
|
|
72
76
|
|
|
73
77
|
def connect(self, other: Cell, key: Coordinate | None = None) -> None:
|
|
74
78
|
"""Connects this cell to another cell.
|
|
@@ -117,7 +121,6 @@ class Cell:
|
|
|
117
121
|
|
|
118
122
|
"""
|
|
119
123
|
self.agents.remove(agent)
|
|
120
|
-
agent.cell = None
|
|
121
124
|
|
|
122
125
|
@property
|
|
123
126
|
def is_empty(self) -> bool:
|
|
@@ -133,7 +136,7 @@ class Cell:
|
|
|
133
136
|
return f"Cell({self.coordinate}, {self.agents})"
|
|
134
137
|
|
|
135
138
|
@cached_property
|
|
136
|
-
def neighborhood(self) -> CellCollection:
|
|
139
|
+
def neighborhood(self) -> CellCollection[Cell]:
|
|
137
140
|
"""Returns the direct neighborhood of the cell.
|
|
138
141
|
|
|
139
142
|
This is equivalent to cell.get_neighborhood(radius=1)
|
|
@@ -145,7 +148,7 @@ class Cell:
|
|
|
145
148
|
@cache # noqa: B019
|
|
146
149
|
def get_neighborhood(
|
|
147
150
|
self, radius: int = 1, include_center: bool = False
|
|
148
|
-
) -> CellCollection:
|
|
151
|
+
) -> CellCollection[Cell]:
|
|
149
152
|
"""Returns a list of all neighboring cells for the given radius.
|
|
150
153
|
|
|
151
154
|
For getting the direct neighborhood (i.e., radius=1) you can also use
|
|
@@ -191,3 +194,29 @@ class Cell:
|
|
|
191
194
|
if not include_center:
|
|
192
195
|
neighborhood.pop(self, None)
|
|
193
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
|