import starsim as ss
import starsim.library as ssl
mnch_pars = dict(start='2000', stop='2005', n_agents=2000, dt=ss.weeks(1))
demographics = lambda: [ss.Pregnancy(fertility_rate=ss.peryear(0.1), burnin=True), ss.Deaths()]
networks = lambda: [ss.PrenatalNet(), ss.RandomNet()]Maternal-child health
Transmission from mother to child, and the outcomes of a pregnancy (stillbirth, preterm birth, low birth weight, neonatal death), are harder to model than a standard disease: they depend on the timing of pregnancy, on which module acts first in a step, and on how deaths around birth are counted. The ssl.mnch submodule of the Starsim library (import starsim.library as ssl; see Library for what the library is) has worked examples of these patterns. All of them build on pregnancy (ss.Pregnancy, which gives each pregnancy as states and transitions, with conception, delivery, and the unborn child as an agent) and on ss.PrenatalNet, the network between a pregnant woman and her unborn child; both are described on Demographics.
| Class | Based on | Description |
|---|---|---|
ssl.mnch.CongenitalDisease |
ssl.AgentSIR |
Congenital outcomes (stillbirth, congenital infection, or none) for children infected before birth |
ssl.mnch.NeonatalSepsis |
ssl.AgentSIR |
Infects newborns at birth, and kills some within days |
ssl.mnch.FetalHealth |
ss.Module |
Tracks fetal growth and birth weight (low, very low, and small for gestational age) |
ssl.mnch.fetal_infection |
ss.Connector |
Links a disease to fetal health: infection in pregnancy brings delivery forward and restricts growth |
ssl.mnch.treat_pregnant |
ss.Delivery |
Treats infected pregnant women, partly reversing the damage to the fetus |
Like the rest of the library, these are illustrative: the probabilities and effect sizes are placeholders, chosen to make the effects easy to see. The examples below use a step of a week, since pregnancy outcomes are timed in weeks, and a high fertility rate, so that a small population gives enough births:
Congenital outcomes
CongenitalDisease is an SIR that is passed from an infected mother to her unborn child through ss.PrenatalNet. For each child infected before birth, it draws an outcome from birth_outcomes (by default stillbirth, congenital infection, or a normal birth, with probabilities 0.3, 0.4, and 0.3), and schedules it for around the time of delivery. An outcome that kills (a stillbirth or miscarriage) is scheduled one step before delivery, so that it happens before birth; otherwise ss.Pregnancy, which runs earlier in each step, would already have delivered the baby, and the loss would be counted as a live birth followed by a neonatal death. Compared with the same population without transmission (beta=0), the extra stillbirths are those the disease causes (and are no longer counted as births):
def congenital(beta):
disease = ssl.mnch.CongenitalDisease(beta=beta, init_prev=0.2)
return ss.Sim(diseases=disease, demographics=demographics(), networks=networks(), **mnch_pars)
for label, beta in {'No transmission': 0, 'Transmission': ss.peryear(20)}.items():
sim = congenital(beta).run()
preg = sim.results.pregnancy
print(label)
print(' Births:', int(preg.birth.sum()))
print(' Stillbirths:', int(preg.stillbirth.sum()))
print(' Congenital infections:', sim.diseases.congenitaldisease.congenital.sum())Sim: congenitaldisease, as 2,033 agents, 2000-01-01 to 2005-01-01
Running as agents: n_agents was given
Note: pregnancy: women pregnant at the start, conceived over the 40 steps before it (burnin=True)
Done in 1.6 s
No transmission
Births: 231
Stillbirths: 1
Congenital infections: 0
Sim: congenitaldisease, as 2,033 agents, 2000-01-01 to 2005-01-01
Running as agents: n_agents was given
Note: pregnancy: women pregnant at the start, conceived over the 40 steps before it (burnin=True)
Done in 1.4 s
Transmission
Births: 157
Stillbirths: 75
Congenital infections: 84
To use this in a disease of your own, build on ssl.mnch.CongenitalDisease, or copy its methods into any ss.AgentDisease:
set_congenital(), called instead ofset_prognoses()for each child infected before birth (the mother ispeople.parent), draws and schedules the outcome;step_congenital(), called fromstep_state(), applies the outcomes that are due;- the parameters
birth_outcome_keysandbirth_outcomes, and ati_<key>state for each outcome (plus a true/false state for each that doesn’t kill), say what the outcomes are. Outcomes named'stillborn'or'miscarriage'are deaths before delivery, and'neonatal_death'after it.
ssl.HIV (see Library: diseases) instead infects a fetus over ss.PrenatalNet as it infects anyone else.
Neonatal deaths
NeonatalSepsis infects a fraction of newborns at birth (p_at_birth, by default 30%), and kills half of them (p_death) within about a week. It shows that neonatal deaths need no special handling: ss.Pregnancy counts any death of a baby aged 0 to 28 days, from any cause, as a neonatal death. The results are therefore in the pregnancy module, not the disease, and comparing with a sim without sepsis gives the deaths it causes:
scenarios = {
'No sepsis': ss.Sim(demographics=demographics(), networks=networks(), **mnch_pars),
'Sepsis': ss.Sim(diseases=ssl.mnch.NeonatalSepsis(), demographics=demographics(), networks=networks(), **mnch_pars),
}
for label, sim in scenarios.items():
sim.run()
print(label)
print(' Births:', int(sim.results.pregnancy.birth.sum()))
print(' Neonatal deaths:', int(sim.results.pregnancy.neonatal_death.sum()))Sim: pregnancy, deaths, prenatalnet, randomnet, as 2,033 agents, 2000-01-01 to 2005-01-01
Running as agents: n_agents was given
Note: pregnancy: women pregnant at the start, conceived over the 40 steps before it (burnin=True)
Done in 1.1 s
No sepsis
Births: 231
Neonatal deaths: 0
Sim: neonatalsepsis, as 2,033 agents, 2000-01-01 to 2005-01-01
Running as agents: n_agents was given
Note: pregnancy: women pregnant at the start, conceived over the 40 steps before it (burnin=True)
Done in 1.1 s
Sepsis
Births: 231
Neonatal deaths: 39
Fetal health
FetalHealth doesn’t depend on any particular disease: it tracks a baseline fetal weight percentile for each pregnancy, and has two levers that other modules can pull: bringing delivery forward (the risk of a preterm birth) and adding growth restriction (the risk of a low birth weight). At delivery, it works out the birth weight from the gestational age, the percentile, and the growth restriction, and classifies newborns as of low birth weight (under 2,500 g), very low birth weight (under 1,500 g), small for gestational age, or small vulnerable newborns (any of these, or preterm). Its results are the numbers of each on each step, and the means and rates over each step’s births (e.g. mean_birth_weight, lbw_rate), which are NaN on a step with no births and are left out of a mean over time (res.mean_birth_weight.resample('year')).
The fetal_infection connector pulls those levers when a pregnant woman is infected: each infection brings delivery forward by about 3 weeks (timing_shift) and restricts growth by 15% (growth_penalty). The treat_pregnant delivery treats infected pregnant women (90% of them, coverage), as ss.treat() would, and reverses 70% of the damage (growth_reversal, timing_reversal). Both are written as code for an SIR written as code, so the disease here is ssl.AgentSIR, named 'sir'. FetalHealth is given in modules, since it is not a disease, network, demographic module, or intervention. Here are the baseline, infection, and infection with treatment from 2001:
def fetal(beta, treat=False):
treatment = ssl.mnch.treat_pregnant(disease='sir', start=2001) if treat else None
return ss.Sim(
diseases = ssl.AgentSIR(beta=beta, name='sir'),
demographics = demographics(),
connectors = ssl.mnch.fetal_infection(),
interventions = treatment,
modules = ssl.mnch.FetalHealth(),
networks = networks(),
**mnch_pars,
)
scenarios = {
'No infection': fetal(beta=0),
'Infection, untreated': fetal(beta=ss.peryear(20)),
'Infection, treated from 2001': fetal(beta=ss.peryear(20), treat=True),
}
msim = ss.parallel(scenarios, workers=1)
for label, sim in zip(scenarios, msim.sims):
res = sim.results.fetal_health
births = int(res.birth.sum())
print(label)
print(' Births:', births)
print(' Low birth weight (%):', round(100*res.lbw.sum()/births))
print(' Small vulnerable newborns (%):', round(100*res.svn.sum()/births))MultiSim: 3 runs of 3 scenarios (No infection, Infection, untreated, Infection, treated from 2001)
Done in 4.5 s
No infection
Births: 231
Low birth weight (%): 4
Small vulnerable newborns (%): 15
Infection, untreated
Births: 233
Low birth weight (%): 44
Small vulnerable newborns (%): 69
Infection, treated from 2001
Births: 232
Low birth weight (%): 11
Small vulnerable newborns (%): 33
To adapt this to another disease, build on fetal_infection and replace _apply_damage() with the disease’s own effects: for example, a larger growth penalty for severe infections than for mild ones.
Further reading
- WHO: Born too soon: the decade of action on preterm birth, and the definitions of preterm and low birth weight used here
- LiST: the Lives Saved Tool: a widely used model of the effect of maternal and child health interventions on mortality