-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathga_tsp_20_March_2019.py
More file actions
333 lines (287 loc) · 10.3 KB
/
ga_tsp_20_March_2019.py
File metadata and controls
333 lines (287 loc) · 10.3 KB
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
# -*- coding: UTF-8 -*-
from collections.__init__ import defaultdict
import random
import sys
import operator
import copy
import math
import time
import collections
from collections import defaultdict
from decimal import Decimal
from random import shuffle
from operator import itemgetter
import itertools
import pandas as pd
import pprint
import plotly.plotly as py
import plotly.tools as tls
import matplotlib.pyplot as plt
import matplotlib.animation as animation
# from matplotlib import style
# style.use ("fivethirtyeight")
landscape="global"
def init_landscape(num_cities):
"""Generate list of random x,y co-ordinates in 2D space.
Only need to represent this seed route - not all possible x,y in the space."""
global landscape
# landscape = [(0, 0), (94, 21), (200, 5), (21, 65), (157, 134), (10, 12), (74, 14), (12,190), (6, 2), (100, 100), (12, 24)]
random.seed (982)
landscape = []
co_count = 0
while co_count <= num_cities:
x = random.randint (0, 50)
y = random.randint (0, 50)
pair = (x,y)
landscape.append(pair)
co_count+=1
print(f"Landscape {landscape}")
return landscape
def init_population(population, num_cities):
"""Create initial population."""
random.seed(123)
seed_route = list (range (0, num_cities))
print(f"seed_route {seed_route}")
pop_counter = 0
new_pop = []
while pop_counter < population:
new_seed = copy.deepcopy(seed_route)
# print(f"new_seed {new_seed}")
shuffle(new_seed)
# print(f"shuffled {new_seed}")
new_pop.append(new_seed)
pop_counter+=1
# print(f"new_pop {new_pop}")
return new_pop
def list_to_dict(incoming_list):
"""Helper function converts route list to dict and renumbers city ready for further processing."""
# This only deals with list of lists so chops up single lists.
route_counter = 0
converted_dict = []
for item in incoming_list:
route_dictionary = {"id": 0, "cities": [], "norm":0, "distance": 0}
route_dictionary["id"] = route_counter
route_dictionary["cities"] = item
converted_dict.append(route_dictionary)
route_counter+=1
return converted_dict
def measure_route(population, num_cities):
"""Calculate total distance for each chromosome dictionary and return updated population."""
route_counter = len(population) + 1
for each_chromosome in population:
each_chromosome["id"] = route_counter
each_chromosome["distance"] = calculate_distance (each_chromosome, num_cities)
route_counter+=1
return population
def calculate_distance(route_dictionary, num_cities):
"""Given route dictionary and the number of cities as arguments in a route return total Euclidean distance for entire route
including back to base to two decimal places."""
total_distance = 0
counter = 0
while counter < num_cities-1:
city_1 = route_dictionary["cities"][counter]
city_2 = route_dictionary["cities"][counter+1]
step_distance = get_distance(city_1, city_2)
total_distance = total_distance + step_distance
counter += 1
back_to_start = get_distance (route_dictionary["cities"][0], route_dictionary["cities"][num_cities - 1])
total_distance = total_distance + back_to_start
route_dictionary["distance"] = round(total_distance,2)
return total_distance
def get_distance(city_1, city_2):
"""Given two city labels as argument and the global landscape, look up x,y coordinates and return
Euclidean distance between them to two decimal places."""
global landscape
this_distance = math.hypot((landscape[city_1][0] - landscape[city_1][1]), (landscape[city_2][0] - landscape[city_2][1]))
return this_distance
def sort_distances(pop_returned):
"""Rank population by route distance - shorter distance is better."""
pop_returned.sort(key=lambda e: e['distance'])
return pop_returned
def analyse_population(population):
"""Takes population with measured routes as input, returns min, max, norm, route lengths."""
# print (f"population IN analyse {population}")
ranked_population=sort_distances(population)
min_value=ranked_population[0]["distance"]
max_value=ranked_population[-1]["distance"]
# print(f"1 Min {min_value} Max {max_value}")
run_stats=normalise_distances (min_value,max_value,ranked_population)
# print(f"2 Min {min_value} Max {max_value}")
return run_stats
def normalise_distances(min_value, max_value,ranked_population):
"""Normalise distances, takes min, max of ranked_population, returns normalised value for each distance."""
# print(f"Ranked pop IN {len(ranked_population)} {ranked_population}")
for each_route in ranked_population:
# print (f"{each_route['distance']} {min_value} {max_value}")
each_route["norm"] = (each_route["distance"]-min_value)/(max_value-min_value)
# print(f"Normalise OUT {len(ranked_population)} {ranked_population}")
return ranked_population
def select_elite(ranked_population, num_elite):
# print(f"ranked_population {ranked_population}")
# elite_num = round(len(ranked_population)*num_elite)/100
# TODO Change num_elite to elite_num
elite = ranked_population[0:num_elite]
# print (f"select_elite out {elite}")
return elite
def utility_function(elite_ids, fit_pop):
"""Main function."""
elite_list = []
elite_pointer=0
while elite_pointer < len(elite_ids):
for each_dict in fit_pop:
for key, value in each_dict.items ():
if key == "id" and value == elite_ids[elite_pointer]:
elite_list.append(each_dict["cities"])
else:
pass
elite_pointer += 1
return elite_list
""" The important thing to remember with 'crossover' is that there is no
one right way of doing it."""
def single_crossover(current_elite, num_cities, population):
"""Single point crossover and mutation."""
# Add all members of the elite list to the new pool.
new_pool = current_elite
# new_pool=[]
# split_point = random.randint(0,num_cities)
split_point = round(num_cities / 2)
p_counter = len(current_elite)
only_routes=[]
# print(f"New pool{new_pool}")
for existing_route in current_elite:
# while p_counter <= population:
p_1, p_2 = random.sample (current_elite, 2)
parent_1 = p_1["cities"][0:split_point]
parent_2 = p_2["cities"][split_point:]
child = parent_1 + parent_2
for key, value in existing_route.items ():
if key == "cities":
print (f"existing_route {value}")
print (f"child {child}")
if value == child:
pass
elif value != child:
only_routes.append(child)
# while p_counter <= population:
# p_1, p_2 = random.sample (current_elite, 2)
# parent_1 = p_1["cities"][0:split_point]
# parent_2 = p_2["cities"][split_point:]
# child = parent_1 + parent_2
# # # TODO CHILD IN NEW POOL!!!!! NOt Equal to one
# print (f"new_pool {new_pool}")
# for existing_route in new_pool:
# if value != child:
# # print (f"Proposed {child} does not exist in pool so adding")
# route_dictionary = {"id": 0, "cities": child, "norm": 0, "distance": 0}
# # print (f'Adding route_dictionary {route_dictionary}')
# new_pool.append (route_dictionary)
# elif key == "cities" and value == child:
# # print (f"Proposed {child} exists in pool as {value}")
# break
p_counter += 1
print (f"only_routes {only_routes}")
# return new_pool
# for i in parent_2:
# if i not in parent_1:
# child_route.append (i)
# else:
# pass
# child = parent_1 + child_route
# if len(child) < num_cities:
# final_cities = []
# filler = list(range(num_cities))
# shuffle(filler)
# for i in filler:
# if i not in child:
# final_cities.append (i)
# else:
# pass
# # print(f'Child 2 {len(child)} {child}')
# child = child + final_cities
# for route in new_pool:
# for key, value in route.items():
# if key == "cities":
# if value == child:
# # print(f"Exists in pool")
# break
# else:
# # print (f"Does not exist in pool so adding")
# print (f"Adding {route_dictionary}")
# print(f"\nCounter {p_counter}")
# print(f"\nNew pool {len(new_pool)}")
# for _ in new_pool:
# print(_)
# shuffle(new_pool)
def mutate(chromosome):
"""Simple mutation function exchanging two cities so as not to create duplicate cities. """
random_cities = list (range (0, len (chromosome)))
x,y = random.sample (random_cities, 2)
chromosome["cities"][x], chromosome["cities"][y] = chromosome["cities"][y], chromosome["cities"][x]
return chromosome
# Mutation with probability of p_mutate
def mutate_pool(new_pool, p_mutate):
""" Mutate new_pool"""
for chromosome in new_pool:
int_random = random.uniform (0, 1)
if int_random <= p_mutate:
target_c = chromosome
mutate (target_c)
chromosome = target_c
new_pool.append (chromosome)
elif int_random > p_mutate:
pass
return new_pool
def run_genetic(runs, population, num_cities, num_elite, p_mutate):
"""Calls main functions."""
# INITIAL SETUP
startTime = time.time ()
global landscape
landscape=init_landscape(num_cities)
initial_pop=init_population(population,num_cities)
new_generation = list_to_dict (initial_pop)
run_count = 0
while run_count < runs:
print(f"\rRun {run_count}\n", end="")
measured_pop = measure_route (new_generation, num_cities)
print (f"\nmeasured_pop")
for _ in measured_pop:
print (f"{_}")
analysed = analyse_population(measured_pop)
print(f"\nanalysed {len(analysed)}")
for _ in analysed:
print(f"{_}")
current_elite = select_elite(analysed, num_elite)
print(f"\ncurrent_elite {len(current_elite)}")
for _ in current_elite:
print(f"{_}")
new_pool=single_crossover(current_elite, num_cities, population)
# print(f"\nnew_pool {new_pool}")
# for _ in new_pool:
# print(f"{_}")
# new_generation=mutate_pool(new_pool, p_mutate)
# print(f"New generation\n")
# for _ in new_generation:
# print(f"{_}")
run_count += 1
# sys.stdout.flush ()
endTime = time.time ()
totalTime = endTime - startTime
print(f"\rElapsed time {totalTime}\n", end="")
# CALL MAIN LOOP
run_genetic(runs=10, population=100, num_cities=10, num_elite=20, p_mutate=0.30)
#
# print (f"COMPLETE")
# # Use df and matplotlib to chart results
# min_distance_data.sort ()
# labels = ['run', 'min_dist', 'max_dist']
# df_min_dist = pd.DataFrame.from_records (min_distance_data, columns=labels)
# min = df_min_dist['min_dist'].min ()
# max = df_min_dist['max_dist'].max ()
# mean = df_min_dist.loc[:, "min_dist"].mean ()
# print (f"Stats Min {min} Max {max} Mean {mean}")
#
# ax = plt.gca ()
# plt.axis ([0, generations, 0, df_min_dist['max_dist'].max ()])
# df_min_dist.plot (kind='line', x='run', y='min_dist', ax=ax)
# plt.title ("TSP GA")
# plt.show ()