├── demos ├── gif1.gif ├── gif2.gif ├── video_demo.mp4 └── video_demo2.mp4 ├── config ├── config_parser.py └── config.ini ├── requirements.txt ├── LICENSE.md ├── utils ├── obstacle_generator.py ├── path_point_generator.py └── plotter.py ├── simulate.py ├── .gitignore ├── README.md └── genetic_algorithm.py /demos/gif1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rofe-dl/genetic-algorithm-shortest-path/HEAD/demos/gif1.gif -------------------------------------------------------------------------------- /demos/gif2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rofe-dl/genetic-algorithm-shortest-path/HEAD/demos/gif2.gif -------------------------------------------------------------------------------- /demos/video_demo.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rofe-dl/genetic-algorithm-shortest-path/HEAD/demos/video_demo.mp4 -------------------------------------------------------------------------------- /demos/video_demo2.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rofe-dl/genetic-algorithm-shortest-path/HEAD/demos/video_demo2.mp4 -------------------------------------------------------------------------------- /config/config_parser.py: -------------------------------------------------------------------------------- 1 | from configparser import ConfigParser 2 | 3 | parser = ConfigParser() 4 | parser.read('config/config.ini') -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cycler==0.11.0 2 | fonttools==4.28.5 3 | kiwisolver==1.3.2 4 | matplotlib==3.5.1 5 | numpy 6 | packaging==21.3 7 | Pillow==9.0.0 8 | pyparsing==3.0.6 9 | PyQt5==5.15.6 10 | PyQt5-Qt5==5.15.2 11 | PyQt5-sip==12.9.0 12 | python-dateutil==2.8.2 13 | Shapely==1.8.0 14 | six==1.16.0 15 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /config/config.ini: -------------------------------------------------------------------------------- 1 | [Obstacles] 2 | generate_randomly = true 3 | number_of_obstacles = 11 4 | max_height = 9 5 | min_height = 4 6 | max_width = 9 7 | min_width = 4 8 | 9 | [Path Points] 10 | generate_randomly = true 11 | number_of_path_points = 25 12 | 13 | [Hardcoded Obstacles] 14 | number_of_hardcoded_obstacles = 7 15 | obstacle_1 = [(2.5, 9), (3.5, 9), (3.5, 12), (2.5, 12)] 16 | obstacle_2 = [(3, 6.5), (4, 6.5), (4, 4), (3, 4)] 17 | obstacle_3 = [(7, 12), (9, 12), (9, 13), (7, 13)] 18 | obstacle_4 = [(6.5, 6), (8, 6), (8, 9.5), (6.5, 9.5)] 19 | obstacle_5 = [(5.7, 2), (8.7, 2), (8.7, 3), (5.7, 3)] 20 | obstacle_6 = [(11, 8.5), (12, 8.5), (12, 12), (11, 12)] 21 | obstacle_7 = [(10, 3.5), (11.5, 3.5), (11.5, 5.5), (10, 5.5)] 22 | 23 | [Hardcoded Path Points] 24 | path_points = [(1, 7), (1, 11), (3, 14), (3, 1), (5, 8), (6, 11), (6, 4), (8, 4), (10, 1), (10, 7), (10, 11), (11, 14), (13, 12), (12, 2), (14, 3), (14, 8)] 25 | 26 | [Genetic Algorithm] 27 | max_generations = 10 28 | population_size = 100 29 | top_percentage = 0.40 30 | mutation_probability = 0.3 31 | crossover_split_random = false 32 | crossover_split_size = 0.5 33 | 34 | [Plot Axes] 35 | ; |x_start - x_end| and |y_start - y_end| must be at least 15 for good results 36 | x_start = 0 37 | x_end = 100 38 | y_start = 0 39 | y_end = 100 -------------------------------------------------------------------------------- /utils/obstacle_generator.py: -------------------------------------------------------------------------------- 1 | from config.config_parser import parser 2 | from random import randint 3 | from shapely.geometry.polygon import Polygon 4 | 5 | def generate_obstacles(obstacles, number_of_obstacles): 6 | print('Generating obstacles ....') 7 | for i in range(number_of_obstacles): 8 | 9 | while True: 10 | obstacle = _generate_obstacle() 11 | 12 | if not _obstacle_near_obstacle(obstacle, obstacles): 13 | break 14 | 15 | obstacles.append(obstacle) 16 | 17 | def _generate_obstacle(): 18 | axes = parser['Plot Axes'] 19 | x_start = int(axes['x_start']) 20 | x_end = int(axes['x_end']) 21 | y_start = int(axes['y_start']) 22 | y_end = int(axes['y_end']) 23 | 24 | max_width = int(parser['Obstacles']['max_width']) 25 | max_height = int(parser['Obstacles']['max_height']) 26 | min_width = int(parser['Obstacles']['min_width']) 27 | min_height = int(parser['Obstacles']['min_height']) 28 | 29 | point_x_1 = randint(x_start + 3, x_end - 3) 30 | point_y_1 = randint(y_start + 3, y_end - 3) 31 | 32 | width = randint(min_width, max_width) 33 | height = randint(min_height, max_height) 34 | 35 | # to prevent obstacles from forming at the very edge of the axes 36 | point_x_2 = point_x_1 + width if point_x_1+width <= 0.8 * x_end else point_x_1 - width 37 | point_y_2 = point_y_1 + height if point_y_1+height <= 0.8 * y_end else point_y_1 - height 38 | 39 | # point_x_2 = point_x_1 + width if randint(1,10) <= 5 else point_x_1 - width 40 | # point_y_2 = point_y_1 + height if randint(1,10) <= 5 else point_y_1 - height 41 | 42 | return [(point_x_1, point_y_1), (point_x_2, point_y_1), (point_x_2, point_y_2), (point_x_1, point_y_2)] 43 | 44 | def _obstacle_near_obstacle(obstacle, obstacles): 45 | obstacle = Polygon(obstacle) 46 | 47 | for o in obstacles: 48 | o = Polygon(o) 49 | 50 | if o.intersects(obstacle): 51 | return True 52 | 53 | return False -------------------------------------------------------------------------------- /simulate.py: -------------------------------------------------------------------------------- 1 | from config.config_parser import parser 2 | from utils.obstacle_generator import generate_obstacles 3 | from utils.path_point_generator import generate_path_points 4 | from genetic_algorithm import start, path_overlaps_obstacle 5 | 6 | obstacles = [] 7 | path_points = [] 8 | path_validity = dict() 9 | 10 | def main(): 11 | _init_obstacles() 12 | _init_path_points() 13 | 14 | _init_path_validity() 15 | 16 | start(obstacles, path_points, path_validity) 17 | 18 | def _init_path_points(): 19 | 20 | if parser['Path Points'].getboolean('generate_randomly'): 21 | generate_path_points(path_points, obstacles) 22 | 23 | else: 24 | # eval will create the list from the string representation of list in config.ini 25 | for element in eval(parser['Hardcoded Path Points']['path_points']): 26 | path_points.append(element) 27 | 28 | def _init_obstacles(): 29 | 30 | if parser['Obstacles'].getboolean('generate_randomly'): 31 | number_of_obstacles = int(parser['Obstacles']['number_of_obstacles']) 32 | generate_obstacles(obstacles, number_of_obstacles) 33 | 34 | else: 35 | for i in range(int(parser['Hardcoded Obstacles']['number_of_hardcoded_obstacles'])): 36 | obstacle = eval(parser['Hardcoded Obstacles'][f"obstacle_{i+1}"]) 37 | obstacles.append(obstacle) 38 | 39 | def _init_path_validity(): 40 | 41 | for i, path_point_start in enumerate(path_points): 42 | 43 | if path_point_start not in path_validity: 44 | path_validity[path_point_start] = [True] * len(path_points) 45 | 46 | for j, path_point_end in enumerate(path_points): 47 | 48 | if path_point_end not in path_validity: 49 | path_validity[path_point_end] = [True] * len(path_points) 50 | 51 | if path_overlaps_obstacle(path_point_start, path_point_end, obstacles): 52 | path_validity[path_point_start][j] = False 53 | path_validity[path_point_end][i] = False 54 | 55 | if __name__ == '__main__': 56 | 57 | main() 58 | -------------------------------------------------------------------------------- /utils/path_point_generator.py: -------------------------------------------------------------------------------- 1 | from config.config_parser import parser 2 | from random import randint 3 | from shapely.geometry import Point 4 | from shapely.geometry.polygon import Polygon 5 | 6 | def generate_path_points(path_points, obstacles): 7 | print('Generating path points ....') 8 | 9 | axes = parser['Plot Axes'] 10 | 11 | source_x = int(axes['x_start']) + 1 12 | source_y = int(axes['y_start']) + 1 13 | # source_y = randint(int(axes['y_start']), int(axes['y_end'])) 14 | path_points.append((source_x, source_y)) 15 | 16 | goal_x = int(axes['x_end']) - 1 17 | goal_y = int(axes['y_end']) - 1 18 | # goal_y = randint(int(axes['y_start']), int(axes['y_end'])) 19 | 20 | number_of_path_points = int(parser['Path Points']['number_of_path_points']) 21 | for i in range(number_of_path_points): 22 | path_points.append(_generate_path_point(obstacles, path_points)) 23 | 24 | path_points.append((goal_x, goal_y)) 25 | 26 | def _generate_path_point(obstacles, path_points): 27 | axes = parser['Plot Axes'] 28 | 29 | while True: 30 | # avoid path points being formed on the edges of the axes 31 | x = randint( int(axes['x_start'])+2, int(axes['x_end'])-2 ) 32 | y = randint( int(axes['y_start'])+2, int(axes['y_end'])-2 ) 33 | 34 | if not _path_point_near_obstacle(x, y, obstacles) and not _path_point_near_another(x, y, path_points): 35 | return (x, y) 36 | 37 | def _path_point_near_obstacle(x, y, obstacles): 38 | point = Point(x, y) 39 | 40 | # forms a radius of 1 around the point to check if point is near any obstacle 41 | circle_perimeter = point.buffer(1).boundary 42 | 43 | for obstacle in obstacles: 44 | obstacle = Polygon(obstacle) 45 | 46 | if circle_perimeter.intersection(obstacle): 47 | return True 48 | 49 | return False 50 | 51 | def _path_point_near_another(x, y, path_points): 52 | point1 = Point(x, y) 53 | circle_perimeter_1 = point1.buffer(2).boundary 54 | 55 | for point in path_points: 56 | point2 = Point(point[0], point[1]) 57 | circle_perimeter_2 = point2.buffer(2).boundary 58 | 59 | if circle_perimeter_1.intersection(circle_perimeter_2): 60 | return True 61 | 62 | return False -------------------------------------------------------------------------------- /utils/plotter.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import numpy as np 3 | 4 | from config.config_parser import parser 5 | plt.ion() 6 | 7 | def plot(obstacles, path_points, population, path_lengths, gen, last_gen): 8 | 9 | for i, chromosome in enumerate(population): 10 | _reset_plot(obstacles, path_points) 11 | 12 | path_x = [path_points[j][0] for j, c in enumerate(chromosome) if c == '1'] 13 | path_y = [path_points[j][1] for j, c in enumerate(chromosome) if c == '1'] 14 | 15 | plt.plot(path_x, path_y, '-') 16 | plt.text(1, int(parser['Plot Axes']['y_end'])+1, f"Generation: {gen}, Chromosome No. {i+1}\nPath Length:{path_lengths[i]}") 17 | 18 | # plt.pause(0.025) this disrupts window focus, so use the below lines 19 | plt.gcf().canvas.draw_idle() 20 | plt.gcf().canvas.start_event_loop(0.025) 21 | 22 | if last_gen: 23 | _show_final_path(obstacles, path_points, population, path_lengths, gen) 24 | 25 | def _show_final_path(obstacles, path_points, population, path_lengths, gen): 26 | index_min = np.argmin(path_lengths) 27 | 28 | plt.ioff() 29 | _reset_plot(obstacles, path_points) 30 | 31 | chromosome = population[index_min] 32 | 33 | path_x = [path_points[j][0] for j, c in enumerate(chromosome) if c == '1'] 34 | path_y = [path_points[j][1] for j, c in enumerate(chromosome) if c == '1'] 35 | 36 | plt.plot(path_x, path_y, '-') 37 | plt.text(1, int(parser['Plot Axes']['y_end']) + 1, f"Finished at generation: {gen}\nShortest Path Found:{path_lengths[index_min]}") 38 | 39 | plt.pause(1) 40 | print('Done! You may now close the window.') 41 | plt.show() 42 | 43 | def _reset_plot(obstacles, path_points): 44 | 45 | plt.clf() 46 | 47 | axes = parser['Plot Axes'] 48 | plt.axis([int(axes['x_start']), 49 | int(axes['x_end']), 50 | int(axes['y_start']), 51 | int(axes['y_end'])]) 52 | 53 | _plot_obstacles(obstacles) 54 | _plot_path_points(path_points) 55 | 56 | def _plot_path_points(path_points): 57 | path_point_x = [path_point[0] for path_point in path_points] 58 | path_point_y = [path_point[1] for path_point in path_points] 59 | 60 | plt.plot(path_point_x[1:-1], path_point_y[1:-1], "k.") 61 | plt.plot(path_point_x[0], path_point_y[0], "bo", label='Source') 62 | plt.plot(path_point_x[-1], path_point_y[-1], "go", label='Goal') 63 | 64 | plt.legend(loc="upper left") 65 | 66 | def _plot_obstacles(obstacles): 67 | 68 | for obstacle in obstacles: 69 | x_values, y_values = [], [] 70 | 71 | for vertex in obstacle: 72 | x_values.append(vertex[0]) 73 | y_values.append(vertex[1]) 74 | 75 | plt.fill(x_values, y_values, 'r') -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 105 | __pypackages__/ 106 | 107 | # Celery stuff 108 | celerybeat-schedule 109 | celerybeat.pid 110 | 111 | # SageMath parsed files 112 | *.sage.py 113 | 114 | # Environments 115 | .env 116 | .venv 117 | env/ 118 | venv/ 119 | ENV/ 120 | env.bak/ 121 | venv.bak/ 122 | 123 | # Spyder project settings 124 | .spyderproject 125 | .spyproject 126 | 127 | # Rope project settings 128 | .ropeproject 129 | 130 | # mkdocs documentation 131 | /site 132 | 133 | # mypy 134 | .mypy_cache/ 135 | .dmypy.json 136 | dmypy.json 137 | 138 | # Pyre type checker 139 | .pyre/ 140 | 141 | # pytype static type analyzer 142 | .pytype/ 143 | 144 | # Cython debug symbols 145 | cython_debug/ 146 | 147 | # PyCharm 148 | # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can 149 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 150 | # and can be added to the global gitignore or merged into this file. For a more nuclear 151 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 152 | #.idea/ 153 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Genetic Algorithm for Path Planning 2 | 3 | An implementation of the genetic algorithm used in finding the shortest path from one point to another with some obstacles in between using the path points available throughout the space. I've used Matplotlib to show the simulation. Some help was taken from [Yaaximus' implementation here](https://github.com/Yaaximus/genetic-algorithm-path-planning), but the approach taken is different and I've coded ways to generate obstacles and path points randomly so the method is a lot more dynamic rather than focusing on static obstacles and paths. 4 | 5 | ## Setup Instructions 6 | 7 | Make sure you have Python 3.6 or above installed. Clone the repo, and do the following in the directory. 8 | 9 | **Linux** 10 | ```python 11 | python3 -m venv env 12 | source env/bin/activate 13 | pip install -r requirements.txt 14 | python3 simulate.py 15 | ``` 16 | 17 | **Windows** 18 | ```python 19 | python -m venv env 20 | env\Scripts\activate.bat 21 | pip install -r requirements.txt 22 | python simulate.py 23 | ``` 24 | 25 | You can change the setup variables in config/config.ini as you wish, although combinations of some values can produce impossible paths or take a very very long time e.g very large population size, huge number of obstacles for the axes, lots of path points, very small axis size etc. 26 | 27 | There are hardcoded obstacles and path points available too for comparison purposes from Yaaximus' implementation, which are used automatically if `generated_randomly` in the config.ini is set to `false`. 28 | 29 | ## Simulation 30 | 31 | ![demo](https://raw.githubusercontent.com/rofe-dl/genetic-algorithm-shortest-path/master/demos/gif1.gif) 32 | ![demo2](https://raw.githubusercontent.com/rofe-dl/genetic-algorithm-shortest-path/master/demos/gif2.gif) 33 | 34 | ## Details 35 | 36 | - Chromosomes are binary coded and generated randomly, each bit representing whether that path point is visited or not. First and last bit are always 1, because source and goal are always visited. 37 | - The source is always generated at the bottom left, and goal on the top right. 38 | - Path points are never generated very close to any obstacle, and obstacles also never overlap. 39 | - For the fitness function, I've used the simple reciprocal of the total distance covered by the path. 40 | - To mutate, I choose any random bit from the chromosome and flip it. Source and goal bits are ignored. 41 | - Crossover is done by splitting parent chromosomes in two and joining with each other. Split size can be configured by user or done randomly. 42 | - Unlike Yaaximus' method, no probability or links are made between close path points that could increase a path's fitness as a more dynamic approach is tried here. If we test using the hardcoded obstacles and path points, we can see the proposed solution finds a shorter path by skipping one path point. 43 | 44 | ## Improvements 45 | 46 | Some improvements can possibly be made to the simulation that could boost the 47 | performance. First, when connections between path points are being validated, we can 48 | just check against nearby obstacles instead of going over every obstacle on the map 49 | and avoid ones farther away. 50 | 51 | Secondly, despite some optimizations, there are still instances where a very difficult 52 | map is randomly generated. For example, an obstacle being generated right in front of 53 | the source, leaving no way out from the starting node. In such cases, solutions are 54 | impossible to find and the simulation gets stuck in a loop trying to come up with 55 | chromosomes to solve it. Edge cases like these can be handled to improve the 56 | program 57 | 58 | Instead of randomly generating obstacles all around, it could be better to create them equally distributed throughout the space still at random. It's seen that sometimes most obstacles lie leaning towards the edge or leaving gaps around the map, making path finding easy. So an equal distribution at the center and at the edges could yield better results. 59 | 60 | ## Conclusion 61 | 62 | While slow, it is a tradeoff made to make a more adaptable algorithm that can work in more dynamic natures without manual intervention or setups. In some cases, it can even come up with shorter paths. Speed can be improved by coming up with a better chromosome generation. 63 | 64 | -------------------------------------------------------------------------------- /genetic_algorithm.py: -------------------------------------------------------------------------------- 1 | import math 2 | from config.config_parser import parser 3 | from random import randint 4 | 5 | from shapely.geometry import Polygon, LineString 6 | 7 | from utils.plotter import plot 8 | 9 | def start(obstacles, path_points, path_validity): 10 | 11 | population = _generate_population(path_points, obstacles, path_validity) 12 | path_lengths = [] 13 | 14 | for chromosome in population: 15 | path_lengths.append(_calculate_path_length(chromosome, path_points)) 16 | 17 | plot(obstacles, path_points, population, path_lengths, 1, False) 18 | 19 | generations = int(parser['Genetic Algorithm']['max_generations']) 20 | 21 | for gen in range(generations - 1): 22 | new_population = [] 23 | path_lengths.clear() 24 | 25 | fitness_list = _sort_by_fitness(population, path_points) 26 | 27 | for chromosome in population: 28 | while True: 29 | parent1 = _choose_random_parent(fitness_list) 30 | parent2 = _choose_random_parent(fitness_list) 31 | 32 | child = _crossover(parent1, parent2) 33 | 34 | if randint(1, 10) <= 10 * float(parser['Genetic Algorithm']['mutation_probability']): 35 | child = _mutation(child) 36 | 37 | if _chromosome_valid(child, obstacles, path_points): 38 | break 39 | 40 | path_lengths.append(_calculate_path_length(child, path_points)) 41 | new_population.append(child) 42 | 43 | population = new_population 44 | plot(obstacles, path_points, new_population, path_lengths, (gen+2), last_gen=True if gen == generations-2 else False ) 45 | 46 | 47 | def _mutation(chromosome): 48 | index = randint(1, len(chromosome) - 2) # we won't mutate source and goal genes 49 | 50 | chromosome = list(chromosome) 51 | chromosome[index] = '1' if chromosome[index] == '0' else '0' 52 | 53 | return ''.join(chromosome) 54 | 55 | def _fitness(chromosome, path_points): 56 | length = _calculate_path_length(chromosome, path_points) 57 | fitness = 1 / length if length != 0 else 0 58 | 59 | return fitness 60 | 61 | def _sort_by_fitness(population, path_points): 62 | fitness_list = [] 63 | 64 | for chromosome in population: 65 | chromosome_to_fitness = (chromosome, _fitness(chromosome, path_points)) 66 | fitness_list.append(chromosome_to_fitness) 67 | 68 | fitness_list.sort(reverse=True, key=lambda tuple: tuple[1]) 69 | 70 | return fitness_list 71 | 72 | def _choose_random_parent(fitness_list): 73 | till_index = len(fitness_list) * float(parser['Genetic Algorithm']['top_percentage']) 74 | till_index = math.floor(till_index) 75 | 76 | parent_to_fitness = fitness_list[randint(0, till_index)] 77 | 78 | return parent_to_fitness[0] 79 | 80 | def _crossover(parent1, parent2): 81 | 82 | if parser['Genetic Algorithm'].getboolean('crossover_split_random'): 83 | split_size = randint(0, len(parent1)) 84 | 85 | else: 86 | fraction = float(parser['Genetic Algorithm']['crossover_split_size']) 87 | split_size = math.floor(fraction * len(parent1)) 88 | 89 | return ''.join([parent1[:split_size], parent2[split_size:]]) 90 | 91 | def _generate_population(path_points, obstacles, path_validity): 92 | 93 | population_size = int(parser['Genetic Algorithm']['population_size']) 94 | 95 | population = [] 96 | print('Generating initial population, please wait ....') 97 | for i in range(population_size): 98 | while True: 99 | chromosome = _generate_chromosome(path_points, path_validity) 100 | if chromosome: 101 | break 102 | 103 | population.append(chromosome) 104 | 105 | print('Successfully created initial population') 106 | print('Simulating genetic algorithm for path planning .... (Press Ctrl+C to stop)') 107 | return population 108 | 109 | def _generate_chromosome(path_points, path_validity): 110 | 111 | chromosome = '1' # source is always visited 112 | previous_path_point = path_points[0] # keep track of the previous path point that was 1 113 | 114 | for i in range(1, len(path_points)): 115 | path_point = path_points[i] 116 | 117 | if i == (len(path_points) - 1) and not path_validity[previous_path_point][i]: 118 | return False 119 | 120 | if path_validity[previous_path_point][i]: 121 | 122 | if i == (len(path_points) - 1): 123 | gene = '1' 124 | else: 125 | gene = '0' if randint(1, 10) > 5 else '1' 126 | 127 | if gene == '1': 128 | previous_path_point = path_point 129 | 130 | chromosome += gene 131 | 132 | else: 133 | chromosome += '0' 134 | 135 | return chromosome 136 | 137 | def _chromosome_valid(chromosome, obstacles, path_points): 138 | path_point_1, path_point_2 = (), () 139 | 140 | for i, gene in enumerate(chromosome): 141 | if gene == '1': 142 | 143 | if not path_point_1: 144 | path_point_1 = path_points[i] 145 | else: 146 | path_point_2 = path_points[i] 147 | 148 | if path_point_1 and path_point_2: 149 | 150 | if path_overlaps_obstacle(path_point_1, path_point_2, obstacles): 151 | return False 152 | 153 | path_point_1 = path_point_2 154 | path_point_2 = () 155 | 156 | return True 157 | 158 | def path_overlaps_obstacle(path_point_1, path_point_2, obstacles): 159 | path = LineString([path_point_1, path_point_2]) 160 | 161 | for obstacle in obstacles: 162 | 163 | obstacle = Polygon(obstacle) 164 | if path.intersects(obstacle): 165 | return True 166 | 167 | return False 168 | 169 | 170 | def _calculate_path_length(chromosome, path_points): 171 | path_point_1, path_point_2 = (), () 172 | length = 0 173 | 174 | for i, gene in enumerate(chromosome): 175 | if gene == '1': 176 | last_path_point = path_points[i] 177 | 178 | if not path_point_1: 179 | path_point_1 = path_points[i] 180 | else: 181 | path_point_2 = path_points[i] 182 | 183 | if path_point_1 and path_point_2: 184 | 185 | length += _distance(path_point_1, path_point_2) 186 | 187 | path_point_1 = path_point_2 188 | path_point_2 = () 189 | 190 | return length 191 | 192 | def _distance(path_point_1, path_point_2): 193 | return math.sqrt( (path_point_2[0] - path_point_1[0])**2 + (path_point_2[1] - path_point_1[1])**2 ) --------------------------------------------------------------------------------