1
+ import random
2
+ import numpy as np
3
+ import matplotlib .pyplot as plt
4
+
5
+
6
+ def tsp (N , N_POP , CROSSOVER_PROB , MUTATION_PROB , CITIES ,
7
+ ITERATIONS_WO_IMPROVEMENT_LIMIT ):
8
+ """
9
+ Solves the Traveling Salesman Problem using a genetic algorithm.
10
+
11
+ Parameters:
12
+ N (int): Number of cities.
13
+ N_POP (int): Population size.
14
+ CROSSOVER_PROB (float): Initial probability of crossover during genetic algorithm.
15
+ MUTATION_PROB (float): Initial probability of mutation during genetic algorithm.
16
+ CITIES (numpy.ndarray): 2D array containing coordinates of cities.
17
+ ITERATIONS_WO_IMPROVEMENT_LIMIT (int): Maximum iterations without improvement before termination.
18
+
19
+ Returns:
20
+ tuple: A tuple containing:
21
+ - list: Best fitness value found in each generation.
22
+ - list: Median fitness value found in each generation.
23
+ - list: Worst fitness value found in each generation.
24
+ - int: Total number of generations.
25
+ - list: Best solution found.
26
+ """
27
+
28
+
29
+ def fitness_function (v ):
30
+ """
31
+ Calculates the fitness function for a given route.
32
+
33
+ Parameters:
34
+ v (list): A list representing the route as a permutation of cities.
35
+
36
+ Returns:
37
+ float: The total distance traveled along the route.
38
+ """
39
+ distance = 0
40
+ for i in range (len (v )):
41
+ distance += np .linalg .norm (CITIES [v [i - 1 ]] - CITIES [v [i ]])
42
+ return distance
43
+
44
+
45
+ def selection (population ):
46
+ """
47
+ Performs selection on the population using ranking method.
48
+
49
+ Parameters:
50
+ population (list): A list of candidate solutions (routes).
51
+
52
+ Returns:
53
+ list: The selected candidates for the next generation.
54
+ """
55
+ return population [:N_POP // 3 ]
56
+
57
+
58
+ def crossover (parent_1 , parent_2 ):
59
+ """
60
+ Performs crossover between two parent routes to create a child route.
61
+
62
+ Parameters:
63
+ parent_1 (list): The first parent route.
64
+ parent_2 (list): The second parent route.
65
+
66
+ Returns:
67
+ list: The child route generated by crossover.
68
+ """
69
+ # Select random positions for crossover
70
+ begin_pos = np .random .randint (N )
71
+ end_pos = np .random .randint (N )
72
+ if begin_pos > end_pos :
73
+ begin_pos , end_pos = end_pos , begin_pos
74
+
75
+ # Extract the crossover segment from parent_1
76
+ cells = parent_1 [begin_pos :end_pos + 1 ]
77
+
78
+ # Initialize child route
79
+ child = [0 ] * N
80
+
81
+ # Place the crossover segment into the child route
82
+ child [begin_pos :end_pos + 1 ] = cells [:]
83
+
84
+ j = 0
85
+ for i in range (N ):
86
+ # Check if the child route is fully filled
87
+ if j == N :
88
+ break
89
+ # Skip the segment already filled with the crossover from parent_1
90
+ if j == begin_pos :
91
+ j += len (cells )
92
+
93
+ # If the current city from parent_2 is not in the crossover segment,
94
+ # place it into the child route
95
+ if parent_2 [i ] not in cells :
96
+ child [j ] = parent_2 [i ]
97
+ j += 1
98
+
99
+ return child
100
+
101
+
102
+ def generation (population , n , cross_prob ):
103
+ """
104
+ Generates a new individual for the next generation using crossover.
105
+
106
+ Parameters:
107
+ population (list): The current population of individuals.
108
+ n (int): The size of the population.
109
+ cross_prob (float): The probability of crossover.
110
+
111
+ Returns:
112
+ list: The new individual generated for the next generation.
113
+ """
114
+ # Select a parent 1 randomly
115
+ p1_index = np .random .randint (n )
116
+ p1 = population [p1_index ][0 ]
117
+
118
+ # Select a parent 2 randomly, ensuring it's not the same as parent 1
119
+ while True :
120
+ p2_index = np .random .randint (n )
121
+ if p2_index != p1_index :
122
+ break
123
+ p2 = population [p2_index ][0 ]
124
+
125
+ # Perform crossover based on crossover probability
126
+ if cross_prob > np .random .rand ():
127
+ return crossover (p1 , p2 )
128
+ else :
129
+ # If crossover is not performed, repeat the process
130
+ return generation (population , n , cross_prob )
131
+
132
+
133
+ def mutation (v ):
134
+ """
135
+ Performs mutation on an individual.
136
+
137
+ Parameters:
138
+ v (list): The individual (route) to be mutated.
139
+
140
+ Returns:
141
+ list: The mutated individual.
142
+ """
143
+ # Mutation type 1: Swap mutation
144
+ if np .random .rand () < 0.5 :
145
+ # Select two distinct indices randomly for swapping
146
+ i1 = np .random .randint (N )
147
+ while True :
148
+ i2 = np .random .randint (N )
149
+ if i2 != i1 :
150
+ break
151
+ # Swap the cities at the selected indices
152
+ v [i1 ], v [i2 ] = v [i2 ], v [i1 ]
153
+ else :
154
+ # Mutation type 2: Shift mutation
155
+ # Select parameters for shift mutation
156
+ group_size = np .random .randint (2 , N // 2 )
157
+ start_index = np .random .randint (0 , N - group_size )
158
+ direction = np .random .choice ([- 1 , 1 ])
159
+ shift_amount = np .random .randint (1 , group_size )
160
+
161
+ if direction == - 1 :
162
+ # Shift the group of cities to the left
163
+ temp = v [start_index :start_index + shift_amount ]
164
+ v [start_index :start_index + group_size - shift_amount ] = v [
165
+ start_index + shift_amount :start_index + group_size ]
166
+ v [start_index + group_size - shift_amount :start_index + group_size ] = temp
167
+ else :
168
+ # Shift the group of cities to the right
169
+ temp = v [start_index + group_size - shift_amount :start_index + group_size ]
170
+ v [start_index + shift_amount :start_index + group_size ] = v [
171
+ start_index :start_index + group_size - shift_amount ]
172
+ v [start_index :start_index + shift_amount ] = temp
173
+
174
+
175
+ def mutation_probability (initial_prob , generation ):
176
+ """
177
+ Calculates the mutation probability for a given generation.
178
+
179
+ Parameters:
180
+ initial_prob (float): The initial mutation probability.
181
+ generation (int): The current generation.
182
+
183
+ Returns:
184
+ float: The mutation probability for the given generation.
185
+ """
186
+ decay_rate = 1e-4
187
+ return max (0 , initial_prob - decay_rate * generation )
188
+
189
+
190
+ def crossover_probability (initial_prob , generation ):
191
+ """
192
+ Calculates the crossover probability for a given generation.
193
+
194
+ Parameters:
195
+ initial_prob (float): The initial crossover probability.
196
+ generation (int): The current generation.
197
+
198
+ Returns:
199
+ float: The crossover probability for the given generation.
200
+ """
201
+ decay_rate = 3e-4
202
+ return max (0 , initial_prob - decay_rate * generation )
203
+
204
+
205
+ # Starting population
206
+ sequence = [i for i in range (N )]
207
+ first_pop = []
208
+
209
+ # Generating initial population with random shuffling of cities
210
+ for i in range (N_POP ):
211
+ random .shuffle (sequence )
212
+ first_pop .append ([sequence [:], fitness_function (sequence )])
213
+
214
+ # Sorting the initial population based on fitness
215
+ first_pop = sorted (first_pop , key = lambda x : x [1 ])
216
+ best = first_pop [0 ][1 ]
217
+ best_results = [best ]
218
+
219
+ # Calculating median fitness of the initial population
220
+ median_results = [(first_pop [N_POP // 2 ][1 ] + first_pop [N_POP // 2 - 1 ][1 ]) / 2 ]
221
+
222
+ # Storing the fitness of the worst individual in the initial population
223
+ worst_results = [first_pop [- 1 ][1 ]]
224
+ n_generations = 1
225
+
226
+ new_gen = first_pop .copy ()
227
+ i_no_imp = 0
228
+
229
+ # Iterating until reaching the limit of iterations without improvement
230
+ while i_no_imp < ITERATIONS_WO_IMPROVEMENT_LIMIT :
231
+ new_gen = selection (new_gen )
232
+ curr_cross_prob = crossover_probability (CROSSOVER_PROB , n_generations )
233
+ curr_mut_prob = mutation_probability (MUTATION_PROB , n_generations )
234
+
235
+ # Generating new individuals for the next generation using crossover and mutation
236
+ for i in range (N_POP - len (new_gen )):
237
+ b = generation (new_gen , len (new_gen ), curr_cross_prob )
238
+ new_gen .append ([b , fitness_function (b )])
239
+
240
+ # Applying mutation to a subset of the population
241
+ for i in new_gen :
242
+ if random .random () < curr_mut_prob :
243
+ mutation (i [0 ])
244
+
245
+ # Sorting the new generation based on fitness
246
+ new_gen = sorted (new_gen , key = lambda x : x [1 ])
247
+
248
+ # Checking for improvement in the best fitness
249
+ if new_gen [0 ][1 ] < best :
250
+ best = new_gen [0 ][1 ]
251
+ i_no_imp = 0
252
+ else :
253
+ i_no_imp += 1
254
+
255
+ # Updating lists for tracking the evolution of solutions
256
+ best_results .append (best )
257
+ median_results .append ((new_gen [N_POP // 2 ][1 ] + new_gen [N_POP // 2 - 1 ][1 ]) / 2 )
258
+ worst_results .append (new_gen [- 1 ][1 ])
259
+ n_generations += 1
260
+
261
+ # Storing the final results
262
+ results = (best_results , median_results , worst_results , n_generations , new_gen [0 ][0 ])
263
+ return results
264
+
265
+
266
+ def randomize_cities_on_circle (num_cities , radius ):
267
+ """
268
+ Randomly distributes cities on a circle.
269
+
270
+ Parameters:
271
+ num_cities (int): Number of cities to distribute.
272
+ radius (float): Radius of the circle.
273
+
274
+ Returns:
275
+ numpy.ndarray: 2D array containing coordinates of cities placed on the circle.
276
+ """
277
+ # Generate angles evenly spaced around the circle
278
+ angles = np .linspace (0 , 2 * np .pi , num_cities , endpoint = False )
279
+ # Calculate x and y coordinates of cities using trigonometric functions
280
+ x_coordinates = radius * np .cos (angles )
281
+ y_coordinates = radius * np .sin (angles )
282
+ # Stack x and y coordinates to form city locations
283
+ cities = np .column_stack ((x_coordinates , y_coordinates ))
284
+ return cities
285
+
286
+
287
+ def plot_points_and_road (points , solution , ax ):
288
+ """
289
+ Plots points representing cities and the route connecting them.
290
+
291
+ Parameters:
292
+ points (numpy.ndarray): 2D array containing coordinates of cities.
293
+ solution (list): List containing the order of cities in the route.
294
+ ax (matplotlib.axes.Axes): Axes object for plotting.
295
+
296
+ Returns:
297
+ None
298
+ """
299
+ # Plot cities as blue circles
300
+ ax .plot (points [:, 0 ], points [:, 1 ], 'bo' )
301
+ ax .set_aspect ('equal' )
302
+ ax .set_xlabel ("X" )
303
+ ax .set_ylabel ("Y" )
304
+ ax .grid ()
305
+ # Plot the route connecting cities
306
+ for i in range (- 1 , len (solution ) - 1 ):
307
+ ax .plot ([points [solution [i ], 0 ], points [solution [i + 1 ], 0 ]],
308
+ [points [solution [i ], 1 ], points [solution [i + 1 ], 1 ]], 'k-' )
0 commit comments