PMTCT via ANC testing

Prevention of mother-to-child transmission (PMTCT) programs identify HIV-positive pregnant women during antenatal care (ANC) and start them on ART immediately, so both the mother and her unborn/breastfeeding child are protected. STIsim models this with three cooperating pieces:

This example compares two scenarios that differ only in whether ANC testing is added on top of routine population-level HIV testing.

Setup

import numpy as np
import sciris as sc
import starsim as ss
import stisim as sti
import matplotlib.pyplot as plt

sc.options(dpi=110)

Building the sims

Both scenarios share the same routine HIVTest (a modest 8%/year testing rate, deliberately low so the ANC-specific effect is visible) and ART (60% coverage target). The ANC scenario layers ANCTest + InfantHIVTest on top — ART is unchanged and simply treats whoever gets scheduled, regardless of which testing pathway diagnosed them.

def make_sim(use_anc, rand_seed=1):
    hiv_test = sti.HIVTest(name='hiv_test', test_prob_data=0.08)
    art = sti.ART(coverage=0.6)
    interventions = [hiv_test, art]

    if use_anc:
        infant_hiv = sti.InfantHIVTest(name='infant_hiv')
        anc = sti.ANCTest(visit_prob=0.9, newborn_tests={'hiv': infant_hiv})
        # anc/infant_hiv must precede art so same-step diagnoses can start ART
        interventions = [hiv_test, anc, infant_hiv, art]

    hiv = sti.HIV(init_prev=0.1, beta_m2f=0.05, beta_m2c=0.1)
    sim = sti.Sim(
        diseases=[hiv],
        demographics=[ss.Pregnancy(), ss.Deaths()],
        networks=[sti.StructuredSexual(), ss.MaternalNet(), ss.BreastfeedingNet()],
        interventions=interventions,
        n_agents=5_000, dur=15, start=2005, verbose=-1, rand_seed=rand_seed,
    )
    sim.label = 'With ANC testing' if use_anc else 'No ANC testing'
    return sim

sims = ss.parallel([make_sim(False), make_sim(True)]).sims
results = {sim.label: sim for sim in sims}
Initializing sim "No ANC testing" with 5000 agents
Initializing sim "With ANC testing" with 5000 agents

  Running "No ANC testing": 2005 ( 0/181) (0.00 s)  ———————————————————— 1%

  Running "With ANC testing": 2005 ( 0/181) (0.00 s)  ———————————————————— 1%

  Running "No ANC testing": 2006 (12/181) (0.38 s)  •——————————————————— 7%

  Running "With ANC testing": 2006 (12/181) (0.39 s)  •——————————————————— 7%

  Running "No ANC testing": 2007 (24/181) (0.75 s)  ••—————————————————— 14%

  Running "With ANC testing": 2007 (24/181) (0.79 s)  ••—————————————————— 14%

  Running "No ANC testing": 2008 (36/181) (1.12 s)  ••••———————————————— 20%

  Running "With ANC testing": 2008 (36/181) (1.19 s)  ••••———————————————— 20%

  Running "No ANC testing": 2009 (48/181) (1.50 s)  •••••——————————————— 27%

  Running "With ANC testing": 2009 (48/181) (1.58 s)  •••••——————————————— 27%

  Running "No ANC testing": 2010 (60/181) (1.86 s)  ••••••—————————————— 34%

  Running "With ANC testing": 2010 (60/181) (1.97 s)  ••••••—————————————— 34%

  Running "No ANC testing": 2011 (72/181) (2.23 s)  ••••••••———————————— 40%

  Running "With ANC testing": 2011 (72/181) (2.36 s)  ••••••••———————————— 40%

  Running "No ANC testing": 2012 (84/181) (2.60 s)  •••••••••——————————— 47%

  Running "With ANC testing": 2012 (84/181) (2.77 s)  •••••••••——————————— 47%

  Running "No ANC testing": 2013 (96/181) (2.99 s)  ••••••••••—————————— 54%

  Running "With ANC testing": 2013 (96/181) (3.16 s)  ••••••••••—————————— 54%

  Running "No ANC testing": 2014 (108/181) (3.35 s)  ••••••••••••———————— 60%

  Running "With ANC testing": 2014 (108/181) (3.55 s)  ••••••••••••———————— 60%

  Running "No ANC testing": 2015 (120/181) (3.74 s)  •••••••••••••——————— 67%

  Running "With ANC testing": 2015 (120/181) (3.95 s)  •••••••••••••——————— 67%

  Running "No ANC testing": 2016 (132/181) (4.11 s)  ••••••••••••••—————— 73%

  Running "With ANC testing": 2016 (132/181) (4.34 s)  ••••••••••••••—————— 73%

  Running "No ANC testing": 2017 (144/181) (4.47 s)  ••••••••••••••••———— 80%

  Running "No ANC testing": 2018 (156/181) (4.83 s)  •••••••••••••••••——— 87%

  Running "With ANC testing": 2017 (144/181) (4.74 s)  ••••••••••••••••———— 80%

  Running "No ANC testing": 2019 (168/181) (5.19 s)  ••••••••••••••••••—— 93%

  Running "With ANC testing": 2018 (156/181) (5.12 s)  •••••••••••••••••——— 87%

  Running "No ANC testing": 2020 (180/181) (5.57 s)  •••••••••••••••••••• 100%


  Running "With ANC testing": 2019 (168/181) (5.53 s)  ••••••••••••••••••—— 93%

  Running "With ANC testing": 2020 (180/181) (5.92 s)  •••••••••••••••••••• 100%

Comparing outcomes

for label, sim in results.items():
    mtct = int(sim.results.hiv.new_infections_mtct.sum())
    p_dx = sim.results.hiv.p_diagnosed_pregnant[-1]
    print(f'{label:>17s}: {mtct:4d} MTCT infections, '
          f'{p_dx:.0%} of pregnant HIV+ women diagnosed by end of sim')
   No ANC testing:  195 MTCT infections, 38% of pregnant HIV+ women diagnosed by end of sim
 With ANC testing:   97 MTCT infections, 77% of pregnant HIV+ women diagnosed by end of sim
yearvec = sims[0].t.yearvec

fig, axes = plt.subplots(1, 2, figsize=(11, 4.5))

# Left: proportion of HIV+ pregnant women diagnosed
for label, sim in results.items():
    axes[0].plot(yearvec, sim.results.hiv.p_diagnosed_pregnant, label=label, lw=2)
axes[0].set_xlabel('Year')
axes[0].set_ylabel('Proportion of HIV+ pregnant women diagnosed')
axes[0].set_title('ANC testing closes the diagnosis gap early')
axes[0].legend()

# Right: cumulative mother-to-child transmission
for label, sim in results.items():
    axes[1].plot(yearvec, np.cumsum(sim.results.hiv.new_infections_mtct), label=label, lw=2)
axes[1].set_xlabel('Year')
axes[1].set_ylabel('Cumulative MTCT infections')
axes[1].set_title('Cumulative mother-to-child transmission')
axes[1].legend()

sc.figlayout()
plt.show()

p_diagnosed_pregnant separates early — ANC testing catches most HIV+ pregnant women within their own pregnancy, whereas routine testing only catches up gradually as background testing accumulates. That earlier diagnosis translates directly into fewer MTCT infections over the run.

Why immediate ART matters here

ANCTest sets hiv.ti_art = ti (the current timestep) for anyone it diagnoses who isn’t already on ART — the same no-delay behavior HIVTest gets from its dur_dx2tx=ss.constant(0) default. This matters because a mother is only protected (and only protects her fetus/infant) once she’s actually on treatment: the diagnosis event itself does nothing without an ART intervention in sim.interventions to act on the schedule.

# HIV positives are diagnosed and scheduled the same way regardless of route:
hiv.diagnosed[pos_uids]    = True
hiv.ti_diagnosed[pos_uids] = ti
hiv.ti_art[to_schedule]    = ti   # to_schedule = pos_uids not already on ART

Non-HIV diseases (e.g. syphilis) route through disease_treatment_map instead — HIV bypasses that map entirely.

Retention through delivery

Once a mother starts ART, maternal_care_scale (default 2) doubles her care-seeking behaviour for the duration of pregnancy, making her much less likely to default off treatment before delivery — see the HIV disease page for details. pmtct_efficacy (default 0.96, on ART) then determines how much her being on treatment reduces her child’s susceptibility, applied via both MaternalNet (prenatal) and BreastfeedingNet (postnatal).

Key takeaways

  • ANCTest auto-detects which diseases to test for from sim.diseases (HIV and syphilis) unless you pass disease_names explicitly.
  • newborn_tests is a dict keyed by disease name ({'hiv': infant_hiv}), not a list — one newborn test per disease you want to follow up on.
  • Intervention order matters: ANCTest (and InfantHIVTest, if scheduled standalone via HIVTest.newborn_test=) must appear before ART in the interventions list so same-step diagnoses can start treatment immediately.
  • ANCTest requires ss.Pregnancy and ss.MaternalNet in the sim (add ss.BreastfeedingNet too if you want postnatal protection modeled).