DEV Community

Cover image for Modeling an epidemic
nickmrgs
nickmrgs

Posted on

Modeling an epidemic

Quarantine Day 6,

It has been almost two weeks since I last went out of my house. As strange as it may sound, the coronavirus outspread has radically changed our daily routines and has managed to completely transform our lives.
It feels like we are starring in a black mirror episode. It feels like someone has pressed the pause button in the simulation machine that is called 'life'.
At first, as you may think, I was depressed about the current situation but soon enough I decided to take advantage of it, so I opened my laptop and started working.
The struggle of finding a new project to work can be a real pain for every programmer, but the idea came to my mind spontaneously, when I heard about the epidemic predictions that some scientists are making to help us fight COV-19.

Enough with my long intro and let me introduce you to our project for today:
A Virus Spread Simulation Programm with Real-Time updating Charts

I made this project using Visual Studio 2015 and C#
You can find the project on my GitHub Account via the following link:
https://github.com/nickmrgs/VirusSpreadSim

For today's project we will need:
-1 Medium Level C# Knowledge
-2 Understanding the SIR model
-3 WPF Charts

SIR model

Let's start with understanding the SIR model
We are going to use the Kermack and McKendrick SIR model that was introduced back in 1927.
The SIR model is one of the simplest compartmental models. The model consists of three compartments: S for the number of susceptible, I for the number of infectious, and R for the number of recovered individuals.
Let's see how it works:
We assume that N is our population. As we mentioned before we divide our population into 3 categories (note: in this model, we do not examine births or deaths of our population):
Susceptible: The people that aren't infected with the virus yet
Infected: The people that have the virus, those people can spread the virus to Susceptible people
Recovered: The people that cannot get infected with the virus, because they have recovered from it
We know that N = S+I+R
We are also going to use two parameters
The infection rate that we are going to call 'beta', that determines how likely it is for an Infected person to infect a Susceptible person
And 'gamma' a parameter that shows us how fast an Infected person recovers.
Gamma equals to 1.0/lifespan (lifespan is how many days a virus lives inside the infected people)
So we can see the three equations that we are going to use in our model
The
Alt Text

That wraps up the theory!

Setting up the Project

Our final product is going to look like this :

Alt Text

Since it is a Visual C# Project, you know it requires some basic UI Design.
In this lesson, we are not going to cover this topic. I am going to give you all the fundamentals you need to create a dynamic modeling program.
If you are not familiar with the UI design don't worry about it, it shouldn't be a hard task. You just need a week of training.

Let's start with our project

Step 1: Install the additional content

We need to install the WPF Live charts
This can be easily done if you follow those steps here:
https://lvcharts.net/App/examples/wf/Install

This is the way I followed as well and I believe it is better than any other installation method I saw. When you are done with the installation you should be able to access those charts from the toolbox
We are going to use the Cartesian Chart:

Alt Text

Step2: Design the UI
Keep it simple! We just need to ensure that we get the correct User Inputs.

Step3: Start Coding!
This is the project's source code
If you name your buttons, comboboxes, labels and etc the same way with mine, it should run. This is for anyone who is in a hurry. If you are here to learn I suggest you follow my lead, as I am going to explain everything

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using LiveCharts; //Core of the library
using LiveCharts.Wpf; //The WPF controls
using LiveCharts.WinForms;//the WinForm wrappers
using LiveCharts.Defaults;
using LiveCharts.Helpers;

namespace VirusSpreadSim
{

    public partial class Form1 : Form
    {
        bool firstinfection = true;
        IList<double> Ilst = new List<double>();
        IList<double> Rlst = new List<double>();
        IList<double> Slst = new List<double>();
        bool helpme=false;
        //mathematical model 
        double S;
        double I;
        double R;
        //
        double Snew;
        double Inew;
        double Rnew;
        //
        bool key1 = false;
        bool key2 = false;
        bool firstime = true;
        int speed = 1;
        double population;
        double beta;
        double daycount=0;
        double lifespan;
        public Form1()
        {
            InitializeComponent();

        }
        private void Form1_Load(object sender, EventArgs e)
        {
            // TODO: This line of code loads data into the 'myDataSet.someTable' table. You can move, or remove it, as needed.

        }

        private void panel2_Paint(object sender, PaintEventArgs e)
        {

        }

        private void daylbl_Click(object sender, EventArgs e)
        {

        }

        private void trackBar1_Scroll(object sender, EventArgs e)
        {
            nodayslbl.Text = trackBar1.Value.ToString();
            if(nodayslbl.Text.Equals("1"))
            {
                label21.Text = "day";
            }
            else
            {
                label21.Text = "days";
            }
        }

        private void nodayslbl_Click(object sender, EventArgs e)
        {

        }

        private void button1_Click(object sender, EventArgs e)
        {
            if(firstime)
            {

                if (!string.IsNullOrWhiteSpace(popbox.Text)&&!string.IsNullOrWhiteSpace(infectionratebox.Text))
                {
                    lifespan = double.Parse(nodayslbl.Text);
                    if (Double.TryParse(popbox.Text, out population))
                    {
                        if(population<1000||population>1000000000)
                        {
                            MessageBox.Show(" Error:  Population must be between (1000-1000000000) people");

                        }
                        else
                        {

                            key1 = true;
                        }
                    }
                    else
                    {
                        MessageBox.Show("Wrong Input population");
                    }
                    if (Double.TryParse(infectionratebox.Text,out beta))
                    {
                        if(beta<0||beta>1)
                        {
                            MessageBox.Show(" Error :Please Select an Infection Rate Value Between (0-1)");
                        }
                        else
                        {
                            key2 = true;
                        }
                    }
                    else
                    {
                        MessageBox.Show(" Wrong Input affection rate");
                    }
                    //
                    if(key1&&key2)
                    {
                        timer1.Start();
                        button1.Enabled = false;
                        button2.Enabled = true;
                        firstime = !firstime;
                    }

                }
                else
                {
                    MessageBox.Show("Please enter correct data");
                }

            }
            else
            {
                button1.Enabled = false;
                button2.Enabled = true;
                timer1.Start();
            }




        }



        private void timer1_Tick(object sender, EventArgs e)
        {
            speedlbl.Text = speedBar.Value.ToString();
            if (speedlbl.Text.Equals("1"))
            {
                timer1.Interval = 1000;
            }
            else if (speedlbl.Text.Equals("2"))
            {
                speed = 2;
                timer1.Interval = 500;
            }
            else if (speedlbl.Text.Equals("3"))
            {
                speed = 3;
                timer1.Interval = 250;
            }

//maths implementation---------------------------------------------------------------------------------------------
//------------------------------------------------------
//------------------------------------------------------


            int newInf;
            double hS;
            double hI;
            double hR;
            double gamma;
            ///mathmodel
            ///
            if(firstinfection)
            {
               // Random rn = new Random();
                newInf = 2;
                I = newInf + I;
                S = population - I;
                firstinfection = false;
                ilbl.Text = Math.Round(I).ToString();
                slbl.Text = Math.Round(S).ToString();
            }
            //S--------
            hS = -(beta * S * I)/population;
            Snew = S +hS;
            S = Snew;
            //R--------
            gamma = (1.0 / lifespan);
            hR = gamma * I;
            Rnew = R + hR;
            R = Rnew;
            //I--------
            hI= -hS - hR;
            Inew = I + hI;
            I = Inew;
            //Inew = I + hI;
            ilbl.Text = Math.Round(I).ToString();
            rlbl.Text = Math.Round(R).ToString();
            slbl.Text = Math.Round(S).ToString();

            daycount++;
            daylbl.Text = daycount.ToString();

            if (daycount % 7 == 1)
            {
                Ilst.Add(I);

            }

            if (daycount % 7 == 1)
            {
                Rlst.Add(R);

            }
            if (daycount % 7 == 1)
            {
                Slst.Add(S);

            }

            if (daycount%7==0)
            {
                cartesianChart1.Series = new SeriesCollection
            {
                new LineSeries
                {
                    Title = "Infected",
                    Values = Ilst.AsChartValues(),

                },
                 new LineSeries
                {
                    Title = "Recovered",
                    Values = Rlst.AsChartValues(),

                },
                  new LineSeries
                {
                    Title = "Susceptible",
                    Values = Slst.AsChartValues(),

                },
            };



        }
            if (I < 0.001 || S < 0.001)
            {
                timer1.Stop();
                button1.Hide();
                button2.Hide();
                label5.Visible = true;
            }

        }

        private void speedBar_Scroll(object sender, EventArgs e)
        {
            speedlbl.Text = speedBar.Value.ToString();
            if(speedlbl.Text.Equals("1"))
            {
                speed = 1;
            }
            else if(speedlbl.Text.Equals("2"))
            {
                speed = 2;
            }
            else if(speedlbl.Text.Equals("3"))
            {
                speed = 3;
            }
        }

        private void button2_Click(object sender, EventArgs e)
        {
            button1.Enabled = true;
            button2.Enabled = false;
            timer1.Stop();
        }



        private void exitToolStripMenuItem_Click(object sender, EventArgs e)
        {
            Application.Exit();
        }

        private void newSimulationToolStripMenuItem_Click(object sender, EventArgs e)
        {
            Form1 NewForm = new Form1();
            NewForm.Show();
            this.Dispose(false);
        }

        private void aboutToolStripMenuItem_Click(object sender, EventArgs e)
        {

        }

        private void aboutVirusSpreadToolStripMenuItem_Click(object sender, EventArgs e)
        {
            AboutVirusSpread jk = new AboutVirusSpread();
            jk.Show();
        }
    }
}

So let's focus on the code step by step:

1. The first thing we need to ensure is that we get the correct input from the user

In order to do do, we use Tryparse()
If the Input is Parsed successfully then we move on and we check if our population number is realistic. If it is then we use a bool key1 to tell our program that we have successfully gained the population data for our model

  if (Double.TryParse(popbox.Text, out population))
                    {
                      //the input is parsed successfully so we check our numbers   
                        if(population<1000||population>1000000000)
                        {
                            MessageBox.Show(" Error:  Population must be between (1000-1000000000) people");

                        }
                        else
                        {

                            key1 = true;
                        }
                    }
                    else
                    {
                        MessageBox.Show("Wrong Input population");
                    }


We repeat the same process for the Infection Rate Input. If the input is correct
our bool key2 will be true
For the lifespan Input, We have used a trackbar so we do not need to check if the input is valid. If you do not use a trackbar then you will need a third key boolean in your program
If all the keys are true that means we have correct inputs, so our simulation can start!
But what do we need in order to create a simulation machine?
A timer!

2.Let's set up the timer.Create the simulation speed

In order for the simulation to run in real-time, we need to use a timer. The logic is that every time the timer ticks, we are going to recalculate the new SIR values.

A cool future that all simulators use is the simulation speed.
We can implement this by adding this piece of code inside the timer tick void.
We are using a trackBar with the name speedBar and then store it's value in a label named speedld .
The values we can get are 1-2-3
In this way, we manage to make 3 options for the speed.
We change the speed by making the Interval smaller (tip: we set the Interval in milliseconds). The smaller the Interval ,the faster the timer is ticking

 speedlbl.Text = speedBar.Value.ToString();
            if (speedlbl.Text.Equals("1"))
            {
                timer1.Interval = 1000;
            }
            else if (speedlbl.Text.Equals("2"))
            {
                speed = 2;
                timer1.Interval = 500;
            }
            else if (speedlbl.Text.Equals("3"))
            {
                speed = 3;
                timer1.Interval = 250;
            }

3.Coding the SIR math model

Before we start implementing the model, we need to check if the infection is the first one happening to our population. This happens because we need to avoid having I=0 and we also want to calculate S.
So S equals to the population minus the infected people

All code below is written inside the timer tick void
 if(firstinfection)
            {

                newInf = 2;
                I = newInf + I;
                S = population - I;
                firstinfection = false;

            }

Here is the mathematic model based on the Kermack and McKendrick SIR model

            //S--------
            hS = -(beta * S * I)/population;
            Snew = S +hS;
            S = Snew;
            //R--------
            gamma = (1.0 / lifespan);
            hR = gamma * I;
            Rnew = R + hR;
            R = Rnew;
            //I--------
            hI= -hS - hR;
            Inew = I + hI;
            I = Inew;

Then we update the labels with the new values and count the days

            ilbl.Text = Math.Round(I).ToString();
            rlbl.Text = Math.Round(R).ToString();
            slbl.Text = Math.Round(S).ToString();

            daycount++;
            daylbl.Text = daycount.ToString();

We use the days to weekly update our cartesian charts:
We are going to use three separate lists for this

      if (daycount % 7 == 1)
            {
                Ilst.Add(I);

            }

            if (daycount % 7 == 1)
            {
                Rlst.Add(R);

            }
            if (daycount % 7 == 1)
            {
                Slst.Add(S);

            }     
4.Build the cartesian charts

In the last step, we are going to use our lists in order to create our chart. We create a new chart every week. We continue adding code inside the timer tick void

  if (daycount%7==0)
            {
                cartesianChart1.Series = new SeriesCollection
            {
                new LineSeries
                {
                    Title = "Infected",
                    Values = Ilst.AsChartValues(),

                },
                 new LineSeries
                {
                    Title = "Recovered",
                    Values = Rlst.AsChartValues(),

                },
                  new LineSeries
                {
                    Title = "Susceptible",
                    Values = Slst.AsChartValues(),

                },
            };

We,are almost there just check our SIR values
If they have become really small , just finish the simulation


 if(I<0.001||S<0.001)
                {
                    timer1.Stop();
                    button1.Hide();
                    button2.Hide();
                    label5.Visible = true;
                }   

We are done

That's all. Hope you liked that project and you have learned something from it. Till next time stay home and stay healthy!:)

Top comments (0)