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 MCV1", 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).
    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
    """
    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
    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)
    @abstractmethod
    def calculate_capacity(self, model) -> int:
        """
        Calculate the capacity of the model.
        """
        raise NotImplementedError("No capacity for this model")