Skip to content

Commit 8f3ab82

Browse files
authored
Add files via upload
1 parent c264fe7 commit 8f3ab82

File tree

2 files changed

+366
-0
lines changed

2 files changed

+366
-0
lines changed

functions.py

Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
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-')

main.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from functions import *
2+
3+
random_seed = 123456789
4+
np.random.seed(random_seed)
5+
random.seed(random_seed)
6+
7+
# Parameters
8+
N = 30 # n. of cities to visit
9+
border_len = 500 # length of border for random city placement
10+
ITERATIONS_WO_IMPROVEMENT_LIMIT = 60 # maximum iterations without improvement before termination
11+
N_POP = N * 4 # population size
12+
CROSSOVER_PROB = 0.95 # initial probability of crossover during genetic algorithm
13+
MUTATION_PROB = 1 / N # initial probability of mutation during genetic algorithm
14+
15+
# Cities to visit
16+
CITIES_RAND = np.random.rand(N, 2) * border_len
17+
CITIES_CIRCLE = randomize_cities_on_circle(N, border_len // 2 - 10)
18+
19+
# Performing TSP on two sets of points
20+
best_results, median_results, worst_results, n_generations, best_solution = (
21+
tsp(N, N_POP, CROSSOVER_PROB, MUTATION_PROB, CITIES_RAND, ITERATIONS_WO_IMPROVEMENT_LIMIT))
22+
23+
circle_results = tsp(N, N_POP, CROSSOVER_PROB, MUTATION_PROB, CITIES_CIRCLE,
24+
ITERATIONS_WO_IMPROVEMENT_LIMIT)
25+
circle_bs = circle_results[-1]
26+
27+
# Visualization
28+
layout = [
29+
["A", "A"],
30+
["B", "C"],
31+
]
32+
33+
fig, axes = plt.subplot_mosaic(layout, figsize=(12, 12))
34+
fig.suptitle('Travelling salesman problem', fontsize=22)
35+
fig.patch.set_facecolor('white')
36+
37+
# Line plot with statistics
38+
temp = [i for i in range(1, n_generations + 1)]
39+
axes["A"].plot(temp, median_results, label='Median Road', color='green', linestyle='--')
40+
axes["A"].plot(temp, best_results, label="Best Solution", color='blue')
41+
axes["A"].plot(temp, worst_results, label='Worst Solution', color='red', linestyle='--')
42+
axes["A"].legend()
43+
axes["A"].set_xlabel(f'Generations')
44+
axes["A"].set_ylabel('Fitness function - total road')
45+
axes["A"].set_title(f'Data = CITIES_RAND; Number of cities = {N}; Population size = {N_POP}\n'
46+
f'The shortest road found: {best_results[-1]:.4f}\n'
47+
f'Total number of generations: {n_generations}')
48+
axes["A"].set_xlim(0, n_generations)
49+
50+
# Points and the best route for CITIES_RAND
51+
plot_points_and_road(CITIES_RAND, best_solution, axes["B"])
52+
axes["B"].set_title("CITIES_RAND")
53+
54+
# Points and the best route for CITIES_CIRCLE
55+
plot_points_and_road(CITIES_CIRCLE, circle_bs, axes["C"])
56+
axes["C"].set_title("CITIES_CIRCLE")
57+
58+
plt.show()

0 commit comments

Comments
 (0)