```python
from preamble import *
%matplotlib notebook
import matplotlib as mpl
mpl.rcParams['legend.numpoints'] = 1
```

## Evaluation Metrics and scoring

### Metrics for binary classification

```python
from sklearn.model_selection import train_test_split

data = pd.read_csv("data/bank-campaign.csv")
X = data.drop("target", axis=1).values
y = data.target.values

X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)

```

```python
from sklearn.dummy import DummyClassifier
dummy_majority = DummyClassifier(strategy='most_frequent').fit(X_train, y_train)
pred_most_frequent = dummy_majority.predict(X_test)
print("predicted labels: %s" % np.unique(pred_most_frequent))
print("score: %f" % dummy_majority.score(X_test, y_test))
```

    predicted labels: ['no']
    score: 0.887540

```python
from sklearn.tree import DecisionTreeClassifier
tree = DecisionTreeClassifier(max_depth=2).fit(X_train, y_train)
pred_tree = tree.predict(X_test)
tree.score(X_test, y_test)
```

    0.90278721957851804

```python
from sklearn.linear_model import LogisticRegression

dummy = DummyClassifier().fit(X_train, y_train)
pred_dummy = dummy.predict(X_test)
print("dummy score: %f" % dummy.score(X_test, y_test))

logreg = LogisticRegression(C=0.1).fit(X_train, y_train)
pred_logreg = logreg.predict(X_test)
print("logreg score: %f" % logreg.score(X_test, y_test))
```

    dummy score: 0.803729
    logreg score: 0.912013

#### Confusion matrices

```python
from sklearn.metrics import confusion_matrix

confusion = confusion_matrix(y_test, pred_logreg)
print(confusion)
```

    [[8911  228]
     [ 678  480]]

```python
mglearn.plots.plot_binary_confusion_matrix()
```

    <IPython.core.display.Javascript object>

```python
print("Most frequent class:")
print(confusion_matrix(y_test, pred_most_frequent))
print("\nDummy model:")
print(confusion_matrix(y_test, pred_dummy))
print("\nDecision tree:")
print(confusion_matrix(y_test, pred_tree))
print("\nLogistic Regression")
print(confusion_matrix(y_test, pred_logreg))
```

    Most frequent class:
    [[9139    0]
     [1158    0]]

    Dummy model:
    [[8111 1028]
     [1024  134]]

    Decision tree:
    [[8809  330]
     [ 671  487]]

    Logistic Regression
    [[8911  228]
     [ 678  480]]

##### Relation to accuracy
\begin{equation}
\text{Accuracy} = \frac{\text{TP} + \text{TN}}{\text{TP} + \text{TN} + \text{FP} + \text{FN}}
\end{equation}

#### Precision, recall and f-score
\begin{equation}
\text{Precision} = \frac{\text{TP}}{\text{TP} + \text{FP}}
\end{equation}

\begin{equation}
\text{Recall} = \frac{\text{TP}}{\text{TP} + \text{FN}}
\end{equation}
\begin{equation}
\text{F} = 2 \cdot \frac{\text{precision} \cdot \text{recall}}{\text{precision} + \text{recall}}
\end{equation}

```python
from sklearn.metrics import f1_score
print("f1 score most frequent: %.2f" % f1_score(y_test, pred_most_frequent, pos_label="yes"))
print("f1 score dummy: %.2f" % f1_score(y_test, pred_dummy, pos_label="yes"))
print("f1 score tree: %.2f" % f1_score(y_test, pred_tree, pos_label="yes"))
print("f1 score: %.2f" % f1_score(y_test, pred_logreg, pos_label="yes"))
```

    f1 score most frequent: 0.00
    f1 score dummy: 0.12
    f1 score tree: 0.49
    f1 score: 0.51

    /home/andy/checkout/scikit-learn/sklearn/metrics/classification.py:1117: UndefinedMetricWarning: F-score is ill-defined and being set to 0.0 due to no predicted samples.
      'precision', 'predicted', average, warn_for)

```python
from sklearn.metrics import classification_report
print(classification_report(y_test, pred_most_frequent,
                            target_names=["no", "yes"]))
```

                 precision    recall  f1-score   support

             no       0.89      1.00      0.94      9139
            yes       0.00      0.00      0.00      1158

    avg / total       0.79      0.89      0.83     10297

    /home/andy/checkout/scikit-learn/sklearn/metrics/classification.py:1117: UndefinedMetricWarning: Precision and F-score are ill-defined and being set to 0.0 in labels with no predicted samples.
      'precision', 'predicted', average, warn_for)

```python
print(classification_report(y_test, pred_tree,
                            target_names=["no", "yes"]))
```

                 precision    recall  f1-score   support

             no       0.93      0.96      0.95      9139
            yes       0.60      0.42      0.49      1158

    avg / total       0.89      0.90      0.90     10297

```python
print(classification_report(y_test, pred_logreg,
                            target_names=["no", "yes"]))
```

                 precision    recall  f1-score   support

             no       0.93      0.98      0.95      9139
            yes       0.68      0.41      0.51      1158

    avg / total       0.90      0.91      0.90     10297

# Taking uncertainty into account

```python
from mglearn.datasets import make_blobs
from sklearn.svm import SVC
X, y = make_blobs(n_samples=(400, 50), centers=2, cluster_std=[7.0, 2],
                  random_state=22)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)
svc = SVC(gamma=.05).fit(X_train, y_train)

```

```python
mglearn.plots.plot_decision_threshold()
```

    <IPython.core.display.Javascript object>

```python
print(classification_report(y_test, svc.predict(X_test)))
```

                 precision    recall  f1-score   support

              0       0.97      0.89      0.93       104
              1       0.35      0.67      0.46         9

    avg / total       0.92      0.88      0.89       113

```python
y_pred_lower_threshold = svc.decision_function(X_test) > -.8
```

```python
print(classification_report(y_test, y_pred_lower_threshold))
```

                 precision    recall  f1-score   support

              0       1.00      0.82      0.90       104
              1       0.32      1.00      0.49         9

    avg / total       0.95      0.83      0.87       113

## Precision-Recall curves and ROC curves

```python
from sklearn.metrics import precision_recall_curve
precision, recall, thresholds = precision_recall_curve(y_test,
                                                       svc.decision_function(X_test))
```

```python
# create a similar dataset as before, but with more samples to get a smoother curve
X, y = make_blobs(n_samples=(4000, 500), centers=2, cluster_std=[7.0, 2], random_state=22)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=0)

svc = SVC(gamma=.05).fit(X_train, y_train)

precision, recall, thresholds = precision_recall_curve(
    y_test, svc.decision_function(X_test))
# find threshold closest to zero:
close_zero = np.argmin(np.abs(thresholds))
plt.figure()
plt.plot(precision[close_zero], recall[close_zero], 'o', markersize=10,
         label="threshold zero", fillstyle="none", c='k', mew=2)

plt.plot(precision, recall, label="precision recall curve")
plt.xlabel("precision")
plt.ylabel("recall")
plt.title("precision_recall_curve");
plt.legend(loc="best")
```

    <IPython.core.display.Javascript object>

    <matplotlib.legend.Legend at 0x7fe27528b940>

```python
from sklearn.ensemble import RandomForestClassifier

rf = RandomForestClassifier(n_estimators=100, random_state=0, max_features=2)
rf.fit(X_train, y_train)

# RandomForestClassifier has predict_proba, but not decision_function
precision_rf, recall_rf, thresholds_rf = precision_recall_curve(
    y_test, rf.predict_proba(X_test)[:, 1])
plt.figure()

plt.plot(precision, recall, label="svc")

plt.plot(precision[close_zero], recall[close_zero], 'o', markersize=10,
         label="threshold zero svc", fillstyle="none", c='k', mew=2)

plt.plot(precision_rf, recall_rf, label="rf")

close_default_rf = np.argmin(np.abs(thresholds_rf - 0.5))
plt.plot(precision_rf[close_default_rf], recall_rf[close_default_rf], '^', markersize=10,
         label="threshold 0.5 rf", fillstyle="none", c='k', mew=2)
plt.xlabel("precision")
plt.ylabel("recall")
plt.legend(loc="best")
plt.title("precision_recall_comparison");
```

    <IPython.core.display.Javascript object>

```python
print("f1_score of random forest: %f" % f1_score(y_test, rf.predict(X_test)))
print("f1_score of svc: %f" % f1_score(y_test, svc.predict(X_test)))
```

    f1_score of random forest: 0.609756
    f1_score of svc: 0.655870

```python
from sklearn.metrics import average_precision_score
ap_rf = average_precision_score(y_test, rf.predict_proba(X_test)[:, 1])
ap_svc = average_precision_score(y_test, svc.decision_function(X_test))
print("average precision of random forest: %f" % ap_rf)
print("average precision of svc: %f" % ap_svc)
```

    average precision of random forest: 0.665737
    average precision of svc: 0.662636

#### Receiver Operating Characteristics (ROC) and AUC
\begin{equation}
\text{FPR} = \frac{\text{FP}}{\text{FP} + \text{TN}}
\end{equation}

```python
from sklearn.metrics import roc_curve
fpr, tpr, thresholds = roc_curve(y_test, svc.decision_function(X_test))

plt.figure()

plt.plot(fpr, tpr, label="ROC Curve")
plt.xlabel("FPR")
plt.ylabel("TPR (recall)")
plt.title("roc_curve");
# find threshold closest to zero:
close_zero = np.argmin(np.abs(thresholds))
plt.plot(fpr[close_zero], tpr[close_zero], 'o', markersize=10,
         label="threshold zero", fillstyle="none", c='k', mew=2)
plt.legend(loc=4)
```

    <IPython.core.display.Javascript object>

    <matplotlib.legend.Legend at 0x7fe273a24be0>

```python
from sklearn.metrics import roc_curve
fpr_rf, tpr_rf, thresholds_rf = roc_curve(y_test, rf.predict_proba(X_test)[:, 1])

plt.figure()
plt.plot(fpr, tpr, label="ROC Curve SVC")
plt.plot(fpr_rf, tpr_rf, label="ROC Curve RF")

plt.xlabel("FPR")
plt.ylabel("TPR (recall)")
plt.title("roc_curve_comparison");
plt.plot(fpr[close_zero], tpr[close_zero], 'o', markersize=10,
         label="threshold zero SVC", fillstyle="none", c='k', mew=2)
close_default_rf = np.argmin(np.abs(thresholds_rf - 0.5))
plt.plot(fpr_rf[close_default_rf], tpr[close_default_rf], '^', markersize=10,
         label="threshold 0.5 RF", fillstyle="none", c='k', mew=2)

plt.legend(loc=4)
```

    <IPython.core.display.Javascript object>

    <matplotlib.legend.Legend at 0x7fe27399a5f8>

```python
from sklearn.metrics import roc_auc_score
rf_auc = roc_auc_score(y_test, rf.predict_proba(X_test)[:, 1])
svc_auc = roc_auc_score(y_test, svc.decision_function(X_test))
print("AUC for Random Forest: %f" % rf_auc)
print("AUC for SVC: %f" % svc_auc)
```

    AUC for Random Forest: 0.936695
    AUC for SVC: 0.916294

```python
X = data.drop("target", axis=1).values
y = data.target.values
X.shape
```

    (41188, 63)

```python
X_train, X_test, y_train, y_test = train_test_split(
    X, y, random_state=0, train_size=.1, test_size=.1)

plt.figure()

for gamma in [1, 0.01, 0.001]:
    svc = SVC(gamma=gamma).fit(X_train, y_train)
    accuracy = svc.score(X_test, y_test)
    auc = roc_auc_score(y_test == "yes", svc.decision_function(X_test))
    fpr, tpr, _ = roc_curve(y_test , svc.decision_function(X_test), pos_label="yes")
    print("gamma = %.03f  accuracy = %.02f  AUC = %.02f" % (gamma, accuracy, auc))
    plt.plot(fpr, tpr, label="gamma=%.03f" % gamma, linewidth=4)
plt.xlabel("FPR")
plt.ylabel("TPR")
plt.xlim(-0.01, 1)
plt.ylim(0, 1.02)
plt.legend(loc="best")
```

    <IPython.core.display.Javascript object>

    gamma = 1.000  accuracy = 0.89  AUC = 0.62
    gamma = 0.010  accuracy = 0.89  AUC = 0.89
    gamma = 0.001  accuracy = 0.91  AUC = 0.88

    <matplotlib.legend.Legend at 0x7ff697fe0208>

### Multi-class classification

```python
from sklearn.metrics import accuracy_score
from sklearn.datasets import load_digits
digits = load_digits()

X_train, X_test, y_train, y_test = train_test_split(
    digits.data, digits.target, random_state=0)
lr = LogisticRegression().fit(X_train, y_train)
pred = lr.predict(X_test)
print("accuracy: %0.3f" % accuracy_score(y_test, pred))
print("confusion matrix:")
print(confusion_matrix(y_test, pred))
```

    accuracy: 0.953
    confusion matrix:
    [[37  0  0  0  0  0  0  0  0  0]
     [ 0 39  0  0  0  0  2  0  2  0]
     [ 0  0 41  3  0  0  0  0  0  0]
     [ 0  0  1 43  0  0  0  0  0  1]
     [ 0  0  0  0 38  0  0  0  0  0]
     [ 0  1  0  0  0 47  0  0  0  0]
     [ 0  0  0  0  0  0 52  0  0  0]
     [ 0  1  0  1  1  0  0 45  0  0]
     [ 0  3  1  0  0  0  0  0 43  1]
     [ 0  0  0  1  0  1  0  0  1 44]]

```python
plt.figure()
scores_image = mglearn.tools.heatmap(confusion_matrix(y_test, pred), xlabel='Predicted label', ylabel='True label',
                                     xticklabels=digits.target_names, yticklabels=digits.target_names,
                                     cmap=plt.cm.gray_r, fmt="%d")

plt.title("Confusion matrix")
plt.gca().invert_yaxis()
```

    <IPython.core.display.Javascript object>

```python
print(classification_report(y_test, pred))
```

                 precision    recall  f1-score   support

              0       1.00      1.00      1.00        37
              1       0.89      0.91      0.90        43
              2       0.95      0.93      0.94        44
              3       0.90      0.96      0.92        45
              4       0.97      1.00      0.99        38
              5       0.98      0.98      0.98        48
              6       0.96      1.00      0.98        52
              7       1.00      0.94      0.97        48
              8       0.93      0.90      0.91        48
              9       0.96      0.94      0.95        47

    avg / total       0.95      0.95      0.95       450

```python
print("micro average f1 score: %f" % f1_score(y_test, pred, average="micro"))
print("macro average f1 score: %f" % f1_score(y_test, pred, average="macro"))
```

    micro average f1 score: 0.953333
    macro average f1 score: 0.954000

## Using evaluation metrics in model selection

```python
from sklearn.cross_validation import cross_val_score
# default scoring for classification is accuracy
print("default scoring ", cross_val_score(SVC(), X, y))
# providing scoring="accuracy" doesn't change the results
explicit_accuracy =  cross_val_score(SVC(), digits.data, digits.target == 9, scoring="accuracy")
print("explicit accuracy scoring ", explicit_accuracy)
roc_auc =  cross_val_score(SVC(), digits.data, digits.target == 9, scoring="roc_auc")
print("AUC scoring ", roc_auc)
```

    default scoring  [ 0.9  0.9  0.9]
    explicit accuracy scoring  [ 0.9  0.9  0.9]
    AUC scoring  [ 0.994  0.99   0.996]

```python
from sklearn.model_selection import GridSearchCV

# back to the bank campaign
X = data.drop("target", axis=1).values
y = data.target.values

X_train, X_test, y_train, y_test = train_test_split(
    X, y, train_size=.1, test_size=.1, random_state=0)

# we provide a somewhat bad grid to illustrate the point:
param_grid = {'gamma': [0.0001, 0.01, 0.1, 1, 10]}
# using the default scoring of accuracy:
grid = GridSearchCV(SVC(), param_grid=param_grid)
grid.fit(X_train, y_train)
print("Grid-Search with accuracy")
print("Best parameters:", grid.best_params_)
print("Best cross-validation score (accuracy)):", grid.best_score_)
print("Test set AUC: %.3f" % roc_auc_score(y_test, grid.decision_function(X_test)))
print("Test set accuracy %.3f: " % grid.score(X_test, y_test))

# using AUC scoring instead:
grid = GridSearchCV(SVC(), param_grid=param_grid, scoring="roc_auc")
grid.fit(X_train, y_train)
print("\nGrid-Search with AUC")
print("Best parameters:", grid.best_params_)
print("Best cross-validation score (AUC):", grid.best_score_)
print("Test set AUC: %.3f" % roc_auc_score(y_test, grid.decision_function(X_test)))
print("Test set accuracy %.3f: " % grid.score(X_test, y_test))

```

    Grid-Search with accuracy
    Best parameters: {'gamma': 0.0001}
    Best cross-validation score (accuracy)): 0.970304380104
    Test set AUC: 0.992
    Test set accuracy 0.973:

    Grid-Search with AUC
    Best parameters: {'gamma': 0.01}
    Best cross-validation score (AUC): 0.997467845028
    Test set AUC: 1.000
    Test set accuracy 1.000:

    /home/andy/anaconda3/lib/python3.5/site-packages/sklearn/grid_search.py:418: ChangedBehaviorWarning: The long-standing behavior to use the estimator's score function in GridSearchCV.score has changed. The scoring parameter is now used.
      ChangedBehaviorWarning)

```python
from sklearn.metrics.scorer import SCORERS
    print(sorted(SCORERS.keys()))
```

    ['accuracy', 'adjusted_rand_score', 'average_precision', 'f1', 'f1_macro', 'f1_micro', 'f1_samples', 'f1_weighted', 'log_loss', 'mean_absolute_error', 'mean_squared_error', 'median_absolute_error', 'precision', 'precision_macro', 'precision_micro', 'precision_samples', 'precision_weighted', 'r2', 'recall', 'recall_macro', 'recall_micro', 'recall_samples', 'recall_weighted', 'roc_auc']

```python
def my_scoring(fitted_estimator, X_test, y_test):
    return (fitted_estimator.predict(X_test) == y_test).mean()

GridSearchCV(SVC(), param_grid, scoring=my_scoring)
```

# Exercises
Load the adult dataset from ``data/adult.csv``, and split it into training and test set.
Apply grid-search to the training set, searching for the best C for Logistic Regression, also search over L1 penalty vs L2 penalty.
Plot the ROC curve of the best model on the test set.

```python
# get dummy variables, needed for scikit-learn models on categorical data:
X = pd.get_dummies(data.drop("income", axis=1))
y = data.income == " >50K"
```


# MODELING RUMORS: THE NO PLANE PENTAGON FRENCH HOAX CASE

A python based implementation of the technique described in the paper "[Modeling Rumors:The No Plane Pentagon French Hoax Case](https://arxiv.org/abs/cond-mat/0211571)" by [Serge Galam](mailto:galam@ccr.jussieu.fr) of CNRS, Paris.

**ABSTRACT :**
The recent astonishing wide adhesion of french people to the rumor claiming ‘No plane did crash on the Pentagon on September the 11”, is given a generic explanation in terms of a model of minority opinion spreading. Using a majority rule reaction-diffusion dynamics, a rumor is shown to invade for sure a social group provided it fulfills simultaneously two criteria. First it must initiate with a support beyond some critical threshold which however, turns out to be always very low. Then it has to be consistent with some larger collective social paradigm of the group. Othewise it just dies out. Both conditions were satisfied in the french case with the associated book sold at more than 200 000 copies in just a few days. The rumor was stopped by the firm stand of most newspaper editors stating it is nonsense. Such an incredible social dynamics is shown to result naturally from an open and free public debate among friends and colleagues. Each one searching for the truth sincerely on a free will basis and without individual biases. The polarization process appears also to be very quick in agreement with reality. It is a very strong anti-democratic reversal of opinion although made quite democratically. The model may apply to a large range of rumors.

```python
import numpy as np
from sympy.solvers import solve
from sympy import Symbol
import math
import matplotlib.pyplot as plt
```

```python
def nCr(n,r):
    f = math.factorial
    return f(n) // (f(r)*f(n-r))
```

## Some Important Variables

In this section we se tup some important variables which are required for the running of the code.
*MaxGroupSize*- Defines the biggest group size a person can interact with (IMHO should not exceed the group of 7 people in small social gatherings).
*maxDays*- The no. of days you want to study the evolution of people's believe
*BelieveTruth* - The ratio of people believing in the truth(not believing the rumor)

```python
MaxGroupSize=7     #DO NOT CHANGE!!
maxDays=20
BelieveTruth=0.80
```

## Building the Probability of Interaction in Every Group Size

This section creates the probability distribution for each size of group that a single person interacts with in a single day. Uncomment each of the array indexes and enter the number of times the population interact with other people equal to the index of a in a day. The example below should give more clarity.

examples:

* a[0]=2 -> means the population interacts with 0 people 2 times a day (Talk to themselves maybe ;P)
* a[3]=5 -> means the population interacts with 3 people 5 times a day

```python
a=np.zeros(MaxGroupSize)
def SetArray():
    #a[0]=0
    a[1]=1
    a[2]=1
    a[3]=1
    #a[4]=1
    #a[5]=1
    #a[6]=1
SetArray()
```

## What is the killing point?

This section defines the function *KillingPointFinder*,which determines the killing points for the interaction pattern described.

```python
def KillingPointFinder(array):
    array=array/array.sum()
    x = Symbol('x')
    potentialKillingPoints=np.array(solve((array[0]-1)*x
                              +(array[1]+3*array[2])*x**2
                              +(-2*array[2]+4*array[3]+10*array[4])*x**3
                              +(-3*array[3]-15*array[4]+15*array[5]+35*array[6])*x**4
                              +(6*array[4]-24*array[5]-84*array[6])*x**5
                              +(10*array[5]+70*array[6])*x**6
                              +50*array[6]*x**7, x))
    location=np.where(np.logical_and(potentialKillingPoints>0, potentialKillingPoints<1))
    KillingPoints=float(potentialKillingPoints[location])

    return KillingPoints

```

```python
print("The killing point is at",float("{0:.4f}".format(KillingPointFinder(a))))
```

    The killing point is at 0.8471

## Will the rumor survive or die out?

This section tries to identify whether the rumor will survive or die out. The check simply checks if the initial number of people believing in the truth is higher than the killing point the rumor will die out. Else, The rumor will survive and spread in the population.

```python
if BelieveTruth>KillingPointFinder(a) :
    print("The rumor will die out from the population")
else:
    print("The rumor will spread throughout the population")
```

    The rumor will spread throughout the population

## How does the rumor evolve over time?

This section defines the function *rumorEvolution*, which determines how the evolution of the rumor takes place for upto *maxDays*, for a given starting point *BelieveTruth* and interaction pattern, all defined in the start.

```python
P=np.zeros(maxDays)
P[0]=BelieveTruth
```

```python
def rumorEvolution(P_plus,array):
    innersum=0
    outersum=0
    array=array/array.sum()
    for t in range(0,maxDays-1):
        for k in range(1,MaxGroupSize+1):
            for j in range(int(k/2+1),k+1):
                innersum=innersum+nCr(k,j)*(P_plus[t]**j)*(1-P_plus[t])**(k-j)
            outersum=outersum+array[k-1]*innersum
            innersum=0
        P_plus[t+1]=outersum
        outersum=0
    return P_plus
P=rumorEvolution(P,a)
```

## Lets Visualize the Evolution of Rumor

```python
plt.figure(1)
start,=plt.plot(range(0,maxDays),P,label=str(float("{0:.3f}".format(P[0])))+' truth believers at start')
P_killing=np.ones(maxDays)*KillingPointFinder(a)
equal,=plt.plot(range(0,maxDays),P_killing,label='killing point at '+str(float("{0:.3f}".format(KillingPointFinder(a)))))
plt.legend(handles=[ equal,start])
plt.title('Evolution of the beliefs of the population')
plt.ylabel("ratio of population believing in the truth")
plt.xlabel("No. of Days")
plt.show()
```

## How the Killing point is affected?

In this section, I evaluate how the killing point is affected with the increase in interaction of population in smaller groups vs that of larger groups. I increase the interaction with 2 and 3 people by keeping rest of the interaction parameters to study the evolution of the killing point.

```python
indexs=[2,3]
SetArray()
killingCollection=[]

for index in indexs:
    killing=np.array([])
    for i in range(1,20):
        a[index]=i
        killing=np.append(killing,KillingPointFinder(a))
    killingCollection.append(killing)
    a[index]=1
```

```python
plt.figure(2)
twoPeople, =plt.plot(range(1,20),killingCollection[0],label='interaction 2 people')
threePeople, =plt.plot(range(1,20),killingCollection[1],label='interaction 3 people')
plt.title('Evolution of the Killing point w.r.t people interaction')
plt.ylabel("Killing Point")
plt.xlabel("Increase in per day interaction")
plt.legend(handles=[twoPeople, threePeople])
plt.show()
```

The killing point lowers with the increase in each type of interaction and then saturates at some time. The saturation point is reached faster for the interaction involving more people. Therefore, it is easier to spread rumors with higher interaction, as the killing point is sufficiently higher(i.e less no of people are required to believe the rumor). Also, the interaction of large number people does not require a higher number of such interaction after the saturation point is achieved.

## How Fast Rumor Spread?

In this section, I evaluate a hypothesis that rumor spreads faster with the increased interaction among people. I keep a fixed value of interaction for a lower number of people in a group and check how the rumor evolves and then repeat it for a higher number of people in a group.

```python
SetArray()
a[2]=5
P_2=np.zeros(maxDays)
P_2[0]=0.60
P_2=rumorEvolution(P_2,a)
SetArray()
a[3]=5
P_3=np.zeros(maxDays)
P_3[0]=0.60
P_3=rumorEvolution(P_3,a)
```

```python
plt.figure(3)
higher,=plt.plot(range(0,maxDays),P_2,label='interaction with 2 people 7 times a day')
lower,=plt.plot(range(0,maxDays),P_3,label='interaction with 3 people 7 times a day')
plt.title('Evolution of the beliefs of the population')
plt.ylabel("ratio of population believing truth")
plt.xlabel("No. of Days")
plt.legend(handles=[higher, lower])
plt.show()
```

The hypothesis is accepted, as from the graph it is visible the interaction of larger groups spread the misinformation faster.

## Rumors Spread Fast and Contract Slow

In this section, I evaluate a hypothesis that rumors spread much faster than compared to that of the dying of the rumor. I keep all conditions similar and only vary the starting ratios equal to killing point, 1% higher than the killing point and 1% lower than the killing point.

```python
SetArray()
KillingPoints=KillingPointFinder(a)
```

```python
P_higher=np.zeros(maxDays)
P_higher[0]=KillingPoints+0.01*KillingPoints
P_killing=np.ones(maxDays)*KillingPoints
P_lower=np.zeros(maxDays)
P_lower[0]=KillingPoints-0.01*KillingPoints
```

```python
P_higher=rumorEvolution(P_higher,a)
P_lower=rumorEvolution(P_lower,a)
```

```python
plt.figure(4)
higher,=plt.plot(range(0,maxDays),P_higher,label='starting at '+str(float("{0:.3f}".format(P_higher[0]))))
equal,=plt.plot(range(0,maxDays),P_killing,label='starting at killing point '+str(float("{0:.3f}".format(KillingPoints))))
lower,=plt.plot(range(0,maxDays),P_lower,label='starting at '+str(float("{0:.3f}".format(P_lower[0]))))
plt.title('Evolution of the beliefs of the population')
plt.ylabel("ratio of population believing truth")
plt.xlabel("No. of Days")
plt.legend(handles=[higher, equal, lower])
plt.show()
```

The hypothesis is indeed proven correct in this particular case the rumor spreads to 100 % of the population in about 17 days whereas the dying of the rumor is not reached in even 20 days.


###### Content under Creative Commons Attribution license CC-BY 4.0, code under MIT license (c)2014 L.A. Barba, G.F. Forsyth, C. Cooper. Based on [CFDPython](https://github.com/barbagroup/CFDPython), (c)2013 L.A. Barba, also under CC-BY license.

# Space & Time

## Burgers' Equation

Hi there! We have reached the final lesson of the series *Space and Time — Introduction to Finite-difference solutions of PDEs*, the second module of ["Practical Numerical Methods with Python"](https://openedx.seas.gwu.edu/courses/course-v1:MAE+MAE6286+2017/about).

We have learned about the finite-difference solution for the linear and non-linear convection equations and the diffusion equation. It's time to combine all these into one: *Burgers' equation*. The wonders of *code reuse*!

Before you continue, make sure you have completed the previous lessons of this series, it will make your life easier. You should have written your own versions of the codes in separate, clean Jupyter Notebooks or Python scripts.

You can read about Burgers' Equation on its [wikipedia page](http://en.wikipedia.org/wiki/Burgers'_equation).
Burgers' equation in one spatial dimension looks like this:

$$
\begin{equation}
\frac{\partial u}{\partial t} + u \frac{\partial u}{\partial x} = \nu \frac{\partial ^2u}{\partial x^2}
\end{equation}
$$

As you can see, it is a combination of non-linear convection and diffusion. It is surprising how much you learn from this neat little equation!

We can discretize it using the methods we've already detailed in the previous notebooks of this module.  Using forward difference for time, backward difference for space and our 2nd-order method for the second derivatives yields:

$$
\begin{equation}
\frac{u_i^{n+1}-u_i^n}{\Delta t} + u_i^n \frac{u_i^n - u_{i-1}^n}{\Delta x} = \nu \frac{u_{i+1}^n - 2u_i^n + u_{i-1}^n}{\Delta x^2}
\end{equation}
$$

As before, once we have an initial condition, the only unknown is $u_i^{n+1}$. We will step in time as follows:

$$
\begin{equation}
u_i^{n+1} = u_i^n - u_i^n \frac{\Delta t}{\Delta x} (u_i^n - u_{i-1}^n) + \nu \frac{\Delta t}{\Delta x^2}(u_{i+1}^n - 2u_i^n + u_{i-1}^n)
\end{equation}
$$

### Initial and Boundary Conditions

To examine some interesting properties of Burgers' equation, it is helpful to use different initial and boundary conditions than we've been using for previous steps.

The initial condition for this problem is going to be:

$$
\begin{eqnarray}
u &=& -\frac{2 \nu}{\phi} \frac{\partial \phi}{\partial x} + 4 \\\
\phi(t=0) = \phi_0 &=& \exp \bigg(\frac{-x^2}{4 \nu} \bigg) + \exp \bigg(\frac{-(x-2 \pi)^2}{4 \nu} \bigg)
\end{eqnarray}
$$

This has an analytical solution, given by:

$$
\begin{eqnarray}
u &=& -\frac{2 \nu}{\phi} \frac{\partial \phi}{\partial x} + 4 \\\
\phi &=& \exp \bigg(\frac{-(x-4t)^2}{4 \nu (t+1)} \bigg) + \exp \bigg(\frac{-(x-4t -2 \pi)^2}{4 \nu(t+1)} \bigg)
\end{eqnarray}
$$

The boundary condition will be:

$$
\begin{equation}
u(0) = u(2\pi)
\end{equation}
$$

This is called a *periodic* boundary condition. Pay attention! This will cause you a bit of headache if you don't tread carefully.

### Saving Time with SymPy

The initial condition we're using for Burgers' Equation can be a bit of a pain to evaluate by hand.  The derivative $\frac{\partial \phi}{\partial x}$ isn't too terribly difficult, but it would be easy to drop a sign or forget a factor of $x$ somewhere, so we're going to use SymPy to help us out.

[SymPy](http://sympy.org/en/) is the symbolic math library for Python.  It has a lot of the same symbolic math functionality as Mathematica with the added benefit that we can easily translate its results back into our Python calculations (it is also free and open source).

Start by loading the SymPy library, together with our favorite library, NumPy.

```python
import numpy
import sympy
from matplotlib import pyplot
%matplotlib inline
```

```python
# Set the font family and size to use for Matplotlib figures.
pyplot.rcParams['font.family'] = 'serif'
pyplot.rcParams['font.size'] = 16
```

We're also going to tell SymPy that we want all of its output to be rendered using $\LaTeX$. This will make our Notebook beautiful!

```python
sympy.init_printing()
```

Start by setting up symbolic variables for the three variables in our initial condition.  It's important to recognize that once we've defined these symbolic variables, they function differently than "regular" Python variables.

If we type `x` into a code block, we'll get an error:

```python
x
```

`x` is not defined, so this shouldn't be a surprise.  Now, let's set up `x` as a *symbolic* variable:

```python
x = sympy.symbols('x')
```

Now let's see what happens when we type `x` into a code cell:

```python
x
```

The value of `x` is $x$.  Sympy is also referred to as a computer algebra system -- normally the value of `5*x` will return the product of `5` and whatever value `x` is pointing to.  But, if we define `x` as a symbol, then something else happens:

```python
5 * x
```

This will let us manipulate an equation with unknowns using Python!  Let's start by defining symbols for $x$, $\nu$ and $t$ and then type out the full equation for $\phi$.  We should get a nicely rendered version of our $\phi$ equation.

```python
x, nu, t = sympy.symbols('x nu t')
phi = (sympy.exp(-(x - 4 * t)**2 / (4 * nu * (t + 1))) +
       sympy.exp(-(x - 4 * t - 2 * numpy.pi)**2 / (4 * nu * (t + 1))))
phi
```

It's maybe a little small, but that looks right.  Now to evaluate our partial derivative $\frac{\partial \phi}{\partial x}$ is a trivial task.  To take a derivative with respect to $x$, we can just use:

```python
phiprime = phi.diff(x)
phiprime
```

If you want to see the non-rendered version, just use the Python print command.

```python
print(phiprime)
```

    -(-8*t + 2*x)*exp(-(-4*t + x)**2/(4*nu*(t + 1)))/(4*nu*(t + 1)) - (-8*t + 2*x - 12.5663706143592)*exp(-(-4*t + x - 6.28318530717959)**2/(4*nu*(t + 1)))/(4*nu*(t + 1))

### Now what?

Now that we have the Pythonic version of our derivative, we can finish writing out the full initial condition equation and then translate it into a usable Python expression.  For this, we'll use the *lambdify* function, which takes a SymPy symbolic equation and turns it into a callable function.

```python
from sympy.utilities.lambdify import lambdify

u = -2 * nu * (phiprime / phi) + 4
print(u)
```

    -2*nu*(-(-8*t + 2*x)*exp(-(-4*t + x)**2/(4*nu*(t + 1)))/(4*nu*(t + 1)) - (-8*t + 2*x - 12.5663706143592)*exp(-(-4*t + x - 6.28318530717959)**2/(4*nu*(t + 1)))/(4*nu*(t + 1)))/(exp(-(-4*t + x - 6.28318530717959)**2/(4*nu*(t + 1))) + exp(-(-4*t + x)**2/(4*nu*(t + 1)))) + 4

### Lambdify

To lambdify this expression into a usable function, we tell lambdify which variables to request and the function we want to plug them into.

```python
u_lamb = lambdify((t, x, nu), u)
print('The value of u at t=1, x=4, nu=3 is {}'.format(u_lamb(1, 4, 3)))
```

    The value of u at t=1, x=4, nu=3 is 3.4917066420644494

### Back to Burgers' Equation

Now that we have the initial conditions set up, we can proceed and finish setting up the problem.  We can generate the plot of the initial condition using our lambdify-ed function.

```python
# Set parameters.
nx = 101  # number of spatial grid points
L = 2.0 * numpy.pi  # length of the domain
dx = L / (nx - 1)  # spatial grid size
nu = 0.07  # viscosity
nt = 100  # number of time steps to compute
sigma = 0.1  # CFL limit
dt = sigma * dx**2 / nu  # time-step size

# Discretize the domain.
x = numpy.linspace(0.0, L, num=nx)
```

We have a function `u_lamb` but we need to create an array `u0` with our initial conditions.  `u_lamb` will return the value for any given time $t$, position $x$ and $nu$.  We can use a `for`-loop to cycle through values of `x` to generate the `u0` array.  That code would look something like this:

```Python
u0 = numpy.empty(nx)

for i, x0 in enumerate(x):
    u0[i] = u_lamb(t, x0, nu)
```

But there's a cleaner, more beautiful way to do this -- *list comprehension*.

We can create a list of all of the appropriate `u` values by typing

```Python
[u_lamb(t, x0, nu) for x0 in x]
```

You can see that the syntax is similar to the `for`-loop, but it only takes one line.  Using a list comprehension will create... a list.  This is different from an *array*, but converting a list to an array is trivial using `numpy.asarray()`.

With the list comprehension in place, the three lines of code above become one:

```Python
u = numpy.asarray([u_lamb(t, x0, nu) for x0 in x])
```

```python
# Set initial conditions.
t = 0.0
u0 = numpy.array([u_lamb(t, xi, nu) for xi in x])
u0
```

    array([4.        , 4.06283185, 4.12566371, 4.18849556, 4.25132741,
           4.31415927, 4.37699112, 4.43982297, 4.50265482, 4.56548668,
           4.62831853, 4.69115038, 4.75398224, 4.81681409, 4.87964594,
           4.9424778 , 5.00530965, 5.0681415 , 5.13097336, 5.19380521,
           5.25663706, 5.31946891, 5.38230077, 5.44513262, 5.50796447,
           5.57079633, 5.63362818, 5.69646003, 5.75929189, 5.82212374,
           5.88495559, 5.94778745, 6.0106193 , 6.07345115, 6.136283  ,
           6.19911486, 6.26194671, 6.32477856, 6.38761042, 6.45044227,
           6.51327412, 6.57610598, 6.63893783, 6.70176967, 6.76460125,
           6.82742866, 6.89018589, 6.95176632, 6.99367964, 6.72527549,
           4.        , 1.27472451, 1.00632036, 1.04823368, 1.10981411,
           1.17257134, 1.23539875, 1.29823033, 1.36106217, 1.42389402,
           1.48672588, 1.54955773, 1.61238958, 1.67522144, 1.73805329,
           1.80088514, 1.863717  , 1.92654885, 1.9893807 , 2.05221255,
           2.11504441, 2.17787626, 2.24070811, 2.30353997, 2.36637182,
           2.42920367, 2.49203553, 2.55486738, 2.61769923, 2.68053109,
           2.74336294, 2.80619479, 2.86902664, 2.9318585 , 2.99469035,
           3.0575222 , 3.12035406, 3.18318591, 3.24601776, 3.30884962,
           3.37168147, 3.43451332, 3.49734518, 3.56017703, 3.62300888,
           3.68584073, 3.74867259, 3.81150444, 3.87433629, 3.93716815,
           4.        ])

Now that we have the initial conditions set up, we can plot it to see what $u(x,0)$ looks like:

```python
# Plot the initial conditions.
pyplot.figure(figsize=(6.0, 4.0))
pyplot.title('Initial conditions')
pyplot.xlabel('x')
pyplot.ylabel('u')
pyplot.grid()
pyplot.plot(x, u0, color='C0', linestyle='-', linewidth=2)
pyplot.xlim(0.0, L)
pyplot.ylim(0.0, 10.0);
```

This is definitely not the hat function we've been dealing with until now. We call it a "saw-tooth function".  Let's proceed forward and see what happens.

### Periodic Boundary Conditions

We will implement Burgers' equation with *periodic* boundary conditions.  If you experiment with the linear and non-linear convection notebooks and make the simulation run longer (by increasing `nt`) you will notice that the wave will keep moving to the right until it no longer even shows up in the plot.

With periodic boundary conditions, when a point gets to the right-hand side of the frame, it *wraps around* back to the front of the frame.

Recall the discretization that we worked out at the beginning of this notebook:

$$
\begin{equation}
u_i^{n+1} = u_i^n - u_i^n \frac{\Delta t}{\Delta x} (u_i^n - u_{i-1}^n) + \nu \frac{\Delta t}{\Delta x^2}(u_{i+1}^n - 2u_i^n + u_{i-1}^n)
\end{equation}
$$

What does $u_{i+1}^n$ *mean* when $i$ is already at the end of the frame?

Think about this for a minute before proceeding.

```python
# Integrate the Burgers' equation in time.
u = u0.copy()
for n in range(nt):
    un = u.copy()
    # Update all interior points.
    u[1:-1] = (un[1:-1] -
               un[1:-1] * dt / dx * (un[1:-1] - un[:-2]) +
               nu * dt / dx**2 * (un[2:] - 2 * un[1:-1] + un[:-2]))
    # Update boundary points.
    u[0] = (un[0] -
            un[0] * dt / dx * (un[0] - un[-1]) +
            nu * dt / dx**2 * (un[1] - 2 * un[0] + un[-1]))
    u[-1] = (un[-1] -
            un[-1] * dt / dx * (un[-1] - un[-2]) +
            nu * dt / dx**2 * (un[0] - 2 * un[-1] + un[-2]))
```

```python
# Compute the analytical solution.
u_analytical = numpy.array([u_lamb(nt * dt, xi, nu) for xi in x])
```

```python
# Plot the numerical solution along with the analytical solution.
pyplot.figure(figsize=(6.0, 4.0))
pyplot.xlabel('x')
pyplot.ylabel('u')
pyplot.grid()
pyplot.plot(x, u, label='Numerical',
            color='C0', linestyle='-', linewidth=2)
pyplot.plot(x, u_analytical, label='Analytical',
            color='C1', linestyle='--', linewidth=2)
pyplot.legend()
pyplot.xlim(0.0, L)
pyplot.ylim(0.0, 10.0);
```

Let's now create an animation with the `animation` module of Matplotlib to observe how the numerical solution changes over time compared to the analytical solution.
We start by importing the module from Matplotlib as well as the special `HTML` display method.

```python
from matplotlib import animation
from IPython.display import HTML
```

We create a function `burgers` to computes the numerical solution of the 1D Burgers' equation over time.
(The function returns the history of the solution: a list with `nt` elements, each one being the solution in the domain at a time step.)

```python
def burgers(u0, dx, dt, nu, nt=20):
    """
    Computes the numerical solution of the 1D Burgers' equation
    over the time steps.

    Parameters
    ----------
    u0 : numpy.ndarray
        The initial conditions as a 1D array of floats.
    dx : float
        The grid spacing.
    dt : float
        The time-step size.
    nu : float
        The viscosity.
    nt : integer, optional
        The number of time steps to compute;
        default: 20.

    Returns
    -------
    u_hist : list of numpy.ndarray objects
        The history of the numerical solution.
    """
    u_hist = [u0.copy()]
    u = u0.copy()
    for n in range(nt):
        un = u.copy()
        # Update all interior points.
        u[1:-1] = (un[1:-1] -
                   un[1:-1] * dt / dx * (un[1:-1] - un[:-2]) +
                   nu * dt / dx**2 * (un[2:] - 2 * un[1:-1] + un[:-2]))
        # Update boundary points.
        u[0] = (un[0] -
                un[0] * dt / dx * (un[0] - un[-1]) +
                nu * dt / dx**2 * (un[1] - 2 * un[0] + un[-1]))
        u[-1] = (un[-1] -
                un[-1] * dt / dx * (un[-1] - un[-2]) +
                nu * dt / dx**2 * (un[0] - 2 * un[-1] + un[-2]))
        u_hist.append(u.copy())
    return u_hist
```

```python
# Compute the history of the numerical solution.
u_hist = burgers(u0, dx, dt, nu, nt=nt)
```

```python
# Compute the history of the analytical solution.
u_analytical = [numpy.array([u_lamb(n * dt, xi, nu) for xi in x])
                for n in range(nt)]
```

```python
fig = pyplot.figure(figsize=(6.0, 4.0))
pyplot.xlabel('x')
pyplot.ylabel('u')
pyplot.grid()
u0_analytical = numpy.array([u_lamb(0.0, xi, nu) for xi in x])
line1 = pyplot.plot(x, u0, label='Numerical',
                    color='C0', linestyle='-', linewidth=2)[0]
line2 = pyplot.plot(x, u0_analytical, label='Analytical',
                    color='C1', linestyle='--', linewidth=2)[0]
pyplot.legend()
pyplot.xlim(0.0, L)
pyplot.ylim(0.0, 10.0)
fig.tight_layout()
```

```python
def update_plot(n, u_hist, u_analytical):
    """
    Update the lines y-data of the Matplotlib figure.

    Parameters
    ----------
    n : integer
        The time-step index.
    u_hist : list of numpy.ndarray objects
        The history of the numerical solution.
    u_analytical : list of numpy.ndarray objects
        The history of the analytical solution.
    """
    fig.suptitle('Time step {:0>2}'.format(n))
    line1.set_ydata(u_hist[n])
    line2.set_ydata(u_analytical[n])
```

```python
# Create an animation.
anim = animation.FuncAnimation(fig, update_plot,
                               frames=nt, fargs=(u_hist, u_analytical),
                               interval=100)
```

```python
# Display the video.
HTML(anim.to_html5_video())
```

## Array Operation Speed Increase

Coding up discretization schemes using array operations can be a bit of a pain.  It requires much more mental effort on the front-end than using two nested `for` loops.  So why do we do it?  Because it's fast.  Very, very fast.

Here's what the Burgers code looks like using two nested `for` loops.  It's easier to write out, plus we only have to add one "special" condition to implement the periodic boundaries.

At the top of the cell, you'll see the decorator `%%timeit`.
This is called a "cell magic".  It runs the cell several times and returns the average execution time for the contained code.

Let's see how long the nested `for` loops take to finish.

```python
%%timeit
# Set initial conditions.
u = numpy.array([u_lamb(t, x0, nu) for x0 in x])
# Integrate in time using a nested for loop.
for n in range(nt):
    un = u.copy()
    # Update all interior points and the left boundary point.
    for i in range(nx - 1):
        u[i] = (un[i] -
                un[i] * dt / dx *(un[i] - un[i - 1]) +
                nu * dt / dx**2 * (un[i + 1] - 2 * un[i] + un[i - 1]))
    # Update the right boundary.
    u[-1] = (un[-1] -
             un[-1] * dt / dx * (un[-1] - un[-2]) +
             nu * dt / dx**2 * (un[0]- 2 * un[-1] + un[-2]))
```

    27 ms ± 740 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

Less than 50 milliseconds.  Not bad, really.

Now let's look at the array operations code cell.  Notice that we haven't changed anything, except we've added the `%%timeit` magic and we're also resetting the array `u` to its initial conditions.

This takes longer to code and we have to add two special conditions to take care of the periodic boundaries.  Was it worth it?

```python
%%timeit
# Set initial conditions.
u = numpy.array([u_lamb(t, xi, nu) for xi in x])
# Integrate in time using array operations.
for n in range(nt):
    un = u.copy()
    # Update all interior points.
    u[1:-1] = (un[1:-1] -
               un[1:-1] * dt / dx * (un[1:-1] - un[:-2]) +
               nu * dt / dx**2 * (un[2:] - 2 * un[1:-1] + un[:-2]))
    # Update boundary points.
    u[0] = (un[0] -
            un[0] * dt / dx * (un[0] - un[-1]) +
            nu * dt / dx**2 * (un[1] - 2 * un[0] + un[-1]))
    u[-1] = (un[-1] -
            un[-1] * dt / dx * (un[-1] - un[-2]) +
            nu * dt / dx**2 * (un[0] - 2 * un[-1] + un[-2]))
```

    4.29 ms ± 182 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Yes, it is absolutely worth it.  That's a nine-fold speed increase.  For this exercise, you probably won't miss the extra 40 milliseconds if you use the nested `for` loops, but what about a simulation that has to run through millions and millions of iterations?  Then that little extra effort at the beginning will definitely pay off.

---

###### The cell below loads the style of the notebook.

```python
from IPython.core.display import HTML
css_file = '../../styles/numericalmoocstyle.css'
HTML(open(css_file, 'r').read())
```

<link href='http://fonts.googleapis.com/css?family=Alegreya+Sans:100,300,400,500,700,800,900,100italic,300italic,400italic,500italic,700italic,800italic,900italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Arvo:400,700,400italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=PT+Mono' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Shadows+Into+Light' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Nixie+One' rel='stylesheet' type='text/css'>
<link href='https://fonts.googleapis.com/css?family=Source+Code+Pro' rel='stylesheet' type='text/css'>
<style>

@font-face {
    font-family: "Computer Modern";
    src: url('http://mirrors.ctan.org/fonts/cm-unicode/fonts/otf/cmunss.otf');
}

#notebook_panel { /* main background */
    background: rgb(245,245,245);
}

div.cell { /* set cell width */
    width: 750px;
}

div #notebook { /* centre the content */
    background: #fff; /* white background for content */
    width: 1000px;
    margin: auto;
    padding-left: 0em;
}

#notebook li { /* More space between bullet points */
    margin-top:0.8em;
}

/* draw border around running cells */
div.cell.border-box-sizing.code_cell.running {
    border: 1px solid #111;
}

/* Put a solid color box around each cell and its output, visually linking them*/
div.cell.code_cell {
    background-color: rgb(256,256,256);
    border-radius: 0px;
    padding: 0.5em;
    margin-left:1em;
    margin-top: 1em;
}

div.text_cell_render{
    font-family: 'Alegreya Sans' sans-serif;
    line-height: 140%;
    font-size: 125%;
    font-weight: 400;
    width:600px;
    margin-left:auto;
    margin-right:auto;
}

/* Formatting for header cells */
.text_cell_render h1 {
    font-family: 'Nixie One', serif;
    font-style:regular;
    font-weight: 400;
    font-size: 45pt;
    line-height: 100%;
    color: rgb(0,51,102);
    margin-bottom: 0.5em;
    margin-top: 0.5em;
    display: block;
}

.text_cell_render h2 {
    font-family: 'Nixie One', serif;
    font-weight: 400;
    font-size: 30pt;
    line-height: 100%;
    color: rgb(0,51,102);
    margin-bottom: 0.1em;
    margin-top: 0.3em;
    display: block;
}

.text_cell_render h3 {
    font-family: 'Nixie One', serif;
    margin-top:16px;
    font-size: 22pt;
    font-weight: 600;
    margin-bottom: 3px;
    font-style: regular;
    color: rgb(102,102,0);
}

.text_cell_render h4 {    /*Use this for captions*/
    font-family: 'Nixie One', serif;
    font-size: 14pt;
    text-align: center;
    margin-top: 0em;
    margin-bottom: 2em;
    font-style: regular;
}

.text_cell_render h5 {  /*Use this for small titles*/
    font-family: 'Nixie One', sans-serif;
    font-weight: 400;
    font-size: 16pt;
    color: rgb(163,0,0);
    font-style: italic;
    margin-bottom: .1em;
    margin-top: 0.8em;
    display: block;
}

.text_cell_render h6 { /*use this for copyright note*/
    font-family: 'PT Mono', sans-serif;
    font-weight: 300;
    font-size: 9pt;
    line-height: 100%;
    color: grey;
    margin-bottom: 1px;
    margin-top: 1px;
}

.CodeMirror{
    font-family: "Source Code Pro";
    font-size: 90%;
}

.alert-box {
    padding:10px 10px 10px 36px;
    margin:5px;
}

.success {
    color:#666600;
    background:rgb(240,242,229);
}
</style>


```python

# check convergence rate of fem, similar to
# previous global func

import sympy as sym
import numpy as np
from fe_approx1D_numint import *

Omega = [-1, 1]
symbolic = False
N = 10

vertices, cells, dof_map = mesh_uniform(N, 2, Omega, symbolic)

x = sym.Symbol("x")
gauss_exact = sym.exp(-x**2) - sym.exp(-1)

u = approximate(gauss_exact, symbolic, 1, N, None, Omega)

vertices = np.array(vertices)
gauss_bell = np.exp(-vertices**2)
gauss_bell -= gauss_bell[0]

import pylab
pylab.plot(gauss_bell)
pylab.plot(u)
pylab.show()

```

```python

```

Data Science and Machine Learning/Machine-Learning-In-Python-THOROUGH/EXAMPLES/FINITE_ELEMENTS/INTRO/SRC/09_CONVERGENCE_RATE_FEM.ipynb


# Quantum Channel Discrimination

Date : December 30, 2021

This notebook contains material supporting a paper, currently titled *Five Starter Pieces: Quantum Information Science via Semi-definite Programs*, by Vikesh Siddhu (vsiddhu@protonmail.com) and Sridhar Tayur (stayur@cmu.edu). The paper is available on this **[arXiv](http://arxiv.org/abs/2112.08276)** link. The arXiv paper is released there is under the **[arXiv.org perpetual, non-exclusive license](https://arxiv.org/licenses/nonexclusive-distrib/1.0/license.html)**, and this code is released under the **[MIT license](https://opensource.org/licenses/MIT)**.

This notebook depends upon various packages including [numpy](https://numpy.org/) >= 1.19.5, [picos](https://picos-api.gitlab.io/picos/index.html) >= 2.2.55, and [cvxopt](http://cvxopt.org/) >= 1.2.5.

[](https://colab.research.google.com/github/vsiddhu/SDP-Quantum-OR/blob/master/Notebook%203%20-%20Quantum%20Channel%20Discrimination.ipynb)

## Introduction

The probability of correctly distinguishing two unifromly chosen quantum channels $\mathcal{B}_1$ and $\mathcal{B}_2$ without using entangled inputs is
\begin{equation}
 q^* = \frac{1}{2}(1 + \frac{1}{2} \max_{\rho}|| \mathcal{B}_1(\rho) - \mathcal{B}_2(\rho) ||_1)
\end{equation}
and using entangled inputs is
\begin{equation}
s^* = \frac{1}{2}(1 + \frac{1}{2}|| \mathcal{B}_1 -  \mathcal{B}_2 ||_{\diamond}).
\end{equation}
Here $||.||_{\diamond}$ refers to the diamond norm of a linear map discussed next.

### Diamond Norm
A linear map $\mathcal{B}:\hat{ \mathcal{H}_a} \mapsto \hat{ \mathcal{H}_b}$ has diamond norm
$$
|| \mathcal{B} ||_{\diamond} =
\max_{ ||\rho_{aa}||_1 \leq 1} ||\mathcal{B} \otimes \mathcal{I} (\rho_{aa})||_1
$$
where $\mathcal{I}$ is the identity map on $\hat{ \mathcal{H}_a}$, and $||X||_1 = \rm Tr (\sqrt{X X^{\dagger}})$.
The map $\mathcal{B}$ has a Choi-Jamiolkowski representation
$$
\mathcal{J}_{ba}(\mathcal{B}) = \mathcal{B} \otimes \mathcal{I} (\gamma)
$$
where $\gamma = \sum_{ij} | ii \rangle \langle  jj|$. Using this representation one can compute $|| \mathcal{B} ||_{\diamond}$ as the optimum value of the semi-definite program (SDP)

 \begin{align}
 \begin{aligned}
   \text{maximize} & \;  \frac{1}{2} \; \big( \rm Tr(\mathcal{J}_{ba}(\mathcal{B})X) + \rm Tr(\mathcal{J}_{ba}(\mathcal{B})X)^* \big) & \\
   \text{subject to} & \;
  \begin{pmatrix}
   I_b \otimes \rho_a & X \\
  X^{\dagger} & I_b \otimes \sigma_a
  \end{pmatrix}
  \succeq 0. & \\
  & \rm Tr(\rho_a) = 1, & \\
  & \rm Tr(\sigma_a) = 1, & \\
  \text{and} \; & \quad X \in \hat{\mathcal{H}}_{ba},
  \end{aligned}
  \end{align}
  where $I_b$ is the identity matrix on $\mathcal{H}_b$. This SDP problem mentioned above has a dual formulation,

\begin{align}
 \begin{aligned}
  \text{minimize} & \;  \frac{1}{2} \big(  \mu + \nu \big) & \\
  \text{subject to} & \;
  \begin{pmatrix}
  N_{ba} & -\mathcal{J}_{ba}(\mathcal{B})\\
  -\mathcal{J}_{ba}(\mathcal{B})^{\dagger} & M_{ba}
  \end{pmatrix}
  \succeq 0,\\
  & \; \rm Tr_b (N_{ba}) \preceq \mu I_a \\
  \text{and} & \; \rm Tr_b (M_{ba}) \preceq \nu I_a.
  \end{aligned}
  \end{align}
In what follows, consider various examples of this SDP.

```python
%pylab inline
```

    Populating the interactive namespace from numpy and matplotlib

```python
# For Google Colab use, commands installing packages
try:
  import google.colab
  IN_COLAB = True
except:
  IN_COLAB = False

# Install PICOS and CVXOPT in Google Colab
if IN_COLAB:
    !pip install -q picos
    !pip install -q cvxopt
```

```python
import picos as pic
import cvxopt as cvx
```

```python
print('Solvers supported on this installation of picos:', pic.solvers.all_solvers().keys())

```

    Solvers supported on this installation of picos: dict_keys(['cplex', 'cvxopt', 'ecos', 'glpk', 'gurobi', 'mosek', 'mskfsn', 'scip', 'smcp'])

```python
print('Solvers available to picos on this machine :', pic.solvers.available_solvers())
```

    Solvers available to picos on this machine : ['cvxopt', 'mosek', 'mskfsn']

### Example 1
Diamond norm of a qubit depolarizing channel
\begin{equation}
    \Delta(\rho) = \lambda \rho + \frac{(1-\lambda)}{2} \rm Tr(\rho) I,
\end{equation}
 where $-1/3 \leq \lambda \leq 1$. The Choi-Jamiolkowski representation of this channel is
 \begin{equation}
 \mathcal{J}_{ba}(\Delta) =
 \begin{pmatrix}
 (1 + \lambda)/2 & 0 & 0 & \lambda \\
 0 & (1 - \lambda)/2 & 0 & 0 \\
 0 & 0 & (1 - \lambda)/2 & 0 \\
 \lambda & 0 & 0 & (1 + \lambda)/2
 \end{pmatrix}.
 \end{equation}
 The depolarizing channel above can also be written using the parameter $p := 3(1-\lambda)/4$ which lies between $0$ and $1$.

```python
#Example 1 using the primal formulation

cjMat = lambda lm : np.array([[(1. + lm)/2, 0., 0., lm],
                    [0., (1. - lm)/2, 0., 0.],
                    [0., 0., (1. - lm)/2, 0.],
                    [lm, 0., 0., (1 + lm)/2]])

p = 1
lmVal = 1.-4.*p/3.
da = 2
db = 2
gammaBA = cjMat(lmVal)
```

```python
#Constants
#----------
gammaPic = pic.Constant("gammaBA", gammaBA)
shpBA = np.shape( gammaBA )
shpB = np.shape(pic.partial_trace(gammaPic, subsystems=(1),dimensions=2))
shpA = np.shape(pic.partial_trace(gammaPic, subsystems=(0),dimensions=2))
iMatB = pic.Constant('Ib', np.eye(shpB[0]))

#Variables
#----------
rhoPic = pic.HermitianVariable("rhoA", shpA)
sigPic = pic.HermitianVariable("sigA", shpB)
XPic = pic.ComplexVariable("X", shpBA)
```

```python
prob1P = pic.Problem()

#Constraint
#----------
prob1P.add_constraint(((iMatB @ rhoPic  & XPic) // (XPic.H & iMatB @ sigPic)) >> 0)
prob1P.add_constraint(pic.trace(rhoPic) == 1)
prob1P.add_constraint(pic.trace(sigPic) == 1)

#Objective
#----------
obj = pic.trace(gammaPic | XPic).real

prob1P.set_objective('max',obj)

```

```python
#User readable view of the problem being composed in PICOS'
print(prob1P)
```

    -------------------------------------
    Complex Semidefinite Program
      maximize Re(⟨gammaBA, X⟩)
      over
        4×4 complex variable X
        2×2 hermitian variable rhoA, sigA
      subject to
        [Ib⊗rhoA, X; Xᴴ, Ib⊗sigA] ≽ 0
        tr(rhoA) = 1
        tr(sigA) = 1
    -------------------------------------

```python
#Solve the problem using cvxopt as a solver
prob1P.solve(verbosity=False,solver='cvxopt')
```

    <primal feasible solution pair (claimed optimal) from cvxopt>

```python
#Solver claims to have found optimal solution
dNorm1P =  prob1P.value
```

```python
#Example 1 Dual Formulation
#Constants
#----------
iMatA = pic.Constant('Ia', np.eye(shpA[0]))

#Variables
#----------
NPicBA = pic.HermitianVariable("Nba", shpBA)
MPicBA = pic.HermitianVariable("Mba", shpBA)
mu = pic.RealVariable("mu")
nu = pic.RealVariable("nu")

```

```python
prob1D = pic.Problem()

#Constraint
#----------
prob1D.add_constraint(((NPicBA & -gammaPic) // (-gammaPic.H & MPicBA)) >> 0)

NPicA = pic.partial_trace(NPicBA,subsystems=(0),dimensions=2)
MPicA = pic.partial_trace(MPicBA,subsystems=(0),dimensions=2)

prob1D.add_constraint(MPicA<<mu*iMatA)
prob1D.add_constraint(NPicA<<nu*iMatA)

#Objective
#----------
obj = (mu + nu)/2

prob1D.set_objective('min',obj)

```

```python
#User readable view of the problem being composed in PICOS'
print(prob1D)
```

    ---------------------------------------
    Complex Semidefinite Program
      minimize (mu + nu)/2
      over
        4×4 hermitian variable Mba, Nba
        1×1 real variable mu, nu
      subject to
        [Nba, -gammaBA; -gammaBAᵀ, Mba] ≽ 0
        Mba.{tr([2×2])⊗[2×2]} ≼ mu·Ia
        Nba.{tr([2×2])⊗[2×2]} ≼ nu·Ia
    ---------------------------------------

```python
#Solve the problem using mosek as a cvxopt
prob1D.solve(verbosity=False,solver='cvxopt')

```

    <primal feasible solution pair (claimed optimal) from cvxopt>

```python
#Solver claims to have found optimal solution
dNorm1D =  prob1D.value
```

```python
dNorm1Alg = 1.0

print('Diamond Norm of a Depolarizing Channel')
print('Using Primal SDP = ', dNorm1P)
print('Using DualSDP = ', dNorm1D)
print('Actual Value', dNorm1Alg)
print('Difference between algebraic and primal numeric', abs(dNorm1Alg - dNorm1P))
print('Difference between algebraic and dual numeric', abs(dNorm1Alg - dNorm1D))
```

    Diamond Norm of a Depolarizing Channel
    Using Primal SDP =  0.9999999999206617
    Using DualSDP =  0.9999999999718845
    Actual Value 1.0
    Difference between algebraic and primal numeric 7.933831369655309e-11
    Difference between algebraic and dual numeric 2.811550992021239e-11

* As mentioned in Sec. 6, any quantum channel (completely positive trace preserving map) $\mathcal{B}$ has a diamond norm $|| \mathcal{B}||_{\diamond} = 1$. The numerics above simply confirm this fact in the case of a qubit depolarizing channel. In what follows we discuss the advantage of using entanglement to distinguish quantum channels.

### Example 2: Distinguishing Depolarizing from Identity

In the introduction state at the top of this notebook, let $\mathcal{B}_1$ be the qubit identity channel $\mathcal{I}$ and $\mathcal{B}_2$ be the qubit depolarizing channel
\begin{equation}
    \Delta(\rho) = \lambda \rho + \frac{(1-\lambda)}{2} \rm Tr(\rho) I,
\end{equation}
where $-1/3 \leq \lambda \leq 1$.

```python
#Example 2. Using the primal formulation
upsBA = cjMat(1)

lmVal = -1/3
thetaBA = cjMat(lmVal)

gamma2BA = upsBA - thetaBA
```

```python
#Primal SDP
#Constants
#----------
gamma2Pic = pic.Constant("gamma2BA", gamma2BA)

shpBA = np.shape( gamma2Pic )
shpB = np.shape(pic.partial_trace(gamma2Pic, subsystems=(1),dimensions=2))
shpA = np.shape(pic.partial_trace(gamma2Pic, subsystems=(0),dimensions=2))
iMatB = pic.Constant('Ib', np.eye(shpB[0]))

#Variables
#----------
rhoPic = pic.HermitianVariable("rhoA", shpA)
sigPic = pic.HermitianVariable("sigA", shpA)
XPic = pic.ComplexVariable("X", shpBA)

prob2P = pic.Problem()

#Constraint
#----------
prob2P.add_constraint(((iMatB @ rhoPic & XPic ) // (XPic.H & iMatB @ sigPic)) >> 0)
prob2P.add_constraint(pic.trace(rhoPic) == 1)
prob2P.add_constraint(pic.trace(sigPic) == 1)

#Objective
#----------
obj = pic.trace(gamma2Pic | XPic).real

prob2P.set_objective('max',obj)

```

```python
#User readable view of the problem being composed in PICOS'
print(prob2P)
```

    -------------------------------------
    Complex Semidefinite Program
      maximize Re(⟨gamma2BA, X⟩)
      over
        4×4 complex variable X
        2×2 hermitian variable rhoA, sigA
      subject to
        [Ib⊗rhoA, X; Xᴴ, Ib⊗sigA] ≽ 0
        tr(rhoA) = 1
        tr(sigA) = 1
    -------------------------------------

```python
#Solve the problem using mosek as a cvxopt
prob2P.solve(verbosity=False,solver='cvxopt')

```

    <primal feasible solution pair (claimed optimal) from cvxopt>

```python
#Solver claims to have found optimal saolution
dNorm2P =  prob2P.value
```

```python
#Example 2 Dual Formulation
#Constants
#----------
iMatA = pic.Constant('Ia', np.eye(shpA[0]))

#Variables
#----------
NPicBA = pic.HermitianVariable("Nba", shpBA)
MPicBA = pic.HermitianVariable("Mba", shpBA)
mu = pic.RealVariable("mu")
nu = pic.RealVariable("nu")

```

```python
prob2D = pic.Problem()

#Constraint
#----------
prob2D.add_constraint(((NPicBA & -gamma2Pic) // (-gamma2Pic.H & MPicBA)) >> 0)

NPicA = pic.partial_trace(NPicBA,subsystems=(0),dimensions=2)
MPicA = pic.partial_trace(MPicBA,subsystems=(0),dimensions=2)

prob2D.add_constraint(MPicA<<mu*iMatA)
prob2D.add_constraint(NPicA<<nu*iMatA)

#Objective
#----------
obj = (mu + nu)/2

prob2D.set_objective('min',obj)

```

```python
#User readable view of the problem being composed in PICOS'
print(prob2D)

```

    -----------------------------------------
    Complex Semidefinite Program
      minimize (mu + nu)/2
      over
        4×4 hermitian variable Mba, Nba
        1×1 real variable mu, nu
      subject to
        [Nba, -gamma2BA; -gamma2BAᵀ, Mba] ≽ 0
        Mba.{tr([2×2])⊗[2×2]} ≼ mu·Ia
        Nba.{tr([2×2])⊗[2×2]} ≼ nu·Ia
    -----------------------------------------

```python
#Solve the problem using mosek as a cvxopt
prob2D.solve(verbosity=False,solver='cvxopt')
```

    <primal feasible solution pair (claimed optimal) from cvxopt>

```python
#Solver claims to have found optimal solution
dNorm2D =  prob2D.value
```

```python
print('Diamond Norm distance between identity and equal probability Pauli error Channel')
print('Using Primal SDP = ', dNorm2P)
print('Using DualSDP = ', dNorm2D)
print('Difference between primal and dual values', abs(dNorm2D - dNorm2P))

```

    Diamond Norm distance between identity and equal probability Pauli error Channel
    Using Primal SDP =  1.9999999997151092
    Using DualSDP =  1.9999999999322486
    Difference between primal and dual values 2.1713941755763244e-10

```python
pE = (1 + dNorm2D/2)/2
print('Probability of distinguishing with an entangled input s* = ', pE)
print('Probability of distinguishing without an entangled input q* = ', (3 - lmVal)/4)
```

    Probability of distinguishing with an entangled input s* =  0.9999999999830622
    Probability of distinguishing without an entangled input q* =  0.8333333333333334

The value pE obtained above is in agreement with algebraic value $s^* = (7 - 3\lambda)/8$ stated in Sec.6. This value is larger than $q^* = (3-\lambda)/4$, the probability of distinguishing without entangled inputs.

### Example 3: Distinguishing Werner-Holevo channels
\begin{equation}
\mathcal{B}_1(\rho) = \frac{1}{d+1} \big( \rm Tr(\rho) I + \rho^T\big),
\quad \text{and} \quad
\mathcal{B}_2(\rho) = \frac{1}{d-1} \big( \rm Tr(\rho) I - \rho^T\big).
\end{equation}
Who have Choi-Jamiolkowski representations
\begin{equation}
\mathcal{J}_{ba}(\mathcal{B}_1) =  \frac{1}{d+1}( I \otimes I + S_{ba}),
\quad \text{and} \quad
\mathcal{J}_{ab}(\mathcal{B}_2) = \frac{1}{d-1}( I \otimes I - S_{ba}),
\end{equation}
resp., where $S_{ba} |i \rangle |j \rangle = |j \rangle |i \rangle$. We compute their diamond norm distance

\begin{equation}
|| \mathcal{B}_1 -  \mathcal{B}_2 ||_{\diamond},
\end{equation}

using which the probability $s^* = \frac{1}{2}(1 + \frac{1}{2}|| \mathcal{B}_1 -  \mathcal{B}_2 ||_{\diamond})$ is be computed with relative ease.

```python
d = 3
iMatJoint = np.eye(d*d)

```

```python
wMatrix = np.zeros(shape=(d*d,d*d))
for i in range(d):
    for j in range(d):
        k1 = i*d + j
        k2 = j*d + i
        wMatrix[k1,k2] = 1.
```

```python
thBA = (iMatJoint + wMatrix)/(d+1)
psiBA = (iMatJoint - wMatrix)/(d-1)
```

```python
#Example 3 using the primal formulation

#Constants
#----------
gamma3Pic = pic.Constant("gammaBA", thBA - psiBA)
shpBA = np.shape( gamma3Pic )

shpB = np.shape(pic.partial_trace(gamma3Pic, subsystems=(1),dimensions=d))
shpA = np.shape(pic.partial_trace(gamma3Pic, subsystems=(0),dimensions=d))
iMatB = pic.Constant('Ib', np.eye(shpB[0]))

#Variables
#----------
rhoPic = pic.HermitianVariable("rhoA", shpA)
sigPic = pic.HermitianVariable("sigA", shpB)
XPic = pic.ComplexVariable("X", shpBA)

prob3P = pic.Problem()

#Constraint
#----------
prob3P.add_constraint(((iMatB @ rhoPic & XPic) // (XPic.H & iMatB @ sigPic)) >> 0)
prob3P.add_constraint(pic.trace(rhoPic) == 1)
prob3P.add_constraint(pic.trace(sigPic) == 1)

#Objective
#----------
obj = pic.trace(gamma3Pic | XPic).real

prob3P.set_objective('max',obj)
```

```python
#User readable view of the problem being composed in PICOS'
print(prob3P)
```

    -------------------------------------
    Complex Semidefinite Program
      maximize Re(⟨gammaBA, X⟩)
      over
        9×9 complex variable X
        3×3 hermitian variable rhoA, sigA
      subject to
        [Ib⊗rhoA, X; Xᴴ, Ib⊗sigA] ≽ 0
        tr(rhoA) = 1
        tr(sigA) = 1
    -------------------------------------

```python
#Solve the problem using mosek as a cvxopt
prob3P.solve(verbosity=False,solver='cvxopt')
```

    <primal feasible solution pair (claimed optimal) from cvxopt>

```python
#Solver claims to have found optimal solution
dNorm3P =  prob3P.value
```

```python
#Example 3 Dual Formulation
#Constants
#----------
iMatA = pic.Constant('Ia', np.eye(shpA[0]))

#Variables
#----------
NPicBA = pic.HermitianVariable("Nba", shpBA)
MPicBA = pic.HermitianVariable("Mba", shpBA)
mu = pic.RealVariable("mu")
nu = pic.RealVariable("nu")

prob3D = pic.Problem()

#Constraint
#----------
prob3D.add_constraint(((NPicBA & -gamma3Pic) // (-gamma3Pic.H & MPicBA)) >> 0)

NPicA = pic.partial_trace(NPicBA,subsystems=(0),dimensions=d)
MPicA = pic.partial_trace(MPicBA,subsystems=(0),dimensions=d)

prob3D.add_constraint(MPicA<<mu*iMatA)
prob3D.add_constraint(NPicA<<nu*iMatA)

#Objective
#----------
obj = (mu + nu)/2

prob3D.set_objective('min',obj)
```

```python
#User readable view of the problem being composed in PICOS'
print(prob3D)

```

    ---------------------------------------
    Complex Semidefinite Program
      minimize (mu + nu)/2
      over
        9×9 hermitian variable Mba, Nba
        1×1 real variable mu, nu
      subject to
        [Nba, -gammaBA; -gammaBAᵀ, Mba] ≽ 0
        Mba.{tr([3×3])⊗[3×3]} ≼ mu·Ia
        Nba.{tr([3×3])⊗[3×3]} ≼ nu·Ia
    ---------------------------------------

```python
#Solve the problem using mosek as a cvxopt
prob3D.solve(verbosity=False,solver='cvxopt')
```

    <primal feasible solution pair (claimed optimal) from cvxopt>

```python
#Solver claims to have found optimal solution
dNorm3D =  prob3D.value
```

```python
print('Diamond Norm distance between identity and equal probability Pauli error Channel')
print('Using Primal SDP = ', dNorm3P)
print('Using DualSDP = ', dNorm3D)
print('Difference between primal and dual values', abs(dNorm3D - dNorm3P))

```

    Diamond Norm distance between identity and equal probability Pauli error Channel
    Using Primal SDP =  1.9999999997341638
    Using DualSDP =  1.9999999999261795
    Difference between primal and dual values 1.9201573664417992e-10

```python
pE = (1 + dNorm3D/2)/2
print('Probability of distinguishing with an entangled input s* = ', pE)
print('Probability of distinguishing without an entangled input q* = ', .5 + 1/(d+1))
```

    Probability of distinguishing with an entangled input s* =  0.9999999999815449
    Probability of distinguishing without an entangled input q* =  0.75

The value pE obtained above is in agreement with algebraic value $s^* = 1$ stated in Sec.6. This value is larger than $q^* = \frac{1}{2} + \frac{1}{d+1}$, the probability of distinguishing without entangled inputs.

Notebook 3 - Quantum Channel Discrimination.ipynb


# CSC421 Assignment 3 - Part II Naive Bayes Classification (5 points) #
### Author: George Tzanetakis

This notebook is based on the supporting material for topics covered in **Chapter 13 Quantifying Uncertainty**and **Chapter 20 - Statistical Learning Method** from the book *Artificial Intelligence: A Modern Approach.* This part does NOT rely on the provided code so you can complete it just using basic Python.

```
Misunderstanding of probability may be the greatest of all impediments
to scientific literacy.

Gould, Stephen Jay
```

# Introduction

Text categorization is the task of assigning a given document to one of a fixed set of categories, on the basis of text it contains. Naive Bayes models are often used for this task. In these models, the query variable is
the document category, and the effect variables are the presence/absence
of each word in the language; the assumption is that words occur independently in documents within a given category (condititional independence), with frequencies determined by document category. Download the following file: http://www.cs.cornell.edu/People/pabo/movie-review-data/review_polarity.tar.gz containing a dataset that has been used for text mining consisting of movie reviews classified into negative and positive. You
will see that there are two folders for the positivie and negative category and they each contain multiple text files with the reviews. You can find more information about the dataset at:
http://www.cs.cornell.edu/People/pabo/movie-review-data/

Our goal will be to build a simple Naive Bayes classifier for this dataset. More complicated approaches using term frequency and inverse document frequency weighting and many more words are possible but the basic concepts
are the same. The goal is to understand the whole process so DO NOT use existing machine learning packages but rather build the classifier from scratch.

Our feature vector representation for each text file will be simply a binary vector that shows which of the following words are present in the text file: Awful Bad Boring Dull Effective Enjoyable Great Hilarious. For example the text file cv996 11592.txt would be represented as (0, 0, 0, 0, 1, 0, 1, 0) because it contains Effective and Great but none of the other words.

# Question 2A (Minimum) CSC421 -  (1 point, CSC581C - 0 points)

Write code that parses the text files and calculates the probabilities for
each dictionary word given the review polarity

```python
# YOUR CODE GOES HERE
from os import listdir
import numpy as np

def parseFiles():
    keyWords = ["awful", "bad","boring","dull","effective","enjoyable","great","hilarious"]
    input_neg = "./review_polarity/txt_sentoken/neg/"
    input_pos = "./review_polarity/txt_sentoken/pos/"

    neg_names = [(input_neg+f) for f in listdir(input_neg)]
    pos_names = [(input_pos+f) for f in listdir(input_pos)]

    num_neg = len(neg_names)
    num_pos = len(pos_names)

    neg_polarities = np.zeros((num_neg,len(keyWords)))
    pos_polarities = np.zeros((num_pos,len(keyWords)))

    for n in range(0,num_neg):
        with open(neg_names[n],'r') as f:
            words = f.read().split()
            words = [w.lower() for w in words] # change every word to lowercase for easier parsing
        neg_polarities[n,:] = [int(w in words) for w in keyWords]

    for n in range(0,num_pos):
        with open(pos_names[n],'r') as f:
            words = f.read().split()
            words = [w.lower() for w in words] # change every word to lowercase for easier parsing
        pos_polarities[n,:] = [int(w in words) for w in keyWords]

    probGivenNeg = np.divide(np.sum(neg_polarities,0,int),num_neg)
    probGivenPos = np.divide(np.sum(pos_polarities,0,int),num_pos)

    probPos = num_pos / (num_pos + num_neg)
    probNeg = num_neg / (num_pos + num_neg)

    return probGivenNeg, probGivenPos, probNeg, probPos, neg_polarities, pos_polarities

probGivenNeg, probGivenPos, probNeg, probPos, neg_polarities, pos_polarities = parseFiles()
print("Probability of each word given a positive review: " + str(probGivenPos))
print("Probability of each word given a negative review: " + str(probGivenNeg))
```

    Probability of each word given a positive review: [0.019 0.255 0.048 0.023 0.12  0.095 0.408 0.125]
    Probability of each word given a negative review: [0.101 0.505 0.169 0.091 0.046 0.053 0.286 0.05 ]

# Question 2B (Minimum) (CSC421 - 1 point, CSC581C - 0 point)

Explain how the probability estimates for each dictionary word given the review polarity can be combined to form a Naive Bayes classifier. You can look up Bernoulli Bayes model for this simple model where only presence/absence of a word is modeled.

Your answer should be a description of the process with equations and a specific example as markdown text NOT python code. You will write the code in the next questinon.

**\# YOUR MARKDOWN TEXT GOES HERE**

With Naive Bayes, we are assuming attributes are conditionally independent given the class value. As such, naive Bayes tells us

\begin{align}
P(c_1|e_1,e_2,...,e_n) = \alpha P(e_1|c_1)...P(e_n|c_1)P(c_1)
\end{align}

Where $E=e_1,e_2,...,e_n$ are the instances, and $C=c_1,c_2,...,c_m$ are the classes.

In our case the classes are the types of reviews, ie. C={pos,neg}. The instances are the 8 dictionary words, ie. E={Awful, Bad, Boring, Dull, Effective, Enjoyable, Great, Hilarious}.

If we have the probability estimates for each dictionary word given the review polarity, eg. P(Awful | pos), we can build a Naive Bayes classifier based on the information above.

We can calculate the probability of each word occuring given the polarity directly from our data, as well as the probability of being a positive or negative review. Using this, if we get a new review and want to classify if it is a positive or negative review, we can first determine if each word occurs or not, building our evidence. From this, we can use the following:
\begin{align}
&P(c|E) = P(c|e_1,e_2,...,e_n)\\
&= P(c,e_1,e_2,...,e_n)\\
&= P(e_1,e_2,...,e_n,c)\\
&= aP(e_1|c)P(e_2|c)...P(e_n|c)P(c)
\end{align}

We would calculate this for $c=pos$ and $c=neg$, and the higher pribability indicates the polarity the review would be classified as. Note $\alpha$ is just a normalization factor. We can calculate it to get a probability between [0,1] for each, however comparing the un-normalized probabilities directly will give the same classification answer.

To give a concrete example, suppose the evidence for a review gives the following: $[Awful, Bad, Boring, Dull, Effective, Enjoyable, Great, Hilarious] = [1,1,0,1,0,0,0,1]$

We want to know if this review is positive or negative.
First, we will calculate $P(pos | e_1,e_2,...,e_8)$. This will give $P(pos | e_1,e_2,...,e_8) = \alpha P(e_1|pos)P(e_2|pos)...P(e_8|pos)P(pos)$.

Next, we will calculate $P(neg | e_1,e_2,...,e_8)$. This will give $P(neg | e_1,e_2,...,e_8) = \alpha P(e_1|neg)P(e_2|neg)...P(e_8|neg)P(neg)$.

In both cases, we will get a value multiplied by $\alpha$ (we dont know $\alpha$). If we want to get $\alpha$, we add the 2 values. The class with the higher value will indicate the more probable classification.

# Question 2C (Expected) 1 point

Write Python code for classifying a particular test instance (in our case movie review) following a Bernolli Bayes approach. Your code should calculate the likelihood the review is positive given the correspondng conditional probabilities for each dictionary word as well as the likelihood the review is negative given the corresponding conditional probabilities for each dictionary word. Check that your code works by providing a few example cases of prediction. Your code should be written from "scratch" and only use numpy/scipy but not machine learning libraries like scikit-learn or tensorflow.

```python
# YOUR CODE GOES HERE

"""
need to classify particular test instance following a bernolli bayes
approach.

-calculate
(1) the likelihood the review is pos given the corresponding
conditional probs for each dictionary word as well as
(2) the likelihood the review is negative given the corresponding
conditional probs for each dictionary word.
- check that your code works by providing a few examples for prediction.
"""

def calcLikelihood(evidence, probGivenNeg, probGivenPos, probNeg, probPos):

    posLikelihood = 1
    negLikelihood = 1
    for n in range(0,len(evidence)):
        if evidence[n] == 1:
            posLikelihood *= probGivenPos[n]
            negLikelihood *= probGivenNeg[n]
        else:
            posLikelihood *= (1-probGivenPos[n])
            negLikelihood *= (1-probGivenNeg[n])
    posLikelihood *= probPos
    negLikelihood *= probNeg

    if posLikelihood > negLikelihood:
        classifier = "pos"
    elif negLikelihood > posLikelihood:
        classifier = "neg"
    else:
        classifier = "equal"

    return classifier

probGivenNeg, probGivenPos, probNeg, probPos, neg_polarities, pos_polarities = parseFiles()
keyWords = ["awful", "bad","boring","dull","effective","enjoyable","great","hilarious"]

review1 = "this is an awful and boring movie."
words = review1.split()
evidence = [int(w in words) for w in keyWords]
classifier = calcLikelihood(evidence, probGivenNeg, probGivenPos, probNeg, probPos)
print("review 1: " + classifier)

review2 = "enjoyable movie with great humour. was a bit dull at times however."
words = review2.split()
evidence = [int(w in words) for w in keyWords]
classifier = calcLikelihood(evidence, probGivenNeg, probGivenPos, probNeg, probPos)
print("review 2: " + classifier)

```

    review 1: neg
    review 2: pos

# QUESTION 2D (Expected ) 1 point

Calculate the classification accuracy and confusion matrix that you would obtain using the whole data set for both training and testing. Do not use machine learning libraries like scikit-learn or tensorflow for this only the basic numpy/scipy stuff.

```python
# YOUR CODE GOES HERE

def calcClassificationAccuracy():
    probGivenNeg, probGivenPos, probNeg, probPos, neg_polarities, pos_polarities = parseFiles()
    keyWords = ["awful", "bad","boring","dull","effective","enjoyable","great","hilarious"]

#     loop through positive polarities and count correct/incorrect
    correctPos = 0
    incorrectPos = 0
    correctNeg = 0
    incorrectNeg = 0
    for n in range(0,len(pos_polarities)):
        classifier = calcLikelihood(pos_polarities[n,:], probGivenNeg, probGivenPos, probNeg, probPos)
        if classifier == "pos":
            correctPos += 1
        else:
            incorrectPos += 1

#     loop through negative
    for n in range(0,len(neg_polarities)):
        classifier = calcLikelihood(neg_polarities[n,:], probGivenNeg, probGivenPos, probNeg, probPos)
        if classifier == "neg":
            correctNeg += 1
        else:
            incorrectNeg += 1

    accuracy = (correctPos + correctNeg) / (correctPos + incorrectPos + correctNeg + incorrectNeg)

    print("Accuracy: " + str(accuracy))

    print("%15s | %15s | %15s" % (" ","Pos Review", "Neg Review"))
    print("----------------|-----------------|----------------")
    print("%15s | %15d | %15d" % ("Pos Class",correctPos,incorrectNeg))
    print("%15s | %15d | %15d" % ("Neg Class",incorrectPos,correctNeg))

calcClassificationAccuracy()
```

    Accuracy: 0.674
                    |      Pos Review |      Neg Review
    ----------------|-----------------|----------------
          Pos Class |             756 |             408
          Neg Class |             244 |             592

# QUESTION 2E (Advanced) 1 point

One can consider the Naive Bayes classifier a generative model that can generate binary feature vectors using the associated probabilities from the training data. The idea is similar to how we do direct sampling in
Bayesian Networks and depends on generating random number from a discrete distribution. Describe how you would generate random movie reviews consisting solely of the words from the dictionary using your model. Show 5 examples of randomly generated positive reviews and 5 examples of randomly generated negative reviews. Each example should consists of a subset of the words in the dictionary. Hint: use probabilities to generate both the presence and absence of a word

```python
# YOUR CODE GOES HERE

"""
Describe how you would generate random movie reviews consisting solely of the words
from the dictionary using your model.

Ans:
From the previous calculations, we have probabilities of a keyword being present
for both positive and negative reviews. To determine the subset of words in a randomly
generated review, we can generate a random number between 0 and 1, and if the word is
greater than the random number, we would add it to the sublist. If not, it would be absent.
We can do this using the probability of a word occuring for a positive or negative review,
depending on the type of review we want.
"""

import random

def generateReview(reviewType):
    probGivenNeg, probGivenPos, probNeg, probPos, neg_polarities, pos_polarities = parseFiles()
    keyWords = ["awful", "bad","boring","dull","effective","enjoyable","great","hilarious"]

    rands = np.random.rand(len(keyWords))

    if reviewType == "pos":
        words = [w for (i,w) in enumerate(keyWords) if probGivenPos[i]>rands[i]]
    else:
        words = [w for (i,w) in enumerate(keyWords) if probGivenNeg[i]>rands[i]]
    random.shuffle(words)
    return " ".join(words)

i = 0
while i < 5:
    review = generateReview("pos")
    if review == "":  # want sentences with at least 1 word
        continue
    print("Positive review #" + str(i) + ": " + str(review))
    i += 1

print()
i = 0
while i < 5:
    review = generateReview("neg")
    if review == "":  # want sentences with at least 1 word
        continue
    print("Negative review #" + str(i) + ": " + str(review))
    i += 1
```

    Positive review #0: great bad
    Positive review #1: hilarious
    Positive review #2: hilarious
    Positive review #3: hilarious great
    Positive review #4: great

    Negative review #0: great effective enjoyable awful
    Negative review #1: dull bad
    Negative review #2: bad dull
    Negative review #3: bad
    Negative review #4: bad

```python

```

# QUESTION 2F (ADVANCED) (CSC421 - 0 points, CSC581C - 2 points)

Check the associated README file and see what convention is used for the 10-fold cross-validation. Calculate the classification accuracy and confusion matrix using the recommended 10-fold cross-validation. Again do NOT use
ML libraries such as scikit-learn or tensorflow and just use numpy/scipy.

```python
# YOUR CODE GOES HERE
```


```python
from sympy.physics.mechanics import *
import sympy as sp
mechanics_printing(pretty_print=True)
```

```python
m1,m2,l01,l02,k1,k2 = sp.symbols(r'm_1,m_2,l_{0\,1},l_{0\,2},k_1,k_2')
t,g = sp.symbols('t,g')
x1, x2, y1, y2, z1, z2 = dynamicsymbols('x_1 x_2 y_1 y_2 z_1 z_2')
dx1, dx2, dy1, dy2, dz1, dz2 = dynamicsymbols('x_1 x_2 y_1 y_2 z_1 z_2', 1)
```

```python
V = m1*g*y1+m2*g*y2+sp.Rational(1,2)*k1*(sp.sqrt(x1**2+y1**2+z1**2)-l01)**2+sp.Rational(1,2)*k2*(sp.sqrt((x2-x1)**2+(y2-y1)**2+(z2-z1)**2)-l02)**2
T = sp.Rational(1,2)*m1*(dx1**2+dy1**2+dz1**2)+sp.Rational(1,2)*m2*(dx2**2+dy2**2+dz2**2)
L = sp.simplify(T - V)
```

```python
LM = LagrangesMethod(L, [x1, y1, z1, x2, y2, z2])
```

```python
soln = LM.form_lagranges_equations()
```

```python
solvedsoln = sp.solve((sp.Eq(soln[0]),sp.Eq(soln[1]),sp.Eq(soln[2]),sp.Eq(soln[3]),sp.Eq(soln[4]),sp.Eq(soln[5])),(x1.diff(t,t),y1.diff(t,t),z1.diff(t,t),x2.diff(t,t),y2.diff(t,t),z2.diff(t,t)))
```

```python

```

```python
sp.latex(solvedsoln)
```

    '\\left \\{ \\frac{d^{2}}{d t^{2}} \\operatorname{x_{1}}{\\left (t \\right )} : \\frac{k_{1} l_{0,1} \\operatorname{x_{1}}{\\left (t \\right )}}{m_{1} \\sqrt{\\operatorname{x_{1}}^{2}{\\left (t \\right )} + \\operatorname{y_{1}}^{2}{\\left (t \\right )} + \\operatorname{z_{1}}^{2}{\\left (t \\right )}}} - \\frac{k_{1} \\operatorname{x_{1}}{\\left (t \\right )}}{m_{1}} + \\frac{k_{2} l_{0,2} \\operatorname{x_{1}}{\\left (t \\right )}}{m_{1} \\sqrt{\\operatorname{x_{1}}^{2}{\\left (t \\right )} - 2 \\operatorname{x_{1}}{\\left (t \\right )} \\operatorname{x_{2}}{\\left (t \\right )} + \\operatorname{x_{2}}^{2}{\\left (t \\right )} + \\operatorname{y_{1}}^{2}{\\left (t \\right )} - 2 \\operatorname{y_{1}}{\\left (t \\right )} \\operatorname{y_{2}}{\\left (t \\right )} + \\operatorname{y_{2}}^{2}{\\left (t \\right )} + \\operatorname{z_{1}}^{2}{\\left (t \\right )} - 2 \\operatorname{z_{1}}{\\left (t \\right )} \\operatorname{z_{2}}{\\left (t \\right )} + \\operatorname{z_{2}}^{2}{\\left (t \\right )}}} - \\frac{k_{2} l_{0,2} \\operatorname{x_{2}}{\\left (t \\right )}}{m_{1} \\sqrt{\\operatorname{x_{1}}^{2}{\\left (t \\right )} - 2 \\operatorname{x_{1}}{\\left (t \\right )} \\operatorname{x_{2}}{\\left (t \\right )} + \\operatorname{x_{2}}^{2}{\\left (t \\right )} + \\operatorname{y_{1}}^{2}{\\left (t \\right )} - 2 \\operatorname{y_{1}}{\\left (t \\right )} \\operatorname{y_{2}}{\\left (t \\right )} + \\operatorname{y_{2}}^{2}{\\left (t \\right )} + \\operatorname{z_{1}}^{2}{\\left (t \\right )} - 2 \\operatorname{z_{1}}{\\left (t \\right )} \\operatorname{z_{2}}{\\left (t \\right )} + \\operatorname{z_{2}}^{2}{\\left (t \\right )}}} - \\frac{k_{2} \\operatorname{x_{1}}{\\left (t \\right )}}{m_{1}} + \\frac{k_{2} \\operatorname{x_{2}}{\\left (t \\right )}}{m_{1}}, \\quad \\frac{d^{2}}{d t^{2}} \\operatorname{x_{2}}{\\left (t \\right )} : - \\frac{k_{2} l_{0,2} \\operatorname{x_{1}}{\\left (t \\right )}}{m_{2} \\sqrt{\\operatorname{x_{1}}^{2}{\\left (t \\right )} - 2 \\operatorname{x_{1}}{\\left (t \\right )} \\operatorname{x_{2}}{\\left (t \\right )} + \\operatorname{x_{2}}^{2}{\\left (t \\right )} + \\operatorname{y_{1}}^{2}{\\left (t \\right )} - 2 \\operatorname{y_{1}}{\\left (t \\right )} \\operatorname{y_{2}}{\\left (t \\right )} + \\operatorname{y_{2}}^{2}{\\left (t \\right )} + \\operatorname{z_{1}}^{2}{\\left (t \\right )} - 2 \\operatorname{z_{1}}{\\left (t \\right )} \\operatorname{z_{2}}{\\left (t \\right )} + \\operatorname{z_{2}}^{2}{\\left (t \\right )}}} + \\frac{k_{2} l_{0,2} \\operatorname{x_{2}}{\\left (t \\right )}}{m_{2} \\sqrt{\\operatorname{x_{1}}^{2}{\\left (t \\right )} - 2 \\operatorname{x_{1}}{\\left (t \\right )} \\operatorname{x_{2}}{\\left (t \\right )} + \\operatorname{x_{2}}^{2}{\\left (t \\right )} + \\operatorname{y_{1}}^{2}{\\left (t \\right )} - 2 \\operatorname{y_{1}}{\\left (t \\right )} \\operatorname{y_{2}}{\\left (t \\right )} + \\operatorname{y_{2}}^{2}{\\left (t \\right )} + \\operatorname{z_{1}}^{2}{\\left (t \\right )} - 2 \\operatorname{z_{1}}{\\left (t \\right )} \\operatorname{z_{2}}{\\left (t \\right )} + \\operatorname{z_{2}}^{2}{\\left (t \\right )}}} + \\frac{k_{2} \\operatorname{x_{1}}{\\left (t \\right )}}{m_{2}} - \\frac{k_{2} \\operatorname{x_{2}}{\\left (t \\right )}}{m_{2}}, \\quad \\frac{d^{2}}{d t^{2}} \\operatorname{y_{1}}{\\left (t \\right )} : - g + \\frac{k_{1} l_{0,1} \\operatorname{y_{1}}{\\left (t \\right )}}{m_{1} \\sqrt{\\operatorname{x_{1}}^{2}{\\left (t \\right )} + \\operatorname{y_{1}}^{2}{\\left (t \\right )} + \\operatorname{z_{1}}^{2}{\\left (t \\right )}}} - \\frac{k_{1} \\operatorname{y_{1}}{\\left (t \\right )}}{m_{1}} + \\frac{k_{2} l_{0,2} \\operatorname{y_{1}}{\\left (t \\right )}}{m_{1} \\sqrt{\\operatorname{x_{1}}^{2}{\\left (t \\right )} - 2 \\operatorname{x_{1}}{\\left (t \\right )} \\operatorname{x_{2}}{\\left (t \\right )} + \\operatorname{x_{2}}^{2}{\\left (t \\right )} + \\operatorname{y_{1}}^{2}{\\left (t \\right )} - 2 \\operatorname{y_{1}}{\\left (t \\right )} \\operatorname{y_{2}}{\\left (t \\right )} + \\operatorname{y_{2}}^{2}{\\left (t \\right )} + \\operatorname{z_{1}}^{2}{\\left (t \\right )} - 2 \\operatorname{z_{1}}{\\left (t \\right )} \\operatorname{z_{2}}{\\left (t \\right )} + \\operatorname{z_{2}}^{2}{\\left (t \\right )}}} - \\frac{k_{2} l_{0,2} \\operatorname{y_{2}}{\\left (t \\right )}}{m_{1} \\sqrt{\\operatorname{x_{1}}^{2}{\\left (t \\right )} - 2 \\operatorname{x_{1}}{\\left (t \\right )} \\operatorname{x_{2}}{\\left (t \\right )} + \\operatorname{x_{2}}^{2}{\\left (t \\right )} + \\operatorname{y_{1}}^{2}{\\left (t \\right )} - 2 \\operatorname{y_{1}}{\\left (t \\right )} \\operatorname{y_{2}}{\\left (t \\right )} + \\operatorname{y_{2}}^{2}{\\left (t \\right )} + \\operatorname{z_{1}}^{2}{\\left (t \\right )} - 2 \\operatorname{z_{1}}{\\left (t \\right )} \\operatorname{z_{2}}{\\left (t \\right )} + \\operatorname{z_{2}}^{2}{\\left (t \\right )}}} - \\frac{k_{2} \\operatorname{y_{1}}{\\left (t \\right )}}{m_{1}} + \\frac{k_{2} \\operatorname{y_{2}}{\\left (t \\right )}}{m_{1}}, \\quad \\frac{d^{2}}{d t^{2}} \\operatorname{y_{2}}{\\left (t \\right )} : - g - \\frac{k_{2} l_{0,2} \\operatorname{y_{1}}{\\left (t \\right )}}{m_{2} \\sqrt{\\operatorname{x_{1}}^{2}{\\left (t \\right )} - 2 \\operatorname{x_{1}}{\\left (t \\right )} \\operatorname{x_{2}}{\\left (t \\right )} + \\operatorname{x_{2}}^{2}{\\left (t \\right )} + \\operatorname{y_{1}}^{2}{\\left (t \\right )} - 2 \\operatorname{y_{1}}{\\left (t \\right )} \\operatorname{y_{2}}{\\left (t \\right )} + \\operatorname{y_{2}}^{2}{\\left (t \\right )} + \\operatorname{z_{1}}^{2}{\\left (t \\right )} - 2 \\operatorname{z_{1}}{\\left (t \\right )} \\operatorname{z_{2}}{\\left (t \\right )} + \\operatorname{z_{2}}^{2}{\\left (t \\right )}}} + \\frac{k_{2} l_{0,2} \\operatorname{y_{2}}{\\left (t \\right )}}{m_{2} \\sqrt{\\operatorname{x_{1}}^{2}{\\left (t \\right )} - 2 \\operatorname{x_{1}}{\\left (t \\right )} \\operatorname{x_{2}}{\\left (t \\right )} + \\operatorname{x_{2}}^{2}{\\left (t \\right )} + \\operatorname{y_{1}}^{2}{\\left (t \\right )} - 2 \\operatorname{y_{1}}{\\left (t \\right )} \\operatorname{y_{2}}{\\left (t \\right )} + \\operatorname{y_{2}}^{2}{\\left (t \\right )} + \\operatorname{z_{1}}^{2}{\\left (t \\right )} - 2 \\operatorname{z_{1}}{\\left (t \\right )} \\operatorname{z_{2}}{\\left (t \\right )} + \\operatorname{z_{2}}^{2}{\\left (t \\right )}}} + \\frac{k_{2} \\operatorname{y_{1}}{\\left (t \\right )}}{m_{2}} - \\frac{k_{2} \\operatorname{y_{2}}{\\left (t \\right )}}{m_{2}}, \\quad \\frac{d^{2}}{d t^{2}} \\operatorname{z_{1}}{\\left (t \\right )} : \\frac{k_{1} l_{0,1} \\operatorname{z_{1}}{\\left (t \\right )}}{m_{1} \\sqrt{\\operatorname{x_{1}}^{2}{\\left (t \\right )} + \\operatorname{y_{1}}^{2}{\\left (t \\right )} + \\operatorname{z_{1}}^{2}{\\left (t \\right )}}} - \\frac{k_{1} \\operatorname{z_{1}}{\\left (t \\right )}}{m_{1}} + \\frac{k_{2} l_{0,2} \\operatorname{z_{1}}{\\left (t \\right )}}{m_{1} \\sqrt{\\operatorname{x_{1}}^{2}{\\left (t \\right )} - 2 \\operatorname{x_{1}}{\\left (t \\right )} \\operatorname{x_{2}}{\\left (t \\right )} + \\operatorname{x_{2}}^{2}{\\left (t \\right )} + \\operatorname{y_{1}}^{2}{\\left (t \\right )} - 2 \\operatorname{y_{1}}{\\left (t \\right )} \\operatorname{y_{2}}{\\left (t \\right )} + \\operatorname{y_{2}}^{2}{\\left (t \\right )} + \\operatorname{z_{1}}^{2}{\\left (t \\right )} - 2 \\operatorname{z_{1}}{\\left (t \\right )} \\operatorname{z_{2}}{\\left (t \\right )} + \\operatorname{z_{2}}^{2}{\\left (t \\right )}}} - \\frac{k_{2} l_{0,2} \\operatorname{z_{2}}{\\left (t \\right )}}{m_{1} \\sqrt{\\operatorname{x_{1}}^{2}{\\left (t \\right )} - 2 \\operatorname{x_{1}}{\\left (t \\right )} \\operatorname{x_{2}}{\\left (t \\right )} + \\operatorname{x_{2}}^{2}{\\left (t \\right )} + \\operatorname{y_{1}}^{2}{\\left (t \\right )} - 2 \\operatorname{y_{1}}{\\left (t \\right )} \\operatorname{y_{2}}{\\left (t \\right )} + \\operatorname{y_{2}}^{2}{\\left (t \\right )} + \\operatorname{z_{1}}^{2}{\\left (t \\right )} - 2 \\operatorname{z_{1}}{\\left (t \\right )} \\operatorname{z_{2}}{\\left (t \\right )} + \\operatorname{z_{2}}^{2}{\\left (t \\right )}}} - \\frac{k_{2} \\operatorname{z_{1}}{\\left (t \\right )}}{m_{1}} + \\frac{k_{2} \\operatorname{z_{2}}{\\left (t \\right )}}{m_{1}}, \\quad \\frac{d^{2}}{d t^{2}} \\operatorname{z_{2}}{\\left (t \\right )} : - \\frac{k_{2} l_{0,2} \\operatorname{z_{1}}{\\left (t \\right )}}{m_{2} \\sqrt{\\operatorname{x_{1}}^{2}{\\left (t \\right )} - 2 \\operatorname{x_{1}}{\\left (t \\right )} \\operatorname{x_{2}}{\\left (t \\right )} + \\operatorname{x_{2}}^{2}{\\left (t \\right )} + \\operatorname{y_{1}}^{2}{\\left (t \\right )} - 2 \\operatorname{y_{1}}{\\left (t \\right )} \\operatorname{y_{2}}{\\left (t \\right )} + \\operatorname{y_{2}}^{2}{\\left (t \\right )} + \\operatorname{z_{1}}^{2}{\\left (t \\right )} - 2 \\operatorname{z_{1}}{\\left (t \\right )} \\operatorname{z_{2}}{\\left (t \\right )} + \\operatorname{z_{2}}^{2}{\\left (t \\right )}}} + \\frac{k_{2} l_{0,2} \\operatorname{z_{2}}{\\left (t \\right )}}{m_{2} \\sqrt{\\operatorname{x_{1}}^{2}{\\left (t \\right )} - 2 \\operatorname{x_{1}}{\\left (t \\right )} \\operatorname{x_{2}}{\\left (t \\right )} + \\operatorname{x_{2}}^{2}{\\left (t \\right )} + \\operatorname{y_{1}}^{2}{\\left (t \\right )} - 2 \\operatorname{y_{1}}{\\left (t \\right )} \\operatorname{y_{2}}{\\left (t \\right )} + \\operatorname{y_{2}}^{2}{\\left (t \\right )} + \\operatorname{z_{1}}^{2}{\\left (t \\right )} - 2 \\operatorname{z_{1}}{\\left (t \\right )} \\operatorname{z_{2}}{\\left (t \\right )} + \\operatorname{z_{2}}^{2}{\\left (t \\right )}}} + \\frac{k_{2} \\operatorname{z_{1}}{\\left (t \\right )}}{m_{2}} - \\frac{k_{2} \\operatorname{z_{2}}{\\left (t \\right )}}{m_{2}}\\right \\}'


```
from sympy import init_session
init_session()
```

    IPython console for SymPy 0.7.6 (Python 2.7.6-64-bit) (ground types: python)

    These commands were executed:
    >>> from __future__ import division
    >>> from sympy import *
    >>> x, y, z, t = symbols('x y z t')
    >>> k, m, n = symbols('k m n', integer=True)
    >>> f, g, h = symbols('f g h', cls=Function)
    >>> init_printing()

    Documentation can be found at http://www.sympy.org

```
p,mu,x,k = symbols('p,mu,x,k')
```

```
f = p * mu * (  ( (mu * x)** (k -2)) / factorial(k - 2) ) * exp(-mu*x) + (1 -p) * mu * ( ( (mu *x )** (k -1)) / factorial(k -1) ) * exp(-mu*x)
```

```
f
```

```
F = integrate(f,x)
```

```
F
```

Check if F is correct by deriving F, then subtract that from the original function

```
diff(F,x)
```

```
eq = diff(F,x) - f
simplify(eq)t
```

```
simplify(F)
```

```

```