Improved documentation, type-hints, and dataclass

This commit is contained in:
SimpleArt
2021-05-06 17:53:22 -04:00
parent b4de63eb48
commit 63c8dc36d2

View File

@ -1,228 +1,102 @@
# Import signature tool to check if functions start with self or ga from __future__ import annotations
from inspect import signature from inspect import signature
from typing import Callable, Optional, Iterable, Any, Dict
# Import math for square root (ga.dist()) and ceil (crossover methods) from math import sqrt, ceil
import math from dataclasses import dataclass, field
from functools import wraps
import random import random
import sqlite3 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 import matplotlib.pyplot as plt
from structure import Population
from structure import Chromosome
from structure import Gene
from examples import Fitness
from termination import Termination
from parent import Parent
from survivor import Survivor
from crossover import Crossover
from mutation import Mutation
from database import sql_database, matplotlib_graph
@dataclass
class Attributes: 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 class which stores all attributes in a dataclass.
attributes have been catigorized to explain sections in the ga process.""" Contains default attributes for each attribute.
"""
#=====================# properties: Dict[str, Any] = field(default_factory=dict, init=False, repr=False, compare=False)
# Default GA methods: #
#=====================#
# Default EasyGA implimentation structure run: int = 0
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 chromosome_length: int = 10
parent_selection_impl = Parent.Rank.tournament population_size: int = 10
crossover_individual_impl = Crossover.Individual.single_point population: Optional[Population] = None
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
target_fitness_type: str = 'max'
update_fitness: bool = False
def dist(self, chromosome_1, chromosome_2): parent_ratio: float = 0.1
"""Default distance lambda. Returns the square root of the difference in fitnesses.""" selection_probability: float = 0.5
return math.sqrt(abs(chromosome_1.fitness - chromosome_2.fitness)) tournament_size_ratio: float = 0.1
current_generation: int = 0
generation_goal: int = 100
fitness_goal: Optional[float] = None
tolerance_goal: Optional[float] = None
percentage_converged: float = 0.5
def weighted_random(self, weight): chromosome_mutation_rate: float = 0.15
"""Returns a random value between 0 and 1. Returns values between the weight and the gene_mutation_rate: float = 0.05
nearest of 0 and 1 less frequently than between weight and the farthest of 0 and 1."""
rand_num = random.random() adapt_rate: float = 0.05
if rand_num < weight: adapt_probability_rate: float = 0.05
return (1-weight) * rand_num / weight adapt_population_flag: bool = True
else:
return 1 - weight * (1-rand_num) / (1-weight)
max_selection_probability: float = 0.75
min_selection_probability: float = 0.25
max_chromosome_mutation_rate: float = None
min_chromosome_mutation_rate: float = None
max_gene_mutation_rate: float = 0.15
min_gene_mutation_rate: float = 0.01
def gene_impl(self, *args, **kwargs): fitness_function_impl: Callable[[Attributes, Chromosome], float] = Fitness.is_it_5
"""Default gene implementation. Returns a random integer from 1 to 10.""" make_population: Callable[[Iterable[Iterable[Any]]], Population] = Population
return random.randint(1, 10) make_chromosome: Callable[[Iterable[Any]], Chromosome] = Chromosome
make_gene: Callable[[Any], Gene] = Gene
gene_impl: Callable[[Attributes], Any] = field(default_factory=lambda: rand_1_to_10)
chromosome_impl: Optional[[Attributes], Iterable[Any]] = field(default_factory=lambda: use_genes)
population_impl: Optional[[Attributes], Iterable[Iterable[Any]]] = field(default_factory=lambda: use_chromosomes)
chromosome_impl = None weighted_random: Callable[[Attributes, float], float] = field(default_factory=lambda: simple_linear)
dist: Callable[[Attributes, Chromosome, Chromosome], float] = field(default_factory=lambda: dist_fitness)
parent_selection_impl: Callable[[Attributes], None] = Parent.Rank.tournament
crossover_individual_impl: Callable[[Attributes], None] = Crossover.Individual.single_point
crossover_population_impl: Callable[[Attributes], None] = Crossover.Population.sequential
survivor_selection_impl: Callable[[Attributes], None] = Survivor.fill_in_best
mutation_individual_impl: Callable[[Attributes], None] = Mutation.Individual.individual_genes
mutation_population_impl: Callable[[Attributes], None] = Mutation.Population.random_avoid_best
termination_impl: Callable[[Attributes], None] = Termination.fitness_generation_tolerance
#=====================================# database: Database = sql_database.SQL_Database
# Special built-in class __methods__: # database_name: str = 'database.db'
#=====================================# sql_create_data_structure: str = """
CREATE TABLE IF NOT EXISTS data (
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,
save_data = True,
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, id INTEGER PRIMARY KEY,
config_id INTEGER DEFAULT NULL, config_id INTEGER DEFAULT NULL,
generation INTEGER NOT NULL, generation INTEGER NOT NULL,
fitness REAL, fitness REAL,
chromosome TEXT chromosome TEXT
); """, );
"""
Graph = matplotlib_graph.Matplotlib_Graph, graph: Callable[[Database], 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
self.save_data = save_data
# 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
#============================# #============================#
@ -230,206 +104,338 @@ class Attributes:
#============================# #============================#
def save_population(self): def save_population(self: Attributes) -> None:
"""Saves the current population to the database.""" """Saves the current population to the database."""
self.database.insert_current_population(self) self.database.insert_current_population(self)
def save_chromosome(self, chromosome): def save_chromosome(self: Attributes, chromosome: Chromosome) -> None:
"""Saves the given chromosome to the database.""" """
Saves a chromosome to the database.
Parameters
----------
chromosome : Chromosome
The chromosome to be saved.
"""
self.database.insert_current_chromosome(self.current_generation, chromosome) self.database.insert_current_chromosome(self.current_generation, chromosome)
#===================# def rand_1_to_10(self: Attributes) -> int:
# Built-in options: # """
#===================# Default gene_impl, returning a random integer from 1 to 10.
Returns
-------
rand : int
A random integer between 1 and 10, inclusive.
"""
return random.randint(1, 10)
def numeric_chromosomes(self): def use_genes(self: Attributes) -> Iterable[Any]:
"""Sets default numerical based methods""" """
Default chromosome_impl, generates a chromosome using the gene_impl and chromosome length.
# Adapt every 10th generation Attributes
self.adapt_rate = 0.10 ----------
gene_impl() -> Any
A gene implementation.
chromosome_length : int
The length of a chromosome.
# Use averaging for crossover Returns
self.crossover_individual_impl = Crossover.Individual.Arithmetic.average -------
chromosome : Iterable[Any]
# Use averaging for mutation Generates the genes for a chromosome.
self.mutation_individual_impl = Mutation.Individual.individual_genes """
for _ in range(self.chromosome_length):
# Euclidean norm yield self.gene_impl()
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): def use_chromosomes(self: Attributes) -> Iterable[Any]:
"""Sets default permutation based methods""" """
Default population_impl, generates a population using the chromosome_impl and population size.
cycle = int(cycle) Attributes
----------
chromosome_impl() -> Any
A chromosome implementation.
population_size : int
The size of the population.
self.crossover_individual_impl = Crossover.Individual.Permutation.ox1 Returns
self.mutation_individual_impl = Mutation.Individual.Permutation.swap_genes -------
population : Iterable[Iterable[Any]]
def dist(self, chromosome_1, chromosome_2): Generates the chromosomes for a population.
"""Count the number of gene pairs they don't have in common.""" """
for _ in range(self.population_size):
return sum( yield self.chromosome_impl()
1
for x, y
in zip(chromosome_1, chromosome_2)
if x != y
)
self.dist = dist
#===========================# def dist_fitness(self: Attributes, chromosome_1: Chromosome, chromosome_2: Chromosome) -> float:
# Getter/setter properties: # """
#===========================# Measures the distance between two chromosomes based on their fitnesses.
Parameters
----------
chromosome_1, chromosome_2 : Chromosome
Chromosomes being compared.
Returns
-------
dist : float
The distance between the two chromosomes.
"""
return sqrt(abs(chromosome_1.fitness - chromosome_2.fitness))
@property def simple_linear(self: Attributes, weight: float) -> float:
def run(self): """
"""Getter function for the run counter.""" Returns a random value between 0 and 1, with increased probability
return self._run closer towards the side with weight.
Parameters
----------
weight : float
A float between 0 and 1 which determines the output distribution.
Returns
-------
rand : float
A random value between 0 and 1.
"""
rand = random.random()
if rand < weight:
return rand * (1-weight) / weight
else:
return 1 - (1-rand) * weight / (1-weight)
@run.setter #==================================================#
def run(self, value): # Properties for attributes behaving like methods. #
"""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 get_method(name: str) -> Callable[[Attributes], Callable[..., Any]]:
def current_generation(self): """
"""Getter function for the current generation.""" Creates a getter method for getting a method from the Attributes class.
return self._current_generation
Parameters
----------
name : str
The name of the method from Attributes.
Returns
-------
getter(ga)(...) -> Any
The getter property, taking in an object and returning the method.
"""
def getter(self: Attributes) -> Callable[..., Any]:
return self.properties[name]
return getter
@current_generation.setter def set_method(name: str) -> Callable[[Attributes, Optional[Callable[..., Any]]], None]:
def current_generation(self, generation): """
"""Setter function for the current generation.""" Creates a setter method for setting a method from the Attributes class.
if not isinstance(generation, int) or generation < 0: Parameters
raise ValueError("ga.current_generation must be an integer greater than or equal to 0") ----------
name : str
The name of the method from Attributes.
self._current_generation = generation Returns
-------
setter(ga, method)
The setter property, taking in an object and returning nothing.
"""
def setter(self: Attributes, method: Optional[Callable[..., Any]]) -> None:
if method is None:
pass
elif not callable(method):
raise TypeError(f"{name} must be a method i.e. callable.")
elif next(iter(signature(method).parameters), None) in ("self", "ga"):
method = wraps(method)(lambda *args, **kwargs: method(self, *args, **kwargs))
self.properties[name] = method
return setter
@property for name in (
def chromosome_length(self): "fitness_function_impl",
"""Getter function for chromosome length""" "parent_selection_impl",
return self._chromosome_length "crossover_individual_impl",
"crossover_population_impl",
"survivor_selection_impl",
"mutation_individual_impl",
"mutation_population_impl",
"termination_impl",
"dist",
"weighted_random",
"gene_impl",
"chromosome_impl",
):
setattr(Attributes, name, property(get_method(name), set_method(name)))
@chromosome_length.setter #============================#
def chromosome_length(self, length): # Static checking properties #
"""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 static_checks = {
def population_size(self): "run": {
"""Getter function for population size""" "check": lambda value: isinstance(value, int) and value >= 0,
"error": "ga.run counter must be an integer greater than or equal to 0.",
return self._population_size },
"current_generation": {
"check": lambda value: isinstance(value, int) and value >= 0,
"error": "ga.current_generation must be an integer greater than or equal to 0",
},
"chromosome_length": {
"check": lambda value: isinstance(value, int) and value > 0,
"error": "ga.chromosome_length must be an integer greater than and not equal to 0.",
},
"population_size": {
"check": lambda value: isinstance(value, int) and value > 0,
"error": "ga.population_size must be an integer greater than and not equal to 0.",
},
}
@population_size.setter def get_attr(name: str) -> Callable[[Attributes], Any]:
def population_size(self, size): """
"""Setter function with error checking for population size""" Creates a getter method for getting an attribute from the Attributes class.
if(not isinstance(size, int) or size <= 0): Parameters
raise ValueError("Population size must be integer greater than 0") ----------
name : str
The name of the attribute.
self._population_size = size Returns
-------
getter(ga) -> Any
A getter method which returns an attribute.
"""
def getter(self: Attributes) -> Any:
return self.properties[name]
return getter
@property def set_attr(name: str, check: Callable[[Any], bool], error: str) -> Callable[[Attributes, Any], None]:
def target_fitness_type(self): """
"""Getter function for target fitness type.""" Creates a setter method for setting an attribute from the Attributes class.
return self._target_fitness_type Parameters
----------
name : str
The name of the attribute.
check(Any) -> bool
The condition needed to be passed for the attribute to be added.
error: str
An error message if check(...) turns False.
Returns
@target_fitness_type.setter -------
def target_fitness_type(self, target_fitness_type): setter(ga, Any) -> None
"""Setter function for target fitness type.""" Raises ValueError(error)
A setter method which saves to an attribute.
self._target_fitness_type = target_fitness_type """
def setter(self: Attributes, value: Any) -> Any:
if check(value):
@property self.properties[name] = value
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: else:
raise ValueError("Max chromosome mutation rate must be between 0 and 1") raise ValueError(error)
return setter
@property for name in static_checks:
def min_chromosome_mutation_rate(self): setattr(
"""Getter function for min chromosome mutation rate""" Attributes,
name,
return self._min_chromosome_mutation_rate property(
get_attr(name),
set_attr(name, static_checks[name]["check"], static_checks[name]["error"]),
)
)
@min_chromosome_mutation_rate.setter #==================#
def min_chromosome_mutation_rate(self, rate): # Other properties #
"""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 get_max_chromosome_mutation_rate(self: Attributes) -> float:
def database_name(self): return self._max_chromosome_mutation_rate
"""Getter function for the database name"""
return self._database_name
@database_name.setter def set_max_chromosome_mutation_rate(self: Attributes, value: Optional[float]) -> None:
def database_name(self, value_input):
"""Setter function with error checking for the database name"""
# Update the database class of the name change # Default value
self.database._database_name = value_input if value is None:
self._max_chromosome_mutation_rate = min(
self.chromosome_mutation_rate * 2,
(self.chromosome_mutation_rate + 1) / 2,
)
# Set the name in the ga attribute # Otherwise check value
self._database_name = value_input elif isinstance(value, (float, int)) and 0 <= value <= 1:
self._max_chromosome_mutation_rate = value
# Raise error
else:
raise ValueError("Max chromosome mutation rate must be between 0 and 1")
def get_min_chromosome_mutation_rate(self: Attributes) -> float:
return self._min_chromosome_mutation_rate
def set_min_chromosome_mutation_rate(self: Attributes, value: Optional[float]) -> None:
# Default value
if value is None:
self._min_chromosome_mutation_rate = max(
self.chromosome_mutation_rate / 2,
self.chromosome_mutation_rate * 2 - 1,
)
# Otherwise check value
elif isinstance(value, (float, int)) and 0 <= value <= 1:
self._min_chromosome_mutation_rate = value
# Raise error
else:
raise ValueError("Min chromosome mutation rate must be between 0 and 1")
def get_database_name(self: Attributes) -> str:
return self._database_name
def set_database_name(self: Attributes, name: str) -> None:
# Update the database class' name
self.database._database_name = name
# Set the attribute for itself
self._database_name = name
def get_graph(self: Attributes) -> Graph:
return self._graph
def set_graph(self: Attributes, graph: Callable[[Database], Graph]) -> None:
self._graph = graph(self.database)
def get_active(self: Attributes) -> Callable[[Attributes], None]:
return self.termination_impl
Attributes.max_chromosome_mutation_rate = property(get_max_chromosome_mutation_rate, set_max_chromosome_mutation_rate)
Attributes.min_chromosome_mutation_rate = property(get_min_chromosome_mutation_rate, set_min_chromosome_mutation_rate)
Attributes.database_name = property(get_database_name, set_database_name)
Attributes.graph = property(get_graph, set_graph)
Attributes.active = property(get_active)