From a5d8f113ccdc6d21d7a1dfbc018038411d892cdf Mon Sep 17 00:00:00 2001 From: sagarvijaygupta Date: Thu, 25 Jan 2018 19:15:00 +0530 Subject: [PATCH] Visualisation of TSP. Add features like selecting cities to be part of tsp, controlling temperature and speed of animation. --- gui/tsp.py | 219 +++++++++++++++++++++++++++++++++++++++++ images/romania_map.png | Bin 0 -> 15206 bytes 2 files changed, 219 insertions(+) create mode 100644 gui/tsp.py create mode 100644 images/romania_map.png diff --git a/gui/tsp.py b/gui/tsp.py new file mode 100644 index 000000000..6a460261e --- /dev/null +++ b/gui/tsp.py @@ -0,0 +1,219 @@ +from tkinter import * +import sys +import os.path +sys.path.append(os.path.join(os.path.dirname(__file__), '..')) +from search import * +import numpy as np + +distances = {} + + +class TSP_problem(Problem): + + """ subclass of Problem to define various functions """ + + def two_opt(self, state): + """ Neighbour generating function for Traveling Salesman Problem """ + neighbour_state = state[:] + left = random.randint(0, len(neighbour_state) - 1) + right = random.randint(0, len(neighbour_state) - 1) + if left > right: + left, right = right, left + neighbour_state[left: right + 1] = reversed(neighbour_state[left: right + 1]) + return neighbour_state + + def actions(self, state): + """ action that can be excuted in given state """ + return [self.two_opt] + + def result(self, state, action): + """ result after applying the given action on the given state """ + return action(state) + + def path_cost(self, c, state1, action, state2): + """ total distance for the Traveling Salesman to be covered if in state2 """ + cost = 0 + for i in range(len(state2) - 1): + cost += distances[state2[i]][state2[i + 1]] + cost += distances[state2[0]][state2[-1]] + return cost + + def value(self, state): + """ value of path cost given negative for the given state """ + return -1 * self.path_cost(None, None, None, state) + + +class TSP_Gui(): + """ Class to create gui of Traveling Salesman using simulated annealing where one can + select cities, change speed and temperature. Distances between cities are euclidean + distances between them. + """ + + def __init__(self, root, all_cities): + self.root = root + self.vars = [] + self.frame_locations = {} + self.calculate_canvas_size() + self.button_text = StringVar() + self.button_text.set("Start") + self.all_cities = all_cities + self.frame_select_cities = Frame(self.root) + self.frame_select_cities.grid(row=1) + self.frame_canvas = Frame(self.root) + self.frame_canvas.grid(row=2) + Label(self.root, text="Map of Romania", font="Times 13 bold").grid(row=0, columnspan=10) + + def create_checkboxes(self, side=LEFT, anchor=W): + """ To select cities which are to be a part of Traveling Salesman Problem """ + + row_number = 0 + column_number = 0 + + for city in self.all_cities: + var = IntVar() + var.set(1) + Checkbutton(self.frame_select_cities, text=city, variable=var).grid( + row=row_number, column=column_number, sticky=W) + + self.vars.append(var) + column_number += 1 + if column_number == 10: + column_number = 0 + row_number += 1 + + def create_buttons(self): + """ Create start and quit button """ + + Button(self.frame_select_cities, textvariable=self.button_text, + command=self.run_traveling_salesman).grid(row=3, column=4, sticky=E + W) + Button(self.frame_select_cities, text='Quit', command=self.root.destroy).grid( + row=3, column=5, sticky=E + W) + + def run_traveling_salesman(self): + """ Choose selected citites """ + + cities = [] + for i in range(len(self.vars)): + if self.vars[i].get() == 1: + cities.append(self.all_cities[i]) + + tsp_problem = TSP_problem(cities) + self.button_text.set("Reset") + self.create_canvas(tsp_problem) + + def calculate_canvas_size(self): + """ Width and height for canvas """ + + minx, maxx = sys.maxsize, -1 * sys.maxsize + miny, maxy = sys.maxsize, -1 * sys.maxsize + + for value in romania_map.locations.values(): + minx = min(minx, value[0]) + maxx = max(maxx, value[0]) + miny = min(miny, value[1]) + maxy = max(maxy, value[1]) + + # New locations squeezed to fit inside the map of romania + for name, coordinates in romania_map.locations.items(): + self.frame_locations[name] = (coordinates[0] / 1.2 - minx + + 150, coordinates[1] / 1.2 - miny + 165) + + canvas_width = maxx - minx + 200 + canvas_height = maxy - miny + 200 + + self.canvas_width = canvas_width + self.canvas_height = canvas_height + + def create_canvas(self, problem): + """ creating map with cities """ + + map_canvas = Canvas(self.frame_canvas, width=self.canvas_width, height=self.canvas_height) + map_canvas.grid(row=3, columnspan=10) + current = Node(problem.initial) + map_canvas.delete("all") + self.romania_image = PhotoImage(file="../images/romania_map.png") + map_canvas.create_image(self.canvas_width / 2, self.canvas_height / 2, + image=self.romania_image) + cities = current.state + for city in cities: + x = self.frame_locations[city][0] + y = self.frame_locations[city][1] + map_canvas.create_oval(x - 3, y - 3, x + 3, y + 3, + fill="red", outline="red") + map_canvas.create_text(x - 15, y - 10, text=city) + + self.cost = StringVar() + Label(self.frame_canvas, textvariable=self.cost, relief="sunken").grid( + row=2, columnspan=10) + + self.speed = IntVar() + speed_scale = Scale(self.frame_canvas, from_=500, to=1, orient=HORIZONTAL, + variable=self.speed, label="Speed ----> ", showvalue=0, font="Times 11", + relief="sunken", cursor="gumby") + speed_scale.grid(row=1, columnspan=5, sticky=N + S + E + W) + self.temperature = IntVar() + temperature_scale = Scale(self.frame_canvas, from_=100, to=0, orient=HORIZONTAL, + length=200, variable=self.temperature, label="Temperature ---->", + font="Times 11", relief="sunken", showvalue=0, cursor="gumby") + + temperature_scale.grid(row=1, column=5, columnspan=5, sticky=N + S + E + W) + self.simulated_annealing_with_tunable_T(problem, map_canvas) + + def exp_schedule(k=100, lam=0.03, limit=1000): + """ One possible schedule function for simulated annealing """ + + return lambda t: (k * math.exp(-lam * t) if t < limit else 0) + + def simulated_annealing_with_tunable_T(self, problem, map_canvas, schedule=exp_schedule()): + """ Simulated annealing where temperature is taken as user input """ + + current = Node(problem.initial) + + while(1): + T = schedule(self.temperature.get()) + if T == 0: + return current.state + neighbors = current.expand(problem) + if not neighbors: + return current.state + next = random.choice(neighbors) + delta_e = problem.value(next.state) - problem.value(current.state) + if delta_e > 0 or probability(math.exp(delta_e / T)): + map_canvas.delete("poly") + + current = next + self.cost.set("Cost = " + str('%0.3f' % (-1 * problem.value(current.state)))) + points = [] + for city in current.state: + points.append(self.frame_locations[city][0]) + points.append(self.frame_locations[city][1]) + map_canvas.create_polygon(points, outline='red', width=3, fill='', tag="poly") + map_canvas.update() + map_canvas.after(self.speed.get()) + + +def main(): + all_cities = [] + for city in romania_map.locations.keys(): + distances[city] = {} + all_cities.append(city) + all_cities.sort() + + # distances['city1']['city2'] contains euclidean distance between their coordinates + for name_1, coordinates_1 in romania_map.locations.items(): + for name_2, coordinates_2 in romania_map.locations.items(): + distances[name_1][name_2] = np.linalg.norm( + [coordinates_1[0] - coordinates_2[0], coordinates_1[1] - coordinates_2[1]]) + distances[name_2][name_1] = np.linalg.norm( + [coordinates_1[0] - coordinates_2[0], coordinates_1[1] - coordinates_2[1]]) + + root = Tk() + root.title("Traveling Salesman Problem") + cities_selection_panel = TSP_Gui(root, all_cities) + cities_selection_panel.create_checkboxes() + cities_selection_panel.create_buttons() + root.mainloop() + + +if __name__ == '__main__': + main() diff --git a/images/romania_map.png b/images/romania_map.png new file mode 100644 index 0000000000000000000000000000000000000000..426c76f1e94b9ee691ab416f9da910944193f549 GIT binary patch literal 15206 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 literal 0 HcmV?d00001