aegis_sim.submodels.genetics.composite.architecture

  1import numpy as np
  2from aegis_sim import constants
  3from aegis_sim import variables
  4
  5from aegis_sim.submodels.genetics.composite.interpreter import Interpreter
  6from aegis_sim import parameterization
  7from aegis_sim.parameterization import parametermanager
  8from aegis_sim.submodels.genetics import ploider
  9
 10
 11class CompositeArchitecture:
 12    """
 13
 14    GUI
 15    - when pleiotropy is not needed;
 16    - it is quick, easy to analyze, delivers a diversity of phenotypes
 17    - every trait (surv repr muta neut) can be evolvable or not
 18    - if not evolvable, the value is set by !!!
 19    - if evolvable, it can be agespecific or age-independent
 20    - probability of a trait at each age is determined by a BITS_PER_LOCUS adjacent bits forming a "locus" / gene
 21    - the method by which these loci are converted into a phenotypic value is the Interpreter type
 22
 23    """
 24
 25    def __init__(self, BITS_PER_LOCUS, AGE_LIMIT, THRESHOLD):
 26        self.BITS_PER_LOCUS = BITS_PER_LOCUS
 27        self.n_loci = sum(trait.length for trait in parameterization.traits.values())
 28        self.length = self.n_loci * BITS_PER_LOCUS
 29        self.AGE_LIMIT = AGE_LIMIT
 30
 31        self.evolvable = [trait for trait in parameterization.traits.values() if trait.evolvable]
 32
 33        self.interpreter = Interpreter(
 34            self.BITS_PER_LOCUS,
 35            THRESHOLD,
 36        )
 37
 38        # Fixed seed=0 so all populations (including hybridizing ones with different RANDOM_SEEDs)
 39        # share an identical physical genome layout; locus_permutation[i] = physical position of logical locus i
 40        self.locus_permutation = np.random.default_rng(0).permutation(self.n_loci)
 41
 42        # Per-locus dominance coefficient h, indexed by *physical* locus position
 43        # (because diploid_to_haploid operates on the physical layout, before reorder).
 44        # Built from each trait's G_<trait>_dominance value (default 0.5 = codominant).
 45        self.dominance_per_locus = np.full(self.n_loci, 0.5, dtype=np.float32)
 46        for trait in parameterization.traits.values():
 47            if trait.length == 0:
 48                continue
 49            phys_pos = self.locus_permutation[trait.start:trait.end]
 50            self.dominance_per_locus[phys_pos] = np.float32(trait.dominance)
 51
 52        # Optional pleiotropy. None unless PHENOMAP_SPECS is given.
 53        self.phenomap_matrix = self._build_phenomap(parametermanager.parameters.PHENOMAP_SPECS)
 54
 55    def _build_phenomap(self, PHENOMAP_SPECS):
 56        """Build a genotype-phenotype matrix from PHENOMAP_SPECS (aegis v1 semantics).
 57
 58        The matrix is the identity plus off-diagonal weights: the diagonal means every
 59        locus keeps encoding its own trait at its own age (so age-specific survival is
 60        preserved), and each spec adds a *pleiotropic* effect of one locus on another
 61        trait/age on top. This is what makes antagonistic pleiotropy expressible --
 62        one locus raising surv at an early age and lowering it at a late one.
 63
 64        Each spec is ``[source_trait, source_index, target_trait, target_age, weight]``
 65        with ``source_index`` (1..trait length) and ``target_age`` (1..AGE_LIMIT) 1-based,
 66        as exported by v1. Indices are resolved in *logical* (trait x age) order, which is
 67        the order compute() reorders genomes into.
 68
 69        Returns None when no specs are given, in which case compute() is unchanged.
 70        """
 71        if not PHENOMAP_SPECS:
 72            return None
 73
 74        map_ = np.diag(np.ones(self.n_loci, dtype=np.float32))
 75        for spec in PHENOMAP_SPECS:
 76            source_trait, source_index, target_trait, target_age, weight = spec
 77            source = parameterization.traits[source_trait]
 78            target = parameterization.traits[target_trait]
 79            geno_i = source.start + (int(source_index) - 1)
 80            pheno_i = target.start + (int(target_age) - 1)
 81            assert source.start <= geno_i < source.end, (
 82                f"PHENOMAP_SPECS source index {source_index} is out of range for trait "
 83                f"'{source_trait}' which has {source.length} loci; "
 84                f"set G_{source_trait}_agespecific to at least {source_index}"
 85            )
 86            assert target.start <= pheno_i < target.end, (
 87                f"PHENOMAP_SPECS target age {target_age} is out of range for trait "
 88                f"'{target_trait}' which has {target.length} loci"
 89            )
 90            map_[geno_i, pheno_i] = np.float32(weight)
 91        return map_
 92
 93    def get_number_of_bits(self):
 94        return ploider.ploider.y * self.n_loci * self.BITS_PER_LOCUS
 95
 96    def get_shape(self):
 97        return (ploider.ploider.y, self.n_loci, self.BITS_PER_LOCUS)
 98
 99    def init_genome_array(self, popsize):
100        # TODO enable agespecific False
101        array = variables.rng.random(size=(popsize, *self.get_shape()))
102
103        for trait in parameterization.traits.values():
104            phys_pos = self.locus_permutation[trait.start:trait.end]
105            array[:, :, phys_pos, :] = array[:, :, phys_pos, :] < trait.initgeno
106
107        return array
108
109    def compute(self, genomes):
110
111        if genomes.shape[1] == 1:  # Do not calculate mean if genomes are haploid
112            genomes = genomes[:, 0]
113        else:
114            genomes = ploider.ploider.diploid_to_haploid(genomes, dominance_per_locus=self.dominance_per_locus)
115
116        # Reorder from physical storage order to logical (trait×age) order
117        genomes = genomes[:, self.locus_permutation, :]
118
119        interpretome = np.zeros(shape=(genomes.shape[0], genomes.shape[1]), dtype=np.float32)
120        for trait in parameterization.traits.values():
121            loci = genomes[:, trait.slice]  # fetch
122            probs = self.interpreter.call(loci, trait.interpreter)  # interpret
123            interpretome[:, trait.slice] += probs  # add back
124
125        # Apply pleiotropy, if any, to the raw [0, 1] interpreter output -- before the
126        # lo/hi mapping below, so that spec weights are expressed in interpreter units
127        # (aegis v1 order: interpret -> phenomap -> lo/hi).
128        if self.phenomap_matrix is not None:
129            interpretome = np.clip(interpretome.dot(self.phenomap_matrix), 0, 1).astype(np.float32)
130
131        # Map the [0, 1] interpreter output onto the trait's [lo, hi] phenotypic range.
132        # This is what makes G_<trait>_lo / G_<trait>_hi do anything — without this scaling
133        # the lo/hi parameters are parsed but discarded. Defaults: G_surv_lo=0.7, G_surv_hi=1.0
134        # (surv never goes to 0); G_repr_lo=0, G_repr_hi=0.5; others lo=0, hi=1.
135        for trait in parameterization.traits.values():
136            interpretome[:, trait.slice] = trait.lo + (trait.hi - trait.lo) * interpretome[:, trait.slice]
137
138        return interpretome
139
140    # def diffuse(self, probs):
141    #     window_size = parametermanager.parameters.DIFFUSION_FACTOR * 2 + 1
142    #     p = np.empty(shape=(probs.shape[0], probs.shape[1] + window_size - 1))
143    #     p[:, :window_size] = np.repeat(probs[:, 0], window_size).reshape(-1, window_size)
144    #     p[:, window_size - 1 :] = probs[:]
145    #     diffusome = np.convolve(p[0], np.ones(window_size) / window_size, mode="valid")
146
147    def get_map(self):
148        pass
class CompositeArchitecture:
 12class CompositeArchitecture:
 13    """
 14
 15    GUI
 16    - when pleiotropy is not needed;
 17    - it is quick, easy to analyze, delivers a diversity of phenotypes
 18    - every trait (surv repr muta neut) can be evolvable or not
 19    - if not evolvable, the value is set by !!!
 20    - if evolvable, it can be agespecific or age-independent
 21    - probability of a trait at each age is determined by a BITS_PER_LOCUS adjacent bits forming a "locus" / gene
 22    - the method by which these loci are converted into a phenotypic value is the Interpreter type
 23
 24    """
 25
 26    def __init__(self, BITS_PER_LOCUS, AGE_LIMIT, THRESHOLD):
 27        self.BITS_PER_LOCUS = BITS_PER_LOCUS
 28        self.n_loci = sum(trait.length for trait in parameterization.traits.values())
 29        self.length = self.n_loci * BITS_PER_LOCUS
 30        self.AGE_LIMIT = AGE_LIMIT
 31
 32        self.evolvable = [trait for trait in parameterization.traits.values() if trait.evolvable]
 33
 34        self.interpreter = Interpreter(
 35            self.BITS_PER_LOCUS,
 36            THRESHOLD,
 37        )
 38
 39        # Fixed seed=0 so all populations (including hybridizing ones with different RANDOM_SEEDs)
 40        # share an identical physical genome layout; locus_permutation[i] = physical position of logical locus i
 41        self.locus_permutation = np.random.default_rng(0).permutation(self.n_loci)
 42
 43        # Per-locus dominance coefficient h, indexed by *physical* locus position
 44        # (because diploid_to_haploid operates on the physical layout, before reorder).
 45        # Built from each trait's G_<trait>_dominance value (default 0.5 = codominant).
 46        self.dominance_per_locus = np.full(self.n_loci, 0.5, dtype=np.float32)
 47        for trait in parameterization.traits.values():
 48            if trait.length == 0:
 49                continue
 50            phys_pos = self.locus_permutation[trait.start:trait.end]
 51            self.dominance_per_locus[phys_pos] = np.float32(trait.dominance)
 52
 53        # Optional pleiotropy. None unless PHENOMAP_SPECS is given.
 54        self.phenomap_matrix = self._build_phenomap(parametermanager.parameters.PHENOMAP_SPECS)
 55
 56    def _build_phenomap(self, PHENOMAP_SPECS):
 57        """Build a genotype-phenotype matrix from PHENOMAP_SPECS (aegis v1 semantics).
 58
 59        The matrix is the identity plus off-diagonal weights: the diagonal means every
 60        locus keeps encoding its own trait at its own age (so age-specific survival is
 61        preserved), and each spec adds a *pleiotropic* effect of one locus on another
 62        trait/age on top. This is what makes antagonistic pleiotropy expressible --
 63        one locus raising surv at an early age and lowering it at a late one.
 64
 65        Each spec is ``[source_trait, source_index, target_trait, target_age, weight]``
 66        with ``source_index`` (1..trait length) and ``target_age`` (1..AGE_LIMIT) 1-based,
 67        as exported by v1. Indices are resolved in *logical* (trait x age) order, which is
 68        the order compute() reorders genomes into.
 69
 70        Returns None when no specs are given, in which case compute() is unchanged.
 71        """
 72        if not PHENOMAP_SPECS:
 73            return None
 74
 75        map_ = np.diag(np.ones(self.n_loci, dtype=np.float32))
 76        for spec in PHENOMAP_SPECS:
 77            source_trait, source_index, target_trait, target_age, weight = spec
 78            source = parameterization.traits[source_trait]
 79            target = parameterization.traits[target_trait]
 80            geno_i = source.start + (int(source_index) - 1)
 81            pheno_i = target.start + (int(target_age) - 1)
 82            assert source.start <= geno_i < source.end, (
 83                f"PHENOMAP_SPECS source index {source_index} is out of range for trait "
 84                f"'{source_trait}' which has {source.length} loci; "
 85                f"set G_{source_trait}_agespecific to at least {source_index}"
 86            )
 87            assert target.start <= pheno_i < target.end, (
 88                f"PHENOMAP_SPECS target age {target_age} is out of range for trait "
 89                f"'{target_trait}' which has {target.length} loci"
 90            )
 91            map_[geno_i, pheno_i] = np.float32(weight)
 92        return map_
 93
 94    def get_number_of_bits(self):
 95        return ploider.ploider.y * self.n_loci * self.BITS_PER_LOCUS
 96
 97    def get_shape(self):
 98        return (ploider.ploider.y, self.n_loci, self.BITS_PER_LOCUS)
 99
100    def init_genome_array(self, popsize):
101        # TODO enable agespecific False
102        array = variables.rng.random(size=(popsize, *self.get_shape()))
103
104        for trait in parameterization.traits.values():
105            phys_pos = self.locus_permutation[trait.start:trait.end]
106            array[:, :, phys_pos, :] = array[:, :, phys_pos, :] < trait.initgeno
107
108        return array
109
110    def compute(self, genomes):
111
112        if genomes.shape[1] == 1:  # Do not calculate mean if genomes are haploid
113            genomes = genomes[:, 0]
114        else:
115            genomes = ploider.ploider.diploid_to_haploid(genomes, dominance_per_locus=self.dominance_per_locus)
116
117        # Reorder from physical storage order to logical (trait×age) order
118        genomes = genomes[:, self.locus_permutation, :]
119
120        interpretome = np.zeros(shape=(genomes.shape[0], genomes.shape[1]), dtype=np.float32)
121        for trait in parameterization.traits.values():
122            loci = genomes[:, trait.slice]  # fetch
123            probs = self.interpreter.call(loci, trait.interpreter)  # interpret
124            interpretome[:, trait.slice] += probs  # add back
125
126        # Apply pleiotropy, if any, to the raw [0, 1] interpreter output -- before the
127        # lo/hi mapping below, so that spec weights are expressed in interpreter units
128        # (aegis v1 order: interpret -> phenomap -> lo/hi).
129        if self.phenomap_matrix is not None:
130            interpretome = np.clip(interpretome.dot(self.phenomap_matrix), 0, 1).astype(np.float32)
131
132        # Map the [0, 1] interpreter output onto the trait's [lo, hi] phenotypic range.
133        # This is what makes G_<trait>_lo / G_<trait>_hi do anything — without this scaling
134        # the lo/hi parameters are parsed but discarded. Defaults: G_surv_lo=0.7, G_surv_hi=1.0
135        # (surv never goes to 0); G_repr_lo=0, G_repr_hi=0.5; others lo=0, hi=1.
136        for trait in parameterization.traits.values():
137            interpretome[:, trait.slice] = trait.lo + (trait.hi - trait.lo) * interpretome[:, trait.slice]
138
139        return interpretome
140
141    # def diffuse(self, probs):
142    #     window_size = parametermanager.parameters.DIFFUSION_FACTOR * 2 + 1
143    #     p = np.empty(shape=(probs.shape[0], probs.shape[1] + window_size - 1))
144    #     p[:, :window_size] = np.repeat(probs[:, 0], window_size).reshape(-1, window_size)
145    #     p[:, window_size - 1 :] = probs[:]
146    #     diffusome = np.convolve(p[0], np.ones(window_size) / window_size, mode="valid")
147
148    def get_map(self):
149        pass

GUI

  • when pleiotropy is not needed;
  • it is quick, easy to analyze, delivers a diversity of phenotypes
  • every trait (surv repr muta neut) can be evolvable or not
  • if not evolvable, the value is set by !!!
  • if evolvable, it can be agespecific or age-independent
  • probability of a trait at each age is determined by a BITS_PER_LOCUS adjacent bits forming a "locus" / gene
  • the method by which these loci are converted into a phenotypic value is the Interpreter type
CompositeArchitecture(BITS_PER_LOCUS, AGE_LIMIT, THRESHOLD)
26    def __init__(self, BITS_PER_LOCUS, AGE_LIMIT, THRESHOLD):
27        self.BITS_PER_LOCUS = BITS_PER_LOCUS
28        self.n_loci = sum(trait.length for trait in parameterization.traits.values())
29        self.length = self.n_loci * BITS_PER_LOCUS
30        self.AGE_LIMIT = AGE_LIMIT
31
32        self.evolvable = [trait for trait in parameterization.traits.values() if trait.evolvable]
33
34        self.interpreter = Interpreter(
35            self.BITS_PER_LOCUS,
36            THRESHOLD,
37        )
38
39        # Fixed seed=0 so all populations (including hybridizing ones with different RANDOM_SEEDs)
40        # share an identical physical genome layout; locus_permutation[i] = physical position of logical locus i
41        self.locus_permutation = np.random.default_rng(0).permutation(self.n_loci)
42
43        # Per-locus dominance coefficient h, indexed by *physical* locus position
44        # (because diploid_to_haploid operates on the physical layout, before reorder).
45        # Built from each trait's G_<trait>_dominance value (default 0.5 = codominant).
46        self.dominance_per_locus = np.full(self.n_loci, 0.5, dtype=np.float32)
47        for trait in parameterization.traits.values():
48            if trait.length == 0:
49                continue
50            phys_pos = self.locus_permutation[trait.start:trait.end]
51            self.dominance_per_locus[phys_pos] = np.float32(trait.dominance)
52
53        # Optional pleiotropy. None unless PHENOMAP_SPECS is given.
54        self.phenomap_matrix = self._build_phenomap(parametermanager.parameters.PHENOMAP_SPECS)
BITS_PER_LOCUS
n_loci
length
AGE_LIMIT
evolvable
interpreter
locus_permutation
dominance_per_locus
phenomap_matrix
def get_number_of_bits(self):
94    def get_number_of_bits(self):
95        return ploider.ploider.y * self.n_loci * self.BITS_PER_LOCUS
def get_shape(self):
97    def get_shape(self):
98        return (ploider.ploider.y, self.n_loci, self.BITS_PER_LOCUS)
def init_genome_array(self, popsize):
100    def init_genome_array(self, popsize):
101        # TODO enable agespecific False
102        array = variables.rng.random(size=(popsize, *self.get_shape()))
103
104        for trait in parameterization.traits.values():
105            phys_pos = self.locus_permutation[trait.start:trait.end]
106            array[:, :, phys_pos, :] = array[:, :, phys_pos, :] < trait.initgeno
107
108        return array
def compute(self, genomes):
110    def compute(self, genomes):
111
112        if genomes.shape[1] == 1:  # Do not calculate mean if genomes are haploid
113            genomes = genomes[:, 0]
114        else:
115            genomes = ploider.ploider.diploid_to_haploid(genomes, dominance_per_locus=self.dominance_per_locus)
116
117        # Reorder from physical storage order to logical (trait×age) order
118        genomes = genomes[:, self.locus_permutation, :]
119
120        interpretome = np.zeros(shape=(genomes.shape[0], genomes.shape[1]), dtype=np.float32)
121        for trait in parameterization.traits.values():
122            loci = genomes[:, trait.slice]  # fetch
123            probs = self.interpreter.call(loci, trait.interpreter)  # interpret
124            interpretome[:, trait.slice] += probs  # add back
125
126        # Apply pleiotropy, if any, to the raw [0, 1] interpreter output -- before the
127        # lo/hi mapping below, so that spec weights are expressed in interpreter units
128        # (aegis v1 order: interpret -> phenomap -> lo/hi).
129        if self.phenomap_matrix is not None:
130            interpretome = np.clip(interpretome.dot(self.phenomap_matrix), 0, 1).astype(np.float32)
131
132        # Map the [0, 1] interpreter output onto the trait's [lo, hi] phenotypic range.
133        # This is what makes G_<trait>_lo / G_<trait>_hi do anything — without this scaling
134        # the lo/hi parameters are parsed but discarded. Defaults: G_surv_lo=0.7, G_surv_hi=1.0
135        # (surv never goes to 0); G_repr_lo=0, G_repr_hi=0.5; others lo=0, hi=1.
136        for trait in parameterization.traits.values():
137            interpretome[:, trait.slice] = trait.lo + (trait.hi - trait.lo) * interpretome[:, trait.slice]
138
139        return interpretome
def get_map(self):
148    def get_map(self):
149        pass