From 9ffa344fae699fb5859b97d3eaedd4d4170f3ef9 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Sat, 7 Sep 2024 12:55:42 -0400 Subject: [PATCH 01/31] Fix Typo --- docs/source/pygad_more.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/source/pygad_more.rst b/docs/source/pygad_more.rst index 69fa342..7e0d748 100644 --- a/docs/source/pygad_more.rst +++ b/docs/source/pygad_more.rst @@ -632,6 +632,7 @@ After running the code again, it will find the same result. 0.04872203136549972 Continue without Losing Progress +================================ In `PyGAD 2.18.0 `__, @@ -2000,7 +2001,7 @@ future. These instances attributes can save the solutions: To configure PyGAD for non-deterministic problems, we have to disable saving the previous solutions. This is by setting these parameters: -1. ``keep_elisitm=0`` +1. ``keep_elitism=0`` 2. ``keep_parents=0`` From e17b999284f81094144a1aae2733e053eefb5207 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Sat, 21 Sep 2024 10:40:10 -0400 Subject: [PATCH 02/31] No gradients before torch predictions --- pygad/torchga/__init__.py | 2 +- pygad/torchga/torchga.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pygad/torchga/__init__.py b/pygad/torchga/__init__.py index 7e51570..b7b98f5 100644 --- a/pygad/torchga/__init__.py +++ b/pygad/torchga/__init__.py @@ -1,3 +1,3 @@ from .torchga import * -__version__ = "1.3.0" +__version__ = "1.4.0" diff --git a/pygad/torchga/torchga.py b/pygad/torchga/torchga.py index cff6d2e..e552d3d 100644 --- a/pygad/torchga/torchga.py +++ b/pygad/torchga/torchga.py @@ -44,9 +44,10 @@ def predict(model, solution, data): _model = copy.deepcopy(model) _model.load_state_dict(model_weights_dict) - predictions = _model(data) + with torch.no_grad(): + predictions = _model(data) - return predictions + return predictions class TorchGA: From d51b4d8cd46b1dcf81e44bb2678fbb2fd4b457d1 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Fri, 6 Dec 2024 14:30:53 -0500 Subject: [PATCH 03/31] Clean code --- pygad/helper/unique.py | 40 +++++++++++++--------------------------- 1 file changed, 13 insertions(+), 27 deletions(-) diff --git a/pygad/helper/unique.py b/pygad/helper/unique.py index 1a0ba63..02e3874 100644 --- a/pygad/helper/unique.py +++ b/pygad/helper/unique.py @@ -171,7 +171,7 @@ def unique_int_gene_from_range(self, max_val, mutation_by_replacement, gene_type, - step=None): + step=1): """ Finds a unique integer value for the gene. @@ -182,38 +182,24 @@ def unique_int_gene_from_range(self, max_val: Maximum value of the range to sample a number randomly. mutation_by_replacement: Identical to the self.mutation_by_replacement attribute. gene_type: Exactly the same as the self.gene_type attribute. - + step: Defaults to 1. + Returns: selected_value: The new value of the gene. It may be identical to the original gene value in case there are no possible unique values for the gene. """ + # The gene_type is of the form [type, precision] if self.gene_type_single == True: - if step is None: - # all_gene_values = numpy.arange(min_val, - # max_val, - # dtype=gene_type[0]) - all_gene_values = numpy.asarray(numpy.arange(min_val, max_val), - dtype=gene_type[0]) - else: - # For non-integer steps, the numpy.arange() function returns zeros if the dtype parameter is set to an integer data type. So, this returns zeros if step is non-integer and dtype is set to an int data type: numpy.arange(min_val, max_val, step, dtype=gene_type[0]) - # To solve this issue, the data type casting will not be handled inside numpy.arange(). The range is generated by numpy.arange() and then the data type is converted using the numpy.asarray() function. - all_gene_values = numpy.asarray(numpy.arange(min_val, - max_val, - step), - dtype=gene_type[0]) + dtype = gene_type[0] else: - if step is None: - # all_gene_values = numpy.arange(min_val, - # max_val, - # dtype=gene_type[gene_index][0]) - all_gene_values = numpy.asarray(numpy.arange(min_val, - max_val), - dtype=gene_type[gene_index][0]) - else: - all_gene_values = numpy.asarray(numpy.arange(min_val, - max_val, - step), - dtype=gene_type[gene_index][0]) + dtype = gene_type[gene_index][0] + + # For non-integer steps, the numpy.arange() function returns zeros if the dtype parameter is set to an integer data type. So, this returns zeros if step is non-integer and dtype is set to an int data type: numpy.arange(min_val, max_val, step, dtype=gene_type[0]) + # To solve this issue, the data type casting will not be handled inside numpy.arange(). The range is generated by numpy.arange() and then the data type is converted using the numpy.asarray() function. + all_gene_values = numpy.asarray(numpy.arange(min_val, + max_val, + step), + dtype=dtype) if mutation_by_replacement: pass From aa49af716c62fe1dfba5f43a0714e53564229cbe Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Sat, 7 Dec 2024 12:23:34 -0500 Subject: [PATCH 04/31] Refine comment --- pygad/pygad.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygad/pygad.py b/pygad/pygad.py index dad0be8..1c0fe2b 100644 --- a/pygad/pygad.py +++ b/pygad/pygad.py @@ -83,7 +83,7 @@ def __init__(self, init_range_high: The upper value of the random range from which the gene values in the initial population are selected. It defaults to -4. Available in PyGAD 1.0.20. # It is OK to set the value of any of the 2 parameters ('init_range_low' and 'init_range_high') to be equal, higher or lower than the other parameter (i.e. init_range_low is not needed to be lower than init_range_high). - gene_type: The type of the gene. It is assigned to any of these types (int, float, numpy.int8, numpy.int16, numpy.int32, numpy.int64, numpy.uint, numpy.uint8, numpy.uint16, numpy.uint32, numpy.uint64, numpy.float16, numpy.float32, numpy.float64) and forces all the genes to be of that type. + gene_type: The type of the gene. It is assigned to any of these types (int, numpy.int8, numpy.int16, numpy.int32, numpy.int64, numpy.uint, numpy.uint8, numpy.uint16, numpy.uint32, numpy.uint64, float, numpy.float16, numpy.float32, numpy.float64) and forces all the genes to be of that type. parent_selection_type: Type of parent selection. keep_parents: If 0, this means no parent in the current population will be used in the next population. If -1, this means all parents in the current population will be used in the next population. If set to a value > 0, then the specified value refers to the number of parents in the current population to be used in the next population. Some parent selection operators such as rank selection, favor population diversity and therefore keeping the parents in the next generation can be beneficial. However, some other parent selection operators, such as roulette wheel selection (RWS), have higher selection pressure and keeping more than one parent in the next generation can seriously harm population diversity. This parameter have an effect only when the keep_elitism parameter is 0. Thanks to Prof. Fernando Jiménez Barrionuevo (http://webs.um.es/fernan) for editing this sentence. From 93337d24ad6d77175c7a3ea3071069f4b7ecfe44 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Sun, 8 Dec 2024 13:47:51 -0500 Subject: [PATCH 05/31] Edit documentation string --- pygad/helper/unique.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/pygad/helper/unique.py b/pygad/helper/unique.py index 02e3874..355e0e3 100644 --- a/pygad/helper/unique.py +++ b/pygad/helper/unique.py @@ -174,18 +174,19 @@ def unique_int_gene_from_range(self, step=1): """ - Finds a unique integer value for the gene. - - solution: A solution with duplicate values. - gene_index: Index of the gene to find a unique value. - min_val: Minimum value of the range to sample a number randomly. - max_val: Maximum value of the range to sample a number randomly. - mutation_by_replacement: Identical to the self.mutation_by_replacement attribute. - gene_type: Exactly the same as the self.gene_type attribute. - step: Defaults to 1. + Finds a unique integer value for a specific gene in a solution. + + Args: + solution (list): A solution containing genes, potentially with duplicate values. + gene_index (int): The index of the gene for which to find a unique value. + min_val (int): The minimum value of the range to sample a number randomly. + max_val (int): The maximum value of the range to sample a number randomly. + mutation_by_replacement (bool): Indicates if mutation is performed by replacement. + gene_type (type): The data type of the gene (e.g., int, float). + step (int, optional): The step size for generating candidate values. Defaults to 1. Returns: - selected_value: The new value of the gene. It may be identical to the original gene value in case there are no possible unique values for the gene. + int: The new value of the gene. If no unique value can be found, the original gene value is returned. """ # The gene_type is of the form [type, precision] From 7f292ca33699f4990fcacad95c5dca786293a04f Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Sun, 8 Dec 2024 13:54:25 -0500 Subject: [PATCH 06/31] Edit documentation string --- pygad/helper/unique.py | 124 +++++++++++++++++++++++------------------ 1 file changed, 71 insertions(+), 53 deletions(-) diff --git a/pygad/helper/unique.py b/pygad/helper/unique.py index 355e0e3..72e0773 100644 --- a/pygad/helper/unique.py +++ b/pygad/helper/unique.py @@ -16,23 +16,24 @@ def solve_duplicate_genes_randomly(self, mutation_by_replacement, gene_type, num_trials=10): - """ - Solves the duplicates in a solution by randomly selecting new values for the duplicating genes. + Resolves duplicates in a solution by randomly selecting new values for the duplicate genes. - solution: A solution with duplicate values. - min_val: Minimum value of the range to sample a number randomly. - max_val: Maximum value of the range to sample a number randomly. - mutation_by_replacement: Identical to the self.mutation_by_replacement attribute. - gene_type: Exactly the same as the self.gene_type attribute. - num_trials: Maximum number of trials to change the gene value to solve the duplicates. + Args: + solution (list): A solution containing genes, potentially with duplicate values. + min_val (int): The minimum value of the range to sample a number randomly. + max_val (int): The maximum value of the range to sample a number randomly. + mutation_by_replacement (bool): Indicates if mutation is performed by replacement. + gene_type (type): The data type of the gene (e.g., int, float). + num_trials (int): The maximum number of attempts to resolve duplicates by changing the gene values. Returns: - new_solution: Solution after trying to solve its duplicates. If no duplicates solved, then it is identical to the passed solution parameter. - not_unique_indices: Indices of the genes with duplicate values. - num_unsolved_duplicates: Number of unsolved duplicates. + tuple: + list: The updated solution after attempting to resolve duplicates. If no duplicates are resolved, the solution remains unchanged. + list: The indices of genes that still have duplicate values. + int: The number of duplicates that could not be resolved. """ - + new_solution = solution.copy() _, unique_gene_indices = numpy.unique(solution, return_index=True) @@ -113,17 +114,20 @@ def solve_duplicate_genes_by_space(self, build_initial_pop=False): """ - Solves the duplicates in a solution by selecting values for the duplicating genes from the gene space. - - solution: A solution with duplicate values. - gene_type: Exactly the same as the self.gene_type attribute. - num_trials: Maximum number of trials to change the gene value to solve the duplicates. - + Resolves duplicates in a solution by selecting new values for the duplicate genes from the gene space. + + Args: + solution (list): A solution containing genes, potentially with duplicate values. + gene_type (type): The data type of the gene (e.g., int, float). + num_trials (int): The maximum number of attempts to resolve duplicates by selecting values from the gene space. + Returns: - new_solution: Solution after trying to solve its duplicates. If no duplicates solved, then it is identical to the passed solution parameter. - not_unique_indices: Indices of the genes with duplicate values. - num_unsolved_duplicates: Number of unsolved duplicates. + tuple: + list: The updated solution after attempting to resolve duplicates. If no duplicates are resolved, the solution remains unchanged. + list: The indices of genes that still have duplicate values. + int: The number of duplicates that could not be resolved. """ + new_solution = solution.copy() _, unique_gene_indices = numpy.unique(solution, return_index=True) @@ -236,18 +240,20 @@ def unique_genes_by_space(self, build_initial_pop=False): """ - Loops through all the duplicating genes to find unique values that from their gene spaces to solve the duplicates. - For each duplicating gene, a call to the unique_gene_by_space() function is made. - - new_solution: A solution with duplicate values. - gene_type: Exactly the same as the self.gene_type attribute. - not_unique_indices: Indices with duplicating values. - num_trials: Maximum number of trials to change the gene value to solve the duplicates. - + Iterates through all duplicate genes to find unique values from their gene spaces and resolve duplicates. + For each duplicate gene, a call is made to the `unique_gene_by_space()` function. + + Args: + new_solution (list): A solution containing genes with duplicate values. + gene_type (type): The data type of the gene (e.g., int, float). + not_unique_indices (list): The indices of genes with duplicate values. + num_trials (int): The maximum number of attempts to resolve duplicates for each gene. + Returns: - new_solution: Solution after trying to solve all of its duplicates. If no duplicates solved, then it is identical to the passed solution parameter. - not_unique_indices: Indices of the genes with duplicate values. - num_unsolved_duplicates: Number of unsolved duplicates. + tuple: + list: The updated solution after attempting to resolve all duplicates. If no duplicates are resolved, the solution remains unchanged. + list: The indices of genes that still have duplicate values. + int: The number of duplicates that could not be resolved. """ num_unsolved_duplicates = 0 @@ -283,15 +289,15 @@ def unique_gene_by_space(self, build_initial_pop=False): """ - Returns a unique gene value for a single gene based on its value space to solve the duplicates. - - solution: A solution with duplicate values. - gene_idx: The index of the gene that duplicates its value with another gene. - gene_type: Exactly the same as the self.gene_type attribute. - + Returns a unique value for a specific gene based on its value space to resolve duplicates. + + Args: + solution (list): A solution containing genes with duplicate values. + gene_idx (int): The index of the gene that has a duplicate value. + gene_type (type): The data type of the gene (e.g., int, float). + Returns: - A unique value, if exists, for the gene. - """ + Any: A unique value for the gene, if one exists; otherwise, the original gene value. """ if self.gene_space_nested: if type(self.gene_space[gene_idx]) in [numpy.ndarray, list, tuple]: @@ -572,11 +578,14 @@ def find_two_duplicates(self, solution, gene_space_unpacked): """ - Returns the first occurrence of duplicate genes. - It returns: - The index of a gene with a duplicate value. - The value of the gene. + Identifies the first occurrence of a duplicate gene in the solution. + + Returns: + tuple: + int: The index of the first gene with a duplicate value. + Any: The value of the duplicate gene. """ + for gene in set(solution): gene_indices = numpy.where(numpy.array(solution) == gene)[0] if len(gene_indices) == 1: @@ -594,13 +603,15 @@ def unpack_gene_space(self, range_max, num_values_from_inf_range=100): """ - Unpack the gene_space for the purpose of selecting a value that solves the duplicates. - This is by replacing each range by a list of values. - It accepts: - range_min: The range minimum value. - range_min: The range maximum value. - num_values_from_inf_range: For infinite range of float values, a fixed number of values equal to num_values_from_inf_range is selected using the numpy.linspace() function. - It returns the unpacked gene space. + Unpacks the gene space for selecting a value to resolve duplicates by converting ranges into lists of values. + + Args: + range_min (float or int): The minimum value of the range. + range_max (float or int): The maximum value of the range. + num_values_from_inf_range (int): The number of values to generate for an infinite range of float values using `numpy.linspace()`. + + Returns: + list: A list representing the unpacked gene space. """ # Copy the gene_space to keep it isolated form the changes. @@ -740,8 +751,15 @@ def solve_duplicates_deeply(self, """ Sometimes it is impossible to solve the duplicate genes by simply selecting another value for either genes. This function solve the duplicates between 2 genes by searching for a third gene that can make assist in the solution. - It returns: - The solution after solving the duplicates or the None if duplicates cannot be solved. + + Args: + solution (list): The current solution containing genes, potentially with duplicates. + gene_idx1 (int): The index of the first gene involved in the duplication. + gene_idx2 (int): The index of the second gene involved in the duplication. + assist_gene_idx (int): The index of the third gene used to assist in resolving the duplication. + + Returns: + list or None: The updated solution with duplicates resolved, or `None` if the duplicates cannot be resolved. """ # gene_space_unpacked = self.unpack_gene_space() From a13953b347c1058e80a154bc1a2daf4e3ccd82a8 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Sun, 8 Dec 2024 17:08:25 -0500 Subject: [PATCH 07/31] Clean the code --- pygad/helper/unique.py | 440 ++++++++++++++--------------------------- 1 file changed, 150 insertions(+), 290 deletions(-) diff --git a/pygad/helper/unique.py b/pygad/helper/unique.py index 72e0773..4187b5f 100644 --- a/pygad/helper/unique.py +++ b/pygad/helper/unique.py @@ -38,68 +38,52 @@ def solve_duplicate_genes_randomly(self, _, unique_gene_indices = numpy.unique(solution, return_index=True) not_unique_indices = set(range(len(solution))) - set(unique_gene_indices) - + num_unsolved_duplicates = 0 if len(not_unique_indices) > 0: for duplicate_index in not_unique_indices: for trial_index in range(num_trials): if self.gene_type_single == True: - if gene_type[0] in pygad.GA.supported_int_types: - temp_val = self.unique_int_gene_from_range(solution=new_solution, - gene_index=duplicate_index, - min_val=min_val, - max_val=max_val, - mutation_by_replacement=mutation_by_replacement, - gene_type=gene_type) - else: - temp_val = numpy.random.uniform(low=min_val, - high=max_val, - size=1)[0] - if mutation_by_replacement: - pass - else: - temp_val = new_solution[duplicate_index] + temp_val + dtype = gene_type else: - if gene_type[duplicate_index][0] in pygad.GA.supported_int_types: - temp_val = self.unique_int_gene_from_range(solution=new_solution, - gene_index=duplicate_index, - min_val=min_val, - max_val=max_val, - mutation_by_replacement=mutation_by_replacement, - gene_type=gene_type) + dtype = gene_type[duplicate_index] + + if dtype[0] in pygad.GA.supported_int_types: + temp_val = self.unique_int_gene_from_range(solution=new_solution, + gene_index=duplicate_index, + min_val=min_val, + max_val=max_val, + mutation_by_replacement=mutation_by_replacement, + gene_type=gene_type) + else: + temp_val = numpy.random.uniform(low=min_val, + high=max_val, + size=1)[0] + if mutation_by_replacement: + pass else: - temp_val = numpy.random.uniform(low=min_val, - high=max_val, - size=1)[0] - if mutation_by_replacement: - pass - else: - temp_val = new_solution[duplicate_index] + temp_val + temp_val = new_solution[duplicate_index] + temp_val # Similar to the round_genes() method in the pygad module, # Create a round_gene() method to round a single gene. - if self.gene_type_single == True: - if not gene_type[1] is None: - temp_val = numpy.round(gene_type[0](temp_val), - gene_type[1]) - else: - temp_val = gene_type[0](temp_val) + if not dtype[1] is None: + temp_val = numpy.round(dtype[0](temp_val), + dtype[1]) else: - if not gene_type[duplicate_index][1] is None: - temp_val = numpy.round(gene_type[duplicate_index][0](temp_val), - gene_type[duplicate_index][1]) - else: - temp_val = gene_type[duplicate_index][0](temp_val) + temp_val = dtype[0](temp_val) if temp_val in new_solution and trial_index == (num_trials - 1): num_unsolved_duplicates = num_unsolved_duplicates + 1 if not self.suppress_warnings: warnings.warn(f"Failed to find a unique value for gene with index {duplicate_index} whose value is {solution[duplicate_index]}. Consider adding more values in the gene space or use a wider range for initial population or random mutation.") elif temp_val in new_solution: + # Keep trying in the other remaining trials. continue else: + # Unique gene value found. new_solution[duplicate_index] = temp_val break - + + # TODO Move this code outside the loops. # Update the list of duplicate indices after each iteration. _, unique_gene_indices = numpy.unique(new_solution, return_index=True) not_unique_indices = set(range(len(solution))) - set(unique_gene_indices) @@ -167,7 +151,7 @@ def solve_duplicate_genes_by_space(self, return new_solution, not_unique_indices, len(not_unique_indices) return new_solution, not_unique_indices, num_unsolved_duplicates - + def unique_int_gene_from_range(self, solution, gene_index, @@ -194,10 +178,7 @@ def unique_int_gene_from_range(self, """ # The gene_type is of the form [type, precision] - if self.gene_type_single == True: - dtype = gene_type[0] - else: - dtype = gene_type[gene_index][0] + dtype = gene_type[0] # For non-integer steps, the numpy.arange() function returns zeros if the dtype parameter is set to an integer data type. So, this returns zeros if step is non-integer and dtype is set to an int data type: numpy.arange(min_val, max_val, step, dtype=gene_type[0]) # To solve this issue, the data type casting will not be handled inside numpy.arange(). The range is generated by numpy.arange() and then the data type is converted using the numpy.asarray() function. @@ -205,26 +186,23 @@ def unique_int_gene_from_range(self, max_val, step), dtype=dtype) - + + # If mutation is by replacement, do not add the current gene value into the list. + # This is to avoid replacing the value by itself again. We are doing nothing in this case. if mutation_by_replacement: pass else: all_gene_values = all_gene_values + solution[gene_index] + # After adding solution[gene_index] to the list, we have to change the data type again. # TODO: The gene data type is converted twine. One above and one here. - if self.gene_type_single == True: - # Note that we already know that the data type is integer. - all_gene_values = numpy.asarray(all_gene_values, - dtype=gene_type[0]) - else: - # Note that we already know that the data type is integer. - all_gene_values = numpy.asarray(all_gene_values, - gene_type[gene_index][0]) + all_gene_values = numpy.asarray(all_gene_values, + dtype) values_to_select_from = list(set(list(all_gene_values)) - set(solution)) if len(values_to_select_from) == 0: - # If there is no values, then keep the current gene value. + # If there are no values, then keep the current gene value. if not self.suppress_warnings: warnings.warn("You set 'allow_duplicate_genes=False' but there is no enough values to prevent duplicates.") selected_value = solution[gene_index] else: @@ -314,131 +292,68 @@ def unique_gene_by_space(self, # If the gene space is None, apply mutation by adding a random value between the range defined by the 2 parameters 'random_mutation_min_val' and 'random_mutation_max_val'. elif curr_gene_space is None: if self.gene_type_single == True: - if gene_type[0] in pygad.GA.supported_int_types: - if build_initial_pop == True: - # If we are building the initial population, then use the range of the initial population. - min_val = self.init_range_low - max_val = self.init_range_high - else: - # If we are NOT building the initial population, then use the range of the random mutation. - min_val = self.random_mutation_min_val - max_val = self.random_mutation_max_val - - value_from_space = self.unique_int_gene_from_range(solution=solution, - gene_index=gene_idx, - min_val=min_val, - max_val=max_val, - mutation_by_replacement=True, - gene_type=gene_type) + dtype = gene_type + else: + dtype = gene_type[gene_idx] + + if dtype[0] in pygad.GA.supported_int_types: + if build_initial_pop == True: + # If we are building the initial population, then use the range of the initial population. + min_val = self.init_range_low + max_val = self.init_range_high else: - if build_initial_pop == True: - low = self.init_range_low - high = self.init_range_high - else: - low = self.random_mutation_min_val - high = self.random_mutation_max_val + # If we are NOT building the initial population, then use the range of the random mutation. + min_val = self.random_mutation_min_val + max_val = self.random_mutation_max_val - value_from_space = numpy.random.uniform(low=low, - high=high, - size=1)[0] - # TODO: Remove check for mutation_by_replacement when solving duplicates. Just replace the gene by the selected value from space. - # if self.mutation_by_replacement: - # pass - # else: - # value_from_space = solution[gene_idx] + value_from_space + value_from_space = self.unique_int_gene_from_range(solution=solution, + gene_index=gene_idx, + min_val=min_val, + max_val=max_val, + mutation_by_replacement=True, + gene_type=dtype) else: - if gene_type[gene_idx][0] in pygad.GA.supported_int_types: - if build_initial_pop == True: - min_val = self.init_range_low - max_val = self.init_range_high - else: - min_val = self.random_mutation_min_val - max_val = self.random_mutation_max_val - - value_from_space = self.unique_int_gene_from_range(solution=solution, - gene_index=gene_idx, - min_val=min_val, - max_val=max_val, - mutation_by_replacement=True, - gene_type=gene_type) + if build_initial_pop == True: + low = self.init_range_low + high = self.init_range_high else: - if build_initial_pop == True: - low = self.init_range_low - high = self.init_range_high - else: - low = self.random_mutation_min_val - high = self.random_mutation_max_val + low = self.random_mutation_min_val + high = self.random_mutation_max_val + + value_from_space = numpy.random.uniform(low=low, + high=high, + size=1)[0] - value_from_space = numpy.random.uniform(low=low, - high=high, - size=1)[0] - # TODO: Remove check for mutation_by_replacement when solving duplicates. Just replace the gene by the selected value from space. - # if self.mutation_by_replacement: - # pass - # else: - # value_from_space = solution[gene_idx] + value_from_space - elif type(curr_gene_space) is dict: if self.gene_type_single == True: - if gene_type[0] in pygad.GA.supported_int_types: - if 'step' in curr_gene_space.keys(): - step = curr_gene_space['step'] - else: - step = None - - value_from_space = self.unique_int_gene_from_range(solution=solution, - gene_index=gene_idx, - min_val=curr_gene_space['low'], - max_val=curr_gene_space['high'], - step=step, - mutation_by_replacement=True, - gene_type=gene_type) + dtype = gene_type + else: + dtype = gene_type[gene_idx] + + # Use index 0 to return the type from the list (e.g. [int, None] or [float, 2]). + if dtype[0] in pygad.GA.supported_int_types: + if 'step' in curr_gene_space.keys(): + step = curr_gene_space['step'] else: - if 'step' in curr_gene_space.keys(): - 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) - else: - value_from_space = numpy.random.uniform(low=curr_gene_space['low'], - high=curr_gene_space['high'], - size=1)[0] - # TODO: Remove check for mutation_by_replacement when solving duplicates. Just replace the gene by the selected value from space. - # if self.mutation_by_replacement: - # pass - # else: - # value_from_space = solution[gene_idx] + value_from_space + step = None + + value_from_space = self.unique_int_gene_from_range(solution=solution, + gene_index=gene_idx, + min_val=curr_gene_space['low'], + max_val=curr_gene_space['high'], + step=step, + mutation_by_replacement=True, + gene_type=dtype) else: - # Use index 0 to return the type from the list (e.g. [int, None] or [float, 2]). - if gene_type[gene_idx][0] in pygad.GA.supported_int_types: - if 'step' in curr_gene_space.keys(): - step = curr_gene_space['step'] - else: - step = None - - value_from_space = self.unique_int_gene_from_range(solution=solution, - gene_index=gene_idx, - min_val=curr_gene_space['low'], - max_val=curr_gene_space['high'], - step=step, - mutation_by_replacement=True, - gene_type=gene_type) + if 'step' in curr_gene_space.keys(): + 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) else: - if 'step' in curr_gene_space.keys(): - 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) - else: - value_from_space = numpy.random.uniform(low=curr_gene_space['low'], - high=curr_gene_space['high'], - size=1)[0] - # TODO: Remove check for mutation_by_replacement when solving duplicates. Just replace the gene by the selected value from space. - # if self.mutation_by_replacement: - # pass - # else: - # value_from_space = solution[gene_idx] + value_from_space - + value_from_space = numpy.random.uniform(low=curr_gene_space['low'], + high=curr_gene_space['high'], + size=1)[0] 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. @@ -473,66 +388,34 @@ def unique_gene_by_space(self, # Selecting a value randomly from the global gene space in the 'gene_space' attribute. if type(self.gene_space) is dict: if self.gene_type_single == True: - if gene_type[0] in pygad.GA.supported_int_types: - if 'step' in self.gene_space.keys(): - step = self.gene_space['step'] - else: - step = None - - value_from_space = self.unique_int_gene_from_range(solution=solution, - gene_index=gene_idx, - min_val=self.gene_space['low'], - max_val=self.gene_space['high'], - step=step, - mutation_by_replacement=True, - gene_type=gene_type) + dtype = gene_type + else: + dtype = gene_type[gene_idx] + + if dtype[0] in pygad.GA.supported_int_types: + if 'step' in self.gene_space.keys(): + step = self.gene_space['step'] else: - # When the gene_space is assigned a dict object, then it specifies the lower and upper limits of all genes in the space. - if 'step' in self.gene_space.keys(): - value_from_space = numpy.random.choice(numpy.arange(start=self.gene_space['low'], - stop=self.gene_space['high'], - step=self.gene_space['step']), - size=1) - else: - value_from_space = numpy.random.uniform(low=self.gene_space['low'], - high=self.gene_space['high'], - size=1)[0] - # TODO: Remove check for mutation_by_replacement when solving duplicates. Just replace the gene by the selected value from space. - # if self.mutation_by_replacement: - # pass - # else: - # value_from_space = solution[gene_idx] + value_from_space + step = None + + value_from_space = self.unique_int_gene_from_range(solution=solution, + gene_index=gene_idx, + min_val=self.gene_space['low'], + max_val=self.gene_space['high'], + step=step, + mutation_by_replacement=True, + gene_type=dtype) else: - if gene_type[gene_idx][0] in pygad.GA.supported_int_types: - if 'step' in self.gene_space.keys(): - step = self.gene_space['step'] - else: - step = None - - value_from_space = self.unique_int_gene_from_range(solution=solution, - gene_index=gene_idx, - min_val=self.gene_space['low'], - max_val=self.gene_space['high'], - step=step, - mutation_by_replacement=True, - gene_type=gene_type) + # When the gene_space is assigned a dict object, then it specifies the lower and upper limits of all genes in the space. + if 'step' in self.gene_space.keys(): + value_from_space = numpy.random.choice(numpy.arange(start=self.gene_space['low'], + stop=self.gene_space['high'], + step=self.gene_space['step']), + size=1) else: - # When the gene_space is assigned a dict object, then it specifies the lower and upper limits of all genes in the space. - if 'step' in self.gene_space.keys(): - value_from_space = numpy.random.choice(numpy.arange(start=self.gene_space['low'], - stop=self.gene_space['high'], - step=self.gene_space['step']), - size=1) - else: - value_from_space = numpy.random.uniform(low=self.gene_space['low'], - high=self.gene_space['high'], - size=1)[0] - # TODO: Remove check for mutation_by_replacement when solving duplicates. Just replace the gene by the selected value from space. - # if self.mutation_by_replacement: - # pass - # else: - # value_from_space = solution[gene_idx] + value_from_space - + value_from_space = numpy.random.uniform(low=self.gene_space['low'], + high=self.gene_space['high'], + size=1)[0] else: # If the space type is not of type dict, then a value is randomly selected from the gene_space attribute. # Remove all the genes in the current solution from the gene_space. @@ -560,17 +443,15 @@ def unique_gene_by_space(self, # Similar to the round_genes() method in the pygad module, # Create a round_gene() method to round a single gene. if self.gene_type_single == True: - if not gene_type[1] is None: - value_from_space = numpy.round(gene_type[0](value_from_space), - gene_type[1]) - else: - value_from_space = gene_type[0](value_from_space) + dtype = gene_type else: - if not gene_type[gene_idx][1] is None: - value_from_space = numpy.round(gene_type[gene_idx][0](value_from_space), - gene_type[gene_idx][1]) - else: - value_from_space = gene_type[gene_idx][0](value_from_space) + dtype = gene_type[gene_idx] + + if not dtype[1] is None: + value_from_space = numpy.round(dtype[0](value_from_space), + dtype[1]) + else: + value_from_space = dtype[0](value_from_space) return value_from_space @@ -672,47 +553,30 @@ def unpack_gene_space(self, elif type(space) is dict: # Create a list of values using the dict range. # Use numpy.linspace() - if self.gene_type_single == True: # self.gene_type_single - if self.gene_type[0] in pygad.GA.supported_int_types: - if 'step' in space.keys(): - step = space['step'] - else: - step = 1 - - gene_space_unpacked[space_idx] = numpy.arange(start=space['low'], - stop=space['high'], - step=step) + if self.gene_type_single == True: + dtype = self.gene_type + else: + dtype = self.gene_type[gene_idx] + + if dtype[0] in pygad.GA.supported_int_types: + if 'step' in space.keys(): + step = space['step'] else: - if 'step' in space.keys(): - gene_space_unpacked[space_idx] = numpy.arange(start=space['low'], - stop=space['high'], - step=space['step']) - else: - gene_space_unpacked[space_idx] = numpy.linspace(start=space['low'], - stop=space['high'], - num=num_values_from_inf_range, - endpoint=False) + step = 1 + + gene_space_unpacked[space_idx] = numpy.arange(start=space['low'], + stop=space['high'], + step=step) else: - if self.gene_type[space_idx][0] in pygad.GA.supported_int_types: - if 'step' in space.keys(): - step = space['step'] - else: - step = 1 - + if 'step' in space.keys(): gene_space_unpacked[space_idx] = numpy.arange(start=space['low'], stop=space['high'], - step=step) + step=space['step']) else: - if 'step' in space.keys(): - gene_space_unpacked[space_idx] = numpy.arange(start=space['low'], - stop=space['high'], - step=space['step']) - else: - gene_space_unpacked[space_idx] = numpy.linspace(start=space['low'], - stop=space['high'], - num=num_values_from_inf_range, - endpoint=False) - + gene_space_unpacked[space_idx] = numpy.linspace(start=space['low'], + stop=space['high'], + num=num_values_from_inf_range, + endpoint=False) elif type(space) in [numpy.ndarray, list, tuple]: # list/tuple/numpy.ndarray # Convert all to list @@ -727,22 +591,18 @@ def unpack_gene_space(self, size=1)[0] gene_space_unpacked[space_idx][idx] = random_value - if self.gene_type_single == True: # self.gene_type_single - # Change the data type. - gene_space_unpacked[space_idx] = numpy.array(gene_space_unpacked[space_idx], - dtype=self.gene_type[0]) - if not self.gene_type[1] is None: - # Round the values for float (non-int) data types. - gene_space_unpacked[space_idx] = numpy.round(gene_space_unpacked[space_idx], - self.gene_type[1]) + if self.gene_type_single == True: + dtype = self.gene_type else: - # Change the data type. - gene_space_unpacked[space_idx] = numpy.array(gene_space_unpacked[space_idx], - self.gene_type[space_idx][0]) - if not self.gene_type[space_idx][1] is None: - # Round the values for float (non-int) data types. - gene_space_unpacked[space_idx] = numpy.round(gene_space_unpacked[space_idx], - self.gene_type[space_idx][1]) + dtype = self.gene_type[gene_idx] + + # Change the data type. + gene_space_unpacked[space_idx] = numpy.array(gene_space_unpacked[space_idx], + dtype=dtype[0]) + if not dtype[1] is None: + # Round the values for float (non-int) data types. + gene_space_unpacked[space_idx] = numpy.round(gene_space_unpacked[space_idx], + dtype[1]) return gene_space_unpacked From f492bb3787a3b776205f51d325476d416f5c1218 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Sun, 8 Dec 2024 17:18:21 -0500 Subject: [PATCH 08/31] Replace gene_idx by space_idx --- pygad/helper/unique.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pygad/helper/unique.py b/pygad/helper/unique.py index 4187b5f..daf643c 100644 --- a/pygad/helper/unique.py +++ b/pygad/helper/unique.py @@ -556,7 +556,7 @@ def unpack_gene_space(self, if self.gene_type_single == True: dtype = self.gene_type else: - dtype = self.gene_type[gene_idx] + dtype = self.gene_type[space_idx] if dtype[0] in pygad.GA.supported_int_types: if 'step' in space.keys(): @@ -594,7 +594,7 @@ def unpack_gene_space(self, if self.gene_type_single == True: dtype = self.gene_type else: - dtype = self.gene_type[gene_idx] + dtype = self.gene_type[space_idx] # Change the data type. gene_space_unpacked[space_idx] = numpy.array(gene_space_unpacked[space_idx], From c6949e1c94d4571368f9bea6d8236042d403d9ea Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Sun, 8 Dec 2024 18:14:52 -0500 Subject: [PATCH 09/31] Refactor code to remove duplicate sections --- pygad/helper/unique.py | 212 +++++++++++++++++++++++++++-------------- 1 file changed, 140 insertions(+), 72 deletions(-) diff --git a/pygad/helper/unique.py b/pygad/helper/unique.py index daf643c..8eb0505 100644 --- a/pygad/helper/unique.py +++ b/pygad/helper/unique.py @@ -25,7 +25,7 @@ def solve_duplicate_genes_randomly(self, max_val (int): The maximum value of the range to sample a number randomly. mutation_by_replacement (bool): Indicates if mutation is performed by replacement. gene_type (type): The data type of the gene (e.g., int, float). - num_trials (int): The maximum number of attempts to resolve duplicates by changing the gene values. + num_trials (int): The maximum number of attempts to resolve duplicates by changing the gene values. Only works for floating-point gene types. Returns: tuple: @@ -42,53 +42,48 @@ def solve_duplicate_genes_randomly(self, num_unsolved_duplicates = 0 if len(not_unique_indices) > 0: for duplicate_index in not_unique_indices: - for trial_index in range(num_trials): - if self.gene_type_single == True: - dtype = gene_type - else: - dtype = gene_type[duplicate_index] - - if dtype[0] in pygad.GA.supported_int_types: - temp_val = self.unique_int_gene_from_range(solution=new_solution, - gene_index=duplicate_index, - min_val=min_val, - max_val=max_val, - mutation_by_replacement=mutation_by_replacement, - gene_type=gene_type) - else: - temp_val = numpy.random.uniform(low=min_val, - high=max_val, - size=1)[0] - if mutation_by_replacement: + if self.gene_type_single == True: + dtype = gene_type + else: + dtype = gene_type[duplicate_index] + + if dtype[0] in pygad.GA.supported_int_types: + temp_val = self.unique_int_gene_from_range(solution=new_solution, + gene_index=duplicate_index, + min_val=min_val, + max_val=max_val, + mutation_by_replacement=mutation_by_replacement, + gene_type=gene_type) + else: + temp_val = self.unique_float_gene_from_range(solution=new_solution, + gene_index=duplicate_index, + min_val=min_val, + max_val=max_val, + mutation_by_replacement=mutation_by_replacement, + gene_type=gene_type, + num_trials=num_trials) + """ + temp_val = numpy.random.uniform(low=min_val, + high=max_val, + size=1)[0] + if mutation_by_replacement: pass - else: + else: temp_val = new_solution[duplicate_index] + temp_val + """ + + if temp_val in new_solution: + num_unsolved_duplicates = num_unsolved_duplicates + 1 + if not self.suppress_warnings: warnings.warn(f"Failed to find a unique value for gene with index {duplicate_index} whose value is {solution[duplicate_index]}. Consider adding more values in the gene space or use a wider range for initial population or random mutation.") + else: + # Unique gene value found. + new_solution[duplicate_index] = temp_val + + # Update the list of duplicate indices after each iteration. + _, unique_gene_indices = numpy.unique(new_solution, return_index=True) + not_unique_indices = set(range(len(solution))) - set(unique_gene_indices) + # self.logger.info("not_unique_indices INSIDE", not_unique_indices) - # Similar to the round_genes() method in the pygad module, - # Create a round_gene() method to round a single gene. - if not dtype[1] is None: - temp_val = numpy.round(dtype[0](temp_val), - dtype[1]) - else: - temp_val = dtype[0](temp_val) - - if temp_val in new_solution and trial_index == (num_trials - 1): - num_unsolved_duplicates = num_unsolved_duplicates + 1 - if not self.suppress_warnings: warnings.warn(f"Failed to find a unique value for gene with index {duplicate_index} whose value is {solution[duplicate_index]}. Consider adding more values in the gene space or use a wider range for initial population or random mutation.") - elif temp_val in new_solution: - # Keep trying in the other remaining trials. - continue - else: - # Unique gene value found. - new_solution[duplicate_index] = temp_val - break - - # TODO Move this code outside the loops. - # Update the list of duplicate indices after each iteration. - _, unique_gene_indices = numpy.unique(new_solution, return_index=True) - not_unique_indices = set(range(len(solution))) - set(unique_gene_indices) - # self.logger.info("not_unique_indices INSIDE", not_unique_indices) - return new_solution, not_unique_indices, num_unsolved_duplicates def solve_duplicate_genes_by_space(self, @@ -167,14 +162,14 @@ def unique_int_gene_from_range(self, Args: solution (list): A solution containing genes, potentially with duplicate values. gene_index (int): The index of the gene for which to find a unique value. - min_val (int): The minimum value of the range to sample a number randomly. - max_val (int): The maximum value of the range to sample a number randomly. + min_val (int): The minimum value of the range to sample an integer randomly. + max_val (int): The maximum value of the range to sample an integer randomly. mutation_by_replacement (bool): Indicates if mutation is performed by replacement. - gene_type (type): The data type of the gene (e.g., int, float). + gene_type (type): The data type of the gene (e.g., int, int8, uint16, etc). step (int, optional): The step size for generating candidate values. Defaults to 1. Returns: - int: The new value of the gene. If no unique value can be found, the original gene value is returned. + int: The new integer value of the gene. If no unique value can be found, the original gene value is returned. """ # The gene_type is of the form [type, precision] @@ -194,22 +189,86 @@ def unique_int_gene_from_range(self, else: all_gene_values = all_gene_values + solution[gene_index] - # After adding solution[gene_index] to the list, we have to change the data type again. - # TODO: The gene data type is converted twine. One above and one here. - all_gene_values = numpy.asarray(all_gene_values, - dtype) + # After adding solution[gene_index] to the list, we have to change the data type again. + all_gene_values = numpy.asarray(all_gene_values, + dtype) values_to_select_from = list(set(list(all_gene_values)) - set(solution)) if len(values_to_select_from) == 0: # If there are no values, then keep the current gene value. - if not self.suppress_warnings: warnings.warn("You set 'allow_duplicate_genes=False' but there is no enough values to prevent duplicates.") selected_value = solution[gene_index] else: selected_value = random.choice(values_to_select_from) + + selected_value = dtype[0](selected_value) return selected_value + def unique_float_gene_from_range(self, + solution, + gene_index, + min_val, + max_val, + mutation_by_replacement, + gene_type, + num_trials=10): + + """ + Finds a unique floating-point value for a specific gene in a solution. + + Args: + solution (list): A solution containing genes, potentially with duplicate values. + gene_index (int): The index of the gene for which to find a unique value. + min_val (int): The minimum value of the range to sample a floating-point number randomly. + max_val (int): The maximum value of the range to sample a floating-point number randomly. + mutation_by_replacement (bool): Indicates if mutation is performed by replacement. + gene_type (type): The data type of the gene (e.g., float, float16, float32, etc). + num_trials (int): The maximum number of attempts to resolve duplicates by changing the gene values. + + Returns: + int: The new floating-point value of the gene. If no unique value can be found, the original gene value is returned. + """ + + # The gene_type is of the form [type, precision] + dtype = gene_type + + for trial_index in range(num_trials): + temp_val = numpy.random.uniform(low=min_val, + high=max_val, + size=1)[0] + + # If mutation is by replacement, do not add the current gene value into the list. + # This is to avoid replacing the value by itself again. We are doing nothing in this case. + if mutation_by_replacement: + pass + else: + temp_val = temp_val + solution[gene_index] + + if not dtype[1] is None: + # Precision is available and we have to round the number. + # Convert the data type and round the number. + temp_val = numpy.round(dtype[0](temp_val), + dtype[1]) + else: + # There is no precision and rounding the number is not needed. The type is [type, None] + # Just convert the data type. + temp_val = dtype[0](temp_val) + + if temp_val in solution and trial_index == (num_trials - 1): + # If there are no values, then keep the current gene value. + if not self.suppress_warnings: warnings.warn("You set 'allow_duplicate_genes=False' but cannot find a value to prevent duplicates.") + selected_value = solution[gene_index] + elif temp_val in solution: + # Keep trying in the other remaining trials. + continue + else: + # Unique gene value found. + selected_value = temp_val + break + + return selected_value + def unique_genes_by_space(self, new_solution, gene_type, @@ -225,7 +284,7 @@ def unique_genes_by_space(self, new_solution (list): A solution containing genes with duplicate values. gene_type (type): The data type of the gene (e.g., int, float). not_unique_indices (list): The indices of genes with duplicate values. - num_trials (int): The maximum number of attempts to resolve duplicates for each gene. + num_trials (int): The maximum number of attempts to resolve duplicates for each gene. Only works for floating-point numbers. Returns: tuple: @@ -236,22 +295,18 @@ def unique_genes_by_space(self, num_unsolved_duplicates = 0 for duplicate_index in not_unique_indices: - for trial_index in range(num_trials): - temp_val = self.unique_gene_by_space(solution=new_solution, - gene_idx=duplicate_index, - gene_type=gene_type, - build_initial_pop=build_initial_pop) - - if temp_val in new_solution and trial_index == (num_trials - 1): - # self.logger.info("temp_val, duplicate_index", temp_val, duplicate_index, new_solution) - num_unsolved_duplicates = num_unsolved_duplicates + 1 - if not self.suppress_warnings: warnings.warn(f"Failed to find a unique value for gene with index {duplicate_index} whose value is {new_solution[duplicate_index]}. Consider adding more values in the gene space or use a wider range for initial population or random mutation.") - elif temp_val in new_solution: - continue - else: - new_solution[duplicate_index] = temp_val - # self.logger.info("SOLVED", duplicate_index) - break + temp_val = self.unique_gene_by_space(solution=new_solution, + gene_idx=duplicate_index, + gene_type=gene_type, + build_initial_pop=build_initial_pop, + num_trials=num_trials) + + if temp_val in new_solution: + # self.logger.info("temp_val, duplicate_index", temp_val, duplicate_index, new_solution) + num_unsolved_duplicates = num_unsolved_duplicates + 1 + if not self.suppress_warnings: warnings.warn(f"Failed to find a unique value for gene with index {duplicate_index} whose value is {new_solution[duplicate_index]}. Consider adding more values in the gene space or use a wider range for initial population or random mutation.") + else: + new_solution[duplicate_index] = temp_val # Update the list of duplicate indices after each iteration. _, unique_gene_indices = numpy.unique(new_solution, return_index=True) @@ -264,7 +319,8 @@ def unique_gene_by_space(self, solution, gene_idx, gene_type, - build_initial_pop=False): + build_initial_pop=False, + num_trials=10): """ Returns a unique value for a specific gene based on its value space to resolve duplicates. @@ -273,6 +329,7 @@ def unique_gene_by_space(self, solution (list): A solution containing genes with duplicate values. gene_idx (int): The index of the gene that has a duplicate value. gene_type (type): The data type of the gene (e.g., int, float). + num_trials (int): The maximum number of attempts to resolve duplicates for each gene. Only works for floating-point numbers. Returns: Any: A unique value for the gene, if one exists; otherwise, the original gene value. """ @@ -320,9 +377,20 @@ def unique_gene_by_space(self, low = self.random_mutation_min_val high = self.random_mutation_max_val + """ value_from_space = numpy.random.uniform(low=low, high=high, size=1)[0] + """ + + value_from_space = self.unique_float_gene_from_range(solution=solution, + gene_index=gene_idx, + min_val=low, + max_val=high, + mutation_by_replacement=True, + gene_type=dtype, + num_trials=num_trials) + elif type(curr_gene_space) is dict: if self.gene_type_single == True: From b760c3ea506dc1ae0604c72de8aaeddc91b70ccd Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Sun, 8 Dec 2024 18:25:12 -0500 Subject: [PATCH 10/31] Fix a bug --- pygad/helper/unique.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pygad/helper/unique.py b/pygad/helper/unique.py index 8eb0505..3da2d71 100644 --- a/pygad/helper/unique.py +++ b/pygad/helper/unique.py @@ -173,14 +173,14 @@ def unique_int_gene_from_range(self, """ # The gene_type is of the form [type, precision] - dtype = gene_type[0] + dtype = gene_type # For non-integer steps, the numpy.arange() function returns zeros if the dtype parameter is set to an integer data type. So, this returns zeros if step is non-integer and dtype is set to an int data type: numpy.arange(min_val, max_val, step, dtype=gene_type[0]) # To solve this issue, the data type casting will not be handled inside numpy.arange(). The range is generated by numpy.arange() and then the data type is converted using the numpy.asarray() function. all_gene_values = numpy.asarray(numpy.arange(min_val, max_val, step), - dtype=dtype) + dtype=dtype[0]) # If mutation is by replacement, do not add the current gene value into the list. # This is to avoid replacing the value by itself again. We are doing nothing in this case. @@ -191,7 +191,7 @@ def unique_int_gene_from_range(self, # After adding solution[gene_index] to the list, we have to change the data type again. all_gene_values = numpy.asarray(all_gene_values, - dtype) + dtype[0]) values_to_select_from = list(set(list(all_gene_values)) - set(solution)) From 01eee8503b2251cda46b6744341b12045cf8bfe8 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Sun, 8 Dec 2024 19:25:51 -0500 Subject: [PATCH 11/31] Return first element of numpy.random.choice() --- pygad/helper/unique.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pygad/helper/unique.py b/pygad/helper/unique.py index 3da2d71..c9b097f 100644 --- a/pygad/helper/unique.py +++ b/pygad/helper/unique.py @@ -282,7 +282,7 @@ def unique_genes_by_space(self, Args: new_solution (list): A solution containing genes with duplicate values. - gene_type (type): The data type of the gene (e.g., int, float). + gene_type (type): The data type of the all the genes (e.g., int, float). not_unique_indices (list): The indices of genes with duplicate values. num_trials (int): The maximum number of attempts to resolve duplicates for each gene. Only works for floating-point numbers. @@ -417,7 +417,7 @@ def unique_gene_by_space(self, 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) + size=1)[0] else: value_from_space = numpy.random.uniform(low=curr_gene_space['low'], high=curr_gene_space['high'], @@ -479,7 +479,7 @@ def unique_gene_by_space(self, value_from_space = numpy.random.choice(numpy.arange(start=self.gene_space['low'], stop=self.gene_space['high'], step=self.gene_space['step']), - size=1) + size=1)[0] else: value_from_space = numpy.random.uniform(low=self.gene_space['low'], high=self.gene_space['high'], From 516ac20ba99504432f206086cc9651d1b0174c5e Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Mon, 9 Dec 2024 12:06:33 -0500 Subject: [PATCH 12/31] Read the requirements from the .txt file --- setup.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b9f88de..bbef371 100644 --- a/setup.py +++ b/setup.py @@ -3,11 +3,15 @@ with open("README.md", "r") as fh: long_description = fh.read() +# Read the requirements from the requirements.txt file +with open("requirements.txt", "r") as f: + requirements = f.read().splitlines() + setuptools.setup( name="pygad", version="3.3.1", author="Ahmed Fawzy Gad", - install_requires=["numpy", "matplotlib", "cloudpickle",], + install_requires=requirements, author_email="ahmed.f.gad@gmail.com", description="PyGAD: A Python Library for Building the Genetic Algorithm and Training Machine Learning Algoithms (Keras & PyTorch).", long_description=long_description, From 1a0c67fcc181cbfa5128d5d395676776e28cdf35 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Mon, 9 Dec 2024 12:12:44 -0500 Subject: [PATCH 13/31] Read the requirements from the .txt file --- setup.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index bbef371..9e79e05 100644 --- a/setup.py +++ b/setup.py @@ -1,10 +1,15 @@ import setuptools +import os with open("README.md", "r") as fh: long_description = fh.read() +# Dynamically read requirements.txt +this_dir = os.path.abspath(os.path.dirname(__file__)) +requirements_path = os.path.join(this_dir, "requirements.txt") + # Read the requirements from the requirements.txt file -with open("requirements.txt", "r") as f: +with open(requirements_path, "r") as f: requirements = f.read().splitlines() setuptools.setup( From 138333ade96f2baf40face76e7c6e69721d5221c Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Mon, 9 Dec 2024 12:15:29 -0500 Subject: [PATCH 14/31] Hardcode the requirements --- setup.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/setup.py b/setup.py index 9e79e05..b9f88de 100644 --- a/setup.py +++ b/setup.py @@ -1,22 +1,13 @@ import setuptools -import os with open("README.md", "r") as fh: long_description = fh.read() -# Dynamically read requirements.txt -this_dir = os.path.abspath(os.path.dirname(__file__)) -requirements_path = os.path.join(this_dir, "requirements.txt") - -# Read the requirements from the requirements.txt file -with open(requirements_path, "r") as f: - requirements = f.read().splitlines() - setuptools.setup( name="pygad", version="3.3.1", author="Ahmed Fawzy Gad", - install_requires=requirements, + install_requires=["numpy", "matplotlib", "cloudpickle",], author_email="ahmed.f.gad@gmail.com", description="PyGAD: A Python Library for Building the Genetic Algorithm and Training Machine Learning Algoithms (Keras & PyTorch).", long_description=long_description, From 2aa059d5c6532374a53b5e60b6bc51b1864aaac1 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Mon, 9 Dec 2024 12:31:14 -0500 Subject: [PATCH 15/31] Install the requirements in GitHub actions --- .github/workflows/main_py310.yml | 5 +++++ .github/workflows/main_py311.yml | 5 +++++ .github/workflows/main_py312.yml | 5 +++++ .github/workflows/main_py37.yml | 5 +++++ .github/workflows/main_py38.yml | 5 +++++ .github/workflows/main_py39.yml | 5 +++++ 6 files changed, 30 insertions(+) diff --git a/.github/workflows/main_py310.yml b/.github/workflows/main_py310.yml index 508dcf4..9602f17 100644 --- a/.github/workflows/main_py310.yml +++ b/.github/workflows/main_py310.yml @@ -20,6 +20,11 @@ jobs: with: python-version: '3.10' + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Build PyGAD from the Repository run: | python3 -m pip install --upgrade build diff --git a/.github/workflows/main_py311.yml b/.github/workflows/main_py311.yml index ce5a196..d468243 100644 --- a/.github/workflows/main_py311.yml +++ b/.github/workflows/main_py311.yml @@ -20,6 +20,11 @@ jobs: with: python-version: '3.11' + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Build PyGAD from the Repository run: | python3 -m pip install --upgrade build diff --git a/.github/workflows/main_py312.yml b/.github/workflows/main_py312.yml index 560d501..87bd648 100644 --- a/.github/workflows/main_py312.yml +++ b/.github/workflows/main_py312.yml @@ -28,6 +28,11 @@ jobs: with: python-version: '3.12.0-beta.2' + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Build PyGAD from the Repository run: | python3 -m pip install --upgrade build diff --git a/.github/workflows/main_py37.yml b/.github/workflows/main_py37.yml index 6268472..037086e 100644 --- a/.github/workflows/main_py37.yml +++ b/.github/workflows/main_py37.yml @@ -20,6 +20,11 @@ jobs: with: python-version: '3.7' + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Build PyGAD from the Repository run: | python3 -m pip install --upgrade build diff --git a/.github/workflows/main_py38.yml b/.github/workflows/main_py38.yml index d8902dc..602f917 100644 --- a/.github/workflows/main_py38.yml +++ b/.github/workflows/main_py38.yml @@ -20,6 +20,11 @@ jobs: with: python-version: '3.8' + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Build PyGAD from the Repository run: | python3 -m pip install --upgrade build diff --git a/.github/workflows/main_py39.yml b/.github/workflows/main_py39.yml index e4e0ef1..c6b61fc 100644 --- a/.github/workflows/main_py39.yml +++ b/.github/workflows/main_py39.yml @@ -20,6 +20,11 @@ jobs: with: python-version: '3.9' + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Build PyGAD from the Repository run: | python3 -m pip install --upgrade build From 9910cbd43e79b57a82f3da2e7b87f3c9caf22b99 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Tue, 10 Dec 2024 09:59:09 -0500 Subject: [PATCH 16/31] Create scorecard.yml --- .github/workflows/scorecard.yml | 73 +++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 .github/workflows/scorecard.yml diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 0000000..30d789a --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,73 @@ +# This workflow uses actions that are not certified by GitHub. They are provided +# by a third-party and are governed by separate terms of service, privacy +# policy, and support documentation. + +name: Scorecard supply-chain security +on: + # For Branch-Protection check. Only the default branch is supported. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection + branch_protection_rule: + # To guarantee Maintained check is occasionally updated. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained + schedule: + - cron: '42 5 * * 2' + push: + branches: [ "master" ] + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + # Needed to publish results and get a badge (see publish_results below). + id-token: write + # Uncomment the permissions below if installing in a private repository. + # contents: read + # actions: read + + steps: + - name: "Checkout code" + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 + with: + results_file: results.sarif + results_format: sarif + # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: + # - you want to enable the Branch-Protection check on a *public* repository, or + # - you are installing Scorecard on a *private* repository + # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional. + # repo_token: ${{ secrets.SCORECARD_TOKEN }} + + # Public repositories: + # - Publish results to OpenSSF REST API for easy access by consumers + # - Allows the repository to include the Scorecard badge. + # - See https://github.com/ossf/scorecard-action#publishing-results. + # For private repositories: + # - `publish_results` will always be set to `false`, regardless + # of the value entered here. + publish_results: true + + # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF + # format to the repository Actions tab. + - name: "Upload artifact" + uses: actions/upload-artifact@97a0fba1372883ab732affbe8f94b823f91727db # v3.pre.node20 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard (optional). + # Commenting out will disable upload of results to your repo's Code Scanning dashboard + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: results.sarif From fca6058cb712a24b1f761febe0a56bbcafcd3154 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Tue, 10 Dec 2024 10:01:03 -0500 Subject: [PATCH 17/31] Reformat docstring --- pygad/utils/crossover.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/pygad/utils/crossover.py b/pygad/utils/crossover.py index e6c489e..d7cd86e 100644 --- a/pygad/utils/crossover.py +++ b/pygad/utils/crossover.py @@ -13,11 +13,15 @@ def __init__(): def single_point_crossover(self, parents, offspring_size): """ - Applies the single-point crossover. It selects a point randomly at which crossover takes place between the pairs of parents. - It accepts 2 parameters: - -parents: The parents to mate for producing the offspring. - -offspring_size: The size of the offspring to produce. - It returns an array the produced offspring. + Applies single-point crossover between pairs of parents. + This function selects a random point at which crossover occurs between the parents, generating offspring. + + Parameters: + parents (array-like): The parents to mate for producing the offspring. + offspring_size (int): The number of offspring to produce. + + Returns: + array-like: An array containing the produced offspring. """ if self.gene_type_single == True: From a9d09b41dbb340dd3ae597acb9cebc721ca0dcf6 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Tue, 10 Dec 2024 10:02:56 -0500 Subject: [PATCH 18/31] Add OSSF and StackOverFlow badges --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cd84391..e2ede3a 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,8 @@ Check documentation of the [PyGAD](https://pygad.readthedocs.io/en/latest). -[![Downloads](https://pepy.tech/badge/pygad)](https://pepy.tech/project/pygad) [![PyPI version](https://badge.fury.io/py/pygad.svg)](https://badge.fury.io/py/pygad) ![Docs](https://readthedocs.org/projects/pygad/badge) [![PyGAD PyTest / Python 3.11](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py311.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py311.yml) [![PyGAD PyTest / Python 3.10](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py310.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py310.yml) [![PyGAD PyTest / Python 3.9](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py39.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py39.yml) [![PyGAD PyTest / Python 3.8](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py38.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py38.yml) [![PyGAD PyTest / Python 3.7](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py37.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py37.yml) [![License](https://img.shields.io/badge/License-BSD_3--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause) [![Translation](https://hosted.weblate.org/widgets/weblate/-/svg-badge.svg)](https://hosted.weblate.org/engage/weblate/) [![REUSE](https://api.reuse.software/badge/github.com/WeblateOrg/weblate)](https://api.reuse.software/info/github.com/WeblateOrg/weblate) +[![Downloads](https://pepy.tech/badge/pygad)](https://pepy.tech/project/pygad) [![PyPI version](https://badge.fury.io/py/pygad.svg)](https://badge.fury.io/py/pygad) ![Docs](https://readthedocs.org/projects/pygad/badge) [![PyGAD PyTest / Python 3.11](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py311.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py311.yml) [![PyGAD PyTest / Python 3.10](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py310.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py310.yml) [![PyGAD PyTest / Python 3.9](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py39.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py39.yml) [![PyGAD PyTest / Python 3.8](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py38.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py38.yml) [![PyGAD PyTest / Python 3.7](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py37.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py37.yml) [![License](https://img.shields.io/badge/License-BSD_3--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause) [![Translation](https://hosted.weblate.org/widgets/weblate/-/svg-badge.svg)](https://hosted.weblate.org/engage/weblate/) [![REUSE](https://api.reuse.software/badge/github.com/WeblateOrg/weblate)](https://api.reuse.software/info/github.com/WeblateOrg/weblate)[![Stack Overflow](https://img.shields.io/badge/stackoverflow-Ask%20questions-blue.svg)]( +https://stackoverflow.com/questions/tagged/pygad)[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/ahmedfgad/GeneticAlgorithmPython/badge)](https://securityscorecards.dev/viewer/?uri=github.com/ahmedfgad/GeneticAlgorithmPython) ![PYGAD-LOGO](https://user-images.githubusercontent.com/16560492/101267295-c74c0180-375f-11eb-9ad0-f8e37bd796ce.png) From bb26d3c39f859a068c8f83e398f814794c5ef300 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Tue, 10 Dec 2024 10:06:32 -0500 Subject: [PATCH 19/31] Add spaces between badges --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e2ede3a..0a39235 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ Check documentation of the [PyGAD](https://pygad.readthedocs.io/en/latest). -[![Downloads](https://pepy.tech/badge/pygad)](https://pepy.tech/project/pygad) [![PyPI version](https://badge.fury.io/py/pygad.svg)](https://badge.fury.io/py/pygad) ![Docs](https://readthedocs.org/projects/pygad/badge) [![PyGAD PyTest / Python 3.11](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py311.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py311.yml) [![PyGAD PyTest / Python 3.10](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py310.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py310.yml) [![PyGAD PyTest / Python 3.9](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py39.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py39.yml) [![PyGAD PyTest / Python 3.8](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py38.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py38.yml) [![PyGAD PyTest / Python 3.7](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py37.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py37.yml) [![License](https://img.shields.io/badge/License-BSD_3--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause) [![Translation](https://hosted.weblate.org/widgets/weblate/-/svg-badge.svg)](https://hosted.weblate.org/engage/weblate/) [![REUSE](https://api.reuse.software/badge/github.com/WeblateOrg/weblate)](https://api.reuse.software/info/github.com/WeblateOrg/weblate)[![Stack Overflow](https://img.shields.io/badge/stackoverflow-Ask%20questions-blue.svg)]( -https://stackoverflow.com/questions/tagged/pygad)[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/ahmedfgad/GeneticAlgorithmPython/badge)](https://securityscorecards.dev/viewer/?uri=github.com/ahmedfgad/GeneticAlgorithmPython) +[![Downloads](https://pepy.tech/badge/pygad)](https://pepy.tech/project/pygad) [![PyPI version](https://badge.fury.io/py/pygad.svg)](https://badge.fury.io/py/pygad) ![Docs](https://readthedocs.org/projects/pygad/badge) [![PyGAD PyTest / Python 3.11](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py311.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py311.yml) [![PyGAD PyTest / Python 3.10](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py310.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py310.yml) [![PyGAD PyTest / Python 3.9](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py39.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py39.yml) [![PyGAD PyTest / Python 3.8](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py38.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py38.yml) [![PyGAD PyTest / Python 3.7](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py37.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py37.yml) [![License](https://img.shields.io/badge/License-BSD_3--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause) [![Translation](https://hosted.weblate.org/widgets/weblate/-/svg-badge.svg)](https://hosted.weblate.org/engage/weblate/) [![REUSE](https://api.reuse.software/badge/github.com/WeblateOrg/weblate)](https://api.reuse.software/info/github.com/WeblateOrg/weblate) [![Stack Overflow](https://img.shields.io/badge/stackoverflow-Ask%20questions-blue.svg)]( +https://stackoverflow.com/questions/tagged/pygad) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/ahmedfgad/GeneticAlgorithmPython/badge)](https://securityscorecards.dev/viewer/?uri=github.com/ahmedfgad/GeneticAlgorithmPython) ![PYGAD-LOGO](https://user-images.githubusercontent.com/16560492/101267295-c74c0180-375f-11eb-9ad0-f8e37bd796ce.png) From 55776d2bae3a36d041d0552102f64dcae0d88c3c Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Tue, 10 Dec 2024 10:08:52 -0500 Subject: [PATCH 20/31] Add Conda downloads badge --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0a39235..c3c20cf 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,8 @@ Check documentation of the [PyGAD](https://pygad.readthedocs.io/en/latest). -[![Downloads](https://pepy.tech/badge/pygad)](https://pepy.tech/project/pygad) [![PyPI version](https://badge.fury.io/py/pygad.svg)](https://badge.fury.io/py/pygad) ![Docs](https://readthedocs.org/projects/pygad/badge) [![PyGAD PyTest / Python 3.11](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py311.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py311.yml) [![PyGAD PyTest / Python 3.10](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py310.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py310.yml) [![PyGAD PyTest / Python 3.9](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py39.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py39.yml) [![PyGAD PyTest / Python 3.8](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py38.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py38.yml) [![PyGAD PyTest / Python 3.7](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py37.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py37.yml) [![License](https://img.shields.io/badge/License-BSD_3--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause) [![Translation](https://hosted.weblate.org/widgets/weblate/-/svg-badge.svg)](https://hosted.weblate.org/engage/weblate/) [![REUSE](https://api.reuse.software/badge/github.com/WeblateOrg/weblate)](https://api.reuse.software/info/github.com/WeblateOrg/weblate) [![Stack Overflow](https://img.shields.io/badge/stackoverflow-Ask%20questions-blue.svg)]( +[![Downloads](https://pepy.tech/badge/pygad)](https://pepy.tech/project/pygad) [![Conda Downloads](https://img.shields.io/conda/dn/conda-forge/pygad.svg?label=Conda%20downloads)]( +https://anaconda.org/conda-forge/PyGAD) [![PyPI version](https://badge.fury.io/py/pygad.svg)](https://badge.fury.io/py/pygad) ![Docs](https://readthedocs.org/projects/pygad/badge) [![PyGAD PyTest / Python 3.11](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py311.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py311.yml) [![PyGAD PyTest / Python 3.10](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py310.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py310.yml) [![PyGAD PyTest / Python 3.9](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py39.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py39.yml) [![PyGAD PyTest / Python 3.8](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py38.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py38.yml) [![PyGAD PyTest / Python 3.7](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py37.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py37.yml) [![License](https://img.shields.io/badge/License-BSD_3--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause) [![Translation](https://hosted.weblate.org/widgets/weblate/-/svg-badge.svg)](https://hosted.weblate.org/engage/weblate/) [![REUSE](https://api.reuse.software/badge/github.com/WeblateOrg/weblate)](https://api.reuse.software/info/github.com/WeblateOrg/weblate) [![Stack Overflow](https://img.shields.io/badge/stackoverflow-Ask%20questions-blue.svg)]( https://stackoverflow.com/questions/tagged/pygad) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/ahmedfgad/GeneticAlgorithmPython/badge)](https://securityscorecards.dev/viewer/?uri=github.com/ahmedfgad/GeneticAlgorithmPython) ![PYGAD-LOGO](https://user-images.githubusercontent.com/16560492/101267295-c74c0180-375f-11eb-9ad0-f8e37bd796ce.png) From a49d9089702d107b86d431fe091c796286054f56 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Tue, 10 Dec 2024 10:14:41 -0500 Subject: [PATCH 21/31] Add paper DOI badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c3c20cf..fde5c6d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Check documentation of the [PyGAD](https://pygad.readthedocs.io/en/latest). [![Downloads](https://pepy.tech/badge/pygad)](https://pepy.tech/project/pygad) [![Conda Downloads](https://img.shields.io/conda/dn/conda-forge/pygad.svg?label=Conda%20downloads)]( https://anaconda.org/conda-forge/PyGAD) [![PyPI version](https://badge.fury.io/py/pygad.svg)](https://badge.fury.io/py/pygad) ![Docs](https://readthedocs.org/projects/pygad/badge) [![PyGAD PyTest / Python 3.11](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py311.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py311.yml) [![PyGAD PyTest / Python 3.10](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py310.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py310.yml) [![PyGAD PyTest / Python 3.9](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py39.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py39.yml) [![PyGAD PyTest / Python 3.8](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py38.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py38.yml) [![PyGAD PyTest / Python 3.7](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py37.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py37.yml) [![License](https://img.shields.io/badge/License-BSD_3--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause) [![Translation](https://hosted.weblate.org/widgets/weblate/-/svg-badge.svg)](https://hosted.weblate.org/engage/weblate/) [![REUSE](https://api.reuse.software/badge/github.com/WeblateOrg/weblate)](https://api.reuse.software/info/github.com/WeblateOrg/weblate) [![Stack Overflow](https://img.shields.io/badge/stackoverflow-Ask%20questions-blue.svg)]( -https://stackoverflow.com/questions/tagged/pygad) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/ahmedfgad/GeneticAlgorithmPython/badge)](https://securityscorecards.dev/viewer/?uri=github.com/ahmedfgad/GeneticAlgorithmPython) +https://stackoverflow.com/questions/tagged/pygad) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/ahmedfgad/GeneticAlgorithmPython/badge)](https://securityscorecards.dev/viewer/?uri=github.com/ahmedfgad/GeneticAlgorithmPython) [![DOI](https://zenodo.org/badge/DOI/10.1007/s11042-023-17167-y.svg)](https://doi.org/10.1007/s11042-023-17167-y) ![PYGAD-LOGO](https://user-images.githubusercontent.com/16560492/101267295-c74c0180-375f-11eb-9ad0-f8e37bd796ce.png) From 27812998ab2dce951ecaee418748ec5bb09f9d4d Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Tue, 10 Dec 2024 10:18:17 -0500 Subject: [PATCH 22/31] Add Conda downloads badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fde5c6d..75cd596 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Check documentation of the [PyGAD](https://pygad.readthedocs.io/en/latest). -[![Downloads](https://pepy.tech/badge/pygad)](https://pepy.tech/project/pygad) [![Conda Downloads](https://img.shields.io/conda/dn/conda-forge/pygad.svg?label=Conda%20downloads)]( +[![PyPI Downloads](https://pepy.tech/badge/pygad)](https://pepy.tech/project/pygad) [![Conda Downloads](https://img.shields.io/conda/dn/conda-forge/pygad.svg?label=Conda%20Downloads)]( https://anaconda.org/conda-forge/PyGAD) [![PyPI version](https://badge.fury.io/py/pygad.svg)](https://badge.fury.io/py/pygad) ![Docs](https://readthedocs.org/projects/pygad/badge) [![PyGAD PyTest / Python 3.11](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py311.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py311.yml) [![PyGAD PyTest / Python 3.10](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py310.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py310.yml) [![PyGAD PyTest / Python 3.9](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py39.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py39.yml) [![PyGAD PyTest / Python 3.8](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py38.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py38.yml) [![PyGAD PyTest / Python 3.7](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py37.yml/badge.svg)](https://github.com/ahmedfgad/GeneticAlgorithmPython/actions/workflows/main_py37.yml) [![License](https://img.shields.io/badge/License-BSD_3--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause) [![Translation](https://hosted.weblate.org/widgets/weblate/-/svg-badge.svg)](https://hosted.weblate.org/engage/weblate/) [![REUSE](https://api.reuse.software/badge/github.com/WeblateOrg/weblate)](https://api.reuse.software/info/github.com/WeblateOrg/weblate) [![Stack Overflow](https://img.shields.io/badge/stackoverflow-Ask%20questions-blue.svg)]( https://stackoverflow.com/questions/tagged/pygad) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/ahmedfgad/GeneticAlgorithmPython/badge)](https://securityscorecards.dev/viewer/?uri=github.com/ahmedfgad/GeneticAlgorithmPython) [![DOI](https://zenodo.org/badge/DOI/10.1007/s11042-023-17167-y.svg)](https://doi.org/10.1007/s11042-023-17167-y) From 67ff07f174678be1d4c352e01f122d4cfc7945d3 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Thu, 12 Dec 2024 16:56:22 -0500 Subject: [PATCH 23/31] Add paper DOI badge --- pygad/helper/unique.py | 383 ++++++++++++++++++++--------------------- 1 file changed, 187 insertions(+), 196 deletions(-) diff --git a/pygad/helper/unique.py b/pygad/helper/unique.py index c9b097f..8b523f3 100644 --- a/pygad/helper/unique.py +++ b/pygad/helper/unique.py @@ -16,75 +16,66 @@ def solve_duplicate_genes_randomly(self, mutation_by_replacement, gene_type, num_trials=10): - """ - Resolves duplicates in a solution by randomly selecting new values for the duplicate genes. - - Args: - solution (list): A solution containing genes, potentially with duplicate values. - min_val (int): The minimum value of the range to sample a number randomly. - max_val (int): The maximum value of the range to sample a number randomly. - mutation_by_replacement (bool): Indicates if mutation is performed by replacement. - gene_type (type): The data type of the gene (e.g., int, float). - num_trials (int): The maximum number of attempts to resolve duplicates by changing the gene values. Only works for floating-point gene types. + """ + Resolves duplicates in a solution by randomly selecting new values for the duplicate genes. - Returns: - tuple: - list: The updated solution after attempting to resolve duplicates. If no duplicates are resolved, the solution remains unchanged. - list: The indices of genes that still have duplicate values. - int: The number of duplicates that could not be resolved. - """ + Args: + solution (list): A solution containing genes, potentially with duplicate values. + min_val (int): The minimum value of the range to sample a number randomly. + max_val (int): The maximum value of the range to sample a number randomly. + mutation_by_replacement (bool): Indicates if mutation is performed by replacement. + gene_type (type): The data type of the gene (e.g., int, float). + num_trials (int): The maximum number of attempts to resolve duplicates by changing the gene values. Only works for floating-point gene types. - new_solution = solution.copy() - - _, unique_gene_indices = numpy.unique(solution, return_index=True) - not_unique_indices = set(range(len(solution))) - set(unique_gene_indices) + Returns: + tuple: + list: The updated solution after attempting to resolve duplicates. If no duplicates are resolved, the solution remains unchanged. + list: The indices of genes that still have duplicate values. + int: The number of duplicates that could not be resolved. + """ - num_unsolved_duplicates = 0 - if len(not_unique_indices) > 0: - for duplicate_index in not_unique_indices: - if self.gene_type_single == True: - dtype = gene_type - else: - dtype = gene_type[duplicate_index] + new_solution = solution.copy() - if dtype[0] in pygad.GA.supported_int_types: - temp_val = self.unique_int_gene_from_range(solution=new_solution, - gene_index=duplicate_index, - min_val=min_val, - max_val=max_val, - mutation_by_replacement=mutation_by_replacement, - gene_type=gene_type) - else: - temp_val = self.unique_float_gene_from_range(solution=new_solution, - gene_index=duplicate_index, - min_val=min_val, - max_val=max_val, - mutation_by_replacement=mutation_by_replacement, - gene_type=gene_type, - num_trials=num_trials) - """ - temp_val = numpy.random.uniform(low=min_val, - high=max_val, - size=1)[0] - if mutation_by_replacement: - pass - else: - temp_val = new_solution[duplicate_index] + temp_val - """ + _, unique_gene_indices = numpy.unique(solution, return_index=True) + not_unique_indices = set(range(len(solution))) - set(unique_gene_indices) - if temp_val in new_solution: - num_unsolved_duplicates = num_unsolved_duplicates + 1 - if not self.suppress_warnings: warnings.warn(f"Failed to find a unique value for gene with index {duplicate_index} whose value is {solution[duplicate_index]}. Consider adding more values in the gene space or use a wider range for initial population or random mutation.") - else: - # Unique gene value found. - new_solution[duplicate_index] = temp_val + num_unsolved_duplicates = 0 + if len(not_unique_indices) > 0: + for duplicate_index in not_unique_indices: + if self.gene_type_single == True: + dtype = gene_type + else: + dtype = gene_type[duplicate_index] + + if dtype[0] in pygad.GA.supported_int_types: + temp_val = self.unique_int_gene_from_range(solution=new_solution, + gene_index=duplicate_index, + min_val=min_val, + max_val=max_val, + mutation_by_replacement=mutation_by_replacement, + gene_type=gene_type) + else: + temp_val = self.unique_float_gene_from_range(solution=new_solution, + gene_index=duplicate_index, + min_val=min_val, + max_val=max_val, + mutation_by_replacement=mutation_by_replacement, + gene_type=gene_type, + num_trials=num_trials) + + if temp_val in new_solution: + num_unsolved_duplicates = num_unsolved_duplicates + 1 + if not self.suppress_warnings: warnings.warn(f"Failed to find a unique value for gene with index {duplicate_index} whose value is {solution[duplicate_index]}. Consider adding more values in the gene space or use a wider range for initial population or random mutation.") + else: + # Unique gene value found. + new_solution[duplicate_index] = temp_val - # Update the list of duplicate indices after each iteration. - _, unique_gene_indices = numpy.unique(new_solution, return_index=True) - not_unique_indices = set(range(len(solution))) - set(unique_gene_indices) - # self.logger.info("not_unique_indices INSIDE", not_unique_indices) + # Update the list of duplicate indices after each iteration. + _, unique_gene_indices = numpy.unique(new_solution, return_index=True) + not_unique_indices = set(range(len(solution))) - set(unique_gene_indices) + # self.logger.info("not_unique_indices INSIDE", not_unique_indices) - return new_solution, not_unique_indices, num_unsolved_duplicates + return new_solution, not_unique_indices, num_unsolved_duplicates def solve_duplicate_genes_by_space(self, solution, @@ -92,60 +83,60 @@ def solve_duplicate_genes_by_space(self, num_trials=10, build_initial_pop=False): - """ - Resolves duplicates in a solution by selecting new values for the duplicate genes from the gene space. + """ + Resolves duplicates in a solution by selecting new values for the duplicate genes from the gene space. - Args: - solution (list): A solution containing genes, potentially with duplicate values. - gene_type (type): The data type of the gene (e.g., int, float). - num_trials (int): The maximum number of attempts to resolve duplicates by selecting values from the gene space. + Args: + solution (list): A solution containing genes, potentially with duplicate values. + gene_type (type): The data type of the gene (e.g., int, float). + num_trials (int): The maximum number of attempts to resolve duplicates by selecting values from the gene space. - Returns: - tuple: - list: The updated solution after attempting to resolve duplicates. If no duplicates are resolved, the solution remains unchanged. - list: The indices of genes that still have duplicate values. - int: The number of duplicates that could not be resolved. - """ + Returns: + tuple: + list: The updated solution after attempting to resolve duplicates. If no duplicates are resolved, the solution remains unchanged. + list: The indices of genes that still have duplicate values. + int: The number of duplicates that could not be resolved. + """ - new_solution = solution.copy() - - _, unique_gene_indices = numpy.unique(solution, return_index=True) - not_unique_indices = set(range(len(solution))) - set(unique_gene_indices) - # self.logger.info("not_unique_indices OUTSIDE", not_unique_indices) - - # First try to solve the duplicates. - # For a solution like [3 2 0 0], the indices of the 2 duplicating genes are 2 and 3. - # The next call to the find_unique_value() method tries to change the value of the gene with index 3 to solve the duplicate. - if len(not_unique_indices) > 0: - new_solution, not_unique_indices, num_unsolved_duplicates = self.unique_genes_by_space(new_solution=new_solution, - gene_type=gene_type, - not_unique_indices=not_unique_indices, - num_trials=10, - build_initial_pop=build_initial_pop) - else: - return new_solution, not_unique_indices, len(not_unique_indices) + new_solution = solution.copy() + + _, unique_gene_indices = numpy.unique(solution, return_index=True) + not_unique_indices = set(range(len(solution))) - set(unique_gene_indices) + # self.logger.info("not_unique_indices OUTSIDE", not_unique_indices) + + # First try to solve the duplicates. + # For a solution like [3 2 0 0], the indices of the 2 duplicating genes are 2 and 3. + # The next call to the find_unique_value() method tries to change the value of the gene with index 3 to solve the duplicate. + if len(not_unique_indices) > 0: + new_solution, not_unique_indices, num_unsolved_duplicates = self.unique_genes_by_space(new_solution=new_solution, + gene_type=gene_type, + not_unique_indices=not_unique_indices, + num_trials=10, + build_initial_pop=build_initial_pop) + else: + return new_solution, not_unique_indices, len(not_unique_indices) - # Do another try if there exist duplicate genes. - # If there are no possible values for the gene 3 with index 3 to solve the duplicate, try to change the value of the other gene with index 2. - if len(not_unique_indices) > 0: - not_unique_indices = set(numpy.where(new_solution == new_solution[list(not_unique_indices)[0]])[0]) - set([list(not_unique_indices)[0]]) - new_solution, not_unique_indices, num_unsolved_duplicates = self.unique_genes_by_space(new_solution=new_solution, - gene_type=gene_type, - not_unique_indices=not_unique_indices, - num_trials=10, - build_initial_pop=build_initial_pop) - else: - # DEEP-DUPLICATE-REMOVAL-NEEDED - # Search by this phrase to find where deep duplicates removal should be applied. - - # If there exist duplicate genes, then changing either of the 2 duplicating genes (with indices 2 and 3) will not solve the problem. - # This problem can be solved by randomly changing one of the non-duplicating genes that may make a room for a unique value in one the 2 duplicating genes. - # For example, if gene_space=[[3, 0, 1], [4, 1, 2], [0, 2], [3, 2, 0]] and the solution is [3 2 0 0], then the values of the last 2 genes duplicate. - # There are no possible changes in the last 2 genes to solve the problem. But it could be solved by changing the second gene from 2 to 4. - # As a result, any of the last 2 genes can take the value 2 and solve the duplicates. - return new_solution, not_unique_indices, len(not_unique_indices) + # Do another try if there exist duplicate genes. + # If there are no possible values for the gene 3 with index 3 to solve the duplicate, try to change the value of the other gene with index 2. + if len(not_unique_indices) > 0: + not_unique_indices = set(numpy.where(new_solution == new_solution[list(not_unique_indices)[0]])[0]) - set([list(not_unique_indices)[0]]) + new_solution, not_unique_indices, num_unsolved_duplicates = self.unique_genes_by_space(new_solution=new_solution, + gene_type=gene_type, + not_unique_indices=not_unique_indices, + num_trials=10, + build_initial_pop=build_initial_pop) + else: + # DEEP-DUPLICATE-REMOVAL-NEEDED + # Search by this phrase to find where deep duplicates removal should be applied. + + # If there exist duplicate genes, then changing either of the 2 duplicating genes (with indices 2 and 3) will not solve the problem. + # This problem can be solved by randomly changing one of the non-duplicating genes that may make a room for a unique value in one the 2 duplicating genes. + # For example, if gene_space=[[3, 0, 1], [4, 1, 2], [0, 2], [3, 2, 0]] and the solution is [3 2 0 0], then the values of the last 2 genes duplicate. + # There are no possible changes in the last 2 genes to solve the problem. But it could be solved by changing the second gene from 2 to 4. + # As a result, any of the last 2 genes can take the value 2 and solve the duplicates. + return new_solution, not_unique_indices, len(not_unique_indices) - return new_solution, not_unique_indices, num_unsolved_duplicates + return new_solution, not_unique_indices, num_unsolved_duplicates def unique_int_gene_from_range(self, solution, @@ -156,54 +147,54 @@ def unique_int_gene_from_range(self, gene_type, step=1): - """ - Finds a unique integer value for a specific gene in a solution. + """ + Finds a unique integer value for a specific gene in a solution. - Args: - solution (list): A solution containing genes, potentially with duplicate values. - gene_index (int): The index of the gene for which to find a unique value. - min_val (int): The minimum value of the range to sample an integer randomly. - max_val (int): The maximum value of the range to sample an integer randomly. - mutation_by_replacement (bool): Indicates if mutation is performed by replacement. - gene_type (type): The data type of the gene (e.g., int, int8, uint16, etc). - step (int, optional): The step size for generating candidate values. Defaults to 1. + Args: + solution (list): A solution containing genes, potentially with duplicate values. + gene_index (int): The index of the gene for which to find a unique value. + min_val (int): The minimum value of the range to sample an integer randomly. + max_val (int): The maximum value of the range to sample an integer randomly. + mutation_by_replacement (bool): Indicates if mutation is performed by replacement. + gene_type (type): The data type of the gene (e.g., int, int8, uint16, etc). + step (int, optional): The step size for generating candidate values. Defaults to 1. - Returns: - int: The new integer value of the gene. If no unique value can be found, the original gene value is returned. - """ + Returns: + int: The new integer value of the gene. If no unique value can be found, the original gene value is returned. + """ - # The gene_type is of the form [type, precision] - dtype = gene_type + # The gene_type is of the form [type, precision] + dtype = gene_type - # For non-integer steps, the numpy.arange() function returns zeros if the dtype parameter is set to an integer data type. So, this returns zeros if step is non-integer and dtype is set to an int data type: numpy.arange(min_val, max_val, step, dtype=gene_type[0]) - # To solve this issue, the data type casting will not be handled inside numpy.arange(). The range is generated by numpy.arange() and then the data type is converted using the numpy.asarray() function. - all_gene_values = numpy.asarray(numpy.arange(min_val, - max_val, - step), - dtype=dtype[0]) + # For non-integer steps, the numpy.arange() function returns zeros if the dtype parameter is set to an integer data type. So, this returns zeros if step is non-integer and dtype is set to an int data type: numpy.arange(min_val, max_val, step, dtype=gene_type[0]) + # To solve this issue, the data type casting will not be handled inside numpy.arange(). The range is generated by numpy.arange() and then the data type is converted using the numpy.asarray() function. + all_gene_values = numpy.asarray(numpy.arange(min_val, + max_val, + step), + dtype=dtype[0]) - # If mutation is by replacement, do not add the current gene value into the list. - # This is to avoid replacing the value by itself again. We are doing nothing in this case. - if mutation_by_replacement: - pass - else: - all_gene_values = all_gene_values + solution[gene_index] + # If mutation is by replacement, do not add the current gene value into the list. + # This is to avoid replacing the value by itself again. We are doing nothing in this case. + if mutation_by_replacement: + pass + else: + all_gene_values = all_gene_values + solution[gene_index] - # After adding solution[gene_index] to the list, we have to change the data type again. - all_gene_values = numpy.asarray(all_gene_values, - dtype[0]) + # After adding solution[gene_index] to the list, we have to change the data type again. + all_gene_values = numpy.asarray(all_gene_values, + dtype[0]) - values_to_select_from = list(set(list(all_gene_values)) - set(solution)) + values_to_select_from = list(set(list(all_gene_values)) - set(solution)) - if len(values_to_select_from) == 0: - # If there are no values, then keep the current gene value. - selected_value = solution[gene_index] - else: - selected_value = random.choice(values_to_select_from) + if len(values_to_select_from) == 0: + # If there are no values, then keep the current gene value. + selected_value = solution[gene_index] + else: + selected_value = random.choice(values_to_select_from) - selected_value = dtype[0](selected_value) + selected_value = dtype[0](selected_value) - return selected_value + return selected_value def unique_float_gene_from_range(self, solution, @@ -214,60 +205,60 @@ def unique_float_gene_from_range(self, gene_type, num_trials=10): - """ - Finds a unique floating-point value for a specific gene in a solution. + """ + Finds a unique floating-point value for a specific gene in a solution. - Args: - solution (list): A solution containing genes, potentially with duplicate values. - gene_index (int): The index of the gene for which to find a unique value. - min_val (int): The minimum value of the range to sample a floating-point number randomly. - max_val (int): The maximum value of the range to sample a floating-point number randomly. - mutation_by_replacement (bool): Indicates if mutation is performed by replacement. - gene_type (type): The data type of the gene (e.g., float, float16, float32, etc). - num_trials (int): The maximum number of attempts to resolve duplicates by changing the gene values. + Args: + solution (list): A solution containing genes, potentially with duplicate values. + gene_index (int): The index of the gene for which to find a unique value. + min_val (int): The minimum value of the range to sample a floating-point number randomly. + max_val (int): The maximum value of the range to sample a floating-point number randomly. + mutation_by_replacement (bool): Indicates if mutation is performed by replacement. + gene_type (type): The data type of the gene (e.g., float, float16, float32, etc). + num_trials (int): The maximum number of attempts to resolve duplicates by changing the gene values. - Returns: - int: The new floating-point value of the gene. If no unique value can be found, the original gene value is returned. - """ + Returns: + int: The new floating-point value of the gene. If no unique value can be found, the original gene value is returned. + """ - # The gene_type is of the form [type, precision] - dtype = gene_type + # The gene_type is of the form [type, precision] + dtype = gene_type - for trial_index in range(num_trials): - temp_val = numpy.random.uniform(low=min_val, - high=max_val, - size=1)[0] + for trial_index in range(num_trials): + temp_val = numpy.random.uniform(low=min_val, + high=max_val, + size=1)[0] - # If mutation is by replacement, do not add the current gene value into the list. - # This is to avoid replacing the value by itself again. We are doing nothing in this case. - if mutation_by_replacement: - pass - else: - temp_val = temp_val + solution[gene_index] + # If mutation is by replacement, do not add the current gene value into the list. + # This is to avoid replacing the value by itself again. We are doing nothing in this case. + if mutation_by_replacement: + pass + else: + temp_val = temp_val + solution[gene_index] - if not dtype[1] is None: - # Precision is available and we have to round the number. - # Convert the data type and round the number. - temp_val = numpy.round(dtype[0](temp_val), - dtype[1]) - else: - # There is no precision and rounding the number is not needed. The type is [type, None] - # Just convert the data type. - temp_val = dtype[0](temp_val) - - if temp_val in solution and trial_index == (num_trials - 1): - # If there are no values, then keep the current gene value. - if not self.suppress_warnings: warnings.warn("You set 'allow_duplicate_genes=False' but cannot find a value to prevent duplicates.") - selected_value = solution[gene_index] - elif temp_val in solution: - # Keep trying in the other remaining trials. - continue - else: - # Unique gene value found. - selected_value = temp_val - break + if not dtype[1] is None: + # Precision is available and we have to round the number. + # Convert the data type and round the number. + temp_val = numpy.round(dtype[0](temp_val), + dtype[1]) + else: + # There is no precision and rounding the number is not needed. The type is [type, None] + # Just convert the data type. + temp_val = dtype[0](temp_val) + + if temp_val in solution and trial_index == (num_trials - 1): + # If there are no values, then keep the current gene value. + if not self.suppress_warnings: warnings.warn("You set 'allow_duplicate_genes=False' but cannot find a value to prevent duplicates.") + selected_value = solution[gene_index] + elif temp_val in solution: + # Keep trying in the other remaining trials. + continue + else: + # Unique gene value found. + selected_value = temp_val + break - return selected_value + return selected_value def unique_genes_by_space(self, new_solution, From 82fa0f8e25cb2cc05cc047c2914f0b993257848e Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Tue, 7 Jan 2025 12:37:53 -0500 Subject: [PATCH 24/31] Create the plot_pareto_front_curve() method to plot the pareto front curve --- pygad/visualize/plot.py | 101 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/pygad/visualize/plot.py b/pygad/visualize/plot.py index 7dffc4b..3ddeaff 100644 --- a/pygad/visualize/plot.py +++ b/pygad/visualize/plot.py @@ -384,3 +384,104 @@ def plot_genes(self, matplotlib.pyplot.show() return fig + + def plot_pareto_front_curve(self, + title="Pareto Front Curve", + xlabel="Objective 1", + ylabel="Objective 2", + linewidth=3, + font_size=14, + label="Pareto Front", + color="#FF6347", + color_fitness="#4169E1", + grid=True, + alpha=0.7, + marker="o", + save_dir=None): + """ + Creates, shows, and returns the pareto front curve. Can only be used with multi-objective problems. + It only works with 2 objectives. + It also works only after completing at least 1 generation. If no generation is completed, an exception is raised. + + Accepts the following: + title: Figure title. + xlabel: Label on the X-axis. + ylabel: Label on the Y-axis. + linewidth: Line width of the plot. Defaults to 3. + font_size: Font size for the labels and title. Defaults to 14. + label: The label used for the legend. + color: Color of the plot. + color_fitness: Color of the fitness points. + grid: Either True or False to control the visibility of the grid. + alpha: The transparency of the pareto front curve. + marker: The marker of the fitness points. + save_dir: Directory to save the figure. + + Returns the figure. + """ + + if self.generations_completed < 1: + self.logger.error("The plot_pareto_front_curve() method can only be called after completing at least 1 generation but ({self.generations_completed}) is completed.") + raise RuntimeError("The plot_pareto_front_curve() method can only be called after completing at least 1 generation but ({self.generations_completed}) is completed.") + + if type(self.best_solutions_fitness[0]) in [list, tuple, numpy.ndarray] and len(self.best_solutions_fitness[0]) > 1: + # Multi-objective optimization problem. + if len(self.best_solutions_fitness[0]) == 2: + # Only 2 objectives. Proceed. + pass + else: + # More than 2 objectives. + self.logger.error(f"The plot_pareto_front_curve() method only supports 2 objectives but there are {self.best_solutions_fitness[0]} objectives.") + raise RuntimeError(f"The plot_pareto_front_curve() method only supports 2 objectives but there are {self.best_solutions_fitness[0]} objectives.") + else: + # Single-objective optimization problem. + self.logger.error("The plot_pareto_front_curve() method only works with multi-objective optimization problems.") + raise RuntimeError("The plot_pareto_front_curve() method only works with multi-objective optimization problems.") + + # Plot the pareto front curve. + remaining_set = list(zip(range(0, self.last_generation_fitness.shape[0]), self.last_generation_fitness)) + dominated_set, non_dominated_set = self.get_non_dominated_set(remaining_set) + + # Extract the fitness values (objective values) of the non-dominated solutions for plotting. + pareto_front_x = [self.last_generation_fitness[item[0]][0] for item in dominated_set] + pareto_front_y = [self.last_generation_fitness[item[0]][1] for item in dominated_set] + + # Sort the Pareto front solutions (optional but can make the plot cleaner) + sorted_pareto_front = sorted(zip(pareto_front_x, pareto_front_y)) + + # Plotting + fig = matplotlib.pyplot.figure() + # First, plot the scatter of all points (population) + all_points_x = [self.last_generation_fitness[i][0] for i in range(self.sol_per_pop)] + all_points_y = [self.last_generation_fitness[i][1] for i in range(self.sol_per_pop)] + matplotlib.pyplot.scatter(all_points_x, + all_points_y, + marker=marker, + color=color_fitness, + label='Fitness', + alpha=1.0) + + # Then, plot the Pareto front as a curve + pareto_front_x_sorted, pareto_front_y_sorted = zip(*sorted_pareto_front) + matplotlib.pyplot.plot(pareto_front_x_sorted, + pareto_front_y_sorted, + marker=marker, + label=label, + alpha=alpha, + color=color, + linewidth=linewidth) + + matplotlib.pyplot.title(title, fontsize=font_size) + matplotlib.pyplot.xlabel(xlabel, fontsize=font_size) + matplotlib.pyplot.ylabel(ylabel, fontsize=font_size) + matplotlib.pyplot.legend() + + matplotlib.pyplot.grid(grid) + + if not save_dir is None: + matplotlib.pyplot.savefig(fname=save_dir, + bbox_inches='tight') + + matplotlib.pyplot.show() + + return fig From 84559cea33e4d2b5ced27fb2f65646f6abf98291 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Tue, 7 Jan 2025 12:59:00 -0500 Subject: [PATCH 25/31] Document the plot_pareto_front_curve() method. --- docs/source/visualize.rst | 142 +++++++++++++++++++++++++++----------- 1 file changed, 101 insertions(+), 41 deletions(-) diff --git a/docs/source/visualize.rst b/docs/source/visualize.rst index 45dc1e4..629d4df 100644 --- a/docs/source/visualize.rst +++ b/docs/source/visualize.rst @@ -10,11 +10,15 @@ visualization in PyGAD. This section discusses the different options to visualize the results in PyGAD through these methods: -1. ``plot_fitness()``: Create plots for the fitness. +1. ``plot_fitness()``: Creates plots for the fitness. -2. ``plot_genes()``: Create plots for the genes. +2. ``plot_genes()``: Creates plots for the genes. -3. ``plot_new_solution_rate()``: Create plots for the new solution rate. +3. ``plot_new_solution_rate()``: Creates plots for the new solution + rate. + +4. ``plot_pareto_front_curve()``: Creates plots for the pareto front for + multi-objective problems. In the following code, the ``save_solutions`` flag is set to ``True`` which means all solutions are saved in the ``solutions`` attribute. The @@ -87,7 +91,7 @@ This method accepts the following parameters: 9. ``save_dir``: Directory to save the figure. -.. _plottypeplot: +.. _plottype=plot: ``plot_type="plot"`` ~~~~~~~~~~~~~~~~~~~~ @@ -101,10 +105,9 @@ line connecting the fitness values across all generations: ga_instance.plot_fitness() # ga_instance.plot_fitness(plot_type="plot") -.. image:: https://user-images.githubusercontent.com/16560492/122472609-d02f5280-cf8e-11eb-88a7-f9366ff6e7c6.png - :alt: +|image1| -.. _plottypescatter: +.. _plottype=scatter: ``plot_type="scatter"`` ~~~~~~~~~~~~~~~~~~~~~~~ @@ -117,10 +120,9 @@ these dots can be changed using the ``linewidth`` parameter. ga_instance.plot_fitness(plot_type="scatter") -.. image:: https://user-images.githubusercontent.com/16560492/122473159-75e2c180-cf8f-11eb-942d-31279b286dbd.png - :alt: +|image2| -.. _plottypebar: +.. _plottype=bar: ``plot_type="bar"`` ~~~~~~~~~~~~~~~~~~~ @@ -132,8 +134,7 @@ bar graph with each individual fitness represented as a bar. ga_instance.plot_fitness(plot_type="bar") -.. image:: https://user-images.githubusercontent.com/16560492/122473340-b7736c80-cf8f-11eb-89c5-4f7db3b653cc.png - :alt: +|image3| New Solution Rate ================= @@ -174,7 +175,7 @@ in the ``plot_fitness()`` method (it also have 3 possible values for 8. ``save_dir``: Directory to save the figure. -.. _plottypeplot-2: +.. _plottype=plot-2: ``plot_type="plot"`` ~~~~~~~~~~~~~~~~~~~~ @@ -192,10 +193,9 @@ first generation is always equal to the number of solutions in the population (i.e. the value assigned to the ``sol_per_pop`` parameter in the constructor of the ``pygad.GA`` class) which is 10 in this example. -.. image:: https://user-images.githubusercontent.com/16560492/122475815-3322e880-cf93-11eb-9648-bf66f823234b.png - :alt: +|image4| -.. _plottypescatter-2: +.. _plottype=scatter-2: ``plot_type="scatter"`` ~~~~~~~~~~~~~~~~~~~~~~~ @@ -207,10 +207,9 @@ The previous graph can be represented as scattered points by setting ga_instance.plot_new_solution_rate(plot_type="scatter") -.. image:: https://user-images.githubusercontent.com/16560492/122476108-adec0380-cf93-11eb-80ac-7588bf90492f.png - :alt: +|image5| -.. _plottypebar-2: +.. _plottype=bar-2: ``plot_type="bar"`` ~~~~~~~~~~~~~~~~~~~ @@ -222,8 +221,7 @@ vertical bar. ga_instance.plot_new_solution_rate(plot_type="bar") -.. image:: https://user-images.githubusercontent.com/16560492/122476173-c2c89700-cf93-11eb-9e77-d39737cd3a96.png - :alt: +|image6| Genes ===== @@ -307,13 +305,13 @@ solutions in the population or from just the best solutions. An exception is raised if: -- ``solutions="all"`` while ``save_solutions=False`` in the constructor - of the ``pygad.GA`` class. . +- ``solutions="all"`` while ``save_solutions=False`` in the constructor + of the ``pygad.GA`` class. . -- ``solutions="best"`` while ``save_best_solutions=False`` in the - constructor of the ``pygad.GA`` class. . +- ``solutions="best"`` while ``save_best_solutions=False`` in the + constructor of the ``pygad.GA`` class. . -.. _graphtypeplot: +.. _graphtype=plot: ``graph_type="plot"`` ~~~~~~~~~~~~~~~~~~~~~ @@ -322,7 +320,7 @@ When ``graph_type="plot"``, then the figure creates a normal graph where the relationship between the gene values and the generation numbers is represented as a continuous plot, scattered points, or bars. -.. _plottypeplot-3: +.. _plottype=plot-3: ``plot_type="plot"`` ^^^^^^^^^^^^^^^^^^^^ @@ -345,8 +343,7 @@ of the next graph) lasted for 83 generations. ga_instance.plot_genes(graph_type="plot", plot_type="plot") -.. image:: https://user-images.githubusercontent.com/16560492/122477158-4a62d580-cf95-11eb-8c93-9b6e74cb814c.png - :alt: +|image7| As the default value for the ``solutions`` parameter is ``"all"``, then the following method calls generate the same plot. @@ -365,7 +362,7 @@ the following method calls generate the same plot. plot_type="plot", solutions="all") -.. _plottypescatter-3: +.. _plottype=scatter-3: ``plot_type="scatter"`` ^^^^^^^^^^^^^^^^^^^^^^^ @@ -381,10 +378,9 @@ scatter plot. plot_type="scatter", solutions='all') -.. image:: https://user-images.githubusercontent.com/16560492/122477273-73836600-cf95-11eb-828f-f357c7b0f815.png - :alt: +|image8| -.. _plottypebar-3: +.. _plottype=bar-3: ``plot_type="bar"`` ^^^^^^^^^^^^^^^^^^^ @@ -397,10 +393,9 @@ scatter plot. plot_type="bar", solutions='all') -.. image:: https://user-images.githubusercontent.com/16560492/122477370-99106f80-cf95-11eb-8643-865b55e6b844.png - :alt: +|image9| -.. _graphtypeboxplot: +.. _graphtype=boxplot: ``graph_type="boxplot"`` ~~~~~~~~~~~~~~~~~~~~~~~~ @@ -419,10 +414,9 @@ figure as the default value for the ``solutions`` parameter is ga_instance.plot_genes(graph_type="boxplot", solutions='all') -.. image:: https://user-images.githubusercontent.com/16560492/122479260-beeb4380-cf98-11eb-8f08-23707929b12c.png - :alt: +|image10| -.. _graphtypehistogram: +.. _graphtype=histogram: ``graph_type="histogram"`` ~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -442,8 +436,74 @@ figure as the default value for the ``solutions`` parameter is ga_instance.plot_genes(graph_type="histogram", solutions='all') -.. image:: https://user-images.githubusercontent.com/16560492/122477314-8007be80-cf95-11eb-9c95-da3f49204151.png - :alt: +|image11| All the previous figures can be created for only the best solutions by setting ``solutions="best"``. + +Pareto Front +============ + +.. _plotparetofrontcurve: + +``plot_pareto_front_curve()`` +----------------------------- + +The ``plot_pareto_front_curve()`` method creates the Pareto front curve +for multi-objective optimization problems. It creates, shows, and +returns a figure that shows the Pareto front curve and points +representing the fitness. It only works when 2 objectives are used. + +It works only after completing at least 1 generation. If no generation +is completed (at least 1), an exception is raised. + +This method accepts the following parameters: + +1. ``title``: Title of the figure. + +2. ``xlabel``: X-axis label. + +3. ``ylabel``: Y-axis label. + +4. ``linewidth``: Line width of the plot. Defaults to ``3``. + +5. ``font_size``: Font size for the labels and title. Defaults to + ``14``. + +6. ``label``: The label used for the legend. + +7. ``color``: Color of the plot which defaults to the royal blue color + ``#FF6347``. + +8. ``color_fitness``: Color of the fitness points which defaults to the + tomato red color ``#4169E1``. + +9. ``grid``: Either ``True`` or ``False`` to control the visibility of + the grid. + +10. ``alpha``: The transparency of the pareto front curve. + +11. ``marker``: The marker of the fitness points. + +12. ``save_dir``: Directory to save the figure. + +This is an example of calling the ``plot_pareto_front_curve()`` method. + +.. code:: python + + ga_instance.plot_pareto_front_curve() + +|image12| + +.. |image1| image:: https://user-images.githubusercontent.com/16560492/122472609-d02f5280-cf8e-11eb-88a7-f9366ff6e7c6.png +.. |image2| image:: https://user-images.githubusercontent.com/16560492/122473159-75e2c180-cf8f-11eb-942d-31279b286dbd.png +.. |image3| image:: https://user-images.githubusercontent.com/16560492/122473340-b7736c80-cf8f-11eb-89c5-4f7db3b653cc.png +.. |image4| image:: https://user-images.githubusercontent.com/16560492/122475815-3322e880-cf93-11eb-9648-bf66f823234b.png +.. |image5| image:: https://user-images.githubusercontent.com/16560492/122476108-adec0380-cf93-11eb-80ac-7588bf90492f.png +.. |image6| image:: https://user-images.githubusercontent.com/16560492/122476173-c2c89700-cf93-11eb-9e77-d39737cd3a96.png +.. |image7| image:: https://user-images.githubusercontent.com/16560492/122477158-4a62d580-cf95-11eb-8c93-9b6e74cb814c.png +.. |image8| image:: https://user-images.githubusercontent.com/16560492/122477273-73836600-cf95-11eb-828f-f357c7b0f815.png +.. |image9| image:: https://user-images.githubusercontent.com/16560492/122477370-99106f80-cf95-11eb-8643-865b55e6b844.png +.. |image10| image:: https://user-images.githubusercontent.com/16560492/122479260-beeb4380-cf98-11eb-8f08-23707929b12c.png +.. |image11| image:: https://user-images.githubusercontent.com/16560492/122477314-8007be80-cf95-11eb-9c95-da3f49204151.png +.. |image12| image:: https://github.com/user-attachments/assets/606d853c-7370-41a0-8ddb-857a4c6c7fb9 From 58a2ecbcb31a9a4dddaa2a2687c790967e1b0293 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Tue, 7 Jan 2025 13:11:11 -0500 Subject: [PATCH 26/31] Remove the delay_after_gen parameter --- pygad/pygad.py | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/pygad/pygad.py b/pygad/pygad.py index beb0a4d..bedca73 100644 --- a/pygad/pygad.py +++ b/pygad/pygad.py @@ -1,7 +1,6 @@ import numpy import random import cloudpickle -import time import warnings import concurrent.futures import inspect @@ -58,7 +57,6 @@ def __init__(self, on_mutation=None, on_generation=None, on_stop=None, - delay_after_gen=0.0, save_best_solutions=False, save_solutions=False, suppress_warnings=False, @@ -114,8 +112,6 @@ def __init__(self, on_generation: Accepts a function/method to be called after each generation. If function, then it must accept a single parameter representing the instance of the genetic algorithm. If the function returned "stop", then the run() method stops without completing the other generations. If method, then it must accept 2 parameters where the second one refers to the method's object. Added in PyGAD 2.6.0. on_stop: Accepts a function/method to be called only once exactly before the genetic algorithm stops or when it completes all the generations. If function, then it must accept 2 parameters: the first one represents the instance of the genetic algorithm and the second one is a list of fitness values of the last population's solutions. If method, then it must accept 3 parameters where the third one refers to the method's object. Added in PyGAD 2.6.0. - delay_after_gen: Added in PyGAD 2.4.0 and deprecated in PyGAD 3.3.0. It accepts a non-negative number specifying the number of seconds to wait after a generation completes and before going to the next generation. It defaults to 0.0 which means no delay after the generation. - save_best_solutions: Added in PyGAD 2.9.0 and its type is bool. If True, then the best solution in each generation is saved into the 'best_solutions' attribute. Use this parameter with caution as it may cause memory overflow when either the number of generations or the number of genes is large. save_solutions: Added in PyGAD 2.15.0 and its type is bool. If True, then all solutions in each generation are saved into the 'solutions' attribute. Use this parameter with caution as it may cause memory overflow when either the number of generations, number of genes, or number of solutions in population is large. @@ -1133,19 +1129,6 @@ def __init__(self, else: self.on_stop = None - # Validate delay_after_gen - if type(delay_after_gen) in GA.supported_int_float_types: - if not self.suppress_warnings: - warnings.warn("The 'delay_after_gen' parameter is deprecated starting from PyGAD 3.3.0. To delay or pause the evolution after each generation, assign a callback function/method to the 'on_generation' parameter to adds some time delay.") - if delay_after_gen >= 0.0: - self.delay_after_gen = delay_after_gen - else: - self.valid_parameters = False - raise ValueError(f"The value passed to the 'delay_after_gen' parameter must be a non-negative number. The value passed is ({delay_after_gen}) of type {type(delay_after_gen)}.") - else: - self.valid_parameters = False - raise TypeError(f"The value passed to the 'delay_after_gen' parameter must be of type int or float but {type(delay_after_gen)} found.") - # Validate save_best_solutions if type(save_best_solutions) is bool: if save_best_solutions == True: @@ -2064,8 +2047,6 @@ def run(self): if stop_run: break - time.sleep(self.delay_after_gen) - # Save the fitness of the last generation. if self.save_solutions: # self.solutions.extend(self.population.copy()) @@ -2535,11 +2516,6 @@ def print_params_summary(): if not print_step_parameters: print_mutation_params() - if self.delay_after_gen != 0: - m = f"Post-Generation Delay: {self.delay_after_gen}" - self.logger.info(m) - summary_output = summary_output + m + "\n" - if not print_step_parameters: print_on_generation_params() From 13e5713ad796060917cd85a2452337f26d8b502d Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Tue, 7 Jan 2025 16:53:55 -0500 Subject: [PATCH 27/31] Update previous_generation_fitness once GA completes --- pygad/pygad.py | 82 +++++++++++++++++------------- pygad/visualize/plot.py | 107 +++++++++++++++++++++++----------------- 2 files changed, 108 insertions(+), 81 deletions(-) diff --git a/pygad/pygad.py b/pygad/pygad.py index bedca73..436237b 100644 --- a/pygad/pygad.py +++ b/pygad/pygad.py @@ -1211,7 +1211,7 @@ def __init__(self, self.valid_parameters = False raise ValueError(f"In the 'stop_criteria' parameter, the supported stop words are {self.supported_stop_words} but '{stop_word}' found.") - if number.replace(".", "").isnumeric(): + if number.replace(".", "").replace("-", "").isnumeric(): number = float(number) else: self.valid_parameters = False @@ -1306,6 +1306,8 @@ def __init__(self, # A list holding the offspring after applying mutation in the last generation. self.last_generation_offspring_mutation = None # Holds the fitness values of one generation before the fitness values saved in the last_generation_fitness attribute. Added in PyGAD 2.16.2. + # They are used inside the cal_pop_fitness() method to fetch the fitness of the parents in one generation before the latest generation. + # This is to avoid re-calculating the fitness for such parents again. self.previous_generation_fitness = None # Added in PyGAD 2.18.0. A NumPy array holding the elitism of the current generation according to the value passed in the 'keep_elitism' parameter. It works only if the 'keep_elitism' parameter has a non-zero value. self.last_generation_elitism = None @@ -1640,14 +1642,12 @@ def cal_pop_fitness(self): # 'last_generation_parents_as_list' is the list version of 'self.last_generation_parents' # It is used to return the parent index using the 'in' membership operator of Python lists. This is much faster than using 'numpy.where()'. if self.last_generation_parents is not None: - last_generation_parents_as_list = [ - list(gen_parent) for gen_parent in self.last_generation_parents] + last_generation_parents_as_list = self.last_generation_parents.tolist() # 'last_generation_elitism_as_list' is the list version of 'self.last_generation_elitism' # It is used to return the elitism index using the 'in' membership operator of Python lists. This is much faster than using 'numpy.where()'. if self.last_generation_elitism is not None: - last_generation_elitism_as_list = [ - list(gen_elitism) for gen_elitism in self.last_generation_elitism] + last_generation_elitism_as_list = self.last_generation_elitism.tolist() pop_fitness = ["undefined"] * len(self.population) if self.parallel_processing is None: @@ -1659,6 +1659,12 @@ def cal_pop_fitness(self): # Make sure that both the solution and 'self.solutions' are of type 'list' not 'numpy.ndarray'. # if (self.save_solutions) and (len(self.solutions) > 0) and (numpy.any(numpy.all(self.solutions == numpy.array(sol), axis=1))) # if (self.save_solutions) and (len(self.solutions) > 0) and (numpy.any(numpy.all(numpy.equal(self.solutions, numpy.array(sol)), axis=1))) + + # Make sure self.best_solutions is a list of lists before proceeding. + # Because the second condition expects that best_solutions is a list of lists. + if type(self.best_solutions) is numpy.ndarray: + self.best_solutions = self.best_solutions.tolist() + if (self.save_solutions) and (len(self.solutions) > 0) and (list(sol) in self.solutions): solution_idx = self.solutions.index(list(sol)) fitness = self.solutions_fitness[solution_idx] @@ -1867,13 +1873,13 @@ def run(self): # self.best_solutions: Holds the best solution in each generation. if type(self.best_solutions) is numpy.ndarray: - self.best_solutions = list(self.best_solutions) + self.best_solutions = self.best_solutions.tolist() # self.best_solutions_fitness: A list holding the fitness value of the best solution for each generation. if type(self.best_solutions_fitness) is numpy.ndarray: self.best_solutions_fitness = list(self.best_solutions_fitness) # self.solutions: Holds the solutions in each generation. if type(self.solutions) is numpy.ndarray: - self.solutions = list(self.solutions) + self.solutions = self.solutions.tolist() # self.solutions_fitness: Holds the fitness of the solutions in each generation. if type(self.solutions_fitness) is numpy.ndarray: self.solutions_fitness = list(self.solutions_fitness) @@ -1913,34 +1919,8 @@ def run(self): self.best_solutions.append(list(best_solution)) for generation in range(generation_first_idx, generation_last_idx): - if not (self.on_fitness is None): - on_fitness_output = self.on_fitness(self, - self.last_generation_fitness) - if on_fitness_output is None: - pass - else: - if type(on_fitness_output) in [tuple, list, numpy.ndarray, range]: - on_fitness_output = numpy.array(on_fitness_output) - if on_fitness_output.shape == self.last_generation_fitness.shape: - self.last_generation_fitness = on_fitness_output - else: - raise ValueError(f"Size mismatch between the output of on_fitness() {on_fitness_output.shape} and the expected fitness output {self.last_generation_fitness.shape}.") - else: - raise ValueError(f"The output of on_fitness() is expected to be tuple/list/range/numpy.ndarray but {type(on_fitness_output)} found.") - - # Appending the fitness value of the best solution in the current generation to the best_solutions_fitness attribute. - self.best_solutions_fitness.append(best_solution_fitness) - - # Appending the solutions in the current generation to the solutions list. - if self.save_solutions: - # self.solutions.extend(self.population.copy()) - population_as_list = self.population.copy() - population_as_list = [list(item) - for item in population_as_list] - self.solutions.extend(population_as_list) - - self.solutions_fitness.extend(self.last_generation_fitness) + self.run_loop_head(best_solution_fitness) # Call the 'run_select_parents()' method to select the parents. # It edits these 2 instance attributes: @@ -1964,7 +1944,6 @@ def run(self): # 1) population: A NumPy array of the population of solutions/chromosomes. self.run_update_population() - # The generations_completed attribute holds the number of the last completed generation. self.generations_completed = generation + 1 @@ -2078,6 +2057,9 @@ def run(self): # Converting the 'best_solutions' list into a NumPy array. self.best_solutions = numpy.array(self.best_solutions) + # Update previous_generation_fitness because it is used to get the fitness of the parents. + self.previous_generation_fitness = self.last_generation_fitness.copy() + # Converting the 'solutions' list into a NumPy array. # self.solutions = numpy.array(self.solutions) except Exception as ex: @@ -2085,6 +2067,35 @@ def run(self): # sys.exit(-1) raise ex + def run_loop_head(self, best_solution_fitness): + if not (self.on_fitness is None): + on_fitness_output = self.on_fitness(self, + self.last_generation_fitness) + + if on_fitness_output is None: + pass + else: + if type(on_fitness_output) in [tuple, list, numpy.ndarray, range]: + on_fitness_output = numpy.array(on_fitness_output) + if on_fitness_output.shape == self.last_generation_fitness.shape: + self.last_generation_fitness = on_fitness_output + else: + raise ValueError(f"Size mismatch between the output of on_fitness() {on_fitness_output.shape} and the expected fitness output {self.last_generation_fitness.shape}.") + else: + raise ValueError(f"The output of on_fitness() is expected to be tuple/list/range/numpy.ndarray but {type(on_fitness_output)} found.") + + # Appending the fitness value of the best solution in the current generation to the best_solutions_fitness attribute. + self.best_solutions_fitness.append(best_solution_fitness) + + # Appending the solutions in the current generation to the solutions list. + if self.save_solutions: + # self.solutions.extend(self.population.copy()) + population_as_list = self.population.copy() + population_as_list = [list(item) for item in population_as_list] + self.solutions.extend(population_as_list) + + self.solutions_fitness.extend(self.last_generation_fitness) + def run_select_parents(self, call_on_parents=True): """ This method must be only callled from inside the run() method. It is not meant for use by the user. @@ -2333,6 +2344,7 @@ def best_solution(self, pop_fitness=None): -best_solution_fitness: Fitness value of the best solution. -best_match_idx: Index of the best solution in the current population. """ + try: if pop_fitness is None: # If the 'pop_fitness' parameter is not passed, then we have to call the 'cal_pop_fitness()' method to calculate the fitness of all solutions in the lastest population. diff --git a/pygad/visualize/plot.py b/pygad/visualize/plot.py index 3ddeaff..3265de3 100644 --- a/pygad/visualize/plot.py +++ b/pygad/visualize/plot.py @@ -3,9 +3,18 @@ """ import numpy -import matplotlib.pyplot +# import matplotlib.pyplot import pygad +def get_matplotlib(): + # Importing matplotlib.pyplot at the module scope causes performance issues. + # This causes matplotlib.pyplot to be imported once pygad is imported. + # An efficient approach is to import matplotlib.pyplot only when needed. + # Inside each function, call get_matplotlib() to return the library object. + # If a function called get_matplotlib() once, then the library object is reused. + import matplotlib.pyplot as matplt + return matplt + class Plot: def __init__(): @@ -43,7 +52,9 @@ def plot_fitness(self, self.logger.error("The plot_fitness() (i.e. plot_result()) method can only be called after completing at least 1 generation but ({self.generations_completed}) is completed.") raise RuntimeError("The plot_fitness() (i.e. plot_result()) method can only be called after completing at least 1 generation but ({self.generations_completed}) is completed.") - fig = matplotlib.pyplot.figure() + matplt = get_matplotlib() + + fig = matplt.figure() if type(self.best_solutions_fitness[0]) in [list, tuple, numpy.ndarray] and len(self.best_solutions_fitness[0]) > 1: # Multi-objective optimization problem. if type(linewidth) in pygad.GA.supported_int_float_types: @@ -70,18 +81,18 @@ def plot_fitness(self, # Return the fitness values for the current objective function across all best solutions acorss all generations. fitness = numpy.array(self.best_solutions_fitness)[:, objective_idx] if plot_type == "plot": - matplotlib.pyplot.plot(fitness, + matplt.plot(fitness, linewidth=current_linewidth, color=current_color, label=current_label) elif plot_type == "scatter": - matplotlib.pyplot.scatter(range(len(fitness)), + matplt.scatter(range(len(fitness)), fitness, linewidth=current_linewidth, color=current_color, label=current_label) elif plot_type == "bar": - matplotlib.pyplot.bar(range(len(fitness)), + matplt.bar(range(len(fitness)), fitness, linewidth=current_linewidth, color=current_color, @@ -89,29 +100,29 @@ def plot_fitness(self, else: # Single-objective optimization problem. if plot_type == "plot": - matplotlib.pyplot.plot(self.best_solutions_fitness, + matplt.plot(self.best_solutions_fitness, linewidth=linewidth, color=color) elif plot_type == "scatter": - matplotlib.pyplot.scatter(range(len(self.best_solutions_fitness)), + matplt.scatter(range(len(self.best_solutions_fitness)), self.best_solutions_fitness, linewidth=linewidth, color=color) elif plot_type == "bar": - matplotlib.pyplot.bar(range(len(self.best_solutions_fitness)), + matplt.bar(range(len(self.best_solutions_fitness)), self.best_solutions_fitness, linewidth=linewidth, color=color) - matplotlib.pyplot.title(title, fontsize=font_size) - matplotlib.pyplot.xlabel(xlabel, fontsize=font_size) - matplotlib.pyplot.ylabel(ylabel, fontsize=font_size) + matplt.title(title, fontsize=font_size) + matplt.xlabel(xlabel, fontsize=font_size) + matplt.ylabel(ylabel, fontsize=font_size) # Create a legend out of the labels. - matplotlib.pyplot.legend() + matplt.legend() if not save_dir is None: - matplotlib.pyplot.savefig(fname=save_dir, + matplt.savefig(fname=save_dir, bbox_inches='tight') - matplotlib.pyplot.show() + matplt.show() return fig @@ -166,21 +177,23 @@ def plot_new_solution_rate(self, generation_num_unique_solutions = len_after - len_before num_unique_solutions_per_generation.append(generation_num_unique_solutions) - fig = matplotlib.pyplot.figure() + matplt = get_matplotlib() + + fig = matplt.figure() if plot_type == "plot": - matplotlib.pyplot.plot(num_unique_solutions_per_generation, linewidth=linewidth, color=color) + matplt.plot(num_unique_solutions_per_generation, linewidth=linewidth, color=color) elif plot_type == "scatter": - matplotlib.pyplot.scatter(range(self.generations_completed), num_unique_solutions_per_generation, linewidth=linewidth, color=color) + matplt.scatter(range(self.generations_completed), num_unique_solutions_per_generation, linewidth=linewidth, color=color) elif plot_type == "bar": - matplotlib.pyplot.bar(range(self.generations_completed), num_unique_solutions_per_generation, linewidth=linewidth, color=color) - matplotlib.pyplot.title(title, fontsize=font_size) - matplotlib.pyplot.xlabel(xlabel, fontsize=font_size) - matplotlib.pyplot.ylabel(ylabel, fontsize=font_size) + matplt.bar(range(self.generations_completed), num_unique_solutions_per_generation, linewidth=linewidth, color=color) + matplt.title(title, fontsize=font_size) + matplt.xlabel(xlabel, fontsize=font_size) + matplt.ylabel(ylabel, fontsize=font_size) if not save_dir is None: - matplotlib.pyplot.savefig(fname=save_dir, + matplt.savefig(fname=save_dir, bbox_inches='tight') - matplotlib.pyplot.show() + matplt.show() return fig @@ -251,7 +264,7 @@ def plot_genes(self, if num_cols == 0: figsize = (10, 8) # There is only a single gene - fig, ax = matplotlib.pyplot.subplots(num_rows, figsize=figsize) + fig, ax = matplt.subplots(num_rows, figsize=figsize) if plot_type == "plot": ax.plot(solutions_to_plot[:, 0], linewidth=linewidth, color=fill_color) elif plot_type == "scatter": @@ -260,7 +273,7 @@ def plot_genes(self, ax.bar(range(self.generations_completed + 1), solutions_to_plot[:, 0], linewidth=linewidth, color=fill_color) ax.set_xlabel(0, fontsize=font_size) else: - fig, axs = matplotlib.pyplot.subplots(num_rows, num_cols) + fig, axs = matplt.subplots(num_rows, num_cols) if num_cols == 1 and num_rows == 1: fig.set_figwidth(5 * num_cols) @@ -297,10 +310,10 @@ def plot_genes(self, gene_idx += 1 fig.suptitle(title, fontsize=font_size, y=1.001) - matplotlib.pyplot.tight_layout() + matplt.tight_layout() elif graph_type == "boxplot": - fig = matplotlib.pyplot.figure(1, figsize=(0.7*self.num_genes, 6)) + fig = matplt.figure(1, figsize=(0.7*self.num_genes, 6)) # Create an axes instance ax = fig.add_subplot(111) @@ -323,10 +336,10 @@ def plot_genes(self, for cap in boxeplots['caps']: cap.set(color=color, linewidth=linewidth) - matplotlib.pyplot.title(title, fontsize=font_size) - matplotlib.pyplot.xlabel(xlabel, fontsize=font_size) - matplotlib.pyplot.ylabel(ylabel, fontsize=font_size) - matplotlib.pyplot.tight_layout() + matplt.title(title, fontsize=font_size) + matplt.xlabel(xlabel, fontsize=font_size) + matplt.ylabel(ylabel, fontsize=font_size) + matplt.tight_layout() elif graph_type == "histogram": # num_rows will always be >= 1 @@ -337,12 +350,12 @@ def plot_genes(self, if num_cols == 0: figsize = (10, 8) # There is only a single gene - fig, ax = matplotlib.pyplot.subplots(num_rows, + fig, ax = matplt.subplots(num_rows, figsize=figsize) ax.hist(solutions_to_plot[:, 0], color=fill_color) ax.set_xlabel(0, fontsize=font_size) else: - fig, axs = matplotlib.pyplot.subplots(num_rows, num_cols) + fig, axs = matplt.subplots(num_rows, num_cols) if num_cols == 1 and num_rows == 1: fig.set_figwidth(4 * num_cols) @@ -375,13 +388,13 @@ def plot_genes(self, gene_idx += 1 fig.suptitle(title, fontsize=font_size, y=1.001) - matplotlib.pyplot.tight_layout() + matplt.tight_layout() if not save_dir is None: - matplotlib.pyplot.savefig(fname=save_dir, + matplt.savefig(fname=save_dir, bbox_inches='tight') - matplotlib.pyplot.show() + matplt.show() return fig @@ -449,12 +462,14 @@ def plot_pareto_front_curve(self, # Sort the Pareto front solutions (optional but can make the plot cleaner) sorted_pareto_front = sorted(zip(pareto_front_x, pareto_front_y)) + matplt = get_matplotlib() + # Plotting - fig = matplotlib.pyplot.figure() + fig = matplt.figure() # First, plot the scatter of all points (population) all_points_x = [self.last_generation_fitness[i][0] for i in range(self.sol_per_pop)] all_points_y = [self.last_generation_fitness[i][1] for i in range(self.sol_per_pop)] - matplotlib.pyplot.scatter(all_points_x, + matplt.scatter(all_points_x, all_points_y, marker=marker, color=color_fitness, @@ -463,7 +478,7 @@ def plot_pareto_front_curve(self, # Then, plot the Pareto front as a curve pareto_front_x_sorted, pareto_front_y_sorted = zip(*sorted_pareto_front) - matplotlib.pyplot.plot(pareto_front_x_sorted, + matplt.plot(pareto_front_x_sorted, pareto_front_y_sorted, marker=marker, label=label, @@ -471,17 +486,17 @@ def plot_pareto_front_curve(self, color=color, linewidth=linewidth) - matplotlib.pyplot.title(title, fontsize=font_size) - matplotlib.pyplot.xlabel(xlabel, fontsize=font_size) - matplotlib.pyplot.ylabel(ylabel, fontsize=font_size) - matplotlib.pyplot.legend() + matplt.title(title, fontsize=font_size) + matplt.xlabel(xlabel, fontsize=font_size) + matplt.ylabel(ylabel, fontsize=font_size) + matplt.legend() - matplotlib.pyplot.grid(grid) + matplt.grid(grid) if not save_dir is None: - matplotlib.pyplot.savefig(fname=save_dir, + matplt.savefig(fname=save_dir, bbox_inches='tight') - matplotlib.pyplot.show() + matplt.show() return fig From 39841bacfd82a12aae7d5e864c58721a3811bcbe Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Tue, 7 Jan 2025 18:35:24 -0500 Subject: [PATCH 28/31] PyGAD 3.4.0 1. The `delay_after_gen` parameter is removed from the `pygad.GA` class constructor. As a result, it is no longer an attribute of the `pygad.GA` class instances. To add a delay after each generation, apply it inside the `on_generation` callback. https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/283 2. In the `single_point_crossover()` method of the `pygad.utils.crossover.Crossover` class, all the random crossover points are returned before the `for` loop. This is by calling the `numpy.random.randint()` function only once before the loop to generate all the K points (where K is the offspring size). This is compared to calling the `numpy.random.randint()` function inside the `for` loop K times, once for each individual offspring. 3. Bug fix in the `examples/example_custom_operators.py` script. https://github.com/ahmedfgad/GeneticAlgorithmPython/pull/285 4. While making prediction using the `pygad.torchga.predict()` function, no gradients are calculated. 5. The `gene_type` parameter of the `pygad.helper.unique.Unique.unique_int_gene_from_range()` method accepts the type of the current gene only instead of the full gene_type list. 6. Created a new method called `unique_float_gene_from_range()` inside the `pygad.helper.unique.Unique` class to find a unique floating-point number from a range. 7. Fix a bug in the `pygad.helper.unique.Unique.unique_gene_by_space()` method to return the numeric value only instead of a NumPy array. 8. Refactoring the `pygad/helper/unique.py` script to remove duplicate codes and reformatting the docstrings. 9. The plot_pareto_front_curve() method added to the pygad.visualize.plot.Plot class to visualize the Pareto front for multi-objective problems. It only supports 2 objectives. https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/279 10. Fix a bug converting a nested NumPy array to a nested list. https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/300 11. The `Matplotlib` library is only imported when a method inside the `pygad/visualize/plot.py` script is used. This is more efficient than using `import matplotlib.pyplot` at the module level as this causes it to be imported when `pygad` is imported even when it is not needed. https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/292 12. Fix a bug when minus sign (-) is used inside the `stop_criteria` parameter (e.g. `stop_criteria=["saturate_10", "reach_-0.5"]`). https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/296 13. Make sure `self.best_solutions` is a list of lists inside the `cal_pop_fitness` method. https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/293 14. Fix a bug where the `cal_pop_fitness()` method was using the `previous_generation_fitness` attribute to return the parents fitness. This instance attribute was not using the fitness of the latest population, instead the fitness of the population before the last one. The issue is solved by updating the `previous_generation_fitness` attribute to the latest population fitness before the GA completes. https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/291 --- docs/source/conf.py | 4 +- docs/source/pygad.rst | 1219 +++++++++++++-------------- docs/source/releases.rst | 323 ++++--- examples/example_multi_objective.py | 1 + pyproject.toml | 2 +- setup.py | 2 +- 6 files changed, 808 insertions(+), 743 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index baef4b6..3ae40c9 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -18,11 +18,11 @@ # -- Project information ----------------------------------------------------- project = 'PyGAD' -copyright = '2024, Ahmed Fawzy Gad' +copyright = '2025, Ahmed Fawzy Gad' author = 'Ahmed Fawzy Gad' # The full version, including alpha/beta/rc tags -release = '3.3.1' +release = '3.4.0' master_doc = 'index' diff --git a/docs/source/pygad.rst b/docs/source/pygad.rst index 9c4179e..df84336 100644 --- a/docs/source/pygad.rst +++ b/docs/source/pygad.rst @@ -29,401 +29,391 @@ algorithm to different types of applications. The ``pygad.GA`` class constructor supports the following parameters: -- ``num_generations``: Number of generations. - -- ``num_parents_mating``: Number of solutions to be selected as - parents. - -- ``fitness_func``: Accepts a function/method and returns the fitness - value(s) of the solution. If a function is passed, then it must - accept 3 parameters (1. the instance of the ``pygad.GA`` class, 2. a - single solution, and 3. its index in the population). If method, then - it accepts a fourth parameter representing the method's class - instance. Check the `Preparing the fitness_func - Parameter `__ - section for information about creating such a function. In `PyGAD - 3.2.0 `__, - multi-objective optimization is supported. To consider the problem as - multi-objective, just return a ``list``, ``tuple``, or - ``numpy.ndarray`` from the fitness function. - -- ``fitness_batch_size=None``: A new optional parameter called - ``fitness_batch_size`` is supported to calculate the fitness function - in batches. If it is assigned the value ``1`` or ``None`` (default), - then the normal flow is used where the fitness function is called for - each individual solution. If the ``fitness_batch_size`` parameter is - assigned a value satisfying this condition - ``1 < fitness_batch_size <= sol_per_pop``, then the solutions are - grouped into batches of size ``fitness_batch_size`` and the fitness - function is called once for each batch. Check the `Batch Fitness - Calculation `__ - section for more details and examples. Added in from `PyGAD - 2.19.0 `__. - -- ``initial_population``: A user-defined initial population. It is - useful when the user wants to start the generations with a custom - initial population. It defaults to ``None`` which means no initial - population is specified by the user. In this case, - `PyGAD `__ creates an initial - population using the ``sol_per_pop`` and ``num_genes`` parameters. An - exception is raised if the ``initial_population`` is ``None`` while - any of the 2 parameters (``sol_per_pop`` or ``num_genes``) is also - ``None``. Introduced in `PyGAD - 2.0.0 `__ - and higher. - -- ``sol_per_pop``: Number of solutions (i.e. chromosomes) within the - population. This parameter has no action if ``initial_population`` - parameter exists. - -- ``num_genes``: Number of genes in the solution/chromosome. This - parameter is not needed if the user feeds the initial population to - the ``initial_population`` parameter. - -- ``gene_type=float``: Controls the gene type. It can be assigned to a - single data type that is applied to all genes or can specify the data - type of each individual gene. It defaults to ``float`` which means - all genes are of ``float`` data type. Starting from `PyGAD - 2.9.0 `__, - the ``gene_type`` parameter can be assigned to a numeric value of any - of these types: ``int``, ``float``, and - ``numpy.int/uint/float(8-64)``. Starting from `PyGAD - 2.14.0 `__, - it can be assigned to a ``list``, ``tuple``, or a ``numpy.ndarray`` - which hold a data type for each gene (e.g. - ``gene_type=[int, float, numpy.int8]``). This helps to control the - data type of each individual gene. In `PyGAD - 2.15.0 `__, - a precision for the ``float`` data types can be specified (e.g. - ``gene_type=[float, 2]``. - -- ``init_range_low=-4``: The lower value of the random range from which - the gene values in the initial population are selected. - ``init_range_low`` defaults to ``-4``. Available in `PyGAD - 1.0.20 `__ - and higher. This parameter has no action if the - ``initial_population`` parameter exists. - -- ``init_range_high=4``: The upper value of the random range from which - the gene values in the initial population are selected. - ``init_range_high`` defaults to ``+4``. Available in `PyGAD - 1.0.20 `__ - and higher. This parameter has no action if the - ``initial_population`` parameter exists. - -- ``parent_selection_type="sss"``: The parent selection type. Supported - types are ``sss`` (for steady-state selection), ``rws`` (for roulette - wheel selection), ``sus`` (for stochastic universal selection), - ``rank`` (for rank selection), ``random`` (for random selection), and - ``tournament`` (for tournament selection). A custom parent selection - function can be passed starting from `PyGAD - 2.16.0 `__. - Check the `User-Defined Crossover, Mutation, and Parent Selection - Operators `__ - section for more details about building a user-defined parent - selection function. - -- ``keep_parents=-1``: Number of parents to keep in the current - population. ``-1`` (default) means to keep all parents in the next - population. ``0`` means keep no parents in the next population. A - value ``greater than 0`` means keeps the specified number of parents - in the next population. Note that the value assigned to - ``keep_parents`` cannot be ``< - 1`` or greater than the number of - solutions within the population ``sol_per_pop``. Starting from `PyGAD - 2.18.0 `__, - this parameter have an effect only when the ``keep_elitism`` - parameter is ``0``. Starting from `PyGAD - 2.20.0 `__, - the parents' fitness from the last generation will not be re-used if - ``keep_parents=0``. - -- ``keep_elitism=1``: Added in `PyGAD - 2.18.0 `__. - It can take the value ``0`` or a positive integer that satisfies - (``0 <= keep_elitism <= sol_per_pop``). It defaults to ``1`` which - means only the best solution in the current generation is kept in the - next generation. If assigned ``0``, this means it has no effect. If - assigned a positive integer ``K``, then the best ``K`` solutions are - kept in the next generation. It cannot be assigned a value greater - than the value assigned to the ``sol_per_pop`` parameter. If this - parameter has a value different than ``0``, then the ``keep_parents`` - parameter will have no effect. - -- ``K_tournament=3``: In case that the parent selection type is - ``tournament``, the ``K_tournament`` specifies the number of parents - participating in the tournament selection. It defaults to ``3``. - -- ``crossover_type="single_point"``: Type of the crossover operation. - Supported types are ``single_point`` (for single-point crossover), - ``two_points`` (for two points crossover), ``uniform`` (for uniform - crossover), and ``scattered`` (for scattered crossover). Scattered - crossover is supported from PyGAD - `2.9.0 `__ - and higher. It defaults to ``single_point``. A custom crossover - function can be passed starting from `PyGAD - 2.16.0 `__. - Check the `User-Defined Crossover, Mutation, and Parent Selection - Operators `__ - section for more details about creating a user-defined crossover - function. Starting from `PyGAD - 2.2.2 `__ - and higher, if ``crossover_type=None``, then the crossover step is - bypassed which means no crossover is applied and thus no offspring - will be created in the next generations. The next generation will use - the solutions in the current population. - -- ``crossover_probability=None``: The probability of selecting a parent - for applying the crossover operation. Its value must be between 0.0 - and 1.0 inclusive. For each parent, a random value between 0.0 and - 1.0 is generated. If this random value is less than or equal to the - value assigned to the ``crossover_probability`` parameter, then the - parent is selected. Added in `PyGAD - 2.5.0 `__ - and higher. - -- ``mutation_type="random"``: Type of the mutation operation. Supported - types are ``random`` (for random mutation), ``swap`` (for swap - mutation), ``inversion`` (for inversion mutation), ``scramble`` (for - scramble mutation), and ``adaptive`` (for adaptive mutation). It - defaults to ``random``. A custom mutation function can be passed - starting from `PyGAD - 2.16.0 `__. - Check the `User-Defined Crossover, Mutation, and Parent Selection - Operators `__ - section for more details about creating a user-defined mutation - function. Starting from `PyGAD - 2.2.2 `__ - and higher, if ``mutation_type=None``, then the mutation step is - bypassed which means no mutation is applied and thus no changes are - applied to the offspring created using the crossover operation. The - offspring will be used unchanged in the next generation. ``Adaptive`` - mutation is supported starting from `PyGAD - 2.10.0 `__. - For more information about adaptive mutation, go the the `Adaptive - Mutation `__ - section. For example about using adaptive mutation, check the `Use - Adaptive Mutation in - PyGAD `__ - section. - -- ``mutation_probability=None``: The probability of selecting a gene - for applying the mutation operation. Its value must be between 0.0 - and 1.0 inclusive. For each gene in a solution, a random value - between 0.0 and 1.0 is generated. If this random value is less than - or equal to the value assigned to the ``mutation_probability`` - parameter, then the gene is selected. If this parameter exists, then - there is no need for the 2 parameters ``mutation_percent_genes`` and - ``mutation_num_genes``. Added in `PyGAD - 2.5.0 `__ - and higher. - -- ``mutation_by_replacement=False``: An optional bool parameter. It - works only when the selected type of mutation is random - (``mutation_type="random"``). In this case, - ``mutation_by_replacement=True`` means replace the gene by the - randomly generated value. If False, then it has no effect and random - mutation works by adding the random value to the gene. Supported in - `PyGAD - 2.2.2 `__ - and higher. Check the changes in `PyGAD - 2.2.2 `__ - under the Release History section for an example. - -- ``mutation_percent_genes="default"``: Percentage of genes to mutate. - It defaults to the string ``"default"`` which is later translated - into the integer ``10`` which means 10% of the genes will be mutated. - It must be ``>0`` and ``<=100``. Out of this percentage, the number - of genes to mutate is deduced which is assigned to the - ``mutation_num_genes`` parameter. The ``mutation_percent_genes`` - parameter has no action if ``mutation_probability`` or - ``mutation_num_genes`` exist. Starting from `PyGAD - 2.2.2 `__ - and higher, this parameter has no action if ``mutation_type`` is - ``None``. - -- ``mutation_num_genes=None``: Number of genes to mutate which defaults - to ``None`` meaning that no number is specified. The - ``mutation_num_genes`` parameter has no action if the parameter - ``mutation_probability`` exists. Starting from `PyGAD - 2.2.2 `__ - and higher, this parameter has no action if ``mutation_type`` is - ``None``. - -- ``random_mutation_min_val=-1.0``: For ``random`` mutation, the - ``random_mutation_min_val`` parameter specifies the start value of - the range from which a random value is selected to be added to the - gene. It defaults to ``-1``. Starting from `PyGAD - 2.2.2 `__ - and higher, this parameter has no action if ``mutation_type`` is - ``None``. - -- ``random_mutation_max_val=1.0``: For ``random`` mutation, the - ``random_mutation_max_val`` parameter specifies the end value of the - range from which a random value is selected to be added to the gene. - It defaults to ``+1``. Starting from `PyGAD - 2.2.2 `__ - and higher, this parameter has no action if ``mutation_type`` is - ``None``. - -- ``gene_space=None``: It is used to specify the possible values for - each gene in case the user wants to restrict the gene values. It is - useful if the gene space is restricted to a certain range or to - discrete values. It accepts a ``list``, ``range``, or - ``numpy.ndarray``. When all genes have the same global space, specify - their values as a ``list``/``tuple``/``range``/``numpy.ndarray``. For - example, ``gene_space = [0.3, 5.2, -4, 8]`` restricts the gene values - to the 4 specified values. If each gene has its own space, then the - ``gene_space`` parameter can be nested like - ``[[0.4, -5], [0.5, -3.2, 8.2, -9], ...]`` where the first sublist - determines the values for the first gene, the second sublist for the - second gene, and so on. If the nested list/tuple has a ``None`` - value, then the gene's initial value is selected randomly from the - range specified by the 2 parameters ``init_range_low`` and - ``init_range_high`` and its mutation value is selected randomly from - the range specified by the 2 parameters ``random_mutation_min_val`` - and ``random_mutation_max_val``. ``gene_space`` is added in `PyGAD - 2.5.0 `__. - Check the `Release History of PyGAD - 2.5.0 `__ - section of the documentation for more details. In `PyGAD - 2.9.0 `__, - NumPy arrays can be assigned to the ``gene_space`` parameter. In - `PyGAD - 2.11.0 `__, - the ``gene_space`` parameter itself or any of its elements can be - assigned to a dictionary to specify the lower and upper limits of the - genes. For example, ``{'low': 2, 'high': 4}`` means the minimum and - maximum values are 2 and 4, respectively. In `PyGAD - 2.15.0 `__, - a new key called ``"step"`` is supported to specify the step of - moving from the start to the end of the range specified by the 2 - existing keys ``"low"`` and ``"high"``. - -- ``on_start=None``: Accepts a function/method to be called only once - before the genetic algorithm starts its evolution. If function, then - it must accept a single parameter representing the instance of the - genetic algorithm. If method, then it must accept 2 parameters where - the second one refers to the method's object. Added in `PyGAD - 2.6.0 `__. - -- ``on_fitness=None``: Accepts a function/method to be called after - calculating the fitness values of all solutions in the population. If - function, then it must accept 2 parameters: 1) a list of all - solutions' fitness values 2) the instance of the genetic algorithm. - If method, then it must accept 3 parameters where the third one - refers to the method's object. Added in `PyGAD - 2.6.0 `__. - -- ``on_parents=None``: Accepts a function/method to be called after - selecting the parents that mates. If function, then it must accept 2 - parameters: 1) the selected parents 2) the instance of the genetic - algorithm If method, then it must accept 3 parameters where the third - one refers to the method's object. Added in `PyGAD - 2.6.0 `__. - -- ``on_crossover=None``: Accepts a function to be called each time the - crossover operation is applied. This function must accept 2 - parameters: the first one represents the instance of the genetic - algorithm and the second one represents the offspring generated using - crossover. Added in `PyGAD - 2.6.0 `__. - -- ``on_mutation=None``: Accepts a function to be called each time the - mutation operation is applied. This function must accept 2 - parameters: the first one represents the instance of the genetic - algorithm and the second one represents the offspring after applying - the mutation. Added in `PyGAD - 2.6.0 `__. - -- ``on_generation=None``: Accepts a function to be called after each - generation. This function must accept a single parameter representing - the instance of the genetic algorithm. If the function returned the - string ``stop``, then the ``run()`` method stops without completing - the other generations. Added in `PyGAD - 2.6.0 `__. - -- ``on_stop=None``: Accepts a function to be called only once exactly - before the genetic algorithm stops or when it completes all the - generations. This function must accept 2 parameters: the first one - represents the instance of the genetic algorithm and the second one - is a list of fitness values of the last population's solutions. Added - in `PyGAD - 2.6.0 `__. - -- ``delay_after_gen=0.0``: It accepts a non-negative number specifying - the time in seconds to wait after a generation completes and before - going to the next generation. It defaults to ``0.0`` which means no - delay after the generation. Available in `PyGAD - 2.4.0 `__ - and higher. - -- ``save_best_solutions=False``: When ``True``, then the best solution - after each generation is saved into an attribute named - ``best_solutions``. If ``False`` (default), then no solutions are - saved and the ``best_solutions`` attribute will be empty. Supported - in `PyGAD - 2.9.0 `__. - -- ``save_solutions=False``: If ``True``, then all solutions in each - generation are appended into an attribute called ``solutions`` which - is NumPy array. Supported in `PyGAD - 2.15.0 `__. - -- ``suppress_warnings=False``: A bool parameter to control whether the - warning messages are printed or not. It defaults to ``False``. - -- ``allow_duplicate_genes=True``: Added in `PyGAD - 2.13.0 `__. - If ``True``, then a solution/chromosome may have duplicate gene - values. If ``False``, then each gene will have a unique value in its - solution. - -- ``stop_criteria=None``: Some criteria to stop the evolution. Added in - `PyGAD - 2.15.0 `__. - Each criterion is passed as ``str`` which has a stop word. The - current 2 supported words are ``reach`` and ``saturate``. ``reach`` - stops the ``run()`` method if the fitness value is equal to or - greater than a given fitness value. An example for ``reach`` is - ``"reach_40"`` which stops the evolution if the fitness is >= 40. - ``saturate`` means stop the evolution if the fitness saturates for a - given number of consecutive generations. An example for ``saturate`` - is ``"saturate_7"`` which means stop the ``run()`` method if the - fitness does not change for 7 consecutive generations. - -- ``parallel_processing=None``: Added in `PyGAD - 2.17.0 `__. - If ``None`` (Default), this means no parallel processing is applied. - It can accept a list/tuple of 2 elements [1) Can be either - ``'process'`` or ``'thread'`` to indicate whether processes or - threads are used, respectively., 2) The number of processes or - threads to use.]. For example, - ``parallel_processing=['process', 10]`` applies parallel processing - with 10 processes. If a positive integer is assigned, then it is used - as the number of threads. For example, ``parallel_processing=5`` uses - 5 threads which is equivalent to - ``parallel_processing=["thread", 5]``. For more information, check - the `Parallel Processing in - PyGAD `__ - section. - -- ``random_seed=None``: Added in `PyGAD - 2.18.0 `__. - It defines the random seed to be used by the random function - generators (we use random functions in the NumPy and random modules). - This helps to reproduce the same results by setting the same random - seed (e.g. ``random_seed=2``). If given the value ``None``, then it - has no effect. - -- ``logger=None``: Accepts an instance of the ``logging.Logger`` class - to log the outputs. Any message is no longer printed using - ``print()`` but logged. If ``logger=None``, then a logger is created - that uses ``StreamHandler`` to logs the messages to the console. - Added in `PyGAD - 3.0.0 `__. - Check the `Logging - Outputs `__ - for more information. +- ``num_generations``: Number of generations. + +- ``num_parents_mating``: Number of solutions to be selected as parents. + +- ``fitness_func``: Accepts a function/method and returns the fitness + value(s) of the solution. If a function is passed, then it must accept + 3 parameters (1. the instance of the ``pygad.GA`` class, 2. a single + solution, and 3. its index in the population). If method, then it + accepts a fourth parameter representing the method's class instance. + Check the `Preparing the fitness_func + Parameter `__ + section for information about creating such a function. In `PyGAD + 3.2.0 `__, + multi-objective optimization is supported. To consider the problem as + multi-objective, just return a ``list``, ``tuple``, or + ``numpy.ndarray`` from the fitness function. + +- ``fitness_batch_size=None``: A new optional parameter called + ``fitness_batch_size`` is supported to calculate the fitness function + in batches. If it is assigned the value ``1`` or ``None`` (default), + then the normal flow is used where the fitness function is called for + each individual solution. If the ``fitness_batch_size`` parameter is + assigned a value satisfying this condition + ``1 < fitness_batch_size <= sol_per_pop``, then the solutions are + grouped into batches of size ``fitness_batch_size`` and the fitness + function is called once for each batch. Check the `Batch Fitness + Calculation `__ + section for more details and examples. Added in from `PyGAD + 2.19.0 `__. + +- ``initial_population``: A user-defined initial population. It is + useful when the user wants to start the generations with a custom + initial population. It defaults to ``None`` which means no initial + population is specified by the user. In this case, + `PyGAD `__ creates an initial + population using the ``sol_per_pop`` and ``num_genes`` parameters. An + exception is raised if the ``initial_population`` is ``None`` while + any of the 2 parameters (``sol_per_pop`` or ``num_genes``) is also + ``None``. Introduced in `PyGAD + 2.0.0 `__ + and higher. + +- ``sol_per_pop``: Number of solutions (i.e. chromosomes) within the + population. This parameter has no action if ``initial_population`` + parameter exists. + +- ``num_genes``: Number of genes in the solution/chromosome. This + parameter is not needed if the user feeds the initial population to + the ``initial_population`` parameter. + +- ``gene_type=float``: Controls the gene type. It can be assigned to a + single data type that is applied to all genes or can specify the data + type of each individual gene. It defaults to ``float`` which means all + genes are of ``float`` data type. Starting from `PyGAD + 2.9.0 `__, + the ``gene_type`` parameter can be assigned to a numeric value of any + of these types: ``int``, ``float``, and + ``numpy.int/uint/float(8-64)``. Starting from `PyGAD + 2.14.0 `__, + it can be assigned to a ``list``, ``tuple``, or a ``numpy.ndarray`` + which hold a data type for each gene (e.g. + ``gene_type=[int, float, numpy.int8]``). This helps to control the + data type of each individual gene. In `PyGAD + 2.15.0 `__, + a precision for the ``float`` data types can be specified (e.g. + ``gene_type=[float, 2]``. + +- ``init_range_low=-4``: The lower value of the random range from which + the gene values in the initial population are selected. + ``init_range_low`` defaults to ``-4``. Available in `PyGAD + 1.0.20 `__ + and higher. This parameter has no action if the ``initial_population`` + parameter exists. + +- ``init_range_high=4``: The upper value of the random range from which + the gene values in the initial population are selected. + ``init_range_high`` defaults to ``+4``. Available in `PyGAD + 1.0.20 `__ + and higher. This parameter has no action if the ``initial_population`` + parameter exists. + +- ``parent_selection_type="sss"``: The parent selection type. Supported + types are ``sss`` (for steady-state selection), ``rws`` (for roulette + wheel selection), ``sus`` (for stochastic universal selection), + ``rank`` (for rank selection), ``random`` (for random selection), and + ``tournament`` (for tournament selection). A custom parent selection + function can be passed starting from `PyGAD + 2.16.0 `__. + Check the `User-Defined Crossover, Mutation, and Parent Selection + Operators `__ + section for more details about building a user-defined parent + selection function. + +- ``keep_parents=-1``: Number of parents to keep in the current + population. ``-1`` (default) means to keep all parents in the next + population. ``0`` means keep no parents in the next population. A + value ``greater than 0`` means keeps the specified number of parents + in the next population. Note that the value assigned to + ``keep_parents`` cannot be ``< - 1`` or greater than the number of + solutions within the population ``sol_per_pop``. Starting from `PyGAD + 2.18.0 `__, + this parameter have an effect only when the ``keep_elitism`` parameter + is ``0``. Starting from `PyGAD + 2.20.0 `__, + the parents' fitness from the last generation will not be re-used if + ``keep_parents=0``. + +- ``keep_elitism=1``: Added in `PyGAD + 2.18.0 `__. + It can take the value ``0`` or a positive integer that satisfies + (``0 <= keep_elitism <= sol_per_pop``). It defaults to ``1`` which + means only the best solution in the current generation is kept in the + next generation. If assigned ``0``, this means it has no effect. If + assigned a positive integer ``K``, then the best ``K`` solutions are + kept in the next generation. It cannot be assigned a value greater + than the value assigned to the ``sol_per_pop`` parameter. If this + parameter has a value different than ``0``, then the ``keep_parents`` + parameter will have no effect. + +- ``K_tournament=3``: In case that the parent selection type is + ``tournament``, the ``K_tournament`` specifies the number of parents + participating in the tournament selection. It defaults to ``3``. + +- ``crossover_type="single_point"``: Type of the crossover operation. + Supported types are ``single_point`` (for single-point crossover), + ``two_points`` (for two points crossover), ``uniform`` (for uniform + crossover), and ``scattered`` (for scattered crossover). Scattered + crossover is supported from PyGAD + `2.9.0 `__ + and higher. It defaults to ``single_point``. A custom crossover + function can be passed starting from `PyGAD + 2.16.0 `__. + Check the `User-Defined Crossover, Mutation, and Parent Selection + Operators `__ + section for more details about creating a user-defined crossover + function. Starting from `PyGAD + 2.2.2 `__ + and higher, if ``crossover_type=None``, then the crossover step is + bypassed which means no crossover is applied and thus no offspring + will be created in the next generations. The next generation will use + the solutions in the current population. + +- ``crossover_probability=None``: The probability of selecting a parent + for applying the crossover operation. Its value must be between 0.0 + and 1.0 inclusive. For each parent, a random value between 0.0 and 1.0 + is generated. If this random value is less than or equal to the value + assigned to the ``crossover_probability`` parameter, then the parent + is selected. Added in `PyGAD + 2.5.0 `__ + and higher. + +- ``mutation_type="random"``: Type of the mutation operation. Supported + types are ``random`` (for random mutation), ``swap`` (for swap + mutation), ``inversion`` (for inversion mutation), ``scramble`` (for + scramble mutation), and ``adaptive`` (for adaptive mutation). It + defaults to ``random``. A custom mutation function can be passed + starting from `PyGAD + 2.16.0 `__. + Check the `User-Defined Crossover, Mutation, and Parent Selection + Operators `__ + section for more details about creating a user-defined mutation + function. Starting from `PyGAD + 2.2.2 `__ + and higher, if ``mutation_type=None``, then the mutation step is + bypassed which means no mutation is applied and thus no changes are + applied to the offspring created using the crossover operation. The + offspring will be used unchanged in the next generation. ``Adaptive`` + mutation is supported starting from `PyGAD + 2.10.0 `__. + For more information about adaptive mutation, go the the `Adaptive + Mutation `__ + section. For example about using adaptive mutation, check the `Use + Adaptive Mutation in + PyGAD `__ + section. + +- ``mutation_probability=None``: The probability of selecting a gene for + applying the mutation operation. Its value must be between 0.0 and 1.0 + inclusive. For each gene in a solution, a random value between 0.0 and + 1.0 is generated. If this random value is less than or equal to the + value assigned to the ``mutation_probability`` parameter, then the + gene is selected. If this parameter exists, then there is no need for + the 2 parameters ``mutation_percent_genes`` and + ``mutation_num_genes``. Added in `PyGAD + 2.5.0 `__ + and higher. + +- ``mutation_by_replacement=False``: An optional bool parameter. It + works only when the selected type of mutation is random + (``mutation_type="random"``). In this case, + ``mutation_by_replacement=True`` means replace the gene by the + randomly generated value. If False, then it has no effect and random + mutation works by adding the random value to the gene. Supported in + `PyGAD + 2.2.2 `__ + and higher. Check the changes in `PyGAD + 2.2.2 `__ + under the Release History section for an example. + +- ``mutation_percent_genes="default"``: Percentage of genes to mutate. + It defaults to the string ``"default"`` which is later translated into + the integer ``10`` which means 10% of the genes will be mutated. It + must be ``>0`` and ``<=100``. Out of this percentage, the number of + genes to mutate is deduced which is assigned to the + ``mutation_num_genes`` parameter. The ``mutation_percent_genes`` + parameter has no action if ``mutation_probability`` or + ``mutation_num_genes`` exist. Starting from `PyGAD + 2.2.2 `__ + and higher, this parameter has no action if ``mutation_type`` is + ``None``. + +- ``mutation_num_genes=None``: Number of genes to mutate which defaults + to ``None`` meaning that no number is specified. The + ``mutation_num_genes`` parameter has no action if the parameter + ``mutation_probability`` exists. Starting from `PyGAD + 2.2.2 `__ + and higher, this parameter has no action if ``mutation_type`` is + ``None``. + +- ``random_mutation_min_val=-1.0``: For ``random`` mutation, the + ``random_mutation_min_val`` parameter specifies the start value of the + range from which a random value is selected to be added to the gene. + It defaults to ``-1``. Starting from `PyGAD + 2.2.2 `__ + and higher, this parameter has no action if ``mutation_type`` is + ``None``. + +- ``random_mutation_max_val=1.0``: For ``random`` mutation, the + ``random_mutation_max_val`` parameter specifies the end value of the + range from which a random value is selected to be added to the gene. + It defaults to ``+1``. Starting from `PyGAD + 2.2.2 `__ + and higher, this parameter has no action if ``mutation_type`` is + ``None``. + +- ``gene_space=None``: It is used to specify the possible values for + each gene in case the user wants to restrict the gene values. It is + useful if the gene space is restricted to a certain range or to + discrete values. It accepts a ``list``, ``range``, or + ``numpy.ndarray``. When all genes have the same global space, specify + their values as a ``list``/``tuple``/``range``/``numpy.ndarray``. For + example, ``gene_space = [0.3, 5.2, -4, 8]`` restricts the gene values + to the 4 specified values. If each gene has its own space, then the + ``gene_space`` parameter can be nested like + ``[[0.4, -5], [0.5, -3.2, 8.2, -9], ...]`` where the first sublist + determines the values for the first gene, the second sublist for the + second gene, and so on. If the nested list/tuple has a ``None`` value, + then the gene's initial value is selected randomly from the range + specified by the 2 parameters ``init_range_low`` and + ``init_range_high`` and its mutation value is selected randomly from + the range specified by the 2 parameters ``random_mutation_min_val`` + and ``random_mutation_max_val``. ``gene_space`` is added in `PyGAD + 2.5.0 `__. + Check the `Release History of PyGAD + 2.5.0 `__ + section of the documentation for more details. In `PyGAD + 2.9.0 `__, + NumPy arrays can be assigned to the ``gene_space`` parameter. In + `PyGAD + 2.11.0 `__, + the ``gene_space`` parameter itself or any of its elements can be + assigned to a dictionary to specify the lower and upper limits of the + genes. For example, ``{'low': 2, 'high': 4}`` means the minimum and + maximum values are 2 and 4, respectively. In `PyGAD + 2.15.0 `__, + a new key called ``"step"`` is supported to specify the step of moving + from the start to the end of the range specified by the 2 existing + keys ``"low"`` and ``"high"``. + +- ``on_start=None``: Accepts a function/method to be called only once + before the genetic algorithm starts its evolution. If function, then + it must accept a single parameter representing the instance of the + genetic algorithm. If method, then it must accept 2 parameters where + the second one refers to the method's object. Added in `PyGAD + 2.6.0 `__. + +- ``on_fitness=None``: Accepts a function/method to be called after + calculating the fitness values of all solutions in the population. If + function, then it must accept 2 parameters: 1) a list of all + solutions' fitness values 2) the instance of the genetic algorithm. If + method, then it must accept 3 parameters where the third one refers to + the method's object. Added in `PyGAD + 2.6.0 `__. + +- ``on_parents=None``: Accepts a function/method to be called after + selecting the parents that mates. If function, then it must accept 2 + parameters: 1) the selected parents 2) the instance of the genetic + algorithm If method, then it must accept 3 parameters where the third + one refers to the method's object. Added in `PyGAD + 2.6.0 `__. + +- ``on_crossover=None``: Accepts a function to be called each time the + crossover operation is applied. This function must accept 2 + parameters: the first one represents the instance of the genetic + algorithm and the second one represents the offspring generated using + crossover. Added in `PyGAD + 2.6.0 `__. + +- ``on_mutation=None``: Accepts a function to be called each time the + mutation operation is applied. This function must accept 2 parameters: + the first one represents the instance of the genetic algorithm and the + second one represents the offspring after applying the mutation. Added + in `PyGAD + 2.6.0 `__. + +- ``on_generation=None``: Accepts a function to be called after each + generation. This function must accept a single parameter representing + the instance of the genetic algorithm. If the function returned the + string ``stop``, then the ``run()`` method stops without completing + the other generations. Added in `PyGAD + 2.6.0 `__. + +- ``on_stop=None``: Accepts a function to be called only once exactly + before the genetic algorithm stops or when it completes all the + generations. This function must accept 2 parameters: the first one + represents the instance of the genetic algorithm and the second one is + a list of fitness values of the last population's solutions. Added in + `PyGAD + 2.6.0 `__. + +- ``save_best_solutions=False``: When ``True``, then the best solution + after each generation is saved into an attribute named + ``best_solutions``. If ``False`` (default), then no solutions are + saved and the ``best_solutions`` attribute will be empty. Supported in + `PyGAD + 2.9.0 `__. + +- ``save_solutions=False``: If ``True``, then all solutions in each + generation are appended into an attribute called ``solutions`` which + is NumPy array. Supported in `PyGAD + 2.15.0 `__. + +- ``suppress_warnings=False``: A bool parameter to control whether the + warning messages are printed or not. It defaults to ``False``. + +- ``allow_duplicate_genes=True``: Added in `PyGAD + 2.13.0 `__. + If ``True``, then a solution/chromosome may have duplicate gene + values. If ``False``, then each gene will have a unique value in its + solution. + +- ``stop_criteria=None``: Some criteria to stop the evolution. Added in + `PyGAD + 2.15.0 `__. + Each criterion is passed as ``str`` which has a stop word. The current + 2 supported words are ``reach`` and ``saturate``. ``reach`` stops the + ``run()`` method if the fitness value is equal to or greater than a + given fitness value. An example for ``reach`` is ``"reach_40"`` which + stops the evolution if the fitness is >= 40. ``saturate`` means stop + the evolution if the fitness saturates for a given number of + consecutive generations. An example for ``saturate`` is + ``"saturate_7"`` which means stop the ``run()`` method if the fitness + does not change for 7 consecutive generations. + +- ``parallel_processing=None``: Added in `PyGAD + 2.17.0 `__. + If ``None`` (Default), this means no parallel processing is applied. + It can accept a list/tuple of 2 elements [1) Can be either + ``'process'`` or ``'thread'`` to indicate whether processes or threads + are used, respectively., 2) The number of processes or threads to + use.]. For example, ``parallel_processing=['process', 10]`` applies + parallel processing with 10 processes. If a positive integer is + assigned, then it is used as the number of threads. For example, + ``parallel_processing=5`` uses 5 threads which is equivalent to + ``parallel_processing=["thread", 5]``. For more information, check the + `Parallel Processing in + PyGAD `__ + section. + +- ``random_seed=None``: Added in `PyGAD + 2.18.0 `__. + It defines the random seed to be used by the random function + generators (we use random functions in the NumPy and random modules). + This helps to reproduce the same results by setting the same random + seed (e.g. ``random_seed=2``). If given the value ``None``, then it + has no effect. + +- ``logger=None``: Accepts an instance of the ``logging.Logger`` class + to log the outputs. Any message is no longer printed using ``print()`` + but logged. If ``logger=None``, then a logger is created that uses + ``StreamHandler`` to logs the messages to the console. Added in `PyGAD + 3.0.0 `__. + Check the `Logging + Outputs `__ + for more information. The user doesn't have to specify all of such parameters while creating an instance of the GA class. A very important parameter you must care @@ -448,25 +438,25 @@ parameter is not correct, an exception is thrown. Plotting Methods in ``pygad.GA`` Class -------------------------------------- -- ``plot_fitness()``: Shows how the fitness evolves by generation. +- ``plot_fitness()``: Shows how the fitness evolves by generation. -- ``plot_genes()``: Shows how the gene value changes for each - generation. +- ``plot_genes()``: Shows how the gene value changes for each + generation. -- ``plot_new_solution_rate()``: Shows the number of new solutions - explored in each solution. +- ``plot_new_solution_rate()``: Shows the number of new solutions + explored in each solution. Class Attributes ---------------- -- ``supported_int_types``: A list of the supported types for the - integer numbers. +- ``supported_int_types``: A list of the supported types for the integer + numbers. -- ``supported_float_types``: A list of the supported types for the - floating-point numbers. +- ``supported_float_types``: A list of the supported types for the + floating-point numbers. -- ``supported_int_float_types``: A list of the supported types for all - numbers. It just concatenates the previous 2 lists. +- ``supported_int_float_types``: A list of the supported types for all + numbers. It just concatenates the previous 2 lists. .. _other-instance-attributes--methods: @@ -483,92 +473,92 @@ The next 2 subsections list such attributes and methods. Other Attributes ~~~~~~~~~~~~~~~~ -- ``generations_completed``: Holds the number of the last completed - generation. - -- ``population``: A NumPy array holding the initial population. - -- ``valid_parameters``: Set to ``True`` when all the parameters passed - in the ``GA`` class constructor are valid. - -- ``run_completed``: Set to ``True`` only after the ``run()`` method - completes gracefully. - -- ``pop_size``: The population size. - -- ``best_solutions_fitness``: A list holding the fitness values of the - best solutions for all generations. - -- ``best_solution_generation``: The generation number at which the best - fitness value is reached. It is only assigned the generation number - after the ``run()`` method completes. Otherwise, its value is -1. - -- ``best_solutions``: A NumPy array holding the best solution per each - generation. It only exists when the ``save_best_solutions`` parameter - in the ``pygad.GA`` class constructor is set to ``True``. - -- ``last_generation_fitness``: The fitness values of the solutions in - the last generation. `Added in PyGAD - 2.12.0 `__. - -- ``previous_generation_fitness``: At the end of each generation, the - fitness of the most recent population is saved in the - ``last_generation_fitness`` attribute. The fitness of the population - exactly preceding this most recent population is saved in the - ``last_generation_fitness`` attribute. This - ``previous_generation_fitness`` attribute is used to fetch the - pre-calculated fitness instead of calling the fitness function for - already explored solutions. `Added in PyGAD - 2.16.2 `__. - -- ``last_generation_parents``: The parents selected from the last - generation. `Added in PyGAD - 2.12.0 `__. - -- ``last_generation_offspring_crossover``: The offspring generated - after applying the crossover in the last generation. `Added in PyGAD - 2.12.0 `__. - -- ``last_generation_offspring_mutation``: The offspring generated after - applying the mutation in the last generation. `Added in PyGAD - 2.12.0 `__. - -- ``gene_type_single``: A flag that is set to ``True`` if the - ``gene_type`` parameter is assigned to a single data type that is - applied to all genes. If ``gene_type`` is assigned a ``list``, - ``tuple``, or ``numpy.ndarray``, then the value of - ``gene_type_single`` will be ``False``. `Added in PyGAD - 2.14.0 `__. - -- ``last_generation_parents_indices``: This attribute holds the indices - of the selected parents in the last generation. Supported in `PyGAD - 2.15.0 `__. - -- ``last_generation_elitism``: This attribute holds the elitism of the - last generation. It is effective only if the ``keep_elitism`` - parameter has a non-zero value. Supported in `PyGAD - 2.18.0 `__. - -- ``last_generation_elitism_indices``: This attribute holds the indices - of the elitism of the last generation. It is effective only if the - ``keep_elitism`` parameter has a non-zero value. Supported in `PyGAD - 2.19.0 `__. - -- ``logger``: This attribute holds the logger from the ``logging`` - module. Supported in `PyGAD - 3.0.0 `__. - -- ``gene_space_unpacked``: This is the unpacked version of the - ``gene_space`` parameter. For example, ``range(1, 5)`` is unpacked to - ``[1, 2, 3, 4]``. For an infinite range like - ``{'low': 2, 'high': 4}``, then it is unpacked to a limited number of - values (e.g. 100). Supported in `PyGAD - 3.1.0 `__. - -- ``pareto_fronts``: A new instance attribute named ``pareto_fronts`` - added to the ``pygad.GA`` instances that holds the pareto fronts when - solving a multi-objective problem. Supported in `PyGAD - 3.2.0 `__. +- ``generations_completed``: Holds the number of the last completed + generation. + +- ``population``: A NumPy array holding the initial population. + +- ``valid_parameters``: Set to ``True`` when all the parameters passed + in the ``GA`` class constructor are valid. + +- ``run_completed``: Set to ``True`` only after the ``run()`` method + completes gracefully. + +- ``pop_size``: The population size. + +- ``best_solutions_fitness``: A list holding the fitness values of the + best solutions for all generations. + +- ``best_solution_generation``: The generation number at which the best + fitness value is reached. It is only assigned the generation number + after the ``run()`` method completes. Otherwise, its value is -1. + +- ``best_solutions``: A NumPy array holding the best solution per each + generation. It only exists when the ``save_best_solutions`` parameter + in the ``pygad.GA`` class constructor is set to ``True``. + +- ``last_generation_fitness``: The fitness values of the solutions in + the last generation. `Added in PyGAD + 2.12.0 `__. + +- ``previous_generation_fitness``: At the end of each generation, the + fitness of the most recent population is saved in the + ``last_generation_fitness`` attribute. The fitness of the population + exactly preceding this most recent population is saved in the + ``last_generation_fitness`` attribute. This + ``previous_generation_fitness`` attribute is used to fetch the + pre-calculated fitness instead of calling the fitness function for + already explored solutions. `Added in PyGAD + 2.16.2 `__. + +- ``last_generation_parents``: The parents selected from the last + generation. `Added in PyGAD + 2.12.0 `__. + +- ``last_generation_offspring_crossover``: The offspring generated after + applying the crossover in the last generation. `Added in PyGAD + 2.12.0 `__. + +- ``last_generation_offspring_mutation``: The offspring generated after + applying the mutation in the last generation. `Added in PyGAD + 2.12.0 `__. + +- ``gene_type_single``: A flag that is set to ``True`` if the + ``gene_type`` parameter is assigned to a single data type that is + applied to all genes. If ``gene_type`` is assigned a ``list``, + ``tuple``, or ``numpy.ndarray``, then the value of + ``gene_type_single`` will be ``False``. `Added in PyGAD + 2.14.0 `__. + +- ``last_generation_parents_indices``: This attribute holds the indices + of the selected parents in the last generation. Supported in `PyGAD + 2.15.0 `__. + +- ``last_generation_elitism``: This attribute holds the elitism of the + last generation. It is effective only if the ``keep_elitism`` + parameter has a non-zero value. Supported in `PyGAD + 2.18.0 `__. + +- ``last_generation_elitism_indices``: This attribute holds the indices + of the elitism of the last generation. It is effective only if the + ``keep_elitism`` parameter has a non-zero value. Supported in `PyGAD + 2.19.0 `__. + +- ``logger``: This attribute holds the logger from the ``logging`` + module. Supported in `PyGAD + 3.0.0 `__. + +- ``gene_space_unpacked``: This is the unpacked version of the + ``gene_space`` parameter. For example, ``range(1, 5)`` is unpacked to + ``[1, 2, 3, 4]``. For an infinite range like + ``{'low': 2, 'high': 4}``, then it is unpacked to a limited number of + values (e.g. 100). Supported in `PyGAD + 3.1.0 `__. + +- ``pareto_fronts``: A new instance attribute named ``pareto_fronts`` + added to the ``pygad.GA`` instances that holds the pareto fronts when + solving a multi-objective problem. Supported in `PyGAD + 3.2.0 `__. Note that the attributes with names starting with ``last_generation_`` are updated after each generation. @@ -576,55 +566,55 @@ are updated after each generation. Other Methods ~~~~~~~~~~~~~ -- ``cal_pop_fitness()``: A method that calculates the fitness values - for all solutions within the population by calling the function - passed to the ``fitness_func`` parameter for each solution. +- ``cal_pop_fitness()``: A method that calculates the fitness values for + all solutions within the population by calling the function passed to + the ``fitness_func`` parameter for each solution. -- ``crossover()``: Refers to the method that applies the crossover - operator based on the selected type of crossover in the - ``crossover_type`` property. +- ``crossover()``: Refers to the method that applies the crossover + operator based on the selected type of crossover in the + ``crossover_type`` property. -- ``mutation()``: Refers to the method that applies the mutation - operator based on the selected type of mutation in the - ``mutation_type`` property. +- ``mutation()``: Refers to the method that applies the mutation + operator based on the selected type of mutation in the + ``mutation_type`` property. -- ``select_parents()``: Refers to a method that selects the parents - based on the parent selection type specified in the - ``parent_selection_type`` attribute. +- ``select_parents()``: Refers to a method that selects the parents + based on the parent selection type specified in the + ``parent_selection_type`` attribute. -- ``adaptive_mutation_population_fitness()``: Returns the average - fitness value used in the adaptive mutation to filter the solutions. +- ``adaptive_mutation_population_fitness()``: Returns the average + fitness value used in the adaptive mutation to filter the solutions. -- ``summary()``: Prints a Keras-like summary of the PyGAD lifecycle. - This helps to have an overview of the architecture. Supported in - `PyGAD - 2.19.0 `__. - Check the `Print Lifecycle - Summary `__ - section for more details and examples. +- ``summary()``: Prints a Keras-like summary of the PyGAD lifecycle. + This helps to have an overview of the architecture. Supported in + `PyGAD + 2.19.0 `__. + Check the `Print Lifecycle + Summary `__ + section for more details and examples. -- 4 methods with names starting with ``run_``. Their purpose is to keep - the main loop inside the ``run()`` method clean. The details inside - the loop are moved to 4 individual methods. Generally, any method - with a name starting with ``run_`` is meant to be called by PyGAD - from inside the ``run()`` method. Supported in `PyGAD - 3.3.1 `__. +- 4 methods with names starting with ``run_``. Their purpose is to keep + the main loop inside the ``run()`` method clean. The details inside + the loop are moved to 4 individual methods. Generally, any method with + a name starting with ``run_`` is meant to be called by PyGAD from + inside the ``run()`` method. Supported in `PyGAD + 3.3.1 `__. - 1. ``run_select_parents(call_on_parents=True)``: Select the parents - and call the callable ``on_parents()`` if defined. If - ``call_on_parents`` is ``True``, then the callable - ``on_parents()`` is called. It must be ``False`` when the - ``run_select_parents()`` method is called to update the parents at - the end of the ``run()`` method. + 1. ``run_select_parents(call_on_parents=True)``: Select the parents + and call the callable ``on_parents()`` if defined. If + ``call_on_parents`` is ``True``, then the callable ``on_parents()`` + is called. It must be ``False`` when the ``run_select_parents()`` + method is called to update the parents at the end of the ``run()`` + method. - 2. ``run_crossover()``: Apply crossover and call the callable - ``on_crossover()`` if defined. + 2. ``run_crossover()``: Apply crossover and call the callable + ``on_crossover()`` if defined. - 3. ``run_mutation()``: Apply mutation and call the callable - ``on_mutation()`` if defined. + 3. ``run_mutation()``: Apply mutation and call the callable + ``on_mutation()`` if defined. - 4. ``run_update_population()``: Update the ``population`` attribute - after completing the processes of crossover and mutation. + 4. ``run_update_population()``: Update the ``population`` attribute + after completing the processes of crossover and mutation. The next sections discuss the methods available in the ``pygad.GA`` class. @@ -639,13 +629,13 @@ saved in the instance attribute named ``population``. Accepts the following parameters: -- ``low``: The lower value of the random range from which the gene - values in the initial population are selected. It defaults to -4. - Available in PyGAD 1.0.20 and higher. +- ``low``: The lower value of the random range from which the gene + values in the initial population are selected. It defaults to -4. + Available in PyGAD 1.0.20 and higher. -- ``high``: The upper value of the random range from which the gene - values in the initial population are selected. It defaults to -4. - Available in PyGAD 1.0.20. +- ``high``: The upper value of the random range from which the gene + values in the initial population are selected. It defaults to -4. + Available in PyGAD 1.0.20. This method assigns the values of the following 3 instance attributes: @@ -731,20 +721,20 @@ the ``pygad.GA`` class constructor. After the generation completes, the following takes place: -- The ``population`` attribute is updated by the new population. +- The ``population`` attribute is updated by the new population. -- The ``generations_completed`` attribute is assigned by the number of - the last completed generation. +- The ``generations_completed`` attribute is assigned by the number of + the last completed generation. -- If there is a callback function assigned to the ``on_generation`` - attribute, then it will be called. +- If there is a callback function assigned to the ``on_generation`` + attribute, then it will be called. After the ``run()`` method completes, the following takes place: -- The ``best_solution_generation`` is assigned the generation number at - which the best fitness value is reached. +- The ``best_solution_generation`` is assigned the generation number at + which the best fitness value is reached. -- The ``run_completed`` attribute is set to ``True``. +- The ``run_completed`` attribute is set to ``True``. Parent Selection Methods ------------------------ @@ -754,10 +744,10 @@ module has several methods for selecting the parents that will mate to produce the offspring. All of such methods accept the same parameters which are: -- ``fitness``: The fitness values of the solutions in the current - population. +- ``fitness``: The fitness values of the solutions in the current + population. -- ``num_parents``: The number of parents to be selected. +- ``num_parents``: The number of parents to be selected. All of such methods return an array of the selected parents. @@ -831,9 +821,9 @@ The ``Crossover`` class in the ``pygad.utils.crossover`` module supports several methods for applying crossover between the selected parents. All of these methods accept the same parameters which are: -- ``parents``: The parents to mate for producing the offspring. +- ``parents``: The parents to mate for producing the offspring. -- ``offspring_size``: The size of the offspring to produce. +- ``offspring_size``: The size of the offspring to produce. All of such methods return an array of the produced offspring. @@ -878,7 +868,7 @@ The ``Mutation`` class in the ``pygad.utils.mutation`` module supports several methods for applying mutation. All of these methods accept the same parameter which is: -- ``offspring``: The offspring to mutate. +- ``offspring``: The offspring to mutate. All of such methods return an array of the mutated offspring. @@ -942,19 +932,19 @@ algorithm. 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 population. +- ``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 population. It returns the following: -- ``best_solution``: Best solution in the current population. +- ``best_solution``: Best solution in the current population. -- ``best_solution_fitness``: Fitness value of the best solution. +- ``best_solution_fitness``: Fitness value of the best solution. -- ``best_match_idx``: Index of the best solution in the current - population. +- ``best_match_idx``: Index of the best solution in the current + population. .. _plotfitness: @@ -1008,8 +998,8 @@ Saves the genetic algorithm instance Accepts the following parameter: -- ``filename``: Name of the file to save the instance. No extension is - needed. +- ``filename``: Name of the file to save the instance. No extension is + needed. Functions in ``pygad`` ====================== @@ -1029,8 +1019,8 @@ be called by the pygad module as follows: ``pygad.load(filename)``. Accepts the following parameter: -- ``filename``: Name of the file holding the saved instance of the - genetic algorithm. No extension is needed. +- ``filename``: Name of the file holding the saved instance of the + genetic algorithm. No extension is needed. Returns the genetic algorithm instance. @@ -1076,16 +1066,16 @@ solution with a low value. The fitness function is where the user can decide whether the optimization problem is single-objective or multi-objective. -- If the fitness function returns a numeric value, then the problem is - single-objective. The numeric data types supported by PyGAD are - listed in the ``supported_int_float_types`` variable of the - ``pygad.GA`` class. +- If the fitness function returns a numeric value, then the problem is + single-objective. The numeric data types supported by PyGAD are listed + in the ``supported_int_float_types`` variable of the ``pygad.GA`` + class. -- If the fitness function returns a ``list``, ``tuple``, or - ``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. +- If the fitness function returns a ``list``, ``tuple``, or + ``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 @@ -1268,8 +1258,7 @@ generations. ga_instance.plot_fitness() -.. image:: https://user-images.githubusercontent.com/16560492/78830005-93111d00-79e7-11ea-9d8e-a8d8325a6101.png - :alt: +|image1| Information about the Best Solution ----------------------------------- @@ -1277,11 +1266,11 @@ Information about the Best Solution The following information about the best solution in the last population is returned using the ``best_solution()`` method. -- Solution +- Solution -- Fitness value of the solution +- Fitness value of the solution -- Index of the solution within the population +- Index of the solution within the population .. code:: python @@ -1340,8 +1329,7 @@ instance of the ``pygad.GA`` class. Note that PyGAD stops when either all generations are completed or when the function passed to the ``on_generation`` parameter returns the string ``stop``. -.. image:: https://user-images.githubusercontent.com/16560492/220486073-c5b6089d-81e4-44d9-a53c-385f479a7273.jpg - :alt: +|image2| The next code implements all the callback functions to trace the execution of the genetic algorithm. Each callback function prints its @@ -1604,8 +1592,7 @@ This is the figure created by the ``plot_fitness()`` method. The fitness of the first objective has the green color. The blue color is used for the second objective fitness. -.. image:: https://github.com/ahmedfgad/GeneticAlgorithmPython/assets/16560492/7896f8d8-01c5-4ff9-8d15-52191c309b63 - :alt: +|image3| Reproducing Images ------------------ @@ -1620,29 +1607,29 @@ For more information about this project, read this tutorial titled Python `__ available at these links: -- `Heartbeat `__: - https://heartbeat.fritz.ai/reproducing-images-using-a-genetic-algorithm-with-python-91fc701ff84 +- `Heartbeat `__: + https://heartbeat.fritz.ai/reproducing-images-using-a-genetic-algorithm-with-python-91fc701ff84 -- `LinkedIn `__: - https://www.linkedin.com/pulse/reproducing-images-using-genetic-algorithm-python-ahmed-gad +- `LinkedIn `__: + https://www.linkedin.com/pulse/reproducing-images-using-genetic-algorithm-python-ahmed-gad Project Steps ~~~~~~~~~~~~~ The steps to follow in order to reproduce an image are as follows: -- Read an image +- Read an image -- Prepare the fitness function +- Prepare the fitness function -- Create an instance of the pygad.GA class with the appropriate - parameters +- Create an instance of the pygad.GA class with the appropriate + parameters -- Run PyGAD +- Run PyGAD -- Plot results +- Plot results -- Calculate some statistics +- Calculate some statistics The next sections discusses the code of each of these steps. @@ -1663,8 +1650,7 @@ to the next code. Here is the read image. -.. image:: https://user-images.githubusercontent.com/16560492/36948808-f0ac882e-1fe8-11e8-8d07-1307e3477fd0.jpg - :alt: +|image4| Based on the chromosome representation used in the example, the pixel values can be either in the 0-255, 0-1, or any other ranges. @@ -1779,8 +1765,7 @@ generations can be viewed in a plot using the ``plot_fitness()`` method. Here is the plot after 20,000 generations. -.. image:: https://user-images.githubusercontent.com/16560492/82232124-77762c00-992e-11ea-9fc6-14a1cd7a04ff.png - :alt: +|image5| Calculate Some Statistics ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -1807,14 +1792,12 @@ Evolution by Generation The solution reached after the 20,000 generations is shown below. -.. image:: https://user-images.githubusercontent.com/16560492/82232405-e0f63a80-992e-11ea-984f-b6ed76465bd1.png - :alt: +|image6| After more generations, the result can be enhanced like what shown below. -.. image:: https://user-images.githubusercontent.com/16560492/82232345-cf149780-992e-11ea-8390-bf1a57a19de7.png - :alt: +|image7| The results can also be enhanced by changing the parameters passed to the constructor of the ``pygad.GA`` class. @@ -1824,38 +1807,31 @@ Here is how the image is evolved from generation 0 to generation Generation 0 -.. image:: https://user-images.githubusercontent.com/16560492/36948589-b47276f0-1fe5-11e8-8efe-0cd1a225ea3a.png - :alt: +|image8| Generation 1,000 -.. image:: https://user-images.githubusercontent.com/16560492/36948823-16f490ee-1fe9-11e8-97db-3e8905ad5440.png - :alt: +|image9| Generation 2,500 -.. image:: https://user-images.githubusercontent.com/16560492/36948832-3f314b60-1fe9-11e8-8f4a-4d9a53b99f3d.png - :alt: +|image10| Generation 4,500 -.. image:: https://user-images.githubusercontent.com/16560492/36948837-53d1849a-1fe9-11e8-9b36-e9e9291e347b.png - :alt: +|image11| Generation 7,000 -.. image:: https://user-images.githubusercontent.com/16560492/36948852-66f1b176-1fe9-11e8-9f9b-460804e94004.png - :alt: +|image12| Generation 8,000 -.. image:: https://user-images.githubusercontent.com/16560492/36948865-7fbb5158-1fe9-11e8-8c04-8ac3c1f7b1b1.png - :alt: +|image13| Generation 20,000 -.. image:: https://user-images.githubusercontent.com/16560492/82232405-e0f63a80-992e-11ea-984f-b6ed76465bd1.png - :alt: +|image14| Clustering ---------- @@ -1886,3 +1862,18 @@ for how the genetic algorithm plays CoinTex: https://blog.paperspace.com/building-agent-for-cointex-using-genetic-algorithm. Check also this `YouTube video `__ showing the genetic algorithm while playing CoinTex. + +.. |image1| image:: https://user-images.githubusercontent.com/16560492/78830005-93111d00-79e7-11ea-9d8e-a8d8325a6101.png +.. |image2| image:: https://user-images.githubusercontent.com/16560492/220486073-c5b6089d-81e4-44d9-a53c-385f479a7273.jpg +.. |image3| image:: https://github.com/ahmedfgad/GeneticAlgorithmPython/assets/16560492/7896f8d8-01c5-4ff9-8d15-52191c309b63 +.. |image4| image:: https://user-images.githubusercontent.com/16560492/36948808-f0ac882e-1fe8-11e8-8d07-1307e3477fd0.jpg +.. |image5| image:: https://user-images.githubusercontent.com/16560492/82232124-77762c00-992e-11ea-9fc6-14a1cd7a04ff.png +.. |image6| image:: https://user-images.githubusercontent.com/16560492/82232405-e0f63a80-992e-11ea-984f-b6ed76465bd1.png +.. |image7| image:: https://user-images.githubusercontent.com/16560492/82232345-cf149780-992e-11ea-8390-bf1a57a19de7.png +.. |image8| image:: https://user-images.githubusercontent.com/16560492/36948589-b47276f0-1fe5-11e8-8efe-0cd1a225ea3a.png +.. |image9| image:: https://user-images.githubusercontent.com/16560492/36948823-16f490ee-1fe9-11e8-97db-3e8905ad5440.png +.. |image10| image:: https://user-images.githubusercontent.com/16560492/36948832-3f314b60-1fe9-11e8-8f4a-4d9a53b99f3d.png +.. |image11| image:: https://user-images.githubusercontent.com/16560492/36948837-53d1849a-1fe9-11e8-9b36-e9e9291e347b.png +.. |image12| image:: https://user-images.githubusercontent.com/16560492/36948852-66f1b176-1fe9-11e8-9f9b-460804e94004.png +.. |image13| image:: https://user-images.githubusercontent.com/16560492/36948865-7fbb5158-1fe9-11e8-8c04-8ac3c1f7b1b1.png +.. |image14| image:: https://user-images.githubusercontent.com/16560492/82232405-e0f63a80-992e-11ea-984f-b6ed76465bd1.png diff --git a/docs/source/releases.rst b/docs/source/releases.rst index 5ad1ec0..fddddf9 100644 --- a/docs/source/releases.rst +++ b/docs/source/releases.rst @@ -1,8 +1,7 @@ Release History =============== -.. image:: https://user-images.githubusercontent.com/16560492/101267295-c74c0180-375f-11eb-9ad0-f8e37bd796ce.png - :alt: +|image1| .. _pygad-1017: @@ -1551,6 +1550,81 @@ Release Date 17 February 2024 Methods `__ section for more information. +.. _pygad-340: + +PyGAD 3.4.0 +----------- + +Release Date 07 January 2025 + +1. The ``delay_after_gen`` parameter is removed from the ``pygad.GA`` + class constructor. As a result, it is no longer an attribute of the + ``pygad.GA`` class instances. To add a delay after each generation, + apply it inside the ``on_generation`` callback. + https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/283 + +2. In the ``single_point_crossover()`` method of the + ``pygad.utils.crossover.Crossover`` class, all the random crossover + points are returned before the ``for`` loop. This is by calling the + ``numpy.random.randint()`` function only once before the loop to + generate all the K points (where K is the offspring size). This is + compared to calling the ``numpy.random.randint()`` function inside + the ``for`` loop K times, once for each individual offspring. + +3. Bug fix in the ``examples/example_custom_operators.py`` script. + https://github.com/ahmedfgad/GeneticAlgorithmPython/pull/285 + +4. While making prediction using the ``pygad.torchga.predict()`` + function, no gradients are calculated. + +5. The ``gene_type`` parameter of the + ``pygad.helper.unique.Unique.unique_int_gene_from_range()`` method + accepts the type of the current gene only instead of the full + gene_type list. + +6. Created a new method called ``unique_float_gene_from_range()`` + inside the ``pygad.helper.unique.Unique`` class to find a unique + floating-point number from a range. + +7. Fix a bug in the + ``pygad.helper.unique.Unique.unique_gene_by_space()`` method to + return the numeric value only instead of a NumPy array. + +8. Refactoring the ``pygad/helper/unique.py`` script to remove + duplicate codes and reformatting the docstrings. + +9. The plot_pareto_front_curve() method added to the + pygad.visualize.plot.Plot class to visualize the Pareto front for + multi-objective problems. It only supports 2 objectives. + https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/279 + +10. Fix a bug converting a nested NumPy array to a nested list. + https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/300 + +11. The ``Matplotlib`` library is only imported when a method inside the + ``pygad/visualize/plot.py`` script is used. This is more efficient + than using ``import matplotlib.pyplot`` at the module level as this + causes it to be imported when ``pygad`` is imported even when it is + not needed. + https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/292 + +12. Fix a bug when minus sign (-) is used inside the ``stop_criteria`` + parameter (e.g. ``stop_criteria=["saturate_10", "reach_-0.5"]``). + https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/296 + +13. Make sure ``self.best_solutions`` is a list of lists inside the + ``cal_pop_fitness`` method. + https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/293 + +14. Fix a bug where the ``cal_pop_fitness()`` method was using the + ``previous_generation_fitness`` attribute to return the parents + fitness. This instance attribute was not using the fitness of the + latest population, instead the fitness of the population before the + last one. The issue is solved by updating the + ``previous_generation_fitness`` attribute to the latest population + fitness before the GA completes. + https://github.com/ahmedfgad/GeneticAlgorithmPython/issues/291 + PyGAD Projects at GitHub ======================== @@ -1721,11 +1795,11 @@ section for more contact details. Within your message, please send the following details: -- Project title +- Project title -- Brief description +- Brief description -- Preferably, a link that directs the readers to your project +- Preferably, a link that directs the readers to your project Tutorials about PyGAD ===================== @@ -1851,7 +1925,7 @@ discussion includes building Keras models using either the Sequential Model or the Functional API, building an initial population of Keras model parameters, creating an appropriate fitness function, and more. -|image1| +|image2| `Train PyTorch Models Using Genetic Algorithm with PyGAD `__ --------------------------------------------------------------------------------------------------------------------------------------------- @@ -1871,7 +1945,7 @@ PyTorch models. It’s very easy to use, but there are a few tricky steps. So, in this tutorial, we’ll explore how to use PyGAD to train PyTorch models. -|image2| +|image3| `A Guide to Genetic ‘Learning’ Algorithms for Optimization `__ ------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1896,7 +1970,7 @@ simple pour résoudre le OpenAI CartPole Jeu. Dans cet article, nous allons former un simple réseau de neurones pour résoudre le OpenAI CartPole . J'utiliserai PyTorch et PyGAD . -|image3| +|image4| Spanish ------- @@ -1914,7 +1988,7 @@ resolver el Juego OpenAI CartPole. En este articulo, entrenaremos una red neuronal simple para resolver el OpenAI CartPole . Usare PyTorch y PyGAD . -|image4| +|image5| Korean ------ @@ -1922,7 +1996,7 @@ Korean `[PyGAD] Python 에서 Genetic Algorithm 을 사용해보기 `__ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -|image5| +|image6| 파이썬에서 genetic algorithm을 사용하는 패키지들을 다 사용해보진 않았지만, 확장성이 있어보이고, 시도할 일이 있어서 살펴봤다. @@ -1971,7 +2045,7 @@ Keras modellerini oluşturmayı, Keras model parametrelerinin ilk popülasyonunu oluşturmayı, uygun bir uygunluk işlevi oluşturmayı ve daha fazlasını içerir. -|image6| +|image7| Hungarian --------- @@ -2003,7 +2077,7 @@ hálózatunk 386 állítható paraméterrel rendelkezik, ezért a DNS-ünk itt populációnk egy 10x386 elemű mátrix lesz. Ezt adjuk át az 51. sorban az initial_population paraméterben. -|image7| +|image8| Russian ------- @@ -2032,77 +2106,75 @@ PyGAD разрабатывали на Python 3.7.3. Зависимости вк из изкейсов использования инструмента — оптимизация весов, которые удовлетворяют заданной функции. -|image8| +|image9| Research Papers using PyGAD =========================== A number of research papers used PyGAD and here are some of them: -- Alberto Meola, Manuel Winkler, Sören Weinrich, Metaheuristic - optimization of data preparation and machine learning hyperparameters - for prediction of dynamic methane production, Bioresource Technology, - Volume 372, 2023, 128604, ISSN 0960-8524. +- Alberto Meola, Manuel Winkler, Sören Weinrich, Metaheuristic + optimization of data preparation and machine learning hyperparameters + for prediction of dynamic methane production, Bioresource Technology, + Volume 372, 2023, 128604, ISSN 0960-8524. -- Jaros, Marta, and Jiri Jaros. "Performance-Cost Optimization of - Moldable Scientific Workflows." +- Jaros, Marta, and Jiri Jaros. "Performance-Cost Optimization of + Moldable Scientific Workflows." -- Thorat, Divya. "Enhanced genetic algorithm to reduce makespan of - multiple jobs in map-reduce application on serverless platform". - Diss. Dublin, National College of Ireland, 2020. +- Thorat, Divya. "Enhanced genetic algorithm to reduce makespan of + multiple jobs in map-reduce application on serverless platform". Diss. + Dublin, National College of Ireland, 2020. -- Koch, Chris, and Edgar Dobriban. "AttenGen: Generating Live - Attenuated Vaccine Candidates using Machine Learning." (2021). +- Koch, Chris, and Edgar Dobriban. "AttenGen: Generating Live Attenuated + Vaccine Candidates using Machine Learning." (2021). -- Bhardwaj, Bhavya, et al. "Windfarm optimization using Nelder-Mead and - Particle Swarm optimization." *2021 7th International Conference on - Electrical Energy Systems (ICEES)*. IEEE, 2021. +- Bhardwaj, Bhavya, et al. "Windfarm optimization using Nelder-Mead and + Particle Swarm optimization." *2021 7th International Conference on + Electrical Energy Systems (ICEES)*. IEEE, 2021. -- Bernardo, Reginald Christian S. and J. Said. “Towards a - model-independent reconstruction approach for late-time Hubble data.” - (2021). +- Bernardo, Reginald Christian S. and J. Said. “Towards a + model-independent reconstruction approach for late-time Hubble data.” + (2021). -- Duong, Tri Dung, Qian Li, and Guandong Xu. "Prototype-based - Counterfactual Explanation for Causal Classification." *arXiv - preprint arXiv:2105.00703* (2021). +- Duong, Tri Dung, Qian Li, and Guandong Xu. "Prototype-based + Counterfactual Explanation for Causal Classification." *arXiv preprint + arXiv:2105.00703* (2021). -- Farrag, Tamer Ahmed, and Ehab E. Elattar. "Optimized Deep Stacked - Long Short-Term Memory Network for Long-Term Load Forecasting." *IEEE - Access* 9 (2021): 68511-68522. +- Farrag, Tamer Ahmed, and Ehab E. Elattar. "Optimized Deep Stacked Long + Short-Term Memory Network for Long-Term Load Forecasting." *IEEE + Access* 9 (2021): 68511-68522. -- Antunes, E. D. O., Caetano, M. F., Marotta, M. A., Araujo, A., - Bondan, L., Meneguette, R. I., & Rocha Filho, G. P. (2021, August). - Soluções Otimizadas para o Problema de Localização de Máxima - Cobertura em Redes Militarizadas 4G/LTE. In *Anais do XXVI Workshop - de Gerência e Operação de Redes e Serviços* (pp. 152-165). SBC. +- Antunes, E. D. O., Caetano, M. F., Marotta, M. A., Araujo, A., Bondan, + L., Meneguette, R. I., & Rocha Filho, G. P. (2021, August). Soluções + Otimizadas para o Problema de Localização de Máxima Cobertura em Redes + Militarizadas 4G/LTE. In *Anais do XXVI Workshop de Gerência e + Operação de Redes e Serviços* (pp. 152-165). SBC. -- M. Yani, F. Ardilla, A. A. Saputra and N. Kubota, "Gradient-Free Deep - Q-Networks Reinforcement learning: Benchmark and Evaluation," *2021 - IEEE Symposium Series on Computational Intelligence (SSCI)*, 2021, - pp. 1-5, doi: 10.1109/SSCI50451.2021.9659941. +- M. Yani, F. Ardilla, A. A. Saputra and N. Kubota, "Gradient-Free Deep + Q-Networks Reinforcement learning: Benchmark and Evaluation," *2021 + IEEE Symposium Series on Computational Intelligence (SSCI)*, 2021, pp. + 1-5, doi: 10.1109/SSCI50451.2021.9659941. -- Yani, Mohamad, and Naoyuki Kubota. "Deep Convolutional Networks with - Genetic Algorithm for Reinforcement Learning Problem." +- Yani, Mohamad, and Naoyuki Kubota. "Deep Convolutional Networks with + Genetic Algorithm for Reinforcement Learning Problem." -- Mahendra, Muhammad Ihza, and Isman Kurniawan. "Optimizing - Convolutional Neural Network by Using Genetic Algorithm for COVID-19 - Detection in Chest X-Ray Image." *2021 International Conference on - Data Science and Its Applications (ICoDSA)*. IEEE, 2021. +- Mahendra, Muhammad Ihza, and Isman Kurniawan. "Optimizing + Convolutional Neural Network by Using Genetic Algorithm for COVID-19 + Detection in Chest X-Ray Image." *2021 International Conference on + Data Science and Its Applications (ICoDSA)*. IEEE, 2021. -- Glibota, Vjeko. *Umjeravanje mikroskopskog prometnog modela primjenom - genetskog algoritma*. Diss. University of Zagreb. Faculty of - Transport and Traffic Sciences. Division of Intelligent Transport - Systems and Logistics. Department of Intelligent Transport Systems, - 2021. +- Glibota, Vjeko. *Umjeravanje mikroskopskog prometnog modela primjenom + genetskog algoritma*. Diss. University of Zagreb. Faculty of Transport + and Traffic Sciences. Division of Intelligent Transport Systems and + Logistics. Department of Intelligent Transport Systems, 2021. -- Zhu, Mingda. *Genetic Algorithm-based Parameter Identification for - Ship Manoeuvring Model under Wind Disturbance*. MS thesis. NTNU, - 2021. +- Zhu, Mingda. *Genetic Algorithm-based Parameter Identification for + Ship Manoeuvring Model under Wind Disturbance*. MS thesis. NTNU, 2021. -- Abdalrahman, Ahmed, and Weihua Zhuang. "Dynamic pricing for - differentiated pev charging services using deep reinforcement - learning." *IEEE Transactions on Intelligent Transportation Systems* - (2020). +- Abdalrahman, Ahmed, and Weihua Zhuang. "Dynamic pricing for + differentiated pev charging services using deep reinforcement + learning." *IEEE Transactions on Intelligent Transportation Systems* + (2020). More Links ========== @@ -2125,19 +2197,19 @@ titled `Genetic Algorithm Implementation in Python `__ available at these links: -- `LinkedIn `__ +- `LinkedIn `__ -- `Towards Data - Science `__ +- `Towards Data + Science `__ -- `KDnuggets `__ +- `KDnuggets `__ `This tutorial `__ is prepared based on a previous version of the project but it still a good resource to start with coding the genetic algorithm. -|image9| +|image10| Tutorial: Introduction to Genetic Algorithm ------------------------------------------- @@ -2147,14 +2219,14 @@ Get started with the genetic algorithm by reading the tutorial titled Algorithm `__ which is available at these links: -- `LinkedIn `__ +- `LinkedIn `__ -- `Towards Data - Science `__ +- `Towards Data + Science `__ -- `KDnuggets `__ +- `KDnuggets `__ -|image10| +|image11| Tutorial: Build Neural Networks in Python ----------------------------------------- @@ -2165,14 +2237,14 @@ Classification of the Fruits360 Image Dataset `__ available at these links: -- `LinkedIn `__ +- `LinkedIn `__ -- `Towards Data - Science `__ +- `Towards Data + Science `__ -- `KDnuggets `__ +- `KDnuggets `__ -|image11| +|image12| Tutorial: Optimize Neural Networks with Genetic Algorithm --------------------------------------------------------- @@ -2183,14 +2255,14 @@ Genetic Algorithm with Python `__ available at these links: -- `LinkedIn `__ +- `LinkedIn `__ -- `Towards Data - Science `__ +- `Towards Data + Science `__ -- `KDnuggets `__ +- `KDnuggets `__ -|image12| +|image13| Tutorial: Building CNN in Python -------------------------------- @@ -2200,21 +2272,21 @@ titled `Building Convolutional Neural Network using NumPy from Scratch `__ available at these links: -- `LinkedIn `__ +- `LinkedIn `__ -- `Towards Data - Science `__ +- `Towards Data + Science `__ -- `KDnuggets `__ +- `KDnuggets `__ -- `Chinese Translation `__ +- `Chinese Translation `__ `This tutorial `__) is prepared based on a previous version of the project but it still a good resource to start with coding CNNs. -|image13| +|image14| Tutorial: Derivation of CNN from FCNN ------------------------------------- @@ -2224,14 +2296,14 @@ Get started with the genetic algorithm by reading the tutorial titled Step-By-Step `__ which is available at these links: -- `LinkedIn `__ +- `LinkedIn `__ -- `Towards Data - Science `__ +- `Towards Data + Science `__ -- `KDnuggets `__ +- `KDnuggets `__ -|image14| +|image15| Book: Practical Computer Vision Applications Using Deep Learning with CNNs -------------------------------------------------------------------------- @@ -2244,69 +2316,70 @@ learning, genetic algorithm, and more. Find the book at these links: -- `Amazon `__ +- `Amazon `__ -- `Springer `__ +- `Springer `__ -- `Apress `__ +- `Apress `__ -- `O'Reilly `__ +- `O'Reilly `__ -- `Google Books `__ +- `Google Books `__ -.. image:: https://user-images.githubusercontent.com/16560492/78830077-ae7c2800-79e7-11ea-980b-53b6bd879eeb.jpg - :alt: +|image16| Contact Us ========== -- E-mail: ahmed.f.gad@gmail.com +- E-mail: ahmed.f.gad@gmail.com -- `LinkedIn `__ +- `LinkedIn `__ -- `Amazon Author Page `__ +- `Amazon Author Page `__ -- `Heartbeat `__ +- `Heartbeat `__ -- `Paperspace `__ +- `Paperspace `__ -- `KDnuggets `__ +- `KDnuggets `__ -- `TowardsDataScience `__ +- `TowardsDataScience `__ -- `GitHub `__ +- `GitHub `__ -.. image:: https://user-images.githubusercontent.com/16560492/101267295-c74c0180-375f-11eb-9ad0-f8e37bd796ce.png - :alt: +|image17| Thank you for using `PyGAD `__ :) -.. |image1| image:: https://user-images.githubusercontent.com/16560492/111009628-2b372500-8362-11eb-90cf-01b47d831624.png +.. |image1| image:: https://user-images.githubusercontent.com/16560492/101267295-c74c0180-375f-11eb-9ad0-f8e37bd796ce.png +.. |image2| image:: https://user-images.githubusercontent.com/16560492/111009628-2b372500-8362-11eb-90cf-01b47d831624.png :target: https://blog.paperspace.com/train-keras-models-using-genetic-algorithm-with-pygad -.. |image2| image:: https://user-images.githubusercontent.com/16560492/111009678-5457b580-8362-11eb-899a-39e2f96984df.png +.. |image3| image:: https://user-images.githubusercontent.com/16560492/111009678-5457b580-8362-11eb-899a-39e2f96984df.png :target: https://neptune.ai/blog/train-pytorch-models-using-genetic-algorithm-with-pygad -.. |image3| image:: https://user-images.githubusercontent.com/16560492/111009275-3178d180-8361-11eb-9e86-7fb1519acde7.png +.. |image4| image:: https://user-images.githubusercontent.com/16560492/111009275-3178d180-8361-11eb-9e86-7fb1519acde7.png :target: https://www.hebergementwebs.com/nouvelles/comment-les-algorithmes-genetiques-peuvent-rivaliser-avec-la-descente-de-gradient-et-le-backprop -.. |image4| image:: https://user-images.githubusercontent.com/16560492/111009257-232ab580-8361-11eb-99a5-7226efbc3065.png +.. |image5| image:: https://user-images.githubusercontent.com/16560492/111009257-232ab580-8361-11eb-99a5-7226efbc3065.png :target: https://www.hebergementwebs.com/noticias/como-los-algoritmos-geneticos-pueden-competir-con-el-descenso-de-gradiente-y-el-backprop -.. |image5| image:: https://user-images.githubusercontent.com/16560492/108586306-85bd0280-731b-11eb-874c-7ac4ce1326cd.jpg +.. |image6| image:: https://user-images.githubusercontent.com/16560492/108586306-85bd0280-731b-11eb-874c-7ac4ce1326cd.jpg :target: https://data-newbie.tistory.com/m/685 -.. |image6| image:: https://user-images.githubusercontent.com/16560492/108586601-85be0200-731d-11eb-98a4-161c75a1f099.jpg +.. |image7| image:: https://user-images.githubusercontent.com/16560492/108586601-85be0200-731d-11eb-98a4-161c75a1f099.jpg :target: https://erencan34.medium.com/pygad-ile-genetik-algoritmay%C4%B1-kullanarak-keras-modelleri-nas%C4%B1l-e%C4%9Fitilir-cf92639a478c -.. |image7| image:: https://user-images.githubusercontent.com/16560492/101267295-c74c0180-375f-11eb-9ad0-f8e37bd796ce.png - :target: https://thebojda.medium.com/tensorflow-alapoz%C3%B3-10-24f7767d4a2c .. |image8| image:: https://user-images.githubusercontent.com/16560492/101267295-c74c0180-375f-11eb-9ad0-f8e37bd796ce.png + :target: https://thebojda.medium.com/tensorflow-alapoz%C3%B3-10-24f7767d4a2c +.. |image9| image:: https://user-images.githubusercontent.com/16560492/101267295-c74c0180-375f-11eb-9ad0-f8e37bd796ce.png :target: https://neurohive.io/ru/frameworki/pygad-biblioteka-dlya-implementacii-geneticheskogo-algoritma -.. |image9| image:: https://user-images.githubusercontent.com/16560492/78830052-a3c19300-79e7-11ea-8b9b-4b343ea4049c.png +.. |image10| image:: https://user-images.githubusercontent.com/16560492/78830052-a3c19300-79e7-11ea-8b9b-4b343ea4049c.png :target: https://www.linkedin.com/pulse/genetic-algorithm-implementation-python-ahmed-gad -.. |image10| image:: https://user-images.githubusercontent.com/16560492/82078259-26252d00-96e1-11ea-9a02-52a99e1054b9.jpg +.. |image11| image:: https://user-images.githubusercontent.com/16560492/82078259-26252d00-96e1-11ea-9a02-52a99e1054b9.jpg :target: https://www.linkedin.com/pulse/introduction-optimization-genetic-algorithm-ahmed-gad -.. |image11| image:: https://user-images.githubusercontent.com/16560492/82078281-30472b80-96e1-11ea-8017-6a1f4383d602.jpg +.. |image12| image:: https://user-images.githubusercontent.com/16560492/82078281-30472b80-96e1-11ea-8017-6a1f4383d602.jpg :target: https://www.linkedin.com/pulse/artificial-neural-network-implementation-using-numpy-fruits360-gad -.. |image12| image:: https://user-images.githubusercontent.com/16560492/82078300-376e3980-96e1-11ea-821c-aa6b8ceb44d4.jpg +.. |image13| image:: https://user-images.githubusercontent.com/16560492/82078300-376e3980-96e1-11ea-821c-aa6b8ceb44d4.jpg :target: https://www.linkedin.com/pulse/artificial-neural-networks-optimization-using-genetic-ahmed-gad -.. |image13| image:: https://user-images.githubusercontent.com/16560492/82431022-6c3a1200-9a8e-11ea-8f1b-b055196d76e3.png +.. |image14| image:: https://user-images.githubusercontent.com/16560492/82431022-6c3a1200-9a8e-11ea-8f1b-b055196d76e3.png :target: https://www.linkedin.com/pulse/building-convolutional-neural-network-using-numpy-from-ahmed-gad -.. |image14| image:: https://user-images.githubusercontent.com/16560492/82431369-db176b00-9a8e-11ea-99bd-e845192873fc.png +.. |image15| image:: https://user-images.githubusercontent.com/16560492/82431369-db176b00-9a8e-11ea-99bd-e845192873fc.png :target: https://www.linkedin.com/pulse/derivation-convolutional-neural-network-from-fully-connected-gad +.. |image16| image:: https://user-images.githubusercontent.com/16560492/78830077-ae7c2800-79e7-11ea-980b-53b6bd879eeb.jpg +.. |image17| image:: https://user-images.githubusercontent.com/16560492/101267295-c74c0180-375f-11eb-9ad0-f8e37bd796ce.png diff --git a/examples/example_multi_objective.py b/examples/example_multi_objective.py index b0ff1a2..048248e 100644 --- a/examples/example_multi_objective.py +++ b/examples/example_multi_objective.py @@ -54,6 +54,7 @@ def on_generation(ga_instance): ga_instance.run() ga_instance.plot_fitness(label=['Obj 1', 'Obj 2']) +ga_instance.plot_pareto_front_curve() # Returning the details of the best solution. solution, solution_fitness, solution_idx = ga_instance.best_solution(ga_instance.last_generation_fitness) diff --git a/pyproject.toml b/pyproject.toml index 7817470..aed6e1d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ build-backend = "setuptools.build_meta" [project] name = "pygad" -version = "3.3.1" +version = "3.4.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 b9f88de..d9ec309 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="pygad", - version="3.3.1", + version="3.4.0", author="Ahmed Fawzy Gad", install_requires=["numpy", "matplotlib", "cloudpickle",], author_email="ahmed.f.gad@gmail.com", From fd0e18f8fc79b8dd2df49420be8f70ffef1c7483 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Thu, 6 Feb 2025 15:44:54 -0500 Subject: [PATCH 29/31] Fix bugs with stop_criteria --- docs/source/pygad_more.rst | 4871 ++++++++++++++++++----------------- pygad/pygad.py | 43 +- tests/test_stop_criteria.py | 60 + 3 files changed, 2538 insertions(+), 2436 deletions(-) diff --git a/docs/source/pygad_more.rst b/docs/source/pygad_more.rst index 7e0d748..a992b1e 100644 --- a/docs/source/pygad_more.rst +++ b/docs/source/pygad_more.rst @@ -1,2417 +1,2454 @@ -More About PyGAD -================ - -Multi-Objective Optimization -============================ - -In `PyGAD -3.2.0 `__, -the library supports multi-objective optimization using the -non-dominated sorting genetic algorithm II (NSGA-II). The code is -exactly similar to the regular code used for single-objective -optimization except for 1 difference. It is the return value of the -fitness function. - -In single-objective optimization, the fitness function returns a single -numeric value. In this example, the variable ``fitness`` is expected to -be a numeric value. - -.. code:: python - - def fitness_func(ga_instance, solution, solution_idx): - ... - return fitness - -But in multi-objective optimization, the fitness function returns any of -these data types: - -1. ``list`` - -2. ``tuple`` - -3. ``numpy.ndarray`` - -.. code:: python - - def fitness_func(ga_instance, solution, solution_idx): - ... - return [fitness1, fitness2, ..., fitnessN] - -Whenever the fitness function returns an iterable of these data types, -then the problem is considered multi-objective. This holds even if there -is a single element in the returned iterable. - -Other than the fitness function, everything else could be the same in -both single and multi-objective problems. - -But it is recommended to use one of these 2 parent selection operators -to solve multi-objective problems: - -1. ``nsga2``: This selects the parents based on non-dominated sorting - and crowding distance. - -2. ``tournament_nsga2``: This selects the parents using tournament - selection which uses non-dominated sorting and crowding distance to - rank the solutions. - -This is a multi-objective optimization example that optimizes these 2 -linear functions: - -1. ``y1 = f(w1:w6) = w1x1 + w2x2 + w3x3 + w4x4 + w5x5 + 6wx6`` - -2. ``y2 = f(w1:w6) = w1x7 + w2x8 + w3x9 + w4x10 + w5x11 + 6wx12`` - -Where: - -1. ``(x1,x2,x3,x4,x5,x6)=(4,-2,3.5,5,-11,-4.7)`` and ``y=50`` - -2. ``(x7,x8,x9,x10,x11,x12)=(-2,0.7,-9,1.4,3,5)`` and ``y=30`` - -The 2 functions use the same parameters (weights) ``w1`` to ``w6``. - -The goal is to use PyGAD to find the optimal values for such weights -that satisfy the 2 functions ``y1`` and ``y2``. - -.. code:: python - - import pygad - import numpy - - """ - Given these 2 functions: - y1 = f(w1:w6) = w1x1 + w2x2 + w3x3 + w4x4 + w5x5 + 6wx6 - y2 = f(w1:w6) = w1x7 + w2x8 + w3x9 + w4x10 + w5x11 + 6wx12 - where (x1,x2,x3,x4,x5,x6)=(4,-2,3.5,5,-11,-4.7) and y=50 - and (x7,x8,x9,x10,x11,x12)=(-2,0.7,-9,1.4,3,5) and y=30 - What are the best values for the 6 weights (w1 to w6)? We are going to use the genetic algorithm to optimize these 2 functions. - This is a multi-objective optimization problem. - - PyGAD considers the problem as multi-objective if the fitness function returns: - 1) List. - 2) Or tuple. - 3) Or numpy.ndarray. - """ - - function_inputs1 = [4,-2,3.5,5,-11,-4.7] # Function 1 inputs. - function_inputs2 = [-2,0.7,-9,1.4,3,5] # Function 2 inputs. - desired_output1 = 50 # Function 1 output. - desired_output2 = 30 # Function 2 output. - - def fitness_func(ga_instance, solution, solution_idx): - output1 = numpy.sum(solution*function_inputs1) - output2 = numpy.sum(solution*function_inputs2) - fitness1 = 1.0 / (numpy.abs(output1 - desired_output1) + 0.000001) - fitness2 = 1.0 / (numpy.abs(output2 - desired_output2) + 0.000001) - return [fitness1, fitness2] - - num_generations = 100 - num_parents_mating = 10 - - sol_per_pop = 20 - num_genes = len(function_inputs1) - - ga_instance = pygad.GA(num_generations=num_generations, - num_parents_mating=num_parents_mating, - sol_per_pop=sol_per_pop, - num_genes=num_genes, - fitness_func=fitness_func, - parent_selection_type='nsga2') - - ga_instance.run() - - ga_instance.plot_fitness(label=['Obj 1', 'Obj 2']) - - 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}") - - prediction = numpy.sum(numpy.array(function_inputs1)*solution) - print(f"Predicted output 1 based on the best solution : {prediction}") - prediction = numpy.sum(numpy.array(function_inputs2)*solution) - print(f"Predicted output 2 based on the best solution : {prediction}") - -This is the result of the print statements. The predicted outputs are -close to the desired outputs. - -.. code:: - - Parameters of the best solution : [ 0.79676439 -2.98823386 -4.12677662 5.70539445 -2.02797016 -1.07243922] - Fitness value of the best solution = [ 1.68090829 349.8591915 ] - Predicted output 1 based on the best solution : 50.59491545442283 - Predicted output 2 based on the best solution : 29.99714270722312 - -This is the figure created by the ``plot_fitness()`` method. The fitness -of the first objective has the green color. The blue color is used for -the second objective fitness. - -.. image:: https://github.com/ahmedfgad/GeneticAlgorithmPython/assets/16560492/7896f8d8-01c5-4ff9-8d15-52191c309b63 - :alt: - -.. _limit-the-gene-value-range-using-the-genespace-parameter: - -Limit the Gene Value Range using the ``gene_space`` Parameter -============================================================= - -In `PyGAD -2.11.0 `__, -the ``gene_space`` parameter supported a new feature to allow -customizing the range of accepted values for each gene. Let's take a -quick review of the ``gene_space`` parameter to build over it. - -The ``gene_space`` parameter allows the user to feed the space of values -of each gene. This way the accepted values for each gene is retracted to -the user-defined values. Assume there is a problem that has 3 genes -where each gene has different set of values as follows: - -1. Gene 1: ``[0.4, 12, -5, 21.2]`` - -2. Gene 2: ``[-2, 0.3]`` - -3. Gene 3: ``[1.2, 63.2, 7.4]`` - -Then, the ``gene_space`` for this problem is as given below. Note that -the order is very important. - -.. code:: python - - gene_space = [[0.4, 12, -5, 21.2], - [-2, 0.3], - [1.2, 63.2, 7.4]] - -In case all genes share the same set of values, then simply feed a -single list to the ``gene_space`` parameter as follows. In this case, -all genes can only take values from this list of 6 values. - -.. code:: python - - gene_space = [33, 7, 0.5, 95. 6.3, 0.74] - -The previous example restricts the gene values to just a set of fixed -number of discrete values. In case you want to use a range of discrete -values to the gene, then you can use the ``range()`` function. For -example, ``range(1, 7)`` means the set of allowed values for the gene -are ``1, 2, 3, 4, 5, and 6``. You can also use the ``numpy.arange()`` or -``numpy.linspace()`` functions for the same purpose. - -The previous discussion only works with a range of discrete values not -continuous values. In `PyGAD -2.11.0 `__, -the ``gene_space`` parameter can be assigned a dictionary that allows -the gene to have values from a continuous range. - -Assuming you want to restrict the gene within this half-open range [1 to -5) where 1 is included and 5 is not. Then simply create a dictionary -with 2 items where the keys of the 2 items are: - -1. ``'low'``: The minimum value in the range which is 1 in the example. - -2. ``'high'``: The maximum value in the range which is 5 in the example. - -The dictionary will look like that: - -.. code:: python - - {'low': 1, - 'high': 5} - -It is not acceptable to add more than 2 items in the dictionary or use -other keys than ``'low'`` and ``'high'``. - -For a 3-gene problem, the next code creates a dictionary for each gene -to restrict its values in a continuous range. For the first gene, it can -take any floating-point value from the range that starts from 1 -(inclusive) and ends at 5 (exclusive). - -.. code:: python - - gene_space = [{'low': 1, 'high': 5}, {'low': 0.3, 'high': 1.4}, {'low': -0.2, 'high': 4.5}] - -.. _more-about-the-genespace-parameter: - -More about the ``gene_space`` Parameter -======================================= - -The ``gene_space`` parameter customizes the space of values of each -gene. - -Assuming that all genes have the same global space which include the -values 0.3, 5.2, -4, and 8, then those values can be assigned to the -``gene_space`` parameter as a list, tuple, or range. Here is a list -assigned to this parameter. By doing that, then the gene values are -restricted to those assigned to the ``gene_space`` parameter. - -.. code:: python - - gene_space = [0.3, 5.2, -4, 8] - -If some genes have different spaces, then ``gene_space`` should accept a -nested list or tuple. In this case, the elements could be: - -1. Number (of ``int``, ``float``, or ``NumPy`` data types): A single - value to be assigned to the gene. This means this gene will have the - same value across all generations. - -2. ``list``, ``tuple``, ``numpy.ndarray``, or any range like ``range``, - ``numpy.arange()``, or ``numpy.linspace``: It holds the space for - each individual gene. But this space is usually discrete. That is - there is a set of finite values to select from. - -3. ``dict``: To sample a value for a gene from a continuous range. The - dictionary must have 2 mandatory keys which are ``"low"`` and - ``"high"`` in addition to an optional key which is ``"step"``. A - random value is returned between the values assigned to the items - with ``"low"`` and ``"high"`` keys. If the ``"step"`` exists, then - this works as the previous options (i.e. discrete set of values). - -4. ``None``: A gene with its space set to ``None`` is initialized - randomly from the range specified by the 2 parameters - ``init_range_low`` and ``init_range_high``. For mutation, its value - is mutated based on a random value from the range specified by the 2 - parameters ``random_mutation_min_val`` and - ``random_mutation_max_val``. If all elements in the ``gene_space`` - parameter are ``None``, the parameter will not have any effect. - -Assuming that a chromosome has 2 genes and each gene has a different -value space. Then the ``gene_space`` could be assigned a nested -list/tuple where each element determines the space of a gene. - -According to the next code, the space of the first gene is ``[0.4, -5]`` -which has 2 values and the space for the second gene is -``[0.5, -3.2, 8.8, -9]`` which has 4 values. - -.. code:: python - - gene_space = [[0.4, -5], [0.5, -3.2, 8.2, -9]] - -For a 2 gene chromosome, if the first gene space is restricted to the -discrete values from 0 to 4 and the second gene is restricted to the -values from 10 to 19, then it could be specified according to the next -code. - -.. code:: python - - gene_space = [range(5), range(10, 20)] - -The ``gene_space`` can also be assigned to a single range, as given -below, where the values of all genes are sampled from the same range. - -.. code:: python - - gene_space = numpy.arange(15) - -The ``gene_space`` can be assigned a dictionary to sample a value from a -continuous range. - -.. code:: python - - gene_space = {"low": 4, "high": 30} - -A step also can be assigned to the dictionary. This works as if a range -is used. - -.. code:: python - - gene_space = {"low": 4, "high": 30, "step": 2.5} - -.. - - Setting a ``dict`` like ``{"low": 0, "high": 10}`` in the - ``gene_space`` means that random values from the continuous range [0, - 10) are sampled. Note that ``0`` is included but ``10`` is not - included while sampling. Thus, the maximum value that could be - returned is less than ``10`` like ``9.9999``. But if the user decided - to round the genes using, for example, ``[float, 2]``, then this - value will become 10. So, the user should be careful to the inputs. - -If a ``None`` is assigned to only a single gene, then its value will be -randomly generated initially using the ``init_range_low`` and -``init_range_high`` parameters in the ``pygad.GA`` class's constructor. -During mutation, the value are sampled from the range defined by the 2 -parameters ``random_mutation_min_val`` and ``random_mutation_max_val``. -This is an example where the second gene is given a ``None`` value. - -.. code:: python - - gene_space = [range(5), None, numpy.linspace(10, 20, 300)] - -If the user did not assign the initial population to the -``initial_population`` parameter, the initial population is created -randomly based on the ``gene_space`` parameter. Moreover, the mutation -is applied based on this parameter. - -.. _how-mutation-works-with-the-genespace-parameter: - -How Mutation Works with the ``gene_space`` Parameter? ------------------------------------------------------ - -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 -value out of these 3 values. - -.. code:: python - - Gene space: [[1, 2, 3], - None] - Solution: [1, 5] - -For a solution like ``[1, 5]``, then mutation happens for the first gene -by simply replacing its current value by a randomly selected value -(other than its current value if possible). So, the value 1 will be -replaced by either 2 or 3. - -For the second gene, its space is set to ``None``. So, traditional -mutation happens for this gene by: - -1. Generating a random value from the range defined by the - ``random_mutation_min_val`` and ``random_mutation_max_val`` - parameters. - -2. Adding this random value to the current gene's value. - -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 -====================== - -In `PyGAD -2.4.0 `__, -it is possible to stop the genetic algorithm after any generation. All -you need to do it to return the string ``"stop"`` in the callback -function ``on_generation``. When this callback function is implemented -and assigned to the ``on_generation`` parameter in the constructor of -the ``pygad.GA`` class, then the algorithm immediately stops after -completing its current generation. Let's discuss an example. - -Assume that the user wants to stop algorithm either after the 100 -generations or if a condition is met. The user may assign a value of 100 -to the ``num_generations`` parameter of the ``pygad.GA`` class -constructor. - -The condition that stops the algorithm is written in a callback function -like the one in the next code. If the fitness value of the best solution -exceeds 70, then the string ``"stop"`` is returned. - -.. code:: python - - def func_generation(ga_instance): - if ga_instance.best_solution()[1] >= 70: - return "stop" - -Stop Criteria -============= - -In `PyGAD -2.15.0 `__, -a new parameter named ``stop_criteria`` is added to the constructor of -the ``pygad.GA`` class. It helps to stop the evolution based on some -criteria. It can be assigned to one or more criterion. - -Each criterion is passed as ``str`` that consists of 2 parts: - -1. Stop word. - -2. Number. - -It takes this form: - -.. code:: python - - "word_num" - -The current 2 supported words are ``reach`` and ``saturate``. - -The ``reach`` word stops the ``run()`` method if the fitness value is -equal to or greater than a given fitness value. An example for ``reach`` -is ``"reach_40"`` which stops the evolution if the fitness is >= 40. - -``saturate`` stops the evolution if the fitness saturates for a given -number of consecutive generations. An example for ``saturate`` is -``"saturate_7"`` which means stop the ``run()`` method if the fitness -does not change for 7 consecutive generations. - -Here is an example that stops the evolution if either the fitness value -reached ``127.4`` or if the fitness saturates for ``15`` generations. - -.. code:: python - - import pygad - import numpy - - equation_inputs = [4, -2, 3.5, 8, 9, 4] - desired_output = 44 - - def fitness_func(ga_instance, solution, solution_idx): - output = numpy.sum(solution * equation_inputs) - - fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) - - return fitness - - ga_instance = pygad.GA(num_generations=200, - sol_per_pop=10, - num_parents_mating=4, - num_genes=len(equation_inputs), - fitness_func=fitness_func, - stop_criteria=["reach_127.4", "saturate_15"]) - - ga_instance.run() - print(f"Number of generations passed is {ga_instance.generations_completed}") - -Elitism Selection -================= - -In `PyGAD -2.18.0 `__, -a new parameter called ``keep_elitism`` is supported. It accepts an -integer to define the number of elitism (i.e. best solutions) to keep in -the next generation. This parameter defaults to ``1`` which means only -the best solution is kept in the next generation. - -In the next example, the ``keep_elitism`` parameter in the constructor -of the ``pygad.GA`` class is set to 2. Thus, the best 2 solutions in -each generation are kept in the next generation. - -.. code:: python - - import numpy - import pygad - - function_inputs = [4,-2,3.5,5,-11,-4.7] - desired_output = 44 - - def fitness_func(ga_instance, solution, solution_idx): - output = numpy.sum(solution*function_inputs) - fitness = 1.0 / numpy.abs(output - desired_output) - return fitness - - ga_instance = pygad.GA(num_generations=2, - num_parents_mating=3, - fitness_func=fitness_func, - num_genes=6, - sol_per_pop=5, - keep_elitism=2) - - ga_instance.run() - -The value passed to the ``keep_elitism`` parameter must satisfy 2 -conditions: - -1. It must be ``>= 0``. - -2. It must be ``<= sol_per_pop``. That is its value cannot exceed the - number of solutions in the current population. - -In the previous example, if the ``keep_elitism`` parameter is set equal -to the value passed to the ``sol_per_pop`` parameter, which is 5, then -there will be no evolution at all as in the next figure. This is because -all the 5 solutions are used as elitism in the next generation and no -offspring will be created. - -.. code:: python - - ... - - ga_instance = pygad.GA(..., - sol_per_pop=5, - keep_elitism=5) - - ga_instance.run() - -.. image:: https://user-images.githubusercontent.com/16560492/189273225-67ffad41-97ab-45e1-9324-429705e17b20.png - :alt: - -Note that if the ``keep_elitism`` parameter is effective (i.e. is -assigned a positive integer, not zero), then the ``keep_parents`` -parameter will have no effect. Because the default value of the -``keep_elitism`` parameter is 1, then the ``keep_parents`` parameter has -no effect by default. The ``keep_parents`` parameter is only effective -when ``keep_elitism=0``. - -Random Seed -=========== - -In `PyGAD -2.18.0 `__, -a new parameter called ``random_seed`` is supported. Its value is used -as a seed for the random function generators. - -PyGAD uses random functions in these 2 libraries: - -1. NumPy - -2. random - -The ``random_seed`` parameter defaults to ``None`` which means no seed -is used. As a result, different random numbers are generated for each -run of PyGAD. - -If this parameter is assigned a proper seed, then the results will be -reproducible. In the next example, the integer 2 is used as a random -seed. - -.. code:: python - - import numpy - import pygad - - function_inputs = [4,-2,3.5,5,-11,-4.7] - desired_output = 44 - - def fitness_func(ga_instance, solution, solution_idx): - output = numpy.sum(solution*function_inputs) - fitness = 1.0 / numpy.abs(output - desired_output) - return fitness - - ga_instance = pygad.GA(num_generations=2, - num_parents_mating=3, - fitness_func=fitness_func, - sol_per_pop=5, - num_genes=6, - random_seed=2) - - ga_instance.run() - best_solution, best_solution_fitness, best_match_idx = ga_instance.best_solution() - print(best_solution) - print(best_solution_fitness) - -This is the best solution found and its fitness value. - -.. code:: - - [ 2.77249188 -4.06570662 0.04196872 -3.47770796 -0.57502138 -3.22775267] - 0.04872203136549972 - -After running the code again, it will find the same result. - -.. code:: - - [ 2.77249188 -4.06570662 0.04196872 -3.47770796 -0.57502138 -3.22775267] - 0.04872203136549972 - -Continue without Losing Progress -================================ - -In `PyGAD -2.18.0 `__, -and thanks for `Felix Bernhard `__ for -opening `this GitHub -issue `__, -the values of these 4 instance attributes are no longer reset after each -call to the ``run()`` method. - -1. ``self.best_solutions`` - -2. ``self.best_solutions_fitness`` - -3. ``self.solutions`` - -4. ``self.solutions_fitness`` - -This helps the user to continue where the last run stopped without -losing the values of these 4 attributes. - -Now, the user can save the model by calling the ``save()`` method. - -.. code:: python - - import pygad - - def fitness_func(ga_instance, solution, solution_idx): - ... - return fitness - - ga_instance = pygad.GA(...) - - ga_instance.run() - - ga_instance.plot_fitness() - - ga_instance.save("pygad_GA") - -Then the saved model is loaded by calling the ``load()`` function. After -calling the ``run()`` method over the loaded instance, then the data -from the previous 4 attributes are not reset but extended with the new -data. - -.. code:: python - - import pygad - - def fitness_func(ga_instance, solution, solution_idx): - ... - return fitness - - loaded_ga_instance = pygad.load("pygad_GA") - - loaded_ga_instance.run() - - loaded_ga_instance.plot_fitness() - -The plot created by the ``plot_fitness()`` method will show the data -collected from both the runs. - -Note that the 2 attributes (``self.best_solutions`` and -``self.best_solutions_fitness``) only work if the -``save_best_solutions`` parameter is set to ``True``. Also, the 2 -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 -================================= - -In `PyGAD -2.13.0 `__, -a new bool parameter called ``allow_duplicate_genes`` is supported to -control whether duplicates are supported in the chromosome or not. In -other words, whether 2 or more genes might have the same exact value. - -If ``allow_duplicate_genes=True`` (which is the default case), genes may -have the same value. If ``allow_duplicate_genes=False``, then no 2 genes -will have the same value given that there are enough unique values for -the genes. - -The next code gives an example to use the ``allow_duplicate_genes`` -parameter. A callback generation function is implemented to print the -population after each generation. - -.. code:: python - - import pygad - - def fitness_func(ga_instance, solution, solution_idx): - return 0 - - def on_generation(ga): - print("Generation", ga.generations_completed) - print(ga.population) - - ga_instance = pygad.GA(num_generations=5, - sol_per_pop=5, - num_genes=4, - mutation_num_genes=3, - random_mutation_min_val=-5, - random_mutation_max_val=5, - num_parents_mating=2, - fitness_func=fitness_func, - gene_type=int, - on_generation=on_generation, - allow_duplicate_genes=False) - ga_instance.run() - -Here are the population after the 5 generations. Note how there are no -duplicate values. - -.. code:: python - - Generation 1 - [[ 2 -2 -3 3] - [ 0 1 2 3] - [ 5 -3 6 3] - [-3 1 -2 4] - [-1 0 -2 3]] - Generation 2 - [[-1 0 -2 3] - [-3 1 -2 4] - [ 0 -3 -2 6] - [-3 0 -2 3] - [ 1 -4 2 4]] - Generation 3 - [[ 1 -4 2 4] - [-3 0 -2 3] - [ 4 0 -2 1] - [-4 0 -2 -3] - [-4 2 0 3]] - Generation 4 - [[-4 2 0 3] - [-4 0 -2 -3] - [-2 5 4 -3] - [-1 2 -4 4] - [-4 2 0 -3]] - Generation 5 - [[-4 2 0 -3] - [-1 2 -4 4] - [ 3 4 -4 0] - [-1 0 2 -2] - [-4 2 -1 1]] - -The ``allow_duplicate_genes`` parameter is configured with use with the -``gene_space`` parameter. Here is an example where each of the 4 genes -has the same space of values that consists of 4 values (1, 2, 3, and 4). - -.. code:: python - - import pygad - - def fitness_func(ga_instance, solution, solution_idx): - return 0 - - def on_generation(ga): - print("Generation", ga.generations_completed) - print(ga.population) - - ga_instance = pygad.GA(num_generations=1, - sol_per_pop=5, - num_genes=4, - num_parents_mating=2, - fitness_func=fitness_func, - gene_type=int, - gene_space=[[1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4]], - on_generation=on_generation, - allow_duplicate_genes=False) - ga_instance.run() - -Even that all the genes share the same space of values, no 2 genes -duplicate their values as provided by the next output. - -.. code:: python - - Generation 1 - [[2 3 1 4] - [2 3 1 4] - [2 4 1 3] - [2 3 1 4] - [1 3 2 4]] - Generation 2 - [[1 3 2 4] - [2 3 1 4] - [1 3 2 4] - [2 3 4 1] - [1 3 4 2]] - Generation 3 - [[1 3 4 2] - [2 3 4 1] - [1 3 4 2] - [3 1 4 2] - [3 2 4 1]] - Generation 4 - [[3 2 4 1] - [3 1 4 2] - [3 2 4 1] - [1 2 4 3] - [1 3 4 2]] - Generation 5 - [[1 3 4 2] - [1 2 4 3] - [2 1 4 3] - [1 2 4 3] - [1 2 4 3]] - -You should care of giving enough values for the genes so that PyGAD is -able to find alternatives for the gene value in case it duplicates with -another gene. - -There might be 2 duplicate genes where changing either of the 2 -duplicating genes will not solve the problem. For example, if -``gene_space=[[3, 0, 1], [4, 1, 2], [0, 2], [3, 2, 0]]`` and the -solution is ``[3 2 0 0]``, then the values of the last 2 genes -duplicate. There are no possible changes in the last 2 genes to solve -the problem. - -This problem can be solved by randomly changing one of the -non-duplicating genes that may make a room for a unique value in one the -2 duplicating genes. For example, by changing the second gene from 2 to -4, then any of the last 2 genes can take the value 2 and solve the -duplicates. The resultant gene is then ``[3 4 2 0]``. But this option is -not yet supported in PyGAD. - -Solve Duplicates using a Third Gene ------------------------------------ - -When ``allow_duplicate_genes=False`` and a user-defined ``gene_space`` -is used, it sometimes happen that there is no room to solve the -duplicates between the 2 genes by simply replacing the value of one gene -by another gene. In `PyGAD -3.1.0 `__, -the duplicates are solved by looking for a third gene that will help in -solving the duplicates. The following examples explain how it works. - -Example 1: - -Let's assume that this gene space is used and there is a solution with 2 -duplicate genes with the same value 4. - -.. code:: python - - Gene space: [[2, 3], - [3, 4], - [4, 5], - [5, 6]] - Solution: [3, 4, 4, 5] - -By checking the gene space, the second gene can have the values -``[3, 4]`` and the third gene can have the values ``[4, 5]``. To solve -the duplicates, we have the value of any of these 2 genes. - -If the value of the second gene changes from 4 to 3, then it will be -duplicate with the first gene. If we are to change the value of the -third gene from 4 to 5, then it will duplicate with the fourth gene. As -a conclusion, trying to just selecting a different gene value for either -the second or third genes will introduce new duplicating genes. - -When there are 2 duplicate genes but there is no way to solve their -duplicates, then the solution is to change a third gene that makes a -room to solve the duplicates between the 2 genes. - -In our example, duplicates between the second and third genes can be -solved by, for example,: - -- Changing the first gene from 3 to 2 then changing the second gene - from 4 to 3. - -- Or changing the fourth gene from 5 to 6 then changing the third gene - from 4 to 5. - -Generally, this is how to solve such duplicates: - -1. For any duplicate gene **GENE1**, select another value. - -2. Check which other gene **GENEX** has duplicate with this new value. - -3. Find if **GENEX** can have another value that will not cause any more - duplicates. If so, go to step 7. - -4. If all the other values of **GENEX** will cause duplicates, then try - another gene **GENEY**. - -5. Repeat steps 3 and 4 until exploring all the genes. - -6. If there is no possibility to solve the duplicates, then there is not - way to solve the duplicates and we have to keep the duplicate value. - -7. If a value for a gene **GENEM** is found that will not cause more - duplicates, then use this value for the gene **GENEM**. - -8. Replace the value of the gene **GENE1** by the old value of the gene - **GENEM**. This solves the duplicates. - -This is an example to solve the duplicate for the solution -``[3, 4, 4, 5]``: - -1. Let's use the second gene with value 4. Because the space of this - gene is ``[3, 4]``, then the only other value we can select is 3. - -2. The first gene also have the value 3. - -3. The first gene has another value 2 that will not cause more - duplicates in the solution. Then go to step 7. - -4. Skip. - -5. Skip. - -6. Skip. - -7. The value of the first gene 3 will be replaced by the new value 2. - The new solution is [2, 4, 4, 5]. - -8. Replace the value of the second gene 4 by the old value of the first - gene which is 3. The new solution is [2, 3, 4, 5]. The duplicate is - solved. - -Example 2: - -.. code:: python - - Gene space: [[0, 1], - [1, 2], - [2, 3], - [3, 4]] - Solution: [1, 2, 2, 3] - -The quick summary is: - -- Change the value of the first gene from 1 to 0. The solution becomes - [0, 2, 2, 3]. - -- Change the value of the second gene from 2 to 1. The solution becomes - [0, 1, 2, 3]. The duplicate is solved. - -.. _more-about-the-genetype-parameter: - -More about the ``gene_type`` Parameter -====================================== - -The ``gene_type`` parameter allows the user to control the data type for -all genes at once or each individual gene. In `PyGAD -2.15.0 `__, -the ``gene_type`` parameter also supports customizing the precision for -``float`` data types. As a result, the ``gene_type`` parameter helps to: - -1. Select a data type for all genes with or without precision. - -2. Select a data type for each individual gene with or without - precision. - -Let's discuss things by examples. - -Data Type for All Genes without Precision ------------------------------------------ - -The data type for all genes can be specified by assigning the numeric -data type directly to the ``gene_type`` parameter. This is an example to -make all genes of ``int`` data types. - -.. code:: python - - gene_type=int - -Given that the supported numeric data types of PyGAD include Python's -``int`` and ``float`` in addition to all numeric types of ``NumPy``, -then any of these types can be assigned to the ``gene_type`` parameter. - -If no precision is specified for a ``float`` data type, then the -complete floating-point number is kept. - -The next code uses an ``int`` data type for all genes where the genes in -the initial and final population are only integers. - -.. code:: python - - import pygad - import numpy - - equation_inputs = [4, -2, 3.5, 8, -2] - desired_output = 2671.1234 - - def fitness_func(ga_instance, solution, solution_idx): - output = numpy.sum(solution * equation_inputs) - fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) - return fitness - - ga_instance = pygad.GA(num_generations=10, - sol_per_pop=5, - num_parents_mating=2, - num_genes=len(equation_inputs), - fitness_func=fitness_func, - gene_type=int) - - print("Initial Population") - print(ga_instance.initial_population) - - ga_instance.run() - - print("Final Population") - print(ga_instance.population) - -.. code:: python - - Initial Population - [[ 1 -1 2 0 -3] - [ 0 -2 0 -3 -1] - [ 0 -1 -1 2 0] - [-2 3 -2 3 3] - [ 0 0 2 -2 -2]] - - Final Population - [[ 1 -1 2 2 0] - [ 1 -1 2 2 0] - [ 1 -1 2 2 0] - [ 1 -1 2 2 0] - [ 1 -1 2 2 0]] - -Data Type for All Genes with Precision --------------------------------------- - -A precision can only be specified for a ``float`` data type and cannot -be specified for integers. Here is an example to use a precision of 3 -for the ``float`` data type. In this case, all genes are of type -``float`` and their maximum precision is 3. - -.. code:: python - - gene_type=[float, 3] - -The next code uses prints the initial and final population where the -genes are of type ``float`` with precision 3. - -.. code:: python - - import pygad - import numpy - - equation_inputs = [4, -2, 3.5, 8, -2] - desired_output = 2671.1234 - - def fitness_func(ga_instance, solution, solution_idx): - output = numpy.sum(solution * equation_inputs) - fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) - - return fitness - - ga_instance = pygad.GA(num_generations=10, - sol_per_pop=5, - num_parents_mating=2, - num_genes=len(equation_inputs), - fitness_func=fitness_func, - gene_type=[float, 3]) - - print("Initial Population") - print(ga_instance.initial_population) - - ga_instance.run() - - print("Final Population") - print(ga_instance.population) - -.. code:: python - - Initial Population - [[-2.417 -0.487 3.623 2.457 -2.362] - [-1.231 0.079 -1.63 1.629 -2.637] - [ 0.692 -2.098 0.705 0.914 -3.633] - [ 2.637 -1.339 -1.107 -0.781 -3.896] - [-1.495 1.378 -1.026 3.522 2.379]] - - Final Population - [[ 1.714 -1.024 3.623 3.185 -2.362] - [ 0.692 -1.024 3.623 3.185 -2.362] - [ 0.692 -1.024 3.623 3.375 -2.362] - [ 0.692 -1.024 4.041 3.185 -2.362] - [ 1.714 -0.644 3.623 3.185 -2.362]] - -Data Type for each Individual Gene without Precision ----------------------------------------------------- - -In `PyGAD -2.14.0 `__, -the ``gene_type`` parameter allows customizing the gene type for each -individual gene. This is by using a ``list``/``tuple``/``numpy.ndarray`` -with number of elements equal to the number of genes. For each element, -a type is specified for the corresponding gene. - -This is an example for a 5-gene problem where different types are -assigned to the genes. - -.. code:: python - - gene_type=[int, float, numpy.float16, numpy.int8, float] - -This is a complete code that prints the initial and final population for -a custom-gene data type. - -.. code:: python - - import pygad - import numpy - - equation_inputs = [4, -2, 3.5, 8, -2] - desired_output = 2671.1234 - - def fitness_func(ga_instance, solution, solution_idx): - output = numpy.sum(solution * equation_inputs) - fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) - return fitness - - ga_instance = pygad.GA(num_generations=10, - sol_per_pop=5, - num_parents_mating=2, - num_genes=len(equation_inputs), - fitness_func=fitness_func, - gene_type=[int, float, numpy.float16, numpy.int8, float]) - - print("Initial Population") - print(ga_instance.initial_population) - - ga_instance.run() - - print("Final Population") - print(ga_instance.population) - -.. code:: python - - Initial Population - [[0 0.8615522360026828 0.7021484375 -2 3.5301821368185866] - [-3 2.648189378595294 -3.830078125 1 -0.9586271572917742] - [3 3.7729827570110714 1.2529296875 -3 1.395741994211889] - [0 1.0490687178053282 1.51953125 -2 0.7243617940450235] - [0 -0.6550158436937226 -2.861328125 -2 1.8212734549263097]] - - Final Population - [[3 3.7729827570110714 2.055 0 0.7243617940450235] - [3 3.7729827570110714 1.458 0 -0.14638754050305036] - [3 3.7729827570110714 1.458 0 0.0869406120516778] - [3 3.7729827570110714 1.458 0 0.7243617940450235] - [3 3.7729827570110714 1.458 0 -0.14638754050305036]] - -Data Type for each Individual Gene with Precision -------------------------------------------------- - -The precision can also be specified for the ``float`` data types as in -the next line where the second gene precision is 2 and last gene -precision is 1. - -.. code:: python - - gene_type=[int, [float, 2], numpy.float16, numpy.int8, [float, 1]] - -This is a complete example where the initial and final populations are -printed where the genes comply with the data types and precisions -specified. - -.. code:: python - - import pygad - import numpy - - equation_inputs = [4, -2, 3.5, 8, -2] - desired_output = 2671.1234 - - def fitness_func(ga_instance, solution, solution_idx): - output = numpy.sum(solution * equation_inputs) - fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) - return fitness - - ga_instance = pygad.GA(num_generations=10, - sol_per_pop=5, - num_parents_mating=2, - num_genes=len(equation_inputs), - fitness_func=fitness_func, - gene_type=[int, [float, 2], numpy.float16, numpy.int8, [float, 1]]) - - print("Initial Population") - print(ga_instance.initial_population) - - ga_instance.run() - - print("Final Population") - print(ga_instance.population) - -.. code:: python - - Initial Population - [[-2 -1.22 1.716796875 -1 0.2] - [-1 -1.58 -3.091796875 0 -1.3] - [3 3.35 -0.107421875 1 -3.3] - [-2 -3.58 -1.779296875 0 0.6] - [2 -3.73 2.65234375 3 -0.5]] - - Final Population - [[2 -4.22 3.47 3 -1.3] - [2 -3.73 3.47 3 -1.3] - [2 -4.22 3.47 2 -1.3] - [2 -4.58 3.47 3 -1.3] - [2 -3.73 3.47 3 -1.3]] - -Parallel Processing in PyGAD -============================ - -Starting from `PyGAD -2.17.0 `__, -parallel processing becomes supported. This section explains how to use -parallel processing in PyGAD. - -According to the `PyGAD -lifecycle `__, -parallel processing can be parallelized in only 2 operations: - -1. Population fitness calculation. - -2. Mutation. - -The reason is that the calculations in these 2 operations are -independent (i.e. each solution/chromosome is handled independently from -the others) and can be distributed across different processes or -threads. - -For the mutation operation, it does not do intensive calculations on the -CPU. Its calculations are simple like flipping the values of some genes -from 0 to 1 or adding a random value to some genes. So, it does not take -much CPU processing time. Experiments proved that parallelizing the -mutation operation across the solutions increases the time instead of -reducing it. This is because running multiple processes or threads adds -overhead to manage them. Thus, parallel processing cannot be applied on -the mutation operation. - -For the population fitness calculation, parallel processing can help -make a difference and reduce the processing time. But this is -conditional on the type of calculations done in the fitness function. If -the fitness function makes intensive calculations and takes much -processing time from the CPU, then it is probably that parallel -processing will help to cut down the overall time. - -This section explains how parallel processing works in PyGAD and how to -use parallel processing in PyGAD - -How to Use Parallel Processing in PyGAD ---------------------------------------- - -Starting from `PyGAD -2.17.0 `__, -a new parameter called ``parallel_processing`` added to the constructor -of the ``pygad.GA`` class. - -.. code:: python - - import pygad - ... - ga_instance = pygad.GA(..., - parallel_processing=...) - ... - -This parameter allows the user to do the following: - -1. Enable parallel processing. - -2. Select whether processes or threads are used. - -3. Specify the number of processes or threads to be used. - -These are 3 possible values for the ``parallel_processing`` parameter: - -1. ``None``: (Default) It means no parallel processing is used. - -2. A positive integer referring to the number of threads to be used - (i.e. threads, not processes, are used. - -3. ``list``/``tuple``: If a list or a tuple of exactly 2 elements is - assigned, then: - - 1. The first element can be either ``'process'`` or ``'thread'`` to - specify whether processes or threads are used, respectively. - - 2. The second element can be: - - 1. A positive integer to select the maximum number of processes or - threads to be used - - 2. ``0`` to indicate that 0 processes or threads are used. It - means no parallel processing. This is identical to setting - ``parallel_processing=None``. - - 3. ``None`` to use the default value as calculated by the - ``concurrent.futures module``. - -These are examples of the values assigned to the ``parallel_processing`` -parameter: - -- ``parallel_processing=4``: Because the parameter is assigned a - positive integer, this means parallel processing is activated where 4 - threads are used. - -- ``parallel_processing=["thread", 5]``: Use parallel processing with 5 - threads. This is identical to ``parallel_processing=5``. - -- ``parallel_processing=["process", 8]``: Use parallel processing with - 8 processes. - -- ``parallel_processing=["process", 0]``: As the second element is - given the value 0, this means do not use parallel processing. This is - identical to ``parallel_processing=None``. - -Examples --------- - -The examples will help you know the difference between using processes -and threads. Moreover, it will give an idea when parallel processing -would make a difference and reduce the time. These are dummy examples -where the fitness function is made to always return 0. - -The first example uses 10 genes, 5 solutions in the population where -only 3 solutions mate, and 9999 generations. The fitness function uses a -``for`` loop with 100 iterations just to have some calculations. In the -constructor of the ``pygad.GA`` class, ``parallel_processing=None`` -means no parallel processing is used. - -.. code:: python - - import pygad - import time - - def fitness_func(ga_instance, solution, solution_idx): - for _ in range(99): - pass - return 0 - - ga_instance = pygad.GA(num_generations=9999, - num_parents_mating=3, - sol_per_pop=5, - num_genes=10, - fitness_func=fitness_func, - suppress_warnings=True, - parallel_processing=None) - - if __name__ == '__main__': - t1 = time.time() - - ga_instance.run() - - t2 = time.time() - print("Time is", t2-t1) - -When parallel processing is not used, the time it takes to run the -genetic algorithm is ``1.5`` seconds. - -In the comparison, let's do a second experiment where parallel -processing is used with 5 threads. In this case, it take ``5`` seconds. - -.. code:: python - - ... - ga_instance = pygad.GA(..., - parallel_processing=5) - ... - -For the third experiment, processes instead of threads are used. Also, -only 99 generations are used instead of 9999. The time it takes is -``99`` seconds. - -.. code:: python - - ... - ga_instance = pygad.GA(num_generations=99, - ..., - parallel_processing=["process", 5]) - ... - -This is the summary of the 3 experiments: - -1. No parallel processing & 9999 generations: 1.5 seconds. - -2. Parallel processing with 5 threads & 9999 generations: 5 seconds - -3. Parallel processing with 5 processes & 99 generations: 99 seconds - -Because the fitness function does not need much CPU time, the normal -processing takes the least time. Running processes for this simple -problem takes 99 compared to only 5 seconds for threads because managing -processes is much heavier than managing threads. Thus, most of the CPU -time is for swapping the processes instead of executing the code. - -In the second example, the loop makes 99999999 iterations and only 5 -generations are used. With no parallelization, it takes 22 seconds. - -.. code:: python - - import pygad - import time - - def fitness_func(ga_instance, solution, solution_idx): - for _ in range(99999999): - pass - return 0 - - ga_instance = pygad.GA(num_generations=5, - num_parents_mating=3, - sol_per_pop=5, - num_genes=10, - fitness_func=fitness_func, - suppress_warnings=True, - parallel_processing=None) - - if __name__ == '__main__': - t1 = time.time() - ga_instance.run() - t2 = time.time() - print("Time is", t2-t1) - -It takes 15 seconds when 10 processes are used. - -.. code:: python - - ... - ga_instance = pygad.GA(..., - parallel_processing=["process", 10]) - ... - -This is compared to 20 seconds when 10 threads are used. - -.. code:: python - - ... - ga_instance = pygad.GA(..., - parallel_processing=["thread", 10]) - ... - -Based on the second example, using parallel processing with 10 processes -takes the least time because there is much CPU work done. Generally, -processes are preferred over threads when most of the work in on the -CPU. Threads are preferred over processes in some situations like doing -input/output operations. - -*Before releasing* `PyGAD -2.17.0 `__\ *,* -`László -Fazekas `__ -*wrote an article to parallelize the fitness function with PyGAD. Check -it:* `How Genetic Algorithms Can Compete with Gradient Descent and -Backprop `__. - -Print Lifecycle Summary -======================= - -In `PyGAD -2.19.0 `__, -a new method called ``summary()`` is supported. It prints a Keras-like -summary of the PyGAD lifecycle showing the steps, callback functions, -parameters, etc. - -This method accepts the following parameters: - -- ``line_length=70``: An integer representing the length of the single - line in characters. - -- ``fill_character=" "``: A character to fill the lines. - -- ``line_character="-"``: A character for creating a line separator. - -- ``line_character2="="``: A secondary character to create a line - separator. - -- ``columns_equal_len=False``: The table rows are split into - equal-sized columns or split subjective to the width needed. - -- ``print_step_parameters=True``: Whether to print extra parameters - about each step inside the step. If ``print_step_parameters=False`` - and ``print_parameters_summary=True``, then the parameters of each - step are printed at the end of the table. - -- ``print_parameters_summary=True``: Whether to print parameters - summary at the end of the table. If ``print_step_parameters=False``, - then the parameters of each step are printed at the end of the table - too. - -This is a quick example to create a PyGAD example. - -.. code:: python - - import pygad - import numpy - - function_inputs = [4,-2,3.5,5,-11,-4.7] - desired_output = 44 - - def genetic_fitness(solution, solution_idx): - output = numpy.sum(solution*function_inputs) - fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) - return fitness - - def on_gen(ga): - pass - - def on_crossover_callback(a, b): - pass - - ga_instance = pygad.GA(num_generations=100, - num_parents_mating=10, - sol_per_pop=20, - num_genes=len(function_inputs), - on_crossover=on_crossover_callback, - on_generation=on_gen, - parallel_processing=2, - stop_criteria="reach_10", - fitness_batch_size=4, - crossover_probability=0.4, - fitness_func=genetic_fitness) - -Then call the ``summary()`` method to print the summary with the default -parameters. Note that entries for the crossover and generation callback -function are created because their callback functions are implemented -through the ``on_crossover_callback()`` and ``on_gen()``, respectively. - -.. code:: python - - ga_instance.summary() - -.. code:: bash - - ---------------------------------------------------------------------- - PyGAD Lifecycle - ====================================================================== - Step Handler Output Shape - ====================================================================== - Fitness Function genetic_fitness() (1) - Fitness batch size: 4 - ---------------------------------------------------------------------- - Parent Selection steady_state_selection() (10, 6) - Number of Parents: 10 - ---------------------------------------------------------------------- - Crossover single_point_crossover() (10, 6) - Crossover probability: 0.4 - ---------------------------------------------------------------------- - On Crossover on_crossover_callback() None - ---------------------------------------------------------------------- - Mutation random_mutation() (10, 6) - Mutation Genes: 1 - Random Mutation Range: (-1.0, 1.0) - Mutation by Replacement: False - Allow Duplicated Genes: True - ---------------------------------------------------------------------- - On Generation on_gen() None - Stop Criteria: [['reach', 10.0]] - ---------------------------------------------------------------------- - ====================================================================== - Population Size: (20, 6) - Number of Generations: 100 - Initial Population Range: (-4, 4) - Keep Elitism: 1 - Gene DType: [, None] - Parallel Processing: ['thread', 2] - Save Best Solutions: False - Save Solutions: False - ====================================================================== - -We can set the ``print_step_parameters`` and -``print_parameters_summary`` parameters to ``False`` to not print the -parameters. - -.. code:: python - - ga_instance.summary(print_step_parameters=False, - print_parameters_summary=False) - -.. code:: bash - - ---------------------------------------------------------------------- - PyGAD Lifecycle - ====================================================================== - Step Handler Output Shape - ====================================================================== - Fitness Function genetic_fitness() (1) - ---------------------------------------------------------------------- - Parent Selection steady_state_selection() (10, 6) - ---------------------------------------------------------------------- - Crossover single_point_crossover() (10, 6) - ---------------------------------------------------------------------- - On Crossover on_crossover_callback() None - ---------------------------------------------------------------------- - Mutation random_mutation() (10, 6) - ---------------------------------------------------------------------- - On Generation on_gen() None - ---------------------------------------------------------------------- - ====================================================================== - -Logging Outputs -=============== - -In `PyGAD -3.0.0 `__, -the ``print()`` statement is no longer used and the outputs are printed -using the `logging `__ -module. A a new parameter called ``logger`` is supported to accept the -user-defined logger. - -.. code:: python - - import logging - - logger = ... - - ga_instance = pygad.GA(..., - logger=logger, - ...) - -The default value for this parameter is ``None``. If there is no logger -passed (i.e. ``logger=None``), then a default logger is created to log -the messages to the console exactly like how the ``print()`` statement -works. - -Some advantages of using the the -`logging `__ module -instead of the ``print()`` statement are: - -1. The user has more control over the printed messages specially if - there is a project that uses multiple modules where each module - prints its messages. A logger can organize the outputs. - -2. Using the proper ``Handler``, the user can log the output messages to - files and not only restricted to printing it to the console. So, it - is much easier to record the outputs. - -3. The format of the printed messages can be changed by customizing the - ``Formatter`` assigned to the Logger. - -This section gives some quick examples to use the ``logging`` module and -then gives an example to use the logger with PyGAD. - -Logging to the Console ----------------------- - -This is an example to create a logger to log the messages to the -console. - -.. code:: python - - import logging - - # Create a logger - logger = logging.getLogger(__name__) - - # Set the logger level to debug so that all the messages are printed. - logger.setLevel(logging.DEBUG) - - # Create a stream handler to log the messages to the console. - stream_handler = logging.StreamHandler() - - # Set the handler level to debug. - stream_handler.setLevel(logging.DEBUG) - - # Create a formatter - formatter = logging.Formatter('%(message)s') - - # Add the formatter to handler. - stream_handler.setFormatter(formatter) - - # Add the stream handler to the logger - logger.addHandler(stream_handler) - -Now, we can log messages to the console with the format specified in the -``Formatter``. - -.. code:: python - - logger.debug('Debug message.') - logger.info('Info message.') - logger.warning('Warn message.') - logger.error('Error message.') - logger.critical('Critical message.') - -The outputs are identical to those returned using the ``print()`` -statement. - -.. code:: - - Debug message. - Info message. - Warn message. - Error message. - Critical message. - -By changing the format of the output messages, we can have more -information about each message. - -.. code:: python - - formatter = logging.Formatter('%(asctime)s %(levelname)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S') - -This is a sample output. - -.. code:: python - - 2023-04-03 18:46:27 DEBUG: Debug message. - 2023-04-03 18:46:27 INFO: Info message. - 2023-04-03 18:46:27 WARNING: Warn message. - 2023-04-03 18:46:27 ERROR: Error message. - 2023-04-03 18:46:27 CRITICAL: Critical message. - -Note that you may need to clear the handlers after finishing the -execution. This is to make sure no cached handlers are used in the next -run. If the cached handlers are not cleared, then the single output -message may be repeated. - -.. code:: python - - logger.handlers.clear() - -Logging to a File ------------------ - -This is another example to log the messages to a file named -``logfile.txt``. The formatter prints the following about each message: - -1. The date and time at which the message is logged. - -2. The log level. - -3. The message. - -4. The path of the file. - -5. The lone number of the log message. - -.. code:: python - - import logging - - level = logging.DEBUG - name = 'logfile.txt' - - logger = logging.getLogger(name) - logger.setLevel(level) - - file_handler = logging.FileHandler(name, 'a+', 'utf-8') - file_handler.setLevel(logging.DEBUG) - file_format = logging.Formatter('%(asctime)s %(levelname)s: %(message)s - %(pathname)s:%(lineno)d', datefmt='%Y-%m-%d %H:%M:%S') - file_handler.setFormatter(file_format) - logger.addHandler(file_handler) - -This is how the outputs look like. - -.. code:: python - - 2023-04-03 18:54:03 DEBUG: Debug message. - c:\users\agad069\desktop\logger\example2.py:46 - 2023-04-03 18:54:03 INFO: Info message. - c:\users\agad069\desktop\logger\example2.py:47 - 2023-04-03 18:54:03 WARNING: Warn message. - c:\users\agad069\desktop\logger\example2.py:48 - 2023-04-03 18:54:03 ERROR: Error message. - c:\users\agad069\desktop\logger\example2.py:49 - 2023-04-03 18:54:03 CRITICAL: Critical message. - c:\users\agad069\desktop\logger\example2.py:50 - -Consider clearing the handlers if necessary. - -.. code:: python - - logger.handlers.clear() - -Log to Both the Console and a File ----------------------------------- - -This is an example to create a single Logger associated with 2 handlers: - -1. A file handler. - -2. A stream handler. - -.. code:: python - - import logging - - level = logging.DEBUG - name = 'logfile.txt' - - logger = logging.getLogger(name) - logger.setLevel(level) - - file_handler = logging.FileHandler(name,'a+','utf-8') - file_handler.setLevel(logging.DEBUG) - file_format = logging.Formatter('%(asctime)s %(levelname)s: %(message)s - %(pathname)s:%(lineno)d', datefmt='%Y-%m-%d %H:%M:%S') - file_handler.setFormatter(file_format) - logger.addHandler(file_handler) - - console_handler = logging.StreamHandler() - console_handler.setLevel(logging.INFO) - console_format = logging.Formatter('%(message)s') - console_handler.setFormatter(console_format) - logger.addHandler(console_handler) - -When a log message is executed, then it is both printed to the console -and saved in the ``logfile.txt``. - -Consider clearing the handlers if necessary. - -.. code:: python - - logger.handlers.clear() - -PyGAD Example -------------- - -To use the logger in PyGAD, just create your custom logger and pass it -to the ``logger`` parameter. - -.. code:: python - - import logging - import pygad - import numpy - - level = logging.DEBUG - name = 'logfile.txt' - - logger = logging.getLogger(name) - logger.setLevel(level) - - file_handler = logging.FileHandler(name,'a+','utf-8') - file_handler.setLevel(logging.DEBUG) - file_format = logging.Formatter('%(asctime)s %(levelname)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S') - file_handler.setFormatter(file_format) - logger.addHandler(file_handler) - - console_handler = logging.StreamHandler() - console_handler.setLevel(logging.INFO) - console_format = logging.Formatter('%(message)s') - console_handler.setFormatter(console_format) - logger.addHandler(console_handler) - - equation_inputs = [4, -2, 8] - desired_output = 2671.1234 - - def fitness_func(ga_instance, solution, solution_idx): - output = numpy.sum(solution * equation_inputs) - fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) - return fitness - - def on_generation(ga_instance): - ga_instance.logger.info(f"Generation = {ga_instance.generations_completed}") - ga_instance.logger.info(f"Fitness = {ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness)[1]}") - - ga_instance = pygad.GA(num_generations=10, - sol_per_pop=40, - num_parents_mating=2, - keep_parents=2, - num_genes=len(equation_inputs), - fitness_func=fitness_func, - on_generation=on_generation, - logger=logger) - ga_instance.run() - - logger.handlers.clear() - -By executing this code, the logged messages are printed to the console -and also saved in the text file. - -.. code:: python - - 2023-04-03 19:04:27 INFO: Generation = 1 - 2023-04-03 19:04:27 INFO: Fitness = 0.00038086960368076276 - 2023-04-03 19:04:27 INFO: Generation = 2 - 2023-04-03 19:04:27 INFO: Fitness = 0.00038214871408010853 - 2023-04-03 19:04:27 INFO: Generation = 3 - 2023-04-03 19:04:27 INFO: Fitness = 0.0003832795907974678 - 2023-04-03 19:04:27 INFO: Generation = 4 - 2023-04-03 19:04:27 INFO: Fitness = 0.00038398612055017196 - 2023-04-03 19:04:27 INFO: Generation = 5 - 2023-04-03 19:04:27 INFO: Fitness = 0.00038442348890867516 - 2023-04-03 19:04:27 INFO: Generation = 6 - 2023-04-03 19:04:27 INFO: Fitness = 0.0003854406039137763 - 2023-04-03 19:04:27 INFO: Generation = 7 - 2023-04-03 19:04:27 INFO: Fitness = 0.00038646083174063284 - 2023-04-03 19:04:27 INFO: Generation = 8 - 2023-04-03 19:04:27 INFO: Fitness = 0.0003875169193024936 - 2023-04-03 19:04:27 INFO: Generation = 9 - 2023-04-03 19:04:27 INFO: Fitness = 0.0003888816727311021 - 2023-04-03 19:04:27 INFO: Generation = 10 - 2023-04-03 19:04:27 INFO: Fitness = 0.000389832593101348 - -Solve Non-Deterministic Problems -================================ - -PyGAD can be used to solve both deterministic and non-deterministic -problems. Deterministic are those that return the same fitness for the -same solution. For non-deterministic problems, a different fitness value -would be returned for the same solution. - -By default, PyGAD settings are set to solve deterministic problems. -PyGAD can save the explored solutions and their fitness to reuse in the -future. These instances attributes can save the solutions: - -1. ``solutions``: Exists if ``save_solutions=True``. - -2. ``best_solutions``: Exists if ``save_best_solutions=True``. - -3. ``last_generation_elitism``: Exists if ``keep_elitism`` > 0. - -4. ``last_generation_parents``: Exists if ``keep_parents`` > 0 or - ``keep_parents=-1``. - -To configure PyGAD for non-deterministic problems, we have to disable -saving the previous solutions. This is by setting these parameters: - -1. ``keep_elitism=0`` - -2. ``keep_parents=0`` - -3. ``keep_solutions=False`` - -4. ``keep_best_solutions=False`` - -.. code:: python - - import pygad - ... - ga_instance = pygad.GA(..., - keep_elitism=0, - keep_parents=0, - save_solutions=False, - save_best_solutions=False, - ...) - -This way PyGAD will not save any explored solution and thus the fitness -function have to be called for each individual solution. - -Reuse the Fitness instead of Calling the Fitness Function -========================================================= - -It may happen that a previously explored solution in generation X is -explored again in another generation Y (where Y > X). For some problems, -calling the fitness function takes much time. - -For deterministic problems, it is better to not call the fitness -function for an already explored solutions. Instead, reuse the fitness -of the old solution. PyGAD supports some options to help you save time -calling the fitness function for a previously explored solution. - -The parameters explored in this section can be set in the constructor of -the ``pygad.GA`` class. - -The ``cal_pop_fitness()`` method of the ``pygad.GA`` class checks these -parameters to see if there is a possibility of reusing the fitness -instead of calling the fitness function. - -.. _1-savesolutions: - -1. ``save_solutions`` ---------------------- - -It defaults to ``False``. If set to ``True``, then the population of -each generation is saved into the ``solutions`` attribute of the -``pygad.GA`` instance. In other words, every single solution is saved in -the ``solutions`` attribute. - -.. _2-savebestsolutions: - -2. ``save_best_solutions`` --------------------------- - -It defaults to ``False``. If ``True``, then it only saves the best -solution in every generation. - -.. _3-keepelitism: - -3. ``keep_elitism`` -------------------- - -It accepts an integer and defaults to 1. If set to a positive integer, -then it keeps the elitism of one generation available in the next -generation. - -.. _4-keepparents: - -4. ``keep_parents`` -------------------- - -It accepts an integer and defaults to -1. It set to ``-1`` or a positive -integer, then it keeps the parents of one generation available in the -next generation. - -Why the Fitness Function is not Called for Solution at Index 0? -=============================================================== - -PyGAD has a parameter called ``keep_elitism`` which defaults to 1. This -parameter defines the number of best solutions in generation **X** to -keep in the next generation **X+1**. The best solutions are just copied -from generation **X** to generation **X+1** without making any change. - -.. code:: python - - ga_instance = pygad.GA(..., - keep_elitism=1, - ...) - -The best solutions are copied at the beginning of the population. If -``keep_elitism=1``, this means the best solution in generation X is kept -in the next generation X+1 at index 0 of the population. If -``keep_elitism=2``, this means the 2 best solutions in generation X are -kept in the next generation X+1 at indices 0 and 1 of the population of -generation 1. - -Because the fitness of these best solutions are already calculated in -generation X, then their fitness values will not be recalculated at -generation X+1 (i.e. the fitness function will not be called for these -solutions again). Instead, their fitness values are just reused. This is -why you see that no solution with index 0 is passed to the fitness -function. - -To force calling the fitness function for each solution in every -generation, consider setting ``keep_elitism`` and ``keep_parents`` to 0. -Moreover, keep the 2 parameters ``save_solutions`` and -``save_best_solutions`` to their default value ``False``. - -.. code:: python - - ga_instance = pygad.GA(..., - keep_elitism=0, - keep_parents=0, - save_solutions=False, - save_best_solutions=False, - ...) - -Batch Fitness Calculation -========================= - -In `PyGAD -2.19.0 `__, -a new optional parameter called ``fitness_batch_size`` is supported. A -new optional parameter called ``fitness_batch_size`` is supported to -calculate the fitness function in batches. Thanks to `Linan -Qiu `__ for opening the `GitHub issue -#136 `__. - -Its values can be: - -- ``1`` or ``None``: If the ``fitness_batch_size`` parameter is - assigned the value ``1`` or ``None`` (default), then the normal flow - is used where the fitness function is called for each individual - solution. That is if there are 15 solutions, then the fitness - function is called 15 times. - -- ``1 < fitness_batch_size <= sol_per_pop``: If the - ``fitness_batch_size`` parameter is assigned a value satisfying this - condition ``1 < fitness_batch_size <= sol_per_pop``, then the - solutions are grouped into batches of size ``fitness_batch_size`` and - the fitness function is called once for each batch. In this case, the - fitness function must return a list/tuple/numpy.ndarray with a length - equal to the number of solutions passed. - -.. _example-without-fitnessbatchsize-parameter: - -Example without ``fitness_batch_size`` Parameter ------------------------------------------------- - -This is an example where the ``fitness_batch_size`` parameter is given -the value ``None`` (which is the default value). This is equivalent to -using the value ``1``. In this case, the fitness function will be called -for each solution. This means the fitness function ``fitness_func`` will -receive only a single solution. This is an example of the passed -arguments to the fitness function: - -.. code:: - - solution: [ 2.52860734, -0.94178795, 2.97545704, 0.84131987, -3.78447118, 2.41008358] - solution_idx: 3 - -The fitness function also must return a single numeric value as the -fitness for the passed solution. - -As we have a population of ``20`` solutions, then the fitness function -is called 20 times per generation. For 5 generations, then the fitness -function is called ``20*5 = 100`` times. In PyGAD, the fitness function -is called after the last generation too and this adds additional 20 -times. So, the total number of calls to the fitness function is -``20*5 + 20 = 120``. - -Note that the ``keep_elitism`` and ``keep_parents`` parameters are set -to ``0`` to make sure no fitness values are reused and to force calling -the fitness function for each individual solution. - -.. code:: python - - import pygad - import numpy - - function_inputs = [4,-2,3.5,5,-11,-4.7] - desired_output = 44 - - number_of_calls = 0 - - def fitness_func(ga_instance, solution, solution_idx): - global number_of_calls - number_of_calls = number_of_calls + 1 - output = numpy.sum(solution*function_inputs) - fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) - return fitness - - ga_instance = pygad.GA(num_generations=5, - num_parents_mating=10, - sol_per_pop=20, - fitness_func=fitness_func, - fitness_batch_size=None, - # fitness_batch_size=1, - num_genes=len(function_inputs), - keep_elitism=0, - keep_parents=0) - - ga_instance.run() - print(number_of_calls) - -.. code:: - - 120 - -.. _example-with-fitnessbatchsize-parameter: - -Example with ``fitness_batch_size`` Parameter ---------------------------------------------- - -This is an example where the ``fitness_batch_size`` parameter is used -and assigned the value ``4``. This means the solutions will be grouped -into batches of ``4`` solutions. The fitness function will be called -once for each patch (i.e. called once for each 4 solutions). - -This is an example of the arguments passed to it: - -.. code:: python - - solutions: - [[ 3.1129432 -0.69123589 1.93792414 2.23772968 -1.54616001 -0.53930799] - [ 3.38508121 0.19890812 1.93792414 2.23095014 -3.08955597 3.10194128] - [ 2.37079504 -0.88819803 2.97545704 1.41742256 -3.95594055 2.45028256] - [ 2.52860734 -0.94178795 2.97545704 0.84131987 -3.78447118 2.41008358]] - solutions_indices: - [16, 17, 18, 19] - -As we have 20 solutions, then there are ``20/4 = 5`` patches. As a -result, the fitness function is called only 5 times per generation -instead of 20. For each call to the fitness function, it receives a -batch of 4 solutions. - -As we have 5 generations, then the function will be called ``5*5 = 25`` -times. Given the call to the fitness function after the last generation, -then the total number of calls is ``5*5 + 5 = 30``. - -.. code:: python - - import pygad - import numpy - - function_inputs = [4,-2,3.5,5,-11,-4.7] - desired_output = 44 - - number_of_calls = 0 - - def fitness_func_batch(ga_instance, solutions, solutions_indices): - global number_of_calls - number_of_calls = number_of_calls + 1 - batch_fitness = [] - for solution in solutions: - output = numpy.sum(solution*function_inputs) - fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) - batch_fitness.append(fitness) - return batch_fitness - - ga_instance = pygad.GA(num_generations=5, - num_parents_mating=10, - sol_per_pop=20, - fitness_func=fitness_func_batch, - fitness_batch_size=4, - num_genes=len(function_inputs), - keep_elitism=0, - keep_parents=0) - - ga_instance.run() - print(number_of_calls) - -.. code:: - - 30 - -When batch fitness calculation is used, then we saved ``120 - 30 = 90`` -calls to the fitness function. - -Use Functions and Methods to Build Fitness and Callbacks -======================================================== - -In PyGAD 2.19.0, it is possible to pass user-defined functions or -methods to the following parameters: - -1. ``fitness_func`` - -2. ``on_start`` - -3. ``on_fitness`` - -4. ``on_parents`` - -5. ``on_crossover`` - -6. ``on_mutation`` - -7. ``on_generation`` - -8. ``on_stop`` - -This section gives 2 examples to assign these parameters user-defined: - -1. Functions. - -2. Methods. - -Assign Functions ----------------- - -This is a dummy example where the fitness function returns a random -value. Note that the instance of the ``pygad.GA`` class is passed as the -last parameter of all functions. - -.. code:: python - - import pygad - import numpy - - def fitness_func(ga_instanse, solution, solution_idx): - return numpy.random.rand() - - def on_start(ga_instanse): - print("on_start") - - def on_fitness(ga_instanse, last_gen_fitness): - print("on_fitness") - - def on_parents(ga_instanse, last_gen_parents): - print("on_parents") - - def on_crossover(ga_instanse, last_gen_offspring): - print("on_crossover") - - def on_mutation(ga_instanse, last_gen_offspring): - print("on_mutation") - - def on_generation(ga_instanse): - print("on_generation\n") - - def on_stop(ga_instanse, last_gen_fitness): - print("on_stop") - - ga_instance = pygad.GA(num_generations=5, - num_parents_mating=4, - sol_per_pop=10, - num_genes=2, - on_start=on_start, - on_fitness=on_fitness, - on_parents=on_parents, - on_crossover=on_crossover, - on_mutation=on_mutation, - on_generation=on_generation, - on_stop=on_stop, - fitness_func=fitness_func) - - ga_instance.run() - -Assign Methods --------------- - -The next example has all the method defined inside the class ``Test``. -All of the methods accept an additional parameter representing the -method's object of the class ``Test``. - -All methods accept ``self`` as the first parameter and the instance of -the ``pygad.GA`` class as the last parameter. - -.. code:: python - - import pygad - import numpy - - class Test: - def fitness_func(self, ga_instanse, solution, solution_idx): - return numpy.random.rand() - - def on_start(self, ga_instanse): - print("on_start") - - def on_fitness(self, ga_instanse, last_gen_fitness): - print("on_fitness") - - def on_parents(self, ga_instanse, last_gen_parents): - print("on_parents") - - def on_crossover(self, ga_instanse, last_gen_offspring): - print("on_crossover") - - def on_mutation(self, ga_instanse, last_gen_offspring): - print("on_mutation") - - def on_generation(self, ga_instanse): - print("on_generation\n") - - def on_stop(self, ga_instanse, last_gen_fitness): - print("on_stop") - - ga_instance = pygad.GA(num_generations=5, - num_parents_mating=4, - sol_per_pop=10, - num_genes=2, - on_start=Test().on_start, - on_fitness=Test().on_fitness, - on_parents=Test().on_parents, - on_crossover=Test().on_crossover, - on_mutation=Test().on_mutation, - on_generation=Test().on_generation, - on_stop=Test().on_stop, - fitness_func=Test().fitness_func) - - ga_instance.run() +More About PyGAD +================ + +Multi-Objective Optimization +============================ + +In `PyGAD +3.2.0 `__, +the library supports multi-objective optimization using the +non-dominated sorting genetic algorithm II (NSGA-II). The code is +exactly similar to the regular code used for single-objective +optimization except for 1 difference. It is the return value of the +fitness function. + +In single-objective optimization, the fitness function returns a single +numeric value. In this example, the variable ``fitness`` is expected to +be a numeric value. + +.. code:: python + + def fitness_func(ga_instance, solution, solution_idx): + ... + return fitness + +But in multi-objective optimization, the fitness function returns any of +these data types: + +1. ``list`` + +2. ``tuple`` + +3. ``numpy.ndarray`` + +.. code:: python + + def fitness_func(ga_instance, solution, solution_idx): + ... + return [fitness1, fitness2, ..., fitnessN] + +Whenever the fitness function returns an iterable of these data types, +then the problem is considered multi-objective. This holds even if there +is a single element in the returned iterable. + +Other than the fitness function, everything else could be the same in +both single and multi-objective problems. + +But it is recommended to use one of these 2 parent selection operators +to solve multi-objective problems: + +1. ``nsga2``: This selects the parents based on non-dominated sorting + and crowding distance. + +2. ``tournament_nsga2``: This selects the parents using tournament + selection which uses non-dominated sorting and crowding distance to + rank the solutions. + +This is a multi-objective optimization example that optimizes these 2 +linear functions: + +1. ``y1 = f(w1:w6) = w1x1 + w2x2 + w3x3 + w4x4 + w5x5 + 6wx6`` + +2. ``y2 = f(w1:w6) = w1x7 + w2x8 + w3x9 + w4x10 + w5x11 + 6wx12`` + +Where: + +1. ``(x1,x2,x3,x4,x5,x6)=(4,-2,3.5,5,-11,-4.7)`` and ``y=50`` + +2. ``(x7,x8,x9,x10,x11,x12)=(-2,0.7,-9,1.4,3,5)`` and ``y=30`` + +The 2 functions use the same parameters (weights) ``w1`` to ``w6``. + +The goal is to use PyGAD to find the optimal values for such weights +that satisfy the 2 functions ``y1`` and ``y2``. + +.. code:: python + + import pygad + import numpy + + """ + Given these 2 functions: + y1 = f(w1:w6) = w1x1 + w2x2 + w3x3 + w4x4 + w5x5 + 6wx6 + y2 = f(w1:w6) = w1x7 + w2x8 + w3x9 + w4x10 + w5x11 + 6wx12 + where (x1,x2,x3,x4,x5,x6)=(4,-2,3.5,5,-11,-4.7) and y=50 + and (x7,x8,x9,x10,x11,x12)=(-2,0.7,-9,1.4,3,5) and y=30 + What are the best values for the 6 weights (w1 to w6)? We are going to use the genetic algorithm to optimize these 2 functions. + This is a multi-objective optimization problem. + + PyGAD considers the problem as multi-objective if the fitness function returns: + 1) List. + 2) Or tuple. + 3) Or numpy.ndarray. + """ + + function_inputs1 = [4,-2,3.5,5,-11,-4.7] # Function 1 inputs. + function_inputs2 = [-2,0.7,-9,1.4,3,5] # Function 2 inputs. + desired_output1 = 50 # Function 1 output. + desired_output2 = 30 # Function 2 output. + + def fitness_func(ga_instance, solution, solution_idx): + output1 = numpy.sum(solution*function_inputs1) + output2 = numpy.sum(solution*function_inputs2) + fitness1 = 1.0 / (numpy.abs(output1 - desired_output1) + 0.000001) + fitness2 = 1.0 / (numpy.abs(output2 - desired_output2) + 0.000001) + return [fitness1, fitness2] + + num_generations = 100 + num_parents_mating = 10 + + sol_per_pop = 20 + num_genes = len(function_inputs1) + + ga_instance = pygad.GA(num_generations=num_generations, + num_parents_mating=num_parents_mating, + sol_per_pop=sol_per_pop, + num_genes=num_genes, + fitness_func=fitness_func, + parent_selection_type='nsga2') + + ga_instance.run() + + ga_instance.plot_fitness(label=['Obj 1', 'Obj 2']) + + 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}") + + prediction = numpy.sum(numpy.array(function_inputs1)*solution) + print(f"Predicted output 1 based on the best solution : {prediction}") + prediction = numpy.sum(numpy.array(function_inputs2)*solution) + print(f"Predicted output 2 based on the best solution : {prediction}") + +This is the result of the print statements. The predicted outputs are +close to the desired outputs. + +.. code:: + + Parameters of the best solution : [ 0.79676439 -2.98823386 -4.12677662 5.70539445 -2.02797016 -1.07243922] + Fitness value of the best solution = [ 1.68090829 349.8591915 ] + Predicted output 1 based on the best solution : 50.59491545442283 + Predicted output 2 based on the best solution : 29.99714270722312 + +This is the figure created by the ``plot_fitness()`` method. The fitness +of the first objective has the green color. The blue color is used for +the second objective fitness. + +|image1| + +.. _limit-the-gene-value-range-using-the-genespace-parameter: + +Limit the Gene Value Range using the ``gene_space`` Parameter +============================================================= + +In `PyGAD +2.11.0 `__, +the ``gene_space`` parameter supported a new feature to allow +customizing the range of accepted values for each gene. Let's take a +quick review of the ``gene_space`` parameter to build over it. + +The ``gene_space`` parameter allows the user to feed the space of values +of each gene. This way the accepted values for each gene is retracted to +the user-defined values. Assume there is a problem that has 3 genes +where each gene has different set of values as follows: + +1. Gene 1: ``[0.4, 12, -5, 21.2]`` + +2. Gene 2: ``[-2, 0.3]`` + +3. Gene 3: ``[1.2, 63.2, 7.4]`` + +Then, the ``gene_space`` for this problem is as given below. Note that +the order is very important. + +.. code:: python + + gene_space = [[0.4, 12, -5, 21.2], + [-2, 0.3], + [1.2, 63.2, 7.4]] + +In case all genes share the same set of values, then simply feed a +single list to the ``gene_space`` parameter as follows. In this case, +all genes can only take values from this list of 6 values. + +.. code:: python + + gene_space = [33, 7, 0.5, 95. 6.3, 0.74] + +The previous example restricts the gene values to just a set of fixed +number of discrete values. In case you want to use a range of discrete +values to the gene, then you can use the ``range()`` function. For +example, ``range(1, 7)`` means the set of allowed values for the gene +are ``1, 2, 3, 4, 5, and 6``. You can also use the ``numpy.arange()`` or +``numpy.linspace()`` functions for the same purpose. + +The previous discussion only works with a range of discrete values not +continuous values. In `PyGAD +2.11.0 `__, +the ``gene_space`` parameter can be assigned a dictionary that allows +the gene to have values from a continuous range. + +Assuming you want to restrict the gene within this half-open range [1 to +5) where 1 is included and 5 is not. Then simply create a dictionary +with 2 items where the keys of the 2 items are: + +1. ``'low'``: The minimum value in the range which is 1 in the example. + +2. ``'high'``: The maximum value in the range which is 5 in the example. + +The dictionary will look like that: + +.. code:: python + + {'low': 1, + 'high': 5} + +It is not acceptable to add more than 2 items in the dictionary or use +other keys than ``'low'`` and ``'high'``. + +For a 3-gene problem, the next code creates a dictionary for each gene +to restrict its values in a continuous range. For the first gene, it can +take any floating-point value from the range that starts from 1 +(inclusive) and ends at 5 (exclusive). + +.. code:: python + + gene_space = [{'low': 1, 'high': 5}, {'low': 0.3, 'high': 1.4}, {'low': -0.2, 'high': 4.5}] + +.. _more-about-the-genespace-parameter: + +More about the ``gene_space`` Parameter +======================================= + +The ``gene_space`` parameter customizes the space of values of each +gene. + +Assuming that all genes have the same global space which include the +values 0.3, 5.2, -4, and 8, then those values can be assigned to the +``gene_space`` parameter as a list, tuple, or range. Here is a list +assigned to this parameter. By doing that, then the gene values are +restricted to those assigned to the ``gene_space`` parameter. + +.. code:: python + + gene_space = [0.3, 5.2, -4, 8] + +If some genes have different spaces, then ``gene_space`` should accept a +nested list or tuple. In this case, the elements could be: + +1. Number (of ``int``, ``float``, or ``NumPy`` data types): A single + value to be assigned to the gene. This means this gene will have the + same value across all generations. + +2. ``list``, ``tuple``, ``numpy.ndarray``, or any range like ``range``, + ``numpy.arange()``, or ``numpy.linspace``: It holds the space for + each individual gene. But this space is usually discrete. That is + there is a set of finite values to select from. + +3. ``dict``: To sample a value for a gene from a continuous range. The + dictionary must have 2 mandatory keys which are ``"low"`` and + ``"high"`` in addition to an optional key which is ``"step"``. A + random value is returned between the values assigned to the items + with ``"low"`` and ``"high"`` keys. If the ``"step"`` exists, then + this works as the previous options (i.e. discrete set of values). + +4. ``None``: A gene with its space set to ``None`` is initialized + randomly from the range specified by the 2 parameters + ``init_range_low`` and ``init_range_high``. For mutation, its value + is mutated based on a random value from the range specified by the 2 + parameters ``random_mutation_min_val`` and + ``random_mutation_max_val``. If all elements in the ``gene_space`` + parameter are ``None``, the parameter will not have any effect. + +Assuming that a chromosome has 2 genes and each gene has a different +value space. Then the ``gene_space`` could be assigned a nested +list/tuple where each element determines the space of a gene. + +According to the next code, the space of the first gene is ``[0.4, -5]`` +which has 2 values and the space for the second gene is +``[0.5, -3.2, 8.8, -9]`` which has 4 values. + +.. code:: python + + gene_space = [[0.4, -5], [0.5, -3.2, 8.2, -9]] + +For a 2 gene chromosome, if the first gene space is restricted to the +discrete values from 0 to 4 and the second gene is restricted to the +values from 10 to 19, then it could be specified according to the next +code. + +.. code:: python + + gene_space = [range(5), range(10, 20)] + +The ``gene_space`` can also be assigned to a single range, as given +below, where the values of all genes are sampled from the same range. + +.. code:: python + + gene_space = numpy.arange(15) + +The ``gene_space`` can be assigned a dictionary to sample a value from a +continuous range. + +.. code:: python + + gene_space = {"low": 4, "high": 30} + +A step also can be assigned to the dictionary. This works as if a range +is used. + +.. code:: python + + gene_space = {"low": 4, "high": 30, "step": 2.5} + +.. + + Setting a ``dict`` like ``{"low": 0, "high": 10}`` in the + ``gene_space`` means that random values from the continuous range [0, + 10) are sampled. Note that ``0`` is included but ``10`` is not + included while sampling. Thus, the maximum value that could be + returned is less than ``10`` like ``9.9999``. But if the user decided + to round the genes using, for example, ``[float, 2]``, then this + value will become 10. So, the user should be careful to the inputs. + +If a ``None`` is assigned to only a single gene, then its value will be +randomly generated initially using the ``init_range_low`` and +``init_range_high`` parameters in the ``pygad.GA`` class's constructor. +During mutation, the value are sampled from the range defined by the 2 +parameters ``random_mutation_min_val`` and ``random_mutation_max_val``. +This is an example where the second gene is given a ``None`` value. + +.. code:: python + + gene_space = [range(5), None, numpy.linspace(10, 20, 300)] + +If the user did not assign the initial population to the +``initial_population`` parameter, the initial population is created +randomly based on the ``gene_space`` parameter. Moreover, the mutation +is applied based on this parameter. + +.. _how-mutation-works-with-the-genespace-parameter: + +How Mutation Works with the ``gene_space`` Parameter? +----------------------------------------------------- + +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 +value out of these 3 values. + +.. code:: python + + Gene space: [[1, 2, 3], + None] + Solution: [1, 5] + +For a solution like ``[1, 5]``, then mutation happens for the first gene +by simply replacing its current value by a randomly selected value +(other than its current value if possible). So, the value 1 will be +replaced by either 2 or 3. + +For the second gene, its space is set to ``None``. So, traditional +mutation happens for this gene by: + +1. Generating a random value from the range defined by the + ``random_mutation_min_val`` and ``random_mutation_max_val`` + parameters. + +2. Adding this random value to the current gene's value. + +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 +====================== + +In `PyGAD +2.4.0 `__, +it is possible to stop the genetic algorithm after any generation. All +you need to do it to return the string ``"stop"`` in the callback +function ``on_generation``. When this callback function is implemented +and assigned to the ``on_generation`` parameter in the constructor of +the ``pygad.GA`` class, then the algorithm immediately stops after +completing its current generation. Let's discuss an example. + +Assume that the user wants to stop algorithm either after the 100 +generations or if a condition is met. The user may assign a value of 100 +to the ``num_generations`` parameter of the ``pygad.GA`` class +constructor. + +The condition that stops the algorithm is written in a callback function +like the one in the next code. If the fitness value of the best solution +exceeds 70, then the string ``"stop"`` is returned. + +.. code:: python + + def func_generation(ga_instance): + if ga_instance.best_solution()[1] >= 70: + return "stop" + +Stop Criteria +============= + +In `PyGAD +2.15.0 `__, +a new parameter named ``stop_criteria`` is added to the constructor of +the ``pygad.GA`` class. It helps to stop the evolution based on some +criteria. It can be assigned to one or more criterion. + +Each criterion is passed as ``str`` that consists of 2 parts: + +1. Stop word. + +2. Number. + +It takes this form: + +.. code:: python + + "word_num" + +The current 2 supported words are ``reach`` and ``saturate``. + +The ``reach`` word stops the ``run()`` method if the fitness value is +equal to or greater than a given fitness value. An example for ``reach`` +is ``"reach_40"`` which stops the evolution if the fitness is >= 40. + +``saturate`` stops the evolution if the fitness saturates for a given +number of consecutive generations. An example for ``saturate`` is +``"saturate_7"`` which means stop the ``run()`` method if the fitness +does not change for 7 consecutive generations. + +Here is an example that stops the evolution if either the fitness value +reached ``127.4`` or if the fitness saturates for ``15`` generations. + +.. code:: python + + import pygad + import numpy + + equation_inputs = [4, -2, 3.5, 8, 9, 4] + desired_output = 44 + + def fitness_func(ga_instance, solution, solution_idx): + output = numpy.sum(solution * equation_inputs) + + fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) + + return fitness + + ga_instance = pygad.GA(num_generations=200, + sol_per_pop=10, + num_parents_mating=4, + num_genes=len(equation_inputs), + fitness_func=fitness_func, + stop_criteria=["reach_127.4", "saturate_15"]) + + ga_instance.run() + print(f"Number of generations passed is {ga_instance.generations_completed}") + +Multi-Objective Stop Criteria +----------------------------- + +When multi-objective is used, then there are 2 options to use the +``stop_criteria`` parameter with the ``reach`` keyword: + +1. Pass a single value to use along the ``reach`` keyword to use across + all the objectives. + +2. Pass multiple values along the ``reach`` keyword. But the number of + values must equal the number of objectives. + +For the ``saturate`` keyword, it is independent to the number of +objectives. + +Suppose there are 3 objectives, this is a working example. It stops when +the fitness value of the 3 objectives reach or exceed 10, 20, and 30, +respectively. + +.. code:: python + + stop_criteria='reach_10_20_30' + +More than one criterion can be used together. In this case, pass the +``stop_criteria`` parameter as an iterable. This is an example. It stops +when either of these 2 conditions hold: + +1. The fitness values of the 3 objectives reach or exceed 10, 20, and + 30, respectively. + +2. The fitness values of the 3 objectives reach or exceed 90, -5.7, and + 10, respectively. + +.. code:: python + + stop_criteria=['reach_10_20_30', 'reach_90_-5.7_10'] + +Elitism Selection +================= + +In `PyGAD +2.18.0 `__, +a new parameter called ``keep_elitism`` is supported. It accepts an +integer to define the number of elitism (i.e. best solutions) to keep in +the next generation. This parameter defaults to ``1`` which means only +the best solution is kept in the next generation. + +In the next example, the ``keep_elitism`` parameter in the constructor +of the ``pygad.GA`` class is set to 2. Thus, the best 2 solutions in +each generation are kept in the next generation. + +.. code:: python + + import numpy + import pygad + + function_inputs = [4,-2,3.5,5,-11,-4.7] + desired_output = 44 + + def fitness_func(ga_instance, solution, solution_idx): + output = numpy.sum(solution*function_inputs) + fitness = 1.0 / numpy.abs(output - desired_output) + return fitness + + ga_instance = pygad.GA(num_generations=2, + num_parents_mating=3, + fitness_func=fitness_func, + num_genes=6, + sol_per_pop=5, + keep_elitism=2) + + ga_instance.run() + +The value passed to the ``keep_elitism`` parameter must satisfy 2 +conditions: + +1. It must be ``>= 0``. + +2. It must be ``<= sol_per_pop``. That is its value cannot exceed the + number of solutions in the current population. + +In the previous example, if the ``keep_elitism`` parameter is set equal +to the value passed to the ``sol_per_pop`` parameter, which is 5, then +there will be no evolution at all as in the next figure. This is because +all the 5 solutions are used as elitism in the next generation and no +offspring will be created. + +.. code:: python + + ... + + ga_instance = pygad.GA(..., + sol_per_pop=5, + keep_elitism=5) + + ga_instance.run() + +|image2| + +Note that if the ``keep_elitism`` parameter is effective (i.e. is +assigned a positive integer, not zero), then the ``keep_parents`` +parameter will have no effect. Because the default value of the +``keep_elitism`` parameter is 1, then the ``keep_parents`` parameter has +no effect by default. The ``keep_parents`` parameter is only effective +when ``keep_elitism=0``. + +Random Seed +=========== + +In `PyGAD +2.18.0 `__, +a new parameter called ``random_seed`` is supported. Its value is used +as a seed for the random function generators. + +PyGAD uses random functions in these 2 libraries: + +1. NumPy + +2. random + +The ``random_seed`` parameter defaults to ``None`` which means no seed +is used. As a result, different random numbers are generated for each +run of PyGAD. + +If this parameter is assigned a proper seed, then the results will be +reproducible. In the next example, the integer 2 is used as a random +seed. + +.. code:: python + + import numpy + import pygad + + function_inputs = [4,-2,3.5,5,-11,-4.7] + desired_output = 44 + + def fitness_func(ga_instance, solution, solution_idx): + output = numpy.sum(solution*function_inputs) + fitness = 1.0 / numpy.abs(output - desired_output) + return fitness + + ga_instance = pygad.GA(num_generations=2, + num_parents_mating=3, + fitness_func=fitness_func, + sol_per_pop=5, + num_genes=6, + random_seed=2) + + ga_instance.run() + best_solution, best_solution_fitness, best_match_idx = ga_instance.best_solution() + print(best_solution) + print(best_solution_fitness) + +This is the best solution found and its fitness value. + +.. code:: + + [ 2.77249188 -4.06570662 0.04196872 -3.47770796 -0.57502138 -3.22775267] + 0.04872203136549972 + +After running the code again, it will find the same result. + +.. code:: + + [ 2.77249188 -4.06570662 0.04196872 -3.47770796 -0.57502138 -3.22775267] + 0.04872203136549972 + +Continue without Losing Progress +================================ + +In `PyGAD +2.18.0 `__, +and thanks for `Felix Bernhard `__ for +opening `this GitHub +issue `__, +the values of these 4 instance attributes are no longer reset after each +call to the ``run()`` method. + +1. ``self.best_solutions`` + +2. ``self.best_solutions_fitness`` + +3. ``self.solutions`` + +4. ``self.solutions_fitness`` + +This helps the user to continue where the last run stopped without +losing the values of these 4 attributes. + +Now, the user can save the model by calling the ``save()`` method. + +.. code:: python + + import pygad + + def fitness_func(ga_instance, solution, solution_idx): + ... + return fitness + + ga_instance = pygad.GA(...) + + ga_instance.run() + + ga_instance.plot_fitness() + + ga_instance.save("pygad_GA") + +Then the saved model is loaded by calling the ``load()`` function. After +calling the ``run()`` method over the loaded instance, then the data +from the previous 4 attributes are not reset but extended with the new +data. + +.. code:: python + + import pygad + + def fitness_func(ga_instance, solution, solution_idx): + ... + return fitness + + loaded_ga_instance = pygad.load("pygad_GA") + + loaded_ga_instance.run() + + loaded_ga_instance.plot_fitness() + +The plot created by the ``plot_fitness()`` method will show the data +collected from both the runs. + +Note that the 2 attributes (``self.best_solutions`` and +``self.best_solutions_fitness``) only work if the +``save_best_solutions`` parameter is set to ``True``. Also, the 2 +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 +================================= + +In `PyGAD +2.13.0 `__, +a new bool parameter called ``allow_duplicate_genes`` is supported to +control whether duplicates are supported in the chromosome or not. In +other words, whether 2 or more genes might have the same exact value. + +If ``allow_duplicate_genes=True`` (which is the default case), genes may +have the same value. If ``allow_duplicate_genes=False``, then no 2 genes +will have the same value given that there are enough unique values for +the genes. + +The next code gives an example to use the ``allow_duplicate_genes`` +parameter. A callback generation function is implemented to print the +population after each generation. + +.. code:: python + + import pygad + + def fitness_func(ga_instance, solution, solution_idx): + return 0 + + def on_generation(ga): + print("Generation", ga.generations_completed) + print(ga.population) + + ga_instance = pygad.GA(num_generations=5, + sol_per_pop=5, + num_genes=4, + mutation_num_genes=3, + random_mutation_min_val=-5, + random_mutation_max_val=5, + num_parents_mating=2, + fitness_func=fitness_func, + gene_type=int, + on_generation=on_generation, + allow_duplicate_genes=False) + ga_instance.run() + +Here are the population after the 5 generations. Note how there are no +duplicate values. + +.. code:: python + + Generation 1 + [[ 2 -2 -3 3] + [ 0 1 2 3] + [ 5 -3 6 3] + [-3 1 -2 4] + [-1 0 -2 3]] + Generation 2 + [[-1 0 -2 3] + [-3 1 -2 4] + [ 0 -3 -2 6] + [-3 0 -2 3] + [ 1 -4 2 4]] + Generation 3 + [[ 1 -4 2 4] + [-3 0 -2 3] + [ 4 0 -2 1] + [-4 0 -2 -3] + [-4 2 0 3]] + Generation 4 + [[-4 2 0 3] + [-4 0 -2 -3] + [-2 5 4 -3] + [-1 2 -4 4] + [-4 2 0 -3]] + Generation 5 + [[-4 2 0 -3] + [-1 2 -4 4] + [ 3 4 -4 0] + [-1 0 2 -2] + [-4 2 -1 1]] + +The ``allow_duplicate_genes`` parameter is configured with use with the +``gene_space`` parameter. Here is an example where each of the 4 genes +has the same space of values that consists of 4 values (1, 2, 3, and 4). + +.. code:: python + + import pygad + + def fitness_func(ga_instance, solution, solution_idx): + return 0 + + def on_generation(ga): + print("Generation", ga.generations_completed) + print(ga.population) + + ga_instance = pygad.GA(num_generations=1, + sol_per_pop=5, + num_genes=4, + num_parents_mating=2, + fitness_func=fitness_func, + gene_type=int, + gene_space=[[1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4]], + on_generation=on_generation, + allow_duplicate_genes=False) + ga_instance.run() + +Even that all the genes share the same space of values, no 2 genes +duplicate their values as provided by the next output. + +.. code:: python + + Generation 1 + [[2 3 1 4] + [2 3 1 4] + [2 4 1 3] + [2 3 1 4] + [1 3 2 4]] + Generation 2 + [[1 3 2 4] + [2 3 1 4] + [1 3 2 4] + [2 3 4 1] + [1 3 4 2]] + Generation 3 + [[1 3 4 2] + [2 3 4 1] + [1 3 4 2] + [3 1 4 2] + [3 2 4 1]] + Generation 4 + [[3 2 4 1] + [3 1 4 2] + [3 2 4 1] + [1 2 4 3] + [1 3 4 2]] + Generation 5 + [[1 3 4 2] + [1 2 4 3] + [2 1 4 3] + [1 2 4 3] + [1 2 4 3]] + +You should care of giving enough values for the genes so that PyGAD is +able to find alternatives for the gene value in case it duplicates with +another gene. + +There might be 2 duplicate genes where changing either of the 2 +duplicating genes will not solve the problem. For example, if +``gene_space=[[3, 0, 1], [4, 1, 2], [0, 2], [3, 2, 0]]`` and the +solution is ``[3 2 0 0]``, then the values of the last 2 genes +duplicate. There are no possible changes in the last 2 genes to solve +the problem. + +This problem can be solved by randomly changing one of the +non-duplicating genes that may make a room for a unique value in one the +2 duplicating genes. For example, by changing the second gene from 2 to +4, then any of the last 2 genes can take the value 2 and solve the +duplicates. The resultant gene is then ``[3 4 2 0]``. But this option is +not yet supported in PyGAD. + +Solve Duplicates using a Third Gene +----------------------------------- + +When ``allow_duplicate_genes=False`` and a user-defined ``gene_space`` +is used, it sometimes happen that there is no room to solve the +duplicates between the 2 genes by simply replacing the value of one gene +by another gene. In `PyGAD +3.1.0 `__, +the duplicates are solved by looking for a third gene that will help in +solving the duplicates. The following examples explain how it works. + +Example 1: + +Let's assume that this gene space is used and there is a solution with 2 +duplicate genes with the same value 4. + +.. code:: python + + Gene space: [[2, 3], + [3, 4], + [4, 5], + [5, 6]] + Solution: [3, 4, 4, 5] + +By checking the gene space, the second gene can have the values +``[3, 4]`` and the third gene can have the values ``[4, 5]``. To solve +the duplicates, we have the value of any of these 2 genes. + +If the value of the second gene changes from 4 to 3, then it will be +duplicate with the first gene. If we are to change the value of the +third gene from 4 to 5, then it will duplicate with the fourth gene. As +a conclusion, trying to just selecting a different gene value for either +the second or third genes will introduce new duplicating genes. + +When there are 2 duplicate genes but there is no way to solve their +duplicates, then the solution is to change a third gene that makes a +room to solve the duplicates between the 2 genes. + +In our example, duplicates between the second and third genes can be +solved by, for example,: + +- Changing the first gene from 3 to 2 then changing the second gene from + 4 to 3. + +- Or changing the fourth gene from 5 to 6 then changing the third gene + from 4 to 5. + +Generally, this is how to solve such duplicates: + +1. For any duplicate gene **GENE1**, select another value. + +2. Check which other gene **GENEX** has duplicate with this new value. + +3. Find if **GENEX** can have another value that will not cause any more + duplicates. If so, go to step 7. + +4. If all the other values of **GENEX** will cause duplicates, then try + another gene **GENEY**. + +5. Repeat steps 3 and 4 until exploring all the genes. + +6. If there is no possibility to solve the duplicates, then there is not + way to solve the duplicates and we have to keep the duplicate value. + +7. If a value for a gene **GENEM** is found that will not cause more + duplicates, then use this value for the gene **GENEM**. + +8. Replace the value of the gene **GENE1** by the old value of the gene + **GENEM**. This solves the duplicates. + +This is an example to solve the duplicate for the solution +``[3, 4, 4, 5]``: + +1. Let's use the second gene with value 4. Because the space of this + gene is ``[3, 4]``, then the only other value we can select is 3. + +2. The first gene also have the value 3. + +3. The first gene has another value 2 that will not cause more + duplicates in the solution. Then go to step 7. + +4. Skip. + +5. Skip. + +6. Skip. + +7. The value of the first gene 3 will be replaced by the new value 2. + The new solution is [2, 4, 4, 5]. + +8. Replace the value of the second gene 4 by the old value of the first + gene which is 3. The new solution is [2, 3, 4, 5]. The duplicate is + solved. + +Example 2: + +.. code:: python + + Gene space: [[0, 1], + [1, 2], + [2, 3], + [3, 4]] + Solution: [1, 2, 2, 3] + +The quick summary is: + +- Change the value of the first gene from 1 to 0. The solution becomes + [0, 2, 2, 3]. + +- Change the value of the second gene from 2 to 1. The solution becomes + [0, 1, 2, 3]. The duplicate is solved. + +.. _more-about-the-genetype-parameter: + +More about the ``gene_type`` Parameter +====================================== + +The ``gene_type`` parameter allows the user to control the data type for +all genes at once or each individual gene. In `PyGAD +2.15.0 `__, +the ``gene_type`` parameter also supports customizing the precision for +``float`` data types. As a result, the ``gene_type`` parameter helps to: + +1. Select a data type for all genes with or without precision. + +2. Select a data type for each individual gene with or without + precision. + +Let's discuss things by examples. + +Data Type for All Genes without Precision +----------------------------------------- + +The data type for all genes can be specified by assigning the numeric +data type directly to the ``gene_type`` parameter. This is an example to +make all genes of ``int`` data types. + +.. code:: python + + gene_type=int + +Given that the supported numeric data types of PyGAD include Python's +``int`` and ``float`` in addition to all numeric types of ``NumPy``, +then any of these types can be assigned to the ``gene_type`` parameter. + +If no precision is specified for a ``float`` data type, then the +complete floating-point number is kept. + +The next code uses an ``int`` data type for all genes where the genes in +the initial and final population are only integers. + +.. code:: python + + import pygad + import numpy + + equation_inputs = [4, -2, 3.5, 8, -2] + desired_output = 2671.1234 + + def fitness_func(ga_instance, solution, solution_idx): + output = numpy.sum(solution * equation_inputs) + fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) + return fitness + + ga_instance = pygad.GA(num_generations=10, + sol_per_pop=5, + num_parents_mating=2, + num_genes=len(equation_inputs), + fitness_func=fitness_func, + gene_type=int) + + print("Initial Population") + print(ga_instance.initial_population) + + ga_instance.run() + + print("Final Population") + print(ga_instance.population) + +.. code:: python + + Initial Population + [[ 1 -1 2 0 -3] + [ 0 -2 0 -3 -1] + [ 0 -1 -1 2 0] + [-2 3 -2 3 3] + [ 0 0 2 -2 -2]] + + Final Population + [[ 1 -1 2 2 0] + [ 1 -1 2 2 0] + [ 1 -1 2 2 0] + [ 1 -1 2 2 0] + [ 1 -1 2 2 0]] + +Data Type for All Genes with Precision +-------------------------------------- + +A precision can only be specified for a ``float`` data type and cannot +be specified for integers. Here is an example to use a precision of 3 +for the ``float`` data type. In this case, all genes are of type +``float`` and their maximum precision is 3. + +.. code:: python + + gene_type=[float, 3] + +The next code uses prints the initial and final population where the +genes are of type ``float`` with precision 3. + +.. code:: python + + import pygad + import numpy + + equation_inputs = [4, -2, 3.5, 8, -2] + desired_output = 2671.1234 + + def fitness_func(ga_instance, solution, solution_idx): + output = numpy.sum(solution * equation_inputs) + fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) + + return fitness + + ga_instance = pygad.GA(num_generations=10, + sol_per_pop=5, + num_parents_mating=2, + num_genes=len(equation_inputs), + fitness_func=fitness_func, + gene_type=[float, 3]) + + print("Initial Population") + print(ga_instance.initial_population) + + ga_instance.run() + + print("Final Population") + print(ga_instance.population) + +.. code:: python + + Initial Population + [[-2.417 -0.487 3.623 2.457 -2.362] + [-1.231 0.079 -1.63 1.629 -2.637] + [ 0.692 -2.098 0.705 0.914 -3.633] + [ 2.637 -1.339 -1.107 -0.781 -3.896] + [-1.495 1.378 -1.026 3.522 2.379]] + + Final Population + [[ 1.714 -1.024 3.623 3.185 -2.362] + [ 0.692 -1.024 3.623 3.185 -2.362] + [ 0.692 -1.024 3.623 3.375 -2.362] + [ 0.692 -1.024 4.041 3.185 -2.362] + [ 1.714 -0.644 3.623 3.185 -2.362]] + +Data Type for each Individual Gene without Precision +---------------------------------------------------- + +In `PyGAD +2.14.0 `__, +the ``gene_type`` parameter allows customizing the gene type for each +individual gene. This is by using a ``list``/``tuple``/``numpy.ndarray`` +with number of elements equal to the number of genes. For each element, +a type is specified for the corresponding gene. + +This is an example for a 5-gene problem where different types are +assigned to the genes. + +.. code:: python + + gene_type=[int, float, numpy.float16, numpy.int8, float] + +This is a complete code that prints the initial and final population for +a custom-gene data type. + +.. code:: python + + import pygad + import numpy + + equation_inputs = [4, -2, 3.5, 8, -2] + desired_output = 2671.1234 + + def fitness_func(ga_instance, solution, solution_idx): + output = numpy.sum(solution * equation_inputs) + fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) + return fitness + + ga_instance = pygad.GA(num_generations=10, + sol_per_pop=5, + num_parents_mating=2, + num_genes=len(equation_inputs), + fitness_func=fitness_func, + gene_type=[int, float, numpy.float16, numpy.int8, float]) + + print("Initial Population") + print(ga_instance.initial_population) + + ga_instance.run() + + print("Final Population") + print(ga_instance.population) + +.. code:: python + + Initial Population + [[0 0.8615522360026828 0.7021484375 -2 3.5301821368185866] + [-3 2.648189378595294 -3.830078125 1 -0.9586271572917742] + [3 3.7729827570110714 1.2529296875 -3 1.395741994211889] + [0 1.0490687178053282 1.51953125 -2 0.7243617940450235] + [0 -0.6550158436937226 -2.861328125 -2 1.8212734549263097]] + + Final Population + [[3 3.7729827570110714 2.055 0 0.7243617940450235] + [3 3.7729827570110714 1.458 0 -0.14638754050305036] + [3 3.7729827570110714 1.458 0 0.0869406120516778] + [3 3.7729827570110714 1.458 0 0.7243617940450235] + [3 3.7729827570110714 1.458 0 -0.14638754050305036]] + +Data Type for each Individual Gene with Precision +------------------------------------------------- + +The precision can also be specified for the ``float`` data types as in +the next line where the second gene precision is 2 and last gene +precision is 1. + +.. code:: python + + gene_type=[int, [float, 2], numpy.float16, numpy.int8, [float, 1]] + +This is a complete example where the initial and final populations are +printed where the genes comply with the data types and precisions +specified. + +.. code:: python + + import pygad + import numpy + + equation_inputs = [4, -2, 3.5, 8, -2] + desired_output = 2671.1234 + + def fitness_func(ga_instance, solution, solution_idx): + output = numpy.sum(solution * equation_inputs) + fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) + return fitness + + ga_instance = pygad.GA(num_generations=10, + sol_per_pop=5, + num_parents_mating=2, + num_genes=len(equation_inputs), + fitness_func=fitness_func, + gene_type=[int, [float, 2], numpy.float16, numpy.int8, [float, 1]]) + + print("Initial Population") + print(ga_instance.initial_population) + + ga_instance.run() + + print("Final Population") + print(ga_instance.population) + +.. code:: python + + Initial Population + [[-2 -1.22 1.716796875 -1 0.2] + [-1 -1.58 -3.091796875 0 -1.3] + [3 3.35 -0.107421875 1 -3.3] + [-2 -3.58 -1.779296875 0 0.6] + [2 -3.73 2.65234375 3 -0.5]] + + Final Population + [[2 -4.22 3.47 3 -1.3] + [2 -3.73 3.47 3 -1.3] + [2 -4.22 3.47 2 -1.3] + [2 -4.58 3.47 3 -1.3] + [2 -3.73 3.47 3 -1.3]] + +Parallel Processing in PyGAD +============================ + +Starting from `PyGAD +2.17.0 `__, +parallel processing becomes supported. This section explains how to use +parallel processing in PyGAD. + +According to the `PyGAD +lifecycle `__, +parallel processing can be parallelized in only 2 operations: + +1. Population fitness calculation. + +2. Mutation. + +The reason is that the calculations in these 2 operations are +independent (i.e. each solution/chromosome is handled independently from +the others) and can be distributed across different processes or +threads. + +For the mutation operation, it does not do intensive calculations on the +CPU. Its calculations are simple like flipping the values of some genes +from 0 to 1 or adding a random value to some genes. So, it does not take +much CPU processing time. Experiments proved that parallelizing the +mutation operation across the solutions increases the time instead of +reducing it. This is because running multiple processes or threads adds +overhead to manage them. Thus, parallel processing cannot be applied on +the mutation operation. + +For the population fitness calculation, parallel processing can help +make a difference and reduce the processing time. But this is +conditional on the type of calculations done in the fitness function. If +the fitness function makes intensive calculations and takes much +processing time from the CPU, then it is probably that parallel +processing will help to cut down the overall time. + +This section explains how parallel processing works in PyGAD and how to +use parallel processing in PyGAD + +How to Use Parallel Processing in PyGAD +--------------------------------------- + +Starting from `PyGAD +2.17.0 `__, +a new parameter called ``parallel_processing`` added to the constructor +of the ``pygad.GA`` class. + +.. code:: python + + import pygad + ... + ga_instance = pygad.GA(..., + parallel_processing=...) + ... + +This parameter allows the user to do the following: + +1. Enable parallel processing. + +2. Select whether processes or threads are used. + +3. Specify the number of processes or threads to be used. + +These are 3 possible values for the ``parallel_processing`` parameter: + +1. ``None``: (Default) It means no parallel processing is used. + +2. A positive integer referring to the number of threads to be used + (i.e. threads, not processes, are used. + +3. ``list``/``tuple``: If a list or a tuple of exactly 2 elements is + assigned, then: + + 1. The first element can be either ``'process'`` or ``'thread'`` to + specify whether processes or threads are used, respectively. + + 2. The second element can be: + + 1. A positive integer to select the maximum number of processes or + threads to be used + + 2. ``0`` to indicate that 0 processes or threads are used. It + means no parallel processing. This is identical to setting + ``parallel_processing=None``. + + 3. ``None`` to use the default value as calculated by the + ``concurrent.futures module``. + +These are examples of the values assigned to the ``parallel_processing`` +parameter: + +- ``parallel_processing=4``: Because the parameter is assigned a + positive integer, this means parallel processing is activated where 4 + threads are used. + +- ``parallel_processing=["thread", 5]``: Use parallel processing with 5 + threads. This is identical to ``parallel_processing=5``. + +- ``parallel_processing=["process", 8]``: Use parallel processing with 8 + processes. + +- ``parallel_processing=["process", 0]``: As the second element is given + the value 0, this means do not use parallel processing. This is + identical to ``parallel_processing=None``. + +Examples +-------- + +The examples will help you know the difference between using processes +and threads. Moreover, it will give an idea when parallel processing +would make a difference and reduce the time. These are dummy examples +where the fitness function is made to always return 0. + +The first example uses 10 genes, 5 solutions in the population where +only 3 solutions mate, and 9999 generations. The fitness function uses a +``for`` loop with 100 iterations just to have some calculations. In the +constructor of the ``pygad.GA`` class, ``parallel_processing=None`` +means no parallel processing is used. + +.. code:: python + + import pygad + import time + + def fitness_func(ga_instance, solution, solution_idx): + for _ in range(99): + pass + return 0 + + ga_instance = pygad.GA(num_generations=9999, + num_parents_mating=3, + sol_per_pop=5, + num_genes=10, + fitness_func=fitness_func, + suppress_warnings=True, + parallel_processing=None) + + if __name__ == '__main__': + t1 = time.time() + + ga_instance.run() + + t2 = time.time() + print("Time is", t2-t1) + +When parallel processing is not used, the time it takes to run the +genetic algorithm is ``1.5`` seconds. + +In the comparison, let's do a second experiment where parallel +processing is used with 5 threads. In this case, it take ``5`` seconds. + +.. code:: python + + ... + ga_instance = pygad.GA(..., + parallel_processing=5) + ... + +For the third experiment, processes instead of threads are used. Also, +only 99 generations are used instead of 9999. The time it takes is +``99`` seconds. + +.. code:: python + + ... + ga_instance = pygad.GA(num_generations=99, + ..., + parallel_processing=["process", 5]) + ... + +This is the summary of the 3 experiments: + +1. No parallel processing & 9999 generations: 1.5 seconds. + +2. Parallel processing with 5 threads & 9999 generations: 5 seconds + +3. Parallel processing with 5 processes & 99 generations: 99 seconds + +Because the fitness function does not need much CPU time, the normal +processing takes the least time. Running processes for this simple +problem takes 99 compared to only 5 seconds for threads because managing +processes is much heavier than managing threads. Thus, most of the CPU +time is for swapping the processes instead of executing the code. + +In the second example, the loop makes 99999999 iterations and only 5 +generations are used. With no parallelization, it takes 22 seconds. + +.. code:: python + + import pygad + import time + + def fitness_func(ga_instance, solution, solution_idx): + for _ in range(99999999): + pass + return 0 + + ga_instance = pygad.GA(num_generations=5, + num_parents_mating=3, + sol_per_pop=5, + num_genes=10, + fitness_func=fitness_func, + suppress_warnings=True, + parallel_processing=None) + + if __name__ == '__main__': + t1 = time.time() + ga_instance.run() + t2 = time.time() + print("Time is", t2-t1) + +It takes 15 seconds when 10 processes are used. + +.. code:: python + + ... + ga_instance = pygad.GA(..., + parallel_processing=["process", 10]) + ... + +This is compared to 20 seconds when 10 threads are used. + +.. code:: python + + ... + ga_instance = pygad.GA(..., + parallel_processing=["thread", 10]) + ... + +Based on the second example, using parallel processing with 10 processes +takes the least time because there is much CPU work done. Generally, +processes are preferred over threads when most of the work in on the +CPU. Threads are preferred over processes in some situations like doing +input/output operations. + +*Before releasing* `PyGAD +2.17.0 `__\ *,* +`László +Fazekas `__ +*wrote an article to parallelize the fitness function with PyGAD. Check +it:* `How Genetic Algorithms Can Compete with Gradient Descent and +Backprop `__. + +Print Lifecycle Summary +======================= + +In `PyGAD +2.19.0 `__, +a new method called ``summary()`` is supported. It prints a Keras-like +summary of the PyGAD lifecycle showing the steps, callback functions, +parameters, etc. + +This method accepts the following parameters: + +- ``line_length=70``: An integer representing the length of the single + line in characters. + +- ``fill_character=" "``: A character to fill the lines. + +- ``line_character="-"``: A character for creating a line separator. + +- ``line_character2="="``: A secondary character to create a line + separator. + +- ``columns_equal_len=False``: The table rows are split into equal-sized + columns or split subjective to the width needed. + +- ``print_step_parameters=True``: Whether to print extra parameters + about each step inside the step. If ``print_step_parameters=False`` + and ``print_parameters_summary=True``, then the parameters of each + step are printed at the end of the table. + +- ``print_parameters_summary=True``: Whether to print parameters summary + at the end of the table. If ``print_step_parameters=False``, then the + parameters of each step are printed at the end of the table too. + +This is a quick example to create a PyGAD example. + +.. code:: python + + import pygad + import numpy + + function_inputs = [4,-2,3.5,5,-11,-4.7] + desired_output = 44 + + def genetic_fitness(solution, solution_idx): + output = numpy.sum(solution*function_inputs) + fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) + return fitness + + def on_gen(ga): + pass + + def on_crossover_callback(a, b): + pass + + ga_instance = pygad.GA(num_generations=100, + num_parents_mating=10, + sol_per_pop=20, + num_genes=len(function_inputs), + on_crossover=on_crossover_callback, + on_generation=on_gen, + parallel_processing=2, + stop_criteria="reach_10", + fitness_batch_size=4, + crossover_probability=0.4, + fitness_func=genetic_fitness) + +Then call the ``summary()`` method to print the summary with the default +parameters. Note that entries for the crossover and generation callback +function are created because their callback functions are implemented +through the ``on_crossover_callback()`` and ``on_gen()``, respectively. + +.. code:: python + + ga_instance.summary() + +.. code:: bash + + ---------------------------------------------------------------------- + PyGAD Lifecycle + ====================================================================== + Step Handler Output Shape + ====================================================================== + Fitness Function genetic_fitness() (1) + Fitness batch size: 4 + ---------------------------------------------------------------------- + Parent Selection steady_state_selection() (10, 6) + Number of Parents: 10 + ---------------------------------------------------------------------- + Crossover single_point_crossover() (10, 6) + Crossover probability: 0.4 + ---------------------------------------------------------------------- + On Crossover on_crossover_callback() None + ---------------------------------------------------------------------- + Mutation random_mutation() (10, 6) + Mutation Genes: 1 + Random Mutation Range: (-1.0, 1.0) + Mutation by Replacement: False + Allow Duplicated Genes: True + ---------------------------------------------------------------------- + On Generation on_gen() None + Stop Criteria: [['reach', 10.0]] + ---------------------------------------------------------------------- + ====================================================================== + Population Size: (20, 6) + Number of Generations: 100 + Initial Population Range: (-4, 4) + Keep Elitism: 1 + Gene DType: [, None] + Parallel Processing: ['thread', 2] + Save Best Solutions: False + Save Solutions: False + ====================================================================== + +We can set the ``print_step_parameters`` and +``print_parameters_summary`` parameters to ``False`` to not print the +parameters. + +.. code:: python + + ga_instance.summary(print_step_parameters=False, + print_parameters_summary=False) + +.. code:: bash + + ---------------------------------------------------------------------- + PyGAD Lifecycle + ====================================================================== + Step Handler Output Shape + ====================================================================== + Fitness Function genetic_fitness() (1) + ---------------------------------------------------------------------- + Parent Selection steady_state_selection() (10, 6) + ---------------------------------------------------------------------- + Crossover single_point_crossover() (10, 6) + ---------------------------------------------------------------------- + On Crossover on_crossover_callback() None + ---------------------------------------------------------------------- + Mutation random_mutation() (10, 6) + ---------------------------------------------------------------------- + On Generation on_gen() None + ---------------------------------------------------------------------- + ====================================================================== + +Logging Outputs +=============== + +In `PyGAD +3.0.0 `__, +the ``print()`` statement is no longer used and the outputs are printed +using the `logging `__ +module. A a new parameter called ``logger`` is supported to accept the +user-defined logger. + +.. code:: python + + import logging + + logger = ... + + ga_instance = pygad.GA(..., + logger=logger, + ...) + +The default value for this parameter is ``None``. If there is no logger +passed (i.e. ``logger=None``), then a default logger is created to log +the messages to the console exactly like how the ``print()`` statement +works. + +Some advantages of using the the +`logging `__ module +instead of the ``print()`` statement are: + +1. The user has more control over the printed messages specially if + there is a project that uses multiple modules where each module + prints its messages. A logger can organize the outputs. + +2. Using the proper ``Handler``, the user can log the output messages to + files and not only restricted to printing it to the console. So, it + is much easier to record the outputs. + +3. The format of the printed messages can be changed by customizing the + ``Formatter`` assigned to the Logger. + +This section gives some quick examples to use the ``logging`` module and +then gives an example to use the logger with PyGAD. + +Logging to the Console +---------------------- + +This is an example to create a logger to log the messages to the +console. + +.. code:: python + + import logging + + # Create a logger + logger = logging.getLogger(__name__) + + # Set the logger level to debug so that all the messages are printed. + logger.setLevel(logging.DEBUG) + + # Create a stream handler to log the messages to the console. + stream_handler = logging.StreamHandler() + + # Set the handler level to debug. + stream_handler.setLevel(logging.DEBUG) + + # Create a formatter + formatter = logging.Formatter('%(message)s') + + # Add the formatter to handler. + stream_handler.setFormatter(formatter) + + # Add the stream handler to the logger + logger.addHandler(stream_handler) + +Now, we can log messages to the console with the format specified in the +``Formatter``. + +.. code:: python + + logger.debug('Debug message.') + logger.info('Info message.') + logger.warning('Warn message.') + logger.error('Error message.') + logger.critical('Critical message.') + +The outputs are identical to those returned using the ``print()`` +statement. + +.. code:: + + Debug message. + Info message. + Warn message. + Error message. + Critical message. + +By changing the format of the output messages, we can have more +information about each message. + +.. code:: python + + formatter = logging.Formatter('%(asctime)s %(levelname)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S') + +This is a sample output. + +.. code:: python + + 2023-04-03 18:46:27 DEBUG: Debug message. + 2023-04-03 18:46:27 INFO: Info message. + 2023-04-03 18:46:27 WARNING: Warn message. + 2023-04-03 18:46:27 ERROR: Error message. + 2023-04-03 18:46:27 CRITICAL: Critical message. + +Note that you may need to clear the handlers after finishing the +execution. This is to make sure no cached handlers are used in the next +run. If the cached handlers are not cleared, then the single output +message may be repeated. + +.. code:: python + + logger.handlers.clear() + +Logging to a File +----------------- + +This is another example to log the messages to a file named +``logfile.txt``. The formatter prints the following about each message: + +1. The date and time at which the message is logged. + +2. The log level. + +3. The message. + +4. The path of the file. + +5. The lone number of the log message. + +.. code:: python + + import logging + + level = logging.DEBUG + name = 'logfile.txt' + + logger = logging.getLogger(name) + logger.setLevel(level) + + file_handler = logging.FileHandler(name, 'a+', 'utf-8') + file_handler.setLevel(logging.DEBUG) + file_format = logging.Formatter('%(asctime)s %(levelname)s: %(message)s - %(pathname)s:%(lineno)d', datefmt='%Y-%m-%d %H:%M:%S') + file_handler.setFormatter(file_format) + logger.addHandler(file_handler) + +This is how the outputs look like. + +.. code:: python + + 2023-04-03 18:54:03 DEBUG: Debug message. - c:\users\agad069\desktop\logger\example2.py:46 + 2023-04-03 18:54:03 INFO: Info message. - c:\users\agad069\desktop\logger\example2.py:47 + 2023-04-03 18:54:03 WARNING: Warn message. - c:\users\agad069\desktop\logger\example2.py:48 + 2023-04-03 18:54:03 ERROR: Error message. - c:\users\agad069\desktop\logger\example2.py:49 + 2023-04-03 18:54:03 CRITICAL: Critical message. - c:\users\agad069\desktop\logger\example2.py:50 + +Consider clearing the handlers if necessary. + +.. code:: python + + logger.handlers.clear() + +Log to Both the Console and a File +---------------------------------- + +This is an example to create a single Logger associated with 2 handlers: + +1. A file handler. + +2. A stream handler. + +.. code:: python + + import logging + + level = logging.DEBUG + name = 'logfile.txt' + + logger = logging.getLogger(name) + logger.setLevel(level) + + file_handler = logging.FileHandler(name,'a+','utf-8') + file_handler.setLevel(logging.DEBUG) + file_format = logging.Formatter('%(asctime)s %(levelname)s: %(message)s - %(pathname)s:%(lineno)d', datefmt='%Y-%m-%d %H:%M:%S') + file_handler.setFormatter(file_format) + logger.addHandler(file_handler) + + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.INFO) + console_format = logging.Formatter('%(message)s') + console_handler.setFormatter(console_format) + logger.addHandler(console_handler) + +When a log message is executed, then it is both printed to the console +and saved in the ``logfile.txt``. + +Consider clearing the handlers if necessary. + +.. code:: python + + logger.handlers.clear() + +PyGAD Example +------------- + +To use the logger in PyGAD, just create your custom logger and pass it +to the ``logger`` parameter. + +.. code:: python + + import logging + import pygad + import numpy + + level = logging.DEBUG + name = 'logfile.txt' + + logger = logging.getLogger(name) + logger.setLevel(level) + + file_handler = logging.FileHandler(name,'a+','utf-8') + file_handler.setLevel(logging.DEBUG) + file_format = logging.Formatter('%(asctime)s %(levelname)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S') + file_handler.setFormatter(file_format) + logger.addHandler(file_handler) + + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.INFO) + console_format = logging.Formatter('%(message)s') + console_handler.setFormatter(console_format) + logger.addHandler(console_handler) + + equation_inputs = [4, -2, 8] + desired_output = 2671.1234 + + def fitness_func(ga_instance, solution, solution_idx): + output = numpy.sum(solution * equation_inputs) + fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) + return fitness + + def on_generation(ga_instance): + ga_instance.logger.info(f"Generation = {ga_instance.generations_completed}") + ga_instance.logger.info(f"Fitness = {ga_instance.best_solution(pop_fitness=ga_instance.last_generation_fitness)[1]}") + + ga_instance = pygad.GA(num_generations=10, + sol_per_pop=40, + num_parents_mating=2, + keep_parents=2, + num_genes=len(equation_inputs), + fitness_func=fitness_func, + on_generation=on_generation, + logger=logger) + ga_instance.run() + + logger.handlers.clear() + +By executing this code, the logged messages are printed to the console +and also saved in the text file. + +.. code:: python + + 2023-04-03 19:04:27 INFO: Generation = 1 + 2023-04-03 19:04:27 INFO: Fitness = 0.00038086960368076276 + 2023-04-03 19:04:27 INFO: Generation = 2 + 2023-04-03 19:04:27 INFO: Fitness = 0.00038214871408010853 + 2023-04-03 19:04:27 INFO: Generation = 3 + 2023-04-03 19:04:27 INFO: Fitness = 0.0003832795907974678 + 2023-04-03 19:04:27 INFO: Generation = 4 + 2023-04-03 19:04:27 INFO: Fitness = 0.00038398612055017196 + 2023-04-03 19:04:27 INFO: Generation = 5 + 2023-04-03 19:04:27 INFO: Fitness = 0.00038442348890867516 + 2023-04-03 19:04:27 INFO: Generation = 6 + 2023-04-03 19:04:27 INFO: Fitness = 0.0003854406039137763 + 2023-04-03 19:04:27 INFO: Generation = 7 + 2023-04-03 19:04:27 INFO: Fitness = 0.00038646083174063284 + 2023-04-03 19:04:27 INFO: Generation = 8 + 2023-04-03 19:04:27 INFO: Fitness = 0.0003875169193024936 + 2023-04-03 19:04:27 INFO: Generation = 9 + 2023-04-03 19:04:27 INFO: Fitness = 0.0003888816727311021 + 2023-04-03 19:04:27 INFO: Generation = 10 + 2023-04-03 19:04:27 INFO: Fitness = 0.000389832593101348 + +Solve Non-Deterministic Problems +================================ + +PyGAD can be used to solve both deterministic and non-deterministic +problems. Deterministic are those that return the same fitness for the +same solution. For non-deterministic problems, a different fitness value +would be returned for the same solution. + +By default, PyGAD settings are set to solve deterministic problems. +PyGAD can save the explored solutions and their fitness to reuse in the +future. These instances attributes can save the solutions: + +1. ``solutions``: Exists if ``save_solutions=True``. + +2. ``best_solutions``: Exists if ``save_best_solutions=True``. + +3. ``last_generation_elitism``: Exists if ``keep_elitism`` > 0. + +4. ``last_generation_parents``: Exists if ``keep_parents`` > 0 or + ``keep_parents=-1``. + +To configure PyGAD for non-deterministic problems, we have to disable +saving the previous solutions. This is by setting these parameters: + +1. ``keep_elitism=0`` + +2. ``keep_parents=0`` + +3. ``keep_solutions=False`` + +4. ``keep_best_solutions=False`` + +.. code:: python + + import pygad + ... + ga_instance = pygad.GA(..., + keep_elitism=0, + keep_parents=0, + save_solutions=False, + save_best_solutions=False, + ...) + +This way PyGAD will not save any explored solution and thus the fitness +function have to be called for each individual solution. + +Reuse the Fitness instead of Calling the Fitness Function +========================================================= + +It may happen that a previously explored solution in generation X is +explored again in another generation Y (where Y > X). For some problems, +calling the fitness function takes much time. + +For deterministic problems, it is better to not call the fitness +function for an already explored solutions. Instead, reuse the fitness +of the old solution. PyGAD supports some options to help you save time +calling the fitness function for a previously explored solution. + +The parameters explored in this section can be set in the constructor of +the ``pygad.GA`` class. + +The ``cal_pop_fitness()`` method of the ``pygad.GA`` class checks these +parameters to see if there is a possibility of reusing the fitness +instead of calling the fitness function. + +.. _1-savesolutions: + +1. ``save_solutions`` +--------------------- + +It defaults to ``False``. If set to ``True``, then the population of +each generation is saved into the ``solutions`` attribute of the +``pygad.GA`` instance. In other words, every single solution is saved in +the ``solutions`` attribute. + +.. _2-savebestsolutions: + +2. ``save_best_solutions`` +-------------------------- + +It defaults to ``False``. If ``True``, then it only saves the best +solution in every generation. + +.. _3-keepelitism: + +3. ``keep_elitism`` +------------------- + +It accepts an integer and defaults to 1. If set to a positive integer, +then it keeps the elitism of one generation available in the next +generation. + +.. _4-keepparents: + +4. ``keep_parents`` +------------------- + +It accepts an integer and defaults to -1. It set to ``-1`` or a positive +integer, then it keeps the parents of one generation available in the +next generation. + +Why the Fitness Function is not Called for Solution at Index 0? +=============================================================== + +PyGAD has a parameter called ``keep_elitism`` which defaults to 1. This +parameter defines the number of best solutions in generation **X** to +keep in the next generation **X+1**. The best solutions are just copied +from generation **X** to generation **X+1** without making any change. + +.. code:: python + + ga_instance = pygad.GA(..., + keep_elitism=1, + ...) + +The best solutions are copied at the beginning of the population. If +``keep_elitism=1``, this means the best solution in generation X is kept +in the next generation X+1 at index 0 of the population. If +``keep_elitism=2``, this means the 2 best solutions in generation X are +kept in the next generation X+1 at indices 0 and 1 of the population of +generation 1. + +Because the fitness of these best solutions are already calculated in +generation X, then their fitness values will not be recalculated at +generation X+1 (i.e. the fitness function will not be called for these +solutions again). Instead, their fitness values are just reused. This is +why you see that no solution with index 0 is passed to the fitness +function. + +To force calling the fitness function for each solution in every +generation, consider setting ``keep_elitism`` and ``keep_parents`` to 0. +Moreover, keep the 2 parameters ``save_solutions`` and +``save_best_solutions`` to their default value ``False``. + +.. code:: python + + ga_instance = pygad.GA(..., + keep_elitism=0, + keep_parents=0, + save_solutions=False, + save_best_solutions=False, + ...) + +Batch Fitness Calculation +========================= + +In `PyGAD +2.19.0 `__, +a new optional parameter called ``fitness_batch_size`` is supported. A +new optional parameter called ``fitness_batch_size`` is supported to +calculate the fitness function in batches. Thanks to `Linan +Qiu `__ for opening the `GitHub issue +#136 `__. + +Its values can be: + +- ``1`` or ``None``: If the ``fitness_batch_size`` parameter is assigned + the value ``1`` or ``None`` (default), then the normal flow is used + where the fitness function is called for each individual solution. + That is if there are 15 solutions, then the fitness function is called + 15 times. + +- ``1 < fitness_batch_size <= sol_per_pop``: If the + ``fitness_batch_size`` parameter is assigned a value satisfying this + condition ``1 < fitness_batch_size <= sol_per_pop``, then the + solutions are grouped into batches of size ``fitness_batch_size`` and + the fitness function is called once for each batch. In this case, the + fitness function must return a list/tuple/numpy.ndarray with a length + equal to the number of solutions passed. + +.. _example-without-fitnessbatchsize-parameter: + +Example without ``fitness_batch_size`` Parameter +------------------------------------------------ + +This is an example where the ``fitness_batch_size`` parameter is given +the value ``None`` (which is the default value). This is equivalent to +using the value ``1``. In this case, the fitness function will be called +for each solution. This means the fitness function ``fitness_func`` will +receive only a single solution. This is an example of the passed +arguments to the fitness function: + +.. code:: + + solution: [ 2.52860734, -0.94178795, 2.97545704, 0.84131987, -3.78447118, 2.41008358] + solution_idx: 3 + +The fitness function also must return a single numeric value as the +fitness for the passed solution. + +As we have a population of ``20`` solutions, then the fitness function +is called 20 times per generation. For 5 generations, then the fitness +function is called ``20*5 = 100`` times. In PyGAD, the fitness function +is called after the last generation too and this adds additional 20 +times. So, the total number of calls to the fitness function is +``20*5 + 20 = 120``. + +Note that the ``keep_elitism`` and ``keep_parents`` parameters are set +to ``0`` to make sure no fitness values are reused and to force calling +the fitness function for each individual solution. + +.. code:: python + + import pygad + import numpy + + function_inputs = [4,-2,3.5,5,-11,-4.7] + desired_output = 44 + + number_of_calls = 0 + + def fitness_func(ga_instance, solution, solution_idx): + global number_of_calls + number_of_calls = number_of_calls + 1 + output = numpy.sum(solution*function_inputs) + fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) + return fitness + + ga_instance = pygad.GA(num_generations=5, + num_parents_mating=10, + sol_per_pop=20, + fitness_func=fitness_func, + fitness_batch_size=None, + # fitness_batch_size=1, + num_genes=len(function_inputs), + keep_elitism=0, + keep_parents=0) + + ga_instance.run() + print(number_of_calls) + +.. code:: + + 120 + +.. _example-with-fitnessbatchsize-parameter: + +Example with ``fitness_batch_size`` Parameter +--------------------------------------------- + +This is an example where the ``fitness_batch_size`` parameter is used +and assigned the value ``4``. This means the solutions will be grouped +into batches of ``4`` solutions. The fitness function will be called +once for each patch (i.e. called once for each 4 solutions). + +This is an example of the arguments passed to it: + +.. code:: python + + solutions: + [[ 3.1129432 -0.69123589 1.93792414 2.23772968 -1.54616001 -0.53930799] + [ 3.38508121 0.19890812 1.93792414 2.23095014 -3.08955597 3.10194128] + [ 2.37079504 -0.88819803 2.97545704 1.41742256 -3.95594055 2.45028256] + [ 2.52860734 -0.94178795 2.97545704 0.84131987 -3.78447118 2.41008358]] + solutions_indices: + [16, 17, 18, 19] + +As we have 20 solutions, then there are ``20/4 = 5`` patches. As a +result, the fitness function is called only 5 times per generation +instead of 20. For each call to the fitness function, it receives a +batch of 4 solutions. + +As we have 5 generations, then the function will be called ``5*5 = 25`` +times. Given the call to the fitness function after the last generation, +then the total number of calls is ``5*5 + 5 = 30``. + +.. code:: python + + import pygad + import numpy + + function_inputs = [4,-2,3.5,5,-11,-4.7] + desired_output = 44 + + number_of_calls = 0 + + def fitness_func_batch(ga_instance, solutions, solutions_indices): + global number_of_calls + number_of_calls = number_of_calls + 1 + batch_fitness = [] + for solution in solutions: + output = numpy.sum(solution*function_inputs) + fitness = 1.0 / (numpy.abs(output - desired_output) + 0.000001) + batch_fitness.append(fitness) + return batch_fitness + + ga_instance = pygad.GA(num_generations=5, + num_parents_mating=10, + sol_per_pop=20, + fitness_func=fitness_func_batch, + fitness_batch_size=4, + num_genes=len(function_inputs), + keep_elitism=0, + keep_parents=0) + + ga_instance.run() + print(number_of_calls) + +.. code:: + + 30 + +When batch fitness calculation is used, then we saved ``120 - 30 = 90`` +calls to the fitness function. + +Use Functions and Methods to Build Fitness and Callbacks +======================================================== + +In PyGAD 2.19.0, it is possible to pass user-defined functions or +methods to the following parameters: + +1. ``fitness_func`` + +2. ``on_start`` + +3. ``on_fitness`` + +4. ``on_parents`` + +5. ``on_crossover`` + +6. ``on_mutation`` + +7. ``on_generation`` + +8. ``on_stop`` + +This section gives 2 examples to assign these parameters user-defined: + +1. Functions. + +2. Methods. + +Assign Functions +---------------- + +This is a dummy example where the fitness function returns a random +value. Note that the instance of the ``pygad.GA`` class is passed as the +last parameter of all functions. + +.. code:: python + + import pygad + import numpy + + def fitness_func(ga_instanse, solution, solution_idx): + return numpy.random.rand() + + def on_start(ga_instanse): + print("on_start") + + def on_fitness(ga_instanse, last_gen_fitness): + print("on_fitness") + + def on_parents(ga_instanse, last_gen_parents): + print("on_parents") + + def on_crossover(ga_instanse, last_gen_offspring): + print("on_crossover") + + def on_mutation(ga_instanse, last_gen_offspring): + print("on_mutation") + + def on_generation(ga_instanse): + print("on_generation\n") + + def on_stop(ga_instanse, last_gen_fitness): + print("on_stop") + + ga_instance = pygad.GA(num_generations=5, + num_parents_mating=4, + sol_per_pop=10, + num_genes=2, + on_start=on_start, + on_fitness=on_fitness, + on_parents=on_parents, + on_crossover=on_crossover, + on_mutation=on_mutation, + on_generation=on_generation, + on_stop=on_stop, + fitness_func=fitness_func) + + ga_instance.run() + +Assign Methods +-------------- + +The next example has all the method defined inside the class ``Test``. +All of the methods accept an additional parameter representing the +method's object of the class ``Test``. + +All methods accept ``self`` as the first parameter and the instance of +the ``pygad.GA`` class as the last parameter. + +.. code:: python + + import pygad + import numpy + + class Test: + def fitness_func(self, ga_instanse, solution, solution_idx): + return numpy.random.rand() + + def on_start(self, ga_instanse): + print("on_start") + + def on_fitness(self, ga_instanse, last_gen_fitness): + print("on_fitness") + + def on_parents(self, ga_instanse, last_gen_parents): + print("on_parents") + + def on_crossover(self, ga_instanse, last_gen_offspring): + print("on_crossover") + + def on_mutation(self, ga_instanse, last_gen_offspring): + print("on_mutation") + + def on_generation(self, ga_instanse): + print("on_generation\n") + + def on_stop(self, ga_instanse, last_gen_fitness): + print("on_stop") + + ga_instance = pygad.GA(num_generations=5, + num_parents_mating=4, + sol_per_pop=10, + num_genes=2, + on_start=Test().on_start, + on_fitness=Test().on_fitness, + on_parents=Test().on_parents, + on_crossover=Test().on_crossover, + on_mutation=Test().on_mutation, + on_generation=Test().on_generation, + on_stop=Test().on_stop, + fitness_func=Test().fitness_func) + + ga_instance.run() + +.. |image1| image:: https://github.com/ahmedfgad/GeneticAlgorithmPython/assets/16560492/7896f8d8-01c5-4ff9-8d15-52191c309b63 +.. |image2| image:: https://user-images.githubusercontent.com/16560492/189273225-67ffad41-97ab-45e1-9324-429705e17b20.png diff --git a/pygad/pygad.py b/pygad/pygad.py index 436237b..88a2d9d 100644 --- a/pygad/pygad.py +++ b/pygad/pygad.py @@ -1147,6 +1147,21 @@ def __init__(self, self.valid_parameters = False raise TypeError(f"The value passed to the 'save_solutions' parameter must be of type bool but {type(save_solutions)} found.") + def validate_multi_stop_criteria(self, stop_word, number): + if stop_word == 'reach': + pass + else: + self.valid_parameters = False + raise ValueError(f"Passing multiple numbers following the keyword in the 'stop_criteria' parameter is expected only with the 'reach' keyword but the keyword ({stop_word}) found.") + + for idx, num in enumerate(number): + if num.replace(".", "").replace("-", "").isnumeric(): + number[idx] = float(num) + else: + self.valid_parameters = False + raise ValueError(f"The value(s) following the stop word in the 'stop_criteria' parameter must be numeric but the value ({num}) of type {type(num)} found.") + return number + self.stop_criteria = [] self.supported_stop_words = ["reach", "saturate"] if stop_criteria is None: @@ -1168,7 +1183,7 @@ def __init__(self, if len(criterion) == 2: # There is only a single number. number = number[0] - if number.replace(".", "").isnumeric(): + if number.replace(".", "").replace("-", "").isnumeric(): number = float(number) else: self.valid_parameters = False @@ -1176,21 +1191,8 @@ def __init__(self, self.stop_criteria.append([stop_word, number]) elif len(criterion) > 2: - if stop_word == 'reach': - pass - else: - self.valid_parameters = False - raise ValueError(f"Passing multiple numbers following the keyword in the 'stop_criteria' parameter is expected only with the 'reach' keyword but the keyword ({stop_word}) found.") - - for idx, num in enumerate(number): - if num.replace(".", "").isnumeric(): - number[idx] = float(num) - else: - self.valid_parameters = False - raise ValueError(f"The value(s) following the stop word in the 'stop_criteria' parameter must be numeric but the value ({num}) of type {type(num)} found.") - + number = validate_multi_stop_criteria(self, stop_word, number) self.stop_criteria.append([stop_word] + number) - else: self.valid_parameters = False raise ValueError(f"For format of a single criterion in the 'stop_criteria' parameter is 'word_number' but '{stop_criteria}' found.") @@ -1201,10 +1203,11 @@ def __init__(self, for idx, val in enumerate(stop_criteria): if type(val) is str: criterion = val.split("_") + stop_word = criterion[0] + number = criterion[1:] if len(criterion) == 2: - stop_word = criterion[0] - number = criterion[1] - + # There is only a single number. + number = number[0] if stop_word in self.supported_stop_words: pass else: @@ -1218,7 +1221,9 @@ def __init__(self, raise ValueError(f"The value following the stop word in the 'stop_criteria' parameter must be a number but the value ({number}) of type {type(number)} found.") self.stop_criteria.append([stop_word, number]) - + elif len(criterion) > 2: + number = validate_multi_stop_criteria(self, stop_word, number) + self.stop_criteria.append([stop_word] + number) else: self.valid_parameters = False raise ValueError(f"The format of a single criterion in the 'stop_criteria' parameter is 'word_number' but {criterion} found.") diff --git a/tests/test_stop_criteria.py b/tests/test_stop_criteria.py index c950be2..3ee1737 100644 --- a/tests/test_stop_criteria.py +++ b/tests/test_stop_criteria.py @@ -144,6 +144,43 @@ def test_number_calls_fitness_function_parallel_processing_process_5_patch_4_mul fitness_batch_size=4, parallel_processing=['process', 5]) +# Stop Criteria +def test_number_calls_fitness_function_multi_objective_stop_criteria_str_single_value(): + multi_objective_problem(multi_objective=True, + stop_criteria='reach_10') + +def test_number_calls_fitness_function_multi_objective_stop_criteria_str(): + multi_objective_problem(multi_objective=True, + stop_criteria='reach_10_20') + +def test_number_calls_fitness_function_multi_objective_stop_criteria_str_decimal(): + multi_objective_problem(multi_objective=True, + stop_criteria='reach_-1.0_0.5') + +def test_number_calls_fitness_function_multi_objective_stop_criteria_list(): + multi_objective_problem(multi_objective=True, + stop_criteria=['reach_10_20', 'reach_5_2']) + +def test_number_calls_fitness_function_multi_objective_stop_criteria_list_decimal(): + multi_objective_problem(multi_objective=True, + stop_criteria=['reach_-1.0_0.5', 'reach_5_-2.8']) + +def test_number_calls_fitness_function_single_objective_stop_criteria_str(): + multi_objective_problem(multi_objective=True, + stop_criteria='reach_10') + +def test_number_calls_fitness_function_single_objective_stop_criteria_str_decimal(): + multi_objective_problem(multi_objective=True, + stop_criteria='reach_-1.7') + +def test_number_calls_fitness_function_single_objective_stop_criteria_list(): + multi_objective_problem(multi_objective=True, + stop_criteria=['reach_10', 'reach_5']) + +def test_number_calls_fitness_function_single_objective_stop_criteria_list_decimal(): + multi_objective_problem(multi_objective=True, + stop_criteria=['reach_-1.5', 'reach_-2.8']) + if __name__ == "__main__": print() test_number_calls_fitness_function_no_parallel_processing() @@ -172,3 +209,26 @@ def test_number_calls_fitness_function_parallel_processing_process_5_patch_4_mul print() test_number_calls_fitness_function_parallel_processing_process_5_patch_4_multi_objective() print() + + #### Multi-Objective Stop Criteria + test_number_calls_fitness_function_multi_objective_stop_criteria_str_single_value() + print() + test_number_calls_fitness_function_multi_objective_stop_criteria_str() + print() + test_number_calls_fitness_function_multi_objective_stop_criteria_str_decimal() + print() + test_number_calls_fitness_function_multi_objective_stop_criteria_list() + print() + test_number_calls_fitness_function_multi_objective_stop_criteria_list_decimal() + print() + + #### Single-Objective Stop Criteria + test_number_calls_fitness_function_single_objective_stop_criteria_str() + print() + test_number_calls_fitness_function_single_objective_stop_criteria_str_decimal() + print() + test_number_calls_fitness_function_single_objective_stop_criteria_list() + print() + test_number_calls_fitness_function_single_objective_stop_criteria_list_decimal() + print() + From 22b579f92dd8c6e9b3b0ffba94ca51a4530a0a97 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Thu, 6 Feb 2025 15:54:43 -0500 Subject: [PATCH 30/31] Call get_matplotlib() --- pygad/visualize/plot.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pygad/visualize/plot.py b/pygad/visualize/plot.py index 3265de3..ab6bd25 100644 --- a/pygad/visualize/plot.py +++ b/pygad/visualize/plot.py @@ -235,6 +235,8 @@ def plot_genes(self, self.logger.error("The plot_genes() method can only be called after completing at least 1 generation but ({self.generations_completed}) is completed.") raise RuntimeError("The plot_genes() method can only be called after completing at least 1 generation but ({self.generations_completed}) is completed.") + matplt = get_matplotlib() + if type(solutions) is str: if solutions == 'all': if self.save_solutions: From 6a5ef12968bad43803c8f79f4694dbaa3ce62c39 Mon Sep 17 00:00:00 2001 From: Ahmed Gad Date: Sat, 8 Mar 2025 11:45:45 -0500 Subject: [PATCH 31/31] Link to optimization gadget --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 75cd596..08bd7e4 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ [PyGAD](https://pypi.org/project/pygad) is an open-source easy-to-use Python 3 library for building the genetic algorithm and optimizing machine learning algorithms. It supports Keras and PyTorch. PyGAD supports optimizing both single-objective and multi-objective problems. +> Try the [Optimization Gadget](https://optimgadget.com), a free cloud-based tool powered by PyGAD. It simplifies optimization by reducing or eliminating the need for coding while providing insightful visualizations. + Check documentation of the [PyGAD](https://pygad.readthedocs.io/en/latest). [![PyPI Downloads](https://pepy.tech/badge/pygad)](https://pepy.tech/project/pygad) [![Conda Downloads](https://img.shields.io/conda/dn/conda-forge/pygad.svg?label=Conda%20Downloads)](