-
Notifications
You must be signed in to change notification settings - Fork 0
/
model.py
254 lines (207 loc) · 7.75 KB
/
model.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
import matplotlib
matplotlib.use('TkAgg')
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
import agentframework
import agentstorage
import csv
import random
import argparse
import tkinter
import requests
import bs4
# Quitter function from tkinter loop
# From: https://stackoverflow.com/a/55206851
def quit_me() -> None:
"""Quit and destroy the tkinter mainloop on closing the model GUI window."""
print('Quitting model runner!')
root.quit()
root.destroy()
# Updates agents one by one
def update(frame_number) -> None:
"""
Update agent position and behaviour.
Parameters
----------
frame_number : int
Set automatically to equal number of model iterations, only used in animation.
Returns
-------
None
"""
fig.clear() # clears scatter points from earlier iteration
random.shuffle(agents) # shuffle agents to eliminate position-based advantages
for a in range(num_of_agents):
agents[a].eat()
for a in range(num_of_agents):
agents[a].move()
for a in range(num_of_agents):
agents[a].share_with_neighbours(neighbourhood)
for a in range(num_of_agents):
agents[a].share_eater()
for a in range(num_of_agents):
agents[a].sick()
# Plot agents on a scatterplot recursively adding points onto the environment raster (only on single model run)
plt.imshow(environment)
for b in range(num_of_agents):
plt.scatter(agents[b].x, agents[b].y)
# Create and display animation, write outputs
def run() -> None:
"""Run the model animation and record output."""
# Defining animation part with stopping at num_of_iterations and no looping
animation = FuncAnimation(fig, update, interval=1, repeat=False, frames=num_of_iterations)
# Only draw result when there is no parameter sweeping
canvas.draw()
# Write environment to outfile
env_writer('out.txt')
# Print overall and one-by-one agent storage, record in storage.txt
agentstorage.all_storage_writer(agents, 'storage.txt')
agentstorage.agent_storage_writer(agents, 'storage.txt')
# Print agents
agent_printer(agents)
# Read environment file to nested list
def env_reader(infile: str) -> list:
"""
Read environment list from infile.
Parameters
----------
infile : str
Name of the file containing the environment list.
Returns
-------
list
Nested (2D) list of the environment.
"""
with open(infile, 'r') as f:
reader = csv.reader(f, quoting=csv.QUOTE_NONNUMERIC) # QUOTE_NONNUMERIC changes everything to float
# For every row of the list, append every row element to an environment row, then append environment row to environment
environment = [[value for value in row] for row in reader]
return environment
# Make agents based on num_of_agents
def agent_maker(num_of_agents: int, environment: list, ys: list, xs: list) -> list:
"""
Create list of agents used for simulation.
Parameters
----------
num_of_agents : int
The desired number of agents, defaults to length of web agent list.
environment : list
Nested(2D) list of environment.
ys : list
List of y coordinates retrieved from the web.
xs : list
List of x coordinates retrieved from the web.
Returns
-------
list
List of agentframework.Agent objects.
"""
# Create empty agents list
agents = []
# For every agent, test if there is a coordinate pair from the web for it (not out of bound)
for i in range(num_of_agents):
if i < len(ys):
y = ys[i]
x = xs[i]
agents.append(agentframework.Agent(i, environment, agents, y, x))
# If there is no coordinate pair (out of bounds), one is assigned randomly
else:
y = random.randint(0, max(ys))
x = random.randint(0, max(xs))
agents.append(agentframework.Agent(i, environment, agents, y, x))
return agents
# Get agent starting coordinates from the web
def web_scraper(url: str) -> tuple:
"""
Scrape the web for agent coordinates.
Parameters
----------
url : str
The URL of the agent coordinates data on the web.
Returns
-------
tuple
Tuple of lists ([y coordinates], [x coordinates]).
"""
# Fetch the url and download the text the html site contains
r = requests.get(url)
content = r.text
# Parse html text to make it selectable
soup = bs4.BeautifulSoup(content, 'html.parser')
# Find coordinate table
table = soup.find(id='yxz')
# Retrieve list of html tags containing x and y values
ys_html = soup.find_all(attrs={'class': 'y'})
xs_html = soup.find_all(attrs={'class': 'x'})
# Extract and convert to integer the numbers from the tags
ys_basic = [int(y.text) for y in ys_html]
xs_basic = [int(x.text) for x in xs_html]
# Scale the numbers to fill the environment list
ys = [y * round(len(environment) / max(ys_basic)) for y in ys_basic]
xs = [x * round(len(environment[0]) / max(xs_basic)) for x in xs_basic]
return ys, xs
# Writes environment file to out.txt
def env_writer(outfile: str) -> None:
"""
Write environment list to text file after simulation.
Parameters
----------
outfile : str
Name of the output textfile.
Returns
-------
None
"""
# Write environment out at the end to a file
with open(outfile, 'w') as f:
writer = csv.writer(f)
for line in environment:
writer.writerow(line)
# 3. Overwrite __str__ method of agents to print location and storage
def agent_printer(agents: list) -> None:
"""
Print agent properties.
Parameters
----------
agents : list
List of agentframework.Agent objects.
Returns
-------
None
"""
for agent in agents:
print(agent)
# Create figure for animated plotting
fig = plt.figure(figsize=(7, 7))
ax = fig.add_axes([0, 0, 1, 1])
ax.set_autoscale_on(False) # Does not scale automatically
# Read environment list
environment = env_reader('in.txt')
# Scrape web for agent coordinate information
ys, xs = web_scraper('https://www.geog.leeds.ac.uk/courses/computing/practicals/python/agent-framework/part9/data.html')
# Create command-line functionality (needs positional arguments from command line to run)
parser = argparse.ArgumentParser(description='Simulate random moving agents grazing a field and sharing food')
# Add arguments
parser.add_argument('--agents', help='Number of agents (integer)', type=int, required=False, default=len(ys))
parser.add_argument('--iterations', help='Number of iterations (integer)', type=int, required=False, default=100)
parser.add_argument('--neighbourhood', help='Radius of agent communication zone (integer)', type=int, required=False, default=20)
# Declare number of agents and iterations, along with neighbourhood size (all from argparse cmd)
num_of_agents = parser.parse_args().agents
num_of_iterations = parser.parse_args().iterations
neighbourhood = parser.parse_args().neighbourhood
# Append to agents list
agents = agent_maker(num_of_agents, environment, ys, xs)
# Create GUI canvas
root = tkinter.Tk()
root.protocol('WM_DELETE_WINDOW', quit_me) # exists program when window closed
root.wm_title('Model')
canvas = matplotlib.backends.backend_tkagg.FigureCanvasTkAgg(fig, master=root) # Plot fig on canvas
canvas._tkcanvas.pack(side=tkinter.TOP, fill=tkinter.BOTH, expand=1)
# Create menu with Run functionality
menu_bar = tkinter.Menu(root)
root.config(menu=menu_bar)
model_menu = tkinter.Menu(menu_bar)
menu_bar.add_cascade(label='Model', menu=model_menu) # Model button
model_menu.add_command(label='Run model', command=run) # Run button in the drop-down list of Model
# Initialise main loop
root.mainloop()