{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# T8 - Vaccines and variants\n", "\n", "This tutorial covers several of the features new to Covasim 3.0, including waning immunity, multi-variant modelling, and advanced vaccination methods." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", " \n", "Click [here](https://mybinder.org/v2/gh/institutefordiseasemodeling/covasim/HEAD?urlpath=lab%2Ftree%2Fdocs%2Ftutorials%2Ftut_immunity.ipynb) to open an interactive version of this notebook.\n", " \n", "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Using waning immunity\n", "\n", "By default, immunity is assumed to grow and then wane over time according to a two-part exponential decay of neutralizing antibodies. This can be changed by setting `use_waning=False`, such that infection is assumed to confer lifelong perfect immunity, meaning that people who have been infected cannot be infected again.\n", "When `use_waning` is set to True, agents in the simulation are assigned a peak level of neutralizing antibodies upon infection or vaccination, drawn from a distribution defined in the parameter dictionary.\n", "This level grows to its peak and then decays over time, leading to declines in the efficacy of protection against infection, symptoms, and severe symptoms.\n", "The following example creates simulations without waning immunity, and compares it to simulations with different speeds of immunity waning." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import covasim as cv\n", "import pylab as pl\n", "cv.options(jupyter=True, verbose=0)\n", "\n", "# Create sims with and without waning immunity\n", "sim_nowaning = cv.Sim(n_days=120, use_waning=False, label='No waning immunity')\n", "sim_waning = cv.Sim(n_days=120, label='Waning immunity')\n", "\n", "# Now create an alternative sim with faster decay for neutralizing antibodies\n", "sim_fasterwaning = cv.Sim(\n", " label='Faster waning immunity',\n", " n_days=120,\n", " nab_decay=dict(form='nab_growth_decay', growth_time=21, decay_rate1=0.07, decay_time1=47, decay_rate2=0.02, decay_time2=106)\n", ")\n", "\n", "# Create a multisim, run, and plot results\n", "msim = cv.parallel(sim_nowaning, sim_waning, sim_fasterwaning)\n", "msim.plot()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Multi-variant modeling\n", "\n", "The next examples show how to introduce new variants into a simulation.\n", "These can either be known variants of concern, or custom new variants.\n", "New variants may have differing levels of transmissibility, symptomaticity, severity, and mortality.\n", "When introducing new variants, `use_waning` must be set to `True`.\n", "The model includes known information about the levels of cross-immunity between different variants.\n", "Cross-immunity can also be manually adjusted." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Define three new variants: B117, B1351, and a custom-defined variant\n", "alpha = cv.variant('alpha', days=0, n_imports=10)\n", "beta = cv.variant('beta', days=0, n_imports=10)\n", "custom = cv.variant(label='3x more transmissible', variant={'rel_beta': 3.0}, days=7, n_imports=10)\n", "\n", "# Create the simulation\n", "sim = cv.Sim(variants=[alpha, beta, custom], pop_infected=10, n_days=32)\n", "\n", "# Run and plot\n", "sim.run()\n", "sim.plot('variant')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Advanced vaccination methods\n", "\n", "The intervention `cv.BaseVaccination` allows you to introduce a selection of known vaccines into the model, each of which is pre-populated with known parameters on their efficacy against different variants, their durations of protection, and the levels of protection that they afford against infection and disease progression. The prioritization of vaccines is implemented with derived classes that implement specific allocation algorithms. Covasim 3.0 comes with two such algorithms:\n", "\n", "- `cv.vaccinate_prob()` - specify a daily probability of vaccination for each person\n", "- `cv.vaccinate_num()` - specify a sequence of people to vaccinate, and the number of available doses each day\n", "\n", "When using any of these vaccination interventions, `use_waning` must be set to `True`.\n", "\n", "### Probability-based vaccination\n", "\n", "The intervention `cv.vaccinate_prob()` allows you specify the daily probability that each individual gets vaccinated. The following example illustrates how to use the `cv.vaccinate_prob()` intervention." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Create some base parameters\n", "pars = dict(\n", " beta = 0.015,\n", " n_days = 90,\n", ")\n", "\n", "# Define probability based vaccination\n", "pfizer = cv.vaccinate_prob(vaccine='pfizer', days=20, prob=0.8)\n", "\n", "# Create and run the sim\n", "sim = cv.Sim(pars=pars, interventions=pfizer)\n", "sim.run()\n", "sim.plot(['new_infections', 'cum_infections', 'new_doses', 'cum_doses'])" ] }, { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "### Sequence-based vaccination\n", "\n", "To use `cv.vaccinate_num`, it is necessary to specify the vaccine prioritization - for example, this may involve defining priority groups like 1A, 1B etc. depending on the setting. The vaccine prioritization is specified as an ordered sequence of people to vaccinate, so in almost all cases, a function can be defined that takes in a `cv.People` instance, and returns an array of indices specifying the order in which people get vaccinated. This function could also incorporate steps such as randomizing the order of people within priority groups, or removing some people from the sequence to account for vaccine hesitancy and peak coverage not reaching 100%. A simple example of a prioritization function would be to simply sort by age in descending order i.e." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def prioritize_by_age(people):\n", " return np.argsort(-people.age)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", " \n", "**Note:** Because age prioritization is so commonly used, you can use the shortcut `sequence='age'` rather than specify this function.\n", " \n", "
" ] }, { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "This function can be passed to `cv.vaccinate_num` where it will be evaluated during initialization, and therefore will run after the population has been generated. In cases where the `cv.People` have been generated offline and are being loaded instead of generated, it's possible to pass a pre-computed sequence of indices to `cv.vaccinate_num` rather than a prioritization function that returns the sequence. \n", "\n", "The example below also shows how to use a simple `Analyzer` to capture additional information about the vaccine state each timestep." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Record the number of doses each person has received each day so\n", "# that we can plot the rollout in this example. Without a custom \n", "# analyzer, only the total number of doses will be recorded\n", "n_doses = []\n", "\n", "# Define sequence based vaccination\n", "pfizer = cv.vaccinate_num(vaccine='pfizer', sequence=prioritize_by_age, num_doses=100)\n", "sim = cv.Sim(\n", " pars=pars,\n", " interventions=pfizer,\n", " analyzers=lambda sim: n_doses.append(sim.people.doses.copy())\n", ")\n", "sim.run()\n", "\n", "pl.figure()\n", "n_doses = np.array(n_doses) # n_doses is an array with shape of [n_days x num_agents] \n", "fully_vaccinated = (n_doses == 2).sum(axis=1) # this line calculates how many we agents had 2 doses on a given day\n", "first_dose = (n_doses == 1).sum(axis=1) # this line calculates how many we agents had 1 dose on a given day\n", "pl.stackplot(sim.tvec, first_dose, fully_vaccinated)\n", "pl.legend(['First dose', 'Fully vaccinated'])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Notice how second doses are prioritized, so after 21 days, there is a backlog of people requiring second doses, so the first doses are suspended until all of the second doses have been delivered. In reality, the pace of vaccination typically increases following the commencement of vaccination, so capacity increases over time. The `num_doses` argument allows this increase to be captured. There are several ways to specify a time-varying number of doses, including a date-based dictionary to facilitate calibration when the number of doses each day is known. A simple option is to use a function that returns the number of doses to distribute based on the `cv.Sim` - for example:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def num_doses(sim):\n", " if sim.t < 50:\n", " return sim.t*10\n", " else:\n", " return 500" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This function corresponds to the vaccination rate increasing linearly for the first 50 days, before then stabilizing. The function can be passed directly into the `cv.vaccinate_num` intervention:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "n_doses = []\n", "pfizer = cv.vaccinate_num(vaccine='pfizer', sequence=prioritize_by_age, num_doses=num_doses)\n", "sim = cv.Sim(\n", " pars=pars,\n", " interventions=pfizer,\n", " analyzers=lambda sim: n_doses.append(sim.people.doses.copy())\n", ")\n", "sim.run()\n", "\n", "pl.figure()\n", "n_doses = np.array(n_doses)\n", "fully_vaccinated = (n_doses == 2).sum(axis=1)\n", "first_dose = (n_doses == 1).sum(axis=1)\n", "pl.stackplot(sim.tvec, first_dose, fully_vaccinated)\n", "pl.legend(['First dose', 'Fully vaccinated']);" ] }, { "cell_type": "markdown", "metadata": { "pycharm": { "name": "#%% md\n" } }, "source": [ "Now the increase in capacity means that when second doses are due, there are sufficient additional doses available to continue distributing first doses. Further customization, particularly to customize second dose prioritization depending on the specific policies implemented in a particular setting, can be readily achieved by implementing a new class deriving from `cv.BaseVaccination` in exactly the same way `cv.vaccinate_prob` and `cv.vaccinate_num` are implemented. \n", "\n", "### Boosters\n", "\n", "By default, the vaccination interventions in Covasim are targeted towards unvaccinated people. If you want to include boosters in your simulation, you can use the `booster=True` argument when calling `cv.vaccinate`. You also need to use the `subtarget` argument so that we know who is going to receive a booster shot:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false }, "pycharm": { "name": "#%%\n" } }, "outputs": [], "source": [ "# Define the number of boosters\n", "def num_boosters(sim):\n", " if sim.t < 50: # None over the first 50 days\n", " return 0\n", " else:\n", " return 50 # Then 100/day thereafter\n", "\n", "# Only give boosters to people who have had 2 doses\n", "booster_target = {'inds': lambda sim: cv.true(sim.people.doses != 2), 'vals': 0}\n", "booster = cv.vaccinate_num(vaccine='pfizer', sequence=prioritize_by_age, subtarget=booster_target, booster=True, num_doses=num_boosters)\n", "\n", "# Track doses\n", "n_doses = []\n", "n_doses_boosters = []\n", "\n", "# Create a sim with boosters\n", "sim_booster = cv.Sim(\n", " pars = pars,\n", " interventions = [pfizer, booster],\n", " label = 'With booster',\n", " analyzers = lambda sim: n_doses_boosters.append(sim.people.doses.copy())\n", ")\n", "sim_booster.run()\n", "\n", "# Plot the sims with and without boosters together\n", "cv.MultiSim([sim, sim_booster]).plot(['cum_infections', 'cum_severe', 'cum_deaths','pop_nabs'])\n", "\n", "# Plot doses again\n", "pl.figure()\n", "n_doses = np.array(n_doses_boosters)\n", "fully_vaccinated = (n_doses == 2).sum(axis=1)\n", "first_dose = (n_doses == 1).sum(axis=1)\n", "boosted = (n_doses > 2).sum(axis=1)\n", "pl.stackplot(sim.tvec, first_dose, fully_vaccinated, boosted)\n", "pl.legend(['First dose', 'Fully vaccinated', 'Boosted'], loc='upper left');" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In this example there isn't a large difference in the epidemic dynamics, because over a short run-time (120 days) immunity doesn't wane very much, so boosters don't have a large effect. However, this example can be adapted to longer run-times, where boosters have a larger impact.\n", "\n", "### Prior immunity methods\n", "\n", "The longer that the COVID-19 pandemic persists, the less feasible/desirable it becomes to simulate the entire ~2 year history of the epidemic. However, starting simulations 1-2 years into the pandemic is also not ideal, as this will fail to capture the pre-existing immunity profile of the population. A good alternative is to use the `cv.prior_immunity` method. The following example starts a simulation in which 50% of the population are assumed to be vaccinated with the Pfizer vaccine 360 days before the simulation begins:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false }, "pycharm": { "name": "#%%\n" } }, "outputs": [], "source": [ "pfizer = cv.historical_vaccinate_prob(vaccine='pfizer', days=[-360], prob=0.5)\n", "sim = cv.Sim(\n", " n_days=1, \n", " interventions=pfizer,\n", " analyzers=cv.nab_histogram(days=[0], edges=np.linspace(-4,2,12+1))\n", ")\n", "\n", "sim.run()\n", "sim.get_analyzer().plot()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The histogram of neutralizing antibody levels after 1 day of simulation shows that there are already significant levels of immunity in the population.\n", "\n", "This next example will initialize the population with immunity levels corresponding to a prior wave of infections peaking 120 days before the simulation starts, with 5% of the population having been infected:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false }, "pycharm": { "name": "#%%\n" } }, "outputs": [], "source": [ "intv = cv.prior_immunity(120, 0.05)\n", "cv.Sim(interventions=intv).run().plot()" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.3" }, "toc": { "base_numbering": 1, "nav_menu": {}, "number_sections": true, "sideBar": true, "skip_h1_title": false, "title_cell": "Table of Contents", "title_sidebar": "Contents", "toc_cell": false, "toc_position": {}, "toc_section_display": true, "toc_window_display": false } }, "nbformat": 4, "nbformat_minor": 4 }