roninkoi

Simulated annealing traveling salesman

Jun 11th, 2023
1,137
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 7.55 KB | Science | 0 0
  1. #!/bin/env python3
  2. # Solve travelling salesman problem using simulated annealing with Metropolis Monte Carlo
  3. # Usage: ./annealed_salesman.py [input.dat] [T0] [nsteps] [show]
  4. # input.dat = input file of location coordinates with names
  5. # T0 = starting temperature for simulated annealing
  6. # nsteps = number of MC steps
  7. # show = if 0, don't show plots
  8.  
  9. import numpy as np
  10. from matplotlib import rc
  11. import matplotlib.pyplot as plt
  12. import matplotlib.ticker as mtick
  13. import sys
  14.  
  15. # line + scatter plot of multiple data sets a
  16. # with different labels and styles
  17. def linescatter(a, titles=["", "", ""], labels=[], styles=[], fpath="", show=True):
  18.     plt.style.use('default')
  19.     rc('font', **{'family': 'serif', 'serif': ['Computer Modern']})
  20.     rc('text', usetex=True)
  21.     plt.rcParams.update({'font.size': 22})
  22.     #colors = plt.cm.hsv(np.linspace(0.66, 0.0, len(a)))
  23.     #colors = plt.cm.plasma(np.linspace(0, 0.95, len(a)))
  24.     if len(a) > 5:
  25.         colors = plt.cm.plasma(np.linspace(0, 0.95, len(a)))
  26.         plt.rcParams['axes.prop_cycle'] = plt.cycler(color=colors)
  27.     f = plt.figure(figsize=(10, 10))
  28.     ax = plt.gca()
  29.     ax.yaxis.set_major_formatter(mtick.FormatStrFormatter('%.5g'))
  30.  
  31.     i = 0
  32.     for ai in a:
  33.         alabel = str(i)
  34.         amarker = ""
  35.         alinestyle = "-"
  36.         amarkersize = 1.0
  37.        
  38.         if len(labels) > 0:
  39.             alabel = labels[i]
  40.            
  41.         if len(styles) > 0:
  42.             if styles[i] == 0:
  43.                 amarker = ""
  44.                 alinestyle = "-"
  45.                 amarkersize = 1.0
  46.             if styles[i] == 1:
  47.                 amarker = "o"
  48.                 alinestyle = ""
  49.                 amarkersize = 5.0
  50.                
  51.         plt.plot(ai[:, 0], ai[:, 1], label=alabel, marker=amarker, markersize=amarkersize, linestyle=alinestyle)
  52.         i += 1
  53.        
  54.     plt.title(titles[0])
  55.     plt.xlabel(titles[1])
  56.     plt.ylabel(titles[2])
  57.    
  58.     plt.grid(True)
  59.  
  60.     if len(labels) > 0 or (len(a) > 1 and len(a) <= 10):
  61.         plt.legend(fontsize='xx-small')
  62.  
  63.     if len(fpath) > 0:
  64.         f.savefig(fpath, bbox_inches='tight') # save to file
  65.  
  66.     if show:
  67.         plt.show()
  68.         plt.close()
  69.    
  70.  
  71. # generate random path with n points
  72. def randpath(n):
  73.     path = np.arange(1, n-1) # generate middle path points
  74.     np.random.shuffle(path) # shuffle points randomly
  75.     path = np.concatenate([[0], path]) # add starting point
  76.     path = np.concatenate([path, [n-1]]) # add end point
  77.     return path
  78.  
  79. # calculate total path length from pos with path indices
  80. def pathlength(pos, path):
  81.     l = pos[path][1:] - pos[path][:-1] # position differences
  82.     l = np.sum(np.linalg.norm(l, axis=1)) # calculate distance by summing path lengths
  83.     return l
  84.  
  85. # return new path with two points swapped
  86. def randswap(path):
  87.     n = len(path)
  88.     swap1, swap2 = np.random.choice(path, 2, replace=False) # pick two random points that are not the same
  89.  
  90.     if swap1 == 0 or swap1 == n-1: # don't swap start or end point
  91.         return path
  92.     if swap2 == 0 or swap2 == n-1:
  93.         return path
  94.    
  95.     newpath = np.copy(path)
  96.     newpath[swap1] = path[swap2] # swap points in path
  97.     newpath[swap2] = path[swap1]
  98.  
  99.     return newpath
  100.  
  101. # Solve travelling salesman problem using simulated annealing
  102. # Position array indexed using path
  103. # Tprof defines shape of temperature profile, length is number of MC steps
  104. # wfreq is write frequency for printing and plotting, show=0 disables plots
  105. def tsp(pos, path, Tprof, output):
  106.     i = 0 # number of MC steps
  107.     if path is None:
  108.         path = randpath(len(pos)) # initial random path
  109.     wfreq, show = output
  110.  
  111.     lmin = pathlength(pos, path) # minimum path length
  112.     minpath = path # indices for minimum path
  113.     for T in Tprof:
  114.         l = pathlength(pos, path) # calculate current path length
  115.         newpath = randswap(path) # generate trial path by swapping
  116.         dl = pathlength(pos, newpath) - l # difference in length
  117.         p = 0.
  118.         if T > 0.:
  119.             p = np.exp(-dl / T) # use temperature for probability
  120.         i += 1
  121.        
  122.         if show and i % wfreq == 0: # plotting
  123.             plt.gca().set_aspect('equal')
  124.             plt.plot(pos[path][:, 0], pos[path][:, 1], marker = 'o')
  125.             plt.pause(0.05)
  126.             plt.cla()
  127.         if i % wfreq == 0: # printing
  128.             print("i:", i, "T:", T, "l:", l, "dl:", dl)
  129.            
  130.         if dl < 0. or np.random.random() < p: # accept trial path
  131.             path = newpath
  132.         if l < lmin:
  133.             lmin = l
  134.             minpath = path
  135.  
  136.     return lmin, minpath
  137.  
  138. show = True
  139. dfpath = "input.dat" # data file for city coordinates
  140. T0 = 10000. # starting temperature
  141. nsteps = 100000 # number of MC steps
  142.  
  143. df = ["279.9  35.1 Helsinki",
  144.       "271.1  33.5 Espoo",
  145.       "227.9 171.1 Tampere",
  146.       "266.3  56.4 Vantaa",
  147.       "141.9  65.6 Turku",
  148.       "340.1 563.9 Oulu",
  149.       "325.2 116.8 Lahti",
  150.       "437.7 328.8 Kuopio",
  151.       "338.1 251.6 Jyväskylä",
  152.       "128.1 183.4 Pori",
  153.       "460.4 121.4 Lappeenranta",
  154.       "138.3 363.9 Vaasa",
  155.       "391.0  68.3 Kotka",
  156.       "540.7 297.2 Joensuu",
  157.       "417.9 197.6 Mikkeli",
  158.       "262.1 121.4 Hämeenlinna",
  159.       "334.8  60.5 Porvoo",
  160.       "282.5  80.7 Hyvinkää",
  161.       "113.6 152.1 Rauma",
  162.       "435.0 472.8 Kajaani"]
  163.  
  164. if len(sys.argv) > 1:
  165.     dfpath = sys.argv[1]
  166.     df = open(dfpath)
  167. if len(sys.argv) > 2:
  168.     T0 = float(sys.argv[2])
  169. if len(sys.argv) > 3:
  170.     nsteps = int(sys.argv[3])
  171. if len(sys.argv) > 4:
  172.     show = bool(int(sys.argv[4]))
  173.  
  174. pos = [] # xy positions of cities
  175. cities = [] # city names
  176. for l in df:
  177.     words = l.split()
  178.     pos.append(np.array([float(words[0]), float(words[1])])) # xy coordinates
  179.     cities.append(words[2]) # labels
  180. pos.append(pos[0]) # add line segment from last point to first point -> closed path
  181. cities.append(cities[0])
  182.  
  183. pos = np.array(pos)
  184. print("Path:", cities)
  185. n = len(pos)
  186. print("Number of points:", n)
  187.  
  188. path = None # shortest path (indices for pos)
  189. tstep = 0 # temperature step
  190. temps = [] # temperature profiles
  191. lengths = [] # path lengths
  192. Te = 20. # end temperature reduction factor
  193. Tpow = 0.5 # temperature profile power (linear, square, square root, ...)
  194. Tf = 2.0 # T1 reduction factor
  195. lmin = pathlength(pos, np.arange(0, n))
  196. Teps = 0.001 # stop annealing when T below
  197. for i in range(1000):
  198.     print("Annealing pass", i+1)
  199.     T0 /= Tf # reduce temperature
  200.     if T0 < Teps:
  201.         break
  202.     Tmin = T0/Te # end point of temperature range
  203.     temp = (np.linspace(T0**Tpow, Tmin**Tpow, nsteps))**(1./Tpow) # generate temperatures in range [T0, Tmin] with shape of T**(1/Tpow)
  204.     temps.append(np.array([np.arange(tstep, tstep+len(temp)), temp]).T)
  205.    
  206.     l, path = tsp(pos, path, temp, [10000, show]) # run MCC
  207.     lengths.append(np.array([tstep, l]))
  208.     tstep += nsteps
  209.     print("l:", l, "lmin:", lmin)
  210.     if l > lmin:
  211.         T0 *= Tf # return previous temperature
  212.     if l < lmin:
  213.         lmin = l
  214.  
  215. minpath = pos[path] # positions sorted using minimum path
  216. lengths = np.array(lengths)
  217. citypos = [minpath]
  218. for p in pos:
  219.     citypos.append(np.array([p]))
  220. cities = ["Path"] + cities
  221. styles = np.insert(np.repeat(1, n), 0, 0)
  222. if n > 30:
  223.     cities = []
  224. linescatter(citypos, styles=styles, labels=cities, titles=["Shortest path", "$x$", "$y$"], fpath="path.pdf", show=show)
  225. linescatter(temps, titles=["Temperature profile", "Step", "$T$"], fpath="temps.pdf", show=show)
  226. linescatter([lengths], titles=["Path length", "Step", "$L$"], fpath="lengths.pdf", show=show)
  227.  
  228. print("Path length (km):", l)
  229.  
Advertisement
Add Comment
Please, Sign In to add comment