Skip to content

Repository files navigation

laser-malaria

An agent-based malaria model built on laser-core, aligned where practical with the EMOD-Malaria modeling spec.

  • Humans are individual agents stored in a LaserFrame.
  • Mosquito vectors are deterministic node-level S/E/I compartments (not individual agents).
  • Parasites are scalar properties on humans (asexual density, gametocyte density, infection state).
  • Geography is loaded from a laser-init input pack (gpkg + age dist + CBR/CDR series + life-expectancy curve).

Highlights

  • Intrahost state machine with stochastic sub-patent clearance — replaces a fixed infectious-window with an immunity-modulated clearance hazard, giving EMOD-style multi-month carriage tails without antigenic-switching machinery.
  • Four-scalar immunity (maternal, anti_infection, anti_parasite, anti_disease) as a coarse projection of EMOD's per-antigen antibody titers. Anti-infection grows from per-bite sporozoite exposure, not just successful infections.
  • EMOD-style clinical incidents: fever-driven onset, bounded duration, refractory cooldown, plus a Fever_IRBC_Kill_Rate analog that accelerates clearance during fever.
  • Real-geography runs via laser-init packs — geometry, populations, age distribution, and time-varying CBR/CDR by simulation year.
  • Gravity human-mixing with configurable strength and off-diagonal cap.
  • Per-node larval habitat heterogeneity (lognormal multipliers, country-total preserved) — the EMOD x_Temporary_Larval_Habitat hook for when real ecology data lands.
  • Plotting: 6-panel summary, per-node × channel grid with map column, choropleth snapshot grid, animated GIF.

Gallery

STP (São Tomé and Príncipe) — 7 admin-2 districts, ~174k population, 5-year endemic run

Real-geography demonstration: 7 districts from a GADM admin-2 shapefile, populations and demographics from a laser-init pack, gravity-mixed transmission with lognormal per-node habitat heterogeneity. The outbreak seeds in the largest district (Água Grande) and propagates outward via gravity flow.

Country-level summary

Six channels over 5 years: prevalence, new infections per day, clinical-case stock, EIR, mosquito S/E/I, mean parasite density. The damped multi-year oscillation in prevalence (visible in panel 1) is driven by the immunity-prevalence feedback loop.

STP summary

Per-district grid

Each row is one of the 7 admin-2 districts; the leftmost column highlights the district's location on the country map; remaining columns are seven key channels. Y-axes are shared per column so heights are directly comparable across districts. The signature finding: small isolated districts (Pagué on Príncipe Island, Caué in the south) end up at higher equilibrium prevalence than the seed district itself — a real consequence of asymmetric gravity flow that uniform-mixing models miss.

STP per-district grid

Spatial dynamics (choropleth snapshots)

Eight evenly-spaced prevalence snapshots across the 5-year run, colored by district.

STP choropleth snapshots

Spatial dynamics (animated)

Same data, animated. The initial wave fires in Água Grande around day 100, sweeps the São Tomé island over the next ~6 months, then crosses the channel to ignite Príncipe with a multi-month lag.

STP prevalence animation

MWI (Malawi) — 28 admin-1 districts, 14.5M agents, 5-year endemic run

Same model, same parameters, two orders of magnitude more agents. 14.5M individual humans across 28 GADM admin-1 districts, full demographic dynamics from a laser-init Malawi pack, 1825-day run (report_every=7 for weekly logging — daily would have produced an unwieldy CSV without adding visualization fidelity).

Total wall time: ~80 minutes for the full 5-year run. The same code path that did STP in seconds, scaled up 84× in agent count.

Country-level summary

MWI summary

Pop-weighted prevalence climbs through year 1, hits a peak around day 500, then oscillates with a 2-3 year period as immunity catches up. Mosquito S/E/I shows the seasonal cycle clearly; mean parasite density tracks the prevalence oscillation.

Per-district grid

28 rows × 7 channels, with a thumbnail map highlighting each district's location on the Malawi map. Read across a row to see one district's full story; read down a column to compare a single channel across all districts.

MWI per-district grid

Wave propagation across the country

Snapshots from day 0 (uniformly low) through day 1825. The outbreak fires in Lilongwe (the seed, in the central region), sweeps the southern districts first (day 259-518), reaches the north by day 777, and settles into a damped multi-year oscillation across the country.

MWI choropleth snapshots

Wave propagation (animated)

Same data, ~260 frames at weekly stride.

MWI prevalence animation

Note: this run uses Numba-jitted hot loops (see laser_malaria/kernels.py). Without Numba the same run would take ~5 hours; with it, ~80 minutes. The full Numba sweep delivered a 4.2× speedup at MWI scale (9920 → 2355 ms/tick).

MWI intervention sweep — baseline vs ITN vs IRS vs MDA at country scale

Same 14.5M-agent, 28-district MWI run, four times: once with no interventions, once each with ITN / IRS / MDA enabled at year 1. Each scenario is identical until day 365 when the configured intervention starts firing. Together these four runs took ~5 hours wall time at MWI scale.

Headline comparison

MWI intervention sweep — prevalence

All four trajectories overlap until day 365 (every scenario is just "baseline" up to that point). Then:

  • ITN (blue) — 80% coverage of bednets nationwide. Prevalence falls smoothly from ~0.38 to 0.0004 by year 5. Essentially eliminates transmission.
  • IRS (green) — 60% coverage of indoor residual spraying. Same shape as ITN, slightly higher floor at 0.018 (because IRS lacks the physical bite-block effect ITN gets).
  • MDA (red) — 3 rounds of mass drug administration at 80% coverage, 30 days apart starting at year 1. Each round produces a sharp drop, but with no sustained vector-control component, the reservoir re-establishes by year 2-3 and even overshoots baseline around day 1000 — because the brief susceptible-rich post-MDA window seeds a synchronized re-introduction wave. By year 5 the MDA scenario has roughly the same prevalence as no-intervention baseline.

This last finding is the real-world policy result: MDA on its own is not a sustained-reduction strategy. It needs to be paired with continuous vector control (ITN / IRS) to lock in the gains.

Scenario Final prevalence Δ vs baseline Mean EIR Clinical-case stock Susceptible fraction
baseline 0.502 8.2e−4 43,717 0.16
ITN 0.00036 −99.9% 1.5e−4 12,051 0.65
IRS 0.018 −96.4% 1.8e−4 14,280 0.60
MDA 0.449 −10.6% 7.4e−4 40,358 0.18

Clinical-case dynamics

MWI intervention sweep — clinical stock

Same pattern, more dramatic: MDA's post-rebound peak (~550k clinical agents around day 1000) exceeds baseline's highest peak. The vector-control scenarios drop clinical stock by 5-10× and stay there.

Per-scenario summaries

Baseline (no interventions)
ITN 80% from year 1
IRS 60% from year 1
MDA 3 rounds × 30d, 80%

Spatial dynamics — the MDA rebound

The MDA animation makes the rebound visible district-by-district. The 3 rounds clear most of the country, then a synchronized re-establishment wave sweeps back in.

MWI MDA prevalence animation

Compared with the ITN scenario, which sustains the suppression:

MWI ITN prevalence animation

Reproduce: laser-malaria-mwi-interventions --timesteps 1825 --report-every 7 --out outputs/mwi_interventions. Full per-scenario CSVs land in that directory. Wall time ~5 hours on a typical workstation with the Numba kernels.

Calibration

The Garki Project (Molineaux & Gramiccia 1980) reconstruction — 22-stage calibration story, cross-model validation, and Docker/AKS harness for the parallel Optuna search — lives under garki_calibration/. Start with garki_calibration/report.md.

Install

git clone https://github.com/laser-models/laser-malaria.git
cd laser-malaria
pip install -e ".[full]"   # core + plotting + geopackage support

Core deps: numpy, laser-core, pandas. The [full] extra adds matplotlib, pillow, geopandas, pyogrio — needed for plots, animations, and the laser-init input pack loader.

Build the C/OpenMP kernels (optional, ~50× speedup at MWI scale)

The model has nine .so kernels under laser_malaria/c_kernels/ that take over the per-tick hot path. Without them the model still works via numba/numpy fallbacks; with them, a 5y MWI baseline (14.5M agents, 16-thread OMP) runs in ~6 min instead of ~5 hours.

cd laser_malaria/c_kernels
./build.sh

Linux: needs only gcc (always present). macOS / Apple Silicon: needs either Homebrew libomp or Homebrew gcc — see c_kernels/BUILD.md for the one-line setup. The Python loader auto-detects which .so files are present; missing ones silently fall back to the slower path.

Quickstart

Synthetic 10-node demo

python -m laser_malaria.run_demo --endemic --plot --timesteps 1825

Writes outputs/malaria_results.csv and a 6-panel summary PNG.

Real-geography run (STP example)

First generate inputs via laser-init:

cd /path/to/laser-init
laser-generate STP 2 2010 2020 --shape-source gadm

Then point laser-malaria at the pack:

python -m laser_malaria.run_stp \
    --pack ~/LASER/laser-init/STP/2010 \
    --endemic --plot --timesteps 1825

This produces in outputs/:

File What it is
stp_results.csv Per-(tick, node) report covering 21 channels
stp_results.png 6-panel country-level summary
stp_node_grid.png Per-node × per-channel grid with map-thumbnail column
stp_choropleth_snapshots.png 2×4 prevalence choropleth across time
stp_prevalence.gif Animated choropleth

Architecture

Tick-driven, single-threaded:

ReportingComponent.reset_events     clear per-tick event flags
DemographyComponent                  age + age-bracketed mortality + births (CBR-driven)
VectorLifecycleComponent             seasonal larval habitat → S/E/I per node
VectorInfectionComponent             humans → mosquitoes (gametocyte half-sat)
HumanExposureComponent               infectious mosquitoes → humans (gravity-mixed FoI)
ParasiteProgressionComponent         within-host state machine + clinical incidents
ImmunityComponent                    maternal decay + acquired gain/wane
ReportingComponent.log               aggregate per-(tick, node) report
laser_malaria/
  model.py                   MalariaModel + default_params + REPORT_COLUMNS
  states.py                  infection-state enum
  inputs.py                  StpInputs + load_stp_pack (laser-init format)
  gravity.py                 row-stochastic person-time mixing matrix
  plot_results.py            summary + per-node grid + choropleth + animation
  run_demo.py                synthetic 10-node demo CLI
  run_stp.py                 real-geography (laser-init pack) demo CLI
  components/
    demography.py            aging + age-bracketed mortality + births
    vector_lifecycle.py      node-level mosquito demography
    vector_infection.py      humans → mosquitoes (gravity-aware)
    human_exposure.py        infectious mosquitoes → humans (gravity-aware)
    parasite_progression.py  within-host state machine + clinical incidents
    immunity.py              4-scalar immunity gain + waning
    reporting.py             per-tick event reset + per-node report log
  tests/                     pytest suite (33 tests, all passing)

EMOD-Malaria mapping

What we have, side-by-side with the EMOD-Malaria spec.

Intrahost infection dynamics

laser-malaria EMOD-Malaria analog Match
LIVER_STAGE, timer 7–14d Hepatic stage, fixed 7d
BLOOD_STAGE_ASEXUAL, logistic growth to peak ~1000 Asexual cycles, Merozoites_Per_Schizont=16
INFECTIOUS_TO_MOSQUITOES + stochastic sub-patent clearance Persistence via antigenic switching (50 PfEMP1 variants) proxy — gives the long tail without the variant machinery
gametocyte_density = gametocyte_fraction × density Base_Gametocyte_Production_Rate=0.02, 10d maturation, 2.5d half-life

Clinical episode model

laser-malaria EMOD-Malaria analog Match
Density-driven p_clinical Bernoulli (eligible only) Pyrogenic threshold sigmoid → fever
Bounded incident duration (3–7d) Clinical_Fever_Threshold_High → Low hysteresis
min_days_between_clinical_incidents=3 Min_Days_Between_Clinical_Incidents=3 ✓ exact
fever_irbc_kill_rate=0.02 during incidents Fever_IRBC_Kill_Rate=0.15 ✓ (scaled to per-tick coarse density)

Immunity (4-scalar coarse projection of EMOD's per-antigen titers)

laser-malaria EMOD-Malaria analog Match
maternal_immunity, log(2)/90d decay, blocks infection Maternal_Antibody_Protection=0.1, Maternal_Antibody_Decay_Rate=0.01/d
anti_infection, per-bite tonic + per-infection bump Anti-CSP antibody growth from sporozoite exposure
anti_parasite accelerates blood-stage clearance Antibody_IRBC_Kill_Rate=2 × titer
anti_disease reduces fever signal Innate-cytokine response strength
Age-seeded initial immunity (saturating in age) Adaptive accumulation over lifetime exposures
Slow waning to memory floor (years) Antibody_Long_Term_Decay_Days=3650, Antibody_Memory_Level=0.2

Vector dynamics

laser-malaria EMOD-Malaria analog Match
Deterministic per-node S/E/I compartments Individual vector agents (cohorts) simplified by design
Seasonal forcing on larval habitat EMOD seasonal habitat
larval_habitat_heterogeneity_sigma (per-node lognormal multipliers, country-total preserved) x_Temporary_Larval_Habitat per node ✓ same hook, placeholder data
Per-node EIR + FoI computed each tick EMOD per-node EIR
Gametocyte → mosquito infection via half-saturation Base_Gametocyte_Mosquito_Survival_Rate=0.01

Spatial / geography

laser-malaria EMOD-Malaria analog Match
laser-init input pack (load_stp_pack) Geospatial / demographic input bundles
Gravity mixing (configurable k, a, b, c, cap) EMOD migration / commuting models
Per-node centroids from gpkg EMOD node coords
Choropleth animation / snapshot grid EMOD plotting helpers

Demography / vital dynamics

laser-malaria EMOD-Malaria analog Match
Age-bracketed mortality (4 buckets) EMOD age-dependent mortality
Births by CBR + maternal antibody inheritance EMOD BirthsByCBR + maternal AB
Time-varying CBR/CDR by year (cxr.csv) EMOD vital-rate time series
Aging 1d/tick, max-age cap EMOD aging
Initial age distribution from age_dist.csv EMOD AliasedDistribution over age pyramid

Reporting / output

Per-(tick, node) channels: human_population, infected_humans, infectious_humans, clinical_cases, new_infections, parasite_prevalence, mean_parasite_density, mean_gametocyte_density, susceptible_vectors, exposed_vectors, infectious_vectors, adult_vectors, eir, human_foi, births, deaths, mean_age_years, mean_anti_infection, mean_anti_parasite, mean_anti_disease, susceptible_fraction. (21 columns.)

Known gaps (EMOD features we don't have)

Feature Status
Antigenic switching (50 PfEMP1 variants) not modeled — replaced by stochastic clearance
Superinfection / multiple concurrent strains not modeled (single infection per agent)
Antimalarial drug effects not modeled
Vector-control interventions: ITN implemented (laser_malaria.interventions.ITN) — per-node bite block + vector mortality uplift, time- and region-targetable
Vector-control interventions: IRS implemented (laser_malaria.interventions.IRS) — vector mortality uplift only (no physical bite block); stacks multiplicatively with ITN when deployed together
Mass Drug Administration implemented (laser_malaria.interventions.MDA) — schedule arbitrary rounds, partial coverage, per-node targeting; clears blood-stage to RECOVERED, leaves liver-stage untouched
Severe disease + parasitemia-driven mortality not modeled (background mortality only)
Vaccines (RTS,S / CSP) not modeled
PfHRP2 tracking not modeled
Age-stratified mortality from life_exp.csv loaded but not consumed (still using 4-bucket scalar params)
Individual mosquito agents by design — kept as deterministic compartments

Test

python -m pytest

33 tests covering: vector compartments, infection-state progression, chronic carriage persistence, bounded clinical incidents and cooldown, fever-accelerated clearance, per-bite anti-infection tonic, three-immunity ordering, maternal immunity decay, anti-parasite density suppression, anti-disease clinical suppression, age-stratified clinical rates, demography aging/births/deaths/age-distribution, STP input loading + admin-level selection + time-varying CBR/CDR + end-to-end model run, gravity-matrix algebra and behavioral spread, seeded-node localization.

Roadmap

Near-term: consume life_exp.csv for age-stratified mortality; add a vector_control component for ITN/IRS scenarios; project node centroids to a metric CRS before computing distances (silences geopandas CRS warning).

Longer-term: per-strain immunity (toward EMOD-style antigenic switching), superinfection, drug treatment, and individual mosquito agents where the deterministic compartments leave performance on the table.

Provenance

This model was developed by Jonathan Bloedow in collaboration with the Anthropic Claude model. The EMOD-Malaria intrahost spec sourced from https://emod.idmod.org/emodpy-malaria/emod/malaria-model-infection-immunity/.

About

Malaria model implementations based on LASER toolkit.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages