diff --git a/pygad/utils/mutation.py b/pygad/utils/mutation.py index 66d72e29..0b6b8f6c 100644 --- a/pygad/utils/mutation.py +++ b/pygad/utils/mutation.py @@ -80,6 +80,12 @@ def mutation_by_space(self, offspring): gene_idx=gene_idx, sample_size=self.sample_size) + if self.allow_duplicate_genes == False and value_from_space == offspring[offspring_idx, gene_idx]: + # No value of the gene space is free (e.g. a permutation): swap the gene with another one instead. + offspring[offspring_idx] = self.swap_gene_by_space(solution=offspring[offspring_idx], + gene_idx=gene_idx) + continue + # Before assigning the selected value from the space to the gene, change its data type and round it. offspring[offspring_idx, gene_idx] = self.change_gene_dtype_and_round(gene_idx, value_from_space) @@ -119,6 +125,12 @@ def mutation_probs_by_space(self, offspring): gene_idx=gene_idx, sample_size=self.sample_size) + if self.allow_duplicate_genes == False and value_from_space == offspring[offspring_idx, gene_idx]: + # No value of the gene space is free (e.g. a permutation): swap the gene with another one instead. + offspring[offspring_idx] = self.swap_gene_by_space(solution=offspring[offspring_idx], + gene_idx=gene_idx) + continue + # Assigning the selected value from the space to the gene. offspring[offspring_idx, gene_idx] = self.change_gene_dtype_and_round(gene_idx, value_from_space) @@ -201,6 +213,57 @@ def mutation_process_gene_value(self, # Even though its name is singular, it might hold multiple values. return value_selected + def swap_gene_by_space(self, + solution, + gene_idx): + """ + With ``allow_duplicate_genes=False``, a gene cannot take a new + value from its space when all of them are used by other genes, + as in a permutation (``gene_space=range(num_genes)``). To still + change the solution, swap the gene's value with the value of + another gene, picked at random among the genes whose value is + in this gene's space and whose space holds this gene's value. + The swap keeps the genes unique. Genes with a + ``gene_constraint`` are not swapped. + + Parameters + ---------- + solution : numpy.ndarray + The solution that owns the gene (modified in place). + gene_idx : int + Index of the gene inside ``solution``. + + Returns + ------- + solution : numpy.ndarray + The solution after the swap, unchanged if no gene qualifies. + """ + + def gene_space_values(idx): + if self.gene_space_nested or not self.gene_type_single: + return self.gene_space_unpacked[idx] + else: + return self.gene_space_unpacked + + def has_constraint(idx): + return bool(self.gene_constraint and self.gene_constraint[idx]) + + if has_constraint(gene_idx): + return solution + + gene_value = solution[gene_idx] + candidates = [other_idx for other_idx in range(len(solution)) + if solution[other_idx] != gene_value + and not has_constraint(other_idx) + and solution[other_idx] in gene_space_values(gene_idx) + and gene_value in gene_space_values(other_idx)] + + if len(candidates) > 0: + other_idx = random.choice(candidates) + solution[gene_idx] = solution[other_idx] + solution[other_idx] = gene_value + return solution + def mutation_randomly(self, offspring): """ Mutate ``self.mutation_num_genes`` genes per offspring by @@ -698,6 +761,12 @@ def adaptive_mutation_by_space(self, offspring): gene_idx=gene_idx, sample_size=self.sample_size) + if self.allow_duplicate_genes == False and value_from_space == offspring[offspring_idx, gene_idx]: + # No value of the gene space is free (e.g. a permutation): swap the gene with another one instead. + offspring[offspring_idx] = self.swap_gene_by_space(solution=offspring[offspring_idx], + gene_idx=gene_idx) + continue + # Assigning the selected value from the space to the gene. offspring[offspring_idx, gene_idx] = self.change_gene_dtype_and_round(gene_idx, value_from_space) @@ -843,6 +912,12 @@ def adaptive_mutation_probs_by_space(self, offspring): gene_idx=gene_idx, sample_size=self.sample_size) + if self.allow_duplicate_genes == False and value_from_space == offspring[offspring_idx, gene_idx]: + # No value of the gene space is free (e.g. a permutation): swap the gene with another one instead. + offspring[offspring_idx] = self.swap_gene_by_space(solution=offspring[offspring_idx], + gene_idx=gene_idx) + continue + # Assigning the selected value from the space to the gene. offspring[offspring_idx, gene_idx] = self.change_gene_dtype_and_round(gene_idx, value_from_space) diff --git a/tests/test_crossover_mutation.py b/tests/test_crossover_mutation.py index acc38942..27d35d39 100644 --- a/tests/test_crossover_mutation.py +++ b/tests/test_crossover_mutation.py @@ -241,6 +241,41 @@ def test_random_mutation_manual_call4(): for value in comp_sorted: assert value in value_space +def random_mutation_permutation(gene_space, mutation_probability=None): + # Each solution is a permutation of range(num_genes): no value of the gene space is free. + num_genes = 8 + ga_instance = pygad.GA(num_generations=num_generations, + num_parents_mating=2, + fitness_func=lambda ga, solution, idx: random.random(), + sol_per_pop=4, + num_genes=num_genes, + gene_space=gene_space, + gene_type=int, + allow_duplicate_genes=False, + mutation_type="random", + mutation_probability=mutation_probability, + suppress_warnings=True, + random_seed=1) + + temp_offspring = numpy.array([numpy.random.permutation(num_genes) for _ in range(100)]) + offspring = ga_instance.random_mutation(offspring=temp_offspring.copy()) + + for solution in offspring: + # The mutation keeps the permutation. + assert sorted(solution) == list(range(num_genes)) + # The mutation changes the solutions. + assert numpy.all(numpy.any(offspring != temp_offspring, axis=1)) + +def test_random_mutation_permutation(): + random_mutation_permutation(gene_space=list(range(8))) + +def test_random_mutation_permutation_probability(): + random_mutation_permutation(gene_space=list(range(8)), + mutation_probability=1.0) + +def test_random_mutation_permutation_nested_gene_space(): + random_mutation_permutation(gene_space=[list(range(8))] * 8) + if __name__ == "__main__": #### Single-objective print() @@ -285,3 +320,12 @@ def test_random_mutation_manual_call4(): test_random_mutation_manual_call4() print() + + test_random_mutation_permutation() + print() + + test_random_mutation_permutation_probability() + print() + + test_random_mutation_permutation_nested_gene_space() + print()