Skip to content

Commit

Permalink
Refactor mutate
Browse files Browse the repository at this point in the history
- mutate_hidden_region now considers whether mutated input gene
affects output of the graph, i.e., is active
- more tests
- cleanup
  • Loading branch information
jakobj committed Apr 28, 2020
1 parent ffd7806 commit 91b62d5
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 30 deletions.
65 changes: 37 additions & 28 deletions gp/genome.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ def _validate_dna(self, dna):
raise ValueError("input gene for output nodes has invalid value")

if output_region[2:] != [None] * (self._primitives.max_arity - 1):
raise ValueError("non-coding input genes for output nodes need to " "be empty")
raise ValueError("inactive input genes for output nodes need to be empty")

def _hidden_column_idx(self, region_idx):
assert self._n_inputs <= region_idx
Expand Down Expand Up @@ -291,11 +291,22 @@ def _is_hidden_region(self, region_idx):
def _is_output_region(self, region_idx):
return self._n_inputs + self._n_hidden <= region_idx

def _is_function_gene(self, idx):
return (idx % self._length_per_region) == 0

def _is_input_gene(self, idx):
return not self._is_function_gene(idx)
def _is_function_gene(self, gene_idx):
return (gene_idx % self._length_per_region) == 0

def _is_active_input_gene(self, gene_idx):
input_index = gene_idx % self._length_per_region
assert input_index > 0
region_idx = gene_idx // self._length_per_region
if self._is_input_region(region_idx):
return False
elif self._is_hidden_region(region_idx):
node_arity = self._primitives[self._dna[region_idx * self._length_per_region]]._arity
return input_index <= node_arity
elif self._is_output_region(region_idx):
return input_index == 1
else:
assert False # should never be reached

def mutate(self, n_mutations, active_regions, rng):
"""Mutate the genome.
Expand Down Expand Up @@ -324,55 +335,53 @@ def mutate(self, n_mutations, active_regions, rng):
gene_idx = rng.randint(0, self._n_genes)
region_idx = gene_idx // self._length_per_region

# TODO: parameters to control mutation rates of specific
# genes?
if self._is_input_region(region_idx):
continue # nothing to do here

elif self._is_output_region(region_idx):
success = self._mutate_output_region(gene_idx, region_idx, rng)
success = self._mutate_output_region(gene_idx, rng)
if success:
only_silent_mutations = only_silent_mutations and (
region_idx not in active_regions
)
silent = False
only_silent_mutations = only_silent_mutations and silent
successful_mutations += 1

elif self._is_hidden_region(region_idx):
silent = self._mutate_hidden_region(gene_idx, active_regions, rng)
only_silent_mutations = only_silent_mutations and silent

else:
success = self._mutate_hidden_region(gene_idx, region_idx, rng)
if success:
only_silent_mutations = only_silent_mutations and (
region_idx not in active_regions
)
successful_mutations += 1
assert False # should never be reached

self._validate_dna(self._dna)

return only_silent_mutations

def _mutate_output_region(self, gene_idx, region_idx, rng):
assert self._is_output_region(region_idx)
def _mutate_output_region(self, gene_idx, rng):
assert self._is_gene_in_output_region(gene_idx)

# only mutate coding output gene
if self._is_input_gene(gene_idx) and self._dna[gene_idx] is not None:
if not self._is_function_gene(gene_idx) and self._is_active_input_gene(gene_idx):
permissable_inputs = self._permissable_inputs_for_output_region()
self._dna[gene_idx] = rng.choice(permissable_inputs)
return True
else:
return False

return False
def _mutate_hidden_region(self, gene_idx, active_regions, rng):
assert self._is_gene_in_hidden_region(gene_idx)

def _mutate_hidden_region(self, gene_idx, region_idx, rng):
assert self._is_hidden_region(region_idx)
region_idx = gene_idx // self._length_per_region
silent_mutation = region_idx not in active_regions

if self._is_function_gene(gene_idx):
self._dna[gene_idx] = self._primitives.sample(rng)
return True
return silent_mutation

else:
permissable_inputs = self._permissable_inputs(region_idx)
self._dna[gene_idx] = rng.choice(permissable_inputs)
return True

return False
silent_mutation = silent_mutation or (not self._is_active_input_gene(gene_idx))
return silent_mutation

@property
def primitives(self):
Expand Down
47 changes: 45 additions & 2 deletions test/test_genome.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def test_check_dna_consistency():
with pytest.raises(ValueError):
genome.dna = [-1, None, None, -1, None, None, 0, 0, 1, -2, 3, None]

# invalid non-coding input gene for output node
# invalid inactive input gene for output node
with pytest.raises(ValueError):
genome.dna = [-1, None, None, -1, None, None, 0, 0, 1, -2, 0, 0]

Expand Down Expand Up @@ -150,7 +150,7 @@ def test_check_levels_back_consistency():
)


def test_catch_invalid_allele_in_non_coding_region():
def test_catch_invalid_allele_in_inactive_region():
primitives = [gp.ConstantFloat]
genome = gp.Genome(1, 1, 1, 1, 1, primitives)

Expand Down Expand Up @@ -238,3 +238,46 @@ def test_is_gene_in_output_region(rng_seed):

assert genome._is_gene_in_output_region(12)
assert not genome._is_gene_in_output_region(11)


def test_mutate_hidden_region(rng_seed):
rng = np.random.RandomState(rng_seed)
genome = gp.Genome(1, 1, 3, 1, None, [gp.Add, gp.ConstantFloat])
dna = [-1, None, None, 1, 0, 0, 1, 0, 0, 0, 0, 2, -2, 3, None]
genome.dna = list(dna)
active_regions = gp.CartesianGraph(genome).determine_active_regions()

# mutating any gene in inactive region returns True
genome.dna = list(dna)
assert genome._mutate_hidden_region(3, active_regions, rng) is True
genome.dna = list(dna)
assert genome._mutate_hidden_region(4, active_regions, rng) is True
genome.dna = list(dna)
assert genome._mutate_hidden_region(5, active_regions, rng) is True

# mutating function gene in active region returns False
genome.dna = list(dna)
assert genome._mutate_hidden_region(6, active_regions, rng) is False
# mutating inactive genes in active region returns True
genome.dna = list(dna)
assert genome._mutate_hidden_region(7, active_regions, rng) is True
genome.dna = list(dna)
assert genome._mutate_hidden_region(8, active_regions, rng) is True

# mutating any gene in active region without silent genes returns False
genome.dna = list(dna)
assert genome._mutate_hidden_region(9, active_regions, rng) is False
genome.dna = list(dna)
assert genome._mutate_hidden_region(10, active_regions, rng) is False
genome.dna = list(dna)
assert genome._mutate_hidden_region(11, active_regions, rng) is False


def test_mutate_output_region(rng_seed):
rng = np.random.RandomState(rng_seed)
genome = gp.Genome(1, 1, 2, 1, None, [gp.Add])
genome.dna = [-1, None, None, 0, 0, 0, 0, 0, 0, -2, 2, None]

assert genome._mutate_output_region(9, rng) is False
assert genome._mutate_output_region(10, rng) is True
assert genome._mutate_output_region(11, rng) is False
19 changes: 19 additions & 0 deletions test/test_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,22 @@ def test_constant_float():

# by default the output value of the ConstantFloat node is 1.0
assert 1.0 == pytest.approx(y[0])


def test_inputs_are_cut_to_match_arity():
"""Test that even if a list of inputs longer than the node arity is
provided, Node.inputs only returns the initial <arity> inputs,
ignoring the inactive genes.
"""
idx = 0
inputs = [1, 2, 3, 4]

node = gp.ConstantFloat(idx, inputs)
assert node.inputs == []

node = gp.node.OutputNode(idx, inputs)
assert node.inputs == inputs[:1]

node = gp.Add(idx, inputs)
assert node.inputs == inputs[:2]

0 comments on commit 91b62d5

Please sign in to comment.