# Description:

* calculations for modeling fragments in a CsCl gradient under non-equilibrium conditions

# Notes

* Good chapter on determining G+C content from CsCl gradient analysis
http://www.academia.edu/428160/Using_Analytical_Ultracentrifugation_of_DNA_in_CsCl_Gradients_to_Explore_Large-Scale_Properties_of_Genomes

http://www.analyticalultracentrifugation.com/dynamic_density_gradients.htm

Meselson et al. - 1957 - Equilibrium Sedimentation of Macromolecules in Den
Vinograd et al. - 1963 - Band-Centrifugation of Macromolecules and Viruses

http://onlinelibrary.wiley.com.proxy.library.cornell.edu/doi/10.1002/bip.360101011/pdf

## Ultracentrigation book
http://books.google.com/books?hl=en&lr=&id=vxcSBQAAQBAJ&oi=fnd&pg=PA143&dq=Measurement+of+Density+Heterogeneity+by+Sedimentation+in&ots=l8ObYN-zVv&sig=Vcldf9_aqrJ-u7nQ1lBRKbknHps#v=onepage&q&f=false

## Forum info
* http://stackoverflow.com/questions/18624005/how-do-i-perform-a-convolution-in-python-with-a-variable-width-gaussian
* http://timstaley.co.uk/posts/convolving-pdfs-in-python/

## Possible workflows:
### KDE convolution
* KDE of fragment GC values
* bandwidth cross validation: https://jakevdp.github.io/blog/2013/12/01/kernel-density-estimation/
* convolution of KDE with diffusion function:
	* gaussian w/ mean of 0 and scale param = 44.5 (kb) / (mean fragment length)
		* http://www.academia.edu/428160/Using_Analytical_Ultracentrifugation_of_DNA_in_CsCl_Gradients_to_Explore_Large-Scale_Properties_of_Genomes
	* http://nbviewer.ipython.org/github/timstaley/ipython-notebooks/blob/compiled/probabilistic_programming/convolving_distributions_illustration.ipynb

##variable KDE
* variable KDE of fragment GC values where kernel sigma is determined by mean fragment length
	* gaussian w/ scale param = 44.5 (kb) / fragment length

# Standard deviation of homogeneous DNA fragments

Vinograd et al., 1963; (band-centrifugation):

\begin{align}
\sigma^2 = \frac{r_0}{r_0^0} \left\{ \frac{r_0}{r_0^0} + 2D \left( t - t^0 \right) \right\}
\end{align}

## Standard deviation of Gaussian band (assuming equilibrium), Meselson et al., 1957:

\begin{align}
\sigma^2 = -\sqrt{w} \\
w = \textrm{molecular weight}
\end{align}

## Standard deviation of Gaussian band at a given time, Meselson et al., 1957:

\begin{equation}
t^* = \frac{\sigma^2}{D} \left(ln \frac{L}{\sigma} + 1.26 \right), \quad L\gg\sigma \\
\sigma^2 = \textrm{stdev at equilibrium} \\
L = \textrm{length of column}
\end{equation}

* Gaussian within 1% of equillibrium value from center.
* ! assumes density gradient established at t = 0

### Alternative form:

\begin{align}
t = \frac{\beta^{\circ}(p_p - p_m)}{w^4 r_p^2 s} * \left(1.26 + ln \frac{r_b - r_t}{\sigma}\right)
\end{align}

\begin{equation}
t = \textrm{time in seconds} \\
\beta^{\circ} = \beta^{\circ} \textrm{ of salt forming the density gradient (CsCl = ?)} \\
p_p = \textrm{buoyant density of the the particle in the salt} \\
p_m = \textrm{density of the medium (at the given radius?)} \\
w = \textrm{angular velocity} \\
r_p = \textrm{distance (cm) of particle from from the axis of rotation} \\
s = \textrm{sedimentation rate } (S_{20,w} * 10^{-13}) \\
r_b = \textrm{distance to top of gradient} \\
r_t = \textrm{distance to bottom of gradient} \\
r_b - r_t = \textrm{length of gradient (L)}
\end{equation}

### Solving for sigma:

\begin{align}
\sigma = \frac{L}{e^{\left(\frac{w^4 r_p^2 s t}{\beta^{\circ}(p_p - p_m)} - 1.26\right)}}
\end{align}

# Variables specific to the Buckley lab setup

\begin{equation}
\omega = (2\pi \times \textrm{RPM}) /60, \quad \textrm{RPM} = 55000 \\
\beta^{\circ} = 1.14 \times 10^9 \\
r_b = 4.85 \\
r_t = 2.6 \\
L = r_b - r_t \\
s = S_{20,w} * 10^{-13} = 2.8 + 0.00834 * (l*666)^{0.479}, \quad \textrm{where l = length of fragment} \\
p_m = 1.7 \\
p_p = \textrm{buoyant density of the particle in CsCl} \\
r_p = ? \\
t = \textrm{independent variable}
\end{equation}

__isoconcentration point__

\begin{equation}
r_c = \sqrt{(r_t^2 + r_t * r_b + r_b^2)/3}
\end{equation}

__rp in relation to the particle's buoyant density (assuming equilibrium?)__

\begin{equation}
r_p = \sqrt{ ((p_p-p_m)*2*\frac{\beta^{\circ}}{w}) + r_c^2 } \\
p_p = \textrm{buoyant density}
\end{equation}

Maybe this should be drawn from a uniform distribution (particules distributed evenly across the gradient)???

__buoyant density of a DNA fragment in CsCl__

\begin{equation}
p_p = 0.098F + 1.66, \quad \textrm{where F = G+C molar fraction}
\end{equation}

__calculating gradient density at specific radius (to calculate p_m)__

??

__info needed on a DNA fragment to determine it's sigma of the Guassian distribution__

* fragment length
* fragment G+C

# Graphing the equations above

```
%pylab inline
```

    Populating the interactive namespace from numpy and matplotlib

```
import scipy as sp
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import mixture
#import sklearn.mixture as mixture
```

## Generating fragments

```
n_frags = 10000
frag_GC = np.random.normal(0.5,0.1,n_frags)
frag_GC[frag_GC < 0] = 0
frag_GC[frag_GC > 1] = 1
frag_len = np.random.normal(10000,1000,n_frags)
```

```
ret = plt.hist2d(frag_GC, frag_len, bins=100)
```

## Setting variables

```
RPM = 55000
omega = (2 * np.pi * RPM) / 60

beta_o = 1.14 * 10**9

radius_bottom = 4.85
radius_top = 2.6
col_len = radius_bottom - radius_top

density_medium = 1.7
```

## Calculation functions

```
# BD from GC
frag_BD = 0.098 * frag_GC + 1.66

ret = plt.hist(frag_BD, bins=100)
```

```
sedimentation = (frag_len*666)**0.479 * 0.00834 + 2.8   # l = length of fragment

ret = plt.hist(sedimentation, bins=100)
```

```
# sedimentation as a function of fragment length
len_range = np.arange(1,10000, 100)

ret = plt.scatter(len_range, 2.8 + 0.00834 * (len_range*666)**0.479 )
```

```
# isoconcentration point
iso_point = sqrt((radius_top**2 + radius_top * radius_bottom + radius_bottom**2)/3)
iso_point
```

    3.7812035121109258

```
# radius of particle

#radius_particle = np.sqrt( (frag_BD - density_medium)*2*(beta_o/omega) + iso_point**2 )

#ret = plt.hist(radius_particle)
```

    array([ 62.83472857,  65.24947889,  71.07927139,  39.2627203 ,
            83.56106181,  60.12239077,  73.64807884,          nan,  91.27392451])

```

```

# Testing out speed of mixture models

```
n_dists = 10
n_samp = 10000
```

```
def make_mm(n_dists):
    dist_loc = np.random.uniform(0,1,n_dists)
    dist_scale = np.random.uniform(0,0.1, n_dists)
    dists = [mixture.NormalDistribution(x,y) for x,y in zip(dist_loc, dist_scale)]
    eq_weights = np.array([1.0 / n_dists] * n_dists)
    eq_weights[0] += 1.0 - np.sum(eq_weights)
    return mixture.MixtureModel(n_dists, eq_weights, dists)
```

```
mm = make_mm(n_dists)
```

```
%%timeit
smp = mm.sampleDataSet(n_samp).getInternalFeature(0).flatten()
```

    10 loops, best of 3: 98.4 ms per loop

```
%%timeit
smp = np.array([mm.sample() for i in arange(n_samp)])
```

    10 loops, best of 3: 65.4 ms per loop

```
n_dists = 1000
mm = make_mm(n_dists)
```

```
%%timeit
smp = mm.sampleDataSet(n_samp).getInternalFeature(0).flatten()
```

    1 loops, best of 3: 1.7 s per loop

```
%%timeit
smp = np.array([mm.sample() for i in arange(n_samp)])
```

    1 loops, best of 3: 1.64 s per loop

```
n_dists = 10000
mm = make_mm(n_dists)
```

```
%%timeit
smp = mm.sampleDataSet(n_samp).getInternalFeature(0).flatten()
```

    1 loops, best of 3: 17 s per loop

```
%%timeit
smp = np.array([mm.sample() for i in arange(n_samp)])
```

    1 loops, best of 3: 16.7 s per loop

```
n_samp = 100000
```

```
%%timeit
smp = mm.sampleDataSet(n_samp).getInternalFeature(0).flatten()
```

    1 loops, best of 3: 2min 51s per loop

```
%%timeit
smp = np.array([mm.sample() for i in arange(n_samp)])
```

__Notes:__

* a mixture model with many distributions (>1000) is very slow for sampling

```
x = np.random.normal(3, 1, 100)
y = np.random.normal(1, 1, 100)
H, xedges, yedges = np.histogram2d(y, x, bins=100)
```

```
H
```

***
***

# Workflow for modeling DNA fragment locations in a gradient

For each genome in mock community, simulate N fragments and calculate their Guassian distributions in the gradient.
Create a mixture model of those Guassian distributions to sample Aa fragments,
where Aa = the absolute abundance of the taxon in the mock community.
One mixture model per genome.

## User defined:

* Rotor specs
* cfg parameters (RPM, time)

## Generate fragment density distributions

* For each genome in the mock community:
  * Simulate fragments
  * Calculate sigma of Gaussian density distribution
  * Create mixture model from all Gaussians of the fragments

## Simulate fraction communities

* For each genome in mock community:
  * sample fragments from mixture model based on total abundance of taxon in mock community
  * bin fragments into gradient fractions

```

```


# Lecture 9: Expectation, Indicator Random Variables, Linearity

## Stat 110, Prof. Joe Blitzstein, Harvard University

----

## More on Cumulative Distribution Functions

A CDF: $F(x) = P(X \le x)$, as a function of real $x$ has to be

* non-negative
* add up to 1

In the following discrete case, it is easy to see how the probability mass function (PMF) relates to the CDF:

Therefore, you can compute any probability given a CDF.

_Ex. Find $P(1 \lt x \le 3)$ using CDF $F$._

\begin{align}
  & &P(x \le 1) + P(1 \lt x \le 3) &= P(x \le 3) \\
  & &\Rightarrow P(1 \lt x \le 3) &= F(3) - F(1)
\end{align}

Note that while we don't need to be so strict in the __continuous case__, for the discrete case you need to be careful about the $\lt$ and $\le$.

### Properties of CDF

A function $F$ is a CDF __iff__ the following three conditions are satisfied.
1. increasing
1. right-continuous (function is continuous as you _approach a point from the right_)
1. $F(x) \rightarrow 0 \text{ as } x \rightarrow - \infty$, and $F(x) \rightarrow 1 \text{ as } x \rightarrow \infty$.

----

## Independence of Random Variables

$X, Y$ are independent r.v. if

\begin{align}
  \underbrace{P(X \le x, Y \le y)}_{\text{joint CDF}} &= P(X \le x) P(Y \le y) & &\text{ for all x, y in the continuous case} \\
  \\
  \underbrace{P(X=x, Y=y)}_{\text{joint PMF}} &= P(X=x) P(Y=y) & &\text{ for all x, y in the discrete case}
\end{align}

----

## Averages of Random Variables (mean, Expected Value)

A mean is... well, the _average of a sequence of values_.

\begin{align}
  1, 2, 3, 4, 5, 6 \rightarrow \frac{1+2+3+4+5+6}{6} = 3.5
\end{align}

In the case where there is repetition in the sequence

\begin{align}
  1,1,1,1,1,3,3,5 \rightarrow & \frac{1+1+1+1+1+3+3+5}{8} \\
  \\
  & \dots \text{ or } \dots \\
  \\
  & \frac{5}{8} ~~ 1 + \frac{2}{8} ~~ 3 + \frac{1}{8} ~~ 5 & &\quad \text{ ... weighted average}
\end{align}

where the weights are the frequency (fraction) of the unique elements in the sequence, and these weights add up to 1.

### Expected value of a discrete r.v. $X$

\begin{align}
  \mathbb{E}(X) = \sum_{x} \underbrace{x}_{\text{value}} ~~ \underbrace{P(X=x)}_{\text{PMF}} ~& &\quad \text{ ... summed over x with  } P(X=x) \gt 0
\end{align}

### Expected value of  $X \sim \operatorname{Bern}(p)$

\begin{align}
  \text{Let } X &\sim \operatorname{Bern}(p) \\
  \mathbb{E}(X) &= \sum_{k=0}^{1} k P(X=k) \\
  &= 1 ~~ P(X=1) + 0 ~~ P(X=0) \\
  &= p
\end{align}

### Expected value of an Indicator Variable

\begin{align}
  X &=
  \begin{cases}
    1, &\text{ if A occurs} \\
    0, &\text{ otherwise }
  \end{cases} \\
  \\
  \therefore \mathbb{E}(X) &= P(A)
\end{align}

Notice how this lets us relate (bridge) the expected value $\mathbb{E}(X)$ with a probability $P(A)$.

#### Average of $X \sim \operatorname{Bin}(n,p)$

There is a hard way to do this, and an easy way.

First the hard way:

\begin{align}
  \mathbb{E}(X) &= \sum_{k=0}^{n} k \binom{n}{k} p^k (1-p)^{n-k} \\
    &= \sum_{k=0}^{n} n \binom{n-1}{k-1} p^k (1-p)^{n-k} & &\text{from Lecture 2, Story proofs, ex. 2, choosing a team and president} \\
    &= np \sum_{k=0}^{n} n \binom{n-1}{k-1} p^{k-1} (1-p)^{n-k} \\
    &= np \sum_{j=0}^{n-1} \binom{n-1}{j} p^j(1-p)^{n-1-j} & &\text{letting } j=k-1 \text{, which sets us up to use the Binomial Theorem} \\
    &= np
\end{align}

Now, what about the _easy way_?

----

## Linearity of Expected Values

Linearity is this:

\begin{align}
  \mathbb{E}(X+Y) &= \mathbb{E}(X) + \mathbb{E}(Y) & &\quad \text{even if X and Y are dependent}\\
  \\
  \mathbb{E}(cX) &= c \mathbb{E}(X)\\
\end{align}

### Expected value of Binomial r.v using Linearity

Let $X \sim \operatorname{Bin}(n,p)$. The easy way to calculate the expected value of a binomial r.v. follows.

Let $X = X_1 + X_2 + \dots + X_n$ where $X_j \sim \operatorname{Bern}(P)$.

\begin{align}
  \mathbb{E}(X) &= \mathbb{E}(X_1 + X_2 + \dots + X_n) \\
  \mathbb{E}(X) &= \mathbb{E}(X_1) + \mathbb{E}(X_2) + \dots + \mathbb{E}(X_n) & &\quad \text{by Linearity}\\
  \mathbb{E}(X) &= n \mathbb{E}(X_1) & &\quad \text{by symmetry}\\
  \mathbb{E}(X) &= np
\end{align}

### Expected value of  Hypergeometric r.v.

Ex. 5-card hand $X=(\# aces)$. Let $X_j$ be the indicator that the $j^{th}$ card is an ace.

\begin{align}
  \mathbb{E}(X) &= \mathbb{E}(X_1 + X_2 + X_3 + X_4 + X_5) \\
  &= \mathbb{E}(X_1) + \mathbb{E}(X_2) + \mathbb{E}(X_3) + \mathbb{E}(X_4) + \mathbb{E}(X_5) & &\quad \text{by Linearity} \\
  &= 5 ~~ \mathbb{E}(X_1) & &\quad \text{by symmetry} \\
  &= 5 ~~ P(1^{st} \text{ card is ace}) & &\quad \text{by the Fundamental Bridge}\\
  &= \boxed{\frac{5}{13}}
\end{align}

Note that when we use linearity in this case, the individual probabilities are _weakly dependent_, in that the probability of getting an ace decreases slightly; and that if you already have four aces, then the fifth card cannot possibly be an ace. But using linearity, we can nevertheless quickly and easily compute $\mathbb{E}(X_1 + X_2 + X_3 + X_4 + X_5)$.

----

## Geometric Distribution

### Description

The Geometric distribution comprises a series of independent $\operatorname{Bern}(p)$ trials where we count the number of failures before the first success.

### Notation

$X \sim \operatorname{Geom}(p)$.

### Parameters

$0 < p < 1 \text{, } p \in \mathbb{R}$

```python
import matplotlib
import numpy as np
import matplotlib.pyplot as plt

from matplotlib.ticker import (MultipleLocator, FormatStrFormatter,
                               AutoMinorLocator)
from scipy.stats import geom

%matplotlib inline

plt.xkcd()
_, ax = plt.subplots(figsize=(12,8))

# seme Geometric parameters
p_values = [0.2, 0.5, 0.75]

# colorblind-safe, qualitative color scheme
colors = ['#1b9e77', '#d95f02', '#7570b3']

for i,p in enumerate(p_values):
    x = np.arange(geom.ppf(0.01, p), geom.ppf(0.99, p))
    pmf = geom.pmf(x, p)
    ax.plot(x, pmf, 'o', color=colors[i], ms=8, label='p={}'.format(p))
    ax.vlines(x, 0, pmf, lw=2, color=colors[i], alpha=0.3)

# legend styling
legend = ax.legend()
for label in legend.get_texts():
    label.set_fontsize('large')
for label in legend.get_lines():
    label.set_linewidth(1.5)

# y-axis
ax.set_ylim([0.0, 0.9])
ax.set_ylabel(r'$P(X=k)$')

# x-axis
ax.set_xlim([0, 20])
ax.set_xlabel('# of failures k before first success')

# x-axis tick formatting
majorLocator = MultipleLocator(5)
majorFormatter = FormatStrFormatter('%d')
minorLocator = MultipleLocator(1)
ax.xaxis.set_major_locator(majorLocator)
ax.xaxis.set_major_formatter(majorFormatter)
ax.xaxis.set_minor_locator(minorLocator)

ax.grid(color='grey', linestyle='-', linewidth=0.3)

plt.suptitle(r'Geometric PMF: $P(X=k) = pq^k$')

plt.show()
```

### Probability mass function

Consider the event $A$ where there are 5 failures before the first success. We could notate this event $A$ as $\text{FFFFFS}$, where $F$ denotes failure and $S$ denotes the first success. Note that this string **must** end with a success. So, $P(A) = q^5p$.

And from just this, we can derive the PMF for a geometric r.v.

\begin{align}
  P(X=k) &= pq^k \text{, } k \in \{1,2, \dots \}
  \\
  \\
  \sum_{k=0}^{\infty} p q^k &= p \sum_{k=0}^{\infty} q^k \\
  &= p ~~ \frac{1}{1-q} & &\quad \text{by the geometric series where } |r| < 1 \\
  &= \frac{p}{p} \\
  &= 1 & &\quad \therefore \text{ this is a valid PMF}
\end{align}

### Expected value

So, the hard way to calculate the expected value $\mathbb{E}(X)$ of a $\operatorname{Geom}(p)$ is

\begin{align}
  \mathbb{E}(X) &= \sum_{k=0}^{\infty} k p q^k \\
  &= p \sum_{k=0}^{\infty} k q^k \\
  \\
  \\
  \text{ now ... } \sum_{k=0}^{\infty} q^k &=  \frac{1}{1-q} & &\quad \text{by the geometric series where |q| < 1} \\
  \sum_{k=0}^{\infty} k q^{k-1} &= \frac{1}{(1-q)^2} & &\quad \text{by differentiating with respect to k} \\
  \sum_{k=0}^{\infty} k q^{k} &= \frac{q}{(1-q)^2} \\
  &= \frac{q}{p^2} \\
  \\
  \\
  \text{ and returning, we have ... } \mathbb{E}(X) &= p ~~ \frac{q}{(p^2} \\
  &= \frac{q}{p} & &\quad \blacksquare
\end{align}

And here is the story proof, without using the geometric series and derivatives:

Again, we are considering a series of independent Bernoulli trials with probability of success $p$, and we are counting the number of failures before getting the first success.

Similar to doing a first step analysis in the case of the Gambler's Ruin, we look at the first case where we either:

* get a heads (success) on the very first try, meaning 0 failures
* or we get 1 failure, but we start the process all over again

Remember that in the case of a coin flip, the coin has no memory.

Let $c=\mathbb{E}(X)$.

\begin{align}
  c &= 0 ~~ p + (1 + c) ~~ q \\
    &= q + qc \\
  \\
  c - cq &= q \\
  c (1 - q) &= q \\
  c &= \frac{q}{1-q} \\
    &= \frac{q}{p} & &\quad \blacksquare
\end{align}

----

View [Lecture 9: Expectation, Indicator Random Variables, Linearity | Statistics 110](http://bit.ly/2nPbrNe) on YouTube.


# 01 Molecular Geometry Analysis

The purpose of this project is to introduce you to fundamental Python programming techniques in the context of a scientific problem, viz. the calculation of the internal coordinates (bond lengths, bond angles, dihedral angles), moments of inertia, and rotational constants of a polyatomic molecule. A concise set of instructions for this project may be found [here](https://github.com/CrawfordGroup/ProgrammingProjects/blob/master/Project%2301/project1-instructions.pdf).

Original authors (Crawford, et al.) thank Dr. Yukio Yamaguchi of the University of Georgia for the original version of this project.

Solution of this project could be {download}`solution_01.py`.

```python
# Following os.chdir code is only for thebe (live code), since only in thebe default directory is /home/jovyan
import os
if os.getcwd().split("/")[-1] != "Project_01":
    os.chdir("source/Project_01")
from solution_01 import Molecule as SolMol
```

## Step 1: Read the Coordinate Data from Input

The input to the program is the set of Cartesian coordinates of the atoms (**in bohr**) and their associated atomic numbers. A sample molecule (acetaldehyde) to use as input to the program is:

    7
    6  0.000000000000     0.000000000000     0.000000000000
    6  0.000000000000     0.000000000000     2.845112131228
    8  1.899115961744     0.000000000000     4.139062527233
    1 -1.894048308506     0.000000000000     3.747688672216
    1  1.942500819960     0.000000000000    -0.701145981971
    1 -1.007295466862    -1.669971842687    -0.705916966833
    1 -1.007295466862     1.669971842687    -0.705916966833

The first line above is the number of atoms (an integer), while the remaining lines contain the z-values (atom charges) and x-, y-, and z-coordinates of each atom (one integer followed by three double-precision floating-point numbers). This input file({download}`input/acetaldehyde.dat`) along with a few other test cases can be found in this repository in the input directory.

After downloading the file to your computer (to a file called `geom.dat`, for example), you must open the file, read the data from each line into appropriate variables, and finally close the file.

````{admonition} Hint 1: Opening and closing the file
:class: dropdown

In Python, using `open` and `close` can easily handle file I/Os:

```python
>>> f = open("input/acetaldehyde.dat", "r")
>>> # handle file `f`
>>> f.close()
```

However, there's more elegent way to do this by `with`:

```python
>>> with open("input/acetaldehyde.dat", "r") as f:
>>>     pass  # handle file `f`
```

The latter code is prefered. Using `with` would automatically close file, avoiding possibilities that programmer forget inserting a line to close file.

````

````{admonition} Hint 2: Reading the numbers
:class: dropdown

Handling multiple words (decimals, floats as strings) is relatively easy in Python. For example,

```python
>>> "6  0.000000000000     0.000000000000     0.000000000000".split()
['6', '0.000000000000', '0.000000000000', '0.000000000000']
```

However, these are string types. One need to transform to proper types to enable calculation afterwards.

````

````{admonition} Hint 3: Storing the z-values (atom charges) and the coordinates by NumPy
:class: dropdown

There are two ways in python.

An intutive way to do this is generating a list to store z-values (as array), and list of list to store coordinates (as matrix). Notice do not use string type, for the sake of convenience of calculation tasks afterwards. It is also the most used way to store array/matrix in C++ and fortran90+. In FORTRAN77, one must use array to store matrix, along with it's leading dimension.

Another way is to use numpy. For example,

```python
>>> arr = np.array([
... ["0.0", "0.1"],
... ["2.5", "4.6"],
... ], dtype=float)
>>> arr
array([[0. , 0.1],
       [2.5, 4.6]])
```

Numpy can be extremely powerful, not only it wraps many array/matrix properties (raw data, dimension, type, etc.) into one class (`numpy.ndarray`), but also offers powerful operations in very concise way (matrix multiplication, SVD, tensor contraction, etc.).

Learning some essentials of NumPy can prove to be useful. In this document, NumPy will be the default array/matrix/tensor storage structure. Other robust array/matrix/tensor engines could be PyTorch/TensorFlow, etc.

````

````{admonition} Hint 4: Wrapping molecule information into class
:class: dropdown

What defines a molecule is somehow complicated. Not only it's atom charges, but also its coordinates. We can derive bond lengths, angles, etc. only from molecule information. We may term atom charges and coordinates as *Attribiutes*; they are variables and information describing molecule, like adjectives in gramma. We may term how we generate bond lengths, angles, etc. as *Methods*; they are functions, mapping from molecule information to some properties we desire, like verbs in gramma.

An example to construct molecule class may be

```python
class Molecule:

    def __init__(self):
        self.atom_charges = NotImplemented  # type: np.ndarray
        self.atom_coords = NotImplemented  # type: np.ndarray
        self.natm = NotImplemented  # type: int

    def construct_from_dat_file(self, file_path: str):
        # Input: Read file from `file_path`
        # Attribute modification: Obtain atom charges to `self.atom_charges`
        # Attribute modification: Obtain coordinates to `self.atom_coords`
        # Attribute modification: Obtain atom numbers in molecule to `self.natm`
        raise NotImplementedError("Reader need to fill this method function!")
```

For convenience, redundant variable `self.natm` (number of atoms in molecule) is also included as *Attribute*. It can be easily obtained by counting length of atom charges array `self.atom_charges`.

````

### Implementation

- If `mole` is an instance of `Molecule` class, and it's acetaldehyde molecule, then

    - `mole.atom_charges` should be a NumPy array with shape (7, )

    - `mole.atom_coords` should be a NumPy array with shape (7, 3) (we use *array* instead of matrix, just convention; like any array/matrix in TensorFlow/PyTorch is termed *tensor*)

    - `mole.natm` should be an integer

Reader should fill all `NotImplementedError` in the following code (notice that `NotImplemented` is coded intentionally, not the same to `NotImplementedError`):

```python
import numpy as np

class Molecule:

    def __init__(self):
        self.atom_charges = NotImplemented  # type: np.ndarray
        self.atom_coords = NotImplemented  # type: np.ndarray
        self.natm = NotImplemented  # type: int

    def construct_from_dat_file(self, file_path: str):
        # Input: Read file from `file_path`
        # Attribute modification: Obtain atom charges to `self.atom_charges`
        # Attribute modification: Obtain coordinates to `self.atom_coords`
        # Attribute modification: Obtain atom numbers in molecule to `self.natm`
        raise NotImplementedError("About 5~15 lines of code")
```

### Solution

Following is reference output. Reader may repeat the output results with his/hers own implementation.

```python
sol_mole = SolMol()
sol_mole.construct_from_dat_file("input/acetaldehyde.dat")
print(sol_mole.atom_charges)
print(sol_mole.atom_coords)
print(sol_mole.natm)
```

    [6 6 8 1 1 1 1]
    [[ 0.          0.          0.        ]
     [ 0.          0.          2.84511213]
     [ 1.89911596  0.          4.13906253]
     [-1.89404831  0.          3.74768867]
     [ 1.94250082  0.         -0.70114598]
     [-1.00729547 -1.66997184 -0.70591697]
     [-1.00729547  1.66997184 -0.70591697]]
    7

## Step 2: Bond Lengths

Calculate the interatomic distances using the expression:

$$
R_{ij} = \sqrt{(x_i - x_j)^2 + (y_i - y_j)^2 + (z_i - z_j)^2}
$$

where $x$, $y$, and $z$ are Cartesian coordinates and $i$ and $j$ denote atomic indices.

Print all bond lengths $R_{ij}$ in Bohr.

````{admonition} Hint 1: Bond length method function signiture
:class: dropdown

Before writing program, we need to ask ourselfs, that what information do we need to definitely define bond length. Actually, we need the two atoms ($i, j$ as subscripts of $R_{ij}$), and molecule coordinates ($x_i, y_i, z_i$). So we define the program as

```python
def bond_length(mole: Molecule, i: int, j: int) -> float:
    # Input: `i`, `j` index of molecule's atom
    # Output: Bond length from atom `i` to atom `j`
    raise NotImplementedError("Reader need to fill this method function!")
```

````

````{admonition} Hint 2: $L_2$ norm
:class: dropdown

Directly programming by the expression of $R_{ij}$ is very intutive. However, this expression can also be written as

$$
R_{ij} = |\boldsymbol{r}_i - \boldsymbol{r}_j | := \Vert \boldsymbol{r}_i - \boldsymbol{r}_j \Vert_2
$$

where $\boldsymbol{r}_i = (x_i, y_i, z_i)$. $R_{ij}$ could be also termed as $L_2$ (Frobenius) norm of vector $\boldsymbol{r}_i - \boldsymbol{r}_j$, and $L_2$ norm of vector is implemented by NumPy (`numpy.linalg.norm`). See [NumPy API](https://numpy.org/doc/stable/reference/generated/numpy.linalg.norm.html) for its usage.

Using `numpy.linalg.norm` is preferred, but not compulsory at this stage.

If reader is not ready to use NumPy, python's standard library `math` is prefered to make sqrt calculation. For example, norm of vector $(0, 3, 4)$ could be calculated as

```python
>>> import math
>>> math.sqrt(0**2 + 3**2 + 4**2)
5.0
```

````

````{admonition} Hint 3: Pretty printing
:class: dropdown

We may use python's default printing utilities for debugging:

```python
>>> print(0, 1, 2.845112131228)
0 1 2.845112131228
```

However, if we want to format printing string, we may use `str.format` method function. Reader may search `python string format` in search engines for more details. For example, if we want to print atom indexes in 3-char-long decimals and bond length in 10-char-long-5-after-decimal-point, we may write program as

```python
>>> print("{:3d} - {:3d}: {:10.5f} Bohr".format(0, 1, 2.845112131228))
  0 -   1:    2.84511 Bohr
```

You may wish to implement another function `print_bond_length` to realize this feature.

````

````{admonition} Hint 4: Loop structure
:class: dropdown

We need to print all bond length information in function `print_bond_length`, as Step 2 Hint 3 stated. Note that bond between atom 1 - atom 2 is the same to atom 2 - atom 1. So, reader should notice the range for each loop. In the following code, atom index `j` is always larger than `i`:

```python
for i in range(mole.natm):
    for j in range(i + 1, mole.natm):
        raise NotImplementedError  # Reader should complete printing code
```

````

````{admonition} Hint 5: Extending molecule class
:class: dropdown

We have discussed how to construct `Molecule` class in Step 1 Hint 4. However, we could regard `bond_length` function defined in Step 2 Hint 1 as method function of `Molecule` class. Though this is somehow not "pythonic", one can add method function (or attribute) after class construction, in fashion like C++. But we note that python is more flexable than C++ when handling methods in class, since in C++, although method implementation is not required when constructing class, one still need to declare signiture of function. In python, there's no need declaring function, so we just do implementations.

In this case, if we have defined function `bond_length` in Step 2 Hint 1, we just make the following statement to add this function to `Molecule` class:

```python
Molecule.bond_length = bond_length
```

We note that when calling `bond_length`, we need use

```python
bond_length(mole, i, j)
```

but when calling `Molecule.bond_length`, we need use

```python
mole.bond_length(i, j)
```

The first parameter `mole: Molecule` in `Molecule.bond_length`, like other (non-static) method functions, is equivalent to `self`.

````

### Implementation

Reader should fill all `NotImplementedError` in the following code:

```python
def bond_length(mole: Molecule, i: int, j: int) -> float:
    # Input: `i`, `j` index of molecule's atom
    # Output: Bond length from atom `i` to atom `j`
    raise NotImplementedError("About 1~3 lines of code")

Molecule.bond_length = bond_length
```

```python
def print_bond_length(mole: Molecule):
    # Usage: Print all bond length
    print("=== Bond Length ===")
    for i in range(mole.natm):
        for j in range(i + 1, mole.natm):
            raise NotImplementedError("Exactly 1 line of code")

Molecule.print_bond_length = print_bond_length
```

### Solution

```python
sol_mole.print_bond_length()
```

    === Bond Length ===
      0 -   1:    2.84511 Bohr
      0 -   2:    4.55395 Bohr
      0 -   3:    4.19912 Bohr
      0 -   4:    2.06517 Bohr
      0 -   5:    2.07407 Bohr
      0 -   6:    2.07407 Bohr
      1 -   2:    2.29803 Bohr
      1 -   3:    2.09811 Bohr
      1 -   4:    4.04342 Bohr
      1 -   5:    4.05133 Bohr
      1 -   6:    4.05133 Bohr
      2 -   3:    3.81330 Bohr
      2 -   4:    4.84040 Bohr
      2 -   5:    5.89151 Bohr
      2 -   6:    5.89151 Bohr
      3 -   4:    5.87463 Bohr
      3 -   5:    4.83836 Bohr
      3 -   6:    4.83836 Bohr
      4 -   5:    3.38971 Bohr
      4 -   6:    3.38971 Bohr
      5 -   6:    3.33994 Bohr

## Step 3: Bond Angles

Calculate bond angles. For example, the angle, $\phi_{ijk}$, between atoms $i$-$j$-$k$, where $j$ is the central atom is given by:

$$
\cos \phi_{ijk} = \boldsymbol{e}_{ji} \cdot \boldsymbol{e}_{jk}
$$

where the $\boldsymbol{e}_{ij}$ are unit vectors between the atoms, e.g.,

$$
e_{ij}^x = - \frac{x_i - x_j}{R_{ij}}, \quad e_{ij}^y = - \frac{y_i - y_j}{R_{ij}}, \quad e_{ij}^z = - \frac{z_i - z_j}{R_{ij}}
$$

Print all valid bond angles $\phi_{ijk}$ (with bond length $R_{ij} < 3 \, \mathsf{Bohr}$ and $R_{jk} < 3 \, \mathsf{Bohr}$, and $\phi_{ijk} > 90^\circ$) in degree.

Original C++ project suggests storing unit vectors in memory. However, this python project will generate unit vectors on-the-fly. Also, the final result of this step is a little different to the original project.

````{admonition} Hint 1: Unit vector generation
:class: dropdown

From the definition above, we may state that

$$
\boldsymbol{e}_{ij} = \frac{\boldsymbol{r}_j - \boldsymbol{r}_i}{| \boldsymbol{r}_j - \boldsymbol{r}_i |}
$$

It is a combined opeation of vector substraction, $L_2$ norm, and vector divided by numerical value. In NumPy, they are quite easy to achieve.

````

````{admonition} Hint 2: Vector inner product
:class: dropdown

We used inner product between $\boldsymbol{e}_{ji}$ and $\boldsymbol{e}_{jk}$ in the fomular above:

$$
\cos \phi_{ijk} = \boldsymbol{e}_{ji} \cdot \boldsymbol{e}_{jk} = e_{ji}^x e_{jk}^x + e_{ji}^y e_{jk}^y + e_{ji}^z e_{jk}^z
$$

In NumPy, there are multiple ways to achieve vector inner product. An intutive way is using `numpy.inner` (see [NumPy API](https://numpy.org/doc/stable/reference/generated/numpy.inner.html)). For example,

```python
>>> np.inner([-1, 1, 2], [3, 9, 5])
16
```

Other ways could be `numpy.dot`, `numpy.tensordot`, `numpy.einsum`, `np.multiply` with `np.sum`.

````

````{admonition} Hint 3: Inverse trigonometric functions
:class: dropdown

We need calculate arccos value of vector inner product. In NumPy, `numpy.arccos` can do this job. Without NumPy, there's still `math.acos`.

Note that we need to print angle in degree, so we need to multiply result by $180 / \pi$.

````

````{admonition} Hint 4: Loop structure
:class: dropdown

From the description, we should know that bond angle 1-2-3 is the same to 3-2-1. However, we regard 1-2-3 as different bond angle to 2-1-3. If bond length of 1-2 is larger than 4 Angstrom, then we silent all bond angles 1-2-\*; but if bond length of 1-3 is larger than 3 Angstrom, as well as 1-2, 2-3 shorter than 3 Angstrom, we still print bond angle 1-2-3.

Utilizing that the sum of interior angles of a triangle is 180°, we know that for any three atoms, they have utmost one angle larger than 90°.

````

### Implementation

Reader should fill all `NotImplementedError` in the following code:

```python
def bond_unit_vector(mole: Molecule, i: int, j: int) -> np.ndarray:
    # Input: `i`, `j` index of molecule's atom
    # Output: Unit vector of bond from atom `i` to atom `j`
    raise NotImplementedError("About 1~5 lines of code")

Molecule.bond_unit_vector = bond_unit_vector
```

```python
def bond_angle(mole: Molecule, i: int, j: int, k: int) -> float:
    # Input: `i`, `j`, `k` index of molecule's atom; where `j` is the central atom
    # Output: Bond angle for atoms `i`-`j`-`k`
    raise NotImplementedError("About 3~5 lines of code")

Molecule.bond_angle = bond_angle
```

```python
def is_valid_angle(mole: Molecule, i: int, j: int, k: int) -> bool:
    # Input: `i`, `j`, `k` index of molecule's atom; where `j` is the central atom
    # Output: Test if `i`-`j`-`k` is a valid bond angle
    #         if i != j != k
    #         and if i-j and j-k bond length smaller than 3 Angstrom,
    #         and if angle i-j-k > 90 degree
    raise NotImplementedError("About 1~5 lines of code")

Molecule.is_valid_angle = is_valid_angle
```

```python
def print_bond_angle(mole: Molecule):
    # Usage: Print all bond angle i-j-k which is considered as valid angle
    raise NotImplementedError("About 5~20 lines of code")

Molecule.print_bond_angle = print_bond_angle
```

### Solution

For this solution, there could be different outputs, such as angle `0-1-2` should be equal to `2-1-0`. Such kind of minor differences could be tolerated. However, angle values, number of valid angles, etc. should be exactly the same.

```python
sol_mole.print_bond_angle()
```

    === Bond Angle ===
      0 -   1 -   2:  124.26831 Degree
      0 -   1 -   3:  115.47934 Degree
      1 -   0 -   4:  109.84706 Degree
      1 -   0 -   5:  109.89841 Degree
      1 -   0 -   6:  109.89841 Degree
      4 -   0 -   5:  109.95368 Degree
      4 -   0 -   6:  109.95368 Degree
      5 -   0 -   6:  107.25265 Degree
      2 -   1 -   3:  120.25235 Degree

## Step 4: Out-of-Plane Angles

Calculate out-of-plane angles. For example, the angle $\theta_{ijkl}$ for atom $i$ out of the plane containing atoms $j$-$k$-$l$ (with $k$ as the central atom, connected to $i$) is given by:

$$
\sin \theta_{ijkl} = \frac{\boldsymbol{e}_{kj} \times \boldsymbol{e}_{kl}}{\sin \phi_{jkl}} \cdot \boldsymbol{e}_{ki}
$$

Print all valid out-of-plane angles (bond angle $\phi_{jkl}$ should be valid bond angle, and bond length $R_{ki} < 3 \, \mathsf{Bohr}$) in degree. Note that situation of $\sin \phi_{jkl} = 0$ could occasionally (especially Molecule HCN or H2C=C=CH2), so excluding this kind of situation is encouraged.

````{admonition} Hint 1: Cross products
:class: dropdown

Cross product of two arrays (length of array is 3) $\boldsymbol{c} = \boldsymbol{a} \times \boldsymbol{b}$ is defined as

$$
\begin{align}
c_x &= a_y b_z - a_z b_y \\
c_y &= a_z b_x - a_x b_z \\
c_z &= a_x b_y - a_y b_x
\end{align}
$$

In NumPy, `numpy.cross` ([NumPy API](https://numpy.org/doc/stable/reference/generated/numpy.cross.html)) could fulfill this feature.

```python
>>> np.cross([5, 2, 3], [7, 8, 9])
array([ -6, -24,  26])
```

````

````{admonition} Hint 2: Numerical precision
:class: dropdown

Actually, when implementing arcsin operation, one may encounter `RuntimeWarning` from NumPy:

```python
>>> np.arcsin(-1.000000000000001)
<stdin>:1: RuntimeWarning: invalid value encountered in arcsin
nan
```

To avoid such kind of error, one need to make sure the value passed into `numpy.arcsin` is not larger than 1 nor smaller than -1, strictly. One may also assert that the passed value is not too large or too small, in case of internal programming mistakes.

```python
>>> theta = -1.000000000000001
>>> assert(np.abs(theta) < 1 + 1e-7)  # make sure that theta is not too wrong, in case of programming mistakes
>>> theta = np.sign(theta) if np.abs(theta) > 1 else theta  # make theta bounded between -1 to 1
>>> np.arcsin(theta)
-1.5707963267948966
```

````

### Implementation

Reader should fill all `NotImplementedError` in the following code:

```python
def out_of_plane_angle(mole: Molecule, i: int, j: int, k: int, l: int) -> float:
    # Input: `i`, `j`, `k`, `l` index of molecule's atom; where `k` is the central atom, and angle is i - j-k-l
    # Output: Out-of-plane bond angle for atoms `i`-`j`-`k`-`l`
    raise NotImplementedError("About 3~8 lines of code")

Molecule.out_of_plane_angle = out_of_plane_angle
```

```python
def is_valid_out_of_plane_angle(mole: Molecule, i: int, j: int, k: int, l: int) -> bool:
    # Input: `i`, `j`, `k`, `l` index of molecule's atom; where `k` is the central atom, and angle is i - j-k-l
    # Output: Test if `i`-`j`-`k` is a valid out-of-plane bond angle
    #         if i != j != k != l
    #         and if angle j-k-l is valid bond angle
    #         and if i-k bond length smaller than 3 Angstrom
    #         and bond angle of j-k-l is not linear
    raise NotImplementedError("About 1~5 lines of code")

Molecule.is_valid_out_of_plane_angle = is_valid_out_of_plane_angle
```

```python
def print_out_of_plane_angle(mole: Molecule):
    # Usage: Print all out-of-plane bond angle i-j-k-l which is considered as valid
    raise NotImplementedError("About 6~15 lines of code")

Molecule.print_out_of_plane_angle = print_out_of_plane_angle
```

### Solution

Difference of output may also be tolerated in this solution.

```python
sol_mole.print_out_of_plane_angle()
```

    === Out-of-Plane Angle ===
      3 -   0 -   1 -   2:    0.00000 Degree
      2 -   0 -   1 -   3:    0.00000 Degree
      5 -   1 -   0 -   4:  -53.62632 Degree
      6 -   1 -   0 -   4:   53.62632 Degree
      4 -   1 -   0 -   5:   53.65153 Degree
      6 -   1 -   0 -   5:  -56.27711 Degree
      4 -   1 -   0 -   6:  -53.65153 Degree
      5 -   1 -   0 -   6:   56.27711 Degree
      1 -   4 -   0 -   5:  -53.67878 Degree
      6 -   4 -   0 -   5:   56.19462 Degree
      1 -   4 -   0 -   6:   53.67878 Degree
      5 -   4 -   0 -   6:  -56.19462 Degree
      1 -   5 -   0 -   6:  -54.97706 Degree
      4 -   5 -   0 -   6:   54.86999 Degree
      0 -   2 -   1 -   3:    0.00000 Degree

## Step 5: Torsion/Dihedral Angles

Calculate torsional angles. For example, the torsional angle $\tau_{ijkl}$ for the atom connectivity $i$-$j$-$k$-$l$ is given by:

$$
\cos \tau_{ijkl} = \frac{(\boldsymbol{e}_{ji} \times \boldsymbol{e}_{jk}) \cdot (\boldsymbol{e}_{kj} \times \boldsymbol{e}_{kl})}{\sin \phi_{ijk} \cdot \sin \phi_{jkl}}
$$

Print all valid dihedral angles ($i, j, k$ and $j, k, l$ constructs a valid angle, where $j, k$ should be bonded or $R_{jk} < 3 \, \mathsf{Bohr}$) in degree.

### Implementation

Reader should fill all `NotImplementedError` in the following code:

```python
def dihedral_angle(mole: Molecule, i: int, j: int, k: int, l: int) -> float:
    # Input: `i`, `j`, `k`, `l` index of molecule's atom; where `k` is the central atom, and angle is i - j-k-l
    # Output: Dihedral angle for atoms `i`-`j`-`k`-`l`
    raise NotImplementedError("About 3~8 lines of code")

Molecule.dihedral_angle = dihedral_angle
```

```python
def is_valid_dihedral_angle(mole: Molecule, i: int, j: int, k: int, l: int) -> bool:
    # Input: `i`, `j`, `k`, `l` index of molecule's atom; where `k` is the central atom, and angle is i - j-k-l
    # Output: Test if `i`-`j`-`k` is a valid dihedral bond angle
    #         if i != j != k != l
    #         and if i, j, k construct a valid bond angle (with j-k bonded)
    #         and if j, k, l construct a valid bond angle (with j-k bonded)
    raise NotImplementedError("About 1~5 lines of code")

Molecule.is_valid_dihedral_angle = is_valid_dihedral_angle
```

```python
def print_dihedral_angle(mole: Molecule):
    # Usage: Print all dihedral bond angle i-j-k-l which is considered as valid
    raise NotImplementedError("About 6~15 lines of code")

Molecule.print_dihedral_angle = print_dihedral_angle
```

### Solution

Difference of output may also be tolerated in this solution.

```python
sol_mole.print_dihedral_angle()
```

    === Dihedral Angle ===
      2 -   0 -   1 -   3:  180.00000 Degree
      2 -   0 -   1 -   4:    0.00000 Degree
      2 -   0 -   1 -   5:  121.09759 Degree
      2 -   0 -   1 -   6:  121.09759 Degree
      3 -   0 -   1 -   4:  180.00000 Degree
      3 -   0 -   1 -   5:   58.90241 Degree
      3 -   0 -   1 -   6:   58.90241 Degree
      4 -   0 -   1 -   5:  121.09759 Degree
      4 -   0 -   1 -   6:  121.09759 Degree
      5 -   0 -   1 -   6:  117.80483 Degree
      1 -   0 -   4 -   5:  121.06434 Degree
      1 -   0 -   4 -   6:  121.06434 Degree
      5 -   0 -   4 -   6:  117.87131 Degree
      1 -   0 -   5 -   4:  121.03351 Degree
      1 -   0 -   5 -   6:  119.43447 Degree
      4 -   0 -   5 -   6:  119.53201 Degree
      1 -   0 -   6 -   4:  121.03351 Degree
      1 -   0 -   6 -   5:  119.43447 Degree
      4 -   0 -   6 -   5:  119.53201 Degree
      0 -   1 -   2 -   3:  180.00000 Degree
      0 -   1 -   3 -   2:  180.00000 Degree

## Step 6: Center-of-Mass Translation

Find the center of mass of the molecule:

$$
\boldsymbol{r}_\mathrm{c.m.} = \left. \sum_{i} m_i \boldsymbol{r}_i \right/ \sum_{i} m_i
$$

or in its components

$$
x_\mathrm{c.m.} = \left. \sum_i m_i x_i \right/ \sum_i m_i , \quad
y_\mathrm{c.m.} = \left. \sum_i m_i y_i \right/ \sum_i m_i , \quad
z_\mathrm{c.m.} = \left. \sum_i m_i z_i \right/ \sum_i m_i
$$

where $m_i$ is the mass of atom $i$ and the summation runs over all atoms in the molecule.

Translate the input coordinates of the molecule to the center-of-mass.

We use isotopic-averaged atom mass, so the final results may be different to the original Crawford's project at $10^{-3} \, \mathsf{a.u.}$ level.

````{admonition} Hint 1: Atomic masses
:class: dropdown

An excellent source for atomic masses and other physical constants is the [National Institute of Standard and Technology (NIST) website](https://physics.nist.gov/cgi-bin/Compositions/stand_alone.pl).

For python users, one may find package [molmass](https://github.com/cgohlke/molmass) useful.

````

### Implementation

```python
def center_of_mass(mole: Molecule) -> np.ndarray or list:
    # Output: Center of mass for this molecule
    raise NotImplementedError("About 5~10 lines of code")

Molecule.center_of_mass = center_of_mass
```

### Solution

```python
sol_mole.center_of_mass()
```

    array([0.64475065, 0.        , 2.31636762])

## Step 7: Principal Moments of Inertia

Calculate elements of the [moment of inertia tensor](http://en.wikipedia.org/wiki/Moment_of_inertia_tensor).

Diagonal:

$$
I_{xx} = \sum_i m_i (\tilde y_i^2 + \tilde z_i^2), \quad I_{yy} = \sum_i m_i (\tilde z_i^2 + \tilde x_i^2) \, \quad I_{zz} = \sum_i m_i (\tilde x_i^2 + \tilde y_i^2)
$$

Off-diagonal (add a negative sign):

$$
I_{xy} = I_{yx} = - \sum_i m_i \tilde x_i \tilde y_i, \quad I_{yz} = I_{zy} = - \sum_i m_i \tilde y_i \tilde z_i, \quad I_{zx} = I_{xz} = - \sum_i m_i \tilde z_i \tilde x_i
$$

Calculate eigenvalue of inertia tensor to obtain the principal moments of inertia:

$$
I_a \leqslant I_b \leqslant I_c
$$

Report the moments of inertia in $\mathsf{amu \cdot Bohr^2}$, $\mathsf{amu \cdot {\buildrel_{\circ} \over{\mathsf{A}}}{}^2}$, $\mathsf{g \cdot cm^2}$.

Note that $\tilde x_i, \tilde y_i, \tilde z_i$ in the equations above are translated by center of mass:

$$
\tilde x_i = x_i - x_\mathrm{c.m.}, \quad \tilde y_i = y_i - y_\mathrm{c.m.}, \quad \tilde z_i = z_i - z_\mathrm{c.m.}
$$

Based on the relative values of the principal moments, determine the [molecular rotor type](http://en.wikipedia.org/wiki/Rotational_spectroscopy). Criterion of equality and far larger/smaller could be set to $10^{-4} \mathsf{amu \cdot Bohr^2}$.

- Spherical: $I_a = I_b = I_c$
- Linear: $I_a \ll I_b = I_c$
- Prolate: $I_a < I_b = I_c$
- Oblate: $I_a = I_b < I_c$

- Asymmetric: $I_a \neq I_b \neq I_c$

````{admonition} Hint 1: Eigenvalue of symmetric matrix
:class: dropdown

Eigenvalue $\lambda$ of moments of inertia matrix can be obtained as

$$
\left\vert
\begin{matrix}
I_{xx} - \lambda & I_{xy} & I_{xz} \\
I_{yx} & I_{yy} - \lambda & I_{yz} \\
I_{zx} & I_{zy} & I_{zz} - \lambda
\end{matrix}
\right\vert = 0
$$

This leads to a cubic equation in $\lambda$, which one can solve directly.

However, solving symmetric matrix by program is usually called *diagonalization*. This can be done by `numpy.linalg.eigh` which returns eigenvalues and eigenvectors ([NumPy API](https://numpy.org/doc/stable/reference/generated/numpy.linalg.eigh.html)) or `numpy.linalg.eigvalsh` which only returns eigenvalues ([NumPy API](https://numpy.org/doc/stable/reference/generated/numpy.linalg.eigvalsh.html)).

````

````{admonition} Hint 2: Physical constants
:class: dropdown

Lots of useful and precise physical constants are available at the [National Institute of Standards and Technology website](https://physics.nist.gov/cuu/Constants/index.html?/codata86.html).

For python, `scipy.constants` ([SciPy API](https://docs.scipy.org/doc/scipy/reference/constants.html)) offers lots of constants and unit conversion utilities.

````

### Implementation

Reader should fill all `NotImplementedError` in the following code:

```python
def moment_of_inertia(mole: Molecule) -> np.ndarray or list:
    # Output: Principle of moments of intertia
    raise NotImplementedError("About 4~25 lines of code")

Molecule.moment_of_inertia = moment_of_inertia
```

```python
def print_moment_of_interia(mole: Molecule):
    # Output: Print moments of inertia in amu bohr2, amu Å2, and g cm2
    raise NotImplementedError("About 3~15 lines of code")

Molecule.print_moment_of_interia = print_moment_of_interia
```

```python
def type_of_moment_of_interia(mole: Molecule) -> str:
    # Output: Judge which type of moment of interia is
    raise NotImplementedError("About 7~11 lines of code")

Molecule.type_of_moment_of_interia = type_of_moment_of_interia
```

### Solution

Value of principles of moment of interia:

```python
sol_mole.print_moment_of_interia()
```

    In      amu bohr^2:   3.19751875e+01   1.78736281e+02   1.99467661e+02
    In  amu angstrom^2:   8.95396445e+00   5.00512562e+01   5.58566339e+01
    In          g cm^2:   1.48684078e-39   8.31120663e-39   9.27521227e-39

Type of interia:

```python
sol_mole.type_of_moment_of_interia()
```

    'Asymmetric'

## Step 8: Rotational Constants

Compute the rotational constants in $\mathsf{cm}^{-1}$ and $\mathsf{MHz}$:

$$
A = \frac{h}{8 \pi^2 c I_a} , \quad B = \frac{h}{8 \pi^2 c I_b} , \quad C = \frac{h}{8 \pi^2 c I_c}
$$

where $A \geqslant B \geqslant C$.

### Implementation

Reader should fill all `NotImplementedError` in the following code:

```python
def rotational_constants(mole: Molecule) -> np.ndarray or list:
    # Output: Rotational constant in cm^-1
    raise NotImplementedError("About 1~10 lines of code")

Molecule.rotational_constants = rotational_constants
```

### Solution

Compute the rotational constants in $\mathsf{cm}^{-1}$

```python
sol_mole.rotational_constants()
```

    array([1.88270004, 0.33680731, 0.30180174])

## Test Cases

**Acetaldehyde**
- Input file: {download}`input/acetaldehyde.dat`

```python
sol_mole = SolMol()
sol_mole.construct_from_dat_file("input/acetaldehyde.dat")
sol_mole.print_solution_01()
```

    === Atom Charges ===
    [6 6 8 1 1 1 1]
    === Coordinates ===
    [[ 0.          0.          0.        ]
     [ 0.          0.          2.84511213]
     [ 1.89911596  0.          4.13906253]
     [-1.89404831  0.          3.74768867]
     [ 1.94250082  0.         -0.70114598]
     [-1.00729547 -1.66997184 -0.70591697]
     [-1.00729547  1.66997184 -0.70591697]]
    === Bond Length ===
      0 -   1:    2.84511 Bohr
      0 -   2:    4.55395 Bohr
      0 -   3:    4.19912 Bohr
      0 -   4:    2.06517 Bohr
      0 -   5:    2.07407 Bohr
      0 -   6:    2.07407 Bohr
      1 -   2:    2.29803 Bohr
      1 -   3:    2.09811 Bohr
      1 -   4:    4.04342 Bohr
      1 -   5:    4.05133 Bohr
      1 -   6:    4.05133 Bohr
      2 -   3:    3.81330 Bohr
      2 -   4:    4.84040 Bohr
      2 -   5:    5.89151 Bohr
      2 -   6:    5.89151 Bohr
      3 -   4:    5.87463 Bohr
      3 -   5:    4.83836 Bohr
      3 -   6:    4.83836 Bohr
      4 -   5:    3.38971 Bohr
      4 -   6:    3.38971 Bohr
      5 -   6:    3.33994 Bohr
    === Bond Angle ===
      0 -   1 -   2:  124.26831 Degree
      0 -   1 -   3:  115.47934 Degree
      1 -   0 -   4:  109.84706 Degree
      1 -   0 -   5:  109.89841 Degree
      1 -   0 -   6:  109.89841 Degree
      4 -   0 -   5:  109.95368 Degree
      4 -   0 -   6:  109.95368 Degree
      5 -   0 -   6:  107.25265 Degree
      2 -   1 -   3:  120.25235 Degree
    === Out-of-Plane Angle ===
      3 -   0 -   1 -   2:    0.00000 Degree
      2 -   0 -   1 -   3:    0.00000 Degree
      5 -   1 -   0 -   4:  -53.62632 Degree
      6 -   1 -   0 -   4:   53.62632 Degree
      4 -   1 -   0 -   5:   53.65153 Degree
      6 -   1 -   0 -   5:  -56.27711 Degree
      4 -   1 -   0 -   6:  -53.65153 Degree
      5 -   1 -   0 -   6:   56.27711 Degree
      1 -   4 -   0 -   5:  -53.67878 Degree
      6 -   4 -   0 -   5:   56.19462 Degree
      1 -   4 -   0 -   6:   53.67878 Degree
      5 -   4 -   0 -   6:  -56.19462 Degree
      1 -   5 -   0 -   6:  -54.97706 Degree
      4 -   5 -   0 -   6:   54.86999 Degree
      0 -   2 -   1 -   3:    0.00000 Degree
    === Dihedral Angle ===
      2 -   0 -   1 -   3:  180.00000 Degree
      2 -   0 -   1 -   4:    0.00000 Degree
      2 -   0 -   1 -   5:  121.09759 Degree
      2 -   0 -   1 -   6:  121.09759 Degree
      3 -   0 -   1 -   4:  180.00000 Degree
      3 -   0 -   1 -   5:   58.90241 Degree
      3 -   0 -   1 -   6:   58.90241 Degree
      4 -   0 -   1 -   5:  121.09759 Degree
      4 -   0 -   1 -   6:  121.09759 Degree
      5 -   0 -   1 -   6:  117.80483 Degree
      1 -   0 -   4 -   5:  121.06434 Degree
      1 -   0 -   4 -   6:  121.06434 Degree
      5 -   0 -   4 -   6:  117.87131 Degree
      1 -   0 -   5 -   4:  121.03351 Degree
      1 -   0 -   5 -   6:  119.43447 Degree
      4 -   0 -   5 -   6:  119.53201 Degree
      1 -   0 -   6 -   4:  121.03351 Degree
      1 -   0 -   6 -   5:  119.43447 Degree
      4 -   0 -   6 -   5:  119.53201 Degree
      0 -   1 -   2 -   3:  180.00000 Degree
      0 -   1 -   3 -   2:  180.00000 Degree
    === Center of Mass ===
       0.64475    0.00000    2.31637
    === Moments of Inertia ===
    In      amu bohr^2:   3.19751875e+01   1.78736281e+02   1.99467661e+02
    In  amu angstrom^2:   8.95396445e+00   5.00512562e+01   5.58566339e+01
    In          g cm^2:   1.48684078e-39   8.31120663e-39   9.27521227e-39
    Type:  Asymmetric
    === Rotational Constants ===
    In cm^-1:      1.88270      0.33681      0.30180
    In   MHz:  56441.92712  10097.22927   9047.78849

**Allene**
- Input file: {download}`input/allene.dat`

```python
sol_mole = SolMol()
sol_mole.construct_from_dat_file("input/allene.dat")
sol_mole.print_solution_01()
```

    === Atom Charges ===
    [6 6 6 1 1 1 1]
    === Coordinates ===
    [[ 0.          0.          1.88972599]
     [ 2.55113008  0.          1.88972599]
     [-2.55113008  0.          1.88972599]
     [ 3.49599308  1.15721611  0.73250988]
     [ 3.49599308 -1.15721611  3.0469421 ]
     [-3.49599308 -1.15721611  0.73250988]
     [-3.49599308  1.15721611  3.0469421 ]]
    === Bond Length ===
      0 -   1:    2.55113 Bohr
      0 -   2:    2.55113 Bohr
      0 -   3:    3.86009 Bohr
      0 -   4:    3.86009 Bohr
      0 -   5:    3.86009 Bohr
      0 -   6:    3.86009 Bohr
      1 -   2:    5.10226 Bohr
      1 -   3:    1.88973 Bohr
      1 -   4:    1.88973 Bohr
      1 -   5:    6.26466 Bohr
      1 -   6:    6.26466 Bohr
      2 -   3:    6.26466 Bohr
      2 -   4:    6.26466 Bohr
      2 -   5:    1.88973 Bohr
      2 -   6:    1.88973 Bohr
      3 -   4:    3.27310 Bohr
      3 -   5:    7.36508 Bohr
      3 -   6:    7.36508 Bohr
      4 -   5:    7.36508 Bohr
      4 -   6:    7.36508 Bohr
      5 -   6:    3.27310 Bohr
    === Bond Angle ===
      1 -   0 -   2:  180.00000 Degree
      0 -   1 -   3:  120.00000 Degree
      0 -   1 -   4:  120.00000 Degree
      0 -   2 -   5:  120.00000 Degree
      0 -   2 -   6:  120.00000 Degree
      3 -   1 -   4:  120.00000 Degree
      5 -   2 -   6:  120.00000 Degree
    === Out-of-Plane Angle ===
      4 -   0 -   1 -   3:    0.00000 Degree
      3 -   0 -   1 -   4:    0.00000 Degree
      6 -   0 -   2 -   5:    0.00000 Degree
      5 -   0 -   2 -   6:    0.00000 Degree
      0 -   3 -   1 -   4:    0.00000 Degree
      0 -   5 -   2 -   6:    0.00000 Degree
    === Dihedral Angle ===
      2 -   0 -   1 -   3:   90.00000 Degree
      2 -   0 -   1 -   4:   90.00000 Degree
      3 -   0 -   1 -   4:  180.00000 Degree
      1 -   0 -   2 -   5:   90.00000 Degree
      1 -   0 -   2 -   6:   90.00000 Degree
      5 -   0 -   2 -   6:  180.00000 Degree
      0 -   1 -   3 -   4:  180.00000 Degree
      0 -   1 -   4 -   3:  180.00000 Degree
      0 -   2 -   5 -   6:  180.00000 Degree
      0 -   2 -   6 -   5:  180.00000 Degree
    === Center of Mass ===
       0.00000    0.00000    1.88973
    === Moments of Inertia ===
    In      amu bohr^2:   1.07982664e+01   2.11013373e+02   2.11013373e+02
    In  amu angstrom^2:   3.02382256e+00   5.90897626e+01   5.90897626e+01
    In          g cm^2:   5.02117550e-40   9.81208592e-39   9.81208592e-39
    Type:  Prolate
    === Rotational Constants ===
    In cm^-1:      5.57494      0.28529      0.28529
    In   MHz: 167132.49483   8552.73379   8552.73379

**Benzene**
- Input file: {download}`input/benzene.dat`

```python
sol_mole = SolMol()
sol_mole.construct_from_dat_file("input/benzene.dat")
sol_mole.print_solution_01()
```

    === Atom Charges ===
    [6 6 6 6 6 6 1 1 1 1 1 1]
    === Coordinates ===
    [[ 0.00000000e+00  0.00000000e+00  0.00000000e+00]
     [ 0.00000000e+00  0.00000000e+00  2.61644846e+00]
     [ 2.26591048e+00  0.00000000e+00  3.92467249e+00]
     [ 4.53182108e+00  0.00000000e+00  2.61644839e+00]
     [ 4.53182108e+00  0.00000000e+00 -1.32281000e-07]
     [ 2.26591069e+00  0.00000000e+00 -1.30822415e+00]
     [ 2.26591069e+00 -0.00000000e+00 -3.33421820e+00]
     [ 6.28638327e+00  0.00000000e+00 -1.01299708e+00]
     [ 6.28638324e+00  0.00000000e+00  3.62944532e+00]
     [ 2.26591048e+00  0.00000000e+00  5.95066654e+00]
     [-1.75456219e+00  0.00000000e+00  3.62944541e+00]
     [-1.75456216e+00 -0.00000000e+00 -1.01299693e+00]]
    === Bond Length ===
      0 -   1:    2.61645 Bohr
      0 -   2:    4.53182 Bohr
      0 -   3:    5.23290 Bohr
      0 -   4:    4.53182 Bohr
      0 -   5:    2.61645 Bohr
      0 -   6:    4.03130 Bohr
      0 -   7:    6.36748 Bohr
      0 -   8:    7.25889 Bohr
      0 -   9:    6.36748 Bohr
      0 -  10:    4.03130 Bohr
      0 -  11:    2.02599 Bohr
      1 -   2:    2.61645 Bohr
      1 -   3:    4.53182 Bohr
      1 -   4:    5.23290 Bohr
      1 -   5:    4.53182 Bohr
      1 -   6:    6.36748 Bohr
      1 -   7:    7.25889 Bohr
      1 -   8:    6.36748 Bohr
      1 -   9:    4.03130 Bohr
      1 -  10:    2.02599 Bohr
      1 -  11:    4.03130 Bohr
      2 -   3:    2.61645 Bohr
      2 -   4:    4.53182 Bohr
      2 -   5:    5.23290 Bohr
      2 -   6:    7.25889 Bohr
      2 -   7:    6.36748 Bohr
      2 -   8:    4.03130 Bohr
      2 -   9:    2.02599 Bohr
      2 -  10:    4.03130 Bohr
      2 -  11:    6.36748 Bohr
      3 -   4:    2.61645 Bohr
      3 -   5:    4.53182 Bohr
      3 -   6:    6.36748 Bohr
      3 -   7:    4.03130 Bohr
      3 -   8:    2.02599 Bohr
      3 -   9:    4.03130 Bohr
      3 -  10:    6.36748 Bohr
      3 -  11:    7.25889 Bohr
      4 -   5:    2.61645 Bohr
      4 -   6:    4.03130 Bohr
      4 -   7:    2.02599 Bohr
      4 -   8:    4.03130 Bohr
      4 -   9:    6.36748 Bohr
      4 -  10:    7.25889 Bohr
      4 -  11:    6.36748 Bohr
      5 -   6:    2.02599 Bohr
      5 -   7:    4.03130 Bohr
      5 -   8:    6.36748 Bohr
      5 -   9:    7.25889 Bohr
      5 -  10:    6.36748 Bohr
      5 -  11:    4.03130 Bohr
      6 -   7:    4.64244 Bohr
      6 -   8:    8.04095 Bohr
      6 -   9:    9.28488 Bohr
      6 -  10:    8.04095 Bohr
      6 -  11:    4.64244 Bohr
      7 -   8:    4.64244 Bohr
      7 -   9:    8.04095 Bohr
      7 -  10:    9.28488 Bohr
      7 -  11:    8.04095 Bohr
      8 -   9:    4.64244 Bohr
      8 -  10:    8.04095 Bohr
      8 -  11:    9.28488 Bohr
      9 -  10:    4.64244 Bohr
      9 -  11:    8.04095 Bohr
     10 -  11:    4.64244 Bohr
    === Bond Angle ===
      0 -   1 -   2:  120.00000 Degree
      1 -   0 -   5:  120.00000 Degree
      0 -   1 -  10:  120.00000 Degree
      1 -   0 -  11:  120.00000 Degree
      0 -   5 -   4:  120.00000 Degree
      0 -   5 -   6:  120.00000 Degree
      5 -   0 -  11:  120.00000 Degree
      1 -   2 -   3:  120.00000 Degree
      1 -   2 -   9:  120.00000 Degree
      2 -   1 -  10:  120.00000 Degree
      2 -   3 -   4:  120.00000 Degree
      2 -   3 -   8:  120.00000 Degree
      3 -   2 -   9:  120.00000 Degree
      3 -   4 -   5:  120.00000 Degree
      3 -   4 -   7:  120.00000 Degree
      4 -   3 -   8:  120.00000 Degree
      4 -   5 -   6:  120.00000 Degree
      5 -   4 -   7:  120.00000 Degree
    === Out-of-Plane Angle ===
     10 -   0 -   1 -   2:    0.00000 Degree
     11 -   1 -   0 -   5:    0.00000 Degree
      2 -   0 -   1 -  10:    0.00000 Degree
      5 -   1 -   0 -  11:    0.00000 Degree
      6 -   0 -   5 -   4:    0.00000 Degree
      4 -   0 -   5 -   6:    0.00000 Degree
      1 -   5 -   0 -  11:    0.00000 Degree
      9 -   1 -   2 -   3:    0.00000 Degree
      3 -   1 -   2 -   9:    0.00000 Degree
      0 -   2 -   1 -  10:    0.00000 Degree
      8 -   2 -   3 -   4:    0.00000 Degree
      4 -   2 -   3 -   8:    0.00000 Degree
      1 -   3 -   2 -   9:    0.00000 Degree
      7 -   3 -   4 -   5:    0.00000 Degree
      5 -   3 -   4 -   7:    0.00000 Degree
      2 -   4 -   3 -   8:    0.00000 Degree
      0 -   4 -   5 -   6:    0.00000 Degree
      3 -   5 -   4 -   7:    0.00000 Degree
    === Dihedral Angle ===
      2 -   0 -   1 -   5:    0.00000 Degree
      2 -   0 -   1 -  10:  180.00000 Degree
      2 -   0 -   1 -  11:  180.00000 Degree
      5 -   0 -   1 -  10:  180.00000 Degree
      5 -   0 -   1 -  11:  180.00000 Degree
     10 -   0 -   1 -  11:    0.00000 Degree
      1 -   0 -   5 -   4:    0.00000 Degree
      1 -   0 -   5 -   6:  180.00000 Degree
      1 -   0 -   5 -  11:  180.00000 Degree
      4 -   0 -   5 -   6:  180.00000 Degree
      4 -   0 -   5 -  11:  180.00000 Degree
      6 -   0 -   5 -  11:    0.00000 Degree
      1 -   0 -  11 -   5:  180.00000 Degree
      0 -   1 -   2 -   3:    0.00000 Degree
      0 -   1 -   2 -   9:  180.00000 Degree
      0 -   1 -   2 -  10:  180.00000 Degree
      3 -   1 -   2 -   9:  180.00000 Degree
      3 -   1 -   2 -  10:  180.00000 Degree
      9 -   1 -   2 -  10:    0.00000 Degree
      0 -   1 -  10 -   2:  180.00000 Degree
      1 -   2 -   3 -   4:    0.00000 Degree
      1 -   2 -   3 -   8:  180.00000 Degree
      1 -   2 -   3 -   9:  180.00000 Degree
      4 -   2 -   3 -   8:  180.00000 Degree
      4 -   2 -   3 -   9:  180.00000 Degree
      8 -   2 -   3 -   9:    0.00000 Degree
      1 -   2 -   9 -   3:  180.00000 Degree
      2 -   3 -   4 -   5:    0.00000 Degree
      2 -   3 -   4 -   7:  180.00000 Degree
      2 -   3 -   4 -   8:  180.00000 Degree
      5 -   3 -   4 -   7:  180.00000 Degree
      5 -   3 -   4 -   8:  180.00000 Degree
      7 -   3 -   4 -   8:    0.00000 Degree
      2 -   3 -   8 -   4:  180.00000 Degree
      0 -   4 -   5 -   3:    0.00000 Degree
      0 -   4 -   5 -   6:  180.00000 Degree
      0 -   4 -   5 -   7:  180.00000 Degree
      3 -   4 -   5 -   6:  180.00000 Degree
      3 -   4 -   5 -   7:  180.00000 Degree
      6 -   4 -   5 -   7:    0.00000 Degree
      3 -   4 -   7 -   5:  180.00000 Degree
      0 -   5 -   6 -   4:  180.00000 Degree
    === Center of Mass ===
       2.26591    0.00000    1.30822
    === Moments of Inertia ===
    In      amu bohr^2:   3.11839639e+02   3.11839704e+02   6.23679344e+02
    In  amu angstrom^2:   8.73239929e+01   8.73240110e+01   1.74648004e+02
    In          g cm^2:   1.45004902e-38   1.45004932e-38   2.90009833e-38
    Type:  Oblate
    === Rotational Constants ===
    In cm^-1:      0.19305      0.19305      0.09652
    In   MHz:   5787.40152   5787.40032   2893.70046

## References

- Wilson, E. B.; Decius, J. C.; Cross, P. C. *Molecular Vibrations* Dover Publication Inc., 1980.

    ISBN-13: 978-0486639413


# 1. Introduction

Collateral Reblance Pool (CRP) dynamically rebalances Collateral to ensure the ayToken minted (i.e. the loan) remains solvent, especially in an adverse market environment (i.e. the value of the loan does not exceed the value of Collateral). This dynamic rebalancing, together with a careful choice of the key parameters (including LTV and volatility assumption) allows ALEX to eliminate the liquidation needs. Any residual gap risk (which CRP cannot address entirely) is addressed through maintaining a strong reserve fund. When a Borrower mints ayToken by providing appropriate Collateral, the Collateral is converted into a basket of Collateral and Token, with the weights determined by CRP.

In this notebook, we will help you understand the key attributes of the CRP pool by answering:
1.  How does CRP achieve the dynamic rebalances with weights determined by CRP?
2.  How does CRP perform in different market environments?
3.  In which parameters space (including LTV and volatility assumption), the CRP would maintain a low default risk and a high pool-value level to a collateral ratio (PVCR)?

Since there is no close form of CPR performance, we use Monte Carlo simulation to show the results based on predicted the future scenarios.  Based on the simulation results: [Conculsion to be inserted...]

# 2. CRP dynamic rebalance mechanisam

The following diagram illustrates how CRP dynamic rebalances with the weights determined by CRP.  Once the pool updates new weights based on the Black Shoal option calculation, the pool will be rebalanced by arbitragers to bring the spot price equaled to the outside market. (???)  We leave the mathematical formula of the weight to session 4.

# 3.  CRP perfomance by Monte Carlo Simulations  [WIP]

In this session, we want to simulate how CRP performs in different market environments.  Basically, a CRP would serve as an agent (bot) response to the actual market environment by updating the pool weight based on current token price $p$, real price volatility $\eta$, and estimated price volatility $\sigma$.  We simplify the market environment and let the token price change follow a linear growth trend with variation, formally named the Geometric Brownian Motion (https://en.wikipedia.org/wiki/Geometric_Brownian_motion).  By setting up different growth rate $r$ and volatility $\eta$, we can approximately mimic different market environments.

Two metrics an LP would be very interested to know are 1) event of default, i.e., when the Loan to Value >1 at any time point, and 2)pool-value to the collateral ratio (PVCR), which is defined as $\frac{PoolValue(t)}{Collateral}$.  We can empirically estimate the default risk and confidence intervals of pool-value to the collateral ratio for any given parameters by conducting Monte Carlo simulations.

For simplicity, fees and token APY are not considered for now. We set initial weights to be 50/50 and loan lifetime equals 91 days, and all key parameters various in the following parameters space:
### Pool parameters

   (1) Initial LTV: range from 0.7 to 0.95

   (2) Black scholes volatility: $\sigma$ range from 0.1 - 1.0

### Market enviroment
   (3) Growth rate: $r$ range from $[-2, 2]$ (corresponding to 25% or 200% of the initial price)

   (4) Growth rate volatility: $\eta$ range from 0.1 - 1.0

### Episode

For each parameter setup (1) to (4), we activate the CRP and monitor its performance during the whole loan life term (called a episode). At each scheduled time point t (e.g. dayily), we rebalance Collateral and token according to $\eqref{eq:CRP}$, and reocrd and plot:

1. Token price at time t
2. Reblanced weight at time t
3. Daily pool value loss (relative to collaterals)
4. LTV without slippage
5. LTV with reblancing slippage cost.
6. Scatter plot of ytoken weight and pool value loss
7. PVTC without slippage
8. PVTC with reblancing slippage cost
9. (8) relative to (7).

Each episode will be one relization of the CRP based on a predited future.  Repeating the episode 100 times, we can estimate the chance of default and confidenct interval of the PVCR by the maturinity time.

### Simulation results:
For each selected combination (1) to (4), run the process 100 times and count the number of events that LVT(t) > 1 at any time point.  Report the frequency by LTV0 and growth rate for a given volatility setup.

For example this is the simulation results when growth rate volatility is high, and black sholes volatility is under-estimated.
The size of button indicate the chance of default when LVT is hight, even the growth rate is low.

# 4 How CRP determine the weights

CRP determines the weights based on the prevailing LTV and uses the following formula:

\begin{equation}
   \begin{cases}
      w_{Token} = N(d_1) \\
      w_{Collateral} = (1 - w_{Token}) \\
      d_1 = \frac{1}{\sigma \sqrt{(t)}} [ln (\frac{P_t}{P_0}) + t \cdot \frac{\sigma^2}{2}]
    \end{cases}
    \label{eq:CRP} \tag{1}
\end{equation}

Some readers may note the similarity of the above formula to the Black & Scholes delta, because it is. CRP essentially implements a delta replicating strategy of a call option on Token / Collateral, buying more Token when LTV moves higher and vice versa.

Token price changes follow:
$$ log(P_t) = log(P_0) + (r - \frac{\eta^2}{2}) \cdot \Delta t + \eta W_t $$
where $W_t$ is a Brownian motion.

Derive token locked value at time t
from

\begin{equation}
   \begin{cases}
      C(t+1) =  U(t) \cdot P_{t+1} + B(t)\\
      U(t+1) = C(t+1) \cdot w_{Token}(t+1)/P_{t+1}\\
      B(t+1) = C(t+1)(1-w_{Token}(t+1))
    \end{cases}
\end{equation}

We can rewrite it as
\begin{equation}
   \begin{cases}
      U(t+1) = U(t)[w_{Token}(t+1) + (1-w_{Token}(t)) \cdot \frac{P_{t} w_{Token}(t+1)}{P_{t+1} w_{Token}(t)}] \\
      B(t+1) = U(t+1) \cdot P_{t+1} \frac{1- w_{Token}(t+1)}{w_{Token}(t+1)}\\
      C(t+1) = U(t) \cdot P_{t+1} + B(t)
    \end{cases}
\end{equation}

Where $C$ = Collateral, $B$ = Collateral_Token_locked and $U$ = Loan_Token_locked.

```python
# plot of liquity

import scipy
import matplotlib.pyplot as plt
import matplotlib.animation
from matplotlib.widgets import Slider
import seaborn as sns
import numpy as np
import random
import pandas as pd
from ipywidgets import *
from scipy.stats import norm

```

### Import simulation function and class

```python
%run rbpool_env.ipynb
# function gen_data_vec_btc
print(help(gen_data_vec_btc))

# import PoolEngin class.
help(PoolEngine)
```

    Help on function gen_data_vec_btc in module __main__:

    gen_data_vec_btc(t, y_price, x, LTV0, bs_vol, Growth_rate, Real_vol)
        Purpose: Simulate a CRP performance during a loan life term (theoretical case, no value lose due to weight change)
            - x: collateral token (BTC)
            - y: loan token (USDC)
        Assumption:
            1. Rebalnce following a delta replicating strategy of a call option on Token / Collateral.
            2. Price percentage change (approximately equals to log(p(t+1)/p(t))) follows a linear growth treand
               (slope = growth rate) plus a random walk Brownie motion.
        Input:
            - t: term of the loan
            - y_price: price of loan token (e.g. USDC = 3e-5 btc)
            - x: Collateral token (e.g. BTC)
            - LTV0: inital loan to value at time 0
            - bs_vol: black shoral option volatilty
            - Growth_rate: linear growth slope of y price
            - Real_vol: growth Brownie motion volatility

        Output:
            - A dataframe with columns: ['t', 'y_price', 'borrowed_y', 'ltv', 'x_locked',
            'y_locked', 'collateral_x', 'w_ytoken', 'PVCR']

    None
    Help on class PoolEngine in module __main__:

    class PoolEngine(builtins.object)
     |  PoolEngine(x, y, w_x)
     |
     |  pool engine class
     |  Reflect real senario when weight changes makes arbitrage oppurtunites and pool value loss
     |
     |  Attribute:
     |      - x: token 1 (e.g BTC) balance
     |      - y: token 2 (e.g USDC) balance
     |      - w_x: weight of x
     |      - value: pool value
     |
     |  Method:
     |      - sp(): spot price of one y token in terms of x token
     |      - in-given-out(inTtype, amount): size of in given out amount
     |      - out-given-in(inTtype, amount): size of our given in amount
     |      - in_given_price(inTtype, p): size of in to bring spot price to p the price of out token as function of in token
     |      - step(new_wx, oracle_p, verbose = 0): attributes updated if weights or price changes
     |
     |  Methods defined here:
     |
     |  __init__(self, x, y, w_x)
     |      Initialize self.  See help(type(self)) for accurate signature.
     |
     |  in_given_out(self, inTtype, amount)
     |
     |  in_given_price(self, inTtype, price)
     |
     |  out_given_in(self, inTtype, amount)
     |
     |  sp(self)
     |
     |  step(self, new_wx, oracle_p, verbose=0)
     |
     |  ----------------------------------------------------------------------
     |  Data descriptors defined here:
     |
     |  __dict__
     |      dictionary for instance variables (if defined)
     |
     |  __weakref__
     |      list of weak references to the object (if defined)

```python
np.set_printoptions(formatter={'float': lambda x: "{0:0.3f}".format(x)})

rbpool = PoolEngine(x = 100, y = 2500*100/7*3 , w_x = 0.7)
#np.array([self.x, self.y, new_wx, spot_price, oracle_p, delta_x, self.value, value_change])
print('step0:', rbpool.sp(), rbpool.value)
rbpool.w_x = 0.55
print('step1a:', rbpool.sp(), rbpool.value)
rbpool.step(0.55, 0.0004)
print('step1b:', rbpool.sp(), rbpool.value)
rbpool.w_x = 0.7
print('step2a:', rbpool.sp(), rbpool.value)
rbpool.step(0.7, 0.0004)
print('step2b:', rbpool.sp(), rbpool.value, rbpool.x, rbpool.y)
```

    step0: 0.0004000000000000001 142.85714285714286
    step1a: 0.0007636363636363634 142.85714285714286
    step1b: 0.0003999999999999999 135.91435742058468
    step2a: 0.00020952380952380956 135.91435742058468
    step2b: 0.0004 129.65162623651895 90.75613836556326 97238.71967738922

```python
def get_episode_full(t,y_price, Collateral, LTV0, bs_vol, Growth_rate, Real_vol, pool_init_x, pool_init_y, pool_init_wx):

    """
    Purpuse:
        generate one simulation based on selected parameters and random price change following the brownie motion.
        pool attributes are rescorded at each step.

    Input:

    Output:
        Dataframe with the following key measures.

        - 1. Token price at time t
        - 2. Reblanced weight at time t
        - 3. Daily pool value change (relative to collaterals)
                changes due to weight changes
                changes due to price changes
        - 4. LTV without slippage
        - 5. LTV with reblancing slippage cost
        - 6. Scatter plot of ytoken weight change and pool value change_ due to weight change
        - 7. PVTC without slippage
        - 8. PVTC with reblancing slippage cost

    """
    # this function replicate the CRP balance sheet.
    episode = gen_data_vec_btc(t,y_price, Collateral, LTV0, bs_vol, Growth_rate, Real_vol)
    wt = episode.w_ytoken
    oracle_price = episode.y_price
    episode_output=[]
    pool_chg_price_chg =[0]

    # add pool rebalancing cost and trading cost
    # Pool starts at time 0 and run through the whole life cicle
    rbpool = PoolEngine(x = pool_init_x, y = pool_init_y, w_x = pool_init_wx)
    for i in wt.index:
        if i==0:
            episode_output.append(rbpool.step(1-wt[i], oracle_price[i]))
        else:
            # weights stays the same, price change due to trading
            _trade = rbpool.step(1-wt[i-1], oracle_price[i])
            pool_chg_price_chg.append(_trade[-1])   # capture the pool value change due to price change
            # weights changes, AMM bring the price back to oracle price
            episode_output.append(rbpool.step(1-wt[i], oracle_price[i]))  # capture the pool value change due to re-weight and update pool attributes

    output = pd.DataFrame(episode_output, columns = ['x', 'y', 'new_wx', 'spot_price', 'oracle_p', 'delta_x', 'value', 'value_change_wt_chg'])
    output['pool_chg_price_chg'] = np.array(pool_chg_price_chg)/Collateral
    output['pool_chg_wt_chg'] = output.value_change_wt_chg/Collateral
    output['pvcr_rebalance'] = output.value/Collateral
    output['pvcr_rebal_theory'] = output['pvcr_rebalance'] / episode.pvcr
    episode_full = pd.concat([episode, output], axis=1)
    episode_full['ltv_rebalance'] = episode_full.borrowed_y/(output.x + output.y*output.oracle_p)
    episode_full['wt_chg'] = episode_full['w_ytoken'].diff(1).fillna(0)
    return episode_full

```

```python
# one simulated crp during a loan life time

t =  np.linspace(91,0,92)/365
real_vol_0 = 0.4
growth_0 = 1
LTV0 = 0.9
bs_vol0 = 0.4
y_price = 1/35000
Collateral = 200
pool_init_x = 100
pool_init_y = 3500000
pool_init_wx = 0.5

# one episode test.  the function
one_test = get_episode_full(t,y_price=1/35000, Collateral=200, LTV0 = LTV0, bs_vol=bs_vol0, Growth_rate=growth_0, \
                            Real_vol = real_vol_0, pool_init_x= 100, pool_init_y=3500000, pool_init_wx = 0.5)
one_test
```

      t
      y_price
      borrowed_y
      ltv
      x_locked
      y_locked
      collateral_x
      w_ytoken
      pvcr
      x
      ...
      oracle_p
      delta_x
      value
      value_change_wt_chg
      pool_chg_price_chg
      pool_chg_wt_chg
      pvcr_rebalance
      pvcr_rebal_theory
      ltv_rebalance
      wt_chg

      0
      0.249315
      0.000029
      180.000000
      0.900000
      92.045315
      3.778414e+06
      200.000000
      0.539773
      1.000000
      91.754251
      ...
      0.000029
      8.245749e+00
      199.367563
      -0.632437
      0.000000
      -0.003162
      0.996838
      0.996838
      0.902855
      0.000000

      1
      0.246575
      0.000028
      176.487120
      0.891830
      98.939790
      3.532304e+06
      197.893157
      0.500034
      0.989466
      98.310180
      ...
      0.000028
      7.526871e+00
      196.633849
      -0.624010
      -0.010549
      -0.003120
      0.983169
      0.993636
      0.897542
      -0.039739

      2
      0.243836
      0.000029
      182.813798
      0.907533
      86.546914
      3.959379e+06
      201.440419
      0.570360
      1.007202
      85.133738
      ...
      0.000029
      1.492315e+01
      198.151209
      -1.976297
      0.017468
      -0.009881
      0.990756
      0.983672
      0.922597
      0.070325

      3
      0.241096
      0.000029
      183.463437
      0.908916
      85.301813
      4.002135e+06
      201.848699
      0.577397
      1.009243
      83.900347
      ...
      0.000029
      1.405809e+00
      198.532426
      -0.020091
      0.002007
      -0.000100
      0.992662
      0.983570
      0.924098
      0.007038

      4
      0.238356
      0.000029
      181.015253
      0.903750
      90.069984
      3.836185e+06
      200.293467
      0.550310
      1.001467
      88.455827
      ...
      0.000029
      5.203762e+00
      196.703979
      -0.294425
      -0.007670
      -0.001472
      0.983520
      0.982079
      0.920242
      -0.027087

      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...

      87
      0.010959
      0.000034
      215.748309
      0.966757
      0.001539
      6.516584e+06
      223.166949
      0.999993
      1.115835
      0.001032
      ...
      0.000034
      1.782338e-02
      149.594659
      -0.014826
      0.008105
      -0.000074
      0.747973
      0.670326
      1.442219
      0.000119

      88
      0.008219
      0.000034
      211.981875
      0.966757
      0.000653
      6.516611e+06
      219.271030
      0.999997
      1.096355
      0.000438
      ...
      0.000034
      5.758819e-04
      146.982914
      -0.000208
      -0.013058
      -0.000001
      0.734915
      0.670325
      1.442221
      0.000004

      89
      0.005479
      0.000035
      217.556352
      0.966757
      0.000002
      6.516630e+06
      225.037172
      1.000000
      1.125186
      0.000002
      ...
      0.000035
      4.476805e-04
      150.847666
      -0.000439
      0.019326
      -0.000002
      0.754238
      0.670323
      1.442226
      0.000003

      90
      0.002740
      0.000035
      222.080214
      0.966757
      0.000002
      6.516630e+06
      229.716591
      1.000000
      1.148583
      0.000002
      ...
      0.000035
      3.419140e-22
      153.984389
      0.000000
      0.015684
      0.000000
      0.769922
      0.670323
      1.442226
      0.000000

      91
      0.000000
      0.000037
      231.803208
      0.966757
      0.000002
      6.516630e+06
      239.773916
      1.000000
      1.198870
      0.000002
      ...
      0.000037
      0.000000e+00
      160.726048
      0.000000
      0.033708
      0.000000
      0.803630
      0.670323
      1.442226
      0.000000

92 rows × 23 columns

```python
one_test.head(3)
```

      t
      y_price
      borrowed_y
      ltv
      x_locked
      y_locked
      collateral_x
      w_ytoken
      pvcr
      x
      ...
      oracle_p
      delta_x
      value
      value_change_wt_chg
      pool_chg_price_chg
      pool_chg_wt_chg
      pvcr_rebalance
      pvcr_rebal_theory
      ltv_rebalance
      wt_chg

      0
      0.249315
      0.000029
      180.000000
      0.900000
      92.045315
      3.778414e+06
      200.000000
      0.539773
      1.000000
      91.754251
      ...
      0.000029
      8.245749
      199.367563
      -0.632437
      0.000000
      -0.003162
      0.996838
      0.996838
      0.902855
      0.000000

      1
      0.246575
      0.000028
      176.487120
      0.891830
      98.939790
      3.532304e+06
      197.893157
      0.500034
      0.989466
      98.310180
      ...
      0.000028
      7.526871
      196.633849
      -0.624010
      -0.010549
      -0.003120
      0.983169
      0.993636
      0.897542
      -0.039739

      2
      0.243836
      0.000029
      182.813798
      0.907533
      86.546914
      3.959379e+06
      201.440419
      0.570360
      1.007202
      85.133738
      ...
      0.000029
      14.923151
      198.151209
      -1.976297
      0.017468
      -0.009881
      0.990756
      0.983672
      0.922597
      0.070325

3 rows × 23 columns

```python
%matplotlib notebook
import matplotlib.pyplot as plt
fig, axs = plt.subplots(3,3,figsize=(10,15))
# Create main axis
fig.subplots_adjust(bottom=0.2, top=0.80)
# Create axes for sliders
ax_growth = fig.add_axes([0.3, 0.82, 0.4, 0.02])
ax_growth.spines['top'].set_visible(True)
ax_growth.spines['right'].set_visible(True)

ax_real_vol = fig.add_axes([0.3, 0.87, 0.4, 0.02])
ax_real_vol.spines['top'].set_visible(True)
ax_real_vol.spines['right'].set_visible(True)

ax_ltv = fig.add_axes([0.3, 0.92, 0.4, 0.02])
ax_ltv.spines['top'].set_visible(True)
ax_ltv.spines['right'].set_visible(True)

ax_bs_vol = fig.add_axes([0.3, 0.97, 0.4, 0.02])
ax_bs_vol.spines['top'].set_visible(True)
ax_bs_vol.spines['right'].set_visible(True)

# Create sliders
s_real_vol= Slider(ax=ax_real_vol, label='Real Vol', valmin=0.1, valmax=1.0, valinit=0.5, valfmt=' %1.2f ', facecolor='#cc7000')
s_growth = Slider(ax=ax_growth, label='Growth', valmin=-2, valmax=2, valinit=2, valfmt=' %1.2f', facecolor='#cc7000')
s_bs_vol= Slider(ax=ax_bs_vol, label='BS Vol', valmin=0.1, valmax=1.0, valinit=0.5, valfmt=' %1.2f ', facecolor='#cc7000')
s_ltv = Slider(ax=ax_ltv, label='LTV0', valmin=0.7, valmax=0.95, valinit=0.9, valfmt=' %1.2f', facecolor='#cc7000')

# Plot default data
t =  np.linspace(91,0,92)/365
Real_vol = 0.5
Growth_rate = 2
LTV0 = 0.9
bs_vol = 0.5
y_price = 1/35000
Collateral = 200
pool_init_x = 100
pool_init_y = 3500000
pool_init_wx = 0.5

_data = get_episode_full(t,y_price, Collateral, LTV0, bs_vol, Growth_rate, Real_vol, pool_init_x, pool_init_y, pool_init_wx)

x = _data.index.values
y1 = _data['y_price']/y_price
y2a = _data['w_ytoken']
y2b = 1-_data['w_ytoken']
y3 = _data['ltv']
y4 = _data['ltv_rebalance']
y5 = _data['pvcr']
y6 = _data['pvcr_rebalance']
y7a = _data['pool_chg_price_chg']
y7b = _data['pool_chg_wt_chg']
y8 = _data['wt_chg']
y9 = _data['pvcr_rebal_theory']

f_d1, = axs[0,0].plot(x, y1, linewidth=2.5)
f_d2a, = axs[0,1].plot(x, y2a, linewidth=2.5)
f_d2b, = axs[0,1].plot(x, y2b, linewidth=2.5)
f_d3, = axs[1,0].plot(x, y3, linewidth=2.5)
f_d4, = axs[1,1].plot(x, y4, linewidth=2.5)
f_d5, = axs[2,0].plot(x, y5, linewidth=2.5)
f_d6, = axs[2,1].plot(x, y6, linewidth=2.5)
f_d7a, = axs[0,2].plot(x, y7a, linewidth =2, label= 'poolchg_price_chg')
f_d7b, = axs[0,2].plot(x, y7b, linewidth =2, label= 'poolchg_wt_chg')
f_d7c, = axs[0,2].plot(x, y7a+y7b, linewidth =1, label= 'poolchg_combined', linestyle='--')
f_d8, = axs[1,2].plot(y8, y7b, marker= 'o', linestyle = 'None')
f_d9, = axs[2,2].plot(x, y9, linewidth =2.5)

axs[0,0].set_ylim(0.5,2.5)
axs[0,0].axhline(y = 1, color = 'r', linestyle = '--')
axs[0,1].set_ylim(-0.02,1.02)
axs[0,1].axhline(y = 0.5, color = 'r', linestyle = '--')
axs[1,0].set_ylim(0.5,2)
axs[1,0].axhline(y = 1, color = 'r', linestyle = '--')
axs[1,1].set_ylim(0.5,2)
axs[1,1].axhline(y = 1, color = 'r', linestyle = '--')
axs[2,0].set_ylim(0.5,2)
axs[2,0].axhline(y = 1, color = 'r', linestyle = '--')
axs[2,1].set_ylim(0,2)
axs[2,1].axhline(y = 1, color = 'r', linestyle = '--')
axs[0,2].set_ylim(-0.1,0.1)
axs[0,2].axhline(y = 0, color = 'r', linestyle = '--')
axs[1,2].set_ylim(-0.15,0.15)
axs[1,2].set_xlim(-0.5,0.5)

axs[1,2].axhline(y = 0, color = 'r', linestyle = '--')
axs[2,2].set_ylim(0.1,1.1)
axs[2,2].axhline(y = 1, color = 'r', linestyle = '--')

axs[0,0].set_ylabel('USDC rlt. price change')
axs[0,1].set_ylabel('Weights (USDC: Blue, BTC: Orange)')
axs[1,0].set_ylabel('Loan-to-Value-Theoratical')
axs[1,1].set_ylabel('LTV_rebalance')
axs[2,0].set_ylabel('PV as % of ICV - Theoratical')
axs[2,1].set_ylabel('PV - rebalance')
axs[0,2].set_ylabel('daily pool loss')
axs[1,2].set_ylabel('wt chg vs pool_chg_wt_chg ')
axs[2,2].set_ylabel('PV rebalance over Theoratical')
for i in range(3):
    for j in range(3):
        axs[i,j].set_xlabel('Day')
        axs[i,j].set_title('({})            '.format(3*i+j+1))
axs[1,2].set_xlabel('weight_chg')
axs[0,2].legend(loc="upper left")

# Update values
def update(val):
    _real_vol = s_real_vol.val
    _growth = s_growth.val
    _bs_vol = s_bs_vol.val
    _ltv = s_ltv.val

    #_data = gen_data_vec(t, BTC, Collateral, _ltv, _bs_vol, _growth, _real_vol)
    _data = get_episode_full(t,y_price, Collateral, _ltv, _bs_vol, _growth, _real_vol, pool_init_x, pool_init_y, pool_init_wx)

    x = _data.index.values
    _y1 = _data['y_price']/y_price
    _y2a = _data['w_ytoken']
    _y2b = 1-_data['w_ytoken']
    _y3 = _data['ltv']
    _y4 = _data['ltv_rebalance']
    _y5 = _data['pvcr']
    _y6 = _data['pvcr_rebalance']
    _y7a = _data['pool_chg_price_chg']
    _y7b = _data['pool_chg_wt_chg']
    _y8 = _data['wt_chg']
    _y9 = _data['pvcr_rebal_theory']

    f_d1.set_data(x,_y1)
    f_d2a.set_data(x,_y2a)
    f_d2b.set_data(x,_y2b)
    f_d3.set_data(x,_y3)
    f_d4.set_data(x,_y4)
    f_d5.set_data(x,_y5)
    f_d6.set_data(x,_y6)
    f_d7a.set_data(x,_y7a)
    f_d7b.set_data(x,_y7b)
    f_d7c.set_data(x,_y7a+_y7b)
    f_d8.set_data(_y8,_y7b)
    f_d9.set_data(x,_y9)
    fig.canvas.draw_idle()

s_real_vol.on_changed(update)
s_growth.on_changed(update)
s_ltv.on_changed(update)
s_bs_vol.on_changed(update)

plt.show()

```

    <IPython.core.display.Javascript object>

```python

```

### Simulation results:
For each selected combination (1) to (4), run the process 100 times and count the number of events that LVT(t) > 1 at any time point.  Report the frequency by LTV0 and growth rate for a given volatility setup.

For example this is the simulation results when growth rate volatility is high, and black sholes volatility is under-estimated.
The size of button indicate the chance of default when LVT is hight, even the growth rate is low.

```python
# simulation 2
def ltv_simulation(s, LTV0, bs_vol, Growth_rate, Real_vol):
    freq = 0
    for k in range(0,s):
        _out = gen_data_vec_btc(t, y_price, Collateral, LTV0, bs_vol, Growth_rate, Real_vol)
        freq+= (max(_out['ltv'])>=1)
    return np.array([freq/s, _out.pvcr.mean(), _out.pvcr.std()])
```

```python

# Generate whole simulation output table for different combinations of parameters

xyz = []
i =0
t = np.linspace(91,0,92)/365
np.random.seed(0)
y_price= 1/35000
Collateral = 200
for real_vol in np.linspace(0.1, 1.0, 10):
    for growth in np.linspace(0.3, 3, 10):
        for ltv in np.linspace(0.7, 0.95, 11):
            for bs_vol in np.linspace(0.1, 1.0, 10):
                if i%1000 == 0:
                    print(i)
                z = ltv_simulation(100, ltv, bs_vol, growth, real_vol)
                param = np.array([real_vol, growth, ltv, bs_vol])
                xyz.append(np.concatenate((param, z), axis = None))
                i+=1
_data = pd.DataFrame(np.array(xyz))
_data.columns = ['real_vol', 'growth', 'ltv', 'bs_vol', 'default_risk', 'pvcr_mean', 'pvcr_std']
# #_data.to_csv('./ALEX/simulation2_out.csv')

```

```python
# df_plot = pd.read_csv('./simulation2_out.csv')
# df_plot.drop('Unnamed: 0', 1, inplace=True)
# df_plot.describe()
df_plot = _data
```

```python
%matplotlib inline
from ipywidgets import interact, interactive, fixed, interact_manual
import ipywidgets as widgets
import matplotlib.pyplot as plt, random

def series(real_vol, bs_vol, target_var):
    _x, _y = 'growth', 'ltv'
    _df = df_plot[(df_plot.real_vol <real_vol+1e-4) &(df_plot.real_vol >real_vol-1e-4) \
                  & (df_plot.bs_vol< bs_vol+1e-4) & (df_plot.bs_vol>bs_vol-1e-4)]
    x, y, z = _df[_x], _df[_y], _df[target_var]*100
    sc = plt.scatter(x,y, s=z)
    plt.legend(*sc.legend_elements("sizes", num = 10))
    plt.xlabel('growth (of borrowed token price)')
    plt.ylabel('ltv')
    plt.xlim(0,4)
    plt.title(target_var)
    return()
plt.rcParams['figure.figsize'] = (10,6)
fig2 = interact(series, real_vol=(0.1, 1, 0.1), bs_vol = (0.1, 1, 0.1), target_var= ['default_risk'])
```

```python

# import matplotlib.pyplot as plt
# from matplotlib import cm
# import numpy as np
# from celluloid import Camera
# import matplotlib.animation

# camera = Camera(plt.figure())
# _x, _y = 'growth', 'ltv'
# i,j = 0,0
# for real_vol in np.linspace(0.1, 0.5, 11):
#     i += 1
#     for bs_vol in np.linspace(0.1, 0.5, 11):
#         j += 1
#         _df = df_plot[(df_plot.real_vol <real_vol+1e-4) &(df_plot.real_vol >real_vol-1e-4) \
#                       & (df_plot.bs_vol< bs_vol+1e-4) & (df_plot.bs_vol>bs_vol-1e-4)]
#         x, y, z = _df[_x], _df[_y], _df['default_risk']*100
#         sc = plt.scatter(x,y, s=z)
#         plt.legend(*sc.legend_elements("sizes", num = 10))
#         plt.xlabel('growth (of borrowed token price)')
#         plt.ylabel('ltv')
#         plt.title('Observed freq. of default (LTV>=1)')
#         plt.xlim(0,4)
#         plt.text(0.5, 0.8, "real_vol={:.2%}, bs_vol= {:.2%}".format(real_vol, bs_vol))
#         if i%3 ==0 & j%3 ==0:
#             camera.snap()
# camera.snap()
# anim = camera.animate()
# writergif = matplotlib.animation.PillowWriter(fps=3)
# anim.save('default_simulation.gif', writer=writergif)
# # numpoints = 10
# # points = np.random.random((2, numpoints))
# # colors = cm.rainbow(np.linspace(0, 1, numpoints))
# # camera = Camera(plt.figure())
# # for _ in range(100):
# #     points += 0.1 * (np.random.random((2, numpoints)) - .5)
# #     plt.scatter(*points, c=colors, s=100)
# #     camera.snap()

```

notebooks/ALEX_issue16-simulation-Vectorization - updates - spotprice .ipynb