Improved documentation
This commit is contained in:
236
EasyGA/EasyGA.py
236
EasyGA/EasyGA.py
@ -1,8 +1,5 @@
|
|||||||
# Import math for square root (ga.dist()) and ceil (crossover methods)
|
from __future__ import annotations
|
||||||
import math
|
from typing import Optional, MutableSequence, Iterable
|
||||||
|
|
||||||
# Import random for many methods
|
|
||||||
import random
|
|
||||||
|
|
||||||
# Import all decorators
|
# Import all decorators
|
||||||
import decorators
|
import decorators
|
||||||
@ -11,6 +8,9 @@ import decorators
|
|||||||
from structure import Population as make_population
|
from structure import Population as make_population
|
||||||
from structure import Chromosome as make_chromosome
|
from structure import Chromosome as make_chromosome
|
||||||
from structure import Gene as make_gene
|
from structure import Gene as make_gene
|
||||||
|
from structure import Population
|
||||||
|
from structure import Chromosome
|
||||||
|
from structure import Gene
|
||||||
|
|
||||||
# Misc. Methods
|
# Misc. Methods
|
||||||
from examples import Fitness
|
from examples import Fitness
|
||||||
@ -37,18 +37,27 @@ import matplotlib.pyplot as plt
|
|||||||
|
|
||||||
|
|
||||||
class GA(Attributes):
|
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
|
GA is the main controller class for EasyGA. Everything is run
|
||||||
attributes class.
|
through the GA class. The GA class inherits all default attributes
|
||||||
|
from the Attributes dataclass.
|
||||||
|
|
||||||
An extensive wiki going over all major functions can be found at
|
An extensive wiki going over all major functionalities can be found at
|
||||||
https://github.com/danielwilczak101/EasyGA/wiki
|
https://github.com/danielwilczak101/EasyGA/wiki
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def evolve(self, number_of_generations = float('inf'), consider_termination = True):
|
def evolve(self: GA, number_of_generations: float = float('inf'), consider_termination: bool = True) -> None:
|
||||||
"""Evolves the ga the specified number of generations
|
"""
|
||||||
or until the ga is no longer active if consider_termination is True."""
|
Evolves the ga until the ga is no longer active.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
number_of_generations : float = inf
|
||||||
|
The number of generations before the GA terminates. Runs forever by default.
|
||||||
|
consider_termination : bool = True
|
||||||
|
Whether GA.active() is checked for termination.
|
||||||
|
"""
|
||||||
|
|
||||||
# Create the initial population if necessary.
|
# Create the initial population if necessary.
|
||||||
if self.population is None:
|
if self.population is None:
|
||||||
@ -84,7 +93,6 @@ class GA(Attributes):
|
|||||||
self.sort_by_best_fitness()
|
self.sort_by_best_fitness()
|
||||||
|
|
||||||
# Save the population to the database
|
# Save the population to the database
|
||||||
if self.save_data == True:
|
|
||||||
self.save_population()
|
self.save_population()
|
||||||
|
|
||||||
# Adapt the ga if the generation times the adapt rate
|
# Adapt the ga if the generation times the adapt rate
|
||||||
@ -97,30 +105,25 @@ class GA(Attributes):
|
|||||||
self.current_generation += 1
|
self.current_generation += 1
|
||||||
|
|
||||||
|
|
||||||
def update_population(self):
|
def update_population(self: GA) -> None:
|
||||||
"""Updates the population to the new population and resets
|
"""
|
||||||
the mating pool and new population."""
|
Updates the population to the new population
|
||||||
|
and resets the mating pool and new population.
|
||||||
|
"""
|
||||||
self.population.update()
|
self.population.update()
|
||||||
|
|
||||||
|
|
||||||
def reset_run(self):
|
def reset_run(self: GA) -> None:
|
||||||
"""Resets a run by re-initializing the population
|
"""
|
||||||
and modifying counters."""
|
Resets a run by re-initializing the
|
||||||
|
population and modifying counters.
|
||||||
|
"""
|
||||||
self.initialize_population()
|
self.initialize_population()
|
||||||
self.current_generation = 0
|
self.current_generation = 0
|
||||||
self.run += 1
|
self.run += 1
|
||||||
|
|
||||||
|
|
||||||
def active(self):
|
def adapt(self: GA) -> None:
|
||||||
"""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."""
|
"""Adapts the ga to hopefully get better results."""
|
||||||
|
|
||||||
self.adapt_probabilities()
|
self.adapt_probabilities()
|
||||||
@ -131,11 +134,11 @@ class GA(Attributes):
|
|||||||
self.sort_by_best_fitness()
|
self.sort_by_best_fitness()
|
||||||
|
|
||||||
|
|
||||||
def adapt_probabilities(self):
|
def adapt_probabilities(self: GA) -> None:
|
||||||
"""Modifies the parent ratio and mutation rates
|
"""
|
||||||
based on the adapt rate and percent converged.
|
Modifies the parent ratio and mutation rates based on the adapt
|
||||||
Attempts to balance out so that a portion of the
|
rate and percent converged. Attempts to balance out so that a
|
||||||
population gradually approaches the solution.
|
portion of the population gradually approaches the solution.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Determines how much to adapt by
|
# Determines how much to adapt by
|
||||||
@ -173,7 +176,7 @@ class GA(Attributes):
|
|||||||
self.gene_mutation_rate = average(bounds[2], self.gene_mutation_rate)
|
self.gene_mutation_rate = average(bounds[2], self.gene_mutation_rate)
|
||||||
|
|
||||||
|
|
||||||
def adapt_population(self):
|
def adapt_population(self: GA) -> None:
|
||||||
"""
|
"""
|
||||||
Performs weighted crossover between the best chromosome and
|
Performs weighted crossover between the best chromosome and
|
||||||
the rest of the chromosomes, using negative weights to push
|
the rest of the chromosomes, using negative weights to push
|
||||||
@ -221,39 +224,24 @@ class GA(Attributes):
|
|||||||
self.population.mating_pool = []
|
self.population.mating_pool = []
|
||||||
|
|
||||||
|
|
||||||
def initialize_population(self):
|
def initialize_population(self: GA) -> None:
|
||||||
"""Initialize the population using
|
|
||||||
the initialization implimentation
|
|
||||||
that is currently set.
|
|
||||||
"""
|
"""
|
||||||
|
Sets self.population using the chromosome implementation and population size.
|
||||||
if self.chromosome_impl is not None:
|
"""
|
||||||
self.population = self.make_population(
|
self.population = self.make_population(self.population_impl())
|
||||||
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):
|
def set_all_fitness(self: GA) -> None:
|
||||||
"""Will get and set the fitness of each chromosome in the population.
|
"""
|
||||||
If update_fitness is set then all fitness values are updated.
|
Sets the fitness of each chromosome in the population.
|
||||||
Otherwise only fitness values set to None (i.e. uninitialized
|
|
||||||
fitness values) are updated.
|
Attributes
|
||||||
|
----------
|
||||||
|
update_fitness : bool
|
||||||
|
Whether fitnesses are recalculated even if they were previously calculated.
|
||||||
|
Allows chromosomes which exist in dynamic environments.
|
||||||
|
fitness_function_impl(chromosome) -> float
|
||||||
|
The fitness function which measures how well a chromosome is doing.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Check each chromosome
|
# Check each chromosome
|
||||||
@ -264,11 +252,40 @@ class GA(Attributes):
|
|||||||
chromosome.fitness = self.fitness_function_impl(chromosome)
|
chromosome.fitness = self.fitness_function_impl(chromosome)
|
||||||
|
|
||||||
|
|
||||||
def sort_by_best_fitness(self, chromosome_list = None, in_place = True):
|
def sort_by_best_fitness(
|
||||||
"""Sorts the chromosome list by fitness based on fitness type.
|
self: GA,
|
||||||
|
chromosome_list: Optional[
|
||||||
|
Union[MutableSequence[Chromosome],
|
||||||
|
Iterable[Chromosome]]
|
||||||
|
] = None,
|
||||||
|
in_place: bool = True,
|
||||||
|
) -> MutableSequence[Chromosome]:
|
||||||
|
"""
|
||||||
|
Sorts the chromosome list by fitness based on fitness type.
|
||||||
1st element has best fitness.
|
1st element has best fitness.
|
||||||
2nd element has second best fitness.
|
2nd element has second best fitness.
|
||||||
etc.
|
etc.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
chromosome_list : MutableSequence[Chromosome] = self.population
|
||||||
|
The list of chromosomes to be sorted. By default, the population is used.
|
||||||
|
May be sorted in-place.
|
||||||
|
chromosome_list : Iterable[Chromosome]
|
||||||
|
The list of chromosomes to be sorted. By default, the population is used.
|
||||||
|
May not be sorted in-place.
|
||||||
|
in_place : bool = True
|
||||||
|
Whether the sort is done in-place, modifying the original object, or not.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
target_fitness_type : str in ('max', 'min')
|
||||||
|
The way the chromosomes should be sorted.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
chromosome_list : MutableSequence[Chromosome]
|
||||||
|
The sorted chromosomes.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self.target_fitness_type not in ('max', 'min'):
|
if self.target_fitness_type not in ('max', 'min'):
|
||||||
@ -282,57 +299,100 @@ class GA(Attributes):
|
|||||||
reverse = (self.target_fitness_type == 'max')
|
reverse = (self.target_fitness_type == 'max')
|
||||||
|
|
||||||
# Sort by fitness, assuming None should be moved to the end of the list
|
# 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)]))
|
def key(chromosome):
|
||||||
|
if chromosome.fitness is not None:
|
||||||
|
return chromosome_fitness
|
||||||
|
elif reverse:
|
||||||
|
return float('-inf')
|
||||||
|
else:
|
||||||
|
return float('inf')
|
||||||
|
|
||||||
if in_place:
|
if in_place:
|
||||||
chromosome_list.sort(key = key, reverse = reverse)
|
chromosome_list.sort(key=key, reverse=reverse)
|
||||||
return chromosome_list
|
return chromosome_list
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return sorted(chromosome_list, key = key, reverse = reverse)
|
return sorted(chromosome_list, key=key, reverse=reverse)
|
||||||
|
|
||||||
|
|
||||||
def get_chromosome_fitness(self, index):
|
def get_chromosome_fitness(self: GA, index: int) -> float:
|
||||||
"""Returns the fitness value of the chromosome
|
|
||||||
at the specified index after conversion based
|
|
||||||
on the target fitness type.
|
|
||||||
"""
|
"""
|
||||||
|
Computes the converted fitness of a chromosome at an index.
|
||||||
|
The converted fitness remaps the fitness to sensible values
|
||||||
|
for various methods.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
index : int
|
||||||
|
The index of the chromosome in the population.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
convert_fitness(float) -> float
|
||||||
|
A method for redefining the fitness value.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
fitness : float
|
||||||
|
The converted fitness value.
|
||||||
|
"""
|
||||||
return self.convert_fitness(self.population[index].fitness)
|
return self.convert_fitness(self.population[index].fitness)
|
||||||
|
|
||||||
|
|
||||||
def convert_fitness(self, fitness_value):
|
def convert_fitness(self: GA, fitness: float) -> float:
|
||||||
"""Returns the fitness value if the type of problem
|
"""
|
||||||
is a maximization problem. Otherwise the fitness is
|
Calculates a modified version of the fitness for various
|
||||||
inverted using max - value + min.
|
methods, which assume the fitness should be maximized.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
fitness : float
|
||||||
|
The fitness value to be changed.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
target_fitness_type : str in ('max', 'min')
|
||||||
|
The way the chromosomes should be sorted.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
fitness : float
|
||||||
|
Unchanged if the fitness is already being maximized.
|
||||||
|
max_fitness - fitness + min_fitness : float
|
||||||
|
The fitness flipped if the fitness is being minimized.
|
||||||
|
|
||||||
|
Requires
|
||||||
|
--------
|
||||||
|
The population must be sorted already, and the fitnesses can't be None.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# No conversion needed
|
# No conversion needed
|
||||||
if self.target_fitness_type == 'max': return fitness_value
|
if self.target_fitness_type == 'max':
|
||||||
|
return fitness
|
||||||
|
|
||||||
max_fitness = self.population[-1].fitness
|
max_fitness = self.population[-1].fitness
|
||||||
min_fitness = self.population[0].fitness
|
min_fitness = self.population[0].fitness
|
||||||
|
|
||||||
return max_fitness - fitness_value + min_fitness
|
return max_fitness - fitness + min_fitness
|
||||||
|
|
||||||
|
|
||||||
def print_generation(self):
|
def print_generation(self: GA) -> None:
|
||||||
"""Prints the current generation"""
|
"""Prints the current generation."""
|
||||||
print(f"Current Generation \t: {self.current_generation}")
|
print(f"Current Generation \t: {self.current_generation}")
|
||||||
|
|
||||||
|
|
||||||
def print_population(self):
|
def print_population(self: GA) -> None:
|
||||||
"""Prints the entire population"""
|
"""Prints the entire population."""
|
||||||
print(self.population)
|
print(self.population)
|
||||||
|
|
||||||
|
|
||||||
def print_best_chromosome(self):
|
def print_best_chromosome(self: GA) -> None:
|
||||||
"""Prints the best chromosome and its fitness"""
|
"""Prints the best chromosome and its fitness."""
|
||||||
print(f"Best Chromosome \t: {self.population[0]}")
|
print(f"Best Chromosome \t: {self.population[0]}")
|
||||||
print(f"Best Fitness \t: {self.population[0].fitness}")
|
print(f"Best Fitness \t: {self.population[0].fitness}")
|
||||||
|
|
||||||
|
|
||||||
def print_worst_chromosome(self):
|
def print_worst_chromosome(self: GA) -> None:
|
||||||
"""Prints the worst chromosome and its fitness"""
|
"""Prints the worst chromosome and its fitness."""
|
||||||
print(f"Worst Chromosome \t: {self.population[-1]}")
|
print(f"Worst Chromosome \t: {self.population[-1]}")
|
||||||
print(f"Worst Fitness \t: {self.population[-1].fitness}")
|
print(f"Worst Fitness \t: {self.population[-1].fitness}")
|
||||||
|
|||||||
Reference in New Issue
Block a user