```
# This is formatted as code
```

# Tutorial 2: Outliers
**Week 3: What should we do when data has problems?**

**Content creators**: Rob Lindgren

**Content reviewers**: Konrad Kording, Keervani Kandala

**Content modifiers**: ---

**Modified Content reviewer**: ---

```python
# @title Due Dates Calendar

from ipywidgets import widgets
from IPython.display import display, IFrame, YouTubeVideo

out1 = widgets.Output()
with out1:
  calendar = IFrame(src="https://calendar.google.com/calendar/embed?src=356b9d2nspjttvgbb3tvgk2f58%40group.calendar.google.com&ctz=America%2FNew_York", width=600, height=480)
  display(calendar)

out = widgets.Tab([out1])
out.set_title(0, 'Calendar')

display(out)
```

    Tab(children=(Output(),), _titles={'0': 'Calendar'})

___
# Tutorial Objectives

*Estimated timing of tutorial: 30 minutes*

This is the tutorial 2 in a 3-part series on how to handle data that has problems. In this tutorial, we will learn about outliers: what problems they can cause, how to identify them, and how to remove them. By the end of this tutorial, you will be able to:

- Explain the two possible causes of outliers, erroneous observations and natural variation
- Identify outliers using histograms, scatterplots, boxplots, and z-scores
- Remove outliers from a DataFrame using Boolean masks

```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/hncv7/?direct%26mode=render%26action=download%26mode=render", width=854, height=480)
```

---
# Setup

```python
# Imports

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import random
```

```python
# @title Plotting Function

# Solution
def plt_cars(df):
  """Plot histograms of 'Horsepower' and ' Highway mpg' from the Cars dataset,
  as well as a scatterplot of 'Horsepower' vs. 'Highway mpg'.

  Args:
    df (DataFrame): Cars dataset, with variables 'Horsepower' and 'Highway 'mpg'.

  Returns:
    None
  """

  # Compute means
  means = df.mean()

  # Create figure and axes objects
  fig_a, (ax1, ax2) = plt.subplots(1, 2)

  # Visualize 'Horsepower'
  ax1.hist('Horsepower', data=df)
  ax1.set_xlabel("Horsepower")
  ax1.set_ylabel("Number of vehicles")
  ax1.axvline(means['Horsepower'], color='Orange')

  # Visualize 'Highway mpg'
  ax2.hist('Highway mpg', data=df)
  ax2.set_xlabel("Highway mpg")
  ax2.set_ylabel("Number of vehicles")
  ax2.axvline(means['Highway mpg'], color='Orange')
  print(fig_a)

  print('\n')

  # Visualize the relationship between 'Horsepower' and 'Highway mpg'
  fig_b, ax = plt.subplots(1, 1)
  ax.scatter('Horsepower', 'Highway mpg', data=df)
  ax.set_xlabel('Horsepower')
  ax.set_ylabel('Highway mpg')
  print(fig_b)

```

```python
# @title Regression Function
def regress(df_in, x_lab, y_lab):
  # Takes a dataframe and two variable names from the dataframe
  # and returns a dictionary with the results of a linear regression
  # of y_lab on x_lab.

  # Output dictionary looks like this...
  # {'prediction' : predicted_values,
  #  'intercept' : intercept
  #  'coef' : coefficient}
  df = df_in.copy()

  from sklearn.linear_model import LinearRegression

  x = df.loc[:, [x_lab]].values.reshape(-1, 1)  # values converts it into a numpy array
  y = df.loc[:, [y_lab]].values.reshape(-1, 1)  # -1 means that calculate the dimension of rows, but have 1 column
  reg = LinearRegression()  # create object for the class
  reg.fit(x, y)  # perform linear regression
  y_pred = reg.predict(x)  # make predictions

  out = {'prediction' : reg.predict(x),
        'intercept' : reg.intercept_[0],
        'coef' : reg.coef_[0][0]}

  return out
```

---
# Prepare Data

*Estimated timing to here from start of tutorial: ???*

Once again, let's load the Cars dataset, subset it, and verify that we got the subset that we expected.

```python
data_url = 'https://raw.githubusercontent.com/RealTimeWeb/datasets/master/datasets/csv/cars/cars.csv'
df = pd.read_csv(data_url)[['ID', 'Horsepower', 'Highway mpg']]
df.head()
```

# Section 1: What problems do they pose?

- **Erroneous observations:** Outliers might indicate problems with measurement or data entry. For this reason, they must be identified and investigated. Data points that do not belong in the data set should be removed and measurement and data entry issues need to be corrected. E.g. we run a questionaire and a user misunderstands a question.
- **Natural variation**: Even accurately measured and recorded data can have outliers, and the presence of these outliers can have a substantial effect on the results of an analysis. It is possible that in this case that you might need to narrow your sample, thus excluding the outliers. E.g. a datapoint that is correct but unusual, and therefore one we may want to ignore for making predictions.

**Philosophy of outliers**
As such, we want to emphasize that how to deal with outliers is complex. If we are running a spam sending facility, we may want to focus on modeling the outliers well - we rely on outstandingly gullible people. If we want to build a tool to be used by many people we may mostly care about people that are somewhat typical and ignore the outliers. It is important to think through what we are trying to achieve with our modeling.

```python
# @title Video 6:How does horsepower relate to efficiency
from ipywidgets import widgets
from IPython.display import display, IFrame, YouTubeVideo

out1 = widgets.Output()
with out1:
  video = YouTubeVideo(id="HhSjVj2AOgc", width=854, height=480, fs=1, rel=0)
  print(f'Video available at https://youtube.com/watch?v={video.id}')
  display(video)

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

display(out)
```

## Coding Exercise 1: What is the relationship between Horsepower and Fuel Efficiency?

Use the `regress()` function, defined above, to estimate a linear model of the relationship between 'Horsepower' and 'Highway mpg'. Then perform the same regression on the subset of `df` for which 'Horsepower' is greater than 300.

```python
###########################################################################
## TODO for students: Estimate a linear model of Horsepower vs. Highway mpg
## for both the whole dataset and the subset with Horsepower > 300.
raise NotImplementedError('student exercise: Regression on whole dataset and subset')
###########################################################################

# Regression on full dataset
results = regress(..., ..., ...)
print('Slope of the linear model estimated on the whole dataset:')
print(results['coef'])

print('\n')

# Regression on subset
# hint you can do things like df[df['Horsepower'] > 100] to select only cars with at least 100 horsepower
results_subset = regress(..., ..., ...)
print('Slope of the linear model estimated on only observations for which Horsepower > 300')
print(results_subset['coef'])
```

```python
# to_remove Solution

# Regression on full dataset
results = regress(df, 'Horsepower', 'Highway mpg')
print('Slope of the linear model estimated on the whole dataset:')
print(results['coef'])

print('\n')

# Regression on subset
results_subset = regress(df[df['Horsepower'] > 300], 'Horsepower', 'Highway mpg')
print('Slope of the linear model estimated on only observations for which Horsepower > 300')
print(results_subset['coef'])
```

## Discussion Question 1

What do the results of our two regressions tell us about Horsepower and Fuel Efficiency?

- Is there a different relationship between 'Horsepower' and 'Highway mpg' for high-horsepower vehicles than for others?
- Why or why not?

```python
#to remove (only for TA)
# The efffect for high HP cars is entirely driven by the one outlier we saw in the previous tutorial. Let the students figure that out themselves!
```

# Section 2: Identifying outliers
So how do we identify outliers? The first approach almost always has to be to visualize the data.

```python
# @title Video 7: Let us visualize outliers
from ipywidgets import widgets
from IPython.display import display, IFrame, YouTubeVideo

out1 = widgets.Output()
with out1:
  video = YouTubeVideo(id="omVZoJ-62u8", width=854, height=480, fs=1, rel=0)
  print(f'Video available at https://youtube.com/watch?v={video.id}')
  display(video)

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

display(out)
```

## Section 2.1 Visualization is your friend

We already noticed that we have outliers in the Cars dataset back in Tutorial 1. Let's revisit our histograms and scatterplot.

```python
plt_cars(df)
```

Another good plot for identifying outliers is the boxplot. DataFrames have their own method for this which produces a collective boxplot of all variables in the dataset.

```python
df.boxplot()
```

We can see, once again, that there is a clear outlier in 'Highway mpg'. It also looks like there are some 'Horsepower' datapoints that lie beyond the 'whiskers', which represents a distance 1.5 times larger than the interquartile range.

## Section 2.2: Z-scores

Another way to identify outliers in your data is by calculating Z-scores for each data point. Z-scores measure the distance of a data point from the mean in terms of standard deviation, calculated as below.

\begin{align}
z = \frac{x - \overline{x}}{s}
\end{align}

$z$ is the score, $x$ the data point for which we're measuring the score, $\overline{x}$ is the sample mean, and $s$ is the sample standard deviation. A data point with a Z-score of 2 is 2 standard deviations above the mean.

How can we calculate Z-scores for all the data in our dataset? Pandas provides methods for calculating both mean and standard deviation by variable.

```python
print(df.mean())
print('\n')
print(df.std())
```

These methods work intuitively with Pandas' indexing to allow us to subtract the mean of each column from each value in that column,

```python
df - df.mean()
```

...as well as divide each value by its column's standard deviation.

```python
df / df.std()
```

### Coding Exercise 2.2: Identifying Outliers by Z-scores

Fill in the function below so that it calculates the Z-score for each entry in the input DataFrame and returns a boolean DataFrame indicating whether each value is an outlier.

Note: It is common to use 3 as your threshold because 99.7% of all observations fall within 3 standard deviations in normally distributed data.

```python
###########################################################################
## TODO for students: Fill in detect_outliers().
raise NotImplementedError('student exercise: detect outliers function')
###########################################################################

def detect_outliers(df_in, thresh):
  df = df_in.copy()
  df_z = (...) / ...
  df_out = ... > thresh # Hint: use np.abs() to get the absolute values of each entry
  return df_out

df_outliers = detect_outliers(df, 3)
df_outliers.head()

```

```python
# to_remove Solution
def detect_outliers(df_in, thresh):
  df = df_in.copy()
  df_z = (df - df.mean()) / df.std()
  df_out = np.abs(df_z) > thresh
  return df_out

df_outliers = detect_outliers(df, 3)
df_outliers.sum()

```

# Section 3: Removing outliers

```python
# @title Video 8: Removing outliers
from ipywidgets import widgets
from IPython.display import display, IFrame, YouTubeVideo

out1 = widgets.Output()
with out1:
  video = YouTubeVideo(id="xIv49TK7OrU", width=854, height=480, fs=1, rel=0)
  print(f'Video available at https://youtube.com/watch?v={video.id}')
  display(video)

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

display(out)
```

### Coding Exercise 3: Detecting and Removing Outliers

```python
###########################################################################
## TODO for students: Fill in remove_outliers().
raise NotImplementedError('student exercise: remove outliers function')
###########################################################################

def remove_outliers(df_in, thresh):
  df = df_in.copy()
  df_out = ...
  return df_out

df_rem = remove_outliers(df, 3)

plt_cars(df_rem)
```

```python
# to_remove Solution

def remove_outliers(df_in, thresh):
  df = df_in.copy()
  df_out = df[np.logical_not(detect_outliers(df, thresh)).all(axis=1)]
  return df_out

df_rem = remove_outliers(df, 3)

plt_cars(df_rem)

```

# Section 4: Discussion

- How can outliers affect the results of an analysis?
- Will every method of outlier detection always give the same results?
- Should you always remove outliers?


# 2. Basic Chemical, Electrical, and Thermodynamic Principles
---

## 2.1 Electrostatic Potential across a Biological Membrane

## 2.2 Biochemical Thermodynamics: The ATP Hydrolysis Potential

## 2.3 A Coupled Electrochemical Process: ATP Synthesis

The electrostatic energy potential across the mitochondrial inner membrane is used to drive the synthesis of ATP in the final step of oxidative ATP synthesis. The mammalian mitochondrial $\rm F_{1} F_{0} \,$ ATP synthase synthesizes ATP in the mitochondrial matrix from ADP and inorganic phosphate, coupled to the translocation of protons (H+ ions) from outside to inside of the matrix. The overall reaction to describe this process takes the following form:

\begin{equation}
\rm MgADP^{1-} + HPO4^{2-} + \frac{1}{2} O_{2} + H^{+}_{inside} + NADH \leftrightharpoons MgATP^{2-} + H_{2}O
+ NAD^{+} \label{eq}\tag{2.1}
\end{equation}

where the term $\rm H^{+}_{inside}$ indicates that a hydrogen ion from inside the matrix is covalently incorporated into the synthesized ATP. The species $\rm MgADP^{1-}$ and $\rm MgATP^{2-}$ are the magnesium-bound species of ADP and ATP. This chemical reaction is coupled to the net transport of nA = $\rm \frac{8}{3}$ protons across the inner membrane:

\begin{equation}
\rm 8(H^{+})_{outside} \leftrightharpoons 3(H^{+})_{inside}
\end{equation}

\begin{equation}
\rm nA = \frac{(H^{+})_{outside}}{(H^{+})_{inside}} = \frac{8}{3}
\end{equation}

where nA represents the P/O ratio, that is, the average number of ATP synthesized per $\rm \frac{1}{2}O_{2}$ consumed. It is important to note that this approximate P/O ratio is measured when NADH is the electron donor as shown in equation 2.1. However, when electrons are oxidized from succinate as the substrate, the average P/O ratio is reduced to $\frac{3}{2}$. This decrease in P/O ratio is measured because electrons derived from succinate oxidation bypass a proton translocation site, complex I, and instead enter through complex II.

### 2.3.1 Given a free magnesium concentration [Mg$\rm^{2+}$] and hydrogen ion activity [H$\rm^{+}$] = 10$\rm^{-pH}$, how can you compute the concentrations of MgADP$\rm^{1-}$, MgATP$\rm ^{2-}$, and HPO4$\rm^{2-}$ in terms of the total concentrations of the reactants [$\Sigma$ADP],  [$\Sigma$ATP], and [$\Sigma$Pi]? (You will need to account for binding of biochemical species to [Mg$\rm ^{2+}$] and [H$\rm ^{+}$].)

Let's start with looking at the species of ATP:

\begin{eqnarray}
[\Sigma \rm ATP] &=& [\rm ATP^{4-}] + [\rm H \cdot \rm ATP^{3-}] + [\rm K \cdot \rm ATP^{3-}] + [\rm Mg \cdot \rm ATP^{2-}]\\
&=& [\rm ATP^{4-}] + \frac{[\rm H^{+}]}{\rm HATP^{3-}}[\rm ATP^{4-}] + \frac{[\rm K^{+}]}{\rm KATP^{3-}}[\rm ATP^{4-}] + \frac{[\rm Mg^{2+}]}{\rm MgATP^{2-}}[\rm ATP^{4-}]\\
&=& [\rm ATP^{4-}] \left(1 + \frac{[\rm H^{+}]}{\rm K_{HATP}}+ \frac{[\rm K^{+}]}{\rm K_{\rm KATP}} + \frac{[\rm Mg^{2+}]}{\rm K_{\rm MgATP}}\right)
\end{eqnarray}

(This expression ignores the two cation bound forms)

then multiplying the right hand side of the expression by $\frac{[\rm Mg^{2+}]}{\rm K_{\rm MgATP}}$, we get the following:

\begin{eqnarray}
&=& \frac{[\rm Mg^{2+}]}{\rm K_{\rm MgATP}}[\rm ATP^{4-}] \cdot
\left(\frac{\rm K_{\rm MgATP}}{\rm[Mg^{2+}]}
+ \frac{\rm K_{\rm MgATP}}{\rm [Mg^{2+}]} \cdot \frac{\rm [H^{+}]}{\rm K_{\rm KHATP}}
+ \frac{\rm K_{\rm MgATP}}{\rm [Mg^{2+}]} \cdot \frac{\rm [K^{+}]}{\rm K_{\rm KATP}}
+ \frac{\rm K_{\rm MgATP}}{\rm [Mg^{2+}]} \cdot \frac{\rm [Mg^{2+}]}{\rm K_{\rm MgATP}}\right);
\end{eqnarray}

and given that

 $$
\rm [Mg \cdot ATP^{2-}] = \frac{\rm [Mg^{2+}]}{\rm K_{\rm MgATP}} \cdot \rm [ATP^{4-}],
 $$

 we substitute it into the expression and get the following:

\begin{eqnarray}
 &=& \rm [Mg \cdot ATP^{2-}] \cdot
\left(\frac{\rm K_{\rm MgATP}}{\rm[Mg^{2+}]}
+ \frac{\rm K_{\rm MgATP}}{\rm [Mg^{2+}]} \cdot \frac{\rm [H^{+}]}{\rm K_{\rm KHATP}}
+ \frac{\rm K_{\rm MgATP}}{\rm [Mg^{2+}]} \cdot \frac{\rm [K^{+}]}{\rm K_{\rm KATP}}
+ \frac{\rm K_{\rm MgATP}}{\rm [Mg^{2+}]} \cdot \frac{\rm [Mg^{2+}]}{\rm K_{\rm MgATP}}\right);
\end{eqnarray}

then pulling out the common factor $\rm \frac{K_{MgATP}}{[Mg^{2+}]}$ we arrive at the following:

\begin{eqnarray}
 &=& \rm [Mg \cdot ATP^{2-}] \cdot \frac{K_{MgATP}}{Mg^{2+}}
\left(1
+ \frac{\rm [H^{+}]}{\rm K_{\rm KHATP}}
+ \frac{\rm [K^{+}]}{\rm K_{\rm KATP}}
+ \frac{\rm [Mg^{2+}]}{\rm K_{\rm MgATP}}\right);
\end{eqnarray}

Then we substitute in the expresssion for the binding polynomial to account for interactions with biochemical species including $\rm H^{+},$ $\rm K^{+},$ and $\rm Mg^{2+}$ ions

$$\rm P_{ATP} = \left(1 + \frac{[H^{+}]}{K_{HATP}} + \frac{[K^{+}]}{K_{KATP}} + \frac{[Mg^{2+}]}{K_{MgATP}}\right),$$

and arrive at the following:

\begin{eqnarray}
\rm[\Sigma ATP] &=& \rm [Mg \cdot ATP^{2-}] \frac{\rm K_{\rm MgATP}}{[\rm Mg^{2+}]} \cdot
P_{ATP}
\end{eqnarray}

which can now be solved as

$$
\rm [Mg \cdot ATP^{2-}] = [\Sigma ATP] \cdot \frac{[Mg^{2+}]/K_{MgATP}}{P_{ATP}}
$$

Following the same process for the other energetic substrates produces the following solutions:

\begin{eqnarray}
[\rm Mg \cdot ADP^{-}] &=& [\Sigma \rm ADP] \cdot \frac{[\rm Mg^{2+}]/K_{MgADP}}{\rm P_{\rm MgADP}}\\
[\rm HPO_{4}^{2-}] &=& [\Sigma \rm Pi] \cdot \frac{[\rm H^{+}]/K_{HPO4}}{P_{Pi}}
\end{eqnarray}

### 2.3.2 Derive an expression for the Gibbs free energy change associated with reaction (2.1) in terms of the reference $\rm \Delta G^{o}$, the concentrations of biochemical reactants, and the cation concentrations [Mg$^{2+}$] and [H$^{+}$]. What is the free energy of ATP synthesis in the mitochondrial matrix? Assume that pH = 7.2 and [$\Sigma$ADP] = 9.5 mM, [$\Sigma$ATP] = 5 mM, and [$\Sigma$Pi] = 1 mM.

Consider the ATP synthesis reaction

\begin{equation}
\rm ADP^{3-} + HPO4^{2-} + \frac{1}{2} O_{2} + H^{+}_{inside} \leftrightharpoons ATP^{4-} + H_{2}O,
\end{equation}

With equlibrium mass action ratio

\begin{equation}
\rm K_{eq} = e^{-\Delta G_{r}^{o}/RT}
= \left(\frac{[ATP^{4-}]}{[H^{+}][HPO_{4}^{2-}][ADP^{3-}]}\right)_{eq}.
\end{equation}

Substituting the expressions for the binding polynomials results in

\begin{equation}
\rm K_{eq} = e^{-\Delta G_{r}^{o}/RT}
= \left(\frac{[\Sigma ATP]}{[\Sigma ADP][\Sigma Pi]}\right)_{eq} \cdot \frac{P_{ADP}P_{Pi}}{[H^{+}]P_{ATP}},
\end{equation}

which yields an expression for the apparent equilibrium constant in terms of biochemical reactants

$$
\rm K^{'}_{eq} = e^{-\Delta_{r} G^{'o}/RT}
= \left(\frac{[\Sigma ATP]}{[\Sigma ADP] [\Sigma Pi]}\right)_{eq}
= K_{eq} \frac{[H^{+}]P_{ATP}}{P_{ADP} P_{Pi}}.
$$

Using these equations, there are a couple of equivalent ways to express the Gibbs free energy change for this reaction. One is based on the reference chemical reaction

$$
\rm \Delta_{r} G^{'} = \Delta_{r} G^{o} + RTln \left(\frac{[ATP^{4-}]}{[H^{+}][HPO_{4}^{2-}][ADP^{3-}]}\right).
$$

The other is

$$
\rm \Delta_{r} G^{'} = \Delta_{r} G^{'o} + RTln \left(\frac{[\Sigma ATP]}{[\Sigma ADP] [\Sigma Pi]}\right).
$$

To verify that these two equations give the same value, take the following values for dissociation constants

|Reactant (L)| $\rm K_{H-L}$            | $\rm K_{K-L}$             | $\rm K_{Mg-L}$           |
|------------|--------------------------|---------------------------|--------------------------|
|ATP         |2.757 $\times$ 10$^{-7}$ M|9.809 $\times$ 10$^{-2}$ M |8.430 $\times$ 10$^{-5}$ M|
|ADP         |4.106 $\times$ 10$^{-7}$ M|1.319 $\times$ 10$^{-1}$ M |7.149 $\times$ 10$^{-4}$ M|
|Pi          |2.308 $\times$ 10$^{-7}$ M|3.803 $\times$ 10$^{-1}$ M |2.815 $\times$ 10$^{-2}$ M|

and $\rm \Delta_{r} G^{o}$ = -4.67 $\rm \frac{kJ}{mol}$, which yields $\rm K_{eq}$ = 6.11698 at 37$\rm^{o}$C.

The Python code below verifies that both approaches yield the same value

```python
# %%

import numpy as np

# Dissociation Constants
K_HATP = 2.757e-7
K_KATP = 9.809e-2
K_MgATP = 8.430e-5
K_HADP = 4.106e-7
K_KADP = 1.319e-1
K_MgADP = 7.149e-4
K_HPI  = 2.308e-7
K_KPI  = 3.803e-1
K_MgPI  = 2.815e-2

# Other constants
DGr0 = -4670 #J/mol
RT = 310.15*8.3145 #J/mol
Keq = np.exp(-DGr0/RT)

# Concentrations
ATP = 0.5e-3
ADP = 9.5e-3
Pi  = 1e-3
Mg  = 1e-3
H   = 10**(-7.2)
K   = 100e-3

# binding polynomials:
P_ATP = 1 + H/K_HATP + K/K_KATP + Mg/K_MgATP
P_ADP = 1 + H/K_HADP + K/K_KADP + Mg/K_MgADP
P_Pi =  1 + H/K_HPI + K/K_KPI + Mg/K_MgPI

# reference species concentration
ATP_s = ATP/P_ATP
ADP_s = ADP/P_ADP
Pi_s = Pi/P_Pi

# compute DeltaG_r in terms of reference species
DG1 = +DGr0 + RT*np.log(ATP_s/(H*Pi_s*ADP_s))
print("First equation:",DG1," J/mole")

K_apparent = Keq*(H*P_ATP/(P_ADP*P_Pi))
DG2 = -RT*np.log(K_apparent) + RT*np.log(ATP/(ADP*Pi))
print("Second equation:",DG2," J/mole")
```

    First equation: 45729.92366366501  J/mole
    Second equation: 45729.92366366501  J/mole

The positive $\rm \Delta_{r} G$ indicates that the reaction is thermodynamically unfavorable in the direction toward ATP synthesis. This result implies that ATP synthesis can only occur when coupled to some other thermodynamically favorable reaction.

### 2.3.3 What is (a) the free energy change of Equation (2.2) at $\rm \Delta \rm \Psi$ = 180 mV? (b) How does the free energy change of Equation (2.1) compare to that of Equation (2.2)? (c) How efficient is the transduction of electrical to chemical free energy in this step in ATP synthesis? To obtain this information, calculate the ratio of energy stored in ATP to the total energy consumed.

**(a)** Gibbs free energy for the reaction can be calculated by taking the product of the number of protons needed for ATP synthesis, the membrane potential in J/C, and Faraday's constant in C/mol

\begin{equation}
\rm \ \Delta_{r} G_{H^{+}} = n_{A}F \Delta \Psi\
=\left(\frac{8}{3}\right) \times \left(0.180 \, \frac{J}{C}\right) \times
\left(96,485 \, \frac{C}{mol}\right)
= 46,312.89 \, \frac{J}{mol}
\end{equation}

This calculation represents the energy required to move the protons needed for synthesis of one ATP molecule against the electrostatic gradient from the matrix to the intermembrane space. Conversely, there is an electrical force driving postive charge into the matrix.

**(b)** Since the ATP synthesis reaction is coupled to $\rm n_{A}$ = $\rm \frac{8}{3}$ protons that translocate to the matrix, the overall free energy change for the coupled transport and reaction process is:

\begin{eqnarray}
\rm \Delta_{r} G &=&
\rm \Delta_{r} G_{ATP}^{'o} + RTln\left(\frac{\Sigma ATP}{\Sigma ADP \Sigma Pi}\right) - \Delta_{r} G_{H^{+}}\\
&=& \rm \Delta_{r} G_{ATP} - \Delta_{r} G_{H^{+}}
\end{eqnarray}

Plugging in the numbers in from section (2.2), we arrive at

\begin{eqnarray}
\rm \Delta_{r} G &=& +45.73 \rm \frac{kJ}{mol} - 46.31 \rm \frac{kJ}{mol}
\\8
&=& -0.58 \frac{\rm kJ}{\rm mol}
\end{eqnarray}

which indicates that, given the assumed concentrations, the system operates near equilibrium.

**(c)** The ratio of chemical energy stored in the ATP hydrolysis potential to electrical energy consumed is $\rm \frac{45.73}{46.32} \approx$ 98.7%

### 2.3.4 Given the concentrations assumed in 2.3.2, what is the minimum value of $\rm \Delta\Psi$ at which ATP can be synthesized in the mitochondrial matrix?

At equilibrium, $\rm \Delta G = 0$, where the electrical energy consumed is equal to the chemical energy required for ATP synthesis:

\begin{equation}
    \rm \Delta G_{\rm{ATP}} = \rm n_{A} \Delta G_{\rm{H}^{+}} = \rm n_{A} (F \Delta \Psi)
\end{equation}

Rearranging the equation to solve for the electrical energy

\begin{equation}
\rm \Delta \Psi  = \rm \frac{\Delta G_\mathrm{ATP} }{ n_A F } = \rm 177.7 \ \mathrm{mV}.
\end{equation}

Therefore, $\Delta \Psi$ = 177.7 is the minimum value of membrane potential at which ATP can be synthesized.

### 2.3.5 Assume that reaction (2.1) proceeds by simple mass-action kinetics, with a constant reverse rate $\rm k_{r}$. How does the forward rate constant necessarily depend on $\rm \Delta \rm \Psi$ for the reaction kinetics to be properly thermodynamically balanced?

The reaction rate kinetics can be written in terms of ATP synthesis as follows:

\begin{equation}
\rm J = J_f - J_r = k_{f}[ADP^{3-}][HPO_{4}^{2-}] - k_{r}[ATP^{4-}],
\end{equation}

or in terms of the sums of species, $\rm J_{F1F0ATPase}$

\begin{equation}
\rm J_{F1F0ATPase} = J_f - J_r = k_{f}[\Sigma ADP][\Sigma Pi] - k_{r}[\Sigma ATP].
\end{equation}

At thermodynamic equilibrium, the equation can be written as,

$$
\rm k_{f}[\Sigma ADP][\Sigma Pi] - k_{r}[\Sigma ATP]= 0.
$$

Solving for $\rm K^{'}_{F1F0ATPase}$ results in,

$$
\rm K^{'}_{F1F0ATPase} = \left(\frac{[\Sigma ATP]}{[\Sigma ADP][\Sigma Pi]}\right)_{eq} = \frac{k_{f}}{k_{r}.}
$$

Writing $\rm k_{f}$ in terms of $\rm k_{r}$ and substituting into the equation above provides,

$$
\rm k_{r}k^{'}_{F1F0ATPase}[\Sigma ADP][\Sigma Pi] - k_{r}[\Sigma ATP].
$$

Then pulling out $\rm k_{r}$ as a common factor,

$$
\rm k_{r}(k^{'}_{F1F0ATPase}[\Sigma ADP][\Sigma Pi] - [\Sigma ATP]).
$$

Therefore, a simple mass-action expression for the reaction flux that satisfies this thermodynamic relation is

\begin{equation}
\rm J_{F1F0ATPase} = \rm k_{r}(k^{'}_{F1F0ATPase}[\Sigma ADP][\Sigma Pi] - [\Sigma ATP]),
\end{equation}

where

\begin{eqnarray}
  \frac{\mathrm{d}}{\mathrm{dt}} [\Sigma \mathrm{ATP}] &=& J_{\mathrm{F}_1\mathrm{F}_0\mathrm{ATPase}} \\
  \frac{\mathrm{d}}{\mathrm{dt}} [\Sigma \mathrm{ADP}] &=& -J_{\mathrm{F}_1\mathrm{F}_0\mathrm{ATPase}} \\
  \frac{\mathrm{d}}{\mathrm{dt}} [\Sigma \mathrm{Pi}] &=& -J_{\mathrm{F}_1\mathrm{F}_0\mathrm{ATPase}} \\
\end{eqnarray}

#### The derivation for $\rm k^{'}_{F1F0ATPase}$ is shown below:

To recap, the free energy for the ATP synthesis reaction

\begin{equation}
\rm ADP^{3-} + \rm HPO_{4}^{2-} + \rm \frac{1}{2} O_{2} + \rm H^{+}_{inside} \leftrightharpoons \rm ATP^{4-} + \rm H_{2}O,
\end{equation}

is

\begin{equation}
\rm \Delta_{r}G_{ATP} = \rm \Delta_{r}G_{ATP}^{'o} + RTln \left(\frac{[ATP^{4-}]}{[H^{+}][HPO_{4}^{2-}][ADP^{3-}]}\right).
\end{equation}

where $\rm \Delta_{r}G_{ATP}^{o} \approx$ -4.6 kJ/mol is the standard Gibbs free energy change for the reference chemical reaction. In mitochondria, ATP synthesis is coupled to proton transport from outside of to inside of the matrix:

\begin{equation}
\rm 8(H^{+})_{outside} \leftrightharpoons 3(H^{+})_{inside}
\end{equation}

\begin{equation}
\rm nA = \frac{(H^{+})_{outside}}{(H^{+})_{inside}} = \frac{8}{3}
\end{equation}

Which is associated with a free energy change,

\begin{equation}
\rm \Delta_{r} G_{H^+} = \rm n_{A}F \Delta \Psi + \rm RTlog \left( \frac{[H^{+}]_{inside}}{[H^{+}]_{outside}}\right)^{n_{A}}.
\end{equation}

This free energy is sometimes called the proton motive force. The last term on the right-hand side represents  the proton-transport free energy produced from the pH gradient across the inner mitochondrial membrane.

The free energy of coupled ATP synthesis and proton transport is

\begin{equation}
\rm \Delta_{r} G = \rm \Delta_{r}G_{ATP}^{'o} + \rm RT log \left(\frac{[ATP^{4-}]}{[H^+][HPO_{4}^{2-}][ADP^{3-}]}\right) - \rm n_{A}F\Delta\Psi - \rm RTlog \frac{([H^{+}]_{outside})^{n_{A}}}{([H^{+}]_{inside})^{n_{A}-1}}.
\end{equation}

Chemical equilibrium occurs when $\rm \Delta_r G = 0$:

$$
\rm RTlog \left( \frac{[HPO_{4}^{2-}][ADP^{3-}]}{[ATP^{4-}]}\right)_{eq} = \Delta_{r}G_{ATP}^{o} - n_{A}F\Delta\Psi - RTlog \frac{([H^{+}]_{outside})^{n_{A}}}{([H^{+}]_{inside})^{n_{A}-1}}.
$$

Dividing by RT on both sides and re-writing the equilibrium expression in terms of ATP synthesis yields

$$
\rm -log \left( \frac{[ATP^{4-}]}{[HPO_{4}^{2-}][ADP^{3-}]}\right)_{eq}= \frac{\Delta_{r}G_{ATP}^{o} - n_{A}F\Delta\Psi}{RT} - log\frac{([H^{+}]_{outside})^{n_{A}}}{([H^{+}]_{inside})^{n_{A}-1}}.
$$

Cancelling the log terms by raising both sides to the base root of 10 produces

$$
\rm k_{eq} = \left( \frac{[ATP^{4-}]}{[HPO_{4}^{2-}][ADP^{3-}]}\right)_{eq} = 10^{-\frac{(\Delta_{r}G_{ATP}^{o} - n_{A}F\Delta\Psi)}{RT}} \cdot \frac{([H^{+}]_{outside})^{n_{A}}}{([H^{+}]_{inside})^{n_{A}-1}}.
$$

Or in terms of sums of species

$$
\rm K^{'}_{F1F0ATPase} = \left(\frac{[\Sigma ATP]}{[\Sigma Pi][\Sigma ADP]}\right)_{eq} =10^{-\frac{(\Delta_{r}G_{ATP}^{o} - n_{A}F\Delta\Psi)}{RT}} \cdot \frac{P_{ATP}}{P_{ADP}P_{Pi}} \cdot \frac{([H^{+}]_{outside})^{n_{A}}}{([H^{+}]_{inside})^{n_{A}-1}}.
$$

### 2.3.6 Write a simple program that simulates the kinetics of [$\Sigma$ADP], [$\Sigma$ATP] , and [$\Sigma$Pi] in the matrix given a fixed membrane potential, pH, and magnesium concentration, and given arbitrary initial conditions. How do the predicted steady-state concentrations depend on membrane potential, pH, and magnesium concentration?

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

```python
#Constants

R = 8.3145        # J / K / mol
T = 310.15        # K
RT = R*T
F = 96485.33      # C / mol
pH_e = 7.2
pH_x = 7.4
H_e = 10**(-pH_e) # M
H_x = 10**(-pH_x) # M
Mg = 1e-3         # M
K = 100e-3        # M
psi = 180 / 1000     # V

# Dissociation Constants

K_HATP = 2.757e-7
K_KATP = 9.809e-2
K_MATP = 8.430e-5
K_HADP = 4.106e-7
K_KADP = 1.319e-1
K_MADP = 7.149e-4
K_HPI  = 2.308e-7
K_KPI  = 3.803e-1
K_MPI  = 2.815e-2

# Binding Polynomials

P_ATPx = 1 + H_x/K_HATP + Mg/K_MATP #P_ATPx = 1 + H_x/K_HATP + K/K_KATP + Mg/K_MATP
P_ADPx = 1 + H_x/K_HADP + Mg/K_MADP #P_ADPx = 1 + H_x/K_HADP + K/K_KADP + Mg/K_MADP
P_PIx = 1 + H_x/K_HPI + Mg/K_MPI    #P_PIx = 1 + H_x/K_HPI + K/K_KPI + Mg/K_MPI

# Species
ATPx = 0.5e-3      # M
ADPx = 9.5e-3      # M
Pix  = 1e-3        # M
```

### F1F0 ATP synthase

```python
def J_F1F0(psi, pH_inner, pH_outer, Mg, concentrations):
    ATPx, ADPx, Pix  = concentrations # define initial conditions

    k_r = 1
    n_H = 8/3
    DGr0 = -4670

    keq_prime_F1F0 = np.exp(-(DGr0 - F * psi * n_H)/(RT))*P_ATPx/(P_ADPx*P_PIx)*H_e**n_H / H_x**(n_H-1)
    J = k_r*(keq_prime_F1F0 * Pix * ADPx - ATPx)

    return J
```

```python
init_conditions = np.array([ATPx, ADPx, Pix])
def dSTATES_dt(STATES, t, psi, pH_inner, pH_outer, Mg):
    rate = J_F1F0(psi, pH_inner, pH_outer, Mg, STATES)
    dATPx = rate
    dADPx = - rate
    dPix = - rate

    dxdt = (dATPx, dADPx, dPix)
    return dxdt

t = np.linspace(0,5)

dSTATES_dt(init_conditions, t, psi, pH_inner, pH_outer, Mg)

ode_results = odeint(dSTATES_dt,init_conditions, t, (psi, pH_inner, pH_outer, Mg)).T
plt.plot(t,ode_results[0] * 1000, label = 'ATPx')
plt.plot(t,ode_results[1] * 1000, label = 'ADPx')
plt.plot(t,ode_results[2] * 1000, label = 'Pix')

plt.xlabel('Time (s)', fontsize=16)
plt.ylabel('Concentation (mM)', fontsize=16)

plt.legend()
ode_results[:,-1][0]/ode_results[:,-1][1]
```

### Mg Analysis

### Psi Analysis

```python
psi_array = np.linspace(120, 220) / 1000 # convert to V
SS_end = []
for psi in psi_array:
    ode_results = odeint(dSTATES_dt,init_conditions, t, (psi, pH_inner, pH_outer, Mg)).T
    SS_end.append(ode_results[:,-1])
SS_end= np.array(SS_end)

plt.figure()
plt.plot(psi_array*1000,SS_end[:,0] * 1000, label = 'ATPx')
plt.plot(psi_array*1000,SS_end[:,1] * 1000, label = 'ADPx')
plt.plot(psi_array*1000,SS_end[:,2] * 1000, label = 'Pix')

plt.xlabel('$\Delta \Psi$ (mV)', fontsize=16)
plt.ylabel('Concentration (mM)', fontsize=16)

plt.legend()
```

### Inner pH


# Translation and scaling invariance in regression models

```python
__author__ = 'Adam Foster and Nick Dingwall'
```

__TL;DR__: In regularized regression models, which matters more: scaling (forcing unit variance) or mean centering? Take our pop quiz and see whether your answer is supported by the maths!

0. [Introduction: a quick pop quiz for the data scientists](#Introduction:-a-quick-pop-quiz-for-the-data-scientists)
0. [Mean-centering has no effect](#Mean-centering-has-no-effect)
0. [Scaling does not affect unregularized regression](#Scaling-does-not-affect-unregularized-regression)
0. [Scaling affects regularized regression](#Scaling-affects-regularized-regression)
0. [Conclusion](#Conclusion)

## Introduction: a quick pop quiz for the data scientists

When you're training a regularized regression model, which preprocessing step on the features is more important: __scaling__ (forcing unit variance) or __mean centering__ (forcing zero mean, a special case of __translating__)?

0. Scaling is always more important.
0. Mean centering is always more important.
0. They're both as important as each other.
0. It depends on the dataset.

### The short answer

When we popped this quiz in the office, 4 was the most popular answer. Perhaps you'll be surprised to learn that the answer is actually 1: mean-centering the features **doesn't make any difference** to model predictions.

### The long answer

If model predictions are what you care about, 1 is the right answer. Mean-centering the features makes no difference as long as you don't penalize the intercept term, whereas scaling does impact predictions in regularized models \cite{the_elements}, pp. 63-64. More generally, regularized regression models show translation invariance: you can add or subtract constants from your features without affecting model predictions. However, they do not show scaling invariance: if you multiply or divide your features by constants, the predictions may change.

If you answered 2, you have a tough argument to make.

If you answered 3, you are half right. 3 is the right answer for **unregularized** regression models because **neither scaling nor mean-centering affects the predictions of unregularized models** \cite{gelmanhill}, p. 53. In more formal language, unregularized regression models are translation *and* scaling invariant.

If you answered 4, maybe you care about interpreting the model coefficients themselves and not just about model predictions. In this case, 4 could be the right answer. The intercept from a model with mean-centered data equals the prediction when all features take their mean value, which could be important for interpretation. On the other hand, we'll show that you can easily recover the intercept you would have found from centered data after you've run your regression with uncentered data.

### Practical benefits of translation invariance

Your first reaction might be, "Well, I'll just mean-center the features anyway, since it doesn't matter and might even help with interpretation." That is likely to be reasonable for datasets of modest size. However, it's disastrous if you were counting on sparse input representations to help manage a massive dataset. Consider all those zeros in your sparse array. When you subtract the mean, they shift to $-\bar{x}$. A matrix that was 1% dense is suddenly nearly 100% dense and your machine has run out of memory.

### Post structure

This difference between scaling and mean-centering is initially unintuitive, so we'll spend a bit of time proving the following two claims:

0. Mean-centering never affects model predictions in regularized or unregularized models.
0. Scaling never affects unregularized models but does affect regularized ones.

We also discuss the effect of rescaling in regularized regression models, showing that, since regularization shrinks model coefficients towards 0, a rescaling which made one feature very small could lose the signal coming from that predictor.

$\newcommand{\VEC}[1]{\boldsymbol{#1}}$
## Mean-centering has no effect

### Formal argument

*Regularized regression* \cite{the_elements}, pp. 61-83, solves the following minimization problem

\begin{equation}
\min_{\mu, \VEC{\beta}}\; \|Y - \mu {\VEC{1}} - X\VEC{\beta} \|_2^2 + \lambda R(\VEC{\beta})
\end{equation}

where $R$ is the penalization function and ${\VEC{1}}$ is a $n\times 1$ vector of 1's. (As usual, $Y$ is a $n\times 1$ response vector, $X$ is a $n\times p$ feature matrix, $\mu$ is a scalar, $\VEC{\beta}$ is a $p\times 1$ vector of regression coefficients and $\lambda > 0$ is the regularization parameter. Equivalent variants using a column of $1$'s instead of a separate intercept are common \cite{the_elements}, p. 45.)

We've separated out the intercept, $\mu$, to emphasize the fact that it is not penalized. Now let's translate the features by subtracting out the mean of each column (we'll denote these by $\bar{x}_1, \bar{x}_2, \ldots$)

\begin{align}
\begin{split}
\text{for } i=1, \ldots, n:& \\
Z_{i1} &= X_{i1} - \bar{x}_1\\
Z_{i2} &= X_{i2} - \bar{x}_2\\
&\text{etc.}
\end{split}
\end{align}

This can be succinctly written

\begin{equation}
Z = X- {\VEC{1}}\bar{\VEC{x}}^\top
\end{equation}

where $\bar{\VEC{x}}$ is a $p\times 1$ vector whose components are the column means $\bar{x}_1, \bar{x}_2, ...$

Now let's examine the objective function for regression with $Z$ to be minimized over some new variables $\nu$ (intercept) and $\VEC{\theta}$ (coefficients)

\begin{align}
\|Y - \nu {\VEC{1}} - Z\VEC{\theta} \|_2^2 + \lambda R(\VEC{\theta})
&=\|Y - \nu {\VEC{1}} - (X- {\VEC{1}}\bar{\VEC{x}}^\top)\VEC{\theta} \|_2^2 + \lambda R(\VEC{\theta}) \\
&=\|Y - (\nu - \bar{\VEC{x}}^\top\VEC{\theta}) {\VEC{1}} - X\VEC{\theta} \|_2^2 + \lambda R(\VEC{\theta})
\end{align}

Now set $\mu = \nu - \bar{\VEC{x}}^\top\VEC{\theta}$ and $\VEC{\beta} = \VEC{\theta}$ and we're back to the original objective with $X$. That means the minimizers must be related simply by $\hat{\mu} = \hat{\nu} - \bar{\VEC{x}}^\top \hat{\VEC{\theta}}$ and $\hat{\VEC{\beta}} = \hat{\VEC{\theta}}$.

*Aside*: The hats differentiate between variables (no hats, can take any value) and minimizers (with hats, take a certain value for fixed $X$ and $Y$).

### Predictions stay the same

What about predictions? Suppose we introduce a new 'test' point $\VEC{x}^\star$ and its corresponding $\VEC{z}$-value, which is $\VEC{z}^\star = \VEC{x}^\star - \bar{\VEC{x}}$. To make predictions with the original (not mean-centered) model we use $\hat{\mu} + \VEC{x}^{\star\top}\hat{\VEC{\beta}}$ and for the centered model, we use $\hat{\nu} + \VEC{z}^{\star\top} \hat{\VEC{\theta}}$. But then

\begin{align}
\hat{\mu} + \VEC{x}^{\star\top} \hat{\VEC{\beta}} &= \left( \hat{\nu} - \bar{\VEC{x}}^\top\hat{\VEC{\theta}} \right) + \VEC{x}^{\star\top} \hat{\VEC{\theta}} \\
&= \hat{\nu} + \left(\VEC{x}^\star - \bar{\VEC{x}}\right)^\top \hat{\VEC{\theta}} \\
&= \hat{\nu} + \VEC{z}^{\star\top} \hat{\VEC{\theta}}
\end{align}

So the predictions from the two models are identical.

The important thing to note is that $\hat{\VEC{\beta}} = \hat{\VEC{\theta}}$ so all the regression coefficients stay the same. Only the intercept needs to change. We defined $Z$ by *mean-centering* the columns. Actually, our proof works for **any translation** that we apply to the columns of $X$. This is what we mean by translation invariance.

Suppose you fit a model without mean-centering (in our notation, that means using $X$). How could you recover the intercept you *would have got* using centered data (in our notation, using $Z$)? It's already there for us. The intercept using centered data was called $\hat{\nu}$ and we know $\hat{\nu} = \hat{\mu} + \bar{\VEC{x}}^\top\hat{\VEC{\theta}} = \hat{\mu} + \bar{\VEC{x}}^\top\hat{\VEC{\beta}}$. So the mean-centered intercept is equivalent to
\begin{equation}
\hat{\nu} = \hat{\mu} + \sum_{i=1}^p \bar{x}_i \hat{\beta}_i
\end{equation}

### An example

That derivation was a bit heavy, so let's work through an example. The code is tucked away in `centering_and_scaling.py`:

```python
from centering_and_scaling import *
```

```python
%matplotlib inline
```

```python
# A dataset:
data = np.random.multivariate_normal(
    mean=[4, 0], cov=[[5, 2], [2, 3]], size=250)
X, y = data[:, 0], data[:, 1]

# Subtract the mean from the features:
empirical_mean = X.mean()
Z = X - empirical_mean

# Before and after comparison:
compare_normalizations(
    X, Z, y, "Before mean-centering", "After mean-centering")
```

Notice that we have a different intercept, but the same slope. The predictions from these two models will be identical. For instance, the prediction at $x = 10$ is around $2.5$ on the upper graph. The corresponding $z$ is $z = 10 - \bar{x} \approx 6$ with precisely the same prediction on the lower graph. The following code works through this in detail:

```python
# The case of `x`:
x_star = 10
lr = LinearRegression()
lr.fit(X.reshape(-1,1), y)
x_prediction = lr.predict(x_star)[0]
print("The prediction at x = {} is {:.4}".format(
        x_star, x_prediction))

# The case of `z`:
z_star = x_star - empirical_mean
lr.fit(Z.reshape(-1,1), y)
z_prediction = lr.predict(z_star)[0]
print("The prediction at z = {} - {:.4} = {:.4} is {:.4}".format(
        x_star, empirical_mean, z_star, z_prediction))
```

    The prediction at x = 10 is 2.362
    The prediction at z = 10 - 3.956 = 6.044 is 2.362

### Ignoring the intercept

It follows from this result that we can ignore $\mu$ in the regression model. Why is this? We know for sure that we can mean-center $X$ without changing the model. If $X$ is mean-centered, then the optimal value of $\mu$ is the mean of $Y$, written $\bar{Y}$. (This requires just a bit of calculus: ${\bf{1}}^{\top}X = 0$ when you've mean-centered $X$.) We might as well mean-center $Y$ as well, leaving $\mu = 0$, so we can ignore it from now on.

## Scaling does not affect unregularized regression

### Formal argument

Let's now look at scaling in the case of unregularized regression.  We've ignored $\mu$ and there is no $R$ either for an unregularized model, so we have the following minimization problem:

\begin{equation}
\min_\VEC{\beta} \; \|Y - X\VEC{\beta} \|_2^2
\end{equation}

Let's rescale the features by dividing by the standard deviation of each column (we'll denote these by $\sigma_1, \sigma_2, \ldots $)

\begin{align}
\begin{split}
\text{for } i=1, \ldots, n:& \\
Z_{i1} &= \frac{X_{i1}}{\sigma_1}\\
Z_{i2} &= \frac{X_{i2}}{\sigma_2}\\
&\text{etc.}
\end{split}
\end{align}

which we can succinctly write as

\begin{equation}
Z = XD
\end{equation}

where $D$ is a diagonal matrix with $D_{ii} = \sigma_i^{-1}$.

The objective function for regression with $Z$ is

\begin{align}
\|Y - Z\VEC{\theta} \|_2^2
= \|Y - XD\VEC{\theta} \|^2_2
\end{align}

Now set $\VEC{\beta} = D\VEC{\theta}$ and we're back to the original objective for $X$. That means the minimizers are related simply by $\hat{\VEC{\beta}} = D\hat{\VEC{\theta}}$. Just like with mean-centering, this result actually applies to any rescaling, not just dividing by the standard deviations.

### Predictions stay the same

What about predictions? Suppose we introduce a test point $\VEC{x}^\star$ and its corresponding $\VEC{z}$-value, which is $\VEC{z}^\star = D^T \VEC{x}^\star $. To make predictions with the original (not unit variance) model we use $\VEC{x}^{\star\top}\hat{\VEC{\beta}}$ and for the unit variance model, we use $\VEC{z}^{\star\top}\hat{\VEC{\theta}}$. But then

\begin{align}
\VEC{x}^{\star\top}\hat{\VEC{\beta}}
&= \VEC{x}^{\star\top}D                    \hat{\VEC{\theta}}   \\
&= \left(D^\top \VEC{x}^\star \right)^\top      \hat{\VEC{\theta}}   \\
&= \VEC{z}^{\star\top}\hat{\VEC{\theta}}
\end{align}

So the predictions from the two models are identical.

### An example

This example is exactly like the previous one, but now we divide by the standard deviation rather than subtracting the mean:

```python
Z = X / X.std()
compare_normalizations(
    X, Z, y,
    title1="Before rescaling",
    title2="After rescaling")
```

These two models certainly _look_ different. It's important to remember that the input to the two models are different, though. For the top model, we input $x = 10$ and get a prediction about $2.5$. For the bottom model we need to input $z = 10/\sigma \approx 4.5$ which then gives a prediction somewhere between $2$ and $3$.

A clearer way to compare these graphs would be:
0. Fit two models: one using $X$ (not unit variance) and one using $Z$ (columns rescaled to unit variance).
0. Plot the $X$ model.
0. Rescale the $Z$ model back so that accepts inputs in $x$-space and plot this.
0. Look to see if the two graphs are the same.

The function `compare_transformed_normalizations` in `centering_and_scaling.py` does just this.

```python
standard_deviation = X.std()
compare_transformed_normalizations(
    X, y,
    transform=(lambda x : x/standard_deviation),
    model=LinearRegression(),
    title1="Trained using original data",
    title2="Trained using rescaled data")
```

It should be completely unsurprising that the lower scatterplot now matches the upper scatterplot. The more notable thing is that the regression line, which was fitted in $z$-space and then transformed, is the same as the line fitted in $x$-space to start with. Thus, we see clearly that scaling doesn't affect the unregularized model.

When we look at regularized regression, these lines will not match.

## Scaling affects regularized regression

### Formal argument

Let's try to redo the proof for regularized regression (that nothing changes) and see where it breaks.
Recall the objective

\begin{equation}
\min_\VEC{\beta} \; \|Y - X\VEC{\beta} \|_2^2 + \lambda R(\VEC{\beta})
\end{equation}
and the rescaling
\begin{equation}
Z = XD
\end{equation}

We'll consider a general $D$ from now on (so need not have $D_{ii} = \sigma_i^{-1}$).

For regression with $Z$ the objective with our new coefficient variable $\VEC{\theta}$ is

\begin{align}
\|Y - Z\VEC{\theta}\|_2^2 + \lambda R(\VEC{\theta})
&= \|Y - XD\VEC{\theta} \|_2^2 + \lambda R(\VEC{\theta})
\end{align}

Now set $\VEC{\beta} = D\VEC{\theta}$

\begin{align}
\|Y - XD\VEC{\theta} \|_2^2 + \lambda R(\VEC{\theta})
&= \|Y - X\VEC{\beta} \|_2^2 + \lambda R(D^{-1}\VEC{\beta}) \\
&\neq \|Y - X\VEC{\beta} \|_2^2 + \lambda R(\VEC{\beta})
\end{align}

Unsurprisingly, the problem is in the regularization term. Instead of regularizing $\VEC{\beta}$ we end up regularizing $D^{-1}\VEC{\beta}$. That means it's unlikely the two problems will have the same minimizers. Although we set $\VEC{\beta} = D\VEC{\theta}$, we can't conclude that $\hat{\VEC{\beta}} = D\hat{\VEC{\theta}}$.

### Quantifying the change

So we know that in regularized regression, the models *might not* be equivalent. Can we be more precise? Let's examine one particular example of regularized regression, the Lasso \cite{tibshirani1996}. For the Lasso we have

\begin{align}
R(\VEC{\beta})&= \sum_{i=1}^p |\beta_i| \\
R(D^{-1}\VEC{\beta}) &= \sum_{i=1}^p \frac{|\beta_i|}{D_{ii}} \label{eq:lasso}
\end{align}

This tells us that we will strongly penalize coefficients which have a small value of $D_{ii}$. That is, if we scale down a certain predictor, the Lasso will more aggressively shrink the corresponding $\beta_i$. Conversely, we will not apply much shrinkage to $\beta_i$ if the $i$th column of $X$ was scaled *up* a lot.

Let's exemplify:

```python
from sklearn.linear_model import Lasso

compare_transformed_normalizations(
    X, y,
    transform=lambda x : x/3,
    model=Lasso(),
    title1="Trained using original data",
    title2="Trained using scaled data")
```

We're seeing exactly what we expected. By making everything three times as small, the model penalized much more strongly, shrinking the regression coefficient exactly to zero.

```python
compare_transformed_normalizations(
    X, y,
    transform=(lambda x : 3*x),
    model=Lasso(),
    title1="Trained using original data",
    title2="Trained using scaled data")
```

Conversely, scaling the data up allows a larger regression coefficient. Without over-complicating matters, in one dimension, the rescaling is equivalent to changing the regularization parameter (look back at equation \ref{eq:lasso} with $p=1$). When we scale the data up a lot sending $D\to \infty$, it's the same as making $\lambda \to 0$, meaning we recover the unregularized linear regression model in the limit.

## Conclusion

You can translate features any which way you want **without changing the model**. With scaling you need to be a little more careful when using a *regularized* model – these models are not scaling invariant. If the scales of predictors vary wildly, models like the Lasso will shrink out the scaled down predictors. To put all predictors on an equal footing, you should be rescaling the columns. Typically this involves forcing the columns to have unit variance, although other options are available \cite{sardy}.

# References

[1] Jerome Friedman, Trevor Hastie and Robert Tibshirani, _The elements of statistical learning_,  2001.

[2] Andrew Gelman and Jennifer Hill, _Data analysis using regression and multilevel/hierarchical models_,  2006.

[3] Tibshirani Robert, _Regression shrinkage and selection via the lasso_, Journal of the Royal Statistical Society. Series B (Methodological), vol. 58, number 1, pp. 267-288,  1996.

[4] Sardy Sylvain, _On the practice of rescaling covariates_, International Statistical Review, vol. 76, number 2, pp. 285-297,  2008.


#   Using Machine Learning in Accelerator Optics

By Kelly Anderson

Image from: https://www.bnl.gov/ps/nsls2/about-nsls-ii.php

---
# Overview

One essential part of accelerator physics is particle tracking. It's used when designing components to make sure the beam travels down the line in the desired manner. There is a lot of software that provides particle tracking for different purposes, some for designing an entire lattice others for inside a specific component. My research looks more at the mathematics that describes how the beam travels. There are complicated components that are necessary to run an accelerator and many introduce nonlinear elements to particle motion. Looking at how these particles develop in phase space can help the design of new accelerators and give information about the equations that describe them.

The position of a particle is described by the translational components perpendicular to the beamline and the longitudinal component along the beam line. An optics function is one way to describe these terms at various points in an accelerator. This will be described in twiss parameters, which are a transformation of variables that is commonly used in accelerator physics. These variables are defined below. The experimental data will come from the various beam position monitors that are placed throughout the accelerator we will look at.

\begin{equation}
    x(s) = A\sqrt{\beta(s)}\cos{(\psi(s) + \delta)}
\end{equation}

\begin{equation}
    \alpha(s) = -\frac{1}{2}\frac{d\beta(s)}{ds}
\end{equation}

\begin{equation}
    \gamma(s) = \frac{1+\alpha(s)^2}{\beta(s)}
\end{equation}

\begin{equation}
    \Delta \psi = \int^{s_2}_{s_1} \frac{ds}{\beta(s)}
\end{equation}

\begin{equation}
    \nu = \frac{1}{2\pi} \oint \frac{ds}{\beta(s)}
\end{equation}

The focus of my research is the analysis of the phase space of the particles in an accelerator. More specifically looking for regions where orbits are stable and their frequencies as well as regions where motion is not stable and finding how quickly it decays. There are many different methods to do this analysis and as a secondary goal, this project hopes to create a basis to use machine learning as a tool in developing these models.

Sources:

S. Y. Lee, Accelerator Physics, 4th Edition

D. A. Edwards, M. J. Syphers, An Introduction to the Physics of High Energy Accelerators

---
# Program Description

The experimental data for the project has already been obtained by my advisor Dr. Yue Hao from the NSLS-II accelerator at Brookhaven National Laboratory. Some existing code that may be used in the project are the particle tracking software Elegant and or Madx. There is also a python library called cpymad that allows one to use Madx through python code and one is being worked on to do the same with Elegant. The NSLS-II is already modelled in Elegant and Madx.

The general purpose of the program is to develop an algorithm that will develop an optics model at various turns and points in the accelerator. Some references that will be used in addition to class material are linked below. Since this information has been studied for this accelerator, it is also the goal to design this program in a modular way that will allow it to accept data from different types of accelerators so it can be applied to them.

Link: https://www.deeplearningbook.org/

Link: http://www.gaussianprocess.org/gpml/chapters/RW.pdf

---
# Project Goals and Timeline

The first goal is to obtain experimental data to use in the accelerator which is discussed in more detail below. The short term goal is to have an optics function at the points where the beam is measured in the NSLS-II. A harder but still obtainable goal is to also find the betatron frequency as a function of the driving amplitude. A long term goal would be to use the techniques to develop an algorithm to analyze the phase space of a particle system, which is more directly applicable to my research.

- 9/11/2020 - Create git repository
- 9/25/2020 - Proposal Due
- 10/09/2020 - Generate Pseudo Data
- 11/01/2020 - Train Algorithm on known system
- 11/06/2020 - Coding Standards and Linting
- 11/20/2020 - Code Review
- 12/04/2020 - Presentation Video Due
- 12/09/2020 - Final Report and Code due.

---
# Anticipating Challenges

The way to get experimental data is by using the data from the beam position monitors that are placed periodically in a lattice. To get a full picture though, one would want more data. One way to generate particle tracking data is by using software. This can be done in software like madx or elegant. The actual experimental data already obtained will be the primary source but pseudo experimental data can be generated by using the model already created for the lattice in this software and adding controlled errors to it.


# 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 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='BV1Bh411o7eV', width=854, height=480, fs=1)
print("Video available at https://www.bilibili.com/video/{0}".format(video.id))
video
```

    Video available at https://www.bilibili.com/video/BV1Bh411o7eV

## 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 = ...
  # Calculate the variance of i
  var_i = ...
  # Calculate the variance of j
  var_j = ...
  # Calculate the correlation coefficient
  rij = ...

  return rij

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

[*Click for solution*](https://github.com/erlichlab/course-content/tree/master//tutorials/W3D1_RealNeurons/solutions/W3D1_Tutorial2_Solution_03e44bdc.py)

*Example output:*

### 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, ..., ..., myseed=2020+i)

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

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

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

  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}')
```

Sample output

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

[*Click for solution*](https://github.com/erlichlab/course-content/tree/master//tutorials/W3D1_RealNeurons/solutions/W3D1_Tutorial2_Solution_c79ab02d.py)

---
# 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.3  # 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=(12, 6))
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=(7, 6))
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()
```

[*Click for solution*](https://github.com/erlichlab/course-content/tree/master//tutorials/W3D1_RealNeurons/solutions/W3D1_Tutorial2_Solution_ecc2167a.py)

---
# 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=(7, 6))
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?

[*Click for solution*](https://github.com/erlichlab/course-content/tree/master//tutorials/W3D1_RealNeurons/solutions/W3D1_Tutorial2_Solution_bb194a8f.py)

## 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)

[*Click for solution*](https://github.com/erlichlab/course-content/tree/master//tutorials/W3D1_RealNeurons/solutions/W3D1_Tutorial2_Solution_4575a736.py)

---
# 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()
```

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 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='BV18K4y1x7Pt', width=854, height=480, fs=1)
print("Video available at https://www.bilibili.com/video/{0}".format(video.id))
video
```

    Video available at https://www.bilibili.com/video/BV18K4y1x7Pt


```python
%load_ext autoreload
%autoreload 2
```

```python
import molsysmt as msm
from molsysmt import puw
import numpy as np
import matplotlib.pyplot as plt
```

# RMSD, Least RMSD and Least RMSD fit

Lets load a small molecular system with a 5000 frames trajectory to show how MolSysMT gets the RMSD, the least RMSD, or the least RMSD fitted structures over a reference frame.

```python
h5_file = msm.demo_systems.files['pentalanine.h5']
molecular_system = msm.convert(h5_file)
```

    /home/diego/MyMachine/locals/miniconda/envs/UIBCDF_lab/lib/python3.7/site-packages/tables/leaf.py:544: VisibleDeprecationWarning: Creating an ndarray from ragged nested sequences (which is a list-or-tuple of lists-or-tuples-or ndarrays with different lengths or shapes) is deprecated. If you meant to do this, you must specify 'dtype=object' when creating the ndarray
      key = numpy.array(key)

```python
msm.info(molecular_system)
```

        form	        n_atoms	        n_groups	        n_components	        n_chains	        n_molecules	        n_entities	        n_peptides	        n_frames

                                molsysmt.MolSys
                        62
                        7
                        1
                        1
                        1
                        1
                        1
                        5000

```python
time = msm.get(molecular_system, target='system', time=True)
```

## RMSD

The root mean squared deviation of a set of atom coordinates $\vec{R}:(\vec{r_{1}}, \vec{r_{2}}, ..., \vec{r_{n}})$, with respect to a reference set of atom coordinates $\vec{R'}:(\vec{r'_{1}}, \vec{r'_{2}}, ..., \vec{r'_{n}})$, is defined as:

\begin{equation}
\text{RMSD} (\vec{R'},\vec{R}) = \sqrt{ \frac{1}{n} \sum_{i=1}^{n} \Vert \vec{r'_{i}} - \vec{r_{i}} \Vert ^{2}}
\end{equation}

\begin{equation}
\text{RMSD} (\vec{R'},\vec{R}) =
\sqrt{ \frac{1}{n} \sum_{i=1}^{n} [(x'-x)^2 + (z'-z)^2 + (z'-z)^2]}
\end{equation}

These two sets of atom coordinates, with the same number of atoms, could belong to structures of two different molecular systems or could also be structures of the same system. Let see the case where the RMSD is calculated only with the backbone atoms of the structure of our molecular system at frame 100-th taking the frame 0-th as reference:

```python
rmsd = msm.rmsd(molecular_system, selection='backbone', frame_indices=100, reference_frame_index=0)
```

There object returned is a numpy array with a single RMSD value:

```python
print(rmsd)
```

    [0.7381704258064243] nanometer

Lets get now the RMSD of every single frame in the trajectory with the same structure as reference, the one found in frame 0-th:

```python
rmsd = msm.rmsd(molecular_system, selection='backbone', frame_indices='all', reference_frame_index=0)
```

```python
plt.plot(time, rmsd, label='RMSD')
plt.xlabel('time [{}]'.format(puw.get_unit(time)))
plt.ylabel('RMSD [{}]'.format(puw.get_unit(rmsd)))
plt.title('RMSD with backbone atoms with respect frame 0-th')
plt.legend()
plt.show()
```

In case of having two different molecular systems, `molsysmt.rmsd()` accepts all necessary input arguments to specify the set of atom coordinates to obtain the rmsd, for a single frame or a set of them: `item`, `selection` and `frame_indices` for the subject molecular system; and `reference_item`, `reference_selection`, and `reference_frame_index` for the reference molecular system.

Lets, for instance, create two different molecular systems (same topology in this case) to illustrate how these input arguments work:

```python
molecular_system_1 = msm.extract(molecular_system, frame_indices=range(0,100))
molecular_system_2 = msm.extract(molecular_system, frame_indices=range(200,300))
```

```python
rmsd = msm.rmsd(molecular_system_1, selection='backbone', frame_indices=80,
                reference_molecular_system=molecular_system_2, reference_selection='backbone',
                reference_frame_index=20)
```

And we got the rmsd between to structures coming from two different frames from two molecular systems:

```python
print(rmsd)
```

    [0.945975307377399] nanometer

## Least RMSD

The RMSD value is not an observable that depends only on the internal geometry of a molecular system. A molecular system can have a frozen geometry but suffering translations or rotations relative to the reference the RMSD varies. Given this frozen geometry and the reference there exist a unique translation and rotation that, combined, minimizes the RMSD. If we want to use the RMSD as a metric to characterize how far two peptide or protein conformations are, the translation and rotation that makes the rmsd minimal needs to be found. Thereby, MolSysMT implements the algorithm proposed by Coutsias, Evangelos A., Chaok Seok, and Ken A. Dill in their work "Using quaternions to calculate RMSD" [[1](#Coutsias2004)] to obtained the least RMSD. The algorithm is implemented behind the method `molsysmt.least_rmsd()` with the same input arguments as `molsysmt.rmsd()`. Let's see the comparison of both methods when the observables are obtained for every frame of a trajectory with respect to the same reference:

```python
lrmsd = msm.least_rmsd(molecular_system, selection='backbone', frame_indices='all', reference_frame_index=0)
rmsd = msm.rmsd(molecular_system, selection='backbone', frame_indices='all', reference_frame_index=0)
```

```python
plt.plot(time, rmsd, color='lightblue', label='RMSD')
plt.plot(time, lrmsd, color='blue', label='LRMSD')
plt.xlabel('time [{}]'.format(puw.get_unit(time)))
plt.ylabel('RMSD [{}]'.format(puw.get_unit(rmsd)))
plt.title('RMSD with backbone atoms with respect frame 0-th')
plt.legend()
plt.show()
```

## Least RMSD fit

The same method to compute the least RMSD [[1](#Coutsias2004)] implemented in `molsysmt.rmsd()` provides with the translational and rotational operators that makes the RMSD minimal. This two operators, applied to the original structure, results with the best fitted structure over the reference one. MolSysMT includes a specific function to execute this least RMSD fitting: `molsysmt.least_rmsd_fit()`. All input arguments of `molsysmt.rmsd()` can be used with `molsysmt.least_rmsd_fit()`. Lets see a couple of examples:

```python
fitted_molecular_system = msm.least_rmsd_fit(molecular_system, selection='backbone', frame_indices='all',
                                             reference_frame_index=0, to_form='molsysmt.MolSys')
```

Take heed to the input argument `to_form`, the above cell results with a new molecular system with the form specified by the former argument. Lets check the RMSD of the new frames over the frame 0-th. We can compare it with the result of `molsysmt.least_rmsd()`:

```python
fitted_rmsd = msm.rmsd(fitted_molecular_system, selection='backbone', frame_indices='all', reference_frame_index=0)
```

```python
plt.plot(time, lrmsd, color='blue', label='Least RMSD')
plt.plot(time, fitted_rmsd, color='red', label='RMSD fitted traj')
plt.xlabel('time [{}]'.format(puw.get_unit(time)))
plt.ylabel('RMSD [{}]'.format(puw.get_unit(rmsd)))
plt.title('RMSD with backbone atoms with respect frame 0-th')
plt.legend()
plt.show()
```

In case of having the input argument `to_form=None`, the original molecular system is modified in place with the fitted frames. Lets, for instance, fit a segment of trajectory without producing a new molecular system:

```python
msm.least_rmsd_fit(molecular_system, selection='backbone', frame_indices=range(2000,4000), reference_frame_index=0)
```

```python
fitted_rmsd = msm.rmsd(molecular_system, selection='backbone', frame_indices='all', reference_frame_index=0)
```

```python
plt.plot(time, lrmsd, color='blue', label='Least RMSD')
plt.plot(time, fitted_rmsd, color='red', label='RMSD fitted traj')
plt.xlabel('time [{}]'.format(puw.get_unit(time)))
plt.ylabel('RMSD [{}]'.format(puw.get_unit(rmsd)))
plt.title('RMSD with backbone atoms with respect frame 0-th')
plt.legend()
plt.show()
```

------------

[1]  [Coutsias, Evangelos A., Chaok Seok, and Ken A. Dill. "Using quaternions to calculate RMSD." Journal of computational chemistry 25, no. 15 (2004): 1849-1857](https://doi.org/10.1002/jcc.20110)


# Variation(변분법)
## functional (범함수)
> ## domain -> functions
>> # rarnge -> functions
> ## variation of calculus
>> ## differential of funcional(변분)
>>> ## cf) differential(미분) of value of function
>>> ## cf) differnce(차분) of indipendent variable

```python
import sympy as sm
list(sm.utilities.iterables.cartes([1,2,3],'ab'))
```

    [(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b'), (3, 'a'), (3, 'b')]

```python
list(sm.utilities.iterables.variations([1,2,3],3,repeat=True))
```

```python
sm.utilities.iterables.flatten([[(1,2)],[(3,4)],[(5,6)]],levels=1)
sm.utilities.iterables.group([1,1,2,2,2,3,1,2,3])
sm.utilities.iterables.group([1,1,1,2,2,2,2,3,3,1,2,3],multiple=False)
sm.utilities.iterables.multiset([1,1,1,2,2,2,2,3,3,1,2,3])
sm.utilities.iterables.has_dups([1,2,3,2])
set()
dict()
sm.Set()
sm.Dict()
```

    {1: 4, 2: 5, 3: 3}

```python
import itertools
list(itertools.permutations(range(3)))
list(itertools.combinations(range(3),2))

list(sm.utilities.iterables.multiset_combinations(range(3),2))
list(sm.utilities.iterables.multiset_permutations(range(3),2))
list(sm.utilities.iterables.multiset_partitions(range(4),2))
list(sm.utilities.iterables.subsets([1,2]))
```

    [(), (1,), (2,), (1, 2)]

```python

```