|
| 1 | +""" |
| 2 | +Use an ant colony optimization algorithm to solve the travelling salesman problem (TSP) |
| 3 | +which asks the following question: |
| 4 | +"Given a list of cities and the distances between each pair of cities, what is the |
| 5 | + shortest possible route that visits each city exactly once and returns to the origin |
| 6 | + city?" |
| 7 | +
|
| 8 | +https://en.wikipedia.org/wiki/Ant_colony_optimization_algorithms |
| 9 | +https://en.wikipedia.org/wiki/Travelling_salesman_problem |
| 10 | +
|
| 11 | +Author: Clark |
| 12 | +""" |
| 13 | + |
| 14 | +import copy |
| 15 | +import random |
| 16 | + |
| 17 | +cities = { |
| 18 | + 0: [0, 0], |
| 19 | + 1: [0, 5], |
| 20 | + 2: [3, 8], |
| 21 | + 3: [8, 10], |
| 22 | + 4: [12, 8], |
| 23 | + 5: [12, 4], |
| 24 | + 6: [8, 0], |
| 25 | + 7: [6, 2], |
| 26 | +} |
| 27 | + |
| 28 | + |
| 29 | +def main( |
| 30 | + cities: dict[int, list[int]], |
| 31 | + ants_num: int, |
| 32 | + iterations_num: int, |
| 33 | + pheromone_evaporation: float, |
| 34 | + alpha: float, |
| 35 | + beta: float, |
| 36 | + q: float, # Pheromone system parameters Q,which is a constant |
| 37 | +) -> tuple[list[int], float]: |
| 38 | + """ |
| 39 | + Ant colony algorithm main function |
| 40 | + >>> main(cities=cities, ants_num=10, iterations_num=20, |
| 41 | + ... pheromone_evaporation=0.7, alpha=1.0, beta=5.0, q=10) |
| 42 | + ([0, 1, 2, 3, 4, 5, 6, 7, 0], 37.909778143828696) |
| 43 | + >>> main(cities={0: [0, 0], 1: [2, 2]}, ants_num=5, iterations_num=5, |
| 44 | + ... pheromone_evaporation=0.7, alpha=1.0, beta=5.0, q=10) |
| 45 | + ([0, 1, 0], 5.656854249492381) |
| 46 | + >>> main(cities={0: [0, 0], 1: [2, 2], 4: [4, 4]}, ants_num=5, iterations_num=5, |
| 47 | + ... pheromone_evaporation=0.7, alpha=1.0, beta=5.0, q=10) |
| 48 | + Traceback (most recent call last): |
| 49 | + ... |
| 50 | + IndexError: list index out of range |
| 51 | + >>> main(cities={}, ants_num=5, iterations_num=5, |
| 52 | + ... pheromone_evaporation=0.7, alpha=1.0, beta=5.0, q=10) |
| 53 | + Traceback (most recent call last): |
| 54 | + ... |
| 55 | + StopIteration |
| 56 | + >>> main(cities={0: [0, 0], 1: [2, 2]}, ants_num=0, iterations_num=5, |
| 57 | + ... pheromone_evaporation=0.7, alpha=1.0, beta=5.0, q=10) |
| 58 | + ([], inf) |
| 59 | + >>> main(cities={0: [0, 0], 1: [2, 2]}, ants_num=5, iterations_num=0, |
| 60 | + ... pheromone_evaporation=0.7, alpha=1.0, beta=5.0, q=10) |
| 61 | + ([], inf) |
| 62 | + >>> main(cities={0: [0, 0], 1: [2, 2]}, ants_num=5, iterations_num=5, |
| 63 | + ... pheromone_evaporation=1, alpha=1.0, beta=5.0, q=10) |
| 64 | + ([0, 1, 0], 5.656854249492381) |
| 65 | + >>> main(cities={0: [0, 0], 1: [2, 2]}, ants_num=5, iterations_num=5, |
| 66 | + ... pheromone_evaporation=0, alpha=1.0, beta=5.0, q=10) |
| 67 | + ([0, 1, 0], 5.656854249492381) |
| 68 | + """ |
| 69 | + # Initialize the pheromone matrix |
| 70 | + cities_num = len(cities) |
| 71 | + pheromone = [[1.0] * cities_num] * cities_num |
| 72 | + |
| 73 | + best_path: list[int] = [] |
| 74 | + best_distance = float("inf") |
| 75 | + for _ in range(iterations_num): |
| 76 | + ants_route = [] |
| 77 | + for _ in range(ants_num): |
| 78 | + unvisited_cities = copy.deepcopy(cities) |
| 79 | + current_city = {next(iter(cities.keys())): next(iter(cities.values()))} |
| 80 | + del unvisited_cities[next(iter(current_city.keys()))] |
| 81 | + ant_route = [next(iter(current_city.keys()))] |
| 82 | + while unvisited_cities: |
| 83 | + current_city, unvisited_cities = city_select( |
| 84 | + pheromone, current_city, unvisited_cities, alpha, beta |
| 85 | + ) |
| 86 | + ant_route.append(next(iter(current_city.keys()))) |
| 87 | + ant_route.append(0) |
| 88 | + ants_route.append(ant_route) |
| 89 | + |
| 90 | + pheromone, best_path, best_distance = pheromone_update( |
| 91 | + pheromone, |
| 92 | + cities, |
| 93 | + pheromone_evaporation, |
| 94 | + ants_route, |
| 95 | + q, |
| 96 | + best_path, |
| 97 | + best_distance, |
| 98 | + ) |
| 99 | + return best_path, best_distance |
| 100 | + |
| 101 | + |
| 102 | +def distance(city1: list[int], city2: list[int]) -> float: |
| 103 | + """ |
| 104 | + Calculate the distance between two coordinate points |
| 105 | + >>> distance([0, 0], [3, 4] ) |
| 106 | + 5.0 |
| 107 | + >>> distance([0, 0], [-3, 4] ) |
| 108 | + 5.0 |
| 109 | + >>> distance([0, 0], [-3, -4] ) |
| 110 | + 5.0 |
| 111 | + """ |
| 112 | + return (((city1[0] - city2[0]) ** 2) + ((city1[1] - city2[1]) ** 2)) ** 0.5 |
| 113 | + |
| 114 | + |
| 115 | +def pheromone_update( |
| 116 | + pheromone: list[list[float]], |
| 117 | + cities: dict[int, list[int]], |
| 118 | + pheromone_evaporation: float, |
| 119 | + ants_route: list[list[int]], |
| 120 | + q: float, # Pheromone system parameters Q,which is a constant |
| 121 | + best_path: list[int], |
| 122 | + best_distance: float, |
| 123 | +) -> tuple[list[list[float]], list[int], float]: |
| 124 | + """ |
| 125 | + Update pheromones on the route and update the best route |
| 126 | + >>> |
| 127 | + >>> pheromone_update(pheromone=[[1.0, 1.0], [1.0, 1.0]], |
| 128 | + ... cities={0: [0,0], 1: [2,2]}, pheromone_evaporation=0.7, |
| 129 | + ... ants_route=[[0, 1, 0]], q=10, best_path=[], |
| 130 | + ... best_distance=float("inf")) |
| 131 | + ([[0.7, 4.235533905932737], [4.235533905932737, 0.7]], [0, 1, 0], 5.656854249492381) |
| 132 | + >>> pheromone_update(pheromone=[], |
| 133 | + ... cities={0: [0,0], 1: [2,2]}, pheromone_evaporation=0.7, |
| 134 | + ... ants_route=[[0, 1, 0]], q=10, best_path=[], |
| 135 | + ... best_distance=float("inf")) |
| 136 | + Traceback (most recent call last): |
| 137 | + ... |
| 138 | + IndexError: list index out of range |
| 139 | + >>> pheromone_update(pheromone=[[1.0, 1.0], [1.0, 1.0]], |
| 140 | + ... cities={}, pheromone_evaporation=0.7, |
| 141 | + ... ants_route=[[0, 1, 0]], q=10, best_path=[], |
| 142 | + ... best_distance=float("inf")) |
| 143 | + Traceback (most recent call last): |
| 144 | + ... |
| 145 | + KeyError: 0 |
| 146 | + """ |
| 147 | + for a in range(len(cities)): # Update the volatilization of pheromone on all routes |
| 148 | + for b in range(len(cities)): |
| 149 | + pheromone[a][b] *= pheromone_evaporation |
| 150 | + for ant_route in ants_route: |
| 151 | + total_distance = 0.0 |
| 152 | + for i in range(len(ant_route) - 1): # Calculate total distance |
| 153 | + total_distance += distance(cities[ant_route[i]], cities[ant_route[i + 1]]) |
| 154 | + delta_pheromone = q / total_distance |
| 155 | + for i in range(len(ant_route) - 1): # Update pheromones |
| 156 | + pheromone[ant_route[i]][ant_route[i + 1]] += delta_pheromone |
| 157 | + pheromone[ant_route[i + 1]][ant_route[i]] = pheromone[ant_route[i]][ |
| 158 | + ant_route[i + 1] |
| 159 | + ] |
| 160 | + |
| 161 | + if total_distance < best_distance: |
| 162 | + best_path = ant_route |
| 163 | + best_distance = total_distance |
| 164 | + |
| 165 | + return pheromone, best_path, best_distance |
| 166 | + |
| 167 | + |
| 168 | +def city_select( |
| 169 | + pheromone: list[list[float]], |
| 170 | + current_city: dict[int, list[int]], |
| 171 | + unvisited_cities: dict[int, list[int]], |
| 172 | + alpha: float, |
| 173 | + beta: float, |
| 174 | +) -> tuple[dict[int, list[int]], dict[int, list[int]]]: |
| 175 | + """ |
| 176 | + Choose the next city for ants |
| 177 | + >>> city_select(pheromone=[[1.0, 1.0], [1.0, 1.0]], current_city={0: [0, 0]}, |
| 178 | + ... unvisited_cities={1: [2, 2]}, alpha=1.0, beta=5.0) |
| 179 | + ({1: [2, 2]}, {}) |
| 180 | + >>> city_select(pheromone=[], current_city={0: [0,0]}, |
| 181 | + ... unvisited_cities={1: [2, 2]}, alpha=1.0, beta=5.0) |
| 182 | + Traceback (most recent call last): |
| 183 | + ... |
| 184 | + IndexError: list index out of range |
| 185 | + >>> city_select(pheromone=[[1.0, 1.0], [1.0, 1.0]], current_city={}, |
| 186 | + ... unvisited_cities={1: [2, 2]}, alpha=1.0, beta=5.0) |
| 187 | + Traceback (most recent call last): |
| 188 | + ... |
| 189 | + StopIteration |
| 190 | + >>> city_select(pheromone=[[1.0, 1.0], [1.0, 1.0]], current_city={0: [0, 0]}, |
| 191 | + ... unvisited_cities={}, alpha=1.0, beta=5.0) |
| 192 | + Traceback (most recent call last): |
| 193 | + ... |
| 194 | + IndexError: list index out of range |
| 195 | + """ |
| 196 | + probabilities = [] |
| 197 | + for city in unvisited_cities: |
| 198 | + city_distance = distance( |
| 199 | + unvisited_cities[city], next(iter(current_city.values())) |
| 200 | + ) |
| 201 | + probability = (pheromone[city][next(iter(current_city.keys()))] ** alpha) * ( |
| 202 | + (1 / city_distance) ** beta |
| 203 | + ) |
| 204 | + probabilities.append(probability) |
| 205 | + |
| 206 | + chosen_city_i = random.choices( |
| 207 | + list(unvisited_cities.keys()), weights=probabilities |
| 208 | + )[0] |
| 209 | + chosen_city = {chosen_city_i: unvisited_cities[chosen_city_i]} |
| 210 | + del unvisited_cities[next(iter(chosen_city.keys()))] |
| 211 | + return chosen_city, unvisited_cities |
| 212 | + |
| 213 | + |
| 214 | +if __name__ == "__main__": |
| 215 | + best_path, best_distance = main( |
| 216 | + cities=cities, |
| 217 | + ants_num=10, |
| 218 | + iterations_num=20, |
| 219 | + pheromone_evaporation=0.7, |
| 220 | + alpha=1.0, |
| 221 | + beta=5.0, |
| 222 | + q=10, |
| 223 | + ) |
| 224 | + |
| 225 | + print(f"{best_path = }") |
| 226 | + print(f"{best_distance = }") |
0 commit comments