diff --git a/README.md b/README.md index dcc786709c76a635bfabf7fa2828c6f13f9f8ec0..fed96643440b54c14b8c6c8c819e9349ec389c88 100755 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ Pythonic Genetic MPI paralelized Algorithm + # PyGMA Documentation ### Words @@ -78,6 +79,281 @@ From this it should create/instantiate (genotype -> phenotype) an individual on By conductiong the experiment the a fitness value is generated for the gene. This should be returned. +# Usage Guide +This section explains how to step by step use PyGMA. +PyGMA works with user defined components/classes. +As such one needs to define the components one want to use. +Then set them up insid the configuration file and tell PyGMA to use this configuration. + +### 1. Define Evolutionary Phase +The first step is to define the evolutionary phase. +This component will contain the experiment component for the individuals and the stop conditions for the evolutionary process. +It will be defined in the file ```components/evolutionary_phase.py``` + +As example we create the following phase: + +``` +class Simple_evolutionary_phase(Evolutionary_phase): + + def completed(self, population_fitness: list, epochs: int) -> bool: + """Called to check if the phase has reached its end.""" + # population_fitness will be a list with the max fitness value in + # each island population + max_fitness = max(population_fitness) + if max_fitness > 90.9: + return True + + if epochs > 300: + return True + + return False +``` +This class inherits all needed variables from the ```Evolutionary_phase``` base class. +We only define the stop condition by overriding the ```completed(self, population_fitness: list, epochs: int) -> bool``` function. +This function will be called by PyGMA to check if the evolutionary process has finised. +As such we define that if the maximal fitness for all populations is > 90.9 or the evolutionary epochs are > 300 we want to stop the process. + + +### 2. Define Genetic Operators +Next we going to define Genetic Operators (GO). +These will be applied to modifie the gene strings of the individuals. +GO can be stacked into list (operator stack) and each operator in that lis is applied in sequence one after another. +All operators will mainly define then function ```operate(self, old_population, new_population)```. +Here ```old_population``` are the genes that are untouched by the GO and are sorted in ascending order based on their fitness, meaning ```old_population[0]``` is the gene with the highest fitness. +```new_population``` is a set of genes which where altered or produced by the genetic operator stack i.e. current state of modifications in operator stack. +GO's are defined in the file ```components/genetic_operators.py``` + +For example we could define a binary (meaning that it will work with binary genes like gene="100101010001001") mutation operator: + +``` +class Mutation_operator(Genetic_operator): + """ + This class represents a binary mutation operator. + It will produce n new individuals by mutating n genes with + the propability defined by mutation rate + """ + + def __init__(self, mutation_rate, new_individuals, rng_seed=9): + self.mutation_rate = mutation_rate + # how many new individuals should this operator produce? + self.new_individuals = new_individuals + self.rng = np.random.default_rng(rng_seed) + + def operate(self, old_population, new_population): + """ + Apply mutation to all genes of the current op stack. + """ + # take n individuals from the old_population (sorted by fitness) and mutate them + for n in range(self.new_individuals): + gene = np.copy(old_population[n]) + gene_size = len(gene) + # mutation indexes + # having at least one mutation + mutations = int(self.mutation_rate*gene_size) + if mutations == 0: + mutations = 1 + indexes = self.rng.integers(low=0, high=gene_size, size=mutations) + # flip the bits at indexes + # note, ~ is like np.invert() + gene[indexes] = ~gene[indexes] + # append the new individual + new_population.append(gene) + return new_population +``` + + +### 3. Define Experiment +If we have sucessfully defined all the operators we want to use then we can define our experiment. +An experiment is a test in which the gene is evaluated and rated with a fitness. +It can be an optimisation task, it can be that one wants to evolve cratures that can swim [https://www.karlsims.com/evolved-virtual-creatures.html](https://) or any other imagination. +Experiments will be defined in the file ```components/experiment.py``` + +For simplicity reasons we will define a simple dummy function optimisation experiment: +We want to find x, y, z such that f(x, y, z) = x*2 + y*3 + z = 424242. + +``` +class Function_optimisation_experiment(Experiment): + """ + Find values for the function such that it matches a certain output + """ + + def f(self, x, y, z): + """The function we want to optimize""" + return x*2 + y*3 + z + + def bool2int(self, x): + r = 0 + for i, b in enumerate(x): + if b: + r += b << i + return r + + def conduct(self, gene) -> float: + # split gene into blocks that are binary representation + # of the numbers + numbers = np.split(gene, 3) + # convert the bins to ints + x = bool2int(numbers[0]) + y = bool2int(numbers[1]) + z = bool2int(numbers[2]) + + # define the objective output for our function + output = 424242 + + # calculate the output + r = self.f(x, y, z) + + # calc error and fitness + error = abs(r-output) + + # if error is small fitness is high + # make sure that if error can not be 0 :) + fitness = 1.0/(error + 0.00000000000000001) + + # return the fitness of this gene to pygma + return fitness +``` +We inherit from the ```Experiment``` class and define the function ``` def conduct(self, gene) -> float:``` +This function will be called by PyGMA on each gene to conduct the experiment and retrieve a fitness value for this gene. + + + +### 4. Define Config +Now that all parts are defined we can definen a config that will make use of all the components we just defined. + +we make a new file ```components/config_function_optimisation.py``` + +Inside this file we put the following content: + +``` +# import the components we defined for further use +from components.genetic_operators import Mutation_operator, Removal_operator +from components.evolutionary_phase import Simple_evolutionary_phase +from components.experiment import Function_optimisation_experiment + + +# ----------------------------- +# Genetic operator definition +# ----------------------------- +# Genetic operators can be used for: +# Populations +# Genetic Phases + +# our mutation operator will crate 3 genes with a mutation propability of 6% +# for each bit +OP_MUTATION = Mutation_operator(0.06, 3) + +# additionally we will use a crossover operator +OP_CROSSOVER = Single_point_crossover_operator(3) + +# since the mutation and crossover operator will create 3+3=6 genes we need to remove 6 genes +# from the population otherwise we will increase the population size in each +# epoch/generation +OP_REMOVAL = Removal_operator(6) + + + +# ----------------------------- +# Phase definition +# ----------------------------- +# Genetic phases allowing for evolution of individuals in +# changing environments or conditions. + +# phase 0 +# phase starting operators (applied at phase start) +# we will mutate in the beginning +PHASE0_SOP = [OP_MUTATION] + +# phase Experiment (conducted on individuals) +# we will use our Function_optimisation_experiment +PHASE0_EXPERIMENT = Function_optimisation_experiment() + +# phase definition +PHASE0 = Simple_evolutionary_phase( + 'Phase_0', PHASE0_SOP, PHASE0_EXPERIMENT) + + + +# ----------------------------- +# GA Configuration parameters +# ----------------------------- +# These are all general parameters for the Algorithm +# in form of a dictionary +CONFIG_FUNC_OP = { + # ------------- + # Processes + # -------------- + # use mpi parallelization + # If use_mpi4py_futures=False start with: + # mpiexec -n 3 python PyGMA.py + # or use threads if not specified via slurm + # mpiexec -n 3 --use-hwthread-cpus python PyGMA.py + 'use_mpi': False, + + # use mpi4py.futures + # this will dynamically spawn workers. + # NOTE: you have to execute the programm in this manner: + # mpiexec -n 3 python -m mpi4py.futures PyGMA.py + 'use_mpi4py_futures': False, + + # if mpi=False, how many local processes to use + # Note if > 1 it will spawn additonal processes that independently + # solves the experiment. Spawning takes time (memcopys etc) + # if the experiment is very simple having only one process + # handling everyting might be faster. + # start programm with: + # python PyGMA.py + 'num_local_processes': 1, + + + # ------------- + # Populations + # -------------- + # how many island populations to use + # is defined by the genetic operators stack lenght + # used to recombine/produce, mutate, extend... + 'genetic_operator_stack': [ + # pop 0 + [OP_REMOVAL, OP_CROSSOVER, OP_CROSSOVER], + # pop 1 + [OP_REMOVAL, OP_CROSSOVER, OP_MUTATION], + # pop 2 + [OP_REMOVAL, OP_CROSSOVER, OP_MUTATION] + ], + + # how many individuals per population + 'num_individuals': 30, + + # genome lenght for each individual + 'genome_length': 60, + + # init populations from defined gene strings + # this will ovveride: + # num_populations, num_individuals, genome_length + 'use_predefined_defined_genes': False, + 'predefined_genes': [ + # pop 0 + [[0, 0, 1], [0, 1, 1]], + # pop 1 + [[0, 1, 1], [1, 1, 1]] + ], + + # ------------- + # Evolutinary Phases + # -------------- + # defining the phases that will be sequantially evolved + 'evolutionary_phases': [ + PHASE0, PHASE0 + ] + +} +``` + +### 5. Run PyGMA +Now we can run PyGMA to start the evolution. +Since we told it in the configuration file to not use MPI we can start it in a single Python process by running + +```python PyGMA.py``` diff --git a/User_interface.pdf b/User_interface.pdf new file mode 100755 index 0000000000000000000000000000000000000000..35c632b1f20acde3a9dd58d156f9b2ed0708f816 Binary files /dev/null and b/User_interface.pdf differ diff --git a/src/components/config_logical_gate_construction.py b/src/components/config_logical_gate_construction.py index 3c6b84b1477961ae8414e2515c60b403c4f7e819..750cff8113e6fef3fa91f67e3425910c02e39c28 100755 --- a/src/components/config_logical_gate_construction.py +++ b/src/components/config_logical_gate_construction.py @@ -83,14 +83,14 @@ OP_REMOVAL = Removal_operator(90) # outputs, i.e. multiplies the numbers correctly # how many bits num a and b have? -num_bits = 3 +num_bits = 2 # calculating all possible combinations exp_inputs, exp_expected_outputs = n_bit_multiplication_inout(num_bits) num_logical_inputs = num_bits+num_bits # we have two numbers each having n bits num_logical_outputs = num_bits+num_bits# we need to save the result -num_logical_gates = 128 # how many logic gates we want to use +num_logical_gates = 42 # how many logic gates we want to use PHASE0_EXPERIMENT = Logical_circuit_experiment(exp_inputs, exp_expected_outputs, num_logical_inputs, num_logical_outputs, num_logical_gates) @@ -121,7 +121,7 @@ CONFIG_CIRCUIT_EVOLVE = { # mpiexec -n 3 python PyGMA.py # or use threads if not specified via slurm # mpiexec -n 3 --use-hwthread-cpus python PyGMA.py - 'use_mpi': True, + 'use_mpi': False, # use mpi4py.futures # this will dynamically spawn workers. diff --git a/src/components/evolutionary_phase.py b/src/components/evolutionary_phase.py index 3b28c13b0fdb5a5ff9f1079c491bc238950df328..af18d305607a114147d221430b7234ef5d70be20 100755 --- a/src/components/evolutionary_phase.py +++ b/src/components/evolutionary_phase.py @@ -23,7 +23,7 @@ class Evolutionary_phase: def completed(self, population_fitness: list, epochs: int) -> bool: """ Called to check if the phase has reached its end. - You have to return true or false depending on if the pase is finished or not. + You have to return true or false depending on if the phase is finished or not. For example: max_fitness = max(population_fitness) if max_fitness > 90.9: @@ -56,13 +56,14 @@ class Simple_evolutionary_phase(Evolutionary_phase): def completed(self, population_fitness: list, epochs: int) -> bool: """ - Called to check if the phase has reached its end. + Called to check if the phase has reached its end. + """ max_fitness = max(population_fitness) if max_fitness > 90.9: return True - if epochs > 3: + if epochs > 3000: return True return False diff --git a/src/components/experiment.py b/src/components/experiment.py index 785643ca4584cf862853227be27e93740d84983c..3c5a2484aaf540772375c2947b35242d7dc5b3a5 100755 --- a/src/components/experiment.py +++ b/src/components/experiment.py @@ -1,11 +1,8 @@ import numpy as np -# https://towardsdatascience.com/an-extensible-evolutionary-algorithm-example-in-python-7372c56a557b -# traveling salesman problem here + # -------------- # Interface idea # -------------- - - class Experiment: """ Class defining an experiment. @@ -48,15 +45,6 @@ class Experiment: # Experiments # -------------- - -def bool2int(x): - r = 0 - for i, b in enumerate(x): - if b: - r += b << i - return r - - class Function_optimisation_experiment(Experiment): """ Find values for the function such that it matches a certain output @@ -64,12 +52,19 @@ class Function_optimisation_experiment(Experiment): def f(self, x, y, z): return x*2 + y*3 + z + + def bool2int(self, x): + r = 0 + for i, b in enumerate(x): + if b: + r += b << i + return r def conduct(self, gene) -> float: # split gene into blocks that are binary representation # of the numbers numbers = np.split(gene, 3) - # convert the numbers to ints + # convert the bins to ints x = bool2int(numbers[0]) y = bool2int(numbers[1]) z = bool2int(numbers[2]) diff --git a/src/components/genetic_operators.py b/src/components/genetic_operators.py index 58a6498230710544b20c6502d24a1b232b9a003b..5097edb8a13024c419549aa9f3a58fe53f5723f8 100755 --- a/src/components/genetic_operators.py +++ b/src/components/genetic_operators.py @@ -104,7 +104,7 @@ class Mutation_operator(Genetic_operator): for n in range(self.new_individuals): # FIXME: # take not always the first ones take by asceding propability from all ones - gene = old_population[n] + gene = np.copy(old_population[0]) gene_size = len(gene) # mutation indexes # having at least one mutation @@ -170,6 +170,9 @@ class Single_point_crossover_operator(Genetic_operator): and append the newly generated genes into the population. """ # parent indexes in the population + # FIXME: + # do not always take the first one take with ascending + # propability pop_index = 0 for _ in range(self.num_offsprings): # get parent genes diff --git a/src/testing/Epoch_Amdahls_speedup.pdf b/src/testing/Epoch_Amdahls_speedup.pdf old mode 100644 new mode 100755 diff --git a/src/testing/Epoch_Amdahls_speedup.png b/src/testing/Epoch_Amdahls_speedup.png old mode 100644 new mode 100755 diff --git a/src/testing/Epoch_Gustavsons_speedup.pdf b/src/testing/Epoch_Gustavsons_speedup.pdf old mode 100644 new mode 100755 diff --git a/src/testing/Epoch_Gustavsons_speedup.png b/src/testing/Epoch_Gustavsons_speedup.png old mode 100644 new mode 100755 diff --git a/src/testing/Epoch_runtime.pdf b/src/testing/Epoch_runtime.pdf old mode 100644 new mode 100755 diff --git a/src/testing/Epoch_runtime.png b/src/testing/Epoch_runtime.png old mode 100644 new mode 100755 diff --git a/src/testing/Epoch_runtime_inlet.pdf b/src/testing/Epoch_runtime_inlet.pdf old mode 100644 new mode 100755 diff --git a/src/testing/Epoch_runtime_inlet.png b/src/testing/Epoch_runtime_inlet.png old mode 100644 new mode 100755 diff --git a/src/testing/Speedup_figures.ipynb b/src/testing/Speedup_figures.ipynb old mode 100644 new mode 100755