Source code for freneticlib.core.mutation.mutators

import logging
from typing import List

import numpy as np

from freneticlib.representations import abstract_representation
from freneticlib.utils.random import seeded_rng
from . import exploiters

from .abstract_operators import AbstractMutationOperator, AbstractMutator
from ...executors.outcome import Outcome

logger = logging.getLogger(__name__)


[docs]class RemoveFront(AbstractMutationOperator): """ Mutation operator for removing a range of road points from the front of the test Args: remove_at_least (int): Minimum number of road points to remove. remove_at_most (int): Maximum number of road points to remove. min_length_for_operator (int): Minimum number of road points required for application. """ def __init__(self, remove_at_least: int = 1, remove_at_most: int = 5, min_length_for_operator: int = 10): self.remove_at_least = remove_at_least self.remove_at_most = remove_at_most self.min_length_for_operator = min_length_for_operator
[docs] def __call__(self, representation: abstract_representation.RoadRepresentation, test): """ Returns a copy of the road by removing a range of road points from the front of the road. Args: representation (RoadRepresentation): The road representation used in this search. test: The road (in a given representation). Returns: The mutated road. """ assert self.is_applicable(test) return test[seeded_rng().integers(self.remove_at_least, self.remove_at_most) :]
[docs] def is_applicable(self, test) -> bool: """ Check if test can be mutated. Args: test: The road (in a given representation). Returns: (bool): Whether the mutation operator is applicable. """ return len(test) >= self.min_length_for_operator
[docs]class RemoveBack(AbstractMutationOperator): """ Mutation operator for removing a range of road points from the back of the road. Args: remove_at_least (int): Minimum number of road points to remove. remove_at_most (int): Maximum number of road points to remove. min_length_for_operator (int): Minimum number of road points required for application. """ def __init__(self, remove_at_least: int = 1, remove_at_most: int = 5, min_length_for_operator: int = 10): self.remove_at_least = remove_at_least self.remove_at_most = remove_at_most self.min_length_for_operator = min_length_for_operator
[docs] def __call__(self, representation: abstract_representation.RoadRepresentation, test): """ Returns a copy of the road by removing a range of road points from the back of the road. Args: representation (RoadRepresentation): The road representation used in this search. test: The road (in a given representation). Returns: The mutated road. """ assert len(test) >= self.min_length_for_operator return test[: -seeded_rng().integers(self.remove_at_least, self.remove_at_most)]
[docs] def is_applicable(self, test) -> bool: """ Check if test can be mutated. Args: test: The road (in a given representation). Returns: (bool): Whether the mutation operator is applicable. """ return len(test) >= self.min_length_for_operator
[docs]class RemoveRandom(AbstractMutationOperator): """ Mutation operator for removing a random road points of the road. Args: remove_at_least (int): Minimum number of road points to remove. remove_at_most (int): Maximum number of road points to remove. min_length_for_operator (int): Minimum number of road points required for application. """ def __init__(self, remove_at_least: int = 1, remove_at_most: int = 5, min_length_for_operator: int = 10): self.remove_at_least = remove_at_least self.remove_at_most = remove_at_most self.min_length_for_operator = min_length_for_operator
[docs] def __call__(self, representation: abstract_representation.RoadRepresentation, test): """ Returns a copy of the road by removing random road points. Args: representation (RoadRepresentation): The road representation used in this search. test: The road (in a given representation). Returns: The mutated road. """ assert len(test) >= self.min_length_for_operator # number of test to be removed k = seeded_rng().integers(self.remove_at_least, self.remove_at_most) modified_test = test[:] while k > 0 and len(modified_test) > 5: # Randomly remove a kappa i = seeded_rng().integers(len(modified_test)) del modified_test[i] k -= 1 return modified_test
[docs] def is_applicable(self, test) -> bool: """ Check if test can be mutated. Args: test: The road (in a given representation). Returns: (bool): Whether the mutation operator is applicable. """ return len(test) >= self.min_length_for_operator
[docs]class AddBack(AbstractMutationOperator): """ Mutation operator for adding random road points at the end of the road. Args: add_at_least (int): Minimum number of road points to add. add_at_most (int): Maximum number of road points to add. """ def __init__(self, add_at_least: int = 1, add_at_most: int = 5): self.add_at_least = add_at_least self.add_at_most = add_at_most
[docs] def __call__(self, representation: abstract_representation.RoadRepresentation, test): """ Returns a copy of the road with new road points added at the back of the road. Args: representation (RoadRepresentation): The road representation used in this search. test: The road (in a given representation). Returns: The mutated road. """ modified_test = test[:] for i in range(seeded_rng().integers(self.add_at_least, self.add_at_most)): modified_test.append(representation.get_value(modified_test)) return modified_test
[docs]class ReplaceRandom(AbstractMutationOperator): """ Mutation operator for replacing random road points replaced with new ones. Args: replace_at_least (int): Minimum number of road points to add. replace_at_most (int): Maximum number of road points to add. """ def __init__(self, replace_at_least: int = 1, replace_at_most: int = 5): self.replace_at_least = replace_at_least self.replace_at_most = replace_at_most
[docs] def __call__(self, representation: abstract_representation.RoadRepresentation, test): """ Returns a copy of the road with random road points replaced with new ones. Args: representation (RoadRepresentation): The road representation used in this search. test: The road (in a given representation). Returns: The mutated road. """ # Randomly replace road points indices = seeded_rng().choice( len(test), seeded_rng().integers(self.replace_at_least, self.replace_at_most), replace=False ) modified_test = test[:] for i in sorted(indices): modified_test[i] = representation.get_value(modified_test[:i]) return modified_test
[docs]class AlterValues(AbstractMutationOperator): """ Mutation operator for altering random road points by multiplying with a factor. Args: mutation_factor_low (float): Minimum factor to apply to a road point. mutation_factor_high (float): Maximum factor to apply to a road point. mutation_chance (float): The mutation chance for each road point. """ def __init__(self, mutation_factor_low: float = 0.9, mutation_factor_high: float = 1.1, mutation_chance: float = 0.1): self.mutation_factor_low = mutation_factor_low self.mutation_factor_high = mutation_factor_high self.mutation_chance = mutation_chance
[docs] def _alter_once(self, test): mutated = test.copy() # for each element, set a mutation factor factors = seeded_rng().uniform(self.mutation_factor_low, self.mutation_factor_high, size=test.shape) # random values to identify which ones to mutate chances = seeded_rng().uniform(size=test.shape) mask = chances < self.mutation_chance # mutate mutated[mask] = mutated[mask] * factors[mask] return mutated
[docs] def __call__(self, representation: abstract_representation.RoadRepresentation, test): """ Returns a copy of the road with random road points altered by a random factor. Args: representation (RoadRepresentation): The road representation used in this search. test: The road (in a given representation). Returns: The mutated road. """ test = np.array(test) # force convert test to numpy array mutated = test.copy() while (mutated == test).all(): # until we have a mutation mutated = self._alter_once(test) return mutated.tolist()
[docs]class KappaStepAlterValues(AlterValues): """ Value mutation operator for the :class:`.KappaRepresentation`. It alters random road points and the steps by multiplying with a factor. Args: mutation_factor_low (float): Minimum factor to apply to a road point. mutation_factor_high (float): Maximum factor to apply to a road point. mutation_chance (float): The mutation chance for each road point. """
[docs] def __call__(self, representation: abstract_representation.RoadRepresentation, test): """ Returns a copy of the road with random road points altered by a random factor. Args: representation (RoadRepresentation): The road representation used in this search. test: The road (in a given representation). Returns: The mutated road. """ test = np.array(test) # force convert test to numpy array mutated = test.copy() # we need to ignore the last step, because it is unused! all_except_last_mask = np.ones(shape=mutated.shape, dtype=np.bool) all_except_last_mask[-1, 1] = False while (mutated == test)[all_except_last_mask].all(): # until we have a mutation, but not in the place of mask mutated = self._alter_once(test) return mutated.tolist()
[docs]class StandardMutator(AbstractMutator): """Default Mutator, applies all operators approach.""" def __init__(self, mutation_operators: List[AbstractMutationOperator] = None): """ Args: mutation_operators (List[AbstractMutationOperator]): The operators used for mutation. """ self.mutation_operators = mutation_operators or [ # default mutators RemoveFront(), RemoveBack(), RemoveRandom(), AddBack(), ReplaceRandom(), AlterValues(), ] super().__init__(self.mutation_operators)
[docs] def __call__(self, representation: abstract_representation.RoadRepresentation, parent): # check if the parent test drove off the road (Outcome.FAIL) or remained on the road (Outcome.PASS) if parent.test is None: return [] # test_info = self.get_parent_info(parent.name) test_info = {"visited": 0} modified_tests = [] for operator in self.mutation_operators: try: mutated_test = operator(representation, parent.test) if not representation.is_valid(mutated_test): logger.debug(f"Mutation operator {str(operator)} produced an invalid test. Attempting to fix it.") mutated_test = representation.fix(mutated_test) if not representation.is_valid(mutated_test): logger.warning("Couldn't fix the test.") continue modified_tests.append(dict(test=mutated_test, method=str(operator), **test_info)) except Exception: logger.error(f"Error during modification of test {test_info} during function {str(operator)}", exc_info=True) return modified_tests
[docs] def get_all(self): return self.mutation_operators
[docs]class FreneticMutator(AbstractMutator): """The default mutator which implements the Frenetic approach. It distinguishes between exploration and exploitation.""" def __init__(self, mutation_operators: List[AbstractMutationOperator] = None, exploitation_operators: List[AbstractMutationOperator] = None): """ Args: mutation_operators (List[AbstractMutationOperator]): The operators used for mutation. exploitation_operators (List[AbstractMutationOperator]): The operators used for exploration """ self.mutation_operators = mutation_operators or [ # default mutators RemoveFront(), RemoveBack(), RemoveRandom(), AddBack(), ReplaceRandom(), AlterValues(), ] self.mutator = StandardMutator(self.mutation_operators) self.exploitation_operators = exploitation_operators or [ # default exploiters exploiters.ReverseTest(), exploiters.SplitAndSwap(), exploiters.FlipSign(), ] self.exploiter = StandardMutator(self.mutation_operators) super().__init__(self.mutation_operators + self.exploitation_operators)
[docs] def __call__(self, representation: abstract_representation.RoadRepresentation, parent): # check if the parent test drove off the road (Outcome.FAIL) or remained on the road (Outcome.PASS) if self.exploitation_operators and parent["outcome"] == Outcome.FAIL: mutants = self.exploiter(representation, parent) for m in mutants: # set stop reproduction! m["visited"] = 1 return mutants elif self.mutation_operators and parent["outcome"] == Outcome.PASS: return self.mutator(representation, parent)