Added decorators file

This commit is contained in:
SimpleArt
2021-01-01 16:09:27 -05:00
parent 423c9884a8
commit b2b1775e16
6 changed files with 292 additions and 258 deletions

View File

@ -1,47 +1,13 @@
from EasyGA import function_info
import random import random
# Import all crossover decorators
from decorators import _check_weight, _gene_by_gene
# 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 _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: class Population:
"""Methods for selecting chromosomes to crossover.""" """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. """Select random pairs from the mating pool.
Every parent is paired with a random parent. Every parent is paired with a random parent.
""" """
@ -100,9 +66,9 @@ class Individual:
@_check_weight @_check_weight
@_gene_by_gene @_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.""" """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: class Arithmetic:
@ -136,7 +102,7 @@ class Individual:
@_check_weight @_check_weight
@_gene_by_gene @_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.""" """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) value = value_1 + ga.weighted_random(weight) * (value_2-value_1)
@ -158,7 +124,7 @@ class Individual:
# Too small to cross # Too small to cross
if len(parent_1) < 2: if len(parent_1) < 2:
raise ValueError("Parent lengths must be at least 2.") return parent_1.gene_list
# Unequal parent lengths # Unequal parent lengths
if len(parent_1) != len(parent_2): if len(parent_1) != len(parent_2):

261
src/decorators.py Normal file
View File

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

View File

@ -1,72 +1,8 @@
from EasyGA import function_info
import random import random
from math import ceil from math import ceil
# Import all mutation decorators
@function_info from decorators import _check_chromosome_mutation_rate, _check_gene_mutation_rate, _reset_fitness, _loop_random_mutations
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: class Population:

View File

@ -1,57 +1,7 @@
from EasyGA import function_info
import random import random
# Import all parent decorators
@function_info from decorators import _check_selection_probability, _check_positive_fitness, _ensure_sorted, _compute_parent_amount
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
class Rank: class Rank:

View File

@ -1,34 +1,23 @@
from EasyGA import function_info
import random import random
# Import all survivor decorators
@function_info from decorators import *
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): def fill_in_best(ga):
"""Fills in the next population with the best chromosomes from the last population""" """Fills in the next population with the best chromosomes from the last population"""
needed_amount = len(ga.population) - len(ga.population.next_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): def fill_in_random(ga):
"""Fills in the next population with random chromosomes from the last population""" """Fills in the next population with random chromosomes from the last population"""
needed_amount = len(ga.population) - len(ga.population.next_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): def fill_in_parents_then_random(ga):
"""Fills in the next population with all parents followed by random chromosomes from the last population""" """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. # Only parents are used.
if random_amount == 0: 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. # Parents need to be removed from the random sample to avoid dupes.
else: else:
return mating_pool + random.sample( ga.population.append_children(mating_pool)
set(ga.population) - mating_pool, ga.population.append_children(
random_amount random.sample(
) set(ga.population) - mating_pool,
random_amount
)
)

View File

@ -1,81 +1,5 @@
from EasyGA import function_info # Import all termination decorators
from decorators import _add_by_fitness_goal, _add_by_generation_goal, _add_by_tolerance_goal
@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_fitness_goal
@_add_by_generation_goal @_add_by_generation_goal