```python
import numpy as np
import pandas as pd
import scipy, scipy.signal

from datetime import date
import time

from random import seed
from random import random
import random
import os, os.path
import shutil

from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression

from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import classification_report

import matplotlib
import matplotlib.pyplot as plt
from pylab import imshow

import h5py
import sys
sys.path.append('/Users/hn/Documents/00_GitHub/Ag/NASA/Python_codes/')
import NASA_core as nc
# import NASA_plot_core.py as rcp
```

```python
from tslearn.metrics import dtw as dtw_metric
# https://dtaidistance.readthedocs.io/en/latest/usage/dtw.html
from dtaidistance import dtw
from dtaidistance import dtw_visualisation as dtwvis
```

```python
x = np.arange(0, 20, .5)
s1 = np.sin(x)
s2 = np.sin(x - 1)
path = dtw.warping_path(s1, s2)
dtwvis.plot_warping(s1, s2, path)
distance = dtw.distance(s1, s2)
```

# Read Fields Metadata

```python
meta_dir = "/Users/hn/Documents/01_research_data/NASA/parameters/"
meta = pd.read_csv(meta_dir+"evaluation_set.csv")
meta.head(2)
```

      ID
      CropTyp
      Irrigtn
      DataSrc
      Acres
      ExctAcr
      LstSrvD
      county

      0
      100010_WSDA_SF_2017
      alfalfa hay
      center pivot
      wsda
      34
      34.310305
      2017/09/12
      Grant

      1
      100204_WSDA_SF_2017
      alfalfa hay
      center pivot
      wsda
      62
      61.826535
      2017/08/09
      Grant

```python
meta_moreThan10Acr=meta[meta.Acres>=10]
print (meta.shape)
print (meta_moreThan10Acr.shape)
```

    (6340, 8)
    (3613, 8)

# Read Training Set Labels

```python
training_set_dir = "/Users/hn/Documents/01_research_data/NASA/ML_data/"
ground_truth_labels = pd.read_csv(training_set_dir+"train_labels.csv")
print ("Unique Votes: ", ground_truth_labels.Vote.unique())
print (len(ground_truth_labels.ID.unique()))
ground_truth_labels.head(2)
```

    Unique Votes:  [2 1]
    1849

      ID
      Vote

      0
      99837_WSDA_SF_2017
      2

      1
      114615_WSDA_SF_2017
      1

```python

```

# Read the Data

```python
VI_idx = "EVI"
data_dir = "/Users/hn/Documents/01_research_data/NASA/VI_TS/04_regularized_TS/"
```

```python
file_names = ["regular_Walla2015_EVI_JFD.csv", "regular_AdamBenton2016_EVI_JFD.csv",
              "regular_Grant2017_EVI_JFD.csv", "regular_FranklinYakima2018_EVI_JFD.csv"]

data=pd.DataFrame()

for file in file_names:
    curr_file=pd.read_csv(data_dir + file)
    curr_file['human_system_start_time'] = pd.to_datetime(curr_file['human_system_start_time'])

    # These data are for 3 years. The middle one is the correct one
    all_years = sorted(curr_file.human_system_start_time.dt.year.unique())
    if len(all_years)==3 or len(all_years)==2:
        proper_year = all_years[1]
    elif len(all_years)==1:
        proper_year = all_years[0]

    curr_file = curr_file[curr_file.human_system_start_time.dt.year==proper_year]
    data=pd.concat([data, curr_file])

data.reset_index(drop=True, inplace=True)
data.head(2)
```

      ID
      human_system_start_time
      EVI

      0
      135073_WSDA_SF_2015
      2015-01-10
      0.049624

      1
      135073_WSDA_SF_2015
      2015-01-20
      0.026503

```python
ground_truth = data[data.ID.isin(list(ground_truth_labels.ID.unique()))].copy()
```

# Toss small fields

```python
ground_truth = ground_truth[ground_truth.ID.isin((list(meta_moreThan10Acr.ID)))].copy()
ground_truth_labels = ground_truth_labels[ground_truth_labels.ID.isin((list(meta_moreThan10Acr.ID)))].copy()
```

# Sort the order of time-series and experts' labels identically

```python
ground_truth.sort_values(by=["ID", 'human_system_start_time'], inplace=True)
ground_truth_labels.sort_values(by=["ID"], inplace=True)

ground_truth.reset_index(drop=True, inplace=True)
ground_truth_labels.reset_index(drop=True, inplace=True)

assert (len(ground_truth.ID.unique()) == len(ground_truth_labels.ID.unique()))

print (list(ground_truth.ID)[0])
print (list(ground_truth_labels.ID)[0])
print ("____________________________________")
print (list(ground_truth.ID)[-1])
print (list(ground_truth_labels.ID)[-1])
print ("____________________________________")
print (list(ground_truth.ID.unique())==list(ground_truth_labels.ID.unique()))
```

    100048_WSDA_SF_2017
    100048_WSDA_SF_2017
    ____________________________________
    99909_WSDA_SF_2017
    99909_WSDA_SF_2017
    ____________________________________
    True

```python
ground_truth.head(2)
```

      ID
      human_system_start_time
      EVI

      0
      100048_WSDA_SF_2017
      2017-01-06
      0.0

      1
      100048_WSDA_SF_2017
      2017-01-16
      0.0

```python
ground_truth_labels.head(2)
```

      ID
      Vote

      0
      100048_WSDA_SF_2017
      1

      1
      100081_WSDA_SF_2017
      1

```python
mins = ground_truth.groupby("ID")[VI_idx].min()
maxs = ground_truth.groupby("ID")[VI_idx].max()
_ranges = maxs-mins
_ranges = pd.DataFrame(_ranges)
_ranges.reset_index(inplace=True)

mins = pd.DataFrame(mins)
mins.reset_index(inplace=True)

_ranges.rename(columns = {'EVI':'EVI_range'}, inplace = True)
mins.rename(columns = {'EVI':'EVI_min'}, inplace = True)

_ranges.head(2)
```

      ID
      EVI_range

      0
      100048_WSDA_SF_2017
      0.602470

      1
      100081_WSDA_SF_2017
      0.721754

```python
ground_truth = pd.merge(ground_truth, _ranges, on=['ID'], how='left')
ground_truth = pd.merge(ground_truth, mins, on=['ID'], how='left')
ground_truth["EVI_ratio"] = (ground_truth["EVI"]-ground_truth["EVI_min"])/ground_truth["EVI_range"]
ground_truth.head(2)
```

      ID
      human_system_start_time
      EVI
      EVI_range
      EVI_min
      EVI_ratio

      0
      100048_WSDA_SF_2017
      2017-01-06
      0.0
      0.60247
      0.0
      0.0

      1
      100048_WSDA_SF_2017
      2017-01-16
      0.0
      0.60247
      0.0
      0.0

```python
ID_1=ground_truth.ID.unique()[0]
ID_2=ground_truth.ID.unique()[12]

# _minimum = ground_truth[ground_truth.ID==ID_1].EVI.values.min()
# _range = ground_truth[ground_truth.ID==ID_1].EVI.values.max()-_minimum
# y_1 = (ground_truth[ground_truth.ID==ID_1].EVI.values-_minimum)/_range
# _minimum = ground_truth[ground_truth.ID==ID_2].EVI.values.min()
# _range = ground_truth[ground_truth.ID==ID_2].EVI.values.max()-_minimum
# y_2 = (ground_truth[ground_truth.ID==ID_2].EVI.values-_minimum)/_range

dtw_score = dtw_metric(ground_truth[ground_truth.ID==ID_1].EVI.values,
                ground_truth[ground_truth.ID==ID_2].EVI.values)

dtw_score_ratios = dtw_metric(ground_truth[ground_truth.ID==ID_1].EVI_ratio.values,
                       ground_truth[ground_truth.ID==ID_2].EVI_ratio.values)

# print ("dtw score is {:.2f}.".format(dtw_score))

plt.figure(figsize=(20,4))
plt.subplot(1, 1, 1)

# plot EVIs
plt.plot(range(len(ground_truth[ground_truth.ID==ID_1].human_system_start_time.values)),
         ground_truth[ground_truth.ID==ID_1].EVI.values,
        c='k', linewidth=5);

plt.plot(range(len(ground_truth[ground_truth.ID==ID_2].human_system_start_time.values)),
         ground_truth[ground_truth.ID==ID_2].EVI.values,
        c='red', linewidth=5);

# plot ratios
plt.plot(range(len(ground_truth[ground_truth.ID==ID_1].human_system_start_time.values)),
         ground_truth[ground_truth.ID==ID_1].EVI_ratio.values,
         ".-.", c='k', linewidth=2, label="ratios");

plt.plot(range(len(ground_truth[ground_truth.ID==ID_2].human_system_start_time.values)),
         ground_truth[ground_truth.ID==ID_2].EVI_ratio.values,
         ".-.", c='red', linewidth=2, label="ratios");

title = "dtw score for EVI is [{:.2f}] and for ratios is [{:.2f}].".format(dtw_score, dtw_score_ratios)
plt.ylim([-0.2, 1.05]);
plt.title(title , fontsize = 20);
```

```python
s1=ground_truth[ground_truth.ID==ID_1].EVI_ratio.values
s2=ground_truth[ground_truth.ID==ID_2].EVI_ratio.values
path = dtw.warping_path(s1, s2)
dtwvis.plot_warping(s1, s2, path)
distance = dtw.distance(s1, s2)
```

```python
d, paths = dtw.warping_paths(s1, s2, window=10, use_pruning=True);
best_path = dtw.best_path(paths);
dtwvis.plot_warpingpaths(s1, s2, paths, best_path);
```

# Human Activity

```python
# HA_dir = "/Users/hn/Downloads/UCI_HAR_Dataset/"

# x_train_file = open(HA_dir + '/train/X_train.txt', 'r')
# y_train_file = open(HA_dir + '/train/y_train.txt', 'r')
# x_test_file = open(HA_dir + '/test/X_test.txt', 'r')
# y_test_file = open(HA_dir + '/test/y_test.txt', 'r')
# # Create empty lists
# x_train = []
# y_train = []
# x_test = []
# y_test = []
# # Mapping table for classes
# labels = {1:'WALKING', 2:'WALKING UPSTAIRS', 3:'WALKING DOWNSTAIRS',
#           4:'SITTING', 5:'STANDING', 6:'LAYING'}
# # Loop through datasets
# for x in x_train_file:
#     x_train.append([float(ts) for ts in x.split()])

# for y in y_train_file:
#     y_train.append(int(y.rstrip('\n')))

# for x in x_test_file:
#     x_test.append([float(ts) for ts in x.split()])

# for y in y_test_file:
#     y_test.append(int(y.rstrip('\n')))

# # Convert to numpy for efficiency
# x_train = np.array(x_train)
# y_train = np.array(y_train)
# x_test = np.array(x_test)
# y_test = np.array(y_test)
# colors = ['#D62728','#2C9F2C','#FD7F23','#1F77B4','#9467BD',
#           '#8C564A','#7F7F7F','#1FBECF','#E377C2','#BCBD27']

# d,_ = dtw.warping_paths(s1, s2, window=10, use_pruning=True);
# print (d)

# d,_ = dtw.warping_paths(s1, s2);
# print (d)

# %%time
# idx=0
# for r in range(1000):
#     distance_prune = dtw.distance(x_train[idx], x_test[r], window=20, use_pruning='True')

# for r in range(1000): # len(x_test)
#     distance = dtw.distance(x_train[idx], x_test[r])

# def classifyNN(k:int, idx:int) -> str:
#     idxs=range(0,x_train.shape[0])
#     n=x_train.shape[0]
#     distances=[]
#     counters={}
#     c=1;
#     max_value=0
```

```python
def DTW_prune(ts1, ts2):
    d,_ = dtw.warping_paths(ts1, ts2, window=10, use_pruning=True);
    return d

```

# Widen Ground Truth Table

```python
EVI_colnames = [VI_idx + "_" + str(ii) for ii in range(1, 37) ]
columnNames = ["ID"] + EVI_colnames
ground_truth_wide = pd.DataFrame(columns=columnNames,
                             index=range(len(ground_truth.ID.unique())))
ground_truth_wide["ID"] = ground_truth.ID.unique()

for an_ID in ground_truth.ID.unique():
    curr_df = ground_truth[ground_truth.ID==an_ID]

    ground_truth_wide_indx = ground_truth_wide[ground_truth_wide.ID==an_ID].index
    ground_truth_wide.loc[ground_truth_wide_indx, "EVI_1":"EVI_36"] = curr_df.EVI.values[:36]
```

```python
ground_truth_wide.head(2)
```

      ID
      EVI_1
      EVI_2
      EVI_3
      EVI_4
      EVI_5
      EVI_6
      EVI_7
      EVI_8
      EVI_9
      ...
      EVI_27
      EVI_28
      EVI_29
      EVI_30
      EVI_31
      EVI_32
      EVI_33
      EVI_34
      EVI_35
      EVI_36

      0
      100048_WSDA_SF_2017
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.0
      0.072227
      0.088491
      ...
      0.111507
      0.119534
      0.13248
      0.061173
      0.055843
      0.050513
      0.045183
      0.039854
      0.045165
      0.050476

      1
      100081_WSDA_SF_2017
      0.014832
      0.167038
      0.319244
      0.47145
      0.378153
      0.284855
      0.198468
      0.290683
      0.398445
      ...
      0.096717
      0.117915
      0.159058
      0.121182
      0.060591
      0.0
      0.050004
      0.100009
      0.102269
      0.104529

2 rows × 37 columns

# Split Train and Test Set

#### Make sure rows of ```ground_truth_allBands``` and ```ground_truth_labels``` are in the same order

```python
ground_truth_labels = ground_truth_labels.set_index('ID')
ground_truth_labels = ground_truth_labels.reindex(index=ground_truth_wide['ID'])
ground_truth_labels = ground_truth_labels.reset_index()
```

```python
ground_truth_labels.head(2)
```

      ID
      Vote

      0
      100048_WSDA_SF_2017
      1

      1
      100081_WSDA_SF_2017
      1

```python
x_train_df, x_test_df, y_train_df, y_test_df = train_test_split(ground_truth_wide,
                                                                ground_truth_labels,
                                                                test_size=0.2,
                                                                random_state=0,
                                                                shuffle=True,
                                                                stratify=ground_truth_labels.Vote.values)
```

# Train

```python
%%time
parameters = {'n_neighbors':[2, 4, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 20],
              "weights":["uniform"]}
KNN_DTW_prune = GridSearchCV(KNeighborsClassifier(metric=DTW_prune), parameters, cv=5, verbose=1)
KNN_DTW_prune.fit(x_train_df.iloc[:, 1:], y_train_df.Vote.values)
```

    Fitting 5 folds for each of 13 candidates, totalling 65 fits
    CPU times: user 3h 41min 43s, sys: 54.5 s, total: 3h 42min 38s
    Wall time: 3h 44min 1s

    GridSearchCV(cv=5,
                 estimator=KNeighborsClassifier(metric=<function DTW_prune at 0x7fd238b10160>),
                 param_grid={'n_neighbors': [2, 4, 8, 9, 10, 11, 12, 13, 14, 15, 16,
                                             17, 20],
                             'weights': ['uniform']},
                 verbose=1)

```python
print (KNN_DTW_prune.best_params_)
```

    {'n_neighbors': 16, 'weights': 'uniform'}

```python
x_test_df = x_test_df[x_test_df.ID.isin((list(meta_moreThan10Acr.ID)))].copy()
y_test_df = y_test_df[y_test_df.ID.isin((list(meta_moreThan10Acr.ID)))].copy()
```

```python
%%time
KNN_DTW_prune_predictions = KNN_DTW_prune.predict(x_test_df.iloc[:, 1:])
```

    CPU times: user 5min 24s, sys: 1.11 s, total: 5min 25s
    Wall time: 5min 26s

```python
(KNN_DTW_prune_predictions==y_test_df.Vote).sum()
```

    261

# Definitions

  - **Precision** Of all instances we predict $\hat y = 1$, what fraction is actually 1.
     \begin{equation}\label{eq:precision}
        \text{Precision} = \frac{\text{TP}}{\text{TP} + \text{FP}}
     \end{equation}

  - **Recall** Of all instances that are actually $y = 1$, what fraction we predict 1.
     \begin{equation}\label{eq:recall}
         \text{Recall} = \text{TPR} = \frac{\text{TP}}{\text{TP} + \text{FN}}
     \end{equation}

  - **Specifity** Fraction of all negative instances that are incorrectly predicted positive.
     \begin{equation}\label{eq:specifity}
        \text{Specifity} = \text{FPR} = \frac{\text{FP}}{\text{TN} + \text{FP}}\\
     \end{equation}

  - **F-Score** Adjust $\beta$ for trade off between  precision and recall. For precision oriented task $\beta = 0.5$.
     \begin{equation}\label{eq:Fscore}
        F_\beta = \frac{(1+\beta^2) \times \text{TP}}{ (1+\beta^2) \times  \text{TP} + \beta^2 \text{FN} + \text{FP}}
     \end{equation}

```python
y_test_df_copy=y_test_df.copy()
y_test_df_copy["KNN_DTW_prune_predictions"] = list(KNN_DTW_prune_predictions)
y_test_df_copy.head(2)
```

      ID
      Vote
      KNN_DTW_prune_predictions

      1321
      98828_WSDA_SF_2017
      1
      1

      299
      105536_WSDA_SF_2018
      1
      1

```python

```

```python
true_single_predicted_single=0
true_single_predicted_double=0

true_double_predicted_single=0
true_double_predicted_double=0

for index in y_test_df_copy.index:
    curr_vote=list(y_test_df_copy[y_test_df_copy.index==index].Vote)[0]
    curr_predict=list(y_test_df_copy[y_test_df_copy.index==index].KNN_DTW_prune_predictions)[0]
    if curr_vote==curr_predict:
        if curr_vote==1:
            true_single_predicted_single+=1
        else:
            true_double_predicted_double+=1
    else:
        if curr_vote==1:
            true_single_predicted_double+=1
        else:
            true_double_predicted_single+=1
```

# Confusion Table for Test Set

```python
confus_tbl_test = pd.DataFrame(columns=['NoName', 'Predict_Single', 'Predict_Double'],
                               index=range(2))
confus_tbl_test.loc[0, 'NoName'] = 'Actual_Single'
confus_tbl_test.loc[1, 'NoName'] = 'Actual_Double'
confus_tbl_test['Predict_Single']=0
confus_tbl_test['Predict_Double']=0

confus_tbl_test.loc[0, "Predict_Single"]=true_single_predicted_single
confus_tbl_test.loc[0, "Predict_Double"]=true_single_predicted_double
confus_tbl_test.loc[1, "Predict_Single"]=true_double_predicted_single
confus_tbl_test.loc[1, "Predict_Double"]=true_double_predicted_double
confus_tbl_test
```

      NoName
      Predict_Single
      Predict_Double

      0
      Actual_Single
      217
      5

      1
      Actual_Double
      6
      44

```python
print(classification_report(y_test_df.Vote, KNN_DTW_prune_predictions))
```

                  precision    recall  f1-score   support

               1       0.97      0.98      0.98       222
               2       0.90      0.88      0.89        50

        accuracy                           0.96       272
       macro avg       0.94      0.93      0.93       272
    weighted avg       0.96      0.96      0.96       272

```python
TP
```

    217

```python
print ("IF **Single** is positive and double is negative:")
print ("")

TP = list(confus_tbl_test[confus_tbl_test.NoName=="Actual_Single"]["Predict_Single"])[0]
FP = list(confus_tbl_test[confus_tbl_test.NoName=="Actual_Double"]["Predict_Single"])[0]
FN = list(confus_tbl_test[confus_tbl_test.NoName=="Actual_Single"]["Predict_Double"])[0]
print ("Precision is [{0:.2f}]".format(TP/(TP+FP)))
print ("Recall is [{0:.2f}]".format(TP/(TP+FN)))

print ("_________________________________________________________")
print ("IF **Single** is negative. double is positive:")

TP = list(confus_tbl_test[confus_tbl_test.NoName=="Actual_Double"]["Predict_Double"])[0]
FP = list(confus_tbl_test[confus_tbl_test.NoName=="Actual_Single"]["Predict_Double"])[0]
FN = list(confus_tbl_test[confus_tbl_test.NoName=="Actual_Double"]["Predict_Single"])[0]

print ("")
print ("Precision is [{0:.2f}]".format(TP/(TP+FP)))
print ("Recall is [{0:.2f}]".format(TP/(TP+FN)))
```

    IF **Single** is positive and double is negative:

    Precision is [0.97]
    Recall is [0.98]
    _________________________________________________________
    IF **Single** is negative. double is positive:

    Precision is [0.90]
    Recall is [0.88]

# Export Trained Model

```python
import pickle
model_dir = "/Users/hn/Documents/01_research_data/NASA/ML_Models/"

filename = model_dir + "00_KNN_DTW_prune_" + KNN_DTW_prune.best_params_["weights"] + "Weight_" + \
                        str(KNN_DTW_prune.best_params_["n_neighbors"]) + "NNisBest_"\
                       "from_00_KNN_TimeWarping_playGround.sav"

pickle.dump(KNN_DTW_prune, open(filename, 'wb'))
```

```python
# How to load the saved model:
# loaded_model_KNN_DTW_prune = pickle.load(open(filename, 'rb'))
# loaded_model.predict(x_test_df.iloc[0:2, 1:])
```

```python
# %%time
# parameters = {'n_neighbors':[10]}
# KNN_DTW_prune_10NN = GridSearchCV(KNeighborsClassifier(metric=DTW_prune), parameters, cv=5, verbose=1);
# KNN_DTW_prune_10NN.fit(x_train_df.iloc[:, 1:], y_train_df.Vote.values);
# %%time
# KNN_DTW_prune_10NN_predictions = KNN_DTW_prune_10NN.predict(x_test_df.iloc[:, 1:])
```

```python

```

# Improve

### Lets see if putting weights will make a difference

```python
%%time
parameters = {'n_neighbors':[2, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20],
              "weights":["distance"]}
KNN_DTW_prune_weightsDistance = GridSearchCV(KNeighborsClassifier(metric=DTW_prune),
                                             parameters, cv=5, verbose=1)
KNN_DTW_prune_weightsDistance.fit(x_train_df.iloc[:, 1:], y_train_df.Vote.values)
```

    Fitting 5 folds for each of 17 candidates, totalling 85 fits
    CPU times: user 4h 51min 12s, sys: 1min 3s, total: 4h 52min 16s
    Wall time: 4h 55min 28s

    GridSearchCV(cv=5,
                 estimator=KNeighborsClassifier(metric=<function DTW_prune at 0x7fd238b10160>),
                 param_grid={'n_neighbors': [2, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14,
                                             15, 16, 17, 18, 19, 20],
                             'weights': ['distance']},
                 verbose=1)

```python
print ("KNN_DTW_prune_weightsDistance.best_params_ is", KNN_DTW_prune_weightsDistance.best_params_)
print ("KNN_DTW_prune.best_params_ is", KNN_DTW_prune.best_params_)
```

    KNN_DTW_prune_weightsDistance.best_params_ is {'n_neighbors': 16, 'weights': 'distance'}
    KNN_DTW_prune.best_params_ is {'n_neighbors': 16, 'weights': 'uniform'}

```python
%%time
KNN_DTW_prune_weightsDistance_predictions = KNN_DTW_prune_weightsDistance.predict(x_test_df.iloc[:, 1:])
```

    CPU times: user 5min 21s, sys: 1.09 s, total: 5min 22s
    Wall time: 5min 27s

```python
print ("******* KNN_DTW_prune_predictions *******")
print(classification_report(y_test_df.Vote, KNN_DTW_prune_predictions))
print ("====================================================================================")
print ("******* KNN_DTW_prune_weightsDistance_predictions *******")
print(classification_report(y_test_df.Vote, KNN_DTW_prune_weightsDistance_predictions))
```

    ******* KNN_DTW_prune_predictions *******
                  precision    recall  f1-score   support

               1       0.97      0.98      0.98       222
               2       0.90      0.88      0.89        50

        accuracy                           0.96       272
       macro avg       0.94      0.93      0.93       272
    weighted avg       0.96      0.96      0.96       272

    ====================================================================================
    ******* KNN_DTW_prune_weightsDistance_predictions *******
                  precision    recall  f1-score   support

               1       0.98      0.98      0.98       222
               2       0.90      0.92      0.91        50

        accuracy                           0.97       272
       macro avg       0.94      0.95      0.95       272
    weighted avg       0.97      0.97      0.97       272

```python
y_test_df_copy=y_test_df.copy()
y_test_df_copy["weightDist_predictions"] = list(KNN_DTW_prune_weightsDistance_predictions)
y_test_df_copy.head(2)

true_single_predicted_single=0
true_single_predicted_double=0

true_double_predicted_single=0
true_double_predicted_double=0

actual_double_predicted_single_IDs=[]
actual_single_predicted_double_IDs=[]

for index in y_test_df_copy.index:
    curr_vote=list(y_test_df_copy[y_test_df_copy.index==index].Vote)[0]
    curr_predict=list(y_test_df_copy[y_test_df_copy.index==index].weightDist_predictions)[0]
    if curr_vote==curr_predict:
        if curr_vote==1:
            true_single_predicted_single+=1
        else:
            true_double_predicted_double+=1
    else:
        if curr_vote==1:
            true_single_predicted_double+=1
            actual_single_predicted_double_IDs+=list(y_test_df_copy[y_test_df_copy.index==index].ID)
        else:
            true_double_predicted_single+=1
            actual_double_predicted_single_IDs += list(y_test_df_copy[y_test_df_copy.index==index].ID)

confus_tbl_test = pd.DataFrame(columns=['None', 'Predict_Single', 'Predict_Double'],
                               index=range(2))
confus_tbl_test.loc[0, 'None'] = 'Actual_Single'
confus_tbl_test.loc[1, 'None'] = 'Actual_Double'
confus_tbl_test['Predict_Single']=0
confus_tbl_test['Predict_Double']=0

confus_tbl_test.loc[0, "Predict_Single"]=true_single_predicted_single
confus_tbl_test.loc[0, "Predict_Double"]=true_single_predicted_double
confus_tbl_test.loc[1, "Predict_Single"]=true_double_predicted_single
confus_tbl_test.loc[1, "Predict_Double"]=true_double_predicted_double
confus_tbl_test
```

      None
      Predict_Single
      Predict_Double

      0
      Actual_Single
      217
      5

      1
      Actual_Double
      4
      46

```python
import pickle
model_dir = "/Users/hn/Documents/01_research_data/NASA/ML_Models/"
filename = model_dir + "00_KNN_DTW_prune_" + KNN_DTW_prune_weightsDistance.best_params_["weights"] + \
                       "Weight_" + str(KNN_DTW_prune_weightsDistance.best_params_["n_neighbors"]) + \
                       "NNisBest_from_00_KNN_TimeWarping_playGround.sav"

pickle.dump(KNN_DTW_prune_weightsDistance, open(filename, 'wb'))
```

```python
actual_double_predicted_single_IDs
```

    ['6577_WSDA_SF_2016',
     '99837_WSDA_SF_2017',
     '99655_WSDA_SF_2017',
     '99064_WSDA_SF_2017']

```python
actual_single_predicted_double_IDs
```

    ['6423_WSDA_SF_2016',
     '41901_WSDA_SF_2018',
     '101372_WSDA_SF_2017',
     '31787_WSDA_SF_2018',
     '153779_WSDA_SF_2015']

```python
plt.figure(figsize=(20,4))
plt.subplot(1, 1, 1)

ID_1 = actual_double_predicted_single_IDs[4]
# plot EVI0
# plt.plot(range(len(ground_truth[ground_truth.ID==ID_1].human_system_start_time.values)),
#          ground_truth[ground_truth.ID==ID_1].EVI.values,
#         c='k', linewidth=5);

plt.plot(ground_truth[ground_truth.ID==ID_1].human_system_start_time.values,
         ground_truth[ground_truth.ID==ID_1].EVI.values,
         c='k', linewidth=5);

title = list(meta[meta.ID==ID_1].CropTyp)[0].replace(",", "")  + ", " + ID_1
plt.ylim([-0.2, 1.05]);
plt.title(title , fontsize=20);
```

```python
meta[meta.ID.isin(actual_double_predicted_single_IDs)]
```

```python
meta[meta.ID.isin(actual_single_predicted_double_IDs)]
```

```python
plt.figure(figsize=(20,4))
plt.subplot(1, 1, 1)

ID_1 = actual_single_predicted_double_IDs[0]
# plot EVIs
# plt.plot(range(len(ground_truth[ground_truth.ID==ID_1].human_system_start_time.values)),
#          ground_truth[ground_truth.ID==ID_1].EVI.values,
#         c='k', linewidth=5);

plt.plot(ground_truth[ground_truth.ID==ID_1].human_system_start_time.values,
         ground_truth[ground_truth.ID==ID_1].EVI.values,
        c='k', linewidth=5);

title = list(meta[meta.ID==ID_1].CropTyp)[0].replace(",", "")  + ", " + ID_1
plt.ylim([-0.2, 1.05]);
plt.title(title , fontsize = 20);
```

```python

```

```python

```

## Fast WTD

```python

# import array
# s1 = array.array('d',[0, 0, 1, 2, 1, 0, 1, 0, 0])
# s2 = array.array('d',[0, 1, 2, 0, 0, 0, 0, 0, 0])
# d = dtw.distance_fast(s1, s2, use_pruning=True)
```

```python
# pip install -vvv --upgrade --force-reinstall dtaidistance
```

```python
import numpy as np
import matplotlib.pyplot as plt
from sklearn.naive_bayes import GaussianNB
from sklearn.svm import SVC
from sklearn.datasets import load_digits
from sklearn.model_selection import learning_curve
from sklearn.model_selection import ShuffleSplit

def plot_learning_curve(estimator, title,
                        X, y, axes=None, ylim=None,
                        cv=None, n_jobs=None,
                        train_sizes=np.linspace(0.1, 1.0, 5)):
    """
    Generate 3 plots: the test and training learning curve, the training
    samples vs fit times curve, the fit times vs score curve.

    Parameters
    ----------
    estimator : estimator instance
        An estimator instance implementing `fit` and `predict` methods which
        will be cloned for each validation.

    title : str
        Title for the chart.

    X : array-like of shape (n_samples, n_features)
        Training vector, where ``n_samples`` is the number of samples and
        ``n_features`` is the number of features.

    y : array-like of shape (n_samples) or (n_samples, n_features)
        Target relative to ``X`` for classification or regression;
        None for unsupervised learning.

    axes : array-like of shape (3,), default=None
        Axes to use for plotting the curves.

    ylim : tuple of shape (2,), default=None
        Defines minimum and maximum y-values plotted, e.g. (ymin, ymax).

    cv : int, cross-validation generator or an iterable, default=None
        Determines the cross-validation splitting strategy.
        Possible inputs for cv are:

          - None, to use the default 5-fold cross-validation,
          - integer, to specify the number of folds.
          - :term:`CV splitter`,
          - An iterable yielding (train, test) splits as arrays of indices.

        For integer/None inputs, if ``y`` is binary or multiclass,
        :class:`StratifiedKFold` used. If the estimator is not a classifier
        or if ``y`` is neither binary nor multiclass, :class:`KFold` is used.

        Refer :ref:`User Guide <cross_validation>` for the various
        cross-validators that can be used here.

    n_jobs : int or None, default=None
        Number of jobs to run in parallel.
        ``None`` means 1 unless in a :obj:`joblib.parallel_backend` context.
        ``-1`` means using all processors. See :term:`Glossary <n_jobs>`
        for more details.

    train_sizes : array-like of shape (n_ticks,)
        Relative or absolute numbers of training examples that will be used to
        generate the learning curve. If the ``dtype`` is float, it is regarded
        as a fraction of the maximum size of the training set (that is
        determined by the selected validation method), i.e. it has to be within
        (0, 1]. Otherwise it is interpreted as absolute sizes of the training
        sets. Note that for classification the number of samples usually have
        to be big enough to contain at least one sample from each class.
        (default: np.linspace(0.1, 1.0, 5))
    """
    if axes is None:
        _, axes = plt.subplots(1, 3, figsize=(20, 5))

    axes[0].set_title(title)
    if ylim is not None:
        axes[0].set_ylim(*ylim)
    axes[0].set_xlabel("Training examples")
    axes[0].set_ylabel("Score")

    train_sizes, train_scores, test_scores, fit_times, _ = learning_curve(
        estimator,
        X,
        y,
        cv=cv,
        n_jobs=n_jobs,
        train_sizes=train_sizes,
        return_times=True,
    )
    train_scores_mean = np.mean(train_scores, axis=1)
    train_scores_std = np.std(train_scores, axis=1)
    test_scores_mean = np.mean(test_scores, axis=1)
    test_scores_std = np.std(test_scores, axis=1)
    fit_times_mean = np.mean(fit_times, axis=1)
    fit_times_std = np.std(fit_times, axis=1)

    # Plot learning curve
    axes[0].grid()
    axes[0].fill_between(
        train_sizes,
        train_scores_mean - train_scores_std,
        train_scores_mean + train_scores_std,
        alpha=0.1,
        color="r",
    )
    axes[0].fill_between(
        train_sizes,
        test_scores_mean - test_scores_std,
        test_scores_mean + test_scores_std,
        alpha=0.1,
        color="g",
    )
    axes[0].plot(
        train_sizes, train_scores_mean, "o-", color="r", label="Training score"
    )
    axes[0].plot(
        train_sizes, test_scores_mean, "o-", color="g", label="Cross-validation score"
    )
    axes[0].legend(loc="best")

    # Plot n_samples vs fit_times
    axes[1].grid()
    axes[1].plot(train_sizes, fit_times_mean, "o-")
    axes[1].fill_between(
        train_sizes,
        fit_times_mean - fit_times_std,
        fit_times_mean + fit_times_std,
        alpha=0.1,
    )
    axes[1].set_xlabel("Training examples")
    axes[1].set_ylabel("fit_times")
    axes[1].set_title("Scalability of the model")

    # Plot fit_time vs score
    fit_time_argsort = fit_times_mean.argsort()
    fit_time_sorted = fit_times_mean[fit_time_argsort]
    test_scores_mean_sorted = test_scores_mean[fit_time_argsort]
    test_scores_std_sorted = test_scores_std[fit_time_argsort]
    axes[2].grid()
    axes[2].plot(fit_time_sorted, test_scores_mean_sorted, "o-")
    axes[2].fill_between(
        fit_time_sorted,
        test_scores_mean_sorted - test_scores_std_sorted,
        test_scores_mean_sorted + test_scores_std_sorted,
        alpha=0.1,
    )
    axes[2].set_xlabel("fit_times")
    axes[2].set_ylabel("Score")
    axes[2].set_title("Performance of the model")

    return plt

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

X, y = load_digits(return_X_y=True)

title = "Learning Curves (Naive Bayes)"
# Cross validation with 50 iterations to get smoother mean test and train
# score curves, each time with 20% data randomly selected as a validation set.
cv = ShuffleSplit(n_splits=50, test_size=0.2, random_state=0)

estimator = GaussianNB()
plot_learning_curve(
    estimator, title, X, y, axes=axes[:, 0], ylim=(0.7, 1.01), cv=cv, n_jobs=4
)

title = r"Learning Curves"
# SVC is more expensive so we do a lower number of CV iterations:
cv = ShuffleSplit(n_splits=10, test_size=0.1, random_state=0)
estimator = SVC(gamma=0.001)
plot_learning_curve(estimator, title,
                    X=ground_truth_wide.loc[:, "EVI_1":],
                    y=ground_truth_labels.Vote.values,
                    axes=axes[:, 1], ylim=(0.7, 1.01), cv=cv, n_jobs=4)

plt.show()
```

```python

```

```python

```

```python

```


```python
# %load /Users/facai/Study/book_notes/preconfig.py
%matplotlib inline

from IPython.display import Image
```

TensorFlow 梯度函数
=========

版本信息：remotes/upstream/r1.8
```
~/W/g/t/t/cc ❯❯❯ git log -n 1
commit 8753e2ebde6c58b56675cc19ab7ff83072824a62 (HEAD, upstream/r1.8)
Author: Yifei Feng <1192265+yifeif@users.noreply.github.com>
Date:   Fri Apr 27 17:05:02 2018 -0700

    Fixing the mock import error for devel docker. (#18940)

    * Fixing the mock import error for devel docker.

    Same as #18843
```

### 0 常见位置

梯度函数在Python侧和C++侧，均有独立的注册机制。

+ 对于Python侧，一般在 `tensorflow/python/ops/*_grad.py`。

+ 对于C++侧，有两个独立的注册位置：
  - `cc/gradients/*_grad.cc`: 这里是和python对应的移植实现，层级简单。
  - `core/ops/*_grad.cc`: 这里似乎是为Function准备的，我对它了解不多，层级很复杂，目前似乎是实验状态，暂不讲，以后清晰了再说。
     注: 这个接口有意思的地方是，它的输入输出是强类型，挺好。

下面我画了个简单的类层级关系，用`AbsGrad`梯度为例，有些地方不严谨，作示意导航用。

```python
Image('./res/tf_gradients.jpg')
```

    IOPub data rate exceeded.
    The notebook server will temporarily stop sending output
    to the client in order to avoid crashing it.
    To change this limit, set the config variable
    `--NotebookApp.iopub_data_rate_limit`.

我们一般是在python端调用`tf.gradients`显式计算梯度。这个方法会从子图的末端算子往前回溯，并同时查询每个算子注册的梯度，串接起来。所以，在TensorFlow中，使用的都是解析梯度，而只有在梯度单元测试中，才会用数值梯度来做校验。详细说明可见[does-tensorflow-use-automatic-or-symbolic-gradients](https://stackoverflow.com/questions/36370129/does-tensorflow-use-automatic-or-symbolic-gradients)

### 1 如何写Gradient函数

官方有说明文档，见 [Implement the gradient in Python](https://www.tensorflow.org/extend/adding_an_op#implement_the_gradient_in_pythoN) 。这里就不赘述，举几个实际例子方便理解。

##### 1.1 SquareGrad

第一类是最常见的实现形式，在Python和C++各自独立实现同样逻辑。

以平方算子为例，$y = x ^ 2$，它的梯度是$dy / dx = 2x$。反向传播时要带上后级梯度，所以结果是$dL / dx = \text{grad} * 2 x$。

+ Python侧代码如下：

    ```python
    @ops.RegisterGradient("Square")
    def _SquareGrad(op, grad):
      x = op.inputs[0]
      # Added control dependencies to prevent 2*x from being computed too early.
      with ops.control_dependencies([grad]):
        x = math_ops.conj(x)
        y = constant_op.constant(2.0, dtype=x.dtype)
        return math_ops.multiply(grad, math_ops.multiply(x, y))
    ```

+ C++侧代码如下：

    ```cpp
    Status SquareGrad(const Scope& scope, const Operation& op,
                      const std::vector<Output>& grad_inputs,
                      std::vector<Output>* grad_outputs) {
      // dy/dx = (2 * x)
      auto two = Cast(scope, Const(scope, 2), op.input(0).type());
      auto dydx = Mul(scope, two, op.input(0));
      // grad(x) = grad(y) * conj(dy/dx)
      grad_outputs->push_back(
          Mul(scope, grad_inputs[0], ConjugateHelper(scope, dydx)));
      return scope.status();
    }
    REGISTER_GRADIENT_OP("Square", SquareGrad);
    ```

    附上些简要说明：
    + `op.input`和`op.output`是`Square`输入x和输出y = x^2。
    + `grad_inputs[0]`是反向传导的后级导数`dy`，其实是`dL/dy`的简写。
    + 这个函数要计算`dx`，是`dy/dx`的简写, 结果是2 x。
    + 返回结果写入`grad_outputs`里，`dL/dx = dL/dy * dy/dx`。

    如果输入有多个`x = (x0, x1, ..., xn)`，要分别算对应的导数`[dL/dx0, dL/dx1, ... dL/dxn]`。

代码比较简单，唯一会让人困惑的点，可能是我们如何知道`op.input`, `op.output`和`grad_inputs`各自对应的具体参数，下面细说下。

##### 关于`op.input`和`op.output`

其实这里输入输出对应的是算子的c++接口定义，一般我们可以在它的API定义(core/api_def/base_api/*.pbtxt)或者算子注册文件(core/ops/*.cc)里找到定义。更简单的方法，是调用程序，直接输出算子的定义，如下：

```python
In [28]: b = tf.square(a)

In [29]: b.op.op_def
Out[29]:
name: "Square"
input_arg {
  name: "x"
  type_attr: "T"
}
output_arg {
  name: "y"
  type_attr: "T"
}
attr {
  name: "T"
  type: "type"
  allowed_values {
    list {
      type: DT_HALF
      type: DT_BFLOAT16
      type: DT_FLOAT
      type: DT_DOUBLE
      type: DT_INT32
      type: DT_INT64
      type: DT_COMPLEX64
      type: DT_COMPLEX128
    }
  }
}
```

我们可以看到，对于Square算子，它只有一个输入$x$，和一个输出$y$，所以它的`op.input(0) = x`, `op.output(0) = y`。

##### 关于`grad_inputs`

这里其实是对应于后级输出`op.output`，有几个输出，每个输出就有一个梯度返回。对于Square算子，它只有一个输出，所以也只有一个`grad_inputs(0) = dL / dy`。

##### 1.2 SqrtGrad

第二类梯度是为了运算效率加速，把梯度本身实现成一个单独的算子，然后在C++和Python端调用同样的底层算子。

以开方为例，$y = \sqrt(x)$，它的梯度是$dy / dx = \frac1{2} x ^{- \frac1{2}} = 0.5 / y$。所以完整结果是$dL / dx = 0.5 * \text{grad} / y$。

这个算子首先在eigen名字空间下实现了梯度逻辑的kernel：
```bash
/core/kernels/cwise_ops.h: 含有Eigen扩展：functor和numext
/core/kernels/cwise_ops_gradients.h: 导数
/core/kernels/cwise_op_sqrt.cc: 注册核函数
```

```cpp
// Gradient for the sqrt function
template <typename T>
struct scalar_sqrt_gradient_op {
  EIGEN_EMPTY_STRUCT_CTOR(scalar_sqrt_gradient_op)
  EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE const T
  operator()(const T& output, const T& output_gradient) const {
    const T out_conj = numext::conj(output);
    return static_cast<T>(0.5) * output_gradient / out_conj;
  }
  template <typename Packet>
  EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE const Packet
  packetOp(const Packet& output, const Packet& output_gradient) const {
    const Packet const_half = pset1<Packet>(static_cast<T>(0.5));
    const Packet out_conj = pconj(output);
    return pdiv(pmul(const_half, output_gradient), out_conj);
  }
};
```

然后在`/core/ops/math_ops.cc`下注册接口：

```cpp
REGISTER_OP("SqrtGrad").UNARY_GRADIENT_COMPLEX();
```

接下来在梯度注册代码处，调用这个算子就可以：

+ Python端调用
    ```python
    @ops.RegisterGradient("Sqrt")
    def _SqrtGrad(op, grad):
      y = op.outputs[0]  # y = x^(1/2)
      return gen_math_ops.sqrt_grad(y, grad)
    ```

+ C++端调用

    ```cpp
    Status SqrtGrad(const Scope& scope, const Operation& op,
                    const std::vector<Output>& grad_inputs,
                    std::vector<Output>* grad_outputs) {
      // Use the built-in operator.
      grad_outputs->push_back(
          internal::SqrtGrad(scope, op.output(0), grad_inputs[0]));
      return scope.status();
    }
    REGISTER_GRADIENT_OP("Sqrt", SqrtGrad);
    ```

##### 1.2 SqrtGradGrad

第三类是一些名字如`xxxGradGrad`的梯度算子。因为反向传播是利用的一阶导的链式法式，我们应该把它单纯当作是`xxxGrad`公式展开的一阶导，而不要把它视为`xxx`的二阶导，否则会带来很多困惑。

以`SqrtGradGrad`算子为例，它旳实际计算式是$dy = 0.5 * \text{grad} / y$，有两个输入grad和$y$，所以它有两个导数：

为了和代码符号一致，我们改写实际计算式为$y = 0.5 * b / a$：

\begin{align}
    dL / da &= \text{grad} * 0.5 * b * -1 * a^{-2} \\
            &= - \text{grad} * 0.5 * b * / a / a \\
            &= - \text{grad} * y / a \\
            &= - \text{grad} / a * y \\
            &= - \text{ga} * y \\
    dL / db &= \text{grad} * 0.5 / a \\
            &= 0.5 * \text{ga} \\
\end{align}

所以Python侧实现代码是：

```python
@ops.RegisterGradient("SqrtGrad")
def _SqrtGradGrad(op, grad):
  a = op.inputs[0]
  y = op.outputs[0]  # y = 0.5 * b / conj(a)
  with ops.control_dependencies([grad]):
    ga = grad / a
    return -math_ops.conj(ga) * y, 0.5 * ga
```

强调，实际计算式一定是和具体实现对应的，要把`xxxGrad`理解成一个算子，它有实际输入和输出，而不要把它认为是`xxx`的附属。

### 2 小结

本文简要介绍了TensorFlow的梯度相关架构，并给出几个具体梯度实现的分析例子。

```python

```


# Casing Integrity Example

- background resistivity average is 10 Ωm
- For each well we measure the radial electric fields as far out as the well is deep. We also measured magnetic fields (tangential) at a few points.
- Return Electrode: 1000m from the wellhead, perpendicular to the array of receivers (wire ran close to a straight line).
- freqs DC (1e-6), 0.1, and 1Hz
- Surface casing : outer diameter 10 3/4 in, thickness = 0.011m - set depth 1242 ft
- Production casing : outer diameter 7 in, production = 0.008m - set depth 6079 ft
- KB 13.5ft above

```python
import discretize
from discretize import utils
import numpy as np
import scipy.sparse as sp
import sympy
import matplotlib.pyplot as plt
from matplotlib.colors import LogNorm
from scipy.constants import mu_0, inch, foot

from SimPEG.EM import FDEM
from SimPEG import Utils, Maps

import casingSimulations

from pymatsolver import Pardiso

%matplotlib inline
```

```python
simDir = '1km_well_10ohm_m_back'
```

## Set up model parameters

```python
# Casing diameters
surface_casing_diameter = 10.75*inch
production_casing_diameter = 7*inch

# casing depths (below surface)
kb = 13.5
surface_casing_depth = (1242-kb)*foot
production_casing_depth = (6079-kb)*foot

print(surface_casing_depth, production_casing_depth)
print(surface_casing_diameter, production_casing_diameter)
```

    (374.44679999999994, 1848.7643999999998)
    (0.27305, 0.17779999999999999)

### Simplified model

```python
# Casing parameters
casing_d = 20e-2 # first pass, lets use a 20cm casing diameter
casing_t = 1e-2 # 1cm casing thickness
casing_l = 1000 # here we use a 1km well and will explore the impacts of meshing choices

freqs = np.r_[1e-4, 1e-3, 1e-2, 0.1, 1.]  # 1e-6

sigma_back = 1e-1
sigma_inside = 1.

src_a = np.r_[0., np.pi, 0.]
src_b = np.r_[1e3, np.pi, 0.]
```

```python
# set up the casing properties
cp = casingSimulations.CasingParameters(
    sigma_back = sigma_back,
    sigma_layer = sigma_back,
    sigma_inside = sigma_inside,
#     sigma_air = 1e-6,
    casing_d = casing_d,
    casing_l = casing_l,
    casing_t = casing_t,
    freqs = freqs,
    src_a = src_a,
    src_b = src_b
)
```

```python
cp.save(directory=simDir)
```

```python
cp.serialize()
```

    {u'__class__': 'CasingParameters',
     'casing_d': 0.2,
     'casing_l': 1000.0,
     'casing_t': 0.01,
     'casing_top': 0.0,
     'freqs': [0.0001, 0.001, 0.01, 0.1, 1.0],
     'layer_z': [-1000.0, -900.0],
     'mur_casing': 100.0,
     'sigma_air': 1e-06,
     'sigma_back': 0.1,
     'sigma_casing': 5500000.0,
     'sigma_inside': 1.0,
     'sigma_layer': 0.1,
     'src_a': [0.0, 3.141592653589793, 0.0],
     'src_b': [1000.0, 3.141592653589793, 0.0],
     'version': u'0.0.1'}

```python
cp.skin_depth()
```

    array([ 159154.9430919 ,   50329.21210449,   15915.49430919,
              5032.92121045,    1591.54943092])

## Set up mesh

```python
npadx, npadz = 20, 27
dx2 = casing_l  # go out as far as the casing is deep
csx2 = 25.
csz = 2.5
pfx2 = 1.4
pfz = 1.4

ncy = 1
nstretchy = 5
stretchfact = 1.5
hy = utils.meshTensor([(1, nstretchy, -stretchfact), (1, ncy), (1, nstretchy, stretchfact)])
hy = hy * 2*np.pi/hy.sum()

cylMeshGen = casingSimulations.CasingMeshGenerator(
    cp=cp,
    npadx=npadx,
    npadz=npadz,
    domain_x2=dx2,
    hy=hy,
    csx2=csx2,
    csz=csz,
    pfx2=pfx2,
    pfz=pfz
)
```

```python
cp.skin_depth()
```

    array([ 159154.9430919 ,   50329.21210449,   15915.49430919,
              5032.92121045,    1591.54943092])

```python
cylMeshGen.mesh.plotGrid()
```

```python
print(
    'nC: {}, max_x: {}, max_z: {}'.format(
        cylMeshGen.mesh.nC, cylMeshGen.mesh.vectorNx.max(), cylMeshGen.mesh.vectorNz.max()
    )
)
```

    nC: 709456, max_x: 74222.2234971, max_z: 77176.684807

```python
cylMeshGen.save(directory=simDir)
```

```python
cylmesh = cylMeshGen.mesh
```

```python
src = casingSimulations.sources.TopCasingSrc(cp, cylMeshGen.mesh)
```

```python
ax = plt.subplot(111, projection='polar')
cylmesh.plotGrid(ax=ax, slice='z')
ax.plot(cylmesh.gridFx[src.surface_wire,1], cylmesh.gridFx[src.surface_wire,0], 'ro')
ax.set_rlim([0., 1900])
```

```python
fig, ax = plt.subplots(1, 1)
cylmesh.plotGrid(ax=ax, slice='theta')
src.plot(ax=ax)

ax.set_xlim([800., 1200.])
ax.set_ylim([-10., 30.])
```

```python
physprops = casingSimulations.PhysicalProperties(cylmesh, cp)
```

```python
mesh2D = discretize.CylMesh([cylmesh.hx, 1., cylmesh.hz], x0 = cylmesh.x0)
```

```python
xlim = [0.08, 0.15]
ylim = [-10., 10.]
fig, ax = plt.subplots(1, 2, figsize=(12, 4))

murplt = physprops.mur.reshape(cylmesh.vnC, order='F')

plt.colorbar(mesh2D.plotImage(utils.mkvc(murplt[:,0,:]), ax=ax[0], mirror=True)[0], ax=ax[0])
ax[0].set_xlim(xlim)
ax[0].set_ylim(ylim)
src.plot(ax=ax[0])

sigmaplt = physprops.sigma.reshape(cylmesh.vnC, order='F')

plt.colorbar(
    mesh2D.plotImage(
        utils.mkvc(sigmaplt[:,0,:]), ax=ax[1], mirror=True, pcolorOpts = {'norm':LogNorm()}
    )[0], ax=ax[1],

)
ax[1].set_xlim(xlim)
ax[1].set_ylim(ylim)

plt.tight_layout()
```

```python
casingSimulations.utils.writeSimulationPy(
    directory=simDir,
    srcType='TopCasingSrc',
)
```

    wrote 1km_well_10ohm_m_back_airn6/simulation.py

## Set up and run 2D simulations

```python
meshGen2D = casingSimulations.CasingMeshGenerator(
    cp=cp,
    npadx=npadx,
    npadz=npadz,
    domain_x2=dx2,
    csx2=csx2,
    csz=csz,
    pfx2=pfx2,
    pfz=pfz
)
```

```python
mesh2D = meshGen2D.mesh
```

```python
mesh2D.plotGrid()
```

```python
sim2d = casingSimulations.run.SimulationFDEM(
    cp=cp, meshGenerator=meshGen2D, directory=simDir,
    simulation_filename='simulation2D.json', fields_filename='fields2D.npy',
    srcType='TopCasingSrc'
)
```

```python
%%time
fields2D = sim2d.run()
```

    Validating parameters...
          max x: 74222.2234971, min z: -78176.684807, max z: 77176.684807
    Starting Simulation
    Elapsed time : 3.25501680374
    CPU times: user 2.62 s, sys: 391 ms, total: 3.02 s
    Wall time: 3.27 s

# Load up 3D results

```python
meshGen = casingSimulations.load_properties('/'.join([simDir, 'MeshParameters.json']))
cp = casingSimulations.load_properties('/'.join([simDir, 'CasingParameters.json']))
hfield = np.load('/'.join([simDir, 'fields.npy']))
# simulation = casingSimulations.load_properties(
#     '/'.join([simDir, 'simulationParameters.json']),
#     targetClass=casingSimulations.run.SimulationFDEM
# )
```

```python
sim3D = casingSimulations.run.SimulationFDEM(cp=cp, meshGenerator=meshGen, srcType='TopCasingSrc')
sim3D.prob.model = sim3D.physprops.model
```

```python
%%time
fields3D = sim3D.prob.fieldsPair(meshGen.mesh, sim3D.survey)
```

    CPU times: user 3min 50s, sys: 7 s, total: 3min 57s
    Wall time: 4min 17s

```python
fields3D[:,'hSolution'] = hfield
srcList = sim3D.survey.srcList
```

```python
cylmesh = meshGen.mesh
src = sim3D.src
```

```python
cp.skin_depth()
```

    array([ 159154.9430919 ,   50329.21210449,   15915.49430919,
              5032.92121045,    1591.54943092])

## Set up and run 3D DC

```python
from SimPEG.EM.Static import DC
```

```python
src_a = meshGen.mesh.gridFz[sim3D.src.tophole_electrode, :][0, :] - np.r_[0., 0., meshGen.csz/2.]
src_b = meshGen.mesh.gridFz[sim3D.src.surface_electrode, :][0, :] - np.r_[0., 0., meshGen.csz/2.]

srcDC = DC.Src.Dipole([], src_a, src_b)
probDC = DC.Problem3D_CC(meshGen.mesh, sigmaMap=sim3D.physprops.wires.sigma, bc_type='Dirichlet', Solver=Pardiso)
surveyDC = DC.Survey([srcDC])

probDC.pair(surveyDC)
```

```python
# %%time
# fieldsDC = probDC.fields(sim3D.physprops.model)
# np.save('/'.join([simDir, 'fieldsDC.npy']), fieldsDC[:, 'phiSolution'])
phi = np.load('/'.join([simDir, 'fieldsDC.npy']))
probDC.sigma = sim3D.physprops.sigma
fieldsDC = probDC.fieldsPair(cylmesh, surveyDC)
fieldsDC[:,'phiSolution'] = phi
```

# Plot the Results

```python
import ipywidgets
```

```python
mesh2D = discretize.CylMesh([cylMeshGen.mesh.hx, 1., cylMeshGen.mesh.hz], x0=cylMeshGen.mesh.x0)

srcind = 2
theta_ind = 5

plotopts = {
    'range_x':np.r_[0., 2000.],
    'range_y':np.r_[-2000., 10.],
    'sample_grid':np.r_[10., 10.],
    'logScale':True,
    'clim':[1e-9, 1e-5]
}

def plotCurrents(srcind=0, theta_ind=5):
    jcyl = fields3D[srcList[srcind],'j']
    # j2D = fields2D[sim2d.survey.srcList[srcind], 'j']
    jDC = fieldsDC[:,'j']

    jplt = casingSimulations.face3DthetaSlice(cylMeshGen.mesh, jcyl, theta_ind=theta_ind)
    jDCplt = casingSimulations.face3DthetaSlice(cylMeshGen.mesh, jDC, theta_ind=theta_ind)

    fig, ax = plt.subplots(1,3, figsize=(15, 5))
    ax = utils.mkvc(ax)

    reim = 'real'
    casingSimulations.plotFace2D(
        mesh2D,
        jplt, real_or_imag=reim, ax=ax[0],
        **plotopts
    )
    ax[0].set_title('3D {}, {} Hz'.format(reim, srcList[srcind].freq))

    casingSimulations.plotFace2D(
        mesh2D,
        jDCplt, real_or_imag='real', ax=ax[1],
        **plotopts
    )
    ax[1].set_title('3D {}'.format('DC'))

    casingSimulations.plotFace2D(
        mesh2D,
        jplt-jDCplt, real_or_imag='real', ax=ax[2],
        **plotopts
    )
    ax[2].set_title('EM - DC')

    plt.tight_layout()
    plt.show()

ipywidgets.interact(plotCurrents, srcind=ipywidgets.IntSlider(min=0, max=len(srcList)-1, step=1), theta_ind=ipywidgets.IntSlider(min=0, max=cylMeshGen.mesh.nCy-1, step=1))
```

```python
mesh2D = discretize.CylMesh([meshGen.mesh.hx, 1., meshGen.mesh.hz], x0=meshGen.mesh.x0)

srcind = 2
theta_ind = 5

plotopts = {
    'range_x':np.r_[0., 2000.],
    'range_y':np.r_[-2000., 100.],
    'sample_grid':np.r_[10., 10.],
    'logScale':True,
    'clim':[1e-9, 1e-4]
}

def plotEfield(srcind=0, theta_ind=5):
    ecyl = fields3D[srcList[srcind],'e']
    # e2D = fields2D[sim2d.survey.srcList[srcind], 'e']
    eDC = fieldsDC[:,'e']

    eplt = casingSimulations.face3DthetaSlice(meshGen.mesh, ecyl, theta_ind=theta_ind)
    eDCplt = casingSimulations.face3DthetaSlice(meshGen.mesh, eDC, theta_ind=theta_ind)

    fig, ax = plt.subplots(1,3, figsize=(15, 5))
    ax = utils.mkvc(ax)

    reim = 'real'
    casingSimulations.plotFace2D(
        mesh2D,
        eplt, real_or_imag=reim, ax=ax[0],
        **plotopts
    )
    ax[0].set_title('3D {}, {} Hz'.format(reim, srcList[srcind].freq))

    casingSimulations.plotFace2D(
        mesh2D,
        eDCplt, real_or_imag='real', ax=ax[1],
        **plotopts
    )
    ax[1].set_title('3D {}'.format('DC'))

    casingSimulations.plotFace2D(
        mesh2D,
        eplt-eDCplt, real_or_imag='real', ax=ax[2],
        **plotopts
    )
    ax[2].set_title('EM - DC')

    plt.tight_layout()
    plt.show()

ipywidgets.interact(plotEfield, srcind=ipywidgets.IntSlider(min=0, max=len(srcList)-1, step=1), theta_ind=ipywidgets.IntSlider(min=0, max=meshGen.mesh.nCy-1, step=1))
```

```python
fig, ax = plt.subplots(1, 2, figsize=(10, 4))
ax = utils.mkvc(ax)

casingSimulations.view.plotLinesFx(
    cylmesh,
    sim3D.survey.srcList[2:],
    fields3D=fields3D,
    fieldsDC=fieldsDC,
    fieldType='e',
    pltType='loglog',
    ax=ax,
    theta_ind=0,
    xlim=[10., 10000.],
    zloc=0.
)
```

```python
cp.serialize()
```

    {u'__class__': 'CasingParameters',
     'casing_d': 0.2,
     'casing_l': 1000.0,
     'casing_t': 0.01,
     'casing_top': 0.0,
     'freqs': [0.0001, 0.001, 0.01, 0.1, 1.0],
     'layer_z': [-1000.0, -900.0],
     'mur_casing': 100.0,
     'sigma_air': 1e-06,
     'sigma_back': 0.1,
     'sigma_casing': 5500000.0,
     'sigma_inside': 1.0,
     'sigma_layer': 0.1,
     'src_a': [0.0, 3.141592653589793, 0.0],
     'src_b': [1000.0, 3.141592653589793, 0.0],
     'version': u'0.0.1'}

```python
a = np.ones(10)
```

```python
a.dtype("complex")
```

```python
a.astype("complex")
```

    array([ 1.+0.j,  1.+0.j,  1.+0.j,  1.+0.j,  1.+0.j,  1.+0.j,  1.+0.j,
            1.+0.j,  1.+0.j,  1.+0.j])

```python

```


# DQN

Originally from https://skettee.github.io/post/q_network/ (in Korean)

## Load Libraries and Extensions

```python
%reload_ext autoreload
%autoreload 2
%matplotlib inline
```

```python
import random
from collections import deque
from IPython.display import display, clear_output, Pretty
from keras import Sequential
from keras.layers import Dense
from keras.models import load_model
import numpy as np
import os
from pprint import pprint
from time import sleep
from tqdm import tqdm_notebook as tqdm

import gym
```

    Using TensorFlow backend.

## Cart Pole Environment

```python
ENV_NAME = 'CartPole-v1'
N_STEP = 100
N_EPISODE = 10
```

```python
env = gym.make(ENV_NAME)
state = env.reset()
action = env.action_space.sample()

for i_episode in range(N_EPISODE):
    observation = env.reset()
    for t in range(N_STEP):
        env.render()

        action = env.action_space.sample()
        observation, reward, done, info = env.step(action)
        if done:
            print('Episode finished after {} steps'.format(t + 1))
            sleep(1)
            break

env.close()

print('State space: ', env.observation_space)
print('Initial state: ', state)
print('\nAction space: ', env.action_space)
print('Random action: ', action)
```

    Episode finished after 14 steps
    Episode finished after 9 steps
    Episode finished after 13 steps
    Episode finished after 10 steps
    Episode finished after 16 steps
    Episode finished after 15 steps
    Episode finished after 20 steps
    Episode finished after 21 steps
    Episode finished after 38 steps
    Episode finished after 34 steps
    State space:  Box(4,)
    Initial state:  [-0.03022219 -0.01513947  0.01063969  0.02018719]

    Action space:  Discrete(2)
    Random action:  1

### Action

$A = \{0, 1\}$

Num	| Action
----|----
0 |	Push cart to the left
1 |	Push cart to the right

### State

$S = \begin{bmatrix}
\{ s_{00}, s_{01}, \cdots\}, \\
\{ s_{10}, s_{11}, \cdots \}, \\
\{ s_{20}, s_{21}, \cdots \}, \\
\{ s_{30}, s_{31}, \cdots \}
\end{bmatrix}$

Index | State                   | Min      | Max
-----|-----------------|--------|------
0      | Cart Position       | -4.8    | 4.8
1       | Cart Velocity       | -Inf     | Inf
2      | Pole Angle           | -24 deg | 24 deg
3      | Pole Velocity At Tip | -Inf     | Inf

### Reward

+1 for each step including the terminal step.

## DQN Model Training

$X = \{ x_1^{(i)}, x_2^{(i)}, x_3^{(i)}, x_4^{(i)} \}$

$Y = \{ q_1^{(i)}, q_2^{(i)} \}$

$\begin{align}
J(w) & = \mathbb E_{\pi} [(q - \hat q)^2] \\
& =  \mathbb E_{\pi} \left [\left (R_{t+1} + \gamma \max_{a'} Q(S_{t+1}, a') - Q(S_t, A_t, w) \right)^2 \right ]
\end{align}$

$ w \leftarrow w-\alpha \dfrac{\partial J(w,b)}{\partial w}$

$\hat A_t = \text{argmax}_{a'}Q(S_t, a', w)$

```python
# Q-Network Modeling
num_state = env.observation_space.shape[0]
num_action = env.action_space.n

model = Sequential()
model.add(Dense(32, input_dim= num_state, activation='relu'))
model.add(Dense(32, activation='relu'))
model.add(Dense(num_action, activation=None))
model.compile(loss='mse', optimizer="adam")
```

```python
model.summary()
```

    Model: "sequential_1"
    _________________________________________________________________
    Layer (type)                 Output Shape              Param #
    =================================================================
    dense_1 (Dense)              (None, 32)                160
    _________________________________________________________________
    dense_2 (Dense)              (None, 32)                1056
    _________________________________________________________________
    dense_3 (Dense)              (None, 2)                 66
    =================================================================
    Total params: 1,282
    Trainable params: 1,282
    Non-trainable params: 0
    _________________________________________________________________

```python
num_iteration = 500
min_timesteps_per_batch = 2500

# Hyper parameter
epsilon = 0.3
gamma = 0.95
batch_size = 32

# Q-Network Learning
for i in tqdm(range(num_iteration)):
    timesteps_this_batch = 0
    memory = []
    while True:
        state = env.reset()
        done = False
        while not done:
            if np.random.uniform() < epsilon:
                action = env.action_space.sample()
            else:
                q_value = model.predict(state.reshape(1, num_state))
                action = np.argmax(q_value[0])
            next_state, reward, done, info = env.step(action)
            # Memory
            memory.append((state, action, reward, next_state, done))

            state = next_state

        timesteps_this_batch += len(memory)
        if timesteps_this_batch > min_timesteps_per_batch:
            break

    # Replay
    for state, action, reward, next_state, done in memory:
        if done:
            target = reward
        else:
            target = reward + gamma * (np.max(model.predict(next_state.reshape(1, num_state))[0]))
        q_value = model.predict(state.reshape(1, num_state))
        q_value[0][action] = target
        model.fit(state.reshape(1, num_state), q_value, epochs=1, batch_size=batch_size, verbose=0)

env.close()
```

    HBox(children=(IntProgress(value=0, max=500), HTML(value='')))

    WARNING: Logging before flag parsing goes to stderr.
    W1030 00:13:10.926017 4639802816 deprecation_wrapper.py:119] From /Users/jeong/.conda/envs/py36/lib/python3.6/site-packages/keras/backend/tensorflow_backend.py:422: The name tf.global_variables is deprecated. Please use tf.compat.v1.global_variables instead.

```python
save_dir = os.getcwd()
model_name = 'keras_dqn_trained_model.h5'

# Save model and weights
model_path = os.path.join(save_dir, model_name)
model.save(model_path)
```

## Solution

```python
model = load_model(model_path)

env = gym.make(ENV_NAME)
num_state = env.observation_space.shape[0]

for i_episode in range(N_EPISODE):
    state = env.reset()
    done = False
    t = 0

    while not done:
        env.render()

        state = np.array(state).reshape(1, num_state)
        q_value = model.predict(state)
        action = np.argmax(q_value[0])
        state, reward, done, info = env.step(action)

        if done:
            print('Episode finished after {} steps'.format(t + 1))
            sleep(1)
            break
        t += 1

env.close()
```

    Episode finished after 279 steps
    Episode finished after 269 steps
    Episode finished after 277 steps
    Episode finished after 311 steps
    Episode finished after 287 steps
    Episode finished after 283 steps
    Episode finished after 263 steps
    Episode finished after 375 steps
    Episode finished after 285 steps
    Episode finished after 305 steps

```python

```


```python
# Import the packages for experiment
import warnings

warnings.simplefilter("ignore")

import pandas as pd
import numpy as np

import random
import keras

from itertools import product
from joblib import Parallel, delayed
```

# Random Classification Experiment

This experiment will use images from the **CIFAR 100** database (https://www.cs.toronto.edu/~kriz/cifar.html) and showcase the classification efficiency of algorithms in the **Progressive Learning** project (https://github.com/neurodata/progressive-learning).

## Progressive Learning

The Progressive Learning project aims to improve program performance on sequentially learned tasks, proposing a lifelong learning approach.

It contains two different algorithms: **Lifelong Learning Forests** (**L2F**) and **Lifelong Learning Network** (**L2N**). **L2F** uses Uncertainy Forest as transformers, while **L2N** uses deep networks. These two algorithms achieve both forward knowledge transfer and backward knowledge transfer, and this experiment is designed to cover the **L2F** model.

### Choosing hyperparameters

The hyperparameters here are used for determining how the experiment will run.

```python
### MAIN HYPERPARAMS ###
num_points_per_task = 500
shift_num = 6
task_num = 20
tree_num = 10
########################
```

### Loading datasets

The CIFAR 100 database contains 100 classes of 600 images, each separating into 500 training images and 100 testing images.

```python
# load image datasets from the CIFAR-100 database
(X_train, y_train), (X_test, y_test) = keras.datasets.cifar100.load_data()

# modify data shapes for specific model
data_x = np.concatenate([X_train, X_test])
data_x = data_x.reshape(
    (data_x.shape[0], data_x.shape[1] * data_x.shape[2] * data_x.shape[3])
)
data_y = np.concatenate([y_train, y_test])
data_y = data_y[:, 0]
```

### Running experiment

The following codes will run multiple experiments in parallel. For each experiment, we have task_num number of tasks. For each task, we randomly select 10 classes of the classes to train on. As we will observe below, each task increases Backwards Transfer Efficiency (BTE) with respect to Task 1 (Task 1 being the first task corresponding to 10 randomly selected classes).

```python
from functions.random_class_functions import run_parallel_exp

slot_num = int(5000/num_points_per_task)
slot_fold = range(slot_num)
shift_fold = range(1, shift_num + 1, 1)

# run the L2F model
n_trees = [tree_num]
iterable = product(n_trees, shift_fold, slot_fold)
df_results = Parallel(n_jobs=-1, verbose=1)(
    delayed(run_parallel_exp)(
        data_x, data_y, ntree, num_points_per_task, task_num, slot=slot, shift=shift
    )
    for ntree, shift, slot in iterable
)
```

    [Parallel(n_jobs=-1)]: Using backend LokyBackend with 4 concurrent workers.
    [Parallel(n_jobs=-1)]: Done  42 tasks      | elapsed: 54.6min
    [Parallel(n_jobs=-1)]: Done  60 out of  60 | elapsed: 73.8min finished

### Plotting backward transfer efficiency

Backward transfer efficiency (BTE) measures the relative effect of future task data on the performance on a certain task.

\begin{align}
BTE^t(f_n):= E[R^t(f_n^{<t}) / R^t(f_n)]
\end{align}

It is the expected ratio of two risk functions of the learned hypothesis, one with access to the data up to and including the last observation from task t, and the other with access to the entire data sequence. The codes below uses the experiment results to calculate the average BTE numbers and display their changes over tasks learned.

```python
from functions.random_class_functions import calculate_results

# obtain bte results
btes = calculate_results(df_results, slot_num, shift_num)

# calculate the average numbers
bte = np.mean(btes, axis=0)

# setting plot parameters
fontsize = 22
ticksize = 20
```

```python
from functions.random_class_functions import plot_bte

plot_bte(bte, fontsize, ticksize)
```


# Tutorial 2: "How" models
**Week 1, Day 1: Model Types**

**By Neuromatch Academy**

__Content creators:__ Matt Laporte, Byron Galbraith, Konrad Kording

__Content reviewers:__ Dalin Guo, Aishwarya Balwani, Madineh Sarvestani, Maryam Vaziri-Pashkam, Michael Waskom, Ella Batty

**Our 2021 Sponsors, including Presenting Sponsor Facebook Reality Labs**

___
# Tutorial Objectives

*Estimated timing of tutorial: 45 minutes*

This is tutorial 2 of a 3-part series on different flavors of models used to understand neural data. In this tutorial we will explore models that can potentially explain *how* the spiking data we have observed is produced

To understand the mechanisms that give rise to the neural data we save in Tutorial 1,  we will build simple neuronal models and compare their spiking response to real data. We will:
- Write code to simulate a  simple "leaky integrate-and-fire" neuron model
- Make the model more complicated — but also more realistic — by adding more physiologically-inspired details

```python
# @title Tutorial slides

# @markdown These are the slides for the videos in all tutorials today
from IPython.display import IFrame
IFrame(src=f"https://mfr.ca-1.osf.io/render?url=https://osf.io/6dxwe/?direct%26mode=render%26action=download%26mode=render", width=854, height=480)
```

```python
# @title Video 1: "How" models
from ipywidgets import widgets

out2 = widgets.Output()
with out2:
  from IPython.display import IFrame
  class BiliVideo(IFrame):
      def __init__(self, id, page=1, width=400, height=300, **kwargs):
          self.id=id
          src = 'https://player.bilibili.com/player.html?bvid={0}&page={1}'.format(id, page)
          super(BiliVideo, self).__init__(src, width, height, **kwargs)

  video = BiliVideo(id="BV1yV41167Di", width=854, height=480, fs=1)
  print('Video available at https://www.bilibili.com/video/{0}'.format(video.id))
  display(video)

out1 = widgets.Output()
with out1:
  from IPython.display import YouTubeVideo
  video = YouTubeVideo(id="PpnagITsb3E", width=854, height=480, fs=1, rel=0)
  print('Video available at https://youtube.com/watch?v=' + video.id)
  display(video)

out = widgets.Tab([out1, out2])
out.set_title(0, 'Youtube')
out.set_title(1, 'Bilibili')

display(out)
```

# Setup

```python
import numpy as np
import matplotlib.pyplot as plt
from scipy import stats
```

```python
#@title Figure Settings
import ipywidgets as widgets #interactive display

%matplotlib inline
%config InlineBackend.figure_format = 'retina'
plt.style.use("https://raw.githubusercontent.com/NeuromatchAcademy/course-content/master/nma.mplstyle")
```

```python
# @title Plotting Functions

def histogram(counts, bins, vlines=(), ax=None, ax_args=None, **kwargs):
  """Plot a step histogram given counts over bins."""
  if ax is None:
    _, ax = plt.subplots()

  # duplicate the first element of `counts` to match bin edges
  counts = np.insert(counts, 0, counts[0])

  ax.fill_between(bins, counts, step="pre", alpha=0.4, **kwargs)  # area shading
  ax.plot(bins, counts, drawstyle="steps", **kwargs)  # lines

  for x in vlines:
    ax.axvline(x, color='r', linestyle='dotted')  # vertical line

  if ax_args is None:
    ax_args = {}

  # heuristically set max y to leave a bit of room
  ymin, ymax = ax_args.get('ylim', [None, None])
  if ymax is None:
    ymax = np.max(counts)
    if ax_args.get('yscale', 'linear') == 'log':
      ymax *= 1.5
    else:
      ymax *= 1.1
      if ymin is None:
        ymin = 0

  if ymax == ymin:
    ymax = None

  ax_args['ylim'] = [ymin, ymax]

  ax.set(**ax_args)
  ax.autoscale(enable=False, axis='x', tight=True)

def plot_neuron_stats(v, spike_times):
  fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(12, 5))

  # membrane voltage trace
  ax1.plot(v[0:100])
  ax1.set(xlabel='Time', ylabel='Voltage')
  # plot spike events
  for x in spike_times:
    if x >= 100:
      break
    ax1.axvline(x, color='red')

  # ISI distribution
  if len(spike_times)>1:
    isi = np.diff(spike_times)
    n_bins = np.arange(isi.min(), isi.max() + 2) - .5
    counts, bins = np.histogram(isi, n_bins)
    vlines = []
    if len(isi) > 0:
      vlines = [np.mean(isi)]
    xmax = max(20, int(bins[-1])+5)
    histogram(counts, bins, vlines=vlines, ax=ax2, ax_args={
      'xlabel': 'Inter-spike interval',
      'ylabel': 'Number of intervals',
      'xlim': [0, xmax]
    })
  else:
    ax2.set(xlabel='Inter-spike interval',
           ylabel='Number of intervals')
  plt.show()
```

---
# Section 1: The Linear Integrate-and-Fire Neuron

*Note that we'll be using some math to describe our leaky integrate-and-fire neurons. If you need to quickly check the notation (i.e. what a certain variable name stands for), you can look at the notation section after the summary!*

How does a neuron spike?

A neuron charges and discharges an electric field across its cell membrane. The state of this electric field can be described by the _membrane potential_. The membrane potential rises due to excitation of the neuron, and when it reaches a threshold a spike occurs. The potential resets, and must rise to a threshold again before the next spike occurs.

One of the simplest models of spiking neuron behavior is the linear integrate-and-fire model neuron. In this model, the neuron increases its membrane potential $V_m$ over time in response to excitatory input currents $I$ scaled by some factor $\alpha$:

\begin{align}
  dV_m = {\alpha}I
\end{align}

Once $V_m$ reaches a threshold value a spike is produced, $V_m$ is reset to a starting value, and the process continues.

Here, we will take the starting and threshold potentials as $0$ and $1$, respectively. So, for example, if $\alpha I=0.1$ is constant---that is, the input current is constant---then $dV_m=0.1$, and at each timestep the membrane potential $V_m$ increases by $0.1$ until after $(1-0)/0.1 = 10$ timesteps it reaches the threshold and resets to $V_m=0$, and so on.

Note that we define the membrane potential $V_m$ as a scalar: a single real (or floating point) number. However, a biological neuron's membrane potential will not be exactly constant at all points on its cell membrane at a given time. We could capture this variation with a more complex model (e.g. with more numbers). Do we need to?

The proposed model is a 1D simplification. There are many details we could add to it, to preserve different parts of the complex structure and dynamics of a real neuron. If we were interested in small or local changes in the membrane potential, our 1D simplification could be a problem. However, we'll assume an idealized "point" neuron model for our current purpose.

#### Spiking Inputs

Given our simplified model for the neuron dynamics, we still need to consider what form the input $I$ will take. How should we specify the firing behavior of the presynaptic neuron(s) providing the inputs to our model neuron?

Unlike in the simple example above, where $\alpha I=0.1$, the input current is generally not constant. Physical inputs tend to vary with time. We can describe this variation with a distribution.

We'll assume the input current $I$ over a timestep is due to equal contributions from a non-negative ($\ge 0$) integer number of input spikes arriving in that timestep. Our model neuron might integrate currents from 3 input spikes in one timestep, and 7 spikes in the next timestep. We should see similar behavior when sampling from our distribution.

Given no other information about the input neurons, we will also assume that the distribution has a mean (i.e. mean rate, or number of spikes received per timestep), and that the spiking events of the input neuron(s) are independent in time. Are these reasonable assumptions in the context of real neurons?

A suitable distribution given these assumptions is the Poisson distribution, which we'll use to model $I$:

\begin{align}
  I \sim \mathrm{Poisson}(\lambda)
\end{align}

where $\lambda$ is the mean of the distribution: the average rate of spikes received per timestep.

## Coding Exercise 1: Compute $dV_m$

For your first exercise, you will write the code to compute the change in voltage $dV_m$ (per timestep) of the linear integrate-and-fire model neuron. The rest of the code to handle numerical integration is provided for you, so you just need to fill in a definition for `dv` in the `lif_neuron` function below. The value of $\lambda$ for the Poisson random variable is given by the function argument `rate`.

The [`scipy.stats`](https://docs.scipy.org/doc/scipy/reference/stats.html) package is a great resource for working with and sampling from various probability distributions. We will use the `scipy.stats.poisson` class and its method `rvs` to produce Poisson-distributed random samples. In this tutorial, we have imported this package with the alias `stats`, so you should refer to it in your code as `stats.poisson`.

```python
def lif_neuron(n_steps=1000, alpha=0.01, rate=10):
  """ Simulate a linear integrate-and-fire neuron.

  Args:
    n_steps (int): The number of time steps to simulate the neuron's activity.
    alpha (float): The input scaling factor
    rate (int): The mean rate of incoming spikes

  """
  # Precompute Poisson samples for speed
  exc = stats.poisson(rate).rvs(n_steps)

  # Initialize voltage and spike storage
  v = np.zeros(n_steps)
  spike_times = []

  ################################################################################
  # Students: compute dv, then comment out or remove the next line
  raise NotImplementedError("Excercise: compute the change in membrane potential")
  ################################################################################

  # Loop over steps
  for i in range(1, n_steps):

    # Update v
    dv = ...
    v[i] = v[i-1] + dv

    # If spike happens, reset voltage and record
    if v[i] > 1:
      spike_times.append(i)
      v[i] = 0

  return v, spike_times

# Set random seed (for reproducibility)
np.random.seed(12)

# Model LIF neuron
v, spike_times = lif_neuron()

# Visualize
plot_neuron_stats(v, spike_times)
```

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W1D1_ModelTypes/solutions/W1D1_Tutorial2_Solution_9e5a4843.py)

*Example output:*

## Interactive Demo 1: Linear-IF neuron
Like last time, you can now explore how various parametes of the LIF model influence the ISI distribution. Specifically, you can vary `alpha`, which is input scaling factor, and `rate`, which is the mean rate of incoming spikes.

1.   What is the spiking pattern of this model?
2.   What effect does raising or lowering alpha have?
3.   What effect does raising or lowering the rate have?
4.   Does the distribution of ISIs ever look like what you observed in the data in Tutorial 1?

```python
# @markdown You don't need to worry about how the code works – but you do need to **run the cell** to enable the sliders.

def _lif_neuron(n_steps=1000, alpha=0.01, rate=10):
  exc = stats.poisson(rate).rvs(n_steps)
  v = np.zeros(n_steps)
  spike_times = []
  for i in range(1, n_steps):
    dv = alpha * exc[i]
    v[i] = v[i-1] + dv
    if v[i] > 1:
      spike_times.append(i)
      v[i] = 0
  return v, spike_times

@widgets.interact(
  alpha=widgets.FloatLogSlider(0.01, min=-2, max=-1),
  rate=widgets.IntSlider(10, min=5, max=20)
)
def plot_lif_neuron(alpha=0.01, rate=10):
  v, spike_times = _lif_neuron(2000, alpha, rate)

  plot_neuron_stats(v, spike_times)
```

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W1D1_ModelTypes/solutions/W1D1_Tutorial2_Solution_2af538db.py)

```python
# @title Video 2: Linear-IF models
from ipywidgets import widgets

out2 = widgets.Output()
with out2:
  from IPython.display import IFrame
  class BiliVideo(IFrame):
      def __init__(self, id, page=1, width=400, height=300, **kwargs):
          self.id=id
          src = 'https://player.bilibili.com/player.html?bvid={0}&page={1}'.format(id, page)
          super(BiliVideo, self).__init__(src, width, height, **kwargs)

  video = BiliVideo(id="BV1iZ4y1u7en", width=854, height=480, fs=1)
  print('Video available at https://www.bilibili.com/video/{0}'.format(video.id))
  display(video)

out1 = widgets.Output()
with out1:
  from IPython.display import YouTubeVideo
  video = YouTubeVideo(id="QBD7kulhg4U", width=854, height=480, fs=1, rel=0)
  print('Video available at https://youtube.com/watch?v=' + video.id)
  display(video)

out = widgets.Tab([out1, out2])
out.set_title(0, 'Youtube')
out.set_title(1, 'Bilibili')

display(out)
```

---
# Section 2: Inhibitory signals

*Estimated timing to here from start of tutorial: 20 min*

Our linear integrate-and-fire neuron from the previous section was indeed able to produce spikes. However, our ISI histogram doesn't look much like empirical ISI histograms seen in Tutorial 1, which had an exponential-like shape. What is our model neuron missing, given that it doesn't behave like a real neuron?

In the previous model we only considered excitatory behavior -- the only way the membrane potential could decrease was upon a spike event. We know, however, that there are other factors that can drive $V_m$ down. First is the natural tendency of the neuron to return to some steady state or resting potential. We can update our previous model as follows:

\begin{align}
  dV_m = -{\beta}V_m + {\alpha}I
\end{align}

where $V_m$ is the current membrane potential and $\beta$ is some leakage factor. This is a basic form of the popular Leaky Integrate-and-Fire model neuron (for a more detailed discussion of the LIF Neuron, see Bonus Section 2 and the Biological Neuron Models day later in this course).

We also know that in addition to excitatory presynaptic neurons, we can have inhibitory presynaptic neurons as well. We can model these inhibitory neurons with another Poisson random variable:

\begin{align}
I = I_{\mathrm{exc}} - I_{\mathrm{inh}} \\
I_{\mathrm{exc}} \sim \mathrm{Poisson}(\lambda_{\mathrm{exc}}) \\
I_{\mathrm{inh}} \sim \mathrm{Poisson}(\lambda_{\mathrm{inh}})
\end{align}

where $\lambda_{\mathrm{exc}}$ and $\lambda_{\mathrm{inh}}$ are the average spike rates (per timestep) of the excitatory and inhibitory presynaptic neurons, respectively.

### Coding Exercise 2: Compute $dV_m$ with inhibitory signals

For your second exercise, you will again write the code to compute the change in voltage $dV_m$, though now of the LIF model neuron described above. Like last time, the rest of the code needed to handle the neuron dynamics are provided for you, so you just need to fill in a definition for `dv` below.

```python
def lif_neuron_inh(n_steps=1000, alpha=0.5, beta=0.1, exc_rate=10, inh_rate=10):
  """ Simulate a simplified leaky integrate-and-fire neuron with both excitatory
  and inhibitory inputs.

  Args:
    n_steps (int): The number of time steps to simulate the neuron's activity.
    alpha (float): The input scaling factor
    beta (float): The membrane potential leakage factor
    exc_rate (int): The mean rate of the incoming excitatory spikes
    inh_rate (int): The mean rate of the incoming inhibitory spikes
  """

  # precompute Poisson samples for speed
  exc = stats.poisson(exc_rate).rvs(n_steps)
  inh = stats.poisson(inh_rate).rvs(n_steps)

  v = np.zeros(n_steps)
  spike_times = []

  ###############################################################################
  # Students: compute dv, then comment out or remove the next line
  raise NotImplementedError("Excercise: compute the change in membrane potential")
  ################################################################################

  for i in range(1, n_steps):

    dv = ...

    v[i] = v[i-1] + dv
    if v[i] > 1:
      spike_times.append(i)
      v[i] = 0

  return v, spike_times

# Set random seed (for reproducibility)
np.random.seed(12)

# Model LIF neuron
v, spike_times = lif_neuron_inh()

# Visualize
plot_neuron_stats(v, spike_times)
```

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W1D1_ModelTypes/solutions/W1D1_Tutorial2_Solution_6bd84e18.py)

*Example output:*

## Interactive Demo 2: LIF + inhibition neuron

As in Interactive Demo 1, you can play with the parameters of the input to our LIF neuron and visualize what happens. Here, in addition to controlling `alpha`, which scales the input, you can also control `beta`, which is a leakage factor on the voltage, `exc_rate`, which is the mean rate of the excitatory presynaptic neurons, and `inh_rate`, which is the mean rate of the inhibitory presynaptic neurons.

1.   What effect does raising the excitatory rate have?
2.   What effect does raising the inhibitory rate have?
3.   What if you raise both the excitatory and inhibitory rate?
4.   Does the distribution of ISIs ever look like what you observed in the data in Tutorial 1?

```python
#@title
#@markdown **Run the cell** to enable the sliders.
def _lif_neuron_inh(n_steps=1000, alpha=0.5, beta=0.1, exc_rate=10, inh_rate=10):
  """ Simulate a simplified leaky integrate-and-fire neuron with both excitatory
  and inhibitory inputs.

  Args:
    n_steps (int): The number of time steps to simulate the neuron's activity.
    alpha (float): The input scaling factor
    beta (float): The membrane potential leakage factor
    exc_rate (int): The mean rate of the incoming excitatory spikes
    inh_rate (int): The mean rate of the incoming inhibitory spikes
  """
  # precompute Poisson samples for speed
  exc = stats.poisson(exc_rate).rvs(n_steps)
  inh = stats.poisson(inh_rate).rvs(n_steps)

  v = np.zeros(n_steps)
  spike_times = []
  for i in range(1, n_steps):
    dv = -beta * v[i-1] + alpha * (exc[i] - inh[i])
    v[i] = v[i-1] + dv
    if v[i] > 1:
      spike_times.append(i)
      v[i] = 0

  return v, spike_times

@widgets.interact(alpha=widgets.FloatLogSlider(0.5, min=-1, max=1),
                  beta=widgets.FloatLogSlider(0.1, min=-1, max=0),
                  exc_rate=widgets.IntSlider(12, min=10, max=20),
                  inh_rate=widgets.IntSlider(12, min=10, max=20))
def plot_lif_neuron(alpha=0.5, beta=0.1, exc_rate=10, inh_rate=10):
  v, spike_times = _lif_neuron_inh(2000, alpha, beta, exc_rate, inh_rate)
  plot_neuron_stats(v, spike_times)
```

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W1D1_ModelTypes/solutions/W1D1_Tutorial2_Solution_570e738e.py)

```python
# @title Video 3: LIF + inhibition
from ipywidgets import widgets

out2 = widgets.Output()
with out2:
  from IPython.display import IFrame
  class BiliVideo(IFrame):
      def __init__(self, id, page=1, width=400, height=300, **kwargs):
          self.id=id
          src = 'https://player.bilibili.com/player.html?bvid={0}&page={1}'.format(id, page)
          super(BiliVideo, self).__init__(src, width, height, **kwargs)

  video = BiliVideo(id="BV1nV41167mS", width=854, height=480, fs=1)
  print('Video available at https://www.bilibili.com/video/{0}'.format(video.id))
  display(video)

out1 = widgets.Output()
with out1:
  from IPython.display import YouTubeVideo
  video = YouTubeVideo(id="Aq7JrxRkn2w", width=854, height=480, fs=1, rel=0)
  print('Video available at https://youtube.com/watch?v=' + video.id)
  display(video)

out = widgets.Tab([out1, out2])
out.set_title(0, 'Youtube')
out.set_title(1, 'Bilibili')

display(out)
```

---
# Section 3: Reflecting on how models

*Estimated timing to here from start of tutorial: 35 min*

## Think! 3: Reflecting on how models

Please discuss the following questions for around 10 minutes with your group:

- Have you seen how models before?
- Have you ever done one?
- Why are how models useful?
- When are they possible? Does your field have how models?
- What do we learn from constructing them?

# Summary

*Estimated timing of tutorial: 45 minutes*

In this tutorial we gained some intuition for the mechanisms that produce the observed behavior in our real neural data. First, we built a simple neuron model with excitatory input and saw that it's behavior, measured using the ISI distribution, did not match our real neurons. We then improved our model by adding leakiness and inhibitory input. The behavior of this balanced model was much closer to the real neural data.

---
# Notation

\begin{align}
V_m &\quad \text{membrane potential} \\
dV_m &\quad \text{change in membrane potential}\\
C_m &\quad \text{membrane capacitance}\\
I &\quad \text{input current}\\
R_m &\quad \text{membrane resistance}:\\
V_\mathrm{rest} &\quad \text{resting potential}\\
\alpha &\quad \text{scaling factor for input current}\\
\beta &\quad \text{leakage factor}\\
\lambda &\quad \text{average spike rate}\\
\lambda_\mathrm{exc} &\quad \text{average spike rate for excitatory neurons}\\
\lambda_\mathrm{inh} &\quad \text{average spike rate for inhibitory neurons}\\
\end{align}

---
# Bonus

---
## Bonus Section 1: Why do neurons spike?

A neuron stores energy in an electric field across its cell membrane, by controlling the distribution of charges (ions) on either side of the membrane. This energy is rapidly discharged to generate a spike when the field potential (or membrane potential) crosses a threshold. The membrane potential may be driven toward or away from this threshold, depending on inputs from other neurons: excitatory or inhibitory, respectively. The membrane potential tends to revert to a resting potential, for example due to the leakage of ions across the membrane, so that reaching the spiking threshold depends not only on the amount of input ever received following the last spike, but also the timing of the inputs.

The storage of energy by maintaining a field potential across an insulating membrane can be modeled by a capacitor. The leakage of charge across the membrane can be modeled by a resistor. This is the basis for the leaky integrate-and-fire neuron model.

---
## Bonus Section 2: The LIF Model Neuron

The full equation for the LIF neuron is

\begin{align}
C_{m}\frac{dV_m}{dt} = -(V_m - V_{\mathrm{rest}})/R_{m} + I
\end{align}

where $C_m$ is the membrane capacitance, $R_m$ is the membrane resistance, $V_{\mathrm{rest}}$ is the resting potential, and $I$ is some input current (from other neurons, an electrode, ...).

In our above examples we set many of these parameters to convenient values ($C_m = R_m = dt = 1$, $V_{\mathrm{rest}} = 0$) to focus more on the general behavior of the model. However, these too can be manipulated to achieve different dynamics, or to ensure the dimensions of the problem are preserved between simulation units and experimental units (e.g. with $V_m$ given in millivolts, $R_m$ in megaohms, $t$ in milliseconds).