Mesa 3.0.0b0__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 +14 -19
- mesa/examples.py +3 -0
- mesa/experimental/__init__.py +8 -2
- mesa/experimental/cell_space/cell.py +9 -0
- mesa/experimental/cell_space/discrete_space.py +7 -1
- mesa/experimental/cell_space/grid.py +13 -0
- mesa/experimental/cell_space/network.py +3 -0
- mesa/model.py +63 -12
- mesa/time.py +5 -3
- mesa/visualization/components/matplotlib.py +9 -4
- mesa/visualization/solara_viz.py +13 -58
- {mesa-3.0.0b0.dist-info → mesa-3.0.0b1.dist-info}/METADATA +1 -1
- mesa-3.0.0b1.dist-info/RECORD +114 -0
- mesa-3.0.0b0.dist-info/RECORD +0 -45
- {mesa-3.0.0b0.dist-info → mesa-3.0.0b1.dist-info}/WHEEL +0 -0
- {mesa-3.0.0b0.dist-info → mesa-3.0.0b1.dist-info}/entry_points.txt +0 -0
- {mesa-3.0.0b0.dist-info → mesa-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
- {mesa-3.0.0b0.dist-info → mesa-3.0.0b1.dist-info}/licenses/NOTICE +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
|
@@ -62,9 +62,9 @@ class Agent:
|
|
|
62
62
|
super().__init__(*args, **kwargs)
|
|
63
63
|
|
|
64
64
|
self.model: Model = model
|
|
65
|
-
self.model.register_agent(self)
|
|
66
65
|
self.unique_id: int = next(self._ids[model])
|
|
67
66
|
self.pos: Position | None = None
|
|
67
|
+
self.model.register_agent(self)
|
|
68
68
|
|
|
69
69
|
def remove(self) -> None:
|
|
70
70
|
"""Remove and delete the agent from the model."""
|
|
@@ -99,14 +99,18 @@ class AgentSet(MutableSet, Sequence):
|
|
|
99
99
|
which means that agents not referenced elsewhere in the program may be automatically removed from the AgentSet.
|
|
100
100
|
"""
|
|
101
101
|
|
|
102
|
-
def __init__(self, agents: Iterable[Agent],
|
|
102
|
+
def __init__(self, agents: Iterable[Agent], random: Random | None = None):
|
|
103
103
|
"""Initializes the AgentSet with a collection of agents and a reference to the model.
|
|
104
104
|
|
|
105
105
|
Args:
|
|
106
106
|
agents (Iterable[Agent]): An iterable of Agent objects to be included in the set.
|
|
107
|
-
|
|
107
|
+
random (Random): the random number generator
|
|
108
108
|
"""
|
|
109
|
-
|
|
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
|
|
110
114
|
self._agents = weakref.WeakKeyDictionary({agent: None for agent in agents})
|
|
111
115
|
|
|
112
116
|
def __len__(self) -> int:
|
|
@@ -177,7 +181,7 @@ class AgentSet(MutableSet, Sequence):
|
|
|
177
181
|
|
|
178
182
|
agents = agent_generator(filter_func, agent_type, at_most)
|
|
179
183
|
|
|
180
|
-
return AgentSet(agents, self.
|
|
184
|
+
return AgentSet(agents, self.random) if not inplace else self._update(agents)
|
|
181
185
|
|
|
182
186
|
def shuffle(self, inplace: bool = False) -> AgentSet:
|
|
183
187
|
"""Randomly shuffle the order of agents in the AgentSet.
|
|
@@ -200,7 +204,7 @@ class AgentSet(MutableSet, Sequence):
|
|
|
200
204
|
return self
|
|
201
205
|
else:
|
|
202
206
|
return AgentSet(
|
|
203
|
-
(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
|
|
204
208
|
)
|
|
205
209
|
|
|
206
210
|
def sort(
|
|
@@ -225,7 +229,7 @@ class AgentSet(MutableSet, Sequence):
|
|
|
225
229
|
sorted_agents = sorted(self._agents.keys(), key=key, reverse=not ascending)
|
|
226
230
|
|
|
227
231
|
return (
|
|
228
|
-
AgentSet(sorted_agents, self.
|
|
232
|
+
AgentSet(sorted_agents, self.random)
|
|
229
233
|
if not inplace
|
|
230
234
|
else self._update(sorted_agents)
|
|
231
235
|
)
|
|
@@ -477,7 +481,7 @@ class AgentSet(MutableSet, Sequence):
|
|
|
477
481
|
Returns:
|
|
478
482
|
dict: A dictionary representing the state of the AgentSet.
|
|
479
483
|
"""
|
|
480
|
-
return {"agents": list(self._agents.keys()), "
|
|
484
|
+
return {"agents": list(self._agents.keys()), "random": self.random}
|
|
481
485
|
|
|
482
486
|
def __setstate__(self, state):
|
|
483
487
|
"""Set the state of the AgentSet during deserialization.
|
|
@@ -485,18 +489,9 @@ class AgentSet(MutableSet, Sequence):
|
|
|
485
489
|
Args:
|
|
486
490
|
state (dict): A dictionary representing the state to restore.
|
|
487
491
|
"""
|
|
488
|
-
self.
|
|
492
|
+
self.random = state["random"]
|
|
489
493
|
self._update(state["agents"])
|
|
490
494
|
|
|
491
|
-
@property
|
|
492
|
-
def random(self) -> Random:
|
|
493
|
-
"""Provide access to the model's random number generator.
|
|
494
|
-
|
|
495
|
-
Returns:
|
|
496
|
-
Random: The random number generator associated with the model.
|
|
497
|
-
"""
|
|
498
|
-
return self.model.random
|
|
499
|
-
|
|
500
495
|
def groupby(self, by: Callable | str, result_type: str = "agentset") -> GroupBy:
|
|
501
496
|
"""Group agents by the specified attribute or return from the callable.
|
|
502
497
|
|
|
@@ -529,7 +524,7 @@ class AgentSet(MutableSet, Sequence):
|
|
|
529
524
|
|
|
530
525
|
if result_type == "agentset":
|
|
531
526
|
return GroupBy(
|
|
532
|
-
{k: AgentSet(v,
|
|
527
|
+
{k: AgentSet(v, random=self.random) for k, v in groups.items()}
|
|
533
528
|
)
|
|
534
529
|
else:
|
|
535
530
|
return GroupBy(groups)
|
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"]
|
|
@@ -211,3 +211,12 @@ class Cell:
|
|
|
211
211
|
self._mesa_property_layers[property_name].modify_cell(
|
|
212
212
|
self.coordinate, operation, value
|
|
213
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
|
|
@@ -22,7 +22,7 @@ class DiscreteSpace(Generic[T]):
|
|
|
22
22
|
all_cells (CellCollection): The cells composing the discrete space
|
|
23
23
|
random (Random): The random number generator
|
|
24
24
|
cell_klass (Type) : the type of cell class
|
|
25
|
-
empties (CellCollection) :
|
|
25
|
+
empties (CellCollection) : collection of all cells that are empty
|
|
26
26
|
property_layers (dict[str, PropertyLayer]): the property layers of the discrete space
|
|
27
27
|
"""
|
|
28
28
|
|
|
@@ -55,6 +55,7 @@ class DiscreteSpace(Generic[T]):
|
|
|
55
55
|
def cutoff_empties(self): # noqa
|
|
56
56
|
return 7.953 * len(self._cells) ** 0.384
|
|
57
57
|
|
|
58
|
+
def _connect_cells(self): ...
|
|
58
59
|
def _connect_single_cell(self, cell: T): ...
|
|
59
60
|
|
|
60
61
|
@cached_property
|
|
@@ -134,3 +135,8 @@ class DiscreteSpace(Generic[T]):
|
|
|
134
135
|
condition: a function that takes a cell and returns a boolean (used to filter cells)
|
|
135
136
|
"""
|
|
136
137
|
self.property_layers[property_name].modify_cells(operation, value, condition)
|
|
138
|
+
|
|
139
|
+
def __setstate__(self, state):
|
|
140
|
+
"""Set the state of the discrete space and rebuild the connections."""
|
|
141
|
+
self.__dict__ = state
|
|
142
|
+
self._connect_cells()
|
|
@@ -22,8 +22,21 @@ class Grid(DiscreteSpace[T], Generic[T]):
|
|
|
22
22
|
random (Random): the random number generator
|
|
23
23
|
_try_random (bool): whether to get empty cell be repeatedly trying random cell
|
|
24
24
|
|
|
25
|
+
Notes:
|
|
26
|
+
width and height are accessible via properties, higher dimensions can be retrieved via dimensions
|
|
27
|
+
|
|
25
28
|
"""
|
|
26
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
|
+
|
|
27
40
|
def __init__(
|
|
28
41
|
self,
|
|
29
42
|
dimensions: Sequence[int],
|
mesa/model.py
CHANGED
|
@@ -8,14 +8,21 @@ Core Objects: Model
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import random
|
|
11
|
+
import sys
|
|
11
12
|
import warnings
|
|
13
|
+
from collections.abc import Sequence
|
|
12
14
|
|
|
13
15
|
# mypy
|
|
14
16
|
from typing import Any
|
|
15
17
|
|
|
18
|
+
import numpy as np
|
|
19
|
+
|
|
16
20
|
from mesa.agent import Agent, AgentSet
|
|
17
21
|
from mesa.datacollection import DataCollector
|
|
18
22
|
|
|
23
|
+
SeedLike = int | np.integer | Sequence[int] | np.random.SeedSequence
|
|
24
|
+
RNGLike = np.random.Generator | np.random.BitGenerator
|
|
25
|
+
|
|
19
26
|
|
|
20
27
|
class Model:
|
|
21
28
|
"""Base class for models in the Mesa ABM library.
|
|
@@ -28,7 +35,8 @@ class Model:
|
|
|
28
35
|
running: A boolean indicating if the model should continue running.
|
|
29
36
|
schedule: An object to manage the order and execution of agent steps.
|
|
30
37
|
steps: the number of times `model.step()` has been called.
|
|
31
|
-
random: a seeded random number generator.
|
|
38
|
+
random: a seeded python.random number generator.
|
|
39
|
+
rng : a seeded numpy.random.Generator
|
|
32
40
|
|
|
33
41
|
Notes:
|
|
34
42
|
Model.agents returns the AgentSet containing all agents registered with the model. Changing
|
|
@@ -37,7 +45,13 @@ class Model:
|
|
|
37
45
|
|
|
38
46
|
"""
|
|
39
47
|
|
|
40
|
-
def __init__(
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
*args: Any,
|
|
51
|
+
seed: float | None = None,
|
|
52
|
+
rng: RNGLike | SeedLike | None = None,
|
|
53
|
+
**kwargs: Any,
|
|
54
|
+
) -> None:
|
|
41
55
|
"""Create a new model.
|
|
42
56
|
|
|
43
57
|
Overload this method with the actual code to initialize the model. Always start with super().__init__()
|
|
@@ -46,25 +60,51 @@ class Model:
|
|
|
46
60
|
Args:
|
|
47
61
|
args: arguments to pass onto super
|
|
48
62
|
seed: the seed for the random number generator
|
|
63
|
+
rng : Pseudorandom number generator state. When `rng` is None, a new `numpy.random.Generator` is created
|
|
64
|
+
using entropy from the operating system. Types other than `numpy.random.Generator` are passed to
|
|
65
|
+
`numpy.random.default_rng` to instantiate a `Generator`.
|
|
49
66
|
kwargs: keyword arguments to pass onto super
|
|
67
|
+
|
|
68
|
+
Notes:
|
|
69
|
+
you have to pass either seed or rng, but not both.
|
|
70
|
+
|
|
50
71
|
"""
|
|
51
72
|
super().__init__(*args, **kwargs)
|
|
52
73
|
self.running = True
|
|
53
74
|
self.steps: int = 0
|
|
54
75
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
76
|
+
if (seed is not None) and (rng is not None):
|
|
77
|
+
raise ValueError("you have to pass either rng or seed, not both")
|
|
78
|
+
elif seed is None:
|
|
79
|
+
self.rng: np.random.Generator = np.random.default_rng(rng)
|
|
80
|
+
self._rng = (
|
|
81
|
+
self.rng.bit_generator.state
|
|
82
|
+
) # this allows for reproducing the rng
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
self.random = random.Random(rng)
|
|
86
|
+
except TypeError:
|
|
87
|
+
seed = int(self.rng.integers(np.iinfo(np.int32).max))
|
|
88
|
+
self.random = random.Random(seed)
|
|
89
|
+
self._seed = seed # this allows for reproducing stdlib.random
|
|
90
|
+
elif rng is None:
|
|
91
|
+
self.random = random.Random(seed)
|
|
92
|
+
self._seed = seed # this allows for reproducing stdlib.random
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
self.rng: np.random.Generator = np.random.default_rng(rng)
|
|
96
|
+
except TypeError:
|
|
97
|
+
rng = self.random.randint(0, sys.maxsize)
|
|
98
|
+
self.rng: np.random.Generator = np.random.default_rng(rng)
|
|
99
|
+
self._rng = self.rng.bit_generator.state
|
|
63
100
|
|
|
64
101
|
# Wrap the user-defined step method
|
|
65
102
|
self._user_step = self.step
|
|
66
103
|
self.step = self._wrapped_step
|
|
67
104
|
|
|
105
|
+
# setup agent registration data structures
|
|
106
|
+
self._setup_agent_registration()
|
|
107
|
+
|
|
68
108
|
def _wrapped_step(self, *args: Any, **kwargs: Any) -> None:
|
|
69
109
|
"""Automatically increments time and steps after calling the user's step method."""
|
|
70
110
|
# Automatically increment time and step counters
|
|
@@ -119,7 +159,9 @@ class Model:
|
|
|
119
159
|
self._agents_by_type: dict[
|
|
120
160
|
type[Agent], AgentSet
|
|
121
161
|
] = {} # a dict with an agentset for each class of agents
|
|
122
|
-
self._all_agents = AgentSet(
|
|
162
|
+
self._all_agents = AgentSet(
|
|
163
|
+
[], random=self.random
|
|
164
|
+
) # an agenset with all agents
|
|
123
165
|
|
|
124
166
|
def register_agent(self, agent):
|
|
125
167
|
"""Register the agent with the model.
|
|
@@ -153,7 +195,7 @@ class Model:
|
|
|
153
195
|
[
|
|
154
196
|
agent,
|
|
155
197
|
],
|
|
156
|
-
self,
|
|
198
|
+
random=self.random,
|
|
157
199
|
)
|
|
158
200
|
|
|
159
201
|
self._all_agents.add(agent)
|
|
@@ -194,6 +236,15 @@ class Model:
|
|
|
194
236
|
self.random.seed(seed)
|
|
195
237
|
self._seed = seed
|
|
196
238
|
|
|
239
|
+
def reset_rng(self, rng: RNGLike | SeedLike | None = None) -> None:
|
|
240
|
+
"""Reset the model random number generator.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
rng: A new seed for the RNG; if None, reset using the current seed
|
|
244
|
+
"""
|
|
245
|
+
self.rng = np.random.default_rng(rng)
|
|
246
|
+
self._rng = self.rng.bit_generator.state
|
|
247
|
+
|
|
197
248
|
def initialize_data_collector(
|
|
198
249
|
self,
|
|
199
250
|
model_reporters=None,
|
mesa/time.py
CHANGED
|
@@ -77,7 +77,7 @@ class BaseScheduler:
|
|
|
77
77
|
if agents is None:
|
|
78
78
|
agents = []
|
|
79
79
|
|
|
80
|
-
self._agents: AgentSet = AgentSet(agents, model)
|
|
80
|
+
self._agents: AgentSet = AgentSet(agents, model.random)
|
|
81
81
|
|
|
82
82
|
self._remove_warning_given = False
|
|
83
83
|
self._agents_key_warning_given = False
|
|
@@ -312,7 +312,9 @@ class RandomActivationByType(BaseScheduler):
|
|
|
312
312
|
try:
|
|
313
313
|
self._agents_by_type[type(agent)].add(agent)
|
|
314
314
|
except KeyError:
|
|
315
|
-
self._agents_by_type[type(agent)] = AgentSet(
|
|
315
|
+
self._agents_by_type[type(agent)] = AgentSet(
|
|
316
|
+
[agent], self.model.random
|
|
317
|
+
)
|
|
316
318
|
|
|
317
319
|
def add(self, agent: Agent) -> None:
|
|
318
320
|
"""Add an Agent object to the schedule.
|
|
@@ -325,7 +327,7 @@ class RandomActivationByType(BaseScheduler):
|
|
|
325
327
|
try:
|
|
326
328
|
self._agents_by_type[type(agent)].add(agent)
|
|
327
329
|
except KeyError:
|
|
328
|
-
self._agents_by_type[type(agent)] = AgentSet([agent], self.model)
|
|
330
|
+
self._agents_by_type[type(agent)] = AgentSet([agent], self.model.random)
|
|
329
331
|
|
|
330
332
|
def remove(self, agent: Agent) -> None:
|
|
331
333
|
"""Remove all instances of a given agent from the schedule.
|