-
-
Notifications
You must be signed in to change notification settings - Fork 51.1k
add genetic_algorithm/travelling_salesman_problem.py #11228
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
949e795
b7e916f
52662b1
64e0f0e
3440ea5
3f3b4e5
507c257
fa704d8
c893e0b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,364 @@ | ||
| """ | ||
| Use a genetic algorithm to solve the travelling salesman problem (TSP) | ||
| which asks the following question: | ||
| "Given a list of cities and the distances between each pair of cities, what is the | ||
| shortest possible route that visits each city exactly once and returns to the origin | ||
| city?" | ||
|
|
||
| https://en.wikipedia.org/wiki/Genetic_algorithm | ||
| https://en.wikipedia.org/wiki/Travelling_salesman_problem | ||
|
|
||
| Author: Clark | ||
| """ | ||
|
|
||
| import copy | ||
| import random | ||
|
|
||
| cities = { | ||
| 0: [0, 0], | ||
| 1: [0, 5], | ||
| 2: [3, 8], | ||
| 3: [8, 10], | ||
| 4: [12, 8], | ||
| 5: [12, 4], | ||
| 6: [8, 0], | ||
| 7: [6, 2], | ||
| } | ||
|
|
||
|
|
||
| def main( | ||
| cities: dict[int, list[int]], | ||
| population_size: int, | ||
| iterations_num: int, | ||
| crossover_probability: float, | ||
| mutation_probability: float, | ||
| ) -> tuple[list[int], float]: | ||
| """ | ||
| Genetic algorithm main function | ||
|
|
||
| The algorithm is stochastic, so seed ``random`` and assert invariants of the | ||
| returned tour rather than one exact ordering (not reproducible across | ||
| platforms / Python versions). | ||
|
|
||
| >>> import random | ||
| >>> random.seed(0) | ||
| >>> path, best = main(cities=cities, population_size=100, iterations_num=100, | ||
| ... crossover_probability=0.6, mutation_probability=0.2) | ||
| >>> path[0] == 0 and path[-1] == 0 # starts and ends at the origin city | ||
| True | ||
| >>> sorted(path[:-1]) == sorted(cities) # every city visited exactly once | ||
| True | ||
| >>> 37 <= best < 45 # converges close to the optimal round-trip (~37.9) | ||
| True | ||
| >>> main(cities={0: [0, 0], 1: [2, 2]}, population_size=10, iterations_num=10, | ||
| ... crossover_probability=0.6, mutation_probability=0.2) | ||
| ([0, 1, 0], 5.656854249492381) | ||
| >>> main(cities={},population_size=10,iterations_num=10, | ||
| ... crossover_probability=0.6,mutation_probability=0.2) | ||
| Traceback (most recent call last): | ||
| ... | ||
| IndexError: list assignment index out of range | ||
| """ | ||
| best_path: list[int] = [] | ||
| best_distance = float("inf") | ||
|
|
||
| chromosomes, cities_list = init(cities, population_size) | ||
| fitness_matrix, best_path, best_distance = fitness( | ||
| cities, chromosomes, best_path, best_distance | ||
| ) | ||
| for _ in range(iterations_num): | ||
| """ | ||
| Uncomment to choose another selection operator | ||
| Only one of the two selection operators can be uncommented at the same time. | ||
| """ | ||
| # chromosomes = chose_ts(fitness_matrix, chromosomes, population_size) | ||
| chromosomes = chose_rws(fitness_matrix, chromosomes, population_size) | ||
| for x in range(int(population_size / 2)): # Population crossover | ||
| chromosomes[x], chromosomes[x + int(population_size / 2)] = crossing( | ||
| chromosomes[x], | ||
| chromosomes[x + int(population_size / 2)], | ||
| crossover_probability, | ||
| cities_list, | ||
| ) | ||
| for x in range(population_size): # Population variation | ||
| chromosomes[x] = mutate(chromosomes[x], mutation_probability) | ||
|
|
||
| fitness_matrix, best_path, best_distance = fitness( | ||
| cities, chromosomes, best_path, best_distance | ||
| ) | ||
|
|
||
| return best_path, best_distance | ||
|
|
||
|
|
||
| def distance(city1: list[int], city2: list[int]) -> float: | ||
| """ | ||
| Calculate the distance between two coordinate points | ||
| >>> distance([0, 0], [3, 4] ) | ||
| 5.0 | ||
| >>> distance([0, 0], [-3, 4] ) | ||
| 5.0 | ||
| >>> distance([0, 0], [-3, -4] ) | ||
| 5.0 | ||
| """ | ||
| return (((city1[0] - city2[0]) ** 2) + ((city1[1] - city2[1]) ** 2)) ** 0.5 | ||
|
|
||
|
|
||
| def init( | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, I don't understand why it needs to be modified like this
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Check your BUILD is failing. |
||
| cities: dict[int, list[int]], population_size: int | ||
| ) -> tuple[list[list[int]], list[int]]: | ||
| """ | ||
| Initialization generates initial population | ||
| >>> init(cities={0: [0, 0], 1: [2, 2]}, population_size=2) | ||
| ([[0, 1, 0], [0, 1, 0]], [1]) | ||
| >>> init(cities={0: [0, 0], 1: [2, 2]}, population_size=0) | ||
| ([], [1]) | ||
| >>> init(cities={},population_size=2) | ||
| Traceback (most recent call last): | ||
| ... | ||
| IndexError: list assignment index out of range | ||
| """ | ||
| chromosomes = [] | ||
| cities_list = list(cities.keys()) | ||
| del cities_list[0] | ||
| for _ in range(population_size): | ||
| chromosome = [] | ||
| chromosome.append(0) # Add starting point | ||
| chromosome.extend(random.sample(cities_list, len(cities_list))) | ||
| chromosome.append(0) # Add end point | ||
| chromosomes.append(chromosome) | ||
| return chromosomes, cities_list | ||
|
|
||
|
|
||
| def fitness( | ||
| cities: dict[int, list[int]], | ||
| chromosomes: list[list[int]], | ||
| best_path: list[int], | ||
| best_distance: float, | ||
| ) -> tuple[list[float], list[int], float]: | ||
| """ | ||
| Calculate population fitness | ||
| Generate a fitness matrix and obtain the optimal value in the current population | ||
| >>> fitness(cities={0: [0, 0], 1: [2, 2]},chromosomes=[[0,1,0]], | ||
| ... best_path=[], best_distance=float("inf")) | ||
| ([0.17677669529663687], [0, 1, 0], 5.656854249492381) | ||
| >>> fitness(cities={0: [0, 0], 1: [2, 2]},chromosomes=[[0,1,0],[0,1,0]], | ||
| ... best_path=[], best_distance=float("inf")) | ||
| ([0.17677669529663687, 0.17677669529663687], [0, 1, 0], 5.656854249492381) | ||
| >>> fitness(cities={}, chromosomes=[[0,1,0]], | ||
| ... best_path=[], best_distance=float("inf")) | ||
| Traceback (most recent call last): | ||
| ... | ||
| KeyError: 0 | ||
| >>> fitness(cities={0: [0, 0], 1: [2, 2]},chromosomes=[], | ||
| ... best_path=[], best_distance=float("inf")) | ||
| ([], [], inf) | ||
| """ | ||
| fitness_matrix = [] | ||
| new_best_path = best_path | ||
| new_best_distance = best_distance | ||
| for chromosome in chromosomes: | ||
| total_distance = 0.0 | ||
| for i in range(len(chromosome) - 1): # Calculate total distance | ||
| total_distance += distance(cities[chromosome[i]], cities[chromosome[i + 1]]) | ||
| fitness_matrix.append(1 / total_distance) | ||
| if total_distance < new_best_distance: | ||
| new_best_path = chromosome | ||
| new_best_distance = total_distance | ||
|
|
||
| return fitness_matrix, new_best_path, new_best_distance | ||
|
|
||
|
|
||
| def chose_ts( | ||
| fitness_matrix: list[float], chromosomes: list[list[int]], population_size: int | ||
| ) -> list[list[int]]: | ||
| """ | ||
| A type of selection operator | ||
| Tournament Selection | ||
| >>> chose_ts(fitness_matrix=[1], chromosomes=[[0,1,0]], population_size=1) | ||
| [[0, 1, 0]] | ||
| >>> chose_ts(fitness_matrix=[1], chromosomes=[0,1,0], population_size=0) | ||
| [] | ||
| >>> chose_ts(fitness_matrix=[], chromosomes=[[0,1,0]], population_size=1) | ||
| Traceback (most recent call last): | ||
| ... | ||
| IndexError: list index out of range | ||
| >>> chose_ts(fitness_matrix=[1], chromosomes=[], population_size=1) | ||
| Traceback (most recent call last): | ||
| ... | ||
| IndexError: list index out of range | ||
| >>> import random | ||
| >>> random.seed(0) | ||
| >>> chose_ts(fitness_matrix=[1], chromosomes=[0, 1, 0], population_size=2) | ||
| Traceback (most recent call last): | ||
| ... | ||
| IndexError: list index out of range | ||
| """ | ||
| chromosomes_new = [] | ||
| for _ in range(population_size): | ||
| x1 = random.randint(0, population_size - 1) | ||
| x2 = random.randint(0, population_size - 1) | ||
| if fitness_matrix[x1] >= fitness_matrix[x2]: | ||
| chromosomes_new.append(chromosomes[x1]) | ||
| else: | ||
| chromosomes_new.append(chromosomes[x2]) | ||
| return chromosomes_new | ||
|
|
||
|
|
||
| def chose_rws( | ||
| fitness_matrix: list[float], chromosomes: list[list[int]], population_size: int | ||
| ) -> list[list[int]]: | ||
| """ | ||
| A type of selection operator | ||
| Roulette Wheel Selection | ||
| >>> chose_rws(fitness_matrix=[1], chromosomes=[[0,1,0]], population_size=1) | ||
| [[0, 1, 0]] | ||
| >>> chose_rws(fitness_matrix=[1], chromosomes=[0,1,0], population_size=0) | ||
| [0, 1, 0] | ||
| >>> chose_rws(fitness_matrix=[], chromosomes=[[0,1,0]], population_size=1) | ||
| Traceback (most recent call last): | ||
| ... | ||
| IndexError: list index out of range | ||
| >>> chose_rws(fitness_matrix=[1], chromosomes=[], population_size=1) | ||
| Traceback (most recent call last): | ||
| ... | ||
| IndexError: list index out of range | ||
| >>> chose_rws(fitness_matrix=[1], chromosomes=[0,1,0], population_size=2) | ||
| [0, 0, 0] | ||
| """ | ||
| probabilitys = [0.0] * len(fitness_matrix) | ||
| total_probability = 0.0 | ||
| for i in fitness_matrix: | ||
| total_probability += i | ||
| for i in range(len(fitness_matrix)): | ||
| probabilitys[i] = fitness_matrix[i] / total_probability | ||
|
|
||
| chromosomes_new = copy.deepcopy(chromosomes) | ||
| for i in range(population_size): | ||
| k = 0.0 | ||
| r = random.uniform(0, 1) | ||
| for j in range(population_size): | ||
| k = k + probabilitys[j] | ||
| if r <= k: | ||
| chromosomes_new[i] = chromosomes[j] | ||
| break | ||
| return chromosomes_new | ||
|
|
||
|
|
||
| def crossing( | ||
| chromosome_a: list[int], | ||
| chromosome_b: list[int], | ||
| crossover_probability: float, | ||
| cities_list: list[int], | ||
| ) -> tuple[list[int], list[int]]: | ||
| """ | ||
| Population crossover | ||
| >>> crossing(chromosome_a=[0,1,0], chromosome_b=[0,1,0], | ||
| ... crossover_probability=0,cities_list=[1]) | ||
| ([0, 1, 0], [0, 1, 0]) | ||
| >>> crossing(chromosome_a=[0,1,0], chromosome_b=[0,1,0], | ||
| ... crossover_probability=1,cities_list=[1]) | ||
| ([0, 1, 0], [0, 1, 0]) | ||
| >>> crossing(chromosome_a=[0,1,0], chromosome_b=[], | ||
| ... crossover_probability=1,cities_list=[1]) | ||
| Traceback (most recent call last): | ||
| ... | ||
| IndexError: list index out of range | ||
| >>> crossing(chromosome_a=[0,1,0], chromosome_b=[0,1,0], | ||
| ... crossover_probability=1,cities_list=[]) | ||
| ([0, 1, 0], [0, 1, 0]) | ||
| """ | ||
| new_chromosome_a = copy.deepcopy(chromosome_a) | ||
| new_chromosome_b = copy.deepcopy(chromosome_b) | ||
|
|
||
| if random.random() <= crossover_probability: | ||
| crossover_segment = sorted( | ||
| [ | ||
| random.randint(0, len(new_chromosome_a) - 1), | ||
| random.randint(0, len(new_chromosome_a) - 1), | ||
| ] | ||
| ) | ||
|
|
||
| for k in range((crossover_segment[1] - crossover_segment[0]) + 1): | ||
| ( | ||
| new_chromosome_a[crossover_segment[0] + k], | ||
| new_chromosome_b[crossover_segment[0] + k], | ||
| ) = ( | ||
| new_chromosome_b[crossover_segment[0] + k], | ||
| new_chromosome_a[crossover_segment[0] + k], | ||
| ) | ||
|
|
||
| for chromosome in [new_chromosome_a, new_chromosome_b]: | ||
| unique_elements_set = set(chromosome) | ||
| if len(unique_elements_set) != len( | ||
| chromosome | ||
| ): # Determine whether the chromosome segment has duplication | ||
| for segment_index in range( | ||
| (crossover_segment[1] - crossover_segment[0]) + 1 | ||
| ): | ||
| target_index = 0 | ||
| for chrom_index in range( | ||
| 1, len(chromosome) - 1 | ||
| ): # Exclude start and end points 0 when searching | ||
| if chrom_index == (crossover_segment[0] + segment_index): | ||
| continue | ||
| if ( | ||
| chromosome[chrom_index] | ||
| == chromosome[crossover_segment[0] + segment_index] | ||
| ): | ||
| target_index = chrom_index | ||
| break | ||
| if target_index != 0: | ||
| cities_list_copy = copy.deepcopy(cities_list) | ||
| for t in chromosome: | ||
| try: | ||
| cities_list_copy.remove(t) | ||
| except ValueError: | ||
| continue | ||
| chromosome[target_index] = random.sample(cities_list_copy, 1)[0] | ||
|
|
||
| return new_chromosome_a, new_chromosome_b | ||
|
|
||
|
|
||
| def mutate(chromosome: list[int], mutation_probability: float) -> list[int]: | ||
| """ | ||
| Population variation: swap two interior cities (endpoints stay at 0). | ||
|
|
||
| >>> mutate([0, 1, 0], mutation_probability=0) # no mutation -> unchanged | ||
| [0, 1, 0] | ||
| >>> import random | ||
| >>> random.seed(1) | ||
| >>> mutate([0, 1, 2, 3, 0], mutation_probability=1) # swaps two interior cities | ||
| [0, 2, 1, 3, 0] | ||
|
|
||
| An empty chromosome has no interior cities to swap; match only the exception | ||
| type since the exact stdlib message changes across Python versions. | ||
|
|
||
| >>> mutate([], mutation_probability=1) # doctest: +IGNORE_EXCEPTION_DETAIL | ||
| Traceback (most recent call last): | ||
| ... | ||
| ValueError | ||
| """ | ||
| new_chromosome = copy.deepcopy(chromosome) | ||
| if random.random() <= mutation_probability: | ||
| mutate_location = [ | ||
| random.randint(1, len(new_chromosome) - 2), | ||
| random.randint(1, len(new_chromosome) - 2), | ||
| ] # Exclude start and end points 0 | ||
| new_chromosome[mutate_location[0]], new_chromosome[mutate_location[1]] = ( | ||
| new_chromosome[mutate_location[1]], | ||
| new_chromosome[mutate_location[0]], | ||
| ) | ||
| return new_chromosome | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| best_path, best_distance = main( | ||
| cities=cities, | ||
| population_size=100, | ||
| iterations_num=100, | ||
| crossover_probability=0.6, | ||
| mutation_probability=0.2, | ||
| ) | ||
|
|
||
| print(f"{best_path = }") | ||
| print(f"{best_distance = }") | ||
Uh oh!
There was an error while loading. Please reload this page.