Source code for laser.measles.abm.components.process_sia_calendar
from collections.abc import Callable
import numpy as np
import polars as pl
from pydantic import BaseModel
from pydantic import Field
from laser.measles.abm.model import ABMModel
from laser.measles.base import BaseLaserModel
from laser.measles.base import BasePhase
class SIACalendarParams(BaseModel):
"""Parameters specific to the SIA calendar component."""
model_config = {"arbitrary_types_allowed": True}
sia_efficacy: float = Field(0.9, description="Fraction of susceptibles to vaccinate in SIA", ge=0.0, le=1.0)
filter_fn: Callable[[str], bool] = Field(lambda x: True, description="Function to filter which nodes to include in aggregation")
aggregation_level: int = Field(3, description="Number of levels to use for aggregation (e.g., 3 for country:state:lga)")
sia_schedule: pl.DataFrame = Field(description="DataFrame containing SIA schedule information")
date_column: str = Field("date", description="Name of the column containing SIA dates")
group_column: str = Field("id", description="Name of the column containing group identifiers")
[docs]
class SIACalendarProcess(BasePhase):
"""
Phase for implementing Supplementary Immunization Activities (SIAs) based on a calendar schedule.
This component:
1. Groups nodes by geographic level using the same aggregation schema as CaseSurveillanceTracker
2. Implements SIAs at scheduled times by vaccinating individual agents
3. Uses the model's current_date to determine when to implement SIAs
4. Applies vaccination with configurable efficacy rate to individual agents
Parameters
----------
model : ABMModel
The ABM simulation model containing agents, patches, and parameters
verbose : bool, default=False
Whether to print verbose output during simulation
params : Optional[SIACalendarParams], default=None
Component-specific parameters. If None, will use default parameters
Notes
-----
- SIA efficacy determines the fraction of susceptibles that get vaccinated
- Individual agents are randomly selected for vaccination based on efficacy
- SIAs are implemented when the model's current_date has passed the scheduled date
- Vaccination moves agents from susceptible (S=0) to recovered (R=3) state
- Both individual agent states and patch-level state aggregations are updated
- Each SIA is implemented exactly once
"""
def __init__(self, model: ABMModel, verbose: bool = False, params: SIACalendarParams | None = None) -> None:
super().__init__(model, verbose)
if params is None:
raise ValueError("SIACalendarParams must be provided")
self.params = params
self._validate_params()
# Extract node IDs and create mapping for filtered nodes
self.node_mapping = {}
for node_idx, node_id in enumerate(model.scenario["id"]):
if self.params.filter_fn(node_id):
# Create geographic grouping key
group_key = ":".join(node_id.split(":")[: self.params.aggregation_level])
if group_key not in self.node_mapping:
self.node_mapping[group_key] = []
self.node_mapping[group_key].append(node_idx)
# Track which SIAs have been implemented
self.implemented_sias = set()
if self.verbose:
print(f"SIACalendar initialized with {len(self.node_mapping)} groups")
def _validate_params(self) -> None:
"""Validate component parameters."""
if self.params.aggregation_level < 1:
raise ValueError("aggregation_level must be at least 1")
# Validate SIA schedule DataFrame
required_columns = [self.params.group_column, self.params.date_column]
if not all(col in self.params.sia_schedule.columns for col in required_columns):
raise ValueError(f"sia_schedule must contain columns: {required_columns}")
[docs]
def __call__(self, model: ABMModel, tick: int) -> None:
# Check for SIAs scheduled for dates up to and including the current date
current_date = model.current_date
sia_schedule = self.params.sia_schedule.filter(pl.col(self.params.date_column) <= current_date)
# Apply SIAs to each scheduled group
for row in sia_schedule.iter_rows(named=True):
group_key = row[self.params.group_column]
scheduled_date = row[self.params.date_column]
# Create a unique identifier for this SIA
sia_id = f"{group_key}_{scheduled_date}"
# Skip if this SIA has already been implemented
if sia_id in self.implemented_sias:
continue
if group_key in self.node_mapping:
node_indices = self.node_mapping[group_key]
vaccinated_count = self._vaccinate_agents(model, node_indices)
# Mark this SIA as implemented
self.implemented_sias.add(sia_id)
if self.verbose and vaccinated_count > 0:
print(
f"Date {current_date}: Implementing SIA for {group_key} (scheduled for {scheduled_date}) - vaccinated {vaccinated_count} individuals"
)
def _vaccinate_agents(self, model: ABMModel, patch_indices: list[int]) -> int:
"""
Vaccinate agents in specified patches.
Args:
model: The ABM model
patch_indices: List of patch indices to vaccinate in
Returns:
Total number of agents vaccinated
"""
people = model.people
patches = model.patches
# Find susceptible agents in target patches
susceptible_mask = people.state[: people.count] == 0 # Susceptible state
patch_mask = np.isin(people.patch_id[: people.count], patch_indices)
target_mask = susceptible_mask & patch_mask
target_indices = np.where(target_mask)[0]
if len(target_indices) == 0:
return 0
# Randomly select agents to vaccinate based on efficacy
num_to_vaccinate = int(len(target_indices) * self.params.sia_efficacy)
if num_to_vaccinate == 0:
return 0
# Randomly select agents without replacement
selected_indices = np.random.choice(target_indices, size=num_to_vaccinate, replace=False)
# Update agent states to recovered (R = 3)
recovered_state = model.params.states.index("R")
people.state[selected_indices] = recovered_state
# Update patch-level state counts
for patch_idx in patch_indices:
# Count vaccinated agents in this patch
patch_vaccinated = np.sum(people.patch_id[selected_indices] == patch_idx)
if patch_vaccinated > 0:
patches.states.S[patch_idx] -= patch_vaccinated
patches.states.R[patch_idx] += patch_vaccinated
return len(selected_indices)
def initialize(self, model: BaseLaserModel) -> None:
pass
[docs]
def get_sia_schedule(self) -> pl.DataFrame:
"""
Get the SIA schedule.
Returns
-------
pl.DataFrame
DataFrame with columns:
- {group_column}: Group identifier
- {date_column}: Scheduled date for SIA
"""
return self.params.sia_schedule