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 from typing import Optional, MutableSequence, Iterable
# Import all decorators # Import all decorators
import decorators import EasyGA.decorators
# Import all the data structure prebuilt modules # Import all the data structure prebuilt modules
from structure import Population as make_population from EasyGA.structure import Population as make_population
from structure import Chromosome as make_chromosome from EasyGA.structure import Chromosome as make_chromosome
from structure import Gene as make_gene from EasyGA.structure import Gene as make_gene
from structure import Population from EasyGA.structure import Population
from structure import Chromosome from EasyGA.structure import Chromosome
from structure import Gene from EasyGA.structure import Gene
# Misc. Methods # Misc. Methods
from examples import Fitness from EasyGA.examples import Fitness
from termination import Termination from EasyGA.termination import Termination
# Parent/Survivor Selection Methods # Parent/Survivor Selection Methods
from parent import Parent from EasyGA.parent import Parent
from survivor import Survivor from EasyGA.survivor import Survivor
# Genetic Operator Methods # Genetic Operator Methods
from crossover import Crossover from EasyGA.crossover import Crossover
from mutation import Mutation from EasyGA.mutation import Mutation
# Default Attributes for the GA # Default Attributes for the GA
from attributes import Attributes from EasyGA.attributes import Attributes
# Database class # Database class
# from database import SQLDatabase from EasyGA.database import sql_database
# from sqlite3 import Error from sqlite3 import Error
# Graphing package # Graphing package
# from database import MatplotlibGraph from EasyGA.database import matplotlib_graph
# import matplotlib.pyplot as plt import matplotlib.pyplot as plt
class GA(Attributes): class GA(Attributes):
@ -46,6 +46,7 @@ class GA(Attributes):
https://github.com/danielwilczak101/EasyGA/wiki https://github.com/danielwilczak101/EasyGA/wiki
""" """
def evolve(self: GA, number_of_generations: float = float('inf'), consider_termination: bool = True) -> None: 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. Evolves the ga until the ga is no longer active.
@ -62,12 +63,9 @@ class GA(Attributes):
if self.population is None: if self.population is None:
self.initialize_population() self.initialize_population()
# Evolve the specified number of generations. cond1 = lambda: number_of_generations > 0 # Evolve the specified number of generations.
def cond1(): return number_of_generations > 0 cond2 = lambda: not consider_termination # If consider_termination flag is set:
# If consider_termination flag is set: cond3 = lambda: cond2() or self.active() # check termination conditions.
def cond2(): return not consider_termination
# check termination conditions.
def cond3(): return cond2() or self.active()
while cond1() and cond3(): while cond1() and cond3():
@ -76,11 +74,10 @@ class GA(Attributes):
# Create the database here to allow the user to change the # Create the database here to allow the user to change the
# database name and structure before running the function. # 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 # Add the current configuration to the config table
# self.database.insert_config(self) self.database.insert_config(self)
pass
# Otherwise evolve the population. # Otherwise evolve the population.
else: else:
@ -96,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: 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.
@ -105,9 +101,10 @@ class GA(Attributes):
if int(adapt_counter) < int(adapt_counter + self.adapt_rate): if int(adapt_counter) < int(adapt_counter + self.adapt_rate):
self.adapt() self.adapt()
number_of_generations -= 1 number_of_generations -= 1
self.current_generation += 1 self.current_generation += 1
def update_population(self: GA) -> None: def update_population(self: GA) -> None:
""" """
Updates the population to the new population Updates the population to the new population
@ -115,6 +112,7 @@ class GA(Attributes):
""" """
self.population.update() self.population.update()
def reset_run(self: GA) -> None: def reset_run(self: GA) -> None:
""" """
Resets a run by re-initializing the Resets a run by re-initializing the
@ -124,6 +122,7 @@ class GA(Attributes):
self.current_generation = 0 self.current_generation = 0
self.run += 1 self.run += 1
def adapt(self: GA) -> None: def adapt(self: GA) -> None:
"""Adapts the ga to hopefully get better results.""" """Adapts the ga to hopefully get better results."""
@ -134,6 +133,7 @@ class GA(Attributes):
self.set_all_fitness() self.set_all_fitness()
self.sort_by_best_fitness() self.sort_by_best_fitness()
def adapt_probabilities(self: GA) -> None: def adapt_probabilities(self: GA) -> None:
""" """
Modifies the parent ratio and mutation rates based on the adapt 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 # Difference between best and i-th chromosomes
best_chromosome = self.population[0] 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 # Too few converged: cross more and mutate less
if tol(amount_converged//2) > tol(amount_converged//4)*2: if tol(amount_converged//2) > tol(amount_converged//4)*2:
@ -168,14 +168,13 @@ class GA(Attributes):
self.max_gene_mutation_rate) self.max_gene_mutation_rate)
# Weighted average of x and y # 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 # Adjust rates towards the bounds
self.selection_probability = average( self.selection_probability = average(bounds[0], self.selection_probability)
bounds[0], self.selection_probability) self.chromosome_mutation_rate = average(bounds[1], self.chromosome_mutation_rate)
self.chromosome_mutation_rate = average( self.gene_mutation_rate = average(bounds[2], self.gene_mutation_rate)
bounds[1], self.chromosome_mutation_rate)
self.gene_mutation_rate = average(bounds[2], self.gene_mutation_rate)
def adapt_population(self: GA) -> None: def adapt_population(self: GA) -> None:
""" """
@ -202,7 +201,7 @@ class GA(Attributes):
self.crossover_individual_impl( self.crossover_individual_impl(
self.population[n], self.population[n],
parent, parent,
weight=-3/4, weight = -3/4,
) )
# If negative weights can't be used or division by 0, use positive weight # If negative weights can't be used or division by 0, use positive weight
@ -210,7 +209,7 @@ class GA(Attributes):
self.crossover_individual_impl( self.crossover_individual_impl(
self.population[n], self.population[n],
parent, parent,
weight=+1/4, weight = +1/4,
) )
# Stop if we've filled up an entire population # Stop if we've filled up an entire population
@ -218,19 +217,20 @@ class GA(Attributes):
break break
# Replace worst chromosomes with new chromosomes, except for the previous best chromosome # Replace worst chromosomes with new chromosomes, except for the previous best chromosome
min_len = min(len(self.population)-1, min_len = min(len(self.population)-1, len(self.population.next_population))
len(self.population.next_population))
if min_len > 0: if min_len > 0:
self.population[-min_len:] = self.population.next_population[:min_len] self.population[-min_len:] = self.population.next_population[:min_len]
self.population.next_population = [] self.population.next_population = []
self.population.mating_pool = [] self.population.mating_pool = []
def initialize_population(self: GA) -> None: def initialize_population(self: GA) -> None:
""" """
Sets self.population using the chromosome implementation and population size. Sets self.population using the chromosome implementation and population size.
""" """
self.population = self.make_population(self.population_impl()) self.population = self.make_population(self.population_impl())
def set_all_fitness(self: GA) -> None: def set_all_fitness(self: GA) -> None:
""" """
Sets the fitness of each chromosome in the population. Sets the fitness of each chromosome in the population.
@ -251,14 +251,15 @@ class GA(Attributes):
if chromosome.fitness is None or self.update_fitness: if chromosome.fitness is None or self.update_fitness:
chromosome.fitness = self.fitness_function_impl(chromosome) chromosome.fitness = self.fitness_function_impl(chromosome)
def sort_by_best_fitness( def sort_by_best_fitness(
self: GA, self: GA,
chromosome_list: Optional[ chromosome_list: Optional[
Union[MutableSequence[Chromosome], Union[MutableSequence[Chromosome],
Iterable[Chromosome]] Iterable[Chromosome]]
] = None, ] = None,
in_place: bool = True, in_place: bool = True,
) -> MutableSequence[Chromosome]: ) -> MutableSequence[Chromosome]:
""" """
Sorts the chromosome list by fitness based on fitness type. Sorts the chromosome list by fitness based on fitness type.
1st element has best fitness. 1st element has best fitness.
@ -313,6 +314,7 @@ class GA(Attributes):
else: else:
return sorted(chromosome_list, key=key, reverse=reverse) return sorted(chromosome_list, key=key, reverse=reverse)
def get_chromosome_fitness(self: GA, index: int) -> float: def get_chromosome_fitness(self: GA, index: int) -> float:
""" """
Computes the converted fitness of a chromosome at an index. 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) return self.convert_fitness(self.population[index].fitness)
def convert_fitness(self: GA, fitness: float) -> float: def convert_fitness(self: GA, fitness: float) -> float:
""" """
Calculates a modified version of the fitness for various Calculates a modified version of the fitness for various
@ -372,19 +375,23 @@ class GA(Attributes):
return max_fitness - fitness + min_fitness return max_fitness - fitness + min_fitness
def print_generation(self: GA) -> None: 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: GA) -> None: 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: GA) -> None: 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: GA) -> None: 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]}")

View File

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

View File

@ -1,29 +1,124 @@
from __future__ import annotations from __future__ import annotations
from inspect import getmro, signature from inspect import signature
from typing import Any, Callable, Dict, Iterable, Iterator, Optional from typing import Callable, Optional, Iterable, Any, Dict
from math import sqrt, ceil from math import sqrt, ceil
from dataclasses import dataclass, field, _MISSING_TYPE from dataclasses import dataclass, field
from types import MethodType from types import MethodType
import random import random
# import sqlite3 import sqlite3
# import matplotlib.pyplot as plt import matplotlib.pyplot as plt
from structure import Population from EasyGA.structure import Population
from structure import Chromosome from EasyGA.structure import Chromosome
from structure import Gene from EasyGA.structure import Gene
from examples import Fitness from EasyGA.examples import Fitness
from termination import Termination from EasyGA.termination import Termination
from parent import Parent from EasyGA.parent import Parent
from survivor import Survivor from EasyGA.survivor import Survivor
from crossover import Crossover from EasyGA.crossover import Crossover
from mutation import Mutation from EasyGA.mutation import Mutation
# from database import SQLDatabase, MatplotlibGraph, SQLDatabase as Database, MatplotlibGraph as Graph 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: 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) 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. 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 Returns
------- -------
chromosome : Iterator[Any] chromosome : Iterable[Any]
Generates the genes for a chromosome. Generates the genes for a chromosome.
""" """
for _ in range(self.chromosome_length): for _ in range(self.chromosome_length):
yield self.gene_impl() 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. 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 Returns
------- -------
population : Iterator[Iterable[Any]] population : Iterable[Iterable[Any]]
Generates the chromosomes for a population. Generates the chromosomes for a population.
""" """
for _ in range(self.population_size): 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) 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. Creates a getter method for getting a method from the Attributes class.
This includes type-hints/annotations and default values, except for methods.
Additionally gains dataclass features, including an __init__ and __repr__ to avoid boilerplate code. Parameters
----------
name : str
The name of the method from Attributes.
Developer Notes: Returns
-------
See the Attributes class for default methods. getter(ga)(...) -> Any
The getter property, taking in an object and returning the method.
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__().
""" """
def getter(self: Attributes) -> Callable[..., Any]:
run: int = 0 return self.properties[name]
return getter
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: def set_method(name: str) -> Callable[[Attributes, Optional[Callable[..., Any]]], None]:
""" """
A descriptor for converting function attributes into bound methods. Creates a setter method for setting a method from the Attributes class.
To support both inheritance and dataclasses, if the method is None, Parameters
then nothing is set. ----------
name : str
The name of the method from Attributes.
Returns
-------
setter(ga, method)
The setter property, taking in an object and returning nothing.
""" """
def setter(self: Attributes, method: Optional[Callable[..., Any]]) -> None:
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: if method is None:
return pass
elif not callable(method): elif not callable(method):
raise TypeError(f"'{self.name}' must be a method i.e. callable.") raise TypeError(f"{name} must be a method i.e. callable.")
elif next(iter(signature(method).parameters), None) in ("self", "ga"): elif next(iter(signature(method).parameters), None) in ("self", "ga"):
method = MethodType(method, obj) method = MethodType(method, self)
vars(obj)[self.name] = method self.properties[name] = method
return setter
def __delete__(self: AsMethod, obj: "Attributes") -> None:
del vars(obj)[self.name]
class Attributes(AttributesData): 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)))
#============================#
# Static checking properties #
#============================#
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.",
},
}
def get_attr(name: str) -> Callable[[Attributes], Any]:
""" """
The Attributes class inherits default attributes from AttributesData Creates a getter method for getting an attribute from the Attributes class.
and implements methods, descriptors, and properties.
The built-in methods provide interfacing to the database. Parameters
>>> ga.save_population() # references ga.database.insert_current_population(ga) ----------
The descriptors are used to convert function attributes into methods. name : str
>>> ga.gene_impl = lambda self: ... # self is turned into an implicit argument. The name of the attribute.
The properties are used to validate certain inputs.
Developer Notes: Returns
-------
If inherited, the descriptors may be overridden with a method implementation, getter(ga) -> Any
but this removes the descriptor. A getter method which returns an attribute.
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
...
""" """
def getter(self: Attributes) -> Any:
return self.properties[name]
return getter
#============================#
# Built-in database methods: #
#============================#
def save_population(self: Attributes) -> None: def set_attr(name: str, check: Callable[[Any], bool], error: str) -> Callable[[Attributes, Any], None]:
"""Saves the current population to the database.""" """
self.database.insert_current_population(self) Creates a setter method for setting an attribute from the Attributes class.
def save_chromosome(self: Attributes, chromosome: Chromosome) -> None: Parameters
""" ----------
Saves a chromosome to the database. 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.
Parameters Returns
---------- -------
chromosome : Chromosome setter(ga, Any) -> None
The chromosome to be saved. Raises ValueError(error)
""" A setter method which saves to an attribute.
self.database.insert_current_chromosome( """
self.current_generation, chromosome) def setter(self: Attributes, value: Any) -> Any:
if check(value):
#===========================# self.properties[name] = value
# 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)
#=============#
# Properties: #
#=============#
@property
def run(self: AttributesProperties) -> int:
return vars(self)["run"]
@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"]
@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"]
@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"]
@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"]
@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
else: else:
raise ValueError( raise ValueError(error)
"Max chromosome mutation rate must be between 0 and 1") 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 for name in static_checks:
def min_chromosome_mutation_rate(self: AttributesProperties, value: Optional[float]) -> None: setattr(
# Use default or a valid float. Attributes,
if value is None or (isinstance(value, (float, int)) and 0 <= value <= 1): name,
vars(self)["min_chromosome_mutation_rate"] = value property(
else: get_attr(name),
raise ValueError( set_attr(name, static_checks[name]["check"], static_checks[name]["error"]),
"Min 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: # Other properties #
# # Update the database's name. #==================#
# self.database._database_name = name
# # Set the attribute for itself.
# vars(self)["database_name"] = name
# @property
# def graph(self: AttributesProperties) -> Graph:
# return vars(self)["graph"]
# @graph.setter def get_max_chromosome_mutation_rate(self: Attributes) -> float:
# def graph(self: AttributesProperties, graph: Callable[[Database], Graph]) -> None: return self._max_chromosome_mutation_rate
# vars(self)["graph"] = graph(self.database)
@property
def active(self: AttributesProperties) -> Callable[[], bool]: def set_max_chromosome_mutation_rate(self: Attributes, value: Optional[float]) -> None:
return self.termination_impl
# 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("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)

View File

@ -1,7 +1,7 @@
import random import random
# Import all crossover decorators # 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 # Round to an integer near x with higher probability
# the closer it is to that integer. # the closer it is to that integer.
@ -160,4 +160,3 @@ class Individual:
input_index += 1 input_index += 1
ga.population.add_child(gene_list_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 import numpy as np
class MatplotlibGraph: class Matplotlib_Graph:
"""Prebuilt graphing functions to make visual """Prebuilt graphing functions to make visual
represention of fitness data.""" represention of fitness data."""

View File

@ -1,24 +1,21 @@
import sqlite3 import sqlite3
from sqlite3 import Error
from tabulate import tabulate from tabulate import tabulate
class SQL_Database:
class SQLDatabase:
"""Main database class that controls all the functionality for input / """Main database class that controls all the functionality for input /
out of the database using SQLite3.""" out of the database using SQLite3."""
def __init__(self): def __init__(self):
self.conn = None self.conn = None
self.config_id = None self.config_id = None
self._database_name = 'database.db' self._database_name = 'database.db'
self.config_structure = """ self.config_structure = f"""
CREATE TABLE IF NOT EXISTS config ( CREATE TABLE IF NOT EXISTS config (
config_id INTEGER, config_id INTEGER,
attribute_name TEXT, attribute_name TEXT,
attribute_value TEXT attribute_value TEXT)"""
);
"""
#=====================================# #=====================================#
@ -26,56 +23,102 @@ class SQLDatabase:
#=====================================# #=====================================#
def create_all_tables(self, ga): def create_all_tables(self, ga):
"""Create the database if it doenst exist and then the data and config tables.""" """Create the database if it doenst exist and then the data and config
# Create the database connection. tables."""
# Create the database connection
self.create_connection() self.create_connection()
# No connection.
if self.conn is None: if self.conn is not None:
# Create data table
self.create_table(ga.sql_create_data_structure)
# Creare config table
self.create_table(self.config_structure)
# Set the config id
self.config_id = self.get_current_config()
else:
raise Exception("Error! Cannot create the database connection.") raise Exception("Error! Cannot create the database connection.")
# Create data table.
self.create_table(ga.sql_create_data_structure)
# Creare config table.
self.create_table(self.config_structure)
# Set the config id.
self.config_id = self.get_current_config()
def insert_config(self, ga):
"""
Insert the configuration attributes into the config.
Notes: def insert_config(self,ga):
"""Insert the configuration attributes into the config."""
"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.
"""
# Get the current config and add one for the new config key # Get the current config and add one for the new config key
self.config_id = self.get_current_config() self.config_id = self.get_current_config()
# Setting the config_id index if there is no file # 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 self.config_id = 0
else: else:
self.config_id = self.config_id + 1 self.config_id = self.config_id + 1
# Getting all attribute fields from the attributes class # Getting all the attributes from the attributes class
db_config = [ db_config_dict = (
(self.config_id, attr_name, attr_value) (attr_name, getattr(ga, attr_name))
for attr_name for attr_name
in ga.__dataclass_fields__ in ga.__annotations__
if isinstance((attr_value := getattr(ga, attr_name)), (bool, float, int, str)) if attr_name != "population"
] )
# Types supported in the database
sql_type_list = [int, float, str]
# Loop through all attributes
for name, value in db_config_dict:
# 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.config_id}', '{name}','{value}');""")
query = """
INSERT INTO config(config_id, attribute_name, attribute_value)
VALUES (?, ?, ?);
"""
self.conn.executemany(query, db_config)
self.config_id = self.get_current_config() 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: # # Request information Queries: #
#=====================================# #=====================================#
@ -88,91 +131,79 @@ class SQLDatabase:
def past_runs(self): def past_runs(self):
"""Show a summerization of the past runs that the user has done.""" """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 SELECT config_id,attribute_name,attribute_value
FROM config; FROM config;""")
""")
table = tabulate( print(
query_data, tabulate(
headers = [ query_data,
'config_id', headers = [
'attribute_name', 'config_id',
'attribute_value', 'attribute_name',
] 'attribute_value'
]
)
) )
print(table)
return table
@default_config_id
def get_generation_total_fitness(self, config_id): def get_generation_total_fitness(self, config_id):
"""Get each generations total fitness sum from the database """ """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""" return self.query_all(f"""
SELECT SUM(fitness) SELECT SUM(fitness)
FROM data FROM data
WHERE config_id={config_id} WHERE config_id={config_id}
GROUP BY generation; GROUP BY generation;""")
""")
@default_config_id
def get_total_generations(self, config_id): def get_total_generations(self, config_id):
"""Get the total generations from the database""" """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""" return self.query_one_item(f"""
SELECT COUNT(DISTINCT generation) SELECT COUNT(DISTINCT generation)
FROM data FROM data
WHERE config_id={config_id}; WHERE config_id={config_id};""")
""")
@default_config_id
def get_highest_chromosome(self, config_id): def get_highest_chromosome(self, config_id):
"""Get the highest fitness of each generation""" """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""" return self.query_all(f"""
SELECT max(fitness) SELECT max(fitness)
FROM data FROM data
WHERE config_id={config_id} WHERE config_id={config_id}
GROUP by generation; GROUP by generation;""")
""")
@default_config_id
def get_lowest_chromosome(self, config_id): def get_lowest_chromosome(self, config_id):
"""Get the lowest fitness of each generation""" """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""" return self.query_all(f"""
SELECT min(fitness) SELECT min(fitness)
FROM data FROM data
WHERE config_id={config_id} WHERE config_id={config_id}
GROUP by generation; GROUP by generation;""")
""")
def get_all_config_id(self): def get_all_config_id(self):
"""Get an array of all the DISTINCT config_id in the database""" """Get an array of all the DISTINCT config_id in the database"""
return self.query_all(f""" return self.query_all(f"""
SELECT DISTINCT config_id SELECT DISTINCT config_id
FROM config; FROM config;""")
""")
def get_each_generation_number(self, config_id): def get_each_generation_number(self,config_id):
"""Get an array of all the generation numbers""" """Get an array of all the generation numbers"""
return self.query_all(f""" return self.query_all(f"""
SELECT DISTINCT generation SELECT DISTINCT generation
FROM data FROM data
WHERE config_id={config_id}; WHERE config_id={config_id};""")
""")
@ -189,14 +220,12 @@ class SQLDatabase:
self.config_id, self.config_id,
generation, generation,
chromosome.fitness, chromosome.fitness,
repr(chromosome), repr(chromosome)
) )
# Create sql query structure # Create sql query structure
sql = """ sql = """INSERT INTO data(config_id, generation, fitness, chromosome)
INSERT INTO data(config_id, generation, fitness, chromosome) VALUES(?,?,?,?)"""
VALUES(?, ?, ?, ?)
"""
cur = self.conn.cursor() cur = self.conn.cursor()
cur.execute(sql, db_chromosome) cur.execute(sql, db_chromosome)
@ -220,10 +249,8 @@ class SQLDatabase:
] ]
# Create sql query structure # Create sql query structure
sql = """ sql = """INSERT INTO data(config_id, generation, fitness, chromosome)
INSERT INTO data(config_id, generation, fitness, chromosome) VALUES(?,?,?,?)"""
VALUES(?,?,?,?)
"""
cur = self.conn.cursor() cur = self.conn.cursor()
cur.executemany(sql, db_chromosome_list) cur.executemany(sql, db_chromosome_list)
@ -236,7 +263,8 @@ class SQLDatabase:
#=====================================# #=====================================#
def create_connection(self): 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: try:
self.conn = sqlite3.connect(self.database_name) self.conn = sqlite3.connect(self.database_name)
@ -244,7 +272,6 @@ class SQLDatabase:
self.conn = None self.conn = None
print(e) print(e)
def create_table(self, create_table_sql): def create_table(self, create_table_sql):
"""Create a table from the create_table_sql statement.""" """Create a table from the create_table_sql statement."""
@ -255,31 +282,22 @@ class SQLDatabase:
print(e) print(e)
def format_query_data(self, data): @format_query_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
def query_all(self, query): def query_all(self, query):
"""Query for muliple rows of data""" """Query for muliple rows of data"""
cur = self.conn.cursor() cur = self.conn.cursor()
cur.execute(query) cur.execute(query)
return self.format_query_data(cur.fetchall()) return cur.fetchall()
@format_query_data
def query_one_item(self, query): def query_one_item(self, query):
"""Query for single data point""" """Query for single data point"""
cur = self.conn.cursor() cur = self.conn.cursor()
cur.execute(query) cur.execute(query)
return self.format_query_data(cur.fetchone()) return cur.fetchone()
def remove_database(self): def remove_database(self):
@ -287,6 +305,16 @@ class SQLDatabase:
os.remove(self._database_name) 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: # # Setters and Getters: #
#=====================================# #=====================================#
@ -299,7 +327,7 @@ class SQLDatabase:
@database_name.setter @database_name.setter
def database_name(self, value_input): 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 @property
@ -318,7 +346,7 @@ class SQLDatabase:
# If the connection doesnt exist then print error # If the connection doesnt exist then print error
except: 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 @conn.setter
@ -345,7 +373,7 @@ class SQLDatabase:
# If the config_id doesnt exist then print error # If the config_id doesnt exist then print error
except: 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 @config_id.setter

View File

@ -1 +0,0 @@

View File

@ -2,7 +2,7 @@ import random
from math import ceil from math import ceil
# Import all mutation decorators # 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: class Population:

View File

@ -1,7 +1,7 @@
import random import random
# Import all parent decorators # 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: class Rank:
@ -14,8 +14,8 @@ class Rank:
@_compute_parent_amount @_compute_parent_amount
def tournament(ga, parent_amount): def tournament(ga, parent_amount):
""" """
Will make tournaments of size tournament_size and choose the winner (best fitness) Will make tournaments of size tournament_size and choose the winner (best fitness)
from the tournament and use it as a parent for the next generation. The total number from the tournament and use it as a parent for the next generation. The total number
of parents selected is determined by parent_ratio, an attribute to the GA object. of parents selected is determined by parent_ratio, an attribute to the GA object.
May require many loops if the selection probability is very low. May require many loops if the selection probability is very low.
""" """

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 from itertools import chain
def to_gene(gene): def to_gene(gene):
@ -107,17 +107,6 @@ class Chromosome():
return (to_gene(gene) in self.gene_list) 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): def __eq__(self, chromosome):
"""Returns self == chromosome, True if all genes match.""" """Returns self == chromosome, True if all genes match."""
return self.gene_list == chromosome.gene_list 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 from itertools import chain
def to_chromosome(chromosome): def to_chromosome(chromosome):
@ -159,17 +159,6 @@ class Population:
return (to_chromosome(chromosome) in self.chromosome_list) 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): def __eq__(self, population):
"""Returns self == population, True if all chromosomes match.""" """Returns self == population, True if all chromosomes match."""
return self.chromosome_list == population.chromosome_list return self.chromosome_list == population.chromosome_list

View File

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

View File

@ -1,5 +1,5 @@
# Import all termination decorators # 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_fitness_goal
@_add_by_generation_goal @_add_by_generation_goal

View File

@ -1,8 +1,13 @@
import random 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 - # USE THIS COMMAND WHEN TESTING -
# python3 -m pytest # python3 -m pytest
# Tests can be broken down into three parts. # Tests can be broken down into three parts.
# - Testing correct size # - Testing correct size
@ -10,10 +15,11 @@ from EasyGA import GA, Parent, Crossover, Mutation, Survivor, Termination
# - Testing correct value # - Testing correct value
# - Testing integration with other functions # - Testing integration with other functions
def test_population_size(): def test_population_size():
"""Test the population size is create correctly""" """Test the population size is create correctly"""
for i in range(4,100): for i in range(4, 100):
# Create the ga to test # Create the ga to test
ga = GA() ga = GA()
@ -26,11 +32,12 @@ def test_population_size():
# If they are not equal throw an error # If they are not equal throw an error
assert int(len(ga.population)) == ga.population_size assert int(len(ga.population)) == ga.population_size
def test_chromosome_length(): def test_chromosome_length():
""" Test to see if the actual chromosome length is the same as defined.""" """ Test to see if the actual chromosome length is the same as defined."""
# Test from 0 to 100 chromosome length # Test from 0 to 100 chromosome length
for i in range(1,100): for i in range(1, 100):
# Create the ga to test # Create the ga to test
ga = GA() ga = GA()
@ -43,14 +50,17 @@ def test_chromosome_length():
# If they are not equal throw an error # If they are not equal throw an error
assert len(ga.population.chromosome_list[0]) == ga.chromosome_length assert len(ga.population.chromosome_list[0]) == ga.chromosome_length
def test_gene_value(): def test_gene_value():
""" """ """ """
pass pass
def test_initilization(): def test_initilization():
""" """ """ """
pass pass
def test_default(): def test_default():
# Create the Genetic algorithm # Create the Genetic algorithm
ga = GA() ga = GA()
@ -70,7 +80,7 @@ def test_attributes_gene_impl():
# Set necessary attributes # Set necessary attributes
ga.population_size = 3 ga.population_size = 3
ga.chromosome_length = 5 ga.chromosome_length = 5
ga.generation_goal = 1 ga.generation_goal = 1
# Set gene_impl # Set gene_impl
ga.gene_impl = lambda: random.randint(1, 10) ga.gene_impl = lambda: random.randint(1, 10)
@ -89,14 +99,15 @@ def test_attributes_chromosome_impl_lambdas():
ga.gene_impl = None ga.gene_impl = None
# Set chromosome_impl # Set chromosome_impl
ga.chromosome_impl = lambda: [ ga.chromosome_impl = lambda: [
random.randrange(1,100), random.randrange(1, 100),
random.uniform(10,5), random.uniform(10, 5),
random.choice(["up","down"]) random.choice(["up", "down"])
] ]
# Evolve the genetic algorithm # Evolve the genetic algorithm
ga.evolve() ga.evolve()
def test_attributes_chromosome_impl_functions(): def test_attributes_chromosome_impl_functions():
# Create the Genetic algorithm # Create the Genetic algorithm
ga = GA() ga = GA()
@ -108,10 +119,10 @@ def test_attributes_chromosome_impl_functions():
# Create chromosome_impl user function # Create chromosome_impl user function
def user_chromosome_function(): def user_chromosome_function():
chromosome_data = [ chromosome_data = [
random.randrange(1,100), random.randrange(1, 100),
random.uniform(10,5), random.uniform(10, 5),
random.choice(["up","down"]) random.choice(["up", "down"])
] ]
return chromosome_data return chromosome_data
# Set the chromosome_impl # Set the chromosome_impl
@ -120,6 +131,7 @@ def test_attributes_chromosome_impl_functions():
# Evolve the genetic algorithm # Evolve the genetic algorithm
ga.evolve() ga.evolve()
def test_while_ga_active(): def test_while_ga_active():
# Create the Genetic algorithm # Create the Genetic algorithm
ga = GA() ga = GA()
@ -142,7 +154,9 @@ def test_parent_selection_impl():
# Evolve the genetic algorithm # Evolve the genetic algorithm
ga.evolve() 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(): def test_crossover_population_impl():
# Create the Genetic algorithm # Create the Genetic algorithm
@ -154,7 +168,9 @@ def test_crossover_population_impl():
# Evolve the genetic algorithm # Evolve the genetic algorithm
ga.evolve() 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(): def test_crossover_individual_impl():
# Create the Genetic algorithm # Create the Genetic algorithm
@ -166,7 +182,9 @@ def test_crossover_individual_impl():
# Evolve the genetic algorithm # Evolve the genetic algorithm
ga.evolve() 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(): def test_mutation_population_impl():
# Create the Genetic algorithm # Create the Genetic algorithm
@ -178,7 +196,9 @@ def test_mutation_population_impl():
# Evolve the genetic algorithm # Evolve the genetic algorithm
ga.evolve() 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(): def test_mutation_individual_impl():
# Create the Genetic algorithm # Create the Genetic algorithm
@ -190,7 +210,9 @@ def test_mutation_individual_impl():
# Evolve the genetic algorithm # Evolve the genetic algorithm
ga.evolve() 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(): def test_survivor_selection_impl():
# Create the Genetic algorithm # Create the Genetic algorithm
@ -202,7 +224,9 @@ def test_survivor_selection_impl():
# Evolve the genetic algorithm # Evolve the genetic algorithm
ga.evolve() 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(): def test_termination_impl():
# Create the Genetic algorithm # Create the Genetic algorithm
@ -214,4 +238,5 @@ def test_termination_impl():
# Evolve the genetic algorithm # Evolve the genetic algorithm
ga.evolve() 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=[ install_requires=[
# "matplotlib ~= 3.3.2", # "matplotlib ~= 3.3.2",
# "pyserial ~= 3.4", "pyserial ~= 3.4",
"pytest>=3.7", # "pytest>=3.7",
"tabulate >=0.8.7" "tabulate >=0.8.7"
], ],
) )