diff --git a/genetic_algorithm.py b/genetic_algorithm.py new file mode 100644 index 0000000..35c2f46 --- /dev/null +++ b/genetic_algorithm.py @@ -0,0 +1,110 @@ +import random +from typing import Tuple + +import numpy as np +import numpy.typing as npt + +from common import plot_plan, read_data + + +class GeneticTSP: + + def __init__(self, population: int, crossover_prob: float, mutation_prob: float, data): + self.generation: int = 0 + self.population: int = population + self.data: npt.NDArray = data + self.genes: int = len(data) + self.crossover_prob: float = crossover_prob + self.mutation_prob: float = mutation_prob + self.generate_first_generation() + + def generate_first_generation(self): + self.candidates: npt.NDArray = np.array([np.random.permutation(np.arange(self.genes)) for _ in range(self.population)]) + + + def get_distance(self, candidate): + return sum([self.data[candidate[i - 1], candidate[i]] for i in range(self.genes)]) + + + def fitness(self): + distances = np.array([ self.get_distance(candidate) for candidate in self.candidates ]) + max_distance = max(distances) + fitness = max_distance - distances + fitness_sum = sum(fitness) + self.fitness_probs: npt.NDArray = fitness / fitness_sum + + + def crossover(self, parent1: npt.NDArray, parent2: npt.NDArray) -> Tuple[npt.NDArray, npt.NDArray]: + if self.crossover_prob < np.random.random(): + return (parent1, parent2) + + cut: int = np.random.randint(0, self.genes) + + offspring1 = parent1[:cut] + offspring2 = parent2[:cut] + + offspring1 = np.concatenate((offspring1, np.array([gene for gene in parent2 if gene not in offspring1]))) + offspring2 = np.concatenate((offspring2, np.array([gene for gene in parent1 if gene not in offspring2]))) + + return (offspring1, offspring2) + + + def mutate(self, offspring): + if self.mutation_prob < np.random.random(): + return + + pos1: int = np.random.randint(0, self.genes) + pos2: int = np.random.randint(0, self.genes) + + offspring[pos1], offspring[pos2] = offspring[pos2], offspring[pos1] + + def select_individual(self): + choice = np.random.choice(self.population, 1, p=self.fitness_probs)[0] + return self.candidates[choice] + + + def generate_next_generation(self): + new_generation = [] + self.fitness() + + for _ in range(0,self.population,2): + offspring1, offspring2 = self.crossover(self.select_individual(), self.select_individual()) + self.mutate(offspring1) + self.mutate(offspring2) + new_generation.append(offspring1) + new_generation.append(offspring2) + + self.candidates = np.array(new_generation) + + + def run(self, generations = 10): + for _ in range(generations): + self.generate_next_generation() + + def get_candidates(self): + return self.candidates + + +if __name__ == "__main__": + cities, data = read_data("./european_cities.csv") + + np.random.seed(1987) + gen = GeneticTSP(500, .8, .4, data[:10,:10]) + + original_cands = gen.get_candidates() + res = [ (gen.get_distance(cand), cand) for cand in original_cands ] + res.sort(key=lambda i: i[0]) + + print(f"Original population") + print(f"Distance: {res[0][0]}") + plot_plan([ cities[i] for i in res[-1][1] ]) + + gen.run(500) + + arr = gen.get_candidates() + res = [ (gen.get_distance(cand), cand) for cand in arr ] + res.sort(key=lambda i: i[0]) + + print(f"Improved population") + print(f"Distance: {res[0][0]}") + plot_plan([ cities[i] for i in res[0][1] ])