# Deep Learning Bootcamp November 2017, GPU Computing for Data Scientists

## 18  PyTorch NUMER.AI  Deep Learning Binary Classification using BCELoss

Web: https://www.meetup.com/Tel-Aviv-Deep-Learning-Bootcamp/events/241762893/

Notebooks:  On GitHub

*Shlomo Kashani*

# What consists a Numerai competition?
- Numerai provides payments based on the number of correctly predictted labels (LOGG_LOSS) in a data-set which changes every week.

- Two data-sets are provided: numerai_training_data.csv and numerai_tournament_data.csv

# Criteria
- On top of LOG_LOSS, they also measure:
* Consistency
* Originality
* Concordance

# PyTorch and Numerai

- This tutorial was written in order to demonstrate a **fully working** example of a PyTorch NN on a real world use case, namely a Binary Classification problem on the NumerAI data set. If you are interested in the sk-learn version of this problem please refer to: https://github.com/QuantScientist/deep-ml-meetups/tree/master/hacking-kaggle/python/numer-ai

- For the scientific foundation behind Binary Classification and Logistic Regression, refer to: https://github.com/QuantScientist/Deep-Learning-Boot-Camp/tree/master/Data-Science-Interviews-Book

- Every step, from reading the CSV into numpy arrays, converting to GPU based tensors, training and validation, are meant to aid newcomers in their first steps in PyTorch.

- Additionally, commonly used Kaggle metrics such as ROC_AUC and LOG_LOSS are logged and plotted both for the training set as well as for the validation set.

- Thus, the NN architecture is naive and by no means **optimized**. Hopefully, I will improve it over time and I am working on a second CNN based version of the same problem.

## Data
- Download from https://numer.ai/leaderboard

# PyTorch Imports

```
import torch
import sys
import torch
from torch.utils.data.dataset import Dataset
from torch.utils.data import DataLoader
from torchvision import transforms
from torch import nn
import torch.nn.functional as F
import torch.optim as optim
from torch.autograd import Variable

from sklearn import cross_validation
from sklearn import metrics
from sklearn.metrics import roc_auc_score, log_loss, roc_auc_score, roc_curve, auc
from sklearn.cross_validation import StratifiedKFold, ShuffleSplit, cross_val_score, train_test_split

print('__Python VERSION:', sys.version)
print('__pyTorch VERSION:', torch.__version__)
print('__CUDA VERSION')
from subprocess import call
# call(["nvcc", "--version"]) does not work
! nvcc --version
print('__CUDNN VERSION:', torch.backends.cudnn.version())
print('__Number CUDA Devices:', torch.cuda.device_count())
print('__Devices')
# call(["nvidia-smi", "--format=csv", "--query-gpu=index,name,driver_version,memory.total,memory.used,memory.free"])
print('Active CUDA Device: GPU', torch.cuda.current_device())

print ('Available devices ', torch.cuda.device_count())
print ('Current cuda device ', torch.cuda.current_device())

import numpy
import numpy as np

use_cuda = torch.cuda.is_available()
FloatTensor = torch.cuda.FloatTensor if use_cuda else torch.FloatTensor
LongTensor = torch.cuda.LongTensor if use_cuda else torch.LongTensor
Tensor = FloatTensor

import pandas
import pandas as pd

import logging
handler=logging.basicConfig(level=logging.INFO)
lgr = logging.getLogger(__name__)
%matplotlib inline

# !pip install psutil
import psutil
import os
def cpuStats():
        print(sys.version)
        print(psutil.cpu_percent())
        print(psutil.virtual_memory())  # physical memory usage
        pid = os.getpid()
        py = psutil.Process(pid)
        memoryUse = py.memory_info()[0] / 2. ** 30  # memory use in GB...I think
        print('memory GB:', memoryUse)

cpuStats()
```

    ('__Python VERSION:', '2.7.12 (default, Nov 19 2016, 06:48:10) \n[GCC 5.4.0 20160609]')
    ('__pyTorch VERSION:', '0.2.0_2')
    __CUDA VERSION
    nvcc: NVIDIA (R) Cuda compiler driver
    Copyright (c) 2005-2016 NVIDIA Corporation
    Built on Tue_Jan_10_13:22:03_CST_2017
    Cuda compilation tools, release 8.0, V8.0.61

    /usr/local/lib/python2.7/dist-packages/sklearn/cross_validation.py:44: DeprecationWarning: This module was deprecated in version 0.18 in favor of the model_selection module into which all the refactored classes and functions are moved. Also note that the interface of the new CV iterators are different from that of this module. This module will be removed in 0.20.
      "This module will be removed in 0.20.", DeprecationWarning)

    ('__CUDNN VERSION:', 6021)
    ('__Number CUDA Devices:', 1L)
    __Devices
    ('Active CUDA Device: GPU', 0L)
    ('Available devices ', 1L)
    ('Current cuda device ', 0L)
    2.7.12 (default, Nov 19 2016, 06:48:10)
    [GCC 5.4.0 20160609]
    26.7
    svmem(total=12596768768, available=9989754880, percent=20.7, used=2275368960, free=8471945216, active=3051741184, inactive=630337536, buffers=330362880, cached=1519091712, shared=13893632)
    ('memory GB:', 0.17356491088867188)

#  CUDA

```
# %%timeit
use_cuda = torch.cuda.is_available()
# use_cuda = False

FloatTensor = torch.cuda.FloatTensor if use_cuda else torch.FloatTensor
LongTensor = torch.cuda.LongTensor if use_cuda else torch.LongTensor
Tensor = FloatTensor

lgr.info("USE CUDA=" + str (use_cuda))

torch.backends.cudnn.benchmark = True

# ! watch -n 0.1 'ps f -o user,pgrp,pid,pcpu,pmem,start,time,command -p `lsof -n -w -t /dev/nvidia*`'
# sudo apt-get install dstat #install dstat
# sudo pip install nvidia-ml-py #install Python NVIDIA Management Library
# wget https://raw.githubusercontent.com/datumbox/dstat/master/plugins/dstat_nvidia_gpu.py
# sudo mv dstat_nvidia_gpu.py /usr/share/dstat/ #move file to the plugins directory of dstat
```

    INFO:__main__:USE CUDA=True

#  Global params

```
# Data params
TARGET_VAR= 'target'
TOURNAMENT_DATA_CSV = 'numerai_tournament_data.csv'
TRAINING_DATA_CSV = 'numerai_training_data.csv'
BASE_FOLDER = 'numerai/'

# fix seed
seed=17*19
np.random.seed(seed)
torch.manual_seed(seed)
if use_cuda:
    torch.cuda.manual_seed(seed)
```

#  Load a CSV file for Binary classification (numpy)

As mentioned, NumerAI provided **numerai_training_data.csv** and **numerai_tournament_data.csv.**

- Training_data.csv is labeled
- Numerai_tournament_data.csv has lebles for the **validation set** and no labels for the **test set**. See belo how I seperate them.

```
# %%timeit
df_train = pd.read_csv(BASE_FOLDER + TRAINING_DATA_CSV)
df_train.head(5)
```

      id
      era
      data_type
      feature1
      feature2
      feature3
      feature4
      feature5
      feature6
      feature7
      ...
      feature13
      feature14
      feature15
      feature16
      feature17
      feature18
      feature19
      feature20
      feature21
      target

      0
      72774
      era1
      train
      0.48937
      0.56969
      0.59150
      0.46432
      0.42291
      0.49616
      0.53542
      ...
      0.42195
      0.62651
      0.51604
      0.42938
      0.56744
      0.60008
      0.46966
      0.50322
      0.42803
      1

      1
      140123
      era1
      train
      0.57142
      0.43408
      0.58771
      0.44570
      0.41471
      0.49137
      0.52791
      ...
      0.46301
      0.55103
      0.39053
      0.48856
      0.54305
      0.59213
      0.44935
      0.56685
      0.59645
      1

      2
      46882
      era1
      train
      0.75694
      0.59942
      0.36154
      0.65571
      0.60520
      0.45317
      0.49847
      ...
      0.68057
      0.43763
      0.46322
      0.63211
      0.32947
      0.35632
      0.56316
      0.33888
      0.40120
      0

      3
      20833
      era1
      train
      0.46059
      0.50856
      0.64215
      0.41382
      0.39550
      0.49282
      0.54697
      ...
      0.38108
      0.65446
      0.54926
      0.36297
      0.61482
      0.64292
      0.52910
      0.53582
      0.47027
      0

      4
      5381
      era1
      train
      0.61195
      0.66684
      0.45877
      0.56730
      0.51889
      0.41257
      0.56030
      ...
      0.54803
      0.59120
      0.58160
      0.51828
      0.43870
      0.47011
      0.56007
      0.36374
      0.31552
      1

5 rows × 25 columns

#  Feature enrichement
- This would be usually not required when using NN's; it is here for demonstration purposes.

```
from sklearn.preprocessing import LabelEncoder
from sklearn.pipeline import Pipeline
from collections import defaultdict

# def genBasicFeatures(inDF):
#     print('Generating basic features ...')
#     df_copy=inDF.copy(deep=True)
#     magicNumber=21
#     feature_cols = list(inDF.columns)

#     inDF['x_mean'] = np.mean(df_copy.ix[:, 0:magicNumber], axis=1)
#     inDF['x_median'] = np.median(df_copy.ix[:, 0:magicNumber], axis=1)
#     inDF['x_std'] = np.std(df_copy.ix[:, 0:magicNumber], axis=1)
#     inDF['x_skew'] = scipy.stats.skew(df_copy.ix[:, 0:magicNumber], axis=1)
#     inDF['x_kurt'] = scipy.stats.kurtosis(df_copy.ix[:, 0:magicNumber], axis=1)
#     inDF['x_var'] = np.var(df_copy.ix[:, 0:magicNumber], axis=1)
#     inDF['x_max'] = np.max(df_copy.ix[:, 0:magicNumber], axis=1)
#     inDF['x_min'] = np.min(df_copy.ix[:, 0:magicNumber], axis=1)

#     return inDF

def addPolyFeatures(inDF, deg=2):
    print('Generating poly features ...')
    df_copy=inDF.copy(deep=True)
    poly=PolynomialFeatures(degree=deg)
    p_testX = poly.fit(df_copy)
    # AttributeError: 'PolynomialFeatures' object has no attribute 'get_feature_names'
    target_feature_names = ['x'.join(['{}^{}'.format(pair[0],pair[1]) for pair in tuple if pair[1]!=0]) for tuple in [zip(df_copy.columns,p) for p in poly.powers_]]
    df_copy = pd.DataFrame(p_testX.transform(df_copy),columns=target_feature_names)

    return df_copy

def oneHOT(inDF):
    d = defaultdict(LabelEncoder)
    X_df=inDF.copy(deep=True)
    # Encoding the variable
    X_df = X_df.apply(lambda x: d['era'].fit_transform(x))

    return X_df

```

#  Train / Validation / Test Split
- Numerai provides a data set that is allready split into train, validation and test sets.

```
from sklearn import preprocessing

# Train, Validation, Test Split
def loadDataSplit():
    df_train = pd.read_csv(BASE_FOLDER + TRAINING_DATA_CSV)
    # TOURNAMENT_DATA_CSV has both validation and test data provided by NumerAI
    df_test_valid = pd.read_csv(BASE_FOLDER + TOURNAMENT_DATA_CSV)

    answers_1_SINGLE = df_train[TARGET_VAR]
    df_train.drop(TARGET_VAR, axis=1,inplace=True)
    df_train.drop('id', axis=1,inplace=True)
    df_train.drop('era', axis=1,inplace=True)
    df_train.drop('data_type', axis=1,inplace=True)

#     df_train=oneHOT(df_train)

    df_train.to_csv(BASE_FOLDER + TRAINING_DATA_CSV + 'clean.csv', header=False,  index = False)
    df_train= pd.read_csv(BASE_FOLDER + TRAINING_DATA_CSV + 'clean.csv', header=None, dtype=np.float32)
    df_train = pd.concat([df_train, answers_1_SINGLE], axis=1)
    feature_cols = list(df_train.columns[:-1])
#     print (feature_cols)
    target_col = df_train.columns[-1]
    trainX, trainY = df_train[feature_cols], df_train[target_col]

    # TOURNAMENT_DATA_CSV has both validation and test data provided by NumerAI
    # Validation set
    df_validation_set=df_test_valid.loc[df_test_valid['data_type'] == 'validation']
    df_validation_set=df_validation_set.copy(deep=True)
    answers_1_SINGLE_validation = df_validation_set[TARGET_VAR]
    df_validation_set.drop(TARGET_VAR, axis=1,inplace=True)
    df_validation_set.drop('id', axis=1,inplace=True)
    df_validation_set.drop('era', axis=1,inplace=True)
    df_validation_set.drop('data_type', axis=1,inplace=True)

#     df_validation_set=oneHOT(df_validation_set)

    df_validation_set.to_csv(BASE_FOLDER + TRAINING_DATA_CSV + '-validation-clean.csv', header=False,  index = False)
    df_validation_set= pd.read_csv(BASE_FOLDER + TRAINING_DATA_CSV + '-validation-clean.csv', header=None, dtype=np.float32)
    df_validation_set = pd.concat([df_validation_set, answers_1_SINGLE_validation], axis=1)
    feature_cols = list(df_validation_set.columns[:-1])

    target_col = df_validation_set.columns[-1]
    valX, valY = df_validation_set[feature_cols], df_validation_set[target_col]

    # Test set for submission (not labeled)
    df_test_set = pd.read_csv(BASE_FOLDER + TOURNAMENT_DATA_CSV)
#     df_test_set=df_test_set.loc[df_test_valid['data_type'] == 'live']
    df_test_set=df_test_set.copy(deep=True)
    df_test_set.drop(TARGET_VAR, axis=1,inplace=True)
    tid_1_SINGLE = df_test_set['id']
    df_test_set.drop('id', axis=1,inplace=True)
    df_test_set.drop('era', axis=1,inplace=True)
    df_test_set.drop('data_type', axis=1,inplace=True)

#     df_test_set=oneHOT(df_validation_set)

    feature_cols = list(df_test_set.columns) # must be run here, we dont want the ID
#     print (feature_cols)
    df_test_set = pd.concat([tid_1_SINGLE, df_test_set], axis=1)
    testX = df_test_set[feature_cols].values

    return trainX, trainY, valX, valY, testX, df_test_set
```

```
# %%timeit
trainX, trainY, valX, valY, testX, df_test_set = loadDataSplit()

min_max_scaler = preprocessing.MinMaxScaler()

# # Number of features for the input layer
N_FEATURES=trainX.shape[1]
print (trainX.shape)
print (trainY.shape)
print (valX.shape)
print (valY.shape)
print (testX.shape)
print (df_test_set.shape)

# print (trainX)
```

    (108405, 21)
    (108405,)
    (16686, 21)
    (16686,)
    (45668, 21)
    (45668, 22)

#  Correlated columns
- Correlation plot
- Scatter plots

```
# seperate out the Categorical and Numerical features
import seaborn as sns

numerical_feature=trainX.dtypes[trainX.dtypes!= 'object'].index
categorical_feature=trainX.dtypes[trainX.dtypes== 'object'].index

print ("There are {} numeric and {} categorical columns in train data".format(numerical_feature.shape[0],categorical_feature.shape[0]))

corr=trainX[numerical_feature].corr()
sns.heatmap(corr)

```

```
from pandas import *
import numpy as np
from scipy.stats.stats import pearsonr
import itertools

# from https://stackoverflow.com/questions/17778394/list-highest-correlation-pairs-from-a-large-correlation-matrix-in-pandas
def get_redundant_pairs(df):
    '''Get diagonal and lower triangular pairs of correlation matrix'''
    pairs_to_drop = set()
    cols = df.columns
    for i in range(0, df.shape[1]):
        for j in range(0, i+1):
            pairs_to_drop.add((cols[i], cols[j]))
    return pairs_to_drop

def get_top_abs_correlations(df, n=5):
    au_corr = df.corr().abs().unstack()
    labels_to_drop = get_redundant_pairs(df)
    au_corr = au_corr.drop(labels=labels_to_drop).sort_values(ascending=False)
    return au_corr[0:n]

print("Top Absolute Correlations")
print(get_top_abs_correlations(trainX, 5))
```

    Top Absolute Correlations
    10  17    0.998092
    12  16    0.996368
    2   17    0.992738
        10    0.987903
        16    0.982656
    dtype: float64

#  Create PyTorch GPU tensors from numpy arrays

- Note how we transfrom the np arrays

```
# Convert the np arrays into the correct dimention and type
# Note that BCEloss requires Float in X as well as in y
def XnumpyToTensor(x_data_np):
    x_data_np = np.array(x_data_np, dtype=np.float32)
    print(x_data_np.shape)
    print(type(x_data_np))

    if use_cuda:
        lgr.info ("Using the GPU")
        X_tensor = Variable(torch.from_numpy(x_data_np).cuda()) # Note the conversion for pytorch
    else:
        lgr.info ("Using the CPU")
        X_tensor = Variable(torch.from_numpy(x_data_np)) # Note the conversion for pytorch

    print(type(X_tensor.data)) # should be 'torch.cuda.FloatTensor'
    print(x_data_np.shape)
    print(type(x_data_np))
    return X_tensor

# Convert the np arrays into the correct dimention and type
# Note that BCEloss requires Float in X as well as in y
def YnumpyToTensor(y_data_np):
    y_data_np=y_data_np.reshape((y_data_np.shape[0],1)) # Must be reshaped for PyTorch!
    print(y_data_np.shape)
    print(type(y_data_np))

    if use_cuda:
        lgr.info ("Using the GPU")
    #     Y = Variable(torch.from_numpy(y_data_np).type(torch.LongTensor).cuda())
        Y_tensor = Variable(torch.from_numpy(y_data_np)).type(torch.FloatTensor).cuda()  # BCEloss requires Float
    else:
        lgr.info ("Using the CPU")
    #     Y = Variable(torch.squeeze (torch.from_numpy(y_data_np).type(torch.LongTensor)))  #
        Y_tensor = Variable(torch.from_numpy(y_data_np)).type(torch.FloatTensor)  # BCEloss requires Float

    print(type(Y_tensor.data)) # should be 'torch.cuda.FloatTensor'
    print(y_data_np.shape)
    print(type(y_data_np))
    return Y_tensor
```

# The NN model

### MLP model
- A multilayer perceptron is a logistic regressor where instead of feeding the input to the logistic regression you insert a intermediate layer, called the hidden layer, that has a nonlinear activation function (usually tanh or sigmoid) . One can use many such hidden layers making the architecture deep.

- Here we define a simple MLP structure. We map the input feature vector to a higher space, then later gradually decrease the dimension, and in the end into a 1-dimension space. Because we are calculating the probability of each genre independently, after the final layer we need to use a sigmoid layer.

###  Initial weights selection

- There are many ways to select the initial weights to a neural network architecture. A common initialization scheme is random initialization, which sets the biases and weights of all the nodes in each hidden layer randomly.

- Before starting the training process, an initial value is assigned to each variable. This is done by pure randomness, using for example a uniform or Gaussian distribution. But if we start with weights that are too small, the signal could decrease so much that it is too small to be useful. On the other side, when the parameters are initialized with high values, the signal can end up to explode while propagating through the network.

- In consequence, a good initialization can have a radical effect on how fast the network will learn useful patterns.For this purpose, some best practices have been developed. One famous example used is **Xavier initialization**. Its formulation is based on the number of input and output neurons and uses sampling from a uniform distribution with zero mean and all biases set to zero.

- In effect (according to theory) initializing the weights of the network to values that would be closer to the optimal, and therefore require less epochs to train.

### References:
* **`nninit.xavier_uniform(tensor, gain=1)`** - Fills `tensor` with values according to the method described in ["Understanding the difficulty of training deep feedforward neural networks" - Glorot, X. and Bengio, Y.](http://jmlr.org/proceedings/papers/v9/glorot10a/glorot10a.pdf), using a uniform distribution.
* **`nninit.xavier_normal(tensor, gain=1)`** - Fills `tensor` with values according to the method described in ["Understanding the difficulty of training deep feedforward neural networks" - Glorot, X. and Bengio, Y.](http://jmlr.org/proceedings/papers/v9/glorot10a/glorot10a.pdf), using a normal distribution.
* **`nninit.kaiming_uniform(tensor, gain=1)`** - Fills `tensor` with values according to the method described in ["Delving deep into rectifiers: Surpassing human-level performance on ImageNet classification" - He, K. et al.](https://arxiv.org/abs/1502.01852) using a uniform distribution.
* **`nninit.kaiming_normal(tensor, gain=1)`** - Fills `tensor` with values according to the method described in ["Delving deep into rectifiers: Surpassing human-level performance on ImageNet classification" - He, K. et al.]

```
# p is the probability of being dropped in PyTorch
# NN params
DROPOUT_PROB = 0.65

LR = 0.005
MOMENTUM= 0.9
dropout = torch.nn.Dropout(p=1 - (DROPOUT_PROB))
sigmoid = torch.nn.Sigmoid()
tanh=torch.nn.Tanh()
relu=torch.nn.LeakyReLU()

lgr.info(dropout)

hiddenLayer1Size=256
hiddenLayer2Size=int(hiddenLayer1Size/2)
hiddenLayer3Size=int(hiddenLayer1Size/2)
hiddenLayer4Size=int(hiddenLayer1Size/2)

linear1=torch.nn.Linear(N_FEATURES, hiddenLayer1Size, bias=True)
torch.nn.init.xavier_uniform(linear1.weight)

linear2=torch.nn.Linear(hiddenLayer1Size, hiddenLayer2Size)
torch.nn.init.xavier_uniform(linear2.weight)

linear3=torch.nn.Linear(hiddenLayer2Size,1)
torch.nn.init.xavier_uniform(linear3.weight)

net = torch.nn.Sequential(linear1,nn.BatchNorm1d(hiddenLayer1Size),dropout,relu,
                          linear2,nn.BatchNorm1d(hiddenLayer2Size),dropout,relu,
                          linear3,dropout,sigmoid,
                          )
lgr.info(net)  # net architecture
```

    INFO:__main__:Dropout (p = 0.35)
    INFO:__main__:Sequential (
      (0): Linear (21 -> 256)
      (1): BatchNorm1d(256, eps=1e-05, momentum=0.1, affine=True)
      (2): Dropout (p = 0.35)
      (3): LeakyReLU (0.01)
      (4): Linear (256 -> 128)
      (5): BatchNorm1d(128, eps=1e-05, momentum=0.1, affine=True)
      (6): Dropout (p = 0.35)
      (7): LeakyReLU (0.01)
      (8): Linear (128 -> 1)
      (9): Dropout (p = 0.35)
      (10): Sigmoid ()
    )

# The cross-entropy loss function

A binary cross-entropy `Criterion` (which expects 0 or 1 valued targets) :

```python
criterion = torch.nn.BCELoss()
```

The BCE loss is defined as :

```
# ! pip install sympy
import sympy as sp
sp.interactive.printing.init_printing(use_latex=True)
from IPython.display import display, Math, Latex
maths = lambda s: display(Math(s))
latex = lambda s: display(Latex(s))

#the loss function is as follows:
maths("\mathbf{Loss Function:} J(x, z) = -\sum_k^d[x_k \log z_k + (1-x_k)log(1-z_k)]")
```

$$\mathbf{Loss Function:} J(x, z) = -\sum_k^d[x_k \log z_k + (1-x_k)log(1-z_k)]$$

```
# optimizer = torch.optim.SGD(net.parameters(), lr=0.02)
# optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9)
# optimizer = optim.SGD(net.parameters(), lr=LR, momentum=MOMENTUM, weight_decay=5e-3)
#L2 regularization can easily be added to the entire model via the optimizer
optimizer = torch.optim.Adam(net.parameters(), lr=LR,weight_decay=5e-2) #  L2 regularization

loss_func=torch.nn.BCELoss() # Binary cross entropy: http://pytorch.org/docs/nn.html#bceloss
# http://andersonjo.github.io/artificial-intelligence/2017/01/07/Cost-Functions/

if use_cuda:
    lgr.info ("Using the GPU")
    net.cuda()
    loss_func.cuda()
#     cudnn.benchmark = True

lgr.info (optimizer)
lgr.info (loss_func)
```

    INFO:__main__:Using the GPU
    INFO:__main__:<torch.optim.adam.Adam object at 0x7f37aa992dd0>
    INFO:__main__:BCELoss (
    )

#  Training in batches  + Measuring the performance of the deep learning model

```
import time
start_time = time.time()
epochs=160 # change to 400 for better results
div_factor=20
all_losses = []
loss_arr =[]

X_tensor_train= XnumpyToTensor(trainX)
Y_tensor_train= YnumpyToTensor(trainY)
print(type(X_tensor_train.data), type(Y_tensor_train.data)) # should be 'torch.cuda.FloatTensor'

# CUDNN_STATUS_NOT_SUPPORTED. This error may appear if you passed in a non-contiguous input.
# X_tensor_train=X_tensor_train.contiguous()
# Y_tensor_train=Y_tensor_train.contiguous()

# dataset = TensorDataset(data_tensor = X_tensor_train,target_tensor = Y_tensor_train)
# loader = DataLoader(dataset=dataset, batch_size=batch_size, shuffle=True)

# From here onwards, we must only use PyTorch Tensors
for step in range(epochs):
    out = net(X_tensor_train)                 # input x and predict based on x
    cost = loss_func(out, Y_tensor_train)     # must be (1. nn output, 2. target), the target label is NOT one-hotted

    optimizer.zero_grad()   # clear gradients for next train
    cost.backward()         # backpropagation, compute gradients
    optimizer.step()        # apply gradients

    if step % div_factor == 0:
        loss = cost.data[0]
        all_losses.append(loss)
        print(step, cost.data.cpu().numpy())
        # RuntimeError: can't convert CUDA tensor to numpy (it doesn't support GPU arrays).
        # Use .cpu() to move the tensor to host memory first.
        prediction = (net(X_tensor_train).data).float() # probabilities
#         prediction = (net(X_tensor).data > 0.5).float() # zero or one
#         print ("Pred:" + str (prediction)) # Pred:Variable containing: 0 or 1
#         pred_y = prediction.data.numpy().squeeze()
        pred_y = prediction.cpu().numpy().squeeze()
        target_y = Y_tensor_train.cpu().data.numpy()

        tu = (log_loss(target_y, pred_y),roc_auc_score(target_y,pred_y ))
        print ('LOG_LOSS={}, ROC_AUC={} '.format(*tu))

        loss_arr.append(cost.cpu().data.numpy()[0])

end_time = time.time()
print ('{} {:6.3f} seconds'.format('GPU:', end_time-start_time))

%matplotlib inline
import matplotlib.pyplot as plt
plt.plot(all_losses)
plt.show()

false_positive_rate, true_positive_rate, thresholds = roc_curve(target_y,pred_y)
roc_auc = auc(false_positive_rate, true_positive_rate)

plt.title('LOG_LOSS=' + str(log_loss(target_y, pred_y)))
plt.plot(false_positive_rate, true_positive_rate, 'b', label='AUC = %0.6f' % roc_auc)
plt.legend(loc='lower right')
plt.plot([0, 1], [0, 1], 'r--')
plt.xlim([-0.1, 1.2])
plt.ylim([-0.1, 1.2])
plt.ylabel('True Positive Rate')
plt.xlabel('False Positive Rate')
plt.show()
```

#  Visualize Loss Graph using Visdom
- Make sure you have Visdom installed and running
- pip install visdom
- python -m visdom.server &

```
# ! pip install visdom

from visdom import Visdom
viz = Visdom()

num_epoch=int(epochs/div_factor)

x = np.reshape([i for i in range(num_epoch)],newshape=[num_epoch,1])
loss_data = np.reshape(loss_arr,newshape=[num_epoch,1])

win3=viz.line(
    X = x,
    Y = loss_data,
    opts=dict(
        xtickmin=0,
        xtickmax=num_epoch,
        xtickstep=1,
        ytickmin=0,
        ytickmax=20,
        ytickstep=1,
        markercolor=np.random.randint(0, 255, num_epoch),
    ),
)
```

    Exception in user code:
    ------------------------------------------------------------

    Traceback (most recent call last):
      File "/usr/local/lib/python2.7/dist-packages/visdom/__init__.py", line 262, in _send
        data=json.dumps(msg),
      File "/usr/lib/python2.7/dist-packages/requests/api.py", line 107, in post
        return request('post', url, data=data, json=json, **kwargs)
      File "/usr/lib/python2.7/dist-packages/requests/api.py", line 53, in request
        return session.request(method=method, url=url, **kwargs)
      File "/usr/lib/python2.7/dist-packages/requests/sessions.py", line 468, in request
        resp = self.send(prep, **send_kwargs)
      File "/usr/lib/python2.7/dist-packages/requests/sessions.py", line 576, in send
        r = adapter.send(request, **kwargs)
      File "/usr/lib/python2.7/dist-packages/requests/adapters.py", line 437, in send
        raise ConnectionError(e, request=request)
    ConnectionError: HTTPConnectionPool(host='localhost', port=8097): Max retries exceeded with url: /events (Caused by NewConnectionError('<requests.packages.urllib3.connection.HTTPConnection object at 0x7f37b0dc2d10>: Failed to establish a new connection: [Errno 111] Connection refused',))

#  Performance of the deep learning model on the Validation set

```
net.eval()
# Validation data
print (valX.shape)
print (valY.shape)

X_tensor_val= XnumpyToTensor(valX)
Y_tensor_val= YnumpyToTensor(valY)

print(type(X_tensor_val.data), type(Y_tensor_val.data)) # should be 'torch.cuda.FloatTensor'

predicted_val = (net(X_tensor_val).data).float() # probabilities
# predicted_val = (net(X_tensor_val).data > 0.5).float() # zero or one
pred_y = predicted_val.cpu().numpy()
target_y = Y_tensor_val.cpu().data.numpy()

print (type(pred_y))
print (type(target_y))

tu = (log_loss(target_y, pred_y),roc_auc_score(target_y,pred_y ))
print ('\n')
print ('log_loss={} roc_auc={} '.format(*tu))

false_positive_rate, true_positive_rate, thresholds = roc_curve(target_y,pred_y)
roc_auc = auc(false_positive_rate, true_positive_rate)

plt.title('LOG_LOSS=' + str(log_loss(target_y, pred_y)))
plt.plot(false_positive_rate, true_positive_rate, 'b', label='AUC = %0.6f' % roc_auc)
plt.legend(loc='lower right')
plt.plot([0, 1], [0, 1], 'r--')
plt.xlim([-0.1, 1.2])
plt.ylim([-0.1, 1.2])
plt.ylabel('True Positive Rate')
plt.xlabel('False Positive Rate')
plt.show()

# print (pred_y)
```

#  Submission on Test set

```
# testX, df_test_set
# df[df.columns.difference(['b'])]
# trainX, trainY, valX, valY, testX, df_test_set = loadDataSplit()

print (df_test_set.shape)
columns = ['id', 'probability']
df_pred=pd.DataFrame(data=np.zeros((0,len(columns))), columns=columns)
# df_pred.id.astype(int)

for index, row in df_test_set.iterrows():
    rwo_no_id=row.drop('id')
#     print (rwo_no_id.values)
    x_data_np = np.array(rwo_no_id.values, dtype=np.float32)
    if use_cuda:
        X_tensor_test = Variable(torch.from_numpy(x_data_np).cuda()) # Note the conversion for pytorch
    else:
        X_tensor_test = Variable(torch.from_numpy(x_data_np)) # Note the conversion for pytorch

    X_tensor_test=X_tensor_test.view(1, trainX.shape[1]) # does not work with 1d tensors
    predicted_val = (net(X_tensor_test).data).float() # probabilities
    p_test =   predicted_val.cpu().numpy().item() # otherwise we get an array, we need a single float

    df_pred = df_pred.append({'id':row['id'], 'probability':p_test},ignore_index=True)
#     df_pred = df_pred.append({'id':row['id'].astype(int), 'probability':p_test},ignore_index=True)

df_pred.head(5)
```

    (349053, 51)

      id
      probability

      0
      9403af5f3de44039
      0.498911

      1
      67d84c8e966047d9
      0.500971

      2
      12f1aa03af504e19
      0.482130

      3
      e003f1be931040fa
      0.527270

      4
      550cac2470fe4644
      0.501118

#  Create a CSV with the ID's and the coresponding probabilities.

```
# df_pred.id=df_pred.id.astype(int)

def savePred(df_pred, loss):
#     csv_path = 'pred/p_{}_{}_{}.csv'.format(loss, name, (str(time.time())))
    csv_path = 'pred/pred_{}_{}.csv'.format(loss, (str(time.time())))
    df_pred.to_csv(csv_path, columns=('id', 'probability'), index=None)
    print (csv_path)

savePred (df_pred, log_loss(target_y, pred_y))
```

    pred/pred_0.691767700923_1505839265.61.csv

#  Actual score on Numer.ai - screenshot of the leader board

```

```

```

```


# VinDsl.jl: Fast and furious statistical modeling

John Pearson
P[λ]ab
Duke Institute for Brain Sciences

# Following along
VinDsl currently makes use of some features of Distributions.jl that are not yet available on master, as well as the latest release of PDMats.jl. You will need to checkout the `jp/autodiff` branch of Distributions:
```julia
Pkg.clone("https://github.com/jmxpearson/VinDsl.jl")
Pkg.update()  # if you don't have latest Distributions
Pkg.checkout("Distributions", "jp/autodiff")
```

# About me

- computational neuroscience lab at Duke
- using Julia for about a year
- member of JuliaStats organization

# Our problem

By AKS.9955, via Wikimedia Commons

https://praneethnamburi.wordpress.com/2015/02/05/simulating-neural-spike-trains/

# Our problem

- model responses to known features (like GLM)
- infer latent features
- do this scalably for large populations of neurons
- use (generative) Bayesian models that account for uncertainty

# One solution: Sampling

- Markov Chain Monte Carlo (MCMC) methods come with guarantees about correctness
- lots of packages (Lora.jl, Mamba.jl, Stan.jl)
- **But** sampling does not scale well to millions of observations and parameters

# Another solution: The Max Power Way

# Variational Bayesian Inference (VB)

Generative model for data: $p(y|\theta)p(\theta)$
Actual posterior: $p(\theta|y) = p(y|\theta)p(\theta)/p(y)$
Approximate posterior: $q(\theta)$

Maximize **E**vidence **L**ower **Bo**und (ELBO) wrt $\theta$:

$$
\mathcal{L} = \log p(y) \ge -KL\left(q \middle\| p\right) = \mathbb{E}_q[\log p(y|\theta)] + \mathcal{H}[q]
$$

- $\mathbb{E}_q[\log p(y|\theta)]$ measures goodness of fit (data likely under approximate posterior)
- $\mathcal{H}[q]$ is the entropy (favors less certain models)

# Why VB?

- Scales well
- Can use well-studied optimization techniques

# Drawbacks:
- !@$*&# hard to code
- Can't quickly spec out a model like with Stan or JAGS/BUGS
- Traditionally, requires that distributions be conjugate, requires doing lots of algebra

# But VB is exploding!

- stochastic variational inference (SVI): [Hoffman et al.](http://dl.acm.org/citation.cfm?id=2502622)
- black box variational inference (BBVI): [Ranganath et al.](http://arxiv.org/abs/1401.0118)
- control variates: [Paisley et al.](http://arxiv.org/abs/1206.6430)
- local expectation gradients (LEG): [Titsias and Lázaro-Gredilla](http://papers.nips.cc/paper/5678-local-expectation-gradients-for-black-box-variational-inference)
- neural variational inference (NVIL): [Mnih and Gregor](http://arxiv.org/abs/1402.0030)
- variational autoencoders [Kingma and Welling](http://arxiv.org/abs/1312.6114), [Rezende et al.](http://arxiv.org/abs/1401.4082)

# What's out there

- research code: individual algorithms, proof of concept
- [Stan](http://mc-stan.org/)
    - **Pros**: Robust, actively developed, good with stats, accessible from Julia
    - **Cons**: variational methods still experimental, C++

- [Edward](https://github.com/blei-lab/edward)
    - **Pros**: Python, multiple backends, from Stan and VB core developers
    - **Cons**: Python, very new

# What's out there
- [Theano](http://deeplearning.net/software/theano/) & [TensorFlow](https://www.tensorflow.org/)
    - **Pros**: well-tested, stable, well-engineered backends
    - **Cons**: complex, C++, not very stats-aware

# What do we want?
- write math, get code — a domain-specific language (DSL)
- easily generalize to different numbers of indices, structures
- only weakly opinionated about model structure or inference
- model code should be *hackable*
    - easy to use prefab pieces
    - not hard to write custom VB tricks
    - fast prototyping
- no (or minimal) algebra
- automatic gradients

Introducing...

## VinDsl.jl: Fast and furious variational inference

# What's the goal?
- quick prototyping
- targeted at researchers
- "internal" DSL
- loosely-coupled parts
- "consenting adults" philosophy

# Today: two prototypes
- "Classical" models (conjugate + some optimization)
- ADVI "Black Box" models

# Model 1

### Finally, some code!

```julia
using Distributions
using VinDsl
```

    WARNING: Method definition logpdf(Distributions.Gamma, Real) in module Distributions at /Users/jmxp/.julia/v0.4/Distributions/src/univariates.jl:321 overwritten in module VinDsl at /Users/jmxp/.julia/v0.4/VinDsl/src/distributions/expfam.jl:42.
    WARNING: Method definition logpdf(Distributions.Poisson, Int64) in module Distributions at /Users/jmxp/.julia/v0.4/Distributions/src/univariates.jl:321 overwritten in module VinDsl at /Users/jmxp/.julia/v0.4/VinDsl/src/distributions/expfam.jl:64.

# Model structure:
### Main idea: Factor graphs
- idea from Dahua Lin in [this talk](http://people.csail.mit.edu/dhlin/jubayes/julia_bayes_inference.pdf)

- Nodes: arrays of distributions
- Factors $\leftrightarrow$ terms in variational objective
    - but not locked in to graphical model structure!

```julia
dims = (20, 6)

μ[j] ~ Normal(zeros(dims[2]), ones(dims[2]))
τ[j] ~ Gamma(1.1 * ones(dims[2]), ones(dims[2]))
μ0[j] ~ Const(zeros(dims[2]))

y[i, j] ~ Const(rand(dims));
```

Nodes: under the hood
nodes define the q/approximate posterior/recognition model
~ defines a node
can use any distribution defined in the Distributions package
code parses the left and right-hand sides
indices on left get tracked and assigned to dimensions of parameter arrays
code is rewritten as a call to a node constructor

```julia
f = @factor LogNormalFactor y μ τ;
```

or...

```julia
@pmodel begin
    y ~ Normal(μ, τ)
end
```

    1-element Array{VinDsl.Factor{N},1}:
     VinDsl.LogNormalFactor{2}
    x: VinDsl.ConstantNode{Float64} y[i,j]
    μ: VinDsl.RandomNode{Normal} μ[j]
    τ: VinDsl.RandomNode{Gamma} τ[j]

New factor types can be defined with yet another macro:

```julia
@deffactor LogNormalFactor [x, μ, τ] begin
    -(1/2) * ((E(τ) * ( V(x) + V(μ) + (E(x) - E(μ))^2 ) + log(2π) + Elog(τ)))
end

@deffactor LogGammaFactor [x, α, β] begin
    (E(α) - 1) * Elog(x) - E(β) * E(x) + E(α) * E(β) - Eloggamma(α)
end
```

- Uses a "mini-language" with `E(x)` $\equiv \mathbb{E}[X]$, `V(x)` $\equiv \textrm{cov}[X]$, etc.
- Again, no need to track indices
    - multivariate distributions (Dirichlet, MvNormal) are automatically multivariate in these expressions
- `VinDsl` generates a `value(f)` function that handles indices appropriately and sums over the dimensions of the array

# Models are just factor graphs
Let's do a simple conjugate model:
p-model:
$$
\begin{align}
\mu_j &\sim \mathcal{N}(0, 1) \\
\tau_j &\sim \mathrm{Gamma}(1.1, 1) \\
y_{ij} &\sim \mathcal{N}(\mu_j, \tau_j)
\end{align}
$$

q-model:
$$
\begin{align}
\mu &\sim \mathcal{N}(m, t) \\
\tau &\sim \mathrm{Gamma}(\alpha, \beta) \\
\end{align}
$$

```julia
dims = (20, 6)

# note: it won't matter how we initialize here
μ[j] ~ Normal(zeros(dims[2]), ones(dims[2]))
τ[j] ~ Gamma(1.1 * ones(dims[2]), ones(dims[2]))
μ0[j] ~ Const(zeros(dims[2]))
τ0[j] ~ Const(2 * ones(dims[2]))
a0[j] ~ Const(1.1 * ones(dims[2]))
b0[j] ~ Const(ones(dims[2]))

y[i, j] ~ Const(rand(dims))

@pmodel begin
    y ~ Normal(μ, τ)
    μ ~ Normal(μ0, τ0)
    τ ~ Gamma(a0, b0)
end

m = VBModel([μ, τ, μ0, τ0, a0, b0, y], pmodel_factors);
```

# What's going on here?

```julia
m.update_strategy
```

    Dict{VinDsl.Node,Symbol} with 7 entries:
      VinDsl.RandomNode{Gamma… => :conjugate
      VinDsl.ConstantNode{Flo… => :constant
      VinDsl.ConstantNode{Flo… => :constant
      VinDsl.ConstantNode{Flo… => :constant
      VinDsl.ConstantNode{Flo… => :constant
      VinDsl.RandomNode{Norma… => :conjugate
      VinDsl.ConstantNode{Flo… => :constant

```julia
update!(m)
```

# Conjugacy
- right now `VinDsl` goes out of its way to handle conjugacy between nodes
- conjugate relationships not automatically detected, but easy to define
- `@defnaturals` returns expected sufficient statistics from a factor for a given target distribution

```julia
@defnaturals LogNormalFactor μ Normal begin
    Ex, Eτ = E(x), E(τ)
    (Ex * Eτ, -Eτ/2)
end
```

# Index Bookkeeping
- nodes have associated indices
- factors know which indices go with which nodes, which indices to sum over
    - inner indices belong to, e.g., elements of a multivariate normal (should not be separated)
    - outer indices correspond to replicates of "atomic" variables

So this is easy: `i` is inner:

```julia
d = 5
μ[i] ~ MvNormalCanon(zeros(d), diagm(ones(d)))
Λ[i, i] ~ Wishart(float(d), diagm(ones(d)));
```

But here, `i` is inner for $\mu$ but not for $\tau$. In any factor combining these two, $\tau$ will be treated like a vector because it matches an inner index for some node:

```julia
μ[i] ~ MvNormalCanon(zeros(d), diagm(ones(d)))
τ[i] ~ Gamma(1.1 * ones(d), ones(d));
```

# Model 2: ADVI
[(Automatic Differentiation Variational Inference)](http://arxiv.org/abs/1603.00788)

# Two major ideas
1. We just need to define an ELBO (or *approxmate* an ELBO)
1. Any unimodal distribution is *approximately* a normal with constrained support

# Implementation
- Approximate ELBO by sampling from normal, transforming to constrained variables
- Let automatic differentiation handle the gradient calculation
- Do gradient ascent

# Hey, we have autodiff!

# But there's always a problem:
- ForwardDiff defines `Dual <: Real`
- But Distributions.jl doesn't allow, e.g., `Dual` $\mu$ and $\sigma$ for `Normal`
- Then a lot of work...

# Today:
- Some distributions allow arbitrary parameter types:
    - using PDMats:
        - MvNormal, MvNormalCanon, GenericMvTDist
        - Wishart, InverseWishart
    - other:
        - Dirichlet, Normal, NormalCanon, Gamma, InverseGamma, Poisson
- for examples see [here](https://github.com/jmxpearson/distribution_diff_tests)

# This month:
- ~40 more distributions — almost all univariates
- PR in progress from @Halmoni100 in my lab
- some special functions in StatsFuns and Base still assume Float64

# Defining a simple model
$$\begin{align}
\sigma_\eta &\sim \mathrm{Gamma}(a_\eta, b_\eta) \\
a_u &\sim \mathcal{N}(\mu_a, \sigma_a) \\
\eta_{tu} &\sim \mathcal{N}(a_u, \sigma_\eta) \\
N_{tu} &\sim \mathrm{Poisson}(e^{\eta_{tu}}) \\
\end{align}$$

```julia
@ELBO begin
    @advi_declarations begin
        a::Real()[U]
        σ::Positive()
        η::Real()[T, U]
    end

    @advi_model begin
        for u in 1:U
            a[u] ~ Normal(log(15.), 0.1)
        end

        σ ~ Gamma(1, 1)

        for t in 1:T
            for u in 1:U
                η[t, u] ~ Normal(a[u], σ)
                spikes[t, u] ~ Poisson(exp(η[t, u]))
            end
        end
    end
end
```

    ELBO (generic function with 1 method)

```julia
using PyPlot
T = 100  # number of time steps
U = 10  # units

baseline = 10.
baseline_sd = 0.10
log_bl = log(baseline)
unit_bl = log_bl + baseline_sd * randn(U);

fr_log = unit_bl' .+ zeros(T, U)  # firing rate

σ_η = 0.05

eta = fr_log + σ_η * randn(T, U)
fr = exp(eta);

spikes = Array{Int}(size(fr)...)
for i in eachindex(fr)
    spikes[i] = rand(Poisson(fr[i]))
end
```

    /Users/jmxp/anaconda/lib/python2.7/site-packages/matplotlib/font_manager.py:273: UserWarning: Matplotlib is building the font cache using fc-list. This may take a moment.
      warnings.warn('Matplotlib is building the font cache using fc-list. This may take a moment.')

Add some data...

```julia
matshow(spikes', aspect="auto", cmap="gray");
```

Number of parameters
- $a$: 2$U$
- $\sigma$: 2
- $\eta$: 2$TU$

```julia
npars = U * VinDsl.num_pars_advi(RReal()) + VinDsl.num_pars_advi(RPositive()) +
T * U * VinDsl.num_pars_advi(RReal())
xx = 0.1 * randn(npars)
ELBO(xx)
```

    -21631.47778954906

```julia
∇L = (storage, x) -> ForwardDiff.gradient!(storage, ELBO, x)
stor = similar(xx)
∇L(stor, xx)
@time ∇L(stor, xx)
```

      1.562912 seconds (8.04 M allocations: 679.587 MB, 6.21% gc time)

    2022-element Array{Float64,1}:
        96.2983
        38.2569
      2615.62
     -7682.52
      -417.951
      -374.225
      -142.068
       -58.1995
       942.229
      -725.058
       273.02
       -36.452
       356.29
         ⋮
        17.0867
       -16.259
        20.1646
        -3.70256
         4.73903
         3.36335
         5.88467
         7.44689
        13.5717
        -6.19642
         8.93425
       -12.4798

```julia
yy = copy(xx)
gg = similar(yy)
avg_sq_grad = similar(gg)
firstpass = true
elbo = Float64[];
```

```julia
step_size = 0.1
decay = 0.9
eps = 1e-8
nsteps = 160

for jj in 1:nsteps
    ∇L(gg, yy)
    nn = norm(gg)
    if nn/length(gg) > 10.
        gg /= norm(gg)  # crude gradient clipping
    end
    if firstpass == false
        avg_sq_grad = avg_sq_grad * decay + gg.^2 * (1 - decay)
    else
        avg_sq_grad = gg.^2
        firstpass = false
    end
    yy += step_size * gg ./(sqrt(avg_sq_grad) + eps)
    push!(elbo, ELBO(yy))
    display("Iteration $(length(elbo)) $(elbo[end])")
end
```

    "Iteration 1 -21901.643192506774"

    "Iteration 2 -21784.408566088663"

    "Iteration 3 -22761.50851929847"

    "Iteration 4 -20361.289769864685"

    "Iteration 5 -21527.27940310702"

    "Iteration 6 -17984.190834400826"

    "Iteration 7 -17219.647018742507"

    "Iteration 8 -20096.171998652517"

    "Iteration 9 -15719.133230555724"

    "Iteration 10 -14807.092746386164"

    "Iteration 11 -14563.375832332598"

    "Iteration 12 -14043.741827632271"

    "Iteration 13 -13715.407754031177"

    "Iteration 14 -12958.378054523191"

    "Iteration 15 -11893.664828330544"

    "Iteration 16 -11416.167999849515"

    "Iteration 17 -10754.339545075985"

    "Iteration 18 -10049.352149340006"

    "Iteration 19 -9402.409352119079"

    "Iteration 20 -9658.607301884787"

    "Iteration 21 -8327.828020622168"

    "Iteration 22 -8908.349523085642"

    "Iteration 23 -7730.0682606714045"

    "Iteration 24 -6830.5324054293405"

    "Iteration 25 -7717.520227847391"

    "Iteration 26 -6949.580812127499"

    "Iteration 27 -6652.553936474353"

    "Iteration 28 -5976.654400816284"

    "Iteration 29 -6357.3658596257765"

    "Iteration 30 -5812.548575407791"

    "Iteration 31 -5490.162040191422"

    "Iteration 32 -5059.303556650625"

    "Iteration 33 -4781.426677334765"

    "Iteration 34 -4551.261856632452"

    "Iteration 35 -4308.270095528986"

    "Iteration 36 -4292.079328215761"

    "Iteration 37 -3994.083445482836"

    "Iteration 38 -4023.492487795364"

    "Iteration 39 -3941.4627487592675"

    "Iteration 40 -3773.625767885166"

    "Iteration 41 -3554.285420824762"

    "Iteration 42 -3471.2177896320422"

    "Iteration 43 -3606.801729149118"

    "Iteration 44 -3458.755837296366"

    "Iteration 45 -3312.558257314446"

    "Iteration 46 -3422.1017210636055"

    "Iteration 47 -3486.693965133956"

    "Iteration 48 -3212.4499594516324"

    "Iteration 49 -3295.4129262072634"

    "Iteration 50 -2894.734068845817"

    "Iteration 51 -3044.5778606475983"

    "Iteration 52 -3043.3890236880393"

    "Iteration 53 -3096.705300079607"

    "Iteration 54 -2800.2168429386597"

    "Iteration 55 -2823.6872893306668"

    "Iteration 56 -2724.815827991035"

    "Iteration 57 -2536.2819942592146"

    "Iteration 58 -2813.8252996675283"

    "Iteration 59 -2622.910766909394"

    "Iteration 60 -2545.5242115244478"

    "Iteration 61 -2716.8188182666395"

    "Iteration 62 -2547.8211304211463"

    "Iteration 63 -2487.1965455038285"

    "Iteration 64 -2595.838009852493"

    "Iteration 65 -2555.6287417482945"

    "Iteration 66 -2496.6940447238594"

    "Iteration 67 -2440.753239815258"

    "Iteration 68 -2529.6677493249813"

    "Iteration 69 -2342.0300345334454"

    "Iteration 70 -2422.372643545843"

    "Iteration 71 -2468.877089588612"

    "Iteration 72 -2275.624680316577"

    "Iteration 73 -2340.410675965846"

    "Iteration 74 -2363.202887175471"

    "Iteration 75 -2298.6586223903637"

    "Iteration 76 -2424.8088286744824"

    "Iteration 77 -2254.9648424563484"

    "Iteration 78 -2256.767659962991"

    "Iteration 79 -2268.489729925305"

    "Iteration 80 -2321.730033369412"

    "Iteration 81 -2263.3048735619204"

    "Iteration 82 -2247.089332315065"

    "Iteration 83 -2239.594407428411"

    "Iteration 84 -2157.729730733962"

    "Iteration 85 -2320.5130380270866"

    "Iteration 86 -2181.973441053312"

    "Iteration 87 -2151.7475185448898"

    "Iteration 88 -2136.0583167507343"

    "Iteration 89 -2207.967863877849"

    "Iteration 90 -2213.9749887090247"

    "Iteration 91 -2239.828309206509"

    "Iteration 92 -2176.8503933645948"

    "Iteration 93 -2127.587617212332"

    "Iteration 94 -2206.123110053107"

    "Iteration 95 -2096.1898543678385"

    "Iteration 96 -2110.604683041632"

    "Iteration 97 -2142.6319010119396"

    "Iteration 98 -2072.0808398411173"

    "Iteration 99 -2082.9017019162893"

    "Iteration 100 -2067.3815735554995"

    "Iteration 101 -2038.6749828045126"

    "Iteration 102 -2008.7349778361252"

    "Iteration 103 -2175.2734896075244"

    "Iteration 104 -2163.3208904355224"

    "Iteration 105 -2062.4252397849423"

    "Iteration 106 -2027.6384133912975"

    "Iteration 107 -2028.4687089222305"

    "Iteration 108 -2092.8963261403974"

    "Iteration 109 -2044.4750625303006"

    "Iteration 110 -1980.050522057777"

    "Iteration 111 -2059.126407304515"

    "Iteration 112 -2016.3900277082657"

    "Iteration 113 -1961.0534237732666"

    "Iteration 114 -2076.216579276364"

    "Iteration 115 -1965.2993420464118"

    "Iteration 116 -2048.9245640823733"

    "Iteration 117 -2052.929140493427"

    "Iteration 118 -2007.0027668345772"

    "Iteration 119 -1930.4442542333984"

    "Iteration 120 -2060.5535192709685"

    "Iteration 121 -1949.518181457218"

    "Iteration 122 -1952.78904923031"

    "Iteration 123 -2000.9738932013327"

    "Iteration 124 -2099.798320407929"

    "Iteration 125 -2093.411012505818"

    "Iteration 126 -1966.694448941153"

    "Iteration 127 -1943.7472773324391"

    "Iteration 128 -2001.3031484555793"

    "Iteration 129 -2032.0800170265766"

    "Iteration 130 -1923.241793347899"

    "Iteration 131 -1910.3122495794478"

    "Iteration 132 -1996.2578334156574"

    "Iteration 133 -1957.6409762797805"

    "Iteration 134 -1951.2053297682116"

    "Iteration 135 -1976.5174852613877"

    "Iteration 136 -1908.6343265168282"

    "Iteration 137 -1925.3980986570036"

    "Iteration 138 -1887.3830020508765"

    "Iteration 139 -1918.5944250428877"

    "Iteration 140 -1991.5230883643699"

    "Iteration 141 -2025.4096647444849"

    "Iteration 142 -1925.7028873309512"

    "Iteration 143 -1950.1036384208073"

    "Iteration 144 -2057.729579572335"

    "Iteration 145 -2017.467800560341"

    "Iteration 146 -1859.708833399075"

    "Iteration 147 -1888.4565974368297"

    "Iteration 148 -1939.0910823624286"

    "Iteration 149 -1948.19136384097"

    "Iteration 150 -1919.7321499975917"

    "Iteration 151 -1950.7692580396708"

    "Iteration 152 -1890.3043823100202"

    "Iteration 153 -1905.7101972997893"

    "Iteration 154 -1888.98786569496"

    "Iteration 155 -1905.1120590604025"

    "Iteration 156 -1940.6845582157573"

    "Iteration 157 -2026.209087976532"

    "Iteration 158 -1931.0174026672742"

    "Iteration 159 -1857.8731177192637"

    "Iteration 160 -1905.7424392670107"

```julia
plot(elbo)
```

# Moving forward with ReverseDiff

- forward mode autodiff for $f: \mathbb{R}^n \rightarrow \mathbb{R}$ is $\mathcal{O}(n)$
- reverse mode is only $\mathcal{O}(1)$
- reverse mode lies behind Theano, TensorFlow, Stan

Once distributions are parametric, defining a new type that **just works** becomes easy
- same(-ish) idea as ForwardDiff.jl
- caveat: egregious memory usage

```julia
include("/Users/jmxp/code/rdiff/prototype6.jl")
```

    WARNING: using DataStructures.update! in module Main conflicts with an existing identifier.

    backprop (generic function with 3 methods)

```julia
@time ∇L(stor, xx)
∇L_r = grad(ELBO, length(xx))
rstor = similar(xx)
∇L_r(xx, rstor);
@time ∇L_r(xx, rstor);
```

      1.447133 seconds (8.04 M allocations: 679.577 MB, 6.46% gc time)
      0.025645 seconds (155.60 k allocations: 5.186 MB)

# VinDsl needs your help!

Still finalizing API
- docs
- tests
- better ideas!
- Reverse mode!


# Import packages
OceanSpy is documented here [readthedocs](https://oceanspy.readthedocs.io/en/latest/) and here [paper](https://joss.theoj.org/papers/10.21105/joss.01506).

```python
# Import OceanSpy
import oceanspy as ospy

# Additional imports
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
from cartopy.crs import PlateCarree
import scipy.io as sio
import os

# Just to make the notebook cleaner for the symposium.
# DO NOT USE IT OTHERWISE!
import warnings
warnings.filterwarnings("ignore")
```

# Download data

For this illustration a small cutout of the full model fields is downloaded, plus some observations from the real ocean.

```python
data_dir = 'IDIES_symposium'
if not os.path.isdir(data_dir):
    # Download data
    import subprocess
    print("Downloading and uncompressing get_started data...")
    print("...it might take a couple of minutes.")
    commands = ['wget -v -O IDIES_symposium.tar.gz -L '
                'https://livejohnshopkins-my.sharepoint.com/'
                ':u:/g/personal/malmans2_jh_edu/'
                'ETCLiwVHdN9d4DNKA_zoMI8BkaiNQY2BGjZpLbMBrMGUSQ?'
                'download=1',
                'tar xvzf IDIES_symposium.tar.gz',
                'rm -f IDIES_symposium.tar.gz']
    subprocess.call('&&'.join(commands), shell=True)
    subprocess.call('&&'.join(commands), shell=True)
```

# Set parameters for plotting

```python
# Coordinates of the sections
X_Latrabjarg = [-29.8, -24.6]
Y_Latrabjarg = [66.8,  65.5]
map_extent = [-39.9, -19, 63, 67.75]
ylim = (-700, 0)

# Colors
land_col = (253/255, 180/255, 108/255)
bottom_col = 'grey'
cmap_vel = 'RdBu_r'
cmap_hyd = 'rainbow'
cmap_N2 = 'PuBuGn_r'

# Level for contours (commonly used by observational oceanographers)
lev_vel = np.arange(-0.5, 0.51, 0.05)
lev_Temp = [-1, -0.25, 0, 0.25, 0.5, 0.75,
            1, 1.5, 2, 2.5, 3, 3.5, 4,
            4.5, 5, 5.5, 6, 7, 8]
lev_S = [31, 33, 33.5, 34, 34.2,
         34.4, 34.5, 34.6, 34.7,
         34.75, 34.8, 34.85, 34.9,
         34.92, 34.96, 35,
         35.05, 35.1, 35.2]
lev_Sigma0 = [26.5, 27, 27.25, 27.5, 27.6,
              27.7, 27.8, 27.9,
              27.95, 28]
lev_N2 = [0, 1.E-6, 2.E-6, 5.E-6, 1.E-5,
          2.E-5, 5.E-5, 1.E-4, 5.E-4]

# Plot parameters
mpl.rcParams.update({'font.size': 22})
clabel_size = 15
```

# Open datasets

```python
# Open model dataset
od_vage2011 = ospy.open_oceandataset.from_zarr(os.path.join(data_dir, 'vage2011'))
od_mastropole2017 = ospy.open_oceandataset.from_zarr(os.path.join(data_dir, 'mastropole2017'))

# Open observations
vage2011 = sio.loadmat(os.path.join(data_dir, 'observations/Velocities_VageEtAl2011.mat'))
mastropole2017 = sio.loadmat(os.path.join(data_dir, 'observations/meanFields_MastrpoleEtAl2017.mat'))
bathyobs = sio.loadmat(os.path.join(data_dir, 'observations/LatBat.mat'))
```

    Opening dataset from [IDIES_symposium/vage2011].
    Opening dataset from [IDIES_symposium/mastropole2017].

# Show datasets
Full metadata are included in the numerical model output.

```python
print('Here is the oceandataset to compare with Våge et al., 2011 - Nature Geoscience:\n')
print(od_vage2011)
print('\n\nAvailable variables:')
print(od_vage2011.dataset)
```

    Here is the oceandataset to compare with Våge et al., 2011 - Nature Geoscience:

    <oceanspy.OceanDataset>

    Main attributes:
       .dataset: <xarray.Dataset>
       .grid: <xgcm.Grid>
       .projection: <cartopy.crs.Mercator object at 0x7f40b168bf10>

    More attributes:
       .name: EGshelfIIseas2km_ERAI_6H
       .description: High-resolution (~2km) numerical simulation covering the east Greenland shelf (EGshelf),
    and the Iceland and Irminger Seas (IIseas) forced by ERA-Interim.
       .parameters: <class 'dict'>
       .grid_coords: <class 'dict'>

    Available variables:
    <xarray.Dataset>
    Dimensions:    (X: 477, Xp1: 478, Y: 262, Yp1: 263, Z: 53, Zl: 53, Zp1: 54, Zu: 53, time: 1)
    Coordinates:
      * X          (X) float64 -39.97 -39.92 -39.88 -39.83 ... -19.12 -19.08 -19.03
        XC         (Y, X) float64 dask.array<shape=(262, 477), chunksize=(262, 477)>
        XG         (Yp1, Xp1) float64 dask.array<shape=(263, 478), chunksize=(263, 478)>
        XU         (Y, Xp1) float64 dask.array<shape=(262, 478), chunksize=(262, 478)>
        XV         (Yp1, X) float64 dask.array<shape=(263, 477), chunksize=(263, 477)>
      * Xp1        (Xp1) float64 -39.99 -39.94 -39.9 -39.86 ... -19.1 -19.05 -19.01
      * Y          (Y) float64 63.01 63.03 63.05 63.07 ... 67.94 67.96 67.97 67.99
        YC         (Y, X) float64 dask.array<shape=(262, 477), chunksize=(262, 477)>
        YG         (Yp1, Xp1) float64 dask.array<shape=(263, 478), chunksize=(263, 478)>
        YU         (Y, Xp1) float64 dask.array<shape=(262, 478), chunksize=(262, 478)>
        YV         (Yp1, X) float64 dask.array<shape=(263, 477), chunksize=(263, 477)>
      * Yp1        (Yp1) float64 63.0 63.02 63.04 63.06 ... 67.95 67.97 67.98 68.0
      * Z          (Z) float64 -1.0 -3.5 -7.0 -11.5 ... -651.5 -666.5 -681.5 -696.5
      * Zl         (Zl) float64 0.0 -2.0 -5.0 -9.0 ... -644.0 -659.0 -674.0 -689.0
      * Zp1        (Zp1) float64 0.0 -2.0 -5.0 -9.0 ... -659.0 -674.0 -689.0 -704.0
      * Zu         (Zu) float64 -2.0 -5.0 -9.0 -14.0 ... -659.0 -674.0 -689.0 -704.0
    Dimensions without coordinates: time
    Data variables:
        Depth      (time, Y, X) float64 dask.array<shape=(1, 262, 477), chunksize=(1, 262, 477)>
        EXFaqh     (time, Y, X) float64 dask.array<shape=(1, 262, 477), chunksize=(1, 262, 477)>
        EXFatemp   (time, Y, X) float64 dask.array<shape=(1, 262, 477), chunksize=(1, 262, 477)>
        EXFempmr   (time, Y, X) float64 dask.array<shape=(1, 262, 477), chunksize=(1, 262, 477)>
        EXFhl      (time, Y, X) float64 dask.array<shape=(1, 262, 477), chunksize=(1, 262, 477)>
        EXFhs      (time, Y, X) float64 dask.array<shape=(1, 262, 477), chunksize=(1, 262, 477)>
        EXFlwnet   (time, Y, X) float64 dask.array<shape=(1, 262, 477), chunksize=(1, 262, 477)>
        EXFswnet   (time, Y, X) float64 dask.array<shape=(1, 262, 477), chunksize=(1, 262, 477)>
        EXFuwind   (time, Y, X) float64 dask.array<shape=(1, 262, 477), chunksize=(1, 262, 477)>
        EXFvwind   (time, Y, X) float64 dask.array<shape=(1, 262, 477), chunksize=(1, 262, 477)>
        Eta        (time, Y, X) float64 dask.array<shape=(1, 262, 477), chunksize=(1, 262, 477)>
        HFacC      (time, Z, Y, X) float64 dask.array<shape=(1, 53, 262, 477), chunksize=(1, 53, 262, 477)>
        HFacS      (time, Z, Yp1, X) float64 dask.array<shape=(1, 53, 263, 477), chunksize=(1, 53, 263, 477)>
        HFacW      (time, Z, Y, Xp1) float64 dask.array<shape=(1, 53, 262, 478), chunksize=(1, 53, 262, 478)>
        MXLDEPTH   (time, Y, X) float64 dask.array<shape=(1, 262, 477), chunksize=(1, 262, 477)>
        RHOAnoma   (time, Z, Y, X) float64 dask.array<shape=(1, 53, 262, 477), chunksize=(1, 53, 262, 477)>
        R_low      (time, Y, X) float64 dask.array<shape=(1, 262, 477), chunksize=(1, 262, 477)>
        Ro_surf    (time, Y, X) float64 dask.array<shape=(1, 262, 477), chunksize=(1, 262, 477)>
        S          (time, Z, Y, X) float64 dask.array<shape=(1, 53, 262, 477), chunksize=(1, 53, 262, 477)>
        Temp       (time, Z, Y, X) float64 dask.array<shape=(1, 53, 262, 477), chunksize=(1, 53, 262, 477)>
        U          (time, Z, Y, Xp1) float64 dask.array<shape=(1, 53, 262, 478), chunksize=(1, 53, 262, 478)>
        V          (time, Z, Yp1, X) float64 dask.array<shape=(1, 53, 263, 477), chunksize=(1, 53, 263, 477)>
        W          (time, Zl, Y, X) float64 dask.array<shape=(1, 53, 262, 477), chunksize=(1, 53, 262, 477)>
        drC        (time, Zp1) float64 dask.array<shape=(1, 54), chunksize=(1, 54)>
        drF        (time, Z) float64 dask.array<shape=(1, 53), chunksize=(1, 53)>
        dxC        (time, Y, Xp1) float64 dask.array<shape=(1, 262, 478), chunksize=(1, 262, 478)>
        dxF        (time, Y, X) float64 dask.array<shape=(1, 262, 477), chunksize=(1, 262, 477)>
        dxG        (time, Yp1, X) float64 dask.array<shape=(1, 263, 477), chunksize=(1, 263, 477)>
        dxV        (time, Yp1, Xp1) float64 dask.array<shape=(1, 263, 478), chunksize=(1, 263, 478)>
        dyC        (time, Yp1, X) float64 dask.array<shape=(1, 263, 477), chunksize=(1, 263, 477)>
        dyF        (time, Y, X) float64 dask.array<shape=(1, 262, 477), chunksize=(1, 262, 477)>
        dyG        (time, Y, Xp1) float64 dask.array<shape=(1, 262, 478), chunksize=(1, 262, 478)>
        dyU        (time, Yp1, Xp1) float64 dask.array<shape=(1, 263, 478), chunksize=(1, 263, 478)>
        fCori      (time, Y, X) float64 dask.array<shape=(1, 262, 477), chunksize=(1, 262, 477)>
        fCoriG     (time, Yp1, Xp1) float64 dask.array<shape=(1, 263, 478), chunksize=(1, 263, 478)>
        phiHyd     (time, Z, Y, X) float64 dask.array<shape=(1, 53, 262, 477), chunksize=(1, 53, 262, 477)>
        phiHydLow  (time, Y, X) float64 dask.array<shape=(1, 262, 477), chunksize=(1, 262, 477)>
        rA         (time, Y, X) float64 dask.array<shape=(1, 262, 477), chunksize=(1, 262, 477)>
        rAs        (time, Yp1, X) float64 dask.array<shape=(1, 263, 477), chunksize=(1, 263, 477)>
        rAw        (time, Y, Xp1) float64 dask.array<shape=(1, 262, 478), chunksize=(1, 262, 478)>
        rAz        (time, Yp1, Xp1) float64 dask.array<shape=(1, 263, 478), chunksize=(1, 263, 478)>
    Attributes:
        OceanSpy_description:    High-resolution (~2km) numerical simulation cove...
        OceanSpy_grid_coords:    {'Y': {'Y': None, 'Yp1': 0.5}, 'X': {'X': None, ...
        OceanSpy_grid_periodic:  []
        OceanSpy_name:           EGshelfIIseas2km_ERAI_6H
        OceanSpy_parameters:     {'rSphere': 6371.0, 'eq_state': 'jmd95', 'rho0':...
        OceanSpy_projection:     Mercator(**{})

```python
print('Here is the oceandataset to compare with Mastropole et al., 2017 - Journal of Physical Oceanography:\n')
print(od_mastropole2017)
print('\n\nAvailable variables:')
print(od_mastropole2017.dataset)
```

    Here is the oceandataset to compare with Mastropole et al., 2017 - Journal of Physical Oceanography:

    <oceanspy.OceanDataset>

    Main attributes:
       .dataset: <xarray.Dataset>
       .grid: <xgcm.Grid>
       .projection: <cartopy.crs.Mercator object at 0x7f40b59d4678>

    More attributes:
       .name: EGshelfIIseas2km_ERAI_6H
       .description: High-resolution (~2km) numerical simulation covering the east Greenland shelf (EGshelf),
    and the Iceland and Irminger Seas (IIseas) forced by ERA-Interim.
       .parameters: <class 'dict'>
       .grid_coords: <class 'dict'>

    Available variables:
    <xarray.Dataset>
    Dimensions:  (X: 477, Xp1: 478, Y: 262, Yp1: 263, Z: 53, Zl: 53, Zp1: 54, Zu: 53, time: 1)
    Coordinates:
      * X        (X) float64 -39.97 -39.92 -39.88 -39.83 ... -19.12 -19.08 -19.03
        XC       (Y, X) float64 dask.array<shape=(262, 477), chunksize=(262, 477)>
        XG       (Yp1, Xp1) float64 dask.array<shape=(263, 478), chunksize=(263, 478)>
        XU       (Y, Xp1) float64 dask.array<shape=(262, 478), chunksize=(262, 478)>
        XV       (Yp1, X) float64 dask.array<shape=(263, 477), chunksize=(263, 477)>
      * Xp1      (Xp1) float64 -39.99 -39.94 -39.9 -39.86 ... -19.1 -19.05 -19.01
      * Y        (Y) float64 63.01 63.03 63.05 63.07 ... 67.94 67.96 67.97 67.99
        YC       (Y, X) float64 dask.array<shape=(262, 477), chunksize=(262, 477)>
        YG       (Yp1, Xp1) float64 dask.array<shape=(263, 478), chunksize=(263, 478)>
        YU       (Y, Xp1) float64 dask.array<shape=(262, 478), chunksize=(262, 478)>
        YV       (Yp1, X) float64 dask.array<shape=(263, 477), chunksize=(263, 477)>
      * Yp1      (Yp1) float64 63.0 63.02 63.04 63.06 ... 67.95 67.97 67.98 68.0
      * Z        (Z) float64 -1.0 -3.5 -7.0 -11.5 ... -651.5 -666.5 -681.5 -696.5
      * Zl       (Zl) float64 0.0 -2.0 -5.0 -9.0 ... -644.0 -659.0 -674.0 -689.0
      * Zp1      (Zp1) float64 0.0 -2.0 -5.0 -9.0 ... -659.0 -674.0 -689.0 -704.0
      * Zu       (Zu) float64 -2.0 -5.0 -9.0 -14.0 ... -659.0 -674.0 -689.0 -704.0
    Dimensions without coordinates: time
    Data variables:
        Depth    (time, Y, X) float64 dask.array<shape=(1, 262, 477), chunksize=(1, 262, 477)>
        HFacC    (time, Z, Y, X) float64 dask.array<shape=(1, 53, 262, 477), chunksize=(1, 53, 262, 477)>
        HFacS    (time, Z, Yp1, X) float64 dask.array<shape=(1, 53, 263, 477), chunksize=(1, 53, 263, 477)>
        HFacW    (time, Z, Y, Xp1) float64 dask.array<shape=(1, 53, 262, 478), chunksize=(1, 53, 262, 478)>
        S        (time, Z, Y, X) float64 dask.array<shape=(1, 53, 262, 477), chunksize=(1, 53, 262, 477)>
        Temp     (time, Z, Y, X) float64 dask.array<shape=(1, 53, 262, 477), chunksize=(1, 53, 262, 477)>
    Attributes:
        OceanSpy_description:    High-resolution (~2km) numerical simulation cove...
        OceanSpy_grid_coords:    {'Y': {'Y': None, 'Yp1': 0.5}, 'X': {'X': None, ...
        OceanSpy_grid_periodic:  []
        OceanSpy_name:           EGshelfIIseas2km_ERAI_6H
        OceanSpy_parameters:     {'rSphere': 6371.0, 'eq_state': 'jmd95', 'rho0':...
        OceanSpy_projection:     Mercator(**{})

# Plot map

```python
# Mask land
Depth = od_vage2011.dataset['Depth']
Depth = Depth.where(Depth>0)
od_vage2011 = od_vage2011.merge_into_oceandataset(Depth.rename('masked_Depth'))

# Plot Depth
fig = plt.figure(figsize=(10, 5))
ax = od_vage2011.plot.horizontal_section(varName='masked_Depth', cmap='bone_r')
line = ax.plot(X_Latrabjarg, Y_Latrabjarg, 'm-',
               transform=PlateCarree(), linewidth=5)
ax.background_patch.set_facecolor(land_col)
ax.set_extent(map_extent)
```

# Survey
Sample the model like the ship sampled the real ocean. This is called "survey" mode. See: [here](https://oceanspy.readthedocs.io/en/latest/generated/oceanspy.subsample.survey_stations.html#oceanspy.subsample.survey_stations).

```python
# Extract transect
od_vage2011_LL = od_vage2011.subsample.survey_stations(Xsurv=X_Latrabjarg, Ysurv=Y_Latrabjarg, delta=2)
print()
od_mastropole2017_LL = od_mastropole2017.subsample.survey_stations(Xsurv=X_Latrabjarg, Ysurv=Y_Latrabjarg, delta=2)
```

    Cutting out the oceandataset.
    Carrying out survey.
    Create weight file: bilinear_72x120_138x138.nc
    Variables to interpolate: ['Depth', 'EXFaqh', 'EXFatemp', 'EXFempmr', 'EXFhl', 'EXFhs', 'EXFlwnet', 'EXFswnet', 'EXFuwind', 'EXFvwind', 'Eta', 'HFacC', 'HFacS', 'HFacW', 'MXLDEPTH', 'RHOAnoma', 'R_low', 'Ro_surf', 'S', 'Temp', 'U', 'V', 'W', 'XC', 'XG', 'XU', 'XV', 'YC', 'YG', 'YU', 'YV', 'dxC', 'dxF', 'dxG', 'dxV', 'dyC', 'dyF', 'dyG', 'dyU', 'fCori', 'fCoriG', 'phiHyd', 'phiHydLow', 'rA', 'rAs', 'rAw', 'rAz', 'masked_Depth'].
    Interpolating [Depth].
    Interpolating [EXFaqh].
    Interpolating [EXFatemp].
    Interpolating [EXFempmr].
    Interpolating [EXFhl].
    Interpolating [EXFhs].
    Interpolating [EXFlwnet].
    Interpolating [EXFswnet].
    Interpolating [EXFuwind].
    Interpolating [EXFvwind].
    Interpolating [Eta].
    Interpolating [HFacC].
    Interpolating [HFacS].
    Interpolating [HFacW].
    Interpolating [MXLDEPTH].
    Interpolating [RHOAnoma].
    Interpolating [R_low].
    Interpolating [Ro_surf].
    Interpolating [S].
    Interpolating [Temp].
    Interpolating [U].
    Interpolating [V].
    Interpolating [W].
    Interpolating [XC].
    Interpolating [XG].
    Interpolating [XU].
    Interpolating [XV].
    Interpolating [YC].
    Interpolating [YG].
    Interpolating [YU].
    Interpolating [YV].
    Interpolating [dxC].
    Interpolating [dxF].
    Interpolating [dxG].
    Interpolating [dxV].
    Interpolating [dyC].
    Interpolating [dyF].
    Interpolating [dyG].
    Interpolating [dyU].
    Interpolating [fCori].
    Interpolating [fCoriG].
    Interpolating [phiHyd].
    Interpolating [phiHydLow].
    Interpolating [rA].
    Interpolating [rAs].
    Interpolating [rAw].
    Interpolating [rAz].
    Interpolating [masked_Depth].
    Remove file bilinear_72x120_138x138.nc

    Cutting out the oceandataset.
    Carrying out survey.
    Create weight file: bilinear_72x120_138x138.nc
    Variables to interpolate: ['Depth', 'HFacC', 'HFacS', 'HFacW', 'S', 'Temp', 'XC', 'XG', 'XU', 'XV', 'YC', 'YG', 'YU', 'YV'].
    Interpolating [Depth].
    Interpolating [HFacC].
    Interpolating [HFacS].
    Interpolating [HFacW].
    Interpolating [S].
    Interpolating [Temp].
    Interpolating [XC].
    Interpolating [XG].
    Interpolating [XU].
    Interpolating [XV].
    Interpolating [YC].
    Interpolating [YG].
    Interpolating [YU].
    Interpolating [YV].
    Remove file bilinear_72x120_138x138.nc

# Use OceanSpy to compute density and $N^2$
Density is computed using the seawater equation of state $\rho = \rho (T, S, p)$ and $\sigma_0 = \rho(T, S, 0) -1000$kgm$^{-3}$. See the [docs](https://oceanspy.readthedocs.io/en/latest/generated/oceanspy.compute.potential_density_anomaly.html#oceanspy.compute.potential_density_anomaly).

The Brunt-Vaisala frequency $N$ measures the vertical density stratification:
\begin{equation}
N^2 = -\frac{g}{\rho_0}\frac{\partial\sigma_0}{\partial z} .
\end{equation}
See the [docs](https://oceanspy.readthedocs.io/en/latest/generated/oceanspy.compute.Brunt_Vaisala_frequency.html).

```python
# Compute density
od_mastropole2017_LL = od_mastropole2017_LL.compute.potential_density_anomaly()
od_mastropole2017_LL = od_mastropole2017_LL.compute.Brunt_Vaisala_frequency()
```

    Computing potential density anomaly using the following parameters: {'eq_state': 'jmd95'}.
    Computing Brunt-Väisälä Frequency using the following parameters: {'rho0': 1027, 'g': 9.81}.
    Computing gradient.

# Plot hydrography
The model fields closely resemble the field data. There are some difference, as discussed in [Almansi et al. (2017)](https://journals.ametsoc.org/doi/full/10.1175/JPO-D-17-0129.1), but they're likely inconsequential.

```python
# Same as before:
# All metadata for the model are handled by OceanSpy and xarray.
# Need to be added manually for observations.

# Initialize figure
xlim = (1, 225)
fig, axes = plt.subplots(3, 2, figsize=(20, 15), sharex=True, sharey=True)
for a, ax in enumerate(axes.flatten()):
    # Pick variable
    if a in [0, 1]:
        var_mod = 'Temp'
        var_obs = 'THE'
        cmap = cmap_hyd
        levels = lev_Temp
        fmt_cbar='%.2f'
    elif a in [2, 3]:
        var_mod = 'S'
        var_obs = 'SAL'
        cmap = cmap_hyd
        levels = lev_S
        fmt_cbar='%.2f'
    elif a in [4, 5]:
        var_mod = 'N2'
        var_obs = 'NSQ'
        cmap = cmap_N2
        levels = lev_N2
        fmt_cbar='%.0e'

    # Isopycnals properties
    contour_kwargs = {'levels': lev_Sigma0,
                      'colors': ['k' if (lev!=27.8) else 'm' for lev in lev_Sigma0],
                      'linewidths': [2 if (lev!=27.8) else 3 for lev in lev_Sigma0]}
    clabel_kwargs = {'fontsize': clabel_size, 'fmt':'%2.2f'}

    if a%2 == 0:
        # Observations
        cols = mpl.colors.BoundaryNorm(levels, 256)
        contf = ax.contourf(mastropole2017['X'], -mastropole2017['Y'].astype('float'), mastropole2017[var_obs],
                            cmap=cmap, levels=levels, norm=cols)
        cont = ax.contour(mastropole2017['X'], -mastropole2017['Y'].astype('float'), mastropole2017['SIG'],
                          **contour_kwargs)
        plt.colorbar(contf, ax=ax, format=fmt_cbar)
        ax.clabel(cont, **clabel_kwargs)
        ax.fill_between(np.squeeze(bathyobs['regdist']), -np.squeeze(bathyobs['regbat']), -np.squeeze(bathyobs['regbat'])*0-700,
                        color=bottom_col, zorder=10)

    else:
        # Model
        ax = od_mastropole2017_LL.plot.vertical_section(varName=var_mod, contourName='Sigma0', plotType='contourf',
                                                        levels=levels, ax=ax, xlim=xlim, cmap=cmap,
                                                        contour_kwargs=contour_kwargs, clabel_kwargs=clabel_kwargs,
                                                        cbar_kwargs={'format': fmt_cbar})
    ax.set_xlim(xlim)
    ax.set_ylim(ylim)
    ax.set_facecolor(bottom_col)
    if a==0:
        ax.set_title('Observations')
    elif a==1:
        ax.set_title('Model')
    else:
        ax.set_title(None)
```

# Use OceanSpy to rotate velocities
We want to plot the velocity component perpendicular to the survey section, like in the observations.

```python
# Compute aligned velocities
od_vage2011_LL = od_vage2011_LL.compute.survey_aligned_velocities()
```

    Computing survey aligned velocities.

# Plot velocities
Again, the model velocity field is realistic. The differences between the measurements and the model results are likely due to different chaotic fluctuations (geostrophic turbulence), not systematic model bias. We have not attempted to match the phases of the chaotic fluctuations in this experiment.

```python
# All metadata for the model are handled by OceanSpy and xarray.
# Need to be added manually for observations.

# Plot
xlim = (0, 175)
fig, axes = plt.subplots(2, 1, figsize=(10, 10), sharex=True)

# Observations
ax = axes[0]
contf = ax.contourf(vage2011['X'], -vage2011['Y'].astype('float'), -vage2011['OrtVel']*1.E-2,
                    levels=lev_vel, cmap=cmap_vel, extend='both')
ax.set_xlim(xlim)
ax.set_ylim(ylim)
plt.colorbar(contf, ax=ax)
_ = ax.set_facecolor(bottom_col)
_ = ax.set_title('Observations')

# Model
ax=axes[1]
ax = od_vage2011_LL.plot.vertical_section(varName='ort_Vel', plotType='contourf',
                                          levels=lev_vel, ax=ax, xlim=xlim, cmap=cmap_vel)
ax.set_ylim(ylim)
_ = ax.set_facecolor(bottom_col)
_ = ax.set_title('Model')
```

# Compute Ertel Potential Vorticity

Ertel PV is computed as
\begin{equation}
Q =
- \frac{\omega \cdot \nabla \rho}{\rho} =
(f + \zeta)\frac{N^2}{g}
+ \frac{\left(\zeta_H+e\hat{\mathbf{y}}\right)
\cdot\nabla_H\rho}{\rho_0}
\end{equation}

This is the single most important variable in dynamical oceanography, yet it's almost impossible to measure in the real ocean. Using the ultra-high resolution numerical model it's easily computed (see: [here](https://oceanspy.readthedocs.io/en/latest/generated/oceanspy.compute.Ertel_potential_vorticity.html)).

We compute and plot the average $Q$ between 100 and 150m depth.

```python
# Compute Ertel PV
od_vage2011_surf = od_vage2011.subsample.cutout(ZRange = [-100, -150])
od_vage2011_surf = od_vage2011_surf.compute.Ertel_potential_vorticity()

# Plot Ertel PV
fig = plt.figure(figsize=(10, 5))
od_vage2011_surf.plot.horizontal_section(varName='Ertel_PV',
                                         contourName='Depth',
                                         meanAxes='Z',
                                         center=False,
                                         robust=True)
```

```python

```


```python
%%capture
## compile PyRoss for this notebook
import os
owd = os.getcwd()
os.chdir('../../')
%run setup.py install
os.chdir(owd)
```

```python
%matplotlib inline
```

## Introduction

In the present example, a control protocol is optimised. The protocol consists of two durations, namely

* the duration $T_1$ of a full lockdown (during which the initial contact matrix reduced via a prefactor 1/4), and
* the duration $T_2$ a subsequent partial lockdown (during which the initial contact matrix reduced via a prefactor 3/4).

The cost of a protocol is defined as

\begin{equation}
C=exp\left[ \kappa\left(  T_1+ T_2/5 \right)\right] + \int_0^{t_f}\mathrm{d}t\,I_s(t),
\end{equation}

where $t_f = 1000$ days is the final time of the simulation, and $I_s(t)$ is the total number of symptomatic infectives at time $t$. The first term in the cost function can be thought of as a cost for social distancing, while the second terms models a cost of infection. We additionally set $C = \infty$ if at any time $I_s(t) > 2000$, which serves as a hard constraint that every protocol must keep the number of infectives below this threshold.

For an SIIR model with two age groups, we then minimise the cost as a function of $(T_1,T_2)$, considering three distinct scenarios: i) low cost of social distancing ($\kappa = 0.01$), ii) intermediate cost of social distancing ($\kappa = 0.1$), and iii) high cost of social distancing ($\kappa = 1$). Finally, we compare the time series corresponding to these optimal protocols.

*This is the Jupyter notebook version of an example from the PyRoss manual.*

## Define system parameters

```python
import numpy as np; from matplotlib import pyplot as plt; import pyross
from multiprocessing import Pool; import cma; import time
from functools import partial

# 1. Define model
M  = 2               # the population has two age groups
N  =  5e4            # and this is the total population
# exact params of reference trajectory
beta  = 0.02         # infection rate
gIa   = 1./7         # removal rate of asymptomatic infectives
gIs   = 1./7         # removal rate of asymptomatic infectives
alpha = 0.2          # fraction of asymptomatic infectives
fsa   = 0.8          # the self-isolation parameter
# set the age structure
fi = np.array([0.25, 0.75])  # fraction of population in age age group
Ni = N*fi
# set up initial condition
Ia0 = np.array([10, 10])  # each age group has asymptomatic infectives
Is0 = np.array([10, 10])   # and also symptomatic infectives
R0  = np.array([0, 0])  # there are no recovered individuals initially
S0  = Ni - (Ia0 + Is0 + R0) # initial number of susceptibles
```

## Define function that generates two-step release protocol

```python
# 2. Define function that generates two-step release protocol
def get_events(protocol= [50,100]):
    end_phase_1, end_phase_2 = protocol
    C = np.array([[18., 9.], [3., 12.]])
    events = []; contactMatrices = []
    # For technical reasons, currently the event functions take a
    # vector argument "rp" instead of a tuple (S, Ia, Is).
    # When defining the event functions, use the following correspondences:
    # S  == rp[ :M]
    # Ia == rp[1*M:2*M]
    # Is == rp[2*M:3*M]
    # More generally, for any model the order of the populations
    # within the vector rp is the same as used for the initial
    # conditions in the model.simulate() function call.
    #
    # Event 0: dummy event used to set the initial contact matrix
    events.append( lambda t, rp: 1. )
    contactMatrices.append( C )
    # Event 1: contact matrix reduced by a factor of 4 once number
    # of symptomatic infectives exceeds a threshold ("start of full lockdown")
    lockdown_threshold_on = 1900
    def event1(t,rp):
        return np.sum(rp[2*M:3*M]) - lockdown_threshold_on
    event1.direction = +1 # need to pass threshold from below for event
    events.append(event1); contactMatrices.append( 0.25*C )
    # Event 2: contact matrix at 75% of original contact matrix after
    # a given time ("start of partial lockdown").
    def event2(t,rp):
        return t - end_phase_1
    events.append(event2); contactMatrices.append( 0.75*C )
    # Event 3: original contact matrix is restored after a given time
    # ("end of partial lockdown").
    def event3(t,rp):
        return t - end_phase_2
    events.append(event3); contactMatrices.append( C )
    return events, contactMatrices
```

## Define cost function, and function to evaluate cost of a given protocol

```python
# 3. Define cost function
maximal_value_for_Is = 2000
def evaluate_cost_function(t_arr,traj,protocol,events_occured,
                           sdc_prefactor=1.,
                           sdc_rate=1/7.):
    infinity = 1e300 # using np.inf would be more proper, but will
    # result in warnings throughout the minimization once a protocol
    # is probed that violates one of the hard constraints.
    #
    # hard constraints:
    # - times of protocol need to be non-negative
    if (np.array(protocol) < 0).any(): return infinity
    # - second stage of intervention needs to end after first stage ends
    if protocol[0] > protocol[1]: return infinity
    # - second stage of intervention needs to end before 700 days
    if protocol[1] > 700: return infinity
    # - number of symptomatic infectives should
    #   never exceed a given maximal value
    if np.max( np.sum( traj[2*M:3*M],axis=0 ) ) > maximal_value_for_Is:
        return infinity
    #
    # Evaluate results of protocol.
    # Which phases have occured in the simulation?
    have_lockdown_start = False
    have_lockdown_end_0 = False
    have_lockdown_end_1 = False
    for i,e in enumerate(events_occured):
        if e[1] == 1:
            lockdown_start = e[0]
            have_lockdown_start = True
        elif e[1] == 2:
            lockdown_end_0 = e[0]
            have_lockdown_end_0 = True
        elif e[1] == 3:
            lockdown_end_1 = e[0]
            have_lockdown_end_1 = True
    #
    # calculate cost function
    cost = 0.
    # cost of social distancing
    if have_lockdown_start:
        if have_lockdown_end_0:
            cost += sdc_prefactor* np.exp(sdc_rate* (lockdown_end_0-lockdown_start))
        else:
            cost += infinity
        #
        if have_lockdown_end_0 and have_lockdown_end_1:
            cost *=  np.exp(0.2*sdc_rate* (lockdown_end_1-lockdown_end_0))
    # cost of infection
    cost += np.trapz( np.sum( traj[2*M:3*M],axis= 0 ),t_arr)
    return cost

# 4. Define function that evaluates the cost of a given protocol
def evaluate_cost_of_protocol(model,protocol,
                             verbose=False,return_trajectory=False,
                              sdc_prefactor=10.,
                              sdc_rate=1/7.):
    # define events corresponding to given protocol
    events, contactMatrices = get_events(protocol)
    # run simulation
    Tf = 1000; Nt = (Tf +1)*10
    result = model.simulate(S0,  Ia0, Is0,
                            events=events,contactMatrices=contactMatrices,
                            Tf=Tf, Nf=Nt)
    # evaluate cost
    cost = evaluate_cost_function(t_arr=result['t'],
                                   traj=result['X'].T,
                                   protocol=protocol,
                                  events_occured=result['events_occured'],
                                  sdc_prefactor=sdc_prefactor,
                                  sdc_rate=sdc_rate)
    if return_trajectory:
        return cost, result['t'], result['X'], result['events_occured']
    else:
        return cost
```

## Define minimiser

```python
# 5. Define minimiser
def minimizing_function(sdc_prefactor,sdc_rate,
                    protocol):
    '''
    This is the function that will be minimised by the cma-es algorithm
    '''
    costs = evaluate_cost_of_protocol(model=model,
                                             protocol=protocol,
                                      sdc_prefactor=sdc_prefactor,
                                      sdc_rate=sdc_rate,
                                              return_trajectory=False)
    return costs

def find_optimal_protocol(initial_guess,sdc_prefactor,sdc_rate,
                          model,
                         verbose=True,initial_variance=5.):
    func = partial(minimizing_function, sdc_prefactor, sdc_rate)
    # set parameters for minimisation based on cma-es algorithm
    number_of_threads = 4
    number_iterations = 1000
    p = Pool(number_of_threads)
    options = cma.CMAOptions()
    options['popsize'] = 12
    # run minimisation
    start_time = time.time()
    optim = cma.CMAEvolutionStrategy(initial_guess, initial_variance,
                                     options)
    iteration = 0
    while not optim.stop() and iteration < number_iterations:
        positions = optim.ask()
        values = p.map(func, positions)
        optim.tell(positions, values)
        optim.disp()
        iteration += 1
    end_time = time.time()
    if verbose:
        print("Time: ", end_time - start_time)
        print("Value: ", optim.best.f)
        print("Parameters: ",optim.best.x)
    return optim.best.x
```

## Find optimal protocol for low-, intermediate- and high cost of social distancing

```python
# 6. Find optimal protocol for three different scenarios

# Initialise model
parameters = {'alpha':alpha,'beta':beta, 'gIa':gIa,'gIs':gIs,'fsa':fsa}
model = pyross.control.SIR(parameters, M, Ni)
initial_guess=[70,200] # use same initial guess for all minimisations
sdc_prefactor = 1. # means that social distancing cost and
# cost of infection have the same weight in our model

# Low, intermediate, and high cost rate for social distancing:
sdc_rates = [1e-2,1e-1,1.]
optimal_protocols = []
for i,sdc_rate in enumerate(sdc_rates):
    optimal_protocol = find_optimal_protocol(initial_guess=initial_guess,
                                           sdc_prefactor=sdc_prefactor,
                                           sdc_rate=sdc_rate,
                                              model=model)
    optimal_protocols.append(optimal_protocol)
```

    (6_w,12)-aCMA-ES (mu_w=3.7,w_1=40%) in dimension 2 (seed=883279, Thu May  7 12:47:32 2020)
    Iterat #Fevals   function value  axis ratio  sigma  min&max std  t[m:s]
        1     12 1.562097806950094e+05 1.0e+00 5.75e+00  6e+00  6e+00 0:00.2
        2     24 1.539263668429960e+05 1.3e+00 1.20e+01  1e+01  1e+01 0:00.3
        3     36 1.534514654519690e+05 1.4e+00 1.50e+01  1e+01  2e+01 0:00.4
       24    288 1.484467686543054e+05 4.5e+00 1.77e+01  3e+00  7e+00 0:03.6
       49    588 1.484468301056222e+05 8.8e+00 1.29e+01  8e-02  1e+00 0:07.7

    /Users/jkappler/miniconda3/envs/snowflake/lib/python3.7/site-packages/cma/utilities/utils.py:347: UserWarning: sigma change np.exp(1.1123091364813413) = 3.041373238100446 clipped to np.exp(+-1) (class=CMAAdaptSigmaCSA method=update iteration=49)
      ')')

       79    948 1.484466017370722e+05 1.1e+02 5.01e+01  6e-03  6e-01 0:12.8
      100   1200 1.484466012318590e+05 3.2e+02 4.63e+01  7e-04  2e-01 0:16.5
      145   1740 1.484466012121084e+05 1.9e+02 3.35e+01  1e-04  2e-02 0:23.5
      188   2256 1.484466009173244e+05 4.7e+00 1.56e+02  4e-05  2e-04 0:31.7
      200   2400 1.484466009111501e+05 6.2e+00 5.24e+01  4e-06  1e-05 0:33.8
      255   3060 1.484466009106378e+05 6.2e+00 1.88e+00  2e-10  1e-09 0:43.9
      263   3156 1.484466009106378e+05 5.8e+00 7.09e-01  4e-11  1e-10 0:45.3
    Time:  45.32984280586243
    Value:  148444.17765471953
    Parameters:  [159.26115181 635.26025207]
    (6_w,12)-aCMA-ES (mu_w=3.7,w_1=40%) in dimension 2 (seed=1004256, Thu May  7 12:48:17 2020)
    Iterat #Fevals   function value  axis ratio  sigma  min&max std  t[m:s]
        1     12 1.561775719632415e+05 1.0e+00 6.91e+00  7e+00  9e+00 0:00.2
        2     24 1.544577384643056e+05 1.3e+00 1.09e+01  1e+01  1e+01 0:00.3
        3     36 1.541644588413770e+05 1.4e+00 1.06e+01  9e+00  9e+00 0:00.5
       27    324 1.533572214052785e+05 6.7e+00 2.83e+00  9e-02  7e-01 0:03.6
       58    696 1.533520254077647e+05 8.0e+01 1.81e+00  1e-03  1e-01 0:07.6
       97   1164 1.533512305502986e+05 3.6e+00 1.83e+00  4e-04  1e-03 0:12.6
      100   1200 1.533511466255758e+05 2.2e+00 2.72e+00  4e-04  1e-03 0:13.0
      155   1860 1.533510878731440e+05 5.7e+00 1.25e-01  2e-08  9e-08 0:20.0
      200   2400 1.533510878703873e+05 6.7e+00 1.89e-02  2e-11  1e-10 0:25.8
      218   2616 1.533510878703818e+05 4.2e+00 9.90e-03  2e-12  8e-12 0:28.1
    Time:  28.166916131973267
    Value:  153351.08787038177
    Parameters:  [ 59.64631831 236.38725147]
    (6_w,12)-aCMA-ES (mu_w=3.7,w_1=40%) in dimension 2 (seed=1036160, Thu May  7 12:48:45 2020)
    Iterat #Fevals   function value  axis ratio  sigma  min&max std  t[m:s]
        1     12 2.646144392700818e+29 1.0e+00 7.84e+00  8e+00  9e+00 0:00.1
        2     24 8.367284295386549e+27 1.1e+00 8.50e+00  6e+00  9e+00 0:00.3
        3     36 4.166733868265053e+26 1.5e+00 9.08e+00  6e+00  9e+00 0:00.4
       27    324 1.027887927892541e+16 7.4e+00 2.52e+01  1e+00  5e+00 0:03.5
       56    672 3.553064776796659e+15 2.2e+00 5.00e+00  9e-03  3e-02 0:07.6
       89   1068 3.528531170711232e+15 4.4e+00 9.49e-01  4e-05  1e-04 0:12.6
      100   1200 3.528399596390992e+15 4.4e+00 7.34e-01  7e-06  5e-05 0:14.2
      148   1776 3.528386763705133e+15 5.8e+00 1.88e-01  1e-08  7e-08 0:21.3
      200   2400 3.528386693217531e+15 1.9e+01 7.57e-03  8e-13  1e-11 0:28.9
      201   2412 3.528386693215952e+15 1.7e+01 6.82e-03  7e-13  8e-12 0:29.1
    Time:  29.08972692489624
    Value:  3528386693215951.5
    Parameters:  [45.25926666 98.81107004]

## Visualise results

```python
# 7. Visualise results
def plot_result(protocol,model,
                title='Optimal protocol',
                t_max=None):
    #
    cost, t_arr, traj, events_occured = evaluate_cost_of_protocol(
                                            model=model,
                                             protocol=protocol,
                                              return_trajectory=True)
    #
    Is = np.sum(traj[:,2*M:],axis=-1)
    #
    fig, ax = plt.subplots(1,1,figsize=(7,5))
    ax.set_title(title)
    ax.axhline(maximal_value_for_Is,color='red',ls='--',
              label=r'Constraint')
    #
    ax.axvspan(events_occured[0][0],events_occured[1][0],
              color='crimson',label='Intervention 1',alpha=0.2)
    ax.axvspan(events_occured[1][0],events_occured[2][0],
              color='orange',label='Intervention 2',alpha=0.2)
    ax.plot(t_arr,Is)
    if t_max == None:
        ax.set_xlim(np.min(t_arr),np.max(t_arr))
    else:
        ax.set_xlim(np.min(t_arr),t_max)
    ax.set_ylabel('Symptomatic infectives'); ax.set_xlabel(r'time [days]')
    ax.legend(loc='upper right',fontsize=12,framealpha=1.)
    #if title != 'Optimal protocol':
    #    fig.savefig(title+'.pdf',bbox_inches='tight')
    plt.show(); plt.close()

optimal_protocol_labels = ['Low cost of social distancing',
                          'Intermediate cost of social distancing',
                          'High cost of social distancing']
t_max = [700,250,250]

for i,optimal_protocol in enumerate(optimal_protocols):
    plot_result(protocol=optimal_protocol,model=model,t_max=t_max[i],
           title=optimal_protocol_labels[i])
```

```python

```


```python
import sys
import numpy as np
import sys
print(sys.version)

from phasor.utilities.ipynb.displays import *
from phasor.utilities.ipynb.filters import *
from phasor.utilities.ipynb.hdf import *
#from YALL.utilities.tabulate import tabulate

from declarative import (
  OverridableObject, mproperty
)

import sympy
from phasor.utilities.ipynb.displays import *
from phasor.utilities.ipynb.ipy_sympy import *
import scipy.linalg
```

    3.5.2 (default, Sep 14 2016, 11:28:32)
    [GCC 6.2.1 20160901 (Red Hat 6.2.1-1)]

```python
dx,dy,dz = sympy.var('d_x, d_y, d_z')
d = sympy.Matrix([dx,dy,dz])
dLsq = dx**2 + dy**2 + dz**2
dLsq = (d.T * d)[0,0]
display(dLsq)
P_torque = sympy.Matrix([[0,-dz,dy],[dz,0,-dx],[-dy,dx,0]])
display(P_torque)
P_force = sympy.Matrix([[0,dz,-dy],[-dz,0,dx],[dy,-dx,0]]) / dLsq
display(P_force)
P_d = (d * d.T)/dLsq
display(P_d)

bm = sympy.Matrix(np.bmat([[P_d, P_force],[P_torque,P_d]]))
display(bm)
display(bm.T)
```

```python
bm.det()
```

```python
ct = (bm * bm)
ct.simplify()
ct
```

```python
d_np = np.array([dx,dy,dz])
d_np
np.outer(d_np, d_np)
```

    array([[d_x**2, d_x*d_y, d_x*d_z],
           [d_x*d_y, d_y**2, d_y*d_z],
           [d_x*d_z, d_y*d_z, d_z**2]], dtype=object)

```python
ct = (P_d * P_d.T)
ct.expand().simplify()
ct
```

```python
ct = (P_force * P_torque)
ct.expand().simplify()
ct

```

```python
ps_In = np.eye(3)
VIAB = np.bmat([[ps_In,I],[ps_In,-ps_In]])
d = 5
X2VI = np.matrix([
    [0,0,0,1,1/d,1],
    [0,d,-1,0,0,0,],
    [1,0,-1,0,0,0],
]) * VIAB.T
print(np.linalg.inv(X2VI[:,0:3]) * X2VI[:,3:6])
print(np.linalg.inv(X2VI[:,3:6]) * X2VI[:,0:3])
np.linalg.eig(np.linalg.inv(X2VI[:,0:3]) * X2VI[:,3:6])
```

    [[ 0.01960784 -0.19607843 -0.98039216]
     [-0.19607843  0.96078431 -0.19607843]
     [-0.98039216 -0.19607843  0.01960784]]
    [[ 0.01960784 -0.19607843 -0.98039216]
     [-0.19607843  0.96078431 -0.19607843]
     [-0.98039216 -0.19607843  0.01960784]]

    (array([ 1., -1.,  1.]), matrix([[ 0.71400555, -0.70014004,  0.51870247],
             [-0.13730876, -0.14002801, -0.77360539],
             [-0.6865438 , -0.70014004, -0.36398139]]))

```python
ps_In = np.eye(3, dtype = object)
VIAB = np.bmat([[ps_In,I],[-ps_In,I]])
d = sympy.var('d')
X2VI = sympy.Matrix(np.matrix([
    [0,0,0,1,d,1],
    [0,d,-1,0,0,0,],
    [1,0,-1,0,0,0],
]) * VIAB.T)
M = sympy.Matrix(X2VI[:,0:3]).inv() * sympy.Matrix(X2VI[:,3:6])
M = M.expand()
M.simplify()
M
```

```python
ps_In = np.eye(4, dtype = object)
VIAB = np.bmat([[ps_In,I],[-ps_In,I]])
d = sympy.var('d')
X2VI = sympy.Matrix(np.matrix([
    [0,0,0,0,1,1,0,1],
    [1,0,d,-1,0,0,0,0,],
    [0,1,-d,-1,0,0,0,0],
    [1,-1,0,0,0,0,0,0],
])) * sympy.Matrix(VIAB).inv()
M = sympy.Matrix(X2VI[:,0:4]).pinv() * sympy.Matrix(X2VI[:,4:8])
M = M.expand()
M.simplify()
display(M)
np.linalg.eig(np.array(M.n()).astype(float))
```

```python
ps_In = np.eye(4, dtype = object)
VIAB = np.bmat([[ps_In,I],[-ps_In,I]])
d = sympy.var('d')
X2VI = sympy.Matrix(np.matrix([
    [0,0,0,0,1,0,d,1],
    [0,0,0,0,0,1,-d,1],
    [0,0,0,0,1,1,0,1],
    [1,0,0,-1 ,0,0,0,0],
    [0,1,0,-1,0,0,0,0],
])) * sympy.Matrix(VIAB).inv()
display(X2VI)
M = sympy.Matrix(X2VI[:,0:4]).pinv() * sympy.Matrix(X2VI[:,4:8])
M = M.expand()
M.simplify()
display(M)
#np.linalg.eig(np.array(M.n()).astype(float))
```

```python
d = sympy.var('d')
IV = np.eye(3, dtype = object)
IV[2,2] = 1/d
IC = np.eye(3, dtype = object)
IC[2,2] = d
VIAB = np.bmat([[IV,IV],[-IC,IC]])
d = sympy.var('d')
X2VI = sympy.Matrix([
    [0, 0,0,  1, 1,0],
    [0, 0,0,  0,-1,1],
    [-1,1,1,  0, 0,0],
]) * sympy.Matrix(VIAB).inv()
display(X2VI)
M = sympy.Matrix(X2VI[:,0:3]).pinv() * sympy.Matrix(X2VI[:,3:6])
M = M.expand()
M.simplify()
display(M)
if isinstance(d, (float,int)):
    print(np.linalg.eig(np.array(M.n()).astype(float)))
```

```python
#For a simple machine
r = sympy.var('r')
#r = 100
IV = np.eye(2, dtype = object)
IC = np.eye(2, dtype = object)
VIAB = np.bmat([[IV,IV],[-IC,IC]])
X2VI = sympy.Matrix([
    [0, 0, 1, r*1],
    [-r,1, 0, 0],
]) * sympy.Matrix(VIAB).inv()
display(X2VI)
M = sympy.Matrix(X2VI[:,0:2]).pinv() * sympy.Matrix(X2VI[:,2:4])
M = M.expand()
M.simplify()
display(M)
if isinstance(r, (float,int)):
    print(np.linalg.eig(np.array(M.n()).astype(float)))
```

```python
#Based on the simple machine APPEARS TO BE GOOD
d = sympy.var('d')
IV = np.eye(3, dtype = object)
#IV[2,2] = 1/d
IC = np.eye(3, dtype = object)
#IC[2,2] = 1/d
VIAB = np.bmat([[IV,IV],[-IC,IC]])
#d = -1
X2VI = sympy.Matrix([
    [0,  0,0,   1, 1, 0],
    [0,  0,0,   1, 0, 1/d],
    [-1, 1,d,   0,0 , 0],
]) * sympy.Matrix(VIAB).inv()
display(X2VI)
M = sympy.Matrix(X2VI[:,0:3]).inv() * sympy.Matrix(X2VI[:,3:6])
M = M.expand()
den = d**2 + 2
M = den * M
M.simplify()
display(M)
if isinstance(d, (float,int)):
    print(np.linalg.eig(np.array((M/den).n()).astype(float)))
```

```python
#Based on the simple machine
d = sympy.var('d')
IV = np.eye(3, dtype = object)
#IV[2,2] = 1/d
IC = np.eye(3, dtype = object)
#IC[2,2] = 1/d
VIAB = np.bmat([[IV,IV],[-IC,IC]])
#d = -1
X2VI = sympy.Matrix([
    [0,  0,0,   -1, 1,-1/d],
    [0,  0,0,   1, 1,  1/d],
    [-1, 1,d,   0, 0,  0],
]) * sympy.Matrix(VIAB).inv()
display(X2VI)
M = sympy.Matrix(X2VI[:,0:3]).inv() * sympy.Matrix(X2VI[:,3:6])
M = M.expand()
den = 1# d**2 + 2
M = den * M
M.simplify()
display(M)
if isinstance(d, (float,int)):
    print(np.linalg.eig(np.array((M/den).n()).astype(float)))
```

```python
#NOT THIS ONE
d = sympy.var('d')
IV = np.eye(3, dtype = object)
IV[2,2] = d
IC = np.eye(3, dtype = object)
IC[2,2] = 1/d
VIAB = np.bmat([[IV,IV],[-IC,IC]])
X2VI = sympy.Matrix([
    [0, 0,0,  1, 1, 0],
    [0, 0,0,  1, -1,1],
    [-1,1,1,  0, 0,0],
]) * sympy.Matrix(VIAB).inv()
display(X2VI)
M = sympy.Matrix(X2VI[:,0:3]).pinv() * sympy.Matrix(X2VI[:,3:6])
M = M.expand()
M.simplify()
display(M)
if isinstance(d, (float,int)):
    print(np.linalg.eig(np.array(M.n()).astype(float)))
```

```python
ps_In = np.eye(2, dtype = object)
VIAB = np.bmat([[-ps_In,-ps_In],[ps_In,-ps_In]])
d = sympy.var('d')
X2VI = sympy.Matrix(np.matrix([
    [0,0,d,1],
    [-d,1,0,0,],
])) * sympy.Matrix(VIAB).inv()
M = sympy.Matrix(X2VI[:,2:4]).inv() * sympy.Matrix(X2VI[:,0:2])
M = M.expand()
M.simplify()
M
```

```python
X2VI
```

```python

```