Usage
Overview
laser-measles is a spatial epidemiological modeling toolkit for measles transmission dynamics, built on the LASER framework. It provides a flexible, component-based architecture for disease simulation with support for multiple geographic scales and demographic configurations.
Key features include:
- Spatial modeling: Support for geographic regions with administrative boundaries and population distributions
- Multiple model types: ABM, Biweekly, and Compartmental models for different use cases
- Component-based architecture: Interchangeable disease dynamics components
- High-performance computing: Optimized data structures and Numba JIT compilation
- Type-safe parameters: Pydantic-based configuration management
Installation and setup
Install laser-measles using pip (requires Python 3.10+):
1 | |
For development installation with all dependencies (recommended: use uv for faster package management):
1 2 3 4 5 6 7 | |
Major dependencies:
laser-core>=1.0.0: Core LASER frameworkpydantic>=2.0: Parameter validation and serializationpolars>=1.0.0: High-performance data manipulationalive-progress>=3.0: Progress bars and status indicatorsrastertoolkit>=0.3.11: Raster data processing utilitiespatito>=0.8: Polars DataFrame validation
Model types
laser-measles provides three complementary modeling approaches, each optimized for different use cases:
- ABM (Agent-Based Model): Individual-level simulation with stochastic agents
- Biweekly Compartmental Model: Population-level SIR dynamics with 2-week timesteps
- Compartmental Model: Population-level SEIR dynamics with daily timesteps
Each model type offers different trade-offs between computational efficiency, temporal resolution, and modeling detail.
ABM (agent-based model)
The ABM model provides individual-level simulation with stochastic agents, allowing for detailed tracking of disease dynamics at the person level.
Key characteristics:
- Individual agents: Each person is represented as a discrete agent with properties like age, location, and disease state
- Daily timesteps: Fine-grained temporal resolution for precise modeling
- Stochastic processes: Individual-level probabilistic events for realistic variability
- Spatial heterogeneity: Agents can move between patches and have location-specific interactions
- Flexible demographics: Full support for births, deaths, aging, and migration
Example usage:
1 2 3 4 5 6 7 8 9 10 11 | |
Biweekly model
The biweekly model is a compartmental model optimized for fast simulation and parameter exploration with 2-week timesteps.
Key characteristics:
- Compartmental approach: SIR (Susceptible-Infected-Recovered) structure. The exposed (E) compartment is omitted because the 14-day timestep is comparable to measles' typical incubation period (~10-14 days), making the distinction between exposed and infectious states negligible at this temporal resolution. For detailed SEIR dynamics with explicit incubation periods, use the Compartmental Model with daily timesteps.
- Time resolution: 14-day fixed time steps (26 ticks per year)
- High performance: Uses Polars DataFrames for efficient data manipulation
- Stochastic sampling: Binomial sampling for realistic variability
- Policy analysis: Recommended for scenario building and intervention assessment
Example usage:
1 2 3 4 5 6 7 8 9 10 11 | |
Compartmental model
The compartmental model provides population-level SEIR dynamics with daily timesteps, optimized for parameter estimation and detailed outbreak modeling.
Key characteristics:
- Daily timesteps: Fine-grained temporal resolution (365 ticks per year)
- SEIR dynamics: Detailed compartmental structure with exposed compartment
- Parameter estimation: Recommended for fitting to surveillance data
- Outbreak modeling: Ideal for detailed temporal analysis of disease dynamics
- Deterministic core: Efficient ODE-based dynamics with optional stochastic elements
Example usage:
1 2 3 4 5 6 7 8 9 10 11 | |
Warning
All three model constructors require both scenario and params.
There is no default — omitting params raises TypeError immediately:
Do not pass only scenario to the constructor — omitting params
raises TypeError: missing 1 required positional argument: 'params'.
Always create the *Params object first, then pass both to the constructor:
1 2 3 4 5 6 7 8 9 | |
Components are added after construction via model.add_component().
params configures duration, seed, and start date — not components.
Demographics package
The demographics package provides comprehensive geographic data handling capabilities for spatial epidemiological modeling.
Core features:
- GADM Integration:
GADMShapefileclass for administrative boundary management - Raster processing:
RasterPatchGeneratorfor population distribution handling - Shapefile utilities: Functions for geographic data visualization and analysis
- Flexible geographic scales: Support from national to sub-district administrative levels
Key classes:
GADMShapefile: Manages administrative boundaries from GADM databaseRasterPatchParams: Configuration for raster-based population patchesRasterPatchGenerator: Creates population patches from raster dataget_shapefile_dataframe: Utility for shapefile data manipulationplot_shapefile_dataframe: Visualization functions for geographic data
Example usage:
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
Technical features
Pydantic integration
laser-measles uses Pydantic for type-safe parameter management, providing automatic validation and documentation.
Parameter classes:
ABMParams: Configuration for agent-based models with individual-level parametersBiweeklyParams: Configuration for biweekly models with epidemiological parametersCompartmentalParams: Configuration for compartmental models with daily dynamics
Component classes: Components come in "process" and "tracker" categories and each component has a corresponding parameter class. Each model (ABM, Biweekly, or Compartmental) has its own set of components. See the API reference section for more details.
Benefits:
- Type safety: Automatic validation of parameter types and ranges
- Documentation: Built-in parameter descriptions and constraints
- Serialization: JSON export/import of model configurations
- IDE support: Enhanced autocomplete and error detection
Example:
1 2 3 4 5 6 7 8 9 | |
High-performance computing
laser-measles is optimized for performance through several technical approaches:
LaserFrame architecture: High-performance array-based structure for agent populations, built on the LASER framework.
numba JIT compilation: Performance-critical operations implemented in numba for maximum speed.
Polars DataFrames: Efficient data manipulation using Polars for biweekly model operations with Arrow backend.
Component modularity: Modular architecture allows for selective component usage and optimization.
Progress tracking: Integrated progress bars using alive-progress for long-running simulations.
Python 3.10+ support: Optimized for modern Python features and performance improvements.
Component system
The component system provides a uniform interface for disease dynamics with interchangeable modules built on a hierarchical base class architecture.
Base architecture:
- BaseLaserModel: Abstract base class for all model types with common functionality
- BaseComponent: Base class for all components with standardized interface
- BasePhase: Components that execute every tick (inherit from BaseComponent)
- Inheritance-based design: Base components define shared functionality and abstract interfaces
Base component classes:
base_transmission.py: Base transmission/infection logicbase_vital_dynamics.py: Base births/deaths logicbase_importation.py: Base importation pressure logicbase_tracker.py: Base tracking/metrics logicbase_infection.py: Base infection state transitionsbase_tracker_state.py: Base state tracking functionality
Component naming convention:
- Process components:
process_*.py- Modify model state (births, deaths, infection, transmission) - Tracker components:
tracker_*.py- Record metrics and state over time
Component Creation Patterns:
1 2 3 4 5 6 7 8 9 10 | |
Complete worked examples
These end-to-end scripts are copy-paste runnable. Each one shows the full pattern — imports, scenario, params, model construction, component wiring, running, and result retrieval — with detailed inline comments on every line that commonly causes errors.
The three non-negotiable constructor facts
Warning
Read this before writing any model code.
These three facts are the source of the most common runtime failures. They apply to every model type without exception.
The only three model classes are ABMModel, BiweeklyModel, CompartmentalModel
Import them from their respective subpackages:
1 2 3 | |
Warning
The following names do not exist in the package and will raise
AttributeError or ImportError:
1 2 3 4 5 6 7 | |
There is no convenience shortcut. Always import from the subpackage.
The constructor signature is always Model(scenario, params)
1 2 | |
Warning
params is not optional. Calling the constructor with only a scenario
raises TypeError immediately, before the simulation runs:
1 2 3 | |
The *Params object is always the second positional argument.
It is mandatory — there is no default and no shortcut.
Passing simulation settings directly as keyword arguments also fails:
1 2 3 4 5 6 | |
Every simulation setting — duration, seed, start date, verbosity —
goes into the *Params object. Then the populated *Params
object is the second argument to the model constructor.
start_time must be "YYYY-MM", never "YYYY-MM-DD"
1 2 | |
Warning
Passing a full date string raises a Pydantic ValidationError at
construction time, before the simulation runs:
Do not pass a full date string like "2000-01-01" — it raises
ValidationError: start_time must be in 'YYYY-MM' format.
Example 1 — ABM: Single-patch outbreak with StateTracker
One population of 100,000 people, no births, outbreak seeded from
InfectionSeedingProcess, peak infectious tracked with StateTracker.
This is the minimal correct ABM script.
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | |
Example 2 — Biweekly: Five-patch endemic run with per-patch StateTracker
Five communities, births/deaths, importation, 5 years. Uses
BiweeklyModel (26 ticks per year) and a per-patch StateTracker
(aggregation_level=0) to read the infectious time series per community.
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | |
Example 3 — Compartmental: R0 sweep with InfectionParams
Single population, three different R0 values, 2-year runs. Shows how
to scale beta from the default to reach a target R0, using
CompartmentalModel and a per-patch StateTracker.
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | |
Gotchas & FAQ
This section 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.
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.
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
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 | |
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 | |
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 | |
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 | |
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 | |
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.
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.
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.
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 | |
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 | |
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.
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.
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.
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 | |
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].
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.
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 | |
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.
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 | |
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.
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 | |
25. 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.
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 | |
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 | |
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 | |
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).
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 | |
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 | |
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.
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 gotcha #2 for correct import paths.