diff --git a/EasyGA/EasyGA.py b/EasyGA/EasyGA.py index 91a8083..26682fd 100644 --- a/EasyGA/EasyGA.py +++ b/EasyGA/EasyGA.py @@ -7,7 +7,7 @@ import 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 Gene as make_gene from structure import Population from structure import Chromosome from structure import Gene @@ -17,23 +17,23 @@ from examples import Fitness from termination import Termination # Parent/Survivor Selection Methods -from parent import Parent +from parent import Parent from survivor import Survivor # Genetic Operator Methods from crossover import Crossover -from mutation import Mutation +from mutation import Mutation # Default Attributes for the GA from attributes import Attributes # Database class -from database import SQLDatabase -from sqlite3 import Error +# from database import SQLDatabase +# from sqlite3 import Error # Graphing package -from database import MatplotlibGraph -import matplotlib.pyplot as plt +# from database import MatplotlibGraph +# import matplotlib.pyplot as plt class GA(Attributes): @@ -46,7 +46,6 @@ 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. @@ -63,9 +62,12 @@ class GA(Attributes): if self.population is None: self.initialize_population() - 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. + # 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() while cond1() and cond3(): @@ -74,10 +76,11 @@ 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) + # self.database.insert_config(self) + pass # Otherwise evolve the population. else: @@ -102,10 +105,9 @@ class GA(Attributes): if int(adapt_counter) < int(adapt_counter + self.adapt_rate): self.adapt() - number_of_generations -= 1 + number_of_generations -= 1 self.current_generation += 1 - def update_population(self: GA) -> None: """ Updates the population to the new population @@ -113,7 +115,6 @@ class GA(Attributes): """ self.population.update() - def reset_run(self: GA) -> None: """ Resets a run by re-initializing the @@ -123,7 +124,6 @@ class GA(Attributes): self.current_generation = 0 self.run += 1 - def adapt(self: GA) -> None: """Adapts the ga to hopefully get better results.""" @@ -134,7 +134,6 @@ 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 @@ -154,7 +153,7 @@ class GA(Attributes): # Difference between best and i-th chromosomes best_chromosome = self.population[0] - tol = lambda i: self.dist(best_chromosome, self.population[i]) + def tol(i): return 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: @@ -169,13 +168,14 @@ class GA(Attributes): self.max_gene_mutation_rate) # Weighted average of x and y - average = lambda x, y: weight * x + (1-weight) * y + def average(x, y): return 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.gene_mutation_rate = average(bounds[2], self.gene_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: """ @@ -202,7 +202,7 @@ class GA(Attributes): self.crossover_individual_impl( self.population[n], parent, - weight = -3/4, + weight=-3/4, ) # If negative weights can't be used or division by 0, use positive weight @@ -210,7 +210,7 @@ class GA(Attributes): self.crossover_individual_impl( self.population[n], parent, - weight = +1/4, + weight=+1/4, ) # Stop if we've filled up an entire population @@ -218,20 +218,19 @@ 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. @@ -252,15 +251,14 @@ 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[ - Union[MutableSequence[Chromosome], - Iterable[Chromosome]] - ] = None, - in_place: bool = True, - ) -> MutableSequence[Chromosome]: + self: GA, + chromosome_list: Optional[ + Union[MutableSequence[Chromosome], + Iterable[Chromosome]] + ] = None, + in_place: bool = True, + ) -> MutableSequence[Chromosome]: """ Sorts the chromosome list by fitness based on fitness type. 1st element has best fitness. @@ -315,7 +313,6 @@ 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. @@ -339,7 +336,6 @@ 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 @@ -376,23 +372,19 @@ 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]}") diff --git a/EasyGA/attributes.py b/EasyGA/attributes.py index 82d8572..6d521c0 100644 --- a/EasyGA/attributes.py +++ b/EasyGA/attributes.py @@ -6,8 +6,8 @@ from dataclasses import dataclass, field, _MISSING_TYPE 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 @@ -19,12 +19,13 @@ 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 database import SQLDatabase, MatplotlibGraph, SQLDatabase as Database, MatplotlibGraph as Graph #========================================# # Default methods not defined elsewhere. # #========================================# + def rand_1_to_10(self: Attributes) -> int: """ Default gene_impl, returning a random integer from 1 to 10. @@ -36,6 +37,7 @@ def rand_1_to_10(self: Attributes) -> int: """ return random.randint(1, 10) + def use_genes(self: Attributes) -> Iterator[Any]: """ Default chromosome_impl, generates a chromosome using the gene_impl and chromosome length. @@ -55,6 +57,7 @@ def use_genes(self: Attributes) -> Iterator[Any]: for _ in range(self.chromosome_length): yield self.gene_impl() + def use_chromosomes(self: Attributes) -> Iterator[Iterable[Any]]: """ Default population_impl, generates a population using the chromosome_impl and population size. @@ -74,6 +77,7 @@ def use_chromosomes(self: Attributes) -> Iterator[Iterable[Any]]: for _ in range(self.population_size): yield self.chromosome_impl() + def dist_fitness(self: Attributes, chromosome_1: Chromosome, chromosome_2: Chromosome) -> float: """ Measures the distance between two chromosomes based on their fitnesses. @@ -90,6 +94,7 @@ def dist_fitness(self: Attributes, chromosome_1: Chromosome, chromosome_2: Chrom """ return sqrt(abs(chromosome_1.fitness - chromosome_2.fitness)) + def simple_linear(self: Attributes, weight: float) -> float: """ Returns a random value between 0 and 1, with increased probability @@ -181,26 +186,27 @@ class AttributesData: parent_selection_impl: Callable[["Attributes"], None] = None crossover_individual_impl: Callable[["Attributes"], None] = None - crossover_population_impl: Callable[["Attributes", Chromosome, Chromosome], 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 - ); - """ + # 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 + # graph: Callable[[Database], Graph] = MatplotlibGraph def __post_init__(self: AttributesData) -> None: """ @@ -311,7 +317,8 @@ class Attributes(AttributesData): chromosome : Chromosome The chromosome to be saved. """ - self.database.insert_current_chromosome(self.current_generation, chromosome) + self.database.insert_current_chromosome( + self.current_generation, chromosome) #===========================# # Descriptors which convert # @@ -327,13 +334,20 @@ class Attributes(AttributesData): 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) + 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: # @@ -346,7 +360,8 @@ class Attributes(AttributesData): @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.") + raise ValueError( + "ga.run counter must be an integer greater than or equal to 0.") vars(self)["run"] = value @property @@ -356,7 +371,8 @@ class Attributes(AttributesData): @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") + raise ValueError( + "ga.current_generation must be an integer greater than or equal to 0") vars(self)["current_generation"] = value @property @@ -366,7 +382,8 @@ class Attributes(AttributesData): @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.") + raise ValueError( + "ga.chromosome_length must be an integer greater than and not equal to 0.") vars(self)["chromosome_length"] = value @property @@ -376,7 +393,8 @@ class Attributes(AttributesData): @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.") + raise ValueError( + "ga.population_size must be an integer greater than and not equal to 0.") vars(self)["population_size"] = value @property @@ -393,7 +411,8 @@ class Attributes(AttributesData): if value is None or (isinstance(value, (float, int)) and 0 <= value <= 1): vars(self)["max_chromosome_mutation_rate"] = value else: - raise ValueError("Max chromosome mutation rate must be between 0 and 1") + raise ValueError( + "Max chromosome mutation rate must be between 0 and 1") @property def min_chromosome_mutation_rate(self: AttributesProperties) -> float: @@ -409,26 +428,27 @@ class Attributes(AttributesData): if value is None or (isinstance(value, (float, int)) and 0 <= value <= 1): vars(self)["min_chromosome_mutation_rate"] = value else: - raise ValueError("Min chromosome mutation rate must be between 0 and 1") + raise ValueError( + "Min chromosome mutation rate must be between 0 and 1") - @property - def database_name(self: AttributesProperties) -> str: - return vars(self)["database_name"] + # @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 + # @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 - @property - def graph(self: AttributesProperties) -> Graph: - return vars(self)["graph"] + # @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) + # @graph.setter + # def graph(self: AttributesProperties, graph: Callable[[Database], Graph]) -> None: + # vars(self)["graph"] = graph(self.database) @property def active(self: AttributesProperties) -> Callable[[], bool]: diff --git a/README.md b/README.md index 773a6dc..919828d 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # EasyGA - Genetic Algorithms made Easy -EasyGA is a python package designed to provide an easy-to-use Genetic Algorithm. The package is designed to work right out of the box, while also allowing the user to customize features as they see fit. +EasyGA is a python package designed to provide an easy-to-use Genetic Algorithm. The package is designed to work right out of the box, while also allowing the user to customize features as they see fit. ## Check out our [Wiki](https://github.com/danielwilczak101/EasyGA/wiki) or [Youtube](https://www.youtube.com/watch?v=jbuDKwIiYBw) for more information. @@ -15,7 +15,9 @@ pip3 install EasyGA ``` ## Getting started with EasyGA(Basic Example): + The goal of the basic example is to get all 5's in the chromosome. + ```Python import EasyGA @@ -31,6 +33,7 @@ ga.print_population() ``` ### Output: + ```bash Current Generation : 15 Current population: @@ -47,6 +50,7 @@ Chromosome - 9 [7][2][8][10][3][5][5][8][1][7] / Fitness = 2 ``` ## Getting started with EasyGA (Password Cracker Example): + ```Python import EasyGA import random @@ -94,8 +98,9 @@ ga.graph.show() ``` ## Ouput: + ``` -Please enter a word: +Please enter a word: EasyGA Current Generation : 44 Chromosome - 0 [E][a][s][y][G][A] / Fitness = 6 @@ -113,8 +118,8 @@ Chromosome - 9 [E][a][s][Y][G][A] / Fitness = 5 ## Issues -We would love to know if your having any issues. Please start a new issue on the [Issues Page](https://github.com/danielwilczak101/EasyGA/issues). +We would love to know if your having any issues. Please start a new issue on the [Issues Page](https://github.com/danielwilczak101/EasyGA/issues). ## Local System Approach @@ -123,6 +128,7 @@ Download the repository to some folder on your computer. ``` https://github.com/danielwilczak101/EasyGA/archive/master.zip ``` -Use the run.py file inside the EasyGA folder to run your code. This is a local version of the package. + +Use the run.py file inside the EasyGA folder to run your code. This is a local version of the package. ## Check out our [wiki](https://github.com/danielwilczak101/EasyGA/wiki) for more information. diff --git a/setup.py b/setup.py index 5fe3294..4ca6a07 100644 --- a/setup.py +++ b/setup.py @@ -13,16 +13,17 @@ setuptools.setup( url="https://github.com/danielwilczak101/EasyGA", author="Daniel Wilczak, Jack RyanNguyen, Ryley Griffith, Jared Curtis, Matthew Chase Oxamendi ", author_email="danielwilczak101@gmail.com", - long_description = long_description, - long_description_content_type = "text/markdown", + long_description=long_description, + long_description_content_type="text/markdown", classifier=[ "Programming Language :: Python :: 3", "License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)", "Operating System :: OS Independent", - ], - install_requires = ["matplotlib ~= 3.3.2", - "pyserial ~= 3.4", - "pytest>=3.7", - "tabulate >=0.8.7" - ], - ) + ], + install_requires=[ + # "matplotlib ~= 3.3.2", + # "pyserial ~= 3.4", + "pytest>=3.7", + "tabulate >=0.8.7" + ], +)