diff --git a/.github/workflows/main_py310.yml b/.github/workflows/main_py310.yml index 9602f17..9e701d1 100644 --- a/.github/workflows/main_py310.yml +++ b/.github/workflows/main_py310.yml @@ -5,6 +5,8 @@ on: branches: - github-actions # - master + # Manually trigger the workflow. + workflow_dispatch: jobs: job_id_1: diff --git a/.github/workflows/main_py311.yml b/.github/workflows/main_py311.yml index d468243..0ca059b 100644 --- a/.github/workflows/main_py311.yml +++ b/.github/workflows/main_py311.yml @@ -5,6 +5,8 @@ on: branches: - github-actions # - master + # Manually trigger the workflow. + workflow_dispatch: jobs: job_id_1: diff --git a/.github/workflows/main_py312.yml b/.github/workflows/main_py312.yml index 87bd648..91cc504 100644 --- a/.github/workflows/main_py312.yml +++ b/.github/workflows/main_py312.yml @@ -1,18 +1,12 @@ name: PyGAD PyTest / Python 3.12 -# Cannot install packages in Python 3.12. -# The reason is that we use pip for installing packages. -# pip uses setuptools for the installation. -# setuptools depends on distutils. -# But Python 3.12 does not support distutils. -# Let's wait until setuptools changes its dependencies. - -# on: -# push: -# branches: - # - github-actions +on: + push: + branches: + - github-actions # - master -on: workflow_dispatch + # Manually trigger the workflow. + workflow_dispatch: jobs: job_id_1: @@ -26,7 +20,7 @@ jobs: - name: Setup Python 3.12 uses: actions/setup-python@v4 with: - python-version: '3.12.0-beta.2' + python-version: '3.12' - name: Install Dependencies run: | diff --git a/.github/workflows/main_py37.yml b/.github/workflows/main_py313.yml similarity index 84% rename from .github/workflows/main_py37.yml rename to .github/workflows/main_py313.yml index 427aaec..0e0301c 100644 --- a/.github/workflows/main_py37.yml +++ b/.github/workflows/main_py313.yml @@ -1,10 +1,12 @@ -name: PyGAD PyTest / Python 3.7 +name: PyGAD PyTest / Python 3.13 on: push: branches: - github-actions # - master + # Manually trigger the workflow. + workflow_dispatch: jobs: job_id_1: @@ -15,10 +17,10 @@ jobs: - name: Checkout Pre-Built Action uses: actions/checkout@v3 - - name: Setup Python 3.7 + - name: Setup Python 3.13 uses: actions/setup-python@v4 with: - python-version: '3.7' + python-version: '3.13' - name: Install Dependencies run: | diff --git a/.github/workflows/main_py38.yml b/.github/workflows/main_py38.yml index 602f917..7838c64 100644 --- a/.github/workflows/main_py38.yml +++ b/.github/workflows/main_py38.yml @@ -5,6 +5,8 @@ on: branches: - github-actions # - master + # Manually trigger the workflow. + workflow_dispatch: jobs: job_id_1: diff --git a/.github/workflows/main_py39.yml b/.github/workflows/main_py39.yml index c6b61fc..be32ab4 100644 --- a/.github/workflows/main_py39.yml +++ b/.github/workflows/main_py39.yml @@ -5,6 +5,8 @@ on: branches: - github-actions # - master + # Manually trigger the workflow. + workflow_dispatch: jobs: job_id_1: diff --git a/pygad/__init__.py b/pygad/__init__.py index 17959b7..53ccec2 100644 --- a/pygad/__init__.py +++ b/pygad/__init__.py @@ -1,3 +1,3 @@ from .pygad import * # Relative import. -__version__ = "3.3.1" +__version__ = "3.5.0" diff --git a/pygad/helper/__init__.py b/pygad/helper/__init__.py index e781d27..49c736c 100644 --- a/pygad/helper/__init__.py +++ b/pygad/helper/__init__.py @@ -1,3 +1,4 @@ from pygad.helper import unique +from pygad.helper import misc -__version__ = "1.1.0" \ No newline at end of file +__version__ = "1.2.0" \ No newline at end of file diff --git a/pygad/helper/misc.py b/pygad/helper/misc.py new file mode 100644 index 0000000..7c91991 --- /dev/null +++ b/pygad/helper/misc.py @@ -0,0 +1,538 @@ +""" +The pygad.helper.misc module has some generic helper methods. +""" + +import numpy +import warnings +import random +import pygad + +class Helper: + + def change_population_dtype_and_round(self, + population): + """ + Change the data type of the population. It works with iterables (e.g. lists or NumPy arrays) of shape 2D. + It does not handle single numeric values or 1D arrays. + + It accepts: + -population: The iterable to change its dtype. + + It returns the iterable with the data type changed for all genes. + """ + + population_new = numpy.array(population.copy(), dtype=object) + + # Forcing the iterable to have the data type assigned to the gene_type parameter. + if self.gene_type_single == True: + # Round the numbers first then change the data type. + # This solves issues with some data types such as numpy.float32. + if self.gene_type[1] is None: + pass + else: + # This block is reached only for non-integer data types (i.e. float). + population_new = numpy.round(numpy.array(population_new, float), + self.gene_type[1]) + + population_new = numpy.array(population_new, + dtype=self.gene_type[0]) + else: + population = numpy.array(population.copy()) + population_new = numpy.zeros(shape=population.shape, + dtype=object) + for gene_idx in range(population.shape[1]): + # Round the numbers first then change the data type. + # This solves issues with some data types such as numpy.float32. + if self.gene_type[gene_idx][1] is None: + # Do not round. + population_new[:, gene_idx] = population[:, gene_idx] + else: + # This block is reached only for non-integer data types (i.e. float). + population_new[:, gene_idx] = numpy.round(numpy.array(population[:, gene_idx], float), + self.gene_type[gene_idx][1]) + # Once rounding is done, change the data type. + # population_new[:, gene_idx] = numpy.asarray(population_new[:, gene_idx], dtype=self.gene_type[gene_idx][0]) + # Use a for loop to maintain the data type of each individual gene. + for sol_idx in range(population.shape[0]): + population_new[sol_idx, gene_idx] = self.gene_type[gene_idx][0](population_new[sol_idx, gene_idx]) + return population_new + + def change_gene_dtype_and_round(self, + gene_index, + gene_value): + """ + Change the data type and round a single gene value or a vector of values FOR THE SAME GENE. E.g., the input could be 6 or [6, 7, 8]. + + It accepts 2 parameters: + -gene_index: The index of the target gene. + -gene_value: The gene value. + + If gene_value has a single value, then it returns a single number with the type changed and value rounded. If gene_value is a vector, then a vector is returned after changing the data type and rounding. + """ + + if self.gene_type_single == True: + dtype = self.gene_type[0] + if self.gene_type[1] is None: + # No rounding for this gene. Use the old gene value. + round_precision = None + else: + round_precision = self.gene_type[1] + else: + dtype = self.gene_type[gene_index][0] + if self.gene_type[gene_index][1] is None: + # No rounding for this gene. Use the old gene value. + round_precision = None + else: + round_precision = self.gene_type[gene_index][1] + + # Sometimes the values represent the gene_space when it is not nested (e.g. gene_space=range(10)) + # Copy it to avoid changing the original gene_space. + gene_value = [gene_value].copy() + + # Round the number before changing its data type to avoid precision loss for some data types like numpy.float32. + if round_precision is None: + pass + else: + gene_value = numpy.round(gene_value, round_precision) + + gene_value_new = numpy.asarray(gene_value, dtype=dtype) + gene_value_new = gene_value_new[0] + + return gene_value_new + + def mutation_change_gene_dtype_and_round(self, + random_value, + gene_index, + gene_value, + mutation_by_replacement): + """ + Change the data type and round the random value used to apply mutation. + + It accepts: + -random_value: The random value to change its data type. + -gene_index: The index of the target gene. + -gene_value: The gene value before mutation. Only used if mutation_by_replacement=False and gene_type_single=False. + -mutation_by_replacement: A flag indicating whether mutation by replacement is enabled or not. The reason is to make this helper method usable while generating the initial population. In this case, mutation_by_replacement does not matter and should be considered False. + + It returns the new value after changing the data type and being rounded. + """ + + if mutation_by_replacement: + # If the mutation_by_replacement attribute is True, then the random value replaces the current gene value. + gene_value = random_value + else: + # If the mutation_by_replacement attribute is False, then the random value is added to the gene value. + gene_value = gene_value + random_value + + gene_value_new = self.change_gene_dtype_and_round(gene_index=gene_index, + gene_value=gene_value) + return gene_value_new + + def filter_gene_values_by_constraint(self, + values, + solution, + gene_idx): + + """ + Filter the random values generated for mutation based on whether they meet the gene constraint in the gene_constraint parameter. + + It accepts: + -values: The values to filter. + -solution: The solution containing the target gene. + -gene_idx: The index of the gene in the solution. + + It returns None if no values satisfy the constraint. Otherwise, an array of values that satisfy the constraint is returned. + """ + + # A list of the indices where the random values satisfy the constraint. + filtered_values_indices = [] + # A temporary solution to avoid changing the original solution. + solution_tmp = solution.copy() + # Loop through the random values to filter the ones satisfying the constraint. + for value_idx, value in enumerate(values): + solution_tmp[gene_idx] = value + # Check if the constraint is satisfied. + if self.gene_constraint[gene_idx](solution_tmp): + # The current value satisfies the constraint. + filtered_values_indices.append(value_idx) + + # After going through all the values, check if any value satisfies the constraint. + if len(filtered_values_indices) > 0: + # At least one value was found that meets the gene constraint. + pass + else: + # No value found for the current gene that satisfies the constraint. + if not self.suppress_warnings: + warnings.warn(f"No value found for the gene at index {gene_idx} with value {solution[gene_idx]} that satisfies its gene constraint.") + return None + + filtered_values = values[filtered_values_indices] + + return filtered_values + + def get_gene_dtype(self, gene_index): + + """ + Returns the data type of the gene by its index. + + It accepts a single parameter: + -gene_index: The index of the gene to get its data type. Only used if each gene has its own data type. + + It returns the data type of the gene. + """ + + if self.gene_type_single == True: + dtype = self.gene_type + else: + dtype = self.gene_type[gene_index] + return dtype + + def get_random_mutation_range(self, gene_index): + + """ + Returns the minimum and maximum values of the mutation range. + + It accepts a single parameter: + -gene_index: The index of the gene to get its range. Only used if the gene has a specific mutation range. + + It returns the minimum and maximum values of the gene mutation range. + """ + + # We can use either random_mutation_min_val or random_mutation_max_val. + if type(self.random_mutation_min_val) in self.supported_int_float_types: + range_min = self.random_mutation_min_val + range_max = self.random_mutation_max_val + else: + range_min = self.random_mutation_min_val[gene_index] + range_max = self.random_mutation_max_val[gene_index] + return range_min, range_max + + def get_initial_population_range(self, gene_index): + + """ + Returns the minimum and maximum values of the initial population range. + + It accepts a single parameter: + -gene_index: The index of the gene to get its range. Only used if the gene has a specific range + + It returns the minimum and maximum values of the gene initial population range. + """ + + # We can use either init_range_low or init_range_high. + if type(self.init_range_low) in self.supported_int_float_types: + range_min = self.init_range_low + range_max = self.init_range_high + else: + range_min = self.init_range_low[gene_index] + range_max = self.init_range_high[gene_index] + return range_min, range_max + + def generate_gene_value_from_space(self, + gene_idx, + mutation_by_replacement, + solution=None, + gene_value=None, + sample_size=1): + """ + Generate/select one or more values for the gene from the gene space. + + It accepts: + -gene_idx: The index of the gene in the solution. + -mutation_by_replacement: A flag indicating whether mutation by replacement is enabled or not. The reason is to make this helper method usable while generating the initial population. In this case, mutation_by_replacement does not matter and should be considered False. + -solution (iterable, optional): The solution where we need to generate a gene. Needed if you are selecting a single value (sample_size=1) to select a value that respects the allow_duplicate_genes parameter instead of selecting a value randomly. If None, then the gene value is selected randomly. + -gene_value (int, optional): The original gene value before applying mutation. Needed if you are calling this method to apply mutation. If None, then a sample is created from the gene space without being summed to the gene value. + -sample_size (int, optional): The number of random values to generate. It tries to generate a number of values up to a maximum of sample_size. But it is not always guaranteed because the total number of values might not be enough or the random generator creates duplicate random values. For int data types, it could be None to keep all the values. For float data types, a None value returns only a single value. + + It returns, + -A single numeric value if sample_size=1. Or + -An array with number of maximum number of values equal to sample_size if sample_size>1. + """ + + if gene_value is None: + # Use the initial population range. + range_min, range_max = self.get_initial_population_range(gene_index=gene_idx) + else: + # Use the mutation range. + range_min, range_max = self.get_random_mutation_range(gene_idx) + + if self.gene_space_nested: + # Returning the current gene space from the 'gene_space' attribute. + # It is used to determine the way of selecting the next gene value: + # 1) List/NumPy Array: Whether it has only one value, multiple values, or one of its values is None. + # 2) Fixed Numeric Value + # 3) None + # 4) Dict: Whether the dict has the key `step` or not. + if type(self.gene_space[gene_idx]) in [numpy.ndarray, list]: + # Get the gene space from the `gene_space_unpacked` property because it undergoes data type change and rounded. + curr_gene_space = self.gene_space_unpacked[gene_idx].copy() + elif type(self.gene_space[gene_idx]) in pygad.GA.supported_int_float_types: + # Get the gene space from the `gene_space_unpacked` property because it undergoes data type change and rounded. + curr_gene_space = self.gene_space_unpacked[gene_idx] + else: + curr_gene_space = self.gene_space[gene_idx] + + if type(curr_gene_space) in pygad.GA.supported_int_float_types: + # If the gene space is simply a single numeric value (e.g. 5), use it as the new gene value. + value_from_space = curr_gene_space + elif curr_gene_space is None: + # 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'. + rand_val = numpy.random.uniform(low=range_min, + high=range_max, + size=sample_size) + if mutation_by_replacement: + value_from_space = rand_val + else: + value_from_space = gene_value + rand_val + elif type(curr_gene_space) is dict: + # Selecting a value randomly from the current gene's space in the 'gene_space' attribute. + # The gene's space of type dict specifies the lower and upper limits of a gene. + if 'step' in curr_gene_space.keys(): + # When the `size` parameter is used, the numpy.random.choice() and numpy.random.uniform() functions return a NumPy array as the output even if the array has a single value (i.e. size=1). + # We have to return the output at index 0 to force a numeric value to be returned not an object of type numpy.ndarray. + # If numpy.ndarray is returned, then it will cause an issue later while using the set() function. + # Randomly select a value from a discrete range. + value_from_space = numpy.random.choice(numpy.arange(start=curr_gene_space['low'], + stop=curr_gene_space['high'], + step=curr_gene_space['step']), + size=sample_size) + else: + value_from_space = numpy.random.uniform(low=curr_gene_space['low'], + high=curr_gene_space['high'], + size=sample_size) + else: + # Selecting a value randomly from 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. + if len(curr_gene_space) == 1: + value_from_space = curr_gene_space + else: + # Change the data type and round the generated values. + curr_gene_space = self.change_gene_dtype_and_round(gene_index=gene_idx, + gene_value=curr_gene_space) + + if gene_value is None: + # Just generate the value(s) without being added to the gene value specially when initializing the population. + value_from_space = curr_gene_space + else: + # If the gene space has more than 1 value, then select a new one that is different from the current value. + # To avoid selecting the current gene value again, remove it from the current gene space and do the selection. + value_from_space = list(set(curr_gene_space) - set([gene_value])) + else: + # Selecting a value randomly from the global gene space in the 'gene_space' attribute. + # The gene's space of type dict specifies the lower and upper limits of a gene. + if type(self.gene_space) is dict: + # 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=sample_size) + else: + value_from_space = numpy.random.uniform(low=self.gene_space['low'], + high=self.gene_space['high'], + size=sample_size) + else: + curr_gene_space = list(self.gene_space).copy() + for idx in range(len(curr_gene_space)): + if curr_gene_space[idx] is None: + curr_gene_space[idx] = numpy.random.uniform(low=range_min, + high=range_max) + curr_gene_space = self.change_gene_dtype_and_round(gene_index=gene_idx, + gene_value=curr_gene_space) + + if gene_value is None: + # Just generate the value(s) without being added to the gene value specially when initializing the population. + value_from_space = curr_gene_space + else: + # If the space type is not of type dict, then a value is randomly selected from the gene_space attribute. + # To avoid selecting the current gene value again, remove it from the current gene space and do the selection. + value_from_space = list(set(curr_gene_space) - set([gene_value])) + + if len(value_from_space) == 0: + if gene_value is None: + raise ValueError(f"There are no values to select from the gene_space for the gene at index {gene_idx}.") + else: + # After removing the current gene value from the space, there are no more values. + # Then keep the current gene value. + value_from_space = gene_value + if sample_size > 1: + value_from_space = numpy.array([gene_value]) + elif sample_size == 1: + if self.allow_duplicate_genes == True: + # Select a value randomly from the current gene space. + value_from_space = random.choice(value_from_space) + else: + # We must check if the selected value will respect the allow_duplicate_genes parameter. + # Instead of selecting a value randomly, we have to select a value that will be unique if allow_duplicate_genes=False. + # Only select a value from the current gene space that is, hopefully, unique. + value_from_space = self.select_unique_value(gene_values=value_from_space, + solution=solution, + gene_index=gene_idx) + + # The gene space might be [None, 1, 7]. + # It might happen that the value None is selected. + # In this case, generate a random value out of the mutation range. + if value_from_space is None: + value_from_space = numpy.random.uniform(low=range_min, + high=range_max, + size=sample_size) + else: + value_from_space = numpy.array(value_from_space) + + # Change the data type and round the generated values. + # It has to be called here for all the missed cases. + value_from_space = self.change_gene_dtype_and_round(gene_index=gene_idx, + gene_value=value_from_space) + if sample_size == 1 and type(value_from_space) not in pygad.GA.supported_int_float_types: + value_from_space = value_from_space[0] + + return value_from_space + + def generate_gene_value_randomly(self, + range_min, + range_max, + gene_value, + gene_idx, + mutation_by_replacement, + sample_size=1, + step=1): + """ + Randomly generate one or more values for the gene. + It accepts: + -range_min: The minimum value in the range from which a value is selected. + -range_max: The maximum value in the range from which a value is selected. + -gene_value: The original gene value before applying mutation. + -gene_idx: The index of the gene in the solution. + -mutation_by_replacement: A flag indicating whether mutation by replacement is enabled or not. The reason is to make this helper method usable while generating the initial population. In this case, mutation_by_replacement does not matter and should be considered False. + -sample_size: The number of random values to generate. It tries to generate a number of values up to a maximum of sample_size. But it is not always guaranteed because the total number of values might not be enough or the random generator creates duplicate random values. For int data types, it could be None to keep all the values. For float data types, a None value returns only a single value. + -step (int, optional): The step size for generating candidate values. Defaults to 1. Only used with genes of an integer data type. + + It returns, + -A single numeric value if sample_size=1. Or + -An array with number of values equal to sample_size if sample_size>1. + """ + + gene_type = self.get_gene_dtype(gene_index=gene_idx) + if gene_type[0] in pygad.GA.supported_int_types: + random_value = numpy.asarray(numpy.arange(range_min, + range_max, + step=step), + dtype=gene_type[0]) + if sample_size is None: + # Keep all the values. + pass + else: + if sample_size >= len(random_value): + # Number of values is larger than or equal to the number of elements in random_value. + # Makes no sense to create a larger sample out of the population because it just creates redundant values. + pass + else: + # Set replace=True to avoid selecting the same value more than once. + random_value = numpy.random.choice(random_value, + size=sample_size, + replace=False) + else: + # Generating a random value. + random_value = numpy.asarray(numpy.random.uniform(low=range_min, + high=range_max, + size=sample_size), + dtype=object) + + # Change the random mutation value data type. + for idx, val in enumerate(random_value): + random_value[idx] = self.mutation_change_gene_dtype_and_round(random_value[idx], + gene_idx, + gene_value, + mutation_by_replacement=mutation_by_replacement) + + # Rounding different values could return the same value multiple times. + # For example, 2.8 and 2.7 will be 3.0. + # Use the unique() function to avoid any duplicates. + random_value = numpy.unique(random_value) + + if sample_size == 1: + random_value = random_value[0] + + return random_value + + def generate_gene_value(self, + gene_value, + gene_idx, + mutation_by_replacement, + solution=None, + range_min=None, + range_max=None, + sample_size=1, + step=1): + """ + Generate one or more values for the gene either randomly or from the gene space. It acts as a router. + It accepts: + -gene_value: The original gene value before applying mutation. + -gene_idx: The index of the gene in the solution. + -mutation_by_replacement: A flag indicating whether mutation by replacement is enabled or not. The reason is to make this helper method usable while generating the initial population. In this case, mutation_by_replacement does not matter and should be considered False. + -solution (iterable, optional): The solution where we need to generate a gene. Needed if you are selecting a single value (sample_size=1) to select a value that respects the allow_duplicate_genes parameter instead of selecting a value randomly. If None, then the gene value is selected randomly. + -range_min (int, optional): The minimum value in the range from which a value is selected. It must be passed for generating the gene value randomly because we cannot decide whether it is the range for the initial population (init_range_low and init_range_high) or mutation (random_mutation_min_val and random_mutation_max_val). + -range_max (int, optional): The maximum value in the range from which a value is selected. It must be passed for generating the gene value randomly because we cannot decide whether it is the range for the initial population (init_range_low and init_range_high) or mutation (random_mutation_min_val and random_mutation_max_val). + -sample_size: The number of random values to generate/select and return. It tries to generate a number of values up to a maximum of sample_size. But it is not always guaranteed because the total number of values might not be enough or the random generator creates duplicate random values. For int data types, it could be None to keep all the values. For float data types, a None value returns only a single value. + -step (int, optional): The step size for generating candidate values. Defaults to 1. Only used with genes of an integer data type. + + It returns, + -A single numeric value if sample_size=1. Or + -An array with number of values equal to sample_size if sample_size>1. + """ + if self.gene_space is None: + output = self.generate_gene_value_randomly(range_min=range_min, + range_max=range_max, + gene_value=gene_value, + gene_idx=gene_idx, + mutation_by_replacement=mutation_by_replacement, + sample_size=sample_size, + step=step) + else: + output = self.generate_gene_value_from_space(gene_value=gene_value, + gene_idx=gene_idx, + mutation_by_replacement=mutation_by_replacement, + solution=solution, + sample_size=sample_size) + return output + + def get_valid_gene_constraint_values(self, + range_min, + range_max, + gene_value, + gene_idx, + mutation_by_replacement, + solution, + sample_size=100, + step=1): + """ + Generate/select values for the gene that satisfy the constraint. The values could be generated randomly or from the gene space. + The number of returned values is at its maximum equal to the sample_size parameter. + It accepts: + -range_min: The minimum value in the range from which a value is selected. + -range_max: The maximum value in the range from which a value is selected. + -gene_value: The original gene value before applying mutation. + -gene_idx: The index of the gene in the solution. + -mutation_by_replacement: A flag indicating whether mutation by replacement is enabled or not. The reason is to make this helper method usable while generating the initial population. In this case, mutation_by_replacement does not matter and should be considered False. + -solution: The solution in which the gene exists. + -sample_size: The number of values to generate or select. It tries to generate a number of values up to a maximum of sample_size. But it is not always guaranteed because the total number of values might not be enough or the random generator creates duplicate random values. + -step (int, optional): The step size for generating candidate values. Defaults to 1. Only used with genes of an integer data type. + + It returns, + -A single numeric value if sample_size=1. Or + -An array with number of values equal to sample_size if sample_size>1. Or + -None if no value found that satisfies the constraint. + """ + + # Either generate the values randomly or from the gene space. + values = self.generate_gene_value(range_min=range_min, + range_max=range_max, + gene_value=gene_value, + gene_idx=gene_idx, + mutation_by_replacement=mutation_by_replacement, + sample_size=sample_size, + step=step) + # It returns None if no value found that satisfies the constraint. + values_filtered = self.filter_gene_values_by_constraint(values=values, + solution=solution, + gene_idx=gene_idx) + return values_filtered diff --git a/pygad/helper/unique.py b/pygad/helper/unique.py index a74a9c0..60b2507 100644 --- a/pygad/helper/unique.py +++ b/pygad/helper/unique.py @@ -15,7 +15,7 @@ def solve_duplicate_genes_randomly(self, max_val, mutation_by_replacement, gene_type, - num_trials=10): + sample_size=100): """ Resolves duplicates in a solution by randomly selecting new values for the duplicate genes. @@ -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. Only works for floating-point gene types. + sample_size (int): The maximum number of random values to generate to find a unique value. Returns: tuple: @@ -42,26 +42,30 @@ def solve_duplicate_genes_randomly(self, 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 + dtype = self.get_gene_dtype(gene_index=duplicate_index) + + if type(min_val) in self.supported_int_float_types: + min_val_gene = min_val + max_val_gene = max_val else: - dtype = gene_type[duplicate_index] + min_val_gene = min_val[duplicate_index] + max_val_gene = max_val[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, + min_val=min_val_gene, + max_val=max_val_gene, 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, + min_val=min_val_gene, + max_val=max_val_gene, mutation_by_replacement=mutation_by_replacement, gene_type=gene_type, - num_trials=num_trials) + sample_size=sample_size) if temp_val in new_solution: num_unsolved_duplicates = num_unsolved_duplicates + 1 @@ -80,16 +84,19 @@ def solve_duplicate_genes_randomly(self, def solve_duplicate_genes_by_space(self, solution, gene_type, - num_trials=10, + mutation_by_replacement, + sample_size=100, build_initial_pop=False): - + """ 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. + mutation_by_replacement (bool): Indicates if mutation is performed by replacement. + sample_size (int, optional): The maximum number of attempts to resolve duplicates by selecting values from the gene space. + build_initial_pop (bool, optional): Indicates if initial population should be built. Returns: tuple: @@ -102,40 +109,28 @@ def solve_duplicate_genes_by_space(self, _, 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, + new_solution, not_unique_indices, num_unsolved_duplicates = self.unique_genes_by_space(solution=new_solution, gene_type=gene_type, not_unique_indices=not_unique_indices, - num_trials=10, + sample_size=sample_size, + mutation_by_replacement=mutation_by_replacement, 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) + # 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, num_unsolved_duplicates def unique_int_gene_from_range(self, @@ -163,32 +158,40 @@ def unique_int_gene_from_range(self, 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 - - # 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 + if self.gene_constraint and self.gene_constraint[gene_index]: + # A unique value is created out of the values that satisfy the constraint. + # sample_size=None to return all the values. + random_values = self.get_valid_gene_constraint_values(range_min=min_val, + range_max=max_val, + gene_value=solution[gene_index], + gene_idx=gene_index, + mutation_by_replacement=mutation_by_replacement, + solution=solution, + sample_size=None, + step=step) + # If there is no value satisfying the constraint, then return the current gene value. + if random_values is None: + return solution[gene_index] + else: + 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]) - - selected_value = self.select_unique_value(gene_values=all_gene_values, + # There is no constraint for the current gene. Return the same range. + # sample_size=None to return all the values. + random_values = self.generate_gene_value(range_min=min_val, + range_max=max_val, + gene_value=solution[gene_index], + gene_idx=gene_index, + solution=solution, + mutation_by_replacement=mutation_by_replacement, + sample_size=None, + step=step) + + selected_value = self.select_unique_value(gene_values=random_values, solution=solution, gene_index=gene_index) - selected_value = dtype[0](selected_value) + # The gene_type is of the form [type, precision] + selected_value = gene_type[0](selected_value) return selected_value @@ -199,7 +202,7 @@ def unique_float_gene_from_range(self, max_val, mutation_by_replacement, gene_type, - num_trials=10): + sample_size=100): """ Finds a unique floating-point value for a specific gene in a solution. @@ -211,46 +214,45 @@ def unique_float_gene_from_range(self, 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. + sample_size (int): The maximum number of random values to generate to find a unique value. 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 - - # We cannot have a list of all values out of a continous range. - # Solution is to create a subset (e.g. 100) of all the values. - some_gene_values = numpy.random.uniform(low=min_val, - high=max_val, - size=100) - - # 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: - some_gene_values = some_gene_values + 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. - some_gene_values = numpy.round(numpy.asarray(some_gene_values, - dtype[0]), - dtype[1]) + if self.gene_constraint and self.gene_constraint[gene_index]: + # A unique value is created out of the values that satisfy the constraint. + values = self.get_valid_gene_constraint_values(range_min=min_val, + range_max=max_val, + gene_value=solution[gene_index], + gene_idx=gene_index, + mutation_by_replacement=mutation_by_replacement, + solution=solution, + sample_size=sample_size) + # If there is no value satisfying the constraint, then return the current gene value. + if values is None: + return solution[gene_index] + else: + pass else: - # There is no precision and rounding the number is not needed. The type is [type, None] - # Just convert the data type. - some_gene_values = numpy.asarray(some_gene_values, - dtype[0]) - - selected_value = self.select_unique_value(gene_values=some_gene_values, + # There is no constraint for the current gene. Return the same range. + values = self.generate_gene_value(range_min=min_val, + range_max=max_val, + gene_value=solution[gene_index], + gene_idx=gene_index, + solution=solution, + mutation_by_replacement=mutation_by_replacement, + sample_size=sample_size) + + selected_value = self.select_unique_value(gene_values=values, solution=solution, gene_index=gene_index) return selected_value - def select_unique_value(self, gene_values, solution, gene_index): + def select_unique_value(self, + gene_values, + solution, + gene_index): """ Select a unique value (if possible) from a list of gene values. @@ -258,27 +260,34 @@ def select_unique_value(self, gene_values, solution, gene_index): Args: gene_values (NumPy Array): An array of values from which a unique value should be selected. 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. Returns: selected_gene: The new (hopefully unique) value of the gene. If no unique value can be found, the original gene value is returned. """ values_to_select_from = list(set(list(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 cannot find a value to prevent duplicates.") - selected_value = solution[gene_index] + if solution[gene_index] is None: + # The initial population is created as an empty array (numpy.empty()). + # If we are assigning values to the initial population, then the gene value is already None. + # If the gene value is None, then we do not have an option other than selecting a value even if it causes duplicates. + # If there is no value that is unique to the solution, then select any of the current values randomly from the current set of gene values. + selected_value = random.choice(gene_values) + else: + # If the gene is not None, then just keep its current value as long as there are no values that make it unique. + selected_value = solution[gene_index] else: selected_value = random.choice(values_to_select_from) - return selected_value def unique_genes_by_space(self, - new_solution, + solution, gene_type, - not_unique_indices, - num_trials=10, + not_unique_indices, + mutation_by_replacement, + sample_size=100, build_initial_pop=False): """ @@ -286,10 +295,12 @@ def unique_genes_by_space(self, 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. + solution (list): A solution containing genes with duplicate values. 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. + mutation_by_replacement (bool): Indicates if mutation is performed by replacement. + sample_size (int): The maximum number of attempts to resolve duplicates for each gene. Only works for floating-point numbers. + build_initial_pop (bool, optional): Indicates if initial population should be built. Returns: tuple: @@ -300,233 +311,84 @@ def unique_genes_by_space(self, num_unsolved_duplicates = 0 for duplicate_index in not_unique_indices: - temp_val = self.unique_gene_by_space(solution=new_solution, + temp_val = self.unique_gene_by_space(solution=solution, gene_idx=duplicate_index, gene_type=gene_type, - build_initial_pop=build_initial_pop, - num_trials=num_trials) + mutation_by_replacement=mutation_by_replacement, + sample_size=sample_size, + build_initial_pop=build_initial_pop) - if temp_val in new_solution: - # self.logger.info("temp_val, duplicate_index", temp_val, duplicate_index, new_solution) + if temp_val in 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.") + 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: - new_solution[duplicate_index] = temp_val + 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(new_solution))) - set(unique_gene_indices) - # self.logger.info("not_unique_indices INSIDE", not_unique_indices) + _, unique_gene_indices = numpy.unique(solution, return_index=True) + not_unique_indices = set(range(len(solution))) - set(unique_gene_indices) - return new_solution, not_unique_indices, num_unsolved_duplicates + return solution, not_unique_indices, num_unsolved_duplicates def unique_gene_by_space(self, solution, gene_idx, - gene_type, - build_initial_pop=False, - num_trials=10): - - """ - 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). - 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. """ - - if self.gene_space_nested: - if type(self.gene_space[gene_idx]) in [numpy.ndarray, list, tuple]: - # Return the current gene space from the 'gene_space' attribute. - curr_gene_space = list(self.gene_space[gene_idx]).copy() - else: - # Return the entire gene space from the 'gene_space' attribute. - # curr_gene_space = list(self.gene_space[gene_idx]).copy() - curr_gene_space = self.gene_space[gene_idx] - - # If the gene space has only a single value, use it as the new gene value. - if type(curr_gene_space) in pygad.GA.supported_int_float_types: - value_from_space = curr_gene_space - # 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: - 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 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=dtype) - 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 - - """ - 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: - 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: - 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: - 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)[0] - else: - 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. - if len(curr_gene_space) == 1: - value_from_space = curr_gene_space[0] - if not self.suppress_warnings: warnings.warn(f"You set 'allow_duplicate_genes=False' but the space of the gene with index {gene_idx} has only a single value. Thus, duplicates are possible.") - # If the gene space has more than 1 value, then select a new one that is different from the current value. - else: - values_to_select_from = list(set(curr_gene_space) - set(solution)) - - if len(values_to_select_from) == 0: - # DEEP-DUPLICATE-REMOVAL-NEEDED - # Search by this phrase to find where deep duplicates removal should be applied. - - # Reaching this block means there is no value in the gene space of this gene to solve the duplicates. - # To solve the duplicate between the 2 genes, the solution is to change the value of a third gene that makes a room to solve the duplicate. - - if not self.suppress_warnings: warnings.warn("You set 'allow_duplicate_genes=False' but the gene space does not have enough values to prevent duplicates.") - - solution2 = self.solve_duplicates_deeply(solution) - if solution2 is None: - # Cannot solve duplicates. At the moment, we are changing the value of a third gene to solve the duplicates between 2 genes. - # Maybe a 4th, 5th, 6th, or even more genes need to be changed to solve the duplicates. - pass - else: - solution = solution2 - value_from_space = solution[gene_idx] - - else: - value_from_space = random.choice(values_to_select_from) - else: - # 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: - 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: - 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: - # 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)[0] - else: - 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. - # This only leaves the unique values that could be selected for the gene. - values_to_select_from = list(set(self.gene_space) - set(solution)) + gene_type, + mutation_by_replacement, + sample_size=100, + build_initial_pop=False): - if len(values_to_select_from) == 0: - if not self.suppress_warnings: warnings.warn("You set 'allow_duplicate_genes=False' but the gene space does not have enough values to prevent duplicates.") - value_from_space = solution[gene_idx] - else: - value_from_space = random.choice(values_to_select_from) - - if value_from_space is None: - 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 + """ + Returns a unique value for a specific gene based on its value space to resolve duplicates. - value_from_space = numpy.random.uniform(low=low, - high=high, - size=1)[0] + 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). + mutation_by_replacement (bool): Indicates if mutation is performed by replacement. + sample_size (int): The maximum number of attempts to resolve duplicates for each gene. Only works for floating-point numbers. + build_initial_pop (bool, optional): Indicates if initial population should be built. - # 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: - dtype = gene_type - else: - dtype = gene_type[gene_idx] + Returns: + Any: A unique value for the gene, if one exists; otherwise, the original gene value. + """ - if not dtype[1] is None: - value_from_space = numpy.round(dtype[0](value_from_space), - dtype[1]) + # When gene_value is None, this forces the gene value generators to select a value for use by the initial population. + # Otherwise, it considers selecting a value for mutation. + if build_initial_pop: + gene_value = None + else: + gene_value = solution[gene_idx] + + if self.gene_constraint and self.gene_constraint[gene_idx]: + # A unique value is created out of the values that satisfy the constraint. + values = self.get_valid_gene_constraint_values(range_min=None, + range_max=None, + gene_value=gene_value, + gene_idx=gene_idx, + mutation_by_replacement=mutation_by_replacement, + solution=solution, + sample_size=sample_size) + # If there is no value satisfying the constraint, then return the current gene value. + if values is None: + return solution[gene_idx] else: - value_from_space = dtype[0](value_from_space) + pass + else: + # There is no constraint for the current gene. Return the same range. + values = self.generate_gene_value(range_min=None, + range_max=None, + gene_value=gene_value, + gene_idx=gene_idx, + solution=solution, + mutation_by_replacement=mutation_by_replacement, + sample_size=sample_size) + + selected_value = self.select_unique_value(gene_values=values, + solution=solution, + gene_index=gene_idx) - return value_from_space + return selected_value def find_two_duplicates(self, solution, @@ -534,10 +396,13 @@ def find_two_duplicates(self, """ Identifies the first occurrence of a duplicate gene in the solution. + Args: + solution: The solution containing genes with duplicate values. + gene_space_unpacked: A list of values from the gene space to choose the values that resolve duplicates. + Returns: - tuple: - int: The index of the first gene with a duplicate value. - Any: The value of the duplicate gene. + int: The index of the first gene with a duplicate value. + Any: The value of the duplicate gene. """ for gene in set(solution): @@ -551,18 +416,18 @@ def find_two_duplicates(self, # This means there is no way to solve the duplicates between the genes. # Because the space of the duplicates genes only has a single value and there is no alternatives. return None, gene - + def unpack_gene_space(self, range_min, range_max, - num_values_from_inf_range=100): + sample_size_from_inf_range=100): """ 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()`. + sample_size_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. @@ -585,11 +450,15 @@ def unpack_gene_space(self, else: gene_space_unpacked = numpy.linspace(start=self.gene_space['low'], stop=self.gene_space['high'], - num=num_values_from_inf_range, + num=sample_size_from_inf_range, endpoint=False) if self.gene_type_single == True: # Change the data type. + for idx in range(len(gene_space_unpacked)): + if gene_space_unpacked[idx] is None: + gene_space_unpacked[idx] = numpy.random.uniform(low=range_min, + high=range_max) gene_space_unpacked = numpy.array(gene_space_unpacked, dtype=self.gene_type[0]) if not self.gene_type[1] is None: @@ -626,10 +495,7 @@ 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: - dtype = self.gene_type - else: - dtype = self.gene_type[space_idx] + dtype = self.get_gene_dtype(gene_index=space_idx) if dtype[0] in pygad.GA.supported_int_types: if 'step' in space.keys(): @@ -648,7 +514,7 @@ def unpack_gene_space(self, else: gene_space_unpacked[space_idx] = numpy.linspace(start=space['low'], stop=space['high'], - num=num_values_from_inf_range, + num=sample_size_from_inf_range, endpoint=False) elif type(space) in [numpy.ndarray, list, tuple]: # list/tuple/numpy.ndarray @@ -664,10 +530,7 @@ def unpack_gene_space(self, size=1)[0] gene_space_unpacked[space_idx][idx] = random_value - if self.gene_type_single == True: - dtype = self.gene_type - else: - dtype = self.gene_type[space_idx] + dtype = self.get_gene_dtype(gene_index=space_idx) # Change the data type. gene_space_unpacked[space_idx] = numpy.array(gene_space_unpacked[space_idx], @@ -687,9 +550,6 @@ def solve_duplicates_deeply(self, 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. @@ -714,10 +574,6 @@ def solve_duplicates_deeply(self, # This removes all the occurrences of this value. gene_other_values = [v for v in gene_other_values if v != duplicate_value] - # The remove() function only removes the first occurrence of the value. - # Do not use it. - # gene_other_values.remove(duplicate_value) - # Two conditions to solve the duplicates of the value D: # 1. From gene_other_values, select a value V such that it is available in the gene space of another gene X. # 2. Find an alternate value for the gene X that will not cause any duplicates. diff --git a/pygad/pygad.py b/pygad/pygad.py index 1a34b9a..0ed9d82 100644 --- a/pygad/pygad.py +++ b/pygad/pygad.py @@ -15,6 +15,7 @@ class GA(utils.parent_selection.ParentSelection, utils.mutation.Mutation, utils.nsga2.NSGA2, helper.unique.Unique, + helper.misc.Helper, visualize.plot.Plot): supported_int_types = [int, numpy.int8, numpy.int16, numpy.int32, numpy.int64, @@ -50,6 +51,7 @@ def __init__(self, random_mutation_max_val=1.0, gene_space=None, gene_constraint=None, + sample_size=100, allow_duplicate_genes=True, on_start=None, on_fitness=None, @@ -72,7 +74,7 @@ def __init__(self, num_parents_mating: Number of solutions to be selected as parents in the mating pool. fitness_func: Accepts a function/method and returns the fitness value of the solution. In PyGAD 2.20.0, a third parameter is passed referring to the 'pygad.GA' instance. If method, then it must accept 4 parameters where the fourth one refers to the method's object. - fitness_batch_size: Added in PyGAD 2.19.0. Supports calculating the fitness in batches. If the value is 1 or None, then the fitness function is called for each invidiaul solution. If given another value X where X is neither 1 nor None (e.g. X=3), then the fitness function is called once for each X (3) solutions. + fitness_batch_size: Added in PyGAD 2.19.0. Supports calculating the fitness in batches. If the value is 1 or None, then the fitness function is called for each individual solution. If given another value X where X is neither 1 nor None (e.g. X=3), then the fitness function is called once for each X (3) solutions. 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. sol_per_pop: Number of solutions in the population. @@ -80,20 +82,20 @@ def __init__(self, init_range_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. 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). + # It is OK to set the value 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, 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. + 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 (http://webs.um.es/fernan) for editing this sentence. K_tournament: When the value of 'parent_selection_type' is 'tournament', the 'K_tournament' parameter specifies the number of solutions from which a parent is selected randomly. - keep_elitism: 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. + keep_elitism: 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 from 0, then the keep_parents parameter will have no effect. - crossover_type: Type of the crossover opreator. 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_type: Type of the crossover operator. 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: The probability of selecting a solution for the crossover operation. If the solution probability is <= crossover_probability, the solution is selected. The value must be between 0 and 1 inclusive. - mutation_type: Type of the mutation opreator. 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. + mutation_type: Type of the mutation operator. 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. mutation_probability: The probability of selecting a gene for the mutation operation. If the gene probability is <= mutation_probability, the gene is selected. It accepts either a single value for fixed mutation or a list/tuple/numpy.ndarray of 2 values for adaptive mutation. The values must be between 0 and 1 inclusive. If specified, then no need for the 2 parameters mutation_percent_genes and mutation_num_genes. mutation_by_replacement: An optional bool parameter. It works only when the selected type of mutation is random (mutation_type="random"). In this case, setting 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. @@ -106,14 +108,15 @@ def __init__(self, gene_space: It accepts a list of all possible values of the gene. This list is used in the mutation step. Should be used only if the gene space is a set of discrete values. No need for the 2 parameters (random_mutation_min_val and random_mutation_max_val) if the parameter gene_space exists. Added in PyGAD 2.5.0. In PyGAD 2.11.0, the gene_space can be assigned a dict. gene_constraint: It accepts a list of constraints for the genes. Each constraint is a Python function. Added in PyGAD 3.5.0. + sample_size: To select a gene value that respects a constraint, this variable defines the size of the sample from which a value is selected randomly. Useful if either allow_duplicate_genes or gene_constraint is used. Added in PyGAD 3.5.0. - on_start: 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: 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: Accepts a function/method to be called after selecting the parents that mates. If function, then it must accept 2 parameters: the first one represents the instance of the genetic algorithm and the second one represents the selected parents. 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: Accepts a function/method to be called each time the crossover operation is applied. If function, then it must accept 2 parameters: the first one represents the instance of the genetic algorithm and the second one represents the offspring generated using crossover. 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_mutation: Accepts a function/method to be called each time the mutation operation is applied. If function, then it 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. 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_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. + on_start: Accepts a function/method to be called only once before the genetic algorithm starts its evolution. If functioned, 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: Accepts a function/method to be called after calculating the fitness values of all solutions in the population. If functioned, 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: Accepts a function/method to be called after selecting the parents that mates. If functioned, then it must accept 2 parameters: the first one represents the instance of the genetic algorithm and the second one represents the selected parents. 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: Accepts a function/method to be called each time the crossover operation is applied. If functioned, then it must accept 2 parameters: the first one represents the instance of the genetic algorithm and the second one represents the offspring generated using crossover. 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_mutation: Accepts a function/method to be called each time the mutation operation is applied. If functioned, then it 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. 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_generation: Accepts a function/method to be called after each generation. If functioned, 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 functioned, 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. 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. @@ -174,7 +177,7 @@ def __init__(self, numpy.random.seed(self.random_seed) random.seed(self.random_seed) - # If suppress_warnings is bool and its valud is False, then print warning messages. + # If suppress_warnings is bool and its value is False, then print warning messages. if type(suppress_warnings) is bool: self.suppress_warnings = suppress_warnings else: @@ -188,6 +191,19 @@ def __init__(self, self.mutation_by_replacement = mutation_by_replacement + # Validate the sample_size parameter. + if type(sample_size) in GA.supported_int_types: + if sample_size > 0: + pass + else: + self.valid_parameters = False + raise ValueError(f"The value of the sample_size parameter must be > 0 but the value ({sample_size}) found.") + else: + self.valid_parameters = False + raise TypeError(f"The type of the sample_size parameter must be integer but the value ({sample_size}) of type ({type(sample_size)}) found.") + + self.sample_size = sample_size + # Validate allow_duplicate_genes if not (type(allow_duplicate_genes) is bool): self.valid_parameters = False @@ -265,18 +281,6 @@ def __init__(self, self.gene_space = gene_space - # Validate init_range_low and init_range_high - # if type(init_range_low) in GA.supported_int_float_types: - # if type(init_range_high) in GA.supported_int_float_types: - # self.init_range_low = init_range_low - # self.init_range_high = init_range_high - # else: - # self.valid_parameters = False - # raise ValueError(f"The value passed to the 'init_range_high' parameter must be either integer or floating-point number but the value ({init_range_high}) of type {type(init_range_high)} found.") - # else: - # self.valid_parameters = False - # raise ValueError(f"The value passed to the 'init_range_low' parameter must be either integer or floating-point number but the value ({init_range_low}) of type {type(init_range_low)} found.") - # Validate init_range_low and init_range_high if type(init_range_low) in GA.supported_int_float_types: if type(init_range_high) in GA.supported_int_float_types: @@ -287,13 +291,6 @@ def __init__(self, self.valid_parameters = False raise TypeError(f"Type mismatch between the 2 parameters 'init_range_low' {type(init_range_low)} and 'init_range_high' {type(init_range_high)}.") elif type(init_range_low) in [list, tuple, numpy.ndarray]: - # The self.num_genes attribute is not created yet. - # if len(init_range_low) == self.num_genes: - # pass - # else: - # self.valid_parameters = False - # raise ValueError(f"The length of the 'init_range_low' parameter is {len(init_range_low)} which is different from the number of genes {self.num_genes}.") - # Get the number of genes before validating the num_genes parameter. if num_genes is None: if initial_population is None: @@ -409,7 +406,7 @@ def __init__(self, if initial_population is None: if (sol_per_pop is None) or (num_genes is None): self.valid_parameters = False - raise TypeError("Error creating the initial population:\n\nWhen the parameter 'initial_population' is None, then the 2 parameters 'sol_per_pop' and 'num_genes' cannot be None too.\nThere are 2 options to prepare the initial population:\n1) Assinging the initial population to the 'initial_population' parameter. In this case, the values of the 2 parameters sol_per_pop and num_genes will be deduced.\n2) Assign integer values to the 'sol_per_pop' and 'num_genes' parameters so that PyGAD can create the initial population automatically.") + raise TypeError("Error creating the initial population:\n\nWhen the parameter 'initial_population' is None, then the 2 parameters 'sol_per_pop' and 'num_genes' cannot be None too.\nThere are 2 options to prepare the initial population:\n1) Assigning the initial population to the 'initial_population' parameter. In this case, the values of the 2 parameters sol_per_pop and num_genes will be deduced.\n2) Assign integer values to the 'sol_per_pop' and 'num_genes' parameters so that PyGAD can create the initial population automatically.") elif (type(sol_per_pop) is int) and (type(num_genes) is int): # Validating the number of solutions in the population (sol_per_pop) if sol_per_pop <= 0: @@ -431,11 +428,9 @@ def __init__(self, # Number of solutions in the population. self.sol_per_pop = sol_per_pop - self.initialize_population(low=self.init_range_low, - high=self.init_range_high, - allow_duplicate_genes=allow_duplicate_genes, - mutation_by_replacement=True, - gene_type=self.gene_type) + self.initialize_population(allow_duplicate_genes=allow_duplicate_genes, + gene_type=self.gene_type, + gene_constraint=gene_constraint) else: self.valid_parameters = False raise TypeError(f"The expected type of both the sol_per_pop and num_genes parameters is int but {type(sol_per_pop)} and {type(num_genes)} found.") @@ -455,45 +450,25 @@ def __init__(self, self.valid_parameters = False raise TypeError(f"The values in the initial population can be integers or floats but the value ({initial_population[row_idx][col_idx]}) of type {type(initial_population[row_idx][col_idx])} found.") - # Forcing the initial_population array to have the data type assigned to the gene_type parameter. - if self.gene_type_single == True: - if self.gene_type[1] == None: - self.initial_population = numpy.array(initial_population, - dtype=self.gene_type[0]) - else: - # This block is reached only for non-integer data types (i.e. float). - self.initial_population = numpy.round(numpy.array(initial_population, - dtype=self.gene_type[0]), - self.gene_type[1]) - else: - initial_population = numpy.array(initial_population) - self.initial_population = numpy.zeros(shape=(initial_population.shape[0], - initial_population.shape[1]), - dtype=object) - for gene_idx in range(initial_population.shape[1]): - if self.gene_type[gene_idx][1] is None: - self.initial_population[:, gene_idx] = numpy.asarray(initial_population[:, gene_idx], - dtype=self.gene_type[gene_idx][0]) - else: - # This block is reached only for non-integer data types (i.e. float). - self.initial_population[:, gene_idx] = numpy.round(numpy.asarray(initial_population[:, gene_idx], - dtype=self.gene_type[gene_idx][0]), - self.gene_type[gene_idx][1]) + # Change the data type and round all genes within the initial population. + self.initial_population = self.change_population_dtype_and_round(initial_population) - # Check if duplicates are allowed. If not, then solve any exisiting duplicates in the passed initial population. + # Check if duplicates are allowed. If not, then solve any existing duplicates in the passed initial population. if self.allow_duplicate_genes == False: for initial_solution_idx, initial_solution in enumerate(self.initial_population): if self.gene_space is None: self.initial_population[initial_solution_idx], _, _ = self.solve_duplicate_genes_randomly(solution=initial_solution, - min_val=self.init_range_low, - max_val=self.init_range_high, - mutation_by_replacement=self.mutation_by_replacement, - gene_type=self.gene_type, - num_trials=10) + min_val=self.init_range_low, + max_val=self.init_range_high, + mutation_by_replacement=True, + gene_type=self.gene_type, + sample_size=self.sample_size) else: self.initial_population[initial_solution_idx], _, _ = self.solve_duplicate_genes_by_space(solution=initial_solution, - gene_type=self.gene_type, - num_trials=10) + gene_type=self.gene_type, + sample_size=self.sample_size, + mutation_by_replacement=True, + build_initial_pop=True) # A NumPy array holding the initial population. self.population = self.initial_population.copy() @@ -504,9 +479,9 @@ def __init__(self, # The population size. self.pop_size = (self.sol_per_pop, self.num_genes) - # Round initial_population and population - self.initial_population = self.round_genes(self.initial_population) - self.population = self.round_genes(self.population) + # Change the data type and round all genes within the initial population. + self.initial_population = self.change_population_dtype_and_round(self.initial_population) + self.population = self.initial_population.copy() # In case the 'gene_space' parameter is nested, then make sure the number of its elements equals to the number of genes. if self.gene_space_nested: @@ -564,24 +539,30 @@ def __init__(self, # Validate that gene_constraint is a list or tuple and every element inside it is either None or callable. if gene_constraint: if type(gene_constraint) in [list, tuple]: - for constraint_idx, item in enumerate(gene_constraint): - # Check whether the element is None or a callable. - if item and callable(item): - if item.__code__.co_argcount == 1: - # Every callable is valid if it receives a single argument. - # This argument represents the solution. + if len(gene_constraint) == self.num_genes: + for constraint_idx, item in enumerate(gene_constraint): + # Check whether the element is None or a callable. + if item is None: pass + elif item and callable(item): + if item.__code__.co_argcount == 1: + # Every callable is valid if it receives a single argument. + # This argument represents the solution. + pass + else: + self.valid_parameters = False + raise ValueError(f"Every callable inside the gene_constraint parameter must accept a single argument representing the solution/chromosome. But the callable at index {constraint_idx} named '{item.__code__.co_name}' accepts {item.__code__.co_argcount} argument(s).") else: self.valid_parameters = False - raise ValueError(f"Every callable inside the gene_constraint parameter must accept a single argument representing the solution/chromosome. But the callable at index {constraint_idx} named '{item.__code__.co_name}' accepts {item.__code__.co_argcount} argument(s).") - else: - self.valid_parameters = False - raise TypeError(f"The expected type of an element in the 'gene_constraint' parameter is None or a callable (e.g. function). But {item} at index {constraint_idx} of type {type(item)} found.") + raise TypeError(f"The expected type of an element in the 'gene_constraint' parameter is None or a callable (e.g. function). But {item} at index {constraint_idx} of type {type(item)} found.") + else: + self.valid_parameters = False + raise ValueError(f"The number of constrains ({len(gene_constraint)}) in the 'gene_constraint' parameter must be equal to the number of genes ({self.num_genes}).") else: self.valid_parameters = False - raise TypeError(f"The expected type of the 'gene_constraint' parameter is either list or tuple. But the value {gene_constraint} of type {type(gene_constraint)} found.") + raise TypeError(f"The expected type of the 'gene_constraint' parameter is either a list or tuple. But the value {gene_constraint} of type {type(gene_constraint)} found.") else: - # It is None. + # gene_constraint is None and not used. pass self.gene_constraint = gene_constraint @@ -603,7 +584,7 @@ def __init__(self, if crossover_type is None: self.crossover = None elif inspect.ismethod(crossover_type): - # Check if the crossover_type is a method that accepts 4 paramaters. + # Check if the crossover_type is a method that accepts 4 parameters. if crossover_type.__code__.co_argcount == 4: # The crossover method assigned to the crossover_type parameter is validated. self.crossover = crossover_type @@ -611,7 +592,7 @@ def __init__(self, self.valid_parameters = False raise ValueError(f"When 'crossover_type' is assigned to a method, then this crossover method must accept 4 parameters:\n1) Expected to be the 'self' object.\n2) The selected parents.\n3) The size of the offspring to be produced.\n4) The instance from the pygad.GA class.\n\nThe passed crossover method named '{crossover_type.__code__.co_name}' accepts {crossover_type.__code__.co_argcount} parameter(s).") elif callable(crossover_type): - # Check if the crossover_type is a function that accepts 2 paramaters. + # Check if the crossover_type is a function that accepts 2 parameters. if crossover_type.__code__.co_argcount == 3: # The crossover function assigned to the crossover_type parameter is validated. self.crossover = crossover_type @@ -623,13 +604,13 @@ def __init__(self, raise TypeError(f"The expected type of the 'crossover_type' parameter is either callable or str but {type(crossover_type)} found.") else: # type crossover_type is str crossover_type = crossover_type.lower() - if (crossover_type == "single_point"): + if crossover_type == "single_point": self.crossover = self.single_point_crossover - elif (crossover_type == "two_points"): + elif crossover_type == "two_points": self.crossover = self.two_points_crossover - elif (crossover_type == "uniform"): + elif crossover_type == "uniform": self.crossover = self.uniform_crossover - elif (crossover_type == "scattered"): + elif crossover_type == "scattered": self.crossover = self.scattered_crossover else: self.valid_parameters = False @@ -641,7 +622,7 @@ def __init__(self, if crossover_probability is None: self.crossover_probability = None elif type(crossover_probability) in GA.supported_int_float_types: - if crossover_probability >= 0 and crossover_probability <= 1: + if 0 <= crossover_probability <= 1: self.crossover_probability = crossover_probability else: self.valid_parameters = False @@ -656,7 +637,7 @@ def __init__(self, if mutation_type is None: self.mutation = None elif inspect.ismethod(mutation_type): - # Check if the mutation_type is a method that accepts 3 paramater. + # Check if the mutation_type is a method that accepts 3 parameters. if (mutation_type.__code__.co_argcount == 3): # The mutation method assigned to the mutation_type parameter is validated. self.mutation = mutation_type @@ -664,7 +645,7 @@ def __init__(self, self.valid_parameters = False raise ValueError(f"When 'mutation_type' is assigned to a method, then it must accept 3 parameters:\n1) Expected to be the 'self' object.\n2) The offspring to be mutated.\n3) The instance from the pygad.GA class.\n\nThe passed mutation method named '{mutation_type.__code__.co_name}' accepts {mutation_type.__code__.co_argcount} parameter(s).") elif callable(mutation_type): - # Check if the mutation_type is a function that accepts 2 paramater. + # Check if the mutation_type is a function that accepts 2 parameters. if (mutation_type.__code__.co_argcount == 2): # The mutation function assigned to the mutation_type parameter is validated. self.mutation = mutation_type @@ -676,15 +657,15 @@ def __init__(self, raise TypeError(f"The expected type of the 'mutation_type' parameter is either callable or str but {type(mutation_type)} found.") else: # type mutation_type is str mutation_type = mutation_type.lower() - if (mutation_type == "random"): + if mutation_type == "random": self.mutation = self.random_mutation - elif (mutation_type == "swap"): + elif mutation_type == "swap": self.mutation = self.swap_mutation - elif (mutation_type == "scramble"): + elif mutation_type == "scramble": self.mutation = self.scramble_mutation - elif (mutation_type == "inversion"): + elif mutation_type == "inversion": self.mutation = self.inversion_mutation - elif (mutation_type == "adaptive"): + elif mutation_type == "adaptive": self.mutation = self.adaptive_mutation else: self.valid_parameters = False @@ -696,10 +677,10 @@ def __init__(self, if not (self.mutation_type is None): if mutation_probability is None: self.mutation_probability = None - elif (mutation_type != "adaptive"): + elif mutation_type != "adaptive": # Mutation probability is fixed not adaptive. if type(mutation_probability) in GA.supported_int_float_types: - if mutation_probability >= 0 and mutation_probability <= 1: + if 0 <= mutation_probability <= 1: self.mutation_probability = mutation_probability else: self.valid_parameters = False @@ -713,7 +694,7 @@ def __init__(self, if len(mutation_probability) == 2: for el in mutation_probability: if type(el) in GA.supported_int_float_types: - if el >= 0 and el <= 1: + if 0 <= el <= 1: pass else: self.valid_parameters = False @@ -723,7 +704,7 @@ def __init__(self, raise TypeError(f"Unexpected type for a value assigned to the 'mutation_probability' parameter. A numeric value is expected but ({el}) of type {type(el)} found.") if mutation_probability[0] < mutation_probability[1]: if not self.suppress_warnings: - warnings.warn(f"The first element in the 'mutation_probability' parameter is {mutation_probability[0]} which is smaller than the second element {mutation_probability[1]}. This means the mutation rate for the high-quality solutions is higher than the mutation rate of the low-quality ones. This causes high disruption in the high qualitiy solutions while making little changes in the low quality solutions. Please make the first element higher than the second element.") + warnings.warn(f"The first element in the 'mutation_probability' parameter is {mutation_probability[0]} which is smaller than the second element {mutation_probability[1]}. This means the mutation rate for the high-quality solutions is higher than the mutation rate of the low-quality ones. This causes high disruption in the high quality solutions while making little changes in the low quality solutions. Please make the first element higher than the second element.") self.mutation_probability = mutation_probability else: self.valid_parameters = False @@ -738,7 +719,7 @@ def __init__(self, if not (self.mutation_type is None): if mutation_num_genes is None: # The mutation_num_genes parameter does not exist. Checking whether adaptive mutation is used. - if (mutation_type != "adaptive"): + if mutation_type != "adaptive": # The percent of genes to mutate is fixed not adaptive. if mutation_percent_genes == 'default'.lower(): mutation_percent_genes = 10 @@ -754,7 +735,7 @@ def __init__(self, mutation_num_genes = 1 elif type(mutation_percent_genes) in GA.supported_int_float_types: - if (mutation_percent_genes <= 0 or mutation_percent_genes > 100): + if mutation_percent_genes <= 0 or mutation_percent_genes > 100: self.valid_parameters = False raise ValueError(f"The percentage of selected genes for mutation (mutation_percent_genes) must be > 0 and <= 100 but ({mutation_percent_genes}) found.\n") else: @@ -782,7 +763,7 @@ def __init__(self, mutation_percent_genes, dtype=numpy.uint32) for idx, el in enumerate(mutation_percent_genes): if type(el) in GA.supported_int_float_types: - if (el <= 0 or el > 100): + if el <= 0 or el > 100: self.valid_parameters = False raise ValueError(f"The values assigned to the 'mutation_percent_genes' must be > 0 and <= 100 but ({mutation_percent_genes}) found.\n") else: @@ -799,8 +780,8 @@ def __init__(self, mutation_num_genes[idx] = 1 if mutation_percent_genes[0] < mutation_percent_genes[1]: if not self.suppress_warnings: - warnings.warn(f"The first element in the 'mutation_percent_genes' parameter is ({mutation_percent_genes[0]}) which is smaller than the second element ({mutation_percent_genes[1]}).\nThis means the mutation rate for the high-quality solutions is higher than the mutation rate of the low-quality ones. This causes high disruption in the high qualitiy solutions while making little changes in the low quality solutions.\nPlease make the first element higher than the second element.") - # At this point outside the loop, all values of the parameter 'mutation_percent_genes' are validated. Eveyrthing is OK. + warnings.warn(f"The first element in the 'mutation_percent_genes' parameter is ({mutation_percent_genes[0]}) which is smaller than the second element ({mutation_percent_genes[1]}).\nThis means the mutation rate for the high-quality solutions is higher than the mutation rate of the low-quality ones. This causes high disruption in the high quality solutions while making little changes in the low quality solutions.\nPlease make the first element higher than the second element.") + # At this point outside the loop, all values of the parameter 'mutation_percent_genes' are validated. Everything is OK. else: self.valid_parameters = False raise ValueError(f"When mutation_type='adaptive', then the 'mutation_percent_genes' parameter must have only 2 elements but ({len(mutation_percent_genes)}) element(s) found.") @@ -809,13 +790,13 @@ def __init__(self, self.valid_parameters = False raise TypeError(f"Unexpected type of the 'mutation_percent_genes' parameter. When mutation_type='adaptive', then the 'mutation_percent_genes' parameter should exist and assigned a list/tuple/numpy.ndarray with 2 values but ({mutation_percent_genes}) found.") # The mutation_num_genes parameter exists. Checking whether adaptive mutation is used. - elif (mutation_type != "adaptive"): + elif mutation_type != "adaptive": # Number of genes to mutate is fixed not adaptive. if type(mutation_num_genes) in GA.supported_int_types: - if (mutation_num_genes <= 0): + if mutation_num_genes <= 0: self.valid_parameters = False raise ValueError(f"The number of selected genes for mutation (mutation_num_genes) cannot be <= 0 but ({mutation_num_genes}) found. If you do not want to use mutation, please set mutation_type=None\n") - elif (mutation_num_genes > self.num_genes): + elif mutation_num_genes > self.num_genes: self.valid_parameters = False raise ValueError(f"The number of selected genes for mutation (mutation_num_genes), which is ({mutation_num_genes}), cannot be greater than the number of genes ({self.num_genes}).\n") else: @@ -827,10 +808,10 @@ def __init__(self, if len(mutation_num_genes) == 2: for el in mutation_num_genes: if type(el) in GA.supported_int_types: - if (el <= 0): + if el <= 0: self.valid_parameters = False raise ValueError(f"The values assigned to the 'mutation_num_genes' cannot be <= 0 but ({el}) found. If you do not want to use mutation, please set mutation_type=None\n") - elif (el > self.num_genes): + elif el > self.num_genes: self.valid_parameters = False raise ValueError(f"The values assigned to the 'mutation_num_genes' cannot be greater than the number of genes ({self.num_genes}) but ({el}) found.\n") else: @@ -839,8 +820,8 @@ def __init__(self, # At this point of the loop, the current value assigned to the parameter 'mutation_num_genes' is validated. if mutation_num_genes[0] < mutation_num_genes[1]: if not self.suppress_warnings: - warnings.warn(f"The first element in the 'mutation_num_genes' parameter is {mutation_num_genes[0]} which is smaller than the second element {mutation_num_genes[1]}. This means the mutation rate for the high-quality solutions is higher than the mutation rate of the low-quality ones. This causes high disruption in the high qualitiy solutions while making little changes in the low quality solutions. Please make the first element higher than the second element.") - # At this point outside the loop, all values of the parameter 'mutation_num_genes' are validated. Eveyrthing is OK. + warnings.warn(f"The first element in the 'mutation_num_genes' parameter is {mutation_num_genes[0]} which is smaller than the second element {mutation_num_genes[1]}. This means the mutation rate for the high-quality solutions is higher than the mutation rate of the low-quality ones. This causes high disruption in the high quality solutions while making little changes in the low quality solutions. Please make the first element higher than the second element.") + # At this point outside the loop, all values of the parameter 'mutation_num_genes' are validated. Everything is OK. else: self.valid_parameters = False raise ValueError(f"When mutation_type='adaptive', then the 'mutation_num_genes' parameter must have only 2 elements but ({len(mutation_num_genes)}) element(s) found.") @@ -863,18 +844,18 @@ def __init__(self, # select_parents: Refers to a method that selects the parents based on the parent selection type specified in the parent_selection_type attribute. # Validating the selected type of parent selection: parent_selection_type if inspect.ismethod(parent_selection_type): - # Check if the parent_selection_type is a method that accepts 4 paramaters. - if (parent_selection_type.__code__.co_argcount == 4): - # population: Added in PyGAD 2.16.0. It should used only to support custom parent selection functions. Otherwise, it should be left to None to retirve the population by self.population. + # Check if the parent_selection_type is a method that accepts 4 parameters. + if parent_selection_type.__code__.co_argcount == 4: + # population: Added in PyGAD 2.16.0. It should use only to support custom parent selection functions. Otherwise, it should be left to None to retrieve the population by self.population. # The parent selection method assigned to the parent_selection_type parameter is validated. self.select_parents = parent_selection_type else: self.valid_parameters = False raise ValueError(f"When 'parent_selection_type' is assigned to a method, then it must accept 4 parameters:\n1) Expected to be the 'self' object.\n2) The fitness values of the current population.\n3) The number of parents needed.\n4) The instance from the pygad.GA class.\n\nThe passed parent selection method named '{parent_selection_type.__code__.co_name}' accepts {parent_selection_type.__code__.co_argcount} parameter(s).") elif callable(parent_selection_type): - # Check if the parent_selection_type is a function that accepts 3 paramaters. - if (parent_selection_type.__code__.co_argcount == 3): - # population: Added in PyGAD 2.16.0. It should used only to support custom parent selection functions. Otherwise, it should be left to None to retirve the population by self.population. + # Check if the parent_selection_type is a function that accepts 3 parameters. + if parent_selection_type.__code__.co_argcount == 3: + # population: Added in PyGAD 2.16.0. It should use only to support custom parent selection functions. Otherwise, it should be left to None to retrieve the population by self.population. # The parent selection function assigned to the parent_selection_type parameter is validated. self.select_parents = parent_selection_type else: @@ -886,35 +867,39 @@ def __init__(self, raise TypeError(f"The expected type of the 'parent_selection_type' parameter is either callable or str but {type(parent_selection_type)} found.") else: parent_selection_type = parent_selection_type.lower() - if (parent_selection_type == "sss"): + if parent_selection_type == "sss": self.select_parents = self.steady_state_selection - elif (parent_selection_type == "rws"): + elif parent_selection_type == "rws": self.select_parents = self.roulette_wheel_selection - elif (parent_selection_type == "sus"): + elif parent_selection_type == "sus": self.select_parents = self.stochastic_universal_selection - elif (parent_selection_type == "random"): + elif parent_selection_type == "random": self.select_parents = self.random_selection - elif (parent_selection_type == "tournament"): + elif parent_selection_type == "tournament": self.select_parents = self.tournament_selection - elif (parent_selection_type == "tournament_nsga2"): # Supported in PyGAD >= 3.2 + elif parent_selection_type == "tournament_nsga2": # Supported in PyGAD >= 3.2 self.select_parents = self.tournament_selection_nsga2 - elif (parent_selection_type == "nsga2"): # Supported in PyGAD >= 3.2 + elif parent_selection_type == "nsga2": # Supported in PyGAD >= 3.2 self.select_parents = self.nsga2_selection - elif (parent_selection_type == "rank"): + elif parent_selection_type == "rank": self.select_parents = self.rank_selection else: self.valid_parameters = False raise TypeError(f"Undefined parent selection type: {parent_selection_type}. \nThe assigned value to the 'parent_selection_type' parameter does not refer to one of the supported parent selection techniques which are: \n-sss (steady state selection)\n-rws (roulette wheel selection)\n-sus (stochastic universal selection)\n-rank (rank selection)\n-random (random selection)\n-tournament (tournament selection)\n-tournament_nsga2: (Tournament selection for NSGA-II)\n-nsga2: (NSGA-II parent selection).\n") # For tournament selection, validate the K value. - if (parent_selection_type == "tournament"): - if (K_tournament > self.sol_per_pop): - K_tournament = self.sol_per_pop - if not self.suppress_warnings: - warnings.warn(f"K of the tournament selection ({K_tournament}) should not be greater than the number of solutions within the population ({self.sol_per_pop}).\nK will be clipped to be equal to the number of solutions in the population (sol_per_pop).\n") - elif (K_tournament <= 0): + if parent_selection_type == "tournament": + if type(K_tournament) in GA.supported_int_types: + if K_tournament > self.sol_per_pop: + K_tournament = self.sol_per_pop + if not self.suppress_warnings: + warnings.warn(f"K of the tournament selection ({K_tournament}) should not be greater than the number of solutions within the population ({self.sol_per_pop}).\nK will be clipped to be equal to the number of solutions in the population (sol_per_pop).\n") + elif K_tournament <= 0: + self.valid_parameters = False + raise ValueError(f"K of the tournament selection cannot be <=0 but ({K_tournament}) found.\n") + else: self.valid_parameters = False - raise ValueError(f"K of the tournament selection cannot be <=0 but ({K_tournament}) found.\n") + raise ValueError(f"The type of K of the tournament selection must be integer but the value ({K_tournament}) of type ({type(K_tournament)}) found.") self.K_tournament = K_tournament @@ -922,7 +907,7 @@ def __init__(self, if not (type(keep_parents) in GA.supported_int_types): self.valid_parameters = False raise TypeError(f"Incorrect type of the value assigned to the keep_parents parameter. The value ({keep_parents}) of type {type(keep_parents)} found but an integer is expected.") - elif (keep_parents > self.sol_per_pop or keep_parents > self.num_parents_mating or keep_parents < -1): + elif keep_parents > self.sol_per_pop or keep_parents > self.num_parents_mating or keep_parents < -1: self.valid_parameters = False raise ValueError(f"Incorrect value to the keep_parents parameter: {keep_parents}. \nThe assigned value to the keep_parent parameter must satisfy the following conditions: \n1) Less than or equal to sol_per_pop\n2) Less than or equal to num_parents_mating\n3) Greater than or equal to -1.") @@ -936,7 +921,7 @@ def __init__(self, if not (type(keep_elitism) in GA.supported_int_types): self.valid_parameters = False raise TypeError(f"Incorrect type of the value assigned to the keep_elitism parameter. The value ({keep_elitism}) of type {type(keep_elitism)} found but an integer is expected.") - elif (keep_elitism > self.sol_per_pop or keep_elitism < 0): + elif keep_elitism > self.sol_per_pop or keep_elitism < 0: self.valid_parameters = False raise ValueError(f"Incorrect value to the keep_elitism parameter: {keep_elitism}. \nThe assigned value to the keep_elitism parameter must satisfy the following conditions: \n1) Less than or equal to sol_per_pop\n2) Greater than or equal to 0.") @@ -945,13 +930,13 @@ def __init__(self, # Validate keep_parents. if self.keep_elitism == 0: # Keep all parents in the next population. - if (self.keep_parents == -1): + if self.keep_parents == -1: self.num_offspring = self.sol_per_pop - self.num_parents_mating # Keep no parents in the next population. - elif (self.keep_parents == 0): + elif self.keep_parents == 0: self.num_offspring = self.sol_per_pop # Keep the specified number of parents in the next population. - elif (self.keep_parents > 0): + elif self.keep_parents > 0: self.num_offspring = self.sol_per_pop - self.keep_parents else: self.num_offspring = self.sol_per_pop - self.keep_elitism @@ -960,15 +945,15 @@ def __init__(self, # In PyGAD 2.19.0, a method can be passed to the fitness function. If function is passed, then it accepts 2 parameters. If method, then it accepts 3 parameters. # In PyGAD 2.20.0, a new parameter is passed referring to the instance of the `pygad.GA` class. So, the function accepts 3 parameters and the method accepts 4 parameters. if inspect.ismethod(fitness_func): - # If the fitness is calculated through a method, not a function, then there is a fourth 'self` paramaters. - if (fitness_func.__code__.co_argcount == 4): + # If the fitness is calculated through a method, not a function, then there is a fourth 'self` parameters. + if fitness_func.__code__.co_argcount == 4: self.fitness_func = fitness_func else: self.valid_parameters = False raise ValueError(f"In PyGAD 2.20.0, if a method is used to calculate the fitness value, then it must accept 4 parameters\n1) Expected to be the 'self' object.\n2) The instance of the 'pygad.GA' class.\n3) A solution to calculate its fitness value.\n4) The solution's index within the population.\n\nThe passed fitness method named '{fitness_func.__code__.co_name}' accepts {fitness_func.__code__.co_argcount} parameter(s).") elif callable(fitness_func): - # Check if the fitness function accepts 2 paramaters. - if (fitness_func.__code__.co_argcount == 3): + # Check if the fitness function accepts 2 parameters. + if fitness_func.__code__.co_argcount == 3: self.fitness_func = fitness_func else: self.valid_parameters = False @@ -992,16 +977,16 @@ def __init__(self, # Check if the on_start exists. if not (on_start is None): if inspect.ismethod(on_start): - # Check if the on_start method accepts 2 paramaters. - if (on_start.__code__.co_argcount == 2): + # Check if the on_start method accepts 2 parameters. + if on_start.__code__.co_argcount == 2: self.on_start = on_start else: self.valid_parameters = False raise ValueError(f"The method assigned to the on_start parameter must accept only 2 parameters:\n1) Expected to be the 'self' object.\n2) The instance of the genetic algorithm.\nThe passed method named '{on_start.__code__.co_name}' accepts {on_start.__code__.co_argcount} parameter(s).") # Check if the on_start is a function. elif callable(on_start): - # Check if the on_start function accepts only a single paramater. - if (on_start.__code__.co_argcount == 1): + # Check if the on_start function accepts only a single parameter. + if on_start.__code__.co_argcount == 1: self.on_start = on_start else: self.valid_parameters = False @@ -1017,16 +1002,16 @@ def __init__(self, if not (on_fitness is None): # Check if the on_fitness is a method. if inspect.ismethod(on_fitness): - # Check if the on_fitness method accepts 3 paramaters. - if (on_fitness.__code__.co_argcount == 3): + # Check if the on_fitness method accepts 3 parameters. + if on_fitness.__code__.co_argcount == 3: self.on_fitness = on_fitness else: self.valid_parameters = False raise ValueError(f"The method assigned to the on_fitness parameter must accept 3 parameters:\n1) Expected to be the 'self' object.\n2) The instance of the genetic algorithm.3) The fitness values of all solutions.\nThe passed method named '{on_fitness.__code__.co_name}' accepts {on_fitness.__code__.co_argcount} parameter(s).") # Check if the on_fitness is a function. elif callable(on_fitness): - # Check if the on_fitness function accepts 2 paramaters. - if (on_fitness.__code__.co_argcount == 2): + # Check if the on_fitness function accepts 2 parameters. + if on_fitness.__code__.co_argcount == 2: self.on_fitness = on_fitness else: self.valid_parameters = False @@ -1041,16 +1026,16 @@ def __init__(self, if not (on_parents is None): # Check if the on_parents is a method. if inspect.ismethod(on_parents): - # Check if the on_parents method accepts 3 paramaters. - if (on_parents.__code__.co_argcount == 3): + # Check if the on_parents method accepts 3 parameters. + if on_parents.__code__.co_argcount == 3: self.on_parents = on_parents else: self.valid_parameters = False raise ValueError(f"The method assigned to the on_parents parameter must accept 3 parameters:\n1) Expected to be the 'self' object.\n2) The instance of the genetic algorithm.\n3) The fitness values of all solutions.\nThe passed method named '{on_parents.__code__.co_name}' accepts {on_parents.__code__.co_argcount} parameter(s).") # Check if the on_parents is a function. elif callable(on_parents): - # Check if the on_parents function accepts 2 paramaters. - if (on_parents.__code__.co_argcount == 2): + # Check if the on_parents function accepts 2 parameters. + if on_parents.__code__.co_argcount == 2: self.on_parents = on_parents else: self.valid_parameters = False @@ -1065,16 +1050,16 @@ def __init__(self, if not (on_crossover is None): # Check if the on_crossover is a method. if inspect.ismethod(on_crossover): - # Check if the on_crossover method accepts 3 paramaters. - if (on_crossover.__code__.co_argcount == 3): + # Check if the on_crossover method accepts 3 parameters. + if on_crossover.__code__.co_argcount == 3: self.on_crossover = on_crossover else: self.valid_parameters = False raise ValueError(f"The method assigned to the on_crossover parameter must accept 3 parameters:\n1) Expected to be the 'self' object.\n2) The instance of the genetic algorithm.\n2) The offspring generated using crossover.\nThe passed method named '{on_crossover.__code__.co_name}' accepts {on_crossover.__code__.co_argcount} parameter(s).") # Check if the on_crossover is a function. elif callable(on_crossover): - # Check if the on_crossover function accepts 2 paramaters. - if (on_crossover.__code__.co_argcount == 2): + # Check if the on_crossover function accepts 2 parameters. + if on_crossover.__code__.co_argcount == 2: self.on_crossover = on_crossover else: self.valid_parameters = False @@ -1089,16 +1074,16 @@ def __init__(self, if not (on_mutation is None): # Check if the on_mutation is a method. if inspect.ismethod(on_mutation): - # Check if the on_mutation method accepts 3 paramaters. - if (on_mutation.__code__.co_argcount == 3): + # Check if the on_mutation method accepts 3 parameters. + if on_mutation.__code__.co_argcount == 3: self.on_mutation = on_mutation else: self.valid_parameters = False raise ValueError(f"The method assigned to the on_mutation parameter must accept 3 parameters:\n1) Expected to be the 'self' object.\n2) The instance of the genetic algorithm.\n2) The offspring after applying the mutation operation.\nThe passed method named '{on_mutation.__code__.co_name}' accepts {on_mutation.__code__.co_argcount} parameter(s).") # Check if the on_mutation is a function. elif callable(on_mutation): - # Check if the on_mutation function accepts 2 paramaters. - if (on_mutation.__code__.co_argcount == 2): + # Check if the on_mutation function accepts 2 parameters. + if on_mutation.__code__.co_argcount == 2: self.on_mutation = on_mutation else: self.valid_parameters = False @@ -1113,16 +1098,16 @@ def __init__(self, if not (on_generation is None): # Check if the on_generation is a method. if inspect.ismethod(on_generation): - # Check if the on_generation method accepts 2 paramaters. - if (on_generation.__code__.co_argcount == 2): + # Check if the on_generation method accepts 2 parameters. + if on_generation.__code__.co_argcount == 2: self.on_generation = on_generation else: self.valid_parameters = False raise ValueError(f"The method assigned to the on_generation parameter must accept 2 parameters:\n1) Expected to be the 'self' object.\n2) The instance of the genetic algorithm.\nThe passed method named '{on_generation.__code__.co_name}' accepts {on_generation.__code__.co_argcount} parameter(s).") # Check if the on_generation is a function. elif callable(on_generation): - # Check if the on_generation function accepts only a single paramater. - if (on_generation.__code__.co_argcount == 1): + # Check if the on_generation function accepts only a single parameter. + if on_generation.__code__.co_argcount == 1: self.on_generation = on_generation else: self.valid_parameters = False @@ -1137,16 +1122,16 @@ def __init__(self, if not (on_stop is None): # Check if the on_stop is a method. if inspect.ismethod(on_stop): - # Check if the on_stop method accepts 3 paramaters. - if (on_stop.__code__.co_argcount == 3): + # Check if the on_stop method accepts 3 parameters. + if on_stop.__code__.co_argcount == 3: self.on_stop = on_stop else: self.valid_parameters = False raise ValueError(f"The method assigned to the on_stop parameter must accept 3 parameters:\n1) Expected to be the 'self' object.\n2) The instance of the genetic algorithm.\n2) A list of the fitness values of the solutions in the last population.\nThe passed method named '{on_stop.__code__.co_name}' accepts {on_stop.__code__.co_argcount} parameter(s).") # Check if the on_stop is a function. elif callable(on_stop): - # Check if the on_stop function accepts 2 paramaters. - if (on_stop.__code__.co_argcount == 2): + # Check if the on_stop function accepts 2 parameters. + if on_stop.__code__.co_argcount == 2: self.on_stop = on_stop else: self.valid_parameters = False @@ -1226,7 +1211,7 @@ def validate_multi_stop_criteria(self, stop_word, number): raise ValueError(f"For format of a single criterion in the 'stop_criteria' parameter is 'word_number' but '{stop_criteria}' found.") elif type(stop_criteria) in [list, tuple, numpy.ndarray]: - # Remove duplicate criterira by converting the list to a set then back to a list. + # Remove duplicate criteria by converting the list to a set then back to a list. stop_criteria = list(set(stop_criteria)) for idx, val in enumerate(stop_criteria): if type(val) is str: @@ -1276,7 +1261,7 @@ def validate_multi_stop_criteria(self, stop_word, number): if parallel_processing[0] in ["process", "thread"]: if (type(parallel_processing[1]) in GA.supported_int_types and parallel_processing[1] > 0) or (parallel_processing[1] == 0) or (parallel_processing[1] is None): if parallel_processing[1] == 0: - # If the number of processes/threads is 0, this means no parallel processing is used. It is equivelant to setting parallel_processing=None. + # If the number of processes/threads is 0, this means no parallel processing is used. It is equivalent to setting parallel_processing=None. self.parallel_processing = None else: # Whether the second value is None or a positive integer. @@ -1303,7 +1288,7 @@ def validate_multi_stop_criteria(self, stop_word, number): # The number of completed generations. self.generations_completed = 0 - # At this point, all necessary parameters validation is done successfully and we are sure that the parameters are valid. + # At this point, all necessary parameters validation is done successfully, and we are sure that the parameters are valid. # Set to True when all the parameters passed in the GA class constructor are valid. self.valid_parameters = True @@ -1367,16 +1352,16 @@ def round_genes(self, solutions): return solutions def initialize_population(self, - low, - high, allow_duplicate_genes, - mutation_by_replacement, - gene_type): + gene_type, + gene_constraint): """ Creates an initial population randomly as a NumPy array. The array is saved in the instance attribute named 'population'. - 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. + It accepts: + -allow_duplicate_genes: Whether duplicate genes are allowed or not. + -gene_type: The data type of the genes. + -gene_constraint: The constraints of the genes. This method assigns the values of the following 3 instance attributes: 1. pop_size: Size of the population. @@ -1388,276 +1373,92 @@ def initialize_population(self, # The population will have sol_per_pop chromosome where each chromosome has num_genes genes. self.pop_size = (self.sol_per_pop, self.num_genes) + # There are 4 steps to build the initial population: + # 1) Generate the population. + # 2) Change the data type and round the values. + # 3) Check for the constraints. + # 4) Solve duplicates if not allowed. + + # Create an empty population. + self.population = numpy.empty(shape=self.pop_size, dtype=object) + + # 1) Create the initial population either randomly or using the gene space. if self.gene_space is None: - # Creating the initial population randomly. - if self.gene_type_single == True: - self.population = numpy.asarray(numpy.random.uniform(low=low, - high=high, - size=self.pop_size), - dtype=self.gene_type[0]) # A NumPy array holding the initial population. - else: - # Create an empty population of dtype=object to support storing mixed data types within the same array. - self.population = numpy.zeros( - shape=self.pop_size, dtype=object) - # Loop through the genes, randomly generate the values of a single gene across the entire population, and add the values of each gene to the population. + # Create the initial population randomly. + + # Set gene_value=None to consider generating values for the initial population instead of generating values for mutation. + # Loop through the genes, randomly generate the values of a single gene at a time, and insert the values of each gene to the population. + for sol_idx in range(self.sol_per_pop): for gene_idx in range(self.num_genes): + range_min, range_max = self.get_initial_population_range(gene_index=gene_idx) + self.population[sol_idx, gene_idx] = self.generate_gene_value_randomly(range_min=range_min, + range_max=range_max, + gene_idx=gene_idx, + mutation_by_replacement=True, + gene_value=None, + sample_size=1, + step=1) - if type(self.init_range_low) in self.supported_int_float_types: - range_min = self.init_range_low - range_max = self.init_range_high - else: - range_min = self.init_range_low[gene_idx] - range_max = self.init_range_high[gene_idx] - - # A vector of all values of this single gene across all solutions in the population. - gene_values = numpy.asarray(numpy.random.uniform(low=range_min, - high=range_max, - size=self.pop_size[0]), - dtype=self.gene_type[gene_idx][0]) - # Adding the current gene values to the population. - self.population[:, gene_idx] = gene_values - - if allow_duplicate_genes == False: - for solution_idx in range(self.population.shape[0]): - # self.logger.info("Before", self.population[solution_idx]) + else: + # Generate the initial population using the gene_space. + for sol_idx in range(self.sol_per_pop): + for gene_idx in range(self.num_genes): + self.population[sol_idx, gene_idx] = self.generate_gene_value_from_space(gene_idx=gene_idx, + mutation_by_replacement=True, + gene_value=None, + solution=self.population[sol_idx], + sample_size=1) + + # 2) Change the data type and round all genes within the initial population. + self.population = self.change_population_dtype_and_round(self.population) + + # Note that gene_constraint is not validated yet. + # We have to set it as a property of the pygad.GA instance to retrieve without passing it as an additional parameter. + self.gene_constraint = gene_constraint + + # 3) Enforce the gene constraints as much as possible. + if self.gene_constraint is None: + pass + else: + for sol_idx, solution in enumerate(self.population): + for gene_idx in range(self.num_genes): + # Check that a constraint is available for the gene and that the current value does not satisfy that constraint + if self.gene_constraint[gene_idx]: + if not self.gene_constraint[gene_idx](solution): + range_min, range_max = self.get_initial_population_range(gene_index=gene_idx) + # While initializing the population, we follow a mutation by replacement approach. So, the original gene value is not needed. + values_filtered = self.get_valid_gene_constraint_values(range_min=range_min, + range_max=range_max, + gene_value=None, + gene_idx=gene_idx, + mutation_by_replacement=True, + solution=solution, + sample_size=self.sample_size) + if values_filtered is None: + if not self.suppress_warnings: + warnings.warn(f"No value satisfied the constraint for the gene at index {gene_idx} with value {solution[gene_idx]} while creating the initial population.") + else: + self.population[sol_idx, gene_idx] = random.choice(values_filtered) + + # 4) Solve duplicate genes. + if allow_duplicate_genes == False: + for solution_idx in range(self.population.shape[0]): + if self.gene_space is None: self.population[solution_idx], _, _ = self.solve_duplicate_genes_randomly(solution=self.population[solution_idx], - min_val=low, - max_val=high, - mutation_by_replacement=True, + min_val=self.init_range_low, + max_val=self.init_range_high, gene_type=gene_type, - num_trials=10) - # self.logger.info("After", self.population[solution_idx]) - - elif self.gene_space_nested: - if self.gene_type_single == True: - # Reaching this block means: - # 1) gene_space is nested (gene_space_nested is True). - # 2) gene_type is not nested (gene_type_single is True). - self.population = numpy.zeros(shape=self.pop_size, - dtype=self.gene_type[0]) - for sol_idx in range(self.sol_per_pop): - for gene_idx in range(self.num_genes): - - if type(self.init_range_low) in self.supported_int_float_types: - range_min = self.init_range_low - range_max = self.init_range_high - else: - range_min = self.init_range_low[gene_idx] - range_max = self.init_range_high[gene_idx] - - if self.gene_space[gene_idx] is None: - - # The following commented code replace the None value with a single number that will not change again. - # This means the gene value will be the same across all solutions. - # self.gene_space[gene_idx] = numpy.asarray(numpy.random.uniform(low=low, - # high=high, - # size=1), dtype=self.gene_type[0])[0] - # self.population[sol_idx, gene_idx] = list(self.gene_space[gene_idx]).copy() - - # The above problem is solved by keeping the None value in the gene_space parameter. This forces PyGAD to generate this value for each solution. - self.population[sol_idx, gene_idx] = numpy.asarray(numpy.random.uniform(low=range_min, - high=range_max, - size=1), - dtype=self.gene_type[0])[0] - elif type(self.gene_space[gene_idx]) in [numpy.ndarray, list, tuple, range]: - # Check if the gene space has None values. If any, then replace it with randomly generated values according to the 3 attributes init_range_low, init_range_high, and gene_type. - if type(self.gene_space[gene_idx]) is range: - temp_gene_space = self.gene_space[gene_idx] - else: - # Convert to list because tuple and range do not have copy(). - # We copy the gene_space to a temp variable to keep its original value. - # In the next for loop, the gene_space is changed. - # Later, the gene_space is restored to its original value using the temp variable. - temp_gene_space = list( - self.gene_space[gene_idx]).copy() - - for idx, val in enumerate(self.gene_space[gene_idx]): - if val is None: - self.gene_space[gene_idx][idx] = numpy.asarray(numpy.random.uniform(low=range_min, - high=range_max, - size=1), - dtype=self.gene_type[0])[0] - # Find the difference between the current gene space and the current values in the solution. - unique_gene_values = list(set(self.gene_space[gene_idx]).difference( - set(self.population[sol_idx, :gene_idx]))) - if len(unique_gene_values) > 0: - self.population[sol_idx, gene_idx] = random.choice(unique_gene_values) - else: - # If there is no unique values, then we have to select a duplicate value. - self.population[sol_idx, gene_idx] = random.choice( - self.gene_space[gene_idx]) - - self.population[sol_idx, gene_idx] = self.gene_type[0]( - self.population[sol_idx, gene_idx]) - # Restore the gene_space from the temp_gene_space variable. - self.gene_space[gene_idx] = list( - temp_gene_space).copy() - elif type(self.gene_space[gene_idx]) is dict: - if 'step' in self.gene_space[gene_idx].keys(): - self.population[sol_idx, gene_idx] = numpy.asarray(numpy.random.choice(numpy.arange(start=self.gene_space[gene_idx]['low'], - stop=self.gene_space[gene_idx]['high'], - step=self.gene_space[gene_idx]['step']), - size=1), - dtype=self.gene_type[0])[0] - else: - self.population[sol_idx, gene_idx] = numpy.asarray(numpy.random.uniform(low=self.gene_space[gene_idx]['low'], - high=self.gene_space[gene_idx]['high'], - size=1), - dtype=self.gene_type[0])[0] - elif type(self.gene_space[gene_idx]) in GA.supported_int_float_types: - self.population[sol_idx, gene_idx] = self.gene_space[gene_idx] - else: - # There is no more options. - pass - else: - # Reaching this block means: - # 1) gene_space is nested (gene_space_nested is True). - # 2) gene_type is nested (gene_type_single is False). - self.population = numpy.zeros(shape=self.pop_size, - dtype=object) - for sol_idx in range(self.sol_per_pop): - for gene_idx in range(self.num_genes): - - if type(self.init_range_low) in self.supported_int_float_types: - range_min = self.init_range_low - range_max = self.init_range_high - else: - range_min = self.init_range_low[gene_idx] - range_max = self.init_range_high[gene_idx] - - if type(self.gene_space[gene_idx]) in [numpy.ndarray, list, tuple, range]: - # Convert to list because tuple and range do not have copy(). - # We copy the gene_space to a temp variable to keep its original value. - # In the next for loop, the gene_space is changed. - # Later, the gene_space is restored to its original value using the temp variable. - temp_gene_space = list(self.gene_space[gene_idx]).copy() - - # Check if the gene space has None values. If any, then replace it with randomly generated values according to the 3 attributes init_range_low, init_range_high, and gene_type. - for idx, val in enumerate(self.gene_space[gene_idx]): - if val is None: - self.gene_space[gene_idx][idx] = numpy.asarray(numpy.random.uniform(low=range_min, - high=range_max, - size=1), - dtype=self.gene_type[gene_idx][0])[0] - - self.population[sol_idx, gene_idx] = random.choice(self.gene_space[gene_idx]) - self.population[sol_idx, gene_idx] = self.gene_type[gene_idx][0](self.population[sol_idx, gene_idx]) - # Restore the gene_space from the temp_gene_space variable. - self.gene_space[gene_idx] = temp_gene_space.copy() - elif type(self.gene_space[gene_idx]) is dict: - if 'step' in self.gene_space[gene_idx].keys(): - self.population[sol_idx, gene_idx] = numpy.asarray(numpy.random.choice(numpy.arange(start=self.gene_space[gene_idx]['low'], - stop=self.gene_space[gene_idx]['high'], - step=self.gene_space[gene_idx]['step']), - size=1), - dtype=self.gene_type[gene_idx][0])[0] - else: - self.population[sol_idx, gene_idx] = numpy.asarray(numpy.random.uniform(low=self.gene_space[gene_idx]['low'], - high=self.gene_space[gene_idx]['high'], - size=1), - dtype=self.gene_type[gene_idx][0])[0] - elif type(self.gene_space[gene_idx]) == type(None): - temp_gene_value = numpy.asarray(numpy.random.uniform(low=range_min, - high=range_max, - size=1), - dtype=self.gene_type[gene_idx][0])[0] - - self.population[sol_idx, gene_idx] = temp_gene_value.copy() - elif type(self.gene_space[gene_idx]) in GA.supported_int_float_types: - self.population[sol_idx, gene_idx] = self.gene_space[gene_idx] - else: - # There is no more options. - pass - else: - # Handle the non-nested gene_space. It can be assigned a numeric value, list, numpy.ndarray, or a dict. - if self.gene_type_single == True: - # Reaching this block means: - # 1) gene_space is not nested (gene_space_nested is False). - # 2) gene_type is not nested (gene_type_single is True). - - # Replace all the None values with random values using the init_range_low, init_range_high, and gene_type attributes. - for gene_idx, curr_gene_space in enumerate(self.gene_space): - - if type(self.init_range_low) in self.supported_int_float_types: - range_min = self.init_range_low - range_max = self.init_range_high - else: - range_min = self.init_range_low[gene_idx] - range_max = self.init_range_high[gene_idx] - - if curr_gene_space is None: - self.gene_space[gene_idx] = numpy.asarray(numpy.random.uniform(low=range_min, - high=range_max, - size=1), - dtype=self.gene_type[0])[0] - - # Creating the initial population by randomly selecting the genes' values from the values inside the 'gene_space' parameter. - if type(self.gene_space) is dict: - if 'step' in self.gene_space.keys(): - self.population = numpy.asarray(numpy.random.choice(numpy.arange(start=self.gene_space['low'], - stop=self.gene_space['high'], - step=self.gene_space['step']), - size=self.pop_size), - dtype=self.gene_type[0]) - else: - self.population = numpy.asarray(numpy.random.uniform(low=self.gene_space['low'], - high=self.gene_space['high'], - size=self.pop_size), - dtype=self.gene_type[0]) # A NumPy array holding the initial population. + mutation_by_replacement=True, + sample_size=self.sample_size) else: - self.population = numpy.asarray(numpy.random.choice(self.gene_space, - size=self.pop_size), - dtype=self.gene_type[0]) # A NumPy array holding the initial population. - else: - # Reaching this block means: - # 1) gene_space is not nested (gene_space_nested is False). - # 2) gene_type is nested (gene_type_single is False). - - # Creating the initial population by randomly selecting the genes' values from the values inside the 'gene_space' parameter. - if type(self.gene_space) is dict: - # Create an empty population of dtype=object to support storing mixed data types within the same array. - self.population = numpy.zeros(shape=self.pop_size, - dtype=object) - # Loop through the genes, randomly generate the values of a single gene across the entire population, and add the values of each gene to the population. - for gene_idx in range(self.num_genes): - # Generate the values of the current gene across all solutions. - # A vector of all values of this single gene across all solutions in the population. - if 'step' in self.gene_space.keys(): - gene_values = numpy.asarray(numpy.random.choice(numpy.arange(start=self.gene_space['low'], - stop=self.gene_space['high'], - step=self.gene_space['step']), - size=self.pop_size[0]), - dtype=self.gene_type[gene_idx][0]) - else: - gene_values = numpy.asarray(numpy.random.uniform(low=self.gene_space['low'], - high=self.gene_space['high'], - size=self.pop_size[0]), - dtype=self.gene_type[gene_idx][0]) - # Adding the current gene values to the population. - self.population[:, gene_idx] = gene_values + self.population[solution_idx], _, _ = self.solve_duplicate_genes_by_space(solution=self.population[solution_idx].copy(), + gene_type=self.gene_type, + mutation_by_replacement=True, + sample_size=self.sample_size, + build_initial_pop=True) - else: - # Reaching this block means that the gene_space is not None or dict. - # It can be either range, numpy.ndarray, or list. - - # Create an empty population of dtype=object to support storing mixed data types within the same array. - self.population = numpy.zeros(shape=self.pop_size, dtype=object) - # Loop through the genes, randomly generate the values of a single gene across the entire population, and add the values of each gene to the population. - for gene_idx in range(self.num_genes): - # A vector of all values of this single gene across all solutions in the population. - gene_values = numpy.asarray(numpy.random.choice(self.gene_space, - size=self.pop_size[0]), - dtype=self.gene_type[gene_idx][0]) - # Adding the current gene values to the population. - self.population[:, gene_idx] = gene_values - - if not (self.gene_space is None): - if allow_duplicate_genes == False: - for sol_idx in range(self.population.shape[0]): - self.population[sol_idx], _, _ = self.solve_duplicate_genes_by_space(solution=self.population[sol_idx], - gene_type=self.gene_type, - num_trials=10, - build_initial_pop=True) + # Change the data type and round all genes within the initial population. + self.population = self.change_population_dtype_and_round(self.population) # Keeping the initial population in the initial_population attribute. self.initial_population = self.population.copy() @@ -1676,11 +1477,15 @@ def cal_pop_fitness(self): # 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 = self.last_generation_parents.tolist() + else: + last_generation_parents_as_list = [] # '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 = self.last_generation_elitism.tolist() + else: + last_generation_elitism_as_list = [] pop_fitness = ["undefined"] * len(self.population) if self.parallel_processing is None: @@ -1991,8 +1796,7 @@ def run(self): if self.save_best_solutions: self.best_solutions.append(list(best_solution)) - - # Note: Any code that has loop-dependant statements (e.g. continue, break, etc) must be kept inside the loop of the 'run()' method. It can be moved to another method to clean the run() method. + # Note: Any code that has loop-dependant statements (e.g. continue, break, etc.) must be kept inside the loop of the 'run()' method. It can be moved to another method to clean the run() method. # If the on_generation attribute is not None, then cal the callback function after the generation. if not (self.on_generation is None): r = self.on_generation(self) @@ -2032,6 +1836,10 @@ def run(self): reach_fitness_value = criterion[obj_idx + 1] elif len(criterion[1:]) == 1: reach_fitness_value = criterion[1] + else: + # Unexpected to be reached, but it is safer to handle it. + self.valid_parameters = False + raise ValueError(f"The number of values does not equal the number of objectives.") if max(self.last_generation_fitness[:, obj_idx]) >= reach_fitness_value: pass @@ -2040,7 +1848,7 @@ def run(self): break elif criterion[0] == "saturate": criterion[1] = int(criterion[1]) - if (self.generations_completed >= criterion[1]): + if self.generations_completed >= criterion[1]: # Single-objective problem. if type(self.last_generation_fitness[0]) in GA.supported_int_float_types: if (self.best_solutions_fitness[self.generations_completed - criterion[1]] - self.best_solutions_fitness[self.generations_completed - 1]) == 0: @@ -2131,7 +1939,7 @@ def run_loop_head(self, best_solution_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. + This method must be only called from inside the run() method. It is not meant for use by the user. Generally, any method with a name starting with 'run_' is meant to be only called by PyGAD from inside the 'run()' method. The objective of the 'run_select_parents()' method is to select the parents and call the callable on_parents() if defined. @@ -2196,7 +2004,7 @@ def run_select_parents(self, call_on_parents=True): if on_parents_selected_parents.shape == self.last_generation_parents.shape: self.last_generation_parents = on_parents_selected_parents else: - raise ValueError(f"Size mismatch between the parents retrned by on_parents() {on_parents_selected_parents.shape} and the expected parents shape {self.last_generation_parents.shape}.") + raise ValueError(f"Size mismatch between the parents returned by on_parents() {on_parents_selected_parents.shape} and the expected parents shape {self.last_generation_parents.shape}.") else: raise ValueError(f"The output of on_parents() is expected to be tuple/list/numpy.ndarray but the first output type is {type(on_parents_selected_parents)}.") @@ -2207,6 +2015,7 @@ def run_select_parents(self, call_on_parents=True): if type(on_parents_selected_parents_indices) in [tuple, list, numpy.ndarray, range]: on_parents_selected_parents_indices = numpy.array(on_parents_selected_parents_indices) if on_parents_selected_parents_indices.shape == self.last_generation_parents_indices.shape: + # Add this new instance attribute. self.last_generation_parents_indices = on_parents_selected_parents_indices else: raise ValueError(f"Size mismatch between the parents indices returned by on_parents() {on_parents_selected_parents_indices.shape} and the expected crossover output {self.last_generation_parents_indices.shape}.") @@ -2218,7 +2027,7 @@ def run_select_parents(self, call_on_parents=True): def run_crossover(self): """ - This method must be only callled from inside the run() method. It is not meant for use by the user. + This method must be only called from inside the run() method. It is not meant for use by the user. Generally, any method with a name starting with 'run_' is meant to be only called by PyGAD from inside the 'run()' method. The objective of the 'run_crossover()' method is to apply crossover and call the callable on_crossover() if defined. @@ -2285,7 +2094,7 @@ def run_crossover(self): def run_mutation(self): """ - This method must be only callled from inside the run() method. It is not meant for use by the user. + This method must be only called from inside the run() method. It is not meant for use by the user. Generally, any method with a name starting with 'run_' is meant to be only called by PyGAD from inside the 'run()' method. The objective of the 'run_mutation()' method is to apply mutation and call the callable on_mutation() if defined. @@ -2335,7 +2144,7 @@ def run_mutation(self): def run_update_population(self): """ - This method must be only callled from inside the run() method. It is not meant for use by the user. + This method must be only called from inside the run() method. It is not meant for use by the user. Generally, any method with a name starting with 'run_' is meant to be only called by PyGAD from inside the 'run()' method. The objective of the 'run_update_population()' method is to update the 'population' attribute after completing the processes of crossover and mutation. @@ -2350,13 +2159,13 @@ def run_update_population(self): # Update the population attribute according to the offspring generated. if self.keep_elitism == 0: # If the keep_elitism parameter is 0, then the keep_parents parameter will be used to decide if the parents are kept in the next generation. - if (self.keep_parents == 0): + if self.keep_parents == 0: self.population = self.last_generation_offspring_mutation - elif (self.keep_parents == -1): + elif self.keep_parents == -1: # Creating the new population based on the parents and offspring. self.population[0:self.last_generation_parents.shape[0],:] = self.last_generation_parents self.population[self.last_generation_parents.shape[0]:, :] = self.last_generation_offspring_mutation - elif (self.keep_parents > 0): + elif self.keep_parents > 0: parents_to_keep, _ = self.steady_state_selection(self.last_generation_fitness, num_parents=self.keep_parents) self.population[0:parents_to_keep.shape[0],:] = parents_to_keep @@ -2452,7 +2261,7 @@ def line_separator(line_length=line_length, line_character=line_character): def create_row(columns, line_length=line_length, fill_character=fill_character, split_percentages=None): filled_columns = [] - if split_percentages == None: + if split_percentages is None: split_percentages = [int(100/len(columns))] * 3 columns_lengths = [int((split_percentages[idx] * line_length) / 100) for idx in range(len(split_percentages))] @@ -2461,7 +2270,6 @@ def create_row(columns, line_length=line_length, fill_character=fill_character, extra_characters = columns_lengths[column_idx] - \ current_column_length filled_column = column + fill_character * extra_characters - filled_column = column + fill_character * extra_characters filled_columns.append(filled_column) return "".join(filled_columns) diff --git a/pygad/utils/__init__.py b/pygad/utils/__init__.py index b39eda1..7d30650 100644 --- a/pygad/utils/__init__.py +++ b/pygad/utils/__init__.py @@ -3,4 +3,4 @@ from pygad.utils import mutation from pygad.utils import nsga2 -__version__ = "1.2.1" \ No newline at end of file +__version__ = "1.3.0" \ No newline at end of file diff --git a/pygad/utils/crossover.py b/pygad/utils/crossover.py index d7cd86e..86d92f6 100644 --- a/pygad/utils/crossover.py +++ b/pygad/utils/crossover.py @@ -70,12 +70,13 @@ def single_point_crossover(self, parents, offspring_size): max_val=self.random_mutation_max_val, mutation_by_replacement=self.mutation_by_replacement, gene_type=self.gene_type, - num_trials=10) + sample_size=self.sample_size) else: offspring[k], _, _ = self.solve_duplicate_genes_by_space(solution=offspring[k], gene_type=self.gene_type, - num_trials=10) - + sample_size=self.sample_size, + mutation_by_replacement=self.mutation_by_replacement, + build_initial_pop=False) return offspring @@ -143,11 +144,13 @@ def two_points_crossover(self, parents, offspring_size): max_val=self.random_mutation_max_val, mutation_by_replacement=self.mutation_by_replacement, gene_type=self.gene_type, - num_trials=10) + sample_size=self.sample_size) else: offspring[k], _, _ = self.solve_duplicate_genes_by_space(solution=offspring[k], gene_type=self.gene_type, - num_trials=10) + sample_size=self.sample_size, + mutation_by_replacement=self.mutation_by_replacement, + build_initial_pop=False) return offspring def uniform_crossover(self, parents, offspring_size): @@ -210,11 +213,13 @@ def uniform_crossover(self, parents, offspring_size): max_val=self.random_mutation_max_val, mutation_by_replacement=self.mutation_by_replacement, gene_type=self.gene_type, - num_trials=10) + sample_size=self.sample_size) else: offspring[k], _, _ = self.solve_duplicate_genes_by_space(solution=offspring[k], gene_type=self.gene_type, - num_trials=10) + sample_size=self.sample_size, + mutation_by_replacement=self.mutation_by_replacement, + build_initial_pop=False) return offspring @@ -274,9 +279,11 @@ def scattered_crossover(self, parents, offspring_size): max_val=self.random_mutation_max_val, mutation_by_replacement=self.mutation_by_replacement, gene_type=self.gene_type, - num_trials=10) + sample_size=self.sample_size) else: offspring[k], _, _ = self.solve_duplicate_genes_by_space(solution=offspring[k], gene_type=self.gene_type, - num_trials=10) + sample_size=self.sample_size, + mutation_by_replacement=self.mutation_by_replacement, + build_initial_pop=False) return offspring diff --git a/pygad/utils/mutation.py b/pygad/utils/mutation.py index 4309269..255cced 100644 --- a/pygad/utils/mutation.py +++ b/pygad/utils/mutation.py @@ -8,6 +8,8 @@ import pygad import concurrent.futures +import warnings + class Mutation: def __init__(self): @@ -43,24 +45,6 @@ def random_mutation(self, offspring): return offspring - def get_mutation_range(self, gene_index): - - """ - Returns the minimum and maximum values of the mutation range. - It accepts a single parameter: - -gene_index: The index of the gene to mutate. Only used if the gene has a specific mutation range - It returns the minimum and maximum values of the mutation range. - """ - - # We can use either random_mutation_min_val or random_mutation_max_val. - if type(self.random_mutation_min_val) in self.supported_int_float_types: - range_min = self.random_mutation_min_val - range_max = self.random_mutation_max_val - else: - range_min = self.random_mutation_min_val[gene_index] - range_max = self.random_mutation_max_val[gene_index] - return range_min, range_max - def mutation_by_space(self, offspring): """ @@ -75,111 +59,11 @@ def mutation_by_space(self, offspring): mutation_indices = numpy.array(random.sample(range(0, self.num_genes), self.mutation_num_genes)) for gene_idx in mutation_indices: - range_min, range_max = self.get_mutation_range(gene_idx) - - if self.gene_space_nested: - # Returning the current gene space from the 'gene_space' attribute. - if type(self.gene_space[gene_idx]) in [numpy.ndarray, list]: - curr_gene_space = self.gene_space[gene_idx].copy() - else: - curr_gene_space = self.gene_space[gene_idx] - - # If the gene space has only a single value, use it as the new gene value. - if type(curr_gene_space) in pygad.GA.supported_int_float_types: - value_from_space = curr_gene_space - # 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: - rand_val = numpy.random.uniform(low=range_min, - high=range_max, - size=1)[0] - if self.mutation_by_replacement: - value_from_space = rand_val - else: - value_from_space = offspring[offspring_idx, gene_idx] + rand_val - elif type(curr_gene_space) is dict: - # The gene's space of type dict specifies the lower and upper limits of a gene. - if 'step' in curr_gene_space.keys(): - # The numpy.random.choice() and numpy.random.uniform() functions return a NumPy array as the output even if the array has a single value. - # We have to return the output at index 0 to force a numeric value to be returned not an object of type numpy.ndarray. - # If numpy.ndarray is returned, then it will cause an issue later while using the set() function. - # Randomly select a value from a discrete range. - value_from_space = numpy.random.choice(numpy.arange(start=curr_gene_space['low'], - stop=curr_gene_space['high'], - step=curr_gene_space['step']), - size=1)[0] - else: - # Return the current gene value. - value_from_space = offspring[offspring_idx, gene_idx] - # Generate a random value to be added to the current gene value. - rand_val = numpy.random.uniform(low=range_min, - high=range_max, - size=1)[0] - # The objective is to have a new gene value that respects the gene_space boundaries. - # The next if-else block checks if adding the random value keeps the new gene value within the gene_space boundaries. - temp_val = value_from_space + rand_val - if temp_val < curr_gene_space['low']: - # Restrict the new value to be > curr_gene_space['low'] - # If subtracting the random value makes the new gene value outside the boundaries [low, high), then use the lower boundary the gene value. - if curr_gene_space['low'] <= value_from_space - rand_val < curr_gene_space['high']: - # Because subtracting the random value keeps the new gene value within the boundaries [low, high), then use such a value as the gene value. - temp_val = value_from_space - rand_val - else: - # Because subtracting the random value makes the new gene value outside the boundaries [low, high), then use the lower boundary as the gene value. - temp_val = curr_gene_space['low'] - elif temp_val >= curr_gene_space['high']: - # Restrict the new value to be < curr_gene_space['high'] - # If subtracting the random value makes the new gene value outside the boundaries [low, high), then use such a value as the gene value. - if curr_gene_space['low'] <= value_from_space - rand_val < curr_gene_space['high']: - # Because subtracting the random value keeps the new value within the boundaries [low, high), then use such a value as the gene value. - temp_val = value_from_space - rand_val - else: - # Because subtracting the random value makes the new gene value outside the boundaries [low, high), then use the lower boundary as the gene value. - temp_val = curr_gene_space['low'] - value_from_space = temp_val - else: - # Selecting a value randomly based on the current gene's space in the 'gene_space' attribute. - # If the gene space has only 1 value, then select it. The old and new values of the gene are identical. - if len(curr_gene_space) == 1: - value_from_space = curr_gene_space[0] - # If the gene space has more than 1 value, then select a new one that is different from the current value. - else: - values_to_select_from = list(set(curr_gene_space) - set([offspring[offspring_idx, gene_idx]])) - - if len(values_to_select_from) == 0: - value_from_space = offspring[offspring_idx, gene_idx] - else: - value_from_space = random.choice(values_to_select_from) - else: - # Selecting a value randomly from the global gene space in the 'gene_space' attribute. - if type(self.gene_space) is dict: - # 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)[0] - else: - 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. - values_to_select_from = list(set(self.gene_space) - set([offspring[offspring_idx, gene_idx]])) - - if len(values_to_select_from) == 0: - value_from_space = offspring[offspring_idx, gene_idx] - else: - value_from_space = random.choice(values_to_select_from) - # value_from_space = random.choice(self.gene_space) - - if value_from_space is None: - # TODO: Return index 0. - # TODO: Check if this if statement is necessary. - value_from_space = numpy.random.uniform(low=range_min, - high=range_max, - size=1)[0] + value_from_space = self.mutation_process_gene_value(solution=offspring[offspring_idx], + gene_idx=gene_idx, + sample_size=self.sample_size) - # Assinging the selected value from the space to the gene. + # Before assigning the selected value from the space to the gene, change its data type and round it. if self.gene_type_single == True: if not self.gene_type[1] is None: offspring[offspring_idx, gene_idx] = numpy.round(self.gene_type[0](value_from_space), @@ -190,14 +74,15 @@ def mutation_by_space(self, offspring): if not self.gene_type[gene_idx][1] is None: offspring[offspring_idx, gene_idx] = numpy.round(self.gene_type[gene_idx][0](value_from_space), self.gene_type[gene_idx][1]) - else: offspring[offspring_idx, gene_idx] = self.gene_type[gene_idx][0](value_from_space) if self.allow_duplicate_genes == False: offspring[offspring_idx], _, _ = self.solve_duplicate_genes_by_space(solution=offspring[offspring_idx], gene_type=self.gene_type, - num_trials=10) + sample_size=self.sample_size, + mutation_by_replacement=self.mutation_by_replacement, + build_initial_pop=False) return offspring def mutation_probs_by_space(self, offspring): @@ -214,71 +99,10 @@ def mutation_probs_by_space(self, offspring): probs = numpy.random.random(size=offspring.shape[1]) for gene_idx in range(offspring.shape[1]): - range_min, range_max = self.get_mutation_range(gene_idx) - if probs[gene_idx] <= self.mutation_probability: - if self.gene_space_nested: - # Returning the current gene space from the 'gene_space' attribute. - if type(self.gene_space[gene_idx]) in [numpy.ndarray, list]: - curr_gene_space = self.gene_space[gene_idx].copy() - else: - curr_gene_space = self.gene_space[gene_idx] - - # If the gene space has only a single value, use it as the new gene value. - if type(curr_gene_space) in pygad.GA.supported_int_float_types: - value_from_space = curr_gene_space - # 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: - rand_val = numpy.random.uniform(low=range_min, - high=range_max, - size=1)[0] - if self.mutation_by_replacement: - value_from_space = rand_val - else: - value_from_space = offspring[offspring_idx, gene_idx] + rand_val - elif type(curr_gene_space) is dict: - # Selecting a value randomly from the current gene's space in the 'gene_space' attribute. - 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)[0] - else: - value_from_space = numpy.random.uniform(low=curr_gene_space['low'], - high=curr_gene_space['high'], - size=1)[0] - else: - # Selecting a value randomly from 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. - if len(curr_gene_space) == 1: - value_from_space = curr_gene_space[0] - # If the gene space has more than 1 value, then select a new one that is different from the current value. - else: - values_to_select_from = list(set(curr_gene_space) - set([offspring[offspring_idx, gene_idx]])) - - if len(values_to_select_from) == 0: - value_from_space = offspring[offspring_idx, gene_idx] - else: - value_from_space = random.choice(values_to_select_from) - else: - # Selecting a value randomly from the global gene space in the 'gene_space' attribute. - if type(self.gene_space) is dict: - 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)[0] - else: - value_from_space = numpy.random.uniform(low=self.gene_space['low'], - high=self.gene_space['high'], - size=1)[0] - else: - values_to_select_from = list(set(self.gene_space) - set([offspring[offspring_idx, gene_idx]])) - - if len(values_to_select_from) == 0: - value_from_space = offspring[offspring_idx, gene_idx] - else: - value_from_space = random.choice(values_to_select_from) + value_from_space = self.mutation_process_gene_value(solution=offspring[offspring_idx], + gene_idx=gene_idx, + sample_size=self.sample_size) # Assigning the selected value from the space to the gene. if self.gene_type_single == True: @@ -297,54 +121,59 @@ def mutation_probs_by_space(self, offspring): if self.allow_duplicate_genes == False: offspring[offspring_idx], _, _ = self.solve_duplicate_genes_by_space(solution=offspring[offspring_idx], gene_type=self.gene_type, - num_trials=10) + sample_size=self.sample_size, + mutation_by_replacement=self.mutation_by_replacement, + build_initial_pop=False) return offspring - - def change_random_mutation_value_dtype(self, random_value, gene_index): - """ - Change the data type of the random value used to apply mutation. - It accepts 2 parameters: - -random_value: The random value to change its data type. - -gene_index: The index of the target gene. - It returns the new value after changing the data type. - """ - - # If the mutation_by_replacement attribute is True, then the random value replaces the current gene value. - if self.mutation_by_replacement: - if self.gene_type_single == True: - random_value = self.gene_type[0](random_value) + def mutation_process_gene_value(self, + solution, + gene_idx, + range_min=None, + range_max=None, + sample_size=100): + + """ + Generate/select values for the gene that satisfy the constraint. The values could be generated randomly or from the gene space. + It accepts: + -range_min: The minimum value in the range from which a value is selected. + -range_max: The maximum value in the range from which a value is selected. + -solution: The solution where the target gene exists. + -gene_idx: The index of the gene in the solution. + -sample_size: The number of random values to generate from which a value is selected. It tries to generate a number of values up to a maximum of sample_size. But it is not always guaranteed because the total number of values might not be enough or the random generator creates duplicate random values. + It returns a single numeric value the satisfies the gene constraint if exists in the gene_constraint parameter. + """ + + # Check if the gene has a constraint. + if self.gene_constraint and self.gene_constraint[gene_idx]: + # Generate values that meet the gene constraint. Select more than 1 value. + # This method: 1) generates or selects the values 2) filters the values according to the constraint. + values = self.get_valid_gene_constraint_values(range_min=range_min, + range_max=range_max, + gene_value=solution[gene_idx], + gene_idx=gene_idx, + mutation_by_replacement=self.mutation_by_replacement, + solution=solution, + sample_size=sample_size) + if values is None: + # No value found that satisfy the constraint. + # Keep the old value. + value_selected = solution[gene_idx] else: - random_value = self.gene_type[gene_index][0](random_value) - if type(random_value) is numpy.ndarray: - random_value = random_value[0] - # If the mutation_by_replacement attribute is False, then the random value is added to the gene value. + # Select a value randomly from the list of values satisfying the constraint. + # If size is used with numpy.random.choice(), it returns an array even if it has a single value. To return a numeric value, not an array, then return index 0. + value_selected = numpy.random.choice(values, size=1)[0] else: - if self.gene_type_single == True: - random_value = self.gene_type[0](offspring[offspring_idx, gene_index] + random_value) - else: - random_value = self.gene_type[gene_index][0](offspring[offspring_idx, gene_index] + random_value) - if type(random_value) is numpy.ndarray: - random_value = random_value[0] - return random_value - - def round_random_mutation_value(self, random_value, gene_index): - """ - Round the random value used to apply mutation. - It accepts 2 parameters: - -random_value: The random value to round its value. - -gene_index: The index of the target gene. Only used if nested gene_type is used. - It returns the new value after being rounded. - """ - - # Round the gene - if self.gene_type_single == True: - if not self.gene_type[1] is None: - random_value = numpy.round(random_value, self.gene_type[1]) - else: - if not self.gene_type[gene_index][1] is None: - random_value = numpy.round(random_value, self.gene_type[gene_index][1]) - return random_value + # The gene does not have a constraint. Just select a single value. + value_selected = self.generate_gene_value(range_min=range_min, + range_max=range_max, + gene_value=solution[gene_idx], + gene_idx=gene_idx, + solution=solution, + mutation_by_replacement=self.mutation_by_replacement, + sample_size=1) + # Even that its name is singular, it might have a multiple values. + return value_selected def mutation_randomly(self, offspring): @@ -357,20 +186,19 @@ def mutation_randomly(self, offspring): # Random mutation changes one or more genes in each offspring randomly. for offspring_idx in range(offspring.shape[0]): - mutation_indices = numpy.array(random.sample(range(0, self.num_genes), self.mutation_num_genes)) + # Return the indices of the genes to mutate. + mutation_indices = numpy.array(random.sample(range(0, self.num_genes), + self.mutation_num_genes)) for gene_idx in mutation_indices: - range_min, range_max = self.get_mutation_range(gene_idx) + range_min, range_max = self.get_random_mutation_range(gene_idx) - # Generating a random value. - random_value = numpy.random.uniform(low=range_min, - high=range_max, - size=1)[0] - # Change the random mutation value data type. - random_value = self.change_random_mutation_value_dtype(random_value, gene_idx) - - # Round the gene. - random_value = self.round_random_mutation_value(random_value, gene_idx) + # Generate a random value for mutation that meet the gene constraint if exists. + random_value = self.mutation_process_gene_value(range_min=range_min, + range_max=range_max, + solution=offspring[offspring_idx], + gene_idx=gene_idx, + sample_size=self.sample_size) offspring[offspring_idx, gene_idx] = random_value @@ -380,7 +208,7 @@ def mutation_randomly(self, offspring): max_val=range_max, mutation_by_replacement=self.mutation_by_replacement, gene_type=self.gene_type, - num_trials=10) + sample_size=self.sample_size) return offspring @@ -395,21 +223,21 @@ def mutation_probs_randomly(self, offspring): # Random mutation changes one or more genes in each offspring randomly. for offspring_idx in range(offspring.shape[0]): + # The mutation probabilities for the current offspring. probs = numpy.random.random(size=offspring.shape[1]) for gene_idx in range(offspring.shape[1]): - range_min, range_max = self.get_mutation_range(gene_idx) + range_min, range_max = self.get_random_mutation_range(gene_idx) + # A gene is mutated only if its mutation probability is less than or equal to the threshold. if probs[gene_idx] <= self.mutation_probability: - # Generating a random value. - random_value = numpy.random.uniform(low=range_min, - high=range_max, - size=1)[0] - # Change the random mutation value data type. - random_value = self.change_random_mutation_value_dtype(random_value, gene_idx) - # Round the gene. - random_value = self.round_random_mutation_value(random_value, gene_idx) + # Generate a random value fpr mutation that meet the gene constraint if exists. + random_value = self.mutation_process_gene_value(range_min=range_min, + range_max=range_max, + solution=offspring[offspring_idx], + gene_idx=gene_idx, + sample_size=self.sample_size) offspring[offspring_idx, gene_idx] = random_value @@ -419,7 +247,7 @@ def mutation_probs_randomly(self, offspring): max_val=range_max, mutation_by_replacement=self.mutation_by_replacement, gene_type=self.gene_type, - num_trials=10) + sample_size=self.sample_size) return offspring def swap_mutation(self, offspring): @@ -617,8 +445,6 @@ def adaptive_mutation_population_fitness(self, offspring): else: raise ValueError(f"The fitness function should return a number or an iterable (list, tuple, or numpy.ndarray) but the value ({sol_fitness}) of type {type(sol_fitness)} found.") - - if len(fitness.shape) > 1: # TODO This is a multi-objective optimization problem. # Calculate the average of each objective's fitness across all solutions in the population. @@ -707,79 +533,9 @@ def adaptive_mutation_by_space(self, offspring): mutation_indices = numpy.array(random.sample(range(0, self.num_genes), adaptive_mutation_num_genes)) for gene_idx in mutation_indices: - range_min, range_max = self.get_mutation_range(gene_idx) - - if self.gene_space_nested: - # Returning the current gene space from the 'gene_space' attribute. - if type(self.gene_space[gene_idx]) in [numpy.ndarray, list]: - curr_gene_space = self.gene_space[gene_idx].copy() - else: - curr_gene_space = self.gene_space[gene_idx] - - # If the gene space has only a single value, use it as the new gene value. - if type(curr_gene_space) in pygad.GA.supported_int_float_types: - value_from_space = curr_gene_space - # 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: - rand_val = numpy.random.uniform(low=range_min, - high=range_max, - size=1)[0] - if self.mutation_by_replacement: - value_from_space = rand_val - else: - value_from_space = offspring[offspring_idx, gene_idx] + rand_val - elif type(curr_gene_space) is dict: - # Selecting a value randomly from the current gene's space in the 'gene_space' attribute. - if 'step' in curr_gene_space.keys(): - # The numpy.random.choice() and numpy.random.uniform() functions return a NumPy array as the output even if the array has a single value. - # We have to return the output at index 0 to force a numeric value to be returned not an object of type numpy.ndarray. - # If numpy.ndarray is returned, then it will cause an issue later while using the set() function. - value_from_space = numpy.random.choice(numpy.arange(start=curr_gene_space['low'], - stop=curr_gene_space['high'], - step=curr_gene_space['step']), - size=1)[0] - else: - value_from_space = numpy.random.uniform(low=curr_gene_space['low'], - high=curr_gene_space['high'], - size=1)[0] - else: - # Selecting a value randomly from 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. - if len(curr_gene_space) == 1: - value_from_space = curr_gene_space[0] - # If the gene space has more than 1 value, then select a new one that is different from the current value. - else: - values_to_select_from = list(set(curr_gene_space) - set([offspring[offspring_idx, gene_idx]])) - - if len(values_to_select_from) == 0: - value_from_space = offspring[offspring_idx, gene_idx] - else: - value_from_space = random.choice(values_to_select_from) - else: - # Selecting a value randomly from the global gene space in the 'gene_space' attribute. - if type(self.gene_space) is dict: - 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)[0] - else: - value_from_space = numpy.random.uniform(low=self.gene_space['low'], - high=self.gene_space['high'], - size=1)[0] - else: - values_to_select_from = list(set(self.gene_space) - set([offspring[offspring_idx, gene_idx]])) - - if len(values_to_select_from) == 0: - value_from_space = offspring[offspring_idx, gene_idx] - else: - value_from_space = random.choice(values_to_select_from) - - - if value_from_space is None: - value_from_space = numpy.random.uniform(low=range_min, - high=range_max, - size=1)[0] + value_from_space = self.mutation_process_gene_value(solution=offspring[offspring_idx], + gene_idx=gene_idx, + sample_size=self.sample_size) # Assigning the selected value from the space to the gene. if self.gene_type_single == True: @@ -798,7 +554,9 @@ def adaptive_mutation_by_space(self, offspring): if self.allow_duplicate_genes == False: offspring[offspring_idx], _, _ = self.solve_duplicate_genes_by_space(solution=offspring[offspring_idx], gene_type=self.gene_type, - num_trials=10) + sample_size=self.sample_size, + mutation_by_replacement=self.mutation_by_replacement, + build_initial_pop=False) return offspring def adaptive_mutation_randomly(self, offspring): @@ -845,17 +603,14 @@ def adaptive_mutation_randomly(self, offspring): mutation_indices = numpy.array(random.sample(range(0, self.num_genes), adaptive_mutation_num_genes)) for gene_idx in mutation_indices: - range_min, range_max = self.get_mutation_range(gene_idx) - - # Generating a random value. - random_value = numpy.random.uniform(low=range_min, - high=range_max, - size=1)[0] - # Change the random mutation value data type. - random_value = self.change_random_mutation_value_dtype(random_value, gene_idx) + range_min, range_max = self.get_random_mutation_range(gene_idx) - # Round the gene. - random_value = self.round_random_mutation_value(random_value, gene_idx) + # Generate a random value fpr mutation that meet the gene constraint if exists. + random_value = self.mutation_process_gene_value(range_min=range_min, + range_max=range_max, + solution=offspring[offspring_idx], + gene_idx=gene_idx, + sample_size=self.sample_size) offspring[offspring_idx, gene_idx] = random_value @@ -865,7 +620,7 @@ def adaptive_mutation_randomly(self, offspring): max_val=range_max, mutation_by_replacement=self.mutation_by_replacement, gene_type=self.gene_type, - num_trials=10) + sample_size=self.sample_size) return offspring def adaptive_mutation_probs_by_space(self, offspring): @@ -914,81 +669,13 @@ def adaptive_mutation_probs_by_space(self, offspring): probs = numpy.random.random(size=offspring.shape[1]) for gene_idx in range(offspring.shape[1]): - range_min, range_max = self.get_mutation_range(gene_idx) - if probs[gene_idx] <= adaptive_mutation_probability: - if self.gene_space_nested: - # Returning the current gene space from the 'gene_space' attribute. - if type(self.gene_space[gene_idx]) in [numpy.ndarray, list]: - curr_gene_space = self.gene_space[gene_idx].copy() - else: - curr_gene_space = self.gene_space[gene_idx] - - # If the gene space has only a single value, use it as the new gene value. - if type(curr_gene_space) in pygad.GA.supported_int_float_types: - value_from_space = curr_gene_space - # 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: - rand_val = numpy.random.uniform(low=range_min, - high=range_max, - size=1)[0] - if self.mutation_by_replacement: - value_from_space = rand_val - else: - value_from_space = offspring[offspring_idx, gene_idx] + rand_val - elif type(curr_gene_space) is dict: - # Selecting a value randomly from the current gene's space in the 'gene_space' attribute. - 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)[0] - else: - value_from_space = numpy.random.uniform(low=curr_gene_space['low'], - high=curr_gene_space['high'], - size=1)[0] - else: - # Selecting a value randomly from 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. - if len(curr_gene_space) == 1: - value_from_space = curr_gene_space[0] - # If the gene space has more than 1 value, then select a new one that is different from the current value. - else: - values_to_select_from = list(set(curr_gene_space) - set([offspring[offspring_idx, gene_idx]])) - - if len(values_to_select_from) == 0: - value_from_space = offspring[offspring_idx, gene_idx] - else: - value_from_space = random.choice(values_to_select_from) - else: - # Selecting a value randomly from the global gene space in the 'gene_space' attribute. - if type(self.gene_space) is dict: - if 'step' in self.gene_space.keys(): - # The numpy.random.choice() and numpy.random.uniform() functions return a NumPy array as the output even if the array has a single value. - # We have to return the output at index 0 to force a numeric value to be returned not an object of type numpy.ndarray. - # If numpy.ndarray is returned, then it will cause an issue later while using the set() function. - 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)[0] - else: - value_from_space = numpy.random.uniform(low=self.gene_space['low'], - high=self.gene_space['high'], - size=1)[0] - else: - values_to_select_from = list(set(self.gene_space) - set([offspring[offspring_idx, gene_idx]])) - if len(values_to_select_from) == 0: - value_from_space = offspring[offspring_idx, gene_idx] - else: - value_from_space = random.choice(values_to_select_from) - - if value_from_space is None: - value_from_space = numpy.random.uniform(low=range_min, - high=range_max, - size=1)[0] + value_from_space = self.mutation_process_gene_value(solution=offspring[offspring_idx], + gene_idx=gene_idx, + sample_size=self.sample_size) - # Assinging the selected value from the space to the gene. + # Assigning the selected value from the space to the gene. if self.gene_type_single == True: if not self.gene_type[1] is None: offspring[offspring_idx, gene_idx] = numpy.round(self.gene_type[0](value_from_space), @@ -1005,7 +692,9 @@ def adaptive_mutation_probs_by_space(self, offspring): if self.allow_duplicate_genes == False: offspring[offspring_idx], _, _ = self.solve_duplicate_genes_by_space(solution=offspring[offspring_idx], gene_type=self.gene_type, - num_trials=10) + sample_size=self.sample_size, + mutation_by_replacement=self.mutation_by_replacement, + build_initial_pop=False) return offspring def adaptive_mutation_probs_randomly(self, offspring): @@ -1052,18 +741,15 @@ def adaptive_mutation_probs_randomly(self, offspring): probs = numpy.random.random(size=offspring.shape[1]) for gene_idx in range(offspring.shape[1]): - range_min, range_max = self.get_mutation_range(gene_idx) + range_min, range_max = self.get_random_mutation_range(gene_idx) if probs[gene_idx] <= adaptive_mutation_probability: - # Generating a random value. - random_value = numpy.random.uniform(low=range_min, - high=range_max, - size=1)[0] - # Change the random mutation value data type. - random_value = self.change_random_mutation_value_dtype(random_value, gene_idx) - - # Round the gene. - random_value = self.round_random_mutation_value(random_value, gene_idx) + # Generate a random value fpr mutation that meet the gene constraint if exists. + random_value = self.mutation_process_gene_value(range_min=range_min, + range_max=range_max, + solution=offspring[offspring_idx], + gene_idx=gene_idx, + sample_size=self.sample_size) offspring[offspring_idx, gene_idx] = random_value @@ -1073,5 +759,5 @@ def adaptive_mutation_probs_randomly(self, offspring): max_val=range_max, mutation_by_replacement=self.mutation_by_replacement, gene_type=self.gene_type, - num_trials=10) + sample_size=self.sample_size) return offspring diff --git a/setup.py b/setup.py index d9ec309..2d9478c 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="pygad", - version="3.4.0", + version="3.5.0", author="Ahmed Fawzy Gad", install_requires=["numpy", "matplotlib", "cloudpickle",], author_email="ahmed.f.gad@gmail.com", diff --git a/tests/test_gene_constraint.py b/tests/test_gene_constraint.py new file mode 100644 index 0000000..43cf38a --- /dev/null +++ b/tests/test_gene_constraint.py @@ -0,0 +1,223 @@ +import pygad +import random +import numpy + +num_generations = 1 + +initial_population = [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]] + +def population_gene_constraint(gene_space=None, + gene_type=float, + num_genes=10, + mutation_by_replacement=False, + random_mutation_min_val=-1, + random_mutation_max_val=1, + init_range_low=-4, + init_range_high=4, + random_seed=123, + crossover_type='single_point', + initial_population=None, + parent_selection_type='sss', + multi_objective=False, + gene_constraint=None, + allow_duplicate_genes=True): + + def fitness_func_no_batch_single(ga, solution, idx): + return random.random() + + def fitness_func_no_batch_multi(ga, solution, idx): + return [random.random(), random.random()] + + if multi_objective == True: + fitness_func = fitness_func_no_batch_multi + else: + fitness_func = fitness_func_no_batch_single + + ga_instance = pygad.GA(num_generations=num_generations, + num_parents_mating=5, + fitness_func=fitness_func, + sol_per_pop=10, + num_genes=num_genes, + gene_space=gene_space, + gene_type=gene_type, + initial_population=initial_population, + parent_selection_type=parent_selection_type, + init_range_low=init_range_low, + init_range_high=init_range_high, + random_mutation_min_val=random_mutation_min_val, + random_mutation_max_val=random_mutation_max_val, + allow_duplicate_genes=allow_duplicate_genes, + mutation_by_replacement=mutation_by_replacement, + random_seed=random_seed, + crossover_type=crossover_type, + gene_constraint=gene_constraint, + save_solutions=True, + suppress_warnings=False) + + ga_instance.run() + + return ga_instance + +#### Single-Objective +def test_initial_population_int_by_replacement(): + gene_constraint=[lambda x: x[0]>=8,lambda x: x[1]>=8,lambda x: 5>=x[2]>=1,lambda x: 5>x[3]>3,lambda x: x[4]<2] + ga_instance = population_gene_constraint(gene_constraint=gene_constraint, + init_range_low=0, + init_range_high=10, + random_mutation_min_val=0, + random_mutation_max_val=10, + num_genes=5, + gene_type=int, + mutation_by_replacement=True) + initial_population = ga_instance.initial_population + # print(initial_population) + + assert numpy.all(initial_population[:, 0] >= 8), "Not all values in column 0 are >= 8" + assert numpy.all(initial_population[:, 1] >= 8), "Not all values in column 1 are >= 8" + assert numpy.all(initial_population[:, 2] >= 1), "Not all values in column 2 are >= 1" + assert numpy.all((initial_population[:, 2] >= 1) & (initial_population[:, 2] <= 5)), "Not all values in column 2 between 1 and 5 (inclusive)" + assert numpy.all(initial_population[:, 3] == 4), "Not all values in column 3 between 3 and 5 (exclusive)" + assert numpy.all(initial_population[:, 4] < 2), "Not all values in column 4 < 2" + +def test_initial_population_int_by_replacement_no_duplicates(): + gene_constraint=[lambda x: x[0]>=5,lambda x: x[1]>=5,lambda x: x[2]>=5,lambda x: x[3]>=5,lambda x: x[4]>=5] + ga_instance = population_gene_constraint(gene_constraint=gene_constraint, + init_range_low=1, + init_range_high=10, + random_mutation_min_val=1, + random_mutation_max_val=10, + gene_type=int, + num_genes=5, + mutation_by_replacement=True, + allow_duplicate_genes=False) + + num_duplicates = 0 + for idx, solution in enumerate(ga_instance.solutions): + num = len(solution) - len(set(solution)) + if num != 0: + print(solution, idx) + num_duplicates += num + + assert num_duplicates == 0 + + initial_population = ga_instance.initial_population + # print(initial_population) + + assert numpy.all(initial_population[:, 0] >= 5), "Not all values in column 0 >= 5" + assert numpy.all(initial_population[:, 1] >= 5), "Not all values in column 1 >= 5" + assert numpy.all(initial_population[:, 2] >= 5), "Not all values in column 2 >= 5" + assert numpy.all(initial_population[:, 3] >= 5), "Not all values in column 3 >= 5" + assert numpy.all(initial_population[:, 4] >= 5), "Not all values in column 4 >= 5" + +def test_initial_population_int_by_replacement_no_duplicates2(): + gene_constraint=[lambda x: x[0]>=98,lambda x: x[1]>=98,lambda x: 20= 98), "Not all values in column 0 are >= 98" + assert numpy.all(initial_population[:, 1] >= 98), "Not all values in column 1 are >= 98" + assert numpy.all((initial_population[:, 2] > 20) & (initial_population[:, 2] < 40)), "Not all values in column 2 between 20 and 40 (exclusive)" + assert numpy.all(initial_population[:, 3] < 40), "Not all values in column 3 < 40" + assert numpy.all(initial_population[:, 4] < 50), "Not all values in column 4 < 50" + assert numpy.all(initial_population[:, 5] < 100), "Not all values in column 4 < 100" + +def test_initial_population_float_by_replacement_no_duplicates(): + gene_constraint=[lambda x: x[0]>=5,lambda x: x[1]>=5,lambda x: x[2]>=5,lambda x: x[3]>=5,lambda x: x[4]>=5] + ga_instance = population_gene_constraint(gene_constraint=gene_constraint, + init_range_low=1, + init_range_high=10, + gene_type=[float, 1], + num_genes=5, + crossover_type=None, + mutation_by_replacement=False, + allow_duplicate_genes=False) + + num_duplicates = 0 + for idx, solution in enumerate(ga_instance.solutions): + num = len(solution) - len(set(solution)) + if num != 0: + print(solution, idx) + num_duplicates += num + + assert num_duplicates == 0 + + initial_population = ga_instance.initial_population + # print(initial_population) + + assert numpy.all(initial_population[:, 0] >= 5), "Not all values in column 0 >= 5" + assert numpy.all(initial_population[:, 1] >= 5), "Not all values in column 1 >= 5" + assert numpy.all(initial_population[:, 2] >= 5), "Not all values in column 2 >= 5" + assert numpy.all(initial_population[:, 3] >= 5), "Not all values in column 3 >= 5" + assert numpy.all(initial_population[:, 4] >= 5), "Not all values in column 4 >= 5" + +def test_initial_population_float_by_replacement_no_duplicates2(): + gene_constraint=[lambda x: x[0]>=1,lambda x: x[1]>=1,lambda x: x[2]>=1,lambda x: x[3]>=1,lambda x: x[4]>=1] + ga_instance = population_gene_constraint(gene_constraint=gene_constraint, + init_range_low=1, + init_range_high=2, + gene_type=[float, 1], + num_genes=5, + crossover_type=None, + mutation_by_replacement=False, + allow_duplicate_genes=False) + + num_duplicates = 0 + for idx, solution in enumerate(ga_instance.solutions): + num = len(solution) - len(set(solution)) + if num != 0: + print(solution, idx) + num_duplicates += num + + assert num_duplicates == 0 + + initial_population = ga_instance.initial_population + # print(initial_population) + + assert numpy.all(initial_population[:, 0] >= 1), "Not all values in column 0 >= 1" + assert numpy.all(initial_population[:, 1] >= 1), "Not all values in column 1 >= 1" + assert numpy.all(initial_population[:, 2] >= 1), "Not all values in column 2 >= 1" + assert numpy.all(initial_population[:, 3] >= 1), "Not all values in column 3 >= 1" + assert numpy.all(initial_population[:, 4] >= 1), "Not all values in column 4 >= 1" + +if __name__ == "__main__": + #### Single-objective + print() + test_initial_population_int_by_replacement() + print() + test_initial_population_int_by_replacement_no_duplicates() + print() + test_initial_population_int_by_replacement_no_duplicates2() + print() + test_initial_population_float_by_replacement_no_duplicates() + print() + test_initial_population_float_by_replacement_no_duplicates2() + print() diff --git a/tests/test_gene_space.py b/tests/test_gene_space.py index b633880..2e83bf6 100644 --- a/tests/test_gene_space.py +++ b/tests/test_gene_space.py @@ -201,7 +201,7 @@ def fitness_func_no_batch_multi(ga, solution, idx): else: num_outside += 1 - print(f"Number of outside range is {num_outside}.") + print(f"Number of values outside the range is: {num_outside}.") return num_outside, ga_instance #### Single-Objective diff --git a/tests/test_gene_space_allow_duplicate_genes.py b/tests/test_gene_space_allow_duplicate_genes.py index c35f388..3a36699 100644 --- a/tests/test_gene_space_allow_duplicate_genes.py +++ b/tests/test_gene_space_allow_duplicate_genes.py @@ -31,6 +31,7 @@ def number_respect_gene_space(gene_space=None, init_range_low=-4, init_range_high=4, initial_population=None, + allow_duplicate_genes=False, parent_selection_type='sss', multi_objective=False): @@ -85,7 +86,7 @@ def fitness_func_no_batch_multi(ga, solution, idx): elif type(ga_instance.gene_space[gene_idx]) is dict: if not "step" in ga_instance.gene_space[gene_idx].keys(): for val in all_gene_values: - if val >= ga_instance.gene_space[gene_idx]["low"] and val < ga_instance.gene_space[gene_idx]["high"]: + if ga_instance.gene_space[gene_idx]["low"] <= val <= ga_instance.gene_space[gene_idx]["high"]: pass else: num_outside += 1 @@ -118,7 +119,7 @@ def fitness_func_no_batch_multi(ga, solution, idx): elif type(ga_instance.gene_space) is dict: if not "step" in ga_instance.gene_space.keys(): for val in all_gene_values: - if val >= ga_instance.gene_space["low"] and val < ga_instance.gene_space["high"]: + if ga_instance.gene_space["low"] <= val <= ga_instance.gene_space["high"]: pass else: num_outside += 1 @@ -197,7 +198,7 @@ def test_gene_space_numpy_nested_gene_type(): def test_gene_space_dict_without_step_nested_gene_type(): num_outside, ga_instance = number_respect_gene_space(gene_space={"low": 0, "high": 10}, - gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]]) + gene_type=[int, float, numpy.float64, [float, 3], [float, 4], numpy.int16, [numpy.float32, 1], int, float, [float, 3]]) assert num_outside == 0 def test_gene_space_dict_with_step_nested_gene_type(): diff --git a/tests/test_gene_type.py b/tests/test_gene_type.py new file mode 100644 index 0000000..691d046 --- /dev/null +++ b/tests/test_gene_type.py @@ -0,0 +1,214 @@ +import pygad +import random +import numpy + +num_generations = 5 + +initial_population = [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]] + +def validate_gene_type_and_rounding(gene_space=None, + gene_type=float, + num_genes=10, + mutation_by_replacement=False, + random_mutation_min_val=-1, + random_mutation_max_val=1, + init_range_low=-4, + init_range_high=4, + initial_population=None, + crossover_probability=None, + mutation_probability=None, + crossover_type=None, + mutation_type=None, + gene_constraint=None, + parent_selection_type='sss', + multi_objective=False): + + def fitness_func_no_batch_single(ga, solution, idx): + return random.random() + + def fitness_func_no_batch_multi(ga, solution, idx): + return [random.random(), random.random()] + + if multi_objective == True: + fitness_func = fitness_func_no_batch_multi + else: + fitness_func = fitness_func_no_batch_single + + ga_instance = pygad.GA(num_generations=num_generations, + num_parents_mating=5, + fitness_func=fitness_func, + sol_per_pop=10, + num_genes=num_genes, + gene_space=gene_space, + gene_constraint=gene_constraint, + gene_type=gene_type, + parent_selection_type=parent_selection_type, + initial_population=initial_population, + init_range_low=init_range_low, + init_range_high=init_range_high, + random_mutation_min_val=random_mutation_min_val, + random_mutation_max_val=random_mutation_max_val, + allow_duplicate_genes=True, + mutation_by_replacement=mutation_by_replacement, + save_solutions=True, + crossover_probability=crossover_probability, + mutation_probability=mutation_probability, + crossover_type=crossover_type, + mutation_type=mutation_type, + suppress_warnings=True, + random_seed=1) + + ga_instance.run() + + for sol_idx in range(len(ga_instance.solutions)): + for gene_idx in range(ga_instance.num_genes): + if ga_instance.gene_type_single: + dtype = ga_instance.gene_type + else: + dtype = ga_instance.gene_type[gene_idx] + + if dtype[0] is float: + # NumPy converts the Python float data type to numpy.float64. Both are identical. + assert type(ga_instance.solutions[sol_idx][gene_idx]) in [dtype[0], numpy.float64] + else: + assert type(ga_instance.solutions[sol_idx][gene_idx]) is dtype[0] + + if dtype[1] is None: + pass + else: + num_decimals = len(str(ga_instance.solutions[sol_idx][gene_idx]).split('.')[1]) + # The numbers may not have the exact precision. + # For example, the float number might be 5.7, and we set the precision to 2. + # Because there is no enough digits after the decimal point, we cannot meet the precision of 2. + # We only care about not exceeding the user-defined precision. + assert num_decimals <= dtype[1] + + return ga_instance + +def test_nested_gene_type(): + ga_instance = validate_gene_type_and_rounding(gene_type=[numpy.int32, + numpy.float16, + numpy.float32, + [numpy.float16, 3], + [numpy.float32, 4], + numpy.int16, + [numpy.float32, 1], + numpy.int32, + numpy.float16, + numpy.float64]) + +def test_single_gene_type_float16(): + ga_instance = validate_gene_type_and_rounding(gene_type=[numpy.float16, 2]) + +def test_single_gene_type_int32(): + ga_instance = validate_gene_type_and_rounding(gene_type=numpy.int32) + +def test_single_gene_space_single_gene_type(): + ga_instance = validate_gene_type_and_rounding(gene_space={"low": 0, "high": 10}, + gene_type=[float, 2]) + +def test_nested_gene_space_single_gene_type(): + ga_instance = validate_gene_type_and_rounding(gene_space=[[0, 1, 2, 3, 4], + numpy.arange(5, 10), + range(10, 15), + {"low": 15, "high": 20}, + {"low": 20, "high": 30, "step": 2}, + None, + numpy.arange(30, 35), + numpy.arange(35, 40), + numpy.arange(40, 45), + [45, 46, 47, 48, 49]], + gene_type=[numpy.float16, 1]) + +def test_nested_gene_space_nested_gene_type(): + ga_instance = validate_gene_type_and_rounding(gene_space=[[0, 1, 2, 3, 4], + numpy.arange(5, 10), + range(10, 15), + {"low": 15, "high": 20}, + {"low": 20, "high": 30, "step": 2}, + None, + numpy.arange(30, 35), + numpy.arange(35, 40), + numpy.arange(40, 45), + [45, 46, 47, 48, 49]], + gene_type=[int, + float, + numpy.float64, + [float, 3], + [float, 4], + numpy.int16, + [numpy.float32, 1], + int, + float, + [float, 3]]) + +def test_single_gene_space_nested_gene_type(): + ga_instance = validate_gene_type_and_rounding(gene_space=numpy.arange(0, 100), + gene_type=[int, + float, + numpy.float64, + [float, 3], + [float, 4], + numpy.int16, + [numpy.float32, 1], + int, + float, + [float, 3]]) + +def test_custom_initial_population_single_gene_type(): + global initial_population + ga_instance = validate_gene_type_and_rounding(initial_population=initial_population, + gene_type=[numpy.float16, 2]) + +def test_custom_initial_population_nested_gene_type(): + global initial_population + ga_instance = validate_gene_type_and_rounding(initial_population=initial_population, + gene_type=[int, + float, + numpy.float64, + [float, 3], + [float, 4], + numpy.int16, + [numpy.float32, 1], + int, + float, + [float, 3]]) + +if __name__ == "__main__": + #### Single-objective + print() + test_nested_gene_type() + print() + + test_single_gene_type_float16() + print() + + test_single_gene_type_int32() + print() + + test_single_gene_space_single_gene_type() + print() + + test_nested_gene_space_single_gene_type() + print() + + test_nested_gene_space_nested_gene_type() + print() + + test_single_gene_space_nested_gene_type() + print() + + test_custom_initial_population_single_gene_type() + print() + + test_custom_initial_population_nested_gene_type() + print() diff --git a/tests/test_save_solutions.py b/tests/test_save_solutions.py index 45216e1..537f4ea 100644 --- a/tests/test_save_solutions.py +++ b/tests/test_save_solutions.py @@ -5,7 +5,7 @@ sol_per_pop = 10 num_parents_mating = 5 -# TODO Verify that the each entry in 'solutions_fitness' and 'best_solutions_fitness' have values equal to the number of objectives. +# TODO Verify that each entry in 'solutions_fitness' and 'best_solutions_fitness' has values equal to the number of objectives. def number_saved_solutions(keep_elitism=1, keep_parents=-1,