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