Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

laser-tb

TB SEILR — a spatial agent-based model that extends laser-generic's SEIR with a slow-latent reservoir compartment L and per-agent reactivation timers. The reservoir sustains transmission by itself, long after the initial epidemic wave has burned out.

Status: prototype; not calibrated to any real-world dataset. Parameters were chosen from published TB natural-history literature (Vynnycky-Fine 1997, Blower 1995, Andrews 2012, Menzies 2018), then subjected to a 1D per-parameter sensitivity sweep — see sensitivity/SENSITIVITY.md — to identify which parameters materially move the model outcomes and which don't.

What this model shows

The latent-TB reservoir sustains transmission by itself, long after the initial epidemic wave has burned out.

Two-timescale infection dynamics, extending laser-generic's SEIR with a slow latent compartment L on a decade-to-century timescale:

S --β--> E --p_fast--> I     ("fast progression", ~10% of infections)
         E --1-p_fast--> L   ("slow progression", ~90% — goes straight to reservoir)
I --p_cure--> R              (self-cure w/ permanent immunity)
I --1-p_cure--> L            (self-cure w/ residual bacilli → reservoir)
L --λ_react--> I             (rare reactivation, ~0.5% per year here)

Only I contributes to force of infection. L is silent — a decades-long reservoir of latent M. tuberculosis. Reactivation is a Poisson process with per-agent time-to-reactivation sampled ~ Exponential(1/λ_react) on entry to L.

Gallery

Eswatini (admin-1, 4 regions, ~1.16M population, 50-year run)

Real-geography demonstration on Eswatini's 4 admin-1 regions (Hhohho, Manzini, Lubombo, Shiselweni). Boundaries come from GADM and populations from WorldPop 2020 via the laser-init microservices; the cached responses ship under laser_tb/data/eswatini/ so the runner works fully offline.

Eswatini has one of the highest TB incidence rates in the world (~650 per 100k/yr per WHO Global TB Report), driven by co-epidemic HIV (~27% adult prevalence). Notification rates are higher in industrial Manzini than in rural Shiselweni. The 4-region setup is small enough to be computationally tractable while still showing the reservoir-driven dynamics on real geography. Not calibrated to Eswatini-specific notification data.

TB SEILR on Eswatini — 50y run, 4 regions

Six panels: aggregate S / L / R compartments over time; active TB per region; primary (E→I) vs reactivation (L→I) flux with 30-day smoothing on a log axis; latent-reservoir choropleth at year 10 (during outbreak wave); latent-reservoir choropleth at year 50 (endemic); end-state L-fraction and 100×I-fraction bars per region.

Animations:

Synthetic 5×5 grid (25-patch, 200k population, 50-year run)

Same model on a synthetic 5×5 grid. Useful for isolating what the reservoir dynamics look like without the confound of real-geography population imbalances.

TB SEILR default run — reservoir dominates by year 15

  • Ever-infected: ~99% of the population.
  • L (latent reservoir): ~89% of the endpoint population; R (cured): ~10%; I (active TB): ~0.2% — endemic equilibrium.
  • The E→I (primary progression) flux dominates until year ~10; by year 15, L→I (reactivation) takes over as the dominant source of new active TB.

Install + run

pip install .[geo]      # Eswatini runner needs geopandas + shapely + requests
python -c "from laser_tb.run_eswatini import run_eswatini; r = run_eswatini({'YEARS': 30}, verbose=True); print(list(r['scenario'].name.values))"

Or as scripts:

laser-tb-eswatini --years 50 --out out_eswatini.png   # real Eswatini geography
laser-tb          --years 30 --seed 42                  # synthetic 5×5 grid

Public API

from laser_tb import DEFAULTS, run_model, metrics

# DEFAULTS is the tabled baseline — see docs below.
result = run_model(overrides={"R0_TRANSMISSION": 12.0, "YEARS": 40}, verbose=True)
model = result["model"]              # laser-generic Model instance
print(metrics(result))

overrides is a dict of parameter names → values. Unknown keys raise KeyError early. The metrics function extracts the six outcome scalars used by the sensitivity harness (endemic active-TB prevalence, latent-reservoir fraction, reservoir-dominance year, reactivation fraction, primary-wave peak, final N).

Parameter table

From published TB natural-history literature; not calibrated to any specific dataset. See sensitivity/SENSITIVITY.md for the per-parameter perturbation study.

symbol value source / justification
R0_TRANSMISSION 15 high-burden setting; Vynnycky-Fine 1997, Blower 1995
P_FAST 0.10 fast-progressor fraction; Vynnycky-Fine 1997, Andrews 2012
P_CURE 0.30 untreated self-cure to R; Menzies 2018
EXP_SCALE_DAYS 90 d mean E period (fast-latent progression time)
INF_SCALE_DAYS 180 d mean untreated smear-positive I duration
LAT_SCALE_DAYS 200 yr mean L→I time; 0.5%/yr reactivation hazard (Andrews 2012 report 0.1–0.3%/yr for immunocompetent LTBI; we use 0.5%/yr to keep reactivation visible in the sim window)
CBR 25/1000/yr high-burden setting (sub-Saharan Africa)
CDR 12/1000/yr net +1.3%/yr population growth
GRID_M × GRID_N 5×5 = 25 patches central urban core (40k), ring (15k each), rural periphery (5k each)
YEARS 50 long enough to see the reservoir take over
SEED 20260710

R0_active ≈ R0_TRANSMISSION × P_FAST ≈ 1.5 — the effective reproduction number for active TB cases spawning further active cases via the fast pathway. This is what sustains ongoing transmission during the early years.

Sensitivity

A 1D per-parameter perturbation study (±30% around each science parameter, three stochastic seeds per cell, 30-year sims, LAT_SCALE_DAYS swept alongside the others despite its wide baseline) landed alongside this repo — see sensitivity/SENSITIVITY.md for the tornado charts and per-parameter interpretation. Rerun the sweep with:

python -m sensitivity.sensitivity --years 30 --seeds 3 --workers 3
python -m sensitivity.make_tornado

Why this is interesting

  • Standard SEIR models can't reproduce TB's decade-scale endemic persistence because they lack the reservoir compartment. Vaccine-preventable diseases like measles burn out in a small population; TB doesn't, even though it has a short infectious period, because the reservoir is unaffected by depletion of the susceptible pool.
  • The R0_active = R0_transmission × P_FAST trick is what distinguishes TB from an SIR outbreak. Naively plugging R0=3 (a reasonable TB contact R0) into an SEIR gives active TB dying out. TB persistence depends on the reservoir.
  • Intervention-relevant: this framework supports LTBI screening + isoniazid preventive therapy (model as an artificial L→R flow with imperfect efficacy) and BCG vaccination (reduces P_FAST without eliminating latent infection).

Extensions (obvious next steps)

  • HIV coinfection — increase λ_react by 20–30× for HIV+ agents (Andrews 2012) via an additional per-agent flag.
  • BCG cohort — bump P_FAST down by ~50% for vaccinated children; requires an age-structured scenario.
  • Treatment — add a T compartment for treated agents with 90-day duration and imperfect cure (drop-out fraction goes back to I).
  • MDR — add a resistant compartment I_r that is treatment-resistant with lower cure probability.
  • More country runners — the Eswatini runner is a template. Adding e.g. India (IND) at admin-1 or South Africa (ZAF) is a _laser_init.load_real_scenario("<ISO>", level=1, year=2020) call plus setting SEED_DIVISION for the new country.

References

  • Blower SM et al. 1995. The intrinsic transmission dynamics of tuberculosis epidemics. Nat Med 1:815-821 — canonical two-progression TB model.
  • Vynnycky E, Fine PEM. 1997. The natural history of tuberculosis. Epidemiol Infect 119:183-201 — natural history parameters.
  • Andrews JR et al. 2012. Risk of progression to active tuberculosis. Clin Infect Dis 54:784-791 — reactivation-rate meta-analysis.
  • Dowdy DW et al. 2013. Heterogeneity in tuberculosis transmission. PNAS 110:9557-9562 — LTBI reservoir maintenance in high-burden settings.
  • Menzies NA et al. 2018. Progression from latent infection to active disease in dynamic tuberculosis transmission models. PLoS Med 15:e1002591.

Implementation notes

  • LATENT = np.int8(4) extends laser-generic's State enum (which uses -1..3). Adding an entry to the enum itself is fragile, so we use a raw numeric.
  • The additional_states=["L"] argument on Model.__init__ makes _initialize_flows automatically propagate model.nodes.L[tick] → L[tick+1].
  • E→(I or L) branching: assigning the "fate" at exposure time (FATE_TO_I / FATE_TO_L, stored in people.fate) lets us run two independent single-target countdown-transitions inside numba, instead of a two-way branch.
  • Reactivation timer is drawn Exponential(scale) at L-entry (ltimer, uint32 days). Then SlowLatentReactivation.step() decrements daily and transitions to I on zero — exact for a homogeneous Poisson process.
  • The mortality mappings passed to MortalityByCDR include the LATENT state so nodes.L is decremented on death (default mortality only knows S/E/I/R).

About

TB SEILR spatial ABM with slow-latent reservoir + reactivation, built on laser-generic.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages