Interventions

HPVsim separates what is given from who gets it. A product is the vaccine, screening test or treatment itself, with its own efficacy or test-positivity by genotype and disease state. An intervention is the delivery program: which people, from which year, at what coverage. The same product can be delivered several ways, and the same delivery pattern can carry different products.

Product characteristics live in CSVs under hpvsim/data/, so you can read exactly what the model assumes:

Product Names What it does
hpv.vx bivalent, quadrivalent, nonavalent Writes per-genotype vaccine immunity
hpv.dx via, pap, lbc, hpv, hpv1618, hpv_type, colposcopy, plus the tx_assigner/txvx_assigner routers Draws a test result per woman from her true state
hpv.tx ablation (93.6% effective), excision (81.0%) Clears precancer and CIN on the treated genotype
hpv.txvx txvx1, txvx2 Writes therapeutic-vaccine immunity
hpv.radiation Extends survival with cancer; it does not cure

Diagnostics are multinomial draws conditioned on true state and genotype. Where a woman is positive for more than one genotype, the most severe result in the product’s hierarchy wins. Results come back as intervention.outcomes['<result label>'], and that dictionary is how a cascade is wired: each downstream step’s eligibility reads the upstream step’s outcomes.

Treatment products act on the model’s disease states directly. Ablation and excision clear precancer and CIN — since v3.1 they clear precancer too, which makes screen-and-treat avert more cancers than in earlier versions. Radiation extends time to cancer death by a drawn duration (mean 18 months); a woman treated for cancer still has cancer.

Delivery, coverage and targeting

Every intervention is either routine or campaign. hpv.routine_screening(start_year=2015, prob=0.3) offers a screen every year from 2015; hpv.campaign_screening(years=[2015, 2025], prob=0.3) offers it twice. prob is an annual probability, converted internally to the timestep in use — so prob=0.3 with quarterly steps does not mean 30% per quarter. Pass a list of probabilities alongside a list of years to ramp coverage over time. start_year and end_year must fall inside the simulation window or the run stops with an error.

All vaccination, screening, treatment and therapeutic-vaccination interventions default to sex='f'. That changed in v3.1: pass sex=None explicitly for gender-neutral vaccination. Age targeting uses age_range=[lo, hi], and the upper bound is treated as exclusive for vaccination, screening and triage but inclusive for treatment — so age_range=[30, 50] on a screening program reaches women up to their 50th birthday.

Give every intervention in a cascade a name, since downstream steps refer to it by name. Names share a namespace with product modules, so avoid 'vx', 'dx', 'tx' and 'radiation' as intervention names — a collision raises Module vx already added at initialisation.

How to build a screen-and-treat cascade

A screening program is a chain: screen, triage the positives, treat what triage finds. Each link is its own intervention, and eligibility functions connect them.

import numpy as np
import starsim as ss
import hpvsim as hpv

screen = hpv.routine_screening(product='hpv', prob=0.35, age_range=[30, 50],
                               start_year=2010, name='screen')

# Women who screen positive go to colposcopy
positives = lambda sim: sim.interventions['screen'].outcomes['positive']
triage = hpv.routine_triage(product='colposcopy', prob=0.9, eligibility=positives,
                            start_year=2010, name='triage')

# Women with a low- or high-grade lesion are offered ablation
lesions = lambda sim: np.union1d(sim.interventions['triage'].outcomes['hsil'],
                                 sim.interventions['triage'].outcomes['lsil'])
ablate = hpv.treat_num(product='ablation', prob=0.9, eligibility=lesions,
                       max_capacity=None)

pars = dict(location='nigeria', genotypes=[16, 18], n_agents=2000,
            start=2000, stop=2020, verbose=0)
baseline = hpv.Sim(**pars, label='no screening')
program = hpv.Sim(**pars, interventions=[screen, triage, ablate],
                    label='screen and treat')

msim = ss.MultiSim([baseline, program])
msim.run(parallel=False, verbose=0)

for sim in msim.sims:
    print(f'{sim.label:>18}: {float(sim.results.all_hpv.cum_cancers[-1]):>11,.0f} cancers by 2020')

prog = msim.sims[1]
print('screens delivered: ', f'{float(prog.interventions.screen.results.new_screens.sum()):,.0f}')
print('screen positives:  ', f'{float(prog.interventions.screen.results.new_dx.sum()):,.0f}')
print('women treated:     ', f'{float(prog.interventions["treat_num"].results.new_cin_treated.sum()):,.0f}')
      no screening:     311,684 cancers by 2020
  screen and treat:     249,347 cancers by 2020
screens delivered:  77,734,008
screen positives:   8,415,470
women treated:      810,379

max_capacity=None treats everyone eligible. Set it to a number of women per timestep to represent a service constrained by staff or theatre time, which is usually closer to reality than unlimited treatment. Use hpv.treat_delay(delay=1.0, ...) where women are referred and treated later, and expect loss to follow-up to matter: the cascade multiplies, so 35% screened × 90% triaged × 90% treated reaches under 30% of the women you intended.

To ramp a program’s coverage over calendar time without writing several interventions, use hpv.dynamic_pars with a dotted path, for example {'screen.prob': {'years': [2010, 2030], 'vals': [0.1, 0.7]}}.

See also