Refactored using descriptors.

This commit is contained in:
SimpleArt
2021-07-07 21:02:41 -04:00
parent 7a7a985b49
commit 82479cf9f2

View File

@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from inspect import signature from inspect import signature
from typing import Callable, Optional, Iterable, Any, Dict from typing import Any, Callable, Dict, Iterable, Iterator, Optional
from math import sqrt, ceil from math import sqrt, ceil
from dataclasses import dataclass, field from dataclasses import dataclass, field
from types import MethodType from types import MethodType
@ -20,6 +20,7 @@ from survivor import Survivor
from crossover import Crossover from crossover import Crossover
from mutation import Mutation from mutation import Mutation
from database import sql_database, matplotlib_graph from database import sql_database, matplotlib_graph
from database import sql_database as Database, matplotlib_graph as Graph
#========================================# #========================================#
# Default methods not defined elsewhere. # # Default methods not defined elsewhere. #
@ -36,8 +37,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.
@ -50,14 +50,13 @@ def use_genes(self: Attributes) -> Iterable[Any]:
Returns Returns
------- -------
chromosome : Iterable[Any] chromosome : Iterator[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.
@ -70,13 +69,12 @@ def use_chromosomes(self: Attributes) -> Iterable[Any]:
Returns Returns
------- -------
population : Iterable[Iterable[Any]] population : Iterator[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):
yield self.chromosome_impl() yield self.chromosome_impl()
def dist_fitness(self: Attributes, chromosome_1: Chromosome, chromosome_2: Chromosome) -> float: def dist_fitness(self: Attributes, chromosome_1: Chromosome, chromosome_2: Chromosome) -> float:
""" """
Measures the distance between two chromosomes based on their fitnesses. Measures the distance between two chromosomes based on their fitnesses.
@ -93,7 +91,6 @@ def dist_fitness(self: Attributes, chromosome_1: Chromosome, chromosome_2: Chrom
""" """
return sqrt(abs(chromosome_1.fitness - chromosome_2.fitness)) return sqrt(abs(chromosome_1.fitness - chromosome_2.fitness))
def simple_linear(self: Attributes, weight: float) -> float: def simple_linear(self: Attributes, weight: float) -> float:
""" """
Returns a random value between 0 and 1, with increased probability Returns a random value between 0 and 1, with increased probability
@ -117,10 +114,16 @@ def simple_linear(self: Attributes, weight: float) -> float:
@dataclass @dataclass
class Attributes: class AttributesData:
""" """
Attributes class which stores all attributes in a dataclass. Attributes class which stores all attributes in a dataclass.
Contains default attributes for each attribute. This includes type-hints/annotations and default values.
Additionally gains dataclass features, including an __init__ and __repr__ to avoid boilerplate code.
Developer Note:
Override this class to set default attributes. See help(Attributes) for more information.
""" """
run: int = 0 run: int = 0
@ -156,28 +159,28 @@ class Attributes:
max_gene_mutation_rate: float = 0.15 max_gene_mutation_rate: float = 0.15
min_gene_mutation_rate: float = 0.01 min_gene_mutation_rate: float = 0.01
fitness_function_impl = Fitness.is_it_5 fitness_function_impl: Callable[["Attributes", Chromosome], float] = Fitness.is_it_5
make_population = Population make_gene: Callable[[Any], Gene] = Gene
make_chromosome = Chromosome make_chromosome: Callable[[Iterable[Any]], Chromosome] = Chromosome
make_gene = Gene make_population: Callable[[Iterable[Iterable[Any]]], Population] = Population
gene_impl = rand_1_to_10 gene_impl: Callable[[], Any] = rand_1_to_10
chromosome_impl = use_genes chromosome_impl: Callable[[], Iterable[Any]] = use_genes
population_impl = use_chromosomes population_impl: Callable[[], Iterable[Iterable[Any]]] = use_chromosomes
weighted_random = simple_linear weighted_random: Callable[[float], float] = simple_linear
dist = dist_fitness dist: Callable[["Attributes", Chromosome, Chromosome], None] = dist_fitness
parent_selection_impl = Parent.Rank.tournament parent_selection_impl: Callable[["Attributes"], None] = Parent.Rank.tournament
crossover_individual_impl = Crossover.Individual.single_point crossover_individual_impl: Callable[["Attributes"], None] = Crossover.Individual.single_point
crossover_population_impl = Crossover.Population.sequential crossover_population_impl: Callable[["Attributes", Chromosome, Chromosome], None] = Crossover.Population.sequential
survivor_selection_impl = Survivor.fill_in_best survivor_selection_impl: Callable[["Attributes"], None] = Survivor.fill_in_best
mutation_individual_impl = Mutation.Individual.individual_genes mutation_individual_impl: Callable[["Attributes", Chromosome], None] = Mutation.Individual.individual_genes
mutation_population_impl = Mutation.Population.random_avoid_best mutation_population_impl: Callable[["Attributes"], None] = Mutation.Population.random_avoid_best
termination_impl = Termination.fitness_generation_tolerance termination_impl: Callable[["Attributes"], bool] = Termination.fitness_generation_tolerance
database: Database = field(default_factory=sql_database.SQL_Database) database: Database = field(default_factory=sql_database.SQL_Database)
database_name: str = 'database.db' database_name: str = "database.db"
save_data: bool = True save_data: bool = True
sql_create_data_structure: str = """ sql_create_data_structure: str = """
CREATE TABLE IF NOT EXISTS data ( CREATE TABLE IF NOT EXISTS data (
@ -192,16 +195,61 @@ class Attributes:
graph: Callable[[Database], Graph] = matplotlib_graph.Matplotlib_Graph graph: Callable[[Database], Graph] = matplotlib_graph.Matplotlib_Graph
class AsMethod:
"""A descriptor for converting function attributes into bound methods."""
def __init__(self: AsMethod, name: str) -> None:
self.name = name
def __get__(self: AsMethod, obj: "AttributesProperties", cls: type) -> MethodType:
return vars(obj)[self.name]
def __set__(self: AsMethod, obj: "AttributesProperties", method: Callable) -> None:
if method is None:
pass
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
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: # # Built-in database methods: #
#============================# #============================#
def save_population(self: Attributes) -> None: def save_population(self: Attributes) -> None:
"""Saves the current population to the database.""" """Saves the current population to the database."""
self.database.insert_current_population(self) self.database.insert_current_population(self)
def save_chromosome(self: Attributes, chromosome: Chromosome) -> None: def save_chromosome(self: Attributes, chromosome: Chromosome) -> None:
""" """
Saves a chromosome to the database. Saves a chromosome to the database.
@ -213,235 +261,120 @@ class Attributes:
""" """
self.database.insert_current_chromosome(self.current_generation, chromosome) self.database.insert_current_chromosome(self.current_generation, chromosome)
#===========================#
# Descriptors which convert #
# functions into methods: #
#===========================#
#=========================# fitness_function_impl = AsMethod("fitness_function_impl")
# Properties for methods. # parent_selection_impl = AsMethod("parent_selection_impl")
#=========================# crossover_individual_impl = AsMethod("crossover_individual_impl")
crossover_population_impl = AsMethod("crossover_population_impl")
survivor_selection_impl = AsMethod("survivor_selection_impl")
mutation_individual_impl = AsMethod("mutation_individual_impl")
mutation_population_impl = AsMethod("mutation_population_impl")
termination_impl = AsMethod("termination_impl")
dist = AsMethod("dist")
weighted_random = AsMethod("weighted_random")
gene_impl = AsMethod("gene_impl")
chromosome_impl = AsMethod("chromosome_impl")
population_impl = AsMethod("population_impl")
#=============#
# Properties: #
#=============#
def get_method(name: str) -> Callable[[Attributes], Callable[..., Any]]: @property
""" def run(self: AttributesProperties) -> int:
Creates a getter method for getting a method from the Attributes class. return vars(self)["run"]
Parameters @run.setter
---------- def run(self: AttributesProperties, value: int) -> None:
name : str if not isinstance(value, int) or value < 0:
The name of the method from Attributes. raise ValueError("ga.run counter must be an integer greater than or equal to 0.")
vars(self)["run"] = value
Returns @property
------- def current_generation(self: AttributesProperties) -> int:
getter(ga)(...) -> Any return vars(self)["current_generation"]
The getter property, taking in an object and returning the method.
"""
def getter(self: Attributes) -> Callable[..., Any]:
return getattr(self, f"_{name}")
return getter
@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
def set_method(name: str) -> Callable[[Attributes, Optional[Callable[..., Any]]], None]: @property
""" def chromosome_length(self: AttributesProperties) -> int:
Creates a setter method for setting a method from the Attributes class. return vars(self)["chromosome_length"]
Parameters @chromosome_length.setter
---------- def chromosome_length(self: AttributesProperties, value: int) -> None:
name : str if not isinstance(value, int) or value <= 0:
The name of the method from Attributes. raise ValueError("ga.chromosome_length must be an integer greater than and not equal to 0.")
vars(self)["chromosome_length"] = value
Returns @property
------- def population_size(self: AttributesProperties) -> int:
setter(ga, method) return vars(self)["population_size"]
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)
setattr(self, f"_{name}", method)
return setter
@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
for name in ( @property
"fitness_function_impl", def max_chromosome_mutation_rate(self: AttributesProperties) -> float:
"parent_selection_impl", # Default value.
"crossover_individual_impl", if vars(self)["max_chromosome_mutation_rate"] is None:
"crossover_population_impl", return min(self.chromosome_mutation_rate * 2, (self.chromosome_mutation_rate + 1) / 2)
"survivor_selection_impl", # Set value.
"mutation_individual_impl", return vars(self)["max_chromosome_mutation_rate"]
"mutation_population_impl",
"termination_impl",
"dist",
"weighted_random",
"gene_impl",
"chromosome_impl",
"population_impl",
):
# Rename to private attribute:
# name -> _name
setattr(Attributes, f"_{name}", getattr(Attributes, name))
# Replace name with property
setattr(Attributes, name, property(get_method(name), set_method(name)))
@max_chromosome_mutation_rate.setter
#============================# def max_chromosome_mutation_rate(self: AttributesProperties, value: Optional[float]) -> None:
# Static checking properties $ # Use default or a valid float.
# for non-methods # if value is None or (isinstance(value, (float, int)) and 0 <= value <= 1):
#============================# vars(self)["max_chromosome_mutation_rate"] = value
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]:
"""
Creates a getter method for getting an attribute from the Attributes class.
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 getattr(self, f"_{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):
setattr(self, f"_{name}", value)
else: else:
raise ValueError(error) raise ValueError("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)["min_chromosome_mutation_rate"] is None:
return max(self.chromosome_mutation_rate / 2, self.chromosome_mutation_rate * 2 - 1)
# Set value.
return vars(self)["min_chromosome_mutation_rate"]
for name in static_checks: @min_chromosome_mutation_rate.setter
setattr( def min_chromosome_mutation_rate(self: AttributesProperties, value: Optional[float]) -> None:
Attributes, # Use default or a valid float.
name, if value is None or (isinstance(value, (float, int)) and 0 <= value <= 1):
property( vars(self)["min_chromosome_mutation_rate"] = value
get_attr(name), else:
set_attr(name, static_checks[name]["check"], static_checks[name]["error"]), raise ValueError("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
# Other properties # 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
@property
def graph(self: AttributesProperties) -> Graph:
return vars(self)["graph"]
def get_max_chromosome_mutation_rate(self: Attributes) -> float: @graph.setter
return self._max_chromosome_mutation_rate def graph(self: AttributesProperties, graph: Callable[[Database], Graph]) -> None:
vars(self)["graph"] = graph(self.database)
@property
def set_max_chromosome_mutation_rate(self: Attributes, value: Optional[float]) -> None: def active(self: AttributesProperties) -> Callable[[], bool]:
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)