Troubleshooting
This page documents common pitfalls when writing laser-measles models.
If you encounter unexpected ImportError, tracker shape mismatches, or
component configuration errors, check the items below first.
These issues occur frequently when users are learning the component system or adapting code between the ABM, biweekly, and compartmental models.
Imports and namespaces
Where does create_component come from?
create_component is available from both the top-level
laser.measles namespace and the shared laser.measles.components
package, regardless of which model type you are using.
It lives in the shared components package because it works with all model types (ABM, biweekly, and compartmental), and is re-exported at the top level for convenience.
1 2 3 4 5 6 7 8 | |
How do I access component classes and their parameter classes?
Import component classes and their parameter classes directly from the
subpackage. Each subpackage's __init__ re-exports everything from its
components module, so all concrete components are available at the
top level.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | |
The same pattern applies to biweekly and compartmental — import directly from
laser.measles.biweekly or laser.measles.compartmental.
Warning
Component and param classes are model-specific. InfectionParams,
SIACalendarParams, NoBirthsProcess, and similar classes have different
fields per model type and live in their respective subpackage. Do not import
them from the shared laser.measles.components package or from the wrong
model subpackage:
Do not import InfectionParams from laser.measles.components or from
laser.measles directly — those paths raise ImportError. Always import
model-specific classes from the correct model subpackage
(laser.measles.abm, laser.measles.biweekly, or
laser.measles.compartmental).
Do not import scenario helpers (single_patch_scenario,
two_patch_scenario, two_cluster_scenario) from laser.measles.abm
or any model subpackage — they are not there. Import them from
laser.measles or laser.measles.scenarios:
1 2 3 4 5 6 7 8 9 | |
NoBirthsProcess and SIACalendarProcess exist in the ABM subpackage only —
there is no equivalent in the biweekly or compartmental subpackages.
There is no lm object in laser.measles
The top-level laser.measles package does not export a convenience
object such as lm.
Some tutorials or AI-generated examples use this alias, but it is not part of the package API.
Do not try from laser.measles import lm — it raises ImportError.
Import the specific model class directly:
1 2 | |
Scenario helpers are in laser.measles or laser.measles.scenarios, not in subpackages
Scenario generators (single_patch_scenario, two_patch_scenario,
two_cluster_scenario, etc.) are exported from laser.measles and
laser.measles.scenarios. They are not available from the model-specific
subpackages (laser.measles.abm, laser.measles.biweekly, etc.).
Do not import scenario helpers from laser.measles.abm,
laser.measles.biweekly, or laser.measles.compartmental — they are
not defined there and will raise ImportError. Always import them from
laser.measles or laser.measles.scenarios:
1 2 3 4 | |
Model construction
model.components is assigned after construction
The model constructors only accept scenario and params.
Components must be attached by assigning to model.components after
the model object is created.
1 2 3 4 5 6 7 8 9 10 | |
The model internally instantiates the component classes when the list is assigned.
Do not pass components as a constructor argument — it raises
TypeError: unexpected keyword argument "components". Always assign
model.components as a separate statement after construction.
This applies to all three model types:
ABMModelBiweeklyModelCompartmentalModel
Components are classes, not instances
Components should be passed as classes, not instantiated objects.
The model constructs the component instances internally.
1 2 3 4 | |
Do not instantiate components before adding them. Neither
model.components = [InfectionProcess()] nor
model.add_component(InfectionProcess()) works — the model
constructs component instances internally. Passing an already-created
instance causes TypeError: 'InfectionProcess' object is not callable.
If parameters are needed, use create_component:
1 2 3 4 5 6 | |
VitalDynamicsProcess must be the first component
When using vital dynamics (births and deaths), VitalDynamicsProcess must
be the first component added to the model.
This is because VitalDynamicsProcess calls calculate_capacity to
pre-allocate the LaserFrame with enough headroom for the births that will
occur over the simulation. If any other component is added first, the
LaserFrame is already initialized at the wrong size, which causes a crash.
1 2 3 4 5 6 | |
Do not add InitializeEquilibriumStatesProcess or any other component
before VitalDynamicsProcess. If VitalDynamicsProcess is not first,
the LaserFrame is already initialized at the wrong capacity and will
crash at runtime.
Do NOT add TransmissionProcess separately when using InfectionProcess (ABM)
InfectionProcess already instantiates TransmissionProcess internally and
registers the etimer property on the population. Adding TransmissionProcess
as a separate component causes a ValueError: Property 'etimer' already exists.
Do not add TransmissionProcess separately — InfectionProcess already
creates it internally. Adding TransmissionProcess before or alongside
InfectionProcess causes ValueError: Property 'etimer' already exists.
1 2 | |
The same applies to any component that is a sub-component of another: check the docs to see which components are stand-alone vs. internally managed.
Custom components added via add_component must accept verbose
ABMModel.add_component(ComponentClass) instantiates the class as
ComponentClass(model, verbose=False). Any custom component class must
accept verbose as a keyword argument or the framework raises:
1 | |
Always include verbose=False in custom component __init__:
1 2 3 4 | |
Scenario DataFrames
Scenario DataFrame must contain required columns
All models expect the scenario DataFrame to contain at least the following columns:
id— patch identifierlat— latitudelon— longitudepop— population sizemcv1— routine vaccination coverage
Missing columns will trigger a validation error when constructing the model.
1 2 3 4 5 6 7 | |
lat and lon columns must be Float64, not Int64
The scenario schema requires lat and lon to be floating-point.
Using Python's range() or integer literals produces Int64 columns,
which fail Polars schema validation when the model is constructed.
Do not use [0] * N or list(range(N)) for lat/lon columns —
Python integer lists produce Int64 which fails schema validation.
Always use float literals:
1 2 3 4 5 6 7 8 | |
Scenario id must be a string; pop must be Int32
Two dtype requirements that produce cryptic errors if violated:
id must be a string (str / Utf8), not an integer.
Python list comprehensions like [0, 1, 2] produce Int64, which fails
schema validation. Use string patch IDs:
Do not use integer lists for id — [0, 1, 2] produces Int64 which
fails schema validation. Always use string patch IDs:
1 2 | |
pop (and all integer columns) must be Int32, not the default Int64.
Python integer lists and np.array(...) without a dtype both produce Int64:
Do not use plain Python integer lists for pop — [100_000, ...]
produces Int64 which fails schema validation. Use np.array(..., dtype=np.int32):
1 2 3 4 5 6 7 8 9 10 11 12 | |
The scenario helper functions (single_patch_scenario, two_patch_scenario, etc.)
handle these dtypes correctly and are the safest way to build test scenarios.
Scenario pop column must be integer (Int32), not float
The scenario DataFrame validator requires pop to be an integer type.
Passing a float column raises:
1 | |
Cast pop to Int32 when building a scenario:
1 2 3 4 5 6 7 8 9 10 11 | |
Use laser.measles.scenarios.synthetic for test scenarios
The synthetic module provides ready-made scenario DataFrames for
testing and development. It is available via several import paths:
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
Each function returns a polars.DataFrame with all required columns
(id, lat, lon, pop, mcv1) already populated. Pass it
directly to any model constructor:
1 2 3 4 5 6 | |
Warning
The patch IDs returned by the helper functions are 1-indexed, not 0-indexed:
single_patch_scenario()→id = "patch_1"(not"patch_0")two_patch_scenario()→id = ["patch_1", "patch_2"]
If you pass target_patches=["patch_0"] to InfectionSeedingParams when
using a helper-built scenario, the model will raise:
1 | |
The safest approach is to omit target_patches entirely — it defaults
to seeding all patches, which is correct for single-patch scenarios:
1 2 3 4 5 6 7 8 9 10 11 | |
Available helpers: single_patch_scenario, two_patch_scenario,
two_cluster_scenario, satellites_scenario. See the
API reference for full parameter details.
two_cluster_scenario returns 100 patches by default (2 × 50)
two_cluster_scenario(n_nodes_per_cluster=50) creates 100 patches (2
clusters × 50 nodes each). A per-patch StateTracker will have shape
(n_states, n_ticks, 100). Using a global tracker and indexing [-1]
gives shape (n_states,) which cannot be divided by a 100-element pop array.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | |
For a smaller scenario pass n_nodes_per_cluster:
1 | |
StateTracker
StateTracker output shape depends on aggregation_level
The StateTracker component stores time-series data differently depending
on how it is configured.
Default behavior (global aggregation)
Adding StateTracker without any params (or with aggregation_level=-1) sums
across all patches. Do not pass aggregation_level=0 or aggregation_level=1
when you want global results — those activate per-patch or per-region tracking
and will produce multi-dimensional arrays.
Arrays are 1-D with shape:
1 | |
1 2 3 | |
Patch-level tracking
If aggregation_level=0 is used, the tracker stores values per patch
(for flat patch IDs with no ":" hierarchy).
Arrays become 2-D with shape:
1 | |
1 2 3 | |
Retrieve the tracker instance after model.run():
1 | |
Retrieval of results from StateTracker
The StateTracker component does not expose a .data, .results,
or .to_polars() attribute. These names do not exist.
After model.run(), retrieve the tracker instance with
model.get_instance("StateTracker")[0] and access the time-series arrays
directly as properties.
Global tracker (default, aggregation_level=-1):
1 2 3 4 5 6 7 | |
Per-patch tracker (aggregation_level=0):
StateTrackerParams is available from all model subpackages:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
Global + per-patch together (add both, retrieve by index):
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
The following attributes do not exist on any tracker and will raise
AttributeError: tracker.data, tracker.results, tracker.to_polars(),
tracker.df. Use get_dataframe() for global trackers or .state_tracker
for per-patch trackers.
StateTracker values are StateArray objects, not plain Python scalars
When you index into a tracker's .S, .I, .R (etc.) arrays you get a
StateArray, not a float. Passing a StateArray to an f-string format spec
(e.g. f"{val:.4f}") raises TypeError: unsupported format string.
Always extract a Python scalar first:
Do not use tracker.I[tick] directly in f-string format specs like
f"{frac:.4f}" — StateArray does not support format specs and raises
TypeError.
1 2 3 | |
For per-patch trackers (aggregation_level=0) the shape is
(n_states, n_ticks, n_patches) — index with [state_idx, tick, patch_idx]
and wrap with int() or float() before arithmetic or formatting.
Cast NumPy scalars before building a Polars DataFrame
Tracker arrays are NumPy arrays, so operations like .max() return
NumPy scalar types (np.int64, np.float64).
Polars expects Python primitive types when constructing row-oriented
DataFrames. Passing NumPy scalars can trigger TypeError or
DataOrientationWarning.
Do not pass NumPy scalar results (e.g. tracker.I[:, p].max()) directly
to Polars DataFrame constructors — wrap with int() or call .item():
1 2 | |
An alternative is to use .item():
1 | |
Per-patch attack rates from StateTracker (multi-patch models)
When using a per-patch tracker (aggregation_level=0), the raw array has
shape (n_states, n_ticks, n_patches). To compute attack rates per patch
at the end of a run:
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 | |
Key rule: the number of patches in the scenario must equal n_patches
in the tracker array. Do not mix a 100-patch scenario with a tracker
configured for 2 patches, or vice versa.
lookup_state_idx does not exist — use params.states.index()
There is no lookup_state_idx function exported from laser.measles. To find
state indices, use the states list on the model params:
1 2 3 4 | |
For the biweekly model the default order is ['S', 'I', 'R'] (indices 0, 1, 2).
Tick granularity and time
Tick granularity: Daily vs. biweekly
ABMModel and CompartmentalModel use daily ticks (1 tick = 1 day).
BiweeklyModel uses 14-day ticks (1 tick = 2 weeks, 26 ticks = 1 year).
Scale num_ticks accordingly:
1 2 3 4 | |
SIA schedule date column must use datetime.date values, not strings
SIACalendarProcess filters the schedule by comparing a polars date column
to the current simulation date. If the column contains Python str values
(e.g. "2024-06-01") rather than datetime.date objects, polars raises:
1 | |
Build the schedule with datetime.date objects (or cast the column):
Do not use string literals like "2024-06-01" for the date column —
polars raises InvalidOperationError when comparing a string column to
a date. Always use datetime.date objects:
1 2 3 4 5 6 7 8 9 | |
SIACalendarParams.aggregation_level must be ≥ 1
SIACalendarParams validates that aggregation_level >= 1. Passing 0 raises:
1 | |
Use aggregation_level=1 for flat (single-level) hierarchies:
1 2 | |
For hierarchical IDs like "country:state:lga", use aggregation_level=3.
ABM-specific issues
model.people has date_of_birth, not age
The ABM people LaserFrame stores date_of_birth (in ticks), not an age
column. Accessing model.people.age raises AttributeError. To get age
in years at a given tick:
Do not access model.people.age — that attribute does not exist and raises
AttributeError. Use date_of_birth (stored in ticks) instead:
1 2 3 4 5 | |
Available people properties: state, susceptibility, patch_id,
active, date_of_birth, date_of_vaccination.
Read the age distribution data from AgePyramidTracker
AgePyramidTracker stores snapshots in its .age_pyramid dict, keyed by
date string ("YYYY-MM-DD"), with numpy histogram arrays as values.
There is no .counts attribute.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | |
The bin edges are set by AgePyramidTrackerParams.age_bins (in days).
Default bins come from pyvd.constants.MORT_XVAL[::2].
AgePyramidTracker.age_pyramid is a dict keyed by date strings — not an array
AgePyramidTracker.age_pyramid returns a dict[str, np.ndarray] where the
keys are date strings (e.g. "2000-01-01"). Indexing with an integer raises
KeyError:
Do not index age_pyramid with integers — it is a dict, not a list.
tracker.age_pyramid[0] raises KeyError: 0. Use dict access:
1 2 3 | |
Or iterate:
1 | |
AgePyramidTracker.age_pyramid key format — do not hard code date strings
The keys of age_pyramid are date strings generated internally and may not
match the format you expect (e.g. '2005-01-01' vs '2005-1-1'). Always
retrieve keys dynamically:
1 2 3 | |
Never do tracker.age_pyramid['2005-01-01'] — use keys[-1] instead.
Parameters and data types
Never pass a plain dict as params to create_component or model constructors
All params objects (ABMParams, BiweeklyParams, InfectionParams, etc.)
are Pydantic models, not plain dicts. Passing a dict raises
AttributeError immediately at model construction — BaseLaserModel.__init__
accesses params.verbose and params.start_time before any component runs.
Always instantiate the typed params class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
Do not write params={"beta": 1.2} — this will fail immediately at model
construction with AttributeError: 'dict' object has no attribute 'verbose'.
Do not use try/except import blocks or dict fallbacks for params
Do not write defensive import blocks like:
1 2 3 4 | |
and then fall back to passing a dict as params. These fallback patterns produce broken code. If an import fails, fix the import path rather than working around it. Consult How do I access component classes and their parameter classes? for correct import paths.
Polars with_column (singular) was removed — use with_columns
Older Polars had DataFrame.with_column(expr) (singular). Current Polars only
has with_columns(*exprs) (plural). Using the singular form raises:
1 2 | |
Always use the plural form with_columns (not with_column):
1 2 | |
numpy has no cummax — use np.maximum.accumulate
np.cummax does not exist in NumPy. The equivalent is np.maximum.accumulate:
Do not use np.cummax — it does not exist in NumPy and raises
AttributeError. Use np.maximum.accumulate instead:
1 2 | |
Mixing models
get_mixing_matrix() takes no arguments — pass scenario at construction
All mixing models (GravityMixing, RadiationMixing, etc.) accept the scenario
at construction time, not at get_mixing_matrix() call time. Calling
mixer.get_mixing_matrix(scenario) raises:
1 | |
Correct pattern:
1 2 3 4 | |
Multiprocessing
Multiprocessing workers must be defined at module level
Python's multiprocessing module uses pickle to transfer functions to worker
processes. Functions defined inside another function (closures / nested defs)
cannot be pickled and will raise:
1 | |
Define worker functions at the top level of the module, not inside another function:
Do not define worker functions inside another function (closures /
nested defs) — they cannot be pickled and raise
AttributeError: Can't pickle local object. Define the worker at the
top level of the module:
1 2 3 4 5 6 7 | |
Alternatively, use concurrent.futures.ProcessPoolExecutor with
functools.partial if you need to pass extra arguments.
See also
- Worked examples — complete runnable scripts for all three model types
- Components — component architecture and design
- How to create a custom component — guide to writing your own components
- Model types — overview of ABM, biweekly, and compartmental models
- Choosing a model type — decision guide for selecting the right model
- Snapshotting — save and resume simulations (common source of component-list errors)
- API reference — full class and parameter details