.. image:: https://github.com/benmaier/epipack/raw/master/img/logo_12_lila_medium.png
   :alt: logo

Fast prototyping of epidemiological models based on reaction equations.
Analyze the ODEs analytically or numerically, or run/animate stochastic
simulations on networks/well-mixed systems.

-  repository: https://github.com/benmaier/epipack/
-  documentation: http://epipack.benmaier.org/

.. code:: python

   import epipack as epk
   from epipack.vis import visualize
   import netwulf as nw

   network, _, __ = nw.load('cookbook/readme_vis/MHRN.json')
   N = len(network['nodes'])
   links = [ (l['source'], l['target'], 1.0) for l in network['links'] ]

   S, I, R = list("SIR")
   model = epk.StochasticEpiModel([S,I,R],N,links)\
               .set_link_transmission_processes([ (I, S, 1.0, I, I) ])\
               .set_node_transition_processes([ (I, 1.0, R) ])\
               .set_random_initial_conditions({ S: N-5, I: 5 })

   visualize(model, network, sampling_dt=0.1)

.. image:: https://github.com/benmaier/epipack/raw/master/img/SIR_example.gif
   :alt: sir-example

Idea
----

Simple compartmental models of infectious diseases are useful to
investigate effects of certain processes on disease dissemination. Using
pen and paper, quickly adding/removing compartments and transition
processes is easy, yet the analytical and numerical analysis or
stochastic simulations can be tedious to set up and debug—especially
when the model changes (even slightly). ``epipack`` aims at streamlining
this process such that all the analysis steps can be performed in an
efficient manner, simply by defining processes based on reaction
equations. ``epipack`` provides three main base classes to accomodate
different problems.

-  ``EpiModel``: Define a model based on transition, birth, death,
   fission, fusion, or transmission reactions, integrate the ordinary
   differential equations (ODEs) of the corresponding well-mixed system
   numerically or simulate it using Gillespie's algorithm. Process rates
   can be numerical functions of time and the system state.
-  ``SymbolicEpiModel``: Define a model based on transition, birth,
   death, fission, fusion, or transmission reactions. Obtain the ODEs,
   fixed points, Jacobian, and the Jacobian's eigenvalues at fixed
   points as symbolic expressions. Process rates can be symbolic
   expressions of time and the system state. Set numerical parameter
   values and integrate the ODEs numerically or simulate the stochastic
   systems using Gillespie's algorithm.
-  ``StochasticEpiModel``: Define a model based on node transition and
   link transmission reactions. Add conditional link transmission
   reactions. Simulate your model on any (un-/)directed, (un-/)weighted
   static/temporal network, or in a well-mixed system.

Additionally, epipack provides a visualization framework to animate
stochastic simulations on networks, lattices, well-mixed systems, or
reaction-diffusion systems based on ``MatrixEpiModel``.

Check out the `Example <#examples>`__ section for some demos.

Note that the internal simulation algorithm for network simulations is
based on the following paper:

"Efficient sampling of spreading processes on complex networks using a
composition and rejection algorithm", G.St-Onge, J.-G. Young, L.
Hébert-Dufresne, and L. J. Dubé, Comput. Phys. Commun. 240, 30-37
(2019), http://arxiv.org/abs/1808.05859.

Install
-------

.. code:: bash

   pip install epipack

``epipack`` was developed and tested for

-  Python 3.6
-  Python 3.7
-  Python 3.8

So far, the package's functionality was tested on Mac OS X and CentOS
only.

Dependencies
------------

``epipack`` directly depends on the following packages which will be
installed by ``pip`` during the installation process

-  ``numpy>=1.17``
-  ``scipy>=1.3``
-  ``sympy==1.6``
-  ``pyglet<1.6``
-  ``matplotlib>=3.0.0``
-  ``ipython>=7.14.0``
-  ``ipywidgets>=7.5.1``

Please note that **fast network simulations are only available if you
install**

-  ``SamplableSet==2.0``
   (`SamplableSet <http://github.com/gstonge/SamplableSet>`__)

**manually** (pip won't do it for you).

Documentation
-------------

The full documentation is available at
`epipack.benmaier.org <http://epipack.benmaier.org>`__.

Changelog
---------

Changes are logged in a `separate
file <https://github.com/benmaier/epipack/blob/master/CHANGELOG.md>`__.

License
-------

This project is licensed under the `MIT
License <https://github.com/benmaier/epipack/blob/master/LICENSE>`__.
Note that this excludes any images/pictures/figures shown here or in the
documentation.

Contributing
------------

If you want to contribute to this project, please make sure to read the
`code of
conduct <https://github.com/benmaier/epipack/blob/master/CODE_OF_CONDUCT.md>`__
and the `contributing
guidelines <https://github.com/benmaier/epipack/blob/master/CONTRIBUTING.md>`__.
In case you're wondering about what to contribute, we're always
collecting ideas of what we want to implement next in the `outlook
notes <https://github.com/benmaier/epipack/blob/master/OUTLOOK.md>`__.

|Contributor Covenant|

Examples
--------

Let's define an SIRS model with infection rate ``eta``, recovery rate
``rho``, and waning immunity rate ``omega`` and analyze the system

Pure Numeric Models
~~~~~~~~~~~~~~~~~~~

Basic Definition (EpiModel)
^^^^^^^^^^^^^^^^^^^^^^^^^^^

Define a pure numeric model with ``EpiModel``. Integrate the ODEs or
simulate the system stochastically.

.. code:: python

   from epipack import EpiModel
   import matplotlib.pyplot as plt
   import numpy as np

   S, I, R = list("SIR")
   N = 1000

   SIRS = EpiModel([S,I,R],N)\
       .set_processes([
           #### transmission process ####
           # S + I (eta=2.5/d)-> I + I
           (S, I, 2.5, I, I),

           #### transition processes ####
           # I (rho=1/d)-> R
           # R (omega=1/14d)-> S
           (I, 1, R),
           (R, 1/14, S),
       ])\
       .set_initial_conditions({S:N-10, I:10})

   t = np.linspace(0,40,1000) 
   result_int = SIRS.integrate(t)
   t_sim, result_sim = SIRS.simulate(t[-1])

   for C in SIRS.compartments:
       plt.plot(t, result_int[C])
       plt.plot(t_sim, result_sim[C])

.. image:: https://github.com/benmaier/epipack/raw/master/img/numeric_model.png
   :alt: numeric-model

Functional Rates
^^^^^^^^^^^^^^^^

It's also straight-forward to define temporally varying (functional)
rates.

.. code:: python

   import numpy as np
   from epipack import SISModel

   N = 100
   recovery_rate = 1.0

   def infection_rate(t, y, *args, **kwargs):
       return 3 + np.sin(2*np.pi*t/100)

   SIS = SISModel(
               infection_rate=infection_rate, 
               recovery_rate=recovery_rate,
               initial_population_size=N
               )\
           .set_initial_conditions({
               'S': 90,
               'I': 10,
           })

   t = np.arange(200)
   result_int = SIS.integrate(t)
   t_sim, result_sim = SIS.simulate(199)

   for C in SIS.compartments:
       plt.plot(t_sim, result_sim[C])
       plt.plot(t, result_int[C])

.. image:: https://github.com/benmaier/epipack/raw/master/img/numeric_model_time_varying_rate.png
   :alt: numeric-model-time-varying

Symbolic Models
~~~~~~~~~~~~~~~

Basic Definition
^^^^^^^^^^^^^^^^

Symbolic models are more powerful because they can do the same as the
pure numeric models while also offering the possibility to do analytical
evaluations

.. code:: python

   from epipack import SymbolicEpiModel
   import sympy as sy

   S, I, R, eta, rho, omega = sy.symbols("S I R eta rho omega")

   SIRS = SymbolicEpiModel([S,I,R])\
       .set_processes([
           (S, I, eta, I, I),
           (I, rho, R),
           (R, omega, S),
       ])    

Analytical Evaluations
^^^^^^^^^^^^^^^^^^^^^^

Print the ODE system in a Jupyter notebook

.. code:: python

   >>> SIRS.ODEs_jupyter()

.. image:: https://github.com/benmaier/epipack/raw/master/img/ODEs.png
   :alt: ODEs

Get the Jacobian

.. code:: python

   >>> SIRS.jacobian()

.. image:: https://github.com/benmaier/epipack/raw/master/img/jacobian.png
   :alt: Jacobian

Find the fixed points

.. code:: python

   >>> SIRS.find_fixed_points()

.. image:: https://github.com/benmaier/epipack/raw/master/img/fixed_points.png
   :alt: fixedpoints

Get the eigenvalues at the disease-free state in order to find the
epidemic threshold

.. code:: python

   >>> SIRS.get_eigenvalues_at_disease_free_state()
   {-omega: 1, eta - rho: 1, 0: 1}

Numerical Evaluations
^^^^^^^^^^^^^^^^^^^^^

Set numerical parameter values and integrate the ODEs numerically

.. code:: python

   >>> SIRS.set_parameter_values({eta: 2.5, rho: 1.0, omega:1/14})
   >>> t = np.linspace(0,40,1000)
   >>> result = SIRS.integrate(t)

If set up as

.. code:: python

   >>> N = 10000
   >>> SIRS = SymbolicEpiModel([S,I,R],N)

the system can simulated directly.

.. code:: python

   >>> t_sim, result_sim = SIRS.simulate(40)

Temporally Varying Rates
^^^^^^^^^^^^^^^^^^^^^^^^

Let's set up some temporally varying rates

.. code:: python

   from epipack import SymbolicEpiModel
   import sympy as sy

   S, I, R, eta, rho, omega, t, T = \
           sy.symbols("S I R eta rho omega t T")

   N = 1000
   SIRS = SymbolicEpiModel([S,I,R],N)\
       .set_processes([
           (S, I, 2+sy.cos(2*sy.pi*t/T), I, I),
           (I, rho, R),
           (R, omega, S),
       ])  

   SIRS.ODEs_jupyter()

.. image:: https://github.com/benmaier/epipack/raw/master/img/SIRS-forced-ODEs.png
   :alt: SIRS-forced-ODEs

Now we can integrate the ODEs or simulate the system using Gillespie's
SSA for inhomogeneous Poisson processes.

.. code:: python

   import numpy as np

   SIRS.set_parameter_values({
       rho : 1,
       omega : 1/14,
       T : 100,
   })
   SIRS.set_initial_conditions({S:N-100, I:100})
   _t = np.linspace(0,200,1000)
   result = SIRS.integrate(_t)
   t_sim, result_sim = SIRS.simulate(max(_t))

.. image:: https://github.com/benmaier/epipack/raw/master/img/symbolic_model_time_varying_rate.png
   :alt: SIRS-forced-results

Interactive Analyses
^^^^^^^^^^^^^^^^^^^^

``epipack`` offers a classs called ``InteractiveIntegrator`` that allows
an interactive exploration of a system in a Jupyter notebook.

Make sure to first run

.. code:: bash

   %matplotlib widget

in a cell.

.. code:: python

   from epipack import SymbolicEpiModel
   from epipack.interactive import InteractiveIntegrator, Range, LogRange
   import sympy

   S, I, R, R0, tau, omega = sympy.symbols("S I R R_0 tau omega")

   I0 = 0.01
   model = SymbolicEpiModel([S,I,R])\
                .set_processes([
                       (S, I, R0/tau, I, I),
                       (I, 1/tau, R),
                       (R, omega, S),
                   ])\
                .set_initial_conditions({S:1-I0, I:I0})

   # define a log slider, a linear slider and a constant value
   parameters = {
       R0: LogRange(min=0.1,max=10,step_count=1000),
       tau: Range(min=0.1,max=10,value=8.0),
       omega: 1/14
   }

   t = np.logspace(-3,2,1000)
   InteractiveIntegrator(model, parameters, t, figsize=(4,4))

.. image:: https://github.com/benmaier/epipack/raw/master/img/interactive.gif
   :alt: interactive

Pure Stochastic Models
~~~~~~~~~~~~~~~~~~~~~~

On a Network
^^^^^^^^^^^^

Let's simulate an SIRS system on a random graph (using the parameter
definitions above).

.. code:: python

   from epipack import StochasticEpiModel
   import networkx as nx

   k0 = 50
   R0 = 2.5
   rho = 1
   eta = R0 * rho / k0
   omega = 1/14
   N = int(1e4)
   edges = [ (e[0], e[1], 1.0) for e in \
             nx.fast_gnp_random_graph(N,k0/(N-1)).edges() ]

   SIRS = StochasticEpiModel(
               compartments=list('SIR'),
               N=N,
               edge_weight_tuples=edges
               )\
           .set_link_transmission_processes([
               ('I', 'S', eta, 'I', 'I'),
           ])\
           .set_node_transition_processes([
               ('I', rho, 'R'),
               ('R', omega, 'S'),
           ])\        
           .set_random_initial_conditions({
                                           'S': N-100,
                                           'I': 100
                                          })
   t_s, result_s = SIRS.simulate(40)

.. image:: https://github.com/benmaier/epipack/raw/master/img/network_simulation.png
   :alt: network-simulation

Visualize
^^^^^^^^^

Likewise, it's straight-forward to visualize this system

.. code:: python

   >>> from epipack.vis import visualize
   >>> from epipack.networks import get_random_layout
   >>> layouted_network = get_random_layout(N, edges)
   >>> visualize(SIRS, layouted_network, sampling_dt=0.1, config={'draw_links': False})

.. image:: https://github.com/benmaier/epipack/raw/master/img/SIRS_visualization.gif
   :alt: sirs-example

On a Lattice
^^^^^^^^^^^^

A lattice is nothing but a network, we can use ``get_grid_layout`` and
``get_2D_lattice_links`` to set up a visualization.

.. code:: python

   from epipack.vis import visualize
   from epipack import (
       StochasticSIRModel, 
       get_2D_lattice_links, 
       get_grid_layout
   )

   # define links and network layout
   N_side = 100
   N = N_side**2
   links = get_2D_lattice_links(N_side, periodic=True, diagonal_links=True)
   lattice = get_grid_layout(N)

   # define model
   R0 = 3; recovery_rate = 1/8
   model = StochasticSIRModel(N,R0,recovery_rate,
                              edge_weight_tuples=links)
   model.set_random_initial_conditions({'I':20,'S':N-20})

   sampling_dt = 1

   visualize(model,lattice,sampling_dt,
           config={
                    'draw_nodes_as_rectangles':True,
                    'draw_links':False,
                  }
             )

.. image:: https://github.com/benmaier/epipack/raw/master/img/SIR_lattice_vis.gif
   :alt: sir-lattice

Reaction-Diffusion Models
~~~~~~~~~~~~~~~~~~~~~~~~~

Since reaction-diffusion systems in discrete space can be interpreted as
being based on reaction equations, we can set those up using
``epipack``'s framework.

Checkout the docs on `Reaction-Diffusion
Systems <http://epipack.benmaier.org/tutorial/reaction_diffusion.html>`__.

Every node in a network is associated with a compartment and we're using
``MatrixEpiModel`` because it's faster than ``EpiModel``.

.. code:: python

   from epipack import MatrixEpiModel

   N = 100
   base_compartments = list("SIR")
   compartments = [ (node, C) for node in range(N) for C in base_compartments ]
   model = MatrixEpiModel(compartments)

Now, we define both epidemiological and movement processes on a
hypothetical list ``links``.

.. code:: python

   infection_rate = 2
   recovery_rate = 1
   mobility_rate = 0.1

   quadratic_processes = []
   linear_processes = []

   for node in range(N):
       quadratic_processes.append(
               (  (node, "S"), (node, "I"), infection_rate, (node, "I"), (node, "I") ),
           )

       linear_processes.append(
                 ( (node, "I"), recovery_rate, (node, "R") ) 
           )

   for u, v, w in links:
       for C in base_compartments:

           linear_processes.extend([
                     ( (u, C), w*mobility_rate, (v, C) ),
                     ( (v, C), w*mobility_rate, (u, C) ),
               ])

.. image:: https://github.com/benmaier/epipack/raw/master/img/reac_diff_lattice.gif
   :alt: reac-diff-lattice

Dev notes
---------

Fork this repository, clone it, and install it in dev mode.

.. code:: bash

   git clone git@github.com:YOURUSERNAME/epipack.git
   make

If you want to upload to PyPI, first convert the new ``README.md`` to
``README.rst``

.. code:: bash

   make readme

It will give you warnings about bad ``.rst``-syntax. Fix those errors in
``README.rst``. Then wrap the whole thing

.. code:: bash

   make pypi

It will probably give you more warnings about ``.rst``-syntax. Fix those
until the warnings disappear. Then do

.. code:: bash

   make upload

.. |Contributor Covenant| image:: https://img.shields.io/badge/Contributor%20Covenant-v1.4%20adopted-ff69b4.svg
   :target: code-of-conduct.md
