def _crossover_ox(self, parent1, parent2): """Performs order crossover to create a child path from two given parent paths. :param Path parent1: First parent path. :param Path parent2: Second parent path. :return: Child path. :rtype: Path """ # Initial child path child = Path(len(parent1)) # Copy random subpath from parent 1 to child start, end = self._rand_subpath() subpath = parent1.path[start:end + 1] tmp = parent2.path # Rotate tmp with pivot in the end + 1 tmp = tmp[end + 1:] + tmp[:end + 1] # Remove cities found in subpath from parent 2 tmp = list(filter(lambda x: x not in subpath, tmp)) # Join subpath and tmp to form a child child.path = subpath + tmp # Rotate the path so it always starts at 0 last_zero_idx = len(child) - child[::-1].index(0) - 1 child.path = child[last_zero_idx:] + child[:last_zero_idx] child.distance = self.tsp.path_dist(child) return child
def _test_crossover(self, fun, expected, mock_rand_subpath, mock_dist): for (p1, p2, subpath), exp in zip(self.data, expected): with self.subTest(p1=p1, p2=p2, subpath=subpath): mock_rand_subpath.return_value = subpath self.gasolver.tsp.specification['DIMENSION'] = len(p1) - 1 parent1 = Path(path=p1) parent2 = Path(path=p2) child = fun(parent1, parent2) self.assertListEqual(child.path, exp)
def test_set_path(self): data = [[1, 2, 3, 4, 5, 6], [9, 20, 1, 5, 55, 7], [0, 0, 0, 0, 0, 0]] path = Path(6) for p in data: with self.subTest(path=p): path.path = p self.assertListEqual(path._path, p)
def test_set_path_exception(self): data = [[1, 2, 3, 4, 5], [1, 2, 3, 4, 5, 6, 7], [1], []] path = Path(6) for p in data: with self.subTest(path=p): with self.assertRaises(ValueError): path.path = p
def test_invert(self): data = [(1, 3, [1, 4, 3, 2, 5, 6]), (0, 4, [5, 2, 3, 4, 1, 6]), (2, 5, [5, 2, 6, 1, 4, 3]), (5, 0, [3, 4, 1, 6, 2, 5]), (2, 3, [3, 4, 6, 1, 2, 5]), (4, 4, [3, 4, 6, 1, 2, 5])] path = Path(path=[1, 2, 3, 4, 5, 6]) for i, j, expected in data: with self.subTest(i=i, j=j): path.invert(i, j) self.assertListEqual(path.path, expected)
def test_move(self): data = [(Neighbourhood.SWAP, 1, 4, [1, 5, 3, 4, 2, 6]), (Neighbourhood.INSERT, 1, 4, [1, 3, 4, 2, 5, 6]), (Neighbourhood.INVERT, 1, 4, [1, 5, 2, 4, 3, 6])] path = Path(path=[1, 2, 3, 4, 5, 6]) for neigh, i, j, expected in data: with self.subTest(neigh=neigh, i=i, j=j): path.move(neigh, i, j) self.assertListEqual(path.path, expected)
def test_in_path(self): data = [(4, None, True), (2, None, True), (7, None, True), (3, None, False), (7, 6, True), (4, 1, True), (8, 3, True), (4, 0, False), (7, 5, False), (10, 3, False)] path = Path(path=[4, 1, 8, 2, 9, 7]) for city, limit, expected in data: with self.subTest(city=city, limit=limit): result = path.in_path(city, limit) self.assertEqual(result, expected)
def test_shuffle(self): data = [([0, 1, 2, 3, 4, 5, 6, 7], 2, 5), ([0, 1, 2, 3, 3, 5, 6, 0], 1, -1), ([5, 4, 3, 2, 1], 0, 4), ([1, 2, 3], 0, 2), ([], 0, 0)] for p, i, j in data: with self.subTest(path=p, i=i, j=j): path = Path(path=deepcopy(p)) path.shuffle(i, j) # Make sure no elements are lost or added for n in p: self.assertEqual(path.path.count(n), p.count(n)) # Compare slices that shouldn't be shuffled self.assertListEqual(path[0:i], p[0:i]) self.assertListEqual(path[j:-1], p[j:-1])
def test_get_stop(self): data = [2, 7, 4, 3, 5, 1] path = Path(path=data) for index, value in enumerate(data): with self.subTest(index=index): result = path[index] self.assertEqual(result, value)
def test_get_stop_exception(self): data = [6, 7, 20, -7] path = Path(6) for index in data: with self.subTest(index=index, length=len(path)): with self.assertRaises(IndexError): path[index]
def _min_neighbour(self, path): """Finds shortest neighbour of the given path. :param Path path: Path whose neighbourhood will be searched. """ min_neigh = Path(self.tsp.dimension + 1) min_neigh.distance = inf best_move = () # Iterate through all possible 2-city moves for i, j in product(range(1, self.tsp.dimension), repeat=2): # Skip redundant moves if self.neighbourhood == Neighbourhood.SWAP or \ self.neighbourhood == Neighbourhood.INVERT: if j <= i: continue if self.neighbourhood == Neighbourhood.INSERT: if abs(i - j) == 1 and i > j: continue # Skip tabu moves if self._tabu[i][j]: continue # Perform the move cur_neigh = deepcopy(path) cur_neigh.move(self.neighbourhood, i, j) cur_neigh.distance = self.tsp.path_dist(cur_neigh) # If resulting path is better than current minimum keep its # length and move indexed if cur_neigh.distance < min_neigh.distance: min_neigh, best_move = cur_neigh, (i, j) # Tabu found move if best_move: self._tabu[best_move[0]][best_move[1]] = self.cadence self._tabu[best_move[1]][best_move[0]] = self.cadence # In small instances it can happen all neighbours are already on tabu # list, if that happens we cannot return an empty path return min_neigh if min_neigh.distance != inf else path
def _crossover_nwox(self, parent1, parent2): """Performs non wrapping order crossover to create a child path from two given parents paths. :param Path parent1: First parent path. :param Path parent2: Second parent path. :return: Child path. :rtype: Path """ # Initial child path child = Path(self.tsp.dimension + 1) # Copy random subpath from parent 1 to child start, end = self._rand_subpath() child[start:end + 1] = parent1[start:end + 1] # Fill in child's empty slots with cities from parent 2 in order parent_pos = child_pos = 0 while parent_pos < self.tsp.dimension + 1: # Skip already filled subpath if start <= child_pos <= end: child_pos = end + 1 continue # Get city from parent path city = parent2[parent_pos] if child.in_path(city): # If this city is already in child path then go to next one parent_pos += 1 continue else: # Otherwise add it to child path and go to next child[child_pos] = city child_pos += 1 parent_pos += 1 # Add return to 0 if last stop is empty child[-1] = child[-1] if child[-1] != -1 else 0 child.distance = self.tsp.path_dist(child) return child
def test_from_path(self): data = [([0, 1, 2, 3, 4, 5, 6, 0], [1, 2, 3, 4, 5, 6, 7]), ([0, 1, 2, 3, 4, 5, 6], [1, 2, 3, 4, 5, 6, 7]), ([9, 8, 7, 6, 5, 4], [10, 9, 8, 7, 6, 5]), ([3, 2, 1, 0, 6, 5, 4, 3], [4, 3, 2, 1, 7, 6, 5])] for path, expected in data: with self.subTest(path=path): p = Path(path=path) result = TSPLibTour.from_path(p).tour self.assertListEqual(result, expected)
def test_from_tour(self): data = [([1, 2, 3, 4, 5, 6], [0, 1, 2, 3, 4, 5, 0]), ([1, 2, 3, 4, 5, 6, 1], [0, 1, 2, 3, 4, 5, 0]), ([5, 1, 3, 4, 2], [4, 0, 2, 3, 1, 4])] for tour, expected in data: with self.subTest(tour=tour): tsplibtour = TSPLibTour() tsplibtour.tour = tour path = Path.from_tour(tsplibtour) self.assertListEqual(path.path, expected)
def _crossover_pmx(self, parent1, parent2): """Performs partially matched crossover to create a child path from two given parent paths. :param Path parent1: First parent path. :param Path parent2: Second parent path. :return: Child path. :rtype: Path """ # Starting path child = Path(len(parent1)) # Copy random subpath from parent 1 to child and create mapping start, end = self._rand_subpath() child[start:end + 1] = parent1[start:end + 1] # Create mapping mapping = dict(zip(parent1[start:end + 1], parent2[start:end + 1])) # Copy stops from parent 2 to child using mapping if necessary child_pos = 0 while child_pos < self.tsp.dimension + 1: # Skip already filled subpath if start <= child_pos <= end: child_pos = end + 1 continue # Get city at current stop in parent 2 city = parent2[child_pos] # Trace mapping if it exists while city in mapping: city = mapping[city] # Set stop in the child path child[child_pos] = city child_pos += 1 child.distance = self.tsp.path_dist(child) return child
def test_path_dist(self): data = [([0, 1, 2, 3], 2 + 7 + 12), ([2, 0, 1, 3], 9 + 2 + 8), ([0, 3, 1, 0], 4 + 14 + 5), ([1, 1, 1, 1], 0), ([-1, -1, -1, -1], 0)] self.tsp.distances = self.distances for p, expected in data: with self.subTest(p=p): path = Path(path=p) result = self.tsp.path_dist(path) self.assertEqual(result, expected)
def test_path_dist(self): data = [([0, 1, 2, 3, 4, 5], 22), ([5, 4, 3, 2, 1, 0], 22), ([5, 2, 0, 1, 4, 2], 34), ([0, 1, 2, 3, 4, 0], 27), ([5, 4, 3], 12), ([0, 0, 0, 0, 0, 0], 0), ([-1, -1, -1, -1, -1, -1], 0)] self.tsp.load('tests/fixtures/test6.tsp') for p, expected in data: with self.subTest(path=p): path = Path(path=p) result = self.tsp.path_dist(path) self.assertEqual(result, expected)
def test_set_stop(self): data = [(1, 5, [-1, 5, -1, -1, -1, -1]), (0, 0, [0, 5, -1, -1, -1, -1]), (5, 9, [0, 5, -1, -1, -1, 9]), (2, 3, [0, 5, 3, -1, -1, 9]), (3, 7, [0, 5, 3, 7, -1, 9]), (4, 6, [0, 5, 3, 7, 6, 9]), (0, 4, [4, 5, 3, 7, 6, 9]), (-1, 8, [4, 5, 3, 7, 6, 8])] path = Path(6) for index, city, expected in data: with self.subTest(index=index, city=city): path[index] = city self.assertListEqual(path._path, expected)
def solve(self, tsp, steps=True): # Make sure given argument is of correct type if not isinstance(tsp, TSP): raise TypeError('solve() argument has to be of type \'TSP\'') self.tsp = tsp # Path will always start and end in 0 path = Path(self.tsp.dimension + 1) path[0] = path[-1] = 0 # Start timer self._start_timer() # For each stop except the first and last one for i in range(1, len(path) - 1): prev = path[i - 1] min_dist = inf # Check all connections to different cities for j in range(self.tsp.dimension): # Skip cities that already are in path if path.in_path(j, i): continue # Keep the new distance if it's lower than current minimum new_dist = self.tsp.dist(prev, j) if new_dist < min_dist: min_dist = new_dist path[i] = j if steps: progress = i / (len(path) - 1) yield SolverState(self._time(), progress, deepcopy(path), None) path.distance = self.tsp.path_dist(path) yield SolverState(self._time(), 1, None, deepcopy(path), True)
def _init_population(self): """Initializes population by creating specified number of random paths. """ self._population.clear() for _ in range(self.population_size): path = Path(self.tsp.dimension + 1) path.path = list(range(self.tsp.dimension)) + [0] path.shuffle(1, -1) path.distance = self.tsp.path_dist(path) self._population.append(path) self._population.sort(key=lambda p: p.distance)
def solve(self, tsp, steps=True): # Make sure given argument is of correct type if not isinstance(tsp, TSP): raise TypeError('solve() argument has to be of type \'TSP\'') self.tsp = tsp # Create starting path: 0, 1, 2, ..., 0, this path will be permuted path = Path(self.tsp.dimension + 1) path.path = list(range(len(path) - 1)) + [0] path.distance = self.tsp.path_dist(path) # Best path min_path = deepcopy(path) # Create permutations skipping the last stop (return to 0) perms = permutations(path.path[1:-1]) if steps: total = factorial(self.tsp.dimension - 1) # Start the timer self._start_timer() # Loop through all permutations to find the shortest path for i, perm in enumerate(perms): path.path = [0] + list(perm) + [0] path.distance = self.tsp.path_dist(path) if path.distance < min_path.distance: min_path = deepcopy(path) if steps: # Need to use deepcopies because object could change before the # reference will be used yield SolverState(self._time(), i / total, deepcopy(path), deepcopy(min_path)) yield SolverState(self._time(), 1, None, min_path, True)
def test_init(self): for i in range(10): result = Path(i) self.assertListEqual(result._path, [-1] * i) self.assertEqual(len(result), i) self.assertEqual(result.distance, -1)
def solve(self, tsp, steps=True): # Make sure given argument is of correct type if not isinstance(tsp, TSP): raise TypeError('solve() argument has to be of type \'TSP\'') self.tsp = tsp # Total number of iterations or time for calculating progress if steps: current = 0 iters = log(self.end_temp / self.init_temp, 1 - self.cooling_rate) total = self.run_time if self.run_time else iters # Start with random path cur_path = Path(self.tsp.dimension + 1) cur_path.path = list(range(len(cur_path) - 1)) + [0] cur_path.shuffle(1, -1) cur_path.distance = self.tsp.path_dist(cur_path) # And set it as current minimum min_path = deepcopy(cur_path) # Start the timer self._start_timer() # Init temperature temp = self.init_temp # Repeat as long as system temperature is higher than minimum while True: # Update iteration counter ro time counterif running in step mode if steps: current = self._time_ms() if self.run_time else current + 1 # Get random neighbour of current path new_path = self._rand_neigh(cur_path) # Difference between current and new path delta_dist = new_path.distance - cur_path.distance # If it's shorter or equal if delta_dist <= 0: # If it's shorter set it as current minimum if new_path.distance < min_path.distance: min_path = deepcopy(new_path) # Set new path as current path cur_path = deepcopy(new_path) elif exp(-delta_dist / temp) > random(): # If path is longer accept it with random probability cur_path = deepcopy(new_path) # Cooling down temp *= 1 - self.cooling_rate # Terminate search after reaching end temperature if not self.run_time and temp < self.end_temp: break # Terminate search after exceeding specified runtime # We use `total` to not have to convert to nanoseconds every time if self.run_time and self._time_ms() >= self.run_time: break # Report current solver state if steps: yield SolverState(self._time(), current / total, deepcopy(new_path), deepcopy(min_path)) yield SolverState(self._time(), 1, None, deepcopy(min_path), True)
def solve(self, tsp, steps=True): # Make sure given argument is of correct type if not isinstance(tsp, TSP): raise TypeError('solve() argument has to be of type \'TSP\'') self.tsp = tsp # Total and current number of steps for calculating progress if steps: total = factorial(self.tsp.dimension - 1) * 2 current = 0 # Working path path = Path(self.tsp.dimension + 1) path[-1] = 0 # Minimal path and distance min_path = Path(self.tsp.dimension + 1) min_path.distance = inf # Nodes list (used as a stack) stack = [] # Add starting city (0) to the stack stack.append((0, 0, 0)) # Start the timer self._start_timer() while len(stack) > 0: # Increment step counter if steps: current += 1 # Get node from the top of the stack cur_node = stack.pop() city, dist, level = cur_node # Update current path with this node path[level] = city # This is the level of all children of this node next_level = level + 1 # If it's the last level of the tree if level == self.tsp.dimension - 1: path.distance = dist + self.tsp.dist(city, 0) # Yield the current state if steps: yield SolverState(self._time(), current / total, deepcopy(path), deepcopy(min_path)) # Distance of full path with return to 0 # Keep it if it's better than the current minimum if path.distance < min_path.distance: min_path = deepcopy(path) else: continue # Iterate through all cities for i in range(self.tsp.dimension): # Skip current city itself, its predecessors and starting city if i == city or path.in_path(i, next_level) or i == 0: continue # Skip this node if its distance is greater than min path next_dist = dist + self.tsp.dist(city, i) if next_dist >= min_path.distance: continue # If it's valid node push it onto stack stack.append((i, next_dist, next_level)) yield SolverState(self._time(), 1, None, deepcopy(min_path), True)