aegis_sim.bioreactor

  1import numpy as np
  2import logging
  3
  4from aegis_sim import variables
  5from aegis_sim import submodels
  6from aegis_sim.constants import VALID_CAUSES_OF_DEATH
  7from aegis_sim.dataclasses.population import Population
  8from aegis_sim.recording import recordingmanager
  9from aegis_sim.parameterization import parametermanager
 10from aegis_sim.submodels.resources.resources import resources
 11
 12
 13class Bioreactor:
 14    def __init__(self, population: Population):
 15        self.eggs: Population = None
 16        self.population: Population = population
 17        self._starvation_steps: int = 0        # consecutive steps where N > resources
 18        self._starvation_multiplier: float = 1.0  # (1 - STARVATION_PENALTY) ** _starvation_steps
 19
 20    ##############
 21    # MAIN LOGIC #
 22    ##############
 23
 24    def run_step(self):
 25        """Perform one step of simulation."""
 26
 27        # If extinct (no living individuals nor eggs left), do nothing
 28        if len(self) == 0:
 29            logging.debug("Population went extinct.")
 30            recordingmanager.summaryrecorder.extinct = True
 31            return
 32
 33        # Scavenge resources and update the starvation multiplier.
 34        # If N > resources: starvation counter increments and multiplier compounds.
 35        # If resources >= N: counter resets to 0 and multiplier returns to 1.0.
 36        self._scavenge_resources()
 37
 38        # Mortality sources
 39        self.mortalities()
 40        resources.replenish()
 41
 42        recordingmanager.popsizerecorder.write_before_reproduction(self.population)
 43        self.growth()  # size increase
 44        self.reproduction()  # reproduction
 45        self.age()  # age increment and potentially death
 46        self.hatch()
 47        submodels.architect.envdrift.evolve(step=variables.steps)
 48
 49        # Record data
 50        recordingmanager.popsizerecorder.write_after_reproduction(self.population)
 51        recordingmanager.popsizerecorder.write_egg_num_after_reproduction(self.eggs)
 52        recordingmanager.envdriftmaprecorder.write(step=variables.steps)
 53        recordingmanager.flushrecorder.collect("additive_age_structure", self.population.ages)  # population census
 54        recordingmanager.picklerecorder.write(self.population)
 55        recordingmanager.featherrecorder.write(self.population)
 56        recordingmanager.ancestryrecorder.write(self.population)
 57        recordingmanager.guirecorder.record(self.population)
 58        recordingmanager.flushrecorder.flush()
 59        recordingmanager.popgenstatsrecorder.write(
 60            self.population.genomes, self.population.phenotypes.extract(ages=self.population.ages, trait_name="muta")
 61        )  # TODO defers calculation of mutation rates; hacky
 62        recordingmanager.summaryrecorder.record_memuse()
 63        recordingmanager.terecorder.record(self.population.ages, "alive")
 64        recordingmanager.checkpointrecorder.write(self.population, self.eggs)
 65
 66    ###############
 67    # STEP LOGIC #
 68    ###############
 69
 70    def mortalities(self):
 71        for source in parametermanager.parameters.MORTALITY_ORDER:
 72            if source == "intrinsic":
 73                self.mortality_intrinsic()
 74            elif source == "abiotic":
 75                self.mortality_abiotic()
 76            elif source == "infection":
 77                self.mortality_infection()
 78            elif source == "predation":
 79                self.mortality_predation()
 80            elif source == "starvation":
 81                self.mortality_starvation()
 82            else:
 83                raise ValueError(f"Invalid source of mortality '{source}'")
 84
 85    def mortality_intrinsic(self):
 86        probs_surv = self.population.phenotypes.extract(ages=self.population.ages, trait_name="surv")
 87        effective_surv = probs_surv * self._starvation_multiplier
 88        age_hazard = submodels.frailty.modify(hazard=1 - effective_surv, ages=self.population.ages)
 89        mask_kill = variables.rng.random(len(probs_surv)) < age_hazard
 90        self._kill(mask_kill=mask_kill, causeofdeath="intrinsic")
 91
 92    def mortality_abiotic(self):
 93        hazard = submodels.abiotic(variables.steps)
 94        age_hazard = submodels.frailty.modify(hazard=hazard, ages=self.population.ages)
 95        mask_kill = variables.rng.random(len(self.population)) < age_hazard
 96        self._kill(mask_kill=mask_kill, causeofdeath="abiotic")
 97
 98    def mortality_infection(self):
 99        submodels.infection(self.population)
100        # TODO add age hazard
101        mask_kill = self.population.infection == -1
102        self._kill(mask_kill=mask_kill, causeofdeath="infection")
103
104    def mortality_predation(self):
105        probs_kill = submodels.predation(len(self))
106        # TODO add age hazard
107        mask_kill = variables.rng.random(len(self)) < probs_kill
108        self._kill(mask_kill=mask_kill, causeofdeath="predation")
109
110    def mortality_starvation(self):
111        # Starvation now acts by scaling surv and repr phenotypes via _resource_ratio
112        # (set at the top of run_step before mortalities). No separate kill step needed.
113        # This method is kept so "starvation" remains a valid MORTALITY_ORDER entry.
114        pass
115
116    def reproduction(self):
117        """Generate offspring of reproducing individuals.
118        Initial is set to 0.
119        """
120
121        # Check if fertile
122        mask_fertile = (
123            self.population.ages >= parametermanager.parameters.MATURATION_AGE
124        )  # Check if mature; mature if survived MATURATION_AGE full cycles
125        if parametermanager.parameters.REPRODUCTION_ENDPOINT > 0:
126            mask_menopausal = (
127                self.population.ages >= parametermanager.parameters.REPRODUCTION_ENDPOINT
128            )  # Check if menopausal; menopausal when lived through REPRODUCTION_ENDPOINT full cycles
129            mask_fertile = (mask_fertile) & (~mask_menopausal)
130
131        if not any(mask_fertile):
132            return
133
134        probs_repr = (
135            self.population.phenotypes.extract(ages=self.population.ages, trait_name="repr", part=mask_fertile)
136            * self._starvation_multiplier
137        )
138
139        # Binomial calculation
140        n = parametermanager.parameters.MAX_OFFSPRING_NUMBER
141        p = probs_repr
142
143        assert np.all(p <= 1)
144        assert np.all(p >= 0)
145        num_repr = variables.rng.binomial(n=n, p=p)
146        mask_repr = num_repr > 0
147
148        if sum(num_repr) == 0:
149            return
150
151        # Indices of reproducing individuals
152        who = np.repeat(np.arange(len(self.population)), num_repr)
153
154        # Count ages at reproduction
155        ages_repr = self.population.ages[who]
156        recordingmanager.flushrecorder.collect("age_at_birth", ages_repr)
157
158        # Increase births statistics
159        self.population.births += num_repr
160
161        # Generate offspring genomes
162        parental_genomes = self.population.genomes.get(individuals=who)
163        parental_sexes = self.population.sexes[who]
164        parental_ancestry = self.population.ancestry[who] if self.population.ancestry is not None else None
165
166        muta_prob = self.population.phenotypes.extract(ages=self.population.ages, trait_name="muta", part=mask_repr)[
167            mask_repr
168        ]
169        muta_prob = np.repeat(muta_prob, num_repr[mask_repr])
170
171        offspring_genomes, offspring_ancestry = submodels.reproduction.generate_offspring_genomes(
172            genomes=parental_genomes,
173            muta_prob=muta_prob,
174            ages=ages_repr,
175            parental_sexes=parental_sexes,
176            ancestry=parental_ancestry,
177        )
178        offspring_sexes = submodels.sexsystem.get_sex(len(offspring_genomes))
179
180        # Randomize order of newly laid egg attributes ..
181        # .. because the order will affect their probability to be removed because of limited carrying capacity
182        order = np.arange(len(offspring_sexes))
183        variables.rng.shuffle(order)
184        offspring_genomes = offspring_genomes[order]
185        offspring_sexes = offspring_sexes[order]
186        if offspring_ancestry is not None:
187            offspring_ancestry = offspring_ancestry[order]
188
189        # Make eggs
190        eggs = Population.make_eggs(
191            offspring_genomes=offspring_genomes,
192            step=variables.steps,
193            offspring_sexes=offspring_sexes,
194            parental_generations=np.zeros(len(offspring_sexes)),  # TODO replace with working calculation
195            offspring_ancestry=offspring_ancestry,
196        )
197        if self.eggs is None:
198            self.eggs = eggs
199        else:
200            self.eggs += eggs
201
202        if parametermanager.parameters.CARRYING_CAPACITY_EGGS is not None and len(self.eggs) > parametermanager.parameters.CARRYING_CAPACITY_EGGS:
203            indices = np.arange(len(self.eggs))[-parametermanager.parameters.CARRYING_CAPACITY_EGGS :]
204            # TODO biased
205            self.eggs *= indices
206
207    def growth(self):
208        # TODO use already scavenged resources to determine growth
209        # max_growth_potential = self.population.phenotypes.extract(ages=self.population.ages, trait_name="grow")
210        # gathered_resources = submodels.resources.scavenge(max_growth_potential)
211        # self.population.sizes += gathered_resources
212        self.population.sizes += 1
213
214    def age(self):
215        """Increase age of all by one and kill those that surpass age limit.
216        Age denotes the number of full cycles that an individual survived and reproduced.
217        AGE_LIMIT is the maximum number of full cycles an individual can go through.
218        """
219        self.population.ages += 1
220        mask_kill = self.population.ages >= parametermanager.parameters.AGE_LIMIT
221        self._kill(mask_kill=mask_kill, causeofdeath="age_limit")
222
223    def hatch(self):
224        """Turn eggs into living individuals"""
225
226        # If nothing to hatch
227        if self.eggs is None or len(self.eggs) == 0:
228            return
229
230        # If REPRODUCTION_REGULATION is True, only reproduce until MAX_POPULATION_SIZE
231        if parametermanager.parameters.REPRODUCTION_REGULATION:
232            current_population_size = len(self.population)
233            remaining_capacity = resources.capacity - current_population_size
234            # If no remaining capacity, do not reproduce
235            if remaining_capacity < 1:
236                self.eggs = None
237                return
238            elif remaining_capacity < len(self.eggs):
239                indices = variables.rng.choice(len(self.eggs), size=int(remaining_capacity), replace=False)
240                self.eggs *= indices
241
242        # If something to hatch
243        if (
244            (
245                parametermanager.parameters.INCUBATION_PERIOD == -1 and len(self.population) == 0
246            )  # hatch when everyone dead
247            or (parametermanager.parameters.INCUBATION_PERIOD == 0)  # hatch immediately
248            or (
249                parametermanager.parameters.INCUBATION_PERIOD > 0
250                and variables.steps % parametermanager.parameters.INCUBATION_PERIOD == 0
251            )  # hatch with delay
252        ):
253
254            self.eggs.phenotypes = submodels.architect.__call__(self.eggs.genomes)
255            self.population += self.eggs
256            self.eggs = None
257
258    ################
259    # HELPER FUNCS #
260    ################
261
262    def _scavenge_resources(self):
263        """Scavenge resources and update the compounding starvation multiplier.
264
265        If N > available resources (deficit): starvation counter increments by 1
266        and multiplier = (1 - STARVATION_PENALTY) ** counter.
267
268        If resources >= N: counter resets to 0 and multiplier returns to 1.0.
269
270        The multiplier is applied to each individual's age-specific surv and repr
271        phenotypes in mortality_intrinsic() and reproduction().
272        """
273        n = len(self.population)
274        if n == 0:
275            self._starvation_steps = 0
276            self._starvation_multiplier = 1.0
277            return
278
279        in_deficit = n > resources.capacity
280
281        recordingmanager.resourcerecorder.write_before_scavenging()
282        resources.scavenge(np.ones(n))
283        recordingmanager.resourcerecorder.write_after_scavenging()
284
285        if in_deficit:
286            self._starvation_steps += 1
287        else:
288            self._starvation_steps = 0
289
290        penalty = parametermanager.parameters.STARVATION_PENALTY
291        self._starvation_multiplier = (1.0 - penalty) ** self._starvation_steps
292
293    def _kill(self, mask_kill, causeofdeath):
294        """Kill individuals and record their data."""
295
296        assert causeofdeath in VALID_CAUSES_OF_DEATH
297
298        # Skip if no one to kill
299        if not any(mask_kill):
300            return
301
302        # Count ages at death
303        # if causeofdeath != "age_limit":
304        ages_death = self.population.ages[mask_kill]
305        recordingmanager.flushrecorder.collect(f"age_at_{causeofdeath}", ages_death)
306        recordingmanager.terecorder.record(ages_death, "dead")
307
308        # Retain survivors
309        self.population *= ~mask_kill
310
311    def __len__(self):
312        """Return the number of living individuals and saved eggs."""
313        return len(self.population) + len(self.eggs) if self.eggs is not None else len(self.population)
class Bioreactor:
 14class Bioreactor:
 15    def __init__(self, population: Population):
 16        self.eggs: Population = None
 17        self.population: Population = population
 18        self._starvation_steps: int = 0        # consecutive steps where N > resources
 19        self._starvation_multiplier: float = 1.0  # (1 - STARVATION_PENALTY) ** _starvation_steps
 20
 21    ##############
 22    # MAIN LOGIC #
 23    ##############
 24
 25    def run_step(self):
 26        """Perform one step of simulation."""
 27
 28        # If extinct (no living individuals nor eggs left), do nothing
 29        if len(self) == 0:
 30            logging.debug("Population went extinct.")
 31            recordingmanager.summaryrecorder.extinct = True
 32            return
 33
 34        # Scavenge resources and update the starvation multiplier.
 35        # If N > resources: starvation counter increments and multiplier compounds.
 36        # If resources >= N: counter resets to 0 and multiplier returns to 1.0.
 37        self._scavenge_resources()
 38
 39        # Mortality sources
 40        self.mortalities()
 41        resources.replenish()
 42
 43        recordingmanager.popsizerecorder.write_before_reproduction(self.population)
 44        self.growth()  # size increase
 45        self.reproduction()  # reproduction
 46        self.age()  # age increment and potentially death
 47        self.hatch()
 48        submodels.architect.envdrift.evolve(step=variables.steps)
 49
 50        # Record data
 51        recordingmanager.popsizerecorder.write_after_reproduction(self.population)
 52        recordingmanager.popsizerecorder.write_egg_num_after_reproduction(self.eggs)
 53        recordingmanager.envdriftmaprecorder.write(step=variables.steps)
 54        recordingmanager.flushrecorder.collect("additive_age_structure", self.population.ages)  # population census
 55        recordingmanager.picklerecorder.write(self.population)
 56        recordingmanager.featherrecorder.write(self.population)
 57        recordingmanager.ancestryrecorder.write(self.population)
 58        recordingmanager.guirecorder.record(self.population)
 59        recordingmanager.flushrecorder.flush()
 60        recordingmanager.popgenstatsrecorder.write(
 61            self.population.genomes, self.population.phenotypes.extract(ages=self.population.ages, trait_name="muta")
 62        )  # TODO defers calculation of mutation rates; hacky
 63        recordingmanager.summaryrecorder.record_memuse()
 64        recordingmanager.terecorder.record(self.population.ages, "alive")
 65        recordingmanager.checkpointrecorder.write(self.population, self.eggs)
 66
 67    ###############
 68    # STEP LOGIC #
 69    ###############
 70
 71    def mortalities(self):
 72        for source in parametermanager.parameters.MORTALITY_ORDER:
 73            if source == "intrinsic":
 74                self.mortality_intrinsic()
 75            elif source == "abiotic":
 76                self.mortality_abiotic()
 77            elif source == "infection":
 78                self.mortality_infection()
 79            elif source == "predation":
 80                self.mortality_predation()
 81            elif source == "starvation":
 82                self.mortality_starvation()
 83            else:
 84                raise ValueError(f"Invalid source of mortality '{source}'")
 85
 86    def mortality_intrinsic(self):
 87        probs_surv = self.population.phenotypes.extract(ages=self.population.ages, trait_name="surv")
 88        effective_surv = probs_surv * self._starvation_multiplier
 89        age_hazard = submodels.frailty.modify(hazard=1 - effective_surv, ages=self.population.ages)
 90        mask_kill = variables.rng.random(len(probs_surv)) < age_hazard
 91        self._kill(mask_kill=mask_kill, causeofdeath="intrinsic")
 92
 93    def mortality_abiotic(self):
 94        hazard = submodels.abiotic(variables.steps)
 95        age_hazard = submodels.frailty.modify(hazard=hazard, ages=self.population.ages)
 96        mask_kill = variables.rng.random(len(self.population)) < age_hazard
 97        self._kill(mask_kill=mask_kill, causeofdeath="abiotic")
 98
 99    def mortality_infection(self):
100        submodels.infection(self.population)
101        # TODO add age hazard
102        mask_kill = self.population.infection == -1
103        self._kill(mask_kill=mask_kill, causeofdeath="infection")
104
105    def mortality_predation(self):
106        probs_kill = submodels.predation(len(self))
107        # TODO add age hazard
108        mask_kill = variables.rng.random(len(self)) < probs_kill
109        self._kill(mask_kill=mask_kill, causeofdeath="predation")
110
111    def mortality_starvation(self):
112        # Starvation now acts by scaling surv and repr phenotypes via _resource_ratio
113        # (set at the top of run_step before mortalities). No separate kill step needed.
114        # This method is kept so "starvation" remains a valid MORTALITY_ORDER entry.
115        pass
116
117    def reproduction(self):
118        """Generate offspring of reproducing individuals.
119        Initial is set to 0.
120        """
121
122        # Check if fertile
123        mask_fertile = (
124            self.population.ages >= parametermanager.parameters.MATURATION_AGE
125        )  # Check if mature; mature if survived MATURATION_AGE full cycles
126        if parametermanager.parameters.REPRODUCTION_ENDPOINT > 0:
127            mask_menopausal = (
128                self.population.ages >= parametermanager.parameters.REPRODUCTION_ENDPOINT
129            )  # Check if menopausal; menopausal when lived through REPRODUCTION_ENDPOINT full cycles
130            mask_fertile = (mask_fertile) & (~mask_menopausal)
131
132        if not any(mask_fertile):
133            return
134
135        probs_repr = (
136            self.population.phenotypes.extract(ages=self.population.ages, trait_name="repr", part=mask_fertile)
137            * self._starvation_multiplier
138        )
139
140        # Binomial calculation
141        n = parametermanager.parameters.MAX_OFFSPRING_NUMBER
142        p = probs_repr
143
144        assert np.all(p <= 1)
145        assert np.all(p >= 0)
146        num_repr = variables.rng.binomial(n=n, p=p)
147        mask_repr = num_repr > 0
148
149        if sum(num_repr) == 0:
150            return
151
152        # Indices of reproducing individuals
153        who = np.repeat(np.arange(len(self.population)), num_repr)
154
155        # Count ages at reproduction
156        ages_repr = self.population.ages[who]
157        recordingmanager.flushrecorder.collect("age_at_birth", ages_repr)
158
159        # Increase births statistics
160        self.population.births += num_repr
161
162        # Generate offspring genomes
163        parental_genomes = self.population.genomes.get(individuals=who)
164        parental_sexes = self.population.sexes[who]
165        parental_ancestry = self.population.ancestry[who] if self.population.ancestry is not None else None
166
167        muta_prob = self.population.phenotypes.extract(ages=self.population.ages, trait_name="muta", part=mask_repr)[
168            mask_repr
169        ]
170        muta_prob = np.repeat(muta_prob, num_repr[mask_repr])
171
172        offspring_genomes, offspring_ancestry = submodels.reproduction.generate_offspring_genomes(
173            genomes=parental_genomes,
174            muta_prob=muta_prob,
175            ages=ages_repr,
176            parental_sexes=parental_sexes,
177            ancestry=parental_ancestry,
178        )
179        offspring_sexes = submodels.sexsystem.get_sex(len(offspring_genomes))
180
181        # Randomize order of newly laid egg attributes ..
182        # .. because the order will affect their probability to be removed because of limited carrying capacity
183        order = np.arange(len(offspring_sexes))
184        variables.rng.shuffle(order)
185        offspring_genomes = offspring_genomes[order]
186        offspring_sexes = offspring_sexes[order]
187        if offspring_ancestry is not None:
188            offspring_ancestry = offspring_ancestry[order]
189
190        # Make eggs
191        eggs = Population.make_eggs(
192            offspring_genomes=offspring_genomes,
193            step=variables.steps,
194            offspring_sexes=offspring_sexes,
195            parental_generations=np.zeros(len(offspring_sexes)),  # TODO replace with working calculation
196            offspring_ancestry=offspring_ancestry,
197        )
198        if self.eggs is None:
199            self.eggs = eggs
200        else:
201            self.eggs += eggs
202
203        if parametermanager.parameters.CARRYING_CAPACITY_EGGS is not None and len(self.eggs) > parametermanager.parameters.CARRYING_CAPACITY_EGGS:
204            indices = np.arange(len(self.eggs))[-parametermanager.parameters.CARRYING_CAPACITY_EGGS :]
205            # TODO biased
206            self.eggs *= indices
207
208    def growth(self):
209        # TODO use already scavenged resources to determine growth
210        # max_growth_potential = self.population.phenotypes.extract(ages=self.population.ages, trait_name="grow")
211        # gathered_resources = submodels.resources.scavenge(max_growth_potential)
212        # self.population.sizes += gathered_resources
213        self.population.sizes += 1
214
215    def age(self):
216        """Increase age of all by one and kill those that surpass age limit.
217        Age denotes the number of full cycles that an individual survived and reproduced.
218        AGE_LIMIT is the maximum number of full cycles an individual can go through.
219        """
220        self.population.ages += 1
221        mask_kill = self.population.ages >= parametermanager.parameters.AGE_LIMIT
222        self._kill(mask_kill=mask_kill, causeofdeath="age_limit")
223
224    def hatch(self):
225        """Turn eggs into living individuals"""
226
227        # If nothing to hatch
228        if self.eggs is None or len(self.eggs) == 0:
229            return
230
231        # If REPRODUCTION_REGULATION is True, only reproduce until MAX_POPULATION_SIZE
232        if parametermanager.parameters.REPRODUCTION_REGULATION:
233            current_population_size = len(self.population)
234            remaining_capacity = resources.capacity - current_population_size
235            # If no remaining capacity, do not reproduce
236            if remaining_capacity < 1:
237                self.eggs = None
238                return
239            elif remaining_capacity < len(self.eggs):
240                indices = variables.rng.choice(len(self.eggs), size=int(remaining_capacity), replace=False)
241                self.eggs *= indices
242
243        # If something to hatch
244        if (
245            (
246                parametermanager.parameters.INCUBATION_PERIOD == -1 and len(self.population) == 0
247            )  # hatch when everyone dead
248            or (parametermanager.parameters.INCUBATION_PERIOD == 0)  # hatch immediately
249            or (
250                parametermanager.parameters.INCUBATION_PERIOD > 0
251                and variables.steps % parametermanager.parameters.INCUBATION_PERIOD == 0
252            )  # hatch with delay
253        ):
254
255            self.eggs.phenotypes = submodels.architect.__call__(self.eggs.genomes)
256            self.population += self.eggs
257            self.eggs = None
258
259    ################
260    # HELPER FUNCS #
261    ################
262
263    def _scavenge_resources(self):
264        """Scavenge resources and update the compounding starvation multiplier.
265
266        If N > available resources (deficit): starvation counter increments by 1
267        and multiplier = (1 - STARVATION_PENALTY) ** counter.
268
269        If resources >= N: counter resets to 0 and multiplier returns to 1.0.
270
271        The multiplier is applied to each individual's age-specific surv and repr
272        phenotypes in mortality_intrinsic() and reproduction().
273        """
274        n = len(self.population)
275        if n == 0:
276            self._starvation_steps = 0
277            self._starvation_multiplier = 1.0
278            return
279
280        in_deficit = n > resources.capacity
281
282        recordingmanager.resourcerecorder.write_before_scavenging()
283        resources.scavenge(np.ones(n))
284        recordingmanager.resourcerecorder.write_after_scavenging()
285
286        if in_deficit:
287            self._starvation_steps += 1
288        else:
289            self._starvation_steps = 0
290
291        penalty = parametermanager.parameters.STARVATION_PENALTY
292        self._starvation_multiplier = (1.0 - penalty) ** self._starvation_steps
293
294    def _kill(self, mask_kill, causeofdeath):
295        """Kill individuals and record their data."""
296
297        assert causeofdeath in VALID_CAUSES_OF_DEATH
298
299        # Skip if no one to kill
300        if not any(mask_kill):
301            return
302
303        # Count ages at death
304        # if causeofdeath != "age_limit":
305        ages_death = self.population.ages[mask_kill]
306        recordingmanager.flushrecorder.collect(f"age_at_{causeofdeath}", ages_death)
307        recordingmanager.terecorder.record(ages_death, "dead")
308
309        # Retain survivors
310        self.population *= ~mask_kill
311
312    def __len__(self):
313        """Return the number of living individuals and saved eggs."""
314        return len(self.population) + len(self.eggs) if self.eggs is not None else len(self.population)
Bioreactor(population: aegis_sim.dataclasses.population.Population)
15    def __init__(self, population: Population):
16        self.eggs: Population = None
17        self.population: Population = population
18        self._starvation_steps: int = 0        # consecutive steps where N > resources
19        self._starvation_multiplier: float = 1.0  # (1 - STARVATION_PENALTY) ** _starvation_steps
def run_step(self):
25    def run_step(self):
26        """Perform one step of simulation."""
27
28        # If extinct (no living individuals nor eggs left), do nothing
29        if len(self) == 0:
30            logging.debug("Population went extinct.")
31            recordingmanager.summaryrecorder.extinct = True
32            return
33
34        # Scavenge resources and update the starvation multiplier.
35        # If N > resources: starvation counter increments and multiplier compounds.
36        # If resources >= N: counter resets to 0 and multiplier returns to 1.0.
37        self._scavenge_resources()
38
39        # Mortality sources
40        self.mortalities()
41        resources.replenish()
42
43        recordingmanager.popsizerecorder.write_before_reproduction(self.population)
44        self.growth()  # size increase
45        self.reproduction()  # reproduction
46        self.age()  # age increment and potentially death
47        self.hatch()
48        submodels.architect.envdrift.evolve(step=variables.steps)
49
50        # Record data
51        recordingmanager.popsizerecorder.write_after_reproduction(self.population)
52        recordingmanager.popsizerecorder.write_egg_num_after_reproduction(self.eggs)
53        recordingmanager.envdriftmaprecorder.write(step=variables.steps)
54        recordingmanager.flushrecorder.collect("additive_age_structure", self.population.ages)  # population census
55        recordingmanager.picklerecorder.write(self.population)
56        recordingmanager.featherrecorder.write(self.population)
57        recordingmanager.ancestryrecorder.write(self.population)
58        recordingmanager.guirecorder.record(self.population)
59        recordingmanager.flushrecorder.flush()
60        recordingmanager.popgenstatsrecorder.write(
61            self.population.genomes, self.population.phenotypes.extract(ages=self.population.ages, trait_name="muta")
62        )  # TODO defers calculation of mutation rates; hacky
63        recordingmanager.summaryrecorder.record_memuse()
64        recordingmanager.terecorder.record(self.population.ages, "alive")
65        recordingmanager.checkpointrecorder.write(self.population, self.eggs)

Perform one step of simulation.

def mortalities(self):
71    def mortalities(self):
72        for source in parametermanager.parameters.MORTALITY_ORDER:
73            if source == "intrinsic":
74                self.mortality_intrinsic()
75            elif source == "abiotic":
76                self.mortality_abiotic()
77            elif source == "infection":
78                self.mortality_infection()
79            elif source == "predation":
80                self.mortality_predation()
81            elif source == "starvation":
82                self.mortality_starvation()
83            else:
84                raise ValueError(f"Invalid source of mortality '{source}'")
def mortality_intrinsic(self):
86    def mortality_intrinsic(self):
87        probs_surv = self.population.phenotypes.extract(ages=self.population.ages, trait_name="surv")
88        effective_surv = probs_surv * self._starvation_multiplier
89        age_hazard = submodels.frailty.modify(hazard=1 - effective_surv, ages=self.population.ages)
90        mask_kill = variables.rng.random(len(probs_surv)) < age_hazard
91        self._kill(mask_kill=mask_kill, causeofdeath="intrinsic")
def mortality_abiotic(self):
93    def mortality_abiotic(self):
94        hazard = submodels.abiotic(variables.steps)
95        age_hazard = submodels.frailty.modify(hazard=hazard, ages=self.population.ages)
96        mask_kill = variables.rng.random(len(self.population)) < age_hazard
97        self._kill(mask_kill=mask_kill, causeofdeath="abiotic")
def mortality_infection(self):
 99    def mortality_infection(self):
100        submodels.infection(self.population)
101        # TODO add age hazard
102        mask_kill = self.population.infection == -1
103        self._kill(mask_kill=mask_kill, causeofdeath="infection")
def mortality_predation(self):
105    def mortality_predation(self):
106        probs_kill = submodels.predation(len(self))
107        # TODO add age hazard
108        mask_kill = variables.rng.random(len(self)) < probs_kill
109        self._kill(mask_kill=mask_kill, causeofdeath="predation")
def mortality_starvation(self):
111    def mortality_starvation(self):
112        # Starvation now acts by scaling surv and repr phenotypes via _resource_ratio
113        # (set at the top of run_step before mortalities). No separate kill step needed.
114        # This method is kept so "starvation" remains a valid MORTALITY_ORDER entry.
115        pass
def reproduction(self):
117    def reproduction(self):
118        """Generate offspring of reproducing individuals.
119        Initial is set to 0.
120        """
121
122        # Check if fertile
123        mask_fertile = (
124            self.population.ages >= parametermanager.parameters.MATURATION_AGE
125        )  # Check if mature; mature if survived MATURATION_AGE full cycles
126        if parametermanager.parameters.REPRODUCTION_ENDPOINT > 0:
127            mask_menopausal = (
128                self.population.ages >= parametermanager.parameters.REPRODUCTION_ENDPOINT
129            )  # Check if menopausal; menopausal when lived through REPRODUCTION_ENDPOINT full cycles
130            mask_fertile = (mask_fertile) & (~mask_menopausal)
131
132        if not any(mask_fertile):
133            return
134
135        probs_repr = (
136            self.population.phenotypes.extract(ages=self.population.ages, trait_name="repr", part=mask_fertile)
137            * self._starvation_multiplier
138        )
139
140        # Binomial calculation
141        n = parametermanager.parameters.MAX_OFFSPRING_NUMBER
142        p = probs_repr
143
144        assert np.all(p <= 1)
145        assert np.all(p >= 0)
146        num_repr = variables.rng.binomial(n=n, p=p)
147        mask_repr = num_repr > 0
148
149        if sum(num_repr) == 0:
150            return
151
152        # Indices of reproducing individuals
153        who = np.repeat(np.arange(len(self.population)), num_repr)
154
155        # Count ages at reproduction
156        ages_repr = self.population.ages[who]
157        recordingmanager.flushrecorder.collect("age_at_birth", ages_repr)
158
159        # Increase births statistics
160        self.population.births += num_repr
161
162        # Generate offspring genomes
163        parental_genomes = self.population.genomes.get(individuals=who)
164        parental_sexes = self.population.sexes[who]
165        parental_ancestry = self.population.ancestry[who] if self.population.ancestry is not None else None
166
167        muta_prob = self.population.phenotypes.extract(ages=self.population.ages, trait_name="muta", part=mask_repr)[
168            mask_repr
169        ]
170        muta_prob = np.repeat(muta_prob, num_repr[mask_repr])
171
172        offspring_genomes, offspring_ancestry = submodels.reproduction.generate_offspring_genomes(
173            genomes=parental_genomes,
174            muta_prob=muta_prob,
175            ages=ages_repr,
176            parental_sexes=parental_sexes,
177            ancestry=parental_ancestry,
178        )
179        offspring_sexes = submodels.sexsystem.get_sex(len(offspring_genomes))
180
181        # Randomize order of newly laid egg attributes ..
182        # .. because the order will affect their probability to be removed because of limited carrying capacity
183        order = np.arange(len(offspring_sexes))
184        variables.rng.shuffle(order)
185        offspring_genomes = offspring_genomes[order]
186        offspring_sexes = offspring_sexes[order]
187        if offspring_ancestry is not None:
188            offspring_ancestry = offspring_ancestry[order]
189
190        # Make eggs
191        eggs = Population.make_eggs(
192            offspring_genomes=offspring_genomes,
193            step=variables.steps,
194            offspring_sexes=offspring_sexes,
195            parental_generations=np.zeros(len(offspring_sexes)),  # TODO replace with working calculation
196            offspring_ancestry=offspring_ancestry,
197        )
198        if self.eggs is None:
199            self.eggs = eggs
200        else:
201            self.eggs += eggs
202
203        if parametermanager.parameters.CARRYING_CAPACITY_EGGS is not None and len(self.eggs) > parametermanager.parameters.CARRYING_CAPACITY_EGGS:
204            indices = np.arange(len(self.eggs))[-parametermanager.parameters.CARRYING_CAPACITY_EGGS :]
205            # TODO biased
206            self.eggs *= indices

Generate offspring of reproducing individuals. Initial is set to 0.

def growth(self):
208    def growth(self):
209        # TODO use already scavenged resources to determine growth
210        # max_growth_potential = self.population.phenotypes.extract(ages=self.population.ages, trait_name="grow")
211        # gathered_resources = submodels.resources.scavenge(max_growth_potential)
212        # self.population.sizes += gathered_resources
213        self.population.sizes += 1
def age(self):
215    def age(self):
216        """Increase age of all by one and kill those that surpass age limit.
217        Age denotes the number of full cycles that an individual survived and reproduced.
218        AGE_LIMIT is the maximum number of full cycles an individual can go through.
219        """
220        self.population.ages += 1
221        mask_kill = self.population.ages >= parametermanager.parameters.AGE_LIMIT
222        self._kill(mask_kill=mask_kill, causeofdeath="age_limit")

Increase age of all by one and kill those that surpass age limit. Age denotes the number of full cycles that an individual survived and reproduced. AGE_LIMIT is the maximum number of full cycles an individual can go through.

def hatch(self):
224    def hatch(self):
225        """Turn eggs into living individuals"""
226
227        # If nothing to hatch
228        if self.eggs is None or len(self.eggs) == 0:
229            return
230
231        # If REPRODUCTION_REGULATION is True, only reproduce until MAX_POPULATION_SIZE
232        if parametermanager.parameters.REPRODUCTION_REGULATION:
233            current_population_size = len(self.population)
234            remaining_capacity = resources.capacity - current_population_size
235            # If no remaining capacity, do not reproduce
236            if remaining_capacity < 1:
237                self.eggs = None
238                return
239            elif remaining_capacity < len(self.eggs):
240                indices = variables.rng.choice(len(self.eggs), size=int(remaining_capacity), replace=False)
241                self.eggs *= indices
242
243        # If something to hatch
244        if (
245            (
246                parametermanager.parameters.INCUBATION_PERIOD == -1 and len(self.population) == 0
247            )  # hatch when everyone dead
248            or (parametermanager.parameters.INCUBATION_PERIOD == 0)  # hatch immediately
249            or (
250                parametermanager.parameters.INCUBATION_PERIOD > 0
251                and variables.steps % parametermanager.parameters.INCUBATION_PERIOD == 0
252            )  # hatch with delay
253        ):
254
255            self.eggs.phenotypes = submodels.architect.__call__(self.eggs.genomes)
256            self.population += self.eggs
257            self.eggs = None

Turn eggs into living individuals