Directory

This commit is contained in:
danielwilczak101
2021-01-27 03:03:30 -06:00
parent cd164832ab
commit 7e5c384872
33 changed files with 0 additions and 0 deletions

337
EasyGA/EasyGA.py Normal file
View 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
View File

433
EasyGA/attributes.py Normal file
View 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

View 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)

View File

View File

View 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

View 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
View File

@ -0,0 +1,261 @@
import random
from math import ceil
def function_info(decorator):
"""Recovers the name and doc-string for decorators throughout EasyGA for documentation purposes."""
def new_decorator(method):
# Apply old decorator
new_method = decorator(method)
# Recover name and doc-string
new_method.__name__ = method.__name__
new_method.__doc__ = method.__doc__
# Return new method with proper name and doc-string
return new_method
return new_decorator
#=======================#
# Crossover decorators: #
#=======================#
@function_info
def _check_weight(individual_method):
"""Checks if the weight is between 0 and 1 before running.
Exception may occur when using ga.adapt, which will catch
the error and try again with valid weight.
"""
def new_method(ga, parent_1, parent_2, *, weight = individual_method.__kwdefaults__.get('weight', None)):
if weight is None:
individual_method(ga, parent_1, parent_2)
elif 0 < weight < 1:
individual_method(ga, parent_1, parent_2, weight = weight)
else:
raise ValueError(f"Weight must be between 0 and 1 when using {individual_method.__name__}.")
return new_method
@function_info
def _gene_by_gene(individual_method):
"""Perform crossover by making a single new chromosome by combining each gene by gene."""
def new_method(ga, parent_1, parent_2, *, weight = individual_method.__kwdefaults__.get('weight', 'None')):
ga.population.add_child(
individual_method(ga, value_1, value_2)
if weight == 'None' else
individual_method(ga, value_1, value_2, weight = weight)
for value_1, value_2
in zip(parent_1.gene_value_iter, parent_2.gene_value_iter)
)
return new_method
#====================#
# Parent decorators: #
#====================#
@function_info
def _check_selection_probability(selection_method):
"""Raises a ValueError if the selection_probability is not between 0 and 1 inclusively.
Otherwise runs the selection method."""
def new_method(ga):
if 0 <= ga.selection_probability <= 1:
selection_method(ga)
else:
raise ValueError("Selection probability must be between 0 and 1 to select parents.")
return new_method
@function_info
def _check_positive_fitness(selection_method):
"""Raises a ValueError if the population contains a chromosome with negative fitness.
Otherwise runs the selection method."""
def new_method(ga):
if ga.get_chromosome_fitness(0) > 0 and ga.get_chromosome_fitness(-1) >= 0:
selection_method(ga)
else:
raise ValueError("Converted fitness values can't have negative values or be all 0."
+ " Consider using rank selection or stochastic selection instead.")
return new_method
@function_info
def _ensure_sorted(selection_method):
"""Sorts the population by fitness and then runs the selection method."""
def new_method(ga):
ga.sort_by_best_fitness()
selection_method(ga)
return new_method
@function_info
def _compute_parent_amount(selection_method):
"""Computes the amount of parents needed to be selected,
and passes it as another argument for the method."""
def new_method(ga):
parent_amount = max(2, round(len(ga.population)*ga.parent_ratio))
selection_method(ga, parent_amount)
return new_method
#======================#
# Mutation decorators: #
#======================#
@function_info
def _check_chromosome_mutation_rate(population_method):
"""Checks if the chromosome mutation rate is a float between 0 and 1 before running."""
def new_method(ga):
if not isinstance(ga.chromosome_mutation_rate, float):
raise TypeError("Chromosome mutation rate must be a float.")
elif 0 < ga.chromosome_mutation_rate < 1:
population_method(ga)
else:
raise ValueError("Chromosome mutation rate must be between 0 and 1.")
return new_method
@function_info
def _check_gene_mutation_rate(individual_method):
"""Checks if the gene mutation rate is a float between 0 and 1 before running."""
def new_method(ga, index):
if not isinstance(ga.gene_mutation_rate, float):
raise TypeError("Gene mutation rate must be a float.")
elif 0 < ga.gene_mutation_rate <= 1:
individual_method(ga, index)
else:
raise ValueError("Gene mutation rate must be between 0 and 1.")
return new_method
@function_info
def _reset_fitness(individual_method):
"""Resets the fitness value of the chromosome."""
def new_method(ga, chromosome):
chromosome.fitness = None
individual_method(ga, chromosome)
return new_method
@function_info
def _loop_random_mutations(individual_method):
"""Runs the individual method until enough
genes are mutated on the indexed chromosome."""
# Change input to include the gene index being mutated.
def new_method(ga, chromosome):
sample_space = range(len(chromosome))
sample_size = ceil(len(chromosome)*ga.gene_mutation_rate)
# Loop the individual method until enough genes are mutated.
for index in random.sample(sample_space, sample_size):
individual_method(ga, chromosome, index)
return new_method
#======================#
# Survivor decorators: #
#======================#
#=========================#
# Termination decorators: #
#=========================#
@function_info
def _add_by_fitness_goal(termination_impl):
"""Adds termination by fitness goal to the method."""
def new_method(ga):
# Try to check the fitness goal
try:
# If minimum fitness goal reached, stop ga.
if ga.target_fitness_type == 'min' and ga.population[0].fitness <= ga.fitness_goal:
return False
# If maximum fitness goal reached, stop ga.
elif ga.target_fitness_type == 'max' and ga.population[0].fitness >= ga.fitness_goal:
return False
# Fitness or fitness goals are None, or Population not initialized
except (TypeError, AttributeError):
pass
# Check other termination methods
return termination_impl(ga)
return new_method
@function_info
def _add_by_generation_goal(termination_impl):
"""Adds termination by generation goal to the method."""
def new_method(ga):
# If generation goal is set, check it.
if ga.generation_goal is not None and ga.current_generation >= ga.generation_goal:
return False
# Check other termination methods
return termination_impl(ga)
return new_method
@function_info
def _add_by_tolerance_goal(termination_impl):
"""Adds termination by tolerance goal to the method."""
def new_method(ga):
# If tolerance is set, check it, if possible.
try:
best_fitness = ga.population[0].fitness
threshhold_fitness = ga.population[round(ga.percent_converged*len(ga.population))].fitness
tol = ga.tolerance_goal * (1 + abs(best_fitness))
# Terminate if the specified amount of the population has converged to the specified tolerance
if abs(best_fitness - threshhold_fitness) < tol:
return False
# Fitness or tolerance goals are None, or population is not initialized
except (TypeError, AttributeError):
pass
# Check other termination methods
return termination_impl(ga)
return new_method

View File

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

View File

View File

@ -0,0 +1 @@

144
EasyGA/mutation/Mutation.py Normal file
View 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]

View File

View File

196
EasyGA/parent/Parent.py Normal file
View 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)

View File

10
EasyGA/run.py Normal file
View 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()

View 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

View 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
View 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)}]'

View 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)
)

View File

@ -0,0 +1 @@

View 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
)
)

View File

View File

View 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

View File

217
EasyGA/test_EasyGA.py Normal file
View 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)