├── TSP_snap2.png ├── README.md └── TSP_comparism_v10.py /TSP_snap2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RandyWaterhouse/TSP_simulated_annealing/HEAD/TSP_snap2.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TSP_simulated_annealing 2 | 3 | Here I provide a Python 2.7 code which determines approximate solutions to the Travelling Salesman Problem (TSP) by direct sampling and by simulated annealing. The progress of the two solutions is shown simultaneously in a pygame graphics window. There are several controls to pause/restart the simulation, save the current state as an image and produce a plot of minimal tour length versus iteration number. 4 | 5 | The number of cities may be chosen from a pre-defined list (10, 20, 50, ...). If a new number of cities is selected, the simulation is restarted with that number of cities placed randomly on the map. 6 | 7 | For details on the method simulated annealing, see e.g. the Wiki article (https://en.wikipedia.org/wiki/Simulated_annealing). 8 | 9 | Please feel free to download, run and modify the code. It requires Python 2.7, pygame, matplotlib and some standard packages like random, math, timeit, time, datetime. 10 | 11 | Some keyboard shortcuts: 12 | p: pause 13 | c: continue 14 | q, ESC: quit 15 | 16 | A video demo of this simulation can be found in my YouTube account: https://www.youtube.com/watch?v=2iBR8v2i0pM&index=5&list=PLlnnSiqU0W2SuojKlqQ0yxowK9VqeQKmF 17 | 18 | Screenshot: 19 | ![Screenshot](TSP_snap2.png) 20 | 21 | -------------------------------------------------------------------------------- /TSP_comparism_v10.py: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # 3 | # TSP (Travelling Salesman Problem), interactive simulation, uses 4 | # direct sampling and simulated annealing to find (or approach...) 5 | # solution 6 | # 7 | # Peter U., July/August 2017 8 | # 9 | # Version 1.0 10 | # 11 | ######################################################################## 12 | # 13 | # Import packages: 14 | import random 15 | import math 16 | import pygame 17 | from pygame.locals import * 18 | import timeit 19 | import time 20 | import datetime 21 | import matplotlib.pyplot as plt 22 | 23 | ######################################################################## 24 | # Global settings: 25 | SIZE = 600 # size of display window for single instance 26 | STATUS_HEIGHT = 80 # height of status bar in display window 27 | STATUS_HEIGHT2 = 30 # height of status bar within instance subwindows 28 | STATUS_HEIGHT3 = 45 # height of status bar at the bottom (Github info) 29 | DELIM_WIDTH = 5 # width of delimiter between direct and sim. ann. output 30 | CITY_RADIUS = 5 # radius of circle representing city 31 | FONTSIZE = 20 # font size for control section buttons 32 | VERBOSE = False # level of chattiness 33 | SAVEPLOT = True # save plot of tour length vs iteration (True) or only display it (False) 34 | SLEEP = 0 # delay (in seconds) after plotting new configuration 35 | N = 20 # initial number of cities 36 | SEED = None # random seed 37 | VERSION = "1.0" # version 38 | 39 | 40 | ######################################################################## 41 | # define colors for graphics output: 42 | COLORS = {"WHITE": (255, 255, 255), "RED": (255, 0, 0), "GREEN": (0, 255, 0), 43 | "BLUE": (0, 0, 255), "BLACK": (0, 0, 0), "YELLOW": (255, 255, 0), 44 | "LIGHT_BLUE": (0, 125, 227), "GREY1": (120, 120, 120), 45 | "GREY2": (224, 224, 224), "LIGHTBLUE": (102, 178, 255), 46 | "LIGHTRED": (255, 153, 153), "LIGHTYELLOW": (255, 255, 153), 47 | "PINK": (255, 51, 255), "DARKBLUE": (0, 0, 153), 48 | "LAVENDER": (204, 153, 255), "LIGHTGREEN": (153, 255, 204), 49 | "BROWN": (102, 51, 0), "OLIVE": (153, 153, 0), "DARKGREY": (105, 105, 105)} 50 | 51 | 52 | ######################################################################## 53 | # Helper functions: 54 | def distance(x, y): 55 | # get Euclidean distance between two points (cities) in 2D space 56 | return math.sqrt((x[0] - y[0])**2 + (x[1] - y[1])**2) 57 | 58 | def tour_length(cities, N): 59 | # get total tour length for current sequence of cities 60 | assert len(cities) == N, "WTF?? " + str(len(cities)) + " vs " + str(N) 61 | return sum(distance(cities[k + 1], cities[k]) for k in range(N - 1)) + distance(cities[0], cities[N - 1]) 62 | 63 | def draw_text(surface, font, text, position, color): 64 | # draw user-defined text in pygame graphics surface 65 | lable = font.render(text, 1, color) 66 | surface.blit(lable, position) 67 | 68 | def generate_cities(N): 69 | # generate positions for N cities randomly in range .025 <= x <= .975 70 | # and .025 <= y <= .975 (leave margins at all sides for aesthetics 71 | # reasons) 72 | random.seed(SEED) 73 | cities = [(.025 + random.uniform(0.0, 0.95), .025 + random.uniform(0.0, 0.95)) for i in range(N)] 74 | random.seed() 75 | return cities[:] 76 | 77 | def change_N(N): 78 | # change number of cities, so various variables have to be reset 79 | iters, siters, diters = 0, 0, 0 80 | d_energy_min, s_energy_min = float('inf'), 10000. 81 | beta, n_accept = 1.0, 0 82 | dcities = generate_cities(N) 83 | scities = dcities[:] 84 | if VERBOSE: print "Simulating", N, "cities." 85 | return N, dcities, scities, iters, siters, diters, d_energy_min, s_energy_min, beta, n_accept, timeit.default_timer(), {"direct": {"iters": [], "lengths": []}, "simann": {"iters": [], "lengths": []}} 86 | 87 | def get_filename(N, iters): 88 | # generate file (without extension), 89 | # contains number of cities, iteration number and timestamp 90 | filename = "Images/TSP_comparism_N" + str(N) + "_iters" + str(iters) + "_" + \ 91 | datetime.datetime.now().strftime("%Y%m%d-%H%M%S") 92 | return filename 93 | 94 | def save_image(surface, N, iters): 95 | # save current graphics surface as image (PNG format); filename 96 | filename = get_filename(N, iters) + "_map.png" 97 | pygame.image.save(surface, filename) 98 | return filename 99 | 100 | def make_plot(plot_data, N, iters): 101 | # generate plot of minimal tour length vs iteration number for both 102 | # direct sampling and simulated annealing; plot has log-log-scale; 103 | # plot is saved to file with filename containing N, iteration and 104 | # timestamp 105 | filename = get_filename(N, iters) + "_plot.png" 106 | fig = plt.figure(figsize=(10, 10)) 107 | ax = fig.add_subplot(1, 1, 1) 108 | ax.set_xscale("log") 109 | ax.set_yscale("log") 110 | xmax = max(plot_data["direct"]["iters"][-1], plot_data["simann"]["iters"][-1]) 111 | x1 = plot_data["direct"]["iters"] 112 | y1 = plot_data["direct"]["lengths"] 113 | x2 = plot_data["simann"]["iters"] 114 | y2 = plot_data["simann"]["lengths"] 115 | if x1[-1] < x2[-1]: 116 | x1.append(x2[-1]) 117 | y1.append(y1[-1]) 118 | elif x1[-1] > x2[-1]: 119 | x2.append(x1[-1]) 120 | y2.append(y2[-1]) 121 | plt.plot(x1, y1, color="red", lw=2, label="direct sampling") 122 | plt.plot(x2, y2, color="blue", lw=2, label="simulated annealing") 123 | plt.legend(loc=3, fontsize=16) 124 | plt.xlabel("iteration", fontsize=16) 125 | plt.ylabel("minimal tour length", fontsize=16) 126 | plt.title("Tour length vs iteration", fontsize=20) 127 | if SAVEPLOT: 128 | plt.savefig(filename) 129 | plt.show() 130 | return filename 131 | 132 | 133 | ######################################################################## 134 | # Initialisation: 135 | start_timer = timeit.default_timer() 136 | pygame.init() 137 | helv20 = pygame.font.SysFont("Helvetica", 20) 138 | helv24 = pygame.font.SysFont("Helvetica", 24) 139 | # start clock: 140 | fpsClock = pygame.time.Clock() 141 | # set display surface for pygame: 142 | SWIDTH = 2 * SIZE + DELIM_WIDTH 143 | SHEIGHT = SIZE + STATUS_HEIGHT + STATUS_HEIGHT2 + STATUS_HEIGHT3 144 | print SWIDTH, SHEIGHT 145 | surface = pygame.display.set_mode((SWIDTH, SHEIGHT)) 146 | surface.set_alpha(None) 147 | pygame.display.set_caption("TSP: direct sampling vs simulated annealing, v" + VERSION) 148 | dcities = generate_cities(N) # cities for direct sampling 149 | scities = dcities[:] # cities for simulated annealing 150 | 151 | 152 | 153 | ###################################################################### 154 | # Button class for control section, PyGame doesn't have ready-to-use 155 | # buttons or similar: 156 | class Button(): 157 | 158 | def __init__(self, width, height, text, color, tcolor): 159 | self.width = width 160 | self.height = height 161 | self.text = text 162 | self.color = color 163 | self.tcolor = tcolor 164 | 165 | def SetText(self, text): 166 | self.text = text 167 | 168 | def PlaceButton(self, surface, x, y): 169 | self.x = x 170 | self.y = y 171 | surface = self.DrawButton(surface, x, y) 172 | surface = self.ButtonText(surface, x, y) 173 | 174 | def DrawButton(self, surface, x, y): 175 | pygame.draw.rect(surface, self.color, (x, y, self.width, self.height), 0) 176 | return surface 177 | 178 | def ButtonText(self, surface, x, y): 179 | font_size = int(self.width // len(self.text)) 180 | font = pygame.font.SysFont("Arial", FONTSIZE) 181 | text = font.render(self.text, 1, self.tcolor) 182 | surface.blit(text, ((x + self.width/2) - text.get_width()/2, (y + self.height/2) - text.get_height()/2)) 183 | return surface 184 | 185 | def IsPressed(self, mouse): 186 | return mouse[0] > self.x and \ 187 | mouse[1] > self.y and \ 188 | mouse[0] <= self.x + self.width and \ 189 | mouse[1] < self.y + self.height 190 | 191 | ######################################################################## 192 | # simulation step for direct sampling, just randomly shuffle cities, 193 | # i.e. previous configuration has to impact whatsoever on next configuration: 194 | def direct_sampling(cities): 195 | random.shuffle(cities) 196 | return cities 197 | 198 | 199 | ######################################################################## 200 | # simulation step for simulated annealing: 201 | # 202 | # The main part of the code for this function was provided by Werner Krauth 203 | # and his team from Ecole Normale Superieure as part of the course 204 | # "Statistical Mechanics - Algorithms and Computations" which was hosted 205 | # on the Coursera Platform (https://www.coursera.org/course/smac) 206 | # For an explanation of the simulated annealing method see 207 | # https://en.wikipedia.org/wiki/Simulated_annealing 208 | # Basically, in each iteration a "neighbouring" route is chosen and: 209 | # - if it has lower energy than the current route (i.e. is shorter): it is 210 | # always accepted 211 | # - if it has higher energy than the current route (i.e. is longer): it is 212 | # accepted with some probability p which depends on the difference in energies 213 | # and a "temperature" which slowly decreases during the simulation. The 214 | # parameter beta is basically the inverse temperature 215 | # 216 | def simulated_annealing(N, cities, beta, n_accept, best_energy): 217 | energy = tour_length(cities, N) 218 | new_route = False 219 | if n_accept >= 100 * math.log(N): 220 | beta *= 1.005 221 | n_accept = 0 222 | p = random.uniform(0.0, 1.0) 223 | if p < 0.2: 224 | # cut sequence somewhere in first half, swap first and second part, 225 | # cut again at new point in first half and reverse first part 226 | i = random.randint(0, N / 2) 227 | cities = cities[i:] + cities[:i] 228 | i = random.randint(0, N / 2) 229 | a = cities[:i] 230 | a.reverse() 231 | new_cities = a + cities[i:] 232 | elif p < 0.6: 233 | # move randomly chosen city to a randomly chosen new position in sequence 234 | new_cities = cities[:] 235 | i = random.randint(1, N - 1) 236 | a = new_cities.pop(i) 237 | j = random.randint(1, N - 2) 238 | new_cities.insert(j, a) 239 | else: 240 | # swap two randomly chosen cities 241 | new_cities = cities[:] 242 | i = random.randint(1, N - 1) 243 | j = random.randint(1, N - 1) 244 | new_cities[i] = cities[j] 245 | new_cities[j] = cities[i] 246 | new_energy = tour_length(new_cities, N) 247 | if random.uniform(0.0, 1.0) < math.exp(- beta * (new_energy - energy)): 248 | # accept new route with probability depending on difference in 249 | # tour length (new - current) and parameter beta 250 | n_accept += 1 251 | energy = new_energy 252 | cities = new_cities[:] 253 | if energy < best_energy: 254 | best_energy = energy 255 | best_tour = cities[:] 256 | new_route = True 257 | return cities, beta, n_accept, best_energy, new_route 258 | 259 | 260 | ######################################################################## 261 | # Main loop: 262 | def mainloop(surface, N, dcities, scities, start_timer): 263 | # main loop, checks user actions, does simulation step, does graphics 264 | # output if necessary 265 | d_energy_min = float('inf') 266 | s_energy_min = 10000 267 | running = True 268 | iters, siters, diters = 0, 0, 0 269 | start = timeit.default_timer() 270 | speed = 0 271 | # parameters for simulated annealing: 272 | beta = 1.0 # inverse temperature 273 | n_accept = 0 274 | # ploting data: 275 | plot_data = {"direct": {"iters": [], "lengths": []}, "simann": {"iters": [], "lengths": []}} 276 | 277 | # define buttons for user control: 278 | button_ncity_10 = Button(50, 30, "10", COLORS["LIGHTBLUE"], COLORS["BLACK"]) 279 | button_ncity_20 = Button(50, 30, "20", COLORS["LIGHTBLUE"], COLORS["BLACK"]) 280 | button_ncity_50 = Button(50, 30, "50", COLORS["LIGHTBLUE"], COLORS["BLACK"]) 281 | button_ncity_100 = Button(50, 30, "100", COLORS["LIGHTBLUE"], COLORS["BLACK"]) 282 | button_ncity_200 = Button(50, 30, "200", COLORS["LIGHTBLUE"], COLORS["BLACK"]) 283 | button_ncity_500 = Button(50, 30, "500", COLORS["LIGHTBLUE"], COLORS["BLACK"]) 284 | button_quit = Button(60, 30, "Quit", COLORS["RED"], COLORS["BLACK"]) 285 | button_pause = Button(60, 30, "Pause", COLORS["LIGHTBLUE"], COLORS["BLACK"]) 286 | button_continue = Button(60, 30, "Cont.", COLORS["GREEN"], COLORS["BLACK"]) 287 | button_save = Button(60, 30, "Save", COLORS["LAVENDER"], COLORS["BLACK"]) 288 | button_plot = Button(60, 30, "Plot", COLORS["LAVENDER"], COLORS["BLACK"]) 289 | 290 | # loop until user event: 291 | while True: 292 | 293 | # Event handler: 294 | for event in pygame.event.get(): 295 | # pygame event handler 296 | if event.type == QUIT: 297 | # graphics window is closed 298 | pygame.quit() 299 | return 300 | elif event.type == KEYDOWN: 301 | # key is pressed 302 | if event.key in [K_ESCAPE, K_q]: 303 | # 'q' or ESC will quit program 304 | pygame.quit() 305 | return 306 | elif event.key == K_c: 307 | # 'c' continues simulation 308 | running = True 309 | elif event.key == K_p: 310 | # 'p' pauses simulation 311 | running = False 312 | elif event.type == MOUSEBUTTONDOWN: 313 | # mouse button is pressed 314 | if button_ncity_10.IsPressed(pygame.mouse.get_pos()): 315 | # N = 10 selected 316 | N, dcities, scities, iters, siters, diters, d_energy_min, s_energy_min, beta, n_accept, start, plot_data = change_N(10) 317 | elif button_ncity_20.IsPressed(pygame.mouse.get_pos()): 318 | # N = 20 selected 319 | N, dcities, scities, iters, siters, diters, d_energy_min, s_energy_min, beta, n_accept, start, plot_data = change_N(20) 320 | elif button_ncity_50.IsPressed(pygame.mouse.get_pos()): 321 | # N = 50 selected 322 | N, dcities, scities, iters, siters, diters, d_energy_min, s_energy_min, beta, n_accept, start, plot_data = change_N(50) 323 | elif button_ncity_100.IsPressed(pygame.mouse.get_pos()): 324 | # N = 100 selected 325 | N, dcities, scities, iters, siters, diters, d_energy_min, s_energy_min, beta, n_accept, start, plot_data = change_N(100) 326 | elif button_ncity_200.IsPressed(pygame.mouse.get_pos()): 327 | # N = 200 selected 328 | N, dcities, scities, iters, siters, diters, d_energy_min, s_energy_min, beta, n_accept, start, plot_data = change_N(200) 329 | elif button_ncity_500.IsPressed(pygame.mouse.get_pos()): 330 | # N = 500 selected 331 | N, dcities, scities, iters, siters, diters, d_energy_min, s_energy_min, beta, n_accept, start, plot_data = change_N(500) 332 | elif button_quit.IsPressed(pygame.mouse.get_pos()): 333 | # 'Quit' selected 334 | if VERBOSE: print "Quitting..." 335 | pygame.quit() 336 | return 337 | elif button_continue.IsPressed(pygame.mouse.get_pos()): 338 | # 'Continue' selected, simulation continues 339 | if VERBOSE: print "Continuing..." 340 | running = True 341 | elif button_pause.IsPressed(pygame.mouse.get_pos()): 342 | # 'Pause' selected, simulation is halted 343 | if VERBOSE: print "Simulation paused." 344 | running = False 345 | elif button_save.IsPressed(pygame.mouse.get_pos()): 346 | # 'Save' selected, image will be saved 347 | filename = save_image(surface, N, iters) 348 | if VERBOSE: print "Image saved, filename:", filename 349 | elif button_plot.IsPressed(pygame.mouse.get_pos()): 350 | # 'Plot' selected, generate plot of tour length vs iteration 351 | filename = make_plot(plot_data, N, iters) 352 | if VERBOSE: print "Plot generated, filename:", filename 353 | 354 | if not running: 355 | # if simulation is paused, skip rest of mainloop 356 | continue 357 | if VERBOSE and iters % 10000 == 0: 358 | print "N/iters/beta/s_energy_min =", N, iters, beta, round(s_energy_min, 3) 359 | iters += 1 # iteration counter 360 | 361 | change = False 362 | # generate new route by direct sampling: 363 | new_dcities = direct_sampling(dcities[:]) 364 | d_energy = tour_length(new_dcities, N) 365 | if d_energy < d_energy_min: 366 | d_energy_min = d_energy 367 | if VERBOSE: 368 | print "Tour length direct sampling:", d_energy_min, "at iteration", iters 369 | dcities = new_dcities[:] 370 | diters = iters 371 | change = True 372 | plot_data["direct"]["iters"].append(iters) 373 | plot_data["direct"]["lengths"].append(d_energy_min) 374 | # generate new route by simulated annealing: 375 | new_scities, beta, n_accept, s_energy_min, new_route = simulated_annealing(N, scities[:], beta, n_accept, s_energy_min) 376 | if new_route: 377 | if VERBOSE: 378 | print "Tour length simulated annealing:", s_energy_min, "at iteration", iters 379 | scities = new_scities[:] 380 | siters = iters 381 | change = True 382 | plot_data["simann"]["iters"].append(iters) 383 | plot_data["simann"]["lengths"].append(s_energy_min) 384 | 385 | 386 | if iters % 1000 == 0: 387 | # every 1k iterations we will plot current configuration even if 388 | # it hasn't changed 389 | change = True 390 | 391 | if change: 392 | 393 | # calculate simulation speed: 394 | delta_iter = 100000 // N 395 | show_speed = iters % delta_iter == 0 396 | if show_speed: 397 | T = timeit.default_timer() - start 398 | start = timeit.default_timer() 399 | speed = delta_iter / T 400 | # only generate graphics output if new route is present for 401 | # direct sampling and/or simulated annealing or if iteration 402 | # count is divisible by 1000 403 | # 404 | # buttons and text elements: 405 | surface.fill(COLORS["WHITE"]) 406 | surface.fill(COLORS["LIGHTYELLOW"], (0, 0, 2 * SIZE + DELIM_WIDTH, STATUS_HEIGHT)) 407 | surface.fill(COLORS["DARKBLUE"], (SIZE, STATUS_HEIGHT, DELIM_WIDTH, SIZE + STATUS_HEIGHT2)) 408 | surface.fill(COLORS["DARKBLUE"], (0, STATUS_HEIGHT, DELIM_WIDTH, SIZE + STATUS_HEIGHT2)) 409 | surface.fill(COLORS["DARKBLUE"], (2 * SIZE, STATUS_HEIGHT, DELIM_WIDTH, SIZE + STATUS_HEIGHT2)) 410 | surface.fill(COLORS["DARKBLUE"], (0, SIZE + STATUS_HEIGHT + STATUS_HEIGHT2, 2 * SIZE + DELIM_WIDTH, DELIM_WIDTH)) 411 | surface.fill(COLORS["DARKBLUE"], (0, STATUS_HEIGHT, 2 * SIZE + DELIM_WIDTH, DELIM_WIDTH)) 412 | draw_text(surface, helv24, "https://github.com/RandyWaterhouse/TSP_simulated_annealing", (SIZE // 2, SIZE + STATUS_HEIGHT + STATUS_HEIGHT2 + DELIM_WIDTH + 5), COLORS["DARKGREY"]) 413 | draw_text(surface, helv24, "No of cities:", (110, 10), COLORS["BLACK"]) 414 | draw_text(surface, helv24, str(N), (250, 10), COLORS["BLUE"]) 415 | button_ncity_10.PlaceButton(surface, 10, 40) 416 | button_ncity_20.PlaceButton(surface, 70, 40) 417 | button_ncity_50.PlaceButton(surface, 130, 40) 418 | button_ncity_100.PlaceButton(surface, 190, 40) 419 | button_ncity_200.PlaceButton(surface, 250, 40) 420 | button_ncity_500.PlaceButton(surface, 310, 40) 421 | pygame.draw.line(surface, COLORS["GREY1"], (380, 0), (380, STATUS_HEIGHT), 3) 422 | draw_text(surface, helv24, "Iterations:", (400, 10), COLORS["BLACK"]) 423 | draw_text(surface, helv24, str(iters // 1000) + " k", (400, 40), COLORS["BLUE"]) 424 | pygame.draw.line(surface, COLORS["GREY1"], (520, 0), (520, STATUS_HEIGHT), 3) 425 | draw_text(surface, helv24, "Tour Length Ratio:", (540, 10), COLORS["BLACK"]) 426 | draw_text(surface, helv24, str(round(d_energy_min / s_energy_min, 3)), (540, 40), COLORS["BLUE"]) 427 | pygame.draw.line(surface, COLORS["GREY1"], (750, 0), (750, STATUS_HEIGHT), 3) 428 | draw_text(surface, helv24, "Speed (iters/sec):", (770, 10), COLORS["BLACK"]) 429 | draw_text(surface, helv24, str(int(round(speed, 0))), (770, 40), COLORS["BLUE"]) 430 | pygame.draw.line(surface, COLORS["GREY1"], (2 * SIZE + DELIM_WIDTH - 230, 0), (2 * SIZE + DELIM_WIDTH - 230, STATUS_HEIGHT), 3) 431 | button_quit.PlaceButton(surface, 2 * SIZE + DELIM_WIDTH - 210, 10) 432 | button_pause.PlaceButton(surface, 2 * SIZE + DELIM_WIDTH - 210, 45) 433 | button_continue.PlaceButton(surface, 2 * SIZE + DELIM_WIDTH - 140, 45) 434 | button_save.PlaceButton(surface, 2 * SIZE + DELIM_WIDTH - 70, 10) 435 | button_plot.PlaceButton(surface, 2 * SIZE + DELIM_WIDTH - 70, 45) 436 | 437 | # draw cities and roads: 438 | # 439 | # direct sampling: 440 | for i in range(N): 441 | x1, y1 = dcities[i] 442 | x2, y2 = dcities[(i+1)%N] 443 | xi1 = int(SIZE * x1) 444 | xi2 = int(SIZE * x2) 445 | yi1 = STATUS_HEIGHT + STATUS_HEIGHT2+ int(SIZE * y1) 446 | yi2 = STATUS_HEIGHT + STATUS_HEIGHT2 + int(SIZE * y2) 447 | pygame.draw.line(surface, COLORS["DARKGREY"], [xi1, yi1], [xi2, yi2], 3) 448 | for x, y in dcities: 449 | xi = int(SIZE * x) 450 | yi = STATUS_HEIGHT + STATUS_HEIGHT2 + int(SIZE * y) 451 | pygame.draw.circle(surface, COLORS["BLUE"], [xi, yi], CITY_RADIUS) 452 | draw_text(surface, helv24, "Iterations:", (30, STATUS_HEIGHT + 10), COLORS["BLACK"]) 453 | draw_text(surface, helv24, str(diters), (150, STATUS_HEIGHT + 10), COLORS["BLUE"]) 454 | draw_text(surface, helv24, "Min. Tour Length:", (260, STATUS_HEIGHT + 10), COLORS["BLACK"]) 455 | draw_text(surface, helv24, str(round(d_energy_min, 3)), (460, STATUS_HEIGHT + 10), COLORS["BLUE"]) 456 | # simulated annealing: 457 | for i in range(N): 458 | x1, y1 = scities[i] 459 | x2, y2 = scities[(i+1)%N] 460 | xi1 = SIZE + DELIM_WIDTH + int(SIZE * x1) 461 | xi2 = SIZE + DELIM_WIDTH + int(SIZE * x2) 462 | yi1 = STATUS_HEIGHT + STATUS_HEIGHT2 + int(SIZE * y1) 463 | yi2 = STATUS_HEIGHT + STATUS_HEIGHT2 + int(SIZE * y2) 464 | pygame.draw.line(surface, COLORS["DARKGREY"], [xi1, yi1], [xi2, yi2], 3) 465 | for x, y in dcities: 466 | xi = SIZE + DELIM_WIDTH + int(SIZE * x) 467 | yi = STATUS_HEIGHT + STATUS_HEIGHT2 + int(SIZE * y) 468 | pygame.draw.circle(surface, COLORS["BLUE"], [xi, yi], CITY_RADIUS) 469 | draw_text(surface, helv24, "Iterations:", (SIZE + DELIM_WIDTH + 30, STATUS_HEIGHT + 10), COLORS["BLACK"]) 470 | draw_text(surface, helv24, str(siters), (SIZE + DELIM_WIDTH + 150, STATUS_HEIGHT + 10), COLORS["BLUE"]) 471 | draw_text(surface, helv24, "Min. Tour Length:", (SIZE + DELIM_WIDTH + 260, STATUS_HEIGHT + 10), COLORS["BLACK"]) 472 | draw_text(surface, helv24, str(round(s_energy_min, 3)), (SIZE + DELIM_WIDTH + 460, STATUS_HEIGHT + 10), COLORS["BLUE"]) 473 | 474 | # update graphics output: 475 | pygame.display.flip() 476 | # wait a moment (SLEEP may be zero): 477 | time.sleep(SLEEP) 478 | 479 | 480 | # calling "mainloop" will start simulation: 481 | mainloop(surface, N, dcities, scities, start_timer) 482 | 483 | --------------------------------------------------------------------------------