/
model.py
193 lines (157 loc) · 6.46 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
from mesa import Agent, Model
from mesa.time import RandomActivation
from mesa.space import MultiGrid
from mesa.datacollection import DataCollector
import numpy as np
from scipy.spatial.distance import pdist
from sklearn.neighbors import DistanceMetric
'''Here we define functions applied at the model-wide scale'''
def compute_total_chemical(model):
agent_chems = [agent.chem for agent in model.schedule.agents if isinstance(
agent, ChemAgent)]
return sum(agent_chems)
def compute_distance_matrix(model):
agent_pos = [agent.pos for agent in model.schedule.agents if isinstance(
agent, SlimeAgent)]
agent_pos = [list(tup) for tup in agent_pos]
dist_mat = pdist(agent_pos)
return np.average(dist_mat)
def compute_average_dist(dist_mat):
return np.average(dist_mat)
'''Here we define the two kinds of agents the model accepts: slime and chem'''
class ChemAgent(Agent):
'''
An agent representing chemical concentration. This will be spawned in
every gridspace and hold a float value representing chemical concentration.
'''
def __init__(self, unique_id, model):
super().__init__(unique_id, model)
self.chem = 0.01
def diffuse(self):
#figure out how many neighbors you have
n_neighbors = len(self.model.grid.get_neighbors(self.pos,
moore=True,
include_center=False))
#amount to diffuse into each neighbor. at first, just divided total
#self.chem by neighbors, but then each cell loses all of it's chem.
#so, now, only 1/2 of the total val leaks out. we can tweak this.
#but seems to work better to multiply by some scalar < 1.
amt_per_neighbor = 0.5*(self.chem/n_neighbors)
for neighbor in self.model.grid.get_neighbors(self.pos,
moore=True,
include_center=False):
if isinstance(neighbor, ChemAgent):
neighbor.chem += amt_per_neighbor
self.chem -= amt_per_neighbor
def evaporate(self):
'''All chem agents lose chemical at 0.005 per step'''
evap_rate = 0.01
if self.chem > evap_rate: # so that self.chem stays pos
self.chem -= evap_rate
else:
self.chem = 0
def step(self):
'''
The agent shares chemical with surrounding cells through diffusion.
Chemical is also lost due to evaporation.
'''
self.diffuse()
self.evaporate()
class SlimeAgent(Agent):
'''
An agent that excretes chemical, senses, and moves towards higher chemical
concentration. This agent represents a single slime mold cell.
'''
def __init__(self, unique_id, model):
super().__init__(unique_id, model)
self.chem = 0
def secrete(self):
'''The agent adds chemical to its surrounding cells'''
for neighbor in self.model.grid.neighbor_iter(self.pos):
if isinstance(neighbor, ChemAgent):
neighbor.chem += 0.02 # add 0.1 chemical to neighboring cells
def move(self):
'''The agent sniffs the surrounding cells for the highest concentration
of chemical - it them moves to that cell.'''
neighbors = self.model.grid.get_neighbors(
self.pos,
moore=True,
include_center=True,
radius=1
)
curIndex = 0
idx = 0
temp = 0
while(curIndex < len(neighbors)):
if(temp < neighbors[curIndex].chem):
temp = neighbors[curIndex].chem # save the objattr
idx = curIndex # save the idx
curIndex += 1 # increment idx
optimal = neighbors[idx] # assign obj w/ said index
new_position = optimal.pos # identify the position of the optimal obj
self.model.grid.move_agent(self, new_position)
def step(self):
if (self.unique_id%10 != 0):
self.move()
self.secrete()
else: #either quell secretion or moving
#self.secrete()
self.move()
'''Above we define the agents'''
'''Below we define the model'''
class SlimeModel(Model):
'''
A model with some number of slime and ubiquitous chemical
Args:
pop: number of slime cells to add to the model
width: grid width
height: grid height
'''
def __init__(self, pop, width, height):
'''Initialize the model'''
self.N = pop # number of slime agents to spawn
self.grid = MultiGrid(width, height, True) # torus = True
self.schedule = RandomActivation(self)
self.running = True # True so that the model runs
# Spawn agents
# Add chem agent to every grid cell
for coord in self.grid.coord_iter():
coord_content, x, y = coord # pull contents, x/y pos from coord obj
id = str(x)+'_'+str(y) # unique_id is x_y position
a = ChemAgent(id, self) # instantiate a chem agent
self.schedule.add(a) # add to the schedule
self.grid.place_agent(a, (x, y)) # spawn the chem agent
# Add slime agent randomly, population specified by N
for i in range(self.N):
slm = SlimeAgent(i, self)
self.schedule.add(slm)
x = self.random.randrange(self.grid.width)
y = self.random.randrange(self.grid.height)
self.grid.place_agent(slm, (x, y))
# Initialize datacollector
self.datacollector = DataCollector(
model_reporters={"Total_Chem": compute_total_chemical,
"Average_Distance": compute_distance_matrix},
agent_reporters={"Chem": "chem"}
)
def validate(self):
'''
This function validates that there is a functioning ChemAgent in every
grid space.
'''
chemCTR = 0
matrixsize = self.grid.height*self.grid.width
for agent in self.schedule.agents:
if isinstance(agent, ChemAgent):
chemCTR += 1
if chemCTR == (self.grid.height*self.grid.width):
pass
else:
print(chemCTR)
print("ERROR: Chem agents not filling space")
print(matrixsize)
def step(self):
'''Advances the model by one step'''
self.validate()
self.datacollector.collect(self)
self.schedule.step()