Source code for laser.measles.components.base_vital_dynamics

from abc import ABC
from abc import abstractmethod
from typing import TypeVar

import numpy as np
from pydantic import BaseModel
from pydantic import Field

from laser.measles.base import BasePhase
from laser.measles.utils import cast_type

ModelType = TypeVar("ModelType")


class BaseVitalDynamicsParams(BaseModel):
    """Parameters specific to vital dynamics."""

    crude_birth_rate: float = Field(default=20.0, description="Annual crude birth rate per 1000 population", ge=0.0)
    crude_death_rate: float = Field(default=8.0, description="Annual crude death rate per 1000 population", ge=0.0)
    mcv1_efficacy: float = Field(
        default=0.9,
        description="Efficacy of routine MCV1 vaccination for newborns. This only applies to births processed by VitalDynamicsProcess, not the existing population.",
        ge=0.0,
        le=1.0,
    )


[docs] class BaseVitalDynamicsProcess(BasePhase, ABC): """ Phase for simulating the vital dynamics in the model with MCV1. This phase handles the simulation of births and deaths in the population model along with routine vaccination (MCV1). .. warning:: The ``mcv1`` scenario parameter **only vaccinates newborns** at each tick. It does **not** immunize the existing population. In short simulations (< 5 years), this produces negligible population-level immunity changes. To model a population with pre-existing vaccine-derived immunity, either: - Use ``InitializeEquilibriumStatesProcess`` to set an appropriate S/R split at the start of the simulation, or - Directly set ``states.S`` and ``states.R`` before running. - Use ``SIACalendarProcess`` for discrete campaign-based vaccination. See the *Vaccination Modeling* tutorial (``docs/tutorials/tut_vaccination.py``) for detailed examples of each approach. Parameters ---------- model : object The simulation model containing nodes, states, and parameters verbose : bool, default=False Whether to print verbose output during simulation params : VitalDynamicsParams | None, default=None Component-specific parameters. If None, will use default parameters Notes ----- - Birth rates are calculated per tick - MCV1 coverage is applied only to newborns; expect 5-10+ years of simulation time before routine immunization significantly shifts population-level immunity """ def __init__(self, model, verbose: bool = False, params: BaseVitalDynamicsParams | None = None) -> None: super().__init__(model, verbose) if params is None: params = BaseVitalDynamicsParams() self.params = params @property def lambda_birth(self) -> float: """birth rate per tick""" return (1 + self.params.crude_birth_rate / 1000) ** (1 / 365 * self.model.params.time_step_days) - 1 @property def mu_death(self) -> float: """death rate per tick""" return (1 + self.params.crude_death_rate / 1000) ** (1 / 365 * self.model.params.time_step_days) - 1
[docs] def __call__(self, model, tick: int) -> None: # state counts states = model.patches.states # num_compartments x num_patches # Vital dynamics population = states.sum(axis=0) avg_births = population * self.lambda_birth vaccinated_births = cast_type( model.prng.poisson(avg_births * np.array(model.scenario["mcv1"]) * self.params.mcv1_efficacy), states.dtype ) # vaccinated AND protected unvaccinated_births = cast_type( model.prng.poisson(avg_births * (1 - np.array(model.scenario["mcv1"]) * self.params.mcv1_efficacy)), states.dtype ) avg_deaths = states * self.mu_death deaths = cast_type(model.prng.poisson(avg_deaths), states.dtype) # number of deaths states.S += unvaccinated_births # add births to S states.R += vaccinated_births # add births to R states -= deaths # remove deaths from each compartment # make sure that all states >= 0 np.maximum(states, 0, out=states)
[docs] @abstractmethod def calculate_capacity(self, model) -> int: """ Calculate the capacity of the model. """ raise NotImplementedError("No capacity for this model")