From 54a774abd2d0cb874a9705739a8cda869dbfbe4a Mon Sep 17 00:00:00 2001 From: SimpleArt <71458112+SimpleArt@users.noreply.github.com> Date: Wed, 30 Dec 2020 14:35:43 -0500 Subject: [PATCH] Add back files with new names --- src/crossover/Crossover_Methods.py | 252 +++++++++++++++++ src/crossover/README.md | 1 + src/crossover/__init__.py | 0 src/crossover/test_crossover_methods.py | 0 src/fitness_function/Fitness_Examples.py | 46 ++++ src/fitness_function/README.md | 1 + src/fitness_function/__init__.py | 0 src/fitness_function/test_methods.py | 1 + src/mutation/Mutation_Methods.py | 208 ++++++++++++++ src/mutation/README.md | 1 + src/mutation/__init__.py | 0 src/mutation/test_mutation_methods.py | 0 src/parent_selection/Parent_Selection.py | 257 ++++++++++++++++++ src/parent_selection/README.md | 1 + src/parent_selection/__init__.py | 0 .../test_parent_selection_methods.py | 0 src/survivor_selection/README.md | 1 + src/survivor_selection/Survivor_Selection.py | 51 ++++ src/survivor_selection/__init__.py | 0 .../test_survivor_methods.py | 0 src/termination_point/README.md | 1 + src/termination_point/Termination_Methods.py | 90 ++++++ src/termination_point/__init__.py | 0 .../test_termination_methods.py | 0 24 files changed, 911 insertions(+) create mode 100644 src/crossover/Crossover_Methods.py create mode 100644 src/crossover/README.md create mode 100644 src/crossover/__init__.py create mode 100644 src/crossover/test_crossover_methods.py create mode 100644 src/fitness_function/Fitness_Examples.py create mode 100644 src/fitness_function/README.md create mode 100644 src/fitness_function/__init__.py create mode 100644 src/fitness_function/test_methods.py create mode 100644 src/mutation/Mutation_Methods.py create mode 100644 src/mutation/README.md create mode 100644 src/mutation/__init__.py create mode 100644 src/mutation/test_mutation_methods.py create mode 100644 src/parent_selection/Parent_Selection.py create mode 100644 src/parent_selection/README.md create mode 100644 src/parent_selection/__init__.py create mode 100644 src/parent_selection/test_parent_selection_methods.py create mode 100644 src/survivor_selection/README.md create mode 100644 src/survivor_selection/Survivor_Selection.py create mode 100644 src/survivor_selection/__init__.py create mode 100644 src/survivor_selection/test_survivor_methods.py create mode 100644 src/termination_point/README.md create mode 100644 src/termination_point/Termination_Methods.py create mode 100644 src/termination_point/__init__.py create mode 100644 src/termination_point/test_termination_methods.py diff --git a/src/crossover/Crossover_Methods.py b/src/crossover/Crossover_Methods.py new file mode 100644 index 0000000..6cf5dd3 --- /dev/null +++ b/src/crossover/Crossover_Methods.py @@ -0,0 +1,252 @@ +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 _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.""" + + + def sequential(ga, mating_pool): + """Select sequential pairs from the mating pool. + 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 + ga.crossover_individual_impl( # apply crossover to + mating_pool[index], # the parent and + mating_pool[index-1] # the previous parent + ) + + + def random(ga, mating_pool): + """Select random pairs from the mating pool. + Every parent is paired with a random parent. + """ + + for parent in mating_pool: # for each parent in the mating pool + ga.crossover_individual_impl( # apply crossover to + parent, # the parent and + random.choice(mating_pool) # a random parent + ) + + +class Individual: + """Methods for crossing parents.""" + + + @_check_weight + 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)) + + # Weighted random integer from 0 to minimum parent length - 1 + swap_index = int(ga.weighted_random(weight) * minimum_parent_length) + + ga.population.add_child(parent_1[:swap_index] + parent_2[swap_index:]) + ga.population.add_child(parent_2[:swap_index] + parent_1[swap_index:]) + + + @_check_weight + def multi_point(ga, parent_1, parent_2, *, weight = 0.5): + """Cross two parents by swapping genes at multiple points.""" + pass + + + @_check_weight + @_gene_by_gene + def uniform(ga, value_1, value_2, *, weight = 0.5): + """Cross two parents by swapping all genes randomly.""" + return random.choices(gene_pair, cum_weights = [weight, 1])[0] + + + class Arithmetic: + """Crossover methods for numerical genes.""" + + @_gene_by_gene + def average(ga, value_1, value_2, *, weight = 0.5): + """Cross two parents by taking the average of the genes.""" + + average_value = weight*value_1 + (1-weight)*value_2 + + if type(value_1) == type(value_2) == int: + average_value = randround(value) + + return average_value + + + @_gene_by_gene + def extrapolate(ga, value_1, value_2, *, weight = 0.5): + """Cross two parents by extrapolating towards the first parent. + May result in gene values outside the expected domain. + """ + + extrapolated_value = weight*value_1 + (1-weight)*value_2 + + if type(value_1) == type(value_2) == int: + extrapolated_value = randround(value) + + return extrapolated_value + + + @_check_weight + @_gene_by_gene + 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) + + if type(value_1) == type(value_2) == int: + value = randround(value) + + return value + + + class Permutation: + """Crossover methods for permutation based chromosomes.""" + + @_check_weight + 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. + """ + + # Too small to cross + if len(parent_1) < 2: + return parent_1.gene_list + + # Unequal parent lengths + if len(parent_1) != len(parent_2): + raise ValueError("Parents do not have the same lengths.") + + # Swap with weighted probability so that most of the genes + # are taken directly from parent 1. + if random.choices([0, 1], cum_weights = [weight, 1]) == 1: + parent_1, parent_2 = parent_2, parent_1 + + # Extract genes from parent 1 between two random indexes + index_2 = random.randrange(1, len(parent_1)) + index_1 = random.randrange(index_2) + + # Create copies of the gene lists + gene_list_1 = [None]*index_1 + parent_1[index_1:index_2] + [None]*(len(parent_1)-index_2) + gene_list_2 = list(parent_2) + + input_index = 0 + + # For each gene from the second parent + for _ in range(len(gene_list_2)): + + # Remove it if it is already used + if gene_list_2[-1] in gene_list_1: + gene_list_2.pop(-1) + + # Add it if it has not been used + else: + if input_index == index_1: + input_index = index_2 + gene_list_1[input_index] = gene_list_2.pop(-1) + input_index += 1 + + ga.population.add_child(gene_list_1) + + + @_check_weight + 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. + + NOTE: Needs to be fixed, since genes are not hashable...""" + + # Too small to cross + if len(parent_1) < 2: + return parent_1.gene_list + + # Unequal parent lengths + if len(parent_1) != len(parent_2): + raise ValueError("Parents do not have the same lengths.") + + # Swap with weighted probability so that most of the genes + # are taken directly from parent 1. + if random.choices([0, 1], cum_weights = [weight, 1]) == 1: + parent_1, parent_2 = parent_2, parent_1 + + # Extract genes from parent 1 between two random indexes + index_2 = random.randrange(1, len(parent_1)) + index_1 = random.randrange(index_2) + + # Create copies of the gene lists + gene_list_1 = [None]*index_1 + parent_1[index_1:index_2] + [None]*(len(parent_1)-index_2) + gene_list_2 = list(parent_2) + + # Create hash for gene list 2 + hash = {gene:index for index, gene in enumerate(gene_list_2)} + + # For each gene in the copied segment from parent 2 + for i in range(index_1, index_2): + + # If it is not already copied, + # find where it got displaced to + j = i + while gene_list_1[(j := hash[gene_list_1[j]])] is not None: + pass + gene_list_1[j] = gene_list_2[i] + + # Fill in whatever is leftover (copied from ox1). + # For each gene from the second parent + for _ in range(len(gene_list_2)): + + # Remove it if it is already used + if gene_list_2[-1] in gene_list_1: + gene_list_2.pop(-1) + + # Add it if it has not been used + else: + if input_index == index_1: + input_index = index_2 + gene_list_1[input_index] = gene_list_2.pop(-1) + input_index += 1 + + ga.population.add_child(gene_list_1) diff --git a/src/crossover/README.md b/src/crossover/README.md new file mode 100644 index 0000000..c38d93c --- /dev/null +++ b/src/crossover/README.md @@ -0,0 +1 @@ +# Mutation functions diff --git a/src/crossover/__init__.py b/src/crossover/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/crossover/test_crossover_methods.py b/src/crossover/test_crossover_methods.py new file mode 100644 index 0000000..e69de29 diff --git a/src/fitness_function/Fitness_Examples.py b/src/fitness_function/Fitness_Examples.py new file mode 100644 index 0000000..4eb85a0 --- /dev/null +++ b/src/fitness_function/Fitness_Examples.py @@ -0,0 +1,46 @@ +def is_it_5(chromosome): + """A very simple case test function - If the chromosome's gene value + is equal to 5 add one to the chromosomes overall fitness value. + """ + + # Overall fitness value + fitness = 0 + + for gene in chromosome: + + # Increment fitness is the gene's value is 5 + if gene.value == 5: + fitness += 1 + + return fitness + + +def near_5(chromosome): + """Test's the GA's ability to handle floats. Computes how close each gene is to 5.""" + + # Overall fitness value + fitness = 0 + + for gene in chromosome: + + # Add squared distance to 5 + fitness += (5 - gene.value) ** 2 + + return fitness + + +def index_dependent_values(chromosome): + """Test of the GA's ability to improve fitness when the value is index-dependent. + If a gene is equal to its index in the chromosome + 1, fitness is incremented. + """ + + # Overall fitness value + fitness = 0 + + for i, gene in enumerate(chromosome): + + # Increment fitness is the gene's value is i+1 + if gene.value == i+1: + fitness += 1 + + return fitness diff --git a/src/fitness_function/README.md b/src/fitness_function/README.md new file mode 100644 index 0000000..9bf8dfd --- /dev/null +++ b/src/fitness_function/README.md @@ -0,0 +1 @@ +# Fitness function diff --git a/src/fitness_function/__init__.py b/src/fitness_function/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/fitness_function/test_methods.py b/src/fitness_function/test_methods.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/fitness_function/test_methods.py @@ -0,0 +1 @@ + diff --git a/src/mutation/Mutation_Methods.py b/src/mutation/Mutation_Methods.py new file mode 100644 index 0000000..fcb8ef8 --- /dev/null +++ b/src/mutation/Mutation_Methods.py @@ -0,0 +1,208 @@ +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 + + +class Population: + """Methods for selecting chromosomes to mutate""" + + @_check_chromosome_mutation_rate + def random_selection(ga): + """Selects random chromosomes.""" + + sample_space = range(len(ga.population)) + sample_size = ceil(len(ga.population)*ga.chromosome_mutation_rate) + + # Loop the individual method until enough genes are mutated. + for index in random.sample(sample_space, sample_size): + ga.mutation_individual_impl(ga.population[index]) + + + @_check_chromosome_mutation_rate + def random_avoid_best(ga): + """Selects random chromosomes while avoiding the best chromosomes. (Elitism)""" + + sample_space = range(ceil(ga.percent_converged*len(ga.population)*3/16), len(ga.population)) + sample_size = ceil(ga.chromosome_mutation_rate*len(ga.population)) + + for index in random.sample(sample_space, sample_size): + ga.mutation_individual_impl(ga.population[index]) + + + @_check_chromosome_mutation_rate + def best_replace_worst(ga): + """Selects the best chromosomes, copies them, and replaces the worst chromosomes.""" + + mutation_amount = ceil(ga.chromosome_mutation_rate*len(ga.population)) + + for i in range(mutation_amount): + ga.population[-i-1] = ga.make_chromosome(ga.population[i]) + ga.mutation_individual_impl(ga.population[-i-1]) + + +class Individual: + """Methods for mutating a single chromosome.""" + + @_check_gene_mutation_rate + @_reset_fitness + @_loop_random_mutations + def individual_genes(ga, chromosome, index): + """Mutates random genes by making completely new genes.""" + + # Using the chromosome_impl + if ga.chromosome_impl is not None: + chromosome[index] = ga.make_gene(ga.chromosome_impl()[index]) + + # Using the gene_impl + elif ga.gene_impl is not None: + chromosome[index] = ga.make_gene(ga.gene_impl()) + + # Exit because no gene creation method specified + else: + raise Exception("Did not specify any initialization constraints.") + + + class Arithmetic: + """Methods for mutating a chromosome by numerically modifying the genes.""" + + @_check_gene_mutation_rate + @_reset_fitness + @_loop_random_mutations + def average(ga, chromosome, index): + """Mutates random genes by making completely new genes + and then averaging them with the old genes. May cause + premature convergence. Weight is the reciprocal of the + number of generations run.""" + + weight = 1/max(1, ga.current_generation) + + # Using the chromosome_impl + if ga.chromosome_impl is not None: + new_value = ga.chromosome_impl()[index] + + # Using the gene_impl + elif ga.gene_impl is not None: + new_value = ga.gene_impl() + + # Exit because no gene creation method specified + else: + raise Exception("Did not specify any initialization constraints.") + + chromosome[index] = ga.make_gene((1-weight)*chromosome[index].value + weight*new_value) + + + @_check_gene_mutation_rate + @_reset_fitness + @_loop_random_mutations + def reflect_genes(ga, chromosome, index): + """Reflects genes against the best chromosome. + Requires large genetic variety to work well but + when it does it may be very fast.""" + + difference = ga.population[0][index].value - chromosome[index].value + value = ga.population[0][index].value + 2*difference + chromosome[index] = ga.make_gene(value) + + + class Permutation: + """Methods for mutating a chromosome by changing the order of the genes.""" + + @_check_gene_mutation_rate + @_reset_fitness + @_loop_random_mutations + def swap_genes(ga, chromosome, index): + """Swaps two random genes in the chromosome.""" + + # Indexes of genes to swap + index_one = index + index_two = random.randrange(len(chromosome)) + + # Swap genes + chromosome[index_one], chromosome[index_two] = chromosome[index_two], chromosome[index_one] + + + @_check_gene_mutation_rate + @_reset_fitness + def swap_segments(ga, chromosome): + """Splits the chromosome into 3 segments and shuffle them.""" + + # Chromosome too short to mutate + if len(chromosome) < 3: + return + + # Indexes to split the chromosome + index_two = random.randrange(2, len(chromosome)) + index_one = random.randrange(1, index_two) + + # Extract segments and shuffle them + segments = [chromosome[:index_one], chromosome[index_one:index_two], chromosome[index_two:]] + random.shuffle(segments) + + # Put segments back together + chromosome.gene_list = segments[0] + segments[1] + segments[2] diff --git a/src/mutation/README.md b/src/mutation/README.md new file mode 100644 index 0000000..c38d93c --- /dev/null +++ b/src/mutation/README.md @@ -0,0 +1 @@ +# Mutation functions diff --git a/src/mutation/__init__.py b/src/mutation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/mutation/test_mutation_methods.py b/src/mutation/test_mutation_methods.py new file mode 100644 index 0000000..e69de29 diff --git a/src/parent_selection/Parent_Selection.py b/src/parent_selection/Parent_Selection.py new file mode 100644 index 0000000..d5c7d4d --- /dev/null +++ b/src/parent_selection/Parent_Selection.py @@ -0,0 +1,257 @@ +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 + the selection method. + """ + + def new_method(ga): + if 0 <= ga.selection_probability <= 1: + selection_method(ga) + else: + raise Exception("Selection probability must be between 0 and 1 to select parents.") + + new_method.__name__ = selection_method.__name__ + 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 + 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 Exception("Converted fitness values can't have negative values or be all 0. Consider using rank selection or stochastic selection instead.") + + new_method.__name__ = selection_method.__name__ + 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.population.sort_by_best_fitness(ga) + selection_method(ga) + + new_method.__name__ = selection_method.__name__ + 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) + + new_method.__name__ = selection_method.__name__ + return new_method + + +class Rank: + """Methods for selecting parents based on their rankings in the population + i.e. the n-th best chromosome has a fixed probability of being selected, + regardless of their chances""" + + @_check_selection_probability + @_ensure_sorted + @_compute_parent_amount + def tournament(ga, parent_amount): + """ + Will make tournaments of size tournament_size and choose the winner (best fitness) + from the tournament and use it as a parent for the next generation. The total number + of parents selected is determined by parent_ratio, an attribute to the GA object. + May require many loops if the selection probability is very low. + """ + + # Choose the tournament size. + # Use no less than 5 chromosomes per tournament. + tournament_size = int(len(ga.population)*ga.tournament_size_ratio) + if tournament_size < 5: + tournament_size = min(5, len(ga.population)) + + # Repeat tournaments until the mating pool is large enough. + while len(ga.population.mating_pool) < parent_amount: + + # Generate a random tournament group and sort by fitness. + tournament_group = sorted(random.sample( + range(len(ga.population)), + tournament_size + )) + + # For each chromosome, add it to the mating pool based on its rank in the tournament. + for index in range(tournament_size): + + # Probability required is selection_probability * (1-selection_probability) ^ index + # Each chromosome is (1-selection_probability) times + # more likely to become a parent than the next ranked. + if random.random() < ga.selection_probability * (1-ga.selection_probability) ** index: + break + + # Use random in tournament if noone wins + else: + index = random.randrange(tournament_size) + + ga.population.set_parent(tournament_group[index]) + + + @_check_selection_probability + @_ensure_sorted + @_compute_parent_amount + def stochastic_geometric(ga, parent_amount): + """ + Selects parents with probabilities given by a geometric progression. This + method is similar to tournament selection, but doesn't create several + tournaments. Instead, it assigns probabilities to each rank and selects + the entire mating pool using random.choices. Since it essentially uses the + entire population as a tournament repeatedly, it is less likely to select + worse parents than tournament selection. + """ + + # Set the weights of each parent based on their rank. + # Each chromosome is (1-selection_probability) times + # more likely to become a parent than the next ranked. + weights = [ + (1-ga.selection_probability) ** i + for i + in range(len(ga.population)) + ] + + # Set the mating pool. + ga.population.mating_pool = random.choices(ga.population, weights, k = parent_amount) + + + @_check_selection_probability + @_ensure_sorted + @_compute_parent_amount + def stochastic_arithmetic(ga, parent_amount): + """ + Selects parents with probabilities given by an arithmetic progression. This + method is similar to stochastic-geometric selection, but is more likely to + select worse parents with its simpler selection scheme. + """ + + # Set the weights of each parent based on their rank. + # The worst chromosome has a weight of 1, + # the next worst chromosome has a weight of 2, + # etc. + # with an inflation of (1-selection probability) * average weight + + average_weight = (len(ga.population)+1) // 2 + inflation = (1-ga.selection_probability) * average_weight + + weights = [ + i + inflation + for i + in range(len(ga.population), 0, -1) + ] + + # Set the mating pool. + ga.population.mating_pool = random.choices(ga.population, weights, k = parent_amount) + + +class Fitness: + + @_check_selection_probability + @_ensure_sorted + @_check_positive_fitness + @_compute_parent_amount + def roulette(ga, parent_amount): + """Roulette selection works based off of how strong the fitness is of the + chromosomes in the population. The stronger the fitness the higher the probability + that it will be selected. Using the example of a casino roulette wheel. + Where the chromosomes are the numbers to be selected and the board size for + those numbers are directly proportional to the chromosome's current fitness. Where + the ball falls is a randomly generated number between 0 and 1. + """ + + # The sum of all the fitnessess in a population + fitness_sum = sum( + ga.get_chromosome_fitness(index) + for index + in range(len(ga.population)) + ) + + # A list of ranges that represent the probability of a chromosome getting chosen + probability = [ga.selection_probability] + + # The chance of being selected increases incrementally + for index in range(len(ga.population)): + probability.append(probability[-1]+ga.get_chromosome_fitness(index)/fitness_sum) + + probability = probability[1:] + + # Loops until it reaches a desired mating pool size + while len(ga.population.mating_pool) < parent_amount: + + # Spin the roulette + rand_number = random.random() + + # Find where the roulette landed. + for index in range(len(probability)): + if (probability[index] >= rand_number): + ga.population.set_parent(index) + break + + + @_check_selection_probability + @_ensure_sorted + @_compute_parent_amount + def stochastic(ga, parent_amount): + """ + Selects parents using the same probability approach as roulette selection, + but doesn't spin a roulette for every selection. Uses random.choices with + weighted values to select parents and may produce duplicate parents. + """ + + # All fitnesses are the same, select randomly. + if ga.get_chromosome_fitness(-1) == ga.get_chromosome_fitness(0): + offset = 1-ga.get_chromosome_fitness(-1) + + # Some chromosomes have negative fitness, shift them all into positives. + elif ga.get_chromosome_fitness(-1) < 0: + offset = -ga.get_chromosome_fitness(-1) + + # No change needed. + else: + offset = 0 + + # Set the weights of each parent based on their fitness + offset. + weights = [ + ga.get_chromosome_fitness(index) + offset + for index + in range(len(ga.population)) + ] + + inflation = sum(weights) * (1 - ga.selection_probability) + + # Rescale and adjust using selection_probability so that + # if selection_probability is high, a low inflation is used, + # making selection mostly based on fitness. + # if selection_probability is low, a high offset is used, + # so everyone has a more equal chance. + weights = [ + weight + inflation + for weight + in weights + ] + + # Set the mating pool. + ga.population.mating_pool = random.choices(ga.population, weights, k = parent_amount) diff --git a/src/parent_selection/README.md b/src/parent_selection/README.md new file mode 100644 index 0000000..98253e9 --- /dev/null +++ b/src/parent_selection/README.md @@ -0,0 +1 @@ +# Selection functions diff --git a/src/parent_selection/__init__.py b/src/parent_selection/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/parent_selection/test_parent_selection_methods.py b/src/parent_selection/test_parent_selection_methods.py new file mode 100644 index 0000000..e69de29 diff --git a/src/survivor_selection/README.md b/src/survivor_selection/README.md new file mode 100644 index 0000000..98253e9 --- /dev/null +++ b/src/survivor_selection/README.md @@ -0,0 +1 @@ +# Selection functions diff --git a/src/survivor_selection/Survivor_Selection.py b/src/survivor_selection/Survivor_Selection.py new file mode 100644 index 0000000..ce41653 --- /dev/null +++ b/src/survivor_selection/Survivor_Selection.py @@ -0,0 +1,51 @@ +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 + + +@_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] + + +@_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) + + +@_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""" + + # Remove dupes from the mating pool + mating_pool = set(ga.population.mating_pool) + + needed_amount = len(ga.population) - len(ga.population.next_population) + parent_amount = min(needed_amount, len(mating_pool)) + random_amount = needed_amount - parent_amount + + # Only parents are used. + if random_amount == 0: + return ga.population.mating_pool[: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 + ) diff --git a/src/survivor_selection/__init__.py b/src/survivor_selection/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/survivor_selection/test_survivor_methods.py b/src/survivor_selection/test_survivor_methods.py new file mode 100644 index 0000000..e69de29 diff --git a/src/termination_point/README.md b/src/termination_point/README.md new file mode 100644 index 0000000..7b74650 --- /dev/null +++ b/src/termination_point/README.md @@ -0,0 +1 @@ +# Termination functions diff --git a/src/termination_point/Termination_Methods.py b/src/termination_point/Termination_Methods.py new file mode 100644 index 0000000..959c5ee --- /dev/null +++ b/src/termination_point/Termination_Methods.py @@ -0,0 +1,90 @@ +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 + + +@_add_by_fitness_goal +@_add_by_generation_goal +@_add_by_tolerance_goal +def fitness_generation_tolerance(ga): + """Terminate GA when any of the + - fitness, + - generation, or + - tolerance + goals are met.""" + + return True diff --git a/src/termination_point/__init__.py b/src/termination_point/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/termination_point/test_termination_methods.py b/src/termination_point/test_termination_methods.py new file mode 100644 index 0000000..e69de29