Modeling an epidemic
nickmrgs
Posted on March 20, 2020
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
That wraps up the theory!
Setting up the Project
Our final product is going to look like this :
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:
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!:)
Posted on March 20, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.