Directory
This commit is contained in:
337
EasyGA/EasyGA.py
Normal file
337
EasyGA/EasyGA.py
Normal file
@ -0,0 +1,337 @@
|
||||
# Import math for square root (ga.dist()) and ceil (crossover methods)
|
||||
import math
|
||||
|
||||
# Import random for many methods
|
||||
import random
|
||||
|
||||
# Import all decorators
|
||||
import decorators
|
||||
|
||||
# Import all the data structure prebuilt modules
|
||||
from structure import Population as make_population
|
||||
from structure import Chromosome as make_chromosome
|
||||
from structure import Gene as make_gene
|
||||
|
||||
# Misc. Methods
|
||||
from examples import Fitness
|
||||
from termination import Termination
|
||||
|
||||
# Parent/Survivor Selection Methods
|
||||
from parent import Parent
|
||||
from survivor import Survivor
|
||||
|
||||
# Genetic Operator Methods
|
||||
from crossover import Crossover
|
||||
from mutation import Mutation
|
||||
|
||||
# Default Attributes for the GA
|
||||
from attributes import Attributes
|
||||
|
||||
# Database class
|
||||
from database import sql_database
|
||||
from sqlite3 import Error
|
||||
|
||||
# Graphing package
|
||||
from database import matplotlib_graph
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
|
||||
class GA(Attributes):
|
||||
"""GA is the main class in EasyGA. Everything is run through the ga
|
||||
class. The GA class inherites all the default ga attributes from the
|
||||
attributes class.
|
||||
|
||||
An extensive wiki going over all major functions can be found at
|
||||
https://github.com/danielwilczak101/EasyGA/wiki
|
||||
"""
|
||||
|
||||
|
||||
def evolve(self, number_of_generations = float('inf'), consider_termination = True):
|
||||
"""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():
|
||||
|
||||
# If its the first generation, setup the database.
|
||||
if self.current_generation == 0:
|
||||
|
||||
# Create the database here to allow the user to change the
|
||||
# database name and structure before running the function.
|
||||
self.database.create_all_tables(self)
|
||||
|
||||
# Add the current configuration to the config table
|
||||
self.database.insert_config(self)
|
||||
|
||||
# Otherwise evolve the population.
|
||||
else:
|
||||
self.parent_selection_impl()
|
||||
self.crossover_population_impl()
|
||||
self.survivor_selection_impl()
|
||||
self.update_population()
|
||||
self.sort_by_best_fitness()
|
||||
self.mutation_population_impl()
|
||||
|
||||
# Update and sort fitnesses
|
||||
self.set_all_fitness()
|
||||
self.sort_by_best_fitness()
|
||||
|
||||
# Save the population to the database
|
||||
self.save_population()
|
||||
|
||||
# Adapt the ga if the generation times the adapt rate
|
||||
# passes through an integer value.
|
||||
adapt_counter = self.adapt_rate*self.current_generation
|
||||
if int(adapt_counter) < int(adapt_counter + self.adapt_rate):
|
||||
self.adapt()
|
||||
|
||||
number_of_generations -= 1
|
||||
self.current_generation += 1
|
||||
|
||||
|
||||
def update_population(self):
|
||||
"""Updates the population to the new population and resets
|
||||
the mating pool and new population."""
|
||||
|
||||
self.population.update()
|
||||
|
||||
|
||||
def reset_run(self):
|
||||
"""Resets a run by re-initializing the population
|
||||
and modifying counters."""
|
||||
|
||||
self.initialize_population()
|
||||
self.current_generation = 0
|
||||
self.run += 1
|
||||
|
||||
|
||||
def active(self):
|
||||
"""Returns if the ga should terminate based on the
|
||||
termination implimented."""
|
||||
|
||||
return self.termination_impl()
|
||||
|
||||
|
||||
def adapt(self):
|
||||
"""Adapts the ga to hopefully get better results."""
|
||||
|
||||
self.adapt_probabilities()
|
||||
self.adapt_population()
|
||||
|
||||
# Update and sort fitnesses
|
||||
self.set_all_fitness()
|
||||
self.sort_by_best_fitness()
|
||||
|
||||
|
||||
def adapt_probabilities(self):
|
||||
"""Modifies the parent ratio and mutation rates
|
||||
based on the adapt rate and percent converged.
|
||||
Attempts to balance out so that a portion of the
|
||||
population gradually approaches the solution.
|
||||
"""
|
||||
|
||||
# Determines how much to adapt by
|
||||
weight = self.adapt_probability_rate
|
||||
|
||||
# Don't adapt
|
||||
if weight is None or weight <= 0:
|
||||
return
|
||||
|
||||
# Amount of the population desired to converge (default 50%)
|
||||
amount_converged = round(self.percent_converged * len(self.population))
|
||||
|
||||
# Difference between best and i-th chromosomes
|
||||
best_chromosome = self.population[0]
|
||||
tol = lambda i: self.dist(best_chromosome, self.population[i])
|
||||
|
||||
# Too few converged: cross more and mutate less
|
||||
if tol(amount_converged//2) > tol(amount_converged//4)*2:
|
||||
bounds = (self.max_selection_probability,
|
||||
self.min_chromosome_mutation_rate,
|
||||
self.min_gene_mutation_rate)
|
||||
|
||||
# Too many converged: cross less and mutate more
|
||||
else:
|
||||
bounds = (self.min_selection_probability,
|
||||
self.max_chromosome_mutation_rate,
|
||||
self.max_gene_mutation_rate)
|
||||
|
||||
# Weighted average of x and y
|
||||
average = lambda x, y: weight * x + (1-weight) * y
|
||||
|
||||
# Adjust rates towards the bounds
|
||||
self.selection_probability = average(bounds[0], self.selection_probability)
|
||||
self.chromosome_mutation_rate = average(bounds[1], self.chromosome_mutation_rate)
|
||||
self.gene_mutation_rate = average(bounds[2], self.gene_mutation_rate)
|
||||
|
||||
|
||||
def adapt_population(self):
|
||||
"""
|
||||
Performs weighted crossover between the best chromosome and
|
||||
the rest of the chromosomes, using negative weights to push
|
||||
away chromosomes that are too similar and small positive
|
||||
weights to pull in chromosomes that are too different.
|
||||
"""
|
||||
|
||||
# Don't adapt the population.
|
||||
if self.adapt_population_flag == False:
|
||||
return
|
||||
|
||||
self.parent_selection_impl()
|
||||
|
||||
# Strongly cross the best chromosome with all other chromosomes
|
||||
for n, parent in enumerate(self.population.mating_pool):
|
||||
|
||||
if self.population[n] != self.population[0]:
|
||||
|
||||
# Strongly cross with the best chromosome
|
||||
# May reject negative weight or division by 0
|
||||
try:
|
||||
self.crossover_individual_impl(
|
||||
self.population[n],
|
||||
parent,
|
||||
weight = -3/4,
|
||||
)
|
||||
|
||||
# If negative weights can't be used or division by 0, use positive weight
|
||||
except ValueError:
|
||||
self.crossover_individual_impl(
|
||||
self.population[n],
|
||||
parent,
|
||||
weight = +1/4,
|
||||
)
|
||||
|
||||
# Stop if we've filled up an entire population
|
||||
if len(self.population.next_population) >= len(self.population):
|
||||
break
|
||||
|
||||
# Replace worst chromosomes with new chromosomes, except for the previous best chromosome
|
||||
min_len = min(len(self.population)-1, len(self.population.next_population))
|
||||
if min_len > 0:
|
||||
self.population[-min_len:] = self.population.next_population[:min_len]
|
||||
self.population.next_population = []
|
||||
self.population.mating_pool = []
|
||||
|
||||
|
||||
def initialize_population(self):
|
||||
"""Initialize the population using
|
||||
the initialization implimentation
|
||||
that is currently set.
|
||||
"""
|
||||
|
||||
if self.chromosome_impl is not None:
|
||||
self.population = self.make_population(
|
||||
self.chromosome_impl()
|
||||
for _
|
||||
in range(self.population_size)
|
||||
)
|
||||
|
||||
elif self.gene_impl is not None:
|
||||
self.population = self.make_population(
|
||||
(
|
||||
self.gene_impl()
|
||||
for __
|
||||
in range(self.chromosome_length)
|
||||
)
|
||||
for _
|
||||
in range(self.population_size)
|
||||
)
|
||||
|
||||
else:
|
||||
raise ValueError("No chromosome or gene impl specified.")
|
||||
|
||||
|
||||
def set_all_fitness(self):
|
||||
"""Will get and set the fitness of each chromosome in the population.
|
||||
If update_fitness is set then all fitness values are updated.
|
||||
Otherwise only fitness values set to None (i.e. uninitialized
|
||||
fitness values) are updated.
|
||||
"""
|
||||
|
||||
# Check each chromosome
|
||||
for chromosome in self.population:
|
||||
|
||||
# Update fitness if needed or asked by the user
|
||||
if chromosome.fitness is None or self.update_fitness:
|
||||
chromosome.fitness = self.fitness_function_impl(chromosome)
|
||||
|
||||
|
||||
def sort_by_best_fitness(self, chromosome_list = None, in_place = True):
|
||||
"""Sorts the chromosome list by fitness based on fitness type.
|
||||
1st element has best fitness.
|
||||
2nd element has second best fitness.
|
||||
etc.
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
# Reversed sort if max fitness should be first
|
||||
reverse = (self.target_fitness_type == 'max')
|
||||
|
||||
# Sort by fitness, assuming None should be moved to the end of the list
|
||||
key = lambda chromosome: (chromosome.fitness if (chromosome.fitness is not None) else (float('inf') * (+1, -1)[int(reverse)]))
|
||||
|
||||
if in_place:
|
||||
chromosome_list.sort(key = key, reverse = reverse)
|
||||
return chromosome_list
|
||||
|
||||
else:
|
||||
return sorted(chromosome_list, key = key, reverse = reverse)
|
||||
|
||||
|
||||
def get_chromosome_fitness(self, index):
|
||||
"""Returns the fitness value of the chromosome
|
||||
at the specified index after conversion based
|
||||
on the target fitness type.
|
||||
"""
|
||||
|
||||
return self.convert_fitness(self.population[index].fitness)
|
||||
|
||||
|
||||
def convert_fitness(self, fitness_value):
|
||||
"""Returns the fitness value if the type of problem
|
||||
is a maximization problem. Otherwise the fitness is
|
||||
inverted using max - value + min.
|
||||
"""
|
||||
|
||||
# No conversion needed
|
||||
if self.target_fitness_type == 'max': return fitness_value
|
||||
|
||||
max_fitness = self.population[-1].fitness
|
||||
min_fitness = self.population[0].fitness
|
||||
|
||||
return max_fitness - fitness_value + min_fitness
|
||||
|
||||
|
||||
def print_generation(self):
|
||||
"""Prints the current generation"""
|
||||
print(f"Current Generation \t: {self.current_generation}")
|
||||
|
||||
|
||||
def print_population(self):
|
||||
"""Prints the entire population"""
|
||||
print(self.population)
|
||||
|
||||
|
||||
def print_best_chromosome(self):
|
||||
"""Prints the best chromosome and its fitness"""
|
||||
print(f"Best Chromosome \t: {self.population[0]}")
|
||||
print(f"Best Fitness \t: {self.population[0].fitness}")
|
||||
|
||||
|
||||
def print_worst_chromosome(self):
|
||||
"""Prints the worst chromosome and its fitness"""
|
||||
print(f"Worst Chromosome \t: {self.population[-1]}")
|
||||
print(f"Worst Fitness \t: {self.population[-1].fitness}")
|
||||
0
EasyGA/__init__.py
Normal file
0
EasyGA/__init__.py
Normal file
433
EasyGA/attributes.py
Normal file
433
EasyGA/attributes.py
Normal file
@ -0,0 +1,433 @@
|
||||
# Import signature tool to check if functions start with self or ga
|
||||
from inspect import signature
|
||||
|
||||
# Import math for square root (ga.dist()) and ceil (crossover methods)
|
||||
import math
|
||||
|
||||
import random
|
||||
import sqlite3
|
||||
from copy import deepcopy
|
||||
|
||||
# Import all the data structure prebuilt modules
|
||||
from structure import Population as make_population
|
||||
from structure import Chromosome as make_chromosome
|
||||
from structure import Gene as make_gene
|
||||
|
||||
# Misc. Methods
|
||||
from examples import Fitness
|
||||
from termination import Termination
|
||||
|
||||
# Parent/Survivor Selection Methods
|
||||
from parent import Parent
|
||||
from survivor import Survivor
|
||||
|
||||
# Genetic Operator Methods
|
||||
from crossover import Crossover
|
||||
from mutation import Mutation
|
||||
|
||||
# Database class
|
||||
from database import sql_database
|
||||
from sqlite3 import Error
|
||||
|
||||
# Graphing package
|
||||
from database import matplotlib_graph
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
|
||||
class Attributes:
|
||||
"""Default GA attributes can be found here. If any attributes have not
|
||||
been set then they will fall back onto the default attribute. All
|
||||
attributes have been catigorized to explain sections in the ga process."""
|
||||
|
||||
#=====================#
|
||||
# Default GA methods: #
|
||||
#=====================#
|
||||
|
||||
# Default EasyGA implimentation structure
|
||||
fitness_function_impl = Fitness.is_it_5
|
||||
make_population = make_population
|
||||
make_chromosome = make_chromosome
|
||||
make_gene = make_gene
|
||||
|
||||
# Methods for accomplishing Parent-Selection -> Crossover -> Survivor_Selection -> Mutation -> Termination
|
||||
parent_selection_impl = Parent.Rank.tournament
|
||||
crossover_individual_impl = Crossover.Individual.single_point
|
||||
crossover_population_impl = Crossover.Population.sequential
|
||||
survivor_selection_impl = Survivor.fill_in_best
|
||||
mutation_individual_impl = Mutation.Individual.individual_genes
|
||||
mutation_population_impl = Mutation.Population.random_avoid_best
|
||||
termination_impl = Termination.fitness_generation_tolerance
|
||||
|
||||
|
||||
def dist(self, chromosome_1, chromosome_2):
|
||||
"""Default distance lambda. Returns the square root of the difference in fitnesses."""
|
||||
return math.sqrt(abs(chromosome_1.fitness - chromosome_2.fitness))
|
||||
|
||||
|
||||
def weighted_random(self, weight):
|
||||
"""Returns a random value between 0 and 1. Returns values between the weight and the
|
||||
nearest of 0 and 1 less frequently than between weight and the farthest of 0 and 1."""
|
||||
|
||||
rand_num = random.random()
|
||||
if rand_num < weight:
|
||||
return (1-weight) * rand_num / weight
|
||||
else:
|
||||
return 1 - weight * (1-rand_num) / (1-weight)
|
||||
|
||||
|
||||
def gene_impl(self, *args, **kwargs):
|
||||
"""Default gene implementation. Returns a random integer from 1 to 10."""
|
||||
return random.randint(1, 10)
|
||||
|
||||
|
||||
chromosome_impl = None
|
||||
|
||||
|
||||
#=====================================#
|
||||
# Special built-in class __methods__: #
|
||||
#=====================================#
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
# Attributes must be passed in using kwargs
|
||||
|
||||
run = 0,
|
||||
|
||||
chromosome_length = 10,
|
||||
population_size = 10,
|
||||
population = None,
|
||||
target_fitness_type = 'max',
|
||||
update_fitness = False,
|
||||
|
||||
parent_ratio = 0.10,
|
||||
selection_probability = 0.50,
|
||||
tournament_size_ratio = 0.10,
|
||||
|
||||
current_generation = 0,
|
||||
current_fitness = 0,
|
||||
|
||||
generation_goal = 100,
|
||||
fitness_goal = None,
|
||||
tolerance_goal = None,
|
||||
percent_converged = 0.50,
|
||||
|
||||
chromosome_mutation_rate = 0.15,
|
||||
gene_mutation_rate = 0.05,
|
||||
|
||||
adapt_rate = 0.05,
|
||||
adapt_probability_rate = 0.05,
|
||||
adapt_population_flag = True,
|
||||
|
||||
max_selection_probability = 0.75,
|
||||
min_selection_probability = 0.25,
|
||||
max_chromosome_mutation_rate = None,
|
||||
min_chromosome_mutation_rate = None,
|
||||
max_gene_mutation_rate = 0.15,
|
||||
min_gene_mutation_rate = 0.01,
|
||||
|
||||
Database = sql_database.SQL_Database,
|
||||
database_name = 'database.db',
|
||||
sql_create_data_structure = f"""
|
||||
CREATE TABLE IF NOT EXISTS data (
|
||||
id INTEGER PRIMARY KEY,
|
||||
config_id INTEGER DEFAULT NULL,
|
||||
generation INTEGER NOT NULL,
|
||||
fitness REAL,
|
||||
chromosome TEXT
|
||||
); """,
|
||||
|
||||
Graph = matplotlib_graph.Matplotlib_Graph,
|
||||
|
||||
**kwargs
|
||||
):
|
||||
|
||||
# Keep track of the current run
|
||||
self.run = run
|
||||
|
||||
# Initilization variables
|
||||
self.chromosome_length = chromosome_length
|
||||
self.population_size = population_size
|
||||
self.population = population
|
||||
self.target_fitness_type = target_fitness_type
|
||||
self.update_fitness = update_fitness
|
||||
|
||||
# Selection variables
|
||||
self.parent_ratio = parent_ratio
|
||||
self.selection_probability = selection_probability
|
||||
self.tournament_size_ratio = tournament_size_ratio
|
||||
|
||||
# Termination variables
|
||||
self.current_generation = current_generation
|
||||
self.current_fitness = current_fitness
|
||||
self.generation_goal = generation_goal
|
||||
self.fitness_goal = fitness_goal
|
||||
self.tolerance_goal = tolerance_goal
|
||||
self.percent_converged = percent_converged
|
||||
|
||||
# Mutation variables
|
||||
self.chromosome_mutation_rate = chromosome_mutation_rate
|
||||
self.gene_mutation_rate = gene_mutation_rate
|
||||
|
||||
# Adapt variables
|
||||
self.adapt_rate = adapt_rate
|
||||
self.adapt_probability_rate = adapt_probability_rate
|
||||
self.adapt_population_flag = adapt_population_flag
|
||||
|
||||
# Bounds on probabilities when adapting
|
||||
self.max_selection_probability = max_selection_probability
|
||||
self.min_selection_probability = min_selection_probability
|
||||
self.max_chromosome_mutation_rate = max_chromosome_mutation_rate
|
||||
self.min_chromosome_mutation_rate = min_chromosome_mutation_rate
|
||||
self.max_gene_mutation_rate = max_gene_mutation_rate
|
||||
self.min_gene_mutation_rate = min_gene_mutation_rate
|
||||
|
||||
# Database varibles
|
||||
self.database = Database()
|
||||
self.database_name = database_name
|
||||
self.sql_create_data_structure = sql_create_data_structure
|
||||
|
||||
# Graphing variables
|
||||
self.graph = Graph(self.database)
|
||||
|
||||
# Any other custom kwargs?
|
||||
for name, value in kwargs.items():
|
||||
self.__setattr__(name, value)
|
||||
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
"""Custom setter for using
|
||||
|
||||
self.name = value
|
||||
|
||||
which follows the following guidelines:
|
||||
- if self.name is a property, the specific property setter is used
|
||||
- else if value is callable and the first parameter is either 'self' or 'ga', self is passed in as the first parameter
|
||||
- else if value is not None or self.name is not set, assign it like normal
|
||||
"""
|
||||
|
||||
# Check for property
|
||||
if hasattr(type(self), name) and isinstance(getattr(type(self), name), property):
|
||||
getattr(type(self), name).fset(self, value)
|
||||
|
||||
# Check for function
|
||||
elif callable(value) and next(iter(signature(value).parameters), None) in ('self', 'ga'):
|
||||
foo = lambda *args, **kwargs: value(self, *args, **kwargs)
|
||||
# Reassign name and doc-string for documentation
|
||||
foo.__name__ = value.__name__
|
||||
foo.__doc__ = value.__doc__
|
||||
self.__dict__[name] = foo
|
||||
|
||||
# Assign like normal unless None or undefined self.name
|
||||
elif value is not None or not hasattr(self, name):
|
||||
self.__dict__[name] = value
|
||||
|
||||
|
||||
#============================#
|
||||
# Built-in database methods: #
|
||||
#============================#
|
||||
|
||||
|
||||
def save_population(self):
|
||||
"""Saves the current population to the database."""
|
||||
self.database.insert_current_population(self)
|
||||
|
||||
|
||||
def save_chromosome(self, chromosome):
|
||||
"""Saves the given chromosome to the database."""
|
||||
self.database.insert_current_chromosome(self.current_generation, chromosome)
|
||||
|
||||
|
||||
#===================#
|
||||
# Built-in options: #
|
||||
#===================#
|
||||
|
||||
|
||||
def numeric_chromosomes(self):
|
||||
"""Sets default numerical based methods"""
|
||||
|
||||
# Adapt every 10th generation
|
||||
self.adapt_rate = 0.10
|
||||
|
||||
# Use averaging for crossover
|
||||
self.crossover_individual_impl = Crossover.Individual.Arithmetic.average
|
||||
|
||||
# Use averaging for mutation
|
||||
self.mutation_individual_impl = Mutation.Individual.individual_genes
|
||||
|
||||
# Euclidean norm
|
||||
self.dist = lambda self, chromosome_1, chromosome_2:\
|
||||
math.sqrt(sum(
|
||||
(gene_1.value - gene_2.value) ** 2
|
||||
for gene_1, gene_2
|
||||
in zip(chromosome_1, chromosome_2)
|
||||
))
|
||||
|
||||
|
||||
def permutation_chromosomes(self, cycle = True):
|
||||
"""Sets default permutation based methods"""
|
||||
|
||||
cycle = int(cycle)
|
||||
|
||||
self.crossover_individual_impl = Crossover.Individual.Permutation.ox1
|
||||
self.mutation_individual_impl = Mutation.Individual.Permutation.swap_genes
|
||||
|
||||
def dist(self, chromosome_1, chromosome_2):
|
||||
"""Count the number of gene pairs they don't have in common."""
|
||||
|
||||
return sum(
|
||||
1
|
||||
for x, y
|
||||
in zip(chromosome_1, chromosome_2)
|
||||
if x != y
|
||||
)
|
||||
|
||||
self.dist = dist
|
||||
|
||||
|
||||
#===========================#
|
||||
# Getter/setter properties: #
|
||||
#===========================#
|
||||
|
||||
|
||||
@property
|
||||
def run(self):
|
||||
"""Getter function for the run counter."""
|
||||
return self._run
|
||||
|
||||
|
||||
@run.setter
|
||||
def run(self, value):
|
||||
"""Setter function for the run counter."""
|
||||
if not(isinstance(value, int) and value >= 0):
|
||||
raise ValueError("ga.run counter must be an integer greater than or equal to 0.")
|
||||
self._run = value
|
||||
|
||||
|
||||
@property
|
||||
def current_generation(self):
|
||||
"""Getter function for the current generation."""
|
||||
return self._current_generation
|
||||
|
||||
|
||||
@current_generation.setter
|
||||
def current_generation(self, generation):
|
||||
"""Setter function for the current generation."""
|
||||
|
||||
if not isinstance(generation, int) or generation < 0:
|
||||
raise ValueError("ga.current_generation must be an integer greater than or equal to 0")
|
||||
|
||||
self._current_generation = generation
|
||||
|
||||
|
||||
@property
|
||||
def chromosome_length(self):
|
||||
"""Getter function for chromosome length"""
|
||||
return self._chromosome_length
|
||||
|
||||
|
||||
@chromosome_length.setter
|
||||
def chromosome_length(self, length):
|
||||
"""Setter function with error checking for chromosome length"""
|
||||
|
||||
if(not isinstance(length, int) or length <= 0):
|
||||
raise ValueError("Chromosome length must be integer greater than 0")
|
||||
|
||||
self._chromosome_length = length
|
||||
|
||||
|
||||
@property
|
||||
def population_size(self):
|
||||
"""Getter function for population size"""
|
||||
|
||||
return self._population_size
|
||||
|
||||
|
||||
@population_size.setter
|
||||
def population_size(self, size):
|
||||
"""Setter function with error checking for population size"""
|
||||
|
||||
if(not isinstance(size, int) or size <= 0):
|
||||
raise ValueError("Population size must be integer greater than 0")
|
||||
|
||||
self._population_size = size
|
||||
|
||||
|
||||
@property
|
||||
def target_fitness_type(self):
|
||||
"""Getter function for target fitness type."""
|
||||
|
||||
return self._target_fitness_type
|
||||
|
||||
|
||||
@target_fitness_type.setter
|
||||
def target_fitness_type(self, target_fitness_type):
|
||||
"""Setter function for target fitness type."""
|
||||
|
||||
self._target_fitness_type = target_fitness_type
|
||||
|
||||
|
||||
@property
|
||||
def max_chromosome_mutation_rate(self):
|
||||
"""Getter function for max chromosome mutation rate"""
|
||||
|
||||
return self._max_chromosome_mutation_rate
|
||||
|
||||
|
||||
@max_chromosome_mutation_rate.setter
|
||||
def max_chromosome_mutation_rate(self, rate):
|
||||
"""Setter function with error checking and default value for max chromosome mutation rate"""
|
||||
|
||||
# Default value
|
||||
if rate is None:
|
||||
self._max_chromosome_mutation_rate = min(self.chromosome_mutation_rate*2, (1+self.chromosome_mutation_rate)/2)
|
||||
|
||||
# Otherwise check value
|
||||
elif 0 <= rate <= 1:
|
||||
self._max_chromosome_mutation_rate = rate
|
||||
|
||||
# Throw error
|
||||
else:
|
||||
raise ValueError("Max chromosome mutation rate must be between 0 and 1")
|
||||
|
||||
|
||||
@property
|
||||
def min_chromosome_mutation_rate(self):
|
||||
"""Getter function for min chromosome mutation rate"""
|
||||
|
||||
return self._min_chromosome_mutation_rate
|
||||
|
||||
|
||||
@min_chromosome_mutation_rate.setter
|
||||
def min_chromosome_mutation_rate(self, rate):
|
||||
"""Setter function with error checking and default value for min chromosome mutation rate"""
|
||||
|
||||
# Default value
|
||||
if rate is None:
|
||||
self._min_chromosome_mutation_rate = self.chromosome_mutation_rate/2
|
||||
|
||||
# Otherwise check value
|
||||
elif 0 <= rate <= 1:
|
||||
self._min_chromosome_mutation_rate = rate
|
||||
|
||||
# Throw error
|
||||
else:
|
||||
raise ValueError("Min chromosome mutation rate must be between 0 and 1")
|
||||
|
||||
|
||||
@property
|
||||
def database_name(self):
|
||||
"""Getter function for the database name"""
|
||||
|
||||
return self._database_name
|
||||
|
||||
|
||||
@database_name.setter
|
||||
def database_name(self, value_input):
|
||||
"""Setter function with error checking for the database name"""
|
||||
|
||||
# Update the database class of the name change
|
||||
self.database._database_name = value_input
|
||||
|
||||
# Set the name in the ga attribute
|
||||
self._database_name = value_input
|
||||
163
EasyGA/crossover/Crossover.py
Normal file
163
EasyGA/crossover/Crossover.py
Normal file
@ -0,0 +1,163 @@
|
||||
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())
|
||||
|
||||
|
||||
class Population:
|
||||
"""Methods for selecting chromosomes to crossover."""
|
||||
|
||||
|
||||
def sequential(ga):
|
||||
"""Select sequential pairs from the mating pool.
|
||||
Every parent is paired with the previous parent.
|
||||
The first parent is paired with the last parent.
|
||||
"""
|
||||
|
||||
mating_pool = ga.population.mating_pool
|
||||
|
||||
for index in range(len(mating_pool)): # for each parent in the mating pool
|
||||
ga.crossover_individual_impl( # apply crossover to
|
||||
mating_pool[index], # the parent and
|
||||
mating_pool[index-1] # the previous parent
|
||||
)
|
||||
|
||||
|
||||
def random(ga):
|
||||
"""Select random pairs from the mating pool.
|
||||
Every parent is paired with a random parent.
|
||||
"""
|
||||
|
||||
mating_pool = ga.population.mating_pool
|
||||
|
||||
for parent in mating_pool: # for each parent in the mating pool
|
||||
ga.crossover_individual_impl( # apply crossover to
|
||||
parent, # the parent and
|
||||
random.choice(mating_pool) # a random parent
|
||||
)
|
||||
|
||||
|
||||
class Individual:
|
||||
"""Methods for crossing parents."""
|
||||
|
||||
|
||||
@_check_weight
|
||||
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))
|
||||
|
||||
# Weighted random integer from 0 to minimum parent length - 1
|
||||
swap_index = int(ga.weighted_random(weight) * minimum_parent_length)
|
||||
|
||||
ga.population.add_child(parent_1[:swap_index] + parent_2[swap_index:])
|
||||
ga.population.add_child(parent_2[:swap_index] + parent_1[swap_index:])
|
||||
|
||||
|
||||
@_check_weight
|
||||
def multi_point(ga, parent_1, parent_2, *, weight = 0.5):
|
||||
"""Cross two parents by swapping genes at multiple points."""
|
||||
pass
|
||||
|
||||
|
||||
@_check_weight
|
||||
@_gene_by_gene
|
||||
def uniform(ga, value_1, value_2, *, weight = 0.5):
|
||||
"""Cross two parents by swapping all genes randomly."""
|
||||
return random.choices(gene_pair, cum_weights = [weight, 1])[0]
|
||||
|
||||
|
||||
class Arithmetic:
|
||||
"""Crossover methods for numerical genes."""
|
||||
|
||||
@_gene_by_gene
|
||||
def average(ga, value_1, value_2, *, weight = 0.5):
|
||||
"""Cross two parents by taking the average of the genes."""
|
||||
|
||||
average_value = weight*value_1 + (1-weight)*value_2
|
||||
|
||||
if type(value_1) == type(value_2) == int:
|
||||
average_value = randround(value)
|
||||
|
||||
return average_value
|
||||
|
||||
|
||||
@_gene_by_gene
|
||||
def extrapolate(ga, value_1, value_2, *, weight = 0.5):
|
||||
"""Cross two parents by extrapolating towards the first parent.
|
||||
May result in gene values outside the expected domain.
|
||||
"""
|
||||
|
||||
extrapolated_value = weight*value_1 + (1-weight)*value_2
|
||||
|
||||
if type(value_1) == type(value_2) == int:
|
||||
extrapolated_value = randround(value)
|
||||
|
||||
return extrapolated_value
|
||||
|
||||
|
||||
@_check_weight
|
||||
@_gene_by_gene
|
||||
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)
|
||||
|
||||
if type(value_1) == type(value_2) == int:
|
||||
value = randround(value)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
class Permutation:
|
||||
"""Crossover methods for permutation based chromosomes."""
|
||||
|
||||
@_check_weight
|
||||
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.
|
||||
"""
|
||||
|
||||
# Too small to cross
|
||||
if len(parent_1) < 2:
|
||||
return parent_1.gene_list
|
||||
|
||||
# Unequal parent lengths
|
||||
if len(parent_1) != len(parent_2):
|
||||
raise ValueError("Parents do not have the same lengths.")
|
||||
|
||||
# Swap with weighted probability so that most of the genes
|
||||
# are taken directly from parent 1.
|
||||
if random.choices([0, 1], cum_weights = [weight, 1]) == 1:
|
||||
parent_1, parent_2 = parent_2, parent_1
|
||||
|
||||
# Extract genes from parent 1 between two random indexes
|
||||
index_2 = random.randrange(1, len(parent_1))
|
||||
index_1 = random.randrange(index_2)
|
||||
|
||||
# Create copies of the gene lists
|
||||
gene_list_1 = [None]*index_1 + parent_1[index_1:index_2] + [None]*(len(parent_1)-index_2)
|
||||
gene_list_2 = list(parent_2)
|
||||
|
||||
input_index = 0
|
||||
|
||||
# For each gene from the second parent
|
||||
for _ in range(len(gene_list_2)):
|
||||
|
||||
# Remove it if it is already used
|
||||
if gene_list_2[-1] in gene_list_1:
|
||||
gene_list_2.pop(-1)
|
||||
|
||||
# Add it if it has not been used
|
||||
else:
|
||||
if input_index == index_1:
|
||||
input_index = index_2
|
||||
gene_list_1[input_index] = gene_list_2.pop(-1)
|
||||
input_index += 1
|
||||
|
||||
ga.population.add_child(gene_list_1)
|
||||
|
||||
0
EasyGA/crossover/__init__.py
Normal file
0
EasyGA/crossover/__init__.py
Normal file
0
EasyGA/crossover/test_crossover_methods.py
Normal file
0
EasyGA/crossover/test_crossover_methods.py
Normal file
0
EasyGA/database/__init__.py
Normal file
0
EasyGA/database/__init__.py
Normal file
96
EasyGA/database/matplotlib_graph.py
Normal file
96
EasyGA/database/matplotlib_graph.py
Normal file
@ -0,0 +1,96 @@
|
||||
# Graphing package
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
class Matplotlib_Graph:
|
||||
"""Prebuilt graphing functions to make visual
|
||||
represention of fitness data."""
|
||||
|
||||
# Common graphing functions
|
||||
type_of_graph_dict = {
|
||||
'line' : plt.plot,
|
||||
'scatter' : plt.scatter,
|
||||
'bar' : plt.bar
|
||||
}
|
||||
|
||||
def __init__(self, database):
|
||||
self.database = database
|
||||
self.type_of_graph = 'line'
|
||||
self.x = None
|
||||
self.y = None
|
||||
self.yscale = "linear"
|
||||
|
||||
|
||||
def generation_total_fitness(self, config_id = None):
|
||||
"""Show a plot of generation by generation total fitness."""
|
||||
|
||||
# Query the X data
|
||||
generations = self.database.get_total_generations(config_id)
|
||||
|
||||
# Create the generations list - [0,1,2,etc]
|
||||
self.x = list(range(generations))
|
||||
|
||||
# Query for Y data
|
||||
self.y = self.database.get_generation_total_fitness(config_id)
|
||||
|
||||
self.type_of_graph(self.x, self.y)
|
||||
plt.yscale(self.yscale)
|
||||
plt.xlabel('Generation')
|
||||
plt.ylabel('Generation Total Fitness')
|
||||
plt.title('Relationship Between Generations and Generation Total Fitness')
|
||||
|
||||
|
||||
def highest_value_chromosome(self,config_id = None):
|
||||
"""Generation by Max value chromosome """
|
||||
|
||||
# Query the X data
|
||||
generations = self.database.get_total_generations(config_id)
|
||||
|
||||
# Create the generations list - [0,1,2,etc]
|
||||
self.x = list(range(generations))
|
||||
|
||||
# Query for Y data
|
||||
self.y = self.database.get_highest_chromosome(config_id)
|
||||
|
||||
self.type_of_graph(self.x, self.y)
|
||||
plt.yscale(self.yscale)
|
||||
plt.xlabel('Generation')
|
||||
plt.ylabel('Highest Fitness')
|
||||
plt.title('Relationship Between Generations and Highest Fitness')
|
||||
|
||||
|
||||
def lowest_value_chromosome(self,config_id = None):
|
||||
"""Generation by Min value Chromosome """
|
||||
|
||||
# Query the X data
|
||||
generations = self.database.get_total_generations(config_id)
|
||||
|
||||
# Create the generations list - [0,1,2,etc]
|
||||
self.x = list(range(generations))
|
||||
|
||||
# Query for Y data
|
||||
self.y = self.database.get_lowest_chromosome(config_id)
|
||||
|
||||
self.type_of_graph(self.x, self.y)
|
||||
plt.yscale(self.yscale)
|
||||
plt.xlabel('Generation')
|
||||
plt.ylabel('Lowest Fitness')
|
||||
plt.title('Relationship Between Generations and Lowest Fitness')
|
||||
|
||||
|
||||
def show(self):
|
||||
"""Used to show the matplot lib graph."""
|
||||
plt.show()
|
||||
|
||||
|
||||
# Getter and setters
|
||||
@property
|
||||
def type_of_graph(self):
|
||||
return self._type_of_graph
|
||||
|
||||
|
||||
@type_of_graph.setter
|
||||
def type_of_graph(self, value_input):
|
||||
if value_input in self.type_of_graph_dict.keys():
|
||||
self._type_of_graph = self.type_of_graph_dict[value_input]
|
||||
else:
|
||||
self._type_of_graph = value_input
|
||||
373
EasyGA/database/sql_database.py
Normal file
373
EasyGA/database/sql_database.py
Normal file
@ -0,0 +1,373 @@
|
||||
import sqlite3
|
||||
import os
|
||||
|
||||
from tabulate import tabulate
|
||||
|
||||
class SQL_Database:
|
||||
"""Main database class that controls all the functionality for input /
|
||||
out of the database using SQLite3."""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self.conn = None
|
||||
self.config_id = None
|
||||
self._database_name = 'database.db'
|
||||
self.config_structure = f"""
|
||||
CREATE TABLE IF NOT EXISTS config (
|
||||
config_id INTEGER,
|
||||
attribute_name TEXT,
|
||||
attribute_value TEXT)"""
|
||||
|
||||
|
||||
#=====================================#
|
||||
# Create Config and Data Table: #
|
||||
#=====================================#
|
||||
|
||||
def create_all_tables(self, ga):
|
||||
"""Create the database if it doenst exist and then the data and config
|
||||
tables."""
|
||||
|
||||
# Create the database connection
|
||||
self.create_connection()
|
||||
|
||||
if self.conn is not None:
|
||||
# Create data table
|
||||
self.create_table(ga.sql_create_data_structure)
|
||||
# Creare config table
|
||||
self.create_table(self.config_structure)
|
||||
# Set the config id
|
||||
self.config_id = self.get_current_config()
|
||||
|
||||
else:
|
||||
raise Exception("Error! Cannot create the database connection.")
|
||||
|
||||
|
||||
|
||||
def insert_config(self,ga):
|
||||
"""Insert the configuration attributes into the config."""
|
||||
|
||||
# Get the current config and add one for the new config key
|
||||
self.config_id = self.get_current_config()
|
||||
|
||||
# Setting the config_id index if there is no file
|
||||
if self.config_id == None:
|
||||
self.config_id = 0
|
||||
else:
|
||||
self.config_id = self.config_id + 1
|
||||
|
||||
# Getting all the attributes from the attributes class
|
||||
db_config_dict = (
|
||||
(attr_name, getattr(ga, attr_name))
|
||||
for attr_name
|
||||
in dir(ga)
|
||||
if attr_name[0] != '_'
|
||||
if attr_name != 'population'
|
||||
)
|
||||
|
||||
# Types supported in the database
|
||||
sql_type_list = [int, float, str]
|
||||
|
||||
# Loop through all attributes
|
||||
for name, value in db_config_dict:
|
||||
|
||||
# Inserting a function, do special stuff
|
||||
if callable(value):
|
||||
value = ""
|
||||
|
||||
# Not a function
|
||||
else:
|
||||
# Convert to the right type
|
||||
if type(value) not in sql_type_list:
|
||||
|
||||
value = str(value)
|
||||
|
||||
# Insert into database
|
||||
self.conn.execute(f"""
|
||||
INSERT INTO config(config_id,attribute_name, attribute_value)
|
||||
VALUES ('{self.config_id}', '{name}','{value}');""")
|
||||
|
||||
|
||||
self.config_id = self.get_current_config()
|
||||
|
||||
|
||||
#=====================================#
|
||||
# Decorators: #
|
||||
#=====================================#
|
||||
|
||||
def default_config_id(method):
|
||||
"""Decorator used to set the default config_id inside other functions."""
|
||||
|
||||
def new_method(self, config_id = None):
|
||||
|
||||
input_id = self.config_id if config_id is None else config_id
|
||||
|
||||
return method(self, input_id)
|
||||
|
||||
return new_method
|
||||
|
||||
|
||||
def format_query_data(method):
|
||||
"""Decorator used to format query data"""
|
||||
|
||||
def new_method(self, config_id):
|
||||
query = method(self, config_id)
|
||||
|
||||
# Unpack elements if they are lists with only 1 element
|
||||
if type(query[0]) in (list, tuple) and len(query[0]) == 1:
|
||||
query = [i[0] for i in query]
|
||||
|
||||
# Unpack list if it is a list with only 1 element
|
||||
if type(query) in (list, tuple) and len(query) == 1:
|
||||
query = query[0]
|
||||
|
||||
return query
|
||||
|
||||
return new_method
|
||||
|
||||
#=====================================#
|
||||
# Request information Queries: #
|
||||
#=====================================#
|
||||
|
||||
|
||||
def get_current_config(self):
|
||||
"""Get the current config_id from the config table."""
|
||||
return self.query_one_item("SELECT MAX(config_id) FROM config")
|
||||
|
||||
def past_runs(self):
|
||||
"""Show a summerization of the past runs that the user has done."""
|
||||
|
||||
query_data = self.query_all(f"""
|
||||
SELECT config_id,attribute_name,attribute_value
|
||||
FROM config;""")
|
||||
|
||||
print(
|
||||
tabulate(
|
||||
query_data,
|
||||
headers = [
|
||||
'config_id',
|
||||
'attribute_name',
|
||||
'attribute_value'
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@default_config_id
|
||||
def get_generation_total_fitness(self, config_id):
|
||||
"""Get each generations total fitness sum from the database """
|
||||
|
||||
return self.query_all(f"""
|
||||
SELECT SUM(fitness)
|
||||
FROM data
|
||||
WHERE config_id={config_id}
|
||||
GROUP BY generation;""")
|
||||
|
||||
|
||||
@default_config_id
|
||||
def get_total_generations(self, config_id):
|
||||
"""Get the total generations from the database"""
|
||||
|
||||
return self.query_one_item(f"""
|
||||
SELECT COUNT(DISTINCT generation)
|
||||
FROM data
|
||||
WHERE config_id={config_id};""")
|
||||
|
||||
|
||||
@default_config_id
|
||||
def get_highest_chromosome(self, config_id):
|
||||
"""Get the highest fitness of each generation"""
|
||||
|
||||
return self.query_all(f"""
|
||||
SELECT fitness, max(fitness)
|
||||
FROM data
|
||||
WHERE config_id={config_id}
|
||||
GROUP by generation;""")
|
||||
|
||||
|
||||
@default_config_id
|
||||
def get_lowest_chromosome(self, config_id):
|
||||
"""Get the lowest fitness of each generation"""
|
||||
|
||||
return self.query_all(f"""
|
||||
SELECT fitness, min(fitness)
|
||||
FROM data
|
||||
WHERE config_id={config_id}
|
||||
GROUP by generation;""")
|
||||
|
||||
|
||||
#=====================================#
|
||||
# Input information Queries: #
|
||||
#=====================================#
|
||||
|
||||
|
||||
|
||||
def insert_chromosome(self, generation, chromosome):
|
||||
""" Insert one chromosome into the database"""
|
||||
|
||||
# Structure the insert data
|
||||
db_chromosome = (
|
||||
self.config_id,
|
||||
generation,
|
||||
chromosome.fitness,
|
||||
repr(chromosome)
|
||||
)
|
||||
|
||||
# Create sql query structure
|
||||
sql = """INSERT INTO data(config_id, generation, fitness, chromosome)
|
||||
VALUES(?,?,?,?)"""
|
||||
|
||||
cur = self.conn.cursor()
|
||||
cur.execute(sql, db_chromosome)
|
||||
self.conn.commit()
|
||||
|
||||
|
||||
|
||||
def insert_current_population(self, ga):
|
||||
""" Insert current generations population """
|
||||
|
||||
# Structure the insert data
|
||||
db_chromosome_list = [
|
||||
(
|
||||
self.config_id,
|
||||
ga.current_generation,
|
||||
chromosome.fitness,
|
||||
repr(chromosome)
|
||||
)
|
||||
for chromosome
|
||||
in ga.population
|
||||
]
|
||||
|
||||
# Create sql query structure
|
||||
sql = """INSERT INTO data(config_id, generation, fitness, chromosome)
|
||||
VALUES(?,?,?,?)"""
|
||||
|
||||
cur = self.conn.cursor()
|
||||
cur.executemany(sql, db_chromosome_list)
|
||||
self.conn.commit()
|
||||
|
||||
|
||||
|
||||
#=====================================#
|
||||
# Functions: #
|
||||
#=====================================#
|
||||
|
||||
def create_connection(self):
|
||||
"""Create a database connection to the SQLite database
|
||||
specified by db_file."""
|
||||
|
||||
try:
|
||||
self.conn = sqlite3.connect(self.database_name)
|
||||
except Error as e:
|
||||
self.conn = None
|
||||
print(e)
|
||||
|
||||
def create_table(self, create_table_sql):
|
||||
"""Create a table from the create_table_sql statement."""
|
||||
|
||||
try:
|
||||
c = self.conn.cursor()
|
||||
c.execute(create_table_sql)
|
||||
except Error as e:
|
||||
print(e)
|
||||
|
||||
|
||||
@format_query_data
|
||||
def query_all(self, query):
|
||||
"""Query for muliple rows of data"""
|
||||
|
||||
cur = self.conn.cursor()
|
||||
cur.execute(query)
|
||||
return cur.fetchall()
|
||||
|
||||
|
||||
@format_query_data
|
||||
def query_one_item(self, query):
|
||||
"""Query for single data point"""
|
||||
|
||||
cur = self.conn.cursor()
|
||||
cur.execute(query)
|
||||
return cur.fetchone()
|
||||
|
||||
|
||||
def remove_database(self):
|
||||
"""Remove the current database file using the database_name attribute."""
|
||||
os.remove(self._database_name)
|
||||
|
||||
|
||||
def get_var_names(self, ga):
|
||||
"""Returns a list of the names of attributes of the ga."""
|
||||
|
||||
# Loop through all attributes
|
||||
for var in ga.__dict__.keys():
|
||||
|
||||
# Remove leading underscore
|
||||
yield (var[1:] if (var[0] == '_') else var)
|
||||
|
||||
|
||||
#=====================================#
|
||||
# Setters and Getters: #
|
||||
#=====================================#
|
||||
|
||||
|
||||
@property
|
||||
def database_name(self):
|
||||
return self._database_name
|
||||
|
||||
|
||||
@database_name.setter
|
||||
def database_name(self, value_input):
|
||||
raise Exception("Invalid usage, please use ga.database_name instead.")
|
||||
|
||||
|
||||
@property
|
||||
def conn(self):
|
||||
"""Getter function for conn"""
|
||||
|
||||
# Return if the connection has already been set
|
||||
if self._conn is not None:
|
||||
return self._conn
|
||||
|
||||
# If the connection has not been set yet
|
||||
try:
|
||||
# Check if you can connect to the database
|
||||
self.create_connection()
|
||||
return self._conn
|
||||
|
||||
# If the connection doesnt exist then print error
|
||||
except:
|
||||
raise Exception("""You are required to run a ga before you can connect to the database. Run ga.evolve() or ga.active()""")
|
||||
|
||||
|
||||
@conn.setter
|
||||
def conn(self, value_input):
|
||||
"""Setter function for conn"""
|
||||
|
||||
# Set the name in the ga attribute
|
||||
self._conn = value_input
|
||||
|
||||
|
||||
@property
|
||||
def config_id(self):
|
||||
"""Getter function for config_id"""
|
||||
|
||||
# Return if the config_id has already been set
|
||||
if self._config_id is not None:
|
||||
return self._config_id
|
||||
|
||||
# If the config_id has not been set yet
|
||||
try:
|
||||
# Check if you can connect to the database
|
||||
self._config_id = self.get_current_config()
|
||||
return self._config_id
|
||||
|
||||
# If the config_id doesnt exist then print error
|
||||
except:
|
||||
raise Exception("""You are required to run a ga before you can connect to the database. Run ga.evolve() or ga.active()""")
|
||||
|
||||
|
||||
@config_id.setter
|
||||
def config_id(self, value_input):
|
||||
"""Setter function for config_id"""
|
||||
|
||||
# Set the name in the ga attribute
|
||||
self._config_id = value_input
|
||||
261
EasyGA/decorators.py
Normal file
261
EasyGA/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
|
||||
|
||||
46
EasyGA/examples/Fitness.py
Normal file
46
EasyGA/examples/Fitness.py
Normal file
@ -0,0 +1,46 @@
|
||||
def is_it_5(self, chromosome):
|
||||
"""A very simple case test function - If the chromosome's gene value
|
||||
is equal to 5 add one to the chromosomes overall fitness value.
|
||||
"""
|
||||
|
||||
# Overall fitness value
|
||||
fitness = 0
|
||||
|
||||
for gene in chromosome:
|
||||
|
||||
# Increment fitness is the gene's value is 5
|
||||
if gene.value == 5:
|
||||
fitness += 1
|
||||
|
||||
return fitness
|
||||
|
||||
|
||||
def near_5(self, chromosome):
|
||||
"""Test's the GA's ability to handle floats. Computes how close each gene is to 5."""
|
||||
|
||||
# Overall fitness value
|
||||
fitness = 0
|
||||
|
||||
for gene in chromosome:
|
||||
|
||||
# Add squared distance to 5
|
||||
fitness += (5 - gene.value) ** 2
|
||||
|
||||
return fitness
|
||||
|
||||
|
||||
def index_dependent_values(self, chromosome):
|
||||
"""Test of the GA's ability to improve fitness when the value is index-dependent.
|
||||
If a gene is equal to its index in the chromosome + 1, fitness is incremented.
|
||||
"""
|
||||
|
||||
# Overall fitness value
|
||||
fitness = 0
|
||||
|
||||
for i, gene in enumerate(chromosome):
|
||||
|
||||
# Increment fitness is the gene's value is i+1
|
||||
if gene.value == i+1:
|
||||
fitness += 1
|
||||
|
||||
return fitness
|
||||
0
EasyGA/examples/__init__.py
Normal file
0
EasyGA/examples/__init__.py
Normal file
1
EasyGA/examples/test_Fitness.py
Normal file
1
EasyGA/examples/test_Fitness.py
Normal file
@ -0,0 +1 @@
|
||||
|
||||
144
EasyGA/mutation/Mutation.py
Normal file
144
EasyGA/mutation/Mutation.py
Normal file
@ -0,0 +1,144 @@
|
||||
import random
|
||||
from math import ceil
|
||||
|
||||
# Import all mutation decorators
|
||||
from decorators import _check_chromosome_mutation_rate, _check_gene_mutation_rate, _reset_fitness, _loop_random_mutations
|
||||
|
||||
|
||||
class Population:
|
||||
"""Methods for selecting chromosomes to mutate"""
|
||||
|
||||
@_check_chromosome_mutation_rate
|
||||
def random_selection(ga):
|
||||
"""Selects random chromosomes."""
|
||||
|
||||
sample_space = range(len(ga.population))
|
||||
sample_size = ceil(len(ga.population)*ga.chromosome_mutation_rate)
|
||||
|
||||
# Loop the individual method until enough genes are mutated.
|
||||
for index in random.sample(sample_space, sample_size):
|
||||
ga.mutation_individual_impl(ga.population[index])
|
||||
|
||||
|
||||
@_check_chromosome_mutation_rate
|
||||
def random_avoid_best(ga):
|
||||
"""Selects random chromosomes while avoiding the best chromosomes. (Elitism)"""
|
||||
|
||||
sample_space = range(ceil(ga.percent_converged*len(ga.population)*3/16), len(ga.population))
|
||||
sample_size = ceil(ga.chromosome_mutation_rate*len(ga.population))
|
||||
|
||||
for index in random.sample(sample_space, sample_size):
|
||||
ga.mutation_individual_impl(ga.population[index])
|
||||
|
||||
|
||||
@_check_chromosome_mutation_rate
|
||||
def best_replace_worst(ga):
|
||||
"""Selects the best chromosomes, copies them, and replaces the worst chromosomes."""
|
||||
|
||||
mutation_amount = ceil(ga.chromosome_mutation_rate*len(ga.population))
|
||||
|
||||
for i in range(mutation_amount):
|
||||
ga.population[-i-1] = ga.make_chromosome(ga.population[i])
|
||||
ga.mutation_individual_impl(ga.population[-i-1])
|
||||
|
||||
|
||||
class Individual:
|
||||
"""Methods for mutating a single chromosome."""
|
||||
|
||||
@_check_gene_mutation_rate
|
||||
@_reset_fitness
|
||||
@_loop_random_mutations
|
||||
def individual_genes(ga, chromosome, index):
|
||||
"""Mutates random genes by making completely new genes."""
|
||||
|
||||
# Using the chromosome_impl
|
||||
if ga.chromosome_impl is not None:
|
||||
chromosome[index] = ga.make_gene(ga.chromosome_impl()[index])
|
||||
|
||||
# Using the gene_impl
|
||||
elif ga.gene_impl is not None:
|
||||
chromosome[index] = ga.make_gene(ga.gene_impl())
|
||||
|
||||
# Exit because no gene creation method specified
|
||||
else:
|
||||
raise Exception("Did not specify any initialization constraints.")
|
||||
|
||||
|
||||
class Arithmetic:
|
||||
"""Methods for mutating a chromosome by numerically modifying the genes."""
|
||||
|
||||
@_check_gene_mutation_rate
|
||||
@_reset_fitness
|
||||
@_loop_random_mutations
|
||||
def average(ga, chromosome, index):
|
||||
"""Mutates random genes by making completely new genes
|
||||
and then averaging them with the old genes. May cause
|
||||
premature convergence. Weight is the reciprocal of the
|
||||
number of generations run."""
|
||||
|
||||
weight = 1/max(1, ga.current_generation)
|
||||
|
||||
# Using the chromosome_impl
|
||||
if ga.chromosome_impl is not None:
|
||||
new_value = ga.chromosome_impl()[index]
|
||||
|
||||
# Using the gene_impl
|
||||
elif ga.gene_impl is not None:
|
||||
new_value = ga.gene_impl()
|
||||
|
||||
# Exit because no gene creation method specified
|
||||
else:
|
||||
raise Exception("Did not specify any initialization constraints.")
|
||||
|
||||
chromosome[index] = ga.make_gene((1-weight)*chromosome[index].value + weight*new_value)
|
||||
|
||||
|
||||
@_check_gene_mutation_rate
|
||||
@_reset_fitness
|
||||
@_loop_random_mutations
|
||||
def reflect_genes(ga, chromosome, index):
|
||||
"""Reflects genes against the best chromosome.
|
||||
Requires large genetic variety to work well but
|
||||
when it does it may be very fast."""
|
||||
|
||||
difference = ga.population[0][index].value - chromosome[index].value
|
||||
value = ga.population[0][index].value + 2*difference
|
||||
chromosome[index] = ga.make_gene(value)
|
||||
|
||||
|
||||
class Permutation:
|
||||
"""Methods for mutating a chromosome by changing the order of the genes."""
|
||||
|
||||
@_check_gene_mutation_rate
|
||||
@_reset_fitness
|
||||
@_loop_random_mutations
|
||||
def swap_genes(ga, chromosome, index):
|
||||
"""Swaps two random genes in the chromosome."""
|
||||
|
||||
# Indexes of genes to swap
|
||||
index_one = index
|
||||
index_two = random.randrange(len(chromosome))
|
||||
|
||||
# Swap genes
|
||||
chromosome[index_one], chromosome[index_two] = chromosome[index_two], chromosome[index_one]
|
||||
|
||||
|
||||
@_check_gene_mutation_rate
|
||||
@_reset_fitness
|
||||
def swap_segments(ga, chromosome):
|
||||
"""Splits the chromosome into 3 segments and shuffle them."""
|
||||
|
||||
# Chromosome too short to mutate
|
||||
if len(chromosome) < 3:
|
||||
return
|
||||
|
||||
# Indexes to split the chromosome
|
||||
index_two = random.randrange(2, len(chromosome))
|
||||
index_one = random.randrange(1, index_two)
|
||||
|
||||
# Extract segments and shuffle them
|
||||
segments = [chromosome[:index_one], chromosome[index_one:index_two], chromosome[index_two:]]
|
||||
random.shuffle(segments)
|
||||
|
||||
# Put segments back together
|
||||
chromosome.gene_list = segments[0] + segments[1] + segments[2]
|
||||
0
EasyGA/mutation/__init__.py
Normal file
0
EasyGA/mutation/__init__.py
Normal file
0
EasyGA/mutation/test_mutation_methods.py
Normal file
0
EasyGA/mutation/test_mutation_methods.py
Normal file
196
EasyGA/parent/Parent.py
Normal file
196
EasyGA/parent/Parent.py
Normal file
@ -0,0 +1,196 @@
|
||||
import random
|
||||
|
||||
# Import all parent decorators
|
||||
from decorators import _check_selection_probability, _check_positive_fitness, _ensure_sorted, _compute_parent_amount
|
||||
|
||||
|
||||
class Rank:
|
||||
"""Methods for selecting parents based on their rankings in the population
|
||||
i.e. the n-th best chromosome has a fixed probability of being selected,
|
||||
regardless of their chances"""
|
||||
|
||||
@_check_selection_probability
|
||||
@_ensure_sorted
|
||||
@_compute_parent_amount
|
||||
def tournament(ga, parent_amount):
|
||||
"""
|
||||
Will make tournaments of size tournament_size and choose the winner (best fitness)
|
||||
from the tournament and use it as a parent for the next generation. The total number
|
||||
of parents selected is determined by parent_ratio, an attribute to the GA object.
|
||||
May require many loops if the selection probability is very low.
|
||||
"""
|
||||
|
||||
# Choose the tournament size.
|
||||
# Use no less than 5 chromosomes per tournament.
|
||||
tournament_size = int(len(ga.population)*ga.tournament_size_ratio)
|
||||
if tournament_size < 5:
|
||||
tournament_size = min(5, len(ga.population))
|
||||
|
||||
# Repeat tournaments until the mating pool is large enough.
|
||||
while len(ga.population.mating_pool) < parent_amount:
|
||||
|
||||
# Generate a random tournament group and sort by fitness.
|
||||
tournament_group = sorted(random.sample(
|
||||
range(len(ga.population)),
|
||||
tournament_size
|
||||
))
|
||||
|
||||
# For each chromosome, add it to the mating pool based on its rank in the tournament.
|
||||
for index in range(tournament_size):
|
||||
|
||||
# Probability required is selection_probability * (1-selection_probability) ^ index
|
||||
# Each chromosome is (1-selection_probability) times
|
||||
# more likely to become a parent than the next ranked.
|
||||
if random.random() < ga.selection_probability * (1-ga.selection_probability) ** index:
|
||||
break
|
||||
|
||||
# Use random in tournament if noone wins
|
||||
else:
|
||||
index = random.randrange(tournament_size)
|
||||
|
||||
ga.population.set_parent(tournament_group[index])
|
||||
|
||||
|
||||
@_check_selection_probability
|
||||
@_ensure_sorted
|
||||
@_compute_parent_amount
|
||||
def stochastic_geometric(ga, parent_amount):
|
||||
"""
|
||||
Selects parents with probabilities given by a geometric progression. This
|
||||
method is similar to tournament selection, but doesn't create several
|
||||
tournaments. Instead, it assigns probabilities to each rank and selects
|
||||
the entire mating pool using random.choices. Since it essentially uses the
|
||||
entire population as a tournament repeatedly, it is less likely to select
|
||||
worse parents than tournament selection.
|
||||
"""
|
||||
|
||||
# Set the weights of each parent based on their rank.
|
||||
# Each chromosome is (1-selection_probability) times
|
||||
# more likely to become a parent than the next ranked.
|
||||
weights = [
|
||||
(1-ga.selection_probability) ** i
|
||||
for i
|
||||
in range(len(ga.population))
|
||||
]
|
||||
|
||||
# Set the mating pool.
|
||||
ga.population.mating_pool = random.choices(ga.population, weights, k = parent_amount)
|
||||
|
||||
|
||||
@_check_selection_probability
|
||||
@_ensure_sorted
|
||||
@_compute_parent_amount
|
||||
def stochastic_arithmetic(ga, parent_amount):
|
||||
"""
|
||||
Selects parents with probabilities given by an arithmetic progression. This
|
||||
method is similar to stochastic-geometric selection, but is more likely to
|
||||
select worse parents with its simpler selection scheme.
|
||||
"""
|
||||
|
||||
# Set the weights of each parent based on their rank.
|
||||
# The worst chromosome has a weight of 1,
|
||||
# the next worst chromosome has a weight of 2,
|
||||
# etc.
|
||||
# with an inflation of (1-selection probability) * average weight
|
||||
|
||||
average_weight = (len(ga.population)+1) // 2
|
||||
inflation = (1-ga.selection_probability) * average_weight
|
||||
|
||||
weights = [
|
||||
i + inflation
|
||||
for i
|
||||
in range(len(ga.population), 0, -1)
|
||||
]
|
||||
|
||||
# Set the mating pool.
|
||||
ga.population.mating_pool = random.choices(ga.population, weights, k = parent_amount)
|
||||
|
||||
|
||||
class Fitness:
|
||||
|
||||
@_check_selection_probability
|
||||
@_ensure_sorted
|
||||
@_check_positive_fitness
|
||||
@_compute_parent_amount
|
||||
def roulette(ga, parent_amount):
|
||||
"""Roulette selection works based off of how strong the fitness is of the
|
||||
chromosomes in the population. The stronger the fitness the higher the probability
|
||||
that it will be selected. Using the example of a casino roulette wheel.
|
||||
Where the chromosomes are the numbers to be selected and the board size for
|
||||
those numbers are directly proportional to the chromosome's current fitness. Where
|
||||
the ball falls is a randomly generated number between 0 and 1.
|
||||
"""
|
||||
|
||||
# The sum of all the fitnessess in a population
|
||||
fitness_sum = sum(
|
||||
ga.get_chromosome_fitness(index)
|
||||
for index
|
||||
in range(len(ga.population))
|
||||
)
|
||||
|
||||
# A list of ranges that represent the probability of a chromosome getting chosen
|
||||
probability = [ga.selection_probability]
|
||||
|
||||
# The chance of being selected increases incrementally
|
||||
for index in range(len(ga.population)):
|
||||
probability.append(probability[-1]+ga.get_chromosome_fitness(index)/fitness_sum)
|
||||
|
||||
probability = probability[1:]
|
||||
|
||||
# Loops until it reaches a desired mating pool size
|
||||
while len(ga.population.mating_pool) < parent_amount:
|
||||
|
||||
# Spin the roulette
|
||||
rand_number = random.random()
|
||||
|
||||
# Find where the roulette landed.
|
||||
for index in range(len(probability)):
|
||||
if (probability[index] >= rand_number):
|
||||
ga.population.set_parent(index)
|
||||
break
|
||||
|
||||
|
||||
@_check_selection_probability
|
||||
@_ensure_sorted
|
||||
@_compute_parent_amount
|
||||
def stochastic(ga, parent_amount):
|
||||
"""
|
||||
Selects parents using the same probability approach as roulette selection,
|
||||
but doesn't spin a roulette for every selection. Uses random.choices with
|
||||
weighted values to select parents and may produce duplicate parents.
|
||||
"""
|
||||
|
||||
# All fitnesses are the same, select randomly.
|
||||
if ga.get_chromosome_fitness(-1) == ga.get_chromosome_fitness(0):
|
||||
offset = 1-ga.get_chromosome_fitness(-1)
|
||||
|
||||
# Some chromosomes have negative fitness, shift them all into positives.
|
||||
elif ga.get_chromosome_fitness(-1) < 0:
|
||||
offset = -ga.get_chromosome_fitness(-1)
|
||||
|
||||
# No change needed.
|
||||
else:
|
||||
offset = 0
|
||||
|
||||
# Set the weights of each parent based on their fitness + offset.
|
||||
weights = [
|
||||
ga.get_chromosome_fitness(index) + offset
|
||||
for index
|
||||
in range(len(ga.population))
|
||||
]
|
||||
|
||||
inflation = sum(weights) * (1 - ga.selection_probability)
|
||||
|
||||
# Rescale and adjust using selection_probability so that
|
||||
# if selection_probability is high, a low inflation is used,
|
||||
# making selection mostly based on fitness.
|
||||
# if selection_probability is low, a high offset is used,
|
||||
# so everyone has a more equal chance.
|
||||
weights = [
|
||||
weight + inflation
|
||||
for weight
|
||||
in weights
|
||||
]
|
||||
|
||||
# Set the mating pool.
|
||||
ga.population.mating_pool = random.choices(ga.population, weights, k = parent_amount)
|
||||
0
EasyGA/parent/__init__.py
Normal file
0
EasyGA/parent/__init__.py
Normal file
0
EasyGA/parent/test_parent_selection_methods.py
Normal file
0
EasyGA/parent/test_parent_selection_methods.py
Normal file
10
EasyGA/run.py
Normal file
10
EasyGA/run.py
Normal file
@ -0,0 +1,10 @@
|
||||
import EasyGA
|
||||
|
||||
#Create the Genetic Algorithm
|
||||
ga = EasyGA.GA()
|
||||
|
||||
ga.evolve()
|
||||
|
||||
#Print your default genetic algorithm
|
||||
ga.print_generation()
|
||||
ga.print_population()
|
||||
4
EasyGA/structure/__init__.py
Normal file
4
EasyGA/structure/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
# FROM (. means local) file_name IMPORT function_name
|
||||
from .gene import Gene
|
||||
from .chromosome import Chromosome
|
||||
from .population import Population
|
||||
224
EasyGA/structure/chromosome.py
Normal file
224
EasyGA/structure/chromosome.py
Normal file
@ -0,0 +1,224 @@
|
||||
from structure import Gene as make_gene
|
||||
from itertools import chain
|
||||
|
||||
def to_gene(gene):
|
||||
"""Converts the input to a gene if it isn't already one."""
|
||||
|
||||
if isinstance(gene, make_gene):
|
||||
return gene
|
||||
else:
|
||||
return make_gene(gene)
|
||||
|
||||
|
||||
class Chromosome():
|
||||
|
||||
def __init__(self, gene_list):
|
||||
"""Initialize the chromosome with fitness value of None, and a
|
||||
set of genes dependent on user-passed parameter."""
|
||||
|
||||
self.gene_list = [make_gene(gene) for gene in gene_list]
|
||||
self.fitness = None
|
||||
|
||||
|
||||
@property
|
||||
def gene_value_list(self):
|
||||
"""Returns a list of gene values"""
|
||||
return [gene.value for gene in self]
|
||||
|
||||
|
||||
@property
|
||||
def gene_value_iter(self):
|
||||
"""Returns an iterable of gene values"""
|
||||
return (gene.value for gene in self)
|
||||
|
||||
|
||||
#==================================================#
|
||||
# Magic-Dunder Methods replicating list structure. #
|
||||
#==================================================#
|
||||
|
||||
|
||||
def __iter__(self):
|
||||
"""
|
||||
Allows the user to use
|
||||
|
||||
iter(chromosome)
|
||||
list(chromosome) == chromosome.gene_list
|
||||
tuple(chromosome)
|
||||
for gene in chromosome
|
||||
|
||||
to loop through the chromosome.
|
||||
|
||||
Note: using list(chromosome) creates a copy of
|
||||
the gene_list. Altering this will not
|
||||
alter the original gene_list.
|
||||
"""
|
||||
return iter(self.gene_list)
|
||||
|
||||
|
||||
def __getitem__(self, index):
|
||||
"""
|
||||
Allows the user to use
|
||||
gene = chromosome[index]
|
||||
to get the indexed gene.
|
||||
"""
|
||||
return self.gene_list[index]
|
||||
|
||||
|
||||
def __setitem__(self, index, gene):
|
||||
"""
|
||||
Allows the user to use
|
||||
chromosome[index] = gene
|
||||
to set the indexed gene.
|
||||
"""
|
||||
|
||||
# Single gene
|
||||
if isinstance(index, int):
|
||||
self.gene_list[index] = to_gene(gene)
|
||||
|
||||
# Multiple genes
|
||||
else:
|
||||
self.gene_list[index] = [to_gene(item) for item in gene]
|
||||
|
||||
|
||||
def __delitem__(self, index):
|
||||
"""
|
||||
Allows the user to use
|
||||
del chromosome[index]
|
||||
to delete a gene at the specified index.
|
||||
"""
|
||||
del self.gene_list[index]
|
||||
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Allows the user to use
|
||||
size = len(chromosome)
|
||||
to get the length of the chromosome.
|
||||
"""
|
||||
return len(self.gene_list)
|
||||
|
||||
|
||||
def __contains__(self, gene):
|
||||
"""
|
||||
Allows the user to use
|
||||
if gene in chromosome
|
||||
to check if a gene is in the chromosome.
|
||||
"""
|
||||
return (to_gene(gene) in self.gene_list)
|
||||
|
||||
|
||||
def __eq__(self, chromosome):
|
||||
"""Returns self == chromosome, True if all genes match."""
|
||||
return self.gene_list == chromosome.gene_list
|
||||
|
||||
|
||||
def __add__(self, chromosome):
|
||||
"""Return self + chromosome, a chromosome made by concatenating the genes."""
|
||||
return Chromosome(chain(self, chromosome))
|
||||
|
||||
|
||||
def __iadd__(self, chromosome):
|
||||
"""Implement self += chromosome by concatenating the new genes."""
|
||||
self.gene_list += (to_gene(gene) for gene in chromosome)
|
||||
|
||||
|
||||
def append(self, gene):
|
||||
"""Append gene to the end of the chromosome."""
|
||||
self.gene_list.append(to_gene(gene))
|
||||
|
||||
|
||||
def clear(self):
|
||||
"""Remove all genes from chromosome."""
|
||||
self.gene_list = []
|
||||
|
||||
|
||||
def copy(self):
|
||||
"""Return a copy of the chromosome."""
|
||||
return Chromosome(self)
|
||||
|
||||
|
||||
def count(self, gene):
|
||||
"""Return number of occurrences of the gene in the chromosome."""
|
||||
return self.gene_list.count(to_gene(gene))
|
||||
|
||||
|
||||
def index(self, gene, guess = None):
|
||||
"""
|
||||
Allows the user to use
|
||||
index = chromosome.index(gene)
|
||||
index = chromosome.index(gene, guess)
|
||||
to find the index of a gene in the chromosome.
|
||||
|
||||
If no guess is given, it finds the index of the first match.
|
||||
If a guess is given, it finds index of the nearest match.
|
||||
"""
|
||||
|
||||
# Cast to gene object
|
||||
gene = to_gene(gene)
|
||||
|
||||
# Use built-in method
|
||||
if guess is None:
|
||||
return self.gene_list.index(gene)
|
||||
|
||||
# Use symmetric mod
|
||||
guess %= len(self)
|
||||
if guess >= len(self)//2:
|
||||
guess -= len(self)
|
||||
|
||||
# Search outwards for the gene
|
||||
for i in range(1+len(self)//2):
|
||||
|
||||
# Search to the left
|
||||
if gene == self[guess-i]:
|
||||
return (guess-i) % len(self)
|
||||
|
||||
# Search to the right
|
||||
elif gene == self[guess+i]:
|
||||
return (guess+i) % len(self)
|
||||
|
||||
# Gene not found
|
||||
raise ValueError("No such gene in the chromosome found")
|
||||
|
||||
|
||||
def insert(self, index, gene):
|
||||
"""Insert gene so that self[index] == gene."""
|
||||
self.gene_list.insert(index, to_gene(gene))
|
||||
|
||||
|
||||
def pop(self, index = -1):
|
||||
"""Remove and return gene at index (default last).
|
||||
|
||||
Raises IndexError if chromosome is empty or index is out of range.
|
||||
"""
|
||||
return self.gene_list.pop(index)
|
||||
|
||||
|
||||
def remove(self, gene):
|
||||
"""Remove first occurrence of gene.
|
||||
|
||||
Raises ValueError if the gene in not present.
|
||||
"""
|
||||
self.gene_list.remove(to_gene(gene))
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
"""
|
||||
Allows the user to use
|
||||
chromosome_string = repr(chromosome)
|
||||
chromosome_data = eval(chromosome_string)
|
||||
chromosome = ga.make_chromosome(chromosome_data)
|
||||
to get a backend representation of the chromosome
|
||||
which can be evaluated directly as code to create
|
||||
the chromosome.
|
||||
"""
|
||||
return repr(self.gene_list)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Allows the user to use
|
||||
str(chromosome)
|
||||
print(chromosome)
|
||||
to get a frontend representation of the chromosome.
|
||||
"""
|
||||
return ''.join(str(gene) for gene in self)
|
||||
40
EasyGA/structure/gene.py
Normal file
40
EasyGA/structure/gene.py
Normal file
@ -0,0 +1,40 @@
|
||||
from copy import deepcopy
|
||||
|
||||
class Gene:
|
||||
|
||||
def __init__(self, value):
|
||||
"""Initialize a gene with the input value."""
|
||||
|
||||
# Copy another gene
|
||||
try:
|
||||
self.value = deepcopy(value.value)
|
||||
|
||||
# Otherwise copy the given value
|
||||
except:
|
||||
self.value = deepcopy(value)
|
||||
|
||||
|
||||
def __eq__(self, other_gene):
|
||||
"""Comparing two genes by their value."""
|
||||
return self.value == Gene(other_gene).value
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
"""
|
||||
Allows the user to use
|
||||
gene_string = repr(gene)
|
||||
gene_data = eval(gene_string)
|
||||
gene = ga.make_gene(gene_data)
|
||||
to get a backend representation of the gene.
|
||||
"""
|
||||
return repr(self.value)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Allows the user to use
|
||||
str(gene)
|
||||
print(gene)
|
||||
to get a frontend representation of the gene.
|
||||
"""
|
||||
return f'[{str(self.value)}]'
|
||||
287
EasyGA/structure/population.py
Normal file
287
EasyGA/structure/population.py
Normal file
@ -0,0 +1,287 @@
|
||||
from structure import Chromosome as make_chromosome
|
||||
from itertools import chain
|
||||
|
||||
def to_chromosome(chromosome):
|
||||
"""Converts the input to a chromosome if it isn't already one."""
|
||||
|
||||
if isinstance(chromosome, make_chromosome):
|
||||
return chromosome
|
||||
else:
|
||||
return make_chromosome(chromosome)
|
||||
|
||||
|
||||
class Population:
|
||||
|
||||
def __init__(self, chromosome_list):
|
||||
"""Initialize the population with a collection
|
||||
of chromosomes dependant on user-passed parameter."""
|
||||
|
||||
self.chromosome_list = [make_chromosome(chromosome) for chromosome in chromosome_list]
|
||||
self.mating_pool = []
|
||||
self.next_population = []
|
||||
|
||||
|
||||
def update(self):
|
||||
"""Sets all the population variables to what they should be at
|
||||
the end of the generation """
|
||||
self.chromosome_list = self.next_population
|
||||
self.reset_mating_pool()
|
||||
self.reset_next_population()
|
||||
|
||||
|
||||
def reset_mating_pool(self):
|
||||
"""Clears the mating pool"""
|
||||
self.mating_pool = []
|
||||
|
||||
|
||||
def reset_next_population(self):
|
||||
"""Clears the next population"""
|
||||
self.next_population = []
|
||||
|
||||
|
||||
def remove_chromosome(self, index):
|
||||
"""Removes and returns a chromosome from the indicated index from the population"""
|
||||
return self.chromosome_list.pop(index)
|
||||
|
||||
|
||||
def remove_parent(self, index):
|
||||
"""Removes and returns a parent from the indicated index from the mating pool"""
|
||||
return self.mating_pool.pop(index)
|
||||
|
||||
|
||||
def remove_child(self, index):
|
||||
"""Removes and returns a child from the indicated index from the next population"""
|
||||
return self.next_population.pop(index)
|
||||
|
||||
|
||||
def append_children(self, chromosome_list):
|
||||
"""Appends a list of chromosomes to the next population."""
|
||||
|
||||
self.next_population += (
|
||||
to_chromosome(chromosome)
|
||||
for chromosome
|
||||
in chromosome_list
|
||||
)
|
||||
|
||||
|
||||
def add_chromosome(self, chromosome, index = None):
|
||||
"""Adds a chromosome to the population at the input index,
|
||||
defaulted to the end of the chromosome set"""
|
||||
|
||||
if index is None:
|
||||
index = len(self)
|
||||
self.chromosome_list.insert(index, to_chromosome(chromosome))
|
||||
|
||||
|
||||
def add_parent(self, chromosome):
|
||||
"""Adds a chromosome to the mating pool"""
|
||||
self.mating_pool.append(to_chromosome(chromosome))
|
||||
|
||||
|
||||
def add_child(self, chromosome):
|
||||
"""Adds a chromosome to the next population"""
|
||||
self.next_population.append(to_chromosome(chromosome))
|
||||
|
||||
|
||||
def set_parent(self, index):
|
||||
"""Sets the indexed chromosome from the population as a parent"""
|
||||
self.add_parent(self[index])
|
||||
|
||||
|
||||
#==================================================#
|
||||
# Magic-Dunder Methods replicating list structure. #
|
||||
#==================================================#
|
||||
|
||||
|
||||
def __iter__(self):
|
||||
"""
|
||||
Allows the user to use
|
||||
|
||||
iter(population)
|
||||
list(population) == population.chromosome_list
|
||||
tuple(population)
|
||||
for chromosome in population
|
||||
|
||||
to loop through the population.
|
||||
"""
|
||||
return iter(self.chromosome_list)
|
||||
|
||||
|
||||
def __getitem__(self, index):
|
||||
"""
|
||||
Allows the user to use
|
||||
chromosome = population[index]
|
||||
to get the indexed chromosome.
|
||||
"""
|
||||
return self.chromosome_list[index]
|
||||
|
||||
|
||||
def __setitem__(self, index, chromosome):
|
||||
"""
|
||||
Allows the user to use
|
||||
population[index] = chromosome
|
||||
to set the indexed chromosome.
|
||||
"""
|
||||
|
||||
# Just one chromosome
|
||||
if isinstance(index, int):
|
||||
self.chromosome_list[index] = to_chromosome(chromosome)
|
||||
|
||||
# Multiple chromosomes
|
||||
else:
|
||||
self.chromosome_list[index] = [to_chromosome(item) for item in chromosome]
|
||||
|
||||
|
||||
def __delitem__(self, index):
|
||||
"""
|
||||
Allows the user to use
|
||||
del population[index]
|
||||
to delete a chromosome at the specified index.
|
||||
"""
|
||||
del self.chromosome_list[index]
|
||||
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Allows the user to use
|
||||
size = len(population)
|
||||
to get the length of the population.
|
||||
"""
|
||||
return len(self.chromosome_list)
|
||||
|
||||
|
||||
def __contains__(self, chromosome):
|
||||
"""
|
||||
Allows the user to use
|
||||
if chromosome in population
|
||||
to check if a chromosome is in the population.
|
||||
"""
|
||||
return (to_chromosome(chromosome) in self.chromosome_list)
|
||||
|
||||
|
||||
def __eq__(self, population):
|
||||
"""Returns self == population, True if all chromosomes match."""
|
||||
return self.chromosome_list == population.chromosome_list
|
||||
|
||||
|
||||
def __add__(self, population):
|
||||
"""Returns self + population, a population made by concatenating the chromosomes."""
|
||||
return Population(chain(self, population))
|
||||
|
||||
|
||||
def __iadd__(self, population):
|
||||
"""Implement self += population by concatenating the new chromosomes."""
|
||||
self.chromosome_list += (to_chromosome(chromosome) for chromosome in population)
|
||||
|
||||
|
||||
def append(self, chromosome):
|
||||
"""Append chromosome to the end of the population."""
|
||||
self.chromosome_list.append(to_chromosome(chromosome))
|
||||
|
||||
|
||||
def clear(self):
|
||||
"""Remove all chromosomes from the population."""
|
||||
self.chromosome_list = []
|
||||
|
||||
|
||||
def copy(self):
|
||||
"""Return a copy of the population."""
|
||||
return Population(self)
|
||||
|
||||
|
||||
def count(self, chromosome):
|
||||
"""Return number of occurrences of the chromosome in the population."""
|
||||
return self.chromosome_list.count(to_chromosome(chromosome))
|
||||
|
||||
|
||||
def index(self, chromosome, guess = None):
|
||||
"""
|
||||
Allows the user to use
|
||||
index = population.index(chromosome)
|
||||
index = population.index(chromosome, guess)
|
||||
to find the index of a chromosome in the population.
|
||||
|
||||
If no guess is given, it finds the index of the first match.
|
||||
If a guess is given, it finds index of the nearest match.
|
||||
"""
|
||||
|
||||
chromosome = to_chromosome(chromosome)
|
||||
|
||||
# Use built-in method
|
||||
if guess is None:
|
||||
return self.chromosome_list.index(chromosome)
|
||||
|
||||
# Use symmetric mod
|
||||
guess %= len(self)
|
||||
if guess >= len(self)//2:
|
||||
guess -= len(self)
|
||||
|
||||
# Search outwards for the chromosome
|
||||
for i in range(len(self)//2):
|
||||
|
||||
# Search to the left
|
||||
if chromosome == self[guess-i]:
|
||||
return (guess-i) % len(self)
|
||||
|
||||
# Search to the right
|
||||
elif chromosome == self[guess+i]:
|
||||
return (guess+i) % len(self)
|
||||
|
||||
# Chromosome not found
|
||||
raise IndexError("No such chromosome in the population found")
|
||||
|
||||
|
||||
def insert(self, index, chromosome):
|
||||
"""Insert chromosome so that self[index] == chromsome."""
|
||||
self.chromosome_list.insert(index, to_chromosome(chromosome))
|
||||
|
||||
|
||||
def pop(self, index = -1):
|
||||
"""Remove and return chromosome at index (default last).
|
||||
|
||||
Raises IndexError if population is empty or index is out of range.
|
||||
"""
|
||||
return self.chromosome_list.pop(index)
|
||||
|
||||
|
||||
def remove(self, chromosome):
|
||||
"""Remove first occurrence of chromosome.
|
||||
|
||||
Raises ValueError if the chromosome is not present.
|
||||
"""
|
||||
self.chromosome_list.remove(to_chromosome(chromosome))
|
||||
|
||||
|
||||
def sort(self, *, key = lambda chromosome: chromosome.fitness, reverse):
|
||||
"""Sorts the population."""
|
||||
self.chromosome_list.sort(
|
||||
key = key,
|
||||
reverse = reverse
|
||||
)
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
"""
|
||||
Allows the user to use
|
||||
population_string = repr(population)
|
||||
population_data = eval(population_string)
|
||||
population = ga.make_population(population_data)
|
||||
to get a backend representation of the population
|
||||
which can be evaluated directly as code to create
|
||||
the population.
|
||||
"""
|
||||
return repr(self.chromosome_list)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Allows the user to use
|
||||
str(population)
|
||||
print(population)
|
||||
to get a frontend representation of the population.
|
||||
"""
|
||||
return ''.join(
|
||||
f'Chromosome - {index} {chromosome} / Fitness = {chromosome.fitness}\n'
|
||||
for index, chromosome
|
||||
in enumerate(self)
|
||||
)
|
||||
1
EasyGA/structure/test_struction.py
Normal file
1
EasyGA/structure/test_struction.py
Normal file
@ -0,0 +1 @@
|
||||
|
||||
48
EasyGA/survivor/Survivor.py
Normal file
48
EasyGA/survivor/Survivor.py
Normal file
@ -0,0 +1,48 @@
|
||||
import random
|
||||
|
||||
# Import all survivor decorators
|
||||
from decorators import *
|
||||
|
||||
|
||||
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)
|
||||
ga.population.append_children(ga.population[:needed_amount])
|
||||
|
||||
|
||||
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)
|
||||
ga.population.append_children(random.sample(ga.population, needed_amount))
|
||||
|
||||
|
||||
def fill_in_parents_then_random(ga):
|
||||
"""Fills in the next population with all parents followed by random chromosomes from the last population"""
|
||||
|
||||
# Remove dupes from the mating pool
|
||||
mating_pool = set(ga.population.mating_pool)
|
||||
|
||||
needed_amount = len(ga.population) - len(ga.population.next_population)
|
||||
parent_amount = min(needed_amount, len(mating_pool))
|
||||
random_amount = needed_amount - parent_amount
|
||||
|
||||
# Only parents are used.
|
||||
if random_amount == 0:
|
||||
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:
|
||||
ga.population.append_children(mating_pool)
|
||||
ga.population.append_children(
|
||||
random.sample(
|
||||
set(ga.population) - mating_pool,
|
||||
random_amount
|
||||
)
|
||||
)
|
||||
0
EasyGA/survivor/__init__.py
Normal file
0
EasyGA/survivor/__init__.py
Normal file
0
EasyGA/survivor/test_survivor_methods.py
Normal file
0
EasyGA/survivor/test_survivor_methods.py
Normal file
14
EasyGA/termination/Termination.py
Normal file
14
EasyGA/termination/Termination.py
Normal file
@ -0,0 +1,14 @@
|
||||
# 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
|
||||
@_add_by_tolerance_goal
|
||||
def fitness_generation_tolerance(ga):
|
||||
"""Terminate GA when any of the
|
||||
- fitness,
|
||||
- generation, or
|
||||
- tolerance
|
||||
goals are met."""
|
||||
|
||||
return True
|
||||
0
EasyGA/termination/__init__.py
Normal file
0
EasyGA/termination/__init__.py
Normal file
0
EasyGA/termination/test_termination_methods.py
Normal file
0
EasyGA/termination/test_termination_methods.py
Normal file
217
EasyGA/test_EasyGA.py
Normal file
217
EasyGA/test_EasyGA.py
Normal file
@ -0,0 +1,217 @@
|
||||
import random
|
||||
from EasyGA import GA, Parent, Crossover, Mutation, Survivor, Termination
|
||||
|
||||
# USE THIS COMMAND WHEN TESTING -
|
||||
# python3 -m pytest
|
||||
|
||||
# Tests can be broken down into three parts.
|
||||
# - Testing correct size
|
||||
# - Testing size while integrated with our function
|
||||
# - Testing correct value
|
||||
# - Testing integration with other functions
|
||||
|
||||
def test_population_size():
|
||||
"""Test the population size is create correctly"""
|
||||
|
||||
for i in range(4,100):
|
||||
# Create the ga to test
|
||||
ga = GA()
|
||||
|
||||
ga.generation_goal = 10
|
||||
# Set the upper limit of testing
|
||||
ga.population_size = i
|
||||
# Evolve the ga
|
||||
ga.evolve()
|
||||
|
||||
# If they are not equal throw an error
|
||||
assert int(len(ga.population)) == ga.population_size
|
||||
|
||||
def test_chromosome_length():
|
||||
""" Test to see if the actual chromosome length is the same as defined."""
|
||||
|
||||
# Test from 0 to 100 chromosome length
|
||||
for i in range(1,100):
|
||||
# Create the ga to test
|
||||
ga = GA()
|
||||
|
||||
ga.generation_goal = 10
|
||||
# Set the upper limit of testing
|
||||
ga.chromosome_length = i
|
||||
# Evolve the ga
|
||||
ga.evolve()
|
||||
|
||||
# If they are not equal throw an error
|
||||
assert len(ga.population.chromosome_list[0]) == ga.chromosome_length
|
||||
|
||||
def test_gene_value():
|
||||
""" """
|
||||
pass
|
||||
|
||||
def test_initilization():
|
||||
""" """
|
||||
pass
|
||||
|
||||
def test_default():
|
||||
# Create the Genetic algorithm
|
||||
ga = GA()
|
||||
|
||||
# Evolve the genetic algorithm
|
||||
ga.evolve()
|
||||
|
||||
# Print your default genetic algorithm
|
||||
ga.print_generation()
|
||||
ga.print_population()
|
||||
|
||||
|
||||
def test_attributes_gene_impl():
|
||||
# Create the Genetic algorithm
|
||||
ga = GA()
|
||||
|
||||
# Set necessary attributes
|
||||
ga.population_size = 3
|
||||
ga.chromosome_length = 5
|
||||
ga.generation_goal = 1
|
||||
# Set gene_impl
|
||||
ga.gene_impl = lambda: random.randint(1, 10)
|
||||
|
||||
# Evolve the genetic algorithm
|
||||
ga.evolve()
|
||||
|
||||
|
||||
def test_attributes_chromosome_impl_lambdas():
|
||||
# Create the Genetic algorithm
|
||||
ga = GA()
|
||||
|
||||
# Set necessary attributes
|
||||
ga.chromosome_length = 3
|
||||
ga.generation_goal = 1
|
||||
# Set gene_impl to None so it won't interfere
|
||||
ga.gene_impl = None
|
||||
# Set chromosome_impl
|
||||
ga.chromosome_impl = lambda: [
|
||||
random.randrange(1,100),
|
||||
random.uniform(10,5),
|
||||
random.choice(["up","down"])
|
||||
]
|
||||
|
||||
# Evolve the genetic algorithm
|
||||
ga.evolve()
|
||||
|
||||
def test_attributes_chromosome_impl_functions():
|
||||
# Create the Genetic algorithm
|
||||
ga = GA()
|
||||
|
||||
# Set necessary attributes
|
||||
ga.chromosome_length = 3
|
||||
ga.generation_goal = 1
|
||||
|
||||
# Create chromosome_impl user function
|
||||
def user_chromosome_function():
|
||||
chromosome_data = [
|
||||
random.randrange(1,100),
|
||||
random.uniform(10,5),
|
||||
random.choice(["up","down"])
|
||||
]
|
||||
return chromosome_data
|
||||
|
||||
# Set the chromosome_impl
|
||||
ga.chromosome_impl = user_chromosome_function
|
||||
|
||||
# Evolve the genetic algorithm
|
||||
ga.evolve()
|
||||
|
||||
def test_while_ga_active():
|
||||
# Create the Genetic algorithm
|
||||
ga = GA()
|
||||
|
||||
# Set necessary attributes
|
||||
ga.generation_goal = 1
|
||||
|
||||
# Evolve using ga.active
|
||||
while ga.active():
|
||||
ga.evolve(5)
|
||||
|
||||
|
||||
def test_parent_selection_impl():
|
||||
# Create the Genetic algorithm
|
||||
ga = GA()
|
||||
|
||||
# Set the parent_selection_impl
|
||||
ga.parent_selection_impl = Parent.Fitness.roulette
|
||||
|
||||
# Evolve the genetic algorithm
|
||||
ga.evolve()
|
||||
|
||||
assert (ga.parent_selection_impl == Parent.Fitness.roulette) and (ga != None)
|
||||
|
||||
def test_crossover_population_impl():
|
||||
# Create the Genetic algorithm
|
||||
ga = GA()
|
||||
|
||||
# Set the crossover_population_impl
|
||||
ga.crossover_population_impl = Cossover.Population.sequential_selection
|
||||
|
||||
# Evolve the genetic algorithm
|
||||
ga.evolve()
|
||||
|
||||
assert (ga.crossover_population_impl == Crossover.Population.sequential_selection) and (ga != None)
|
||||
|
||||
def test_crossover_individual_impl():
|
||||
# Create the Genetic algorithm
|
||||
ga = GA()
|
||||
|
||||
# Set the crossover_individual_impl
|
||||
ga.crossover_individual_impl = Crossover.Individual.single_point
|
||||
|
||||
# Evolve the genetic algorithm
|
||||
ga.evolve()
|
||||
|
||||
assert (ga.crossover_individual_impl == Crossover.Individual.single_point) and (ga != None)
|
||||
|
||||
def test_mutation_population_impl():
|
||||
# Create the Genetic algorithm
|
||||
ga = GA()
|
||||
|
||||
# Set the mutation_population_impl
|
||||
ga.mutation_population_impl = Mutation.Population.random_selection
|
||||
|
||||
# Evolve the genetic algorithm
|
||||
ga.evolve()
|
||||
|
||||
assert (ga.mutation_population_impl == Mutation.Population.random_selection) and (ga != None)
|
||||
|
||||
def test_mutation_individual_impl():
|
||||
# Create the Genetic algorithm
|
||||
ga = GA()
|
||||
|
||||
# Set the mutation_population_impl
|
||||
ga.mutation_individual_impl = Mutation.Individual.single_gene
|
||||
|
||||
# Evolve the genetic algorithm
|
||||
ga.evolve()
|
||||
|
||||
assert (ga.mutation_individual_impl == Mutation.Individual.single_gene) and (ga != None)
|
||||
|
||||
def test_survivor_selection_impl():
|
||||
# Create the Genetic algorithm
|
||||
ga = GA()
|
||||
|
||||
# Set the survivor_selection_impl
|
||||
ga.survivor_selection_impl = Survivor.fill_in_random
|
||||
|
||||
# Evolve the genetic algorithm
|
||||
ga.evolve()
|
||||
|
||||
assert (ga.survivor_selection_impl == Survivor.fill_in_random) and (ga != None)
|
||||
|
||||
def test_termination_impl():
|
||||
# Create the Genetic algorithm
|
||||
ga = GA()
|
||||
|
||||
# Set the termination_impl
|
||||
ga.termination_impl = Termination.fitness_and_generation_based
|
||||
|
||||
# Evolve the genetic algorithm
|
||||
ga.evolve()
|
||||
|
||||
assert (ga.termination_impl == Termination.fitness_and_generation_based) and (ga != None)
|
||||
Reference in New Issue
Block a user