-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathsolvers.py
392 lines (314 loc) · 12.2 KB
/
solvers.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
"""
Module containing class implementations of different TSP solver algorithms.
The classes extend an abstract Solver class, which contains abstract method
solve(), get_next_order(), and get_best_order()
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from heapq import heappop, heappush
from math import dist, exp, factorial, log10
from random import randrange, shuffle, uniform
INFTY = float("inf")
class Solver(ABC):
"""
Abstract class to be extended by other solvers
"""
def __init__(self, cities: list[tuple[float, float]]):
self.cities = cities
self.n = len(cities)
self.order = list(range(self.n))
self.adj = [
[dist(i, j) if i != j else 0 for i in cities] for j in cities
]
@staticmethod
def get_solver(solver_name: str, cities: list[tuple[float, float]]) -> "Solver":
"""
Return a new solver based off the name
@param solver_name: the name of the solver
@param cities: a list of the coordinates of the cities
@return: the solver with the respective name
"""
solver_name = solver_name.lower().replace(" ", "")
if solver_name == "bruteforce":
return BruteForce(cities)
if solver_name == "branchandbound":
return BranchAndBound(cities)
if solver_name == "simulatedannealing":
return SimulatedAnnealing(cities)
raise Exception("Invalid solver name")
def get_total_dist(self, order: list[int]) -> float:
"""
Get the total distance between the cities based off the ordering
@param order: a list containing the order of cities to visit
@return: the total distance of travelling in the order and returning to the start
"""
if not order:
return 0
total = 0.0
for i in range(len(order) - 1):
total += self.adj[order[i + 1]][order[i]]
total += self.adj[order[-1]][order[0]]
return total
@abstractmethod
def solve(self) -> None:
"""
Loop the get_next_order() method until the solver
has found the optimal (or what it determines) to be
the optimal solution
"""
@abstractmethod
def get_next_order(self) -> list[int]:
"""
Returns the list of the next ordering of the path.
@return an empty list if there is no more orderings.
"""
@abstractmethod
def get_best_order(self) -> list[int]:
"""
@return the list of the current best ordering.
"""
################################################################################
class SimulatedAnnealing(Solver):
"""
Solver using the simulated annealing algorithm
"""
def __init__(self, cities, t: float = -1, r: float = -1):
super().__init__(cities)
shuffle(self.order)
self.temperature = self.get_diff_city_dist(cities) if t == -1 else t
if r != -1:
self.cooling_rate = r
else:
self.cooling_rate = 1 - 100 ** (-log10(self.n) + 1) if self.n != 0 else 0
self.initial_temperature = self.temperature
self.curr_dist = self.get_total_dist(self.order)
self.solved = False
self.__iterations = 0
self.__max_repeats = int(100 * (1 / (1 - self.cooling_rate)))
@staticmethod
def get_diff_city_dist(cities: list[tuple[float, float]]) -> float:
"""
Get the distances between all the cities, and then return the
average difference of the distances
@param cities: a list of the coordinates of the cities
@return: the average difference in distances between the cities
"""
if cities == []:
return 0
n = len(cities)
# Generate all combination of distances in sorted order in O(n^2 log n)
d = sorted([dist(cities[i], cities[j]) for i in range(n - 1) for j in range(i + 1, n)])
m = len(d)
# Find the total difference between the smallest distance and the rest
prev_diff = sum([x - d[0] for x in d[1:]])
# Loop over the rest of the values calculating total difference
total_diff = prev_diff
for i in range(1, m):
prev_diff = prev_diff - (d[i] - d[i - 1]) * (m - i)
total_diff += prev_diff
# Return the average distance
return total_diff / ((m * (m - 1)) / 2)
def solve(self) -> None:
"""
Continue cooling and finding distance until the optimal distance has
not changed after self.__max_repeats iterations
"""
repeat = 0
lowest_dist = float("inf")
while repeat < self.__max_repeats:
self.get_next_order()
if self.curr_dist < lowest_dist:
repeat = 0
lowest_dist = self.curr_dist
else:
repeat += 1
self.solved = True
def get_next_order(self) -> list[int]:
self.__iterations += 1
self.temperature = self.temperature * self.cooling_rate
# Find new order
a, b = self.get_two_cities()
loss = self.get_swap_cost(a, b)
prob = 0 if (loss <= 0 or self.temperature <= 0) else exp(-loss / self.temperature)
# If new distance shorter, or within probability then use it
if loss <= 0 or uniform(0, 1) < prob:
self.two_opt(a, b)
self.curr_dist = self.get_total_dist(self.order)
return self.order
def get_best_order(self) -> list[int]:
return self.order
def get_two_cities(self) -> tuple[int, int]:
"""
@return: two indexes between 0 and n, where the first is smaller
"""
a = randrange(self.n)
b = randrange(self.n)
return (a, b) if a < b else (b, a)
def get_swap_cost(self, a: int, b: int) -> float:
"""
Given two indexes, return the cost if we were to reverse the
ordering between the two indexes
@param a: the lower index
@param b: the higher index
@return: the change in distance after the swap
"""
n, order = self.n, self.order
# Find which cities a and b are, and their next city
a1, a2 = order[a], order[(a + 1) % n]
b1, b2 = order[b], order[(b + 1) % n]
# Find the current and new distance
curr_dist = self.adj[a1][a2] + self.adj[b1][b2]
new_dist = self.adj[a1][b1] + self.adj[a2][b2]
return new_dist - curr_dist
def two_opt(self, a: int, b: int) -> None:
"""
Reverse the position between two values in the ordering,
so that the path "uncrosses" itself
@param a: the lower index
@param b: the higher index
"""
self.order = self.order[:a + 1] + self.order[b:a:-1] + self.order[b + 1:]
@property
def iterations(self) -> int:
"""
@return the current number of iterations, else if solved return the number
of iterations required to converge to the solution
"""
if self.solved:
return self.__iterations - self.__max_repeats
return self.__iterations
################################################################################
class BruteForce(Solver):
"""
Solver by brute forcing the lexicographical ordering
"""
def __init__(self, cities):
super().__init__(cities)
self.best_order = list(range(self.n))
def solve(self) -> None:
for _ in range(factorial(self.n - 1)):
self.get_next_order()
def get_next_order(self) -> list[int]:
"""
Find the next path in lexicographical ordering
@return: the next path found
"""
order = self.order.copy()
x = -1
for i in range(1, self.n - 1):
if order[i] < order[i + 1]:
x = i
if x == -1:
return []
y = -1
for j in range(self.n):
if order[x] < order[j]:
y = j
order[x], order[y] = order[y], order[x]
order = order[:x + 1] + order[:x:-1]
# Update ordering
self.order = order
# Find the best ordering
curr_dist = self.get_total_dist(self.best_order)
new_dist = self.get_total_dist(self.order)
if new_dist <= curr_dist:
self.best_order = order
return self.order
def get_best_order(self) -> list[int]:
return self.best_order
################################################################################
class BranchAndBound(Solver):
"""
Solver using the branch and bound algorithm
"""
def __init__(self, cities):
super().__init__(cities)
self.adj = [
[dist(i, j) if i != j else INFTY for i in cities] for j in cities
]
self.cost = reduce_adj(self.adj, self.n)
adj_arr = self.adj.copy()
cost = reduce_adj(adj_arr, self.n)
self.paths = [BranchAndBoundPath(adj_arr, cost, [0])]
self.found_best = False
def solve(self) -> None:
order = self.get_best_order()
while len(order) < self.n:
order = self.get_best_order()
def get_next_order(self) -> list[int]:
"""
Find the next order using the branch and bound method
@return: the next optimal path that we have found so far
"""
if self.found_best:
return []
# Look at the best possible non complete
curr_path = heappop(self.paths)
curr_node = curr_path.order[-1]
curr_adj = curr_path.adj
curr_cost = curr_path.cost
# List of the next possible cities that we can go to
next_cities = [i for i in range(self.n) if i not in curr_path.order]
# Generate their paths
for next_node in next_cities:
# Find the new adj matrix after travelling to the next node
new_adj = set_infty(curr_adj, curr_node, next_node)
# Base cost is the cost of travelling from the curr_node to next_node
base_cost = curr_adj[curr_node][next_node]
new_cost = curr_cost + base_cost + reduce_adj(new_adj, self.n)
new_order = curr_path.order + [next_node]
heappush(self.paths, BranchAndBoundPath(new_adj, new_cost, new_order))
result = self.get_best_order()
self.found_best = len(result) == self.n
return result
def get_best_order(self) -> list[int]:
order = self.paths[0].order
return order
################################################################################
class BranchAndBoundPath:
"""
A class for which contains an adjacency matrix, cost and ordering which
has an __lt__ method to support sorting by cost
"""
def __init__(self, adj: list[list[float]], cost: float, order: list[int]):
self.adj = adj
self.cost = cost
self.order = order
def __lt__(self, other) -> bool:
return self.cost < other.cost
def set_infty(arr: list[list[float]], r: int, c: int) -> list[list[float]]:
"""
Given a 2D square array and the index of a row and column, return a new list
where the values of the row and col is now set to infinity
@param arr: 2d list of floats
@param r: the row to set to infinity
@param c: the col to set to infinity
@return: the new list with values set to infinity
"""
n = len(arr)
return [[INFTY if (i == r or j == c) else arr[i][j] for j in range(n)] for i in range(n)]
def reduce_adj(arr: list[list[float]], n: int) -> float:
"""
Subtract the minimum value of each row from the row, and subtract the
minimum value of each col from the col. Then return the total subtracted
@param arr: 2d list of floats
@return: the total value reduced
"""
total = 0.0
for i in range(n):
row = [val for val in arr[i] if val != INFTY]
if row:
row_min = min(row)
total += row_min
for j in range(n):
if arr[i][j] != INFTY:
arr[i][j] -= row_min
for j in range(n):
col = [arr[i][j] for i in range(n) if arr[i][j] != INFTY]
if col:
col_min = min(col)
total += col_min
for i in range(n):
if arr[i][j] != INFTY:
arr[i][j] -= col_min
return total