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-initinput pack (gpkg + age dist + CBR/CDR series + life-expectancy curve).
- 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_Rateanalog that accelerates clearance during fever. - Real-geography runs via
laser-initpacks — 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_Habitathook for when real ecology data lands. - Plotting: 6-panel summary, per-node × channel grid with map column, choropleth snapshot grid, animated GIF.
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.
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.
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.
Eight evenly-spaced prevalence snapshots across the 5-year run, colored by district.
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.
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.
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.
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.
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.
Same data, ~260 frames at weekly stride.
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).
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.
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 |
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.
Baseline (no interventions)![]() |
ITN 80% from year 1![]() |
IRS 60% from year 1![]() |
MDA 3 rounds × 30d, 80%![]() |
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.
Compared with the ITN scenario, which sustains the suppression:
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.
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.
git clone https://github.com/laser-models/laser-malaria.git
cd laser-malaria
pip install -e ".[full]" # core + plotting + geopackage supportCore deps: numpy, laser-core, pandas. The [full] extra adds matplotlib, pillow, geopandas, pyogrio — needed for plots, animations, and the laser-init input pack loader.
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.shLinux: 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.
python -m laser_malaria.run_demo --endemic --plot --timesteps 1825Writes outputs/malaria_results.csv and a 6-panel summary PNG.
First generate inputs via laser-init:
cd /path/to/laser-init
laser-generate STP 2 2010 2020 --shape-source gadmThen point laser-malaria at the pack:
python -m laser_malaria.run_stp \
--pack ~/LASER/laser-init/STP/2010 \
--endemic --plot --timesteps 1825This 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 |
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)
What we have, side-by-side with the EMOD-Malaria spec.
| 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 |
≈ |
| 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) |
| 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 |
≈ |
| 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 |
≈ |
| 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 | ✓ |
| 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 |
✓ |
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.)
| 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 |
python -m pytest33 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.
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.
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/.















