Source code for laser_measles.abm.components.process_vital_dynamics

"""
Component defining the VitalDynamicsProcess, which simulates the vital dynamics in the ABM model with MCV1.
"""

import numpy as np
from laser_core import SortedQueue
from pydantic import Field

from laser_measles.abm.model import ABMModel
from laser_measles.components import BaseVitalDynamicsParams
from laser_measles.components import BaseVitalDynamicsProcess
from laser_measles.utils import cast_type


class VitalDynamicsParams(BaseVitalDynamicsParams):
    """
    Parameters for VitalDynamicsProcess.
    """

    routine_immunization_delay: int = Field(default=9 * 30, description="Delay in days before routine immunization is administered")


[docs] class VitalDynamicsProcess(BaseVitalDynamicsProcess): """ Process for simulating vital dynamics in the ABM model with MCV1 and constant birth and mortality rates (not age-structured). """ def __init__(self, model, verbose: bool = False, params: VitalDynamicsParams | None = None) -> None: params_: VitalDynamicsParams = params or VitalDynamicsParams() super().__init__(model, verbose=verbose, params=params_) # re-initialize people frame with correct capacity capacity = self.calculate_capacity(model=model) model.initialize_people_capacity(capacity=capacity, initial_count=model.scenario["pop"].sum()) people = model.people patches = model.patches date_of_birth_dtype = np.int32 self.null_value = np.iinfo(date_of_birth_dtype).max people.add_scalar_property("active", dtype=np.bool, default=False) people.add_scalar_property("date_of_birth", dtype=date_of_birth_dtype, default=self.null_value) people.add_scalar_property("date_of_vaccination", dtype=np.uint32, default=self.null_value) patches.add_scalar_property("births", dtype=np.uint32) self.vaccination_queue: SortedQueue = SortedQueue(capacity=capacity, values=people.date_of_vaccination) if model.params.num_ticks >= self.null_value: raise ValueError("Simulation is too long; birth and vaccination dates must be able to store the number of ticks") def __call__(self, model, tick: int) -> None: """ Simulate the vital dynamics process. """ patches = model.patches people = model.people population = patches.states.sum(axis=0) # Deaths # ------ # Calculate number of deaths deaths = model.prng.poisson(population.sum() * self.mu_death) # over all patches # select agents idx = model.prng.choice(np.where(model.people.active)[0], size=deaths, replace=False) # deactivate agents model.people.active[idx] = False # update state counter for state_idx, _ in enumerate(model.params.states): mask = model.people.state[idx] == state_idx # mask of in-active agents in the current state cnt = np.bincount(model.people.patch_id[idx][mask], minlength=model.patches.states.shape[-1]) model.patches.states[state_idx] -= cast_type(cnt, model.patches.states.dtype) if self.lambda_birth > 0: # Births # ------ # Calculate number of births births = model.prng.poisson(population * self.lambda_birth) # in each patch # find indices of the people frame for initializing istart, iend = people.add(births.sum()) people.date_of_birth[istart:iend] = tick # born today people.susceptibility[istart:iend] = 1.0 # all newborns are susceptible TODO: add maternal immunity component people.date_of_vaccination[istart:iend] = tick + self._routine_immunization_delay() index = istart # update patch id for this_patch_id, this_patch_births in enumerate(births): people.patch_id[index : index + this_patch_births] = this_patch_id index += this_patch_births # update states patches.states.S += cast_type(births, patches.states.dtype) # Routine immunization # -------------------- while len(self.vaccination_queue) > 0 and people.date_of_vaccination[self.vaccination_queue.peeki()] <= tick: i = self.vaccination_queue.popi() people.susceptibility[i] = 0.0 # susceptibility people.state[i] = model.params.states.index("R") # move to recovered state def calculate_capacity(self, model) -> int: """Estimate the necessary capacity of the people laserframe.""" rate = self.lambda_birth # - self.mu_death # calculated per tick buffered_ticks = (model.params.num_ticks * model.params.time_step_days // 365 + 1) * 365 / model.params.time_step_days N = model.scenario["pop"].to_numpy() * np.exp(rate * buffered_ticks) return int(N.sum()) def _routine_immunization_delay(self) -> int: """Delay in ticks before routine immunization is administered.""" params: VitalDynamicsParams = self.params # type: ignore return params.routine_immunization_delay * self.model.params.time_step_days def _initialize(self, model: ABMModel) -> None: # initialize the people laserframe with correct capacity # initial_pop = model.scenario["pop"].sum() # model.initialize_people_capacity(capacity=self.calculate_capacity(model), initial_count=initial_pop) # Activate population model.people.active[0 : model.people.count] = True # Simple initializer for ages where birth rate = mortality rate: # Initialize ages for existing population if self.mu_death > 0: model.people.date_of_birth[0 : model.people.count] = cast_type( -1 * model.prng.exponential(1 / self.mu_death, model.people.count), model.people.date_of_birth.dtype )