-
Notifications
You must be signed in to change notification settings - Fork 54
/
Copy pathorganism_v1.py
331 lines (235 loc) · 10.9 KB
/
organism_v1.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
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
#------------------------------------------------------------------------------+
#
# Nathan A. Rooy
# Evolving Simple Organisms
# 2017-Nov.
#
#------------------------------------------------------------------------------+
#--- IMPORT DEPENDENCIES ------------------------------------------------------+
from __future__ import division, print_function
from collections import defaultdict
from matplotlib import pyplot as plt
from matplotlib.patches import Circle
import matplotlib.lines as lines
from plotting import plot_food
from plotting import plot_organism
import numpy as np
import operator
from math import atan2
from math import cos
from math import degrees
from math import floor
from math import radians
from math import sin
from math import sqrt
from random import randint
from random import random
from random import sample
from random import uniform
#--- CONSTANTS ----------------------------------------------------------------+
settings = {}
# EVOLUTION SETTINGS
settings['pop_size'] = 50 # number of organisms
settings['food_num'] = 100 # number of food particles
settings['gens'] = 50 # number of generations
settings['elitism'] = 0.20 # elitism (selection bias)
settings['mutate'] = 0.10 # mutation rate
# SIMULATION SETTINGS
settings['gen_time'] = 100 # generation length (seconds)
settings['dt'] = 0.04 # simulation time step (dt)
settings['dr_max'] = 720 # max rotational speed (degrees per second)
settings['v_max'] = 0.5 # max velocity (units per second)
settings['dv_max'] = 0.25 # max acceleration (+/-) (units per second^2)
settings['x_min'] = -2.0 # arena western border
settings['x_max'] = 2.0 # arena eastern border
settings['y_min'] = -2.0 # arena southern border
settings['y_max'] = 2.0 # arena northern border
settings['plot'] = False # plot final generation?
# ORGANISM NEURAL NET SETTINGS
settings['inodes'] = 1 # number of input nodes
settings['hnodes'] = 5 # number of hidden nodes
settings['onodes'] = 2 # number of output nodes
#--- FUNCTIONS ----------------------------------------------------------------+
def dist(x1,y1,x2,y2):
return sqrt((x2-x1)**2 + (y2-y1)**2)
def calc_heading(org, food):
d_x = food.x - org.x
d_y = food.y - org.y
theta_d = degrees(atan2(d_y, d_x)) - org.r
if abs(theta_d) > 180: theta_d += 360
return theta_d / 180
def plot_frame(settings, organisms, foods, gen, time):
fig, ax = plt.subplots()
fig.set_size_inches(9.6, 5.4)
plt.xlim([settings['x_min'] + settings['x_min'] * 0.25, settings['x_max'] + settings['x_max'] * 0.25])
plt.ylim([settings['y_min'] + settings['y_min'] * 0.25, settings['y_max'] + settings['y_max'] * 0.25])
# PLOT ORGANISMS
for organism in organisms:
plot_organism(organism.x, organism.y, organism.r, ax)
# PLOT FOOD PARTICLES
for food in foods:
plot_food(food.x, food.y, ax)
# MISC PLOT SETTINGS
ax.set_aspect('equal')
frame = plt.gca()
frame.axes.get_xaxis().set_ticks([])
frame.axes.get_yaxis().set_ticks([])
plt.figtext(0.025, 0.95,r'GENERATION: '+str(gen))
plt.figtext(0.025, 0.90,r'T_STEP: '+str(time))
plt.savefig(str(gen)+'-'+str(time)+'.png', dpi=100)
## plt.show()
def evolve(settings, organisms_old, gen):
elitism_num = int(floor(settings['elitism'] * settings['pop_size']))
new_orgs = settings['pop_size'] - elitism_num
#--- GET STATS FROM CURRENT GENERATION ----------------+
stats = defaultdict(int)
for org in organisms_old:
if org.fitness > stats['BEST'] or stats['BEST'] == 0:
stats['BEST'] = org.fitness
if org.fitness < stats['WORST'] or stats['WORST'] == 0:
stats['WORST'] = org.fitness
stats['SUM'] += org.fitness
stats['COUNT'] += 1
stats['AVG'] = stats['SUM'] / stats['COUNT']
#--- ELITISM (KEEP BEST PERFORMING ORGANISMS) ---------+
orgs_sorted = sorted(organisms_old, key=operator.attrgetter('fitness'), reverse=True)
organisms_new = []
for i in range(0, elitism_num):
organisms_new.append(organism(settings, wih=orgs_sorted[i].wih, who=orgs_sorted[i].who, name=orgs_sorted[i].name))
#--- GENERATE NEW ORGANISMS ---------------------------+
for w in range(0, new_orgs):
# SELECTION (TRUNCATION SELECTION)
canidates = range(0, elitism_num)
random_index = sample(canidates, 2)
org_1 = orgs_sorted[random_index[0]]
org_2 = orgs_sorted[random_index[1]]
# CROSSOVER
crossover_weight = random()
wih_new = (crossover_weight * org_1.wih) + ((1 - crossover_weight) * org_2.wih)
who_new = (crossover_weight * org_1.who) + ((1 - crossover_weight) * org_2.who)
# MUTATION
mutate = random()
if mutate <= settings['mutate']:
# PICK WHICH WEIGHT MATRIX TO MUTATE
mat_pick = randint(0,1)
# MUTATE: WIH WEIGHTS
if mat_pick == 0:
index_row = randint(0,settings['hnodes']-1)
wih_new[index_row] = wih_new[index_row] * uniform(0.9, 1.1)
if wih_new[index_row] > 1: wih_new[index_row] = 1
if wih_new[index_row] < -1: wih_new[index_row] = -1
# MUTATE: WHO WEIGHTS
if mat_pick == 1:
index_row = randint(0,settings['onodes']-1)
index_col = randint(0,settings['hnodes']-1)
who_new[index_row][index_col] = who_new[index_row][index_col] * uniform(0.9, 1.1)
if who_new[index_row][index_col] > 1: who_new[index_row][index_col] = 1
if who_new[index_row][index_col] < -1: who_new[index_row][index_col] = -1
organisms_new.append(organism(settings, wih=wih_new, who=who_new, name='gen['+str(gen)+']-org['+str(w)+']'))
return organisms_new, stats
def simulate(settings, organisms, foods, gen):
total_time_steps = int(settings['gen_time'] / settings['dt'])
#--- CYCLE THROUGH EACH TIME STEP ---------------------+
for t_step in range(0, total_time_steps, 1):
# PLOT SIMULATION FRAME
if settings['plot']==True and gen==settings['gens']-1:
plot_frame(settings, organisms, foods, gen, t_step)
# UPDATE FITNESS FUNCTION
for food in foods:
for org in organisms:
food_org_dist = dist(org.x, org.y, food.x, food.y)
# UPDATE FITNESS FUNCTION
if food_org_dist <= 0.075:
org.fitness += food.energy
food.respawn(settings)
# RESET DISTANCE AND HEADING TO NEAREST FOOD SOURCE
org.d_food = 100
org.r_food = 0
# CALCULATE HEADING TO NEAREST FOOD SOURCE
for food in foods:
for org in organisms:
# CALCULATE DISTANCE TO SELECTED FOOD PARTICLE
food_org_dist = dist(org.x, org.y, food.x, food.y)
# DETERMINE IF THIS IS THE CLOSEST FOOD PARTICLE
if food_org_dist < org.d_food:
org.d_food = food_org_dist
org.r_food = calc_heading(org, food)
# GET ORGANISM RESPONSE
for org in organisms:
org.think()
# UPDATE ORGANISMS POSITION AND VELOCITY
for org in organisms:
org.update_r(settings)
org.update_vel(settings)
org.update_pos(settings)
return organisms
#--- CLASSES ------------------------------------------------------------------+
class food():
def __init__(self, settings):
self.x = uniform(settings['x_min'], settings['x_max'])
self.y = uniform(settings['y_min'], settings['y_max'])
self.energy = 1
def respawn(self,settings):
self.x = uniform(settings['x_min'], settings['x_max'])
self.y = uniform(settings['y_min'], settings['y_max'])
self.energy = 1
class organism():
def __init__(self, settings, wih=None, who=None, name=None):
self.x = uniform(settings['x_min'], settings['x_max']) # position (x)
self.y = uniform(settings['y_min'], settings['y_max']) # position (y)
self.r = uniform(0,360) # orientation [0, 360]
self.v = uniform(0,settings['v_max']) # velocity [0, v_max]
self.dv = uniform(-settings['dv_max'], settings['dv_max']) # dv
self.d_food = 100 # distance to nearest food
self.r_food = 0 # orientation to nearest food
self.fitness = 0 # fitness (food count)
self.wih = wih
self.who = who
self.name = name
# NEURAL NETWORK
def think(self):
# SIMPLE MLP
af = lambda x: np.tanh(x) # activation function
h1 = af(np.dot(self.wih, self.r_food)) # hidden layer
out = af(np.dot(self.who, h1)) # output layer
# UPDATE dv AND dr WITH MLP RESPONSE
self.nn_dv = float(out[0]) # [-1, 1] (accelerate=1, deaccelerate=-1)
self.nn_dr = float(out[1]) # [-1, 1] (left=1, right=-1)
# UPDATE HEADING
def update_r(self, settings):
self.r += self.nn_dr * settings['dr_max'] * settings['dt']
self.r = self.r % 360
# UPDATE VELOCITY
def update_vel(self, settings):
self.v += self.nn_dv * settings['dv_max'] * settings['dt']
if self.v < 0: self.v = 0
if self.v > settings['v_max']: self.v = settings['v_max']
# UPDATE POSITION
def update_pos(self, settings):
dx = self.v * cos(radians(self.r)) * settings['dt']
dy = self.v * sin(radians(self.r)) * settings['dt']
self.x += dx
self.y += dy
#--- MAIN ---------------------------------------------------------------------+
def run(settings):
#--- POPULATE THE ENVIRONMENT WITH FOOD ---------------+
foods = []
for i in range(0,settings['food_num']):
foods.append(food(settings))
#--- POPULATE THE ENVIRONMENT WITH ORGANISMS ----------+
organisms = []
for i in range(0,settings['pop_size']):
wih_init = np.random.uniform(-1, 1, (settings['hnodes'], settings['inodes'])) # mlp weights (input -> hidden)
who_init = np.random.uniform(-1, 1, (settings['onodes'], settings['hnodes'])) # mlp weights (hidden -> output)
organisms.append(organism(settings, wih_init, who_init, name='gen[x]-org['+str(i)+']'))
#--- CYCLE THROUGH EACH GENERATION --------------------+
for gen in range(0, settings['gens']):
# SIMULATE
organisms = simulate(settings, organisms, foods, gen)
# EVOLVE
organisms, stats = evolve(settings, organisms, gen)
print('> GEN:',gen,'BEST:',stats['BEST'],'AVG:',stats['AVG'],'WORST:',stats['WORST'])
pass
#--- RUN ----------------------------------------------------------------------+
run(settings)
#--- END ----------------------------------------------------------------------+