Mesa 3.0.0b0__py3-none-any.whl → 3.0.0b2__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 +37 -27
- mesa/examples/README.md +37 -0
- mesa/examples/__init__.py +21 -0
- mesa/examples/advanced/__init__.py +0 -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 +158 -0
- mesa/examples/advanced/epstein_civil_violence/app.py +72 -0
- mesa/examples/advanced/epstein_civil_violence/model.py +146 -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 +50 -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 +70 -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 +77 -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 +65 -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 +39 -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 +136 -0
- mesa/examples/basic/virus_on_network/model.py +96 -0
- mesa/experimental/__init__.py +8 -2
- mesa/experimental/cell_space/cell.py +9 -0
- mesa/experimental/cell_space/discrete_space.py +13 -1
- mesa/experimental/cell_space/grid.py +13 -0
- mesa/experimental/cell_space/network.py +3 -0
- mesa/experimental/devs/eventlist.py +6 -0
- mesa/model.py +76 -12
- mesa/space.py +70 -5
- mesa/time.py +5 -3
- mesa/visualization/components/altair.py +87 -19
- mesa/visualization/components/matplotlib.py +65 -16
- mesa/visualization/solara_viz.py +13 -58
- {mesa-3.0.0b0.dist-info → mesa-3.0.0b2.dist-info}/METADATA +1 -3
- mesa-3.0.0b2.dist-info/RECORD +93 -0
- mesa/cookiecutter-mesa/cookiecutter.json +0 -8
- mesa/cookiecutter-mesa/hooks/post_gen_project.py +0 -13
- mesa/cookiecutter-mesa/{{cookiecutter.snake}}/README.md +0 -4
- mesa/cookiecutter-mesa/{{cookiecutter.snake}}/app.pytemplate +0 -27
- mesa/cookiecutter-mesa/{{cookiecutter.snake}}/setup.pytemplate +0 -11
- mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/__init__.py +0 -1
- mesa/cookiecutter-mesa/{{cookiecutter.snake}}/{{cookiecutter.snake}}/model.pytemplate +0 -60
- mesa/main.py +0 -65
- mesa-3.0.0b0.dist-info/RECORD +0 -45
- {mesa-3.0.0b0.dist-info → mesa-3.0.0b2.dist-info}/WHEEL +0 -0
- {mesa-3.0.0b0.dist-info → mesa-3.0.0b2.dist-info}/entry_points.txt +0 -0
- {mesa-3.0.0b0.dist-info → mesa-3.0.0b2.dist-info}/licenses/LICENSE +0 -0
- {mesa-3.0.0b0.dist-info → mesa-3.0.0b2.dist-info}/licenses/NOTICE +0 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cells": [
|
|
3
|
+
{
|
|
4
|
+
"cell_type": "markdown",
|
|
5
|
+
"metadata": {},
|
|
6
|
+
"source": [
|
|
7
|
+
"# Schelling Segregation Model\n",
|
|
8
|
+
"\n",
|
|
9
|
+
"## Background\n",
|
|
10
|
+
"\n",
|
|
11
|
+
"The Schelling (1971) segregation model is a classic of agent-based modeling, demonstrating how agents following simple rules lead to the emergence of qualitatively different macro-level outcomes. Agents are randomly placed on a grid. There are two types of agents, one constituting the majority and the other the minority. All agents want a certain number (generally, 3) of their 8 surrounding neighbors to be of the same type in order for them to be happy. Unhappy agents will move to a random available grid space. While individual agents do not have a preference for a segregated outcome (e.g. they would be happy with 3 similar neighbors and 5 different ones), the aggregate outcome is nevertheless heavily segregated.\n",
|
|
12
|
+
"\n",
|
|
13
|
+
"## Implementation\n",
|
|
14
|
+
"\n",
|
|
15
|
+
"This is a demonstration of running a Mesa model in an IPython Notebook. The actual model and agent code are implemented in Schelling.py, in the same directory as this notebook. Below, we will import the model class, instantiate it, run it, and plot the time series of the number of happy agents."
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"cell_type": "code",
|
|
20
|
+
"metadata": {},
|
|
21
|
+
"source": [
|
|
22
|
+
"import matplotlib.pyplot as plt\n",
|
|
23
|
+
"import pandas as pd\n",
|
|
24
|
+
"\n",
|
|
25
|
+
"%matplotlib inline\n",
|
|
26
|
+
"\n",
|
|
27
|
+
"from model import Schelling"
|
|
28
|
+
],
|
|
29
|
+
"outputs": [],
|
|
30
|
+
"execution_count": null
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"cell_type": "markdown",
|
|
34
|
+
"metadata": {},
|
|
35
|
+
"source": [
|
|
36
|
+
"Now we instantiate a model instance: a 10x10 grid, with an 80% change of an agent being placed in each cell, approximately 20% of agents set as minorities, and agents wanting at least 3 similar neighbors."
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"cell_type": "code",
|
|
41
|
+
"metadata": {},
|
|
42
|
+
"source": "model = Schelling(height=10, width=10, homophily=3, density=0.8, minority_pc=0.2)",
|
|
43
|
+
"outputs": [],
|
|
44
|
+
"execution_count": null
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"cell_type": "markdown",
|
|
48
|
+
"metadata": {},
|
|
49
|
+
"source": [
|
|
50
|
+
"We want to run the model until all the agents are happy with where they are. However, there's no guarantee that a given model instantiation will *ever* settle down. So let's run it for either 100 steps or until it stops on its own, whichever comes first:"
|
|
51
|
+
]
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"cell_type": "code",
|
|
55
|
+
"metadata": {},
|
|
56
|
+
"source": [
|
|
57
|
+
"while model.running and model.steps < 100:\n",
|
|
58
|
+
" model.step()\n",
|
|
59
|
+
"print(model.steps) # Show how many steps have actually run"
|
|
60
|
+
],
|
|
61
|
+
"outputs": [],
|
|
62
|
+
"execution_count": null
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"cell_type": "markdown",
|
|
66
|
+
"metadata": {},
|
|
67
|
+
"source": [
|
|
68
|
+
"The model has a DataCollector object, which checks and stores how many agents are happy at the end of each step. It can also generate a pandas DataFrame of the data it has collected:"
|
|
69
|
+
]
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"cell_type": "code",
|
|
73
|
+
"metadata": {},
|
|
74
|
+
"source": [
|
|
75
|
+
"model_out = model.datacollector.get_model_vars_dataframe()"
|
|
76
|
+
],
|
|
77
|
+
"outputs": [],
|
|
78
|
+
"execution_count": null
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"cell_type": "code",
|
|
82
|
+
"metadata": {},
|
|
83
|
+
"source": [
|
|
84
|
+
"model_out.head()"
|
|
85
|
+
],
|
|
86
|
+
"outputs": [],
|
|
87
|
+
"execution_count": null
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"cell_type": "markdown",
|
|
91
|
+
"metadata": {},
|
|
92
|
+
"source": [
|
|
93
|
+
"Finally, we can plot the 'happy' series:"
|
|
94
|
+
]
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"cell_type": "code",
|
|
98
|
+
"metadata": {},
|
|
99
|
+
"source": [
|
|
100
|
+
"model_out.happy.plot()"
|
|
101
|
+
],
|
|
102
|
+
"outputs": [],
|
|
103
|
+
"execution_count": null
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"cell_type": "markdown",
|
|
107
|
+
"metadata": {},
|
|
108
|
+
"source": [
|
|
109
|
+
"# Effect of Homophily on happiness\n",
|
|
110
|
+
"\n",
|
|
111
|
+
"Now, we can do a parameter sweep to see how happiness changes with homophily.\n",
|
|
112
|
+
"\n",
|
|
113
|
+
"First, we create a function which takes a model instance and returns what fraction of agents are segregated -- that is, have no neighbors of the opposite type."
|
|
114
|
+
]
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"cell_type": "code",
|
|
118
|
+
"metadata": {},
|
|
119
|
+
"source": "from mesa.batchrunner import batch_run",
|
|
120
|
+
"outputs": [],
|
|
121
|
+
"execution_count": null
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"cell_type": "markdown",
|
|
125
|
+
"metadata": {},
|
|
126
|
+
"source": [
|
|
127
|
+
"Now, we set up the batch run, with a dictionary of fixed and changing parameters. Let's hold everything fixed except for Homophily."
|
|
128
|
+
]
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"cell_type": "code",
|
|
132
|
+
"metadata": {},
|
|
133
|
+
"source": [
|
|
134
|
+
"fixed_params = {\"height\": 10, \"width\": 10, \"density\": 0.8, \"minority_pc\": 0.2}\n",
|
|
135
|
+
"variable_parms = {\"homophily\": range(1, 9)}\n",
|
|
136
|
+
"all_params = fixed_params | variable_parms"
|
|
137
|
+
],
|
|
138
|
+
"outputs": [],
|
|
139
|
+
"execution_count": null
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"cell_type": "code",
|
|
143
|
+
"metadata": {},
|
|
144
|
+
"source": [
|
|
145
|
+
"results = batch_run(\n",
|
|
146
|
+
" Schelling,\n",
|
|
147
|
+
" parameters=all_params,\n",
|
|
148
|
+
" iterations=10,\n",
|
|
149
|
+
" max_steps=200,\n",
|
|
150
|
+
")"
|
|
151
|
+
],
|
|
152
|
+
"outputs": [],
|
|
153
|
+
"execution_count": null
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
"metadata": {},
|
|
157
|
+
"cell_type": "code",
|
|
158
|
+
"source": [
|
|
159
|
+
"df = pd.DataFrame(results)\n",
|
|
160
|
+
"df"
|
|
161
|
+
],
|
|
162
|
+
"outputs": [],
|
|
163
|
+
"execution_count": null
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
"cell_type": "code",
|
|
167
|
+
"metadata": {},
|
|
168
|
+
"source": [
|
|
169
|
+
"plt.scatter(df.homophily, df.happy)\n",
|
|
170
|
+
"plt.xlabel(\"Homophily\")\n",
|
|
171
|
+
"plt.ylabel(\"Happy Agents\")\n",
|
|
172
|
+
"plt.grid()\n",
|
|
173
|
+
"plt.title(\"Effect of Homophily on segregation\")\n",
|
|
174
|
+
"plt.show()"
|
|
175
|
+
],
|
|
176
|
+
"outputs": [],
|
|
177
|
+
"execution_count": null
|
|
178
|
+
}
|
|
179
|
+
],
|
|
180
|
+
"metadata": {
|
|
181
|
+
"kernelspec": {
|
|
182
|
+
"display_name": "Python 3 (ipykernel)",
|
|
183
|
+
"language": "python",
|
|
184
|
+
"name": "python3"
|
|
185
|
+
},
|
|
186
|
+
"language_info": {
|
|
187
|
+
"codemirror_mode": {
|
|
188
|
+
"name": "ipython",
|
|
189
|
+
"version": 3
|
|
190
|
+
},
|
|
191
|
+
"file_extension": ".py",
|
|
192
|
+
"mimetype": "text/x-python",
|
|
193
|
+
"name": "python",
|
|
194
|
+
"nbconvert_exporter": "python",
|
|
195
|
+
"pygments_lexer": "ipython3",
|
|
196
|
+
"version": "3.9.9"
|
|
197
|
+
},
|
|
198
|
+
"widgets": {
|
|
199
|
+
"state": {},
|
|
200
|
+
"version": "1.1.2"
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
"nbformat": 4,
|
|
204
|
+
"nbformat_minor": 1
|
|
205
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import solara
|
|
2
|
+
|
|
3
|
+
from mesa.examples.basic.schelling.model import Schelling
|
|
4
|
+
from mesa.visualization import (
|
|
5
|
+
Slider,
|
|
6
|
+
SolaraViz,
|
|
7
|
+
make_plot_measure,
|
|
8
|
+
make_space_matplotlib,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_happy_agents(model):
|
|
13
|
+
"""Display a text count of how many happy agents there are."""
|
|
14
|
+
return solara.Markdown(f"**Happy agents: {model.happy}**")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def agent_portrayal(agent):
|
|
18
|
+
return {"color": "tab:orange" if agent.type == 0 else "tab:blue"}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
model_params = {
|
|
22
|
+
"density": Slider("Agent density", 0.8, 0.1, 1.0, 0.1),
|
|
23
|
+
"minority_pc": Slider("Fraction minority", 0.2, 0.0, 1.0, 0.05),
|
|
24
|
+
"homophily": Slider("Homophily", 3, 0, 8, 1),
|
|
25
|
+
"width": 20,
|
|
26
|
+
"height": 20,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
model1 = Schelling(20, 20, 0.8, 0.2, 3)
|
|
30
|
+
|
|
31
|
+
HappyPlot = make_plot_measure("happy")
|
|
32
|
+
|
|
33
|
+
page = SolaraViz(
|
|
34
|
+
model1,
|
|
35
|
+
components=[
|
|
36
|
+
make_space_matplotlib(agent_portrayal),
|
|
37
|
+
make_plot_measure("happy"),
|
|
38
|
+
get_happy_agents,
|
|
39
|
+
],
|
|
40
|
+
model_params=model_params,
|
|
41
|
+
)
|
|
42
|
+
page # noqa
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import mesa
|
|
2
|
+
from mesa import Model
|
|
3
|
+
from mesa.examples.basic.schelling.agents import SchellingAgent
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Schelling(Model):
|
|
7
|
+
"""Model class for the Schelling segregation model."""
|
|
8
|
+
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
height=20,
|
|
12
|
+
width=20,
|
|
13
|
+
homophily=3,
|
|
14
|
+
radius=1,
|
|
15
|
+
density=0.8,
|
|
16
|
+
minority_pc=0.2,
|
|
17
|
+
seed=None,
|
|
18
|
+
):
|
|
19
|
+
"""Create a new Schelling model.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
width, height: Size of the space.
|
|
23
|
+
density: Initial Chance for a cell to populated
|
|
24
|
+
minority_pc: Chances for an agent to be in minority class
|
|
25
|
+
homophily: Minimum number of agents of same class needed to be happy
|
|
26
|
+
radius: Search radius for checking similarity
|
|
27
|
+
seed: Seed for Reproducibility
|
|
28
|
+
"""
|
|
29
|
+
super().__init__(seed=seed)
|
|
30
|
+
self.homophily = homophily
|
|
31
|
+
self.radius = radius
|
|
32
|
+
|
|
33
|
+
self.grid = mesa.space.SingleGrid(width, height, torus=True)
|
|
34
|
+
|
|
35
|
+
self.happy = 0
|
|
36
|
+
self.datacollector = mesa.DataCollector(
|
|
37
|
+
model_reporters={"happy": "happy"}, # Model-level count of happy agents
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Set up agents
|
|
41
|
+
# We use a grid iterator that returns
|
|
42
|
+
# the coordinates of a cell as well as
|
|
43
|
+
# its contents. (coord_iter)
|
|
44
|
+
for _, pos in self.grid.coord_iter():
|
|
45
|
+
if self.random.random() < density:
|
|
46
|
+
agent_type = 1 if self.random.random() < minority_pc else 0
|
|
47
|
+
agent = SchellingAgent(self, agent_type)
|
|
48
|
+
self.grid.place_agent(agent, pos)
|
|
49
|
+
|
|
50
|
+
self.datacollector.collect(self)
|
|
51
|
+
|
|
52
|
+
def step(self):
|
|
53
|
+
"""Run one step of the model."""
|
|
54
|
+
self.happy = 0 # Reset counter of happy agents
|
|
55
|
+
self.agents.shuffle_do("step")
|
|
56
|
+
|
|
57
|
+
self.datacollector.collect(self)
|
|
58
|
+
|
|
59
|
+
self.running = self.happy != len(self.agents)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Virus on a Network
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
|
|
5
|
+
This model is based on the NetLogo model "Virus on Network". It demonstrates the spread of a virus through a network and follows the SIR model, commonly seen in epidemiology.
|
|
6
|
+
|
|
7
|
+
The SIR model is one of the simplest compartmental models, and many models are derivatives of this basic form. The model consists of three compartments:
|
|
8
|
+
|
|
9
|
+
S: The number of susceptible individuals. When a susceptible and an infectious individual come into "infectious contact", the susceptible individual contracts the disease and transitions to the infectious compartment.
|
|
10
|
+
I: The number of infectious individuals. These are individuals who have been infected and are capable of infecting susceptible individuals.
|
|
11
|
+
R for the number of removed (and immune) or deceased individuals. These are individuals who have been infected and have either recovered from the disease and entered the removed compartment, or died. It is assumed that the number of deaths is negligible with respect to the total population. This compartment may also be called "recovered" or "resistant".
|
|
12
|
+
|
|
13
|
+
For more information about this model, read the NetLogo's web page: http://ccl.northwestern.edu/netlogo/models/VirusonaNetwork.
|
|
14
|
+
|
|
15
|
+
JavaScript library used in this example to render the network: [d3.js](https://d3js.org/).
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
To install the dependencies use pip and the requirements.txt in this directory. e.g.
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
$ pip install -r requirements.txt
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## How to Run
|
|
26
|
+
|
|
27
|
+
To run the model interactively, run ``mesa runserver`` in this directory. e.g.
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
$ mesa runserver
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Then open your browser to [http://127.0.0.1:8521/](http://127.0.0.1:8521/) and press Reset, then Run.
|
|
34
|
+
|
|
35
|
+
or
|
|
36
|
+
|
|
37
|
+
Directly run the file ``run.py`` in the terminal. e.g.
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
$ python run.py
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
## Files
|
|
45
|
+
|
|
46
|
+
* ``model.py``: Contains the agent class, and the overall model class.
|
|
47
|
+
* ``agents.py``: Contains the agent class.
|
|
48
|
+
* ``app.py``: Contains the code for the interactive Solara visualization.
|
|
49
|
+
|
|
50
|
+
## Further Reading
|
|
51
|
+
|
|
52
|
+
The full tutorial describing how the model is built can be found at:
|
|
53
|
+
https://mesa.readthedocs.io/en/latest/tutorials/intro_tutorial.html
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
[Stonedahl, F. and Wilensky, U. (2008). NetLogo Virus on a Network model](http://ccl.northwestern.edu/netlogo/models/VirusonaNetwork).
|
|
57
|
+
Center for Connected Learning and Computer-Based Modeling, Northwestern University, Evanston, IL.
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
[Wilensky, U. (1999). NetLogo](http://ccl.northwestern.edu/netlogo/)
|
|
61
|
+
Center for Connected Learning and Computer-Based Modeling, Northwestern University, Evanston, IL.
|
|
File without changes
|
|
@@ -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,136 @@
|
|
|
1
|
+
import math
|
|
2
|
+
|
|
3
|
+
import solara
|
|
4
|
+
from matplotlib.figure import Figure
|
|
5
|
+
from matplotlib.ticker import MaxNLocator
|
|
6
|
+
|
|
7
|
+
from mesa.examples.basic.virus_on_network.model import (
|
|
8
|
+
State,
|
|
9
|
+
VirusOnNetwork,
|
|
10
|
+
number_infected,
|
|
11
|
+
)
|
|
12
|
+
from mesa.visualization import Slider, SolaraViz, make_space_matplotlib
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def agent_portrayal(graph):
|
|
16
|
+
def get_agent(node):
|
|
17
|
+
return graph.nodes[node]["agent"][0]
|
|
18
|
+
|
|
19
|
+
edge_width = []
|
|
20
|
+
edge_color = []
|
|
21
|
+
for u, v in graph.edges():
|
|
22
|
+
agent1 = get_agent(u)
|
|
23
|
+
agent2 = get_agent(v)
|
|
24
|
+
w = 2
|
|
25
|
+
ec = "#e8e8e8"
|
|
26
|
+
if State.RESISTANT in (agent1.state, agent2.state):
|
|
27
|
+
w = 3
|
|
28
|
+
ec = "black"
|
|
29
|
+
edge_width.append(w)
|
|
30
|
+
edge_color.append(ec)
|
|
31
|
+
node_color_dict = {
|
|
32
|
+
State.INFECTED: "tab:red",
|
|
33
|
+
State.SUSCEPTIBLE: "tab:green",
|
|
34
|
+
State.RESISTANT: "tab:gray",
|
|
35
|
+
}
|
|
36
|
+
node_color = [node_color_dict[get_agent(node).state] for node in graph.nodes()]
|
|
37
|
+
return {
|
|
38
|
+
"width": edge_width,
|
|
39
|
+
"edge_color": edge_color,
|
|
40
|
+
"node_color": node_color,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_resistant_susceptible_ratio(model):
|
|
45
|
+
ratio = model.resistant_susceptible_ratio()
|
|
46
|
+
ratio_text = r"$\infty$" if ratio is math.inf else f"{ratio:.2f}"
|
|
47
|
+
infected_text = str(number_infected(model))
|
|
48
|
+
|
|
49
|
+
return f"Resistant/Susceptible Ratio: {ratio_text}<br>Infected Remaining: {infected_text}"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def make_plot(model):
|
|
53
|
+
# This is for the case when we want to plot multiple measures in 1 figure.
|
|
54
|
+
fig = Figure()
|
|
55
|
+
ax = fig.subplots()
|
|
56
|
+
measures = ["Infected", "Susceptible", "Resistant"]
|
|
57
|
+
colors = ["tab:red", "tab:green", "tab:gray"]
|
|
58
|
+
for i, m in enumerate(measures):
|
|
59
|
+
color = colors[i]
|
|
60
|
+
df = model.datacollector.get_model_vars_dataframe()
|
|
61
|
+
ax.plot(df.loc[:, m], label=m, color=color)
|
|
62
|
+
fig.legend()
|
|
63
|
+
# Set integer x axis
|
|
64
|
+
ax.xaxis.set_major_locator(MaxNLocator(integer=True))
|
|
65
|
+
ax.set_xlabel("Step")
|
|
66
|
+
ax.set_ylabel("Number of Agents")
|
|
67
|
+
return solara.FigureMatplotlib(fig)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
model_params = {
|
|
71
|
+
"num_nodes": Slider(
|
|
72
|
+
label="Number of agents",
|
|
73
|
+
value=10,
|
|
74
|
+
min=10,
|
|
75
|
+
max=100,
|
|
76
|
+
step=1,
|
|
77
|
+
),
|
|
78
|
+
"avg_node_degree": Slider(
|
|
79
|
+
label="Avg Node Degree",
|
|
80
|
+
value=3,
|
|
81
|
+
min=3,
|
|
82
|
+
max=8,
|
|
83
|
+
step=1,
|
|
84
|
+
),
|
|
85
|
+
"initial_outbreak_size": Slider(
|
|
86
|
+
label="Initial Outbreak Size",
|
|
87
|
+
value=1,
|
|
88
|
+
min=1,
|
|
89
|
+
max=10,
|
|
90
|
+
step=1,
|
|
91
|
+
),
|
|
92
|
+
"virus_spread_chance": Slider(
|
|
93
|
+
label="Virus Spread Chance",
|
|
94
|
+
value=0.4,
|
|
95
|
+
min=0.0,
|
|
96
|
+
max=1.0,
|
|
97
|
+
step=0.1,
|
|
98
|
+
),
|
|
99
|
+
"virus_check_frequency": Slider(
|
|
100
|
+
label="Virus Check Frequency",
|
|
101
|
+
value=0.4,
|
|
102
|
+
min=0.0,
|
|
103
|
+
max=1.0,
|
|
104
|
+
step=0.1,
|
|
105
|
+
),
|
|
106
|
+
"recovery_chance": Slider(
|
|
107
|
+
label="Recovery Chance",
|
|
108
|
+
value=0.3,
|
|
109
|
+
min=0.0,
|
|
110
|
+
max=1.0,
|
|
111
|
+
step=0.1,
|
|
112
|
+
),
|
|
113
|
+
"gain_resistance_chance": Slider(
|
|
114
|
+
label="Gain Resistance Chance",
|
|
115
|
+
value=0.5,
|
|
116
|
+
min=0.0,
|
|
117
|
+
max=1.0,
|
|
118
|
+
step=0.1,
|
|
119
|
+
),
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
SpacePlot = make_space_matplotlib(agent_portrayal)
|
|
123
|
+
|
|
124
|
+
model1 = VirusOnNetwork()
|
|
125
|
+
|
|
126
|
+
page = SolaraViz(
|
|
127
|
+
model1,
|
|
128
|
+
[
|
|
129
|
+
SpacePlot,
|
|
130
|
+
make_plot,
|
|
131
|
+
# get_resistant_susceptible_ratio, # TODO: Fix and uncomment
|
|
132
|
+
],
|
|
133
|
+
model_params=model_params,
|
|
134
|
+
name="Virus Model",
|
|
135
|
+
)
|
|
136
|
+
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/__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
|