A "social bubble" policy lets people keep seeing a small, fixed set of others
while cutting off the rest of their contacts. bubbles() implements such a
policy for models built on an explicit contact network (e.g. ModelSEIR or
ModelSIR after agents_smallworld() or agents_from_edgelist()), following
the household-based rules used during COVID-19.
Usage
bubbles(
model,
household_id,
flavor = c("household", "peer"),
group_size,
transmission_factor = 0,
start_day = 0L,
end_day = -1L,
rewire_every = 0L,
name = "Social bubble",
max_households = 2L,
param_name = "Bubble transmission factor"
)
sample_household_sizes(n_agents, min_size = 2L, max_size = 6L)Arguments
- model
An object of class epiworld_model whose agents and contact network have already been created.
- household_id
Integer vector of household labels, one per agent, in agent order; its length must equal the number of agents. Labels are arbitrary and need not be consecutive. Agents sharing a label form a household and are always placed in the same bubble.
- flavor
Character scalar.
"household"if whole households choose a bubble together,"peer"if individuals choose contacts (see Details).- group_size
Integer scalar. For
"household", the maximum number of households per bubble (1gives a strict household-only lockdown). For"peer", the maximum number of contacts each agent may keep outside its household.- transmission_factor
Numeric scalar in [0, 1]. Multiplier applied to transmission between agents of different bubbles, i.e. how leaky the bubble is:
0(the default) is a perfectly efficient bubble that cuts out-of-bubble contact entirely,0.5halves it (a soft contact reduction), and1turns the intervention off. Contact within a bubble is never altered. This is the initial value of the model parameterparam_name; from then on the model, not the intervention, holds it (see Details).- start_day
Integer scalar. First day on which the policy applies.
- end_day
Integer scalar. Day on which the policy is lifted (exclusive); a negative value means it never lifts. Must be greater than
start_day.- rewire_every
Integer scalar. Re-randomize the bubbles every this-many days, for policies whose contacts change over time;
0keeps them fixed.- name
Character scalar. Name given to the intervention's tool and event.
- max_households
Integer scalar,
"peer"flavor only. The largest number of households a bubble may contain; a nomination that would exceed it is declined. This is what stops one household's choices from chaining into the next (see Details). The default,2, represents two households joined by a close contact. Ignored whenflavor = "household", wheregroup_sizeis already the cap.- param_name
Character scalar. Name of the model parameter in which the transmission factor is stored. Give two interventions deployed on the same model different names if they are to be dialled independently.
- n_agents
Integer scalar. The total number of agents to be divided into households.
- min_size, max_size
Integer scalar. The minimum and maximum size of each household.
Value
Invisibly returns model, which is modified in place.
The function sample_household_sizes() returns an integer vector of
household sizes that sum to n_agents, with each size between min_size
and max_size.
Details
The contact network is not modified. Instead, every agent receives a tool
that, on each exposure, compares the bubble of the susceptible agent with the
bubble of the infectious one: if they match, transmission is left untouched –
contacts inside the bubble are exactly what the policy preserves – and if
they differ, transmission is multiplied by the transmission factor. With a
factor of 0 this is equivalent to deleting the out-of-bubble contacts
(contact weights are uniform in these models), while leaving the network
available for other purposes (such as contact tracing or network summaries).
Dialling the policy through the model
The transmission factor is not stored in the intervention: bubbles()
registers it as a model parameter (param_name, "Bubble transmission factor" by default, overwriting any value the parameter already had), and the
tool reads it from the model on every exposure. It can therefore be inspected
with get_param(), changed at any point – including between runs, or from a
globalevent_fun() in the middle of one – with set_param(), and swept over
in a calibration without rebuilding the intervention:
bubbles(model, household_id, "household", group_size = 2)
set_param(model, "Bubble transmission factor", 0.25) # leakier bubblesValues outside [0, 1] are clamped when read.
How bubbles are formed
Bubbles are always disjoint (each agent is in exactly one), never split a household, and are only ever built between households that are actually connected in the contact network.
That last property matters: the intervention can only suppress transmission
along existing edges, never create new ones, so placing two households that
share no contact in the same bubble would change nothing. Pairing households
at random would make group_size inert – indistinguishable from a strict
lockdown however large the bubbles are. It also matches the real policy: a
household picks a bubble partner it already socialises with.
The algorithms
Both rules start from the household contact graph: one node per household, with an edge between two households whenever at least one member of the first is connected to a member of the second in the agents' contact network. Every random draw uses the model's generator, so a run is reproducible from its seed.
flavor = "household" grows bubbles from seed households:
Visit the households in random order.
Skip a household if it already belongs to a bubble. Otherwise open a new bubble containing it, and take its unassigned neighbours in the household contact graph as the frontier.
While the bubble holds fewer than
group_sizehouseholds and the frontier is not empty, draw a household from the frontier at random, add it, and extend the frontier with that household's unassigned neighbours. A household tied to several members of the bubble appears in the frontier more than once and is more likely to be drawn, so stronger ties are favoured.Stop when no unassigned neighbour is left, even if the bubble is smaller than
group_size.
Each bubble is therefore a connected group of households, though not
necessarily one where every pair is directly tied: with group_size > 2, two
households in the same bubble may be linked only through a third. Households
with no available partner stay on their own, so there are always at least
ceiling(n_households / group_size) bubbles.
flavor = "peer" has agents choose from the contacts still available:
Visit the agents in random order.
Skip an agent whose household is already in a full bubble: it has left the pool and can neither choose nor be chosen.
Otherwise draw one of the agent's contacts outside its own household, at random and without replacement, repeating until the agent has made
group_sizesuccessful choices or no contact is left that its bubble can still take in. A draw is accepted when the two households are in different bubbles and the merged bubble would hold at mostmax_householdshouseholds; otherwise that contact is unavailable and the agent draws again. Accepting merges the two households (household commitment: choosing a peer brings both households into the bubble), and the agent stops as soon as its bubble is full.
The cap is essential. Without it the merges percolate: with a few members per household each choosing someone, the household graph becomes connected and the entire population ends up in a single bubble, imposing no restriction at all.
In practice max_households is the dial that sets bubble size, while
group_size rarely binds: since a choice by any member commits the whole
household, one household's members tend to fill its bubble however many
choices each of them is allowed. With max_households = 2 a single accepted
choice fills the bubble, so group_size has no effect at all. Households whose
every contact was taken first stay on their own.
Timing and replicates
The bubbles are drawn afresh at the start of every run, using that run's seed,
and are already in force on day 1. With rewire_every > 0 they are redrawn
during the run, taking effect the following day. Because the intervention
keeps a single shared state, replicates with run_multiple must use one
thread (nthreads = 1); bubbles() flags the model accordingly.
What this cannot represent
Both rules produce exclusive bubbles, as the modelled policies prescribe. A rule that instead gives each person a personal budget of contacts that need be neither mutual nor exclusive – "up to ten different people a week", say – is not a partition of the population and cannot be expressed this way.
Fixed out-of-bubble "sport partners" and travel between regions are not yet modelled either.
The function sample_household_sizes() draws a vector of household sizes
that sum to n_agents, with each size between min_size and max_size. It
ensures that the total number of agents is exactly n_agents by adjusting
the last household size if necessary.
Examples
# A network SEIR model on a small-world contact network
model <- ModelSEIR(
name = "Flu",
prevalence = 0.01,
transmission_rate = 0.1,
incubation_days = 4.5,
recovery_rate = 1 / 7
)
agents_smallworld(model, n = 2000, k = 8, d = FALSE, p = 0.05)
# Households of three agents (in agent order)
household_id <- rep(seq_len(ceiling(2000 / 3)), each = 3)[1:2000]
# Two households per bubble from day 10, halving out-of-bubble transmission
bubbles(
model,
household_id = household_id,
flavor = "household",
group_size = 2,
transmission_factor = 0.5,
start_day = 10
)
# The factor is a model parameter, so it can be changed without rebuilding
# the intervention
get_param(model, "Bubble transmission factor")
#> [1] 0.5
set_param(model, "Bubble transmission factor", 0.25)
run(model, ndays = 100, seed = 55)
#> _________________________________________________________________________
#> Running the model...
#> ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| done.
model
#> ________________________________________________________________________________
#> Susceptible-Exposed-Infected-Removed (SEIR)
#> It features 2000 agents, 1 virus(es), and 1 tool(s).
#> The model has 4 states.
#> The final distribution is: 257 Susceptible, 10 Exposed, 28 Infected, and 1705 Removed.
# Other policies:
#
# Strict household-only lockdown (a common reference scenario):
# bubbles(model, household_id, "household", group_size = 1)
#
# One close contact per person, joining the two households:
# bubbles(model, household_id, "peer", group_size = 1)
#
# As above, but the bubbles are drawn again every week:
# bubbles(model, household_id, "peer", group_size = 1, rewire_every = 7)
#
# Individually chosen bubbles of up to four households:
# bubbles(model, household_id, "peer", group_size = 2, max_households = 4)
