def variable_onepoint(p_0, p_1): """ Given two individuals, create two children using one-point crossover and return them. A different point is selected on each genome for crossover to occur. Note that this allows for genomes to grow or shrink in size. Crossover points are selected within the used portion of the genome by default (i.e. crossover does not occur in the tail of the individual). :param p_0: Parent 0 :param p_1: Parent 1 :return: A list of crossed-over individuals. """ # Get the chromosomes. genome_0, genome_1 = p_0.genome, p_1.genome # Uniformly generate crossover points. max_p_0, max_p_1 = get_max_genome_index(p_0, p_1) # Select unique points on each genome for crossover to occur. pt_0, pt_1 = randint(1, max_p_0), randint(1, max_p_1) # Make new chromosomes by crossover: these slices perform copies. if random() < params['CROSSOVER_PROBABILITY']: c_0 = genome_0[:pt_0] + genome_1[pt_1:] c_1 = genome_1[:pt_1] + genome_0[pt_0:] else: c_0, c_1 = genome_0[:], genome_1[:] # Put the new chromosomes into new individuals. ind_0 = individual.Individual(c_0, None) ind_1 = individual.Individual(c_1, None) return [ind_0, ind_1]
def fixed_twopoint(p_0, p_1): """ Given two individuals, create two children using two-point crossover and return them. The same points are selected on both genomes for crossover to occur. Crossover points are selected within the used portion of the genome by default (i.e. crossover does not occur in the tail of the individual). :param p_0: Parent 0 :param p_1: Parent 1 :return: A list of crossed-over individuals. """ genome_0, genome_1 = p_0.genome, p_1.genome # Uniformly generate crossover points. max_p_0, max_p_1 = get_max_genome_index(p_0, p_1) # Select the same points on both genomes for crossover to occur. a, b = randint(1, max_p_0), randint(1, max_p_1) pt_0, pt_1 = min([a, b]), max([a, b]) # Make new chromosomes by crossover: these slices perform copies. if random() < params['CROSSOVER_PROBABILITY']: c_0 = genome_0[:pt_0] + genome_1[pt_0:pt_1] + genome_0[pt_1:] c_1 = genome_1[:pt_0] + genome_0[pt_0:pt_1] + genome_1[pt_1:] else: c_0, c_1 = genome_0[:], genome_1[:] # Put the new chromosomes into new individuals. ind_0 = individual.Individual(c_0, None) ind_1 = individual.Individual(c_1, None) return [ind_0, ind_1]
def LTGE_crossover(p_0, p_1): """Crossover in the LTGE representation.""" # crossover and repair. # the LTGE crossover produces one child, and is symmetric (ie # xover(p0, p1) is not different from xover(p1, p0)), but since it's # stochastic we can just run it twice to get two individuals # expected to be different. g_0, ph_0 = latent_tree_repair( latent_tree_crossover(p_0.genome, p_1.genome), params['BNF_GRAMMAR'], params['MAX_TREE_DEPTH']) g_1, ph_1 = latent_tree_repair( latent_tree_crossover(p_0.genome, p_1.genome), params['BNF_GRAMMAR'], params['MAX_TREE_DEPTH']) # wrap up in Individuals and fix up various Individual attributes ind_0 = individual.Individual(g_0, None, False) ind_1 = individual.Individual(g_1, None, False) ind_0.phenotype = ph_0 ind_1.phenotype = ph_1 # number of nodes is the number of decisions in the genome ind_0.nodes = ind_0.used_codons = len(g_0) ind_1.nodes = ind_1.used_codons = len(g_1) # each key is the length of a path from root ind_0.depth = max(len(k) for k in g_0) ind_1.depth = max(len(k) for k in g_1) # in LTGE there are no invalid individuals ind_0.invalid = False ind_1.invalid = False return [ind_0, ind_1]
def int_flip_per_ind(ind): """ Mutate the genome of an individual by randomly choosing a new int with probability p_mut. Works per-individual. Mutation is performed over the entire length of the genome by default, but the flag within_used is provided to limit mutation to only the effective length of the genome. :param ind: An individual to be mutated. :return: A mutated individual. """ # Set effective genome length over which mutation will be performed. eff_length = get_effective_length(ind) if not eff_length: # Linear mutation cannot be performed on this individual. return ind for _ in range(params['MUTATION_EVENTS']): idx = randint(0, eff_length - 1) ind.genome[idx] = randint(0, params['CODON_SIZE']) # Re-build a new individual with the newly mutated genetic information. new_ind = individual.Individual(ind.genome, None) return new_ind
def check_ind_from_parser(ind, target): """ Checks the mapping of an individual generated by the GE parser against the specified target string to ensure the GE individual is correct. :param ind: An instance of the representation.individaul.Individual class. :param target: A target string against which to match the phenotype of the individual. :return: Nothing. """ # Re-map individual using genome mapper to check everything is ok. new_ind = individual.Individual(ind.genome, None) # Check phenotypes are the same. if new_ind.phenotype != ind.phenotype: s = "utilities.representation.check_methods.check_ind_from_parser\n" \ "Error: Solution phenotype doesn't match genome mapping.\n" \ " Solution phenotype: \t %s\n" \ " Solution from genome:\t %s\n" \ " Derived genome: \t %s" % \ (ind.phenotype, new_ind.phenotype, ind.genome) raise Exception(s) # Check the phenotype matches the target string. elif ind.phenotype != target: s = "utilities.representation.check_methods.check_ind_from_parser\n" \ "Error: Solution phenotype doesn't match target.\n" \ " Target: \t %s\n" \ " Solution: \t %s" % (target, ind.phenotype) raise Exception(s) else: # Check the tree matches the phenotype. check_genome_mapping(ind)
def LTGE_initialisation(size): """Initialise a population in the LTGE representation.""" pop = [] for _ in range(size): # Random genotype g, ph = latent_tree_random_ind(params['BNF_GRAMMAR'], params['MAX_TREE_DEPTH']) # wrap up in an Individual and fix up various Individual attributes ind = individual.Individual(g, None, False) ind.phenotype = ph # number of nodes is the number of decisions in the genome ind.nodes = ind.used_codons = len(g) # each key is the length of a path from root ind.depth = max(len(k) for k in g) # in LTGE there are no invalid individuals ind.invalid = False pop.append(ind) return pop
def subtree(ind): """ Mutate the individual by replacing a randomly selected subtree with a new randomly generated subtree. Guaranteed one event per individual, unless params['MUTATION_EVENTS'] is specified as a higher number. :param ind: An individual to be mutated. :return: A mutated individual. """ def subtree_mutate(ind_tree): """ Creates a list of all nodes and picks one node at random to mutate. Because we have a list of all nodes, we can (but currently don't) choose what kind of nodes to mutate on. Handy. :param ind_tree: The full tree of an individual. :return: The full mutated tree and the associated genome. """ # Find the list of nodes we can mutate from. targets = ind_tree.get_target_nodes( [], target=params['BNF_GRAMMAR'].non_terminals) # Pick a node. new_tree = choice(targets) # Set the depth limits for the new subtree. if params['MAX_TREE_DEPTH']: # Set the limit to the tree depth. max_depth = params['MAX_TREE_DEPTH'] - new_tree.depth else: # There is no limit to tree depth. max_depth = None # Mutate a new subtree. generate_tree(new_tree, [], [], "random", 0, 0, 0, max_depth) return ind_tree if ind.invalid: # The individual is invalid. tail = [] else: # Save the tail of the genome. tail = ind.genome[ind.used_codons:] # Allows for multiple mutation events should that be desired. for i in range(params['MUTATION_EVENTS']): ind.tree = subtree_mutate(ind.tree) # Re-build a new individual with the newly mutated genetic information. ind = individual.Individual(None, ind.tree) # Add in the previous tail. ind.genome = ind.genome + tail return ind
def uniform_genome(size): """ Create a population of individuals by sampling genomes uniformly. :param size: The size of the required population. :return: A full population composed of randomly generated individuals. """ return [individual.Individual(sample_genome(), None) for _ in range(size)]
def check_snippets_for_solution(): """ Check the snippets repository to see if we have built up the correct solution yet. :return: An individual representing the correct solution if it exists, otherwise None. """ # Initialise None biggest snippet biggest_snippet = [0, None] for snippet in sorted(trackers.snippets.keys()): # Check each snippet to find the largest one. # Find length of snippet index = get_num_from_str(snippet) length = index[1] - index[0] if length > biggest_snippet[0]: # We have a new biggest snippet. biggest_snippet = [length, snippet] # Get the phenotype of the largest snippet largest_snippet = get_output(trackers.snippets[biggest_snippet[1]]) if largest_snippet != params['REVERSE_MAPPING_TARGET']: # The solution doesn't match the target string. # Get the location of the phenotype of the largest snippet on the # target string. largest_indexes = get_num_from_str(biggest_snippet[1]) # Generate whitespace to position the phenotype accordingly. spaces = "".join([" " for _ in range(largest_indexes[0] - 1)]) s = "operators.subtree_parse.check_snippets_for_solution\n" \ "Error: Solution doesn't match the target string.\n" \ " Target: \t %s\n" \ " Solution: \t %s %s\n" \ " Check grammar file `%s` to ensure the grammar is capable" \ " of producing the exact target string." % \ (params['REVERSE_MAPPING_TARGET'], spaces, largest_snippet, params['GRAMMAR_FILE']) raise Exception(s) if largest_snippet == params['REVERSE_MAPPING_TARGET']: # We have a perfect match # Generate individual that represents the perfect solution. ind = individual.Individual(None, trackers.snippets[biggest_snippet[1]]) # Return ind. return ind
def check_genome_mapping(ind): """ Re-maps individual to ensure genome is correct, i.e. that it maps to the correct phenotype and individual. :param ind: An instance of the representation.individual.Individual class. :return: Nothing. """ # Re-map individual using fast genome mapper to check everything is ok new_ind = individual.Individual(ind.genome, None) # Get attributes of both individuals. attributes_0 = vars(ind) attributes_1 = vars(new_ind) if params['GENOME_OPERATIONS']: # If this parameter is set then the new individual will have no tree. attributes_0['tree'] = None else: if attributes_0['tree'] != attributes_1['tree']: s = "utilities.representation.check_methods.check_ind.\n" \ "Error: Individual trees do not match." raise Exception(s) # Check that all attributes match across both individuals. for a_0 in sorted(attributes_0.keys()): for a_1 in sorted(attributes_1.keys()): if a_0 == a_1 and attributes_0[a_0] != attributes_1[a_1] and not \ (type(attributes_0[a_0]) is float and type(attributes_1[a_1]) is float and np.isnan(attributes_0[a_0]) and np.isnan(attributes_1[a_1])): s = "utilities.representation.check_methods." \ "check_genome_mapping\n" \ "Error: Individual attributes do not match genome-" \ "encoded attributes.\n" \ " Original attribute:\n" \ " %s :\t %s\n" \ " Encoded attribute:\n" \ " %s :\t %s" % \ (a_0, attributes_0[a_0], a_1, attributes_1[a_1]) raise Exception(s)
def int_flip_per_codon(ind): """ Mutate the genome of an individual by randomly choosing a new int with probability p_mut. Works per-codon. Mutation is performed over the effective length (i.e. within used codons, not tails) by default; within_used=False switches this off. :param ind: An individual to be mutated. :return: A mutated individual. """ # Set effective genome length over which mutation will be performed. eff_length = get_effective_length(ind) if not eff_length: # Linear mutation cannot be performed on this individual. return ind # Set mutation probability. Default is 1 over the length of the genome. if params['MUTATION_PROBABILITY'] and params['MUTATION_EVENTS'] == 1: p_mut = params['MUTATION_PROBABILITY'] elif params['MUTATION_PROBABILITY'] and params['MUTATION_EVENTS'] > 1: s = "operators.mutation.int_flip_per_codon\n" \ "Error: mutually exclusive parameters for 'MUTATION_PROBABILITY'" \ "and 'MUTATION_EVENTS' have been explicitly set.\n" \ " Only one of these parameters can be used at a time with" \ "int_flip_per_codon mutation." raise Exception(s) else: # Default mutation events per individual is 1. Raising this number # will influence the mutation probability for each codon. p_mut = params['MUTATION_EVENTS'] / eff_length # Mutation probability works per-codon over the portion of the # genome as defined by the within_used flag. for i in range(eff_length): if random() < p_mut: ind.genome[i] = randint(0, params['CODON_SIZE']) # Re-build a new individual with the newly mutated genetic information. new_ind = individual.Individual(ind.genome, None) return new_ind
def LTGE_mutation(ind): """Mutation in the LTGE representation.""" # mutate and repair. g, ph = latent_tree_repair(latent_tree_mutate(ind.genome), params['BNF_GRAMMAR'], params['MAX_TREE_DEPTH']) # wrap up in an Individual and fix up various Individual attributes ind = individual.Individual(g, None, False) ind.phenotype = ph # number of nodes is the number of decisions in the genome ind.nodes = ind.used_codons = len(g) # each key is the length of a path from root ind.depth = max(len(k) for k in g) # in LTGE there are no invalid individuals ind.invalid = False return ind
def generate_ind_tree(max_depth, method): """ Generate an individual using a given subtree initialisation method. :param max_depth: The maximum depth for the initialised subtree. :param method: The method of subtree initialisation required. :return: A fully built individual. """ # Initialise an instance of the tree class ind_tree = Tree(str(params['BNF_GRAMMAR'].start_rule["symbol"]), None) # Generate a tree genome, output, nodes, _, depth = generate_tree(ind_tree, [], [], method, 0, 0, 0, max_depth) # Get remaining individual information phenotype, invalid, used_cod = "".join(output), False, len(genome) if params['BNF_GRAMMAR'].python_mode: # Grammar contains python code phenotype = python_filter(phenotype) # Initialise individual ind = individual.Individual(genome, ind_tree, map_ind=False) # Set individual parameters ind.phenotype, ind.nodes = phenotype, nodes ind.depth, ind.used_codons, ind.invalid = depth, used_cod, invalid # Generate random tail for genome. ind.genome = genome + [ randint(0, params['CODON_SIZE']) for _ in range(int(ind.used_codons / 2)) ] return ind
def subtree(p_0, p_1): """ Given two individuals, create two children using subtree crossover and return them. Candidate subtrees are selected based on matching non-terminal nodes rather than matching terminal nodes. :param p_0: Parent 0. :param p_1: Parent 1. :return: A list of crossed-over individuals. """ def do_crossover(tree0, tree1, shared_nodes): """ Given two instances of the representation.tree.Tree class ( derivation trees of individuals) and a list of intersecting non-terminal nodes across both trees, performs subtree crossover on these trees. :param tree0: The derivation tree of individual 0. :param tree1: The derivation tree of individual 1. :param shared_nodes: The sorted list of all non-terminal nodes that are in both derivation trees. :return: The new derivation trees after subtree crossover has been performed. """ # Randomly choose a non-terminal from the set of permissible # intersecting non-terminals. crossover_choice = choice(shared_nodes) # Find all nodes in both trees that match the chosen crossover node. nodes_0 = tree0.get_target_nodes([], target=[crossover_choice]) nodes_1 = tree1.get_target_nodes([], target=[crossover_choice]) # Randomly pick a node. t0, t1 = choice(nodes_0), choice(nodes_1) # Check the parents of both chosen subtrees. p0 = t0.parent p1 = t1.parent if not p0 and not p1: # Crossover is between the entire tree of both tree0 and tree1. return t1, t0 elif not p0: # Only t0 is the entire of tree0. tree0 = t1 # Swap over the subtrees between parents. i1 = p1.children.index(t1) p1.children[i1] = t0 # Set the parents of the crossed-over subtrees as their new # parents. Since the entire tree of t1 is now a whole # individual, it has no parent. t0.parent = p1 t1.parent = None elif not p1: # Only t1 is the entire of tree1. tree1 = t0 # Swap over the subtrees between parents. i0 = p0.children.index(t0) p0.children[i0] = t1 # Set the parents of the crossed-over subtrees as their new # parents. Since the entire tree of t0 is now a whole # individual, it has no parent. t1.parent = p0 t0.parent = None else: # The crossover node for both trees is not the entire tree. # For the parent nodes of the original subtrees, get the indexes # of the original subtrees. i0 = p0.children.index(t0) i1 = p1.children.index(t1) # Swap over the subtrees between parents. p0.children[i0] = t1 p1.children[i1] = t0 # Set the parents of the crossed-over subtrees as their new # parents. t1.parent = p0 t0.parent = p1 return tree0, tree1 def intersect(l0, l1): """ Returns the intersection of two sets of labels of nodes of derivation trees. Only returns matching non-terminal nodes across both derivation trees. :param l0: The labels of all nodes of tree 0. :param l1: The labels of all nodes of tree 1. :return: The sorted list of all non-terminal nodes that are in both derivation trees. """ # Find all intersecting elements of both sets l0 and l1. shared_nodes = l0.intersection(l1) # Find only the non-terminals present in the intersecting set of # labels. shared_nodes = [ i for i in shared_nodes if i in params['BNF_GRAMMAR'].non_terminals ] return sorted(shared_nodes) if random() > params['CROSSOVER_PROBABILITY']: # Crossover is not to be performed, return entire individuals. ind0 = p_1 ind1 = p_0 else: # Crossover is to be performed. if p_0.invalid: # The individual is invalid. tail_0 = [] else: # Save tail of each genome. tail_0 = p_0.genome[p_0.used_codons:] if p_1.invalid: # The individual is invalid. tail_1 = [] else: # Save tail of each genome. tail_1 = p_1.genome[p_1.used_codons:] # Get the set of labels of non terminals for each tree. labels1 = p_0.tree.get_node_labels(set()) labels2 = p_1.tree.get_node_labels(set()) # Find overlapping non-terminals across both trees. shared_nodes = intersect(labels1, labels2) if len(shared_nodes) != 0: # There are overlapping NTs, cross over parts of trees. ret_tree0, ret_tree1 = do_crossover(p_0.tree, p_1.tree, shared_nodes) else: # There are no overlapping NTs, cross over entire trees. ret_tree0, ret_tree1 = p_1.tree, p_0.tree # Initialise new individuals using the new trees. ind0 = individual.Individual(None, ret_tree0) ind1 = individual.Individual(None, ret_tree1) # Preserve tails. ind0.genome = ind0.genome + tail_0 ind1.genome = ind1.genome + tail_1 return [ind0, ind1]
def PI_grow(size): """ Create a population of size using Position Independent Grow and return. :param size: The size of the required population. :return: A full population of individuals. """ # Calculate the range of depths to ramp individuals from. depths = range(params['BNF_GRAMMAR'].min_ramp + 1, params['MAX_INIT_TREE_DEPTH'] + 1) population = [] if size < 2: # If the population size is too small, can't use PI Grow # initialisation. print("Error: population size too small for PI Grow initialisation.") print("Returning randomly built trees.") return [ individual.Individual(sample_genome(), None) for _ in range(size) ] elif not depths: # If we have no depths to ramp from, then params['MAX_INIT_DEPTH'] is # set too low for the specified grammar. s = "operators.initialisation.PI_grow\n" \ "Error: Maximum initialisation depth too low for specified " \ "grammar." raise Exception(s) else: if size < len(depths): # The population size is too small to fully cover all ramping # depths. Only ramp to the number of depths we can reach. depths = depths[:int(size)] # Calculate how many individuals are to be generated by each # initialisation method. times = int(floor(size / len(depths))) remainder = int(size - (times * len(depths))) # Iterate over depths. for depth in depths: # Iterate over number of required individuals per depth. for i in range(times): # Generate individual using "Grow" ind = generate_PI_ind_tree(depth) # Append individual to population population.append(ind) if remainder: # The full "size" individuals were not generated. The population # will be completed with individuals of random depths. depths = list(depths) shuffle(depths) for i in range(remainder): depth = depths.pop() # Generate individual using "Grow" ind = generate_PI_ind_tree(depth) # Append individual to population population.append(ind) return population