SciPy, NumPy, and Matplotlib: Zombie Outbreak

You can see this app running online at: Zombie Outbreak

This app aims to demonstrate:

What this app does:

  • Accepts a set of parameters for a Zombie Outbreak scenario (initial human, zombie and dead populations, human birth rate, human natural death percent per day, transmission percent per day, resurect percent per day, destroy percent per day).
  • Creates a plot of the Zombie Outbreak using the ODEs specified in Munz et al. 2009.
  • Displays this plot.

Setup Instructions

Before you can run this app, you will need to install:

There are many ways to install SciPy, NumPy and Matplotlib, most of which as listed here. Anaconda, provide easy installers for Windows, OSX, Ubuntu and Debian.

If you don’t want to use a pre-built installer:

  • Matplotlib has non Python dependencies which must be installed first:
    • Windows: Download an installer and run. Get the .exe file which matches your Python version (probably 2.7)
    • Unix: May need additional non Python dependencies. E.g for Ubuntu run:
    sudo apt-get install libfreetype6-dev libpng-dev
    
  • Then install the Python packages (note separating the commands, seems to work smoother):

    (tropofy_env) $ pip install numpy; pip install scipy; pip install matplotlib
    
  • Errors installing Scipy? On Ubuntu you may need to also run:

    sudo apt-get install libblas-dev liblapack-dev gfortran
    

Next, use the app name 'tropofy_zombie_outbreak' to quickstart as in Running and Debugging Tropofy Apps

Full Source

"""
Author:      www.tropofy.com

Copyright 2015 Tropofy Pty Ltd, all rights reserved.

This source file is part of Tropofy and governed by the Tropofy terms of service
available at: http://www.tropofy.com/terms_of_service.html

This source file is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details.
"""

from sqlalchemy.types import Float, Text
from sqlalchemy.schema import Column, UniqueConstraint
from sqlalchemy.orm import exc
from tropofy.database.tropofy_orm import DataSetMixin
from tropofy.app import AppWithDataSets, Step, StepGroup
from tropofy.file_io import read_write_aws_s3
from tropofy.widgets import SimpleGrid, StaticImage, ExecuteFunction, Form

import StringIO
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import odeint
import pkg_resources


class Scenario(DataSetMixin):
    name = Column(Text, nullable=False)
    initial_population = Column(Float, nullable=False)
    initial_zombie_population = Column(Float, nullable=False)
    initial_dead_population = Column(Float, nullable=False)
    birth_rate = Column(Float, nullable=False)
    natural_death_percent = Column(Float, nullable=False)
    transmission_percent = Column(Float, nullable=False)
    resurect_percent = Column(Float, nullable=False)
    destroy_percent = Column(Float, nullable=False)

    @classmethod
    def get_table_args(cls):
        return (UniqueConstraint('data_set_id', 'name'),)


class OutputPlot(StaticImage):
    def get_file_path(self, app_session):
        return read_write_aws_s3.AwsS3Reader.get_file_access_url(app_session, file_name='output.png')



class SelectScenario(Form):
    def get_form_elements(self, app_session):
        scenarios = app_session.data_set.query(Scenario).all()
        scenario_names = [s.name for s in scenarios]

        # Example of using alternative text of select options.
        options = []
        for i, name in enumerate(scenario_names):
            options.append({
                'value': name,
                'text': 'Scenario %s: %s' % (i + 1, name)
            })

        default = app_session.data_set.get_var('scenario_name')
        if not default:
            options[0:0] = ['None']
        if options:
            return [Form.Element(
                name='scenario',
                input_type='select',
                default=default if default else options[0],
                label='Choose Scenario to Model',
                options=options,
                disabled=len(options) == 0
            )]

    def process_data(self, app_session, data):
        scenario_name = data.get('scenario')
        if scenario_name == 'None':
            return {
                'results': [{
                    'name': 'scenario',
                    'success': False,
                    'message': "'None' is not a valid scenario."
                }]
            }             
        app_session.data_set.set_var('scenario_name', scenario_name)

class VictoryLossImage(StaticImage):
    def get_file_path(self, app_session):
        zombie_victory = app_session.data_set.get_var('zombie_victory')
        if zombie_victory is not None:
            filename = 'tombstone_zombie_victory.png' if zombie_victory else 'human_victory.png'
            return "/{}/static/{}/{}.png".format(
                app_session.app.url_name,
                app_session.app.get_app_version(app_session),
                filename,
            )


class ZombieOutbreakApp(AppWithDataSets):
    def get_name(self):
        return "Zombie Outbreak"

    def get_static_content_path(self, app_session):
        return pkg_resources.resource_filename('te_zombie_outbreak', 'static')

    def get_gui(self):
        return [
            StepGroup(
                name='Input',
                steps=[
                    Step(
                        name='Scenario Details',
                        widgets=[SimpleGrid(Scenario)],
                        help_text="Enter scenario details. All rates and percentages are per day."
                    ),
                    Step(
                        name='Model the Outbreak',
                        widgets=[SelectScenario(), CreatePlot()],
                    )
                ]
            ),
            StepGroup(
                name='Output',
                steps=[Step(
                    name='Output',
                    widgets=[
                        {'widget': OutputPlot(), 'cols': 6},
                        {'widget': VictoryLossImage(), 'cols': 6}
                    ],
                )]
            ),
        ]

    def get_examples(self):
        return {
            "Sample scenarios": load_sample_scenarios,
        }

    def get_icon_url(self):
        return "/{}/static/{}/tombstone.png".format(
            self.url_name,
            self.get_app_version(),
        )


class CreatePlot(ExecuteFunction):
    def get_button_text(self, app_session):
        return "Model the Apocalypse"

    def execute_function(self, app_session):
        scenario_name = app_session.data_set.get_var('scenario_name')
        try:
            scenario = app_session.data_set.query(Scenario).filter_by(name=scenario_name).one()
        except exc.NoResultFound:
            app_session.task_manager.send_progress_message('No scenarios entered. Cannot model the apocalypse.')
            return

        plt.ion()

        P = scenario.birth_rate
        d = scenario.natural_death_percent
        B = scenario.transmission_percent
        G = scenario.resurect_percent
        A = scenario.destroy_percent

        def f(y, t):
            Si = y[0]
            Zi = y[1]
            Ri = y[2]

            # Munz et al. 2009, ODE equations.
            f0 = P - B*Si*Zi - d*Si
            f1 = B*Si*Zi + G*Ri - A*Si*Zi
            f2 = d*Si + A*Si*Zi - G*Ri
            return [f0, f1, f2]

        t = np.linspace(0, 10, 1000)  # time grid
        S0 = scenario.initial_population
        Z0 = scenario.initial_zombie_population
        R0 = scenario.initial_dead_population
        y0 = [S0, Z0, R0]

        # solve the DEs
        solution = odeint(f, y0, t)
        S = solution[:, 0]
        Z = solution[:, 1]
        #R = solution[:, 2]

        zombie_population = Z[-1]
        population = S[-1]
        app_session.data_set.set_var('zombie_victory', bool(zombie_population >= population))  # Need to convert to bool as otherwise evals to numpy.bool_ which isn't JSON serializable (i.e. app_session.data_set.set_var will throw error)

        plt.figure()
        plt.plot(t, S, label='Living')
        plt.plot(t, Z, label='Zombies')
        plt.xlabel('Days from outbreak')
        plt.ylabel('Population')
        plt.title('Zombie Apocalypse - Zombies vs. Humans')
        plt.legend(loc=0)

        img_data = StringIO.StringIO()
        plt.savefig(img_data, format="png", bbox_inches=0)
        img_data.seek(0)
        file = img_data.buf

        read_write_aws_s3.AwsS3Writer.save_file_string(app_session, string_data=file, file_name='output.png')
        app_session.task_manager.send_progress_message("Apocalypse modelled for scenario '{scenario}v'! Go to next step to view it. {note}".format(
            scenario=scenario_name,
            note="Warning: It doesn't look good for the humans..." if app_session.data_set.get_var('zombie_victory') else "You showed those Zombies!"
        ))


def load_sample_scenarios(app_session):
    app_session.data_set.add_all([
        Scenario(
            name='Doomed humans',
            initial_population=500,
            initial_zombie_population=0,
            initial_dead_population=0,
            birth_rate=0,
            natural_death_percent=0.0001,
            transmission_percent=0.0095,
            resurect_percent=0.0001,
            destroy_percent=0.0001,
        ),
        Scenario(
            name='Doomed zombies',
            initial_population=500,
            initial_zombie_population=200,
            initial_dead_population=0,
            birth_rate=0,
            natural_death_percent=0.0001,
            transmission_percent=0.0095,
            resurect_percent=0.0001,
            destroy_percent=0.015,
        )
    ])