#!/usr/bin/env python
# coding: utf-8
# # T11 - Advanced features
#
# This tutorial covers advanced features of Covasim, including custom population options and changing the internal computational methods.
#
#
# Click [here](https://mybinder.org/v2/gh/institutefordiseasemodeling/covasim/HEAD?urlpath=lab%2Ftree%2Fdocs%2Ftutorials%2Ftut_advanced.ipynb) to open an interactive version of this notebook.
#
#
# ## Defining populations with SynthPops
#
# For complex populations, we suggest using [SynthPops](http://synthpops.org), a Python library designed specifically for this purpose. In contrast the population methods built-in to Covasim, SynthPops uses data to produce synthetic populations that are statistically indistinguishable from real ones. For a relatively complex example of how SynthPops was used to create a complex school network for the Seattle region, see [here](https://github.com/institutefordiseasemodeling/testing-the-waters/blob/main/covasim_schools/school_pop.py).
#
# ## Defining contact layers
#
# As mentioned in Tutorial 4, contact layers are the graph connecting the people in the simulation. Each person is a node, and each contact is an edge. While enormous complexity can be used to define realistic contact networks, a reasonable approximation in many cases is random connectivity, often with some age assortativity. Here is an example for generating a new contact layer, nominally representing public transportation, and adding it to a simulation:
# In[ ]:
import numpy as np
import covasim as cv
cv.options(jupyter=True, verbose=0)
# Create the first sim
orig_sim = cv.Sim(pop_type='hybrid', n_days=120, label='Default hybrid population')
orig_sim.initialize() # Initialize the population
# Create the second sim
sim = orig_sim.copy()
# Define the new layer, 'transport'
n_people = len(sim.people)
n_contacts_per_person = 0.5
n_contacts = int(n_contacts_per_person*n_people)
contacts_p1 = cv.choose(max_n=n_people, n=n_contacts)
contacts_p2 = cv.choose(max_n=n_people, n=n_contacts)
beta = np.ones(n_contacts)
layer = cv.Layer(p1=contacts_p1, p2=contacts_p2, beta=beta) # Create the new layer
# Add this layer in and re-initialize the sim
sim.people.contacts.add_layer(transport=layer)
sim.reset_layer_pars() # Automatically add layer 'transport' to the parameters using default values
sim.initialize() # Reinitialize
sim.label = f'Transport layer with {n_contacts_per_person} contacts/person'
# Run and compare
msim = cv.parallel(orig_sim, sim)
msim.plot()
# ## Defining dynamic layers
#
# You can also define custom layers that update dynamically, e.g. based on a supplied number of contacts per day. To do this, create a new `Layer` class and define the `update()` method. For example:
# In[ ]:
import covasim as cv
import numpy as np
import sciris as sc
class CustomLayer(cv.Layer):
''' Create a custom layer that updates daily based on supplied contacts '''
def __init__(self, layer, contact_data):
''' Convert an existing layer to a custom layer and store contact data '''
for k,v in layer.items():
self[k] = v
self.contact_data = contact_data
return
def update(self, people):
''' Update the contacts '''
pop_size = len(people)
n_new = self.contact_data[people.t] # Pull out today's contacts
self['p1'] = np.array(cv.choose_r(max_n=pop_size, n=n_new), dtype=cv.default_int) # Choose with replacement
self['p2'] = np.array(cv.choose_r(max_n=pop_size, n=n_new), dtype=cv.default_int) # Paired contact
self['beta'] = np.ones(n_new, dtype=cv.default_float) # Per-contact transmission (just 1.0)
return
# Define some simple parameters
pars = sc.objdict(
pop_size = 1000,
n_days = 90,
)
# Set up and run the simulation
base_sim = cv.Sim(pars, label='Default simulation')
sim = cv.Sim(pars, dynam_layer=True, label='Dynamic layers')
sim.initialize()
# Update to custom layer
for key in sim.layer_keys():
contact_data = np.random.randint(pars.pop_size*10, pars.pop_size*20, size=pars.n_days+1) # Generate a number of contacts for today
sim.people.contacts[key] = CustomLayer(sim.people.contacts[key], contact_data=contact_data)
# Run and plot
msim = cv.parallel(base_sim, sim)
msim.plot()
# ## Defining custom population properties
#
# Another useful feature is adding additional features to people, for use in subtargeting. For example, this example shows how to define a subpopulation with higher baseline mortality rates. This is a simple example illustrating how you would identify and target people based on whether or not the have a prime-number index, based on the protecting the elderly example from Tutorial 1.
# In[ ]:
import numpy as np
import sciris as sc
import covasim as cv
def protect_the_prime(sim):
if sim.t == sim.day('2020-04-01'):
are_prime = sim.people.prime
sim.people.rel_sus[are_prime] = 0.0
pars = dict(
pop_type = 'hybrid',
pop_infected = 100,
n_days = 90,
verbose = 0,
)
# Default simulation
orig_sim = cv.Sim(pars, label='Default')
# Create the simulation
sim = cv.Sim(pars, label='Protect the prime', interventions=protect_the_prime)
sim.initialize() # Initialize to create the people array
sim.people.prime = np.array([sc.isprime(i) for i in range(len(sim.people))]) # Define whom to target
# Run and plot
msim = cv.parallel(orig_sim, sim)
msim.plot()
# ## Changing Numba options
#
# Finally, this example shows how you can change the default Numba calculation options. It's not recommended – especially running with multithreading, which is faster but gives stochastically unreproducible results – but it's there if you want it.
# In[ ]:
import covasim as cv
# Create a standard 32-bit simulation
sim32 = cv.Sim(label='32-bit, single-threaded (default)', verbose='brief')
sim32.run()
# Use 64-bit instead of 32
cv.options.set(precision=64)
sim64 = cv.Sim(label='64-bit, single-threaded', verbose='brief')
sim64.run()
# Use parallel threading
cv.options.set(numba_parallel=True)
sim_par = cv.Sim(label='64-bit, multi-threaded', verbose='brief')
sim_par.run()
# Reset to defaults
cv.options.set('defaults')
sim32b = cv.Sim(label='32-bit, single-threaded (restored)', verbose='brief')
sim32b.run()
# Plot
msim = cv.MultiSim([sim32, sim64, sim_par, sim32b])
msim.plot()