Improved documentation

This commit is contained in:
SimpleArt
2021-05-06 17:55:15 -04:00
parent 63c8dc36d2
commit 71d2658501

View File

@ -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,8 +93,7 @@ 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
# passes through an integer value. # passes through an integer value.
@ -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}")