Decorators cleanly preserve both function names and doc-strings.

This commit is contained in:
SimpleArt
2020-12-27 10:11:43 -05:00
parent 44683c7fae
commit 2b6f0e2e97
5 changed files with 73 additions and 39 deletions

View File

@ -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 for square root (ga.dist()) and ceil (crossover methods)
import math import math
@ -47,16 +65,16 @@ class GA(Attributes):
"""Evolves the ga the specified number of generations """Evolves the ga the specified number of generations
or until the ga is no longer active if consider_termination is True.""" 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. cond1 = lambda: number_of_generations > 0 # Evolve the specified number of generations.
cond2 = lambda: not consider_termination # If consider_termination flag is set: cond2 = lambda: not consider_termination # If consider_termination flag is set:
cond3 = lambda: cond2() or self.active() # check termination conditions. cond3 = lambda: cond2() or self.active() # check termination conditions.
while cond1() and cond3(): 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 its the first generation, setup the database.
if self.current_generation == 0: if self.current_generation == 0:
@ -70,15 +88,15 @@ class GA(Attributes):
# Otherwise evolve the population. # Otherwise evolve the population.
else: else:
self.parent_selection_impl(self) self.parent_selection_impl()
self.crossover_population_impl(self) self.crossover_population_impl()
self.survivor_selection_impl(self) self.survivor_selection_impl()
self.population.update() self.population.update()
self.mutation_population_impl(self) self.mutation_population_impl()
# Update and sort fitnesses # Update and sort fitnesses
self.set_all_fitness() self.set_all_fitness()
self.population.sort_by_best_fitness(self) self.sort_by_best_fitness()
# Save the population to the database # Save the population to the database
self.save_population() self.save_population()
@ -96,7 +114,7 @@ class GA(Attributes):
def active(self): def active(self):
"""Returns if the ga should terminate based on the termination implimented.""" """Returns if the ga should terminate based on the termination implimented."""
return self.termination_impl(self) return self.termination_impl()
def adapt(self): def adapt(self):
@ -107,7 +125,7 @@ class GA(Attributes):
# Update and sort fitnesses # Update and sort fitnesses
self.set_all_fitness() self.set_all_fitness()
self.population.sort_by_best_fitness(self) self.sort_by_best_fitness()
def adapt_probabilities(self): def adapt_probabilities(self):
@ -204,7 +222,7 @@ class GA(Attributes):
self, self,
self.population[n], self.population[n],
best_chromosome, 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, # If negative weights can't be used,
@ -214,7 +232,7 @@ class GA(Attributes):
self, self,
self.population[n], self.population[n],
self.population[j], self.population[j],
0.75 weight = 0.75
) )
# Update fitnesses # Update fitnesses
@ -275,7 +293,7 @@ class GA(Attributes):
chromosome.fitness = self.fitness_function_impl(chromosome) 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. """Sorts the chromosome list by fitness based on fitness type.
1st element has best fitness. 1st element has best fitness.
2nd element has second best fitness. 2nd element has second best fitness.
@ -285,6 +303,10 @@ class GA(Attributes):
if self.target_fitness_type not in ('max', 'min'): if self.target_fitness_type not in ('max', 'min'):
raise ValueError("Unknown target fitness type") 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: if in_place:
chromosome_list.sort( # list to be sorted chromosome_list.sort( # list to be sorted
key = lambda chromosome: chromosome.fitness, # by fitness key = lambda chromosome: chromosome.fitness, # by fitness

View File

@ -1,9 +1,12 @@
from EasyGA import function_info
import random import random
# Round to an integer near x with higher probability # Round to an integer near x with higher probability
# the closer it is to that integer. # the closer it is to that integer.
randround = lambda x: int(x + random.random()) randround = lambda x: int(x + random.random())
@function_info
def _append_to_next_population(population_method): def _append_to_next_population(population_method):
"""Appends the new chromosomes to the next population. """Appends the new chromosomes to the next population.
Also modifies the input to include the mating pool. 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) population_method(ga, ga.population.mating_pool)
) )
new_method.__name__ = population_method.__name__
return new_method return new_method
@function_info
def _check_weight(individual_method): def _check_weight(individual_method):
"""Checks if the weight is between 0 and 1 before running. """Checks if the weight is between 0 and 1 before running.
Exception may occur when using ga.adapt, which will catch Exception may occur when using ga.adapt, which will catch
the error and try again with valid weight. 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: if weight is None:
return individual_method(ga, parent_1, parent_2, weight) return individual_method(ga, parent_1, parent_2)
elif 0 < weight < 1:
return individual_method(ga, parent_1, parent_2, weight = weight)
else: else:
raise ValueError("""Weight must be between 0 and 1 when using raise ValueError(f"Weight must be between 0 and 1 when using {individual_method.__name__}.")
the given crossover method.""")
new_method.__name__ = individual_method.__name__
return new_method return new_method
@ -53,10 +56,9 @@ class Crossover_Methods:
Every parent is paired with the previous parent. Every parent is paired with the previous parent.
The first parent is paired with the last parent. The first parent is paired with the last parent.
""" """
for index in range(len(mating_pool)): # for each parent in the mating pool for index in range(len(mating_pool)): # for each parent in the mating pool
yield ga.crossover_individual_impl( # apply crossover to yield ga.crossover_individual_impl( # apply crossover to
ga, #
mating_pool[index], # the parent and mating_pool[index], # the parent and
mating_pool[index-1], # the previous parent 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 for parent in mating_pool: # for each parent in the mating pool
yield ga.crossover_individual_impl( # apply crossover to yield ga.crossover_individual_impl( # apply crossover to
ga, #
parent, # the parent and parent, # the parent and
random.choice(mating_pool), # a random parent random.choice(mating_pool), # a random parent
) )
@ -81,7 +82,7 @@ class Crossover_Methods:
@_check_weight @_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.""" """Cross two parents by swapping genes at one random point."""
minimum_parent_length = min(len(parent_1), len(parent_2)) minimum_parent_length = min(len(parent_1), len(parent_2))
@ -107,13 +108,13 @@ class Crossover_Methods:
@_check_weight @_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.""" """Cross two parents by swapping genes at multiple points."""
pass pass
@_check_weight @_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.""" """Cross two parents by swapping all genes randomly."""
for gene_pair in zip(parent_1, parent_2): for gene_pair in zip(parent_1, parent_2):
@ -123,7 +124,7 @@ class Crossover_Methods:
class Arithmetic: class Arithmetic:
"""Crossover methods for numerical genes.""" """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.""" """Cross two parents by taking the average of the genes."""
values_1 = parent_1.gene_value_iter values_1 = parent_1.gene_value_iter
@ -139,7 +140,7 @@ class Crossover_Methods:
yield value 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. """Cross two parents by extrapolating towards the first parent.
May result in gene values outside the expected domain. May result in gene values outside the expected domain.
@ -159,7 +160,7 @@ class Crossover_Methods:
@_check_weight @_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.""" """Cross two parents by taking a random integer or float value between each of the genes."""
values_1 = parent_1.gene_value_iter values_1 = parent_1.gene_value_iter
@ -189,7 +190,7 @@ class Crossover_Methods:
"""Crossover methods for permutation based chromosomes.""" """Crossover methods for permutation based chromosomes."""
@_check_weight @_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 """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.""" and then filling in the rest of the genes from the second parent."""
@ -234,7 +235,7 @@ class Crossover_Methods:
@_check_weight @_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 """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, and then filling in the rest of the genes from the second parent,
preserving the ordering of genes wherever possible. preserving the ordering of genes wherever possible.

View File

@ -1,6 +1,9 @@
from EasyGA import function_info
import random import random
from math import ceil from math import ceil
@function_info
def _check_chromosome_mutation_rate(population_method): def _check_chromosome_mutation_rate(population_method):
"""Checks if the chromosome mutation rate is a float between 0 and 1 before running.""" """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: else:
raise ValueError("Chromosome mutation rate must be between 0 and 1.") raise ValueError("Chromosome mutation rate must be between 0 and 1.")
new_method.__name__ = population_method.__name__
return new_method return new_method
@function_info
def _check_gene_mutation_rate(individual_method): def _check_gene_mutation_rate(individual_method):
"""Checks if the gene mutation rate is a float between 0 and 1 before running.""" """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: else:
raise ValueError("Gene mutation rate must be between 0 and 1.") raise ValueError("Gene mutation rate must be between 0 and 1.")
new_method.__name__ = individual_method.__name__
return new_method return new_method
@function_info
def _reset_fitness(individual_method): def _reset_fitness(individual_method):
"""Resets the fitness value of the chromosome.""" """Resets the fitness value of the chromosome."""
@ -44,10 +47,10 @@ def _reset_fitness(individual_method):
chromosome.fitness = None chromosome.fitness = None
individual_method(ga, chromosome) individual_method(ga, chromosome)
new_method.__name__ = individual_method.__name__
return new_method return new_method
@function_info
def _loop_random_mutations(individual_method): def _loop_random_mutations(individual_method):
"""Runs the individual method until enough """Runs the individual method until enough
genes are mutated on the indexed chromosome. 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): for index in random.sample(sample_space, sample_size):
individual_method(ga, chromosome, index) individual_method(ga, chromosome, index)
new_method.__name__ = individual_method.__name__
return new_method return new_method
@ -87,7 +89,7 @@ class Mutation_Methods:
# Loop the individual method until enough genes are mutated. # Loop the individual method until enough genes are mutated.
for index in random.sample(sample_space, sample_size): 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 @_check_chromosome_mutation_rate
@ -98,7 +100,7 @@ class Mutation_Methods:
sample_size = ceil(ga.chromosome_mutation_rate*len(ga.population)) sample_size = ceil(ga.chromosome_mutation_rate*len(ga.population))
for index in random.sample(sample_space, sample_size): 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 @_check_chromosome_mutation_rate
@ -109,7 +111,7 @@ class Mutation_Methods:
for i in range(mutation_amount): for i in range(mutation_amount):
ga.population[-i-1] = ga.make_chromosome(ga.population[i]) 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: class Individual:

View File

@ -1,5 +1,8 @@
from EasyGA import function_info
import random import random
@function_info
def _check_selection_probability(selection_method): def _check_selection_probability(selection_method):
"""Raises an exception if the selection_probability """Raises an exception if the selection_probability
is not between 0 and 1 inclusively. Otherwise runs is not between 0 and 1 inclusively. Otherwise runs
@ -16,6 +19,7 @@ def _check_selection_probability(selection_method):
return new_method return new_method
@function_info
def _check_positive_fitness(selection_method): def _check_positive_fitness(selection_method):
"""Raises an exception if the population contains a """Raises an exception if the population contains a
chromosome with negative fitness. Otherwise runs chromosome with negative fitness. Otherwise runs
@ -32,6 +36,7 @@ def _check_positive_fitness(selection_method):
return new_method return new_method
@function_info
def _ensure_sorted(selection_method): def _ensure_sorted(selection_method):
"""Sorts the population by fitness """Sorts the population by fitness
and then runs the selection method. and then runs the selection method.
@ -45,6 +50,7 @@ def _ensure_sorted(selection_method):
return new_method return new_method
@function_info
def _compute_parent_amount(selection_method): def _compute_parent_amount(selection_method):
"""Computes the amount of parents """Computes the amount of parents
needed to be selected, and passes it needed to be selected, and passes it

View File

@ -1,5 +1,8 @@
from EasyGA import function_info
import random import random
@function_info
def _append_to_next_population(survivor_method): def _append_to_next_population(survivor_method):
"""Appends the selected chromosomes to the next population.""" """Appends the selected chromosomes to the next population."""