# Project 4, deadline  April 30

 **Quantum Monte Carlo of confined electrons**

Date: **Spring semester 2017**

## Theoretical background and description of the physical system

The aim of this project is to use the Variational Monte Carlo (VMC)
method to evaluate the ground state energy, the relative distance between two electrons and
expectation values of the kinetic and potential energies of a quantum dots with $N=2$
electrons in three dimensions. It can be seen as a continuation of project 2, but instead of using eigenvalue solvers
we will use a stochastic approach.

The systems we will focus on is thus  a three-dimensional one, with two electrons
confined to move in harmonic oscillator like traps. As in project 2, we have analytical
results for the energy for specific frequencies, see Taut's article in the reference list below.
Furthermore, for other frequencies, we have our results from project 2
which can be used to test our Monte Carlo results.

These systems are called [quantum dots](http://en.wikipedia.org/wiki/Quantum_dot) and constitute a lively research
area in condensed matter physics and materials science, with
applications spanning from the contruction of quantum circuits to
applications to solar cells and nano-medicine. Although most studies are done for electrons in two dimensions,
we will limit ourselves to the three-dimensional case since it allows us to compare our variational Monte Carlo (VMC)
results with what we did in project 2.

Our aim here is to use the variational Monte Carlo method to study
such systems, with an emphasis on understanding correlations due to
the repulsive interaction between electrons.
The advantage of the VMC approach is that we can carry out our calculations using cartesian coordinates.

The relevant background material can be found in chapter 14 of the lecture notes.

We consider a system of electrons confined in a pure three-dimensional
isotropic harmonic oscillator potential, with an idealized  total Hamiltonian given by

$$
\begin{equation}
\label{eq:finalH} \tag{1}
  \hat{H}=\sum_{i=1}^{N} \left(  -\frac{1}{2} \nabla_i^2 + \frac{1}{2} \omega^2r_i^2  \right)+\sum_{i<j}\frac{1}{r_{ij}},
\end{equation}
$$

where natural units ($\hbar=c=e=m_e=1$) are used and all energies are
in so-called atomic units a.u. It means that all distances
$r_i$ and $r_{ij}$ are dimensionless.

We will study various trial wave functions for  two electrons
($N=2$) as functions of the oscillator frequency $\omega$ using the above
Hamiltonian.  The Hamiltonian includes a standard harmonic oscillator
part

$$
\hat{H}_0=\sum_{i=1}^{N} \left(  -\frac{1}{2} \nabla_i^2 + \frac{1}{2} \omega^2r_i^2  \right),
$$

and the repulsive interaction between two electrons given by

$$
\hat{H}_1=\sum_{i<j}\frac{1}{r_{ij}},
$$

with the distance between electrons given by $r_{ij}=\sqrt{\mathbf{r}_1-\mathbf{r}_2}$. We define the
modulus of the positions of the electrons (for a given electron $i$) as $r_i = \sqrt{x_i^2+y_i^2+z_i^2}$.

**For this project you can collaborate with fellow students and you can  hand in a common report.**
This project (together with projects 3 and 4) counts 1/3 of the final mark.

### Project 5a): Non-interacting system

We will first deal with a system of
  two electrons in a quantum dot with a frequency of $\hbar\omega = 1$.
  If we only include  the harmonic oscillator part of the Hamiltonian,
  the so-called unperturbed part,  for $N=2$

$$
\hat{H}_0=\sum_{i=1}^{N} \left(  -\frac{1}{2} \nabla_i^2 + \frac{1}{2} \omega^2r_i^2  \right),
$$

the exact energy is $3$ a.u.  This serves as an excellent benchmark when we develop our code.
  The wave function for one electron in an oscillator potential in three dimensions is

$$
\phi_{n_x,n_y,n_z}(x,y,z) = A H_{n_x}(\sqrt{\omega}x)H_{n_y}(\sqrt{\omega}y)H_{n_z}(\sqrt{\omega}z)\exp{(-\omega(x^2+y^2+z^2)/2}.
$$

The functions $H_{n_x}(\sqrt{\omega}x)$ etc are so-called Hermite polynomials, discussed below  here while $A$ is a normalization constant.
  For the lowest-lying state we have $n_x=n_y=n_z=0$ and an energy $\epsilon_{n_x,n_y,n_x}=\omega(n_x+n_y+n_z+3/2) = 3/2\omega$.
  Convince yourself that the lowest-lying energy for the two-electron system  is simply $3\omega$. This results provides a useful benchmark for your code.

  The unperturbed wave function for the ground state of the two-electron system is given by

$$
\Phi(\mathbf{r}_1,\mathbf{r}_2) = C\exp{\left(-\omega(r_1^2+r_2^2)/2\right)},
$$

with $C$ being a normalization constant and $r_i = \sqrt{x_i^2+y_i^2+z_i^2}$. Note that the vector $\mathbf{r}_i$
  refers to the $x$, $y$ and $z$ coordinates for a given particle.
  What is the total spin of this wave function? Find arguments for why the ground state should have
  this specific total spin.

### Project 5b): Analytical form for the trial wave functions and local energy

Find closed form expressions for the local energy (see below) for the
two trial wave functions presented here and explain shortly if these
trial functions satisfy
the so-called cusp condition when $r_{12}\rightarrow 0$.
The first wave function

$$
\Psi_{T1}(\mathbf{r}_1,\mathbf{r}_2) = C\exp{\left(-\alpha\omega(r_1^2+r_2^2)/2\right)},
$$

while the second trial function is

$$
\Psi_{T2}(\mathbf{r}_1,\mathbf{r}_2) =
    C\exp{\left(-\alpha\omega(r_1^2+r_2^2)/2\right)}
    \exp{\left(\frac{r_{12}}{2(1+\beta r_{12})}\right)},
$$

where $\alpha$ and $\beta$ are variational parameters.
Show that the first trial function gives an analytical expression for the local energy given by
gives a closed-form expression

$$
E_{L1} = \frac{1}{2}\omega^2\left( r_1^2+r_2^2\right)\left(1-\alpha^2\right) +3\alpha\omega.
$$

Use this expression when developing your program.
For the first trial function it will give you the exact analytical result when you exclude the Coulomb interaction. The exact result for the ground state is then $3\omega$.
Adding the repulsive Coulomb interaction gives us then

$$
E_{L1} = \frac{1}{2}\omega^2\left( r_1^2+r_2^2\right)\left(1-\alpha^2\right) +3\alpha\omega+\frac{1}{r_{12}}.
$$

When you study the final energy for the first trial function, this is the result you should compare the second trial function with.
The analytical expression for the second trial wave function (with $E_{L1}$ now including the Coulomb repulsion)

$$
E_{L2} = E_{L1}+\frac{1}{2(1+\beta r_{12})^2}\left\{\alpha\omega r_{12}-\frac{1}{2(1+\beta r_{12})^2}-\frac{2}{r_{12}}+\frac{2\beta}{1+\beta r_{12}}\right\}
$$

The exact ground state energy for $\omega =1 $ is $3.558$ a.u. Check this result against your results from project 2 and remember to add the contribution from the center-of-mass energy, which is 1.5 a.u.

You will need to program both equations when computing the expectation value of the energy and the energy variance.

### Project 5c):

We want to perform  a Variational Monte Carlo calculation of the ground state of two electrons in a quantum dot well with different oscillator energies, assuming total spin $S=0$.
Compute then

$$
\langle H \rangle = \frac{\int d{\bf
R}\Psi^{\ast}_T({\bf R})H({\bf R})\Psi_T({\bf R})}
{\int d{\bf
R}\Psi^{\ast}_T({\bf R})\Psi_T({\bf R})},
$$

for the two-electron
system using a variational Monte Carlo method employing the Metropolis
algorithm to sample over different states.  You will have to calculate

$$
\langle H \rangle =\int P({\bf R})E_L({\bf R})d{\bf R},
$$

where $E_L$ is the local energy.  Here all
calculations are performed with the trial wave function
$\psi_{T1}({\bf r_1},{\bf r_2}, {\bf r_{12}})$ only.  Study the
stability of your calculation as function of the number of Monte Carlo
cycles and compare these results with the exact results from project
2 (remember to add the center-of-mass energy for the results from project 2)
Your Monte Carlo moves are determined by

$$
{\bf R}' = {\bf R} +\delta\times r
$$

where $r$ is a random number from the uniform distribution and
$\delta$ a chosen step length.  In solving this
		       exercise you need to devise an algorithm which
		       finds an optimal value of $\delta$ for each
		       variational parameter $\alpha$, resulting in
		       roughly $50\%$ accepted moves.

Plot the energy as a function of the variational parameter $\alpha$ and discuss your results. Plot the variance as well
and find the energy and variance minima as functions of $\alpha$. Discuss your results.

Compute also the expectation value of the mean distance at the energy minimun
  $r_{12}=\sqrt{\mathbf{r}_1-\mathbf{r}_2}$ between the two electrons for the optimal set of the variational parameters using $\omega=0.01$,
  $\omega=0.5$ and $\omega=1.0$.  Comment your results.
You will find it convenient to object orient your code.

### Project 5d):

Use thereafter the  optimal value for  $\alpha$ as a starting point for
computing the ground state energy of the two-electron quantum dot using the trial wave functions
$\psi_{T2}({\bf r_1},{\bf r_2}, {\bf r_{12}})$. Use the analytical expression for the local energy.
In this case you need to vary both $\alpha$ and $\beta$.
The strategy here is to use
$\alpha$ from the previous exercise, [5c)] and then
vary $\beta$ in order to find the lowest energy as function of
$\beta$. Thereafter you change
$\alpha$ in order to see whether you find an even lower energy and so forth.

With the optimal parameters for the ground state wave function, compute the expectation values of the  energy and the variance using $\omega=0.01$,
  $\omega=0.5$ and $\omega=1.0$. How important are the correlations introduced by the Jastrow factor? Comment your results. Compare the best VMC results wth those from project 2 for the same frequencies.  Compute also the expectation value of the mean distance at the energy minimun
  $r_{12}=\sqrt{\mathbf{r}_1-\mathbf{r}_2}$ between the two electrons for the optimal set of the variational parameters using $\omega=0.01$,
  $\omega=0.5$ and $\omega=1.0$.  Comment your results and compare the results with and without the Jastrow factor.

### Project 5e):

Finally, we want to test the virial theorem for a range of frequencies. The virial theorem states that the expectation value of the total kinetic energy $\langle T \rangle$ is proportional to the expectation value of the total potential energy $\langle V \rangle$. For a pure harmonic oscillator this proportionality is given by

$$
\langle T \rangle= \langle V \rangle.
$$

Use your optimal results for $\omega=0.01$,
  $\omega=0.5$ and $\omega=1.0$ and add to these other values in the range  $\omega\in [0.01,1]$
  and   compute the expectation value of the kinetic energy and potential energy with and without the
  repulsive interaction between electrons for $N=2$. Plot the
  ratio $\langle T \rangle/\langle V \rangle$ as function of
  $\omega$ and comment your results.

## Background literature

   * [M. Taut, Phys. Rev. A 48, 3561 (1993)](http://journals.aps.org/pra/abstract/10.1103/PhysRevA.48.3561).

   * B. L. Hammond, W. A. Lester and P. J. Reynolds, Monte Carlo methods   in Ab Initio Quantum Chemistry, World Scientific, Singapore, 1994, chapters   2-5 and appendix B.

## Additional material on Hermite polynomials

The Hermite polynomials are the solutions of the following differential
equation

$$
\begin{equation}
  \frac{d^2H(x)}{dx^2}-2x\frac{dH(x)}{dx}+
  (\lambda-1)H(x)=0.
\label{eq:hermite} \tag{2}
\end{equation}
$$

The first few polynomials are

$$
H_0(x)=1,
$$

$$
H_1(x)=2x,
$$

$$
H_2(x)=4x^2-2,
$$

$$
H_3(x)=8x^3-12x,
$$

and

$$
H_4(x)=16x^4-48x^2+12.
$$

They fulfil the orthogonality relation

$$
\int_{-\infty}^{\infty}e^{-x^2}H_n(x)^2dx=2^nn!\sqrt{\pi},
$$

and the recursion relation

$$
H_{n+1}(x)=2xH_{n}(x)-2nH_{n-1}(x).
$$

## Introduction to numerical projects

Here follows a brief recipe and recommendation on how to write a report for each
project.

  * Give a short description of the nature of the problem and the eventual  numerical methods you have used.

  * Describe the algorithm you have used and/or developed. Here you may find it convenient to use pseudocoding. In many cases you can describe the algorithm in the program itself.

  * Include the source code of your program. Comment your program properly.

  * If possible, try to find analytic solutions, or known limits in order to test your program when developing the code.

  * Include your results either in figure form or in a table. Remember to        label your results. All tables and figures should have relevant captions        and labels on the axes.

  * Try to evaluate the reliabilty and numerical stability/precision of your results. If possible, include a qualitative and/or quantitative discussion of the numerical stability, eventual loss of precision etc.

  * Try to give an interpretation of you results in your answers to  the problems.

  * Critique: if possible include your comments and reflections about the  exercise, whether you felt you learnt something, ideas for improvements and  other thoughts you've made when solving the exercise. We wish to keep this course at the interactive level and your comments can help us improve it.

  * Try to establish a practice where you log your work at the  computerlab. You may find such a logbook very handy at later stages in your work, especially when you don't properly remember  what a previous test version  of your program did. Here you could also record  the time spent on solving the exercise, various algorithms you may have tested or other topics which you feel worthy of mentioning.

## Format for electronic delivery of report and programs

The preferred format for the report is a PDF file. You can also use DOC or postscript formats or as an ipython notebook file.  As programming language we prefer that you choose between C/C++, Fortran2008 or Python. The following prescription should be followed when preparing the report:

  * Use your github repository to upload your report. Indicate where the report is by creating for example a **Report** folder. Please send us as soon as possible your github username.

  * Place your programs in a folder called for example **Programs** or **src**, in order to indicate where your programs are. You can use a README file to tell us how your github folders are organized.

  * In your git repository, please include a folder which contains selected results. These can be in the form of output from your code for a selected set of runs and input parameters.

  * In this and all later projects, you should include tests (for example unit tests) of your code(s).

  * Comments  from us on your projects, with score and detailed feedback will be emailed to you.

Finally,
we encourage you to work two and two together. Optimal working groups consist of
2-3 students. You can then hand in a common report.


# Readability Exercise

Welcome! Below you will implement two metrics for evaluating the readability of documents:

1. Flesch–Kincaid readability Grade Index
2. Gunning Fog Index

The solutions are in [readability_solutions.py](./readability_solutions.py). You can also click the jupyter icon to see all the files in the folder.

To load all the functions in the solutions, simply include `from solutions import *`

## 0. Initialization

Let's read-in our text files. We have three different texts files to play with:

1. `physics.txt`: taken from a technical wikipedia article about a theoretical physics idea called [Supersymmetry](https://en.wikipedia.org/wiki/Supersymmetry)

2. `APPL_10k_2017.txt`: the 2017 10-K Item IA for APPLE INC, taken from the EDGAR website

3. `alice.txt`: Excerpts from "Alice in Wonderland", the novel is in the public domain and freely available

```python
# download some excerpts from 10-K files

from download10k import get_files

CIK = {'ebay': '0001065088', 'apple':'0000320193', 'sears': '0001310067'}
get_files(CIK['ebay'], 'EBAY')
get_files(CIK['apple'], 'AAPL')
get_files(CIK['sears'], 'SHLDQ')

```

```python
# sentences separated by ; are better viewed as multiple sentences
# join combines all the newlines in the file

f = open("physics.txt", "r")
text_phy=''.join(f).replace(';','.')

f = open("alice.txt", "r")
text_alice=''.join(f).replace(';','.')

f = open("AAPL_10k_2017.txt", "r")
text_10k=''.join(f).replace(';','.')

```

```python
# check out some of the texts
print(text_10k[:500]+"...\n")
print(text_phy[:500]+"...\n")
print(text_alice[:500]+"...\n")
```

## 1. Pre-processing
Here, we need to define functions that can split our texts into sentences, and split our sentences into words

```python
# tokenize and clean the text
import nltk
from nltk.stem import WordNetLemmatizer, SnowballStemmer
from collections import Counter
from nltk.corpus import stopwords

from nltk import word_tokenize
from syllable_count import syllable_count

nltk.download('wordnet')

sno = SnowballStemmer('english')
wnl = WordNetLemmatizer()

from nltk.tokenize import RegexpTokenizer

# tokenizer that selects out non letter and non symbol (i.e. all alphabets)
word_tokenizer = RegexpTokenizer(r'[^\d\W]+')

def word_tokenize(sent):
    return [ w for w in word_tokenizer.tokenize(sent) if w.isalpha() ]

# for the sentence tokenizer
nltk.download('punkt')
from nltk.tokenize import sent_tokenize

# you can tokenize sentences by calling
# sent_tokenize(document)

# and tokenize words by calling
# word_tokenize(sentence)

# syllable_count counts the number of syllables in a word
# it's included in syllable_count.py
```

Now implement three functions

1. `sentence_count`: a simple function that returns the number of sentences in a document

2. `word_count`: a simple function that returns the number of words in a sentence

3. `hard_word_count`: a simple function that returns the number of words with more than 3 syllables, while removing suffixes like "-ed", and "-ing". This can be done by lemmatizing the word, i.e. `wnl.lemmatize(word, pos='v')`

the function `word_tokenize(sentence)` will be useful here

```python
def sentence_count(text):
    # TO DO
    pass

def word_count(sent):
    # TO DO
    pass

def hard_word_count(sent):
    # TO DO
    pass

```

## 2. Readability Grade-Levels

Here, you will implement the two readability indices (grade levels). They are defined by

\begin{align}
\textrm{Flesch–Kincaid Grade}
= 0.39 \left(
\frac{\textrm{Number of words}}{\textrm{Number of sentences}}
\right) \\
+11.8
\left(
\frac{\textrm{Number of hard words}}{\textrm{Number of words}}
\right)
-15.59
\end{align}

and

\begin{align}
\textrm{Gunning-Fog Grade}
=\; &0.4 \bigg[
\left(
\frac{\textrm{Number of words}}{\textrm{Number of sentences}}
\right)
+100
\left(
\frac{\textrm{Number of syllables}}{\textrm{Number of words}}
\right)
\bigg]
\end{align}

To count syllables, we've added a syllable_count function you can access via

```
from syllable_count import syllable_count
syllable_count("syllable")
```

Below, implement the function `flesch_index` and `fog_index` that computes the readability grade level for a given text.

```python
import numpy as np

def flesch_index(text):
    # TO DO
    pass

def fog_index(text):
    # TO DO
    pass
```

## 3 Results

Now that you've coded up the exercises, compute the grade levels for each of the texts given.

```python
# to test the solutions
# uncommon next line
# from readability_solutions import *

print(flesch_index(text_alice),fog_index(text_alice))
print(flesch_index(text_phy),fog_index(text_phy))
print(flesch_index(text_10k),fog_index(text_10k))
```

You should expect a grade level around 7-10 for `alice.txt`, and around 16-19 for `physics.txt`, and 18+ for financial documents!

It turns out 10-Ks are really *hard* to read legal documents!
Now, let's compute the readability for all the 10-Ks we have

```python
filelist_10k=!ls *10k*txt

flesch = []
fog = []

for file in filelist_10k:
    with open(file, 'r') as f:
        text=''.join(f).replace(';','.')
        flesch.append(flesch_index(text))
        fog.append(fog_index(text))
        print(file, flesch[-1],fog[-1])
```

Superficially, and according to our readability metrics, reading 10-Ks is harder than reading articles on theoretical physics!

# Bonus exercise:
How are the two readability grade-levels correlated? Compute the covariance matrix of the two readability indices we have on all the 10K documents, and make a scatter plot of Flesch index vs Fog index. Also perform a least-squared fit to the result and plot it as well.

(change bottom cell to code and remove html tags for solution)

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

# TO DO
```

<font color="white">

#solution
cov = np.cov(flesch,fog)
print(cov)

%matplotlib inline

plt.figure(figsize=(6,5))
plt.scatter(flesch,fog)

slope, intercept, r_value, p_value, std_err = linregress(flesch, fog)

x=np.linspace(16.5,19,101)
y=slope*x+intercept
plt.plot(x,y)

plt.xlabel("Flesch Index")
plt.ylabel("Fog Index")

</font>


\begin{align}
    \frac{d T_R}{dt} = \frac{F}{V}(T_{\alpha} - T_R) + \frac{Q}{V\,\rho\,c_p}
\end{align}

\begin{align}
    s\,Y(s) = \frac{F}{V}\,T_\alpha - \frac{F}{V}\,Y(s) + \frac{1}{V\,\rho\,c_p}\,U(s)
\end{align}

\begin{align}
    \dot{x} = \begin{pmatrix} 0 & 0 \\ \frac{F}{R} & -\frac{F}{R} \end{pmatrix}\,x + \begin{pmatrix} 0 \\ \frac{1}{V\,\rho\,c_p} \end{pmatrix}\,u \\
    y = \begin{pmatrix} 0 & 1 \end{pmatrix}\,x
\end{align}

```python
import control

A = np.array([[0, 0], [F/V, -F/V]])
B = np.array([[0], [1/(V*rho*c_p)]])
C = np.array([0, 1])
D = 0
sys = control.ss(A, B, C, D)
```

```python
G = control.ss2tf(sys)
```

```python
G
```

$$\frac{0.02396}{s + 0.015}$$

```python
1/0.015
```

    66.66666666666667

```python
s = control.tf([1, 0], [1])
K = K_P + K_P/(T_I*s)
K
```

$$\frac{29.21 s + 0.626}{46.67 s}$$

```python
L = K * G
L_bode = control.bode_plot(L)
```

```python
T = L/(1+L)
T_step = control.step_response(T)
plt.plot(T_step[0], T_step[1])
```

```python
wU = control.tf([20], [1])
M = 1.5
omegaB = 1e2
A = 1e-4
wP = (s/M + omegaB) / (s + omegaB*A)
K = control.mixsyn(G, wP, wU, wU)
```

```python
K_tf = control.tf(K[0])
K_tf
```

$$\frac{9.654e+10 s + 1.448e+09}{s^2 + 6.769e+09 s + 6.769e+07}$$

```python
L = K_tf * G
T = L/(1+L)
S = 1 - T
T_step = control.step_response(T)
KS_step = control.step_response(K_tf*S)
plt.plot(T_step[0], T_step[1])
plt.plot(KS_step[0], KS_step[1])
```

@book{ Baehr2013,
    title = "Wärme- und Stoffübertragung",
    title_sort = "Wärme- und Stoffübertragung",
    author_sort = "Baehr, Hans D. \& Stephan, Karl",
    author = "Hans D. Baehr and Karl Stephan",
    note = "Dieses bereits in dritter Auflage vorliegende Lehrbuch ist eine umfassende und gründliche Darstellung der Wärme- und Stoffübertragung. Ihre Theorie wird systematisch entwickelt, und die Lösungsmethoden aller wichtigen Probleme werden ausführlich behandelt. Daher eignet sich dieses Buch nicht nur für Studenten, sondern nützt auch Wissenschaftlern und Ingenieuren in der Praxis als zuverlässiges Nachschlagewerk. Alle Gebiete der Wärme- und Stoffübertragung werden dargestellt: Wärmeleitung und Diffusion, konvektiver Wärme- und Stoffaustausch, Wärmetransport beim Kondensieren und Verdampfen, die Wärmestrahlung sowie die Berechnung der Wärme- und Stoffübertragung. Zahlreiche durchgerechnete Beispiele sowie Übungsaufgaben und Lösungen erleichtern das Verständnis.
",
    cover = "/Users/felixfebrian/Google Drive/CalibreLibrary/Hans D. Baehr/Warme- und Stoffubertragung (92)/cover.jpg",
    formats = "pdf",
    calibreid = "92",
    isbn = "978-36-6210-835-2",
    library_name = "CalibreLibrary",
    year = "2013",
    month = "Apr",
    publisher = "Springer-Verlag",
    volume = "1",
    size = "21403691 octets",
    tags = "Science, Mechanics, Thermodynamics, Physics, Condensed Matter, Chemistry, Industrial \& Technical, General",
    timestamp = "2020-06-07",
    uuid = "b8143a2c-3266-405c-98fb-7c8aeea95ea8",
    languages = "deu",
    identifiers = "google:QGS0BgAAQBAJ,isbn:9783662108352" }

```python

```

```python

```


```python
import matplotlib.pyplot as plt
plt.style.use('classic')
%matplotlib inline
```

# Discussion: Week 2

```python
# Import the NumPy module
import numpy as np
```

## Exercise: Capital Evolution in the Solow Model

Suppose that capital per worker $k_t$ evolves according to the following equation:

\begin{align}
k_{t+1} & = 0.12 \cdot 100 \cdot k_t^{1/3} + 0.9\cdot k_t, \tag{1}
\end{align}

where the first term on the right-hand side implies that the economy has a 12 percent savings rate, that total factor productivity equals 100, and that there is no growth in technology (or "labor efficiency"). The second term implies that the rate of capital depreciation is 10 percent (i.e., $1-\delta = 0.9 \Rightarrow \delta = 0.1$). Assume that capital per worker in the initial period $k_0$ is given.

The *steady state* quantity of capital per worker is the number $k^*$ such that if $k_t = k^*$, $k_{t+1} = k^*$. Find $k^*$ by dropping the time subscripts in equation (1) and solving for $k$. Obtain:

\begin{align}
k^* & = \left(\frac{0.1}{0.12\cdot 100}\right)^{3/2} = 1{,}314.53414 \tag{2}
\end{align}

### Part (a): Simulate 100 Periods

```python
# Create a variable called 'k0' that stores the initial quantity of capital in the economy. Set 'k0' to 400
k0 = 400

# Create a variable called 'T' equal to the number of periods after 0 to simulate. Set T = 100
T = 100

# Use the function np.zeros to create a variable called 'capital' equal to an array of zeros of length T+1
capital = np.zeros(T+1)

# Print the value of 'capital'
print(capital)
```

    [0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
     0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
     0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
     0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.
     0. 0. 0. 0. 0.]

```python
# Set the first element of 'capital' to the value in k0
capital[0] = k0

# Print the value of 'capital'
print(capital)
```

    [400.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.
       0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.
       0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.
       0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.
       0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.
       0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.
       0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.   0.
       0.   0.   0.]

```python
# Use a for loop to iterate over the additional elemnts of the 'capital' array that need to be computed.
# Hint: capital has length T+1. The first value is filled, so you need fill the remaining T values.
for t in np.arange(0,T):
    capital[t+1] = 0.12*100*capital[t]**(1/3) + 0.9*capital[t]

# Print the value of 'capital'
print(capital)
```

    [ 400.          448.41675597  495.42424066  540.83444627  584.52034406
      626.40223186  666.43723721  704.61114237  740.93195374  775.4248048
      808.12789312  839.08922871  868.36402523  896.01260477  922.09871546
      946.68818293  969.84783273  991.64463349 1012.14502031 1031.41436557
     1049.51657041 1066.51375499 1082.46602951 1097.43133129 1111.46531557
     1124.62128999 1136.95018439 1148.50054886 1159.31857453 1169.44813202
     1178.93082386 1187.80604749 1196.11106608 1203.88108506 1211.14933252
     1217.94714186 1224.30403557 1230.24780921 1235.80461464 1240.99904199
     1245.85419985 1250.39179329 1254.63219944 1258.59454041 1262.29675337
     1265.75565785 1268.98701999 1272.00561392 1274.82528017 1277.45898122
     1279.91885426 1282.21626108 1284.36183543 1286.36552773 1288.23664726
     1289.98390207 1291.61543649 1293.13886659 1294.56131345 1295.88943451
     1297.12945307 1298.28718594 1299.36806948 1300.377184   1301.31927667
     1302.19878305 1303.01984716 1303.78634044 1304.50187942 1305.16984232
     1305.79338456 1306.37545328 1306.91880101 1307.42599837 1307.89944599
     1308.34138567 1308.75391082 1309.1389762  1309.49840706 1309.83390766
     1310.14706924 1310.43937751 1310.71221957 1310.96689047 1311.20459925
     1311.42647467 1311.63357048 1311.82687045 1312.00729293 1312.17569525
     1312.3328777  1312.47958739 1312.61652173 1312.7443317  1312.86362503
     1312.97496896 1313.078893   1313.17589142 1313.26642559 1313.35092616
     1313.42979511]

```python
# Print the value of  the last element of 'capital'
print(capital[-1])
```

    1313.429795111732

```python
# Plot the simulated capital per worker
plt.plot(capital)
plt.title('Simulated capital per worker')
plt.grid()
```

### Part (b): Simulate 1,000 Periods

```python
# Create a variable called 'T' equal to the number of periods after 0 to simulate. Set T = 1000
T = 1000

# Use the function np.zeros to create a variable called 'capital' equal to an array of zeros of length T+1
capital = np.zeros(T+1)

# Set the first element of 'capital' to the value in k0
capital[0] = k0

# Use a for loop to iterate over the additional elemnts of the 'capital' array that need to be computed.
for t in np.arange(0,T):
    capital[t+1] = 0.12*100*capital[t]**(1/3) + 0.9*capital[t]

# Print the value of  the last element of 'capital'
print(capital[-1])
```

    1314.534138012397

```python
# Plot the simulated capital per worker
plt.plot(capital)
plt.title('Simulated capital per worker')
plt.grid()
```

### Part (c): Evaluation

Provide answers to the following questions in the next cell.

**Question**

1. Why is the final value of capital computed in Part (b) closer to the true steady state than the value computed in Part (a)?

**Answer**

1. Part (b) iterated on the capital evolution equation longer and therefore the capital stock had more "time" to approach the steady state. <!-- answer -->


# 循环神经网络

作者：杨岱川

时间：2020年03月

github：https://github.com/DrDavidS/basic_Machine_Learning

开源协议：[MIT](https://github.com/DrDavidS/basic_Machine_Learning/blob/master/LICENSE)

参考文献：

- 《深度学习》，作者：Ian Goodfellow 、Yoshua Bengio、Aaron Courville。
- [CS231n](http://cs231n.stanford.edu/)
- 《动手学深度学习》，作者：李沐、阿斯顿·张

## 本节目的

多层感知机和卷积神经网络能够有效处理空间信息，而循环神经网络是为了更好地处理时序信息而设计。循环神经网络引入状态变量来保存过去的信息，然后与当前的输入共同决定当前的输出。

本节我们会学习循环神经网络的原理和使用方法。

## 序列数据与语言模型

首先我们会举例说明什么事序列数据，然后简单讲述一下语言模型。

### 序列数据是什么

一段文字、一段声音，或者电影胶片中前后相接的画面顺序，都是序列数据。可以认为有一些时间顺序和规律存在其中。

拿文字来说明，有这么一句话：

> 今天的天气真好呀

这句话就能被看做是一段离散的时间序列数据。这句话共有 $T = 8$ 个字，按索引来说，每个字我们表示为 $w_1, w_2, \cdots, w_T$，自然有

> $w_1 = 今， w_2 = 天， \cdots，w_8 = 呀$

这样的一个序列可以用概率来表示，如 $P(w_1,w_2,\cdots,w_T)$，称为序列的概率。

在深度学习领域，对于语言文字的处理，或者叫 **自然语言处理（NLP）** 是非常热门的方向之一。在这个领域处理的数据大多都是类似的序列数据。

### 语言模型

**语言模型（Language Model）** 是自然语言处理的重要技术。上文中序列的概率 $P(w_1,w_2,\cdots,w_T)$ 就是通过语言模型计算出来的。

语言模型的应用十分广泛，比如“谷歌翻译”、“百度翻译”或者是一些智能问答系统等产品中都会使用语言模型来建模。

比如在英文翻译中常见的笑话：

> “How old are you？”

如果字对字地翻译会成为：

> “怎么老是你？”

当然按句意翻译实际应该是：

> “你多大了？”

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

语言模型会针对这样的翻译文本计算一个概率。如果算出 “你多大了” 这个翻译结果的概率 $P$ 最大，那么我们就将

> “How old are you？”

翻译为

> “你多大了？”。

### 语言模型的建立

语言模型会怎么计算概率呢？这里我们要给出一个假设：

> 在序列 $w_1, w_2, \cdots, w_T$ 中，每个字（每个词）是依次生成的。

就如我们看书和说话一般，说了上一个字才说下一个字。

所以我们有：

$$\large P(w_1, w_2, \cdots, w_T) = \prod_{t=1}^{T}P(w_t|w_1,\cdots,w_{t-1})$$

比如有这么一句话 $S$:

> $S = 天气真好$

则我们有：

> $w_1 = 天， w_2 = 气， w_3 = 真，w_4 = 好$

那么这句话的概率就是：

$$\large P(S)=P(w_1, w_2, w_3, w_4)=P(w_1)P(w_2|w_1)P(w_3|w_1,w_2)P(w_4|w_1,w_2,w_3)$$

需要说明的是，对于中文不一定是**按字分割**，传统做法更多是**按词分割**，比如 $w_1=天气$ ，$w_2=真$，$w_3=好$。

### 语言模型的计算

建立好语言模型以后，我们需要实际进行计算。

那么，$P(w_1=天)$ 到底等于多少呢？

假设我们有一个大型的文本语料库，比如百度百科或者别的什么数据集，我们可以从中计算出“天”字出现的次数占语料库总字数比例。

比如语料库总共有 10000 个字（实际通常多得多），天字有 33 个。那么 ：

$$\large P(w_1=天) = 0.0033$$

接下来是 $P(w_2|w_1)$：

> $P(w_2=气|w_1=天)$ ：第一个字是“天”的情况下，第二个字是“气”的概率。

所以我们要统计在语料库的 33 个天字后面，有多少跟着“气”字。

假定有 5 个“天气”吧，所以有：

$$\large P(w_2=气|w_1=天) = 0.1515$$

以此类推，我们可以知道 $P(w_3|w_1,w_2)$ 就是要计算 “天气” 后面跟着 “真” 字的概率。

最后根据上面的公式，我们就能得到 $P(w_1, w_2, w_3, w_4)$ 的概率了。

### n元语法

当序列长度非常长的时候，计算和储存多个字（词）的概率会非常非常复杂，所以人们发明了 **n元语法（n-gram grammar）** 来简化语言模型的计算。

> **马尔科夫假设（Markov Assumption）**：一个字（词）的出现只和前面 $n$ 个字（词）有关。

这也叫做 **$n$ 阶马尔科夫链**。

如果 $n=1$，那么有 $P(w_3|w_1,w_2)=P(w_3|w_2)$。

如果基于 $n-1$ 阶马尔科夫链，我们可以把语言模型改写为

$$\large(w_1,w_2,\cdots,w_T) \approx \prod_{t=1}^{T}P(w_t|w_{t-(n-1)},\cdots,w_{t-1})$$

特别说明，当 $n$ 分别为 1、2、3 的时候，我们将其称为一元语法（unigram）、二元语法（bigram）、三元语法（trigram）。

以二元语法为例，之前的例子可以化简为：

$$\large P(w_1, w_2, w_3, w_4)=P(w_1)P(w_2|w_1)P(w_3|w_2)P(w_4|w_3)$$

当然我们要明白：

>当 $n$ 较小的时候，通常并不准确；而当 $n$ 较大的时候，计算和存储又会相当复杂，参数数量呈指数级增长。

## 循环神经网络

为了解决 n元语法 的问题，我们会介绍**循环神经网络**。

循环神经网络并非刚性地几亿所有固定长度的序列，而是通过隐藏状态来存储之前时间步的信息。

### 隐藏状态与展开

与普通的神经网络不同，循环神经网络增加了一个隐藏状态 $h$ 来记录之前时间步的信息。

> “时间步”不一定是字面上的现实世界中流逝的时间，有时候它仅仅表示序列中的位置。

我们可以用下式来表示隐藏状态的变化：

$$\large h^{(t)} = f \left( h^{(t-1)}, x^{(t)}; \theta \right)$$

其中，

- $h^{(t)}$ 表示 $t$ 时间步的隐藏状态；
- $x^{(t)}$ 表示 $t$ 时间步的输入；
- $\theta$ 表示函数的相关参数。

当然这么写可能不是特别明确，因为这个函数是一个**抽象函数**的写法。我们需要看着相关的结构图来理解。

首先我们看一个没有输出的循环网络：

在这个网络中，只处理来自输入 $x$ 的信息，然后将其合并到经过时间想去传播的状态 $h$ 。

图片左边是回路的**原理图**，方块表示单个时间步的延迟。

右边的同一网络被视为**展开的计算图**，其中每个节点现在与一个特定的时间实例相关联。

具体来说，原理图中的橙色方块表示时刻 $t$ 的状态到时刻 $t+1$ 的状态之间的相互作用，具体的数学表达式我们会在后面介绍。

而在展开图中，每个时间步的每个变量被绘制为图中的一个独立节点。至于展开图应该有多长，实际上取决于**序列的长度**。

我们用一个函数 $g^{(t)}$ 代表经过 $t$ 步展开后的循环：

$$
\large
\begin{equation}\begin{split}
h^{(t)} &= g^{(t)}\left(x^{(t)}, x^{(t-1)}, x^{(t-2)}, \cdots, x^{(2)}, x^{(1)} \right)\\
&= f\left( h^{(t-1)}, x^{(t)};\theta \right)
\end{split} \end{equation}$$

函数 $g^{(t)}$ 将全部的过去序列 $\left(x^{(t)}, x^{(t-1)}, x^{(t-2)}, \cdots, x^{(2)}, x^{(1)} \right)$ 作为输入来生成当前状态。在展开的计算图中，我们用更简单的方法，也就是吧 $g^{(t)}$ 分解为函数 $f$ 的重复应用。

展开过程的优点主要有两个：

- 无论序列的长度，学成的模型始终具有相同的输入大小，即上个时间步的隐藏状态和当前时间步的输入
- 我们可以在每个时间步使用相同参数的相同转移函数 $f$

### 循环神经网络的结构

基于上一节的**图展开**和**参数共享**的思想，我们可以继续学习现在最常用的 RNN 形式。

实际上 RNN 的具体形式或者构造种类非常多，这里我们只介绍最常见的 RNN 形式，其他的部分 RNN 形式可以参考《深度学习》 10.2 节“循环神经网络”。

下图是当前最具有代表性的 RNN 例子：

其中：

- 损失 $L$ 衡量每个输出 $o$ 与相应训练目标真实值 $y$ 的距离；
- RNN 输入到隐藏的连接由权重矩阵 $U$ 参数化；
- 隐藏到隐藏的循环连接由权重矩阵 $W$ 参数化；
- 隐藏到输出的连接由权重矩阵 $V$ 参数化。

现在我们来研究一下这个图中 RNN 的前向传播公式。

首先，此图中没有指定隐藏单元的**激活函数**，我们假定它用的是双曲正切函数作为激活函数。

其次，此图中没有指定**输出**和**损失函数**，我们假定输出是离散的，比如这是一个用于预测字符、词语的 RNN。

那么，表现离散变量的常用方式（回想一下分类任务是怎么做的），就是把输出 $o$ 作为每个离散变量可能值的非标准化对数概率。然后，我们可以应用 `softmax` 函数处理，获得标准化后概率的输出向量 $\hat y$。

RNN 从特定的初始状态 $h^{(0)}$ 开始前向传播。从 $t=1$ 到 $t=\tau$ 的每个时间步，我们有如下更新方程：

$$
\large
\begin{equation}\begin{split}
a^{(t)} &= b + Wh^{(t-1)} + Ux^{(t)} \\
h^{(t)} &= \tanh (a^{(t)})\\
o^{(t)} &= c + Vh^{(t)}\\
\hat y^{(t)} &= {\rm softmax}(o^{(t)})\\
\end{split} \end{equation}$$

刚刚开始看方程可能会有些困难，所以我们一步一步，利用图片来理解。

首先是 $a^{(t)} = b + Wh^{(t-1)} + Ux^{(t)}$，它在上面的大图中所在的过程如下图所示。

经过上图的运算我们得到了 $a^{(t)}$。注意，这里新增了偏置 $b$ 等细节，比之前的整体展开图更加详细。

接下来是 $h^{(t)} = \tanh (a^{(t)})$，过程如下：

仅仅是对 $a^{(t)}$ 应用了一个激活函数，非常简单的一步。

第三步是 $o^{(t)} = c + Vh^{(t)}$，过程如下：

其中 $c$ 也是偏置向量。到现在我们得到了输出 $o^{(t)}$。

至于第四步 $\hat y^{(t)} = {\rm softmax}(o^{(t)})$ 的过程，其实是在损失函数 $L$ 内部计算的，然后会与真实标签 $y^{(t)}$ 相比较，这里就略去不画了。

到这里我们就大致了解了循环神经网络的前向传播过程。

> 在这种 RNN 结构中，由于前向传播图是固有顺序的，每个时间步只能一前一后地计算，而且前向传播过程中各个状态 $h$ 必须保存，用以在反向传播中再次使用，所以其运行时间复杂度为 $O(\tau)$，而空间复杂度也是 $O(\tau)$。

> 关于循环神经网络的反向传播推导可参考《深度学习》第10.2.2 节 “计算循环神经网络的梯度”。

## 运用实例说明RNN

## 循环神经网络的编程实现

```python

```

```python

```


#  Neuromatch Academy: Week 1, Day 1, Tutorial 2
# Model Types: "How" models
__Content creators:__ Matt Laporte, Byron Galbraith, Konrad Kording

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

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

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

```python
#@title Video 1: "How" models
from IPython.display import IFrame
class BiliVideo(IFrame):
    def __init__(self, id, page=1, width=400, height=300, **kwargs):
        self.id=id
        src = "//player.bilibili.com/player.html?bvid={0}&page={1}".format(id, page)
        super(BiliVideo, self).__init__(src, width, height, **kwargs)

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

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

# Setup

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

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

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

```

```python
#@title Helper Functions
def histogram(counts, bins, vlines=(), ax=None, ax_args=None, **kwargs):
  """Plot a step histogram given counts over bins."""
  if ax is None:
    _, ax = plt.subplots()

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

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

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

  if ax_args is None:
    ax_args = {}

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

  if ymax == ymin:
    ymax = None

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

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

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

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

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

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

How does a neuron spike?

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

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

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

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

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

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

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

#### Spiking Inputs

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

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

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

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

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

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

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

### Exercise 1: Compute $dV_m$

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

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

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

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

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

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

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

  for i in range(1, n_steps):

    dv = ...

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

  return v, spike_times

# Uncomment these lines after completing the lif_neuron function
# v, spike_times = lif_neuron()
# plot_neuron_stats(v, spike_times)
```

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

*Example output:*

## Interactive Demo: Linear-IF neuron
Like last time, you can now explore how various parametes of the LIF model influence the ISI distribution.

```python
#@title

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

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

@widgets.interact(
  n_steps=widgets.FloatLogSlider(1000.0, min=2, max=4),
  alpha=widgets.FloatLogSlider(0.01, min=-2, max=-1),
  rate=widgets.IntSlider(10, min=5, max=20)
)
def plot_lif_neuron(n_steps=1000, alpha=0.01, rate=10):
  v, spike_times = _lif_neuron(int(n_steps), alpha, rate)
  plot_neuron_stats(v, spike_times)
```

```python
#@title Video 2: Linear-IF models
from IPython.display import IFrame
class BiliVideo(IFrame):
    def __init__(self, id, page=1, width=400, height=300, **kwargs):
        self.id=id
        src = "//player.bilibili.com/player.html?bvid={0}&page={1}".format(id, page)
        super(BiliVideo, self).__init__(src, width, height, **kwargs)

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

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

# Section 2: Inhibitory signals

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

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

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

where $V_m$ is the current membrane potential and $\beta$ is some leakage factor. This is a basic form of the popular Leaky Integrate-and-Fire model neuron (for a more detailed discussion of the LIF Neuron, see the Appendix).

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

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

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

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

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

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

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

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

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

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

  for i in range(1, n_steps):

    dv = ...

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

  return v, spike_times

# Uncomment these lines do make the plot once you've completed the function
#v, spike_times = lif_neuron_inh()
#plot_neuron_stats(v, spike_times)
```

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

*Example output:*

## Interactive Demo: LIF + inhibition neuron

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

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

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

  return v, spike_times

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

```python
#@title Video 3: LIF + inhibition
from IPython.display import IFrame
class BiliVideo(IFrame):
    def __init__(self, id, page=1, width=400, height=300, **kwargs):
        self.id=id
        src = "//player.bilibili.com/player.html?bvid={0}&page={1}".format(id, page)
        super(BiliVideo, self).__init__(src, width, height, **kwargs)

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

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

#Summary

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

# Bonus

### Why do neurons spike?

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

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

### The LIF Model Neuron

The full equation for the LIF neuron is

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

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

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


# Three level Bloch equations solver

Progressive regime

This notebook presents and explains a three level Bloch equation solver. It works with standard python packages including Numpy and SciPy. The theory is summerized quickly in the following. The unperturbed Hamiltonian describing a three level systems is given
by:

\begin{equation}
\hat{\mathcal{H}}_{0}=\begin{pmatrix}E_{a} & 0 & 0\\
0 & E_{b} & 0\\
0 & 0 & E_{c}
\end{pmatrix}
\end{equation}

with $E_{c}>E_{b}>E_{a}$. If all transitions of a three level system
are dipole allowed, three different combinations of driving two transitions
are possible: the progressive regime ($\left|a\right\rangle \rightarrow\left|b\right\rangle $
and $\left|b\right\rangle \rightarrow\left|c\right\rangle $, Figure), the upper regressive regime
($\left|a\right\rangle \rightarrow\left|c\right\rangle $ and $\left|b\right\rangle \rightarrow\left|c\right\rangle $,
Figure) and the lower regressive
regime ($\left|a\right\rangle \rightarrow\left|c\right\rangle $ and
$\left|a\right\rangle \rightarrow\left|b\right\rangle $, Figure).
For the corresponding Hamiltonians including the interactions with
the electromagnetic fields follows in the progressive regime $\hat{\mathcal{H}}_{\text{pro}}$:

\begin{equation}
{\scriptstyle \hat{\mathcal{H}}_{\text{pro}}=\begin{pmatrix}E_{a} & -\frac{1}{2}\mu_{ab}\mathcal{E}_{ab}\left(e^{i\omega_{\text{ab}}t}+e^{-i\omega_{\text{ab}}t}\right) & 0\\
-\frac{1}{2}\mu_{ba}\mathcal{E}_{ba}\left(e^{i\omega_{\text{ab}}t}+e^{-i\omega_{\text{ab}}t}\right) & E_{b} & -\frac{1}{2}\mu_{bc}\mathcal{E}_{bc}\left(e^{i\omega_{\text{bc}}t}+e^{-i\omega_{\text{bc}}t}\right)\\
0 & -\frac{1}{2}\mu_{cb}\mathcal{E}_{cb}\left(e^{i\omega_{\text{bc}}t}+e^{-i\omega_{\text{bc}}t}\right) & E_{c}
\end{pmatrix}}
\end{equation}
in the upper regressive regime $\hat{\mathcal{H}}_{\text{reg,up}}$:
\begin{equation}
{\scriptstyle \hat{\mathcal{H}}_{\text{reg,up}}=\begin{pmatrix}E_{a} & 0 & -\frac{1}{2}\mu_{ac}\mathcal{E}_{ac}\left(e^{i\omega_{\text{ac}}t}+e^{-i\omega_{\text{ac}}t}\right)\\
0 & E_{b} & -\frac{1}{2}\mu_{bc}\mathcal{E}_{bc}\left(e^{i\omega_{\text{bc}}t}+e^{-i\omega_{\text{bc}}t}\right)\\
-\frac{1}{2}\mu_{ca}\mathcal{E}_{ca}\left(e^{i\omega_{\text{ac}}t}+e^{-i\omega_{\text{ac}}t}\right) & -\frac{1}{2}\mu_{cb}\mathcal{E}_{cb}\left(e^{i\omega_{\text{bc}}t}+e^{-i\omega_{\text{bc}}t}\right) & E_{c}
\end{pmatrix}}
\end{equation}
and in the lower regressive regime $\hat{\mathcal{H}}_{\text{reg,low}}$:
\begin{equation}
{\scriptstyle \hat{\mathcal{H}}_{\text{reg,low}}=\begin{pmatrix}E_{a} & -\frac{1}{2}\mu_{ab}\mathcal{E}_{ab}\left(e^{i\omega_{\text{ab}}t}+e^{-i\omega_{\text{ab}}t}\right) & -\frac{1}{2}\mu_{ac}\mathcal{E}_{ac}\left(e^{i\omega_{\text{ac}}t}+e^{-i\omega_{\text{ac}}t}\right)\\
-\frac{1}{2}\mu_{ba}\mathcal{E}_{ba}\left(e^{i\omega_{\text{ab}}t}+e^{-i\omega_{\text{ab}}t}\right) & E_{b} & 0\\
-\frac{1}{2}\mu_{ca}\mathcal{E}_{ca}\left(e^{i\omega_{\text{ac}}t}+e^{-i\omega_{\text{ac}}t}\right) & 0 & E_{c}
\end{pmatrix}}
\end{equation}
A few approximations have been applied to reduce the complexity. First
of all the assumption was made that the interaction volume is small
compared to the wavelength by neglecting the dependence of the electromagnetic
field on the wave vector $\mathbf{k}$. Also the initial phases of
both incoming waves were set to zero. Furthermore, the assumption
is made that the incoming fields $\mathbf{F}_{ab}$, $\mathbf{F}_{bc}$,
$\mathbf{F}_{ac}$ are only coupling to the dipole moment component
$\mu_{ab}$, $\mu_{bc}$, $\mu_{ac}$, respectively. This assumption
is valid for this work, because the transition frequencies are well
separated and the polarization direction of the two incoming waves
is perpendicular to each other in the experimental setup.

The time dependence of the density matrix elements can be obtained
by solving the Liouville equation:

\begin{equation}
i\hbar\frac{\partial\hat{\rho}\left(t\right)}{\partial t}=\left[\hat{\mathcal{H}}\left(t\right),\,\hat{\rho}\left(t\right)\right]
\end{equation}

The resulting ordinary differential equations (ODE) for the three
different regimes are compiled in Appendix ??. The simulation results
in this chapter were obtained using the ODE solver 'zvode' of the
Python package SciPy (citation). The expectation value of the polarization
is then given by:
\begin{equation}
\left\langle \hat{P}\right\rangle =N\cdot Tr\left(\hat{\rho}\hat{\mu}\right)
\end{equation}
with:
\begin{equation}
\hat{\mu}=-\begin{pmatrix}0 & \mu_{ab} & \mu_{ac}\\
\mu_{ba} & 0 & \mu_{bc}\\
\mu_{ca} & \mu_{cb} & 0
\end{pmatrix}
\end{equation}

## Load some modules

```python
# Load some modules
import scipy.constants as con
from scipy.integrate import ode
from scipy import optimize
import copy as cp
from matplotlib.patches import Rectangle
from tempfile import TemporaryFile
import time
import matplotlib as mpl
import numpy as np
import matplotlib.pyplot as plt

%matplotlib inline

params = {'axes.labelsize': 14,
          'legend.fontsize': 14,
          'xtick.labelsize': 14,
          'ytick.labelsize': 14,
          'text.usetex': True,
          'font.size': 14,
          'font.family': 'serif',
          'font.serif': 'serif',
          'figure.figsize': [16,10]}
mpl.rcParams.update(params)
```

## Define some functions

```python
# This function returns a sin
def fitfunc(t,A, omega, phi):

    return A*sin((t)*omega+phi)

'''
This function returns the phase of the cohererences compared to a single frequency om_0 (Hz) starting at t0.
dt is the timestep
'''
def phase(t, dt, om_0, rho):

    om_0 *= (2*np.pi*1.E6)
    signal = (rho.real)[-100:]
    ref = ((np.exp(-1j*(om_0*t-np.pi/2))).real*.1)[-100:]
    time = t[-100:]

    h=1.
    for i in np.arange(0,100):
        if np.sign(signal[i]*h) < 0.:
            g2 = np.abs(signal[i])/(np.abs(h) + np.abs(signal[i]))
            if h < signal[i]:
                ts = time[i]-g2*dt
        h = signal[i]

    h=1.
    for i in np.arange(0,100):
        if np.sign(ref[i]*h) < 0.:
            g2 = np.abs(ref[i])/(np.abs(h) + np.abs(ref[i]))
            if h < ref[i]:
                tr = time[i]-g2*dt
        h = ref[i]

    return (tr-ts)*om_0
```

```python
# Setting up the differential equations by solving the commutator
def f_3(t, y, arg):

    # Make it human readable
    E_a = arg[0]; E_b = arg[1]; E_c = arg[2]
    mu_ab = arg[3]; mu_bc = arg[4]; mu_ac = arg[5]
    F_ab = arg[6]; F_bc = arg[7]; F_ac = arg[8]
    o_ab = arg[9]; o_bc = arg[10]; o_ac = arg[11]
    phi_ab = arg[12]; phi_bc = arg[13]; phi_ac = arg[14]

    # Set up Hamiltonian
    H = np.zeros((3,3), dtype=complex)
    H[0,0] = E_a; H[1,1] = E_b; H[2,2] = E_c;

    H[0,1] = - mu_ab * F_ab / 2. * (np.exp(-1.j*(o_ab*t+phi_ab)) + np.exp(1.j*(o_ab*t+phi_ab))); H[1,0] = np.conjugate(H[0,1])
    H[1,2] = - mu_bc * F_bc / 2. * (np.exp(-1.j*(o_bc*t+phi_bc)) + np.exp(1.j*(o_bc*t+phi_bc))); H[2,1] = np.conjugate(H[1,2])
    H[0,2] = - mu_ac * F_ac / 2. * (np.exp(-1.j*(o_ac*t+phi_ac)) + np.exp(1.j*(o_ac*t+phi_ac))); H[2,0] = np.conjugate(H[0,2])

    # Set up density matrix
    rho = np.zeros((3,3), dtype=complex)
    rho[0,0] = y[0]; rho[0,1] = y[1]; rho[0,2] = y[2]
    rho[1,0] = y[3]; rho[1,1] = y[4]; rho[1,2] = y[5]
    rho[2,0] = y[6]; rho[2,1] = y[7]; rho[2,2] = y[8]

    # Calculate commutator
    dt_rho = -1.j/con.hbar * ( np.dot(H, rho) - np.dot(rho, H) )
    return [[dt_rho[0,0]], [dt_rho[0,1]], [dt_rho[0,2]],
            [dt_rho[1,0]], [dt_rho[1,1]], [dt_rho[1,2]],
            [dt_rho[2,0]], [dt_rho[2,1]], [dt_rho[2,2]]]

```

```python
# rho is the density matrix at every timestep
# t is the time vector
# This function solves the Bloch equations and carries the full output
def bloch_solver_3(par, dump=True, cut=False, Ndpoints=1000, Ncpoints=1000):

    para = cp.deepcopy(par)
    if dump == True:
        tmpfile_t = TemporaryFile()
        tmpfile_rho = TemporaryFile()
        cut = False

    def set_init(para, i):
        return [para['E_a'], para['E_b'], para['E_c'], para['mu_ab'], para['mu_bc'], para['mu_ac'],
                para['F_ab'][i], para['F_bc'][i], para['F_ac'][i], para['o_ab'][i], para['o_bc'][i], para['o_ac'][i],
                para['phi_ab'][i], para['phi_bc'][i], para['phi_ac'][i]]

    def save_dump(rho, t, tmpfile_t, tmpfile_rho):
        tmpfile_t.seek(0); tmpfile_rho.seek(0)
        t_h = np.load(tmpfile_t); rho_h = np.load(tmpfile_rho)
        tmpfile_t.seek(0); tmpfile_rho.seek(0)
        rho = np.vstack((rho_h, rho[:-1])); t = np.append(t_h, t[:-1])
        np.save(tmpfile_t, t); np.save(tmpfile_rho, rho)

    para['o_ab'] *= (2.*np.pi*1.E6); para['o_bc'] *= (2.*np.pi*1.E6); para['o_ac'] *= (2.*np.pi*1.E6)
    para['mu_ab'] *= 3.335E-30; para['mu_bc'] *= 3.335E-30; para['mu_ac'] *= 3.335E-30

    nb = len(para['tp'])
    r = ode(f_3).set_integrator('zvode')

    for i in np.arange(nb):

        # Set parameters and starting values at t0
        if i == 0:
            r.set_initial_value(para['rho0'], para['t0']).set_f_params(set_init(para, 0))
            t = np.array(para['t0'])
            rho = np.array([[[para['rho0'][0], para['rho0'][1], para['rho0'][2]],
                          [para['rho0'][3], para['rho0'][4], para['rho0'][5]],
                          [para['rho0'][6], para['rho0'][7], para['rho0'][8]]]])
            tpp = 0.

            if dump == True:
                np.save(tmpfile_rho, rho); np.save(tmpfile_t, t)

        # Set parameters for the next pulse
        else:
            r.set_initial_value(r.y, r.t).set_f_params(set_init(para, i))
            tpp += para['tp'][i-1]

        # Integrate and solve ODEs
        j = 0
        while r.successful() and r.t <= tpp + para['tp'][i]:
            r.integrate(r.t + para['dt'])

            if dump == True and j == Ndpoints:
                save_dump(rho, t, tmpfile_t, tmpfile_rho)
                rho = np.array([rho[-1]])
                t = np.array([t[-1]])
                j = 0

            if cut == False or r.t > np.sum(para['tp']) - Ncpoints*para['dt']:
                rho = np.vstack((rho, [[[r.y[0], r.y[1], r.y[2]],
                                     [r.y[3], r.y[4], r.y[5]],
                                     [r.y[6], r.y[7], r.y[8]]]]))
                t = np.append(t, r.t)

            j += 1

    if dump == True:
        save_dump(rho, t, tmpfile_t, tmpfile_rho)
        tmpfile_t.seek(0); tmpfile_rho.seek(0)
        rho = np.load(tmpfile_rho); rho = rho[1:]
        t = np.load(tmpfile_t); t = t[1:]
        tmpfile_rho.close(); tmpfile_t.close()

    if cut == False:
        return t, rho
    else:
        return t[1:], rho[1:]
```

## Here the interactive part starts
First you need to define the parameters:

```python
'''
The par dictionary defines the parameters of the experiment:
dt: timestep
t0: t zero (in seconds)
tp: list of the individual pulse lengths (in seconds)
mu_ij: transitions dipole moments (complex) for the transition |i> to |j> in Debye
E_i: eigen energies of the individual levels |i> (in Joule)
o_ij: radiation frequency (MHz) addressing the particular transition |i> to |j>
F_ij: radiation intensity (V/m) addressing the particular transition |i> to |j>
rho0: starting density matrix (1D array) with entries in the order rho_aa, rho_ab ... rho_ba ... rho_cc
'''

par = dict({ 'dt': 1.E-11, 'tp': np.array([2.60E-8, 2.40E-8, 3.5e-8]),'t0': 0.,
             'mu_ab': 2.+0.j, 'mu_bc': 4.+0.j, 'mu_ac':  1.+0.j,
             'E_a': 0., 'E_b': 2.46037e-24, 'E_c': 2.67492e-24,
             'o_ab': np.array([3713.17, 3713.17, 3713.17]), 'o_bc': np.array([323.8, 323.8, 323.8]), 'o_ac': np.array([4036.97, 4036.97, 4036.97]),
             'F_ab': np.array([1000., 0., 00.]), 'F_bc': np.array([0., 1000., 000.]), 'F_ac': np.array([0., 0., 0.]),
             'phi_ab': np.array([0.,0.,0.]), 'phi_bc': np.array([0.,0.,0.]), 'phi_ac': np.array([0.,0.,0.]),
             'rho0': np.array([.5 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j,
                          0.0 + 0.0j, 0.3 + 0.0j, 0.0 + 0.0j,
                          0.0 + 0.0j, 0.0 + 0.0j, 0.2 + 0.0j]),
             })
```

## Three wave mixing experiment

```python
#Solving the Bloch equations
t, rho = bloch_solver_3(par)

# Plotting
fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, sharex=True)
fig.set_size_inches(16,10)

ma = np.max([np.max(rho[:,0,1].real), np.max(rho[:,0,2].real), np.max(rho[:,1,2].real)])
mi = np.min([np.min(rho[:,0,1].real), np.min(rho[:,0,2].real), np.min(rho[:,1,2].real)])

ax1.set_title('Coherences', fontsize=18)
ax1.plot(t, rho[:,0,1].real, label=r'$\rho_{ab}$')
ax1.plot(t, rho[:,0,2].real, label=r'$\rho_{ac}$')
ax1.plot(t, rho[:,1,2].real, label=r'$\rho_{bc}$')

ax1.legend(loc='upper left', fontsize=18)
ax1.set_xlim(par['t0'], np.sum(par['tp'])); ax1.set_ylim((mi, ma));

ax2.set_title('Populations', fontsize=18)
ax2.plot(t, rho[:,0,0].real, label=r'$\rho_{aa}$')
ax2.plot(t, rho[:,1,1].real, label=r'$\rho_{bb}$')
ax2.plot(t, rho[:,2,2].real, label=r'$\rho_{cc}$')
ax2.legend(fontsize=18)
ax2.set_xlim(par['t0'], np.sum(par['tp']));

ax3.set_title('Pulse composition', fontsize=18)
for i in np.arange(len(par['tp'])):
    m = max([np.max(par['F_ab']), np.max(par['F_ac']), np.max(par['F_bc'])])
    if i == 0:
        x = 0.
    else:
        x += par['tp'][i-1]
    ax3.add_patch(Rectangle((x, 0.), par['tp'][i], par['F_ab'][i]/m, facecolor="blue"))
    ax3.add_patch(Rectangle((x, 4.), par['tp'][i], par['F_ac'][i]/m, facecolor="green"))
    ax3.add_patch(Rectangle((x, 2.), par['tp'][i], par['F_bc'][i]/m, facecolor="red"))

ax3.text(0, .5, r'$F_{ab}$', fontsize=18); ax3.text(0, 2.5, r'$F_{bc}$', fontsize=18); ax3.text(0, 4.5, r'$F_{ac}$', fontsize=18)
ax3.set_xlim(par['t0'], sum(par['tp'])); ax3.set_ylim(0., 6.); ax3.set_xlabel('time (s)')

ax4.set_title('Residuals', fontsize=18)
ax4.plot(t, (rho[:,2,2].real + rho[:,1,1].real + rho[:,0,0].real)-1.)
ax4.set_xlim(par['t0'], sum(par['tp'])); ax4.set_xlabel('time (s)');
```

## Lets have a look at the Polarizations

```python
fig, (ax1, ax2) = plt.subplots(1, 2)
fig.set_size_inches(16,5)

ma = np.max([max(rho[:,0,1].real), np.max(rho[:,0,2].real), np.max(rho[:,1,2].real)])
mi = np.min([min(rho[:,0,1].real), np.min(rho[:,0,2].real), np.min(rho[:,1,2].real)])

ax1.set_title('Coherences', fontsize=18)
ax1.plot(t, rho[:,0,1].real, label=r'$\rho_{ab}$')
ax1.plot(t, rho[:,0,2].real, label=r'$\rho_{ac}$')
ax1.plot(t, rho[:,1,2].real, label=r'$\rho_{bc}$')
ax1.legend(loc='upper left', fontsize=18)
ax1.set_xlim(par['t0'], np.sum(par['tp'])); ax1.set_ylim((mi, ma));

mi = min([min((rho[:,0,1]*np.conjugate(par['mu_ab'])).real),
          min((rho[:,0,2]*np.conjugate(par['mu_ac'])).real),
          min((rho[:,1,2]*np.conjugate(par['mu_bc'])).real)])

ma = max([max((rho[:,0,1]*np.conjugate(par['mu_ab'])).real),
          max((rho[:,0,2]*np.conjugate(par['mu_ac'])).real),
          max((rho[:,1,2]*np.conjugate(par['mu_bc'])).real)])

ax2.set_title('Polarization', fontsize=18)
ax2.plot(t, (rho[:,0,1]*np.conjugate(par['mu_ab'])).real, label=r'$P_{ab}$')
ax2.plot(t, (rho[:,0,2]*np.conjugate(par['mu_ac'])).real, label=r'$P_{ac}$')
ax2.plot(t, (rho[:,1,2]*np.conjugate(par['mu_bc'])).real, label=r'$P_{bc}$')
ax2.legend(loc='upper left', fontsize=18)
ax2.set_xlim(par['t0'], np.sum(par['tp'])); ax2.set_ylim((mi, ma));
```

## Double resonance experiment with a varying frequency
In this example the frequency o_bc is tuned through the resonance

```python
# Define the starting parameters
par = dict({ 'dt': 1.E-11, 'tp': np.array([2.50E-8, 2.5E-8, 1.0E-8]), 't0': 0.,
             'mu_ab': 1.+0.j, 'mu_bc': 1.+0.j, 'mu_ac':  1.+1.j,
             'E_a': 0., 'E_b': 2.460372e-24, 'E_c': 2.674924e-24,
             'o_ab': np.array([3713.17, 3713.17, 3713.17]), 'o_bc': np.array([323.8, 323.8, 323.8]), 'o_ac': np.array([4036.97, 4036.97, 4036.97]),
             'F_ab': np.array([2000., 0.,0.]), 'F_bc': np.array([0., 4000.,0.]), 'F_ac': np.array([0., 0.,0.]),
             'phi_ab': np.array([0.,0.,0.]), 'phi_bc': np.array([0.,0.,0.]), 'phi_ac': np.array([0.,0.,0.]),
             'rho0': np.array([.5 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j,
                          0.0 + 0.0j, 0.3 + 0.0j, 0.0 + 0.0j,
                          0.0 + 0.0j, 0.0 + 0.0j, 0.2 + 0.0j]),
             })

# Tune the o_bc frequency and store the phase and the intensity of the o_ab coherence
ph = np.array([])
lint = np.array([])
h = np.array(par['o_bc'])
tune = np.arange(-100,101,10)

for i in tune:
    par['o_bc'] = h + float(i)
    t, rho = bloch_solver_3(par, dump=False, cut=True)
    ph = np.append(ph, phase(t, par['dt'], par['o_ab'][0], rho[:,0,1]))
    lint = np.append(lint, max((rho[:,0,1].real)[-100:]))

# Here the plotting starts
x = par['o_bc'][0] + tune

for i in np.arange(len(ph)):
    if ph[i] > np.pi:
        ph[i] = -np.pi + np.abs(ph[i] - np.pi)
    if ph[i] < -np.pi:
        ph[i] = np.pi - np.abs(ph[i] + np.pi)

fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True)
fig.set_size_inches(8,10)

ax1.set_title('Phase', fontsize=18)
ax1.plot(x, ph, 'o:')
ax1.set_ylabel('phase (rad)', fontsize=18);
ax1.set_yticks([-np.pi, -np.pi/2, 0., np.pi/2, np.pi])
ax1.set_yticklabels([r'$-\pi$', r'$-\pi/2$', r'$0$', r'$\pi/2$', r'$\pi$'])

ax2.set_title('Intensity', fontsize=18)
ax2.plot(x, lint, 'o:')
ax2.set_xlabel('frequency (MHz)', fontsize=18);
ax2.set_ylabel('Intensity (arb. units)', fontsize=18);

ax1.grid(); ax2.grid();
```

```python
# Define the starting parameters
par = dict({ 'dt': 1.E-11, 'tp': np.array([2.50E-8, 2.5E-8, 1.0E-8]), 't0': 0.,
             'mu_ab': 1.+0.j, 'mu_bc': 1.+0.j, 'mu_ac':  1.+0.j,
             'E_a': 0., 'E_b': 2.460372e-24, 'E_c': 2.674924e-24,
             'o_ab': np.array([3713.17, 3713.17, 3713.17]), 'o_bc': np.array([323.8, 323.8, 323.8]), 'o_ac': np.array([4036.97, 4036.97, 4036.97]),
             'F_ab': np.array([0., 0.,0.]), 'F_bc': np.array([0., 4000.,0.]), 'F_ac': np.array([2000., 0.,0.]),
             'phi_ab': np.array([0.,0.,0.]), 'phi_bc': np.array([0.,0.,0.]), 'phi_ac': np.array([0.,0.,0.]),
             'rho0': np.array([.5 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j,
                          0.0 + 0.0j, 0.3 + 0.0j, 0.0 + 0.0j,
                          0.0 + 0.0j, 0.0 + 0.0j, 0.2 + 0.0j]),
             })

# Tune the o_bc frequency and store the phase and the intensity of the o_ac coherence
ph = np.array([])
lint = np.array([])
h = np.array(par['o_bc'])
tune = np.arange(-100,101,10)

for i in tune:
    par['o_bc'] = h + float(i)
    t, rho = bloch_solver_3(par, dump=False, cut=True)
    ph = np.append(ph, phase(t, par['dt'], par['o_ac'][0], rho[:,0,2]))
    lint = np.append(lint, max((rho[:,0,2].real)[-100:]))

# Here the plotting starts
x = par['o_bc'][0] + tune

for i in np.arange(len(ph)):
    if ph[i] > np.pi:
        ph[i] = -np.pi + abs(ph[i] - np.pi)
    if ph[i] < -np.pi:
        ph[i] = np.pi - abs(ph[i] + np.pi)

fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True)
fig.set_size_inches(8,10)

ax1.set_title('Phase', fontsize=18)
ax1.plot(x, ph, 'o:')
ax1.set_ylabel('phase (rad)', fontsize=18);
ax1.set_yticks([-np.pi, -np.pi/2, 0., np.pi/2, np.pi])
ax1.set_yticklabels([r'$-\pi$', r'$-\pi/2$', r'$0$', r'$\pi/2$', r'$\pi$'])

ax2.set_title('Intensity', fontsize=18)
ax2.plot(x, lint, 'o:')
ax2.set_xlabel('frequency (MHz)', fontsize=18);
ax2.set_ylabel('Intensity (arb. units)', fontsize=18);

ax1.grid(); ax2.grid();
```

```python
# Define the starting parameters
par = dict({ 'dt': 1.E-11, 'tp': np.array([2.50E-8, 2.5E-8, 1.0E-8]), 't0': 0.,
             'mu_ab': 1.+0.j, 'mu_bc': 1.+0.j, 'mu_ac':  1.+0.j,
             'E_a': 0., 'E_b': 2.460372e-24, 'E_c': 2.674924e-24,
             'o_ab': np.array([3713.17, 3713.17, 3713.17]), 'o_bc': np.array([323.8, 323.8, 323.8]), 'o_ac': np.array([4036.97, 4036.97, 4036.97]),
             'F_ab': np.array([0., 4000.,0.]), 'F_bc': np.array([0., 000.,0.]), 'F_ac': np.array([2000., 0.,0.]),
             'phi_ab': np.array([0.,0.,0.]), 'phi_bc': np.array([0.,0.,0.]), 'phi_ac': np.array([0.,0.,0.]),
             'rho0': np.array([.5 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j,
                          0.0 + 0.0j, 0.3 + 0.0j, 0.0 + 0.0j,
                          0.0 + 0.0j, 0.0 + 0.0j, 0.2 + 0.0j]),
             })

# Tune the o_bc frequency and store the phase and the intensity of the o_ac coherence
ph = np.array([])
lint = np.array([])
h = np.array(par['o_ab'])
tune = np.arange(-100,101,10)

for i in tune:
    par['o_ab'] = h + float(i)
    t, rho = bloch_solver_3(par, dump=False, cut=True)
    ph = np.append(ph, phase(t, par['dt'], par['o_ac'][0], rho[:,0,2]))
    lint = np.append(lint, max((rho[:,0,2].real)[-100:]))

# Here the plotting starts
x = par['o_ab'][0] + tune

for i in np.arange(len(ph)):
    if ph[i] > np.pi:
        ph[i] = -np.pi + np.abs(ph[i] - np.pi)
    if ph[i] < -np.pi:
        ph[i] = np.pi - np.abs(ph[i] + np.pi)

fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True)
fig.set_size_inches(8,10)

ax1.set_title('Phase', fontsize=18)
ax1.plot(x, ph, 'o:')
ax1.set_ylabel('phase (rad)', fontsize=18);
ax1.set_yticks([-np.pi, -np.pi/2, 0., np.pi/2, np.pi])
ax1.set_yticklabels([r'$-\pi$', r'$-\pi/2$', r'$0$', r'$\pi/2$', r'$\pi$'])

ax2.set_title('Intensity', fontsize=18)
ax2.plot(x, lint, 'o:')
ax2.set_xlabel('frequency (MHz)', fontsize=18);
ax2.set_ylabel('Intensity (arb. units)', fontsize=18);

ax1.grid(); ax2.grid();
```

## Performance check

```python
# Define the starting parameters
par = dict({ 'dt': 1.E-11, 'tp': np.array([2.50E-8, 20.0E-8, 1.E-8]), 't0': 0.,
             'mu_ab': 2.+0.j, 'mu_bc': 1.+0.j, 'mu_ac':  1.+1.j,
             'E_a': 0., 'E_b': 2.460372e-24, 'E_c': 2.674924e-24,
             'o_ab': np.array([3713.17, 3713.17, 3713.17]), 'o_bc': np.array([323.8, 323.8, 323.8]), 'o_ac': np.array([4036.97, 4036.97, 4036.97]),
             'F_ab': np.array([1000., 0.,0]), 'F_bc': np.array([0., 50.,0]), 'F_ac': np.array([0., 0.,0]),
             'phi_ab': np.array([0.,0.,0.]), 'phi_bc': np.array([0.,0.,0.]), 'phi_ac': np.array([0.,0.,0.]),
             'rho0': np.array([.5 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j,
                          0.0 + 0.0j, 0.3 + 0.0j, 0.0 + 0.0j,
                          0.0 + 0.0j, 0.0 + 0.0j, 0.2 + 0.0j]),
             })

start = time.time()
t, rho = bloch_solver_3(par, dump=True)
print time.time() - start

start = time.time()
t, rho = bloch_solver_3(par, dump=False, cut=True)
print time.time() - start

start = time.time()
t, rho = bloch_solver_3(par, dump=False)
print time.time() - start
```

    6.00986504555
    5.00609612465
    22.6084740162

## Autler-Towns splitting

```python
# Define the starting parameters
par = dict({ 'dt': 1.E-11, 'tp': np.array([2.50E-8, 500E-8, 500E-8]), 't0': 0.,
             'mu_ab': 2.+0.j, 'mu_bc': 1.+0.j, 'mu_ac':  1.+1.j,
             'E_a': 0., 'E_b': 2.460372e-24, 'E_c': 2.674924e-24,
             'o_ab': np.array([3713.17, 3713.17, 3713.17]), 'o_bc': np.array([323.8, 323.7, 323.8]), 'o_ac': np.array([4036.97, 4036.97, 4036.97]),
             'F_ab': np.array([1000., 0.,0]), 'F_bc': np.array([0., 100.,0]), 'F_ac': np.array([0., 0.,0]),
             'phi_ab': np.array([0.,0.,0.]), 'phi_bc': np.array([0.,0.,0.]), 'phi_ac': np.array([0.,0.,0.]),
             'rho0': np.array([.5 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j,
                          0.0 + 0.0j, 0.3 + 0.0j, 0.0 + 0.0j,
                          0.0 + 0.0j, 0.0 + 0.0j, 0.2 + 0.0j]),
             })

t, rho = bloch_solver_3(par, dump=True)

fig, ax = plt.subplots(1, 1)
fig.set_size_inches(10,6)

freq_fft = np.fft.fftfreq(len(t[-1000000:-500000]), par['dt'])
fft_fft = np.fft.fft(rho[-1000000:-500000,0,1].real)/len(t)*2.
ax.plot(freq_fft/1E6, np.abs(fft_fft), label='driven')

freq_fft = np.fft.fftfreq(len(t[-500000:]), par['dt'])
fft_fft = np.fft.fft(rho[-500000:,0,1].real*2)/len(t)*2.
ax.plot(freq_fft/1E6, abs(fft_fft), label='FID')
ax.legend()
ax.set_xlim(3710.,3715); ax.set_xlabel('freq'); ax.set_ylabel('intensity'); ax.set_title('FFT')
```

```python

```

notebooks/Bloch Gleichung Three Level Tutorial.ipynb