Source code for laser_measles.biweekly.components.process_infection
import numpy as np
from pydantic import Field
from laser_measles.base import BaseLaserModel
from laser_measles.biweekly.mixing import init_gravity_diffusion
from laser_measles.components import BaseInfectionParams
from laser_measles.components import BaseInfectionProcess
class InfectionParams(BaseInfectionParams):
    """Parameters specific to the infection process component."""
    beta: float = Field(
        default=1 * 8 / 14, description="Base transmission rate (infections per day)", ge=0.0
    )  # beta = R0 / (mean infectious period)
    seasonality: float = Field(default=0.0, description="Seasonality factor, default is no seasonality", ge=0.0, le=1.0)
    season_start: int = Field(default=0, description="Season start tick (0-25)", ge=0, le=25)
    distance_exponent: float = Field(default=1.5, description="Distance exponent", ge=0.0)
    mixing_scale: float = Field(default=0.001, description="Mixing scale", ge=0.0)
    @property
    def beta_per_tick(self) -> float:
        return (self.beta * 365) / 26
[docs]
class InfectionProcess(BaseInfectionProcess):
    """
    Component for simulating the spread of infection in the model.
    This class implements a stochastic infection process that models disease transmission
    between different population groups. It uses a seasonally-adjusted transmission rate
    and accounts for mixing between different population groups.
    The infection process follows these steps:
    1. Calculates expected new infections based on:
       - Base transmission rate (beta)
       - Seasonal variation
       - Population mixing matrix
       - Current number of infected individuals
    2. Converts expected infections to probabilities
    3. Samples actual new infections from a binomial distribution
    4. Updates population states:
       - Moves current infected to recovered (configurable recovery period)
       - Adds new infections to infected population
       - Removes new infections from susceptible population
    Parameters
    ----------
    model : object
        The simulation model containing population states and parameters
    verbose : bool, default=False
        Whether to print detailed information during execution
    params : InfectionParams | None, default=None
        Component-specific parameters. If None, will use default parameters
    Notes
    -----
    The infection process uses a configurable recovery period and seasonal
    transmission rate that varies sinusoidally over time.
    """
    def __init__(self, model: BaseLaserModel, verbose: bool = False, params: InfectionParams | None = None) -> None:
        super().__init__(model, verbose)
        if params is None:
            params = InfectionParams()
        self.params = params
        self._mixing = None
    def __call__(self, model: BaseLaserModel, tick: int) -> None:
        # state counts
        states = model.patches.states
        # prevalence in each patch
        prevalence = states.I / states.sum(axis=0)  # I_j / N_j
        lambda_i = (
            self.params.beta_per_tick
            * (1 + self.params.seasonality * np.sin(2 * np.pi * (tick - self.params.season_start) / 26.0))
            * prevalence
        ) @ self.mixing
        prob = 1 - np.exp(-lambda_i)  # already per-susceptible
        dI = model.prng.binomial(states[0], prob).astype(states.dtype)
        # move all currently infected to recovered (using configurable recovery period)
        states[2] += states[1]
        states[1] = 0
        # update susceptible and infected populations
        states[1] += dI  # add new infections to I
        states[0] -= dI  # remove new infections from S
        return
    @property
    def mixing(self) -> np.ndarray:
        """Returns the mixing matrix, initializing if necessary"""
        if self._mixing is None:
            self._mixing = init_gravity_diffusion(self.model.scenario, self.params.mixing_scale, self.params.distance_exponent)
        return self._mixing
    @mixing.setter
    def mixing(self, mixing: np.ndarray) -> None:
        """Sets the mixing matrix"""
        self._mixing = mixing
    def _initialize(self, model: BaseLaserModel) -> None:
        """Initializes the mixing component"""
        self.mixing = init_gravity_diffusion(model.scenario, self.params.mixing_scale, self.params.distance_exponent)