diff --git a/src/EasyGA.py b/src/EasyGA.py index 8c8d17f..503ab11 100644 --- a/src/EasyGA.py +++ b/src/EasyGA.py @@ -1,3 +1,21 @@ +def function_info(decorator): + """Recovers the name and doc-string for decorators.""" + + def new_decorator(method): + + # Apply old decorator + new_method = decorator(method) + + # Recover name and doc-string + new_method.__name__ = method.__name__ + new_method.__doc__ = method.__doc__ + + # Return new method with proper name and doc-string + return new_method + + return new_decorator + + # Import math for square root (ga.dist()) and ceil (crossover methods) import math @@ -47,16 +65,16 @@ class GA(Attributes): """Evolves the ga the specified number of generations or until the ga is no longer active if consider_termination is True.""" + # Create the initial population if necessary. + if self.population is None: + self.initialize_population() + cond1 = lambda: number_of_generations > 0 # Evolve the specified number of generations. cond2 = lambda: not consider_termination # If consider_termination flag is set: cond3 = lambda: cond2() or self.active() # check termination conditions. while cond1() and cond3(): - # Create the initial population if necessary. - if self.population is None: - self.initialize_population() - # If its the first generation, setup the database. if self.current_generation == 0: @@ -70,15 +88,15 @@ class GA(Attributes): # Otherwise evolve the population. else: - self.parent_selection_impl(self) - self.crossover_population_impl(self) - self.survivor_selection_impl(self) + self.parent_selection_impl() + self.crossover_population_impl() + self.survivor_selection_impl() self.population.update() - self.mutation_population_impl(self) + self.mutation_population_impl() # Update and sort fitnesses self.set_all_fitness() - self.population.sort_by_best_fitness(self) + self.sort_by_best_fitness() # Save the population to the database self.save_population() @@ -96,7 +114,7 @@ class GA(Attributes): def active(self): """Returns if the ga should terminate based on the termination implimented.""" - return self.termination_impl(self) + return self.termination_impl() def adapt(self): @@ -107,7 +125,7 @@ class GA(Attributes): # Update and sort fitnesses self.set_all_fitness() - self.population.sort_by_best_fitness(self) + self.sort_by_best_fitness() def adapt_probabilities(self): @@ -204,7 +222,7 @@ class GA(Attributes): self, self.population[n], best_chromosome, - min(0.25, 2 * tol_j / (tol(n) - tol_j)) + weight = min(0.25, 2 * tol_j / (tol(n) - tol_j)) ) # If negative weights can't be used, @@ -214,7 +232,7 @@ class GA(Attributes): self, self.population[n], self.population[j], - 0.75 + weight = 0.75 ) # Update fitnesses @@ -275,7 +293,7 @@ class GA(Attributes): chromosome.fitness = self.fitness_function_impl(chromosome) - def sort_by_best_fitness(self, chromosome_list, in_place = False): + def sort_by_best_fitness(self, chromosome_list = None, in_place = False): """Sorts the chromosome list by fitness based on fitness type. 1st element has best fitness. 2nd element has second best fitness. @@ -285,6 +303,10 @@ class GA(Attributes): if self.target_fitness_type not in ('max', 'min'): raise ValueError("Unknown target fitness type") + # Sort the population if no chromosome list is given + if chromosome_list is None: + chromosome_list = self.population + if in_place: chromosome_list.sort( # list to be sorted key = lambda chromosome: chromosome.fitness, # by fitness diff --git a/src/crossover/crossover_methods.py b/src/crossover/crossover_methods.py index b7a799e..a066741 100644 --- a/src/crossover/crossover_methods.py +++ b/src/crossover/crossover_methods.py @@ -1,9 +1,12 @@ +from EasyGA import function_info import random # Round to an integer near x with higher probability # the closer it is to that integer. randround = lambda x: int(x + random.random()) + +@function_info def _append_to_next_population(population_method): """Appends the new chromosomes to the next population. Also modifies the input to include the mating pool. @@ -14,25 +17,25 @@ def _append_to_next_population(population_method): population_method(ga, ga.population.mating_pool) ) - new_method.__name__ = population_method.__name__ return new_method +@function_info def _check_weight(individual_method): """Checks if the weight is between 0 and 1 before running. Exception may occur when using ga.adapt, which will catch the error and try again with valid weight. """ - def new_method(ga, parent_1, parent_2, weight = 0.5): + def new_method(ga, parent_1, parent_2, *, weight = individual_method.__kwdefaults__.get('weight', None)): - if 0 < weight < 1: - return individual_method(ga, parent_1, parent_2, weight) + if weight is None: + return individual_method(ga, parent_1, parent_2) + elif 0 < weight < 1: + return individual_method(ga, parent_1, parent_2, weight = weight) else: - raise ValueError("""Weight must be between 0 and 1 when using - the given crossover method.""") + raise ValueError(f"Weight must be between 0 and 1 when using {individual_method.__name__}.") - new_method.__name__ = individual_method.__name__ return new_method @@ -53,10 +56,9 @@ class Crossover_Methods: Every parent is paired with the previous parent. The first parent is paired with the last parent. """ - + for index in range(len(mating_pool)): # for each parent in the mating pool yield ga.crossover_individual_impl( # apply crossover to - ga, # mating_pool[index], # the parent and mating_pool[index-1], # the previous parent ) @@ -70,7 +72,6 @@ class Crossover_Methods: for parent in mating_pool: # for each parent in the mating pool yield ga.crossover_individual_impl( # apply crossover to - ga, # parent, # the parent and random.choice(mating_pool), # a random parent ) @@ -81,7 +82,7 @@ class Crossover_Methods: @_check_weight - def single_point(ga, parent_1, parent_2, weight = 0.5): + def single_point(ga, parent_1, parent_2, *, weight = 0.5): """Cross two parents by swapping genes at one random point.""" minimum_parent_length = min(len(parent_1), len(parent_2)) @@ -107,13 +108,13 @@ class Crossover_Methods: @_check_weight - def multi_point(ga, parent_1, parent_2, weight = 0.5): + def multi_point(ga, parent_1, parent_2, *, weight = 0.5): """Cross two parents by swapping genes at multiple points.""" pass @_check_weight - def uniform(ga, parent_1, parent_2, weight = 0.5): + def uniform(ga, parent_1, parent_2, *, weight = 0.5): """Cross two parents by swapping all genes randomly.""" for gene_pair in zip(parent_1, parent_2): @@ -123,7 +124,7 @@ class Crossover_Methods: class Arithmetic: """Crossover methods for numerical genes.""" - def average(ga, parent_1, parent_2, weight = 0.5): + def average(ga, parent_1, parent_2, *, weight = 0.5): """Cross two parents by taking the average of the genes.""" values_1 = parent_1.gene_value_iter @@ -139,7 +140,7 @@ class Crossover_Methods: yield value - def extrapolate(ga, parent_1, parent_2, weight = 0.5): + def extrapolate(ga, parent_1, parent_2, *, weight = 0.5): """Cross two parents by extrapolating towards the first parent. May result in gene values outside the expected domain. @@ -159,7 +160,7 @@ class Crossover_Methods: @_check_weight - def random(ga, parent_1, parent_2, weight = 0.5): + def random(ga, parent_1, parent_2, *, weight = 0.5): """Cross two parents by taking a random integer or float value between each of the genes.""" values_1 = parent_1.gene_value_iter @@ -189,7 +190,7 @@ class Crossover_Methods: """Crossover methods for permutation based chromosomes.""" @_check_weight - def ox1(ga, parent_1, parent_2, weight = 0.5): + def ox1(ga, parent_1, parent_2, *, weight = 0.5): """Cross two parents by slicing out a random part of one parent and then filling in the rest of the genes from the second parent.""" @@ -234,7 +235,7 @@ class Crossover_Methods: @_check_weight - def partially_mapped(ga, parent_1, parent_2, weight = 0.5): + def partially_mapped(ga, parent_1, parent_2, *, weight = 0.5): """Cross two parents by slicing out a random part of one parent and then filling in the rest of the genes from the second parent, preserving the ordering of genes wherever possible. diff --git a/src/mutation/mutation_methods.py b/src/mutation/mutation_methods.py index 45906e7..b8ab89d 100644 --- a/src/mutation/mutation_methods.py +++ b/src/mutation/mutation_methods.py @@ -1,6 +1,9 @@ +from EasyGA import function_info import random from math import ceil + +@function_info def _check_chromosome_mutation_rate(population_method): """Checks if the chromosome mutation rate is a float between 0 and 1 before running.""" @@ -15,10 +18,10 @@ def _check_chromosome_mutation_rate(population_method): else: raise ValueError("Chromosome mutation rate must be between 0 and 1.") - new_method.__name__ = population_method.__name__ return new_method +@function_info def _check_gene_mutation_rate(individual_method): """Checks if the gene mutation rate is a float between 0 and 1 before running.""" @@ -33,10 +36,10 @@ def _check_gene_mutation_rate(individual_method): else: raise ValueError("Gene mutation rate must be between 0 and 1.") - new_method.__name__ = individual_method.__name__ return new_method +@function_info def _reset_fitness(individual_method): """Resets the fitness value of the chromosome.""" @@ -44,10 +47,10 @@ def _reset_fitness(individual_method): chromosome.fitness = None individual_method(ga, chromosome) - new_method.__name__ = individual_method.__name__ return new_method +@function_info def _loop_random_mutations(individual_method): """Runs the individual method until enough genes are mutated on the indexed chromosome. @@ -63,7 +66,6 @@ def _loop_random_mutations(individual_method): for index in random.sample(sample_space, sample_size): individual_method(ga, chromosome, index) - new_method.__name__ = individual_method.__name__ return new_method @@ -87,7 +89,7 @@ class Mutation_Methods: # Loop the individual method until enough genes are mutated. for index in random.sample(sample_space, sample_size): - ga.mutation_individual_impl(ga, ga.population[index]) + ga.mutation_individual_impl(ga.population[index]) @_check_chromosome_mutation_rate @@ -98,7 +100,7 @@ class Mutation_Methods: sample_size = ceil(ga.chromosome_mutation_rate*len(ga.population)) for index in random.sample(sample_space, sample_size): - ga.mutation_individual_impl(ga, ga.population[index]) + ga.mutation_individual_impl(ga.population[index]) @_check_chromosome_mutation_rate @@ -109,7 +111,7 @@ class Mutation_Methods: for i in range(mutation_amount): ga.population[-i-1] = ga.make_chromosome(ga.population[i]) - ga.mutation_individual_impl(ga, ga.population[-i-1]) + ga.mutation_individual_impl(ga.population[-i-1]) class Individual: diff --git a/src/parent_selection/parent_selection_methods.py b/src/parent_selection/parent_selection_methods.py index 287e057..0310ae6 100644 --- a/src/parent_selection/parent_selection_methods.py +++ b/src/parent_selection/parent_selection_methods.py @@ -1,5 +1,8 @@ +from EasyGA import function_info import random + +@function_info def _check_selection_probability(selection_method): """Raises an exception if the selection_probability is not between 0 and 1 inclusively. Otherwise runs @@ -16,6 +19,7 @@ def _check_selection_probability(selection_method): return new_method +@function_info def _check_positive_fitness(selection_method): """Raises an exception if the population contains a chromosome with negative fitness. Otherwise runs @@ -32,6 +36,7 @@ def _check_positive_fitness(selection_method): return new_method +@function_info def _ensure_sorted(selection_method): """Sorts the population by fitness and then runs the selection method. @@ -45,6 +50,7 @@ def _ensure_sorted(selection_method): return new_method +@function_info def _compute_parent_amount(selection_method): """Computes the amount of parents needed to be selected, and passes it diff --git a/src/survivor_selection/survivor_selection_methods.py b/src/survivor_selection/survivor_selection_methods.py index 72a3c19..995f435 100644 --- a/src/survivor_selection/survivor_selection_methods.py +++ b/src/survivor_selection/survivor_selection_methods.py @@ -1,5 +1,8 @@ +from EasyGA import function_info import random + +@function_info def _append_to_next_population(survivor_method): """Appends the selected chromosomes to the next population."""