Added decorators file
This commit is contained in:
@ -1,47 +1,13 @@
|
||||
from EasyGA import function_info
|
||||
import random
|
||||
|
||||
# Import all crossover decorators
|
||||
from decorators import _check_weight, _gene_by_gene
|
||||
|
||||
# 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."""
|
||||
|
||||
@ -61,7 +27,7 @@ class Population:
|
||||
)
|
||||
|
||||
|
||||
def random_mate(ga):
|
||||
def random(ga):
|
||||
"""Select random pairs from the mating pool.
|
||||
Every parent is paired with a random parent.
|
||||
"""
|
||||
@ -100,9 +66,9 @@ class Individual:
|
||||
|
||||
@_check_weight
|
||||
@_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."""
|
||||
return random.choices(gene_values, cum_weights = [weight, 1])[0]
|
||||
return random.choices(gene_pair, cum_weights = [weight, 1])[0]
|
||||
|
||||
|
||||
class Arithmetic:
|
||||
@ -136,7 +102,7 @@ class Individual:
|
||||
|
||||
@_check_weight
|
||||
@_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."""
|
||||
|
||||
value = value_1 + ga.weighted_random(weight) * (value_2-value_1)
|
||||
@ -158,7 +124,7 @@ class Individual:
|
||||
|
||||
# Too small to cross
|
||||
if len(parent_1) < 2:
|
||||
raise ValueError("Parent lengths must be at least 2.")
|
||||
return parent_1.gene_list
|
||||
|
||||
# Unequal parent lengths
|
||||
if len(parent_1) != len(parent_2):
|
||||
|
||||
261
src/decorators.py
Normal file
261
src/decorators.py
Normal 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
|
||||
|
||||
@ -1,72 +1,8 @@
|
||||
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
|
||||
# Import all mutation decorators
|
||||
from decorators import _check_chromosome_mutation_rate, _check_gene_mutation_rate, _reset_fitness, _loop_random_mutations
|
||||
|
||||
|
||||
class Population:
|
||||
|
||||
@ -1,57 +1,7 @@
|
||||
from EasyGA import function_info
|
||||
import random
|
||||
|
||||
|
||||
@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
|
||||
# Import all parent decorators
|
||||
from decorators import _check_selection_probability, _check_positive_fitness, _ensure_sorted, _compute_parent_amount
|
||||
|
||||
|
||||
class Rank:
|
||||
|
||||
@ -1,34 +1,23 @@
|
||||
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
|
||||
# Import all survivor decorators
|
||||
from decorators import *
|
||||
|
||||
|
||||
@_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]
|
||||
ga.population.append_children(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)
|
||||
ga.population.append_children(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"""
|
||||
|
||||
@ -41,11 +30,19 @@ def fill_in_parents_then_random(ga):
|
||||
|
||||
# Only parents are used.
|
||||
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.
|
||||
else:
|
||||
return mating_pool + random.sample(
|
||||
set(ga.population) - mating_pool,
|
||||
random_amount
|
||||
)
|
||||
ga.population.append_children(mating_pool)
|
||||
ga.population.append_children(
|
||||
random.sample(
|
||||
set(ga.population) - mating_pool,
|
||||
random_amount
|
||||
)
|
||||
)
|
||||
|
||||
@ -1,81 +1,5 @@
|
||||
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
|
||||
|
||||
# Import all termination decorators
|
||||
from decorators import _add_by_fitness_goal, _add_by_generation_goal, _add_by_tolerance_goal
|
||||
|
||||
@_add_by_fitness_goal
|
||||
@_add_by_generation_goal
|
||||
|
||||
Reference in New Issue
Block a user