Model structure#

This tutorial compares the structure of compartmental and agent-based models, focusing on their LaserFrame data structures and how they operate.

Overview#

laser-measles provides two primary modeling approaches:

  • Compartmental Model: Population-level SEIR dynamics using aggregated patch data

  • ABM Model: Individual-level simulation with stochastic agents

The key difference lies in their data organization and LaserFrame structures.

Patches#

Patches exist for both the compartmental and ABM models and track the spatial data and aggregates in the model. The patches use a BasePatchLaserFrame (or child class) for population-level aggregates:

[1]:
import polars as pl

from laser_measles.compartmental import CompartmentalModel
from laser_measles.compartmental.components import CaseSurveillanceParams
from laser_measles.compartmental.components import CaseSurveillanceTracker
from laser_measles.compartmental.params import CompartmentalParams

# Create a simple scenario
scenario = pl.DataFrame(
    {"id": ["1", "2", "3"], "pop": [1000, 2000, 1500], "lat": [40.0, 41.0, 42.0], "lon": [-74.0, -73.0, -72.0], "mcv1": [0.0, 0.0, 0.0]}
)

# Initialize compartmental model
params = CompartmentalParams(num_ticks=100)
comp_model = CompartmentalModel(scenario, params)

# Examine the patch structure
print("Compartmental model patches:")
print(f"Shape: {comp_model.patches.states.shape}")
print(f"State names: {comp_model.patches.states.state_names}")
print(f"Initial S compartment: {comp_model.patches.states.S}")
print(f"Total population: {comp_model.patches.states.S.sum()}")

# You can also print the model to get some info:
print("Compartmental model 'out of the box':")
print(comp_model)

# Create a CaseSurveillanceTracker to monitor infections
case_tracker = lm.create_component(
    CaseSurveillanceTracker,
    CaseSurveillanceParams(detection_rate=1.0),  # 100% detection for accurate infection counting
)

# Add transmission and surveillance to the model
from laser_measles.compartmental.components import TransmissionProcess

comp_model.add_component(TransmissionProcess)
comp_model.add_component(case_tracker)

print("\nCompartmental model with surveillance:")
print(comp_model)

# Run the simulation
comp_model.run()

# Access infection data
case_tracker_instance = comp_model.get_instance(CaseSurveillanceTracker)[0]
comp_infections_df = case_tracker_instance.get_dataframe()
print(f"\nCompartmental model total infections: {comp_infections_df['cases'].sum()}")
Compartmental model patches:
Shape: (4, 3)
State names: ['S', 'E', 'I', 'R']
Initial S compartment: [1000 2000 1500]
Total population: 4500
Compartmental model 'out of the box':
<CompartmentalModel>:
name: compartmental
patches:
┌─ PatchLaserFrame (capacity: 3, count: 3) ───────┐
└─ states          uint32       (4, 3)
└─────────────────────────────────────────────────┘>
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[1], line 29
     26 print(comp_model)
     28 # Create a CaseSurveillanceTracker to monitor infections
---> 29 case_tracker = lm.create_component(
     30     CaseSurveillanceTracker,
     31     CaseSurveillanceParams(detection_rate=1.0),  # 100% detection for accurate infection counting
     32 )
     34 # Add transmission and surveillance to the model
     35 from laser_measles.compartmental.components import TransmissionProcess

NameError: name 'lm' is not defined

Key Features of patches (e.g., BasePatchLaserFrame):#

  • ``states`` property: StateArray with shape (num_states, num_patches)

  • Attribute access: states.S, states.E, states.I, states.R

  • Population aggregates: Each patch contains total counts by disease state

  • Spatial organization: Patches represent geographic locations

People#

In addition to a patch, the ABM uses people (e.g., BasePeopleLaserFrame) for individual agents:

[2]:
import laser_measles as lm
from laser_measles.abm import ABMModel
from laser_measles.abm.components import TransmissionProcess
from laser_measles.abm.params import ABMParams

# Initialize ABM model
abm_params = ABMParams(num_ticks=100)
abm_model = ABMModel(scenario, abm_params)

# Examine the model
print("ABM model 'out of the box':")
print(abm_model)

# Now what if add a transmission?
abm_model.add_component(TransmissionProcess)
print("ABM model after adding transmission:")
print(abm_model)

# Add CaseSurveillanceTracker to ABM model
abm_case_tracker = lm.create_component(
    lm.abm.components.CaseSurveillanceTracker, lm.abm.components.CaseSurveillanceParams(detection_rate=1.0)
)
abm_model.add_component(abm_case_tracker)

print("\nABM model with surveillance:")
print(abm_model)

# Run the simulation
abm_model.run()

# Access infection data
abm_case_tracker_instance = abm_model.get_instance(lm.abm.components.CaseSurveillanceTracker)[0]
abm_infections_df = abm_case_tracker_instance.get_dataframe()
print(f"\nABM model total infections: {abm_infections_df['cases'].sum()}")
ABM model 'out of the box':
<ABMModel>:
name: abm
patches:
┌─ PatchLaserFrame (capacity: 3, count: 3) ───────┐
└─ states          uint32       (4, 3)
└─────────────────────────────────────────────────┘
people:
┌─ PeopleLaserFrame (capacity: 1, count: 1) ──────┐
├─ patch_id        uint16       (1,)
├─ state           uint8        (1,)
└─ susceptibility  float32      (1,)
└─────────────────────────────────────────────────┘>
ABM model after adding transmission:
<ABMModel>:
name: abm
patches:
┌─ PatchLaserFrame (capacity: 3, count: 3) ───────┐
├─ incidence       uint32       (3,)
└─ states          uint32       (4, 3)
└─────────────────────────────────────────────────┘
people:
┌─ PeopleLaserFrame (capacity: 1, count: 1) ──────┐
├─ etimer          uint16       (1,)
├─ itimer          uint16       (1,)
├─ patch_id        uint16       (1,)
├─ state           uint8        (1,)
└─ susceptibility  float32      (1,)
└─────────────────────────────────────────────────┘>

ABM model with surveillance:
<ABMModel>:
name: abm
patches:
┌─ PatchLaserFrame (capacity: 3, count: 3) ───────┐
├─ incidence       uint32       (3,)
└─ states          uint32       (4, 3)
└─────────────────────────────────────────────────┘
people:
┌─ PeopleLaserFrame (capacity: 1, count: 1) ──────┐
├─ etimer          uint16       (1,)
├─ itimer          uint16       (1,)
├─ patch_id        uint16       (1,)
├─ state           uint8        (1,)
└─ susceptibility  float32      (1,)
└─────────────────────────────────────────────────┘>
|████████████████████████████████████████| 100/100 [100%] in 0.0s (10092.64/s)

ABM model total infections: 0

Key Features of BasePeopleLaserFrame:#

  • Individual agents: Each row represents one person

  • Agent properties: patch_id, state, susceptibility, active

  • Dynamic capacity: Can grow/shrink as agents are born/die

  • Stochastic processes: Each agent processed individually

Key Differences#

Aspect

Compartmental

ABM

Data Structure

BasePatchLaserFrame

BasePeopleLaserFrame

Population Storage

Aggregated counts by patch

Individual agents

State Representation

states.S[patch_id]

people.state[agent_id]

Spatial Organization

Patch-level mixing matrix

Agent patch assignment

Transitions

Binomial sampling

Individual stochastic events

Performance

Faster (fewer calculations)

Slower (more detailed)

Memory Usage

Lower (aggregates)

Higher (individual records)

When to Use Each Model#

Use a Patches model only (e.g., Compartmental Model) when:

  • Analyzing population-level dynamics

  • Running many scenarios quickly

  • Interested in aggregate outcomes

  • Working with large populations

Use a Patches+People Model (e.g., ABM Model) when:

  • Modeling individual heterogeneity

  • Studying contact networks

  • Tracking individual histories

  • Need detailed stochastic processes

Both models share the same component architecture and can use similar initialization and analysis tools, making it easy to switch between approaches.