Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1import numpy as np 

2import scipy.spatial as spatial 

3import itertools 

4from collections import deque 

5 

6# Solve Traveling Salesperson using convex hulls. 

7# re-write of https://github.com/jameskrysiak/ConvexSalesman/blob/master/convex_salesman.py 

8# This like a good explination too https://www.youtube.com/watch?v=syRSy1MFuho 

9 

10 

11def generate_dist_matrix(towns): 

12 """Generate the matrix for the distance between town i and j 

13 

14 Parameters 

15 ---------- 

16 towns : np.array 

17 The x,y positions of the towns 

18 """ 

19 

20 x = towns[:, 0] 

21 y = towns[:, 1] 

22 # Broadcast to i,j 

23 x_dist = x - x[:, np.newaxis] 

24 y_dist = y - y[:, np.newaxis] 

25 distances = np.sqrt(x_dist**2 + y_dist**2) 

26 return distances 

27 

28 

29def route_length(town_indx, dist_matrix): 

30 """Find the length of a route 

31 

32 Parameters 

33 ---------- 

34 town_indx : array of int 

35 The indices of the towns. 

36 dist_matrix : np.array 

37 The matrix where the (i,j) elements are the distance 

38 between the ith and jth town 

39 """ 

40 

41 # This closes the path and return to the start 

42 town_i = town_indx 

43 town_j = np.roll(town_indx, -1) 

44 distances = dist_matrix[town_i, town_j] 

45 return np.sum(distances) 

46 

47 

48def generate_hulls(towns): 

49 """Given an array of x,y points, sort them into concentric hulls 

50 

51 Parameters 

52 ---------- 

53 towns : np.array (n,2) 

54 Array of town x,y positions 

55 

56 Returns 

57 ------- 

58 list of lists of the indices of the concentric hulls 

59 """ 

60 

61 # The indices we have to sort 

62 all_indices = np.arange(towns.shape[0]) 

63 # array to note if a town has been used in a hull 

64 indices_used = np.zeros(towns.shape[0], dtype=bool) 

65 results = [] 

66 

67 # Continue until every point is inside a convex hull. 

68 while False in indices_used: 

69 # Try to find the convex hull of the remaining points. 

70 try: 

71 new_hull = spatial.ConvexHull(towns[all_indices[~indices_used]]) 

72 new_indices = all_indices[~indices_used][new_hull.vertices] 

73 results.append(new_indices.tolist()) 

74 indices_used[new_indices] = True 

75 

76 # In a degenerate case (fewer than three points, points collinear) 

77 # Add all of the remaining points to the innermost convex hull. 

78 except: 

79 results.append(all_indices[~indices_used].tolist()) 

80 indices_used[~indices_used] = True 

81 return results 

82 

83 return results 

84 

85 

86def merge_hulls(indices_lists, dist_matrix): 

87 """Combine the hulls 

88 

89 Parameters 

90 ---------- 

91 indices_list : list of lists with ints 

92 dist_matric : np.array 

93 """ 

94 # start with the outer hull one. Use deque to rotate fast. 

95 collapsed_indices = deque(indices_lists[0]) 

96 for ind_list in indices_lists[1:]: 

97 # insert each point indvidually 

98 for indx in ind_list: 

99 possible_results = [] 

100 possible_lengths = [] 

101 dindex = deque([indx]) 

102 # In theory, I think this could loop over fewer points. Only need to check 

103 # points that can "see" the inner points? 

104 for i in range(len(collapsed_indices)): 

105 collapsed_indices.rotate(1) 

106 possible_results.append(collapsed_indices + dindex) 

107 possible_lengths.append(route_length(possible_results[-1], dist_matrix)) 

108 best = np.min(np.where(possible_lengths == np.min(possible_lengths))) 

109 collapsed_indices = possible_results[best] 

110 return list(collapsed_indices) 

111 

112 

113def three_opt(route, dist_matrix): 

114 """Iterates over all possible 3-opt transformations. 

115 

116 Parameters 

117 --------- 

118 route : list 

119 The indices of the route 

120 dist_matrix : np.array 

121 Distance matrix for the towns 

122 

123 Returns 

124 ------- 

125 min_route : list 

126 The new route 

127 min_length : float 

128 The length of the new route 

129 

130 """ 

131 # The combinations of three places that we can split each route. 

132 combinations = list(itertools.combinations(range(len(route)), 3)) 

133 

134 min_route = route 

135 min_length = route_length(min_route, dist_matrix) 

136 

137 for cuts in combinations: 

138 # The three chunks that the route is broken into based on the cuts. 

139 c1 = route[cuts[0]:cuts[1]] 

140 c2 = route[cuts[1]:cuts[2]] 

141 c3 = route[cuts[2]:] + route[:cuts[0]] 

142 

143 # Reversed chunks 2 and 3. 

144 rc2 = c2[::-1] 

145 rc3 = c3[::-1] 

146 

147 # The unique permutations of all of those chunks. 

148 route_perms = [c1+c2+c3, c1+c3+c2, c1+rc2+c3, c1+c3+rc2, 

149 c1+c2+rc3, c1+rc3+c2, c1+rc2+rc3, c1+rc3+rc2] 

150 

151 # Find the smallest of these permutations. 

152 for perm in route_perms: 

153 temp_length = route_length(perm, dist_matrix) 

154 if temp_length < min_length: 

155 min_length = temp_length 

156 min_route = perm 

157 

158 return min_route, min_length 

159 

160 

161def tsp_convex(towns, optimize=False, niter=10): 

162 """Find a route through towns 

163 

164 Parameters 

165 ---------- 

166 towns : np.array (shape n,2) 

167 The points to find a path through 

168 optimize : bool (False) 

169 Optional to run the 3-opt transformation to optimize route 

170 niter : int (10) 

171 Max number of iterations to run on optimize loop. 

172 

173 Returns 

174 ------- 

175 indices that order towns. 

176 """ 

177 hull_verts = generate_hulls(towns) 

178 dist_matrix = generate_dist_matrix(towns) 

179 route = merge_hulls(hull_verts, dist_matrix) 

180 if optimize: 

181 distance = route_length(route, dist_matrix) 

182 iter_count = 0 

183 optimized = False 

184 while not optimized: 

185 new_route, new_distance = three_opt(route, dist_matrix) 

186 if new_distance < distance: 

187 route = new_route 

188 distance = new_distance 

189 iter_count += 1 

190 else: 

191 optimized = True 

192 if iter_count == niter: 

193 return route 

194 return route