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
@ -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

View File

@ -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.

View File

@ -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:

View File

@ -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

View File

@ -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."""