From b2b1775e16b5bbb3f8d4e873a2d3a8a2e7cb3d5c Mon Sep 17 00:00:00 2001 From: SimpleArt <71458112+SimpleArt@users.noreply.github.com> Date: Fri, 1 Jan 2021 16:09:27 -0500 Subject: [PATCH] Added decorators file --- src/crossover/Crossover.py | 50 +------ src/decorators.py | 261 +++++++++++++++++++++++++++++++++ src/mutation/Mutation.py | 68 +-------- src/parent/Parent.py | 54 +------ src/survivor/Survivor.py | 37 +++-- src/termination/Termination.py | 80 +--------- 6 files changed, 292 insertions(+), 258 deletions(-) create mode 100644 src/decorators.py diff --git a/src/crossover/Crossover.py b/src/crossover/Crossover.py index a3e16e3..298e5cb 100644 --- a/src/crossover/Crossover.py +++ b/src/crossover/Crossover.py @@ -1,47 +1,13 @@ -from EasyGA import function_info import random +# Import all crossover decorators +from decorators import _check_weight, _gene_by_gene + # 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 _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 = individual_method.__kwdefaults__.get('weight', None)): - - if weight is None: - individual_method(ga, parent_1, parent_2) - elif 0 < weight < 1: - individual_method(ga, parent_1, parent_2, weight = weight) - else: - raise ValueError(f"Weight must be between 0 and 1 when using {individual_method.__name__}.") - - return new_method - - -@function_info -def _gene_by_gene(individual_method): - """Perform crossover by making a single new chromosome by combining each gene by gene.""" - - def new_method(ga, parent_1, parent_2, *, weight = individual_method.__kwdefaults__.get('weight', 'None')): - - ga.population.add_child( - individual_method(ga, value_1, value_2) - if weight == 'None' else - individual_method(ga, value_1, value_2, weight = weight) - for value_1, value_2 - in zip(parent_1.gene_value_iter, parent_2.gene_value_iter) - ) - - return new_method - - class Population: """Methods for selecting chromosomes to crossover.""" @@ -61,7 +27,7 @@ class Population: ) - def random_mate(ga): + def random(ga): """Select random pairs from the mating pool. Every parent is paired with a random parent. """ @@ -100,9 +66,9 @@ class Individual: @_check_weight @_gene_by_gene - def uniform(ga, *gene_values, *, weight = 0.5): + def uniform(ga, value_1, value_2, *, weight = 0.5): """Cross two parents by swapping all genes randomly.""" - return random.choices(gene_values, cum_weights = [weight, 1])[0] + return random.choices(gene_pair, cum_weights = [weight, 1])[0] class Arithmetic: @@ -136,7 +102,7 @@ class Individual: @_check_weight @_gene_by_gene - def random_value(ga, value_1, value_2, *, weight = 0.5): + def random(ga, value_1, value_2, *, weight = 0.5): """Cross two parents by taking a random integer or float value between each of the genes.""" value = value_1 + ga.weighted_random(weight) * (value_2-value_1) @@ -158,7 +124,7 @@ class Individual: # Too small to cross if len(parent_1) < 2: - raise ValueError("Parent lengths must be at least 2.") + return parent_1.gene_list # Unequal parent lengths if len(parent_1) != len(parent_2): diff --git a/src/decorators.py b/src/decorators.py new file mode 100644 index 0000000..b0e119f --- /dev/null +++ b/src/decorators.py @@ -0,0 +1,261 @@ +import random +from math import ceil + +def function_info(decorator): + """Recovers the name and doc-string for decorators throughout EasyGA for documentation purposes.""" + + 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 + + +#=======================# +# Crossover decorators: # +#=======================# + +@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 = individual_method.__kwdefaults__.get('weight', None)): + + if weight is None: + individual_method(ga, parent_1, parent_2) + elif 0 < weight < 1: + individual_method(ga, parent_1, parent_2, weight = weight) + else: + raise ValueError(f"Weight must be between 0 and 1 when using {individual_method.__name__}.") + + return new_method + +@function_info +def _gene_by_gene(individual_method): + """Perform crossover by making a single new chromosome by combining each gene by gene.""" + + def new_method(ga, parent_1, parent_2, *, weight = individual_method.__kwdefaults__.get('weight', 'None')): + + ga.population.add_child( + individual_method(ga, value_1, value_2) + if weight == 'None' else + individual_method(ga, value_1, value_2, weight = weight) + for value_1, value_2 + in zip(parent_1.gene_value_iter, parent_2.gene_value_iter) + ) + + return new_method + + +#====================# +# Parent decorators: # +#====================# + +@function_info +def _check_selection_probability(selection_method): + """Raises a ValueError if the selection_probability is not between 0 and 1 inclusively. + Otherwise runs the selection method.""" + + def new_method(ga): + if 0 <= ga.selection_probability <= 1: + selection_method(ga) + else: + raise ValueError("Selection probability must be between 0 and 1 to select parents.") + + return new_method + + +@function_info +def _check_positive_fitness(selection_method): + """Raises a ValueError if the population contains a chromosome with negative fitness. + Otherwise runs the selection method.""" + + def new_method(ga): + if ga.get_chromosome_fitness(0) > 0 and ga.get_chromosome_fitness(-1) >= 0: + selection_method(ga) + else: + raise ValueError("Converted fitness values can't have negative values or be all 0." + + " Consider using rank selection or stochastic selection instead.") + + return new_method + +@function_info +def _ensure_sorted(selection_method): + """Sorts the population by fitness and then runs the selection method.""" + + def new_method(ga): + ga.sort_by_best_fitness() + selection_method(ga) + + return new_method + +@function_info +def _compute_parent_amount(selection_method): + """Computes the amount of parents needed to be selected, + and passes it as another argument for the method.""" + + def new_method(ga): + parent_amount = max(2, round(len(ga.population)*ga.parent_ratio)) + selection_method(ga, parent_amount) + + return new_method + + +#======================# +# Mutation decorators: # +#======================# + + +@function_info +def _check_chromosome_mutation_rate(population_method): + """Checks if the chromosome mutation rate is a float between 0 and 1 before running.""" + + def new_method(ga): + + if not isinstance(ga.chromosome_mutation_rate, float): + raise TypeError("Chromosome mutation rate must be a float.") + + elif 0 < ga.chromosome_mutation_rate < 1: + population_method(ga) + + else: + raise ValueError("Chromosome mutation rate must be between 0 and 1.") + + 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.""" + + def new_method(ga, index): + + if not isinstance(ga.gene_mutation_rate, float): + raise TypeError("Gene mutation rate must be a float.") + + elif 0 < ga.gene_mutation_rate <= 1: + individual_method(ga, index) + + else: + raise ValueError("Gene mutation rate must be between 0 and 1.") + + return new_method + + +@function_info +def _reset_fitness(individual_method): + """Resets the fitness value of the chromosome.""" + + def new_method(ga, chromosome): + chromosome.fitness = None + individual_method(ga, chromosome) + + return new_method + + +@function_info +def _loop_random_mutations(individual_method): + """Runs the individual method until enough + genes are mutated on the indexed chromosome.""" + + # Change input to include the gene index being mutated. + def new_method(ga, chromosome): + + sample_space = range(len(chromosome)) + sample_size = ceil(len(chromosome)*ga.gene_mutation_rate) + + # Loop the individual method until enough genes are mutated. + for index in random.sample(sample_space, sample_size): + individual_method(ga, chromosome, index) + + return new_method + + +#======================# +# Survivor decorators: # +#======================# + + +#=========================# +# Termination decorators: # +#=========================# + +@function_info +def _add_by_fitness_goal(termination_impl): + """Adds termination by fitness goal to the method.""" + + def new_method(ga): + + # Try to check the fitness goal + try: + + # If minimum fitness goal reached, stop ga. + if ga.target_fitness_type == 'min' and ga.population[0].fitness <= ga.fitness_goal: + return False + + # If maximum fitness goal reached, stop ga. + elif ga.target_fitness_type == 'max' and ga.population[0].fitness >= ga.fitness_goal: + return False + + # Fitness or fitness goals are None, or Population not initialized + except (TypeError, AttributeError): + pass + + # Check other termination methods + return termination_impl(ga) + + return new_method + + +@function_info +def _add_by_generation_goal(termination_impl): + """Adds termination by generation goal to the method.""" + + def new_method(ga): + + # If generation goal is set, check it. + if ga.generation_goal is not None and ga.current_generation >= ga.generation_goal: + return False + + # Check other termination methods + return termination_impl(ga) + + return new_method + + +@function_info +def _add_by_tolerance_goal(termination_impl): + """Adds termination by tolerance goal to the method.""" + + def new_method(ga): + + # If tolerance is set, check it, if possible. + try: + best_fitness = ga.population[0].fitness + threshhold_fitness = ga.population[round(ga.percent_converged*len(ga.population))].fitness + tol = ga.tolerance_goal * (1 + abs(best_fitness)) + + # Terminate if the specified amount of the population has converged to the specified tolerance + if abs(best_fitness - threshhold_fitness) < tol: + return False + + # Fitness or tolerance goals are None, or population is not initialized + except (TypeError, AttributeError): + pass + + # Check other termination methods + return termination_impl(ga) + + return new_method + diff --git a/src/mutation/Mutation.py b/src/mutation/Mutation.py index fcb8ef8..33f18dc 100644 --- a/src/mutation/Mutation.py +++ b/src/mutation/Mutation.py @@ -1,72 +1,8 @@ -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.""" - - def new_method(ga): - - if not isinstance(ga.chromosome_mutation_rate, float): - raise TypeError("Chromosome mutation rate must be a float.") - - elif 0 < ga.chromosome_mutation_rate < 1: - population_method(ga) - - else: - raise ValueError("Chromosome mutation rate must be between 0 and 1.") - - 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.""" - - def new_method(ga, index): - - if not isinstance(ga.gene_mutation_rate, float): - raise TypeError("Gene mutation rate must be a float.") - - elif 0 < ga.gene_mutation_rate <= 1: - individual_method(ga, index) - - else: - raise ValueError("Gene mutation rate must be between 0 and 1.") - - return new_method - - -@function_info -def _reset_fitness(individual_method): - """Resets the fitness value of the chromosome.""" - - def new_method(ga, chromosome): - chromosome.fitness = None - individual_method(ga, chromosome) - - return new_method - - -@function_info -def _loop_random_mutations(individual_method): - """Runs the individual method until enough - genes are mutated on the indexed chromosome. - """ - - # Change input to include the gene index being mutated. - def new_method(ga, chromosome): - - sample_space = range(len(chromosome)) - sample_size = ceil(len(chromosome)*ga.gene_mutation_rate) - - # Loop the individual method until enough genes are mutated. - for index in random.sample(sample_space, sample_size): - individual_method(ga, chromosome, index) - - return new_method +# Import all mutation decorators +from decorators import _check_chromosome_mutation_rate, _check_gene_mutation_rate, _reset_fitness, _loop_random_mutations class Population: diff --git a/src/parent/Parent.py b/src/parent/Parent.py index 8b2e95a..0c01335 100644 --- a/src/parent/Parent.py +++ b/src/parent/Parent.py @@ -1,57 +1,7 @@ -from EasyGA import function_info import random - -@function_info -def _check_selection_probability(selection_method): - """Raises a ValueError if the selection_probability is not between 0 and 1 inclusively. - Otherwise runs the selection method.""" - - def new_method(ga): - if 0 <= ga.selection_probability <= 1: - selection_method(ga) - else: - raise ValueError("Selection probability must be between 0 and 1 to select parents.") - - return new_method - - -@function_info -def _check_positive_fitness(selection_method): - """Raises a ValueError if the population contains a chromosome with negative fitness. - Otherwise runs the selection method.""" - - def new_method(ga): - if ga.get_chromosome_fitness(0) > 0 and ga.get_chromosome_fitness(-1) >= 0: - selection_method(ga) - else: - raise ValueError("Converted fitness values can't have negative values or be all 0." - + " Consider using rank selection or stochastic selection instead.") - - return new_method - - -@function_info -def _ensure_sorted(selection_method): - """Sorts the population by fitness and then runs the selection method.""" - - def new_method(ga): - ga.sort_by_best_fitness() - selection_method(ga) - - return new_method - - -@function_info -def _compute_parent_amount(selection_method): - """Computes the amount of parents needed to be selected, - and passes it as another argument for the method.""" - - def new_method(ga): - parent_amount = max(2, round(len(ga.population)*ga.parent_ratio)) - selection_method(ga, parent_amount) - - return new_method +# Import all parent decorators +from decorators import _check_selection_probability, _check_positive_fitness, _ensure_sorted, _compute_parent_amount class Rank: diff --git a/src/survivor/Survivor.py b/src/survivor/Survivor.py index ce41653..ed6307c 100644 --- a/src/survivor/Survivor.py +++ b/src/survivor/Survivor.py @@ -1,34 +1,23 @@ -from EasyGA import function_info import random - -@function_info -def _append_to_next_population(survivor_method): - """Appends the selected chromosomes to the next population.""" - - def new_method(ga): - ga.population.append_children(survivor_method(ga)) - - return new_method +# Import all survivor decorators +from decorators import * -@_append_to_next_population def fill_in_best(ga): """Fills in the next population with the best chromosomes from the last population""" needed_amount = len(ga.population) - len(ga.population.next_population) - return ga.population[:needed_amount] + ga.population.append_children(ga.population[:needed_amount]) -@_append_to_next_population def fill_in_random(ga): """Fills in the next population with random chromosomes from the last population""" needed_amount = len(ga.population) - len(ga.population.next_population) - return random.sample(ga.population, needed_amount) + ga.population.append_children(random.sample(ga.population, needed_amount)) -@_append_to_next_population def fill_in_parents_then_random(ga): """Fills in the next population with all parents followed by random chromosomes from the last population""" @@ -41,11 +30,19 @@ def fill_in_parents_then_random(ga): # Only parents are used. if random_amount == 0: - return ga.population.mating_pool[:parent_amount] + ga.population.append_children( + chromosome + for i, chromosome + in enumerate(mating_pool) + if i < parent_amount + ) # Parents need to be removed from the random sample to avoid dupes. else: - return mating_pool + random.sample( - set(ga.population) - mating_pool, - random_amount - ) + ga.population.append_children(mating_pool) + ga.population.append_children( + random.sample( + set(ga.population) - mating_pool, + random_amount + ) + ) diff --git a/src/termination/Termination.py b/src/termination/Termination.py index 959c5ee..b36453d 100644 --- a/src/termination/Termination.py +++ b/src/termination/Termination.py @@ -1,81 +1,5 @@ -from EasyGA import function_info - -@function_info -def _add_by_fitness_goal(termination_impl): - """Adds termination by fitness goal to the method.""" - - def new_method(ga): - - # Try to check the fitness goal - try: - - # If minimum fitness goal reached, stop ga. - if ga.target_fitness_type == 'min' and ga.population[0].fitness <= ga.fitness_goal: - return False - - # If maximum fitness goal reached, stop ga. - elif ga.target_fitness_type == 'max' and ga.population[0].fitness >= ga.fitness_goal: - return False - - # Fitness or fitness goals are None - except TypeError: - pass - - # Population not initialized - except AttributeError: - pass - - # Check other termination methods - return termination_impl(ga) - - return new_method - - -@function_info -def _add_by_generation_goal(termination_impl): - """Adds termination by generation goal to the method.""" - - def new_method(ga): - - # If generation goal is set, check it. - if ga.generation_goal is not None and ga.current_generation >= ga.generation_goal: - return False - - # Check other termination methods - return termination_impl(ga) - - return new_method - - -@function_info -def _add_by_tolerance_goal(termination_impl): - """Adds termination by tolerance goal to the method.""" - - def new_method(ga): - - # If tolerance is set, check it, if possible. - try: - best_fitness = ga.population[0].fitness - threshhold_fitness = ga.population[round(ga.percent_converged*len(ga.population))].fitness - tol = ga.tolerance_goal * (1 + abs(best_fitness)) - - # Terminate if the specified amount of the population has converged to the specified tolerance - if abs(best_fitness - threshhold_fitness) < tol: - return False - - # Fitness or tolerance goals are None - except TypeError: - pass - - # Population not initialized - except AttributeError: - pass - - # Check other termination methods - return termination_impl(ga) - - return new_method - +# Import all termination decorators +from decorators import _add_by_fitness_goal, _add_by_generation_goal, _add_by_tolerance_goal @_add_by_fitness_goal @_add_by_generation_goal