tsp_local.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. import sys
  2. import copy
  3. import math
  4. from heapq import heappush, heappop
  5. from random import shuffle, sample, randint, SystemRandom
  6. import matplotlib.pyplot as plt
  7. # globals to contain reference solutions
  8. neos14 = [318, 324, 336, 319, 351, 311, 272, 361, 274, 322]
  9. neos15 = [313, 318, 281, 324, 378, 291, 348, 342, 353, 325]
  10. neos16 = [404, 353, 361, 349, 358, 344, 373, 355, 243, 330]
  11. neos36 = 464
  12. neos = {14: neos14, 15: neos15, 16: neos16, 36: neos36}
  13. # returns a shuffled version of the cities list while maintaining the first & last elements
  14. def rand_order(cities):
  15. # shitty variable name
  16. suburbs = cities[1:-1]
  17. shuffle(suburbs)
  18. suburbs.insert(0, cities[0])
  19. suburbs.append(cities[-1])
  20. return suburbs
  21. # returns distance between 2 vertices
  22. def dist(city1, city2):
  23. return math.sqrt((city1[2] - city2[2]) ** 2 + (city1[3] - city2[3]) ** 2)
  24. # returns total cost/distance of a tour (list of cities)
  25. def length(cities):
  26. sum = 0
  27. for i in range(len(cities) - 1):
  28. sum += dist(cities[i], cities[i+1])
  29. return sum
  30. # returns the best ordered child state of a state (hill climbing, basic) & its cost
  31. def best_child(cities, cost):
  32. bestorder = cities
  33. bestcost = cost
  34. for i in range(1, len(cities) - 2):
  35. for j in range(i, len(cities) - 1):
  36. temporder = copy.deepcopy(cities)
  37. tempcity = temporder[i]
  38. temporder[i] = temporder[j]
  39. temporder[j] = tempcity
  40. templen = length(temporder)
  41. if (templen < cost):
  42. bestorder = temporder
  43. bestcost = templen
  44. return (bestorder, bestcost)
  45. # returns the best child using tabu
  46. # returns the best ordered child state of a state (hill climbing, basic) & its cost
  47. def bester_child(cities, cost, tabu):
  48. bestorder = cities
  49. bestcost = cost
  50. for i in range(1, len(cities) - 2):
  51. for j in range(i, len(cities) - 1):
  52. temporder = copy.deepcopy(cities)
  53. tempcity = temporder[i]
  54. temporder[i] = temporder[j]
  55. temporder[j] = tempcity
  56. if (get_hashable(temporder) in tabu):
  57. continue
  58. templen = length(temporder)
  59. if (templen < cost):
  60. bestorder = temporder
  61. bestcost = templen
  62. return (bestorder, bestcost)
  63. # returns a random child for simulated annealing search
  64. # returns the ordering and the cost of it
  65. def random_child(cities):
  66. #i = randbelow(len(cities) - 3) + 1
  67. i = randint(1, len(cities) - 2)
  68. j = i
  69. while (j == i):
  70. #j = randbelow(len(cities) - 3) + 1
  71. j = randint(1, len(cities) - 2)
  72. temporder = copy.deepcopy(cities)
  73. tempcity = temporder[i]
  74. temporder[i] = temporder[j]
  75. temporder[j] = tempcity
  76. return(temporder, length(temporder))
  77. # returns the basic hill climbing solution for a given input
  78. def sir_edmund_hillary(cities):
  79. curcost = length(cities)
  80. curpath = copy.deepcopy(cities)
  81. steps = 0
  82. # keep trying things til we can no longer try things
  83. while(1):
  84. steps += 1
  85. child = best_child(curpath, curcost)
  86. if (child[1] < curcost):
  87. curcost = child[1]
  88. curpath = child[0]
  89. continue
  90. break
  91. return(curpath, curcost, steps)
  92. # retuns a tuple of the indices (ordered) in a list of cities
  93. def get_hashable(cities):
  94. newlist = []
  95. for i in range(1, len(cities) - 1):
  96. newlist.append(cities[i][1])
  97. return tuple(newlist)
  98. # returns the hill climbing solution w/tabu & sideways moves for a given input
  99. def tenzing_norgay(cities):
  100. curcost = length(cities)
  101. curpath = copy.deepcopy(cities)
  102. tabu = {get_hashable(cities)}
  103. laterals = 0
  104. # keep trying things til we can no longer try things
  105. while(1):
  106. child = bester_child(curpath, curcost, tabu)
  107. if (child[1] < curcost):
  108. curcost = child[1]
  109. curpath = child[0]
  110. tabu.add(get_hashable(curpath))
  111. continue
  112. if (child[1] == curcost and laterals < n):
  113. laterals += 1
  114. curcost = child[1]
  115. curpath = child[0]
  116. tabu.add(get_hashable(curpath))
  117. continue
  118. break
  119. return(curpath, curcost)
  120. # returns the hill climbing solution w/ x random restarts for a given input
  121. # if you want x restarts, call with x + 1
  122. def reinhold_messner(cities, x):
  123. curcost = length(cities)
  124. curpath = copy.deepcopy(cities)
  125. bestcost = length(cities)
  126. bestpath = copy.deepcopy(cities)
  127. # keep trying things til we can no longer try things
  128. while(x):
  129. child = best_child(curpath, curcost)
  130. if (child[1] < curcost):
  131. curcost = child[1]
  132. curpath = child[0]
  133. continue
  134. else:
  135. if (curcost < bestcost):
  136. bestcost = curcost
  137. bestpath = copy.deepcopy(curpath)
  138. curpath = rand_order(cities)
  139. curcost = length(curpath)
  140. x -= 1
  141. return(bestpath, bestcost)
  142. # returns true or false randomly, based on the distribution e^(delta / temp)
  143. def bad_weather(delta, temp):
  144. return random.SystemRandom().random() < math.exp(delta / temp)
  145. # decrements temp by the option specified by opt: 1 is linear, 2 is logarithmic, 3 is exponential
  146. def colder(temp, opt):
  147. if (opt == 1):
  148. return temp - 25
  149. if (opt == 2):
  150. return temp - (math.log(temp) + 2)
  151. else:
  152. return temp - math.exp(-0.001 * temp)
  153. # returns the hill climbing solution using simulated annealing w/ schedule opt
  154. def beck_weathers(cities, opt):
  155. temp = 1000
  156. curcost = length(cities)
  157. curpath = copy.deepcopy(cities)
  158. # keep trying things til we can no longer try things
  159. while(temp > 0):
  160. child = random_child(curpath)
  161. delta = curcost - child[1]
  162. if (delta > 0 or bad_weather(delta, temp)):
  163. curcost = child[1]
  164. curpath = child[0]
  165. temp = colder(temp, opt)
  166. return(curpath, curcost)
  167. # solves the inputted TSP problem (filepath), using algorithm algo
  168. # 1 = hill climbing, 2 = tabu/sideways allowed, 3 = random restarts, 4 = annealing
  169. # x represents the optional parameters for 3 and 4
  170. # specifically, the # of restarts and the annealing schedule, respectively
  171. def solver(problem, algo, *x):
  172. # import map
  173. prob = problem
  174. with open(prob) as file:
  175. prob = file.read().splitlines()
  176. global n
  177. cities = prob[1:]
  178. counter = 0
  179. # convert to list of list of ints
  180. # original format is: letter, coord, coord
  181. # output format is: iD#, letter, coord, coord
  182. for l in cities:
  183. temp = l.split()
  184. temp[1] = int(temp[1])
  185. temp[2] = int(temp[2])
  186. temp.insert(0, counter)
  187. counter += 1
  188. cities[cities.index(l)] = temp
  189. # add in goal state
  190. cities.append(cities[0])
  191. input = rand_order(cities)
  192. if (algo == 1):
  193. result = sir_edmund_hillary(cities)
  194. elif (algo == 2):
  195. result = tenzing_norgay(cities)
  196. elif (algo == 3):
  197. result = reinhold_messner(cities, x)
  198. else:
  199. result = beck_weathers(cities, x)
  200. return result
  201. # main function to get aggregate data and graph stuff
  202. def main():
  203. plt.ioff()
  204. plt.switch_backend('agg')
  205. solution_steps = []
  206. solution_scores = []
  207. good_sol_counts = []
  208. for i in range(14, 17):
  209. steps = []
  210. solution_steps.append(steps)
  211. scores = []
  212. solution_scores.append(scores)
  213. good_sols = []
  214. good_sol_counts.append(good_sols)
  215. for j in range(1, 11):
  216. filepath = "tsp_problems/" + str(i) + "/instance_" + str(j) + ".txt"
  217. prob_steps = 0
  218. prob_scores = 0
  219. good_solutions = 0
  220. # run problem 100 times
  221. for k in range(0, 100):
  222. # path, cost, steps returned
  223. result = solver(filepath, 1)
  224. prob_steps += result[2]
  225. prob_scores += (result[1] / neos[i][j - 1])
  226. if (result[1] <= neos[i][j- 1]):
  227. good_solutions += 1
  228. steps.append(prob_steps / 100.0)
  229. scores.append(prob_scores / 100.0)
  230. good_sols.append(good_solutions)
  231. figure, axes = plt.subplots(3, 2, True)
  232. for i in range(0, 3):
  233. axes[i][0].plot(range(1, 11), solution_steps[i], label = "Steps to Solution", color = 'b')
  234. axes[i][0].set_xlabel("Problem Instance w/ " + str(i + 14) + " cities")
  235. axes[i][0].set_ylabel("Steps Taken", color = 'b')
  236. axis_twin = axes[i].twinx()
  237. axis_twin.plot(range(1, 11), solution_scores[i], label = "Solution Quality", color = 'r')
  238. axis_twin.set_ylabel("Solution Quality", color = 'r')
  239. #axes[i].legend()
  240. #axis_twin.legend()
  241. for i in range(0, 3):
  242. axes[i][1].plot(range(1, 11), good_sol_counts[i], color = 'b')
  243. axes[i][1].set_xlabel("Problem Instance w/ " + str(i + 14) + " cities")
  244. axes[i][1].set_ylabel("% of runs <= NEOS", color = 'b')
  245. plt.savefig("hill_climbing.pdf")
  246. main()