```python
#%run NB01-Load.ipynb
```

```python
import collections
import gc
import IPython
import lightgbm as lgb
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import os
import plotly.graph_objs as go
import plotly.plotly as py
import random
#import seaborn as sb
import seaborn as sns
#import seaborn as snss
#import scipy
import scipy as sp
import sklearn
import statsmodels.api as sm
import statsmodels.formula.api as smf
import sys
import warnings

from datetime import date
from IPython.display import HTML
from lightgbm import LGBMClassifier
from lightgbm import plot_importance
from pandas import DataFrame
from pandas import Series
from patsy import dmatrices
from plotly import tools
from plotly.offline import init_notebook_mode
from plotly.offline import iplot
from pylab import rcParams
from random import choice
from random import choices # Python 3.6+
from random import sample
from sklearn import datasets
from sklearn import model_selection
from sklearn import metrics
from sklearn import preprocessing
#from sklearn.cross_validation import train_test_split
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
from sklearn.metrics import average_precision_score
from sklearn.metrics import classification_report
from sklearn.metrics import confusion_matrix
from sklearn.metrics import mean_squared_error
from sklearn.metrics import precision_recall_curve
from sklearn.metrics import roc_auc_score
from sklearn.metrics import roc_curve
from sklearn.model_selection import KFold
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import Imputer
from sklearn.preprocessing import LabelEncoder
from sklearn.preprocessing import StandardScaler
from statsmodels.stats.outliers_influence import variance_inflation_factor
from xgboost import XGBClassifier
from wordcloud import WordCloud

init_notebook_mode(connected=True)
plt.rcParams.update({'figure.max_open_warning': 200})
# Suppress warnings
warnings.filterwarnings("ignore")

# In a notebook environment, display the plots inline
%matplotlib inline
# Set some parameters to apply to all plots. These can be overridden in each plot if desired
# Plot size to 14" x 7"
matplotlib.rc('figure', figsize = (14, 7))
# Font size to 14
matplotlib.rc('font', size = 14)
# Do not display top and right frame lines
matplotlib.rc('axes.spines', top = False, right = False)
# Remove grid lines
matplotlib.rc('axes', grid = False)
# Set backgound color to white
matplotlib.rc('axes', facecolor = 'white')

np.set_printoptions(threshold=np.nan)
np.set_printoptions(suppress=True, formatter={'float_kind':'{:f}'.format})

#import C:/Development/kaggle--home-credit-default-risk/rand_jitter
#import C:/Development/kaggle--home-credit-default-risk/draw_feature_distribution
import sys
# sys.path.insert(0, 'C:/Development/kaggle--home-credit-default-risk/') # ~= sys.path.prepend
sys.path.append('C:/Development/kaggle--home-credit-default-risk/')
# import rand_jitter
# import draw_feature_distribution
##from rand_jitter import * # NOTE: added directly to draw_feature_distribution_v2
# from draw_feature_distribution import *
# from draw_feature_distribution_v1 import *
from draw_feature_distribution_v2 import *

# C:\Users\jbalcomb\Anaconda3\lib\site-packages\statsmodels\compat\pandas.py:56: FutureWarning:
# The pandas.core.datetools module is deprecated and will be removed in a future version. Please use the pandas.tseries module instead.

```

```python
print("Python version: {}".format(sys.version))
print("pandas version: {}".format(pd.__version__))
print("NumPy version: {}".format(np.__version__))
print("SciPy version: {}".format(sp.__version__))
print("scikit-learn version: {}".format(sklearn.__version__))
print("matplotlib version: {}".format(matplotlib.__version__))
print("IPython version: {}".format(IPython.__version__))
```

```python
random.seed(1234567890)
numpy.random.seed(1234567890)
```

```python
data_file_path = 'C:/Development/kaggle--home-credit-default-risk/data/'
data_file_path
```

```python
#application_train = pd.read_table(path + 'application_train.csv', sep=',', dtype=object)
application_train = pd.read_table(data_file_path + 'application_train.csv', sep=',')
```

```python
application_train.head()
```

```python

```

```python
#application_train[other_columns] = ds13[other_columns].fillna(value = "MISSING")
```

```python

```

```python
application_train__ext_source = application_train.loc[:, ['EXT_SOURCE_1','EXT_SOURCE_2','EXT_SOURCE_3','TARGET']]
```

```python
# Remove observations with missing values
#application_train__ext_source.dropna(inplace = True)
application_train__ext_source__dropna = application_train__ext_source.dropna()
```

```python

```

```python
#Create train and validation set
X_train, X_test, y_train, y_test = train_test_split(application_train__ext_source, application_train__ext_source['TARGET'], random_state=0)
#train_x, valid_x, train_y, valid_y = train_test_split(data, y, test_size=0.2, shuffle=True, stratify=y, random_state=1301)
```

```python
print("X_train shape: {}".format(X_train.shape))
print("y_train shape: {}".format(y_train.shape))
```

```python
print("X_test shape: {}".format(X_test.shape))
print("y_test shape: {}".format(y_test.shape))
```

```python

```

```python

```

```python
logreg = LogisticRegression()
```

```python
# Train model
model = logreg.fit(X_train, y_train)
```

```python
# Predict class
model.predict(X_test)
```

```python
# View predicted probabilities
model.predict_proba(X_test)
```

```python

```

```python

```

```python
logisticRegr = LogisticRegression()
```

```python
logisticRegr.fit(ds_train, convert_train.values.ravel())
```

```python
#print("Score=%.3f" % clf.score(X, grades["Letter"]))
```

```python
#print(logisticRegr.score(ds_test, convert_test))
```

```python
predictions = logisticRegr.predict(ds_test)
```

```python
print(logisticRegr.score(ds_test, convert_test))
```

```python
#cm = confusion_matrix(predictions, convert_test)
```

```python
#print(pd.DataFrame(cm, columns=labels, index=labels))
#print(pd.DataFrame(cm)
```

```python

```

```python

```

```python
seed = 1234567890
```

```python
kfold = model_selection.KFold(n_splits=10, random_state=seed)
```

```python
model = LogisticRegression(class_weight='balanced')
```

```python
scoring = 'accuracy'
```

```python
results = model_selection.cross_val_score(model, X_train, y_train, cv=kfold, scoring=scoring)
```

```python
print("Accuracy: %.3f (%.3f)") % (results.mean(), results.std())
```

```python
print(results.mean())
```

```python
print(results.std())
```

```python

```

```python
logit = sm.Logit(y_train, X_train)
```

```python
result = logit.fit(maxiter=999)
```

```python
print result.summary()
```

```python

```

```python
#------------------------Build LightGBM Model-----------------------
train_data=lgb.Dataset(train_x,label=train_y)
valid_data=lgb.Dataset(valid_x,label=valid_y)
```

```python

```

```python

```

```python
# https://medium.com/@sunwoopark/kaggle-%EB%8F%84%EC%A0%84%EA%B8%B0-home-credit-default-risk-part-1-735030d40ee0
### SMOTE
from imblearn.over_sampling import SMOTE
X_resampled, y_resampled = SMOTE().fit_sample(X_train, y_train)

### ROC_AUC_SCORE
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score

logreg = LogisticRegression()
logreg.fit(X_train, y_train)
y_pred = logreg.predict_proba(X_test_set)[:,1]
roc_auc_score(y_test_set, y_pred)
```

```python

```

```python
# https://medium.com/@faizanahemad/participating-in-kaggle-data-science-competitions-part-1-step-by-step-guide-and-baseline-model-5b0c6973022a
```

```python
df = application_train
```

```python
# How many classes
df["TARGET"].nunique()
```

```python
# Distribution of those classes
df["TARGET"].value_counts(dropna=False)
```

```python
dtypes = df.dtypes
dtypes = dtypes[dtypes != 'object']
features = list(set(dtypes.index) - set(['TARGET']))
len(features)
```

```python
X = df[features]
y = df['TARGET']
```

```python
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
```

```python
model = XGBClassifier(max_depth=6,
                      learning_rate=0.1,
                      n_estimators=100,
                      n_jobs=16,
                      scale_pos_weight=4,
                      missing=np.nan,
                      gamma=16,
                      eval_metric='auc',
                      reg_lambda=40,reg_alpha=40
                     )
model.fit(X_train,y_train)
```

### Step 10: Scoring on Train and Test set

We will predict probabilities of our TARGET=1,
  P(TARGET=1|X) and use it for finding AUC_ROC metric.

```python
from sklearn.metrics import roc_auc_score
y_train_predicted = model.predict_proba(X_train)[:,1]
y_test_predicted = model.predict_proba(X_test)[:,1]

print('Train AUC %.4f' % roc_auc_score(y_train,y_train_predicted))
print('Test AUC %.4f' % roc_auc_score(y_test,y_test_predicted))
```

```python
def generate_results(model,df_test,features,id_col,target,file):
    dft = df_test[features]
    results = df_test[[id_col]]
    results[target] = model.predict_proba(dft)[:,1]
    results.to_csv(file,index=False,columns=results.columns
)

generate_results(model,df_test,features,"SK_ID_CURR","TARGET","results/results.csv")
```

```python

```

```python

```

[Jupyter Notebook - Typesetting Equations](http://jupyter-notebook.readthedocs.io/en/stable/examples/Notebook/Typesetting%20Equations.html)
The Markdown parser included in the Jupyter Notebook is MathJax-aware. This means that you can freely mix in mathematical expressions using the [MathJax subset of Tex and LaTeX](http://docs.mathjax.org/en/latest/tex.html#tex-support). Some examples from the MathJax site are reproduced below, as well as the Markdown+TeX source.
[MathJax basic tutorial and quick reference](https://math.meta.stackexchange.com/questions/5020/mathjax-basic-tutorial-and-quick-reference)

\begin{align}
\dot{x} & = \sigma(y-x) \\
\dot{y} & = \rho x - y - xz \\
\dot{z} & = -\beta z + xy
\end{align}

\begin{equation*}
P(E)   = {n \choose k} p^k (1-p)^{ n-k}
\end{equation*}

This expression $\sqrt{3x-1}+(1+x)^2$ is an example of a TeX inline equation in a [Markdown-formatted](https://daringfireball.net/projects/markdown/) sentence.

###### Other Syntax
You will notice in other places on the web that $$ are needed explicitly to begin and end MathJax typesetting. This is not required if you will be using TeX environments, but the Jupyter notebook will accept this syntax on legacy notebooks.

$$\begin{eqnarray}
x' &=& &x \sin\phi &+& z \cos\phi \\
z' &=& - &x \cos\phi &+& z \sin\phi \\
\end{eqnarray}$$

\begin{align}
Y = \beta_0 + \beta_1 X_1 + \beta_2 X_2 + \beta_3 X_3 + \beta_4 X_4
\end{align}
\begin{align}
R^2
\end{align}

# Multicollinearity Detection
https://statinfer.com/204-1-9-issue-of-multicollinearity-in-python/
    Y = β0 + β1X1 + β2X2 + β3X3 + β4X4
    Build a model X1 vs X2 X3 X4 find R^2, say R1
    Build a model X2 vs X1 X3 X4 find R^2, say R2
    Build a model X3 vs X1 X2 X4 find R^2, say R3
    Build a model X4 vs X1 X2 X3 find R^2, say R4
    For example if R3 is 95% then we don’t really need X3 in the model.
    Since it can be explained as liner combination of other three.
    For each variable we find individual R2.
    1/(1 − R^2) is called VIF.

```python
application_train__ext_source = application_train.loc[:, ['EXT_SOURCE_1','EXT_SOURCE_2','EXT_SOURCE_3']]
```

```python
# Remove observations with missing values
application_train__ext_source__dropna = application_train__ext_source.dropna()
```

```python
"""
There is no function for calculating VIF values.
None of the pre-built libraries have this VIF calculation function
We may have to write our own function to calculate VIF values for each variable
"""

# Code for VIF Calculation
# Writing a function to calculate the VIF values
#def variable_inflation_factor_calculations(input_data, dependent_col):
def vif_cal(input_data, dependent_col):
    x_vars = input_data.drop([dependent_col], axis=1)
    xvar_names = x_vars.columns
    for i in range(0, xvar_names.shape[0]):
        y = x_vars[xvar_names[i]]
        x = x_vars[xvar_names.drop(xvar_names[i])]
        rsq = smf.ols(formula='y ~ x', data = x_vars).fit().rsquared
        vif = round(1 / (1 - rsq), 2)
        print (xvar_names[i], 'VIF = ' , vif)
```

```python
# EXT_SOURCE_1 ~ EXT_SOURCE_2 + EXT_SOURCE_3
linreg1 = LinearRegression()
linreg1.fit(application_train__ext_source__dropna[['EXT_SOURCE_2'] + ['EXT_SOURCE_3']], application_train__ext_source__dropna[['EXT_SOURCE_1']])
linreg1_predicted = linreg1.predict(application_train__ext_source__dropna[['EXT_SOURCE_2'] + ['EXT_SOURCE_3']])
linreg1_model = smf.ols(formula='EXT_SOURCE_1 ~ EXT_SOURCE_2 + EXT_SOURCE_3', data=application_train__ext_source__dropna)
linreg1_fitted = linreg1_model.fit()
```

```python
linreg1_fitted.summary()
```

```python
linreg1_fitted.summary2()
```

```python
# Calculating VIF values using that function
vif_cal(input_data = application_train__ext_source__dropna, dependent_col = 'EXT_SOURCE_1')
```

```python
# EXT_SOURCE_2 ~ EXT_SOURCE_3 + EXT_SOURCE_1
linreg2 = LinearRegression()
linreg2.fit(application_train__ext_source__dropna[['EXT_SOURCE_3'] + ['EXT_SOURCE_1']], application_train__ext_source__dropna[['EXT_SOURCE_2']])
linreg2_predicted = linreg1.predict(application_train__ext_source__dropna[['EXT_SOURCE_3'] + ['EXT_SOURCE_1']])
linreg2_model = smf.ols(formula='EXT_SOURCE_2 ~ EXT_SOURCE_3 + EXT_SOURCE_1', data=application_train__ext_source__dropna)
linreg2_fitted = linreg1_model.fit()
```

```python
linreg2_fitted.summary()
```

```python
linreg2_fitted.summary2()
```

```python
# Calculating VIF values using that function
vif_cal(input_data = application_train__ext_source__dropna, dependent_col = 'EXT_SOURCE_2')
```

```python
# EXT_SOURCE_3 ~ EXT_SOURCE_1 + EXT_SOURCE_2
linreg3 = LinearRegression()
linreg3.fit(application_train__ext_source__dropna[['EXT_SOURCE_1'] + ['EXT_SOURCE_2']], application_train__ext_source__dropna[['EXT_SOURCE_3']])
linreg3_predicted = linreg1.predict(application_train__ext_source__dropna[['EXT_SOURCE_1'] + ['EXT_SOURCE_2']])
linreg3_model = smf.ols(formula='EXT_SOURCE_3 ~ EXT_SOURCE_1 + EXT_SOURCE_2', data=application_train__ext_source__dropna)
linreg3_fitted = linreg1_model.fit()
```

```python
linreg3_fitted.summary()
```

```python
linreg3_fitted.summary2()
```

```python
# Calculating VIF values using that function
vif_cal(input_data = application_train__ext_source__dropna, dependent_col = 'EXT_SOURCE_3')
```

```python

```

```python
# https://stackoverflow.com/questions/22470690/get-list-of-pandas-dataframe-columns-based-on-data-type
# df.columns.to_series().groupby(df.dtypes).groups
application_train.columns.to_series().groupby(application_train.dtypes).groups
```


# Neuromatch Academy: Week 3, Day 1, Tutorial 2
# Real Neurons: Effects of Input Correlation
__Content creators:__ Qinglong Gu, Songtin Li, John Murray, Richard Naud, Arvind Kumar

__Content reviewers:__ Maryam Vaziri-Pashkam, Ella Batty, Lorenzo Fontolan, Richard Gao, Matthew Krause, Spiros Chavlis, Michael Waskom

---
# Tutorial Objectives
In this tutorial, we will use the leaky integrate-and-fire (LIF) neuron model (see Tutorial 1) to study how they transform input correlations to output properties (transfer of correlations). In particular, we are going to write a few lines of code to:

- inject correlated GWN in a pair of neurons

- measure correlations between the spiking activity of the two neurons

- study how the transfer of correlation depends on the statistics of the input, i.e. mean and standard deviation.

---
# Setup

```python
# Import libraries
import matplotlib.pyplot as plt
import numpy as np
import time
```

```python
# @title Figure Settings
import ipywidgets as widgets       # interactive display
%config InlineBackend.figure_format = 'retina'
# use NMA plot style
plt.style.use("https://raw.githubusercontent.com/NeuromatchAcademy/course-content/master/nma.mplstyle")
my_layout = widgets.Layout()
```

```python
# @title Helper functions
def default_pars(**kwargs):
  pars = {}

  ### typical neuron parameters###
  pars['V_th'] = -55.     # spike threshold [mV]
  pars['V_reset'] = -75.  # reset potential [mV]
  pars['tau_m'] = 10.     # membrane time constant [ms]
  pars['g_L'] = 10.       # leak conductance [nS]
  pars['V_init'] = -75.   # initial potential [mV]
  pars['V_L'] = -75.      # leak reversal potential [mV]
  pars['tref'] = 2.       # refractory time (ms)

  ### simulation parameters ###
  pars['T'] = 400. # Total duration of simulation [ms]
  pars['dt'] = .1  # Simulation time step [ms]

  ### external parameters if any ###
  for k in kwargs:
    pars[k] = kwargs[k]

  pars['range_t'] = np.arange(0, pars['T'], pars['dt'])  # Vector of discretized
                                                         # time points [ms]
  return pars

def run_LIF(pars, Iinj):
  """
  Simulate the LIF dynamics with external input current

  Args:
    pars       : parameter dictionary
    Iinj       : input current [pA]. The injected current here can be a value or an array

  Returns:
    rec_spikes : spike times
    rec_v      : mebrane potential
  """

  # Set parameters
  V_th, V_reset = pars['V_th'], pars['V_reset']
  tau_m, g_L = pars['tau_m'], pars['g_L']
  V_init, V_L = pars['V_init'], pars['V_L']
  dt, range_t = pars['dt'], pars['range_t']
  Lt = range_t.size
  tref = pars['tref']

  # Initialize voltage and current
  v = np.zeros(Lt)
  v[0] = V_init
  Iinj = Iinj * np.ones(Lt)
  tr = 0.

  # simulate the LIF dynamics
  rec_spikes = []   # record spike times
  for it in range(Lt - 1):
    if tr > 0:
      v[it] = V_reset
      tr = tr - 1
    elif v[it] >= V_th:  # reset voltage and record spike event
      rec_spikes.append(it)
      v[it] = V_reset
      tr = tref / dt

    # calculate the increment of the membrane potential
    dv = (-(v[it] - V_L) + Iinj[it] / g_L) * (dt / tau_m)

    # update the membrane potential
    v[it + 1] = v[it] + dv

  rec_spikes = np.array(rec_spikes) * dt

  return v, rec_spikes

def my_GWN(pars, sig, myseed=False):
  """
  Function that calculates Gaussian white noise inputs

  Args:
    pars       : parameter dictionary
    mu         : noise baseline (mean)
    sig        : noise amplitute (standard deviation)
    myseed     : random seed. int or boolean
                 the same seed will give the same random number sequence

  Returns:
    I          : Gaussian white noise input
  """

  # Retrieve simulation parameters
  dt, range_t = pars['dt'], pars['range_t']
  Lt = range_t.size

  # Set random seed. You can fix the seed of the random number generator so
  # that the results are reliable however, when you want to generate multiple
  # realization make sure that you change the seed for each new realization
  if myseed:
      np.random.seed(seed=myseed)
  else:
      np.random.seed()

  # generate GWN
  # we divide here by 1000 to convert units to sec.
  I_GWN = sig * np.random.randn(Lt) * np.sqrt(pars['tau_m'] / dt)

  return I_GWN

def Poisson_generator(pars, rate, n, myseed=False):
  """
  Generates poisson trains

  Args:
    pars       : parameter dictionary
    rate       : noise amplitute [Hz]
    n          : number of Poisson trains
    myseed     : random seed. int or boolean

  Returns:
    pre_spike_train : spike train matrix, ith row represents whether
                      there is a spike in ith spike train over time
                      (1 if spike, 0 otherwise)
  """

  # Retrieve simulation parameters
  dt, range_t = pars['dt'], pars['range_t']
  Lt = range_t.size

  # set random seed
  if myseed:
      np.random.seed(seed=myseed)
  else:
      np.random.seed()

  # generate uniformly distributed random variables
  u_rand = np.random.rand(n, Lt)

  # generate Poisson train
  poisson_train = 1. * (u_rand < rate * (dt / 1000.))

  return poisson_train

def example_plot_myCC():
  pars = default_pars(T=50000, dt=.1)

  c = np.arange(10) * 0.1
  r12 = np.zeros(10)
  for i in range(10):
    I1gL, I2gL = correlate_input(pars, mu=20.0, sig=7.5, c=c[i])
    r12[i] = my_CC(I1gL, I2gL)

  plt.figure()
  plt.plot(c, r12, 'bo', alpha=0.7, label='Simulation', zorder=2)
  plt.plot([-0.05, 0.95], [-0.05, 0.95], 'k--', label='y=x',
           dashes=(2, 2), zorder=1)
  plt.xlabel('True CC')
  plt.ylabel('Sample CC')
  plt.legend(loc='best')

def LIF_output_cc(pars, mu, sig, c, bin_size, n_trials=20):
  """ Simulates two LIF neurons with correlated input and computes output correlation

  Args:
  pars       : parameter dictionary
  mu         : noise baseline (mean)
  sig        : noise amplitute (standard deviation)
  c          : correlation coefficient ~[0, 1]
  bin_size   : bin size used for time series
  n_trials   : total simulation trials

  Returns:
  r          : output corr. coe.
  sp_rate    : spike rate
  sp1        : spike times of neuron 1 in the last trial
  sp2        : spike times of neuron 2 in the last trial
  """

  r12 = np.zeros(n_trials)
  sp_rate = np.zeros(n_trials)
  for i_trial in range(n_trials):
    I1gL, I2gL = correlate_input(pars, mu, sig, c)
    _, sp1 = run_LIF(pars, pars['g_L'] * I1gL)
    _, sp2 = run_LIF(pars, pars['g_L'] * I2gL)

    my_bin = np.arange(0, pars['T'], bin_size)

    sp1_count, _ = np.histogram(sp1, bins=my_bin)
    sp2_count, _ = np.histogram(sp2, bins=my_bin)

    r12[i_trial] = my_CC(sp1_count[::20], sp2_count[::20])
    sp_rate[i_trial] = len(sp1) / pars['T'] * 1000.

  return r12.mean(), sp_rate.mean(), sp1, sp2

def plot_c_r_LIF(c, r, mycolor, mylabel):
  z = np.polyfit(c, r, deg=1)
  c_range = np.array([c.min() - 0.05, c.max() + 0.05])
  plt.plot(c, r, 'o', color=mycolor, alpha=0.7, label=mylabel, zorder=2)
  plt.plot(c_range, z[0] * c_range + z[1], color=mycolor, zorder=1)

```

The helper function contains the:

- Parameter dictionary: `default_pars( **kwargs)`
- LIF simulator: `run_LIF`
- Gaussian white noise generator: `my_GWN(pars, sig, myseed=False)`
- Poisson type spike train generator: `Poisson_generator(pars, rate, n, myseed=False)`
- Two LIF neurons with correlated inputs simulator: `LIF_output_cc(pars, mu, sig, c, bin_size, n_trials=20)`
- Some additional plotting utilities

---
# Section 1: Correlations (Synchrony)
Correlation or synchrony in neuronal activity can be described for any readout of brain activity. Here, we are concerned with the spiking activity of neurons.

In the simplest way, correlation/synchrony refers to coincident spiking of neurons, i.e., when two neurons spike together, they are firing in **synchrony** or are **correlated**. Neurons can be synchronous in their instantaneous activity, i.e., they spike together with some probability. However, it is also possible that spiking of a neuron at time $t$ is correlated with the spikes of another neuron with a delay (time-delayed synchrony).

## Origin of synchronous neuronal activity:
- Common inputs, i.e., two neurons are receiving input from the same sources. The degree of correlation of the shared inputs is proportional to their output correlation.
- Pooling from the same sources. Neurons do not share the same input neurons but are receiving inputs from neurons which themselves are correlated.
- Neurons are connected to each other (uni- or bi-directionally): This will only give rise to time-delayed synchrony. Neurons could also be connected via gap-junctions.
- Neurons have similar parameters and initial conditions.

## Implications of synchrony
When neurons spike together, they can have a stronger impact on downstream neurons. Synapses in the brain are sensitive to the temporal correlations (i.e., delay) between pre- and postsynaptic activity, and this, in turn, can lead to the formation of functional neuronal networks - the basis of unsupervised learning (we will study some of these concepts in a forthcoming tutorial).

Synchrony implies a reduction in the dimensionality of the system. In addition, correlations, in many cases, can impair the decoding of neuronal activity.

```python
# @title Video 1: Input & output correlations
from IPython.display import YouTubeVideo
video = YouTubeVideo(id="nsAYFBcAkes", width=854, height=480, fs=1)
print("Video available at https://youtube.com/watch?v=" + video.id)
video
```

    Video available at https://youtube.com/watch?v=nsAYFBcAkes

## How to study the emergence of correlations

A simple model to study the emergence of correlations is to inject common inputs to a pair of neurons and measure the output correlation as a function of the fraction of common inputs.

Here, we are going to investigate the transfer of correlations by computing the correlation coefficient of spike trains recorded from two unconnected LIF neurons, which received correlated inputs.

The input current to LIF neuron $i$ $(i=1,2)$ is:

\begin{equation}
\frac{I_i}{g_L} =\mu_i + \sigma_i (\sqrt{1-c}\xi_i + \sqrt{c}\xi_c) \quad (1)
\end{equation}

where $\mu_i$ is the temporal average of the current. The Gaussian white noise $\xi_i$ is independent for each neuron, while $\xi_c$ is common to all neurons. The variable $c$ ($0\le c\le1$) controls the fraction of common and independent inputs. $\sigma_i$ shows the variance of the total input.

So, first, we will generate correlated inputs.

```python
# @title
#@markdown Execute this cell to get a function for generating correlated GWN inputs
def correlate_input(pars, mu=20., sig=7.5, c=0.3):
  """
  Args:
    pars       : parameter dictionary
    mu         : noise baseline (mean)
    sig        : noise amplitute (standard deviation)
    c.         : correlation coefficient ~[0, 1]

  Returns:
    I1gL, I2gL : two correlated inputs with corr. coe. c
  """

  # generate Gaussian whute noise xi_1, xi_2, xi_c
  xi_1 = my_GWN(pars, sig)
  xi_2 = my_GWN(pars, sig)
  xi_c = my_GWN(pars, sig)

  # Generate two correlated inputs by Equation. (1)
  I1gL = mu + np.sqrt(1. - c) * xi_1 + np.sqrt(c) * xi_c
  I2gL = mu + np.sqrt(1. - c) * xi_2 + np.sqrt(c) * xi_c

  return I1gL, I2gL

print(help(correlate_input))
```

    Help on function correlate_input in module __main__:

    correlate_input(pars, mu=20.0, sig=7.5, c=0.3)
        Args:
          pars       : parameter dictionary
          mu         : noise baseline (mean)
          sig        : noise amplitute (standard deviation)
          c.         : correlation coefficient ~[0, 1]

        Returns:
          I1gL, I2gL : two correlated inputs with corr. coe. c

    None

### Exercise 1: Compute the correlation

The _sample correlation coefficient_ between two input currents $I_i$ and $I_j$ is defined as the sample covariance of $I_i$ and $I_j$ divided by the square root of the sample variance of $I_i$ multiplied with the square root of the sample variance of $I_j$. In equation form:

\begin{align}
r_{ij} &= \frac{cov(I_i, I_j)}{\sqrt{var(I_i)} \sqrt{var(I_j)}}\\
cov(I_i, I_j) &= \sum_{k=1}^L (I_i^k -\bar{I}_i)(I_j^k -\bar{I}_j) \\
var(I_i) &= \sum_{k=1}^L (I_i^k -\bar{I}_i)^2
\end{align}

where $\bar{I}_i$ is the sample mean, k is the time bin, and L is the length of $I$.  This means that $I_i^k$ is current i at time $k\cdot dt$. Note that the equations above are not accurate for sample covariances and variances as they should be additionally divided by L-1 - we have dropped this term because it cancels out in the sample correlation coefficient formula.

The _sample correlation coefficient_ may also be referred to as the _sample Pearson correlation coefficient_. Here, is a beautiful paper that explains multiple ways to calculate and understand correlations [Rodgers and Nicewander 1988](https://www.stat.berkeley.edu/~rabbee/correlation.pdf).

In this exercise, we will create a function, `my_CC` to compute the sample correlation coefficient between two time series. Note that while we introduced this computation here in the context of input currents, the sample correlation coefficient is used to compute the correlation between any two time series - we will use it later on binned spike trains.

```python
def my_CC(i, j):
  """
  Args:
    i, j  : two time series with the same length

  Returns:
    rij   : correlation coefficient
  """
  ########################################################################
  ## TODO for students: compute rxy, then remove the NotImplementedError #
  # Tip1: array([a1, a2, a3])*array([b1, b2, b3]) = array([a1*b1, a2*b2, a3*b3])
  # Tip2: np.sum(array([a1, a2, a3])) = a1+a2+a3
  # Tip3: square root, np.sqrt()
  # Fill out function and remove
  # raise NotImplementedError("Student exercise: compute the sample correlation coefficient")
  ########################################################################
  # Calculate the covariance of i and j
  cov = np.sum((i - np.mean(i)) * (j - np.mean(j)))
  # Calculate the variance of i
  var_i = np.sum((i - np.mean(i))**2)
  # Calculate the variance of j
  var_j = np.sum((j - np.mean(j))**2)
  # Calculate the correlation coefficient
  rij = cov / (np.sqrt(var_i * var_j))

  return rij

# Uncomment the line after completing the my_CC function
example_plot_myCC()
```

```python
# to_remove solution
def my_CC(i, j):
  """
  Args:
    i, j  : two time series with the same length

  Returns:
    rij   : correlation coefficient
  """

  # Calculate the covariance of i and j
  cov = ((i - i.mean()) * (j - j.mean())).sum()
  # Calculate the variance of i
  var_i = ((i - i.mean()) * (i - i.mean())).sum()
  # Calculate the variance of j
  var_j = ((j - j.mean()) * (j - j.mean())).sum()
  # Calculate the correlation coefficient
  rij = cov / np.sqrt(var_i*var_j)

  return rij

with plt.xkcd():
  example_plot_myCC()
```

### Exercise 2: Measure the correlation between spike trains

After recording the spike times of the two neurons, how can we estimate their correlation coefficient?

In order to find this, we need to bin the spike times and obtain two time series. Each data point in the time series is the number of spikes in the corresponding time bin. You can use `np.histogram()` to bin the spike times.

Complete the code below to bin the spike times and calculate the correlation coefficient for two Poisson spike trains. Note that `c` here is the ground-truth correlation coefficient that we define.

```python
# @title

# @markdown Execute this cell to get a function for generating correlated Poisson inputs (generate_corr_Poisson)

def generate_corr_Poisson(pars, poi_rate, c, myseed=False):
  """
  function to generate correlated Poisson type spike trains
  Args:
    pars       : parameter dictionary
    poi_rate   : rate of the Poisson train
    c.         : correlation coefficient ~[0, 1]

  Returns:
    sp1, sp2   : two correlated spike time trains with corr. coe. c
  """

  range_t = pars['range_t']

  mother_rate = poi_rate / c
  mother_spike_train = Poisson_generator(pars, rate=mother_rate,
                                         n=1, myseed=myseed)[0]
  sp_mother = range_t[mother_spike_train > 0]

  L_sp_mother = len(sp_mother)
  sp_mother_id = np.arange(L_sp_mother)
  L_sp_corr = int(L_sp_mother * c)

  np.random.shuffle(sp_mother_id)
  sp1 = np.sort(sp_mother[sp_mother_id[:L_sp_corr]])

  np.random.shuffle(sp_mother_id)
  sp2 = np.sort(sp_mother[sp_mother_id[:L_sp_corr]])

  return sp1, sp2

print(help(generate_corr_Poisson))
```

    Help on function generate_corr_Poisson in module __main__:

    generate_corr_Poisson(pars, poi_rate, c, myseed=False)
        function to generate correlated Poisson type spike trains
        Args:
          pars       : parameter dictionary
          poi_rate   : rate of the Poisson train
          c.         : correlation coefficient ~[0, 1]

        Returns:
          sp1, sp2   : two correlated spike time trains with corr. coe. c

    None

```python
def corr_coeff_pairs(pars, rate, c, trials, bins):
  """
  Calculate the correlation coefficient of two spike trains, for different
  realizations

  Args:
      pars   : parameter dictionary
      rate   : rate of poisson inputs
      c      : correlation coefficient ~ [0, 1]
      trials  : number of realizations
      bins   : vector with bins for time discretization

  Returns:
    r12      : correlation coefficient of a pair of inputs
  """

  r12 = np.zeros(n_trials)

  for i in range(n_trials):
    ##############################################################
    ## TODO for students: Use np.histogram to bin the spike time #
    ##   e.g., sp1_count, _= np.histogram(...)
    # Use my_CC() compute corr coe, compare with c
    # Note that you can run multiple realizations and compute their r_12(diff_trials)
    # with the defined function above. The average r_12 over trials can get close to c.
    # Note: change seed to generate different input per trial
    # Fill out function and remove
    # raise NotImplementedError("Student exercise: compute the correlation coefficient")
    ##############################################################

    # Generate correlated Poisson inputs
    sp1, sp2 = generate_corr_Poisson(pars, rate, c, myseed=2020+i)

    # Bin the spike times of the first input
    sp1_count, _ = np.histogram(sp1, bins=bins)

    # Bin the spike times of the second input
    sp2_count, _ = np.histogram(sp2, bins=bins)

    # Calculate the correlation coefficient
    r12[i] = my_CC(sp1_count, sp2_count)

  return r12

poi_rate = 20.
c = 0.2  # set true correlation
pars = default_pars(T=10000)

# bin the spike time
bin_size = 20  # [ms]
my_bin = np.arange(0, pars['T'], bin_size)
n_trials = 100  # 100 realizations

# Uncomment to test your function
r12 = corr_coeff_pairs(pars, rate=poi_rate, c=c, trials=n_trials, bins=my_bin)
print(f'True corr coe = {c:.3f}')
print(f'Simu corr coe = {r12.mean():.3f}')
```

    True corr coe = 0.200
    Simu corr coe = 0.197

Sample output

```
True corr coe = 0.200
Simu corr coe = 0.197
```

```python
# to_remove solution
def corr_coeff_pairs(pars, rate, c, trials, bins):
  """
  Calculate the correlation coefficient of two spike trains, for different
  realizations

  Args:
      pars   : parameter dictionary
      rate   : rate of poisson inputs
      c      : correlation coefficient ~ [0, 1]
      trials  : number of realizations
      bins   : vector with bins for time discretization

  Returns:
    r12      : correlation coefficient of a pair of inputs
  """

  r12 = np.zeros(n_trials)

  for i in range(n_trials):
    # Generate correlated Poisson inputs
    sp1, sp2 = generate_corr_Poisson(pars, poi_rate, c, myseed=2020+i)

    # Bin the spike times of the first input
    sp1_count, _ = np.histogram(sp1, bins=bins)

    # Bin the spike times of the second input
    sp2_count, _ = np.histogram(sp2, bins=bins)

    # Calculate the correlation coefficient
    r12[i] = my_CC(sp1_count, sp2_count)

  return r12

poi_rate = 20.
c = 0.2  # set true correlation
pars = default_pars(T=10000)
# bin the spike time
bin_size = 20  # [ms]
my_bin = np.arange(0, pars['T'], bin_size)
n_trials = 100  # 100 realizations

r12 = corr_coeff_pairs(pars, rate=poi_rate, c=c, trials=n_trials, bins=my_bin)
print(f'True corr coe = {c:.3f}')
print(f'Simu corr coe = {r12.mean():.3f}')
```

    True corr coe = 0.200
    Simu corr coe = 0.197

---
# Section 2: Investigate the effect of input correlation on the output correlation

Now let's combine the aforementioned two procedures. We first generate the correlated inputs by Equation (1). Then we inject the correlated inputs $I_1, I_2$ into a pair of neurons and record their output spike times. We continue measuring the correlation between the output and
investigate the relationship between the input correlation and the output correlation.

## Drive a neuron with correlated inputs and visualize its output
In the following, you will inject correlated GWN in two neurons. You need to define the mean (`gwn_mean`), standard deviation (`gwn_std`), and input correlations (`c_in`).

We will simulate $10$ trials to get a better estimate of the output correlation. Change the values in the following cell for the above variables (and then run the next cell) to explore how they impact the output correlation.

```python
# Play around with these parameters

pars = default_pars(T=80000, dt=1.)  # get the parameters
c_in = 0.1  # set input correlation value
gwn_mean = 10.
gwn_std = 10.

```

```python
# @title

# @markdown Do not forget to execute this cell to simulate the LIF

bin_size = 10.  # ms

starttime = time.perf_counter()  # time clock

r12_ss, sp_ss, sp1, sp2 = LIF_output_cc(pars, mu=gwn_mean, sig=gwn_std, c=c_in,
                                        bin_size=bin_size, n_trials=10)

# just the time counter
endtime = time.perf_counter()
timecost = (endtime - starttime) / 60.
print(f"Simulation time = {timecost:.2f} min")

print(f"Input correlation = {c_in}")
print(f"Output correlation = {r12_ss}")

plt.figure(figsize=(6, 3))
plt.plot(sp1, np.ones(len(sp1)) * 1, '|', ms=20, label='neuron 1')
plt.plot(sp2, np.ones(len(sp2)) * 1.1, '|', ms=20, label='neuron 2')
plt.xlabel('time (ms)')
plt.ylabel('neuron id.')
plt.xlim(1000, 8000)
plt.ylim(0.9, 1.2)
plt.legend()
plt.show()
```

## Think!
- Is the output correlation always smaller than the input correlation? If yes, why?
- Should there be a systematic relationship between input and output correlations?

You will explore these questions in the next figure but try to develop your own intuitions first!

Lets vary `c_in` and plot the relationship between the `c_in` and output correlation. This might take some time depending on the number of trials.

```python
#@title

#@markdown Don't forget to execute this cell!

pars = default_pars(T=80000, dt=1.)  # get the parameters
bin_size = 10.
c_in = np.arange(0, 1.0, 0.1)  # set the range for input CC
r12_ss = np.zeros(len(c_in))  # small mu, small sigma

starttime = time.perf_counter() # time clock
for ic in range(len(c_in)):
  r12_ss[ic], sp_ss, sp1, sp2 = LIF_output_cc(pars, mu=10.0, sig=10.,
                                              c=c_in[ic], bin_size=bin_size,
                                              n_trials=10)

endtime = time.perf_counter()
timecost = (endtime - starttime) / 60.
print(f"Simulation time = {timecost:.2f} min")

plt.figure(figsize=(5, 4))
plot_c_r_LIF(c_in, r12_ss, mycolor='b', mylabel='Output CC')
plt.plot([c_in.min() - 0.05, c_in.max() + 0.05],
         [c_in.min() - 0.05, c_in.max() + 0.05],
         'k--', dashes=(2, 2), label='y=x')

plt.xlabel('Input CC')
plt.ylabel('Output CC')
plt.legend(loc='best', fontsize=16)
plt.show()

```

```python
# to_remove explanation

"""
Discussion: The results above show that
- output correlation is smaller than input correlation
- output correlation varies linearly as a function of input correlation.

While the general result holds, this relationship might change depending on the neuron type.
""";
```

---
# Section 3: Correlation transfer function
The above plot of input correlation vs. output correlation is called the __correlation transfer function__ of the neurons.

## Section 3.1: How do the mean and standard deviation of the GWN affect the correlation transfer function?

The correlations transfer function appears to be linear. The above can be taken as the input/output transfer function of LIF neurons for correlations, instead of the transfer function for input/output firing rates as we had discussed in the previous tutorial (i.e., F-I curve).

What would you expect to happen to the slope of the correlation transfer function if you vary the mean and/or the standard deviation of the GWN?

```python
#@markdown Execute this cell to visualize correlation transfer functions

pars = default_pars(T=80000, dt=1.) # get the parameters
no_trial = 10
bin_size = 10.
c_in = np.arange(0., 1., 0.2)  # set the range for input CC
r12_ss = np.zeros(len(c_in))   # small mu, small sigma
r12_ls = np.zeros(len(c_in))   # large mu, small sigma
r12_sl = np.zeros(len(c_in))   # small mu, large sigma

starttime = time.perf_counter()  # time clock
for ic in range(len(c_in)):
  r12_ss[ic], sp_ss, sp1, sp2 = LIF_output_cc(pars, mu=10.0, sig=10.,
                                              c=c_in[ic], bin_size=bin_size,
                                              n_trials=no_trial)
  r12_ls[ic], sp_ls, sp1, sp2 = LIF_output_cc(pars, mu=18.0, sig=10.,
                                              c=c_in[ic], bin_size=bin_size,
                                              n_trials=no_trial)
  r12_sl[ic], sp_sl, sp1, sp2 = LIF_output_cc(pars, mu=10.0, sig=20.,
                                              c=c_in[ic], bin_size=bin_size,
                                              n_trials=no_trial)
endtime = time.perf_counter()
timecost = (endtime - starttime) / 60.
print(f"Simulation time = {timecost:.2f} min")

plt.figure(figsize=(5, 4))
plot_c_r_LIF(c_in, r12_ss, mycolor='b', mylabel=r'Small $\mu$, small $\sigma$')
plot_c_r_LIF(c_in, r12_ls, mycolor='y', mylabel=r'Large $\mu$, small $\sigma$')
plot_c_r_LIF(c_in, r12_sl, mycolor='r', mylabel=r'Small $\mu$, large $\sigma$')
plt.plot([c_in.min() - 0.05, c_in.max() + 0.05],
         [c_in.min() - 0.05, c_in.max() + 0.05],
         'k--', dashes=(2, 2), label='y=x')
plt.xlabel('Input CC')
plt.ylabel('Output CC')
plt.legend(loc='best', fontsize=14)
plt.show()
```

### Think!
Why do both the mean and the standard deviation of the GWN affect the slope of the correlation transfer function?

```python
# to_remove explanation

"""
Discussion: This has got to do with which part of the input current distribution
is transferred to the spiking activity.

Intuitive understanding is difficult but this relationship arises due to non-linearities
in the neuron F-I curve.  When F-I curve is linear, output correlation is independent
of the mean and standard deviation. But this relationship arises even in neurons with
threshold-linear F-I curve.

Please see:
De La Rocha J, Doiron B, Shea-Brown E, Josić K, Reyes A. Correlation between
neural spike trains increases with firing rate. Nature. 2007 Aug;448(7155):802-6.
""";
```

## Section 3.2: What is the rationale behind varying $\mu$ and $\sigma$?
The mean and the variance of the synaptic current depends on the spike rate of a Poisson process. We can use [Campbell's theorem](https://en.wikipedia.org/wiki/Campbell%27s_theorem_(probability)) to estimate the mean and the variance of the synaptic current:

\begin{align}
\mu_{\rm syn} = \lambda J \int P(t) \\
\sigma_{\rm syn} = \lambda J \int P(t)^2 dt\\
\end{align}

where $\lambda$ is the firing rate of the Poisson input, $J$ the amplitude of the postsynaptic current and $P(t)$ is the shape of the postsynaptic current as a function of time.

Therefore, when we varied $\mu$ and/or $\sigma$ of the GWN, we mimicked a change in the input firing rate. Note that, if we change the firing rate, both $\mu$ and $\sigma$ will change simultaneously, not independently.

Here, since we observe an effect of $\mu$ and $\sigma$ on correlation transfer, this implies that the input rate has an impact on the correlation transfer function.

### Think!

- What are the factors that would make output correlations smaller than input correlations? (Notice that the colored lines are below the black dashed line)
- What does it mean for the correlation in the network?
- Here we have studied the transfer of correlations by injecting GWN. But in the previous tutorial, we mentioned that GWN is unphysiological. Indeed, neurons receive colored noise (i.e., Shot noise or OU process). How do these results obtained from injection of GWN apply to the case where correlated spiking inputs are injected in the two LIFs? Will the results be the same or different?

Reference
- De La Rocha, Jaime, et al. "Correlation between neural spike trains increases with firing rate." Nature (2007) (https://www.nature.com/articles/nature06028/)

- Bujan AF, Aertsen A, Kumar A. Role of input correlations in shaping the variability and noise correlations of evoked activity in the neocortex. Journal of Neuroscience. 2015 Jun 3;35(22):8611-25. (https://www.jneurosci.org/content/35/22/8611)

```python
# to_remove explanation

"""
Discussion:

1. Anything that tries to reduce the mean or variance of the input e.g. mean can
be reduced by inhibition, sigma can be reduced by the membrane time constant.
Obviously, if the two neurons have different parameters that will decorrelate them.
But more importantly, it is the slope of neuron transfer function that will affect the
output correlation.

2. These observations pose an interesting problem at the network level. If the
output correlation are smaller than the input correlation, then the network activity
should eventually converge to zero correlation. But that does not happen. So there
is something missing in this model to understand origin of synchrony in the network.

3. For spike trains, if we do not have explicit control over mu and sigma.
And these two variables will be tied to the firing rate of the inputs. So the
results will be qualitatively similar. But when we think of multiple spike inputs
two different types of correlations arise (see Bujan et al. 2015 for more info)
""";
```

---
# Summary

In this tutorial, we studied how the input correlation of two LIF neurons is mapped to their output correlation. Specifically, we:

- injected correlated GWN in a pair of neurons,

- measured correlations between the spiking activity of the two neurons, and

- studied how the transfer of correlation depends on the statistics of the input, i.e., mean and standard deviation.

Here, we were concerned with zero time lag correlation. For this reason, we restricted estimation of correlation to instantaneous correlations. If you are interested in time-lagged correlation, then we should estimate the cross-correlogram of the spike trains and find out the dominant peak and area under the peak to get an estimate of output correlations.

We leave this as a future to-do for you if you are interested.

---
# Bonus 1: Example of a conductance-based LIF model
Above, we have written code to generate correlated Poisson spike trains. You can write code to stimulate the LIF neuron with such correlated spike trains and study the correlation transfer function for spiking input and compare it to the correlation transfer function obtained by injecting correlated GWNs.

```python
# @title Function to simulate conductance-based LIF

def run_LIF_cond(pars, I_inj, pre_spike_train_ex, pre_spike_train_in):
  """
  conductance-based LIF dynamics

  Args:
    pars               : parameter dictionary
    I_inj              : injected current [pA]. The injected current here can
                         be a value or an array
    pre_spike_train_ex : spike train input from presynaptic excitatory neuron
    pre_spike_train_in : spike train input from presynaptic inhibitory neuron

  Returns:
    rec_spikes         : spike times
    rec_v              : mebrane potential
    gE                 : postsynaptic excitatory conductance
    gI                 : postsynaptic inhibitory conductance
  """

  # Retrieve parameters
  V_th, V_reset = pars['V_th'], pars['V_reset']
  tau_m, g_L = pars['tau_m'], pars['g_L']
  V_init, E_L = pars['V_init'], pars['E_L']
  gE_bar, gI_bar = pars['gE_bar'], pars['gI_bar']
  VE, VI = pars['VE'], pars['VI']
  tau_syn_E, tau_syn_I = pars['tau_syn_E'], pars['tau_syn_I']
  tref = pars['tref']
  dt, range_t = pars['dt'], pars['range_t']
  Lt = range_t.size

  # Initialize
  tr = 0.
  v = np.zeros(Lt)
  v[0] = V_init
  gE = np.zeros(Lt)
  gI = np.zeros(Lt)
  Iinj = I_inj * np.ones(Lt)  # ensure I has length Lt

  if pre_spike_train_ex.max() == 0:
    pre_spike_train_ex_total = np.zeros(Lt)
  else:
    pre_spike_train_ex_total = pre_spike_train_ex * np.ones(Lt)

  if pre_spike_train_in.max() == 0:
    pre_spike_train_in_total = np.zeros(Lt)
  else:
    pre_spike_train_in_total = pre_spike_train_in * np.ones(Lt)

  # simulation
  rec_spikes = []  # recording spike times
  for it in range(Lt - 1):
    if tr > 0:
      v[it] = V_reset
      tr = tr - 1
    elif v[it] >= V_th:   # reset voltage and record spike event
      rec_spikes.append(it)
      v[it] = V_reset
      tr = tref / dt
    # update the synaptic conductance
    gE[it+1] = gE[it] - (dt / tau_syn_E) * gE[it] + gE_bar * pre_spike_train_ex_total[it + 1]
    gI[it+1] = gI[it] - (dt / tau_syn_I) * gI[it] + gI_bar * pre_spike_train_in_total[it + 1]

    # calculate the increment of the membrane potential
    dv = (-(v[it] - E_L) - (gE[it + 1] / g_L) * (v[it] - VE) - \
            (gI[it + 1] / g_L) * (v[it] - VI) + Iinj[it] / g_L) * (dt / tau_m)

    # update membrane potential
    v[it + 1] = v[it] + dv

  rec_spikes = np.array(rec_spikes) * dt

  return v, rec_spikes, gE, gI

print(help(run_LIF_cond))
```

    Help on function run_LIF_cond in module __main__:

    run_LIF_cond(pars, I_inj, pre_spike_train_ex, pre_spike_train_in)
        conductance-based LIF dynamics

        Args:
          pars               : parameter dictionary
          I_inj              : injected current [pA]. The injected current here can
                               be a value or an array
          pre_spike_train_ex : spike train input from presynaptic excitatory neuron
          pre_spike_train_in : spike train input from presynaptic inhibitory neuron

        Returns:
          rec_spikes         : spike times
          rec_v              : mebrane potential
          gE                 : postsynaptic excitatory conductance
          gI                 : postsynaptic inhibitory conductance

    None

## Interactive Demo: Correlated spike input to an LIF neuron

In the following you can explore what happens when the neurons receive correlated spiking input.

You can vary the correlation between excitatory input spike trains. For simplicity, the correlation between inhibitory spike trains is set to 0.01.

Vary both excitatory rate and correlation and see how the output correlation changes. Check if the results are qualitatively similar to what you observed previously when you varied the $\mu$ and $\sigma$.

```python
# @title

# @markdown Make sure you execute this cell to enable the widget!

my_layout.width = '450px'
@widgets.interact(
  pwc_ee=widgets.FloatSlider(0.3, min=0.05, max=0.99, step=0.01,
                             layout=my_layout),
  exc_rate=widgets.FloatSlider(1e3, min=500., max=5e3, step=50.,
                               layout=my_layout),
  inh_rate=widgets.FloatSlider(500., min=300., max=5e3, step=5.,
                               layout=my_layout),
)

def EI_isi_regularity(pwc_ee, exc_rate, inh_rate):
  pars = default_pars(T=1000.)
  # Add parameters
  pars['V_th'] = -55.     # spike threshold [mV]
  pars['V_reset'] = -75.  # reset potential [mV]
  pars['tau_m'] = 10.     # membrane time constant [ms]
  pars['g_L'] = 10.       # leak conductance [nS]
  pars['V_init'] = -65.   # initial potential [mV]
  pars['E_L'] = -75.      # leak reversal potential [mV]
  pars['tref'] = 2.       # refractory time (ms)

  pars['gE_bar'] = 4.0    # [nS]
  pars['VE'] = 0.         # [mV] excitatory reversal potential
  pars['tau_syn_E'] = 2.  # [ms]
  pars['gI_bar'] = 2.4    # [nS]
  pars['VI'] = -80.       # [mV] inhibitory reversal potential
  pars['tau_syn_I'] = 5.  # [ms]

  my_bin = np.arange(0, pars['T']+pars['dt'], .1)  # 20 [ms] bin-size

  # exc_rate = 1e3
  # inh_rate = 0.4e3
  # pwc_ee = 0.3
  pwc_ii = 0.01
  # generate two correlated spike trains for excitatory input
  sp1e, sp2e = generate_corr_Poisson(pars, exc_rate, pwc_ee)

  sp1_spike_train_ex, _ = np.histogram(sp1e, bins=my_bin)
  sp2_spike_train_ex, _ = np.histogram(sp2e, bins=my_bin)

  # generate two uncorrelated spike trains for inhibitory input
  sp1i, sp2i = generate_corr_Poisson(pars, inh_rate, pwc_ii)
  sp1_spike_train_in, _ = np.histogram(sp1i, bins=my_bin)
  sp2_spike_train_in, _ = np.histogram(sp2i, bins=my_bin)

  v1, rec_spikes1, gE, gI = run_LIF_cond(pars, 0, sp1_spike_train_ex, sp1_spike_train_in)
  v2, rec_spikes2, gE, gI = run_LIF_cond(pars, 0, sp2_spike_train_ex, sp2_spike_train_in)

  # bin the spike time
  bin_size = 20  # [ms]
  my_bin = np.arange(0, pars['T'], bin_size)
  spk_1, _ = np.histogram(rec_spikes1, bins=my_bin)
  spk_2, _ = np.histogram(rec_spikes2, bins=my_bin)

  r12 = my_CC(spk_1, spk_2)
  print(f"Input correlation = {pwc_ee}")
  print(f"Output correlation = {r12}")

  plt.figure(figsize=(14, 7))
  plt.subplot(211)
  plt.plot(sp1e, np.ones(len(sp1e)) * 1, '|', ms=20,
           label='Exc. input 1')
  plt.plot(sp2e, np.ones(len(sp2e)) * 1.1, '|', ms=20,
           label='Exc. input 2')
  plt.plot(sp1i, np.ones(len(sp1i)) * 1.3, '|k', ms=20,
           label='Inh. input 1')
  plt.plot(sp2i, np.ones(len(sp2i)) * 1.4, '|k', ms=20,
           label='Inh. input 2')

  plt.ylim(0.9, 1.5)
  plt.legend()
  plt.ylabel('neuron id.')

  plt.subplot(212)
  plt.plot(pars['range_t'], v1, label='neuron 1')
  plt.plot(pars['range_t'], v2, label='neuron 2')
  plt.xlabel('time (ms)')
  plt.ylabel('membrane voltage $V_{m}$')
  plt.tight_layout()
  plt.show()
```

    interactive(children=(FloatSlider(value=0.3, description='pwc_ee', layout=Layout(width='450px'), max=0.99, min…

Above, we are estimating the output correlation for one trial. You can modify the code to get a trial average of output correlations.

---
# Bonus 2: Ensemble Response

Finally, there is a short BONUS lecture video on the firing response of an ensemble of neurons to time-varying input. There are no associated coding exercises - just enjoy.

```python
#@title Video 2 (Bonus): Response of ensemble of neurons to time-varying input
from IPython.display import YouTubeVideo
video = YouTubeVideo(id="78_dWa4VOIo", width=854, height=480, fs=1)
print("Video available at https://youtube.com/watch?v=" + video.id)
video
```

    Video available at https://youtube.com/watch?v=78_dWa4VOIo


```python
%%capture
# Load the quadrotor simulator from a previous notebook
%run quadsim.ipynb
```

```python
from IPython.core.display import HTML
HTML('<style>.impl-inner{background-color:#e4ffe6} .impl-outer{background-color:#e4ebff} .text_cell_render p { text-align: justify; text-justify: inter-word;}</style>')
```

<style>.impl-inner{background-color:#e4ffe6} .impl-outer{background-color:#e4ebff} .text_cell_render p { text-align: justify; text-justify: inter-word;}</style>

# Image-Based Visual Servoing of a Quadrotor

Our goal is to use visual information to steer the quadrotor into a desired configuration. The implementation found in this notebook is based largely off of the work by Zheng et al. \[1].

## II. Quadrotor Model

The dynamical quadrotor model used throughout this notebook is the essentially the same rigid-body model found in the [quadrotor_model](quadrotor_mode.ipynb) notebook. Specifically, this model expressed velocity $V$ in the body frame. The equations of motion (EOM) are rewritten here for convenience
$$
\begin{align}
\dot{r} &= RV \\
\dot{R} &= R\omega^\wedge \\
m\dot{V} &= -m\omega\times V + F \\
J\dot{\omega} &= -\omega\times J\omega + \tau \\
F &= -u_1 E_3 + mgR^\top e_3,
\end{align}
$$
where $r,V\in\mathbb{R}^3$ are the inertial position and body velocity of the vehicle, $R\in\text{SO}(3)$ is the attitude of the body w.r.t the inertial frame, $\omega\in\mathbb{R}^3$ is the angular velocity of the body w.r.t the inertial frame, expressed in the body frame. The mass of the vehicle is given by $m\in\mathbb{R}$, the inertia matrix by $J\in\mathbb{R}^{3\times 3}$, and the acceleration due to gravity by $g\in\mathbb{R}$. The vectors $E_3 = e_3 = (0\ 0\ 1)^\top$ are expressed in the body frame and the inertial frame, respectively. Finally, $\cdot^\wedge$ is the *hat-map*, defined by
$$
\begin{align}
\cdot^\wedge : \mathbb{R}^3 &\to \mathfrak{so}(3) \\
\omega^\wedge &\mapsto \begin{bmatrix} 0&-r&q\\r&0&-p\\-q&p&0 \end{bmatrix}.
\end{align}
$$

## IIII. Image Features and Their Dynamics

### Coordinate Frames

In order to design a controller for the quadrotor, we would like to express image features in the *virtual camera* frame. This frame is also known as the local-level or vehicle-1 frame. Therefore, we require the rotation $R_c^b$ from the camera frame, $\mathcal{F}_c$ to the body frame $\mathcal{F}_b$. Note that in the case where there is no gimbal, this rotation matrix is known from the mechanical design of the quadrotor-camera system and does not change. In this notebook, our camera is mounted at a 45-degree angle below the horizontal. Accounting for the coordinate frame of a camera (see [sensors](sensors.ipynb)), the desired rotation matrix is
$$
\begin{align}
R_c^b := \left[R_x(\pi/4) R_y(0) R_z(\pi/2)\right]^\top.
\end{align}
$$

The rotation from the body frame $\mathcal{F}_b$ to the vehicle-1 frame $\mathcal{F}_{v1}$ is given by
$$
\begin{align}
R_b^{v1}(\phi,\theta) &= R_{v2}^{v1}(\theta) R_b^{v2}(\phi) \\
&=
\begin{bmatrix}
\cos\theta & 0 & -\sin\theta \\
0 & 1 & 0 \\
\sin\theta & 0 & \cos\theta
\end{bmatrix}
\begin{bmatrix}
1 & 0 & 0 \\
0 & \cos\phi & \sin\phi \\
0 & -\sin\phi & \cos\phi
\end{bmatrix}.
\end{align}
$$

The relevant Python code is given below.

```python
Rb2c = rot3d(45,0,90) # camera is mounted at 45-degree down

def Rot_v1_to_b(phi, theta, *args): return Rot_v2_to_b(phi).dot(Rot_v1_to_v2(theta))
def Rot_b_to_v1(phi, theta, *args): return Rot_v1_to_b(phi, theta).T
```

### Camera Measurement Model

Recall that a camera is essentially a bearing-only sensor. Given the pixel coordinates $(u, v)^\top$ of a 3D object projected onto the plane, the [camera geometry](sensors.ipynb#Camera) can be used to find a unit line-of-sight vector.

The camera matrix $K$ encodes the intrinsic parameters of a camera/lens system and can be found via camera calibration. It is used to project points on the normalized image plane onto the pixel plane. It has the form
$$
K = \begin{bmatrix} f & 0 & c_x \\ 0 & f & c_y \\ 0 & 0 & 1 \end{bmatrix}
$$
where $f$ is the focal length in pixels, and $c_x, c_y$ are the pixel offsets to the principal point (i.e., the center of the image).

Unit line-of-sight (LOS) vectors can be constructed from points on the normalized image plane via $\hat\ell = \frac{p^{im}}{\left|p^{im}\right|}$. To convert pixel coordinates to normalized image coordinates, simply pre-multiply by the inverse of the camera matrix $K$.

The following Python code converts pixels to unit LOS vectors in the vehicle-1 frame.

```python
def target_los(x_cam, state, camera):
    # rotation from body to camera
    Rb2c = camera.R

    # make homogeneous
    hx_cam = np.vstack((x_cam,1))

    # project to normalized image plane, with unit focal length
    hx_cam_nip = np.linalg.inv(camera.K).dot(hx_cam)

    # rotate to vehicle-1 frame
    hx_veh1 = Rot_b_to_v1(*state[6:9,0]).dot(Rb2c.T.dot( hx_cam_nip ))

    # normalize
    hx_veh1 = hx_veh1 / np.linalg.norm(hx_veh1[:3,:])

    return hx_veh1
```

## IV. IBVS Controller

Here we design an image-based visual servoing controller based on the nonlinear design tools backstepping and Lyapunov analysis.

```python
class IBVS(Controller):
    """Image-based Visual Servoing Controller
    """
    def __init__(self, camera):
        self.name = "IBVS"

        # camera object
        self.camera = camera

        # estimates of the physical properties of the quadrotor
        self.g = 9.8
        self.mass = 4
        self.I = np.diag(np.array([0.1, 0.1, 0.1]))

        self.g = 9.81
        self.mass = 3.81
        self.I = np.diag(np.array([0.060224, 0.122198, 0.132166]))

        # Attitude (inner loop) PID controllers
        self.pid_roll = SimplePID(7.63, None, 0.95, -np.pi/6, np.pi/6)
        self.pid_pitch = SimplePID(7.63, None, 1.55, -np.pi/6, np.pi/6)
        self.pid_yaw_rate = SimplePID(2.86, None, None, -np.pi, np.pi)

    def innerPID(self, cmds, state, Ts):

        # flatten the inputs for easy access
        s = state.flatten()

        U1 = cmds[0]
        phid = cmds[1]
        thetad = cmds[2]

        # ?
        psidotd = 0

        # Force (f) and moments (tau_*)
        tph = self.pid_roll.run(phid-s[6], Ts)
        tth = self.pid_pitch.run(thetad-s[7], Ts)
        tps = self.pid_yaw_rate.run(psidotd-s[11], Ts)

        return np.array([U1, tph, tth, tps])

    def outer(self, x_cam, state, Ts):
        # calculate unit LOS vectors to targets
        hx_veh1 = target_los(x_cam, state, self.camera)
#         print(hx_veh1.T)

        # calculate the depth value when the camera is at the desired pose
        h = -state[2,0]
        zdes = h/np.cos(np.pi/4)

        # LOS vector is the feature q
        q = hx_veh1

        # image error
        q1 = q - np.array([[0.707,0,0.707]]).T

#         print("q1: ", q1.T)

        # other error
        c1 = 10
        q2 = q1 - 1.0/c1 * state[3:6]

        k3 = 1
        f = k3*q2

        f = f.flatten()

        # convert
        phi = -f[1] / (f[2] - self.g)
        theta = f[0] / (f[2] - self.g)
        U1 = -self.mass*(f[2] - self.g)

        return np.array([U1, phi, theta])

    def update(self, commanded, state, pkt, Ts):

        if 'Camera' in pkt:
            x_cam = pkt['Camera']

        else:
            x_cam = np.array([[400,300]]).T
#             x_cam = np.array([[0,0]]).T

        #
        # Outer Loop
        #

        cmds = self.outer(x_cam, state, Ts)

#         print(cmds)

        #
        # Inner Loop
        #

        cmds[1] = 0
        cmds[2] = 0

        u = self.innerPID(cmds, state, Ts)

#         print(u.T)

        # update the commanded states
        commanded[6] = cmds[1] # phi
        commanded[7] = cmds[2] # theta

        # actuator commands
        return u, commanded
```

```python
# target dynamics
def target_tick(capture, n, Ts):

    # ellipse
    f = 0.05 # Hz
    x = 10*np.sin(2*np.pi*f*n*Ts)
    y = 5*np.cos(2*np.pi*f*n*Ts)

    x = y = 0
    x = -5

#     f = 0.017
#     x = 0*np.sin(2*np.pi*f*n*Ts)
#     y = 2.5*np.cos(2*np.pi*f*n*Ts)

    return np.array([[x,y,0]]).T

# Instantiate a quadrotor model with the given initial conditions
quad = Quadrotor(r=np.array([[-10],[0],[-10]]),
                 v=np.array([[0],[0],[0]]),
               Phi=np.array([[0],[0],[0]]))

# Create sensors
camera = Camera(tick=target_tick)
camera.set_transformation(R=Rb2c)

# Register sensors with the sensor manager
sens = SensorManager()
sens.register(camera)

# Instantiate an IBVS controller
ctrl = IBVS(camera)

# Instantiate an estimator that gives truth with body velocities
estm = Estimator(body=True)

# Setup a setpoint commander
cmdr = Commander(default=True)

# Run the simulation
sim = Simulator(quad, ctrl, estm, cmdr, sens)
sim.run(60, Ts=0.01)
sim.plot()

print(quad)
```

## Velocity Observer

In our IBVS controller we will need to use the velocity of the virtual camera frame $\mathcal{F}^{VC}$. Here we design an observer to estimate the velocity of the virtual camera frame.

```python
%%capture
_NO_SIM = True
%run sliding_mode_control.ipynb
_NO_SIM = False
```

```python
class VelocityObserver(Estimator):
    def __init__(self, camera, body=True):
        super().__init__(body=body)
        self.name = "Virtual Camera Velocity Observer"

        self.camera = camera

        self.g = 9.81
        self.m = 3.81

        # observer variables
        self.v0 = np.zeros((3,1))
        self.vhat = np.zeros((3,1))
        self.q1hat = np.zeros((3,1))

    def update(self, quad, u, Ts):

        truth = self.get_truth(quad)
        state = np.copy(truth)

        x_cam = self.camera.meas

        # calculate unit LOS vectors to targets
        hx_veh1 = target_los(x_cam, state, self.camera)

#         print(x_cam.T, hx_veh1.T)

        ########
        ########
        ########

        # calculate the depth value when the camera is at the desired pose
        h = -truth[2,0]
        zdes = h/np.cos(np.pi/4)

        # LOS vector is the feature q
        q = hx_veh1

        # image error
        q1 = q - np.array([[0,0,1]]).T

        # gains
        k1 = 5
        k2 = 0.5

        # estimation error equation (19)
        q1err = q1 - self.q1hat

        # equation (23)
        Q = state[11, 0] # yaw dot
        f = lambda q1hat: -1/zdes*self.vhat - np.array([[0,-Q,0],[Q,0,0],[0,0,0]]).dot(q1) + k1*q1err
        self.q1hat = rk4(f, self.q1hat, Ts)

        # equation (22)
        Rb_2_v1 = rot3(state[6,0],state[7,0],0).T
        force = -Rb_2_v1.dot(np.array([[0,0,u[0]]]).T)/self.m + np.array([[0,0,self.g]]).T
        f = lambda v0: -np.array([[0,-Q,0],[Q,0,0],[0,0,0]]).dot(self.vhat) + force - k2*k1*q1err - 1/zdes*q1err
        self.v0 = rk4(f, self.v0, Ts)

        # equation (21)
        self.vhat = self.v0 - k2*q1err

        state[3:6] = self.vhat

        return state, truth
```

```python
# Instantiate a quadrotor model with the given initial conditions
quad = Quadrotor(r=np.array([[-13],[0],[-13.5]]),
                 v=np.array([[0],[0],[0]]),
               Phi=np.array([[0],[0],[0]]))

def target_tick(capture, n, Ts):
    return np.array([[1,0,0]]).T

# Create sensors
camera = Camera(fps=30, tick=target_tick)
camera.set_transformation(R=Rb2c)

# Register sensors with the sensor manager
sens = SensorManager()
sens.register(camera)

# Instantiate a tracking sliding mode controller
ctrl = SMC()

# Instantiate a virtual camera velocity observer
estm = VelocityObserver(camera, body=False)

# Setup a setpoint commander
cmdr = Commander(default=True)
def set_position(i, Ts):
    f = 0.1
    y = np.cos(2*np.pi*f*i*Ts)
    x = 0.1*np.sin(2*np.pi*f*i*Ts)
    return np.array([-15+x, y, -15])
cmdr.position(set_position)

# Run the simulation
sim = Simulator(quad, ctrl, estm, cmdr, sens)
sim.run(20, Ts=0.01)
sim.plot()
```

## References

[1] D. Zheng, H. Wang, J. Wang, S. Chen, W. Chen, and X. Liang, “Image-based visual servoing of a quadrotor using virtual camera approach,” IEEE/ASME Trans. Mechatronics, vol. 22, no. 2, pp. 972–982, 2017.