From fb67d8dbccc1808fbe9d5035af838e08bdff0223 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Fri, 26 Jan 2024 23:45:26 -0500 Subject: [PATCH 1/6] Fix typos --- docs/source/pygad.rst | 25 +++++++++++-------------- docs/source/pygad_more.rst | 6 +++--- docs/source/releases.rst | 25 +++++++++++++++++++++++-- pygad/pygad.py | 6 +++--- 4 files changed, 40 insertions(+), 22 deletions(-) diff --git a/docs/source/pygad.rst b/docs/source/pygad.rst index 9a512f6..560964c 100644 --- a/docs/source/pygad.rst +++ b/docs/source/pygad.rst @@ -920,9 +920,7 @@ It accepts the following parameters: - ``pop_fitness=None``: An optional parameter that accepts a list of the fitness values of the solutions in the population. If ``None``, then the ``cal_pop_fitness()`` method is called to calculate the - fitness values of the ``self.population``. Use - ``ga_instance.last_generation_fitness`` to use latest fitness value - and skip recalculation of the population fitness. + fitness values of the population. It returns the following: @@ -1039,11 +1037,10 @@ Let's discuss how to do each of these steps. Preparing the ``fitness_func`` Parameter ----------------------------------------- -Even there are some steps in the genetic algorithm pipeline that can -work the same regardless of the problem being solved, one critical step -is the calculation of the fitness value. There is no unique way of -calculating the fitness value and it changes from one problem to -another. +Even though some steps in the genetic algorithm pipeline can work the +same regardless of the problem being solved, one critical step is the +calculation of the fitness value. There is no unique way of calculating +the fitness value and it changes from one problem to another. PyGAD has a parameter called ``fitness_func`` that allows the user to specify a custom function/method to use when calculating the fitness. @@ -1060,15 +1057,15 @@ optimization problem is single-objective or multi-objective. ``pygad.GA`` class. - If the fitness function returns a ``list``, ``tuple``, or - ``numpy.ndarray``, then the problem is single-objective. Even if - there is only one element, the problem is still considered - multi-objective. Each element represents the fitness value of its - corresponding objective. + ``numpy.ndarray``, then the problem is multi-objective. Even if there + is only one element, the problem is still considered multi-objective. + Each element represents the fitness value of its corresponding + objective. Using a user-defined fitness function allows the user to freely use PyGAD to solve any problem by passing the appropriate fitness -function/method. It is very important to understand the problem well for -creating it. +function/method. It is very important to understand the problem well +before creating it. Let's discuss an example: diff --git a/docs/source/pygad_more.rst b/docs/source/pygad_more.rst index 33c3442..6eb7e81 100644 --- a/docs/source/pygad_more.rst +++ b/docs/source/pygad_more.rst @@ -595,8 +595,8 @@ After running the code again, it will find the same result. [ 2.77249188 -4.06570662 0.04196872 -3.47770796 -0.57502138 -3.22775267] 0.04872203136549972 -Continue without Loosing Progress -================================= +Continue without Losing Progress +================================ In `PyGAD 2.18.0 `__, @@ -615,7 +615,7 @@ call to the ``run()`` method. 4. ``self.solutions_fitness`` This helps the user to continue where the last run stopped without -loosing the values of these 4 attributes. +losing the values of these 4 attributes. Now, the user can save the model by calling the ``save()`` method. diff --git a/docs/source/releases.rst b/docs/source/releases.rst index 035a0a0..6a81de2 100644 --- a/docs/source/releases.rst +++ b/docs/source/releases.rst @@ -1020,8 +1020,8 @@ Release Date: 9 September 2022 generation. Another advantage happens when the instance is loaded and the ``run()`` method is called, as the old fitness value are shown on the graph alongside with the new fitness values. Read more in this - section: `Continue without Loosing - Progress `__ + section: `Continue without Losing + Progress `__ 4. Thanks `Prof. Fernando Jiménez Barrionuevo `__ (Dept. of Information and @@ -1464,6 +1464,27 @@ Release Date 7 September 2023 class is removed. Instead, please use the ``plot_fitness()`` if you did not upgrade yet. +.. _pygad-321: + +PyGAD 3.2.1 +----------- + +Release Date ... 2023 + +1. Fix a bug when multi-objective optimization is used with batch + fitness calculation (e.g. ``fitness_batch_size`` set to a non-zero + number). + +2. Fix a bug in the ``pygad.py`` script when finding the index of the + best solution. It does not work properly with multi-objective + optimization where ``self.best_solutions_fitness`` have multiple + columns. + + .. code:: python + + self.best_solution_generation = numpy.where(numpy.array( + self.best_solutions_fitness) == numpy.max(numpy.array(self.best_solutions_fitness)))[0][0] + PyGAD Projects at GitHub ======================== diff --git a/pygad/pygad.py b/pygad/pygad.py index bec6aa5..6a6a75e 100644 --- a/pygad/pygad.py +++ b/pygad/pygad.py @@ -726,7 +726,7 @@ def __init__(self, if self.mutation_probability is None: if not self.suppress_warnings: warnings.warn( - f"The percentage of genes to mutate (mutation_percent_genes={mutation_percent_genes}) resutled in selecting ({mutation_num_genes}) genes. The number of genes to mutate is set to 1 (mutation_num_genes=1).\nIf you do not want to mutate any gene, please set mutation_type=None.") + f"The percentage of genes to mutate (mutation_percent_genes={mutation_percent_genes}) resulted in selecting ({mutation_num_genes}) genes. The number of genes to mutate is set to 1 (mutation_num_genes=1).\nIf you do not want to mutate any gene, please set mutation_type=None.") mutation_num_genes = 1 elif type(mutation_percent_genes) in GA.supported_int_float_types: @@ -745,7 +745,7 @@ def __init__(self, if mutation_num_genes == 0: if self.mutation_probability is None: if not self.suppress_warnings: - warnings.warn(f"The percentage of genes to mutate (mutation_percent_genes={mutation_percent_genes}) resutled in selecting ({mutation_num_genes}) genes. The number of genes to mutate is set to 1 (mutation_num_genes=1).\nIf you do not want to mutate any gene, please set mutation_type=None.") + warnings.warn(f"The percentage of genes to mutate (mutation_percent_genes={mutation_percent_genes}) resulted in selecting ({mutation_num_genes}) genes. The number of genes to mutate is set to 1 (mutation_num_genes=1).\nIf you do not want to mutate any gene, please set mutation_type=None.") mutation_num_genes = 1 else: self.valid_parameters = False @@ -771,7 +771,7 @@ def __init__(self, # Based on the mutation percentage of genes, if the number of selected genes for mutation is less than the least possible value which is 1, then the number will be set to 1. if mutation_num_genes[idx] == 0: if not self.suppress_warnings: - warnings.warn(f"The percentage of genes to mutate ({mutation_percent_genes[idx]}) resutled in selecting ({mutation_num_genes[idx]}) genes. The number of genes to mutate is set to 1 (mutation_num_genes=1).\nIf you do not want to mutate any gene, please set mutation_type=None.") + warnings.warn(f"The percentage of genes to mutate ({mutation_percent_genes[idx]}) resulted in selecting ({mutation_num_genes[idx]}) genes. The number of genes to mutate is set to 1 (mutation_num_genes=1).\nIf you do not want to mutate any gene, please set mutation_type=None.") mutation_num_genes[idx] = 1 if mutation_percent_genes[0] < mutation_percent_genes[1]: if not self.suppress_warnings: From 7748e2e16f615c584e6d4a3a8aac8ab6b97991dc Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Sat, 27 Jan 2024 18:11:21 -0500 Subject: [PATCH 2/6] mutation using dict gene_space --- pygad/utils/mutation.py | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/pygad/utils/mutation.py b/pygad/utils/mutation.py index 9aac12f..d0ca1b4 100644 --- a/pygad/utils/mutation.py +++ b/pygad/utils/mutation.py @@ -89,14 +89,40 @@ def mutation_by_space(self, offspring): # The numpy.random.choice() and numpy.random.uniform() functions return a NumPy array as the output even if the array has a single value. # We have to return the output at index 0 to force a numeric value to be returned not an object of type numpy.ndarray. # If numpy.ndarray is returned, then it will cause an issue later while using the set() function. + # Randomly select a value from a discrete range. value_from_space = numpy.random.choice(numpy.arange(start=curr_gene_space['low'], stop=curr_gene_space['high'], step=curr_gene_space['step']), size=1)[0] else: - value_from_space = numpy.random.uniform(low=curr_gene_space['low'], - high=curr_gene_space['high'], - size=1)[0] + # Return the current gene value. + value_from_space = offspring[offspring_idx, gene_idx] + # Generate a random value to be added to the current gene value. + rand_val = numpy.random.uniform(low=range_min, + high=range_max, + size=1)[0] + # The objective is to have a new gene value that respects the gene_space boundaries. + # The next if-else block checks if adding the random value keeps the new gene value within the gene_space boundaries. + temp_val = value_from_space + rand_val + if temp_val < curr_gene_space['low']: + # Restrict the new value to be > curr_gene_space['low'] + # If subtracting the random value makes the new gene value outside the boundaries [low, high), then use the lower boundary the gene value. + if curr_gene_space['low'] <= value_from_space - rand_val < curr_gene_space['high']: + # Because subtracting the random value keeps the new gene value within the boundaries [low, high), then use such a value as the gene value. + temp_val = value_from_space - rand_val + else: + # Because subtracting the random value makes the new gene value outside the boundaries [low, high), then use the lower boundary as the gene value. + temp_val = curr_gene_space['low'] + elif temp_val >= curr_gene_space['high']: + # Restrict the new value to be < curr_gene_space['high'] + # If subtracting the random value makes the new gene value outside the boundaries [low, high), then use such a value as the gene value. + if curr_gene_space['low'] <= value_from_space - rand_val < curr_gene_space['high']: + # Because subtracting the random value keeps the new value within the boundaries [low, high), then use such a value as the gene value. + temp_val = value_from_space - rand_val + else: + # Because subtracting the random value makes the new gene value outside the boundaries [low, high), then use the lower boundary as the gene value. + temp_val = curr_gene_space['low'] + value_from_space = temp_val else: # Selecting a value randomly based on the current gene's space in the 'gene_space' attribute. # If the gene space has only 1 value, then select it. The old and new values of the gene are identical. From 08c0840b5538e93318e7fc941116f05d12206cf3 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Sat, 27 Jan 2024 18:23:30 -0500 Subject: [PATCH 3/6] Change versions --- pygad/__init__.py | 2 +- pygad/utils/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pygad/__init__.py b/pygad/__init__.py index c098928..e674105 100644 --- a/pygad/__init__.py +++ b/pygad/__init__.py @@ -1,3 +1,3 @@ from .pygad import * # Relative import. -__version__ = "3.2.0" +__version__ = "3.3.0" diff --git a/pygad/utils/__init__.py b/pygad/utils/__init__.py index 95bf6e5..093cf85 100644 --- a/pygad/utils/__init__.py +++ b/pygad/utils/__init__.py @@ -3,4 +3,4 @@ from pygad.utils import mutation from pygad.utils import nsga2 -__version__ = "1.1.0" \ No newline at end of file +__version__ = "1.1.1" \ No newline at end of file From d06223483386af0a4d1f24de57e6117c0b83e917 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Sun, 28 Jan 2024 00:55:27 -0500 Subject: [PATCH 4/6] Support object data type --- examples/example_parallel_processing.py | 39 +++++++++++++++++++++++++ pygad/pygad.py | 13 +++++---- 2 files changed, 46 insertions(+), 6 deletions(-) create mode 100644 examples/example_parallel_processing.py diff --git a/examples/example_parallel_processing.py b/examples/example_parallel_processing.py new file mode 100644 index 0000000..9efd1ea --- /dev/null +++ b/examples/example_parallel_processing.py @@ -0,0 +1,39 @@ +import pygad +import numpy + +function_inputs = [4,-2,3.5,5,-11,-4.7] # Function inputs. +desired_output = 44 # Function output. + +def fitness_func(ga_instance, solution, solution_idx): + output = numpy.sum(solution*function_inputs) + fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) + return fitness + +last_fitness = 0 +def on_generation(ga_instance): + global last_fitness + print(f"Generation = {ga_instance.generations_completed}") + print(f"Fitness = {ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness)[1]}") + print(f"Change = {ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness)[1] - last_fitness}") + last_fitness = ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness)[1] + +if __name__ == '__main__': + ga_instance = pygad.GA(num_generations=100, + num_parents_mating=10, + sol_per_pop=20, + num_genes=len(function_inputs), + fitness_func=fitness_func, + on_generation=on_generation, + # parallel_processing=['process', 2], + parallel_processing=['thread', 2] + ) + + # Running the GA to optimize the parameters of the function. + ga_instance.run() + + # Returning the details of the best solution. + solution, solution_fitness, solution_idx = ga_instance.best_solution(ga_instance.last_generation_fitness) + print(f"Parameters of the best solution : {solution}") + print(f"Fitness value of the best solution = {solution_fitness}") + print(f"Index of the best solution : {solution_idx}") + diff --git a/pygad/pygad.py b/pygad/pygad.py index 6a6a75e..e02a9ad 100644 --- a/pygad/pygad.py +++ b/pygad/pygad.py @@ -20,9 +20,10 @@ class GA(utils.parent_selection.ParentSelection, visualize.plot.Plot): supported_int_types = [int, numpy.int8, numpy.int16, numpy.int32, numpy.int64, - numpy.uint, numpy.uint8, numpy.uint16, numpy.uint32, numpy.uint64] - supported_float_types = [ - float, numpy.float16, numpy.float32, numpy.float64] + numpy.uint, numpy.uint8, numpy.uint16, numpy.uint32, numpy.uint64, + object] + supported_float_types = [float, numpy.float16, numpy.float32, numpy.float64, + object] supported_int_float_types = supported_int_types + supported_float_types def __init__(self, @@ -726,7 +727,7 @@ def __init__(self, if self.mutation_probability is None: if not self.suppress_warnings: warnings.warn( - f"The percentage of genes to mutate (mutation_percent_genes={mutation_percent_genes}) resulted in selecting ({mutation_num_genes}) genes. The number of genes to mutate is set to 1 (mutation_num_genes=1).\nIf you do not want to mutate any gene, please set mutation_type=None.") + f"The percentage of genes to mutate (mutation_percent_genes={mutation_percent_genes}) resutled in selecting ({mutation_num_genes}) genes. The number of genes to mutate is set to 1 (mutation_num_genes=1).\nIf you do not want to mutate any gene, please set mutation_type=None.") mutation_num_genes = 1 elif type(mutation_percent_genes) in GA.supported_int_float_types: @@ -745,7 +746,7 @@ def __init__(self, if mutation_num_genes == 0: if self.mutation_probability is None: if not self.suppress_warnings: - warnings.warn(f"The percentage of genes to mutate (mutation_percent_genes={mutation_percent_genes}) resulted in selecting ({mutation_num_genes}) genes. The number of genes to mutate is set to 1 (mutation_num_genes=1).\nIf you do not want to mutate any gene, please set mutation_type=None.") + warnings.warn(f"The percentage of genes to mutate (mutation_percent_genes={mutation_percent_genes}) resutled in selecting ({mutation_num_genes}) genes. The number of genes to mutate is set to 1 (mutation_num_genes=1).\nIf you do not want to mutate any gene, please set mutation_type=None.") mutation_num_genes = 1 else: self.valid_parameters = False @@ -771,7 +772,7 @@ def __init__(self, # Based on the mutation percentage of genes, if the number of selected genes for mutation is less than the least possible value which is 1, then the number will be set to 1. if mutation_num_genes[idx] == 0: if not self.suppress_warnings: - warnings.warn(f"The percentage of genes to mutate ({mutation_percent_genes[idx]}) resulted in selecting ({mutation_num_genes[idx]}) genes. The number of genes to mutate is set to 1 (mutation_num_genes=1).\nIf you do not want to mutate any gene, please set mutation_type=None.") + warnings.warn(f"The percentage of genes to mutate ({mutation_percent_genes[idx]}) resutled in selecting ({mutation_num_genes[idx]}) genes. The number of genes to mutate is set to 1 (mutation_num_genes=1).\nIf you do not want to mutate any gene, please set mutation_type=None.") mutation_num_genes[idx] = 1 if mutation_percent_genes[0] < mutation_percent_genes[1]: if not self.suppress_warnings: From 1615baf463ce428f9c56034c78c5690f34ca9f0a Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Sun, 28 Jan 2024 01:00:57 -0500 Subject: [PATCH 5/6] Use raise instead of sys.exit --- pygad/pygad.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/pygad/pygad.py b/pygad/pygad.py index e02a9ad..40cc4e4 100644 --- a/pygad/pygad.py +++ b/pygad/pygad.py @@ -9,7 +9,6 @@ from pygad import utils from pygad import helper from pygad import visualize -import sys # Extend all the classes so that they can be referenced by just the `self` object of the `pygad.GA` class. class GA(utils.parent_selection.ParentSelection, @@ -446,7 +445,7 @@ def __init__(self, raise TypeError(f"The value assigned to the 'initial_population' parameter is expected to by of type list, tuple, or ndarray but {type(initial_population)} found.") elif numpy.array(initial_population).ndim != 2: self.valid_parameters = False - raise ValueError(f"A 2D list is expected to the initail_population parameter but a ({numpy.array(initial_population).ndim}-D) list found.") + raise ValueError(f"A 2D list is expected to the initial_population parameter but a ({numpy.array(initial_population).ndim}-D) list found.") else: # Validate the type of each value in the 'initial_population' parameter. for row_idx in range(len(initial_population)): @@ -1333,7 +1332,8 @@ def __init__(self, self.pareto_fronts = None except Exception as e: self.logger.exception(e) - sys.exit(-1) + # sys.exit(-1) + raise e def round_genes(self, solutions): for gene_idx in range(self.num_genes): @@ -1867,7 +1867,8 @@ def cal_pop_fitness(self): pop_fitness = numpy.array(pop_fitness) except Exception as ex: self.logger.exception(ex) - sys.exit(-1) + # sys.exit(-1) + raise ex return pop_fitness def run(self): @@ -2242,7 +2243,8 @@ def run(self): # self.solutions = numpy.array(self.solutions) except Exception as ex: self.logger.exception(ex) - sys.exit(-1) + # sys.exit(-1) + raise ex def best_solution(self, pop_fitness=None): """ @@ -2277,7 +2279,8 @@ def best_solution(self, pop_fitness=None): best_solution_fitness = pop_fitness[best_match_idx] except Exception as ex: self.logger.exception(ex) - sys.exit(-1) + # sys.exit(-1) + raise ex return best_solution, best_solution_fitness, best_match_idx From f83c9732183a7e6fd4978199df7f314cbadf3bf4 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Mon, 29 Jan 2024 01:20:29 -0500 Subject: [PATCH 6/6] PyGAD 3.3.0 Release Date 29 January 2024 1. Solve bugs when multi-objective optimization is used. https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/238 2. When the `stop_ciiteria` parameter is used with the `reach` keyword, then multiple numeric values can be passed when solving a multi-objective problem. For example, if a problem has 3 objective functions, then `stop_criteria="reach_10_20_30"` means the GA stops if the fitness of the 3 objectives are at least 10, 20, and 30, respectively. The number values must match the number of objective functions. If a single value found (e.g. `stop_criteria=reach_5`) when solving a multi-objective problem, then it is used across all the objectives. https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/238 3. The `delay_after_gen` parameter is now deprecated and will be removed in a future release. If it is necessary to have a time delay after each generation, then assign a callback function/method to the `on_generation` parameter to pause the evolution. 4. Parallel processing now supports calculating the fitness during adaptive mutation. https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/201 5. The population size can be changed during runtime by changing all the parameters that would affect the size of any thing used by the GA. For more information, check the [Change Population Size during Runtime](https://pygad.readthedocs.io/en/latest/pygad_more.html#change-population-size-during-runtime) section. https://github.com/ahmedfgad/GeneticAlgorithmPython/discussions/234 6. When a dictionary exists in the `gene_space` parameter without a step, then mutation occurs by adding a random value to the gene value. The random vaue is generated based on the 2 parameters `random_mutation_min_val` and `random_mutation_max_val`. For more information, check the [How Mutation Works with the gene_space Parameter?](https://pygad.readthedocs.io/en/latest/pygad_more.html#how-mutation-works-with-the-gene-space-parameter) section. https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/229 7. Add `object` as a supported data type for int (GA.supported_int_types) and float (GA.supported_float_types). https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/174 8. Use the `raise` clause instead of the `sys.exit(-1)` to terminate the execution. https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/213 9. Fix a bug when multi-objective optimization is used with batch fitness calculation (e.g. `fitness_batch_size` set to a non-zero number). 10. Fix a bug in the `pygad.py` script when finding the index of the best solution. It does not work properly with multi-objective optimization where `self.best_solutions_fitness` have multiple columns. --- docs/source/conf.py | 4 +- docs/source/pygad_more.rst | 108 +++++++++++++++++++- docs/source/releases.rst | 73 ++++++++++--- examples/example_dynamic_population_size.py | 87 +++++++++++----- pyproject.toml | 2 +- setup.py | 2 +- 6 files changed, 231 insertions(+), 45 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 9dccbea..6619c49 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -18,11 +18,11 @@ # -- Project information ----------------------------------------------------- project = 'PyGAD' -copyright = '2023, Ahmed Fawzy Gad' +copyright = '2024, Ahmed Fawzy Gad' author = 'Ahmed Fawzy Gad' # The full version, including alpha/beta/rc tags -release = '3.2.0' +release = '3.3.0' master_doc = 'index' diff --git a/docs/source/pygad_more.rst b/docs/source/pygad_more.rst index 6eb7e81..efa3e8b 100644 --- a/docs/source/pygad_more.rst +++ b/docs/source/pygad_more.rst @@ -344,10 +344,13 @@ is applied based on this parameter. How Mutation Works with the ``gene_space`` Parameter? ----------------------------------------------------- -If a gene has its static space defined in the ``gene_space`` parameter, -then mutation works by replacing the gene value by a value randomly -selected from the gene space. This happens for both ``int`` and -``float`` data types. +Mutation changes based on whether the ``gene_space`` has a continuous +range or discrete set of values. + +If a gene has its **static/discrete space** defined in the +``gene_space`` parameter, then mutation works by replacing the gene +value by a value randomly selected from the gene space. This happens for +both ``int`` and ``float`` data types. For example, the following ``gene_space`` has the static space ``[1, 2, 3]`` defined for the first gene. So, this gene can only have a @@ -377,6 +380,39 @@ If its current value is 5 and the random value is ``-0.5``, then the new value is 4.5. If the gene type is integer, then the value will be rounded. +On the other hand, if a gene has a **continuous space** defined in the +``gene_space`` parameter, then mutation occurs by adding a random value +to the current gene value. + +For example, the following ``gene_space`` has the continuous space +defined by the dictionary ``{'low': 1, 'high': 5}``. This applies to all +genes. So, mutation is applied to one or more selected genes by adding a +random value to the current gene value. + +.. code:: python + + Gene space: {'low': 1, 'high': 5} + Solution: [1.5, 3.4] + +Assuming ``random_mutation_min_val=-1`` and +``random_mutation_max_val=1``, then a random value such as ``0.3`` can +be added to the gene(s) participating in mutation. If only the first +gene is mutated, then its new value changes from ``1.5`` to +``1.5+0.3=1.8``. Note that PyGAD verifies that the new value is within +the range. In the worst scenarios, the value will be set to either +boundary of the continuous range. For example, if the gene value is 1.5 +and the random value is -0.55, then the new value is 0.95 which smaller +than the lower boundary 1. Thus, the gene value will be rounded to 1. + +If the dictionary has a step like the example below, then it is +considered a discrete range and mutation occurs by randomly selecting a +value from the set of values. In other words, no random value is added +to the gene value. + +.. code:: python + + Gene space: {'low': 1, 'high': 5, 'step': 0.5} + Stop at Any Generation ====================== @@ -663,6 +699,70 @@ Note that the 2 attributes (``self.best_solutions`` and attributes (``self.solutions`` and ``self.solutions_fitness``) only work if the ``save_solutions`` parameter is ``True``. +Change Population Size during Runtime +===================================== + +Starting from `PyGAD +3.3.0 `__, +the population size can changed during runtime. In other words, the +number of solutions/chromosomes and number of genes can be changed. + +The user has to carefully arrange the list of *parameters* and *instance +attributes* that have to be changed to keep the GA consistent before and +after changing the population size. Generally, change everything that +would be used during the GA evolution. + + CAUTION: If the user failed to change a parameter or an instance + attributes necessary to keep the GA running after the population size + changed, errors will arise. + +These are examples of the parameters that the user should decide whether +to change. The user should check the `list of +parameters `__ +and decide what to change. + +1. ``population``: The population. It *must* be changed. + +2. ``num_offspring``: The number of offspring to produce out of the + crossover and mutation operations. Change this parameter if the + number of offspring have to be changed to be consistent with the new + population size. + +3. ``num_parents_mating``: The number of solutions to select as parents. + Change this parameter if the number of parents have to be changed to + be consistent with the new population size. + +4. ``fitness_func``: If the way of calculating the fitness changes after + the new population size, then the fitness function have to be + changed. + +5. ``sol_per_pop``: The number of solutions per population. It is not + critical to change it but it is recommended to keep this number + consistent with the number of solutions in the ``population`` + parameter. + +These are examples of the instance attributes that might be changed. The +user should check the `list of instance +attributes `__ +and decide what to change. + +1. All the ``last_generation_*`` parameters + + 1. ``last_generation_fitness``: A 1D NumPy array of fitness values of + the population. + + 2. ``last_generation_parents`` and + ``last_generation_parents_indices``: Two NumPy arrays: 2D array + representing the parents and 1D array of the parents indices. + + 3. ``last_generation_elitism`` and + ``last_generation_elitism_indices``: Must be changed if + ``keep_elitism != 0``. The default value of ``keep_elitism`` is 1. + Two NumPy arrays: 2D array representing the elitism and 1D array + of the elitism indices. + +2. ``pop_size``: The population size. + Prevent Duplicates in Gene Values ================================= diff --git a/docs/source/releases.rst b/docs/source/releases.rst index 6a81de2..2674eb1 100644 --- a/docs/source/releases.rst +++ b/docs/source/releases.rst @@ -1464,26 +1464,73 @@ Release Date 7 September 2023 class is removed. Instead, please use the ``plot_fitness()`` if you did not upgrade yet. -.. _pygad-321: +.. _pygad-330: -PyGAD 3.2.1 +PyGAD 3.3.0 ----------- -Release Date ... 2023 +Release Date 29 January 2024 -1. Fix a bug when multi-objective optimization is used with batch - fitness calculation (e.g. ``fitness_batch_size`` set to a non-zero - number). +1. Solve bugs when multi-objective optimization is used. + https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/238 -2. Fix a bug in the ``pygad.py`` script when finding the index of the - best solution. It does not work properly with multi-objective - optimization where ``self.best_solutions_fitness`` have multiple - columns. +2. When the ``stop_ciiteria`` parameter is used with the ``reach`` + keyword, then multiple numeric values can be passed when solving a + multi-objective problem. For example, if a problem has 3 objective + functions, then ``stop_criteria="reach_10_20_30"`` means the GA + stops if the fitness of the 3 objectives are at least 10, 20, and + 30, respectively. The number values must match the number of + objective functions. If a single value found (e.g. + ``stop_criteria=reach_5``) when solving a multi-objective problem, + then it is used across all the objectives. + https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/238 - .. code:: python +3. The ``delay_after_gen`` parameter is now deprecated and will be + removed in a future release. If it is necessary to have a time delay + after each generation, then assign a callback function/method to the + ``on_generation`` parameter to pause the evolution. - self.best_solution_generation = numpy.where(numpy.array( - self.best_solutions_fitness) == numpy.max(numpy.array(self.best_solutions_fitness)))[0][0] +4. Parallel processing now supports calculating the fitness during + adaptive mutation. + https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/201 + +5. The population size can be changed during runtime by changing all + the parameters that would affect the size of any thing used by the + GA. For more information, check the `Change Population Size during + Runtime `__ + section. + https://github.com/ahmedfgad/GeneticAlgorithmPython/discussions/234 + +6. When a dictionary exists in the ``gene_space`` parameter without a + step, then mutation occurs by adding a random value to the gene + value. The random vaue is generated based on the 2 parameters + ``random_mutation_min_val`` and ``random_mutation_max_val``. For + more information, check the `How Mutation Works with the gene_space + Parameter? `__ + section. + https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/229 + +7. Add ``object`` as a supported data type for int + (GA.supported_int_types) and float (GA.supported_float_types). + https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/174 + +8. Use the ``raise`` clause instead of the ``sys.exit(-1)`` to + terminate the execution. + https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/213 + +9. Fix a bug when multi-objective optimization is used with batch + fitness calculation (e.g. ``fitness_batch_size`` set to a non-zero + number). + +10. Fix a bug in the ``pygad.py`` script when finding the index of the + best solution. It does not work properly with multi-objective + optimization where ``self.best_solutions_fitness`` have multiple + columns. + +.. code:: python + + self.best_solution_generation = numpy.where(numpy.array( + self.best_solutions_fitness) == numpy.max(numpy.array(self.best_solutions_fitness)))[0][0] PyGAD Projects at GitHub ======================== diff --git a/examples/example_dynamic_population_size.py b/examples/example_dynamic_population_size.py index 190e61f..8a724b3 100644 --- a/examples/example_dynamic_population_size.py +++ b/examples/example_dynamic_population_size.py @@ -3,44 +3,83 @@ """ This is an example to dynamically change the population size (i.e. number of solutions/chromosomes per population) during runtime. -The following 2 instance attributes must be changed to meet the new desired population size: - 1) population: This is a NumPy array holding the population. - 2) num_offspring: This represents the number of offspring to produce during crossover. -For example, if the population initially has 20 solutions and 6 genes. To change it to have 30 solutions, then: - 1)population: Create a new NumPy array with the desired size (30, 6) and assign it to the population instance attribute. - 2)num_offspring: Set the num_offspring attribute accordingly (e.g. 29 assuming that keep_elitism has the default value of 1). + +The user has to carefully inspect the parameters and instance attributes to select those that must be changed to be consistent with the new population size. +Check this link for more information: https://pygad.readthedocs.io/en/latest/pygad_more.html#change-population-size-during-runtime """ +def update_GA(ga_i, + pop_size): + """ + Update the parameters and instance attributes to match the new population size. + + Parameters + ---------- + ga_i : TYPE + The pygad.GA instance. + pop_size : TYPE + The new population size. + + Returns + ------- + None. + """ + + ga_i.pop_size = pop_size + ga_i.sol_per_pop = ga_i.pop_size[0] + ga_i.num_parents_mating = int(ga_i.pop_size[0]/2) + + # Calculate the new value for the num_offspring parameter. + if ga_i.keep_elitism != 0: + ga_i.num_offspring = ga_i.sol_per_pop - ga_i.keep_elitism + elif ga_i.keep_parents != 0: + if ga_i.keep_parents == -1: + ga_i.num_offspring = ga_i.sol_per_pop - ga_i.num_parents_mating + else: + ga_i.num_offspring = ga_i.sol_per_pop - ga_i.keep_parents + + ga_i.num_genes = ga_i.pop_size[1] + ga_i.population = numpy.random.uniform(low=ga_i.init_range_low, + high=ga_i.init_range_low, + size=ga_i.pop_size) + fitness = [] + for solution, solution_idx in enumerate(ga_i.population): + fitness.append(fitness_func(ga_i, solution, solution_idx)) + ga_i.last_generation_fitness = numpy.array(fitness) + parents, parents_fitness = ga_i.steady_state_selection(ga_i.last_generation_fitness, + ga_i.num_parents_mating) + ga_i.last_generation_elitism = parents[:ga_i.keep_elitism] + ga_i.last_generation_elitism_indices = parents_fitness[:ga_i.keep_elitism] + + ga_i.last_generation_parents = parents + ga_i.last_generation_parents_indices = parents_fitness + def fitness_func(ga_instance, solution, solution_idx): - return [numpy.random.rand(), numpy.random.rand()] + return numpy.sum(solution) def on_generation(ga_i): # The population starts with 20 solutions. - print(ga_i.generations_completed, ga_i.num_offspring, ga_i.population.shape) - # At generation 15, increase the population size to 40 solutions. + print(ga_i.generations_completed, ga_i.population.shape) + # At generation 15, set the population size to 30 solutions and 10 genes. if ga_i.generations_completed >= 15: - ga_i.num_offspring = 49 - new_population = numpy.zeros(shape=(ga_i.num_offspring+1, ga_i.population.shape[1]), dtype=ga_i.population.dtype) - new_population[:ga_i.population.shape[0], :] = ga_i.population - ga_i.population = new_population + ga_i.pop_size = (30, 10) + update_GA(ga_i=ga_i, + pop_size=(30, 10)) + # At generation 10, set the population size to 15 solutions and 8 genes. elif ga_i.generations_completed >= 10: - ga_i.num_offspring = 39 - new_population = numpy.zeros(shape=(ga_i.num_offspring+1, ga_i.population.shape[1]), dtype=ga_i.population.dtype) - new_population[:ga_i.population.shape[0], :] = ga_i.population - ga_i.population = new_population - # At generation 10, increase the population size to 30 solutions. + update_GA(ga_i=ga_i, + pop_size=(15, 8)) + # At generation 5, set the population size to 10 solutions and 3 genes. elif ga_i.generations_completed >= 5: - ga_i.num_offspring = 29 - new_population = numpy.zeros(shape=(ga_i.num_offspring+1, ga_i.population.shape[1]), dtype=ga_i.population.dtype) - new_population[:ga_i.population.shape[0], :] = ga_i.population - ga_i.population = new_population + update_GA(ga_i=ga_i, + pop_size=(10, 3)) ga_instance = pygad.GA(num_generations=20, sol_per_pop=20, num_genes=6, num_parents_mating=10, fitness_func=fitness_func, - on_generation=on_generation, - parent_selection_type='nsga2') + on_generation=on_generation) ga_instance.run() + diff --git a/pyproject.toml b/pyproject.toml index 4f5055e..7289a9f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta" [project] name = "pygad" -version = "3.2.0" +version = "3.3.0" description = "PyGAD: A Python Library for Building the Genetic Algorithm and Training Machine Learning Algoithms (Keras & PyTorch)." readme = {file = "README.md", content-type = "text/markdown"} requires-python = ">=3" diff --git a/setup.py b/setup.py index 9017723..5b24838 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="pygad", - version="3.2.0", + version="3.3.0", author="Ahmed Fawzy Gad", install_requires=["numpy", "matplotlib", "cloudpickle",], author_email="ahmed.f.gad@gmail.com",