Attempted to reimport from egg

This commit is contained in:
2023-01-02 20:00:29 +01:00
parent a107bdf69e
commit b9d8dbe0a2
22 changed files with 577 additions and 587 deletions

View File

@ -2,38 +2,38 @@ from __future__ import annotations
from typing import Optional, MutableSequence, Iterable
# Import all decorators
import decorators
import EasyGA.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
from structure import Population
from structure import Chromosome
from structure import Gene
from EasyGA.structure import Population as make_population
from EasyGA.structure import Chromosome as make_chromosome
from EasyGA.structure import Gene as make_gene
from EasyGA.structure import Population
from EasyGA.structure import Chromosome
from EasyGA.structure import Gene
# Misc. Methods
from examples import Fitness
from termination import Termination
from EasyGA.examples import Fitness
from EasyGA.termination import Termination
# Parent/Survivor Selection Methods
from parent import Parent
from survivor import Survivor
from EasyGA.parent import Parent
from EasyGA.survivor import Survivor
# Genetic Operator Methods
from crossover import Crossover
from mutation import Mutation
from EasyGA.crossover import Crossover
from EasyGA.mutation import Mutation
# Default Attributes for the GA
from attributes import Attributes
from EasyGA.attributes import Attributes
# Database class
# from database import SQLDatabase
# from sqlite3 import Error
from EasyGA.database import sql_database
from sqlite3 import Error
# Graphing package
# from database import MatplotlibGraph
# import matplotlib.pyplot as plt
from EasyGA.database import matplotlib_graph
import matplotlib.pyplot as plt
class GA(Attributes):
@ -46,6 +46,7 @@ class GA(Attributes):
https://github.com/danielwilczak101/EasyGA/wiki
"""
def evolve(self: GA, number_of_generations: float = float('inf'), consider_termination: bool = True) -> None:
"""
Evolves the ga until the ga is no longer active.
@ -62,12 +63,9 @@ class GA(Attributes):
if self.population is None:
self.initialize_population()
# Evolve the specified number of generations.
def cond1(): return number_of_generations > 0
# If consider_termination flag is set:
def cond2(): return not consider_termination
# check termination conditions.
def cond3(): return cond2() or self.active()
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():
@ -76,11 +74,10 @@ class GA(Attributes):
# 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)
self.database.create_all_tables(self)
# Add the current configuration to the config table
# self.database.insert_config(self)
pass
self.database.insert_config(self)
# Otherwise evolve the population.
else:
@ -96,7 +93,6 @@ class GA(Attributes):
self.sort_by_best_fitness()
# Save the population to the database
if self.save_data:
self.save_population()
# Adapt the ga if the generation times the adapt rate
@ -108,6 +104,7 @@ class GA(Attributes):
number_of_generations -= 1
self.current_generation += 1
def update_population(self: GA) -> None:
"""
Updates the population to the new population
@ -115,6 +112,7 @@ class GA(Attributes):
"""
self.population.update()
def reset_run(self: GA) -> None:
"""
Resets a run by re-initializing the
@ -124,6 +122,7 @@ class GA(Attributes):
self.current_generation = 0
self.run += 1
def adapt(self: GA) -> None:
"""Adapts the ga to hopefully get better results."""
@ -134,6 +133,7 @@ class GA(Attributes):
self.set_all_fitness()
self.sort_by_best_fitness()
def adapt_probabilities(self: GA) -> None:
"""
Modifies the parent ratio and mutation rates based on the adapt
@ -153,7 +153,7 @@ class GA(Attributes):
# Difference between best and i-th chromosomes
best_chromosome = self.population[0]
def tol(i): return self.dist(best_chromosome, self.population[i])
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:
@ -168,15 +168,14 @@ class GA(Attributes):
self.max_gene_mutation_rate)
# Weighted average of x and y
def average(x, y): return weight * x + (1-weight) * 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.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: GA) -> None:
"""
Performs weighted crossover between the best chromosome and
@ -218,19 +217,20 @@ class GA(Attributes):
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))
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: GA) -> None:
"""
Sets self.population using the chromosome implementation and population size.
"""
self.population = self.make_population(self.population_impl())
def set_all_fitness(self: GA) -> None:
"""
Sets the fitness of each chromosome in the population.
@ -251,6 +251,7 @@ class GA(Attributes):
if chromosome.fitness is None or self.update_fitness:
chromosome.fitness = self.fitness_function_impl(chromosome)
def sort_by_best_fitness(
self: GA,
chromosome_list: Optional[
@ -313,6 +314,7 @@ class GA(Attributes):
else:
return sorted(chromosome_list, key=key, reverse=reverse)
def get_chromosome_fitness(self: GA, index: int) -> float:
"""
Computes the converted fitness of a chromosome at an index.
@ -336,6 +338,7 @@ class GA(Attributes):
"""
return self.convert_fitness(self.population[index].fitness)
def convert_fitness(self: GA, fitness: float) -> float:
"""
Calculates a modified version of the fitness for various
@ -372,19 +375,23 @@ class GA(Attributes):
return max_fitness - fitness + min_fitness
def print_generation(self: GA) -> None:
"""Prints the current generation."""
print(f"Current Generation \t: {self.current_generation}")
def print_population(self: GA) -> None:
"""Prints the entire population."""
print(self.population)
def print_best_chromosome(self: GA) -> None:
"""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: GA) -> None:
"""Prints the worst chromosome and its fitness."""
print(f"Worst Chromosome \t: {self.population[-1]}")

View File

@ -0,0 +1,2 @@
import EasyGA
from .EasyGA import GA

View File

@ -1,29 +1,124 @@
from __future__ import annotations
from inspect import getmro, signature
from typing import Any, Callable, Dict, Iterable, Iterator, Optional
from inspect import signature
from typing import Callable, Optional, Iterable, Any, Dict
from math import sqrt, ceil
from dataclasses import dataclass, field, _MISSING_TYPE
from dataclasses import dataclass, field
from types import MethodType
import random
# import sqlite3
# import matplotlib.pyplot as plt
import sqlite3
import matplotlib.pyplot as plt
from structure import Population
from structure import Chromosome
from structure import Gene
from EasyGA.structure import Population
from EasyGA.structure import Chromosome
from EasyGA.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 SQLDatabase, MatplotlibGraph, SQLDatabase as Database, MatplotlibGraph as Graph
from EasyGA.examples import Fitness
from EasyGA.termination import Termination
from EasyGA.parent import Parent
from EasyGA.survivor import Survivor
from EasyGA.crossover import Crossover
from EasyGA.mutation import Mutation
from EasyGA.database import sql_database, matplotlib_graph
#========================================#
# Default methods not defined elsewhere. #
#========================================#
@dataclass
class Attributes:
"""
Attributes class which stores all attributes in a dataclass.
Contains default attributes for each attribute.
"""
properties: Dict[str, Any] = field(default_factory=dict, init=False, repr=False, compare=False)
run: int = 0
chromosome_length: int = 10
population_size: int = 10
population: Optional[Population] = None
target_fitness_type: str = 'max'
update_fitness: bool = False
parent_ratio: float = 0.1
selection_probability: float = 0.5
tournament_size_ratio: float = 0.1
current_generation: int = 0
generation_goal: int = 100
fitness_goal: Optional[float] = None
tolerance_goal: Optional[float] = None
percent_converged: float = 0.5
chromosome_mutation_rate: float = 0.15
gene_mutation_rate: float = 0.05
adapt_rate: float = 0.05
adapt_probability_rate: float = 0.05
adapt_population_flag: bool = True
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
fitness_function_impl: Callable[[Attributes, Chromosome], float] = Fitness.is_it_5
make_population: Callable[[Iterable[Iterable[Any]]], Population] = Population
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)
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 = field(default_factory=sql_database.SQL_Database)
database_name: str = 'database.db'
sql_create_data_structure: str = """
CREATE TABLE IF NOT EXISTS data (
id INTEGER PRIMARY KEY,
config_id INTEGER DEFAULT NULL,
generation INTEGER NOT NULL,
fitness REAL,
chromosome TEXT
);
"""
graph: Callable[[Database], Graph] = matplotlib_graph.Matplotlib_Graph
#============================#
# Built-in database methods: #
#============================#
def save_population(self: Attributes) -> None:
"""Saves the current population to the database."""
self.database.insert_current_population(self)
def save_chromosome(self: Attributes, chromosome: Chromosome) -> None:
"""
Saves a chromosome to the database.
Parameters
----------
chromosome : Chromosome
The chromosome to be saved.
"""
self.database.insert_current_chromosome(self.current_generation, chromosome)
def rand_1_to_10(self: Attributes) -> int:
@ -38,7 +133,7 @@ def rand_1_to_10(self: Attributes) -> int:
return random.randint(1, 10)
def use_genes(self: Attributes) -> Iterator[Any]:
def use_genes(self: Attributes) -> Iterable[Any]:
"""
Default chromosome_impl, generates a chromosome using the gene_impl and chromosome length.
@ -51,14 +146,14 @@ def use_genes(self: Attributes) -> Iterator[Any]:
Returns
-------
chromosome : Iterator[Any]
chromosome : Iterable[Any]
Generates the genes for a chromosome.
"""
for _ in range(self.chromosome_length):
yield self.gene_impl()
def use_chromosomes(self: Attributes) -> Iterator[Iterable[Any]]:
def use_chromosomes(self: Attributes) -> Iterable[Any]:
"""
Default population_impl, generates a population using the chromosome_impl and population size.
@ -71,7 +166,7 @@ def use_chromosomes(self: Attributes) -> Iterator[Iterable[Any]]:
Returns
-------
population : Iterator[Iterable[Any]]
population : Iterable[Iterable[Any]]
Generates the chromosomes for a population.
"""
for _ in range(self.population_size):
@ -117,339 +212,231 @@ def simple_linear(self: Attributes, weight: float) -> float:
return 1 - (1-rand) * weight / (1-weight)
@dataclass
class AttributesData:
#==================================================#
# Properties for attributes behaving like methods. #
#==================================================#
def get_method(name: str) -> Callable[[Attributes], Callable[..., Any]]:
"""
Attributes class which stores all attributes in a dataclass.
This includes type-hints/annotations and default values, except for methods.
Additionally gains dataclass features, including an __init__ and __repr__ to avoid boilerplate code.
Developer Notes:
See the Attributes class for default methods.
Override this class to set default attributes. See help(Attributes) for more information.
If you must override the __post_init__, don't forget to use super().__post_init__().
"""
run: int = 0
chromosome_length: int = 10
population_size: int = 10
population: Optional[Population] = None
target_fitness_type: str = 'max'
update_fitness: bool = False
parent_ratio: float = 0.1
selection_probability: float = 0.5
tournament_size_ratio: float = 0.1
current_generation: int = 0
generation_goal: int = 100
fitness_goal: Optional[float] = None
tolerance_goal: Optional[float] = None
percent_converged: float = 0.5
chromosome_mutation_rate: float = 0.15
gene_mutation_rate: float = 0.05
adapt_rate: float = 0.05
adapt_probability_rate: float = 0.05
adapt_population_flag: bool = True
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
#=================================#
# Default methods are implemented #
# in the Attributes descriptors: #
#=================================#
fitness_function_impl: Callable[["Attributes", Chromosome], float] = None
make_gene: Callable[[Any], Gene] = None
make_chromosome: Callable[[Iterable[Any]], Chromosome] = None
make_population: Callable[[Iterable[Iterable[Any]]], Population] = None
gene_impl: Callable[[], Any] = None
chromosome_impl: Callable[[], Iterable[Any]] = None
population_impl: Callable[[], Iterable[Iterable[Any]]] = None
weighted_random: Callable[[float], float] = None
dist: Callable[["Attributes", Chromosome, Chromosome], None] = None
parent_selection_impl: Callable[["Attributes"], None] = None
crossover_individual_impl: Callable[["Attributes"], None] = None
crossover_population_impl: Callable[[
"Attributes", Chromosome, Chromosome], None] = None
survivor_selection_impl: Callable[["Attributes"], None] = None
mutation_individual_impl: Callable[["Attributes", Chromosome], None] = None
mutation_population_impl: Callable[["Attributes"], None] = None
termination_impl: Callable[["Attributes"], bool] = None
# database: Database = field(default_factory=SQLDatabase)
#database_name: str = "database.db"
#save_data: bool = True
# sql_create_data_structure: str = """
# CREATE TABLE IF NOT EXISTS data (
# id INTEGER PRIMARY KEY,
# config_id INTEGER DEFAULT NULL,
# generation INTEGER NOT NULL,
# fitness REAL,
# chromosome TEXT
# );
# """
# graph: Callable[[Database], Graph] = MatplotlibGraph
def __post_init__(self: AttributesData) -> None:
"""
Undo any instance attributes that are None when they should be methods from the class.
Attributes here refers to the __dataclass_fields__.
Methods here refers to AsMethod descriptors on any of the super classes of self's class.
"""
def is_method(cls: type, name: str) -> bool:
"""
The class has the attribute `name` as a method if:
- it has the attribute,
- and it's the AsMethod descriptor.
"""
return hasattr(cls, name) and isinstance(getattr(cls, name), AsMethod)
# Check each dataclass attribute.
for name in self.__dataclass_fields__:
# If the instance attribute is None
# and any of the super classes has that as a method,
# then delete the None instance attribute.
if (
getattr(self, name) is None
and any(is_method(cls, name) for cls in getmro(type(self)))
):
delattr(self, name)
class AsMethod:
"""
A descriptor for converting function attributes into bound methods.
To support both inheritance and dataclasses, if the method is None,
then nothing is set.
"""
def __init__(self: AsMethod, name: str, default: Callable) -> None:
if not callable(default):
raise TypeError(f"'default' must be a method i.e. callable.")
self.name = name
self.default = default
def __get__(self: AsMethod, obj: "Attributes", cls: type) -> Callable:
# Already has the attribute on the object.
if self.name in vars(obj):
return vars(obj)[self.name]
# Otherwise use the default as a method.
if next(iter(signature(self.default).parameters), None) in ("self", "ga"):
return MethodType(self.default, obj)
# Otherwise use the default as a function.
return self.default
def __set__(self: AsMethod, obj: "Attributes", method: Optional[Callable]) -> None:
if method is None:
return
elif not callable(method):
raise TypeError(f"'{self.name}' must be a method i.e. callable.")
elif next(iter(signature(method).parameters), None) in ("self", "ga"):
method = MethodType(method, obj)
vars(obj)[self.name] = method
def __delete__(self: AsMethod, obj: "Attributes") -> None:
del vars(obj)[self.name]
class Attributes(AttributesData):
"""
The Attributes class inherits default attributes from AttributesData
and implements methods, descriptors, and properties.
The built-in methods provide interfacing to the database.
>>> ga.save_population() # references ga.database.insert_current_population(ga)
The descriptors are used to convert function attributes into methods.
>>> ga.gene_impl = lambda self: ... # self is turned into an implicit argument.
The properties are used to validate certain inputs.
Developer Notes:
If inherited, the descriptors may be overridden with a method implementation,
but this removes the descriptor.
To override default attributes, we recommend creating a dataclass inheriting AttributesData.
Then inherit the Attributes and AttributesDataSubclass, in that order.
>>> from dataclasses import dataclass
>>> @dataclass
>>> class MyDefaults(AttributesData):
... run: int = 10
...
>>> class MyAttributes(Attributes, MyDefaults):
... pass
...
"""
#============================#
# Built-in database methods: #
#============================#
def save_population(self: Attributes) -> None:
"""Saves the current population to the database."""
self.database.insert_current_population(self)
def save_chromosome(self: Attributes, chromosome: Chromosome) -> None:
"""
Saves a chromosome to the database.
Creates a getter method for getting a method from the Attributes class.
Parameters
----------
chromosome : Chromosome
The chromosome to be saved.
name : str
The name of the method from Attributes.
Returns
-------
getter(ga)(...) -> Any
The getter property, taking in an object and returning the method.
"""
self.database.insert_current_chromosome(
self.current_generation, chromosome)
def getter(self: Attributes) -> Callable[..., Any]:
return self.properties[name]
return getter
#===========================#
# Descriptors which convert #
# functions into methods: #
#===========================#
fitness_function_impl = AsMethod("fitness_function_impl", Fitness.is_it_5)
make_gene = AsMethod("make_gene", Gene)
make_chromosome = AsMethod("make_chromosome", Chromosome)
make_population = AsMethod("make_population", Population)
gene_impl = AsMethod("gene_impl", rand_1_to_10)
chromosome_impl = AsMethod("chromosome_impl", use_genes)
population_impl = AsMethod("population_impl", use_chromosomes)
dist = AsMethod("dist", dist_fitness)
weighted_random = AsMethod("weighted_random", simple_linear)
parent_selection_impl = AsMethod(
"parent_selection_impl", Parent.Rank.tournament)
crossover_individual_impl = AsMethod(
"crossover_individual_impl", Crossover.Individual.single_point)
crossover_population_impl = AsMethod(
"crossover_population_impl", Crossover.Population.sequential)
survivor_selection_impl = AsMethod(
"survivor_selection_impl", Survivor.fill_in_best)
mutation_individual_impl = AsMethod(
"mutation_individual_impl", Mutation.Individual.individual_genes)
mutation_population_impl = AsMethod(
"mutation_population_impl", Mutation.Population.random_avoid_best)
termination_impl = AsMethod(
"termination_impl", Termination.fitness_generation_tolerance)
def set_method(name: str) -> Callable[[Attributes, Optional[Callable[..., Any]]], None]:
"""
Creates a setter method for setting a method from the Attributes class.
#=============#
# Properties: #
#=============#
Parameters
----------
name : str
The name of the method from Attributes.
@property
def run(self: AttributesProperties) -> int:
return vars(self)["run"]
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 = MethodType(method, self)
self.properties[name] = method
return setter
@run.setter
def run(self: AttributesProperties, value: int) -> None:
if not isinstance(value, int) or value < 0:
raise ValueError(
"ga.run counter must be an integer greater than or equal to 0.")
vars(self)["run"] = value
@property
def current_generation(self: AttributesProperties) -> int:
return vars(self)["current_generation"]
for name in (
"fitness_function_impl",
"parent_selection_impl",
"crossover_individual_impl",
"crossover_population_impl",
"survivor_selection_impl",
"mutation_individual_impl",
"mutation_population_impl",
"termination_impl",
"dist",
"weighted_random",
"gene_impl",
"chromosome_impl",
"population_impl",
):
setattr(Attributes, name, property(get_method(name), set_method(name)))
@current_generation.setter
def current_generation(self: AttributesProperties, value: int) -> None:
if not isinstance(value, int) or value < 0:
raise ValueError(
"ga.current_generation must be an integer greater than or equal to 0")
vars(self)["current_generation"] = value
@property
def chromosome_length(self: AttributesProperties) -> int:
return vars(self)["chromosome_length"]
#============================#
# Static checking properties #
#============================#
@chromosome_length.setter
def chromosome_length(self: AttributesProperties, value: int) -> None:
if not isinstance(value, int) or value <= 0:
raise ValueError(
"ga.chromosome_length must be an integer greater than and not equal to 0.")
vars(self)["chromosome_length"] = value
@property
def population_size(self: AttributesProperties) -> int:
return vars(self)["population_size"]
static_checks = {
"run": {
"check": lambda value: isinstance(value, int) and value >= 0,
"error": "ga.run counter must be an integer greater than or equal to 0.",
},
"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 population_size(self: AttributesProperties, value: int) -> None:
if not isinstance(value, int) or value <= 0:
raise ValueError(
"ga.population_size must be an integer greater than and not equal to 0.")
vars(self)["population_size"] = value
@property
def max_chromosome_mutation_rate(self: AttributesProperties) -> float:
# Default value.
if vars(self).get("max_chromosome_mutation_rate", None) is None:
return min(self.chromosome_mutation_rate * 2, (self.chromosome_mutation_rate + 1) / 2)
# Set value.
return vars(self)["max_chromosome_mutation_rate"]
def get_attr(name: str) -> Callable[[Attributes], Any]:
"""
Creates a getter method for getting an attribute from the Attributes class.
@max_chromosome_mutation_rate.setter
def max_chromosome_mutation_rate(self: AttributesProperties, value: Optional[float]) -> None:
# Use default or a valid float.
if value is None or (isinstance(value, (float, int)) and 0 <= value <= 1):
vars(self)["max_chromosome_mutation_rate"] = value
Parameters
----------
name : str
The name of the attribute.
Returns
-------
getter(ga) -> Any
A getter method which returns an attribute.
"""
def getter(self: Attributes) -> Any:
return self.properties[name]
return getter
def set_attr(name: str, check: Callable[[Any], bool], error: str) -> Callable[[Attributes, Any], None]:
"""
Creates a setter method for setting an attribute from the Attributes class.
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
-------
setter(ga, Any) -> None
Raises ValueError(error)
A setter method which saves to an attribute.
"""
def setter(self: Attributes, value: Any) -> Any:
if check(value):
self.properties[name] = value
else:
raise ValueError(
"Max chromosome mutation rate must be between 0 and 1")
raise ValueError(error)
return setter
@property
def min_chromosome_mutation_rate(self: AttributesProperties) -> float:
# Default value.
if vars(self).get("min_chromosome_mutation_rate", None) is None:
return max(self.chromosome_mutation_rate / 2, self.chromosome_mutation_rate * 2 - 1)
# Set value.
return vars(self)["min_chromosome_mutation_rate"]
@min_chromosome_mutation_rate.setter
def min_chromosome_mutation_rate(self: AttributesProperties, value: Optional[float]) -> None:
# Use default or a valid float.
if value is None or (isinstance(value, (float, int)) and 0 <= value <= 1):
vars(self)["min_chromosome_mutation_rate"] = value
for name in static_checks:
setattr(
Attributes,
name,
property(
get_attr(name),
set_attr(name, static_checks[name]["check"], static_checks[name]["error"]),
)
)
#==================#
# Other properties #
#==================#
def get_max_chromosome_mutation_rate(self: Attributes) -> float:
return self._max_chromosome_mutation_rate
def set_max_chromosome_mutation_rate(self: Attributes, value: Optional[float]) -> None:
# Default value
if value is None:
self._max_chromosome_mutation_rate = min(
self.chromosome_mutation_rate * 2,
(self.chromosome_mutation_rate + 1) / 2,
)
# Otherwise check value
elif isinstance(value, (float, int)) and 0 <= value <= 1:
self._max_chromosome_mutation_rate = value
# Raise error
else:
raise ValueError(
"Min chromosome mutation rate must be between 0 and 1")
raise ValueError("Max chromosome mutation rate must be between 0 and 1")
# @property
# def database_name(self: AttributesProperties) -> str:
# return vars(self)["database_name"]
# @database_name.setter
# def database_name(self: AttributesProperties, name: str) -> None:
# # Update the database's name.
# self.database._database_name = name
# # Set the attribute for itself.
# vars(self)["database_name"] = name
def get_min_chromosome_mutation_rate(self: Attributes) -> float:
return self._min_chromosome_mutation_rate
# @property
# def graph(self: AttributesProperties) -> Graph:
# return vars(self)["graph"]
# @graph.setter
# def graph(self: AttributesProperties, graph: Callable[[Database], Graph]) -> None:
# vars(self)["graph"] = graph(self.database)
def set_min_chromosome_mutation_rate(self: Attributes, value: Optional[float]) -> None:
@property
def active(self: AttributesProperties) -> Callable[[], bool]:
# 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)

View File

@ -1,7 +1,7 @@
import random
# Import all crossover decorators
from decorators import _check_weight, _gene_by_gene
from EasyGA.decorators import _check_weight, _gene_by_gene
# Round to an integer near x with higher probability
# the closer it is to that integer.
@ -160,4 +160,3 @@ class Individual:
input_index += 1
ga.population.add_child(gene_list_1)

View File

@ -1,2 +0,0 @@
from .sql_database import SQLDatabase
from .matplotlib_graph import MatplotlibGraph

View File

@ -3,7 +3,7 @@ import matplotlib.pyplot as plt
import numpy as np
class MatplotlibGraph:
class Matplotlib_Graph:
"""Prebuilt graphing functions to make visual
represention of fitness data."""

View File

@ -1,24 +1,21 @@
import sqlite3
from sqlite3 import Error
from tabulate import tabulate
class SQLDatabase:
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 = """
self.config_structure = f"""
CREATE TABLE IF NOT EXISTS config (
config_id INTEGER,
attribute_name TEXT,
attribute_value TEXT
);
"""
attribute_value TEXT)"""
#=====================================#
@ -26,56 +23,102 @@ class SQLDatabase:
#=====================================#
def create_all_tables(self, ga):
"""Create the database if it doenst exist and then the data and config tables."""
# Create the database connection.
"""Create the database if it doenst exist and then the data and config
tables."""
# Create the database connection
self.create_connection()
# No connection.
if self.conn is None:
raise Exception("Error! Cannot create the database connection.")
# Create data table.
if self.conn is not None:
# Create data table
self.create_table(ga.sql_create_data_structure)
# Creare config table.
# Creare config table
self.create_table(self.config_structure)
# Set the config id.
# 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.
Notes:
"Attributes" here refers to ga.__dataclass_fields__.keys(),
which allows the attributes to be customized.
Only attributes that are bool, float, int, or str will be used.
"""
"""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 is None:
if self.config_id == None:
self.config_id = 0
else:
self.config_id = self.config_id + 1
# Getting all attribute fields from the attributes class
db_config = [
(self.config_id, attr_name, attr_value)
# Getting all the attributes from the attributes class
db_config_dict = (
(attr_name, getattr(ga, attr_name))
for attr_name
in ga.__dataclass_fields__
if isinstance((attr_value := getattr(ga, attr_name)), (bool, float, int, str))
]
in ga.__annotations__
if attr_name != "population"
)
query = """
# Types supported in the database
sql_type_list = [int, float, str]
# Loop through all attributes
for name, value in db_config_dict:
# not a function
if not callable(value):
# Convert to the right type
value = str(value)
if "'" not in value and '"' not in value:
# Insert into database
self.conn.execute(f"""
INSERT INTO config(config_id, attribute_name, attribute_value)
VALUES (?, ?, ?);
"""
self.conn.executemany(query, db_config)
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: #
#=====================================#
@ -88,73 +131,63 @@ class SQLDatabase:
def past_runs(self):
"""Show a summerization of the past runs that the user has done."""
query_data = self.query_all("""
query_data = self.query_all(f"""
SELECT config_id,attribute_name,attribute_value
FROM config;
""")
FROM config;""")
table = tabulate(
print(
tabulate(
query_data,
headers = [
'config_id',
'attribute_name',
'attribute_value',
'attribute_value'
]
)
print(table)
return table
)
@default_config_id
def get_generation_total_fitness(self, config_id):
"""Get each generations total fitness sum from the database """
config_id = self.config_id if config_id is None else config_id
return self.query_all(f"""
SELECT SUM(fitness)
FROM data
WHERE config_id={config_id}
GROUP BY generation;
""")
GROUP BY generation;""")
@default_config_id
def get_total_generations(self, config_id):
"""Get the total generations from the database"""
config_id = self.config_id if config_id is None else config_id
return self.query_one_item(f"""
SELECT COUNT(DISTINCT generation)
FROM data
WHERE config_id={config_id};
""")
WHERE config_id={config_id};""")
@default_config_id
def get_highest_chromosome(self, config_id):
"""Get the highest fitness of each generation"""
config_id = self.config_id if config_id is None else config_id
return self.query_all(f"""
SELECT max(fitness)
FROM data
WHERE config_id={config_id}
GROUP by generation;
""")
GROUP by generation;""")
@default_config_id
def get_lowest_chromosome(self, config_id):
"""Get the lowest fitness of each generation"""
config_id = self.config_id if config_id is None else config_id
return self.query_all(f"""
SELECT min(fitness)
FROM data
WHERE config_id={config_id}
GROUP by generation;
""")
GROUP by generation;""")
def get_all_config_id(self):
@ -162,8 +195,7 @@ class SQLDatabase:
return self.query_all(f"""
SELECT DISTINCT config_id
FROM config;
""")
FROM config;""")
def get_each_generation_number(self,config_id):
"""Get an array of all the generation numbers"""
@ -171,8 +203,7 @@ class SQLDatabase:
return self.query_all(f"""
SELECT DISTINCT generation
FROM data
WHERE config_id={config_id};
""")
WHERE config_id={config_id};""")
@ -189,14 +220,12 @@ class SQLDatabase:
self.config_id,
generation,
chromosome.fitness,
repr(chromosome),
repr(chromosome)
)
# Create sql query structure
sql = """
INSERT INTO data(config_id, generation, fitness, chromosome)
VALUES(?, ?, ?, ?)
"""
sql = """INSERT INTO data(config_id, generation, fitness, chromosome)
VALUES(?,?,?,?)"""
cur = self.conn.cursor()
cur.execute(sql, db_chromosome)
@ -220,10 +249,8 @@ class SQLDatabase:
]
# Create sql query structure
sql = """
INSERT INTO data(config_id, generation, fitness, chromosome)
VALUES(?,?,?,?)
"""
sql = """INSERT INTO data(config_id, generation, fitness, chromosome)
VALUES(?,?,?,?)"""
cur = self.conn.cursor()
cur.executemany(sql, db_chromosome_list)
@ -236,7 +263,8 @@ class SQLDatabase:
#=====================================#
def create_connection(self):
"""Create a database connection to the SQLite database specified by db_file."""
"""Create a database connection to the SQLite database
specified by db_file."""
try:
self.conn = sqlite3.connect(self.database_name)
@ -244,7 +272,6 @@ class SQLDatabase:
self.conn = None
print(e)
def create_table(self, create_table_sql):
"""Create a table from the create_table_sql statement."""
@ -255,31 +282,22 @@ class SQLDatabase:
print(e)
def format_query_data(self, data):
"""Used to format query data."""
# Unpack elements if they are lists with only 1 element
if isinstance(data[0], (list, tuple)) and len(data[0]) == 1:
data = [i[0] for i in data]
# Unpack list if it is a list with only 1 element
if isinstance(data, (list, tuple)) and len(data) == 1:
data = data[0]
return data
@format_query_data
def query_all(self, query):
"""Query for muliple rows of data"""
cur = self.conn.cursor()
cur.execute(query)
return self.format_query_data(cur.fetchall())
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 self.format_query_data(cur.fetchone())
return cur.fetchone()
def remove_database(self):
@ -287,6 +305,16 @@ class SQLDatabase:
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: #
#=====================================#
@ -299,7 +327,7 @@ class SQLDatabase:
@database_name.setter
def database_name(self, value_input):
raise AttributeError("Invalid usage, please use ga.database_name instead.")
raise Exception("Invalid usage, please use ga.database_name instead.")
@property
@ -318,7 +346,7 @@ class SQLDatabase:
# 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()")
raise Exception("""You are required to run a ga before you can connect to the database. Run ga.evolve() or ga.active()""")
@conn.setter
@ -345,7 +373,7 @@ class SQLDatabase:
# 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()")
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

View File

@ -1 +0,0 @@

View File

@ -2,7 +2,7 @@ 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
from EasyGA.decorators import _check_chromosome_mutation_rate, _check_gene_mutation_rate, _reset_fitness, _loop_random_mutations
class Population:

View File

@ -1,7 +1,7 @@
import random
# Import all parent decorators
from decorators import _check_selection_probability, _check_positive_fitness, _ensure_sorted, _compute_parent_amount
from EasyGA.decorators import _check_selection_probability, _check_positive_fitness, _ensure_sorted, _compute_parent_amount
class Rank:

View File

@ -1,33 +0,0 @@
import EasyGA
import random
# Create the Genetic algorithm
ga = EasyGA.GA()
ga.save_data = False
def is_it_5(chromosome):
"""A very simple case test function - If the chromosomes gene value is a 5 add one
to the chromosomes overall fitness value."""
# Overall fitness value
fitness = 0
# For each gene in the chromosome
for gene in chromosome.gene_list:
# Check if its value = 5
if(gene.value == 5):
# If its value is 5 then add one to
# the overal fitness of the chromosome.
fitness += 1
return fitness
ga.fitness_function_impl = is_it_5
# Create random genes from 0 to 10
ga.gene_impl = lambda: random.randint(0, 10)
ga.evolve()
# Print your default genetic algorithm
ga.print_generation()
ga.print_population()

View File

@ -1,4 +1,4 @@
from structure import Gene as make_gene
from EasyGA.structure import Gene as make_gene
from itertools import chain
def to_gene(gene):
@ -107,17 +107,6 @@ class Chromosome():
return (to_gene(gene) in self.gene_list)
def __hash__(self):
"""
Returns hash(self).
Allows the user to use
{chromosome}
{chromosome: x}
or any other thing requiring hashes with chromosomes.
"""
return hash(tuple(self))
def __eq__(self, chromosome):
"""Returns self == chromosome, True if all genes match."""
return self.gene_list == chromosome.gene_list

View File

@ -1,4 +1,4 @@
from structure import Chromosome as make_chromosome
from EasyGA.structure import Chromosome as make_chromosome
from itertools import chain
def to_chromosome(chromosome):
@ -159,17 +159,6 @@ class Population:
return (to_chromosome(chromosome) in self.chromosome_list)
def __hash__(self):
"""
Returns hash(self).
Allows the user to use
{population}
{population: x}
or any other thing requiring hashes with populations.
"""
return hash(tuple(self))
def __eq__(self, population):
"""Returns self == population, True if all chromosomes match."""
return self.chromosome_list == population.chromosome_list

View File

@ -1,7 +1,7 @@
import random
# Import all survivor decorators
from decorators import *
from EasyGA.decorators import *
def fill_in_best(ga):

View File

@ -1,5 +1,5 @@
# Import all termination decorators
from decorators import _add_by_fitness_goal, _add_by_generation_goal, _add_by_tolerance_goal
from EasyGA.decorators import _add_by_fitness_goal, _add_by_generation_goal, _add_by_tolerance_goal
@_add_by_fitness_goal
@_add_by_generation_goal

View File

@ -1,5 +1,10 @@
import random
from EasyGA import GA, Parent, Crossover, Mutation, Survivor, Termination
from EasyGA import GA
from parent import Parent
from crossover import Crossover
from mutation import Mutation
from survivor import Survivor
from termination import Termination
# USE THIS COMMAND WHEN TESTING -
# python3 -m pytest
@ -10,6 +15,7 @@ from EasyGA import GA, Parent, Crossover, Mutation, Survivor, Termination
# - Testing correct value
# - Testing integration with other functions
def test_population_size():
"""Test the population size is create correctly"""
@ -26,6 +32,7 @@ def test_population_size():
# 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."""
@ -43,14 +50,17 @@ def test_chromosome_length():
# 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()
@ -97,6 +107,7 @@ def test_attributes_chromosome_impl_lambdas():
# Evolve the genetic algorithm
ga.evolve()
def test_attributes_chromosome_impl_functions():
# Create the Genetic algorithm
ga = GA()
@ -120,6 +131,7 @@ def test_attributes_chromosome_impl_functions():
# Evolve the genetic algorithm
ga.evolve()
def test_while_ga_active():
# Create the Genetic algorithm
ga = GA()
@ -142,7 +154,9 @@ def test_parent_selection_impl():
# Evolve the genetic algorithm
ga.evolve()
assert (ga.parent_selection_impl == Parent.Fitness.roulette) and (ga != None)
assert (ga.parent_selection_impl ==
Parent.Fitness.roulette) and (ga != None)
def test_crossover_population_impl():
# Create the Genetic algorithm
@ -154,7 +168,9 @@ def test_crossover_population_impl():
# Evolve the genetic algorithm
ga.evolve()
assert (ga.crossover_population_impl == Crossover.Population.sequential_selection) and (ga != None)
assert (ga.crossover_population_impl ==
Crossover.Population.sequential_selection) and (ga != None)
def test_crossover_individual_impl():
# Create the Genetic algorithm
@ -166,7 +182,9 @@ def test_crossover_individual_impl():
# Evolve the genetic algorithm
ga.evolve()
assert (ga.crossover_individual_impl == Crossover.Individual.single_point) and (ga != None)
assert (ga.crossover_individual_impl ==
Crossover.Individual.single_point) and (ga != None)
def test_mutation_population_impl():
# Create the Genetic algorithm
@ -178,7 +196,9 @@ def test_mutation_population_impl():
# Evolve the genetic algorithm
ga.evolve()
assert (ga.mutation_population_impl == Mutation.Population.random_selection) and (ga != None)
assert (ga.mutation_population_impl ==
Mutation.Population.random_selection) and (ga != None)
def test_mutation_individual_impl():
# Create the Genetic algorithm
@ -190,7 +210,9 @@ def test_mutation_individual_impl():
# Evolve the genetic algorithm
ga.evolve()
assert (ga.mutation_individual_impl == Mutation.Individual.single_gene) and (ga != None)
assert (ga.mutation_individual_impl ==
Mutation.Individual.single_gene) and (ga != None)
def test_survivor_selection_impl():
# Create the Genetic algorithm
@ -202,7 +224,9 @@ def test_survivor_selection_impl():
# Evolve the genetic algorithm
ga.evolve()
assert (ga.survivor_selection_impl == Survivor.fill_in_random) and (ga != None)
assert (ga.survivor_selection_impl ==
Survivor.fill_in_random) and (ga != None)
def test_termination_impl():
# Create the Genetic algorithm
@ -214,4 +238,5 @@ def test_termination_impl():
# Evolve the genetic algorithm
ga.evolve()
assert (ga.termination_impl == Termination.fitness_and_generation_based) and (ga != None)
assert (ga.termination_impl ==
Termination.fitness_and_generation_based) and (ga != None)

View File

@ -22,8 +22,8 @@ setuptools.setup(
],
install_requires=[
# "matplotlib ~= 3.3.2",
# "pyserial ~= 3.4",
"pytest>=3.7",
"pyserial ~= 3.4",
# "pytest>=3.7",
"tabulate >=0.8.7"
],
)