Skip to content

How to create a custom component

This guide walks through creating a custom component for laser-measles. Components are modular pieces that execute once per timestep to modify model state or record metrics.

Decide what your component does

Components fall into two categories:

  • Process components modify model state — they move people between compartments, apply interventions, or update demographics. Inherit from BasePhase.
  • Tracker components record metrics without modifying state — they log compartment counts, case counts, or other outputs. Inherit from BaseComponent.

Create a process component

A process component needs three things: an __init__ method, an _initialize method (called once before the simulation loop), and a __call__ method (called every timestep).

Here is a minimal example — a component that applies a one-time pulse vaccination campaign at a specified tick:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from laser.measles.base import BaseLaserModel, BasePhase

class PulseVaccinationProcess(BasePhase):
    """Vaccinates a fixed fraction of susceptibles at a specified tick."""

    def __init__(self, model, verbose=False, coverage=0.5, target_tick=100):
        super().__init__(model, verbose)
        self.coverage = coverage
        self.target_tick = target_tick

    def _initialize(self, model: BaseLaserModel) -> None:
        # Called once before model.run() starts the simulation loop.
        # Use this for setup that depends on other components being present.
        pass

    def __call__(self, model: BaseLaserModel, tick: int) -> None:
        # Called every timestep. This is where the work happens.
        if tick != self.target_tick:
            return

        states = model.patches.states
        vaccinated = (states.S * self.coverage).astype(states.dtype)
        states.S -= vaccinated
        states.R += vaccinated

        if self.verbose:
            print(f"Tick {tick}: vaccinated {vaccinated.sum()} individuals")

Create a tracker component

Tracker components record data but do not modify model state. They inherit from BaseComponent and implement __call__ to collect data at each timestep.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import numpy as np
from laser.measles.base import BaseLaserModel, BasePhase

class IncidenceTracker(BasePhase):
    """Records new infections per patch at each timestep."""

    def __init__(self, model, verbose=False):
        super().__init__(model, verbose)
        self.incidence = []
        self._prev_I = None

    def _initialize(self, model: BaseLaserModel) -> None:
        self._prev_I = model.patches.states.I.copy()

    def __call__(self, model: BaseLaserModel, tick: int) -> None:
        current_I = model.patches.states.I
        new_infections = np.maximum(current_I - self._prev_I, 0)
        self.incidence.append(new_infections.copy())
        self._prev_I = current_I.copy()

Add your component to a model

Add custom components the same way as built-in components — either in the component list or with add_component:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from laser.measles.biweekly import BiweeklyModel, BiweeklyParams
from laser.measles.biweekly.components import InfectionSeedingProcess, InfectionProcess

params = BiweeklyParams(num_ticks=260, seed=42, start_time="2000-01")
model = BiweeklyModel(scenario, params)

# Add built-in components first
model.add_component(InfectionSeedingProcess)
model.add_component(InfectionProcess)

# Add your custom component
model.add_component(PulseVaccinationProcess)

model.run()

Or set all components at once:

1
2
3
4
5
6
model.components = [
    InfectionSeedingProcess,
    InfectionProcess,
    PulseVaccinationProcess,
    IncidenceTracker,
]

Use Pydantic for component parameters

For components with multiple configurable parameters, use a Pydantic model for validation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from pydantic import BaseModel, Field
from laser.measles.base import BaseLaserModel, BasePhase

class PulseVaccinationParams(BaseModel):
    coverage: float = Field(default=0.5, ge=0.0, le=1.0, description="Fraction of susceptibles to vaccinate")
    target_tick: int = Field(default=100, ge=0, description="Tick at which to apply vaccination")

class PulseVaccinationProcess(BasePhase):
    """Vaccinates a fixed fraction of susceptibles at a specified tick."""

    def __init__(self, model, verbose=False, params=None):
        super().__init__(model, verbose)
        self.params = params if params is not None else PulseVaccinationParams()

    def _initialize(self, model: BaseLaserModel) -> None:
        pass

    def __call__(self, model: BaseLaserModel, tick: int) -> None:
        if tick != self.params.target_tick:
            return

        states = model.patches.states
        vaccinated = (states.S * self.params.coverage).astype(states.dtype)
        states.S -= vaccinated
        states.R += vaccinated

Access other components

If your component needs to read state from another component (e.g., checking whether an infection component exists), use model.get_instance():

1
2
3
4
def _initialize(self, model: BaseLaserModel) -> None:
    trackers = model.get_instance(StateTracker)
    if trackers[0] is not None:
        self.state_tracker = trackers[0]

Component ordering considerations

Place your component in the list based on what it needs:

  • If it reads compartment counts that other components modify, place it after those components.
  • If it modifies state that downstream components use, place it before them.
  • Trackers generally go last so they record the final state for each timestep.

See also