aegis_sim.submodels.reproduction.mutation

  1import numpy as np
  2from aegis_sim import variables
  3
  4
  5class Mutator:
  6    def init(self, MUTATION_RATIO, MUTATION_METHOD, MUTATION_AGE_MULTIPLIER):
  7        self.MUTATION_RATIO = MUTATION_RATIO
  8        self.MUTATION_METHOD = MUTATION_METHOD
  9        self.MUTATION_AGE_MULTIPLIER = MUTATION_AGE_MULTIPLIER
 10        self.rate_0to1 = MUTATION_RATIO / (1 + MUTATION_RATIO)
 11        self.rate_1to0 = 1 / (1 + MUTATION_RATIO)
 12        # Set mutation method
 13        if self.MUTATION_METHOD == "by_index":
 14            self._mutate = self._mutate_by_index
 15        elif self.MUTATION_METHOD == "by_bit":
 16            self._mutate = self._mutate_by_bit
 17        else:
 18            raise ValueError("MUTATION_METHOD must be 'by_index' or 'by_bit'")
 19
 20    def _mutate_by_bit(self, genomes, muta_prob, ages, random_probabilities=None):
 21        """Induce germline mutations."""
 22
 23        if random_probabilities is None:
 24            random_probabilities = variables.rng.random(genomes.shape)
 25
 26        # Broadcast to fit [individual, chromatid, locus, bit] shape
 27        mutation_probabilities = muta_prob[:, None, None, None]
 28
 29        mutation_probabilities = self.apply_mutation_age_multiplier(
 30            mutation_probabilities=mutation_probabilities,
 31            ages=ages,
 32            MUTATION_AGE_MULTIPLIER=self.MUTATION_AGE_MULTIPLIER,
 33        )
 34        mutate_0to1 = (~genomes) & (
 35            random_probabilities < (mutation_probabilities * self.rate_0to1).astype("float32")
 36        )  # genome == 0 &
 37        mutate_1to0 = genomes & (
 38            random_probabilities < (mutation_probabilities * self.rate_1to0).astype("float32")
 39        )  # genomes == 1 &
 40
 41        genomes[mutate_0to1] = 1
 42        genomes[mutate_1to0] = 0
 43
 44        return genomes
 45
 46    def _mutate_by_index(self, genomes, muta_prob, ages):
 47        """Alternative faster method for introducing mutations.
 48
 49        Instead of generating a random probability for every bit in the array of genomes,
 50        generate random indices of bits that could be mutated."""
 51
 52        if genomes.size == 0:
 53            return genomes
 54
 55        bits_per_genome = genomes[0].size
 56
 57        muta_prob = self.apply_mutation_age_multiplier(
 58            mutation_probabilities=muta_prob,
 59            ages=ages,
 60            MUTATION_AGE_MULTIPLIER=self.MUTATION_AGE_MULTIPLIER,
 61        )
 62
 63        # Calculate number of bits to mutate
 64        n_mutations_per_individual = variables.rng.binomial(n=bits_per_genome, p=muta_prob, size=len(genomes))
 65        n_mutations_total = np.sum(n_mutations_per_individual)
 66
 67        # Generate indices to mutate
 68        mutation_indices = (
 69            np.repeat(np.arange(len(genomes)), n_mutations_per_individual),
 70            variables.rng.integers(genomes.shape[1], size=n_mutations_total),
 71            variables.rng.integers(genomes.shape[2], size=n_mutations_total),
 72            variables.rng.integers(genomes.shape[3], size=n_mutations_total),
 73        )
 74
 75        # Extract indices of 0-bits and 1-bits
 76        bits = genomes[mutation_indices]  # NOTE Use tuple for ndarray indexing
 77        bits0_indices = (~bits).nonzero()[0]
 78        bits1_indices = bits.nonzero()[0]
 79
 80        # Take into consideration the MUTATION_RATIO
 81        bits0_include = variables.rng.random(len(bits0_indices)) < self.rate_0to1
 82        bits1_include = variables.rng.random(len(bits1_indices)) < self.rate_1to0
 83        bits0_indices = bits0_indices[bits0_include]
 84        bits1_indices = bits1_indices[bits1_include]
 85
 86        # Mutate bits at mutation_indices
 87        mutation_indices = np.array(mutation_indices)
 88        genomes[tuple(mutation_indices[:, bits1_indices.T])] = False
 89        genomes[tuple(mutation_indices[:, bits0_indices.T])] = True
 90
 91        return genomes
 92
 93    @staticmethod
 94    def apply_mutation_age_multiplier(mutation_probabilities, ages, MUTATION_AGE_MULTIPLIER):
 95        """
 96        final = initial(1 + age * multiplier)
 97        """
 98        multipliers = ages * MUTATION_AGE_MULTIPLIER
 99        multipliers = multipliers.reshape(mutation_probabilities.shape)
100        return mutation_probabilities * (1 + multipliers)
101
102
103mutator = Mutator()
class Mutator:
  6class Mutator:
  7    def init(self, MUTATION_RATIO, MUTATION_METHOD, MUTATION_AGE_MULTIPLIER):
  8        self.MUTATION_RATIO = MUTATION_RATIO
  9        self.MUTATION_METHOD = MUTATION_METHOD
 10        self.MUTATION_AGE_MULTIPLIER = MUTATION_AGE_MULTIPLIER
 11        self.rate_0to1 = MUTATION_RATIO / (1 + MUTATION_RATIO)
 12        self.rate_1to0 = 1 / (1 + MUTATION_RATIO)
 13        # Set mutation method
 14        if self.MUTATION_METHOD == "by_index":
 15            self._mutate = self._mutate_by_index
 16        elif self.MUTATION_METHOD == "by_bit":
 17            self._mutate = self._mutate_by_bit
 18        else:
 19            raise ValueError("MUTATION_METHOD must be 'by_index' or 'by_bit'")
 20
 21    def _mutate_by_bit(self, genomes, muta_prob, ages, random_probabilities=None):
 22        """Induce germline mutations."""
 23
 24        if random_probabilities is None:
 25            random_probabilities = variables.rng.random(genomes.shape)
 26
 27        # Broadcast to fit [individual, chromatid, locus, bit] shape
 28        mutation_probabilities = muta_prob[:, None, None, None]
 29
 30        mutation_probabilities = self.apply_mutation_age_multiplier(
 31            mutation_probabilities=mutation_probabilities,
 32            ages=ages,
 33            MUTATION_AGE_MULTIPLIER=self.MUTATION_AGE_MULTIPLIER,
 34        )
 35        mutate_0to1 = (~genomes) & (
 36            random_probabilities < (mutation_probabilities * self.rate_0to1).astype("float32")
 37        )  # genome == 0 &
 38        mutate_1to0 = genomes & (
 39            random_probabilities < (mutation_probabilities * self.rate_1to0).astype("float32")
 40        )  # genomes == 1 &
 41
 42        genomes[mutate_0to1] = 1
 43        genomes[mutate_1to0] = 0
 44
 45        return genomes
 46
 47    def _mutate_by_index(self, genomes, muta_prob, ages):
 48        """Alternative faster method for introducing mutations.
 49
 50        Instead of generating a random probability for every bit in the array of genomes,
 51        generate random indices of bits that could be mutated."""
 52
 53        if genomes.size == 0:
 54            return genomes
 55
 56        bits_per_genome = genomes[0].size
 57
 58        muta_prob = self.apply_mutation_age_multiplier(
 59            mutation_probabilities=muta_prob,
 60            ages=ages,
 61            MUTATION_AGE_MULTIPLIER=self.MUTATION_AGE_MULTIPLIER,
 62        )
 63
 64        # Calculate number of bits to mutate
 65        n_mutations_per_individual = variables.rng.binomial(n=bits_per_genome, p=muta_prob, size=len(genomes))
 66        n_mutations_total = np.sum(n_mutations_per_individual)
 67
 68        # Generate indices to mutate
 69        mutation_indices = (
 70            np.repeat(np.arange(len(genomes)), n_mutations_per_individual),
 71            variables.rng.integers(genomes.shape[1], size=n_mutations_total),
 72            variables.rng.integers(genomes.shape[2], size=n_mutations_total),
 73            variables.rng.integers(genomes.shape[3], size=n_mutations_total),
 74        )
 75
 76        # Extract indices of 0-bits and 1-bits
 77        bits = genomes[mutation_indices]  # NOTE Use tuple for ndarray indexing
 78        bits0_indices = (~bits).nonzero()[0]
 79        bits1_indices = bits.nonzero()[0]
 80
 81        # Take into consideration the MUTATION_RATIO
 82        bits0_include = variables.rng.random(len(bits0_indices)) < self.rate_0to1
 83        bits1_include = variables.rng.random(len(bits1_indices)) < self.rate_1to0
 84        bits0_indices = bits0_indices[bits0_include]
 85        bits1_indices = bits1_indices[bits1_include]
 86
 87        # Mutate bits at mutation_indices
 88        mutation_indices = np.array(mutation_indices)
 89        genomes[tuple(mutation_indices[:, bits1_indices.T])] = False
 90        genomes[tuple(mutation_indices[:, bits0_indices.T])] = True
 91
 92        return genomes
 93
 94    @staticmethod
 95    def apply_mutation_age_multiplier(mutation_probabilities, ages, MUTATION_AGE_MULTIPLIER):
 96        """
 97        final = initial(1 + age * multiplier)
 98        """
 99        multipliers = ages * MUTATION_AGE_MULTIPLIER
100        multipliers = multipliers.reshape(mutation_probabilities.shape)
101        return mutation_probabilities * (1 + multipliers)
def init(self, MUTATION_RATIO, MUTATION_METHOD, MUTATION_AGE_MULTIPLIER):
 7    def init(self, MUTATION_RATIO, MUTATION_METHOD, MUTATION_AGE_MULTIPLIER):
 8        self.MUTATION_RATIO = MUTATION_RATIO
 9        self.MUTATION_METHOD = MUTATION_METHOD
10        self.MUTATION_AGE_MULTIPLIER = MUTATION_AGE_MULTIPLIER
11        self.rate_0to1 = MUTATION_RATIO / (1 + MUTATION_RATIO)
12        self.rate_1to0 = 1 / (1 + MUTATION_RATIO)
13        # Set mutation method
14        if self.MUTATION_METHOD == "by_index":
15            self._mutate = self._mutate_by_index
16        elif self.MUTATION_METHOD == "by_bit":
17            self._mutate = self._mutate_by_bit
18        else:
19            raise ValueError("MUTATION_METHOD must be 'by_index' or 'by_bit'")
@staticmethod
def apply_mutation_age_multiplier(mutation_probabilities, ages, MUTATION_AGE_MULTIPLIER):
 94    @staticmethod
 95    def apply_mutation_age_multiplier(mutation_probabilities, ages, MUTATION_AGE_MULTIPLIER):
 96        """
 97        final = initial(1 + age * multiplier)
 98        """
 99        multipliers = ages * MUTATION_AGE_MULTIPLIER
100        multipliers = multipliers.reshape(mutation_probabilities.shape)
101        return mutation_probabilities * (1 + multipliers)

final = initial(1 + age * multiplier)

mutator = <Mutator object>