Skip to content

Commit 311ccca

Browse files
Clarkzzzzzcclausspriya-sundaram-dev
authored
add genetic_algorithm/travelling_salesman_problem.py (#11228)
* add genetic_algorithm/travelling_salesman_problem.py * check BUILD * Update comment for pheromone matrix initialization * Update genetic_algorithm/travelling_salesman_problem.py Co-authored-by: priya-sundaram-dev <oc-409d01@agentmail.to> * Update genetic_algorithm/travelling_salesman_problem.py Co-authored-by: priya-sundaram-dev <oc-409d01@agentmail.to> * Update genetic_algorithm/travelling_salesman_problem.py Co-authored-by: priya-sundaram-dev <oc-409d01@agentmail.to> * Apply suggestion from @cclauss * updating DIRECTORY.md --------- Co-authored-by: Christian Clauss <cclauss@me.com> Co-authored-by: priya-sundaram-dev <oc-409d01@agentmail.to> Co-authored-by: cclauss <cclauss@users.noreply.github.com>
1 parent 29399c5 commit 311ccca

2 files changed

Lines changed: 365 additions & 0 deletions

File tree

‎DIRECTORY.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,7 @@
570570

571571
## [Genetic Algorithm](genetic_algorithm)
572572
* [Basic String](genetic_algorithm/basic_string.py)
573+
* [Travelling Salesman Problem](genetic_algorithm/travelling_salesman_problem.py)
573574

574575
## [Geodesy](geodesy)
575576
* [Haversine Distance](geodesy/haversine_distance.py)
Lines changed: 364 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,364 @@
1+
"""
2+
Use a genetic algorithm to solve the travelling salesman problem (TSP)
3+
which asks the following question:
4+
"Given a list of cities and the distances between each pair of cities, what is the
5+
shortest possible route that visits each city exactly once and returns to the origin
6+
city?"
7+
8+
https://en.wikipedia.org/wiki/Genetic_algorithm
9+
https://en.wikipedia.org/wiki/Travelling_salesman_problem
10+
11+
Author: Clark
12+
"""
13+
14+
import copy
15+
import random
16+
17+
cities = {
18+
0: [0, 0],
19+
1: [0, 5],
20+
2: [3, 8],
21+
3: [8, 10],
22+
4: [12, 8],
23+
5: [12, 4],
24+
6: [8, 0],
25+
7: [6, 2],
26+
}
27+
28+
29+
def main(
30+
cities: dict[int, list[int]],
31+
population_size: int,
32+
iterations_num: int,
33+
crossover_probability: float,
34+
mutation_probability: float,
35+
) -> tuple[list[int], float]:
36+
"""
37+
Genetic algorithm main function
38+
39+
The algorithm is stochastic, so seed ``random`` and assert invariants of the
40+
returned tour rather than one exact ordering (not reproducible across
41+
platforms / Python versions).
42+
43+
>>> import random
44+
>>> random.seed(0)
45+
>>> path, best = main(cities=cities, population_size=100, iterations_num=100,
46+
... crossover_probability=0.6, mutation_probability=0.2)
47+
>>> path[0] == 0 and path[-1] == 0 # starts and ends at the origin city
48+
True
49+
>>> sorted(path[:-1]) == sorted(cities) # every city visited exactly once
50+
True
51+
>>> 37 <= best < 45 # converges close to the optimal round-trip (~37.9)
52+
True
53+
>>> main(cities={0: [0, 0], 1: [2, 2]}, population_size=10, iterations_num=10,
54+
... crossover_probability=0.6, mutation_probability=0.2)
55+
([0, 1, 0], 5.656854249492381)
56+
>>> main(cities={},population_size=10,iterations_num=10,
57+
... crossover_probability=0.6,mutation_probability=0.2)
58+
Traceback (most recent call last):
59+
...
60+
IndexError: list assignment index out of range
61+
"""
62+
best_path: list[int] = []
63+
best_distance = float("inf")
64+
65+
chromosomes, cities_list = init(cities, population_size)
66+
fitness_matrix, best_path, best_distance = fitness(
67+
cities, chromosomes, best_path, best_distance
68+
)
69+
for _ in range(iterations_num):
70+
"""
71+
Uncomment to choose another selection operator
72+
Only one of the two selection operators can be uncommented at the same time.
73+
"""
74+
# chromosomes = chose_ts(fitness_matrix, chromosomes, population_size)
75+
chromosomes = chose_rws(fitness_matrix, chromosomes, population_size)
76+
for x in range(int(population_size / 2)): # Population crossover
77+
chromosomes[x], chromosomes[x + int(population_size / 2)] = crossing(
78+
chromosomes[x],
79+
chromosomes[x + int(population_size / 2)],
80+
crossover_probability,
81+
cities_list,
82+
)
83+
for x in range(population_size): # Population variation
84+
chromosomes[x] = mutate(chromosomes[x], mutation_probability)
85+
86+
fitness_matrix, best_path, best_distance = fitness(
87+
cities, chromosomes, best_path, best_distance
88+
)
89+
90+
return best_path, best_distance
91+
92+
93+
def distance(city1: list[int], city2: list[int]) -> float:
94+
"""
95+
Calculate the distance between two coordinate points
96+
>>> distance([0, 0], [3, 4] )
97+
5.0
98+
>>> distance([0, 0], [-3, 4] )
99+
5.0
100+
>>> distance([0, 0], [-3, -4] )
101+
5.0
102+
"""
103+
return (((city1[0] - city2[0]) ** 2) + ((city1[1] - city2[1]) ** 2)) ** 0.5
104+
105+
106+
def init(
107+
cities: dict[int, list[int]], population_size: int
108+
) -> tuple[list[list[int]], list[int]]:
109+
"""
110+
Initialization generates initial population
111+
>>> init(cities={0: [0, 0], 1: [2, 2]}, population_size=2)
112+
([[0, 1, 0], [0, 1, 0]], [1])
113+
>>> init(cities={0: [0, 0], 1: [2, 2]}, population_size=0)
114+
([], [1])
115+
>>> init(cities={},population_size=2)
116+
Traceback (most recent call last):
117+
...
118+
IndexError: list assignment index out of range
119+
"""
120+
chromosomes = []
121+
cities_list = list(cities.keys())
122+
del cities_list[0]
123+
for _ in range(population_size):
124+
chromosome = []
125+
chromosome.append(0) # Add starting point
126+
chromosome.extend(random.sample(cities_list, len(cities_list)))
127+
chromosome.append(0) # Add end point
128+
chromosomes.append(chromosome)
129+
return chromosomes, cities_list
130+
131+
132+
def fitness(
133+
cities: dict[int, list[int]],
134+
chromosomes: list[list[int]],
135+
best_path: list[int],
136+
best_distance: float,
137+
) -> tuple[list[float], list[int], float]:
138+
"""
139+
Calculate population fitness
140+
Generate a fitness matrix and obtain the optimal value in the current population
141+
>>> fitness(cities={0: [0, 0], 1: [2, 2]},chromosomes=[[0,1,0]],
142+
... best_path=[], best_distance=float("inf"))
143+
([0.17677669529663687], [0, 1, 0], 5.656854249492381)
144+
>>> fitness(cities={0: [0, 0], 1: [2, 2]},chromosomes=[[0,1,0],[0,1,0]],
145+
... best_path=[], best_distance=float("inf"))
146+
([0.17677669529663687, 0.17677669529663687], [0, 1, 0], 5.656854249492381)
147+
>>> fitness(cities={}, chromosomes=[[0,1,0]],
148+
... best_path=[], best_distance=float("inf"))
149+
Traceback (most recent call last):
150+
...
151+
KeyError: 0
152+
>>> fitness(cities={0: [0, 0], 1: [2, 2]},chromosomes=[],
153+
... best_path=[], best_distance=float("inf"))
154+
([], [], inf)
155+
"""
156+
fitness_matrix = []
157+
new_best_path = best_path
158+
new_best_distance = best_distance
159+
for chromosome in chromosomes:
160+
total_distance = 0.0
161+
for i in range(len(chromosome) - 1): # Calculate total distance
162+
total_distance += distance(cities[chromosome[i]], cities[chromosome[i + 1]])
163+
fitness_matrix.append(1 / total_distance)
164+
if total_distance < new_best_distance:
165+
new_best_path = chromosome
166+
new_best_distance = total_distance
167+
168+
return fitness_matrix, new_best_path, new_best_distance
169+
170+
171+
def chose_ts(
172+
fitness_matrix: list[float], chromosomes: list[list[int]], population_size: int
173+
) -> list[list[int]]:
174+
"""
175+
A type of selection operator
176+
Tournament Selection
177+
>>> chose_ts(fitness_matrix=[1], chromosomes=[[0,1,0]], population_size=1)
178+
[[0, 1, 0]]
179+
>>> chose_ts(fitness_matrix=[1], chromosomes=[0,1,0], population_size=0)
180+
[]
181+
>>> chose_ts(fitness_matrix=[], chromosomes=[[0,1,0]], population_size=1)
182+
Traceback (most recent call last):
183+
...
184+
IndexError: list index out of range
185+
>>> chose_ts(fitness_matrix=[1], chromosomes=[], population_size=1)
186+
Traceback (most recent call last):
187+
...
188+
IndexError: list index out of range
189+
>>> import random
190+
>>> random.seed(0)
191+
>>> chose_ts(fitness_matrix=[1], chromosomes=[0, 1, 0], population_size=2)
192+
Traceback (most recent call last):
193+
...
194+
IndexError: list index out of range
195+
"""
196+
chromosomes_new = []
197+
for _ in range(population_size):
198+
x1 = random.randint(0, population_size - 1)
199+
x2 = random.randint(0, population_size - 1)
200+
if fitness_matrix[x1] >= fitness_matrix[x2]:
201+
chromosomes_new.append(chromosomes[x1])
202+
else:
203+
chromosomes_new.append(chromosomes[x2])
204+
return chromosomes_new
205+
206+
207+
def chose_rws(
208+
fitness_matrix: list[float], chromosomes: list[list[int]], population_size: int
209+
) -> list[list[int]]:
210+
"""
211+
A type of selection operator
212+
Roulette Wheel Selection
213+
>>> chose_rws(fitness_matrix=[1], chromosomes=[[0,1,0]], population_size=1)
214+
[[0, 1, 0]]
215+
>>> chose_rws(fitness_matrix=[1], chromosomes=[0,1,0], population_size=0)
216+
[0, 1, 0]
217+
>>> chose_rws(fitness_matrix=[], chromosomes=[[0,1,0]], population_size=1)
218+
Traceback (most recent call last):
219+
...
220+
IndexError: list index out of range
221+
>>> chose_rws(fitness_matrix=[1], chromosomes=[], population_size=1)
222+
Traceback (most recent call last):
223+
...
224+
IndexError: list index out of range
225+
>>> chose_rws(fitness_matrix=[1], chromosomes=[0,1,0], population_size=2)
226+
[0, 0, 0]
227+
"""
228+
probabilitys = [0.0] * len(fitness_matrix)
229+
total_probability = 0.0
230+
for i in fitness_matrix:
231+
total_probability += i
232+
for i in range(len(fitness_matrix)):
233+
probabilitys[i] = fitness_matrix[i] / total_probability
234+
235+
chromosomes_new = copy.deepcopy(chromosomes)
236+
for i in range(population_size):
237+
k = 0.0
238+
r = random.uniform(0, 1)
239+
for j in range(population_size):
240+
k = k + probabilitys[j]
241+
if r <= k:
242+
chromosomes_new[i] = chromosomes[j]
243+
break
244+
return chromosomes_new
245+
246+
247+
def crossing(
248+
chromosome_a: list[int],
249+
chromosome_b: list[int],
250+
crossover_probability: float,
251+
cities_list: list[int],
252+
) -> tuple[list[int], list[int]]:
253+
"""
254+
Population crossover
255+
>>> crossing(chromosome_a=[0,1,0], chromosome_b=[0,1,0],
256+
... crossover_probability=0,cities_list=[1])
257+
([0, 1, 0], [0, 1, 0])
258+
>>> crossing(chromosome_a=[0,1,0], chromosome_b=[0,1,0],
259+
... crossover_probability=1,cities_list=[1])
260+
([0, 1, 0], [0, 1, 0])
261+
>>> crossing(chromosome_a=[0,1,0], chromosome_b=[],
262+
... crossover_probability=1,cities_list=[1])
263+
Traceback (most recent call last):
264+
...
265+
IndexError: list index out of range
266+
>>> crossing(chromosome_a=[0,1,0], chromosome_b=[0,1,0],
267+
... crossover_probability=1,cities_list=[])
268+
([0, 1, 0], [0, 1, 0])
269+
"""
270+
new_chromosome_a = copy.deepcopy(chromosome_a)
271+
new_chromosome_b = copy.deepcopy(chromosome_b)
272+
273+
if random.random() <= crossover_probability:
274+
crossover_segment = sorted(
275+
[
276+
random.randint(0, len(new_chromosome_a) - 1),
277+
random.randint(0, len(new_chromosome_a) - 1),
278+
]
279+
)
280+
281+
for k in range((crossover_segment[1] - crossover_segment[0]) + 1):
282+
(
283+
new_chromosome_a[crossover_segment[0] + k],
284+
new_chromosome_b[crossover_segment[0] + k],
285+
) = (
286+
new_chromosome_b[crossover_segment[0] + k],
287+
new_chromosome_a[crossover_segment[0] + k],
288+
)
289+
290+
for chromosome in [new_chromosome_a, new_chromosome_b]:
291+
unique_elements_set = set(chromosome)
292+
if len(unique_elements_set) != len(
293+
chromosome
294+
): # Determine whether the chromosome segment has duplication
295+
for segment_index in range(
296+
(crossover_segment[1] - crossover_segment[0]) + 1
297+
):
298+
target_index = 0
299+
for chrom_index in range(
300+
1, len(chromosome) - 1
301+
): # Exclude start and end points 0 when searching
302+
if chrom_index == (crossover_segment[0] + segment_index):
303+
continue
304+
if (
305+
chromosome[chrom_index]
306+
== chromosome[crossover_segment[0] + segment_index]
307+
):
308+
target_index = chrom_index
309+
break
310+
if target_index != 0:
311+
cities_list_copy = copy.deepcopy(cities_list)
312+
for t in chromosome:
313+
try:
314+
cities_list_copy.remove(t)
315+
except ValueError:
316+
continue
317+
chromosome[target_index] = random.sample(cities_list_copy, 1)[0]
318+
319+
return new_chromosome_a, new_chromosome_b
320+
321+
322+
def mutate(chromosome: list[int], mutation_probability: float) -> list[int]:
323+
"""
324+
Population variation: swap two interior cities (endpoints stay at 0).
325+
326+
>>> mutate([0, 1, 0], mutation_probability=0) # no mutation -> unchanged
327+
[0, 1, 0]
328+
>>> import random
329+
>>> random.seed(1)
330+
>>> mutate([0, 1, 2, 3, 0], mutation_probability=1) # swaps two interior cities
331+
[0, 2, 1, 3, 0]
332+
333+
An empty chromosome has no interior cities to swap; match only the exception
334+
type since the exact stdlib message changes across Python versions.
335+
336+
>>> mutate([], mutation_probability=1) # doctest: +IGNORE_EXCEPTION_DETAIL
337+
Traceback (most recent call last):
338+
...
339+
ValueError
340+
"""
341+
new_chromosome = copy.deepcopy(chromosome)
342+
if random.random() <= mutation_probability:
343+
mutate_location = [
344+
random.randint(1, len(new_chromosome) - 2),
345+
random.randint(1, len(new_chromosome) - 2),
346+
] # Exclude start and end points 0
347+
new_chromosome[mutate_location[0]], new_chromosome[mutate_location[1]] = (
348+
new_chromosome[mutate_location[1]],
349+
new_chromosome[mutate_location[0]],
350+
)
351+
return new_chromosome
352+
353+
354+
if __name__ == "__main__":
355+
best_path, best_distance = main(
356+
cities=cities,
357+
population_size=100,
358+
iterations_num=100,
359+
crossover_probability=0.6,
360+
mutation_probability=0.2,
361+
)
362+
363+
print(f"{best_path = }")
364+
print(f"{best_distance = }")

0 commit comments

Comments
 (0)