Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions pygad/utils/mutation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)

Expand Down
44 changes: 44 additions & 0 deletions tests/test_crossover_mutation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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()