Decorators cleanly preserve both function names and doc-strings.
This commit is contained in:
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user