aegis_sim.submodels.genetics.composite.interpreter

Interpreter of genomic loci

Transforms bool array into an array of numbers. These numbers can be loosely understood as gene activity.

  1"""Interpreter of genomic loci
  2
  3Transforms bool array into an array of numbers.
  4These numbers can be loosely understood as gene activity.
  5"""
  6
  7import numpy as np
  8from aegis_sim import variables
  9
 10
 11class Interpreter:
 12    def __init__(self, BITS_PER_LOCUS, THRESHOLD):
 13
 14        self.BITS_PER_LOCUS = BITS_PER_LOCUS
 15        self.THRESHOLD = THRESHOLD
 16
 17        self.exp_base = 0.5  # Important for _exp
 18        self.binary_exp_base = 0.98  # Important for _binary_exp
 19
 20        # Parameters for the binary interpreter
 21        binary_weights = 2 ** np.arange(self.BITS_PER_LOCUS)[::-1]
 22        self.binary_weights = binary_weights / binary_weights.sum()
 23
 24        # Parameters for the binary switch interpreter
 25        binary_switch_weights = 2 ** np.arange(self.BITS_PER_LOCUS)[::-1]
 26        binary_switch_weights[-1] = 0  # Switch bit does not add to locus value
 27        with np.errstate(invalid="ignore"):
 28            self.binary_switch_weights = binary_switch_weights / binary_switch_weights.sum()
 29        # When BITS_PER_LOCUS is 1, all weights are 0 — that's fine, _binary_switch
 30        # will produce zeros for all "on" loci.
 31
 32        # Parameters for the linear interpreter
 33        linear_weights = np.arange(self.BITS_PER_LOCUS)[::-1] + 1
 34        self.linear_weights = linear_weights / linear_weights.sum()
 35
 36    def call(self, loci, interpreter_kind):
 37        """Exposed method"""
 38
 39        # shape is (n_individuals, ?, bits_per_locus)
 40        assert loci.shape[0] > 0, f"loci.shape[0] is {loci.shape[0]}"
 41        assert loci.shape[2] == self.BITS_PER_LOCUS
 42
 43        method = {
 44            "const1": self._const1,
 45            "single_bit": self._single_bit,
 46            "threshold": self._threshold,
 47            "linear": self._linear,
 48            "binary": self._binary,
 49            "switch": self._switch,
 50            "binary_switch": self._binary_switch,
 51            "uniform": self._uniform,
 52            "exp": self._exp,
 53            "binary_exp": self._binary_exp,
 54        }[interpreter_kind]
 55        interpretome = method(loci)
 56        return interpretome
 57
 58    @staticmethod
 59    def _const1(loci):
 60        return np.ones((len(loci), 1))
 61
 62    @staticmethod
 63    def _single_bit(loci):
 64        return loci[:, :, 0]
 65
 66    def _threshold(self, loci):
 67        """Penna interpreter
 68        Cares only about the first bit of the locus.
 69        """
 70        return (~loci[:, :, 0]).cumsum(-1) < self.THRESHOLD
 71
 72    def _linear(self, loci):
 73        return np.matmul(loci, self.linear_weights)
 74
 75    def _binary(self, loci):
 76        """Interpret locus as a binary number and normalize.
 77
 78        High resolution (can produce 2^bits_per_locus different numbers).
 79        Position-dependent.
 80        """
 81
 82        return np.matmul(loci, self.binary_weights)
 83
 84    @staticmethod
 85    def _switch(loci):
 86        """Return 0 if all bits are 0; 1 if all bits are 1; 0 or 1 randomly otherwise.
 87
 88        Low resolution (can produce 2 different numbers).
 89        Position-independent.
 90        """
 91        sums = loci.mean(2)
 92        rand_values = variables.rng.random(loci.shape[:-1]) < 0.5
 93        return np.select([sums == 0, (sums > 0) & (sums < 1), sums == 1], [0, rand_values, 1])
 94
 95    def _binary_switch(self, loci):
 96        """Interpret first n-1 bits as a binary number if the last bit is 1.
 97
 98        High resolution (can produce 2^(bits_per_locus-1) different numbers).
 99        Position-dependent.
100        """
101        where_on = loci[:, :, -1] == 1  # Loci which are turned on
102        values = np.zeros(loci.shape[:-1], dtype=np.float32)  # Initialize output array with zeros
103        values[where_on] = loci[where_on].dot(
104            self.binary_switch_weights
105        )  # If the locus is turned on, make the value in the output array be the binary value
106        return values
107
108    def _uniform(self, loci):
109        """Return normalized sum of bits.
110
111        Medium resolution (can produce bits_per_locus+1 different numbers).
112        Position-independent.
113        """
114        return loci.sum(-1) / loci.shape[-1]
115
116    def _exp(self, loci):
117        """Return base^total_number_of_zeros.
118
119        Medium resolution (can produce bits_per_locus+1 different numbers).
120        Suitable for generating very small numbers.
121        Position-independent.
122        """
123        return self.exp_base ** np.sum(1 - loci, axis=2)
124
125    def _binary_exp(self, loci):
126        """Return base^binary_value_of_locus
127
128        High resolution (can produce 2^bits_per_locus different numbers).
129        Suitable for generating very small numbers.
130        Position-dependent.
131        """
132        binary = self._binary(loci)
133        return self.binary_exp_base**binary
class Interpreter:
 12class Interpreter:
 13    def __init__(self, BITS_PER_LOCUS, THRESHOLD):
 14
 15        self.BITS_PER_LOCUS = BITS_PER_LOCUS
 16        self.THRESHOLD = THRESHOLD
 17
 18        self.exp_base = 0.5  # Important for _exp
 19        self.binary_exp_base = 0.98  # Important for _binary_exp
 20
 21        # Parameters for the binary interpreter
 22        binary_weights = 2 ** np.arange(self.BITS_PER_LOCUS)[::-1]
 23        self.binary_weights = binary_weights / binary_weights.sum()
 24
 25        # Parameters for the binary switch interpreter
 26        binary_switch_weights = 2 ** np.arange(self.BITS_PER_LOCUS)[::-1]
 27        binary_switch_weights[-1] = 0  # Switch bit does not add to locus value
 28        with np.errstate(invalid="ignore"):
 29            self.binary_switch_weights = binary_switch_weights / binary_switch_weights.sum()
 30        # When BITS_PER_LOCUS is 1, all weights are 0 — that's fine, _binary_switch
 31        # will produce zeros for all "on" loci.
 32
 33        # Parameters for the linear interpreter
 34        linear_weights = np.arange(self.BITS_PER_LOCUS)[::-1] + 1
 35        self.linear_weights = linear_weights / linear_weights.sum()
 36
 37    def call(self, loci, interpreter_kind):
 38        """Exposed method"""
 39
 40        # shape is (n_individuals, ?, bits_per_locus)
 41        assert loci.shape[0] > 0, f"loci.shape[0] is {loci.shape[0]}"
 42        assert loci.shape[2] == self.BITS_PER_LOCUS
 43
 44        method = {
 45            "const1": self._const1,
 46            "single_bit": self._single_bit,
 47            "threshold": self._threshold,
 48            "linear": self._linear,
 49            "binary": self._binary,
 50            "switch": self._switch,
 51            "binary_switch": self._binary_switch,
 52            "uniform": self._uniform,
 53            "exp": self._exp,
 54            "binary_exp": self._binary_exp,
 55        }[interpreter_kind]
 56        interpretome = method(loci)
 57        return interpretome
 58
 59    @staticmethod
 60    def _const1(loci):
 61        return np.ones((len(loci), 1))
 62
 63    @staticmethod
 64    def _single_bit(loci):
 65        return loci[:, :, 0]
 66
 67    def _threshold(self, loci):
 68        """Penna interpreter
 69        Cares only about the first bit of the locus.
 70        """
 71        return (~loci[:, :, 0]).cumsum(-1) < self.THRESHOLD
 72
 73    def _linear(self, loci):
 74        return np.matmul(loci, self.linear_weights)
 75
 76    def _binary(self, loci):
 77        """Interpret locus as a binary number and normalize.
 78
 79        High resolution (can produce 2^bits_per_locus different numbers).
 80        Position-dependent.
 81        """
 82
 83        return np.matmul(loci, self.binary_weights)
 84
 85    @staticmethod
 86    def _switch(loci):
 87        """Return 0 if all bits are 0; 1 if all bits are 1; 0 or 1 randomly otherwise.
 88
 89        Low resolution (can produce 2 different numbers).
 90        Position-independent.
 91        """
 92        sums = loci.mean(2)
 93        rand_values = variables.rng.random(loci.shape[:-1]) < 0.5
 94        return np.select([sums == 0, (sums > 0) & (sums < 1), sums == 1], [0, rand_values, 1])
 95
 96    def _binary_switch(self, loci):
 97        """Interpret first n-1 bits as a binary number if the last bit is 1.
 98
 99        High resolution (can produce 2^(bits_per_locus-1) different numbers).
100        Position-dependent.
101        """
102        where_on = loci[:, :, -1] == 1  # Loci which are turned on
103        values = np.zeros(loci.shape[:-1], dtype=np.float32)  # Initialize output array with zeros
104        values[where_on] = loci[where_on].dot(
105            self.binary_switch_weights
106        )  # If the locus is turned on, make the value in the output array be the binary value
107        return values
108
109    def _uniform(self, loci):
110        """Return normalized sum of bits.
111
112        Medium resolution (can produce bits_per_locus+1 different numbers).
113        Position-independent.
114        """
115        return loci.sum(-1) / loci.shape[-1]
116
117    def _exp(self, loci):
118        """Return base^total_number_of_zeros.
119
120        Medium resolution (can produce bits_per_locus+1 different numbers).
121        Suitable for generating very small numbers.
122        Position-independent.
123        """
124        return self.exp_base ** np.sum(1 - loci, axis=2)
125
126    def _binary_exp(self, loci):
127        """Return base^binary_value_of_locus
128
129        High resolution (can produce 2^bits_per_locus different numbers).
130        Suitable for generating very small numbers.
131        Position-dependent.
132        """
133        binary = self._binary(loci)
134        return self.binary_exp_base**binary
Interpreter(BITS_PER_LOCUS, THRESHOLD)
13    def __init__(self, BITS_PER_LOCUS, THRESHOLD):
14
15        self.BITS_PER_LOCUS = BITS_PER_LOCUS
16        self.THRESHOLD = THRESHOLD
17
18        self.exp_base = 0.5  # Important for _exp
19        self.binary_exp_base = 0.98  # Important for _binary_exp
20
21        # Parameters for the binary interpreter
22        binary_weights = 2 ** np.arange(self.BITS_PER_LOCUS)[::-1]
23        self.binary_weights = binary_weights / binary_weights.sum()
24
25        # Parameters for the binary switch interpreter
26        binary_switch_weights = 2 ** np.arange(self.BITS_PER_LOCUS)[::-1]
27        binary_switch_weights[-1] = 0  # Switch bit does not add to locus value
28        with np.errstate(invalid="ignore"):
29            self.binary_switch_weights = binary_switch_weights / binary_switch_weights.sum()
30        # When BITS_PER_LOCUS is 1, all weights are 0 — that's fine, _binary_switch
31        # will produce zeros for all "on" loci.
32
33        # Parameters for the linear interpreter
34        linear_weights = np.arange(self.BITS_PER_LOCUS)[::-1] + 1
35        self.linear_weights = linear_weights / linear_weights.sum()
BITS_PER_LOCUS
THRESHOLD
exp_base
binary_exp_base
binary_weights
linear_weights
def call(self, loci, interpreter_kind):
37    def call(self, loci, interpreter_kind):
38        """Exposed method"""
39
40        # shape is (n_individuals, ?, bits_per_locus)
41        assert loci.shape[0] > 0, f"loci.shape[0] is {loci.shape[0]}"
42        assert loci.shape[2] == self.BITS_PER_LOCUS
43
44        method = {
45            "const1": self._const1,
46            "single_bit": self._single_bit,
47            "threshold": self._threshold,
48            "linear": self._linear,
49            "binary": self._binary,
50            "switch": self._switch,
51            "binary_switch": self._binary_switch,
52            "uniform": self._uniform,
53            "exp": self._exp,
54            "binary_exp": self._binary_exp,
55        }[interpreter_kind]
56        interpretome = method(loci)
57        return interpretome

Exposed method