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")