NovaScript SIR Tutorial

This tutorial was written for an older version of Nova and needs to be double-checked to see if everything still works in the current version.

This tutorial is to show the steps to creating an Systems Dynamics SIR model in Nova using the NovaScript scripting interface. This tutorial will show one how to build a NovaScript model given no prior background in NovaScript or programming in general. However, this tutorial assumes prior background with Nova concepts such as stocks and flows.

Beginning The Project

Opening the NovaScript Interface

In order to begin using the NovaScript interface, click on the lambda symbol in the toolbar. This will open a text editor with the controls on the top, the code in the middle, and a console on the bottom.

The Syntax of NovaScript

NovaScript is written using the programming language JavaScript. Thus, if you know JavaScript you can use all of the little goodies that you know! If you do not know JavaScript, do not worry. “All” of the syntax that you will need to know in order to build models is contained in this tutorial: http://www.cs.brown.edu/courses/bridge/1998/res/javascript/javascript-tutorial.html. A good guide for future reference of NovaScript syntax is the NovaScript Cheat Sheet.

Quick Note On Language For Non-Programmers

If you are new to programming, this may seem like a big task! However, don’t be discouraged, all of the syntax you need is right here! By syntax I mean the representation of an idea in the language. For example, in JavaScript we have this idea called a “variable” which we can store values in. Thus we say that the syntax for defining a variable is:

var VariableName = value;

This is read as VariableName gets the value. Notice that this is not the same as saying VariableName is equal to the value, and as such we can do things like say:

VariableName = VariableName + 1;

Which will increase the value of VariableName by 1. If the value is words instead of numbers, then the words are what are called a String and must be encased by parenthesis. Thus if we want to store the value “NovaScript is awesome”, we would say:

var VariableName = "NovaScript is awesome";

A valuable part of programming is using what are called functions or methods. Basically, a function is simply a predefined sequence of actions. To use a function, simply call it. Lets say we want to print out something to the console, to do so we would use the print function and give it an argument of a String to print as such:

print("This will be printed!")

This should be enough to get you started. A complete tutorial in JavaScript can be found at this site.

Initializing A Project

To begin any model in Nova, the project must be initialized. To do so, you have to define the project object and begin the project. For the SIR project, we would do this simply as such:

var SIR = new Project('SIR');
beginProject(SIR);

However, this model is still not initialized. Every project must begin and end! Thus to finish the initialization, we must use the endProject method:

endProject(0,100,.1,'rk4')

This method defines a clock. The first argument tells what the first time period is, the second tells the last time period, the third defines the dt (the time steps for calculation), and the last defines the integration method. This this says “start at time 0, end at time 100, and calculate by going up by .1 each step”. Don’t worry about the integration method for now, simply use rk4.

Good job! Now you have a model that is initialized! However, it does nothing! Lets get this model doing something.

Defining the Main Schema

To define a systems dynamics model in Nova, you must define a capsule. Think of the capsule as your model. To define your capsule, you define what is called a “schema” which lays out everything that you would have made in the visual Nova model. Thus what is defined in the schema are things like the stocks, flows, methods, controls, graphics, and all of the model dynamics. The syntax for defining the schema is as such:

SIR.defineSchema( 'main', {
	 specifies: 'CAPSULE',
	 stocks:
	 flows:
	 displays:
	 dynamics:
})

Notice that what we use is the project name, then a period, then the method defineSchema with the arguments defining the model. The first argument is the name of the capsule. Since we are only using one, we will by convention call it ‘main’. This should be done for the main model in any Nova model. The second argument is EVERYTHING that is contained in the curly brackets. This is known as the property list. The property list simply contains all of the necessary information to build the model. This, to populate our SIR model, what we want is to have 3 different populations: susceptible, infected, and recovered. Thus we would define these three populations as stocks using the following syntax:

SIR.defineSchema( 'main', {
	specifies: 'CAPSULE',
	stocks: ['susceptible','infected','recovered'],
	flows: 
	displays:
	dynamics:
})

Notice that the comma is REALLY important. If you have errors that start at that line, chances are you left out a comma! Also notice the syntax for the stocks. Any object definition must be written with that syntax whenever there is more than one thing that is being defined. If there is only one object you could define it like:

stocks: 'susceptible',

Now for our model we will define our necessary flows in the same way to get this:

SIR.defineSchema( 'main', {
	specifies: 'CAPSULE',
	stocks: ['susceptible','infected','recovered'],
	flows: ['infection','recoveryf'],
	displays:
	dynamics:
})

Now as in the visual Nova, the next step is to define the dynamics. First, we need to set the initial values for all of our stocks. The syntax for doing so is this:

SIR.defineSchema( 'main', {
	specifies: 'CAPSULE',
	stocks: ['susceptible','infected','recovered'],
	flows: ['infection','recoveryf'],
	displays:
	dynamics: [
		//Initial values
		"susceptible.initial = 1000",
		"infected.initial = 1",
		"recovered.initial = 0",
	]
})

Every stock or stock-like object must be define as such. Notice that the words “Initial values” comes after two forward dashed lines, //. This denotes a comment in Javascript and means that it will not affect your code. I encourage you to comment your code like this to help you find out what you wrote in large models!

Next we must connect our flows to our stocks. Notice NovaScript only allows defining the outputs of the objects, so to define the connections the syntax will be:

SIR.defineSchema( 'main', {
	specifies: 'CAPSULE',
	stocks: ['susceptible','infected','recovered'],
	flows: ['infection','recoveryf'],
	displays:
	dynamics: [
		//Initial values
		"susceptible.initial = 1000",
		"infected.initial = 1",
		"recovered.initial = 0",
		// Connections
		"susceptible.output = infection",
		"infection.output = infected",
		"infected.output = recoveryf",
		"recoveryf.output = recovered",
	]
})

Next we will put in any flow equations. To define the flow equations, simply set the flow equation to required function. Thus the syntax is:

SIR.defineSchema( 'main', {
	specifies: 'CAPSULE',
	stocks: ['susceptible','infected','recovered'],
	flows: ['infection','recoveryf'],
	displays:
	dynamics: [
		//Initial values
		"susceptible.initial = 1000",
		"infected.initial = 1",
		"recovered.initial = 0",
		// Connections
		"susceptible.output = infection",
		"infection.output = infected",
		"infected.output = recoveryf",
		"recoveryf.output = recovered",
		//Flow Equations
		"infection = infectionModifier*((infected*susceptible)/(infected+susceptible))",
		"recoveryf = 0",
	]
})

Notice for now that we have a model. All stocks have initial values, the flows are connected, and we have defined the flow from susceptible to infected to be the value

infectionModifier*((infected*susceptible)/(infected+susceptible))

and the flow from infected to recovered as 0 (for now). We will add in the recovery function later.

Lastly we want to make sure these stocks do not go negative since negative people do not make sense. To do so we simply add to our dynamics the following statements:

 

//Misc
"susceptible.nonnegative = true",
"infected.nonnegative = true",
"recovered.nonnegative = true",

This makes our schema the following:

SIR.defineSchema( 'main', {
	specifies: 'CAPSULE',
	stocks: ['susceptible','infected','recovered'],
	flows: ['infection','recoveryf'],
	displays:
	dynamics: [
		//Initial values
		"susceptible.initial = 1000",
		"infected.initial = 1",
		"recovered.initial = 0",
		// Connections
		"susceptible.output = infection",
		"infection.output = infected",
		"infected.output = recoveryf",
		"recoveryf.output = recovered",
		//Flow Equations
		"infection = infectionModifier*((infected*susceptible)/(infected+susceptible))",
		"recoveryf = 0",
		//Misc
		"susceptible.nonnegative = true",
		"infected.nonnegative = true",
		"recovered.nonnegative = true",
	]
})

This is our model! That’s it!

Adding Displays and Controls

To add displays, we will define the schema for a display in much the same way as we defined the schema for our main capsule. The only different is that the property list is different. Thus lets say we want to add a table and a graph. To define the graph we would simply use:

SIR.defineSchema( 'popgraph1', {
	 specifies: "Graph",
	 proxy: "popgraph",
	 type: "TIMESERIES",
	 title: ["SIR Results"]
	 display: "[[susceptible, infected, recovered]]",
})

If you ever need a reference, the NovaScript Cheat Sheet help you remember what properties must be specified for all of the different objects. The property specifies tells Nova what kind of an object it is. Here it is a graph. Proxy is the title that is on the visual element of Nova. So go and create a graph on the visual side of Nova and make sure its title is the same as the string given in proxy. Type defines what type of graph is used. Title is the title of the graph that is shown in the display. And display is an array of values that you wish to graph. Take note that the syntax “[[susceptible, infected, recovered]]” will graph all of the stocks together on the same graph while “[susceptible, infected, recovered]” while graph three different tabs with those respective stocks.

To now implement the graph into the main schema, simply reference it in the display property as such:

displays: {
	 popgraph: "popgraph1"
}

Now the graph should be working! Notice another way to define the displays is to defining them inline. Lets do this for a table. For a table we need to define just the specifies, proxy, title, and display. Thus we can add to displays the table by:

displays: {
	 popgraph: "popgraph1"
	 sirtable: {
			 specifies: "Table",
			 proxy: "sirtable",
			 title: ["SIR Results"],
			 display: "[[susceptible, infected, recovered]]",
		 }
}

Make sure you go to the visual side and make a table with the name sirtable!

Lastly, we need to define the controls. This is the same as defining a display! Just define a schema for a slider. The required properties are in the NovaScript Cheat Sheet. The slider will look something like this:

SIR.defineSchema('infectionModSlider', {
 	 specifies: "Slider",
 	 proxy: "infectionModifier",
 	 lo: 0,
 	 hi: 1,
 	 dec: .00001,
 	 value: .26558,
})

Notice we just specifiy it as a slider and give it a proxy as before. However, for a slider we must add the lowest value, the highest value, the decrement value, and the starting value.

At this point you should have a running model with a graph, a table, and a slider. Now we will get to the harder stuff.

Defining Methods and Variables

Now it’s time to implement our recovery function. For this model we want people to recovery 5 days after they are infected. Thus the value for recovery is the value of infected 5 time units ago. Great, now how do we implement this?

We can use a variable to make our code look easier. We know that we want the number of days we want someone to be infected for is 5, however we may want to change this later. To put it all in one spot to make it easier to change later, simply write at the top of the page

const infectedDays = 5;

This simply means that we want to define infectedDays as a constant equal to 5. Thus anywhere in our project we use the phrase infectedDays (caps matters), it will be a stand in for the value 5. You can also define it by saying

var infectedDays = 5;

which means it is possible for the program to change it later. Since we aren’t going to change it later, the choice doesn’t matter, but lets make it a constant so we do not accidentally mess it up.

Now to define our recovery function we use the Javascript syntax:

function recovery(){
	//Code goes here
}

Notice that if we use the phrase recovery() it will be a standin for whatever code we use in there. Now a very easy way to do what we want here is to use the DELAY() primop. A list of primops with descriptions can be found at the Primops page. A primop is simply a primitive operation, or a fairly useful function that is already implemented. The DELAY() primop takes in 3 arguments. It takes in a stock, the number of days to delay, and then the number of days before the recovery function to begin. Thus we can say that we want the value of infected decayDays ago and the function can start at time 0. Thus we would write:

DELAY(infected, decayDays, 0);

Constants are globally defined so we can use it anywhere, but if we want to reference the stock we must do some silly things. The easiest way to do this is have the stock be an argument of the function. Thus we can write:

function recovery(infected){
	return DELAY(infected, decayDays, 0);
}

Notice that the keyword return means that what follows it is what the function returns. So the value of recovery(infected) is DELAY(infected, decayDays, 0). Great! Our function is made. Now lets put it into the schema.

To enter the model into the schema, we must first reference it in the property list. Thus we must have a property methods containing a property list of our methods as such:

	methods: {
		recovery: recovery
		},

Now we can add that flow for recovery is defined by the recovery function evaluated at infection. Putting this in we complete our model. An example of our complete script is as follows:

var SIR = new Project('SIR');
beginProject(SIR);

//Constants

const infectedDays= 5;

//Functions

function recovery(infected){
	return DELAY(infected, decayDays, 0);
}

// Notice the tipping point at .26558
// At .26559, the whole population becomes infected, while if at
// .26558, only 40 total people become infected
// Most values below the tipping point give about 40 total people infected
// All above .26559 give all people as getting infected

SIR.defineSchema( 'main',{
	specifies: 'CAPSULE',
	stocks: ['susceptible','infected','recovered'],
	flows: ['infection','recoveryf'],
	methods: {
		recovery: recovery
		},
	controls: {
		infectionModifier: "infectionModSlider",
	},
	displays: {
		popgraph: {
			specifies: "Graph",
			proxy: "popgraph",
			type: "TIMESERIES",
			title: ["SIR Results"],
			display: "[[susceptible, infected, recovered]]",
		},
		sirtable: {
			specifies: "Table",
			proxy: "sirtable",
			title: ["SIR Results"],
			display: "[[susceptible, infected, recovered]]",
		}
	},
	dynamics: [
		//Initial values
		"susceptible.initial = 1000",
		"infected.initial = 1",
		"recovered.initial = 0",

		// Connections
		"susceptible.output = infection",
		"infection.output = infected",
		"infected.output = recoveryf",
		"recoveryf.output = recovered",

		//Flow Equations
		//Notice the strong tipping point at .27
 		"infection = infectionModifier*((infected*susceptible)/(infected+susceptible))",
		"recoveryf = recovery(infected)",

		//Misc
		"susceptible.nonnegative = true",
		"infected.nonnegative = true",
		"recovered.nonnegative = true",
]
})

SIR.defineSchema('infectionModSlider', {
	specifies: "Slider",
	proxy: "infectionModifier",
	lo: 0,
	hi: 1,
	dec: .00001,
	value: .26558,
})

endProject(0,100,.1,'rk4');

Play around with the model and go have some fun with NovaScript!

A Note On Other Commands

Objects Other Than Stocks

NovaScript lets you use other state-based objects that are like stocks. These can be easier to use in many situations. Below are quick descriptions:

Variable

A variable is a stock which does not have an output. If you think about it, a stock with only a flow that goes in is just a variable whose derivative is the function defined by the flow. Thus if you have a stock which only has one flow in, you can instead declare it as a variable.

To declare a variable, add the following to the main schema:

variables: 'VariableName',

To initialize it, simply use

VariableName.initial = initialValue;

To define the change in the variable, there is no need to specify another object. Simply define the derivative as such:

VariableName.prime = derivativeFunction();

Sequences

A sequence is a stock which has discrete changes. In mathematics these are encountered as variables that are defined by a sequence such as <math>N_{t} = N_{t-1} + 3</math> which means that the value of N is 3 more than the value of N at the last time step. While these are useful for discrete stocks, this becomes also useful for states. This used in the NovaScript AgentSIR Tutorial.

To declare a sequence, add the following to the main schema:

sequences: 'SequenceName',

To initialize it, simply use

SequenceName.initial = initialValue;

To define the dynamics for the variable, all you need to specify is how to get the next value. Simply define the next value as such:

SequenceName.next = sequenceFunction();

Conditionals

Conditionals are a useful part of any program. In JavaScript, a conditional is written as follows:

if(condition==true){ code;}
else {code;}

In the parenthesis goes a condition. This condition can either be a boolean variable (where if it’s true it runs the if code) or it could be a statement like “x==5” which is “if x==5, then …”. Other options like greater than, greater than or equal to, etc. exist. See the JavaScript tutorial for more information.

Debugging

Oh no! Your model doesn’t work? On the cheat sheet there are a bunch of useful commands for debugging. In general, some useful tips for debugging a NovaScript model are:

  • Usually one syntax error will break everything below it. Look at what the error gives as the first line that has a problem. The error is somewhere around there
  • If you are getting nothing running, initialize the program and run it step by step. In the console use the command main.info() to see a list of all of the values of the stocks in your system. This should help you find the problem. If you want to print out a specific stock, add “stockName.verbose()= true” to the dynamics of the main schema to have it print out the data. This works for flows too.

Math Functions

Need some better math functions? Use the JavaScript math library! A list of commands are in the NovaScript Cheat Sheet. Also, you can in the top right of the toolbar load in jStat which contains many statistical commands that are documented on the Statistical Operations.

–Chris Rackauckas, 4 June 2012