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_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_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 _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 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)