# Handwritten Music Symbol Recognition with Deep Ensemble

In ancient times, there was no system to record or document the music. Later, the musical pieces from the classical and post-classical period of European music were documented as scores using western staff notations. These notations are used by most of the modern genres of music due to its versatility. Hence, it is very important to develop a method that can store such sheets of handwritten music scores digitally. Optical Music Recognition (OMR) is a system which automatically interprets the scanned handwritten music scores. In this work, we have proposed a classifier ensemble of deep transfer learning models with Support Vector Machine (SVM) as the aggregation function for handwritten music symbol recognition. We have applied three pre-trained deep learning models, namely ResNet50, GoogleNet and DenseNet161 (each trained on ImageNet) and fine-tuned on our target datasets i.e., music symbol image datasets. The proposed ensemble is able to capture a more complex association of the base learners, thus improving the overall performance. We have evaluated the proposed model on three publicly available standard datasets, namely Handwritten Online Music Symbols (HOMUS), Capitan_Score_Non-uniform and Rebelo_real,and achieved state-of-the-art results for all three datasets.

Hyperparameter Initialization

```python
#hyper params
lr = 1e-4
bs = 32
val_split = 0.85
num_epoch = 20
num_classes = 32
```

We use pytorch to implement the project. Here we include relevant modules and check for GPU.

```python
#imports
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.optim import lr_scheduler
from torch.utils import data
import numpy as np
import torchvision
from  numpy import exp,absolute
from torchvision import datasets, transforms
import matplotlib.pyplot as plt
import time
import os
import copy
import math
from sklearn import svm
import sklearn.model_selection as model_selection
from sklearn.metrics import accuracy_score,f1_score,precision_score ,recall_score

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
```

This function gives us training, validation and test set and takes the path to folder as input. This folder must be arranged as per `torchvision.datasets.Imagefolder` specification.

```python
def get_TVT(path):
    data_transforms = transforms.Compose([
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ])

    dataset = datasets.ImageFolder(path+'train/',transform=data_transforms)

    train_size = math.floor(len(dataset)*val_split)
    val_size = len(dataset) - train_size
    trainset, valset = data.random_split(dataset,lengths=[train_size,val_size])
    testset = datasets.ImageFolder(path+'test/',transform=data_transforms)
    return trainset,valset,testset
```

This is the function to train the model

```python
def train_model(trainset, valset, model, criterion, optimizer, scheduler, num_epochs):
    dataloaders = {
        'train': data.DataLoader(trainset,batch_size=bs,shuffle=True),
        'val' : data.DataLoader(valset,batch_size=bs,shuffle=True)
    }
    dataset_sizes = {'train':len(trainset),'val':len(valset)}
    since = time.time()

    best_model_wts = copy.deepcopy(model.state_dict())
    best_acc = 0.0

    for epoch in range(num_epochs):
        print('Epoch {}/{}'.format(epoch, num_epochs - 1))
        print('-' * 10)

        # Each epoch has a training and validation phase
        for phase in ['train', 'val']:
            if phase == 'train':
                model.train()  # Set model to training mode
            else:
                model.eval()   # Set model to evaluate mode

            running_loss = 0.0
            running_corrects = 0

            # Iterate over data.
            for inputs, labels in dataloaders[phase]:
                inputs = inputs.to(device)
                labels = labels.to(device)
                # print('bruh')

                # zero the parameter gradients
                optimizer.zero_grad()

                # forward
                # track history if only in train
                with torch.set_grad_enabled(phase == 'train'):
                    outputs = model(inputs)
                    _, preds = torch.max(outputs, 1)
                    loss = criterion(outputs, labels)

                    # backward + optimize only if in training phase
                    if phase == 'train':
                        loss.backward()
                        optimizer.step()

                # statistics
                running_loss += loss.item() * inputs.size(0)
                running_corrects += torch.sum(preds == labels.data)
            if phase == 'train':
                scheduler.step()

            epoch_loss = running_loss / dataset_sizes[phase]
            epoch_acc = running_corrects.double() / dataset_sizes[phase]

            print('{} Loss: {:.4f} Acc: {:.4f}'.format(
                phase, epoch_loss, epoch_acc))

            # deep copy the model
            if phase == 'val' and epoch_acc > best_acc:
                best_acc = epoch_acc
                best_model_wts = copy.deepcopy(model.state_dict())

        print()

    time_elapsed = time.time() - since
    print('Training complete in {:.0f}m {:.0f}s'.format(
        time_elapsed // 60, time_elapsed % 60))
    print('Best val Acc: {:4f}'.format(best_acc))

    # load best model weights
    model.load_state_dict(best_model_wts)
    return model
```

This function calculates the model accuracy on test set.

```python
def test_acc(model, testset):
    running_corrects = 0
    testloader = data.DataLoader(testset,batch_size=bs,shuffle=True)
    for inputs, labels in testloader:
        inputs = inputs.to(device)
        labels = labels.to(device)

        with torch.set_grad_enabled(False):
            outputs = model(inputs)
            _, preds = torch.max(outputs, 1)

            running_corrects += torch.sum(preds == labels.data)
    return (running_corrects/len(testset))
```

This function returns a pair of set of data X and label Y. The elements in X represent the concatenated score from the models. If size of dataset is N, number of classes is c and number of trained model is k then the shape of X is (N,ck). The samples are also given weight based on total number of unique classification made on them (Explained later).

```python
def get_weighted_score_ft(models,dataset):
    num_models = len(models)
    X = np.empty((0,num_models*num_classes))
    Y = np.empty((0),dtype=int)
    dataloader = data.DataLoader(dataset,batch_size=1,shuffle=True)
    for inputs,labels in dataloader:
        inputs,labels = inputs.to(device),labels.to(device)
        predictions = set()
        with torch.set_grad_enabled(False):
            x = models[0](inputs)
            _, preds = torch.max(x, 1)
            predictions.add(preds)
            for i in range(1,num_models):
                x1 = models[i](inputs)
                _, preds = torch.max(x1, 1)
                predictions.add(preds)
                x = torch.cat((x,x1),dim=1)
            if len(predictions) > 1:
                X = np.append(X,x.cpu().numpy()*3,axis=0)
            else:
                X = np.append(X,x.cpu().numpy(),axis=0)
            Y = np.append(Y,labels.cpu().numpy(),axis=0)
    return X,Y

```

We load the models with pretrained weights

```python
def get_models():
    googlenet = torchvision.models.googlenet(pretrained=True)
    resnet = torchvision.models.resnet50(pretrained=True)
    densenet = torchvision.models.densenet161(pretrained=True)

    densenet.classifier = nn.Linear(2208,num_classes)
    resnet.fc = nn.Linear(2048,num_classes)
    googlenet.fc = nn.Linear(1024,num_classes)
    densenet = densenet.to(device)
    resnet = resnet.to(device)
    googlenet = googlenet.to(device)

    return [densenet,googlenet,resnet]
```

This is the main code cell where all the functions are utilised together. Now let us consider there are $K$ number of base classifiers $\{CF_1, CF_2, \dots, CF_K\}$ to deal with an $n$-class classification problem. Hence, the output of any classifier (say, $CF_i$) is an $n$-dimensional vector $O_i = {s_1^i, s_2^i, \dots, s_n^i}$. Here, $s_j^i$ is confidence score produced by $i_{th}$ classifier for the $j_{th}$ class. We concatenate all the output vectors produced by the classifiers $\{CF_1, CF_2, \dots, CF_K\}$ to get a vector $S$ of length $nK$. $S$ is represented by

\begin{equation}
    \label{equ:final_vector}
    S = \{s_1^1, s_1^2, \dots, s_2^1, s_2^2, \dots, s_n^K\}
\end{equation}

One such vector $S$ is produced for every sample of the dataset. Let us consider that we have $N$ such samples with corresponding labels $y_i$ in the dataset to be used for training. Thus obtained the set $\{(S_1, y_1), (S_2, y_2),\\ \dots, (S_N, y_N)\}$ on which we train the SVM model. To introduce weights on samples, we consider the total number of unique predictions made on a sample by the base classifiers. For example, if there are three base classifiers and for some sample two of the classifiers are predicting the label 'class-x' and the remaining one is predicting the label 'class-y', then the total number of unique predictions of that sample is $2$. If the total number of prediction is greater than $1$, it suggests that there is a conflict among the classifiers on the correct class. So we propose that the SVM must put more emphasis on these samples in order to approximate a better decision boundary or support vectors.

A sample is assigned with $\mathcal{W}$ times more weight if the number of unique predictions regarding the corresponding image is greater than $\lambda$, which is an integer and whose value lies between $[1, K]$. In this work, we choose the values of both $K$ and $\lambda$ to be 3. The value of $\mathcal{W}$ is taken as 3 which is decided experimentally.

While testing and image it is first passed through all of the three DL models and the three output vectors are obtained. Then these output vectors are concatenated, during this concatenation the order of the models are maintained (as same as during training). We pass this vector through the trained SVM classifier, which predicts the final class of our test image.

```python
criterion = nn.CrossEntropyLoss()
ensemble_accuracy=[]
for fold in ['Fold_1','Fold_2','Fold_3','Fold_4','Fold_5']:
    for folder in  ['HOMUS']: #['Capitan_Score_Non-uniform','Capitan_Score_Uniform','Fornes_Music_Symbols_labelled']['Rebelo_Syn_labelled']:
        trainset,valset,testset = get_TVT('/content/homus/'+fold+'/',folder)
        models = get_models()
        for model in models:
            optimizer = optim.Adam(model.parameters(),lr=lr)
            exp_lr_scheduler = lr_scheduler.StepLR(optimizer, step_size=9, gamma=0.1)
            model = train_model(trainset, valset, model, criterion, optimizer, exp_lr_scheduler,num_epoch)

            print(test_acc(model,testset))
    train_X, train_Y = get_weighted_score_ft(models,trainset)
    test_X, test_Y = get_weighted_score_ft(models,testset)
    clf = svm.SVC(kernel='rbf',break_ties=True).fit(train_X, train_Y)
    pred = clf.predict(test_X)
    acc = accuracy_score(test_Y, pred)
    ensemble_accuracy.append(acc)
    print('Ensemble on '+fold+': '+str(acc))
print("Average Ensemble Accuracy:",sum(ensemble_accuracy)/len(ensemble_accuracy))
```


# Dynamic Simulation: The Basic Procedure
Once a set of dynamic mass balance equations has been formulated, they can be numerically solved, and thus the behavior of a network can be simulated in response to environmental and genetic changes. Simulation results can be obtained using a number of different software packages. Dynamic simulation generates the time dependent behavior of the concentrations, i.e., $\textbf{x}$(t). This solution can be obtained in response to several different types of perturbations and the results graphically displayed. The basic principles and procedures associated with dynamic simulation are covered in this chapter. The following three chapters then apply the simulation process to a set of simple but progressively more complex and relevant examples.

## Numerical Solutions
Network dynamics are described by a set of ordinary differential equations (ODEs): the dynamic mass balance equations; see Eq. (1.1). To obtain the dynamic solutions, we need three things: first, the equations themselves; second, the numerical values for the kinetic constants that are in the equations; and third, the initial conditions and parameters that are being perturbed. We describe each briefly.

**1.** To formulate the mass balances we have to specify the system boundary, the fluxes in and out of the system, and the reactions that take place in the network. From the set of reactions that are taking place, a stoichiometric matrix is formed. This matrix is then put into Eq. (1.1) . One can multiply out the individual dynamic mass balances, as was done in Eq. (2.13)  for the adenosine phosphate network, to prevent a large number of numerical operations that involve multiplication of reaction rates by the zero elements in $\textbf{S}$. The reaction rate laws for the reactions are then identified and substituted into the equations. Typically, one would use elementary kinetics as shown in Eq. (2.6), or apply more complex rate laws if they are appropriate and available. This process leads to the definition of the dynamic mass balances.

**2.** The numerical values for the kinetic parameters in the rate laws have to be specified, as do any imposed fluxes across the system boundary. Obtaining numerical values for the kinetic constants is typically difficult. They are put into a parameter vector designated by $\textbf{k}$. In select cases, detailed kinetic characterization has been carried out. More often, though, one only knows these values approximately. It is important to make sure that all units are consistent throughout the equations and that the numerical values used are in the appropriate units.

**3.** With the equations and numerical values for the kinetic constants specified $(\textbf{k})$, we can simulate the responses of the network that they represent. To do so, we have to set initial conditions $(x_0)$. This leads to the numerical solution of

$$\begin{equation} \frac{d\textbf{x}}{dt} = \textbf{Sv(x;k)},\ \textbf{x}(t = 0) = \textbf{x}_0 \tag{3.1} \end{equation}$$

There are three conditions that are typically considered.

1. First, the initial conditions for the concentrations are set, and the motion of the network into a steady state (open system) or equilibrium (closed system) is simulated. This scenario is typically physiologically unrealistic since individual concentrations cannot simply change individually in a living cell.
2. Second, a change in an input flux is imposed on a network that is in a steady state. This scenario can be used to simulate the response of a cell to a change in its environment.
3. Third, a change in a kinetic parameter is implemented at the initial time. The initial concentrations are typically set at the steady state values with the nominal value of the parameter. The equations are then simulated to a long time to obtain the steady state values that correspond to the altered kinetic parameters. These are set as the initial conditions when examining the responses of the system with the altered kinetic properties.

**4.** Once the solution has been obtained it can be graphically displayed and the results analyzed. There are several ways to accomplish this step, as detailed in the next two sections. The analysis of the results can lead to post-processing of the output to form an alternative set of dynamic variables.

The simulation is implemented using a numerical solver. Currently, such implementation is carried out using standard and readily available software, such as Mathematica or MATLAB. Specialized simulation packages are also available (Table 3.1). After the simulation is set up and the conditions specified, the software computes the concentrations as a function of time. The output is a file that contains the numerical values of the concentrations at a series of time points (Figure 3.1). This set of numbers is typically graphically displayed, and/or used for subsequent computations.

**Table 3.1:** Available software for dynamic simulation. Assembled by Neema Jamshidi.

We will be using iPython notebooks using the python software package **MASSpy**  as our simulation software.

## Graphically Displaying the Solution
The simulation procedure described in the previous section results in a file that contains the concentrations as a function of time (Figure 3.1). These results are graphically displayed, typically in two ways: by plotting the concentrations as a function of time, or by plotting two concentrations against one another with time as a parameter along the trajectory.

**Figure 3.1:** The fundamental structure of the file $\textbf{x}$(t) that results from a numerical simulation. The two vertical bars show the list of values that would be used to compute $\sigma_{12}$(2) (see Eq. 3.8); that is, the correlation between $x_1$ and $x_2$ with a time lag of 2.

Before describing these methods, we observe certain fundamental aspects of the equations that we are solving. The dynamic mass balances can be expanded as:

$$\begin{equation} \frac{d\textbf{x}}{dt} = \textbf{Sv(x)} = \sum\textbf{s}_i v_i(\textbf{x}) \tag{3.2} \end{equation}$$

In other words, the time derivatives are linear combinations of the reaction vectors $(\textbf{s}_i)$ weighted by the reaction rates, that in turn change with the concentrations that are time varying. Thus, the motions are linear combinations of the directions specified by $\textbf{s}_i$. This characteristic is important because if the $v_i$. have different time constants, the motion can be decomposed in time along these reaction vectors.

### Time Profiles
The simulation results in a file that contains the vector $\textbf{x}$(t) and the time points at which the numerical values for the concentrations are given. These time points can be specified by the user or are automatically generated by the solver used. Typically, the user specifies the initial time, the final time, and sometimes the time increment between the time points where the simulator stores the computed concentration values in the file. The results can then be graphically displayed depending on a few features of the solution. Some of these are shown in Figure 3.2 and are now briefly described:

* Panel A: The most common way to display a dynamic solution is to plot the concentration as a function of time.

* Panel B: If there are many concentration variables they are often displayed on the same graph.

* Panel C: In many cases there are different response times and one plots multiple time profiles where the x-axis on each plot is scaled to a particular response time. Alternatively, one can use a logarithmic scale for time.

* Panel D: If a variable moves on many time scales changing over many orders of magnitude, the y-axis is often displayed on a logarithmic scale.

**Figure 3.2:** Graphing concentrations over time. (a) A single concentration shown as a function of time. (b) Many concentrations shown as a function of time. (c) A single concentration shown as a function of time separately on different time scales. (d) The logarithm of a single concentration shown as a function of time to distinguish the decay on different time scales.

The solution can thus be displayed in different ways depending on the characteristics of the time profiles. One normally plays with these representations to get an understanding of the responses of the network that they have formulated and to represent the features in which one is interested.

### Dynamic phase portraits
Dynamic phase portraits represent trajectories formed when two concentrations plotted against each other, parameterized with respect to time (Figure 3.3). The dynamic trajectories in the diagram move from an initial state to a final state. Analysis of these trajectories can point to key dynamic relationships between compounds in a biochemical reaction network. For example, if a system is dynamically stable, the dynamic trajectories will converge to a single point in the plane, known as an attracting fixed pointattracting fixed point. A stable steady-state point would represent a homeostatic state. Conversely, if the system is unstable, the trajectories will not approach a fixed point but diverge away from it. The former is essentially always the case for biochemical reaction networks representing real cells. The way the trajectories converge on the steady state is highly informative as different dynamic characteristics are evident from the trajectory.

**Figure 3.3:** A dynamic phase portrait.

### Characteristic features of phase portraits
A trajectory in the phase portraitphase portrait may indicate the presence of one or more general dynamic features. Namely, the shapes of the trajectories contain significant information about the dynamic characteristics of a network. Some important features of trajectories in a phase portrait are shown in Figure 3.4

**Figure 3.4:** General features of dynamic phase portraits. Dynamic phase portraits are formed by graphing the time dependent concentrations of two concentrations $(x_1$ and $x_2)$ against one another. Phase portraits have certain characteristic features. (a) Conservation relationship. (b) A pair of concentrations that could be in quasi-equilibrium with one another. (c) Motion of the two concentrations dynamically independent of one another. (d) Closed loop traces representing either a periodic motion or a return to the original steady state. Modified from Kauffman 2002 [64].

1. When the trajectory has a negative slope, it indicates that one concentration is increasing while the other is decreasing. The concentrations are moving on the same time scales but in opposite directions; that is, one is consumed while the other is produced. This feature might represent the substrate concentration versus the product concentration of a given reaction. Such behavior helps define aggregate concentration variablesaggregate concentration variables.

2. When a trajectory in the phase portrait between two concentrations is a straight line with a positive slope, it means that the two concentrations are moving in tandem; i.e., as one increases so does the other. This feature is observed when two or more concentrations move on the same time scales and are in quasi-equilibrium with one another. Such behavior helps define aggregate concentration variables.

3. When a trajectory is vertical or horizontal, it indicates that one of the concentrations is changing while the other remains constant. This feature implies either that the motions of the concentrations during the trajectory are independent of one another or that the dynamic motions of the concentrations progress on different characteristic time scales. Such behavior helps define time scale decomposition.

4. When a trajectory forms a closed loop, it implies one of two possibilities. The system never converges to a steady state over time but oscillates forming a closed loop trajectory. On the other hand, if the orbit begins at one point, moves away from it, then returns to the same point after a sufficiently long time interval, then it implies that a change in another variable in the system forced it away from its steady state temporarily, but it returned to the original steady state. Such behavior helps define disturbance rejection characteristics.

**Figure 3.5:** A schematic of a tiled phase portrait.The matrix is symmetric, making it possible to display statistical information about a phase portrait in the mirror position.The diagonal elements are meaningless.Originally developed in Kauffman 2002 [64].

The qualitative characteristics of dynamic phase portraitsphase portrait can provide insight into the dynamic features of a network. A trajectory may have more than one of these basic features. For instance, there can be a fast independent motion (i.e., a horizontal phase portrait trajectory) followed by a line with a positive slope after an equilibrium state has been reached.

### Tiling dynamic phase portraits
Phase portraits show the dynamic relationships between two variables on multiple time scales, see Figure 3.5. If a system has $n$ variables, then there are $n^2$ dynamic phase portraits. All pair-wise phase portraits can be tiled in a matrix form where the $\textit{i}$, $\textit{j}$ entry represents the dynamic phase portrait between variables $x_i$ and $x_j$. Note that such an array is symmetric and that the diagonal elements are un-informative. Thus, there are $(n^2-n)/2$ phase portraits of interest. This feature of this graphical representation opens the possibility of putting the phase portrait in the $\textit{i}$, $\textit{j}$ position in the array and showing other information (such as a regression coefficient or a slope) in the corresponding $\textit{j}$, $\textit{i}$ position.

Since the time scales in biochemical reaction networks typically vary over many orders of magnitude, it often makes sense to make a series of tiled phase portraits, each of which represents a key time scale. For instance, rapid equilibration leads to straight lines with positive slopes in the phase portrait (Figure 3.4b) where the slope is the equilibrium constant of the reaction. This may be one of many dynamic events taking place. If a phase portrait is graphed separately on this time scale alone, the positive line will show up with a high regression coefficient and a slope that corresponds to the equilibrium constant.

## Post-Processing the Solution

The initial suggestions obtained from graphing and visualizing the concentration vector $\textbf{x}$(t) can lead to a more formal analysis of the results. We describe three post-processing procedures of $\textbf{x}$(t).

### Computing the fluxes from the concentration variables:
The solution for the concentrations $\textbf{x}$(t)can be used to compute the fluxes from

$$\begin{equation} \textbf{v}(t)= \textbf{v}(\textbf{x}(t)) \tag{3.3} \end{equation}$$

and subsequently we can plot the fluxes in the same way as the concentrations. Graphical information about both the $\textbf{x}$(t) and $\textbf{v}$(t) is useful.

### Combining concentrations to form aggregate variables:
The graphical and statistical multi-time scale analysis discussed above may lead to the identification of aggregate variables. Pooled variables, p, are computed from

$$\begin{equation} \textbf{p}(t)= \textbf{Px}(t)) \tag{3.4} \end{equation}$$

where the pool transformation matrix, $\textbf{P}$, defines the linear combination of the concentration variables that forms the aggregate variables. For instance, if we find that a logical way to pool two variables, $x_1$ and $x_2$, into new aggregate variables is $p_1 = x_1 + x_2$ and $p_2 = x_1 - x_2$, then we form the following matrix equation describing these relationships as:

$$\begin{equation} \textbf{p}(t) = \textbf{Px}(t) = \begin{pmatrix} {p_1(t)} \\ {p_2(t)} \end{pmatrix} = \begin{pmatrix} {1} & {1} \\ {1} & {-1} \end{pmatrix} \begin{pmatrix} {x_1(t)} \\ {x_2(t)} \end{pmatrix} = \begin{pmatrix} {x_1(t) + x_2(t)} \\ {x_1(t) - x_2(t)} \end{pmatrix} \end{equation}$$

The dynamic variables, $\textbf{p}$(t), can be graphically studied as described in the previous section.

#### Example: The Phosphorylated Adenosines
The pool formation discussed in Chapter 2 can be described by the pool transformation matrix:

$$\begin{equation}\textbf{P} = \begin{pmatrix} {1} & {1} & {0} \\ {2} & {1} & {0} \\ {1} & {1} & {1} \end{pmatrix} \end{equation}$$
$$\tag{3.5}$$

and thus

$$\begin{equation}\\textbf{p} = \textbf{Px} = \textbf{P}\begin{pmatrix} {\text{ATP}} \\ {\text{ADP}} \\ {\text{AMP}} \end{pmatrix} = \begin{pmatrix} {\text{ATP} + \text{ADP}} \\ {2 \text{ATP} + \text{ADP}} \\ {\text{ATP} + \text{ADP} + \text{AMP}} \end{pmatrix}\end{equation}$$
$$\tag{3.6}$$

The pool sizes $p_i$(t) can then be graphed as a function of time.

### Correlating concentrations over time:
One can construct the time-separated correlation matrix, $\textbf{R}$, based on a time scale structure of a system. In this matrix, we compute the correlation between two concentrations on a time scale as:

$$\begin{equation} \textbf{R}(\tau) = (r_{ij}) = \frac{\sigma_{ij}(\tau)}{\sqrt{\sigma_{ii}\sigma_{jj}}} \tag{3.7} \end{equation}$$

in which $\sigma_{ii}$ is the variance of the dataset $x_i(k)$ and $\sigma_{ij}(\tau)$ is the time-lagged covariance between the discrete, uniformly sampled datasets $x_i(k)$ and $x_j(k + \tau)$, determined as,

$$\begin{equation} \sigma_{ij}(\tau) = \frac{1}{n}\sum\limits_{k=1}^{n-\tau} (x_i(k) - \bar{x_i})(x_j(k + \tau) - \bar{x_j}) \tag{3.8} \end{equation}$$

in which $n$ is the number of data points in the series, and $\bar{x_i}$ indicates the average value of the series $x_i$. The values in $\textbf{R}$ range from -1 to 1, indicating perfect anti-correlation or correlation, respectively, between two datasets with a delay of time steps. Elements in $\textbf{R}$ equal to zero indicate that the two corresponding datasets are completely uncorrelated. If such correlation computations were done for the cases shown in Figure 3.4, one would expect to find a strong negative correlation for the data shown in Figure 3.4a, a strong positive correlation for Figure 3.4b, and no correlation for Figure 3.4c,

The correlation computations can be performed with an increment, $\tau$, offset in time between two concentrations. An example of a time offset is shown in Figure 3.2 showing the values used from the output file to compute the correlation between $x_1$ and $x_2$ with a time lag of 2.

The matrix of phase portraits is symmetric with uninformative diagonal elements. One can therefore enter a correlation coefficient corresponding to a particular phase portrait in the transpose position to the phase portrait in the matrix. A correlation coefficient provides a quantitative description of the phase portrait's linearity between the two variables over the time scale displayed. In addition to the correlation coefficient, the slope can be computed and displayed, giving the equilibrium constant between the two compounds displayed.

## Demonstration of the Simulation Procedure in MASSpy
### Setting up the model
The following builds the model of three reactions in series that is described on pages 51-56 in the book. We show how the model is built, simulated, solutions graphically displayed, solutions post processed and analyzed mathematically.

To construct a model in **MASSpy**, the `MassModel`, `MassReaction`, and `MassMetabolite` objects need to be imported into the environment.

```python
from mass import MassModel, MassMetabolite, MassReaction
```

#### Defining metabolites and reactions
One method for creating the model is to objects that represent the metabolites and reactions.
Metabolite are represented by `MassMetabolite` objects, and can be created by providing a unique identifier for that object. Therefore we can define the four metabolites, $x_1, x_2, x_3$, and $x_4$ by the following;

```python
x1 = MassMetabolite('x1')
x2 = MassMetabolite('x2')
x3 = MassMetabolite('x3')
x4 = MassMetabolite('x4')
```

Reactions are represented by `MassReaction` objects, and like metabolites, they can be also created by providing a unique identifier for that object.

```python
v1 = MassReaction('v1')
v2 = MassReaction('v2')
```

By default, a reaction is considered reversible. However, if we wish to make an irreversible reaction, we set the `reversible` argument to `False`.

```python
v3 = MassReaction('v3', reversible=False)
```

Once the `MassReaction` objects have been created, metabolites can be added to the reaction using the `MassReaction.add_metabolites` method.
To quickly see how this method is used, we can use the `help()` function. Alternatively, we can go to the [API documentation](https://masspy.readthedocs.io/en/latest/autoapi/index.html) and read about how the [MassReaction.add_metabolites](https://masspy.readthedocs.io/en/latest/autoapi/mass/core/mass_reaction/index.html#mass.core.mass_reaction.MassReaction.add_metabolites) method works.

```python
help(MassReaction.add_metabolites)
```

    Help on function add_metabolites in module mass.core.mass_reaction:

    add_metabolites(self, metabolites_to_add, combine=True, reversibly=True)
        Add metabolites and their coefficients to the reaction.

        If the final coefficient for a metabolite is 0 then it is removed from
        the reaction.

        The change is reverted upon exit when using the :class:`~.MassModel`
        as a context.

        Notes
        -----
        * A final coefficient of < 0 implies a reactant and a final
          coefficient of > 0 implies a product.

        * Extends :meth:`~cobra.core.reaction.Reaction.add_metabolites` of the
          :class:`cobra.Reaction <cobra.core.reaction.Reaction>` by first
          ensuring that the metabolites to be added are
          :class:`.MassMetabolite`\ s and not
          :class:`cobra.Metabolites <cobra.core.metabolite.Metabolite>`.
          and error message raised reflects the :mod:`mass` object.

        * If a :class:`cobra.Metabolite <cobra.core.metabolite.Metabolite>` is
          provided. a warning is raised and a :class:`.MassMetabolite`
          will be instantiated using the
          :class:`cobra.Metabolite <cobra.core.metabolite.Metabolite>`.

        Parameters
        ----------
        metabolites_to_add : dict
            A ``dict`` with :class:`.MassMetabolite`\ s or metabolite
            identifiers as keys and stoichiometric coefficients as values. If
            keys are strings (id of a metabolite), the reaction must already
            be part of a :class:`~.MassModel` and a metabolite with the given
            id must already exist in the :class:`~.MassModel`.
        combine : bool
            Describes the behavior of existing metabolites.
            If ``True``, the metabolite coefficients are combined together.
            If ``False`` the coefficients are replaced.
        reversibly : bool
            Whether to add the change to the context to make the change
            reversible (primarily intended for internal use).

        See Also
        --------
        :meth:`subtract_metabolites`

To use `MassReaction.add_metabolites`, a dictionary input is required, where the `MassMetabolite` objects are keys and the value is their stoichiometric coefficient. Reactants are defined with negative coefficients, while products are defined with positive coefficients.

```python
v1.add_metabolites({x1 : -1, x2 : 1})
v2.add_metabolites({x2 : -1, x3 : 1})
v3.add_metabolites({x3 : -1, x4 : 1})
```

Reactions, e.g., $v_1$ can be used to define any kind of chemical transformation, association, activation etc. A series of methods are provided for inspection of the reaction.

```python
v1.id
```

    'v1'

```python
v1.reactants
```

    [<MassMetabolite x1 at 0x7fee003f4b20>]

```python
v1.products
```

    [<MassMetabolite x2 at 0x7fee003f4a90>]

```python
v1.stoichiometry
```

    [-1, 1]

```python
v1.reversible
```

    True

Check the documentation for the `MassReaction` class for further details.

#### Model Setup
To construct a model capable of dynamic simulation, a `MassModel` object must be created. The minimal input for creating a `MassModel` object is a unique identifier.

```python
model = MassModel('Model')
```

    Set parameter Username

To add reactions and their corresponding metabolites to the model, the `MassModel.add_reactions` method can be used by providing a list of reactions to add to the model.

```python
model.add_reactions([v1, v2, v3])
```

#### Model Inspection
Similar to the `MassReaction` object, the `MassModel` object also has various methods that can be used to inspect the model. For example, to obtain the list of reactions and species in the system:

```python
model.reactions
```

    [<MassReaction v1 at 0x7fed803a5610>,
     <MassReaction v2 at 0x7fed803a56d0>,
     <MassReaction v3 at 0x7fee003f4af0>]

```python
model.metabolites
```

    [<MassMetabolite x1 at 0x7fee003f4b20>,
     <MassMetabolite x2 at 0x7fee003f4a90>,
     <MassMetabolite x3 at 0x7fee003f4b50>,
     <MassMetabolite x4 at 0x7fee003f4be0>]

In some circumstances, it is helpful to iterate through a reaction and its associated metabolites using a loop:

```python
print("Model ID: %s" % model.id)
for rxn in model.reactions:
    print("\nReaction: %s\n------------" % rxn.id)
    for metab, stoichiometry in rxn.metabolites.items():
        print("%s: %s " % (metab.id, stoichiometry))
```

    Model ID: Model

    Reaction: v1
    ------------
    x1: -1
    x2: 1

    Reaction: v2
    ------------
    x3: 1
    x2: -1

    Reaction: v3
    ------------
    x4: 1
    x3: -1

To examine the stoichiometric matrix:

```python
model.S
```

    array([[-1.,  0.,  0.],
           [ 1., -1.,  0.],
           [ 0.,  1., -1.],
           [ 0.,  0.,  1.]])

The stoichiometric matrix can also be viewed as a `pandas.DataFrame` with annotated information about the metabolites and reactions.

Note: The `update_model` argument can be used to store matrix as the specified `array_type` for the next time the stoichiometric matrix is viewed.

```python
model.update_S(array_type="DataFrame", update_model=True)
```

<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>v1</th>
      <th>v2</th>
      <th>v3</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>x1</th>
      <td>-1.0</td>
      <td>0.0</td>
      <td>0.0</td>
    </tr>
    <tr>
      <th>x2</th>
      <td>1.0</td>
      <td>-1.0</td>
      <td>0.0</td>
    </tr>
    <tr>
      <th>x3</th>
      <td>0.0</td>
      <td>1.0</td>
      <td>-1.0</td>
    </tr>
    <tr>
      <th>x4</th>
      <td>0.0</td>
      <td>0.0</td>
      <td>1.0</td>
    </tr>
  </tbody>
</table>
</div>

The rate equations can be examined,

```python
for rxn, rate in model.rates.items():
    print("%s: %s" % (rxn.id, rate))
```

    v1: kf_v1*(x1(t) - x2(t)/Keq_v1)
    v2: kf_v2*(x2(t) - x3(t)/Keq_v2)
    v3: kf_v3*x3(t)

or just one rate equation can be called out:

```python
print(model.rates[v2])
```

    kf_v2*(x2(t) - x3(t)/Keq_v2)

The ordinary differential equations can be also be listed in full,

```python
for metab, ode in model.odes.items():
    print("%s: %s" % (metab.id, ode))
```

    x1: -kf_v1*(x1(t) - x2(t)/Keq_v1)
    x2: kf_v1*(x1(t) - x2(t)/Keq_v1) - kf_v2*(x2(t) - x3(t)/Keq_v2)
    x3: kf_v2*(x2(t) - x3(t)/Keq_v2) - kf_v3*x3(t)
    x4: kf_v3*x3(t)

or just one ordiniary differential equation can be called out:

```python
print(model.odes[x3])
```

    kf_v2*(x2(t) - x3(t)/Keq_v2) - kf_v3*x3(t)

Note that none of these expressions have been provided during the model construction process. Instead the expresions have been generated automatically from the provided list of reactions and their metabolites.

#### Set parameters and initial condtions
When using Jupyter notebooks, an overview of the model is rendered as a table when only the model object is called. Note that this also applies to metabolites and reactions.

```python
model
```

<table>
    <tr>
        <td><strong>Name</strong></td><td>Model</td>
    </tr><tr>
        <td><strong>Memory address</strong></td><td>0x07fedd07132b0</td>
    </tr><tr>
        <td><strong>Stoichiometric Matrix</strong></td>
        <td>4x3</td>
    </tr><tr>
        <td><strong>Matrix Rank</strong></td>
        <td>3</td>
    </tr><tr>
        <td><strong>Number of metabolites</strong></td>
        <td>4</td>
    </tr><tr>
        <td><strong>Initial conditions defined</strong></td>
        <td>0/4</td>
    </tr><tr>
        <td><strong>Number of reactions</strong></td>
        <td>3</td>
    </tr><tr>
        <td><strong>Number of genes</strong></td>
        <td>0</td>
    </tr><tr>
        <td><strong>Number of enzyme modules</strong></td>
        <td>0</td>
    </tr><tr>
        <td><strong>Number of groups</strong></td>
        <td>0</td>
    </tr><tr>
        <td><strong>Objective expression</strong></td>
        <td>0</td>
    </tr><tr>
        <td><strong>Compartments</strong></td>
        <td></td>
    </tr>
</table>

From the model overview it can be seen that no parameters or initial conditions have been defined. Parameters can be defined directly for a specific reaction:

```python
v1.forward_rate_constant = 1
v2.kf = 0.01 # Shorthand method
v3.kf = 0.0001

v1.equilibrium_constant = 1
v2.Keq = 1 # Shorthand method

for param_type, param_dict in model.parameters.items():
    print("%s: %s" %(param_type, param_dict))
```

    kf: {'kf_v1': 1, 'kf_v2': 0.01, 'kf_v3': 0.0001}
    Keq: {'Keq_v1': 1, 'Keq_v2': 1, 'Keq_v3': inf}
    kr: {}
    v: {}
    Custom: {}
    Boundary: {}

Initial conditions for metabolites can be defined directly for a specific metabolite,

```python
x1.initial_condition = 1
x2.ic  = 0 # Shorthand method
model.initial_conditions
```

    {<MassMetabolite x1 at 0x7fee003f4b20>: 1,
     <MassMetabolite x2 at 0x7fee003f4a90>: 0}

or a dictionary can be used to define them in a model directly. The `update_metabolites` argument will subsequently update the initial condition in the metabolite object as well.

```python
model.update_initial_conditions({x3: 0, x4:0})
model.initial_conditions
```

    {<MassMetabolite x1 at 0x7fee003f4b20>: 1,
     <MassMetabolite x2 at 0x7fee003f4a90>: 0,
     <MassMetabolite x3 at 0x7fee003f4b50>: 0,
     <MassMetabolite x4 at 0x7fee003f4be0>: 0}

Check the documentation for further details on the `MassModel` class.

### Simulating Dynamic Responses
#### Simulate
Simulating the model once it is set up properly is very simple. To set up the simulation, we use a `Simulation` object. The simulation object requires a `MassModel` for initialization.

```python
from mass import Simulation
```

```python
sim = Simulation(model, verbose=True)
```

    WARNING: No compartments found in model. Therefore creating compartment 'compartment' for entire model.

    Successfully loaded MassModel 'Model' into RoadRunner.

The `Simulation.simulate` method from the will integrate the ordinary differential equations of the system in the provided time interval and return the dynamic responses of concentrations and fluxes.

```python
t0 = 0
tf = 1e6

conc_sol, flux_sol = sim.simulate(
    model, time=(t0, tf), interpolate=True, verbose=True)
```

    Getting time points
    Setting output selections
    Setting simulation values for 'Model'
    Simulating 'Model'
    Simulation for 'Model' successful
    Adding 'Model' simulation solutions to output
    Updating stored solutions

Note: If a model is unable to be simulated, a warning will be raised. By setting the `verbose` argument to `True`, a QC/QA report outlining inconsistencies, missing values, and other issues will also be generated and displayed to assist in diagnosing the reason why a model could not be simulated.

#### Inspect the solution
As the default setting, the Simulation object utilizes scipy interpolating functions to capture the concentration and flux responses (see documentation for scipy.interpolate for additional information). The `Simulation.simulate_model` method returns two `cobra.DictLists` containing specialized dictionaries known as `MassSolution` objects.

The first `MassSolution` object contains the `MassMetabolite` identifiers as keys, and their corresponding concentration solutions as values.

```python
for metabolite, solution in conc_sol.items():
    print(metabolite, solution)
```

    x1 <scipy.interpolate.interpolate.interp1d object at 0x7fedf092c6d0>
    x2 <scipy.interpolate.interpolate.interp1d object at 0x7fed88de0e50>
    x3 <scipy.interpolate.interpolate.interp1d object at 0x7fedf092c720>
    x4 <scipy.interpolate.interpolate.interp1d object at 0x7fedf092c770>

Similarly, the second `MassSolution` object contains the `MassReaction` identifiers as keys, and their corresponding flux solutions as values.

```python
for reaction, solution in flux_sol.items():
    print(reaction, solution)
```

    v1 <scipy.interpolate.interpolate.interp1d object at 0x7fedf092c8b0>
    v2 <scipy.interpolate.interpolate.interp1d object at 0x7fedf092c900>
    v3 <scipy.interpolate.interpolate.interp1d object at 0x7fedf092c950>

#### Query time responses
The interpolating functions are functions of time. Therefore, we can evaluate the interpolating function at a specific time point using the following:

```python
time_points = 100;
for metabolite, interpolating_function in conc_sol.items():
    print("%s: %s" % (metabolite, interpolating_function(time_points)))
print()
for reaction, interpolating_function in flux_sol.items():
    print("%s: %s" % (reaction, interpolating_function(time_points)))
```

    x1: 0.3710242389082219
    x2: 0.3704524448547136
    x3: 0.2569363810507253
    x4: 0.0015869284328310024

    v1: 0.0005717940535082393
    v2: 0.0011351606380398832
    v3: 2.5693638105072534e-05

It is also possible to get values for multiple time points at once:

```python
time_points = [0.01, 0.1, 1, 10, 100, 1000];
for metabolite, interpolating_function in conc_sol.items():
    print("%s: %s" % (metabolite, interpolating_function(time_points)))
print()
for reaction, interpolating_function in flux_sol.items():
    print("%s: %s" % (reaction, interpolating_function(time_points)))
```

    x1: [0.99009934 0.90936384 0.56699581 0.4790072  0.37102424 0.32389592]
    x2: [0.00990017 0.09058937 0.43018534 0.47682748 0.37045244 0.32388517]
    x3: [4.96651050e-07 4.67952432e-05 2.81873928e-03 4.41437817e-02
     2.56936381e-01 3.21735095e-01]
    x4: [1.65778860e-13 1.58583691e-10 1.07541682e-07 2.15291648e-05
     1.58692843e-03 3.04838051e-02]

    v1: [9.80199167e-01 8.18774471e-01 1.36810476e-01 2.17971609e-03
     5.71794054e-04 1.07505761e-05]
    v2: [9.89967148e-05 9.05425714e-04 4.27366598e-03 4.32683701e-03
     1.13516064e-03 2.15007648e-05]
    v3: [4.96651050e-11 4.67952432e-09 2.81873928e-07 4.41437817e-06
     2.56936381e-05 3.21735095e-05]

For example, a `pandas.Dataframe` of concentration values at different time points could be generated using this method:

```python
import pandas as pd
```

```python
data = [interpolating_function(time_points)
        for interpolating_function in conc_sol.values()]
index_col = [metabolite for metabolite in conc_sol.keys()]
pd.DataFrame(data, index=index_col, columns=time_points)
```

<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>0.01</th>
      <th>0.10</th>
      <th>1.00</th>
      <th>10.00</th>
      <th>100.00</th>
      <th>1000.00</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>x1</th>
      <td>9.900993e-01</td>
      <td>9.093638e-01</td>
      <td>5.669958e-01</td>
      <td>0.479007</td>
      <td>0.371024</td>
      <td>0.323896</td>
    </tr>
    <tr>
      <th>x2</th>
      <td>9.900168e-03</td>
      <td>9.058937e-02</td>
      <td>4.301853e-01</td>
      <td>0.476827</td>
      <td>0.370452</td>
      <td>0.323885</td>
    </tr>
    <tr>
      <th>x3</th>
      <td>4.966510e-07</td>
      <td>4.679524e-05</td>
      <td>2.818739e-03</td>
      <td>0.044144</td>
      <td>0.256936</td>
      <td>0.321735</td>
    </tr>
    <tr>
      <th>x4</th>
      <td>1.657789e-13</td>
      <td>1.585837e-10</td>
      <td>1.075417e-07</td>
      <td>0.000022</td>
      <td>0.001587</td>
      <td>0.030484</td>
    </tr>
  </tbody>
</table>
</div>

The same can be done for the fluxes:

```python
data = [interpolating_function(time_points)
        for interpolating_function in flux_sol.values()]
index_col = [reaction for reaction in flux_sol.keys()]
pd.DataFrame(data, index=index_col, columns=time_points)
```

<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>0.01</th>
      <th>0.10</th>
      <th>1.00</th>
      <th>10.00</th>
      <th>100.00</th>
      <th>1000.00</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>v1</th>
      <td>9.801992e-01</td>
      <td>8.187745e-01</td>
      <td>1.368105e-01</td>
      <td>0.002180</td>
      <td>0.000572</td>
      <td>0.000011</td>
    </tr>
    <tr>
      <th>v2</th>
      <td>9.899671e-05</td>
      <td>9.054257e-04</td>
      <td>4.273666e-03</td>
      <td>0.004327</td>
      <td>0.001135</td>
      <td>0.000022</td>
    </tr>
    <tr>
      <th>v3</th>
      <td>4.966510e-11</td>
      <td>4.679524e-09</td>
      <td>2.818739e-07</td>
      <td>0.000004</td>
      <td>0.000026</td>
      <td>0.000032</td>
    </tr>
  </tbody>
</table>
</div>

#### Filtering for specific species and fluxes
Because concentration and flux `MassSolution` objects are specialized dictionaries, they can be handled like any other dictionary. Therefore, obtaining the solution for individual species and fluxes can be done easily by using the `MassMetabolite` or `MassReaction` identifiers as keys.

```python
print(x1.id, conc_sol[x1.id])
```

    x1 <scipy.interpolate.interpolate.interp1d object at 0x7fedf092c6d0>

```python
for flux in [v1, v2]:
    print(flux.id, flux_sol[flux.id])
```

    v1 <scipy.interpolate.interpolate.interp1d object at 0x7fedf092c8b0>
    v2 <scipy.interpolate.interpolate.interp1d object at 0x7fedf092c900>

#### Switching between numerical arrays and interpolating functions
Suppose that instead of working with interpolating functions, we would rather work with the original time points and the corresponding solutions utilized by the ODE solver. One way this can be done would be to access the original time point values stored in the Solution object, and use those in the interpolating function:

```python
time_points = conc_sol.t
# Get a slice of the first 50 points
print(conc_sol["x1"](time_points)[:50])
```

    [1.         1.         0.99999943 0.99999734 0.99999525 0.99999316
     0.99998821 0.99997787 0.99995537 0.99990413 0.99978077 0.99942553
     0.99907055 0.99871581 0.99806461 0.99741426 0.99676475 0.9961161
     0.9948862  0.99290131 0.98987466 0.9868666  0.983877   0.98090577
     0.97795277 0.97334374 0.96877916 0.96425858 0.95727034 0.94370569
     0.92186543 0.90109967 0.88135539 0.86258222 0.84473223 0.82775987
     0.81162185 0.78756746 0.76536568 0.7448733  0.72595816 0.70849829
     0.69238106 0.67750262 0.66376717 0.64105858 0.62146897 0.6045664
     0.58997873 0.57738534]

To quickly convert an entire `MassSolution` object from interpolating functions to numerical arrays or vice-versa, we use the `MassSolution.interpolate` setter method:

```python
conc_sol.interpolate = False
# Get a slice of the first 50 points
conc_sol["x1"][:50]
```

    array([1.        , 1.        , 0.99999943, 0.99999734, 0.99999525,
           0.99999316, 0.99998821, 0.99997787, 0.99995537, 0.99990413,
           0.99978077, 0.99942553, 0.99907055, 0.99871581, 0.99806461,
           0.99741426, 0.99676475, 0.9961161 , 0.9948862 , 0.99290131,
           0.98987466, 0.9868666 , 0.983877  , 0.98090577, 0.97795277,
           0.97334374, 0.96877916, 0.96425858, 0.95727034, 0.94370569,
           0.92186543, 0.90109967, 0.88135539, 0.86258222, 0.84473223,
           0.82775987, 0.81162185, 0.78756746, 0.76536568, 0.7448733 ,
           0.72595816, 0.70849829, 0.69238106, 0.67750262, 0.66376717,
           0.64105858, 0.62146897, 0.6045664 , 0.58997873, 0.57738534])

```python
conc_sol.interpolate = True
conc_sol["x1"]
```

    <scipy.interpolate.interpolate.interp1d at 0x7fedf09422c0>

```python
for key, value in conc_sol.items():
    print(key, value)
```

    x1 <scipy.interpolate.interpolate.interp1d object at 0x7fedf09422c0>
    x2 <scipy.interpolate.interpolate.interp1d object at 0x7fedf093c900>
    x3 <scipy.interpolate.interpolate.interp1d object at 0x7fedf0942590>
    x4 <scipy.interpolate.interpolate.interp1d object at 0x7fedf0942540>

```python
conc_sol["x1"]
```

    <scipy.interpolate.interpolate.interp1d at 0x7fedf09422c0>

```python
conc_sol.x1
```

    <scipy.interpolate.interpolate.interp1d at 0x7fedf09422c0>

### Visualizing the Solution Graphically
Once the model has been simulated, the solutions can be visualized using the visualization tools in **MASSpy**.

```python
import matplotlib.pyplot as plt
import numpy as np

from mass.visualization import (
    plot_phase_portrait, plot_time_profile, plot_tiled_phase_portraits)
```

All visualization tools utilize the matplotlib python package. See documentation for the visualization class for more details on the available plotting kwargs.

#### Draw time course
Plotting the dynamic responses is straightforward using the `plot_time_profile`  function:

```python
plot_time_profile(conc_sol);
```

For this model and simulation, plotting on a linear scale does not provide us information about the dyanmics at various time scales. Therefore, we can use the `plot_function` kwarg to change the scale. Let us keep a linear scale on the y-axis, but change the x-axis to a logarithmic scale.

```python
plot_time_profile(conc_sol, plot_function="semilogx");
```

The `observable` argument allows one to specify particular solutions from the solution profile to observe while filtering out all other solutions. For example, only the solutions for $x_1$ and $x_2$ can be observed by setting observable to an list of these two keys in the solution profile.

```python
plot_time_profile(conc_sol, observable=["x1", "x2"],
                  plot_function="semilogx");
```

Though the dynamic behavior is clear, the above plots do not provide any other information. Let us add axes labels, a title, and a legend to the plot.

```python
plot_time_profile(
    conc_sol, legend="right outside", plot_function="semilogx",
    xlabel="Time", ylabel="Concentration",
    title=("Concentration Solutions", {"size": "large"}));
```

#### Draw phase portraits
Plotting the dynamic responses against one another is also straightforward by using the `plot_phase_portrait` function:

```python
plot_phase_portrait(conc_sol, x="x1", y="x2",
                    xlabel="x1", ylabel="x2");
```

$x_1$ vs $x_2$: note that you can use the `annotate_time_points` argument to highlight particular time points of interest. This argument can be utilized either by providing iterable of time points of interest. The `annotate_time_points_color` can be used to set the color of the time points. To use color to distinguish time points, the number of colors should equal the number of time points specified.

```python
plot_phase_portrait(
    conc_sol, x="x1", y="x2", xlabel="x1", ylabel="x2",
    annotate_time_points=[t0, 1e-1, 1e0, 1e1, 1e3, tf],
    annotate_time_points_color= [
        "red", "green", "purple", "yellow", "cyan", "blue"],
    annotate_time_points_legend="lower outside");
```

All pairwise phase portraits can be generated and viewed at once in a tiled format using the `plot_tiled_phase_portrait` function:

```python
plot_tiled_phase_portraits(conc_sol,
                           annotate_time_points_legend="right outside");
```

This method is particularly useful for looking at correlations at various time scales. For example, looking at the overall behavior, a fast time timescale of (0, 1), an intermediate timescale of (3, 100), and a slow timescale of (300, 10000), we can generate the following:

```python
correlations = [
    np.empty((3, 3)).astype(str),
    np.empty((3, 3)).astype(str),
    np.empty((3, 3)).astype(str),
    np.empty((3, 3)).astype(str)]
for entry in correlations:
    entry.fill("")

fmt_str = "{0:.2f}\n{1:.2f}"
correlations[1][0, 1] = fmt_str.format(*[1, 1])
correlations[2][0, 1] = fmt_str.format(*[1, 1.02])
correlations[2][0, 2] = fmt_str.format(*[1, 0.51])
correlations[2][1, 2] = fmt_str.format(*[1, 0.5])
correlations[3][0, 1] = fmt_str.format(*[1, 1.02])
correlations[3][0, 2] = fmt_str.format(*[1, 1.02])
correlations[3][1, 2] = fmt_str.format(*[1, 1.02])

fig, axes = plt.subplots(2, 2, figsize=(10, 10))
axes = axes.flatten()

times = [(0, 400000), (0, 1), (3, 100), (300, 10000)]
titles = ["{0}\nt0={1}; tf={2}".format(label, *time)
         for label, time in zip(["(a)", "(b)", "(c)", "(d)"], times)]

for i, ax in enumerate(axes.flatten()):
    plot_tiled_phase_portraits(
        conc_sol, observable=["x1", "x2", "x3"], ax=ax,
        plot_tile_placement="lower", additional_data=correlations[i],
        time_vector=np.linspace(*times[i], int(1e6)),
        tile_xlabel_fontdict={"size": "large"},
        tile_ylabel_fontdict={"size": "large"},
        title=titles[i])
```

### Post process the solution
#### Analyze pool behavior
In order to analyze the behavior of pools, pools can be created using the `MassSolution.make_aggregate_solution` method using the string representation of the pooling formulas. Additional parameters can also be incorporated into the pool formulation using a dictionary input for the `parameters` argument.

```python
pools = ["x1 - x2", "x1 + x2 - 2*x3", "x1 + x2 + x3"]

for i, equation_str in enumerate(pools):
    pool_id = "p" + str(i + 1)
    conc_sol.make_aggregate_solution(
        pool_id, equation=equation_str, update=True)
    print(pool_id, conc_sol[pool_id])
```

    p1 <scipy.interpolate.interpolate.interp1d object at 0x7fee51042720>
    p2 <scipy.interpolate.interpolate.interp1d object at 0x7fee205d9bd0>
    p3 <scipy.interpolate.interpolate.interp1d object at 0x7fed90538a40>

This method utilizes the solutions for the individual metabolites over the time range input, and then creates new solutions to represent the behavior of those pools.

```python
plot_time_profile(
    conc_sol, observable=["p1", "p2", "p3"], legend="best",
    plot_function="semilogx",
    xlabel="time",  ylabel="Concentrations",
    title=("Pool profile", {"size": "large"}));
```

#### Compute and plot the fluxes
A similar process as above can be utilized to obtain behavior of the net flux through a group of reactions. Note that the `MassSolution.make_aggregate_solution` method relies on the `sympy.sympify` function and can therefore utilize specific methods, such as the absolute value function, in the string as well.

```python
flux_sol.make_aggregate_solution(
    "v_net", equation='Abs(v1) + Abs(v2) + Abs(v3)', update=True)
```

    {'v_net': <scipy.interpolate.interpolate.interp1d at 0x7fed80ea7db0>}

Again, this method obtains the solutions for the individual fluxes over the time range input, and then creates new solutions to represent the behavior of various flux combinations.

```python
plot_time_profile(
    flux_sol, observable=["v_net"], legend="best",
    plot_function="semilogx",  xlabel="time", ylabel="Fluxes",
    title=("Net Flux", {"size": "large"}));
```

#### Plot phase portraits of pools

```python
fig, ax = plt.subplots(1, 1, figsize=(5, 5))
plot_tiled_phase_portraits(
    conc_sol, observable=["p1", "p2", "p3"], ax=ax,
    plot_tile_placement="lower",
    annotate_time_points_legend="right outside");
```

Here, we can see that all of the defined pools are dynamically independent of one another.

## Summary

* Network dynamics are described by dynamic mass balances $(d\textbf{x}/dt = \textbf{Sv}(\textbf{x}; \textbf{k}))$ that are formulated after applying a series of simplifying assumptions

* To simulate the dynamic mass balances we have to specify the numerical values of the kinetic constants $(\textbf{k})$, the initial conditions $(\textbf{x}_0)$, and any fixed boundary fluxes.

* The equations with the initial conditions can be integrated numerically.

* The solution contains numerical values for the concentration variables at discrete time points. The solution is graphically displayed as concentrations over time, or in a phase portrait.

* The solution can be post-processed following its initial analysis to bring out special dynamic features of the network. Such features will be described in more detail in the subsequent notebooks.

$\tiny{\text{© B. Ø. Palsson 2011;}\
\text{This publication is in copyright.}\\
\text{Subject to statutory exception and to the provisions of relevant collective licensing agreements,}\\ \text{no reproduction of any part may take place without the written permission of Cambridge University Press.}}$


# Week 04: Bottom-Up Parsing I & II

## Predictive Parser: LL(1)

### Predictive Parsing

- **Concept**
  - Look at the next? tokens.
  - No backtracking.
  - Accept LL(K) grammars. (Left-to-Right, Leftmost derivation, k tokens)
  - At each step, only one choice.
  - To avoid ambiguousness, need to **left-factor** the grammar.
- **Parsing Table**
  - **Leftmode Non-terminal** x **Next Input Token**
  - Use stack to record frontier of parse tree
- **Differnce** from Recursive Descent
  - For the leftmost non-terminal **S**
  - Look at the next input token **a**
  - Choose the production shown at **[S,a]**
- **Reject** on reaching error state
- **Accept** on (end of input && empty stack)

### First Set

**Def**. (First Set)

$$First(X)=\{t\ |\ X\rightarrow^*t\alpha\}\cup\{\epsilon\ |\ X\rightarrow^*\epsilon\}$$

**Algo**

1. $First(X)=\{t\}, if\ t\ is\ a\ terminal.$
2. $\epsilon \in First(X), if
\begin{cases}
X\rightarrow \epsilon \\
X\rightarrow A_1A_2\dots A_n, and\ \epsilon \in First(A_i), \forall 1\leq i\leq n
\end{cases}$
3. $First(\alpha) \subseteq First(X), if\ X\rightarrow A_1A_2\dots A_n\alpha, and\ \epsilon \in First(A_i), \forall 1\leq i\leq n$

### Follow Set

**Def**. (Follow Set)

$$Follow(X)=\{t\ |\ S\rightarrow^*\beta Xt\delta\}$$

**Algo**

1. $\$ \in Follow(S)$
2. $First(\beta)-\{\epsilon\}\subseteq Follow(X), for\ each\ A\rightarrow\alpha X\beta$
3. $Follow(A) \subseteq Follow(X), for\ each A\rightarrow\alpha X\beta\ where\ \epsilon\in First(\beta)$

### How to Construct LL(1) Parsing Tables

- Construct a parsing table **T** for CFG **G**
- For each production $A \rightarrow \alpha$ in G do:
  - For each terminal $t \in First(\alpha)$, $T[A, t] = \alpha$
  - If $\epsilon \in First(\alpha),$, for each $t \in Follow(A)$, $T[A,t] = \alpha$
  - If $\epsilon \in First(\alpha)$ and $ \$ \in Follow(A)$, $T[A,\$] = \alpha$
- If any entry is multiply defined, then G is not LL(1).

## Bottom-Up Parsing

### Shift-Reduce Parsing

- Concept
  - Don't need **left-factored** grammars.
  - Reduce the string to the start symbol. (Inverting production)
  - A bottom-up parser traces a rightmost derivation in reverse. $$
\begin{align}
& int*int + int \\
& \textbf{int*T} + int \\
& \textbf{T} + int \\
& T + \textbf{T} \\
& T + \textbf{E} \\
& \textbf{E}
\end{align}
$$
  - Thm.
    - In some step, let string as $\alpha \beta \gamma$.
    - Assume the next reduction is by $X \rightarrow \beta$. ($\alpha \beta \gamma \rightarrow \alpha X \gamma$)
    - Then $\gamma$ is a string which contains only terminals.
  - Split the string as $L_{Str} | R_{Str}$.
  - $L_{Str}$ contains non-terminals and terminals. $R_{Str}$ contains only terminals.
- Actions
  - **Shift**: Move | one place to the right. Shift a terminal to the left string.
  - **Reduce**: Apply an inverse production at the right of $L_{str}$. $$
for\ A \rightarrow xy, Cbxy|ijk \Rightarrow CbA|ijk $$
- Implementation
  - Use a stack to maintain $L_{Str}$.
- If is's legal to shift or reduce, there is a **shift-reduce** conflict.
- If is's legal to reduce by two productions, there is a **reduce-reduce** conflict.

### Term. (Handle)

- **Scenario**
  - Grammar
      - $E \rightarrow T+E|E$
      - $T \rightarrow int*T|int|(E)$
  - At the steop $int|*int+int$
  - If we reduce by ($T \rightarrow int$) given ($T | *int+int$), it will fail.
- **Target**
    - Want to reduce only if the result can still be reduced to the start symbol.
- A **handle** is the rhs of the production that you can trace back in the parse tree.
    - $\Rightarrow$ Handles are the **children of some internal node** of some AST.
- Assume a rightmost derivation $$S \rightarrow^* \alpha X \omega \rightarrow \alpha\beta\omega$$
  - Then $\beta$ is a **handle** of $\alpha \beta \omega$.
- Thm.
  - In shift-reduce parsing, handles appear only **at the top of the stack**, never inside.
- Bottom-up parsing algorithms are based on recognizing handles

### Recognizing Handles

- There are no known efficient algo to recognize handles. But there are good heuristics.
- **Viable Prefix**: $\alpha_1\dots\alpha_n$ is a **viable prefix** if there is an $\omega$ s.t. $\alpha_1\dots\alpha_n\ |\ \omega$ is a state of a shift-reduce parser.
  - A viable prefix is always the prefix of some handle.
  - If we can maintain the stack's contents are viable prefixes all the time, no parsing error will occur.
- Thm. For any grammar, the set of viable prefixes is a **regular language**.
- **Item**: An **item** is a production with '.' somewhere on the rhs.
  - Example. The items for the production rule $T \rightarrow (E)$ are
    - $T \rightarrow .(E)$
    - $T \rightarrow (.E)$
    - $T \rightarrow (E.)$
    - $T \rightarrow (E).$
  - Example. The items for ($X \rightarrow \epsilon$) are $X \rightarrow .$
  - Items are often called **LR(0) items**.
- Target: To describe the states of the production rule.
  - If we need to use some production rule $R$ to reduce, the top of the stack will be the left string split by '.' on some item of $R$.
  - Example.
    - Input: $(int)$
    - Grammar
        - $E \rightarrow T+E\ |\ T$
        - $T \rightarrow int*T\ |\ int\ |\ (E)$
    - $(E$ is the left string split by '.' on the item $T \rightarrow (E.)$

### Recognizing Viable Prefixes

1. Add a dummy production $S' \rightarrow S$ to $G$
2. The NFA states are the items of $G$
3. For item $E \rightarrow \alpha .X\beta$ add transition: $(E \rightarrow \alpha . X \beta) \rightarrow^X (E \rightarrow \alpha X.\beta)$
4. For item $E \rightarrow \alpha .X\beta$ and production $X \rightarrow \gamma$, add transition: $(E \rightarrow \alpha . X \beta) \rightarrow^\epsilon (X \rightarrow .\gamma)$
5. Every state is an accepting state
6. Start state is $S' \rightarrow .S$

### Valid Items

- Concept
  - Item $X \rightarrow \beta .\gamma$ is **valid** for a viable prefix $\alpha\beta$ if $$S' \rightarrow^* \alpha X\omega \rightarrow \alpha\beta\gamma\omega$$ by a right-most derivation.
  - An item $I$ is valid for a viable prefix $\alpha(\alpha_1\dots\alpha_n)$ if the DFA accepts $\alpha_1\dots\alpha_n$ and terminates on some state $s$ containing $I$.
  - The items in $s$ describe what the top of the item stack might be after reading input $\alpha_1\dots\alpha_n$.

### LR(0) Parsing

- **Assume**
  - Stack contains $\alpha_1\dots\alpha_k$
  - Next input is $t$
  - DFA on input $\alpha_1\dots\alpha_k$ teminates in state $s$
- **Action**
  - Reduce by $X \rightarrow \beta$ if $s$ contains item $X \rightarrow \beta.$
  - Shift if $s$ contains item $X \rightarrow \beta .t\omega$
- **Conflict**
  - **Reduce/Reduce** if any state contains two reduce rule.
  - **Shift/Reduce** if any state has a reduced item and a shift item

### Simple LR Paring (SLR(1) Parsing)

- **Assume**
  - Stack contains $\alpha_1\dots\alpha_k$
  - Next input is $t$
  - DFA on input $\alpha_1\dots\alpha_k$ teminates in state $s$
- **Action**
  - Reduce by $X \rightarrow \beta$ if $s$ contains item $X \rightarrow \beta.$ and $t \in Follow(X)$
  - Shift if $s$ contains item $X \rightarrow \beta .t\omega$
- If there are conflicts under these rules, the grammar is not SLR.
- We can use **precedence declarations** to resolve conflicts. \* is grater than \+.

### LR(1)

- More powerful than SLR(1)
- Build by (item x lookahead).
- SLR(1) only checks whether lookahead is in follow set.

```python

```


- To navigate in this slideshow, the spacebar moves to the next slide and shift+spacebar moves to the previous slide.
- If the slides do not fit in your screen, reduce the zoom in your browser until you can see them correctly.

    The “Basic Reproduction Number” and Modeling the Spread of an Epidemic

~~~or~~~

    How to stop the spread of Covid19 using linear ordinary differential equations

(at least, in a mathematical model…)

Basic Reproduction Number — Modeling the Spread of an Epidemic.  Peter Thomas. CWRU. March 5, 2020.

“Basic Reproduction Number” and Modeling the Spread of an Epidemic

Definition: The basic reproduction number (R0) of a disease is the average number of secondary infections produced through directly direct contact with a single infected individual, in a fully susceptible population.

R0 depends on the transmission mechanism, the duration of the contagious period of an infected person, and properties of the susceptible population (hygiene, detection capabilities, quarantine policies, etc.)

- If R0>1, the epidemic spreads.
- If R0<1, the epidemic dies out.

Source: https://en.wikipedia.org/wiki/Basic_reproduction_number

Values of R0 of well-known infectious diseases

Disease
Transmission
R0

Measles
Airborne
12–18

Diphtheria
Saliva
6–7

Smallpox
Airborne droplet
5–7

Polio
Fecal–oral route
5–7

Rubella
Airborne droplet
5–7

Mumps
Airborne droplet
4–7

Pertussis
Airborne droplet
5.5

HIV
Sexual contact
2–5

Values of R0 of well-known infectious diseases

Disease
Transmission
R0

SARS
Airborne droplet
2–5

COVID-19
Airborne droplet
1.4–3.8

Influenza
(1918 pandemic strain)
Airborne droplet
2–3

Ebola
(2014 Ebola outbreak)
Body fluids
1.5–2.5

MERS
Airborne droplet
0.3-0.8

See also: Lipsitch, Marc, et al. "Transmission dynamics and control of severe acute respiratory syndrome." Science 300.5627 (2003): 1966-1970.

### BRN for a basic Susceptible-Infected-Recovered model

$S$ = Susceptible,   $I$ = Infected,   $R=1-(S+I)$ = Recovered

\begin{align*}
\frac{dS}{dt} &= -\beta SI \\
\frac{dI}{dt} &= \beta SI -\gamma I
\end{align*}

$\beta$: Rate of new infection per infected idividual, per day.

$\gamma$: Rate of recovery of infected individual, per day.

__Question:__ Given the BRN for a basic Susceptible-Infected-Recovered model,

\begin{align*}
\frac{dS}{dt} &= -\beta SI \\
\frac{dI}{dt} &= \beta SI -\gamma I
\end{align*}

What is the basic reproduction number $R_0$ for this model?

__Answer:__ $R_0 = \beta/\gamma$.

Recall $S+I+R\equiv 1$. For a total population of size $N\gg 1$, starting with $S(0)=(N-1)/N$ and $I(0)=1/N$, the initial growth rate is $\beta-\gamma$, and $I(t)\approx \frac{1}{N}e^{(\beta-\gamma)t}$ for small $t$. If $\beta>\gamma$, then $R_0>1$ and $I(t)$ increases.

Other Epidemic Models

I0 = infected but undiagnosed / asymptomatic (latent)

I1 = infected and diagnosed / with symptoms

D = deceased

To find the initial growth rate $\lambda$, consider the system of two linear equations
\begin{align}
\frac{dI_0}{dt}&=\beta_0I_0+\beta_1I_1-\gamma_0I_0-\delta I_0\\
\frac{dI_1}{dt}&=\delta I_0 -\gamma_1I_1-\mu I_1.
\end{align}
Equivalently: $\frac{d\mathbf{Y}}{dt}=A\mathbf{Y}$, with
$A=\begin{pmatrix}\beta_0-\gamma_0-\delta & \beta_1 \\ \delta & -\gamma_1-\mu \end{pmatrix}$ and $\mathbf{Y}=\begin{pmatrix}I_0 \\ I_1\end{pmatrix}$.

### Your assignment
$\frac{d\mathbf{Y}}{dt}=A\mathbf{Y}$, with
$A=\begin{pmatrix}\beta_0-\gamma_0-\delta & \beta_1 \\ \delta & -\gamma_1-\mu \end{pmatrix}$ and $\mathbf{Y}=\begin{pmatrix}I_0 \\ I_1\end{pmatrix}$.
Suppose the matrix $A$ has two real eigenvalues.  The larger is $\lambda$.  If $\lambda>0$, the epidemic spreads.   If $\lambda<0$, it dies out.  Your assignment:

1. Find $\lambda$ for the parameters below.  For these parameters, does the epidemic spread or not?  (See next slide.)
2. The parameter $\beta_1$ could be reduced through public policy such as hygiene and quarantine practices.  If $\lambda>0$ with the original parameters, how much must you reduce $\beta_1$
to control the epidemic?

Equations: $\frac{d\mathbf{Y}}{dt}=A\mathbf{Y}$, with
$A=\left(\begin{matrix}\beta_0-\gamma_0-\delta & \beta_1 \\ \delta & -\gamma_1-\mu \end{matrix}\right)$ and $\mathbf{Y}=\left(\begin{matrix}I_0 \\ I_1\end{matrix}\right)$.

| Parameter  | Value | Interpretation                                        |
| :---------:| :----:| :-----------------------------------------------------|
| $\beta_0$  | 0.2   | Coefficient of infectivity from state $I_0$ (per day) |
| $\beta_1$  | 0.4   | Coefficient of infectivity from state $I_1$ (per day) |
| $\gamma_0$ | 0.2   | Recovery rate from state $I_0$ (per day)              |
| $\gamma_0$ | 0.1   | Recovery rate from state $I_1$ (per day)              |
| $\delta$   | 0.2   | Transition rate from $I_0$ to $I_1$ (per day)         |
| $\mu$      | 0.03  | Mortality rate (per day)                              |

*Nota bene:* Prof.~Thomas made up these parameter values for purposes of illustration only, so take them with a grain of salt.

# Your mission, should you accept it...

Question 1

Find the eigenvalues of the matrix $A=\begin{pmatrix} -0.2 & 0.4 \\  0.2 & -0.13 \end{pmatrix}$

Click on the cell below and run it using SHIFT+Enter/Return

The cell will create the matrix $A$ and it will store its eigenvectors and eigenvalues.

```octave
# Definition of the parameters:

b0=.2; % coefficient of infectivity from state I0 (per day)
b1=.4; % coefficient of infectivity from state I1 (per day)
g0=.2; % recovery rate from state I0 (per day)
g1=.1; % recovery rate from state I1 (per day)
delta=.2; % transition rate from I0 to I1 (per day)
mu=.03; % mortality rate (per day)

# Construction of the matrix
A=[b0-g0-delta,b1;
    delta,-g1-mu];

# Eigenvectors and Eigenvalues
[V,dd]=eig(A);
```

Question 1 (continued)

Run the cell below to see the eigenvalues.

```octave
diag(dd)
```

Run the cell below to see the eigenvectors (in matrix form).

```octave
V
```

Question 1 (continued)

Follow up questions:
1. What does the eigenvector that goes with the largest eigenvalue tell you?

2. As of March 5, 2020, there have been 95,000 cases of Covid19 since the first cases appeared in early December 2019, about 90 days ago. If the infecton where still in the linear growth phase, how large would you expect $\lambda$ to be (in units of days$^{-1})$?

Question 2

 (Numerically) find the eigenvalues of $A=\begin{pmatrix} -0.2 & \beta_1 \\  0.2 & -0.13 \end{pmatrix}$

How small must $\beta_1$ be to stop the epidemic?

In the next cell you will find a snippet of code that will plot the real part of the eigenvalues depending on the parameter $\beta_1$.

If you cannot see the plot after running the cell, zoom out in your browser.

```octave
b1min=0.1; % adjust to taste.  Try +0.1 first.
b1max=.4;  % adjust to taste.  Try +0.4 first.
for b1=linspace(b1min,b1max,201)
A=[b0-g0-delta,b1;
    delta,-g1-mu];
dd=eigs(A);
plot([b1,b1],real(dd),'o','MarkerSize',10)
hold on
end
set(line([b1min b1max],[0 0]),'Color','k','LineWidth',10)
set(gca,'FontSize',20)
xlabel('Parameter \beta_1')
title('Growth Rate of Epidemic')
ylabel('Real(\lambda_\pm)')
grid on
xlim([b1min,b1max])
```

In this SLIR model, reducing $\beta_1$ by a factor of four, stops the epidemic.

The characteristic polynomial $P(\lambda) = \text{det}(A-\lambda I)$ always has two roots, even for non-physical parameter values ($\beta_1<0$). However, sometimes the roots form a complex conjugate pair, so here we plot the real part of $\lambda_{\pm}$.

## Question 3
For the SIR model, $R_0>1$ if and only if the growth rate $\beta-\gamma>0$.
Calculate $R_0$ for the SLIR model, and investigate whether $R_0>1$ iff $\lambda_+>0$ for this model as well.

# THE END

- Original Presentation: Professor Peter J. Thomas (CWRU)
- Adaptation to Octave and Binder Creation: Dr. Daniel Balague Guardia (CWRU)

## What now?

- You can go back to the previous slides with Octave code and modify them with different paramters.
- Play with the models and the code
- Please share/copy/modify!