ABM Model Introduction#
This tutorial serves as an introduction to the ABM model in laser-measles
As introduced in the “model structure” tutorial, the abm model keeps track of individual agents located in the people
LaserFrame. In order to improve performance, laser-measles stores agent attributes in a single lass with an array associated with each attribute (rather than a e.g., single array of pointers to an agent class structure).
This tutorial covers important details on model initializaton and setup. When we first setup the model you’ll note that the capacity and count of the LaserFrame is 1 even though the scenario has an initial population of 150k.
[1]:
from laser_measles.abm.model import ABMModel
from laser_measles.scenarios.synthetic import two_patch_scenario
from laser_measles.abm.model import ABMParams
scenario = two_patch_scenario()
params = ABMParams(seed=20250314, start_time="2000-01", num_ticks=365)
model = ABMModel(scenario=scenario, params=params)
print("Initial scenario:")
print(scenario.head())
print("Initial people LaserFrame:")
print(model.people)
Initial scenario:
shape: (2, 5)
┌─────────┬────────┬──────┬──────┬──────┐
│ id ┆ pop ┆ lat ┆ lon ┆ mcv1 │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ i64 ┆ f64 ┆ f64 ┆ f64 │
╞═════════╪════════╪══════╪══════╪══════╡
│ patch_1 ┆ 100000 ┆ 40.0 ┆ 4.0 ┆ 0.0 │
│ patch_2 ┆ 50000 ┆ 34.0 ┆ 10.0 ┆ 0.0 │
└─────────┴────────┴──────┴──────┴──────┘
Initial people LaserFrame:
┌─ PeopleLaserFrame (capacity: 1, count: 1) ──────┐
├─ patch_id uint16 (1,)
├─ state uint8 (1,)
└─ susceptibility float32 (1,)
└─────────────────────────────────────────────────┘
The reason for this is that initialization of the agents (e.g. age distribution and susceptibility) is dealt with by the components you add. However, to be able to maintin the cross-over with the compartmental models we assume no vital dynamics unless otherwise provided. For example, if we initialize with the rought equilibrium distribution between S and R the laserframe is initialized appropriatedly with 150k agents.
[2]:
from laser_measles.abm.components import ConstantPopProcess
model = ABMModel(scenario=scenario, params=params)
model.add_component(ConstantPopProcess)
print(model.people)
┌─ PeopleLaserFrame (capacity: 150000, count: 150000) ─┐
├─ date_of_birth int32 (150000,)
├─ patch_id uint16 (150000,)
├─ state uint8 (150000,)
└─ susceptibility float32 (150000,)
└──────────────────────────────────────────────────────┘
If, we run the model without adding a component that sets the vital dynamics then the NoBirthsProcess
is added by default:
[3]:
from laser_measles.abm.components import InfectionProcess, InfectionSeedingProcess
model = ABMModel(scenario=scenario, params=params)
model.components = [InfectionSeedingProcess, InfectionProcess]
model.run()
print("People LaserFrame:")
print(model.people)
print("Model components:")
print(model.components)
|████████████████████████████████████████| 365/365 [100%] in 0.3s (1284.55/s)
People LaserFrame:
┌─ PeopleLaserFrame (capacity: 150000, count: 150000) ─┐
├─ etimer uint16 (150000,)
├─ itimer uint16 (150000,)
├─ patch_id uint16 (150000,)
├─ state uint8 (150000,)
└─ susceptibility float32 (150000,)
└──────────────────────────────────────────────────────┘
Model components:
┌─ Components (count: 3) ─────────────────────────┐
├─ NoBirthsProcess
├─ InfectionSeedingProcess
└─ InfectionProcess
└─────────────────────────────────────────────────┘
One of the reasons the abm model waits to initialize the laserframe until a component with vital dynamics is added is because the capacity (or size) of the laserframe/arrays needs to be determined based on how the population is expected to grow over the course of the simulation. In order to manage this, a component that sets the vital dynamics has a ‘calculate_capacity` method that returns the requires array size based on the duration of the simulation.
For example, the ConstantPopProcess
recycles entries in the arrays so that the array sizes can remain the same
[4]:
vd = ConstantPopProcess(model)
print(f"Capacity for a constant population size: {vd.calculate_capacity(model)}")
#
Capacity for a constant population size: 150000
In the case the population will grow in size then the capacity of the laserframe also grows. The LaserFrame has some special methods that differentiate between the size of the array holding agents that have entered the simulation and what is the full size of the arrays in memory.
The VitalDynamicsProcess
is a constant birth / mortality rate with no enforced age structure.
[5]:
from laser_measles.abm.components import VitalDynamicsProcess
vd = VitalDynamicsProcess(model)
print(f"Capacity for the {model.params.num_ticks} tick simulation: {vd.calculate_capacity(model)}")
print(f"len(model.people): {len(model.people)} at the start of the simulation")
Capacity for the 365 tick simulation: 156060
len(model.people): 150000 at the start of the simulation
During the instantiation of the component, the calculate_capacity
method is utilized to re-initialize the laserframe with the correct capacity. The ABMModel
contains a method, initialize_people_capacity
that goes through the existing laserframe attributes (e.g., susceptibility, etimer, itimer) and constructs the arrays with the correct size.
[6]:
help(model.initialize_people_capacity)
Help on method initialize_people_capacity in module laser_measles.abm.model:
initialize_people_capacity(capacity: int, initial_count: int = -1) -> None method of laser_measles.abm.model.ABMModel instance
Initialize the people LaserFrame with a new capacity while preserving all properties.
This method uses the factory method from BasePeopleLaserFrame to create a new
instance of the same type with the specified capacity, copying all properties
from the existing instance.
Args:
capacity: The new capacity for the people LaserFrame