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 LaserFrame with an
array associated with each attribute (rather than a single array of pointers to an
agent class structure).
This tutorial covers important details on model initialization 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.
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 (for example, age distribution and susceptibility) is dealt with by the components you add. However, to be able to maintain the cross-over with the compartmental models we assume no vital dynamics unless otherwise provided. For example, if we initialize with the rough equilibrium distribution between S and R the LaserFrame is initialized appropriately with 150k agents.
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:
from laser.measles.abm.components import InfectionProcess, InfectionSeedingProcess
def make_model():
model = ABMModel(scenario=scenario, params=params)
model.components = [InfectionSeedingProcess, InfectionProcess]
model.run()
return model
model = make_model()
print("People LaserFrame:")
print(model.people)
print("Model components:")
print(model.components)
|████████████████████████████████████████| 365/365 [100%] in 0.2s (2022.35/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
model = make_model()
vd = ConstantPopProcess(model)
print(f"Capacity for a constant population size: {vd.calculate_capacity(model)}")
#
|████████████████████████████████████████| 365/365 [100%] in 0.2s (2052.02/s)
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.
from laser.measles.abm.components import VitalDynamicsProcess
model = make_model()
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")
|████████████████████████████████████████| 365/365 [100%] in 0.2s (2035.14/s)
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 (for example, susceptibility, etimer, itimer) and constructs the
arrays with the correct size.
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