-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfunctions.py
More file actions
308 lines (245 loc) · 10.8 KB
/
functions.py
File metadata and controls
308 lines (245 loc) · 10.8 KB
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
import random
import numpy as np
import matplotlib.pyplot as plt
def tsp(N, N_POP, CROSSOVER_PROB, MUTATION_PROB, CITIES,
ITERATIONS_WO_IMPROVEMENT_LIMIT):
"""
Solves the Traveling Salesman Problem using a genetic algorithm.
Parameters:
N (int): Number of cities.
N_POP (int): Population size.
CROSSOVER_PROB (float): Initial probability of crossover during genetic algorithm.
MUTATION_PROB (float): Initial probability of mutation during genetic algorithm.
CITIES (numpy.ndarray): 2D array containing coordinates of cities.
ITERATIONS_WO_IMPROVEMENT_LIMIT (int): Maximum iterations without improvement before termination.
Returns:
tuple: A tuple containing:
- list: Best fitness value found in each generation.
- list: Median fitness value found in each generation.
- list: Worst fitness value found in each generation.
- int: Total number of generations.
- list: Best solution found.
"""
def fitness_function(v):
"""
Calculates the fitness function for a given route.
Parameters:
v (list): A list representing the route as a permutation of cities.
Returns:
float: The total distance traveled along the route.
"""
distance = 0
for i in range(len(v)):
distance += np.linalg.norm(CITIES[v[i - 1]] - CITIES[v[i]])
return distance
def selection(population):
"""
Performs selection on the population using ranking method.
Parameters:
population (list): A list of candidate solutions (routes).
Returns:
list: The selected candidates for the next generation.
"""
return population[:N_POP // 3]
def crossover(parent_1, parent_2):
"""
Performs crossover between two parent routes to create a child route.
Parameters:
parent_1 (list): The first parent route.
parent_2 (list): The second parent route.
Returns:
list: The child route generated by crossover.
"""
# Select random positions for crossover
begin_pos = np.random.randint(N)
end_pos = np.random.randint(N)
if begin_pos > end_pos:
begin_pos, end_pos = end_pos, begin_pos
# Extract the crossover segment from parent_1
cells = parent_1[begin_pos:end_pos + 1]
# Initialize child route
child = [0] * N
# Place the crossover segment into the child route
child[begin_pos:end_pos + 1] = cells[:]
j = 0
for i in range(N):
# Check if the child route is fully filled
if j == N:
break
# Skip the segment already filled with the crossover from parent_1
if j == begin_pos:
j += len(cells)
# If the current city from parent_2 is not in the crossover segment,
# place it into the child route
if parent_2[i] not in cells:
child[j] = parent_2[i]
j += 1
return child
def generation(population, n, cross_prob):
"""
Generates a new individual for the next generation using crossover.
Parameters:
population (list): The current population of individuals.
n (int): The size of the population.
cross_prob (float): The probability of crossover.
Returns:
list: The new individual generated for the next generation.
"""
# Select a parent 1 randomly
p1_index = np.random.randint(n)
p1 = population[p1_index][0]
# Select a parent 2 randomly, ensuring it's not the same as parent 1
while True:
p2_index = np.random.randint(n)
if p2_index != p1_index:
break
p2 = population[p2_index][0]
# Perform crossover based on crossover probability
if cross_prob > np.random.rand():
return crossover(p1, p2)
else:
# If crossover is not performed, repeat the process
return generation(population, n, cross_prob)
def mutation(v):
"""
Performs mutation on an individual.
Parameters:
v (list): The individual (route) to be mutated.
Returns:
list: The mutated individual.
"""
# Mutation type 1: Swap mutation
if np.random.rand() < 0.5:
# Select two distinct indices randomly for swapping
i1 = np.random.randint(N)
while True:
i2 = np.random.randint(N)
if i2 != i1:
break
# Swap the cities at the selected indices
v[i1], v[i2] = v[i2], v[i1]
else:
# Mutation type 2: Shift mutation
# Select parameters for shift mutation
group_size = np.random.randint(2, N // 2)
start_index = np.random.randint(0, N - group_size)
direction = np.random.choice([-1, 1])
shift_amount = np.random.randint(1, group_size)
if direction == -1:
# Shift the group of cities to the left
temp = v[start_index:start_index + shift_amount]
v[start_index:start_index + group_size - shift_amount] = v[
start_index + shift_amount:start_index + group_size]
v[start_index + group_size - shift_amount:start_index + group_size] = temp
else:
# Shift the group of cities to the right
temp = v[start_index + group_size - shift_amount:start_index + group_size]
v[start_index + shift_amount:start_index + group_size] = v[
start_index:start_index + group_size - shift_amount]
v[start_index:start_index + shift_amount] = temp
def mutation_probability(initial_prob, generation):
"""
Calculates the mutation probability for a given generation.
Parameters:
initial_prob (float): The initial mutation probability.
generation (int): The current generation.
Returns:
float: The mutation probability for the given generation.
"""
decay_rate = 1e-4
return max(0, initial_prob - decay_rate * generation)
def crossover_probability(initial_prob, generation):
"""
Calculates the crossover probability for a given generation.
Parameters:
initial_prob (float): The initial crossover probability.
generation (int): The current generation.
Returns:
float: The crossover probability for the given generation.
"""
decay_rate = 3e-4
return max(0, initial_prob - decay_rate * generation)
# Starting population
sequence = [i for i in range(N)]
first_pop = []
# Generating initial population with random shuffling of cities
for i in range(N_POP):
random.shuffle(sequence)
first_pop.append([sequence[:], fitness_function(sequence)])
# Sorting the initial population based on fitness
first_pop = sorted(first_pop, key=lambda x: x[1])
best = first_pop[0][1]
best_results = [best]
# Calculating median fitness of the initial population
median_results = [(first_pop[N_POP // 2][1] + first_pop[N_POP // 2 - 1][1]) / 2]
# Storing the fitness of the worst individual in the initial population
worst_results = [first_pop[-1][1]]
n_generations = 1
new_gen = first_pop.copy()
i_no_imp = 0
# Iterating until reaching the limit of iterations without improvement
while i_no_imp < ITERATIONS_WO_IMPROVEMENT_LIMIT:
new_gen = selection(new_gen)
curr_cross_prob = crossover_probability(CROSSOVER_PROB, n_generations)
curr_mut_prob = mutation_probability(MUTATION_PROB, n_generations)
# Generating new individuals for the next generation using crossover and mutation
for i in range(N_POP - len(new_gen)):
b = generation(new_gen, len(new_gen), curr_cross_prob)
new_gen.append([b, fitness_function(b)])
# Applying mutation to a subset of the population
for i in new_gen:
if random.random() < curr_mut_prob:
mutation(i[0])
# Sorting the new generation based on fitness
new_gen = sorted(new_gen, key=lambda x: x[1])
# Checking for improvement in the best fitness
if new_gen[0][1] < best:
best = new_gen[0][1]
i_no_imp = 0
else:
i_no_imp += 1
# Updating lists for tracking the evolution of solutions
best_results.append(best)
median_results.append((new_gen[N_POP // 2][1] + new_gen[N_POP // 2 - 1][1]) / 2)
worst_results.append(new_gen[-1][1])
n_generations += 1
# Storing the final results
results = (best_results, median_results, worst_results, n_generations, new_gen[0][0])
return results
def randomize_cities_on_circle(num_cities, radius):
"""
Randomly distributes cities on a circle.
Parameters:
num_cities (int): Number of cities to distribute.
radius (float): Radius of the circle.
Returns:
numpy.ndarray: 2D array containing coordinates of cities placed on the circle.
"""
# Generate angles evenly spaced around the circle
angles = np.linspace(0, 2 * np.pi, num_cities, endpoint=False)
# Calculate x and y coordinates of cities using trigonometric functions
x_coordinates = radius * np.cos(angles)
y_coordinates = radius * np.sin(angles)
# Stack x and y coordinates to form city locations
cities = np.column_stack((x_coordinates, y_coordinates))
return cities
def plot_points_and_road(points, solution, ax):
"""
Plots points representing cities and the route connecting them.
Parameters:
points (numpy.ndarray): 2D array containing coordinates of cities.
solution (list): List containing the order of cities in the route.
ax (matplotlib.axes.Axes): Axes object for plotting.
Returns:
None
"""
# Plot cities as blue circles
ax.plot(points[:, 0], points[:, 1], 'bo')
ax.set_aspect('equal')
ax.set_xlabel("X")
ax.set_ylabel("Y")
ax.grid()
# Plot the route connecting cities
for i in range(-1, len(solution) - 1):
ax.plot([points[solution[i], 0], points[solution[i + 1], 0]],
[points[solution[i], 1], points[solution[i + 1], 1]], 'k-')