diff --git a/searches/tabuTestData.txt b/searches/tabuTestData.txt new file mode 100644 index 000000000..f797ff1c6 --- /dev/null +++ b/searches/tabuTestData.txt @@ -0,0 +1,10 @@ +a b 20 +a c 18 +a d 22 +a e 26 +b c 10 +b d 11 +b e 12 +c d 23 +c e 24 +d e 40 diff --git a/searches/tabu_search.py b/searches/tabu_search.py new file mode 100644 index 000000000..b93b3202b --- /dev/null +++ b/searches/tabu_search.py @@ -0,0 +1,313 @@ +""" +This is pure python implementation of Tabu search algorithm for a Travelling Salesman Problem, that the distances +between the cities are symmetric (the distance between city 'a' and city 'b' is the same between city 'b' and city 'a'). +The TSP can be represented into a graph. The cities are represented by nodes and the distance between them is +represented by the weight of the ark between the nodes. + +The .txt file with the graph has the form: + +node1 node2 distance_between_node1_and_node2 +node1 node3 distance_between_node1_and_node3 +... + +Be careful node1, node2 and the distance between them, must exist only once. This means in the .txt file +should not exist: +node1 node2 distance_between_node1_and_node2 +node2 node1 distance_between_node2_and_node1 + +For pytests run following command: +pytest + +For manual testing run: +python tabu_search.py -f your_file_name.txt -number_of_iterations_of_tabu_search -s size_of_tabu_search +e.g. python tabu_search.py -f tabudata2.txt -i 4 -s 3 +""" + +import copy +import argparse +import sys + + +def generate_neighbours(path): + """ + Pure implementation of generating a dictionary of neighbors and the cost with each + neighbor, given a path file that includes a graph. + + :param path: The path to the .txt file that includes the graph (e.g.tabudata2.txt) + :return dict_of_neighbours: Dictionary with key each node and value a list of lists with the neighbors of the node + and the cost (distance) for each neighbor. + + Example of dict_of_neighbours: + >>> dict_of_neighbours[a] + [[b,20],[c,18],[d,22],[e,26]] + + This indicates the neighbors of node (city) 'a', which has neighbor the node 'b' with distance 20, + the node 'c' with distance 18, the node 'd' with distance 22 and the node 'e' with distance 26. + + """ + f = open(path, "r") + + dict_of_neighbours = {} + + for line in f: + if line.split()[0] not in dict_of_neighbours: + _list = list() + _list.append([line.split()[1], line.split()[2]]) + dict_of_neighbours[line.split()[0]] = _list + else: + dict_of_neighbours[line.split()[0]].append([line.split()[1], line.split()[2]]) + if line.split()[1] not in dict_of_neighbours: + _list = list() + _list.append([line.split()[0], line.split()[2]]) + dict_of_neighbours[line.split()[1]] = _list + else: + dict_of_neighbours[line.split()[1]].append([line.split()[0], line.split()[2]]) + f.close() + + return dict_of_neighbours + + +def generate_first_solution(path, dict_of_neighbours): + """ + Pure implementation of generating the first solution for the Tabu search to start, with the redundant resolution + strategy. That means that we start from the starting node (e.g. node 'a'), then we go to the city nearest (lowest + distance) to this node (let's assume is node 'c'), then we go to the nearest city of the node 'c', etc + till we have visited all cities and return to the starting node. + + :param path: The path to the .txt file that includes the graph (e.g.tabudata2.txt) + :param dict_of_neighbours: Dictionary with key each node and value a list of lists with the neighbors of the node + and the cost (distance) for each neighbor. + :return first_solution: The solution for the first iteration of Tabu search using the redundant resolution strategy + in a list. + :return distance_of_first_solution: The total distance that Travelling Salesman will travel, if he follows the path + in first_solution. + + """ + + f = open(path, "r") + start_node = f.read(1) + end_node = start_node + + first_solution = [] + + visiting = start_node + + distance_of_first_solution = 0 + f.close() + while visiting not in first_solution: + minim = 10000 + for k in dict_of_neighbours[visiting]: + if int(k[1]) < int(minim) and k[0] not in first_solution: + minim = k[1] + best_node = k[0] + + first_solution.append(visiting) + distance_of_first_solution = distance_of_first_solution + int(minim) + visiting = best_node + + first_solution.append(end_node) + + position = 0 + for k in dict_of_neighbours[first_solution[-2]]: + if k[0] == start_node: + break + position += 1 + + distance_of_first_solution = distance_of_first_solution + int( + dict_of_neighbours[first_solution[-2]][position][1]) - 10000 + return first_solution, distance_of_first_solution + + +def find_neighborhood(solution, dict_of_neighbours): + """ + Pure implementation of generating the neighborhood (sorted by total distance of each solution from + lowest to highest) of a solution with 1-1 exchange method, that means we exchange each node in a solution with each + other node and generating a number of solution named neighborhood. + + :param solution: The solution in which we want to find the neighborhood. + :param dict_of_neighbours: Dictionary with key each node and value a list of lists with the neighbors of the node + and the cost (distance) for each neighbor. + :return neighborhood_of_solution: A list that includes the solutions and the total distance of each solution + (in form of list) that are produced with 1-1 exchange from the solution that the method took as an input + + + Example: + >>> find_neighborhood(['a','c','b','d','e','a']) + [['a','e','b','d','c','a',90], [['a','c','d','b','e','a',90],['a','d','b','c','e','a',93], + ['a','c','b','e','d','a',102], ['a','c','e','d','b','a',113], ['a','b','c','d','e','a',93]] + + """ + + neighborhood_of_solution = [] + + for n in solution[1:-1]: + idx1 = solution.index(n) + for kn in solution[1:-1]: + idx2 = solution.index(kn) + if n == kn: + continue + + _tmp = copy.deepcopy(solution) + _tmp[idx1] = kn + _tmp[idx2] = n + + distance = 0 + + for k in _tmp[:-1]: + next_node = _tmp[_tmp.index(k) + 1] + for i in dict_of_neighbours[k]: + if i[0] == next_node: + distance = distance + int(i[1]) + _tmp.append(distance) + + if _tmp not in neighborhood_of_solution: + neighborhood_of_solution.append(_tmp) + + indexOfLastItemInTheList = len(neighborhood_of_solution[0]) - 1 + + neighborhood_of_solution.sort(key=lambda x: x[indexOfLastItemInTheList]) + return neighborhood_of_solution + + +def tabu_search(first_solution, distance_of_first_solution, dict_of_neighbours, iters, size): + """ + Pure implementation of Tabu search algorithm for a Travelling Salesman Problem in Python. + + :param first_solution: The solution for the first iteration of Tabu search using the redundant resolution strategy + in a list. + :param distance_of_first_solution: The total distance that Travelling Salesman will travel, if he follows the path + in first_solution. + :param dict_of_neighbours: Dictionary with key each node and value a list of lists with the neighbors of the node + and the cost (distance) for each neighbor. + :param iters: The number of iterations that Tabu search will execute. + :param size: The size of Tabu List. + :return best_solution_ever: The solution with the lowest distance that occured during the execution of Tabu search. + :return best_cost: The total distance that Travelling Salesman will travel, if he follows the path in best_solution + ever. + + """ + count = 1 + solution = first_solution + tabu_list = list() + best_cost = distance_of_first_solution + best_solution_ever = solution + + while count <= iters: + neighborhood = find_neighborhood(solution, dict_of_neighbours) + index_of_best_solution = 0 + best_solution = neighborhood[index_of_best_solution] + best_cost_index = len(best_solution) - 1 + + found = False + while found is False: + i = 0 + while i < len(best_solution): + + if best_solution[i] != solution[i]: + first_exchange_node = best_solution[i] + second_exchange_node = solution[i] + break + i = i + 1 + + if [first_exchange_node, second_exchange_node] not in tabu_list and [second_exchange_node, + first_exchange_node] not in tabu_list: + tabu_list.append([first_exchange_node, second_exchange_node]) + found = True + solution = best_solution[:-1] + cost = neighborhood[index_of_best_solution][best_cost_index] + if cost < best_cost: + best_cost = cost + best_solution_ever = solution + else: + index_of_best_solution = index_of_best_solution + 1 + best_solution = neighborhood[index_of_best_solution] + + if len(tabu_list) >= size: + tabu_list.pop(0) + + count = count + 1 + + return best_solution_ever, best_cost + + +def main(args=None): + dict_of_neighbours = generate_neighbours(args.File) + + first_solution, distance_of_first_solution = generate_first_solution(args.File, dict_of_neighbours) + + best_sol, best_cost = tabu_search(first_solution, distance_of_first_solution, dict_of_neighbours, args.Iterations, + args.Size) + + print("Best solution: {0}, with total distance: {1}.".format(best_sol, best_cost)) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Tabu Search") + parser.add_argument( + "-f", "--File", type=str, help="Path to the file containing the data", required=True) + parser.add_argument( + "-i", "--Iterations", type=int, help="How many iterations the algorithm should perform", required=True) + parser.add_argument( + "-s", "--Size", type=int, help="Size of the tabu list", required=True) + + # Pass the arguments to main method + sys.exit(main(parser.parse_args())) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/searches/test_tabu_search.py b/searches/test_tabu_search.py new file mode 100644 index 000000000..1bc2d3361 --- /dev/null +++ b/searches/test_tabu_search.py @@ -0,0 +1,46 @@ +import unittest +import os +from tabu_search import generate_neighbours, generate_first_solution, find_neighborhood, tabu_search + +TEST_FILE = os.path.join(os.path.dirname(__file__), './tabuTestData.txt') + +NEIGHBOURS_DICT = {'a': [['b', '20'], ['c', '18'], ['d', '22'], ['e', '26']], + 'c': [['a', '18'], ['b', '10'], ['d', '23'], ['e', '24']], + 'b': [['a', '20'], ['c', '10'], ['d', '11'], ['e', '12']], + 'e': [['a', '26'], ['b', '12'], ['c', '24'], ['d', '40']], + 'd': [['a', '22'], ['b', '11'], ['c', '23'], ['e', '40']]} + +FIRST_SOLUTION = ['a', 'c', 'b', 'd', 'e', 'a'] + +DISTANCE = 105 + +NEIGHBOURHOOD_OF_SOLUTIONS = [['a', 'e', 'b', 'd', 'c', 'a', 90], + ['a', 'c', 'd', 'b', 'e', 'a', 90], + ['a', 'd', 'b', 'c', 'e', 'a', 93], + ['a', 'c', 'b', 'e', 'd', 'a', 102], + ['a', 'c', 'e', 'd', 'b', 'a', 113], + ['a', 'b', 'c', 'd', 'e', 'a', 119]] + + +class TestClass(unittest.TestCase): + def test_generate_neighbours(self): + neighbours = generate_neighbours(TEST_FILE) + + self.assertEquals(NEIGHBOURS_DICT, neighbours) + + def test_generate_first_solutions(self): + first_solution, distance = generate_first_solution(TEST_FILE, NEIGHBOURS_DICT) + + self.assertEquals(FIRST_SOLUTION, first_solution) + self.assertEquals(DISTANCE, distance) + + def test_find_neighbours(self): + neighbour_of_solutions = find_neighborhood(FIRST_SOLUTION, NEIGHBOURS_DICT) + + self.assertEquals(NEIGHBOURHOOD_OF_SOLUTIONS, neighbour_of_solutions) + + def test_tabu_search(self): + best_sol, best_cost = tabu_search(FIRST_SOLUTION, DISTANCE, NEIGHBOURS_DICT, 4, 3) + + self.assertEquals(['a', 'd', 'b', 'e', 'c', 'a'], best_sol) + self.assertEquals(87, best_cost) \ No newline at end of file