Skip to main content

Mixing

🧠8% 🤖92%

A disease model says how infectious people are; the mixing says who meets whom, and how often. This page goes from the simplest mixing to the most detailed: homogeneous mixing, mixing pools, contact matrices between groups (such as age groups), and networks of individual contacts. Everything but networks runs as written as compartments or as agents; networks are about particular people, so only agents run them as written, and a compartmental solver approximates them (below). Movement between places is in Places and data, and the tutorial T4 – Mixing builds up the same material as a single worked path.

Choosing how people mix

The disease and the mixing are separate: changing from homogeneous mixing to a network does not change the disease. Each way of mixing is a route (a path by which infection can pass), given to the sim with networks=, which takes networks and mixing pools alike, singly or as a list:

  • No route given: homogeneous mixing, the assumption of the standard ODE models. The transmission rate β (beta) is a rate per unit time: in S >> I @ beta*I, each susceptible person is infected at a rate of β times the fraction of people who are infectious.
  • A mixing pool, ss.MixingPool(n_contacts=10): homogeneous mixing with 10 contacts per unit time. β is then per contact, so β × the number of contacts is the transmission rate.
  • A contact matrix, ss.contacts(...): mixing between groups, such as age groups, with a number of contacts for each pair of groups (see below).
  • A network, such as ss.RandomNet(n_contacts=10) or ss.SexualNet(): contacts between particular people, with β per contact (or per sex act, on a network with acts).

Which to choose depends on how people meet:

How people meet Use Runs as
Many brief contacts, at random No route (homogeneous mixing), or ss.MixingPool() Compartments or agents
At random, but more within some groups (e.g. by age) ss.contacts() with a stratified disease Compartments or agents
The same few people, again and again (households, partners) A network: ss.RandomNet(dynamic=False), ss.SexualNet(), or the library’s household networks Agents (a compartmental solver approximates it: see below)
From mother to child ss.PrenatalNet(), with ss.Pregnancy() Agents

Here is the same disease under three kinds of mixing. With no route, β = 0.05 per day and an infectious period of 10 days give R0 = 0.5; with 10 contacts a day, R0 = 5, and the mixing pool and the random network give nearly the same epidemic:

import starsim as ss
import starsim.library as ssl

S, I, R = ss.states('S I R')
infection = S >> I @ 0.05*I       # β = 0.05: per day with no route, per contact with one
recovery  = I >> R @ ss.days(10)  # Infectious for 10 days on average
sir = ss.Disease(infection, recovery, init_prev=0.01)

kw = dict(n_agents=10_000, stop=ss.days(120), verbose=0)
scenarios = {
    'Homogeneous mixing':    ss.Sim(sir, **kw),
    'Mixing pool (10 contacts a day)': ss.Sim(sir, networks=ss.MixingPool(n_contacts=10), **kw),
    'Random network (10 contacts a day)': ss.Sim(sir, networks=ss.RandomNet(n_contacts=10), **kw),
}
msim = ss.parallel(scenarios)
msim.plot('sir.I')

library(starsim)

states("S I R")
sir <- Disease(
  infection = Transition(S, I, rate = 0.05 * I), # β = 0.05: per day with no route, per contact with one
  recovery  = Transition(I, R, dur = days(10)),  # Infectious for 10 days on average
  init_prev = 0.01
)

make_sim <- function(...) { # A sim with these settings, and any others given
  Sim(sir, n_agents = 10000, stop = days(120), verbose = 0, ...)
}
msim <- parallel(list(
  "Homogeneous mixing"                 = make_sim(),
  "Mixing pool (10 contacts a day)"    = make_sim(networks = ss$MixingPool(n_contacts = 10)),
  "Random network (10 contacts a day)" = make_sim(networks = ss$RandomNet(n_contacts = 10))
))
msim$plot("sir.I")

Transmission along every route is frequency-dependent: what matters is the fraction of a person’s contacts who are infectious (a density-dependent rate is written with the count, ss.perday(3e-5)*I.n; see Diseases). β belongs to the disease, not to the route, and a disease transmits over every route unless given a β of 0 for it (see Transmission that differs by route).

Which solver runs it

A model whose mixing has a compartmental form (no route, a mixing pool, or a contact matrix) runs as an ODE unless it is given agents. A model with a network runs as agents. A compartmental solver can still be asked for (solver='ode'); it then approximates the network, and says so in one line:

Route As agents As compartments
none given exact exact
ss.MixingPool(n_contacts=c) exact exact
ss.networks.MixingPools(...), ss.contacts(...) exact exact
ss.RandomNet(n_contacts=c) exact homogeneous mixing with c contacts per unit time
ssl.StaticNet(graph=...) exact homogeneous mixing with the graph’s mean number of contacts
ss.DegreeNet(), ss.SexualNet(), and other partnership networks exact the partner-change model (partnerships formed at a rate, each with the chance it transmits over its whole length), which needs the model stratified by sex for ss.SexualNet

The approximations are rough where the network matters most; see Solvers and backends. Contact tracing, partner notification, and the transmission tree need a network, and so agents.

Numbers of contacts

n_contacts= is a mean on every route, and a distribution gives its shape. For a count of contacts (ss.RandomNet, mixing pools), a number gives everyone that many; for a target number of partners (ss.DegreeNet, ss.SexualNet) or a size (ssl.ClusterNet’s cluster_size), it is the mean of a Poisson. A distribution says otherwise: ss.poisson(10) for contacts that vary between people, ss.constant(1) for exactly one partner each. A compartmental run uses only the mean.

Mixing pools

A mixing pool is homogeneous mixing among a group of people: no individual contacts, but each person has an effective number of contacts in the pool (n_contacts; a distribution is drawn once for each person). With no groups, a pool is everyone. source= and target= restrict it to the people who can infect and the people who can be infected, so a pool can be one group, or one-way between two; each is a condition on people (e.g. ss.person.age < 15), a function of the sim, or UIDs. Each susceptible person in the pool has a hazard of β × the mean infectiousness of the sources × their number of contacts × their rel_sus: the term β × S × I/N of the ODE, applied to each person. Here a flu-like infection spreads through a school, in which children aged 5 to 14 have 10 contacts a day with each other, and the community, in which everyone has 3:

school = ss.MixingPool(
    name = 'school',
    source = (ss.person.age >= 5) & (ss.person.age < 15), # Children aged 5-14 infect ...
    target = (ss.person.age >= 5) & (ss.person.age < 15), # ... each other
    n_contacts = ss.poisson(mean=10),    # 10 contacts a day on average
)
community = ss.MixingPool(name='community', n_contacts=ss.poisson(mean=3))

S, I, R = ss.states('S I R')
infection = S >> I @ 0.03*I       # Per contact
recovery  = I >> R @ ss.days(5)
flu = ss.Disease(infection, recovery, init_prev=0.001)
flu.stratify(age=[0, 5, 15]) # Ages 0-4, 5-14, and 15+, for the results
sim = ss.Sim(flu, networks=[school, community], n_agents=10_000, stop=ss.days(120), verbose=0)
sim.run()
sim.results.flu.infection.by('age').plot()

ss.networks.MixingPools (plural) is a set of pools between groups, with source and target as dicts of groups and n_contacts as a matrix read as a contact survey is: row i is the contacts of a person in target group i, with each source group by column. For mixing by age, the usual case, ss.contacts() makes it from a contact matrix, as below.

Contact matrices

A contact matrix by age

A contact matrix needs groups to act between, so the model is stratified by age first (see Stratification), here with the fraction of the population in each group. ss.contacts() takes the matrix and the lower edge of each of its age groups, and β is per contact:

import starsim as ss

S, I, R = ss.states('S I R')
infection = S >> I @ 0.03*I       # β = 0.03 per contact
recovery  = I >> R @ ss.days(5)   # Infectious for 5 days on average
flu = ss.Disease(infection, recovery, init_prev=0.001)
flu.stratify(age={0: 0.3, 15: 0.6, 65: 0.1}) # Ages 0-14, 15-64, and 65+, with the fraction of people in each

contacts = ss.contacts(ages=[0, 15, 65], matrix=[[10, 4, 0.5], [2, 8, 0.5], [1.5, 3, 2]]) # Contacts a day
sim = ss.Sim(flu, networks=contacts, total_pop=100_000, stop=ss.days(150))
sim.run()
sim.results.flu.I.by('age').plot()
Sim: flu, 100,000 people, as an ODE, days 0 to 150
  flu: R0 = 1.83, with 100 infectious at the start
  Done in 0.0059 s; flu: 67,902 infections; at most 10,741 infectious, on day 40

library(starsim)

states("S I R")
flu <- Disease(
  infection = Transition(S, I, rate = 0.03 * I), # β = 0.03 per contact
  recovery  = Transition(I, R, dur = days(5)),   # Infectious for 5 days on average
  init_prev = 0.001
)
flu$stratify(age = list("0" = 0.3, "15" = 0.6, "65" = 0.1)) # Ages 0-14, 15-64, and 65+, with the fraction of people in each

age_contacts <- contacts(ages = c(0, 15, 65), matrix = rbind(c(10, 4, 0.5), c(2, 8, 0.5), c(1.5, 3, 2))) # Contacts a day
sim <- Sim(flu, networks = age_contacts, total_pop = 100000, stop = days(150))
sim$run()
sim$results$flu$I$by("age")$plot()

With a contact matrix, the R0 that the sim prints is β × the infectious period × the largest eigenvalue of the matrix (about 12.2 here, so R0 ≈ 1.8).

Reciprocity. Each contact between two groups is counted from both sides, so Ni × Cij = Nj × Cji. Starsim uses a matrix as given, with each row as the contacts of a person in that group, and says so in a note when it is not reciprocal for the sizes of the model’s groups.

The forms of a matrix

ss.contacts() takes a matrix and what it is over:

Form Example The groups
A matrix with its own age groups ss.contacts(matrix, ages=[0, 15, 65]) The matrix’s own, given by their lower edges; they need not be the model’s
A matrix over the model’s groups ss.contacts(matrix) (its age groups), ss.contacts(risk=matrix) The levels of the model’s stratification by that name, in order
One matrix over several stratifications together ss.contacts(matrix, dims=['sex', 'risk']), with an axis for each, rows before columns (e.g. shape 2 × 2 × 2 × 2) Every combination of their levels
Everyone with everyone ss.contacts(10): 10 contacts per person per unit time (an ss.MixingPool of everyone) None
A matrix for each of several stratifications ss.contacts(age=..., risk=...) Every combination of their levels, with the product of the entries for each
A country’s matrix, by name ss.contacts('Kenya') 5-year age groups, from a published study

A matrix with its own age groups suits data from a study. In a compartmental run it is aggregated to the model’s age groups, weighted by population; as agents, each person is counted in the matrix’s group for their age. Groups need not be ages: here 20% of people are at high risk, with four times as many contacts with each other as people at low risk have:

S, I = ss.states('S I')
infection = S >> I @ 0.05*I
recovery  = I >> S @ ss.days(10) # No lasting immunity
sis = ss.Disease(infection, recovery, init_prev=0.01)
sis.stratify(risk={'low': 0.8, 'high': 0.2})

mixing = ss.contacts(risk=[[2, 1], [4, 8]]) # Reciprocal: 80% × 1 = 20% × 4
sim = ss.Sim(sis, networks=mixing, total_pop=100_000, stop=ss.days(200))
sim.run()
sim.results.sis.I.by('risk').plot()
Sim: sis, 100,000 people, as an ODE, days 0 to 200
  sis: R0 = 4.3, with 1,000 infectious at the start
  Done in 0.0026 s; sis: 1,028,384 infections; at most 53,280 infectious, on day 200

The entries of a matrix are contacts per unit time, in the sim’s unit unless unit= says otherwise (e.g. unit='day', so that in a sim in years, 10 is not taken as 10 contacts a year). A country’s matrix is always contacts a day.

What a matrix acts on

The rate of the infection says what the matrix multiplies: the fraction of each group who are infectious, their number, or their number as a fraction of everyone. With C the matrix, Ij and Nj the number infectious and the number of people in group j, and N everyone:

The rate The force of infection on group i The matrix is As in
beta*I β Σj Cij Ij / Nj Contacts per person with each group, and β is per contact Contact surveys and the matrices of countries; frequency-dependent models
beta*I.n β Σj Cij Ij A rate for each pair of people (density-dependent) Lloyd & Jansen (2004), and metapopulation models in densities
beta*I.frac, or beta*I.n/N β Σj Cij Ij / N A rate on the fraction of everyone who are infectious in each group Keeling & Rohani (2008), chapter 3; the WAIFW (who acquires infection from whom) matrices of Bjørnstad (2018)

So a matrix from a textbook goes in as given, with the rate written as the textbook writes it. Keeling & Rohani’s Program 3.1, an SIS in a high- and a low-risk group whose β acts on fractions of the whole population, is:

import numpy as np
beta = np.array([[10, 0.1], [0.1, 1]]) # Per year: row i is the group infected, column j the group infecting
S, I = ss.states('S I')
sis = ss.Disease(S >> I @ I.frac, I >> S @ ss.peryear(1), init=dict(I=ss.by(risk=[1e-5/0.2, 1e-3/0.8])), name='sis')
sis.stratify(risk={'high': 0.2, 'low': 0.8})
sim = ss.Sim(sis, networks=ss.contacts(risk=beta), total_pop=1, stop=15, unit='year')
sim.run()
print('Infectious at year 15, as fractions of everyone:', sim.results.sis.I.by('risk').values[-1].round(6)) # The book's 0.100132 and 0.026810
Sim: sis, in proportions (total_pop=1), as an ODE, years 0 to 15
  sis: R0 = 2, with 0.00101 infectious at the start
  Done in 0.0044 s; sis: 1.06 infections; at most 0.127 infectious, on year 15
Infectious at year 15, as fractions of everyone: [0.100136 0.02681 ]

equations() says what a matrix acts on in each rate. In a model with places, N is everyone in the same place. A weighted count works the same way: beta*I.n_infectious weights each group by its infectiousness (rel_trans).

Contact matrices by setting

A country’s contact matrix can be split by setting: ss.contacts('Kenya', layer=['home', 'school', 'work', 'community']) gives a matrix for each, named after it, which together make up the whole (layer='all', the default). study= picks the study they come from (see Places and data). Each layer can then be changed on its own with ss.modify() (see Interventions); here schools close from day 10:

S, I, R = ss.states('S I R')
infection = S >> I @ 0.03*I
recovery  = I >> R @ ss.days(5)
flu = ss.Disease(infection, recovery, init_prev=0.001)
flu.stratify(age=[0, 5, 15, 65]) # The fractions in each come from the population

layers = ss.contacts('Kenya', layer=['home', 'school', 'work', 'community'])
home, school, work, community = layers
closure = ss.modify(school, multiply=0, start=ss.days(10)) # No school contacts from day 10

kw = dict(people=ss.population('Kenya'), networks=layers, stop=ss.days(100), verbose=0)
scenarios = {
    'Schools open': ss.Sim(flu, **kw),
    'Schools closed from day 10': ss.Sim(flu, interventions=closure, **kw),
}
msim = ss.parallel(scenarios)
msim.plot('flu.I')

Each layer’s matrix is also a table by age group (ss.data.contacts('Kenya', layer='school')), which ss.contacts() also takes, e.g. after changing it.

Contact matrices as agents

Everything above runs as agents too: given n_agents, the matrix works as a set of mixing pools, one for each pair of groups (ss.networks.MixingPools), not as a network of particular contacts. Everyone in an age group then has the mean number of contacts; for contacts that differ between people, give a distribution whose parameters are the matrix, here a lognormal with a standard deviation of half the mean:

C = ss.data.contacts('Kenya') # The mean contacts a day, by 5-year age group
mixing = ss.contacts(ss.lognormal(mean=C, std=C*0.5))

S, I, R = ss.states('S I R')
flu = ss.Disease(S >> I @ 0.03*I, I >> R @ ss.days(5), init_prev=0.001)
sim = ss.Sim(flu, people=ss.population('Kenya', n_agents=10_000), networks=mixing, stop=ss.days(100), verbose=0).run()
sim.results.flu.I.plot()

Each person draws their contacts when they enter an age group, and keeps them while they are in it. Any distribution of values that are not negative will do (e.g. ss.poisson(C)); a compartmental run uses the means.

Networks

Random networks

ss.RandomNet pairs people at random, with n_contacts contacts per person. By default the contacts are redrawn every step; with dynamic=False, they are made once at the start and kept, and with dur=, each lasts that long and is then replaced. who= restricts the network to some people. Several networks can be given together, each with its own name, as a layered population: here households (fully connected clusters of about three people, ssl.ClusterNet) and contacts at work among adults:

households = ssl.ClusterNet(name='households', cluster_size=ss.poisson(3))
work = ss.RandomNet(name='work', n_contacts=10, who=(ss.person.age >= 20) & (ss.person.age < 65), dynamic=False)

S, I = ss.states('S I')
infection = S >> I @ 0.02*I       # Per contact, per day
recovery  = I >> S @ ss.days(10)  # No lasting immunity
sis = ss.Disease(infection, recovery, init_prev=0.01)

def make_sim(networks):
    return ss.Sim(sis, networks=networks, n_agents=2000, stop=ss.days(200), verbose=0)

msim = ss.parallel({'Work only': make_sim(work), 'Households and work': make_sim([households, work])})
msim.plot('sis.I')
print('Number of households:', msim.sims[1].networks.households.n_clusters)

Number of households: 669

The same β applies on every layer unless it is given by route. ssl.HybridNet gives the four layers of Covasim in one: households (h), schools (s), workplaces (w), and the community (c); once in a sim, it becomes four networks, each reached on its own, e.g. sim.networks.s.

Sexual networks

ss.SexualNet is a network of partnerships between two groups of people: by default a man and a woman, or two men (between='mm'), or any two groups by name, such as female sex workers and their clients (between=('fsw', 'client')). A partnership lasts dur (a duration is a mean; a distribution gives any other spread), and has acts sex acts per unit time, for a probability of transmission per act.

By default, people with no partner are paired on every step, so nobody has more than one partner at a time. With n_contacts=, the mean number of partners at a time is a target instead, and so are concurrency= (the fraction with two or more), max_contacts=, and match= (the fraction of partnerships within the same level of a property, e.g. match=dict(risk=0.8)). net.check() runs the sim again and compares the network it makes with each target:

net = ss.SexualNet(n_contacts=1.2, concurrency=0.1, dur=ss.years(2))
sim = ss.Sim(n_agents=2000, networks=net, start=2000, stop=2010, dt=ss.months(1), verbose=0)
sim.run()
sim.networks.sexualnet.check(output=True)
/home/cliffk/idm/ssv4/starsim/networks.py:1737: RuntimeWarning:


the network "sexualnet" has concurrency 0.0806, against a target of 0.1

/home/cliffk/idm/ssv4/starsim/networks.py:1737: RuntimeWarning:


the network "sexualnet" has dur 35.2, against a target of 24
target realized spread
mean_degree 1.2 1.095715 0.053972
concurrency 0.1 0.080621 0.008167
max_contacts NaN 9.333333 2.081666
dur 24.0 35.162809 0.919516

ss.SexualNet is ss.DegreeNet between men and women, with an age of debut and participation. Partnerships that are not sexual, such as needle sharing, are ss.DegreeNet itself, which takes the same targets among anyone (or the people that who= gives, or between two groups with between=).

Mother-to-child networks

Transmission from mother to child needs pregnancy (ss.Pregnancy; see People and demographics), which adds and removes the one-way edges from mother to child: ss.PrenatalNet while the child is in the womb, ss.BreastfeedingNet while the mother breastfeeds, and ss.PostnatalNet for a period after birth (dur=). Here a chronic infection passes through partnerships, to children before birth, and through breastfeeding:

S, I = ss.states('S I')
beta = dict(
    sexualnet        = ss.peract(m2f=0.002, f2m=0.001), # Chance of infection per sex act
    prenatalnet      = ss.permonth(0.03),               # Rate of infection per month of pregnancy
    breastfeedingnet = ss.permonth(0.008),              # Rate of infection per month of breastfeeding
)
infection = S >> I @ beta*I
chronic = ss.Disease(infection, init_prev=ss.by(age=[0, 0.1])) # 10% of adults, and no children, at the start
chronic.stratify(age=[0, 15]) # Children (0-14) and adults (15+)

pregnancy = ss.Pregnancy(fertility_rate=ss.peryear(0.1)) # 100 conceptions per 1,000 women a year
deaths = ss.Deaths(death_rate=ss.peryear(0.01))          # 10 deaths per 1,000 people a year
sim = ss.Sim(chronic, networks=[ss.SexualNet(), ss.PrenatalNet(), ss.BreastfeedingNet()], demographics=[pregnancy, deaths],
             analyzers='infection_log', n_agents=5000, start=2000, stop=2020, dt=ss.months(1), verbose=0)
sim.run()
sim.results.chronic.I.select(age='0-14').plot()
print(sim.analyzers.infection_log.log.to_df().network.value_counts())

network
sexualnet           420
prenatalnet          85
breastfeedingnet     18
Name: count, dtype: int64

Working with edges

A network keeps its edges as arrays: p1 (the first person in each edge), p2 (the second), and weight (a multiplier on the chance each edge transmits, 1 by default), reached as net.edges or as net['p1']. net.find_contacts(uids) gives the contacts of some people, net.to_df() the edges as a table, and a network can be made from a table of edges with from_df():

sim = ss.Sim(n_agents=1000, networks=ss.RandomNet(n_contacts=4), stop=ss.days(10), verbose=0)
sim.run()
net = sim.networks.randomnet
print('Edges:', len(net))
print('Contacts of person 0:', net.find_contacts([0]))

custom = ss.Network(name='custom').from_df(dict(p1=[0, 1, 2], p2=[1, 2, 3]))
custom.to_df()
Edges: 2000
Contacts of person 0: [ 98 138 474 518 540 714]
p1 p2 weight
0 0 1 1.0
1 1 2 1.0
2 2 3 1.0

net.remove_edges() removes edges by a condition on them (e.g. net['p1'] < 100), net.to_graph() gives the network as a NetworkX graph, and net.plot() draws it. For a network of partnerships, net.degrees(uids) gives the number of partners each person has now, and memory= (e.g. ss.SexualNet(memory=ss.years(1))) keeps partnerships that ended within that time, so that contact tracing (the trace= of a delivery: see Interventions) also reaches recent past partners.

Writing a network

A network written as code is a subclass of one of the networks in the appendix (usually ss.DynamicNetwork, for partnerships that form and end, or ss.SexualNet) that overrides how edges are made. The methods it can override:

Method What it does
add_pairs() Makes new edges; the main method to override
remove_pairs() Ends edges, e.g. those whose duration is over
step() Called every step; by default removes, then adds, pairs
init_post() Sets the network up once the people exist
find_contacts() Gives the contacts of some people

Custom modules has the general pattern, and the tutorial T8 – Custom modules writes a network step by step.

Transmission that differs by route

How infectious someone is can depend on the route: HIV, for example, passes far more readily through a shared needle than through sex. The disease then gives one β for each route, as a dict keyed by the route’s name (the network’s class in lowercase, unless it was given a name=), with a value for every route. On a network with directions, β takes a value for each: ss.peract(m2f=..., f2m=...) is a probability per act from men to women and from women to men. On a network without acts, β is a rate for each contact with an infectious person, e.g. ss.peryear(0.2). The infection log (ss.infection_log; see Analyzers) records the route of each infection:

S, I = ss.states('S I')
beta = dict(randomnet=ss.peryear(0.2), sexualnet=ss.peract(m2f=0.01, f2m=0.005))
infection = S >> I @ beta*I
recovery  = I >> S @ ss.years(1)
sis = ss.Disease(infection, recovery, init_prev=0.05)

sim = ss.Sim(sis, networks=[ss.RandomNet(), ss.SexualNet()], analyzers='infection_log', n_agents=2000, start=2000, stop=2020, dt=ss.months(1), verbose=0)
sim.run()
log = sim.analyzers.infection_log.log.to_df()
print('Infections by route:')
print(log.network.value_counts()) # The seeds at the start have no route
Infections by route:
network
randomnet    17584
sexualnet     2724
Name: count, dtype: int64

Pitfalls

  • β changes meaning when a route is added. With no route, β is a rate per unit time; with a mixing pool, contact matrix, or network, it is a rate per contact, so a contact with an infectious person infects with probability 1 − e^−β·dt over a step (give ss.probperweek() and the like for a chance per contact over a time; ss.peract() for one per act). Adding networks=ss.RandomNet(n_contacts=10) to a model fitted with homogeneous mixing multiplies its R0 by 10. Check the R0 that the sim prints.
  • A probability per act needs acts. ss.peract() on a route without acts (a random network, a mixing pool, a mother-to-child network) raises an error; give a rate per contact instead.
  • A contact matrix needs groups. A matrix with its own age groups, on a model with no age groups, stratifies it by the matrix’s groups, with the fraction in each from the world’s population, a guess that describe() marks; give the fractions (or a named population) if they matter.

Further reading

Appendix: every network

The networks and mixing pools in Starsim, with their main parameters (see Networks for all of them):

Network What it is Main parameters
ss.RandomNet Contacts at random, redrawn each step, lasting, or made once n_contacts, dur, dynamic, who, method, memory
ss.DegreeNet Partnerships among anyone, or between two groups, with targets for the number of partners between, who, dur, acts, n_contacts, concurrency, max_contacts, match, memory
ss.SexualNet Partnerships between two groups (men and women by default): ss.DegreeNet with sexual debut and participation between, dur, acts, debut, participation, n_contacts, concurrency, max_contacts, match, memory
ss.PrenatalNet Mother and unborn child, from ss.Pregnancy none
ss.BreastfeedingNet Mother and child while she breastfeeds, from ss.Pregnancy none
ss.PostnatalNet Mother and child after birth dur
ss.MixingPool Homogeneous mixing within a group, or from one group to another source, target, n_contacts
ss.networks.MixingPools Mixing between several groups source, target, n_contacts (a matrix, or a distribution of matrices, e.g. ss.poisson(M))
ss.contacts() A contact matrix (returns ss.networks.MixingPools) see The forms of a matrix
ss.Network, ss.DynamicNetwork The base classes, for a network of your own (see above) or one made from a table of edges (from_df())

The library (import starsim.library as ssl) has more:

Network What it is Main parameters
ssl.ClusterNet Fully connected clusters, such as households cluster_size, dynamic, who
ssl.HouseholdNet Households from survey data (such as the DHS), which also sets each person’s age and sex to match; with ss.Pregnancy, newborns join their mother’s household dhs_data, dynamic
ssl.HybridNet Households, schools, workplaces, and community, as in Covasim household_size, n_contacts, school_ages, work_ages, clustered
ssl.StaticNet A fixed network from a NetworkX graph (such as one of its generators), by default an Erdős–Rényi graph (each pair of people connected with a probability) graph, p, n_contacts
ssl.GeoNet People in real places, with contacts falling off with distance places, local, c, n_contacts
ssl.GridNet People on a square grid diagonal, periodic
ssl.SmallWorldNet, ssl.ScaleFreeNet The classic small-world and scale-free graphs n_contacts, rewire; n_contacts
ssl.DiskNet People who move in a plane, in contact within a radius r, v
ssl.NullNet No contacts, for testing none